{{< figure src="/blog/implementing-correlation-ids-fsharp-giraffe-serilog/01.webp" title="São Paulo's Penha subway station. (2022-08-03)" >}}
I spent a stupid amount of time trying to setup an ASP.NET Middleware to handle correlation IDs on requests. I must confess that I just got my first .NET and F#1 job, therefore most of the time spent was just getting used to the whole ecosystem. However during my trial and error I saw a bunch of blog posts showing me how to do this in different manners and a lot discussions about the correct order to implement things.
A correlation ID is a unique ID that is assigned to every transaction. So, when a transaction becomes distributed across multiple services, we can follow that transaction across different services using the logging information. --- Gaurav Kumar Aroraa, Lalit Kale and Kanwar Manish
This was written with the following versions:
- .NET SDK 6.0.400
- Giraffe 6.0.0 -
dotnet add package Giraffe -v 6.0.0
- Serilog 2.11.0 -
dotnet add package Serilog -v 2.11.0
- Serilog.AspNetCore -
dotnet add package Serilog.AspNetCore -v 6.0.1
Importing the needed modules
Let's get started by importing all the needed packages:
open System
open Microsoft.AspNetCore.Builder
open Microsoft.AspNetCore.Http
open Microsoft.Extensions.DependencyInjection
open Microsoft.Extensions.Hosting
open Microsoft.AspNetCore.Hosting
open Giraffe
open Serilog
open Serilog.Context
Starting the web host
Differently from Saturn, Giraffe doesn't have a computation expression to configure our web host. With that in mind, the code below must do the job.
module Entry =
open Configuration
Log.Logger <-
LoggerConfiguration()
.Enrich.FromLogContext()
.WriteTo.Console(
outputTemplate = "[{Timestamp:HH:mm:ss} {CorrelationId} {Level:u3}] {Message:lj}{NewLine}{Exception}"
)
.CreateLogger()
[<EntryPoint>]
let main args =
Host
.CreateDefaultBuilder(args)
.ConfigureWebHost(configureWebHost)
.UseSerilog()
.Build()
.Run()
0
The key parts of the code are:
.Enrich.FromLogContext()
- The
outputTemplate
containing theCorrelationId
property
We will define the configureWebHost
in another module called Configuration
.
This same module contains other helper functions related to the Host
configuration.
module Configuration =
let configureApp (builder: IApplicationBuilder) =
builder
.UseMiddleware<Middleware.CorrelationId>()
.UseGiraffe Endpoint.router
let configureServices (services: IServiceCollection) = services.AddGiraffe() |> ignore
let configureWebHost (builder: IWebHostBuilder) =
builder
.Configure(configureApp)
.ConfigureServices(configureServices)
.UseKestrel()
.UseUrls([| "http://0.0.0.0:8000" |])
.UseWebRoot("/")
|> ignore
Here we can see a Middleware.CorrelationId
being implemented as an ASP.NET
Middleware.
Implementing the middleware
The mechanism of this middleware is quite simple. One of the possible ways to
implement a correlation ID propagation on web APIs is to pass a unique value as
request header. In our case, it will be passed around on a header key called
X-Correlation-Id
.
module Middleware =
type CorrelationId(next: RequestDelegate) =
member this.Invoke(context: HttpContext) =
let headerName = "X-Correlation-Id"
let logPropertyName = "CorrelationId"
let success, value =
context.Request.Headers.TryGetValue headerName
let correlationId =
if success
then value.ToString()
else Guid.NewGuid().ToString()
context.Response.Headers.Add(headerName, correlationId)
using (LogContext.PushProperty(logPropertyName, correlationId)) (fun _ ->
next.Invoke(context)
)
The logic is the following:
- Check if there's a value on the
X-Correlation-Id
header key - If there's a value, we turn this into a string. Otherwise, we create a Guid as the correlation id.
- Add the header to the response with the extracted correlation id
Testing with an actual request
For a testing purpose, let's create a Hello, World! endpoint with a simple log.
module Endpoint =
let HelloHandler: HttpHandler =
fun (next: HttpFunc) (ctx: HttpContext) ->
Log.Information "Helloing the world!"
json {| message = "Hello, World!" |} next ctx
let router = route "/" >=> HelloHandler
Doing a simple request through a web browser should return a basic { "message": "Hello, World!" }
json text and show a your console should show the correlation
id of our request.
[20:34:49 INF] Application started. Press Ctrl+C to shut down.
[20:34:49 INF] Hosting environment: Production
[20:34:49 INF] Content root path: /home/user/foo/barr
[20:34:49 INF] Request starting HTTP/1.1 GET http://localhost:8000/ - -
[20:34:49 fe7b6dd7-eec4-4792-9fda-de814ef5dd14 INF] Helloing the world!
[20:34:50 INF] Request finished HTTP/1.1 GET http://localhost:8000/ - - - 200 27 application/json;+charset=utf-8 1126.8972ms
-
It has been my first production encounter with functional programming and I'm loving it! 🤓↩