Skip to content

Commit c8b89e9

Browse files
authored
Propagate context to self-observability measurements in sdk/trace (#7209)
Ensures metric functionality that integrates with trace context (e.g. exemplars) correctly receive the trace context and anything else the user has passed.
1 parent 49be001 commit c8b89e9

File tree

4 files changed

+158
-14
lines changed

4 files changed

+158
-14
lines changed

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ The next release will require at least [Go 1.24].
5050
The package contains semantic conventions from the `v1.36.0` version of the OpenTelemetry Semantic Conventions.
5151
See the [migration documentation](./semconv/v1.36.0/MIGRATION.md) for information on how to upgrade from `go.opentelemetry.io/otel/semconv/v1.34.0.`(#7032)
5252
- Add experimental self-observability span and batch span processor metrics in `go.opentelemetry.io/otel/sdk/trace`.
53-
Check the `go.opentelemetry.io/otel/sdk/trace/internal/x` package documentation for more information. (#7027, #6393)
53+
Check the `go.opentelemetry.io/otel/sdk/trace/internal/x` package documentation for more information. (#7027, #6393, #7209)
5454
- Add native histogram exemplar support in `go.opentelemetry.io/otel/exporters/prometheus`. (#6772)
5555
- Add experimental self-observability log metrics in `go.opentelemetry.io/otel/sdk/log`.
5656
Check the `go.opentelemetry.io/otel/sdk/log/internal/x` package documentation for more information. (#7121)

sdk/trace/span.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -509,7 +509,10 @@ func (s *recordingSpan) End(options ...trace.SpanEndOption) {
509509
attrSamplingResult = s.tracer.spanLiveMetric.AttrSpanSamplingResult(otelconv.SpanSamplingResultRecordOnly)
510510
}
511511

512-
s.tracer.spanLiveMetric.Add(context.Background(), -1, attrSamplingResult)
512+
// Add the span to the context to ensure the metric is recorded
513+
// with the correct span context.
514+
ctx := trace.ContextWithSpan(context.Background(), s)
515+
s.tracer.spanLiveMetric.Add(ctx, -1, attrSamplingResult)
513516
}()
514517
}
515518

sdk/trace/trace_test.go

Lines changed: 142 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2306,8 +2306,15 @@ func TestSelfObservability(t *testing.T) {
23062306
},
23072307
},
23082308
}
2309+
23092310
got := scopeMetrics()
2310-
metricdatatest.AssertEqual(t, want, got, metricdatatest.IgnoreTimestamp())
2311+
metricdatatest.AssertEqual(
2312+
t,
2313+
want,
2314+
got,
2315+
metricdatatest.IgnoreTimestamp(),
2316+
metricdatatest.IgnoreExemplars(),
2317+
)
23112318

23122319
span.End()
23132320

@@ -2361,7 +2368,13 @@ func TestSelfObservability(t *testing.T) {
23612368
},
23622369
}
23632370
got = scopeMetrics()
2364-
metricdatatest.AssertEqual(t, want, got, metricdatatest.IgnoreTimestamp())
2371+
metricdatatest.AssertEqual(
2372+
t,
2373+
want,
2374+
got,
2375+
metricdatatest.IgnoreTimestamp(),
2376+
metricdatatest.IgnoreExemplars(),
2377+
)
23652378
},
23662379
},
23672380
{
@@ -2404,7 +2417,13 @@ func TestSelfObservability(t *testing.T) {
24042417
}
24052418

24062419
got := scopeMetrics()
2407-
metricdatatest.AssertEqual(t, want, got, metricdatatest.IgnoreTimestamp())
2420+
metricdatatest.AssertEqual(
2421+
t,
2422+
want,
2423+
got,
2424+
metricdatatest.IgnoreTimestamp(),
2425+
metricdatatest.IgnoreExemplars(),
2426+
)
24082427
},
24092428
},
24102429
{
@@ -2465,7 +2484,13 @@ func TestSelfObservability(t *testing.T) {
24652484
}
24662485

24672486
got := scopeMetrics()
2468-
metricdatatest.AssertEqual(t, want, got, metricdatatest.IgnoreTimestamp())
2487+
metricdatatest.AssertEqual(
2488+
t,
2489+
want,
2490+
got,
2491+
metricdatatest.IgnoreTimestamp(),
2492+
metricdatatest.IgnoreExemplars(),
2493+
)
24692494
},
24702495
},
24712496
{
@@ -2535,7 +2560,13 @@ func TestSelfObservability(t *testing.T) {
25352560
},
25362561
}
25372562
got := scopeMetrics()
2538-
metricdatatest.AssertEqual(t, want, got, metricdatatest.IgnoreTimestamp())
2563+
metricdatatest.AssertEqual(
2564+
t,
2565+
want,
2566+
got,
2567+
metricdatatest.IgnoreTimestamp(),
2568+
metricdatatest.IgnoreExemplars(),
2569+
)
25392570
},
25402571
},
25412572
{
@@ -2607,7 +2638,13 @@ func TestSelfObservability(t *testing.T) {
26072638
}
26082639

26092640
got := scopeMetrics()
2610-
metricdatatest.AssertEqual(t, want, got, metricdatatest.IgnoreTimestamp())
2641+
metricdatatest.AssertEqual(
2642+
t,
2643+
want,
2644+
got,
2645+
metricdatatest.IgnoreTimestamp(),
2646+
metricdatatest.IgnoreExemplars(),
2647+
)
26112648

26122649
childSpan.End()
26132650
parentSpan.End()
@@ -2674,7 +2711,13 @@ func TestSelfObservability(t *testing.T) {
26742711
}
26752712

26762713
got = scopeMetrics()
2677-
metricdatatest.AssertEqual(t, want, got, metricdatatest.IgnoreTimestamp())
2714+
metricdatatest.AssertEqual(
2715+
t,
2716+
want,
2717+
got,
2718+
metricdatatest.IgnoreTimestamp(),
2719+
metricdatatest.IgnoreExemplars(),
2720+
)
26782721
},
26792722
},
26802723
}
@@ -2700,6 +2743,98 @@ func TestSelfObservability(t *testing.T) {
27002743
}
27012744
}
27022745

