Prometheus exporter for [Xonotic](https://xonotic.org/) game servers.
Scrapes metrics via RCON (`sv_public` + `status 1`) and exposes them
in Prometheus exposition format.
A production-grade [Prometheus](https://prometheus.io/) exporter for [Xonotic](https://xonotic.org/) game servers. It scrapes server metrics via RCON — using all three Xonotic authentication modes — and exposes them in Prometheus exposition format on a single HTTP endpoint. One process monitors as many servers as you like.
scrapes │ GET /metrics?target=vehicles ───────────► UDP RCON ──► Xonotic srv A
/metrics?target=N │ GET /metrics?target=dm ───────────► UDP RCON ──► Xonotic srv B
│ GET /geo?target=vehicles ───────────► ip-api.com │
│ GET /match?target=vehicles ───────────► UDP RCON │
│ POST /-/reload │ │
│ GET /-/healthy │ │
│ │
│ TOML config ◄── SIGHUP reloads on disk │
└─────────────────────────────────────────────┘
```
### How a single scrape works
```
Prometheus xonotic-exporter Xonotic server
│ │ │
│ GET /metrics?target=dm │ │
│───────────────────────────►│ │
│ │ UDP: ping packet │
│ │──────────────────────────────►│
│ │ UDP: getchallenge (mode 2) │
│ │──────────────────────────────►│
│ │ challenge token │
│ │◄──────────────────────────────│
│ │ UDP: RCON "sv_public\0status 1"
│ │ (HMAC-MD4 signed) │
│ │──────────────────────────────►│
│ │ pong │
│ │◄──────────────────────────────│
│ │ RCON response chunks │
│ │◄──────────────────────────────│
│ │ parse → build Prometheus reg │
│ 200 OK + metrics text │ │
│◄───────────────────────────│ │
```
Both the ping and the RCON query are sent concurrently. The parser uses an adaptive wait: it reads the first chunk, notes the RTT, then waits up to `2× RTT` for additional packets (Xonotic responses can span multiple UDP datagrams).
---
## Requirements
- Python **3.11** or later (3.12 / 3.13 fully supported)
The exporter uses the [blackbox exporter pattern](https://prometheus.io/docs/guides/multi-target-exporter/): Prometheus sends a `?target=<name>` query parameter, and the exporter queries that specific server on demand. This means one exporter process and one HTTP port serves all your game servers.
Copy `examples/prometheus.yml` or add this to your existing `prometheus.yml`:
```yaml
scrape_configs:
- job_name:"xonotic"
scrape_interval:15s
scrape_timeout:10s
relabel_configs:
# Pass the server name as ?target=
- source_labels:[__address__]
target_label:__param_target
# Use the server name as the instance label in Grafana
- source_labels:[__param_target]
target_label:instance
# Point all scrapes at the exporter
- target_label:__address__
replacement:"127.0.0.1:9260"
static_configs:
- targets:
- "vehicles"# must match a [[servers]] name in the TOML
- "deathmatch"
```
The `relabel_configs` block is required — without it, Prometheus would try to scrape the server name directly as an HTTP address.
For GeoIP and match data, add separate scrape jobs pointing at `/geo` and `/match`:
```yaml
- job_name:"xonotic_geo"
metrics_path:/geo
scrape_interval:30s
# ... same relabel_configs and targets as above ...
- job_name:"xonotic_match"
metrics_path:/match
scrape_interval:15s
# ... same relabel_configs and targets as above ...
```
---
## Metrics reference
### Core server metrics (`/metrics`)
All metrics carry an `instance` label set to the server name from the TOML config.
| Metric | Type | Description |
|--------|------|-------------|
| `xonotic_up` | Gauge | `1` if the server was reachable on this scrape, `0` otherwise |
| `xonotic_sv_public` | Gauge | Value of the `sv_public` cvar (`1` = listed on master server) |
| `xonotic_ping_seconds` | Gauge | Round-trip time to the server in seconds |
| `xonotic_timing_cpu_percent` | Gauge | Server CPU usage % (from `status 1`) |
| `xonotic_player_geo` | Always `1`; carries geolocation labels including `lat` and `lon` |
| `xonotic_player_ping` | Per-player ping to the server in milliseconds |
| `xonotic_player_packetloss_percent` | Per-player packet loss percentage |
Private/loopback IPs are never resolved or emitted. GeoIP results are cached for 24 hours using ip-api.com's free batch endpoint (up to 100 IPs per request, 15 req/min rate limit).
### Match data metrics (`/match`)
| Metric | Labels | Description |
|--------|--------|-------------|
| `xonotic_match_timelimit_seconds` | `instance` | Time limit converted to seconds (timelimit × 60) |
| `xonotic_match_fraglimit` | `instance` | Frag limit for the current match |
Xonotic supports three RCON authentication modes. The mode in your exporter config **must match** the `rcon_secure` / `rcon_restricted` setting in each game server's `server.cfg`.
| Mode | Name | `server.cfg` | Security | Notes |
|------|------|--------------|----------|-------|
| `0` | nonsecure | `rcon_restricted 0` | Low — password sent in plaintext | Avoid in production; use only on localhost |
| `1` | secure-time | `rcon_secure 1` | Medium — HMAC-MD4 signed with current timestamp | Requires clocks to be in sync (within a few seconds) |
| `2` | secure-challenge | `rcon_secure 2` | High — HMAC-MD4 signed with a server-issued challenge | **Recommended.** Not susceptible to replay attacks |
In mode `2` (the default), the exporter first requests a challenge token from the server, then signs the RCON command with `HMAC-MD4(password, challenge + command)`. This exchange happens automatically before every RCON command.
---
## Project structure
```
xonotic-exporter/
├── pyproject.toml # build config, dependencies, entry point
├── examples/
│ ├── xonotic_exporter.toml # annotated example configuration
└── geoip.py # async GeoIP cache backed by ip-api.com batch API
```
### Module responsibilities
`cli.py` is the entry point. It parses arguments and dispatches to one of three sub-commands: `serve`, `query`, or `validate`. The `serve` command calls `run_server()` in `server.py`, which creates an `asyncio` HTTP server that handles every incoming request.
`rcon.py` owns all network I/O. It implements `_XonoticProtocol`, a low-level `asyncio.DatagramProtocol` that handles ping, challenge, and RCON packet framing. The public `scrape_server()` coroutine opens a UDP endpoint, fires ping and RCON commands concurrently, and retries up to the configured limit.
`metrics_parser.py` contains `XonoticMetricsParser`, a state machine that processes the raw bytes returned by `sv_public` and `status 1` line by line. It handles Xonotic color codes (`^1`, `^x1A2`), IPv4, IPv6, and bot clients.
`prometheus.py` builds a fresh `CollectorRegistry` on every scrape request — this is the correct multi-target pattern, as it avoids any shared global metric state between servers.
`geoip.py` maintains a long-lived, process-wide IP → GeoResult cache. Cache entries expire after 24 hours. Lookups for private and loopback addresses are skipped entirely without a network call.
---
A simple Prometheus Xonotic Exporter written in Python
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.