Skip to content

Architecture and Core Concepts

This guide explains why Pragmatic.Logging exists, how its pieces fit together, and how to choose the right configuration for each situation. Read this before diving into the individual feature guides.


Logging sits on the hot path of every request. A production application needs structured log output, correlation IDs for distributed tracing, PII redaction for compliance, rate limiting to prevent log spam, and multiple output targets — all without adding measurable latency. The standard .NET logging infrastructure provides the foundation, but leaves these concerns to the developer.

// Every call allocates a new string, even when the level is disabled
logger.LogInformation($"Order {orderId} placed by {customerId} for {total:C}");
// Even the recommended structured approach allocates parameter arrays
logger.LogInformation("Order {OrderId} placed by {CustomerId} for {Total}",
orderId, customerId, total);

Under high throughput, string allocations from logging alone can create GC pressure that causes visible latency spikes. The standard LoggerMessage.Define pattern helps, but requires boilerplate for every message.

// This logs the full connection string, including the password
logger.LogError("Failed to connect: {ConnectionString}", connectionString);
// This leaks the JWT token into your log aggregator
logger.LogDebug("Auth header: {AuthHeader}", request.Headers.Authorization);
// This writes PII to disk in a GDPR-regulated environment
logger.LogInformation("User {Email} placed order {OrderId}", user.Email, orderId);

There is no standard mechanism to detect and redact secrets, API keys, or personally identifiable information before they reach the log output. Developers must remember to sanitize every call site — and they will forget.

// Without enrichment, you pass context manually at every call site
logger.LogInformation("Order placed. CorrelationId={CorrelationId}, UserId={UserId}, " +
"Machine={Machine}, RequestPath={Path}",
correlationId, userId, Environment.MachineName, httpContext.Request.Path);

When debugging a production incident across dozens of service replicas, you need every log entry to carry ambient context — correlation IDs, user identifiers, request paths, machine names. Passing these values explicitly at every call site is error-prone and clutters the business code.

Each concern — output targets, formatting, filtering, rate limiting, compliance — requires its own configuration surface. Developers cobble together Serilog enrichers, NLog targets, custom middleware, and manual redaction code. There is no single place to configure “production-ready logging” with one call.


Pragmatic.Logging wraps the standard Microsoft.Extensions.Logging infrastructure with a fluent PragmaticLoggingBuilder API. It provides:

  • Zero-allocation formatting — stack-allocated buffers and pooled StringBuilder instances eliminate GC pressure on hot paths
  • Automatic secret detection and PII redaction — pre-compiled regex patterns scan log content before it reaches any output provider
  • Context enrichment — ambient properties (correlation IDs, user identity, machine name) are attached to every log entry without changes to business code
  • Multiple providers — console, file, JSON, NDJSON, memory, debug, Windows Event Log, and custom providers
  • Configuration presets — one-call setup for Development, Production, High Performance, and Compliance scenarios
  • Rate limiting — lock-free token bucket, sliding window, and fixed window strategies prevent log spam
  • Audit trail — compliance-grade recording of every redaction event and sensitive data access
  • Standard interface — everything works through ILogger<T>. No proprietary abstractions.
// With Pragmatic: one builder, all concerns configured
builder.Services.AddLogging(logging =>
{
logging.ClearProviders();
var pragmatic = logging.AddPragmaticLogging();
pragmatic
.UseProductionPreset() // Sensible defaults for production
.AddConsole() // Colored terminal output
.AddFile("logs/app-{Date}.log") // Rolling file with retention
.AddJson(Console.OpenStandardOutput()); // Structured JSON for log aggregators
});

Or integrated with the Pragmatic.Composition host:

await PragmaticApp.RunAsync(args, app =>
{
app.UseLogging(log =>
{
log.AddConsole(PragmaticConsoleConfiguration.ForDevelopment());
log.AddFile("logs/app-{Date}.log");
});
});

Benchmarked against Serilog and NLog on .NET 10 (BenchmarkDotNet 0.14.0):

ScenarioPragmaticNLogSerilog
Simple logging21.87 ns / 64 B138.52 ns / 488 B155.06 ns / 440 B
Structured logging46.98 ns / 216 B279.84 ns / 696 B557.59 ns / 2016 B
Exception logging21.48 ns / 64 B134.99 ns / 504 B163.39 ns / 440 B
High volume (1000/iter)18.88 us / 64 KB134.75 us / 476 KB154.71 us / 430 KB

