Blaztrap.Aspire.FileLogging
0.1.0
dotnet add package Blaztrap.Aspire.FileLogging --version 0.1.0
NuGet\Install-Package Blaztrap.Aspire.FileLogging -Version 0.1.0
<PackageReference Include="Blaztrap.Aspire.FileLogging" Version="0.1.0" />
<PackageVersion Include="Blaztrap.Aspire.FileLogging" Version="0.1.0" />
<PackageReference Include="Blaztrap.Aspire.FileLogging" />
paket add Blaztrap.Aspire.FileLogging --version 0.1.0
#r "nuget: Blaztrap.Aspire.FileLogging, 0.1.0"
#:package Blaztrap.Aspire.FileLogging@0.1.0
#addin nuget:?package=Blaztrap.Aspire.FileLogging&version=0.1.0
#tool nuget:?package=Blaztrap.Aspire.FileLogging&version=0.1.0
Blaztrap.Aspire.FileLogging
Unified per-resource and host file logger for .NET Aspire
AppHosts. Writes one file per Aspire-orchestrated executable
(ProjectResource / ExecutableResource) plus a single shared apphost.log
that captures everything the AppHost itself emits — Aspire framework,
DCP, dashboard, Microsoft.Hosting.*, application host code, etc.
Works in both modes with the same API:
- Integration tests using
DistributedApplicationTestingBuilder.CreateAsync<T>(). - Regular AppHost runs (
aspire run/dotnet run) for exploratory provisioning.
No dependency on Aspire.Hosting.Testing.
Why
Blaztrap.Aspire.Testing.FileLogging v0.1.x captures per-resource child stdout
but only inside an integration-test session, because it relies on
ResourceLoggerForwarderService from Aspire.Hosting.Testing (auto-registered
only by DistributedApplicationTestingBuilder). It also drops everything the
AppHost itself logs.
This package replaces both pieces:
| Concern | This package |
|---|---|
| Per-resource child stdout/stderr | Bundled ResourceLogPump subscribes to ResourceLoggerService.WatchAsync(resource) directly and forwards lines through ILoggerFactory. No dependency on the testing-only forwarder. |
| AppHost / framework log stream | Captured automatically — FileLoggerProvider is a regular ILoggerProvider, so any category not matched by a resource name lands in apphost.log with the category as a prefix. |
Wiring
In a regular AppHost (exploratory provisioning)
using Blaztrap.Aspire.FileLogging;
var builder = DistributedApplication.CreateBuilder(args);
var api = builder.AddProject<Projects.Contoso_Foo_Api>("api");
var web = builder.AddProject<Projects.Contoso_Foo_Web>("web");
var worker = builder.AddProject<Projects.Contoso_Foo_Worker>("worker");
// AFTER all AddProject / AddExecutable calls, BEFORE Build:
builder.AddFileLogging(Path.Combine(AppContext.BaseDirectory, "logs"));
builder.Build().Run();
In an MSTest integration test (centralized fixture, one AppHost per class)
The recommended shape is a single fixture base class that mounts a fresh
DistributedApplication per derived test class. The artefact root always
comes from MSTest's TestContext — do not introduce environment variables
(BLAZTRAP_TEST_RUN_DIR or similar) or in-repo log folders: TestContext is
the single source of truth, and orchestrators that need a deterministic root
pass --results-directory to dotnet test instead.
using System.Reflection;
using Blaztrap.Aspire.FileLogging;
public abstract class AppHostFixture
{
protected static DistributedApplication App { get; private set; } = null!;
protected static string LogsDir { get; private set; } = string.Empty;
[ClassInitialize(InheritanceBehavior.BeforeEachDerivedClass)]
public static async Task FixtureInitAsync(TestContext context)
{
var className = Type.GetType(context.FullyQualifiedTestClassName!)!.Name;
var runDir = context.TestRunResultsDirectory
?? Path.Combine(AppContext.BaseDirectory, "TestResults");
LogsDir = Path.Combine(runDir, "logs", className); // per-class folder
var appHost = await DistributedApplicationTestingBuilder
.CreateAsync<Projects.Contoso_Foo_AppHost>();
appHost.Services.ConfigureHttpClientDefaults(c => c.AddStandardResilienceHandler());
// AFTER all AddProject / AddExecutable calls, BEFORE BuildAsync:
appHost.AddFileLogging(LogsDir);
App = await appHost.BuildAsync();
await App.StartAsync();
}
[ClassCleanup(InheritanceBehavior.BeforeEachDerivedClass)]
public static async Task FixtureCleanupAsync()
{
if (App is not null) await App.DisposeAsync();
App = null!;
}
}
[TestClass]
public class Orders_Tests : AppHostFixture
{
// Inherits the mount; adds only its own clients in a plain [ClassInitialize].
}
Pin [assembly: Parallelize(Workers = 1, Scope = ExecutionScope.MethodLevel)]
so classes (and their AppHosts) never boot concurrently. Avoid
[AssemblyInitialize] mounting one shared AppHost for the whole suite — state
leaks across classes and a single startup failure takes down every test with
no per-class log folder to tell you which scenario broke it.
File layout produced
For an AppHost that registers webfrontend, api, and worker:
{outputDirectory}/
apphost.log # AppHost / Aspire / DCP / Dashboard / Microsoft.Hosting.* / Default
webfrontend.log # ProjectResource "webfrontend" stdout + stderr
api.log # ProjectResource "api" stdout + stderr
worker.log # ExecutableResource "worker" stdout + stderr
Per-resource line format (no category — the file already identifies the source):
[2026-04-23T15:42:07.1234567Z] [information] Application started. Press Ctrl+C to shut down.
apphost.log line format (category prefixed so multiple sources are
distinguishable):
[2026-04-23T15:42:07.1234567Z] [information] Aspire.Hosting.Dcp.DcpHostService: Starting DCP...
[2026-04-23T15:42:07.5670000Z] [information] Microsoft.Hosting.Lifetime: Application started.
Exceptions are serialized on the line(s) following the message.
Public API
| Type | Purpose |
|---|---|
DistributedApplicationBuilderExtensions.AddFileLogging(builder, outputDirectory) |
High-level. Snapshots ProjectResource + ExecutableResource names, registers FileLoggerProvider, and wires ResourceLogPump as a hosted service. Call AFTER every AddProject / AddExecutable. |
LoggingBuilderExtensions.AddFileLogging(loggingBuilder, outputDirectory, params names) |
Low-level. Pass the resource-name whitelist yourself. Useful when you only want host-side logs and provide your own resource forwarder, or outside the AppHost context. |
FileLoggerProvider |
The ILoggerProvider itself. Constructable directly if you need fine-grained control. |
Resource discovery — why it's a snapshot
AddFileLogging walks builder.Resources once and freezes the resulting list
of names. Resources added later (rare in practice) are not captured. If you
need that, call the low-level overload yourself with an explicit name list,
or compose the AppHost so all resource registrations precede the
AddFileLogging call.
Why Aspire.Hosting.Testing is NOT a dependency
ResourceLoggerForwarderService is a BackgroundService shipped in
Aspire.Hosting.Testing that subscribes to ResourceNotificationService and
ResourceLoggerService and forwards every resource's console output through
ILoggerFactory. Both source services are public types in Aspire.Hosting.
This package re-implements the same loop in ResourceLogPump, so the
testing-only package is not pulled into AppHost projects that use it for
exploratory runs.
The behavior is observationally equivalent: the pump and the forwarder both
register a category named after the resource and emit one ILogger call per
log line, with LogLevel.Error for IsErrorMessage = true and
LogLevel.Information otherwise.
Coexistence with the dashboard
ResourceLoggerService.WatchAsync(resource) returns an independent stream per
subscriber, so ResourceLogPump and the dashboard can both subscribe to the
same resource without interfering. Stopping the pump (host shutdown) does not
affect the dashboard or any other consumer.
Caveats
- Failures before a resource starts: if a container or project fails to
start (image missing, port collision, bad connection string), the failing
resource produces zero lines on stdout — there is nothing for the pump to
forward, so
{resource}.logstays empty. The matching error is inapphost.logunderAspire.Hosting.Dcp.*or similar. When diagnosing, inspectapphost.logfirst. - Resources added after the call: snapshot at registration time. See Resource discovery above.
- Log levels: the pump assigns
LogLevel.Informationto every line that Aspire does not flag as an error. Standard .NET logging filters (Logging:LogLevel:{resourceName},Logging:LogLevel:Aspire.*) apply exactly as for any other logger and let you reduce verbosity per resource or per framework subsystem. - Filename sanitization: characters illegal on the host filesystem are
replaced with
_. Resource-name collisions after sanitization (rare) result in shared writes to the same file.
Design notes
- One
FileSinkper output file, shared by every category that targets that file. Writes are serialized with a per-sink lock, so categories interleaving onapphost.logcannot tear lines. AutoFlush = trueon everyStreamWriter, so a crashing host still leaves a tailable file on disk.- Categories are matched as
categoryName == resourceNameORcategoryName.EndsWith("." + resourceName), mirroringResourceLoggerForwarderServicefor fully-qualified categories likeContoso.Foo.AppHost.Resources.api. - The provider is process-local. It does not read child-process stdout
directly —
ResourceLoggerService(provided by Aspire) does that, the pump forwards whatResourceLoggerServiceexposes throughILoggerFactory.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- Aspire.Hosting (>= 13.2.1)
- MessagePack (>= 2.5.302)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 0.1.0 | 43 | 7/14/2026 |