Mert Özen Further With Every Line
Backend .NET Core Web API — #14

Logging: ILogger, Serilog and Structured Logging

Mert Özen Aug 8, 2026 12 min 81 views
Logging: ILogger, Serilog and Structured Logging

We cover logging: the basics of ILogger, log levels, the difference between plain-text and structured logging, and why Serilog makes this job much more powerful in serious projects.

In the previous article we caught errors in a central place and used a line like _logger.LogError(...) inside, but passed over it without dwelling. Today we open up the world behind that line. Because logging is something that seems unimportant at first glance but saves your life when an application goes live. When there's a problem, the only honest answer to the question "what happened?" is often hidden inside the logs.

Why Is Logging So Important?

While developing on your own computer, when something goes wrong, you see the error message on your screen and step through the code line by line. But when your application runs on a server, far away from you, you can't do any of these. At that moment, the only window you have is the logs. When the user says "the system crashed," only the logs can tell what happened.

Think of the log like an airplane's black box. When everything's fine, no one looks at it. But when there's a problem, it's the only way to go back and understand what happened and how. A good logging infrastructure is the difference between "something happened but I don't know what" and "it blew up at exactly this time, on this user, in this operation."

ILogger: .NET's Ready-Made Logging Tool

Good news: .NET comes with a ready-made infrastructure for logging. If you ask for the ILogger interface in any class's constructor, dependency injection gives it to you; no extra setup is needed. You use it inside a service like this:

public class UserService : IUserService
{
    private readonly ILogger _logger;

    public UserService(ILogger logger)
    {
        _logger = logger;
    }

    public async Task GetByIdAsync(int id)
    {
        _logger.LogInformation("Searching for user. Id: {UserId}", id);
        // ... database operation
        return null;
    }
}

There's a detail to note: we give the class name as a type parameter, in the form ILogger<UserService>. This automatically adds the information "this message came from UserService" to the log record; so when you look at the log, you instantly see which message came from where.

Log Levels: Not Every Message Is the Same

Not every log record has the same importance. A "user logged in" message shouldn't be treated as if it were on the same shelf as a "database crashed" message. So logs are divided into levels. The ones you'll use most are these:

LogTrace and LogDebug — The lowest levels; for following detail during development. Usually turned off in production.

LogInformation — Records the normal flow. Like "this operation was done, this request arrived." You see the application breathing.

LogWarning — Not a problem but a noteworthy situation. Like "slower response than expected" or "an unused path was tried."

LogError — An operation failed, an error occurred. This is what we used in the central catcher in the previous article.

LogCritical — The top level. For disasters that threaten the entire application; for example, if the database can't be reached at all.

Levels help you in two ways. First, in the live environment you can reduce log noise by saying "only record Warning and above." Second, while searching for a problem, you can filter directly to the Error level and separate what matters from the noise.

Plain Text or Structured Log?

Now we come to the most critical concept. Look again at how we wrote the log message in the example above:

_logger.LogInformation("Searching for user. Id: {UserId}", id);

Here we didn't paste the id value into the text as a string; instead, we used a placeholder named {UserId} and gave the value separately. This small difference creates the entire difference between plain-text logging and structured logging.

In plain text, the log is just a sentence: "Searching for user. Id: 42". A human reads it, understands it, done. But in structured logging, this record carries a separate field inside it, like UserId = 42. That is, the log is no longer just a sentence, but searchable and filterable data. One day, when you want to say "bring me all logs related to user number 42," in a structured log this is a single query; in plain text it means manually searching among thousands of lines.

Serilog: When You Take the Job Seriously

.NET's built-in ILogger is worth learning and quite enough for small projects. But when the job grows, you'll want to format logs properly, write them to a file, or even send them to different places (file, database, an external system) at the same time. This is where Serilog comes in; it's the most loved logging library in the .NET world and supports structured logging very powerfully.

The beauty of Serilog is that it doesn't change the ILogger interface. That is, inside your code you keep using the familiar _logger.LogInformation(...) calls; Serilog just manages where and how the logs are written in the background. After adding the packages, the setup in Program.cs looks roughly like this:

builder.Host.UseSerilog((context, config) =>
{
    config.WriteTo.Console();
    config.WriteTo.File("logs/app.txt", rollingInterval: RollingInterval.Day);
});

This tiny setting defines two targets: let the logs be written both to the console and saved to a separate file each day in the logs folder. Thanks to rollingInterval, each day's log is kept in a separate file; so the files don't reach enormous sizes and looking at a specific day becomes easy. Serilog has dozens of different targets (outputs it calls sinks); you can route logs to different places according to your need.

A Few Rules for Writing Good Logs

The tool matters, but how you write matters more. A few practical principles: Always give values with a placeholder, not with string concatenation; the power of structured logging comes from here. Never log sensitive information (passwords, tokens, personal data); log files can leak too. Write the log message clearly enough for the person who'll read it later (that is, future you) to understand. And keep the noise balanced: if you log everything, the important message gets lost; if you log nothing, you're empty-handed the moment there's a problem.

A Small Experiment

Add a few logs at different levels inside one of your services: LogInformation for a normal operation, LogWarning for a noteworthy situation, LogError for an error. Be sure to give values in the {Placeholder} form. Run the application and look at the log output in the console; see which level and which class each message came from. If you like, go one step further, set up Serilog, and observe that the logs are also written to a file. This experiment turns logging from an abstract concept into a concrete tool in your hands.

In the next article we'll move on to configuration and the Options pattern: we'll talk about how to properly manage settings like the log file path we embedded in the code in this article, in appsettings files. The time is coming to separate connection strings, file paths, and values that change by environment from the code.