Draft, not yet published
Logging for AI-generated backends: what to capture before you need it
7 July 2026· 5 min read · by Stackbastion
Something’s broken in production, a user is reporting an error you can’t reproduce, and you open your logs to find… console.log("here") and a stack trace with no context. You have no idea which user, which request, or what data triggered it. The time to fix your logging was before this happened. Let’s fix it now so the next incident is a five-minute job.
Why this happens
AI coding tools scatter console.log around while building and never replace it with real logging. That’s fine for the tool’s job, which is getting the app to run. It’s useless for the job you have now, which is figuring out why a specific request failed for a specific user an hour ago.
Plain console.log fails in production for three reasons. The output is unstructured text, so you can’t search or filter it. It carries no context, so you can’t tell which request a line belongs to. And it usually vanishes when the process restarts, so the evidence is gone before you look. Good logging fixes all three, and it’s a small change.
How to fix it
Step 1: log structured JSON, not free text
Switch from printing sentences to printing objects. A structured log line is a JSON object you can search, filter, and sort. Instead of this:
console.log("User " + userId + " failed to pay");
Do this:
logger.error({
event: 'payment_failed',
userId,
amount,
reason: err.message,
});
Now you can answer “show me every payment_failed for this user today” instead of grepping through prose.
Step 2: pick a real logger
Use a proper logging library. In Node, pino is fast and outputs JSON by default:
pnpm add pino
// lib/logger.js
import pino from 'pino';
export const logger = pino({
level: process.env.LOG_LEVEL ?? 'info',
// in production, JSON; in dev, pretty-printed for reading
transport:
process.env.NODE_ENV === 'production'
? undefined
: { target: 'pino-pretty' },
});
Replace your console.log calls with logger.info, logger.warn, and logger.error.
Step 3: the field checklist
Every log line for a request should carry enough context to reconstruct what happened. Capture these:
- timestamp (your logger adds this automatically)
- level (info, warn, error)
- request ID (a unique id per request, so you can follow one request across many log lines)
- user ID (who it happened to, if logged in)
- route and method (
POST /api/checkout) - status code (200, 500)
- duration in ms (how long it took)
- event name (a short label like
payment_failed) - error message and type on failures
Never log passwords, full card numbers, API keys, or tokens. Redact them. A log file is a breach waiting to happen if it’s full of secrets.
Step 4: tie a request ID through everything
The single most useful trick is a request ID: one random id assigned when a request arrives, attached to every log line it produces. When a user reports an error, you find their one bad request and see the whole story in order.
// middleware that stamps every request
import { randomUUID } from 'crypto';
export function requestLogger(req, res, next) {
const requestId = req.headers['x-request-id'] ?? randomUUID();
req.log = logger.child({ requestId, route: req.url, method: req.method });
const start = Date.now();
res.on('finish', () => {
req.log.info({
event: 'request_completed',
status: res.statusCode,
durationMs: Date.now() - start,
});
});
next();
}
Step 5: what a good log line looks like
Here’s the format you’re aiming for, one JSON object per line:
{"level":"error","time":"2026-07-07T14:32:10.221Z","requestId":"a3f9-2b1c","userId":"u_8842","route":"/api/checkout","method":"POST","status":500,"durationMs":842,"event":"payment_failed","errType":"StripeCardError","msg":"card declined"}
That one line tells you who, what, when, which request, how long it took, and why it failed. That’s the difference between a five-minute fix and an afternoon of guessing.
Step 6: keep the logs somewhere they survive
Logs that die with the process are no help. At minimum, write them to a file that persists, and rotate it so it doesn’t fill the disk. On Docker, use the json-file driver with limits, or ship logs to a service:
# in docker-compose.yml, per service
logging:
driver: json-file
options:
max-size: "10m"
max-file: "5"
For error alerting on top of this, pair it with an error tracker, covered in monitoring error rates.
Or, we do it for you
We wire structured logging and log retention into customer setups so that when something breaks, the evidence is already there and searchable. If your logs are still a pile of console.log, book a free audit and we’ll show you the gaps.
FAQ
Won’t logging everything slow my app down?
No, if you use a fast logger like pino and log at sensible levels. Pino is asynchronous and adds negligible overhead. What slows apps is logging huge objects on a hot path, so log identifiers and small fields, not entire request bodies.
How long should I keep logs?
For a small app, 7 to 30 days covers most debugging and stays cheap. Keep error-level logs longer than info-level if you can. If you handle personal data, remember logs are personal data too: match your retention to your privacy policy and redact anything sensitive.
What log level should each thing be?
Use info for normal events (request completed, user signed up), warn for recoverable oddities (retry succeeded, deprecated path hit), and error for actual failures. In production, set LOG_LEVEL=info so you get the useful lines without drowning in debug noise.
Do I need a fancy log platform?
Not to start. A structured JSON logger writing to rotated files gets you most of the value. Add a hosted log or error platform once you’re actually reaching for the logs often enough that grepping files gets annoying. Structure first, tooling second.