gameserver-observability · v0.1.1
Configuration
The pack is configuration: the scrape jobs in prometheus/scrape/, the json_exporter modules in
json_exporter/config.yml, the blackbox_exporter modules in blackbox/blackbox.yml, the
recording and alerting rules in rules/ and the dashboard in grafana/. Change the values below
for your servers, then run python3 scripts/check_pack.py, bash scripts/check_promtool.sh and
python3 scripts/lint_dashboards.py (with promtool on the PATH) before you load them.
Metrics reference describes what each job collects.
The pack check accepts only loopback and documentation addresses (192.0.2.0/24,
198.51.100.0/24, 203.0.113.0/24, 2001:db8::/32), so that no real address gets into this
repository. A copy with the addresses of your servers, such as your clone with the targets filled
in or a folder with the same prometheus/, json_exporter/ and blackbox/ layout (--root),
reports each of them as an [addresses] finding: check such a copy with
python3 scripts/check_pack.py --allow-any-address, which skips only that check.
Game server
/perf/ must be protected, otherwise anyone who reaches the game port can read it. Put the two
variables into the server’s secrets.cfg (or wherever its secrets live, never into git):
set sv_prometheusBasicAuthUser "prometheus"
set sv_prometheusBasicAuthPassword "CHANGE_ME_PERF_PASSWORD"
With the luminare.gameserver collection, set fxserver_perf_user and fxserver_perf_password
(or the txadmin role’s txadmin_prometheus_basic_auth) instead, and its fxserver role writes
the two lines (installation).
The server compares the whole Authorization header with these values. With neither set,
/perf/ is open by the server’s source code (not yet tried on a running server), and the probe
http_401_expected reports it (probe_success 0). The JSON
endpoints need no setting; a proxy between Prometheus and the server is refused from
sv_requestParanoia 1 on (metrics reference).
Scrape jobs (prometheus/scrape/fxserver.yml)
| Setting | Default | Description |
|---|---|---|
server label |
game-01 |
Your name for the game server, the same in every job. Rules and dashboards group by it; use a short name, not a host name or address. |
fxserver_perf target |
127.0.0.1:30120 |
Host and game port of the server. |
basic_auth.username |
prometheus |
Equal to sv_prometheusBasicAuthUser. |
basic_auth.password_file |
/etc/prometheus/secrets/fxserver_perf |
File with the value of sv_prometheusBasicAuthPassword and nothing else. Prometheus strips leading and trailing whitespace (spaces, tabs, the newline) from it and keeps spaces inside the password, so do not use a password that starts or ends with a space. Readable by Prometheus only (for example mode 0640, group of the Prometheus user). |
fxserver_json targets |
http://127.0.0.1:30120/dynamic.json (module fxserver_dynamic), http://127.0.0.1:30120/info.json (module fxserver_info) |
The URLs json_exporter fetches; the module label selects the json_exporter module. |
fxserver_blackbox targets |
127.0.0.1:30120 (tcp_connect), http://127.0.0.1:30120/info.json (http_fxserver_info), http://127.0.0.1:30120/perf/ (http_401_expected) |
What blackbox_exporter probes; the module label selects the probe module. |
replacement of __address__ |
127.0.0.1:7979 (json_exporter), 127.0.0.1:9115 (blackbox_exporter) |
Where the exporters listen, as seen from Prometheus. |
scrape_interval |
15s for /perf/, 30s for the exporter jobs |
info.json is rate-limited by the server (4 requests per second per address), so keep the JSON jobs at 30 seconds or more. |
sample_limit |
100, 20, 60 |
Upper bound of series per target (cardinality budget). |
Never change the relabel rules of fxserver_perf without the pack check: they keep only the
tick-time histogram, rename it and drop the server’s name label
(ADR 0010).
More than one game server
Add one target group per server and job, each with its own server label:
static_configs:
- targets: ["192.0.2.10:30120"]
labels:
server: game-01
- targets: ["192.0.2.11:30120"]
labels:
server: game-02
Basic auth is set per job, so every server scraped by fxserver_perf needs the same /perf/
user and password. The credentials travel unencrypted with plain HTTP: scrape a game server on
another host only over a private network or a tunnel, never over the internet.
The admin hostname probe (prometheus/scrape/tunnel.yml)
Optional. Set the target to the HTTPS address of your access-protected admin hostname (for
example https://demo-tx.example.com/) and keep the server label of the game server. The probe
succeeds when a request without a login is redirected (302, 303) or refused (403).
The version list (prometheus/scrape/versions.yml)
Optional, once per Prometheus. It needs no change: the target is the platform’s public version
list, which json_exporter fetches over HTTPS every 4 minutes, so the Prometheus host needs
outbound HTTPS. A shorter interval brings no benefit; a longer one lets the series go stale
between scrapes. Without this job, FXServerBuildBehindRecommended never fires; the other alerts
do not need it.
Host and database jobs
Some alerts read node_exporter and mysqld_exporter (metrics reference),
and the dashboard also reads the metrics of the tunnel connector, cloudflared
(metrics reference). This pack ships
no jobs for them, because most setups scrape these exporters already. Give each of their targets
the server label of the game server it belongs to, so that the alerts and the dashboard name
it:
scrape_configs:
- job_name: node
static_configs:
- targets: ["127.0.0.1:9100"]
labels:
server: game-01
- job_name: mysqld
static_configs:
- targets: ["127.0.0.1:9104"]
labels:
server: game-01
- job_name: cloudflared
static_configs:
- targets: ["127.0.0.1:2000"]
labels:
server: game-01
cloudflared serves its metrics only with --metrics <address> (for example
--metrics 127.0.0.1:2000 in its service’s command line); keep that address on loopback or a
private network.
For the two build alerts, node_exporter reads the textfile collector folder that the fxserver
role of the luminare.gameserver collection writes to (fxserver_textfile_dir), for example
--collector.textfile.directory=/var/lib/node_exporter/textfile_collector. Without these jobs
the host, build and database alerts stay silent and the dashboard’s host, MariaDB and tunnel
panels show no data; without the server label the alerts fire without naming a game server, the
alerts of two hosts merge into one, and the dashboard’s server filter does not reach these
panels.
Dashboard (grafana/)
| File | Content |
|---|---|
grafana/dashboards/game-server-overview.json |
The dashboard “Game server overview” (uid gso-game-server-overview), for provisioning. |
grafana/dashboards/shareable/game-server-overview.json |
The same dashboard without variables (uid gso-game-server-overview-shared), for importing; written by python3 scripts/lint_dashboards.py --fix (ADR 0018). |
grafana/provisioning/dashboards/gameserver-observability.yml |
Loads the dashboards from /var/lib/grafana/dashboards/gameserver-observability into the Grafana folder “Game server”. |
grafana/provisioning/datasources/gameserver-observability.yml |
Optional: a Prometheus data source “Game server metrics” (uid gso-prometheus), for a Grafana that has none for the pack’s metrics. |
The dashboard has six rows: Overview (status, players, slots in use, svMain p95, build, build
age, restarts, alerts), Tick time (heatmap of the svMain histogram, p95 and p99, ticks per
second, mean), Players, Host, MariaDB, and Probes and tunnel. It has no row for the game server’s
resources (scripts and assets), because the pack does not collect the resource list
(metrics reference); the host row shows the machine’s CPU, memory, disk
and network.
Its variables:
| Variable | Default | Description |
|---|---|---|
datasource (Data source) |
Grafana’s default Prometheus data source | Any Prometheus data source with the pack’s series. The file stores no data source, so every viewer gets their own default. |
server (Game server) |
All | The values of the server label of up{job="fxserver_perf"}; one or several. Every panel filters on it. |
Settings in the provisioning files:
| Setting | Default | Description |
|---|---|---|
options.path (dashboards) |
/var/lib/grafana/dashboards/gameserver-observability |
The folder Grafana reads the dashboard files from. Copy only grafana/dashboards/*.json there: Grafana also reads subfolders, and would then provision the shareable copy as a second dashboard. |
folder, folderUid (dashboards) |
Game server, gso-game-server |
The Grafana folder of the dashboard. |
allowUiUpdates, disableDeletion (dashboards) |
false, true |
The files are the source: changes made in Grafana cannot be saved over them, and a dashboard is not deleted from Grafana when its file goes missing. |
updateIntervalSeconds (dashboards) |
60 |
How often Grafana reads the folder for changed files. |
url (data source) |
http://prometheus:9090 |
Your Prometheus, as Grafana reaches it. |
timeInterval (data source) |
30s |
The scrape interval Grafana assumes for this data source. $__rate_interval is at least four times this value, so the rate panels use windows of 2 minutes or more, which hold two samples even of jobs scraped once a minute. |
The file does not mark the data source as the default, so it never takes over from a default you have; Grafana makes it the default only when it is the first data source.
To change the dashboard, edit grafana/dashboards/game-server-overview.json (for example by
pasting the JSON model of a changed copy from Grafana, with "id": null, the same uid and the
data source variable kept), then write the shareable copy and check both:
python3 scripts/lint_dashboards.py --fix
python3 scripts/lint_dashboards.py --require-promtool
The check fails on platform or vendor marks in titles, a stored data source uid or variable
selection, a metric that is not in the metrics reference, a label that could identify
a player, the instance label, an address or a link outside this repository, and a shareable
copy that does not match (ADR 0019).
json_exporter modules (json_exporter/config.yml)
| Module | Fetches | Metrics |
|---|---|---|
fxserver_dynamic |
dynamic.json |
fxserver_clients, fxserver_max_clients |
fxserver_info |
info.json |
fxserver_info (labels version, and build from the scrape job), fxserver_onesync_enabled |
fxserver_versions |
the platform’s version list (job fxserver_versions) |
fxserver_recommended_build |
The modules do not follow redirects. json_exporter ignores keys it does not know, so a typo
would silently drop a metric; scripts/check_pack.py reports unknown keys. Start json_exporter
with --config.file=/etc/json_exporter/config.yml --web.listen-address=127.0.0.1:7979.
blackbox_exporter modules (blackbox/blackbox.yml)
| Module | Prober | Succeeds when |
|---|---|---|
tcp_connect |
TCP | the port accepts a connection within 5 s |
http_fxserver_info |
HTTP | GET answers 200 with "resources" in the body within 5 s |
http_401_expected |
HTTP | GET without credentials answers 401 within 5 s |
http_access_protected |
HTTP | GET over HTTPS answers 302, 303 or 403 within 10 s |
Start blackbox_exporter with --config.file=/etc/blackbox_exporter/blackbox.yml --web.listen-address=127.0.0.1:9115. Both exporters fetch any URL that Prometheus (or anyone
else who reaches them) asks for, so they listen on loopback or a private address only.
Rules (rules/)
Load all rule files with rule_files: [/etc/prometheus/rules/*.yml], or leave out the files whose
data you do not collect. Every alert has the labels server and severity and the annotations
summary, description and runbook_url (a section of the runbook)
(ADR 0015).
| File | Rules | Needs |
|---|---|---|
fxserver.recording.yml |
the recorded tick-time series | job fxserver_perf |
fxserver.alerts.yml |
the game server alerts | the jobs in prometheus/scrape/fxserver.yml (and tunnel.yml for the admin hostname alerts), the recorded series |
build.alerts.yml |
the build alerts | node_exporter with the build textfile, and the job fxserver_versions for FXServerBuildBehindRecommended |
host.alerts.yml |
the disk alert | node_exporter |
mariadb.alerts.yml |
the database alerts | mysqld_exporter |
The thresholds:
| Alert | File | Fires when | For | Severity |
|---|---|---|---|---|
FXServerDown |
fxserver.alerts.yml |
the probe tcp_connect or http_fxserver_info fails |
2 min | critical |
FXServerTargetDown |
fxserver.alerts.yml |
a target of fxserver_perf, fxserver_json, fxserver_blackbox or fxserver_tunnel cannot be scraped while the probes do not report the server as down |
10 min | warning |
FXServerTickTimeHigh |
fxserver.alerts.yml |
the svMain tick time’s 95th percentile over a minute is above 50 ms |
5 min | warning |
FXServerNoPlayersAfterRestart |
fxserver.alerts.yml |
0 players for 30 minutes after a restart 30 to 60 minutes ago, with players in the 30 minutes before (ADR 0016) | none | warning |
FXServerPerfEndpointExposed |
fxserver.alerts.yml |
/perf/ answers a request without credentials with 2xx |
5 min | warning |
FXServerAdminHostnameUnprotected |
fxserver.alerts.yml |
the admin hostname answers a request without a login with 2xx |
5 min | critical |
FXServerAdminHostnameProbeFailing |
fxserver.alerts.yml |
the admin hostname probe fails for another reason | 10 min | warning |
FXServerArtifactOld |
build.alerts.yml |
the active build was switched on more than 56 days (8 weeks) ago | 1 h | warning |
FXServerBuildBehindRecommended |
build.alerts.yml |
the active build is older than every Recommended build of the last 14 days (ADR 0017) | 1 h | warning |
HostDiskAlmostFull |
host.alerts.yml |
a writable filesystem is more than 85 % full (less than 15 % available) | 10 min | warning |
MariaDBDown |
mariadb.alerts.yml |
mysql_up is 0 |
2 min | critical |
MariaDBSlowQueries |
mariadb.alerts.yml |
more than 1 slow query a minute, averaged over 5 minutes | 10 min | warning |
To change a threshold, edit the number in the alert’s expr (or its for) in your copy, and in
this repository change the expected alerts in tests/rules/ and the alert’s runbook section with
it; bash scripts/check_promtool.sh must pass. The rule groups are evaluated every 30 seconds
(fxserver-tick-time, fxserver-alerts) or every minute (the others), whatever
evaluation_interval the Prometheus config sets.
Send the alerts to Alertmanager
The pack ships no Alertmanager configuration: Prometheus evaluates the rules and shows pending and
firing alerts (Status → Alerts, the ALERTS series, the dashboard), and sending them to people is
the job of your Alertmanager (ADR 0023). Point Prometheus at
it in /etc/prometheus/prometheus.yml:
alerting:
alertmanagers:
- static_configs:
- targets: ["127.0.0.1:9093"]
Then route on the alerts’ labels in your Alertmanager configuration. Every alert has the labels
alertname, server (the game server) and severity (critical or warning), and the
annotations summary, description and runbook_url, which most receivers can put into the
message. This route sends critical alerts to one receiver and everything else to another, grouped
by alert and game server:
route:
receiver: default
group_by: [alertname, server]
routes:
- matchers: ['severity="critical"']
receiver: on-call
receivers:
- name: default
- name: on-call
The two receivers are empty here, so they deliver nothing; add your mail, chat or webhook
settings to them in your own configuration, and keep their passwords, tokens and webhook URLs out
of git. On 2026-10-22, amtool check-config (amtool 0.34.1) accepted this file,
amtool config routes test --config.file=alertmanager.yml severity=critical printed on-call,
and the alerting block above passed promtool check config together with the pack’s rule
files.
Fake game server (demo/fake-fxserver/fake_fxserver.py)
For tests and demos only; every value it serves is synthetic. Each option can also be set with the environment variable in the second column.
| Option | Variable | Default | Description |
|---|---|---|---|
--bind |
FAKE_BIND |
127.0.0.1 |
Address to listen on. |
--port |
FAKE_PORT |
30120 |
Port for all endpoints. |
--build |
FAKE_BUILD |
35245 |
Build number in the version string. |
--clients, --max-clients |
FAKE_CLIENTS, FAKE_MAX_CLIENTS |
3, 32 |
Connected clients and slots. |
--main-tick-ms |
FAKE_MAIN_TICK_MS |
4 |
The load knob: mean svMain tick time in milliseconds. 70 or more is a spike; above 50 the thread also ticks less often. |
--request-paranoia |
FAKE_REQUEST_PARANOIA |
0 |
Like sv_requestParanoia, levels 0 to 3. |
--seed |
FAKE_SEED |
-1 (random) |
Seed for repeatable tick times. |
--perf-user |
FAKE_PERF_USER |
empty | User for /perf/. |
--perf-password-file |
FAKE_PERF_PASSWORD_FILE |
empty | File with the /perf/ password; without it FAKE_PERF_PASSWORD is used. With neither user nor password, /perf/ is open, as on the real server. |
--no-control |
control on | Turns off /fake/state and /fake/control. |
|
--quiet |
log on | Does not log requests. |
At run time, POST /fake/control with a JSON object changes the load and the clients, for example
a two-minute spike:
curl -fsS -X POST -d '{"main_tick_ms": 90, "for_seconds": 120}' http://127.0.0.1:30120/fake/control
The fields are main_tick_ms, for_seconds (with main_tick_ms: return to the previous value
afterwards), clients and restart (true resets the histograms). GET /fake/state shows the
settings and how often each path was requested.
Repository checks
| Option | Type | Default | Description |
|---|---|---|---|
TEST_VENV (environment, scripts/test.sh) |
path | in CI $RUNNER_TEMP/gameserver-observability-test-venv, elsewhere ${XDG_CACHE_HOME:-~/.cache}/gameserver-observability/test-venv |
Where the test script keeps its virtual environment. It is reused if it exists and belongs to the current user (otherwise the script stops with an error), and created again if it has no working pip; the requirements are installed again on every run. |
test-command (input in ci.yml and release.yml) |
string | bash scripts/test.sh |
The test command for ci / lint-test and for the release build on the tag. Both files must use the same value (a unit test checks it). |
archive-name (input in release.yml) |
string | gameserver-observability |
Name of the release asset <name>-X.Y.Z.tar.gz. |
PROMTOOL_VERSION, PROMTOOL_SHA256 (env: of the promtool job in ci.yml) |
string | 3.15.0, the sha256 of its linux-amd64 tarball |
The promtool download. Change both together and update toolchain.md; a unit test compares them. |
PROMETHEUS_*, JSON_EXPORTER_* and BLACKBOX_EXPORTER_*, each _VERSION and _SHA256 (env: of the exporters job in ci.yml) |
string | 3.15.0, 0.8.0, 0.29.0 and the sha256 of each linux-amd64 tarball |
The downloads for scripts/smoke_exporters.py. Prometheus must equal the promtool pin; a unit test compares all of them with toolchain.md. |
GITLEAKS_VERSION, GITLEAKS_SHA256 (variables in scripts/test.sh) |
string | 8.30.1, the sha256 of its linux-x64 tarball |
The gitleaks download that scripts/test.sh makes in CI (CI=true), so the secret-rule tests run there. Keep them equal to the defaults in reusable-gitleaks.yml and to toolchain.md; a unit test compares them. |
--allow-any-address (scripts/check_pack.py) |
flag | off | Accept any IP address instead of only loopback and the documentation ranges, for a copy of the configs with the addresses of your servers. CI never sets it. |
--require-promtool (scripts/check_pack.py) |
flag | off | Fail when promtool is not on the PATH, instead of skipping promtool check config. The promtool job sets it. |
--require-promtool (scripts/lint_dashboards.py) |
flag | off | Fail when promtool is not on the PATH, instead of skipping the parse of every panel query with promtool check rules. The promtool job sets it. |
--fix (scripts/lint_dashboards.py) |
flag | off | Write the dashboards in the repository’s JSON format and their shareable copies, then check them. |
--bin-dir, --timeout (scripts/smoke_exporters.py) |
path, seconds | the PATH, 120 |
Where the three binaries are, and how long to wait for each step: the first scrapes, the first evaluation of the rules, and each alert to go pending. |
Secrets
Secrets never go into this repository, an issue or a log. Pass them through files outside the repository, environment variables or a secret store. CI scans every commit with gitleaks; a leaked secret is rotated first and removed from history second.
- The
/perf/password goes into the password file on the Prometheus host and into the server’ssecrets.cfg; the scrape config only names the file.scripts/check_pack.pyfails on any inlinepassword, token or credentials setting, on inlinesecretsin anhttp_headersmap, and on an inline value for a header named like a credential (Authorization, a cookie, token, key, secret or password header) in the header maps of the Prometheus jobs, json_exporter and blackbox_exporter modules. - The repository’s CI needs no secret: it uses only the workflow’s own
GITHUB_TOKEN, and the end-to-end run creates a random password for each run. - Never commit a server license key (
sv_licenseKey,cfxk_…), a txAdmin token or a password. Where an example needs a license key, writeCHANGE_ME_LICENSE_KEY, and never start a placeholder withcfxk_. Anycfxk_value with 20 or more key characters after the prefix fails the secret scan, wherever it stands in a line; the only exception is a placeholder made ofxand_characters. Shorter values such ascfxk_REPLACE_MEare not reported, but they still look like keys, so do not use them either (ADR 0008).
Not affiliated with or endorsed by Cfx.re, Rockstar Games or Take-Two.
docs/configuration.md on GitHub
Licensed under Apache-2.0. Source: luminare-dev/gameserver-observability at v0.1.1.