Tools
The local log, stat and game 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, the server-health statistics, and the game-telemetry log tree, and the client pulls them incrementally into your content root.
GET /api/v1/logs
GET /api/v1/logs/file
GET /api/v1/stats
GET /api/v1/stats/file
GET /api/v1/stats/now
GET /api/v1/gamelogs
GET /api/v1/gamelogs/file
Private API
Every call is authenticated with a bearer token — the
gamehoster-config-apiToken from your local tool
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), exact validated name (stats) or validated tree-relative path
(game logs) only, so a request can never escape the served directory and there is no path-traversal surface.
| Endpoint | Purpose |
|---|---|
GET /api/v1/logs | List the access-log files: the active log's size and each rotated file's timestamp. |
GET /api/v1/logs/file | Stream one access-log file; a Range fetches only the newly-appended tail. |
GET /api/v1/stats | List the server-health day-files and the sampling interval. |
GET /api/v1/stats/file | Stream one stat day-file by name; a Range fetches only the growing tail. |
GET /api/v1/stats/now | A single live server-health snapshot, for debugging. |
GET /api/v1/gamelogs | List the game-telemetry log tree: every stats/snapshot file by its tree-relative path and size. |
GET /api/v1/gamelogs/file | Stream one game-log file by path; a Range fetches only the growing tail. |
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
| Method | GET |
|---|---|
| Path | /api/v1/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 client names it (access-<ts>.log) and de-duplicates it. |
rotated[].size | integer | Its 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-
| 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 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/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
| Method | GET |
|---|---|
| Path | /api/v1/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/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-
| 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/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
| Method | GET |
|---|---|
| Path | /api/v1/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 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:
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. The
Game Logs browser mirrors the game-telemetry tree the same
way, then builds its overview, per-game and per-instance pages from it.
GET /api/v1/gamelogs
Lists the game-telemetry log tree so the client can mirror
it. Every stats and snapshot file is reported by its tree-relative path
(<domain>/<game>/gamehoster-processes/<process>/<file>) and size; a past
hour is frozen and mirrored once, the current hour's files grow. The path is the only addressing, and the server
validates it against the served root, so a request can never escape the tree.
Request
| Method | GET |
|---|---|
| Path | /api/v1/gamelogs |
| Auth | Authorization: Bearer <token> |
| Parameters | None. |
Response
A 200 JSON object:
{
"files": [
{ "path": "game.asteroids.net/asteroids/gamehoster-processes/3f9c1a2b7d4e/stats-2026-07-30-18.jsonl", "size": 48210 },
{ "path": "game.asteroids.net/asteroids/gamehoster-processes/3f9c1a2b7d4e/snapshot-2026-07-30-18.jsonl", "size": 9044 }
]
}
| Field | Type | Meaning |
|---|---|---|
files | array | One entry per log file across every game and process, sorted by path. |
files[].path | string | The tree-relative path to request from /gamelogs/file; also where the client writes it locally. |
files[].size | integer | Its size in bytes; a size change means the client re-fetches the tail. |
GET /api/v1/gamelogs/file
Streams the bytes of one game-log file, addressed by its tree-relative path. A Range
header is honoured so the client fetches only the tail appended since last time; a frozen past-hour file is
fetched once, whole.
Request
GET /api/v1/gamelogs/file?path=game.asteroids.net/asteroids/gamehoster-processes/3f9c1a2b7d4e/stats-2026-07-30-18.jsonl HTTP/1.1
Authorization: Bearer <token>
Range: bytes=48210-
| Parameter | In | Meaning |
|---|---|---|
path | query | The exact tree-relative path from /gamelogs. Validated against the served root; a path that escapes it is rejected. |
Range | header | Optional. 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 file bytes — JSON Lines, one object per line —
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.