EthicsComputerCourses
Join training +91 90532 10052

Logging

ASP.NET Core · Lesson 10 of 29 Intermediate 8 min read Updated

When a patient in Jaipur says "my booking failed at 10:42", you cannot attach a debugger to the production server. What you can do is read the logs: time-stamped messages your app wrote while it worked. Good logs tell you what happened, to which request, and why. Bad logs are either silent or so noisy that the one useful line is buried.

ASP.NET Core has logging built in. In this lesson you will write log messages with ILogger<T>, choose the right level, filter noise through appsettings.json, use structured message templates, log exceptions properly, and switch on request logging when you need it.

Writing your first log messages

CreateBuilder already registers logging with the Console, Debug and EventSource providers (plus the Windows Event Log on Windows). You only need to ask for an ILogger<T>, where T is the class that is logging:

C#
namespace Clinic.Services;

public class BookingService(ILogger<BookingService> logger)
{
    public int Book(string patient, int doctorId, string time)
    {
        int bookingId = 42; // the database would create this
        logger.LogInformation("Booking {BookingId} confirmed for {Patient} with doctor {DoctorId} at {Time}",
            bookingId, patient, doctorId, time);
        return bookingId;
    }
}

When Book("Meera", 2, "10:30") runs, the console shows:

Output
info: Clinic.Services.BookingService[0]
      Booking 42 confirmed for Meera with doctor 2 at 10:30

Each console entry has three parts: the level (info), the category (the full name of T, here Clinic.Services.BookingService) and the event ID in square brackets (0 when you do not set one), followed by the message on the next line.

Log levels

Every message has a level. Configuration then decides which levels are actually written.

LevelMethodConsole labelUse it for
TraceLogTracetrceVery detailed steps; never in production
DebugLogDebugdbugInformation useful while developing
InformationLogInformationinfoNormal events: booking created, user signed in
WarningLogWarningwarnUnusual but handled: slot already taken, retrying
ErrorLogErrorfailAn operation failed: SMS could not be sent
CriticalLogCriticalcritThe app cannot continue: database unreachable

A good habit: if a person must act on it, it is at least a Warning. If it happens on every request, it is Information or lower.

Structured logging with message templates

Notice that the message above uses placeholders such as {BookingId}, not string interpolation. This is called a message template. The console shows the filled-in text, but providers that store logs as data (Seq, Elasticsearch, Application Insights, OpenTelemetry back ends) also keep BookingId = 42 and Patient = Meera as separate, searchable fields. You can then ask "show every log for booking 42" instead of searching text.

C#
// Good: structured, and formatting is skipped if the level is disabled
logger.LogWarning("Slot {Time} for doctor {DoctorId} is already booked", time, doctorId);

// Avoid: interpolation builds the string even when Warning is filtered out,
// and the values are lost as separate fields
logger.LogWarning($"Slot {time} for doctor {doctorId} is already booked");

Placeholders are matched to arguments by position, not by name, so keep them in the same order.

Filtering with appsettings.json

The Logging:LogLevel section sets the minimum level per category. A category matches by prefix, and the longest matching prefix wins:

JSON
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning",
      "Microsoft.EntityFrameworkCore.Database.Command": "Information",
      "Clinic.Services": "Debug"
    }
  }
}

With this file, your own Clinic.Services classes log Debug and above, ASP.NET Core's internal messages are limited to Warning and above, and EF Core prints every SQL command it runs, which is very useful while learning. Put noisy settings like the EF Core one in appsettings.Development.json so production stays quiet. Because appsettings.json reloads automatically, changing levels takes effect without restarting the app.

Logging exceptions and scopes

Pass the exception object as the first argument. The provider then records the type, message and stack trace:

C#
try
{
    await smsGateway.SendAsync(phone, text);
}
catch (HttpRequestException ex)
{
    logger.LogError(ex, "SMS reminder for booking {BookingId} could not be sent", bookingId);
}

The console prints the message, then the exception type and message, then the stack trace, all indented under the fail: line.

A scope attaches values to every message logged inside a block, so you do not repeat them:

C#
using (logger.BeginScope("Booking {BookingId}", bookingId))
{
    logger.LogInformation("Checking slot");
    logger.LogInformation("Saving booking");
}

Structured providers record BookingId on both messages. The console provider shows scopes only when you enable them with "Console": { "FormatterOptions": { "IncludeScopes": true } } inside the Logging section.

Faster logging with LoggerMessage

For messages logged very often, the LoggerMessage source generator creates optimised logging methods at compile time. You declare a partial method with an attribute, and the compiler writes the body:

C#
namespace Clinic.Services;

public static partial class BookingLog
{
    [LoggerMessage(EventId = 1001, Level = LogLevel.Warning,
        Message = "Slot {Time} for doctor {DoctorId} is already booked")]
    public static partial void SlotTaken(ILogger logger, string time, int doctorId);
}

// inside BookingService:
// BookingLog.SlotTaken(logger, "10:30", 2);
Output
warn: Clinic.Services.BookingService[1001]
      Slot 10:30 for doctor 2 is already booked

The category still comes from the logger you pass in, and the event ID 1001 now appears in the brackets, which makes alerts easy to set up ("notify me when event 1001 happens 50 times in an hour").

Logging HTTP requests

To see every request and response while debugging, add HTTP logging:

C#
builder.Services.AddHttpLogging(options =>
{
    options.LoggingFields = HttpLoggingFields.RequestMethod | HttpLoggingFields.RequestPath
                          | HttpLoggingFields.ResponseStatusCode | HttpLoggingFields.Duration;
});

var app = builder.Build();
app.UseHttpLogging();

It logs at Information under the category Microsoft.AspNetCore.HttpLogging.HttpLoggingMiddleware, so add that category with "Information" to Logging:LogLevel, otherwise the template's "Microsoft.AspNetCore": "Warning" rule hides it. HttpLoggingFields lives in the Microsoft.AspNetCore.HttpLogging namespace.

Beyond the console

The built-in providers are enough to start. Larger systems usually send logs to a central store: OpenTelemetry (an open standard that also covers metrics and traces) or libraries such as Serilog and NLog that plug into the same ILogger interface. Your code does not change; only the providers registered in Program.cs do.

Common mistakes

  • String interpolation in log calls. Use message templates so values stay structured and work is skipped for disabled levels.
  • Logging everything at Information. Production logs become huge and the important lines are lost.
  • Catching an exception and logging only ex.Message. Pass ex itself so the stack trace is kept.
  • Logging secrets or personal data. Log identifiers, not the sensitive values.
  • Editing levels in code. Keep them in appsettings.json so they can change per environment without a rebuild.

Key points

  • Inject ILogger<T>; the category is the full name of T, shown with the level and event ID.
  • Levels from lowest to highest: Trace, Debug, Information, Warning, Error, Critical.
  • Use message templates with placeholders, not interpolation, for structured, efficient logs.
  • Filter by category prefix in Logging:LogLevel; put noisy development settings in appsettings.Development.json.
  • Pass exceptions as the first argument, use scopes for shared context, and [LoggerMessage] for hot paths.

Exercise

Write a TicketService class in the namespace Railway.Services for a train booking app. It should log Information "Ticket {Pnr} booked for {Passenger} on train {TrainNo}" when booking succeeds, Warning "Waitlist {Position} given to {Passenger}" when the train is full, and Error with the exception when payment fails. Then write the Logging section that shows Debug messages for Railway.Services but only Warning for everything from Microsoft.

Show solution
C#
namespace Railway.Services;

public class TicketService(ILogger<TicketService> logger)
{
    public void Confirmed(string pnr, string passenger, int trainNo) =>
        logger.LogInformation("Ticket {Pnr} booked for {Passenger} on train {TrainNo}", pnr, passenger, trainNo);

    public void Waitlisted(int position, string passenger) =>
        logger.LogWarning("Waitlist {Position} given to {Passenger}", position, passenger);

    public void PaymentFailed(Exception ex, string passenger) =>
        logger.LogError(ex, "Payment failed for {Passenger}", passenger);
}
JSON
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Railway.Services": "Debug"
    }
  }
}

A call such as Confirmed("2456789012", "Ananya", 12955) prints info: Railway.Services.TicketService[0] followed by Ticket 2456789012 booked for Ananya on train 12955. The Microsoft prefix covers both Microsoft.AspNetCore and Microsoft.EntityFrameworkCore categories.

Quick Quiz

1. What is the category of a logger injected as ILogger<BookingService> in namespace Clinic.Services?

2. Why prefer LogInformation("Booked {Id}", id) over LogInformation($"Booked {id}")?

3. Which console label means Error level?

4. With "Microsoft.AspNetCore": "Warning", why do you not see HTTP logging output by default?

Learn ASP.NET Core with a mentor

Free lessons get you started. Our Programming & Software Development training adds real projects and code reviews from working developers.

  • Project-based learning
  • Mentor code reviews
  • Free counselling call