A misleading 404 top-talkers pipeline on a 4 GB nginx log
An nginx access log at /var/log/nginx/access.log is 4.1 GB and 2.4 million lines in combined log format. A typical line begins: 10.0.0.5 - - [12/Mar/2024:10:00:00 +0000] GET /missing HTTP/1.1 404 153 - curl/7.68. A junior admin runs:
grep 404 /var/log/nginx/access.log | cut -d' ' -f1 | sort | uniq -c | sort -n | head -10
The output's top entry is 127.0.0.1 with count 3, and 10.0.0.5, which the admin knows generates thousands of 404s, is absent. The admin concludes the log is truncated. You notice that some request paths contain 404, some response sizes contain 404, and a few clients are IPv6, for example 2001:db8::1. The server has 2 GB RAM and the file must not be loaded into memory. The on-call window is 15 minutes.
1. Explain at least three independent reasons the pipeline's output is misleading. Do not just give a corrected command.
Read the worked answer
At least four independent defects:
grep 404is a substring match. It selects lines where 404 appears anywhere: the status field, a path such as/404.html, a byte count such as1404, or even a user agent. Those lines are counted as 404 responses even when the status is 200.sort -nis ascending.head -10therefore prints the ten smallest counts, not the largest. The correct order issort -rn, orsort -n | tail.headafter ascending sort hides the heavy hitters. 10.0.0.5 with thousands of 404s is at the bottom and is cut off. That is why it is absent, not because the log is truncated.cut -d' ' -f1assumes a single-space separator and a fixed first field. It happens to work for these IPv4 lines, but it will break on malformed lines or if the log format changes, and it does nothing to validate the status.uniq -conly counts adjacent duplicates, so the input must be sorted on the same key. Here it is sorted by the whole line, which is fine, but the pattern is fragile if you later add fields.
A quick check is to compare grep -c ' 404 ' access.log with awk '$9==404' access.log | wc -l; the counts will differ.
2. Write a streaming pipeline that lists the top 10 client addresses with HTTP status exactly 404, and explain the field choices.
Read the worked answer
Use awk to test the status field exactly, then sort and count:
awk '$9 == 404 {print $1}' /var/log/nginx/access.log \
| LC_ALL=C sort \
| uniq -c \
| sort -rn \
| head -10
Why it works:
- In combined log format the fields are: 1 client address, 2 ident, 3 auth user, 4 date, 5 timezone, 6 method, 7 path, 8 protocol, 9 status, 10 bytes.
$9is the status, so$9 == 404is an exact numeric comparison and does not match a 404 in the path or in the byte count. - IPv6 addresses contain colons but no spaces, so they remain a single
$1field and are counted correctly. No special handling is needed. awkstreams line by line, so the 4.1 GB file is never loaded into memory; the sort's working set is bounded by the number of distinct addresses, not lines.LC_ALL=C sortgives a stable byte order, anduniq -cthen counts adjacent identical addresses.sort -rnputs the largest count first.- If the log can contain leading spaces or malformed lines, add
NF >= 9to the awk condition.
3. The admin wants to reuse the same pipeline for status 500 and for bytes sent. What changes and what stays the same? Mention one pitfall with awk field numbers.
Read the worked answer
The structure stays the same; only the filter and the extracted field change.
For 500s: awk '$9 == 500 {print $1}' ... | sort | uniq -c | sort -rn | head.
For bytes sent, you usually want a sum rather than a count: awk '$9 == 200 && $10 ~ /^[0-9]+$/ {sum += $10} END {print sum}'. The $10 ~ /^[0-9]+$/ guard matters because a - in the byte field would otherwise be treated as zero silently, understating traffic.
The pitfall is that field numbers are positional. They are reliable only while the line matches the expected combined format. A request path containing an unencoded space, a different log format such as JSON, or an extra vhost field shifts everything after it, and $9 may no longer be the status. For production, validate with awk 'NF != 10 {print NR": "$0}' | head or parse with a format-aware tool. Do not assume $9 is universal across nginx configurations.