0

The Serilog documentation and online examples all use log properties inside a using statement like this:

using (LogContext.PushProperty("Username", _username))
{
    _logger.LogInformation("Hello!");
}

In my specific case I have a service class LoggedInUserService which will live for longer than "saying hello" (has other methods inside) and needs to prefix all log messages with the username. I'm running my setup in an embedded environment, and the username has nothing to do with HttpContext etc, as it is not doing any http requests or so... Up until now I just added this functionality by manually adding the username, but this looks a bit cumbersome after a while:

_logger.LogInformation("{username}: Hello!", _username);

I've created an example project (you can copy and run it locally) to demonstrate my project setup. The current log output is this:

[INF] : The program started.
[INF] Joe: Hello!
[INF] Joe: Hello!
[INF] Joe: The program ended.

The desired log output should be:

[INF] The program started.
[INF] Frank: Hello!
[INF] Joe: Hello!
[INF] The program ended.

And here is the demo project:

class Program
{
    static void Main(string[] args)
    {
        Log.Logger = new LoggerConfiguration()
            .Enrich.FromLogContext()
            .WriteTo.Console(outputTemplate: 
                "[{Level:u3}] {Username}: {Message}{NewLine:1}{Exception:1}")
            .CreateLogger();

        var host = Host.CreateDefaultBuilder()
            .UseSerilog()
            .ConfigureServices((_, services) =>
            {
                services.AddSingleton<LoggedInUserServiceFactory>();
            })
            .Build();

        var factory = host.Services.GetRequiredService<LoggedInUserServiceFactory>();
        
        Log.Logger.Information("The program started.");

        var loggedInUser1 = factory.Create("Frank");
        var loggedInUser2 = factory.Create("Joe");
        
        loggedInUser1.SayHello();
        loggedInUser2.SayHello();
        
        Log.Logger.Information("The program ended.");

    }
}

internal class LoggedInUserServiceFactory
{
    private readonly IServiceProvider _serviceProvider;

    public LoggedInUserServiceFactory(
        IServiceProvider serviceProvider)
    {
        _serviceProvider = serviceProvider;
    }

    public LoggedInUserService Create(string username)
    {
        return new LoggedInUserService(
            _serviceProvider.GetRequiredService<ILogger<LoggedInUserService>>(), 
            username);
    }
}

internal class LoggedInUserService
{
    private readonly ILogger<LoggedInUserService> _logger;
    private readonly string _username;

    public LoggedInUserService(
        ILogger<LoggedInUserService> logger,
        string username)
    {
        _logger = logger;
        _username = username;

        // How to push log properties on class level??
        LogContext.PushProperty("Username", _username);
    }

    public void SayHello()
    {
        _logger.LogInformation("Hello!");
    }
}
  • So what you really ask is how to log the request's user. That's not long lived, it only exists only for a specific request and is exposed by the HttpContext. For every request, a new Controller instance is created with a new HttpContext that contains the request's information, including credentials. Those obviously aren't available at startup time, they're different for every request. If you use Serilog.AspNetCore one of the duplicate answers shows how you can enrich Serilog logs with HttpContext properties. Otherwise you can create a middleware that pushes the property – Panagiotis Kanavos Feb 14 '22 at 08:47
  • The container registers `LoggedInUserServiceFactory` as a single object: services.AddSingleton(); Even though you have two separate obects `loggedInUser1` and `loggedInUser2`, they both wrap the exact same object. So, when either of these objects run `SayHello()`, it will output the last assigned value of `username`, which is this case is `Joe`. – Phillip Ngan Feb 14 '22 at 08:50

0 Answers0