From 5c636763c68205a509ac4b12179a521603ff6d02 Mon Sep 17 00:00:00 2001 From: yingjiexu2002 <2235797040@qq.com> Date: Fri, 10 Jul 2026 16:14:14 +0800 Subject: [PATCH] feat(telemetry): propagate W3C traceparent from parent process - Add ContextWithTraceParentFromEnv to extract TRACEPARENT env var and inject upstream span context via OTel TextMapPropagator. - Register TraceContext+Baggage composite propagator in Init(). - Wire trace parent propagation into review and scan entry points. - Add tests for valid, absent, disabled, and malformed TRACEPARENT. --- cmd/opencodereview/review_cmd.go | 2 +- cmd/opencodereview/scan_cmd.go | 2 +- internal/telemetry/provider.go | 5 +++ internal/telemetry/span.go | 16 +++++++ internal/telemetry/span_test.go | 72 ++++++++++++++++++++++++++++++++ 5 files changed, 95 insertions(+), 2 deletions(-) diff --git a/cmd/opencodereview/review_cmd.go b/cmd/opencodereview/review_cmd.go index adf553c2..db079188 100644 --- a/cmd/opencodereview/review_cmd.go +++ b/cmd/opencodereview/review_cmd.go @@ -115,7 +115,7 @@ func runReview(args []string) error { q := newQuietHandle(opts.outputFormat, opts.audience) defer q.Restore() - ctx, span := telemetry.StartSpan(context.Background(), "review.run") + ctx, span := telemetry.StartSpan(telemetry.ContextWithTraceParentFromEnv(context.Background()), "review.run") defer span.End() telemetry.SetAttr(span, "review.repo", cc.RepoDir) telemetry.SetAttr(span, "review.from", opts.from) diff --git a/cmd/opencodereview/scan_cmd.go b/cmd/opencodereview/scan_cmd.go index c3835795..f9bee6ed 100644 --- a/cmd/opencodereview/scan_cmd.go +++ b/cmd/opencodereview/scan_cmd.go @@ -208,7 +208,7 @@ func runScan(args []string) error { q := newQuietHandle(opts.outputFormat, opts.audience) defer q.Restore() - ctx, span := telemetry.StartSpan(context.Background(), "scan.run") + ctx, span := telemetry.StartSpan(telemetry.ContextWithTraceParentFromEnv(context.Background()), "scan.run") defer span.End() var traceID string if telemetry.IsEnabled() { diff --git a/internal/telemetry/provider.go b/internal/telemetry/provider.go index 611234ba..32d59313 100644 --- a/internal/telemetry/provider.go +++ b/internal/telemetry/provider.go @@ -6,6 +6,7 @@ import ( "os" "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/propagation" sdkmetric "go.opentelemetry.io/otel/sdk/metric" "go.opentelemetry.io/otel/sdk/resource" sdktrace "go.opentelemetry.io/otel/sdk/trace" @@ -57,6 +58,10 @@ func Init(ctx context.Context) bool { otel.SetTracerProvider(tracerProvider) otel.SetMeterProvider(meterProvider) + otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator( + propagation.TraceContext{}, + propagation.Baggage{}, + )) return len(shutdownFuncs) > 0 } diff --git a/internal/telemetry/span.go b/internal/telemetry/span.go index 0b412ed7..6713f3e1 100644 --- a/internal/telemetry/span.go +++ b/internal/telemetry/span.go @@ -3,11 +3,13 @@ package telemetry import ( "context" "fmt" + "os" "time" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/codes" + "go.opentelemetry.io/otel/propagation" "go.opentelemetry.io/otel/trace" ) @@ -24,6 +26,20 @@ func StartSpan(ctx context.Context, name string, opts ...trace.SpanStartOption) return getTracer().Start(ctx, name, opts...) } +// ContextWithTraceParentFromEnv extracts W3C traceparent from the TRACEPARENT +// environment variable and returns a context carrying the upstream span context. +// Returns ctx unchanged when telemetry is disabled or the variable is unset. +func ContextWithTraceParentFromEnv(ctx context.Context) context.Context { + if !IsEnabled() { + return ctx + } + tp := os.Getenv("TRACEPARENT") + if tp == "" { + return ctx + } + return otel.GetTextMapPropagator().Extract(ctx, propagation.MapCarrier{"traceparent": tp}) +} + // TraceIDFromContext returns the hex-encoded trace ID of the span carried by // ctx, or "" if ctx carries no valid span (e.g. telemetry is disabled). func TraceIDFromContext(ctx context.Context) string { diff --git a/internal/telemetry/span_test.go b/internal/telemetry/span_test.go index 089aac06..afc235c8 100644 --- a/internal/telemetry/span_test.go +++ b/internal/telemetry/span_test.go @@ -7,7 +7,9 @@ import ( "testing" "time" + "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/propagation" "go.opentelemetry.io/otel/trace" ) @@ -170,3 +172,73 @@ func TestRecordLLMResult_NilSpan(t *testing.T) { RecordLLMResult(nil, 100*time.Millisecond, 0, nil) RecordLLMResult(nil, 100*time.Millisecond, 0, fmt.Errorf("err")) } + +// A well-formed W3C traceparent: version-traceID-spanID-flags. +const validTraceParent = "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01" +const wantExtractedTraceID = "0af7651916cd43dd8448eb211c80319c" + +func TestContextWithTraceParentFromEnv_Extracts(t *testing.T) { + setupEnabledTelemetry(t) + // Init registers TraceContext+Baggage at the global propagator; mirror + // that here so Extract works without going through the full Init path. + otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator( + propagation.TraceContext{}, propagation.Baggage{})) + + t.Setenv("TRACEPARENT", validTraceParent) + ctx := ContextWithTraceParentFromEnv(context.Background()) + + sc := trace.SpanContextFromContext(ctx) + if !sc.IsValid() { + t.Fatal("expected a valid SpanContext after extracting TRACEPARENT") + } + if got := sc.TraceID().String(); got != wantExtractedTraceID { + t.Errorf("extracted TraceID = %q, want %q", got, wantExtractedTraceID) + } + + // A span started on this ctx must inherit the upstream trace (child, not root). + _, span := StartSpan(ctx, "test.child") + defer span.End() + if got := span.SpanContext().TraceID().String(); got != wantExtractedTraceID { + t.Errorf("child span TraceID = %q, want %q (must inherit upstream)", got, wantExtractedTraceID) + } +} + +func TestContextWithTraceParentFromEnv_AbsentOrDisabled(t *testing.T) { + // Telemetry disabled: must short-circuit and leave ctx with no span context. + initialized = false + shutdownFuncs = nil + t.Setenv("TRACEPARENT", validTraceParent) + ctx := ContextWithTraceParentFromEnv(context.Background()) + if sc := trace.SpanContextFromContext(ctx); sc.IsValid() { + t.Error("expected no valid SpanContext when telemetry is disabled") + } + + // Telemetry enabled but TRACEPARENT unset: ctx unchanged (no upstream parent). + setupEnabledTelemetry(t) + otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator( + propagation.TraceContext{}, propagation.Baggage{})) + t.Setenv("TRACEPARENT", "") + ctx = ContextWithTraceParentFromEnv(context.Background()) + if sc := trace.SpanContextFromContext(ctx); sc.IsValid() { + t.Error("expected no valid SpanContext when TRACEPARENT is unset") + } +} + +func TestContextWithTraceParentFromEnv_Malformed(t *testing.T) { + setupEnabledTelemetry(t) + otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator( + propagation.TraceContext{}, propagation.Baggage{})) + + for _, bad := range []string{ + "not-a-traceparent", + "00-invalidtraceid-invalidspanid-01", + } { + t.Run(bad, func(t *testing.T) { + t.Setenv("TRACEPARENT", bad) + ctx := ContextWithTraceParentFromEnv(context.Background()) + if sc := trace.SpanContextFromContext(ctx); sc.IsValid() { + t.Errorf("expected no valid SpanContext for malformed TRACEPARENT %q", bad) + } + }) + } +}