Mastering Structured Logging in Go with slog: A Complete Integration Guide for OpenTelemetry
In today's cloud-native applications, effective logging is no longer optional—it's essential. With Go's official slog package (introduced in Go 1.21), developers finally have a standard, powerful structured logging solution built into the language. This game-changing addition to the standard library brings structured logging capabilities that previously required third-party dependencies.
But how does slog
fit into the broader observability landscape, particularly with OpenTelemetry becoming the industry standard? In this comprehensive guide, we'll explore how Go's slog package works and demonstrate how to integrate it seamlessly with OpenTelemetry for robust, production-ready logging.
What is slog and Why Should You Care?
The slog
package (short for "structured logging") represents a significant evolution in Go's approach to logging. Unlike the traditional log
package that primarily deals with unstructured text, slog
creates logs as structured data with key-value pairs, making them far more valuable for analysis, filtering, and troubleshooting.
Key Benefits of slog:
- Standard Library Solution: No need for external dependencies for basic structured logging
- Performance-Focused: Designed with high-performance applications in mind
- Flexible Output Formats: Easy JSON output for modern logging systems
- Contextual Information: Simple addition of metadata to log entries
- Hierarchical Logging: Support for creating loggers with pre-defined attributes
- Handler Interface: Extensible design for custom log processing
With slog, Go developers can now implement sophisticated logging strategies without leaving the standard library, significantly simplifying dependency management and ensuring long-term compatibility.
Getting Started with slog
Let's explore the basics of using slog
in your Go applications:
1345689101112131415161718192021222324252627282930package mainimport ("log/slog""os")func main() {// Create a JSON loggerlogger := slog.New(slog.NewJSONHandler(os.Stdout, nil))// Set as default loggerslog.SetDefault(logger)// Log with structured dataslog.Info("User logged in","userID", "user-123","loginAttempt", 3,"source", "mobile-app")// Using groups to organize related fieldsslog.Info("Payment processed",slog.Group("user","id", "user-123","plan", "premium"),slog.Group("payment","id", "pmt-456","amount", 99.99,"currency", "USD"))}
The above code produces neatly structured JSON logs that are easily consumable by log aggregation systems:
12{"time":"2023-09-15T14:22:31.339Z","level":"INFO","msg":"User logged in","userID":"user-123","loginAttempt":3,"source":"mobile-app"}{"time":"2023-09-15T14:22:31.340Z","level":"INFO","msg":"Payment processed","user":{"id":"user-123","plan":"premium"},"payment":{"id":"pmt-456","amount":99.99,"currency":"USD"}}
Integrating slog with OpenTelemetry
While slog
provides excellent structured logging capabilities, modern observability practices often center around the OpenTelemetry framework, which unifies metrics, traces, and logs. Let's explore how to integrate slog
with OpenTelemetry to create a comprehensive logging solution.
Step 1: Setting Up Dependencies
First, add the necessary OpenTelemetry packages:
123go get go.opentelemetry.io/otelgo get go.opentelemetry.io/otel/sdkgo get go.opentelemetry.io/otel/exporters/otlp/otlplog
Step 2: Create an OpenTelemetry-Compatible slog Handler
The magic happens when we create a custom slog.Handler
that bridges between slog
and OpenTelemetry:
1345689101112131416171819202123242526272830313234353637383940414243444546474849505152535455565758596061626364666768697071727374757678798081package mainimport ("context""log/slog""os""go.opentelemetry.io/otel""go.opentelemetry.io/otel/attribute""go.opentelemetry.io/otel/exporters/otlp/otlplog""go.opentelemetry.io/otel/sdk/resource"sdktrace "go.opentelemetry.io/otel/sdk/trace"semconv "go.opentelemetry.io/otel/semconv/v1.17.0")// OTelHandler implements slog.Handler interfacetype OTelHandler struct {logExporter *otlplog.Exporterattrs []attribute.KeyValueresource *resource.Resource}func NewOTelHandler(exporter *otlplog.Exporter, res *resource.Resource) *OTelHandler {return &OTelHandler{logExporter: exporter,resource: res,}}func (h *OTelHandler) Enabled(ctx context.Context, level slog.Level) bool {return true // Or implement level filtering as needed}func (h *OTelHandler) Handle(ctx context.Context, r slog.Record) error {// Convert slog.Record to OpenTelemetry logattrs := make([]attribute.KeyValue, 0, r.NumAttrs())// Add level as attributeattrs = append(attrs, attribute.String("level", r.Level.String()))// Convert slog attributes to OTel attributesr.Attrs(func(a slog.Attr) bool {attrs = append(attrs, attribute.String(a.Key, a.Value.String()))return true})// Get trace context if availablespanCtx := trace.SpanContextFromContext(ctx)// Create and export the loglog := otlplog.NewLogRecord(otlplog.WithTimestamp(r.Time),otlplog.WithBody(r.Message),otlplog.WithAttributes(attrs...),otlplog.WithResource(h.resource),)if spanCtx.IsValid() {log.SetTraceID(spanCtx.TraceID())log.SetSpanID(spanCtx.SpanID())}return h.logExporter.ExportLogs(ctx, []otlplog.LogRecord{log})}func (h *OTelHandler) WithAttrs(attrs []slog.Attr) slog.Handler {// Convert slog.Attr to attribute.KeyValueotelAttrs := make([]attribute.KeyValue, 0, len(attrs))for _, attr := range attrs {otelAttrs = append(otelAttrs, attribute.String(attr.Key, attr.Value.String()))}newHandler := *hnewHandler.attrs = append(newHandler.attrs, otelAttrs...)return &newHandler}func (h *OTelHandler) WithGroup(name string) slog.Handler {// Implement group supportreturn h}
Step 3: Setting Up the Full Integration
Now let's put everything together with a complete example:
13456891011121415161718192021222324252627282930313233343536373839404142434445464748495051525354555657package mainimport ("context""log/slog""os""go.opentelemetry.io/otel""go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp""go.opentelemetry.io/otel/sdk/resource"semconv "go.opentelemetry.io/otel/semconv/v1.17.0")func main() {ctx := context.Background()// Create resource with service informationres := resource.NewWithAttributes(semconv.SchemaURL,semconv.ServiceNameKey.String("my-service"),semconv.ServiceVersionKey.String("1.0.0"),)// Create OTLP exporterexporter, err := otlploghttp.New(ctx,otlploghttp.WithEndpoint("otel-collector:4318"),otlploghttp.WithInsecure(),)if err != nil {panic(err)}// Create both handlers - one for console output and one for OpenTelemetryconsoleHandler := slog.NewJSONHandler(os.Stdout, nil)otelHandler := NewOTelHandler(exporter, res)// Combine handlersmultiHandler := slog.NewMultiHandler(consoleHandler, otelHandler)// Create and set the loggerlogger := slog.New(multiHandler)slog.SetDefault(logger)// Now logs will go to both console and OpenTelemetryslog.InfoContext(ctx, "Application started","environment", "production","commitSHA", "abc123")// Log with trace contextctx, span := otel.Tracer("my-service").Start(ctx, "important-operation")defer span.End()slog.InfoContext(ctx, "Processing customer order","orderID", "order-789","customerID", "cust-456","items", 3)}
Key Benefits of This Integration
By combining slog
with OpenTelemetry, you gain several advantages:
- Unified Observability: Logs are correlated with traces and metrics in a single system
- Distributed Tracing Context: Logs automatically include trace and span IDs
- Vendor Neutrality: Flexibility to switch between observability backends
- Standardized Format: Logs follow OpenTelemetry's semantic conventions
- Correlation: Easily correlate logs with other telemetry data
Best Practices for Production Use
When implementing this solution in production, consider these best practices:
- Structured Context: Always include relevant context in your logs
- Log Levels: Use appropriate log levels based on the message importance
- Performance Consideration: Batch logs when possible for better performance
- Error Handling: Implement robust error handling for the exporter
- Resource Attribution: Include detailed service information in your resource
Analyzing OpenTelemetry Logs in Dash0
Logs can be directly routed into Dash0. Dash0 with OpenTelemetry provides the ability to filter, search, group, and triage within a simple user interface, with full keyboard support. Dash0 also gives full log context by showing trace context, the call and resource that created the log - including details like the Kubernetes Pod, server, and cloud environment.
Log AI also enhanced the logs with more semantical metadata and structure without any manual pattern declaration.
Conclusion
Go's slog
package represents a significant step forward for logging in the language, bringing structured logging to the standard library. When combined with OpenTelemetry, it creates a powerful, future-proof logging solution that integrates seamlessly with modern observability practices.
By following the approach outlined in this guide, you can implement a comprehensive logging strategy that not only captures detailed information but also correlates it with traces and metrics for complete system visibility.