39

I have this Serilog configuration in program.cs

public class Program
    {
        public static IConfiguration Configuration { get; } = new ConfigurationBuilder()
            .SetBasePath(Directory.GetCurrentDirectory())
            .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
            .AddJsonFile($"appsettings.{Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Production"}.json", optional: true)
            .Build();

        public static void Main(string[] args)
        {
            Log.Logger = new LoggerConfiguration()
                .MinimumLevel.Override("Microsoft", LogEventLevel.Warning)
                .MinimumLevel.Override("System", LogEventLevel.Warning)
                .WriteTo.MSSqlServer(Configuration.GetConnectionString("DefaultConnection"), "dbo.Log")
                .Enrich.WithThreadId()
                .Enrich.WithProperty("Version", "1.0.0")
                .CreateLogger();
            try
            {
                BuildWebHost(args).Run();
            }
            catch (Exception ex)
            {
                Log.Fatal(ex, "Host terminated unexpectedly");
            }
            finally
            {
                Log.CloseAndFlush();
            }

        }

        public static IWebHost BuildWebHost(string[] args) =>
            WebHost.CreateDefaultBuilder(args)
                .UseStartup<Startup>()
                .UseSerilog()
                .Build();
    }

Now i want to add HttpContext.Current.User.Identity.Name into all log messages.

I tried to create new Enrich class following documentation https://github.com/serilog/serilog/wiki/Configuration-Basics#enrichers

class UsernameEnricher : ILogEventEnricher
    {
        public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory, HttpContext httpContext)
        {
            logEvent.AddPropertyIfAbsent(propertyFactory.CreateProperty(
                    "Username", httpContext.User.Identity.Name));
        }
    }

But there is conflict with ILogEventEnricher which does not know HttpContext.

I also tried to install Nuget package Serilog.Web.Classic which contains Username Enricher, but there is conflict between target framework .Net Framework and .Net Core therefore i cannot use this plugin.

Any idea ?

Douglas Gaskell
  • 9,017
  • 9
  • 71
  • 128
Muflix
  • 6,192
  • 17
  • 77
  • 153

5 Answers5

51

You can create a middleware to put required property to LogContext.

public class LogUserNameMiddleware
{
    private readonly RequestDelegate next;

    public LogUserNameMiddleware(RequestDelegate next)
    {
        this.next = next;
    }

    public Task Invoke(HttpContext context)
    {
        LogContext.PushProperty("UserName", context.User.Identity.Name);

        return next(context);
    }
}

Also you need to add the following to your logger configuration:

.Enrich.FromLogContext()

In Startup add the middleware LogUserNameMiddleware, and also note that the middleware should be added after UserAuthentication, in order to have context.User.Identity initialized

e.g.

    app.UseAuthentication();     

    app.UseMiddleware<LogUserNameMiddleware>();
Alex Riabov
  • 8,655
  • 5
  • 47
  • 48
  • 3
    It works, you are my hero ! :)) (Also the middleware has to be loaded `app.UseMiddleware();` in `startup.cs`) – Muflix Jul 10 '18 at 09:36
  • 2
    I tried this, but it doesn't seem to work - the middleware is always called *before* the authentication handler, so it doesn't have a user. I tried assing it after the call to `app.UseAuthentication()`, but with the same result. Any ideas? – Cocowalla Sep 10 '18 at 14:18
  • Cocowalla: I do not use Authentication middleware in my scenario :/ – Muflix Oct 18 '18 at 11:39
  • @JianYA I did, by using an action filter instead of middleware. I'm on mobile now, it I'll post an answer here later if I remember! – Cocowalla Dec 20 '18 at 11:09
  • @Cocowalla Thank you! – JianYA Dec 20 '18 at 11:11
  • 1
    @JianYA answer posted, hope it helps! – Cocowalla Dec 20 '18 at 19:39
  • 2
    `app.UseMiddleware()` may be best placed as the line preceding `app.UseMvc()`, make sure that any authentication middleware is before it. – Josh Brown Jan 03 '20 at 18:09
  • 2
    There is a number of issues with this approach. – Mike Jun 11 '20 at 19:48
