Implementation — Log Analyzer¶
Build the report incrementally, one section at a time.
Step 1 — Scaffold and Argument Parsing¶
#!/usr/bin/env bash
set -euo pipefail
TOP_N=10
OUTPUT=""
FILTER_STATUS=""
LOGFILE=""
usage() {
cat <<EOF
Usage: $(basename "$0") [OPTIONS] LOGFILE
-n, --top N Show top N results (default: 10)
-o, --output FILE Write report to FILE (default: stdout)
-s, --status CODE Filter to a specific HTTP status code
-h, --help Show this help
EOF
}
while [[ "${1:-}" =~ ^- ]]; do
case "$1" in
-n|--top) TOP_N="${2:?--top requires a value}"; shift ;;
-o|--output) OUTPUT="${2:?--output requires a value}"; shift ;;
-s|--status) FILTER_STATUS="${2:?--status requires a value}"; shift ;;
-h|--help) usage; exit 0 ;;
*) echo "Unknown option: $1" >&2; usage; exit 1 ;;
esac
shift
done
LOGFILE="${1:-}"
[[ -z "$LOGFILE" ]] && { echo "Error: LOGFILE required" >&2; usage; exit 1; }
[[ -f "$LOGFILE" ]] || { echo "Error: '$LOGFILE' not found" >&2; exit 1; }
[[ -r "$LOGFILE" ]] || { echo "Error: '$LOGFILE' not readable" >&2; exit 1; }
Step 2 — Output Routing¶
Route all report output to either a file or stdout:
# Open output descriptor
if [[ -n "$OUTPUT" ]]; then
exec 3>"$OUTPUT"
else
exec 3>&1
fi
report() { echo "$*" >&3; }
report_line() { printf " %-35s %7s\n" "$1" "$2" >&3; }
Step 3 — Traffic Summary¶
print_traffic_summary() {
local total unique_ips total_bytes
total=$(wc -l < "$LOGFILE")
unique_ips=$(awk '{print $1}' "$LOGFILE" | sort -u | wc -l)
total_bytes=$(awk '{sum += $10} END {print sum+0}' "$LOGFILE")
# Convert bytes to human-readable
local human_bytes
if (( total_bytes > 1073741824 )); then
human_bytes=$(awk -v b="$total_bytes" 'BEGIN { printf "%.1f GB", b/1073741824 }')
elif (( total_bytes > 1048576 )); then
human_bytes=$(awk -v b="$total_bytes" 'BEGIN { printf "%.1f MB", b/1048576 }')
else
human_bytes=$(awk -v b="$total_bytes" 'BEGIN { printf "%.1f KB", b/1024 }')
fi
report "── Traffic ──────────────────────────────"
report_line "Total requests:" "$total"
report_line "Unique IPs:" "$unique_ips"
report_line "Total bytes:" "$human_bytes"
report ""
}
Step 4 — Status Code Breakdown¶
print_status_codes() {
local total
total=$(wc -l < "$LOGFILE")
report "── Status Codes ─────────────────────────"
awk '{print $9}' "$LOGFILE" | sort | uniq -c | sort -rn |
while read -r count code; do
local pct
pct=$(awk -v c="$count" -v t="$total" 'BEGIN { printf "%.1f%%", c/t*100 }')
printf " %-6s %7d (%s)\n" "$code" "$count" "$pct" >&3
done
report ""
}
sort | uniq -c | sort -rn is the standard shell frequency analysis pattern
sort— groups identical lines together (required foruniq)uniq -c— prefixes each group with its countsort -rn— sort by count, descending, numerically
Step 5 — Top N IPs¶
print_top_ips() {
report "── Top ${TOP_N} IPs ─────────────────────────"
awk '{print $1}' "$LOGFILE" |
sort | uniq -c | sort -rn | head -"$TOP_N" |
while read -r count ip; do
printf " %-20s %7d requests\n" "$ip" "$count" >&3
done
report ""
}
Step 6 — Top Requested Paths¶
print_top_paths() {
report "── Top ${TOP_N} Requested Paths ────────────"
awk '{print $7}' "$LOGFILE" |
sort | uniq -c | sort -rn | head -"$TOP_N" |
while read -r count path; do
printf " %-35s %7d requests\n" "$path" "$count" >&3
done
report ""
}
Step 7 — Error Listing¶
print_errors() {
local error_count
error_count=$(awk '$9 ~ /^5/' "$LOGFILE" | wc -l)
[[ "$error_count" -eq 0 ]] && return
report "── Server Errors (5xx) — ${error_count} total ────────"
awk '$9 ~ /^5/ {
gsub(/[\[\]]/, "", $4)
printf " [%s] %s \"%s %s\" %s\n", $4, $1, $6, $7, $9
}' "$LOGFILE" | head -20 >&3
(( error_count > 20 )) && report " ... and $(( error_count - 20 )) more"
report ""
}
Step 8 — Wire Everything in main()¶
main() {
local filename
filename="$(basename "$LOGFILE")"
local line_count
line_count=$(wc -l < "$LOGFILE")
report "=== Log Analysis Report ==="
report "File: $LOGFILE"
report "Lines: $(printf "%'d" "$line_count")"
report "Analyzed: $(date '+%Y-%m-%d %H:%M:%S')"
report ""
print_traffic_summary
print_status_codes
print_top_ips
print_top_paths
print_errors
report "=== End of Report ==="
[[ -n "$OUTPUT" ]] && echo "Report written to: $OUTPUT"
}
[[ "${BASH_SOURCE[0]}" == "$0" ]] && main "$@"
Why read the log multiple times
Each awk pass reads the log sequentially. For most log files (< 500 MB) this is fast enough. If the file is very large, consider sorting and preprocessing once into a temp file, then running all analysis passes against that.