Logging feels simple… until you’re staring at a production incident and your logs are either uselessly noisy or weirdly empty.
Here are 10 .NET logging mistakes that quietly hurt you in production - and how to fix them.
1. Treating Logs as Plain Strings
The mistake
_logger.LogInformation("User " + userId + " purchased " + itemCount + " items");
Everything is one big string, so your logging system can’t query individual fields.
Why it hurts
- No structured search (e.g.
UserId = 123) - Hard to build dashboards on numeric fields
- Formatting drifts over time
Fix
Use structured properties:
_logger.LogInformation(
"User {UserId} purchased {ItemCount} items",
userId,
itemCount);
If you want to filter or graph it later, give it a named property.
2. Using the Same Log Level for Everything
The mistake
Everything is LogInformation or everything is LogError.
Why it hurts
- Can’t tell what’s actually broken
- Filtering by severity is useless
- Alerting becomes noise
Fix
Use levels with intent:
-
Trace– ultra-detailed, usually local only -
Debug– dev diagnostics -
Information– key business events -
Warning– unexpected but not fatal -
Error– failed operation -
Critical– system is in real trouble
Example
_logger.LogInformation("Processing order {OrderId}", orderId);
if (!inventoryAvailable)
_logger.LogWarning("Inventory missing for order {OrderId}", orderId);
try
{
await _paymentService.ChargeAsync(order);
}
catch (PaymentException ex)
{
_logger.LogError(ex, "Payment failed for order {OrderId}", orderId);
throw;
}
3. Spamming Logs Instead of Capturing Signal
The mistake
- Logging every function entry/exit
- Logging every item in big loops
- Serializing massive objects on every request
Why it hurts
- Log volume (and cost) explodes
- Performance drops
- Important lines get buried
Fix
- Log at boundaries: incoming requests, external calls, DB operations, key decisions, errors
- Avoid per-item logs in hot loops
- Use sampling for progress logs:
for (var i = 0; i < items.Count; i++)
{
if (i % 100 == 0)
_logger.LogDebug("Processed {Count} items so far", i);
Process(items[i]);
}
4. Logging Sensitive Data
The mistake
_logger.LogInformation("User {Email} logged in with password {Password}", email, password);
Yes, this happens.
Why it hurts
- Massive security and compliance risk
- If logs leak, attackers get passwords, tokens, PII
Fix
- Maintain a “never log” list: passwords, tokens, card numbers, national IDs, etc.
- Log identifiers, not secrets:
_logger.LogInformation("User {UserId} logged in", userId);
- Consider wrappers whose
ToString()returns[REDACTED]for secrets - Make “no sensitive data in logs” part of your review checklist
5. No Correlation ID or Context
The mistake
Each log line is isolated:
_logger.LogInformation("Started payment");
_logger.LogInformation("Calling gateway");
_logger.LogError("Payment failed");
With multiple services and threads, you can’t follow a single request.
Why it hurts
- Debugging across services is painful
- Hard to answer: “What happened to this request?”
Fix
Use scopes with correlation IDs:
using (_logger.BeginScope(new Dictionary<string, object>
{
["CorrelationId"] = correlationId,
["UserId"] = userId
}))
{
_logger.LogInformation("Processing payment");
// everything here includes CorrelationId and UserId
}
- Generate or accept a
CorrelationIdper request - Propagate it in headers to downstream services
- Make it a first-class field in your queries/dashboards
6. Mixing Logging Frameworks Instead of Using ILogger<T>
The mistake
- Some code uses
ILogger<T> - Some uses NLog directly
- Some uses
Console.WriteLine - Some uses
Trace.WriteLine
Why it hurts
- Config and behavior are inconsistent
- Hard to swap providers (Serilog, NLog, App Insights, etc.)
- Testing/mocking logging is messy
Fix
Standardize on Microsoft.Extensions.Logging in app code:
public class MyService
{
private readonly ILogger<MyService> _logger;
public MyService(ILogger<MyService> logger)
{
_logger = logger;
}
public void DoWork()
{
_logger.LogInformation("Doing work");
}
}
Configure the actual provider (Serilog, etc.) at startup only:
Host.CreateDefaultBuilder(args)
.UseSerilog((context, services, configuration) =>
{
configuration
.ReadFrom.Configuration(context.Configuration)
.ReadFrom.Services(services);
});
App code = abstraction. Startup = implementation.
7. Same Logging Config in Every Environment
The mistake
Dev, test, prod — all using the same log levels and sinks.
Why it hurts
- Dev: too noisy, hard to see real issues
- Prod: too noisy, too expensive, or not verbose enough when you need it
Fix
Use appsettings.*.json:
// Development
"Logging": {
"LogLevel": {
"Default": "Debug",
"Microsoft": "Information"
}
}
// Production
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning"
}
}
And override via environment variables when you need temporary extra verbosity:
Logging__LogLevel__MyAppNamespace=Debug
8. Logging Exceptions Poorly (or Twice)
The mistakes
Logging only the message:
catch (Exception ex)
{
_logger.LogError("Something failed: " + ex.Message);
}
Logging in multiple layers for the same exception:
// Repo
catch (Exception ex)
{
_logger.LogError(ex, "Failed in repository");
throw;
}
// Service
catch (Exception ex)
{
_logger.LogError(ex, "Failed in service");
throw;
}
Why it hurts
- Missing stack traces
- Duplicate error logs, inflated dashboards
Fix
- Always pass the exception:
catch (Exception ex)
{
_logger.LogError(ex, "Failed to process order {OrderId}", orderId);
throw;
}
- Decide where errors get logged (usually at the edges: request handlers, background workers)
- Consider global exception handling (middleware) to centralize error logging
9. Ignoring Performance Cost of Logging
The mistake
_logger.LogDebug("Result: " + JsonSerializer.Serialize(bigObject));
Even if Debug is off, the string concatenation and serialization still happen.
Why it hurts
- Wasted CPU
- Extra allocations and GC pressure
- Slower hot paths
Fix
- Use structured templates:
_logger.LogDebug("Result: {@Result}", bigObject);
- Guard expensive logs:
if (_logger.IsEnabled(LogLevel.Debug))
{
_logger.LogDebug("Detailed result: {@Result}", bigObject);
}
- Avoid logging giant payloads by default; log summaries instead
10. Having Logs but No Logging Strategy
The mistake
Every dev logs in their own style. There’s no shared idea of:
- What to log
- At which level
- Where to look during incidents
Why it hurts
- Inconsistent logs are harder to read
- New devs repeat old mistakes
- Incidents take longer to debug
Fix
Create a lightweight team logging guide:
- What to log: requests, external calls, key domain events, warnings, errors
-
Property names:
UserId,OrderId,CorrelationId(pick a standard) -
Levels: examples of what counts as
InformationvsWarningvsError - Where logs go: link to your log system and common queries
Review and update it after real incidents.
Quick .NET Logging Checklist
Before you ship:
- [ ] Using structured properties, not string concatenation
- [ ] Levels (
Info/Warn/Error) actually mean something - [ ] No passwords/tokens/PII in logs
- [ ] Correlation ID included where it matters
- [ ] Environment-specific log levels configured
- [ ] Exceptions logged once, with stack trace
About me
I’m a Systems Architect who is passionate about distributed systems, .Net clean code, logging, performance, and production debugging.
- 🧑💻 Check out my projects on GitHub: mashrulhaque
- 👉 Follow me here on dev.to for more .NET posts
Top comments (0)