gameserver-observability · v0.1.1
Metrics reference
Every series this pack collects, where it comes from and how far it is verified. All of them are
collected from outside the server process: Prometheus scrapes /perf/ on the game port directly
and asks json_exporter and blackbox_exporter for the rest
(prometheus/scrape/fxserver.yml). Counts only: no series
and no label holds a player’s name, identifier, address or anything else tied to one player
(Not collected). How many series this makes per server:
cardinality budget. The rules in rules/ add
recorded series and also read a few
series of other exporters, and the dashboard reads
a few more.
How far each metric is verified
The Verified column says what was checked for the source field on the game server:
- source: the endpoint, the field, its type and its access rule were read in the FXServer
source code of build 35245 (commit
6efb47d, the Recommended build this project pins), on 2026-10-21. - exporter: the metric is made by the exporter or by Prometheus itself, not by the game server.
- live: the field was read from the real endpoint through the exporter with this pack’s module (only possible for the public version list, not for a game server).
- unverified: an assumption; the row says which one.
No row has been checked against a running FXServer yet. That needs a server with a license key;
until then this is listed under Limitations in the README, and the probe log
holds the commands for that check and its empty result tables. The source files behind the rows
were compared with those of build 37692 (commit
ccd9221),
the Latest build on 2026-10-22: the endpoint handlers, the tick-time histograms and the version
string’s build script are unchanged, and so is the code around onesync_enabled (README,
Results).
The exported side is checked for every row except the optional tunnel probe:
scripts/smoke_exporters.py (the CI job exporters) runs Prometheus 3.15.0, json_exporter 0.8.0
and blackbox_exporter 0.29.0 with these configs against the fake server
(demo/fake-fxserver/fake_fxserver.py) and a synthetic version list, which answer with the shapes
listed here, and checks the metric names, labels and values that arrive in Prometheus, and the
recorded series of the rules.
Every series also has the labels job (the scrape job) and instance (for /perf/ the
host:port, for the exporter jobs the probed URL or address). server is the label you set per
game server in the scrape config. Because instance can hold a host address or hostname, shared
views group by server and leave instance out
(ADR 0013).
Job fxserver_perf: /perf/ on the game port
| Metric | Type | Labels | Source endpoint and field | Verified |
|---|---|---|---|---|
fxserver_tick_time_seconds_bucket |
counter (histogram bucket) | server, thread, le |
GET /perf/: tickTime_bucket{name="…",le="…"} |
source |
fxserver_tick_time_seconds_sum |
counter (seconds) | server, thread |
GET /perf/: tickTime_sum{name="…"} |
source |
fxserver_tick_time_seconds_count |
counter (ticks) | server, thread |
GET /perf/: tickTime_count{name="…"} |
source |
- What it measures: the time each server thread spends on one tick, measured in whole
milliseconds and recorded in seconds
(
GameServer.cpp).threadissvMain(server frame with the resources’ work, a timer every 50 ms),svNetwork(network packets, every 10 ms) orsvSync(OneSync state, every 8 ms). A tick that takes longer than its interval delays the next one, so the tick rate drops under load. - Buckets: 0.001, 0.002, 0.004, 0.006, 0.008, 0.010, 0.015, 0.020, 0.030, 0.050, 0.070, 0.100,
0.150 and 0.250 seconds, plus
+Inf: 15 bucket series per thread. - Renaming: the scrape job keeps only
tickTime_bucket,tickTime_sumandtickTime_count(anything else a server version serves on/perf/is dropped), renames them tofxserver_tick_time_seconds_*, copies thenamelabel tothreadand dropsname(ADR 0010). - Access: when
sv_prometheusBasicAuthUserorsv_prometheusBasicAuthPasswordis set, a request without exactly these Basic credentials gets401with the bodyUnauthorized.; when both are empty,/perf/is open to anyone, limited to 2 requests per second (burst 5) per address (PerfHttpHandler.cpp). The probehttp_401_expectedbelow fails in that case. - Format: the server answers with
Content-Type: text/plain; Prometheus 3.15.0 reads that as the classic text format without a fallback setting (checked in theexportersrun).
Job fxserver_json: info.json and dynamic.json through json_exporter
Modules in json_exporter/config.yml. The extra label module
is the json_exporter module that made the series.
| Metric | Type | Labels | Source endpoint and field | Verified |
|---|---|---|---|---|
fxserver_clients |
gauge | server, module |
GET /dynamic.json: clients, the number of connected clients |
source |
fxserver_max_clients |
gauge | server, module |
GET /dynamic.json: sv_maxclients, the player slots, sent as a string such as "32" that json_exporter reads as a number |
source |
fxserver_info |
gauge, always 1 | server, module, version, build |
GET /info.json: server, FXServer- and the build description |
source; build: unverified |
fxserver_onesync_enabled |
gauge, 0 or 1 | server, module |
GET /info.json: vars.onesync_enabled, "true" or "false" |
source |
- Fields:
dynamic.jsonholdshostname,gametype,mapname,clients,ivandsv_maxclients;info.jsonholdsserver,enhancedHostSupport,resources,vars,requestSteamTicket,enforceSteamAuth,versionand, with a server icon,icon(InfoHttpHandler.cpp). Only the four fields in the table are read. versionandbuild:versionis the rawserverstring,FXServer-followed by the build description. The Linux build script writes the description as<branch> <CI build number> linuxfor master and release builds, and as<branch> SERVER v1.0.0.<date> linuxonly for feature branches (build_server_proot_alpine.sh). The server library in the build 35245 download (libcitizen-server-impl.so) holds the textFXServer-master v1.0.0.35245 linux; it has not yet been seen on a running server. The fake server serves that shape. The scrape job copies the digits afterv<a>.<b>.<c>.intobuild, with or withoutSERVERbefore them (a unit test checks both); if the real string has another shape,buildis missing andversionstill has the whole string. The label changes only when the server is upgraded.- OneSync: the server sets
onesync_enabledto true at start whenonesyncisonorlegacy(ServerGameState.cpp). When the variable is missing, the module returns no series instead of an error. - Errors: json_exporter answers
503when the server’s status code is not 2xx (for example403 Nope., see Request paranoia); thenupis 0 for that target.
Job fxserver_blackbox: probes of the game port
Modules in blackbox/blackbox.yml; each probe target has the labels
server and module.
| Module | Target | Succeeds when | Verified |
|---|---|---|---|
tcp_connect |
host:30120 |
the game port accepts a TCP connection | exporter |
http_fxserver_info |
http://host:30120/info.json |
GET answers 200 with a body that contains "resources" |
exporter; field: source |
http_401_expected |
http://host:30120/perf/ |
GET without credentials is refused with 401 |
exporter; behaviour: source |
The probes produce these series (blackbox_exporter 0.29.0, as seen in the exporters run):
| Metric | Type | Labels | Modules |
|---|---|---|---|
probe_success |
gauge, 0 or 1 | server, module |
all |
probe_duration_seconds |
gauge | server, module |
all |
probe_dns_lookup_time_seconds |
gauge | server, module |
all |
probe_ip_protocol |
gauge (4 or 6) | server, module |
all |
probe_ip_addr_hash |
gauge (hash of the address, to spot changes) | server, module |
all |
probe_timeout_seconds |
gauge | server, module |
all |
probe_failed_due_to_regex |
gauge, 0 or 1 | server, module |
all |
probe_failed_due_to_bytes |
gauge, 0 or 1 | server, module |
tcp_connect |
probe_http_status_code |
gauge | server, module |
HTTP modules |
probe_http_duration_seconds |
gauge | server, module, phase |
HTTP modules |
probe_http_content_length |
gauge | server, module |
HTTP modules |
probe_http_uncompressed_body_length |
gauge | server, module |
HTTP modules |
probe_http_redirects |
gauge | server, module |
HTTP modules |
probe_http_ssl |
gauge, 0 or 1 | server, module |
HTTP modules |
probe_http_version |
gauge | server, module |
HTTP modules |
UDP on the game port has no generic probe; the TCP connect and the two HTTP probes go to the same port.
Job fxserver_tunnel (optional): the admin hostname
prometheus/scrape/tunnel.yml probes a hostname that an
access-protected tunnel publishes, for example the txAdmin web interface, with the module
http_access_protected: a GET without a login must answer 302, 303 or 403 over HTTPS.
The series are the HTTP ones above, plus the TLS series blackbox_exporter adds for HTTPS targets
(certificate expiry, TLS version). Verified: unverified, because no tunnel exists here; the job
is not part of the exporters run. The dashboard reads one of the TLS series:
| Metric | Type | Labels | Source endpoint and field | Verified |
|---|---|---|---|---|
probe_ssl_earliest_cert_expiry |
gauge (Unix time) | server, module |
the expiry of the certificate in the hostname’s chain that expires first | exporter (blackbox_exporter 0.29.0 against a local HTTPS stand-in with a test certificate, 2026-10-21) |
Job fxserver_versions (optional): the Recommended build
prometheus/scrape/versions.yml asks json_exporter, with the
module fxserver_versions, for the platform’s public list of Linux server builds,
https://changelogs-live.fivem.net/api/changelog/versions/linux/server, every 4 minutes. The list
is the same for every game server, so the job runs once per Prometheus and its series have no
server label. The request goes from json_exporter on the Prometheus host to the platform and
carries nothing about your servers.
| Metric | Type | Labels | Source endpoint and field | Verified |
|---|---|---|---|---|
fxserver_recommended_build |
gauge | module |
GET version list: recommended, the build number of the current Recommended build, sent as a string such as "35245" that json_exporter reads as a number |
live (35245 on 2026-10-21) |
- Fields: the list also holds the Latest build, the download URL of each channel, and in
support_policythe date each build’s support ends. Onlyrecommendedis read. - Used by: the alert
FXServerBuildBehindRecommended(rules/build.alerts.yml), which compares it with the build each game server runs.
Series Prometheus adds per target
| Metric | Type | Labels |
|---|---|---|
up |
gauge, 0 or 1 | server, module (exporter jobs) |
scrape_duration_seconds |
gauge | same |
scrape_samples_scraped |
gauge | same |
scrape_samples_post_metric_relabeling |
gauge | same |
scrape_series_added |
gauge | same |
Each job sets a sample_limit, so a target that suddenly serves more series fails its scrape
(up 0) instead of filling the database (cardinality budget).
Recorded series (rules/fxserver.recording.yml)
The recording rules turn the tick-time histogram into one series per server and thread, every
30 seconds. Their names follow the Prometheus convention level:metric:operations: the level
lists the labels the series keep (ADR 0014).
| Series | Labels | Meaning |
|---|---|---|
server_thread:fxserver_tick_time_seconds:p95_1m |
server, thread |
95th percentile of the tick time over the last minute, in seconds |
server_thread:fxserver_tick_time_seconds:p99_1m |
server, thread |
99th percentile of the tick time over the last minute, in seconds |
server_thread:fxserver_tick_time_seconds:mean1m |
server, thread |
mean tick time over the last minute, in seconds |
server_thread:fxserver_tick_time_seconds_count:rate1m |
server, thread |
ticks per second over the last minute (svMain about 20 while its ticks are short) |
- Windows: one minute, so a short spike stays visible and
FXServerTickTimeHigh’s five minutes are five minutes of slow ticks. A one-minute rate needs at least two/perf/scrapes per minute; the pack scrapes every 15 seconds. - Percentiles: interpolated within the bucket the rank falls into. The highest bucket bound
below
+Infis 0.25 s, so a percentile among slower ticks reads 0.25. - Checked:
tests/rules/recording.test.ymlcomputes each series from hand-made histograms, and theexportersrun checks that each one exists per thread on the fake server’s data.
Alerts that are pending or firing also appear as series; the dashboard reads ALERTS for its
alert count, its alert table and its annotations:
| Series | Labels | Meaning |
|---|---|---|
ALERTS |
alertname, alertstate (pending or firing), server, severity and, for some alerts, thread, job or mountpoint |
1 for every alert that is pending or firing |
ALERTS_FOR_STATE |
the same, without alertstate |
the Unix time the alert became active; Prometheus uses it to keep the for wait across a restart |
Series the rules read from other exporters
This pack ships no jobs for these exporters. Scrape them with jobs of your own and give every
target the server label of its game server
(configuration); the alerts group by that label.
| Metric | Exporter | Labels the rules use | Rule |
|---|---|---|---|
node_filesystem_avail_bytes, node_filesystem_size_bytes, node_filesystem_readonly |
node_exporter, filesystem collector (on by default) | server, mountpoint, fstype |
HostDiskAlmostFull |
fxserver_artifact_installed_timestamp_seconds |
node_exporter, textfile collector: fxserver_artifact.prom, written by the fxserver role of the luminare.gameserver collection when fxserver_textfile_dir is set |
server |
FXServerArtifactOld |
fxserver_artifact_build |
the same file | server |
FXServerBuildBehindRecommended |
mysql_up |
mysqld_exporter | server |
MariaDBDown |
mysql_global_status_slow_queries |
mysqld_exporter, global status collector (on by default) | server |
MariaDBSlowQueries |
The two textfile metrics have no labels of their own: fxserver_artifact_build is the build
number the current link points to, fxserver_artifact_installed_timestamp_seconds the Unix time
that link was last replaced. Any other tool can write the same file for the alerts to work.
Series the dashboard reads from other exporters
The dashboard (grafana/dashboards/) reads the series above and these,
from the same jobs of your own plus one for the tunnel connector, each target with the server
label of its game server (configuration). Without a
job, its panels show no data; nothing else depends on them. scripts/lint_dashboards.py fails
when a dashboard query reads a metric that is not in a table of this page.
| Metric | Exporter | Labels the dashboard uses | Panel |
|---|---|---|---|
node_cpu_seconds_total |
node_exporter, cpu collector (on by default) | server, mode (idle, steal) |
CPU |
node_memory_MemAvailable_bytes, node_memory_MemTotal_bytes |
node_exporter, meminfo collector (on by default) | server |
Memory |
node_network_receive_bytes_total, node_network_transmit_bytes_total |
node_exporter, netdev collector (on by default) | server, device (all but lo) |
Network |
mysql_global_status_queries |
mysqld_exporter, global status collector (on by default) | server |
Queries per second |
mysql_global_status_threads_connected, mysql_global_variables_max_connections |
mysqld_exporter, global status and global variables collectors (on by default) | server |
Connections |
mysql_global_status_innodb_buffer_pool_reads, mysql_global_status_innodb_buffer_pool_read_requests |
mysqld_exporter, global status collector | server |
Buffer pool hit ratio |
cloudflared_tunnel_ha_connections |
cloudflared, metrics endpoint (--metrics 127.0.0.1:2000) |
server |
Tunnel connections |
cloudflared_tunnel_total_requests, cloudflared_tunnel_request_errors |
the same | server |
Tunnel requests |
The names and types were read from node_exporter 1.12.1, mysqld_exporter 0.20.0 (with MariaDB
11.8.9) and cloudflared 2026.10.2 on 2026-10-21, in the dashboard check described in the README’s
Results. cloudflared counts an edge connection in cloudflared_tunnel_ha_connections also while
it retries it (seen with an unreachable edge), so the dashboard shows it next to the admin
hostname probe. Prometheus 3 adds an information notice (“metric might not be a counter”) to
rate() over counters whose names do not end in _total, such as mysqld_exporter’s global status
counters and cloudflared’s request counters; Grafana shows it as an icon on the panel, and the
values are right.
Request paranoia and rate limits
From the source of build 35245:
sv_requestParanoia1 refusesinfo.json,dynamic.jsonandplayers.jsonrequests that carry aViaheader (sent by proxies); 2 also refuses requests withUpgrade-Insecure-Requests(sent by browsers). A refused request gets403 Nope.and the server blocks the sending address. json_exporter and blackbox_exporter send neither header, so direct scrapes keep working at every level; a proxy between them and the server would be refused from level 1.info.jsonandplayers.jsonare limited to 4 requests per second (burst 10) per address; this pack requestsinfo.jsontwice per 30 seconds.dynamic.jsonhas no limit. Unauthenticated/perf/requests are limited as described above; authenticated ones are not.
Not collected
players.json, in any form. The player count comes fromdynamic.jsonclients. Without the players token (sv_playersToken) the server serves an anonymous entry per client there, with it names and identifiers; the pack needs neither, so no target, module or rule names that endpoint, andscripts/check_pack.pyfails if one does (ADR 0009).- Any label tied to a player: names, identifiers (license, Steam, Discord and so on), IP
addresses, endpoints, pings or player IDs.
scripts/check_pack.pyrejects such label names in the scrape config, the relabel rules and the exporter modules, and theexportersrun checks the labels that arrive in Prometheus. - Everything else in
info.jsonanddynamic.json: project name and description, host name, tags, locale, other variables, the resource list (one series per resource would break the cardinality budget), the icon and the version hash. - txAdmin’s own status endpoint: its fields have not been checked yet, so there is no module for it (probe log).
- The exporters’ own
/metrics: not scraped by these jobs; add a job of your own if you want the exporters’ process metrics.
Not affiliated with or endorsed by Cfx.re, Rockstar Games or Take-Two.
Licensed under Apache-2.0. Source: luminare-dev/gameserver-observability at v0.1.1.