Logging with Serilog

I recently needed to add logging to my Static Site Generator project. After spending some time researching, I discovered the generic .NET Core interfaces for logging without locking into a specific logging tool. Unfortunately, Microsoft's provided implementation of those interfaces doesn't provide a logger that sends its information to a file, so I went in search of something that would. Serilog came up frequently in my searches, so I started out using that, and I did run into a few issues during my experimentation. Ultimately, I switched to Nreco.Logging.File only because Serilog was overkill for my very simple use case.

What I needed

Before getting directly into the nitty-gritty of Serilog, it helps to understand the background. My static site generator project is a C# program that accepts a path to a folder containing Markdown files (and supporting files like images) and a path to a template (to help style the result) and outputs HTML files (and supporting files) to create a website. To support debugging, I wanted to start implementing logging in the project to make it easier to troubleshoot issues as they come up. I also wanted to future proof whatever code I wrote given that I will likely need to change my approach as the project grows. This is why I wanted to find a logger that supported the ILogger<T> interface from Microsoft. Hopefully, if the tool I pick fills that interface, it should be easy to migrate to whatever solution I need for logging later. I will not be discussing those interfaces in much detail here, but it is worth exploring if you are writing C# today!

One other thing worth knowing before looking at examples is that my project is using the .NET Core dependency injection (DI) container framework. I will also not be going in depth on that particular tool here, though if you are unfamiliar with the general pattern of DI or with DI containers more specifically, you may want to take a look at this Wikipedia article.

What is Serilog

Serilog is a logging library written in C# designed specifically to handle structured data efficiently. It has a main library that contains an API for writing to a log, and separate "sinks" for each target log type (such as a console window, a file, or an S3 bucket). This means that you can write logging code for one API and send it to one or more output mechanism(s) without changing the actual logging code. If you choose to write to multiple sinks, you can configure different options for each sink independently. That means that you could have only "info" level logs write to the console and have a separate "debug" level log written to a hidden file. That's useful when logging because you may want to provide some feedback to users but wouldn't want to send every logged statement to users, since that would include details that are meaningless to end users. It's also worth noting that this kind of "log multiplexing" is standard functionality for the libraries I have seen (it isn't unique to Serilog).

Setup

Unfortunately, setting up Serilog is the most complex part of using it. First off, you will need to add the following nuget packages:

  • Microsoft.Extensions.Logging is the standard Microsoft logging interfaces. To make your code portable to other libraries, I highly recommend you use this.

  • Serilog.AspNetCore is the core logging API, but the only sink it can write to is the console, so for our purpose, we will also need another package.

  • Serilog.Sinks.File is the sink for writing to a file.

Once you have those packages installed, setting up a logger is fairly simple

    
                // Grab a configuration object
            Log.Logger = new LoggerConfiguration() 
                 // Set a minimum logging level for all sinks
                 .MinimumLevel.Verbose()           
                 // Set up a particular sink. In our case, we want to log to a file called
                 // "log.txt" and we want it to only keep a day's worth of entries
                 .WriteTo.File("log.txt", rollingInterval: RollingInterval.Day)
                 // Finally, use this configuration to build a logger
                 .CreateLogger();

    

That is all well and good for a simple program, but if you are using a dependency injection framework, you probably want to integrate the logger with your DI framework. The Dotnet Core DI framework gives us some tools for doing that like below.

    
                services.AddLogging(loggingBuilder =>
            {
                loggingBuilder.AddSerilog()
                    .AddFilter("Microsoft", LogLevel.Warning)
                    .AddFilter("System", LogLevel.Warning)
                    .AddFilter("StaticSiteGenerator.Program", LogLevel.Information);
            });

    

Final Thoughts

As I mentioned before, while I did try out Serilog I actually went with a different solution for my project so far. I think the really interesting features of Serilog (the structured data logging in particular) are only useful when your interfaces in your program are stable, but most of what I needed from my logging library was just writing simple strings for use when debugging a project that wasn't even in an alpha stage yet (meaning things were being changed and refactored constantly.) I will likely return to Serilog once my code is more mature. Fortunately, I am using the Microsoft.Extensions.Logging library, so when I make that change, it should be more or less painless.


I am currently in the process of building my own static site generator! You can follow progress on that project here