Vendor temporarily
This commit is contained in:
1 parent
dd8e532d18
commit
1c02ed9115
4708 files changed
+1226392
-12
No files matched your search
+19
@@ -0,0 +1,19 @@
|
||||
Copyright (c) 2015 Datadog, Inc
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
## Overview
|
||||
|
||||
Package `statsd` provides a Go [dogstatsd](http://docs.datadoghq.com/guides/dogstatsd/) client. Dogstatsd extends Statsd, adding tags
|
||||
and histograms.
|
||||
+178
@@ -0,0 +1,178 @@
|
||||
package statsd
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
type (
|
||||
countsMap map[string]*countMetric
|
||||
gaugesMap map[string]*gaugeMetric
|
||||
setsMap map[string]*setMetric
|
||||
)
|
||||
|
||||
type aggregator struct {
|
||||
nbContext int32
|
||||
|
||||
countsM sync.RWMutex
|
||||
gaugesM sync.RWMutex
|
||||
setsM sync.RWMutex
|
||||
|
||||
gauges gaugesMap
|
||||
counts countsMap
|
||||
sets setsMap
|
||||
|
||||
closed chan struct{}
|
||||
exited chan struct{}
|
||||
|
||||
client *Client
|
||||
}
|
||||
|
||||
type aggregatorMetrics struct {
|
||||
nbContext int32
|
||||
}
|
||||
|
||||
func newAggregator(c *Client) *aggregator {
|
||||
return &aggregator{
|
||||
client: c,
|
||||
counts: countsMap{},
|
||||
gauges: gaugesMap{},
|
||||
sets: setsMap{},
|
||||
closed: make(chan struct{}),
|
||||
exited: make(chan struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
func (a *aggregator) start(flushInterval time.Duration) {
|
||||
ticker := time.NewTicker(flushInterval)
|
||||
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
a.sendMetrics()
|
||||
case <-a.closed:
|
||||
close(a.exited)
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (a *aggregator) sendMetrics() {
|
||||
for _, m := range a.flushMetrics() {
|
||||
a.client.send(m)
|
||||
}
|
||||
}
|
||||
|
||||
func (a *aggregator) stop() {
|
||||
close(a.closed)
|
||||
<-a.exited
|
||||
a.sendMetrics()
|
||||
}
|
||||
|
||||
func (a *aggregator) flushTelemetryMetrics() *aggregatorMetrics {
|
||||
if a == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &aggregatorMetrics{
|
||||
nbContext: a.nbContext,
|
||||
}
|
||||
}
|
||||
|
||||
func (a *aggregator) flushMetrics() []metric {
|
||||
metrics := []metric{}
|
||||
|
||||
// We reset the values to avoid sending 'zero' values for metrics not
|
||||
// sampled during this flush interval
|
||||
|
||||
a.setsM.Lock()
|
||||
sets := a.sets
|
||||
a.sets = setsMap{}
|
||||
a.setsM.Unlock()
|
||||
|
||||
atomic.StoreInt32(&a.nbContext, int32(len(sets)))
|
||||
|
||||
for _, s := range sets {
|
||||
metrics = append(metrics, s.flushUnsafe()...)
|
||||
}
|
||||
|
||||
a.gaugesM.Lock()
|
||||
gauges := a.gauges
|
||||
a.gauges = gaugesMap{}
|
||||
a.gaugesM.Unlock()
|
||||
|
||||
atomic.AddInt32(&a.nbContext, int32(len(gauges)))
|
||||
for _, g := range gauges {
|
||||
metrics = append(metrics, g.flushUnsafe())
|
||||
}
|
||||
|
||||
a.countsM.Lock()
|
||||
counts := a.counts
|
||||
a.counts = countsMap{}
|
||||
a.countsM.Unlock()
|
||||
|
||||
atomic.AddInt32(&a.nbContext, int32(len(counts)))
|
||||
for _, c := range counts {
|
||||
metrics = append(metrics, c.flushUnsafe())
|
||||
}
|
||||
|
||||
return metrics
|
||||
}
|
||||
|
||||
func getContext(name string, tags []string) string {
|
||||
return name + ":" + strings.Join(tags, ",")
|
||||
}
|
||||
|
||||
func (a *aggregator) count(name string, value int64, tags []string, rate float64) error {
|
||||
context := getContext(name, tags)
|
||||
a.countsM.RLock()
|
||||
if count, found := a.counts[context]; found {
|
||||
count.sample(value)
|
||||
a.countsM.RUnlock()
|
||||
return nil
|
||||
}
|
||||
a.countsM.RUnlock()
|
||||
|
||||
a.countsM.Lock()
|
||||
a.counts[context] = newCountMetric(name, value, tags, rate)
|
||||
a.countsM.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *aggregator) gauge(name string, value float64, tags []string, rate float64) error {
|
||||
context := getContext(name, tags)
|
||||
a.gaugesM.RLock()
|
||||
if gauge, found := a.gauges[context]; found {
|
||||
gauge.sample(value)
|
||||
a.gaugesM.RUnlock()
|
||||
return nil
|
||||
}
|
||||
a.gaugesM.RUnlock()
|
||||
|
||||
gauge := newGaugeMetric(name, value, tags, rate)
|
||||
|
||||
a.gaugesM.Lock()
|
||||
a.gauges[context] = gauge
|
||||
a.gaugesM.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *aggregator) set(name string, value string, tags []string, rate float64) error {
|
||||
context := getContext(name, tags)
|
||||
a.setsM.RLock()
|
||||
if set, found := a.sets[context]; found {
|
||||
set.sample(value)
|
||||
a.setsM.RUnlock()
|
||||
return nil
|
||||
}
|
||||
a.setsM.RUnlock()
|
||||
|
||||
a.setsM.Lock()
|
||||
a.sets[context] = newSetMetric(name, value, tags, rate)
|
||||
a.setsM.Unlock()
|
||||
return nil
|
||||
}
|
||||
+130
@@ -0,0 +1,130 @@
|
||||
package statsd
|
||||
|
||||
type bufferFullError string
|
||||
|
||||
func (e bufferFullError) Error() string { return string(e) }
|
||||
|
||||
const errBufferFull = bufferFullError("statsd buffer is full")
|
||||
|
||||
const metricOverhead = 512
|
||||
|
||||
// statsdBuffer is a buffer containing statsd messages
|
||||
// this struct methods are NOT safe for concurent use
|
||||
type statsdBuffer struct {
|
||||
buffer []byte
|
||||
maxSize int
|
||||
maxElements int
|
||||
elementCount int
|
||||
}
|
||||
|
||||
func newStatsdBuffer(maxSize, maxElements int) *statsdBuffer {
|
||||
return &statsdBuffer{
|
||||
buffer: make([]byte, 0, maxSize+metricOverhead), // pre-allocate the needed size + metricOverhead to avoid having Go re-allocate on it's own if an element does not fit
|
||||
maxSize: maxSize,
|
||||
maxElements: maxElements,
|
||||
}
|
||||
}
|
||||
|
||||
func (b *statsdBuffer) writeGauge(namespace string, globalTags []string, name string, value float64, tags []string, rate float64) error {
|
||||
if b.elementCount >= b.maxElements {
|
||||
return errBufferFull
|
||||
}
|
||||
originalBuffer := b.buffer
|
||||
b.writeSeparator()
|
||||
b.buffer = appendGauge(b.buffer, namespace, globalTags, name, value, tags, rate)
|
||||
return b.validateNewElement(originalBuffer)
|
||||
}
|
||||
|
||||
func (b *statsdBuffer) writeCount(namespace string, globalTags []string, name string, value int64, tags []string, rate float64) error {
|
||||
if b.elementCount >= b.maxElements {
|
||||
return errBufferFull
|
||||
}
|
||||
originalBuffer := b.buffer
|
||||
b.writeSeparator()
|
||||
b.buffer = appendCount(b.buffer, namespace, globalTags, name, value, tags, rate)
|
||||
return b.validateNewElement(originalBuffer)
|
||||
}
|
||||
|
||||
func (b *statsdBuffer) writeHistogram(namespace string, globalTags []string, name string, value float64, tags []string, rate float64) error {
|
||||
if b.elementCount >= b.maxElements {
|
||||
return errBufferFull
|
||||
}
|
||||
originalBuffer := b.buffer
|
||||
b.writeSeparator()
|
||||
b.buffer = appendHistogram(b.buffer, namespace, globalTags, name, value, tags, rate)
|
||||
return b.validateNewElement(originalBuffer)
|
||||
}
|
||||
|
||||
func (b *statsdBuffer) writeDistribution(namespace string, globalTags []string, name string, value float64, tags []string, rate float64) error {
|
||||
if b.elementCount >= b.maxElements {
|
||||
return errBufferFull
|
||||
}
|
||||
originalBuffer := b.buffer
|
||||
b.writeSeparator()
|
||||
b.buffer = appendDistribution(b.buffer, namespace, globalTags, name, value, tags, rate)
|
||||
return b.validateNewElement(originalBuffer)
|
||||
}
|
||||
|
||||
func (b *statsdBuffer) writeSet(namespace string, globalTags []string, name string, value string, tags []string, rate float64) error {
|
||||
if b.elementCount >= b.maxElements {
|
||||
return errBufferFull
|
||||
}
|
||||
originalBuffer := b.buffer
|
||||
b.writeSeparator()
|
||||
b.buffer = appendSet(b.buffer, namespace, globalTags, name, value, tags, rate)
|
||||
return b.validateNewElement(originalBuffer)
|
||||
}
|
||||
|
||||
func (b *statsdBuffer) writeTiming(namespace string, globalTags []string, name string, value float64, tags []string, rate float64) error {
|
||||
if b.elementCount >= b.maxElements {
|
||||
return errBufferFull
|
||||
}
|
||||
originalBuffer := b.buffer
|
||||
b.writeSeparator()
|
||||
b.buffer = appendTiming(b.buffer, namespace, globalTags, name, value, tags, rate)
|
||||
return b.validateNewElement(originalBuffer)
|
||||
}
|
||||
|
||||
func (b *statsdBuffer) writeEvent(event Event, globalTags []string) error {
|
||||
if b.elementCount >= b.maxElements {
|
||||
return errBufferFull
|
||||
}
|
||||
originalBuffer := b.buffer
|
||||
b.writeSeparator()
|
||||
b.buffer = appendEvent(b.buffer, event, globalTags)
|
||||
return b.validateNewElement(originalBuffer)
|
||||
}
|
||||
|
||||
func (b *statsdBuffer) writeServiceCheck(serviceCheck ServiceCheck, globalTags []string) error {
|
||||
if b.elementCount >= b.maxElements {
|
||||
return errBufferFull
|
||||
}
|
||||
originalBuffer := b.buffer
|
||||
b.writeSeparator()
|
||||
b.buffer = appendServiceCheck(b.buffer, serviceCheck, globalTags)
|
||||
return b.validateNewElement(originalBuffer)
|
||||
}
|
||||
|
||||
func (b *statsdBuffer) validateNewElement(originalBuffer []byte) error {
|
||||
if len(b.buffer) > b.maxSize {
|
||||
b.buffer = originalBuffer
|
||||
return errBufferFull
|
||||
}
|
||||
b.elementCount++
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *statsdBuffer) writeSeparator() {
|
||||
if b.elementCount != 0 {
|
||||
b.buffer = appendSeparator(b.buffer)
|
||||
}
|
||||
}
|
||||
|
||||
func (b *statsdBuffer) reset() {
|
||||
b.buffer = b.buffer[:0]
|
||||
b.elementCount = 0
|
||||
}
|
||||
|
||||
func (b *statsdBuffer) bytes() []byte {
|
||||
return b.buffer
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
package statsd
|
||||
|
||||
type bufferPool struct {
|
||||
pool chan *statsdBuffer
|
||||
bufferMaxSize int
|
||||
bufferMaxElements int
|
||||
}
|
||||
|
||||
func newBufferPool(poolSize, bufferMaxSize, bufferMaxElements int) *bufferPool {
|
||||
p := &bufferPool{
|
||||
pool: make(chan *statsdBuffer, poolSize),
|
||||
bufferMaxSize: bufferMaxSize,
|
||||
bufferMaxElements: bufferMaxElements,
|
||||
}
|
||||
for i := 0; i < poolSize; i++ {
|
||||
p.addNewBuffer()
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
func (p *bufferPool) addNewBuffer() {
|
||||
p.pool <- newStatsdBuffer(p.bufferMaxSize, p.bufferMaxElements)
|
||||
}
|
||||
|
||||
func (p *bufferPool) borrowBuffer() *statsdBuffer {
|
||||
select {
|
||||
case b := <-p.pool:
|
||||
return b
|
||||
default:
|
||||
return newStatsdBuffer(p.bufferMaxSize, p.bufferMaxElements)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *bufferPool) returnBuffer(buffer *statsdBuffer) {
|
||||
buffer.reset()
|
||||
select {
|
||||
case p.pool <- buffer:
|
||||
default:
|
||||
}
|
||||
}
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
package statsd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Events support
|
||||
// EventAlertType and EventAlertPriority became exported types after this issue was submitted: https://github.com/DataDog/datadog-go/issues/41
|
||||
// The reason why they got exported is so that client code can directly use the types.
|
||||
|
||||
// EventAlertType is the alert type for events
|
||||
type EventAlertType string
|
||||
|
||||
const (
|
||||
// Info is the "info" AlertType for events
|
||||
Info EventAlertType = "info"
|
||||
// Error is the "error" AlertType for events
|
||||
Error EventAlertType = "error"
|
||||
// Warning is the "warning" AlertType for events
|
||||
Warning EventAlertType = "warning"
|
||||
// Success is the "success" AlertType for events
|
||||
Success EventAlertType = "success"
|
||||
)
|
||||
|
||||
// EventPriority is the event priority for events
|
||||
type EventPriority string
|
||||
|
||||
const (
|
||||
// Normal is the "normal" Priority for events
|
||||
Normal EventPriority = "normal"
|
||||
// Low is the "low" Priority for events
|
||||
Low EventPriority = "low"
|
||||
)
|
||||
|
||||
// An Event is an object that can be posted to your DataDog event stream.
|
||||
type Event struct {
|
||||
// Title of the event. Required.
|
||||
Title string
|
||||
// Text is the description of the event. Required.
|
||||
Text string
|
||||
// Timestamp is a timestamp for the event. If not provided, the dogstatsd
|
||||
// server will set this to the current time.
|
||||
Timestamp time.Time
|
||||
// Hostname for the event.
|
||||
Hostname string
|
||||
// AggregationKey groups this event with others of the same key.
|
||||
AggregationKey string
|
||||
// Priority of the event. Can be statsd.Low or statsd.Normal.
|
||||
Priority EventPriority
|
||||
// SourceTypeName is a source type for the event.
|
||||
SourceTypeName string
|
||||
// AlertType can be statsd.Info, statsd.Error, statsd.Warning, or statsd.Success.
|
||||
// If absent, the default value applied by the dogstatsd server is Info.
|
||||
AlertType EventAlertType
|
||||
// Tags for the event.
|
||||
Tags []string
|
||||
}
|
||||
|
||||
// NewEvent creates a new event with the given title and text. Error checking
|
||||
// against these values is done at send-time, or upon running e.Check.
|
||||
func NewEvent(title, text string) *Event {
|
||||
return &Event{
|
||||
Title: title,
|
||||
Text: text,
|
||||
}
|
||||
}
|
||||
|
||||
// Check verifies that an event is valid.
|
||||
func (e Event) Check() error {
|
||||
if len(e.Title) == 0 {
|
||||
return fmt.Errorf("statsd.Event title is required")
|
||||
}
|
||||
if len(e.Text) == 0 {
|
||||
return fmt.Errorf("statsd.Event text is required")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Encode returns the dogstatsd wire protocol representation for an event.
|
||||
// Tags may be passed which will be added to the encoded output but not to
|
||||
// the Event's list of tags, eg. for default tags.
|
||||
func (e Event) Encode(tags ...string) (string, error) {
|
||||
err := e.Check()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
var buffer []byte
|
||||
buffer = appendEvent(buffer, e, tags)
|
||||
return string(buffer), nil
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
package statsd
|
||||
|
||||
const (
|
||||
// FNV-1a
|
||||
offset32 = uint32(2166136261)
|
||||
prime32 = uint32(16777619)
|
||||
|
||||
// init32 is what 32 bits hash values should be initialized with.
|
||||
init32 = offset32
|
||||
)
|
||||
|
||||
// HashString32 returns the hash of s.
|
||||
func hashString32(s string) uint32 {
|
||||
return addString32(init32, s)
|
||||
}
|
||||
|
||||
// AddString32 adds the hash of s to the precomputed hash value h.
|
||||
func addString32(h uint32, s string) uint32 {
|
||||
i := 0
|
||||
n := (len(s) / 8) * 8
|
||||
|
||||
for i != n {
|
||||
h = (h ^ uint32(s[i])) * prime32
|
||||
h = (h ^ uint32(s[i+1])) * prime32
|
||||
h = (h ^ uint32(s[i+2])) * prime32
|
||||
h = (h ^ uint32(s[i+3])) * prime32
|
||||
h = (h ^ uint32(s[i+4])) * prime32
|
||||
h = (h ^ uint32(s[i+5])) * prime32
|
||||
h = (h ^ uint32(s[i+6])) * prime32
|
||||
h = (h ^ uint32(s[i+7])) * prime32
|
||||
i += 8
|
||||
}
|
||||
|
||||
for _, c := range s[i:] {
|
||||
h = (h ^ uint32(c)) * prime32
|
||||
}
|
||||
|
||||
return h
|
||||
}
|
||||
+232
@@ -0,0 +1,232 @@
|
||||
package statsd
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
gaugeSymbol = []byte("g")
|
||||
countSymbol = []byte("c")
|
||||
histogramSymbol = []byte("h")
|
||||
distributionSymbol = []byte("d")
|
||||
setSymbol = []byte("s")
|
||||
timingSymbol = []byte("ms")
|
||||
)
|
||||
|
||||
func appendHeader(buffer []byte, namespace string, name string) []byte {
|
||||
if namespace != "" {
|
||||
buffer = append(buffer, namespace...)
|
||||
}
|
||||
buffer = append(buffer, name...)
|
||||
buffer = append(buffer, ':')
|
||||
return buffer
|
||||
}
|
||||
|
||||
func appendRate(buffer []byte, rate float64) []byte {
|
||||
if rate < 1 {
|
||||
buffer = append(buffer, "|@"...)
|
||||
buffer = strconv.AppendFloat(buffer, rate, 'f', -1, 64)
|
||||
}
|
||||
return buffer
|
||||
}
|
||||
|
||||
func appendWithoutNewlines(buffer []byte, s string) []byte {
|
||||
// fastpath for strings without newlines
|
||||
if strings.IndexByte(s, '\n') == -1 {
|
||||
return append(buffer, s...)
|
||||
}
|
||||
|
||||
for _, b := range []byte(s) {
|
||||
if b != '\n' {
|
||||
buffer = append(buffer, b)
|
||||
}
|
||||
}
|
||||
return buffer
|
||||
}
|
||||
|
||||
func appendTags(buffer []byte, globalTags []string, tags []string) []byte {
|
||||
if len(globalTags) == 0 && len(tags) == 0 {
|
||||
return buffer
|
||||
}
|
||||
buffer = append(buffer, "|#"...)
|
||||
firstTag := true
|
||||
|
||||
for _, tag := range globalTags {
|
||||
if !firstTag {
|
||||
buffer = append(buffer, ',')
|
||||
}
|
||||
buffer = appendWithoutNewlines(buffer, tag)
|
||||
firstTag = false
|
||||
}
|
||||
for _, tag := range tags {
|
||||
if !firstTag {
|
||||
buffer = append(buffer, ',')
|
||||
}
|
||||
buffer = appendWithoutNewlines(buffer, tag)
|
||||
firstTag = false
|
||||
}
|
||||
return buffer
|
||||
}
|
||||
|
||||
func appendFloatMetric(buffer []byte, typeSymbol []byte, namespace string, globalTags []string, name string, value float64, tags []string, rate float64, precision int) []byte {
|
||||
buffer = appendHeader(buffer, namespace, name)
|
||||
buffer = strconv.AppendFloat(buffer, value, 'f', precision, 64)
|
||||
buffer = append(buffer, '|')
|
||||
buffer = append(buffer, typeSymbol...)
|
||||
buffer = appendRate(buffer, rate)
|
||||
buffer = appendTags(buffer, globalTags, tags)
|
||||
return buffer
|
||||
}
|
||||
|
||||
func appendIntegerMetric(buffer []byte, typeSymbol []byte, namespace string, globalTags []string, name string, value int64, tags []string, rate float64) []byte {
|
||||
buffer = appendHeader(buffer, namespace, name)
|
||||
buffer = strconv.AppendInt(buffer, value, 10)
|
||||
buffer = append(buffer, '|')
|
||||
buffer = append(buffer, typeSymbol...)
|
||||
buffer = appendRate(buffer, rate)
|
||||
buffer = appendTags(buffer, globalTags, tags)
|
||||
return buffer
|
||||
}
|
||||
|
||||
func appendStringMetric(buffer []byte, typeSymbol []byte, namespace string, globalTags []string, name string, value string, tags []string, rate float64) []byte {
|
||||
buffer = appendHeader(buffer, namespace, name)
|
||||
buffer = append(buffer, value...)
|
||||
buffer = append(buffer, '|')
|
||||
buffer = append(buffer, typeSymbol...)
|
||||
buffer = appendRate(buffer, rate)
|
||||
buffer = appendTags(buffer, globalTags, tags)
|
||||
return buffer
|
||||
}
|
||||
|
||||
func appendGauge(buffer []byte, namespace string, globalTags []string, name string, value float64, tags []string, rate float64) []byte {
|
||||
return appendFloatMetric(buffer, gaugeSymbol, namespace, globalTags, name, value, tags, rate, -1)
|
||||
}
|
||||
|
||||
func appendCount(buffer []byte, namespace string, globalTags []string, name string, value int64, tags []string, rate float64) []byte {
|
||||
return appendIntegerMetric(buffer, countSymbol, namespace, globalTags, name, value, tags, rate)
|
||||
}
|
||||
|
||||
func appendHistogram(buffer []byte, namespace string, globalTags []string, name string, value float64, tags []string, rate float64) []byte {
|
||||
return appendFloatMetric(buffer, histogramSymbol, namespace, globalTags, name, value, tags, rate, -1)
|
||||
}
|
||||
|
||||
func appendDistribution(buffer []byte, namespace string, globalTags []string, name string, value float64, tags []string, rate float64) []byte {
|
||||
return appendFloatMetric(buffer, distributionSymbol, namespace, globalTags, name, value, tags, rate, -1)
|
||||
}
|
||||
|
||||
func appendSet(buffer []byte, namespace string, globalTags []string, name string, value string, tags []string, rate float64) []byte {
|
||||
return appendStringMetric(buffer, setSymbol, namespace, globalTags, name, value, tags, rate)
|
||||
}
|
||||
|
||||
func appendTiming(buffer []byte, namespace string, globalTags []string, name string, value float64, tags []string, rate float64) []byte {
|
||||
return appendFloatMetric(buffer, timingSymbol, namespace, globalTags, name, value, tags, rate, 6)
|
||||
}
|
||||
|
||||
func escapedEventTextLen(text string) int {
|
||||
return len(text) + strings.Count(text, "\n")
|
||||
}
|
||||
|
||||
func appendEscapedEventText(buffer []byte, text string) []byte {
|
||||
for _, b := range []byte(text) {
|
||||
if b != '\n' {
|
||||
buffer = append(buffer, b)
|
||||
} else {
|
||||
buffer = append(buffer, "\\n"...)
|
||||
}
|
||||
}
|
||||
return buffer
|
||||
}
|
||||
|
||||
func appendEvent(buffer []byte, event Event, globalTags []string) []byte {
|
||||
escapedTextLen := escapedEventTextLen(event.Text)
|
||||
|
||||
buffer = append(buffer, "_e{"...)
|
||||
buffer = strconv.AppendInt(buffer, int64(len(event.Title)), 10)
|
||||
buffer = append(buffer, ',')
|
||||
buffer = strconv.AppendInt(buffer, int64(escapedTextLen), 10)
|
||||
buffer = append(buffer, "}:"...)
|
||||
buffer = append(buffer, event.Title...)
|
||||
buffer = append(buffer, '|')
|
||||
if escapedTextLen != len(event.Text) {
|
||||
buffer = appendEscapedEventText(buffer, event.Text)
|
||||
} else {
|
||||
buffer = append(buffer, event.Text...)
|
||||
}
|
||||
|
||||
if !event.Timestamp.IsZero() {
|
||||
buffer = append(buffer, "|d:"...)
|
||||
buffer = strconv.AppendInt(buffer, int64(event.Timestamp.Unix()), 10)
|
||||
}
|
||||
|
||||
if len(event.Hostname) != 0 {
|
||||
buffer = append(buffer, "|h:"...)
|
||||
buffer = append(buffer, event.Hostname...)
|
||||
}
|
||||
|
||||
if len(event.AggregationKey) != 0 {
|
||||
buffer = append(buffer, "|k:"...)
|
||||
buffer = append(buffer, event.AggregationKey...)
|
||||
}
|
||||
|
||||
if len(event.Priority) != 0 {
|
||||
buffer = append(buffer, "|p:"...)
|
||||
buffer = append(buffer, event.Priority...)
|
||||
}
|
||||
|
||||
if len(event.SourceTypeName) != 0 {
|
||||
buffer = append(buffer, "|s:"...)
|
||||
buffer = append(buffer, event.SourceTypeName...)
|
||||
}
|
||||
|
||||
if len(event.AlertType) != 0 {
|
||||
buffer = append(buffer, "|t:"...)
|
||||
buffer = append(buffer, string(event.AlertType)...)
|
||||
}
|
||||
|
||||
buffer = appendTags(buffer, globalTags, event.Tags)
|
||||
return buffer
|
||||
}
|
||||
|
||||
func appendEscapedServiceCheckText(buffer []byte, text string) []byte {
|
||||
for i := 0; i < len(text); i++ {
|
||||
if text[i] == '\n' {
|
||||
buffer = append(buffer, "\\n"...)
|
||||
} else if text[i] == 'm' && i+1 < len(text) && text[i+1] == ':' {
|
||||
buffer = append(buffer, "m\\:"...)
|
||||
i++
|
||||
} else {
|
||||
buffer = append(buffer, text[i])
|
||||
}
|
||||
}
|
||||
return buffer
|
||||
}
|
||||
|
||||
func appendServiceCheck(buffer []byte, serviceCheck ServiceCheck, globalTags []string) []byte {
|
||||
buffer = append(buffer, "_sc|"...)
|
||||
buffer = append(buffer, serviceCheck.Name...)
|
||||
buffer = append(buffer, '|')
|
||||
buffer = strconv.AppendInt(buffer, int64(serviceCheck.Status), 10)
|
||||
|
||||
if !serviceCheck.Timestamp.IsZero() {
|
||||
buffer = append(buffer, "|d:"...)
|
||||
buffer = strconv.AppendInt(buffer, int64(serviceCheck.Timestamp.Unix()), 10)
|
||||
}
|
||||
|
||||
if len(serviceCheck.Hostname) != 0 {
|
||||
buffer = append(buffer, "|h:"...)
|
||||
buffer = append(buffer, serviceCheck.Hostname...)
|
||||
}
|
||||
|
||||
buffer = appendTags(buffer, globalTags, serviceCheck.Tags)
|
||||
|
||||
if len(serviceCheck.Message) != 0 {
|
||||
buffer = append(buffer, "|m:"...)
|
||||
buffer = appendEscapedServiceCheckText(buffer, serviceCheck.Message)
|
||||
}
|
||||
return buffer
|
||||
}
|
||||
|
||||
func appendSeparator(buffer []byte) []byte {
|
||||
return append(buffer, '\n')
|
||||
}
|
||||
+125
@@ -0,0 +1,125 @@
|
||||
package statsd
|
||||
|
||||
import (
|
||||
"math"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
/*
|
||||
Those are metrics type that can be aggregated on the client side:
|
||||
- Gauge
|
||||
- Count
|
||||
- Set
|
||||
*/
|
||||
|
||||
type countMetric struct {
|
||||
value int64
|
||||
name string
|
||||
tags []string
|
||||
rate float64
|
||||
}
|
||||
|
||||
func newCountMetric(name string, value int64, tags []string, rate float64) *countMetric {
|
||||
return &countMetric{
|
||||
value: value,
|
||||
name: name,
|
||||
tags: tags,
|
||||
rate: rate,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *countMetric) sample(v int64) {
|
||||
atomic.AddInt64(&c.value, v)
|
||||
}
|
||||
|
||||
func (c *countMetric) flushUnsafe() metric {
|
||||
return metric{
|
||||
metricType: count,
|
||||
name: c.name,
|
||||
tags: c.tags,
|
||||
rate: c.rate,
|
||||
ivalue: c.value,
|
||||
}
|
||||
}
|
||||
|
||||
// Gauge
|
||||
|
||||
type gaugeMetric struct {
|
||||
value uint64
|
||||
name string
|
||||
tags []string
|
||||
rate float64
|
||||
}
|
||||
|
||||
func newGaugeMetric(name string, value float64, tags []string, rate float64) *gaugeMetric {
|
||||
return &gaugeMetric{
|
||||
value: math.Float64bits(value),
|
||||
name: name,
|
||||
tags: tags,
|
||||
rate: rate,
|
||||
}
|
||||
}
|
||||
|
||||
func (g *gaugeMetric) sample(v float64) {
|
||||
atomic.StoreUint64(&g.value, math.Float64bits(v))
|
||||
}
|
||||
|
||||
func (g *gaugeMetric) flushUnsafe() metric {
|
||||
return metric{
|
||||
metricType: gauge,
|
||||
name: g.name,
|
||||
tags: g.tags,
|
||||
rate: g.rate,
|
||||
fvalue: math.Float64frombits(g.value),
|
||||
}
|
||||
}
|
||||
|
||||
// Set
|
||||
|
||||
type setMetric struct {
|
||||
data map[string]struct{}
|
||||
name string
|
||||
tags []string
|
||||
rate float64
|
||||
sync.Mutex
|
||||
}
|
||||
|
||||
func newSetMetric(name string, value string, tags []string, rate float64) *setMetric {
|
||||
set := &setMetric{
|
||||
data: map[string]struct{}{},
|
||||
name: name,
|
||||
tags: tags,
|
||||
rate: rate,
|
||||
}
|
||||
set.data[value] = struct{}{}
|
||||
return set
|
||||
}
|
||||
|
||||
func (s *setMetric) sample(v string) {
|
||||
s.Lock()
|
||||
defer s.Unlock()
|
||||
s.data[v] = struct{}{}
|
||||
}
|
||||
|
||||
// Sets are aggregated on the agent side too. We flush the keys so a set from
|
||||
// multiple application can be correctly aggregated on the agent side.
|
||||
func (s *setMetric) flushUnsafe() []metric {
|
||||
if len(s.data) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
metrics := make([]metric, len(s.data))
|
||||
i := 0
|
||||
for value := range s.data {
|
||||
metrics[i] = metric{
|
||||
metricType: set,
|
||||
name: s.name,
|
||||
tags: s.tags,
|
||||
rate: s.rate,
|
||||
svalue: value,
|
||||
}
|
||||
i++
|
||||
}
|
||||
return metrics
|
||||
}
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
package statsd
|
||||
|
||||
import "time"
|
||||
|
||||
// NoOpClient is a statsd client that does nothing. Can be useful in testing
|
||||
// situations for library users.
|
||||
type NoOpClient struct{}
|
||||
|
||||
// Gauge does nothing and returns nil
|
||||
func (n *NoOpClient) Gauge(name string, value float64, tags []string, rate float64) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Count does nothing and returns nil
|
||||
func (n *NoOpClient) Count(name string, value int64, tags []string, rate float64) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Histogram does nothing and returns nil
|
||||
func (n *NoOpClient) Histogram(name string, value float64, tags []string, rate float64) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Distribution does nothing and returns nil
|
||||
func (n *NoOpClient) Distribution(name string, value float64, tags []string, rate float64) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Decr does nothing and returns nil
|
||||
func (n *NoOpClient) Decr(name string, tags []string, rate float64) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Incr does nothing and returns nil
|
||||
func (n *NoOpClient) Incr(name string, tags []string, rate float64) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Set does nothing and returns nil
|
||||
func (n *NoOpClient) Set(name string, value string, tags []string, rate float64) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Timing does nothing and returns nil
|
||||
func (n *NoOpClient) Timing(name string, value time.Duration, tags []string, rate float64) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// TimeInMilliseconds does nothing and returns nil
|
||||
func (n *NoOpClient) TimeInMilliseconds(name string, value float64, tags []string, rate float64) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Event does nothing and returns nil
|
||||
func (n *NoOpClient) Event(e *Event) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// SimpleEvent does nothing and returns nil
|
||||
func (n *NoOpClient) SimpleEvent(title, text string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// ServiceCheck does nothing and returns nil
|
||||
func (n *NoOpClient) ServiceCheck(sc *ServiceCheck) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// SimpleServiceCheck does nothing and returns nil
|
||||
func (n *NoOpClient) SimpleServiceCheck(name string, status ServiceCheckStatus) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close does nothing and returns nil
|
||||
func (n *NoOpClient) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Flush does nothing and returns nil
|
||||
func (n *NoOpClient) Flush() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetWriteTimeout does nothing and returns nil
|
||||
func (n *NoOpClient) SetWriteTimeout(d time.Duration) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Verify that NoOpClient implements the ClientInterface.
|
||||
// https://golang.org/doc/faq#guarantee_satisfies_interface
|
||||
var _ ClientInterface = &NoOpClient{}
|
||||
+271
@@ -0,0 +1,271 @@
|
||||
package statsd
|
||||
|
||||
import (
|
||||
"math"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
// DefaultNamespace is the default value for the Namespace option
|
||||
DefaultNamespace = ""
|
||||
// DefaultTags is the default value for the Tags option
|
||||
DefaultTags = []string{}
|
||||
// DefaultMaxBytesPerPayload is the default value for the MaxBytesPerPayload option
|
||||
DefaultMaxBytesPerPayload = 0
|
||||
// DefaultMaxMessagesPerPayload is the default value for the MaxMessagesPerPayload option
|
||||
DefaultMaxMessagesPerPayload = math.MaxInt32
|
||||
// DefaultBufferPoolSize is the default value for the DefaultBufferPoolSize option
|
||||
DefaultBufferPoolSize = 0
|
||||
// DefaultBufferFlushInterval is the default value for the BufferFlushInterval option
|
||||
DefaultBufferFlushInterval = 100 * time.Millisecond
|
||||
// DefaultBufferShardCount is the default value for the BufferShardCount option
|
||||
DefaultBufferShardCount = 32
|
||||
// DefaultSenderQueueSize is the default value for the DefaultSenderQueueSize option
|
||||
DefaultSenderQueueSize = 0
|
||||
// DefaultWriteTimeoutUDS is the default value for the WriteTimeoutUDS option
|
||||
DefaultWriteTimeoutUDS = 1 * time.Millisecond
|
||||
// DefaultTelemetry is the default value for the Telemetry option
|
||||
DefaultTelemetry = true
|
||||
// DefaultReceivingMode is the default behavior when sending metrics
|
||||
DefaultReceivingMode = MutexMode
|
||||
// DefaultChannelModeBufferSize is the default size of the channel holding incoming metrics
|
||||
DefaultChannelModeBufferSize = 4096
|
||||
// DefaultAggregationFlushInterval is the default interval for the aggregator to flush metrics.
|
||||
DefaultAggregationFlushInterval = 3 * time.Second
|
||||
// DefaultAggregation
|
||||
DefaultAggregation = false
|
||||
)
|
||||
|
||||
// Options contains the configuration options for a client.
|
||||
type Options struct {
|
||||
// Namespace to prepend to all metrics, events and service checks name.
|
||||
Namespace string
|
||||
// Tags are global tags to be applied to every metrics, events and service checks.
|
||||
Tags []string
|
||||
// MaxBytesPerPayload is the maximum number of bytes a single payload will contain.
|
||||
// The magic value 0 will set the option to the optimal size for the transport
|
||||
// protocol used when creating the client: 1432 for UDP and 8192 for UDS.
|
||||
MaxBytesPerPayload int
|
||||
// MaxMessagesPerPayload is the maximum number of metrics, events and/or service checks a single payload will contain.
|
||||
// This option can be set to `1` to create an unbuffered client.
|
||||
MaxMessagesPerPayload int
|
||||
// BufferPoolSize is the size of the pool of buffers in number of buffers.
|
||||
// The magic value 0 will set the option to the optimal size for the transport
|
||||
// protocol used when creating the client: 2048 for UDP and 512 for UDS.
|
||||
BufferPoolSize int
|
||||
// BufferFlushInterval is the interval after which the current buffer will get flushed.
|
||||
BufferFlushInterval time.Duration
|
||||
// BufferShardCount is the number of buffer "shards" that will be used.
|
||||
// Those shards allows the use of multiple buffers at the same time to reduce
|
||||
// lock contention.
|
||||
BufferShardCount int
|
||||
// SenderQueueSize is the size of the sender queue in number of buffers.
|
||||
// The magic value 0 will set the option to the optimal size for the transport
|
||||
// protocol used when creating the client: 2048 for UDP and 512 for UDS.
|
||||
SenderQueueSize int
|
||||
// WriteTimeoutUDS is the timeout after which a UDS packet is dropped.
|
||||
WriteTimeoutUDS time.Duration
|
||||
// Telemetry is a set of metrics automatically injected by the client in the
|
||||
// dogstatsd stream to be able to monitor the client itself.
|
||||
Telemetry bool
|
||||
// ReceiveMode determins the behavior of the client when receiving to many
|
||||
// metrics. The client will either drop the metrics if its buffers are
|
||||
// full (ChannelMode mode) or block the caller until the metric can be
|
||||
// handled (MutexMode mode). By default the client will MutexMode. This
|
||||
// option should be set to ChannelMode only when use under very high
|
||||
// load.
|
||||
//
|
||||
// MutexMode uses a mutex internally which is much faster than
|
||||
// channel but causes some lock contention when used with a high number
|
||||
// of threads. Mutex are sharded based on the metrics name which
|
||||
// limit mutex contention when goroutines send different metrics.
|
||||
//
|
||||
// ChannelMode: uses channel (of ChannelModeBufferSize size) to send
|
||||
// metrics and drop metrics if the channel is full. Sending metrics in
|
||||
// this mode is slower that MutexMode (because of the channel), but
|
||||
// will not block the application. This mode is made for application
|
||||
// using many goroutines, sending the same metrics at a very high
|
||||
// volume. The goal is to not slow down the application at the cost of
|
||||
// dropping metrics and having a lower max throughput.
|
||||
ReceiveMode ReceivingMode
|
||||
// ChannelModeBufferSize is the size of the channel holding incoming metrics
|
||||
ChannelModeBufferSize int
|
||||
// AggregationFlushInterval is the interval for the aggregator to flush metrics
|
||||
AggregationFlushInterval time.Duration
|
||||
// [beta] Aggregation enables/disables client side aggregation
|
||||
Aggregation bool
|
||||
// TelemetryAddr specify a different endpoint for telemetry metrics.
|
||||
TelemetryAddr string
|
||||
}
|
||||
|
||||
func resolveOptions(options []Option) (*Options, error) {
|
||||
o := &Options{
|
||||
Namespace: DefaultNamespace,
|
||||
Tags: DefaultTags,
|
||||
MaxBytesPerPayload: DefaultMaxBytesPerPayload,
|
||||
MaxMessagesPerPayload: DefaultMaxMessagesPerPayload,
|
||||
BufferPoolSize: DefaultBufferPoolSize,
|
||||
BufferFlushInterval: DefaultBufferFlushInterval,
|
||||
BufferShardCount: DefaultBufferShardCount,
|
||||
SenderQueueSize: DefaultSenderQueueSize,
|
||||
WriteTimeoutUDS: DefaultWriteTimeoutUDS,
|
||||
Telemetry: DefaultTelemetry,
|
||||
ReceiveMode: DefaultReceivingMode,
|
||||
ChannelModeBufferSize: DefaultChannelModeBufferSize,
|
||||
AggregationFlushInterval: DefaultAggregationFlushInterval,
|
||||
Aggregation: DefaultAggregation,
|
||||
}
|
||||
|
||||
for _, option := range options {
|
||||
err := option(o)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return o, nil
|
||||
}
|
||||
|
||||
// Option is a client option. Can return an error if validation fails.
|
||||
type Option func(*Options) error
|
||||
|
||||
// WithNamespace sets the Namespace option.
|
||||
func WithNamespace(namespace string) Option {
|
||||
return func(o *Options) error {
|
||||
if strings.HasSuffix(namespace, ".") {
|
||||
o.Namespace = namespace
|
||||
} else {
|
||||
o.Namespace = namespace + "."
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithTags sets the Tags option.
|
||||
func WithTags(tags []string) Option {
|
||||
return func(o *Options) error {
|
||||
o.Tags = tags
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithMaxMessagesPerPayload sets the MaxMessagesPerPayload option.
|
||||
func WithMaxMessagesPerPayload(maxMessagesPerPayload int) Option {
|
||||
return func(o *Options) error {
|
||||
o.MaxMessagesPerPayload = maxMessagesPerPayload
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithMaxBytesPerPayload sets the MaxBytesPerPayload option.
|
||||
func WithMaxBytesPerPayload(MaxBytesPerPayload int) Option {
|
||||
return func(o *Options) error {
|
||||
o.MaxBytesPerPayload = MaxBytesPerPayload
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithBufferPoolSize sets the BufferPoolSize option.
|
||||
func WithBufferPoolSize(bufferPoolSize int) Option {
|
||||
return func(o *Options) error {
|
||||
o.BufferPoolSize = bufferPoolSize
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithBufferFlushInterval sets the BufferFlushInterval option.
|
||||
func WithBufferFlushInterval(bufferFlushInterval time.Duration) Option {
|
||||
return func(o *Options) error {
|
||||
o.BufferFlushInterval = bufferFlushInterval
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithBufferShardCount sets the BufferShardCount option.
|
||||
func WithBufferShardCount(bufferShardCount int) Option {
|
||||
return func(o *Options) error {
|
||||
o.BufferShardCount = bufferShardCount
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithSenderQueueSize sets the SenderQueueSize option.
|
||||
func WithSenderQueueSize(senderQueueSize int) Option {
|
||||
return func(o *Options) error {
|
||||
o.SenderQueueSize = senderQueueSize
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithWriteTimeoutUDS sets the WriteTimeoutUDS option.
|
||||
func WithWriteTimeoutUDS(writeTimeoutUDS time.Duration) Option {
|
||||
return func(o *Options) error {
|
||||
o.WriteTimeoutUDS = writeTimeoutUDS
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithoutTelemetry disables the telemetry
|
||||
func WithoutTelemetry() Option {
|
||||
return func(o *Options) error {
|
||||
o.Telemetry = false
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithChannelMode will use channel to receive metrics
|
||||
func WithChannelMode() Option {
|
||||
return func(o *Options) error {
|
||||
o.ReceiveMode = ChannelMode
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithMutexMode will use mutex to receive metrics
|
||||
func WithMutexMode() Option {
|
||||
return func(o *Options) error {
|
||||
o.ReceiveMode = MutexMode
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithChannelModeBufferSize the channel buffer size when using "drop mode"
|
||||
func WithChannelModeBufferSize(bufferSize int) Option {
|
||||
return func(o *Options) error {
|
||||
o.ChannelModeBufferSize = bufferSize
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithAggregationInterval set the aggregation interval
|
||||
func WithAggregationInterval(interval time.Duration) Option {
|
||||
return func(o *Options) error {
|
||||
o.AggregationFlushInterval = interval
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithClientSideAggregation enables client side aggregation. Client side aggregation is a beta feature.
|
||||
func WithClientSideAggregation() Option {
|
||||
return func(o *Options) error {
|
||||
o.Aggregation = true
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithoutClientSideAggregation disables client side aggregation.
|
||||
func WithoutClientSideAggregation() Option {
|
||||
return func(o *Options) error {
|
||||
o.Aggregation = false
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithTelemetryAddr specify a different address for telemetry metrics.
|
||||
func WithTelemetryAddr(addr string) Option {
|
||||
return func(o *Options) error {
|
||||
o.TelemetryAddr = addr
|
||||
return nil
|
||||
}
|
||||
}
|
||||
+118
@@ -0,0 +1,118 @@
|
||||
package statsd
|
||||
|
||||
import (
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
// A statsdWriter offers a standard interface regardless of the underlying
|
||||
// protocol. For now UDS and UPD writers are available.
|
||||
// Attention: the underlying buffer of `data` is reused after a `statsdWriter.Write` call.
|
||||
// `statsdWriter.Write` must be synchronous.
|
||||
type statsdWriter interface {
|
||||
Write(data []byte) (n int, err error)
|
||||
SetWriteTimeout(time.Duration) error
|
||||
Close() error
|
||||
}
|
||||
|
||||
// SenderMetrics contains metrics about the health of the sender
|
||||
type SenderMetrics struct {
|
||||
TotalSentBytes uint64
|
||||
TotalSentPayloads uint64
|
||||
TotalDroppedPayloads uint64
|
||||
TotalDroppedBytes uint64
|
||||
TotalDroppedPayloadsQueueFull uint64
|
||||
TotalDroppedBytesQueueFull uint64
|
||||
TotalDroppedPayloadsWriter uint64
|
||||
TotalDroppedBytesWriter uint64
|
||||
}
|
||||
|
||||
type sender struct {
|
||||
transport statsdWriter
|
||||
pool *bufferPool
|
||||
queue chan *statsdBuffer
|
||||
metrics *SenderMetrics
|
||||
stop chan struct{}
|
||||
}
|
||||
|
||||
func newSender(transport statsdWriter, queueSize int, pool *bufferPool) *sender {
|
||||
sender := &sender{
|
||||
transport: transport,
|
||||
pool: pool,
|
||||
queue: make(chan *statsdBuffer, queueSize),
|
||||
metrics: &SenderMetrics{},
|
||||
stop: make(chan struct{}),
|
||||
}
|
||||
|
||||
go sender.sendLoop()
|
||||
return sender
|
||||
}
|
||||
|
||||
func (s *sender) send(buffer *statsdBuffer) {
|
||||
select {
|
||||
case s.queue <- buffer:
|
||||
default:
|
||||
atomic.AddUint64(&s.metrics.TotalDroppedPayloads, 1)
|
||||
atomic.AddUint64(&s.metrics.TotalDroppedBytes, uint64(len(buffer.bytes())))
|
||||
atomic.AddUint64(&s.metrics.TotalDroppedPayloadsQueueFull, 1)
|
||||
atomic.AddUint64(&s.metrics.TotalDroppedBytesQueueFull, uint64(len(buffer.bytes())))
|
||||
s.pool.returnBuffer(buffer)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *sender) write(buffer *statsdBuffer) {
|
||||
_, err := s.transport.Write(buffer.bytes())
|
||||
if err != nil {
|
||||
atomic.AddUint64(&s.metrics.TotalDroppedPayloads, 1)
|
||||
atomic.AddUint64(&s.metrics.TotalDroppedBytes, uint64(len(buffer.bytes())))
|
||||
atomic.AddUint64(&s.metrics.TotalDroppedPayloadsWriter, 1)
|
||||
atomic.AddUint64(&s.metrics.TotalDroppedBytesWriter, uint64(len(buffer.bytes())))
|
||||
} else {
|
||||
atomic.AddUint64(&s.metrics.TotalSentPayloads, 1)
|
||||
atomic.AddUint64(&s.metrics.TotalSentBytes, uint64(len(buffer.bytes())))
|
||||
}
|
||||
s.pool.returnBuffer(buffer)
|
||||
}
|
||||
|
||||
func (s *sender) flushTelemetryMetrics() SenderMetrics {
|
||||
return SenderMetrics{
|
||||
TotalSentBytes: atomic.SwapUint64(&s.metrics.TotalSentBytes, 0),
|
||||
TotalSentPayloads: atomic.SwapUint64(&s.metrics.TotalSentPayloads, 0),
|
||||
TotalDroppedPayloads: atomic.SwapUint64(&s.metrics.TotalDroppedPayloads, 0),
|
||||
TotalDroppedBytes: atomic.SwapUint64(&s.metrics.TotalDroppedBytes, 0),
|
||||
TotalDroppedPayloadsQueueFull: atomic.SwapUint64(&s.metrics.TotalDroppedPayloadsQueueFull, 0),
|
||||
TotalDroppedBytesQueueFull: atomic.SwapUint64(&s.metrics.TotalDroppedBytesQueueFull, 0),
|
||||
TotalDroppedPayloadsWriter: atomic.SwapUint64(&s.metrics.TotalDroppedPayloadsWriter, 0),
|
||||
TotalDroppedBytesWriter: atomic.SwapUint64(&s.metrics.TotalDroppedBytesWriter, 0),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *sender) sendLoop() {
|
||||
defer close(s.stop)
|
||||
for {
|
||||
select {
|
||||
case buffer := <-s.queue:
|
||||
s.write(buffer)
|
||||
case <-s.stop:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *sender) flush() {
|
||||
for {
|
||||
select {
|
||||
case buffer := <-s.queue:
|
||||
s.write(buffer)
|
||||
default:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *sender) close() error {
|
||||
s.stop <- struct{}{}
|
||||
<-s.stop
|
||||
s.flush()
|
||||
return s.transport.Close()
|
||||
}
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
package statsd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ServiceCheckStatus support
|
||||
type ServiceCheckStatus byte
|
||||
|
||||
const (
|
||||
// Ok is the "ok" ServiceCheck status
|
||||
Ok ServiceCheckStatus = 0
|
||||
// Warn is the "warning" ServiceCheck status
|
||||
Warn ServiceCheckStatus = 1
|
||||
// Critical is the "critical" ServiceCheck status
|
||||
Critical ServiceCheckStatus = 2
|
||||
// Unknown is the "unknown" ServiceCheck status
|
||||
Unknown ServiceCheckStatus = 3
|
||||
)
|
||||
|
||||
// A ServiceCheck is an object that contains status of DataDog service check.
|
||||
type ServiceCheck struct {
|
||||
// Name of the service check. Required.
|
||||
Name string
|
||||
// Status of service check. Required.
|
||||
Status ServiceCheckStatus
|
||||
// Timestamp is a timestamp for the serviceCheck. If not provided, the dogstatsd
|
||||
// server will set this to the current time.
|
||||
Timestamp time.Time
|
||||
// Hostname for the serviceCheck.
|
||||
Hostname string
|
||||
// A message describing the current state of the serviceCheck.
|
||||
Message string
|
||||
// Tags for the serviceCheck.
|
||||
Tags []string
|
||||
}
|
||||
|
||||
// NewServiceCheck creates a new serviceCheck with the given name and status. Error checking
|
||||
// against these values is done at send-time, or upon running sc.Check.
|
||||
func NewServiceCheck(name string, status ServiceCheckStatus) *ServiceCheck {
|
||||
return &ServiceCheck{
|
||||
Name: name,
|
||||
Status: status,
|
||||
}
|
||||
}
|
||||
|
||||
// Check verifies that a service check is valid.
|
||||
func (sc ServiceCheck) Check() error {
|
||||
if len(sc.Name) == 0 {
|
||||
return fmt.Errorf("statsd.ServiceCheck name is required")
|
||||
}
|
||||
if byte(sc.Status) < 0 || byte(sc.Status) > 3 {
|
||||
return fmt.Errorf("statsd.ServiceCheck status has invalid value")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Encode returns the dogstatsd wire protocol representation for a service check.
|
||||
// Tags may be passed which will be added to the encoded output but not to
|
||||
// the Service Check's list of tags, eg. for default tags.
|
||||
func (sc ServiceCheck) Encode(tags ...string) (string, error) {
|
||||
err := sc.Check()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
var buffer []byte
|
||||
buffer = appendServiceCheck(buffer, sc, tags)
|
||||
return string(buffer), nil
|
||||
}
|
||||
+567
@@ -0,0 +1,567 @@
|
||||
// Copyright 2013 Ooyala, Inc.
|
||||
|
||||
/*
|
||||
Package statsd provides a Go dogstatsd client. Dogstatsd extends the popular statsd,
|
||||
adding tags and histograms and pushing upstream to Datadog.
|
||||
|
||||
Refer to http://docs.datadoghq.com/guides/dogstatsd/ for information about DogStatsD.
|
||||
|
||||
Example Usage:
|
||||
|
||||
// Create the client
|
||||
c, err := statsd.New("127.0.0.1:8125")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
// Prefix every metric with the app name
|
||||
c.Namespace = "flubber."
|
||||
// Send the EC2 availability zone as a tag with every metric
|
||||
c.Tags = append(c.Tags, "us-east-1a")
|
||||
err = c.Gauge("request.duration", 1.2, nil, 1)
|
||||
|
||||
statsd is based on go-statsd-client.
|
||||
*/
|
||||
package statsd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
/*
|
||||
OptimalUDPPayloadSize defines the optimal payload size for a UDP datagram, 1432 bytes
|
||||
is optimal for regular networks with an MTU of 1500 so datagrams don't get
|
||||
fragmented. It's generally recommended not to fragment UDP datagrams as losing
|
||||
a single fragment will cause the entire datagram to be lost.
|
||||
*/
|
||||
const OptimalUDPPayloadSize = 1432
|
||||
|
||||
/*
|
||||
MaxUDPPayloadSize defines the maximum payload size for a UDP datagram.
|
||||
Its value comes from the calculation: 65535 bytes Max UDP datagram size -
|
||||
8byte UDP header - 60byte max IP headers
|
||||
any number greater than that will see frames being cut out.
|
||||
*/
|
||||
const MaxUDPPayloadSize = 65467
|
||||
|
||||
// DefaultUDPBufferPoolSize is the default size of the buffer pool for UDP clients.
|
||||
const DefaultUDPBufferPoolSize = 2048
|
||||
|
||||
// DefaultUDSBufferPoolSize is the default size of the buffer pool for UDS clients.
|
||||
const DefaultUDSBufferPoolSize = 512
|
||||
|
||||
/*
|
||||
DefaultMaxAgentPayloadSize is the default maximum payload size the agent
|
||||
can receive. This can be adjusted by changing dogstatsd_buffer_size in the
|
||||
agent configuration file datadog.yaml.
|
||||
*/
|
||||
const DefaultMaxAgentPayloadSize = 8192
|
||||
|
||||
/*
|
||||
UnixAddressPrefix holds the prefix to use to enable Unix Domain Socket
|
||||
traffic instead of UDP.
|
||||
*/
|
||||
const UnixAddressPrefix = "unix://"
|
||||
|
||||
/*
|
||||
ddEnvTagsMapping is a mapping of each "DD_" prefixed environment variable
|
||||
to a specific tag name.
|
||||
*/
|
||||
var ddEnvTagsMapping = map[string]string{
|
||||
// Client-side entity ID injection for container tagging.
|
||||
"DD_ENTITY_ID": "dd.internal.entity_id",
|
||||
// The name of the env in which the service runs.
|
||||
"DD_ENV": "env",
|
||||
// The name of the running service.
|
||||
"DD_SERVICE": "service",
|
||||
// The current version of the running service.
|
||||
"DD_VERSION": "version",
|
||||
}
|
||||
|
||||
type metricType int
|
||||
|
||||
const (
|
||||
gauge metricType = iota
|
||||
count
|
||||
histogram
|
||||
distribution
|
||||
set
|
||||
timing
|
||||
event
|
||||
serviceCheck
|
||||
)
|
||||
|
||||
type ReceivingMode int
|
||||
|
||||
const (
|
||||
MutexMode ReceivingMode = iota
|
||||
ChannelMode
|
||||
)
|
||||
|
||||
type metric struct {
|
||||
metricType metricType
|
||||
namespace string
|
||||
globalTags []string
|
||||
name string
|
||||
fvalue float64
|
||||
ivalue int64
|
||||
svalue string
|
||||
evalue *Event
|
||||
scvalue *ServiceCheck
|
||||
tags []string
|
||||
rate float64
|
||||
}
|
||||
|
||||
type noClientErr string
|
||||
|
||||
// ErrNoClient is returned if statsd reporting methods are invoked on
|
||||
// a nil client.
|
||||
const ErrNoClient = noClientErr("statsd client is nil")
|
||||
|
||||
func (e noClientErr) Error() string {
|
||||
return string(e)
|
||||
}
|
||||
|
||||
// ClientInterface is an interface that exposes the common client functions for the
|
||||
// purpose of being able to provide a no-op client or even mocking. This can aid
|
||||
// downstream users' with their testing.
|
||||
type ClientInterface interface {
|
||||
// Gauge measures the value of a metric at a particular time.
|
||||
Gauge(name string, value float64, tags []string, rate float64) error
|
||||
|
||||
// Count tracks how many times something happened per second.
|
||||
Count(name string, value int64, tags []string, rate float64) error
|
||||
|
||||
// Histogram tracks the statistical distribution of a set of values on each host.
|
||||
Histogram(name string, value float64, tags []string, rate float64) error
|
||||
|
||||
// Distribution tracks the statistical distribution of a set of values across your infrastructure.
|
||||
Distribution(name string, value float64, tags []string, rate float64) error
|
||||
|
||||
// Decr is just Count of -1
|
||||
Decr(name string, tags []string, rate float64) error
|
||||
|
||||
// Incr is just Count of 1
|
||||
Incr(name string, tags []string, rate float64) error
|
||||
|
||||
// Set counts the number of unique elements in a group.
|
||||
Set(name string, value string, tags []string, rate float64) error
|
||||
|
||||
// Timing sends timing information, it is an alias for TimeInMilliseconds
|
||||
Timing(name string, value time.Duration, tags []string, rate float64) error
|
||||
|
||||
// TimeInMilliseconds sends timing information in milliseconds.
|
||||
// It is flushed by statsd with percentiles, mean and other info (https://github.com/etsy/statsd/blob/master/docs/metric_types.md#timing)
|
||||
TimeInMilliseconds(name string, value float64, tags []string, rate float64) error
|
||||
|
||||
// Event sends the provided Event.
|
||||
Event(e *Event) error
|
||||
|
||||
// SimpleEvent sends an event with the provided title and text.
|
||||
SimpleEvent(title, text string) error
|
||||
|
||||
// ServiceCheck sends the provided ServiceCheck.
|
||||
ServiceCheck(sc *ServiceCheck) error
|
||||
|
||||
// SimpleServiceCheck sends an serviceCheck with the provided name and status.
|
||||
SimpleServiceCheck(name string, status ServiceCheckStatus) error
|
||||
|
||||
// Close the client connection.
|
||||
Close() error
|
||||
|
||||
// Flush forces a flush of all the queued dogstatsd payloads.
|
||||
Flush() error
|
||||
|
||||
// SetWriteTimeout allows the user to set a custom write timeout.
|
||||
SetWriteTimeout(d time.Duration) error
|
||||
}
|
||||
|
||||
// A Client is a handle for sending messages to dogstatsd. It is safe to
|
||||
// use one Client from multiple goroutines simultaneously.
|
||||
type Client struct {
|
||||
// Sender handles the underlying networking protocol
|
||||
sender *sender
|
||||
// Namespace to prepend to all statsd calls
|
||||
Namespace string
|
||||
// Tags are global tags to be added to every statsd call
|
||||
Tags []string
|
||||
// skipErrors turns off error passing and allows UDS to emulate UDP behaviour
|
||||
SkipErrors bool
|
||||
flushTime time.Duration
|
||||
metrics *ClientMetrics
|
||||
telemetry *telemetryClient
|
||||
stop chan struct{}
|
||||
wg sync.WaitGroup
|
||||
workers []*worker
|
||||
closerLock sync.Mutex
|
||||
receiveMode ReceivingMode
|
||||
agg *aggregator
|
||||
options []Option
|
||||
addrOption string
|
||||
}
|
||||
|
||||
// ClientMetrics contains metrics about the client
|
||||
type ClientMetrics struct {
|
||||
TotalMetrics uint64
|
||||
TotalEvents uint64
|
||||
TotalServiceChecks uint64
|
||||
TotalDroppedOnReceive uint64
|
||||
}
|
||||
|
||||
// Verify that Client implements the ClientInterface.
|
||||
// https://golang.org/doc/faq#guarantee_satisfies_interface
|
||||
var _ ClientInterface = &Client{}
|
||||
|
||||
func resolveAddr(addr string) (statsdWriter, string, error) {
|
||||
if !strings.HasPrefix(addr, UnixAddressPrefix) {
|
||||
w, err := newUDPWriter(addr)
|
||||
return w, "udp", err
|
||||
}
|
||||
|
||||
w, err := newUDSWriter(addr[len(UnixAddressPrefix):])
|
||||
return w, "uds", err
|
||||
}
|
||||
|
||||
// New returns a pointer to a new Client given an addr in the format "hostname:port" or
|
||||
// "unix:///path/to/socket".
|
||||
func New(addr string, options ...Option) (*Client, error) {
|
||||
o, err := resolveOptions(options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
w, writerType, err := resolveAddr(addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
client, err := newWithWriter(w, o, writerType)
|
||||
if err == nil {
|
||||
client.options = append(client.options, options...)
|
||||
client.addrOption = addr
|
||||
}
|
||||
return client, err
|
||||
}
|
||||
|
||||
// NewWithWriter creates a new Client with given writer. Writer is a
|
||||
// io.WriteCloser + SetWriteTimeout(time.Duration) error
|
||||
func NewWithWriter(w statsdWriter, options ...Option) (*Client, error) {
|
||||
o, err := resolveOptions(options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return newWithWriter(w, o, "custom")
|
||||
}
|
||||
|
||||
// CloneWithExtraOptions create a new Client with extra options
|
||||
func CloneWithExtraOptions(c *Client, options ...Option) (*Client, error) {
|
||||
if c == nil {
|
||||
return nil, ErrNoClient
|
||||
}
|
||||
|
||||
if c.addrOption == "" {
|
||||
return nil, fmt.Errorf("can't clone client with no addrOption")
|
||||
}
|
||||
opt := append(c.options, options...)
|
||||
return New(c.addrOption, opt...)
|
||||
}
|
||||
|
||||
func newWithWriter(w statsdWriter, o *Options, writerName string) (*Client, error) {
|
||||
|
||||
w.SetWriteTimeout(o.WriteTimeoutUDS)
|
||||
|
||||
c := Client{
|
||||
Namespace: o.Namespace,
|
||||
Tags: o.Tags,
|
||||
metrics: &ClientMetrics{},
|
||||
}
|
||||
if o.Aggregation {
|
||||
c.agg = newAggregator(&c)
|
||||
c.agg.start(o.AggregationFlushInterval)
|
||||
}
|
||||
|
||||
// Inject values of DD_* environment variables as global tags.
|
||||
for envName, tagName := range ddEnvTagsMapping {
|
||||
if value := os.Getenv(envName); value != "" {
|
||||
c.Tags = append(c.Tags, fmt.Sprintf("%s:%s", tagName, value))
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME: The agent has a performance pitfall preventing us from using better defaults for UDS,
|
||||
// this is why we fallback on UDP defaults even in UDS mode.
|
||||
// Once it's fixed, use `DefaultMaxAgentPayloadSize` and `DefaultUDSBufferPoolSize` instead for UDS.
|
||||
if o.MaxBytesPerPayload == 0 {
|
||||
o.MaxBytesPerPayload = OptimalUDPPayloadSize
|
||||
}
|
||||
if o.BufferPoolSize == 0 {
|
||||
o.BufferPoolSize = DefaultUDPBufferPoolSize
|
||||
}
|
||||
if o.SenderQueueSize == 0 {
|
||||
o.SenderQueueSize = DefaultUDPBufferPoolSize
|
||||
}
|
||||
|
||||
bufferPool := newBufferPool(o.BufferPoolSize, o.MaxBytesPerPayload, o.MaxMessagesPerPayload)
|
||||
c.sender = newSender(w, o.SenderQueueSize, bufferPool)
|
||||
c.receiveMode = o.ReceiveMode
|
||||
for i := 0; i < o.BufferShardCount; i++ {
|
||||
w := newWorker(bufferPool, c.sender)
|
||||
c.workers = append(c.workers, w)
|
||||
if c.receiveMode == ChannelMode {
|
||||
w.startReceivingMetric(o.ChannelModeBufferSize)
|
||||
}
|
||||
}
|
||||
|
||||
c.flushTime = o.BufferFlushInterval
|
||||
c.stop = make(chan struct{}, 1)
|
||||
|
||||
c.wg.Add(1)
|
||||
go func() {
|
||||
defer c.wg.Done()
|
||||
c.watch()
|
||||
}()
|
||||
|
||||
if o.Telemetry {
|
||||
if o.TelemetryAddr == "" {
|
||||
c.telemetry = NewTelemetryClient(&c, writerName)
|
||||
} else {
|
||||
var err error
|
||||
c.telemetry, err = NewTelemetryClientWithCustomAddr(&c, writerName, o.TelemetryAddr, bufferPool)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
c.telemetry.run(&c.wg, c.stop)
|
||||
}
|
||||
|
||||
return &c, nil
|
||||
}
|
||||
|
||||
// NewBuffered returns a Client that buffers its output and sends it in chunks.
|
||||
// Buflen is the length of the buffer in number of commands.
|
||||
//
|
||||
// When addr is empty, the client will default to a UDP client and use the DD_AGENT_HOST
|
||||
// and (optionally) the DD_DOGSTATSD_PORT environment variables to build the target address.
|
||||
func NewBuffered(addr string, buflen int) (*Client, error) {
|
||||
return New(addr, WithMaxMessagesPerPayload(buflen))
|
||||
}
|
||||
|
||||
// SetWriteTimeout allows the user to set a custom UDS write timeout. Not supported for UDP.
|
||||
func (c *Client) SetWriteTimeout(d time.Duration) error {
|
||||
if c == nil {
|
||||
return ErrNoClient
|
||||
}
|
||||
return c.sender.transport.SetWriteTimeout(d)
|
||||
}
|
||||
|
||||
func (c *Client) watch() {
|
||||
ticker := time.NewTicker(c.flushTime)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
for _, w := range c.workers {
|
||||
w.flush()
|
||||
}
|
||||
case <-c.stop:
|
||||
ticker.Stop()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Flush forces a flush of all the queued dogstatsd payloads
|
||||
// This method is blocking and will not return until everything is sent
|
||||
// through the network
|
||||
func (c *Client) Flush() error {
|
||||
if c == nil {
|
||||
return ErrNoClient
|
||||
}
|
||||
if c.agg != nil {
|
||||
c.agg.sendMetrics()
|
||||
}
|
||||
for _, w := range c.workers {
|
||||
w.flush()
|
||||
}
|
||||
c.sender.flush()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) FlushTelemetryMetrics() ClientMetrics {
|
||||
return ClientMetrics{
|
||||
TotalMetrics: atomic.SwapUint64(&c.metrics.TotalMetrics, 0),
|
||||
TotalEvents: atomic.SwapUint64(&c.metrics.TotalEvents, 0),
|
||||
TotalServiceChecks: atomic.SwapUint64(&c.metrics.TotalServiceChecks, 0),
|
||||
TotalDroppedOnReceive: atomic.SwapUint64(&c.metrics.TotalDroppedOnReceive, 0),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) send(m metric) error {
|
||||
if c == nil {
|
||||
return ErrNoClient
|
||||
}
|
||||
|
||||
m.globalTags = c.Tags
|
||||
m.namespace = c.Namespace
|
||||
|
||||
h := hashString32(m.name)
|
||||
worker := c.workers[h%uint32(len(c.workers))]
|
||||
|
||||
if c.receiveMode == ChannelMode {
|
||||
select {
|
||||
case worker.inputMetrics <- m:
|
||||
default:
|
||||
atomic.AddUint64(&c.metrics.TotalDroppedOnReceive, 1)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return worker.processMetric(m)
|
||||
}
|
||||
|
||||
// Gauge measures the value of a metric at a particular time.
|
||||
func (c *Client) Gauge(name string, value float64, tags []string, rate float64) error {
|
||||
if c == nil {
|
||||
return ErrNoClient
|
||||
}
|
||||
atomic.AddUint64(&c.metrics.TotalMetrics, 1)
|
||||
if c.agg != nil {
|
||||
return c.agg.gauge(name, value, tags, rate)
|
||||
}
|
||||
return c.send(metric{metricType: gauge, name: name, fvalue: value, tags: tags, rate: rate})
|
||||
}
|
||||
|
||||
// Count tracks how many times something happened per second.
|
||||
func (c *Client) Count(name string, value int64, tags []string, rate float64) error {
|
||||
if c == nil {
|
||||
return ErrNoClient
|
||||
}
|
||||
atomic.AddUint64(&c.metrics.TotalMetrics, 1)
|
||||
if c.agg != nil {
|
||||
return c.agg.count(name, value, tags, rate)
|
||||
}
|
||||
return c.send(metric{metricType: count, name: name, ivalue: value, tags: tags, rate: rate})
|
||||
}
|
||||
|
||||
// Histogram tracks the statistical distribution of a set of values on each host.
|
||||
func (c *Client) Histogram(name string, value float64, tags []string, rate float64) error {
|
||||
if c == nil {
|
||||
return ErrNoClient
|
||||
}
|
||||
atomic.AddUint64(&c.metrics.TotalMetrics, 1)
|
||||
return c.send(metric{metricType: histogram, name: name, fvalue: value, tags: tags, rate: rate})
|
||||
}
|
||||
|
||||
// Distribution tracks the statistical distribution of a set of values across your infrastructure.
|
||||
func (c *Client) Distribution(name string, value float64, tags []string, rate float64) error {
|
||||
if c == nil {
|
||||
return ErrNoClient
|
||||
}
|
||||
atomic.AddUint64(&c.metrics.TotalMetrics, 1)
|
||||
return c.send(metric{metricType: distribution, name: name, fvalue: value, tags: tags, rate: rate})
|
||||
}
|
||||
|
||||
// Decr is just Count of -1
|
||||
func (c *Client) Decr(name string, tags []string, rate float64) error {
|
||||
return c.Count(name, -1, tags, rate)
|
||||
}
|
||||
|
||||
// Incr is just Count of 1
|
||||
func (c *Client) Incr(name string, tags []string, rate float64) error {
|
||||
return c.Count(name, 1, tags, rate)
|
||||
}
|
||||
|
||||
// Set counts the number of unique elements in a group.
|
||||
func (c *Client) Set(name string, value string, tags []string, rate float64) error {
|
||||
if c == nil {
|
||||
return ErrNoClient
|
||||
}
|
||||
atomic.AddUint64(&c.metrics.TotalMetrics, 1)
|
||||
if c.agg != nil {
|
||||
return c.agg.set(name, value, tags, rate)
|
||||
}
|
||||
return c.send(metric{metricType: set, name: name, svalue: value, tags: tags, rate: rate})
|
||||
}
|
||||
|
||||
// Timing sends timing information, it is an alias for TimeInMilliseconds
|
||||
func (c *Client) Timing(name string, value time.Duration, tags []string, rate float64) error {
|
||||
return c.TimeInMilliseconds(name, value.Seconds()*1000, tags, rate)
|
||||
}
|
||||
|
||||
// TimeInMilliseconds sends timing information in milliseconds.
|
||||
// It is flushed by statsd with percentiles, mean and other info (https://github.com/etsy/statsd/blob/master/docs/metric_types.md#timing)
|
||||
func (c *Client) TimeInMilliseconds(name string, value float64, tags []string, rate float64) error {
|
||||
if c == nil {
|
||||
return ErrNoClient
|
||||
}
|
||||
atomic.AddUint64(&c.metrics.TotalMetrics, 1)
|
||||
return c.send(metric{metricType: timing, name: name, fvalue: value, tags: tags, rate: rate})
|
||||
}
|
||||
|
||||
// Event sends the provided Event.
|
||||
func (c *Client) Event(e *Event) error {
|
||||
if c == nil {
|
||||
return ErrNoClient
|
||||
}
|
||||
atomic.AddUint64(&c.metrics.TotalEvents, 1)
|
||||
return c.send(metric{metricType: event, evalue: e, rate: 1})
|
||||
}
|
||||
|
||||
// SimpleEvent sends an event with the provided title and text.
|
||||
func (c *Client) SimpleEvent(title, text string) error {
|
||||
e := NewEvent(title, text)
|
||||
return c.Event(e)
|
||||
}
|
||||
|
||||
// ServiceCheck sends the provided ServiceCheck.
|
||||
func (c *Client) ServiceCheck(sc *ServiceCheck) error {
|
||||
if c == nil {
|
||||
return ErrNoClient
|
||||
}
|
||||
atomic.AddUint64(&c.metrics.TotalServiceChecks, 1)
|
||||
return c.send(metric{metricType: serviceCheck, scvalue: sc, rate: 1})
|
||||
}
|
||||
|
||||
// SimpleServiceCheck sends an serviceCheck with the provided name and status.
|
||||
func (c *Client) SimpleServiceCheck(name string, status ServiceCheckStatus) error {
|
||||
sc := NewServiceCheck(name, status)
|
||||
return c.ServiceCheck(sc)
|
||||
}
|
||||
|
||||
// Close the client connection.
|
||||
func (c *Client) Close() error {
|
||||
if c == nil {
|
||||
return ErrNoClient
|
||||
}
|
||||
|
||||
// Acquire closer lock to ensure only one thread can close the stop channel
|
||||
c.closerLock.Lock()
|
||||
defer c.closerLock.Unlock()
|
||||
|
||||
// Notify all other threads that they should stop
|
||||
select {
|
||||
case <-c.stop:
|
||||
return nil
|
||||
default:
|
||||
}
|
||||
close(c.stop)
|
||||
|
||||
if c.receiveMode == ChannelMode {
|
||||
for _, w := range c.workers {
|
||||
w.stopReceivingMetric()
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for the threads to stop
|
||||
c.wg.Wait()
|
||||
|
||||
// Finally flush any remaining metrics that may have come in at the last moment
|
||||
if c.agg != nil {
|
||||
c.agg.stop()
|
||||
}
|
||||
c.Flush()
|
||||
|
||||
return c.sender.close()
|
||||
}
|
||||
+118
@@ -0,0 +1,118 @@
|
||||
package statsd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
/*
|
||||
TelemetryInterval is the interval at which telemetry will be sent by the client.
|
||||
*/
|
||||
const TelemetryInterval = 10 * time.Second
|
||||
|
||||
/*
|
||||
clientTelemetryTag is a tag identifying this specific client.
|
||||
*/
|
||||
var clientTelemetryTag = "client:go"
|
||||
|
||||
/*
|
||||
clientVersionTelemetryTag is a tag identifying this specific client version.
|
||||
*/
|
||||
var clientVersionTelemetryTag = "client_version:4.0.0"
|
||||
|
||||
type telemetryClient struct {
|
||||
c *Client
|
||||
tags []string
|
||||
sender *sender
|
||||
worker *worker
|
||||
}
|
||||
|
||||
func NewTelemetryClient(c *Client, transport string) *telemetryClient {
|
||||
return &telemetryClient{
|
||||
c: c,
|
||||
tags: append(c.Tags, clientTelemetryTag, clientVersionTelemetryTag, "client_transport:"+transport),
|
||||
}
|
||||
}
|
||||
|
||||
func NewTelemetryClientWithCustomAddr(c *Client, transport string, telemetryAddr string, pool *bufferPool) (*telemetryClient, error) {
|
||||
telemetryWriter, _, err := resolveAddr(telemetryAddr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Could not resolve telemetry address: %v", err)
|
||||
}
|
||||
|
||||
t := NewTelemetryClient(c, transport)
|
||||
|
||||
// Creating a custom sender/worker with 1 worker in mutex mode for the
|
||||
// telemetry that share the same bufferPool.
|
||||
// FIXME due to performance pitfall, we're always using UDP defaults
|
||||
// even for UDS.
|
||||
t.sender = newSender(telemetryWriter, DefaultUDPBufferPoolSize, pool)
|
||||
t.worker = newWorker(pool, t.sender)
|
||||
return t, nil
|
||||
}
|
||||
|
||||
func (t *telemetryClient) run(wg *sync.WaitGroup, stop chan struct{}) {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
ticker := time.NewTicker(TelemetryInterval)
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
t.sendTelemetry()
|
||||
case <-stop:
|
||||
ticker.Stop()
|
||||
if t.sender != nil {
|
||||
t.sender.close()
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (t *telemetryClient) sendTelemetry() {
|
||||
for _, m := range t.flush() {
|
||||
if t.worker != nil {
|
||||
t.worker.processMetric(m)
|
||||
} else {
|
||||
t.c.send(m)
|
||||
}
|
||||
}
|
||||
|
||||
if t.worker != nil {
|
||||
t.worker.flush()
|
||||
}
|
||||
}
|
||||
|
||||
// flushTelemetry returns Telemetry metrics to be flushed. It's its own function to ease testing.
|
||||
func (t *telemetryClient) flush() []metric {
|
||||
m := []metric{}
|
||||
|
||||
// same as Count but without global namespace
|
||||
telemetryCount := func(name string, value int64) {
|
||||
m = append(m, metric{metricType: count, name: name, ivalue: value, tags: t.tags, rate: 1})
|
||||
}
|
||||
|
||||
clientMetrics := t.c.FlushTelemetryMetrics()
|
||||
telemetryCount("datadog.dogstatsd.client.metrics", int64(clientMetrics.TotalMetrics))
|
||||
telemetryCount("datadog.dogstatsd.client.events", int64(clientMetrics.TotalEvents))
|
||||
telemetryCount("datadog.dogstatsd.client.service_checks", int64(clientMetrics.TotalServiceChecks))
|
||||
telemetryCount("datadog.dogstatsd.client.metric_dropped_on_receive", int64(clientMetrics.TotalDroppedOnReceive))
|
||||
|
||||
senderMetrics := t.c.sender.flushTelemetryMetrics()
|
||||
telemetryCount("datadog.dogstatsd.client.packets_sent", int64(senderMetrics.TotalSentPayloads))
|
||||
telemetryCount("datadog.dogstatsd.client.bytes_sent", int64(senderMetrics.TotalSentBytes))
|
||||
telemetryCount("datadog.dogstatsd.client.packets_dropped", int64(senderMetrics.TotalDroppedPayloads))
|
||||
telemetryCount("datadog.dogstatsd.client.bytes_dropped", int64(senderMetrics.TotalDroppedBytes))
|
||||
telemetryCount("datadog.dogstatsd.client.packets_dropped_queue", int64(senderMetrics.TotalDroppedPayloadsQueueFull))
|
||||
telemetryCount("datadog.dogstatsd.client.bytes_dropped_queue", int64(senderMetrics.TotalDroppedBytesQueueFull))
|
||||
telemetryCount("datadog.dogstatsd.client.packets_dropped_writer", int64(senderMetrics.TotalDroppedPayloadsWriter))
|
||||
telemetryCount("datadog.dogstatsd.client.bytes_dropped_writer", int64(senderMetrics.TotalDroppedBytesWriter))
|
||||
|
||||
if aggMetrics := t.c.agg.flushTelemetryMetrics(); aggMetrics != nil {
|
||||
telemetryCount("datadog.dogstatsd.client.aggregated_context", int64(aggMetrics.nbContext))
|
||||
}
|
||||
return m
|
||||
}
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
package statsd
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
autoHostEnvName = "DD_AGENT_HOST"
|
||||
autoPortEnvName = "DD_DOGSTATSD_PORT"
|
||||
defaultUDPPort = "8125"
|
||||
)
|
||||
|
||||
// udpWriter is an internal class wrapping around management of UDP connection
|
||||
type udpWriter struct {
|
||||
conn net.Conn
|
||||
}
|
||||
|
||||
// New returns a pointer to a new udpWriter given an addr in the format "hostname:port".
|
||||
func newUDPWriter(addr string) (*udpWriter, error) {
|
||||
if addr == "" {
|
||||
addr = addressFromEnvironment()
|
||||
}
|
||||
if addr == "" {
|
||||
return nil, errors.New("No address passed and autodetection from environment failed")
|
||||
}
|
||||
|
||||
udpAddr, err := net.ResolveUDPAddr("udp", addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
conn, err := net.DialUDP("udp", nil, udpAddr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
writer := &udpWriter{conn: conn}
|
||||
return writer, nil
|
||||
}
|
||||
|
||||
// SetWriteTimeout is not needed for UDP, returns error
|
||||
func (w *udpWriter) SetWriteTimeout(d time.Duration) error {
|
||||
return errors.New("SetWriteTimeout: not supported for UDP connections")
|
||||
}
|
||||
|
||||
// Write data to the UDP connection with no error handling
|
||||
func (w *udpWriter) Write(data []byte) (int, error) {
|
||||
return w.conn.Write(data)
|
||||
}
|
||||
|
||||
func (w *udpWriter) Close() error {
|
||||
return w.conn.Close()
|
||||
}
|
||||
|
||||
func (w *udpWriter) remoteAddr() net.Addr {
|
||||
return w.conn.RemoteAddr()
|
||||
}
|
||||
|
||||
func addressFromEnvironment() string {
|
||||
autoHost := os.Getenv(autoHostEnvName)
|
||||
if autoHost == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
autoPort := os.Getenv(autoPortEnvName)
|
||||
if autoPort == "" {
|
||||
autoPort = defaultUDPPort
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s:%s", autoHost, autoPort)
|
||||
}
|
||||
+98
@@ -0,0 +1,98 @@
|
||||
package statsd
|
||||
|
||||
import (
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
/*
|
||||
UDSTimeout holds the default timeout for UDS socket writes, as they can get
|
||||
blocking when the receiving buffer is full.
|
||||
*/
|
||||
const defaultUDSTimeout = 1 * time.Millisecond
|
||||
|
||||
// udsWriter is an internal class wrapping around management of UDS connection
|
||||
type udsWriter struct {
|
||||
// Address to send metrics to, needed to allow reconnection on error
|
||||
addr net.Addr
|
||||
// Established connection object, or nil if not connected yet
|
||||
conn net.Conn
|
||||
// write timeout
|
||||
writeTimeout time.Duration
|
||||
sync.RWMutex // used to lock conn / writer can replace it
|
||||
}
|
||||
|
||||
// newUDSWriter returns a pointer to a new udsWriter given a socket file path as addr.
|
||||
func newUDSWriter(addr string) (*udsWriter, error) {
|
||||
udsAddr, err := net.ResolveUnixAddr("unixgram", addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Defer connection to first Write
|
||||
writer := &udsWriter{addr: udsAddr, conn: nil, writeTimeout: defaultUDSTimeout}
|
||||
return writer, nil
|
||||
}
|
||||
|
||||
// SetWriteTimeout allows the user to set a custom write timeout
|
||||
func (w *udsWriter) SetWriteTimeout(d time.Duration) error {
|
||||
w.writeTimeout = d
|
||||
return nil
|
||||
}
|
||||
|
||||
// Write data to the UDS connection with write timeout and minimal error handling:
|
||||
// create the connection if nil, and destroy it if the statsd server has disconnected
|
||||
func (w *udsWriter) Write(data []byte) (int, error) {
|
||||
conn, err := w.ensureConnection()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
conn.SetWriteDeadline(time.Now().Add(w.writeTimeout))
|
||||
n, e := conn.Write(data)
|
||||
|
||||
if err, isNetworkErr := e.(net.Error); err != nil && (!isNetworkErr || !err.Temporary()) {
|
||||
// Statsd server disconnected, retry connecting at next packet
|
||||
w.unsetConnection()
|
||||
return 0, e
|
||||
}
|
||||
return n, e
|
||||
}
|
||||
|
||||
func (w *udsWriter) Close() error {
|
||||
if w.conn != nil {
|
||||
return w.conn.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *udsWriter) ensureConnection() (net.Conn, error) {
|
||||
// Check if we've already got a socket we can use
|
||||
w.RLock()
|
||||
currentConn := w.conn
|
||||
w.RUnlock()
|
||||
|
||||
if currentConn != nil {
|
||||
return currentConn, nil
|
||||
}
|
||||
|
||||
// Looks like we might need to connect - try again with write locking.
|
||||
w.Lock()
|
||||
defer w.Unlock()
|
||||
if w.conn != nil {
|
||||
return w.conn, nil
|
||||
}
|
||||
|
||||
newConn, err := net.Dial(w.addr.Network(), w.addr.String())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
w.conn = newConn
|
||||
return newConn, nil
|
||||
}
|
||||
|
||||
func (w *udsWriter) unsetConnection() {
|
||||
w.Lock()
|
||||
defer w.Unlock()
|
||||
w.conn = nil
|
||||
}
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
package statsd
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type worker struct {
|
||||
pool *bufferPool
|
||||
buffer *statsdBuffer
|
||||
sender *sender
|
||||
sync.Mutex
|
||||
|
||||
inputMetrics chan metric
|
||||
stop chan struct{}
|
||||
}
|
||||
|
||||
func newWorker(pool *bufferPool, sender *sender) *worker {
|
||||
return &worker{
|
||||
pool: pool,
|
||||
sender: sender,
|
||||
buffer: pool.borrowBuffer(),
|
||||
stop: make(chan struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
func (w *worker) startReceivingMetric(bufferSize int) {
|
||||
w.inputMetrics = make(chan metric, bufferSize)
|
||||
go w.pullMetric()
|
||||
}
|
||||
|
||||
func (w *worker) stopReceivingMetric() {
|
||||
w.stop <- struct{}{}
|
||||
}
|
||||
|
||||
func (w *worker) pullMetric() {
|
||||
for {
|
||||
select {
|
||||
case m := <-w.inputMetrics:
|
||||
w.processMetric(m)
|
||||
case <-w.stop:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (w *worker) processMetric(m metric) error {
|
||||
if !w.shouldSample(m.rate) {
|
||||
return nil
|
||||
}
|
||||
w.Lock()
|
||||
var err error
|
||||
if err = w.writeMetricUnsafe(m); err == errBufferFull {
|
||||
w.flushUnsafe()
|
||||
err = w.writeMetricUnsafe(m)
|
||||
}
|
||||
w.Unlock()
|
||||
return err
|
||||
}
|
||||
|
||||
func (w *worker) shouldSample(rate float64) bool {
|
||||
if rate < 1 && rand.Float64() > rate {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (w *worker) writeMetricUnsafe(m metric) error {
|
||||
switch m.metricType {
|
||||
case gauge:
|
||||
return w.buffer.writeGauge(m.namespace, m.globalTags, m.name, m.fvalue, m.tags, m.rate)
|
||||
case count:
|
||||
return w.buffer.writeCount(m.namespace, m.globalTags, m.name, m.ivalue, m.tags, m.rate)
|
||||
case histogram:
|
||||
return w.buffer.writeHistogram(m.namespace, m.globalTags, m.name, m.fvalue, m.tags, m.rate)
|
||||
case distribution:
|
||||
return w.buffer.writeDistribution(m.namespace, m.globalTags, m.name, m.fvalue, m.tags, m.rate)
|
||||
case set:
|
||||
return w.buffer.writeSet(m.namespace, m.globalTags, m.name, m.svalue, m.tags, m.rate)
|
||||
case timing:
|
||||
return w.buffer.writeTiming(m.namespace, m.globalTags, m.name, m.fvalue, m.tags, m.rate)
|
||||
case event:
|
||||
return w.buffer.writeEvent(*m.evalue, m.globalTags)
|
||||
case serviceCheck:
|
||||
return w.buffer.writeServiceCheck(*m.scvalue, m.globalTags)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (w *worker) flush() {
|
||||
w.Lock()
|
||||
w.flushUnsafe()
|
||||
w.Unlock()
|
||||
}
|
||||
|
||||
// flush the current buffer. Lock must be held by caller.
|
||||
// flushed buffer written to the network asynchronously.
|
||||
func (w *worker) flushUnsafe() {
|
||||
if len(w.buffer.bytes()) > 0 {
|
||||
w.sender.send(w.buffer)
|
||||
w.buffer = w.pool.borrowBuffer()
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user