Skip to content

KartikZCoding/ASP.NET-Core-Web-API-Logging

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

1 Commit
Β 
Β 

Repository files navigation

πŸ“ Logging in ASP.NET Core β€” A Complete Guide

This guide covers everything about Logging in ASP.NET Core β€” from the built-in logging system to third-party providers like Serilog and structured logging. Written in simple language so anyone can understand and explain it to others.


πŸ“‹ Table of Contents

  1. Logging in ASP.NET Core
  2. Logging Configuration in ASP.NET Core
  3. Logging to Files with Serilog in ASP.NET Core
  4. Structured Logging

1. Logging in ASP.NET Core

πŸ“Œ What is Logging?

Logging means recording what your application is doing while it runs. It's like a diary for your application β€” it writes down events, errors, warnings, and information so you can see what happened later.

Think of it like a CCTV camera πŸ“Ή: It records everything. If something goes wrong, you go back and watch the recording to find the problem. Logging does the same thing for your code.

πŸ€” Why Do We Need Logging?

Reason Explanation
Debugging Find where and why your code failed
Monitoring See how your app is performing in production
Auditing Track who did what and when
Error Tracking Get instant alerts when errors happen
Performance Analysis Find slow endpoints or database queries

🧩 Key Interfaces β€” The Building Blocks

ASP.NET Core provides a built-in logging system through the Microsoft.Extensions.Logging namespace. There are 3 main interfaces you need to know:


1️⃣ ILogger<T>

This is the main interface you use in your code to write log messages.

  • T is the category β€” usually the class name where you're logging.
  • It tells you which class produced the log message.
// The <RolesController> tells us that this log came from RolesController
public class RolesController : ControllerBase
{
    // Inject ILogger with the category of this class
    private readonly ILogger<RolesController> _logger;

    public RolesController(ILogger<RolesController> logger)
    {
        _logger = logger; // ASP.NET Core automatically provides this
    }

    [HttpGet("all")]
    public async Task<ActionResult> GetAllRoles()
    {
        // βœ… Log an information message
        _logger.LogInformation("GetAllRoles endpoint was called");

        // βœ… Log a warning
        _logger.LogWarning("This is a warning message");

        // βœ… Log an error
        _logger.LogError("Something went wrong!");

        return Ok();
    }
}

Output in Console:

info: EmpMS.Controllers.RolesController[0]
      GetAllRoles endpoint was called
warn: EmpMS.Controllers.RolesController[0]
      This is a warning message
fail: EmpMS.Controllers.RolesController[0]
      Something went wrong!

Notice EmpMS.Controllers.RolesController β€” that's the category from ILogger<RolesController>. It tells you exactly which class generated this log.


2️⃣ ILoggerProvider

This is the destination where your logs go. Each provider sends logs to a different place.

ASP.NET Core comes with these built-in providers:

Provider Where are logs sent? Good for
Console Terminal / Command Line window Development
Debug Visual Studio Debug Output window Debugging
EventLog Windows Event Viewer Windows Servers
EventSource ETW (Event Tracing for Windows) Advanced diagnostics

Each ILoggerProvider creates its own ILogger. So if you have 3 providers (Console, Debug, EventLog), your one log message gets sent to ALL 3 places!


3️⃣ ILoggerFactory

This is the manager/boss that controls everything. It:

  • Registers all the ILoggerProvider(s)
  • Creates ILogger instances
  • Distributes log messages to all registered providers

Think of a TV Broadcasting Station πŸ“‘: The ILoggerFactory is the station. Each ILoggerProvider is a TV channel (Console, Debug, EventLog). When someone speaks (logs a message), the station broadcasts it to ALL channels at the same time.


πŸ—οΈ How Logging Works β€” The Full Picture

Here's the complete flow of how logging works inside ASP.NET Core:

                  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                  β”‚         ILoggerFactory           β”‚  ← The Boss / Manager
                  β”‚  (Creates loggers, manages       β”‚
                  β”‚   all providers)                 β”‚
                  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                             β”‚
            β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
            β”‚                β”‚                β”‚
            β–Ό                β–Ό                β–Ό
   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
   β”‚ILoggerProviderβ”‚ β”‚ILoggerProviderβ”‚ β”‚ILoggerProviderβ”‚   ← Destinations
   β”‚   Console     β”‚ β”‚  EventLog    β”‚ β”‚    Debug      β”‚
   β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜
          β”‚                β”‚                β”‚
          β–Ό                β–Ό                β–Ό
   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
   β”‚   ILogger     β”‚ β”‚   ILogger    β”‚ β”‚   ILogger     β”‚   ← Each provider
   β”‚ (Console)     β”‚ β”‚ (EventLog)   β”‚ β”‚ (Debug)       β”‚      creates its
   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜      own ILogger

