Under interview or on-call time pressure without an attached debugger, logging remains the fastest tool if it is structured, correlated, and sampled deliberately rather than scattered ad hoc.
Log levels under pressure
Pick the level by "who needs to see this, and how urgently": debug for verbose developer detail (off by default in production), info for normal operational milestones (request received, job completed), warn for a recoverable anomaly a human should notice the trend of, error for a failed operation that needs attention, fatal for the process being unable to continue. When triaging live, temporarily raising one component's level (not the whole fleet's) is faster than adding new log lines and redeploying.
When to use structured JSON logs
Plain text lines are fine for a human tailing a single process locally; once logs are aggregated across many services and queried by a log platform (which is the normal production case), structured JSON is the right default because it lets a query engine filter and aggregate on exact fields (status_code:500 AND route:"/checkout") instead of parsing free text with regex. The trade-off is verbosity and a small serialization cost, which is why local/dev tooling often prefers a pretty-printed or plain line format and reserves JSON for anything actually shipped off-box.
Correlation IDs across services
Generate a request/trace ID at the system's entry point (the edge/gateway) if one doesn't already arrive from upstream, propagate it on every outbound call via a header (a custom X-Request-Id, or the standard traceparent header if using distributed tracing), and include it as its own field in every log line and every span for that request. This is what lets you join a metric spike, a slow trace, and the exact log line explaining it, without it you're correlating by timestamp and guesswork.
Sampling strategies
Full-volume logging at info/debug for every request is rarely affordable at scale. Reasonable defaults: sample a small fixed percentage of normal-path requests (e.g. 1-5%) at info level, but always log 100% of errors and warnings regardless of sampling, and temporarily raise the sample rate to 100% for a specific route/tenant once an anomaly is already flagged there (targeted, time-boxed, not permanent). This keeps volume and cost bounded while still guaranteeing the failing cases are always captured in full.
Log query best practices
Query on structured fields (level, route, status_code, trace_id), not by grepping free text, since free-text search doesn't scale once volume is high and is easy to get wrong with partial matches. Start a query scoped to the incident's time window and the fields already narrowed by metrics/traces (a specific route or tenant), rather than an unscoped full-text search across the whole fleet's logs, which is slow and returns too much to scan manually.
Example log schema (JSON) for a single request
json
{
"timestamp": "2026-07-23T14:02:03.512Z",
"level": "error",
"service": "checkout-api",
"trace_id": "8f2b1c1e4a9d4e2f9b6a1c2d3e4f5061",
"span_id": "a1b2c3d4e5f60718",
"request_id": "req_9f8e7d6c",
"route": "/api/checkout",
"method": "POST",
"status_code": 500,
"duration_ms": 812,
"user_id": "u_42",
"tenant_id": "acme-corp",
"message": "downstream payment call failed",
"error": {
"type": "TimeoutError",
"downstream_service": "payments-api"
}
}
trace_id (and span_id if tracing is in use) is what joins this line to the request's other log lines and its trace; route/tenant_id/status_code are the fields a query filters on; duration_ms and the nested error object give the two things needed for the "why" once metrics and traces have already scoped the problem.
Where to place logs
Log at the boundaries: entry and exit of the function under suspicion, immediately before and after any I/O call, and at every branch point in the failing conditional, so the recorded sequence tells you exactly which path the code took and with what state, without needing to single-step.
Trade-offs and pitfalls
Print-debugging or debug-level logging that isn't removed afterward becomes log spam in production and, at high enough volume, buries the one relevant line under noise while also raising storage cost; the discipline is to either delete temporary lines before merging or promote the useful ones to a proper structured log line at an appropriate level and sample rate. A debugger is still the better tool when you need to inspect complex object state interactively rather than guessing which fields matter in advance, or when the bug is in a tight loop where adding log lines changes timing enough to hide a race.