Tools

The local log and stat views are built from data the 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 client pulls them incrementally into your content root. The third tool, Game Logs, is built from your local game definitions and uses no API.

GET /api/v1/logs
GET /api/v1/logs/file
GET /api/v1/stats
GET /api/v1/stats/file
GET /api/v1/stats/now
The read-only tools API: list and stream the access-log and server-health files.

Private API

Every call is authenticated with a bearer token — the gamehoster-config-apiToken from your client configuration, sent as Authorization: Bearer <token>. These routes are read-only and identical across every hoster (they live in common/server/logs-stats.js). As with the sync, a correctly authenticated call returns 200 and anything else — bad token, unknown route, wrong version — returns an identical 404; the routes are versioned under /api/v1/, discovered from /api/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.

EndpointPurpose
GET /api/v1/logsList the access-log files: the active log's size and each rotated file's timestamp.
GET /api/v1/logs/fileStream one access-log file; a Range fetches only the newly-appended tail.
GET /api/v1/statsList the server-health day-files and the sampling interval.
GET /api/v1/stats/fileStream one stat day-file by name; a Range fetches only the growing tail.
GET /api/v1/stats/nowA single live server-health snapshot, for debugging.

GET /api/v1/logs

Lists the Caddy access logs so the 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 client writes, and a newly-appeared timestamp is how the client detects that the active log rotated.

Request

MethodGET
Path/api/v1/logs
AuthAuthorization: Bearer <token>
ParametersNone.

Response

A 200 JSON object:

{
  "active": { "size": 12345 },
  "rotated": [
    { "ts": 1784370489663, "size": 67108864 },
    { "ts": 1784514159284, "size": 41943040 }
  ]
}
FieldTypeMeaning
activeobjectThe live log as { size } in bytes, or null if there is none.
rotatedarrayOne entry per rotated file, sorted oldest-first.
rotated[].tsintegerThe file's rotation time in milliseconds — how the client names it (access-<ts>.log) and de-duplicates it.
rotated[].sizeintegerIts size in bytes; a size change means the client must re-fetch it.

GET /api/v1/logs/file

Streams the bytes of one access-log file. A Range header is honoured so the client fetches only the tail appended since last time; an immutable rotated file is fetched once, whole. See pull-logs.js.

Request

GET /api/v1/logs/file?which=active HTTP/1.1
Authorization: Bearer <token>
Range: bytes=12345-
Fetch only the tail of the active log appended since last time.
ParameterInMeaning
whichqueryactive for the live log, or a rotated file's timestamp (the ts from /logs).
RangeheaderOptional. bytes=N- fetches only from byte N — the 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 Content with a Content-Range header when a Range was sent; 200 otherwise.
  • An absent file, or a Range at or past the end, returns an empty 200 body — never an error.

GET /api/v1/stats

Lists the server-health day-files and the sampling interval, so the 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

MethodGET
Path/api/v1/stats
AuthAuthorization: Bearer <token>
ParametersNone.

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 }
  ]
}
FieldTypeMeaning
intervalintegerMilliseconds between samples (15000 = every 15 seconds).
filesarrayOne entry per day-file, sorted by name.
files[].namestringThe exact filename to request from /stats/file.
files[].typestringThe statistic type: cpu, memory, disk, diskio or netio.
files[].datestringThe file's UTC date, YYYY-MM-DD.
files[].sizeintegerIts size in bytes.

GET /api/v1/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/v1/stats/file?name=cpu-2026-07-20.log HTTP/1.1
Authorization: Bearer <token>
Range: bytes=61200-
Fetch only today's newly-appended samples.
ParameterInMeaning
namequeryThe exact day-file name from /stats, e.g. cpu-2026-07-20.log.
RangeheaderOptional. 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 Content with a Content-Range when a Range was sent; 200 otherwise.
  • An unknown or absent name, or a Range at or past the end, returns an empty 200 body.

GET /api/v1/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

MethodGET
Path/api/v1/stats/now
AuthAuthorization: Bearer <token>
ParametersNone.

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 }
}
FieldTypeMeaning
tsintegerThe sample's server Unix time, in seconds.
uptimeintegerSeconds since boot.
hostnamestringThe machine's hostname.
loadarrayThe 1, 5 and 15-minute load averages.
cpuobjectmodel plus cores: per-core cumulative times (user, nice, sys, idle, irq).
memoryobjecttotal / used / available / free bytes.
diskobjectUsage of the filesystem holding / in bytes, or null.
diskIoobjectCumulative readBytes / writeBytes, or null.
netIoobjectCumulative rxBytes / txBytes, or null.

Mirroring Process

The client mirrors both sources the same way, statelessly, deriving everything from what it already has on disk — the counterpart of how the sync diffs hashes. It negotiates the version from /api/version, then:

1

List

/logs or /stats returns what the server holds — sizes and timestamps (logs), or day-files and the interval (stats).

2

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.

3

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. The Game Logs browser is built from your local game definitions and touches no API at all.