Step-by-step flow:

  1. You call _logger.LogInformation("User logged in") in your code.
  2. ILogger<T> sends this message to the ILoggerFactory.
  3. ILoggerFactory distributes the message to ALL registered ILoggerProviders.
  4. Each ILoggerProvider writes the message to its destination (Console, EventLog, Debug window, etc.).

⚠️ The Problem β€” Built-in Providers Are Not Enough for Production!

The built-in ILoggerProviders (Console, Debug, EventLog) are NOT enough for real production applications. Here's why:

Problem Explanation
No File Logging Built-in providers CAN'T write logs to files!
No Database Logging Can't save logs in a database for later analysis
No Cloud Logging Can't send logs to cloud services (Azure, AWS, etc.)
Limited Formatting Console output is basic β€” no JSON, no structured data
No Log Rotation Can't automatically create new log files daily or by size
No Centralized Logging Can't send logs to tools like Elasticsearch, Seq, Splunk

Console β€” only shows logs while the app is running. Once you close the window, logs are gone forever!

Debug β€” only works inside Visual Studio during debugging.

EventLog β€” only works on Windows, and it's hard to search through.

βœ… The Solution β€” Third-Party Logging Providers

That's why we use third-party logging providers that give us much more power:

Provider What It Does
Serilog Most popular! File, Console, JSON, Database, Seq, etc.
NLog Flexible, many targets (file, database, email, etc.)
Log4Net Classic logger from Java world, mature and stable

How do third-party providers work? They integrate with the ILoggerFactory! The factory manages the third-party provider just like it manages the built-in ones. So your code (ILogger<T>) doesn't change at all β€” you just register the new provider, and the factory does the rest.

                  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                  β”‚         ILoggerFactory           β”‚  ← Manages EVERYTHING
                  β”‚  (Built-in + Third-party)        β”‚
                  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                             β”‚
       β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
       β”‚                     β”‚                     β”‚
       β–Ό                     β–Ό                     β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚Built-in       β”‚   β”‚Built-in       β”‚   β”‚ 🌟 SERILOG       β”‚  ← Third-party
β”‚Console        β”‚   β”‚Debug          β”‚   β”‚ Provider         β”‚     provider
β”‚Provider       β”‚   β”‚Provider       β”‚   β”‚                  β”‚
β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜   β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜   β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜
       β”‚                  β”‚                     β”‚
       β–Ό                  β–Ό                     β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚   ILogger     β”‚   β”‚   ILogger    β”‚   β”‚    ILogger       β”‚
β”‚ (Console)     β”‚   β”‚ (Debug)      β”‚   β”‚  (Serilog)       β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                              β”‚
                                    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                                    β”‚         β”‚         β”‚
                                    β–Ό         β–Ό         β–Ό
                               πŸ“„ File   πŸ–₯️ Console  πŸ—ƒοΈ Database
                               (logs.txt) (formatted) (SQL/Seq)

Key Point: Your code NEVER changes! You always use ILogger<T>. The ILoggerFactory handles which providers are active. You just configure it once in Program.cs.


πŸ“Š Log Levels

Every log message has a level that tells you how important it is:

Level Value When to Use Example
Trace 0 Very detailed info, only for deep debugging "Entering method GetAllRoles"
Debug 1 Useful during development "Query returned 5 roles"
Information 2 General app flow, normal events "User logged in successfully"
Warning 3 Something unexpected but not an error "API response took 5 seconds"
Error 4 Something failed but app can continue "Failed to send email notification"
Critical 5 App is about to crash or is unusable "Database connection lost!"
None 6 Disables logging completely β€”
// How to use each log level in your code
_logger.LogTrace("Entering GetAllRoles method");            // Level 0
_logger.LogDebug("Query returned {Count} roles", count);     // Level 1
_logger.LogInformation("User {User} logged in", username);   // Level 2
_logger.LogWarning("API response slow: {Time}ms", time);     // Level 3
_logger.LogError("Failed to create role: {Error}", ex.Message); // Level 4
_logger.LogCritical("Database connection lost!");             // Level 5

