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
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Blaztrap.Aspire.FileLogging" Version="0.1.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Blaztrap.Aspire.FileLogging" Version="0.1.0" />
                    
Directory.Packages.props
<PackageReference Include="Blaztrap.Aspire.FileLogging" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Blaztrap.Aspire.FileLogging --version 0.1.0
                    
#r "nuget: Blaztrap.Aspire.FileLogging, 0.1.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Blaztrap.Aspire.FileLogging@0.1.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Blaztrap.Aspire.FileLogging&version=0.1.0
                    
Install as a Cake Addin
#tool nuget:?package=Blaztrap.Aspire.FileLogging&version=0.1.0
                    
Install as a Cake Tool

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}.log stays empty. The matching error is in apphost.log under Aspire.Hosting.Dcp.* or similar. When diagnosing, inspect apphost.log first.
  • Resources added after the call: snapshot at registration time. See Resource discovery above.
  • Log levels: the pump assigns LogLevel.Information to 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 FileSink per output file, shared by every category that targets that file. Writes are serialized with a per-sink lock, so categories interleaving on apphost.log cannot tear lines.
  • AutoFlush = true on every StreamWriter, so a crashing host still leaves a tailable file on disk.
  • Categories are matched as categoryName == resourceName OR categoryName.EndsWith("." + resourceName), mirroring ResourceLoggerForwarderService for fully-qualified categories like Contoso.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 what ResourceLoggerService exposes through ILoggerFactory.
Product 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

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