diff --git a/CHANGELOG.md b/CHANGELOG.md index 9e1c6976989..949ea926d7c 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 appropriate attributes. (#3487) ### Changed diff --git a/example/otel-collector/Makefile b/example/otel-collector/Makefile index 47c560b34c8..a5707834b39 100644 --- a/example/otel-collector/Makefile +++ b/example/otel-collector/Makefile @@ -1,9 +1,11 @@ +JAEGER_OPERATOR_VERSION = v1.36.0 + namespace-k8s: kubectl apply -f k8s/namespace.yaml jaeger-operator-k8s: # Create the jaeger operator and necessary artifacts in ns observability - kubectl create -n observability -f https://github.com/jaegertracing/jaeger-operator/releases/download/v1.31.0/jaeger-operator.yaml + kubectl create -n observability -f https://github.com/jaegertracing/jaeger-operator/releases/download/$(JAEGER_OPERATOR_VERSION)/jaeger-operator.yaml jaeger-k8s: kubectl apply -f k8s/jaeger.yaml @@ -23,4 +25,4 @@ clean-k8s: - kubectl delete -f k8s/jaeger.yaml - - kubectl delete -n observability -f https://github.com/jaegertracing/jaeger-operator/releases/download/v1.31.0/jaeger-operator.yaml + - kubectl delete -n observability -f https://github.com/jaegertracing/jaeger-operator/releases/download/$(JAEGER_OPERATOR_VERSION)/jaeger-operator.yaml diff --git a/example/otel-collector/main.go b/example/otel-collector/main.go index 507260c8bd5..7f426e6bf3e 100644 --- a/example/otel-collector/main.go +++ b/example/otel-collector/main.go @@ -57,10 +57,14 @@ func initProvider() (func(context.Context) error, error) { // microk8s), it should be accessible through the NodePort service at the // `localhost:30080` endpoint. Otherwise, replace `localhost` with the // endpoint of your cluster. If you run the app inside k8s, then you can - // probably connect directly to the service through dns + // probably connect directly to the service through dns. ctx, cancel := context.WithTimeout(ctx, time.Second) defer cancel() - conn, err := grpc.DialContext(ctx, "localhost:30080", grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithBlock()) + conn, err := grpc.DialContext(ctx, "localhost:30080", + // Note the use of insecure transport here. TLS is recommended in production. + grpc.WithTransportCredentials(insecure.NewCredentials()), + grpc.WithBlock(), + ) if err != nil { return nil, fmt.Errorf("failed to create gRPC connection to collector: %w", err) } diff --git a/example/prometheus/main.go b/example/prometheus/main.go index e1d87693c22..bc15f041486 100644 --- a/example/prometheus/main.go +++ b/example/prometheus/main.go @@ -18,9 +18,11 @@ import ( "context" "fmt" "log" + "math/rand" "net/http" "os" "os/signal" + "time" "github.com/prometheus/client_golang/prometheus/promhttp" @@ -30,6 +32,10 @@ import ( "go.opentelemetry.io/otel/sdk/metric" ) +func init() { + rand.Seed(time.Now().UnixNano()) +} + func main() { ctx := context.Background() @@ -58,12 +64,17 @@ func main() { } counter.Add(ctx, 5, attrs...) - gauge, err := meter.SyncFloat64().UpDownCounter("bar", instrument.WithDescription("a fun little gauge")) + gauge, err := meter.AsyncFloat64().Gauge("bar", instrument.WithDescription("a fun little gauge")) + if err != nil { + log.Fatal(err) + } + err = meter.RegisterCallback([]instrument.Asynchronous{gauge}, func(ctx context.Context) { + n := -10. + rand.Float64()*(90.) // [-10, 100) + gauge.Observe(ctx, n, attrs...) + }) if err != nil { log.Fatal(err) } - gauge.Add(ctx, 100, attrs...) - gauge.Add(ctx, -25, attrs...) // This is the equivalent of prometheus.NewHistogramVec histogram, err := meter.SyncFloat64().Histogram("baz", instrument.WithDescription("a very nice histogram")) diff --git a/exporters/otlp/otlptrace/README.md b/exporters/otlp/otlptrace/README.md index ca91fd4f489..6e9cc0366ba 100644 --- a/exporters/otlp/otlptrace/README.md +++ b/exporters/otlp/otlptrace/README.md @@ -12,8 +12,8 @@ go get -u go.opentelemetry.io/otel/exporters/otlp/otlptrace ## Examples -- [Exporter setup and examples](./otlptracehttp/example_test.go) -- [Full example sending telemetry to a local collector](../../../example/otel-collector) +- [HTTP Exporter setup and examples](./otlptracehttp/example_test.go) +- [Full example of gRPC Exporter sending telemetry to a local collector](../../../example/otel-collector) ## [`otlptrace`](https://pkg.go.dev/go.opentelemetry.io/otel/exporters/otlp/otlptrace) diff --git a/sdk/metric/metricdata/metricdatatest/assertion.go b/sdk/metric/metricdata/metricdatatest/assertion.go index 1e52cfea107..193be1ff708 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() + + 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 +} 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 +}