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
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:
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:
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
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:
[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:
]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:
{"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:
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:
[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
- Unable to immediately recreate and write to the current rolling file, which can delay logging to new files.
- Doesn't natively log exceptions as structured objects, often necessitating third-party tools like Serilog.Exceptions for structured error logging.
- Requires a substantial number of dependencies for full setup, potentially complicating the initial configuration process.
Learn more: How To Start Logging With Serilog
3. 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:
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:
{ "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
- 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.
NLog cons
- 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
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:
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:
{"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:
{"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
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:
{"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!
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 usBuild 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.comor submit a pull request and help us build better products for everyone.
See the full list of amazing projects on github