Logging You Can Actually Search at 2am

Written By
SprintX Team
AI & Product Engineering
August 13, 2026
8 min read

Most startup logging is a wall of text nobody can query during an incident. Here is the small set of rules that turns it into something you can answer questions with.
Here is the only test that matters for a logging setup. It is 2am, a customer says checkout failed twenty minutes ago, you have their email address, and you are on a phone. Can you find out what happened in under two minutes?
Most startup logging fails that test badly. Not because there are no logs — there are thousands of lines — but because they are prose. "Error processing request." "Something went wrong." "undefined". You cannot filter prose by customer, and you cannot count it.
Fixing this is a couple of hours of work, and it changes what an outage feels like. The rules below are the ones that survive contact with a real incident.
Log events, not sentences
A log line is a data record that happens to be readable, not a message that happens to be stored. Every line should be one JSON object with stable keys, emitted as a single line.
That means instead of writing a sentence describing that a payment failed for a user, you emit an object with an event name like payment.failed, plus fields for the user, the amount, the provider's error code, and the duration. The message text, if you keep one, is for humans skimming — the fields are what you query.
Once every line is structured, questions that used to require reading become filters. Which customers hit payment.failed today. What is the p95 duration on checkout.completed. Did the error rate change after the 3pm deploy. None of that is possible against free text, and all of it is trivial against fields.
Use a real logging library rather than console output, so structure is enforced rather than remembered. In Node that is Pino or Winston; in Python, structlog. The library matters less than the constraint it imposes.
The five fields that make logs searchable
Almost all of the value comes from a small set of keys present on every single line. Configure them once at the logger level so nobody has to remember them.
- Timestamp in UTC, ISO format. Local timezones in logs cost you an hour of confusion per incident, permanently.
- A request ID. Generate one at the edge of every request, attach it to the logger for that request's lifetime, and return it in the response and in your error UI. When a customer sends you a screenshot with an ID on it, you have their entire request in one filter. This single field is worth more than every other logging improvement combined.
- The user and tenant. Who was acting and in which account. Use IDs, not emails — see the section on what not to log.
- The event name. A stable, lowercase, dotted string. Stability is the point: if the name changes between releases, your saved searches and dashboards silently break.
- Duration and outcome on anything that finishes. How long it took and whether it succeeded. Half of performance work is just having this.
Then add context specific to the event. A failed external call should log the provider, the status code, the attempt number, and whether a retry is scheduled. Over-logging context on errors is nearly always the right trade — you are writing a message to yourself at 2am, and that person cannot go back and add fields.
Use levels the way an on-call person reads them
Levels are not a measure of how interesting something is. They are a routing decision about who finds out and how fast.
| Level | Means | What should happen |
|---|---|---|
| ERROR | A user-visible operation failed and did not recover | Goes to error tracking, may page if the rate spikes |
| WARN | Something degraded but recovered — a retry, a fallback | Reviewed in aggregate, never pages |
| INFO | A meaningful business event happened | Kept, searched during incidents |
| DEBUG | Detail for reproducing a specific problem | Off in production, or sampled |
The failure mode is grade inflation. When an expected condition — a user typing a wrong password, a webhook arriving twice — is logged as ERROR, the error stream becomes noise, and the one real error of the week goes unread. Expected failures are INFO or WARN. ERROR means somebody's work was lost.
Log the whole exception object, not the string of its message. Stack traces are where the answer usually is, and a stringified error throws away the cause chain. Where those errors go and how they get grouped is a separate concern from logging — error handling in AI-generated apps covers the code side, and adding observability covers the tooling around it.
What must never end up in a log
Logs get replicated to third-party services, retained for months, exported to spreadsheets, and pasted into support tickets. Treat them as semi-public.
Never log passwords, session tokens, API keys, full card numbers, or complete request bodies on auth endpoints. That last one is how most accidental credential leaks happen — a well-meaning "log the request for debugging" on the login route, shipped once and forgotten. Given that GitGuardian counted 28.65 million new hardcoded secrets in public GitHub commits in 2025, with AI-assisted commits leaking at roughly 3.2% versus a 1.5% baseline, assume your codebase already has a secret-handling problem and do not add a logged one. Secret scanning an AI codebase is the cleanup pass.
Be deliberate about personal data too. Log user IDs rather than emails and names, and you keep your logs useful while keeping them out of scope for most of the awkward parts of privacy compliance — an ID is meaningless without your database, an email is not. Add a redaction list to your logger for keys like password, token, authorization, and secret so that even an accidental object dump is scrubbed on the way out.
One practical addition: run a check in CI that fails the build if a log call passes an entire request or user object. It is a five-line rule that prevents the most common version of this mistake, especially in code written quickly with an assistant.
Keep the volume, and the bill, sane
Log platforms charge by ingestion, and a chatty application at scale can produce a bill that outgrows the infrastructure it is monitoring.
Sample the boring things. Successful, fast, high-volume requests do not all need to be stored — one in ten tells you the same story. Sample errors at 100%, always, and use head-based sampling keyed by request ID so an entire request is either kept or dropped together, never half-recorded.
Tier your retention. Seven to fourteen days of full-fidelity searchable logs covers virtually every incident investigation. Anything you need for months — audit events, security-relevant actions — belongs in a separate, cheaper, append-only audit store, which is a different job with different retention rules.
And do not log inside tight loops. A per-iteration log line in a batch job is how a team discovers its logging bill on a Monday morning.
Logs are one of three things, and the smallest
Logs answer "what happened in this specific request." They are the wrong tool for "is the system healthy right now" — that is metrics — and for "where did the time go across six services" — that is tracing.
Small teams that try to do all three with logs end up paying to store millions of lines in order to compute a number a metric would have given them for free. Emit counters and timers for the things you watch continuously, and let logs be the detail you drop into once a metric or an alert tells you where to look. Monitoring and alerts for a small SaaS covers what to watch, and incident response when you are the whole team covers what to do once something fires.
Frequently asked questions
What should I use to store logs at an early stage? Whatever your hosting platform already gives you, until searching it becomes painful. Vercel, Fly, Railway, and Render all retain and search logs adequately for a small product. Move to a dedicated platform when you find yourself unable to answer questions during incidents — not because a comparison article said you should.
How do I log usefully in serverless functions? Write structured JSON to standard output; every serverless platform collects it. The part that needs care is the request ID, since a function does not share memory across invocations — generate or accept one at the entry point and pass it explicitly through everything the invocation calls.
Is it worth retrofitting logging into an existing app? Yes, and you do not have to do it everywhere. Instrument the three or four paths that generate support tickets — signup, checkout, the main create action, anything touching an external provider — and you will cover most of what you actually get paged about. The rest can improve as you touch it.
If your logs are a wall of text and every customer report turns into an archaeology project, the fix is a day of work rather than a platform migration. SprintX instruments applications with structured, searchable, redaction-safe logging and the dashboards to read it. Tell us where your blind spots are and we will start with the paths that page you.


