Shaunebu.Common.Logging
1.0.1
dotnet add package Shaunebu.Common.Logging --version 1.0.1
NuGet\Install-Package Shaunebu.Common.Logging -Version 1.0.1
<PackageReference Include="Shaunebu.Common.Logging" Version="1.0.1" />
<PackageVersion Include="Shaunebu.Common.Logging" Version="1.0.1" />
<PackageReference Include="Shaunebu.Common.Logging" />
paket add Shaunebu.Common.Logging --version 1.0.1
#r "nuget: Shaunebu.Common.Logging, 1.0.1"
#:package Shaunebu.Common.Logging@1.0.1
#addin nuget:?package=Shaunebu.Common.Logging&version=1.0.1
#tool nuget:?package=Shaunebu.Common.Logging&version=1.0.1
Shaunebu.Common.Logging ππ
Overview β¨
Shaunebu.Common.Logging is a lightweight, extensible logging library for modern .NET applications targeting .NET 9 and .NET 10. It provides a custom Shaunebu.Common.Logging.Abstractions.ILogger abstraction, structured message templates, scopes, lazy message factories, queue-based processing, deterministic flushing, file rotation, diagnostics, redaction, and console/file providers.
The primary API is the custom ILogger interface. Standard Microsoft.Extensions.Logging abstractions can also be enabled with the additive AddShaunebuMicrosoftLogging adapter, which feeds the same queue, provider, scope, redaction, flush, and disposal pipeline.
For complete runnable examples, see Shaunebu.Common.Logging.Client.
Feature Comparison π
| Feature | Microsoft.Extensions.Logging | Serilog | Shaunebu.Common.Logging | Benefit |
|---|---|---|---|---|
| Structured Logging π·οΈ | β Built-in abstraction | β Excellent | β Built in | Rich, queryable context |
| Custom Lightweight API π§© | β Standard-only API | β Serilog-specific API | β
Custom ILogger |
Small focused surface |
| Microsoft ILogger Adapter π | β Native | β Via packages | β Additive adapter | Host and ASP.NET integration |
| MAUI Apps π± | β Generic abstractions | β Works through sinks | β DI-friendly setup | Mobile and desktop app readiness |
| Lazy Evaluation β‘ | β οΈ Pattern-based | β οΈ Pattern-based | β Built-in factories | Avoid work when disabled |
| Queue Processing π¦ | Provider-dependent | Sink-dependent | β Built-in queue | Smooth producer flow |
| Bounded Queue Policies π¦ | Provider-dependent | Sink-dependent | β Opt-in policies | Explicit drop/reject/wait behavior |
| File Rotation π | β Requires provider | β Via sinks | β Built-in file provider | Automatic file management |
| Redaction π | Provider-dependent | Enricher/sink-dependent | β Opt-in name-based redaction | Reduce accidental exposure |
| Internal Diagnostics π©Ί | Provider-dependent | Sink-dependent | β Immutable snapshot | Operational visibility |
| Dependencies π¦ | Minimal | Moderate | β Lightweight | Faster onboarding |
Installation π¦
dotnet add package Shaunebu.Common.Logging
Quick Start π
1. Basic Setup
using Microsoft.Extensions.DependencyInjection;
using Shaunebu.Common.Logging.Abstractions;
using Shaunebu.Common.Logging.Enums;
using Shaunebu.Common.Logging.Extensions;
var services = new ServiceCollection();
services
.AddShaunebuLogging(options =>
{
options.MinimumLevel = LogLevel.Information;
options.ApplicationName = "MyApp";
options.Environment = "Development";
options.IncludeScopes = true;
options.BatchSize = 50;
options.BatchInterval = TimeSpan.FromSeconds(2);
})
.AddConsole()
.AddFile(options =>
{
options.FilePath = "logs/application.log";
options.MaxFileSizeBytes = 5 * 1024 * 1024;
options.MaxFileCount = 5;
});
using var serviceProvider = services.BuildServiceProvider();
var logger = serviceProvider.GetRequiredService<ILogger>();
logger.LogInformation("Application started");
2. Basic Usage
public sealed class OrderService
{
private readonly ILogger _logger;
public OrderService(ILogger logger)
{
_logger = logger;
}
public void ProcessOrder(Order order)
{
_logger.LogInformation("Processing order {OrderId}", order.Id);
try
{
Process(order);
_logger.LogInformation("Order {OrderId} processed successfully", order.Id);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to process order {OrderId}", order.Id);
}
}
}
Core Features π―
1. Structured Logging π·οΈ
logger.LogInformation(
"User {UserName} with ID {UserId} purchased {ItemCount} items for {Total}",
"JohnDoe",
12345,
3,
99.99m);
logger.LogError(exception, "Order {OrderId} failed for customer {CustomerId}", orderId, customerId);
Message-template placeholders are rendered into the message and captured as properties by name. If more values are supplied than placeholders, only matched placeholders become properties. If fewer values are supplied, unmatched placeholders remain in the rendered message.
2. Lazy Evaluation β‘
logger.LogDebug(() =>
{
var expensiveData = CalculateExpensiveMetrics();
return $"Performance metrics: {expensiveData}";
});
Lazy factories are only evaluated when the configured level and provider filters can accept the entry.
3. Scoped Logging π
using (logger.BeginScope(new Dictionary<string, object>
{
["OrderId"] = order.Id,
["CustomerId"] = order.CustomerId,
["TotalAmount"] = order.Total
}))
{
logger.LogInformation("Starting order processing");
logger.LogDebug("Processing order items");
logger.LogInformation("Order processing completed");
}
When IncludeScopes is true, scope properties are copied into each accepted entry. Global context is added first, outer scopes then inner scopes override it, explicit log properties override scopes, and explicit LogEntry context fields such as UserId, SessionId, and CorrelationId have final precedence.
4. Multiple Providers πͺ
services
.AddShaunebuLogging(options =>
{
options.MinimumLevel = LogLevel.Debug;
options.IncludeScopes = true;
})
.AddConsole(options =>
{
options.MinimumLevel = LogLevel.Debug;
options.IncludeCategory = true;
options.IncludeProperties = true;
options.TimestampFormat = "HH:mm:ss.fff";
})
.AddFile(options =>
{
options.FilePath = "logs/app.log";
options.AllowedRootDirectory = AppContext.BaseDirectory;
options.RejectSymbolicLinks = true;
options.MaxFileSizeBytes = 10 * 1024 * 1024;
options.MaxFileCount = 10;
options.IncludeExceptionStacktrace = true;
});
5. Queue Policies π¦
services.AddShaunebuLogging(options =>
{
options.QueueCapacity = 10_000;
options.QueueFullPolicy = QueueFullPolicy.DropNewest;
options.QueueAdmissionTimeout = TimeSpan.FromMilliseconds(100);
});
QueueCapacity = null preserves the original unbounded queue behavior. A positive capacity enables the configured QueueFullPolicy: Wait, DropNewest, DropOldest, or Reject.
6. Redaction π
services.AddShaunebuLogging(options =>
{
options.Redaction.Enabled = true;
options.Redaction.ReplacementText = "[redacted]";
options.Redaction.PropertyNames.Add("Password");
options.Redaction.PropertyNames.Add("Authorization");
options.Redaction.PropertyNames.Add("AccessToken");
});
Redaction is opt-in and property-name based. It applies before provider rendering to configured structured properties, scope properties, Microsoft logging state, context fields, rendered template values, and supported exception data.
Advanced Usage π οΈ
Configuration-Based Setup βοΈ
{
"Logging": {
"MinimumLevel": "Information",
"ApplicationName": "MyApp",
"Environment": "Production",
"IncludeScopes": true,
"BatchSize": 100,
"BatchInterval": "00:00:05",
"QueueCapacity": 10000,
"QueueFullPolicy": "DropNewest",
"LevelOverrides": {
"Microsoft": "Warning",
"System": "Error",
"MyApp.Business": "Debug"
}
}
}
services.AddShaunebuLogging(configuration.GetSection("Logging"));
Microsoft.Extensions.Logging Integration π
services
.AddShaunebuLogging(options =>
{
options.MinimumLevel = LogLevel.Information;
options.IncludeScopes = true;
})
.AddShaunebuMicrosoftLogging()
.AddConsole();
This registers Microsoft.Extensions.Logging.ILogger, ILogger<T>, and ILoggerFactory adapters backed by the same LoggingService. Disposing the Microsoft adapter does not dispose the shared service.
Custom Logging Providers π§©
public sealed class CustomLoggingProvider : ILoggingProvider
{
public string Name => "Custom";
public Task WriteAsync(LogEntry entry)
{
return SendToExternalService(entry);
}
public bool IsEnabled(LogLevel level)
{
return level >= LogLevel.Information;
}
public Task FlushAsync()
{
return Task.CompletedTask;
}
public void Dispose()
{
}
}
Diagnostics Snapshot π©Ί
var loggingService = serviceProvider.GetRequiredService<LoggingService>();
var diagnostics = loggingService.GetDiagnosticsSnapshot();
Console.WriteLine($"Accepted: {diagnostics.AcceptedEntries}");
Console.WriteLine($"Processed: {diagnostics.ProcessedEntries}");
Console.WriteLine($"Dropped: {diagnostics.DroppedEntries}");
Console.WriteLine($"Rejected: {diagnostics.RejectedEntries}");
Console.WriteLine($"Provider failures: {diagnostics.ProviderFailures}");
Diagnostics snapshots are immutable and do not expose mutable counters or raw provider exception object graphs.
Lifecycle and Flushing π
LoggingService accepts entries until disposed. FlushAsync waits for all entries accepted before the flush boundary to be processed, then flushes providers. Dispose stops accepting new entries, drains accepted entries, flushes providers, and then disposes providers.
var loggingService = serviceProvider.GetRequiredService<LoggingService>();
logger.LogInformation("Before shutdown");
await loggingService.FlushAsync(TimeSpan.FromSeconds(10));
loggingService.Dispose();
Log calls after disposal are ignored.
Performance Tips π
- Use lazy evaluation for expensive log message generation.
- Set provider minimum levels so disabled entries skip formatting and rendering work.
- Choose queue policies intentionally for high-volume applications.
- Use scopes for contextual data instead of repeating properties manually.
- Flush during shutdown when the application owns the logging lifetime.
Performance Comparison
| Scenario | Traditional Approach | Shaunebu.Common.Logging | Improvement |
|---|---|---|---|
| Disabled Debug Logs | Message work may run anyway | β Lazy factories stay cold | Less wasted CPU |
| Provider-Filtered Logs | Formatting can happen before sink checks | β Provider pre-filtering | Lower allocation path |
| High-Volume Logging | Immediate writes can block producers | β Queue-based processing | Smoother producer flow |
| Shutdown | Pending entries can be lost if not coordinated | β Flush and deterministic dispose | Safer lifecycle |
| Troubleshooting | Failures may be invisible | β Diagnostics snapshot | Better operational visibility |
Best Practices π
β DO
logger.LogInformation("User {UserId} completed action {ActionType}", userId, actionType);
logger.LogDebug(() => $"Computed value: {ExpensiveCalculation()}");
using (logger.BeginScope(new Dictionary<string, object>
{
["CorrelationId"] = correlationId,
["TenantId"] = tenantId
}))
{
logger.LogInformation("Processing request");
}
options.LevelOverrides["Microsoft.EntityFrameworkCore"] = LogLevel.Warning;
β DON'T
logger.LogDebug("User " + userId + " did " + action); // Avoid string concatenation
logger.LogError("Something failed"); // Include the exception when one exists
logger.LogInformation("User password: {Password}", password); // Configure redaction first
API Reference π
ILogger Interface
| Method | Description |
|---|---|
Log(level, message, exception, properties) |
Core logging method |
LogTrace(message, properties) / LogTrace(messageTemplate, params) |
Trace level logging |
LogDebug(message, properties) / LogDebug(messageTemplate, params) |
Debug level logging |
LogInformation(message, properties) / LogInformation(messageTemplate, params) |
Information level logging |
LogWarning(message, properties) / LogWarning(messageTemplate, params) |
Warning level logging |
LogError(message, exception, properties) / LogError(exception, messageTemplate, params) |
Error level logging |
LogCritical(message, exception, properties) / LogCritical(exception, messageTemplate, params) |
Critical level logging |
BeginScope(scopeName) / BeginScope(properties) |
Create logging scope |
IsEnabled(level) |
Check whether a level is enabled |
LoggingService
| Method | Description |
|---|---|
CreateLogger(categoryName) |
Create a category-specific logger |
FlushAsync(timeout?) |
Flush entries accepted before the flush boundary |
GetDiagnosticsSnapshot() |
Read immutable queue and provider diagnostics |
Dispose() |
Stop accepting entries, drain, flush, and dispose providers |
Configuration Options
| Option | Default | Description |
|---|---|---|
MinimumLevel |
Information |
Minimum log level |
ApplicationName |
"Unknown" |
Application identifier |
Environment |
"Production" |
Runtime environment |
BatchSize |
100 |
Log batch size |
BatchInterval |
5 seconds |
Queue processing interval |
FlushTimeout |
30 seconds |
Default flush timeout |
IncludeScopes |
true |
Include scope properties |
IncludeCategory |
true |
Include category information in provider output |
IncludeExceptionStacktrace |
true |
Include exception stack traces in provider output |
MaxMessageLength |
4000 |
Maximum rendered message length |
QueueCapacity |
null |
Optional bounded queue capacity |
QueueFullPolicy |
DropNewest |
Full-queue behavior when capacity is configured |
LevelOverrides |
Empty | Category-specific levels |
Redaction |
Disabled | Opt-in property-name based redaction |
Examples π¨
API Controller Logging
[ApiController]
[Route("api/[controller]")]
public sealed class UsersController : ControllerBase
{
private readonly Microsoft.Extensions.Logging.ILogger<UsersController> _logger;
public UsersController(Microsoft.Extensions.Logging.ILogger<UsersController> logger)
{
_logger = logger;
}
[HttpGet("{id}")]
public async Task<ActionResult<User>> GetUser(int id)
{
using (_logger.BeginScope(new Dictionary<string, object>
{
["Action"] = "GetUser",
["UserId"] = id,
["RequestId"] = HttpContext.TraceIdentifier
}))
{
_logger.LogInformation("Getting user {UserId}", id);
var user = await _userService.GetUserAsync(id);
if (user is null)
{
_logger.LogWarning("User {UserId} not found", id);
return NotFound();
}
return user;
}
}
}
Background Service Logging
public sealed class BackgroundWorkerService : BackgroundService
{
private readonly Microsoft.Extensions.Logging.ILogger<BackgroundWorkerService> _logger;
public BackgroundWorkerService(Microsoft.Extensions.Logging.ILogger<BackgroundWorkerService> logger)
{
_logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
_logger.LogInformation("Background worker started");
while (!stoppingToken.IsCancellationRequested)
{
try
{
_logger.LogDebug("Starting work iteration");
await DoWorkAsync(stoppingToken);
_logger.LogDebug("Work iteration completed");
}
catch (Exception ex) when (ex is not OperationCanceledException)
{
_logger.LogError(ex, "Error in background work iteration");
}
await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken);
}
}
}
Security Behavior π
Line-oriented providers escape control characters in rendered text as \uXXXX, including carriage returns, line feeds, tabs, and other control characters. This keeps one logical log entry from creating forged extra physical lines.
Property rendering is resilient. If a property value cannot be serialized because it is cyclic, too deep, or has throwing members, the provider writes deterministic fallback text such as:
<serialization-failed: Namespace.TypeName>
Redaction is disabled by default. When enabled, configured property names are matched with ordinal case-insensitive comparison. Matching structured properties, scope properties, Microsoft logging state, and LogEntry context properties are replaced before provider rendering. Rendered message values originating from redacted structured properties are replaced with the configured replacement text. Exception data values are not mutated on the original exception; rendered exception text is redacted where configured data keys match.
Troubleshooting π§
Logs not appearing?
- Check
MinimumLeveland provider-specific minimum levels. - Verify dependency injection setup.
- Call
FlushAsyncbefore reading file output in short-lived apps. - Check
GetDiagnosticsSnapshot()for dropped, rejected, or provider-failure counts.
Missing scoped properties?
- Ensure
IncludeScopes = true. - Use a
usingstatement aroundBeginScope. - Remember that explicit log properties override scope properties with the same key.
File path rejected?
- Check
AllowedRootDirectory. - Use paths that remain inside the allowed root after canonicalization.
- Disable
RejectSymbolicLinksonly when your deployment explicitly accepts that risk.
Limitations β οΈ
- Redaction is property-name based and opt-in; it does not perform automatic PII detection.
- Queue bounding is opt-in; the default remains unbounded for compatibility.
- File allowed-root validation is opt-in; existing consumers without a root keep existing path behavior.
- Provider APIs do not include batch write or cancellation-token methods.
- Public option and model classes remain mutable for compatibility, but runtime services snapshot configuration and caller-owned dictionaries.
Roadmap πΊοΈ
See docs/Roadmap.md for planned features, version buckets, and acceptance gates.
Target Frameworks π―
net9.0net10.0
Version Compatibility π
Current compatibility policy keeps existing public API signatures, target frameworks, and package versioning behavior while adding optional integration, queue, redaction, file-security, diagnostics, and snapshot features.
License π
This package declares the MIT license in NuGet metadata.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | 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.Configuration (>= 10.0.10)
- Microsoft.Extensions.Configuration.Binder (>= 10.0.10)
- Microsoft.Extensions.DependencyInjection (>= 10.0.10)
- Microsoft.Extensions.Logging (>= 10.0.10)
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.10)
-
net9.0
- Microsoft.Extensions.Configuration (>= 10.0.10)
- Microsoft.Extensions.Configuration.Binder (>= 10.0.10)
- Microsoft.Extensions.DependencyInjection (>= 10.0.10)
- Microsoft.Extensions.Logging (>= 10.0.10)
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.10)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.