Back to Logging guides

Logging in .NET: A Comparison of the Top 4 Libraries

Stanley Ulili
Updated on January 9, 2024

Logging libraries vary in strengths, with some better suited for specific tasks than others. Certain projects demand lightweight solutions, while others require robust capabilities to handle high log data volumes. As a result, choosing a logging library can be challenging, especially with numerous options available.

This article aims to comprehensively compare various .NET logging libraries, assisting you in making an informed decision for your project's specific needs.

Let's get started!

1. Microsoft.Extensions.Logging

Sreenshot of Microsoft documentation page

The Microsoft.Extensions.Logging is a flexible logging framework, which uses the ILogger API. It was introduced as the default logging framework in the .NET Core framework and is well supported across the .NET spectrum, including with ASP.NET, NuGet packages, and Entity Framework.

This framework was developed to cater to the varying logging needs of different projects. Sometimes, a project may start with one logging library and later decide to switch to a more sophisticated one, which often requires significant code changes. The main goal of Microsoft.Extensions.Logging is to provide a layer of abstraction over the logging API used in your project. It introduces a provider system, allowing logs to be routed to various destinations or libraries like Serilog or NLog. This approach helps to minimize dependency on any single logging framework.

Microsoft.Extensions.Logging accommodates several severity levels: Trace, Debug, Information, Warning, Error, and Critical. It also includes various built-in Logging Providers:

  • Console: Directs logs to the console.
  • Debug: Channels log events to the debug output window, viewable in IDEs like Visual Studio.

  • EventSource: Routes log events to an event source.

  • EventLog: Transmits logs to the Windows Event Log (only on Windows).

Beginning with Microsoft.Extensions.Logging is made easy, as demonstrated in the subsequent example:

 
using System;
using Microsoft.Extensions.Logging;

public class Program
{
    static void Main(string[] args)
    {
        using ILoggerFactory factory = LoggerFactory.Create(builder => builder.AddConsole().SetMinimumLevel(LogLevel.Information)); // Set to Information level
        ILogger logger = factory.CreateLogger("Program");

        logger.LogTrace("Trace message");
        logger.LogDebug("Debug message");
        logger.LogInformation("Info message");
        logger.LogWarning("Warning message");
        logger.LogError("Error message");
        logger.LogCritical("Critical message");
    }
}

In this example, you set up a logger that sends logs to the console with a minimum severity level of Information. When run, it will show output that looks like this:

Output
info: Program[0]
      Info message
warn: Program[0]
      Warning message
fail: Program[0]
      Error message
crit: Program[0]
      Critical message

You can also add context data to enrich the logs with information:

 
using System;
using Microsoft.Extensions.Logging;

public class Program
{
    static void Main(string[] args)
    {
        using ILoggerFactory factory = LoggerFactory.Create(builder => builder.AddConsole().SetMinimumLevel(LogLevel.Information));
        ILogger logger = factory.CreateLogger("Program");

        string productName = "Example Product";
        int quantity = 3;
        decimal totalPrice = 150.99M;

        logger.LogInformation($"Ordering {quantity} units of {productName} with a total price of {totalPrice:C}");
    }
}

After the file runs, it logs the following:

Output
info: Program[0]
      Ordering 3 units of Example Product with a total price of ¤150.99

Microsoft.Extensions.Logging pros

  • High performance, especially with source-generation logging.
  • Standardizes your code, allowing compatibility with any logging framework.
  • Lightweight in terms of resource usage.
  • Easy to configure.

Microsoft.Extensions.Logging cons

  • Inability to send logs to a file.
  • Limited support for structured logging.
  • Lack of useful features such as object state observation through destructuring, and LogContext.

2. Serilog

Screenshot of Serilog Github homepage

Serilog is one of the most popular and fastest logging frameworks available for .NET applications. Its features include support for structured logging and configuration options in XML or JSON format. Additionally, Serilog supports numerous sinks (destinations), such as cloud servers, databases, and message queues.

Getting started with Serilog is straightforward. Here's a simple example:

 
using System;
using Serilog;

