Using Serilog sub-loggers in .NET Core/5

If you aren’t familiar with Serilog, it is a powerful, full-featured logging library that integrates seamlessly with .NET Core/5. Some of the features it provides are flat and structured logging, numerous plugins to write data to various endpoints (called sinks), and an implementation that can be easily extended.

One of the concepts that Serilog brings to the logging world is the ability to create sub-loggers. A standard logging setup will have logs written out to a file, console, and/or a database. But if you want to write out to multiple instances of a sink then what you’ll want to look into are sub-loggers. Sub-loggers enable the instantiation of more than one instance of a sink with a custom configuration applied to each one. Some reasons that you might want to do this would be:

  • Output multiple formats of logs to a file system for digestion by different monitoring applications.
  • Recording log output from specific classes to their own files.
  • Generating multiple log files based on specific filters.

In this post I’ll show an example where we use appsettings.json to configure the logging library. Within the SpaHost project two log files will be created. One will contain all log entries while the other will hold only log entries with the level error or above. Within a second project, CrossDomainApi, we’ll have another scenario defined where one log file will contain all log entries while a second log file will contain only the log entries that originate from the Api.TestController namespace.

In order to get this configuration working a few NuGet packages need to be added.

Within the startup code of the ASP.NET Core/5 application a small bit of standard code is required. Within the Program.cs:CreateWebHostBuilder() method we need to add .UseSerilog() to the call chain. This will set Serilog as the logger for the web app. In this implementation we also make calls set the configuration, use IIS settings and capture startup errors. Depending on your implementation you may not need those calls.

public static IWebHostBuilder CreateWebHostBuilder(string[] args)
{
   var configuration = GetConfiguration();
   return WebHost.CreateDefaultBuilder(args)
      .UseSerilog()
      .UseConfiguration(configuration)
      .UseIIS()
      .CaptureStartupErrors(true)
      .UseStartup<Startup>();
}

The rest of the core logging configuration is defined in the appsettings.Development.json file. If you would rather define the logging setting in code you can do so but I personally like defining the set up in configuration files so we can customize logging based on the environment where the application is executing.

Within this file there is a section named "Serilog" that defines how the logger will handle log events, what gets written, and where those events are written. The sample below is fromthe CrossDomainApi‘s configuration file. As mentioned earlier this configuration writes logs to two files, one with all log events and another with only log events from a specific namespace.

{
  "Serilog": {
    "Using": [ "Serilog.Sinks.Console", "Serilog.Sinks.File" ],
    "MinimumLevel": {
      "Default": "Information",
      "Override": {
        "Microsoft": "Warning",
        "System": "Warning"
      }
    },
    "WriteTo": [
      {
        "Name": "Console",
        "Args": {
          "outputTemplate": "{Timestamp:HH:mm:ss.fff zzz}|{Level}|{ThreadId}|{SourceContext}|{Message:lj}|{Exception}{NewLine}"
        }
      },
      {
        "Name": "Logger",
        "Args": {
          "configureLogger": {
            "WriteTo": [
              {
                "Name": "File",
                "Args": {
                  "rollingInterval": "Day",
                  "path": "C:/Logs/CrossDomainApi/all-.log",
                  "outputTemplate": "{Timestamp:HH:mm:ss.fff zzz}|{Level}|{ThreadId}|{SourceContext}|{Message:lj}|{Exception}{NewLine}"
                }
              }
            ]
          }
        }
      },
      {
        "Name": "Logger",
        "Args": {
          "configureLogger": {
            "Filter": [
              {
                "Name": "ByIncludingOnly",
                "Args": {
                  "expression": "Contains(SourceContext, 'Api.TestController')"
                }
              }
            ],
            "WriteTo": [
              {
                "Name": "File",
                "Args": {
                  "rollingInterval": "Day",
                  "path": "C:/Logs/CrossDomainApi/api-.log",
                  "outputTemplate": "{Timestamp:HH:mm:ss.fff zzz}|{Level}|{ThreadId}|{SourceContext}|{Message:lj}|{Exception}{NewLine}"
                }
              }
            ]
          }
        }
      }
    ],
    "Enrich": [ "FromLogContext", "WithMachineName", "WithThreadId" ],
    "Properties": {
      "Application": "CrossDomainApi"
    }
  }
}

What makes this configuration use sub-loggers are the child definition of "WriteTo" within the array of "WriteTo" objects. In this case we have two sub-loggers, both write logs via the "File" sink. It is in the last Logger that we apply a filter within the "configureLogger" object definition. This filter first checks that the SourceContext value contains the string Api.TestController. All log events that match this check will be sent to the sub-logger for processing.

With this configuration in place you can add an ILogger<classname> parameter to your controllers for writing out the log files. The Dependency Injection system provided by .NET will automatically pass in the instance of ILogger that was defined during startup. Additionally, any logging output by referenced projects or libraries will also use the configured Serilog instance.

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;

namespace Api
{
    [Route("test")]
    public class TestController : ControllerBase
    {
        private readonly ILogger<TestController> _logger;

        public TestController(ILogger<TestController> logger)
        {
            _logger = logger;
        }
        public IActionResult Get()
        {
            _logger.LogDebug("Request received");
            return new JsonResult("OK");
        }
    }
}

To see the working solution take a look at the BackendForFrontend project. This project also demonstrates using an external Identity Server for authentication which was written up about in How to use Backend for Frontend to simplify authentication in an Angular SPA.