From 5f41f276c15d68f2fdcb214b68f25ba1c3b71617 Mon Sep 17 00:00:00 2001 From: Sean Marciniak Date: Wed, 10 Jan 2024 17:22:29 +1030 Subject: [PATCH] Adding OTLP backend configuration --- pkg/backends/otlp/client.go | 17 ++++-- pkg/backends/otlp/config.go | 89 ++++++++++++++++++++++++++++++++ pkg/backends/otlp/config_test.go | 70 +++++++++++++++++++++++++ 3 files changed, 173 insertions(+), 3 deletions(-) create mode 100644 pkg/backends/otlp/config.go create mode 100644 pkg/backends/otlp/config_test.go diff --git a/pkg/backends/otlp/client.go b/pkg/backends/otlp/client.go index 2c4d1e25..faa31ff8 100644 --- a/pkg/backends/otlp/client.go +++ b/pkg/backends/otlp/client.go @@ -3,7 +3,11 @@ package otlp import ( "context" + "github.com/sirupsen/logrus" + "github.com/spf13/viper" + "github.com/atlassian/gostatsd" + "github.com/atlassian/gostatsd/pkg/transport" ) const ( @@ -13,12 +17,19 @@ const ( // Client contains additional meta data in order // to export values as OTLP metrics. // The zero value is not safe to use. -type Client struct { - resources []string -} +type Client struct{} var _ gostatsd.Backend = (*Client)(nil) +func NewClientFromViper(v *viper.Viper, logger logrus.FieldLogger, pool *transport.TransportPool) (gostatsd.Backend, error) { + _, err := NewConfig(v) + if err != nil { + return nil, err + } + + return Client{}, nil +} + func (Client) Name() string { return namedBackend } func (Client) SendEvent(ctx context.Context, e *gostatsd.Event) error { diff --git a/pkg/backends/otlp/config.go b/pkg/backends/otlp/config.go new file mode 100644 index 00000000..05e02ce3 --- /dev/null +++ b/pkg/backends/otlp/config.go @@ -0,0 +1,89 @@ +package otlp + +import ( + "errors" + "runtime" + + "github.com/spf13/viper" + "go.uber.org/multierr" +) + +const ( + ConversionAsGauge = "AsGauge" + ConversionAsHistogram = "AsHistogram" +) + +type Config struct { + // Endpoint (Required) is the FQDN with path for the metrics ingestion endpoint + Endpoint string `mapstructure:"endpoint"` + // MaxRequests (Optional, default: cpu.count * 2) is the upper limit on the number of inflight requests + MaxRequests int `mapstructure:"max_requests"` + // ResourceKeys (Optional) is used to extract values from provided tags + // to apply to all values within a resource instead within each attribute. + // Strongly encouraged to allow down stream consumers to + // process based on values defined at the top level resource. + ResourceKeys []string `mapstructure:"resource_keys"` + // TimerConversion (Optional, Default: AsGauge) determines if a timers are emitted as one of the following: + // - AsGauges emits each calcauted value (min, max, sum, count, upper, upper_99, etc...) as a guage value + // - AsHistograms each timer is set as an empty bucket histogram with only Min, Max, Sum, and Count set + TimerConversion string `mapstructure:"timer_conversion"` + // HistogramConversion (Optional, Default: AsGauge) determines how a GoStatsD histogram should be converted into OTLP. + // The allowed values for this are: + // - AsGauges: sends each bucket count (including +inf) as a guage value and the bucket boundry as a tag + // - AsHistogram: sends each histogram as a single Histogram value with bucket and statistics calculated. + HistogramConversion string `mapstructure:"histogram_conversion"` + // Transport (Optional, Default: "default") is used to reference to configured transport + // to be used for this backend + Transport string `mapstructure:"transport"` + // UserAgent (Optional, default: "gostatsd") allows you to set the + // user agent header when making requests. + UserAgent string `mapstructure:"user_agent"` +} + +func newDefaultConfig() Config { + return Config{ + Transport: "default", + MaxRequests: runtime.NumCPU() * 2, + TimerConversion: "AsGauge", + HistogramConversion: "AsGauge", + UserAgent: "gostatsd", + } +} + +func NewConfig(v *viper.Viper) (*Config, error) { + cfg := newDefaultConfig() + if err := v.Unmarshal(&cfg); err != nil { + return nil, err + } + if err := cfg.Validate(); err != nil { + return nil, err + } + return &cfg, nil +} + +func (c Config) Validate() (errs error) { + if c.Endpoint == "" { + errs = multierr.Append(errs, errors.New("no endpoint defined")) + } + if c.MaxRequests <= 0 { + errs = multierr.Append(errs, errors.New("max request must be a positive value")) + } + if c.Transport == "" { + errs = multierr.Append(errs, errors.New("no transport defined")) + } + + conversion := map[string]struct{}{ + ConversionAsGauge: {}, + ConversionAsHistogram: {}, + } + + if _, ok := conversion[c.TimerConversion]; !ok { + errs = multierr.Append(errs, errors.New(`time conversion must be one of ["AsGauge", "AsHistogram"]`)) + } + + if _, ok := conversion[c.HistogramConversion]; !ok { + errs = multierr.Append(errs, errors.New(`histogram conversion must be one of ["AsGauge", "AsHistogram"]`)) + } + + return errs +} diff --git a/pkg/backends/otlp/config_test.go b/pkg/backends/otlp/config_test.go new file mode 100644 index 00000000..359d89bc --- /dev/null +++ b/pkg/backends/otlp/config_test.go @@ -0,0 +1,70 @@ +package otlp + +import ( + "testing" + + "github.com/spf13/viper" + "github.com/stretchr/testify/assert" +) + +func TestNewConfig(t *testing.T) { + t.Parallel() + + for _, tc := range []struct { + name string + v *viper.Viper + expect *Config + errVal string + }{ + { + name: "empty configuration", + v: viper.New(), + expect: nil, + errVal: "no endpoint defined", + }, + { + name: "min configuration defined", + v: func() *viper.Viper { + v := viper.New() + v.SetDefault("endpoint", "http://local") + v.SetDefault("max_requests", 1) + return v + }(), + expect: &Config{ + Endpoint: "http://local", + MaxRequests: 1, + TimerConversion: "AsGauge", + HistogramConversion: "AsGauge", + Transport: "default", + UserAgent: "gostatsd", + }, + errVal: "", + }, + { + name: "invalid options", + v: func() *viper.Viper { + v := viper.New() + v.SetDefault("max_requests", 0) + v.SetDefault("transport", "") + v.SetDefault("timer_conversion", "values") + v.SetDefault("histogram_conversion", "values") + return v + }(), + expect: nil, + errVal: "no endpoint defined; max request must be a positive value; no transport defined; time conversion must be one of [\"AsGauge\", \"AsHistogram\"]; histogram conversion must be one of [\"AsGauge\", \"AsHistogram\"]", + }, + } { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + cfg, err := NewConfig(tc.v) + assert.Equal(t, tc.expect, cfg, "Must match the expected configuration") + if tc.errVal != "" { + assert.EqualError(t, err, tc.errVal, "Must match the expected error") + } else { + assert.NoError(t, err, "Must not error") + } + }) + } +}