Logging

ASP.NET Core · Practical · 10 min

Logs are your main window into production behavior and incidents.

ASP.NET Core ships with Microsoft.Extensions.Logging. Inject ILogger<T> (the category is the type name) and call LogInformation, LogWarning, LogError and friends.

Use message templates, not string interpolation:

logger.LogInformation("Order {OrderId} shipped to {City}", order.Id, city);

The named placeholders become structured properties that log backends (Seq, Elasticsearch, Application Insights, OpenTelemetry) can filter and aggregate. Interpolation would bake the values into a unique string and also format it even when the level is disabled.

Levels from lowest to highest: Trace, Debug, Information, Warning, Error, Critical. Configure minimum levels per category in configuration:

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

Pass exceptions as the first argument: logger.LogError(ex, "Payment {Id} failed", id) keeps the stack trace as structured data.

Scopes (using (logger.BeginScope("Request {RequestId}", id))) attach properties to every log in a block. For hot paths, the [LoggerMessage] source generator creates strongly typed, allocation-free logging methods.

Built-in providers write to console, debug and EventSource. Libraries like Serilog or the OpenTelemetry exporter plug into the same ILogger abstraction, so your code doesn't change when the destination does. Never log secrets, tokens or full personal data.

Example

var builder = WebApplication.CreateBuilder(args);
builder.Logging.AddJsonConsole();            // structured console output
var app = builder.Build();

app.MapPost("/orders/{id:int}/ship", (int id, string city, ILogger<Program> logger) =>
{
    logger.LogInformation("Order {OrderId} shipped to {City}", id, city);
    using (logger.BeginScope("Batch {BatchId}", 7))
    {
        try { if (city == "Atlantis") throw new InvalidOperationException("no route"); }
        catch (Exception ex) { logger.LogError(ex, "Shipping {OrderId} failed", id); }
    }
    Log.Shipped(logger, id);
    return Results.Ok();
});
app.Run();
// {"LogLevel":"Information","Category":"Program","Message":"Order 5 shipped to Paris",
//  "State":{"OrderId":5,"City":"Paris","{OriginalFormat}":"Order {OrderId} shipped to {City}"}}

static partial class Log
{
    [LoggerMessage(Level = LogLevel.Debug, Message = "Shipped {OrderId}")]
    public static partial void Shipped(ILogger logger, int orderId);
}

Remember

  • Inject ILogger<T>; the type becomes the category
  • Use message templates with named placeholders, not interpolation
  • Configure minimum levels per category in Logging:LogLevel
  • Pass the exception as the first argument to LogError

Watch out for

  • Writing logger.LogInformation($"User {id}"), which loses structure and always formats the string.

Check yourself

  1. Why prefer logger.LogInformation("User {Id}", id) over $"User {id}"?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: learn.microsoft.com/en-us/aspnet/core/fundamentals/logging/