Logging

By the end of this lesson you'll be able to build production-grade logging in C#: choose the right log level, write structured logs that tools can query, wire up ILogger and providers through dependency injection, send logs to multiple destinations with Serilog — and know exactly what you must never log.

Learn Python, JavaScript, Java and more with free interactive lessons, real projects and AI-powered help. Beginner-friendly.

Part of the free C# course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.

A log is the flight recorder — the black box — of your application. A plane's recorder runs continuously, capturing what happened at each moment so that after an incident investigators can reconstruct events without putting the plane back in the air. Your logs do the same: a production bug usually can't be reproduced on demand, so the only evidence you have is what the running system wrote down at the time. Log levels are like the recorder's channels — routine cockpit chatter versus a stall warning — and you tune how much detail you keep. And structured logging is the difference between a searchable digital recorder and a scribbled paper note: both capture the event, but only one lets you instantly find every flight where the same fault appeared.

A typical minimum level : Debug in development (see everything), Information or Warning in production (signal over noise), and Trace only temporarily while chasing a specific bug. Each level's number is what makes filtering a simple = comparison.

1. One Method, One Choke Point

Before any framework, understand what a logger fundamentally is : a single method every message passes through, so the format and destination live in one place. You model the levels with an enum — a named set of constants — and route everything through one Log(level, message) method. Read this worked example and run it, then you'll write the method body yourself.

Your turn. The Log method is empty and two calls are missing pieces — fill in the three ___ blanks using the hints, then run it.

2. Filtering by Level (the Volume Knob)

You almost never want every message. A minimum level acts like a volume knob: anything below it is dropped before it's ever written. Because an enum compares by its underlying number, the filter is one line — if (level MinimumLevel) return; . Set the knob to Debug in development and Warning in production, with no other code changes. Study the worked example, then implement the check yourself.

Now you try. Add the filtering condition and set the minimum level so only Warning and above get through. Fill in the two ___ blanks:

3. ILogger<T> and Structured Logging

In real .NET apps you don't build the logger — you receive one. ILogger<T> is the standard interface; you inject it through the constructor (just like in the DI lesson), and the <T> tags every line with that class's name as a category . The crucial habit is structured logging : instead of gluing values into a string, you pass a message template with named {' Placeholders '} and the values after it. Each value is captured as a searchable property, so later you can query "every log where OrderId = ORD-001 ".

The next three examples use Microsoft.Extensions.Logging and Serilog . The in-browser runner doesn't ship those packages, so read these as worked examples and run them in a real project — the expected output is shown in the comments.

Here's the single most important habit in this lesson side by side: the same event logged the wrong way and the right way. They produce nearly identical console text, but only the template version is searchable in a log aggregator.

When you call _logger.LogInformation(...) , the message doesn't go anywhere by itself. It's handed to every registered provider — a small adapter that knows how to write to one destination. AddConsole() registers the console provider; there are also providers for Debug output, the Windows Event Log, Azure App Service, and more. One log call, fanned out to all of them.

This is why ILogger code stays the same no matter where logs end up: your class depends on the interface , and configuration at startup decides the providers. Swapping console for a file, or adding a second destination, never touches your business logic.

Serilog plugs into this same pipeline but adds its own richer model of destinations, which it calls sinks — that's the next example.

4. Serilog and Sinks

Serilog is the most popular third-party logging library, built around structured logging from the ground up. Its key idea is the sink : a destination for log events. You configure a list of sinks once at startup — console, a rolling file, a queryable server like Seq, cloud services — and every log event is written to all of them. The logging calls themselves use the same {' Placeholder '} template style as ILogger , so what you learned above carries straight over.

Logs are written in plain text, copied to multiple sinks, and often shipped to third-party services and read by many people. Treat every log line as if it could end up in a data breach — because it can.

If you must reference sensitive data, log a masked or hashed form — e.g. card ending 4242 , not the full number.

Here's a small but realistic logger that combines everything runnable from this lesson — a level enum , a minimum-level filter, a category, a timestamp, and friendly Info / Warn / Error helpers. It mirrors what ILogger does for you in production, built from parts you now understand line by line.

The helper methods ( Info , Warn , Error ) are exactly why ILogger gives you LogInformation , LogWarning , and LogError — they're thin wrappers over the one core Log method.

Q: Why is LogInformation("Id {' '}", id) better than

quot;Id {' '}" ?

The template keeps Id as a named, searchable property in the log store; interpolation flattens it into plain text. Both look the same in the console, but only the template lets you later query or alert on that value.

Q: What's the difference between a provider and a sink?

