Analyzing Nginx Access Logs with Bash
Parse an Nginx access log with Bash associative arrays to find the busiest IPs, paths, status codes, and user agents.
An access log is a record of individual requests. Operational questions are usually about patterns: Which clients are sending the most traffic? Which endpoints are hottest? Are errors concentrated around one status code? What kinds of clients are connecting?
My nginx-log-analyzer project answers four of those questions with one Bash script:
- Top five client IP addresses
- Top five requested paths
- Top five HTTP response codes
- Top five user agents
The implementation is intentionally transparent. It reads a standard combined-style Nginx access log, counts values in Bash associative arrays, and ranks the results with sort and head.
Understanding the input
A typical access-log line contains fields similar to:
203.0.113.10 - - [time] "GET /health HTTP/1.1" 200 123 "-" "User Agent"
Whitespace works for the early fields, but quoted request and user-agent values require extra care. The project uses two passes: one for IPs, paths, and status codes, and one split on double quotes for user agents.
Counting IPs, paths, and codes
Associative arrays map a string key to its count:
declare -A ips
declare -A paths
declare -A codes
The first pass extracts selected fields from each line:
while read -r ip _ _ _ _ _ path _ code _; do
if [[ "$ip" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]; then
((ips["$ip"]++))
fi
if [[ "$path" == *"/"* ]]; then
((paths["$path"]++))
fi
if [[ "$code" =~ ^[0-9]+$ ]]; then
((codes["$code"]++))
fi
done < nginx-access.log
Basic validation stops obviously malformed values from becoming report keys. The field positions assume the log format used by the sample file; a custom Nginx log_format may require different parsing.
Ranking a Bash associative array
Associative arrays do not preserve frequency order. The script converts each entry into count value, sorts numerically in descending order, and keeps five rows:
for ip in "${!ips[@]}"; do
echo "${ips[$ip]} $ip"
done | sort -rn | head -5 | while read -r count ip; do
echo "$ip - $count requests"
done
The same pattern is used for paths and response codes. This is a useful shell technique: normalize data into a sortable first column, use mature text tools for ordering, then format only the final result.
Parsing user agents separately
User agents contain spaces, so ordinary whitespace field positions cannot represent them reliably. Splitting on the quote character exposes quoted sections as fields:
declare -A user_agents
while IFS='"' read -r _ _ _ _ _ ua _; do
((user_agents["$ua"]++))
done < nginx-access.log
The counts are ranked with the same sort -rn | head -5 pipeline.
Running the analyzer
git clone https://github.com/Mohammed-Aftab-Siddique/nginx-log-analyzer.git
cd nginx-log-analyzer
chmod +x nginx-log-analyser.sh
./nginx-log-analyser.sh
The script currently expects nginx-access.log in its working directory. For a real server, copy a log into a safe analysis directory or extend the script to accept a path argument. Reading /var/log/nginx/access.log directly may require group membership or elevated permission.
What the report can reveal
- A dominant IP may be a trusted uptime probe, a crawler, a noisy integration, or abusive traffic.
- A hot path can guide caching and capacity decisions.
- High
4xxcounts may reveal broken clients, stale links, or scanning. - High
5xxcounts indicate requests the server or upstream could not complete. - User-agent concentration helps separate browser traffic from bots and automated clients.
Counts provide direction, not a complete diagnosis. Always combine them with a time window, application behavior, and known traffic sources.
Where to take it next
For larger or continuously written logs, a production-oriented version should accept a filename, handle rotated and compressed logs, include timestamps, guard against format changes, and avoid holding every unique value in memory. Tools such as awk, GoAccess, Loki, or a log analytics platform become attractive as scale and query complexity grow.
The Bash version remains valuable because every stage is visible. It turns raw text into a ranked operational signal with no service to install and no hidden query engine.
See the script and sample input in the nginx-log-analyzer repository.