Ever lost an hour hunting a bug because your logs were hard to read?
Serilog output templates control the final layout of each log line: timestamp, level, message, properties, and exception text, so a small tweak can cut debugging time.
This post shows what an output template is, the most useful tokens, quick templates you can copy for console and file sinks, how to set templates in appsettings.json or code, and when to use ExpressionTemplate for advanced formatting.
You’ll finish able to make logs easy to scan and parse.
Key Concepts of the Serilog Output Template for Immediate Practical Use

Serilog output templates control how your log events actually look when they’re written to console, files, or wherever else you’re sending them. They’re different from message templates (the ones you write in Log.Information("User {UserId} logged in", userId)). Output templates define the final display layout for every log event your app produces.
Here’s something you can use right now:
[{Timestamp:HH:mm:ss} {Level:u3}] {Message}{NewLine}{Exception}
That gives you logs like [14:32:17 INF] Application started for info messages and includes stack traces when exceptions happen. The template uses predefined tokens that Serilog swaps out with real event data whenever it writes a log entry.
Message templates and output templates work differently. In message templates, missing arguments cause the token itself to show up (like {Token}) so you catch the mistake during testing. Output templates ignore missing properties silently to prevent clutter when events lack optional fields. Since Serilog 1.3.7, output templates default to literal rendering (the :l specifier is on by default), so you won’t see unwanted quotation marks around exception text or newlines.
The tokens you’ll use most are:
{Timestamp} — when the event was logged, supports standard .NET date/time format strings
{Level} — the log level (Information, Warning, Error, etc.), can be shortened and cased
{Message} — the rendered log message with arguments filled in
{Properties} — structured properties and enriched fields that aren’t part of the message
{NewLine} — platform newline character
{Exception} — exception details including stack trace when present
Output templates are configured at the sink level, so you can format console output one way (compact, color coded) and file output another way (detailed, structured). The :l literal behavior matters because it prevents Serilog from treating your log output like a C# string literal where things get quoted or escaped.
Working With Serilog Output Template Syntax Mechanics

Serilog parses output templates left to right. Text outside braces is literal, text inside {...} is a token reference. When a sink writes a log event, Serilog walks the template, copies literals as they are, and replaces tokens with the corresponding event data. If a token references a property that doesn’t exist, Serilog skips it silently instead of crashing or inserting error text.
Format specifiers follow the token name after a colon. {Timestamp:yyyy-MM-dd} formats the timestamp using that pattern, and {Level:u3} renders the level as three uppercase characters. Multiple format options can be chained in some contexts. Serilog passes standard .NET format strings through to the underlying type’s formatter when applicable.
Alignment and padding use the same syntax as .NET composite formatting. Add a comma and a number after the token name to pad the output to a fixed width: {Level,5} pads to five characters with spaces on the left. Prefix the number with a minus to right align: {Level,-5} pads on the right instead. By default, Serilog uses the current thread culture for number and date formatting, but you can pass a CultureInfo when constructing an ExpressionTemplate to lock formatting to a specific locale regardless of runtime settings. To include a literal brace character in your template, double it: {{ or }}.
Customizing Serilog Output Template Timestamps, Levels, and Exceptions

Timestamp formatting uses standard .NET date and time format strings. {Timestamp:HH:mm:ss} gives you 24 hour time without the date, {Timestamp:yyyy-MM-dd HH:mm:ss.fff} includes milliseconds, and {Timestamp:o} produces ISO 8601 round trip format. The :o specifier is especially useful when logs will be parsed by systems that expect standardized timestamps.
When you need UTC timestamps instead of local time, call UtcDateTime() in an ExpressionTemplate: {@t:u UtcDateTime():yyyy-MM-dd HH:mm:ss}. This converts the event timestamp to UTC before formatting, so you get consistent absolute times regardless of server time zones. You can combine precision adjustments by trimming the format string or adding fractional second specifiers like fff for milliseconds or ffffff for microseconds.
Level formatting supports custom width and casing rules:
{Level:u3} — three uppercase characters (INF, WRN, ERR)
{Level:w3} — three lowercase characters (inf, wrn, err)
{Level} — full title case name (Information, Warning, Error)
{Level:u} — full uppercase name (INFORMATION, WARNING, ERROR)
The numeric suffix (3 in the examples) controls how many characters to display. Longer levels like “Information” get truncated, and shorter ones like “Debug” get padded with spaces if you specify a fixed width.
Exception output improved in Serilog 1.3.7 when literal formatting became the default for output templates. Before that change, you had to write {Exception:l} to prevent quotation marks from appearing around stack traces. Now {Exception} renders cleanly without extra syntax, and the full exception detail (type, message, stack trace, inner exceptions) appears whenever an exception is attached to the log event.
Practical Serilog Output Template Examples for Console and File Sinks

