Add internal/tracing: env-gated OTel setup, span/log helpers, decorators
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
63
internal/tracing/logger.go
Normal file
63
internal/tracing/logger.go
Normal file
@@ -0,0 +1,63 @@
|
||||
package tracing
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/go-logr/logr"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
logf "sigs.k8s.io/controller-runtime/pkg/log"
|
||||
)
|
||||
|
||||
// baseLoggerKey stores the pre-enrichment logger. logr sinks never see a
|
||||
// context, so trace IDs must ride on the logger as WithValues — and zap does
|
||||
// not dedupe repeated keys, so each nested span must re-derive from the same
|
||||
// base instead of stacking traceID/spanID onto an already-enriched logger.
|
||||
type baseLoggerKey struct{}
|
||||
|
||||
// Start begins a span from the global tracer and returns a context whose
|
||||
// logf.FromContext logger carries the span's traceID/spanID. With tracing
|
||||
// disabled the span is a no-op with an invalid span context and the logger
|
||||
// is left untouched.
|
||||
//
|
||||
// Trade-off, documented: values pushed via logf.IntoContext *between* two
|
||||
// Start calls are dropped by the inner Start's re-derivation. Nothing
|
||||
// first-party does that; controller-runtime's per-reconcile logger
|
||||
// (reconcileID etc.) is captured as the base and survives.
|
||||
func Start(ctx context.Context, name string, opts ...trace.SpanStartOption) (context.Context, trace.Span) {
|
||||
return StartSpan(ctx, newOptions(nil).tracer(), name, opts...)
|
||||
}
|
||||
|
||||
// StartSpan is Start with an explicit tracer, for decorators that carry
|
||||
// their own (test-injected) TracerProvider.
|
||||
func StartSpan(ctx context.Context, tracer trace.Tracer, name string, opts ...trace.SpanStartOption) (context.Context, trace.Span) {
|
||||
ctx, span := tracer.Start(ctx, name, opts...)
|
||||
sc := span.SpanContext()
|
||||
if !sc.IsValid() {
|
||||
return ctx, span
|
||||
}
|
||||
base, ok := ctx.Value(baseLoggerKey{}).(logr.Logger)
|
||||
if !ok {
|
||||
base = logf.FromContext(ctx)
|
||||
ctx = context.WithValue(ctx, baseLoggerKey{}, base)
|
||||
}
|
||||
return logf.IntoContext(ctx, withSpanValues(base, sc)), span
|
||||
}
|
||||
|
||||
// ContextWithLogger seeds ctx with base as both the current logger and the
|
||||
// base for span enrichment. If ctx already carries a valid span (the HTTP
|
||||
// middleware calls this inside the otelhttp handler), the logger is
|
||||
// enriched immediately.
|
||||
func ContextWithLogger(ctx context.Context, base logr.Logger) context.Context {
|
||||
ctx = context.WithValue(ctx, baseLoggerKey{}, base)
|
||||
if sc := trace.SpanContextFromContext(ctx); sc.IsValid() {
|
||||
return logf.IntoContext(ctx, withSpanValues(base, sc))
|
||||
}
|
||||
return logf.IntoContext(ctx, base)
|
||||
}
|
||||
|
||||
// withSpanValues uses lowerCamel keys to match the house style
|
||||
// (reconcileID, providerID); Grafana/Loki derived fields must match
|
||||
// "traceID", not the trace_id default.
|
||||
func withSpanValues(base logr.Logger, sc trace.SpanContext) logr.Logger {
|
||||
return base.WithValues("traceID", sc.TraceID().String(), "spanID", sc.SpanID().String())
|
||||
}
|
||||
Reference in New Issue
Block a user