In this article, we’ll take a look at some best practices when it comes to logging in .NET with Serilog.
Logging is essential to any application we build and Serilog is one of the major logging providers. For this reason, we’ve prepared some Serilog best practices to help you improve your logs. Through the article, we’ll use the boilerplate weather API to showcase any of the practices mentioned here.
Let’s start!
Avoid the Static Logger Class
Serilog comes with its static Log
class that we can use through our application to log events and information. We can use it to access its Logger
property and write any sort of logs we wish. But by doing this we break the dependency inversion principle.
We can easily integrate Serilog with Microsoft’s built-in logging interface, which is one of the most useful practices to follow. By employing this approach, we not only stick to the dependency inversion principle but also make testing our application much easier.
However, one of the use cases for the static Log
class is in the Program
class:
Log.Logger = new LoggerConfiguration() .WriteTo.File( "logs/log.txt", retainedFileCountLimit: 7, rollingInterval: RollingInterval.Day) .MinimumLevel.Information() .CreateLogger(); try { var builder = WebApplication.CreateBuilder(args); // code ommited for brevity app.Run(); } catch (Exception ex) { Log.Error(ex, "The exception was thrown during application startup"); } finally { Log.CloseAndFlush(); }
We use the Logger
property of the static Log
class to configure it to write the logs to a file. Then we wrap our application’s configuration in a try
–catch
block – this way we’ll log any errors that happen during the start process of our application. The finally
block just takes care of closing and flushing the remaining logs.
We can also use the static Log
class in any other places where dependency injection is not possible.
Configure Serilog From appsettings.json
First and foremost, we need to configure Serilog corresponding to our needs. We have two options: Fluent API or the configurations system. While the Fluent API is very intuitive and easy to read, there is one big downside – every time we change something in our configuration, we need to publish a new build of our application.
This is why it’s better to use the configuration system to set up Serilog in our applications:
Install-Package Serilog.Settings.Configuration
We start by installing the Serilog.Settings.Configuration
NuGet package.
Now that we have the package, let’s add our basic configuration:
"Serilog": { "Using": [ "Serilog.Sinks.Console" ], "MinimumLevel": { "Default": "Information" }, "WriteTo": [ { "Name": "Console", "Args": { "OutputTemplate": "[{Timestamp:HH:mm:ss} {Level:u11}] {Message:lj}{NewLine}" } } ], "Properties": { "ApplicationName": "Weather API" } }
First, in the appsettings.json
file, we add a new section called Serilog
. Inside it, we start by creating the Using
sub-section in which we state the sink that we want to use.
Next, we configure the default log level. Then we move on to the WriteTo
sub-section, where we configure the different sinks, which can even include things like the message template.
Then, we need to apply the configuration:
builder.Host.UseSerilog((context, config) => config.ReadFrom.Configuration(context.Configuration));
In our Program
class, we use the UseSerilog()
extension method to specify that our application should use the appsettings.json
file to configure Serilog. With this, we remove the need to republish our application when our logging configuration changes.
Forget About Serilog’s Console and File Sinks in Production
When we develop an application it’s great to see event logs as they happen. Logging to the console is a great way to achieve this. However, logging everything to the console can make it very difficult to track down events as it quickly gets clogged with information. Moreover, we want to stay away from this in our production environment as it may cause performance issues.
The Serlilog’s file sink is even better than the console one when it comes to development. We can easily filter and sort the logs making searching for certain events very easy. However, as with console logging, this becomes very burdensome to deal with in production.
For production purposes, we can use Seq, Elasticsearch, or any other Serilog sink that is more suited for production environments. This way we can gain better scalability and reliability compared to file or console logs.
It’s worth mentioning that depending on our situation, there might be scenarios where file and console logging can have their uses in Production. For example, our log provider might have a problem receiving logs so having file or console logs might prove useful.
Always Use Structured Logging
When possible, we should always avoid simple strings when logging:
logger.LogInformation( $"The weather today will be {forecast[0].Summary} and {forecast[0].TemperatureC} degrees.");
This will produce a simple log message that may not be very useful. Moreover, even though the first parameter of ILogger<T>
‘s LogInformation()
method has the name of message
, this is not correct and this is the message template.
Let’s use it properly:
logger.LogInformation( "The weather today will be {Summary} and {Temperature} degrees.", forecast[0].Summary, forecast[0].TemperatureC);
Here, we first pass the message template and then follow up with the two parameters required by it. This will produce a structured log, where both the Summary
and Temperature
will be stored as parameters associated with the log message. This makes querying our logs much easier, saving us time and effort.
Use Built-in Event Log Enrichers
Adding additional information to our logs can prove beneficial.
For this purpose, Serilog comes with various built-in log event enriches:
Install-Package Serilog.Enrichers.Thread Install-Package Serilog.Enrichers.Process Install-Package Serilog.Enrichers.Environment
We start by installing Serilog’s Thread
, Process
, and Environment
enrichment packages.
Next, we update our configuration:
"Enrich": [ "WithThreadId", "WithProcessId", "WithMachineName", "WithEnvironmentName" ]
In the appsettings.json
file, we add a new sub-section called Enrich
. Inside it, we add properties specifying that Serilog should enrich our logs with ThreadId
, ProcessId
, MachineName
as well as EnvironmentName
.
We can send a request to our API and explore the output in Seq:
We can see that the log includes all the additional properties we specified in the Enrich
sub-section of the appsettings.json
file.
Create Custom Log Event Enricher for Serilog
It comes as no surprise that we can create custom log event enrichers:
public class ThreadPriorityEnricher : ILogEventEnricher { public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory) { logEvent.AddPropertyIfAbsent( propertyFactory.CreateProperty( "ThreadPriority", Thread.CurrentThread.Priority.ToString())); } }
We start by creating the ThreadPriorityEnricher
class and implementing the ILogEventEnricher
interface. The interface forces us to implement the Enrich()
method. Using the LogEvent
class’ AddPropertyIfAbsent()
method, we try to add a new property to the logs if it’s not already present. Our additional property will add the thread priority to the log events. To get the priority itself we use the Thread
class and its properties. The property is attached to the logs via the CreateProperty()
method of the ILogEventPropertyFactory
interface.
Next, we register the enricher:
builder.Host.UseSerilog((context, config) => config.ReadFrom.Configuration(context.Configuration) .Enrich.With(new ThreadPriorityEnricher()));
In the Program
class, we use the With()
method on the Enrich
property of the LoggerConfiguration
class. To the method, we pass a new instance of our ThreadPriorityEnricher
class.
With this, all logs of our application will have the ThreadPriority
as a property.
Request Logging With Serilog
Requests are a vital part of one application so detailed logging is a must:
app.UseSerilogRequestLogging();
In our Program
class, we call the UseSerilogRequestLogging()
extension method on our WebApplication
instance. With this, our request logs will now have information about the HTTP method, path, status code, and how long it took for our application to respond.
We can go a step further and create a custom request log enricher:
public static class RequestEnricher { public static void LogAdditionalInfo( IDiagnosticContext diagnosticContext, HttpContext httpContext) { diagnosticContext.Set( "ClientIP", httpContext.Connection.RemoteIpAddress?.ToString()); } }
We start by creating a new RequestEnricher
class.
Next, we create the LogAdditionalInfo()
method. It takes two parameters: Serilog’s IDiagnosticContext
and HttpContext
instances. Then, we use the diagnostic context’s Set()
method to create the ClientIP
property and assign it the RemoteIpAddress
property of the HttpContext
that is passed to the method.
Note that this is different from implementing the ILogEventEnricher
interface and has to be registered differently.
Next, we add our custom enricher:
app.UseSerilogRequestLogging(options => options.EnrichDiagnosticContext = RequestEnricher.LogAdditionalInfo);
To do this, we set to EnrichDiagnosticContext
property inside the UseSerilogRequestLogging()
method to be equal to the LogAdditionalInfo()
method we just wrote.
Finally, we can send a request and check the log:
We can see that our log now has information about the HTTP method, path, and status code. We also get a property called ClientIP
with a value of ::1
which means we send the request from the same machine on which our application is running.
Conclusion
In conclusion, mastering logging practices with Serilog in .NET is essential for optimizing our application’s performance and troubleshooting. By configuring Serilog through the application settings we gain the flexibility to change logging settings without requiring constant republishing.
We ensure scalability and reliability by steering clear of console and file sinks in favor of specialized alternatives when it comes to production.
When we enrich logs with additional information and adopt detailed request-logging practices we further enhance our application’s diagnostic capabilities. By adhering to the practices mentioned here, we can harness Serilog’s power to create robust, insightful logging solutions for our applications. We hope you enjoyed exploring some of the Serilog best practices, please let us know in the comments of any others you think are worthy contenders.