< 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
99%
Covered lines: 128
Uncovered lines: 1
Coverable lines: 129
Total lines: 193
Line coverage: 99.2%
Branch coverage
90%
Covered branches: 9
Total branches: 10
Branch coverage: 90%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
AddApplicationRequestLogging(...)100%11100%
UseApplicationRequestLogging(...)50%2299%
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    {
 11726        IConfigurationSection section = configuration.GetSection(ApplicationRequestLoggingOptions.SectionName);
 27
 11728        services
 11729            .AddOptions<ApplicationRequestLoggingOptions>()
 11730            .Bind(section)
 11731            .Configure(options => ConfigurationListBinding.ReplaceWithConfiguredValues(
 11732                options.ExcludedPathPrefixes,
 11733                section.GetSection(nameof(ApplicationRequestLoggingOptions.ExcludedPathPrefixes))))
 11734            .Validate(
 11735                options => !string.IsNullOrWhiteSpace(options.CorrelationHeaderName),
 11736                "ProjectTemplate:RequestLogging:CorrelationHeaderName is required.")
 11737            .Validate(
 11738                options => options.ExcludedPathPrefixes.All(path =>
 11739                    !string.IsNullOrWhiteSpace(path) &&
 11740                    path.StartsWith('/')),
 11741                "ProjectTemplate:RequestLogging:ExcludedPathPrefixes values must start with '/'.")
 11742            .ValidateOnStart();
 43
 11744        return services;
 45    }
 46
 47    /// <summary>
 48    /// Configures structured request logging with correlation identifiers,
 49    /// request duration metrics, filtering, and safe diagnostic enrichment.
 50    /// </summary>
 51    /// <param name="app">The web application to configure.</param>
 52    /// <returns>The same web application instance for chaining.</returns>
 53    public static WebApplication UseApplicationRequestLogging(this WebApplication app)
 54    {
 10755        ApplicationRequestLoggingOptions requestLoggingOptions = app.Services
 10756            .GetRequiredService<IOptions<ApplicationRequestLoggingOptions>>()
 10757            .Value;
 58
 10759        if (!requestLoggingOptions.Enabled)
 60        {
 061            return app;
 62        }
 63
 10764        app.Use(async (context, next) =>
 10765        {
 10766            string correlationId = GetCorrelationId(context, requestLoggingOptions);
 10767
 10768            context.Response.OnStarting(() =>
 10769            {
 10770                if (!context.Response.Headers.ContainsKey(requestLoggingOptions.CorrelationHeaderName))
 10771                {
 10772                    context.Response.Headers[requestLoggingOptions.CorrelationHeaderName] = correlationId;
 10773                }
 10774
 10775                return Task.CompletedTask;
 10776            });
 10777
 10778            Activity? activity = Activity.Current;
 10779            string? traceId = activity?.TraceId.ToString();
 10780            string? spanId = activity?.SpanId.ToString();
 10781            string? traceParent = activity?.Id;
 10782
 10783            using (LogContext.PushProperty("CorrelationId", correlationId))
 10784            using (LogContext.PushProperty("RequestId", context.TraceIdentifier))
 10785            using (LogContext.PushProperty("TraceId", traceId))
 10786            using (LogContext.PushProperty("SpanId", spanId))
 10787            using (LogContext.PushProperty("TraceParent", traceParent))
 10788            {
 10789                await next(context);
 10790            }
 10791        });
 92
 10793        app.UseSerilogRequestLogging(options =>
 10794        {
 10795            options.MessageTemplate =
 10796                "HTTP {RequestMethod} {RequestPath} responded {StatusCode} in {Elapsed:0.0000} ms";
 10797
 10798            options.GetLevel = (httpContext, _, exception) =>
 10799            {
 107100                if (IsExcludedPath(httpContext.Request.Path, requestLoggingOptions))
 107101                {
 107102                    return LogEventLevel.Verbose;
 107103                }
 107104
 107105                if (exception is not null)
 107106                {
 107107                    return LogEventLevel.Error;
 107108                }
 107109
 107110                int statusCode = httpContext.Response.StatusCode;
 107111
 107112                return statusCode >= StatusCodes.Status500InternalServerError
 107113                    ? LogEventLevel.Error
 107114                    : statusCode >= StatusCodes.Status400BadRequest
 107115                        ? LogEventLevel.Warning
 107116                        : LogEventLevel.Information;
 107117            };
 107118
 107119            // Do not enrich request logs with request bodies, response bodies, cookies,
 107120            // authorization headers, access tokens, refresh tokens, SAML/OIDC payloads,
 107121            // password fields, form fields, or query strings unless explicitly reviewed.
 107122            // Request logging should default to operational metadata only.
 107123            options.EnrichDiagnosticContext = (diagnosticContext, httpContext) =>
 107124            {
 107125                Activity? activity = Activity.Current;
 107126
 107127                diagnosticContext.Set("CorrelationId", GetCorrelationId(httpContext, requestLoggingOptions));
 107128                diagnosticContext.Set("RequestId", httpContext.TraceIdentifier);
 107129                diagnosticContext.Set("TraceIdentifier", httpContext.TraceIdentifier);
 107130                diagnosticContext.Set("RequestHost", httpContext.Request.Host.Value);
 107131                diagnosticContext.Set("RequestScheme", httpContext.Request.Scheme);
 107132                diagnosticContext.Set("RequestPathBase", httpContext.Request.PathBase.Value);
 107133                diagnosticContext.Set("TraceId", activity?.TraceId.ToString());
 107134                diagnosticContext.Set("SpanId", activity?.SpanId.ToString());
 107135                diagnosticContext.Set("TraceParent", activity?.Id);
 107136
 107137                if (requestLoggingOptions.IncludeQueryString)
 107138                {
 107139                    diagnosticContext.Set("QueryString", httpContext.Request.QueryString.Value);
 107140                }
 107141
 107142                if (requestLoggingOptions.IncludeRemoteIpAddress)
 107143                {
 107144                    diagnosticContext.Set(
 107145                        "RemoteIpAddress",
 107146                        httpContext.Connection.RemoteIpAddress?.ToString());
 107147                }
 107148
 107149                if (requestLoggingOptions.IncludeUserName)
 107150                {
 107151                    diagnosticContext.Set(
 107152                        "UserName",
 107153                        httpContext.User?.Identity?.IsAuthenticated == true
 107154                            ? httpContext.User.Identity.Name
 107155                            : null);
 107156                }
 107157            };
 107158        });
 159
 107160        return app;
 161    }
 162
 163    private static string GetCorrelationId(
 164        HttpContext httpContext,
 165        ApplicationRequestLoggingOptions options)
 166    {
 173167        if (httpContext.Request.Headers.TryGetValue(options.CorrelationHeaderName, out StringValues headerValues))
 168        {
 6169            string? headerValue = headerValues.FirstOrDefault();
 170
 6171            if (!string.IsNullOrWhiteSpace(headerValue))
 172            {
 4173                string cleanValue = headerValue.Trim();
 174
 4175                return cleanValue.Length <= 128
 4176                    ? cleanValue
 4177                    : cleanValue[..128];
 178            }
 179        }
 180
 169181        return httpContext.TraceIdentifier;
 182    }
 183
 184    private static bool IsExcludedPath(
 185        PathString requestPath,
 186        ApplicationRequestLoggingOptions options)
 187    {
 96188        return requestPath.HasValue && options.ExcludedPathPrefixes.Any(prefix =>
 96189            requestPath.StartsWithSegments(
 96190                new PathString(prefix),
 96191                StringComparison.OrdinalIgnoreCase));
 192    }
 193}