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
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Shaunebu.Common.Logging" Version="1.0.1" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Shaunebu.Common.Logging" Version="1.0.1" />
                    
Directory.Packages.props
<PackageReference Include="Shaunebu.Common.Logging" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Shaunebu.Common.Logging --version 1.0.1
                    
#r "nuget: Shaunebu.Common.Logging, 1.0.1"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Shaunebu.Common.Logging@1.0.1
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Shaunebu.Common.Logging&version=1.0.1
                    
Install as a Cake Addin
#tool nuget:?package=Shaunebu.Common.Logging&version=1.0.1
                    
Install as a Cake Tool

Shaunebu.Common.Logging πŸ“πŸš€

Platform License Version Production Ready Performance Easy

NuGet Version NuGet Downloads

NET Support MAUI CSharp Architecture Logging Providers ThreadSafe

Support

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 πŸš€

  1. Use lazy evaluation for expensive log message generation.
  2. Set provider minimum levels so disabled entries skip formatting and rendering work.
  3. Choose queue policies intentionally for high-volume applications.
  4. Use scopes for contextual data instead of repeating properties manually.
  5. 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 MinimumLevel and provider-specific minimum levels.
  • Verify dependency injection setup.
  • Call FlushAsync before reading file output in short-lived apps.
  • Check GetDiagnosticsSnapshot() for dropped, rejected, or provider-failure counts.

Missing scoped properties?

  • Ensure IncludeScopes = true.
  • Use a using statement around BeginScope.
  • 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 RejectSymbolicLinks only 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.0
  • net10.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 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.0.1 97 7/29/2026
1.0.0 462 11/21/2025