Core Concepts

Serilog is a structured logging library for .NET. Instead of writing flat text lines, it captures each log event as an object with named properties. Seq is a log server designed specifically to store, index, and query those structured events. Together they turn logs from grep-able text files into a queryable database.

Structured logging vs. text logging

Classic loggers concatenate values into a single string. Once written, the values are lost inside that string. Structured logging keeps the values as first-class fields you can filter, sort, and aggregate on.

Text log "Order 4211 for user alice" "placed at 12:03:11 total $84.20" Everything is a string. You grep, you parse, you cry. Structured event OrderId: 4211 User: "alice" Total: 84.20 Timestamp:2026-07-31T12:03:11Z Filter: Total > 50 Aggregate: sum(Total) by User
A structured event keeps every value queryable. The message template stays for humans.

Message templates

Serilog uses message templates, not format strings. Placeholders are named, and Serilog stores each substituted value as a property on the event.

csharp
// String.Format style — DON'T DO THIS
_logger.LogInformation($"Order {orderId} placed by {user}");

// Message template — YES
_logger.LogInformation("Order {OrderId} placed by {User}", orderId, user);

The first form flattens everything to text before Serilog sees it — you lose OrderId and User as properties. The second form keeps them structured. This is the single most important rule in Serilog.

Sinks

A sink is a destination for log events. Serilog has one core library and dozens of sink packages: Console, File, Seq, Elasticsearch, Application Insights, PostgreSQL, and so on. You can attach multiple sinks and each one can have its own minimum level and filter.

Serilog vs. Microsoft.Extensions.Logging

ASP.NET Core exposes ILogger<T> through the built-in Microsoft.Extensions.Logging abstraction. Serilog plugs in underneath that abstraction: your code keeps injecting ILogger<T>, and Serilog becomes the provider that actually writes the events. You get structured logging without changing every log call site.

Your code ILogger<T> MEL abstraction ILoggerFactory Serilog SerilogLoggerProvider Seq File Console sinks
Serilog installs as an MEL provider. Your ILogger<T> calls flow through it to every configured sink.

What Seq is

Seq is a self-hosted log server. It ingests JSON-formatted events, indexes their properties, and gives you a query language that reads like SQL over structured data. It runs as a single Windows service, a Docker container, or a Linux daemon. Free for individuals; paid for teams.

FeatureWhat it gives you
Ingestion APIHTTP endpoint that accepts batches of JSON events from any Serilog Seq sink or plain HTTP.
Property indexingEvery property in every event is queryable. No schema up front.
Query languageSQL-like syntax: select count(*) from stream where Level = 'Error' and Application = 'OrderService'.
SignalsSaved filters. "Errors in OrderService today" becomes a one-click view.
DashboardsCharts driven by queries.
AlertsTrigger Slack/email/webhook when a query matches.
API keysPer-application tokens that identify the source and can force properties on events. Covered in depth in section 6.
💡
Mental model: Serilog captures rich events. Seq is the database that stores and searches them. The API key is the badge each app wears so Seq knows who sent what.

Setup

NuGet packages

shell
# Core Serilog + ASP.NET Core integration
dotnet add package Serilog.AspNetCore

# Read logger config from appsettings.json
dotnet add package Serilog.Settings.Configuration

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

# Common enrichers
dotnet add package Serilog.Enrichers.Environment
dotnet add package Serilog.Enrichers.Process
dotnet add package Serilog.Enrichers.Thread

Serilog.AspNetCore is the meta package. It brings Serilog, Serilog.Extensions.Hosting, and the middleware that hooks Serilog into the generic host and request pipeline.

Minimal Program.cs

csharp
using Serilog;

// Bootstrap logger — used during startup, before the host is built.
// Catches any failure in Startup itself.
Log.Logger = new LoggerConfiguration()
    .WriteTo.Console()
    .CreateBootstrapLogger();

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

    // Replace the default MEL provider with Serilog, and read the real
    // configuration from appsettings.json.
    builder.Host.UseSerilog((context, services, config) => config
        .ReadFrom.Configuration(context.Configuration)
        .ReadFrom.Services(services)
        .Enrich.FromLogContext());

    var app = builder.Build();

    // Adds request logging: one structured event per HTTP request with
    // method, path, status code, and elapsed ms.
    app.UseSerilogRequestLogging();

    app.MapGet("/", () => "Hello");
    app.Run();
}
catch (Exception ex)
{
    Log.Fatal(ex, "Application terminated unexpectedly");
}
finally
{
    Log.CloseAndFlush();
}
⚠️
Always call Log.CloseAndFlush(). Serilog batches events for performance. If your process exits without flushing, the last few seconds of logs — often the ones you need most — are lost.

