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.
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.
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.
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.
Two independent signals, and you want both:
sqlmap, nikto, zgrab,
nuclei, gobuster. Cheap, but trivially spoofed.../, /.env, /.git/config,
wp-login.php, union select, /shell.php. Harder to fake,
because the path is the attack.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.
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.
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.
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.
Your nginx access log turned into a report: what is erroring, what is slow, who is scanning you.
Get it — 19 USDT