| name | logging |
| description | Serilog structured logging for ASP.NET Core — setup, message templates, LogContext enrichment, request logging middleware, and log level guidelines. |
Serilog Structured Logging
Reference material for adding and using Serilog in ASP.NET Core. Used by dnp-architect and dnp-tdd-developer-hard.
NuGet Packages
<PackageReference Include="Serilog.AspNetCore" Version="8.*" />
<PackageReference Include="Serilog.Enrichers.Environment" Version="3.*" />
<PackageReference Include="Serilog.Enrichers.Thread" Version="4.*" />
<PackageReference Include="Serilog.Formatting.Compact" Version="3.*" />
<PackageReference Include="Serilog.Sinks.OpenTelemetry" Version="4.*" />
Setup — Program.cs
Replace the default ASP.NET Core logging before builder.Build():
builder.Host.UseSerilog((context, config) =>
config
.ReadFrom.Configuration(context.Configuration)
.Enrich.FromLogContext()
.Enrich.WithMachineName()
.Enrich.WithThreadId()
.WriteTo.Console(new CompactJsonFormatter())
.WriteTo.OpenTelemetry());
For early startup errors, bootstrap a minimal logger before the host is built:
Log.Logger = new LoggerConfiguration()
.WriteTo.Console()
.CreateBootstrapLogger();
try
{
var builder = WebApplication.CreateBuilder(args);
builder.Host.UseSerilog((ctx, cfg) => cfg.ReadFrom.Configuration(ctx.Configuration)...);
}
catch (Exception ex)
{
Log.Fatal(ex, "Application startup failed.");
}
finally
{
Log.CloseAndFlush();
}
appsettings.json Configuration
{
"Serilog": {
"MinimumLevel": {
"Default": "Information",
"Override": {
"Microsoft": "Warning",
"Microsoft.EntityFrameworkCore": "Warning",
"Microsoft.EntityFrameworkCore.Database.Command": "Information",
"System": "Warning"
}
}
}
}
Flip EF Core's Database.Command override to Information temporarily when debugging query issues; revert before shipping.
Structured Logging — Message Templates
Always use message templates, never string interpolation. Interpolation loses the structured property; templates capture it.
_logger.LogInformation("Order {OrderId} created for customer {CustomerId}", order.Id, order.CustomerId);
_logger.LogWarning("Payment failed for order {OrderId}: {Reason}", orderId, reason);
_logger.LogError(ex, "Failed to process order {OrderId}", orderId);
_logger.LogInformation($"Order {order.Id} created for customer {order.CustomerId}");
Naming convention for template properties: PascalCase, noun-first (OrderId, not id).
LogContext Enrichment
Push properties onto the log context to include them in every log line emitted within a scope. Useful for request-scoped IDs.
using (LogContext.PushProperty("UserId", userId))
using (LogContext.PushProperty("RequestId", httpContext.TraceIdentifier))
{
await ProcessOrderAsync(orderId, ct);
}
In middleware, enrich once per request:
public class UserContextMiddleware(RequestDelegate next)
{
public async Task InvokeAsync(HttpContext context)
{
var userId = context.User.FindFirstValue(ClaimTypes.NameIdentifier);
if (userId is not null)
using (LogContext.PushProperty("UserId", userId))
{
await next(context);
return;
}
await next(context);
}
}
Request Logging Middleware
Replace ASP.NET Core's default request logging with Serilog's, which emits one structured line per request:
app.UseSerilogRequestLogging(options =>
{
options.MessageTemplate =
"HTTP {RequestMethod} {RequestPath} responded {StatusCode} in {Elapsed:0.0000} ms";
options.GetLevel = (ctx, elapsed, ex) =>
ex is not null || ctx.Response.StatusCode >= 500
? LogEventLevel.Error
: LogEventLevel.Information;
options.EnrichDiagnosticContext = (diagCtx, httpContext) =>
{
diagCtx.Set("UserId", httpContext.User.FindFirstValue(ClaimTypes.NameIdentifier));
diagCtx.Set("ClientIp", httpContext.Connection.RemoteIpAddress?.ToString());
};
});
Log Level Guidelines
| Level | When to use | Production default |
|---|
Verbose / Trace | SQL queries, cache hits, per-frame loops | Off |
Debug | Detailed flow for diagnosing a specific issue | Off |
Information | Business events (order created, user logged in, job started) | On |
Warning | Recoverable issues: retry attempt, cache miss storm, deprecated API call | On |
Error | Failures affecting one request: payment declined, DB timeout, validation failure | On |
Fatal | Application cannot continue: startup failure, unrecoverable state | On |
Performance Guard
Avoid building expensive log messages that may not be written. Use the IsEnabled guard:
if (_logger.IsEnabled(LogLevel.Debug))
{
var payload = JsonSerializer.Serialize(largeObject);
_logger.LogDebug("Full request payload: {Payload}", payload);
}
For hot paths, prefer LoggerMessage.Define for zero-allocation logging:
private static readonly Action<ILogger, Guid, Exception?> _orderCreated =
LoggerMessage.Define<Guid>(LogEventLevel.Information, new EventId(1, "OrderCreated"),
"Order {OrderId} created.");
_orderCreated(_logger, order.Id, null);
Security Rules
Never log:
- Passwords, PINs, security questions
- Auth tokens, API keys, session IDs
- PII: email addresses, names, SSNs, credit card numbers, phone numbers
- Full request bodies containing any of the above
Redact at source — don't rely on log pipeline filtering:
_logger.LogInformation("User login attempt: {Email} / {Password}", email, password);
_logger.LogInformation("User login attempt for account {AccountId}", accountId);
Correlation with OpenTelemetry
Add Serilog.Enrichers.Span to include TraceId and SpanId in log entries, correlating logs with traces in your observability platform:
<PackageReference Include="Serilog.Enrichers.Span" Version="3.*" />
config.Enrich.WithSpan();
See Also
skills/opentelemetry/SKILL.md — trace/span setup and correlation with logs
- EF Core query logging: set
Microsoft.EntityFrameworkCore.Database.Command to Information in config