Logs¶
Every request handled by the webserver is logged for you, so you can debug issues and see who accessed your site.
Access logs¶
Privacy
To protect user privacy, we log only the first 16 bits of an IPv4 address and the first 32 bits of an IPv6 address, while the rest are set to zero.
Using the IP addresses of uberspace.de as an example, 82.98.87.93 and 2a02:2e0:3fc:52:0:62:5768:38 are converted to 82.98.0.0 and 2a02:2e0::, respectively, in the actual log files.
You can find your webserver's access log at ~/logs/caddy, which resolves to /var/log/caddy-userlogs/$USER.
[isabell@moondust ~]$ tail --follow=name ~/logs/caddy/access.log
{"level":"info","ts":"2026-07-11T11:02:18.428+0200","logger":"http.log.access.[...]","msg":"handled request","request":{"client_ip":"192.0.2.100","proto":"HTTP/2.0","method":"GET","host":"isabell.uber.space","uri":"/robots.txt","headers":{"User-Agent":["[...]"]}},"bytes_read":0,"user_id":"","duration":0.002604887,"size":512,"status":200}
Access logging is always on
There is currently no way to disable this log.
Each line is a JSON object. The most relevant fields are:
{
"ts": "2026-07-11T11:02:18.428+0200",
"request": {
"client_ip": "192.0.2.100",
"proto": "HTTP/2.0",
"method": "GET",
"host": "isabell.uber.space",
"uri": "/robots.txt"
},
"status": 200,
"size": 512,
"duration": 0.002604887
}
You can use jq to filter and format the log, for example to only show the timestamp, method, path and status code:
[isabell@moondust ~]$ tail --follow=name ~/logs/caddy/access.log | jq --unbuffered --raw-output '[.ts, .request.method, .request.uri, .status] | @tsv'
2026-07-11T11:02:18.428+0200 GET /robots.txt 200
Apache-style logs¶
Some tools expect the Apache combined log format. You can use jq to convert the live output:
[isabell@moondust ~]$ tail --follow=name ~/logs/caddy/access.log | jq --unbuffered --raw-output '
def apache_time:
capture("^(?<date>[0-9-]+T[0-9:]+)(?:\\.[0-9]+)?(?<zone>[+-][0-9]{4})$")
| ((.date + "Z" | fromdateiso8601 | strftime("%d/%b/%Y:%H:%M:%S")) + " " + .zone);
[
.request.client_ip,
"-",
((.user_id | select(length > 0)) // "-"),
("[" + (.ts | apache_time) + "]"),
("\"" + .request.method + " " + .request.uri + " " + .request.proto + "\""),
(.status | tostring),
(.size | if . == 0 then "-" else tostring end),
("\"" + (.request.headers.Referer[0] // "-") + "\""),
("\"" + (.request.headers["User-Agent"][0] // "-") + "\"")
]
| join(" ")'
192.0.2.100 - - [11/Jul/2026:11:02:18 +0200] "GET /robots.txt HTTP/2.0" 200 512 "-" "[...]"
This only reformats the command's output. The original log remains unchanged in structured JSON and can still be filtered and parsed directly.