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}
| Field | Operational question it answers |
|---|---|
status + route | Which endpoint is failing, as opposed to the whole box? |
duration_ms | Slow, or fast-failing? A 500 in 8ms is a bug; a 200 in 8s is saturation. |
request_id | Which application-log lines belong to this request? |
response_bytes | Did 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 sendX-Forwarded-For: 8.8.8.8and 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.
