Our Summer Sale is Live! 🎉
Everything 30% off with code SUMMER30! (Excl. Team and VS Pro)
00
Days
00
Hrs
00
Min
00
Sec
Get 30% off anything!

Logging in .NET - Best Practices

13/08/2025

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.


Quick start (Minimal API)

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.


Log levels that make sense

  • 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.


Structured logging beats string concatenation

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);
}

Configure logging via appsettings.json

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning",
      "Microsoft.EntityFrameworkCore.Database.Command": "Warning"
    },
    "Console": {
      "FormatterName": "json",
      "FormatterOptions": {
        "IncludeScopes": true,
        "UseUtcTimestamp": true
      }
    }
  }
}
  • Lower noisy categories (e.g., framework internals) to Warning.
  • JSON formatter plays well with log aggregators and keeps fields structured.

Categories and DI

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 and correlation IDs

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).


Request logging middleware (built‑in)

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.


High‑throughput hot paths: 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.


Filtering per endpoint or area

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.


File logs (optional, with Serilog)

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.


What to log (and what to skip)

  • Do log: start/stop events, external calls (target + duration), business events, warnings with context, handled errors with stack traces.
  • Skip: secrets, full request/response bodies with PII, chatty loops, heartbeat spam.
  • Protect data: hash or mask identifiers when in doubt.

Small sample tying it together

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.


Common mistakes

  • Using string interpolation in log messages. Use templates with named placeholders.
  • Logging exceptions without passing the exception object.
  • No correlation between logs from the same request.
  • Turning everything to Information or Debug and never trimming it.
  • Writing secrets to logs by accident (tokens, passwords).

A short checklist

  • [ ] Message templates with named fields.
  • [ ] Log levels picked with intent.
  • [ ] Per‑category filters to cut noise.
  • [ ] BeginScope for correlation IDs.
  • [ ] UseHttpLogging for basic request logs.
  • [ ] LoggerMessage.Define for hot paths.
  • [ ] No secrets or PII.

About the Author

author_img

Nick Chapsas

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