feat(aspire): opt-in live forwarding of resource logs into test output (#6341)#6345
Conversation
#6341) AspireFixture captures OpenTelemetry but never surfaces a resource's raw stdout/stderr. The blind spot is worst at startup: a resource that crashes during boot exits before its OTel exporter flushes, and host/DI/config errors go to stderr, not OTel — so "a resource won't come up" failures show only a generic startup timeout, not the cause. Add an opt-in AspireFixtureOptions.ForwardResourceLogs. When on, subscribe to each selected resource's ResourceLoggerService.WatchAsync *before* StartAsync (a boot crash makes StartAsync throw/hang, so a post-start subscribe would never run; WatchAsync replays the backlog so early lines are still delivered) and forward every line, prefixed " [name] line", into the owning test's captured output. - New AspireFixtureOptions bag, exposed via virtual Options; existing knobs untouched. - ResourceLogNames selects a subset; empty = every ShouldWaitForResource pick. - Pump runs on a background thread where TestContext.Current (AsyncLocal) does not flow — capture the owner context once; fall back to stderr when init runs outside a test. - Refactor WatchResourceLogs to share PumpResourceLogsAsync. - Cancel pumps first in teardown so nothing writes to a finished test's output. - Pure (no-Docker) tests for SelectResourceLogNames.
Up to standards ✅🟢 Issues
|
| Metric | Results |
|---|---|
| Complexity | 11 |
NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.
Code reviewNo issues found. Checked for bugs and CLAUDE.md compliance. Reviewed
🤖 Generated with Claude Code |
- Hoist the owner-null decision out of the per-line path: bind the write sink once per resource pump instead of branching every line; drop the now-dead WriteForwardedLine helper. - Call the async pump directly instead of wrapping in Task.Run — it yields at its first await, so pumps still run concurrently without a thread-pool hop. - Make _options a local (only read in InitializeAsync); tighten teardown. - Consolidate the duplicated fake IResource classes (this test file and WaitForHealthyReproductionTests) into Helpers/FakeResources.cs.
Code review (updated after latest push)Re-reviewed the latest commit ( The follow-up commit is a clean simplification, verified against the surrounding code:
No dual-mode, snapshot, VSTest, or AOT/reflection-annotation concerns — this stays scoped to 🤖 Generated with Claude Code |
Closes #6341.
Problem
AspireFixture<TAppHost>captures OpenTelemetry but never surfaces a resource's raw stdout/stderr. The blind spot is worst at startup: a resource that throws during boot exits before its OTel exporter flushes, and host-builder/DI/config failures go to stderr, not OTel. So "a resource won't come up" — the single most common Aspire-test failure — shows only a generic startup timeout, never the cause.Main already has a post-mortem error-log dump on failure (
DumpResourceLogsOnFailure, #6323), but it fetches after the fact and races to "(no logs available)" for already-exited resources. This adds a live, opt-in forward.What changed
AspireFixtureOptionsbag, exposed viaprotected virtual AspireFixtureOptions Options. Existing virtual knobs untouched.ResourceLoggerService.WatchAsyncand forwards every line — prefixed[name] line— into the owning test's captured output.ResourceLogNamesselects a subset; null/empty = every resourceShouldWaitForResourcepicks.Key correctness points
StartAsync, not after. A boot crash makesStartAsyncthrow/hang (that is the failure), so the issue's post-start reference impl would never subscribe.WatchAsyncreplays the buffered backlog, so the earliest lines are still delivered.TestContext.Current(an AsyncLocal) does not flow. Capture the owner context once at subscribe time; fall back to stderr (LogProgress) when init runs outside a test (session-shared).WatchResourceLogsto share the newPumpResourceLogsAsync.Acceptance criteria
Known limitation
Logs attach to the fixture-owner test (the test whose execution triggered init) for the subscription's life — matching the issue's "test that owns the fixture lifecycle" wording and the AsyncLocal constraint. Per-request correlation remains the OTLP receiver's job.
Tests
ResourceLogSelectionTestsforSelectResourceLogNames(explicit-subset, case-sensitivity,ShouldWaitForResourcefallback, empty-names). 4/4 pass.TUnit.Aspire.Corebuilds across net8.0/net9.0/net10.0.