11

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?

3
  • I have this same problem. Have you found any decent solution? Commented Oct 26, 2020 at 19:21
  • 1
    I know this is old but thought I'd give an idea. Why don't you use middleware registered higher up and remove the properties in the middleware, so you only have to call it once? Commented May 7, 2021 at 7:25
  • 2
    Look into Serilog "outputTemplate", you can set what properties you want to appear in your final message. Commented Nov 10, 2021 at 9:52

1 Answer 1

5

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).

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.