namespace SerilogAdvanced
{
    class Program
    {
        static void Main(string[] args)
        {
            using var log = new LoggerConfiguration()
                .WriteTo.Console()
                .CreateLogger();

            log.Information("Hello, Serilog!");
        }
    }
}

When you run this program, it produces output similar to the following:

Output
[10:38:47 INF] Hello, Serilog!

While you've encountered one severity level so far, Serilog offers five logging level methods: Verbose(), Debug(), Information(), Warning(), Error(), and Fatal.

As mentioned, Serilog is highly configurable. To be able to configure it successfully, first, you need to be familiar with the following components:

  • Sinks: the destinations to which logs are forwarded, such as databases or the console. A curated list of examples can be found here.

  • Output Templates: responsible for formatting log entries.

  • Enrichers: modify or add properties to a log entry.

  • Filters: enable the filtering of logs based on specified criteria.

Now, let's look at an example that demonstrates how to configure Serilog to format logs as JSON and use sinks to forward logs to various destinations while setting the minimum severity level for each destination based on specific needs:

 
using System;
using Serilog;
using Serilog.Events;
using Serilog.Formatting.Json;

namespace SerilogAdvanced
{
    class Program
    {
        static void Main(string[] args)
        {
            using var log = new LoggerConfiguration()
                             .WriteTo.Console()
                             .WriteTo.File(new JsonFormatter(),
                                           "app.json",
                                           restrictedToMinimumLevel: LogEventLevel.Warning)
                             .WriteTo.File("all-.logs",
                                           rollingInterval: RollingInterval.Day)
                             .MinimumLevel.Debug()
                             .CreateLogger();
            log.Information("Hello, Serilog!");
            log.Warning("Warning, Serilog!");
        }
    }
}

In this example, logs are formatted into JSON and directed to an app.json file with a minimum severity warning level. Simultaneously, other logs are stored in the all-*.logs file.

When you run the file, the console shows the following output:

Output
]10:58:58 INF] Hello, Serilog!
[10:58:59 WRN] Warning, Serilog!

You will also find that the app.json log file has been created with the following JSON logs:

Output
{"Timestamp":"2023-11-30T10:58:59.0672316+00:00","Level":"Warning","MessageTemplate":"Warning, Serilog!"}

And the all-20231130.logs file, will have the following contents:

Output
2023-11-30 10:58:58.992 +00:00 [INF] Hello, Serilog!
2023-11-30 10:58:59.067 +00:00 [WRN] Warning, Serilog!

Serilog also facilitates adding context data to logs, which simplifies the debugging process. Here's an example:

 
using System;
using Serilog;

namespace SerilogAdvanced
{
    class Program
    {
        static void Main(string[] args)
        {

            using var log = new LoggerConfiguration()
                .WriteTo.Console()
                .CreateLogger();

            var orderId = 123;
            var customerId = "ABC123";

            log.Information("Processing order {OrderId} for customer {CustomerId}", orderId, customerId);

            Log.CloseAndFlush();
        }
    }
}

After running your file, the log entry will look the following:

Output
[11:06:46 INF] Processing order 123 for customer ABC123

Serilog pros

  • Offers extensive support for various sinks (destinations).

  • Benefits from active development and a robust community, ensuring continuous improvement and support.

  • Features a wide array of plugins to expand its functionality.

  • Supports advanced capabilities like destructuring and LogContext for enhanced logging details.

Serilog cons

Learn more: How To Start Logging With Serilog

3. Nlog

Screenshot of Nlog

NLog is a robust logging library for .NET, known for its high performance, flexibility, and full support for structured logging.

NLog supports the following severity levels: Fatal, Error, Warn, Info, Debug, and Trace.

Here's a simple example that demonstrates how to get started with NLog:

 
using System;
using NLog;

namespace LoggerExample
{
    class Program
    {
        private static readonly Logger logger = LogManager.GetCurrentClassLogger();

        static void Main(string[] args)
        {
            logger.Info("Hello from Nlog");
            logger.Error("Error from Nlog");

            LogManager.Shutdown();
        }
    }
}

