diff --git a/otellog/otellog.go b/otellog/otellog.go new file mode 100644 index 0000000..a43425c --- /dev/null +++ b/otellog/otellog.go @@ -0,0 +1,62 @@ +// 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...) +}