Rule of thumb: In production, you usually set the minimum level to Warning or Information. You don't want Trace and Debug messages flooding your production logs!


2. Logging Configuration in ASP.NET Core

πŸ“Œ What is Logging Configuration?

By default, ILogger works without any configuration β€” it just logs everything to all registered providers. But in real applications, you want control over:

  • Which log levels are shown
  • Which providers receive which logs
  • Which categories (classes) are logged

Think of a volume control πŸŽ›οΈ: Without configuration, all speakers (providers) play at full volume. Configuration lets you turn down the volume on some speakers and mute others.

πŸ€” Why Do We Need Configuration?

Example scenario: You have the Console and EventLog providers active.

  • In the Console β€” you want to see ALL logs (Trace, Debug, Info, etc.) because you're developing.
  • In the EventLog (Windows Event Viewer) β€” you ONLY want to see Error and Warning because EventLog is for serious issues on production servers.

Without configuration, both providers show ALL logs. That's messy and makes EventLog useless β€” flooded with unnecessary messages.

βš™οΈ Configuring Logging using appsettings.json

ASP.NET Core reads logging configuration from the appsettings.json file. Here's how:

Default Configuration (Already in Your Project)

// appsettings.json
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  }
}

What this means:

Setting Meaning
"Default": "Information" Show logs at Information level and above (Info, Warning, Error, Critical) for all categories
"Microsoft.AspNetCore": "Warning" For ASP.NET Core internal stuff, only show Warning and above. This hides all the noisy framework logs!

Configuring Per Provider

You can set different log levels for different providers:

// appsettings.json
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    },
    "Console": {
      "LogLevel": {
        "Default": "Debug"
      }
    },
    "EventLog": {
      "LogLevel": {
        "Default": "Warning"
      }
    }
  }
}

What this means:

Provider Minimum Level What You'll See
Console Debug Debug, Information, Warning, Error, Critical
EventLog Warning Warning, Error, Critical ONLY
Others Information Falls back to the default LogLevel

How it works: Provider-specific settings override the default LogLevel. If a provider doesn't have its own section, it uses the global LogLevel settings.

Configuring Per Category (Class)

You can even control logging for specific classes or namespaces:

// appsettings.json
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning",
      "EmpMS.Controllers": "Debug",
      "EmpMS.Services.RoleService": "Trace"
    },
    "EventLog": {
      "LogLevel": {
        "Default": "Error"
      }
    }
  }
}

What this means:

Category Level Explanation
Default Information Everything else shows Info and above
Microsoft.AspNetCore Warning Framework logs β€” only Warning+
EmpMS.Controllers Debug All controllers β€” show Debug and above
EmpMS.Services.RoleService Trace RoleService specifically β€” show EVERYTHING

Category matching works like namespaces: EmpMS.Controllers matches EmpMS.Controllers.RolesController, EmpMS.Controllers.AuthController, etc. More specific categories override less specific ones.

πŸ“Š Configuration Hierarchy

The configuration follows this priority (most specific wins):

Most Specific ──────────────────────────────── Least Specific

Provider + Category  >  Provider Default  >  Global Category  >  Global Default

Example:
"Console": {                   "Console": {         "LogLevel": {        "LogLevel": {
  "LogLevel": {                  "LogLevel": {        "EmpMS.Controllers"   "Default"
    "EmpMS.Controllers"           "Default"           : "Debug"             : "Info"
    : "Trace"                     : "Warning"       }                     }
  }                              }
}                              }

πŸ”§ Environment-Specific Configuration

You can have different configurations for Development and Production:

appsettings.Development.json β€” Used when running locally:

{
  "Logging": {
    "LogLevel": {
      "Default": "Debug",
      "Microsoft.AspNetCore": "Information"
    }
  }
}

appsettings.Production.json β€” Used in production server:

{
  "Logging": {
    "LogLevel": {
      "Default": "Warning",
      "Microsoft.AspNetCore": "Error"
    }
  }
}

In Development: You see more logs (Debug+) to help you debug.

In Production: You only see important logs (Warning+) to keep it clean and performant.


3. Logging to Files with Serilog in ASP.NET Core

πŸ“Œ What is Serilog?

Serilog is the most popular third-party logging library for .NET. It lets you write logs to files, console, databases, cloud services, and much more β€” all with very little setup.

