Exception Logging in ASP.NET Core: Your Guide to Reliable Error Tracking

Introduction: Exception Logging in Asp.Net core

Ever get the sense that you’re flying blind when your ASP.NET Core application runs into an issue in production? That’s where solid exception logging saves the day as your superhero! Consider logging as your application’s personal journal, carefully noting down every significant event, error, and interaction. It’s having a 24/7 surveillance camera installed over your codebase, providing you with the “special glasses” to actually see what your application is doing in the background.

In today’s software world, bugs are not “if” but “when.” A good error-handling approach is not only about correcting bugs; it is about making your users satisfied and your debugging effective. Logging is not only for debugging following a crash; it is an active tool to monitor your system’s health, user activity, and performance patterns. It’s an essential component of the “Holy Trinity of Application Observability,” and it functions in tandem with tracing and metrics to provide you with a comprehensive view.

By the end of this blog, you’ll have the knowledge and code snippets needed to add best-practice exception logging to your ASP.NET Core projects, making them more robust, easier to maintain, and a pleasure to debug. Let’s get started!

1. Getting Started: Built in Exception Logging in Asp.Net core

ASP.NET Core has a robust, extensible logging system built in. It’s built to be highly pluggable, so you can quickly change up your logging tools without having to recode your whole application.

The ILogger Abstraction: Your Logging Interface

At the core of ASP.NET Core’s logging is the ILogger interface, found in the Microsoft.Extensions.Logging.Abstractions namespace. This is super important because it means your application code doesn’t need to know which logging library (like Serilog or NLog) you’re using. You write your log messages against ILogger, and the underlying framework handles the rest. This flexibility is a game-changer if you ever decide to switch logging providers down the line!  

You’ll typically get an ILogger<TCategoryName> object injected into your classes using Dependency Injection (DI). The TCategoryName usually matches the class where you’re logging (e.g., ILogger<HomeController>). This helps organize your logs and makes it easy to see where each message came from.  

Example: Basic ILogger Usage in a Controller

using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging; // Don't forget this!

namespace MyWebApp.Controllers
{
    public class HomeController : Controller
    {
        private readonly ILogger<HomeController> _logger;

        // Inject ILogger via constructor
        public HomeController(ILogger<HomeController> logger)
        {
            _logger = logger;
        }

        public IActionResult Index()
        {
            _logger.LogInformation("Homepage visited at {DateTimeUtc}", DateTime.UtcNow); // Log an informational message
            return View();
        }

        public IActionResult Privacy()
        {
            try
            {
                // Simulate an error
                throw new InvalidOperationException("Something went wrong in Privacy page!");
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "An unexpected error occurred in Privacy page."); // Log an exception
            }
            return View();
        }
    }
}

Built-in Logging Providers: Where Do Logs Go?

When you add a new ASP.NET Core web application, it comes with a number of default logging providers automatically. These providers dictate where your log messages go. 

Here are the common ones:

  • Console Provider: Your logs appear directly in your console window. Super convenient for development!
  • Debug Provider: Logs are directed to the System.Diagnostics.Debug output, accessible when you have a debugger hooked up.  
  • EventSource Provider: Outputs logs into a cross-platform event source, valuable for advanced diagnostics.  
  • EventLog Provider: (Windows only) Outputs to Windows Event Log. It will, by default, only log messages of level Warning and above unless configured otherwise.

It’s also possible to add more advanced providers such as AzureAppServicesFile, AzureAppServicesBlob, or ApplicationInsights by adding their NuGet packages.  

Configuring Logging: appsettings.json and Log Levels

You’ll typically set your logging options in appsettings.json files. These might be environment-specific (such as appsettings.Development.json or appsettings.Production.json) to allow you to have fine-grained control over verbosity of logging in varying environments.

The LogLevel property is important here. It determines the minimum severity level at which messages get logged. If you set it to Warning, you’ll be seeing Warning, Error, and Critical messages but not Information, Debug, or Trace.

Example: appsettings.json Configuration

{
  "Logging": {
    "LogLevel": {
      "Default": "Information", // Default for all categories and providers
      "Microsoft": "Warning",   // Microsoft-related logs (e.g., ASP.NET Core internals)
      "Microsoft.Hosting.Lifetime": "Information"
    },
    "Debug": { // Specific settings for the Debug provider
      "LogLevel": {
        "Default": "Information" // Overrides the global "Default" for Debug provider
      }
    },
    "EventLog": { // Specific settings for the EventLog provider (Windows only)
      "LogLevel": {
        "Default": "Warning" // EventLog defaults to Warning if not specified [11]
      }
    }
  },
  "AllowedHosts": "*"
}

