BrandGhost
How to Set Up Serilog in ASP.NET Core: Step-by-Step Guide

How to Set Up Serilog in ASP.NET Core: Step-by-Step Guide

07/07/2026(Updated 07/13/2026)

Setting up Serilog in ASP.NET Core is straightforward once you understand the recommended pattern -- but there are a few gotchas that trip up developers the first time. The most common mistake is wiring Serilog in a way that misses startup exceptions, leaves the bootstrap logger running too long, or doesn't take advantage of appsettings.json-driven configuration.

This guide walks through the complete, production-ready Serilog setup for ASP.NET Core on .NET 9 and .NET 10: package installation, two-stage initialization, appsettings.json configuration, structured request logging middleware, and verification that everything is working.

Before following this guide, make sure you're familiar with the basics of Serilog in .NET and the broader context of logging in .NET.

Watch this video on Dev Leader

Step 1: Install the Required Packages

Start by installing the core Serilog packages for ASP.NET Core:

dotnet add package Serilog.AspNetCore
dotnet add package Serilog.Settings.Configuration

Serilog.AspNetCore provides UseSerilog() and the request logging middleware. Serilog.Settings.Configuration enables appsettings.json configuration. Then add the sinks you need:

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

For development with Seq:

dotnet add package Serilog.Sinks.Seq

For enrichers:

dotnet add package Serilog.Enrichers.Environment
dotnet add package Serilog.Enrichers.Thread

Step 2: Configure Two-Stage Initialization

The recommended pattern for Serilog setup in ASP.NET Core uses two-stage initialization. This solves a critical problem: if your application fails to start (bad configuration, missing dependency, database connection issue), you need a logger running before the host is built.

// Program.cs
using Serilog;

// Stage 1: Bootstrap logger -- captures errors before the host is built
Log.Logger = new LoggerConfiguration()
    .MinimumLevel.Override("Microsoft", LogEventLevel.Warning)
    .Enrich.FromLogContext()
    .WriteTo.Console()
    .CreateBootstrapLogger();

try
{
    Log.Information("Starting application");

    var builder = WebApplication.CreateBuilder(args);

    // Stage 2: Full logger -- reads from appsettings.json and DI services
    builder.Host.UseSerilog((context, services, configuration) =>
        configuration
            .ReadFrom.Configuration(context.Configuration)
            .ReadFrom.Services(services)
            .Enrich.FromLogContext());

    builder.Services.AddControllers();
    // other service registrations...

    var app = builder.Build();

    app.UseSerilogRequestLogging(); // Add before UseRouting/UseEndpoints

    app.UseRouting();
    app.UseAuthorization();
    app.MapControllers();

    app.Run();
}
catch (Exception ex)
{
    Log.Fatal(ex, "Application terminated unexpectedly");
    return 1;
}
finally
{
    // IMPORTANT: flush all buffered log events before the process exits
    await Log.CloseAndFlushAsync();
}

return 0;

Why Two-Stage?

Concern Bootstrap Logger Full Logger
Catches startup failures ✅ Yes ❌ Too late
appsettings.json config ❌ Not available yet ✅ Full access
Injected services (e.g., enrichers) ❌ Not registered yet ✅ Via ReadFrom.Services()
Runs Before builder.Build() After builder.Host.UseSerilog()

The try/catch/finally block ensures startup exceptions are logged to the bootstrap logger, and Log.CloseAndFlushAsync() in finally ensures all buffered log events are written before the process exits -- critical for async sinks (file, Seq, cloud services).


Step 3: Configure appsettings.json

Move your Serilog configuration from code into appsettings.json for per-environment flexibility. The full logger reads this via .ReadFrom.Configuration(context.Configuration).

appsettings.json (base configuration)

{
  "Serilog": {
    "Using": [
      "Serilog.Sinks.Console",
      "Serilog.Sinks.File"
    ],
    "MinimumLevel": {
      "Default": "Information",
      "Override": {
        "Microsoft": "Warning",
        "Microsoft.AspNetCore": "Warning",
        "Microsoft.EntityFrameworkCore": "Warning",
        "System": "Warning"
      }
    },
    "WriteTo": [
      {
        "Name": "Console",
        "Args": {
          "outputTemplate": "[{Timestamp:HH:mm:ss} {Level:u3}] {SourceContext}{NewLine}  {Message:lj}{NewLine}{Exception}"
        }
      },
      {
        "Name": "File",
        "Args": {
          "path": "logs/app-.log",
          "rollingInterval": "Day",
          "retainedFileCountLimit": 7,
          "outputTemplate": "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {SourceContext} {Message:lj}{NewLine}{Exception}"
        }
      }
    ],
    "Enrich": [
      "FromLogContext",
      "WithMachineName",
      "WithThreadId"
    ]
  }
}

