Passing correlationID's across requests in .net core 5.0

Viewed 4006

I am wanting to create a correlation-id to help analyze logs, but I am wanting to generate a single correlation-id per user "session". I.e. one single correlation id from the start to the end of the application (regardless of the operations performed on the web mvc). I was reading up on how to do this using a middleware in .net. I attempted to implement this in my project, however when I start the application and perform certain operations (homescreen -> 2nd page view -> 3rd page view -> final page view) it will create a new correlation-id for each view. Is there a way to generate one single correlation-id that will be for all the operations performed (home view, 2nd page view, 3rd page view, and final page view)?

Startup.cs:

        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {

            app.UseMiddleware<CorrelationIdMiddleware>();
            
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            else
            {
                app.UseExceptionHandler("/Home/Error");
                // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
                app.UseHsts();
            }

            app.UseHttpsRedirection();
            app.UseStaticFiles();

            app.UseRouting();

            app.UseCors(x => x
               .AllowAnyMethod()
               .AllowAnyHeader()
               .SetIsOriginAllowed(origin => true) // allow any origin
               .AllowCredentials()); // allow credentials

            app.UseAuthorization();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllerRoute(
                    name: "default",
                    pattern: "{controller=Home}/{action=Index}/{id?}");
            });
        }

CorrelationIdContext.cs:

    public class CorrelationIdContext
    {
        private static readonly AsyncLocal<string> _correlationId = new AsyncLocal<string>();

        public static void SetCorrelationId(string correlationId)
        {
            if (string.IsNullOrWhiteSpace(correlationId))
            {
                throw new ArgumentException("Correlation Id cannot be null or empty", nameof(correlationId));
            }

            if (!string.IsNullOrWhiteSpace(_correlationId.Value))
            {
                throw new InvalidOperationException("Correlation Id is already set for the context");
            }

            _correlationId.Value = correlationId;
        }

        public static string GetCorrelationId()
        {
            return _correlationId.Value;
        }
    }

CorrelationIdMiddleware.cs:

    public class CorrelationIdMiddleware
    {
        private readonly RequestDelegate _next;

    public CorrelationIdMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public async Task Invoke(HttpContext context)
    {
        context.Request.Headers.TryGetValue("correlation-id", out var correlationIds);

        var correlationId = correlationIds.FirstOrDefault() ?? Guid.NewGuid().ToString();

        CorrelationIdContext.SetCorrelationId(correlationId);

        // Serilog
        using (LogContext.PushProperty("correlation-id", correlationId))
        {
            await _next.Invoke(context);
        }
    }
    }

and in my controllers I just have a simple logger i.e._logger.Log(LogLevel.Information, "First page...");

I noticed when debugging in the CorrelationIdMiddleware.cs file, when it hits the line: var correlationId = correlationIds.FirstOrDefault() ?? Guid.NewGuid().ToString(); it will create a new correlationID even though one already exists. Is there a way to generate one single correlationId for an entire user session (start to end of application)?

context.Request.Headers.TryGetValue("correlation-id", out var correlationIds); value after the first correlationId is created:

enter image description here

enter image description here

1 Answers

CorrelationId as presented here and by many middleware implementations is a logging concept traditionally used to track or correlate multiple actions that spawned off of a single user action, which would normally be represented as a single HTTP Request.

What you haven't adequately described is the lifetime of your expected correlationId. You have specified that the server needs to create it when it does not exist, but in what scenario does it stop existing?

When you want to correlate across multiple requests from the client, then you need to consider one of the following patterns:

  1. Client Side manages the lifetime of the token and passes it through as a HTTP Header or otherwise part of the payload that is sent to the server. Server side simply includes this token in the relevant logs.

    • This passive server implementation works great for applications that have a back-end distributed over multiple processes, either in the form of web garden, web farm or scaled out cloud infrastructure.
  2. Authentication pattern - Client makes a call to obtain the Id or a token that can be used as a unique identifier for the current user session. As with option 1, the client then makes sure that the token is included as a header with all requests to the server.

    • This is identical to token based authenticate patterns, in terms of how the client and the server need to interact
    • You are already using an Authorization middleware, there is a high chance that you could simply use the token used in that process as the correlation Id. If you do not want to store the authentication token, then you could modify your authentication process (and response) to simply create the new token and pass it back either in the payload or as a Http Header, then modify the client to receive this header and pass it back with all requests, at the same time that the authentication token is passed back.
    • This still works if you want to create a Log Out process or Get New Id, I'm assuming here that the token will remain for the duration of all user interactions for this session.
  3. Server side session context - When processing requests on the server, use session storage to save and retrieve the current correlationId, however I would strongly advise you refer to this as the SessionId or SessionToken.

    • Refer to Session and state management in ASP.NET Core for guidance on how to setup Session State in the first place.
    • Session State and the management of it can get complicated if your web deployment is scaled out, it will work well enough in single instance deployments, but to be production ready you need to ensure that your session is not broken when the web app is deployed over multiple servers.
    • Depending on your chosen implementation, Load balancing may affect your management of the correlation, make sure you set appropriate Session based affinity settings in your network configuration.
    • Session storage is technically designed for storing this sort of metadata, but you may find it simpler to implement either of the first two options, especially for stateless APIs. Depending on your current environment, enabling Session State may introduce additional complexities that might not be worth the effort if you are using Session Storage for a single keyed value.
Related