Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
33b3ee5
add self observability for stdout exporter
yumosx Apr 26, 2026
fa36181
fix lint
yumosx Apr 26, 2026
304f31e
Merge branch 'main' into feat/yumosx/inst-stdlog
yumosx Apr 27, 2026
757f422
fix: update ScopeName for stdout exporter instrumentation path
yumosx Apr 28, 2026
8e1b963
fix: update test for disabled stdout exporter to handle buffer initia…
yumosx Apr 28, 2026
7fd062f
fix: correct usage of OTelComponentTypeKey in stdout exporter instrum…
yumosx Apr 28, 2026
2925f7c
fix: update test for disabled stdout exporter to use blank identifier…
yumosx Apr 28, 2026
82b455a
chore: add experimental self-observability metrics to stdout exporter
yumosx Apr 28, 2026
9c3263d
fix: add checks for inflight and exported metrics before updating in …
yumosx Apr 28, 2026
429a0f1
refactor(instrumentation): consolidate metric options and optimize re…
yumosx May 6, 2026
e09e163
refactor(instrumentation): rename span variables to log variables for…
yumosx May 6, 2026
4e99a51
Merge branch 'main' into feat/yumosx/inst-stdlog
yumosx May 7, 2026
b336b48
fix(instrumentation): add error.type attribute when opDurationEnable …
yumosx May 8, 2026
31181a5
Merge remote-tracking branch 'origin/feat/yumosx/inst-stdlog' into fe…
yumosx May 8, 2026
acea3e7
fix(stdoutlog): align New behavior with other stdout exporters
yumosx May 8, 2026
a0b41ed
Merge branch 'main' into feat/yumosx/inst-stdlog
yumosx May 11, 2026
baa4652
Merge branch 'main' into feat/yumosx/inst-stdlog
yumosx May 13, 2026
02d2667
Merge remote-tracking branch 'upstream/main' into feat/yumosx/inst-st…
yumosx May 18, 2026
a9320f5
fix(stdoutlog): update semconv import to v1.41.0 in test
yumosx May 18, 2026
c7f67de
Merge branch 'main' into feat/yumosx/inst-stdlog
pellared May 22, 2026
a55eda4
fix markdown
yumosx May 24, 2026
aabf803
Merge branch 'main' into feat/yumosx/inst-stdlog
pellared May 26, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ 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)
- 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)

Expand Down
21 changes: 18 additions & 3 deletions exporters/stdout/stdoutlog/exporter.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand All @@ -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].
Expand All @@ -29,21 +33,31 @@ func New(options ...Option) (*Exporter, error) {
enc.SetIndent("", "\t")
}

e := Exporter{
e := &Exporter{
timestamps: cfg.Timestamps,
}
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.
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)
}()
}
Comment thread
yumosx marked this conversation as resolved.

for _, record := range records {
// Honor context cancellation.
if err := ctx.Err(); err != nil {
Expand All @@ -55,6 +69,7 @@ func (e *Exporter) Export(ctx context.Context, records []log.Record) error {
if err := enc.Encode(recordJSON); err != nil {
return err
}
success++
}
return nil
}
Expand Down
223 changes: 223 additions & 0 deletions exporters/stdout/stdoutlog/exporter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,29 @@ import (
"bytes"
"context"
"encoding/json"
"errors"
"sync"
"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/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.41.0"
"go.opentelemetry.io/otel/semconv/v1.41.0/otelconv"
"go.opentelemetry.io/otel/trace"
)

Expand Down Expand Up @@ -458,3 +468,216 @@ 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, _ func() metricdata.ScopeMetrics) {
var buf bytes.Buffer
exporter, err := New(WithWriter(&buf))
require.NoError(t, err)
assert.Nil(t, exporter.inst)
},
Comment thread
yumosx marked this conversation as resolved.
},
{
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)
Comment thread
yumosx marked this conversation as resolved.
}
Comment thread
dashpole marked this conversation as resolved.
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 := b.Context()
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)
}
})
}

// 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.OTelComponentTypeKey.String(observ.ComponentType),
}
if err != nil {
attrs = append(attrs, semconv.ErrorType(err))
}
return attribute.NewSet(attrs...)
Comment thread
yumosx marked this conversation as resolved.
}

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,
Comment thread
yumosx marked this conversation as resolved.
}
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)
}
31 changes: 31 additions & 0 deletions exporters/stdout/stdoutlog/internal/counter/counter.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading