diff --git a/exporter/datadogexporter/factory.go b/exporter/datadogexporter/factory.go index 526db8ca0c4e..91a4eaf7aaf6 100644 --- a/exporter/datadogexporter/factory.go +++ b/exporter/datadogexporter/factory.go @@ -133,7 +133,12 @@ func createMetricsExporter( return nil } } else { - pushMetricsFn = newMetricsExporter(ctx, set, cfg).PushMetricsData + exp, err := newMetricsExporter(ctx, set, cfg) + if err != nil { + cancel() + return nil, err + } + pushMetricsFn = exp.PushMetricsData } exporter, err := exporterhelper.NewMetricsExporter( diff --git a/exporter/datadogexporter/internal/translator/config.go b/exporter/datadogexporter/internal/translator/config.go new file mode 100644 index 000000000000..9e85f31d2bb0 --- /dev/null +++ b/exporter/datadogexporter/internal/translator/config.go @@ -0,0 +1,142 @@ +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package translator + +import "fmt" + +type translatorConfig struct { + // metrics export behavior + HistMode HistogramMode + SendCountSum bool + Quantiles bool + SendMonotonic bool + ResourceAttributesAsTags bool + + // cache configuration + sweepInterval int64 + deltaTTL int64 + + // hostname provider configuration + fallbackHostnameProvider HostnameProvider +} + +// Option is a translator creation option. +type Option func(*translatorConfig) error + +// WithDeltaTTL sets the delta TTL for cumulative metrics datapoints. +// By default, 3600 seconds are used. +func WithDeltaTTL(deltaTTL int64) Option { + return func(t *translatorConfig) error { + if deltaTTL <= 0 { + return fmt.Errorf("time to live must be positive: %d", deltaTTL) + } + t.deltaTTL = deltaTTL + t.sweepInterval = 1 + if t.deltaTTL > 1 { + t.sweepInterval = t.deltaTTL / 2 + } + return nil + } +} + +// WithFallbackHostnameProvider sets the fallback hostname provider. +// By default, an empty hostname is used as a fallback. +func WithFallbackHostnameProvider(provider HostnameProvider) Option { + return func(t *translatorConfig) error { + t.fallbackHostnameProvider = provider + return nil + } +} + +// WithQuantiles enables quantiles exporting for summary metrics. +func WithQuantiles() Option { + return func(t *translatorConfig) error { + t.Quantiles = true + return nil + } +} + +// WithResourceAttributesAsTags sets resource attributes as tags. +func WithResourceAttributesAsTags() Option { + return func(t *translatorConfig) error { + t.ResourceAttributesAsTags = true + return nil + } +} + +// HistogramMode is an export mode for OTLP Histogram metrics. +type HistogramMode string + +const ( + // HistogramModeNoBuckets disables bucket export. + HistogramModeNoBuckets HistogramMode = "nobuckets" + // HistogramModeCounters exports buckets as Datadog counts. + HistogramModeCounters HistogramMode = "counters" + // HistogramModeDistributions exports buckets as Datadog distributions. + HistogramModeDistributions HistogramMode = "distributions" +) + +// WithHistogramMode sets the histograms mode. +// The default mode is HistogramModeOff. +func WithHistogramMode(mode HistogramMode) Option { + return func(t *translatorConfig) error { + + switch mode { + case HistogramModeNoBuckets, HistogramModeCounters, HistogramModeDistributions: + t.HistMode = mode + default: + return fmt.Errorf("unknown histogram mode: %q", mode) + } + return nil + } +} + +// WithCountSumMetrics exports .count and .sum histogram metrics. +func WithCountSumMetrics() Option { + return func(t *translatorConfig) error { + t.SendCountSum = true + return nil + } +} + +// NumberMode is an export mode for OTLP Number metrics. +type NumberMode string + +const ( + // NumberModeCumulativeToDelta calculates delta for + // cumulative monotonic metrics in the client side and reports + // them as Datadog counts. + NumberModeCumulativeToDelta NumberMode = "cumulative_to_delta" + + // NumberModeRawValue reports the raw value for cumulative monotonic + // metrics as a Datadog gauge. + NumberModeRawValue NumberMode = "raw_value" +) + +// WithNumberMode sets the number mode. +// The default mode is NumberModeCumulativeToDelta. +func WithNumberMode(mode NumberMode) Option { + return func(t *translatorConfig) error { + switch mode { + case NumberModeCumulativeToDelta: + t.SendMonotonic = true + case NumberModeRawValue: + t.SendMonotonic = false + default: + return fmt.Errorf("unknown number mode: %q", mode) + } + return nil + } +} diff --git a/exporter/datadogexporter/internal/translator/hostname_provider.go b/exporter/datadogexporter/internal/translator/hostname_provider.go new file mode 100644 index 000000000000..829d0654add2 --- /dev/null +++ b/exporter/datadogexporter/internal/translator/hostname_provider.go @@ -0,0 +1,31 @@ +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package translator + +import "context" + +// HostnameProvider gets a hostname +type HostnameProvider interface { + // Hostname gets the hostname from the machine. + Hostname(ctx context.Context) (string, error) +} + +var _ HostnameProvider = (*noHostProvider)(nil) + +type noHostProvider struct{} + +func (*noHostProvider) Hostname(context.Context) (string, error) { + return "", nil +} diff --git a/exporter/datadogexporter/internal/translator/metrics_translator.go b/exporter/datadogexporter/internal/translator/metrics_translator.go index 20d8c36d8ce3..9202eb661382 100644 --- a/exporter/datadogexporter/internal/translator/metrics_translator.go +++ b/exporter/datadogexporter/internal/translator/metrics_translator.go @@ -27,7 +27,6 @@ import ( "go.uber.org/zap" "gopkg.in/zorkian/go-datadog-api.v2" - "github.com/open-telemetry/opentelemetry-collector-contrib/exporter/datadogexporter/config" "github.com/open-telemetry/opentelemetry-collector-contrib/exporter/datadogexporter/internal/attributes" "github.com/open-telemetry/opentelemetry-collector-contrib/exporter/datadogexporter/internal/metrics" "github.com/open-telemetry/opentelemetry-collector-contrib/exporter/datadogexporter/internal/sketches" @@ -35,28 +34,38 @@ import ( const metricName string = "metric name" -const ( - histogramModeNoBuckets = "nobuckets" - histogramModeCounters = "counters" - histogramModeDistributions = "distributions" -) - -// HostnameProvider gets a hostname -type HostnameProvider interface { - // Hostname gets the hostname from the machine. - Hostname(ctx context.Context) (string, error) -} - type Translator struct { - prevPts *TTLCache - logger *zap.Logger - cfg config.MetricsConfig - buildInfo component.BuildInfo - fallbackHostnameProvider HostnameProvider + prevPts *ttlCache + logger *zap.Logger + cfg translatorConfig + buildInfo component.BuildInfo } -func New(cache *TTLCache, params component.ExporterCreateSettings, cfg config.MetricsConfig, fallbackHostProvider HostnameProvider) *Translator { - return &Translator{cache, params.Logger, cfg, params.BuildInfo, fallbackHostProvider} +func New(params component.ExporterCreateSettings, options ...Option) (*Translator, error) { + cfg := translatorConfig{ + HistMode: HistogramModeDistributions, + SendCountSum: false, + Quantiles: false, + SendMonotonic: true, + ResourceAttributesAsTags: false, + sweepInterval: 1800, + deltaTTL: 3600, + fallbackHostnameProvider: &noHostProvider{}, + } + + for _, opt := range options { + err := opt(&cfg) + if err != nil { + return nil, err + } + } + + if cfg.HistMode == HistogramModeNoBuckets && !cfg.SendCountSum { + return nil, fmt.Errorf("no buckets mode and no send count sum are incompatible") + } + + cache := newTTLCache(cfg.sweepInterval, cfg.deltaTTL) + return &Translator{cache, params.Logger, cfg, params.BuildInfo}, nil } // getTags maps an attributeMap into a slice of Datadog tags @@ -232,7 +241,7 @@ func (t *Translator) getLegacyBuckets(name string, p pdata.HistogramDataPoint, d func (t *Translator) mapHistogramMetrics(name string, slice pdata.HistogramDataPointSlice, delta bool, attrTags []string) (ms []datadog.Metric, sl sketches.SketchSeriesList) { // Allocate assuming none are nil and no buckets ms = make([]datadog.Metric, 0, 2*slice.Len()) - if t.cfg.HistConfig.Mode == histogramModeDistributions { + if t.cfg.HistMode == HistogramModeDistributions { sl = make(sketches.SketchSeriesList, 0, slice.Len()) } for i := 0; i < slice.Len(); i++ { @@ -241,7 +250,7 @@ func (t *Translator) mapHistogramMetrics(name string, slice pdata.HistogramDataP tags := getTags(p.Attributes()) tags = append(tags, attrTags...) - if t.cfg.HistConfig.SendCountSum { + if t.cfg.SendCountSum { count := float64(p.Count()) countName := fmt.Sprintf("%s.count", name) if delta { @@ -251,7 +260,7 @@ func (t *Translator) mapHistogramMetrics(name string, slice pdata.HistogramDataP } } - if t.cfg.HistConfig.SendCountSum { + if t.cfg.SendCountSum { sum := p.Sum() sumName := fmt.Sprintf("%s.sum", name) if !t.isSkippable(sumName, p.Sum()) { @@ -263,10 +272,10 @@ func (t *Translator) mapHistogramMetrics(name string, slice pdata.HistogramDataP } } - switch t.cfg.HistConfig.Mode { - case histogramModeCounters: + switch t.cfg.HistMode { + case HistogramModeCounters: ms = append(ms, t.getLegacyBuckets(name, p, delta, tags)...) - case histogramModeDistributions: + case HistogramModeDistributions: sl = append(sl, t.getSketchBuckets(name, ts, p, true, tags)) } } @@ -349,7 +358,7 @@ func (t *Translator) mapSummaryMetrics(name string, slice pdata.SummaryDataPoint } // MapMetrics maps OTLP metrics into the DataDog format -func (t *Translator) MapMetrics(md pdata.Metrics) (series []datadog.Metric, sl sketches.SketchSeriesList) { +func (t *Translator) MapMetrics(ctx context.Context, md pdata.Metrics) (series []datadog.Metric, sl sketches.SketchSeriesList, err error) { pushTime := uint64(time.Now().UTC().UnixNano()) rms := md.ResourceMetrics() seenHosts := make(map[string]struct{}) @@ -360,16 +369,15 @@ func (t *Translator) MapMetrics(md pdata.Metrics) (series []datadog.Metric, sl s // Only fetch attribute tags if they're not already converted into labels. // Otherwise some tags would be present twice in a metric's tag list. - if !t.cfg.ExporterConfig.ResourceAttributesAsTags { + if !t.cfg.ResourceAttributesAsTags { attributeTags = attributes.TagsFromAttributes(rm.Resource().Attributes()) } host, ok := attributes.HostnameFromAttributes(rm.Resource().Attributes()) if !ok { - fallbackHost, err := t.fallbackHostnameProvider.Hostname(context.Background()) - host = "" - if err == nil { - host = fallbackHost + host, err = t.cfg.fallbackHostnameProvider.Hostname(context.Background()) + if err != nil { + return nil, nil, fmt.Errorf("failed to get fallback host: %w", err) } } seenHosts[host] = struct{}{} diff --git a/exporter/datadogexporter/internal/translator/metrics_translator_test.go b/exporter/datadogexporter/internal/translator/metrics_translator_test.go index bbf22e9ce3af..385b41368a9d 100644 --- a/exporter/datadogexporter/internal/translator/metrics_translator_test.go +++ b/exporter/datadogexporter/internal/translator/metrics_translator_test.go @@ -31,19 +31,10 @@ import ( "go.uber.org/zap/zaptest/observer" "gopkg.in/zorkian/go-datadog-api.v2" - "github.com/open-telemetry/opentelemetry-collector-contrib/exporter/datadogexporter/config" "github.com/open-telemetry/opentelemetry-collector-contrib/exporter/datadogexporter/internal/attributes" "github.com/open-telemetry/opentelemetry-collector-contrib/exporter/datadogexporter/internal/metrics" ) -var defaultCfg = config.MetricsConfig{ - SendMonotonic: true, - HistConfig: config.HistogramConfig{ - Mode: histogramModeNoBuckets, - SendCountSum: true, - }, -} - func TestMetricValue(t *testing.T) { var ( name = "name" @@ -130,7 +121,7 @@ func (t testProvider) Hostname(context.Context) (string, error) { return string(t), nil } -func newTranslator(logger *zap.Logger, cfg config.MetricsConfig) *Translator { +func newTranslator(t *testing.T, logger *zap.Logger) *Translator { params := component.ExporterCreateSettings{ BuildInfo: component.BuildInfo{ Version: "1.0", @@ -139,7 +130,17 @@ func newTranslator(logger *zap.Logger, cfg config.MetricsConfig) *Translator { Logger: logger, }, } - return New(newTestCache(), params, cfg, testProvider("fallbackHostname")) + + tr, err := New( + params, + WithFallbackHostnameProvider(testProvider("fallbackHostname")), + WithCountSumMetrics(), + WithHistogramMode(HistogramModeNoBuckets), + WithNumberMode(NumberModeCumulativeToDelta), + ) + + require.NoError(t, err) + return tr } func TestMapIntMetrics(t *testing.T) { @@ -148,7 +149,7 @@ func TestMapIntMetrics(t *testing.T) { point := slice.AppendEmpty() point.SetIntVal(17) point.SetTimestamp(ts) - tr := newTranslator(zap.NewNop(), config.MetricsConfig{}) + tr := newTranslator(t, zap.NewNop()) assert.ElementsMatch(t, tr.mapNumberMetrics("int64.test", metrics.Gauge, slice, []string{}), @@ -173,7 +174,7 @@ func TestMapDoubleMetrics(t *testing.T) { point := slice.AppendEmpty() point.SetDoubleVal(math.Pi) point.SetTimestamp(ts) - tr := newTranslator(zap.NewNop(), config.MetricsConfig{}) + tr := newTranslator(t, zap.NewNop()) assert.ElementsMatch(t, tr.mapNumberMetrics("float64.test", metrics.Gauge, slice, []string{}), @@ -221,7 +222,7 @@ func TestMapIntMonotonicMetrics(t *testing.T) { expected[i] = metrics.NewCount(metricName, uint64(seconds(i+1)), float64(val), []string{}) } - tr := newTranslator(zap.NewNop(), defaultCfg) + tr := newTranslator(t, zap.NewNop()) output := tr.mapNumberMonotonicMetrics(metricName, slice, []string{}) assert.ElementsMatch(t, output, expected) @@ -259,7 +260,7 @@ func TestMapIntMonotonicDifferentDimensions(t *testing.T) { point.SetTimestamp(seconds(1)) point.Attributes().InsertString("key1", "valB") - tr := newTranslator(zap.NewNop(), defaultCfg) + tr := newTranslator(t, zap.NewNop()) assert.ElementsMatch(t, tr.mapNumberMonotonicMetrics(metricName, slice, []string{}), @@ -283,7 +284,7 @@ func TestMapIntMonotonicWithReboot(t *testing.T) { point.SetIntVal(val) } - tr := newTranslator(zap.NewNop(), defaultCfg) + tr := newTranslator(t, zap.NewNop()) assert.ElementsMatch(t, tr.mapNumberMonotonicMetrics(metricName, slice, []string{}), []datadog.Metric{ @@ -307,7 +308,7 @@ func TestMapIntMonotonicOutOfOrder(t *testing.T) { point.SetIntVal(val) } - tr := newTranslator(zap.NewNop(), defaultCfg) + tr := newTranslator(t, zap.NewNop()) assert.ElementsMatch(t, tr.mapNumberMonotonicMetrics(metricName, slice, []string{}), []datadog.Metric{ @@ -341,7 +342,7 @@ func TestMapDoubleMonotonicMetrics(t *testing.T) { expected[i] = metrics.NewCount(metricName, uint64(seconds(i+1)), val, []string{}) } - tr := newTranslator(zap.NewNop(), defaultCfg) + tr := newTranslator(t, zap.NewNop()) output := tr.mapNumberMonotonicMetrics(metricName, slice, []string{}) assert.ElementsMatch(t, expected, output) @@ -379,8 +380,7 @@ func TestMapDoubleMonotonicDifferentDimensions(t *testing.T) { point.SetTimestamp(seconds(1)) point.Attributes().InsertString("key1", "valB") - tr := newTranslator(zap.NewNop(), defaultCfg) - + tr := newTranslator(t, zap.NewNop()) assert.ElementsMatch(t, tr.mapNumberMonotonicMetrics(metricName, slice, []string{}), []datadog.Metric{ @@ -403,7 +403,7 @@ func TestMapDoubleMonotonicWithReboot(t *testing.T) { point.SetDoubleVal(val) } - tr := newTranslator(zap.NewNop(), defaultCfg) + tr := newTranslator(t, zap.NewNop()) assert.ElementsMatch(t, tr.mapNumberMonotonicMetrics(metricName, slice, []string{}), []datadog.Metric{ @@ -427,7 +427,7 @@ func TestMapDoubleMonotonicOutOfOrder(t *testing.T) { point.SetDoubleVal(val) } - tr := newTranslator(zap.NewNop(), defaultCfg) + tr := newTranslator(t, zap.NewNop()) assert.ElementsMatch(t, tr.mapNumberMonotonicMetrics(metricName, slice, []string{}), []datadog.Metric{ @@ -457,10 +457,10 @@ func TestMapDeltaHistogramMetrics(t *testing.T) { metrics.NewCount("doubleHist.test.bucket", uint64(ts), 18, []string{"lower_bound:0", "upper_bound:inf"}), } - tr := newTranslator(zap.NewNop(), defaultCfg) + tr := newTranslator(t, zap.NewNop()) delta := true - tr.cfg.HistConfig.Mode = histogramModeNoBuckets + tr.cfg.HistMode = HistogramModeNoBuckets res, sl := tr.mapHistogramMetrics("doubleHist.test", slice, delta, []string{}) require.Empty(t, sl) assert.ElementsMatch(t, @@ -468,7 +468,7 @@ func TestMapDeltaHistogramMetrics(t *testing.T) { noBuckets, ) - tr.cfg.HistConfig.Mode = histogramModeCounters + tr.cfg.HistMode = HistogramModeCounters res, sl = tr.mapHistogramMetrics("doubleHist.test", slice, delta, []string{}) require.Empty(t, sl) assert.ElementsMatch(t, @@ -487,7 +487,7 @@ func TestMapDeltaHistogramMetrics(t *testing.T) { metrics.NewCount("doubleHist.test.bucket", uint64(ts), 18, []string{"lower_bound:0", "upper_bound:inf", "attribute_tag:attribute_value"}), } - tr.cfg.HistConfig.Mode = histogramModeNoBuckets + tr.cfg.HistMode = HistogramModeNoBuckets res, sl = tr.mapHistogramMetrics("doubleHist.test", slice, delta, []string{"attribute_tag:attribute_value"}) require.Empty(t, sl) assert.ElementsMatch(t, @@ -495,7 +495,7 @@ func TestMapDeltaHistogramMetrics(t *testing.T) { noBucketsAttributeTags, ) - tr.cfg.HistConfig.Mode = histogramModeCounters + tr.cfg.HistMode = HistogramModeCounters res, sl = tr.mapHistogramMetrics("doubleHist.test", slice, delta, []string{"attribute_tag:attribute_value"}) require.Empty(t, sl) assert.ElementsMatch(t, @@ -527,10 +527,10 @@ func TestMapCumulativeHistogramMetrics(t *testing.T) { metrics.NewCount("doubleHist.test.bucket", uint64(seconds(2)), 2, []string{"lower_bound:0", "upper_bound:inf"}), } - tr := newTranslator(zap.NewNop(), defaultCfg) + tr := newTranslator(t, zap.NewNop()) delta := false - tr.cfg.HistConfig.Mode = histogramModeCounters + tr.cfg.HistMode = HistogramModeCounters res, sl := tr.mapHistogramMetrics("doubleHist.test", slice, delta, []string{}) require.Empty(t, sl) assert.ElementsMatch(t, @@ -541,8 +541,7 @@ func TestMapCumulativeHistogramMetrics(t *testing.T) { func TestLegacyBucketsTags(t *testing.T) { // Test that passing the same tags slice doesn't reuse the slice. - cfg := config.MetricsConfig{} - tr := newTranslator(zap.NewNop(), cfg) + tr := newTranslator(t, zap.NewNop()) tags := make([]string, 0, 10) @@ -620,7 +619,14 @@ func TestMapSummaryMetrics(t *testing.T) { c := newTestCache() c.cache.Set(c.metricDimensionsToMapKey("summary.example.count", tags), numberCounter{0, 1}, gocache.NoExpiration) c.cache.Set(c.metricDimensionsToMapKey("summary.example.sum", tags), numberCounter{0, 1}, gocache.NoExpiration) - return New(c, componenttest.NewNopExporterCreateSettings(), config.MetricsConfig{Quantiles: quantiles}, testProvider("fallbackHostname")) + options := []Option{WithFallbackHostnameProvider(testProvider("fallbackHostname"))} + if quantiles { + options = append(options, WithQuantiles()) + } + tr, err := New(componenttest.NewNopExporterCreateSettings(), options...) + require.NoError(t, err) + tr.prevPts = c + return tr } noQuantiles := []datadog.Metric{ @@ -683,12 +689,12 @@ func TestRunningMetrics(t *testing.T) { resAttrs.Insert(attributes.AttributeDatadogHostname, pdata.NewAttributeValueString("resource-hostname-2")) rms.AppendEmpty() + ctx := context.Background() + tr := newTranslator(t, zap.NewNop()) - cfg := config.MetricsConfig{} - tr := newTranslator(zap.NewNop(), cfg) - - series, sl := tr.MapMetrics(ms) + series, sl, err := tr.MapMetrics(ctx, ms) require.Empty(t, sl) + require.NoError(t, err) runningHostnames := []string{} @@ -900,9 +906,11 @@ func TestMapMetrics(t *testing.T) { core, observed := observer.New(zapcore.DebugLevel) testLogger := zap.New(core) - tr := newTranslator(testLogger, defaultCfg) - series, sl := tr.MapMetrics(md) + ctx := context.Background() + tr := newTranslator(t, testLogger) + series, sl, err := tr.MapMetrics(ctx, md) require.Empty(t, sl) + require.NoError(t, err) filtered := removeRunningMetrics(series) assert.ElementsMatch(t, filtered, []datadog.Metric{ @@ -1023,9 +1031,11 @@ func TestNaNMetrics(t *testing.T) { core, observed := observer.New(zapcore.DebugLevel) testLogger := zap.New(core) - tr := newTranslator(testLogger, defaultCfg) - series, sl := tr.MapMetrics(md) + tr := newTranslator(t, testLogger) + ctx := context.Background() + series, sl, err := tr.MapMetrics(ctx, md) require.Empty(t, sl) + require.NoError(t, err) filtered := removeRunningMetrics(series) assert.ElementsMatch(t, filtered, []datadog.Metric{ diff --git a/exporter/datadogexporter/internal/translator/sketches_test.go b/exporter/datadogexporter/internal/translator/sketches_test.go index 9a83b4131a70..af274026e426 100644 --- a/exporter/datadogexporter/internal/translator/sketches_test.go +++ b/exporter/datadogexporter/internal/translator/sketches_test.go @@ -23,8 +23,6 @@ import ( "github.com/stretchr/testify/assert" "go.opentelemetry.io/collector/model/pdata" "go.uber.org/zap" - - "github.com/open-telemetry/opentelemetry-collector-contrib/exporter/datadogexporter/config" ) func TestHistogramSketches(t *testing.T) { @@ -87,7 +85,7 @@ func TestHistogramSketches(t *testing.T) { defaultEps := 1.0 / 128.0 tol := 1e-8 cfg := quantile.Default() - tr := newTranslator(zap.NewNop(), config.MetricsConfig{}) + tr := newTranslator(t, zap.NewNop()) for _, test := range tests { t.Run(test.name, func(t *testing.T) { @@ -177,7 +175,7 @@ func TestInfiniteBounds(t *testing.T) { }, } - tr := newTranslator(zap.NewNop(), config.MetricsConfig{}) + tr := newTranslator(t, zap.NewNop()) for _, testInstance := range tests { t.Run(testInstance.name, func(t *testing.T) { p := testInstance.getHist() diff --git a/exporter/datadogexporter/internal/translator/ttlcache.go b/exporter/datadogexporter/internal/translator/ttlcache.go index e093f2892a8f..1d164aa6cbf1 100644 --- a/exporter/datadogexporter/internal/translator/ttlcache.go +++ b/exporter/datadogexporter/internal/translator/ttlcache.go @@ -26,7 +26,7 @@ const ( metricKeySeparator = string(byte(0)) ) -type TTLCache struct { +type ttlCache struct { cache *gocache.Cache } @@ -37,9 +37,9 @@ type numberCounter struct { value float64 } -func NewTTLCache(sweepInterval int64, deltaTTL int64) *TTLCache { +func newTTLCache(sweepInterval int64, deltaTTL int64) *ttlCache { cache := gocache.New(time.Duration(deltaTTL)*time.Second, time.Duration(sweepInterval)*time.Second) - return &TTLCache{cache} + return &ttlCache{cache} } // Uses a logic similar to what is done in the span processor to build metric keys: @@ -52,7 +52,7 @@ func concatDimensionValue(metricKeyBuilder *strings.Builder, value string) { // metricDimensionsToMapKey maps name and tags to a string to use as an identifier // The tags order does not matter -func (*TTLCache) metricDimensionsToMapKey(name string, tags []string) string { +func (*ttlCache) metricDimensionsToMapKey(name string, tags []string) string { var metricKeyBuilder strings.Builder dimensions := make([]string, len(tags)) @@ -69,7 +69,7 @@ func (*TTLCache) metricDimensionsToMapKey(name string, tags []string) string { // putAndGetDiff submits a new value for a given metric and returns the difference with the // last submitted value (ordered by timestamp). The diff value is only valid if `ok` is true. -func (t *TTLCache) putAndGetDiff(name string, tags []string, ts uint64, val float64) (dx float64, ok bool) { +func (t *ttlCache) putAndGetDiff(name string, tags []string, ts uint64, val float64) (dx float64, ok bool) { key := t.metricDimensionsToMapKey(name, tags) if c, found := t.cache.Get(key); found { cnt := c.(numberCounter) diff --git a/exporter/datadogexporter/internal/translator/ttlcache_test.go b/exporter/datadogexporter/internal/translator/ttlcache_test.go index 1e768ae8d94c..c9124fb88d53 100644 --- a/exporter/datadogexporter/internal/translator/ttlcache_test.go +++ b/exporter/datadogexporter/internal/translator/ttlcache_test.go @@ -20,8 +20,8 @@ import ( "github.com/stretchr/testify/assert" ) -func newTestCache() *TTLCache { - cache := NewTTLCache(1800, 3600) +func newTestCache() *ttlCache { + cache := newTTLCache(1800, 3600) return cache } func TestPutAndGetDiff(t *testing.T) { diff --git a/exporter/datadogexporter/metrics_exporter.go b/exporter/datadogexporter/metrics_exporter.go index 926a006bdb7e..10ee3f05987d 100644 --- a/exporter/datadogexporter/metrics_exporter.go +++ b/exporter/datadogexporter/metrics_exporter.go @@ -54,20 +54,51 @@ func (p *hostProvider) Hostname(context.Context) (string, error) { return metadata.GetHost(p.logger, p.cfg), nil } -func newMetricsExporter(ctx context.Context, params component.ExporterCreateSettings, cfg *config.Config) *metricsExporter { +// translatorFromConfig creates a new metrics translator from the exporter config. +func translatorFromConfig(params component.ExporterCreateSettings, cfg *config.Config) (*translator.Translator, error) { + options := []translator.Option{ + translator.WithDeltaTTL(cfg.Metrics.DeltaTTL), + translator.WithFallbackHostnameProvider(&hostProvider{params.Logger, cfg}), + } + + if cfg.Metrics.HistConfig.SendCountSum { + options = append(options, translator.WithCountSumMetrics()) + } + + if cfg.Metrics.Quantiles { + options = append(options, translator.WithQuantiles()) + } + + if cfg.Metrics.ExporterConfig.ResourceAttributesAsTags { + options = append(options, translator.WithResourceAttributesAsTags()) + } + + options = append(options, translator.WithHistogramMode(translator.HistogramMode(cfg.Metrics.HistConfig.Mode))) + + var numberMode translator.NumberMode + if cfg.Metrics.SendMonotonic { + numberMode = translator.NumberModeCumulativeToDelta + } else { + numberMode = translator.NumberModeRawValue + } + options = append(options, translator.WithNumberMode(numberMode)) + + return translator.New(params, options...) +} + +func newMetricsExporter(ctx context.Context, params component.ExporterCreateSettings, cfg *config.Config) (*metricsExporter, error) { client := utils.CreateClient(cfg.API.Key, cfg.Metrics.TCPAddr.Endpoint) client.ExtraHeader["User-Agent"] = utils.UserAgent(params.BuildInfo) client.HttpClient = utils.NewHTTPClient(10 * time.Second) utils.ValidateAPIKey(params.Logger, client) - var sweepInterval int64 = 1 - if cfg.Metrics.DeltaTTL > 1 { - sweepInterval = cfg.Metrics.DeltaTTL / 2 + tr, err := translatorFromConfig(params, cfg) + if err != nil { + return nil, err } - prevPts := translator.NewTTLCache(sweepInterval, cfg.Metrics.DeltaTTL) - tr := translator.New(prevPts, params, cfg.Metrics, &hostProvider{params.Logger, cfg}) - return &metricsExporter{params, cfg, ctx, client, tr} + + return &metricsExporter{params, cfg, ctx, client, tr}, nil } func (exp *metricsExporter) pushSketches(ctx context.Context, sl sketches.SketchSeriesList) error { @@ -115,10 +146,15 @@ func (exp *metricsExporter) PushMetricsData(ctx context.Context, md pdata.Metric }) } - ms, sl := exp.tr.MapMetrics(md) + ms, sl, err := exp.tr.MapMetrics(ctx, md) + if err != nil { + return fmt.Errorf("failed to map metrics: %w", err) + } + metrics.ProcessMetrics(ms, exp.cfg) if len(ms) > 0 { + exp.params.Logger.Info("exporting payload", zap.Any("metric", ms)) if err := exp.client.PostMetrics(ms); err != nil { return err } diff --git a/exporter/datadogexporter/metrics_exporter_test.go b/exporter/datadogexporter/metrics_exporter_test.go index 07530dceea02..b7ad0eeeb96f 100644 --- a/exporter/datadogexporter/metrics_exporter_test.go +++ b/exporter/datadogexporter/metrics_exporter_test.go @@ -27,6 +27,7 @@ import ( "github.com/open-telemetry/opentelemetry-collector-contrib/exporter/datadogexporter/config" "github.com/open-telemetry/opentelemetry-collector-contrib/exporter/datadogexporter/internal/metadata" "github.com/open-telemetry/opentelemetry-collector-contrib/exporter/datadogexporter/internal/testutils" + "github.com/open-telemetry/opentelemetry-collector-contrib/exporter/datadogexporter/internal/translator" ) func TestNewExporter(t *testing.T) { @@ -41,12 +42,18 @@ func TestNewExporter(t *testing.T) { TCPAddr: confignet.TCPAddr{ Endpoint: server.URL, }, + DeltaTTL: 3600, + HistConfig: config.HistogramConfig{ + Mode: string(translator.HistogramModeNoBuckets), + SendCountSum: true, + }, }, } params := componenttest.NewNopExporterCreateSettings() // The client should have been created correctly - exp := newMetricsExporter(context.Background(), params, cfg) + exp, err := newMetricsExporter(context.Background(), params, cfg) + require.NoError(t, err) assert.NotNil(t, exp) _ = exp.PushMetricsData(context.Background(), testutils.TestMetrics.Clone()) assert.Equal(t, len(server.MetadataChan), 0) @@ -56,7 +63,7 @@ func TestNewExporter(t *testing.T) { _ = exp.PushMetricsData(context.Background(), testutils.TestMetrics.Clone()) body := <-server.MetadataChan var recvMetadata metadata.HostMetadata - err := json.Unmarshal(body, &recvMetadata) + err = json.Unmarshal(body, &recvMetadata) require.NoError(t, err) assert.Equal(t, recvMetadata.InternalHostname, "custom-hostname") }