Adding Serilog to ASP.NET Core: a practical guide
Serilog is a logging library for .NET. It can be used on its own, but it is also compatible with Microsoft.Extensions.Logging
, making it ideal for ASP.NET Core applications. In this article, we’ll explain why you should use Serilog in your ASP.NET Core application and demonstrate how to integrate it into your project. We’ll also cover ASP.NET middleware as a way to enrich your logs with contextual properties, and techniques to add high-verbosity logging without boilerplate code.
Why use Serilog in ASP.NET Core?
By default, ASP.NET Core apps have a built-in logging system thanks to the Microsoft.Extensions.Logging
namespace. However, it lacks many useful features. And while it is extensible, someone has to implement and maintain those extensions. Therefore, it often makes sense to use a third-party library like Serilog, which is already feature-rich and well-maintained.
Here are the features that make Serilog a good choice for ASP.NET Core applications:
- Contexts and enrichers: While
Microsoft.Extensions.Logging
supports scopes, they are quite limited and verbose. Serilog contexts make it much easier to add custom contextual information to your logs. Enrichers provide automatic ways of adding commonly used information, such as the correlation ID. - Sinks, formatting, and filtering:
Microsoft.Extensions.Logging
supports a limited number of logging providers, while Serilog offers about a hundred different sinks, including logging to a file and various cloud services and databases. Serilog also supports advanced formatting and filtering options. - Structured logging:
Microsoft.Extensions.Logging
has only very limited support for structured logging, while Serilog is built around structured logging. This makes it easier to query and analyze logs, especially when you have a large number of logs.
Using Serilog is not an all-or-nothing decision. As long as you use only Serilog sinks, you can, for example, combine Microsoft.Extensions.Logging
scopes and Serilog contexts.
Adding Serilog to your ASP.NET Core project
Step 1. Add the Serilog packages
To add Serilog to your ASP.NET Core project, install the Serilog.AspNetCore
NuGet package:
dotnet add package Serilog.AspNetCore
This package includes support for registering Serilog with your application and optionally enabling request logging.
Step 2. Add the Serilog service
To configure Serilog, add code like the following to your Program.cs
file:
Log.Logger = new LoggerConfiguration()
.WriteTo.Console()
.CreateLogger();
builder.Services.AddSerilog();
You can also delete the now unnecessary Microsoft.Extensions.Logging
configuration from your appsettings.json
file.
The console output from an ASP.NET Core Web API application will now look something like this:
[15:56:46 INF] Now listening on: http://localhost:5106
[15:56:46 INF] Application started. Press Ctrl+C to shut down.
[15:56:46 INF] Hosting environment: Development
[15:56:46 INF] Content root path: /mnt/c/src/TimelessDotNetEngineer/src/logging/serilog-aspnetcore/SerilogInAspNetCore
[15:56:53 INF] Request starting HTTP/1.1 GET http://localhost:5106/swagger/index.html - null null
[15:56:53 INF] Request finished HTTP/1.1 GET http://localhost:5106/swagger/index.html - 200 null text/html;charset=utf-8 71.1747ms
[15:56:53 INF] Request starting HTTP/1.1 GET http://localhost:5106/swagger/v1/swagger.json - null null
[15:56:54 INF] Request finished HTTP/1.1 GET http://localhost:5106/swagger/v1/swagger.json - 200 null application/json;charset=utf-8 70.2124ms
Step 3. Enable HTTP request logging
Since the default ASP.NET Core logging is very noisy, it’s a good practice to reduce its verbosity (using .MinimumLevel.Override(...)
) and replace it with Serilog request logging by adding a call to app.UseSerilogRequestLogging()
in your Program.cs
:
Log.Logger = new LoggerConfiguration()
.WriteTo.Console()
.MinimumLevel.Verbose()
.MinimumLevel.Override( "Microsoft.AspNetCore", LogEventLevel.Warning )
.MinimumLevel.Override( "Microsoft.Extensions.Hosting", LogEventLevel.Information )
.MinimumLevel.Override( "Microsoft.Hosting", LogEventLevel.Information )
.CreateLogger();
builder.Services.AddSerilog();
var app = builder.Build();
app.UseSerilogRequestLogging();
Using that, the output will be nicer:
[16:18:03 INF] Now listening on: http://localhost:5106
[16:18:03 INF] Application started. Press Ctrl+C to shut down.
[16:18:03 INF] Hosting environment: Development
[16:18:03 INF] Content root path: /mnt/c/src/TimelessDotNetEngineer/src/logging/serilog-aspnetcore/SerilogInAspNetCore
[16:18:07 INF] HTTP GET /swagger/index.html responded 200 in 57.3368 ms
[16:18:07 INF] HTTP GET /swagger/v1/swagger.json responded 200 in 68.1877 ms
Adding logging to your code
Now that Serilog is properly configured, let’s talk about how and when to add logging to your code.
Step 1. Pull the ILogger service
The first step is to pull the logging interface from the dependency injection container. It’s recommended to use the regular Microsoft.Extensions.Logging.ILogger
interface instead of the product-specific Serilog.ILogger
one because it will make your code more standard and portable. However, there are no significant differences between these interfaces.
Instead of pulling the non-generic ILogger
interface, pull the generic ILogger<T>
interface where T
is the current class. This trick will allow you to adjust the verbosity of the logging for this specific class and easily filter or search messages reported by this specific class.
[ApiController]
[Route( "[controller]" )]
public class WeatherForecastController( ILogger<WeatherForecastController> logger ) : ControllerBase
Step 2. Add logging instructions
The general rules are not specific to Serilog: you should log whenever something happens that might be useful to troubleshoot a problem in production. Use the LogError
, LogWarning
, LogInformation
, and LogVerbose
methods of the ILogger
interface to write messages of different severities.
By default, Serilog will use the ToString()
method to format parameters to text. If you want Serilog to include the JSON representation of the log parameter instead, you can use the @
destructuring operator. This will work even if you use the regular Microsoft.Extensions.Logging.ILogger
interface.
logger.LogDebug(
"Returning weather forecast for the {days} days after today: {@forecast}",
days,
forecast );
This will produce a log message like this:
[19:09:56 DBG] Returning weather forecast for the 5 days after today: [{"Date": "2024-07-10", "Temperature": 31, "$type": "WeatherForecast"}, {"Date": "2024-07-11", "Temperature": 26, "$type": "WeatherForecast"}, {"Date": "2024-07-12", "Temperature": 31, "$type": "WeatherForecast"}, {"Date": "2024-07-13", "Temperature": 19, "$type": "WeatherForecast"}, {"Date": "2024-07-14", "Temperature": 25, "$type": "WeatherForecast"}]
Step 3. Optionally, add scope information
In the examples above, we wrote the logs as text to the console. Serilog supports several semantic backends like Elasticsearch or the .NET Aspire dashboard, where Serilog messages are stored as JSON objects. This allows you to easily search and filter messages based on object queries rather than text. This is what is meant by semantic logging.
Serilog supports several mechanisms to add properties to the JSON payload. Often, you’ll want to search according to the execution context (the current username, client IP, operation, etc.), so it’s a good idea to enrich messages with these properties.
To enable this feature, the first thing to do is add a call to .Enrich.FromLogContext()
in your Serilog configuration code.
If you want to visualize properties in the console or text output, you also need to override the console message template.
These two steps are achieved by the following snippet:
Log.Logger = new LoggerConfiguration()
.Enrich.FromLogContext()
.WriteTo.Console(
outputTemplate:
"[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj} {Properties}{NewLine}{Exception}" )
We can now add contextual information to our logging.
The first and standard way is to use the BeginScope
method of the Microsoft.Extensions.Logging.ILogger
interface:
using ( logger.BeginScope( "Getting weather forecast for {ScopeDays} days", days ) )
This approach defines a text property named Scope
, which is not very semantically searchable.
Serilog’s proprietary API offers a better solution: adding properties to the current execution context using the LogContext.PushProperty
static method. You can use it from anywhere. If you want to enrich the logs with information about the current HTTP request, it’s best to create an ASP.NET Middleware. A middleware is a C# class with a single method InvokeAsync
that gets called in the request pipeline, before any controller or API is called. Here is an example that pushes two properties: Client
and RequestId
.
public sealed class PushPropertiesMiddleware : IMiddleware
{
public async Task InvokeAsync( HttpContext context, RequestDelegate next )
{
var requestId = Guid.NewGuid().ToString();
using ( LogContext.PushProperty( "Client", context.Request.Host ) )
using ( LogContext.PushProperty( "RequestId", requestId ) )
{
await next( context );
}
}
}
To add the middleware to the pipeline, you must first add it to the service collection:
builder.Services.AddSingleton<PushPropertiesMiddleware>();
Then, call the UseMiddleware
method.
app.UseMiddleware<PushPropertiesMiddleware>();
Keep in mind that using a middleware is just one of the options. You can call LogContext.PushProperty
from anywhere.
Automatically adding verbose logging to your code
Adding logging to your code by hand makes sense for low-volume, high-relevance pieces of information such as errors, warnings, and important messages. However, it would be a severe waste of time for more noisy messages. If all you want is better request tracing, you can write messages from your custom middleware. If you want logging deep inside your application and still avoid boilerplate, you can use a free code generation tool like Metalama.
With Metalama, you can create a template (called an aspect) that teaches the compiler how to implement logging, then apply the templates to all required methods.
First, install the Metalama.Extensions.DependencyInjection. Most of the magic is done by the Metalama.Framework package, which is implicitly added.
Then, create an aspect to fit your logging needs. Here’s a simple example:
using Metalama.Extensions.DependencyInjection;
using Metalama.Framework.Aspects;
using Microsoft.Extensions.Logging;
public class LogAttribute : OverrideMethodAspect
{
[IntroduceDependency]
private readonly ILogger _logger;
public override dynamic? OverrideMethod()
{
var formatString = BuildFormatString();
var isLoggingEnabled = this._logger.IsEnabled( LogLevel.Trace );
try
{
if ( isLoggingEnabled )
{
this._logger.LogTrace(
formatString + " started.",
(object[]) meta.Target.Parameters.ToValueArray() );
}
return meta.Proceed();
}
finally
{
if ( isLoggingEnabled )
{
this._logger.LogTrace(
formatString + " finished.",
(object[]) meta.Target.Parameters.ToValueArray() );
}
}
}
[CompileTime]
private static string BuildFormatString()
{
var parameters = meta.Target.Parameters
.Where( x => x.RefKind != RefKind.Out )
.Select( p => $"{p.Name}: {{{p.Name}}}" );
return $"{meta.Target.Type}.{meta.Target.Method.Name}({string.Join( ", ", parameters )})";
}
}
You can now add the aspect to all methods you want to log. You can mark methods one at a time using the [Log]
attribute. Another option is to use a Metalama fabric (a commercial feature) to add logging more broadly using a single piece of code.
using Metalama.Framework.Code;
using Metalama.Framework.Fabrics;
internal class Fabric : ProjectFabric
{
public override void AmendProject( IProjectAmender amender )
{
amender
.Select( c => c.GlobalNamespace.GetDescendant( "SerilogInAspNetCore.Controllers" )! )
.SelectMany( c => c.Types )
.Where( t => t.Accessibility == Accessibility.Public )
.SelectMany( c => c.Methods )
.Where( m => m.Accessibility == Accessibility.Public )
.AddAspectIfEligible<LogAttribute>();
}
}
When applied to a WeatherForecastController.Get(int days)
method (whether using an attribute or a fabric), the aspect generates code equivalent to the following at the start of the method:
var isLoggingEnabled = this._logger.IsEnabled( LogLevel.Trace );
try
{
if ( isLoggingEnabled )
{
this._logger.LogTrace( "WeatherForecastController.Get(days: {days}) started.", days );
}
// Method body here.
}
finally
{
if ( isLoggingEnabled )
{
this._logger.LogTrace( "WeatherForecastController.Get(days: {days}) finished.", days );
}
}
Summary
Serilog is an excellent semantic logging provider and it’s fully compatible with ASP.NET Core. In this article, we discussed how to set up Serilog, and how to use it in your code in different scenarios, including the use of an ASP.NET middleware. We mentioned Metalama as a possible tool to add verbose logging to your code without boilerplate.