RandomSkunk.StructuredLogging
0.9.4
See the version list below for details.
dotnet add package RandomSkunk.StructuredLogging --version 0.9.4
NuGet\Install-Package RandomSkunk.StructuredLogging -Version 0.9.4
<PackageReference Include="RandomSkunk.StructuredLogging" Version="0.9.4" />
<PackageVersion Include="RandomSkunk.StructuredLogging" Version="0.9.4" />
<PackageReference Include="RandomSkunk.StructuredLogging" />
paket add RandomSkunk.StructuredLogging --version 0.9.4
#r "nuget: RandomSkunk.StructuredLogging, 0.9.4"
#:package RandomSkunk.StructuredLogging@0.9.4
#addin nuget:?package=RandomSkunk.StructuredLogging&version=0.9.4
#tool nuget:?package=RandomSkunk.StructuredLogging&version=0.9.4
RandomSkunk.StructuredLogging
RandomSkunk.StructuredLogging provides structured logging extensions for .NET, designed to separate human-readable messages from machine-readable properties. This approach helps keep your logs clear, maintainable, and easy to query, without mixing data into message templates.
Why Choose RandomSkunk.StructuredLogging?
The default logging extension methods from Microsoft.Extensions.Logging force you to embed structured log properties into message templates. This often leads to:
- Verbose and Unreadable Messages:
logger.LogInformation("User {UserId} logged in from {IPAddress}", userId, ipAddress) - Performance Overhead: Message template caching can consume memory and CPU.
- Rigid Structure: You can only log what your template allows.
This library takes a different approach by treating messages and properties as separate concerns, giving you the best of both worlds: clean, readable messages and rich, queryable data.
Features
- ✨ Clean Separation: Keep your log messages for humans and your properties for machines.
- 🚀 High Performance: A design that avoids message template caching overhead.
- 📝 Powerful Interpolated Strings: Automatically extract attributes from interpolated strings
(
$"User {user.Name:<UserName>}") without sacrificing performance. The interpolation only happens if the log level is enabled! - 💪 Flexible & Type-Safe: Pass properties using tuples, dictionaries, or arrays with a rich set of overloads.
- 🔄 Operation Logging: Track operation start/completion logs with shared context, per-operation trace entries, and optional return values/exceptions.
Quick Start
1. Install the Package
dotnet add package RandomSkunk.StructuredLogging
2. Start Logging
Use the extension methods provided by RandomSkunk.StructuredLogging on Microsoft.Extensions.Logging.ILogger.
Basic Logging with Properties
Pass properties as a list of (string, object?) tuples. The message remains clean and readable, and properties are attached as structured data.
logger.Information("User logged in successfully",
("UserId", user.Id),
("SessionId", sessionId),
("LoginTime", DateTime.UtcNow));
Example output (conceptual JSON):
{
"Message": "User logged in successfully",
"UserId": 123,
"SessionId": "xyz-abc",
"LoginTime": "2024-01-01T12:00:00Z"
}
Property Extraction from Interpolated Strings
You can extract properties directly from an interpolated string using the syntax {value:<PropertyName>}. This captures the value as a property and embeds it in the message.
The library uses a custom interpolated string handler, so arguments and formatting only occur if the log level is enabled.
// The values for username, attemptCount, and clientIp are captured as properties.
logger.Warning($"Failed login attempt for {username:<Username>}",
("AttemptCount", attemptCount),
("IPAddress", clientIp));
Example output (conceptual JSON):
{
"Message": "Failed login attempt for brian",
"Username": "brian",
"AttemptCount": 3,
"IPAddress": "127.0.0.1"
}
Operation Logging
Use LogOperation to create an operation log that writes a single structured log entry when disposed. The returned IOperationLog instance provides methods to log values, conditions, and return values within the operation. This log summarizes the operation's context and any details you record during its execution.
public int Divide(int dividend, int divisor, int? fallbackValue = null)
{
using var log = logger.LogOperation(
$"{typeof(Calculator)}.{nameof(Divide)}",
("Dividend", dividend),
("Divisor", divisor),
("FallbackValue", fallbackValue));
if (!log.IsNull(fallbackValue) && log.Condition(divisor == 0))
{
log.Append($"Cannot divide by zero. Returning fallback value, {fallbackValue}.");
return log.ReturnValue(fallbackValue.Value);
}
try
{
return log.ReturnValue(dividend / divisor);
}
catch (Exception ex)
{
logger.Error(log.EventId, log.Exception(ex), "Error performing division. Rethrowing exception...", log.Properties);
throw;
}
}
Example output (conceptual JSON):
{
"Message": "Operation complete: MathUtilities.Calculator.Divide",
"Dividend": 10,
"Divisor": 2,
"FallbackValue": null,
"Operation.ReturnValue": 5,
"Operation.Log": "[12:34:56.785Z] Operation started
[12:34:56.786Z] `fallbackValue` is not null
[12:34:56.787Z] `divisor == 0` is false
[12:34:56.788Z] Return value set to `dividend / divisor`
[12:34:56.789Z] Operation complete"
}
Performance: Fast and Memory-Efficient
Performance is a core feature. This library is designed to minimize overhead in your application.
Conditional Evaluation
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");
No Message Caching
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.
Advanced Usage
Additional Features
RandomSkunk.StructuredLogging supports a variety of advanced scenarios for structured logging.
Logging with Exceptions and Event IDs
All logging methods support EventId and Exception parameters, allowing you to attach additional context to your logs.
logger.Error(new EventId(500, "DatabaseError"), exception, "Database connection failed",
("ConnectionString", connectionString),
("RetryCount", retryCount));
Using Dictionaries for Properties
You can pass properties as any IReadOnlyCollection<KeyValuePair<string, object?>>, such as a Dictionary:
var metadata = new Dictionary<string, object?>
{
["UserId"] = user.Id,
["TenantId"] = tenant.Id,
["CorrelationId"] = correlationId
};
logger.Information("Operation completed", metadata);
How It Works
The library extends ILogger with a new set of extension methods for structured event logs and operation logs. These methods
use custom [interpolated string handlers]
(https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/proposals/csharp-10.0/improved-interpolated-strings) to
intercept string formatting.
- The handler checks if the requested
LogLevelis enabled. - If not, it does nothing, and the call is nearly free.
- If enabled, it processes the interpolated string, extracting any properties defined with the
<Key>syntax. - It then combines all properties and passes them, along with the formatted message, to the underlying
ILoggerinstance. - The library uses an optimized struct-based approach for passing properties.
Compatibility
RandomSkunk.StructuredLogging targets:
- .NET 8.0
- .NET 9.0
- .NET 10.0
It is compatible with all Microsoft.Extensions.Logging providers, including OpenTelemetry, Serilog, and the built-in Console logger.
License
This project is licensed under the MIT License.
Contributing & Support
Contributions, issues, and suggestions are welcome. Please open an issue or pull request on GitHub if you have feedback or need help.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 is compatible. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.3)
- Microsoft.Extensions.ObjectPool (>= 10.0.5)
-
net8.0
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.3)
- Microsoft.Extensions.ObjectPool (>= 10.0.5)
-
net9.0
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.3)
- Microsoft.Extensions.ObjectPool (>= 10.0.5)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.