diff --git a/.chloggen/mx-psi_infer-interval.yaml b/.chloggen/mx-psi_infer-interval.yaml new file mode 100644 index 00000000..5c307536 --- /dev/null +++ b/.chloggen/mx-psi_infer-interval.yaml @@ -0,0 +1,16 @@ +# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix' +change_type: enhancement + +# The name of the component (e.g. pkg/quantile) +component: pkg/otlp/metrics + +# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`). +note: Add `WithInferDeltaInterval` option to infer the interval for delta metrics + +# The PR related to this change +issues: [765] + +# (Optional) One or more lines of additional information to render under the primary note. +# These lines will be padded with 2 spaces and then inserted directly into the document. +# Use pipe (|) for multiline entries. +subtext: diff --git a/pkg/otlp/metrics/config.go b/pkg/otlp/metrics/config.go index d913e849..6a694ca0 100644 --- a/pkg/otlp/metrics/config.go +++ b/pkg/otlp/metrics/config.go @@ -29,6 +29,7 @@ type translatorConfig struct { InitialCumulMonoValueMode InitialCumulMonoValueMode InstrumentationLibraryMetadataAsTags bool InstrumentationScopeMetadataAsTags bool + InferDeltaInterval bool originProduct OriginProduct @@ -232,3 +233,12 @@ func WithInitialCumulMonoValueMode(mode InitialCumulMonoValueMode) TranslatorOpt return nil } } + +// WithInferDeltaInterval infers the interval for delta sums. +// By default the interval is set to 0. +func WithInferDeltaInterval() TranslatorOption { + return func(t *translatorConfig) error { + t.InferDeltaInterval = true + return nil + } +} diff --git a/pkg/otlp/metrics/metrics_translator.go b/pkg/otlp/metrics/metrics_translator.go index cf650ba0..79ab7c00 100644 --- a/pkg/otlp/metrics/metrics_translator.go +++ b/pkg/otlp/metrics/metrics_translator.go @@ -40,6 +40,10 @@ import ( const ( metricName string = "metric name" errNoBucketsNoSumCount string = "no buckets mode and no send count sum are incompatible" + + // intervalTolerance is the tolerance for interval calculation in seconds + // We use 0.05 seconds as tolerance to allow for some jitter. + intervalTolerance float64 = 0.05 ) var ( @@ -62,6 +66,32 @@ var ( } ) +// inferDeltaInterval calculates the interval for Datadog counts from OTLP delta sums. +// It returns the interval in seconds if the time difference between start and end timestamps +// is close to a whole number of seconds (within intervalTolerance), otherwise returns 0. +func inferDeltaInterval(startTimestamp, timestamp uint64) int64 { + if startTimestamp == 0 { + // We can't infer the interval without the startTimestamp + return 0 + } + + if startTimestamp > timestamp { + // malformed data + return 0 + } + + // Convert nanoseconds to seconds + deltaSeconds := float64(timestamp-startTimestamp) / 1e9 + roundedDelta := math.Round(deltaSeconds) + + if math.Abs(roundedDelta-deltaSeconds) < intervalTolerance { + return int64(roundedDelta) + } + + // delta is outside of tolerance range + return 0 +} + var _ source.Provider = (*noSourceProvider)(nil) type noSourceProvider struct{} @@ -169,7 +199,13 @@ func (t *Translator) mapNumberMetrics( continue } - consumer.ConsumeTimeSeries(ctx, pointDims, dt, uint64(p.Timestamp()), 0, val) + // Calculate interval for Count type metrics (from OTLP delta sums) + var interval int64 + if t.cfg.InferDeltaInterval && dt == Count { + interval = inferDeltaInterval(uint64(p.StartTimestamp()), uint64(p.Timestamp())) + } + + consumer.ConsumeTimeSeries(ctx, pointDims, dt, uint64(p.Timestamp()), interval, val) } } @@ -418,7 +454,11 @@ func (t *Translator) getSketchBuckets( sketch.Basic.Max = math.Min(p.Max(), sketch.Basic.Max) } - consumer.ConsumeSketch(ctx, pointDims, ts, 0, sketch) + var interval int64 + if t.cfg.InferDeltaInterval && delta { + interval = inferDeltaInterval(startTs, ts) + } + consumer.ConsumeSketch(ctx, pointDims, ts, interval, sketch) } return nil } diff --git a/pkg/otlp/metrics/metrics_translator_test.go b/pkg/otlp/metrics/metrics_translator_test.go index fe6b24e1..2bee4422 100644 --- a/pkg/otlp/metrics/metrics_translator_test.go +++ b/pkg/otlp/metrics/metrics_translator_test.go @@ -2345,3 +2345,55 @@ var statsPayloads = []*pb.ClientStatsPayload{ }, }, } + +func TestInferInterval(t *testing.T) { + tests := []struct { + name string + startTs, ts uint64 + expected int64 + }{ + { + name: "exact difference", + startTs: 1e9, + ts: 11e9, + expected: 10, + }, + { + name: "under within tolerance", + startTs: 1e9, + ts: 11e9 - 30e6, + expected: 10, + }, + { + name: "over within tolerance", + startTs: 1e9, + ts: 11e9 + 30e6, + expected: 10, + }, + { + name: "outside tolerance", + startTs: 1e9, + ts: 11e9 + 50e7, + expected: 0, + }, + { + name: "no starttimestamp", + startTs: 0, + ts: 11e9, + expected: 0, + }, + { + name: "malformed data", + startTs: 710000000, + ts: 0, + expected: 0, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := inferDeltaInterval(tt.startTs, tt.ts) + assert.Equal(t, tt.expected, got) + }) + } +} diff --git a/pkg/otlp/metrics/mixed_metrics_test.go b/pkg/otlp/metrics/mixed_metrics_test.go index 4023761b..4ac692a6 100644 --- a/pkg/otlp/metrics/mixed_metrics_test.go +++ b/pkg/otlp/metrics/mixed_metrics_test.go @@ -135,6 +135,19 @@ func TestMapMetrics(t *testing.T) { expectedUnknownMetricType: 1, expectedUnsupportedAggregationTemporality: 2, }, + { + name: "with-infer-delta-interval", + otlpfile: "testdata/otlpdata/mixed/interval.json", + ddogfile: "testdata/datadogdata/mixed/interval_inferred.json", + options: []TranslatorOption{ + WithInferDeltaInterval(), + }, + }, + { + name: "with-infer-delta-interval", + otlpfile: "testdata/otlpdata/mixed/interval.json", + ddogfile: "testdata/datadogdata/mixed/interval_not_inferred.json", + }, } for _, testinstance := range tests { diff --git a/pkg/otlp/metrics/testdata/datadogdata/mixed/interval_inferred.json b/pkg/otlp/metrics/testdata/datadogdata/mixed/interval_inferred.json new file mode 100644 index 00000000..3f5d2d1a --- /dev/null +++ b/pkg/otlp/metrics/testdata/datadogdata/mixed/interval_inferred.json @@ -0,0 +1,110 @@ +{ + "Metrics": { + "Sketches": [ + { + "Name": "delta_histogram", + "Tags": [], + "Host": "", + "OriginID": "", + "OriginProduct": 10, + "OriginSubProduct": 17, + "OriginProductDetail": 0, + "Interval": 20, + "Timestamp": 1755603980235511877, + "Summary": { + "Min": -100, + "Max": 99.95733062532716, + "Sum": 3.141592653589793, + "Avg": 0.15707963267948966, + "Cnt": 20 + }, + "Keys": [ + -1635, + -1590, + 0, + 1453, + 1498, + 1524, + 1543, + 1558, + 1570, + 1580, + 1589, + 1597, + 1604, + 1610, + 1616, + 1621, + 1626, + 1631, + 1635 + ], + "Counts": [ + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 2 + ] + } + ], + "TimeSeries": [ + { + "Name": "random_delta_sum", + "Tags": [], + "Host": "", + "OriginID": "", + "OriginProduct": 10, + "OriginSubProduct": 17, + "OriginProductDetail": 0, + "Type": "count", + "Interval": 20, + "Timestamp": 1755603940234853944, + "Value": 8617 + }, + { + "Name": "random_delta_sum", + "Tags": [], + "Host": "", + "OriginID": "", + "OriginProduct": 10, + "OriginSubProduct": 17, + "OriginProductDetail": 0, + "Type": "count", + "Interval": 20, + "Timestamp": 1755603960236383064, + "Value": 9227 + }, + { + "Name": "random_delta_sum", + "Tags": [], + "Host": "", + "OriginID": "", + "OriginProduct": 10, + "OriginSubProduct": 17, + "OriginProductDetail": 0, + "Type": "count", + "Interval": 20, + "Timestamp": 1755603980235511877, + "Value": 9125 + } + ] + }, + "Hosts": { + "": {} + } +} \ No newline at end of file diff --git a/pkg/otlp/metrics/testdata/datadogdata/mixed/interval_not_inferred.json b/pkg/otlp/metrics/testdata/datadogdata/mixed/interval_not_inferred.json new file mode 100644 index 00000000..7e34b240 --- /dev/null +++ b/pkg/otlp/metrics/testdata/datadogdata/mixed/interval_not_inferred.json @@ -0,0 +1,110 @@ +{ + "Metrics": { + "Sketches": [ + { + "Name": "delta_histogram", + "Tags": [], + "Host": "", + "OriginID": "", + "OriginProduct": 10, + "OriginSubProduct": 17, + "OriginProductDetail": 0, + "Interval": 0, + "Timestamp": 1755603980235511877, + "Summary": { + "Min": -100, + "Max": 99.95733062532716, + "Sum": 3.141592653589793, + "Avg": 0.15707963267948966, + "Cnt": 20 + }, + "Keys": [ + -1635, + -1590, + 0, + 1453, + 1498, + 1524, + 1543, + 1558, + 1570, + 1580, + 1589, + 1597, + 1604, + 1610, + 1616, + 1621, + 1626, + 1631, + 1635 + ], + "Counts": [ + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 2 + ] + } + ], + "TimeSeries": [ + { + "Name": "random_delta_sum", + "Tags": [], + "Host": "", + "OriginID": "", + "OriginProduct": 10, + "OriginSubProduct": 17, + "OriginProductDetail": 0, + "Type": "count", + "Interval": 0, + "Timestamp": 1755603940234853944, + "Value": 8617 + }, + { + "Name": "random_delta_sum", + "Tags": [], + "Host": "", + "OriginID": "", + "OriginProduct": 10, + "OriginSubProduct": 17, + "OriginProductDetail": 0, + "Type": "count", + "Interval": 0, + "Timestamp": 1755603960236383064, + "Value": 9227 + }, + { + "Name": "random_delta_sum", + "Tags": [], + "Host": "", + "OriginID": "", + "OriginProduct": 10, + "OriginSubProduct": 17, + "OriginProductDetail": 0, + "Type": "count", + "Interval": 0, + "Timestamp": 1755603980235511877, + "Value": 9125 + } + ] + }, + "Hosts": { + "": {} + } +} \ No newline at end of file diff --git a/pkg/otlp/metrics/testdata/otlpdata/mixed/interval.json b/pkg/otlp/metrics/testdata/otlpdata/mixed/interval.json new file mode 100644 index 00000000..bc05fd29 --- /dev/null +++ b/pkg/otlp/metrics/testdata/otlpdata/mixed/interval.json @@ -0,0 +1,86 @@ +{ +"resourceMetrics":[ + { + "resource":{ + "attributes":[ + { + "key":"telemetry.sdk.language", + "value":{ + "stringValue":"go" + } + }, + { + "key":"telemetry.sdk.name", + "value":{ + "stringValue":"opentelemetry" + } + }, + { + "key":"telemetry.sdk.version", + "value":{ + "stringValue":"1.37.0" + } + } + ] + }, + "scopeMetrics":[ + { + "scope":{ + "name":"simple-go-client" + }, + "metrics":[ + { + "name":"random_delta_sum", + "description":"A delta sum metric updated with random values", + "sum":{ + "dataPoints":[ + { + "startTimeUnixNano":"1755603920234358178", + "timeUnixNano":"1755603940234853944", + "asInt":"8617" + }, + { + "startTimeUnixNano":"1755603940234853944", + "timeUnixNano":"1755603960236383064", + "asInt":"9227" + }, + { + "startTimeUnixNano":"1755603960236383064", + "timeUnixNano":"1755603980235511877", + "asInt":"9125" + } + ], + "aggregationTemporality":1, + "isMonotonic":true + } + }, + { + "name": "delta_histogram", + "histogram": { + "dataPoints": [ + { + "startTimeUnixNano":"1755603960236383064", + "timeUnixNano":"1755603980235511877", + "count": "20", + "sum": 3.141592653589793, + "bucketCounts": [ + "0", + "2", + "17", + "1" + ], + "explicitBounds": [ + -100, 0, 100 + ] + } + ], + "aggregationTemporality": 1 + } + } + ] + } + ], + "schemaUrl":"https://opentelemetry.io/schemas/1.34.0" + } +] +} diff --git a/pkg/otlp/metrics/testhelper_test.go b/pkg/otlp/metrics/testhelper_test.go index 5edc87a0..b825e73d 100644 --- a/pkg/otlp/metrics/testhelper_test.go +++ b/pkg/otlp/metrics/testhelper_test.go @@ -62,6 +62,7 @@ type TestDimensions struct { type TestSketch struct { TestDimensions + Interval int64 Timestamp uint64 Summary summary.Summary Keys []int32 @@ -170,6 +171,7 @@ func (t *testConsumer) ConsumeTimeSeries( }, Type: typ, Timestamp: timestamp, + Interval: interval, Value: value, }) } @@ -193,6 +195,7 @@ func (t *testConsumer) ConsumeSketch( OriginSubProduct: dimensions.OriginSubProduct(), OriginProductDetail: dimensions.OriginProductDetail(), }, + Interval: interval, Timestamp: timestamp, Summary: sketch.Basic, Keys: k,