From 106778d02c90f0e7eb2e466f2bc378033be0e3c6 Mon Sep 17 00:00:00 2001 From: Niels Henrik Hagen Date: Thu, 19 Dec 2024 08:05:46 +0100 Subject: [PATCH 1/4] feat: Improved metrics setup Closes: https://github.com/coopnorge/go-datadog-lib/issues/433 --- bootstrap.go | 8 ++- docs/index.md | 68 +++++++++++----------- metric/datadog.go | 2 + metric/metric.go | 5 ++ metrics/example_test.go | 32 +++++++++++ metrics/metrics.go | 122 ++++++++++++++++++++++++++++++++++++++++ metrics/noop.go | 107 +++++++++++++++++++++++++++++++++++ metrics/options.go | 84 +++++++++++++++++++++++++++ metrics/options_test.go | 22 ++++++++ options.go | 10 ++++ 10 files changed, 424 insertions(+), 36 deletions(-) create mode 100644 metrics/example_test.go create mode 100644 metrics/metrics.go create mode 100644 metrics/noop.go create mode 100644 metrics/options.go create mode 100644 metrics/options_test.go diff --git a/bootstrap.go b/bootstrap.go index 86fc09f..0a56839 100644 --- a/bootstrap.go +++ b/bootstrap.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" @@ -69,6 +70,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 } @@ -99,5 +105,5 @@ func startProfiler(options *options) error { func stop() error { tracer.Stop() profiler.Stop() - return nil + return metrics.Flush() } diff --git a/docs/index.md b/docs/index.md index e320786..8e8aa72 100644 --- a/docs/index.md +++ b/docs/index.md @@ -532,51 +532,49 @@ func main() { } ``` -## Metric - Datadog StatsD +## Metrics -Datadog supports custom metrics that you can utilize depending on the -application. +The package `github.com/coopnorge/go-datadog-lib/v2/metrics` contains function +to send telemetry. Before sending metrics `go-datadog-lib` has to be +[initialized](#application-setup). -For example, you could use it to track value of cart in side e-commerce shop. +Use cases: -Or you could register events for auth attempts. - -All depends on the case and what you're looking forward to achieve. - -### How to use StatsD in Go - Datadog - -There is created an abstract client that simply connects to Datadog StatsD -service. - -Also, you will already implement simple metric the collector that you can -extend or just use it to send your events and measurements. - -### How to initialize Go - Datadog - -To prepare the configuration you need to look at the `Setup` section. After -that you can create new instance of Datadog client for DD StatsD. +- track value of carts in an e-commerce shop. +- count failed authentication attempts. ```go -package your_pkg +package main import ( - "github.com/coopnorge/go-datadog-lib/v2/config" - "github.com/coopnorge/go-datadog-lib/v2/metric" + "context" + + coopdatadog "github.com/coopnorge/go-datadog-lib/v2" + "github.com/coopnorge/go-datadog-lib/v2/metrics" ) -func MyServiceContainer(ddCfg *config.DatadogConfig) error { - // After that you will have pure DD StatsD client - ddClient := metrics.NewDatadogMetrics(ddCfg) +func main() { + err := run() + if err != nil { + panic(err) + } +} - // If you need simple metric collector then create - ddMetricCollector, ddMetricCollectorErr := metrics.NewBaseMetricCollector(ddClient) - if ddMetricCollectorErr != nil { - // Handle error / log error - } - // ddMetricCollector -> *BaseMetricCollector allows you to send metrics to Datadog - - // ensure the metrics are sent before the program is terminated - defer ddMetricCollector.GracefulShutdown() +func run() error { + stop, err := coopdatadog.Start(context.Background()) + if err != nil { + return err + } + defer func() { + err := stop() + if err != nil { + panic(err) + } + }() + + metrics.Incr("my-metric") + + return nil } ``` diff --git a/metric/datadog.go b/metric/datadog.go index 79673e7..3e7bc08 100644 --- a/metric/datadog.go +++ b/metric/datadog.go @@ -30,6 +30,8 @@ type ( ) // NewDatadogMetrics instance required to have cfg config.DatadogParameters to get information about service and optional orgPrefix to append into for metric name +// +// Deprecated: Use coopdatadog.Start() instead. func NewDatadogMetrics(cfg config.DatadogParameters, orgPrefix string) (*DatadogMetrics, error) { var ddClient *statsd.Client var ddClientErr error diff --git a/metric/metric.go b/metric/metric.go index ab31c76..5dba146 100644 --- a/metric/metric.go +++ b/metric/metric.go @@ -1,3 +1,6 @@ +// Package metric implements custom metrics with Dogstatsd +// +// Deprecated: use metrics instead package metric import ( @@ -51,6 +54,8 @@ func NewBaseMetricCollector(dm *DatadogMetrics) *BaseMetricCollector { } // AddMetric related to name with given value +// +// Deprecated: Use functions from the metrics package func (m BaseMetricCollector) AddMetric(ctx context.Context, d Data) { if m.DatadogMetrics == nil || m.DatadogMetrics.GetClient() == nil { return diff --git a/metrics/example_test.go b/metrics/example_test.go new file mode 100644 index 0000000..bb9a7ba --- /dev/null +++ b/metrics/example_test.go @@ -0,0 +1,32 @@ +package metrics_test + +import ( + "context" + + coopdatadog "github.com/coopnorge/go-datadog-lib/v2" + "github.com/coopnorge/go-datadog-lib/v2/metrics" +) + +func Example() { + err := run() + if err != nil { + panic(err) + } +} + +func run() error { + stop, err := coopdatadog.Start(context.Background()) + if err != nil { + return err + } + defer func() { + err := stop() + if err != nil { + panic(err) + } + }() + + metrics.Incr("my-metric") + + return nil +} diff --git a/metrics/metrics.go b/metrics/metrics.go new file mode 100644 index 0000000..ad9f4dc --- /dev/null +++ b/metrics/metrics.go @@ -0,0 +1,122 @@ +// Package metrics implements custom metrics with Dogstatsd +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 + opts *options +) + +// 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 + } + + opts, setupErr = resolveOptions(options) + if setupErr != nil { + return + } + + statsdClient, setupErr = statsd.New(opts.dsdEndpoint, statsd.WithTags(opts.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, opts.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, opts.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, opts.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. +func Distribution(name string, value float64, tags ...string) { + err := statsdClient.Distribution(name, value, tags, opts.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, opts.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. +func TimeInMilliseconds(name string, value float64, tags ...string) { + err := statsdClient.TimeInMilliseconds(name, value, tags, opts.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..17ce86c --- /dev/null +++ b/metrics/options.go @@ -0,0 +1,84 @@ +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(*options) error + +type options struct { + errorHandler errors.ErrorHandler + dsdEndpoint string + metricSampleRate float64 + tags []string +} + +func resolveOptions(opts []Option) (*options, error) { + err := internal.VerifyEnvVarsSet( + internal.DatadogDSDEndpoint, + internal.DatadogEnvironment, + internal.DatadogService, + internal.DatadogVersion, + ) + if err != nil { + return nil, err + } + options := &options{ + 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 opts { + err = option(options) + if err != nil { + return nil, err + } + } + + return options, nil +} + +// WithTags sets the tags that are sent with every metric, shorthand for +// statsd.WithTags() +func WithTags(tags ...string) Option { + return func(options *options) error { + options.tags = append(options.tags, tags...) + return nil + } +} + +// WithMetricSampleRate sets the sampling rate for metrics +func WithMetricSampleRate(rate float64) Option { + return func(options *options) error { + options.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(options *options) error { + options.errorHandler = handler + return nil + } +} diff --git a/metrics/options_test.go b/metrics/options_test.go new file mode 100644 index 0000000..cc17fb7 --- /dev/null +++ b/metrics/options_test.go @@ -0,0 +1,22 @@ +package metrics + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestWithTags(t *testing.T) { + options := &options{ + tags: []string{"a", "b"}, + } + + err := WithTags("c")(options) + assert.NoError(t, err) + + assert.Equal(t, []string{"a", "b", "c"}, options.tags) + + err = WithTags("d")(options) + assert.NoError(t, err) + assert.Equal(t, []string{"a", "b", "c", "d"}, options.tags) +} 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 { From 016d7a26b6478bfe6665215bf0557f83d91b60f8 Mon Sep 17 00:00:00 2001 From: Niels Henrik Hagen Date: Thu, 19 Dec 2024 15:05:16 +0100 Subject: [PATCH 2/4] Improve option doc --- metrics/options.go | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/metrics/options.go b/metrics/options.go index 17ce86c..e068be2 100644 --- a/metrics/options.go +++ b/metrics/options.go @@ -66,8 +66,14 @@ func WithTags(tags ...string) Option { } } -// WithMetricSampleRate sets the sampling rate for metrics -func WithMetricSampleRate(rate float64) Option { +// WithMetricSampling configures how many metrics are sent to Datadog by +// setting a sampling rate. +// +//For high-volume metrics, sampling helps reduce network traffic while +//maintaining statistical accuracy. Datadog automatically scales up the +//received values to compensate for sampling. Rate must be between 0 and 1, +//where 1.0 sends all metrics and 0.5 sends half. +func WithMetricSampling(rate float64) Option { return func(options *options) error { options.metricSampleRate = rate return nil From e5df1c163d8d404387c681afab2f40e38c1ed5ff Mon Sep 17 00:00:00 2001 From: Niels Henrik Hagen Date: Thu, 19 Dec 2024 22:11:28 +0100 Subject: [PATCH 3/4] Fix formatting --- metrics/options.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/metrics/options.go b/metrics/options.go index e068be2..247ecd9 100644 --- a/metrics/options.go +++ b/metrics/options.go @@ -69,10 +69,10 @@ func WithTags(tags ...string) Option { // WithMetricSampling configures how many metrics are sent to Datadog by // setting a sampling rate. // -//For high-volume metrics, sampling helps reduce network traffic while -//maintaining statistical accuracy. Datadog automatically scales up the -//received values to compensate for sampling. Rate must be between 0 and 1, -//where 1.0 sends all metrics and 0.5 sends half. +// For high-volume metrics, sampling helps reduce network traffic while +// maintaining statistical accuracy. Datadog automatically scales up the +// received values to compensate for sampling. Rate must be between 0 and 1, +// where 1.0 sends all metrics and 0.5 sends half. func WithMetricSampling(rate float64) Option { return func(options *options) error { options.metricSampleRate = rate From 30a3a3ce0208d833471308eae69195bed463352c Mon Sep 17 00:00:00 2001 From: Niels Henrik Hagen Date: Thu, 19 Dec 2024 22:43:42 +0100 Subject: [PATCH 4/4] Remove WithMetricSampling --- metrics/options.go | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/metrics/options.go b/metrics/options.go index 247ecd9..dab160f 100644 --- a/metrics/options.go +++ b/metrics/options.go @@ -66,20 +66,6 @@ func WithTags(tags ...string) Option { } } -// WithMetricSampling configures how many metrics are sent to Datadog by -// setting a sampling rate. -// -// For high-volume metrics, sampling helps reduce network traffic while -// maintaining statistical accuracy. Datadog automatically scales up the -// received values to compensate for sampling. Rate must be between 0 and 1, -// where 1.0 sends all metrics and 0.5 sends half. -func WithMetricSampling(rate float64) Option { - return func(options *options) error { - options.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 {