Pragmatic is 5.9x faster than NLog and 11.9x faster than Serilog for structured logging.


Pragmatic.Logging is built on three layers: the builder that configures the pipeline, the provider base that enforces the pipeline for every output target, and the providers that write log entries to their destinations.

The PragmaticLoggingBuilder is the entry point for all configuration. It extends ILoggingBuilder and provides a fluent API for adding providers, applying presets, enabling privacy features, and configuring performance options.

var pragmatic = logging.AddPragmaticLogging();
// Presets configure all knobs at once
pragmatic.UseProductionPreset();
// Then override individual settings
pragmatic.Configure(opts => opts.MinimumLevel = LogLevel.Debug);
// Add providers
pragmatic.AddConsole();
pragmatic.AddFile("logs/app-{Date}.log");
// Enable privacy features
pragmatic.EnableDataRedaction(r => r.RedactionPlaceholder = "[REDACTED]");
pragmatic.EnableAuditTrail(a => a.StorageType = "FileSystem");

Every provider — built-in or custom — extends PragmaticLoggerProviderBase. This base class enforces a consistent pipeline for every log entry:

Log Entry
|
v
[IsEnabled check] --no--> (dropped, zero work)
|
yes
v
[Advanced filter chain] --filtered--> (dropped, counter incremented)
|
pass
v
[Context enrichment] -- adds CorrelationId, UserId, MachineName, etc.
|
v
[Structured property filter] -- applies ContextFilterMode
|
v
[Data redaction]
|-- SecretDetector scans message + property values
|-- PragmaticDataRedactor checks property names + patterns
|-- Audit trail records each redaction event
|
v
[WriteLogCore] -- provider-specific output (console, file, JSON, etc.)

This means every provider automatically benefits from the full privacy stack, context enrichment, and filtering. You never need to implement redaction in a custom provider.

Each provider implements WriteLogCore and handles the specific output format and destination. Pragmatic.Logging ships with eight built-in providers:

ProviderClassOutput
ConsolePragmaticConsoleProviderColored terminal output with structured data
FilePragmaticFileProviderFile-based with rolling, retention, async I/O
JSONPragmaticJsonProviderStructured JSON for log aggregators
Enhanced JSONPragmaticEnhancedJsonProviderNDJSON with async buffering
MemoryPragmaticMemoryProviderIn-memory buffer for testing
DebugPragmaticDebugProviderDebug output window (Visual Studio, Rider)
NullPragmaticNullProviderNo-op for benchmarking
Windows Event LogPragmaticWindowsEventLogProviderWindows Event Log

.NET provides [LoggerMessage] for high-performance logging with source-generated methods. Pragmatic.Logging builds on this foundation and extends it.

Section titled “Standard LoggerMessage (recommended for hot paths)”
public partial class OrderService(ILogger<OrderService> logger)
{
[LoggerMessage(Level = LogLevel.Information, Message = "Order {OrderId} placed by {CustomerId}")]
partial void LogOrderPlaced(Guid orderId, string customerId);
public void PlaceOrder(Guid orderId, string customerId)
{
LogOrderPlaced(orderId, customerId);
}
}

The [LoggerMessage] attribute generates a partial method that:

  • Performs an IsEnabled check before any work
  • Uses cached delegates for zero-allocation parameter passing
  • Avoids string interpolation entirely
ApproachAllocationUse Case
[LoggerMessage] partial methodZeroHot paths, high-throughput code
logger.LogInformation("...", args)Low (parameter array)Business logic, moderate throughput
String interpolation $"..."High (string + boxing)Never in production code

Pragmatic.Logging provides a layered defense against accidental exposure of sensitive data.

The SecretDetector scans log messages and property values against a curated library of pre-compiled regex patterns. Each detection carries a confidence score and severity level.

CategoryExamplesSeverity
CryptoRSA/EC/PGP private keysCritical
API KeysAWS (AKIA...), GitHub PAT (ghp_...), Stripe (sk_live_...)Critical
TokensJWT (eyJ...), Bearer tokens, OAuth refresh tokensHigh
DatabaseConnection strings with Password=, MongoDB URIsCritical
CloudAzure Storage Key, GCP Service Account KeyCritical
ApplicationEncryption keys, hash saltsMedium

