ILogger Best Practices in .NET: Structured Logging That Helps You Debug

Most production incidents are debugged from logs, and most logs are written in a hurry. ILogger in .NET supports structured logging out of the box, but only if you use it the way it was designed. These are the practices that make logs searchable, cheap, and useful when something breaks.

Video explainer (1:20) · Watch with transcript

1. Use message templates, not string interpolation

ILogger message templates keep each value as a named property that log tools like Seq, Elasticsearch, Application Insights, or Datadog can filter on. String interpolation flattens everything into one string and builds it even when the level is disabled.

_logger.LogInformation(
quot;Order {order.Id} paid by {customerId}");
_logger.LogInformation("Order {OrderId} paid by {CustomerId}", order.Id, customerId);

2. Pass the exception as the first argument

LogError(ex, "...") keeps the exception type, message, and full stack trace. Logging only ex.Message throws away the stack trace, which is the part you need.

catch (Exception ex)
{
    _logger.LogError(ex, "Refund {PaymentId} failed", payment.Id);
    throw;
}

3. Pick log levels deliberately

  • Trace and Debug: detail for local debugging; normally off in production
  • Information: business events worth keeping (order placed, job finished)
  • Warning: something unexpected that the app recovered from
  • Error: the current operation failed
  • Critical: the app or a dependency is down and needs immediate attention

4. Add context once with scopes

BeginScope attaches properties to every log written inside it, so you do not repeat the same IDs on every line. ASP.NET Core already adds a request ID and trace ID scope for each request.

using (_logger.BeginScope(new Dictionary<string, object> { ["OrderId"] = order.Id }))
{
    _logger.LogInformation("Charging card");
    _logger.LogInformation("Sending receipt");
}

5. Use the LoggerMessage source generator on hot paths

For code that logs thousands of times a second, the [LoggerMessage] attribute generates logging methods at compile time with no boxing or template parsing at runtime.

public static partial class Log
{
    [LoggerMessage(Level = LogLevel.Warning, Message = "Cache miss for {Key}")]
    public static partial void CacheMiss(ILogger logger, string key);
}

6. Never log secrets or personal data

  • No passwords, tokens, API keys, or full card numbers
  • Be careful when logging whole request bodies or entity objects; they often contain personal data
  • Mask or omit sensitive fields, and keep log retention as short as you can

Find catch blocks that log nothing

A catch block without any logging is how failures go unnoticed. CodeInspector flags it as rule CI0003 ("Missing Logger"), and empty catch blocks as CI0002, with the file and line for each. Scan a repository at https://codeinspector.app with 100 free credits.

Related