What to actually look for in an nginx access log (and what everyone gets wrong)

Error rate, latency percentiles, path normalisation, and why average request rate hides every scanner that ever hit you.

An access log is the cheapest observability you have and the least used. Here is what to pull out of it, in the order that matters.

1 · Error rate, first and always

5xx percentage is the one number that tells you whether the site is broken right now. Anything above about 1% is worth looking at; above 5% is an incident. 4xx is mostly noise (bots, stale links) — except a sudden 403 spike on one path, which usually means something is probing.

2 · Latency percentiles, not the average

An average of 0.4s can hide a 9-second endpoint used by 3% of requests. Percentiles tell the truth:

p50  0.251s     the typical request
p95  2.138s     what 1 in 20 users feels
p99  7.069s     what makes people leave

You need $request_time in your log format for this. Add it once:

log_format main '$remote_addr - $remote_user [$time_local] "$request" '
                '$status $body_bytes_sent "$http_referer" "$http_user_agent" $request_time';

Without it, a report should say "not available", not print a plausible number.

3 · Normalise paths before counting

Raw top-paths output looks like this: /api/order/8811 (1), /api/order/8812 (1), /api/order/8813 (1)… — a list of unique ids that tells you nothing. Collapse numeric segments and UUIDs first:

path = re.sub(r"/d+(?=/|$)", "/:id", path.split("?")[0])

Now it reads /api/order/:id (447) and you can see the endpoint that carries 17% of traffic.

4 · Scanner detection: by UA, and by path

Two independent signals, and you want both:

Record which signature matched. "23 suspicious requests" is not actionable; "70 requests from 45.155.205.233 matching /.git/config and ../" is a firewall rule waiting to be written.

5 · The mistake everyone makes: averaging the rate

Someone fires 70 requests in 13 seconds, then stops. Over a day that is 70/86400 = 0.0008 req/s — invisible to any threshold on average rate. Measured as peak density in a sliding window, it is 5.38 req/s, which is obviously a burst.

70 req in 13.0s   = 5.38 req/s   <- caught
125 req in 140s   =  0.89 req/s  <- averaged away

Any "rate anomaly" check that divides total requests by total duration will never fire. Use the busiest window.

6 · Compare to yesterday, automatically

A single day's report is a snapshot; the delta is the signal. 5xx 0.90% → 8.20% is an incident. Keep a one-line JSON baseline per run and diff it.

The tool

LogDigest does all six: 5xx rate and percentiles, normalized top paths, scanner hits with the matched signature, peak-density bursts, an hourly series, and a --baseline diff. Seven-sheet xlsx plus an optional self-contained HTML report, exit code 1 when something is wrong so cron only emails you on a bad day. 19 USDT.

LogDigest — web log report

Your nginx access log turned into a report: what is erroring, what is slow, who is scanning you.

Get it — 19 USDT