Best Practices für strukturiertes Logging in .NET cover image

Best Practices für strukturiertes Logging in .NET

Von Julian Krause · Donnerstag, 5. März 2026 · ~3 Min. Lesezeit

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.


Kommentare (2)

David Sonntag, 22. Februar 2026 17:08

Could you elaborate on this topic in a follow-up post?

Felix Sonntag, 22. Februar 2026 20:08

Good question, I'd like to know too.

Greta Montag, 23. Februar 2026 20:08

Interesting thought, thanks for adding that.

Elena Montag, 23. Februar 2026 17:08

This is exactly what I was looking for, thank you!

Kommentar hinterlassen