C# - Serilog, how to enrich all request log events with properties in asp.net

This post describes how to combine serilog's property enrichment with ASP.Net's middleware and pipeline to enrich all logs in a request with properties. A full example of the program.cs used in this post is at the bottom of the page. Note: if you are looking for logging all requests with their status codes etc. you should look into the ASP.net HTTP request Logging.

We will start with our loggerConfiguration:

var app = builder.Build();
var loggerConfiguration = new LoggerConfiguration();

Log.Logger = loggerConfiguration
    .Enrich.FromLogContext()
    .WriteTo
    .Console(outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] {Properties:j}{Message:lj}{NewLine}{Exception}")
    .CreateLogger();

In the above we create a new serilog logger, notably we Enable enrichment from LogContext and we change the outputTemplate so that we can see the properties in the output.

To have an endpoint to test against we will use the standard weather app with some modifications:

var summaries = new[]
{
    "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};

app.MapGet("/weatherforecast", () =>
{
    Log.Logger.Information("Starting!");
    WeatherForecast[] forecast = GetForecast(summaries);
    using (LogContext.PushProperty("LengthOfForecast", forecast.Length))
    {
        Log.Logger.Information("Forecast created!");
    }

    return forecast;
})
.WithName("GetWeatherForecast")
.WithOpenApi();

//Not important
static WeatherForecast[] GetForecast(string[] summaries)
{
    return Enumerable.Range(1, 5).Select(index =>
    new WeatherForecast
    (
        DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
        Random.Shared.Next(-20, 55),
        summaries[Random.Shared.Next(summaries.Length)]
    ))
    .ToArray();
}

In the above we have a simple endpoint /weatherforecast that returns a weather forecast. It creates two log entries, one with the message "Starting!" and another with the message "Forecast Created!" with a property LengthOfForecast that has the length of the forecast array (yeah I know, it is always five). Let us imagine that we want to add a correlation Id to all our ASP.net requests, in order to do this we have to add some middleware:

app.Use(async (context, next) =>
{
    using (LogContext.PushProperty("CorrelationId", Guid.NewGuid()))
    {
        Log.Logger.Information("Added correlationId!"); //ÓK, unneccessary in real implementation!
        await next.Invoke();
    }
});

In the above we push a GUID as a CorrelationId property in the pipeline. All subsequent middleware and endpoints will have this property on their LogContext. If we run the above code and hit the /weatherforecast endpoint we will get the following log entries:

[08:46:14 INF] {"CorrelationId": "935b8b13-a8de-48c7-a165-0304c487d41d", "RequestId": "0HMSJTKK2IVUD:00000009", "RequestPath": "/weatherforecast"}
Added correlationId!
[08:46:14 INF] {"CorrelationId": "935b8b13-a8de-48c7-a165-0304c487d41d", "RequestId": "0HMSJTKK2IVUD:00000009", "RequestPath": "/weatherforecast"}
Starting!
[08:46:14 INF] {"LengthOfForecast": 5, "CorrelationId": "935b8b13-a8de-48c7-a165-0304c487d41d", "RequestId": "0HMSJTKK2IVUD:00000009", "RequestPath": "/weatherforecast"}
Forecast created!

We can see in the above that all our log events have the same correlation id (CorrelationId). We can also see that the additionally enriched property LengthOfForecast from the endpoint is also there.

That is all

I hope you found this helpful, please leave a comment down below if you did. Also you can find a full working example of the above just below, in case you are missing some context.

Have a great day!

Full working code example of the program.cs

using Serilog;
using Serilog.Context;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Host.UseSerilog();


var app = builder.Build();
var loggerConfiguration = new LoggerConfiguration();

Log.Logger = loggerConfiguration
    .Enrich.FromLogContext()
    .WriteTo
    .Console(outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] {Properties:j}{Message:lj}{NewLine}{Exception}")
    .CreateLogger();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();

var summaries = new[]
{
    "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};

app.MapGet("/weatherforecast", () =>
{
    Log.Logger.Information("Starting!");
    WeatherForecast[] forecast = GetForecast(summaries);
    using (LogContext.PushProperty("LengthOfForecast", forecast.Length))
    {
        Log.Logger.Information("Forecast created!");
    }

    return forecast;
})
.WithName("GetWeatherForecast")
.WithOpenApi();

app.Use(async (context, next) =>
{
    using (LogContext.PushProperty("CorrelationId", Guid.NewGuid()))
    {
        Log.Logger.Information("Added correlationId!"); //ÓK, unneccessary in real implementation!
        await next.Invoke();
    }
});

app.Run();

static WeatherForecast[] GetForecast(string[] summaries)
{
    return Enumerable.Range(1, 5).Select(index =>
    new WeatherForecast
    (
        DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
        Random.Shared.Next(-20, 55),
        summaries[Random.Shared.Next(summaries.Length)]
    ))
    .ToArray();
}

internal record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary)
{
    public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}