serilog / serilog-extensions-hosting

Serilog logging for Microsoft.Extensions.Hosting
Apache License 2.0
141 stars 34 forks source link

UseSerilog support for .net8 IHostApplicationBuilder #76

Closed scottmwyant closed 7 months ago

scottmwyant commented 1 year ago

This package has extension methods for IHostBuilder. In .net7, the official documentation for Generic Host encourages use of Host.CreateApplicationBuilder which returns a HostApplicationBuilder. The trouble is that HostApplicationBuilder does not implement the IHostBuilder interface.

I propose that, in addition to the current capabilities, the Serilog.Extensions.Hosting package should offer a .UseSerilog extension method for IHostApplicationBuilder.

Reference:

Edit: I now see that IHostApplicationBuilder is included in .net8. Updating title. Best course of action for .net7 would probaby be to use Host.CreateDefaultBuilder to get the .net6 style API (using callbacks).

nblumhardt commented 1 year ago

Hi @scottmwyant, thanks for the note. Does builder.Services.AddSerilog() work as you expect? (I'm happy adding the UseSerilog() sugar on the higher-level interface - just keen to validate that we got the functionality right in #66). Thanks!

scottmwyant commented 1 year ago

@nblumhardt I will need to review. Apologies for not being clear about the actual problem; perhaps I just need some education. Looking to use Serilog.Sinks.File. Typically I would specify the log file to be the content root. Need access to host context to do that. Is there a different way using the IServiceCollection extension?

scottmwyant commented 1 year ago

Leaving a solution in hopes to save somebody else some time. I just started working with at the new "linear" API for HostApplicationBuilder, after looking at it for a while, it's perfectly fine as is.

var builder = Host.CreateApplicationBuilder();
builder.Services.AddWindowsService(options => options.ServiceName = ".NET Joke Service");
 builder.Logging
    .ClearProviders()
    .AddSerilog(
        new LoggerConfiguration()
            .MinimumLevel.Debug()
            .MinimumLevel.Override("Microsoft", Serilog.Events.LogEventLevel.Information)
            .Enrich.FromLogContext()
            .WriteTo.File(Path.Join(builder.Environment.ContentRootPath, "myApp.log"))
            .CreateLogger()
    );

IHost host = builder.Build();
nblumhardt commented 1 year ago

Thanks for closing the loop, @scottmwyant.

I think the intended solution via the earlier PR is actually:

var builder = Host.CreateApplicationBuilder();
builder.Services.AddWindowsService(options => options.ServiceName = ".NET Joke Service");
builder.Services.AddSerilog(lc => lc
            .MinimumLevel.Debug()
            .MinimumLevel.Override("Microsoft", Serilog.Events.LogEventLevel.Information)
            .Enrich.FromLogContext()
            .WriteTo.File(Path.Join(builder.Environment.ContentRootPath, "myApp.log")));

IHost host = builder.Build();

Does this variation work for you?

scottmwyant commented 1 year ago

@nblumhardt, actually no, I'm not seeing that. I don't have an .AddSerilog() overload that takes an Action<LoggerConfiguration>. Serilog.Extensions.Hosting v7.0.0, dotnet runtime 7.0.10.

nblumhardt commented 1 year ago

Thanks - I'll take another look at it 👍

gwlg-mjh commented 1 year ago

Not to be the proverbial "It works on my machine" but it works on my machine. Was just working out how to add serilog to an IHostApplicationBuilder and found this issue, the comment about builder.Services.AddSerilog helped as I'd written my own extension method to do the whole builder.Logging.ClearProviders beforehand.

Working MVP

Project.csproj

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net6.0</TargetFramework>
    <RootNamespace>SerilogTest</RootNamespace>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

  <ItemGroup>
    <Content Include="appsettings.json">
      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
    </Content>
  </ItemGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.Extensions.Hosting" Version="7.0.1" />
    <PackageReference Include="Serilog.Enrichers.Environment" Version="2.2.0" />
    <PackageReference Include="Serilog.Enrichers.Thread" Version="3.1.0" />
    <PackageReference Include="Serilog.Extensions.Hosting" Version="7.0.0" />
    <PackageReference Include="Serilog.Formatting.Compact" Version="1.1.0" />
    <PackageReference Include="Serilog.Settings.Configuration" Version="7.0.1" />
    <PackageReference Include="Serilog.Sinks.Console" Version="4.1.0" />
  </ItemGroup>

</Project>

Program.cs

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Serilog;

namespace SerilogTest;
internal static class Program
{
    static async Task Main(string[] args)
    {
        var builder = Host.CreateApplicationBuilder(args);

        builder.Services.AddSerilog(config =>
        {
            config.ReadFrom.Configuration(builder.Configuration);
        });

        builder.Services.AddHostedService<App>();

        var host = builder.Build();

        await host.RunAsync();
    }
}

App.cs

using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

namespace SerilogTest;
internal class App : IHostedService
{
    private readonly ILogger<App> logger;

    public App(ILogger<App> logger)
    {
        this.logger = logger;
    }

    public async Task StartAsync(CancellationToken cancellationToken)
    {
        logger.LogInformation("Application Started");
        await Task.CompletedTask;
    }

    public async Task StopAsync(CancellationToken cancellationToken)
    {
        logger.LogInformation("Application Stopped");
        await Task.CompletedTask;
    }
}

appsettings.json

{
  "Serilog": {
    "Using": [ ],
    "WriteTo": [
      {
        "Name": "Console",
        "Args": {
          "formatter": "Serilog.Formatting.Compact.CompactJsonFormatter, Serilog.Formatting.Compact"
        }
      }
     ],
    "Enrich": [ "FromLogContext", "WithThreadId" ]
  }
}
{"@t":"2023-08-31T09:58:52.0739377Z","@mt":"Application Started","SourceContext":"SerilogTest.App","ThreadId":1}
{"@t":"2023-08-31T09:58:52.0901752Z","@mt":"Application started. Press Ctrl+C to shut down.","SourceContext":"Microsoft.Hosting.Lifetime","ThreadId":1}
{"@t":"2023-08-31T09:58:52.0991706Z","@mt":"Hosting environment: {EnvName}","EnvName":"Production","SourceContext":"Microsoft.Hosting.Lifetime","ThreadId":1}
{"@t":"2023-08-31T09:58:52.0996216Z","@mt":"Content root path: {ContentRoot}","ContentRoot":"\\bin\\Debug\\net6.0","SourceContext":"Microsoft.Hosting.Lifetime","ThreadId":1}
{"@t":"2023-08-31T09:58:54.2816410Z","@mt":"Application is shutting down...","SourceContext":"Microsoft.Hosting.Lifetime","ThreadId":9}
{"@t":"2023-08-31T09:58:54.3143022Z","@mt":"Application Stopped","SourceContext":"SerilogTest.App","ThreadId":3}
zsharp-gls commented 1 year ago

@gwlg-mjh's solution worked for me! I would still be interested in sugar for a .UseSerilog() on HostApplicationBuilder

DavidHopkinsFbr commented 1 year ago

@gwlg-mjh's solution is very clean. I think it would be helpful to update the serilog-extensions-hosting or serilog-settings-configuration README samples to take this approach, since it shows very nicely how to combine both together with the .NET 7/8 hosting APIs, which I was struggling with a bit as a newbie.

jasonswearingen commented 1 year ago

need to add Serilog.Settings.Configuration nuget package for this to work. (for the .Configuration() extension method). serilog docs kind of are lacking.

crozone commented 8 months ago

For anyone still looking for an extension method to use, here's an example of what the extension method could look like:

public static IHostApplicationBuilder UseSerilog(this IHostApplicationBuilder builder, Action<IHostApplicationBuilder, IServiceProvider, LoggerConfiguration> configureLogger, bool preserveStaticLogger = false, bool writeToProviders = false)
{
    if (builder is null)
    {
        throw new ArgumentNullException(nameof(builder));
    }

    if (configureLogger is null)
    {
        throw new ArgumentNullException(nameof(configureLogger));
    }

    builder.Logging.Services.AddSerilog(
        (services, loggerConfiguration) => configureLogger(builder, services, loggerConfiguration),
        preserveStaticLogger,
        writeToProviders);

    return builder;
}

Instead of an HostBuilderContext being passed into the lambda, it gets replaced with IHostApplicationBuilder itself.

This is basically @gwlg-mjh 's comment wrapped in an extension method, however it uses builder.Logger.Services instead, which I believe is more correct for registering logging services.

jozsefcsontos commented 7 months ago

For anyone still looking for an extension method to use, here's an example of what the extension method could look like:

public static IHostApplicationBuilder UseSerilog(this IHostApplicationBuilder builder, Action<IHostApplicationBuilder, IServiceProvider, LoggerConfiguration> configureLogger, bool preserveStaticLogger = false, bool writeToProviders = false)
{
    if (builder is null)
    {
        throw new ArgumentNullException(nameof(builder));
    }

    if (configureLogger is null)
    {
        throw new ArgumentNullException(nameof(configureLogger));
    }

    builder.Logging.Services.AddSerilog(
        (services, loggerConfiguration) => configureLogger(builder, services, loggerConfiguration),
        preserveStaticLogger,
        writeToProviders);

    return builder;
}

Instead of an HostBuilderContext being passed into the lambda, it gets replaced with IHostApplicationBuilder itself.

This is basically @gwlg-mjh 's comment wrapped in an extension method, however it uses builder.Logger.Services instead, which I believe is more correct for registering logging services.

What do you think about clearing the existing logging providers before the AddSerilog call? i.e.: builder.Logging.ClearProviders();

nblumhardt commented 7 months ago

Reopening because we need some documentation around this. I think we should also look at exposing IConfiguration through an overload of the AddSerilog() callback.

nblumhardt commented 7 months ago

Dug in some more, consuming configuration is already as simple as:

builder.Services.AddSerilog(lc => lc
    .ReadFrom.Configuration(builder.Configuration));
nblumhardt commented 7 months ago

README updated now - thanks everyone :-)