appsettings.json (source of truth)

json
{
  "Serilog": {
    "Using": [
      "Serilog.Sinks.Console",
      "Serilog.Sinks.Seq",
      "Serilog.Sinks.File"
    ],
    "MinimumLevel": {
      "Default": "Information",
      "Override": {
        "Microsoft": "Warning",
        "Microsoft.Hosting.Lifetime": "Information",
        "System": "Warning"
      }
    },
    "Enrich": [ "FromLogContext", "WithMachineName", "WithThreadId", "WithEnvironmentName" ],
    "Properties": {
      "Application": "OrderService"
    },
    "WriteTo": [
      { "Name": "Console" },
      {
        "Name": "File",
        "Args": {
          "path": "logs/app-.log",
          "rollingInterval": "Day",
          "retainedFileCountLimit": 14
        }
      },
      {
        "Name": "Seq",
        "Args": {
          "serverUrl": "https://seq.internal.example.com",
          "apiKey": "REPLACE_ME_WITH_APP_SPECIFIC_KEY"
        }
      }
    ]
  }
}
Keep the apiKey out of source control. Set it via Serilog:WriteTo:2:Args:apiKey as an environment variable or a secret manager entry. Example: SERILOG__WRITETO__2__ARGS__APIKEY=abc123.

Self-log (debugging Serilog itself)

When Serilog fails to write — bad config, unreachable Seq server, disk full — it swallows the exception by design so it never crashes your app. Enable SelfLog to see those errors during development.

csharp
// Very first line of Program.cs, before any logger call
Serilog.Debugging.SelfLog.Enable(msg => Console.Error.WriteLine(msg));

Structured Logging

Named placeholders

Every placeholder in a message template becomes a property. Pick names in PascalCase — that's the convention Seq expects and it keeps queries clean.

csharp
_logger.LogInformation(
    "Charged {Amount} to card ending {Last4} for order {OrderId}",
    84.20m, "1234", 4211);

// In Seq you can now query:
//   Amount > 50 and Last4 = '1234'

Destructuring with @

Prefixing a placeholder with @ tells Serilog to serialize the whole object as structured data instead of calling ToString() on it.

csharp
var order = new { Id = 4211, Total = 84.20m, Items = 3 };

// Without @: "Placed order { Id = 4211, Total = 84.20, Items = 3 }"
_logger.LogInformation("Placed order {Order}", order);

// With @: Order becomes a nested object in Seq — queryable as Order.Total > 50
_logger.LogInformation("Placed order {@Order}", order);

Force scalar with $

The opposite of @. Prefix with $ to force ToString(), even for types Serilog would otherwise destructure.

csharp
_logger.LogInformation("User connected from {$Endpoint}", ipEndpoint);

LogContext — properties scoped to a block

LogContext.PushProperty attaches a property to every event logged inside the using block, on the current async flow. Perfect for per-request data.

csharp
using (LogContext.PushProperty("CustomerId", customerId))
using (LogContext.PushProperty("Correlation", Guid.NewGuid()))
{
    _logger.LogInformation("Processing checkout");   // has CustomerId + Correlation
    await _payments.ChargeAsync(...);                // any logs inside also have them
    _logger.LogInformation("Checkout complete");
}
⚠️
LogContext only works if you enabled .Enrich.FromLogContext() in the logger configuration. If your pushed properties never appear in Seq, that's why.

BeginScope — the MEL equivalent

If you prefer ILogger.BeginScope (the MEL abstraction), Serilog picks it up and turns scope values into structured properties automatically.

csharp
using (_logger.BeginScope(new Dictionary<string, object>
{
    ["CustomerId"] = customerId,
    ["Correlation"] = correlationId
}))
{
    _logger.LogInformation("Processing checkout");
}

Log levels

LevelWhen to use
VerboseVery noisy trace. Off in production.
DebugDiagnostic detail. Off in production by default.
InformationNormal operational events: requests, state transitions, key business actions.
WarningSomething unexpected but recoverable. Deserves attention, not a page.
ErrorA failure that affected the operation. Something broke.
FatalThe process is going down. Last-gasp log before crash.

Common anti-patterns

