diff --git a/TUnit.AspNetCore.Core/TUnit.AspNetCore.Core.csproj b/TUnit.AspNetCore.Core/TUnit.AspNetCore.Core.csproj
index 9f9f7bc802..4daa3782ed 100644
--- a/TUnit.AspNetCore.Core/TUnit.AspNetCore.Core.csproj
+++ b/TUnit.AspNetCore.Core/TUnit.AspNetCore.Core.csproj
@@ -16,10 +16,17 @@
+
+
+
+
+
+
+
diff --git a/TUnit.AspNetCore.Core/TestWebApplicationFactory.cs b/TUnit.AspNetCore.Core/TestWebApplicationFactory.cs
index 3bc0e810c3..12af759107 100644
--- a/TUnit.AspNetCore.Core/TestWebApplicationFactory.cs
+++ b/TUnit.AspNetCore.Core/TestWebApplicationFactory.cs
@@ -6,11 +6,13 @@
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Http;
+using OpenTelemetry.Trace;
using TUnit.AspNetCore.Extensions;
using TUnit.AspNetCore.Http;
using TUnit.AspNetCore.Interception;
using TUnit.AspNetCore.Logging;
using TUnit.Core;
+using TUnit.OpenTelemetry;
namespace TUnit.AspNetCore;
@@ -50,6 +52,11 @@ public WebApplicationFactory GetIsolatedFactory(
services.TryAddEnumerable(
ServiceDescriptor.Singleton());
}
+
+ if (options.AutoConfigureOpenTelemetry)
+ {
+ AddTUnitOpenTelemetry(services);
+ }
});
if (options.EnableHttpExchangeCapture)
@@ -94,6 +101,24 @@ protected override void ConfigureWebHost(IWebHostBuilder builder)
});
}
+ ///
+ /// Adds TUnit's default OpenTelemetry tracing configuration to :
+ /// the TUnit.AspNetCore.Http activity source, the
+ /// , and ASP.NET Core + HttpClient instrumentation.
+ /// Safe to call even if the SUT already registers these — OpenTelemetry de-duplicates them.
+ /// Also safe when combined with the TUnit.OpenTelemetry zero-config package: the
+ /// SUT and test-runner TracerProviders each carry their own processor, but the
+ /// processor's idempotent OnStart guard prevents duplicate tunit.test.id tags.
+ ///
+ private static void AddTUnitOpenTelemetry(IServiceCollection services)
+ {
+ services.AddOpenTelemetry().WithTracing(tracing => tracing
+ .AddSource(TUnitActivitySource.AspNetCoreHttpSourceName)
+ .AddProcessor(new TUnitTestCorrelationProcessor())
+ .AddAspNetCoreInstrumentation()
+ .AddHttpClientInstrumentation());
+ }
+
///
/// Controls whether every registered
/// has its StartAsync dispatched onto a thread-pool worker with a clean
diff --git a/TUnit.AspNetCore.Core/WebApplicationTestOptions.cs b/TUnit.AspNetCore.Core/WebApplicationTestOptions.cs
index 91050dd0fc..2c924f79df 100644
--- a/TUnit.AspNetCore.Core/WebApplicationTestOptions.cs
+++ b/TUnit.AspNetCore.Core/WebApplicationTestOptions.cs
@@ -22,4 +22,22 @@ public record WebApplicationTestOptions
///
///
public bool AutoPropagateHttpClientFactory { get; set; } = true;
+
+ ///
+ /// Gets or sets a value indicating whether the SUT's
+ /// should be automatically augmented with the TUnit HTTP activity source, the
+ /// TUnitTestCorrelationProcessor, and ASP.NET Core + HttpClient instrumentation.
+ /// Default is true.
+ ///
+ /// When enabled, test spans emitted inside the SUT are tagged with the ambient
+ /// tunit.test.id baggage so they remain queryable per-test in backends like
+ /// Seq or Jaeger, even when third-party libraries break the parent-chain.
+ ///
+ ///
+ /// Set to false to leave the SUT's OpenTelemetry configuration untouched —
+ /// useful if the SUT configures its own processors and you do not want TUnit's
+ /// defaults layered on top.
+ ///
+ ///
+ public bool AutoConfigureOpenTelemetry { get; set; } = true;
}
diff --git a/TUnit.AspNetCore.Tests/AutoConfigureOpenTelemetryTests.cs b/TUnit.AspNetCore.Tests/AutoConfigureOpenTelemetryTests.cs
new file mode 100644
index 0000000000..bac62309a3
--- /dev/null
+++ b/TUnit.AspNetCore.Tests/AutoConfigureOpenTelemetryTests.cs
@@ -0,0 +1,72 @@
+using System.Diagnostics;
+using Microsoft.Extensions.DependencyInjection;
+using OpenTelemetry.Trace;
+using TUnit.AspNetCore;
+using TUnit.Core;
+
+namespace TUnit.AspNetCore.Tests;
+
+///
+/// Coverage for thomhurst/TUnit#5594 —
+/// automatically augments the SUT's with TUnit's
+/// correlation processor + ASP.NET Core instrumentation.
+///
+///
+/// Serialized against sibling auto-wire tests because
+/// attaches a process-global per TracerProvider,
+/// so a parallel factory's correlation processor can tag activities created by another
+/// factory's SUT. Serializing keeps assertions observing only their own factory's wiring.
+///
+[NotInParallel(nameof(AutoConfigureOpenTelemetryTests))]
+public class AutoConfigureOpenTelemetryTests : WebApplicationTest
+{
+ private readonly List _exported = [];
+
+ protected override void ConfigureTestServices(IServiceCollection services)
+ {
+ services.AddOpenTelemetry().WithTracing(t => t.AddInMemoryExporter(_exported));
+ }
+
+ [Test]
+ public async Task AutoWires_TagsAspNetCoreSpans_WithTestId()
+ {
+ using var client = Factory.CreateClient();
+ var response = await client.GetAsync("/ping");
+ response.EnsureSuccessStatusCode();
+
+ var testId = TestContext.Current!.Id;
+ var taggedSpan = _exported.FirstOrDefault(a => (a.GetTagItem(TUnitActivitySource.TagTestId) as string) == testId);
+ await Assert.That(taggedSpan).IsNotNull();
+ }
+}
+
+[NotInParallel(nameof(AutoConfigureOpenTelemetryTests))]
+public class AutoConfigureOpenTelemetryOptOutTests : WebApplicationTest
+{
+ private readonly List _exported = [];
+
+ protected override void ConfigureTestOptions(WebApplicationTestOptions options)
+ {
+ options.AutoConfigureOpenTelemetry = false;
+ }
+
+ protected override void ConfigureTestServices(IServiceCollection services)
+ {
+ services.AddOpenTelemetry().WithTracing(t => t
+ .AddAspNetCoreInstrumentation()
+ .AddInMemoryExporter(_exported));
+ }
+
+ [Test]
+ public async Task OptOut_DoesNotTag_AspNetCoreSpans()
+ {
+ using var client = Factory.CreateClient();
+ var response = await client.GetAsync("/ping");
+ response.EnsureSuccessStatusCode();
+
+ foreach (var activity in _exported)
+ {
+ await Assert.That(activity.GetTagItem(TUnitActivitySource.TagTestId)).IsNull();
+ }
+ }
+}
diff --git a/TUnit.AspNetCore.Tests/TUnit.AspNetCore.Tests.csproj b/TUnit.AspNetCore.Tests/TUnit.AspNetCore.Tests.csproj
index ac3c24f9bc..8ae0077e6a 100644
--- a/TUnit.AspNetCore.Tests/TUnit.AspNetCore.Tests.csproj
+++ b/TUnit.AspNetCore.Tests/TUnit.AspNetCore.Tests.csproj
@@ -30,6 +30,10 @@
+
+
+
+
diff --git a/TUnit.Core/TUnitActivitySource.cs b/TUnit.Core/TUnitActivitySource.cs
index 9440d0009a..5f547cb713 100644
--- a/TUnit.Core/TUnitActivitySource.cs
+++ b/TUnit.Core/TUnitActivitySource.cs
@@ -12,6 +12,13 @@ public static class TUnitActivitySource
internal const string SourceName = "TUnit";
internal const string LifecycleSourceName = "TUnit.Lifecycle";
+ ///
+ /// Activity source emitted by TUnit's ASP.NET Core HTTP propagation handlers.
+ /// Registered automatically on the SUT's
+ /// listeners by TestWebApplicationFactory.
+ ///
+ public const string AspNetCoreHttpSourceName = "TUnit.AspNetCore.Http";
+
/// W3C baggage HTTP header name.
internal const string BaggageHeader = "baggage";
diff --git a/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet10_0.verified.txt b/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet10_0.verified.txt
index f06c61bcb3..0465a759f5 100644
--- a/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet10_0.verified.txt
+++ b/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet10_0.verified.txt
@@ -1330,6 +1330,7 @@ namespace
}
public static class TUnitActivitySource
{
+ public const string AspNetCoreHttpSourceName = ".Http";
public const string TagTestId = ".id";
}
public class TUnitAttribute : { }
diff --git a/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet8_0.verified.txt b/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet8_0.verified.txt
index 4fa9b816b6..2bc5a56bc9 100644
--- a/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet8_0.verified.txt
+++ b/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet8_0.verified.txt
@@ -1330,6 +1330,7 @@ namespace
}
public static class TUnitActivitySource
{
+ public const string AspNetCoreHttpSourceName = ".Http";
public const string TagTestId = ".id";
}
public class TUnitAttribute : { }
diff --git a/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet9_0.verified.txt b/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet9_0.verified.txt
index f9f244f3d2..979d9a4793 100644
--- a/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet9_0.verified.txt
+++ b/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet9_0.verified.txt
@@ -1330,6 +1330,7 @@ namespace
}
public static class TUnitActivitySource
{
+ public const string AspNetCoreHttpSourceName = ".Http";
public const string TagTestId = ".id";
}
public class TUnitAttribute : { }
diff --git a/docs/docs/examples/aspnet.md b/docs/docs/examples/aspnet.md
index c58e88bb21..568daf2871 100644
--- a/docs/docs/examples/aspnet.md
+++ b/docs/docs/examples/aspnet.md
@@ -86,6 +86,7 @@ public class TodoApiTests : TestsBase
- **Client-side tracing**: `CreateClient()` / `CreateDefaultClient()` return an `HttpClient` that propagates `traceparent`, `baggage`, and `X-TUnit-TestId` headers to the SUT.
- **SUT `IHttpClientFactory` tracing**: Every pipeline built inside the SUT via `AddHttpClient()`, named clients, or typed clients also gets those headers prepended — outbound calls from your app to downstream services correlate with the originating test. Opt out per-test with `WebApplicationTestOptions.AutoPropagateHttpClientFactory = false`.
+- **SUT-side OpenTelemetry**: The SUT's `TracerProvider` is augmented with the `TUnit.AspNetCore.Http` activity source, the `TUnitTestCorrelationProcessor` (stamps the `tunit.test.id` baggage item onto every span as a tag), and ASP.NET Core + HttpClient instrumentation. Spans emitted inside the SUT stay queryable per-test in backends like Jaeger or Seq, even when third-party libraries break the parent-chain. Opt out per-test with `WebApplicationTestOptions.AutoConfigureOpenTelemetry = false`.
- **Correlated logging**: Server-side `ILogger` output is routed to the test that triggered the request.
- **Hosted-service context hygiene**: `IHostedService.StartAsync` runs under `ExecutionContext.SuppressFlow()` so background work doesn't inherit the first test's `Activity.Current`.
diff --git a/docs/docs/examples/opentelemetry.md b/docs/docs/examples/opentelemetry.md
index 099b6d6c8c..f0f09d75e1 100644
--- a/docs/docs/examples/opentelemetry.md
+++ b/docs/docs/examples/opentelemetry.md
@@ -258,8 +258,15 @@ dotnet add package OpenTelemetry.Exporter.Zipkin
If you use `TestWebApplicationFactory` or `TracedWebApplicationFactory`, outgoing requests
automatically propagate the current test trace via W3C `traceparent` and `baggage` headers.
-Add `"TUnit.AspNetCore.Http"` as a source only if you also want TUnit's synthetic client spans
-to appear in your exporter. Header propagation works either way.
+The factory also augments the SUT's `TracerProvider` automatically — no manual `services.AddOpenTelemetry().WithTracing(...)` wiring is needed for the basics:
+
+- Registers the `TUnit.AspNetCore.Http` activity source.
+- Adds the `TUnitTestCorrelationProcessor` so spans from libraries with broken parent chains are still tagged with `tunit.test.id`.
+- Adds ASP.NET Core and HttpClient instrumentation.
+
+Your own `WithTracing` callback on the SUT is preserved; TUnit's defaults are layered on top. If you configure your own exporter (OTLP, Jaeger, Zipkin, in-memory), test spans flow straight through it.
+
+Set `WebApplicationTestOptions.AutoConfigureOpenTelemetry = false` per-test to opt out — useful if the SUT owns its own processors and you don't want TUnit's defaults layered on top.
## Test Context Correlation via Activity Baggage
diff --git a/docs/docs/guides/distributed-tracing.md b/docs/docs/guides/distributed-tracing.md
index be75cb986b..8a68d346f1 100644
--- a/docs/docs/guides/distributed-tracing.md
+++ b/docs/docs/guides/distributed-tracing.md
@@ -150,6 +150,12 @@ protected override void ConfigureTestOptions(WebApplicationTestOptions options)
}
```
+### SUT-side OpenTelemetry wiring
+
+`TestWebApplicationFactory` also augments the SUT's `TracerProvider` automatically — the `TUnit.AspNetCore.Http` activity source, the `TUnitTestCorrelationProcessor`, and ASP.NET Core + HttpClient instrumentation are layered on top of whatever `AddOpenTelemetry().WithTracing(...)` wiring the SUT already has. That means spans emitted inside the SUT stay queryable per-test (`tunit.test.id` tag) even when third-party libraries break the parent chain.
+
+Opt out per-test with `WebApplicationTestOptions.AutoConfigureOpenTelemetry = false` when the SUT owns its own processors and you don't want TUnit's defaults layered on top.
+
### Raw `HttpClient`
`new HttpClient()` can't be intercepted. Either route through `IHttpClientFactory` or set the `traceparent` header manually.