Set up error tracking and logs
A console.log survives exactly as long as the terminal window it printed to. Once an app runs on a server nobody is watching, that stops being a debugging tool and becomes nothing at all. This is the setup that actually tells you when something breaks: structured logs, an error tracker for anything unhandled, context attached without leaking a secret or a person's data, alerts that fire before a user complains, searchable logs on a sane retention window, and a short triage flow for the five minutes after an alert goes off.
What you’ll learn
- Replace scattered print statements with structured, machine-readable logs that carry request context
- Wire up an error tracker that groups repeat errors, keeps stack traces, and tags each one with its release
- Attach request id, route, and user context to every error, with no secrets or personal data logged
- Set alerts on error-rate spikes and brand-new error types, so you hear about it before users do
- Keep logs centralized and searchable with a retention window that fits how long you need to investigate
- Run a repeatable triage flow the moment an alert fires, instead of improvising mid-incident
Print statements are how most projects start logging, and how most stay blind to what actually breaks once real users show up. A scattered console.log works fine, watched live, on your own machine. It stops working the moment the app runs on a server nobody is watching, across more than one instance, at 3 a.m. Six pieces turn "no idea something broke" into "found it in ninety seconds": structured logs, an error tracker for anything unhandled, the context worth attaching to each error, alerts that page you before a user does, a searchable setup with sane retention, and a short triage flow for the moment an alert fires.
Log levels, and when to reach for each one
| Level | When to use it |
|---|---|
| DEBUG | Step-by-step detail useful only while actively debugging one issue; off by default in production |
| INFO | A normal event worth a permanent record: a job started, a user signed up, a scheduled task ran |
| WARN | Something unexpected happened but the app recovered on its own: a retry succeeded, a fallback kicked in |
| ERROR | A specific operation failed and a user or a process was affected; worth investigating |
| FATAL / CRITICAL | The process cannot continue and is about to crash or exit |
1. Add structured logging instead of scattered prints
A print statement is a sentence for a human watching a terminal in that moment, with no fixed shape, so nothing downstream can search or alert on it. Structured logging fixes that: each line becomes data, usually JSON, with a fixed set of fields. The Twelve-Factor App treats logs as "the stream of aggregated, time-ordered events collected from the output streams of all running processes and backing services,"1 and says a well-built app "never concerns itself with routing or storage of its output stream,"1 it writes to stdout and lets the environment route it. OpenTelemetry adds what actually makes a log structured: not the JSON encoding, but "a defined, consistent schema... that downstream systems can reliably parse and interpret."2
{
"timestamp": "2026-08-11T14:32:07.441Z",
"level": "error",
"message": "payment charge failed",
"service": "checkout-api",
"request_id": "req_9f2a1c",
"route": "POST /api/checkout",
"user_id": "usr_4471",
"duration_ms": 812,
"error": {
"type": "CardDeclinedError",
"message": "card_declined"
}
}- A timestamp in a fixed format, ISO 8601, UTC, not the server's local time zone.
- A level (DEBUG, INFO, WARN, ERROR) set on purpose, not left at the framework default.
- The same wording every time the same event happens, so identical failures group instead of scattering under fifty near-duplicate strings.
- The service or process name, so a shared log stream still filters back to one component.
- A request id and route (step 3), plus only the fields that actually explain what happened.
2. Capture unhandled errors with an error tracker
Structured logs catch what you thought to log. They miss the exception nobody wrote a log line for, the one that crashes a background job overnight while nobody is watching. An error tracker closes that gap, a category, not one product: an SDK for your language that hooks into the runtime and reports every unhandled exception automatically, wrapped in a try/catch or not.
A good one does three things. It groups repeat occurrences of the same error into one issue by fingerprinting the type, message, and stack trace shape, a hundred crashes from one bug reads as one issue, not a hundred alerts. It keeps the full stack trace readable through a minified or compiled build, source maps for JavaScript, debug symbols elsewhere, the exact line that failed. And it tags every event with the release that shipped it, so a spike points straight at the deploy that caused it.
- Automatic grouping (fingerprinting) of repeat errors into one issue, with an accurate count.
- Full stack traces, readable through minification or compilation.
- Release or version tagging on every captured event.
- Custom context attached per error (step 3), not just default runtime data.
- A real SDK for your language, not a bare HTTP endpoint you wire up by hand.
3. Attach useful context, never secrets or personal data
An error with no context is a stack trace and a shrug. Attach a request id generated once at the start of a request and threaded through every downstream call, so one failure can be traced end to end. The route that failed. An opaque user identifier if the app is authenticated, an id, never a name or an email. And how long the request had been running when it failed.
Nothing that identifies a person beyond a system-generated id, and nothing that grants access on its own, belongs in a log line or an error's context. The OWASP Logging Cheat Sheet is direct: certain data should not be recorded "unless it is legally sanctioned,"3 naming passwords, session identifiers, access tokens, encryption keys, and full payment card data among what should never be written into logs.3 A field that is useful but still sensitive should be "removed, masked, sanitized, hashed, or encrypted"3 instead: the last four digits of a card, not the card; a hash of an email, not the address.
- Safe to attach: request id, route or endpoint, HTTP status code, an opaque user id, duration, error type and message, release version.
- Never log: passwords, API keys and access tokens, session identifiers, full card numbers, government ID numbers, health data, or a raw request body from a login or payment endpoint.
4. Set alerts on error spikes and brand-new error types
A dashboard nobody watches catches nothing. An alert is the difference between finding out from your error tracker and finding out from a user. The Google SRE book sets the bar directly: an alert should catch "an otherwise undetected condition that is urgent, actionable, and actively or imminently user-visible,"4 and "if a page merely merits a robotic response, it shouldn't be a page."4 That rules out paging on every single error; alert on what actually changed.
Two triggers cover most of what a small team needs: a spike in the error rate for something already known, and a brand-new error type never seen before, often the earliest sign a deploy went wrong. Favor symptoms over causes, "the server is returning 500s to clients" over "the database is refusing connections,"4 a symptom says users are affected right now, a cause is just one possible reason. This is one of the SRE book's four golden signals, latency, traffic, errors, and saturation,4 worth alerting on together once error tracking alone is solid.
5. Keep logs searchable, with a retention window that fits
Logs you cannot search are logs you will not use during an incident. Centralize application logs, error-tracker events, and infrastructure logs in one place, queryable by request id, route, user id, or time range, instead of SSHing into a box to tail a file. Set retention deliberately: long enough to investigate something reported days later, commonly two to four weeks for a small team's application logs, no longer without a reason, since old logs past that are mostly cost and risk. Vary it by type: debug logs can expire in days, an error tracker's far lower-volume issues can live for months.
6. Run a simple triage flow when an alert fires
The alert firing is the easy part. What happens in the next five minutes decides whether it stays a minor blip or turns into an outage nobody caught in time. A short sequence, decided in advance, beats improvising it fresh every time.
- Read the error first: message, stack trace, how often it has fired in the last hour.
- Check what changed: the release tag on the error, any deploy around when the spike started.
- Check the blast radius: one user or many, one route or many, using the fields from step 3.
- Decide fast: roll back a clear-cause deploy first and investigate after; do not debug live in production.
- No deploy explains it: look for a pattern instead, one bad input, one dependency, one region.
- Once fixed, close the loop: resolve the issue, and add an alert if one should have caught it sooner.
None of this needs expensive tooling: a structured JSON logger, one error-tracking SDK, and an alert on spikes and new error types covers most of what a small team needs. The rest, retention, symptom-based alerts, a written triage flow, is process, and it costs an afternoon to set up once. Skipping it does not make errors happen less often, it just moves the moment you find out from a dashboard to an angry review.
- Replace print statements with structured JSON logs carrying a timestamp, level, message, and request context.
- Install an error-tracking SDK that groups repeat errors, keeps full stack traces, and tags each one with its release.
- Attach request id, route, and an opaque user id to every error, and keep passwords, tokens, and personal data out entirely.
- Set alerts on error-rate spikes and brand-new error types, not on every single error.
- Centralize logs in one searchable place with a retention window sized to how long you actually need to investigate.
- Write down a short triage flow: read the error, check what changed, check the blast radius, decide fast, close the loop.
Sources
Verified against primary sources: August 2026.
- Logs: Treat logs as event streams. The Twelve-Factor App, Factor XI. https://12factor.net/logs
- Logs. OpenTelemetry Documentation. https://opentelemetry.io/docs/concepts/signals/logs/
- Logging Cheat Sheet. OWASP Cheat Sheet Series. https://cheatsheetseries.owasp.org/cheatsheets/Logging_Cheat_Sheet.html
- Monitoring Distributed Systems. Google SRE Book. https://sre.google/sre-book/monitoring-distributed-systems/
- Traces. OpenTelemetry Documentation. https://opentelemetry.io/docs/concepts/signals/traces/