Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

gameserver-exporter

ci Go 1.26 License: MIT

A multi-target Prometheus exporter for game servers. It follows the blackbox_exporter model — Prometheus scrapes /probe?target=host:port&module=MODULE, and the exporter speaks the game's query protocol to report liveness, player counts and server info.

Modules

Module Protocol Metrics
tcp plain TCP connect gameserver_up
steam-a2s Source-engine A2S (UDP query) gameserver_up, gameserver_players, gameserver_max_players, gameserver_info{name,map,game,version}
minecraft-slp Minecraft Server List Ping gameserver_up, gameserver_players, gameserver_max_players, gameserver_info{version,protocol,motd}

Every probe also exposes gameserver_probe_duration_seconds.

Try it

make run   # listens on :9116 (probe) and :9117 (health)

curl 'localhost:9116/probe?target=mc.hypixel.net:25565&module=minecraft-slp'
# gameserver_up 1
# gameserver_players 78012
# gameserver_max_players 200000
# gameserver_info{version="Requires MC 1.8-1.21",protocol="47",motd="..."} 1
# gameserver_probe_duration_seconds 0.07

How it works

  • Multi-target pattern: one exporter instance probes many servers. Each scrape gets a fresh prometheus.Registry, so per-target metrics never leak into one another — the same isolation blackbox_exporter uses.
  • minecraft-slp is a hand-rolled implementation of the SLP protocol (VarInt codec + length-prefixed handshake/status packets → JSON status).
  • steam-a2s uses go-a2s; A2S is UDP with a challenge/response and packet fragmentation, so a maintained library is the sensible choice over a bespoke parser.
  • Respects the X-Prometheus-Scrape-Timeout-Seconds header, so probes stay inside the scrape budget.
  • The exporter's own health lives on a separate port (:9117 /-/healthy, /-/ready); operational counters (gameserver_exporter_probes_total, gameserver_exporter_probe_errors_total) are on /metrics.

Prometheus configuration

Scrape the list of servers and rewrite the request onto the exporter — see deploy/prometheus-scrape-example.yaml:

scrape_configs:
  - job_name: gameservers
    metrics_path: /probe
    params: { module: [minecraft-slp] }
    static_configs:
      - targets: [mc-1.example.com:25565, mc-2.example.com:25565]
    relabel_configs:
      - { source_labels: [__address__], target_label: __param_target }
      - { source_labels: [__param_target], target_label: instance }
      - { target_label: __address__, replacement: gameserver-exporter.monitoring.svc:9116 }

Deploy

kubectl apply -f deploy/deployment.yaml   # Deployment + Service (namespace: monitoring)

Design decisions

Why not blackbox_exporter? It speaks HTTP/TCP/ICMP/DNS, not game protocols. This reuses the same /probe + relabel pattern (so it drops into Prometheus identically) while adding game semantics: online players, version, map, MOTD.

Why a fresh registry per request? A single instance probes many targets; global gauges would race and blur "whose up is this". A per-scrape registry keeps each target's series clean and cardinality bounded.

Why hand-roll SLP but use a library for A2S? SLP is simple enough (VarInt + handshake + JSON) that a small, tested codec is clearer than a dependency; A2S's UDP challenge/response and fragmentation are error-prone enough to be worth a maintained library.

Tests

go test -race -cover ./...

85% statement coverage of internal/probe, no network access beyond loopback, about a second to run.

The Minecraft module is the part that earns its tests. It speaks the Server List Ping protocol directly, because the wire format is VarInt-length-prefixed and there is no maintained Go library worth the dependency — which means the codec is hand-written, and hand-written codecs are where boundary cases go wrong. A wrong VarInt does not return an error; it frames the next packet at the wrong offset and the failure surfaces as an unexplained read_error.

So the suite covers:

  • VarInt boundaries — every width transition (127/128, 16383/16384, and so on), MaxInt32, and negatives, which always occupy the full five bytes.
  • Refusal to trust the peer. A stream of continuation bits must not loop forever, and every length read off the wire is range-checked before it sizes an allocation: a remote server should not be able to ask this exporter for a gigabyte.
  • The handshake, byte by byte — frame length, packet id, protocol version of -1, the length-prefixed host, big-endian port, and next-state 1. It also checks the requested hostname is sent rather than the resolved address, since virtual-hosted servers route on it.
  • The probe end to end, against a fake server on a real loopback socket rather than an io.Reader. Framing bugs only appear once the bytes actually cross a connection in two separate writes.
  • Every error label the probe can return: bad_target, the dial-class errors, read_error and parse_error each have a test that produces them.
  • Scrape-timeout resolution. Prometheus sends bare seconds in a header, not a Go duration; accepting only the Go form would silently ignore every real scrape, and a probe that outlives its scrape budget is worse than a failed one.

The MOTD becomes a metric label, so it is asserted to stay clamped regardless of what the server sends, and gameserver_info is asserted to be exactly one series — more than one means a label is varying per scrape.

Limitations

  • gameserver_info carries motd/name labels — bounded (MOTD clamped to 96 chars), but watch cardinality if MOTDs are dynamic across many servers.
  • Probes are synchronous per request (standard for the blackbox pattern); mind scrape_timeout for slow/unreachable targets.

License

MIT — see LICENSE.

About

Multi-target Prometheus exporter for game servers: Steam A2S, Minecraft SLP and TCP probes via the blackbox /probe pattern

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages