BrandGhost
NLog Rules and Filters: Routing Logs in .NET

NLog Rules and Filters: Routing Logs in .NET

NLog rules and filters give you surgical control over which log events reach which targets and under what conditions. Most developers know how to set a minimum log level, but NLog's rule engine goes much further -- you can route logs by logger name pattern, suppress entire namespaces, send warning-and-above to a separate file, or write only specific business events to a database while everything else goes to the console.

This guide covers every dimension of NLog's rules and filters system with practical C# examples. By the end, you'll understand how to silence the Microsoft.* noise that clutters development logs, how final="true" stops rule evaluation early, how to use when filter conditions for fine-grained control, and how to design a multi-target routing strategy for a production ASP.NET Core application.

If you're new to NLog, start with package installation and basic configuration before working through the routing system -- getting a working logger first makes the rules examples below much easier to experiment with. The complete guide to logging in .NET provides foundational context on how NLog fits into the broader .NET logging ecosystem alongside Serilog and Microsoft.Extensions.Logging.

How NLog Rules Work

The <rules> section in NLog configuration defines a pipeline. NLog evaluates rules top-to-bottom for every log event. Each rule can match by logger name, level, or both, and directs matching events to one or more targets. Rules keep evaluating until a final="true" rule matches -- at that point, evaluation stops for that log event.

<rules>
  <!-- Rule 1: Microsoft.* logs at Warn+ go to file -->
  <logger name="Microsoft.*" minlevel="Warn" writeTo="file" final="true" />
  <!-- Rule 2: Everything at Debug+ goes to console -->
  <logger name="*" minlevel="Debug" writeTo="console" />
</rules>

This configuration:

  1. Sends Microsoft.* events at Warn or above to the file target, then stops (no console)
  2. Sends everything else at Debug or above to console

Without final="true" on the first rule, Microsoft.* Warn events would also hit the console.

Rules Are Ordered and Cumulative

Rules are not exclusive by default -- a single log event can match multiple rules and be sent to multiple targets. This is intentional: NLog lets you fan out the same event to a console target, a file target, and a database target simultaneously by having multiple non-final rules match it.

The order matters because final="true" stops evaluation. Place more specific rules (with name patterns or level ranges) before more general ones.

Logger Name Matching

The name attribute on a rule supports glob-style patterns:

Pattern Matches
* Everything
MyApp.* Any logger starting with MyApp.
MyApp.Services.* Any logger in the Services namespace
Microsoft.* Any logger starting with Microsoft.
MyApp.Controllers.HomeController Exact match only

The logger name is the category name passed to ILogger<T>. For ILogger<HomeController>, the logger name is the fully qualified type name: MyApp.Controllers.HomeController.

Namespaced Routing Example

Here is a realistic routing setup for a production ASP.NET Core application that routes application logs, framework logs, and security audit logs to separate destinations:

<rules>
  <!-- Audit logs: SecurityAudit category → dedicated file, stop here -->
  <logger name="SecurityAudit" minlevel="Info" writeTo="auditFile" final="true" />

  <!-- Framework noise: Debug/Info Microsoft.* and System.* → discard -->
  <logger name="Microsoft.*" maxlevel="Info" final="true" />
  <logger name="System.*" maxlevel="Info" final="true" />

  <!-- Framework warnings: Warn+ from Microsoft.* → general file only, stop here -->
  <logger name="Microsoft.*" minlevel="Warn" writeTo="file" final="true" />

  <!-- Application logs: everything at Debug+ → console and rolling file -->
  <logger name="*" minlevel="Debug" writeTo="console,file" />
</rules>

The maxlevel="Info" rule with no writeTo and final="true" is the standard idiom for silencing a namespace entirely at a given level range. Because it has no target, the events are discarded. The final="true" prevents them from falling through to the catch-all rule at the bottom.

Log Level Ranges

NLog rules support minlevel, maxlevel, level, and levels attributes for precise level control. Understanding the difference between these is important -- minlevel is the most commonly used and matches the specified level and everything above it, while maxlevel caps at the specified level and includes everything below it. You can combine both to target a specific range.

Attribute Meaning Example
minlevel This level and above minlevel="Warn" → Warn, Error, Fatal
maxlevel This level and below maxlevel="Info" → Trace, Debug, Info
level Exactly this level level="Error"
levels Comma-separated list levels="Trace,Debug"

