Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds an Attribute assertion to metric data test #3487

Merged
merged 7 commits into from Nov 29, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -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 appropriate attributes. (#3487)

### Changed

Expand Down
44 changes: 44 additions & 0 deletions sdk/metric/metricdata/metricdatatest/assertion.go
Expand Up @@ -20,6 +20,7 @@ import (
"fmt"
"testing"

"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/sdk/metric/metricdata"
)

Expand Down Expand Up @@ -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()

var reasons []string

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
}
19 changes: 19 additions & 0 deletions sdk/metric/metricdata/metricdatatest/assertion_fail_test.go
Expand Up @@ -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
Expand Down Expand Up @@ -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))
}
75 changes: 75 additions & 0 deletions sdk/metric/metricdata/metricdatatest/assertion_test.go
Expand Up @@ -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)))
}
113 changes: 113 additions & 0 deletions sdk/metric/metricdata/metricdatatest/comparisons.go
Expand Up @@ -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
}