Modern structured logging extensions for .NET that separate human-readable messages from machine-readable attributes. Include contextual data (user IDs, correlation IDs, metrics) in logs without forcing them into message templates.
$ dotnet add package RandomSkunk.StructuredLoggingModern, high-performance structured logging extensions for .NET that cleanly separate human-readable messages from machine-readable attributes. Stop cluttering your message templates with structured data and start writing logs that are easier to read and query.
Traditional structured logging forces you to embed data into message templates. This often leads to:
Log("User {UserId} logged in from {IPAddress}", userId, ipAddress)This library takes a different approach by treating messages and attributes as separate concerns, giving you the best of both worlds: clean, readable messages and rich, queryable data.
$"User {user.Name:<UserName>}") without sacrificing performance. The interpolation only happens if the log level is enabled!dotnet add package RandomSkunk.StructuredLoggingUse the extension methods on Microsoft.Extensions.Logging.ILogger.
Pass attributes as a list of (string, object) tuples. The message remains clean and readable.
logger.Information("User logged in successfully",
("UserId", user.Id),
("SessionId", sessionId),
("LoginTime", DateTime.UtcNow));Output Log (conceptual JSON):
{
"Message": "User logged in successfully",
"UserId": 123,
"SessionId": "xyz-abc",
"LoginTime": "2024-01-01T12:00:00Z"
}For ultimate convenience, extract attributes directly from an interpolated string. The syntax {value:<AttributeName>} captures the value as an attribute and embeds it in the message.
This is not just a simple string.Format. The library uses a custom interpolated string handler that only evaluates the arguments and formats the string if the log level is enabled.
// The values for username and attemptCount are captured as attributes.
logger.Warning($"Failed login attempt for {username:<Username>}",
("AttemptCount", attemptCount),
("IPAddress", clientIp));Output Log (conceptual JSON):
{
"Message": "Failed login attempt for brian",
"Username": "brian",
"AttemptCount": 3,
"IPAddress": "127.0.0.1"
}Performance is a core feature. This library is designed to minimize overhead in your application.
The custom interpolated string handlers are the magic behind the performance. String formatting and method calls inside an interpolated string only occur if the log level is enabled.
// If Debug logging is disabled, CalculateSize() is never called and no string is created.
logger.Debug($"Processing {items.Count:<ItemsCount>} items with total size {CalculateSize(items):<ItemsByteCount>N0} bytes");Unlike other libraries, we do not cache message templates. This eliminates memory overhead and performance penalties associated with managing a cache, making it ideal for dynamic log messages.
The library is flexible enough to handle any scenario.
All overloads support standard EventId and Exception arguments.
logger.Error(new EventId(500, "DatabaseError"), exception, "Database connection failed",
("ConnectionString", connectionString),
("RetryCount", retryCount));You can pass attributes in any IReadOnlyCollection<KeyValuePair<string, object?>>, including a Dictionary.
var metadata = new Dictionary<string, object?>
{
["UserId"] = user.Id,
["TenantId"] = tenant.Id,
["CorrelationId"] = correlationId
};
logger.Information("Operation completed", metadata);The library extends ILogger with a new set of extension methods. These methods use custom interpolated string handlers to intercept string formatting.
LogLevel is enabled.<Key> syntax.ILogger instance.Microsoft.Extensions.Logging providers (Serilog, Console, etc.)This project is licensed under the MIT License.
Copyright (c) 2025-2026 Brian Friesen. All rights reserved.