Quantcast
Channel: logging – .NET Liberty
Viewing all articles
Browse latest Browse all 4

ASP.NET 5 Logging with Log4net

$
0
0

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.

aspnet5-with-log4net-title

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.json
in 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.config
file which is where I’ve typically kept my log4net configuration. Instead, I created a standalone XML file called
log4net.xml
in 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");

            // ...

ConfigureLog4Net
is 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
ConfigureLog4Net
is 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.ApplicationBasePath
into the log4net global context with the key
appRoot
. Our
log4net.xml
file accesses this value using the
%property{appRoot}
pattern we talked about above.

As you can see, rather than calling

XmlConfigurator.Configure
with no arguments, we have to provide a
FileInfo
to point it at the standalone
log4net.xml
file.

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

ILogger
instances that hand off logging events to log4net. Here’s the
Log4NetProvider
implementation 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

aspnet5-with-log4net-logs

The only part left for us to implement is the

Log4NetAdapter
. This is a class that implements the ASP.NET 5 
ILogger
interface. 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
LogLevel
passed to the
IsEnabled
method.

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.Verbose
and
LogLevel.Debug
into 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.Verbose
and
LogLevel.Debug
into 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

Configure
method of
Startup.cs
:
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
    loggerFactory.MinimumLevel = LogLevel.Verbose;
    loggerFactory.AddConsole();
    loggerFactory.AddDebug();
    loggerFactory.AddLog4Net();

AddLog4Net
is 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

HomeController
to have an
ILogger<HomeController>
injected and emit a log statement at the beginning of the
Index
action:
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.log
file:
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.xml
and the appropriate hooks in the
Startup
class 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.

 


Viewing all articles
Browse latest Browse all 4

Trending Articles