The SecretDetectionOptions.MinimumConfidenceForRedaction threshold (default: 0.7) controls which detections trigger automatic redaction. Detections below the threshold are logged for review but not redacted.

Beyond secret detection, the PragmaticDataRedactor handles PII and business-sensitive data based on property names and message content patterns:

  1. Explicit flag — Properties marked with PropertyCharacteristics.Redacted are always redacted
  2. Name matching — Property names checked against a HashSet<string> of sensitive names (case-insensitive) and compiled regex patterns
  3. Message scanning — Log message text scanned for emails, credit card numbers, SSNs, and custom patterns
pragmatic.EnableDataRedaction(redaction =>
{
redaction.RedactionPlaceholder = "[REDACTED]";
redaction.SensitivePropertyNames = ["password", "apiKey", "email", "ssn"];
redaction.PropertyNamePatterns = [@".*password.*", @".*secret.*"];
redaction.MessageRedactionPatterns = [@"[\w.+-]+@[\w-]+\.[\w.]+"]; // Email
});
StyleOutputDescription
Placeholder[API_KEY_REDACTED]Descriptive placeholder with secret type
PreserveLength********************************Asterisks matching original length
PreserveStructureeyJ***.***.***Keeps format markers (JWT segments)
Minimal[REDACTED]Simple constant replacement

Pre-built compliance templates configure the correct sensitive property lists, message patterns, redaction placeholders, audit requirements, and data retention periods:

StandardPlaceholderDeep RedactionAudit TrailRetention
GDPR[GDPR-REDACTED]YesYes2 years
HIPAA[PHI-REDACTED]YesYes6 years
PCI-DSS[CARD-DATA-REDACTED]YesYes1 year
CCPA[REDACTED]YesYes90 days
SOX[REDACTED]YesYes7 years
// GDPR-compliant logging
pragmatic.UseCompliancePreset(ComplianceStandard.Gdpr);
// Multi-compliance (uses most restrictive settings)
var config = ComplianceTemplates.CreateMultiCompliance(
includeGdpr: true, includeHipaa: true, includePciDss: false);

For hot paths, Pragmatic.Logging provides infrastructure that eliminates heap allocations.

Small messages (up to 1,024 characters) are formatted into a stackalloc buffer. Larger messages fall back to ArrayPool<char>.Shared so the rented array is reused across calls.

The core formatting engine uses a ThreadLocal<StringBuilder> pool and ArrayPool<object?> for parameter arrays, minimizing allocations on every call. It has specialized handlers for common types:

TypeAllocationMethod
int, long, float, double, decimalZeroTryFormat(Span<char>)
DateTime, DateTimeOffsetZeroTryFormat with ISO 8601
TimeSpan, GuidZeroTryFormat
boolZeroLiteral copy
stringZeroAsSpan().CopyTo()
ISpanFormattableZeroInterface TryFormat
Other1 allocationToString() fallback
PoolPurpose
StringBuilderPoolReuses StringBuilder instances with automatic capacity management
DefaultObjectPool<T>Generic pool for LogEntry objects, formatter state, serialization buffers
PropertyPoolReuses Dictionary<string, object?> instances for structured log properties

Every provider performs an IsEnabled check with [MethodImpl(AggressiveInlining)] before any formatting work begins. If the log level is below the configured minimum, the call returns immediately with zero work done.


Context enrichment automatically attaches ambient information to every log entry without requiring the developer to pass values explicitly at every call site.

ProviderPriorityScopeKey Properties
HttpContextProvider100Per-requestRequestPath, RequestMethod, UserId, RemoteIpAddress
CorrelationIdProvider50Per-requestCorrelationId, TraceId, SpanId
ThreadContextProvider500Per-callThreadId, ThreadName, IsBackground
ProcessContextProvider900Per-processProcessId, ProcessName, AppVersion
MachineContextProvider1000Per-machineMachineName, OSVersion, CLRVersion

Extend ContextProviderBase for domain-specific context:

public sealed class TenantContextProvider : ContextProviderBase
{
private readonly ITenantContext _tenantContext;
public TenantContextProvider(ITenantContext tenantContext)
: base("Tenant", priority: 80) { _tenantContext = tenantContext; }
public override bool IsAvailable() => _tenantContext.CurrentTenantId is not null;
public override IReadOnlyDictionary<string, object?> GetContextProperties()
=> CreatePropertiesDictionary(
("TenantId", _tenantContext.CurrentTenantId),
("TenantName", _tenantContext.CurrentTenantName));
}

