Trace & Baggage Context Propagation in .NET with OpenTelemetry
Context propagation is a key part of distributed tracing. It enables transmitting both trace
and baggage
context across network boundaries so that different services in your system can participate in the same trace
.
The W3C provides standards for propagation:
These contexts are transferred in HTTP headers (or other protocols), typically as traceparent
for trace context
and baggage
for baggage context
. Below, we will explore how this works in .NET with OpenTelemetry.
TraceContext
The traceparent
header contains information about the current trace.
traceparent: 00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01
Format
- Version (00)
- Trace-Id (0af7651916cd43dd8448eb211c80319c)
- Parent-Id (b7ad6b7169203331)
- Trace-Flags (01)
A trace (or span) is represented by an Activity
object. Each Activity
instance corresponds to one span in the trace.
BaggageContext
The baggage
header holds additional context in the form of key=value
pairs.
baggage: key1=value1
Do not use Activity.Baggage
to read or modify baggage. Instead, use Baggage.Current
from the OpenTelemetry.Api
package.
This ensures that baggage context
is managed consistently with the OpenTelemetry specifications.
Important: These header values are transferred in plaintext to outgoing HTTP requests — internal, third parties, etc. Avoid placing sensitive or personally identifiable information in the baggage context.
.NET Usage
Tracing configuration registers TraceContextPropagator
and BaggageContextPropagator
out of the box.
builder.Services.AddOpenTelemetry()
.UseOtlpExporter()
.ConfigureResource(configure => {
configure.AddService(builder.Environment.ApplicationName)
})
.WithTracing();
These propagators enable extracting traceparent
and baggage
headers format from incoming requests and injecting them into outgoing requests.
Custom Propagators
If you need support for alternative or custom propagators (e.g., Zipkin headers), you can set them explicitly using a CompositeTextMapPropagator
Sdk.SetDefaultTextMapPropagator(new CompositeTextMapPropagator(
new List<TextMapPropagator>() {
new MyCustomPropagator()
}));
HTTP Propagation
By adding the following instrumentation, OpenTelemetry will automatically handle context propagation for incoming and outgoing HTTP requests.
- AddAspNetCoreInstrumentation extracts traceparent and baggage from incoming HTTP requests.
- AddHttpClientInstrumentation injects traceparent and baggage into outgoing HTTP requests.
builder.Services.AddOpenTelemetry()
.UseOtlpExporter()
.ConfigureResource(configure => {
configure.AddService(builder.Environment.ApplicationName)
})
.WithTracing(tracing =>
{
tracing
.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation();
});
If you want to see how this works under the hood, check out the OpenTelemetry .NET code for incoming HTTP instrumentation and outgoing HTTP instrumentation.
Manual Propagation
For protocols not natively instrumented (messaging systems, outbox, etc), you’ll need to manually propagate the context by calling the Inject
and Extract
methods on the Propagators.DefaultTextMapPropagator
.
public void SendMessage(IMessage message)
{
var propagationContext = new PropagationContext(Activity.Current?.Context ?? default, Baggage.Current);
Propagators.DefaultTextMapPropagator.Inject(
propagationContext,
message.Headers,
(headers, key, value) => headers[key] = value
);
// Publishing ...
}
public void ProcessMessage(IMessage message)
{
var propagationContext = Propagators.DefaultTextMapPropagator.Extract(
default,
message.Headers,
(headers, key) => headers.TryGetValue(key, out var value) ? new[] { value } : Array.Empty<string>()
);
using var activity = ActivityProvider.ActivitySource.StartActivity("receive", ActivityKind.Consumer, propagationContext.ActivityContext);
Baggage.Current = propagationContext.Baggage;
// Processing ...
}
Using this approach, you maintain consistent trace
and baggage
context across any protocol or communication mechanism in your distributed system.