Understanding Log Levels: When to Use What

Choosing the right log level is crucial for effective error tracking and avoiding log “noise.”

Pro Tip: During production, restrict Trace, Debug, and Information levels to a certain category or output them into low-cost storage. Excessive verbose logging in production will require high-cost storage, impact performance, and even pose a security threat by exposing too much information.

2. Handling Exceptions Gracefully with Middleware

Exception handling middleware is your application’s safety net. It catches unhandled exceptions in the HTTP pipeline, ensuring your users don’t see ugly error screens and that you get the information you need to fix things.

Development Environment: The DeveloperExceptionPage

During development, you want the dirty details when something does break. The DeveloperExceptionPageMiddleware is the way to go! It displays it all: stack traces, query parameters, cookies, headers, and more. It’s turned on by default in new ASP.NET Core projects.

Critical Security Warning: NEVER turn on DeveloperExceptionPage() in production. Leaking that level of detail is a huge security risk because it can expose sensitive system information to potential attackers.

Example: Program.cs for Development Error Handling

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddControllersWithViews();

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseDeveloperExceptionPage(); // Shows detailed errors in development [3, 13]
}
else
{
    // Production error handling will go here
}

app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");

app.Run();

Production Environment: UseExceptionHandler() for a Smooth User Experience

During production, you’d like to present a friendly, generic error page to your users, without exposing internal application information. That’s where UseExceptionHandler comes in handy. It intercepts unhandled exceptions, logs them, and then re-executes the request to an error path of your choice (such as /Error).

Key Point: If your request has already begun sending the HTTP response headers, UseExceptionHandler cannot replay the request or alter the status code. The response will have to complete or the connection will be aborted. This is to say that for streaming responses, you must have try-catch blocks more towards the source.

Example: Program.cs for Production Error Handling

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddControllersWithViews();

var app = builder.Build();

// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment()) // Only for non-development environments
{
    app.UseExceptionHandler("/Home/Error"); // Redirects to a custom error page [3, 13, 14]
    // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");

app.Run();

Creating Your Custom Error Page (e.g., HomeController.cs and Error.cshtml)

In your HomeController, you’d have an Error action:

using Microsoft.AspNetCore.Diagnostics; // Important for IExceptionHandlerPathFeature
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;

namespace MyWebApp.Controllers
{
    public class HomeController : Controller
    {
        private readonly ILogger<HomeController> _logger;

        public HomeController(ILogger<HomeController> logger)
        {
            _logger = logger;
        }

       
        // Ensure this route matches UseExceptionHandler path
        public IActionResult Error()
        {
            var exceptionHandlerPathFeature = HttpContext.Features.Get<IExceptionHandlerPathFeature>();

            if (exceptionHandlerPathFeature!= null)
            {
                // Log the full exception details internally
                _logger.LogError(exceptionHandlerPathFeature.Error,
                                 "An unhandled exception occurred at path: {Path}",
                                 exceptionHandlerPathFeature.Path);

                // You can customize the message based on exception type if needed
                ViewBag.ErrorMessage = "Oops! Something went wrong on our end. We're working to fix it.";

                // Example: Specific message for a known error type
                if (exceptionHandlerPathFeature.Error is FileNotFoundException)
                {
                    ViewBag.ErrorMessage = "Sorry, the file you were looking for wasn't found.";
                }
            }

            // Return a generic error view to the user
            return View();
        }
    }
}

And your Views/Home/Error.cshtml would be a simple, user-friendly page:

@{
    ViewData = "Error";
}

<h1 class="text-danger">Error.</h1>
<h2 class="text-danger">@ViewBag.ErrorMessage</h2>

@* Optionally, show RequestId for support, but avoid sensitive details *@
@if (ViewBag.RequestId!= null)
{
    <p>
        <strong>Request ID:</strong> <code>@ViewBag.RequestId</code>
    </p>
}

<h3>What happened?</h3>
<p>
    We're really sorry, but something unexpected happened while processing your request.
    Our team has been notified and is looking into it.
</p>
<p>
    Please try refreshing the page or navigating back to the <a href="/">homepage</a>.
</p>

Exception Handler Lambda: Quick and Direct Error Handling

For more straightforward situations, or if you have to deal with errors explicitly within the middleware pipeline without a redirect of an entire page, you may pass a lambda expression to UseExceptionHandler. This allows you to access the details of the exception immediately.

Example: Lambda for Exception Handling in Program.cs

var app = builder.Build();
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler(exceptionHandlerApp =>
    {
        exceptionHandlerApp.Run(async context =>
        {
            context.Response.StatusCode = StatusCodes.Status500InternalServerError;
            context.Response.ContentType = System.Net.Mime.MediaTypeNames.Text.Plain; // Set content type
            await context.Response.WriteAsync("An unexpected error occurred.");

            var exceptionHandlerPathFeature = context.Features.Get<IExceptionHandlerPathFeature>();
            if (exceptionHandlerPathFeature?.Error!= null)
            {
                // Log the exception details here using your ILogger
                // For brevity, we're just writing a simple message to the response
                await context.Response.WriteAsync($"\nError details: {exceptionHandlerPathFeature.Error.Message}");
                // In a real app, you'd log this to your logging system, not expose it to the client!
            }
        });
    });
    app.UseHsts();
}

