feat: generalize OTLP receiver for use outside TUnit.Aspire#5606
feat: generalize OTLP receiver for use outside TUnit.Aspire#5606
Conversation
Extracts OtlpReceiver + parsers from TUnit.Aspire to TUnit.OpenTelemetry and auto-starts them at test discovery. Out-of-process SUTs (spawned processes, testcontainers, external services) can now export spans into TUnit's HTML report via OTLP/HTTP without Aspire. ActivityCollector gains RegisterExternalTrace/IngestExternalSpan and a process-wide Current pointer so the receiver can route spans without explicit wiring. Unknown trace IDs are dropped; registered traces are capped at 100 external spans. Opt out with TUNIT_OTEL_RECEIVER=0. Adds OtlpTraceParser (field-by-field protobuf parser, no external dep) so incoming /v1/traces are ingested, not just forwarded. Closes #5595
Not up to standards ⛔🔴 Issues
|
| Category | Results |
|---|---|
| Security | 1 critical 1 high |
| CodeStyle | 2 minor |
🟢 Metrics 266 complexity
Metric Results Complexity 266
TIP This summary will be updated as you push new changes. Give us feedback
There was a problem hiding this comment.
Code Review — PR #5606: Generalize OTLP receiver for use outside TUnit.Aspire
Overall this is a well-motivated refactor that makes the OTLP receiver broadly useful. The extraction from TUnit.Aspire, the opt-out env var, and the test coverage are all solid. A few architectural concerns are worth addressing before merge.
Major: Tight coupling via InternalsVisibleTo + static singleton
The core design issue is that TUnit.OpenTelemetry (a library) reaches into TUnit.Engine internals via ActivityCollector.Current?.IngestExternalSpan(span). This requires InternalsVisibleTo entries in both TUnit.Core and TUnit.Engine, and creates a compile-time dependency from the library to the runner's internal types.
This inverts the correct dependency direction. The engine is the host; the library should not know about the engine's internals.
Better approach: Define a public interface in TUnit.Core (already a dependency of both):
// TUnit.Core/IOtlpSpanSink.cs
public interface IOtlpSpanSink
{
void IngestExternalSpan(SpanData span);
void RegisterExternalTrace(string traceId);
}ActivityCollector implements it. OtlpReceiver accepts an IOtlpSpanSink? callback (injected by AutoReceiver, which is allowed to see the engine). This eliminates the static singleton, removes the InternalsVisibleTo hacks, and makes the data flow explicit.
Major: ActivityCollector._current is a mutable process-wide static
private static ActivityCollector? _current;
public static ActivityCollector? Current => _current;Two concerns:
-
Thread safety: The PR description mentions
Interlocked.CompareExchangebut the field is just directly assigned. A naive assignment is torn on 32-bit platforms (unlikely in practice, but the intent was an atomic swap). Either useVolatile.Write/Volatile.ReadorInterlocked.Exchangeconsistently. -
Multiple sessions in one process: If the host ever runs back-to-back test sessions in the same process (e.g., in test-on-save tooling),
Stop()clears_currentbut a newStart()must win the race before the next session emits spans. The current code handles this with a null check inStart(), but stopping the receiver and clearing_currentare two separate steps — a window exists where a span from the new session is dropped.
Both issues go away with the interface injection approach above.
Moderate: Hand-rolled 429-line protobuf parser
OtlpTraceParser.cs is a significant maintenance liability:
- Field numbers are magic integers (
case 1,case 9,case 15) with no reference to the proto field names. Future maintainers cannot easily verify correctness. ParseAnyValuesilently skipsarray_value(field 5) andkvlist_value(field 6), which are legal in OTLP attributes. A span with a nested-array attribute produces an empty string with no warning.- If the OTLP spec evolves (new wire fields, packed repeated fields for timestamps), this parser will silently misparse.
At minimum, add a link to the proto definition in a header comment:
// Parses opentelemetry.proto.trace.v1.ExportTraceServiceRequest
// Spec: https://github.com/open-telemetry/opentelemetry-proto/blob/main/opentelemetry/proto/trace/v1/trace.protoLonger term, consider consuming OpenTelemetry.Proto.* NuGet packages if they're already transitive — they provide generated, versioned types with no parsing risk.
Minor: Hardcoded cap of 100 is silent data loss
private const int MaxExternalSpansPerTest = 100;
private const int MaxExternalSpansPerTrace = 100;A SUT with many child spans (e.g., a database-heavy integration test) silently loses spans beyond 100 with no log warning and no way to raise the limit. Consider:
- Adding a one-time warning log when the cap is hit:
"[TUnit.OpenTelemetry] External span cap (100) reached for trace {traceId}; additional spans dropped." - Making the limit configurable via environment variable (consistent with the
TUNIT_OTEL_RECEIVER=0opt-out pattern).
Minor: Duplicate test helper
OtlpTraceCaptureServer exists in both TUnit.Aspire.Tests/Helpers/ and TUnit.OpenTelemetry.Tests/Helpers/ with the same interface but different implementations. The Aspire one was simplified to call LoopbackHttpListenerFactory.Create(), so the duplication is already partially consolidated — but the capture/wait logic is still duplicated. Consider moving it to a TUnit.OpenTelemetry.Testing or TUnit.TestUtilities shared project.
Positive notes
- The
LoopbackHttpListenerFactoryextraction to deduplicate port-binding retry logic is clean. [Before(HookType.TestDiscovery)]ordering ensures the receiver is up before any test-startup code runs.- The
TUNIT_OTEL_RECEIVER=0opt-out is idiomatic and discoverable. - Test coverage for cap enforcement and unknown-trace dropping is exactly the right edge-case coverage.
- Docs section with end-to-end example is helpful.
Summary: The extraction is the right architectural move. The main ask is to decouple TUnit.OpenTelemetry from TUnit.Engine internals — the InternalsVisibleTo+static-singleton coupling is the kind of thing that compounds over time. The interface injection approach resolves that and the thread-safety concern together.
- Add OTLP proto spec links and field-name comments to OtlpTraceParser and OtlpLogParser so magic field numbers can be cross-referenced. - Make external span cap overridable via TUNIT_OTEL_MAX_EXTERNAL_SPANS env var and emit a one-time stderr warning when the cap is first hit.
|
Thanks for the review. Addressed two concrete items in 3e73e07; responding inline to the rest. Fixed
Discussing
|
There was a problem hiding this comment.
Code Review
Thanks for addressing the previous feedback — the configurable span cap with a one-time warning is a nice UX touch, and the proto spec URL references make the hand-rolled parser much more maintainable.
Bug: Case-sensitivity mismatch in RegisterExternalTrace
Convert.ToHexString() (used in the parser to decode span/trace IDs) returns uppercase hex. Activity.TraceId.ToString() returns lowercase hex. The docs tell users to call .ToUpperInvariant(), but if they don't, spans are silently dropped with no warning or error — a hard-to-diagnose footgun.
The fix belongs in the API, not in the caller:
// ActivityCollector.cs
internal void RegisterExternalTrace(string traceId)
{
_knownTraceIds.TryAdd(traceId.ToUpperInvariant(), 0);
}Or initialise the dictionary with StringComparer.OrdinalIgnoreCase. Either way, callers shouldn't need to know that the parser produces uppercase hex internally.
Design question: SpanType = span.Name in ToSpanData
In OtlpReceiver.cs:
return new SpanData
{
Name = span.Name,
SpanType = span.Name, // same value as Name?
Kind = MapSpanKind(span.Kind),
...
};If the HTML report renders SpanType as a separate column or label from Name, this is redundant/confusing. If SpanType is meant to communicate the instrumentation scope or kind, it should be span.ScopeName or MapSpanKind(span.Kind). Worth a quick check against how the HTML template consumes SpanType on in-process spans to confirm the right mapping.
Addressed from previous round ✅
- Proto spec URLs added to
OtlpTraceParser/OtlpLogParser— excellent TUNIT_OTEL_MAX_EXTERNAL_SPANSoverride + one-timestderrwarning — good UXLoopbackHttpListenerFactoryextraction eliminates the duplicated port-probing logic cleanly
Author's responses acknowledged
The defence of the IVT + Interlocked.CompareExchange-based singleton is reasonable given that TUnit.OpenTelemetry and TUnit.Engine ship together as sibling assemblies. The one-session-one-collector invariant is a sound constraint, and the CompareExchange already achieves first-wins semantics correctly.
The case-sensitivity issue above is the only concrete bug I can see in the new code. Everything else looks solid.
- Move TUNIT_OTEL_MAX_EXTERNAL_SPANS into EnvironmentConstants so the env var name isn't duplicated across resolver and warning message. - Collapse the MaxExternalSpansPerTest/MaxExternalSpansPerTrace alias pair into a single MaxExternalSpans field — per-test vs per-trace distinction is already clear at the use sites. - Use Interlocked.CompareExchange for the one-shot warning latch so we don't re-write the flag on every dropped span once the cap is hit.
There was a problem hiding this comment.
Code Review — feat: generalize OTLP receiver for use outside TUnit.Aspire
This is a well-structured PR that delivers real value: non-Aspire tests can now capture out-of-process spans into the HTML report with near-zero ceremony. The extraction of OtlpReceiver/OtlpLogParser into TUnit.OpenTelemetry, the hand-rolled OtlpTraceParser (no external proto dep, AOT-safe), the LoopbackHttpListenerFactory refactor, the Interlocked.CompareExchange one-shot warning latch, and the configurable cap via env var are all solid. Test coverage is thorough. One real bug and a few architectural points below.
Bug: span ID case mismatch in IngestExternalSpan
_testCaseSpanIds is populated with lowercase keys:
// ActivityCollector.cs line 168
_testCaseSpanIds.TryAdd(activity.SpanId.ToString(), 0);
// ^^ lowercase hexBut OtlpTraceParser produces uppercase IDs:
// OtlpTraceParser.cs — fields 2 and 4
parentSpanId = Convert.ToHexString(parentBytes); // UPPERCASEThe per-test cap check in IngestExternalSpan therefore never matches for OTLP-received spans:
if (span.ParentSpanId is { } parentSpanId && _testCaseSpanIds.ContainsKey(parentSpanId))
// ^^ dict keyed lowercase
// parentSpanId is UPPERCASE ^^All OTLP-external spans silently fall through to the per-trace cap path. The behaviour is still bounded, but the per-test capping granularity is wrong and any future code that relies on this path will be confused.
Note that _knownTraceIds already uses StringComparer.OrdinalIgnoreCase to handle exactly this kind of mismatch — the same fix should apply to _testCaseSpanIds and _externalSpanCountsByTest. Alternatively, normalize Convert.ToHexString calls in OtlpTraceParser to lowercase to stay consistent with how Activity returns IDs.
Architectural concern: layer violation via InternalsVisibleTo
OtlpReceiver now references TUnit.Engine internal types (ActivityCollector, SpanData, ReportKeyValue, SpanEvent, SpanLink) directly. This means:
- Any change to those types breaks
TUnit.OpenTelemetry TUnit.OpenTelemetrycan never be shipped independently ofTUnit.Engine's internal API stability- The
ToSpanDatamapper inOtlpReceiver.csis a 60-line method that knows the HTML reporter's internal shape
A better approach would be a thin IExternalSpanSink interface in TUnit.Engine (or TUnit.Core) that ActivityCollector implements:
public interface IExternalSpanSink
{
void RegisterExternalTrace(string traceId);
void IngestExternalSpan(string traceId, string spanId, string? parentSpanId, string name, /* ... */);
}Then OtlpReceiver takes an IExternalSpanSink? (defaulting to ActivityCollector.Current) and the ToSpanData mapping stays in TUnit.Engine where it belongs. This doesn't have to block this PR but is worth noting before more code builds on the current coupling.
Silent exception swallowing in ProcessTraces
try { spans = OtlpTraceParser.Parse(body); }
catch { return; } // no diagnostic outputCompare with the existing ForwardAsync pattern which emits Trace.WriteLine. A malformed payload would be silently dropped with no way to diagnose it. Even a low-cost Trace.WriteLine($"[TUnit.OpenTelemetry] Failed to parse /v1/traces body: {ex.GetType().Name}") would make debugging far easier.
Minor: [EditorBrowsable] on an internal member
[EditorBrowsable(EditorBrowsableState.Never)]
internal static bool HasReceiverForTesting { get; }internal already prevents external consumers from seeing this. [EditorBrowsable] only suppresses IDE completion for public members — on internal it is a no-op. This can just be internal.
Summary
| Bug | Span ID case mismatch makes per-test cap path dead code for OTLP spans — needs fix |
| Architecture | InternalsVisibleTo coupling is workable but worth an interface before it grows |
| Minor | Silent parse failure in ProcessTraces; redundant [EditorBrowsable] |
The core feature is sound and the test suite covers the important paths. The case-mismatch bug is the only thing that needs addressing before merge.
OTLP parser emitted uppercase hex IDs (via Convert.ToHexString) while System.Diagnostics.Activity serializes lowercase. That meant external spans landed in a separate trace bucket from in-process spans on the same logical trace, the per-test cap never matched a test case span ID, and users had to know to call .ToUpperInvariant() when registering traces manually. - Parser now emits lowercase via a HexLower helper (uses ToHexStringLower on net9+, falls back to ToLowerInvariant on net8) so IDs round-trip with Activity's format without caller ceremony. - All trace/span ID dictionaries in ActivityCollector are now OrdinalIgnoreCase as defense-in-depth against any remaining mixed-case caller. - SpanType no longer duplicates Name for external spans — it's a TUnit-only classifier and has no OTLP analogue. - Regression test IngestExternalSpan_TraceIdCaseMismatch_StillCorrelates. - Docs and existing tests updated to drop the now-unnecessary ToUpperInvariant.
|
Thanks — both bugs are real. Fixed in d76bfee. Case-sensitivity — You're right that As defense-in-depth I also flipped the remaining trace/span-ID dicts in
|
There was a problem hiding this comment.
Code Review — feat: generalize OTLP receiver for use outside TUnit.Aspire (final pass)
All three bugs from previous rounds have been cleanly resolved in d76bfee. The PR is in good shape. A couple of small stragglers worth a quick fix before merge.
Previous issues — resolved ✅
- Case-sensitivity mismatch — Fixed correctly. helper produces lowercase to match serialization; all dicts in
ActivityCollectorflipped toOrdinalIgnoreCaseas defense-in-depth. The regression testIngestExternalSpan_TraceIdCaseMismatch_StillCorrelatescovers exactly the right scenario. SpanType = span.Name— Fixed. NowSpanType = nullfor external spans, which is correct since the OTLP spec has no equivalent concept.- Configurable span cap —
TUNIT_OTEL_MAX_EXTERNAL_SPANS+EnvironmentConstantscentral home +Interlocked.CompareExchangeone-shot warning latch — all good. - Architecture (IVT + singleton) — Author's rationale stands for sibling assemblies shipping together.
CompareExchange-based first-wins semantics are already in place.
Remaining: silent exception swallowing in ProcessTraces
This was raised in the third review and wasn't addressed:
try { spans = OtlpTraceParser.Parse(body); }
catch { return; }Every other error path in OtlpReceiver emits a Trace.WriteLine diagnostic. A malformed payload silently disappears here with no trace. One line is all that's needed:
catch (Exception ex)
{
Trace.WriteLine($"[TUnit.OpenTelemetry] Failed to parse /v1/traces body: {ex.GetType().Name}: {ex.Message}");
return;
}Remaining: [EditorBrowsable] on an internal member
[EditorBrowsable(EditorBrowsableState.Never)]
internal static bool HasReceiverForTesting { get; }[EditorBrowsable] only suppresses IDE completion for public members. On internal it is a complete no-op — the IDE already hides internal from external projects by default. The attribute can just be removed.
Minor: cap test hardcodes default
// ActivityCollectorIngestionTests.cs
await Assert.That(ours.Count).IsEqualTo(100); // hardcoded defaultIf TUNIT_OTEL_MAX_EXTERNAL_SPANS is set in the CI environment this test will fail with a confusing mismatch. Reading from DefaultMaxExternalSpans (or making the const accessible to tests) would make the test environment-independent.
Summary
| Bug | None remaining |
| Two-line fixes | Silent catch in ProcessTraces; remove no-op [EditorBrowsable] |
| Nice-to-have | Cap test against constant rather than literal 100 |
Good feature — solid approach, thorough test coverage, and the case-fix correctly roots the problem in the parser rather than patching callers. The two remaining items are small enough they could go in a follow-up, but the catch {} in particular takes 30 seconds to fix and pays off in the first time someone hits a corrupt payload in CI.
There was a problem hiding this comment.
Code Review — feat: generalize OTLP receiver for use outside TUnit.Aspire (final pass)
All three bugs from previous rounds have been cleanly resolved in d76bfee. The PR is in good shape. A couple of small stragglers worth a quick fix before merge.
Previous issues — resolved
- Case-sensitivity mismatch — Fixed correctly.
HexLowerhelper produces lowercase to matchActivityserialization; all dicts inActivityCollectorflipped toOrdinalIgnoreCaseas defense-in-depth. The regression testIngestExternalSpan_TraceIdCaseMismatch_StillCorrelatescovers exactly the right scenario. SpanType = span.Name— Fixed. NowSpanType = nullfor external spans, which is correct since the OTLP spec has no equivalent concept.- Configurable span cap —
TUNIT_OTEL_MAX_EXTERNAL_SPANS+EnvironmentConstantscentral home +Interlocked.CompareExchangeone-shot warning latch — all good. - Architecture (IVT + singleton) — Author's rationale stands for sibling assemblies shipping together.
CompareExchange-based first-wins semantics are already in place.
Remaining: silent exception swallowing in ProcessTraces
This was raised in the third review and wasn't addressed:
try { spans = OtlpTraceParser.Parse(body); }
catch { return; }Every other error path in OtlpReceiver emits a Trace.WriteLine diagnostic. A malformed payload silently disappears here with no trace. One line is all that's needed:
catch (Exception ex)
{
Trace.WriteLine($"[TUnit.OpenTelemetry] Failed to parse /v1/traces body: {ex.GetType().Name}: {ex.Message}");
return;
}Remaining: [EditorBrowsable] on an internal member
[EditorBrowsable(EditorBrowsableState.Never)]
internal static bool HasReceiverForTesting { get; }[EditorBrowsable] only suppresses IDE completion for public members. On internal it is a complete no-op — the IDE already hides internal from external projects by default. The attribute can just be removed.
Minor: cap test hardcodes default
// ActivityCollectorIngestionTests.cs
await Assert.That(ours.Count).IsEqualTo(100); // hardcoded defaultIf TUNIT_OTEL_MAX_EXTERNAL_SPANS is set in the CI environment this test will fail with a confusing mismatch. Reading from DefaultMaxExternalSpans (or making the const accessible to tests) would make the test environment-independent.
Summary
| Bugs | None remaining |
| Two-line fixes | Silent catch in ProcessTraces; remove no-op [EditorBrowsable] |
| Nice-to-have | Cap test against constant rather than literal 100 |
Good feature — solid approach, thorough test coverage, and the case-fix correctly roots the problem in the parser rather than patching callers. The two remaining items are small enough they could go in a follow-up, but the catch {} in particular takes 30 seconds to fix and pays off the first time someone hits a corrupt payload in CI.
- Log parse failures in ProcessTraces/ProcessLogs via Trace.WriteLine instead of swallowing silently, matching the rest of OtlpReceiver's error paths. - Drop the [EditorBrowsable(Never)] attribute on the internal HasReceiverForTesting property — it's a no-op on internal members. - Expose ActivityCollector.MaxExternalSpans internal so the cap test reads the runtime value instead of a hardcoded 100, keeping it correct when TUNIT_OTEL_MAX_EXTERNAL_SPANS is set in the environment.
|
All three addressed in cd5c6b0.
|
Updated [TUnit](https://github.com/thomhurst/TUnit) from 1.35.2 to 1.37.10. <details> <summary>Release notes</summary> _Sourced from [TUnit's releases](https://github.com/thomhurst/TUnit/releases)._ ## 1.37.10 <!-- Release notes generated using configuration in .github/release.yml at v1.37.10 --> ## What's Changed ### Other Changes * docs(test-filters): add migration callout for --filter → --treenode-filter by @johnkattenhorn in thomhurst/TUnit#5628 * fix: re-enable RPC tests and modernize harness (#5540) by @thomhurst in thomhurst/TUnit#5632 * fix(mocks): propagate [Obsolete] and null-forgiving raise dispatch (#5626) by @JohnVerheij in thomhurst/TUnit#5631 * ci: use setup-dotnet built-in NuGet cache by @thomhurst in thomhurst/TUnit#5635 * feat(playwright): propagate W3C trace context into browser contexts by @thomhurst in thomhurst/TUnit#5636 ### Dependencies * chore(deps): update tunit to 1.37.0 by @thomhurst in thomhurst/TUnit#5625 ## New Contributors * @johnkattenhorn made their first contribution in thomhurst/TUnit#5628 * @JohnVerheij made their first contribution in thomhurst/TUnit#5631 **Full Changelog**: thomhurst/TUnit@v1.37.0...v1.37.10 ## 1.37.0 <!-- Release notes generated using configuration in .github/release.yml at v1.37.0 --> ## What's Changed ### Other Changes * fix: stabilize flaky tests across analyzer, OTel, and engine suites by @thomhurst in thomhurst/TUnit#5609 * perf: engine hot-path allocation wins (#5528 B) by @thomhurst in thomhurst/TUnit#5610 * feat(analyzers): detect collection IsEqualTo reference equality (TUnitAssertions0016) by @thomhurst in thomhurst/TUnit#5615 * perf: consolidate test dedup + hook register guards (#5528 A) by @thomhurst in thomhurst/TUnit#5612 * perf: engine discovery/init path cleanup (#5528 C) by @thomhurst in thomhurst/TUnit#5611 * fix(assertions): render collection contents in IsEqualTo failure messages (#5613 B) by @thomhurst in thomhurst/TUnit#5619 * feat(analyzers): code-fix for TUnit0015 to insert CancellationToken (#5613 D) by @thomhurst in thomhurst/TUnit#5621 * fix(assertions): add Task reference forwarders on AsyncDelegateAssertion by @thomhurst in thomhurst/TUnit#5618 * test(asp-net): fix race in FactoryMethodOrderTests by @thomhurst in thomhurst/TUnit#5623 * feat(analyzers): code-fix for TUnit0049 to insert [MatrixDataSource] (#5613 C) by @thomhurst in thomhurst/TUnit#5620 * fix(pipeline): isolate AOT publish outputs to stop clobbering pack DLLs (#5622) by @thomhurst in thomhurst/TUnit#5624 ### Dependencies * chore(deps): update tunit to 1.36.0 by @thomhurst in thomhurst/TUnit#5608 * chore(deps): update modularpipelines to 3.2.8 by @thomhurst in thomhurst/TUnit#5614 **Full Changelog**: thomhurst/TUnit@v1.36.0...v1.37.0 ## 1.36.0 <!-- Release notes generated using configuration in .github/release.yml at v1.36.0 --> ## What's Changed ### Other Changes * fix: don't render test's own trace as Linked Trace in HTML report by @thomhurst in thomhurst/TUnit#5580 * fix(docs): benchmark index links 404 by @thomhurst in thomhurst/TUnit#5587 * docs: replace repeated benchmark link suffix with per-test descriptions by @thomhurst in thomhurst/TUnit#5588 * docs: clearer distributed tracing setup and troubleshooting by @thomhurst in thomhurst/TUnit#5597 * fix: auto-suppress ExecutionContext flow for hosted services (#5589) by @thomhurst in thomhurst/TUnit#5598 * feat: auto-align DistributedContextPropagator to W3C by @thomhurst in thomhurst/TUnit#5599 * feat: TUnit0064 analyzer + code fix for direct WebApplicationFactory inheritance by @thomhurst in thomhurst/TUnit#5601 * feat: auto-propagate test trace context through IHttpClientFactory by @thomhurst in thomhurst/TUnit#5603 * feat: TUnit.OpenTelemetry zero-config tracing package by @thomhurst in thomhurst/TUnit#5602 * fix: restore [Obsolete] members removed in v1.27 (#5539) by @thomhurst in thomhurst/TUnit#5605 * feat: generalize OTLP receiver for use outside TUnit.Aspire by @thomhurst in thomhurst/TUnit#5606 * feat: auto-configure OpenTelemetry in TestWebApplicationFactory SUT by @thomhurst in thomhurst/TUnit#5607 ### Dependencies * chore(deps): update tunit to 1.35.2 by @thomhurst in thomhurst/TUnit#5581 * chore(deps): update dependency typescript to ~6.0.3 by @thomhurst in thomhurst/TUnit#5582 * chore(deps): update dependency coverlet.collector to v10 by @thomhurst in thomhurst/TUnit#5600 **Full Changelog**: thomhurst/TUnit@v1.35.2...v1.36.0 Commits viewable in [compare view](thomhurst/TUnit@v1.35.2...v1.37.10). </details> [](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) --- <details> <summary>Dependabot commands and options</summary> <br /> You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show <dependency name> ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) </details> Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Summary
OtlpReceiver+OtlpLogParserfromTUnit.AspireintoTUnit.OpenTelemetry, addsOtlpTraceParser, and auto-starts the receiver at test discovery so non-Aspire tests (plainWebApplicationFactory, spawned processes, testcontainers, external services) can export spans into TUnit's HTML report over OTLP/HTTP.ActivityCollectorgainsRegisterExternalTrace/IngestExternalSpan+ a process-wideCurrentpointer so the receiver routes spans without explicit wiring. Unknown trace IDs are dropped; registered traces capped at 100 external spans. Opt out withTUNIT_OTEL_RECEIVER=0.distributed-tracing.md; comparison table updated.Closes #5595. Builds on #5602.
Test plan
TUnit.OpenTelemetry.Tests— 27 tests, 3 consecutive runs green (covers ingestion, caps, auto-start hook, OTLP parser, receiver round-trip, upstream forwarding).TUnit.Aspire+TUnit.Aspire.Testsstill build — Aspire consumes the extracted receiver via the new namespace.