2746+
// ctxKeyT is a custom context value type used for testing context propagation.
2747+
type ctxKeyT string
2748+
2749+
// ctxKey is a context key used to store and retrieve values in the context.
2750+
var ctxKey = ctxKeyT("testKey")
2751+
2752+
func TestSelfObservabilityContextPropagation(t *testing.T) {
2753+
t.Setenv("OTEL_GO_X_SELF_OBSERVABILITY", "True")
2754+
prev := otel.GetMeterProvider()
2755+
t.Cleanup(func() { otel.SetMeterProvider(prev) })
2756+
2757+
// Approximate number of expected measuresments. This is not a strict
2758+
// requirement, but it should be enough to ensure no backpressure.
2759+
const count = 3 * 2 // 3 measurements per span, 2 spans (parent and child).
2760+
ctxCh, fltr := filterFn(count)
2761+
2762+
const want = "testValue"
2763+
n := make(chan int)
2764+
go func() {
2765+
// Validate the span context is propagated to all measurements by
2766+
// testing the context passed to the registered exemplar filter. This
2767+
// filter receives the measurement context in the standard metric SDK
2768+
// that we have registered.
2769+
2770+
// Count of how many contexts were received.
2771+
var count int
2772+
2773+
for ctx := range ctxCh {
2774+
count++
2775+
2776+
s := trace.SpanFromContext(ctx)
2777+
2778+
// All spans should have a valid span context. This should be
2779+
// passed to the measurements in all cases.
2780+
isValid := s.SpanContext().IsValid()
2781+
assert.True(t, isValid, "Context should have a valid span")
2782+
2783+
if s.IsRecording() {
2784+
// Check if the context value is propagated correctly for Span
2785+
// starts. The Span end operation does not receive any user
2786+
// context so do not check this if the span is not recording
2787+
// (i.e. end operation).
2788+
2789+
got := ctx.Value(ctxKey)
2790+
assert.Equal(t, want, got, "Context value not propagated")
2791+
}
2792+
}
2793+
n <- count
2794+
}()
2795+
2796+
// At least one reader is required to not get a no-op MeterProvider and
2797+
// short-circuit any instrumentation measurements.
2798+
r := metric.NewManualReader()
2799+
mp := metric.NewMeterProvider(
2800+
metric.WithExemplarFilter(fltr),
2801+
metric.WithReader(r),
2802+
)
2803+
otel.SetMeterProvider(mp)
2804+
2805+
tp := NewTracerProvider()
2806+
2807+
wrap := func(parentCtx context.Context, name string, fn func(context.Context)) {
2808+
const tracer = "TestSelfObservabilityContextPropagation"
2809+
ctx, s := tp.Tracer(tracer).Start(parentCtx, name)
2810+
defer s.End()
2811+
fn(ctx)
2812+
}
2813+
2814+
ctx := context.WithValue(context.Background(), ctxKey, want)
2815+
wrap(ctx, "parent", func(ctx context.Context) {
2816+
wrap(ctx, "child", func(context.Context) {})
2817+
})
2818+
2819+
require.NoError(t, tp.Shutdown(context.Background()))
2820+
2821+
// The TracerProvider shutdown returned, no more measurements will be sent
2822+
// to the exemplar filter.
2823+
close(ctxCh)
2824+
2825+
assert.Positive(t, <-n, "Expected at least 1 context propagations")
2826+
}
2827+
2828+
// filterFn returns a channel that receives contexts passed to the returned
2829+
// exemplar filter function.
2830+
func filterFn(n int) (chan context.Context, func(ctx context.Context) bool) {
2831+
out := make(chan context.Context, n)
2832+
return out, func(ctx context.Context) bool {
2833+
out <- ctx
2834+
return true
2835+
}
2836+
}
2837+
27032838
// RecordingOnly creates a Sampler that samples no traces, but enables recording.
27042839
// The created sampler maintains any tracestate from the parent span context.
27052840
func RecordingOnly() Sampler {

sdk/trace/tracer.go

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ func (tr *tracer) Start(
5252
}
5353

5454
s := tr.newSpan(ctx, name, &config)
55+
newCtx := trace.ContextWithSpan(ctx, s)
5556
if tr.selfObservabilityEnabled {
5657
// Check if the span has a parent span and set the origin attribute accordingly.
5758
var attrParentOrigin attribute.KeyValue
@@ -78,20 +79,21 @@ func (tr *tracer) Start(
7879
attrSamplingResult = tr.spanStartedMetric.AttrSpanSamplingResult(otelconv.SpanSamplingResultDrop)
7980
}
8081

81-
tr.spanStartedMetric.Add(context.Background(), 1, attrParentOrigin, attrSamplingResult)
82+
tr.spanStartedMetric.Add(newCtx, 1, attrParentOrigin, attrSamplingResult)
8283
}
8384

8485
if rw, ok := s.(ReadWriteSpan); ok && s.IsRecording() {
8586
sps := tr.provider.getSpanProcessors()
8687
for _, sp := range sps {
88+
// Use original context.
8789
sp.sp.OnStart(ctx, rw)
8890
}
8991
}
9092
if rtt, ok := s.(runtimeTracer); ok {
91-
ctx = rtt.runtimeTrace(ctx)
93+
newCtx = rtt.runtimeTrace(newCtx)
9294
}
9395

94-
return trace.ContextWithSpan(ctx, s), s
96+
return newCtx, s
9597
}
9698