Console output benefits from compact formatting because developers scan logs quickly during debugging. A template like [{Timestamp:HH:mm:ss} {Level:u3}] {Message}{NewLine}{Exception} keeps each line short and highlights the important parts. Time, severity, message. File output often needs more detail for post mortem analysis, so templates include structured properties: {Timestamp:o} [{Level}] {Message} {Properties}{NewLine}{Exception}.
Rolling file sinks timestamp each file name, but the timestamp in the template controls the content inside the file. If you configure a rolling file to create a new file daily, the template timestamp might show only HH:mm:ss since the date is already in the file name. For centralized logging that merges files from multiple sources, always include the full date and time plus a unique identifier like correlation ID in the output template.
| Environment | Example Template | Notes |
|---|---|---|
| Development console | [{Timestamp:HH:mm:ss} {Level:u3}] {Message}{NewLine} | Minimal, fast to read, omits properties to reduce noise |
| Production file | {Timestamp:o} [{Level}] {SourceContext} {Message} {Properties}{NewLine}{Exception} | Includes source context and all properties for detailed troubleshooting |
| Staging console | [{Timestamp:HH:mm:ss} {Level:u3}] {SourceContext} {Message}{NewLine}{Exception} | Adds source context to help trace request flow without overwhelming detail |
Configuring Serilog Output Template in appsettings.json and Programmatic Setup

When you use Serilog.Settings.Configuration, you can define output templates directly in appsettings.json under the sink configuration. For the console sink, add "outputTemplate": "[{Timestamp:HH:mm:ss} {Level:u3}] {Message}{NewLine}{Exception}" inside the console sink’s options. File sinks work the same way. Put the template string in the configuration and Serilog applies it when writing each event.
Standard output templates (the ones using simple tokens like {Timestamp} and {Level}) work in JSON configuration. If you need more advanced transformations (conditional blocks, string functions, array iteration) you must use ExpressionTemplate, which implements ITextFormatter and can only be configured in C# code. That means sinks like Console() and File() have overloads that accept an ITextFormatter instead of an outputTemplate string, and you pass a new ExpressionTemplate(...) instance to that parameter.
To wire up Serilog output templates with ASP.NET Core logging via dependency injection:
- Install
Serilog.AspNetCoreandSerilog.Settings.Configuration. - Add the output template to your
appsettings.jsonunderSerilog:WriteTofor each sink. - Call
UseSerilog()on the host builder and configureLoggerConfigurationto read from configuration. - Register the logger in the DI container so controllers and services can inject
ILogger<T>. - If using
ExpressionTemplate, bypass JSON configuration and configure sinks programmatically insideUseSerilog((context, services, config) => ...)by calling theITextFormatteroverload directly.
Advanced Serilog Output Template Transformations Using ExpressionTemplate

