Core API Logging with Serilog

Every production API needs logging. When the BookStore API behaves unexpectedly — a book is missing, an error occurs, or a suspicious request pattern is detected — logs are the only way to understand what happened. Serilog is a popular structured logging library for .NET that writes readable, searchable log messages to files, consoles, and databases.

Why Serilog Over the Default Logger?

FeatureBuilt-in LoggerSerilog
Structured loggingLimitedFull support
Log to fileNot built-inRolling file sink included
Log to databaseNot built-inMany sinks available
Readable formatPlain textConfigurable, rich templates
PerformanceGoodAsync, non-blocking

Step 1 – Install Serilog Packages

dotnet add package Serilog.AspNetCore
dotnet add package Serilog.Sinks.Console
dotnet add package Serilog.Sinks.File
PackagePurpose
Serilog.AspNetCoreCore Serilog integration for ASP.NET Core
Serilog.Sinks.ConsoleWrite logs to the console (terminal)
Serilog.Sinks.FileWrite logs to files with rolling support

Step 2 – Configure Serilog in Program.cs

// Program.cs
using Serilog;

// Configure Serilog before the app builder
Log.Logger = new LoggerConfiguration()
    .MinimumLevel.Information()
    .MinimumLevel.Override("Microsoft", Serilog.Events.LogEventLevel.Warning)
    .MinimumLevel.Override("Microsoft.Hosting.Lifetime", Serilog.Events.LogEventLevel.Information)
    .Enrich.FromLogContext()
    .WriteTo.Console(outputTemplate:
        "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}")
    .WriteTo.File(
        path: "Logs/bookstore-.log",
        rollingInterval: RollingInterval.Day,
        outputTemplate: "[{Timestamp:yyyy-MM-dd HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}")
    .CreateLogger();

var builder = WebApplication.CreateBuilder(args);

// Replace default logger with Serilog
builder.Host.UseSerilog();

Step 3 – Add Configuration-Based Logging (Optional)

For environments where log levels differ, settings can be moved to appsettings.json:

// appsettings.json
{
  "Serilog": {
    "MinimumLevel": {
      "Default": "Information",
      "Override": {
        "Microsoft": "Warning",
        "System": "Warning"
      }
    },
    "WriteTo": [
      { "Name": "Console" },
      {
        "Name": "File",
        "Args": {
          "path": "Logs/bookstore-.log",
          "rollingInterval": "Day"
        }
      }
    ],
    "Enrich": [ "FromLogContext" ]
  }
}
// Program.cs – read Serilog config from appsettings.json
Log.Logger = new LoggerConfiguration()
    .ReadFrom.Configuration(builder.Configuration)
    .CreateLogger();

Step 4 – Using the Logger in Services and Controllers

Inject ILogger<T> into any class through the constructor:

// Services/BookService.cs
public class BookService : IBookService
{
    private readonly IBookRepository _bookRepository;
    private readonly ILogger<BookService> _logger;

    public BookService(IBookRepository bookRepository, ILogger<BookService> logger)
    {
        _bookRepository = bookRepository;
        _logger = logger;
    }

    public async Task<List<Book>> GetAllAsync()
    {
        _logger.LogInformation("Fetching all books from the database.");
        var books = await _bookRepository.GetAllAsync();
        _logger.LogInformation("Retrieved {Count} books.", books.Count);
        return books;
    }

    public async Task<Book> CreateAsync(Book book)
    {
        _logger.LogInformation("Creating new book: {Title} by {Author}", book.Title, book.Author);
        book.CreatedDate = DateTime.UtcNow;
        await _bookRepository.AddAsync(book);
        await _bookRepository.SaveAsync();
        _logger.LogInformation("Book created successfully with Id {Id}.", book.Id);
        return book;
    }

    public async Task<bool> DeleteAsync(int id)
    {
        _logger.LogInformation("Attempting to delete book with Id {Id}.", id);
        var book = await _bookRepository.GetByIdAsync(id);
        if (book == null)
        {
            _logger.LogWarning("Delete failed: Book with Id {Id} was not found.", id);
            return false;
        }
        await _bookRepository.DeleteAsync(book);
        await _bookRepository.SaveAsync();
        _logger.LogInformation("Book with Id {Id} deleted successfully.", id);
        return true;
    }
}

Log Levels

LevelMethodWhen to UseBookStore Example
VerboseLogTrace()Very detailed diagnostic infoIndividual EF Core SQL queries
DebugLogDebug()Development diagnosticsMethod entry/exit points
InformationLogInformation()Normal operations"Book created with Id 5"
WarningLogWarning()Unexpected but handled situations"Book Id 999 not found"
ErrorLogError()Failures that need attention"Database connection failed"
CriticalLogCritical()System-level failures"App cannot start — missing config"

Structured Logging — Why It Matters

Structured logging stores log data as key-value pairs, not just plain strings. This makes logs searchable and filterable in log analysis tools.

// Plain text logging (hard to search):
_logger.LogInformation("Book Clean Code by Robert C. Martin created with Id 3");

// Structured logging (searchable by Title, Author, Id):
_logger.LogInformation("Book {Title} by {Author} created with Id {Id}",
    book.Title, book.Author, book.Id);

Serilog stores this as:

{
  "Timestamp": "2024-01-15T10:30:00",
  "Level": "Information",
  "Message": "Book Clean Code by Robert C. Martin created with Id 3",
  "Title": "Clean Code",
  "Author": "Robert C. Martin",
  "Id": 3
}

Logging HTTP Requests Automatically

Serilog can log every HTTP request automatically with one line in Program.cs:

// Program.cs — add after app is built
app.UseSerilogRequestLogging(options =>
{
    options.MessageTemplate = "HTTP {RequestMethod} {RequestPath} responded {StatusCode} in {Elapsed:0.0000}ms";
});

Console output:

[10:35:22 INF] HTTP GET /api/books responded 200 in 12.5ms
[10:35:25 INF] HTTP POST /api/books responded 201 in 45.2ms
[10:35:30 WRN] HTTP GET /api/books/999 responded 404 in 8.1ms

Log File Output

With rolling file configuration, daily log files are created in the Logs/ folder:

Logs/
├── bookstore-20240115.log
├── bookstore-20240116.log
└── bookstore-20240117.log

Each file contains timestamped log entries:

[2024-01-15 10:30:00 INF] Fetching all books from the database.
[2024-01-15 10:30:00 INF] Retrieved 5 books.
[2024-01-15 10:35:00 INF] Creating new book: Refactoring by Martin Fowler
[2024-01-15 10:35:00 INF] Book created successfully with Id 6.
[2024-01-15 10:40:00 WRN] Delete failed: Book with Id 999 was not found.

Key Points

  • Serilog is a structured logging library that replaces ASP.NET Core's built-in logger with richer features.
  • Structured logging stores properties (like Title, Id) as named values, not just plain text — making logs searchable.
  • Serilog writes to multiple sinks simultaneously: console during development, files in production.
  • ILogger<T> is injected into services and controllers through DI — no static logger references are needed.
  • UseSerilogRequestLogging() automatically logs every HTTP request with method, path, status code, and elapsed time.

Leave a Comment