Think of Serilog like a Swiss Army Knife πŸ”ͺ: The built-in logging is a simple knife. Serilog gives you scissors, screwdriver, bottle opener, and 20 other tools β€” all in one package.

πŸ€” Why Use Serilog?

Feature Built-in Logging Serilog
Console Logging βœ… βœ… (with better formatting)
File Logging ❌ βœ…
JSON Output ❌ βœ…
Daily Log Files ❌ βœ… (automatic rolling)
Database Logging ❌ βœ… (SQL Server, PostgreSQL, etc.)
Cloud Logging ❌ βœ… (Seq, Elasticsearch, Azure, AWS)
Structured Logging Basic βœ… (First-class support)
Log File Size Limit ❌ βœ… (auto-delete old files)

πŸ“¦ Step 1: Install Dependencies

You need to install 3 NuGet packages:

# 1. Core Serilog package for ASP.NET Core integration
dotnet add package Serilog.AspNetCore

# 2. Serilog Console Sink β€” to write formatted logs to console
dotnet add package Serilog.Sinks.Console

# 3. Serilog File Sink β€” to write logs to files
dotnet add package Serilog.Sinks.File

What is a "Sink"? In Serilog, a Sink is a destination where logs are written. Console Sink = write to console. File Sink = write to files. You can have multiple sinks active at the same time!

βš™οΈ Step 2: Configure Serilog in Program.cs

There are two ways to configure Serilog β€” through code or through appsettings.json. Here's both:

Option A: Configure in Code (Program.cs)

// Program.cs
using Serilog;

var builder = WebApplication.CreateBuilder(args);

// βœ… Step 1: Configure Serilog
Log.Logger = new LoggerConfiguration()
    .MinimumLevel.Debug()                                // Minimum log level
    .WriteTo.Console()                                   // Sink 1: Write to Console
    .WriteTo.File(
        "Logs/app-log-.txt",                             // Sink 2: Write to File
        rollingInterval: RollingInterval.Day,            // Create new file every day
        retainedFileCountLimit: 30,                      // Keep only last 30 days of logs
        outputTemplate: "[{Timestamp:yyyy-MM-dd HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}"
    )
    .CreateLogger();

// βœ… Step 2: Tell ASP.NET Core to use Serilog instead of built-in logging
builder.Host.UseSerilog();

// ... rest of your services
builder.Services.AddControllers();

var app = builder.Build();

// βœ… Step 3: (Optional) Add Serilog request logging middleware
// This logs every HTTP request automatically (method, path, status code, time)
app.UseSerilogRequestLogging();

app.MapControllers();
app.Run();

What each configuration does:

Configuration What It Does
.MinimumLevel.Debug() Log everything from Debug level and above
.WriteTo.Console() Display logs in the terminal
.WriteTo.File("Logs/app-log-.txt") Write logs to a file in the Logs/ folder
rollingInterval: RollingInterval.Day Create a new file each day (e.g., app-log-20260225.txt)
retainedFileCountLimit: 30 Auto-delete log files older than 30 days
outputTemplate: "..." Custom format for each log line
builder.Host.UseSerilog() Replace built-in logging with Serilog
app.UseSerilogRequestLogging() Auto-log every HTTP request

Option B: Configure in appsettings.json

First, install one more package:

dotnet add package Serilog.Settings.Configuration

Then configure in appsettings.json:

// appsettings.json
{
  "Serilog": {
    "Using": ["Serilog.Sinks.Console", "Serilog.Sinks.File"],
    "MinimumLevel": {
      "Default": "Information",
      "Override": {
        "Microsoft.AspNetCore": "Warning",
        "System": "Warning"
      }
    },
    "WriteTo": [
      {
        "Name": "Console"
      },
      {
        "Name": "File",
        "Args": {
          "path": "Logs/app-log-.txt",
          "rollingInterval": "Day",
          "retainedFileCountLimit": 30,
          "outputTemplate": "[{Timestamp:yyyy-MM-dd HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}"
        }
      }
    ]
  }
}

And update Program.cs to read from config:

// Program.cs
using Serilog;

var builder = WebApplication.CreateBuilder(args);

// βœ… Read Serilog configuration from appsettings.json
Log.Logger = new LoggerConfiguration()
    .ReadFrom.Configuration(builder.Configuration)   // Read from appsettings.json
    .CreateLogger();

builder.Host.UseSerilog();

// ... rest of setup