9799
type runtimeTracer interface {
@@ -147,11 +149,12 @@ func (tr *tracer) newSpan(ctx context.Context, name string, config *trace.SpanCo
147149
if !isRecording(samplingResult) {
148150
return tr.newNonRecordingSpan(sc)
149151
}
150-
return tr.newRecordingSpan(psc, sc, name, samplingResult, config)
152+
return tr.newRecordingSpan(ctx, psc, sc, name, samplingResult, config)
151153
}
152154

153155
// newRecordingSpan returns a new configured recordingSpan.
154156
func (tr *tracer) newRecordingSpan(
157+
ctx context.Context,
155158
psc, sc trace.SpanContext,
156159
name string,
157160
sr SamplingResult,
@@ -199,7 +202,10 @@ func (tr *tracer) newRecordingSpan(
199202
attrSamplingResult = tr.spanLiveMetric.AttrSpanSamplingResult(otelconv.SpanSamplingResultRecordOnly)
200203
}
201204

202-
tr.spanLiveMetric.Add(context.Background(), 1, attrSamplingResult)
205+
// Propagate any existing values from the context with the new span to
206+
// the measurement context.
207+
ctx = trace.ContextWithSpan(ctx, s)
208+
tr.spanLiveMetric.Add(ctx, 1, attrSamplingResult)
203209
}
204210

205211
return s

0 commit comments

Comments
 (0)