/ Serilog

ASP.NET Core + Serilog + Seq

I have been a great fan of Serilog and Seq for over 2 years and I delivered great value to many of my clients.

After many projects of integrating Serilog and Seq into ASP.NET Core applications, I finally found my favorite way to integrate them.

There are few things this integration needs to nail:

  • Integrate with Serilog, so we have Seq and other important integrations
  • Capture errors that might happen on app startup
  • Have all configuration in the appsettings.json except the dynamic ones (application name)

PS: If you're looking for .NET Core Console app with Serilog integration, check my Why is Serilog not writing my logs into Seq? post.

TL;DR; Here are the delta changes required to add Serilog to ASP .NET Core 2.2+: https://github.com/jernejk/AspNetCoreSerilogExample/pull/2/files

Recommend configuration

My recommended configuration consists on Serilog + Seq integration, with all configuration in appsettings.logs.json (in this example I put it in appsettings.json) and creating the logger as the second thing in the program, right after loading the configuration.

Get right Nuget packages

In .csproj we add following packages:

  <ItemGroup>
    <!-- Serilog dependencies -->
    <PackageReference Include="Serilog" Version="2.8.0" />
    <PackageReference Include="Serilog.AspNetCore" Version="2.1.1" />
    <PackageReference Include="Serilog.Enrichers.Environment" Version="2.1.3" />
    <PackageReference Include="Serilog.Exceptions" Version="5.0.0" />
    <PackageReference Include="Serilog.Extensions.Logging" Version="2.0.4" />
    <PackageReference Include="Serilog.Settings.Configuration" Version="3.1.0" />
    <PackageReference Include="Serilog.Sinks.Async" Version="1.3.0" />
    <PackageReference Include="Serilog.Sinks.Console" Version="3.1.1" />
    <PackageReference Include="Serilog.Sinks.RollingFile" Version="3.3.0" />
    <PackageReference Include="Serilog.Sinks.Seq" Version="4.0.0" />
  </ItemGroup>

Also, make sure that appsettings.json is being copied on the build. Some empty ASP.NET Core templates don't do that!

Use the configuration below if appsettings.json are not being copied.

  <ItemGroup>
    <!-- Make sure all of the necessary appsettings are included with the application. -->
    <Content Update="appsettings*.json">
      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
      <CopyToPublishDirectory>Always</CopyToPublishDirectory>
    </Content>
    <Content Update="appsettings.Local.json">
      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
      <CopyToPublishDirectory>Never</CopyToPublishDirectory>
    </Content>
  </ItemGroup>

Add right configuration.

Most of our Serilog configuration will be in appsettings.json file instead of in code, so we can more easily change it.

In appsettings.json we add:

  "Serilog": {
    "Using": [ "Serilog.Exceptions", "Serilog", "Serilog.Sinks.Console", "Serilog.Sinks.Seq" ],
    "MinimumLevel": {
      "Default": "Verbose",
      "Override": {
        "System": "Information",
        "Microsoft": "Information",
        "Microsoft.EntityFrameworkCore": "Information"
      }
    },
    "WriteTo": [
      {
        "Name": "Seq",
        "Args": {
          "serverUrl": "http://localhost:5341",
          "apiKey": "none",
          "restrictedToMinimumLevel": "Verbose"
        }
      },
      {
        "Name": "Async",
        "Args": {
          "configure": [
            {
              "Name": "Console",
              "Args": {
                "restrictedToMinimumLevel": "Information"
              }
            }
          ]
        }
      }
    ],
    "Enrich": [ "FromLogContext", "WithExceptionDetails" ],
    "Properties": {
      "Environment": "LocalDev"
    }
  }

The Console sink is under "Async" for performance reasons. Writing to console can literally slow down your application, especially when using the "Verbose" log level.

NOTE: If you're like me and like to separate log configuration away from other configuration, you can put them into appsettings.logs.json instead!

Final integration part.