🚫
String interpolation in the template. _logger.LogInformation($"Order {id}") destroys the property. Always: _logger.LogInformation("Order {Id}", id).
🚫
Logging the exception message instead of the exception. Pass the exception as the first argument: _logger.LogError(ex, "Failed to charge {OrderId}", id). Serilog captures the stack trace, inner exceptions, and data dictionary.
🚫
Destructuring everything. {@HttpRequest} or {@DbContext} serializes graphs you didn't intend. Log the fields that matter, not the object.

Enrichers

Enrichers add properties to every log event automatically. You don't want to type MachineName on every log call — an enricher does it once.

Built-in enrichers

EnricherPackageAdds
FromLogContextcoreEverything pushed via LogContext.PushProperty or BeginScope.
WithMachineNameSerilog.Enrichers.EnvironmentMachineName
WithEnvironmentNameSerilog.Enrichers.EnvironmentEnvironmentName (Development/Staging/Production)
WithEnvironmentUserNameSerilog.Enrichers.EnvironmentEnvironmentUserName
WithProcessIdSerilog.Enrichers.ProcessProcessId
WithThreadIdSerilog.Enrichers.ThreadThreadId
WithCorrelationIdSerilog.Enrichers.CorrelationIdReads/generates X-Correlation-ID header
WithClientIpSerilog.Enrichers.ClientInfoCaller's IP (respects X-Forwarded-For)

Attaching enrichers from JSON

json
"Enrich": [
  "FromLogContext",
  "WithMachineName",
  "WithEnvironmentName",
  "WithProcessId",
  "WithThreadId"
]

Global properties

Constant properties that should appear on every event go in the Properties block. This is the classic place to tag the application and the version.

json
"Properties": {
  "Application": "OrderService",
  "Version": "3.2.1"
}
💡
Setting Application here is optional if you also stamp it via a per-app Seq API key (section 6). The API key wins — it's the enforced version that the app can't accidentally change.

Custom enricher: HTTP context

You often want the request path, HTTP method, and authenticated user on every event during a request. Write a small enricher.

csharp
public sealed class HttpContextEnricher : ILogEventEnricher
{
    private readonly IHttpContextAccessor _accessor;
    public HttpContextEnricher(IHttpContextAccessor accessor) => _accessor = accessor;

    public void Enrich(LogEvent logEvent, ILogEventPropertyFactory pf)
    {
        var http = _accessor.HttpContext;
        if (http is null) return;

        logEvent.AddPropertyIfAbsent(pf.CreateProperty("RequestPath",   http.Request.Path.Value));
        logEvent.AddPropertyIfAbsent(pf.CreateProperty("RequestMethod", http.Request.Method));

        var user = http.User?.Identity?.Name;
        if (!string.IsNullOrEmpty(user))
            logEvent.AddPropertyIfAbsent(pf.CreateProperty("User", user));
    }
}

// Program.cs
builder.Services.AddHttpContextAccessor();
builder.Services.AddSingleton<ILogEventEnricher, HttpContextEnricher>();

// The .ReadFrom.Services(services) call in your Serilog setup
// picks up any ILogEventEnricher registered in DI.

Custom enricher: build info

csharp
public sealed class BuildInfoEnricher : ILogEventEnricher
{
    private static readonly string _version =
        typeof(BuildInfoEnricher).Assembly.GetName().Version?.ToString() ?? "0.0.0";
    private static readonly string _commit =
        Environment.GetEnvironmentVariable("GIT_COMMIT") ?? "local";

    public void Enrich(LogEvent e, ILogEventPropertyFactory pf)
    {
        e.AddPropertyIfAbsent(pf.CreateProperty("AppVersion", _version));
        e.AddPropertyIfAbsent(pf.CreateProperty("GitCommit", _commit));
    }
}

The Seq Sink

How it ships events

Your app ILogger<T> In-memory buffer events queued flushed every 2s or 1000 events Seq server POST /api/events/raw ?clef (compact JSON) X-Seq-ApiKey: ... async HTTPS batch buffer file on disk (durable mode)
The sink batches events in memory (and optionally on disk) and ships them to Seq's HTTP ingestion endpoint.

All sink arguments