NLog's levels in ascending order: Trace → Debug → Info → Warn → Error → Fatal.

Level-Based Fan-Out

A common pattern sends high-severity events to a real-time alerting target while all events go to persistent storage:

<rules>
  <!-- Critical errors → Slack/PagerDuty webhook -->
  <logger name="*" minlevel="Error" writeTo="alertWebhook" />
  <!-- Everything Info+ → rolling file (separate from above, both fire) -->
  <logger name="*" minlevel="Info" writeTo="rollingFile" />
  <!-- Debug+ → console (dev only, disabled in prod via config) -->
  <logger name="*" minlevel="Debug" writeTo="console" />
</rules>

Because none of these rules use final="true", an Error event matches all three rules and is sent to all three targets simultaneously. This is the intended behavior -- you want errors in both the alert webhook and the rolling file.

The final Attribute

final="true" stops NLog from evaluating further rules for the current log event once this rule matches. It is the single most important tool for preventing log duplication and implementing "route-and-stop" logic.

Without final, every non-final rule that matches a log event will process it. With final, NLog stops at the first matching rule with final="true".

Silencing Microsoft Internals

The most common use of final is suppressing ASP.NET Core's verbose internal logging:

<rules>
  <!-- Suppress Microsoft.* Debug and Info entirely -->
  <logger name="Microsoft.*" maxlevel="Info" final="true" />
  <!-- Suppress System.* Debug and Info entirely -->
  <logger name="System.*" maxlevel="Info" final="true" />
  <!-- Everything else at Debug+ → console and file -->
  <logger name="*" minlevel="Debug" writeTo="console,file" />
</rules>

Notice the rules have no writeTo attribute -- they match and terminate without writing anywhere. This is how you discard log events in NLog. The final="true" then prevents the discarded events from reaching the catch-all rule below.

Route-and-Stop Pattern

Use final when a category of logs has special handling and must not also flow through general routing. Without final, the event would continue matching subsequent rules and end up in targets that were not intended for that category -- leading to duplicate log entries or cluttered outputs.

<rules>
  <!-- Performance metrics → metrics target only, stop here -->
  <logger name="Performance.*" minlevel="Info" writeTo="metricsTarget" final="true" />
  <!-- Background job logs → background jobs file only, stop here -->
  <logger name="*.BackgroundJob" minlevel="Debug" writeTo="backgroundFile" final="true" />
  <!-- General application → default targets -->
  <logger name="*" minlevel="Info" writeTo="console,file" />
</rules>

Each specialized category is handled in isolation. The catch-all at the bottom never sees Performance or BackgroundJob log events.

Filter Conditions with when

The when filter provides fine-grained, expression-based filtering within a rule. While name and level attributes filter on the rule level, when filters on a per-event basis using NLog's condition language.

<rules>
  <logger name="*" minlevel="Debug" writeTo="file">
    <filters>
      <!-- Skip health check endpoint noise -->
      <when condition="contains('${aspnet-request-url}', '/health')" action="Ignore" />
    </filters>
  </logger>
</rules>

The action attribute controls what happens when the condition is true:

Action Behavior
Ignore Discard this log event for this rule
IgnoreFinal Discard and stop all rule evaluation
Log Write this event (default, inverts the filter)
LogFinal Write this event and stop all rule evaluation
Neutral Defer to next filter in chain

Condition Language Basics

NLog's condition language is a mini expression language built into the logging library -- no external dependencies required. It supports comparisons, string operations, and boolean logic, and can reference any built-in renderer value including request URL, message content, and log level. The following expressions illustrate common patterns you can use:

level == LogLevel.Error
level >= LogLevel.Warn
message contains 'payment'
logger starts-with 'MyApp.Services'
contains('${aspnet-request-url}', '/healthz')
length(message) > 500

Conditions can be combined with and, or, not:

<when condition="level >= LogLevel.Warn and contains(logger, 'Database')"
      action="LogFinal" />

Practical Filter Example: Deduplication

In a high-throughput service, the same error can fire thousands of times per second. Use a when filter with a custom property to limit repetition. The ThrottleKey property must be set on the log event scope -- use NLog.ScopeContext.PushProperty to attach it before logging:

