Skip to content

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.