// Package otellog provides logging helpers that bridge slog with OpenTelemetry // spans. Every Error/Info call both emits a structured log record AND touches // the active span — so errors appear as red markers in Jaeger/Tempo AND as // correlated log lines in your log backend (Loki, Elasticsearch, etc.). // // Usage: // // otellog.Error(ctx, logger, "checkout failed", err, "orderId", id.String()) // // The helpers are safe when no span is active (the span is a no-op in that // case) — callers don't need to guard the call. package otellog import ( "context" "log/slog" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/codes" "go.opentelemetry.io/otel/trace" ) // Error records an error on the active span (with stack trace) and emits a // structured slog.ErrorContext log. The span status is set to Error. // Any extra key/value pairs are appended to the log record. func Error(ctx context.Context, logger *slog.Logger, msg string, err error, args ...any) { if span := trace.SpanFromContext(ctx); span != nil { span.RecordError(err, trace.WithStackTrace(true)) span.SetStatus(codes.Error, msg) } all := make([]any, 0, len(args)+2) all = append(all, "error", err) all = append(all, args...) logger.ErrorContext(ctx, msg, all...) } // Info emits a structured slog.InfoContext log. When a span is active the // trace/span IDs are automatically injected by the OTel slog handler (if // configured), correlating the log line with the trace. func Info(ctx context.Context, logger *slog.Logger, msg string, args ...any) { logger.InfoContext(ctx, msg, args...) } // Warn emits a structured slog.WarnContext log. Like Info, span correlation // is handled by the OTel slog handler when configured. func Warn(ctx context.Context, logger *slog.Logger, msg string, args ...any) { logger.WarnContext(ctx, msg, args...) } // SpanFromContext returns the current span — useful when callers need to add // attributes or events directly. Returns a valid (no-op) span when no span is // active, so the caller never needs a nil check. func SpanFromContext(ctx context.Context) trace.Span { return trace.SpanFromContext(ctx) } // StartSpan is a convenience wrapper around otel.Tracer(name).Start(ctx, name) // that returns the ctx and span. Panics if the tracer provider is not // configured — but in practice the global no-op provider handles that. func StartSpan(ctx context.Context, tracerName, spanName string, opts ...trace.SpanStartOption) (context.Context, trace.Span) { return otel.Tracer(tracerName).Start(ctx, spanName, opts...) }