Analyzing Apache logs: command line, GoAccess and AWStats

Updated November 2026

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:

awk fields of a combined log line
FieldExampleMeaning
$1203.0.113.45client address
$3aliceauthenticated user
$4[03/Nov/2026:14:02:17date and time
$6"GETmethod (with the opening quote)
$7/guides/logging.htmlrequested path
$9200status code
$1018342bytes

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:

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:

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

Sources