Die meisten .NET-Anwendungen protokollieren. Nur wenige protokollieren gut. Der Unterschied liegt in der Struktur. Strukturierte Logs sind keine menschenlesbaren Zeichenketten — sie sind abfragbare Daten mit benannten Eigenschaften, konsistenten Schemata und aussagekräftigem Kontext. So machen Sie es richtig.
Nachrichtenvorlagen statt String-Interpolation
Der häufigste Logging-Fehler in .NET ist die Verwendung von String-Interpolation. Es sieht sauber aus, zerstört aber die Möglichkeit, Logs nach Vorlage abzufragen und zu aggregieren.
// WRONG — string interpolation
logger.LogInformation($"Order {orderId} created by {userId} for {total:C}");
// Produces: "Order 3fa85f64-5717-4562-b3fc-2c963f66afa6 created by abc123 for $150.00"
// Every log entry has a unique message — impossible to group or query
// CORRECT — message template with named properties
logger.LogInformation(
"Order {OrderId} created by {UserId} for {Total}",
orderId, userId, total);
// Produces structured data:
// {
// "MessageTemplate": "Order {OrderId} created by {UserId} for {Total}",
// "Properties": {
// "OrderId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
// "UserId": "abc123",
// "Total": 150.00
// }
// }
Mit strukturiertem Logging können Sie abfragen: „Zeige mir alle Logs, bei denen OrderId = X ist“ oder „Zähle Log-Einträge gruppiert nach MessageTemplate.“ String-Interpolation macht das unmöglich.
Scoped Properties für Kontext
Log-Scopes hängen jedem Log-Eintrag innerhalb eines Scopes Eigenschaften an. Verwenden Sie sie für Request-Level-Kontext wie Benutzer-IDs, Correlation-IDs und Tenant-IDs.
public class CorrelationIdMiddleware(RequestDelegate next, ILogger<CorrelationIdMiddleware> logger)
{
public async Task InvokeAsync(HttpContext context)
{
var correlationId = context.Request.Headers["X-Correlation-Id"]
.FirstOrDefault() ?? Guid.NewGuid().ToString();
using (logger.BeginScope(new Dictionary<string, object>
{
["CorrelationId"] = correlationId,
["UserId"] = context.User.FindFirst("sub")?.Value ?? "anonymous",
["TenantId"] = context.Request.Headers["X-Tenant-Id"].FirstOrDefault() ?? "default"
}))
{
context.Response.Headers["X-Correlation-Id"] = correlationId;
await next(context);
}
}
}
Jeder Log-Eintrag innerhalb des Requests trägt nun CorrelationId, UserId und TenantId, ohne dass ein expliziter Logging-Aufruf sie einschließen müsste.
Log-Level haben eine Bedeutung
Verwenden Sie Log-Level konsistent in Ihrer gesamten Anwendung. Dokumentieren Sie, was jedes Level für Ihr Team bedeutet, und setzen Sie es in Code-Reviews durch.
public class OrderService(ILogger<OrderService> logger)
{
public async Task<Order> ProcessOrderAsync(CreateOrderCommand command, CancellationToken ct)
{
// Trace — very detailed diagnostic info, usually disabled in production
logger.LogTrace("Entering ProcessOrderAsync with {ItemCount} items", command.Items.Count);
// Debug — diagnostic info useful during development
logger.LogDebug("Validating inventory for order from customer {CustomerId}", command.CustomerId);
// Information — normal operations worth recording
logger.LogInformation("Order {OrderId} created for customer {CustomerId}", order.Id, command.CustomerId);
// Warning — unexpected but handled situations
logger.LogWarning("Order {OrderId} total {Total} exceeds threshold {Threshold}",
order.Id, total, threshold);
// Error — failures that need attention
logger.LogError(ex, "Failed to process payment for order {OrderId}", order.Id);
// Critical — application-wide failures
logger.LogCritical(ex, "Database connection lost during order processing");
}
}
High-Performance-Logging mit LoggerMessage
Für Hot Paths eliminiert der LoggerMessage-Source-Generator Boxing und String-Allokations-Overhead. Der Performance-Unterschied ist spürbar, wenn Millionen von Einträgen pro Minute protokolliert werden.
public static partial class LogMessages
{
[LoggerMessage(Level = LogLevel.Information,
Message = "Order {OrderId} created for customer {CustomerId} with total {Total}")]
public static partial void OrderCreated(
this ILogger logger, Guid orderId, Guid customerId, decimal total);
[LoggerMessage(Level = LogLevel.Warning,
Message = "Order {OrderId} processing took {ElapsedMs}ms, exceeding {ThresholdMs}ms threshold")]
public static partial void SlowOrderProcessing(
this ILogger logger, Guid orderId, long elapsedMs, long thresholdMs);
}
// Usage — zero allocation, strongly typed
logger.OrderCreated(order.PublicId, command.CustomerId, total);
Strukturiertes Logging ist fundamental. Machen Sie es früh richtig, setzen Sie es konsequent durch, und Ihr Produktions-Debugging wird sich grundlegend verändern. Wenn um 3 Uhr nachts ein Vorfall auftritt, ist der Unterschied zwischen abfragbaren strukturierten Logs und einer Wand aus Text der Unterschied zwischen einer 10-minütigen Lösung und einer dreistündigen Untersuchung.
Comments (2)
Could you elaborate on this topic in a follow-up post?
Good question, I'd like to know too.
Interesting thought, thanks for adding that.
This is exactly what I was looking for, thank you!
Leave a Comment