Skip to content

Commit e251d3c

Browse files
committed
Don't attempt to replace the current SpanContext
In general, it turns out that replacing the current SpanContext is not a good idea. Indeed, the OTel documentation [says][1]: > Please note, since `SpanContext` is immutable, it is not possible to > update `SpanContext` with a new `TraceState`. Such changes then make > sense only right before `SpanContext` propagation or telemetry data > exporting. In both cases, `Propagator`s and `SpanExporter`s may create > a modified `TraceState` copy before serializing it to the wire. So this commit updates how we set `TraceOptions` *during* a span. Namely, when we update `TraceOptions` using `WithTraceOptions`, we no longer modify the `SpanContext`. Instead, the value of `TraceOptions` is stored on the `Context`, from where it can be retrieved by `TraceOptionsPropagator` at propagation time. This means that at propagation we will respect any `TraceOptions` set in the `TraceState`, *unless* TraceOptions have been set in the `Context`, in which case those will be used instead. [1]: https://opentelemetry.io/docs/specs/otel/trace/api/#tracestate
1 parent 209e6ed commit e251d3c

File tree

5 files changed

+175
-19
lines changed

5 files changed

+175
-19
lines changed

telemetry/propagators.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package telemetry
2+
3+
import (
4+
"context"
5+
6+
"go.opentelemetry.io/otel/propagation"
7+
"go.opentelemetry.io/otel/trace"
8+
)
9+
10+
// Check TraceOptionsPropagator implements TextMapPropagator
11+
var _ propagation.TextMapPropagator = new(TraceOptionsPropagator)
12+
13+
type TraceOptionsPropagator struct {
14+
Next propagation.TextMapPropagator
15+
}
16+
17+
func (p *TraceOptionsPropagator) Inject(ctx context.Context, carrier propagation.TextMapCarrier) {
18+
sc := trace.SpanContextFromContext(ctx)
19+
if !sc.IsValid() {
20+
return
21+
}
22+
23+
// If TraceOptions has been set directly in the context, then replace the
24+
// SpanContext with one that has the appropriate TraceState.
25+
//
26+
// Note: it is generally only safe to do this in a propagator or an exporter.
27+
if to, ok := traceOptionsFromContextOnly(ctx); ok {
28+
ts := setTraceOptions(sc.TraceState(), to)
29+
ctx = trace.ContextWithSpanContext(ctx, sc.WithTraceState(ts))
30+
}
31+
32+
p.Next.Inject(ctx, carrier)
33+
}
34+
35+
func (p *TraceOptionsPropagator) Extract(ctx context.Context, carrier propagation.TextMapCarrier) context.Context {
36+
return p.Next.Extract(ctx, carrier)
37+
}
38+
39+
func (p *TraceOptionsPropagator) Fields() []string {
40+
return p.Next.Fields()
41+
}

telemetry/propagators_test.go

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
package telemetry
2+
3+
import (
4+
"context"
5+
"testing"
6+
7+
"github.com/stretchr/testify/assert"
8+
"github.com/stretchr/testify/require"
9+
"go.opentelemetry.io/otel/propagation"
10+
"go.opentelemetry.io/otel/trace"
11+
)
12+
13+
func makeValidSpanContextConfig() trace.SpanContextConfig {
14+
traceID, _ := trace.TraceIDFromHex("0123456789abcdef0123456789abcdef")
15+
spanID, _ := trace.SpanIDFromHex("0123456789abcdef")
16+
return trace.SpanContextConfig{
17+
TraceID: traceID,
18+
SpanID: spanID,
19+
}
20+
}
21+
22+
func makeValidSpanContext() trace.SpanContext {
23+
return trace.NewSpanContext(makeValidSpanContextConfig())
24+
}
25+
26+
// Check that we're correctly passing the work onto the Next propagator.
27+
func TestTraceOptionsPropagatorUsesNextPropagator(t *testing.T) {
28+
ctx := context.Background()
29+
ctx = trace.ContextWithSpanContext(ctx, makeValidSpanContext())
30+
propagator := TraceOptionsPropagator{
31+
Next: propagation.TraceContext{},
32+
}
33+
carrier := propagation.MapCarrier{}
34+
35+
propagator.Inject(ctx, carrier)
36+
37+
require.Contains(t, carrier, "traceparent")
38+
}
39+
40+
// Check that TraceOptions are respected both in SpanContext and
41+
// (preferentially) from the Context itself.
42+
func TestTraceOptionsPropagatorInjectsTraceOptions(t *testing.T) {
43+
ctx := context.Background()
44+
45+
ts := trace.TraceState{}
46+
ts, _ = ts.Insert("r8/sm", "always")
47+
48+
scc := makeValidSpanContextConfig()
49+
scc.TraceState = ts
50+
ctx = trace.ContextWithSpanContext(ctx, trace.NewSpanContext(scc))
51+
propagator := TraceOptionsPropagator{
52+
Next: propagation.TraceContext{},
53+
}
54+
55+
// First check that only the sample mode field is set
56+
{
57+
carrier := propagation.MapCarrier{}
58+
propagator.Inject(ctx, carrier)
59+
require.Contains(t, carrier, "tracestate")
60+
assert.Equal(t, carrier["tracestate"], "r8/sm=always")
61+
}
62+
63+
// Then update TraceOptions locally and ensure that the values override those
64+
// set in the SpanContext.
65+
{
66+
ctx := WithTraceOptions(ctx, TraceOptions{
67+
DetailLevel: DetailLevelFull,
68+
SampleMode: SampleModeNever,
69+
})
70+
carrier := propagation.MapCarrier{}
71+
propagator.Inject(ctx, carrier)
72+
require.Contains(t, carrier, "tracestate")
73+
assert.Contains(t, carrier["tracestate"], "r8/sm=never")
74+
assert.Contains(t, carrier["tracestate"], "r8/dl=full")
75+
}
76+
}
77+
78+
func TestTraceOptionsPropagatorPrefersTraceOptionsFromContext(t *testing.T) {
79+
ctx := trace.ContextWithSpanContext(context.Background(), makeValidSpanContext())
80+
ctx = WithTraceOptions(ctx, TraceOptions{
81+
DetailLevel: DetailLevelFull,
82+
SampleMode: SampleModeAlways,
83+
})
84+
85+
propagator := TraceOptionsPropagator{
86+
Next: propagation.TraceContext{},
87+
}
88+
carrier := propagation.MapCarrier{}
89+
90+
propagator.Inject(ctx, carrier)
91+
92+
require.Contains(t, carrier, "tracestate")
93+
assert.Contains(t, carrier["tracestate"], "r8/sm=always")
94+
assert.Contains(t, carrier["tracestate"], "r8/dl=full")
95+
}

