Documentation ¶
Overview ¶
Package otelslog provides a powerful integration between Go's structured logging (slog) and OpenTelemetry tracing. This package enables developers to correlate logs and traces seamlessly, creating a comprehensive observability solution that enhances debugging and monitoring capabilities in distributed systems.
Core Concepts ¶
The package centers around a custom slog.Handler implementation that automatically enriches log entries with distributed tracing context while maintaining the flexibility and simplicity of slog's interface. It preserves the hierarchical structure of logged attributes in both logs and trace spans, ensuring consistency across your observability data.
Key Features ¶
Trace Context Integration:
- Automatic injection of trace and span IDs into log records
- Preservation of parent-child relationships between spans
- Support for context propagation across service boundaries
Flexible Configuration:
- Customizable trace and span ID field names
- Configurable minimum log level for trace creation
- Optional span event recording
- Support for mandatory spans that bypass log level filtering
Structured Data Support:
- Full support for slog's group functionality
- Hierarchical attribute preservation in both logs and spans
- Type-aware attribute conversion between slog and OpenTelemetry formats
Basic Usage ¶
1. Setting up the handler with default configuration:
slog.SetDefault(slog.New(otelslog.NewHandler(slog.NewJSONHandler(os.Stdout, nil)))) slog.Info("hello, world")
2. Configuring the handler with custom options:
slog.SetDefault(slog.New( otelslog.NewHandler( slog.NewJSONHandler(os.Stdout, nil), otelslog.WithTraceIDKey("trace_id"), otelslog.WithSpanIDKey("span_id"), otelslog.WithTraceLevel(slog.LevelDebug), ), ))
Advanced Usage ¶
1. Creating spans with context propagation:
// Create a root span span1 := otelslog.NewSpanContext("trace", "span1") slog.Info("processing request", "operation", span1, "key1", "value1", ) defer span1.End() // Create a child span span2Ctx := otelslog.NewSpanContextWithContext(span1, "trace", "span2") slog.InfoContext(span2Ctx, "nested operation") defer span2Ctx.Done()
2. Working with attribute groups:
span := otelslog.NewSpanContext("trace", "span") slog.Default().WithGroup("request").Info("processing", "operation", span, slog.Group("user", slog.String("id", "123"), slog.String("role", "admin"), ), ) defer span.End()
3. Creating mandatory spans:
span := otelslog.NewMustSpanContext("trace", "critical-operation") slog.Info("critical processing", "operation", span) defer span.End()
Configuration Options ¶
The handler supports several functional options for customization:
WithTraceIDKey(key string):
Customizes the field name for trace IDs in log records
WithSpanIDKey(key string):
Customizes the field name for span IDs in log records
WithSpanEventKey(key string):
Customizes the field name used when recording log entries as span events
WithTraceLevel(level slog.Level):
Sets the minimum log level at which spans are created
WithNoSpanEvents():
Disables the recording of log entries as span events
Best Practices ¶
1. Span Management:
- Use defer for span.End() calls to ensure proper cleanup
- Create spans with meaningful names that describe the operation
- Use NewMustSpanContext for critical operations that should always be traced
2. Context Handling:
- Propagate context through your application using NewSpanContextWithContext
- Use InfoContext/ErrorContext when you have an existing context
- Maintain proper parent-child relationships between spans
3. Attribute Organization:
- Use groups to logically organize related attributes
- Maintain consistent attribute naming across your application
- Consider the hierarchical structure when designing attribute groups
4. Performance Considerations:
- Configure appropriate trace levels to control span creation
- Use WithNoSpanEvents when span events are not needed
- Consider the overhead of attribute conversion in high-throughput scenarios
Thread Safety ¶
The handler implementation is fully thread-safe and can be safely used concurrently from multiple goroutines. All operations on spans and contexts are designed to be thread-safe as well.
Integration with OpenTelemetry ¶
The package seamlessly integrates with OpenTelemetry's trace API and SDK. It supports:
- Standard OpenTelemetry trace exporters
- Custom trace samplers
- Resource attributes for service identification
- Context propagation across service boundaries
Example Setup with OTLP Exporter:
func initTracer(ctx context.Context) (func(), error) { exporter, err := otlptrace.New( ctx, otlptracegrpc.NewClient( otlptracegrpc.WithEndpoint("localhost:4317"), otlptracegrpc.WithInsecure(), ), ) if err != nil { return nil, err } tp := sdktrace.NewTracerProvider( sdktrace.WithBatcher(exporter), sdktrace.WithResource(resource.NewWithAttributes( semconv.ServiceName("your-service"), )), ) otel.SetTracerProvider(tp) return func() { tp.Shutdown(ctx) }, nil }
The package is designed to be a comprehensive solution for correlating logs and traces in Go applications, providing the flexibility and features needed for effective observability in modern distributed systems.
Index ¶
- type Handler
- type Options
- type SpanContext
- func NewMustSpanContext(spanName string, traceNameOpt ...string) *SpanContext
- func NewMustSpanContextWithContext(ctx context.Context, spanName string, traceNameOpt ...string) *SpanContext
- func NewSpanContext(spanName string, traceNameOpt ...string) *SpanContext
- func NewSpanContextWithContext(ctx context.Context, spanName string, traceNameOpt ...string) *SpanContext
Examples ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Handler ¶
type Handler struct { // Next slog.Handler in the chain Next slog.Handler // contains filtered or unexported fields }
Handler is responsible for managing OpenTelemetry trace context and handling slog attributes. It contains keys for trace and span IDs, controls for recording span events, and options for including baggage attributes in slog records.
func NewHandler ¶
NewHandler creates a new slog.Handler with the given options.
Example ¶
ExampleNewHandler shows how to use the default logger.
// /* // * Copyright (c) 2024 yakumioto <yaku.mioto@gmail.com> // * All rights reserved. // */ package main import ( "context" "log/slog" "os" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/exporters/otlp/otlptrace" "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc" "go.opentelemetry.io/otel/sdk/resource" sdktrace "go.opentelemetry.io/otel/sdk/trace" semconv "go.opentelemetry.io/otel/semconv/v1.24.0" "github.com/yakumioto/otelslog" ) // initTracer initializes an OTLP exporter, and configures the corresponding trace provider. func initTracer(ctx context.Context) (func(), error) { // Create OTLP exporter exporter, err := otlptrace.New( ctx, otlptracegrpc.NewClient( otlptracegrpc.WithEndpoint("127.0.0.1:4317"), // Your collector endpoint otlptracegrpc.WithInsecure(), // For testing only ), ) if err != nil { return nil, err } // Create resource with service information res, err := resource.New(ctx, resource.WithAttributes( semconv.ServiceName("your-service-name"), semconv.ServiceVersion("1.0.0"), ), ) if err != nil { return nil, err } // Create TracerProvider tp := sdktrace.NewTracerProvider( sdktrace.WithBatcher(exporter), sdktrace.WithResource(res), sdktrace.WithSampler(sdktrace.AlwaysSample()), ) // Set global TracerProvider otel.SetTracerProvider(tp) // Return a cleanup function return func() { if err := tp.Shutdown(ctx); err != nil { slog.Error("Error shutting down tracer provider", "error", err) } }, nil } // ExampleNewHandler shows how to use the default logger. func main() { // Set the default logger to use the OTEL SLOG handler with JSON output to standard output. slog.SetDefault(slog.New(otelslog.NewHandler(slog.NewJSONHandler(os.Stdout, nil)))) slog.Info("hello, world") // Set the default logger to use the OTEL SLOG handler with JSON output to standard output. // And set the trace ID key to "trace_id", span ID key to "span_id", and the trace level to debug. slog.SetDefault(slog.New( otelslog.NewHandler( slog.NewJSONHandler(os.Stdout, nil), otelslog.WithTraceIDKey("trace_id"), otelslog.WithSpanIDKey("span_id"), otelslog.WithTraceLevel(slog.LevelDebug), ), )) // Initialize the tracer. ctx, cancel := context.WithCancel(context.Background()) defer cancel() cleanup, err := initTracer(ctx) if err != nil { panic(err) } defer cleanup() // no trace log slog.Info("hello, world") // trace with slog attributes span1 := otelslog.NewSpanContextWithContext(ctx, "", "span1") slog.Info("processing request1", "trace1", span1, "key", "1", slog.Group("group1", slog.String("key1", "value1"), slog.String("key2", "value2"), ), ) defer span1.End() // trace with slog.XXXContext span2Ctx := otelslog.NewSpanContextWithContext(span1, "trace2", "span2") slog.InfoContext(span2Ctx, "processing request2", "key", "1", slog.Group("group2", slog.String("key1", "value1"), slog.String("key2", "value2"), ), ) defer span2Ctx.Done() // trace with slog.With span3Ctx := otelslog.NewSpanContextWithContext(span2Ctx, "trace3", "span3") slog.Default().WithGroup("group3").With("trace3", span3Ctx).Error("processing request3", "key", "1", slog.Group("group4", slog.String("key1", "value1"), slog.String("key2", "value2"), ), ) defer span3Ctx.End() }
Output:
func (*Handler) Handle ¶
Handle processes the slog.Record and adds OpenTelemetry attributes and events.
type Options ¶
type Options func(*Handler)
Options is a functional option for the Handler.
func WithNoSpanEvents ¶
func WithNoSpanEvents() Options
WithNoSpanEvents disables recording slog attributes as span events.
func WithSpanEventKey ¶
WithSpanEventKey sets the key used to record slog attributes as span events.
func WithSpanIDKey ¶
WithSpanIDKey sets the key used to record the span ID in slog records.
func WithTraceIDKey ¶
WithTraceIDKey sets the key used to record the trace ID in slog records.
func WithTraceLevel ¶
type SpanContext ¶ added in v1.1.0
SpanContext is a wrapper around trace.Span that provides a context.Context. It contains the span, context, trace name, span name, and a flag to ensure the span is created.
func NewMustSpanContext ¶ added in v1.1.0
func NewMustSpanContext(spanName string, traceNameOpt ...string) *SpanContext
NewMustSpanContext creates a new SpanContext with the given span name and ensures it is always created.
func NewMustSpanContextWithContext ¶ added in v1.1.0
func NewMustSpanContextWithContext(ctx context.Context, spanName string, traceNameOpt ...string) *SpanContext
NewMustSpanContextWithContext creates a new SpanContext with the given context and ensures it is always created.
func NewSpanContext ¶ added in v1.1.0
func NewSpanContext(spanName string, traceNameOpt ...string) *SpanContext
NewSpanContext creates a new SpanContext with the given span name.
func NewSpanContextWithContext ¶ added in v1.1.0
func NewSpanContextWithContext(ctx context.Context, spanName string, traceNameOpt ...string) *SpanContext
NewSpanContextWithContext creates a new SpanContext with the given context.
func (*SpanContext) Done ¶ added in v1.1.0
func (s *SpanContext) Done() <-chan struct{}
Done ends the span and returns the context's done channel.