Skip to content

Commit 3d1f74d

Browse files
authored
[sdk-tracing] Allow samplers to set TraceState for propagation-only spans (open-telemetry#6058)
1 parent 17bbd83 commit 3d1f74d

File tree

3 files changed

+131
-84
lines changed

3 files changed

+131
-84
lines changed

src/OpenTelemetry/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ Notes](../../RELEASENOTES.md).
1212
now lead to unique metrics.
1313
([#5982](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5982))
1414

15+
* Fixed a bug in tracing where `TraceState` set by a custom `Sampler` is not
16+
applied when creating propagation-only spans.
17+
([#6058](https://github.com/open-telemetry/opentelemetry-dotnet/pull/6058))
18+
1519
## 1.11.0-rc.1
1620

1721
Released 2024-Dec-11

src/OpenTelemetry/Trace/TracerProviderSdk.cs

Lines changed: 17 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,7 @@ internal TracerProviderSdk(
237237
else if (this.sampler is AlwaysOffSampler)
238238
{
239239
activityListener.Sample = (ref ActivityCreationOptions<ActivityContext> options) =>
240-
!Sdk.SuppressInstrumentation ? PropagateOrIgnoreData(options.Parent) : ActivitySamplingResult.None;
240+
!Sdk.SuppressInstrumentation ? PropagateOrIgnoreData(ref options) : ActivitySamplingResult.None;
241241
this.getRequestedDataAction = this.RunGetRequestedDataAlwaysOffSampler;
242242
}
243243
else
@@ -493,47 +493,46 @@ private static ActivitySamplingResult ComputeActivitySamplingResult(
493493
{
494494
SamplingDecision.RecordAndSample => ActivitySamplingResult.AllDataAndRecorded,
495495
SamplingDecision.RecordOnly => ActivitySamplingResult.AllData,
496-
_ => ActivitySamplingResult.PropagationData,
496+
_ => PropagateOrIgnoreData(ref options),
497497
};
498498

499-
if (activitySamplingResult != ActivitySamplingResult.PropagationData)
499+
if (activitySamplingResult > ActivitySamplingResult.PropagationData)
500500
{
501501
foreach (var att in samplingResult.Attributes)
502502
{
503503
options.SamplingTags.Add(att.Key, att.Value);
504504
}
505+
}
505506

507+
if (activitySamplingResult != ActivitySamplingResult.None
508+
&& samplingResult.TraceStateString != null)
509+
{
506510
// https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/sdk.md#sampler
507511
// Spec requires clearing Tracestate if empty Tracestate is returned.
508512
// Since .NET did not have this capability, it'll break
509513
// existing samplers if we did that. So the following is
510514
// adopted to remain spec-compliant and backward compat.
511515
// The behavior is:
512-
// if sampler returns null, its treated as if it has no intend
516+
// if sampler returns null, its treated as if it has not intended
513517
// to change Tracestate. Existing SamplingResult ctors will put null as default TraceStateString,
514518
// so all existing samplers will get this behavior.
515519
// if sampler returns non-null, then it'll be used as the
516520
// new value for Tracestate
517521
// A sampler can return string.Empty if it intends to clear the state.
518-
if (samplingResult.TraceStateString != null)
519-
{
520-
options = options with { TraceState = samplingResult.TraceStateString };
521-
}
522-
523-
return activitySamplingResult;
522+
options = options with { TraceState = samplingResult.TraceStateString };
524523
}
525524

526-
return PropagateOrIgnoreData(options.Parent);
525+
return activitySamplingResult;
527526
}
528527

529528
[MethodImpl(MethodImplOptions.AggressiveInlining)]
530-
private static ActivitySamplingResult PropagateOrIgnoreData(in ActivityContext parentContext)
529+
private static ActivitySamplingResult PropagateOrIgnoreData(ref ActivityCreationOptions<ActivityContext> options)
531530
{
532-
var isRootSpan = parentContext.TraceId == default;
531+
var isRootSpan = options.Parent.TraceId == default;
533532

534533
// If it is the root span or the parent is remote select PropagationData so the trace ID is preserved
535534
// even if no activity of the trace is recorded (sampled per OpenTelemetry parlance).
536-
return (isRootSpan || parentContext.IsRemote)
535+
return (isRootSpan || options.Parent.IsRemote)
537536
? ActivitySamplingResult.PropagationData
538537
: ActivitySamplingResult.None;
539538
}
@@ -606,11 +605,11 @@ private void RunGetRequestedDataOtherSampler(Activity activity)
606605
{
607606
activity.SetTag(att.Key, att.Value);
608607
}
608+
}
609609

610-
if (samplingResult.TraceStateString != null)
611-
{
612-
activity.TraceStateString = samplingResult.TraceStateString;
613-
}
610+
if (samplingResult.TraceStateString != null)
611+
{
612+
activity.TraceStateString = samplingResult.TraceStateString;
614613
}
615614
}
616615
}

test/OpenTelemetry.Tests/Trace/SamplersTest.cs

Lines changed: 110 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -100,37 +100,7 @@ public void TracerProviderSdkSamplerAttributesAreAppliedToLegacyActivity(Samplin
100100
[InlineData(SamplingDecision.RecordAndSample)]
101101
public void SamplersCanModifyTraceStateOnLegacyActivity(SamplingDecision samplingDecision)
102102
{
103-
var existingTraceState = "a=1,b=2";
104-
var newTraceState = "a=1,b=2,c=3,d=4";
105-
var testSampler = new TestSampler
106-
{
107-
SamplingAction = (samplingParams) =>
108-
{
109-
Assert.Equal(existingTraceState, samplingParams.ParentContext.TraceState);
110-
return new SamplingResult(samplingDecision, newTraceState);
111-
},
112-
};
113-
114-
var operationNameForLegacyActivity = Utils.GetCurrentMethodName();
115-
using var tracerProvider = Sdk.CreateTracerProviderBuilder()
116-
.SetSampler(testSampler)
117-
.AddLegacySource(operationNameForLegacyActivity)
118-
.Build();
119-
120-
using var parentActivity = new Activity("Foo");
121-
parentActivity.TraceStateString = existingTraceState;
122-
parentActivity.Start();
123-
124-
using var activity = new Activity(operationNameForLegacyActivity);
125-
activity.Start();
126-
Assert.NotNull(activity);
127-
if (samplingDecision != SamplingDecision.Drop)
128-
{
129-
Assert.Equal(newTraceState, activity.TraceStateString);
130-
}
131-
132-
activity.Stop();
133-
parentActivity.Stop();
103+
RunLegacyActivitySamplerTest(samplingDecision, samplerTraceState: "a=1,b=2,c=3,d=4");
134104
}
135105

136106
[Theory]
@@ -139,36 +109,7 @@ public void SamplersCanModifyTraceStateOnLegacyActivity(SamplingDecision samplin
139109
[InlineData(SamplingDecision.RecordAndSample)]
140110
public void SamplersDoesNotImpactTraceStateWhenUsingNullLegacyActivity(SamplingDecision samplingDecision)
141111
{
142-
var existingTraceState = "a=1,b=2";
143-
var testSampler = new TestSampler
144-
{
145-
SamplingAction = (samplingParams) =>
146-
{
147-
Assert.Equal(existingTraceState, samplingParams.ParentContext.TraceState);
148-
return new SamplingResult(samplingDecision);
149-
},
150-
};
151-
152-
var operationNameForLegacyActivity = Utils.GetCurrentMethodName();
153-
using var tracerProvider = Sdk.CreateTracerProviderBuilder()
154-
.SetSampler(testSampler)
155-
.AddLegacySource(operationNameForLegacyActivity)
156-
.Build();
157-
158-
using var parentActivity = new Activity("Foo");
159-
parentActivity.TraceStateString = existingTraceState;
160-
parentActivity.Start();
161-
162-
using var activity = new Activity(operationNameForLegacyActivity);
163-
activity.Start();
164-
Assert.NotNull(activity);
165-
if (samplingDecision != SamplingDecision.Drop)
166-
{
167-
Assert.Equal(existingTraceState, activity.TraceStateString);
168-
}
169-
170-
activity.Stop();
171-
parentActivity.Stop();
112+
RunLegacyActivitySamplerTest(samplingDecision, samplerTraceState: null);
172113
}
173114

174115
[Theory]
@@ -183,7 +124,15 @@ public void SamplersCanModifyTraceState(SamplingDecision sampling)
183124
{
184125
SamplingAction = (samplingParams) =>
185126
{
186-
Assert.Equal(parentTraceState, samplingParams.ParentContext.TraceState);
127+
if (samplingParams.Name == "root")
128+
{
129+
Assert.Equal(parentTraceState, samplingParams.ParentContext.TraceState);
130+
}
131+
else
132+
{
133+
Assert.Equal(newTraceState, samplingParams.ParentContext.TraceState);
134+
}
135+
187136
return new SamplingResult(sampling, newTraceState);
188137
},
189138
};
@@ -195,13 +144,54 @@ public void SamplersCanModifyTraceState(SamplingDecision sampling)
195144
.SetSampler(testSampler)
196145
.Build();
197146

198-
var parentContext = new ActivityContext(ActivityTraceId.CreateRandom(), ActivitySpanId.CreateRandom(), ActivityTraceFlags.Recorded, parentTraceState, true);
147+
// Note: Remote parent is set as recorded
148+
var parentContext = new ActivityContext(ActivityTraceId.CreateRandom(), ActivitySpanId.CreateRandom(), ActivityTraceFlags.Recorded, parentTraceState, isRemote: true);
149+
150+
using var root = activitySource.StartActivity("root", ActivityKind.Server, parentContext);
151+
152+
// Note: We always create a root even for Drop. When dropping the
153+
// created root is for propagation only
154+
Assert.NotNull(root);
155+
Assert.Equal(newTraceState, root.TraceStateString);
156+
157+
if (sampling == SamplingDecision.RecordAndSample)
158+
{
159+
Assert.True(root.Recorded);
160+
Assert.True(root.IsAllDataRequested);
161+
}
162+
else if (sampling == SamplingDecision.RecordOnly)
163+
{
164+
// TODO: Update this when repo consumes DS v10.
165+
// Note: Seems to be a bug in DiagnosticSource. Root in this case
166+
// inherits context from the remote parent and Recorded doesn't get
167+
// cleared. This should be fixed in .NET 10:
168+
// https://github.com/dotnet/runtime/pull/111289
169+
// Assert.False(root.Recorded);
170+
171+
Assert.True(root.IsAllDataRequested);
172+
}
173+
else
174+
{
175+
// TODO: Update this when repo consumes DS v10.
176+
// Note: Seems to be a bug in DiagnosticSource. Root in this case
177+
// inherits context from the remote parent and Recorded doesn't get
178+
// cleared. This should be fixed in .NET 10:
179+
// https://github.com/dotnet/runtime/pull/111289
180+
// Assert.False(root.Recorded);
181+
182+
Assert.False(root.IsAllDataRequested);
183+
}
184+
185+
using var child = activitySource.StartActivity("child", ActivityKind.Server);
199186

200-
using var activity = activitySource.StartActivity("root", ActivityKind.Server, parentContext);
201187
if (sampling != SamplingDecision.Drop)
202188
{
203-
Assert.NotNull(activity);
204-
Assert.Equal(newTraceState, activity.TraceStateString);
189+
Assert.NotNull(child);
190+
Assert.Equal(newTraceState, child.TraceStateString);
191+
}
192+
else
193+
{
194+
Assert.Null(child);
205195
}
206196
}
207197

@@ -260,6 +250,60 @@ public void SamplerExceptionBubblesUpTest()
260250
Assert.Throws<InvalidOperationException>(() => activitySource.StartActivity("ThrowingSampler"));
261251
}
262252

253+
private static void RunLegacyActivitySamplerTest(SamplingDecision samplingDecision, string? samplerTraceState)
254+
{
255+
var existingTraceState = "a=1,b=2";
256+
257+
var operationNameForLegacyActivity = Utils.GetCurrentMethodName();
258+
259+
var testSampler = new TestSampler
260+
{
261+
SamplingAction = (samplingParams) =>
262+
{
263+
Assert.Equal(samplingParams.Name, operationNameForLegacyActivity);
264+
Assert.Equal(existingTraceState, samplingParams.ParentContext.TraceState);
265+
return new SamplingResult(samplingDecision, samplerTraceState);
266+
},
267+
};
268+
269+
using var tracerProvider = Sdk.CreateTracerProviderBuilder()
270+
.SetSampler(testSampler)
271+
.AddLegacySource(operationNameForLegacyActivity)
272+
.Build();
273+
274+
using var parentActivity = new Activity("Foo");
275+
parentActivity.TraceStateString = existingTraceState;
276+
parentActivity.Start();
277+
278+
using var childActivity = new Activity(operationNameForLegacyActivity);
279+
childActivity.Start();
280+
281+
if (samplerTraceState != null)
282+
{
283+
Assert.Equal(samplerTraceState, childActivity.TraceStateString);
284+
}
285+
else
286+
{
287+
Assert.Equal(existingTraceState, childActivity.TraceStateString);
288+
}
289+
290+
if (samplingDecision == SamplingDecision.RecordAndSample)
291+
{
292+
Assert.True(childActivity.Recorded);
293+
Assert.True(childActivity.IsAllDataRequested);
294+
}
295+
else if (samplingDecision == SamplingDecision.RecordOnly)
296+
{
297+
Assert.False(childActivity.Recorded);
298+
Assert.True(childActivity.IsAllDataRequested);
299+
}
300+
else
301+
{
302+
Assert.False(childActivity.Recorded);
303+
Assert.False(childActivity.IsAllDataRequested);
304+
}
305+
}
306+
263307
private sealed class ThrowingSampler : Sampler
264308
{
265309
public override SamplingResult ShouldSample(in SamplingParameters samplingParameters)

0 commit comments

Comments
 (0)