In .NET, we have a first-class Logging system built-in, based on the ILogger interface.
When we are learning to program, our main tool to know what’s happening is Console.WriteLine("Got here").
But when we move to a “real” application, using the console is a terrible practice.
- ❌ You have no control over what is shown and what is not (levels).
- ❌ You have no context (when did it happen? In which thread?).
- ❌ It’s not easy to search or export to a file or database.
Today we will learn how to improve logging in our application so that when something fails in production, we know exactly why.
Where do logs go?
By default (builder.CreateBuilder), .NET configures:
- Console: Standard output (useful for containers/Docker).
- Debug: Visual Studio Output window.
- EventLog: On Windows (only if you run on Windows).
If you want to save logs to a text file, .NET does not come with a native file provider (curiously). For that, the industry usually uses very famous third-party libraries like Serilog or NLog, which integrate perfectly over ILogger.
The logging architecture
.NET’s logging system is based on three pillars:
- ILogger (The interface): What we use in our code to write messages.
- Providers (The destinations): They are responsible for “listening” to those messages and saving them. By default, .NET writes to the Console and the Visual Studio Debug window. But you can add providers to write to files, Azure, AWS, ElasticSearch, etc.
- Log Levels: They categorize the importance of the message.
Log Levels
Not everything that happens in the app has the same importance. .NET defines 6 standard levels. Understanding them is important to avoid saturating your logs with “noise”.
| Level | Method | Usage |
|---|---|---|
| Trace | LogTrace | Minute details. A lot of noise. (e.g., step-by-step of an algorithm). |
| Debug | LogDebug | Information useful for the developer while programming. |
| Information | LogInformation | The standard. Things that happen normally (e.g., “User registered”, “Email sent”). |
| Warning | LogWarning | Something strange happened, but the app recovered (e.g., “Retrying database connection”). |
| Error | LogError | Failure. The current request failed (e.g., “NullReference Exception”, “Database down”). |
| Critical | LogCritical | The entire application is in danger (e.g., “Disk full”, “Memory exhausted”). |
Using ILogger in your code
To use logging, we simply request ILogger<T> in the constructor of our class (or in the endpoint). The T is usually the class itself, to know who is writing the message.
app.MapGet("/product/{id}", (int id, ILogger<Program> logger) =>
{
logger.LogInformation("Requesting product with ID: {Id}", id);
// Simulate logic...
if (id < 0)
{
logger.LogWarning("Attempted access with negative ID: {Id}", id);
return Results.BadRequest();
}
return Results.Ok(new { Id = id });
});
public class PaymentService
{
private readonly ILogger<PaymentService> _logger;
// Inject the specific logger for this class
public PaymentService(ILogger<PaymentService> logger)
{
_logger = logger;
}
public void ProcessPayment()
{
_logger.LogInformation("Starting payment process...");
try
{
// Complex logic
}
catch(Exception ex)
{
// NOTE: We pass the exception as the first parameter
_logger.LogError(ex, "Critical error processing payment");
}
}
}
Structured Logging
Notice how I wrote the message earlier:
// ✅ GOOD: Structured Logging
logger.LogInformation("Requesting product with ID: {Id}", id);
And notice how it should NOT be done:
// ❌ BAD: String concatenation
logger.LogInformation($"Requesting product with ID: {id}");
What is the difference?
- In the BAD one, a final string is generated:
"Requesting product with ID: 5". It’s just plain text. - In the GOOD one, a template and the data are saved separately.
This allows that, if we send the logs to an intelligent system (like Application Insights, ElasticSearch, or Seq), we can perform searches like: SELECT * WHERE Id > 100
Configuring in appsettings.json
We don’t want to see Trace logs in production, but we do want to see them while developing. This is controlled in appsettings.json, in the Logging section.
{
"Logging": {
"LogLevel": {
"Default": "Information", // By default, show me Info and above
"Microsoft.AspNetCore": "Warning" // Silence Microsoft!
}
}
}
Notice the line "Microsoft.AspNetCore": "Warning". The ASP.NET framework is very “chatty”. If you set it to Information, it will tell you every time it enters and exits a middleware.
In production, we usually raise it to Warning to see only our logs and the framework’s errors.