ArgumentDefaultDescription
serverUrlBase URL of the Seq server. Required.
apiKeynullPer-app API key. Identifies the source and can force properties. Covered next section.
restrictedToMinimumLevelVerboseLocal floor for the sink. Events below this never leave the app for Seq (regardless of the global minimum).
batchPostingLimit1000Max events per HTTP batch.
period2sFlush interval when the batch is not full.
bufferBaseFilenamenullEnable durable buffering. Events are written to this path on disk before being shipped. Survives Seq downtime and app restarts.
bufferSizeLimitBytesnullCap on the disk buffer size.
eventBodyLimitBytes256 KBDrop events larger than this. Prevents a runaway log from blowing up a batch.
controlLevelSwitchnullPowerful: a LoggingLevelSwitch that Seq controls remotely. Change the app's live minimum level from the Seq UI without redeploy. Covered in section 8.
messageHandlernullCustom HttpMessageHandler. Useful for corporate proxies or custom TLS.
queueSizeLimit100000Max events held in the in-memory queue before Serilog starts dropping.

Configured from JSON

json
{
  "Name": "Seq",
  "Args": {
    "serverUrl": "https://seq.internal.example.com",
    "apiKey": "REPLACE_ME",
    "restrictedToMinimumLevel": "Information",
    "batchPostingLimit": 500,
    "period": "00:00:02",
    "bufferBaseFilename": "./logs/seq-buffer/buffer",
    "bufferSizeLimitBytes": 268435456
  }
}

Configured from code

csharp
.WriteTo.Seq(
    serverUrl:                "https://seq.internal.example.com",
    apiKey:                    builder.Configuration["Seq:ApiKey"],
    restrictedToMinimumLevel:  LogEventLevel.Information,
    bufferBaseFilename:        "./logs/seq-buffer/buffer")
Always turn on bufferBaseFilename in production. If Seq is being upgraded, network is flaky, or the app restarts, events pile up on disk and get shipped when Seq comes back — no data loss.

Per-App API Keys

This is the piece most teams get wrong. An API key in Seq isn't just an authentication token — it's a policy object attached to every event that arrives with it. One key per application gives you clean filtering, enforced tagging, and blast-radius control that no in-app configuration can match.

🔑
The rule: one API key per application per environment. OrderService-Prod, OrderService-Staging, PaymentApi-Prod, and so on. Never share.

What a Seq API key can do

FeatureEffect
TitleHuman-readable label for the key. Shown in Seq UI.
TokenThe actual secret the app sends in the X-Seq-ApiKey header.
Forced propertiesThe key feature. Properties that Seq stamps on every event received through this key. The app cannot override them. This is how you tag Application, Environment, and Team reliably.
PermissionsUsually just Ingest. Never grant Read, Write, or admin permissions to an application key.
Minimum levelServer-side floor. Events below this level are rejected before they hit storage. Lets you throttle a noisy app centrally.
FilterOptional expression. Events that don't match are dropped. Rarely needed.
Input templateRewrites incoming events. Advanced; usually the forced properties block is enough.

Why forced properties matter

You could set "Application": "OrderService" in appsettings.json. But then:

  • A dev copies the config to a new service and forgets to change the name — logs land tagged OrderService.
  • An overzealous log call passes Application = "Something" and overrides the enricher.
  • The property is missing entirely from a legacy component and its events blend in with everyone else.

Forced properties on the API key remove the trust. Seq stamps Application = "OrderService" on every event that came in with that key — always, unconditionally, unforgeable from the app side.

OrderService apiKey: ord_p_9k... PaymentApi apiKey: pay_p_2m... CatalogWorker apiKey: cat_p_7q... API key: ord_p_9k... Application = OrderService Environment = Production Team = Fulfilment API key: pay_p_2m... Application = PaymentApi Environment = Production Team = Payments API key: cat_p_7q... Application = CatalogWorker Environment = Production Team = Catalog Seq storage events tagged with forced props
Each app carries its own key. Seq stamps the key's forced properties onto every event before storing it. Filtering by Application is now bulletproof.

Creating an API key (Seq UI)

StepAction
1Log in to Seq as an admin. Open Settings → API Keys.
2Click Add API Key.
3Title: OrderService-Production. This is what you and other admins see.
4Token: click Generate. Copy it now — Seq only shows it once. Store in your secret manager.
5Permissions: check only Ingest events.
6Minimum level: set to Information in prod, Debug in staging.
7Properties applied to events: add key/value pairs. This is the important part.

Recommended forced properties

yaml
Application:  OrderService
Environment:  Production
Team:         Fulfilment
Datacenter:   eu-west-1

Keep this list short and consistent across every key. Every app in every env should have the same set of property names — only the values differ. That's what makes cross-app filters work.

