Vendor temporarily

This commit is contained in:
Paul Tyng committed 2020-10-14 11:34:14 -04:00
1 parent dd8e532d18
commit 1c02ed9115
4708 files changed
+1226392 -12

No files matched your search

+19
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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()
}
}