Logging
The Logging page lists your websites, with two live views for each:
| View | Contains |
|---|---|
| Access Log | One record per request, including those the edge answers itself, e.g. one stopped by a rate limit. |
| Error Log | What went wrong answering those requests: an application that failed, a permission problem, a broken rewrite rule. |
A view streams the log into a read-only terminal in your browser and follows new records as they arrive, one line per record in the classic web server format, with times in UTC.
The log files
The live view has no search box and no download button. For anything more, work with the files in a terminal or over SSH.
They're in your account's log directory, /home/<account>/log, named
after the website: access_log+www.example.com and
error_log+www.example.com.
Your home directory is /home/<account>/home, and the log directory sits
next to it, so ~/log doesn't exist. In a shell, "${HOME%/*}/log" is
the log directory. The file manager shows only
your home directory, so it can't open the log files.
The examples on this page start in the log directory:
cd "${HOME%/*}/log"
Every day, the server moves old records to the archive directory as
zstd-compressed files and keeps them for 15 days. Read them with
zstdcat, e.g. zstdcat archive/access_log+www.example.com+* | weblogconv.
The record format
Both files are JSON Lines: each line is one JSON object, one record. The keys follow the OpenTelemetry semantic conventions where a name exists. A key without a value is left out. All times are UTC.
An access record (one line in the file):
{"timestamp":"2026-10-18T07:03:12.481Z","otel.event.name":"elpan.http.access","http.request.method":"POST","url.scheme":"https","url.path":"/wp-login.php","server.address":"www.example.com","server.port":443,"client.address":"203.0.113.7","network.protocol.name":"http","network.protocol.version":"2","http.response.status_code":429,"http.request.size":918,"http.response.size":1834,"http.response.body.size":1650,"user_agent.original":"Mozilla/5.0 (X11; Linux x86_64)","elpan.request.id":"6f1c0e3a9b2d4c5e8f7a1b2c3d4e5f60","elpan.request.duration":0.0,"elpan.answered_by":"edge","elpan.limit_req.status":"REJECTED"}
The access record keys you'll use most:
| Key | Value |
|---|---|
timestamp | When the request started, in UTC, with milliseconds. |
otel.event.name | elpan.http.access for an access record. |
http.request.method | The method, e.g. GET. An unusual method is _OTHER, with the original in http.request.method_original. |
url.path, url.query | Path and query string as the client sent them. The query has no ?. |
server.address | The hostname the client asked for. For a website with wildcard subdomains, the subdomain's name. |
client.address | The client's IP address. |
network.protocol.version | 1.0, 1.1, or 2. |
http.response.status_code | The status code, as a number. |
http.response.body.size | The body size the client got, in bytes. |
user_agent.original | The client's User-Agent header. |
http.request.header.referer | The Referer header, as a one-item list. |
user.name | The user your application named. See Log your application's users. |
elpan.auth.user | The user the web server's authentication accepted, e.g. with access control. |
elpan.request.id | The request ID. See Follow a request to its errors. |
elpan.request.duration | Seconds the server took to answer. |
elpan.answered_by | origin if the request reached your website's web server, edge if the edge answered it. |
elpan.limit_req.status, elpan.limit_conn.status | Only when a rate or connection limit acted on the request, e.g. REJECTED. |
The error record keys:
| Key | Value |
|---|---|
timestamp | The time of the error, in UTC, with microseconds. |
otel.event.name | elpan.http.error for an error record. |
severity_text | The web server's level: error, warn, notice, and the others. |
severity_number | The same level as an OpenTelemetry number: error is 17, warn is 13. |
body | The full message. A message from PHP starts with AH01071: Got error '. |
client.address | The client's IP address. |
elpan.request.id | The ID of the request that caused the error. |
elpan.httpd.module | The part of the web server that wrote the message, e.g. proxy_fcgi. |
elpan.httpd.message_id | The code at the start of the message, e.g. AH01071. |
A program that reads the records must ignore keys it doesn't know; a later version can add new ones.
Search the files
Use jq, which is on every server:
# The last 20 requests that failed with a status of 500 or more
jq -c 'select(."http.response.status_code" >= 500)' access_log+www.example.com | tail -20
# The number of requests for each status code
jq -r '."http.response.status_code"' access_log+www.example.com | sort | uniq -c | sort -rn
# The requests that a rate limit stopped
jq -c 'select(."elpan.limit_req.status" == "REJECTED")' access_log+www.example.com
# The messages of the error log since 18 October 2026 (UTC)
jq -r 'select(.timestamp >= "2026-10-18") | .body' error_log+www.example.com
On the day the server switched to JSON records, a file can also hold some
lines in the old text format, and jq stops at such a line. Add -R and
fromjson? to skip them:
jq -cR 'fromjson? | select(."http.response.status_code" >= 500)' access_log+www.example.com.
Requests that the edge answers
The edge receives requests from the internet and passes them to your website's web server. The ones it answers itself are in your access log too, with these status codes:
| Status | What happened |
|---|---|
| 444 | The edge closed the connection without an answer: the request looked like a scan for a known weakness, e.g. /.git/config. |
| 403 on a gated path | The edge sent the bot challenge page: the visitor had no valid pass yet. |
| 429 | One client sent too many requests or opened too many connections in a short time. elpan.limit_req.status or elpan.limit_conn.status says which limit acted. |
| 499 | The client closed the connection before the answer was ready. |
| 502 | Your website's web server didn't answer. The visitor got the edge's error page. |
| 504 | Your website's web server didn't answer in time. |
200 for /.well-known/acme-challenge/ | The edge answered a validation request for a certificate. |
A client the server blocks for abuse is stopped before the edge, so its requests are in no log.
The access log describes the request as the client saw it: the protocol version is the client's, and the sizes are what the client sent and got.
Follow a request to its errors
Each request gets one ID, elpan.request.id, of 32 hexadecimal digits,
shared by its access record and every error record it caused. The same ID
also appears:
- on the error pages the server shows to visitors, so a visitor can read it to you;
- in your application, as the
UNIQUE_IDenvironment variable and theX-Request-IDrequest header ($_SERVER['UNIQUE_ID']and$_SERVER['HTTP_X_REQUEST_ID']in PHP).
To find the errors of the last request that failed with a 500:
id=$(jq -r 'select(."http.response.status_code" == 500) | ."elpan.request.id"' access_log+www.example.com | tail -1)
jq -r --arg id "$id" 'select(."elpan.request.id" == $id) | .body' error_log+www.example.com
Convert the records to text: weblogconv
weblogconv turns the records back into the classic text formats, for
tools that read the Combined Log Format, e.g. GoAccess and AWStats, and
for your own scripts that read the old format. man weblogconv has the
details.
weblogconv access_log+www.example.com | tail -20
tail -f access_log+www.example.com | weblogconv
weblogconv --tz Asia/Jakarta error_log+www.example.com
| Option | Effect |
|---|---|
no --format | An access record becomes combined, an error record text. |
--format combined | The Combined Log Format. |
--format common | The Common Log Format: combined without referer and user agent. |
--format elpan | The access log's format before the switch to JSON. |
--format text | An error line in the Apache web server's layout. |
--user auth, --user app, --user either | Which user goes into the user field: elpan.auth.user, user.name, or the first of the two with a value (the default). |
--tz <zone> | Times in that time zone, e.g. Asia/Jakarta, instead of UTC. |
A line that isn't a record passes through unchanged. Every value is escaped, so a value from a visitor can't change a line's fields or send control characters to your terminal.
What is somewhere else
This page covers websites only. Other activity has its own, more structured views:
- Mail. Delivery history and sign-in activity are on the traffic log pages for email.
- App servers and containers. Each has a Logs button on its own page that streams the program's output. See App Servers and Containers.