27

If you are using Serilog.AspNetCore it's very easy to add authentication/user properties.

    app.UseSerilogRequestLogging(options =>
    {
         options.EnrichDiagnosticContext = PushSeriLogProperties;
    });



    public void PushSeriLogProperties(IDiagnosticContext diagnosticContext, HttpContext httpContext)
    {
            diagnosticContext.Set("SomePropertyName", httpContext.User...);
    }
flux
  • 1,518
  • 1
  • 17
  • 31
  • This answer really small code snippet helped me greatly. Possibly even better then other answers – Stephan Aug 13 '20 at 13:07
  • 6
    This would add the "SomePropertyName" only to the Request Log Entry. How can I add this property to all the log entries that i write to serilog ? PushSeriLogProperties method isn't getting called for other logs and doesn't carry this property. – Himal Patel Sep 16 '20 at 14:25
  • The question was about adding a user property to Serilog. User properties are associated with a request. If you want to add other properties independent of the httpcontext look at https://github.com/serilog/serilog/wiki/Enrichment. – flux Sep 17 '20 at 15:53
15

There is a number of issues with the approach suggested by @Alex Riabov.

  1. One needs to Dispose the pushed property
  2. The Invoke method in a middleware is asynchronous, so you can't just return next(), you need await next()
  3. The request information is logged by UseSerilogRequestLogging() middleware. If the property is popped before it is reached, the property becomes empty.

To fix them, I could suggest the following modifications.

In the middleware:

public async Task Invoke(HttpContext context)
{
    using (LogContext.PushProperty("UserName", context.User.Identity.Name ?? "anonymous"))
    {
        await next(context);
    }
}

In Startup.cs:

appl.UseRouting()
    .UseAuthentication()
    .UseAuthorization()
    .UseMiddleware<SerilogUserNameMiddleware>()
    .UseSerilogRequestLogging()
    .UseEndpoints(endpoints =>
    {
        endpoints.MapControllers();
        endpoints.MapRazorPages();
        endpoints.MapHealthChecks("/health");
    });
Mike
  • 843
  • 7
  • 13
  • 1
    @MassimilianoKraus while that is true, you have to be extremely careful when disposing objects in that context: if you return the task without awaiting, and it relies on a disposable resource that you (the caller) created and needs to dispose, the disposal will invariably run before the resource is actually used in some cases leading into exceptions. – julealgon Jan 02 '23 at 18:23
11

An alternative to using middleware is to use an action filter.

using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Filters;
using Serilog.Context;

namespace Acme.Widgets.Infrastructure
{
    public class LogEnrichmentFilter : IActionFilter
    {
        private readonly IHttpContextAccessor httpContextAccessor;

        public LogEnrichmentFilter(IHttpContextAccessor httpContextAccessor)
        {
            this.httpContextAccessor = httpContextAccessor;
        }

        public void OnActionExecuting(ActionExecutingContext context)
        {
            var httpUser = this.httpContextAccessor.HttpContext.User;

            if (httpUser.Identity.IsAuthenticated)
            {
                var appUser = new AppIdentity(httpUser);
                LogContext.PushProperty("Username", appUser.Username);
            }
            else
            {
                LogContext.PushProperty("Username", "-");
            }
        }

        public void OnActionExecuted(ActionExecutedContext context)
        {
            // Do nothing
        }
    }
}

In your Startup.ConfigureServices you will need to:

  1. Ensure IHttpContextAccessor is added to the IoC container
  2. Add the LogEnrichmentFilter to the IoC container, scoped to the request
  3. Register LogEnrichmentFilter as a global action filter

Startup.cs:

services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.AddScoped<LogEnrichmentFilter>();

