packages/shared-skills/skills/programming/references/logging.md
Every log line has exactly two legitimate readers: the operator reconstructing an incident and the developer reproducing a bug. A line that serves neither is cost — storage, noise, and attention stolen from the lines that matter. Everything below derives from that.
This reference is deliberately stack-agnostic. It never tells you which logging library to use — the ecosystem table in SKILL.md owns stack defaults, and the project's existing practice overrides everything (Rule 0).
BEFORE adding a single log line, find out how this project logs. Grep for the logger initialization, a wrapper module, prior art in sibling files, and the project's agent docs (AGENTS.md / CLAUDE.md).
| What you find | Required behavior |
|---|---|
| A designated logger or wrapper | Use it exactly — its levels, its field conventions, its error-passing shape. Copy the call shape of the nearest well-written call site, not your habit. |
| A logging lib you like better | Irrelevant. NEVER introduce a second logging framework into a project that already has one. |
Raw console.* / print culture | Follow it for user-facing CLI output. For diagnostics, propose structured logging in your reply — do not unilaterally convert the project. |
| No logging at all (library, small CLI, script) | Respect the absence. A library that suddenly logs is a behavior change its consumers never asked for; a 40-line script does not need a logging framework. If logging would genuinely help, say so in your reply — add it only when the user asks or the task itself is about observability. |
Bypassing the project's designated logger with a bare console.log / print "just for this one line" is the logging equivalent of as any: it escapes every contract the project set up — stage routing, formatting, redaction, shipping.
Serialization contracts (reserved field names, argument order, the key an Error must be passed under) are project knowledge. Discover the contract from the logger config and existing call sites; if the project's agent docs do not record it, record what you found there as part of your change.
A new service earns exactly this much logging infrastructure, and no more:
NODE_ENV-style vars to decide how to log.debug, prod = info, overridable with a LOG_LEVEL-style env var.{} where the stack trace should have been — discovered during the incident. This is a one-line test; write it at setup time and the whole bug class is dead permanently.A level is a routing decision, not a severity vibe. Choose it by naming who consumes the line and what they do about it. If you cannot name the consumer, do not emit the line.
| Level | Consumer and action | The line earns this level when |
|---|---|---|
error | Alerting wakes a human NOW | The service failed at a user-visible operation and cannot recover on its own. |
warn | Reviewed in batch — dashboards, weekly triage | The request SUCCEEDED but took an abnormal path: retry needed, fallback engaged, degraded mode, suspicious data. |
info | Read during an incident to reconstruct the timeline | A state transition without which the request's story does not reconstruct: session/job/connection created, completed, destroyed. |
debug | The developer reproducing locally | Detailed tracing. Does not exist in prod. |
Two corollaries that get violated constantly:
error means the SERVICE failed, not the request. A 4xx is the client's mistake handled correctly — that is warn at most, info for routine misses. Reserve error for 5xx-class outcomes: the service could not do its job. Log 4xx as error and the alert channel drowns in noise from every crawler probing /wp-admin; a drowned alert channel is equivalent to no alerting.info is invisible. If the message says "failed", the level is warn or error — never info.Log where the system decides something, not where it does something:
Never log inside pure functions, utilities, or private helpers — callers with context log outcomes; internals stay silent. Two mechanical rules:
if statements.No speculative logs. "Might need it later" is not a consumer. A log line earns its place through evidence: a debugging session that burned rounds because this state was invisible (see the debugging bridge below), an incident postmortem, an alert that needs the field.
logger.warn({orderId, attempt}, "payment retry") — never `retrying payment for ${orderId}`. An interpolated message cannot be counted, aggregated, or alerted on.session.destroy, payment.fallback), never positionally ("Step 3"). Step numbers couple the log stream to today's call structure; the first refactor makes them lie.token, key) before logging. A leaked log is a leaked credential.warn through a channel that cannot fail, and the operation continues. An empty catch around logging is still an empty catch.| Anti-pattern | Why it fails |
|---|---|
console.* / print bypassing the project's designated logger | Escapes stage routing, redaction, and shipping — invisible in prod |
| Introducing a logging framework to a project that has none | Uninvited behavior change; Rule 0 violation |
4xx logged as error | Alert noise buries real pages |
| Log-and-rethrow at every layer | One incident looks like five |
| Variables interpolated into the message string | Un-aggregatable, un-alertable |
| "Might need it later" logs | No consumer → pure cost |
Debug-time prints promoted to permanent info | Narration, not state transitions |
| Trusting Error serialization without the proof | error: {} in prod, discovered during the incident |
When a debugging-skill session takes extra rounds because state was invisible — no line told you which branch ran, what the value was, whether the fallback engaged — that invisibility is a defect adjacent to the bug. The fix ships with the log line that would have made diagnosis one round: placed at a decision point, leveled by consumer, fields not interpolation, through the project's designated logger.
The inverse also holds: every temporary print / dbg! / console.log planted during diagnosis is a debug artifact and gets scrubbed at cleanup. The triage between the two is the consumer test — a line whose ongoing consumer you can name is part of the fix; a line that only served today's session is an artifact.