The Logger is configured to log informational and error messages in this example. When you run the file, you won't see any output.

For Nlog to work, it needs proper configuration, which can be done programmatically or using a configuration file.

Regardless of which configuration you choose, you will need to familiarize yourself with the following concepts:

  • Targets: these are the destinations to which logs are sent; examples include files, databases, and the console.
  • Rules: defines the target to send logs to and the minimum severity level.
  • Layouts: format log messages in various formats such as JSON, CSV, etc.
  • Filters: Filter logs according to given criteria.

With that, let's look at a programmatically configured example:

 
using NLog;
using NLog.Common;
using NLog.Config;
using NLog.Targets;
using NLog.Layouts;

namespace LoggerExample
{
    class Program
    {
        private static Logger logger = LogManager.GetCurrentClassLogger();

        static void Main(string[] args)
        {
            // Create logging configuration
            var config = new LoggingConfiguration();

            // Define file target with JSON layout
            var fileTarget = new FileTarget { FileName = "myApp.log" };
            fileTarget.Layout = new JsonLayout
            {
                Attributes = {
                    new JsonAttribute("timestamp", "${date:format=yyyy-MM-ddTHH:mm:ssZ}"),
                    new JsonAttribute("level", "${level}"),
                    new JsonAttribute("message", "${message}"),
                    new JsonAttribute("exception", "${exception:format=Message}")
                }
            };
            config.AddTarget("file", fileTarget);

            // Define console target
            var consoleTarget = new ConsoleTarget();
            config.AddTarget("console", consoleTarget);

            // Create logging rules
            var fileRule = new LoggingRule("*", LogLevel.Info, fileTarget);
            config.LoggingRules.Add(fileRule);

            var consoleRule = new LoggingRule("*", LogLevel.Debug, consoleTarget);
            config.LoggingRules.Add(consoleRule);

            // Set configuration
            LogManager.Configuration = config;

            // Log messages
            logger.Info("Hello from Nlog");
            logger.Error("Error from Nlog");
        }
    }
}

In this example, you've configured two targets: FileTarget to send logs to a file (myApp.log) and ConsoleTarget to display logs in the console. The FileTarget is configured to use a JsonLayout for formatting the log messages in JSON format.

You then set up rules that define the minimum severity level and the target for each rule. This ensures that logs with a severity level of Info and above go to the file, while logs with a severity level of Debug and above go to the console.

Now, when you run the program, it will produce output similar to the following:

Output
2023-11-30 13:06:52.4780|INFO|LoggerExample.Program|Hello from Nlog
2023-11-30 13:06:52.5209|ERROR|LoggerExample.Program|Error from Nlog

It will also write JSON logs to the myApp.log file in the /bin/Debug/net8.0 directory, containing the following contents:

Output
{ "timestamp": "52Z", "level": "Info", "message": "Hello from Nlog" }
{ "timestamp": "52Z", "level": "Error", "message": "Error from Nlog" }

As mentioned, NLog can also be configured using an XML configuration file (nlog.config):

 
<?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"
    xsi:schemaLocation="http://www.nlog-project.org/schemas/NLog.xsd NLog.xsd">

    <targets>
        <target name="file" type="File" fileName="myApp.log">
            <layout type="JsonLayout">
                <attributes>
                    <attribute name="timestamp" layout="${date:format=yyyy-MM-ddTHH:mm:ssZ}" />
                    <attribute name="level" layout="${level}" />
                    <attribute name="message" layout="${message}" />
                    <attribute name="exception" layout="${exception:format=Message}" />
                </attributes>
            </layout>
        </target>

        <target name="console" type="Console" />
    </targets>

    <rules>
        <logger name="*" minLevel="Info" target="file" />
        <logger name="*" minLevel="Debug" target="console" />
    </rules>

</nlog>

This way, you don't have to configure programmatically; you add all the configurations in the external file, whichever suits you better.

Nlog pros

Nlog cons

  • Configuring can sometimes get complex.
  • Nlog is geting popular but its communtiy is smaller than other librarlies like Serilog
  • Nlog has a poor intergration with C# interpolated strings

