Skip to content

Commit a32f852

Browse files
feat(telemetry): propagate W3C traceparent from parent process (#352)
- 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.
1 parent b12649c commit a32f852

5 files changed

Lines changed: 95 additions & 2 deletions

File tree

cmd/opencodereview/review_cmd.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ func runReview(args []string) error {
126126
q := newQuietHandle(opts.outputFormat, opts.audience)
127127
defer q.Restore()
128128

129-
ctx, span := telemetry.StartSpan(context.Background(), "review.run")
129+
ctx, span := telemetry.StartSpan(telemetry.ContextWithTraceParentFromEnv(context.Background()), "review.run")
130130
defer span.End()
131131
telemetry.SetAttr(span, "review.repo", cc.RepoDir)
132132
telemetry.SetAttr(span, "review.from", opts.from)

cmd/opencodereview/scan_cmd.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,7 @@ func runScan(args []string) error {
208208
q := newQuietHandle(opts.outputFormat, opts.audience)
209209
defer q.Restore()
210210

211-
ctx, span := telemetry.StartSpan(context.Background(), "scan.run")
211+
ctx, span := telemetry.StartSpan(telemetry.ContextWithTraceParentFromEnv(context.Background()), "scan.run")
212212
defer span.End()
213213
var traceID string
214214
if telemetry.IsEnabled() {

internal/telemetry/provider.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"os"
77

88
"go.opentelemetry.io/otel"
9+
"go.opentelemetry.io/otel/propagation"
910
sdkmetric "go.opentelemetry.io/otel/sdk/metric"
1011
"go.opentelemetry.io/otel/sdk/resource"
1112
sdktrace "go.opentelemetry.io/otel/sdk/trace"
@@ -57,6 +58,10 @@ func Init(ctx context.Context) bool {
5758

5859
otel.SetTracerProvider(tracerProvider)
5960
otel.SetMeterProvider(meterProvider)
61+
otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(
62+
propagation.TraceContext{},
63+
propagation.Baggage{},
64+
))
6065

6166
return len(shutdownFuncs) > 0
6267
}

internal/telemetry/span.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@ package telemetry
33
import (
44
"context"
55
"fmt"
6+
"os"
67
"time"
78

89
"go.opentelemetry.io/otel"
910
"go.opentelemetry.io/otel/attribute"
1011
"go.opentelemetry.io/otel/codes"
12+
"go.opentelemetry.io/otel/propagation"
1113
"go.opentelemetry.io/otel/trace"
1214
)
1315

@@ -24,6 +26,20 @@ func StartSpan(ctx context.Context, name string, opts ...trace.SpanStartOption)
2426
return getTracer().Start(ctx, name, opts...)
2527
}
2628

29+
// ContextWithTraceParentFromEnv extracts W3C traceparent from the TRACEPARENT
30+
// environment variable and returns a context carrying the upstream span context.
31+
// Returns ctx unchanged when telemetry is disabled or the variable is unset.
32+
func ContextWithTraceParentFromEnv(ctx context.Context) context.Context {
33+
if !IsEnabled() {
34+
return ctx
35+
}
36+
tp := os.Getenv("TRACEPARENT")
37+
if tp == "" {
38+
return ctx
39+
}
40+
return otel.GetTextMapPropagator().Extract(ctx, propagation.MapCarrier{"traceparent": tp})
41+
}
42+
2743
// TraceIDFromContext returns the hex-encoded trace ID of the span carried by
2844
// ctx, or "" if ctx carries no valid span (e.g. telemetry is disabled).
2945
func TraceIDFromContext(ctx context.Context) string {

internal/telemetry/span_test.go

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ import (
77
"testing"
88
"time"
99

10+
"go.opentelemetry.io/otel"
1011
"go.opentelemetry.io/otel/attribute"
12+
"go.opentelemetry.io/otel/propagation"
1113
"go.opentelemetry.io/otel/trace"
1214
)
1315

@@ -170,3 +172,73 @@ func TestRecordLLMResult_NilSpan(t *testing.T) {
170172
RecordLLMResult(nil, 100*time.Millisecond, 0, nil)
171173
RecordLLMResult(nil, 100*time.Millisecond, 0, fmt.Errorf("err"))
172174
}
175+
176+
// A well-formed W3C traceparent: version-traceID-spanID-flags.
177+
const validTraceParent = "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01"
178+
const wantExtractedTraceID = "0af7651916cd43dd8448eb211c80319c"
179+
180+
func TestContextWithTraceParentFromEnv_Extracts(t *testing.T) {
181+
setupEnabledTelemetry(t)
182+
// Init registers TraceContext+Baggage at the global propagator; mirror
183+
// that here so Extract works without going through the full Init path.
184+
otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(
185+
propagation.TraceContext{}, propagation.Baggage{}))
186+
187+
t.Setenv("TRACEPARENT", validTraceParent)
188+
ctx := ContextWithTraceParentFromEnv(context.Background())
189+
190+
sc := trace.SpanContextFromContext(ctx)
191+
if !sc.IsValid() {
192+
t.Fatal("expected a valid SpanContext after extracting TRACEPARENT")
193+
}
194+
if got := sc.TraceID().String(); got != wantExtractedTraceID {
195+
t.Errorf("extracted TraceID = %q, want %q", got, wantExtractedTraceID)
196+
}
197+
198+
// A span started on this ctx must inherit the upstream trace (child, not root).
199+
_, span := StartSpan(ctx, "test.child")
200+
defer span.End()
201+
if got := span.SpanContext().TraceID().String(); got != wantExtractedTraceID {
202+
t.Errorf("child span TraceID = %q, want %q (must inherit upstream)", got, wantExtractedTraceID)
203+
}
204+
}
205+
206+
func TestContextWithTraceParentFromEnv_AbsentOrDisabled(t *testing.T) {
207+
// Telemetry disabled: must short-circuit and leave ctx with no span context.
208+
initialized = false
209+
shutdownFuncs = nil
210+
t.Setenv("TRACEPARENT", validTraceParent)
211+
ctx := ContextWithTraceParentFromEnv(context.Background())
212+
if sc := trace.SpanContextFromContext(ctx); sc.IsValid() {
213+
t.Error("expected no valid SpanContext when telemetry is disabled")
214+
}
215+
216+
// Telemetry enabled but TRACEPARENT unset: ctx unchanged (no upstream parent).
217+
setupEnabledTelemetry(t)
218+
otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(
219+
propagation.TraceContext{}, propagation.Baggage{}))
220+
t.Setenv("TRACEPARENT", "")
221+
ctx = ContextWithTraceParentFromEnv(context.Background())
222+
if sc := trace.SpanContextFromContext(ctx); sc.IsValid() {
223+
t.Error("expected no valid SpanContext when TRACEPARENT is unset")
224+
}
225+
}
226+
227+
func TestContextWithTraceParentFromEnv_Malformed(t *testing.T) {
228+
setupEnabledTelemetry(t)
229+
otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(
230+
propagation.TraceContext{}, propagation.Baggage{}))
231+
232+
for _, bad := range []string{
233+
"not-a-traceparent",
234+
"00-invalidtraceid-invalidspanid-01",
235+
} {
236+
t.Run(bad, func(t *testing.T) {
237+
t.Setenv("TRACEPARENT", bad)
238+
ctx := ContextWithTraceParentFromEnv(context.Background())
239+
if sc := trace.SpanContextFromContext(ctx); sc.IsValid() {
240+
t.Errorf("expected no valid SpanContext for malformed TRACEPARENT %q", bad)
241+
}
242+
})
243+
}
244+
}

0 commit comments

Comments
 (0)