3. Level Up Your Logs: Structured Logging

Classic logging tends to deliver plain text messages that are difficult to search and analyze. Structured logging turns your logs into machine-readable data, typically JSON. A giant leap forward for exception tracking!

Why Structured Logging is a Game-Changer

Picture trying to locate all errors pertaining to a particular user in a vast ocean of plaintext logs. Nightmare! Structured logging simplifies this by providing context and making your logs searchable data.  

Here’s why it’s superior:

  • Easier Search & Analysis: You can search your logs like a database, querying by specific properties (e.g., UserId, OrderId, ExceptionType).
  • Richer Context: Each log message can contain more, contextually relevant information, painting a better picture of what occurred, when, and why.
  • Better Integration: Machine-readable logs are a cinch to integrate with analysis tools, dashboards, and alerting systems.  

Serilog: The Structured Logging Powerhouse

The Powerhouse of Structured Logging Serilog is perhaps the most widely used third-party logging library for .NET, and it’s a leader in structured logging.

Installation and Setup:

First, add the Serilog.AspNetCore NuGet package to your project:

dotnet add package Serilog.AspNetCore

Next, set up Serilog in your Program.cs. It’s usual to remove ASP.NET Core’s default providers to let Serilog have full reign.
 
Example: Basic Serilog Setup in Program.cs

using Serilog; // Don't forget this!

var builder = WebApplication.CreateBuilder(args);

// Configure Serilog to read from appsettings.json
Log.Logger = new LoggerConfiguration()
   .ReadFrom.Configuration(builder.Configuration) // Reads Serilog settings from appsettings.json [17, 16, 18]
   .Enrich.FromLogContext() // Adds contextual information like method name, user, etc. [17, 18]
   .WriteTo.Console() // Output logs to console [17, 18]
   .CreateLogger();

builder.Logging.ClearProviders(); // Remove ASP.NET Core's default loggers [1, 17]
builder.Host.UseSerilog(); // Tell ASP.NET Core to use Serilog [1, 18, 15]

// Add services to the container.
builder.Services.AddControllersWithViews();

var app = builder.Build();

//... (rest of your app configuration, e.g., UseExceptionHandler)

// Ensure all logs are flushed before the application exits
app.Lifetime.ApplicationStopped.Register(Log.CloseAndFlush); [23]

app.Run();

Example: appSetting.json for Serilog

{
  "Serilog": {
    "MinimumLevel": {
      "Default": "Information",
      "Override": {
        "Microsoft": "Warning",
        "System": "Warning"
      }
    },
    "WriteTo": [
      {
        "Name": "Console" // Console sink [17, 16]
      },
      {
        "Name": "File", // File sink [17, 16]
        "Args": {
          "path": "logs/myapp-.log", // Log file path
          "rollingInterval": "Day", // New file each day
          "outputTemplate": "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} {CorrelationId} {Level:u3} {Username} {Message:lj}{NewLine}{Exception}" // Human-readable format [17]
        }
      }
    ],
    "Enrich": [
      "FromLogContext", // Adds context from LogContext [17, 18]
      "WithMachineName",
      "WithProcessId",
      "WithThreadId"
    ]
  },
  "Logging": { // Keep this for other ASP.NET Core logging if needed, but Serilog overrides it
    "LogLevel": {
      "Default": "Information"
    }
  },
  "AllowedHosts": "*"
}

Logging with Serilog: Message Templates and Structured Data

Serilog employs a special “message template” syntax with named parameters (e.g., {UserName}). Rather than merely formatting a string, Serilog inspects the values of these parameters as separate properties in your structured log event. This is the secret to searchable logs!

To log a whole complex object as structured data, use the @ symbol (e.g., {@UserObject}).

Example: Structured Logging with Serilog

using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Serilog.Context; // For LogContext

namespace MyWebApp.Controllers
{
    public class ProductController : Controller
    {
        private readonly ILogger<ProductController> _logger;

