Skip to main content
Logging

Server access logs: the fields that make an incident solvable

Access logs are not application logs. Log the route template, status, duration, and request ID per request, keep the ID joinable downstream, and stop leaking credentials while you debug.

Thien Nguyen
By Thien Nguyen
Updated July 21, 2026 · 3 min read

Access logs are not application logs. Application logs tell you why a request went wrong — the stack trace, the null pointer, the failed downstream call. Access logs tell you that it went wrong, and to whom, and how often: one line per completed request, written by the edge (nginx, Envoy, the framework's HTTP layer) whether your code logged anything or not. When an endpoint goes slow at 3am, the access log is the only place that already has every request recorded before you knew to look.

The catch is that the default format is nearly useless for an incident. nginx's combined format gives you an IP, a raw URL, a status, and a byte count — and the raw URL is the problem. /orders/1042, /orders/1043, /orders/1044 are three distinct strings, so grouping by path in a dashboard explodes into millions of unique values. Log the route template instead.

{"event":"http.request","method":"POST","route":"/orders/:id","status":500,"duration_ms":8421,"request_id":"r_82f","response_bytes":73}
FieldOperational question it answers
status + routeWhich endpoint is failing, as opposed to the whole box?
duration_msSlow, or fast-failing? A 500 in 8ms is a bug; a 200 in 8s is saturation.
request_idWhich application-log lines belong to this request?
response_bytesDid the payload shape change — an empty body, a truncated list?

A worked incident

Checkout error rate ticks up. You query the access log for route=/orders/:id status>=500 over the last 15 minutes and get 340 hits, all with duration_ms around 8400 — suspiciously close to a fixed timeout. You pull one request_id (r_82f) and search the application log for it: one line, payment gateway connection timeout after 8000ms. That single ID turned "checkout is broken" into "the payment gateway is timing out" in two searches. Without it, you'd be grepping unstructured prose across three services trying to line up timestamps.

That join is the entire point, and it only works if the ID is generated at the edge and threaded through every downstream call.

Don't log the things attackers want

Redact Authorization, Cookie, session IDs, and any query parameter that carries a token — ?reset_token=... in an access log is a password-reset link sitting in plaintext in your log store, replicated to whatever ships your logs. IP addresses are often personal data under GDPR, so they need a retention policy, not an infinite bucket.

Behind a proxy, configure trusted-proxy handling before you read X-Forwarded-For. It's a client-supplied header — anyone can send X-Forwarded-For: 8.8.8.8 and forge their apparent origin, poisoning both your audit trail and any rate-limiting keyed on it. Trust only the last hop you control.

Get the request ID visible at the edge, pass it down, and pick a format your tooling can parse — an access log formatter will turn a raw combined line into fields you can actually query. An incident stops being three disconnected searches and becomes one join.

Cover photo by panumas nikhomkhai on Pexels.

References

Primary documentation and specifications checked when this article was last updated.

LoggingHTTPDevOps

Related articles

All articles