Skip to content

[TUnit.Aspire] Forward Aspire resource logs into captured test output (opt-in) #6341

Description

@thomhurst

[TUnit.Aspire] Forward Aspire resource logs into captured test output (opt-in)

Labels: enhancement, TUnit.Aspire

Problem

AspireFixture<TAppHost> captures OpenTelemetry today, but a resource's raw stdout/stderr is never surfaced to the test. Aspire does not pipe child-resource console output to the test process, so when something goes wrong inside a resource, the test author sees only resource state transitions — not why the resource misbehaved.

This bites hardest at startup: a resource that throws during boot frequently exits before its OTel exporter flushes, and host-builder/DI/config failures are written to stderr, not OTel. So OTel capture has a blind spot at the single most common Aspire-test failure — "a resource won't come up."

Real repro (Aspire 13.4.6, TUnit.Aspire 1.57.14)

A full-fleet E2E fixture timed out after 480s with a generic "Timed out waiting for the Aspire application to start." The actual cause was one service (chat) crashing ~40s into boot with an unhandled exception (exit 0xE0434352). Nothing about that exception reached the test output — diagnosing it required a hand-written throwaway harness that tapped ResourceLoggerService.WatchAsync from StartAsync.

Proposal

Add an opt-in option to forward every resource's logs into the fixture's captured output, subscribed from StartAsync (so logs of resources that later exit are still captured, because capture is live rather than fetched post-mortem).

// Option A: fixture option
public class MyFixture : AspireFixture<Projects.AppHost>
{
    protected override AspireFixtureOptions Options => new() { ForwardResourceLogs = true };
}

// Option B: attribute
[CaptureResourceLogs]           // all resources
[CaptureResourceLogs("chat", "api")]  // subset

Behaviour:

  • For each (selected) resource, subscribe ResourceLoggerService.WatchAsync(name) right after the app starts.
  • Write each line to the fixture's output writer (TestContext.Current?.OutputWriter / Console) prefixed with the resource name, e.g. [chat] <line>.
  • Because TUnit already captures per-test console output into the result/report, the resource logs then appear in the failing test's report for free — including for resources that crash mid-boot.

Reference implementation (proven to capture the crash the fixture missed)

var loggerService = app.Services.GetRequiredService<ResourceLoggerService>();
foreach (var resource in selectedResources)
{
    _ = Task.Run(async () =>
    {
        await foreach (var batch in loggerService.WatchAsync(resource).WithCancellation(ct))
            foreach (var line in batch)
                context.OutputWriter.WriteLine($"[{resource}] {line}");
    }, ct);
}

Acceptance criteria

  • Opt-in (default off) — no behaviour change for existing users.
  • Forwarded logs appear in the captured output of the test that owns the fixture lifecycle.
  • Logs of a resource that exits during startup are still captured (live subscription, not post-mortem fetch).
  • Resource-name prefixing; optional include/exclude filter.

Related

  • Depends-on/foundation for the fail-fast-on-crash issue (attributed startup-failure messages).
  • Removes the need for consumers to hand-roll ResourceLoggerService dumps (which race and return "(no logs available)" for exited resources).

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions