From 74ca5c32e6231e0ed6afd28f443b0a022dfb94b2 Mon Sep 17 00:00:00 2001 From: Aaron Clawson <3766680+MadVikingGod@users.noreply.github.com> Date: Mon, 21 Nov 2022 10:19:05 -0600 Subject: [PATCH 1/3] Adds an Attribute assertion to metric data test --- .../metricdata/metricdatatest/assertion.go | 44 +++++++ .../metricdatatest/assertion_fail_test.go | 19 +++ .../metricdatatest/assertion_test.go | 75 ++++++++++++ .../metricdata/metricdatatest/comparisons.go | 113 ++++++++++++++++++ 4 files changed, 251 insertions(+) diff --git a/sdk/metric/metricdata/metricdatatest/assertion.go b/sdk/metric/metricdata/metricdatatest/assertion.go index 1e52cfea107..3bf8b8f9bcc 100644 --- a/sdk/metric/metricdata/metricdatatest/assertion.go +++ b/sdk/metric/metricdata/metricdatatest/assertion.go @@ -20,6 +20,7 @@ import ( "fmt" "testing" + "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/sdk/metric/metricdata" ) @@ -129,3 +130,46 @@ func AssertAggregationsEqual(t *testing.T, expected, actual metricdata.Aggregati } return true } + +// AssertHasAttributes asserts that all Datapoints or HistogramDataPoints have all passed attrs. +func AssertHasAttributes[T Datatypes](t *testing.T, actual T, attrs ...attribute.KeyValue) bool { + t.Helper() + + reasons := []string{"unknown datatype"} + + switch e := interface{}(actual).(type) { + case metricdata.DataPoint[int64]: + reasons = hasAttributesDataPoints(e, attrs...) + case metricdata.DataPoint[float64]: + reasons = hasAttributesDataPoints(e, attrs...) + case metricdata.Gauge[int64]: + reasons = hasAttributesGauge(e, attrs...) + case metricdata.Gauge[float64]: + reasons = hasAttributesGauge(e, attrs...) + case metricdata.Sum[int64]: + reasons = hasAttributesSum(e, attrs...) + case metricdata.Sum[float64]: + reasons = hasAttributesSum(e, attrs...) + case metricdata.HistogramDataPoint: + reasons = hasAttributesHistogramDataPoints(e, attrs...) + case metricdata.Histogram: + reasons = hasAttributesHistogram(e, attrs...) + case metricdata.Metrics: + reasons = hasAttributesMetrics(e, attrs...) + case metricdata.ScopeMetrics: + reasons = hasAttributesScopeMetrics(e, attrs...) + case metricdata.ResourceMetrics: + reasons = hasAttributesResourceMetrics(e, attrs...) + default: + // We control all types passed to this, panic to signal developers + // early they changed things in an incompatible way. + panic(fmt.Sprintf("unknown types: %T", actual)) + } + + if len(reasons) > 0 { + t.Error(reasons) + return false + } + + return true +} diff --git a/sdk/metric/metricdata/metricdatatest/assertion_fail_test.go b/sdk/metric/metricdata/metricdatatest/assertion_fail_test.go index 4c608e4a2c9..8d0ce4f3d48 100644 --- a/sdk/metric/metricdata/metricdatatest/assertion_fail_test.go +++ b/sdk/metric/metricdata/metricdatatest/assertion_fail_test.go @@ -19,6 +19,8 @@ package metricdatatest // import "go.opentelemetry.io/otel/sdk/metric/metricdata import ( "testing" + + "go.opentelemetry.io/otel/attribute" ) // These tests are used to develop the failure messages of this package's @@ -57,3 +59,20 @@ func TestFailAssertAggregationsEqual(t *testing.T) { AssertAggregationsEqual(t, gaugeFloat64A, gaugeFloat64B) AssertAggregationsEqual(t, histogramA, histogramB) } + +func TestFailAssertAttribute(t *testing.T) { + AssertHasAttributes(t, dataPointInt64A, attribute.Bool("A", false)) + AssertHasAttributes(t, dataPointFloat64A, attribute.Bool("B", true)) + AssertHasAttributes(t, gaugeInt64A, attribute.Bool("A", false)) + AssertHasAttributes(t, gaugeFloat64A, attribute.Bool("B", true)) + AssertHasAttributes(t, sumInt64A, attribute.Bool("A", false)) + AssertHasAttributes(t, sumFloat64A, attribute.Bool("B", true)) + AssertHasAttributes(t, histogramDataPointA, attribute.Bool("A", false)) + AssertHasAttributes(t, histogramDataPointA, attribute.Bool("B", true)) + AssertHasAttributes(t, histogramA, attribute.Bool("A", false)) + AssertHasAttributes(t, histogramA, attribute.Bool("B", true)) + AssertHasAttributes(t, metricsA, attribute.Bool("A", false)) + AssertHasAttributes(t, metricsA, attribute.Bool("B", true)) + AssertHasAttributes(t, resourceMetricsA, attribute.Bool("A", false)) + AssertHasAttributes(t, resourceMetricsA, attribute.Bool("B", true)) +} diff --git a/sdk/metric/metricdata/metricdatatest/assertion_test.go b/sdk/metric/metricdata/metricdatatest/assertion_test.go index 3935ed15091..b7da4344353 100644 --- a/sdk/metric/metricdata/metricdatatest/assertion_test.go +++ b/sdk/metric/metricdata/metricdatatest/assertion_test.go @@ -314,3 +314,78 @@ func TestAssertAggregationsEqual(t *testing.T) { r = equalAggregations(histogramA, histogramC, config{ignoreTimestamp: true}) assert.Equalf(t, len(r), 0, "%v == %v", histogramA, histogramC) } + +func TestAssertAttributes(t *testing.T) { + AssertHasAttributes(t, dataPointInt64A, attribute.Bool("A", true)) + AssertHasAttributes(t, dataPointFloat64A, attribute.Bool("A", true)) + AssertHasAttributes(t, gaugeInt64A, attribute.Bool("A", true)) + AssertHasAttributes(t, gaugeFloat64A, attribute.Bool("A", true)) + AssertHasAttributes(t, sumInt64A, attribute.Bool("A", true)) + AssertHasAttributes(t, sumFloat64A, attribute.Bool("A", true)) + AssertHasAttributes(t, histogramDataPointA, attribute.Bool("A", true)) + AssertHasAttributes(t, histogramA, attribute.Bool("A", true)) + AssertHasAttributes(t, metricsA, attribute.Bool("A", true)) + AssertHasAttributes(t, scopeMetricsA, attribute.Bool("A", true)) + AssertHasAttributes(t, resourceMetricsA, attribute.Bool("A", true)) + + r := hasAttributesAggregation(gaugeInt64A, attribute.Bool("A", true)) + assert.Equal(t, len(r), 0, "gaugeInt64A has A=True") + r = hasAttributesAggregation(gaugeFloat64A, attribute.Bool("A", true)) + assert.Equal(t, len(r), 0, "gaugeFloat64A has A=True") + r = hasAttributesAggregation(sumInt64A, attribute.Bool("A", true)) + assert.Equal(t, len(r), 0, "sumInt64A has A=True") + r = hasAttributesAggregation(sumFloat64A, attribute.Bool("A", true)) + assert.Equal(t, len(r), 0, "sumFloat64A has A=True") + r = hasAttributesAggregation(histogramA, attribute.Bool("A", true)) + assert.Equal(t, len(r), 0, "histogramA has A=True") + + r = hasAttributesAggregation(gaugeInt64A, attribute.Bool("A", false)) + assert.Greater(t, len(r), 0, "gaugeInt64A does not have A=False") + r = hasAttributesAggregation(gaugeFloat64A, attribute.Bool("A", false)) + assert.Greater(t, len(r), 0, "gaugeFloat64A does not have A=False") + r = hasAttributesAggregation(sumInt64A, attribute.Bool("A", false)) + assert.Greater(t, len(r), 0, "sumInt64A does not have A=False") + r = hasAttributesAggregation(sumFloat64A, attribute.Bool("A", false)) + assert.Greater(t, len(r), 0, "sumFloat64A does not have A=False") + r = hasAttributesAggregation(histogramA, attribute.Bool("A", false)) + assert.Greater(t, len(r), 0, "histogramA does not have A=False") + + r = hasAttributesAggregation(gaugeInt64A, attribute.Bool("B", true)) + assert.Greater(t, len(r), 0, "gaugeInt64A does not have Attribute B") + r = hasAttributesAggregation(gaugeFloat64A, attribute.Bool("B", true)) + assert.Greater(t, len(r), 0, "gaugeFloat64A does not have Attribute B") + r = hasAttributesAggregation(sumInt64A, attribute.Bool("B", true)) + assert.Greater(t, len(r), 0, "sumInt64A does not have Attribute B") + r = hasAttributesAggregation(sumFloat64A, attribute.Bool("B", true)) + assert.Greater(t, len(r), 0, "sumFloat64A does not have Attribute B") + r = hasAttributesAggregation(histogramA, attribute.Bool("B", true)) + assert.Greater(t, len(r), 0, "histogramA does not have Attribute B") +} + +func TestAssertAttributesFail(t *testing.T) { + fakeT := &testing.T{} + assert.False(t, AssertHasAttributes(fakeT, dataPointInt64A, attribute.Bool("A", false))) + assert.False(t, AssertHasAttributes(fakeT, dataPointFloat64A, attribute.Bool("B", true))) + assert.False(t, AssertHasAttributes(fakeT, gaugeInt64A, attribute.Bool("A", false))) + assert.False(t, AssertHasAttributes(fakeT, gaugeFloat64A, attribute.Bool("B", true))) + assert.False(t, AssertHasAttributes(fakeT, sumInt64A, attribute.Bool("A", false))) + assert.False(t, AssertHasAttributes(fakeT, sumFloat64A, attribute.Bool("B", true))) + assert.False(t, AssertHasAttributes(fakeT, histogramDataPointA, attribute.Bool("A", false))) + assert.False(t, AssertHasAttributes(fakeT, histogramDataPointA, attribute.Bool("B", true))) + assert.False(t, AssertHasAttributes(fakeT, histogramA, attribute.Bool("A", false))) + assert.False(t, AssertHasAttributes(fakeT, histogramA, attribute.Bool("B", true))) + assert.False(t, AssertHasAttributes(fakeT, metricsA, attribute.Bool("A", false))) + assert.False(t, AssertHasAttributes(fakeT, metricsA, attribute.Bool("B", true))) + assert.False(t, AssertHasAttributes(fakeT, resourceMetricsA, attribute.Bool("A", false))) + assert.False(t, AssertHasAttributes(fakeT, resourceMetricsA, attribute.Bool("B", true))) + + sum := metricdata.Sum[int64]{ + Temporality: metricdata.CumulativeTemporality, + IsMonotonic: true, + DataPoints: []metricdata.DataPoint[int64]{ + dataPointInt64A, + dataPointInt64B, + }, + } + assert.False(t, AssertHasAttributes(fakeT, sum, attribute.Bool("A", true))) +} diff --git a/sdk/metric/metricdata/metricdatatest/comparisons.go b/sdk/metric/metricdata/metricdatatest/comparisons.go index 841c03b12e0..9879c96f109 100644 --- a/sdk/metric/metricdata/metricdatatest/comparisons.go +++ b/sdk/metric/metricdata/metricdatatest/comparisons.go @@ -358,3 +358,116 @@ func compareDiff[T any](extraExpected, extraActual []T) string { return msg.String() } + +func missingAttrStr(name string) string { + return fmt.Sprintf("missing attribute %s", name) +} + +func hasAttributesDataPoints[T int64 | float64](dp metricdata.DataPoint[T], attrs ...attribute.KeyValue) (reasons []string) { + for _, attr := range attrs { + val, ok := dp.Attributes.Value(attr.Key) + if !ok { + reasons = append(reasons, missingAttrStr(string(attr.Key))) + continue + } + if val != attr.Value { + reasons = append(reasons, notEqualStr(string(attr.Key), attr.Value.Emit(), val.Emit())) + } + } + return reasons +} + +func hasAttributesGauge[T int64 | float64](gauge metricdata.Gauge[T], attrs ...attribute.KeyValue) (reasons []string) { + for n, dp := range gauge.DataPoints { + reas := hasAttributesDataPoints(dp, attrs...) + if len(reas) > 0 { + reasons = append(reasons, fmt.Sprintf("gauge datapoint %d attributes:\n", n)) + reasons = append(reasons, reas...) + } + } + return reasons +} + +func hasAttributesSum[T int64 | float64](sum metricdata.Sum[T], attrs ...attribute.KeyValue) (reasons []string) { + for n, dp := range sum.DataPoints { + reas := hasAttributesDataPoints(dp, attrs...) + if len(reas) > 0 { + reasons = append(reasons, fmt.Sprintf("sum datapoint %d attributes:\n", n)) + reasons = append(reasons, reas...) + } + } + return reasons +} + +func hasAttributesHistogramDataPoints(dp metricdata.HistogramDataPoint, attrs ...attribute.KeyValue) (reasons []string) { + for _, attr := range attrs { + val, ok := dp.Attributes.Value(attr.Key) + if !ok { + reasons = append(reasons, missingAttrStr(string(attr.Key))) + continue + } + if val != attr.Value { + reasons = append(reasons, notEqualStr(string(attr.Key), attr.Value.Emit(), val.Emit())) + } + } + return reasons +} + +func hasAttributesHistogram(histogram metricdata.Histogram, attrs ...attribute.KeyValue) (reasons []string) { + for n, dp := range histogram.DataPoints { + reas := hasAttributesHistogramDataPoints(dp, attrs...) + if len(reas) > 0 { + reasons = append(reasons, fmt.Sprintf("histogram datapoint %d attributes:\n", n)) + reasons = append(reasons, reas...) + } + } + return reasons +} + +func hasAttributesAggregation(agg metricdata.Aggregation, attrs ...attribute.KeyValue) (reasons []string) { + switch agg := agg.(type) { + case metricdata.Gauge[int64]: + reasons = hasAttributesGauge(agg, attrs...) + case metricdata.Gauge[float64]: + reasons = hasAttributesGauge(agg, attrs...) + case metricdata.Sum[int64]: + reasons = hasAttributesSum(agg, attrs...) + case metricdata.Sum[float64]: + reasons = hasAttributesSum(agg, attrs...) + case metricdata.Histogram: + reasons = hasAttributesHistogram(agg, attrs...) + default: + reasons = []string{fmt.Sprintf("unknown aggregation %T", agg)} + } + return reasons +} + +func hasAttributesMetrics(metrics metricdata.Metrics, attrs ...attribute.KeyValue) (reasons []string) { + reas := hasAttributesAggregation(metrics.Data, attrs...) + if len(reas) > 0 { + reasons = append(reasons, fmt.Sprintf("Metric %s:\n", metrics.Name)) + reasons = append(reasons, reas...) + } + return reasons +} + +func hasAttributesScopeMetrics(sm metricdata.ScopeMetrics, attrs ...attribute.KeyValue) (reasons []string) { + for n, metrics := range sm.Metrics { + reas := hasAttributesMetrics(metrics, attrs...) + if len(reas) > 0 { + reasons = append(reasons, fmt.Sprintf("ScopeMetrics %s Metrics %d:\n", sm.Scope.Name, n)) + reasons = append(reasons, reas...) + } + } + return reasons +} +func hasAttributesResourceMetrics(rm metricdata.ResourceMetrics, attrs ...attribute.KeyValue) (reasons []string) { + for n, sm := range rm.ScopeMetrics { + reas := hasAttributesScopeMetrics(sm, attrs...) + if len(reas) > 0 { + reasons = append(reasons, fmt.Sprintf("ResourceMetrics ScopeMetrics %d:\n", n)) + reasons = append(reasons, reas...) + } + } + return reasons +} From e4828a93d9570570beacf584b68048833a4ad996 Mon Sep 17 00:00:00 2001 From: Aaron Clawson <3766680+MadVikingGod@users.noreply.github.com> Date: Wed, 23 Nov 2022 13:48:04 -0600 Subject: [PATCH 2/3] Fixes lint, adds changelog. Signed-off-by: Aaron Clawson <3766680+MadVikingGod@users.noreply.github.com> --- CHANGELOG.md | 1 + sdk/metric/metricdata/metricdatatest/assertion.go | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a08cf2af749..2119b640f72 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm - The `Instrument` and `InstrumentKind` type are added to `go.opentelemetry.io/otel/sdk/metric`. These additions are replacements for the `Instrument` and `InstrumentKind` types from `go.opentelemetry.io/otel/sdk/metric/view`. (#3459) - The `Stream` type is added to `go.opentelemetry.io/otel/sdk/metric` to define a metric data stream a view will produce. (#3459) +- The `AssertHasAttributes` allows instrument authors to test that datapoints returned have appropiate attributes. (#3487) ### Changed diff --git a/sdk/metric/metricdata/metricdatatest/assertion.go b/sdk/metric/metricdata/metricdatatest/assertion.go index 3bf8b8f9bcc..193be1ff708 100644 --- a/sdk/metric/metricdata/metricdatatest/assertion.go +++ b/sdk/metric/metricdata/metricdatatest/assertion.go @@ -135,7 +135,7 @@ func AssertAggregationsEqual(t *testing.T, expected, actual metricdata.Aggregati func AssertHasAttributes[T Datatypes](t *testing.T, actual T, attrs ...attribute.KeyValue) bool { t.Helper() - reasons := []string{"unknown datatype"} + var reasons []string switch e := interface{}(actual).(type) { case metricdata.DataPoint[int64]: From a8d1ad61082cdd66559ab60ff173b2e7822d4e18 Mon Sep 17 00:00:00 2001 From: Aaron Clawson <3766680+MadVikingGod@users.noreply.github.com> Date: Mon, 28 Nov 2022 09:25:34 -0600 Subject: [PATCH 3/3] fix lint errors --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 86519217cc6..daa5584bed7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,7 +29,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm - The `Instrument` and `InstrumentKind` type are added to `go.opentelemetry.io/otel/sdk/metric`. These additions are replacements for the `Instrument` and `InstrumentKind` types from `go.opentelemetry.io/otel/sdk/metric/view`. (#3459) - The `Stream` type is added to `go.opentelemetry.io/otel/sdk/metric` to define a metric data stream a view will produce. (#3459) -- The `AssertHasAttributes` allows instrument authors to test that datapoints returned have appropiate attributes. (#3487) +- The `AssertHasAttributes` allows instrument authors to test that datapoints returned have appropriate attributes. (#3487) ### Changed