Two middleware components wire enrichment into the HTTP pipeline:

  • LoggingEnrichmentMiddleware — Creates a logging scope with correlation ID, request path, request method, and user identity. Optionally logs request start/completion with timing and maps HTTP status codes to log levels (5xx = Error, 4xx = Warning, 2xx/3xx = Information).

  • BaggagePropagationMiddleware — Propagates selected context values into Activity.Baggage for automatic forwarding to downstream services via W3C Baggage headers.

app.UseMiddleware<BaggagePropagationMiddleware>();
app.UseMiddleware<LoggingEnrichmentMiddleware>();

Each provider can independently control which context properties it includes via ContextFilterConfiguration. This lets you add full context to file logs while keeping console output compact.

logging.AddConsole(config =>
{
config.ContextFilter = new ContextFilterConfiguration
{
Mode = ContextFilterMode.Include,
PropertyNames = new HashSet<string> { "CorrelationId", "UserId" }
};
});
ModeBehavior
AllInclude all context properties (default)
IncludeInclude only listed properties
ExcludeInclude all except listed properties
NoneInclude no context properties

PragmaticLoggingBuilder provides four built-in presets that configure all knobs at once.

pragmatic.UseDevelopmentPreset();
  • MinimumLevel: Trace
  • HighPerformanceMode: disabled
  • Telemetry: disabled
  • Machine context: enabled, user context: disabled
pragmatic.UseProductionPreset();
  • MinimumLevel: Information
  • Background processing: enabled, buffer size 5000
  • Rate limiting: enabled (1000 msg/sec)
  • Correlation IDs and user context: enabled
pragmatic.UseHighPerformancePreset();
  • MinimumLevel: Information (skips Debug/Trace)
  • HighPerformanceMode: enabled
  • Zero-allocation optimizations: enabled
  • Buffer size: 10000, flush threshold: 1000
pragmatic.UseCompliancePreset(ComplianceStandard.Gdpr);
  • Redaction: enabled
  • Secret detection: enabled
  • Audit trail: enabled with database storage
  • Confidence thresholds vary by standard (GDPR: 0.8, HIPAA: 0.9, PCI-DSS: 0.95)

Presets set sensible defaults. Override individual settings after applying a preset:

pragmatic
.UseProductionPreset()
.Configure(opts => opts.MinimumLevel = LogLevel.Debug);

Under sustained load, a noisy log category can produce thousands of duplicate messages per second. The HighPerformanceRateLimiter throttles log output using one of three strategies without blocking the caller.

StrategyAlgorithmBehavior
TokenBucketToken bucketAllows bursts up to bucket size, then rate-limits. Best for most scenarios.
SlidingWindowSliding windowPrecise rate over a moving window. Best for strict compliance.
FixedWindowFixed windowSimple counter reset at boundary. Fastest, but allows edge bursts.
pragmatic.EnableRateLimiting(rl =>
{
rl.MaxMessagesPerSecond = 1000;
rl.BurstSize = 100;
rl.Strategy = "TokenBucket";
});

Synchronous writes to disk or network block the calling thread. Pragmatic.Logging decouples log production from log output.

When enabled, log entries are enqueued into a bounded channel. A background consumer thread drains the queue and writes entries in batches.

Caller thread Background thread
| |
|-- Enqueue(entry) --> |
| (non-blocking) |
| [Wait for batch/timer]
| |
| [WriteLogCore(batch)]
StrategyData LossBlocking
DropOldestOldest entriesNo
DropNewestNewest entriesNo
BlockNoneYes
ExpandNoneNo

For production, DropOldest is recommended. Use Block only in compliance-critical scenarios.

Individual disk writes are expensive. Batching amortizes the cost by collecting entries until either the BatchSize threshold or FlushTimeout is reached.

ScenarioRecommended Batch SizeFlush Interval
Web API (moderate traffic)50-1001-2 seconds
High-throughput service200-5005-10 seconds
Real-time streaming10-20100 ms
Compliance (every entry matters)25-50500 ms

The PragmaticAuditService records every redaction event, sensitive data access, and compliance violation into a durable audit store. It runs as an IHostedService with batch processing and configurable flush intervals.

