Analyzing Apache logs: command line, GoAccess and AWStats
An Apache access log answers most day-to-day questions (what is broken, what is slow, who is hammering the server) with a few shell commands. For charts and regular reports, GoAccess is the current open-source choice; AWStats still works but its author has stopped releasing it. This page gives tested commands for the combined log format and shows what to do with the results.
Updated from 2000. Apache Today covered log-file analysis in its September 2000 logging series (see the 2000 archive). The tools of that time are mostly gone; the approach, counting lines and looking for patterns, is the same. If your log format is not yet set up, start with the logging guide.
Know your field positions
All commands below assume the standard combined format. Split on spaces, a line breaks into these awk fields:
| Field | Example | Meaning |
|---|---|---|
$1 | 203.0.113.45 | client address |
$3 | alice | authenticated user |
$4 | [03/Nov/2026:14:02:17 | date and time |
$6 | "GET | method (with the opening quote) |
$7 | /guides/logging.html | requested path |
$9 | 200 | status code |
$10 | 18342 | bytes |
Split on double quotes instead (awk -F'"') and $2 is the request line, $4 the referrer and $6 the user agent. Three things shift fields:
- Debian's
other_vhosts_access.logusesvhost_combined, which putshost:portfirst. Add one to every field number. - Extra fields at the end (such as a response time) do not move the others; use
$NFfor the last field. - Junk requests do. A TLS handshake sent to port 80, for example, logs as
"\x16\x03\x01" 400. Find such lines withawk '$9 !~ /^[0-9]{3}$/' access.log.
Rotated logs are often compressed. zcat -f reads plain and gzipped files alike, so sudo zcat -f /var/log/apache2/access.log* (RHEL: /var/log/httpd/access_log*) feeds a whole fortnight into any command below. The examples use access.log as the file name.
Everyday one-liners
Requests per status code. A sudden rise in 404, 403 or 5xx is the first thing to spot (the output shown here and below comes from a short test log):
$ awk '{print $9}' access.log | sort | uniq -c | sort -rn
5 404
3 200
2 502
1 304
The busiest client addresses:
awk '{print $1}' access.log | sort | uniq -c | sort -rn | head -20
Requests per hour, to see peaks and quiet times (the file must be in time order, so use one file at a time):
$ awk '{print substr($4, 2, 14)}' access.log | uniq -c
6 03/Nov/2026:14
4 03/Nov/2026:15
1 03/Nov/2026:16
Most common user agents and referring pages:
awk -F'"' '{print $6}' access.log | sort | uniq -c | sort -rn | head -20
awk -F'"' '$4 != "-" {print $4}' access.log | sort | uniq -c | sort -rn | head -20
Total traffic in megabytes (a hyphen in the bytes field counts as zero):
awk '{bytes += $10} END {printf "%.1f MB\n", bytes/1024/1024}' access.log
The slowest requests, if you added %{ms}T as the second-to-last field as shown in the logging guide:
awk '{print $(NF-1), $7}' access.log | sort -rn | head -20
404 and 5xx reports
The most-requested missing pages. Old URLs with many hits deserve a redirect (redirects guide):
awk '$9 == 404 {print $7}' access.log | sort | uniq -c | sort -rn | head -20
Broken links on other sites: 404s that arrived with a referrer. Each line tells you who links to a page that no longer exists:
$ awk -F'"' '$3 ~ / 404 / && $4 != "-" {print $4, "->", $2}' access.log
https://forum.example.net/thread/42 -> GET /old-page.html HTTP/1.1
Server errors with their time, so you can match them with the error log:
$ awk '$9 ~ /^5/ {print $4, $9, $7}' access.log
[03/Nov/2026:15:12:44 502 /app/api/orders
[03/Nov/2026:15:12:45 502 /app/api/orders
A 502 or 504 usually means the application behind a proxy failed or timed out; a 500 points to a script or .htaccess error. Troubleshooting errors explains each code.
Finding attackers and bad bots
Most hostile traffic is automated scanning for known weak spots. Count who probes for them:
$ grep -Ei '"[A-Z]+ [^"]*(wp-login\.php|xmlrpc\.php|/\.env|/\.git/|phpmyadmin)' access.log \
| awk '{print $1}' | sort | uniq -c | sort -rn | head
4 198.51.100.7
If you do not run WordPress or phpMyAdmin, these requests are harmless 404s, but the same addresses often try other things. Two more signals:
# addresses with 50 or more 404s
awk '$9 == 404 {n[$1]++} END {for (ip in n) if (n[ip] >= 50) print n[ip], ip}' access.log | sort -rn
# POST requests per address per minute (login and form floods)
awk '$6 == "\"POST" {print $1, substr($4, 2, 17)}' access.log | sort | uniq -c | sort -rn | head
Password guessing against Basic authentication shows in the error log as AH01617 (wrong password) and AH01618 (unknown user). Count the sources:
grep -E 'AH0161[78]' error.log | grep -oE '\[client [^]]+\]' \
| sed -E 's/\[client (.*):[0-9]+\]/\1/' | sort | uniq -c | sort -rn
Do not trust a user agent that says "Googlebot"; anyone can send it. Google's documented check is a reverse lookup followed by a forward lookup that must return the same address:
$ host 66.249.66.1
1.66.249.66.in-addr.arpa domain name pointer crawl-66-249-66-1.googlebot.com.
$ host crawl-66-249-66-1.googlebot.com
crawl-66-249-66-1.googlebot.com has address 66.249.66.1
The name must end in googlebot.com, google.com or googleusercontent.com. Google also publishes its crawler address ranges as JSON files for automated checks.
What to do with what you find: block single addresses with Require not ip (access control), let fail2ban or a front proxy handle repeat offenders (bots and floods), and if requests for odd .php files in upload folders return 200, follow the compromised-site checklist.
GoAccess
GoAccess is an open-source log analyzer that shows reports in the terminal or writes a single self-contained HTML page, optionally updated live. As of November 2026 the current release is 1.12 (16 September 2026); Debian 13 ships 1.9.3, which runs all the commands here.
sudo apt install goaccess # Debian, Ubuntu
sudo dnf install goaccess # Fedora; on RHEL, Alma and Rocky you may need the EPEL repository first
Interactive report in the terminal, and an HTML report of all logs including rotated ones:
sudo goaccess /var/log/apache2/access.log --log-format=COMBINED
sudo zcat -f /var/log/apache2/access.log* | goaccess - --log-format=COMBINED -o /var/www/reports/index.html
Use --log-format=VCOMBINED for Debian's other_vhosts_access.log. Useful options:
--anonymize-ipzeroes the last octet of IPv4 and the last 80 bits of IPv6 addresses in the report.--ignore-crawlersleaves out known bots,--crawlers-onlyshows only them.--exclude-ip 192.0.2.10leaves out your own monitoring or office address (ranges with a dash).--persist --db-path=/var/lib/goaccesssaves the parsed data (create the folder first); the next run with--restore --persistadds only new lines, so a daily job does not reparse everything.
A daily report from cron, built from yesterday's rotated file:
# /etc/cron.d/goaccess-report
30 6 * * * root zcat -f /var/log/apache2/access.log.1 | goaccess - --log-format=COMBINED --anonymize-ip -o /var/www/reports/index.html
For a live dashboard add --real-time-html. GoAccess then runs a WebSocket server on port 7890 that listens on all addresses (0.0.0.0) by default; use --addr=127.0.0.1 and publish it through your proxy, or open the port only to yourself. Reports reveal your visitors' paths and addresses, so put the reports folder behind a password or Require ip.
AWStats today
AWStats, the long-running Perl log analyzer, released version 8.0 on 26 August 2025. Its website states that this is the last version its author will release and that further maintenance releases can only come from the community; as of November 2026 none has appeared there. The site itself points users to Matomo, including its log analytics import, as an alternative. Debian 13 still ships AWStats 7.9.
If you keep using it, the setup is: copy the sample config (awstats.model.conf upstream, /etc/awstats/awstats.conf on Debian) to /etc/awstats/awstats.www.example.com.conf, set LogFile to your access log (a command ending in | also works, for example LogFile="gzip -cd /var/log/apache2/access.log.2.gz |"), set SiteDomain and HostAliases, and keep LogFormat=1, which means the combined format. Then update and write static pages:
sudo perl /usr/lib/cgi-bin/awstats.pl -config=www.example.com -update
sudo perl /usr/lib/cgi-bin/awstats.pl -config=www.example.com -output -staticlinks > /var/www/reports/awstats.html
That path is Debian's; elsewhere use wherever awstats.pl was installed. Static pages are safer than exposing awstats.pl as a CGI script on a public site. For a new setup, GoAccess is the simpler choice.
What logs cannot tell you
- People, not addresses. One address can be a whole office or mobile network behind NAT; one person can use several addresses.
- Cached views. Pages served from a CDN or browser cache never reach Apache. With a CDN in front, use its statistics as well, and log the real client address with mod_remoteip.
- Humans versus bots. User agents can lie. Treat bot figures as estimates.
Sources
- Log Files, Apache HTTP Server 2.4 documentation
- mod_log_config, Apache HTTP Server 2.4 documentation
- GoAccess manual page, goaccess.io
- Getting started, goaccess.io
- Download, goaccess.io
- Release notes, goaccess.io
- goaccess package in Debian 13 (trixie), packages.debian.org
- AWStats project website, awstats.sourceforge.io
- AWStats installation and setup, AWStats documentation
- AWStats configuration directives, AWStats documentation
- awstats package in Debian 13 (trixie), packages.debian.org
- Verifying Googlebot and other Google crawlers, Google Search Central