From 48b1568a71a79dc3b3cec2a32204bb452790db96 Mon Sep 17 00:00:00 2001 From: Mahendra Bishnoi Date: Mon, 4 Aug 2025 22:18:26 +0530 Subject: [PATCH 01/42] copied code with minor modifications for docs and feature flag: OTEL_GO_X_SELF_OBSERVABILITY --- .../stdout/stdoutmetric/internal/x/README.md | 36 +++++++++++ exporters/stdout/stdoutmetric/internal/x/x.go | 63 +++++++++++++++++++ .../stdout/stdoutmetric/internal/x/x_test.go | 59 +++++++++++++++++ 3 files changed, 158 insertions(+) create mode 100644 exporters/stdout/stdoutmetric/internal/x/README.md create mode 100644 exporters/stdout/stdoutmetric/internal/x/x.go create mode 100644 exporters/stdout/stdoutmetric/internal/x/x_test.go diff --git a/exporters/stdout/stdoutmetric/internal/x/README.md b/exporters/stdout/stdoutmetric/internal/x/README.md new file mode 100644 index 00000000000..9c116e63600 --- /dev/null +++ b/exporters/stdout/stdoutmetric/internal/x/README.md @@ -0,0 +1,36 @@ +# Experimental Features + +The `stdoutmetric` exporter contains features that have not yet stabilized in the OpenTelemetry specification. +These features are added to the `stdoutmetric` exporter prior to stabilization in the specification so that users can start experimenting with them and provide feedback. + +These feature may change in backwards incompatible ways as feedback is applied. +See the [Compatibility and Stability](#compatibility-and-stability) section for more information. + +## Features + +- [Self-Observability](#self-observability) + +### Self-Observability + +The `stdoutmetric` exporter provides a self-observability feature that allows you to monitor the exporter itself. + +To opt-in, set the environment variable `OTEL_GO_X_SELF_OBSERVABILITY` to `true`. + +When enabled, the exporter will create the following metrics using the global `MeterProvider`: + +- `otel.sdk.exporter.metric_data_point.inflight` +- `otel.sdk.exporter.metric_data_point.exported` +- `otel.sdk.exporter.operation.duration` + +Please see the [Semantic conventions for OpenTelemetry SDK metrics] documentation for more details on these metrics. + +[Semantic conventions for OpenTelemetry SDK metrics]: https://github.com/open-telemetry/semantic-conventions/blob/v1.36.0/docs/otel/sdk-metrics.md + +## Compatibility and Stability + +Experimental features do not fall within the scope of the OpenTelemetry Go versioning and stability [policy](../../../../VERSIONING.md). +These features may be removed or modified in successive version releases, including patch versions. + +When an experimental feature is promoted to a stable feature, a migration path will be included in the changelog entry of the release. +There is no guarantee that any environment variable feature flags that enabled the experimental feature will be supported by the stable version. +If they are supported, they may be accompanied with a deprecation notice stating a timeline for the removal of that support. diff --git a/exporters/stdout/stdoutmetric/internal/x/x.go b/exporters/stdout/stdoutmetric/internal/x/x.go new file mode 100644 index 00000000000..fb6dfd97564 --- /dev/null +++ b/exporters/stdout/stdoutmetric/internal/x/x.go @@ -0,0 +1,63 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +// Package x documents experimental features for [go.opentelemetry.io/otel/exporters/stdout/stdoutmetric]. +package x // import "go.opentelemetry.io/otel/exporters/stdout/stdoutmetric/internal/x" + +import ( + "os" + "strings" +) + +// SelfObservability is an experimental feature flag that determines if SDK +// self-observability metrics are enabled. +// +// To enable this feature set the OTEL_GO_X_SELF_OBSERVABILITY environment variable +// to the case-insensitive string value of "true" (i.e. "True" and "TRUE" +// will also enable this). +var SelfObservability = newFeature("SELF_OBSERVABILITY", func(v string) (string, bool) { + if strings.EqualFold(v, "true") { + return v, true + } + return "", false +}) + +// Feature is an experimental feature control flag. It provides a uniform way +// to interact with these feature flags and parse their values. +type Feature[T any] struct { + key string + parse func(v string) (T, bool) +} + +func newFeature[T any](suffix string, parse func(string) (T, bool)) Feature[T] { + const envKeyRoot = "OTEL_GO_X_" + return Feature[T]{ + key: envKeyRoot + suffix, + parse: parse, + } +} + +// Key returns the environment variable key that needs to be set to enable the +// feature. +func (f Feature[T]) Key() string { return f.key } + +// Lookup returns the user configured value for the feature and true if the +// user has enabled the feature. Otherwise, if the feature is not enabled, a +// zero-value and false are returned. +func (f Feature[T]) Lookup() (v T, ok bool) { + // https://github.com/open-telemetry/opentelemetry-specification/blob/62effed618589a0bec416a87e559c0a9d96289bb/specification/configuration/sdk-environment-variables.md#parsing-empty-value + // + // > The SDK MUST interpret an empty value of an environment variable the + // > same way as when the variable is unset. + vRaw := os.Getenv(f.key) + if vRaw == "" { + return v, ok + } + return f.parse(vRaw) +} + +// Enabled reports whether the feature is enabled. +func (f Feature[T]) Enabled() bool { + _, ok := f.Lookup() + return ok +} diff --git a/exporters/stdout/stdoutmetric/internal/x/x_test.go b/exporters/stdout/stdoutmetric/internal/x/x_test.go new file mode 100644 index 00000000000..15124ca91d1 --- /dev/null +++ b/exporters/stdout/stdoutmetric/internal/x/x_test.go @@ -0,0 +1,59 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package x + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestSelfObservability(t *testing.T) { + const key = "OTEL_GO_X_SELF_OBSERVABILITY" + require.Equal(t, key, SelfObservability.Key()) + + t.Run("100", run(setenv(key, "100"), assertDisabled(SelfObservability))) + t.Run("true", run(setenv(key, "true"), assertEnabled(SelfObservability, "true"))) + t.Run("True", run(setenv(key, "True"), assertEnabled(SelfObservability, "True"))) + t.Run("false", run(setenv(key, "false"), assertDisabled(SelfObservability))) + t.Run("empty", run(assertDisabled(SelfObservability))) +} + +func run(steps ...func(*testing.T)) func(*testing.T) { + return func(t *testing.T) { + t.Helper() + for _, step := range steps { + step(t) + } + } +} + +func setenv(k, v string) func(t *testing.T) { //nolint:unparam // This is a reusable test utility function. + return func(t *testing.T) { t.Setenv(k, v) } +} + +func assertEnabled[T any](f Feature[T], want T) func(*testing.T) { + return func(t *testing.T) { + t.Helper() + assert.True(t, f.Enabled(), "not enabled") + + v, ok := f.Lookup() + assert.True(t, ok, "Lookup state") + assert.Equal(t, want, v, "Lookup value") + } +} + +func assertDisabled[T any](f Feature[T]) func(*testing.T) { + var zero T + return func(t *testing.T) { + t.Helper() + + assert.False(t, f.Enabled(), "enabled") + + v, ok := f.Lookup() + assert.False(t, ok, "Lookup state") + assert.Equal(t, zero, v, "Lookup value") + } +} From 987b77e8fd4a5c92c27d35b37d6022e4bdd5e28f Mon Sep 17 00:00:00 2001 From: Mahendra Bishnoi Date: Fri, 8 Aug 2025 00:35:43 +0530 Subject: [PATCH 02/42] added self-observability support to stdoutmetric exporter for below metrics 1. otel.sdk.exporter.metric_data_point.inflight 2. otel.sdk.exporter.metric_data_point.exported 3. otel.sdk.exporter.operation.duration --- exporters/stdout/stdoutmetric/exporter.go | 84 ++++++++++++++++++- .../selfobservability/selfobservability.go | 65 ++++++++++++++ 2 files changed, 148 insertions(+), 1 deletion(-) create mode 100644 exporters/stdout/stdoutmetric/internal/selfobservability/selfobservability.go diff --git a/exporters/stdout/stdoutmetric/exporter.go b/exporters/stdout/stdoutmetric/exporter.go index 36c6c1d414a..5af13704d8d 100644 --- a/exporters/stdout/stdoutmetric/exporter.go +++ b/exporters/stdout/stdoutmetric/exporter.go @@ -10,11 +10,26 @@ import ( "sync" "sync/atomic" + "go.opentelemetry.io/otel/exporters/stdout/stdoutmetric/internal/selfobservability" + "go.opentelemetry.io/otel/exporters/stdout/stdoutmetric/internal/x" "go.opentelemetry.io/otel/internal/global" "go.opentelemetry.io/otel/sdk/metric" "go.opentelemetry.io/otel/sdk/metric/metricdata" + semconv "go.opentelemetry.io/otel/semconv/v1.34.0" + "go.opentelemetry.io/otel/semconv/v1.36.0/otelconv" ) +var ( + stdoutMetricExporterComponentType = otelconv.ComponentTypeAttr("stdout_metric_exporter") + exporterIDCounter atomic.Int64 +) + +// nextExporterID returns an identifier for this stdoutmetric exporter, +// starting with 0 and incrementing by 1 each time it is called. +func nextExporterID() int64 { + return exporterIDCounter.Add(1) - 1 +} + // exporter is an OpenTelemetry metric exporter. type exporter struct { encVal atomic.Value // encoderHolder @@ -25,6 +40,9 @@ type exporter struct { aggregationSelector metric.AggregationSelector redactTimestamps bool + + selfObservabilityEnabled bool + exporterMetric *selfobservability.ExporterMetrics } // New returns a configured metric exporter. @@ -38,10 +56,25 @@ func New(options ...Option) (metric.Exporter, error) { aggregationSelector: cfg.aggregationSelector, redactTimestamps: cfg.redactTimestamps, } + exp.initSelfObservability() exp.encVal.Store(*cfg.encoder) return exp, nil } +func (e *exporter) initSelfObservability() { + if !x.SelfObservability.Enabled() { + return + } + + e.selfObservabilityEnabled = true + componentName := fmt.Sprintf("%s/%d", stdoutMetricExporterComponentType, nextExporterID()) + e.exporterMetric = selfobservability.NewExporterMetrics( + "go.opentelemetry.io/otel/exporters/stdout/stdoutmetric", + semconv.OTelComponentName(componentName), + semconv.OTelComponentTypeKey.String(string(stdoutMetricExporterComponentType)), + ) +} + func (e *exporter) Temporality(k metric.InstrumentKind) metricdata.Temporality { return e.temporalitySelector(k) } @@ -51,7 +84,9 @@ func (e *exporter) Aggregation(k metric.InstrumentKind) metric.Aggregation { } func (e *exporter) Export(ctx context.Context, data *metricdata.ResourceMetrics) error { + trackExportFunc := e.trackExport(context.Background(), countDataPoints(data)) if err := ctx.Err(); err != nil { + trackExportFunc(err) return err } if e.redactTimestamps { @@ -60,7 +95,20 @@ func (e *exporter) Export(ctx context.Context, data *metricdata.ResourceMetrics) global.Debug("STDOUT exporter export", "Data", data) - return e.encVal.Load().(encoderHolder).Encode(data) + err := e.encVal.Load().(encoderHolder).Encode(data) + if err != nil { + trackExportFunc(err) + return err + } + trackExportFunc(nil) + return nil +} + +func (e *exporter) trackExport(ctx context.Context, count int64) func(err error) { + if !e.selfObservabilityEnabled { + return func(error) {} + } + return e.exporterMetric.TrackExport(ctx, count) } func (e *exporter) ForceFlush(context.Context) error { @@ -159,3 +207,37 @@ func redactDataPointTimestamps[T int64 | float64](sdp []metricdata.DataPoint[T]) } return out } + +// countDataPoints counts the total number of data points in a ResourceMetrics. +func countDataPoints(rm *metricdata.ResourceMetrics) int64 { + if rm == nil { + return 0 + } + + var total int64 + for _, sm := range rm.ScopeMetrics { + for _, m := range sm.Metrics { + switch data := m.Data.(type) { + case metricdata.Gauge[int64]: + total += int64(len(data.DataPoints)) + case metricdata.Gauge[float64]: + total += int64(len(data.DataPoints)) + case metricdata.Sum[int64]: + total += int64(len(data.DataPoints)) + case metricdata.Sum[float64]: + total += int64(len(data.DataPoints)) + case metricdata.Histogram[int64]: + total += int64(len(data.DataPoints)) + case metricdata.Histogram[float64]: + total += int64(len(data.DataPoints)) + case metricdata.ExponentialHistogram[int64]: + total += int64(len(data.DataPoints)) + case metricdata.ExponentialHistogram[float64]: + total += int64(len(data.DataPoints)) + case metricdata.Summary: + total += int64(len(data.DataPoints)) + } + } + } + return total +} diff --git a/exporters/stdout/stdoutmetric/internal/selfobservability/selfobservability.go b/exporters/stdout/stdoutmetric/internal/selfobservability/selfobservability.go new file mode 100644 index 00000000000..adbf6c4f78d --- /dev/null +++ b/exporters/stdout/stdoutmetric/internal/selfobservability/selfobservability.go @@ -0,0 +1,65 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +// Package selfobservability provides self-observability metrics for stdout metric exporter. +// This is an experimental feature controlled by the x.SelfObservability feature flag. +package selfobservability // import "go.opentelemetry.io/otel/exporters/stdout/stdoutmetric/internal/selfobservability" + +import ( + "context" + "time" + + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/metric" + "go.opentelemetry.io/otel/sdk" + semconv "go.opentelemetry.io/otel/semconv/v1.36.0" + "go.opentelemetry.io/otel/semconv/v1.36.0/otelconv" +) + +type ExporterMetrics struct { + inflight otelconv.SDKExporterMetricDataPointInflight + exported otelconv.SDKExporterMetricDataPointExported + duration otelconv.SDKExporterOperationDuration + attrs []attribute.KeyValue +} + +func NewExporterMetrics( + name string, + componentName, componentType attribute.KeyValue, +) *ExporterMetrics { + em := &ExporterMetrics{} + mp := otel.GetMeterProvider() + m := mp.Meter( + name, + metric.WithInstrumentationVersion(sdk.Version()), + metric.WithSchemaURL(semconv.SchemaURL)) + + var err error + if em.inflight, err = otelconv.NewSDKExporterMetricDataPointInflight(m); err != nil { + otel.Handle(err) + } + if em.exported, err = otelconv.NewSDKExporterMetricDataPointExported(m); err != nil { + otel.Handle(err) + } + if em.duration, err = otelconv.NewSDKExporterOperationDuration(m); err != nil { + otel.Handle(err) + } + + em.attrs = []attribute.KeyValue{componentName, componentType} + return em +} + +func (em *ExporterMetrics) TrackExport(ctx context.Context, count int64) func(err error) { + begin := time.Now() + em.inflight.Add(ctx, count, em.attrs...) + return func(err error) { + duration := time.Since(begin).Seconds() + em.inflight.Add(ctx, -count, em.attrs...) + if err != nil { + em.attrs = append(em.attrs, semconv.ErrorType(err)) + } + em.exported.Add(ctx, count, em.attrs...) + em.duration.Record(ctx, duration, em.attrs...) + } +} From b4d2756c60598b0c90499c85a4b3a72ffc4dbfea Mon Sep 17 00:00:00 2001 From: Mahendra Bishnoi Date: Fri, 8 Aug 2025 00:36:27 +0530 Subject: [PATCH 03/42] fixed broken link --- exporters/stdout/stdoutmetric/internal/x/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/exporters/stdout/stdoutmetric/internal/x/README.md b/exporters/stdout/stdoutmetric/internal/x/README.md index 9c116e63600..3a64525ae59 100644 --- a/exporters/stdout/stdoutmetric/internal/x/README.md +++ b/exporters/stdout/stdoutmetric/internal/x/README.md @@ -28,7 +28,7 @@ Please see the [Semantic conventions for OpenTelemetry SDK metrics] documentatio ## Compatibility and Stability -Experimental features do not fall within the scope of the OpenTelemetry Go versioning and stability [policy](../../../../VERSIONING.md). +Experimental features do not fall within the scope of the OpenTelemetry Go versioning and stability [policy](../../../../../VERSIONING.md). These features may be removed or modified in successive version releases, including patch versions. When an experimental feature is promoted to a stable feature, a migration path will be included in the changelog entry of the release. From b7181a48f901237ee5336899bcc104e55822aece Mon Sep 17 00:00:00 2001 From: Mahendra Bishnoi Date: Fri, 8 Aug 2025 00:45:48 +0530 Subject: [PATCH 04/42] added changelog entry for self-observability support in stdoutmetric exporter --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f8d1a4f91ef..b427e39245a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -51,6 +51,8 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm - Add native histogram exemplar support in `go.opentelemetry.io/otel/exporters/prometheus`. (#6772) - Add experimental self-observability log metrics in `go.opentelemetry.io/otel/sdk/log`. Check the `go.opentelemetry.io/otel/sdk/log/internal/x` package documentation for more information. (#7121) +- Add experimental self-observability exporter metrics in `go.opentelemetry.io/otel/exporters/stdout/stdoutmetric`. + Check the `go.opentelemetry.io/otel/exporters/stdout/stdoutmetric/internal/x` package documentation for more information. (#7150) ### Changed From c6f91ac4d4334b3a1aff6255c7e46e090b7cf22c Mon Sep 17 00:00:00 2001 From: Mahendra Bishnoi Date: Fri, 8 Aug 2025 01:16:08 +0530 Subject: [PATCH 05/42] run `make precommit` --- exporters/stdout/stdoutmetric/go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/exporters/stdout/stdoutmetric/go.mod b/exporters/stdout/stdoutmetric/go.mod index cdf96d4b072..287c09d3610 100644 --- a/exporters/stdout/stdoutmetric/go.mod +++ b/exporters/stdout/stdoutmetric/go.mod @@ -5,6 +5,7 @@ go 1.23.0 require ( github.com/stretchr/testify v1.10.0 go.opentelemetry.io/otel v1.37.0 + go.opentelemetry.io/otel/metric v1.37.0 go.opentelemetry.io/otel/sdk v1.37.0 go.opentelemetry.io/otel/sdk/metric v1.37.0 ) @@ -16,7 +17,6 @@ require ( github.com/google/uuid v1.6.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect - go.opentelemetry.io/otel/metric v1.37.0 // indirect go.opentelemetry.io/otel/trace v1.37.0 // indirect golang.org/x/sys v0.35.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect From a9ad0e26574d59e0b80b1dd46b0388fea4d75efd Mon Sep 17 00:00:00 2001 From: Mahendra Bishnoi Date: Sat, 9 Aug 2025 13:19:04 +0530 Subject: [PATCH 06/42] fix a bug where attributes defined in ExporterMetrics are mutated --- .../internal/selfobservability/selfobservability.go | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/exporters/stdout/stdoutmetric/internal/selfobservability/selfobservability.go b/exporters/stdout/stdoutmetric/internal/selfobservability/selfobservability.go index adbf6c4f78d..e5d0d45c700 100644 --- a/exporters/stdout/stdoutmetric/internal/selfobservability/selfobservability.go +++ b/exporters/stdout/stdoutmetric/internal/selfobservability/selfobservability.go @@ -55,11 +55,14 @@ func (em *ExporterMetrics) TrackExport(ctx context.Context, count int64) func(er em.inflight.Add(ctx, count, em.attrs...) return func(err error) { duration := time.Since(begin).Seconds() - em.inflight.Add(ctx, -count, em.attrs...) + attrs := em.attrs + em.inflight.Add(ctx, -count, attrs...) if err != nil { - em.attrs = append(em.attrs, semconv.ErrorType(err)) + attrs = make([]attribute.KeyValue, len(em.attrs)+1) + copy(attrs, em.attrs) + attrs = append(attrs, semconv.ErrorType(err)) } - em.exported.Add(ctx, count, em.attrs...) - em.duration.Record(ctx, duration, em.attrs...) + em.exported.Add(ctx, count, attrs...) + em.duration.Record(ctx, duration, attrs...) } } From 7b1fb2d0cf944b123155af18d72f6e055809bce0 Mon Sep 17 00:00:00 2001 From: Mahendra Bishnoi Date: Sat, 9 Aug 2025 13:21:21 +0530 Subject: [PATCH 07/42] test cases for ExporterMetrics --- .../selfobservability_test.go | 177 ++++++++++++++++++ 1 file changed, 177 insertions(+) create mode 100644 exporters/stdout/stdoutmetric/internal/selfobservability/selfobservability_test.go diff --git a/exporters/stdout/stdoutmetric/internal/selfobservability/selfobservability_test.go b/exporters/stdout/stdoutmetric/internal/selfobservability/selfobservability_test.go new file mode 100644 index 00000000000..485f47e208b --- /dev/null +++ b/exporters/stdout/stdoutmetric/internal/selfobservability/selfobservability_test.go @@ -0,0 +1,177 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package selfobservability + +import ( + "context" + "errors" + "github.com/stretchr/testify/assert" + "go.opentelemetry.io/otel/semconv/v1.36.0/otelconv" + "testing" + "time" + + "go.opentelemetry.io/otel" + sdkmetric "go.opentelemetry.io/otel/sdk/metric" + "go.opentelemetry.io/otel/sdk/metric/metricdata" + semconv "go.opentelemetry.io/otel/semconv/v1.34.0" +) + +type testSetup struct { + reader *sdkmetric.ManualReader + mp *sdkmetric.MeterProvider + ctx context.Context + em *ExporterMetrics +} + +func setupTestMeterProvider(t *testing.T) *testSetup { + reader := sdkmetric.NewManualReader() + mp := sdkmetric.NewMeterProvider(sdkmetric.WithReader(reader)) + + originalMP := otel.GetMeterProvider() + otel.SetMeterProvider(mp) + t.Cleanup(func() { otel.SetMeterProvider(originalMP) }) + + componentName := semconv.OTelComponentName("test") + componentType := semconv.OTelComponentTypeKey.String("exporter") + em := NewExporterMetrics("go.opentelemetry.io/otel/exporters/stdout/stdoutmetric", componentName, componentType) + + return &testSetup{ + reader: reader, + mp: mp, + ctx: context.Background(), + em: em, + } +} + +func collectMetrics(t *testing.T, setup *testSetup) metricdata.ResourceMetrics { + var rm metricdata.ResourceMetrics + err := setup.reader.Collect(setup.ctx, &rm) + assert.NoError(t, err) + return rm +} + +func findMetric(rm metricdata.ResourceMetrics, name string) (metricdata.Metrics, bool) { + for _, sm := range rm.ScopeMetrics { + for _, m := range sm.Metrics { + if m.Name == name { + return m, true + } + } + } + return metricdata.Metrics{}, false +} + +func TestExporterMetrics_TrackExport(t *testing.T) { + setup := setupTestMeterProvider(t) + + done1 := setup.em.TrackExport(setup.ctx, 2) + done2 := setup.em.TrackExport(setup.ctx, 3) + done3 := setup.em.TrackExport(setup.ctx, 1) + time.Sleep(5 * time.Millisecond) + done2(nil) + done1(errors.New("failed")) + done3(nil) + + rm := collectMetrics(t, setup) + assert.NotEmpty(t, rm.ScopeMetrics) + + inflight, found := findMetric(rm, otelconv.SDKExporterMetricDataPointInflight{}.Name()) + assert.True(t, found) + var totalInflightValue int64 + if sum, ok := inflight.Data.(metricdata.Sum[int64]); ok { + for _, dp := range sum.DataPoints { + totalInflightValue += dp.Value + } + } + + exported, found := findMetric(rm, otelconv.SDKExporterMetricDataPointExported{}.Name()) + assert.True(t, found) + var totalExported int64 + if sum, ok := exported.Data.(metricdata.Sum[int64]); ok { + for _, dp := range sum.DataPoints { + totalExported += dp.Value + } + } + + duration, found := findMetric(rm, otelconv.SDKExporterOperationDuration{}.Name()) + assert.True(t, found) + var operationCount uint64 + if hist, ok := duration.Data.(metricdata.Histogram[float64]); ok { + for _, dp := range hist.DataPoints { + operationCount += dp.Count + assert.Positive(t, dp.Sum) + } + } + + assert.Equal(t, int64(6), totalExported) + assert.Equal(t, uint64(3), operationCount) + assert.Equal(t, int64(0), totalInflightValue) +} + +func TestExporterMetrics_TrackExport_WithError(t *testing.T) { + setup := setupTestMeterProvider(t) + count := int64(3) + testErr := errors.New("export failed") + + done := setup.em.TrackExport(setup.ctx, count) + done(testErr) + + rm := collectMetrics(t, setup) + assert.NotEmpty(t, rm.ScopeMetrics) + + exported, found := findMetric(rm, otelconv.SDKExporterMetricDataPointExported{}.Name()) + assert.True(t, found) + if sum, ok := exported.Data.(metricdata.Sum[int64]); ok { + attr, hasErrorAttr := sum.DataPoints[0].Attributes.Value(semconv.ErrorTypeKey) + assert.True(t, hasErrorAttr) + assert.Equal(t, "*errors.errorString", attr.AsString()) + } +} + +func TestExporterMetrics_TrackExport_InflightTracking(t *testing.T) { + setup := setupTestMeterProvider(t) + count := int64(10) + + done := setup.em.TrackExport(setup.ctx, count) + rm := collectMetrics(t, setup) + inflight, found := findMetric(rm, otelconv.SDKExporterMetricDataPointInflight{}.Name()) + assert.True(t, found) + + var inflightValue int64 + if sum, ok := inflight.Data.(metricdata.Sum[int64]); ok { + for _, dp := range sum.DataPoints { + inflightValue = dp.Value + } + } + assert.Equal(t, count, inflightValue) + + done(nil) + rm = collectMetrics(t, setup) + inflight, found = findMetric(rm, otelconv.SDKExporterMetricDataPointInflight{}.Name()) + assert.True(t, found) + if sum, ok := inflight.Data.(metricdata.Sum[int64]); ok { + for _, dp := range sum.DataPoints { + assert.Equal(t, int64(0), dp.Value) + } + } +} + +func TestExporterMetrics_AttributesNotPermanentlyModified(t *testing.T) { + componentName := semconv.OTelComponentName("test-component") + componentType := semconv.OTelComponentTypeKey.String("test-exporter") + em := NewExporterMetrics("test", componentName, componentType) + + assert.Len(t, em.attrs, 2) + assert.Contains(t, em.attrs, componentName) + assert.Contains(t, em.attrs, componentType) + + done := em.TrackExport(context.Background(), 1) + done(errors.New("test error")) + done = em.TrackExport(context.Background(), 1) + done(nil) + + assert.Len(t, em.attrs, 2) + assert.Contains(t, em.attrs, componentName) + assert.Contains(t, em.attrs, componentType) +} From ef4a62908d1d195b74eaf95bbdbb1c0be7d53ff8 Mon Sep 17 00:00:00 2001 From: Mahendra Bishnoi Date: Sat, 9 Aug 2025 17:50:01 +0530 Subject: [PATCH 08/42] test cases for stdoutmetric exporter --- .../stdout/stdoutmetric/exporter_test.go | 210 ++++++++++++++++++ .../selfobservability_test.go | 5 +- 2 files changed, 213 insertions(+), 2 deletions(-) diff --git a/exporters/stdout/stdoutmetric/exporter_test.go b/exporters/stdout/stdoutmetric/exporter_test.go index d7f957e63e1..1b78ca6d935 100644 --- a/exporters/stdout/stdoutmetric/exporter_test.go +++ b/exporters/stdout/stdoutmetric/exporter_test.go @@ -7,16 +7,22 @@ import ( "bytes" "context" "encoding/json" + "errors" "io" + "strconv" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/exporters/stdout/stdoutmetric" "go.opentelemetry.io/otel/sdk/metric" "go.opentelemetry.io/otel/sdk/metric/metricdata" + semconv "go.opentelemetry.io/otel/semconv/v1.36.0" + "go.opentelemetry.io/otel/semconv/v1.36.0/otelconv" ) func testEncoderOption() stdoutmetric.Option { @@ -25,6 +31,13 @@ func testEncoderOption() stdoutmetric.Option { return stdoutmetric.WithEncoder(enc) } +// failingEncoder always returns an error when Encode is called. +type failingEncoder struct{} + +func (f failingEncoder) Encode(any) error { + return errors.New("encoding failed") +} + func testCtxErrHonored(factory func(*testing.T) func(context.Context) error) func(t *testing.T) { return func(t *testing.T) { t.Helper() @@ -178,3 +191,200 @@ func TestAggregationSelector(t *testing.T) { var unknownKind metric.InstrumentKind assert.Equal(t, metric.AggregationDrop{}, exp.Aggregation(unknownKind)) } + +func TestExporter_Export_SelfObservability(t *testing.T) { + tests := []struct { + name string + selfObservabilityEnabled bool + expectedExportedCount int64 + }{ + { + name: "Enabled", + selfObservabilityEnabled: true, + expectedExportedCount: 19, + }, + { + name: "Disabled", + selfObservabilityEnabled: false, + expectedExportedCount: 0, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Setenv("OTEL_GO_X_SELF_OBSERVABILITY", strconv.FormatBool(tt.selfObservabilityEnabled)) + reader := metric.NewManualReader() + mp := metric.NewMeterProvider(metric.WithReader(reader)) + origMp := otel.GetMeterProvider() + otel.SetMeterProvider(mp) + defer otel.SetMeterProvider(origMp) + + exp, err := stdoutmetric.New(testEncoderOption()) + require.NoError(t, err) + + rm := &metricdata.ResourceMetrics{ + ScopeMetrics: []metricdata.ScopeMetrics{ + { + Metrics: []metricdata.Metrics{ + { + Name: "gauge_int64", + Data: metricdata.Gauge[int64]{ + DataPoints: []metricdata.DataPoint[int64]{{Value: 1}, {Value: 2}}, + }, + }, + { + Name: "gauge_float64", + Data: metricdata.Gauge[float64]{ + DataPoints: []metricdata.DataPoint[float64]{ + {Value: 1.0}, + {Value: 2.0}, + {Value: 3.0}, + }, + }, + }, + { + Name: "sum_int64", + Data: metricdata.Sum[int64]{ + DataPoints: []metricdata.DataPoint[int64]{{Value: 10}}, + }, + }, + { + Name: "sum_float64", + Data: metricdata.Sum[float64]{ + DataPoints: []metricdata.DataPoint[float64]{{Value: 10.5}, {Value: 20.5}}, + }, + }, + { + Name: "histogram_int64", + Data: metricdata.Histogram[int64]{ + DataPoints: []metricdata.HistogramDataPoint[int64]{ + {Count: 1}, + {Count: 2}, + {Count: 3}, + }, + }, + }, + { + Name: "histogram_float64", + Data: metricdata.Histogram[float64]{ + DataPoints: []metricdata.HistogramDataPoint[float64]{{Count: 1}}, + }, + }, + { + Name: "exponential_histogram_int64", + Data: metricdata.ExponentialHistogram[int64]{ + DataPoints: []metricdata.ExponentialHistogramDataPoint[int64]{ + {Count: 1}, + {Count: 2}, + }, + }, + }, + { + Name: "exponential_histogram_float64", + Data: metricdata.ExponentialHistogram[float64]{ + DataPoints: []metricdata.ExponentialHistogramDataPoint[float64]{ + {Count: 1}, + {Count: 2}, + {Count: 3}, + {Count: 4}, + }, + }, + }, + { + Name: "summary", + Data: metricdata.Summary{ + DataPoints: []metricdata.SummaryDataPoint{{Count: 1}}, + }, + }, + }, + }, + }, + } + + ctx := context.Background() + err = exp.Export(ctx, rm) + require.NoError(t, err) + + var metrics metricdata.ResourceMetrics + err = reader.Collect(ctx, &metrics) + require.NoError(t, err) + + var foundExported, foundDuration, foundInflight bool + var exportedCount int64 + + for _, sm := range metrics.ScopeMetrics { + for _, m := range sm.Metrics { + switch m.Name { + case otelconv.SDKExporterMetricDataPointExported{}.Name(): + foundExported = true + if sum, ok := m.Data.(metricdata.Sum[int64]); ok { + for _, dp := range sum.DataPoints { + exportedCount += dp.Value + } + } + case otelconv.SDKExporterOperationDuration{}.Name(): + foundDuration = true + case otelconv.SDKExporterMetricDataPointInflight{}.Name(): + foundInflight = true + } + } + } + + assert.Equal(t, tt.selfObservabilityEnabled, foundExported) + assert.Equal(t, tt.selfObservabilityEnabled, foundDuration) + assert.Equal(t, tt.selfObservabilityEnabled, foundInflight) + assert.Equal(t, tt.expectedExportedCount, exportedCount) + }) + } +} + +func TestExporter_Export_EncodingErrorTracking(t *testing.T) { + t.Setenv("OTEL_GO_X_SELF_OBSERVABILITY", "true") + reader := metric.NewManualReader() + mp := metric.NewMeterProvider(metric.WithReader(reader)) + origMp := otel.GetMeterProvider() + otel.SetMeterProvider(mp) + defer otel.SetMeterProvider(origMp) + + exp, err := stdoutmetric.New(stdoutmetric.WithEncoder(failingEncoder{})) + assert.NoError(t, err) + + rm := &metricdata.ResourceMetrics{ + ScopeMetrics: []metricdata.ScopeMetrics{ + { + Metrics: []metricdata.Metrics{ + { + Name: "test_gauge", + Data: metricdata.Gauge[int64]{ + DataPoints: []metricdata.DataPoint[int64]{{Value: 1}, {Value: 2}}, + }, + }, + }, + }, + }, + } + + ctx := context.Background() + err = exp.Export(ctx, rm) + assert.EqualError(t, err, "encoding failed") + + var metrics metricdata.ResourceMetrics + err = reader.Collect(ctx, &metrics) + require.NoError(t, err) + + var foundErrorType bool + for _, sm := range metrics.ScopeMetrics { + for _, m := range sm.Metrics { + x := otelconv.SDKExporterMetricDataPointExported{}.Name() + if m.Name == x { + if sum, ok := m.Data.(metricdata.Sum[int64]); ok { + for _, dp := range sum.DataPoints { + var attr attribute.Value + attr, foundErrorType = dp.Attributes.Value(semconv.ErrorTypeKey) + assert.Equal(t, "*errors.errorString", attr.AsString()) + } + } + } + } + } + assert.True(t, foundErrorType) +} diff --git a/exporters/stdout/stdoutmetric/internal/selfobservability/selfobservability_test.go b/exporters/stdout/stdoutmetric/internal/selfobservability/selfobservability_test.go index 485f47e208b..9dd923ef24f 100644 --- a/exporters/stdout/stdoutmetric/internal/selfobservability/selfobservability_test.go +++ b/exporters/stdout/stdoutmetric/internal/selfobservability/selfobservability_test.go @@ -6,15 +6,16 @@ package selfobservability import ( "context" "errors" - "github.com/stretchr/testify/assert" - "go.opentelemetry.io/otel/semconv/v1.36.0/otelconv" "testing" "time" + "github.com/stretchr/testify/assert" + "go.opentelemetry.io/otel" sdkmetric "go.opentelemetry.io/otel/sdk/metric" "go.opentelemetry.io/otel/sdk/metric/metricdata" semconv "go.opentelemetry.io/otel/semconv/v1.34.0" + "go.opentelemetry.io/otel/semconv/v1.36.0/otelconv" ) type testSetup struct { From 38eb31654fc53397735c51711f2905d273155808 Mon Sep 17 00:00:00 2001 From: Mahendra Bishnoi Date: Sat, 9 Aug 2025 17:59:30 +0530 Subject: [PATCH 09/42] remove unused receiver to make linter (unused-receiver) happy --- exporters/stdout/stdoutmetric/exporter_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/exporters/stdout/stdoutmetric/exporter_test.go b/exporters/stdout/stdoutmetric/exporter_test.go index 1b78ca6d935..89f20a55138 100644 --- a/exporters/stdout/stdoutmetric/exporter_test.go +++ b/exporters/stdout/stdoutmetric/exporter_test.go @@ -34,7 +34,7 @@ func testEncoderOption() stdoutmetric.Option { // failingEncoder always returns an error when Encode is called. type failingEncoder struct{} -func (f failingEncoder) Encode(any) error { +func (failingEncoder) Encode(any) error { return errors.New("encoding failed") } From b67b5938df3697ccd181744c1ebef0b2fdbe306a Mon Sep 17 00:00:00 2001 From: Mahendra Bishnoi Date: Sat, 9 Aug 2025 20:23:25 +0530 Subject: [PATCH 10/42] fix version --- exporters/stdout/stdoutmetric/exporter.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/exporters/stdout/stdoutmetric/exporter.go b/exporters/stdout/stdoutmetric/exporter.go index 83ec2b7c93c..6f44b766bda 100644 --- a/exporters/stdout/stdoutmetric/exporter.go +++ b/exporters/stdout/stdoutmetric/exporter.go @@ -15,7 +15,7 @@ import ( "go.opentelemetry.io/otel/internal/global" "go.opentelemetry.io/otel/sdk/metric" "go.opentelemetry.io/otel/sdk/metric/metricdata" - semconv "go.opentelemetry.io/otel/semconv/v1.34.0" + semconv "go.opentelemetry.io/otel/semconv/v1.36.0" "go.opentelemetry.io/otel/semconv/v1.36.0/otelconv" ) From ecbb337d133401fc79e260a6106b86667d8530be Mon Sep 17 00:00:00 2001 From: Mahendra Bishnoi Date: Sat, 9 Aug 2025 20:33:59 +0530 Subject: [PATCH 11/42] fix version --- .../internal/selfobservability/selfobservability_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/exporters/stdout/stdoutmetric/internal/selfobservability/selfobservability_test.go b/exporters/stdout/stdoutmetric/internal/selfobservability/selfobservability_test.go index 9dd923ef24f..7bb46cd8da3 100644 --- a/exporters/stdout/stdoutmetric/internal/selfobservability/selfobservability_test.go +++ b/exporters/stdout/stdoutmetric/internal/selfobservability/selfobservability_test.go @@ -14,7 +14,7 @@ import ( "go.opentelemetry.io/otel" sdkmetric "go.opentelemetry.io/otel/sdk/metric" "go.opentelemetry.io/otel/sdk/metric/metricdata" - semconv "go.opentelemetry.io/otel/semconv/v1.34.0" + semconv "go.opentelemetry.io/otel/semconv/v1.36.0" "go.opentelemetry.io/otel/semconv/v1.36.0/otelconv" ) From d6d0dd8c151822b9fbc6b17c8ff9dd2fd1c1190e Mon Sep 17 00:00:00 2001 From: Mahendra Bishnoi Date: Mon, 11 Aug 2025 19:26:50 +0530 Subject: [PATCH 12/42] make stdoutMetricExporterComponentType as constant --- exporters/stdout/stdoutmetric/exporter.go | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/exporters/stdout/stdoutmetric/exporter.go b/exporters/stdout/stdoutmetric/exporter.go index 6f44b766bda..2e83a57efe7 100644 --- a/exporters/stdout/stdoutmetric/exporter.go +++ b/exporters/stdout/stdoutmetric/exporter.go @@ -19,10 +19,9 @@ import ( "go.opentelemetry.io/otel/semconv/v1.36.0/otelconv" ) -var ( - stdoutMetricExporterComponentType = otelconv.ComponentTypeAttr("stdout_metric_exporter") - exporterIDCounter atomic.Int64 -) +const stdoutMetricExporterComponentType = otelconv.ComponentTypeAttr("stdout_metric_exporter") + +var exporterIDCounter atomic.Int64 // nextExporterID returns an identifier for this stdoutmetric exporter, // starting with 0 and incrementing by 1 each time it is called. From ee64105baf7569e2062de1f4386123655e5e5d33 Mon Sep 17 00:00:00 2001 From: Mahendra Bishnoi Date: Mon, 11 Aug 2025 19:31:06 +0530 Subject: [PATCH 13/42] Use defer to call trackExportFunc, Thanks to @flc1125 --- exporters/stdout/stdoutmetric/exporter.go | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/exporters/stdout/stdoutmetric/exporter.go b/exporters/stdout/stdoutmetric/exporter.go index 2e83a57efe7..55a39f9b418 100644 --- a/exporters/stdout/stdoutmetric/exporter.go +++ b/exporters/stdout/stdoutmetric/exporter.go @@ -83,9 +83,10 @@ func (e *exporter) Aggregation(k metric.InstrumentKind) metric.Aggregation { } func (e *exporter) Export(ctx context.Context, data *metricdata.ResourceMetrics) error { + var err error trackExportFunc := e.trackExport(context.Background(), countDataPoints(data)) - if err := ctx.Err(); err != nil { - trackExportFunc(err) + defer func() { trackExportFunc(err) }() + if err = ctx.Err(); err != nil { return err } if e.redactTimestamps { @@ -94,12 +95,9 @@ func (e *exporter) Export(ctx context.Context, data *metricdata.ResourceMetrics) global.Debug("STDOUT exporter export", "Data", data) - err := e.encVal.Load().(encoderHolder).Encode(data) - if err != nil { - trackExportFunc(err) + if err = e.encVal.Load().(encoderHolder).Encode(data); err != nil { return err } - trackExportFunc(nil) return nil } From ddfb3c30ad461b9e07f4facca4e2a2ee5f6a932e Mon Sep 17 00:00:00 2001 From: Mahendra Bishnoi Date: Mon, 11 Aug 2025 19:32:37 +0530 Subject: [PATCH 14/42] duration -> durationSeconds --- .../internal/selfobservability/selfobservability.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/exporters/stdout/stdoutmetric/internal/selfobservability/selfobservability.go b/exporters/stdout/stdoutmetric/internal/selfobservability/selfobservability.go index e5d0d45c700..9072a73907e 100644 --- a/exporters/stdout/stdoutmetric/internal/selfobservability/selfobservability.go +++ b/exporters/stdout/stdoutmetric/internal/selfobservability/selfobservability.go @@ -54,7 +54,7 @@ func (em *ExporterMetrics) TrackExport(ctx context.Context, count int64) func(er begin := time.Now() em.inflight.Add(ctx, count, em.attrs...) return func(err error) { - duration := time.Since(begin).Seconds() + durationSeconds := time.Since(begin).Seconds() attrs := em.attrs em.inflight.Add(ctx, -count, attrs...) if err != nil { @@ -63,6 +63,6 @@ func (em *ExporterMetrics) TrackExport(ctx context.Context, count int64) func(er attrs = append(attrs, semconv.ErrorType(err)) } em.exported.Add(ctx, count, attrs...) - em.duration.Record(ctx, duration, attrs...) + em.duration.Record(ctx, durationSeconds, attrs...) } } From 089252eef4c9780619e775045e6a65580605676d Mon Sep 17 00:00:00 2001 From: Mahendra Bishnoi Date: Mon, 11 Aug 2025 20:03:41 +0530 Subject: [PATCH 15/42] suppress linter as err is used in defer statement --- exporters/stdout/stdoutmetric/exporter.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/exporters/stdout/stdoutmetric/exporter.go b/exporters/stdout/stdoutmetric/exporter.go index 55a39f9b418..d7629a83975 100644 --- a/exporters/stdout/stdoutmetric/exporter.go +++ b/exporters/stdout/stdoutmetric/exporter.go @@ -86,7 +86,7 @@ func (e *exporter) Export(ctx context.Context, data *metricdata.ResourceMetrics) var err error trackExportFunc := e.trackExport(context.Background(), countDataPoints(data)) defer func() { trackExportFunc(err) }() - if err = ctx.Err(); err != nil { + if err = ctx.Err(); err != nil { // nolint: gocritic // err is used in defer statement return err } if e.redactTimestamps { @@ -95,7 +95,7 @@ func (e *exporter) Export(ctx context.Context, data *metricdata.ResourceMetrics) global.Debug("STDOUT exporter export", "Data", data) - if err = e.encVal.Load().(encoderHolder).Encode(data); err != nil { + if err = e.encVal.Load().(encoderHolder).Encode(data); err != nil { // nolint: gocritic // err is used in defer statement return err } return nil From 884ade7415e621104874594ed47fc0e54ef29e1e Mon Sep 17 00:00:00 2001 From: Mahendra Bishnoi Date: Mon, 11 Aug 2025 20:05:48 +0530 Subject: [PATCH 16/42] instead of suppressing error, split if and err check on 2 lines --- exporters/stdout/stdoutmetric/exporter.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/exporters/stdout/stdoutmetric/exporter.go b/exporters/stdout/stdoutmetric/exporter.go index d7629a83975..3dca500ec91 100644 --- a/exporters/stdout/stdoutmetric/exporter.go +++ b/exporters/stdout/stdoutmetric/exporter.go @@ -86,7 +86,8 @@ func (e *exporter) Export(ctx context.Context, data *metricdata.ResourceMetrics) var err error trackExportFunc := e.trackExport(context.Background(), countDataPoints(data)) defer func() { trackExportFunc(err) }() - if err = ctx.Err(); err != nil { // nolint: gocritic // err is used in defer statement + err = ctx.Err() + if err != nil { return err } if e.redactTimestamps { @@ -95,7 +96,8 @@ func (e *exporter) Export(ctx context.Context, data *metricdata.ResourceMetrics) global.Debug("STDOUT exporter export", "Data", data) - if err = e.encVal.Load().(encoderHolder).Encode(data); err != nil { // nolint: gocritic // err is used in defer statement + err = e.encVal.Load().(encoderHolder).Encode(data) + if err != nil { return err } return nil From 460f3031b17ad15753b5915dc8b065991a777932 Mon Sep 17 00:00:00 2001 From: Mahendra Bishnoi Date: Sat, 16 Aug 2025 12:04:56 +0530 Subject: [PATCH 17/42] addressed review comment: use named return to make code more readable --- exporters/stdout/stdoutmetric/exporter.go | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/exporters/stdout/stdoutmetric/exporter.go b/exporters/stdout/stdoutmetric/exporter.go index 3dca500ec91..c8ace6d1b4c 100644 --- a/exporters/stdout/stdoutmetric/exporter.go +++ b/exporters/stdout/stdoutmetric/exporter.go @@ -82,13 +82,12 @@ func (e *exporter) Aggregation(k metric.InstrumentKind) metric.Aggregation { return e.aggregationSelector(k) } -func (e *exporter) Export(ctx context.Context, data *metricdata.ResourceMetrics) error { - var err error +func (e *exporter) Export(ctx context.Context, data *metricdata.ResourceMetrics) (err error) { trackExportFunc := e.trackExport(context.Background(), countDataPoints(data)) defer func() { trackExportFunc(err) }() err = ctx.Err() if err != nil { - return err + return } if e.redactTimestamps { redactTimestamps(data) @@ -97,10 +96,7 @@ func (e *exporter) Export(ctx context.Context, data *metricdata.ResourceMetrics) global.Debug("STDOUT exporter export", "Data", data) err = e.encVal.Load().(encoderHolder).Encode(data) - if err != nil { - return err - } - return nil + return } func (e *exporter) trackExport(ctx context.Context, count int64) func(err error) { From a6f0637e0117cf1c8c7ac1024c8422a2779aa3ca Mon Sep 17 00:00:00 2001 From: Mahendra Bishnoi Date: Sat, 16 Aug 2025 20:31:21 +0530 Subject: [PATCH 18/42] name component similar to https://github.com/open-telemetry/opentelemetry-go/pull/7195 --- exporters/stdout/stdoutmetric/exporter.go | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/exporters/stdout/stdoutmetric/exporter.go b/exporters/stdout/stdoutmetric/exporter.go index c8ace6d1b4c..ec1d5d74864 100644 --- a/exporters/stdout/stdoutmetric/exporter.go +++ b/exporters/stdout/stdoutmetric/exporter.go @@ -16,10 +16,12 @@ import ( "go.opentelemetry.io/otel/sdk/metric" "go.opentelemetry.io/otel/sdk/metric/metricdata" semconv "go.opentelemetry.io/otel/semconv/v1.36.0" - "go.opentelemetry.io/otel/semconv/v1.36.0/otelconv" ) -const stdoutMetricExporterComponentType = otelconv.ComponentTypeAttr("stdout_metric_exporter") +// otelComponentType is a name identifying the type of the OpenTelemetry +// component. It is not a standardized OTel component type, so it uses the +// Go package prefixed type name to ensure uniqueness and identity. +const otelComponentType = "go.opentelemetry.io/otel/exporters/stdout/stdoutmetric.exporter" var exporterIDCounter atomic.Int64 @@ -66,11 +68,11 @@ func (e *exporter) initSelfObservability() { } e.selfObservabilityEnabled = true - componentName := fmt.Sprintf("%s/%d", stdoutMetricExporterComponentType, nextExporterID()) + componentName := fmt.Sprintf("%s/%d", otelComponentType, nextExporterID()) e.exporterMetric = selfobservability.NewExporterMetrics( "go.opentelemetry.io/otel/exporters/stdout/stdoutmetric", semconv.OTelComponentName(componentName), - semconv.OTelComponentTypeKey.String(string(stdoutMetricExporterComponentType)), + semconv.OTelComponentTypeKey.String(otelComponentType), ) } From 938cbb0ec7d168c63c915b31999c93e686e5bf02 Mon Sep 17 00:00:00 2001 From: Mahendra Bishnoi Date: Sat, 16 Aug 2025 23:35:17 +0530 Subject: [PATCH 19/42] flatten the self-observability initialization and return the error to the user --- exporters/stdout/stdoutmetric/exporter.go | 31 ++++++++----------- .../selfobservability/selfobservability.go | 29 ++++++++++------- .../selfobservability_test.go | 10 ++++-- 3 files changed, 38 insertions(+), 32 deletions(-) diff --git a/exporters/stdout/stdoutmetric/exporter.go b/exporters/stdout/stdoutmetric/exporter.go index ec1d5d74864..a5b5b7a5a8f 100644 --- a/exporters/stdout/stdoutmetric/exporter.go +++ b/exporters/stdout/stdoutmetric/exporter.go @@ -53,27 +53,22 @@ type exporter struct { func New(options ...Option) (metric.Exporter, error) { cfg := newConfig(options...) exp := &exporter{ - temporalitySelector: cfg.temporalitySelector, - aggregationSelector: cfg.aggregationSelector, - redactTimestamps: cfg.redactTimestamps, + temporalitySelector: cfg.temporalitySelector, + aggregationSelector: cfg.aggregationSelector, + redactTimestamps: cfg.redactTimestamps, + selfObservabilityEnabled: x.SelfObservability.Enabled(), } - exp.initSelfObservability() exp.encVal.Store(*cfg.encoder) - return exp, nil -} - -func (e *exporter) initSelfObservability() { - if !x.SelfObservability.Enabled() { - return + var err error + if exp.selfObservabilityEnabled { + componentName := fmt.Sprintf("%s/%d", otelComponentType, nextExporterID()) + exp.exporterMetric, err = selfobservability.NewExporterMetrics( + "go.opentelemetry.io/otel/exporters/stdout/stdoutmetric", + semconv.OTelComponentName(componentName), + semconv.OTelComponentTypeKey.String(otelComponentType), + ) } - - e.selfObservabilityEnabled = true - componentName := fmt.Sprintf("%s/%d", otelComponentType, nextExporterID()) - e.exporterMetric = selfobservability.NewExporterMetrics( - "go.opentelemetry.io/otel/exporters/stdout/stdoutmetric", - semconv.OTelComponentName(componentName), - semconv.OTelComponentTypeKey.String(otelComponentType), - ) + return exp, err } func (e *exporter) Temporality(k metric.InstrumentKind) metricdata.Temporality { diff --git a/exporters/stdout/stdoutmetric/internal/selfobservability/selfobservability.go b/exporters/stdout/stdoutmetric/internal/selfobservability/selfobservability.go index 9072a73907e..a6a814bcf8c 100644 --- a/exporters/stdout/stdoutmetric/internal/selfobservability/selfobservability.go +++ b/exporters/stdout/stdoutmetric/internal/selfobservability/selfobservability.go @@ -7,6 +7,8 @@ package selfobservability // import "go.opentelemetry.io/otel/exporters/stdout/s import ( "context" + "errors" + "fmt" "time" "go.opentelemetry.io/otel" @@ -27,27 +29,30 @@ type ExporterMetrics struct { func NewExporterMetrics( name string, componentName, componentType attribute.KeyValue, -) *ExporterMetrics { - em := &ExporterMetrics{} +) (*ExporterMetrics, error) { + em := &ExporterMetrics{ + attrs: []attribute.KeyValue{componentName, componentType}, + } mp := otel.GetMeterProvider() m := mp.Meter( name, metric.WithInstrumentationVersion(sdk.Version()), metric.WithSchemaURL(semconv.SchemaURL)) - var err error - if em.inflight, err = otelconv.NewSDKExporterMetricDataPointInflight(m); err != nil { - otel.Handle(err) + var err, e error + if em.inflight, e = otelconv.NewSDKExporterMetricDataPointInflight(m); e != nil { + e = fmt.Errorf("failed to create metric_data_point inflight metric: %w", e) + err = errors.Join(err, e) } - if em.exported, err = otelconv.NewSDKExporterMetricDataPointExported(m); err != nil { - otel.Handle(err) + if em.exported, e = otelconv.NewSDKExporterMetricDataPointExported(m); e != nil { + e = fmt.Errorf("failed to create metric_data_point exported metric: %w", e) + err = errors.Join(err, e) } - if em.duration, err = otelconv.NewSDKExporterOperationDuration(m); err != nil { - otel.Handle(err) + if em.duration, e = otelconv.NewSDKExporterOperationDuration(m); e != nil { + e = fmt.Errorf("failed to create operation duration metric: %w", e) + err = errors.Join(err, e) } - - em.attrs = []attribute.KeyValue{componentName, componentType} - return em + return em, err } func (em *ExporterMetrics) TrackExport(ctx context.Context, count int64) func(err error) { diff --git a/exporters/stdout/stdoutmetric/internal/selfobservability/selfobservability_test.go b/exporters/stdout/stdoutmetric/internal/selfobservability/selfobservability_test.go index 7bb46cd8da3..0b5d8521250 100644 --- a/exporters/stdout/stdoutmetric/internal/selfobservability/selfobservability_test.go +++ b/exporters/stdout/stdoutmetric/internal/selfobservability/selfobservability_test.go @@ -35,7 +35,12 @@ func setupTestMeterProvider(t *testing.T) *testSetup { componentName := semconv.OTelComponentName("test") componentType := semconv.OTelComponentTypeKey.String("exporter") - em := NewExporterMetrics("go.opentelemetry.io/otel/exporters/stdout/stdoutmetric", componentName, componentType) + em, err := NewExporterMetrics( + "go.opentelemetry.io/otel/exporters/stdout/stdoutmetric", + componentName, + componentType, + ) + assert.NoError(t, err) return &testSetup{ reader: reader, @@ -161,7 +166,8 @@ func TestExporterMetrics_TrackExport_InflightTracking(t *testing.T) { func TestExporterMetrics_AttributesNotPermanentlyModified(t *testing.T) { componentName := semconv.OTelComponentName("test-component") componentType := semconv.OTelComponentTypeKey.String("test-exporter") - em := NewExporterMetrics("test", componentName, componentType) + em, err := NewExporterMetrics("test", componentName, componentType) + assert.NoError(t, err) assert.Len(t, em.attrs, 2) assert.Contains(t, em.attrs, componentName) From 4c357a95c1c3e173a775ba02fbd4495f0caa7284 Mon Sep 17 00:00:00 2001 From: Mahendra Bishnoi Date: Thu, 28 Aug 2025 21:05:38 +0530 Subject: [PATCH 20/42] address review comments - use pool to amortize slice allocation - pass actual context - use t.Cleanup instead of defer in tests - improve readability by returning without using err var --- exporters/stdout/stdoutmetric/exporter.go | 5 ++-- .../stdout/stdoutmetric/exporter_test.go | 4 +-- .../selfobservability/selfobservability.go | 30 ++++++++++++++----- 3 files changed, 27 insertions(+), 12 deletions(-) diff --git a/exporters/stdout/stdoutmetric/exporter.go b/exporters/stdout/stdoutmetric/exporter.go index a5b5b7a5a8f..41cf4dd4728 100644 --- a/exporters/stdout/stdoutmetric/exporter.go +++ b/exporters/stdout/stdoutmetric/exporter.go @@ -80,7 +80,7 @@ func (e *exporter) Aggregation(k metric.InstrumentKind) metric.Aggregation { } func (e *exporter) Export(ctx context.Context, data *metricdata.ResourceMetrics) (err error) { - trackExportFunc := e.trackExport(context.Background(), countDataPoints(data)) + trackExportFunc := e.trackExport(ctx, countDataPoints(data)) defer func() { trackExportFunc(err) }() err = ctx.Err() if err != nil { @@ -92,8 +92,7 @@ func (e *exporter) Export(ctx context.Context, data *metricdata.ResourceMetrics) global.Debug("STDOUT exporter export", "Data", data) - err = e.encVal.Load().(encoderHolder).Encode(data) - return + return e.encVal.Load().(encoderHolder).Encode(data) } func (e *exporter) trackExport(ctx context.Context, count int64) func(err error) { diff --git a/exporters/stdout/stdoutmetric/exporter_test.go b/exporters/stdout/stdoutmetric/exporter_test.go index 89f20a55138..c32a9c59498 100644 --- a/exporters/stdout/stdoutmetric/exporter_test.go +++ b/exporters/stdout/stdoutmetric/exporter_test.go @@ -216,7 +216,7 @@ func TestExporter_Export_SelfObservability(t *testing.T) { mp := metric.NewMeterProvider(metric.WithReader(reader)) origMp := otel.GetMeterProvider() otel.SetMeterProvider(mp) - defer otel.SetMeterProvider(origMp) + t.Cleanup(func() { otel.SetMeterProvider(origMp) }) exp, err := stdoutmetric.New(testEncoderOption()) require.NoError(t, err) @@ -343,7 +343,7 @@ func TestExporter_Export_EncodingErrorTracking(t *testing.T) { mp := metric.NewMeterProvider(metric.WithReader(reader)) origMp := otel.GetMeterProvider() otel.SetMeterProvider(mp) - defer otel.SetMeterProvider(origMp) + t.Cleanup(func() { otel.SetMeterProvider(origMp) }) exp, err := stdoutmetric.New(stdoutmetric.WithEncoder(failingEncoder{})) assert.NoError(t, err) diff --git a/exporters/stdout/stdoutmetric/internal/selfobservability/selfobservability.go b/exporters/stdout/stdoutmetric/internal/selfobservability/selfobservability.go index a6a814bcf8c..3c1672c1944 100644 --- a/exporters/stdout/stdoutmetric/internal/selfobservability/selfobservability.go +++ b/exporters/stdout/stdoutmetric/internal/selfobservability/selfobservability.go @@ -9,6 +9,7 @@ import ( "context" "errors" "fmt" + "sync" "time" "go.opentelemetry.io/otel" @@ -19,6 +20,17 @@ import ( "go.opentelemetry.io/otel/semconv/v1.36.0/otelconv" ) +var measureAttrsPool = sync.Pool{ + New: func() any { + // "component.name" + "component.type" + "error.type" + const n = 1 + 1 + 1 + s := make([]attribute.KeyValue, 0, n) + // Return a pointer to a slice instead of a slice itself + // to avoid allocations on every call. + return &s + }, +} + type ExporterMetrics struct { inflight otelconv.SDKExporterMetricDataPointInflight exported otelconv.SDKExporterMetricDataPointExported @@ -60,14 +72,18 @@ func (em *ExporterMetrics) TrackExport(ctx context.Context, count int64) func(er em.inflight.Add(ctx, count, em.attrs...) return func(err error) { durationSeconds := time.Since(begin).Seconds() - attrs := em.attrs - em.inflight.Add(ctx, -count, attrs...) + attrs := &em.attrs + em.inflight.Add(ctx, -count, *attrs...) if err != nil { - attrs = make([]attribute.KeyValue, len(em.attrs)+1) - copy(attrs, em.attrs) - attrs = append(attrs, semconv.ErrorType(err)) + attrs = measureAttrsPool.Get().(*[]attribute.KeyValue) + defer func() { + *attrs = (*attrs)[:0] // reset the slice for reuse + measureAttrsPool.Put(attrs) + }() + copy(*attrs, em.attrs) + *attrs = append(*attrs, semconv.ErrorType(err)) } - em.exported.Add(ctx, count, attrs...) - em.duration.Record(ctx, durationSeconds, attrs...) + em.exported.Add(ctx, count, *attrs...) + em.duration.Record(ctx, durationSeconds, *attrs...) } } From 2edfed9c5b5e7bd6c448fb900385e9af04565522 Mon Sep 17 00:00:00 2001 From: Mahendra Bishnoi Date: Thu, 28 Aug 2025 22:01:45 +0530 Subject: [PATCH 21/42] address review comments - use metricdatatest for comparision in testcase --- .../stdout/stdoutmetric/exporter_test.go | 97 ++++++++++++++----- 1 file changed, 74 insertions(+), 23 deletions(-) diff --git a/exporters/stdout/stdoutmetric/exporter_test.go b/exporters/stdout/stdoutmetric/exporter_test.go index c32a9c59498..fe78d1c6c13 100644 --- a/exporters/stdout/stdoutmetric/exporter_test.go +++ b/exporters/stdout/stdoutmetric/exporter_test.go @@ -19,8 +19,11 @@ import ( "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/exporters/stdout/stdoutmetric" + "go.opentelemetry.io/otel/sdk" + "go.opentelemetry.io/otel/sdk/instrumentation" "go.opentelemetry.io/otel/sdk/metric" "go.opentelemetry.io/otel/sdk/metric/metricdata" + "go.opentelemetry.io/otel/sdk/metric/metricdata/metricdatatest" semconv "go.opentelemetry.io/otel/semconv/v1.36.0" "go.opentelemetry.io/otel/semconv/v1.36.0/otelconv" ) @@ -308,31 +311,79 @@ func TestExporter_Export_SelfObservability(t *testing.T) { err = reader.Collect(ctx, &metrics) require.NoError(t, err) - var foundExported, foundDuration, foundInflight bool - var exportedCount int64 - - for _, sm := range metrics.ScopeMetrics { - for _, m := range sm.Metrics { - switch m.Name { - case otelconv.SDKExporterMetricDataPointExported{}.Name(): - foundExported = true - if sum, ok := m.Data.(metricdata.Sum[int64]); ok { - for _, dp := range sum.DataPoints { - exportedCount += dp.Value - } - } - case otelconv.SDKExporterOperationDuration{}.Name(): - foundDuration = true - case otelconv.SDKExporterMetricDataPointInflight{}.Name(): - foundInflight = true - } + if !tt.selfObservabilityEnabled { + assert.Empty(t, metrics.ScopeMetrics) + } else { + assert.Len(t, metrics.ScopeMetrics, 1) + durationMetric := metrics.ScopeMetrics[0].Metrics[2].Data.(metricdata.Histogram[float64]).DataPoints[0] + expectedMetrics := metricdata.ScopeMetrics{ + Scope: instrumentation.Scope{ + Name: "go.opentelemetry.io/otel/exporters/stdout/stdoutmetric", + Version: sdk.Version(), + SchemaURL: semconv.SchemaURL, + }, + Metrics: []metricdata.Metrics{ + { + Name: otelconv.SDKExporterMetricDataPointInflight{}.Name(), + Description: otelconv.SDKExporterMetricDataPointInflight{}.Description(), + Unit: otelconv.SDKExporterMetricDataPointInflight{}.Unit(), + Data: metricdata.Sum[int64]{ + DataPoints: []metricdata.DataPoint[int64]{ + { + Value: 0, + Attributes: attribute.NewSet( + semconv.OTelComponentName("go.opentelemetry.io/otel/exporters/stdout/stdoutmetric.exporter/0"), + semconv.OTelComponentTypeKey.String("go.opentelemetry.io/otel/exporters/stdout/stdoutmetric.exporter"), + ), + }, + }, + Temporality: metricdata.CumulativeTemporality, + }, + }, + { + Name: otelconv.SDKExporterMetricDataPointExported{}.Name(), + Description: otelconv.SDKExporterMetricDataPointExported{}.Description(), + Unit: otelconv.SDKExporterMetricDataPointExported{}.Unit(), + Data: metricdata.Sum[int64]{ + DataPoints: []metricdata.DataPoint[int64]{ + { + Value: tt.expectedExportedCount, + Attributes: attribute.NewSet( + semconv.OTelComponentName("go.opentelemetry.io/otel/exporters/stdout/stdoutmetric.exporter/0"), + semconv.OTelComponentTypeKey.String("go.opentelemetry.io/otel/exporters/stdout/stdoutmetric.exporter"), + ), + }, + }, + Temporality: metricdata.CumulativeTemporality, + IsMonotonic: true, + }, + }, + { + Name: otelconv.SDKExporterOperationDuration{}.Name(), + Description: otelconv.SDKExporterOperationDuration{}.Description(), + Unit: otelconv.SDKExporterOperationDuration{}.Unit(), + Data: metricdata.Histogram[float64]{ + DataPoints: []metricdata.HistogramDataPoint[float64]{ + { + Attributes: attribute.NewSet( + semconv.OTelComponentName("go.opentelemetry.io/otel/exporters/stdout/stdoutmetric.exporter/0"), + semconv.OTelComponentTypeKey.String("go.opentelemetry.io/otel/exporters/stdout/stdoutmetric.exporter"), + ), + Count: durationMetric.Count, + Bounds: durationMetric.Bounds, + BucketCounts: durationMetric.BucketCounts, + Min: durationMetric.Min, + Max: durationMetric.Max, + Sum: durationMetric.Sum, + }, + }, + Temporality: metricdata.CumulativeTemporality, + }, + }, + }, } + metricdatatest.AssertEqual(t, expectedMetrics, metrics.ScopeMetrics[0], metricdatatest.IgnoreTimestamp()) } - - assert.Equal(t, tt.selfObservabilityEnabled, foundExported) - assert.Equal(t, tt.selfObservabilityEnabled, foundDuration) - assert.Equal(t, tt.selfObservabilityEnabled, foundInflight) - assert.Equal(t, tt.expectedExportedCount, exportedCount) }) } } From 18e053040df6e14a6bffe784fee9d8a598bd0e27 Mon Sep 17 00:00:00 2001 From: Mahendra Bishnoi Date: Thu, 28 Aug 2025 22:16:58 +0530 Subject: [PATCH 22/42] generate internal counter package so that it can be tested by resetting in tests --- exporters/stdout/stdoutmetric/exporter.go | 11 +--- .../stdoutmetric/internal/counter/counter.go | 31 +++++++++ .../internal/counter/counter_test.go | 65 +++++++++++++++++++ exporters/stdout/stdoutmetric/internal/gen.go | 9 +++ 4 files changed, 107 insertions(+), 9 deletions(-) create mode 100644 exporters/stdout/stdoutmetric/internal/counter/counter.go create mode 100644 exporters/stdout/stdoutmetric/internal/counter/counter_test.go create mode 100644 exporters/stdout/stdoutmetric/internal/gen.go diff --git a/exporters/stdout/stdoutmetric/exporter.go b/exporters/stdout/stdoutmetric/exporter.go index 41cf4dd4728..5f5606effe8 100644 --- a/exporters/stdout/stdoutmetric/exporter.go +++ b/exporters/stdout/stdoutmetric/exporter.go @@ -10,6 +10,7 @@ import ( "sync" "sync/atomic" + "go.opentelemetry.io/otel/exporters/stdout/stdoutmetric/internal/counter" "go.opentelemetry.io/otel/exporters/stdout/stdoutmetric/internal/selfobservability" "go.opentelemetry.io/otel/exporters/stdout/stdoutmetric/internal/x" "go.opentelemetry.io/otel/internal/global" @@ -23,14 +24,6 @@ import ( // Go package prefixed type name to ensure uniqueness and identity. const otelComponentType = "go.opentelemetry.io/otel/exporters/stdout/stdoutmetric.exporter" -var exporterIDCounter atomic.Int64 - -// nextExporterID returns an identifier for this stdoutmetric exporter, -// starting with 0 and incrementing by 1 each time it is called. -func nextExporterID() int64 { - return exporterIDCounter.Add(1) - 1 -} - // exporter is an OpenTelemetry metric exporter. type exporter struct { encVal atomic.Value // encoderHolder @@ -61,7 +54,7 @@ func New(options ...Option) (metric.Exporter, error) { exp.encVal.Store(*cfg.encoder) var err error if exp.selfObservabilityEnabled { - componentName := fmt.Sprintf("%s/%d", otelComponentType, nextExporterID()) + componentName := fmt.Sprintf("%s/%d", otelComponentType, counter.NextExporterID()) exp.exporterMetric, err = selfobservability.NewExporterMetrics( "go.opentelemetry.io/otel/exporters/stdout/stdoutmetric", semconv.OTelComponentName(componentName), diff --git a/exporters/stdout/stdoutmetric/internal/counter/counter.go b/exporters/stdout/stdoutmetric/internal/counter/counter.go new file mode 100644 index 00000000000..bc002abe7e7 --- /dev/null +++ b/exporters/stdout/stdoutmetric/internal/counter/counter.go @@ -0,0 +1,31 @@ +// Code generated by gotmpl. DO NOT MODIFY. +// source: internal/shared/counter/counter.go.tmpl + +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +// Package counter provides a simple counter for generating unique IDs. +// +// This package is used to generate unique IDs while allowing testing packages +// to reset the counter. +package counter // import "go.opentelemetry.io/otel/exporters/stdout/stdoutmetric/internal/counter" + +import "sync/atomic" + +// exporterN is a global 0-based count of the number of exporters created. +var exporterN atomic.Int64 + +// NextExporterID returns the next unique ID for an exporter. +func NextExporterID() int64 { + const inc = 1 + return exporterN.Add(inc) - inc +} + +// SetExporterID sets the exporter ID counter to v and returns the previous +// value. +// +// This function is useful for testing purposes, allowing you to reset the +// counter. It should not be used in production code. +func SetExporterID(v int64) int64 { + return exporterN.Swap(v) +} diff --git a/exporters/stdout/stdoutmetric/internal/counter/counter_test.go b/exporters/stdout/stdoutmetric/internal/counter/counter_test.go new file mode 100644 index 00000000000..d23bb8a4535 --- /dev/null +++ b/exporters/stdout/stdoutmetric/internal/counter/counter_test.go @@ -0,0 +1,65 @@ +// Code generated by gotmpl. DO NOT MODIFY. +// source: internal/shared/counter/counter_test.go.tmpl + +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package counter + +import ( + "sync" + "testing" +) + +func TestNextExporterID(t *testing.T) { + SetExporterID(0) + + var expected int64 + for range 10 { + id := NextExporterID() + if id != expected { + t.Errorf("NextExporterID() = %d; want %d", id, expected) + } + expected++ + } +} + +func TestSetExporterID(t *testing.T) { + SetExporterID(0) + + prev := SetExporterID(42) + if prev != 0 { + t.Errorf("SetExporterID(42) returned %d; want 0", prev) + } + + id := NextExporterID() + if id != 42 { + t.Errorf("NextExporterID() = %d; want 42", id) + } +} + +func TestNextExporterIDConcurrentSafe(t *testing.T) { + SetExporterID(0) + + const goroutines = 100 + const increments = 10 + + var wg sync.WaitGroup + wg.Add(goroutines) + + for range goroutines { + go func() { + defer wg.Done() + for range increments { + NextExporterID() + } + }() + } + + wg.Wait() + + expected := int64(goroutines * increments) + if id := NextExporterID(); id != expected { + t.Errorf("NextExporterID() = %d; want %d", id, expected) + } +} diff --git a/exporters/stdout/stdoutmetric/internal/gen.go b/exporters/stdout/stdoutmetric/internal/gen.go new file mode 100644 index 00000000000..de48e168a39 --- /dev/null +++ b/exporters/stdout/stdoutmetric/internal/gen.go @@ -0,0 +1,9 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +// Package internal provides internal functionality for the stdoutmetric +// package. +package internal // import "go.opentelemetry.io/otel/exporters/stdout/stdoutmetric/internal" + +//go:generate gotmpl --body=../../../../internal/shared/counter/counter.go.tmpl "--data={ \"pkg\": \"go.opentelemetry.io/otel/exporters/stdout/stdoutmetric/internal/counter\" }" --out=counter/counter.go +//go:generate gotmpl --body=../../../../internal/shared/counter/counter_test.go.tmpl "--data={}" --out=counter/counter_test.go From 9976cfc95bb36a544d8494fcc7c86566ff26e412 Mon Sep 17 00:00:00 2001 From: Mahendra Bishnoi Date: Thu, 28 Aug 2025 23:24:08 +0530 Subject: [PATCH 23/42] review comments: improve tests, merge tests --- .../stdout/stdoutmetric/exporter_test.go | 234 ++++++++++-------- .../selfobservability/selfobservability.go | 3 +- 2 files changed, 130 insertions(+), 107 deletions(-) diff --git a/exporters/stdout/stdoutmetric/exporter_test.go b/exporters/stdout/stdoutmetric/exporter_test.go index fe78d1c6c13..39e71fc99ce 100644 --- a/exporters/stdout/stdoutmetric/exporter_test.go +++ b/exporters/stdout/stdoutmetric/exporter_test.go @@ -19,6 +19,7 @@ import ( "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/exporters/stdout/stdoutmetric" + "go.opentelemetry.io/otel/exporters/stdout/stdoutmetric/internal/counter" "go.opentelemetry.io/otel/sdk" "go.opentelemetry.io/otel/sdk/instrumentation" "go.opentelemetry.io/otel/sdk/metric" @@ -196,21 +197,52 @@ func TestAggregationSelector(t *testing.T) { } func TestExporter_Export_SelfObservability(t *testing.T) { + componentNameAttr := semconv.OTelComponentName("go.opentelemetry.io/otel/exporters/stdout/stdoutmetric.exporter/0") + componentTypeAttr := semconv.OTelComponentTypeKey.String( + "go.opentelemetry.io/otel/exporters/stdout/stdoutmetric.exporter", + ) + wantErr := errors.New("encoding failed") + tests := []struct { name string + ctx context.Context + exporterOpts []stdoutmetric.Option selfObservabilityEnabled bool expectedExportedCount int64 + inflightAttrs attribute.Set + attributes attribute.Set + wantErr error }{ { name: "Enabled", + ctx: context.Background(), + exporterOpts: []stdoutmetric.Option{testEncoderOption()}, selfObservabilityEnabled: true, expectedExportedCount: 19, + inflightAttrs: attribute.NewSet(componentNameAttr, componentTypeAttr), + attributes: attribute.NewSet(componentNameAttr, componentTypeAttr), }, { name: "Disabled", + ctx: context.Background(), + exporterOpts: []stdoutmetric.Option{testEncoderOption()}, selfObservabilityEnabled: false, expectedExportedCount: 0, }, + { + name: "EncodingError", + ctx: context.Background(), + exporterOpts: []stdoutmetric.Option{stdoutmetric.WithEncoder(failingEncoder{})}, + selfObservabilityEnabled: true, + expectedExportedCount: 19, + inflightAttrs: attribute.NewSet(componentNameAttr, componentTypeAttr), + attributes: attribute.NewSet( + componentNameAttr, + componentTypeAttr, + semconv.ErrorType(wantErr), + ), + wantErr: wantErr, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -221,101 +253,25 @@ func TestExporter_Export_SelfObservability(t *testing.T) { otel.SetMeterProvider(mp) t.Cleanup(func() { otel.SetMeterProvider(origMp) }) - exp, err := stdoutmetric.New(testEncoderOption()) + exp, err := stdoutmetric.New(tt.exporterOpts...) require.NoError(t, err) + rm := &metricdata.ResourceMetrics{ScopeMetrics: scopeMetrics()} - rm := &metricdata.ResourceMetrics{ - ScopeMetrics: []metricdata.ScopeMetrics{ - { - Metrics: []metricdata.Metrics{ - { - Name: "gauge_int64", - Data: metricdata.Gauge[int64]{ - DataPoints: []metricdata.DataPoint[int64]{{Value: 1}, {Value: 2}}, - }, - }, - { - Name: "gauge_float64", - Data: metricdata.Gauge[float64]{ - DataPoints: []metricdata.DataPoint[float64]{ - {Value: 1.0}, - {Value: 2.0}, - {Value: 3.0}, - }, - }, - }, - { - Name: "sum_int64", - Data: metricdata.Sum[int64]{ - DataPoints: []metricdata.DataPoint[int64]{{Value: 10}}, - }, - }, - { - Name: "sum_float64", - Data: metricdata.Sum[float64]{ - DataPoints: []metricdata.DataPoint[float64]{{Value: 10.5}, {Value: 20.5}}, - }, - }, - { - Name: "histogram_int64", - Data: metricdata.Histogram[int64]{ - DataPoints: []metricdata.HistogramDataPoint[int64]{ - {Count: 1}, - {Count: 2}, - {Count: 3}, - }, - }, - }, - { - Name: "histogram_float64", - Data: metricdata.Histogram[float64]{ - DataPoints: []metricdata.HistogramDataPoint[float64]{{Count: 1}}, - }, - }, - { - Name: "exponential_histogram_int64", - Data: metricdata.ExponentialHistogram[int64]{ - DataPoints: []metricdata.ExponentialHistogramDataPoint[int64]{ - {Count: 1}, - {Count: 2}, - }, - }, - }, - { - Name: "exponential_histogram_float64", - Data: metricdata.ExponentialHistogram[float64]{ - DataPoints: []metricdata.ExponentialHistogramDataPoint[float64]{ - {Count: 1}, - {Count: 2}, - {Count: 3}, - {Count: 4}, - }, - }, - }, - { - Name: "summary", - Data: metricdata.Summary{ - DataPoints: []metricdata.SummaryDataPoint{{Count: 1}}, - }, - }, - }, - }, - }, + err = exp.Export(tt.ctx, rm) + if tt.wantErr != nil { + assert.EqualError(t, err, tt.wantErr.Error()) + } else { + assert.NoError(t, err) } - ctx := context.Background() - err = exp.Export(ctx, rm) - require.NoError(t, err) - var metrics metricdata.ResourceMetrics - err = reader.Collect(ctx, &metrics) + err = reader.Collect(tt.ctx, &metrics) require.NoError(t, err) if !tt.selfObservabilityEnabled { assert.Empty(t, metrics.ScopeMetrics) } else { assert.Len(t, metrics.ScopeMetrics, 1) - durationMetric := metrics.ScopeMetrics[0].Metrics[2].Data.(metricdata.Histogram[float64]).DataPoints[0] expectedMetrics := metricdata.ScopeMetrics{ Scope: instrumentation.Scope{ Name: "go.opentelemetry.io/otel/exporters/stdout/stdoutmetric", @@ -330,11 +286,8 @@ func TestExporter_Export_SelfObservability(t *testing.T) { Data: metricdata.Sum[int64]{ DataPoints: []metricdata.DataPoint[int64]{ { - Value: 0, - Attributes: attribute.NewSet( - semconv.OTelComponentName("go.opentelemetry.io/otel/exporters/stdout/stdoutmetric.exporter/0"), - semconv.OTelComponentTypeKey.String("go.opentelemetry.io/otel/exporters/stdout/stdoutmetric.exporter"), - ), + Value: 0, + Attributes: tt.inflightAttrs, }, }, Temporality: metricdata.CumulativeTemporality, @@ -347,11 +300,8 @@ func TestExporter_Export_SelfObservability(t *testing.T) { Data: metricdata.Sum[int64]{ DataPoints: []metricdata.DataPoint[int64]{ { - Value: tt.expectedExportedCount, - Attributes: attribute.NewSet( - semconv.OTelComponentName("go.opentelemetry.io/otel/exporters/stdout/stdoutmetric.exporter/0"), - semconv.OTelComponentTypeKey.String("go.opentelemetry.io/otel/exporters/stdout/stdoutmetric.exporter"), - ), + Value: tt.expectedExportedCount, + Attributes: tt.attributes, }, }, Temporality: metricdata.CumulativeTemporality, @@ -365,16 +315,7 @@ func TestExporter_Export_SelfObservability(t *testing.T) { Data: metricdata.Histogram[float64]{ DataPoints: []metricdata.HistogramDataPoint[float64]{ { - Attributes: attribute.NewSet( - semconv.OTelComponentName("go.opentelemetry.io/otel/exporters/stdout/stdoutmetric.exporter/0"), - semconv.OTelComponentTypeKey.String("go.opentelemetry.io/otel/exporters/stdout/stdoutmetric.exporter"), - ), - Count: durationMetric.Count, - Bounds: durationMetric.Bounds, - BucketCounts: durationMetric.BucketCounts, - Min: durationMetric.Min, - Max: durationMetric.Max, - Sum: durationMetric.Sum, + Attributes: tt.attributes, }, }, Temporality: metricdata.CumulativeTemporality, @@ -382,12 +323,95 @@ func TestExporter_Export_SelfObservability(t *testing.T) { }, }, } - metricdatatest.AssertEqual(t, expectedMetrics, metrics.ScopeMetrics[0], metricdatatest.IgnoreTimestamp()) + assert.Equal(t, expectedMetrics.Scope, metrics.ScopeMetrics[0].Scope) + metricdatatest.AssertEqual(t, expectedMetrics.Metrics[0], metrics.ScopeMetrics[0].Metrics[0], metricdatatest.IgnoreTimestamp()) + metricdatatest.AssertEqual(t, expectedMetrics.Metrics[1], metrics.ScopeMetrics[0].Metrics[1], metricdatatest.IgnoreTimestamp()) + metricdatatest.AssertEqual(t, expectedMetrics.Metrics[2], metrics.ScopeMetrics[0].Metrics[2], metricdatatest.IgnoreTimestamp(), metricdatatest.IgnoreValue()) } + _ = counter.SetExporterID(0) }) } } +func scopeMetrics() []metricdata.ScopeMetrics { + return []metricdata.ScopeMetrics{ + { + Metrics: []metricdata.Metrics{ + { + Name: "gauge_int64", + Data: metricdata.Gauge[int64]{ + DataPoints: []metricdata.DataPoint[int64]{{Value: 1}, {Value: 2}}, + }, + }, + { + Name: "gauge_float64", + Data: metricdata.Gauge[float64]{ + DataPoints: []metricdata.DataPoint[float64]{ + {Value: 1.0}, + {Value: 2.0}, + {Value: 3.0}, + }, + }, + }, + { + Name: "sum_int64", + Data: metricdata.Sum[int64]{ + DataPoints: []metricdata.DataPoint[int64]{{Value: 10}}, + }, + }, + { + Name: "sum_float64", + Data: metricdata.Sum[float64]{ + DataPoints: []metricdata.DataPoint[float64]{{Value: 10.5}, {Value: 20.5}}, + }, + }, + { + Name: "histogram_int64", + Data: metricdata.Histogram[int64]{ + DataPoints: []metricdata.HistogramDataPoint[int64]{ + {Count: 1}, + {Count: 2}, + {Count: 3}, + }, + }, + }, + { + Name: "histogram_float64", + Data: metricdata.Histogram[float64]{ + DataPoints: []metricdata.HistogramDataPoint[float64]{{Count: 1}}, + }, + }, + { + Name: "exponential_histogram_int64", + Data: metricdata.ExponentialHistogram[int64]{ + DataPoints: []metricdata.ExponentialHistogramDataPoint[int64]{ + {Count: 1}, + {Count: 2}, + }, + }, + }, + { + Name: "exponential_histogram_float64", + Data: metricdata.ExponentialHistogram[float64]{ + DataPoints: []metricdata.ExponentialHistogramDataPoint[float64]{ + {Count: 1}, + {Count: 2}, + {Count: 3}, + {Count: 4}, + }, + }, + }, + { + Name: "summary", + Data: metricdata.Summary{ + DataPoints: []metricdata.SummaryDataPoint{{Count: 1}}, + }, + }, + }, + }, + } +} + func TestExporter_Export_EncodingErrorTracking(t *testing.T) { t.Setenv("OTEL_GO_X_SELF_OBSERVABILITY", "true") reader := metric.NewManualReader() diff --git a/exporters/stdout/stdoutmetric/internal/selfobservability/selfobservability.go b/exporters/stdout/stdoutmetric/internal/selfobservability/selfobservability.go index 3c1672c1944..7f155c6f324 100644 --- a/exporters/stdout/stdoutmetric/internal/selfobservability/selfobservability.go +++ b/exporters/stdout/stdoutmetric/internal/selfobservability/selfobservability.go @@ -80,8 +80,7 @@ func (em *ExporterMetrics) TrackExport(ctx context.Context, count int64) func(er *attrs = (*attrs)[:0] // reset the slice for reuse measureAttrsPool.Put(attrs) }() - copy(*attrs, em.attrs) - *attrs = append(*attrs, semconv.ErrorType(err)) + *attrs = append(*attrs, em.attrs[0], em.attrs[1], semconv.ErrorType(err)) } em.exported.Add(ctx, count, *attrs...) em.duration.Record(ctx, durationSeconds, *attrs...) From 2f84c09231bb68df345f967639ba2e8eb41b378b Mon Sep 17 00:00:00 2001 From: Mahendra Bishnoi Date: Thu, 28 Aug 2025 23:44:24 +0530 Subject: [PATCH 24/42] Run `make precommit` --- exporters/stdout/stdoutmetric/internal/counter/counter_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/exporters/stdout/stdoutmetric/internal/counter/counter_test.go b/exporters/stdout/stdoutmetric/internal/counter/counter_test.go index d23bb8a4535..f3e380d3325 100644 --- a/exporters/stdout/stdoutmetric/internal/counter/counter_test.go +++ b/exporters/stdout/stdoutmetric/internal/counter/counter_test.go @@ -62,4 +62,4 @@ func TestNextExporterIDConcurrentSafe(t *testing.T) { if id := NextExporterID(); id != expected { t.Errorf("NextExporterID() = %d; want %d", id, expected) } -} +} \ No newline at end of file From 1f1eb84915b19e7a4eb18f02c8778407f30612d1 Mon Sep 17 00:00:00 2001 From: Mahendra Bishnoi Date: Sat, 11 Oct 2025 23:37:49 +0530 Subject: [PATCH 25/42] run `make precommit` --- exporters/stdout/stdoutmetric/exporter.go | 2 +- exporters/stdout/stdoutmetric/exporter_test.go | 8 ++++---- exporters/stdout/stdoutmetric/go.mod | 2 +- .../internal/selfobservability/selfobservability_test.go | 6 +++--- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/exporters/stdout/stdoutmetric/exporter.go b/exporters/stdout/stdoutmetric/exporter.go index 5f5606effe8..5b4a2b5975b 100644 --- a/exporters/stdout/stdoutmetric/exporter.go +++ b/exporters/stdout/stdoutmetric/exporter.go @@ -77,7 +77,7 @@ func (e *exporter) Export(ctx context.Context, data *metricdata.ResourceMetrics) defer func() { trackExportFunc(err) }() err = ctx.Err() if err != nil { - return + return err } if e.redactTimestamps { redactTimestamps(data) diff --git a/exporters/stdout/stdoutmetric/exporter_test.go b/exporters/stdout/stdoutmetric/exporter_test.go index 3368abd93ea..6fd5f01e736 100644 --- a/exporters/stdout/stdoutmetric/exporter_test.go +++ b/exporters/stdout/stdoutmetric/exporter_test.go @@ -215,7 +215,7 @@ func TestExporter_Export_SelfObservability(t *testing.T) { }{ { name: "Enabled", - ctx: context.Background(), + ctx: t.Context(), exporterOpts: []stdoutmetric.Option{testEncoderOption()}, selfObservabilityEnabled: true, expectedExportedCount: 19, @@ -224,14 +224,14 @@ func TestExporter_Export_SelfObservability(t *testing.T) { }, { name: "Disabled", - ctx: context.Background(), + ctx: t.Context(), exporterOpts: []stdoutmetric.Option{testEncoderOption()}, selfObservabilityEnabled: false, expectedExportedCount: 0, }, { name: "EncodingError", - ctx: context.Background(), + ctx: t.Context(), exporterOpts: []stdoutmetric.Option{stdoutmetric.WithEncoder(failingEncoder{})}, selfObservabilityEnabled: true, expectedExportedCount: 19, @@ -438,7 +438,7 @@ func TestExporter_Export_EncodingErrorTracking(t *testing.T) { }, } - ctx := context.Background() + ctx := t.Context() err = exp.Export(ctx, rm) assert.EqualError(t, err, "encoding failed") diff --git a/exporters/stdout/stdoutmetric/go.mod b/exporters/stdout/stdoutmetric/go.mod index 50a558725cb..a0b19f2bbe9 100644 --- a/exporters/stdout/stdoutmetric/go.mod +++ b/exporters/stdout/stdoutmetric/go.mod @@ -5,6 +5,7 @@ go 1.24.0 require ( github.com/stretchr/testify v1.11.1 go.opentelemetry.io/otel v1.38.0 + go.opentelemetry.io/otel/metric v1.38.0 go.opentelemetry.io/otel/sdk v1.38.0 go.opentelemetry.io/otel/sdk/metric v1.38.0 ) @@ -16,7 +17,6 @@ require ( github.com/google/uuid v1.6.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect - go.opentelemetry.io/otel/metric v1.38.0 // indirect go.opentelemetry.io/otel/trace v1.38.0 // indirect golang.org/x/sys v0.37.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/exporters/stdout/stdoutmetric/internal/selfobservability/selfobservability_test.go b/exporters/stdout/stdoutmetric/internal/selfobservability/selfobservability_test.go index 0b5d8521250..e0ec9424a13 100644 --- a/exporters/stdout/stdoutmetric/internal/selfobservability/selfobservability_test.go +++ b/exporters/stdout/stdoutmetric/internal/selfobservability/selfobservability_test.go @@ -45,7 +45,7 @@ func setupTestMeterProvider(t *testing.T) *testSetup { return &testSetup{ reader: reader, mp: mp, - ctx: context.Background(), + ctx: t.Context(), em: em, } } @@ -173,9 +173,9 @@ func TestExporterMetrics_AttributesNotPermanentlyModified(t *testing.T) { assert.Contains(t, em.attrs, componentName) assert.Contains(t, em.attrs, componentType) - done := em.TrackExport(context.Background(), 1) + done := em.TrackExport(t.Context(), 1) done(errors.New("test error")) - done = em.TrackExport(context.Background(), 1) + done = em.TrackExport(t.Context(), 1) done(nil) assert.Len(t, em.attrs, 2) From 51e48eb38ef4a87da6ade7fff49f1e8143206957 Mon Sep 17 00:00:00 2001 From: Mahendra Bishnoi Date: Sat, 11 Oct 2025 23:41:05 +0530 Subject: [PATCH 26/42] fix CHANGELOG.md --- CHANGELOG.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 643a5dd6627..79818af984a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm - Add experimental observability metrics in `go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc`. (#7353) - Add experimental observability metrics in `go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc`. (#7459) - Add experimental observability metrics in `go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp`. (#7486) +- Add experimental observability metrics in `go.opentelemetry.io/otel/exporters/stdout/stdoutmetric`. (#7492)s ### Fixed @@ -110,8 +111,6 @@ The next release will require at least [Go 1.24]. - The `go.opentelemetry.io/otel/semconv/v1.37.0` package. The package contains semantic conventions from the `v1.37.0` version of the OpenTelemetry Semantic Conventions. See the [migration documentation](./semconv/v1.37.0/MIGRATION.md) for information on how to upgrade from `go.opentelemetry.io/otel/semconv/v1.36.0.`(#7254) -- Add experimental self-observability stdoutmetric exporter metrics in `go.opentelemetry.io/otel/exporters/stdout/stdoutmetric`. - Check the `go.opentelemetry.io/otel/exporters/stdout/stdoutmetric/internal/x` package documentation for more information. (#7150) ### Changed From 7f64c38fbee2ca93bc1655aef6d7bdaa9fd460a1 Mon Sep 17 00:00:00 2001 From: Mahendra Bishnoi Date: Sat, 11 Oct 2025 23:51:18 +0530 Subject: [PATCH 27/42] refactoring to accommodate auto generation of x.go, replace usages of `OTEL_GO_X_SELF_OBSERVABILITY` with `OTEL_GO_X_OBSERVABILITY` --- exporters/stdout/stdoutmetric/exporter.go | 2 +- .../stdout/stdoutmetric/exporter_test.go | 4 +- exporters/stdout/stdoutmetric/internal/gen.go | 3 ++ .../stdout/stdoutmetric/internal/x/README.md | 2 +- .../stdoutmetric/internal/x/features.go | 22 ++++++++++ .../stdoutmetric/internal/x/features_test.go | 24 +++++++++++ exporters/stdout/stdoutmetric/internal/x/x.go | 43 ++++++++----------- .../stdout/stdoutmetric/internal/x/x_test.go | 32 ++++++++++---- 8 files changed, 96 insertions(+), 36 deletions(-) create mode 100644 exporters/stdout/stdoutmetric/internal/x/features.go create mode 100644 exporters/stdout/stdoutmetric/internal/x/features_test.go diff --git a/exporters/stdout/stdoutmetric/exporter.go b/exporters/stdout/stdoutmetric/exporter.go index 5b4a2b5975b..39ac6af78c2 100644 --- a/exporters/stdout/stdoutmetric/exporter.go +++ b/exporters/stdout/stdoutmetric/exporter.go @@ -49,7 +49,7 @@ func New(options ...Option) (metric.Exporter, error) { temporalitySelector: cfg.temporalitySelector, aggregationSelector: cfg.aggregationSelector, redactTimestamps: cfg.redactTimestamps, - selfObservabilityEnabled: x.SelfObservability.Enabled(), + selfObservabilityEnabled: x.Observability.Enabled(), } exp.encVal.Store(*cfg.encoder) var err error diff --git a/exporters/stdout/stdoutmetric/exporter_test.go b/exporters/stdout/stdoutmetric/exporter_test.go index 6fd5f01e736..3da899106ff 100644 --- a/exporters/stdout/stdoutmetric/exporter_test.go +++ b/exporters/stdout/stdoutmetric/exporter_test.go @@ -246,7 +246,7 @@ func TestExporter_Export_SelfObservability(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - t.Setenv("OTEL_GO_X_SELF_OBSERVABILITY", strconv.FormatBool(tt.selfObservabilityEnabled)) + t.Setenv("OTEL_GO_X_OBSERVABILITY", strconv.FormatBool(tt.selfObservabilityEnabled)) reader := metric.NewManualReader() mp := metric.NewMeterProvider(metric.WithReader(reader)) origMp := otel.GetMeterProvider() @@ -413,7 +413,7 @@ func scopeMetrics() []metricdata.ScopeMetrics { } func TestExporter_Export_EncodingErrorTracking(t *testing.T) { - t.Setenv("OTEL_GO_X_SELF_OBSERVABILITY", "true") + t.Setenv("OTEL_GO_X_OBSERVABILITY", "true") reader := metric.NewManualReader() mp := metric.NewMeterProvider(metric.WithReader(reader)) origMp := otel.GetMeterProvider() diff --git a/exporters/stdout/stdoutmetric/internal/gen.go b/exporters/stdout/stdoutmetric/internal/gen.go index de48e168a39..725722aa7ba 100644 --- a/exporters/stdout/stdoutmetric/internal/gen.go +++ b/exporters/stdout/stdoutmetric/internal/gen.go @@ -7,3 +7,6 @@ package internal // import "go.opentelemetry.io/otel/exporters/stdout/stdoutmetr //go:generate gotmpl --body=../../../../internal/shared/counter/counter.go.tmpl "--data={ \"pkg\": \"go.opentelemetry.io/otel/exporters/stdout/stdoutmetric/internal/counter\" }" --out=counter/counter.go //go:generate gotmpl --body=../../../../internal/shared/counter/counter_test.go.tmpl "--data={}" --out=counter/counter_test.go + +//go:generate gotmpl --body=../../../../internal/shared/x/x.go.tmpl "--data={ \"pkg\": \"go.opentelemetry.io/otel/exporters/stdout/stdoutmetric/internal\" }" --out=x/x.go +//go:generate gotmpl --body=../../../../internal/shared/x/x_test.go.tmpl "--data={}" --out=x/x_test.go diff --git a/exporters/stdout/stdoutmetric/internal/x/README.md b/exporters/stdout/stdoutmetric/internal/x/README.md index 3a64525ae59..b465e260741 100644 --- a/exporters/stdout/stdoutmetric/internal/x/README.md +++ b/exporters/stdout/stdoutmetric/internal/x/README.md @@ -14,7 +14,7 @@ See the [Compatibility and Stability](#compatibility-and-stability) section for The `stdoutmetric` exporter provides a self-observability feature that allows you to monitor the exporter itself. -To opt-in, set the environment variable `OTEL_GO_X_SELF_OBSERVABILITY` to `true`. +To opt-in, set the environment variable `OTEL_GO_X_OBSERVABILITY` to `true`. When enabled, the exporter will create the following metrics using the global `MeterProvider`: diff --git a/exporters/stdout/stdoutmetric/internal/x/features.go b/exporters/stdout/stdoutmetric/internal/x/features.go new file mode 100644 index 00000000000..460d2244c16 --- /dev/null +++ b/exporters/stdout/stdoutmetric/internal/x/features.go @@ -0,0 +1,22 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package x // import "go.opentelemetry.io/otel/exporters/stdout/stdoutmetric/internal/x" + +import "strings" + +// Observability is an experimental feature flag that determines if SDK +// observability metrics are enabled. +// +// To enable this feature set the OTEL_GO_X_OBSERVABILITY environment variable +// to the case-insensitive string value of "true" (i.e. "True" and "TRUE" +// will also enable this). +var Observability = newFeature( + []string{"OBSERVABILITY", "SELF_OBSERVABILITY"}, + func(v string) (string, bool) { + if strings.EqualFold(v, "true") { + return v, true + } + return "", false + }, +) diff --git a/exporters/stdout/stdoutmetric/internal/x/features_test.go b/exporters/stdout/stdoutmetric/internal/x/features_test.go new file mode 100644 index 00000000000..de307257923 --- /dev/null +++ b/exporters/stdout/stdoutmetric/internal/x/features_test.go @@ -0,0 +1,24 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package x + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestObservability(t *testing.T) { + const key = "OTEL_GO_X_OBSERVABILITY" + require.Contains(t, Observability.Keys(), key) + + const altKey = "OTEL_GO_X_SELF_OBSERVABILITY" + require.Contains(t, Observability.Keys(), altKey) + + t.Run("100", run(setenv(key, "100"), assertDisabled(Observability))) + t.Run("true", run(setenv(key, "true"), assertEnabled(Observability, "true"))) + t.Run("True", run(setenv(key, "True"), assertEnabled(Observability, "True"))) + t.Run("false", run(setenv(key, "false"), assertDisabled(Observability))) + t.Run("empty", run(assertDisabled(Observability))) +} diff --git a/exporters/stdout/stdoutmetric/internal/x/x.go b/exporters/stdout/stdoutmetric/internal/x/x.go index fb6dfd97564..3a0e42897a7 100644 --- a/exporters/stdout/stdoutmetric/internal/x/x.go +++ b/exporters/stdout/stdoutmetric/internal/x/x.go @@ -1,45 +1,38 @@ +// Code generated by gotmpl. DO NOT MODIFY. +// source: internal/shared/x/x.go.tmpl + // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -// Package x documents experimental features for [go.opentelemetry.io/otel/exporters/stdout/stdoutmetric]. +// Package x documents experimental features for [go.opentelemetry.io/otel/exporters/stdout/stdoutmetric/internal]. package x // import "go.opentelemetry.io/otel/exporters/stdout/stdoutmetric/internal/x" import ( "os" - "strings" ) -// SelfObservability is an experimental feature flag that determines if SDK -// self-observability metrics are enabled. -// -// To enable this feature set the OTEL_GO_X_SELF_OBSERVABILITY environment variable -// to the case-insensitive string value of "true" (i.e. "True" and "TRUE" -// will also enable this). -var SelfObservability = newFeature("SELF_OBSERVABILITY", func(v string) (string, bool) { - if strings.EqualFold(v, "true") { - return v, true - } - return "", false -}) - // Feature is an experimental feature control flag. It provides a uniform way // to interact with these feature flags and parse their values. type Feature[T any] struct { - key string + keys []string parse func(v string) (T, bool) } -func newFeature[T any](suffix string, parse func(string) (T, bool)) Feature[T] { +func newFeature[T any](suffix []string, parse func(string) (T, bool)) Feature[T] { const envKeyRoot = "OTEL_GO_X_" + keys := make([]string, 0, len(suffix)) + for _, s := range suffix { + keys = append(keys, envKeyRoot+s) + } return Feature[T]{ - key: envKeyRoot + suffix, + keys: keys, parse: parse, } } -// Key returns the environment variable key that needs to be set to enable the +// Keys returns the environment variable keys that can be set to enable the // feature. -func (f Feature[T]) Key() string { return f.key } +func (f Feature[T]) Keys() []string { return f.keys } // Lookup returns the user configured value for the feature and true if the // user has enabled the feature. Otherwise, if the feature is not enabled, a @@ -49,11 +42,13 @@ func (f Feature[T]) Lookup() (v T, ok bool) { // // > The SDK MUST interpret an empty value of an environment variable the // > same way as when the variable is unset. - vRaw := os.Getenv(f.key) - if vRaw == "" { - return v, ok + for _, key := range f.keys { + vRaw := os.Getenv(key) + if vRaw != "" { + return f.parse(vRaw) + } } - return f.parse(vRaw) + return v, ok } // Enabled reports whether the feature is enabled. diff --git a/exporters/stdout/stdoutmetric/internal/x/x_test.go b/exporters/stdout/stdoutmetric/internal/x/x_test.go index 15124ca91d1..a715d7608a7 100644 --- a/exporters/stdout/stdoutmetric/internal/x/x_test.go +++ b/exporters/stdout/stdoutmetric/internal/x/x_test.go @@ -1,24 +1,40 @@ +// Code generated by gotmpl. DO NOT MODIFY. +// source: internal/shared/x/x_text.go.tmpl + // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 package x import ( + "strings" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -func TestSelfObservability(t *testing.T) { - const key = "OTEL_GO_X_SELF_OBSERVABILITY" - require.Equal(t, key, SelfObservability.Key()) +const ( + mockKey = "OTEL_GO_X_MOCK_FEATURE" + mockKey2 = "OTEL_GO_X_MOCK_FEATURE2" +) + +var mockFeature = newFeature([]string{"MOCK_FEATURE", "MOCK_FEATURE2"}, func(v string) (string, bool) { + if strings.EqualFold(v, "true") { + return v, true + } + return "", false +}) + +func TestFeature(t *testing.T) { + require.Contains(t, mockFeature.Keys(), mockKey) + require.Contains(t, mockFeature.Keys(), mockKey2) - t.Run("100", run(setenv(key, "100"), assertDisabled(SelfObservability))) - t.Run("true", run(setenv(key, "true"), assertEnabled(SelfObservability, "true"))) - t.Run("True", run(setenv(key, "True"), assertEnabled(SelfObservability, "True"))) - t.Run("false", run(setenv(key, "false"), assertDisabled(SelfObservability))) - t.Run("empty", run(assertDisabled(SelfObservability))) + t.Run("100", run(setenv(mockKey, "100"), assertDisabled(mockFeature))) + t.Run("true", run(setenv(mockKey, "true"), assertEnabled(mockFeature, "true"))) + t.Run("True", run(setenv(mockKey, "True"), assertEnabled(mockFeature, "True"))) + t.Run("false", run(setenv(mockKey, "false"), assertDisabled(mockFeature))) + t.Run("empty", run(assertDisabled(mockFeature))) } func run(steps ...func(*testing.T)) func(*testing.T) { From c1069883ebe6e7ef4dd44fe181f10e595b8c83d6 Mon Sep 17 00:00:00 2001 From: Mahendra Bishnoi Date: Sun, 12 Oct 2025 09:12:06 +0530 Subject: [PATCH 28/42] benchmark for TrackExport --- .../selfobservability_test.go | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/exporters/stdout/stdoutmetric/internal/selfobservability/selfobservability_test.go b/exporters/stdout/stdoutmetric/internal/selfobservability/selfobservability_test.go index e0ec9424a13..846dd042964 100644 --- a/exporters/stdout/stdoutmetric/internal/selfobservability/selfobservability_test.go +++ b/exporters/stdout/stdoutmetric/internal/selfobservability/selfobservability_test.go @@ -10,6 +10,8 @@ import ( "time" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.opentelemetry.io/otel/metric/noop" "go.opentelemetry.io/otel" sdkmetric "go.opentelemetry.io/otel/sdk/metric" @@ -182,3 +184,53 @@ func TestExporterMetrics_AttributesNotPermanentlyModified(t *testing.T) { assert.Contains(t, em.attrs, componentName) assert.Contains(t, em.attrs, componentType) } + +func BenchmarkTrackExport(b *testing.B) { + b.Setenv("OTEL_GO_X_OBSERVABILITY", "true") + orig := otel.GetMeterProvider() + b.Cleanup(func() { + otel.SetMeterProvider(orig) + }) + + // Ensure deterministic benchmark by using noop meter. + otel.SetMeterProvider(noop.NewMeterProvider()) + + newExp := func(b *testing.B) *ExporterMetrics { + b.Helper() + componentName := semconv.OTelComponentName("benchmark") + componentType := semconv.OTelComponentTypeKey.String("exporter") + em, err := NewExporterMetrics( + "go.opentelemetry.io/otel/exporters/stdout/stdoutmetric", + componentName, + componentType, + ) + require.NoError(b, err) + require.NotNil(b, em) + return em + } + + b.Run("Success", func(b *testing.B) { + em := newExp(b) + b.ResetTimer() + b.ReportAllocs() + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + done := em.TrackExport(b.Context(), 10) + done(nil) + } + }) + }) + + b.Run("WithError", func(b *testing.B) { + em := newExp(b) + testErr := errors.New("export failed") + b.ResetTimer() + b.ReportAllocs() + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + done := em.TrackExport(b.Context(), 10) + done(testErr) + } + }) + }) +} From 49c5c9abd8d5abc4ff1c3d2ceb397e817fdd239a Mon Sep 17 00:00:00 2001 From: Mahendra Bishnoi Date: Sun, 12 Oct 2025 09:16:38 +0530 Subject: [PATCH 29/42] use precomputed set for happy path, still not optimal 5 allocs/op --- .../selfobservability/selfobservability.go | 37 ++++++++++++------- 1 file changed, 24 insertions(+), 13 deletions(-) diff --git a/exporters/stdout/stdoutmetric/internal/selfobservability/selfobservability.go b/exporters/stdout/stdoutmetric/internal/selfobservability/selfobservability.go index 7f155c6f324..47f34402273 100644 --- a/exporters/stdout/stdoutmetric/internal/selfobservability/selfobservability.go +++ b/exporters/stdout/stdoutmetric/internal/selfobservability/selfobservability.go @@ -36,14 +36,17 @@ type ExporterMetrics struct { exported otelconv.SDKExporterMetricDataPointExported duration otelconv.SDKExporterOperationDuration attrs []attribute.KeyValue + set attribute.Set } func NewExporterMetrics( name string, componentName, componentType attribute.KeyValue, ) (*ExporterMetrics, error) { + attrs := []attribute.KeyValue{componentName, componentType} em := &ExporterMetrics{ - attrs: []attribute.KeyValue{componentName, componentType}, + attrs: attrs, + set: attribute.NewSet(attrs...), } mp := otel.GetMeterProvider() m := mp.Meter( @@ -69,20 +72,28 @@ func NewExporterMetrics( func (em *ExporterMetrics) TrackExport(ctx context.Context, count int64) func(err error) { begin := time.Now() - em.inflight.Add(ctx, count, em.attrs...) + em.inflight.AddSet(ctx, count, em.set) return func(err error) { durationSeconds := time.Since(begin).Seconds() - attrs := &em.attrs - em.inflight.Add(ctx, -count, *attrs...) - if err != nil { - attrs = measureAttrsPool.Get().(*[]attribute.KeyValue) - defer func() { - *attrs = (*attrs)[:0] // reset the slice for reuse - measureAttrsPool.Put(attrs) - }() - *attrs = append(*attrs, em.attrs[0], em.attrs[1], semconv.ErrorType(err)) + em.inflight.AddSet(ctx, -count, em.set) + if err == nil { + em.exported.AddSet(ctx, count, em.set) + em.duration.RecordSet(ctx, durationSeconds, em.set) + return } - em.exported.Add(ctx, count, *attrs...) - em.duration.Record(ctx, durationSeconds, *attrs...) + + attrs := measureAttrsPool.Get().(*[]attribute.KeyValue) + defer func() { + *attrs = (*attrs)[:0] // reset the slice for reuse + measureAttrsPool.Put(attrs) + }() + *attrs = append(*attrs, em.attrs...) + *attrs = append(*attrs, semconv.ErrorType(err)) + + // Do not inefficiently make a copy of attrs by using + // WithAttributes instead of WithAttributeSet. + set := attribute.NewSet(*attrs...) + em.exported.AddSet(ctx, count, set) + em.duration.RecordSet(ctx, durationSeconds, set) } } From 1643d6d29225fba30c480808aaac17c96ff8d2ca Mon Sep 17 00:00:00 2001 From: Mahendra Bishnoi Date: Sun, 12 Oct 2025 09:34:34 +0530 Subject: [PATCH 30/42] happy path allocs reduced to 1/op, will fix the ugly code later --- .../selfobservability/selfobservability.go | 34 ++++++++++++------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/exporters/stdout/stdoutmetric/internal/selfobservability/selfobservability.go b/exporters/stdout/stdoutmetric/internal/selfobservability/selfobservability.go index 47f34402273..98a7ac46b4d 100644 --- a/exporters/stdout/stdoutmetric/internal/selfobservability/selfobservability.go +++ b/exporters/stdout/stdoutmetric/internal/selfobservability/selfobservability.go @@ -32,11 +32,14 @@ var measureAttrsPool = sync.Pool{ } type ExporterMetrics struct { - inflight otelconv.SDKExporterMetricDataPointInflight - exported otelconv.SDKExporterMetricDataPointExported - duration otelconv.SDKExporterOperationDuration - attrs []attribute.KeyValue - set attribute.Set + inflight otelconv.SDKExporterMetricDataPointInflight + inflightCounter metric.Int64UpDownCounter + addOpts []metric.AddOption + exported otelconv.SDKExporterMetricDataPointExported + duration otelconv.SDKExporterOperationDuration + recordOpts []metric.RecordOption + attrs []attribute.KeyValue + set attribute.Set } func NewExporterMetrics( @@ -44,9 +47,15 @@ func NewExporterMetrics( componentName, componentType attribute.KeyValue, ) (*ExporterMetrics, error) { attrs := []attribute.KeyValue{componentName, componentType} + attrSet := attribute.NewSet(attrs...) + attrOpts := metric.WithAttributeSet(attrSet) + addOpts := []metric.AddOption{attrOpts} + recordOpts := []metric.RecordOption{attrOpts} em := &ExporterMetrics{ - attrs: attrs, - set: attribute.NewSet(attrs...), + attrs: attrs, + addOpts: addOpts, + set: attrSet, + recordOpts: recordOpts, } mp := otel.GetMeterProvider() m := mp.Meter( @@ -59,6 +68,7 @@ func NewExporterMetrics( e = fmt.Errorf("failed to create metric_data_point inflight metric: %w", e) err = errors.Join(err, e) } + em.inflightCounter = em.inflight.Int64UpDownCounter if em.exported, e = otelconv.NewSDKExporterMetricDataPointExported(m); e != nil { e = fmt.Errorf("failed to create metric_data_point exported metric: %w", e) err = errors.Join(err, e) @@ -72,13 +82,13 @@ func NewExporterMetrics( func (em *ExporterMetrics) TrackExport(ctx context.Context, count int64) func(err error) { begin := time.Now() - em.inflight.AddSet(ctx, count, em.set) + em.inflightCounter.Add(ctx, count, em.addOpts...) return func(err error) { durationSeconds := time.Since(begin).Seconds() - em.inflight.AddSet(ctx, -count, em.set) + em.inflightCounter.Add(ctx, -count, em.addOpts...) if err == nil { - em.exported.AddSet(ctx, count, em.set) - em.duration.RecordSet(ctx, durationSeconds, em.set) + em.exported.Int64Counter.Add(ctx, count, em.addOpts...) + em.duration.Float64Histogram.Record(ctx, durationSeconds, em.recordOpts...) return } @@ -90,8 +100,6 @@ func (em *ExporterMetrics) TrackExport(ctx context.Context, count int64) func(er *attrs = append(*attrs, em.attrs...) *attrs = append(*attrs, semconv.ErrorType(err)) - // Do not inefficiently make a copy of attrs by using - // WithAttributes instead of WithAttributeSet. set := attribute.NewSet(*attrs...) em.exported.AddSet(ctx, count, set) em.duration.RecordSet(ctx, durationSeconds, set) From 3903f8559491313ddda9e875135c44b6bff862d6 Mon Sep 17 00:00:00 2001 From: Mahendra Bishnoi Date: Sun, 12 Oct 2025 09:52:55 +0530 Subject: [PATCH 31/42] minor refactor --- .../selfobservability/selfobservability.go | 29 +++++++++---------- .../selfobservability_test.go | 2 +- 2 files changed, 14 insertions(+), 17 deletions(-) diff --git a/exporters/stdout/stdoutmetric/internal/selfobservability/selfobservability.go b/exporters/stdout/stdoutmetric/internal/selfobservability/selfobservability.go index 98a7ac46b4d..325fc3ecc68 100644 --- a/exporters/stdout/stdoutmetric/internal/selfobservability/selfobservability.go +++ b/exporters/stdout/stdoutmetric/internal/selfobservability/selfobservability.go @@ -32,14 +32,12 @@ var measureAttrsPool = sync.Pool{ } type ExporterMetrics struct { - inflight otelconv.SDKExporterMetricDataPointInflight - inflightCounter metric.Int64UpDownCounter - addOpts []metric.AddOption - exported otelconv.SDKExporterMetricDataPointExported - duration otelconv.SDKExporterOperationDuration - recordOpts []metric.RecordOption - attrs []attribute.KeyValue - set attribute.Set + inflight metric.Int64UpDownCounter + addOpts []metric.AddOption + exported otelconv.SDKExporterMetricDataPointExported + duration otelconv.SDKExporterOperationDuration + recordOpts []metric.RecordOption + attrs []attribute.KeyValue } func NewExporterMetrics( @@ -47,14 +45,12 @@ func NewExporterMetrics( componentName, componentType attribute.KeyValue, ) (*ExporterMetrics, error) { attrs := []attribute.KeyValue{componentName, componentType} - attrSet := attribute.NewSet(attrs...) - attrOpts := metric.WithAttributeSet(attrSet) + attrOpts := metric.WithAttributeSet(attribute.NewSet(attrs...)) addOpts := []metric.AddOption{attrOpts} recordOpts := []metric.RecordOption{attrOpts} em := &ExporterMetrics{ attrs: attrs, addOpts: addOpts, - set: attrSet, recordOpts: recordOpts, } mp := otel.GetMeterProvider() @@ -63,12 +59,13 @@ func NewExporterMetrics( metric.WithInstrumentationVersion(sdk.Version()), metric.WithSchemaURL(semconv.SchemaURL)) - var err, e error - if em.inflight, e = otelconv.NewSDKExporterMetricDataPointInflight(m); e != nil { + var err error + inflightMetric, e := otelconv.NewSDKExporterMetricDataPointInflight(m) + if e != nil { e = fmt.Errorf("failed to create metric_data_point inflight metric: %w", e) err = errors.Join(err, e) } - em.inflightCounter = em.inflight.Int64UpDownCounter + em.inflight = inflightMetric.Int64UpDownCounter if em.exported, e = otelconv.NewSDKExporterMetricDataPointExported(m); e != nil { e = fmt.Errorf("failed to create metric_data_point exported metric: %w", e) err = errors.Join(err, e) @@ -82,10 +79,10 @@ func NewExporterMetrics( func (em *ExporterMetrics) TrackExport(ctx context.Context, count int64) func(err error) { begin := time.Now() - em.inflightCounter.Add(ctx, count, em.addOpts...) + em.inflight.Add(ctx, count, em.addOpts...) return func(err error) { durationSeconds := time.Since(begin).Seconds() - em.inflightCounter.Add(ctx, -count, em.addOpts...) + em.inflight.Add(ctx, -count, em.addOpts...) if err == nil { em.exported.Int64Counter.Add(ctx, count, em.addOpts...) em.duration.Float64Histogram.Record(ctx, durationSeconds, em.recordOpts...) diff --git a/exporters/stdout/stdoutmetric/internal/selfobservability/selfobservability_test.go b/exporters/stdout/stdoutmetric/internal/selfobservability/selfobservability_test.go index 846dd042964..ae64eebe3db 100644 --- a/exporters/stdout/stdoutmetric/internal/selfobservability/selfobservability_test.go +++ b/exporters/stdout/stdoutmetric/internal/selfobservability/selfobservability_test.go @@ -11,9 +11,9 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "go.opentelemetry.io/otel/metric/noop" "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/metric/noop" sdkmetric "go.opentelemetry.io/otel/sdk/metric" "go.opentelemetry.io/otel/sdk/metric/metricdata" semconv "go.opentelemetry.io/otel/semconv/v1.36.0" From 7ede99093f185036cb6dcfb3a3ce4999d34e98d3 Mon Sep 17 00:00:00 2001 From: Mahendra Bishnoi Date: Sun, 12 Oct 2025 11:13:43 +0530 Subject: [PATCH 32/42] move feature flag part to observ package. --- exporters/stdout/stdoutmetric/exporter.go | 30 ++++---------- .../exporter.go} | 39 +++++++++++++------ .../exporter_test.go} | 38 +++++++----------- 3 files changed, 49 insertions(+), 58 deletions(-) rename exporters/stdout/stdoutmetric/internal/{selfobservability/selfobservability.go => observ/exporter.go} (66%) rename exporters/stdout/stdoutmetric/internal/{selfobservability/selfobservability_test.go => observ/exporter_test.go} (84%) diff --git a/exporters/stdout/stdoutmetric/exporter.go b/exporters/stdout/stdoutmetric/exporter.go index 39ac6af78c2..6b50bd8d54b 100644 --- a/exporters/stdout/stdoutmetric/exporter.go +++ b/exporters/stdout/stdoutmetric/exporter.go @@ -11,19 +11,12 @@ import ( "sync/atomic" "go.opentelemetry.io/otel/exporters/stdout/stdoutmetric/internal/counter" - "go.opentelemetry.io/otel/exporters/stdout/stdoutmetric/internal/selfobservability" - "go.opentelemetry.io/otel/exporters/stdout/stdoutmetric/internal/x" + "go.opentelemetry.io/otel/exporters/stdout/stdoutmetric/internal/observ" "go.opentelemetry.io/otel/internal/global" "go.opentelemetry.io/otel/sdk/metric" "go.opentelemetry.io/otel/sdk/metric/metricdata" - semconv "go.opentelemetry.io/otel/semconv/v1.36.0" ) -// otelComponentType is a name identifying the type of the OpenTelemetry -// component. It is not a standardized OTel component type, so it uses the -// Go package prefixed type name to ensure uniqueness and identity. -const otelComponentType = "go.opentelemetry.io/otel/exporters/stdout/stdoutmetric.exporter" - // exporter is an OpenTelemetry metric exporter. type exporter struct { encVal atomic.Value // encoderHolder @@ -35,8 +28,7 @@ type exporter struct { redactTimestamps bool - selfObservabilityEnabled bool - exporterMetric *selfobservability.ExporterMetrics + exporterMetric *observ.StdoutMetricExporter } // New returns a configured metric exporter. @@ -46,21 +38,13 @@ type exporter struct { func New(options ...Option) (metric.Exporter, error) { cfg := newConfig(options...) exp := &exporter{ - temporalitySelector: cfg.temporalitySelector, - aggregationSelector: cfg.aggregationSelector, - redactTimestamps: cfg.redactTimestamps, - selfObservabilityEnabled: x.Observability.Enabled(), + temporalitySelector: cfg.temporalitySelector, + aggregationSelector: cfg.aggregationSelector, + redactTimestamps: cfg.redactTimestamps, } exp.encVal.Store(*cfg.encoder) var err error - if exp.selfObservabilityEnabled { - componentName := fmt.Sprintf("%s/%d", otelComponentType, counter.NextExporterID()) - exp.exporterMetric, err = selfobservability.NewExporterMetrics( - "go.opentelemetry.io/otel/exporters/stdout/stdoutmetric", - semconv.OTelComponentName(componentName), - semconv.OTelComponentTypeKey.String(otelComponentType), - ) - } + exp.exporterMetric, err = observ.NewStdoutMetricExporter(counter.NextExporterID()) return exp, err } @@ -89,7 +73,7 @@ func (e *exporter) Export(ctx context.Context, data *metricdata.ResourceMetrics) } func (e *exporter) trackExport(ctx context.Context, count int64) func(err error) { - if !e.selfObservabilityEnabled { + if e.exporterMetric == nil { return func(error) {} } return e.exporterMetric.TrackExport(ctx, count) diff --git a/exporters/stdout/stdoutmetric/internal/selfobservability/selfobservability.go b/exporters/stdout/stdoutmetric/internal/observ/exporter.go similarity index 66% rename from exporters/stdout/stdoutmetric/internal/selfobservability/selfobservability.go rename to exporters/stdout/stdoutmetric/internal/observ/exporter.go index 325fc3ecc68..36d3d2d319e 100644 --- a/exporters/stdout/stdoutmetric/internal/selfobservability/selfobservability.go +++ b/exporters/stdout/stdoutmetric/internal/observ/exporter.go @@ -1,9 +1,9 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -// Package selfobservability provides self-observability metrics for stdout metric exporter. +// Package observ provides self-observability metrics for stdout metric exporter. // This is an experimental feature controlled by the x.SelfObservability feature flag. -package selfobservability // import "go.opentelemetry.io/otel/exporters/stdout/stdoutmetric/internal/selfobservability" +package observ // import "go.opentelemetry.io/otel/exporters/stdout/stdoutmetric/internal/observ" import ( "context" @@ -14,12 +14,23 @@ import ( "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/exporters/stdout/stdoutmetric/internal/x" "go.opentelemetry.io/otel/metric" "go.opentelemetry.io/otel/sdk" semconv "go.opentelemetry.io/otel/semconv/v1.36.0" "go.opentelemetry.io/otel/semconv/v1.36.0/otelconv" ) +const ( + // ScopeName is the unique name of the meter used for instrumentation. + ScopeName = "go.opentelemetry.io/otel/exporters/stdout/stdoutmetric" + + // ComponentType is a name identifying the type of the OpenTelemetry + // component. It is not a standardized OTel component type, so it uses the + // Go package prefixed type name to ensure uniqueness and identity. + ComponentType = "go.opentelemetry.io/otel/exporters/stdout/stdoutmetric.exporter" +) + var measureAttrsPool = sync.Pool{ New: func() any { // "component.name" + "component.type" + "error.type" @@ -31,7 +42,7 @@ var measureAttrsPool = sync.Pool{ }, } -type ExporterMetrics struct { +type StdoutMetricExporter struct { inflight metric.Int64UpDownCounter addOpts []metric.AddOption exported otelconv.SDKExporterMetricDataPointExported @@ -40,22 +51,28 @@ type ExporterMetrics struct { attrs []attribute.KeyValue } -func NewExporterMetrics( - name string, - componentName, componentType attribute.KeyValue, -) (*ExporterMetrics, error) { - attrs := []attribute.KeyValue{componentName, componentType} +// NewStdoutMetricExporter returns a new StdoutMetricExporter for the stdout metric exporter. +// The id parameter is used to create a unique component name for the exporter instance. +func NewStdoutMetricExporter(id int64) (*StdoutMetricExporter, error) { + if !x.Observability.Enabled() { + return nil, nil + } + componentName := fmt.Sprintf("%s/%d", ComponentType, id) + attrs := []attribute.KeyValue{ + semconv.OTelComponentName(componentName), + semconv.OTelComponentTypeKey.String(ComponentType), + } attrOpts := metric.WithAttributeSet(attribute.NewSet(attrs...)) addOpts := []metric.AddOption{attrOpts} recordOpts := []metric.RecordOption{attrOpts} - em := &ExporterMetrics{ + em := &StdoutMetricExporter{ attrs: attrs, addOpts: addOpts, recordOpts: recordOpts, } mp := otel.GetMeterProvider() m := mp.Meter( - name, + ScopeName, metric.WithInstrumentationVersion(sdk.Version()), metric.WithSchemaURL(semconv.SchemaURL)) @@ -77,7 +94,7 @@ func NewExporterMetrics( return em, err } -func (em *ExporterMetrics) TrackExport(ctx context.Context, count int64) func(err error) { +func (em *StdoutMetricExporter) TrackExport(ctx context.Context, count int64) func(err error) { begin := time.Now() em.inflight.Add(ctx, count, em.addOpts...) return func(err error) { diff --git a/exporters/stdout/stdoutmetric/internal/selfobservability/selfobservability_test.go b/exporters/stdout/stdoutmetric/internal/observ/exporter_test.go similarity index 84% rename from exporters/stdout/stdoutmetric/internal/selfobservability/selfobservability_test.go rename to exporters/stdout/stdoutmetric/internal/observ/exporter_test.go index ae64eebe3db..4f9fcfe193e 100644 --- a/exporters/stdout/stdoutmetric/internal/selfobservability/selfobservability_test.go +++ b/exporters/stdout/stdoutmetric/internal/observ/exporter_test.go @@ -1,7 +1,7 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -package selfobservability +package observ import ( "context" @@ -24,7 +24,7 @@ type testSetup struct { reader *sdkmetric.ManualReader mp *sdkmetric.MeterProvider ctx context.Context - em *ExporterMetrics + em *StdoutMetricExporter } func setupTestMeterProvider(t *testing.T) *testSetup { @@ -35,13 +35,7 @@ func setupTestMeterProvider(t *testing.T) *testSetup { otel.SetMeterProvider(mp) t.Cleanup(func() { otel.SetMeterProvider(originalMP) }) - componentName := semconv.OTelComponentName("test") - componentType := semconv.OTelComponentTypeKey.String("exporter") - em, err := NewExporterMetrics( - "go.opentelemetry.io/otel/exporters/stdout/stdoutmetric", - componentName, - componentType, - ) + em, err := NewStdoutMetricExporter(0) assert.NoError(t, err) return &testSetup{ @@ -166,23 +160,25 @@ func TestExporterMetrics_TrackExport_InflightTracking(t *testing.T) { } func TestExporterMetrics_AttributesNotPermanentlyModified(t *testing.T) { - componentName := semconv.OTelComponentName("test-component") - componentType := semconv.OTelComponentTypeKey.String("test-exporter") - em, err := NewExporterMetrics("test", componentName, componentType) + em, err := NewStdoutMetricExporter(42) assert.NoError(t, err) + // Should have component.name and component.type attributes assert.Len(t, em.attrs, 2) - assert.Contains(t, em.attrs, componentName) - assert.Contains(t, em.attrs, componentType) + expectedComponentName := semconv.OTelComponentName("go.opentelemetry.io/otel/exporters/stdout/stdoutmetric.exporter/42") + expectedComponentType := semconv.OTelComponentTypeKey.String(ComponentType) + assert.Contains(t, em.attrs, expectedComponentName) + assert.Contains(t, em.attrs, expectedComponentType) done := em.TrackExport(t.Context(), 1) done(errors.New("test error")) done = em.TrackExport(t.Context(), 1) done(nil) + // Attributes should not be modified after tracking exports assert.Len(t, em.attrs, 2) - assert.Contains(t, em.attrs, componentName) - assert.Contains(t, em.attrs, componentType) + assert.Contains(t, em.attrs, expectedComponentName) + assert.Contains(t, em.attrs, expectedComponentType) } func BenchmarkTrackExport(b *testing.B) { @@ -195,15 +191,9 @@ func BenchmarkTrackExport(b *testing.B) { // Ensure deterministic benchmark by using noop meter. otel.SetMeterProvider(noop.NewMeterProvider()) - newExp := func(b *testing.B) *ExporterMetrics { + newExp := func(b *testing.B) *StdoutMetricExporter { b.Helper() - componentName := semconv.OTelComponentName("benchmark") - componentType := semconv.OTelComponentTypeKey.String("exporter") - em, err := NewExporterMetrics( - "go.opentelemetry.io/otel/exporters/stdout/stdoutmetric", - componentName, - componentType, - ) + em, err := NewStdoutMetricExporter(0) require.NoError(b, err) require.NotNil(b, em) return em From d03013d62404a538ebd949ab4ab38ec29f65be53 Mon Sep 17 00:00:00 2001 From: Mahendra Bishnoi Date: Sun, 12 Oct 2025 11:47:41 +0530 Subject: [PATCH 33/42] fix failing test cases --- exporters/stdout/stdoutmetric/exporter_test.go | 4 ++++ .../stdout/stdoutmetric/internal/observ/exporter_test.go | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/exporters/stdout/stdoutmetric/exporter_test.go b/exporters/stdout/stdoutmetric/exporter_test.go index 3da899106ff..837914b5e4a 100644 --- a/exporters/stdout/stdoutmetric/exporter_test.go +++ b/exporters/stdout/stdoutmetric/exporter_test.go @@ -247,6 +247,10 @@ func TestExporter_Export_SelfObservability(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Setenv("OTEL_GO_X_OBSERVABILITY", strconv.FormatBool(tt.selfObservabilityEnabled)) + + // Reset the exporter ID counter to ensure consistent component names + _ = counter.SetExporterID(0) + reader := metric.NewManualReader() mp := metric.NewMeterProvider(metric.WithReader(reader)) origMp := otel.GetMeterProvider() diff --git a/exporters/stdout/stdoutmetric/internal/observ/exporter_test.go b/exporters/stdout/stdoutmetric/internal/observ/exporter_test.go index 4f9fcfe193e..a6595c5c402 100644 --- a/exporters/stdout/stdoutmetric/internal/observ/exporter_test.go +++ b/exporters/stdout/stdoutmetric/internal/observ/exporter_test.go @@ -28,6 +28,8 @@ type testSetup struct { } func setupTestMeterProvider(t *testing.T) *testSetup { + t.Setenv("OTEL_GO_X_OBSERVABILITY", "true") + reader := sdkmetric.NewManualReader() mp := sdkmetric.NewMeterProvider(sdkmetric.WithReader(reader)) @@ -160,6 +162,8 @@ func TestExporterMetrics_TrackExport_InflightTracking(t *testing.T) { } func TestExporterMetrics_AttributesNotPermanentlyModified(t *testing.T) { + t.Setenv("OTEL_GO_X_OBSERVABILITY", "true") + em, err := NewStdoutMetricExporter(42) assert.NoError(t, err) From 5742661ed1940fc299b0b8e34370f3b32dce192b Mon Sep 17 00:00:00 2001 From: Mahendra Bishnoi Date: Sun, 12 Oct 2025 11:56:58 +0530 Subject: [PATCH 34/42] `make precommit` --- .../stdout/stdoutmetric/internal/observ/exporter_test.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/exporters/stdout/stdoutmetric/internal/observ/exporter_test.go b/exporters/stdout/stdoutmetric/internal/observ/exporter_test.go index a6595c5c402..83f300f20f7 100644 --- a/exporters/stdout/stdoutmetric/internal/observ/exporter_test.go +++ b/exporters/stdout/stdoutmetric/internal/observ/exporter_test.go @@ -169,7 +169,9 @@ func TestExporterMetrics_AttributesNotPermanentlyModified(t *testing.T) { // Should have component.name and component.type attributes assert.Len(t, em.attrs, 2) - expectedComponentName := semconv.OTelComponentName("go.opentelemetry.io/otel/exporters/stdout/stdoutmetric.exporter/42") + expectedComponentName := semconv.OTelComponentName( + "go.opentelemetry.io/otel/exporters/stdout/stdoutmetric.exporter/42", + ) expectedComponentType := semconv.OTelComponentTypeKey.String(ComponentType) assert.Contains(t, em.attrs, expectedComponentName) assert.Contains(t, em.attrs, expectedComponentType) From a723bfea2ae50f0071e2fbb0d5a626ee47ef151a Mon Sep 17 00:00:00 2001 From: Mahendra Bishnoi Date: Sun, 12 Oct 2025 12:08:08 +0530 Subject: [PATCH 35/42] rename to Instrumentation --- exporters/stdout/stdoutmetric/exporter.go | 8 ++++---- .../stdout/stdoutmetric/internal/observ/exporter.go | 10 +++++----- .../stdoutmetric/internal/observ/exporter_test.go | 10 +++++----- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/exporters/stdout/stdoutmetric/exporter.go b/exporters/stdout/stdoutmetric/exporter.go index 6b50bd8d54b..21916617a8b 100644 --- a/exporters/stdout/stdoutmetric/exporter.go +++ b/exporters/stdout/stdoutmetric/exporter.go @@ -28,7 +28,7 @@ type exporter struct { redactTimestamps bool - exporterMetric *observ.StdoutMetricExporter + inst *observ.Instrumentation } // New returns a configured metric exporter. @@ -44,7 +44,7 @@ func New(options ...Option) (metric.Exporter, error) { } exp.encVal.Store(*cfg.encoder) var err error - exp.exporterMetric, err = observ.NewStdoutMetricExporter(counter.NextExporterID()) + exp.inst, err = observ.NewInstrumentation(counter.NextExporterID()) return exp, err } @@ -73,10 +73,10 @@ func (e *exporter) Export(ctx context.Context, data *metricdata.ResourceMetrics) } func (e *exporter) trackExport(ctx context.Context, count int64) func(err error) { - if e.exporterMetric == nil { + if e.inst == nil { return func(error) {} } - return e.exporterMetric.TrackExport(ctx, count) + return e.inst.TrackExport(ctx, count) } func (*exporter) ForceFlush(context.Context) error { diff --git a/exporters/stdout/stdoutmetric/internal/observ/exporter.go b/exporters/stdout/stdoutmetric/internal/observ/exporter.go index 36d3d2d319e..85268399f31 100644 --- a/exporters/stdout/stdoutmetric/internal/observ/exporter.go +++ b/exporters/stdout/stdoutmetric/internal/observ/exporter.go @@ -42,7 +42,7 @@ var measureAttrsPool = sync.Pool{ }, } -type StdoutMetricExporter struct { +type Instrumentation struct { inflight metric.Int64UpDownCounter addOpts []metric.AddOption exported otelconv.SDKExporterMetricDataPointExported @@ -51,9 +51,9 @@ type StdoutMetricExporter struct { attrs []attribute.KeyValue } -// NewStdoutMetricExporter returns a new StdoutMetricExporter for the stdout metric exporter. +// NewInstrumentation returns a new Instrumentation for the stdout metric exporter. // The id parameter is used to create a unique component name for the exporter instance. -func NewStdoutMetricExporter(id int64) (*StdoutMetricExporter, error) { +func NewInstrumentation(id int64) (*Instrumentation, error) { if !x.Observability.Enabled() { return nil, nil } @@ -65,7 +65,7 @@ func NewStdoutMetricExporter(id int64) (*StdoutMetricExporter, error) { attrOpts := metric.WithAttributeSet(attribute.NewSet(attrs...)) addOpts := []metric.AddOption{attrOpts} recordOpts := []metric.RecordOption{attrOpts} - em := &StdoutMetricExporter{ + em := &Instrumentation{ attrs: attrs, addOpts: addOpts, recordOpts: recordOpts, @@ -94,7 +94,7 @@ func NewStdoutMetricExporter(id int64) (*StdoutMetricExporter, error) { return em, err } -func (em *StdoutMetricExporter) TrackExport(ctx context.Context, count int64) func(err error) { +func (em *Instrumentation) TrackExport(ctx context.Context, count int64) func(err error) { begin := time.Now() em.inflight.Add(ctx, count, em.addOpts...) return func(err error) { diff --git a/exporters/stdout/stdoutmetric/internal/observ/exporter_test.go b/exporters/stdout/stdoutmetric/internal/observ/exporter_test.go index 83f300f20f7..07b21edaa26 100644 --- a/exporters/stdout/stdoutmetric/internal/observ/exporter_test.go +++ b/exporters/stdout/stdoutmetric/internal/observ/exporter_test.go @@ -24,7 +24,7 @@ type testSetup struct { reader *sdkmetric.ManualReader mp *sdkmetric.MeterProvider ctx context.Context - em *StdoutMetricExporter + em *Instrumentation } func setupTestMeterProvider(t *testing.T) *testSetup { @@ -37,7 +37,7 @@ func setupTestMeterProvider(t *testing.T) *testSetup { otel.SetMeterProvider(mp) t.Cleanup(func() { otel.SetMeterProvider(originalMP) }) - em, err := NewStdoutMetricExporter(0) + em, err := NewInstrumentation(0) assert.NoError(t, err) return &testSetup{ @@ -164,7 +164,7 @@ func TestExporterMetrics_TrackExport_InflightTracking(t *testing.T) { func TestExporterMetrics_AttributesNotPermanentlyModified(t *testing.T) { t.Setenv("OTEL_GO_X_OBSERVABILITY", "true") - em, err := NewStdoutMetricExporter(42) + em, err := NewInstrumentation(42) assert.NoError(t, err) // Should have component.name and component.type attributes @@ -197,9 +197,9 @@ func BenchmarkTrackExport(b *testing.B) { // Ensure deterministic benchmark by using noop meter. otel.SetMeterProvider(noop.NewMeterProvider()) - newExp := func(b *testing.B) *StdoutMetricExporter { + newExp := func(b *testing.B) *Instrumentation { b.Helper() - em, err := NewStdoutMetricExporter(0) + em, err := NewInstrumentation(0) require.NoError(b, err) require.NotNil(b, em) return em From 3ab2586537e6a96098594216f482de3c45deb20e Mon Sep 17 00:00:00 2001 From: Mahendra Bishnoi Date: Sun, 12 Oct 2025 12:09:04 +0530 Subject: [PATCH 36/42] fix CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 79818af984a..16680293c96 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,7 +19,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm - Add experimental observability metrics in `go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc`. (#7353) - Add experimental observability metrics in `go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc`. (#7459) - Add experimental observability metrics in `go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp`. (#7486) -- Add experimental observability metrics in `go.opentelemetry.io/otel/exporters/stdout/stdoutmetric`. (#7492)s +- Add experimental observability metrics in `go.opentelemetry.io/otel/exporters/stdout/stdoutmetric`. (#7492) ### Fixed From e34443fe49b1c0cae6a1988df2e546ab9c42172b Mon Sep 17 00:00:00 2001 From: Mahendra Bishnoi Date: Sun, 12 Oct 2025 12:11:29 +0530 Subject: [PATCH 37/42] rename self observability to observability --- .../stdout/stdoutmetric/exporter_test.go | 58 +++++++++---------- .../stdoutmetric/internal/observ/exporter.go | 2 +- 2 files changed, 30 insertions(+), 30 deletions(-) diff --git a/exporters/stdout/stdoutmetric/exporter_test.go b/exporters/stdout/stdoutmetric/exporter_test.go index 837914b5e4a..a90319d0a3c 100644 --- a/exporters/stdout/stdoutmetric/exporter_test.go +++ b/exporters/stdout/stdoutmetric/exporter_test.go @@ -196,7 +196,7 @@ func TestAggregationSelector(t *testing.T) { assert.Equal(t, metric.AggregationDrop{}, exp.Aggregation(unknownKind)) } -func TestExporter_Export_SelfObservability(t *testing.T) { +func TestExporter_Export_Observability(t *testing.T) { componentNameAttr := semconv.OTelComponentName("go.opentelemetry.io/otel/exporters/stdout/stdoutmetric.exporter/0") componentTypeAttr := semconv.OTelComponentTypeKey.String( "go.opentelemetry.io/otel/exporters/stdout/stdoutmetric.exporter", @@ -204,38 +204,38 @@ func TestExporter_Export_SelfObservability(t *testing.T) { wantErr := errors.New("encoding failed") tests := []struct { - name string - ctx context.Context - exporterOpts []stdoutmetric.Option - selfObservabilityEnabled bool - expectedExportedCount int64 - inflightAttrs attribute.Set - attributes attribute.Set - wantErr error + name string + ctx context.Context + exporterOpts []stdoutmetric.Option + observabilityEnabled bool + expectedExportedCount int64 + inflightAttrs attribute.Set + attributes attribute.Set + wantErr error }{ { - name: "Enabled", - ctx: t.Context(), - exporterOpts: []stdoutmetric.Option{testEncoderOption()}, - selfObservabilityEnabled: true, - expectedExportedCount: 19, - inflightAttrs: attribute.NewSet(componentNameAttr, componentTypeAttr), - attributes: attribute.NewSet(componentNameAttr, componentTypeAttr), + name: "Enabled", + ctx: t.Context(), + exporterOpts: []stdoutmetric.Option{testEncoderOption()}, + observabilityEnabled: true, + expectedExportedCount: 19, + inflightAttrs: attribute.NewSet(componentNameAttr, componentTypeAttr), + attributes: attribute.NewSet(componentNameAttr, componentTypeAttr), }, { - name: "Disabled", - ctx: t.Context(), - exporterOpts: []stdoutmetric.Option{testEncoderOption()}, - selfObservabilityEnabled: false, - expectedExportedCount: 0, + name: "Disabled", + ctx: t.Context(), + exporterOpts: []stdoutmetric.Option{testEncoderOption()}, + observabilityEnabled: false, + expectedExportedCount: 0, }, { - name: "EncodingError", - ctx: t.Context(), - exporterOpts: []stdoutmetric.Option{stdoutmetric.WithEncoder(failingEncoder{})}, - selfObservabilityEnabled: true, - expectedExportedCount: 19, - inflightAttrs: attribute.NewSet(componentNameAttr, componentTypeAttr), + name: "EncodingError", + ctx: t.Context(), + exporterOpts: []stdoutmetric.Option{stdoutmetric.WithEncoder(failingEncoder{})}, + observabilityEnabled: true, + expectedExportedCount: 19, + inflightAttrs: attribute.NewSet(componentNameAttr, componentTypeAttr), attributes: attribute.NewSet( componentNameAttr, componentTypeAttr, @@ -246,7 +246,7 @@ func TestExporter_Export_SelfObservability(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - t.Setenv("OTEL_GO_X_OBSERVABILITY", strconv.FormatBool(tt.selfObservabilityEnabled)) + t.Setenv("OTEL_GO_X_OBSERVABILITY", strconv.FormatBool(tt.observabilityEnabled)) // Reset the exporter ID counter to ensure consistent component names _ = counter.SetExporterID(0) @@ -272,7 +272,7 @@ func TestExporter_Export_SelfObservability(t *testing.T) { err = reader.Collect(tt.ctx, &metrics) require.NoError(t, err) - if !tt.selfObservabilityEnabled { + if !tt.observabilityEnabled { assert.Empty(t, metrics.ScopeMetrics) } else { assert.Len(t, metrics.ScopeMetrics, 1) diff --git a/exporters/stdout/stdoutmetric/internal/observ/exporter.go b/exporters/stdout/stdoutmetric/internal/observ/exporter.go index 85268399f31..600c6e1cef7 100644 --- a/exporters/stdout/stdoutmetric/internal/observ/exporter.go +++ b/exporters/stdout/stdoutmetric/internal/observ/exporter.go @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 // Package observ provides self-observability metrics for stdout metric exporter. -// This is an experimental feature controlled by the x.SelfObservability feature flag. +// This is an experimental feature controlled by the x.Observability feature flag. package observ // import "go.opentelemetry.io/otel/exporters/stdout/stdoutmetric/internal/observ" import ( From 69c1612ec31e07496007c4375b34957c11ea15be Mon Sep 17 00:00:00 2001 From: Mahendra Bishnoi Date: Sun, 12 Oct 2025 12:14:42 +0530 Subject: [PATCH 38/42] make constants unexported --- .../stdoutmetric/internal/observ/exporter.go | 14 +++++++------- .../stdoutmetric/internal/observ/exporter_test.go | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/exporters/stdout/stdoutmetric/internal/observ/exporter.go b/exporters/stdout/stdoutmetric/internal/observ/exporter.go index 600c6e1cef7..da7d91d48d9 100644 --- a/exporters/stdout/stdoutmetric/internal/observ/exporter.go +++ b/exporters/stdout/stdoutmetric/internal/observ/exporter.go @@ -22,13 +22,13 @@ import ( ) const ( - // ScopeName is the unique name of the meter used for instrumentation. - ScopeName = "go.opentelemetry.io/otel/exporters/stdout/stdoutmetric" + // scope is the unique name of the meter used for instrumentation. + scope = "go.opentelemetry.io/otel/exporters/stdout/stdoutmetric" - // ComponentType is a name identifying the type of the OpenTelemetry + // componentType is a name identifying the type of the OpenTelemetry // component. It is not a standardized OTel component type, so it uses the // Go package prefixed type name to ensure uniqueness and identity. - ComponentType = "go.opentelemetry.io/otel/exporters/stdout/stdoutmetric.exporter" + componentType = "go.opentelemetry.io/otel/exporters/stdout/stdoutmetric.exporter" ) var measureAttrsPool = sync.Pool{ @@ -57,10 +57,10 @@ func NewInstrumentation(id int64) (*Instrumentation, error) { if !x.Observability.Enabled() { return nil, nil } - componentName := fmt.Sprintf("%s/%d", ComponentType, id) + componentName := fmt.Sprintf("%s/%d", componentType, id) attrs := []attribute.KeyValue{ semconv.OTelComponentName(componentName), - semconv.OTelComponentTypeKey.String(ComponentType), + semconv.OTelComponentTypeKey.String(componentType), } attrOpts := metric.WithAttributeSet(attribute.NewSet(attrs...)) addOpts := []metric.AddOption{attrOpts} @@ -72,7 +72,7 @@ func NewInstrumentation(id int64) (*Instrumentation, error) { } mp := otel.GetMeterProvider() m := mp.Meter( - ScopeName, + scope, metric.WithInstrumentationVersion(sdk.Version()), metric.WithSchemaURL(semconv.SchemaURL)) diff --git a/exporters/stdout/stdoutmetric/internal/observ/exporter_test.go b/exporters/stdout/stdoutmetric/internal/observ/exporter_test.go index 07b21edaa26..c0b4a665343 100644 --- a/exporters/stdout/stdoutmetric/internal/observ/exporter_test.go +++ b/exporters/stdout/stdoutmetric/internal/observ/exporter_test.go @@ -172,7 +172,7 @@ func TestExporterMetrics_AttributesNotPermanentlyModified(t *testing.T) { expectedComponentName := semconv.OTelComponentName( "go.opentelemetry.io/otel/exporters/stdout/stdoutmetric.exporter/42", ) - expectedComponentType := semconv.OTelComponentTypeKey.String(ComponentType) + expectedComponentType := semconv.OTelComponentTypeKey.String(componentType) assert.Contains(t, em.attrs, expectedComponentName) assert.Contains(t, em.attrs, expectedComponentType) From 6233067e3ba3d46fb36122e0f79e759f917821a6 Mon Sep 17 00:00:00 2001 From: Mahendra Bishnoi Date: Sun, 12 Oct 2025 12:24:37 +0530 Subject: [PATCH 39/42] helper for component name --- .../stdoutmetric/internal/observ/exporter.go | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/exporters/stdout/stdoutmetric/internal/observ/exporter.go b/exporters/stdout/stdoutmetric/internal/observ/exporter.go index da7d91d48d9..85eb971b16d 100644 --- a/exporters/stdout/stdoutmetric/internal/observ/exporter.go +++ b/exporters/stdout/stdoutmetric/internal/observ/exporter.go @@ -42,6 +42,7 @@ var measureAttrsPool = sync.Pool{ }, } +// Instrumentation is the instrumentation for stdout metric exporter type Instrumentation struct { inflight metric.Int64UpDownCounter addOpts []metric.AddOption @@ -51,15 +52,21 @@ type Instrumentation struct { attrs []attribute.KeyValue } -// NewInstrumentation returns a new Instrumentation for the stdout metric exporter. -// The id parameter is used to create a unique component name for the exporter instance. +func exporterComponentName(id int64) attribute.KeyValue { + componentName := fmt.Sprintf("%s/%d", componentType, id) + return semconv.OTelComponentName(componentName) +} + +// NewInstrumentation returns a new Instrumentation for the stdout metric exporter +// with the provided ID. +// +// If the experimental observability is disabled, nil is returned. func NewInstrumentation(id int64) (*Instrumentation, error) { if !x.Observability.Enabled() { return nil, nil } - componentName := fmt.Sprintf("%s/%d", componentType, id) attrs := []attribute.KeyValue{ - semconv.OTelComponentName(componentName), + exporterComponentName(id), semconv.OTelComponentTypeKey.String(componentType), } attrOpts := metric.WithAttributeSet(attribute.NewSet(attrs...)) From 909bc47451d6c90ab54f1f1b59b47e9849150417 Mon Sep 17 00:00:00 2001 From: Mahendra Bishnoi Date: Sun, 12 Oct 2025 12:33:30 +0530 Subject: [PATCH 40/42] update docs --- exporters/stdout/stdoutmetric/internal/observ/exporter.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/exporters/stdout/stdoutmetric/internal/observ/exporter.go b/exporters/stdout/stdoutmetric/internal/observ/exporter.go index 85eb971b16d..3233439d93a 100644 --- a/exporters/stdout/stdoutmetric/internal/observ/exporter.go +++ b/exporters/stdout/stdoutmetric/internal/observ/exporter.go @@ -1,7 +1,7 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -// Package observ provides self-observability metrics for stdout metric exporter. +// Package observ provides observability for stdout metric exporter. // This is an experimental feature controlled by the x.Observability feature flag. package observ // import "go.opentelemetry.io/otel/exporters/stdout/stdoutmetric/internal/observ" @@ -22,7 +22,6 @@ import ( ) const ( - // scope is the unique name of the meter used for instrumentation. scope = "go.opentelemetry.io/otel/exporters/stdout/stdoutmetric" // componentType is a name identifying the type of the OpenTelemetry @@ -107,7 +106,7 @@ func (em *Instrumentation) TrackExport(ctx context.Context, count int64) func(er return func(err error) { durationSeconds := time.Since(begin).Seconds() em.inflight.Add(ctx, -count, em.addOpts...) - if err == nil { + if err == nil { // short circuit in case of success to avoid allocations em.exported.Int64Counter.Add(ctx, count, em.addOpts...) em.duration.Float64Histogram.Record(ctx, durationSeconds, em.recordOpts...) return From ff814ca05436b6902a3c4f3e78e62e647b306201 Mon Sep 17 00:00:00 2001 From: Mahendra Bishnoi Date: Sun, 12 Oct 2025 12:36:20 +0530 Subject: [PATCH 41/42] update docs --- exporters/stdout/stdoutmetric/internal/x/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/exporters/stdout/stdoutmetric/internal/x/README.md b/exporters/stdout/stdoutmetric/internal/x/README.md index b465e260741..fe875d42c8f 100644 --- a/exporters/stdout/stdoutmetric/internal/x/README.md +++ b/exporters/stdout/stdoutmetric/internal/x/README.md @@ -8,11 +8,11 @@ See the [Compatibility and Stability](#compatibility-and-stability) section for ## Features -- [Self-Observability](#self-observability) +- [Observability](#observability) -### Self-Observability +### Observability -The `stdoutmetric` exporter provides a self-observability feature that allows you to monitor the exporter itself. +The `stdoutmetric` exporter provides an observability feature that allows you to monitor the exporter itself. To opt-in, set the environment variable `OTEL_GO_X_OBSERVABILITY` to `true`. From 325bda9616a283a6e200ba20ae2bfd260ab00309 Mon Sep 17 00:00:00 2001 From: Mahendra Bishnoi Date: Sun, 12 Oct 2025 12:51:01 +0530 Subject: [PATCH 42/42] linter happy --- exporters/stdout/stdoutmetric/internal/observ/exporter.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/exporters/stdout/stdoutmetric/internal/observ/exporter.go b/exporters/stdout/stdoutmetric/internal/observ/exporter.go index 3233439d93a..070247013b9 100644 --- a/exporters/stdout/stdoutmetric/internal/observ/exporter.go +++ b/exporters/stdout/stdoutmetric/internal/observ/exporter.go @@ -41,7 +41,7 @@ var measureAttrsPool = sync.Pool{ }, } -// Instrumentation is the instrumentation for stdout metric exporter +// Instrumentation is the instrumentation for stdout metric exporter. type Instrumentation struct { inflight metric.Int64UpDownCounter addOpts []metric.AddOption