From b335c0795c04dc4868fa62a8ba58edd04528b060 Mon Sep 17 00:00:00 2001 From: Tyler Yahn Date: Mon, 8 Sep 2025 07:29:01 -0700 Subject: [PATCH] Encapsulate observability in Logs SDK (#7315) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - [Follow guidelines](https://github.com/open-telemetry/opentelemetry-go/blob/a5dcd68ebb2f3669f7685ac7b0f3f1624251a381/CONTRIBUTING.md#encapsulation) and encapsulate the Observability for the Logs SDK logger by moving it into a single function. - Add benchmarks to ensure no performance regressions ### Benchmarks ```console $ benchstat main_2de26d1de.txt enc-sdk-log-obs_c06e888.txt goos: linux goarch: amd64 pkg: go.opentelemetry.io/otel/sdk/log cpu: Intel(R) Core(TM) i7-8550U CPU @ 1.80GHz │ main_2de26d1de.txt │ enc-sdk-log-obs_c06e888.txt │ │ sec/op │ sec/op vs base │ LoggerEmitObservability/Disabled-8 167.8n ± 4% 167.3n ± 5% ~ (p=0.796 n=10) LoggerEmitObservability/Enabled-8 556.6n ± 4% 556.1n ± 4% ~ (p=0.955 n=10) geomean 305.6n 305.0n -0.19% │ main_2de26d1de.txt │ enc-sdk-log-obs_c06e888.txt │ │ B/op │ B/op vs base │ LoggerEmitObservability/Disabled-8 448.0 ± 0% 448.0 ± 0% ~ (p=1.000 n=10) ¹ LoggerEmitObservability/Enabled-8 448.0 ± 0% 448.0 ± 0% ~ (p=1.000 n=10) ¹ geomean 448.0 448.0 +0.00% ¹ all samples are equal │ main_2de26d1de.txt │ enc-sdk-log-obs_c06e888.txt │ │ allocs/op │ allocs/op vs base │ LoggerEmitObservability/Disabled-8 1.000 ± 0% 1.000 ± 0% ~ (p=1.000 n=10) ¹ LoggerEmitObservability/Enabled-8 1.000 ± 0% 1.000 ± 0% ~ (p=1.000 n=10) ¹ geomean 1.000 1.000 +0.00% ¹ all samples are equal ``` --- sdk/log/instrumentation.go | 39 +++++++++++++++++++++++++++++++++ sdk/log/logger.go | 27 ++++++----------------- sdk/log/logger_bench_test.go | 42 ++++++++++++++++++++++++++++++++++++ 3 files changed, 88 insertions(+), 20 deletions(-) create mode 100644 sdk/log/instrumentation.go diff --git a/sdk/log/instrumentation.go b/sdk/log/instrumentation.go new file mode 100644 index 000000000..0a82ef8d5 --- /dev/null +++ b/sdk/log/instrumentation.go @@ -0,0 +1,39 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package log // import "go.opentelemetry.io/otel/sdk/log" + +import ( + "context" + "fmt" + + "go.opentelemetry.io/otel" + "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" +) + +// newRecordCounterIncr returns a function that increments the log record +// counter metric. If observability is disabled, it returns nil. +func newRecordCounterIncr() (func(context.Context), error) { + if !x.SelfObservability.Enabled() { + return nil, nil + } + + m := otel.GetMeterProvider().Meter( + "go.opentelemetry.io/otel/sdk/log", + metric.WithInstrumentationVersion(sdk.Version()), + metric.WithSchemaURL(semconv.SchemaURL), + ) + + created, err := otelconv.NewSDKLogCreated(m) + if err != nil { + err = fmt.Errorf("failed to create log created metric: %w", err) + return nil, err + } + inst := created.Inst() + f := func(ctx context.Context) { inst.Add(ctx, 1) } + return f, nil +} diff --git a/sdk/log/logger.go b/sdk/log/logger.go index 7dad98c92..d305aee6d 100644 --- a/sdk/log/logger.go +++ b/sdk/log/logger.go @@ -5,18 +5,12 @@ package log // import "go.opentelemetry.io/otel/sdk/log" import ( "context" - "fmt" "time" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/log" "go.opentelemetry.io/otel/log/embedded" - "go.opentelemetry.io/otel/metric" - "go.opentelemetry.io/otel/sdk" "go.opentelemetry.io/otel/sdk/instrumentation" - "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" "go.opentelemetry.io/otel/trace" ) @@ -31,8 +25,9 @@ type logger struct { provider *LoggerProvider instrumentationScope instrumentation.Scope - selfObservabilityEnabled bool - logCreatedMetric otelconv.SDKLogCreated + // recCntIncr increments the count of log records created. It will be nil + // if observability is disabled. + recCntIncr func(context.Context) } func newLogger(p *LoggerProvider, scope instrumentation.Scope) *logger { @@ -40,18 +35,10 @@ func newLogger(p *LoggerProvider, scope instrumentation.Scope) *logger { provider: p, instrumentationScope: scope, } - if !x.SelfObservability.Enabled() { - return l - } - l.selfObservabilityEnabled = true - mp := otel.GetMeterProvider() - m := mp.Meter("go.opentelemetry.io/otel/sdk/log", - metric.WithInstrumentationVersion(sdk.Version()), - metric.WithSchemaURL(semconv.SchemaURL)) var err error - if l.logCreatedMetric, err = otelconv.NewSDKLogCreated(m); err != nil { - err = fmt.Errorf("failed to create log created metric: %w", err) + l.recCntIncr, err = newRecordCounterIncr() + if err != nil { otel.Handle(err) } return l @@ -119,8 +106,8 @@ func (l *logger) newRecord(ctx context.Context, r log.Record) Record { attributeCountLimit: l.provider.attributeCountLimit, allowDupKeys: l.provider.allowDupKeys, } - if l.selfObservabilityEnabled { - l.logCreatedMetric.Add(ctx, 1) + if l.recCntIncr != nil { + l.recCntIncr(ctx) } // This ensures we deduplicate key-value collections in the log body diff --git a/sdk/log/logger_bench_test.go b/sdk/log/logger_bench_test.go index 2afe8a313..d7bf70291 100644 --- a/sdk/log/logger_bench_test.go +++ b/sdk/log/logger_bench_test.go @@ -10,7 +10,11 @@ import ( "github.com/stretchr/testify/require" + "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/log" + "go.opentelemetry.io/otel/sdk/instrumentation" + "go.opentelemetry.io/otel/sdk/metric" + "go.opentelemetry.io/otel/sdk/metric/metricdata" ) func BenchmarkLoggerEmit(b *testing.B) { @@ -62,6 +66,44 @@ func BenchmarkLoggerEmit(b *testing.B) { }) } +func BenchmarkLoggerEmitObservability(b *testing.B) { + r := log.Record{} + + orig := otel.GetMeterProvider() + b.Cleanup(func() { otel.SetMeterProvider(orig) }) + reader := metric.NewManualReader() + mp := metric.NewMeterProvider(metric.WithReader(reader)) + otel.SetMeterProvider(mp) + + run := func(logger *logger) func(b *testing.B) { + return func(b *testing.B) { + b.ResetTimer() + b.ReportAllocs() + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + logger.Emit(context.Background(), r) + } + }) + } + } + + lp := NewLoggerProvider() + scope := instrumentation.Scope{} + + b.Run("Disabled", run(newLogger(lp, scope))) + + b.Run("Enabled", func(b *testing.B) { + b.Setenv("OTEL_GO_X_SELF_OBSERVABILITY", "true") + + run(newLogger(lp, scope))(b) + }) + + var rm metricdata.ResourceMetrics + err := reader.Collect(context.Background(), &rm) + require.NoError(b, err) + require.Len(b, rm.ScopeMetrics, 1) +} + func BenchmarkLoggerEnabled(b *testing.B) { logger := newTestLogger(b) ctx := context.Background()