Logging information in .NET, or really in any production application, is invaluable. In many cases, developers don’t have direct access to the production environment to debug issues. Good quality logs are the difference between solving problems like Sherlock Holmes and stumbling upon solutions like Inspector Jacques Clouseau. As you can imagine, we’re pretty big on logging here at Stackify, and we’ve written quite a few other blog posts on .NET logging frameworks. I’d encourage you to try out the search and read a few of our previous articles.

Top .NET logging frameworks comparison

In this article, we’ll take a look at three of the most popular logging frameworks in the .NET space: log4net, NLog, and Serilog. If you’ve read any of my comparison articles in the past, you’ll know that I typically end up with some wishy-washy conclusion like “It all depends on your situation”, or some variation of “different spices for different mices”. But not this time! This time I promise to declare one winner, one logging framework to rule them all and in the CLR bind them.

log4net overview

Way back at the beginning of time, or at least pretty close to it, there was only one logging framework for .NET: log4net. It started out in 2001 as a port of the Java framework log4j. It was hosted on Sourceforge for those of you old enough to remember that platform. Over the years, development continued under the Apache Logging Services project.

log4net has been used in tens of thousands of applications over the last 17 years; certainly, it is the grandparent of all modern .NET logging frameworks. Concepts such as log levels, loggers and appenders are nearly universal in logging frameworks. This can be a mixed blessing: no other framework is as battle-tested as log4net.

We have, in the past, given some tips about using log4net, as well as a quick tutorial on using it in .NET Core. At the risk of repeating ourselves, let’s at least look at how to set up a logger and log a message. We’ll do the same thing for each of the .NET logging frameworks: generate a message and log it to the console and a log file. All the code for this article is up on Github.

log4net can be installed from NuGet by just installing the log4net package.

log4net code example

We’re going to code everything against .NET Core because it’s the future. I mentioned earlier that log4net is old; part of that legacy is that it doesn’t quite play right with .NET Core. The configuration examples you’ll find in most places on the net won’t work but fortunately, the answer to configuring is right here on this blog (thanks, Matt!).

using log4net; using log4net.Config; using System; using System.IO; using System.Reflection; namespace LoggingDemo.Log4Net { class Program { private static readonly ILog log = LogManager.GetLogger(typeof(Program)); static void Main(string[] args) { var logRepository = LogManager.GetRepository(Assembly.GetEntryAssembly()); XmlConfigurator.Configure(logRepository, new FileInfo("log4net.config")); log.Debug("Starting up"); log.Debug("Shutting down"); Console.ReadLine(); } } }

We’re going to load the configuration from a file called log4net.config. This is an XML configuration file that contains the settings for logging. Don’t forget to set that file to copy to the output directory.

<log4net> <appender name="Console" type="log4net.Appender.ConsoleAppender"> <layout type="log4net.Layout.PatternLayout"> <!-- Pattern to output the caller's file name and line number --> <conversionPattern value="%utcdate %5level [%thread] - %message%newline" /> </layout> </appender> <appender name="RollingFile" type="log4net.Appender.RollingFileAppender"> <file value="logfile.log" /> <appendToFile value="true" /> <maximumFileSize value="100KB" /> <maxSizeRollBackups value="2" /> <layout type="log4net.Layout.PatternLayout"> <conversionPattern value="%utcdate %level %thread %logger - %message%newline" /> </layout> <appender> <root> <level value="DEBUG" /> <appender-ref ref="Console" /> <appender-ref ref="RollingFile" /> </root> </log4net>

This configuration file is quite long and complicated. Two appenders are defined: one to write to the console, and one to write to a rolling file that has a file size of 100KB. We’ve had to define a custom log format in the conversionPattern because the default log4net configuration is trash. With the conversionPattern in place we get logs that look like

2018-08-18 18:57:37,278 DEBUG 1 LoggingDemo.Log4Net.Program - Starting up 2018-08-18 18:57:37,298 DEBUG 1 LoggingDemo.Log4Net.Program - Shutting down

Conclusion on log4net

The configuration here is the failing of log4net. It is complicated and XML is always difficult to approach. It is possible to configure it using code, but it isn’t well documented. In fact, “not well documented” is pretty much the catchphrase for all of log4net. The examples are convoluted and focused on things like logging to SQL databases, which is sooo 2009. There are plenty of different appenders for log4net, so chances are you’ll not be hung out to dry for your particular logging case.

NLog Overview

NLog is also a pretty old project. Version 1.0 was released back in 2006, but it is still under active development with the latest version having been released in December of 2017. log4net hasn’t seen a release in 18 months, which isn’t necessarily bad as the project is stable. The latest release of NLog adds structured logging and support for .NET Standard.

It can be installed from NuGet by installing the nlog package.

NLog code example

using NLog; using System; namespace LoggingDemo.Nlog { class Program { static void Main(string[] args) { LogManager.LoadConfiguration("nlog.config"); var log = LogManager.GetCurrentClassLogger(); log.Debug("Starting up"); log.Debug("Shutting down"); Console.ReadLine(); } } }

The code here is pretty similar to log4net: load a configuration file and then log.

