It’s 2:14 AM on a Tuesday. Your phone is screaming on your nightstand. PagerDuty reports that the checkout API is spiking with 500 errors, and enterprise clients are failing to process payments.
Adrenaline pumping, you stumble to your desk, open your log aggregator, and type in the correlation ID. You need to know exactly why the system is falling apart, right now.
Instead of a clear diagnosis, you are greeted by a wall of text that looks like a chaotic group chat between caffeinated junior developers:
- Log.Information(“Entering checkout controller…”)
- Log.Information(“User loaded okay”)
- Log.Information(“Got here !!!”)
- Log.Warning(“Something looked weird but ignoring it”)
- Log.Error(“Object reference not set to an instance of an object.”).
That last line is your error. But which object was null? Was it the user? The shopping cart? The payment gateway response? The logs don’t say. You have thousands of lines of useless noise, but zero context on what actually broke.
You spend the next two hours digging through source code, trying to guess which variable caused the exception, while the company loses thousands of pounds every minute.
This is the cost of bad logging. In a lot of teams, logs are treated like a casual Discord channel, a place where developers dump raw thoughts and text strings just to see if their local environment is running. But in production, that text stream is a liability. It’s time to grow up and talk about real observability.
1. The Text-Stream Anti-Pattern: Stop Using String.Format
The root of all bad logging is the plain text mentality. Many developers still write logs like they’re writing to a console application in a university project:
C#
Log.Information($”User {user.Id} failed to pay {amount} for order {orderId}”);
This looks innocent, but it’s a disaster at scale. When this is shipped to production, it becomes a single flat string: “User 4221 failed to pay 99.99 for order 90812”.
Tomorrow, your manager asks you to find out how many users failed to pay for a transaction over £50. To find that out from flat text logs, you have to write a complex regular expression (regex) to parse that string, extract the amount, and filter it. If a developer tweaks the text format next week (e.g., changing “failed to pay” to “payment failed”), your regex breaks, your dashboards go blank, and you’re blind again.
Stop using string interpolation in your logs. You are destroying your data before it even leaves the application.
2. Structured Logging: Thinking in Data, Not Sentences
Observability for grown-ups requires Structured Logging. You need to stop treating logs as diary entries and start treating them as structured database events (usually JSON) that just happen to have a message attached.
In modern .NET, using libraries like Serilog or standard OpenTelemetry, you write logs using message templates:
C#
Log.Information(“User {UserId} failed to pay {Amount} for order {OrderId}”, user.Id, amount, orderId);
Notice the difference? We aren’t building a string. We are passing a template with placeholder names, followed by the arguments. When this hits your logging tool (Elasticsearch, Seq, Azure Monitor), it doesn’t just save a text line. It saves a fully queryable object:
JSON
{
“Timestamp”: “2026-05-27T14:15:00Z”,
“MessageTemplate”: “User {UserId} failed to pay {Amount} for order {OrderId}”,
“Properties”: {
“UserId”: 4221,
“Amount”: 99.99,
“OrderId”: 90812
}
}
Now, if you want to find all failed transactions over £50, you don’t write a regex. You write a clean database query: Properties.Amount > 50. It takes two seconds, it’s lightning-fast, and it doesn’t break if someone changes the wording of the sentence later.
3. The Log Level Discipline
Another common symptom of “Discord logging” is the complete misuse of log levels. When everything is an Information log, nothing is important. When everything is an Error log, you quickly develop alert fatigue and start ignoring your dashboards.
If you want your logs to save you at 2:00 AM, your team needs to agree on a strict log level matrix:
| Level | When to Use It | Example |
| Debug | Internal system noise. Only turned on when hunting a local bug. | “Connection pool opened socket #3.” |
| Information | Major system milestones. High-level business value events. | “Order #90812 processed successfully.” |
| Warning | Something unexpected happened, but the system recovered. | “Payment gateway timed out; retrying on secondary provider.” |
| Error | A feature failed for a specific user, but the overall app is still running. | “Cannot process payment for User #4221 due to insufficient funds.” |
| Critical | The application is dying. Crash loop, database down, out of memory. | “Unable to connect to the primary database cluster. System halting.” |
If you find yourself writing Log.Error for a standard validation failure (like a user typing a wrong password), stop. A user typo is a normal operational path, not an application crash. Keep your errors sacred, so that when an error actually fires, the team knows it requires immediate action.
4. The Context Matrix: What to Attach to Every Log
A log message in isolation is rarely enough to solve a production puzzle. To understand why a failure occurred, you need the surrounding context.
Instead of manually typing that context into every single log statement, you should use Logging Contexts (or Scopes) to automatically attach vital metadata to every log generated during a request lifecycle.
Every modern production log should automatically include:
- Correlation ID / Trace ID: A unique string generated the second a request hits your gateway. If that request touches five different microservices, they should all use the same ID. This allows you to search one ID and see the entire lifecycle of that specific user action across your network.
- Environment Context: Machine name, region, and software version number. (Did the bug only happen on Server B after the latest deployment?)
- Tenant / Customer ID: Essential for multi-tenant applications. Is this bug happening to everyone, or just one high-value client with a weird configuration?
- No-BS Warning on PII: Never, under any circumstances, log raw passwords, credit card numbers, or sensitive personal data (like medical records) to your logs. If a security auditor finds unencrypted PII in your Elasticsearch cluster, your company faces massive fines. Log IDs and metadata, not secrets.
5. Moving Past Logs to True Observability
Logs are just one piece of the puzzle. In a modern distributed architecture, especially if you’re using tools like .NET Aspire, logs shouldn’t live in a silo. They need to be paired with Metrics and Traces.
- Metrics indicate there is a problem (e.g., CPU is at 98%).
- Traces show you where the problem is (e.g., the request spent 1.4 seconds waiting on a specific SQL query).
- Logs tell you why it failed (e.g., a database timeout exception occurred).
When you tie these three pillars together using OpenTelemetry standards, you stop guessing and start diagnosing with surgical precision.
But mastering this ecosystem requires moving past simple syntax tutorials. Most online courses show you how to write Console.WriteLine or set up a basic logger, but they skip the architecture required to manage millions of events without tanking your system’s performance.
This is where intentional, production-grade education matters. At Dometrain, courses skip the basic syntax sugar and focus heavily on the forensics of software engineering. Understanding how to handle structured data, memory allocation in hot paths, and distributed architecture is what separates a mid-level coder from a senior architect who can confidently own production uptime.
The 2:00 AM Rules for Clean Logging
If you want your logging strategy to turn from a text graveyard into a powerful tool, enforce these rules across your codebase today:
- Ban string interpolation ($””) in logs: Enforce structured logging with message templates across the entire team via linters.
- Automate the context: Use ILogger.BeginScope or middleware to automatically attach Correlation IDs and User context to every request.
- Clean up the “Got Here” noise: If a log doesn’t provide business value or troubleshooting context, delete it. Do not use production logs as a personal print-debugging canvas.
- Log the Exception properly: Never just log ex.Message. Pass the whole exception object (Log.Error(ex, “Message”)) so your logging platform captures the entire stack trace.
Your logs are the eyes and ears of your application when the lights go out. Stop treating them like a casual chat window, clean up the noise, and start building a structured, data-driven observability strategy that lets you fix bugs in minutes instead of hours.

