Log analysis pipeline
Compose find, grep, cut, sort, uniq -c, and awk into a multi-stage pipeline that extracts real insight from access logs — top client IPs, status-code distribution, and error spikes — in a single shell one-liner.
You have a server generating gigabytes of Nginx access logs. Something spiked P99 latency at 03:47 this morning. You need the answer in two minutes, not two hours of writing Python. Every tool you need — find, grep, cut, sort, uniq -c, awk — is already on the machine, and they compose cleanly through pipes. This lesson builds that pipeline one stage at a time, so you understand exactly what each stage contributes and can adapt it to any log format you encounter in production.
After this lesson you can build an incremental log-analysis pipeline using find | xargs grep, cut, sort | uniq -c | sort -rn, and awk to extract top client IPs, status-code counts, and error rates from standard Combined Log Format access logs.
Start with find to locate the log files across rotated archives. A production server often has access.log, access.log.1, access.log.2.gz, and so on. find locates all of them; xargs passes them as arguments to the next command rather than piping content — that distinction matters because some commands (like grep -h) need file arguments, not stdin.
find /var/log/nginx -name 'access.log*' -not -name '*.gz'Output: a newline-separated list of matching paths.
Add -mtime -1 to restrict to files modified in the last 24 hours — essential when logs rotate daily and you only care about recent data.
find /var/log/nginx -name 'access.log*' -not -name '*.gz' -mtime -1Never pipe find output directly to cat for very large lists — use xargs instead to batch the paths into fewer grep invocations.
Add grep to filter by time window or error class. The Combined Log Format timestamp looks like [21/Jun/2026:03:. Grep for an hour prefix to scope the data to the spike window.
find /var/log/nginx -name 'access.log*' -not -name '*.gz' -mtime -1 \
| xargs grep '21/Jun/2026:03:'-h suppresses the filename prefix from grep output (cleaner for downstream parsing):
find /var/log/nginx -name 'access.log*' -not -name '*.gz' -mtime -1 \
| xargs grep -h '21/Jun/2026:03:'You now have every request logged in that hour on stdout — potentially millions of lines but all relevant.
Extract the status code field with cut. A Combined Log Format line looks like:
192.0.2.1 - alice [21/Jun/2026:03:47:12 +0000] "GET /api/users HTTP/1.1" 200 1234Fields are space-delimited. The status code is field 9. cut -d' ' -f9 extracts it.
find /var/log/nginx -name 'access.log*' -not -name '*.gz' -mtime -1 \
| xargs grep -h '21/Jun/2026:03:' \
| cut -d' ' -f9Output: a column of status codes — 200, 404, 503, … one per line. This is the input for the counting stage.
Count and rank with sort | uniq -c | sort -rn. This three-command sequence is the canonical histogram idiom:
sort— groups identical lines adjacent (required byuniq)uniq -c— collapses runs and prefixes each with its countsort -rn— sorts by numeric count descending (highest first)
find /var/log/nginx -name 'access.log*' -not -name '*.gz' -mtime -1 \
| xargs grep -h '21/Jun/2026:03:' \
| cut -d' ' -f9 \
| sort | uniq -c | sort -rnSample output:
4821 200
312 503
87 404
14 401You now know 503s spiked to 312 during that hour — that is the signal. The whole pipeline runs in seconds on millions of lines.
Extend with awk for multi-field analysis and calculated rates. awk is the Swiss Army knife of field-based text processing. Use it to compute the error rate in a single pass, or to pull the client IP (field 1) for top-talker analysis.
Top client IPs during the spike:
find /var/log/nginx -name 'access.log*' -not -name '*.gz' -mtime -1 \
| xargs grep -h '21/Jun/2026:03:' \
| awk '{print $1}' \
| sort | uniq -c | sort -rn \
| head -10Error rate as a percentage with awk:
find /var/log/nginx -name 'access.log*' -not -name '*.gz' -mtime -1 \
| xargs grep -h '21/Jun/2026:03:' \
| awk '$9 >= 500 {err++} {total++} END {printf "5xx rate: %.1f%%\n", err/total*100}'awk processes each line in one pass — no intermediate sort needed. Use awk when you need arithmetic, conditionals, or multiple output columns. Use sort | uniq -c when you need ranked counts.
Full incident triage: find the top 5 IPs hammering 503 errors during a 10-minute window.
find /var/log/nginx -name 'access.log*' -not -name '*.gz' -mtime -1 \
| xargs grep -h '21/Jun/2026:03:4[0-9]:' \
| awk '$9 == 503 {print $1}' \
| sort | uniq -c | sort -rn \
| head -5Breaking this down:
grep '21/Jun/2026:03:4[0-9]:'— the regex4[0-9]matches minutes 40–49 (the 10-minute window).awk '$9 == 503 {print $1}'— keeps only 503 lines and prints the client IP (field 1).sort | uniq -c | sort -rn | head -5— the ranking idiom, limited to 5 results.
Sample output:
142 192.0.2.47
89 192.0.2.23
61 192.0.2.105
44 192.0.2.8
31 192.0.2.66One IP accounts for 142 out of ~329 total 503s in that window — that is a stuck client or an upstream dependency retry loop hammering the service. You found the source in under 10 seconds.
▸Why this works
On macOS / BSD, sort does not support -rn combined as one flag in some older versions — use sort -r -n explicitly. awk on macOS is one-true-awk (BWK awk), not gawk; most field-processing idioms work identically. For gzip-compressed rotated logs (access.log.2.gz), replace the cat / xargs grep stage with zcat or zgrep: find /var/log/nginx -name '*.gz' | xargs zgrep -h 'pattern'.
▸Common mistake
uniq -c requires sorted input. If you pipe directly to uniq -c without sorting first, you get counts per consecutive run — identical values that appear in different positions are counted separately. Always sort | uniq -c, not uniq -c alone. A second common mistake: using sort -n (ascending) without -r when you want the highest count first — you end up reading from the bottom of the output.
You run: cat access.log | cut -d' ' -f9 | uniq -c | sort -rn. The output looks wrong — counts like '1' for every line. What is the most likely cause?
The log-analysis pipeline composes six tools in sequence: find locates log files across rotations; xargs grep scopes to a time window or pattern; cut -d' ' -fN extracts a single field; sort | uniq -c | sort -rn is the ranking idiom for any counted histogram; awk handles multi-field arithmetic and computed rates in one pass. Build the pipeline incrementally — add one stage at a time and verify its output before adding the next. uniq -c requires sorted input. awk is the right tool when you need conditionals or arithmetic; sort | uniq -c is the right tool when you need ranked counts. Together they answer incident questions in seconds on gigabyte logs without installing anything.
Practice
Start at the top. Tasks go easiest → hardest: recall a fact, apply it to a case, then a senior-level stretch. Open one, attempt it, then reveal.
Something unclear?
Ask a question about this lesson. Questions are anonymous and go straight to the author to make the lesson better.