// Set ThrottleKey on the ambient scope so the filter condition can read it
using (NLog.ScopeContext.PushProperty("ThrottleKey", "payment-timeout"))
{
    _logger.LogError("Payment gateway timeout for {OrderId}", orderId);
}
<rules>
  <logger name="*" minlevel="Warn" writeTo="alertFile">
    <filters defaultAction="Log">
      <!-- Only log events that have ThrottleKey set (i.e., are throttle-eligible) -->
      <when condition="${event-properties:item=ThrottleKey} == ''"
            action="Log" />
    </filters>
  </logger>
</rules>

This pattern is a starting point -- NLog's WhenRepeatedFilter (available as a NuGet extension) provides a more complete implementation with built-in time-based deduplication.

JSON Configuration (appsettings.json)

Everything above can be expressed in appsettings.json for ASP.NET Core applications. The JSON format is more CI/CD-friendly since DevOps teams can inject values via environment variables without touching XML files.

{
  "NLog": {
    "rules": [
      {
        "logger": "Microsoft.*",
        "maxlevel": "Info",
        "final": true
      },
      {
        "logger": "System.*",
        "maxlevel": "Info",
        "final": true
      },
      {
        "logger": "Microsoft.*",
        "minlevel": "Warn",
        "writeTo": "file",
        "final": true
      },
      {
        "logger": "*",
        "minlevel": "Debug",
        "writeTo": "console,file"
      }
    ]
  }
}

The final property in JSON corresponds to final="true" in XML. The behavior is identical -- NLog evaluates JSON and XML configurations the same way at runtime.

Environment-Specific Rules

A real production setup often needs different rules for development vs. production environments. Development teams want verbose output with Trace and Debug enabled for fast feedback, while production should be quiet and efficient -- logging Info and above from application code, and suppressing framework Debug/Info entirely to reduce storage costs and improve signal quality. There are two main approaches to managing environment-specific rules.

Approach 1: Environment Variables in Config

Important: minlevel in NLog rules is not a layout -- it expects a literal level value (Trace, Debug, Info, Warn, Error, Fatal). NLog does not evaluate layout renderers or ternary expressions in minlevel. A config like minlevel="${environment:...}=Development ? Trace : Info" is invalid and will be rejected or silently ignored at runtime.

If you need environment-conditional minimum levels in a single config file, use NLog's <variable> support with ${gdc:item=...} or set a NLog global diagnostic context value from Program.cs before loading configuration. In practice, Approach 2 (separate config files) is simpler and less error-prone.

Approach 2: Separate Config Files

The simpler and more maintainable approach is a separate nlog.Production.config with tighter rules, loaded by your deployment pipeline. During development, nlog.Development.config uses minlevel="Debug" everywhere. In production, nlog.Production.config uses minlevel="Info" for application code and suppresses all Microsoft.* Debug/Info. This avoids complex condition expressions and makes each environment's routing obvious.

// In Program.cs, resolve config file by environment
var env = builder.Environment.EnvironmentName;
var configFile = $"nlog.{env}.config";
if (!File.Exists(configFile)) configFile = "nlog.config"; // fallback

LogManager.Setup().LoadConfigurationFromFile(configFile);

Complete Production Rules Template

Here is a complete rules configuration for a production ASP.NET Core application with console, rolling file, and error-only alert targets:

<rules>
  <!-- 1. Suppress EF Core query spam (Debug/Info) -->
  <logger name="Microsoft.EntityFrameworkCore.Database.Command"
          maxlevel="Info" final="true" />

  <!-- 2. Suppress all Microsoft.*/System.* Debug and Info -->
  <logger name="Microsoft.*" maxlevel="Info" final="true" />
  <logger name="System.*" maxlevel="Info" final="true" />

  <!-- 3. Microsoft.* Warn+ → file only (no console) -->
  <logger name="Microsoft.*" minlevel="Warn" writeTo="file" final="true" />
  <logger name="System.*" minlevel="Warn" writeTo="file" final="true" />

  <!-- 4. Application errors → dedicated error file (in addition to rules below) -->
  <logger name="MyApp.*" minlevel="Error" writeTo="errorFile" />

  <!-- 5. All application logs → console + rolling file -->
  <logger name="*" minlevel="Info" writeTo="console,file" />
</rules>

This layered approach eliminates framework noise from the console, keeps framework warnings in the file for postmortem analysis, and maintains a separate error file for fast filtering during incidents. NLog's target system -- file, console, database, Seq, and others -- is what backs the writeTo attribute in each rule.

