Apply consistent logging so operations are debuggable and compliant without leaking secrets or PII.
When to Activate
- Adding or refactoring log statements
- User asks for "logging", "log format", or "what to log"
- Defining or reviewing logging standards for the project
- After an incident where logs were insufficient or leaked data
Core Principles
- Define "working" before instrumenting — Write the 2–4 questions on-call will actually ask ("did checkout succeed for this user?", "which dependency is slow?") and make every signal map to one of them. A log line that answers no operational question is noise.
- Structured — Prefer key-value fields (e.g. JSON) over long prose so logs are queryable and parseable. Use the same structure across the app (timestamp, level, message, fields).
- Cardinality — Never use unbounded values as index/label keys (user_id, email, full URL, raw error text) in metrics/labels — they explode cardinality; keep them as event fields instead. Alert on symptoms (user-visible failure), not causes (one host's CPU).
- Verify the telemetry itself — After instrumenting, induce the failure and confirm you can locate it from the logs/metrics alone. Untested observability tends to be silently wrong — e.g. sampling upstream of metric generation skews a request-rate metric by the sampling ratio while nothing looks broken.
- Levels — Use consistently: ERROR (failures, exceptions), WARN (recoverable issues, deprecations), INFO (key business events, request summary), DEBUG (detailed flow; disable or sample in production).
- Context — Include request_id, trace_id, or correlation_id when available. Include user_id, order_id, or similar only when safe and allowed by policy. Do not log full PII (email, phone, address) in plain text unless required and compliant.
- No secrets — Never log passwords, tokens, API keys, or full card numbers. Redact or omit. For debugging, mask or show last 4 digits only where policy allows.
- One place — Use the project's logging library (Winston, Pino, log4j, structlog, etc.) and output to the same pipeline (e.g. stdout) that the platform collects.
Work Process
- Check existing practice — What format does the project use (JSON, plain text)? What library? What levels? Match it.
- Choose level — error for failures; warn for recoverable or deprecated; info for key actions (request completed, order created); debug for detailed flow. Do not use info for verbose per-item logs in hot paths; use debug or sampled info.
- Add context — Request id, operation name, duration, status. Identifiers (user_id, order_id) only if policy allows. Structured fields, not interpolated into message string when the logger supports structured fields.
- Redact — No secrets; no full PII in message or fields. If you must log something sensitive for debugging, use redaction or sampling and document.
- Verify — Logs go to stdout or the configured sink; format is parseable; no secrets or PII in sample output.
What to Log
| Category | Level | Content | Do not log |
|---|---|---|---|
| Request start/end | INFO | method, path (query string stripped), status, duration_ms, request_id | Body, headers with tokens, query strings, path segments carrying tokens or PII |
| Errors | ERROR | message, error type, stack (server-side), request_id | Full request/response, secrets |
| Recoverable issues | WARN | message, context (e.g. retry count), request_id | |
| Key business events | INFO | event name, relevant ids (order_id, user_id if safe), outcome | Full payloads, PII |
| External calls | INFO or DEBUG | service, operation, duration_ms, outcome (success/failure) | Full request/response, credentials |
| Detailed flow | DEBUG | step, state, ids | Secrets, PII |
Format (structured)
JSON (recommended for production):
{
"timestamp": "2024-03-15T10:30:00.123Z",
"level": "info",
"message": "Request completed",
"request_id": "abc-123",
"method": "GET",
"path": "/api/orders",
"status": 200,
"duration_ms": 45
}
Fields: Prefer consistent names (snake_case or camelCase per project). Put variable data in fields, not only in the message string, so logs are queryable.
Wide events (canonical log lines)
For request-driven services, prefer one context-rich structured event per request per service over scattered log lines:
- Middleware creates the event and owns timing, status, and emission — emit in
finallyso failures still produce the event. Handlers only add business fields along the way. - Include environment fields in every event: commit hash, service version, region.
- Capture business context, not just mechanics: "premium customer failed a $2,499 checkout" beats "checkout failed".
- Scattered step-by-step lines stay at debug level; the wide event is the queryable record of what happened.
Good vs Bad
Good:
logger.info({ request_id, method, path, status, duration_ms }, 'Request completed');
logger.error({ err, request_id }, 'Payment failed');
logger.debug({ order_id, step: 'validation' }, 'Validating order');
Bad:
console.log('User ' + user.email + ' did something'); // PII in log
logger.info('Token: ' + token); // Secret in log
logger.error('Error: ' + err); // May include stack or internal detail in message; use structured field
Rules
- Do not add logs that dump full request/response or env vars. Suggest redaction or sampling if needed for debugging.
- If the project has a logging or privacy policy (retention, PII, secrets), align with it.
- Scrub the
pathbefore it is written: drop the query string and replace path segments that carry a token, an email or another personal identifier with a route template (/users/:id). Mask client IPs where the full address is not needed (zero the last octet of IPv4, the last 80 bits of IPv6). - Log the event, not the data: "user changed email" with the user id, never the old and new values.
- Keep the audit log (sensitive reads, admin actions) apart from the application log, with its own retention and its own access list; the app log is wide-readable and short-lived, the audit log is neither.
- Use the same library and format as the rest of the codebase. Do not introduce a second logging system without good reason.
Checklist
- Level appropriate (error/warn/info/debug)
- Context included (request_id, operation, duration where relevant)
- No secrets or full PII in message or fields
- Structured format (fields) when logger supports it
- Matches project library and format
Anti-patterns
| Anti-pattern | Better approach |
|---|---|
| Logging full request/response | Log method, path, status, duration; redact or omit body/headers |
| Using console.log in server code | Use project logger with levels and structure |
| Interpolating everything into message | Use structured fields (request_id, order_id, etc.) |
| Logging at info for every iteration in a loop | Use debug or sample (e.g. every Nth) |
Leftover print-debugging (print(), fmt.Println, console.log) in production paths |
Remove, or convert to logger.debug with fields |
| "We'll redact later" | Redact or omit from the start; do not log secrets |