< Summary

Information
Class: ProjectTemplate.Web.Extensions.RequestLoggingExtensions
Assembly: ProjectTemplate.Web
File(s): /home/runner/work/NetCoreApplicationTemplate/NetCoreApplicationTemplate/src/ProjectTemplate.Web/Extensions/RequestLoggingExtensions.cs
Line coverage
97%
Covered lines: 122
Uncovered lines: 3
Coverable lines: 125
Total lines: 188
Line coverage: 97.6%
Branch coverage
69%
Covered branches: 32
Total branches: 46
Branch coverage: 69.5%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
AddApplicationRequestLogging(...)100%22100%
UseApplicationRequestLogging(...)61.11%363697%
GetCorrelationId(...)100%66100%
IsExcludedPath(...)100%22100%

File(s)

/home/runner/work/NetCoreApplicationTemplate/NetCoreApplicationTemplate/src/ProjectTemplate.Web/Extensions/RequestLoggingExtensions.cs

#LineLine coverage
 1using System.Diagnostics;
 2using Microsoft.Extensions.Options;
 3using Microsoft.Extensions.Primitives;
 4using ProjectTemplate.Web.Options;
 5using Serilog;
 6using Serilog.Context;
 7using Serilog.Events;
 8
 9namespace ProjectTemplate.Web.Extensions;
 10
 11/// <summary>
 12/// Provides extension methods for configuring structured HTTP request logging.
 13/// </summary>
 14public static class RequestLoggingExtensions
 15{
 16    /// <summary>
 17    /// Registers structured request logging options.
 18    /// </summary>
 19    /// <param name="services">The service collection to configure.</param>
 20    /// <param name="configuration">The application configuration.</param>
 21    /// <returns>The original service collection for chaining.</returns>
 22    public static IServiceCollection AddApplicationRequestLogging(
 23        this IServiceCollection services,
 24        IConfiguration configuration)
 25    {
 15826        services
 15827            .AddOptions<ApplicationRequestLoggingOptions>()
 15828            .Bind(configuration.GetSection(ApplicationRequestLoggingOptions.SectionName))
 15829            .Validate(
 28030                options => !string.IsNullOrWhiteSpace(options.CorrelationHeaderName),
 15831                "ProjectTemplate:RequestLogging:CorrelationHeaderName is required.")
 15832            .Validate(
 28033                options => options.ExcludedPathPrefixes.All(path =>
 414034                    !string.IsNullOrWhiteSpace(path) &&
 414035                    path.StartsWith('/')),
 15836                "ProjectTemplate:RequestLogging:ExcludedPathPrefixes values must start with '/'.")
 15837            .ValidateOnStart();
 38
 15839        return services;
 40    }
 41
 42    /// <summary>
 43    /// Configures structured request logging with correlation identifiers,
 44    /// request duration metrics, filtering, and safe diagnostic enrichment.
 45    /// </summary>
 46    /// <param name="app">The web application to configure.</param>
 47    /// <returns>The same web application instance for chaining.</returns>
 48    public static WebApplication UseApplicationRequestLogging(this WebApplication app)
 49    {
 14250        ApplicationRequestLoggingOptions requestLoggingOptions = app.Services
 14251            .GetRequiredService<IOptions<ApplicationRequestLoggingOptions>>()
 14252            .Value;
 53
 14254        if (!requestLoggingOptions.Enabled)
 55        {
 056            return app;
 57        }
 58
 14259        app.Use(async (context, next) =>
 14260        {
 10261            string correlationId = GetCorrelationId(context, requestLoggingOptions);
 14262
 10263            context.Response.OnStarting(() =>
 10264            {
 10265                if (!context.Response.Headers.ContainsKey(requestLoggingOptions.CorrelationHeaderName))
 10266                {
 10267                    context.Response.Headers[requestLoggingOptions.CorrelationHeaderName] = correlationId;
 10268                }
 10269
 10270                return Task.CompletedTask;
 10271            });
 14272
 10273            Activity? activity = Activity.Current;
 10274            string? traceId = activity?.TraceId.ToString();
 10275            string? spanId = activity?.SpanId.ToString();
 10276            string? traceParent = activity?.Id;
 14277
 10278            using (LogContext.PushProperty("CorrelationId", correlationId))
 10279            using (LogContext.PushProperty("RequestId", context.TraceIdentifier))
 10280            using (LogContext.PushProperty("TraceId", traceId))
 10281            using (LogContext.PushProperty("SpanId", spanId))
 10282            using (LogContext.PushProperty("TraceParent", traceParent))
 14283            {
 10284                await next(context);
 10285            }
 24486        });
 87
 14288        app.UseSerilogRequestLogging(options =>
 14289        {
 14290            options.MessageTemplate =
 14291                "HTTP {RequestMethod} {RequestPath} responded {StatusCode} in {Elapsed:0.0000} ms";
 14292
 14293            options.GetLevel = (httpContext, _, exception) =>
 14294            {
 10295                if (IsExcludedPath(httpContext.Request.Path, requestLoggingOptions))
 14296                {
 2097                    return LogEventLevel.Verbose;
 14298                }
 14299
 82100                if (exception is not null)
 142101                {
 0102                    return LogEventLevel.Error;
 142103                }
 142104
 82105                int statusCode = httpContext.Response.StatusCode;
 142106
 82107                return statusCode >= StatusCodes.Status500InternalServerError
 82108                    ? LogEventLevel.Error
 82109                    : statusCode >= StatusCodes.Status400BadRequest
 82110                        ? LogEventLevel.Warning
 82111                        : LogEventLevel.Information;
 142112            };
 142113
 142114            // Do not enrich request logs with request bodies, response bodies, cookies,
 142115            // authorization headers, access tokens, refresh tokens, SAML/OIDC payloads,
 142116            // password fields, form fields, or query strings unless explicitly reviewed.
 142117            // Request logging should default to operational metadata only.
 142118            options.EnrichDiagnosticContext = (diagnosticContext, httpContext) =>
 142119            {
 82120                Activity? activity = Activity.Current;
 142121
 82122                diagnosticContext.Set("CorrelationId", GetCorrelationId(httpContext, requestLoggingOptions));
 82123                diagnosticContext.Set("RequestId", httpContext.TraceIdentifier);
 82124                diagnosticContext.Set("TraceIdentifier", httpContext.TraceIdentifier);
 82125                diagnosticContext.Set("RequestHost", httpContext.Request.Host.Value);
 82126                diagnosticContext.Set("RequestScheme", httpContext.Request.Scheme);
 82127                diagnosticContext.Set("RequestPathBase", httpContext.Request.PathBase.Value);
 82128                diagnosticContext.Set("TraceId", activity?.TraceId.ToString());
 82129                diagnosticContext.Set("SpanId", activity?.SpanId.ToString());
 82130                diagnosticContext.Set("TraceParent", activity?.Id);
 142131
 82132                if (requestLoggingOptions.IncludeQueryString)
 142133                {
 0134                    diagnosticContext.Set("QueryString", httpContext.Request.QueryString.Value);
 142135                }
 142136
 82137                if (requestLoggingOptions.IncludeRemoteIpAddress)
 142138                {
 82139                    diagnosticContext.Set(
 82140                        "RemoteIpAddress",
 82141                        httpContext.Connection.RemoteIpAddress?.ToString());
 142142                }
 142143
 82144                if (requestLoggingOptions.IncludeUserName)
 142145                {
 82146                    diagnosticContext.Set(
 82147                        "UserName",
 82148                        httpContext.User?.Identity?.IsAuthenticated == true
 82149                            ? httpContext.User.Identity.Name
 82150                            : null);
 142151                }
 224152            };
 284153        });
 154
 142155        return app;
 156    }
 157
 158    private static string GetCorrelationId(
 159        HttpContext httpContext,
 160        ApplicationRequestLoggingOptions options)
 161    {
 194162        if (httpContext.Request.Headers.TryGetValue(options.CorrelationHeaderName, out StringValues headerValues))
 163        {
 12164            string? headerValue = headerValues.FirstOrDefault();
 165
 12166            if (!string.IsNullOrWhiteSpace(headerValue))
 167            {
 8168                string cleanValue = headerValue.Trim();
 169
 8170                return cleanValue.Length <= 128
 8171                    ? cleanValue
 8172                    : cleanValue[..128];
 173            }
 174        }
 175
 186176        return httpContext.TraceIdentifier;
 177    }
 178
 179    private static bool IsExcludedPath(
 180        PathString requestPath,
 181        ApplicationRequestLoggingOptions options)
 182    {
 110183        return requestPath.HasValue && options.ExcludedPathPrefixes.Any(prefix =>
 1290184            requestPath.StartsWithSegments(
 1290185                new PathString(prefix),
 1290186                StringComparison.OrdinalIgnoreCase));
 187    }
 188}