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?
| Feature | Built-in Logger | Serilog |
|---|---|---|
| Structured logging | Limited | Full support |
| Log to file | Not built-in | Rolling file sink included |
| Log to database | Not built-in | Many sinks available |
| Readable format | Plain text | Configurable, rich templates |
| Performance | Good | Async, non-blocking |
Step 1 – Install Serilog Packages
dotnet add package Serilog.AspNetCore
dotnet add package Serilog.Sinks.Console
dotnet add package Serilog.Sinks.File
| Package | Purpose |
|---|---|
| Serilog.AspNetCore | Core Serilog integration for ASP.NET Core |
| Serilog.Sinks.Console | Write logs to the console (terminal) |
| Serilog.Sinks.File | Write 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
| Level | Method | When to Use | BookStore Example |
|---|---|---|---|
| Verbose | LogTrace() | Very detailed diagnostic info | Individual EF Core SQL queries |
| Debug | LogDebug() | Development diagnostics | Method entry/exit points |
| Information | LogInformation() | Normal operations | "Book created with Id 5" |
| Warning | LogWarning() | Unexpected but handled situations | "Book Id 999 not found" |
| Error | LogError() | Failures that need attention | "Database connection failed" |
| Critical | LogCritical() | 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.
