set-up · caddy · json access log
Caddy log analytics from Caddy's JSON access log
Caddy log analytics starts with one log directive in your Caddyfile. Skald reads Caddy's default JSON records as they are, and since they hold the request headers, a Next.js site needs nothing more.
Caddy log analytics is reading the JSON access log Caddy writes when a site block has a log directive, and turning it into visits, sources and pages. Skald reads the format as caddy_json, and its independent audit recounts it too.
30-day free trial · no card · we set it up within one working day · free to self-host
example.com { # ... your site as it is ... log { output file /var/log/caddy/access.log } }
One log directive in the site's block. Copy it, reload Caddy, and every request is written as one JSON record.
[01] one record
What Skald reads from one Caddy JSON record
Caddy writes one JSON object per request. Skald reads eight of its fields and leaves the rest. Click a field to see which, and why.
/var/log/caddy/access.log · one sample record
click a fieldheaders.Next-Router-Prefetch: a click or a prefetch
What Caddy writes
Sent by Next.js when it fetches a link ahead of a click. Absent here: this was a real visit.
What Skald does with it
Caddy logs it with the other headers, so a click inside a Next.js site is a page view and a prefetch never is. Nothing to add.
this record → people · source ChatGPT · click-through
[02] set it up
Ship a Caddy access log to Skald
The one command finds nginx and Apache logs by itself. On Caddy, you name the file once. Everything after that is the same.
- Add the log directive to the site's block, as at the top of this page, and reload Caddy.
- Copy the one command from Settings > Connections in Skald. Its code works once, for 24 hours.
- Run it with
--logand the file. It runs the shipper with Docker if Docker works, or else with systemd and Python 3.9 or newer. - Let group adm read the file. The check prints the fix if it cannot.
$ curl -fsSL https://<your Skald>/traffic-kit/<code> | sudo bash -s -- --log /var/log/caddy/access.log
Your Skald shows the real line, with your own code in it.
$ sudo skald-ship-check ✗ PROBLEM neither nginx nor Apache is running ✓ OK the access log /var/log/caddy/access.log is there and group adm may read it ✓ OK the access log is growing (written in the last 10 minutes) ✓ OK the send address answers (401 without a token is expected) ✓ OK skald-ship is running (0 restarts)
Sample output, shortened. The check looks for nginx or Apache, so on Caddy its first line says PROBLEM. The lines after it are the ones that count.
[03] behind a proxy
Caddy behind Cloudflare or a load balancer
When something sits in front of Caddy, the address that reaches Caddy is the proxy's. Two settings keep the visitor's.
Set Caddy's trusted proxies
Then Caddy writes the visitor as client_ip, and Skald takes it. Without them, client_ip is the proxy, and many visitors look like one address.
Countries come free behind Cloudflare
Caddy logs the Cf-Ipcountry header with the others, and Skald reads it. Without Cloudflare, the country comes from a GeoIP file on the Skald server when one is set, or says Not recorded.
Cache hits never reach Caddy
A page served from a CDN's cache is not in Caddy's log. On Cloudflare, the Skald Worker sends those too.
- 09:14:02 203.0.113.24 /pricing/ 200 GPTBot/1.2AI · training
- 09:14:08 198.51.100.7 /docs/setup/ 200 Referer chatgpt.compeople · from ChatGPT
- 09:14:09 198.51.100.7 /pricing/?_rsc=… Next-Router-Prefetch 1a prefetch: not a page
- 09:14:11 203.0.113.66 /blog/ 200 Googlebot/2.1search
- 09:14:14 192.0.2.14 /.env 404 zgrab/0.xbots · probe
Sample records, shortened to the fields that matter. Each one lands in exactly one of five groups.
[04] skald on the same server
Run Skald beside Caddy, under your site's own name
Run Skald in Docker on the Caddy server, and no shipper is needed: mount the log folder read-only and set the format to caddy_json.
Skald listens on 127.0.0.1:8080 by default. Caddy sends it Skald's own paths, /traffic/ and the /traffic-* routes, and log_skip keeps your own dashboard views out of the log. Then set SKALD_BASE_URL to your site's address.
@skald path_regexp ^/traffic($|/|-(auth|api|ingest|mcp|hooks|beacon|js)(/|$))
log_skip @skald
handle @skald {
reverse_proxy 127.0.0.1:8080 {
header_up X-Real-IP {remote_host}
}
}
-v /var/log/caddy:/logs:ro -e SKALD_LOG_FILE=access.log -e SKALD_LOG_FORMAT=caddy_json
Part of a docker run, not a whole command. The Docker page has the full one.
[05] honest limits
What Caddy log analytics with Skald does not do
What it does not do
- It does not find a Caddy log by itself. The one command reads nginx and Apache settings only, so name the file with
--log. - It does not turn on Caddy's logging. A site block with no log directive writes no access log, and Skald then has nothing to read.
- It does not change Caddy's time format. Skald reads
tsas Caddy's default number of seconds. - It does not know the visitor's address behind a proxy unless Caddy's trusted proxies are set.
- It has no country without Cloudflare's header or a GeoIP country file on the Skald server. Then the country says Not recorded.
- It sends lines written after the install. Older records in the file are not sent.
keep reading
More on Caddy log analytics
questions
Caddy log analytics FAQ
Straight answers, including what Skald does not do.
Does Caddy write an access log by default?
No. Caddy writes access logs only for a site block that has a log directive. Add log { output file /var/log/caddy/access.log } to the block and reload Caddy. The records are JSON, one per line, in Caddy's default format, which Skald reads as it is.
Does the one command find the Caddy log by itself?
No. It reads nginx and Apache settings only. On a Caddy server, run it with --log and the path of the file, after bash -s --. The rest is the same: Docker if Docker works, else a systemd service with Python 3.9 or newer.
Do I need to add anything for a Next.js site?
No. Caddy's JSON log holds the request headers, Next-Router-Prefetch among them, so Skald tells a click inside the site from a link prefetch with nothing to add. On nginx and Apache, that takes one extra field.
Which address does Skald use when Caddy sits behind a proxy?
Skald takes client_ip when the record has it, else remote_ip. Behind a proxy or a CDN, client_ip is the visitor only once Caddy's trusted proxies are set. Without them it is the proxy, and many visitors look like one address.
Where do the countries come from?
From the Cf-Ipcountry header when the site is behind Cloudflare, since Caddy logs the headers. Otherwise from a GeoIP country file on the Skald server, when one is set. With neither, the country says Not recorded.
Is Caddy's log counted twice like the others?
Yes. The independent audit reads Caddy's JSON log with its own parser, since Skald 2.8.0, and a day where the two counts disagree is withheld.
$ tail -f access.log | skald
See what AI does with your site. Free for 30 days.
No card. We set up hosted Skald for you within one working day, or you self-host it free. Every line of your log, sorted and added up.