< Summary

Information
Class: Gateway.Logging
Assembly: Gateway
File(s): /home/runner/work/dotnet-microservice/dotnet-microservice/Gateway/GlobalConfigurations/Logging.cs
Tag: 34_11887803474
Line coverage
0%
Covered lines: 0
Uncovered lines: 121
Coverable lines: 121
Total lines: 185
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 12
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.cctor()100%210%
UseRequestLogging(...)100%210%
AddLogger(...)100%210%
SinkLog(...)0%620%
MapProtocol(...)0%2040%
EnrichLog(...)100%210%
GetPath(...)0%2040%
LogCompletion(...)0%620%
.ctor()100%210%

File(s)

/home/runner/work/dotnet-microservice/dotnet-microservice/Gateway/GlobalConfigurations/Logging.cs

#LineLine coverage
 1using System.Diagnostics;
 2using Microsoft.AspNetCore.Http.Features;
 3using Microsoft.Extensions.Options;
 4using OpenTelemetry.Exporter;
 5using Serilog;
 6using Serilog.Enrichers.Sensitive;
 7using Serilog.Enrichers.Span;
 8using Serilog.Events;
 9using Serilog.Exceptions;
 10using Serilog.Sinks.OpenTelemetry;
 11using static Gateway.OpenTelemetryIntegration;
 12using MicrosoftLogger = Microsoft.Extensions.Logging.ILogger;
 13
 14namespace Gateway;
 15
 16public static class Logging
 17{
 018    private static readonly Action<
 019        MicrosoftLogger,
 020        string,
 021        string,
 022        int?,
 023        TimeSpan,
 024        Exception?
 025    > _logCompletion = LoggerMessage.Define<string, string, int?, TimeSpan>(
 026        LogLevel.Information,
 027        new EventId(1, nameof(Logging)),
 028        "HTTP {RequestMethod} {RequestPath} responded {StatusCode} in {Elapsed} ms"
 029    );
 30
 31    public static WebApplication UseRequestLogging(this WebApplication app)
 32    {
 033        app.Use(
 034            async (context, next) =>
 035            {
 036                var stopWatch = Stopwatch.StartNew();
 037                try
 038                {
 039                    await next.Invoke();
 040                    stopWatch.Stop();
 041                    LogCompletion(context, stopWatch.Elapsed);
 042                }
 043                catch (Exception e)
 044                {
 045                    if (stopWatch.IsRunning)
 046                        stopWatch.Stop();
 047                    LogCompletion(context, stopWatch.Elapsed, e);
 048                    throw;
 049                }
 050            }
 051        );
 052        return app;
 53    }
 54
 55    public static WebApplicationBuilder AddLogger(this WebApplicationBuilder builder)
 56    {
 057        builder.Logging.ClearProviders();
 058        builder.Host.UseSerilog(
 059            (_, sp, configuration) =>
 060            {
 061                EnrichLog(configuration, sp);
 062                SinkLog(configuration, sp);
 063            }
 064        );
 65
 066        return builder;
 67    }
 68
 69    private static void SinkLog(
 70        LoggerConfiguration logConfiguration,
 71        IServiceProvider serviceProvider
 72    )
 73    {
 074        var serviceMetaData = serviceProvider.GetRequiredService<IOptions<ServiceMetadata>>();
 075        var openTelemetryOptions = serviceProvider.GetRequiredService<
 076            IOptions<OpenTelemetryOptions>
 077        >();
 78
 79        const string LogTemplate =
 80            "[{Timestamp:HH:mm:ss}][{Level:u3}][{SourceContext:1}] {Message:lj}{NewLine}{Exception}";
 081        logConfiguration.WriteTo.Console(outputTemplate: LogTemplate);
 082        if (openTelemetryOptions.Value.Enabled)
 83        {
 084            logConfiguration.WriteTo.OpenTelemetry(cfg =>
 085            {
 086                cfg.Endpoint = $"{openTelemetryOptions.Value.Endpoint}/v1/logs";
 087                cfg.Headers = openTelemetryOptions.Value.ParsedHeaders;
 088                cfg.Protocol = MapProtocol(openTelemetryOptions.Value.Protocol);
 089                cfg.IncludedData =
 090                    IncludedData.MessageTemplateTextAttribute
 091                    | IncludedData.TraceIdField
 092                    | IncludedData.SpanIdField;
 093                cfg.ResourceAttributes = new Dictionary<string, object>
 094                {
 095                    // TODO: Temporary solution, remove _logs
 096                    // see https://github.com/dotnet/aspire/issues/1072
 097                    { "service.name", serviceMetaData.Value.ServiceName + "_logs" },
 098                    { "index", 10 },
 099                    { "flag", true },
 0100                    { "value", 3.14 }
 0101                };
 0102            });
 103        }
 0104    }
 105
 106    private static OtlpProtocol MapProtocol(OtlpExportProtocol protocol)
 107    {
 0108        return protocol switch
 0109        {
 0110            OtlpExportProtocol.Grpc => OtlpProtocol.Grpc,
 0111            OtlpExportProtocol.HttpProtobuf => OtlpProtocol.HttpProtobuf,
 0112            _ => OtlpProtocol.Grpc,
 0113        };
 114    }
 115
 116    private static void EnrichLog(
 117        LoggerConfiguration configuration,
 118        IServiceProvider serviceProvider
 119    )
 120    {
 0121        var serviceMetaData = serviceProvider.GetRequiredService<IOptions<ServiceMetadata>>();
 122
 0123        configuration
 0124            .MinimumLevel.Override("Microsoft.AspNetCore", LogEventLevel.Warning)
 0125            .Enrich.WithProperty(
 0126                nameof(serviceMetaData.Value.ServiceName),
 0127                serviceMetaData.Value.ServiceName
 0128            )
 0129            .Enrich.WithExceptionDetails()
 0130            .Enrich.FromLogContext()
 0131            .Enrich.WithThreadId()
 0132            .Enrich.WithThreadName()
 0133            .Enrich.WithProcessName()
 0134            .Enrich.WithProcessId()
 0135            .Enrich.WithEnvironmentName()
 0136            .Enrich.WithMachineName()
 0137            .Enrich.FromGlobalLogContext()
 0138            .Enrich.WithClientIp()
 0139            .Enrich.WithSensitiveDataMasking(
 0140                (options) =>
 0141                {
 0142                    options.MaskingOperators =
 0143                    [
 0144                        new EmailAddressMaskingOperator(),
 0145                        new IbanMaskingOperator(),
 0146                        new CreditCardMaskingOperator()
 0147                    ];
 0148                }
 0149            )
 0150            .Enrich.WithSpan(
 0151                new SpanOptions
 0152                {
 0153                    IncludeBaggage = true,
 0154                    IncludeOperationName = true,
 0155                    IncludeTags = true
 0156                }
 0157            );
 0158    }
 159
 160    private static string GetPath(HttpContext httpContext)
 161    {
 0162        var requestPath = httpContext.Features.Get<IHttpRequestFeature>()?.Path;
 0163        if (string.IsNullOrEmpty(requestPath))
 164        {
 0165            requestPath = httpContext.Request.Path.ToString();
 166        }
 167
 0168        return requestPath!;
 169    }
 170
 171    private static void LogCompletion(
 172        HttpContext httpContext,
 173        TimeSpan elapsed,
 174        Exception? exception = null
 175    )
 176    {
 0177        var logger = httpContext.RequestServices.GetRequiredService<ILogger<ApiLogging>>();
 0178        var method = httpContext.Request.Method;
 0179        var path = GetPath(httpContext);
 0180        var statusCode = httpContext.Response?.StatusCode;
 0181        _logCompletion(logger, method, path, statusCode, elapsed, exception);
 0182    }
 183
 0184    private record ApiLogging();
 185}