diff --git a/correlationIdMiddleware/correlationIdMiddleware/CorrelationContext.cs b/correlationIdMiddleware/correlationIdMiddleware/CorrelationContext.cs index 1e57354..9c21791 100644 --- a/correlationIdMiddleware/correlationIdMiddleware/CorrelationContext.cs +++ b/correlationIdMiddleware/correlationIdMiddleware/CorrelationContext.cs @@ -1,14 +1,18 @@ -using Ardalis.GuardClauses; +namespace Dfe.Academisation.CorrelationIdMiddleware; -namespace Dfe.Academisation.CorrelationIdMiddleware; - -public record CorrelationContext() : ICorrelationContext +/// +public class CorrelationContext : ICorrelationContext { - public string? CorrelationId { get; private set; } - public void SetContext(string correlationId) + /// + public Guid CorrelationId { get; private set; } + + /// + public void SetContext(Guid correlationId) { - this.CorrelationId = Guard.Against.NullOrWhiteSpace(correlationId); + if (correlationId == Guid.Empty) + { + throw new ArgumentException("Guid cannot be empty", nameof(correlationId)); + } + this.CorrelationId = correlationId; } - - public string HeaderKey { get => "x-correlation-id"; } } \ No newline at end of file diff --git a/correlationIdMiddleware/correlationIdMiddleware/CorrelationIdMiddleware.cs b/correlationIdMiddleware/correlationIdMiddleware/CorrelationIdMiddleware.cs index be9e04f..d51ccea 100644 --- a/correlationIdMiddleware/correlationIdMiddleware/CorrelationIdMiddleware.cs +++ b/correlationIdMiddleware/correlationIdMiddleware/CorrelationIdMiddleware.cs @@ -1,13 +1,11 @@ -using Ardalis.GuardClauses; +namespace Dfe.Academisation.CorrelationIdMiddleware; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; -namespace Dfe.Academisation.CorrelationIdMiddleware; - /// /// Middleware that checks incoming requests for a correlation and causation id header. If not found then default values will be created. /// Saves these values in the correlationContext instance. Be sure to register correlation context as scoped or the equivalent in you ioc container. -/// Header used in requests is 'x-correlation-id' +/// Header used in requests is 'x-correlationId' /// public class CorrelationIdMiddleware { @@ -17,34 +15,43 @@ public class CorrelationIdMiddleware public CorrelationIdMiddleware(RequestDelegate next, ILogger logger) { _next = next; - _logger = Guard.Against.Null(logger); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); } // ReSharper disable once UnusedMember.Global // Invoked by asp.net public Task Invoke(HttpContext httpContext, ICorrelationContext correlationContext) { - string thisCorrelationId; + Guid thisCorrelationId; // correlation id. An ID that spans many requests - if (httpContext.Request.Headers.ContainsKey(correlationContext.HeaderKey) && - !string.IsNullOrWhiteSpace(httpContext.Request.Headers[correlationContext.HeaderKey])) + if (httpContext.Request.Headers.ContainsKey(Keys.HeaderKey) + && !string.IsNullOrWhiteSpace(httpContext.Request.Headers[Keys.HeaderKey])) { - thisCorrelationId = httpContext.Request.Headers[correlationContext.HeaderKey]; - _logger.LogInformation("CorrelationId detected from header: {correlationId}", thisCorrelationId); + if (!Guid.TryParse(httpContext.Request.Headers[Keys.HeaderKey], out thisCorrelationId)) + { + thisCorrelationId = Guid.NewGuid(); + this._logger.LogWarning("Detected header x-correlationId, but value cannot be parsed to a GUID. Other values are not supported. Generated a new one: {correlationId}", thisCorrelationId); + } + else + { + _logger.LogInformation("CorrelationIdMiddleware:Invoke - x-correlationId detected in request headers: {correlationId}", thisCorrelationId); + } } else { - thisCorrelationId = Guid.NewGuid().ToString(); - _logger.LogInformation("CorrelationId not detected from headers. Generated a new one: {correlationId}", - thisCorrelationId); + thisCorrelationId = Guid.NewGuid(); + _logger.LogWarning("CorrelationIdMiddleware:Invoke - x-correlationId not detected in request headers. Generated a new one: {correlationId}", thisCorrelationId); } - httpContext.Request.Headers[correlationContext.HeaderKey] = thisCorrelationId; + httpContext.Request.Headers[Keys.HeaderKey] = thisCorrelationId.ToString(); correlationContext.SetContext(thisCorrelationId); - httpContext.Response.Headers[correlationContext.HeaderKey] = thisCorrelationId; - return _next(httpContext); + httpContext.Response.Headers[Keys.HeaderKey] = thisCorrelationId.ToString(); + using (_logger.BeginScope("x-correlationId {x-correlationId}", correlationContext.CorrelationId.ToString())) + { + return _next(httpContext); + } } } \ No newline at end of file diff --git a/correlationIdMiddleware/correlationIdMiddleware/Dfe.Academisation.CorrelationIdMiddleware.csproj b/correlationIdMiddleware/correlationIdMiddleware/Dfe.Academisation.CorrelationIdMiddleware.csproj index 4a71630..15f4407 100644 --- a/correlationIdMiddleware/correlationIdMiddleware/Dfe.Academisation.CorrelationIdMiddleware.csproj +++ b/correlationIdMiddleware/correlationIdMiddleware/Dfe.Academisation.CorrelationIdMiddleware.csproj @@ -13,7 +13,7 @@ dfe;academisation;correlation; 4ac4e7ef-aaff-48a4-9e4d-44371c231191 README.md - 1.0.0 + 2.0.0 DFE-Digital @@ -25,10 +25,8 @@ - - - + diff --git a/correlationIdMiddleware/correlationIdMiddleware/ICorrelationContext.cs b/correlationIdMiddleware/correlationIdMiddleware/ICorrelationContext.cs index 31cf350..24da7b2 100644 --- a/correlationIdMiddleware/correlationIdMiddleware/ICorrelationContext.cs +++ b/correlationIdMiddleware/correlationIdMiddleware/ICorrelationContext.cs @@ -1,8 +1,19 @@ namespace Dfe.Academisation.CorrelationIdMiddleware; +/// +/// Provides access to the current correlation id. You should register this as a scoped / per web request +/// dependency in your IoC/DI container. +/// public interface ICorrelationContext { - public string? CorrelationId { get; } - public void SetContext(string correlationId); - public string HeaderKey { get; } + /// + /// Returns the current correlation id if it has been set + /// + public Guid CorrelationId { get; } + + /// + /// Used by the middleware to store the current correlation id. Do not call this method yourself. + /// + /// + public void SetContext(Guid correlationId); } \ No newline at end of file diff --git a/correlationIdMiddleware/correlationIdMiddleware/Keys.cs b/correlationIdMiddleware/correlationIdMiddleware/Keys.cs new file mode 100644 index 0000000..ddd16ad --- /dev/null +++ b/correlationIdMiddleware/correlationIdMiddleware/Keys.cs @@ -0,0 +1,13 @@ +namespace Dfe.Academisation.CorrelationIdMiddleware; +/// +/// The keys used by the correlation id middleware. +/// + +public class Keys +{ + /// + /// The header key use to detect incoming correlation ids, and to send them in responses. + /// Use this key if you are making subsequent requests so that correlation flows between services + /// + public const string HeaderKey = "x-correlationId"; +} diff --git a/correlationIdMiddleware/correlationIdMiddleware/README.md b/correlationIdMiddleware/correlationIdMiddleware/README.md index 828316c..8ba7019 100644 --- a/correlationIdMiddleware/correlationIdMiddleware/README.md +++ b/correlationIdMiddleware/correlationIdMiddleware/README.md @@ -2,6 +2,50 @@ ## What does this do ? +This package contains middleware that you can register in your AspNet application to enable detection of `x-correlationId` request headers that can then be propagated and use in logging, further requests, and responses. + ## Why does it do it ? -## How to use it \ No newline at end of file +CorrelationIds that are passed between services and recorded in all logs help to dramatically reduce the complexity involved in debugging applications, particularly where multiple services are involved. + +## How to use it + +* Reference the Nuget package. + +* In your application start-up, register the correlation context as a scoped dependency. For example +`services.AddScoped();` + +* Add the CorrelationId middleware to your AspNet middleware pipeline. You should add this as early as possible so that correlationIds can be logged. +`app.UseMiddleware();` + +* Anywhere that you need access to the current correlation id, inject `ICorrelationContext` and access the current context using the `CorrelationId` property. It will return a `string?` that you can use in subsequent requests or wherever you need it. + +--- + +## Default AspNet Logger +The middleware will add the current correlation id as scoped data before calling the next middleware in the AspNet middleware pipeline. +If you are using the default AspNet logger, to see this in your log output you need to enable scopes in the output. +One simple way of doing this is to use the console output by adding the following to your logger configuration. +Alternatively use something like Seq to view logs locally. + +```json + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Information", + "Microsoft.Hosting.Lifetime": "Information", + "Microsoft.AspNetCore": "Warning" + }, + "Console": { + "FormatterName": "simple", + "FormatterOptions": { + "IncludeScopes": "true" + } + } + }, +``` + +## Not using the default AspNet Logger ? +The middleware will push an `x-correlationId` scope property onto the default Microsoft logging implementation. + +If you are using another logger, create a middleware and register it as the next one in the pipeline after the `CorrelationIdMiddleware`. Inject the `ICorrelationContext` into your middleware and use the `ICorrelationContext.CorrelationId` property to access the correlationId value and include it in your own logs. For example Serilog.