Cart
Logs should help you fix bugs and explain what the app is doing right now. They shouldn’t drown you in text or hide the one line you need. This guide shows how to wire logging in modern .NET, write structured messages, pick the right level, and add request correlation. Also, don't confuse logs with metrics, it is wrong to add logs to check for API calls per X amount of time.
Program.cs
var builder = WebApplication.CreateBuilder(args);
// Console logging is on by default; you can tweak it via appsettings.json
var app = builder.Build();
app.MapGet("/", (ILogger<Program> log) =>
{
log.LogInformation("Health probe hit at {UtcNow}", DateTime.UtcNow);
return "OK";
});
app.Run();
This already writes to the console with a timestamp, log level, category, and message. Configuration lives in appsettings.json
.
Trace
— very chatty, step‑by‑step flow. Off in production.Debug
— useful during development; safe to disable later.Information
— high‑level events: app start, request completed, business events.Warning
— unusual conditions: retries, timeouts that recover.Error
— failures you catch and handle (include the exception).Critical
— the app is not healthy.Pick the lowest level that still conveys the right urgency. If everything is Information
, nothing is.
Message templates keep data in fields for querying later. Avoid string interpolation here.
// âś… good
log.LogInformation("User {UserId} logged in from {Ip}", userId, ip);
// ❌ bad (hard to query)
log.LogInformation($"User {userId} logged in from {ip}");
With templates, tools can filter on UserId
or Ip
without parsing text.
Include exceptions as the first argument:
try
{
await service.ProcessAsync(orderId);
}
catch (Exception ex)
{
log.LogError(ex, "Failed to process order {OrderId}", orderId);
}
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning",
"Microsoft.EntityFrameworkCore.Database.Command": "Warning"
},
"Console": {
"FormatterName": "json",
"FormatterOptions": {
"IncludeScopes": true,
"UseUtcTimestamp": true
}
}
}
}
Warning
.The logger category is the type you ask for. This helps filter and group logs.
public sealed class BillingService(ILogger<BillingService> log)
{
public void Charge(Guid orderId, decimal amount)
=> log.LogInformation("Charging {OrderId} {Amount}", orderId, amount);
}
In Minimal APIs, inject ILogger<Program>
(or another type) into the handler.
Scopes attach extra properties to every log line inside a block. Add a correlation ID per request so you can trace a path across modules.
app.Use(async (ctx, next) =>
{
var cid = ctx.Request.Headers.TryGetValue("X-Correlation-ID", out var h) && !string.IsNullOrWhiteSpace(h)
? h.ToString()
: Guid.NewGuid().ToString();
using (loggerFactory.CreateLogger("Correlation").BeginScope(new Dictionary<string, object?>
{
["CorrelationId"] = cid
}))
{
ctx.Response.Headers.Append("X-Correlation-ID", cid);
await next();
}
});
Now every log line in that request includes CorrelationId
(and will show up in JSON output).
For basic HTTP logs without extra packages:
using Microsoft.AspNetCore.HttpLogging;
builder.Services.AddHttpLogging(o =>
{
o.LoggingFields = HttpLoggingFields.RequestMethod
| HttpLoggingFields.RequestPath
| HttpLoggingFields.ResponseStatusCode
| HttpLoggingFields.Duration;
});
var app = builder.Build();
app.UseHttpLogging();
This records method, path, status, and duration. Avoid logging bodies unless you have a strong reason.
LoggerMessage.Define
Avoid string formatting allocations by pre‑compiling message templates. The source generator creates efficient logging code (no boxing/allocations on the hot path) and validates your templates at build time.
static class Logs
{
private static readonly Action<ILogger, string, Exception?> _cacheMiss =
LoggerMessage.Define<string>(LogLevel.Debug, new EventId(1001, "CacheMiss"),
"Cache miss for key {Key}");
public static void CacheMiss(this ILogger log, string key) => _cacheMiss(log, key, null);
}
// usage
log.CacheMiss(key);
You still get structured fields, but with fewer allocations.
Sometimes a feature needs extra detail without turning the whole app to Debug
.
builder.Logging.AddFilter("Payments", LogLevel.Debug); // category prefix
app.MapGroup("/payments")
.WithGroupName("Payments")
.MapPost("/", (ILoggerFactory factory) =>
{
var log = factory.CreateLogger("Payments.Checkout");
log.LogDebug("Entering checkout");
return Results.Ok();
});
You can also tune filters in appsettings.json
by category name.
The built‑in providers don’t write to files. If you want rolling files locally, add Serilog:
dotnet add package Serilog.AspNetCore
dotnet add package Serilog.Sinks.File
using Serilog;
var logger = new LoggerConfiguration()
.WriteTo.Console()
.WriteTo.File("logs/app-.log", rollingInterval: RollingInterval.Day, retainedFileCountLimit: 7)
.CreateLogger();
builder.Host.UseSerilog(logger);
Keep this optional. For many apps, console + an aggregator is enough.
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddHttpLogging(o =>
{
o.LoggingFields = HttpLoggingFields.RequestMethod
| HttpLoggingFields.RequestPath
| HttpLoggingFields.ResponseStatusCode
| HttpLoggingFields.Duration;
});
var app = builder.Build();
var log = app.Services.GetRequiredService<ILoggerFactory>().CreateLogger("Startup");
app.UseHttpLogging();
app.Use(async (ctx, next) =>
{
var cid = ctx.Request.Headers["X-Correlation-ID"].FirstOrDefault() ?? Guid.NewGuid().ToString();
using (app.Logger.BeginScope(new { CorrelationId = cid }))
{
ctx.Response.Headers.Append("X-Correlation-ID", cid);
await next();
}
});
app.MapGet("/weather", (ILogger<Program> logger) =>
{
using var _ = logger.BeginScope(new { Feature = "Weather" });
logger.LogInformation("Fetching weather");
return new[] { "Rain", "Sun" };
});
app.Run();
Try hitting /weather
and check the console output. You’ll see JSON lines that include CorrelationId
and Feature
.
Information
or Debug
and never trimming it.BeginScope
for correlation IDs.UseHttpLogging
for basic request logs.LoggerMessage.Define
for hot paths.
Nick Chapsas is a .NET & C# content creator, educator and a Microsoft MVP for Developer Technologies with years of experience in Software Engineering and Engineering Management.
He has worked for some of the biggest companies in the world, building systems that served millions of users and tens of thousands of requests per second.
Nick creates free content on YouTube and is the host of the Keep Coding Podcast.
More courses by Nick Chapsas© 2025 Dometrain. All rights reserved.