Structured logging in .NET is one of those unglamorous jobs that decides whether a 2am incident takes ten minutes or three hours. I have been on both sides of that. The difference was never the cleverness of the code, it was whether the logs could actually answer a question.
If you run a small SaaS with a handful of developers, you do not have a platform team to build you an observability stack. You need something simple that works and does not cost a fortune. Here is how I set it up.
What structured logging actually means
Plain text logging gives you a sentence like "Failed to process invoice 4821 for customer 77". It reads nicely, but you cannot filter it, count it or group it without writing regular expressions at midnight.
Structured logging keeps the message template and the values separate. The invoice id and customer id become named properties you can search on. Suddenly "show me every failure for customer 77 this week" is a one line query instead of a grep adventure.
The mistake everyone makes
Do not build the message with string interpolation. If you write the values straight into the string, you throw away the structure and just end up with plain text again.
Use a message template with named placeholders and pass the values as arguments. The framework captures each one as a property. It is a tiny habit change, and it is the whole game.
Pick a logging setup and stop fiddling
Microsoft.Extensions.Logging is built into .NET and is perfectly fine for most teams. If you want more control over sinks and enrichment, Serilog is still my favourite. I have used it on production systems for years and it has never let me down.
Whichever you pick, send the output as JSON. Console and file logs are fine locally, but in production you want a format a machine can read. Azure Application Insights, Seq or Elastic will all happily ingest it.
Start with Application Insights if you are on Azure
If you already host on Azure, Application Insights is the obvious default. It collects requests, dependencies and exceptions with almost no setup, and your structured properties show up as custom dimensions you can query.
The costs creep up if you log everything at debug level, so watch the volume. I wrote about keeping that bill under control in my post on Azure cost optimisation.
Log levels that mean something
Most codebases I inherit have every message logged as information. That makes the level useless. Agree a simple rule as a team and stick to it.
Here is the version I use:
- Error means something failed and a human may need to act.
- Warning means something odd happened but the system coped.
- Information means a meaningful business event, like an order placed or a subscription cancelled.
- Debug means detail for developers, switched off in production unless you are chasing a bug.
If your error log is full of noise, people stop reading it. An error channel that cries wolf is worse than having none at all.
Correlation ids make it all hang together
A single user action in a modern system touches an API, a queue, a background worker and maybe a third party service. Without a shared identifier, those log lines are strangers.
Generate a correlation id at the edge, usually in middleware, and attach it to every log entry for that request. Pass it along on outgoing calls and put it in your queue messages. When a customer reports a problem, you find one id and see the entire journey.
Add context once, not everywhere
Use a logging scope or enricher to attach the tenant id, user id and correlation id automatically. Developers should not have to remember to include them in every line. If it relies on memory, it will be missing exactly when you need it.
What you must never log
Logs have a habit of living for years and being readable by far more people than the database. Treat them as sensitive. Never write passwords, tokens, full card numbers or API keys into them.
Personal data deserves the same care, especially with GDPR in the picture. Log ids rather than names and email addresses wherever you can. Your secrets management is only as good as your logging discipline, because a leaked connection string in an exception message undoes the lot.
Turn logs into alerts you will actually act on
Logs are only useful if somebody looks at them. Pick a few queries that matter, such as error rate per minute, failed payments and background job failures, and alert on those. Do not alert on everything.
This ties directly into proper monitoring and alerting. Good logs feed good alerts, and good alerts mean you hear about problems before your customers email you.
A practical starting checklist
You do not need to fix everything this week. Pick the high value changes first and build from there.
- Switch to message templates and stop interpolating values into strings.
- Output JSON in production and send it somewhere searchable.
- Add a correlation id and tenant id to every request automatically.
- Agree log levels as a team and clean up the noisy errors.
- Review what you are logging for secrets and personal data.
It is a day or two of work for most codebases, and it pays back the first time something breaks in production. Add it to your deployment pipeline too, so you can see exactly which release started the problem.
If you would like a second pair of eyes on how your application is instrumented, that is something I do regularly through my software development and consulting services. Bad logging is cheap to fix, and expensive to live with.