        public ProductController(ILogger<ProductController> logger)
        {
            _logger = logger;
        }

        public IActionResult GetProduct(int id)
        {
            // Example of adding contextual data to the log scope
            using (LogContext.PushProperty("ProductId", id))
            {
                _logger.LogInformation("Attempting to retrieve product with ID: {ProductId}", id);

                try
                {
                    // Simulate fetching product data
                    if (id <= 0)
                    {
                        throw new ArgumentException("Product ID must be positive.");
                    }
                    var product = new { Id = id, Name = $"Product {id}", Price = 10.0m * id };

                    // Log the entire product object as structured data
                    _logger.LogInformation("Successfully retrieved product: {@Product}", product);
                    return Ok(product);
                }
                catch (Exception ex)
                {
                    // Log the exception with structured context
                    _logger.LogError(ex, "Error retrieving product with ID: {ProductId}. User: {UserIdentifier}", id, User?.Identity?.Name?? "Anonymous");
                    return StatusCode(500, "Internal Server Error");
                }
            }
        }
    }
}

When this logs, you’ll get something like this in your structured log (e.g., JSON):

{
  "Timestamp": "2025-05-28T10:30:00.123Z",
  "Level": "Information",
  "MessageTemplate": "Successfully retrieved product: {@Product}",
  "Properties": {
    "ProductId": 1,
    "Product": {
      "Id": 1,
      "Name": "Product 1",
      "Price": 10.0
    },
    "SourceContext": "MyWebApp.Controllers.ProductController",
    //... other enriched properties
  }
}

NLog: A Robust Alternative

NLog is yet another great, high-performance, open-source .NET logging framework. It also supports structured logging in full and provides a whole lot of flexibility.  

Installation and Setup:

Add the NLog.Web.AspNetCore NuGet package:

dotnet add package NLog.Web.AspNetCore

NLog is often configured using an nlog.config XML file at your project’s root, but it can also read from appsettings.json.  

Example: Basic NLog Setup in Program.cs

using NLog.Web; // Don't forget this!

var builder = WebApplication.CreateBuilder(args);

// Configure NLog
builder.Logging.ClearProviders(); // Clear default providers
builder.Host.UseNLog(); // Use NLog as the logging provider [18, 26, 27]

// Add services to the container.
builder.Services.AddControllersWithViews();

var app = builder.Build();

//... (rest of your app configuration)

// Ensure NLog is shut down on application exit
app.Lifetime.ApplicationStopped.Register(NLog.LogManager.Shutdown); [26]

app.Run();

Example: nlog.config (XML)

Create an nlog.config file in your project root and ensure its “Copy to Output Directory” property is set to “Copy Always” or “Copy if newer.”

<?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="Info"
      internalLogFile="logs/internal-nlog.txt">

  <targets>
    <target xsi:type="ColoredConsole"
            name="logconsole"
            layout="${longdate}|${level:uppercase=true}|${logger}|${message} ${exception:format=tostring}" />

    <target xsi:type="File"
            name="logfile"
            fileName="${basedir}/logs/myapp-${shortdate}.log"
            layout="${longdate}|${level:uppercase=true}|${logger}|${message} ${exception:format=tostring,Data}"
            archiveFileName="${basedir}/archives/myapp-{#}.log"
            archiveEvery="Day"
            archiveNumbering="DateAndSequence"
            maxArchiveFiles="7"
            keepFileOpen="false" />
  </targets>

  <rules>
    <logger name="*" minlevel="Debug" writeTo="logconsole" />
    <logger name="Microsoft.*" minlevel="Information" writeTo="logfile" />
    <logger name="*" minlevel="Information" writeTo="logfile" />
  </rules>
</nlog>

Example: Structured Logging with NLog

NLog also supports structured logging through message templates and custom properties

using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging; // Use Microsoft's ILogger abstraction
using NLog; // For NLog's static LogManager if needed, but ILogger is preferred

namespace MyWebApp.Controllers
{
    public class OrderController : Controller
    {
        private readonly ILogger<OrderController> _logger;

        public OrderController(ILogger<OrderController> logger)
        {
            _logger = logger;
        }

        [HttpPost]
        public IActionResult PlaceOrder( OrderModel order)
        {
            if (order == null)
            {
                _logger.LogWarning("Attempted to place a null order.");
                return BadRequest("Order cannot be null.");
            }

            try
            {
                // Simulate order processing
                if (order.Amount <= 0)
                {
                    throw new ArgumentOutOfRangeException(nameof(order.Amount), "Order amount must be positive.");
                }

                // Log structured data using ILogger
                _logger.LogInformation("Order placed successfully. OrderId: {OrderId}, CustomerId: {CustomerId}, Amount: {Amount}",
                                       order.OrderId, order.CustomerId, order.Amount);

                return Ok($"Order {order.OrderId} placed.");
            }
            catch (Exception ex)
            {
                // Log the exception with relevant order details
                _logger.LogError(ex, "Failed to place order {OrderId} for customer {CustomerId}. Amount: {Amount}",
                                 order.OrderId, order.CustomerId, order.Amount);
                return StatusCode(500, "Failed to process order.");
            }
        }
    }

