Apache logs explained: access_log, error_log and combined format

Updated November 2026

Apache httpd writes two kinds of log: the access log, one line per request (who asked for what, and what they got), and the error log, where the server reports problems. This page shows where they are, explains every field of the standard "combined" line, and covers custom formats, error levels, rotation and privacy for Apache 2.4.

Revisiting a 2000 series. Apache Today's "Logging with Apache: Understanding Your access_log" and its follow-ups on error logs, custom logs and analysis ran from August to September 2000 (see the 2000 archive). The format has hardly changed; the tools around it have. Examples were tested on Apache httpd 2.4 in November 2026.

Where the logs are

Default log locations
SystemAccess logError log
Debian, Ubuntu/var/log/apache2/access.log/var/log/apache2/error.log
RHEL, Alma, Rocky, Fedora/var/log/httpd/access_log/var/log/httpd/error_log
Built from source/usr/local/apache2/logs/access_log/usr/local/apache2/logs/error_log

Debian also writes other_vhosts_access.log for virtual hosts that have no log of their own. Virtual hosts often log to their own files, so check the real paths with sudo apachectl -S (it prints "Main ErrorLog") and grep -ri customlog /etc/apache2/ (or /etc/httpd/). Watch a log live with sudo tail -F /var/log/apache2/access.log.

The combined format, field by field

Almost every distribution logs in the "combined" format, defined like this:

LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\"" combined
CustomLog "logs/access_log" combined

A real line from it looks like this (the addresses are documentation examples):

203.0.113.45 - alice [03/Nov/2026:14:02:17 +0000] "GET /reports/q3.html?print=1 HTTP/1.1" 200 18342 "https://www.example.com/reports/" "Mozilla/5.0 (X11; Linux x86_64; rv:144.0) Gecko/20100101 Firefox/144.0"
The nine fields of a combined log line
Value in the exampleCodeWhat it means
203.0.113.45%hThe client address. It is an IP address because HostnameLookups is off by default. Behind a proxy or CDN it is the proxy's address unless you use mod_remoteip (then log %a).
-%lThe "identd" login name. Always a hyphen unless mod_ident is loaded and IdentityCheck On; nobody uses it today. A hyphen anywhere means "no value".
alice%uThe user name from HTTP authentication. On a 401 line it is whatever name was typed, even if the login failed.
[03/Nov/2026:14:02:17 +0000]%tWhen the request was received: day/month/year:hour:minute:second and the offset from UTC.
"GET /reports/q3.html?print=1 HTTP/1.1""%r"The request line: method, path with query string, and protocol. HTTP/2 requests show HTTP/2.0.
200%>sThe final status code. The > matters: plain %s logs the status of the original request when Apache redirected internally (for example to an error page).
18342%bSize of the response body in bytes, without headers. A hyphen means zero bytes, as on 304 Not Modified or HEAD responses.
"https://www.example.com/reports/""%{Referer}i"The page that linked to this one (the header's spelling comes from the HTTP standard). By default browsers send only the site's origin, such as https://other.example/, when the link is on another site.
"Mozilla/5.0 (X11; …) Firefox/144.0""%{User-Agent}i"What the client says it is. Free text that any client can fake.

The "common" format is the first seven fields only. One distribution difference: Debian and Ubuntu define combined with %O instead of %b, which counts bytes actually sent including headers (from mod_logio), so you never see a hyphen there. Quotes and backslashes inside fields are escaped as \" and \\, and control characters as \xhh.

Adding your own fields

Define a new format under its own name rather than editing combined, because log analyzers expect the standard. Useful additions are the time taken and the virtual host:

LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\" %{ms}T %v" combined_plus
# Debian, Ubuntu (on RHEL, Fedora use "logs/access_log")
CustomLog ${APACHE_LOG_DIR}/access.log combined_plus
Other useful format codes
CodeLogs
%D, %{ms}TTime to serve the request in microseconds, or milliseconds (%{UNIT}T needs 2.4.13+)
%vThe ServerName of the virtual host
%a, %{c}aClient address (after mod_remoteip), and the address of the actual TCP peer
%I, %OBytes received and sent, including headers (mod_logio)
%{Name}i, %{Name}oAny request or response header
%kNumber of keepalive requests on this connection
%LRequest log ID, matching the ID in the error log

To leave out noise such as a load balancer's health checks, mark those requests and log the rest:

SetEnvIf Request_URI "^/health$" dontlog
CustomLog ${APACHE_LOG_DIR}/access.log combined env=!dontlog

Put CustomLog and ErrorLog inside each <VirtualHost> to give each site its own files; see virtual hosts. With hundreds of sites, one shared log with %v uses fewer file descriptors.

The error log

An Apache 2.4 error log line names the module and the severity, the process and thread, the client, and an error code (AH followed by five digits) you can search for:

[Tue Nov 03 14:10:05.233171 2026] [authz_core:error] [pid 41721:tid 41760] [client 198.51.100.7:50412] AH01630: client denied by server configuration: /var/www/example.com/admin/

LogLevel sets how much is written. The default is warn, and each level includes the ones above it:

LogLevel values, most to least severe
LevelMeaning
emerg, alert, critThe server cannot work or needs action now
errorA request failed, for example "Premature end of script headers"
warnSomething odd but not fatal (default)
noticeNormal but significant events, such as start and restart; always written to a log file
info, debugDetails for troubleshooting
trace1 … trace8Very detailed tracing; trace7 and trace8 dump raw data

Raise the level for one module only, and only while you debug, because trace levels fill disks fast:

# follow mod_rewrite's decisions
LogLevel warn rewrite:trace3
# more detail on TLS handshakes
LogLevel warn ssl:info
# bring back "File does not exist" lines for 404s
LogLevel warn core:info

Use one of these lines at a time: a later LogLevel in the same context replaces the earlier one, so combine modules on one line, such as LogLevel warn rewrite:trace3 ssl:info. The module must be loaded, or configtest stops with "Cannot find module rewrite".

Since 2.4, missing-file messages for 404s are logged at info, so you will not see them at the default level. LogLevel also works inside a <Directory>, and ErrorLog syslog:local1 sends errors to syslog instead of a file. For reading common messages, see troubleshooting errors; for rewrite tracing, mod_rewrite.

Rotating logs

A busy site's access log grows by gigabytes. There are two ways to keep it in check.

logrotate (the distribution default)

Debian and Ubuntu ship /etc/logrotate.d/apache2: daily rotation, 14 old files kept, compression delayed by one day, then apache2 is reloaded so it reopens its files. RHEL and Fedora ship /etc/logrotate.d/httpd, which reloads httpd.service. For logs in another folder, add a file like this:

# /etc/logrotate.d/example-com
/var/www/example.com/logs/*.log {
    daily
    rotate 30
    maxage 30
    missingok
    notifempty
    compress
    delaycompress
    dateext
    create 640 root adm
    sharedscripts
    postrotate
        if systemctl is-active --quiet apache2; then systemctl reload apache2; fi
    endscript
}

On RHEL use httpd instead of apache2 and a group such as root. Dry-run it with sudo logrotate -d /etc/logrotate.d/example-com. Avoid copytruncate for Apache; its own man page warns that lines can be lost between the copy and the truncate.

rotatelogs (piped logging)

Apache can write through a pipe to its own rotatelogs program, which starts a new file on schedule without any reload:

CustomLog "|/usr/bin/rotatelogs -l -L /var/log/apache2/access.log /var/log/apache2/access.%Y-%m-%d.log 86400" combined
ErrorLog  "|/usr/bin/rotatelogs -l /var/log/apache2/error.%Y-%m-%d.log 86400"

86400 means every 24 hours, -l uses local time instead of UTC, -L keeps a hard link with a fixed name for tail -F, and a size such as 100M rotates by size instead. rotatelogs does not delete old files (unless you use -n to cycle a fixed number of them), so add a cron job (for example find /var/log/apache2 -name 'access.*.log' -mtime +30 -delete). It lives in /usr/bin on Debian; find it elsewhere with command -v rotatelogs. Disable the distribution's logrotate entry for files you rotate this way.

Privacy and safety

Access logs hold IP addresses, user names, and full URLs, which sometimes include e-mail addresses or tokens in query strings. Treat them as personal information:

Apache has no built-in masking code, but a piped logger can do it. This script zeroes the last part of IPv4 addresses and keeps only the first 48 bits of IPv6 addresses:

#!/bin/sh
# /usr/local/bin/apache-anon-log - mask client IPs, append to the file given as $1
exec /usr/bin/sed -u -E \
  -e 's/^([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})\.[0-9]{1,3} /\1.0 /' \
  -e 's/^([0-9a-fA-F]{1,4}:[0-9a-fA-F]{1,4}:[0-9a-fA-F]{1,4}):[0-9a-fA-F:]* /\1:: /' \
  -e 's/^([0-9a-fA-F]{1,4}:[0-9a-fA-F]{1,4})::[0-9a-fA-F:]* /\1:: /' \
  >> "$1"
sudo chmod 755 /usr/local/bin/apache-anon-log
# in the virtual host, instead of the normal CustomLog:
CustomLog "|/usr/local/bin/apache-anon-log /var/log/apache2/access.log" combined

A line then starts 203.0.113.0 - - [03/Nov/2026…. Apache restarts the script on every reload, so logrotate keeps working. Piped log programs run as the user that started Apache, usually root, so keep them this simple and writable only by root. If you only need anonymous reports, GoAccess can mask addresses at report time instead (see log analysis).

Protect the log folder. The Apache documentation warns that anyone who can write to the log directory can almost certainly gain the rights of the user that starts the server, normally root. Logs also contain text sent by clients, so view raw logs with less rather than printing them straight to a terminal.

Sources