ILogger and Log Levels

How to get an ILogger, what each of the six log levels means in production, and how a minimum level is set per category.

SyntaxILogger<T> logger; logger.LogInformation("order {OrderId} shipped", orderId);

A running program is invisible. Logging is how it tells you what it did. You could print with Console.WriteLine, but printed text has no severity, cannot be switched off for one class, and goes nowhere except the screen. .NET uses an interface for this instead: ILogger. An interface is a job description for a type, and many different providers can do the job of writing the lines out.

Getting a logger

Ask for ILogger<T> in a constructor and the dependency injection container hands one over. T is the class doing the logging. Every line that logger writes carries a category: the full name of T. The category is the label you filter on later.

The six levels

LevelWhat it means
TraceStep-by-step detail. Off in production; may hold sensitive data.
DebugDetail for investigating locally. No long-term value.
InformationThe normal flow. A real thing happened. Keep these.
WarningSomething unexpected, but the work still finished.
ErrorThis operation failed. One caller did not get served.
CriticalThe process or a dependency is gone. Wake someone.

Minimum level

Each provider drops anything below its configured minimum. A configuration key matches the start of a category name. The longest matching prefix wins, and if two rules tie the last one wins. There is a seventh value, LogLevel.None; you never log at it, but as a minimum it means write nothing.

logger.LogInformation(message, args) is shorthand for logger.Log(LogLevel.Information, message, args). Both are extension methods and they produce the same entry.

Example

Example · csharp
// dotnet add package Microsoft.Extensions.Logging.Console
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Console;

// A factory makes loggers. In ASP.NET Core the framework owns the factory.
using ILoggerFactory factory = LoggerFactory.Create(builder =>
{
    builder.SetMinimumLevel(LogLevel.Debug);
    builder.AddSimpleConsole(options =>
    {
        options.ColorBehavior = LoggerColorBehavior.Disabled;
    });
});

// The container writes this line for you in a real application.
ILogger<OrderService> logger = factory.CreateLogger<OrderService>();
var service = new OrderService(logger);
service.Ship(7);

class OrderService
{
    private readonly ILogger<OrderService> _logger;

    public OrderService(ILogger<OrderService> logger)
    {
        _logger = logger;
    }

    public void Ship(int orderId)
    {
        _logger.LogDebug("checking stock for order {OrderId}", orderId);
        _logger.LogInformation("order {OrderId} shipped", orderId);
        _logger.LogWarning("order {OrderId} has no tracking number", orderId);
    }
}

// Output:
// dbug: OrderService[0]
//       checking stock for order 7
// info: OrderService[0]
//       order 7 shipped
// warn: OrderService[0]
//       order 7 has no tracking number

When to use it

  • Add a logger to a service class without passing a logger down through every method.
  • Turn on Debug logging for one class while the rest of the app stays at Information.
  • Silence a chatty framework category that fills the log with one line per request.

More examples

Setting levels per category in appsettings.json

These keys are category prefixes, not class names. "Microsoft.AspNetCore" covers every category starting with that text. A logger for Shop.Orders.OrderService matches both "Default" and its own key; the longest prefix wins, so it logs at Debug while everything else stays at Information. Reach for this when you need detail from one class on a live system without multiplying the whole log volume.

Example · json
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning",
      "Shop.Orders.OrderService": "Debug"
    }
  }
}

Guarding work you only do for a log line

The logger itself checks the level, so an ordinary log call on a disabled level costs very little. What it cannot skip is work you did to build an argument. Wrap the call in IsEnabled only when an argument is expensive to produce: a join, a serialisation, a count over a collection.

Example · csharp
using Microsoft.Extensions.Logging;

public static class StockLogging
{
    public static void LogLines(ILogger logger, IReadOnlyList<string> lines)
    {
        // string.Join runs even if Debug is switched off, because arguments
        // are evaluated before the call. IsEnabled skips that cost.
        if (logger.IsEnabled(LogLevel.Debug))
        {
            logger.LogDebug("order lines: {Lines}", string.Join(", ", lines));
        }
    }
}

Logging a failure with its exception

Every Log method has an overload whose first argument is the exception. Use it for anything you caught. The provider then writes the message, the exception type, its own message and the stack trace as one entry, which is what you need when the line turns into a support ticket.

Example · csharp
using Microsoft.Extensions.Logging;

public static class Shipping
{
    public static void Ship(ILogger logger, int orderId)
    {
        try
        {
            throw new InvalidOperationException("carrier refused the booking");
        }
        catch (InvalidOperationException ex)
        {
            // LogError(Exception, string, params object?[]) keeps the
            // stack trace. The message is still a template.
            logger.LogError(ex, "could not ship order {OrderId}", orderId);
        }
    }
}

Discussion

  • Be the first to comment on this lesson.