Why Your Logging Strategy Fails in Production
Let me tell you what I’ve learned over the years from watching teams deploy logging strategies that looked great on paper and failed spectacularly at 3 AM when production burned.
It’s not that they didn’t know the theory. They’d read the Azure documentation. They’d seen the structured logging samples. They’d studied distributed tracing. The real problem was different: they knew what to do but had no idea why it mattered until production broke catastrophically.
This article isn’t about generic “best practices” or theoretical frameworks. Instead, it’s about the specific, concrete ways logging strategies fail in real production systems—why teams log things that don’t actually help, miss logging things that critically do, and build expensive observability infrastructure that doesn’t deliver when it matters most.
And I’m quite confident that your team is already doing at least two of these things right now.
The Core Problem: Logging Isn’t About Logging
Here’s the fundamental issue: most teams approach logging in a fundamentally backward way. They start by asking themselves: “What should we log?”
That’s completely wrong. The right question—the one that changes everything—is: “What information do we absolutely need to diagnose a production failure when everything is burning?”
Because logging isn’t a feature. It’s insurance. And like all insurance, you want to pay the minimum premium for maximum coverage. You don’t insure against every possible outcome; you insure against the catastrophic ones.
Anti-Pattern 1: Logging Everything “Just in Case”
I’ve seen applications log 50+ MB per request. Developers reasoned with apparent logic: “More data = better debugging.”
This is not just wrong. It’s catastrophically wrong. And I can prove it with concrete math and real-world consequences.
The Reality of Excessive Logging
Let’s walk through a concrete example. Consider a typical e-commerce order processing request that touches multiple services. A well-intentioned developer adds “detailed diagnostic logging” at every single step—serializing objects, logging variable states, capturing full request/response payloads. It seems reasonable. It looks thorough. It feels safe.
Then production hits real load. Assume 100 requests per second, each with 5 MB of unfiltered diagnostic data. That’s 500 MB per second of logs flowing into your systems. Your log ingestion pipeline starts struggling. You’re either dropping logs or compressing aggressively (and losing critical detail). Your monthly storage bill—depending on your tool and retention policy—can easily escalate from a comfortable $200 to several thousand dollars. The actual impact varies depending on your setup: Application Insights charges per GB ingested, Datadog per host/span volume, Elasticsearch per GB stored. It’s not always catastrophic, but it’s significant enough to force painful cost-cutting decisions.
But more importantly than cost, here’s what actually happens in practice:
- Search becomes genuinely frustrating. With gigabytes of noise, finding a specific error means sifting through thousands of irrelevant entries. A query for “payment timeout” returns 500 results. Which one is actually yours? You don’t know.
- Logs stop being useful entirely. Not because they’re stored badly, but because finding signal in the noise takes longer than just restarting the service and hoping it works. So teams gradually stop using logs for diagnosis and instead use luck.
- Real problems hide effectively. The actual error is there somewhere, buried in noise about every intermediate step, every variable assignment, every function entry. By the time you find it, the incident is already over and customers are angry.
- You’re paying for data nobody uses. Not $13,000/day in runaway costs, but definitely enough to notice and enough to make management ask questions.
This is exactly what happens when you optimize for completeness instead of signal.
The solution is surprisingly simple: Log only what you’d actually need to diagnose a failure. Not what might be useful someday. Not “this function was called.” Not “this variable is 42.” Only things that directly help answer: “Why did this critical operation fail?”
In concrete terms: when an order fails, you truly need to know what failed and why. Did validation reject it? Did payment timeout? Did the warehouse queue overflow? Did inventory run out? Each failure mode has a completely different cause and a different fix. So you log specifically for those scenarios, not for everything in between.
A typical refactoring looks like this: instead of logging every intermediate step (retrieved order, started validation, started payment, called warehouse), you log only outcome points (order complete, order failed with specific reason X). This cuts noise by roughly 80% while actually improving diagnostic value. You know what mattered. You can find it in seconds.
Anti-Pattern 2: Fire-and-Forget Observability
You’ve attended a cloud architecture conference. You heard talks about observability and its importance. You read the Microsoft Learn documentation on Application Insights. You diligently configured it—set up the Azure SDK, added OpenTelemetry, made sure logs flow reliably to the cloud.
You check the box: “Observability: Done.” Problem solved, right?
Then production breaks at 2 AM. You wake up. You go to Application Insights and… find nothing useful. No signal, just noise. So you deploy a quick fix with logging at DEBUG level. Now you have terabytes of noise flooding in. You restart the service and hope it doesn’t happen again. Problem “fixed” (until it does).
This pattern happens constantly. Not because Application Insights is fundamentally bad. Not because you’re incompetent. But because observability was never actually designed for your specific application and your specific failure modes. You bought expensive tools. You installed them correctly. You patted yourself on the back. Then you walked away without thinking deeply.
Observability without genuine understanding isn’t observability. It’s just expensive logging theater—looking good in slides but useless when it matters.
Real observability requires answering three critical questions:
First: What are the critical paths in your system? Not every code path. The ones that, if they break, create real incidents and wake people up. In e-commerce: order placement, payment processing, inventory updates. In SaaS: user authentication, data export, billing operations. In APIs: request validation, database queries, external service calls. You need to identify and understand these before you write a single log statement.
Second: What can go wrong on each of these paths? Not everything theoretically possible. The specific failure modes you’ve actually seen in production or can reasonably expect based on your architecture. Payment timeout? Insufficient funds? Database deadlock? API rate limiting? Service unavailable? Malformed request? Rate limit exceeded? Each has a completely different diagnosis path and different fix. So you log for each of these specific scenarios, not for the thousands of things that don’t go wrong.
Third: What minimum information do I need to diagnose each specific failure? Not “all the data.” Not the entire request. The minimum information that tells you which specific failure mode occurred and why. For a payment timeout, you need: order ID, amount, payment provider, timeout duration, retry count. You don’t need the entire customer object serialized. You don’t need the full response payload. You need the signal, not the noise.
Then—and only then—you instrument for exactly those scenarios. Not generically. Specifically and intentionally.
In practice, this means source-generated log methods (using LoggerMessage) for each specific failure mode. Not generic “OrderProcessingStarted” and “OrderProcessingEnded” messages. Instead: “PaymentTimeout,” “PaymentDeclined,” “WarehouseQueueFull,” “InventoryInsufficient.” Each log message tells you exactly what state the system entered and what concrete cause triggered it.
Anti-Pattern 3: Logging Without Correlation
A customer reports: “My order didn’t process.” In a microservices architecture, that single request touched four different services. Now you’re essentially a detective trying to solve a mystery.
Without correlation IDs, finding the relevant logs across four different services becomes tedious, frustrating detective work. You search for “order timeout” and get 6 different orders from across the entire day. Which one is actually theirs? You cross-reference timestamps. You check payment logs. You check warehouse logs. You piece together a story. 30 minutes later, you finally find it. By then, the incident is already over. The customer has called your support team twice. You’re exhausted.
With proper correlation, one single trace ID connects everything together. ASP.NET Core generates this automatically—it’s called HttpContext.TraceIdentifier. The same trace ID flows through every log entry for that specific request, across every service it touches. When a customer reports “my order didn’t process,” you search by that one trace ID and see every step: API received it, validation passed, payment service timed out, warehouse was never notified. Done. You understand the entire story in 30 seconds instead of 30 minutes.
The W3C Trace Context standard makes this correlation work across service boundaries. It’s built into ASP.NET Core natively. You get it for free. But there’s a crucial requirement: you have to structure your logs so the trace ID is actually queryable—which means using structured logging (key-value pairs, not free-form text blobs).
Anti-Pattern 4: Logging Performance Secrets
Here’s a pattern I’ve seen derail production performance more often than most people admit: logging that hurts performance so severely that teams simply disable observability rather than pay the performance cost.
Your application runs beautifully on your local machine. You ship it to production. Suddenly in production, it feels sluggish. Latency starts climbing. P95 latency goes from 50ms to 200ms. Users complain. You add more logging to debug the slow path. Now it’s even slower. Much, much slower. You profile the application and find the surprising culprit: the logging itself is the bottleneck.
This is the moment most teams give up on observability entirely. “It’s too expensive,” they say. What they really mean: “We instrumented it wrong and now we’re paying the performance price.”
The culprit: string formatting and object serialization happening automatically regardless of whether anyone is listening. You’re serializing objects, building strings, allocating temporary memory—all of it discarded if the log level isn’t even enabled. This is particularly insidious because it only hurts production performance (where logging is at higher levels) while looking perfectly fine in local testing (where you control the verbosity level).
// KILLER: Always executes expensive work
logger.LogDebug("Processing user. FullDetails: {Details}",
JsonConvert.SerializeObject(complexUser));
// BETTER: Guards it, but still wasteful
if (logger.IsEnabled(LogLevel.Debug))
logger.LogDebug("Processing user. FullDetails: {Details}",
JsonConvert.SerializeObject(complexUser));
// BEST: Source-generated logging—zero overhead when disabled
[LoggerMessage(Level = LogLevel.Debug, Message = "Processing user. UserId={UserId}")]
public static partial void ProcessingUser(this ILogger logger, int userId);
In production with Debug logging disabled, the first version still executes the expensive serialization anyway. That’s performance death by a thousand cuts. The template parser runs. The object is serialized. The memory is allocated. Only then does the code check “is debug level enabled?” and discard the entire result. Wasted CPU cycles. Wasted memory. And this happens repeated thousands of times per second.
This is exactly the kind of hidden performance killer that shows up and hurts production but not in load tests. Because load tests usually don’t add this kind of logging to their code paths.
The Solution: Source-Generated Logging
Source-generated logging (LoggerMessage attribute, .NET 6+) completely flips this on its head. The compiler generates code at build time that knows: “this parameter matters, that one doesn’t. Here’s the most efficient way to capture and format it.” No runtime template parsing. No boxing. No wasted string allocation. Zero overhead when disabled.
A clarification: the performance gain is primarily noticeable in high-frequency logging scenarios (thousands of calls per second). For low-frequency events like error logging or rare business events, the difference is measurable but not dramatic. The real power of LoggerMessage is its consistency across high-volume paths. Also worth noting: LoggerMessage requires partial methods, which means you can’t use it everywhere—instance methods on regular classes need to be static partials, which limits where you can apply this pattern.
I wrote extensively about this pattern in my CompositeFormat article, where I showed concretely how parsing overhead compounds at scale. The same principle applies here: parse once (at compile time), use a thousand times (at runtime). Source-generated logging is the logging equivalent of that core optimization. It delivers measurably better performance. It means measurably lower CPU usage. And the code is even cleaner and more maintainable.
Anti-Pattern 5: Unstructured Logs in Structured Systems
You’ve set up Application Insights correctly. You’re sending structured logs to the cloud. But then someone does this:
// DON'T: Free-form text—not queryable or searchable
logger.LogError($"Order 12345 failed. Payment service returned 429...");
// DO: Structured data—queryable and analyzable
logger.LogError("Payment rate limited. OrderId={OrderId}, StatusCode={StatusCode}",
orderId, statusCode);
The second version is queryable. The first version is just noise that wastes storage.
Application Insights, Datadog, Elasticsearch—all of these powerful tools only work effectively because logs are structured. When you log unstructured text, you throw away the tool’s entire value proposition. You might as well be writing to a flat file somewhere. You’ve spent significant money on enterprise observability and gained nothing from it.
The Practical Path Forward
So how do you actually fix these patterns? The answer isn’t more generic best practices. It’s not buying more tools. It’s building deliberate, intentional, carefully designed observability built specifically for your application.
Step 1: Identify Your Critical Paths
Write down the 3-5 user flows that actually matter in your system. Not every single code path. The ones where failure creates real incidents and angry customers.
For an e-commerce system: order placement → payment processing → warehouse notification. For a SaaS platform: user sign-up → authentication → data access → export. For an API service: request validation → business logic → response serialization → client response.
You’ll complete this exercise in an afternoon or two. It immediately clarifies what’s actually important in your system and what you should care about.
Step 2: Map Failure Modes
For each critical path, list concretely what can go wrong. Not everything theoretically possible. The specific failures you’ve actually dealt with in production:
- Payment timeout (how long does it take to decide? What’s the timeout value?)
- Insufficient funds (is this handled gracefully? Do you notify the user?)
- Service unavailable (do you have fallbacks? Do you retry?)
- Rate limiting (do you respect backoff headers? Do you queue?)
- Invalid input (where’s the validation boundary? What gets validated?)
- Database deadlock (how often does it happen? What query triggers it?)
This exercise takes longer than step one, but it’s where the real insight happens. You’re not speculating about what could theoretically go wrong. You’re building on what actually has gone wrong in production.
Step 3: Instrument Deliberately
Now you log only when something meaningful happens:
- A critical path step completes (success or specific failure)
- An operation enters a retry/fallback state (you’re doing something non-standard)
- A threshold is crossed (queue is full, latency exceeds SLA, rate limit triggered, circuit breaker opened)
Nothing else. Not method entry/exit. Not variable assignments. Not successful intermediate steps that didn’t fail. Only things that directly help answer: “Why did this critical path fail?”
Step 4: Make Logs Actionable
Here’s the test: when someone reads a log line at 3 AM during an incident, can they immediately understand what was happening and what went wrong? Or do they need to cross-reference five other services, query the database, check five other log systems, and piece together a story?
If it’s the latter, restructure your log. Make it self-contained. Include the context that matters. Make it so someone can understand what happened without detective work.
Step 5: Use Sampling for Scale
You can’t keep every single log entry. But you actually don’t need to. Use context-aware, intelligent sampling:
- Keep 100% of errors and warnings (these are rare and valuable)
- For information logs, consider adaptive sampling: sample heavily on errors (100%), moderately on warnings (50%), lightly on success paths (5-10%)
- Disable debug logs in production entirely (add them on-demand when troubleshooting a specific incident)
Important note: Sampling must be consistent across all services in a distributed trace (W3C Trace Context propagates the sampled flag for this reason). If one service samples at 10% and another at 50%, you’ll have incomplete and inconsistent traces. Either all services honor the same sampling decision, or you lose correlation.
With this approach, you might sample 1 out of every 10 successful order completions. But you’ll still see 100 order completions per second even with sampling. You see the patterns. You see the anomalies. You catch bugs. And you’re not paying for 90% noise.
Real Example: The Safe Approach
When you combine all these principles—deliberate instrumentation, source-generated logging, correlation IDs, specific failure modes—the result looks like this:
You log only when a critical path step completes. If it succeeds, one single log entry confirms it happened. If it fails, you log the specific failure mode (timeout, rate limit, validation error) with enough context to diagnose immediately. You use ActivitySource to track the operation through services. You keep the happy path silent—no noise about intermediate steps that didn’t fail.
Instead of sprawling code with dozens of unnecessary log statements, you have surgical, intentional instrumentation. Each log line earns its place because it answers a specific diagnostic question. You use W3C Trace Context headers (traceparent/tracestate) to correlate across services automatically. The result: when something breaks at 3 AM, you don’t sift through chaos. You have a clear narrative: here’s what the request tried to do, here’s where it failed in which service, here’s why. One single trace ID connects everything.
Conclusion: Know Why Before You Know What
The difference between teams that own production and teams that merely survive it isn’t logging volume. It’s logging intelligence and intention.
The teams with genuinely healthy observability don’t log more. They log smarter. They understand their failure modes deeply. They instrument not for completeness, but for purpose. They keep logs queryable because they know they’ll search them under pressure. They use sampling strategically instead of trying to keep everything.
Most importantly: they make every log line count. There’s no filler. No speculation. No “this might be useful someday.” Every log line answers a question.
Meanwhile, other teams are paying extra storage fees for logs nobody reads. They’re adding more logging and watching performance tank. They’re frustrated because diagnosis takes hours instead of minutes.
It doesn’t have to be this way.
Start with the hardest question: “What would I need to see in a log line to immediately understand why this customer’s order failed? Why this API call timed out? Why this background job got stuck?”
Then instrument for exactly that. Nothing more. Nothing less.
When a bug escapes to production—and it will—you won’t be digging through gigabytes of noise hoping to find something relevant. You’ll have the signal right there in front of you. You’ll see what failed, why it failed, and what the system tried to do about it.
At 3 AM, when production is burning and everyone is exhausted and frustrated, that’s the difference between “we found it in minutes and fixed it” and “we flew blind for hours and lost customers.”
Build for that moment. Your future self will thank you.
