Top Talkers from a Rotated Apache Log
An e-commerce team has a single Apache web server. The access log is in combined format, one request per line; the client IP is the first field and the timestamp is the fourth bracket-delimited field, for example 203.0.113.45 - - [12/Oct/2024:14:07:33 +0000] "GET /cart HTTP/1.1" 200 5123. The site was slow on 12 October between 14:00 and 15:00. The log for that day is rotated and compressed as /var/log/apache2/access.log.2.gz; the live file has already rolled over. You need the top 10 client IPs by number of requests in that exact hour to give to the network team. You may use only standard shell tools on the server; Python and awk scripts saved to disk are not permitted by change policy, but one-liners are fine. You also must not modify the log files.
1. Write a one-line pipeline that prints the 10 IPs with the most requests between 14:00:00 and 14:59:59 on 12 October, using the compressed log. Explain each stage and why sorting matters.
Read the worked answer
Pipeline: zcat /var/log/apache2/access.log.2.gz | grep '12/Oct/2024:14:' | awk '{print $1}' | sort | uniq -c | sort -rn | head -10. Stage by stage: zcat decompresses to stdout without creating a file. The grep pattern is quoted so the shell does not treat the slashes or brackets specially; it keeps only lines in the 14:00 hour. awk '{print $1}' prints the first whitespace-delimited field, the client IP. sort is essential because uniq only collapses adjacent identical lines; without sorting, the same IP in two places is counted twice. uniq -c prefixes each run with its count. sort -rn sorts numerically in reverse so the largest counts are first; plain sort would put 9 above 10 lexically. head -10 limits output. If the log contains malformed lines with missing fields, an empty first field may appear; use awk 'NF>=7 {print $1}' to skip those. Set LC_ALL=C before sorting if you need deterministic byte order across locales. Do not use grep -c alone: it gives one total, not a count per IP.
2. A colleague suggests `zcat access.log.2.gz > access.log.2` to make it easier, then `sort access.log.2 | uniq -c`. What is wrong with this, and how would you safely keep a copy of the intermediate result if needed?
Read the worked answer
zcat access.log.2.gz > access.log.2 is dangerous if the output name equals the input name: the shell truncates the target before zcat starts reading, destroying the compressed log. Even with a different name, it may fill /var on a server already near capacity, and it creates an uncompressed copy of possibly sensitive client IPs. The change policy also says not to modify log files. If an intermediate is truly needed, check space with df -h /tmp, create a unique file with mktemp, and redirect into that: tmp=$(mktemp) && zcat ... > "$tmp", then remove it with rm -f "$tmp" and never leave it world-readable. set -o noclobber prevents accidental overwrite with >, but does not protect you if you choose a new name. Better: keep the streaming pipeline, or use tee only when you need both display and a saved copy: ... | tee /tmp/hour.txt | .... Also note that > creates with the shell's umask, often 644, which may expose IP addresses; use umask 077 for the temporary file. Verify the compressed original's size and mtime before and after.
3. Explain why the timestamp pattern must be quoted in the shell and why a literal bracket needs escaping inside awk. Give a safer alternative.
Read the worked answer
The timestamp is enclosed in square brackets, and the shell treats [ and ] as glob (filename pattern) metacharacters. Unquoted, a pattern like [12/Oct/2024:14:] could be expanded to matching filenames in the current directory, changing the argument or leaving it unchanged depending on what files exist. Single quotes prevent any shell expansion. Inside awk, the pattern is a regular expression, not a glob; square brackets there mean a character class, so to match a literal bracket you escape it: awk '$4 ~ /\[12\/Oct\/2024:14:/ {print $1}'. The slashes also need escaping inside a /.../ regex. A simpler, less error-prone approach is to use index($4, "12/Oct/2024:14:") == 1 or pass the pattern as a variable with -v. Test on a small sample with zcat ... | head -3 before running the full pipeline. This is exactly the kind of quoting error that silently produces zero results and wastes an hour.