They're the same idea under two names. Microsoft.Extensions.Logging calls a destination a provider ; Serilog calls it a sink . Both are adapters that write a log event somewhere — console, file, server.

Q: ILogger is built in — why would I add Serilog?

Use the built-in ILogger abstraction in your code regardless. Serilog plugs in behind it to provide richer sinks, easy file rolling, and powerful structured output. Your classes still depend only on ILogger<T> .

Q: Should I ever log a password to debug a login issue?

No — never, not even temporarily. Logs are persisted and copied widely, so a secret in a log is a leaked secret. Log the username or a request ID, and a masked or hashed value if you truly need to correlate.

Usually Information or Warning as the minimum — enough to see normal flow and every problem, without drowning in Trace / Debug noise. Drop to Debug or Trace only while actively investigating.

No blanks this time — just a brief and an outline. Build a Logger with a category, a minimum-level filter, and a timestamped output line, then exercise it from Main . Run it and confirm only the Warning and Error lines survive the filter, as shown in the comments.

Logging is one half of observability ; the other halves are metrics and distributed tracing. Once you're comfortable here, look at ILogger.BeginScope for per-request context, and the OpenTelemetry libraries for correlating logs, traces, and metrics across services.

Practice quiz

Which is the lowest-severity log level?

  • Critical
  • Information
  • Trace
  • Warning

Answer: Trace. The six levels run Trace (0, lowest) → Debug → Information → Warning → Error → Critical (5, highest).

How does a minimum-level filter decide what to drop?

  • Anything below the minimum level is dropped before being written
  • It randomly samples messages
  • It keeps only Error and above always
  • It drops the newest messages

Answer: Anything below the minimum level is dropped before being written. A minimum level acts like a volume knob: because an enum compares by its number, the check is one line — if (level < MinimumLevel) return;.

Why is structured logging better than string concatenation?

  • It produces shorter text
  • It is faster to type
  • It encrypts the message
  • Named placeholder values are captured as searchable properties, not glued into flat text

Answer: Named placeholder values are captured as searchable properties, not glued into flat text. A message template like LogInformation("Order {OrderId}", id) stores OrderId as a queryable property; interpolation flattens it into unsearchable text.

What does the <T> in ILogger<T> provide?

  • The minimum log level
  • It tags every line with that class name as its category
  • The output file path
  • The number of providers

Answer: It tags every line with that class name as its category. ILogger<OrderProcessor> tags each log line with the class name as its 'category' automatically.

When logging an exception, where should the exception be passed?

  • As the first argument: LogError(ex, "...")
  • As the last argument
  • Inside the message string
  • It cannot be logged

Answer: As the first argument: LogError(ex, "..."). Pass the exception first — LogError(ex, "Failed {Id}", id) — so the full stack trace is recorded alongside the message.

In Microsoft.Extensions.Logging, what is a 'provider'?

  • A class that creates loggers only
  • The minimum level setting
  • An adapter that knows how to write a log event to one destination
  • A message template

Answer: An adapter that knows how to write a log event to one destination. A provider is a small adapter that writes to one destination (console, debug, event log). One log call fans out to every registered provider.

In Serilog terminology, what is a 'sink'?

  • A log level
  • A destination for log events (console, file, server)
  • A message template
  • A way to drop logs

Answer: A destination for log events (console, file, server). A sink is a Serilog destination for log events. It is the same idea that Microsoft.Extensions.Logging calls a provider.

Which of these must you NEVER log?

  • Order IDs and request IDs
  • The outcome of an operation
  • Exception stack traces
  • Passwords, API keys, tokens, and connection strings

Answer: Passwords, API keys, tokens, and connection strings. Never log secrets or PII. Logs are plain text copied to many sinks; log identifiers and masked values (e.g. 'card ending 4242') instead.

Why guard an expensive log argument with if (logger.IsEnabled(LogLevel.Debug))?

  • To change the log level
  • To avoid building a costly string the filter would only discard
  • To force a flush
  • To encrypt the argument

Answer: To avoid building a costly string the filter would only discard. Without the guard, an expensive argument (e.g. BuildHugeReport()) is computed even when Debug is filtered out. IsEnabled skips that work.

Why call Log.CloseAndFlush() on shutdown when using Serilog?

  • To change the minimum level
  • To open a new sink
  • Otherwise buffered log lines can be lost when the process exits
  • To reset the timestamp

Answer: Otherwise buffered log lines can be lost when the process exits. Serilog buffers events; calling CloseAndFlush() on shutdown ensures buffered lines are written instead of lost when the process ends.