From 33b3ee510484d748b07f7a566793a8e5cc954c01 Mon Sep 17 00:00:00 2001 From: yumosx Date: Sun, 26 Apr 2026 17:48:37 +0800 Subject: [PATCH 01/14] add self observability for stdout exporter --- exporters/stdout/stdoutlog/exporter.go | 21 +- exporters/stdout/stdoutlog/exporter_test.go | 233 ++++++++++++++++++ .../stdoutlog/internal/counter/counter.go | 31 +++ .../internal/counter/counter_test.go | 65 +++++ exporters/stdout/stdoutlog/internal/gen.go | 2 + 5 files changed, 351 insertions(+), 1 deletion(-) create mode 100644 exporters/stdout/stdoutlog/internal/counter/counter.go create mode 100644 exporters/stdout/stdoutlog/internal/counter/counter_test.go diff --git a/exporters/stdout/stdoutlog/exporter.go b/exporters/stdout/stdoutlog/exporter.go index 3d48d67081e..33a043801c9 100644 --- a/exporters/stdout/stdoutlog/exporter.go +++ b/exporters/stdout/stdoutlog/exporter.go @@ -8,6 +8,9 @@ import ( "encoding/json" "sync/atomic" + "go.opentelemetry.io/otel/exporters/stdout/stdoutlog/internal/counter" + "go.opentelemetry.io/otel/exporters/stdout/stdoutlog/internal/observ" + "go.opentelemetry.io/otel/sdk/log" ) @@ -18,6 +21,7 @@ var _ log.Exporter = &Exporter{} type Exporter struct { encoder atomic.Pointer[json.Encoder] timestamps bool + inst *observ.Instrumentation } // New creates an [Exporter]. @@ -29,8 +33,14 @@ func New(options ...Option) (*Exporter, error) { enc.SetIndent("", "\t") } + inst, err := observ.NewInstrumentation(counter.NextExporterID()) + if err != nil { + return nil, err + } + e := Exporter{ timestamps: cfg.Timestamps, + inst: inst, } e.encoder.Store(enc) @@ -38,12 +48,20 @@ func New(options ...Option) (*Exporter, error) { } // Export exports log records to writer. -func (e *Exporter) Export(ctx context.Context, records []log.Record) error { +func (e *Exporter) Export(ctx context.Context, records []log.Record) (err error) { enc := e.encoder.Load() if enc == nil { return nil } + var success int64 + if e.inst != nil { + op := e.inst.ExportLogs(ctx, int64(len(records))) + defer func() { + op.End(success, err) + }() + } + for _, record := range records { // Honor context cancellation. if err := ctx.Err(); err != nil { @@ -55,6 +73,7 @@ func (e *Exporter) Export(ctx context.Context, records []log.Record) error { if err := enc.Encode(recordJSON); err != nil { return err } + success++ } return nil } diff --git a/exporters/stdout/stdoutlog/exporter_test.go b/exporters/stdout/stdoutlog/exporter_test.go index 8772e9790bf..43e0b7605ff 100644 --- a/exporters/stdout/stdoutlog/exporter_test.go +++ b/exporters/stdout/stdoutlog/exporter_test.go @@ -7,6 +7,7 @@ import ( "bytes" "context" "encoding/json" + "errors" "sync" "testing" "time" @@ -14,12 +15,21 @@ import ( "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/stdoutlog/internal" + "go.opentelemetry.io/otel/exporters/stdout/stdoutlog/internal/counter" + "go.opentelemetry.io/otel/exporters/stdout/stdoutlog/internal/observ" "go.opentelemetry.io/otel/log" "go.opentelemetry.io/otel/sdk/instrumentation" sdklog "go.opentelemetry.io/otel/sdk/log" "go.opentelemetry.io/otel/sdk/log/logtest" + "go.opentelemetry.io/otel/sdk/metric" + "go.opentelemetry.io/otel/sdk/metric/metricdata" + "go.opentelemetry.io/otel/sdk/metric/metricdata/metricdatatest" "go.opentelemetry.io/otel/sdk/resource" + semconv "go.opentelemetry.io/otel/semconv/v1.40.0" + "go.opentelemetry.io/otel/semconv/v1.40.0/otelconv" "go.opentelemetry.io/otel/trace" ) @@ -456,3 +466,226 @@ func TestValueMarshalJSON(t *testing.T) { }) } } + +func TestObservability(t *testing.T) { + tests := []struct { + name string + enabled bool + test func(t *testing.T, scopeMetrics func() metricdata.ScopeMetrics) + }{ + { + name: "Disabled", + enabled: false, + test: func(t *testing.T, scopeMetrics func() metricdata.ScopeMetrics) { + exporter := &Exporter{} + assert.Nil(t, exporter.inst) + }, + }, + { + name: "upload success", + enabled: true, + test: func(t *testing.T, scopeMetrics func() metricdata.ScopeMetrics) { + ctx := t.Context() + var buf bytes.Buffer + exporter, err := New(WithWriter(&buf)) + require.NoError(t, err) + err = exporter.Export(ctx, []sdklog.Record{getRecord(time.Now())}) + require.NoError(t, err) + assertStdoutLogObservabilityMetrics(t, scopeMetrics(), 1, 1, nil) + }, + }, + { + name: "upload failed", + enabled: true, + test: func(t *testing.T, scopeMetrics func() metricdata.ScopeMetrics) { + ctx := t.Context() + writeErr := errors.New("write failed") + exporter, err := New(WithWriter(&failingWriter{err: writeErr})) + require.NoError(t, err) + err = exporter.Export(ctx, []sdklog.Record{getRecord(time.Now())}) + require.ErrorIs(t, err, writeErr) + assertStdoutLogObservabilityMetrics(t, scopeMetrics(), 1, 0, writeErr) + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + if tc.enabled { + t.Setenv("OTEL_GO_X_OBSERVABILITY", "true") + _ = counter.SetExporterID(0) + } + provider := otel.GetMeterProvider() + t.Cleanup(func() { + otel.SetMeterProvider(provider) + }) + r := metric.NewManualReader() + mp := metric.NewMeterProvider(metric.WithReader(r)) + otel.SetMeterProvider(mp) + + scopeMetrics := func() metricdata.ScopeMetrics { + var got metricdata.ResourceMetrics + err := r.Collect(t.Context(), &got) + require.NoError(t, err) + require.Len(t, got.ScopeMetrics, 1) + return got.ScopeMetrics[0] + } + tc.test(t, scopeMetrics) + }) + } +} + +func BenchmarkExporterObservability(b *testing.B) { + ctx := context.Background() + rec := getRecord(time.Date(2020, 1, 1, 12, 0, 0, 0, time.UTC)) + records := []sdklog.Record{rec} + + b.Run("Disabled", func(b *testing.B) { + b.Setenv("OTEL_GO_X_OBSERVABILITY", "false") + var buf bytes.Buffer + exporter, err := New(WithWriter(&buf)) + require.NoError(b, err) + + b.ReportAllocs() + b.ResetTimer() + for b.Loop() { + buf.Reset() + _ = exporter.Export(ctx, records) + } + }) + + setupObservability := func(b *testing.B) { + b.Helper() + b.Setenv("OTEL_GO_X_OBSERVABILITY", "true") + _ = counter.SetExporterID(0) + provider := otel.GetMeterProvider() + b.Cleanup(func() { + otel.SetMeterProvider(provider) + }) + r := metric.NewManualReader() + mp := metric.NewMeterProvider(metric.WithReader(r)) + otel.SetMeterProvider(mp) + } + + b.Run("UploadSuccess", func(b *testing.B) { + setupObservability(b) + var buf bytes.Buffer + exporter, err := New(WithWriter(&buf)) + require.NoError(b, err) + b.ReportAllocs() + b.ResetTimer() + for b.Loop() { + buf.Reset() + _ = exporter.Export(ctx, records) + } + }) + + b.Run("UploadFailed", func(b *testing.B) { + setupObservability(b) + writeErr := errors.New("write failed") + exporter, err := New(WithWriter(&failingWriter{err: writeErr})) + require.NoError(b, err) + b.ReportAllocs() + b.ResetTimer() + for b.Loop() { + _ = exporter.Export(ctx, records) + } + }) +} + +// failingWriter implements [io.Writer] and always returns an error. +type failingWriter struct { + err error +} + +func (w *failingWriter) Write([]byte) (int, error) { + return 0, w.err +} + +func stdoutObservAttrSet(err error) attribute.Set { + attrs := []attribute.KeyValue{ + semconv.OTelComponentName(observ.GetComponentName(0)), + semconv.OTelComponentNameKey.String(observ.ComponentType), + } + if err != nil { + attrs = append(attrs, semconv.ErrorType(err)) + } + return attribute.NewSet(attrs...) +} + +func stdoutLogInflightMetric() metricdata.Metrics { + inflight := otelconv.SDKExporterLogInflight{} + return metricdata.Metrics{ + Name: inflight.Name(), + Description: inflight.Description(), + Unit: inflight.Unit(), + Data: metricdata.Sum[int64]{ + Temporality: metricdata.CumulativeTemporality, + DataPoints: []metricdata.DataPoint[int64]{ + {Attributes: stdoutObservAttrSet(nil), Value: 0}, + }, + }, + } +} + +func stdoutLogExportedMetric(success, total int64, err error) metricdata.Metrics { + dp := []metricdata.DataPoint[int64]{ + {Attributes: stdoutObservAttrSet(nil), Value: success}, + } + if err != nil { + dp = append(dp, metricdata.DataPoint[int64]{ + Attributes: stdoutObservAttrSet(err), + Value: total - success, + }) + } + exported := otelconv.SDKExporterLogExported{} + return metricdata.Metrics{ + Name: exported.Name(), + Description: exported.Description(), + Unit: exported.Unit(), + Data: metricdata.Sum[int64]{ + Temporality: metricdata.CumulativeTemporality, + IsMonotonic: true, + DataPoints: dp, + }, + } +} + +func stdoutLogDurationMetric(err error) metricdata.Metrics { + duration := otelconv.SDKExporterOperationDuration{} + return metricdata.Metrics{ + Name: duration.Name(), + Description: duration.Description(), + Unit: duration.Unit(), + Data: metricdata.Histogram[float64]{ + Temporality: metricdata.CumulativeTemporality, + DataPoints: []metricdata.HistogramDataPoint[float64]{ + {Attributes: stdoutObservAttrSet(err)}, + }, + }, + } +} + +func assertStdoutLogObservabilityMetrics( + t *testing.T, + got metricdata.ScopeMetrics, + logs int64, + success int64, + err error, +) { + t.Helper() + wantScope := instrumentation.Scope{ + Name: observ.ScopeName, + Version: internal.Version, + SchemaURL: semconv.SchemaURL, + } + assert.Equal(t, wantScope, got.Scope) + + m := got.Metrics + require.Len(t, m, 3) + + o := metricdatatest.IgnoreTimestamp() + metricdatatest.AssertEqual(t, stdoutLogInflightMetric(), m[0], o) + metricdatatest.AssertEqual(t, stdoutLogExportedMetric(success, logs, err), m[1], o) + metricdatatest.AssertEqual(t, stdoutLogDurationMetric(err), m[2], metricdatatest.IgnoreValue(), o) +} diff --git a/exporters/stdout/stdoutlog/internal/counter/counter.go b/exporters/stdout/stdoutlog/internal/counter/counter.go new file mode 100644 index 00000000000..bbb4cd7ddc9 --- /dev/null +++ b/exporters/stdout/stdoutlog/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/stdoutlog/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/stdoutlog/internal/counter/counter_test.go b/exporters/stdout/stdoutlog/internal/counter/counter_test.go new file mode 100644 index 00000000000..f3e380d3325 --- /dev/null +++ b/exporters/stdout/stdoutlog/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) + } +} \ No newline at end of file diff --git a/exporters/stdout/stdoutlog/internal/gen.go b/exporters/stdout/stdoutlog/internal/gen.go index 0dda966df6c..79085225bf9 100644 --- a/exporters/stdout/stdoutlog/internal/gen.go +++ b/exporters/stdout/stdoutlog/internal/gen.go @@ -7,3 +7,5 @@ package internal // import "go.opentelemetry.io/otel/exporters/stdout/stdoutlog/ //go:generate gotmpl --body=../../../../internal/shared/x/x.go.tmpl "--data={ \"pkg\": \"go.opentelemetry.io/otel/exporters/stdout/stdoutlog\" }" --out=x/x.go //go:generate gotmpl --body=../../../../internal/shared/x/x_test.go.tmpl "--data={}" --out=x/x_test.go +//go:generate gotmpl --body=../../../../internal/shared/counter/counter.go.tmpl "--data={ \"pkg\": \"go.opentelemetry.io/otel/exporters/stdout/stdoutlog/internal/counter\" }" --out=counter/counter.go +//go:generate gotmpl --body=../../../../internal/shared/counter/counter_test.go.tmpl "--data={}" --out=counter/counter_test.go From fa36181cc0a3cc2374a351d46e33699ece719f81 Mon Sep 17 00:00:00 2001 From: yumosx Date: Sun, 26 Apr 2026 17:54:26 +0800 Subject: [PATCH 02/14] fix lint --- exporters/stdout/stdoutlog/exporter_test.go | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/exporters/stdout/stdoutlog/exporter_test.go b/exporters/stdout/stdoutlog/exporter_test.go index 43e0b7605ff..b611b5c1773 100644 --- a/exporters/stdout/stdoutlog/exporter_test.go +++ b/exporters/stdout/stdoutlog/exporter_test.go @@ -476,7 +476,7 @@ func TestObservability(t *testing.T) { { name: "Disabled", enabled: false, - test: func(t *testing.T, scopeMetrics func() metricdata.ScopeMetrics) { + test: func(t *testing.T, _ func() metricdata.ScopeMetrics) { exporter := &Exporter{} assert.Nil(t, exporter.inst) }, @@ -536,7 +536,7 @@ func TestObservability(t *testing.T) { } func BenchmarkExporterObservability(b *testing.B) { - ctx := context.Background() + ctx := b.Context() rec := getRecord(time.Date(2020, 1, 1, 12, 0, 0, 0, time.UTC)) records := []sdklog.Record{rec} @@ -579,18 +579,6 @@ func BenchmarkExporterObservability(b *testing.B) { _ = exporter.Export(ctx, records) } }) - - b.Run("UploadFailed", func(b *testing.B) { - setupObservability(b) - writeErr := errors.New("write failed") - exporter, err := New(WithWriter(&failingWriter{err: writeErr})) - require.NoError(b, err) - b.ReportAllocs() - b.ResetTimer() - for b.Loop() { - _ = exporter.Export(ctx, records) - } - }) } // failingWriter implements [io.Writer] and always returns an error. From 757f4226ddcc367e84cb042f1414ea603ad883be Mon Sep 17 00:00:00 2001 From: yumosx Date: Tue, 28 Apr 2026 09:09:18 +0800 Subject: [PATCH 03/14] fix: update ScopeName for stdout exporter instrumentation path --- exporters/stdout/stdoutlog/internal/observ/instrumentation.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/exporters/stdout/stdoutlog/internal/observ/instrumentation.go b/exporters/stdout/stdoutlog/internal/observ/instrumentation.go index 939f76f0612..3d496337ff4 100644 --- a/exporters/stdout/stdoutlog/internal/observ/instrumentation.go +++ b/exporters/stdout/stdoutlog/internal/observ/instrumentation.go @@ -24,7 +24,7 @@ import ( const ( // ScopeName is the unique name of the meter used for instrumentation. - ScopeName = "go.opentelemetry.io/otel/exporters/stdoutlog/internal/observ" + ScopeName = "go.opentelemetry.io/otel/exporters/stdout/stdoutlog/internal/observ" // ComponentType uniquely identifies the OpenTelemetry Exporter component // being instrumented. From 8e1b96352ab9c0f77c046d0e065316dd3fa3ad94 Mon Sep 17 00:00:00 2001 From: yumosx Date: Tue, 28 Apr 2026 09:24:45 +0800 Subject: [PATCH 04/14] fix: update test for disabled stdout exporter to handle buffer initialization --- exporters/stdout/stdoutlog/exporter_test.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/exporters/stdout/stdoutlog/exporter_test.go b/exporters/stdout/stdoutlog/exporter_test.go index b611b5c1773..27d37af3d73 100644 --- a/exporters/stdout/stdoutlog/exporter_test.go +++ b/exporters/stdout/stdoutlog/exporter_test.go @@ -476,8 +476,10 @@ func TestObservability(t *testing.T) { { name: "Disabled", enabled: false, - test: func(t *testing.T, _ func() metricdata.ScopeMetrics) { - exporter := &Exporter{} + test: func(t *testing.T, scopeMetrics func() metricdata.ScopeMetrics) { + var buf bytes.Buffer + exporter, err := New(WithWriter(&buf)) + require.NoError(t, err) assert.Nil(t, exporter.inst) }, }, From 7fd062f1298bca48adddf7c4eba36578c1bdc754 Mon Sep 17 00:00:00 2001 From: yumosx Date: Tue, 28 Apr 2026 09:32:27 +0800 Subject: [PATCH 05/14] fix: correct usage of OTelComponentTypeKey in stdout exporter instrumentation --- exporters/stdout/stdoutlog/exporter_test.go | 2 +- exporters/stdout/stdoutlog/internal/observ/instrumentation.go | 2 +- .../stdout/stdoutlog/internal/observ/instrumentation_test.go | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/exporters/stdout/stdoutlog/exporter_test.go b/exporters/stdout/stdoutlog/exporter_test.go index 27d37af3d73..04db8e46a45 100644 --- a/exporters/stdout/stdoutlog/exporter_test.go +++ b/exporters/stdout/stdoutlog/exporter_test.go @@ -595,7 +595,7 @@ func (w *failingWriter) Write([]byte) (int, error) { func stdoutObservAttrSet(err error) attribute.Set { attrs := []attribute.KeyValue{ semconv.OTelComponentName(observ.GetComponentName(0)), - semconv.OTelComponentNameKey.String(observ.ComponentType), + semconv.OTelComponentTypeKey.String(observ.ComponentType), } if err != nil { attrs = append(attrs, semconv.ErrorType(err)) diff --git a/exporters/stdout/stdoutlog/internal/observ/instrumentation.go b/exporters/stdout/stdoutlog/internal/observ/instrumentation.go index 3d496337ff4..2f2928508bd 100644 --- a/exporters/stdout/stdoutlog/internal/observ/instrumentation.go +++ b/exporters/stdout/stdoutlog/internal/observ/instrumentation.go @@ -96,7 +96,7 @@ func getAttrs(id int64) []attribute.KeyValue { attrs := make([]attribute.KeyValue, 0, 2) attrs = append(attrs, semconv.OTelComponentName(GetComponentName(id)), - semconv.OTelComponentNameKey.String(ComponentType)) + semconv.OTelComponentTypeKey.String(ComponentType)) return attrs } diff --git a/exporters/stdout/stdoutlog/internal/observ/instrumentation_test.go b/exporters/stdout/stdoutlog/internal/observ/instrumentation_test.go index 89b4bbe9d26..4a2c21525a1 100644 --- a/exporters/stdout/stdoutlog/internal/observ/instrumentation_test.go +++ b/exporters/stdout/stdoutlog/internal/observ/instrumentation_test.go @@ -81,7 +81,7 @@ func TestNewInstrumentation(t *testing.T) { func set(err error) attribute.Set { attrs := []attribute.KeyValue{ semconv.OTelComponentName(GetComponentName(ID)), - semconv.OTelComponentNameKey.String(ComponentType), + semconv.OTelComponentTypeKey.String(ComponentType), } if err != nil { attrs = append(attrs, semconv.ErrorType(err)) From 2925f7cb376bf4d3263e944160bf9316be75f387 Mon Sep 17 00:00:00 2001 From: yumosx Date: Tue, 28 Apr 2026 09:41:24 +0800 Subject: [PATCH 06/14] fix: update test for disabled stdout exporter to use blank identifier for scopeMetrics --- exporters/stdout/stdoutlog/exporter_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/exporters/stdout/stdoutlog/exporter_test.go b/exporters/stdout/stdoutlog/exporter_test.go index 04db8e46a45..243c293fc41 100644 --- a/exporters/stdout/stdoutlog/exporter_test.go +++ b/exporters/stdout/stdoutlog/exporter_test.go @@ -476,7 +476,7 @@ func TestObservability(t *testing.T) { { name: "Disabled", enabled: false, - test: func(t *testing.T, scopeMetrics func() metricdata.ScopeMetrics) { + test: func(t *testing.T, _ func() metricdata.ScopeMetrics) { var buf bytes.Buffer exporter, err := New(WithWriter(&buf)) require.NoError(t, err) From 82b455acf01d178c43a13dac1bd12c5610b04eff Mon Sep 17 00:00:00 2001 From: yumosx Date: Tue, 28 Apr 2026 09:49:00 +0800 Subject: [PATCH 07/14] chore: add experimental self-observability metrics to stdout exporter --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d1a0f60423..35b8f9ed622 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,6 +36,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm - Add `WithDefaultAttributes` to `go.opentelemetry.io/otel/metric/x` to support setting default attributes on instruments. (#8135) - Add `Settable` to `go.opentelemetry.io/otel/metric/x` to allow reusing attribute options. (#8178) - Add experimental self-observability metrics in `go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp`. (#8194) +- Add experimental self-observability metrics in `go.opentelemetry.io/otel/exporters/stdout/stdoutlog`. (#8263) ### Changed From 9c3263d7f7716e71a8ff9dfac1f3fe78ccdc8970 Mon Sep 17 00:00:00 2001 From: yumosx Date: Tue, 28 Apr 2026 10:05:00 +0800 Subject: [PATCH 08/14] fix: add checks for inflight and exported metrics before updating in stdout exporter instrumentation --- .../internal/observ/instrumentation.go | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/exporters/stdout/stdoutlog/internal/observ/instrumentation.go b/exporters/stdout/stdoutlog/internal/observ/instrumentation.go index 2f2928508bd..2432e06725f 100644 --- a/exporters/stdout/stdoutlog/internal/observ/instrumentation.go +++ b/exporters/stdout/stdoutlog/internal/observ/instrumentation.go @@ -154,11 +154,12 @@ func NewInstrumentation(id int64) (*Instrumentation, error) { func (i *Instrumentation) ExportLogs(ctx context.Context, count int64) ExportOp { start := time.Now() - addOpt := get[metric.AddOption](addOptPool) - defer put(addOptPool, addOpt) - *addOpt = append(*addOpt, i.addOpt) - - i.inflight.Add(ctx, count, *addOpt...) + if i.inflight.Enabled(ctx) { + addOpt := get[metric.AddOption](addOptPool) + defer put(addOptPool, addOpt) + *addOpt = append(*addOpt, i.addOpt) + i.inflight.Add(ctx, count, *addOpt...) + } return ExportOp{ count: count, @@ -188,11 +189,15 @@ func (e ExportOp) End(success int64, err error) { defer put(addOptPool, addOpt) *addOpt = append(*addOpt, e.inst.addOpt) - e.inst.inflight.Add(e.ctx, -e.count, *addOpt...) + if e.inst.inflight.Enabled(e.ctx) { + e.inst.inflight.Add(e.ctx, -e.count, *addOpt...) + } - e.inst.exported.Add(e.ctx, success, *addOpt...) + if e.inst.exported.Enabled(e.ctx) { + e.inst.exported.Add(e.ctx, success, *addOpt...) + } - if err != nil { + if err != nil && e.inst.exported.Enabled(e.ctx) { // Add the error.type attribute to the attribute set. attrs := get[attribute.KeyValue](attrsPool) defer put(attrsPool, attrs) From 429a0f18a23d3479c0529e59692465456d018934 Mon Sep 17 00:00:00 2001 From: yumosx Date: Wed, 6 May 2026 10:03:01 +0800 Subject: [PATCH 09/14] refactor(instrumentation): consolidate metric options and optimize recording --- .../internal/observ/instrumentation.go | 49 +++++++++---------- 1 file changed, 23 insertions(+), 26 deletions(-) diff --git a/exporters/stdout/stdoutlog/internal/observ/instrumentation.go b/exporters/stdout/stdoutlog/internal/observ/instrumentation.go index 2432e06725f..f8244795bf1 100644 --- a/exporters/stdout/stdoutlog/internal/observ/instrumentation.go +++ b/exporters/stdout/stdoutlog/internal/observ/instrumentation.go @@ -82,8 +82,7 @@ type Instrumentation struct { duration metric.Float64Histogram attrs []attribute.KeyValue - addOpt metric.AddOption - recOpt metric.RecordOption + setOpt metric.MeasurementOption } // GetComponentName returns the constant name for the exporter with the @@ -143,8 +142,7 @@ func NewInstrumentation(id int64) (*Instrumentation, error) { return nil, err } inst.attrs = getAttrs(id) - inst.addOpt = metric.WithAttributeSet(attribute.NewSet(inst.attrs...)) - inst.recOpt = metric.WithAttributeSet(attribute.NewSet(inst.attrs...)) + inst.setOpt = metric.WithAttributeSet(attribute.NewSet(inst.attrs...)) return inst, nil } @@ -157,7 +155,7 @@ func (i *Instrumentation) ExportLogs(ctx context.Context, count int64) ExportOp if i.inflight.Enabled(ctx) { addOpt := get[metric.AddOption](addOptPool) defer put(addOptPool, addOpt) - *addOpt = append(*addOpt, i.addOpt) + *addOpt = append(*addOpt, i.setOpt) i.inflight.Add(ctx, count, *addOpt...) } @@ -185,46 +183,45 @@ type ExportOp struct { // If err is not nil, End records failed log exports as count-success with the // error.type attribute set from err. func (e ExportOp) End(success int64, err error) { + inflightSpansEnable := e.inst.inflight.Enabled(e.ctx) + exportedSpansEnable := e.inst.exported.Enabled(e.ctx) + opDurationEnable := e.inst.duration.Enabled(e.ctx) + + if !inflightSpansEnable && !exportedSpansEnable && !opDurationEnable { + return + } + addOpt := get[metric.AddOption](addOptPool) defer put(addOptPool, addOpt) - *addOpt = append(*addOpt, e.inst.addOpt) + *addOpt = append(*addOpt, e.inst.setOpt) - if e.inst.inflight.Enabled(e.ctx) { + if inflightSpansEnable { e.inst.inflight.Add(e.ctx, -e.count, *addOpt...) } - if e.inst.exported.Enabled(e.ctx) { + if exportedSpansEnable { e.inst.exported.Add(e.ctx, success, *addOpt...) } - if err != nil && e.inst.exported.Enabled(e.ctx) { + mOpt := e.inst.setOpt + if err != nil && exportedSpansEnable { // Add the error.type attribute to the attribute set. attrs := get[attribute.KeyValue](attrsPool) defer put(attrsPool, attrs) *attrs = append(*attrs, e.inst.attrs...) *attrs = append(*attrs, semconv.ErrorType(err)) - o := metric.WithAttributeSet(attribute.NewSet(*attrs...)) + mOpt = metric.WithAttributeSet(attribute.NewSet(*attrs...)) - *addOpt = append((*addOpt)[:0], o) + *addOpt = append((*addOpt)[:0], mOpt) e.inst.exported.Add(e.ctx, e.count-success, *addOpt...) } - recordOpt := get[metric.RecordOption](recordOptPool) - defer put(recordOptPool, recordOpt) + if opDurationEnable { + recordOpt := get[metric.RecordOption](recordOptPool) + defer put(recordOptPool, recordOpt) - *recordOpt = append(*recordOpt, e.inst.recordOption(err)) - e.inst.duration.Record(e.ctx, time.Since(e.start).Seconds(), *recordOpt...) -} - -func (i *Instrumentation) recordOption(err error) metric.RecordOption { - if err == nil { - return i.recOpt + *recordOpt = append(*recordOpt, mOpt) + e.inst.duration.Record(e.ctx, time.Since(e.start).Seconds(), *recordOpt...) } - attrs := get[attribute.KeyValue](attrsPool) - defer put(attrsPool, attrs) - - *attrs = append(*attrs, i.attrs...) - *attrs = append(*attrs, semconv.ErrorType(err)) - return metric.WithAttributeSet(attribute.NewSet(*attrs...)) } From e09e16341962d9bde68bd4142a813bc878637725 Mon Sep 17 00:00:00 2001 From: yumosx Date: Wed, 6 May 2026 11:33:47 +0800 Subject: [PATCH 10/14] refactor(instrumentation): rename span variables to log variables for clarity --- .../stdoutlog/internal/observ/instrumentation.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/exporters/stdout/stdoutlog/internal/observ/instrumentation.go b/exporters/stdout/stdoutlog/internal/observ/instrumentation.go index f8244795bf1..f7ef04b30eb 100644 --- a/exporters/stdout/stdoutlog/internal/observ/instrumentation.go +++ b/exporters/stdout/stdoutlog/internal/observ/instrumentation.go @@ -183,11 +183,11 @@ type ExportOp struct { // If err is not nil, End records failed log exports as count-success with the // error.type attribute set from err. func (e ExportOp) End(success int64, err error) { - inflightSpansEnable := e.inst.inflight.Enabled(e.ctx) - exportedSpansEnable := e.inst.exported.Enabled(e.ctx) + inflightLogsEnable := e.inst.inflight.Enabled(e.ctx) + exportedLogsEnable := e.inst.exported.Enabled(e.ctx) opDurationEnable := e.inst.duration.Enabled(e.ctx) - if !inflightSpansEnable && !exportedSpansEnable && !opDurationEnable { + if !inflightLogsEnable && !exportedLogsEnable && !opDurationEnable { return } @@ -195,16 +195,16 @@ func (e ExportOp) End(success int64, err error) { defer put(addOptPool, addOpt) *addOpt = append(*addOpt, e.inst.setOpt) - if inflightSpansEnable { + if inflightLogsEnable { e.inst.inflight.Add(e.ctx, -e.count, *addOpt...) } - if exportedSpansEnable { + if exportedLogsEnable { e.inst.exported.Add(e.ctx, success, *addOpt...) } mOpt := e.inst.setOpt - if err != nil && exportedSpansEnable { + if err != nil && exportedLogsEnable { // Add the error.type attribute to the attribute set. attrs := get[attribute.KeyValue](attrsPool) defer put(attrsPool, attrs) From b336b48d09f5ab9ae2411faf8a2d24f20e46effe Mon Sep 17 00:00:00 2001 From: yumosx Date: Fri, 8 May 2026 09:26:52 +0800 Subject: [PATCH 11/14] fix(instrumentation): add error.type attribute when opDurationEnable is true Previously, the error.type attribute was only added when exportedLogsEnable was true. If opDurationEnable was true but exportedLogsEnable was false, the error attribute was never recorded. Now the condition is expanded to (exportedLogsEnable || opDurationEnable), while still only calling e.inst.exported.Add when exportedLogsEnable is true. --- .../stdout/stdoutlog/internal/observ/instrumentation.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/exporters/stdout/stdoutlog/internal/observ/instrumentation.go b/exporters/stdout/stdoutlog/internal/observ/instrumentation.go index f7ef04b30eb..63c6b02b868 100644 --- a/exporters/stdout/stdoutlog/internal/observ/instrumentation.go +++ b/exporters/stdout/stdoutlog/internal/observ/instrumentation.go @@ -204,7 +204,7 @@ func (e ExportOp) End(success int64, err error) { } mOpt := e.inst.setOpt - if err != nil && exportedLogsEnable { + if err != nil && (exportedLogsEnable || opDurationEnable) { // Add the error.type attribute to the attribute set. attrs := get[attribute.KeyValue](attrsPool) defer put(attrsPool, attrs) @@ -213,8 +213,10 @@ func (e ExportOp) End(success int64, err error) { mOpt = metric.WithAttributeSet(attribute.NewSet(*attrs...)) - *addOpt = append((*addOpt)[:0], mOpt) - e.inst.exported.Add(e.ctx, e.count-success, *addOpt...) + if exportedLogsEnable { + *addOpt = append((*addOpt)[:0], mOpt) + e.inst.exported.Add(e.ctx, e.count-success, *addOpt...) + } } if opDurationEnable { From acea3e7bd63d72e3dd38f0d25ea89b3dae339b38 Mon Sep 17 00:00:00 2001 From: yumosx Date: Fri, 8 May 2026 09:38:49 +0800 Subject: [PATCH 12/14] fix(stdoutlog): align New behavior with other stdout exporters Previously, stdoutlog.New returned (nil, err) when self-observability setup failed, making the exporter unusable. This differed from stdouttrace.New and stdoutmetric.New, which return the exporter alongside the error, allowing core export functionality to continue even when experimental observability fails. Change stdoutlog.New to return the exporter with inst=nil and the instrumentation error separately, matching the behavior of the other stdout exporters. The Export method already guards against nil inst, so this is safe. Fixes the inconsistency where enabling observability could prevent stdout log export from working. --- exporters/stdout/stdoutlog/exporter.go | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/exporters/stdout/stdoutlog/exporter.go b/exporters/stdout/stdoutlog/exporter.go index 33a043801c9..94e9bfd3609 100644 --- a/exporters/stdout/stdoutlog/exporter.go +++ b/exporters/stdout/stdoutlog/exporter.go @@ -33,18 +33,14 @@ func New(options ...Option) (*Exporter, error) { enc.SetIndent("", "\t") } - inst, err := observ.NewInstrumentation(counter.NextExporterID()) - if err != nil { - return nil, err - } - - e := Exporter{ + e := &Exporter{ timestamps: cfg.Timestamps, - inst: inst, } e.encoder.Store(enc) - return &e, nil + var err error + e.inst, err = observ.NewInstrumentation(counter.NextExporterID()) + return e, err } // Export exports log records to writer. From a9320f570d837a7c6a6162f720a199b8f1ed93b1 Mon Sep 17 00:00:00 2001 From: yumosx Date: Mon, 18 May 2026 14:15:47 +0800 Subject: [PATCH 13/14] fix(stdoutlog): update semconv import to v1.41.0 in test The observ package uses semconv v1.41.0, but the test was importing v1.40.0, causing SchemaURL mismatch failures in TestObservability. Updates the test imports to match the actual package version. --- exporters/stdout/stdoutlog/exporter_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/exporters/stdout/stdoutlog/exporter_test.go b/exporters/stdout/stdoutlog/exporter_test.go index ed10318782f..242b86200b7 100644 --- a/exporters/stdout/stdoutlog/exporter_test.go +++ b/exporters/stdout/stdoutlog/exporter_test.go @@ -28,8 +28,8 @@ import ( "go.opentelemetry.io/otel/sdk/metric/metricdata" "go.opentelemetry.io/otel/sdk/metric/metricdata/metricdatatest" "go.opentelemetry.io/otel/sdk/resource" - semconv "go.opentelemetry.io/otel/semconv/v1.40.0" - "go.opentelemetry.io/otel/semconv/v1.40.0/otelconv" + semconv "go.opentelemetry.io/otel/semconv/v1.41.0" + "go.opentelemetry.io/otel/semconv/v1.41.0/otelconv" "go.opentelemetry.io/otel/trace" ) From a55eda4384962bb54e05406ff5aa3ae2e18f9c04 Mon Sep 17 00:00:00 2001 From: yumosx Date: Mon, 25 May 2026 00:08:57 +0800 Subject: [PATCH 14/14] fix markdown --- CHANGELOG.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 028ce3691e4..cebf526fcf2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -41,12 +41,9 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm - Add `go.opentelemetry.io/otel/semconv/v1.41.0` package. The package contains semantic conventions from the `v1.41.0` version of the OpenTelemetry Semantic Conventions. See the [migration documentation](./semconv/v1.41.0/MIGRATION.md) for information on how to upgrade from `go.opentelemetry.io/otel/semconv/v1.40.0`. (#8324) -<<<<<<< feat/yumosx/inst-stdlog - Add experimental self-observability metrics in `go.opentelemetry.io/otel/exporters/stdout/stdoutlog`. (#8263) -======= - Add Observable variants of instruments to `go.opentelemetry.io/otel/semconv/v1.41.0` package. (#8350) - Generate explicit histogram bucket boundaries from weaver configuration for HTTP and RPC duration instruments in `go.opentelemetry.io/otel/semconv/v1.41.0`. (#8002) ->>>>>>> main ### Changed