Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Next Next commit
feat: auto-align DistributedContextPropagator to W3C (#5592)
.NET's default LegacyPropagator emits Correlation-Context; the OpenTelemetry
SDK's BaggagePropagator only reads W3C baggage. The mismatch silently drops
tunit.test.id across processes so test correlation breaks on the SUT side.

Add a module initializer in TUnit.Core that swaps the runtime-default
LegacyPropagator for CreateW3CPropagator(), leaving user-customised
propagators untouched. TestWebApplicationFactory.ConfigureWebHost re-applies
the same alignment so SUT startup code cannot accidentally revert it. Opt
out via TUNIT_KEEP_LEGACY_PROPAGATOR=1.

Docs updated to reflect automatic alignment; manual OTel SetDefaultTextMapPropagator
snippet retained only for out-of-process SUTs that don't reference TUnit.Core.
  • Loading branch information
thomhurst committed Apr 17, 2026
commit c0c24b26e2dae99cff2943b6668cd89ad2e6eab0
4 changes: 4 additions & 0 deletions TUnit.AspNetCore.Core/TestWebApplicationFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -69,12 +69,16 @@ protected virtual void ConfigureStartupConfiguration(IConfigurationBuilder confi
/// Registers <see cref="CorrelatedTUnitLoggingExtensions.AddCorrelatedTUnitLogging"/> here
/// (rather than in <see cref="CreateHostBuilder"/>) so that minimal API hosts — where
/// <see cref="CreateHostBuilder"/> returns <c>null</c> — also get correlated logging.
/// Also re-aligns <see cref="System.Diagnostics.DistributedContextPropagator.Current"/>
/// to the W3C propagator for the SUT, in case user startup code reset it to the default.
/// Subclasses overriding this method must call <c>base.ConfigureWebHost(builder)</c>.
/// </summary>
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
base.ConfigureWebHost(builder);

PropagatorAlignment.AlignIfDefault();

builder.ConfigureServices(services =>
{
services.AddCorrelatedTUnitLogging();
Expand Down
51 changes: 51 additions & 0 deletions TUnit.Core/PropagatorAlignment.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
#if NET

using System.Diagnostics;
using System.Runtime.CompilerServices;

namespace TUnit.Core;

/// <summary>
/// Auto-aligns <see cref="DistributedContextPropagator.Current"/> to the W3C composite
/// propagator (traceparent + baggage) when the current propagator is .NET's default
/// <c>LegacyPropagator</c>. Without this, cross-process test correlation baggage
/// (<c>tunit.test.id</c>) is emitted as <c>Correlation-Context</c>, which the OpenTelemetry
/// SDK's <c>BaggagePropagator</c> does not read — the baggage silently drops between
/// the test process and the SUT.
/// </summary>
/// <remarks>
/// Set the environment variable <c>TUNIT_KEEP_LEGACY_PROPAGATOR=1</c> to opt out and
/// retain the default LegacyPropagator. Any user-configured propagator that isn't the
/// runtime default is left untouched.
/// </remarks>
internal static class PropagatorAlignment
{
private const string LegacyPropagatorTypeName = "System.Diagnostics.LegacyPropagator";

// Read once: env vars don't change within a process and GetEnvironmentVariable allocates.
private static readonly bool OptedOut =
Environment.GetEnvironmentVariable("TUNIT_KEEP_LEGACY_PROPAGATOR") == "1";

#pragma warning disable CA2255 // Module initializer is the intended entry point per issue #5592.
[ModuleInitializer]
#pragma warning restore CA2255
internal static void AlignOnModuleLoad() => AlignIfDefault();

/// <summary>
/// Idempotent: re-align only if the current propagator is still the runtime default.
/// </summary>
internal static void AlignIfDefault()
{
if (OptedOut)
{
return;
}

if (DistributedContextPropagator.Current.GetType().FullName == LegacyPropagatorTypeName)
{
DistributedContextPropagator.Current = DistributedContextPropagator.CreateW3CPropagator();

Check failure on line 46 in TUnit.Core/PropagatorAlignment.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (de-DE)

"DistributedContextPropagator" enthält keine Definition für "CreateW3CPropagator".

Check failure on line 46 in TUnit.Core/PropagatorAlignment.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (de-DE)

"DistributedContextPropagator" enthält keine Definition für "CreateW3CPropagator".

Check failure on line 46 in TUnit.Core/PropagatorAlignment.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (de-DE)

"DistributedContextPropagator" enthält keine Definition für "CreateW3CPropagator".

Check failure on line 46 in TUnit.Core/PropagatorAlignment.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (de-DE)

"DistributedContextPropagator" enthält keine Definition für "CreateW3CPropagator".

Check failure on line 46 in TUnit.Core/PropagatorAlignment.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (pl-PL)

'Element „DistributedContextPropagator” nie zawiera definicji „CreateW3CPropagator”.

Check failure on line 46 in TUnit.Core/PropagatorAlignment.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (pl-PL)

'Element „DistributedContextPropagator” nie zawiera definicji „CreateW3CPropagator”.

Check failure on line 46 in TUnit.Core/PropagatorAlignment.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (pl-PL)

'Element „DistributedContextPropagator” nie zawiera definicji „CreateW3CPropagator”.

Check failure on line 46 in TUnit.Core/PropagatorAlignment.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (pl-PL)

'Element „DistributedContextPropagator” nie zawiera definicji „CreateW3CPropagator”.

Check failure on line 46 in TUnit.Core/PropagatorAlignment.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (fr-FR)

'DistributedContextPropagator' ne contient pas de définition pour 'CreateW3CPropagator'

Check failure on line 46 in TUnit.Core/PropagatorAlignment.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (fr-FR)

'DistributedContextPropagator' ne contient pas de définition pour 'CreateW3CPropagator'

Check failure on line 46 in TUnit.Core/PropagatorAlignment.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (fr-FR)

'DistributedContextPropagator' ne contient pas de définition pour 'CreateW3CPropagator'

Check failure on line 46 in TUnit.Core/PropagatorAlignment.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (fr-FR)

'DistributedContextPropagator' ne contient pas de définition pour 'CreateW3CPropagator'

Check failure on line 46 in TUnit.Core/PropagatorAlignment.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (macos-latest)

'DistributedContextPropagator' does not contain a definition for 'CreateW3CPropagator'

Check failure on line 46 in TUnit.Core/PropagatorAlignment.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (macos-latest)

'DistributedContextPropagator' does not contain a definition for 'CreateW3CPropagator'

Check failure on line 46 in TUnit.Core/PropagatorAlignment.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (macos-latest)

'DistributedContextPropagator' does not contain a definition for 'CreateW3CPropagator'

Check failure on line 46 in TUnit.Core/PropagatorAlignment.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (macos-latest)

'DistributedContextPropagator' does not contain a definition for 'CreateW3CPropagator'

Check failure on line 46 in TUnit.Core/PropagatorAlignment.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (ubuntu-latest)

'DistributedContextPropagator' does not contain a definition for 'CreateW3CPropagator'

Check failure on line 46 in TUnit.Core/PropagatorAlignment.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (ubuntu-latest)

'DistributedContextPropagator' does not contain a definition for 'CreateW3CPropagator'

Check failure on line 46 in TUnit.Core/PropagatorAlignment.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (windows-latest)

'DistributedContextPropagator' does not contain a definition for 'CreateW3CPropagator'

Check failure on line 46 in TUnit.Core/PropagatorAlignment.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (windows-latest)

'DistributedContextPropagator' does not contain a definition for 'CreateW3CPropagator'
}
}
}

#endif
56 changes: 56 additions & 0 deletions TUnit.UnitTests/PropagatorAlignmentTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
#if NET
using System.Diagnostics;
using TUnit.Core;

namespace TUnit.UnitTests;

// Tests mutate DistributedContextPropagator.Current (process-global) — must not run concurrently.
[NotInParallel(nameof(PropagatorAlignmentTests))]
public class PropagatorAlignmentTests
{
[Test]
public async Task ModuleInitializer_Replaces_Default_Legacy_Propagator()
{
// Module init runs on first touch of any TUnit.Core type, so by now the default
// LegacyPropagator must already be gone; otherwise cross-process baggage breaks.
var current = DistributedContextPropagator.Current.GetType().FullName;
await Assert.That(current).IsNotEqualTo("System.Diagnostics.LegacyPropagator");
}

[Test]
public async Task AlignIfDefault_Leaves_Custom_Propagator_Untouched()
{
var original = DistributedContextPropagator.Current;
var custom = DistributedContextPropagator.CreatePassThroughPropagator();

try
{
DistributedContextPropagator.Current = custom;
PropagatorAlignment.AlignIfDefault();
await Assert.That(DistributedContextPropagator.Current).IsSameReferenceAs(custom);
}
finally
{
DistributedContextPropagator.Current = original;
}
}

[Test]
public async Task AlignIfDefault_Does_Not_Replace_Existing_W3C_Propagator()
{
var original = DistributedContextPropagator.Current;
var w3c = DistributedContextPropagator.CreateW3CPropagator();

try
{
DistributedContextPropagator.Current = w3c;
PropagatorAlignment.AlignIfDefault();
await Assert.That(DistributedContextPropagator.Current).IsSameReferenceAs(w3c);
}
finally
{
DistributedContextPropagator.Current = original;
}
}
}
#endif
4 changes: 3 additions & 1 deletion docs/docs/examples/opentelemetry.md
Original file line number Diff line number Diff line change
Expand Up @@ -299,7 +299,9 @@ Two common causes.

**1. The parent span isn't exported to the same backend.** The test-side `test case` span lives in the test process. If you only export from the SUT, the backend sees a child whose parent it has never seen. Either export the `"TUnit"` source from the test process too, or rely on the `tunit.test.id` tag (above) instead of trace hierarchy.

**2. The two processes use different baggage formats.** .NET defaults to `Correlation-Context`. The OpenTelemetry SDK reads W3C `baggage`. The two don't speak to each other. Use the same propagator on both sides:
**2. The two processes use different baggage formats.** .NET defaults to `Correlation-Context`. The OpenTelemetry SDK reads W3C `baggage`. Since TUnit 0.x (issue #5592), the test process auto-aligns `DistributedContextPropagator.Current` to W3C on module load, and `TestWebApplicationFactory<T>` re-applies this for in-process SUTs — no manual wiring needed. Set `TUNIT_KEEP_LEGACY_PROPAGATOR=1` to opt out.

For an **out-of-process** SUT that doesn't reference `TUnit.Core`, you still need to align it yourself:

```csharp
using OpenTelemetry;
Expand Down
8 changes: 5 additions & 3 deletions docs/docs/guides/distributed-tracing.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,11 @@ For cross-process correlation (your test calling your SUT), use `tunit.test.id`.

## When tracing across processes

If your test process and your SUT are different processes (or you're using `WebApplicationFactory` heavily), make sure both sides agree on the propagator:
Cross-process baggage propagation (e.g. `tunit.test.id` reaching your SUT) depends on both sides using the W3C `baggage` header rather than .NET's default `Correlation-Context`.

TUnit handles this automatically: a module initializer in `TUnit.Core` replaces the default `DistributedContextPropagator.LegacyPropagator` with `DistributedContextPropagator.CreateW3CPropagator()`. Any custom propagator you set yourself is left alone. If you want to retain the legacy behavior, set `TUNIT_KEEP_LEGACY_PROPAGATOR=1`.

For the SUT side, if it shares the test process (e.g. `TestWebApplicationFactory<T>`), alignment flows automatically. For out-of-process SUTs that don't reference `TUnit.Core`, align the propagator yourself on startup — either match `DistributedContextPropagator.Current` or, if you use the OpenTelemetry SDK:

```csharp
using OpenTelemetry;
Expand All @@ -112,8 +116,6 @@ Sdk.SetDefaultTextMapPropagator(new CompositeTextMapPropagator(
]));
```

Without this, .NET's default propagator emits `Correlation-Context`, but the OpenTelemetry SDK only reads W3C `baggage`. The mismatch silently drops baggage and you lose `tunit.test.id` on the SUT side.

## Limitations

### Static `ActivitySource` in third-party libraries
Expand Down
Loading