ConfuedProblemSolver
ConfuedProblemSolver

Reputation: 673

How to push custom properties to Serilog’s LogContext without adding other context properties?

In a .Net Core 2.2 API, I am trying to use Serilog for logging to SQL Server to take advantage of its structured logging capabilities. Besides the standard fields (Id, Timestamp, Level, Message, MessageTemplate, LogEvent), I need to capture the user’s name and IP address with every log entry. I have seen this post but I’d like to do this in one place so the developers don’t have to add it manually with each log statement. I have the following snippet in my Startup.cs ctor:

    public Startup(IConfiguration configuration)
    {
        _configuration = configuration;

        Log.Logger = new LoggerConfiguration()
            .ReadFrom.Configuration(configuration)
            .Enrich.FromLogContext()
            .Enrich.With<SerilogContextEnricher>()
            .CreateLogger();
    }

The .Enrich.FromLogContext() call makes the static LogContext available for me to add my custom properties (username and IP address) in a custom middleware like so:

    public class SerilogMiddleware
    {
        private static readonly ILogger Log = Serilog.Log.ForContext<SerilogMiddleware>();
        private readonly RequestDelegate _next;

        public SerilogMiddleware(RequestDelegate next)
        {
            this._next = next ?? throw new ArgumentNullException(nameof(next));
        }

        public async Task Invoke(HttpContext httpContext)
        {
            if (httpContext == null) throw new ArgumentNullException(nameof(httpContext));

            // Get user info
            var user = httpContext.User.FindFirst(ClaimTypes.Name)?.Value ?? "anonymous";

            // Get client info
            var client = httpContext.Connection.RemoteIpAddress.ToString() ?? "unknown";

            // enrich LogContext with user info
            using (LogContext.PushProperty("User", user))
            using (LogContext.PushProperty("Client", client))
            {
                try
                {
                    await _next(httpContext);
                }

                // Never caught, because LogException() returns false.
                catch (Exception ex) when (LogException(httpContext, ex)) { }
            }
        }

        private static bool LogException(HttpContext httpContext, Exception ex)
        {
            var logForContext = Log.ForContext("StackTrace", ex.StackTrace);

            logForContext.Error(ex, ex.Message);

            return false;
        }
    }

However, the .Enrich.FromLogContext() call also adds ActionId, ActionName, RequestId, RequestPath and CorrelationId properties to the LogEvent field. I don’t want to bloat my log table with these 5 properties. My current solution is to enrich my logger with the below custom enricher that removes these properties from EventLog.

    public class SerilogContextEnricher : ILogEventEnricher
    {
        public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory)
        {
            logEvent.RemovePropertyIfPresent("ActionId");
            logEvent.RemovePropertyIfPresent("ActionName");
            logEvent.RemovePropertyIfPresent("RequestId");
            logEvent.RemovePropertyIfPresent("RequestPath");
            logEvent.RemovePropertyIfPresent("CorrelationId");
        }
    }

This all works fine but it seems silly to initially add these properties to the logger and then remove them afterwards. Does anyone know of a way to push custom properties to all log entries universally without this game of adding and removing unwanted properties?

Upvotes: 11

Views: 21864

Answers (1)

bret
bret

Reputation: 359

Serilog allows you to inject services from dependency injection (DI) into your logging pipeline. You can implement a custom ILogEventEnricher that injects the IHttpContextAccessor to resolve the current HTTP context:

class HttpContextLogEventEnricher : ILogEventEnricher
{
  private readonly IHttpContextAccessor _contextAccessor;

  HttpContextLogEventEnricher(IHttpContextAccessor contextAccessor)
  {
    _contextAccessor = contextAccessor;
  }

  public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory)
  {
    // HttpContext may be null if there is no active context
    var userName = _contextAccessor.HttpContext?.User.Identity.Name;
    
    if (!string.IsNullOrEmpty(userName))
    {
      var userNameProperty = propertyFactory.CreateProperty("UserName", userName);
      logEvent.AddPropertyIfAbsent(userNameProperty);
    }
  }
}

Register your customer enricher with dependency injection along with the HttpContextAccessor. With the Microsoft DI container:

services.AddHttpContextAccessor();
services.AddTransient<ILogEventEnricher, HttpContextLogEventEnricher>();

Finally, in your Serilog configuration use the ReadFrom.Services() method. Serilog will use all the ILogEventEnrichers it finds in the service provider:

Host.CreateDefaultBuilder().UseSerilog(
  (hostBuilderContext, serviceProvider, loggerConfiguration) =>
    {
      loggerConfiguration.ReadFrom.Services(serviceProvider)
    }

Note: The ReadFrom.Services() extension is part of the Serilog.Extensions.Hosting NuGet package (which is included in the Serilog.AspNetCore package).

Upvotes: 4

Related Questions