services.AddMvc(o =>
{
    o.Filters.Add<LogEnrichmentFilter>();
});

You should then have the current username in the log context for code that runs in the MVC action invocation pipeline. I imagine the username would be attached to a few more log entries if you used a resource filter instead of an action filter, as they run slightly earlier in the pipeline (I've only just found out about these!)

Cocowalla
  • 13,822
  • 6
  • 66
  • 112
  • Hello! Thank you for your answer. How does this work when the user is logging in? As of now it doesn't seem like anyone can detect when the user first logs in, only the subsequent requests after. – JianYA Dec 21 '18 at 09:25
  • @JianYA It will only add the username to the log context *after* authorisation. If I wanted to log something during the actual signin process, I'd handle that separately, in the controller/handler/service that was responsible. – Cocowalla Dec 21 '18 at 10:33
  • I see. Thank you! – JianYA Dec 21 '18 at 10:41
  • 1
    What is the 'AppIdentity' method in your code above? – spankymac Oct 21 '19 at 06:15
  • 1
    @spankymac I didn't include it here, but it's not important to what I'm demonstrating - `AppIdentity` simply extends `ClaimsIdentity` to provide some convenience properties for accessing claim values – Cocowalla Oct 21 '19 at 19:27
  • LogContext.PushProperty() is a static method. What happens if two users make a request at the same time? Isn't there a big concurrency problem? – Massimiliano Kraus Nov 21 '22 at 01:30
  • @MassimilianoKraus LogContext is thread-safe via AsyncLocal/ThreadStatic – Cocowalla Nov 21 '22 at 14:01
9

Simply you can achieve it in two steps

1- Create an Enricher that can access services.

using Microsoft.AspNetCore.Http;
using Serilog.Core;
using Serilog.Events;
using System.Security.Claims;

namespace CoolProject.Logging.Enricher;
public class UserEnricher : ILogEventEnricher
{
private readonly IHttpContextAccessor _httpContextAccessor;

public UserEnricher() : this(new HttpContextAccessor())
{
}

//Dependency injection can be used to retrieve any service required to get a user or any data.
//Here, I easily get data from HTTPContext
public UserEnricher(IHttpContextAccessor httpContextAccessor)
{
    _httpContextAccessor = httpContextAccessor;
}

public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory)
{
    logEvent.AddPropertyIfAbsent(propertyFactory.CreateProperty(
            "UserId", _httpContextAccessor.HttpContext?.User?.FindFirstValue(ClaimTypes.NameIdentifier) ?? "anonymous"));
}
}

2-Use With to include your UserEnricher.

loggerConfiguration.Enrich.FromLogContext()
            .MinimumLevel.Is(level)
            .Enrich.With<UserEnricher>()

It only requires two steps to add the user enricher, but I will also add my driver code. Don't forget to inject IHttpContextAccessor!

 public static IHostBuilder UseLogging(this IHostBuilder webHostBuilder, string applicationName = null)
    => webHostBuilder.UseSerilog((context ,loggerConfiguration) =>
    {
        var logOptions = context.Configuration.GetSection("logging");
        var serilogOptions = logOptions.GetSection("serilog").Get<SerilogOptions>();
        if (!Enum.TryParse<LogEventLevel>(serilogOptions.Level, true, out var level))
        {
            level = LogEventLevel.Error;
        }

        loggerConfiguration.Enrich.FromLogContext()
            .MinimumLevel.Is(level)
            .Enrich.With<UserEnricher>()
            .Enrich.WithProperty("Environment", context.HostingEnvironment.EnvironmentName)
            .Enrich.WithProperty("ApplicationName", applicationName);
        loggerConfiguration.WriteTo.Console(outputTemplate: "{Timestamp:HH:mm:ss} [{Level}]  {Environment} {ApplicationName} {UserId} {Message:lj}{NewLine}{Exception}");

    });
Shervin Ivari
  • 1,759
  • 5
  • 20
  • 28