#csharp #dotnet #backend

Day 28: Logging & Monitoring

Welcome to Day 28. When an exception brings down your production server, users will complain, but they won’t give you the stack trace. The only way you can trace the issue is through Logs.

ASP.NET Core has an excellent built-in logging provider (ILogger), but it usually only prints to the Console during development. In production, you need logs streamed to files or cloud services.

The ILogger Interface

You can ask for an ILogger<T> anywhere via Dependency Injection. The generic <T> is the class that is doing the logging.

app.MapPost("/orders", (Order request, ILogger<Program> logger) => 
{
    // Log levels from least to most severe:
    // Trace -> Debug -> Information -> Warning -> Error -> Critical
    
    logger.LogInformation("Processing order for User {UserId} at {Time}", 
        request.UserId, DateTime.UtcNow);
        
    try 
    {
        // ... process ...
    }
    catch (Exception ex)
    {
        // Passing the exception object automatically logs the massive stack trace!
        logger.LogError(ex, "Failed to process order for {UserId}", request.UserId);
        return Results.Problem();
    }
    
    return Results.Ok();
});

Pro Tip: Notice we didn’t use $"UserId {request.UserId}" string interpolation inside the log method. Always use Structured Logging ({UserId}) so logging databases can query and sort by those specific JSON properties!

Enter Serilog (The King of .NET Logging)

The default Console logger is slow and barebones. Almost every professional .NET application replaces it with Serilog.

Serilog allows you to seamlessly configure “Sinks” (where the logs go). You can send logs to the Console, a daily rolling .txt file, ElasticSearch, Application Insights, or Datadog with just one line of code!

Setting Up Serilog

First, install the main package and the sinks you want:

dotnet add package Serilog.AspNetCore
dotnet add package Serilog.Sinks.File
dotnet add package Serilog.Sinks.Console

Then, configure it immediately at the absolute top of Program.cs. We literally hijack the startup sequence so if the builder crashes, Serilog catches it!

using Serilog;

Log.Logger = new LoggerConfiguration()
    .MinimumLevel.Information()         // Control verbosity
    .WriteTo.Console()                  // Sink 1: The terminal
    .WriteTo.File("logs/api-log-.txt", rollingInterval: RollingInterval.Day) // Sink 2: A text file per day
    .CreateLogger();

try 
{
    Log.Information("Starting web server...");
    
    var builder = WebApplication.CreateBuilder(args);
    
    // Completely replace the default Microsoft logger with Serilog!
    builder.Host.UseSerilog(); 
    
    var app = builder.Build();
    app.MapGet("/", () => "Hello");
    app.Run();
}
catch (Exception ex) 
{
    // Catch massive startup failures (like totally invalid appsettings.json files)
    Log.Fatal(ex, "Server terminated unexpectedly!");
}
finally 
{
    Log.CloseAndFlush();
}

Challenge for Day 28

Implement Serilog with the Console Sink into an ASP.NET Core API. Add it to appsettings.Development.json configuration so that you can change the .MinimumLevel from Information to Warning without recompiling your app!

Tomorrow: Unit Testing!