Back to: ASP.NET Core Web API Tutorials
How to Implement Logging using nLog in ASP.NET Core Web API
In this article, I will discuss How to Implement Logging using nLog in the ASP.NET Core Web API Application with Examples. Please read our previous article discussing Logging into a SQL Server Database with Serilog in ASP.NET Core Web API Application.
What is NLog?
NLog is a free, open-source, and high-performance logging library designed for various .NET platforms, such as .NET Core, .NET Framework, Mono, and Xamarin. It supports structured logging, asynchronous logging, and log filtering, making it ideal for debugging, performance tracking, and error monitoring in ASP.NET Core Web API applications.
NLog Key Features:
The following are the Key Features of NLog:
- Multiple Targets: Write log messages to files, databases, consoles, etc.
- Flexible Log Levels: Support for Trace, Debug, Information, Warning, Error, and Critical logging.
- Structured Logging: Ability to log rich, structured data (e.g., JSON).
- Asynchronous Logging: Offloads log writing to a background thread to improve the application performance.
How Do We Configure NLog in an ASP.NET Core Web API?
Let us understand the step-by-step process of installing and configuring NLog and using NLog to log different types of messages to different targets such as Consoles, Files, and Databases in an ASP.NET Core Web API Application. First, create a new ASP.NET Core Web API Project named LoggingDemo.
Install NLog Packages
To use NLog in ASP.NET Core applications, we need to add NLog.Web.AspNetCore packages. This package provides the necessary components to integrate NLog with ASP.NET Core’s dependency injection and logging infrastructure. You can install this package using the Package Manager Console by executing the following command:
Install-Package NLog.Web.AspNetCore
Adding and Configuring NLog.config
After installing the required package, we need to configure NLog by adding the NLog.config file to the project root directory. Keeping the configuration file at the project root makes it easier to manage and ensures that it is picked up during runtime. To do so, add a new XML file using the Add-> New Item window and give the file name NLog.config. This file should be created in the project root directory, as shown in the image below.
This NLog.config file defines how logging should be handled (e.g., Log Levels (Trace, Debug, Information, Warning, Error, Fatal), Targets (Console, File, Database, etc.), and Rules (Ignoring the System, Microsoft Namespaces logs)). Once you have the NLog.config file, please copy and paste the following code. The following code is self-explained, so please read the comment lines for a better understanding.
<?xml version="1.0" encoding="utf-8" ?> <!-- autoReload="true": Automatically reloads the config if changes occur during runtime. internalLogLevel="Warn": Set internal logging level for NLog. Logs only warnings and errors related to NLog itself. internalLogFile="internal-nlog.txt": Log internal NLog issues to this file, useful for debugging NLog issues. throwConfigExceptions="true": Throws exceptions for Nlog config errors for easier debugging. --> <nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" autoReload="true" internalLogLevel="Warn" internalLogFile="internal-nlog.txt" throwConfigExceptions="true"> <!-- Define various log targets where logs will be written --> <targets> <!-- type="File": File Traget. Write logs to a text file. name="file": Name for this target. Rule section will use this name while defining rules. fileName="D:/Logs/nlog-all-${shortdate}.log": Path to the file where logs will be saved with date-based naming. layout: Log format with date, level, logger (fully qualified class name), message (actual log message), and exception details if any. --> <target xsi:type="File" name="file" fileName="D:/Logs/nlog-all-${shortdate}.log" layout="${longdate}|${level:uppercase=true}|${logger}|${message} ${exception:format=ToString}" /> <!-- type="Console: Console target. Display logs in the console window. name="console": Name for this target. Rule section will use this name while defining rules. layout: Format similar to file target. If you want you can give a different format --> <target xsi:type="Console" name="console" layout="${longdate}|${level:uppercase=true}|${logger}|${message} ${exception:format=ToString}" /> <!-- type="Null": Null target. Discard logs, used to filter out logs from specific namespaces. name="blackhole": Any logs sent to this target will be ignored or discarded. --> <target xsi:type="Null" name="blackhole" /> </targets> <!-- Define logging rules to control which logs go to which targets. Define Rules for Each Target --> <rules> <!-- Ignore logs from Microsoft and System namespaces by sending them to the "blackhole" target name="Microsoft.*": Microsoft and its related namespaces minlevel="Information": Minimum log level, capturing all logs from Information level upwards. writeTo="blackhole": Send logs to the blackhole target to ignore them final="true": Stop further processing of logs that match this rule --> <logger name="Microsoft.*" minlevel="Information" writeTo="blackhole" final="true" /> <!-- Filter out logs from the "System" namespace by sending them to the "blackhole" target. This is similar to Microsoft namespace --> <logger name="System.*" minlevel="Trace" writeTo="blackhole" final="true" /> <!-- Send all logs with level Information or higher to the file target name="*": Apply this rule to all loggers or you can say all namespaces minlevel="Information": Minimum log level is Information writeTo="file": Send matching logs to the "file" target --> <logger name="*" minlevel="Information" writeTo="file" /> <!-- Send all logs with level Debug or higher to the console target name="*": Apply this rule to all loggers or you can say all namespaces minlevel="Debug": Minimum log level is Debug writeTo="console": Send matching logs to the "console" target --> <logger name="*" minlevel="Information" writeTo="console" /> </rules> </nlog>
Key Elements:
- targets: Define where logs are written (e.g., files, console).
- rules: Control which loggers write to which targets and at what levels.
- autoReload, internalLogLevel, throwConfigExceptions: Control NLog’s internal behavior, including automatic configuration reload, internal logging verbosity, and whether exceptions in the configuration cause application errors
Copying NLog.config to the Output Directory
Make sure that the NLog.config file is available at runtime by setting it to copy to the output directory. You can set this in the file properties within Visual Studio or modify the .csproj file to include the following statements:
<ItemGroup> <None Include="NLog.config"> <CopyToOutputDirectory>Always</CopyToOutputDirectory> </None> </ItemGroup>
This step is essential because the runtime needs access to the NLog.config file to apply the logging configuration. The above code snippet always copies the NLog.config file to the output directory whenever the project is built. This is important for applications that depend on external configuration at runtime.
Configure NLog as Default Logging Provider
Next, we need to modify our Program.cs class file to use NLog as the default ASP.NET Core logging provider. So, please modify the Program class as follows. The following code is self-explained, so please read the comment lines for a better understanding.
using NLog.Web; namespace LoggingDemo { public class Program { public static void Main(string[] args) { // Create the WebApplication builder. var builder = WebApplication.CreateBuilder(args); // Add services to the DI container. builder.Services.AddControllers(); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); // Remove default logging providers to avoid duplicate logs. // Clears built-in providers (console, debug, etc.) builder.Logging.ClearProviders(); // Set NLog as the logging provider for the application. // Configures the host to use NLog builder.Host.UseNLog(); // Build the WebApplication. var app = builder.Build(); // Configure the HTTP request pipeline. if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(); } app.UseHttpsRedirection(); app.UseAuthorization(); app.MapControllers(); // Run the application. app.Run(); } } }
What does this code do?
- ClearProviders(): Removes the default logging providers (e.g., console, debug) to ensure only NLog is used.
- UseNLog(): Integrates NLog as the primary logging provider, making all logging calls in your application use NLog under the hood.
Using NLog for Logging in Controllers
With NLog configured, we can inject the ILogger<T> service into our controllers or services to log messages. So, create an API Empty Controller named TestController within the Controllers folder and copy and paste the following code. The ASP.NET Core dependency injection container automatically provides an ILogger<T> instance. The following Test Controller demonstrates logging at various levels (Trace, Debug, Information, Warning, Error, and Critical)
using Microsoft.AspNetCore.Mvc; namespace LoggingDemo.Controllers { [Route("api/[controller]")] [ApiController] public class TestController : ControllerBase { // Dependency injection of ILogger for this controller. private readonly ILogger<TestController> _logger; // Constructor injection for ILogger<T> public TestController(ILogger<TestController> logger) { _logger = logger; } [HttpGet("all-logs")] public IActionResult LogAllLevels() { // Log at Trace level (for debugging) _logger.LogTrace("LogTrace: Entering the LogAllLevels endpoint with Trace-level logging."); // Logging a calculated variable at Trace level. int calculation = 5 * 10; _logger.LogTrace("LogTrace: Calculation value is {calculation}", calculation); // Debug level logs help in debugging the application flow. _logger.LogDebug("LogDebug: Initializing debug-level logs for debugging purposes."); // Logging a structured object at Debug level. var debugInfo = new { Action = "LogAllLevels", Status = "Debugging" }; _logger.LogDebug("LogDebug: Debug information: {@debugInfo}", debugInfo); // Information level logs for successful operations. _logger.LogInformation("LogInformation: The LogAllLevels endpoint was reached successfully."); // Warning log if a potential issue is detected. bool resourceLimitApproaching = true; if (resourceLimitApproaching) { _logger.LogWarning("LogWarning: Resource usage is nearing the limit. Action may be required soon."); } // Error and Exception logging. try { // Simulate an error scenario. int x = 0; int result = 10 / x; // This will throw an exception. } catch (Exception ex) { // Error-level logging that captures the exception details. _logger.LogError(ex, "LogError: An error occurred while processing the request."); } // Critical-level log for severe failures. bool criticalFailure = true; if (criticalFailure) { _logger.LogCritical("LogCritical: A critical system failure has been detected. Immediate attention is required."); } return Ok("All logging levels demonstrated in this endpoint."); } } }
Run and Test
After configuring NLog and updating your controllers, run the application and verify that logs are being written to the configured targets (e.g., console, files). Check the log file paths you set in NLog.config to ensure the logs appear as expected. In the Console Window, you will see the following:
Now, verify the Logs folder, and with the nlog-all-current date text file should be created, and you should see the following log messages:
What is Structured Logging, and how can we implement it with NLog in ASP.NET Core?
Structured logging is an approach that captures log messages as structured data (e.g., JSON objects) rather than plain text. Each log entry is stored as key-value pairs, making filtering, searching, and analyzing logs easier. Structured logging is particularly useful when logs need to be handled by log management systems or when searching logs for specific data points.
Let’s see how to log-structured data from the controller and services. Let’s assume we are building an API to manage books in a library.
Creating Book Model:
First, add a folder named Models in the Project root directory. Then, add a class file named Book.cs within the Models folder and copy and paste the following code. This model represents the data structure you may want to log as structured information.
namespace LoggingDemo.Models { public class Book { public int Id { get; set; } // Unique identifier for the book public string Title { get; set; } // Book title public string Author { get; set; } // Book author public int YearPublished { get; set; } // Year the book was published } }
Create a Controller to Log-Structured Data
Next, create an API Empty Controller named BooksController within the Controllers folder and then copy and paste the following code, which adds actions to add and retrieve books. Here, we will log-structured data about the operations being performed. Using the @ operator in the logging calls tells NLog to log the objects as structured data (i.e., in JSON format) rather than converting them to a string.
using LoggingDemo.Models; using Microsoft.AspNetCore.Mvc; namespace LoggingDemo.Controllers { [Route("api/[controller]")] [ApiController] public class BooksController : ControllerBase { // In-memory storage for demonstration purposes. private static readonly List<Book> Books = new List<Book>() { new Book() { Id = 1001, Title = "ASP.NET Core", Author = "Pranaya", YearPublished = 2019 }, new Book() { Id = 1002, Title = "SQL Server", Author = "Pranaya", YearPublished = 2022 } }; private readonly ILogger<BooksController> _logger; public BooksController(ILogger<BooksController> logger) { _logger = logger; } // POST action to add a new book. [HttpPost] public IActionResult AddBook([FromBody] Book book) { Books.Add(book); // Log the book object as structured data using the "@" operator. _logger.LogInformation("Added a new book {@Book}", book); return Ok(); } // GET action to retrieve all books. [HttpGet] public IActionResult GetBooks() { // Log all books as structured data. _logger.LogInformation("Retrieved all books. Books: {@Books}", Books); return Ok(Books); } } }
Note: The @ symbol before the object (e.g., {@Book}) instructs NLog to serialize the object into structured data (typically JSON). This allows tools and log aggregators to parse and query log properties easily.
Testing and Observing Structured Logs
Run your application and use a tool like Swagger, Postman, or Fiddler to test the API endpoints. For example, to add a new book, you could POST to https://localhost:7234/api/books with a JSON body:
{ "id": 1003, "title": "C#.NET", "author": "Pranaya", "yearPublished": 2023 }
Once you access the API with the above request body, you can verify the Console window, and it should display the following message:
Now, if you open the txt file, then you will also see the same log as shown in the below image:
Similarly, you can also check the other GET Endpoint and observe the logs.
NLOG with Asynchronous Logging in ASP.NET Core Web API
Asynchronous logging helps improve application performance by offloading the log-writing tasks to a separate background thread. This is particularly beneficial for high-volume logging scenarios where synchronous logging could slow down the main application thread. Using asynchronous logging, our application can continue processing without waiting for each log message to be written.
Enabling Asynchronous Logging for File Target
To enable asynchronous logging with NLog, we need to wrap our targets within the AsyncWrapper target. This AsyncWrapper buffers log events and writes them asynchronously, thereby improving our application’s responsiveness. By wrapping the File target within an AsyncWrapper, we enable asynchronous logging to that file target. So, please modify the NLog.config file as follows.
<?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" internalLogLevel="Warn" internalLogFile="internal-nlog.txt" throwConfigExceptions="true"> <targets> <!-- AsyncWrapper: Buffers log events and writes them asynchronously --> <target xsi:type="AsyncWrapper" name="asyncFileWrapper"> <!-- Wrapped File target configuration --> <target xsi:type="File" name="file" fileName="D:/Logs/nlog-all-${shortdate}.log" layout="${longdate}|${level:uppercase=true}|${logger}|${message} ${exception:format=ToString}" /> </target> <!-- Console target remains synchronous as it is fast --> <target xsi:type="Console" name="console" layout="${longdate}|${level:uppercase=true}|${logger}|${message} ${exception:format=ToString}" /> <!-- Null target: Discard logs, used to filter out logs from specific namespaces --> <target xsi:type="Null" name="blackhole" /> </targets> <!-- Define logging rules to control which logs go to which targets --> <rules> <!-- Filter out logs from the "Microsoft" and "System" namespaces by sending them to the "blackhole" target --> <logger name="Microsoft.*" minlevel="Trace" writeTo="blackhole" final="true" /> <logger name="System.*" minlevel="Trace" writeTo="blackhole" final="true" /> <!-- Send all logs with level Information or higher to the file target --> <logger name="*" minlevel="Information" writeTo="file" /> <!-- Send all logs with level Information or higher to the console target --> <logger name="*" minlevel="Information" writeTo="console" /> </rules> </nlog>
Note: The AsyncWrapper improves performance by buffering and processing logs asynchronously. The File Target is wrapped within the AsyncWrapper to benefit from asynchronous writes.
Managing Log File Retention (Removing Older Files)
To prevent log files from consuming too much disk space, we can configure NLog to archive logs at specific intervals (e.g., daily, Hourly, etc., and also based on the file size). In NLog, Archiving is the process of closing the current active log file and renaming or moving it to a separate location (the archive folder) based on a defined schedule (daily, hourly, etc., or based on the file size).
Configuring Log Archiving in NLog.config
To automatically archive older log files in NLog, we need to configure the File target with the archiveEvery, archiveFileName, archiveNumbering, and maxArchiveFiles options. For a better understanding, please modify the NLog.config file as follows. The following configuration archives logs daily and retains only the last seven archived files.
<?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" internalLogLevel="Warn" internalLogFile="internal-nlog.txt" throwConfigExceptions="true"> <targets> <!-- AsyncWrapper: Buffers log events and writes them asynchronously --> <target xsi:type="AsyncWrapper" name="asyncFileWrapper"> <!-- Wrapped File target configuration --> <target xsi:type="File" name="file" fileName="D:/Logs/nlog-all-${shortdate}.log" archiveFileName="D:/Logs/archived/log_{#}.log" archiveEvery="Day" archiveNumbering="Rolling" archiveAboveSize="10485760" maxArchiveFiles="7" layout="${longdate}|${level:uppercase=true}|${logger}| ${message} ${exception:format=ToString}" /> </target> <!-- Console target remains synchronous as it is fast --> <target xsi:type="Console" name="console" layout="${longdate}|${level:uppercase=true}|${logger}|${message} ${exception:format=ToString}" /> <!-- Null target: Discard logs, used to filter out logs from specific namespaces --> <target xsi:type="Null" name="blackhole" /> </targets> <!-- Define logging rules to control which logs go to which targets --> <rules> <!-- Filter out logs from the "Microsoft" and "System" namespaces by sending them to the "blackhole" target --> <logger name="Microsoft.*" minlevel="Trace" writeTo="blackhole" final="true" /> <logger name="System.*" minlevel="Trace" writeTo="blackhole" final="true" /> <!-- Send all logs with level Information or higher to the file target --> <logger name="*" minlevel="Information" writeTo="file" /> <!-- Send all logs with level Information or higher to the console target --> <logger name="*" minlevel="Information" writeTo="console" /> </rules> </nlog>
How It Works:
Please consider the following key settings in the NLog configuration file for File Target.
Let us understand how it works internally.
Active Log File (fileName):
fileName=”D:/Logs/nlog-all-${shortdate}.log”: Here, ${shortdate} dynamically appends the current date to your log file name. That means you get a unique log file for each day in your primary Logs folder (e.g., nlog-all-2025-02-24.log for today). At any moment, your Logs folder holds the active log file for the current day.
Archive Log File (archiveFileName):
archiveFileName=”D:/Logs/archived/log_{#}.log”: When the active log file is archived, it’s moved (or renamed) into the Logs/archived folder. The {#} placeholder is replaced with a sequential number. As a result, we end up with archived log files named something like log_1.log, log_2.log, etc.
When Archiving Occurs:
Two conditions we have specified:
By Size: If the active log file grows larger than 10 MB, it is immediately archived (moved/renamed to the archive folder) and a new file is started. The following is the configuration.
- archiveAboveSize=”10485760″: When the log file exceeds 10 MB (10 × 1024 × 1024 bytes), NLog will roll over the current log file and start a new one. This triggers archiving based on file size.
By Time: If the day changes (even if the file is under 10 MB), the active log file is archived at midnight. The following is the configuration.
- archiveEvery=”Day”: This setting tells NLog to archive the active log file at the end of each day. At midnight (or when the day changes), the current active log file is closed and moved to the archive folder with a new sequential name.
Archive Numbering:
archiveNumbering=”Rolling”: The rolling numbering means each new archive file gets the next sequential number. When the maximum count is reached, the oldest archive file is deleted to make room for the new one.
Archive Retention:
maxArchiveFiles=”7″: This limits the number of archived log files to 7. In the Logs/archived folder, you will always have up to 7 archived files. Once there are seven archives, when a new day’s log is archived, the oldest file (e.g., the one from 8 days ago) will be deleted automatically.
At any given point:
- Active Log File: There is one current log file in your main Logs folder (e.g., D:/Logs/nlog-all-<current-date>.log).
- Archived Log Files: There are up to 7 archived log files in your Logs/archived folder, representing the previous days’ logs. When a new archive is created, if there are already seven archived files, the oldest one is automatically deleted. You can configure this value as per your requirements.
That’s it. We have discussed the essential aspects of integrating and using NLog in an ASP.NET Core Web API Application, including configuration, structured logging, asynchronous logging, and log file management. So, NLog provides a robust, structured, and asynchronous logging solution for ASP.NET Core Web API applications. It supports multi-target logging, log retention, and performance optimization with async wrappers.
In the next article, I will discuss how to log into an SQL Server Database with NLog in an ASP.NET Core Web API Application. In this article, I explain how to implement logging using nLog in the ASP.NET Core Web API Application, along with examples. I hope you enjoy this article, How to Implement Logging using nLog in ASP.NET Core Web API Application.