package metrics import ( "fmt" "os" "time" "github.com/alexcesaro/statsd" "github.com/pkg/errors" ) type config struct { prefix string addr string errorHandler func(error) } // Option is a type for configuring a StatsDReporter type Option func(*config) error // Address sets the address where we send metrics // The default is "telegraf:8125". This is so "telegraf" // can be mounted into docker containers as a network interface func Address(addr string) Option { return Option(func(c *config) error { c.addr = addr return nil }) } // AddressFromEnv reads the address from the specified env variable. // If the specified env variable is not set it will return an error. func AddressFromEnv(varName string) Option { value, isSet := os.LookupEnv(varName) if !isSet { return Option(func(c *config) error { return fmt.Errorf("missing variable (%s) for metrics.AddressFromEnv", varName) }) } return Option(func(c *config) error { c.addr = value return nil }) } // ErrorHandler sets the function called when an error // happens when sending metrics to the remote metrics store func ErrorHandler(h func(error)) Option { return Option(func(c *config) error { c.errorHandler = h return nil }) } // StatsDReporter is a concrete implementation of Reporter that sends metrics to a statsd service. // It will buffer and periodically flush metrics. type StatsDReporter struct { client client errorHandler func(error) } // MetricTimer is a concrete implementation of Timer that records a timing. type MetricTimer struct { Reporter *StatsDReporter Start time.Time Name string Tags []Meta } // Record internally calls RecordTiming to submit the MetricsTimer. func (t *MetricTimer) Record() { t.Reporter.RecordTiming(t.Name, time.Since(t.Start), t.Tags...) } // EnableStatsDReporter will create a new StatsDReporter as the DefaultReporter or return an error. func EnableStatsDReporter(prefix string, opts ...Option) error { r, err := NewReporter(prefix, opts...) if err != nil { return err } DefaultReporter = r return nil } // NewReporter creates a new StatsDReporter. // If a connection cannot be estabilished with the remote metrics service // a DiscardReporter and and error will be returned. func NewReporter(prefix string, opts ...Option) (Reporter, error) { cfg := &config{ prefix: prefix, addr: "telegraf:8125", errorHandler: func(error) {}, } for _, o := range opts { if err := o(cfg); err != nil { return DiscardReporter, err } } client, err := statsd.New( statsd.Prefix(cfg.prefix), statsd.Address(cfg.addr), statsd.TagsFormat(statsd.InfluxDB), statsd.ErrorHandler(cfg.errorHandler), ) if err != nil { return DiscardReporter, errors.Wrap(err, "error creating client/connection for StatsDReporter") } reporter := &StatsDReporter{ client: client, errorHandler: cfg.errorHandler, } return reporter, nil } func mapMetasToSliceMetas(tags []Meta) []string { var kvs []string for _, t := range tags { kvs = append(kvs, t.Key, t.Value) } return kvs } func (mr *StatsDReporter) getClientWithMetas(tags ...Meta) client { if tagsInSlice := mapMetasToSliceMetas(tags); len(tagsInSlice) > 0 { return mr.client.Clone( statsd.Tags(tagsInSlice...), ) } return mr.client } // IncCounter increments a metric count. func (mr *StatsDReporter) IncCounter(name string, tags ...Meta) { if name == "" { mr.errorHandler(errors.New("missing name for IncCounter func")) return } mr.getClientWithMetas(tags...).Increment(name) } // AddToCounter adds a given value to a metric count. func (mr *StatsDReporter) AddToCounter(name string, value int64, tags ...Meta) { if name == "" { mr.errorHandler(errors.New("missing name for AddToCounter func")) return } mr.getClientWithMetas(tags...).Count(name, value) } // UpdateGauge sets a new value for a metric. func (mr *StatsDReporter) UpdateGauge(name string, value int64, tags ...Meta) { if name == "" { mr.errorHandler(errors.New("missing name for UpdateGauge func")) return } mr.getClientWithMetas(tags...).Gauge(name, value) } // RecordHistogram submits a new timing metric. func (mr *StatsDReporter) RecordHistogram(name string, v float64, tags ...Meta) { if name == "" { mr.errorHandler(errors.New("missing name for RecordHistogram func")) return } mr.getClientWithMetas(tags...).Histogram(name, v) } // RecordTiming submits a new timing metric. func (mr *StatsDReporter) RecordTiming(name string, d time.Duration, tags ...Meta) { if name == "" { mr.errorHandler(errors.New("missing name for RecordTiming func")) return } mr.getClientWithMetas(tags...).Timing(name, d.Nanoseconds()/1000000) } // NewTimer creates a MetricsTimer that can be recorded later. // A common pattern is to create and immediately defer the closure of a Timer: // // defer reporter.NewTimer("http_request", Tag("protocol", "http2")).Record() func (mr *StatsDReporter) NewTimer(name string, tags ...Meta) Timer { return &MetricTimer{ Reporter: mr, Start: time.Now(), Name: name, Tags: tags, } } // Flush flushes all metrics that are currently buffered. func (mr *StatsDReporter) Flush() { mr.client.Flush() } // Close will close the StatsDReporter and all open statsd connections. func (mr *StatsDReporter) Close() error { mr.client.Close() return nil } var _ Reporter = &StatsDReporter{}