Skip to content

Commit

Permalink
Improved metrics setup
Browse files Browse the repository at this point in the history
  • Loading branch information
nhhagen committed Dec 16, 2024
1 parent 2b33af6 commit c113094
Show file tree
Hide file tree
Showing 6 changed files with 342 additions and 1 deletion.
8 changes: 7 additions & 1 deletion bootstrap2.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -118,5 +124,5 @@ func startProfiler(options *options) error {
func stop() error {
tracer.Stop()
profiler.Stop()
return nil
return metrics.Flush()
}
2 changes: 2 additions & 0 deletions internal/env.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
130 changes: 130 additions & 0 deletions metrics/metrics.go
Original file line number Diff line number Diff line change
@@ -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))
}
}
107 changes: 107 additions & 0 deletions metrics/noop.go
Original file line number Diff line number Diff line change
@@ -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
}
86 changes: 86 additions & 0 deletions metrics/options.go
Original file line number Diff line number Diff line change
@@ -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
}
}
10 changes: 10 additions & 0 deletions options.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand All @@ -14,6 +15,7 @@ const (
type options struct {
enableExtraProfiling bool
errorHandler errors.ErrorHandler
metricOptions []metrics.Option
}

func resolveOptions(opts []Option) (*options, error) {
Expand Down Expand Up @@ -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 {
Expand Down

0 comments on commit c113094

Please sign in to comment.