// Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: BUSL-1.1 package telemetry import ( "strings" "testing" "time" "github.com/armon/go-metrics" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/sdk/instrumentation" "go.opentelemetry.io/otel/sdk/metric/metricdata" "go.opentelemetry.io/otel/sdk/resource" semconv "go.opentelemetry.io/otel/semconv/v1.17.0" cpb "go.opentelemetry.io/proto/otlp/common/v1" mpb "go.opentelemetry.io/proto/otlp/metrics/v1" rpb "go.opentelemetry.io/proto/otlp/resource/v1" ) var ( // Common attributes for test cases. start = time.Date(2000, time.January, 01, 0, 0, 0, 0, time.FixedZone("GMT", 0)) end = start.Add(30 * time.Second) alice = attribute.NewSet(attribute.String("user", "alice")) bob = attribute.NewSet(attribute.String("user", "bob")) pbAlice = &cpb.KeyValue{Key: "user", Value: &cpb.AnyValue{ Value: &cpb.AnyValue_StringValue{StringValue: "alice"}, }} pbBob = &cpb.KeyValue{Key: "user", Value: &cpb.AnyValue{ Value: &cpb.AnyValue_StringValue{StringValue: "bob"}, }} // DataPoint test case : Histogram Datapoints (Histogram) minA, maxA, sumA = 2.0, 4.0, 90.0 minB, maxB, sumB = 4.0, 150.0, 234.0 inputHDP = []metricdata.HistogramDataPoint[float64]{{ Attributes: alice, StartTime: start, Time: end, Count: 30, Bounds: []float64{1, 5}, BucketCounts: []uint64{0, 30, 0}, Min: metricdata.NewExtrema(minA), Max: metricdata.NewExtrema(maxA), Sum: sumA, }, { Attributes: bob, StartTime: start, Time: end, Count: 3, Bounds: []float64{1, 5}, BucketCounts: []uint64{0, 1, 2}, Min: metricdata.NewExtrema(minB), Max: metricdata.NewExtrema(maxB), Sum: sumB, }} expectedHDP = []*mpb.HistogramDataPoint{{ Attributes: []*cpb.KeyValue{pbAlice}, StartTimeUnixNano: uint64(start.UnixNano()), TimeUnixNano: uint64(end.UnixNano()), Count: 30, Sum: &sumA, ExplicitBounds: []float64{1, 5}, BucketCounts: []uint64{0, 30, 0}, Min: &minA, Max: &maxA, }, { Attributes: []*cpb.KeyValue{pbBob}, StartTimeUnixNano: uint64(start.UnixNano()), TimeUnixNano: uint64(end.UnixNano()), Count: 3, Sum: &sumB, ExplicitBounds: []float64{1, 5}, BucketCounts: []uint64{0, 1, 2}, Min: &minB, Max: &maxB, }} // DataPoint test case : Number Datapoints (Gauge / Counter) inputDP = []metricdata.DataPoint[float64]{ {Attributes: alice, StartTime: start, Time: end, Value: 1.0}, {Attributes: bob, StartTime: start, Time: end, Value: 2.0}, } expectedDP = []*mpb.NumberDataPoint{ { Attributes: []*cpb.KeyValue{pbAlice}, StartTimeUnixNano: uint64(start.UnixNano()), TimeUnixNano: uint64(end.UnixNano()), Value: &mpb.NumberDataPoint_AsDouble{AsDouble: 1.0}, }, { Attributes: []*cpb.KeyValue{pbBob}, StartTimeUnixNano: uint64(start.UnixNano()), TimeUnixNano: uint64(end.UnixNano()), Value: &mpb.NumberDataPoint_AsDouble{AsDouble: 2.0}, }, } invalidSumTemporality = metricdata.Metrics{ Name: "invalid-sum", Description: "Sum with invalid temporality", Unit: "1", Data: metricdata.Sum[float64]{ Temporality: metricdata.DeltaTemporality, IsMonotonic: false, DataPoints: inputDP, }, } invalidSumAgg = metricdata.Metrics{ Name: "unknown", Description: "Unknown aggregation", Unit: "1", Data: metricdata.Sum[int64]{}, } invalidHistTemporality = metricdata.Metrics{ Name: "invalid-histogram", Description: "Invalid histogram", Unit: "1", Data: metricdata.Histogram[float64]{ Temporality: metricdata.DeltaTemporality, DataPoints: inputHDP, }, } validFloat64Gauge = metricdata.Metrics{ Name: "float64-gauge", Description: "Gauge with float64 values", Unit: "1", Data: metricdata.Gauge[float64]{DataPoints: inputDP}, } validFloat64Sum = metricdata.Metrics{ Name: "float64-sum", Description: "Sum with float64 values", Unit: "1", Data: metricdata.Sum[float64]{ Temporality: metricdata.CumulativeTemporality, IsMonotonic: false, DataPoints: inputDP, }, } validFloat64Hist = metricdata.Metrics{ Name: "float64-histogram", Description: "Histogram", Unit: "1", Data: metricdata.Histogram[float64]{ Temporality: metricdata.CumulativeTemporality, DataPoints: inputHDP, }, } // Metrics Test Case // - 3 invalid metrics and 3 Valid to test filtering // - 1 invalid metric type // - 2 invalid cummulative temporalities (only cummulative supported) // - 3 types (Gauge, Counter, and Histogram) supported inputMetrics = []metricdata.Metrics{ validFloat64Gauge, validFloat64Sum, validFloat64Hist, invalidSumTemporality, invalidHistTemporality, invalidSumAgg, } expectedMetrics = []*mpb.Metric{ { Name: "float64-gauge", Description: "Gauge with float64 values", Unit: "1", Data: &mpb.Metric_Gauge{Gauge: &mpb.Gauge{DataPoints: expectedDP}}, }, { Name: "float64-sum", Description: "Sum with float64 values", Unit: "1", Data: &mpb.Metric_Sum{Sum: &mpb.Sum{ AggregationTemporality: mpb.AggregationTemporality_AGGREGATION_TEMPORALITY_CUMULATIVE, IsMonotonic: false, DataPoints: expectedDP, }}, }, { Name: "float64-histogram", Description: "Histogram", Unit: "1", Data: &mpb.Metric_Histogram{Histogram: &mpb.Histogram{ AggregationTemporality: mpb.AggregationTemporality_AGGREGATION_TEMPORALITY_CUMULATIVE, DataPoints: expectedHDP, }}, }, } // ScopeMetrics Test Cases inputScopeMetrics = []metricdata.ScopeMetrics{{ Scope: instrumentation.Scope{ Name: "test/code/path", Version: "v0.1.0", }, Metrics: inputMetrics, }} expectedScopeMetrics = []*mpb.ScopeMetrics{{ Scope: &cpb.InstrumentationScope{ Name: "test/code/path", Version: "v0.1.0", }, Metrics: expectedMetrics, }} // ResourceMetrics Test Cases inputResourceMetrics = &metricdata.ResourceMetrics{ Resource: resource.NewSchemaless( semconv.ServiceName("test server"), semconv.ServiceVersion("v0.1.0"), ), ScopeMetrics: inputScopeMetrics, } expectedResourceMetrics = &mpb.ResourceMetrics{ Resource: &rpb.Resource{ Attributes: []*cpb.KeyValue{ { Key: "service.name", Value: &cpb.AnyValue{ Value: &cpb.AnyValue_StringValue{StringValue: "test server"}, }, }, { Key: "service.version", Value: &cpb.AnyValue{ Value: &cpb.AnyValue_StringValue{StringValue: "v0.1.0"}, }, }, }, }, ScopeMetrics: expectedScopeMetrics, } ) // TestTransformOTLP runs tests from the "bottom-up" of the metricdata data types. func TestTransformOTLP(t *testing.T) { t.Parallel() // Histogram DataPoint Test Case (Histograms) assert.Equal(t, expectedHDP, histogramDataPointsToPB(inputHDP)) // Number DataPoint Test Case (Counters / Gauges) require.Equal(t, expectedDP, dataPointsToPB(inputDP)) // MetricType Error Test Cases _, err := metricTypeToPB(invalidHistTemporality) require.Error(t, err) require.ErrorIs(t, err, errTemporality) _, err = metricTypeToPB(invalidSumTemporality) require.Error(t, err) require.ErrorIs(t, err, errTemporality) _, err = metricTypeToPB(invalidSumAgg) require.Error(t, err) require.ErrorIs(t, err, errAggregaton) // Metrics Test Case m := metricsToPB(inputMetrics) require.Equal(t, expectedMetrics, m) require.Equal(t, len(expectedMetrics), 3) // Scope Metrics Test Case sm := scopeMetricsToPB(inputScopeMetrics) require.Equal(t, expectedScopeMetrics, sm) // // Resource Metrics Test Case rm := transformOTLP(inputResourceMetrics) require.Equal(t, expectedResourceMetrics, rm) } // TestTransformOTLP_CustomMetrics tests that a custom metric (hcp.otel.transform.failure) is emitted // when transform fails. This test cannot be run in parallel as the metrics.NewGlobal() // sets a shared global sink. func TestTransformOTLP_CustomMetrics(t *testing.T) { for name, tc := range map[string]struct { inputRM *metricdata.ResourceMetrics expectedMetricCount int }{ "successNoMetric": { inputRM: &metricdata.ResourceMetrics{ // 3 valid metrics. ScopeMetrics: []metricdata.ScopeMetrics{ { Metrics: []metricdata.Metrics{ validFloat64Gauge, validFloat64Hist, validFloat64Sum, }, }, }, }, }, "failureEmitsMetric": { // inputScopeMetrics contains 3 bad metrics. inputRM: inputResourceMetrics, expectedMetricCount: 3, }, } { tc := tc t.Run(name, func(t *testing.T) { // Init global sink. serviceName := "test.transform" cfg := metrics.DefaultConfig(serviceName) cfg.EnableHostname = false sink := metrics.NewInmemSink(10*time.Second, 10*time.Second) metrics.NewGlobal(cfg, sink) // Perform operation that emits metric. transformOTLP(tc.inputRM) // Collect sink metrics. intervals := sink.Data() require.Len(t, intervals, 1) key := serviceName + "." + strings.Join(internalMetricTransformFailure, ".") sv := intervals[0].Counters[key] if tc.expectedMetricCount == 0 { require.Empty(t, sv) return } // Verify count for transform failure metric. require.NotNil(t, sv) require.NotNil(t, sv.AggregateSample) require.Equal(t, 3, sv.AggregateSample.Count) }) } }