Common Mistakes
These are the most common issues developers encounter when using Pragmatic.Logging. Each section shows the wrong approach, the correct approach, and explains why.
1. Using String Interpolation Instead of Structured Logging
Section titled “1. Using String Interpolation Instead of Structured Logging”Wrong:
logger.LogInformation($"Order {orderId} placed by {customerId} for {total:C}");Right:
logger.LogInformation("Order {OrderId} placed by {CustomerId} for {Total}", orderId, customerId, total);Why: String interpolation creates a new string on every call, even when the log level is disabled. Structured logging preserves parameter names as semantic properties, enabling search and filtering in log aggregators. The $"" form allocates unconditionally and loses the structured property names.
For hot paths, use [LoggerMessage]:
[LoggerMessage(Level = LogLevel.Information, Message = "Order {OrderId} placed by {CustomerId}")]partial void LogOrderPlaced(Guid orderId, string customerId);2. Forgetting to Call ClearProviders Before Adding Pragmatic
Section titled “2. Forgetting to Call ClearProviders Before Adding Pragmatic”Wrong:
builder.Services.AddLogging(logging =>{ var pragmatic = logging.AddPragmaticLogging(); pragmatic.AddConsole(); // Now you have TWO console providers});Right:
builder.Services.AddLogging(logging =>{ logging.ClearProviders(); // Remove the default console provider
var pragmatic = logging.AddPragmaticLogging(); pragmatic.AddConsole();});Why: ASP.NET Core registers a default ConsoleLoggerProvider automatically. If you add PragmaticConsoleProvider without clearing first, every log entry is written to the console twice — once by each provider. The Pragmatic provider includes structured data, colored output, and redaction; the default provider does not. Clear providers first, then add only the ones you need.
3. Logging Sensitive Data Without Enabling Redaction
Section titled “3. Logging Sensitive Data Without Enabling Redaction”Wrong:
// No redaction configured -- this writes the password to disklogger.LogError("Login failed for {Username} with password {Password}", username, password);
// Connection string with embedded credentialslogger.LogInformation("Connected to {ConnectionString}", connectionString);Right:
// Option A: Don't log sensitive data in the first placelogger.LogError("Login failed for {Username}", username);
// Option B: Enable automatic redactionpragmatic.EnableDataRedaction(redaction =>{ redaction.SensitivePropertyNames = ["password", "secret", "apiKey", "connectionString"]; redaction.PropertyNamePatterns = [@".*password.*", @".*secret.*", @".*token.*"];});
// Option C: Enable secret detection for automatic pattern matchingpragmatic.Configure(opts =>{ opts.Privacy.EnableSecretDetection = true; opts.Privacy.SecretDetection.PrecompilePatterns = true;});Why: The best defense is not logging sensitive data at all. When that is not practical (connection strings in error diagnostics, tokens in debug logs), enable data redaction to catch sensitive values before they reach the output. Secret detection catches patterns like JWT tokens, AWS keys, and connection string passwords even when the property name does not hint at sensitivity.
4. Using the Development Preset in Production
Section titled “4. Using the Development Preset in Production”Wrong:
// In production codepragmatic.UseDevelopmentPreset() .AddFile("logs/app-{Date}.log");Right:
pragmatic.UseProductionPreset() .AddFile("logs/app-{Date}.log");Or use environment-based selection:
if (builder.Environment.IsDevelopment()) pragmatic.UseDevelopmentPreset();else pragmatic.UseProductionPreset();Why: The Development preset sets MinimumLevel to Trace, disables background processing, and disables rate limiting. In production, this means:
- Every Debug and Trace message is processed, creating massive log volumes
- Synchronous writes block the calling thread on every log call
- No rate limiting allows a noisy component to fill your disk in minutes
The Production preset enables background processing, sets MinimumLevel to Information, enables rate limiting, and turns on correlation IDs.
5. Not Configuring File Retention Policies
Section titled “5. Not Configuring File Retention Policies”Wrong:
// No retention configured -- log files grow foreverpragmatic.AddFile("logs/app-{Date}.log");Right:
pragmatic.AddFile("logs/app-{Date}.log", opts =>{ opts.MaxFileSizeMB = 100; // Roll at 100 MB opts.RetentionDays = 30; // Delete files older than 30 days opts.CompressOldFiles = true; // GZip compress rolled files});Why: Without retention policies, log files accumulate until they fill the disk. In a high-throughput service, this can happen within days. The file provider supports automatic rolling by size and time, retention policies for automatic cleanup, and compression of old files. Always configure at least RetentionDays for any production file provider.
6. Placing Enrichment Middleware Too Late in the Pipeline
Section titled “6. Placing Enrichment Middleware Too Late in the Pipeline”Wrong:
var app = builder.Build();
app.UseAuthentication();app.UseAuthorization();app.MapControllers();
// Too late -- all controller logs miss the enrichment contextapp.UseMiddleware<LoggingEnrichmentMiddleware>();Right:
var app = builder.Build();
// Early in the pipeline -- all downstream middleware and handlers benefitapp.UseMiddleware<BaggagePropagationMiddleware>();app.UseMiddleware<LoggingEnrichmentMiddleware>();
app.UseAuthentication();app.UseAuthorization();app.MapControllers();Why: The enrichment middleware creates a logging scope that includes CorrelationId, RequestPath, RequestMethod, and UserId. If it runs after your handlers, the log entries produced during request processing will not have these properties. Place enrichment middleware early so that all downstream components — including authentication, authorization, and your business logic — produce enriched log entries.
Note: BaggagePropagationMiddleware should run before LoggingEnrichmentMiddleware so that distributed tracing context is available for enrichment.
7. Ignoring Queue Overflow Strategy in High-Throughput Scenarios
Section titled “7. Ignoring Queue Overflow Strategy in High-Throughput Scenarios”Wrong:
// Default Block strategy -- under heavy load, log calls block the request threadpragmatic.Configure(opts =>{ opts.Performance.UseBackgroundProcessing = true; opts.Performance.MaxQueueSize = 100; // Very small queue // OverflowStrategy defaults to Block});Right:
pragmatic.Configure(opts =>{ opts.Performance.UseBackgroundProcessing = true; opts.Performance.MaxQueueSize = 10000; opts.Performance.OverflowStrategy = QueueOverflowStrategy.DropOldest;});Why: When the background queue fills up, the Block strategy makes the calling thread wait until space is available. In a web API, this means request threads block on log calls, causing timeouts and cascading failures. DropOldest is the safe default for production — it keeps the most recent entries (which are usually most relevant for debugging) and never blocks the caller.
Use Block only in compliance-critical scenarios where every log entry must be persisted (audit logs, financial transactions).
8. Mixing Global and Per-Provider Privacy Configuration
Section titled “8. Mixing Global and Per-Provider Privacy Configuration”Wrong:
// Global redaction disabled, but expecting per-provider redaction to workpragmatic.Configure(opts => opts.Privacy.EnableRedaction = false);
// This provider-level setting has no effect when global is disabledpragmatic.AddFile("logs/app.log", config =>{ config.Privacy.EnableRedaction = true; config.Privacy.RedactionMode = RedactionMode.Aggressive;});Right:
// Enable global redaction, then configure per-providerpragmatic.Configure(opts => opts.Privacy.EnableRedaction = true);
// Aggressive redaction for file logspragmatic.AddFile("logs/app.log", config =>{ config.Privacy.RedactionMode = RedactionMode.Aggressive;});
// No redaction for in-memory testingpragmatic.AddMemory(); // Memory provider uses its own privacy settingsWhy: Privacy redaction runs in the PragmaticLoggerProviderBase pipeline. The global EnableRedaction flag acts as a master switch. When it is false, per-provider redaction settings are ignored because the redaction step is skipped entirely. Enable redaction globally, then use per-provider RedactionMode to control aggressiveness per output target.
9. Not Using Presets When a Standard Configuration Exists
Section titled “9. Not Using Presets When a Standard Configuration Exists”Wrong:
// Manually configuring 15 settings that match the Production presetpragmatic.Configure(opts =>{ opts.MinimumLevel = LogLevel.Information; opts.Performance.UseBackgroundProcessing = true; opts.Performance.BufferSize = 5000; opts.Context.IncludeCorrelationId = true; opts.Context.IncludeUserContext = true; opts.RateLimiting.Enabled = true; opts.RateLimiting.MaxMessagesPerSecond = 1000; opts.EnableTelemetry = true; // ... more settings});Right:
// Use the preset, then override only what differspragmatic .UseProductionPreset() .Configure(opts => opts.MinimumLevel = LogLevel.Debug); // Override one settingWhy: Presets exist to provide tested, coherent configurations. Manually replicating a preset means you might miss a setting (like rate limiting burst size) or introduce an inconsistency (like enabling background processing but setting the buffer too small). Start with a preset and override only what your application needs to change.
10. Not Registering OptionalConverterFactory When Using Compliance Exports
Section titled “10. Not Registering OptionalConverterFactory When Using Compliance Exports”Wrong:
// Audit export includes sensitive metadata, but the endpoint has no redactionapp.MapGet("/api/audit/export", async (PragmaticAuditService audit) =>{ var csv = await audit.ExportAuditAsync( from: DateTime.UtcNow.AddDays(-90), until: DateTime.UtcNow, format: AuditExportFormat.Csv); return Results.Text(csv, "text/csv");});Right:
// Protect audit export endpoints with authorizationapp.MapGet("/api/audit/export", async (PragmaticAuditService audit) =>{ var csv = await audit.ExportAuditAsync( from: DateTime.UtcNow.AddDays(-90), until: DateTime.UtcNow, format: AuditExportFormat.Csv); return Results.Text(csv, "text/csv");}).RequireAuthorization("AuditAccess");Why: Audit trail data contains metadata about what was redacted, when, and by whom. If you expose audit export endpoints without authorization, you are effectively leaking information about where sensitive data exists in your system. Always protect audit endpoints with appropriate authorization policies.