Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/OpenTelemetry.Api/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@

## Unreleased

* **Breaking change:** CompositeTextMapPropagator.Fields now returns a
unioned set of fields from all combined propagators. Previously this always
returned an empty set.
([#5745](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5745))

## 1.9.0

Released 2024-Jun-14
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ namespace OpenTelemetry.Context.Propagation;
/// </summary>
public class CompositeTextMapPropagator : TextMapPropagator
{
private static readonly ISet<string> EmptyFields = new HashSet<string>();
private readonly List<TextMapPropagator> propagators;
private readonly IReadOnlyList<TextMapPropagator> propagators;
private readonly ISet<string> allFields;

/// <summary>
/// Initializes a new instance of the <see cref="CompositeTextMapPropagator"/> class.
Expand All @@ -22,18 +22,55 @@ public CompositeTextMapPropagator(IEnumerable<TextMapPropagator> propagators)
{
Guard.ThrowIfNull(propagators);

this.propagators = new List<TextMapPropagator>(propagators);
var propagatorsList = new List<TextMapPropagator>();

foreach (var propagator in propagators)
{
if (propagator is not null)
{
propagatorsList.Add(propagator);
}
}

this.propagators = propagatorsList;

// For efficiency, we resolve the fields from all propagators only once, as they are
// not expected to change (although the implementation doesn't strictly prevent that).
if (this.propagators.Count == 0)
{
// Use a new empty HashSet for each instance to avoid any potential mutation issues.
this.allFields = new HashSet<string>();
}
else
{
ISet<string> fields = this.propagators[0].Fields;

var output = fields is not null
? new HashSet<string>(fields)
: [];

for (int i = 1; i < this.propagators.Count; i++)
{
fields = this.propagators[i].Fields;
if (fields is not null)
{
output.UnionWith(fields);
}
}

this.allFields = output;
}
}

/// <inheritdoc/>
public override ISet<string> Fields => EmptyFields;
public override ISet<string> Fields => this.allFields;

/// <inheritdoc/>
public override PropagationContext Extract<T>(PropagationContext context, T carrier, Func<T, string, IEnumerable<string>> getter)
{
foreach (var propagator in this.propagators)
for (int i = 0; i < this.propagators.Count; i++)
{
context = propagator.Extract(context, carrier, getter);
context = this.propagators[i].Extract(context, carrier, getter);
}

return context;
Expand All @@ -42,9 +79,9 @@ public override PropagationContext Extract<T>(PropagationContext context, T carr
/// <inheritdoc/>
public override void Inject<T>(PropagationContext context, T carrier, Action<T, string, string> setter)
{
foreach (var propagator in this.propagators)
for (int i = 0; i < this.propagators.Count; i++)
{
propagator.Inject(context, carrier, setter);
this.propagators[i].Inject(context, carrier, setter);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,28 +31,72 @@ public class CompositePropagatorTest
private readonly ActivitySpanId spanId = ActivitySpanId.CreateRandom();

[Fact]
public void CompositePropagator_NullTextFormatList()
public void CompositePropagator_NullTextMapPropagators()
{
Assert.Throws<ArgumentNullException>(() => new CompositeTextMapPropagator(null));
}

[Fact]
public void CompositePropagator_EmptyTextMapPropagators()
{
var compositePropagator = new CompositeTextMapPropagator([]);
Assert.Empty(compositePropagator.Fields);
}

[Fact]
public void CompositePropagator_NullTextMapPropagator()
{
var compositePropagator = new CompositeTextMapPropagator([null]);
Assert.Empty(compositePropagator.Fields);
}

[Fact]
public void CompositePropagator_NoOpTextMapPropagators()
{
var compositePropagator = new CompositeTextMapPropagator([new NoopTextMapPropagator()]);
Assert.Empty(compositePropagator.Fields);
}

[Fact]
public void CompositePropagator_SingleTextMapPropagator()
{
var testPropagator = new TestPropagator("custom-traceparent-1", "custom-tracestate-1");

var compositePropagator = new CompositeTextMapPropagator([testPropagator]);

// We expect a new HashSet, with a copy of the values from the propagator.
Assert.Equal(testPropagator.Fields, compositePropagator.Fields);
Assert.NotSame(testPropagator.Fields, compositePropagator.Fields);
}

[Fact]
public void CompositePropagator_TestPropagator()
{
var compositePropagator = new CompositeTextMapPropagator(new List<TextMapPropagator>
{
new TestPropagator("custom-traceparent-1", "custom-tracestate-1"),
new TestPropagator("custom-traceparent-2", "custom-tracestate-2"),
});
var testPropagatorA = new TestPropagator("custom-traceparent-1", "custom-tracestate-1");
var testPropagatorB = new TestPropagator("custom-traceparent-2", "custom-tracestate-2");

var compositePropagator = new CompositeTextMapPropagator([testPropagatorA, testPropagatorB,]);

var activityContext = new ActivityContext(this.traceId, this.spanId, ActivityTraceFlags.Recorded, traceState: null);
PropagationContext propagationContext = new PropagationContext(activityContext, default);
var propagationContext = new PropagationContext(activityContext, default);
var carrier = new Dictionary<string, string>();
using var activity = new Activity("test");

compositePropagator.Inject(propagationContext, carrier, Setter);
Assert.Contains(carrier, kv => kv.Key == "custom-traceparent-1");
Assert.Contains(carrier, kv => kv.Key == "custom-traceparent-2");

Assert.Equal(testPropagatorA.Fields.Count + testPropagatorB.Fields.Count, compositePropagator.Fields.Count);
Assert.Subset(compositePropagator.Fields, testPropagatorA.Fields);
Assert.Subset(compositePropagator.Fields, testPropagatorB.Fields);

Assert.Equal(1, testPropagatorA.InjectCount);
Assert.Equal(1, testPropagatorB.InjectCount);

compositePropagator.Extract(default, new Dictionary<string, string>(), Getter);

Assert.Equal(1, testPropagatorA.ExtractCount);
Assert.Equal(1, testPropagatorB.ExtractCount);
}

[Fact]
Expand All @@ -61,20 +105,24 @@ public void CompositePropagator_UsingSameTag()
const string header01 = "custom-tracestate-01";
const string header02 = "custom-tracestate-02";

var compositePropagator = new CompositeTextMapPropagator(new List<TextMapPropagator>
{
new TestPropagator("custom-traceparent", header01, true),
new TestPropagator("custom-traceparent", header02),
});
var testPropagatorA = new TestPropagator("custom-traceparent", header01, true);
var testPropagatorB = new TestPropagator("custom-traceparent", header02);

var compositePropagator = new CompositeTextMapPropagator([testPropagatorA, testPropagatorB,]);

var activityContext = new ActivityContext(this.traceId, this.spanId, ActivityTraceFlags.Recorded, traceState: null);
PropagationContext propagationContext = new PropagationContext(activityContext, default);
var propagationContext = new PropagationContext(activityContext, default);

var carrier = new Dictionary<string, string>();

compositePropagator.Inject(propagationContext, carrier, Setter);
Assert.Contains(carrier, kv => kv.Key == "custom-traceparent");

Assert.Equal(3, compositePropagator.Fields.Count);

Assert.Equal(1, testPropagatorA.InjectCount);
Assert.Equal(1, testPropagatorB.InjectCount);

// checking if the latest propagator is the one with the data. So, it will replace the previous one.
Assert.Equal($"00-{this.traceId}-{this.spanId}-{header02.Split('-').Last()}", carrier["custom-traceparent"]);

Expand All @@ -85,6 +133,9 @@ public void CompositePropagator_UsingSameTag()
// checking if we accessed only two times: header/headerstate options
// if that's true, we skipped the first one since we have a logic to for the default result
Assert.Equal(2, count);

Assert.Equal(1, testPropagatorA.ExtractCount);
Assert.Equal(1, testPropagatorB.ExtractCount);
}

[Fact]
Expand All @@ -99,13 +150,13 @@ public void CompositePropagator_ActivityContext_Baggage()
var activityContext = new ActivityContext(this.traceId, this.spanId, ActivityTraceFlags.Recorded, traceState: null, isRemote: true);
var baggage = new Dictionary<string, string> { ["key1"] = "value1" };

PropagationContext propagationContextActivityOnly = new PropagationContext(activityContext, default);
PropagationContext propagationContextBaggageOnly = new PropagationContext(default, new Baggage(baggage));
PropagationContext propagationContextBoth = new PropagationContext(activityContext, new Baggage(baggage));
var propagationContextActivityOnly = new PropagationContext(activityContext, default);
var propagationContextBaggageOnly = new PropagationContext(default, new Baggage(baggage));
var propagationContextBoth = new PropagationContext(activityContext, new Baggage(baggage));

var carrier = new Dictionary<string, string>();
compositePropagator.Inject(propagationContextActivityOnly, carrier, Setter);
PropagationContext extractedContext = compositePropagator.Extract(default, carrier, Getter);
var extractedContext = compositePropagator.Extract(default, carrier, Getter);
Assert.Equal(propagationContextActivityOnly, extractedContext);

carrier = new Dictionary<string, string>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,23 +11,32 @@ public class TestPropagator : TextMapPropagator
private readonly string stateHeaderName;
private readonly bool defaultContext;

private int extractCount = 0;
private int injectCount = 0;

public TestPropagator(string idHeaderName, string stateHeaderName, bool defaultContext = false)
{
this.idHeaderName = idHeaderName;
this.stateHeaderName = stateHeaderName;
this.defaultContext = defaultContext;
}

public int ExtractCount => this.extractCount;

public int InjectCount => this.injectCount;

public override ISet<string> Fields => new HashSet<string>() { this.idHeaderName, this.stateHeaderName };

public override PropagationContext Extract<T>(PropagationContext context, T carrier, Func<T, string, IEnumerable<string>> getter)
{
Interlocked.Increment(ref this.extractCount);

if (this.defaultContext)
{
return context;
}

IEnumerable<string> id = getter(carrier, this.idHeaderName);
var id = getter(carrier, this.idHeaderName);
if (!id.Any())
{
return context;
Expand All @@ -39,8 +48,8 @@ public override PropagationContext Extract<T>(PropagationContext context, T carr
return context;
}

string tracestate = string.Empty;
IEnumerable<string> tracestateCollection = getter(carrier, this.stateHeaderName);
var tracestate = string.Empty;
var tracestateCollection = getter(carrier, this.stateHeaderName);
if (tracestateCollection?.Any() ?? false)
{
TraceContextPropagator.TryExtractTracestate(tracestateCollection.ToArray(), out tracestate);
Expand All @@ -53,14 +62,16 @@ public override PropagationContext Extract<T>(PropagationContext context, T carr

public override void Inject<T>(PropagationContext context, T carrier, Action<T, string, string> setter)
{
string headerNumber = this.stateHeaderName.Split('-').Last();
Interlocked.Increment(ref this.injectCount);

var headerNumber = this.stateHeaderName.Split('-').Last();

var traceparent = string.Concat("00-", context.ActivityContext.TraceId.ToHexString(), "-", context.ActivityContext.SpanId.ToHexString());
traceparent = string.Concat(traceparent, "-", headerNumber);

setter(carrier, this.idHeaderName, traceparent);

string tracestateStr = context.ActivityContext.TraceState;
var tracestateStr = context.ActivityContext.TraceState;
if (tracestateStr?.Length > 0)
{
setter(carrier, this.stateHeaderName, tracestateStr);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="$(RepoRoot)\src\OpenTelemetry.Api\OpenTelemetry.Api.csproj" />
<ProjectReference Include="$(RepoRoot)\src\OpenTelemetry.Exporter.InMemory\OpenTelemetry.Exporter.InMemory.csproj" />
</ItemGroup>

Expand Down