<?xml version="1.0" encoding="utf-8" ?> <nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <targets> <target name="logfile" xsi:type="File" fileName="logfile.txt" /> <target name="logconsole" xsi:type="Console" /> </targets> <rules> <logger name="*" minlevel="Debug" writeTo="logconsole" /> <logger name="*" minlevel="Debug" writeTo="logfile" /> </rules> </nlog>

Ah, now here is the difference between NLog and log4net. The configuration file is still XML, but it is a much cleaner looking XML file. We don’t have the need to write our own format.

The log format looks like this:

2018-08-18 13:27:10.5022|DEBUG|LoggingDemo.Nlog.Program|Starting up 2018-08-18 13:27:10.5623|DEBUG|LoggingDemo.Nlog.Program|Shutting down

There is also a way to configure the logging using code.

var config = new NLog.Config.LoggingConfiguration(); var logfile = new NLog.Targets.FileTarget("logfile") { FileName = "logfile.txt" }; var logconsole = new NLog.Targets.ConsoleTarget("logconsole"); config.AddRule(LogLevel.Debug, LogLevel.Fatal, logconsole); config.AddRule(LogLevel.Debug, LogLevel.Fatal, logfile); NLog.LogManager.Configuration = config;

The code-based configuration looks okay to me, but certainly isn’t as fluent as it could be.

Conclusion on NLog

There is some small difference between NLog and log4net. NLog is easier to configure, and supports a much cleaner code-based configuration than log4net. I think the defaults in NLog are also more sensible than in log4net.

One problem I had with both of these frameworks is that if something goes wrong with the logger (in my case forgetting to copy the configuration file), then there is no hint as to what is wrong. The idea is that logging problems should not take down the application. I can appreciate that desire, but if logging fails during startup that should be fatal. Logging isn’t, or shouldn’t be, an after-thought. A lot of automated problem detection relies on log output, and not having it is serious.

Serilog Overview

The newest logging framework of the bunch, Serilog, was released in 2013. The big difference between Serilog and the other frameworks is that it is designed to do structured logging out of the box. NLog also supports structured logging, but it is was only added recently and the benchmarks suggest that using it has some serious performance implications. Edit: It seems that there were some issues with that benchmark and that this one is a better baseline.

Wait! What’s structured logging? Gosh, I thought you’d never ask.

Structured logging example

Often you’ll find that you’re writing logs that contain two things: a message and value.

Example:

log.Debug($"User id is: ${userId}");

With most logging frameworks, this is simply translated to text in the log file. Text is nice and all, but knowing that the value logged was called userId and being able to search for that is actually very useful. Serilog keeps the properties available all the way to the destination.

log.Debug("User id is {@userId}", userId);

Serilog is backed by a commercial company https://getseq.net/ who provide a really nice log aggregation tool. Of course, you can also send your logs to our very favorite log aggregator: Retrace. Retrace also supports preserving the properties in the logs.

Installing Serilog is slightly more complex than NLog or log4net because it strives to be highly modular. For our example code, you’ll need the packages serilog, serilog.sinks.file, and serilog.sinks.console.

Serilog code example

No need for any XML configuration this time! The configuration for Serilog uses a fluent interface, which makes it very nice and clean. Compare how easy it is to set up a rolling file logger, as compared with log4net.

using Serilog; using System; namespace LoggingDemo.Serilog { class Program { static void Main(string[] args) { Log.Logger = new LoggerConfiguration() .MinimumLevel.Debug() .WriteTo.Console() .WriteTo.File("logfile.log", rollingInterval: RollingInterval.Day) .CreateLogger(); Log.Debug("Starting up"); Log.Debug("Shutting down"); Console.ReadLine(); } } } The logging output from Serilog looks like:

2018-08-18 14:37:21.463 -06:00 [DBG] Starting up 2018-08-18 14:37:21.560 -06:00 [DBG] Shutting down

Serilog Conclusion

The structured logging support in Serilog is really nice, as is the ease of configuration. I was able to get up and running on Seirlog more easily than any of the others. There are a huge number of destinations to which the logs in Serilog can be sent. Serilog calls these things “sinks” and you can check out a pretty comprehensive list at https://github.com/serilog/serilog/wiki/Provided-Sinks.

One other concept that I really like in Serilog is that of an enricher. This is a piece of code which runs for every log request, adding new information to it. For instance, if you needed to include information about the request in ASP.NET, you could use an enricher to append properties to the log messages. You can even use enrichers to change log entries. For instance, there is an enricher that improves the understandability of stack traces.

Conclusion: The Best Logging Framework is…

I promised that we’d end up with a single recommendation at the end of this article and this is it: Serilog. The API is more modern, it is easier to set up, it is better maintained and it does structured logging by default. The ability to add enrichers give you the ability to intercept and modify the messages is jolly useful. Serilog is my pick as the best of the .NET logging frameworks.

Logging is an important aspect to any application and shouldn’t be treated as an afterthought. Get your application off on the right foot and use structured logging. You should also use a log aggregator, like Retrace, to view all of your application logs in one place.