diff --git a/xonotic_exporter/prometheus.py b/xonotic_exporter/prometheus.py index e499857..95e2a30 100644 --- a/xonotic_exporter/prometheus.py +++ b/xonotic_exporter/prometheus.py @@ -21,44 +21,22 @@ from .metrics_parser import XonoticMetrics # ── Metric definitions (names, help strings, labels) ───────────────────────── _METRIC_DEFS: list[tuple[str, str]] = [ - ("xonotic_up", - "1 if the server was reachable, 0 otherwise"), - - ("xonotic_sv_public", - "Value of sv_public cvar (1 = listed on master server)"), - - ("xonotic_ping_seconds", - "Round-trip time to the server in seconds"), - - ("xonotic_timing_cpu_percent", - "Server CPU usage percentage reported by status"), - - ("xonotic_timing_lost_percent", - "Percentage of packets lost reported by status"), - - ("xonotic_timing_offset_avg_ms", - "Average timing offset in milliseconds"), - - ("xonotic_timing_offset_max_ms", - "Maximum timing offset in milliseconds"), - - ("xonotic_timing_offset_sdev_ms", - "Standard deviation of timing offset in milliseconds"), - - ("xonotic_players_active", - "Number of active (scoring) players"), - - ("xonotic_players_spectators", - "Number of spectators"), - - ("xonotic_players_bots", - "Number of bots"), - - ("xonotic_players_total", - "Total players connected (active + spectators + bots)"), - - ("xonotic_players_max", - "Maximum player slots on the server"), + ("xonotic_up", "1 if the server was reachable, 0 otherwise"), + ("xonotic_sv_public", "Value of sv_public cvar (1 = listed on master server)"), + ("xonotic_ping_seconds", "Round-trip time to the server in seconds"), + ("xonotic_timing_cpu_percent", "Server CPU usage percentage reported by status"), + ("xonotic_timing_lost_percent", "Percentage of packets lost reported by status"), + ("xonotic_timing_offset_avg_ms", "Average timing offset in milliseconds"), + ("xonotic_timing_offset_max_ms", "Maximum timing offset in milliseconds"), + ( + "xonotic_timing_offset_sdev_ms", + "Standard deviation of timing offset in milliseconds", + ), + ("xonotic_players_active", "Number of active (scoring) players"), + ("xonotic_players_spectators", "Number of spectators"), + ("xonotic_players_bots", "Number of bots"), + ("xonotic_players_total", "Total players connected (active + spectators + bots)"), + ("xonotic_players_max", "Maximum player slots on the server"), ] @@ -86,26 +64,42 @@ def build_registry( Additional constant labels to attach to every metric (optional). """ registry = CollectorRegistry(auto_describe=False) - labels = {"instance": server_name, **(extra_labels or {})} + labels = {"instance": server_name, **(extra_labels or {})} def _gauge(name: str, helpstr: str, value: float) -> None: g = Gauge(name, helpstr, labelnames=list(labels), registry=registry) g.labels(**labels).set(value) # always emit "up" - _gauge("xonotic_up", "1 if the server was reachable, 0 otherwise", 1.0 if up else 0.0) + _gauge( + "xonotic_up", "1 if the server was reachable, 0 otherwise", 1.0 if up else 0.0 + ) if metrics is not None and up: - _gauge("xonotic_sv_public", "Value of sv_public cvar", metrics.sv_public) - _gauge("xonotic_ping_seconds", "Round-trip time in seconds", metrics.ping) - _gauge("xonotic_timing_cpu_percent", "Server CPU usage %", metrics.timing_cpu) - _gauge("xonotic_timing_lost_percent", "Packet loss %", metrics.timing_lost) - _gauge("xonotic_timing_offset_avg_ms", "Avg timing offset ms", metrics.timing_offset_avg) - _gauge("xonotic_timing_offset_max_ms", "Max timing offset ms", metrics.timing_offset_max) - _gauge("xonotic_timing_offset_sdev_ms","Timing offset sdev ms", metrics.timing_offset_sdev) - _gauge("xonotic_players_active", "Active (scoring) players", metrics.players_active) - _gauge("xonotic_players_spectators", "Spectators", metrics.players_spectators) - _gauge("xonotic_players_bots", "Bots", metrics.players_bots) + _gauge("xonotic_sv_public", "Value of sv_public cvar", metrics.sv_public) + _gauge("xonotic_ping_seconds", "Round-trip time in seconds", metrics.ping) + _gauge("xonotic_timing_cpu_percent", "Server CPU usage %", metrics.timing_cpu) + _gauge("xonotic_timing_lost_percent", "Packet loss %", metrics.timing_lost) + _gauge( + "xonotic_timing_offset_avg_ms", + "Avg timing offset ms", + metrics.timing_offset_avg, + ) + _gauge( + "xonotic_timing_offset_max_ms", + "Max timing offset ms", + metrics.timing_offset_max, + ) + _gauge( + "xonotic_timing_offset_sdev_ms", + "Timing offset sdev ms", + metrics.timing_offset_sdev, + ) + _gauge( + "xonotic_players_active", "Active (scoring) players", metrics.players_active + ) + _gauge("xonotic_players_spectators", "Spectators", metrics.players_spectators) + _gauge("xonotic_players_bots", "Bots", metrics.players_bots) _gauge( "xonotic_players_total", "Total connected (active + spectators + bots)", @@ -119,6 +113,7 @@ def build_registry( CONTENT_TYPE = CONTENT_TYPE_LATEST + def _safe_label(value: str) -> str: """Keep only printable ASCII characters (32–126).""" r = "".join(c for c in value if 32 <= ord(c) <= 126).strip() @@ -126,10 +121,11 @@ def _safe_label(value: str) -> str: return r return "Anonymous" + def build_player_geo_registry( server_name: str, - players: list, # list[PlayerRow] - geo_results: dict, # dict[ip, GeoResult] + players: list, # list[PlayerRow] + geo_results: dict, # dict[ip, GeoResult] ) -> bytes: """ Emits xonotic_player_info and xonotic_player_geo metrics. @@ -171,9 +167,9 @@ def build_player_geo_registry( continue # private IP — excluded country = geo.country - city = geo.city - lat = str(round(geo.lat, 4)) - lon = str(round(geo.lon, 4)) + city = geo.city + lat = str(round(geo.lat, 4)) + lon = str(round(geo.lon, 4)) info_gauge.labels( instance=server_name, @@ -182,7 +178,7 @@ def build_player_geo_registry( slot=player.slot, name=_safe_label(player.name), country=country, - city=city + city=city, ).set(1) geo_gauge.labels( @@ -193,7 +189,7 @@ def build_player_geo_registry( city=city, lat=lat, lon=lon, - ping=str(player.ping) + ping=str(player.ping), ).set(1) ping_gauge.labels( @@ -210,6 +206,7 @@ def build_player_geo_registry( return generate_latest(registry) + def build_match_registry( server_name: str, metrics: XonoticMetrics, @@ -217,24 +214,30 @@ def build_match_registry( ) -> bytes: """ Emits match-level and per-player metrics for the match dashboard. + + This works but its not great... """ registry = CollectorRegistry(auto_describe=False) - labels = {"instance": server_name} + labels = {"instance": server_name} def _gauge(name, helpstr, value): g = Gauge(name, helpstr, labelnames=list(labels), registry=registry) g.labels(**labels).set(value) # match metadata - _gauge("xonotic_match_timelimit_seconds", - "Match time limit in seconds", - match_meta.get("timelimit", 0) * 60) - _gauge("xonotic_match_fraglimit", - "Frag limit for current match", - match_meta.get("fraglimit", 0)) - _gauge("xonotic_match_teamplay", - "Teamplay mode (0=FFA)", - match_meta.get("teamplay", 0)) + _gauge( + "xonotic_match_timelimit_seconds", + "Match time limit in seconds", + match_meta.get("timelimit", 0) * 60, + ) + _gauge( + "xonotic_match_fraglimit", + "Frag limit for current match", + match_meta.get("fraglimit", 0), + ) + _gauge( + "xonotic_match_teamplay", "Teamplay mode (0=FFA)", match_meta.get("teamplay", 0) + ) # map name as a label on a info metric map_gauge = Gauge( @@ -263,7 +266,7 @@ def build_match_registry( "Player connection stats", labelnames=["instance", "name", "slot", "ping", "packetloss", "time_seconds"], registry=registry, - ) + ) for player in metrics.players: if player.is_bot: @@ -282,6 +285,6 @@ def build_match_registry( ping=str(player.ping), packetloss=str(player.packetloss), time_seconds=str(player.time_seconds), - ).set(1) + ).set(player.time_seconds) return generate_latest(registry)