< Summary

Information
Class: NexusLabs.Needlr.Serilog.NeedlrSerilogBootstrapper
Assembly: NexusLabs.Needlr.Serilog
File(s): /home/runner/work/needlr/needlr/src/NexusLabs.Needlr.Serilog/NeedlrSerilogBootstrapper.cs
Line coverage
97%
Covered lines: 40
Uncovered lines: 1
Coverable lines: 41
Total lines: 174
Line coverage: 97.5%
Branch coverage
90%
Covered branches: 9
Total branches: 10
Branch coverage: 90%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
get_ConfigureAction()100%11100%
get_ConfigureWithConfigAction()100%11100%
get_ConfigureBootstrapConfigurationBuilder()100%11100%
RunAsync()100%6696.15%
RunInnerBootstrapper()75%44100%

File(s)

/home/runner/work/needlr/needlr/src/NexusLabs.Needlr.Serilog/NeedlrSerilogBootstrapper.cs

#LineLine coverage
 1using NexusLabs.Needlr.Hosting;
 2
 3using Serilog;
 4using Serilog.Extensions.Logging;
 5
 6using Microsoft.Extensions.Configuration;
 7
 8namespace NexusLabs.Needlr.Serilog;
 9
 10/// <summary>
 11/// Wraps an application entry point with Serilog-specific bootstrap lifecycle management:
 12/// two-stage initialization, a pre-DI Serilog logger, pre-DI <see cref="IConfiguration"/>,
 13/// top-level exception handling, and automatic log flushing on shutdown.
 14/// </summary>
 15/// <remarks>
 16/// <para>
 17/// <see cref="NeedlrSerilogBootstrapper"/> composes <see cref="NeedlrBootstrapper"/> internally.
 18/// All lifecycle behaviour (exception logging and propagation, cancellation, cleanup, and logger
 19/// factory ownership) is delegated to <see cref="NeedlrBootstrapper"/> — this type only adds
 20/// Serilog-specific wiring:
 21/// setting <c>Log.Logger</c> before the callback runs and flushing it in <c>finally</c>.
 22/// </para>
 23/// <para>
 24/// By default a console sink is configured. Override with
 25/// <see cref="NeedlrSerilogBootstrapperExtensions.Configure(NeedlrSerilogBootstrapper, Action{LoggerConfiguration})"/>
 26/// to apply your own <see cref="LoggerConfiguration"/>, or use
 27/// <see cref="NeedlrSerilogBootstrapperExtensions.Configure(NeedlrSerilogBootstrapper, Action{LoggerConfiguration, ICon
 28/// to configure Serilog using the bootstrap <see cref="IConfiguration"/>.
 29/// </para>
 30/// <para>
 31/// By default the bootstrap configuration is <strong>empty</strong>. Use
 32/// <see cref="NeedlrSerilogBootstrapperExtensions.ConfigureBootstrapConfiguration"/> to add
 33/// configuration sources needed during the bootstrap phase.
 34/// The bootstrap configuration is <strong>not</strong> the same <see cref="IConfiguration"/>
 35/// that the application's DI container will provide.
 36/// </para>
 37/// </remarks>
 38/// <example>
 39/// <code>
 40/// await new NeedlrSerilogBootstrapper()
 41///     .ConfigureBootstrapConfiguration(builder => builder
 42///         .AddJsonFile("appsettings.json", optional: true))
 43///     .Configure((cfg, bootstrapConfiguration) => cfg
 44///         .ReadFrom.Configuration(bootstrapConfiguration)
 45///         .WriteTo.Console())
 46///     .RunAsync(async (ctx, ct) =>
 47///     {
 48///         var webApp = new Syringe()
 49///             .UsingSourceGen()
 50///             .ForWebApplication()
 51///             .UsingOptions(() => CreateWebApplicationOptions.Default
 52///                 .UsingCurrentProcessCliArgs()
 53///                 .UsingLogger(ctx.Logger))
 54///             .BuildWebApplication();
 55///
 56///         await webApp.RunAsync(ct);
 57///     });
 58/// </code>
 59/// </example>
 60[DoNotAutoRegister]
 61public sealed record NeedlrSerilogBootstrapper
 62{
 4563    internal Action<LoggerConfiguration>? ConfigureAction { get; init; }
 3664    internal Action<LoggerConfiguration, IConfiguration>? ConfigureWithConfigAction { get; init; }
 3765    internal Action<IConfigurationBuilder>? ConfigureBootstrapConfigurationBuilder { get; init; }
 66
 67    /// <summary>
 68    /// Runs the application entry point with Serilog bootstrap lifecycle management.
 69    /// </summary>
 70    /// <param name="runAsync">
 71    /// The application callback. Receives a <see cref="NeedlrBootstrapContext"/> containing
 72    /// a bootstrap logger backed by the configured Serilog pipeline and a bootstrap
 73    /// <see cref="IConfiguration"/>.
 74    /// </param>
 75    /// <param name="cancellationToken">
 76    /// Optional cancellation token forwarded to the callback.
 77    /// </param>
 78    /// <returns>
 79    /// A <see cref="Task"/> that completes when the application exits normally or through
 80    /// cooperative cancellation, and faults after Serilog flushes for an unexpected exception.
 81    /// </returns>
 82    public async Task RunAsync(
 83        Func<NeedlrBootstrapContext, CancellationToken, Task> runAsync,
 84        CancellationToken cancellationToken = default)
 85    {
 1786        ArgumentNullException.ThrowIfNull(runAsync);
 87
 88        // Build bootstrap config early so the Serilog Configure delegate can use it.
 89        // This is built outside NeedlrBootstrapper so we can pass it to Serilog's
 90        // ReadFrom.Configuration before the inner bootstrapper runs.
 1691        var configBuilder = new ConfigurationBuilder();
 1692        ConfigureBootstrapConfigurationBuilder?.Invoke(configBuilder);
 1693        IConfigurationRoot? bootstrapConfiguration = null;
 94
 95        try
 96        {
 1697            bootstrapConfiguration = configBuilder.Build();
 98
 1699            var configuration = new LoggerConfiguration();
 16100            if (ConfigureWithConfigAction is not null)
 101            {
 2102                ConfigureWithConfigAction(configuration, bootstrapConfiguration);
 103            }
 14104            else if (ConfigureAction is not null)
 105            {
 13106                ConfigureAction(configuration);
 107            }
 108            else
 109            {
 1110                configuration.WriteTo.Console();
 111            }
 112
 15113            Log.Logger = configuration.CreateLogger();
 15114        }
 115        catch (Exception ex)
 116        {
 117            // Serilog configuration failed — fall back to a bare console logger so
 118            // the inner bootstrapper can log the failure before cleanup and propagation.
 1119            Log.Logger = new LoggerConfiguration().WriteTo.Console().CreateLogger();
 120
 1121            var capturedEx = ex;
 1122            await RunInnerBootstrapper(
 1123                bootstrapConfiguration,
 1124                (_, _) => throw capturedEx,
 1125                cancellationToken)
 1126                .ConfigureAwait(false);
 0127            return;
 128        }
 129
 15130        await RunInnerBootstrapper(
 15131            bootstrapConfiguration,
 15132            runAsync,
 15133            cancellationToken)
 15134            .ConfigureAwait(false);
 13135    }
 136
 137    private async Task RunInnerBootstrapper(
 138        IConfigurationRoot? bootstrapConfiguration,
 139        Func<NeedlrBootstrapContext, CancellationToken, Task> runAsync,
 140        CancellationToken cancellationToken)
 141    {
 16142        var loggerFactory = new SerilogLoggerFactory(dispose: false);
 143
 16144        var inner = new NeedlrBootstrapper()
 16145            .UsingLoggerFactory(loggerFactory)
 32146            .WithCleanup(() => Log.CloseAndFlushAsync().AsTask());
 147
 16148        if (ConfigureBootstrapConfigurationBuilder is not null)
 149        {
 2150            inner = inner.ConfigureBootstrapConfiguration(ConfigureBootstrapConfigurationBuilder);
 151        }
 152
 153        // If we already built bootstrap config for Serilog, override the inner
 154        // bootstrapper's config building to reuse the same instance instead of
 155        // building it twice. We pass a no-op configure action — the inner
 156        // bootstrapper will still build a ConfigurationBuilder, but we need
 157        // it to have the same sources. Simpler: we forward the same configure action.
 158        // The inner bootstrapper will build its own IConfigurationRoot from the
 159        // same sources — this is acceptable because bootstrap config is cheap
 160        // and the Serilog config phase is done.
 161
 162        try
 163        {
 16164            await inner.RunAsync(runAsync, cancellationToken)
 16165                .ConfigureAwait(false);
 13166        }
 167        finally
 168        {
 169            // Dispose our pre-built config after the inner bootstrapper has
 170            // disposed its own copy and completed cleanup.
 16171            (bootstrapConfiguration as IDisposable)?.Dispose();
 172        }
 13173    }
 174}