How to Serilog Logging in ASP.NET Core

In this article, let's talk all about logging with Serilog and how to integrate in ASP.NET Core with examples on File and Database logging.

First of all, why do you need Logging?

In any application, when properly configured; Logging can help you with situations such as –

  • Troubleshoot problems after the application is deployed Production / goes live
  • Early detection of malicious activity
  • Insights about the application usage and gather statistics

Serilog is one of the several popular logging providers available in the market for ASP.NET Core applications.

What is Serilog?

Like many other libraries for .NET, Serilog provides diagnostic logging to files, the console, and elsewhere. It is easy to set up, has a clean API, and is portable between recent .NET platforms.

Unlike other logging libraries, Serilog is built with powerful structured event data in mind.

– documentation (https://serilog.net)

Integrating Serilog in ASP.NET Core

Integrating Serilog logger into an ASP.NET Core application is very simple. You just need to follow the following steps:

  1. Install the Serilog nuget package
  2. Install the Serilog Sink package of your choice
  3. Configure Serilog as a Logging provider in the .NET Core host or, register Serilog with the Logger service during startup

To demonstrate these steps, let’s start by creating a simple Web API application that does some CRUD operations on an Item entity.

You can start afresh, or you can use the ContainerNinja.CleanArchitecture boilerplate solution that I’ve designed for easily getting started with ASP.NET Core.

I’m modifying on the ContainerNinja.API project present in the boilerplate solution, just to save me some time 😁 (that’s what it was designed for in the first place!)

Step 1 – Installing the package(s)

To install Serilog nuget package, we have two options:

  1. We can install the Serilog package along with Serilog.Extensions.Logging package, or
  2. We can install Serilog.AspNetCore package which comes as a complete package for logging specifically with ASP.NET Core
> dotnet add package Serilog
> dotnet add package Serilog.Extensions.Logging

<or>

> dotnet add package Serilog.AspNetCore

Step 2 – Installing Sinks, what are these?

Sinks are as the name suggests; they are the outlets to which you want Serilog to write to. There are many sinks available to write to, including popular ones such as File, Database, AWS CloudWatch, Azure App Insights and so on.

In this guide, I want to show you how to –

  • log to a File and
  • log to a Database

since these are the most common approaches we generally apply in our applications.

File Logging with Serilog

To log to a file with Serilog, we need to install File sink. It goes as below –

> dotnet add package Serilog.Sinks.File

Heads up – You don’t need to install this if you’re using Serilog.AspNetCore package

Step 3 – Add Serilog to ILogger

The third step is to configure Serilog as a Logging provider to the LoggingBuilder. Again, we have two ways to do this:

  1. We can add Logger service to the ServiceCollection and then configure Serilog. We do this as below:
// startup.cs; inside ConfigureServices() method

services.AddLogging(builder => { 
    builder.AddSerilog(_serilogLogger);
});
  1. We can also configure Logger while building the Host inside the Program.cs, as below:
public static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
            .ConfigureWebHostDefaults(webBuilder =>
                {
                    webBuilder.UseStartup<Startup>();
                })
            // attach Serilog as a logging service 
            // while building the host
            .ConfigureLogging(logger =>
            {
                logger.AddSerilog(_serilogLogger);
            });

On the other hand, if we install Serilog.AspNetCore nuget package which as I mentioned before is a complete package that contains these two packages as well –

we can configure Serilog in a third way, using an extension method over the IHostBuilder as below. This sets Serilog as the default Logging provider on this host.

public static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
            .ConfigureWebHostDefaults(webBuilder =>
                {
                    webBuilder.UseStartup<Startup>();
                })
            // set serilog as default logging provider
            .UseSerilog(_serilogLogger);

Configuring the Logger

If you observe, I’ve been passing a variable _serilogLogger in all these configuration approaches. What is this variable for?

This is an instance of ILogger which contains all the configuration about how the Serilog logger should work.

The variable definition is as follows:

namespace ContainerNinja
{
    public class Startup
    {
        private readonly ILogger _serilogLogger;

        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
            _serilogLogger = new LoggerConfiguration()
                    // attach sinks here
                    .CreateLogger();
        }

        public IConfiguration Configuration { get; }

        ...

We create a Serilog.Core.Logger object from the Logger Configuration and pass this to the Serilog extension methods while configuring it as the logging provider. In this section, we want to configure Serilog to write all the logs captured into a File. To do this, we attach a File sink to this LoggerConfiguration() with just one line as below:

_serilogLogger = new LoggerConfiguration()
    .WriteTo.File(
        "logs/log.log", 
        rollingInterval: RollingInterval.Hour)
    .CreateLogger();

WriteTo is a property on the LoggerConfiguration which returns an instance of LoggerSinkConfiguration. Because we have installed Serilog.Sinks.File package, we have an extension method File() over this instance.

It takes a parameter of the path where the files need to be created, which we have given as “logs/log.log”. This means that Serilog writes to file under logs directory inside the ContentRoot. You can find more about the ContentRoot and WebRoot here.

The named parameter rollingInterval defines the interval at which Serilog rolls over to a new file, this depends on how frequent we are logging. Since I’ve configured it to RollingInterval.Hour, the file name is appended with the date and hour at which the log is created.

Serilog logs in this format by default – “{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj}{NewLine}{Exception}”.


That’s all is there to Serilog integration! 🥳

From here on, we can use the ILogger interface provided for us by the Microsoft.Extensions.Logging package, which is built into ASP.NET Core and all the logs we generate are captured and rolled over to file via Serilog.

File logging in Action

For example, inside the AliveController I inject and write a simple log that is written when the Get() method is called:

namespace ContainerNinja.Controllers.V1
{
    [ApiController]
    [ApiVersion("1.0")]
    [Route("api/v{version:apiVersion}/[controller]")]
    public class AliveController : ControllerBase
    {
        private readonly ILogger<AliveController> _logger;

        public AliveController(ILogger<AliveController> logger)
        {
            _logger = logger;
        }

        [HttpGet]
        public string Get() 
        {
            _logger.LogInformation("Executing Action AliveController.Get()");

            return "API is alive!";
        }
    }
}

You can see that I haven’t done anything out of the ordinary here – I just injected ILogger with the current controller type parameter passed. I’m calling _logger.LogInformation() method with some message to log.

Heads up – What are these Log Levels? 🤔

While writing logs, we can segregate the data into several levels based on the kind of data we’re writing. The framework provides us with the following levels by default –

0TraceLogs that contain the most detailed messages. These messages may contain sensitive application data. These messages are disabled by default and should never be enabled in a production environment.
1DebugLogs that are used for interactive investigation during development. These logs should primarily contain information useful for debugging and have no long-term value.
2InformationLogs that track the general flow of the application. These logs should have long-term value.
3WarningLogs that highlight an abnormal or unexpected event in the application flow, but do not otherwise cause the application execution to stop.
4ErrorLogs that highlight when the current flow of execution is stopped due to a failure. These should indicate a failure in the current activity, not an application-wide failure.
5CriticalLogs that describe an unrecoverable application or system crash, or a catastrophic failure that requires immediate attention.
6NoneNot used for writing log messages. Specifies that a logging category should not write any messages.
Log Levels

You can find more about these Log Levels here.


When I run this solution, I can see there’s a logs folder created in my API project folder, and I can see one file log2022053111.log created (which is the time I ran this application).

file created with serilog

And when I open this file, I can see the log printed (along with all other logs generated by other middlewares configured in the application).

file contents look like this

Database Logging with Serilog

Serilog also provides a sink to log to SQL Server, with several useful customizations to offer. To configure this, we need to install the following Sink.

> dotnet add package Serilog.Sinks.MSSqlServer

The rest of the configuration remains same as we did for File logging, except that now instead of WriteTo.File() method, we can the following:

_serilogLogger = new LoggerConfiguration()
                .WriteTo.MSSqlServer(
                    configuration.GetConnectionString("LoggingDbConnection"), 
                    sinkOptions: new Serilog.Sinks.MSSqlServer.MSSqlServerSinkOptions
                        {
                            AutoCreateSqlTable = true,
                            TableName = "SeriLogs"
                        })
                .CreateLogger();

Observe that we’ve now used the WriteTo.MSSqlServer() method. This method accepts a connection string to the Database to which it needs to write to. The other named option is sinkOptions which accepts an instance of MSSqlServerSinkOptions. Here we configure if we’re interested to create a new table for our Serilog Logging.

I provide the AutoCreateSqlTable flag to true and then provide a table name I like. Serilog now creates a table with all the columns for me and writes to this table whenever new logs are generated. I can also customize the default columns through the columnOptions named parameter, which can look something like this.

_serilogLogger = new LoggerConfiguration()
                .WriteTo.MSSqlServer(
                    configuration.GetConnectionString("LoggingDbConnection"),
                    sinkOptions: new Serilog.Sinks.MSSqlServer.MSSqlServerSinkOptions
                    {
                        AutoCreateSqlTable = true,
                        TableName = "SeriLogs"
                    },
                    columnOptions: new Serilog.Sinks.MSSqlServer.ColumnOptions
                    {
                        // customizing the Exception column 
                        // there are options to customize
                        // other columns as well
                        Exception = new Serilog.Sinks.MSSqlServer.ColumnOptions.ExceptionColumnOptions
                        {
                            AllowNull = true,
                            ColumnName = "Exception",
                            NonClusteredIndex = false,
                            PropertyName = "Exception"
                        }
                    })
                .CreateLogger();

Once done, when I run this application I can now see the logs generated being stored in the table SeriLogs inside the database I provided.

log table is created which looks like this

Just to verify, I’ll throw a custom exception inside the AliveController and we can see that the exception is also logged inside the database.

    [HttpGet]
    public string Get()
    {
        _logger.LogInformation("Executing Action AliveController.Get()");
        throw new System.Exception("Some Exception");
    }
even an unhandled exception is logged with full trace

Configuring Serilog via appsettings JSON

Now you might be thinking, we’re configuring all the sinks and customizations within the code which might require a code change everytime we want some configuration to be updated. Isn’t there a better approach to this?

The answer is yes, we can also configure the Serilog logger through the appSettings json and let the Logger be configured via the IConfiguration object. The appSettings file looks like below:

{
    "Serilog": {
    "Using": [
      "Serilog.Sinks.Console",
      "Serilog.Sinks.File",
      "Serilog.Sinks.MSSqlServer"
    ],
    "MinimumLevel": "Debug",
    "WriteTo": [
      {
        "Name": "Console"
      },
      {
        "Name": "File",
        "Args": {
          "path": "Logs/log.log",
          "rollingInterval": "Hour"
        }
      },
      {
        "Name": "MSSqlServer",
        "Args": {
          "connectionString": "Data Source=(localdb)\\mssqllocaldb;Database=serilogninja;Connection Timeout=30",
          "sinkOptionsSection": {
            "autoCreateSqlTable": true,
            "tableName": "SeriLogs"
          }
        }
      }
    ],
    "Enrich": [
      "FromLogContext",
      "WithMachineName",
      "WithThreadId"
    ],
    "Destructure": [
      {
        "Name": "ToMaximumDepth",
        "Args": {
          "maximumDestructuringDepth": 4
        }
      },
      {
        "Name": "ToMaximumStringLength",
        "Args": {
          "maximumStringLength": 100
        }
      },
      {
        "Name": "ToMaximumCollectionCount",
        "Args": {
          "maximumCollectionCount": 10
        }
      }
    ],
    "Properties": {
      "Application": "ContainerNinja"
    }
  }
}

And in the code, we can just attach the configuration to the Serilog –

_serilogLogger = new LoggerConfiguration()
    .ReadFrom.Configuration(configuration)
    .CreateLogger();

Buy Me A Coffee

Found this article helpful? Please consider supporting!

Conclusion

Serilog is a simple and efficient logging provider that has a great integration with ASP.NET Core framework. Serilog specializes in logging structural data and we can further pass on the data logged by serilog to several outlets or sinks. In this article, we’ve seen Console, File and Database logging through Serilog which are all both clean and simple to implement.

Further more, we can externalize all this configuration into our appSettings JSON and link it up to the Serilog logger which is a great feature for starters.

How are you going to implement Serilog logging? Let me know your thoughts below 👇


Buy Me A Coffee

Found this article helpful? Please consider supporting!

Ram
Ram

I'm a full-stack developer and a software enthusiast who likes to play around with cloud and tech stack out of curiosity. You can connect with me on Medium, Twitter or LinkedIn.

Leave a Reply

Your email address will not be published. Required fields are marked *