Add Username into Serilog

15,568

Solution 1

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>();

Solution 2

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

Solution 3

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

Solution 4

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");
    });

Solution 5

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}");

    });
Share:
15,568

Related videos on Youtube

Muflix
Author by

Muflix

Updated on March 07, 2022

Comments

  • Muflix
    Muflix about 2 years

    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 ?

  • Muflix
    Muflix almost 6 years
    It works, you are my hero ! :)) (Also the middleware has to be loaded app.UseMiddleware<LogUserNameMiddleware>(); in startup.cs)
  • Cocowalla
    Cocowalla over 5 years
    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?
  • Muflix
    Muflix over 5 years
    Cocowalla: I do not use Authentication middleware in my scenario :/
  • Cocowalla
    Cocowalla over 5 years
    @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!
  • JianYA
    JianYA over 5 years
    @Cocowalla Thank you!
  • Cocowalla
    Cocowalla over 5 years
    @JianYA answer posted, hope it helps!
  • JianYA
    JianYA over 5 years
    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.
  • Cocowalla
    Cocowalla over 5 years
    @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.
  • JianYA
    JianYA over 5 years
    I see. Thank you!
  • spankymac
    spankymac over 4 years
    What is the 'AppIdentity' method in your code above?
  • Cocowalla
    Cocowalla over 4 years
    @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
  • Josh Brown
    Josh Brown over 4 years
    app.UseMiddleware<LogUserNameMiddleware>() may be best placed as the line preceding app.UseMvc(), make sure that any authentication middleware is before it.
  • Mike
    Mike almost 4 years
    There is a number of issues with this approach.
  • Stephan
    Stephan almost 4 years
    This answer really small code snippet helped me greatly. Possibly even better then other answers
  • Himal Patel
    Himal Patel over 3 years
    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.
  • flux
    flux over 3 years
    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 github.com/serilog/serilog/wiki/Enrichment.