Which option to use? Option B (appsettings.json) is better for production because you can change log settings without recompiling your code! Just update the JSON file and restart the app.

πŸ“‚ Step 3: What the Output Looks Like

Console Output (formatted by Serilog):

[2026-02-25 11:30:00 INF] Application starting up
[2026-02-25 11:30:01 INF] Now listening on: https://localhost:5001
[2026-02-25 11:30:05 INF] HTTP GET /api/roles/all responded 200 in 45.2ms
[2026-02-25 11:30:10 WRN] API response slow: 3200ms
[2026-02-25 11:30:15 ERR] Failed to create role: Role name already exists

File Output (Logs/app-log-20260225.txt):

[2026-02-25 11:30:00 INF] Application starting up
[2026-02-25 11:30:01 INF] Now listening on: https://localhost:5001
[2026-02-25 11:30:05 INF] HTTP GET /api/roles/all responded 200 in 45.2ms
[2026-02-25 11:30:10 WRN] API response slow: 3200ms
[2026-02-25 11:30:15 ERR] Failed to create role: Role name already exists

Folder structure after a few days:

Logs/
β”œβ”€β”€ app-log-20260223.txt    ← Day 1 logs
β”œβ”€β”€ app-log-20260224.txt    ← Day 2 logs
└── app-log-20260225.txt    ← Today's logs (current file)

Each day gets its own file automatically! And files older than 30 days get deleted automatically. No manual cleanup needed!

πŸ’‘ Using Serilog in Your Code

Your code doesn't change at all! You still use ILogger<T> β€” Serilog handles everything behind the scenes:

// Controllers/RolesController.cs
public class RolesController : ControllerBase
{
    // βœ… Same ILogger<T> β€” no Serilog-specific code here!
    private readonly ILogger<RolesController> _logger;

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

    [HttpPost("create")]
    public async Task<ActionResult<APIResponse>> CreateRole(RoleDto roleDto)
    {
        // βœ… Log information β€” goes to BOTH Console AND File automatically
        _logger.LogInformation("Creating role: {RoleName}", roleDto.RoleName);

        try
        {
            await _roleService.CreateRoleAsync(roleDto);

            // βœ… Log success
            _logger.LogInformation("Role {RoleName} created successfully", roleDto.RoleName);

            return Ok(_apiResponse);
        }
        catch (Exception ex)
        {
            // βœ… Log the error with exception details
            _logger.LogError(ex, "Failed to create role: {RoleName}", roleDto.RoleName);

            return BadRequest(_apiResponse);
        }
    }
}

4. Structured Logging

πŸ“Œ What is Structured Logging?

Structured logging means writing logs in a machine-readable format (like JSON) instead of plain text. Each log entry has named properties that can be searched, filtered, and analyzed.

Think of it like an Excel spreadsheet πŸ“Š vs a notepad πŸ“:

  • Unstructured log (notepad): "User Kartik logged in at 11:30 AM from 192.168.1.1"
  • Structured log (spreadsheet): Each piece of info is in its own column β€” Name: Kartik, Time: 11:30, IP: 192.168.1.1

πŸ€” The Problem with Unstructured Logs

Traditional text log files look like this:

[2026-02-25 11:30:00] User Kartik logged in from 192.168.1.1
[2026-02-25 11:31:00] User Aryan logged in from 10.0.0.5
[2026-02-25 11:32:00] User Kartik created role Admin
[2026-02-25 11:33:00] Failed to create role Manager - already exists
[2026-02-25 11:34:00] User Aryan logged out from 10.0.0.5

Now try to answer these questions:

Question Can You Easily Answer?
How many times did Kartik log in today? ❌ Hard β€” need to manually search through text
Which IP address had the most login attempts? ❌ Hard β€” need regex or manual counting
How many role creation failures happened this week? ❌ Hard β€” need to grep specific text patterns
What's the average response time per endpoint? ❌ Almost impossible from plain text

The problem is clear: Plain text logs are for humans to read, but NOT for machines to analyze. When you have millions of log lines, you can't read them manually!

βœ… The Solution β€” Structured Logging

With structured logging, the same logs look like this in JSON format:

{
  "Timestamp": "2026-02-25T11:30:00",
  "Level": "Information",
  "MessageTemplate": "User {UserName} logged in from {IpAddress}",
  "Properties": {
    "UserName": "Kartik",
    "IpAddress": "192.168.1.1",
    "SourceContext": "EmpMS.Controllers.AuthController"
  }
}

