< 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    {
 17026        services
 17027            .AddOptions<ApplicationRequestLoggingOptions>()
 17028            .Bind(configuration.GetSection(ApplicationRequestLoggingOptions.SectionName))
 17029            .Validate(
 30030                options => !string.IsNullOrWhiteSpace(options.CorrelationHeaderName),
 17031                "ProjectTemplate:RequestLogging:CorrelationHeaderName is required.")
 17032            .Validate(
 30033                options => options.ExcludedPathPrefixes.All(path =>
 444034                    !string.IsNullOrWhiteSpace(path) &&
 444035                    path.StartsWith('/')),
 17036                "ProjectTemplate:RequestLogging:ExcludedPathPrefixes values must start with '/'.")
 17037            .ValidateOnStart();
 38
 17039        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    {
 15450        ApplicationRequestLoggingOptions requestLoggingOptions = app.Services
 15451            .GetRequiredService<IOptions<ApplicationRequestLoggingOptions>>()
 15452            .Value;
 53
 15454        if (!requestLoggingOptions.Enabled)
 55        {
 056            return app;
 57        }
 58
 15459        app.Use(async (context, next) =>
 15460        {
 11061            string correlationId = GetCorrelationId(context, requestLoggingOptions);
 15462
 11063            context.Response.OnStarting(() =>
 11064            {
 11065                if (!context.Response.Headers.ContainsKey(requestLoggingOptions.CorrelationHeaderName))
 11066                {
 11067                    context.Response.Headers[requestLoggingOptions.CorrelationHeaderName] = correlationId;
 11068                }
 11069
 11070                return Task.CompletedTask;
 11071            });
 15472
 11073            Activity? activity = Activity.Current;
 11074            string? traceId = activity?.TraceId.ToString();
 11075            string? spanId = activity?.SpanId.ToString();
 11076            string? traceParent = activity?.Id;
 15477
 11078            using (LogContext.PushProperty("CorrelationId", correlationId))
 11079            using (LogContext.PushProperty("RequestId", context.TraceIdentifier))
 11080            using (LogContext.PushProperty("TraceId", traceId))
 11081            using (LogContext.PushProperty("SpanId", spanId))
 11082            using (LogContext.PushProperty("TraceParent", traceParent))
 15483            {
 11084                await next(context);
 11085            }
 26486        });
 87
 15488        app.UseSerilogRequestLogging(options =>
 15489        {
 15490            options.MessageTemplate =
 15491                "HTTP {RequestMethod} {RequestPath} responded {StatusCode} in {Elapsed:0.0000} ms";
 15492
 15493            options.GetLevel = (httpContext, _, exception) =>
 15494            {
 11095                if (IsExcludedPath(httpContext.Request.Path, requestLoggingOptions))
 15496                {
 2097                    return LogEventLevel.Verbose;
 15498                }
 15499
 90100                if (exception is not null)
 154101                {
 0102                    return LogEventLevel.Error;
 154103                }
 154104
 90105                int statusCode = httpContext.Response.StatusCode;
 154106
 90107                return statusCode >= StatusCodes.Status500InternalServerError
 90108                    ? LogEventLevel.Error
 90109                    : statusCode >= StatusCodes.Status400BadRequest
 90110                        ? LogEventLevel.Warning
 90111                        : LogEventLevel.Information;
 154112            };
 154113
 154114            // Do not enrich request logs with request bodies, response bodies, cookies,
 154115            // authorization headers, access tokens, refresh tokens, SAML/OIDC payloads,
 154116            // password fields, form fields, or query strings unless explicitly reviewed.
 154117            // Request logging should default to operational metadata only.
 154118            options.EnrichDiagnosticContext = (diagnosticContext, httpContext) =>
 154119            {
 90120                Activity? activity = Activity.Current;
 154121
 90122                diagnosticContext.Set("CorrelationId", GetCorrelationId(httpContext, requestLoggingOptions));
 90123                diagnosticContext.Set("RequestId", httpContext.TraceIdentifier);
 90124                diagnosticContext.Set("TraceIdentifier", httpContext.TraceIdentifier);
 90125                diagnosticContext.Set("RequestHost", httpContext.Request.Host.Value);
 90126                diagnosticContext.Set("RequestScheme", httpContext.Request.Scheme);
 90127                diagnosticContext.Set("RequestPathBase", httpContext.Request.PathBase.Value);
 90128                diagnosticContext.Set("TraceId", activity?.TraceId.ToString());
 90129                diagnosticContext.Set("SpanId", activity?.SpanId.ToString());
 90130                diagnosticContext.Set("TraceParent", activity?.Id);
 154131
 90132                if (requestLoggingOptions.IncludeQueryString)
 154133                {
 0134                    diagnosticContext.Set("QueryString", httpContext.Request.QueryString.Value);
 154135                }
 154136
 90137                if (requestLoggingOptions.IncludeRemoteIpAddress)
 154138                {
 90139                    diagnosticContext.Set(
 90140                        "RemoteIpAddress",
 90141                        httpContext.Connection.RemoteIpAddress?.ToString());
 154142                }
 154143
 90144                if (requestLoggingOptions.IncludeUserName)
 154145                {
 90146                    diagnosticContext.Set(
 90147                        "UserName",
 90148                        httpContext.User?.Identity?.IsAuthenticated == true
 90149                            ? httpContext.User.Identity.Name
 90150                            : null);
 154151                }
 244152            };
 308153        });
 154
 154155        return app;
 156    }
 157
 158    private static string GetCorrelationId(
 159        HttpContext httpContext,
 160        ApplicationRequestLoggingOptions options)
 161    {
 210162        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
 202176        return httpContext.TraceIdentifier;
 177    }
 178
 179    private static bool IsExcludedPath(
 180        PathString requestPath,
 181        ApplicationRequestLoggingOptions options)
 182    {
 118183        return requestPath.HasValue && options.ExcludedPathPrefixes.Any(prefix =>
 1410184            requestPath.StartsWithSegments(
 1410185                new PathString(prefix),
 1410186                StringComparison.OrdinalIgnoreCase));
 187    }
 188}