StorageClassPersistenceBest For
MemoryMemoryAuditStorageIn-processTesting, development
File SystemFileSystemAuditStorageDiskSingle-server deployments
PolicyDescription
DefaultRecords all redaction and access events
GDPRRecords everything, forces immediate flush for violations
HighPerformanceOnly records high-severity events
DevelopmentMinimal auditing for fast iteration
pragmatic.EnableAuditTrail(audit =>
{
audit.StorageType = "FileSystem";
audit.PolicyType = "GDPR";
audit.BatchSize = 100;
audit.FlushIntervalSeconds = 30;
});

For logging before the DI container is built (during startup configuration):

using var bootstrap = BootstrapLogger.Create();
bootstrap.LogInformation("Starting application...");
var builder = WebApplication.CreateBuilder(args);
// ... configure services ...

All options bind to the PragmaticLogging configuration section:

{
"PragmaticLogging": {
"Enabled": true,
"MinimumLevel": "Information",
"HighPerformanceMode": false,
"Privacy": {
"EnableRedaction": true,
"EnableSecretDetection": true,
"ComplianceStandard": "General",
"SecretDetection": {
"MinimumSecretLength": 8,
"PrecompilePatterns": true,
"MinimumConfidenceForRedaction": 0.7
},
"DataRedaction": {
"RedactionPlaceholder": "[REDACTED]",
"SensitivePropertyNames": ["password", "secret"]
}
},
"Performance": {
"BufferSize": 1000,
"FlushThreshold": 100,
"EnableZeroAllocation": true,
"UseBackgroundProcessing": true
},
"RateLimiting": {
"Enabled": false,
"MaxMessagesPerSecond": 1000,
"Strategy": "TokenBucket"
},
"Providers": {
"Console": { "Enabled": true, "UseColors": true },
"File": { "Enabled": true, "BasePath": "./logs", "MaxFileSizeMB": 100 }
}
}
}

Pragmatic.Logging integrates with the Pragmatic.Composition host via UseLogging():

await PragmaticApp.RunAsync(args, app =>
{
app.UseLogging(log =>
{
log.AddConsole(PragmaticConsoleConfiguration.ForDevelopment());
log.AddFile("logs/app-{Date}.log");
});
});

After registration, inject and use ILogger<T> as usual. Pragmatic.Logging handles enrichment, redaction, and routing behind the scenes:

public class OrderService(ILogger<OrderService> logger)
{
public void ProcessOrder(int orderId)
{
logger.LogInformation("Processing order {OrderId}", orderId);
}
}

Implement IPragmaticLoggerProvider or extend PragmaticLoggerProviderBase for custom output targets. The base class handles configuration, metrics, health checks, enrichment, and redaction — you only implement WriteLogCore.

public sealed class SlackAlertProvider : PragmaticLoggerProviderBase
{
protected override void WriteLogCore(LogEntry logEntry)
{
if (logEntry.LogLevel < LogLevel.Error) return;
// Send to Slack webhook
}
}

TypeNamespacePurpose
PragmaticLoggingBuilderPragmatic.LoggingFluent configuration API
PragmaticLoggingOptionsPragmatic.Logging.ConfigurationMain options class
SecretDetectorPragmatic.Logging.PrivacySecret detection engine
PragmaticDataRedactorPragmatic.Logging.PrivacyData redaction service
PragmaticAuditServicePragmatic.Logging.Privacy.AuditAudit trail service
PragmaticFileProviderPragmatic.Logging.ProvidersHigh-performance file provider
PragmaticConsoleProviderPragmatic.Logging.ProvidersConsole provider
PragmaticJsonProviderPragmatic.Logging.ProvidersJSON/NDJSON provider
PragmaticMemoryProviderPragmatic.Logging.ProvidersIn-memory provider for testing
LoggingEnrichmentMiddlewarePragmatic.Logging.AspNetCoreASP.NET Core enrichment middleware
BaggagePropagationMiddlewarePragmatic.Logging.AspNetCoreDistributed tracing baggage propagation
BootstrapLoggerPragmatic.Logging.AspNetCorePre-DI logger
ContextManagerPragmatic.Logging.ContextContext provider registry
HighPerformanceRateLimiterPragmatic.LoggingLock-free rate limiter
ZeroAllocMessageFormatterPragmatic.Logging.ZeroAllocationZero-allocation message formatting
StringBuilderPoolPragmatic.Logging.ObjectPoolingPooled StringBuilder instances