Logging, categories and message templates
ILogger categories and levels, how the most specific filter wins, and why a message template is not an interpolated string.
After this lesson you can
- Configure log levels per category and predict what gets written
- Write a structured log call with a message template
- Explain why string interpolation in a log call is a mistake
A class asks for a logger through DI:
public class CheckoutService(ILogger<CheckoutService> logger)
{
public void Place(int orderId, decimal total) =>
logger.LogInformation("Order {OrderId} placed for {Total}", orderId, total);
}
ILogger<CheckoutService> writes under the category
MyShop.Checkout.CheckoutService, the full name of the type. Every call
has a level: Trace (0), Debug, Information, Warning, Error,
Critical (5), plus None (6), which is used only in configuration to
switch a category off.
Filtering by category
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning",
"MyShop.Payments": "Debug"
}
}
For each category the most specific rule wins: the longest configured
name that the category starts with, and Default when none does. A
message is written when its level is at or above that minimum. So
Microsoft.AspNetCore.Routing logs only warnings and up, while
MyShop.Payments.Refunds logs from Debug. A section per provider (for
example "Console": { "LogLevel": { ... } }) overrides these rules for
that provider only.
Message templates
The first argument is a template, not a format string. Its
placeholders become named properties that a structured sink (Seq,
Application Insights, Elasticsearch) can query: OrderId = 42 rather
than text to grep. Two details matter:
- Placeholders are filled by position. The names label the values; they do not look anything up.
- Rendering is deferred until a provider decides to write the message,
so a disabled level builds no string. The call itself still happens,
and value-type arguments are still boxed into the
paramsarray.
The example is a small model of a logger, not ILogger itself, that
binds and renders templates the same way.
using System.Text.RegularExpressions; public class Money(decimal amount, List<string> log){ public override string ToString() { log.Add("formatted Money"); return amount.ToString("0.00"); }} public class TinyLogger(int minLevel, List<string> output){ public void Log(int level, string template, params object[] args) { if (level < minLevel) return; // nothing is rendered var names = Regex.Matches(template, @"\{(\w+)\}").Select(m => m.Groups[1].Value).ToList(); var values = args.Select(a => a.ToString()!).ToList(); // each argument formatted once var i = 0; var text = Regex.Replace(template, @"\{(\w+)\}", _ => values[i++]); var props = string.Join(", ", names.Select((n, k) => n + "=" + values[k])); output.Add(text + " {" + props + "}"); }} public static class Solution{ public static List<string> Run() { var output = new List<string>(); var logger = new TinyLogger(minLevel: 2, output); // Information and up var total = new Money(19.5m, output); logger.Log(2, "Order {OrderId} placed for {Total}", 42, total); logger.Log(2, "Order {OrderId} placed for {Total}", total, 42); // arguments swapped logger.Log(1, "Debug detail for {Total}", total); // below the minimum return output; }}An interpolated string, logger.LogInformation($"Order {orderId} placed"),
throws both benefits away: the text is built before the call whether or
not the level is enabled, and every message is a different string with
no properties. For hot paths, the [LoggerMessage] source generator
produces a strongly typed method that checks the level first and avoids
boxing the arguments.
logger.BeginScope(...) attaches properties, such as an order id, to
every message written inside it, which is how one request's lines are
tied together.
Common mistakes
- Interpolating into the template. It allocates on every call and loses the structured properties.
- Reordering arguments without reordering placeholders. The names stay put and the values swap, as in the model's second line.
- Setting
DefaulttoDebugin production to see one noisy class. Add a rule for that category instead. - Logging secrets or personal data inside a template property. Structured sinks index it and keep it.
What interviewers ask
- Why use a message template instead of string interpolation?
Structured properties and deferred formatting; mention
[LoggerMessage]for hot paths. - How would you turn on debug logging for one namespace without
flooding the logs? A
LogLevelentry for that category prefix; the longest match wins. - How do you correlate all the log lines of one request? Scopes (or the trace and span ids the host attaches) carried as properties.
Exercise · Pro
Query structured log entries
Exercises are free in the first 3 lessons of every course. From here on they are part of Pro and Sprint; the lesson and its runnable examples stay open.
Upgrade