BrandGhost
NLog Targets in .NET: File, Database, Console, and Custom

NLog Targets in .NET: File, Database, Console, and Custom

NLog targets are the destinations where your log messages are delivered -- files on disk, a console window, a database table, a remote log server, or anywhere else you define. Every NLog configuration requires at least one target, and choosing the right combination of targets is one of the most impactful decisions in your logging setup.

This guide covers the built-in targets that come with NLog, the most useful third-party targets for production use, and how to write your own custom target when the built-ins don't fit your requirements.

Before diving into specific targets, understanding the relationship between targets, layout renderers, and rules makes the individual targets much easier to configure correctly. Targets handle the "where" of log delivery -- the layout renderer and rules system handles the "what" and "which events".

How NLog Targets Work

Targets receive LogEventInfo objects from the NLog rules engine and write them somewhere. The built-in targets cover the most common destinations. All targets share a few universal properties:

  • name -- The identifier used in writeTo="..." in your rules
  • layout -- The format string applied before writing. Every target that writes text has a layout.
  • encoding -- Character encoding for file-based targets (default: UTF-8)

Some targets add async buffering. Others provide connection management for databases and network sockets. The rest of this article covers the details.

File Target

The File target is the workhorse of NLog configuration. It writes log messages to disk with optional archiving, compression, and concurrent write support.

<targets>
  <target xsi:type="File"
          name="logfile"
          fileName="${basedir}/logs/app-${shortdate}.log"
          archiveFileName="${basedir}/logs/archives/app-{#}.log"
          archiveEvery="Day"
          archiveNumbering="Rolling"
          maxArchiveFiles="30"
          archiveAboveSize="10485760"
          concurrentWrites="true"  <!-- NLog 5.x only; ConcurrentWrites was removed in NLog 6 -->
          keepFileOpen="true"
          layout="${longdate}|${uppercase:${level}}|${logger}|${message} ${exception:format=tostring}" />
</targets>

Key File target attributes:

