Learnixo
Back to blog
AI Systemsintermediate

Serilog — Structured Logging, Enrichers, and Sinks in Clean Architecture

How to configure Serilog in a Clean Architecture .NET project: structured logging, request logging middleware, enrichers for correlation and user context, multiple sinks, and the production mistakes that make logs unsearchable.

Asma Hafeez KhanMay 16, 20264 min read
Clean Architecture.NETSerilogLoggingStructured LoggingObservability
Share:𝕏

Why Structured Logging

Unstructured log:
  "Patient MRN-001 updated by user john@hospital.org at 2026-05-16 10:32:11"

Structured log (Serilog):
  {
    "Level": "Information",
    "Timestamp": "2026-05-16T10:32:11.345Z",
    "MessageTemplate": "Patient {MRN} updated by {Email}",
    "MRN": "MRN-001",
    "Email": "john@hospital.org",
    "CorrelationId": "req-abc123",
    "UserId": "user-456"
  }

Structured logs are searchable, filterable, and machine-readable. You can query Seq or Datadog for all events where MRN = "MRN-001" without parsing strings.

Production issue I've seen: A team used Console.WriteLine and string.Format for logging. When a clinical audit required all actions taken on a specific patient in the last 30 days, someone had to grep through unstructured log files and manually parse timestamps and patient IDs. With structured logging, this is a single query in Seq: SELECT * WHERE MRN = 'MRN-001' AND Timestamp > now() - 30d.


Installation

XML
<!-- Api.csproj -->
<PackageReference Include="Serilog.AspNetCore" Version="9.*" />
<PackageReference Include="Serilog.Sinks.Console" Version="6.*" />
<PackageReference Include="Serilog.Sinks.Seq" Version="9.*" />
<PackageReference Include="Serilog.Enrichers.Environment" Version="3.*" />
<PackageReference Include="Serilog.Enrichers.Thread" Version="4.*" />
<PackageReference Include="Serilog.Enrichers.CorrelationId" Version="3.*" />

Program.cs Configuration

C#
// Api/Program.cs
using Serilog;

// Bootstrap logger for startup errors (before host is built)
Log.Logger = new LoggerConfiguration()
    .WriteTo.Console()
    .CreateBootstrapLogger();

try
{
    var builder = WebApplication.CreateBuilder(args);

    builder.Host.UseSerilog((context, services, config) =>
        config.ReadFrom.Configuration(context.Configuration)
              .ReadFrom.Services(services)
              .Enrich.FromLogContext()
              .Enrich.WithMachineName()
              .Enrich.WithThreadId()
              .Enrich.WithCorrelationId());

    // ... AddApplication, AddInfrastructure, etc.

    var app = builder.Build();

    app.UseSerilogRequestLogging(options =>
    {
        options.MessageTemplate =
            "HTTP {RequestMethod} {RequestPath} responded {StatusCode} in {Elapsed:0.0000}ms";

        options.EnrichDiagnosticContext = (diagnosticContext, httpContext) =>
        {
            diagnosticContext.Set("RequestHost", httpContext.Request.Host.Value);
            diagnosticContext.Set("UserAgent", httpContext.Request.Headers["User-Agent"]);
            diagnosticContext.Set("UserId", httpContext.User?.FindFirst(ClaimTypes.NameIdentifier)?.Value);
        };
    });

    app.Run();
}
catch (Exception ex)
{
    Log.Fatal(ex, "Application startup failed.");
}
finally
{
    Log.CloseAndFlush();
}

appsettings.json Serilog Configuration

JSON
{
  "Serilog": {
    "Using":  ["Serilog.Sinks.Console", "Serilog.Sinks.Seq"],
    "MinimumLevel": {
      "Default": "Information",
      "Override": {
        "Microsoft":                      "Warning",
        "Microsoft.EntityFrameworkCore":  "Warning",
        "System":                         "Warning"
      }
    },
    "WriteTo": [
      {
        "Name": "Console",
        "Args": {
          "theme": "Serilog.Sinks.SystemConsole.Themes.AnsiConsoleTheme::Code, Serilog.Sinks.Console",
          "outputTemplate": "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj} {Properties:j}{NewLine}{Exception}"
        }
      },
      {
        "Name": "Seq",
        "Args": {
          "serverUrl": "http://localhost:5341",
          "restrictedToMinimumLevel": "Information"
        }
      }
    ],
    "Enrich": ["FromLogContext", "WithMachineName", "WithThreadId"]
  }
}

