In my previous post on logging in ASP.NET 5 we talked about how to make use of the new logging framework that comes with ASP.NET 5. In essence it is a clean abstraction layer that integrates well with the ASP.NET 5 dependency injection system to allow us to make use of various logging providers in our web applications – all without having to tie our code to those provider implementations directly.
Current Limitations
Since we are still in the early stages of the ASP.NET 5 release cycle, with 1.0.0 not slated for release until Q1 2016, most third party logging providers (such as log4net or NLog) have not completed their integrations as of yet. As of the writing of this post, only Serilog and elmah.io have provided support, and even that is a bit limited: Serilog doesn’t support file based sinks on the CoreCLR as of yet (although we can make use of file sinks if we target the full .NET framework).
Another significant challenge is that many of us have invested heavily in providers like log4net and built up highly tailored logging configurations that suit our needs. If we want to get a head start on porting our apps to ASP.NET 5, we either have to ignore the new logging framework for the time being or convert to something like Serilog or exclusively cloud-based logging with elmah.io.
The Goal: Integrate log4net with ASP.NET 5
In this post I’m going to share how I got a basic integration with log4net working with the ASP.NET 5 logging framework to write log entries to a simple rolling file appender.
There is still one limitation: since log4net doesn’t have CoreCLR support yet, we will have to target only the full .NET framework in our
project.jsonin order to integrate with log4net using this approach.
The good news is this unblocks us in integrating with the ASP.NET 5 logging framework so long as we are okay with targeting the full .NET framework for now. My plan is to switch to official log4net support once it is made available.
My Integration Approach
1. log4net Configuration
The first challenge we have to overcome is that ASP.NET 5 does away with the concept of a
web.configfile which is where I’ve typically kept my log4net configuration. Instead, I created a standalone XML file called
log4net.xmlin the root of my project to store my configuration:
<?xml version="1.0" encoding="utf-8" ?> <log4net> <appender name="RollingFile" type="log4net.Appender.FileAppender"> <file type="log4net.Util.PatternString" value="%property{appRoot}\app.log" /> <layout type="log4net.Layout.PatternLayout"> <conversionPattern value="%-5p %d{hh:mm:ss} %message%newline" /> </layout> </appender> <root> <level value="DEBUG" /> <appender-ref ref="RollingFile" /> </root> </log4net>
There is one new concept I had to introduce on line 4: rather than providing a relative path, I used a pattern string in order to inject the application root path at runtime. When I didn’t do this, the runtime log path resolved to a dnx package cache outside of my project root.
2. Bootstrap log4net
Where does the value of
%property{appRoot}come from? This is something I had to inject at runtime during application startup. Let’s take a look at the constructor of Startup.cs:
using dotnetliberty.AspNet.log4net; namespace AspNet5Log4Net { public class Startup { public Startup(IHostingEnvironment env, IApplicationEnvironment appEnv) { // Setup configuration sources. var builder = new ConfigurationBuilder() .SetBasePath(appEnv.ApplicationBasePath) .AddJsonFile("appsettings.json") .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true); appEnv.ConfigureLog4Net("log4net.xml"); // ...
ConfigureLog4Netis an extension method that I defined in a separate package (You can grab this package on GitHub here to use it in your project if you’d like). The code for
ConfigureLog4Netis pretty simple:
using log4net; using log4net.Config; using Microsoft.Dnx.Runtime; using Microsoft.Framework.Logging; using System.IO; namespace dotnetliberty.AspNet.log4net { public static class Log4NetAspExtensions { public static void ConfigureLog4Net(this IApplicationEnvironment appEnv, string configFileRelativePath) { GlobalContext.Properties["appRoot"] = appEnv.ApplicationBasePath; XmlConfigurator.Configure(new FileInfo(Path.Combine(appEnv.ApplicationBasePath, configFileRelativePath))); } } }
Line 13 injects the value of
appEnv.ApplicationBasePathinto the log4net global context with the key
appRoot. Our
log4net.xmlfile accesses this value using the
%property{appRoot}pattern we talked about above.
As you can see, rather than calling
XmlConfigurator.Configurewith no arguments, we have to provide a
FileInfoto point it at the standalone
log4net.xmlfile.
3. Create a log4net logger provider
Next we have to create a logger provider that will be used by the ASP.NET 5 logging framework to request
ILoggerinstances that hand off logging events to log4net. Here’s the
Log4NetProviderimplementation I made. I did a bit of double-checked-locking trickery since it is quite likely that multiple threads could request loggers at the same time.
using Microsoft.Framework.Logging; using System.Collections.Generic; namespace dotnetliberty.AspNet.log4net { public class Log4NetProvider : ILoggerProvider { private IDictionary<string, ILogger> _loggers = new Dictionary<string, ILogger>(); public ILogger CreateLogger(string name) { if (!_loggers.ContainsKey(name)) { lock (_loggers) { // Have to check again since another thread may have gotten the lock first if (!_loggers.ContainsKey(name)) { _loggers[name] = new Log4NetAdapter(name); } } } return _loggers[name]; } public void Dispose() { _loggers.Clear(); _loggers = null; } } }
4. Implement log4net ILogger adapter
The only part left for us to implement is the
Log4NetAdapter. This is a class that implements the ASP.NET 5
ILoggerinterface. There’s only two significant pieces of code here: one is to check whether the log4net equivalent log level is enabled based on the ASP.NET 5
LogLevelpassed to the
IsEnabledmethod.
The second is to format the incoming message and pass it off to the real log4net logger implementation. For the sake of getting this working, I ignored scopes, eventIds, and preserving named placeholders (object state can be a ILogValues instance). If you’re feeling up to it, please submit a pull request to fill in these gaps!
Nonetheless, this implementation gets me most of what I’m looking for and is an acceptable starting point without getting bogged down with the extras early on.
using log4net; using Microsoft.Framework.Logging; using System; namespace dotnetliberty.AspNet.log4net { public class Log4NetAdapter : ILogger { private ILog _logger; public Log4NetAdapter(string loggerName) { _logger = LogManager.GetLogger(loggerName); } public IDisposable BeginScopeImpl(object state) { return null; } public bool IsEnabled(LogLevel logLevel) { switch (logLevel) { case LogLevel.Verbose: case LogLevel.Debug: return _logger.IsDebugEnabled; case LogLevel.Information: return _logger.IsInfoEnabled; case LogLevel.Warning: return _logger.IsWarnEnabled; case LogLevel.Error: return _logger.IsErrorEnabled; case LogLevel.Critical: return _logger.IsFatalEnabled; default: throw new ArgumentException($"Unknown log level {logLevel}.", nameof(logLevel)); } } public void Log(LogLevel logLevel, int eventId, object state, Exception exception, Func<object, Exception, string> formatter) { if (!IsEnabled(logLevel)) { return; } string message = null; if (null != formatter) { message = formatter(state, exception); } else { message = LogFormatter.Formatter(state, exception); } switch (logLevel) { case LogLevel.Verbose: case LogLevel.Debug: _logger.Debug(message, exception); break; case LogLevel.Information: _logger.Info(message, exception); break; case LogLevel.Warning: _logger.Warn(message, exception); break; case LogLevel.Error: _logger.Error(message, exception); break; case LogLevel.Critical: _logger.Fatal(message, exception); break; default: _logger.Warn($"Encountered unknown log level {logLevel}, writing out as Info."); _logger.Info(message, exception); break; } } } }
Notice that we combine
LogLevel.Verboseand
LogLevel.Debuginto log4net’s
Debug. This is because log4net doesn’t make the same distinction. There’s a discussion going on right now on GitHub about possibly merging
LogLevel.Verboseand
LogLevel.Debuginto a single level here. I personally like their proposal of calling it
LogLevel.Verbug

5. Register log4net logger provider
The only step remaining for hooking this up is to register the new provider in the
Configuremethod of
Startup.cs:
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) { loggerFactory.MinimumLevel = LogLevel.Verbose; loggerFactory.AddConsole(); loggerFactory.AddDebug(); loggerFactory.AddLog4Net();
AddLog4Netis a simple extension method that I’ve also included in the dotnetliberty.AspNet.log4net package:
using log4net; using log4net.Config; using Microsoft.Dnx.Runtime; using Microsoft.Framework.Logging; using System.IO; namespace dotnetliberty.AspNet.log4net { public static class Log4NetAspExtensions { public static void ConfigureLog4Net(this IApplicationEnvironment appEnv, string configFileRelativePath) { GlobalContext.Properties["appRoot"] = appEnv.ApplicationBasePath; XmlConfigurator.Configure(new FileInfo(Path.Combine(appEnv.ApplicationBasePath, configFileRelativePath))); } public static void AddLog4Net(this ILoggerFactory loggerFactory) { loggerFactory.AddProvider(new Log4NetProvider()); } } }
6. Test
Now we can test this out. I modified my
HomeControllerto have an
ILogger<HomeController>injected and emit a log statement at the beginning of the
Indexaction:
using System; using Microsoft.AspNet.Mvc; using Microsoft.Framework.Logging; namespace AspNet5Log4Net.Controllers { public class HomeController : Controller { private ILogger<HomeController> _logger; public HomeController(ILogger<HomeController> logger) { _logger = logger; } public IActionResult Index() { _logger.LogInformation("Index action requested at {requestTime}", DateTime.Now); return View(); } // ...
After running the app, I could see the log lines in my
app.logfile:
INFO 12:30:48 User profile is available. Using 'C:\Users\armen\AppData\Local\ASP.NET\DataProtection-Keys' as key repository and Windows DPAPI to encrypt keys at rest. DEBUG 12:30:49 Request successfully matched the route with name 'default' and template '{controller=Home}/{action=Index}/{id?}'. DEBUG 12:30:49 Executing action AspNet5Log4Net.Controllers.HomeController.Index INFO 12:30:50 Index action requested at 11/01/2015 12:30:50 DEBUG 12:30:50 The view 'Index' was found.
This is good
In the end I was pretty happy to get a basic log4net integration working with ASP.NET 5. Obviously this is not a fully polished solution but I think it is a pretty solid starting point for beginning to work with log4net in my ASP.NET 5 apps.
I’ve create a separate package that contains the extension methods, provider, and adapter that I’ve shared on GitHub here so you can use and improve upon it if you like. Using this package all you have to do is add your own
log4net.xmland the appropriate hooks in the
Startupclass and you’re off to the races. (see the README for exact steps).
A call to action
One last thing: I’m trying to build up my (budding) Twitter following – if you found this post at least a little bit useful I would greatly appreciate a Twitter share (click the Twitter link on the left hand side). Maybe even follow me (@ArmenShimoon). Much appreciated.