Frequently Asked Questions

What does final="true" do in NLog rules?

final="true" stops NLog from evaluating further rules for the current log event once the rule with final matches. Without it, NLog continues evaluating all remaining rules and sends the event to any additional matching targets. Use final when you want route-and-stop behavior -- the event is handled by exactly one rule and goes no further.

How do I silence Microsoft.* logs without affecting my application logs?

Add two rules before your catch-all: one that matches Microsoft.* with maxlevel="Info" and final="true" (no writeTo -- this discards Debug/Info silently), and one that matches Microsoft.* with minlevel="Warn" and routes to a file target with final="true". This sends framework warnings to file without showing them on the console, and completely drops Debug/Info noise.

Can a single log event match multiple NLog rules?

Yes -- by default, NLog evaluates all rules top-to-bottom and a single event can match and be written by multiple rules simultaneously. This is how you fan out to multiple targets. The only exception is when a matching rule has final="true", which stops evaluation after that rule.

What is the difference between level and minlevel in NLog rules?

level matches exactly one log level (e.g., level="Error" matches only Error, not Fatal). minlevel matches that level and all higher levels (e.g., minlevel="Warn" matches Warn, Error, and Fatal). maxlevel matches that level and all lower levels. Use levels="Error,Fatal" when you need a non-contiguous set.

How do I use when filters to ignore health check log events?

Add a <filters> block inside the rule and use a when condition that matches the URL or message content. For ASP.NET Core health checks, contains('${aspnet-request-url}', '/health') with action="Ignore" drops events from health check endpoints before they are written to any target. Install NLog.Web.AspNetCore to enable the aspnet-request-url renderer.

How do I route logs to different files by log level?

Use multiple non-final rules, each with writeTo pointing to a different target, and level constraints that don't overlap. For example: a rule with minlevel="Error" and writeTo="errorFile", and a separate rule with minlevel="Info" maxlevel="Warn" and writeTo="infoFile". Since neither rule uses final, Error events match the first rule only (because Errors are above maxlevel=Warn), while Warn and Info events match only the second rule.

Should I configure NLog rules in XML or JSON?

Both are functionally equivalent. Use JSON (appsettings.json) for ASP.NET Core applications where you want environment-specific overrides via appsettings.Production.json or environment variables. Use XML (nlog.config) when your operations team is more comfortable with it or when you need NLog-specific features like dynamic reloading via autoReload="true". In ASP.NET Core specifically, integrating NLog rules with the host's middleware pipeline -- for instance, to enrich log context per-request -- works cleanly alongside custom middleware.

Wrapping Up

NLog's rules and filters system is one of its biggest strengths over simpler logging libraries. The combination of glob name matching, level ranges, final="true" short-circuits, and expression-based when filters lets you implement arbitrarily complex routing logic without any C# code -- just configuration.

The key patterns to internalize:

  • Silence by namespace: name="Microsoft.*" maxlevel="Info" final="true" (no writeTo)
  • Route and stop: match a category, send it to a target, add final="true"
  • Fan-out: multiple non-final rules all matching name="*" send to multiple targets simultaneously
  • Condition filtering: when conditions inside rules for per-event decisions

Complex rule chains have measurable performance implications -- especially when rules fan out to multiple slow targets synchronously. Wrapping synchronous targets in an AsyncWrapper and benchmarking with BenchmarkDotNet are the key techniques for keeping NLog from becoming a throughput bottleneck in high-traffic applications. For comparison on how Serilog approaches similar routing decisions, the Serilog in .NET complete guide covers the equivalent sink and filter configuration patterns.

NLog in .NET: Complete Guide to Flexible Logging

Master NLog in .NET with this complete guide. Learn targets, rules, layout renderers, structured logging, and performance optimization for C# applications.

Weekly Recap: NLog in .NET, OpenTelemetry, and Engineering Careers [Aug 2026]

This week covers NLog setup, targets, layout renderers, and structured logging in .NET. Plus, explore OpenTelemetry distributed tracing and practical career questions about difficult peers and what comes after senior engineer.

Getting Started with NLog in ASP.NET Core

Learn how to set up NLog in ASP.NET Core step by step. Covers XML nlog.config and appsettings.json configuration, Program.cs setup, Worker Services, and ILogger usage.

An error has occurred. This application may no longer respond until reloaded. Reload