Tools
The local log and stat views are built from data the local client mirrors down from the server over a token-protected API on the server's IP. Like the sync, but read-only: these endpoints serve the Caddy access logs and the server-health statistics, and the local client pulls them incrementally into your content root. The Data Logs browser is built instead from the columnar data files the sync mirrors down, read back through the storage engine, not from these endpoints.
GET /api/v3/logs
GET /api/v3/logs/file
GET /api/v3/stats
GET /api/v3/stats/file
GET /api/v3/stats/now
Private API
Every call is authenticated with the same bearer token as the sync — the
datahoster-config-apiToken from your local client
config, sent as Authorization: Bearer <token>. These
routes are read-only and identical across every hoster (they live in the shared
common/server/logs-stats.js). As with the
sync, without a valid token every reply — a bad token or an
unknown route — is an identical 404 wall; with a valid token a read returns
200, and an authenticated request on a different API version's path answers 426.
The routes are versioned under /api/v3/, pinned to the local client's own
shared/version.js version. Files are exposed by
timestamp (logs) or exact validated name (stats) only, so no filename ever crosses the
wire and there is no path-traversal surface.
| Endpoint | Purpose |
|---|---|
GET /api/v3/logs | List the access-log files: the active log's size and each rotated file's timestamp. |
GET /api/v3/logs/file | Stream one access-log file; a Range fetches only the newly-appended tail. |
GET /api/v3/stats | List the server-health day-files and the sampling interval. |
GET /api/v3/stats/file | Stream one stat day-file by name; a Range fetches only the growing tail. |
GET /api/v3/stats/now | A single live server-health snapshot, for debugging. |
GET /api/v3/logs
Lists the Caddy access logs so the local client can mirror them. The active log is reported by size only; each rotated file is keyed by its rotation timestamp, never its name — so the server can't influence where the local client writes, and a newly-appeared timestamp is how the local client detects that the active log rotated.
Request
| Method | GET |
|---|---|
| Path | /api/v3/logs |
| Auth | Authorization: Bearer <token> |
| Parameters | None. |
Response
A 200 JSON object:
{
"active": { "size": 12345 },
"rotated": [
{ "ts": 1784370489663, "size": 67108864 },
{ "ts": 1784514159284, "size": 41943040 }
]
}
| Field | Type | Meaning |
|---|---|---|
active | object | The live log as { size } in bytes, or null if there is none. |
rotated | array | One entry per rotated file, sorted oldest-first. |
rotated[].ts | integer | The file's rotation time in milliseconds — how the local client names it (access-<ts>.log) and de-duplicates it. |
rotated[].size | integer | Its size in bytes; a size change means the local client must re-fetch it. |
GET /api/v3/logs/file
Streams the bytes of one access-log file. A Range header is honoured so the local client fetches
only the tail appended since last time; an immutable rotated file is fetched once, whole. See
pull-logs.js.
Request
GET /api/v3/logs/file?which=active
Authorization: Bearer <token>
Range: bytes=12345-
| Parameter | In | Meaning |
|---|---|---|
which | query | active for the live log, or a rotated file's timestamp (the ts from /logs). |
Range | header | Optional. bytes=N- fetches only from byte N — the local client passes its local size to append just the new tail. |
Plus Authorization: Bearer <token>.
Response
The raw log bytes — Caddy's JSON access lines, one per request —
Content-Type: application/octet-stream with Accept-Ranges: bytes.
206 Partial Contentwith aContent-Rangeheader when aRangewas sent;200otherwise.- An absent file, or a
Rangeat or past the end, returns an empty200body — never an error.
GET /api/v3/stats
Lists the server-health day-files and the sampling
interval, so the local client can mirror them. Files are named <type>-YYYY-MM-DD.log; a
past day is frozen and mirrored once, today's file grows.
Request
| Method | GET |
|---|---|
| Path | /api/v3/stats |
| Auth | Authorization: Bearer <token> |
| Parameters | None. |
Response
A 200 JSON object:
{
"interval": 15000,
"files": [
{ "name": "cpu-2026-07-20.log", "type": "cpu", "date": "2026-07-20", "size": 82944 },
{ "name": "memory-2026-07-20.log", "type": "memory", "date": "2026-07-20", "size": 61200 }
]
}
| Field | Type | Meaning |
|---|---|---|
interval | integer | Milliseconds between samples (15000 = every 15 seconds). |
files | array | One entry per day-file, sorted by name. |
files[].name | string | The exact filename to request from /stats/file. |
files[].type | string | The statistic type: cpu, memory, disk, diskio or netio. |
files[].date | string | The file's UTC date, YYYY-MM-DD. |
files[].size | integer | Its size in bytes. |
GET /api/v3/stats/file
Streams one stat day-file by its exact name, which is validated against
<type>-YYYY-MM-DD.log — the only thing that reaches the filesystem, so there is no
path-traversal surface. A Range fetches only the growing tail of the current day. See
pull-stats.js.
Request
GET /api/v3/stats/file?name=cpu-2026-07-20.log
Authorization: Bearer <token>
Range: bytes=61200-
| Parameter | In | Meaning |
|---|---|---|
name | query | The exact day-file name from /stats, e.g. cpu-2026-07-20.log. |
Range | header | Optional. bytes=N- to fetch only from byte N. |
Plus Authorization: Bearer <token>.
Response
The raw bytes of the day-file — one JSON sample per line —
Content-Type: application/octet-stream with Accept-Ranges: bytes.
206 Partial Contentwith aContent-Rangewhen aRangewas sent;200otherwise.- An unknown or absent name, or a
Rangeat or past the end, returns an empty200body.
GET /api/v3/stats/now
Returns a single live health snapshot for debugging — the same shape the recorder samples on its
interval, but taken on demand and never stored. Counter fields (CPU core times, disk/network bytes) are
cumulative; a consumer diffs two snapshots to get a rate. Metrics a host can't report (e.g.
disk/network IO off Linux) are null rather than an error. Built by
shared/stats.js.
Request
| Method | GET |
|---|---|
| Path | /api/v3/stats/now |
| Auth | Authorization: Bearer <token> |
| Parameters | None. |
Response
A 200 JSON snapshot (values below are illustrative):
{
"ts": 1784514159,
"uptime": 864000,
"hostname": "vps-1",
"load": [0.12, 0.09, 0.05],
"cpu": { "model": "…", "cores": [ { "user": 0, "nice": 0, "sys": 0, "idle": 0, "irq": 0 } ] },
"memory": { "total": 0, "used": 0, "available": 0, "free": 0 },
"disk": { "path": "/", "total": 0, "used": 0, "free": 0 },
"diskIo": { "readBytes": 0, "writeBytes": 0 },
"netIo": { "rxBytes": 0, "txBytes": 0 }
}
| Field | Type | Meaning |
|---|---|---|
ts | integer | The sample's server Unix time, in seconds. |
uptime | integer | Seconds since boot. |
hostname | string | The machine's hostname. |
load | array | The 1, 5 and 15-minute load averages. |
cpu | object | model plus cores: per-core cumulative times (user, nice, sys, idle, irq). |
memory | object | total / used / available / free bytes. |
disk | object | Usage of the filesystem holding / in bytes, or null. |
diskIo | object | Cumulative readBytes / writeBytes, or null. |
netIo | object | Cumulative rxBytes / txBytes, or null. |
Mirroring Process
The local client mirrors both sources the same way, statelessly, deriving everything from what it already has
on disk — the counterpart of how the sync mirrors the data files.
It uses the same version-pinned /api/v<N> prefix as the
sync, then:
List
/logs or
/stats returns what the server holds
— sizes and timestamps (logs), or day-files and the interval (stats).
Fetch what's frozen
An immutable file — a rotated log, or a past day-file — is downloaded once, whole, if it is missing locally or its size differs.
Append the tail
The growing file — the active log, or today's day-file — is fetched with a Range from
the local size, appending only the new bytes. A rotated log appearing resets the active log and
re-fetches it.
The download itself lives in the shared toolkit
(common/browse/pull-logs.js and
common/browse/pull-stats.js),
so every hoster mirrors the same way. The mirrored files then feed the static
Web Logs and
Server Logs browsers. Datahoster's own
Data Logs browser is built separately, from the
columnar files the sync mirrors down
(_data/<host>/datahoster-data/…), read back through the storage engine
(shared/store.js) as a read-only loader, not from
this API.