Pros of NLog

  • It is fast.
  • Offers flexible configuration options, allowing both programmatic and file-based setups.
  • Has comprehensive and well-maintained and documentation.
  • Supports enhanced logging capabilities with features like the asynchronous wrapper.
  • Extensible through a variety of third-party plugins.

Cons of NLog

  • Configuration can sometimes become challenging, especially for complex scenarios.
  • While growing in popularity, NLog's community is still smaller compared to other libraries like Serilog, which might affect support and resource availability.
  • Limited integration with C# interpolated strings.

Learn more: How To Start Logging With NLog

4. Log4net

Screenshot of Log4net Github homepage

Log4net is a powerful logging framework for .NET that draws inspiration from Java's Log4j logging framework. It is known for its high-performance, modular, and extensively designed architecture, allowing it be extended through the use of plugins. Furthermore, It can be configured through XML or using dynamic configuration options.

You can kick off using Log4net with an example like this:

 
using log4net;
using log4net.Config;

public class Program
{
    private static readonly ILog logger = LogManager.GetLogger(typeof(Program));
    public static void Main(string[] args)
    {
        BasicConfigurator.Configure();

        logger.Info("This is an info message");

    }
}

In this example, Log4net is configured to send logs to the console.

After running the file, the output will look like this:

Output
118 [1] INFO Program (null) - This is an info message

Log4net is flexible and allows you to configure its behavior. Before configuring it, it helps to understand these two key components:

  • Appenders: destinations to which logs are forwarded. These destinations can include databases, files, or the console.
  • layouts : Layouts format log messages, determining how the information is presented.

To configure Log4net, you use the log4net.config file:

 
<log4net>
  <appender name="RollingFile" type="log4net.Appender.FileAppender">
    <file value="app.log" />
    <layout type='log4net.Layout.SerializedLayout, log4net.Ext.Json'>
      <decorator type='log4net.Layout.Decorators.StandardTypesDecorator, log4net.Ext.Json' />
      <default />
    </layout>
  </appender>

  <!-- Add a Console Appender -->
  <appender name="ConsoleAppender" type="log4net.Appender.ConsoleAppender">
    <layout type='log4net.Layout.SerializedLayout, log4net.Ext.Json'>
      <decorator type='log4net.Layout.Decorators.StandardTypesDecorator, log4net.Ext.Json' />
      <default />
    </layout>
  </appender>

  <root>
    <level value="ALL" />
    <appender-ref ref="RollingFile" />
    <!-- Reference the Console Appender -->
    <appender-ref ref="ConsoleAppender" />
  </root>
</log4net>

This configuration includes two appenders: ConsoleAppender, which directs logs to the console, and RollingFile, which sends logs to a file and rolls them based on size or date. Both of these appenders use a <layout> that uses the log4net.Ext.Json package to format logs as JSON.

For the configuration to work, the configuration file must be referenced in the code, as demonstrated here:

 
...
class Program
{
    private static readonly ILog log = LogManager.GetLogger(typeof(Program));

    static void Main()
    {
XmlConfigurator.Configure(new System.IO.FileInfo("log4net.config"));
log.Info("This is an info message"); log.Warn("This is a warning message"); } }

Running the program yields the following output:

Output
{"date":"2023-11-30T13:46:23.9033810+00:00","level":"INFO","logger":"Program","thread":"1","ndc":"(null)","message":"This is an info message"}
{"date":"2023-11-30T13:46:23.9363428+00:00","level":"WARN","logger":"Program","thread":"1","ndc":"(null)","message":"This is a warning message"}

The app.log in the bin/Debug/net8.0 or any directory of your choosing will also contain the JSON content:

Output
{"date":"2023-11-30T13:46:23.9033810+00:00","level":"INFO","logger":"Program","thread":"1","ndc":"(null)","message":"This is an info message"}
{"date":"2023-11-30T13:46:23.9363428+00:00","level":"WARN","logger":"Program","thread":"1","ndc":"(null)","message":"This is a warning message"}

