ASP.NET Core Structured Logging
Trigger On
- setup logging cho app mới hoặc upgrade từ Console.WriteLine/text logs
- thêm Serilog, Seq, Elasticsearch, Application Insights sinks
- enrich logs với correlation ID, user context, request path
- request/response logging middleware
- redact sensitive data (password, token, PII) khỏi logs
- log levels per namespace (
Microsoft.EntityFrameworkCore debug, app info)
- distributed tracing với OpenTelemetry
Documentation
Built-in ILogger vs Serilog — khi nào dùng cái nào
|
Built-in ILogger<T> |
Serilog |
| Setup |
Có sẵn, không cần package |
Serilog.AspNetCore |
| Structured logging |
✓ (qua message template) |
✓ (richer, easier sinks) |
| Sinks |
Console, Debug, EventSource, EventLog |
100+ (File, Seq, Elastic, Application Insights, Splunk...) |
| Enrichment |
Limited (Scopes) |
Rich (UseSerilog().Enrich.WithX()) |
| Recommend |
Dev, simple app, library |
Production app, multi-sink, enrichment |
Rule chung: dev / library code → built-in. Production app → Serilog. App ASP.NET Core hosted → Serilog kết hợp WriteTo.Console() + sink production (Seq/Elastic/AppInsights).
Structured Logging — message template, KHÔNG string interpolation
// ❌ Bad — string interpolation, không structured (sink chỉ thấy 1 string)
_logger.LogInformation($"User {userId} purchased {productId} at ${price}");
// ✅ Good — message template, structured (sink lưu userId/productId/price riêng)
_logger.LogInformation(
"User {UserId} purchased {ProductId} at {Price:C}",
userId, productId, price);
Properties trong template (PascalCase: {UserId}, {ProductId}) → tự thành searchable fields trong sink. Interpolation $"" mất hết structure.
Setup Serilog
dotnet add package Serilog.AspNetCore
dotnet add package Serilog.Sinks.Console
dotnet add package Serilog.Sinks.File
dotnet add package Serilog.Sinks.Seq # nếu dùng Seq local
dotnet add package Serilog.Enrichers.Environment
dotnet add package Serilog.Enrichers.Process
dotnet add package Serilog.Enrichers.Thread
// Program.cs
using Serilog;
var builder = WebApplication.CreateBuilder(args);
builder.Host.UseSerilog((ctx, services, config) => config
.ReadFrom.Configuration(ctx.Configuration)
.ReadFrom.Services(services)
.Enrich.FromLogContext()
.Enrich.WithEnvironmentName()
.Enrich.WithMachineName()
.Enrich.WithProperty("Application", "MyApp")
.WriteTo.Console(
outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj} {Properties:j}{NewLine}{Exception}")
.WriteTo.File("logs/app-.log",
rollingInterval: RollingInterval.Day,
retainedFileCountLimit: 14)
.WriteTo.Seq("http://localhost:5341"));
var app = builder.Build();
app.UseSerilogRequestLogging(); // log mỗi HTTP request
app.Run();
// appsettings.json — config bằng file, dễ thay đổi không recompile
{
"Serilog": {
"MinimumLevel": {
"Default": "Information",
"Override": {
"Microsoft.AspNetCore": "Warning",
"Microsoft.EntityFrameworkCore": "Warning",
"Microsoft.EntityFrameworkCore.Database.Command": "Information"
}
},
"WriteTo": [
{ "Name": "Console" },
{
"Name": "Seq",
"Args": { "serverUrl": "http://localhost:5341" }
}
],
"Enrich": ["FromLogContext", "WithEnvironmentName", "WithMachineName"]
}
}
Correlation ID + Request Logging
Auto correlation qua middleware
public class CorrelationIdMiddleware
{
private readonly RequestDelegate _next;
private const string HeaderName = "X-Correlation-ID";
public CorrelationIdMiddleware(RequestDelegate next) => _next = next;
public async Task InvokeAsync(HttpContext context, IDiagnosticContext diagnostics)
{
var correlationId = context.Request.Headers[HeaderName].FirstOrDefault()
?? Guid.NewGuid().ToString();
context.Response.Headers[HeaderName] = correlationId;
// Thêm vào Serilog LogContext → mọi log trong request này tự có CorrelationId
using (Serilog.Context.LogContext.PushProperty("CorrelationId", correlationId))
{
diagnostics.Set("CorrelationId", correlationId);
await _next(context);
}
}
}
// Program.cs — đặt SỚM trong pipeline
app.UseMiddleware<CorrelationIdMiddleware>();
app.UseSerilogRequestLogging();
Customize Serilog request logging
app.UseSerilogRequestLogging(options =>
{
options.MessageTemplate = "HTTP {RequestMethod} {RequestPath} → {StatusCode} in {Elapsed:0.0}ms";
options.GetLevel = (httpContext, elapsed, ex) => ex != null
? LogEventLevel.Error
: httpContext.Response.StatusCode >= 500 ? LogEventLevel.Error
: httpContext.Response.StatusCode >= 400 ? LogEventLevel.Warning
: elapsed > 1000 ? LogEventLevel.Warning
: LogEventLevel.Information;
options.EnrichDiagnosticContext = (diagnostics, httpContext) =>
{
diagnostics.Set("UserId", httpContext.User.FindFirst("sub")?.Value);
diagnostics.Set("RequestHost", httpContext.Request.Host.Value);
diagnostics.Set("UserAgent", httpContext.Request.Headers.UserAgent.ToString());
};
});
Log Levels — guideline
| Level |
Khi dùng |
Ví dụ |
Trace |
Chi tiết step-by-step (loop iteration) |
"Processing item 47/1000" — chỉ on-demand |
Debug |
Dev debug info |
"Cache miss for key {Key}", SQL query EF |
Information |
Sự kiện quan trọng app flow |
"User {UserId} logged in", "Order {OrderId} placed" |
Warning |
Thứ bất thường nhưng app vẫn chạy |
"Retry HTTP {Url} attempt {N}", "Slow query {Ms}ms" |
Error |
Operation fail, cần investigate |
Exception caught, business rule violation |
Critical |
App-level failure, có thể restart |
DB connection lost, OOM, fatal startup error |
Production default: Information. Trace/Debug → bật on-demand qua dynamic config (Serilog level switch) hoặc per-namespace override.
Log Scopes — group related logs
public async Task ProcessOrderAsync(int orderId)
{
using var scope = _logger.BeginScope("OrderId: {OrderId}", orderId);
_logger.LogInformation("Validating order");
await ValidateAsync();
_logger.LogInformation("Charging payment");
await ChargeAsync();
_logger.LogInformation("Order processed");
// Mọi log trong scope này tự có {OrderId} property
}
Dùng using var scope cho block work — mọi log trong block tự enrich. Sink cho phép filter theo OrderId.
Sensitive Data Redaction
// ❌ Bad — log password/token raw
_logger.LogInformation("Login: {Email} {Password}", email, password);
// ✅ Good — không log secret. Nếu cần log object, redact field
public record LoginRequest(string Email, [property: SensitiveData] string Password);
// Custom destructurer cho Serilog
config.Destructure.ByTransforming<LoginRequest>(
req => new { req.Email, Password = "[REDACTED]" });
.NET 8+ có Microsoft.Extensions.Compliance.Redaction cho redaction chuẩn. Cân nhắc dùng cho app GDPR/HIPAA.
OpenTelemetry tích hợp
dotnet add package OpenTelemetry.Exporter.Console
dotnet add package OpenTelemetry.Extensions.Hosting
dotnet add package OpenTelemetry.Instrumentation.AspNetCore
dotnet add package OpenTelemetry.Instrumentation.Http
builder.Services.AddOpenTelemetry()
.WithTracing(tracing => tracing
.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation()
.AddSource("MyApp")
.AddOtlpExporter()) // export đến Tempo/Jaeger/AppInsights
.WithMetrics(metrics => metrics
.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation()
.AddRuntimeInstrumentation()
.AddOtlpExporter());
OpenTelemetry = future-proof, vendor-neutral. Khuyến cáo cho service production-grade. Serilog vẫn dùng song song cho structured event logs.
Anti-patterns
| Anti-pattern |
Vấn đề |
Fix |
_logger.LogInformation($"User {id}") |
String interpolation mất structure |
Message template "User {UserId}", userId arg |
Console.WriteLine cho log |
Không qua pipeline, không filter, không sink |
ILogger<T> |
| Log password/secret/token |
Security breach |
Redact field, dùng [SensitiveData] |
Exception chỉ log message (ex.Message) |
Mất stack trace, inner exception |
_logger.LogError(ex, "Failed to X") |
| Log Trace/Debug ở production mặc định |
Volume ngập, perf hit |
MinimumLevel.Default = Information; bật Debug per-namespace khi cần |
| Log lặp ở mọi layer |
Duplicate data, noise |
Log 1 lần ở boundary (controller, handler), hoặc rely on tracing |
Quên await Log.CloseAndFlushAsync() lúc shutdown |
Mất logs cuối cùng |
Dùng Host.UseSerilog() (auto handle) |
| Sink HTTP synchronous trong hot path |
Block request |
Dùng async sink hoặc batched (Serilog.Sinks.Async) |
| File log không rolling/retention |
Disk full |
rollingInterval: Day, retainedFileCountLimit: N |
Performance
- Async sinks:
Serilog.Sinks.Async wrap sinks slow (HTTP, file) — không block request thread
- Batch sinks: Seq, Elastic, AppInsights tự batch — set
BatchPostingLimit phù hợp
- Sample: high-volume endpoint → log subset (
Information cho 1/100 request) qua filter
- Avoid
LogInformation trong tight loop: dùng LogTrace hoặc bỏ hẳn
Validate
- App dùng
ILogger<T> + Serilog (production) — không Console.WriteLine
- Message template với property names PascalCase
- Correlation ID middleware đặt SỚM trong pipeline
- Log levels per-namespace override (EF Core, ASP.NET → Warning thường)
- Không log sensitive data (password, token, PII)
- Exception log có ex object:
LogError(ex, "...")
- Production có sink ngoài Console (Seq, File rolling, AppInsights, ...)
- Đã test logs bằng cách trigger error → check sink nhận đúng
Hand off to
- App pipeline / middleware order →
aspnet-core
- Background jobs cần log riêng →
background-jobs
- Health checks (logs server health) →
aspnet-health-checks
- Distributed tracing chuyên sâu → OpenTelemetry docs
1---2name: aspnet-logging3description: Structured logging trong ASP.NET Core với built-in `ILogger<T>` và Serilog: sinks (Console/File/Seq/Elasticsearch), enrichment (correlation ID, user context, environment), log scopes, log levels per namespace, request logging middleware, sensitive data redaction. Use khi setup logging cho production app, debug logs scattered/khó truy vết, cần distributed tracing, output bị flat string không structured, hoặc khi user nói 'logs khó tìm', 'cần track request flow', 'log chậm', 'thêm Serilog'.4---56# ASP.NET Core Structured Logging78## Trigger On910- setup logging cho app mới hoặc upgrade từ Console.WriteLine/text logs11- thêm Serilog, Seq, Elasticsearch, Application Insights sinks12- enrich logs với correlation ID, user context, request path13- request/response logging middleware14- redact sensitive data (password, token, PII) khỏi logs15- log levels per namespace (`Microsoft.EntityFrameworkCore` debug, app info)16- distributed tracing với OpenTelemetry1718## Documentation1920- [Logging in .NET](https://learn.microsoft.com/en-us/dotnet/core/extensions/logging)21- [Serilog](https://serilog.net/)22- [Serilog.AspNetCore](https://github.com/serilog/serilog-aspnetcore)23- [OpenTelemetry .NET](https://learn.microsoft.com/en-us/dotnet/core/diagnostics/observability-with-otel)2425## Built-in `ILogger` vs Serilog — khi nào dùng cái nào2627| | Built-in `ILogger<T>` | Serilog |28|---|---|---|29| Setup | Có sẵn, không cần package | `Serilog.AspNetCore` |30| Structured logging | ✓ (qua message template) | ✓ (richer, easier sinks) |31| Sinks | Console, Debug, EventSource, EventLog | 100+ (File, Seq, Elastic, Application Insights, Splunk...) |32| Enrichment | Limited (Scopes) | Rich (`UseSerilog().Enrich.WithX()`) |33| Recommend | Dev, simple app, library | **Production app**, multi-sink, enrichment |3435> **Rule chung**: dev / library code → built-in. Production app → Serilog. App ASP.NET Core hosted → Serilog kết hợp `WriteTo.Console()` + sink production (Seq/Elastic/AppInsights).3637## Structured Logging — message template, KHÔNG string interpolation3839```csharp40// ❌ Bad — string interpolation, không structured (sink chỉ thấy 1 string)41_logger.LogInformation($"User {userId} purchased {productId} at ${price}");4243// ✅ Good — message template, structured (sink lưu userId/productId/price riêng)44_logger.LogInformation(45 "User {UserId} purchased {ProductId} at {Price:C}",46 userId, productId, price);47```4849> Properties trong template (PascalCase: `{UserId}`, `{ProductId}`) → tự thành searchable fields trong sink. Interpolation `$""` mất hết structure.5051## Setup Serilog5253```bash54dotnet add package Serilog.AspNetCore55dotnet add package Serilog.Sinks.Console56dotnet add package Serilog.Sinks.File57dotnet add package Serilog.Sinks.Seq # nếu dùng Seq local58dotnet add package Serilog.Enrichers.Environment59dotnet add package Serilog.Enrichers.Process60dotnet add package Serilog.Enrichers.Thread61```6263```csharp64// Program.cs65using Serilog;6667var builder = WebApplication.CreateBuilder(args);6869builder.Host.UseSerilog((ctx, services, config) => config70 .ReadFrom.Configuration(ctx.Configuration)71 .ReadFrom.Services(services)72 .Enrich.FromLogContext()73 .Enrich.WithEnvironmentName()74 .Enrich.WithMachineName()75 .Enrich.WithProperty("Application", "MyApp")76 .WriteTo.Console(77 outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj} {Properties:j}{NewLine}{Exception}")78 .WriteTo.File("logs/app-.log",79 rollingInterval: RollingInterval.Day,80 retainedFileCountLimit: 14)81 .WriteTo.Seq("http://localhost:5341"));8283var app = builder.Build();8485app.UseSerilogRequestLogging(); // log mỗi HTTP request8687app.Run();88```8990```json91// appsettings.json — config bằng file, dễ thay đổi không recompile92{93 "Serilog": {94 "MinimumLevel": {95 "Default": "Information",96 "Override": {97 "Microsoft.AspNetCore": "Warning",98 "Microsoft.EntityFrameworkCore": "Warning",99 "Microsoft.EntityFrameworkCore.Database.Command": "Information"100 }101 },102 "WriteTo": [103 { "Name": "Console" },104 {105 "Name": "Seq",106 "Args": { "serverUrl": "http://localhost:5341" }107 }108 ],109 "Enrich": ["FromLogContext", "WithEnvironmentName", "WithMachineName"]110 }111}112```113114## Correlation ID + Request Logging115116### Auto correlation qua middleware117118```csharp119public class CorrelationIdMiddleware120{121 private readonly RequestDelegate _next;122 private const string HeaderName = "X-Correlation-ID";123124 public CorrelationIdMiddleware(RequestDelegate next) => _next = next;125126 public async Task InvokeAsync(HttpContext context, IDiagnosticContext diagnostics)127 {128 var correlationId = context.Request.Headers[HeaderName].FirstOrDefault()129 ?? Guid.NewGuid().ToString();130131 context.Response.Headers[HeaderName] = correlationId;132133 // Thêm vào Serilog LogContext → mọi log trong request này tự có CorrelationId134 using (Serilog.Context.LogContext.PushProperty("CorrelationId", correlationId))135 {136 diagnostics.Set("CorrelationId", correlationId);137 await _next(context);138 }139 }140}141142// Program.cs — đặt SỚM trong pipeline143app.UseMiddleware<CorrelationIdMiddleware>();144app.UseSerilogRequestLogging();145```146147### Customize Serilog request logging148149```csharp150app.UseSerilogRequestLogging(options =>151{152 options.MessageTemplate = "HTTP {RequestMethod} {RequestPath} → {StatusCode} in {Elapsed:0.0}ms";153154 options.GetLevel = (httpContext, elapsed, ex) => ex != null155 ? LogEventLevel.Error156 : httpContext.Response.StatusCode >= 500 ? LogEventLevel.Error157 : httpContext.Response.StatusCode >= 400 ? LogEventLevel.Warning158 : elapsed > 1000 ? LogEventLevel.Warning159 : LogEventLevel.Information;160161 options.EnrichDiagnosticContext = (diagnostics, httpContext) =>162 {163 diagnostics.Set("UserId", httpContext.User.FindFirst("sub")?.Value);164 diagnostics.Set("RequestHost", httpContext.Request.Host.Value);165 diagnostics.Set("UserAgent", httpContext.Request.Headers.UserAgent.ToString());166 };167});168```169170## Log Levels — guideline171172| Level | Khi dùng | Ví dụ |173|---|---|---|174| `Trace` | Chi tiết step-by-step (loop iteration) | "Processing item 47/1000" — chỉ on-demand |175| `Debug` | Dev debug info | "Cache miss for key {Key}", SQL query EF |176| `Information` | Sự kiện quan trọng app flow | "User {UserId} logged in", "Order {OrderId} placed" |177| `Warning` | Thứ bất thường nhưng app vẫn chạy | "Retry HTTP {Url} attempt {N}", "Slow query {Ms}ms" |178| `Error` | Operation fail, cần investigate | Exception caught, business rule violation |179| `Critical` | App-level failure, có thể restart | DB connection lost, OOM, fatal startup error |180181> Production default: `Information`. Trace/Debug → bật on-demand qua dynamic config (Serilog level switch) hoặc per-namespace override.182183## Log Scopes — group related logs184185```csharp186public async Task ProcessOrderAsync(int orderId)187{188 using var scope = _logger.BeginScope("OrderId: {OrderId}", orderId);189190 _logger.LogInformation("Validating order");191 await ValidateAsync();192193 _logger.LogInformation("Charging payment");194 await ChargeAsync();195196 _logger.LogInformation("Order processed");197 // Mọi log trong scope này tự có {OrderId} property198}199```200201> Dùng `using var scope` cho block work — mọi log trong block tự enrich. Sink cho phép filter theo OrderId.202203## Sensitive Data Redaction204205```csharp206// ❌ Bad — log password/token raw207_logger.LogInformation("Login: {Email} {Password}", email, password);208209// ✅ Good — không log secret. Nếu cần log object, redact field210public record LoginRequest(string Email, [property: SensitiveData] string Password);211212// Custom destructurer cho Serilog213config.Destructure.ByTransforming<LoginRequest>(214 req => new { req.Email, Password = "[REDACTED]" });215```216217> .NET 8+ có `Microsoft.Extensions.Compliance.Redaction` cho redaction chuẩn. Cân nhắc dùng cho app GDPR/HIPAA.218219## OpenTelemetry tích hợp220221```bash222dotnet add package OpenTelemetry.Exporter.Console223dotnet add package OpenTelemetry.Extensions.Hosting224dotnet add package OpenTelemetry.Instrumentation.AspNetCore225dotnet add package OpenTelemetry.Instrumentation.Http226```227228```csharp229builder.Services.AddOpenTelemetry()230 .WithTracing(tracing => tracing231 .AddAspNetCoreInstrumentation()232 .AddHttpClientInstrumentation()233 .AddSource("MyApp")234 .AddOtlpExporter()) // export đến Tempo/Jaeger/AppInsights235 .WithMetrics(metrics => metrics236 .AddAspNetCoreInstrumentation()237 .AddHttpClientInstrumentation()238 .AddRuntimeInstrumentation()239 .AddOtlpExporter());240```241242> OpenTelemetry = future-proof, vendor-neutral. Khuyến cáo cho service production-grade. Serilog vẫn dùng song song cho structured event logs.243244## Anti-patterns245246| Anti-pattern | Vấn đề | Fix |247|---|---|---|248| `_logger.LogInformation($"User {id}")` | String interpolation mất structure | Message template `"User {UserId}"`, userId arg |249| `Console.WriteLine` cho log | Không qua pipeline, không filter, không sink | `ILogger<T>` |250| Log password/secret/token | Security breach | Redact field, dùng `[SensitiveData]` |251| Exception chỉ log message (`ex.Message`) | Mất stack trace, inner exception | `_logger.LogError(ex, "Failed to X")` |252| Log Trace/Debug ở production mặc định | Volume ngập, perf hit | `MinimumLevel.Default = Information`; bật Debug per-namespace khi cần |253| Log lặp ở mọi layer | Duplicate data, noise | Log 1 lần ở boundary (controller, handler), hoặc rely on tracing |254| Quên `await Log.CloseAndFlushAsync()` lúc shutdown | Mất logs cuối cùng | Dùng `Host.UseSerilog()` (auto handle) |255| Sink HTTP synchronous trong hot path | Block request | Dùng async sink hoặc batched (Serilog.Sinks.Async) |256| File log không rolling/retention | Disk full | `rollingInterval: Day`, `retainedFileCountLimit: N` |257258## Performance259260- **Async sinks**: `Serilog.Sinks.Async` wrap sinks slow (HTTP, file) — không block request thread261- **Batch sinks**: Seq, Elastic, AppInsights tự batch — set `BatchPostingLimit` phù hợp262- **Sample**: high-volume endpoint → log subset (`Information` cho 1/100 request) qua filter263- **Avoid `LogInformation` trong tight loop**: dùng `LogTrace` hoặc bỏ hẳn264265## Validate266267- App dùng `ILogger<T>` + Serilog (production) — không `Console.WriteLine`268- Message template với property names PascalCase269- Correlation ID middleware đặt SỚM trong pipeline270- Log levels per-namespace override (EF Core, ASP.NET → Warning thường)271- Không log sensitive data (password, token, PII)272- Exception log có ex object: `LogError(ex, "...")`273- Production có sink ngoài Console (Seq, File rolling, AppInsights, ...)274- Đã test logs bằng cách trigger error → check sink nhận đúng275276## Hand off to277278- App pipeline / middleware order → `aspnet-core`279- Background jobs cần log riêng → `background-jobs`280- Health checks (logs server health) → `aspnet-health-checks`281- Distributed tracing chuyên sâu → OpenTelemetry docs