Filtering by app in Seq

Once forced properties are in place, filters are trivial and reliable. Seq's search bar accepts expressions:

seq
# Everything from OrderService in prod
Application = 'OrderService' and Environment = 'Production'

# All errors from any app on the Payments team
Team = 'Payments' and @Level = 'Error'

# One customer's activity across every service
CustomerId = 'cus_42' and Environment = 'Production'

# Compare error rates: two apps side by side
Application in ['OrderService', 'PaymentApi'] and @Level in ['Error', 'Fatal']

Save filters as signals

A signal in Seq is a saved filter you can toggle from the sidebar. Create one per application:

  • OrderService — Prod: Application = 'OrderService' and Environment = 'Production'
  • OrderService — Errors: adds and @Level in ['Error','Fatal']
  • Payments team: Team = 'Payments'

Because signals compose, "click OrderService, then click Errors" filters to just those errors — no typing.

Dashboards, per app

Duplicate one dashboard template per app by baking Application = 'X' into every chart's query. Errors/min, response times, request volumes — all scoped to a single service. Teams get their own view without polluting global metrics.

Rotation and secret storage

⚠️
Never commit API keys. Never paste them into chat, tickets, or docs. If a key leaks, revoke it in Seq (Settings → API Keys → Revoke) and issue a new one.

Where to store the key:

EnvironmentStore the key in
Local devdotnet user-secrets set Seq:ApiKey "dev_..."
CICI secret variable, surfaced as env var Seq__ApiKey
KubernetesSecret mounted as env var
Azure / AWSKey Vault / Secrets Manager, pulled at startup

Wiring the key in code

csharp
// appsettings.json still declares the Seq sink, but the apiKey stays out.
// We inject it from configuration/env vars at bootstrap.

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

    // Override the API key from a secure source.
    var apiKey = context.Configuration["Seq:ApiKey"];
    if (string.IsNullOrEmpty(apiKey))
        throw new InvalidOperationException("Seq API key is not configured.");

    config.WriteTo.Seq(
        serverUrl: context.Configuration["Seq:ServerUrl"]!,
        apiKey:    apiKey,
        bufferBaseFilename: "./logs/seq-buffer/buffer");
});
Fail fast on missing keys. A silently unconfigured Seq sink means logs go nowhere and you find out at 3 AM.

Correlation & Tracing

Per-app API keys tell you which service logged something. Correlation IDs tell you which request logged it — across every service that request touched. You need both.

The pattern

  1. Middleware reads traceparent (W3C) or a custom header from the incoming request.
  2. If missing, it generates one.
  3. The value is pushed into LogContext, so every event during this request carries it.
  4. Outbound HTTP calls (via a DelegatingHandler) attach the same header. Downstream services pick it up and repeat the process.

Middleware

csharp
public sealed class CorrelationMiddleware
{
    private const string Header = "X-Correlation-ID";
    private readonly RequestDelegate _next;
    public CorrelationMiddleware(RequestDelegate next) => _next = next;

    public async Task InvokeAsync(HttpContext ctx)
    {
        var correlationId = ctx.Request.Headers.TryGetValue(Header, out var v)
            && !string.IsNullOrWhiteSpace(v)
                ? v.ToString()
                : Guid.NewGuid().ToString("N");

        ctx.Response.Headers[Header] = correlationId;

        using (LogContext.PushProperty("CorrelationId", correlationId))
        using (LogContext.PushProperty("TraceId", Activity.Current?.TraceId.ToString()))
        {
            await _next(ctx);
        }
    }
}

// Program.cs
app.UseMiddleware<CorrelationMiddleware>();
app.UseSerilogRequestLogging();  // request logging sees the correlation too

Propagating across HTTP calls

csharp
public sealed class CorrelationHandler : DelegatingHandler
{
    private readonly IHttpContextAccessor _accessor;
    public CorrelationHandler(IHttpContextAccessor accessor) => _accessor = accessor;

    protected override Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request, CancellationToken ct)
    {
        var correlationId = _accessor.HttpContext?.Response.Headers["X-Correlation-ID"].ToString();
        if (!string.IsNullOrEmpty(correlationId))
            request.Headers.TryAddWithoutValidation("X-Correlation-ID", correlationId);
        return base.SendAsync(request, ct);
    }
}

// Program.cs
builder.Services.AddHttpContextAccessor();
builder.Services.AddTransient<CorrelationHandler>();
builder.Services
    .AddHttpClient("payments", c => c.BaseAddress = new Uri("https://payments.internal"))
    .AddHttpMessageHandler<CorrelationHandler>();