appsettings.Development.json (override for local dev)

{
  "Serilog": {
    "MinimumLevel": {
      "Default": "Debug",
      "Override": {
        "Microsoft": "Information",
        "System": "Information"
      }
    },
    "WriteTo": [
      {
        "Name": "Console"
      },
      {
        "Name": "Seq",
        "Args": {
          "serverUrl": "http://localhost:5341"
        }
      }
    ]
  }
}

appsettings.Production.json (cloud/container configuration)

{
  "Serilog": {
    "MinimumLevel": {
      "Default": "Information",
      "Override": {
        "Microsoft": "Warning",
        "System": "Warning"
      }
    },
    "WriteTo": [
      {
        "Name": "Console",
        "Args": {
          "formatter": "Serilog.Formatting.Compact.CompactJsonFormatter, Serilog.Formatting.Compact"
        }
      }
    ]
  }
}

For production containers, writing JSON to stdout lets your log aggregator (Fluent Bit, Logstash, Vector, Azure Monitor) parse structured properties without regex extraction.


Step 4: Add Serilog Request Logging

ASP.NET Core's default request logging emits multiple log entries per request -- one for routing, one for the controller, one for the response. Replace these with a single, structured entry per request:

// In Program.cs, before UseRouting
app.UseSerilogRequestLogging(options =>
{
    // Customize the message template
    options.MessageTemplate =
        "HTTP {RequestMethod} {RequestPath} responded {StatusCode} in {Elapsed:0.0000} ms";

    // Control log level based on response
    options.GetLevel = (httpContext, elapsed, ex) =>
    {
        if (ex != null || httpContext.Response.StatusCode >= 500)
            return LogEventLevel.Error;
        if (httpContext.Response.StatusCode >= 400 || elapsed > 2000)
            return LogEventLevel.Warning;
        return LogEventLevel.Information;
    };

    // Add extra properties per request
    options.EnrichDiagnosticContext = (diagnosticContext, httpContext) =>
    {
        diagnosticContext.Set("RequestHost", httpContext.Request.Host.Value);
        diagnosticContext.Set("RequestScheme", httpContext.Request.Scheme);
        diagnosticContext.Set("UserAgent", httpContext.Request.Headers.UserAgent.ToString());
        if (httpContext.User.Identity?.IsAuthenticated == true)
        {
            diagnosticContext.Set("UserId", httpContext.User.FindFirst("sub")?.Value ?? "unknown");
        }
    };
});

This produces one line per request like:

[14:23:01 INF] HTTP GET /api/orders responded 200 in 12.4 ms

With structured properties: RequestMethod=GET, RequestPath=/api/orders, StatusCode=200, Elapsed=12.4, RequestHost=localhost, UserId=user-123.


Step 5: Using ILogger in Your Application

With Serilog configured, inject ILogger<T> exactly as you normally would. Serilog transparently receives all log events routed through the ILogger bridge:

public class OrderController : ControllerBase
{
    private readonly ILogger<OrderController> _logger;
    private readonly IOrderService _orderService;

    public OrderController(
        ILogger<OrderController> logger,
        IOrderService orderService)
    {
        _logger = logger;
        _orderService = orderService;
    }

    [HttpPost]
    public async Task<IActionResult> CreateOrder(CreateOrderRequest request)
    {
        _logger.LogInformation(
            "Creating order for customer {CustomerId} with {ItemCount} items",
            request.CustomerId,
            request.Items.Count);

        try
        {
            var order = await _orderService.CreateAsync(request);

            _logger.LogInformation(
                "Order {OrderId} created successfully for customer {CustomerId}",
                order.Id,
                request.CustomerId);

            return Created($"/api/orders/{order.Id}", order);
        }
        catch (ValidationException ex)
        {
            _logger.LogWarning(ex,
                "Order validation failed for customer {CustomerId}: {ValidationErrors}",
                request.CustomerId,
                string.Join(", ", ex.Errors));

            return BadRequest(ex.Errors);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex,
                "Unexpected error creating order for customer {CustomerId}",
                request.CustomerId);

            return StatusCode(500);
        }
    }
}

Note the message templates: {CustomerId}, {ItemCount}, {OrderId} become structured properties that Seq and other log aggregators can filter and query.


Step 6: Verify the Setup

Run your application and check that Serilog is working:

dotnet run

You should see output like:

[14:23:00 INF] Starting application
[14:23:00 INF] Now listening on: https://localhost:7001
[14:23:01 INF] HTTP GET /api/health responded 200 in 5.2 ms
[14:23:01 INF] Creating order for customer cust-456 with 3 items
[14:23:01 INF] Order ord-789 created successfully for customer cust-456

If you're using Seq locally:

docker run -d --name seq -e ACCEPT_EULA=Y -p 5341:80 datalust/seq

Then navigate to http://localhost:5341 to query your structured log properties.


Common Setup Issues

Serilog not logging anything

The most common cause is missing Log.CloseAndFlushAsync() -- buffered sinks haven't flushed before the process exits (particularly in short-lived test runs). For tests, call Log.CloseAndFlush() explicitly.

Startup exceptions not appearing

If your application crashes before builder.Host.UseSerilog() runs, the bootstrap logger handles it. Verify the try/catch/finally block wraps all of Program.cs, not just app.Run().

appsettings.json overrides not applying

The Serilog:Using array in appsettings.json must list the assembly names (not NuGet package names) of the sinks you're configuring. For example, Serilog.Sinks.Seq in Using corresponds to the Serilog.Sinks.Seq NuGet package.

High log volume in development

Override minimum levels in appsettings.Development.json to Debug for your own namespaces but keep Microsoft.* and System.* at Warning to avoid noise from framework internals.


Patterns in Serilog Setup

The two-stage initialization approach mirrors the Template Method pattern -- the bootstrap phase defines the skeleton (simple console sink), and the full initialization fills in the details from configuration. The sink pipeline itself follows the Chain of Responsibility: each sink in the WriteTo array independently processes every log event that meets its level threshold.


Frequently Asked Questions

What is the correct order of middleware for Serilog request logging?

Place app.UseSerilogRequestLogging() before UseRouting(), UseAuthorization(), and endpoint mapping. This ensures the middleware captures timing for the full pipeline including authentication and routing overhead.

Should I call Log.CloseAndFlush() or Log.CloseAndFlushAsync()?

Prefer await Log.CloseAndFlushAsync() in top-level Program.cs using the await top-level statement pattern in .NET 6+. Use Log.CloseAndFlush() in contexts where async isn't available (legacy Main methods, test teardown).

Can I use Serilog without appsettings.json configuration?

Yes. Full code-based configuration with .WriteTo.Console(), .WriteTo.File(), etc. works fine. However, appsettings.json configuration is recommended for ASP.NET Core because it supports per-environment overrides without recompiling.

Do I need Serilog.Settings.Configuration?

Only if you want appsettings.json configuration. If you're configuring Serilog entirely in code, you can skip this package. For most ASP.NET Core apps, appsettings.json configuration is strongly recommended.

How do I suppress specific framework log messages?

Use MinimumLevel.Override() in your configuration. For example, "Microsoft.EntityFrameworkCore.Database.Command": "Warning" suppresses EF Core SQL query logs. The Override section accepts any namespace prefix and applies to all loggers in that namespace.

How do I add Serilog to a .NET Worker Service?

The same pattern applies. Call builder.Services.AddLogging() is implicit, and builder.Host.UseSerilog(...) configures Serilog as the provider. Worker services don't have HTTP request logging, so skip UseSerilogRequestLogging().

What is the Serilog.AspNetCore package?

Serilog.AspNetCore is the main integration package. It provides the UseSerilog() host extension, UseSerilogRequestLogging() middleware, CreateBootstrapLogger() for two-stage initialization, and integration with ASP.NET Core's IDiagnosticContext for per-request structured properties.


Summary

Setting up Serilog in ASP.NET Core comes down to five steps: install packages, configure two-stage initialization in Program.cs, set up appsettings.json with per-environment overrides, add UseSerilogRequestLogging() middleware, and inject ILogger<T> as normal.

With this foundation in place, your application captures structured log data that you can query in Seq, export to cloud logging services, and use to diagnose issues with precision.

Continue with the series to learn about Serilog sinks for writing to multiple output destinations.

Serilog in .NET: Complete Guide to Structured Logging

Learn Serilog in .NET from scratch. Setup, sinks, enrichers, appsettings configuration, and best practices for ASP.NET Core with .NET 9 and .NET 10.

Serilog Enrichers: Adding Context to Every Log Entry

Master Serilog enrichers in .NET: add machine name, thread ID, correlation IDs, and custom properties to every log entry automatically. Complete C# guide.

Serilog vs Microsoft.Extensions.Logging: Which Should You Use?

Serilog vs Microsoft.Extensions.Logging: understand the difference between an abstraction and a provider, when MEL is enough, and when Serilog adds real value in .NET.

An error has occurred. This application may no longer respond until reloaded. Reload