In Program.cs, I'm doing something interesting that some might think is a bit odd for an ASP.NET Core application. First, I'm loading configuration and logger, only then I start creating IWebHost. This is because if something goes wrong while building IWebHost, logger might not yet be available and debugging an issue like that can be a nightmare.

The other thing that you might notice is that I'm loading configuration twice. There are 2 reasons for that.
Firstly, to my surprise .UseConfiguration() wasn't working correctly at the time of writing. Based on the .NET Core 2.2 documentation, it should have worked.
Secondly, the default configuration is great but there is no easy way to replicate it without building IWebHost. For logging, we don't need all of the configuration, just the ones that are related to logging.

    public class Program
    {
        public static void Main(string[] args)
        {
            // There are 2 reasons why we are building logging this early and is used only for logging.
            // 1. We want to log any issues that might happen while the server is spinning up.
            //    Those are hard to find bugs and quite often are not logged properly.
            // 2. Unfortunately ".UseConfiguration()" doesn't work correctly in the .NET Core 2.2.5 (the version I made this demo).
            //    It will ignore the configuration and will load default configuration.
            var configuration = new ConfigurationBuilder()
                .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
                .AddJsonFile($"appsettings.{Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Production"}.json", optional: true)
                .AddJsonFile("appsettings.local.json", optional: true)
                .Build();

            // Lets make sure that if creating web host fails, we can log that error.
            var loggerConfiguration = new LoggerConfiguration()
                .ReadFrom.Configuration(configuration)
                .Enrich.FromLogContext()
                .Enrich.WithProperty("ApplicationName", typeof(Program).Assembly.GetName().Name);

#if DEBUG
            // Used to filter out potentially bad data due debugging.
            // Very useful when doing Seq dashboards and want to remove logs under debugging session.
            loggerConfiguration.Enrich.WithProperty("DebuggerAttached", Debugger.IsAttached);
#endif

            // When using ".UseSerilog()" it will use "Log.Logger".
            Log.Logger = loggerConfiguration.CreateLogger();

            try
            {
                // In some rare cases, creating web host can fail.
                // This style of logging increases the chances of logging this issue.
                Log.Logger.Information("Bootstrapping web app...");
                using (var host = CreateWebHostBuilder(args).Build())
                {
                    host.Run();
                }
            }
            catch (Exception e)
            {
                // Happens rarely but when it does, you'll thank me. :)
                Log.Logger.Fatal(e, "Unable to bootstrap web app.");
            }

            // Make sure all the log sinks have processed the last log before closing the application.
            Log.CloseAndFlush();
        }

        public static IWebHostBuilder CreateWebHostBuilder(string[] args)
            => WebHost.CreateDefaultBuilder(args)
                            .UseStartup<Startup>()
                            .ConfigureAppConfiguration(configuration =>
                            {
                                // It's a good practice to add local settings for local dev.
                                configuration.AddJsonFile("appsettings.local.json", optional: true);
                            })
                            .UseSerilog();
    }

The UseSerilog will initialize the logger globally (Serilog.Log.Logger) and add it to built-in DI container, so now you can use either Serilog.ILogger<T> or Microsoft.Extensions.Logging.ILogger<T> in the constructors.

If possible, always use Microsoft.Extensions.Logging.ILogger<T> as this is an interface that can work with or without Serilog integration. This interface is identical to Serilog.ILogger<T> and with the above code, will use Serilog behind the scenes.

Bonus - Non-generic ILogger

If you want to use non-generic ILogger you have 2 options.

Autofac

Using Autofac has multiple advantages over built-in and that is also true for Serilog integration. Using AutofacSerilogIntegration Nuget package not only you can use non-generic Serilog.ILogger interface, also SourceContext is configured correctly for you!

Just add builder.RegisterLogger(); to the Autofac ContainerBuilder.

Built-in .NET Core DI

You can use non-genric Serilog.ILogger with .NET Core DI, however, I haven't found a way to correctly set SourceContext automatically as Autofac can do.

Here is how you can register non-generic ILogger.

services.AddSingleton((Serilog.ILogger)Log.Logger);