Now you can query this data:

  • "Show me all logs where UserName = Kartik" β†’ Easy!
  • "Count logs where Level = Error" β†’ Easy!
  • "Find all logs from AuthController" β†’ Easy!

πŸ€” Why is Structured Logging Useful?

Benefit Explanation
Machine Readable Tools like Seq, Elasticsearch can parse and index logs
Searchable Search by any property (user, IP, error type, etc.)
Filterable Filter logs by time, level, user, endpoint, etc.
Analytics Create dashboards, charts, and alerts from log data
Debugging Quickly find exactly what you need from millions of logs
Correlation Track a single request across multiple services

πŸ’» How to Do Structured Logging in ASP.NET Core

The good news β€” Microsoft.Extensions.Logging already supports structured logging! And Serilog has first-class support for it. You just need to follow some simple conventions.

βœ… The Rules / Conventions

Rule 1: Use Message Templates with Named Placeholders (NOT string interpolation)

// ❌ BAD β€” String interpolation β€” produces UNSTRUCTURED text
_logger.LogInformation($"User {username} logged in from {ipAddress}");
// Output: "User Kartik logged in from 192.168.1.1"
// Problem: The values are baked into the string. Machines can't extract them!

// βœ… GOOD β€” Message template with named placeholders β€” produces STRUCTURED data
_logger.LogInformation("User {UserName} logged in from {IpAddress}", username, ipAddress);
// Output includes: Properties: { "UserName": "Kartik", "IpAddress": "192.168.1.1" }
// Benefit: Each value is stored as a searchable, indexed property!

The {UserName} inside the string is NOT string interpolation! It's a message template. Serilog and the logging framework capture UserName and IpAddress as separate, queryable properties.

Rule 2: Use Meaningful Property Names

// ❌ BAD β€” Generic names, hard to understand
_logger.LogInformation("Processing {X} with {Y}", roleId, userId);

// βœ… GOOD β€” Descriptive names, easy to search
_logger.LogInformation("Processing {RoleId} with {UserId}", roleId, userId);

Rule 3: Log Objects with @ for Destructuring

var role = new Role { Id = 1, RoleName = "Admin", Description = "Full access" };

// ❌ BAD β€” Just logs the type name: "EmpMS.Models.Role"
_logger.LogInformation("Created role: {Role}", role);

// βœ… GOOD β€” Destructures the object into its properties
_logger.LogInformation("Created role: {@Role}", role);
// Output: { "Role": { "Id": 1, "RoleName": "Admin", "Description": "Full access" } }

The @ symbol before the property name tells Serilog to destructure the object β€” meaning it breaks the object into its individual properties and logs them all.

πŸ’‘ Complete Example β€” Structured Logging in Your EMS Project

// Controllers/RolesController.cs β€” With proper structured logging
public class RolesController : ControllerBase
{
    private readonly IRoleService _roleService;
    private readonly ILogger<RolesController> _logger;

    public RolesController(IRoleService roleService, ILogger<RolesController> logger)
    {
        _roleService = roleService;
        _logger = logger;
    }

    [HttpPost("create")]
    public async Task<ActionResult<APIResponse>> CreateRole(RoleDto roleDto)
    {
        // βœ… Structured log β€” {RoleName} becomes a searchable property
        _logger.LogInformation("Attempting to create role: {RoleName}", roleDto.RoleName);

        try
        {
            await _roleService.CreateRoleAsync(roleDto);

            // βœ… Structured log with multiple properties
            _logger.LogInformation(
                "Role created successfully. Name: {RoleName}, Description: {Description}",
                roleDto.RoleName,
                roleDto.Description
            );

            _apiResponse.Data = "Successful";
            _apiResponse.Status = true;
            return Ok(_apiResponse);
        }
        catch (Exception ex)
        {
            // βœ… Log error with exception β€” the exception object is captured automatically
            _logger.LogError(
                ex,
                "Failed to create role: {RoleName}. Error: {ErrorMessage}",
                roleDto.RoleName,
                ex.Message
            );

            _apiResponse.Status = false;
            _apiResponse.ErrorMessages = new List<string> { ex.Message };
            return BadRequest(_apiResponse);
        }
    }

