Including the User Name Info in the Conversion Pattern of Log4Net on ASP.NET Core

If you have ever used %username in the conversion pattern of the log4net configuration on your ASP.NET Core application, you would noticed that you wouldn’t get the correct logged in user. Based on the documentation, there are quite a number of pattern converters that are not yet supported in ASP.NET Core.

Thus, in order for us to output the currently logged in user, we need to write custom code and the best option we have is to create a custom PatternLayoutConverter. The implementation of your PatternLayoutConverter depends on how you store the information of your authenticated user. This article assumes your are using ClaimsIdentity to authenticate the users of your application.

Normally, when you define your claims, your would include the Name and/or NameIdentifier claims:

private async Task Login(string username, string password)
{
    // code to validate the identity of the user
    var claims = new List<Claim>
    {
        new Claim(ClaimTypes.Name, username),
        new Claim(ClaimTypes.NameIdentifier, username),
        // other claims here...
    };
            
    var claimsIdentity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
            
    await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(claimsIdentity));
}

Then, let’s extend a PatternLayoutConverter for us to get the Name or NameIdentifier claims (depending on which claim you store in your ClaimsIdentity):

public class HttpContextUserPatternConverter : PatternLayoutConverter
{
    protected override void Convert(TextWriter writer, LoggingEvent loggingEvent)
    {
        string name = String.Empty;
        var nameClaim = HttpContextHelper.Current.User.FindFirst(ClaimTypes.NameIdentifier);
        name = (null != nameClaim && !String.IsNullOrWhiteSpace(nameClaim.Value)) ? nameClaim.Value : "Guest";
        writer.Write(name);
    }
}

Take note that our PatternLayoutConverter class is independent of the ASP.NET Core Web Application. This means that the current HTTP context is unknown to log4net. Thus, we have the HttpContextHelper class in order for us to get the instance of the current HTTP context.

public static class HttpContextHelper
{
    private static IHttpContextAccessor httpContextAccessor;

    public static void Configure(IHttpContextAccessor httpContextAccessor)
    {
        HttpContextHelper.httpContextAccessor = httpContextAccessor;
    }

    public static HttpContext Current
    {
        get
        {
            return httpContextAccessor.HttpContext;
        }
    }
}

In your Startup.cs, we need to set or configure the HttpContextAccessor since this would be responsible for knowing the context per user accessing the application:

public void ConfigureServices(IServiceCollection services)
{
    services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();

    // more code here...
}

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    // more code here...
    HttpContextHelper.Configure(app.ApplicationServices.GetRequiredService<IHttpContextAccessor>());
}

Finally, in your log4net configuration, define the pattern converter in the converter section of the layout element:

<?xml version="1.0" encoding="utf-8" ?>
<log4net debug="true">
	<appender name="RollingLogFileAppender" type="log4net.Appender.RollingFileAppender">
		<file value="Logs/Test.log" />
		<appendToFile value="true" />
		<rollingStyle value="Size" />
		<maxSizeRollBackups value="10" />
		<maximumFileSize value="10MB" />
		<staticLogFileName value="true" />
		<lockingModel type="log4net.Appender.FileAppender+MinimalLock" />
		<layout type="log4net.Layout.PatternLayout">
			<converter>
				<name value="httpuser" />
				<type value="HelloWorld.Web.Logging.HttpContextUserPatternConverter, HelloWorld.Web" />
			</converter>
			
			<!-- Format is [date/time] [log level] [thread] [UserName=?] message-->
			<conversionPattern value="[%date] [%level] [%thread] [UserName=%httpuser] %m%n" />
		</layout>
	</appender>

	<root>
		<level value="ALL" />
		<appender-ref ref="RollingLogFileAppender" />
	</root>
</log4net>

You would see that we define httpuser as the alias of our custom converter for us to refer in our conversion pattern. Run the application and you would see logging of the logged in user in action: