From f5071c3658246dfab047982eb240dbe252c5eb23 Mon Sep 17 00:00:00 2001 From: Brighten Tompkins Date: Thu, 28 Aug 2025 14:40:31 -0700 Subject: [PATCH 1/7] feat: otlp logging added to config --- clue/config.go | 29 +++- clue/config_test.go | 9 + clue/exporters.go | 39 +++++ .../forecaster/cmd/forecaster/main.go | 15 ++ .../weather/services/front/cmd/front/main.go | 15 ++ .../services/locator/cmd/locator/main.go | 15 ++ .../services/tester/cmd/tester/main.go | 15 ++ go.mod | 5 + go.sum | 12 ++ go.work.sum | 23 +++ interceptors/trace_stream_test.go | 7 +- log/context.go | 4 + log/log.go | 161 +++++++++++++++++- log/options.go | 12 ++ 14 files changed, 354 insertions(+), 7 deletions(-) diff --git a/clue/config.go b/clue/config.go index ce71d066..494bbc81 100644 --- a/clue/config.go +++ b/clue/config.go @@ -5,9 +5,12 @@ import ( "github.com/go-logr/logr" "go.opentelemetry.io/otel" + otellog "go.opentelemetry.io/otel/log" + "go.opentelemetry.io/otel/log/global" "go.opentelemetry.io/otel/metric" metricnoop "go.opentelemetry.io/otel/metric/noop" "go.opentelemetry.io/otel/propagation" + sdklog "go.opentelemetry.io/otel/sdk/log" sdkmetric "go.opentelemetry.io/otel/sdk/metric" "go.opentelemetry.io/otel/sdk/resource" sdktrace "go.opentelemetry.io/otel/sdk/trace" @@ -30,6 +33,8 @@ type ( MeterProvider metric.MeterProvider // TracerProvider is the OpenTelemetry tracer provider used clue TracerProvider trace.TracerProvider + // LoggerProvider is the OpenTelemetry logger provider used by clue + LoggerProvider otellog.LoggerProvider // Propagators is the OpenTelemetry propagator used by clue Propagators propagation.TextMapPropagator // ErrorHandler is the error handler used by OpenTelemetry @@ -42,9 +47,15 @@ type ( func ConfigureOpenTelemetry(ctx context.Context, cfg *Config) { otel.SetMeterProvider(cfg.MeterProvider) otel.SetTracerProvider(cfg.TracerProvider) + global.SetLoggerProvider(cfg.LoggerProvider) otel.SetTextMapPropagator(cfg.Propagators) otel.SetLogger(logr.New(log.ToLogrSink(ctx))) otel.SetErrorHandler(cfg.ErrorHandler) + + if cfg.LoggerProvider != nil { + otelLogger := cfg.LoggerProvider.Logger("clue") + ctx = log.Context(ctx, log.WithOTELLogger(otelLogger)) + } } // NewConfig creates a new Config object adequate for use by @@ -67,13 +78,18 @@ func ConfigureOpenTelemetry(ctx context.Context, cfg *Config) { // if err != nil { // return err // } -// cfg := clue.NewConfig(ctx, "mysvc", "1.0.0", metricExporter, spanExporter) +// logExporter, err := stdoutlog.New() +// if err != nil { +// return err +// } +// cfg := clue.NewConfig(ctx, "mysvc", "1.0.0", metricExporter, spanExporter, logExporter) func NewConfig( ctx context.Context, svcName string, svcVersion string, metricExporter sdkmetric.Exporter, spanExporter sdktrace.SpanExporter, + logExporter sdklog.Exporter, opts ...Option, ) (*Config, error) { options := defaultOptions(ctx) @@ -125,9 +141,20 @@ func NewConfig( sdktrace.WithBatcher(spanExporter), ) } + var loggerProvider otellog.LoggerProvider + if logExporter == nil { + loggerProvider = global.GetLoggerProvider() + } else { + loggerProvider = sdklog.NewLoggerProvider( + sdklog.WithResource(res), + sdklog.WithProcessor(sdklog.NewBatchProcessor(logExporter)), + ) + } + return &Config{ MeterProvider: meterProvider, TracerProvider: tracerProvider, + LoggerProvider: loggerProvider, Propagators: options.propagators, ErrorHandler: options.errorHandler, }, nil diff --git a/clue/config_test.go b/clue/config_test.go index 6eb262f1..b3669c72 100644 --- a/clue/config_test.go +++ b/clue/config_test.go @@ -8,11 +8,13 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/exporters/stdout/stdoutlog" "go.opentelemetry.io/otel/exporters/stdout/stdoutmetric" "go.opentelemetry.io/otel/exporters/stdout/stdouttrace" "go.opentelemetry.io/otel/metric" metricnoop "go.opentelemetry.io/otel/metric/noop" "go.opentelemetry.io/otel/propagation" + sdklog "go.opentelemetry.io/otel/sdk/log" sdkmetric "go.opentelemetry.io/otel/sdk/metric" sdktrace "go.opentelemetry.io/otel/sdk/trace" "go.opentelemetry.io/otel/trace" @@ -87,12 +89,15 @@ func TestNewConfig(t *testing.T) { require.NoError(t, err) metricsExporter, err := stdoutmetric.New() require.NoError(t, err) + logExporter, err := stdoutlog.New() + require.NoError(t, err) noopErrorHandler := dummyErrorHandler{} cases := []struct { name string metricsExporter sdkmetric.Exporter spanExporter sdktrace.SpanExporter + logExporter sdklog.Exporter propagators propagation.TextMapPropagator errorHandler otel.ErrorHandler @@ -107,6 +112,9 @@ func TestNewConfig(t *testing.T) { }, { name: "tracer provider", spanExporter: spanExporter, + }, { + name: "log exporter", + logExporter: logExporter, }, { name: "propagators", propagators: propagation.Baggage{}, @@ -124,6 +132,7 @@ func TestNewConfig(t *testing.T) { svcVersion, c.metricsExporter, c.spanExporter, + c.logExporter, WithPropagators(c.propagators), WithErrorHandler(c.errorHandler)) require.NoError(t, err) diff --git a/clue/exporters.go b/clue/exporters.go index 0862fa7d..107876c3 100644 --- a/clue/exporters.go +++ b/clue/exporters.go @@ -3,10 +3,13 @@ package clue import ( "context" + "go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc" + "go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp" "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc" "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp" "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc" "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp" + otellog "go.opentelemetry.io/otel/sdk/log" "go.opentelemetry.io/otel/sdk/metric" "go.opentelemetry.io/otel/sdk/trace" @@ -15,12 +18,31 @@ import ( // Allow mocking var ( + otlploggrpcNew = otlploggrpc.New + otlploghttpNew = otlploghttp.New otlpmetricgrpcNew = otlpmetricgrpc.New otlpmetrichttpNew = otlpmetrichttp.New otlptracegrpcNew = otlptracegrpc.New otlptracehttpNew = otlptracehttp.New ) +// NewGRPCLogExporter returns an OpenTelementry Protocol logs exporter that +// report logs to a gRPC collector. +func NewGRPCLogExporter(ctx context.Context, options ...otlploggrpc.Option) (exporter otellog.Exporter, shutdown func(), err error) { + exporter, err = otlploggrpcNew(ctx, options...) + if err != nil { + return + } + shutdown = func() { + // Create new context in case the parent context has been canceled. + ctx := log.WithContext(context.Background(), ctx) + if err := exporter.Shutdown(ctx); err != nil { + log.Errorf(ctx, err, "failed to shutdown log exporter") + } + } + return +} + // NewGRPCMetricExporter returns an OpenTelementry Protocol metric exporter that // report metrics to a gRPC collector. func NewGRPCMetricExporter(ctx context.Context, options ...otlpmetricgrpc.Option) (exporter metric.Exporter, shutdown func(), err error) { @@ -55,6 +77,23 @@ func NewGRPCSpanExporter(ctx context.Context, options ...otlptracegrpc.Option) ( return } +// NewHTTPLogExporter returns an OpenTelementry Protocol logs exporter that +// report logs to a HTTP collector. +func NewHTTPLogExporter(ctx context.Context, options ...otlploghttp.Option) (exporter otellog.Exporter, shutdown func(), err error) { + exporter, err = otlploghttpNew(ctx, options...) + if err != nil { + return + } + shutdown = func() { + // Create new context in case the parent context has been canceled. + ctx := log.WithContext(context.Background(), ctx) + if err := exporter.Shutdown(ctx); err != nil { + log.Errorf(ctx, err, "failed to shutdown log exporter") + } + } + return +} + // NewHTTPMetricExporter returns an OpenTelementry Protocol metric exporter that // report metrics to a HTTP collector. func NewHTTPMetricExporter(ctx context.Context, options ...otlpmetrichttp.Option) (exporter metric.Exporter, shutdown func(), err error) { diff --git a/example/weather/services/forecaster/cmd/forecaster/main.go b/example/weather/services/forecaster/cmd/forecaster/main.go index 354698ca..fd985fd1 100644 --- a/example/weather/services/forecaster/cmd/forecaster/main.go +++ b/example/weather/services/forecaster/cmd/forecaster/main.go @@ -17,6 +17,7 @@ import ( "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc" "go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace" "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" + "go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc" "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc" "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc" "goa.design/clue/clue" @@ -82,11 +83,25 @@ func main() { log.Errorf(ctx, err, "failed to shutdown metrics") } }() + logExporter, err := otlploggrpc.New(ctx, + otlploggrpc.WithEndpoint(*coladdr), + otlploggrpc.WithInsecure()) + if err != nil { + log.Errorf(ctx, err, "failed to initialize logging") + } + defer func() { + ctx := log.Context(context.Background()) + if err := logExporter.Shutdown(ctx); err != nil { + log.Errorf(ctx, err, "failed to shutdown logging") + } + }() + cfg, err := clue.NewConfig(ctx, genforecaster.ServiceName, genforecaster.APIVersion, metricExporter, spanExporter, + logExporter, ) if err != nil { log.Fatalf(ctx, err, "failed to initialize instrumentation") diff --git a/example/weather/services/front/cmd/front/main.go b/example/weather/services/front/cmd/front/main.go index 7f7f39c4..57f9e7ba 100644 --- a/example/weather/services/front/cmd/front/main.go +++ b/example/weather/services/front/cmd/front/main.go @@ -14,6 +14,7 @@ import ( "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc" "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" + "go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc" "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc" "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc" "goa.design/clue/clue" @@ -85,11 +86,25 @@ func main() { log.Errorf(ctx, err, "failed to shutdown metrics") } }() + logExporter, err := otlploggrpc.New(ctx, + otlploggrpc.WithEndpoint(*coladdr), + otlploggrpc.WithInsecure()) + if err != nil { + log.Errorf(ctx, err, "failed to initialize logging") + } + defer func() { + // Create new context in case the parent context has been canceled. + ctx := log.Context(context.Background(), log.WithFormat(format)) + if err := logExporter.Shutdown(ctx); err != nil { + log.Errorf(ctx, err, "failed to shutdown logging") + } + }() cfg, err := clue.NewConfig(ctx, genfront.ServiceName, genfront.APIVersion, metricExporter, spanExporter, + logExporter, ) if err != nil { log.Fatalf(ctx, err, "failed to initialize instrumentation") diff --git a/example/weather/services/locator/cmd/locator/main.go b/example/weather/services/locator/cmd/locator/main.go index a50e9cc2..7fb665c3 100644 --- a/example/weather/services/locator/cmd/locator/main.go +++ b/example/weather/services/locator/cmd/locator/main.go @@ -17,6 +17,7 @@ import ( "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc" "go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace" "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" + "go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc" "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc" "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc" "goa.design/clue/clue" @@ -84,11 +85,25 @@ func main() { log.Errorf(ctx, err, "failed to shutdown metrics") } }() + logExporter, err := otlploggrpc.New(ctx, + otlploggrpc.WithEndpoint(*oteladdr), + otlploggrpc.WithInsecure()) + if err != nil { + log.Errorf(ctx, err, "failed to initialize logging") + } + defer func() { + // Create new context in case the parent context has been canceled. + ctx := log.Context(context.Background(), log.WithFormat(format)) + if err := logExporter.Shutdown(ctx); err != nil { + log.Errorf(ctx, err, "failed to shutdown logging") + } + }() cfg, err := clue.NewConfig(ctx, genlocator.ServiceName, genlocator.APIVersion, metricExporter, spanExporter, + logExporter, ) if err != nil { log.Fatalf(ctx, err, "failed to initialize instrumentation") diff --git a/example/weather/services/tester/cmd/tester/main.go b/example/weather/services/tester/cmd/tester/main.go index 481ef411..84fbd4b1 100644 --- a/example/weather/services/tester/cmd/tester/main.go +++ b/example/weather/services/tester/cmd/tester/main.go @@ -14,6 +14,7 @@ import ( "time" "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc" + "go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc" "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc" "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc" "goa.design/clue/clue" @@ -86,11 +87,25 @@ func main() { log.Errorf(ctx, err, "failed to shutdown metrics") } }() + logExporter, err := otlploggrpc.New(ctx, + otlploggrpc.WithEndpoint(*oteladdr), + otlploggrpc.WithInsecure()) + if err != nil { + log.Errorf(ctx, err, "failed to initialize logging") + } + defer func() { + // Create new context in case the parent context has been canceled. + ctx := log.Context(context.Background(), log.WithFormat(format)) + if err := logExporter.Shutdown(ctx); err != nil { + log.Errorf(ctx, err, "failed to shutdown logging") + } + }() cfg, err := clue.NewConfig(ctx, gentester.ServiceName, gentester.APIVersion, metricExporter, spanExporter, + logExporter, ) if err != nil { log.Fatalf(ctx, err, "failed to initialize instrumentation") diff --git a/go.mod b/go.mod index 1536758a..60490cde 100644 --- a/go.mod +++ b/go.mod @@ -8,15 +8,20 @@ require ( github.com/stretchr/testify v1.11.0 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0 go.opentelemetry.io/otel v1.37.0 + go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.13.0 + go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.13.0 go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.37.0 go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.37.0 go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0 go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.37.0 + go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.13.0 go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.37.0 go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.37.0 + go.opentelemetry.io/otel/log v0.13.0 go.opentelemetry.io/otel/metric v1.37.0 go.opentelemetry.io/otel/sdk v1.37.0 + go.opentelemetry.io/otel/sdk/log v0.13.0 go.opentelemetry.io/otel/sdk/metric v1.37.0 go.opentelemetry.io/otel/trace v1.37.0 goa.design/goa/v3 v3.22.1 diff --git a/go.sum b/go.sum index 2f3a5cf2..23900c05 100644 --- a/go.sum +++ b/go.sum @@ -49,6 +49,10 @@ go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0 h1:Hf9xI/X go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0/go.mod h1:NfchwuyNoMcZ5MLHwPrODwUF1HWCXWrL31s8gSAdIKY= go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.13.0 h1:z6lNIajgEBVtQZHjfw2hAccPEBDs+nx58VemmXWa2ec= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.13.0/go.mod h1:+kyc3bRx/Qkq05P6OCu3mTEIOxYRYzoIg+JsUp5X+PM= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.13.0 h1:zUfYw8cscHHLwaY8Xz3fiJu+R59xBnkgq2Zr1lwmK/0= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.13.0/go.mod h1:514JLMCcFLQFS8cnTepOk6I09cKWJ5nGHBxHrMJ8Yfg= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.37.0 h1:zG8GlgXCJQd5BU98C0hZnBbElszTmUgCNCfYneaDL0A= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.37.0/go.mod h1:hOfBCz8kv/wuq73Mx2H2QnWokh/kHZxkh6SNF2bdKtw= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.37.0 h1:9PgnL3QNlj10uGxExowIDIZu66aVBwWhXmbOp1pa6RA= @@ -59,14 +63,22 @@ go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0 h1:EtFWS go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0/go.mod h1:QjUEoiGCPkvFZ/MjK6ZZfNOS6mfVEVKYE99dFhuN2LI= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.37.0 h1:bDMKF3RUSxshZ5OjOTi8rsHGaPKsAt76FaqgvIUySLc= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.37.0/go.mod h1:dDT67G/IkA46Mr2l9Uj7HsQVwsjASyV9SjGofsiUZDA= +go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.13.0 h1:yEX3aC9KDgvYPhuKECHbOlr5GLwH6KTjLJ1sBSkkxkc= +go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.13.0/go.mod h1:/GXR0tBmmkxDaCUGahvksvp66mx4yh5+cFXgSlhg0vQ= go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.37.0 h1:6VjV6Et+1Hd2iLZEPtdV7vie80Yyqf7oikJLjQ/myi0= go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.37.0/go.mod h1:u8hcp8ji5gaM/RfcOo8z9NMnf1pVLfVY7lBY2VOGuUU= go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.37.0 h1:SNhVp/9q4Go/XHBkQ1/d5u9P/U+L1yaGPoi0x+mStaI= go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.37.0/go.mod h1:tx8OOlGH6R4kLV67YaYO44GFXloEjGPZuMjEkaaqIp4= +go.opentelemetry.io/otel/log v0.13.0 h1:yoxRoIZcohB6Xf0lNv9QIyCzQvrtGZklVbdCoyb7dls= +go.opentelemetry.io/otel/log v0.13.0/go.mod h1:INKfG4k1O9CL25BaM1qLe0zIedOpvlS5Z7XgSbmN83E= go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE= go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E= go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI= go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg= +go.opentelemetry.io/otel/sdk/log v0.13.0 h1:I3CGUszjM926OphK8ZdzF+kLqFvfRY/IIoFq/TjwfaQ= +go.opentelemetry.io/otel/sdk/log v0.13.0/go.mod h1:lOrQyCCXmpZdN7NchXb6DOZZa1N5G1R2tm5GMMTpDBw= +go.opentelemetry.io/otel/sdk/log/logtest v0.13.0 h1:9yio6AFZ3QD9j9oqshV1Ibm9gPLlHNxurno5BreMtIA= +go.opentelemetry.io/otel/sdk/log/logtest v0.13.0/go.mod h1:QOGiAJHl+fob8Nu85ifXfuQYmJTFAvcrxL6w5/tu168= go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc= go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps= go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= diff --git a/go.work.sum b/go.work.sum index 0e78772b..347f03d8 100644 --- a/go.work.sum +++ b/go.work.sum @@ -492,12 +492,18 @@ cloud.google.com/go/workflows v1.13.1 h1:DkxrZ4HyXvjQLZWsYAUOV1w7d2a43XscM9dmkIG cloud.google.com/go/workflows v1.13.1/go.mod h1:xNdYtD6Sjoug+khNCAtBMK/rdh8qkjyL6aBas2XlkNc= cloud.google.com/go/workflows v1.13.3/go.mod h1:Xi7wggEt/ljoEcyk+CB/Oa1AHBCk0T1f5UH/exBB5CE= cloud.google.com/go/workflows v1.14.2/go.mod h1:5nqKjMD+MsJs41sJhdVrETgvD5cOK3hUcAs8ygqYvXQ= +codeberg.org/go-fonts/liberation v0.5.0/go.mod h1:zS/2e1354/mJ4pGzIIaEtm/59VFCFnYC7YV6YdGl5GU= +codeberg.org/go-latex/latex v0.1.0/go.mod h1:LA0q/AyWIYrqVd+A9Upkgsb+IqPcmSTKc9Dny04MHMw= +codeberg.org/go-pdf/fpdf v0.10.0/go.mod h1:Y0DGRAdZ0OmnZPvjbMp/1bYxmIPxm0ws4tfoPOc4LjU= +git.sr.ht/~sbinet/gg v0.6.0/go.mod h1:uucygbfC9wVPQIfrmwM2et0imr8L7KQWywX0xpFMm94= github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.24.2 h1:cZpsGsWTIFKymTA0je7IIvi1O7Es7apb9CF3EQlOcfE= github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.24.2/go.mod h1:itPGVDKf9cC/ov4MdvJ2QZ0khw4bfoo9jzwTJlaxy2k= github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.25.0/go.mod h1:obipzmGjfSjam60XLwGfqUkJsfiheAl+TUjG+4yzyPM= github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.26.0/go.mod h1:2bIszWvQRlJVmJLiuLhukLImRjKPcYdzzsx6darK02A= github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0/go.mod h1:yAZHSGnqScoU556rBOVkwLze6WP5N+U11RHuWaGVxwY= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0/go.mod h1:Cz6ft6Dkn3Et6l2v2a9/RpN7epQ1GtDlO6lj8bEcOvw= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b/go.mod h1:1KcenG0jGWcpt8ov532z81sp/kMMUG485J2InIOyADM= github.com/antihax/optional v1.0.0 h1:xK2lYat7ZLaVVcIuj82J8kIro4V6kDe0AUDFboUCwcg= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/aws/aws-lambda-go v1.47.0 h1:0H8s0vumYx/YKs4sE7YM0ktwL2eWse+kfopsRI1sXVI= @@ -505,6 +511,7 @@ github.com/aws/aws-lambda-go v1.47.0/go.mod h1:dpMpZgvWx5vuQJfBt0zqBha60q7Dd7Rfg github.com/aws/aws-lambda-go v1.49.0/go.mod h1:dpMpZgvWx5vuQJfBt0zqBha60q7Dd7RfgJv23DymV8A= github.com/bazelbuild/rules_go v0.49.0 h1:5vCbuvy8Q11g41lseGJDc5vxhDjJtfxr6nM/IC4VmqM= github.com/bazelbuild/rules_go v0.49.0/go.mod h1:Dhcz716Kqg1RHNWos+N6MlXNkjNP2EwZQ0LukRKJfMs= +github.com/campoy/embedmd v1.0.0/go.mod h1:oxyr9RCiSXg0M3VJ3ks0UGfp98BpSSGr0kpiX3MzVl8= github.com/census-instrumentation/opencensus-proto v0.4.1 h1:iKLQ0xPNFxR/2hzXZMrBo8f1j86j5WHzznCCQxV/b8g= github.com/census-instrumentation/opencensus-proto v0.4.1/go.mod h1:4T9NM4+4Vw91VeyqjLS6ao50K5bOcLKN6Q42XnYaRYw= github.com/cncf/xds/go v0.0.0-20240723142845-024c85f92f20/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= @@ -517,6 +524,8 @@ github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443/go.mod h1:W+zGtBO5Y1Ig github.com/creack/pty v1.1.9 h1:uDmaGzcdjhF4i/plgjmEsriH11Y0o7RKapEf/LDaM3w= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/envoyproxy/go-control-plane v0.13.0/go.mod h1:GRaKG3dwvFoTg4nj7aXdZnvMg4d7nvT/wl9WgVXn3Q8= github.com/envoyproxy/go-control-plane v0.13.1 h1:vPfJZCkob6yTMEgS+0TwfTUfbHjfy/6vOJ8hUWX/uXE= github.com/envoyproxy/go-control-plane v0.13.1/go.mod h1:X45hY0mufo6Fd0KW3rqsGvQMw58jvjymeCzBU3mWyHw= @@ -538,6 +547,7 @@ github.com/getkin/kin-openapi v0.132.0/go.mod h1:3OlG51PCYNsPByuiMB0t4fjnNlIDnaE github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= github.com/go-jose/go-jose/v4 v4.0.4/go.mod h1:NKb5HO1EZccyMpiZNbdUw/14tiXNyUJh188dfnMCAfc= github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA= +github.com/go-jose/go-jose/v4 v4.1.1/go.mod h1:BdsZGqgdO3b6tTc6LSE56wcDbMMLuPsw5d4ZD5f94kA= github.com/go-kit/kit v0.13.0 h1:OoneCcHKHQ03LfBpoQCUfCluwd2Vt3ohz+kvbJneZAU= github.com/go-kit/kit v0.13.0/go.mod h1:phqEHMMUbyrCFCTgH48JueqrM3md2HcAZ8N3XE4FKDg= github.com/go-kit/log v0.2.1 h1:MRVx0/zhvdseW+Gza6N9rVzU/IVzaeE1SFI4raAhmBU= @@ -548,7 +558,9 @@ github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+Gr github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM= github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= +github.com/goccmack/gocc v0.0.0-20230228185258-2292f9e40198/go.mod h1:DTh/Y2+NbnOVVoypCCQrovMPDKUGp4yZpSbWg5D0XIM= github.com/gohugoio/hashstructure v0.3.0/go.mod h1:8ohPTAfQLTs2WdzB6k9etmQYclDUeNsIHGPAFejbsEA= +github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= github.com/golang/glog v1.2.2 h1:1+mZ9upx1Dh6FmUTFR1naJ77miKiXgALjWOZ3NVFPmY= github.com/golang/glog v1.2.2/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= github.com/golang/glog v1.2.3/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= @@ -594,6 +606,8 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_model v0.6.0 h1:k1v3CzpSRUTrKMppY35TLwPvxHqBu0bYgxZzqGIgaos= github.com/prometheus/client_model v0.6.0/go.mod h1:NTQHnmxFpouOD0DpvP4XujX3CdOAGQPoaGhyTchlyt8= github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= @@ -630,6 +644,7 @@ go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zL go.opentelemetry.io/otel/sdk v1.32.0/go.mod h1:LqgegDBjKMmb2GC6/PrTnteJG39I8/vJCAP9LlJXEjU= go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg= go.opentelemetry.io/otel/sdk v1.36.0/go.mod h1:+lC+mTgD+MUWfjJubi2vvXWcVxyr9rmlshZni72pXeY= +go.opentelemetry.io/otel/sdk/log/logtest v0.13.0/go.mod h1:QOGiAJHl+fob8Nu85ifXfuQYmJTFAvcrxL6w5/tu168= go.opentelemetry.io/otel/sdk/metric v1.32.0/go.mod h1:PWeZlq0zt9YkYAp3gjKZ0eicRYvOh1Gd+X99x6GHpCQ= go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w= go.opentelemetry.io/otel/sdk/metric v1.36.0/go.mod h1:qTNOhFDfKRwX0yXOqJYegL5WRaW376QbB7P4Pb0qva4= @@ -653,6 +668,8 @@ golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbV golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U= golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY= +golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= +golang.org/x/image v0.25.0/go.mod h1:tCAmOEGthTtkalusGp1g3xa2gke8J6c2N565dTyl9Rs= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= @@ -695,6 +712,7 @@ golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/telemetry v0.0.0-20240521205824-bda55230c457 h1:zf5N6UOrA487eEFacMePxjXAJctxKmyjKUsjA11Uzuk= golang.org/x/telemetry v0.0.0-20240521205824-bda55230c457/go.mod h1:pRgIJT+bRLFKnoM1ldnzKoxTIn14Yxz928LQRYYgIN0= golang.org/x/telemetry v0.0.0-20250710130107-8d8967aff50b/go.mod h1:4ZwOYna0/zsOKwuR5X/m0QFOJpSZvAxFfkQT+Erd9D4= +golang.org/x/telemetry v0.0.0-20250807160809-1a19826ec488/go.mod h1:fGb/2+tgXXjhjHsTNdVEEMZNWA0quBnfrO+AfoDSAKw= golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek= golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= @@ -714,6 +732,7 @@ golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gonum.org/v1/plot v0.15.2/go.mod h1:DX+x+DWso3LTha+AdkJEv5Txvi+Tql3KAGkehP0/Ubg= google.golang.org/genproto v0.0.0-20220822174746-9e6da59bd2fc h1:Nf+EdcTLHR8qDNN/KfkQL0u0ssxt9OhbaWCl5C0ucEI= google.golang.org/genproto v0.0.0-20220822174746-9e6da59bd2fc/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142/go.mod h1:d6be+8HhtEtucleCbxpPW9PA9XwISACu8nvpPqF0BVo= @@ -728,6 +747,7 @@ google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb/go. google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463/go.mod h1:U90ffi8eUL9MwPcrJylN5+Mk2v3vuPDptd5yyNUiRR8= google.golang.org/genproto/googleapis/api v0.0.0-20250428153025-10db94c68c34/go.mod h1:0awUlEkap+Pb1UMeJwJQQAdJQrt3moU7J2moTy69irI= google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a/go.mod h1:a77HrdMjoeKbnd2jmgcWdaS++ZLZAEq3orIOAEIKiVw= +google.golang.org/genproto/googleapis/api v0.0.0-20250707201910-8d1bb00bc6a7/go.mod h1:kXqgZtrWaf6qS3jZOCnCH7WYfrvFjkC51bM8fz3RsCA= google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= google.golang.org/genproto/googleapis/rpc v0.0.0-20240930140551-af27646dc61f/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= @@ -746,6 +766,7 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463/go. google.golang.org/genproto/googleapis/rpc v0.0.0-20250428153025-10db94c68c34/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= google.golang.org/genproto/googleapis/rpc v0.0.0-20250512202823-5a2f75b736a9/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= google.golang.org/genproto/googleapis/rpc v0.0.0-20250715232539-7130f93afb79/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= google.golang.org/grpc v1.67.0/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= @@ -765,6 +786,8 @@ google.golang.org/protobuf v1.36.0/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojt google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= google.golang.org/protobuf v1.36.3/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= google.golang.org/protobuf v1.36.4/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/protobuf v1.36.7/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/interceptors/trace_stream_test.go b/interceptors/trace_stream_test.go index 7815d557..2585f075 100644 --- a/interceptors/trace_stream_test.go +++ b/interceptors/trace_stream_test.go @@ -6,6 +6,7 @@ import ( "github.com/stretchr/testify/assert" "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/exporters/stdout/stdoutlog" "go.opentelemetry.io/otel/exporters/stdout/stdoutmetric" "go.opentelemetry.io/otel/exporters/stdout/stdouttrace" "go.opentelemetry.io/otel/trace" @@ -74,7 +75,11 @@ func init() { if err != nil { panic(err) } - cfg, err := clue.NewConfig(ctx, "test", "0.0.1", metricExporter, traceExporter) + logExporter, err := stdoutlog.New() + if err != nil { + panic(err) + } + cfg, err := clue.NewConfig(ctx, "test", "0.0.1", metricExporter, traceExporter, logExporter) if err != nil { panic(err) } diff --git a/log/context.go b/log/context.go index d000ec33..69100dc5 100644 --- a/log/context.go +++ b/log/context.go @@ -17,6 +17,10 @@ func Context(ctx context.Context, opts ...LogOption) context.Context { for _, opt := range opts { opt(l.options) } + // Copy OTEL logger from options to logger struct + l.otellog = l.options.otellog + // Reset the OTEL logging guard for new loggers + l.otelLogging = false if l.options.disableBuffering != nil && l.options.disableBuffering(ctx) { l.flush() } diff --git a/log/log.go b/log/log.go index bd7cd797..2657ef72 100644 --- a/log/log.go +++ b/log/log.go @@ -3,12 +3,16 @@ package log import ( "bytes" "context" + "encoding/json" "errors" "fmt" "io" "os" + "strings" "sync" "time" + + otellog "go.opentelemetry.io/otel/log" ) type ( @@ -21,11 +25,13 @@ type ( // Logger implementation logger struct { - options *options - lock sync.Mutex - keyvals kvList - entries []*Entry - flushed bool + options *options + lock sync.Mutex + keyvals kvList + entries []*Entry + flushed bool + otellog otellog.Logger // OpenTelemetry logger + otelLogging bool // Guard against recursive OTEL logging } // Log severity enum @@ -151,6 +157,7 @@ func With(ctx context.Context, keyvals ...Fielder) context.Context { entries: l.entries, keyvals: l.keyvals.merge(keyvals), flushed: l.flushed, + otellog: l.otellog, // Copy OTEL logger } if l.options.disableBuffering != nil && l.options.disableBuffering(ctx) { l.flush() @@ -176,8 +183,152 @@ func FlushAndDisableBuffering(ctx context.Context) { l.flush() } +// writeEntry writes the log entry to both the local writer and OpenTelemetry func (l *logger) writeEntry(e *Entry) { + // Write to local writer l.options.w.Write(l.options.format(e)) // nolint: errcheck + + // Send to OpenTelemetry if available and not already logging to OTEL + if l.otellog != nil && !l.otelLogging { + // Set recursion guard + l.otelLogging = true + defer func() { l.otelLogging = false }() + + // Convert severity to OTEL level + var level otellog.Severity + switch e.Severity { + case SeverityDebug: + level = otellog.SeverityDebug + case SeverityInfo: + level = otellog.SeverityInfo + case SeverityWarn: + level = otellog.SeverityWarn + case SeverityError: + level = otellog.SeverityError + default: + level = otellog.SeverityInfo + } + + // Create OTEL record + record := otellog.Record{} + record.SetSeverity(level) + record.SetTimestamp(e.Time) + + // Extract message and build structured body + var message string + var attributes []otellog.KeyValue + var bodyParts []string + + for _, kv := range e.KeyVals { + if kv.K == MessageKey || kv.K == ErrorMessageKey { + message = fmt.Sprintf("%v", kv.V) + } else { + // Add to attributes with proper type handling + switch v := kv.V.(type) { + case string: + attributes = append(attributes, otellog.String(kv.K, v)) + bodyParts = append(bodyParts, fmt.Sprintf("%s=%q", kv.K, v)) + case int: + attributes = append(attributes, otellog.Int64(kv.K, int64(v))) + bodyParts = append(bodyParts, fmt.Sprintf("%s=%d", kv.K, v)) + case int8: + attributes = append(attributes, otellog.Int64(kv.K, int64(v))) + bodyParts = append(bodyParts, fmt.Sprintf("%s=%d", kv.K, v)) + case int16: + attributes = append(attributes, otellog.Int64(kv.K, int64(v))) + bodyParts = append(bodyParts, fmt.Sprintf("%s=%d", kv.K, v)) + case int32: + attributes = append(attributes, otellog.Int64(kv.K, int64(v))) + bodyParts = append(bodyParts, fmt.Sprintf("%s=%d", kv.K, v)) + case int64: + attributes = append(attributes, otellog.Int64(kv.K, v)) + bodyParts = append(bodyParts, fmt.Sprintf("%s=%d", kv.K, v)) + case uint: + attributes = append(attributes, otellog.Int64(kv.K, int64(v))) + bodyParts = append(bodyParts, fmt.Sprintf("%s=%d", kv.K, v)) + case uint8: + attributes = append(attributes, otellog.Int64(kv.K, int64(v))) + bodyParts = append(bodyParts, fmt.Sprintf("%s=%d", kv.K, v)) + case uint16: + attributes = append(attributes, otellog.Int64(kv.K, int64(v))) + bodyParts = append(bodyParts, fmt.Sprintf("%s=%d", kv.K, v)) + case uint32: + attributes = append(attributes, otellog.Int64(kv.K, int64(v))) + bodyParts = append(bodyParts, fmt.Sprintf("%s=%d", kv.K, v)) + case uint64: + attributes = append(attributes, otellog.Int64(kv.K, int64(v))) + bodyParts = append(bodyParts, fmt.Sprintf("%s=%d", kv.K, v)) + case float32: + attributes = append(attributes, otellog.Float64(kv.K, float64(v))) + bodyParts = append(bodyParts, fmt.Sprintf("%s=%g", kv.K, v)) + case float64: + attributes = append(attributes, otellog.Float64(kv.K, v)) + bodyParts = append(bodyParts, fmt.Sprintf("%s=%g", kv.K, v)) + case bool: + attributes = append(attributes, otellog.Bool(kv.K, v)) + bodyParts = append(bodyParts, fmt.Sprintf("%s=%t", kv.K, v)) + default: + // Check for Stringer interface + if stringer, ok := v.(fmt.Stringer); ok { + attributes = append(attributes, otellog.String(kv.K, stringer.String())) + bodyParts = append(bodyParts, fmt.Sprintf("%s=%q", kv.K, stringer.String())) + } else if jsonMarshaler, ok := v.(json.Marshaler); ok { + // Check for MarshalJSON interface + if data, err := jsonMarshaler.MarshalJSON(); err == nil { + // Remove quotes from JSON string if it's a string + jsonStr := string(data) + if len(jsonStr) >= 2 && jsonStr[0] == '"' && jsonStr[len(jsonStr)-1] == '"' { + jsonStr = jsonStr[1 : len(jsonStr)-1] + } + attributes = append(attributes, otellog.String(kv.K, jsonStr)) + bodyParts = append(bodyParts, fmt.Sprintf("%s=%q", kv.K, jsonStr)) + } else { + // Fallback to string representation if MarshalJSON fails + attributes = append(attributes, otellog.String(kv.K, fmt.Sprintf("%v", v))) + bodyParts = append(bodyParts, fmt.Sprintf("%s=%q", kv.K, v)) + } + } else { + // For any other type, convert to string + attributes = append(attributes, otellog.String(kv.K, fmt.Sprintf("%v", v))) + bodyParts = append(bodyParts, fmt.Sprintf("%s=%q", kv.K, v)) + } + } + } + } + + // Build the structured body: "message key1=value1 key2=value2" + var body string + if message != "" { + body = message + } + if len(bodyParts) > 0 { + if body != "" { + body += " " + } + body += strings.Join(bodyParts, " ") + } + + // Set the body to the structured format + record.SetBody(otellog.StringValue(body)) + + // Add all keyvals as attributes for proper OTEL structure + for _, attr := range attributes { + record.AddAttributes(attr) + } + + // Log to OTEL - use a background context to avoid recursive logging + // This prevents the OTEL logger from calling back into the clue logger + otelCtx := context.Background() + + // Add timeout to prevent hanging + ctx, cancel := context.WithTimeout(otelCtx, 5*time.Second) + defer cancel() + + // Use a goroutine to prevent blocking + go func() { + l.otellog.Emit(ctx, record) + }() + } } func (l *logger) flush() { diff --git a/log/options.go b/log/options.go index 3031df76..6b5f722d 100644 --- a/log/options.go +++ b/log/options.go @@ -9,6 +9,7 @@ import ( "golang.org/x/term" + otellog "go.opentelemetry.io/otel/log" "go.opentelemetry.io/otel/trace" ) @@ -31,6 +32,7 @@ type ( keyvals kvList kvfuncs []func(context.Context) []KV maxsize int + otellog otellog.Logger // OpenTelemetry logger } ) @@ -117,6 +119,16 @@ func WithFunc(fn func(context.Context) []KV) LogOption { } } +// WithOTELLogger sets the OpenTelemetry logger to send logs to OTLP. +func WithOTELLogger(otelLogger otellog.Logger) LogOption { + return func(o *options) { + // Safety check: don't set the OTEL logger if it's nil to avoid issues + if otelLogger != nil { + o.otellog = otelLogger + } + } +} + // IsTerminal returns true if the process is running in a terminal. func IsTerminal() bool { return term.IsTerminal(int(os.Stdout.Fd())) From 9379f30dcfd5796e9fca033fadc134048058b155 Mon Sep 17 00:00:00 2001 From: Brighten Tompkins Date: Fri, 29 Aug 2025 04:56:54 -0700 Subject: [PATCH 2/7] docs: update documentation --- README.md | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 6c472e86..0f67cfb7 100644 --- a/README.md +++ b/README.md @@ -180,11 +180,22 @@ metricExporter, err := otlpmetricgrpc.New( otlpmetricgrpc.WithTLSCredentials(insecure.NewCredentials())) ``` +And configuring an OTLP compliant logs exporters can be done as follows: + +```go +import "go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc" +// ... +metricExporter, err := otlploggrpc.New( + context.Background(), + otlploggrpc.WithEndpoint("localhost:4317"), + otlploggrpc.WithTLSCredentials(insecure.NewCredentials())) +``` + These exporters can then be used to configure Clue: ```go // Configure OpenTelemetry. -cfg := clue.NewConfig(ctx, "service", "1.0.0", metricExporter, spanExporter) +cfg := clue.NewConfig(ctx, "service", "1.0.0", metricExporter, spanExporter, logExporter) clue.ConfigureOpenTelemetry(ctx, cfg) ``` @@ -330,7 +341,8 @@ v1.x: ctx := log.Context(context.Background()) traceExporter := tracestdout.New() metricsExporter := metricstdout.New() -cfg := clue.NewConfig(ctx, "service", "1.0.0", metricsExporter, traceExporter) +logsExporter = logsstdout.New() +cfg := clue.NewConfig(ctx, "service", "1.0.0", metricsExporter, traceExporter, logsExplorer) clue.ConfigureOpenTelemetry(ctx, cfg) ``` From 1ffd93967683481ca989c437b4bccaf6bf5d1fc9 Mon Sep 17 00:00:00 2001 From: Brighten Tompkins Date: Fri, 29 Aug 2025 05:39:13 -0700 Subject: [PATCH 3/7] fix: remove adding in otel logger --- clue/config.go | 5 ----- log/options.go | 10 ---------- 2 files changed, 15 deletions(-) diff --git a/clue/config.go b/clue/config.go index 494bbc81..6818ec2a 100644 --- a/clue/config.go +++ b/clue/config.go @@ -51,11 +51,6 @@ func ConfigureOpenTelemetry(ctx context.Context, cfg *Config) { otel.SetTextMapPropagator(cfg.Propagators) otel.SetLogger(logr.New(log.ToLogrSink(ctx))) otel.SetErrorHandler(cfg.ErrorHandler) - - if cfg.LoggerProvider != nil { - otelLogger := cfg.LoggerProvider.Logger("clue") - ctx = log.Context(ctx, log.WithOTELLogger(otelLogger)) - } } // NewConfig creates a new Config object adequate for use by diff --git a/log/options.go b/log/options.go index 6b5f722d..11dce91a 100644 --- a/log/options.go +++ b/log/options.go @@ -119,16 +119,6 @@ func WithFunc(fn func(context.Context) []KV) LogOption { } } -// WithOTELLogger sets the OpenTelemetry logger to send logs to OTLP. -func WithOTELLogger(otelLogger otellog.Logger) LogOption { - return func(o *options) { - // Safety check: don't set the OTEL logger if it's nil to avoid issues - if otelLogger != nil { - o.otellog = otelLogger - } - } -} - // IsTerminal returns true if the process is running in a terminal. func IsTerminal() bool { return term.IsTerminal(int(os.Stdout.Fd())) From b1ba599857df97ccf1ba229c8253698f000a3cbb Mon Sep 17 00:00:00 2001 From: Brighten Tompkins Date: Fri, 29 Aug 2025 05:39:34 -0700 Subject: [PATCH 4/7] fix: provision lognoop for empty exporter --- clue/config.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/clue/config.go b/clue/config.go index 6818ec2a..037ff57d 100644 --- a/clue/config.go +++ b/clue/config.go @@ -17,6 +17,7 @@ import ( semconv "go.opentelemetry.io/otel/semconv/v1.34.0" "go.opentelemetry.io/otel/trace" tracenoop "go.opentelemetry.io/otel/trace/noop" + lognoop "go.opentelemetry.io/otel/log/noop" "goa.design/clue/log" @@ -138,7 +139,7 @@ func NewConfig( } var loggerProvider otellog.LoggerProvider if logExporter == nil { - loggerProvider = global.GetLoggerProvider() + loggerProvider = lognoop.NewLoggerProvider() } else { loggerProvider = sdklog.NewLoggerProvider( sdklog.WithResource(res), From 8382273297906c377e847ca8d9979c9570673a3c Mon Sep 17 00:00:00 2001 From: Brighten Tompkins Date: Fri, 29 Aug 2025 05:40:16 -0700 Subject: [PATCH 5/7] fix: add logger to config test --- clue/config_test.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/clue/config_test.go b/clue/config_test.go index b3669c72..ad85e90f 100644 --- a/clue/config_test.go +++ b/clue/config_test.go @@ -11,6 +11,9 @@ import ( "go.opentelemetry.io/otel/exporters/stdout/stdoutlog" "go.opentelemetry.io/otel/exporters/stdout/stdoutmetric" "go.opentelemetry.io/otel/exporters/stdout/stdouttrace" + otellog "go.opentelemetry.io/otel/log" + "go.opentelemetry.io/otel/log/global" + lognoop "go.opentelemetry.io/otel/log/noop" "go.opentelemetry.io/otel/metric" metricnoop "go.opentelemetry.io/otel/metric/noop" "go.opentelemetry.io/otel/propagation" @@ -31,17 +34,20 @@ func TestConfigureOpenTelemetry(t *testing.T) { ctx := log.Context(context.Background()) noopMeterProvider := metricnoop.NewMeterProvider() noopTracerProvider := tracenoop.NewTracerProvider() + noopLoggerProvider := lognoop.NewLoggerProvider() noopErrorHandler := dummyErrorHandler{} cases := []struct { name string meterProvider metric.MeterProvider tracerProvider trace.TracerProvider + loggerProvider otellog.LoggerProvider propagators propagation.TextMapPropagator errorHandler otel.ErrorHandler wantMeterProvider metric.MeterProvider wantTracerProvider trace.TracerProvider + wantLoggerProvider otellog.LoggerProvider wantPropagators propagation.TextMapPropagator wantErrorHandler bool }{ @@ -55,6 +61,10 @@ func TestConfigureOpenTelemetry(t *testing.T) { name: "tracer provider", tracerProvider: noopTracerProvider, wantTracerProvider: noopTracerProvider, + }, { + name: "logger provider", + loggerProvider: noopLoggerProvider, + wantLoggerProvider: noopLoggerProvider, }, { name: "propagators", propagators: propagation.Baggage{}, @@ -70,12 +80,14 @@ func TestConfigureOpenTelemetry(t *testing.T) { cfg := &Config{ MeterProvider: c.meterProvider, TracerProvider: c.tracerProvider, + LoggerProvider: c.loggerProvider, Propagators: c.propagators, ErrorHandler: c.errorHandler, } ConfigureOpenTelemetry(ctx, cfg) assert.Equal(t, c.wantMeterProvider, otel.GetMeterProvider()) assert.Equal(t, c.wantTracerProvider, otel.GetTracerProvider()) + assert.Equal(t, c.wantLoggerProvider, global.GetLoggerProvider()) assert.Equal(t, c.wantPropagators, otel.GetTextMapPropagator()) }) } From 6dd7359091ace99a82d228a3b10af3613ca88fd6 Mon Sep 17 00:00:00 2001 From: Brighten Tompkins Date: Fri, 29 Aug 2025 05:40:32 -0700 Subject: [PATCH 6/7] fix: remove overprotective recursion guard --- log/log.go | 247 ++++++++++++++++++++++++++--------------------------- 1 file changed, 122 insertions(+), 125 deletions(-) diff --git a/log/log.go b/log/log.go index 2657ef72..39597f66 100644 --- a/log/log.go +++ b/log/log.go @@ -188,147 +188,144 @@ func (l *logger) writeEntry(e *Entry) { // Write to local writer l.options.w.Write(l.options.format(e)) // nolint: errcheck - // Send to OpenTelemetry if available and not already logging to OTEL - if l.otellog != nil && !l.otelLogging { - // Set recursion guard - l.otelLogging = true - defer func() { l.otelLogging = false }() - - // Convert severity to OTEL level - var level otellog.Severity - switch e.Severity { - case SeverityDebug: - level = otellog.SeverityDebug - case SeverityInfo: - level = otellog.SeverityInfo - case SeverityWarn: - level = otellog.SeverityWarn - case SeverityError: - level = otellog.SeverityError - default: - level = otellog.SeverityInfo - } + if l.otellog == nil { + return + } - // Create OTEL record - record := otellog.Record{} - record.SetSeverity(level) - record.SetTimestamp(e.Time) - - // Extract message and build structured body - var message string - var attributes []otellog.KeyValue - var bodyParts []string - - for _, kv := range e.KeyVals { - if kv.K == MessageKey || kv.K == ErrorMessageKey { - message = fmt.Sprintf("%v", kv.V) - } else { - // Add to attributes with proper type handling - switch v := kv.V.(type) { - case string: - attributes = append(attributes, otellog.String(kv.K, v)) - bodyParts = append(bodyParts, fmt.Sprintf("%s=%q", kv.K, v)) - case int: - attributes = append(attributes, otellog.Int64(kv.K, int64(v))) - bodyParts = append(bodyParts, fmt.Sprintf("%s=%d", kv.K, v)) - case int8: - attributes = append(attributes, otellog.Int64(kv.K, int64(v))) - bodyParts = append(bodyParts, fmt.Sprintf("%s=%d", kv.K, v)) - case int16: - attributes = append(attributes, otellog.Int64(kv.K, int64(v))) - bodyParts = append(bodyParts, fmt.Sprintf("%s=%d", kv.K, v)) - case int32: - attributes = append(attributes, otellog.Int64(kv.K, int64(v))) - bodyParts = append(bodyParts, fmt.Sprintf("%s=%d", kv.K, v)) - case int64: - attributes = append(attributes, otellog.Int64(kv.K, v)) - bodyParts = append(bodyParts, fmt.Sprintf("%s=%d", kv.K, v)) - case uint: - attributes = append(attributes, otellog.Int64(kv.K, int64(v))) - bodyParts = append(bodyParts, fmt.Sprintf("%s=%d", kv.K, v)) - case uint8: - attributes = append(attributes, otellog.Int64(kv.K, int64(v))) - bodyParts = append(bodyParts, fmt.Sprintf("%s=%d", kv.K, v)) - case uint16: - attributes = append(attributes, otellog.Int64(kv.K, int64(v))) - bodyParts = append(bodyParts, fmt.Sprintf("%s=%d", kv.K, v)) - case uint32: - attributes = append(attributes, otellog.Int64(kv.K, int64(v))) - bodyParts = append(bodyParts, fmt.Sprintf("%s=%d", kv.K, v)) - case uint64: - attributes = append(attributes, otellog.Int64(kv.K, int64(v))) - bodyParts = append(bodyParts, fmt.Sprintf("%s=%d", kv.K, v)) - case float32: - attributes = append(attributes, otellog.Float64(kv.K, float64(v))) - bodyParts = append(bodyParts, fmt.Sprintf("%s=%g", kv.K, v)) - case float64: - attributes = append(attributes, otellog.Float64(kv.K, v)) - bodyParts = append(bodyParts, fmt.Sprintf("%s=%g", kv.K, v)) - case bool: - attributes = append(attributes, otellog.Bool(kv.K, v)) - bodyParts = append(bodyParts, fmt.Sprintf("%s=%t", kv.K, v)) - default: - // Check for Stringer interface - if stringer, ok := v.(fmt.Stringer); ok { - attributes = append(attributes, otellog.String(kv.K, stringer.String())) - bodyParts = append(bodyParts, fmt.Sprintf("%s=%q", kv.K, stringer.String())) - } else if jsonMarshaler, ok := v.(json.Marshaler); ok { - // Check for MarshalJSON interface - if data, err := jsonMarshaler.MarshalJSON(); err == nil { - // Remove quotes from JSON string if it's a string - jsonStr := string(data) - if len(jsonStr) >= 2 && jsonStr[0] == '"' && jsonStr[len(jsonStr)-1] == '"' { - jsonStr = jsonStr[1 : len(jsonStr)-1] - } - attributes = append(attributes, otellog.String(kv.K, jsonStr)) - bodyParts = append(bodyParts, fmt.Sprintf("%s=%q", kv.K, jsonStr)) - } else { - // Fallback to string representation if MarshalJSON fails - attributes = append(attributes, otellog.String(kv.K, fmt.Sprintf("%v", v))) - bodyParts = append(bodyParts, fmt.Sprintf("%s=%q", kv.K, v)) + // Convert severity to OTEL level + var level otellog.Severity + switch e.Severity { + case SeverityDebug: + level = otellog.SeverityDebug + case SeverityInfo: + level = otellog.SeverityInfo + case SeverityWarn: + level = otellog.SeverityWarn + case SeverityError: + level = otellog.SeverityError + default: + level = otellog.SeverityInfo + } + + // Create OTEL record + record := otellog.Record{} + record.SetSeverity(level) + record.SetTimestamp(e.Time) + + // Extract message and build structured body + var message string + var attributes []otellog.KeyValue + var bodyParts []string + + for _, kv := range e.KeyVals { + if kv.K == MessageKey || kv.K == ErrorMessageKey { + message = fmt.Sprintf("%v", kv.V) + } else { + // Add to attributes with proper type handling + switch v := kv.V.(type) { + case string: + attributes = append(attributes, otellog.String(kv.K, v)) + bodyParts = append(bodyParts, fmt.Sprintf("%s=%q", kv.K, v)) + case int: + attributes = append(attributes, otellog.Int64(kv.K, int64(v))) + bodyParts = append(bodyParts, fmt.Sprintf("%s=%d", kv.K, v)) + case int8: + attributes = append(attributes, otellog.Int64(kv.K, int64(v))) + bodyParts = append(bodyParts, fmt.Sprintf("%s=%d", kv.K, v)) + case int16: + attributes = append(attributes, otellog.Int64(kv.K, int64(v))) + bodyParts = append(bodyParts, fmt.Sprintf("%s=%d", kv.K, v)) + case int32: + attributes = append(attributes, otellog.Int64(kv.K, int64(v))) + bodyParts = append(bodyParts, fmt.Sprintf("%s=%d", kv.K, v)) + case int64: + attributes = append(attributes, otellog.Int64(kv.K, v)) + bodyParts = append(bodyParts, fmt.Sprintf("%s=%d", kv.K, v)) + case uint: + attributes = append(attributes, otellog.Int64(kv.K, int64(v))) + bodyParts = append(bodyParts, fmt.Sprintf("%s=%d", kv.K, v)) + case uint8: + attributes = append(attributes, otellog.Int64(kv.K, int64(v))) + bodyParts = append(bodyParts, fmt.Sprintf("%s=%d", kv.K, v)) + case uint16: + attributes = append(attributes, otellog.Int64(kv.K, int64(v))) + bodyParts = append(bodyParts, fmt.Sprintf("%s=%d", kv.K, v)) + case uint32: + attributes = append(attributes, otellog.Int64(kv.K, int64(v))) + bodyParts = append(bodyParts, fmt.Sprintf("%s=%d", kv.K, v)) + case uint64: + attributes = append(attributes, otellog.Int64(kv.K, int64(v))) + bodyParts = append(bodyParts, fmt.Sprintf("%s=%d", kv.K, v)) + case float32: + attributes = append(attributes, otellog.Float64(kv.K, float64(v))) + bodyParts = append(bodyParts, fmt.Sprintf("%s=%g", kv.K, v)) + case float64: + attributes = append(attributes, otellog.Float64(kv.K, v)) + bodyParts = append(bodyParts, fmt.Sprintf("%s=%g", kv.K, v)) + case bool: + attributes = append(attributes, otellog.Bool(kv.K, v)) + bodyParts = append(bodyParts, fmt.Sprintf("%s=%t", kv.K, v)) + default: + // Check for Stringer interface + if stringer, ok := v.(fmt.Stringer); ok { + attributes = append(attributes, otellog.String(kv.K, stringer.String())) + bodyParts = append(bodyParts, fmt.Sprintf("%s=%q", kv.K, stringer.String())) + } else if jsonMarshaler, ok := v.(json.Marshaler); ok { + // Check for MarshalJSON interface + if data, err := jsonMarshaler.MarshalJSON(); err == nil { + // Remove quotes from JSON string if it's a string + jsonStr := string(data) + if len(jsonStr) >= 2 && jsonStr[0] == '"' && jsonStr[len(jsonStr)-1] == '"' { + jsonStr = jsonStr[1 : len(jsonStr)-1] } + attributes = append(attributes, otellog.String(kv.K, jsonStr)) + bodyParts = append(bodyParts, fmt.Sprintf("%s=%q", kv.K, jsonStr)) } else { - // For any other type, convert to string + // Fallback to string representation if MarshalJSON fails attributes = append(attributes, otellog.String(kv.K, fmt.Sprintf("%v", v))) bodyParts = append(bodyParts, fmt.Sprintf("%s=%q", kv.K, v)) } + } else { + // For any other type, convert to string + attributes = append(attributes, otellog.String(kv.K, fmt.Sprintf("%v", v))) + bodyParts = append(bodyParts, fmt.Sprintf("%s=%q", kv.K, v)) } } } + } - // Build the structured body: "message key1=value1 key2=value2" - var body string - if message != "" { - body = message - } - if len(bodyParts) > 0 { - if body != "" { - body += " " - } - body += strings.Join(bodyParts, " ") + // Build the structured body: "message key1=value1 key2=value2" + var body string + if message != "" { + body = message + } + if len(bodyParts) > 0 { + if body != "" { + body += " " } + body += strings.Join(bodyParts, " ") + } - // Set the body to the structured format - record.SetBody(otellog.StringValue(body)) + // Set the body to the structured format + record.SetBody(otellog.StringValue(body)) - // Add all keyvals as attributes for proper OTEL structure - for _, attr := range attributes { - record.AddAttributes(attr) - } + // Add all keyvals as attributes for proper OTEL structure + for _, attr := range attributes { + record.AddAttributes(attr) + } - // Log to OTEL - use a background context to avoid recursive logging - // This prevents the OTEL logger from calling back into the clue logger - otelCtx := context.Background() + // Log to OTEL - use a background context to avoid recursive logging + // This prevents the OTEL logger from calling back into the clue logger + otelCtx := context.Background() - // Add timeout to prevent hanging - ctx, cancel := context.WithTimeout(otelCtx, 5*time.Second) - defer cancel() + // Add timeout to prevent hanging + ctx, cancel := context.WithTimeout(otelCtx, 5*time.Second) + defer cancel() - // Use a goroutine to prevent blocking - go func() { - l.otellog.Emit(ctx, record) - }() - } + // Use a goroutine to prevent blocking + go func() { + l.otellog.Emit(ctx, record) + }() } func (l *logger) flush() { From e4757b4b3d0d6b9dc1984ae7c47da0d3cd351a57 Mon Sep 17 00:00:00 2001 From: Brighten Tompkins Date: Fri, 29 Aug 2025 05:41:49 -0700 Subject: [PATCH 7/7] docs: typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 0f67cfb7..00d67df3 100644 --- a/README.md +++ b/README.md @@ -185,7 +185,7 @@ And configuring an OTLP compliant logs exporters can be done as follows: ```go import "go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc" // ... -metricExporter, err := otlploggrpc.New( +logExporter, err := otlploggrpc.New( context.Background(), otlploggrpc.WithEndpoint("localhost:4317"), otlploggrpc.WithTLSCredentials(insecure.NewCredentials()))