Logging Strategy
Intro
Good logs are structured, leveled, correlated, and free of secrets.
Pick a JSON logger, generate a request_id at every system boundary,
and propagate it through every downstream call. The cost of bad
logging is cost itself — most overspend comes from a few noisy
endpoints logged at INFO.
Overview
Log levels
Use levels intentionally. The semantic ladder:
- TRACE — fine-grained diagnostic detail (loop iterations).
Never on in production.
- DEBUG — useful during development (SQL queries, cache hits,
loaded config). Off by default in production.
- INFO — normal operational events (service started, request
handled, job completed). The default production level.
- WARN — unexpected but recoverable (retry succeeded, deprecated
API called, approaching rate limit). Not page-worthy.
- ERROR — a failure that prevented an operation from completing.
Should trigger an alert or ticket.
- FATAL — process cannot continue (missing required config, port
bound, unrecoverable state). Extremely rare.
When unsure between two levels, pick the lower one. Over-logging at
DEBUG is better than missing context at ERROR.
Structured over unstructured
Always prefer structured logging (JSON or key-value pairs). Structured
logs are machine-parseable, searchable, and survive message format
changes. Every log line should include timestamp (ISO 8601 UTC),
level, service, message, and a request_id or trace_id. See
references/structured-logging.md for language-specific libraries.
Correlation and context propagation
Generate a unique request_id (UUID or ULID) at the system boundary —
API gateway, message consumer, scheduled job. Propagate it through
every downstream call via headers (X-Request-ID) or message
metadata. Include it in every log line so you can correlate. In
distributed systems, also propagate trace_id and span_id (W3C
Trace Context). Use MDC or contextvars to avoid threading IDs through
every function signature.
What to log
Request and response summaries (method, path, status, duration);
state transitions (order placed, payment processed); decision points
(why a branch was taken, which cache was hit); error details with
stack trace and the input that caused the failure; performance data
(query duration, queue depth); lifecycle events (start, config load,
graceful shutdown); retry attempts with attempt number and backoff;
external calls with downstream service, latency, and status.
What NOT to log
- PII: names, emails, phone numbers, addresses, IP addresses
(unless legally required and compliant)
- Secrets: passwords, API keys, tokens, session IDs, card numbers
- Sensitive business data: salary, health records, financial
details
- High-cardinality user input: full request bodies — log a hash or
a truncation instead
- When in doubt, mask:
email=b***@example.com, card=****1234
Gotchas
Agent-specific failure modes — provider-neutral pause-and-self-check items:
- Logging at ERROR level for expected operational conditions. A database query that returns zero results, a user failing authentication, or a cache miss are not errors — they are normal application states. Logging them as ERROR trains the team to ignore error logs and buries actual errors in noise. Match the level to severity: INFO for normal operations, WARN for unexpected-but-recoverable, ERROR for actual failures.
- Log and rethrow — logging the same error at every level of the call stack. When a low-level function logs an error and then throws an exception, and every caller also logs before rethrowing, the same error appears three or four times in the log stream with different (and sometimes contradictory) context. Choose one place to log — typically the outermost handler that decides whether to return an error response or retry.
- PII or secrets appearing in log statements. User email addresses, full names, passwords, API keys, and session tokens written to logs create a compliance liability and a security exposure — log aggregation systems are often less strictly access-controlled than the application itself. Never log these values; log opaque IDs (numeric user IDs, hashed email) and redact sensitive fields in error messages.
- No correlation ID or request ID propagated through the request lifecycle. A log line that says "database query failed" with no request context is useless for debugging — you cannot tell which user, which request, or which code path caused it. Generate a unique request ID at the system boundary and include it in every log line for the duration of that request's handling. In distributed systems, propagate it as a header.
- Logging inside tight loops. A single request that logs once per iteration of a loop processing 10,000 items produces 10,000 log lines — overwhelming the log system and hiding every other log line from concurrent requests. Aggregate counts and log a summary after the loop: "processed 10,000 items, 42 failed" rather than one line per item.
- Unstructured log messages that vary format across similar events.
"User 123 logged in" and "Login succeeded for user_id=456" and "auth success: {\"id\": 789}" are the same event with three different formats — impossible to query consistently in a log aggregation system. Define standard event types with consistent field names and use them everywhere the same event occurs.
- Missing timestamps or timestamps without timezone. A log line without a timestamp makes it impossible to correlate with other events or determine the sequence of operations during an incident. A timestamp without a timezone is ambiguous across server locations and daylight saving time transitions. Always log ISO 8601 UTC timestamps.
Full reference
Common fields for every entry
| Field |
Description |
Example |
timestamp |
ISO 8601 with timezone |
2026-03-22T14:30:00.123Z |
level |
Log severity |
info, error, debug |
service |
Service or application name |
order-service |
message |
Human-readable description |
Order created successfully |
trace_id |
Distributed trace identifier |
4bf92f3577b34da6a3ce929d0e0e4736 |
span_id |
Span within the trace |
00f067aa0ba902b7 |
request_id |
Unique ID for the incoming request |
req_01HXYZ... |
duration_ms |
Operation duration in milliseconds |
142 |
error |
Error type or message (if applicable) |
ConnectionTimeout |
Event naming convention
Use dot-delimited event names so logs are easy to filter:
http.request.received, http.response.sent, db.query.executed,
cache.hit, cache.miss, order.created, order.cancelled,
auth.login.success, auth.login.failed,
queue.message.published, queue.message.consumed. See
references/structured-logging.md for full language patterns
(structlog, pino, slog, tracing) and ELK/Loki ingestion examples.
Aggregation and rotation
Ship logs to a centralized system (ELK, Grafana Loki, CloudWatch
Logs). Use JSON for ingestion and avoid multi-line messages where
possible. Set tiered retention: hot (7-30 days searchable), warm
(90 days), cold archive (1+ year if regulated). In containers, log to
stdout/stderr and let the orchestrator collect — never write to local
files. For VMs, rotate by size (100MB) or daily to prevent disk
exhaustion.
Performance considerations
Logging has real cost — I/O, serialization, allocation. Use lazy
evaluation for expensive log arguments so debug strings are not built
when DEBUG is off. Avoid logging inside tight loops; aggregate counts
and log a summary after. Sample high-volume logs (e.g. 1 in 100 health
checks). Use async appenders so log I/O does not block requests.
Anti-patterns
- Log and throw — logs the error, then logs it again at every
caller. Choose one place to log
- Everything at INFO — buries the events that matter
- String concatenation in log calls — wastes CPU when level is
off; use parameterized messages
"Error occurred" with no context about what, where, or why
- Mixing structured and unstructured within the same service
- Logging in a loop — aggregate and log a summary
- Timestamps without timezone — always UTC, ISO 8601
- Swallowing exceptions — catching then logging only a generic
message loses the stack trace
1---2name: logging-strategy-23description: Structured logging strategy — levels, correlation IDs, context propagation, PII avoidance. Use when designing a logging approach, reviewing log statements, setting up log aggregation (ELK, Loki, CloudWatch), adding correlation IDs to a distributed system, or deciding what to log and what to keep out.4---56# Logging Strategy78## Intro910Good logs are structured, leveled, correlated, and free of secrets.11Pick a JSON logger, generate a `request_id` at every system boundary,12and propagate it through every downstream call. The cost of bad13logging is cost itself — most overspend comes from a few noisy14endpoints logged at INFO.1516## Overview1718### Log levels1920Use levels intentionally. The semantic ladder:21221. **TRACE** — fine-grained diagnostic detail (loop iterations).23 Never on in production.242. **DEBUG** — useful during development (SQL queries, cache hits,25 loaded config). Off by default in production.263. **INFO** — normal operational events (service started, request27 handled, job completed). The default production level.284. **WARN** — unexpected but recoverable (retry succeeded, deprecated29 API called, approaching rate limit). Not page-worthy.305. **ERROR** — a failure that prevented an operation from completing.31 Should trigger an alert or ticket.326. **FATAL** — process cannot continue (missing required config, port33 bound, unrecoverable state). Extremely rare.3435When unsure between two levels, pick the lower one. Over-logging at36DEBUG is better than missing context at ERROR.3738### Structured over unstructured3940Always prefer structured logging (JSON or key-value pairs). Structured41logs are machine-parseable, searchable, and survive message format42changes. Every log line should include `timestamp` (ISO 8601 UTC),43`level`, `service`, `message`, and a `request_id` or `trace_id`. See44`references/structured-logging.md` for language-specific libraries.4546### Correlation and context propagation4748Generate a unique `request_id` (UUID or ULID) at the system boundary —49API gateway, message consumer, scheduled job. Propagate it through50every downstream call via headers (`X-Request-ID`) or message51metadata. Include it in every log line so you can correlate. In52distributed systems, also propagate `trace_id` and `span_id` (W3C53Trace Context). Use MDC or contextvars to avoid threading IDs through54every function signature.5556### What to log5758Request and response summaries (method, path, status, duration);59state transitions (order placed, payment processed); decision points60(why a branch was taken, which cache was hit); error details with61stack trace and the input that caused the failure; performance data62(query duration, queue depth); lifecycle events (start, config load,63graceful shutdown); retry attempts with attempt number and backoff;64external calls with downstream service, latency, and status.6566### What NOT to log6768- **PII**: names, emails, phone numbers, addresses, IP addresses69 (unless legally required and compliant)70- **Secrets**: passwords, API keys, tokens, session IDs, card numbers71- **Sensitive business data**: salary, health records, financial72 details73- **High-cardinality user input**: full request bodies — log a hash or74 a truncation instead75- When in doubt, mask: `email=b***@example.com`, `card=****1234`7677## Gotchas7879Agent-specific failure modes — provider-neutral pause-and-self-check items:8081- **Logging at ERROR level for expected operational conditions.** A database query that returns zero results, a user failing authentication, or a cache miss are not errors — they are normal application states. Logging them as ERROR trains the team to ignore error logs and buries actual errors in noise. Match the level to severity: INFO for normal operations, WARN for unexpected-but-recoverable, ERROR for actual failures.82- **Log and rethrow — logging the same error at every level of the call stack.** When a low-level function logs an error and then throws an exception, and every caller also logs before rethrowing, the same error appears three or four times in the log stream with different (and sometimes contradictory) context. Choose one place to log — typically the outermost handler that decides whether to return an error response or retry.83- **PII or secrets appearing in log statements.** User email addresses, full names, passwords, API keys, and session tokens written to logs create a compliance liability and a security exposure — log aggregation systems are often less strictly access-controlled than the application itself. Never log these values; log opaque IDs (numeric user IDs, hashed email) and redact sensitive fields in error messages.84- **No correlation ID or request ID propagated through the request lifecycle.** A log line that says "database query failed" with no request context is useless for debugging — you cannot tell which user, which request, or which code path caused it. Generate a unique request ID at the system boundary and include it in every log line for the duration of that request's handling. In distributed systems, propagate it as a header.85- **Logging inside tight loops.** A single request that logs once per iteration of a loop processing 10,000 items produces 10,000 log lines — overwhelming the log system and hiding every other log line from concurrent requests. Aggregate counts and log a summary after the loop: "processed 10,000 items, 42 failed" rather than one line per item.86- **Unstructured log messages that vary format across similar events.** `"User 123 logged in"` and `"Login succeeded for user_id=456"` and `"auth success: {\"id\": 789}"` are the same event with three different formats — impossible to query consistently in a log aggregation system. Define standard event types with consistent field names and use them everywhere the same event occurs.87- **Missing timestamps or timestamps without timezone.** A log line without a timestamp makes it impossible to correlate with other events or determine the sequence of operations during an incident. A timestamp without a timezone is ambiguous across server locations and daylight saving time transitions. Always log ISO 8601 UTC timestamps.8889## Full reference9091### Common fields for every entry9293| Field | Description | Example |94|--------------|--------------------------------------|------------------------------------|95| `timestamp` | ISO 8601 with timezone | `2026-03-22T14:30:00.123Z` |96| `level` | Log severity | `info`, `error`, `debug` |97| `service` | Service or application name | `order-service` |98| `message` | Human-readable description | `Order created successfully` |99| `trace_id` | Distributed trace identifier | `4bf92f3577b34da6a3ce929d0e0e4736` |100| `span_id` | Span within the trace | `00f067aa0ba902b7` |101| `request_id` | Unique ID for the incoming request | `req_01HXYZ...` |102| `duration_ms`| Operation duration in milliseconds | `142` |103| `error` | Error type or message (if applicable)| `ConnectionTimeout` |104105### Event naming convention106107Use dot-delimited event names so logs are easy to filter:108`http.request.received`, `http.response.sent`, `db.query.executed`,109`cache.hit`, `cache.miss`, `order.created`, `order.cancelled`,110`auth.login.success`, `auth.login.failed`,111`queue.message.published`, `queue.message.consumed`. See112`references/structured-logging.md` for full language patterns113(structlog, pino, slog, tracing) and ELK/Loki ingestion examples.114115### Aggregation and rotation116117Ship logs to a centralized system (ELK, Grafana Loki, CloudWatch118Logs). Use JSON for ingestion and avoid multi-line messages where119possible. Set tiered retention: hot (7-30 days searchable), warm120(90 days), cold archive (1+ year if regulated). In containers, log to121stdout/stderr and let the orchestrator collect — never write to local122files. For VMs, rotate by size (100MB) or daily to prevent disk123exhaustion.124125### Performance considerations126127Logging has real cost — I/O, serialization, allocation. Use lazy128evaluation for expensive log arguments so debug strings are not built129when DEBUG is off. Avoid logging inside tight loops; aggregate counts130and log a summary after. Sample high-volume logs (e.g. 1 in 100 health131checks). Use async appenders so log I/O does not block requests.132133### Anti-patterns134135- **Log and throw** — logs the error, then logs it again at every136 caller. Choose one place to log137- **Everything at INFO** — buries the events that matter138- **String concatenation in log calls** — wastes CPU when level is139 off; use parameterized messages140- **`"Error occurred"`** with no context about what, where, or why141- **Mixing structured and unstructured** within the same service142- **Logging in a loop** — aggregate and log a summary143- **Timestamps without timezone** — always UTC, ISO 8601144- **Swallowing exceptions** — catching then logging only a generic145 message loses the stack trace