< 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
100%
Covered lines: 41
Uncovered lines: 0
Coverable lines: 41
Total lines: 169
Line coverage: 100%
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%66100%
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 catching, cleanup, logger factory ownership) is delegated
 19/// to <see cref="NeedlrBootstrapper"/> — this type only adds Serilog-specific wiring:
 20/// setting <c>Log.Logger</c> before the callback runs and flushing it in <c>finally</c>.
 21/// </para>
 22/// <para>
 23/// By default a console sink is configured. Override with
 24/// <see cref="NeedlrSerilogBootstrapperExtensions.Configure(NeedlrSerilogBootstrapper, Action{LoggerConfiguration})"/>
 25/// to apply your own <see cref="LoggerConfiguration"/>, or use
 26/// <see cref="NeedlrSerilogBootstrapperExtensions.Configure(NeedlrSerilogBootstrapper, Action{LoggerConfiguration, ICon
 27/// to configure Serilog using the bootstrap <see cref="IConfiguration"/>.
 28/// </para>
 29/// <para>
 30/// By default the bootstrap configuration is <strong>empty</strong>. Use
 31/// <see cref="NeedlrSerilogBootstrapperExtensions.ConfigureBootstrapConfiguration"/> to add
 32/// configuration sources needed during the bootstrap phase.
 33/// The bootstrap configuration is <strong>not</strong> the same <see cref="IConfiguration"/>
 34/// that the application's DI container will provide.
 35/// </para>
 36/// </remarks>
 37/// <example>
 38/// <code>
 39/// await new NeedlrSerilogBootstrapper()
 40///     .ConfigureBootstrapConfiguration(builder => builder
 41///         .AddJsonFile("appsettings.json", optional: true))
 42///     .Configure((cfg, bootstrapConfiguration) => cfg
 43///         .ReadFrom.Configuration(bootstrapConfiguration)
 44///         .WriteTo.Console())
 45///     .RunAsync(async (ctx, ct) =>
 46///     {
 47///         var webApp = new Syringe()
 48///             .UsingSourceGen()
 49///             .ForWebApplication()
 50///             .UsingOptions(() => CreateWebApplicationOptions.Default
 51///                 .UsingCurrentProcessCliArgs()
 52///                 .UsingLogger(ctx.Logger))
 53///             .BuildWebApplication();
 54///
 55///         await webApp.RunAsync(ct);
 56///     });
 57/// </code>
 58/// </example>
 59[DoNotAutoRegister]
 60public sealed record NeedlrSerilogBootstrapper
 61{
 3662    internal Action<LoggerConfiguration>? ConfigureAction { get; init; }
 3063    internal Action<LoggerConfiguration, IConfiguration>? ConfigureWithConfigAction { get; init; }
 3164    internal Action<IConfigurationBuilder>? ConfigureBootstrapConfigurationBuilder { get; init; }
 65
 66    /// <summary>
 67    /// Runs the application entry point with Serilog bootstrap lifecycle management.
 68    /// </summary>
 69    /// <param name="runAsync">
 70    /// The application callback. Receives a <see cref="NeedlrBootstrapContext"/> containing
 71    /// a bootstrap logger backed by the configured Serilog pipeline and a bootstrap
 72    /// <see cref="IConfiguration"/>.
 73    /// </param>
 74    /// <param name="cancellationToken">
 75    /// Optional cancellation token forwarded to the callback.
 76    /// </param>
 77    /// <returns>A <see cref="Task"/> that completes when the application exits.</returns>
 78    public async Task RunAsync(
 79        Func<NeedlrBootstrapContext, CancellationToken, Task> runAsync,
 80        CancellationToken cancellationToken = default)
 81    {
 1482        ArgumentNullException.ThrowIfNull(runAsync);
 83
 84        // Build bootstrap config early so the Serilog Configure delegate can use it.
 85        // This is built outside NeedlrBootstrapper so we can pass it to Serilog's
 86        // ReadFrom.Configuration before the inner bootstrapper runs.
 1387        var configBuilder = new ConfigurationBuilder();
 1388        ConfigureBootstrapConfigurationBuilder?.Invoke(configBuilder);
 1389        IConfigurationRoot? bootstrapConfiguration = null;
 90
 91        try
 92        {
 1393            bootstrapConfiguration = configBuilder.Build();
 94
 1395            var configuration = new LoggerConfiguration();
 1396            if (ConfigureWithConfigAction is not null)
 97            {
 298                ConfigureWithConfigAction(configuration, bootstrapConfiguration);
 99            }
 11100            else if (ConfigureAction is not null)
 101            {
 10102                ConfigureAction(configuration);
 103            }
 104            else
 105            {
 1106                configuration.WriteTo.Console();
 107            }
 108
 12109            Log.Logger = configuration.CreateLogger();
 12110        }
 111        catch (Exception ex)
 112        {
 113            // Serilog configuration failed — fall back to a bare console logger so
 114            // the error is visible, then rethrow into the inner bootstrapper's
 115            // catch/cleanup path via a wrapper callback.
 1116            Log.Logger = new LoggerConfiguration().WriteTo.Console().CreateLogger();
 1117            Log.Fatal(ex, "Failed to configure Serilog bootstrap logger.");
 118
 119            // Let the inner bootstrapper handle cleanup. The callback will throw
 120            // the original exception so it is logged at Critical by NeedlrBootstrapper.
 1121            var capturedEx = ex;
 1122            await RunInnerBootstrapper(
 1123                bootstrapConfiguration,
 1124                (_, _) => throw capturedEx,
 1125                cancellationToken)
 1126                .ConfigureAwait(false);
 1127            return;
 128        }
 129
 12130        await RunInnerBootstrapper(
 12131            bootstrapConfiguration,
 12132            runAsync,
 12133            cancellationToken)
 12134            .ConfigureAwait(false);
 13135    }
 136
 137    private async Task RunInnerBootstrapper(
 138        IConfigurationRoot? bootstrapConfiguration,
 139        Func<NeedlrBootstrapContext, CancellationToken, Task> runAsync,
 140        CancellationToken cancellationToken)
 141    {
 13142        var loggerFactory = new SerilogLoggerFactory(dispose: false);
 143
 13144        var inner = new NeedlrBootstrapper()
 13145            .UsingLoggerFactory(loggerFactory)
 26146            .WithCleanup(() => Log.CloseAndFlushAsync().AsTask());
 147
 13148        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
 13162        await inner.RunAsync(runAsync, cancellationToken)
 13163            .ConfigureAwait(false);
 164
 165        // Dispose our pre-built config after the inner bootstrapper has
 166        // disposed its own copy and completed cleanup.
 13167        (bootstrapConfiguration as IDisposable)?.Dispose();
 13168    }
 169}