210 lines
5.3 KiB
Go
Executable File
210 lines
5.3 KiB
Go
Executable File
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{}
|