diff --git a/bootstrap2.go b/bootstrap2.go index 57e4b16..0265c0f 100644 --- a/bootstrap2.go +++ b/bootstrap2.go @@ -6,6 +6,7 @@ import ( "fmt" "github.com/coopnorge/go-datadog-lib/v2/internal" + "github.com/coopnorge/go-datadog-lib/v2/metrics" datadogLogger "github.com/coopnorge/go-logger/adapter/datadog" "gopkg.in/DataDog/dd-trace-go.v1/ddtrace" "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer" @@ -88,6 +89,11 @@ func start(options *options) error { if err != nil { return err } + metricOptions := append([]metrics.Option{metrics.WithErrorHandler(options.errorHandler)}, options.metricOptions...) + err = metrics.GlobalSetup(metricOptions...) + if err != nil { + return err + } return nil } @@ -118,5 +124,5 @@ func startProfiler(options *options) error { func stop() error { tracer.Stop() profiler.Stop() - return nil + return metrics.Flush() } diff --git a/internal/env.go b/internal/env.go index 17026b9..80ebec8 100644 --- a/internal/env.go +++ b/internal/env.go @@ -11,6 +11,8 @@ const ( DatadogDisable = "DD_DISABLE" // DatadogEnableExtraProfiling is the environment variable key for whether to enable extra profiling or not. DatadogEnableExtraProfiling = "DD_ENABLE_EXTRA_PROFILING" + // DatadogEnableMetrics is the environment variable key for whether to enable custom metrics collection. + DatadogEnableMetrics = "DD_ENABLE_METRICS" // DatadogEnvironment is the environment variable key determining the Datadog Environment to use. DatadogEnvironment = "DD_ENV" diff --git a/metrics/metrics.go b/metrics/metrics.go new file mode 100644 index 0000000..1affe0a --- /dev/null +++ b/metrics/metrics.go @@ -0,0 +1,130 @@ +package metrics + +import ( + "fmt" + "sync" + "time" + + "github.com/DataDog/datadog-go/v5/statsd" + "github.com/coopnorge/go-datadog-lib/v2/errors" + "github.com/coopnorge/go-datadog-lib/v2/internal" +) + +var ( + setupOnce sync.Once + setupErr error + statsdClient statsd.ClientInterface + errorHandler errors.ErrorHandler + cfg *config +) + +// GlobalSetup configures the Dogstatsd Client. GlobalSetup is intended to be +// called from coopdatadog.Start(), but can be called directly. +func GlobalSetup(options ...Option) error { + setupOnce.Do(func() { + if internal.IsDatadogDisabled() { + statsdClient = &noopClient{} + return + } + + cfg, setupErr = resolveConfig(options) + if setupErr != nil { + return + } + + if !cfg.enableMetrics { + statsdClient = &noopClient{} + return + } + + statsdClient, setupErr = statsd.New(cfg.dsdEndpoint, statsd.WithTags(cfg.tags)) + if setupErr != nil { + return + } + }) + return setupErr +} + +// Flush forces a flush of all the queued dogstatsd payloads. +func Flush() error { + err := statsdClient.Flush() + if err != nil { + return fmt.Errorf("failed to flush: %w", err) + } + return nil +} + +// Gauge measures the value of a metric at a particular time. +func Gauge(name string, value float64, tags ...string) { + err := statsdClient.Gauge(name, value, tags, cfg.metricSampleRate) + if err != nil { + errorHandler(fmt.Errorf("failed to send Gauge: %w", err)) + } +} + +// Count tracks how many times something happened per second. +func Count(name string, value int64, tags ...string) { + err := statsdClient.Count(name, value, tags, cfg.metricSampleRate) + if err != nil { + errorHandler(fmt.Errorf("failed to to send Count: %w", err)) + } +} + +// Histogram tracks the statistical distribution of a set of values on each host. +func Histogram(name string, value float64, tags ...string) { + err := statsdClient.Histogram(name, value, tags, cfg.metricSampleRate) + if err != nil { + errorHandler(fmt.Errorf("failed to to send Histogram: %w", err)) + } +} + +// Distribution tracks the statistical distribution of a set of values across your infrastructure. +// +// It is recommended to use `WithMaxBufferedMetricsPerContext` to avoid dropping metrics at high throughput, `cfg.metricSampleRate` can +// also be used to limit the load. Both options can *not* be used together. +func Distribution(name string, value float64, tags ...string) { + err := statsdClient.Distribution(name, value, tags, cfg.metricSampleRate) + if err != nil { + errorHandler(fmt.Errorf("failed to to send Distribution: %w", err)) + } +} + +// Decr is just Count of -1 +func Decr(name string, tags ...string) { + Count(name, -1, tags...) +} + +// Incr is just Count of 1 +func Incr(name string, tags ...string) { + Count(name, 1, tags...) +} + +// Set counts the number of unique elements in a group. +func Set(name string, value string, tags ...string) { + err := statsdClient.Set(name, value, tags, cfg.metricSampleRate) + if err != nil { + errorHandler(fmt.Errorf("failed to to send Set: %w", err)) + } +} + +// Timing sends timing information, it is an alias for TimeInMilliseconds +func Timing(name string, value time.Duration, tags ...string) { + TimeInMilliseconds(name, value.Seconds()*1000, tags...) +} + +// 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 TimeInMilliseconds(name string, value float64, tags ...string) { + err := statsdClient.TimeInMilliseconds(name, value, tags, cfg.metricSampleRate) + if err != nil { + errorHandler(fmt.Errorf("failed to to send TimeInMilliseconds: %w", err)) + } +} + +// SimpleEvent sends an event with the provided title and text. +func SimpleEvent(title, text string) { + err := statsdClient.SimpleEvent(title, text) + if err != nil { + errorHandler(fmt.Errorf("failed to send Event: %w", err)) + } +} diff --git a/metrics/noop.go b/metrics/noop.go new file mode 100644 index 0000000..1c631a2 --- /dev/null +++ b/metrics/noop.go @@ -0,0 +1,107 @@ +package metrics + +import ( + "time" + + "github.com/DataDog/datadog-go/v5/statsd" +) + +// Verify that Client implements the ClientInterface. +var _ statsd.ClientInterface = &noopClient{} + +type noopClient struct{} + +// Close implements statsd.ClientInterface. +func (n *noopClient) Close() error { + return nil +} + +// Count implements statsd.ClientInterface. +func (n *noopClient) Count(_ string, _ int64, _ []string, _ float64) error { + return nil +} + +// CountWithTimestamp implements statsd.ClientInterface. +func (n *noopClient) CountWithTimestamp(_ string, _ int64, _ []string, _ float64, _ time.Time) error { + return nil +} + +// Decr implements statsd.ClientInterface. +func (n *noopClient) Decr(_ string, _ []string, _ float64) error { + return nil +} + +// Distribution implements statsd.ClientInterface. +func (n *noopClient) Distribution(_ string, _ float64, _ []string, _ float64) error { + return nil +} + +// Event implements statsd.ClientInterface. +func (n *noopClient) Event(_ *statsd.Event) error { + return nil +} + +// Flush implements statsd.ClientInterface. +func (n *noopClient) Flush() error { + return nil +} + +// Gauge implements statsd.ClientInterface. +func (n *noopClient) Gauge(_ string, _ float64, _ []string, _ float64) error { + return nil +} + +// GaugeWithTimestamp implements statsd.ClientInterface. +func (n *noopClient) GaugeWithTimestamp(_ string, _ float64, _ []string, _ float64, _ time.Time) error { + return nil +} + +// GetTelemetry implements statsd.ClientInterface. +func (n *noopClient) GetTelemetry() statsd.Telemetry { + return statsd.Telemetry{} +} + +// Histogram implements statsd.ClientInterface. +func (n *noopClient) Histogram(_ string, _ float64, _ []string, _ float64) error { + return nil +} + +// Incr implements statsd.ClientInterface. +func (n *noopClient) Incr(_ string, _ []string, _ float64) error { + return nil +} + +// IsClosed implements statsd.ClientInterface. +func (n *noopClient) IsClosed() bool { + return true +} + +// ServiceCheck implements statsd.ClientInterface. +func (n *noopClient) ServiceCheck(_ *statsd.ServiceCheck) error { + return nil +} + +// Set implements statsd.ClientInterface. +func (n *noopClient) Set(_ string, _ string, _ []string, _ float64) error { + return nil +} + +// SimpleEvent implements statsd.ClientInterface. +func (n *noopClient) SimpleEvent(_ string, _ string) error { + return nil +} + +// SimpleServiceCheck implements statsd.ClientInterface. +func (n *noopClient) SimpleServiceCheck(_ string, _ statsd.ServiceCheckStatus) error { + return nil +} + +// TimeInMilliseconds implements statsd.ClientInterface. +func (n *noopClient) TimeInMilliseconds(_ string, _ float64, _ []string, _ float64) error { + return nil +} + +// Timing implements statsd.ClientInterface. +func (n *noopClient) Timing(_ string, _ time.Duration, _ []string, _ float64) error { + return nil +} diff --git a/metrics/options.go b/metrics/options.go new file mode 100644 index 0000000..6ce5c7d --- /dev/null +++ b/metrics/options.go @@ -0,0 +1,86 @@ +package metrics + +import ( + "fmt" + "os" + + "github.com/coopnorge/go-datadog-lib/v2/errors" + "github.com/coopnorge/go-datadog-lib/v2/internal" + "github.com/coopnorge/go-logger" +) + +const ( + defaultEnableMetrics = true + defaultMetricSampleRate = 1 +) + +// Option is used to configure the behaviour of the metrics integration. +type Option func(*config) error + +type config struct { + enableMetrics bool + errorHandler errors.ErrorHandler + dsdEndpoint string + metricSampleRate float64 + tags []string +} + +func resolveConfig(options []Option) (*config, error) { + err := internal.VerifyEnvVarsSet( + internal.DatadogDSDEndpoint, + internal.DatadogEnvironment, + internal.DatadogService, + internal.DatadogVersion, + ) + if err != nil { + return nil, err + } + cfg := &config{ + enableMetrics: internal.GetBool(internal.DatadogEnableMetrics, defaultEnableMetrics), + errorHandler: func(err error) { + logger.WithError(err).Error(err.Error()) + }, + dsdEndpoint: os.Getenv(internal.DatadogDSDEndpoint), + metricSampleRate: defaultMetricSampleRate, + tags: []string{ + fmt.Sprintf("environment:%s", os.Getenv(internal.DatadogEnvironment)), + fmt.Sprintf("service:%s", os.Getenv(internal.DatadogService)), + fmt.Sprintf("version:%s", os.Getenv(internal.DatadogVersion)), + }, + } + + for _, option := range options { + err = option(cfg) + if err != nil { + return nil, err + } + } + + return cfg, nil +} + +// WithTags sets the tags that are sent with every metric, shorthand for +// statsd.WithTags() +func WithTags(tags ...string) Option { + return func(cfg *config) error { + cfg.tags = tags + return nil + } +} + +// WithMetricSampleRate sets the sampling rate for metrics +func WithMetricSampleRate(rate float64) Option { + return func(cfg *config) error { + cfg.metricSampleRate = rate + return nil + } +} + +// WithErrorHandler allows for setting a custom ErrorHandler to be called on +// function that may error but does not return an error +func WithErrorHandler(handler errors.ErrorHandler) Option { + return func(cfg *config) error { + cfg.errorHandler = handler + return nil + } +} diff --git a/options.go b/options.go index 62a632a..236224c 100644 --- a/options.go +++ b/options.go @@ -3,6 +3,7 @@ package coopdatadog import ( "github.com/coopnorge/go-datadog-lib/v2/errors" "github.com/coopnorge/go-datadog-lib/v2/internal" + "github.com/coopnorge/go-datadog-lib/v2/metrics" "github.com/coopnorge/go-logger" ) @@ -14,6 +15,7 @@ const ( type options struct { enableExtraProfiling bool errorHandler errors.ErrorHandler + metricOptions []metrics.Option } func resolveOptions(opts []Option) (*options, error) { @@ -44,6 +46,14 @@ func withConfigFromEnvVars() Option { } } +// WithMetricsOptions allows for passing the options for setting up metrics +func WithMetricsOptions(metricOptions ...metrics.Option) Option { + return func(options *options) error { + options.metricOptions = metricOptions + return nil + } +} + // WithErrorHandler allows for setting a custom ErrorHandler to be called on // function that may error but does not return an error func WithErrorHandler(handler errors.ErrorHandler) Option {