Log4net pros

  • Easy to extend with plugins.
  • Features dynamic configuration, allowing changes without needing to restart the application.
  • Compatible with a wide range of frameworks, including .NET Framework 1+, Mono 1+, and others, ensuring broad application.
  • Known for its maturity and stability.

Log4net cons

  • Configuration processes can be complex and daunting, particularly for new users.
  • Finding .NET Core specific documentation can be challenging, complicating its implementation for modern applications.
  • The .NET Core implementation of Log4net is missing several features like colored console outputs, stack trace patterns, and others, as detailed here.
  • Encounters compatibility issues with Linux due to kernel calls in the Log4net codebase, leading to potential operational problems.
  • Updates for Log4net are infrequent, with the last major version released on NuGet in 2022, raising concerns about its ongoing maintenance and support.

Learn more: How To Start Logging With Log4net

5. ZLogger

Screenshot of ZLogger Github homepage

ZLogger Overview

ZLogger is a modern, zero-allocation text/structured logger for .NET and Unity, enhancing Microsoft.Extensions.Logging. It achieves high performance and minimal overhead by using string interpolation and IUtf8SpanFormattable for UTF8 output, unlike typical UTF16-based loggers.

It supports various output destinations like File, RollingFile, InMemory, Console, Stream, and an AsyncBatchingProcessor.

Getting started with ZLogger is quite straightforward:

 
using Microsoft.Extensions.Logging;
using ZLogger;

using var factory = LoggerFactory.Create(logging =>
{
    logging.SetMinimumLevel(LogLevel.Trace);

    // Output Structured Logging, setup options
    logging.AddZLoggerConsole(options => options.UseJsonFormatter());
});


var logger = factory.CreateLogger("Program");
var userName = "AliceSmith";
var action = "login";

// Use **Log** method and string interpolation to log a user login message
logger.LogInformation($"User '{userName}' just performed a '{action}' action.");

When run, it yields an output similar to this:

Output
{"Timestamp":"2024-01-07T19:07:01.5530174+00:00","LogLevel":"Information","Category":"Program","Message":"User \u0027AliceSmith\u0027 just performed a \u0027login\u0027 action.","{OriginalFormat}":"User \u0027AliceSmith\u0027 just performed a \u0027login\u0027 action."}

ZLogger pros

  • Offers high performance through the zero-allocation approach.
  • Specifically designed to support the latest .NET features.
  • Simplifies structured logging setup without the need for extensive configurations.
  • Customization is straightforward and can be done directly in the code.
  • Provides seamless integration support for Unity.

ZLogger cons

  • Lacks a variety of log forwarding destinations compared to Serilog and NLog.
  • As a newer tool, ZLogger has a smaller community, which may pose challenges in finding support and resources.

Final thoughts

In this article, we've explored several .NET logging frameworks. If you're uncertain about which to choose, we suggest starting with Microsoft.Extensions.Logging. It offers abstractions over other logging systems, allowing for easy switching as your project evolves. Depending on your specific needs, consider pairing it with a more comprehensive framework like Serilog or NLog for enhanced logging capabilities.

Thanks for reading, and happy logging!

Author's avatar
Article by
Stanley Ulili
Stanley is a freelance web developer and researcher from Malawi. He loves learning new things and writing about them to understand and solidify concepts. He hopes that by sharing his experience, others can learn something from them too!
Got an article suggestion? Let us know
Next article
10 Best Practices for Logging in Java
In this comprehensive tutorial, we will delve into the realm of best practices for creating a robust logging system specifically tailored for Java applications
Licensed under CC-BY-NC-SA

This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.

Make your mark

Join the writer's program

Are you a developer and love writing and sharing your knowledge with the world? Join our guest writing program and get paid for writing amazing technical guides. We'll get them to the right readers that will appreciate them.

Write for us
Writer of the month
Marin Bezhanov
Marin is a software engineer and architect with a broad range of experience working...
Build on top of Better Stack

Write a script, app or project on top of Better Stack and share it with the world. Make a public repository and share it with us at our email.

community@betterstack.com

or submit a pull request and help us build better products for everyone.

See the full list of amazing projects on github