ExpressionTemplate from the Serilog.Expressions package enables rich text transformations that go beyond basic token replacement. You can call functions like Substring() to trim strings, Coalesce() to provide defaults when properties are missing, and Rest() to capture all properties not explicitly mentioned elsewhere in the template. The catch is that ExpressionTemplate requires C# configuration. It can’t be set via appsettings.json or XML config files and must be passed to sinks using the ITextFormatter overload.
Install Serilog.Expressions from NuGet and add using Serilog.Templates; to your logger configuration file. Then construct an ExpressionTemplate with your template string and pass it to sinks like Console(new ExpressionTemplate(...)) or File(new ExpressionTemplate(...), "log.txt"). The template language supports dotted accessors for nested properties, bracket indexers for arrays, and function calls for advanced scenarios described at Customizing Serilog text output.
Conditional and Iterative Blocks
Use {#if} to include template sections only when a condition is true. For example, {#if SourceContext is not null}[{SourceContext}]{#end} renders the source context in brackets when it’s present and omits it entirely otherwise. You can chain {#else if} and {#else} blocks for multi branch logic.
Iterate over collections with {#each}. If a property is an array or object, {#each} exposes member variables inside the block. For objects, you get the member name and value. For arrays, you get the element. Combine {#delimit} to insert separators between elements. {#each item in Scope}{#delimit}, {#end}{item}{#end} renders array elements with commas between them.
String and Property Manipulation
Substring(text, start, length) extracts part of a string. To show only the class name from a fully qualified SourceContext like MyApp.Services.Service1, use Substring(SourceContext, LastIndexOf(SourceContext, '.') + 1). LastIndexOf() finds the position of the last dot, and Substring() takes everything after it.
Coalesce(...) returns the first non null argument, useful for defaults: Coalesce(RequestId, '<none>') displays <none> when RequestId is missing. ElementAt(ci) accesses properties with inconsistent casing. ElementAt(Properties, 'threadid', ci) matches ThreadId, ThreadID, or threadId. Rest() returns an object containing all properties not already referenced in the template, helpful for catch all property logging.
When you need locale specific number or date formatting regardless of server culture, pass a CultureInfo to the ExpressionTemplate constructor. Arrays and nested objects render naturally with dotted paths and indexers, and functions like UtcDateTime() convert timestamps to UTC before formatting.
Structured Properties, Enrichers, and Their Role in Serilog Output Template Design

Enrichers add structured data to every log event automatically. They implement ILogEventEnricher and populate the Properties collection with fields like correlation IDs, user identifiers, or machine names. When you reference {Properties} in an output template, Serilog renders all enriched and captured properties that aren’t part of the message itself.
For distributed tracing, enrichers attach trace and span IDs from Activity or OpenTelemetry contexts to each event. Output templates can pull those fields individually ({TraceId} and {SpanId}) or include them in {Properties} for automatic logging. Custom enrichers let you add domain specific context like tenant ID, environment name, or deployment version, and those values appear in every log line without manual logging calls.
The Properties collection holds key value pairs. When you destructure an object in a message template ({@User}), Serilog stores the entire object in Properties under the key User. Output templates can access nested fields with dotted syntax in ExpressionTemplate or reference the whole object with {Properties}.
Common enriched fields include:
CorrelationId — ties related events across services and requests
TraceId and SpanId — distributed tracing identifiers from Activity or W3C Trace Context
UserId or UserName — identity claims from authentication middleware
MachineName and ProcessId — host and process identifiers for multi instance deployments
Environment — development, staging, production, set at startup
Serilog Output Templates for JSON Comparison, Migration, and Compatibility

Serilog offers multiple JSON formatters alongside traditional output templates. JsonFormatter writes structured JSON with fixed root properties (Timestamp, Level, MessageTemplate) and stores all other fields in a Properties object. CompactJsonFormatter produces smaller payloads by omitting the rendered message and using shorter field names. RenderedCompactJsonFormatter includes the fully rendered message for human readability while keeping the compact structure.
JsonFormatter(renderMessage: true) forces the rendered message into the JSON output, but the renderMessage flag can’t be set via appsettings.json or XML configuration. It must be configured in code when constructing the formatter. That limitation matters when you want JSON logs for ELK or Seq but also need immediate message readability without parsing the template and properties at query time.
For ELK and Kibana, compact JSON formatters align better with Logstash field mapping expectations because they produce flatter structures. Kibana queries work cleaner when fields are top level rather than nested inside a Properties object, and the smaller payload reduces Elasticsearch storage costs. The comparison at Serilog: Advanced Formatting: From Templates to Readable Logs details practical tradeoffs.
Troubleshooting Serilog Output Template Errors and Common Pitfalls

Template validation happens at runtime, so typos in token names or format specifiers cause silent failures. The token disappears instead of throwing an exception. If a log line is missing expected data, check the token spelling and verify the property exists in the log event. Missing properties are ignored in output templates (a behavior introduced in Serilog 1.3.7), so a template referencing {RequestId} produces no output if the event lacks that property.
Format specifier mismatches cause similar silent issues. If you write {Timestamp:HHHH} (four Hs), Serilog treats it as an invalid format and falls back to default rendering. Standard .NET format strings apply, so refer to DateTime and numeric formatting documentation when specifying custom patterns.
Common template issues and fixes:
Missing newlines between entries: Forgot {NewLine} at the end of the template. Logs run together on one line.
Timestamps in wrong time zone: Use UtcDateTime() in ExpressionTemplate or append z to the format string for local offset.
Levels displayed as full words when you want short codes: Change {Level} to {Level:u3} for three character uppercase abbreviations.
Properties not rendering: The property name is misspelled, or the event doesn’t include it. Check enricher configuration and message template destructuring.
Final Words
In the action, we showed what an output template does, gave a ready example ([{Timestamp:HH:mm:ss} {Level:u3}] {Message}{NewLine}{Exception}), and spelled out how message templates differ from output templates.
We walked through tokens, syntax rules, timestamp and level formatting, ExpressionTemplate power, enrichers, JSON vs. template choices, config options, and common troubleshooting gotchas.
Use the serilog output template as a quick lever to make logs consistent and easy to parse. Try the sample, tweak level casing and UTC timestamps, and you’ll debug faster and ship with more confidence.
FAQ
Q: What is the Serilog output template and can you give a quick example?
A: The Serilog output template is a sink-facing string that formats log output. Example: “[{Timestamp:HH:mm:ss} {Level:u3}] {Message}{NewLine}{Exception}” — a compact, readable console/file layout.
Q: How does an output template differ from a message template?
A: An output template formats rendered log text for sinks; a message template defines structured message + properties. Output templates ignore missing properties and enable :l literal behavior by default (per Serilog 1.3.7).
Q: What are the fundamental output template tokens I should know?
A: The key tokens are {Timestamp} (time), {Level} (severity), {Message} (rendered message), {Properties} (enriched fields), {NewLine} (line break), and {Exception} (stack/exception details).
Q: How do I control level casing and get short forms like WRN or inf?
A: You control level casing with format specifiers on {Level}, e.g. {Level:u3} for three-letter uppercase and variants for lowercase/other casing to produce examples like WRN, inf, Err.
Q: How do I format timestamps and force UTC in templates?
A: Use .NET format specifiers like 😮 for ISO‑8601 or custom patterns such as HH:mm:ss; use UtcDateTime() inside ExpressionTemplate to force UTC rendering when needed.
Q: How do padding, alignment, and escaping braces work in output templates?
A: Alignment uses width specifiers (negative for right alignment). CultureInfo controls formatting. Escape literal braces by doubling them: “{{” or “}}”. Missing properties render as empty (silently ignored).
Q: Can I configure ExpressionTemplate via appsettings.json or XML?
A: ExpressionTemplate cannot be configured via JSON or XML; it requires C# setup with an ITextFormatter and code-based registration (Serilog.Settings.Configuration can’t wire it).
Q: How do Console() and File() sinks accept output templates, and any caveats?
A: Console() and File() accept an outputTemplate string directly. ExpressionTemplate requires the ITextFormatter overload. Note Serilog 1.3.7 relaxed the need for explicit :l in many simple templates.
Q: What advanced transformations can I do with ExpressionTemplate?
A: ExpressionTemplate supports Substring(), LastIndexOf(), Coalesce(), UtcDateTime(), {#if}/{#each}/{#delimit} blocks, Rest(), ElementAt(ci), and CultureInfo overrides for complex inline transformations.
Q: How do enrichers affect output templates and which fields are common?
A: Enrichers populate the Properties collection accessible from templates; custom enrichers implement ILogEventEnricher. Common enriched fields: CorrelationId, ActivityId/TraceId, UserId, Environment, and ServiceName.
Q: When should I use JSON formatters versus output templates for ELK/Seq/Kibana?
A: Use JsonFormatter/CompactJsonFormatter/RenderedCompactJsonFormatter for ELK/Seq to preserve structured fields; use output templates for human-readable logs. Note renderMessage and some options are code-only, not JSON-configurable.
Q: What common template errors should I watch for and how do missing properties behave?
A: Common issues: unmatched/incorrect braces, wrong format specifiers, trying ExpressionTemplate in JSON. Missing tokens disappear silently in output templates (per 1.3.7); validate templates and double braces to escape literals.
Q: How should I include exceptions so they render cleanly?
A: Include exceptions by appending {NewLine}{Exception} in the output template. Serilog 1.3.7 improved exception rendering and default :l behavior helps show exception details cleanly for most sinks.