    [HttpGet("all")]
    public async Task<ActionResult<APIResponse>> GetAllRoles()
    {
        // βœ… Structured log
        _logger.LogInformation("Fetching all roles");

        var roles = await _roleService.GetAllRolesAsync();

        // βœ… Log the count as a named property β€” very useful for analytics!
        _logger.LogInformation("Retrieved {RoleCount} roles", roles.Count);

        _apiResponse.Data = roles;
        _apiResponse.Status = true;
        return Ok(_apiResponse);
    }

    [HttpGet("{id}")]
    public async Task<ActionResult<APIResponse>> GetRoleById(int id)
    {
        // βœ… Log the ID as a structured property
        _logger.LogInformation("Fetching role with {RoleId}", id);

        var role = await _roleService.GetRoleByIdAsync(id);

        if (role == null)
        {
            // βœ… Warning with structured data
            _logger.LogWarning("Role not found with {RoleId}", id);
            return NotFound();
        }

        // βœ… Destructure the entire object with @
        _logger.LogDebug("Found role: {@Role}", role);

        _apiResponse.Data = role;
        _apiResponse.Status = true;
        return Ok(_apiResponse);
    }
}

πŸ“„ Structured Output Examples

Console Output (readable by humans):

[2026-02-25 11:30:00 INF] Attempting to create role: Admin
[2026-02-25 11:30:01 INF] Role created successfully. Name: Admin, Description: Full access
[2026-02-25 11:30:05 INF] Fetching all roles
[2026-02-25 11:30:05 INF] Retrieved 5 roles
[2026-02-25 11:30:10 WRN] Role not found with 99

JSON File Output (readable by machines) β€” when using Serilog JSON formatter:

{
  "Timestamp": "2026-02-25T11:30:00.123",
  "Level": "Information",
  "MessageTemplate": "Attempting to create role: {RoleName}",
  "Properties": {
    "RoleName": "Admin",
    "SourceContext": "EmpMS.Controllers.RolesController",
    "RequestPath": "/api/roles/create",
    "RequestId": "0HN4ABCDEF:00000001"
  }
}

{
  "Timestamp": "2026-02-25T11:30:01.456",
  "Level": "Information",
  "MessageTemplate": "Role created successfully. Name: {RoleName}, Description: {Description}",
  "Properties": {
    "RoleName": "Admin",
    "Description": "Full access",
    "SourceContext": "EmpMS.Controllers.RolesController"
  }
}

Now any log analysis tool can read this JSON and create dashboards like:

  • "How many roles were created today?" β†’ Count where MessageTemplate contains "created successfully"
  • "Which user created the most roles?" β†’ Group by UserName
  • "How many 404s happened this week?" β†’ Count where Level = "Warning" and MessageTemplate contains "not found"

πŸ”§ Enable JSON File Output with Serilog

To write structured JSON logs to a file, update your Serilog configuration:

// Program.cs β€” Add JSON formatted file sink
using Serilog;
using Serilog.Formatting.Json;

Log.Logger = new LoggerConfiguration()
    .MinimumLevel.Information()
    .WriteTo.Console()                                    // Human-readable console
    .WriteTo.File(
        new JsonFormatter(),                              // βœ… JSON format for machines
        "Logs/app-log-.json",                             // Save as .json file
        rollingInterval: RollingInterval.Day
    )
    .CreateLogger();

πŸ“ Summary β€” Do's and Don'ts of Structured Logging

βœ… Do This ❌ Don't Do This
_logger.LogInformation("User {Name}", name) _logger.LogInformation($"User {name}")
Use meaningful names: {RoleName}, {UserId} Use vague names: {X}, {Data}, {Value}
Use @ to destructure objects: {@Role} Log objects directly: {Role} (shows type name only)
Log exceptions with _logger.LogError(ex, "...") Log only ex.Message and lose the stack trace
Include relevant context: IDs, names, counts Log generic messages: "Something happened"

🎯 Quick Recap

Topic Key Takeaway
ILogger<T> The interface you use in your code to write logs
ILoggerProvider The destination (Console, Debug, EventLog, Serilog, etc.)
ILoggerFactory The manager that creates loggers and manages providers
Configuration Use appsettings.json to control which logs go where
Serilog The best third-party provider β€” supports files, JSON, databases
Structured Logging Use message templates ({PropertyName}) not string interpolation

Remember: Built-in logging is fine for learning, but for production applications, always use a third-party provider like Serilog with structured logging for maximum power and flexibility! πŸš€


Made with ❀️ for learning ASP.NET Core Logging

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors