Projects / Observability & Telemetry

Multi-Protocol Player Telemetry

A dependency-free Python collector that asks 12 game servers how many people are playing, over four binary protocols and four JSON APIs. It publishes counts-only JSON for a public page, richer detail for internal dashboards and Prometheus metrics for 90 days of history.

The problem

I host game servers for friends: Phantasy Star Online: Blue Burst on three instances, four Minecraft servers, four classic arena shooters, Palworld and a card game. The first question anyone asks is “is anyone on?”, and the honest answer used to be “let me log in and look”.

Uptime monitoring didn’t answer it. A port check says a server is up, not whether anyone is playing, and it doesn’t separate players from bots. I wanted one place that answers the question for every game, publicly and safely, and remembers the answer so I can see when people actually play.

Constraints

  • Speak each game’s own language. There’s no common protocol: some games have JSON APIs, others only answer a 1990s-era UDP query, and one compresses its replies with a fixed Huffman tree.
  • Publish counts, never people. The public page must not reveal player names, IPs or anything about the server.
  • No new attack surface. The collector shouldn’t need database credentials or write access to any game, and nothing it serves internally should be reachable from the internet.
  • Survive any one game misbehaving. A crashed or slow server must not stall or break the rest.

Architecture

One small Python process (standard library only: socket, struct, urllib, json) runs a probe for every server every 30 seconds and writes three files. A separate nginx container serves them, and only one is routed publicly.

Output Contents Who can read it
players.json Per game: state, players, slots. Counts only. Public status page, this site’s live panel, a join guide
details.json The above plus player names, current map, server build, accounts, uptime Internal network only (admin dashboard)
metrics.prom Prometheus gauges: players online, slots, open games, server up Internal network only, scraped by VictoriaMetrics

The split is enforced in code: probes return private data under a separate extra key, and the function that writes the public file strips it. I verified the public output carries none of it before shipping.

Talking to each game

Game How it’s asked
Minecraft (4 servers) The Server List Ping: a hand-built TCP handshake with VarInt framing, then a JSON status reply. The reply also reveals the modpack and loader, so dashboards show each server’s build with no configuration.
Quake III, OpenArena The UDP getstatus query. Bots report a ping of 0, so they’re excluded from the count.
Doom (Zandronum) Zandronum’s launcher protocol over UDP. Replies are Huffman-compressed with a fixed tree, so the collector rebuilds that tree from its published description and decodes the reply bit by bit.
Unreal Tournament 2004 The Unreal 2 query protocol over UDP. Replies can span several packets, and an empty server sends no player packet at all, which has to read as “0”, not “down”.
PSO: Blue Burst (production, canary) newserv’s HTTP API.
PSO: Blue Burst (legacy, Tethealla) The account portal’s status endpoint, which already counts online players. Reusing it means the collector never needs credentials for the game’s database.
Palworld, card game Their REST APIs. Palworld’s requires authentication, held in a git-ignored env file.

Every probe has a 5-second timeout and runs inside its own error boundary. A server that doesn’t answer is reported as offline, and the rest of the run carries on.

Reliability

  • Health check on freshness, not liveness. The container is healthy only if players.json was rewritten in the last three minutes, so a hung loop is caught even though the process is still running.
  • Atomic writes. Each file is written to a temp file and renamed, so readers never see half a file.
  • Rate-limit aware. Zandronum refuses a second query from the same address within 10 seconds; the 30-second cycle stays well clear of that.
  • Monitored from both sides. Uptime Kuma watches the collector, the internal web server and the public page as separate checks.

Results

  • 12 servers across 9 game types report live player counts from one feed.
  • 90 days of history in VictoriaMetrics, shown as a stacked 24-hour / 7-day chart on the admin dashboard, so I can see when friends actually play.
  • No credentials exposed and no new public routes beyond one read-only, counts-only JSON file.
  • Reused everywhere: the public status page, a “how to join” guide, the admin dashboard and this site’s live panel all read the same feed.

What I’d do next

  • Alert on nobody playing for an unusual stretch, as a soft signal that something is wrong.
  • Split the probes into small, separately testable modules with recorded protocol fixtures, so each decoder is checked against real captured replies in CI.

← All projects