telemetry/telemetry.go

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,12 @@ func Start(ctx context.Context) (*Telemetry, error) {
3737
otel.SetErrorHandler(ErrorHandler{})
3838
otel.SetTracerProvider(tp)
3939
otel.SetTextMapPropagator(
40-
propagation.NewCompositeTextMapPropagator(
41-
propagation.TraceContext{},
42-
propagation.Baggage{},
43-
),
40+
&TraceOptionsPropagator{
41+
Next: propagation.NewCompositeTextMapPropagator(
42+
propagation.TraceContext{},
43+
propagation.Baggage{},
44+
),
45+
},
4446
)
4547

4648
return &Telemetry{tp}, nil

telemetry/tracestate.go

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ import (
77
"go.uber.org/zap"
88
)
99

10+
type traceOptionsContextKeyT string
11+
12+
const traceOptionsContextKey = traceOptionsContextKeyT("traceOptions")
13+
1014
const (
1115
TraceStateKeyDetailLevel = "r8/dl"
1216
TraceStateKeySampleMode = "r8/sm"
@@ -57,16 +61,20 @@ type TraceOptions struct {
5761
// TraceOptionsFromContext extracts any custom trace options from the trace
5862
// state carried in the passed context.
5963
func TraceOptionsFromContext(ctx context.Context) TraceOptions {
60-
ts := trace.SpanContextFromContext(ctx).TraceState()
61-
return parseTraceOptions(ts)
64+
// First we see if any TraceOptions are set directly in the context. If so,
65+
// they override any in the SpanContext TraceState.
66+
if to, ok := traceOptionsFromContextOnly(ctx); ok {
67+
return to
68+
}
69+
70+
// Otherwise we fall back to using any TraceOptions set in the SpanContext.
71+
return parseTraceOptions(trace.SpanContextFromContext(ctx).TraceState())
6272
}
6373

6474
// WithTraceOptions returns a copy of the provided context with the passed
6575
// TraceOptions set.
6676
func WithTraceOptions(ctx context.Context, to TraceOptions) context.Context {
67-
sc := trace.SpanContextFromContext(ctx)
68-
ts := setTraceOptions(sc.TraceState(), to)
69-
return trace.ContextWithSpanContext(ctx, sc.WithTraceState(ts))
77+
return context.WithValue(ctx, traceOptionsContextKey, to)
7078
}
7179

7280
// WithFullTrace returns a new context with full tracing mode enabled. This
@@ -78,6 +86,15 @@ func WithFullTrace(ctx context.Context) context.Context {
7886
return WithTraceOptions(ctx, to)
7987
}
8088

89+
func traceOptionsFromContextOnly(ctx context.Context) (TraceOptions, bool) {
90+
if v := ctx.Value(traceOptionsContextKey); v != nil {
91+
if to, ok := v.(TraceOptions); ok {
92+
return to, true
93+
}
94+
}
95+
return TraceOptions{}, false
96+
}
97+
8198
func parseTraceOptions(ts trace.TraceState) TraceOptions {
8299
to := TraceOptions{}
83100

telemetry/tracestate_test.go

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package telemetry
22

33
import (
44
"context"
5-
"strings"
65
"testing"
76

87
"github.com/stretchr/testify/assert"
@@ -32,17 +31,19 @@ func TestWithTraceOptionsSetsTraceOptions(t *testing.T) {
3231
assert.Equal(t, SampleModeAlways, to.SampleMode)
3332
}
3433

35-
func TestWithTraceOptionsSerialization(t *testing.T) {
34+
func TestTraceOptionsUsesSpanContextTraceOptions(t *testing.T) {
3635
ctx := context.Background()
3736

38-
ctx = WithTraceOptions(ctx, TraceOptions{
39-
DetailLevel: DetailLevelFull,
40-
SampleMode: SampleModeAlways,
41-
})
37+
ts := trace.TraceState{}
38+
ts, _ = ts.Insert("r8/dl", "full")
39+
ts, _ = ts.Insert("r8/sm", "always")
4240

43-
tsString := trace.SpanContextFromContext(ctx).TraceState().String()
44-
elements := strings.Split(tsString, ",")
41+
scc := makeValidSpanContextConfig()
42+
scc.TraceState = ts
43+
sc := trace.NewSpanContext(scc)
44+
ctx = trace.ContextWithSpanContext(ctx, sc)
4545

46-
assert.Contains(t, elements, "r8/sm=always")
47-
assert.Contains(t, elements, "r8/dl=full")
46+
to := TraceOptionsFromContext(ctx)
47+
assert.Equal(t, DetailLevelFull, to.DetailLevel)
48+
assert.Equal(t, SampleModeAlways, to.SampleMode)
4849
}

0 commit comments

Comments
 (0)