From f22fb359722fb7086a6bdebccc25e5471102741c Mon Sep 17 00:00:00 2001 From: yumosx Date: Tue, 21 Oct 2025 11:06:23 +0800 Subject: [PATCH 1/8] feat(sdk/log): add observability instrumentation for SimpleLogProcessor --- sdk/log/internal/observ/doc.go | 6 + .../internal/observ/simple_log_processor.go | 104 ++++++++++++++++++ 2 files changed, 110 insertions(+) create mode 100644 sdk/log/internal/observ/doc.go create mode 100644 sdk/log/internal/observ/simple_log_processor.go diff --git a/sdk/log/internal/observ/doc.go b/sdk/log/internal/observ/doc.go new file mode 100644 index 00000000000..6b927ff1d28 --- /dev/null +++ b/sdk/log/internal/observ/doc.go @@ -0,0 +1,6 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +// Package observ provides observability instrumentation for the OTel trace SDK +// package. +package observ // import "go.opentelemetry.io/otel/sdk/log/internal/observ" diff --git a/sdk/log/internal/observ/simple_log_processor.go b/sdk/log/internal/observ/simple_log_processor.go new file mode 100644 index 00000000000..8cd687de68b --- /dev/null +++ b/sdk/log/internal/observ/simple_log_processor.go @@ -0,0 +1,104 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package observ + +import ( + "context" + "fmt" + "sync" + + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/metric" + "go.opentelemetry.io/otel/sdk" + "go.opentelemetry.io/otel/sdk/log/internal/x" + semconv "go.opentelemetry.io/otel/semconv/v1.37.0" + "go.opentelemetry.io/otel/semconv/v1.37.0/otelconv" +) + +const ( + ScopeName = "go.opentelemetry.io/otel/sdk/log/internal/observ" +) + +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 + }, +} + +func GetComponentName(id int64) attribute.KeyValue { + t := otelconv.ComponentTypeSimpleLogProcessor + name := fmt.Sprintf("%s/%d", t, id) + return semconv.OTelComponentName(name) +} + +// SLP is the instrumentation fot an OTel SDK SimpleLogProcessor. +type SLP struct { + processed metric.Int64Counter + attrs []attribute.KeyValue + addOpts []metric.AddOption +} + +// NewSlP returns instrumentation for an OTel SDK SimpleLogProcessor with the +// provided ID. +// +// If the experimental observability is disabled, nil is returned. +func NewSLP(id int64) (*SLP, error) { + if !x.Observability.Enabled() { + return nil, nil + } + + meter := otel.GetMeterProvider() + mt := meter.Meter( + ScopeName, + metric.WithInstrumentationVersion(sdk.Version()), + metric.WithSchemaURL(semconv.SchemaURL), + ) + + p, err := otelconv.NewSDKProcessorLogProcessed(mt) + if err != nil { + err = fmt.Errorf("failed to created a processed metrics %w", err) + return nil, err + } + + name := GetComponentName(id) + componentType := p.AttrComponentType(otelconv.ComponentTypeBatchingLogProcessor) + attrs := []attribute.KeyValue{name, componentType} + addOpts := []metric.AddOption{metric.WithAttributeSet(attribute.NewSet(attrs...))} + + return &SLP{ + processed: p.Inst(), + attrs: attrs, + addOpts: addOpts, + }, nil +} + +// LogProcessed records that a log has been processed by the SimpleLogProcessor. +// If err is non-nil, it records the processing error as an attribute. +func (slp *SLP) LogProcessed(ctx context.Context, err error) { + slp.processed.Add(ctx, 1, slp.addOption(err)...) +} + +func (slp *SLP) addOption(err error) []metric.AddOption { + if err != nil { + return slp.addOpts + } + attrs := measureAttrsPool.Get().(*[]attribute.KeyValue) + defer func() { + *attrs = (*attrs)[:0] // reset the slice + measureAttrsPool.Put(attrs) + }() + + *attrs = append(*attrs, slp.attrs...) + *attrs = append(*attrs, semconv.ErrorType(err)) + + // Do not inefficiently make a copy of attrs by using + // WithAttributes instead of WithAttributeSet. + return []metric.AddOption{metric.WithAttributeSet(attribute.NewSet(*attrs...))} +} From 2b3582b8d9be7f1015dd2006fba22db41bc65b0d Mon Sep 17 00:00:00 2001 From: yumosx Date: Tue, 21 Oct 2025 23:39:12 +0800 Subject: [PATCH 2/8] init test --- .../internal/observ/simple_log_processor.go | 9 +-- .../observ/simple_log_processor_test.go | 69 +++++++++++++++++++ 2 files changed, 74 insertions(+), 4 deletions(-) create mode 100644 sdk/log/internal/observ/simple_log_processor_test.go diff --git a/sdk/log/internal/observ/simple_log_processor.go b/sdk/log/internal/observ/simple_log_processor.go index 8cd687de68b..e368b204288 100644 --- a/sdk/log/internal/observ/simple_log_processor.go +++ b/sdk/log/internal/observ/simple_log_processor.go @@ -1,7 +1,7 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -package observ +package observ // import "go.opentelemetry.io/otel/sdk/log/internal/observ" import ( "context" @@ -18,6 +18,7 @@ import ( ) const ( + // ScopeName is the name of the instrumentation scope. ScopeName = "go.opentelemetry.io/otel/sdk/log/internal/observ" ) @@ -45,7 +46,7 @@ type SLP struct { addOpts []metric.AddOption } -// NewSlP returns instrumentation for an OTel SDK SimpleLogProcessor with the +// NewSLP returns instrumentation for an OTel SDK SimpleLogProcessor with the // provided ID. // // If the experimental observability is disabled, nil is returned. @@ -63,7 +64,7 @@ func NewSLP(id int64) (*SLP, error) { p, err := otelconv.NewSDKProcessorLogProcessed(mt) if err != nil { - err = fmt.Errorf("failed to created a processed metrics %w", err) + err = fmt.Errorf("failed to create a processed log metric: %w", err) return nil, err } @@ -86,7 +87,7 @@ func (slp *SLP) LogProcessed(ctx context.Context, err error) { } func (slp *SLP) addOption(err error) []metric.AddOption { - if err != nil { + if err == nil { return slp.addOpts } attrs := measureAttrsPool.Get().(*[]attribute.KeyValue) diff --git a/sdk/log/internal/observ/simple_log_processor_test.go b/sdk/log/internal/observ/simple_log_processor_test.go new file mode 100644 index 00000000000..2ad449fce43 --- /dev/null +++ b/sdk/log/internal/observ/simple_log_processor_test.go @@ -0,0 +1,69 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package observ + +import ( + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.opentelemetry.io/otel" + mapi "go.opentelemetry.io/otel/metric" + "go.opentelemetry.io/otel/sdk/metric" + "go.opentelemetry.io/otel/sdk/metric/metricdata" + "testing" +) + +type errMeterProvider struct { + mapi.MeterProvider + err error +} + +func (m *errMeterProvider) Meter(string, ...mapi.MeterOption) mapi.Meter { + return &errMeter{err: m.err} +} + +type errMeter struct { + mapi.Meter + err error +} + +func (m *errMeter) Int64Counter(string, ...mapi.Int64CounterOption) (mapi.Int64Counter, error) { + return nil, m.err +} + +const slpComponentID = 0 + +func TestNewSLPError(t *testing.T) { + t.Setenv("OTEL_GO_X_OBSERVABILITY", "true") + orig := otel.GetMeterProvider() + t.Cleanup(func() { otel.SetMeterProvider(orig) }) + + errMp := &errMeterProvider{err: assert.AnError} + otel.SetMeterProvider(errMp) + + _, err := NewSLP(slpComponentID) + require.ErrorIs(t, err, assert.AnError) + assert.ErrorContains(t, err, "failed to create a processed log metric") +} + +func setup(t *testing.T) func() metricdata.ScopeMetrics { + t.Setenv("OTEL_GO_X_OBSERVABILITY", "true") + + orig := otel.GetMeterProvider() + t.Cleanup(func() { + otel.SetMeterProvider(orig) + }) + + reader := metric.NewManualReader() + mp := metric.NewMeterProvider(metric.WithReader(reader)) + otel.SetMeterProvider(mp) + + return func() metricdata.ScopeMetrics { + var got metricdata.ResourceMetrics + require.NoError(t, reader.Collect(t.Context(), &got)) + if len(got.ScopeMetrics) != 1 { + return metricdata.ScopeMetrics{} + } + return got.ScopeMetrics[0] + } +} From f713b45317df39cea4d43e340f67462ad20bc548 Mon Sep 17 00:00:00 2001 From: yumosx Date: Thu, 23 Oct 2025 00:03:36 +0800 Subject: [PATCH 3/8] refactor the test and imporve --- .../internal/observ/simple_log_processor.go | 2 +- .../observ/simple_log_processor_test.go | 87 +++++++++++++++++-- 2 files changed, 82 insertions(+), 7 deletions(-) diff --git a/sdk/log/internal/observ/simple_log_processor.go b/sdk/log/internal/observ/simple_log_processor.go index e368b204288..e3300de4618 100644 --- a/sdk/log/internal/observ/simple_log_processor.go +++ b/sdk/log/internal/observ/simple_log_processor.go @@ -69,7 +69,7 @@ func NewSLP(id int64) (*SLP, error) { } name := GetComponentName(id) - componentType := p.AttrComponentType(otelconv.ComponentTypeBatchingLogProcessor) + componentType := p.AttrComponentType(otelconv.ComponentTypeSimpleLogProcessor) attrs := []attribute.KeyValue{name, componentType} addOpts := []metric.AddOption{metric.WithAttributeSet(attribute.NewSet(attrs...))} diff --git a/sdk/log/internal/observ/simple_log_processor_test.go b/sdk/log/internal/observ/simple_log_processor_test.go index 2ad449fce43..3480df56414 100644 --- a/sdk/log/internal/observ/simple_log_processor_test.go +++ b/sdk/log/internal/observ/simple_log_processor_test.go @@ -4,13 +4,22 @@ package observ import ( + "errors" + "testing" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" mapi "go.opentelemetry.io/otel/metric" + "go.opentelemetry.io/otel/sdk" + "go.opentelemetry.io/otel/sdk/instrumentation" "go.opentelemetry.io/otel/sdk/metric" "go.opentelemetry.io/otel/sdk/metric/metricdata" - "testing" + "go.opentelemetry.io/otel/sdk/metric/metricdata/metricdatatest" + semconv "go.opentelemetry.io/otel/semconv/v1.37.0" + "go.opentelemetry.io/otel/semconv/v1.37.0/otelconv" ) type errMeterProvider struct { @@ -46,7 +55,7 @@ func TestNewSLPError(t *testing.T) { assert.ErrorContains(t, err, "failed to create a processed log metric") } -func setup(t *testing.T) func() metricdata.ScopeMetrics { +func setup(t *testing.T) (*SLP, func() metricdata.ScopeMetrics) { t.Setenv("OTEL_GO_X_OBSERVABILITY", "true") orig := otel.GetMeterProvider() @@ -58,12 +67,78 @@ func setup(t *testing.T) func() metricdata.ScopeMetrics { mp := metric.NewMeterProvider(metric.WithReader(reader)) otel.SetMeterProvider(mp) - return func() metricdata.ScopeMetrics { + slp, err := NewSLP(slpComponentID) + require.NoError(t, err) + require.NotNil(t, slp) + + return slp, func() metricdata.ScopeMetrics { var got metricdata.ResourceMetrics require.NoError(t, reader.Collect(t.Context(), &got)) - if len(got.ScopeMetrics) != 1 { - return metricdata.ScopeMetrics{} - } + require.Len(t, got.ScopeMetrics, 1) return got.ScopeMetrics[0] } } + +func processedMetric(err error) metricdata.Metrics { + processed := &otelconv.SDKProcessorLogProcessed{} + + attrs := []attribute.KeyValue{ + GetComponentName(slpComponentID), + processed.AttrComponentType(otelconv.ComponentTypeSimpleLogProcessor), + } + + if err != nil { + attrs = append(attrs, semconv.ErrorType(err)) + } + + dp := []metricdata.DataPoint[int64]{ + { + Attributes: attribute.NewSet(attrs...), + Value: 1, + }, + } + + return metricdata.Metrics{ + Name: processed.Name(), + Description: processed.Description(), + Unit: processed.Unit(), + Data: metricdata.Sum[int64]{ + Temporality: metricdata.CumulativeTemporality, + IsMonotonic: true, + DataPoints: dp, + }, + } +} + +var Scope = instrumentation.Scope{ + Name: ScopeName, + Version: sdk.Version(), + SchemaURL: semconv.SchemaURL, +} + +func assertMetric(t *testing.T, got metricdata.ScopeMetrics, err error) { + t.Helper() + assert.Equal(t, Scope, got.Scope, "unexpected scope") + m := got.Metrics + require.Len(t, m, 1, "expected 1 metrics") + + o := metricdatatest.IgnoreTimestamp() + want := processedMetric(err) + + metricdatatest.AssertEqual(t, want, m[0], o) +} + +func TestSLP(t *testing.T) { + t.Run("NoError", func(t *testing.T) { + slp, collect := setup(t) + slp.LogProcessed(t.Context(), nil) + assertMetric(t, collect(), nil) + }) + + t.Run("Error", func(t *testing.T) { + processErr := errors.New("error processing log") + slp, collect := setup(t) + slp.LogProcessed(t.Context(), processErr) + assertMetric(t, collect(), processErr) + }) +} From 8f48afdc99855f331ff03d65a31127d42e34dc2e Mon Sep 17 00:00:00 2001 From: yumosx Date: Thu, 23 Oct 2025 21:16:39 +0800 Subject: [PATCH 4/8] add benchamarkSLP --- .../observ/simple_log_processor_test.go | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/sdk/log/internal/observ/simple_log_processor_test.go b/sdk/log/internal/observ/simple_log_processor_test.go index 3480df56414..bec8a6c2a71 100644 --- a/sdk/log/internal/observ/simple_log_processor_test.go +++ b/sdk/log/internal/observ/simple_log_processor_test.go @@ -13,6 +13,7 @@ import ( "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" mapi "go.opentelemetry.io/otel/metric" + "go.opentelemetry.io/otel/metric/noop" "go.opentelemetry.io/otel/sdk" "go.opentelemetry.io/otel/sdk/instrumentation" "go.opentelemetry.io/otel/sdk/metric" @@ -142,3 +143,55 @@ func TestSLP(t *testing.T) { assertMetric(t, collect(), processErr) }) } + +func BenchmarkSLP(b *testing.B) { + b.Setenv("OTEL_GO_X_OBSERVABILITY", "true") + + newSLP := func(b *testing.B) *SLP { + b.Helper() + slp, err := NewSLP(slpComponentID) + require.NoError(b, err) + require.NotNil(b, slp) + return slp + } + + b.Run("LogProcessed", func(b *testing.B) { + orig := otel.GetMeterProvider() + b.Cleanup(func() { + otel.SetMeterProvider(orig) + }) + + otel.SetMeterProvider(noop.NewMeterProvider()) + + ssp := newSLP(b) + ctx := b.Context() + + b.ResetTimer() + b.ReportAllocs() + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + ssp.LogProcessed(ctx, nil) + } + }) + }) + + b.Run("LogProcessedWithError", func(b *testing.B) { + orig := otel.GetMeterProvider() + b.Cleanup(func() { + otel.SetMeterProvider(orig) + }) + otel.SetMeterProvider(noop.NewMeterProvider()) + slp := newSLP(b) + ctx := b.Context() + + processErr := errors.New("error processing log") + + b.ResetTimer() + b.ReportAllocs() + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + slp.LogProcessed(ctx, processErr) + } + }) + }) +} From 41b2d055ac12111be48254ad9bab6b06c323c699 Mon Sep 17 00:00:00 2001 From: yumosx Date: Thu, 23 Oct 2025 21:37:12 +0800 Subject: [PATCH 5/8] fix --- sdk/log/internal/observ/simple_log_processor.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/log/internal/observ/simple_log_processor.go b/sdk/log/internal/observ/simple_log_processor.go index e3300de4618..9ea7bf2714d 100644 --- a/sdk/log/internal/observ/simple_log_processor.go +++ b/sdk/log/internal/observ/simple_log_processor.go @@ -39,7 +39,7 @@ func GetComponentName(id int64) attribute.KeyValue { return semconv.OTelComponentName(name) } -// SLP is the instrumentation fot an OTel SDK SimpleLogProcessor. +// SLP is the instrumentation for an OTel SDK SimpleLogProcessor. type SLP struct { processed metric.Int64Counter attrs []attribute.KeyValue From 8f3c645da69bd3487ebe981700679a65221de51b Mon Sep 17 00:00:00 2001 From: yumosx Date: Thu, 23 Oct 2025 21:41:33 +0800 Subject: [PATCH 6/8] add TestNewSLPDisabled --- sdk/log/internal/observ/simple_log_processor_test.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/sdk/log/internal/observ/simple_log_processor_test.go b/sdk/log/internal/observ/simple_log_processor_test.go index bec8a6c2a71..a95525e3aac 100644 --- a/sdk/log/internal/observ/simple_log_processor_test.go +++ b/sdk/log/internal/observ/simple_log_processor_test.go @@ -56,6 +56,13 @@ func TestNewSLPError(t *testing.T) { assert.ErrorContains(t, err, "failed to create a processed log metric") } +func TestNewSLPDisabled(t *testing.T) { + // Do not set OTEL_GO_X_OBSERVABILITY + bsp, err := NewSLP(slpComponentID) + assert.NoError(t, err) + assert.Nil(t, bsp) +} + func setup(t *testing.T) (*SLP, func() metricdata.ScopeMetrics) { t.Setenv("OTEL_GO_X_OBSERVABILITY", "true") From 83d382f400ecbfd2b10475d527cfafd087a8876a Mon Sep 17 00:00:00 2001 From: yumosx Date: Thu, 23 Oct 2025 21:48:41 +0800 Subject: [PATCH 7/8] add comment --- sdk/log/internal/observ/simple_log_processor.go | 6 ++++-- sdk/log/internal/observ/simple_log_processor_test.go | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/sdk/log/internal/observ/simple_log_processor.go b/sdk/log/internal/observ/simple_log_processor.go index 9ea7bf2714d..f69bc5f1d38 100644 --- a/sdk/log/internal/observ/simple_log_processor.go +++ b/sdk/log/internal/observ/simple_log_processor.go @@ -33,7 +33,9 @@ var measureAttrsPool = sync.Pool{ }, } -func GetComponentName(id int64) attribute.KeyValue { +// GetSLPComponentName returns the component name attribute for a +// SimpleLogProcessor with the given ID. +func GetSLPComponentName(id int64) attribute.KeyValue { t := otelconv.ComponentTypeSimpleLogProcessor name := fmt.Sprintf("%s/%d", t, id) return semconv.OTelComponentName(name) @@ -68,7 +70,7 @@ func NewSLP(id int64) (*SLP, error) { return nil, err } - name := GetComponentName(id) + name := GetSLPComponentName(id) componentType := p.AttrComponentType(otelconv.ComponentTypeSimpleLogProcessor) attrs := []attribute.KeyValue{name, componentType} addOpts := []metric.AddOption{metric.WithAttributeSet(attribute.NewSet(attrs...))} diff --git a/sdk/log/internal/observ/simple_log_processor_test.go b/sdk/log/internal/observ/simple_log_processor_test.go index a95525e3aac..1d63c4d6a94 100644 --- a/sdk/log/internal/observ/simple_log_processor_test.go +++ b/sdk/log/internal/observ/simple_log_processor_test.go @@ -91,7 +91,7 @@ func processedMetric(err error) metricdata.Metrics { processed := &otelconv.SDKProcessorLogProcessed{} attrs := []attribute.KeyValue{ - GetComponentName(slpComponentID), + GetSLPComponentName(slpComponentID), processed.AttrComponentType(otelconv.ComponentTypeSimpleLogProcessor), } From f6e8deaeb5da3923d271783ee4a05f445d739bd3 Mon Sep 17 00:00:00 2001 From: ian <141902143+yumosx@users.noreply.github.com> Date: Thu, 23 Oct 2025 23:51:43 +0800 Subject: [PATCH 8/8] Update sdk/log/internal/observ/doc.go Co-authored-by: Tyler Yahn --- sdk/log/internal/observ/doc.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/log/internal/observ/doc.go b/sdk/log/internal/observ/doc.go index 6b927ff1d28..6879567cbf4 100644 --- a/sdk/log/internal/observ/doc.go +++ b/sdk/log/internal/observ/doc.go @@ -1,6 +1,6 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -// Package observ provides observability instrumentation for the OTel trace SDK +// Package observ provides observability instrumentation for the OTel log SDK // package. package observ // import "go.opentelemetry.io/otel/sdk/log/internal/observ"