Logging in Handlers

C#
// Application/Patients/Commands/CreatePatient/CreatePatientCommandHandler.cs
public sealed class CreatePatientCommandHandler
{
    private readonly IPatientRepository _patients;
    private readonly IUnitOfWork _unitOfWork;
    private readonly ILogger<CreatePatientCommandHandler> _logger;

    public CreatePatientCommandHandler(
        IPatientRepository patients,
        IUnitOfWork unitOfWork,
        ILogger<CreatePatientCommandHandler> logger)
    {
        _patients   = patients;
        _unitOfWork = unitOfWork;
        _logger     = logger;
    }

    public async Task<Result<PatientId>> Handle(
        CreatePatientCommand command, CancellationToken ct)
    {
        _logger.LogInformation(
            "Registering patient with MRN {MRN}", command.MRN);

        if (await _patients.ExistsByMRNAsync(command.MRN, ct))
        {
            _logger.LogWarning(
                "Registration rejected: MRN {MRN} already exists", command.MRN);
            return Result.Failure<PatientId>(PatientErrors.MRNAlreadyExists);
        }

        var result = Patient.Create(command.Name, command.DateOfBirth, command.MRN);
        if (result.IsFailure)
        {
            _logger.LogWarning(
                "Patient creation failed: {ErrorCode} — {ErrorDescription}",
                result.Error.Code, result.Error.Description);
            return Result.Failure<PatientId>(result.Error);
        }

        await _patients.AddAsync(result.Value, ct);
        await _unitOfWork.SaveChangesAsync(ct);

        _logger.LogInformation(
            "Patient {PatientId} registered with MRN {MRN}",
            result.Value.Id.Value, command.MRN);

        return Result.Success(result.Value.Id);
    }
}

Adding User Context to All Logs

C#
// Api/Middleware/LogEnrichmentMiddleware.cs
public sealed class LogEnrichmentMiddleware
{
    private readonly RequestDelegate _next;

    public LogEnrichmentMiddleware(RequestDelegate next) => _next = next;

    public async Task InvokeAsync(HttpContext context)
    {
        using (LogContext.PushProperty("UserId",
            context.User?.FindFirst(ClaimTypes.NameIdentifier)?.Value))
        using (LogContext.PushProperty("UserEmail",
            context.User?.FindFirst(ClaimTypes.Email)?.Value))
        using (LogContext.PushProperty("CorrelationId",
            context.Request.Headers["X-Correlation-Id"].FirstOrDefault()
            ?? context.TraceIdentifier))
        {
            await _next(context);
        }
    }
}

// All log entries written during the request will include UserId, UserEmail, CorrelationId

Avoiding Common Logging Mistakes

C#
// ✗ String concatenation — not structured, not queryable
_logger.LogInformation("Patient " + command.MRN + " registered");

// ✓ Message template — structured, indexable by sink
_logger.LogInformation("Patient {MRN} registered", command.MRN);

// ✗ Logging PII in production
_logger.LogDebug("Patient full name: {Name}, DOB: {DateOfBirth}", patient.Name, patient.DateOfBirth);
// In healthcare systems: DOB and full name may be PII/PHI — be careful what goes to external sinks

// ✓ Log identifiers, not content
_logger.LogDebug("Patient {PatientId} data retrieved", patient.Id.Value);

PRO TIP: In healthcare systems governed by HIPAA or similar regulations, patient names, dates of birth, and diagnosis codes are Protected Health Information (PHI). Log identifiers (patient ID, MRN) — not identifiable data — unless your log storage is explicitly approved for PHI and protected accordingly.


Key Takeaway

Structured logging is the difference between an audit trail you can query and one you have to grep. Serilog's message templates ({MRN}, {PatientId}) are not just format strings — they are named properties stored alongside the log entry. UseSerilogRequestLogging replaces ASP.NET's noisy request logging with one clean line per request. Enrichers add context (correlation ID, machine name, user ID) to every log entry automatically.

Enjoyed this article?

Explore the AI Systems learning path for more.

Found this helpful?

Share:𝕏

Leave a comment

Have a question, correction, or just found this helpful? Leave a note below.