Skip to content

Message Templates and Structured Logging

thushxr edited this page Jun 27, 2026 · 1 revision

Message Templates & Structured Logging

The non-generic Log uses Serilog-style message templates. A template is a string with named {placeholder} tokens; you pass the values positionally. Each value is rendered into the message and (in JSON mode) emitted as its own field you can filter on.

Templates are a feature of the non-generic Log. Log<T> messages are plain strings — see Log vs Log<T>.

Basics

Log.Information("User {userId} logged in from {ip}", "asda", "10.0.0.1");

Text output:

2026-06-27 09:14:02.214 🟩 [INFO] User asda logged in from 10.0.0.1

JSON output (UseStructured()):

{"timestamp":"2026-06-27 09:14:02.214","level":"Information","icon":"🟩","message":"User asda logged in from 10.0.0.1","userId":"asda","ip":"10.0.0.1"}

userId and ip are now first-class fields — directly queryable in CloudWatch Insights, Seq, Elastic, Loki, or any JSON log tool, without parsing the message string.

Values are matched by position

Placeholders bind to arguments left to right, by position (the same as Serilog). The placeholder name is the field key; it does not have to match the variable name.

Log.Information("{a} then {b}", first, second);
// a = first, b = second

If you pass fewer values than placeholders, the missing ones render as empty / null. If you pass more values than placeholders, the extras are ignored.

Types are preserved in JSON

Values are serialized by their runtime type, so numbers stay numbers, booleans stay booleans:

Log.Information("Processed {count} items in {ok}", 3, true);
{"","message":"Processed 3 items in True","count":3,"ok":true}

Note count is 3 (a JSON number) and ok is true (a JSON boolean) — not strings.

Exceptions

Every level has an exception overload. The exception is appended after the message (in text) or written as an exception field (in JSON). You can still use template values:

try
{
    ProcessPayment();
}
catch (Exception ex)
{
    Log.Error(ex, "Payment failed for {orderId}", orderId);
}

Text:

2026-06-27 09:14:02.500 🟥 [ERROR] Payment failed for 1234
System.InvalidOperationException: Gateway timeout
   at Payments.Process() in C:\app\Payments.cs:line 42

JSON:

{"","message":"Payment failed for 1234","orderId":1234,"exception":"System.InvalidOperationException: Gateway timeout\n   at ..."}

Reserved field names (collision protection)

The logger writes a fixed set of built-in fields:

timestamp · level · icon · traceId · class · method · message · exception

If one of your placeholder names collides with a built-in field, the placeholder is skipped as a JSON field so it can't overwrite the real one. Consider:

Log.Information("Severity {level} reported", "high");

Without protection you'd get two "level" keys, and most log tools do last-wins — your "high" would clobber the real log level. Instead, the built-in field wins:

{"level":"Information","message":"Severity high reported"}

The value isn't lost — it's still rendered into message ("Severity high reported"); it just doesn't get a duplicate, colliding field. Avoid naming placeholders after reserved fields if you want them as queryable fields. This protection applies to JSON only; text output has no key/value structure to collide.

See Performance & Internals for how this is implemented.

Escaping note

The JSON encoder uses relaxed escaping (<, >, &, and emoji are written literally rather than as \uXXXX), which keeps console output readable. This is safe for stdout / log files; it is not intended for embedding directly into HTML.

Clone this wiki locally