568 lines
16 KiB
Go
568 lines
16 KiB
Go
// 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()
|
|
}
|