Stop Digging Through console.log: Structured Logging for Vibe Projects
Logs are the product's flight recorder — but only if they are written by the rules. Structure, levels, correlation IDs, fields, redaction, sampling, and retention: seven things a one-person team can ship today.

The verdict first: logs are not for the machine — they are for you at 3 a.m. when something breaks

Almost every vibe project reaches the same night: a user says "my payment just failed", you open the logs, and all you see is a wall of console.log('here'), console.log(data), console.log('ok'). No timestamps, no idea which user it was, no idea which step failed, and no idea what happened before the failure. So you guess, you try to reproduce it, you ask the user "what exactly did you click". That night did not have to happen.
My verdict up front: logs are not "printing things to make debugging easier" — they are the product's flight recorder. After a plane crash, investigators do not rely on the pilot's memory; they rely on the black box recording every measurement in time order. Your product is the same: when a user hits a problem, the only reliable evidence you have is what the system wrote down at the time. The more structured the record, the faster the reconstruction; the more casual the record, the more troubleshooting resembles fortune-telling.
This guide is about turning logs from "casual printing" into a flight recorder. No enterprise log platform costing six figures — just what a one-person team can ship today: structure, levels, correlation IDs, fields, redaction, sampling, and retention. Get those seven things done, and logs will actually help when something breaks.
The product "Shiguang Ledger" and the people and numbers in the examples below are fictional, used only to illustrate the writing style; the method itself is general.
Step 1: Write logs as structure, not as sentences
Compare two log lines and feel the difference. The first is how most vibe projects start:
2026-11-05 10:23:11 user 123 payment failed, amount 99, reason insufficient balance
The second is the same event, structured:
{"ts":"2026-11-05T10:23:11+08:00","level":"warn","event":"payment.failed","user_id":"u_123","amount_cents":9900,"reason":"insufficient_balance","request_id":"req_9f31"}
The first is a sentence written for a human; the second is data written for a machine. The difference: the second can be searched, counted, and alerted on. Asking "how many insufficient-balance payment failures happened in the last hour" means counting lines by eye in the first case and one query in the second. Setting an alert threshold on "payment failures" is impossible with the first and trivial with the second.
Structured logging has three rules. First, one log line is one JSON object, with fixed field names and machine-readable values: amounts in cents, not dollars; timestamps in ISO 8601 with a timezone; states as enum values, not prose. Second, every event has a fixed name, such as payment.failed, auth.login, ai.generate, in a dotted namespace — nobody invents a new name on the spot. Third, fields record facts, not feelings: write "reason":"insufficient_balance", not "the user is broke again".
Shipping this is simple: stop calling console.log directly and wrap it in a minimal logger. All logs go through the wrapper from then on, and the format is guaranteed by the wrapper rather than by everyone's discipline. A minimal wrapper only needs to do three things: add a timestamp and a level automatically, attach the current request's correlation ID automatically, and serialize the object into one line of JSON. A dozen lines of code, and every investigation after that gets faster.
Step 2: Levels must mean something — not everything is info
The second most common problem is level chaos: everything is info, so finding a failure means digging through a pile of info; or the opposite, the screen is full of errors and the real error drowns. A level is not a decoration for severity — it is a division of labor over "does a human need to act".
My split is four levels, each with its own job. Debug is for development only, recording "information needed to understand what the code is doing", such as a function's inputs and outputs; it stays off in production and gets switched on selectively when something breaks. Info records "things that normally happen in the business and may need checking later", such as a user registering, an order being created, a payment succeeding. Warn records "things did not go as expected, but the system handled it", such as a retry that succeeded on the second attempt, a fallback that kicked in, or an external API that went slow. Error is reserved for "things a human needs to deal with", such as a payment that was charged but failed to write to the database, an AI call timing out three times in a row, or a background job crashing.
The test for this split is simple: when an error-level log appears, someone should be notified. If errors appear hundreds of times an hour and nobody acts, the levels are wrong — either downgrade the ones that should not be errors to warn, or fix the real problem. Conversely, if there has not been a single error in a week, do not celebrate too soon: errors may be swallowed by try-catch blocks without ever being logged. Check the warn count regularly — a system with zero warns for a long time usually does not mean there are no problems; it means nothing is being recorded.
Step 3: Give every request a correlation ID so logs can be threaded together
One user action usually crosses several steps in a system: gateway, authentication, business logic, database, external API, AI model. Without a correlation ID, the logs from those steps are scattered beads; with one, they form a thread. The implementation is not complicated: generate a unique ID when the request arrives, such as req_9f31, attach it to every log the request produces, and include it in the error message returned to the user.
Its value shows up most clearly during investigation. A user says "my payment just failed, error code req_9f31", and one search shows the request's full timeline within ten minutes: how long each step took, which step retried, what each step returned. Without that ID, you are guessing by time plus user ID — five minutes on a lucky day, an afternoon on a bad one.
The ID must also travel downstream: put it in the header when calling an external API, in the metadata when calling an AI model, and in the payload when enqueueing a background job. That way, one action can be traced with a single ID from frontend to backend, from synchronous to asynchronous. Background jobs are the easiest to overlook: many jobs fail and leave a lonely error in the job system, with nobody knowing which user or which action it belonged to. Putting the correlation ID in the job payload makes that problem disappear.
Step 4: Fields before sentences — record what can be queried, not what reads nicely
The easiest mistake in structured logging is stuffing a sentence into JSON as-is: {"msg":"user 123 failed to pay 99 yuan, insufficient balance"}. The format is JSON but the content is still a sentence, which changes nothing. The right approach is to split the sentence into fields: who, did what, what happened, and the key numbers. A sentence can be reassembled from fields when displayed, but what gets stored must be fields.
Which fields are worth recording? My checklist: event name, time, level, correlation ID, user ID, key business IDs (order number, subscription number), outcome (success, failure, retry), duration in milliseconds, and the name and status code of each external dependency. AI calls need two extra fields: model name and token usage — both tie directly to cost, and when something breaks they immediately show whether "the model changed" or "usage exploded".
Three kinds of things, on the other hand, must not go into logs. First, passwords, tokens, secrets, and full credit-card numbers — never, under any circumstances. Second, a user's full private content, such as complete chat transcripts, private messages, or national ID numbers. Third, oversized objects such as entire request bodies or the base64 of a whole image — logs exist to locate problems, not to back up data. For large objects, record a summary only: length, hash, and key fields, not the full text.
Step 5: Redact before writing the log, not when reading it later
Redaction is the step most often postponed to "later" in structured logging — and the most expensive one to get wrong. The correct approach is centralized redaction inside the logger wrapper: keep a list of sensitive fields such as password, token, secret, api_key, card_number, id_card, and phone, and before writing a log, pass through the list and replace every hit with [REDACTED]. Redaction happens before data hits the disk, not by cleaning up logs afterwards — sensitive data that has already been stored is ten times harder to delete than to never write.
Two kinds of redaction are especially easy to miss. The first is sensitive fields inside nested objects: a request body containing user info, which contains a phone number — checking only top-level fields will not catch it. The redaction function must recurse, or flatten sensitive paths before serializing. The second is error objects: many errors wrap the whole request or response body into their message or stack, and one log line leaks everything. When recording an error, keep only its type, message, and key context — never serialize the whole error object as-is.
One more discipline: treat access to production logs the same as access to the database. Not everyone should be able to read production logs, and anyone who can should know what may be screenshotted and what must never leave the team. However good the redaction is, it cannot stop a screenshot that happens to include a user's phone number. Technology and discipline have to arrive together.
Step 6: Sampling and retention — control cost, not visibility
Once log volume grows, cost becomes a problem immediately. A product with a few thousand daily users can easily produce millions of log lines a day. The way to control cost is not "log less" but "retain by tier": keep all errors and warns, keep info by business importance, and only switch debug on temporarily when needed. Sampling must also be conditional: info logs for normal requests can be sampled, say keeping 10% — but logs for failed requests must be kept in full, because the lines you need most when something breaks are exactly the failing ones.
Retention periods should be tiered too: errors at least 90 days, warns 30 days, ordinary info 7 to 14 days. Logs past retention should be deleted automatically rather than piling up forever. Before deleting, aggregate first: group each day's errors by event name and reason, record counts and trends, and the trend survives after the raw logs are gone.
One trap to avoid: do not sample so aggressively to save money that the logs you need are gone when something breaks. The test for a sampling rate is to take the last three real incidents and check whether the logs at the time were enough to find the root cause. If even one of the three failed because "that log line was sampled away", the sampling is too aggressive — turn sampling off for failure paths first, then talk about saving money.
Three investigation stories: how you write logs decides how fast problems get found
Story one: a user says the payment was charged but the order still shows unpaid. Without structured logs, you ask the user when they paid and how much, reconcile with the payment provider, then check the order in the database — at least half an hour. With structured logs, you search by order number for the payment.succeeded and order.paid events and see what happened in between: if the callback never arrived, the logs show a gateway timeout; if the callback failed while writing to the database, the error log shows a unique-key conflict or an exhausted connection pool. Ten minutes, root cause found.
Story two: the AI feature suddenly gets slow and users complain. Without logs, you can only guess whether the model is slow, the network is slow, or your own code is slow. With structured logs, you plot the duration of ai.generate events over time, grouped by model name: if only one model slowed down, it is upstream; if all models slowed down, it is your network or saturated concurrency. Once grouped, it is clear whether to chase the provider or fix your own side — no more arguing.
Story three: at 3 a.m., a background job quietly stops. Without logs, you find out in the morning, with half a night of data lost. With logs, every job run writes a job.finished line with the number of items processed and the duration, and the job system gets one simple heartbeat check: if no new job.finished appears past the expected time, send an alert. Here the log's role is not post-incident investigation but early detection: it makes "stopped" itself visible.
The shipping checklist: seven things you can do today
First, wrap a logger and route all logs through it — no more direct console.log calls; the wrapper adds timestamps, levels, and correlation IDs automatically. Second, define a list of event names with consistent naming, and stop inventing names on the spot. Third, set levels by "does a human need to act", and alert whenever an error appears. Fourth, generate a correlation ID when a request arrives and pass it through frontend and backend, synchronous and asynchronous paths alike. Fifth, keep a sensitive-field list with recursive redaction, applied before anything hits the disk. Sixth, keep logs for failure paths in full, and sample success paths. Seventh, run a post-incident review every month, and let the first question always be: were the logs at the time enough to find the root cause? If not, add logs — not guesses.
Back to the opening line: logs are written for you at 3 a.m. When writing a log, think one step further — "what will I need to see when this breaks" — and you will save an hour when it does. Structured logging is not advanced technology; it is simply replacing "write casually" with "write by the rules". Once the rules are in place, logs stop being a pile of text to dig through and become a flight recorder you can actually use.
Comments (0)
Related articles

Preview is the first gate for any file product: confirm in three seconds that this is the right file. Rendering choices per file type, security boundaries, mobile, and performance — in that order.

Cognition released SWE-2 Flash on November 4: three times faster at one-fifth the price of SWE-2, free through November 30. The real story is the new mid-training stage — and the decision to keep the weights private.

OpenAI released Images 2.6 on November 4: GIFs in ChatGPT and Codex, reusable reference images, and two API models — Flare for speed, Sunburst for consistency. Speed is the least interesting part; memory and motion are what change the workflow.