NLog in .NET is one of the most battle-tested and flexible logging frameworks available for C# developers. While Serilog gets much of the spotlight in modern .NET circles, NLog has been in production systems since 2006 and offers a configuration model that many teams find uniquely powerful. Targets, layout renderers, and rules -- three independent concerns that compose into a remarkably expressive logging architecture.
This guide covers everything you need to start using NLog in production .NET applications. We'll walk through installation, both XML and appsettings.json configuration approaches, the core concepts of targets and rules, structured logging, and performance optimization -- all with practical C# examples targeting .NET 8 and above.
If you're evaluating logging frameworks broadly, Logging in .NET: The Complete Developer's Guide gives you the full landscape before you commit to any specific framework.
What is NLog and Why Use It?
NLog is a free, open-source logging platform for .NET. At its core, three concepts compose the entire system:
- Targets -- where log output goes (files, databases, the console, Seq, Elasticsearch, custom destinations)
- Layout Renderers -- what appears in each log message (timestamp, log level, logger name, structured properties, HTTP request context)
- Rules -- which loggers write to which targets, at what minimum log level, and with what filters
This separation makes NLog highly composable. Route debug logs to a rotating file, route errors to a database, and route fatal alerts to email -- all without touching application code. Add hot-reload support (autoReload="true") and you can change routing behavior at runtime without restarting the service.
NLog supports .NET 8, .NET 9, .NET Standard 2.0, and .NET Framework 4.6+. Its primary integration for ASP.NET Core is the NLog.Web.AspNetCore package, which adds HTTP-aware layout renderers and a clean host builder extension.
Installing NLog
For an ASP.NET Core application, install:
dotnet add package NLog.Web.AspNetCore
This pulls in the base NLog package as a dependency. The NLog.Web.AspNetCore package adds the ${aspnet-*} layout renderers (request URL, user identity, trace identifier) and the UseNLog() host builder extension.
For a Worker Service or console application, install:
dotnet add package NLog
dotnet add package NLog.Extensions.Logging
Setting Up NLog in ASP.NET Core (.NET 8)
NLog integrates with Microsoft.Extensions.Logging so your application code continues to use the familiar ILogger<T>. Only the registration is NLog-specific.
Here's the minimal Program.cs setup:
using NLog.Web;
var logger = NLogBuilder
.ConfigureNLog("nlog.config")
.GetCurrentClassLogger();
try
{
var builder = WebApplication.CreateBuilder(args);
builder.Logging.ClearProviders();
builder.Host.UseNLog();
builder.Services.AddControllers();
// ... other service registrations
var app = builder.Build();
app.MapControllers();
app.Run();
}
catch (Exception ex)
{
logger.Fatal(ex, "Application startup failed");
throw;
}
finally
{
NLog.LogManager.Shutdown();
}
The try/catch/finally pattern is important. The outer logger instance captures fatal startup failures before the DI container is initialized. NLog.LogManager.Shutdown() in finally flushes any buffered messages before the process exits -- skip this and you may lose the last few log entries on shutdown.
Two Configuration Approaches
NLog supports two distinct configuration styles. Choose whichever fits your team's conventions.
Option 1: nlog.config (XML)
The classic approach uses an XML file in your project root:
<?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"
autoReload="true"
throwConfigExceptions="true"
internalLogLevel="Warn"
internalLogFile="${basedir}/internal-nlog.txt">
<targets>
<target xsi:type="File"
name="appFile"
fileName="${basedir}/logs/app-${shortdate}.log"
layout="${longdate}|${uppercase:${level}}|${logger}|${message} ${exception:format=tostring}" />
<target xsi:type="Console"
name="console"
layout="${level:truncate=4:uppercase=true}|${logger:shortName=true}|${message} ${exception:format=message}" />
</targets>
<rules>
<logger name="Microsoft.*" maxlevel="Info" final="true" />
<logger name="System.Net.Http.*" maxlevel="Info" final="true" />
<logger name="*" minlevel="Debug" writeTo="appFile,console" />
</rules>
</nlog>
Set the file to copy to the output directory in your .csproj:
<ItemGroup>
<Content Include="nlog.config">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup>
Option 2: appsettings.json
For teams that prefer all configuration in one place, NLog supports appsettings.json when using NLog.Extensions.Logging (for generic host apps) or NLog.Web.AspNetCore (for ASP.NET Core). Install the integration package and call UseNLog() on the host builder -- NLog will then read its config from the "NLog" section automatically:
{
"NLog": {
"autoReload": true,
"throwConfigExceptions": true,
"internalLogLevel": "Warn",
"extensions": [
{ "assembly": "NLog.Web.AspNetCore" }
],
"targets": {
"appFile": {
"type": "File",
"fileName": "${basedir}/logs/app-${shortdate}.log",
"layout": "${longdate}|${uppercase:${level}}|${logger}|${message} ${exception:format=tostring}"
},
"console": {
"type": "Console",
"layout": "${level:truncate=4:uppercase=true}|${logger:shortName=true}|${message}"
}
},
"rules": [
{ "logger": "Microsoft.*", "maxLevel": "Info", "final": true },
{ "logger": "System.Net.Http.*", "maxLevel": "Info", "final": true },
{ "logger": "*", "minLevel": "Debug", "writeTo": "appFile,console" }
]
}
}
Update Program.cs to use the configuration-based setup instead of ConfigureNLog:
using NLog.Web;
var builder = WebApplication.CreateBuilder(args);
builder.Logging.ClearProviders();
builder.Host.UseNLog();
// NLog reads from builder.Configuration automatically when NLog section is present
The JSON approach works especially well in containerized environments where configuration overrides come from environment variables or Azure App Configuration.
Core Concept: Targets
Targets define where log output goes. NLog ships with dozens of built-in targets and an extensible target API for custom destinations.
| Target | Package | Use Case |
|---|---|---|
File |
Built-in | Rolling log files with archiving |
Console |
Built-in | Terminal output; great for containers |
Database |
Built-in | Direct SQL Server / PostgreSQL inserts |
Network |
Built-in | UDP/TCP forwarding to syslog receivers |
Mail |
Built-in | Email on critical errors |
Seq |
NLog.Targets.Seq |
Self-hosted structured log server |
Elasticsearch |
Elastic.NLog.Targets |
ELK stack ingestion |
Version note: The built-in status and exact feature set of targets like
Databaseand
For production throughput, wrapping any target in AsyncWrapper is essential:
<target xsi:type="AsyncWrapper" name="asyncFile" queueLimit="10000" overflowAction="Discard">
<target xsi:type="File" fileName="${basedir}/logs/app-${shortdate}.log" />
</target>
The AsyncWrapper offloads writes to a background thread. Without it, every log statement blocks the calling thread while waiting for disk I/O or a network round-trip.
Core Concept: Layout Renderers
Layout renderers are the ${} tokens that compose log message formats. Common ones:
| Renderer | Output |
|---|---|
${longdate} |
2026-08-01 21:00:00.0000 |
${level} |
Info, Warn, Error |
${logger} |
Fully qualified class name |
${message} |
The log message text |
${exception:format=tostring} |
Exception with full stack trace |
${callsite} |
Class + method name |
${aspnet-request-url} |
HTTP request URL (ASP.NET Core only) |
${event-properties:item=X} |
Structured log property value |
The ${event-properties} renderer is the key to structured logging in NLog. When you write LogInformation("Order {OrderId} received", orderId), NLog captures OrderId as a named property. Use ${event-properties:item=OrderId} in your layout to reference it -- or use JsonLayout with includeAllProperties="true" to emit all properties as a JSON document.
Core Concept: Rules
Rules connect loggers to targets. Each rule specifies a logger name pattern, a minimum (and optional maximum) log level, and the target(s) to write to.
<rules>
<!-- Silence framework noise - final="true" stops further rule evaluation -->
<logger name="Microsoft.*" maxlevel="Info" final="true" />
<logger name="System.Net.Http.*" maxlevel="Info" final="true" />
<!-- Application logs go to file -->
<logger name="*" minlevel="Debug" writeTo="appFile" />
<!-- Errors also route to a secondary target -->
<logger name="*" minlevel="Error" writeTo="errorFile" />
</rules>
final="true" is a critical attribute. Without it, after matching a rule NLog continues evaluating subsequent rules. With it, evaluation stops at the first match. This is how you suppress verbose Microsoft framework logs without losing your own application output.
Using ILogger in Your Services
Once registered, you use standard ILogger<T> throughout your application:
public sealed class OrderService
{
private readonly ILogger<OrderService> _logger;
public OrderService(ILogger<OrderService> logger)
{
_logger = logger;
}
public async Task ProcessOrderAsync(int orderId)
{
_logger.LogInformation("Processing order {OrderId}", orderId);
try
{
// ... business logic
_logger.LogInformation("Order {OrderId} processed successfully", orderId);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to process order {OrderId}", orderId);
throw;
}
}
}
Use message templates with named placeholders ({OrderId}) rather than string interpolation. String interpolation discards the property structure -- with a target like Seq or Elasticsearch, that structure is what makes logs queryable. For more on how ILogger<T> is registered in the DI container, see IServiceCollection in C# -- Complete Guide with AddSingleton, AddScoped & AddTransient.
Structured Logging with NLog
Structured logging emits log events as structured data rather than flat strings. NLog supports this via JsonLayout:
<target xsi:type="File" name="jsonFile" fileName="${basedir}/logs/structured-${shortdate}.json">
<layout xsi:type="JsonLayout" includeAllProperties="true" includeEventProperties="true">
<attribute name="time" layout="${longdate}" />
<attribute name="level" layout="${level}" />
<attribute name="logger" layout="${logger}" />
<attribute name="message" layout="${message}" />
<attribute name="exception" layout="${exception:format=tostring}" encode="false" />
</layout>
</target>
With includeAllProperties="true", any named properties from your message templates appear as top-level JSON fields. A log entry for LogInformation("Order {OrderId} from {CustomerId}", 42, "CUST-7") produces:
{
"time": "2026-08-01 21:00:00.0000",
"level": "Info",
"logger": "MyApp.OrderService",
"message": "Order 42 from CUST-7",
"OrderId": 42,
"CustomerId": "CUST-7"
}
This is directly queryable in Seq, Kibana, or Grafana Loki without any additional parsing pipeline.
What Each Article in This Series Covers
This article is the hub for a complete NLog series. The deep-dive spokes cover:
- Getting Started with NLog in ASP.NET Core -- Full setup walkthrough including both XML and
appsettings.jsonapproaches, the startup/shutdown pattern, and a Worker Service example - NLog Targets -- File, Console, Database, and a complete custom target implementation in C#
- NLog Layout Renderers and Structured Logging --
JsonLayout, event properties, Seq integration, and Elasticsearch output - NLog Rules and Filters -- Routing by level, logger, and condition; MDC/NDC context for request correlation
- NLog Performance: AsyncWrapper and High-Throughput Logging --
AsyncWrappertuning, queue limits, and how to keep logging off the hot path - NLog vs Serilog -- A side-by-side comparison with real code to help you choose
How NLog Fits in the .NET Logging Ecosystem
The .NET logging ecosystem layers neatly. At the top is Microsoft.Extensions.Logging -- the abstraction that all ILogger<T> calls flow through. Underneath that abstraction, you plug in a provider: NLog, Serilog, or any other implementation. Your application code is completely decoupled from the choice.
NLog's NLog.Extensions.Logging (and NLog.Web.AspNetCore for ASP.NET Core) act as that provider, translating Microsoft.Extensions.Logging calls into NLog's internal pipeline.
The ecosystem comparison between providers is worth understanding before picking one. Serilog vs Microsoft.Extensions.Logging: Which Should You Use? explains the abstraction-vs-provider distinction in depth -- the same reasoning applies when evaluating NLog. And Serilog in .NET: Complete Guide to Structured Logging covers Serilog's approach for a direct comparison.
Frequently Asked Questions
What is NLog used for in .NET?
NLog is used for structured and unstructured application logging in .NET. It provides a configurable pipeline that routes log output to one or more destinations -- files, databases, email, log aggregation tools like Seq or Elasticsearch -- based on log level, logger name, and custom filter conditions. It integrates with Microsoft.Extensions.Logging so application code uses ILogger<T> regardless of which underlying logging framework is configured.
How do I install NLog in ASP.NET Core?
Install the NLog.Web.AspNetCore NuGet package. In Program.cs, call builder.Logging.ClearProviders() and builder.Host.UseNLog(). Provide configuration either via an nlog.config XML file or the NLog section in appsettings.json. Wrap the application startup in try/catch/finally with NLog.LogManager.Shutdown() in finally to ensure buffered logs flush on exit.
What is the difference between NLog targets and layout renderers?
Targets define the destination for log output (a file, a database table, the console, a remote API). Layout renderers define the content of each log message -- the format string made up of ${} tokens like ${longdate}, ${level}, ${message}, and ${event-properties}. Targets and layout renderers are independent: any renderer can be used in any target's layout.
Does NLog support structured logging?
Yes. NLog supports structured logging through ${event-properties} layout renderers and the JsonLayout target layout. Message templates like LogInformation("User {UserId} logged in", userId) capture UserId as a named property that JsonLayout with includeAllProperties="true" emits as a top-level JSON field, enabling downstream search and filtering in tools like Seq, Kibana, or Grafana.
Can NLog write to multiple targets simultaneously?
Yes. Define multiple targets and create rules that route to each. A single logger can match multiple non-final rules, writing the same event to a file, a database, and the console simultaneously. Use final="true" on a rule to stop further evaluation when you want exclusive routing -- for example, to silence a noisy framework logger without affecting your application logs.
What is the AsyncWrapper in NLog?
AsyncWrapper is an NLog target that wraps another target and queues writes to a background thread. Without it, logging blocks the calling thread while waiting for disk I/O or a database insert. Configure queueLimit and overflowAction to control behavior under backpressure. The async="true" attribute on <targets> applies asynchronous behavior to all targets at once -- but use one approach or the other, never both, or you double-wrap.
How does NLog compare to Serilog?
NLog and Serilog are both mature, production-ready .NET logging frameworks. NLog's primary strengths are its file-based configuration (XML or JSON, hot-reloadable) and its flexible rule-based routing. Serilog's strengths are its C# fluent configuration API, first-class structured logging, and a large sink ecosystem. Both integrate identically with Microsoft.Extensions.Logging at the application code level. The choice usually comes down to configuration style preference and specific target/sink ecosystem fit.