Finding the whole story in Seq

Once every service tags its events with the same CorrelationId, tracing a request across your fleet is one query:

seq
CorrelationId = '9f2b8d1c4e7a4a67b1'

Seq shows the events in timestamp order, interleaved across every Application. Click any event to see the full property bag; group by Application to see the call flow.

💡
If you already use OpenTelemetry, use its TraceId as the correlation. Seq surfaces TraceId and SpanId as first-class properties and can link to your APM.

Production Concerns

Dynamic level control

When something breaks in production, you want Debug for five minutes on one app — without a redeploy. Seq can flip the switch remotely if you wire up a LoggingLevelSwitch.

csharp
var levelSwitch = new LoggingLevelSwitch(LogEventLevel.Information);

Log.Logger = new LoggerConfiguration()
    .MinimumLevel.ControlledBy(levelSwitch)
    .Enrich.FromLogContext()
    .WriteTo.Seq(
        serverUrl:          "https://seq.internal.example.com",
        apiKey:              apiKey,
        controlLevelSwitch:  levelSwitch)   // Seq now controls this
    .CreateLogger();

In Seq: Settings → API Keys → your key → Minimum level. Change it, save, and within a few seconds every instance of that app starts logging at the new level. Reset when you're done.

Minimum level overrides

Namespaces let you keep the framework quiet while your app logs at Information:

json
"MinimumLevel": {
  "Default": "Information",
  "Override": {
    "Microsoft.AspNetCore.Mvc":           "Warning",
    "Microsoft.EntityFrameworkCore":      "Warning",
    "Microsoft.AspNetCore.Hosting":       "Warning",
    "Microsoft.AspNetCore.Routing":       "Warning",
    "System.Net.Http.HttpClient":         "Warning"
  }
}

Redacting secrets

Never log passwords, full card numbers, tokens, or PII. Serilog gives you three ways to enforce it:

  • Destructuring policy: tell Serilog how to serialize a specific type — drop or mask fields.
  • Custom enricher: walk the event's properties and mask anything matching a pattern.
  • Don't log it at all: the safest option. Log the operation, not the payload.
csharp
public sealed class CustomerDestructurer : IDestructuringPolicy
{
    public bool TryDestructure(object value, ILogEventPropertyValueFactory f, out LogEventPropertyValue? result)
    {
        if (value is not Customer c) { result = null; return false; }
        result = f.CreatePropertyValue(new
        {
            c.Id,
            EmailMasked = Mask(c.Email),
            // Password, SSN, etc: not included at all
        }, destructureObjects: true);
        return true;
    }

    private static string Mask(string s) =>
        s.Length < 4 ? "***" : s[..2] + "***" + s[^2..];
}

// Serilog config
.Destructure.With<CustomerDestructurer>()

Fallback file sink

Always keep a local file sink alongside Seq. If Seq is unreachable and the disk buffer is lost, you still have text logs on the box for last-resort investigation.

Log volume budget

Rule of thumbNumber
Events per request3–10 (request-in, key business steps, request-out)
Event sizeUnder 4 KB. Warn if larger; drop if over 256 KB (eventBodyLimitBytes).
Cost driver in SeqProperty count and cardinality, not raw event count. Avoid logging unbounded string values as properties (URLs with random IDs, error messages with GUIDs) — they explode the index.

Environment matrix

DevelopmentStagingProduction
Default levelDebugInformationInformation
Seq API keyshared dev keyApp-StagingApp-Production
Buffer fileoptionalonon
File sinkoptionalon (rolling daily)on (rolling daily, 14 day retention)
Console sinkon (human-friendly)on (JSON)on (JSON, for container logs)

Checklist before shipping

  • API key is unique per app per environment, stored in a secret manager, and has only Ingest permission.
  • Forced properties on the key include Application, Environment, and Team.
  • bufferBaseFilename is set and writable.
  • Log.CloseAndFlush() is in the finally block.
  • Framework namespaces are overridden down to Warning.
  • Correlation middleware and CorrelationHandler are wired for every outbound HTTP client.
  • A signal per app exists in Seq. A signal per team exists in Seq.
  • SelfLog is off in production (or routed to stderr only).
If someone can answer "show me every error from OrderService in the last hour" in one click, and "show me everything that happened for correlation X" in one query, your logging is doing its job.