Attribute Description
fileName Active log file path. Supports layout renderers -- ${shortdate} creates a new file per day.
archiveFileName Archive file path pattern. {#} is replaced by the archive number or date.
archiveEvery Archive trigger: Day, Hour, Month, Year, None
archiveNumbering How archives are numbered: Rolling (overwrite oldest), Sequence (append), Date, DateAndSequence
maxArchiveFiles Maximum number of archive files to keep. Oldest are deleted when exceeded.
archiveAboveSize Archive when the file exceeds this size in bytes (10 MB in the example above)
concurrentWrites Allow multiple processes to write to the same file (uses file locking). NLog 5.x only -- removed in NLog 6.
keepFileOpen Keep the file handle open between writes (much faster than reopening every write)

File Naming with Date Variables

The ${shortdate} layout renderer in the file name creates daily rolling logs automatically -- no explicit archiveEvery needed if you want one file per day by filename:

<!-- One file per day, named by date -->
<target xsi:type="File"
        name="daily"
        fileName="${basedir}/logs/${shortdate}/app.log"
        layout="${longdate}|${level:uppercase=true}|${logger:shortName=true}|${message}" />

This pattern creates a directory per day and puts app.log inside it -- useful when you want to browse logs by directory rather than by filename.

AsyncWrapper for File Targets

File I/O is blocking. Wrapping a File target in AsyncWrapper puts the writes on a background queue:

<targets>
  <target xsi:type="AsyncWrapper" name="asyncFile" queueLimit="5000" overflowAction="Discard">
    <target xsi:type="File"
            name="file"
            fileName="${basedir}/logs/app-${shortdate}.log"
            layout="${longdate}|${level:uppercase=true}|${logger}|${message} ${exception:format=tostring}" />
  </target>
</targets>

overflowAction="Discard" drops messages when the queue is full rather than blocking the application thread. For latency-sensitive services this is a common choice -- dropping a log entry is usually preferable to slowing down request processing. For regulated or compliance-sensitive systems, however, you may need overflowAction="Block" to guarantee no log data is silently lost.

When using appsettings.json, the "async": true property on the targets block applies AsyncWrapper to all targets automatically:

"targets": {
  "async": true,
  "logfile": {
    "type": "File",
    "fileName": "${basedir}/logs/app-${shortdate}.log"
  }
}

Console Target

The Console target writes to standard output. It's the right choice for development, containerized workloads (where container orchestrators collect stdout), and any environment where log aggregation happens outside the process.

<target xsi:type="Console"
        name="console"
        layout="${level:truncate=4:uppercase=true}|${logger:shortName=true}|${message} ${exception:format=message}" />

For a shorter layout on the console than what you write to files: ${logger:shortName=true} strips the namespace prefix, leaving just the class name.

ColoredConsole Target

ColoredConsole extends the Console target with ANSI color coding by log level:

<target xsi:type="ColoredConsole"
        name="coloredConsole"
        layout="${time}|${level:uppercase=true}|${logger:shortName=true}|${message} ${exception:format=message}">
  <highlight-row condition="level == LogLevel.Error" foregroundColor="Red" />
  <highlight-row condition="level == LogLevel.Warn" foregroundColor="Yellow" />
  <highlight-row condition="level == LogLevel.Debug" foregroundColor="DarkGray" />
</target>

This is useful for local development but adds no value in containerized or log-aggregated environments where you don't see the raw console output.

Null Target

The Null target discards all messages sent to it. It sounds useless but has a specific and valuable purpose -- silencing a logger category without affecting other rules:

<targets>
  <target xsi:type="Null" name="blackhole" />
</targets>

<rules>
  <!-- Silently discard all EF Core query logs -->
  <logger name="Microsoft.EntityFrameworkCore.Database.Command" maxlevel="Info" writeTo="blackhole" final="true" />
  <logger name="*" minlevel="Info" writeTo="asyncFile,console" />
</rules>

writeTo="blackhole" final="true" is cleaner than using maxlevel alone when you want to completely discard specific loggers.

Network Target

The Network target sends log messages over TCP or UDP, enabling log aggregation to a remote host:

<target xsi:type="Network"
        name="logstash"
        address="tcp://logstash-host:5000"
        layout="${message}" />

For UDP (fire-and-forget):

<target xsi:type="Network"
        name="udpTarget"
        address="udp://syslog-host:514"
        layout="${message}" />

Network targets require careful consideration for high-throughput applications -- always wrap them in AsyncWrapper and use overflowAction="Discard" to prevent network latency from blocking application threads.

Database Target

The Database target writes log entries to a relational database. It works with any ADO.NET provider.

<target xsi:type="Database"
        name="database"
        dbProvider="MySql.Data.MySqlClient.MySqlConnection, MySql.Data"
        connectionString="${configsetting:item=ConnectionStrings.AppDb}"
        commandText="INSERT INTO Logs (Timestamp, Level, Logger, Message, Exception)
                     VALUES (@timestamp, @level, @logger, @message, @exception)">
  <parameter name="@timestamp" layout="${date:universalTime=true:format=yyyy-MM-dd HH:mm:ss}" />
  <parameter name="@level" layout="${level}" />
  <parameter name="@logger" layout="${logger}" />
  <parameter name="@message" layout="${message}" />
  <parameter name="@exception" layout="${exception:format=tostring}" />
</target>

The required Logs table:

CREATE TABLE Logs (
    Id         INT AUTO_INCREMENT PRIMARY KEY,
    Timestamp  DATETIME        NOT NULL,
    Level      VARCHAR(16)     NOT NULL,
    Logger     VARCHAR(512)    NOT NULL,
    Message    TEXT            NOT NULL,
    Exception  TEXT            NULL
);

${configsetting:item=ConnectionStrings.AppDb} reads directly from appsettings.json -- no hardcoded connection strings in the XML config file.

Database targets should always be wrapped in AsyncWrapper. Synchronous database writes on the logging path will noticeably slow request-heavy applications.

Seq Target

Seq is a self-hosted log server with a full-text search UI. The NLog.Targets.Seq package provides a first-class NLog integration:

dotnet add package NLog.Targets.Seq

Configuration:

<extensions>
  <add assembly="NLog.Targets.Seq" />
</extensions>

<targets>
  <target xsi:type="BufferingWrapper" name="seqBuffer" bufferSize="200" flushTimeout="2000">
    <target xsi:type="Seq"
            name="seq"
            serverUrl="http://localhost:5341"
            apiKey="${configsetting:item=Seq.ApiKey}">
      <!-- Structured properties -- shown as filterable fields in the Seq UI -->
      <property name="Application" value="MyApp" />
      <property name="Environment" value="${configsetting:item=ASPNETCORE_ENVIRONMENT}" />
    </target>
  </target>
</targets>

The BufferingWrapper batches events before sending them to Seq over HTTP, reducing per-event HTTP overhead significantly.

Elasticsearch Target

For centralized log aggregation at scale, the Elastic.NLog.Targets package writes structured log events to Elasticsearch:

dotnet add package Elastic.NLog.Targets
<extensions>
  <add assembly="Elastic.NLog.Targets" />
</extensions>

<targets>
  <target xsi:type="ElasticSearch"
          name="elastic"
          uri="http://localhost:9200"
          index="app-logs-${shortdate}"
          includeAllProperties="true" />
</targets>

includeAllProperties="true" sends all structured log properties (message template parameters) as separate fields in the Elasticsearch document -- important for using Kibana dashboards effectively.

Custom Target

When the built-in targets don't cover your requirements, write your own. The pattern is simple: inherit from TargetWithLayout, decorate with [Target], and override Write:

using NLog;
using NLog.Config;
using NLog.Targets;

[Target("SlackAlert")]
public sealed class SlackAlertTarget : AsyncTaskTarget
{
    // Configuration properties declared on the target
    // (readable from nlog.config attributes or appsettings.json)
    [RequiredParameter]
    public string WebhookUrl { get; set; } = string.Empty;

    public string Channel { get; set; } = "#alerts";

    private readonly HttpClient _httpClient = new();

    protected override async Task WriteAsyncTask(LogEventInfo logEvent, CancellationToken cancellationToken)
    {
        // Layout.Render applies the target's layout string to the log event
        var message = Layout.Render(logEvent);

        var payload = new
        {
            channel = Channel,
            text = $"[{logEvent.Level.Name.ToUpper()}] {message}"
        };

        var json = System.Text.Json.JsonSerializer.Serialize(payload);
        var content = new StringContent(json, System.Text.Encoding.UTF8, "application/json");

        await _httpClient.PostAsync(WebhookUrl, content, cancellationToken);
    }
}

Register the custom target before loading NLog configuration:

// Register before ConfigureNLog or UseNLog
NLog.Config.ConfigurationItemFactory.Default.Targets
    .RegisterDefinition("SlackAlert", typeof(SlackAlertTarget));

var builder = WebApplication.CreateBuilder(args);
builder.Host.UseNLog();

Use in nlog.config:

<extensions>
  <add assembly="YourApp" />  <!-- Or register programmatically as shown above -->
</extensions>

<targets>
  <target xsi:type="SlackAlert"
          name="slack"
          webhookUrl="${configsetting:item=Slack.WebhookUrl}"
          channel="#production-alerts" />
</targets>

<rules>
  <!-- Only route Error and Fatal to Slack -->
  <logger name="*" minlevel="Error" writeTo="slack" />
</rules>

For targets that do I/O (HTTP, database, network), inherit from AsyncTaskTarget and override WriteAsyncTask -- this is NLog's canonical async extension point. Wrap the target in AsyncWrapper in your config for production use.

Combining Multiple Targets

Most production configurations write to multiple targets simultaneously. Rules control which targets each logger writes to:

<rules>
  <!-- Framework noise: discard at Info and below -->
  <logger name="Microsoft.*" maxlevel="Info" final="true" />
  
  <!-- Errors to file, database, and Slack alert -->
  <logger name="*" minlevel="Error" writeTo="asyncFile,database,slack" />
  
  <!-- Everything Info and above to file and console -->
  <logger name="*" minlevel="Info" writeTo="asyncFile,console" />
</rules>

A single logger event can match multiple rules (unless final="true" is set). The event is sent to all targets from all matching rules. This is how you achieve "log everything to file but only errors to Slack" without duplicating rules.

Understanding how rules and filters interact is a deep topic -- the companion article on NLog Rules and Filters covers routing by level, logger name, and custom conditions in detail.

For more on integrating NLog targets with ASP.NET Core middleware (request-scoped logging, correlation IDs), Ultimate Starter Guide to Middleware in ASP.NET Core explains how the pipeline fits together.

Frequently Asked Questions

What is the difference between a target and a layout renderer in NLog?

A target is the destination for log output -- a file, database, console, or network endpoint. A layout renderer formats the log message content before it is written to the target. Targets decide where log messages go; layout renderers decide what gets written. Every target that produces text output has a layout property that contains one or more layout renderers.

Should I always use AsyncWrapper with File targets?

For any production application with more than trivial logging volume, yes. Without AsyncWrapper, every log write blocks the calling thread while waiting for the disk I/O to complete. With AsyncWrapper, log writes are queued in memory and written by a background thread. The tradeoff is that messages queued at application shutdown may be lost if LogManager.Shutdown() isn't called -- which is why the try/finally shutdown pattern is essential.

How do I read the connection string from appsettings.json in a Database target?

Use ${configsetting:item=ConnectionStrings.AppDb} in the connectionString attribute. The configsetting layout renderer reads values from NLog's registered IConfiguration instance, which is populated from appsettings.json when you call builder.Host.UseNLog(). This works for any configuration key, not just connection strings.

Can I write to multiple targets in a single rule?

Yes. Set writeTo to a comma-separated list of target names: writeTo="asyncFile,console,database". All three targets receive the log event. The incremental overhead per additional target is usually negligible when each is wrapped in AsyncWrapper, but measure under your specific load profile -- adding targets does add per-event allocation and dispatch cost.

How do I create a custom NLog target for an HTTP endpoint?

Inherit from AsyncTaskTarget, decorate the class with [Target("YourTargetName")], and override WriteAsyncTask(LogEventInfo, CancellationToken). Inject your HTTP client as a shared field (not constructed per write). Register the type before loading NLog configuration using ConfigurationItemFactory.Default.Targets.RegisterDefinition(...). Wrap the target in AsyncWrapper in your config to prevent HTTP latency from blocking application threads.

What is the Null target used for?

The Null target discards all messages silently. It is used with final="true" in rules to completely suppress specific logger categories -- for example, silencing EF Core command logs for a specific database context without affecting other EF Core loggers. It is more explicit and flexible than using maxlevel alone.

How does NLog handle target failures?

When a target encounters a write error (disk full, database connection lost, network timeout), NLog by default logs the error to its internal log (internal-nlog.txt) and continues. The failing target does not propagate exceptions to the application. You can configure retry behavior per target or use FallbackGroup to route to a secondary target when the primary fails.

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.

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.

How to Multi-Target a NuGet Package for .NET 6, .NET 8, and .NET Standard

Learn how to multi-target NuGet packages in .NET using TargetFrameworks. Support .NET 6, .NET 8, and .NET Standard 2.0 in a single package with conditional compilation.

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