← back to knowledge-hub

Serilog and Seq in ASP.NET Core Web API (.NET 8)

While working on my ASP.NET Core Web API project, I hit a wall with local debugging. In production, I had Amazon CloudWatch — searchable, filterable, color-coded logs. Locally? A wall of console text that was nearly impossible to trace through. I needed something that gave me CloudWatch-level visibility on my own machine.

That’s when I found Seq. Paired with Serilog for structured logging, it gave me a clean, searchable dashboard for local development — and the setup took less than fifteen minutes.

Here’s the full walkthrough. You can grab the code from GitHub.

Why Serilog and Seq?

Serilog captures logs as structured data rather than plain text — meaning you can filter by request ID, log level, user ID, or any property you enrich your logs with. Seq takes that structured data and presents it in a dashboard with real-time search.

Why this combination works for me:

  • Structured logging — filter by any field, not just text search
  • Centralized dashboard — all logs in one place with color-coded severity
  • Fast search — Seq lets me find the exact error in seconds
  • Docker-based — consistent setup, runs anywhere
  • Rich context — Serilog enrichers add machine name, thread ID, environment automatically
  • Lightweight — doesn’t slow down my app, free for local dev

Step 1: Create the Project

1
2
dotnet new webapi -n SerilogSeqDemo -f net8.0
cd SerilogSeqDemo

This scaffolds a basic Web API with a sample WeatherForecast controller. We’ll add structured logging on top.

Step 2: Install NuGet Packages

1
2
3
4
5
6
dotnet add package Serilog.AspNetCore
dotnet add package Serilog.Sinks.Console
dotnet add package Serilog.Sinks.Seq
dotnet add package Serilog.Exceptions
dotnet add package Serilog.Enrichers.Environment
dotnet add package Serilog.Enrichers.Thread
  • Serilog.AspNetCore — hooks Serilog into the ASP.NET Core pipeline
  • Serilog.Sinks.Console — console output for all environments
  • Serilog.Sinks.Seq — sends logs to Seq
  • Serilog.Exceptions — detailed exception info including stack traces
  • Serilog.Enrichers.Environment — adds machine name and environment
  • Serilog.Enrichers.Thread — adds thread IDs for tracking concurrent operations

Step 3: Run Seq in Docker

Make sure Docker Desktop is running, then:

1
docker run --name seq -d --restart unless-stopped -e ACCEPT_EULA=Y -p 5341:80 datalust/seq:latest

Navigate to http://localhost:5341 — you should see the Seq dashboard. That’s it. Seq is ready to receive logs.

Step 4: Configure Serilog in Program.cs

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using Serilog;
using Serilog.Enrichers;
using Serilog.Events;

var builder = WebApplication.CreateBuilder(args);

// Set up Serilog
var loggerConfiguration = new LoggerConfiguration()
    .MinimumLevel.Information()
    .Enrich.FromLogContext()
    .Enrich.WithEnvironmentName()
    .Enrich.WithMachineName()
    .Enrich.WithThreadId()
    .WriteTo.Console();

// Use Seq only in DEBUG mode
#if DEBUG
    loggerConfiguration.WriteTo.Seq("http://localhost:5341");
#endif

Log.Logger = loggerConfiguration.CreateLogger();

builder.Host.UseSerilog();

builder.Services.AddControllers();

var app = builder.Build();

// Add Serilog request logging
app.UseSerilogRequestLogging();
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();

app.Run();

The key decision here: Seq only runs in DEBUG mode. In production, you’d point to a different Seq instance with API keys and retention policies. For local dev, the free single-user license is perfect.

Step 5: Create a Controller with Log Levels

I created a LogController.cs with endpoints that demonstrate each log level:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
using Microsoft.AspNetCore.Mvc;
using Serilog;

namespace SerilogSeqDemo.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class LogController : ControllerBase
    {
        private readonly ILogger<LogController> _logger;

        public LogController(ILogger<LogController> logger)
        {
            _logger = logger;
        }

        [HttpGet("info")]
        public IActionResult LogInformation()
        {
            _logger.LogInformation("Processing request to {Endpoint} for user {UserId}", nameof(LogInformation), "User123");
            return Ok("Information log recorded.");
        }

        [HttpGet("warning")]
        public IActionResult LogWarning()
        {
            _logger.LogWarning("Potential issue detected in {Endpoint}. Check configuration for {Setting}", nameof(LogWarning), "ApiKey");
            return Ok("Warning log recorded.");
        }

        [HttpGet("error")]
        public IActionResult LogError()
        {
            try
            {
                throw new InvalidOperationException("Simulated error in the system.");
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "An error occurred in {Endpoint} while processing request.", nameof(LogError));
                return StatusCode(500, "Error log recorded.");
            }
        }
    }
}

Notice the structured log templates — {Endpoint}, {UserId}, {Setting} become searchable properties in Seq, not just interpolated strings.

Seq all

Step 6: Run and Test

1
dotnet run --configuration Debug

Hit these endpoints:

  • https://localhost:<port>/api/log/info
  • https://localhost:<port>/api/log/warning
  • https://localhost:<port>/api/log/error

Open Seq at http://localhost:5341 and you’ll see each log entry with full structured data — endpoint name, user IDs, environment, thread IDs. Use Seq’s query bar to filter: Level = "Error" shows only errors, Endpoint = "LogInformation" narrows to specific endpoints.

Seq filter

Step 7: Enhance with Enrichers

For richer error context, add more properties to your log calls:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
[HttpGet("error")]
public IActionResult LogError()
{
    try
    {
        throw new InvalidOperationException("Simulated error in the system.");
    }
    catch (Exception ex)
    {
        _logger.LogError(ex, "An error occurred in {Endpoint} for user {UserId} with request {RequestId}", nameof(LogError), "User123", Request.HttpContext.TraceIdentifier);
        return StatusCode(500, "Error log recorded.");
    }
}

Now Seq shows the full picture: stack trace, user ID, request trace identifier, environment name, and thread ID — all filterable.

The Payoff

This setup gave me CloudWatch-level observability on my local machine. Debugging went from “scroll through console hoping to spot the error” to “type a query, find the exact log entry, see full context.” The fifteen-minute setup has saved me hours.

The full source is on GitHub. For production deployment, check the Seq documentation for API key configuration and retention policies.