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.
- Logging in ASP.NET Core
- Logging Configuration in ASP.NET Core
- Logging to Files with Serilog in ASP.NET Core
- Structured 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.
| 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 |
ASP.NET Core provides a built-in logging system through the Microsoft.Extensions.Logging namespace. There are 3 main interfaces you need to know:
This is the main interface you use in your code to write log messages.
Tis 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 fromILogger<RolesController>. It tells you exactly which class generated this log.
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
ILoggerProvidercreates its ownILogger. So if you have 3 providers (Console, Debug, EventLog), your one log message gets sent to ALL 3 places!
This is the manager/boss that controls everything. It:
- Registers all the
ILoggerProvider(s) - Creates
ILoggerinstances - Distributes log messages to all registered providers
Think of a TV Broadcasting Station π‘: The
ILoggerFactoryis the station. EachILoggerProvideris a TV channel (Console, Debug, EventLog). When someone speaks (logs a message), the station broadcasts it to ALL channels at the same time.
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:
- You call
_logger.LogInformation("User logged in")in your code. ILogger<T>sends this message to theILoggerFactory.ILoggerFactorydistributes the message to ALL registeredILoggerProviders.- Each
ILoggerProviderwrites the message to its destination (Console, EventLog, Debug window, etc.).
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.
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>. TheILoggerFactoryhandles which providers are active. You just configure it once inProgram.cs.
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 5Rule of thumb: In production, you usually set the minimum level to
WarningorInformation. You don't wantTraceandDebugmessages flooding your production logs!
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.
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
ErrorandWarningbecause 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.
ASP.NET Core reads logging configuration from the appsettings.json file. Here's how:
// 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! |
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 globalLogLevelsettings.
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.ControllersmatchesEmpMS.Controllers.RolesController,EmpMS.Controllers.AuthController, etc. More specific categories override less specific ones.
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" } }
} }
} }
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.
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.
| 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) |
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.FileWhat 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!
There are two ways to configure Serilog β through code or through appsettings.json. Here's both:
// 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 |
First, install one more package:
dotnet add package Serilog.Settings.ConfigurationThen 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 setupWhich 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.
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!
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);
}
}
}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
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!
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!
| 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 |
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.
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 captureUserNameandIpAddressas 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.
// 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);
}
}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
MessageTemplatecontains "created successfully"- "Which user created the most roles?" β Group by
UserName- "How many 404s happened this week?" β Count where
Level= "Warning" andMessageTemplatecontains "not found"
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();| β 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" |
| 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