    public class OrderModel
    {
        public string OrderId { get; set; } = Guid.NewGuid().ToString();
        public string CustomerId { get; set; } = string.Empty;
        public decimal Amount { get; set; }
    }
}

4. Centralized Logging: Seeing the Big Picture

In today’s world of microservices and distributed systems, logs scattered across different servers are a nightmare. Centralized logging is essential for effective monitoring, debugging, and gaining a holistic view of your application’s health.  

The Elastic Stack (ELK): Your Log Command Center

The Elastic Stack (formerly ELK) is a popular open-source suite for powerful log management. It consists of:

  • Elasticsearch: A super-fast search and analytics engine for storing and querying massive amounts of log data.  
  • Logstash: A flexible data processing pipeline that collects logs from various sources, transforms them (parsing, filtering, enriching), and sends them to Elasticsearch.  
  • Kibana: A fantastic visualization layer that lets you create interactive dashboards, query logs in real-time, and monitor trends.  

Together, ELK provides a robust, real-time solution for deep insights into application performance and error tracking.  

Integration Patterns with ASP.NET Core:

  1. Direct Serilog Sink: You can configure Serilog to send logs directly to Elasticsearch using the Serilog.Sinks.Elasticsearch NuGet package. This is simpler to set up.  
    • Consideration: This approach might offer lower guarantees on log delivery if your Elastic Stack isn’t highly available or experiences bursts of logs it can’t process quickly enough. Logs could be lost.  
    Example: Serilog appsettings.json for Elasticsearch Sink
{
  "Serilog": {
    "MinimumLevel": { "Default": "Information" },
    "WriteTo":,
    "Enrich":
  }
}

Filebeat/Logstash Agent-Based Approach: This is often preferred for higher reliability. You configure Serilog (or NLog) to write structured logs to local files (e.g., JSON format). Then, a lightweight agent like Filebeat collects these files and ships them to Logstash (for processing) or directly to Elasticsearch.  

  • Benefit: Filebeat is optimized for speed and low resource usage. It buffers logs, decoupling your application’s logging from the ingestion pipeline’s health, which means logs are less likely to be lost during network issues or backend unavailability.  

Example: Serilog appsettings.json for File Sink (for Filebeat)

{
  "Serilog": {
    "MinimumLevel": { "Default": "Information" },
    "WriteTo":,
    "Enrich":
  }
}

Example: Basic filebeat.yml Configuration (for Filebeat to pick up logs)

filebeat.inputs:
- type: log
  enabled: true
  paths:
    - C:\YourApp\logs\myapp-structured-*.json # Adjust path to your log files
  json.keys_under_root: true # Treat JSON fields as top-level fields
  json.add_error_key: true   # Add an error field if JSON parsing fails
  json.expand_keys: true     # Expand nested JSON objects

output.elasticsearch:
  hosts: ["localhost:9200"] # Your Elasticsearch host
  # username: "elastic"
  # password: "changeme"

Seq: Real-time Structured Log Server for.NET

Seq is a fantastic, web-based log server specifically designed for structured logs. It’s super easy to set up and provides powerful querying, alerts, and dashboards in real-time. It integrates seamlessly with Serilog.  

One cool feature of Seq is its dynamic log level control. You can adjust your application’s logging verbosity at runtime through Seq’s API keys, which is incredibly useful for troubleshooting live issues without redeploying.  

Example: Serilog appsettings.json for Seq Sink

{
  "Serilog": {
    "MinimumLevel": { "Default": "Information" },
    "WriteTo":,
    "Enrich":
  }
}

Azure Application Insights: Cloud-Native Observability

For applications hosted in Azure, Application Insights is a comprehensive observability platform. It automatically collects a wealth of telemetry data, including requests, dependencies, performance counters, and, of course, exceptions!  

Key Features for Exception Tracking:

  • Automatic Exception Collection: Application Insights automatically picks up unhandled exceptions from your ASP.NET Core app, giving you immediate visibility into critical errors.  
  • Smart Detection: This is a game-changer! Application Insights uses machine learning to automatically analyze your exceptions

For any query, contact us

Loggings in Asp.Net Core

Leave a Comment