Init
This commit is contained in:
@@ -0,0 +1,276 @@
|
||||
"""
|
||||
CLI entry point for xonotic-exporter.
|
||||
|
||||
Sub-commands
|
||||
────────────
|
||||
serve Start the HTTP exporter (default when no sub-command given).
|
||||
query Scrape one server and print results (human-readable or raw Prometheus).
|
||||
validate Check a config file for errors and exit.
|
||||
|
||||
Examples
|
||||
────────
|
||||
# Start the exporter
|
||||
xonotic-exporter serve /etc/xonotic_exporter/config.toml
|
||||
|
||||
# Query a server defined in a config file
|
||||
xonotic-exporter query vehicles --config /etc/xonotic_exporter/config.toml
|
||||
|
||||
# Query a server ad-hoc (no config file needed)
|
||||
xonotic-exporter query --host localhost --port 26010 \\
|
||||
--password secret --mode 2
|
||||
|
||||
# Same but print raw Prometheus exposition format
|
||||
xonotic-exporter query vehicles --config config.toml --prometheus
|
||||
|
||||
# Validate config only
|
||||
xonotic-exporter validate /etc/xonotic_exporter/config.toml
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
import logging
|
||||
import sys
|
||||
from typing import Optional
|
||||
|
||||
from .config import (
|
||||
ConfigError,
|
||||
ExporterConfig,
|
||||
ServerConfig,
|
||||
RCON_MODE_NAMES,
|
||||
RCON_MODE_SECURE_CHALLENGE,
|
||||
load_config,
|
||||
)
|
||||
from .metrics_parser import XonoticMetrics
|
||||
from .prometheus import build_registry
|
||||
from .rcon import RconError, scrape_server
|
||||
from .server import run_server
|
||||
|
||||
|
||||
# ── Logging setup ──────────────────────────────────────────────────────────────
|
||||
|
||||
def _setup_logging(verbose: bool) -> None:
|
||||
level = logging.DEBUG if verbose else logging.INFO
|
||||
logging.basicConfig(
|
||||
format="%(asctime)s %(levelname)-8s %(name)s: %(message)s",
|
||||
level=level,
|
||||
)
|
||||
|
||||
|
||||
# ── Human-readable output ──────────────────────────────────────────────────────
|
||||
|
||||
_SEP = "─" * 52
|
||||
|
||||
def _print_human(server_name: str, metrics: XonoticMetrics) -> None:
|
||||
mode_label = RCON_MODE_NAMES.get(0, "unknown") # filled per-server in query
|
||||
|
||||
ping_ms = metrics.ping * 1000
|
||||
|
||||
print(_SEP)
|
||||
print(f" Server : {server_name}")
|
||||
print(f" Hostname : {metrics.hostname}")
|
||||
print(f" Map : {metrics.map_name}")
|
||||
print(f" Public : {'yes' if metrics.sv_public > 0 else 'no'} (sv_public={metrics.sv_public})")
|
||||
print(_SEP)
|
||||
print(f" Ping : {ping_ms:.1f} ms")
|
||||
print(f" CPU : {metrics.timing_cpu:.1f} %")
|
||||
print(f" Lost : {metrics.timing_lost:.1f} %")
|
||||
print(f" Offset : avg={metrics.timing_offset_avg:.2f}ms "
|
||||
f"max={metrics.timing_offset_max:.2f}ms "
|
||||
f"sdev={metrics.timing_offset_sdev:.2f}ms")
|
||||
print(_SEP)
|
||||
total = metrics.players_active + metrics.players_spectators + metrics.players_bots
|
||||
print(f" Players : {total}/{metrics.players_max}")
|
||||
print(f" Active : {metrics.players_active}")
|
||||
print(f" Spectators : {metrics.players_spectators}")
|
||||
print(f" Bots : {metrics.players_bots}")
|
||||
print(_SEP)
|
||||
|
||||
|
||||
def _print_prometheus(server_name: str, metrics: XonoticMetrics) -> None:
|
||||
_, raw = build_registry(server_name, metrics, up=True)
|
||||
sys.stdout.buffer.write(raw)
|
||||
|
||||
|
||||
# ── Sub-command: serve ────────────────────────────────────────────────────────
|
||||
|
||||
def _cmd_serve(args: argparse.Namespace) -> None:
|
||||
try:
|
||||
cfg = load_config(args.config)
|
||||
except ConfigError as exc:
|
||||
print(f"Error: {exc}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
_setup_logging(args.verbose)
|
||||
|
||||
# CLI overrides for listen address / port
|
||||
if args.host:
|
||||
cfg.host = args.host
|
||||
if args.port:
|
||||
cfg.port = args.port
|
||||
|
||||
run_server(cfg, config_path=args.config)
|
||||
|
||||
|
||||
# ── Sub-command: query ────────────────────────────────────────────────────────
|
||||
|
||||
def _cmd_query(args: argparse.Namespace) -> None:
|
||||
_setup_logging(args.verbose)
|
||||
|
||||
# Build a ServerConfig — either from config file or ad-hoc flags
|
||||
server: ServerConfig
|
||||
|
||||
if args.config and args.target:
|
||||
# named server from config file
|
||||
try:
|
||||
cfg = load_config(args.config)
|
||||
except ConfigError as exc:
|
||||
print(f"Config error: {exc}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
server = cfg.get_server(args.target)
|
||||
if server is None:
|
||||
print(
|
||||
f"Server {args.target!r} not found in config. "
|
||||
f"Available: {cfg.server_names()}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
elif args.host:
|
||||
# ad-hoc connection from CLI flags
|
||||
name = args.target or f"{args.host}:{args.port}"
|
||||
try:
|
||||
server = ServerConfig(
|
||||
name=name,
|
||||
host=args.host,
|
||||
port=args.port,
|
||||
rcon_password=args.password or "",
|
||||
rcon_mode=args.mode,
|
||||
)
|
||||
except ConfigError as exc:
|
||||
print(f"Invalid arguments: {exc}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
else:
|
||||
print(
|
||||
"Error: provide either --config + target name, or --host (with optional flags).",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
# Run the scrape
|
||||
try:
|
||||
metrics = asyncio.run(scrape_server(server, retries=args.retries, timeout=args.timeout))
|
||||
except RconError as exc:
|
||||
print(f"Scrape failed: {exc}", file=sys.stderr)
|
||||
sys.exit(2)
|
||||
|
||||
rcon_mode_label = RCON_MODE_NAMES.get(server.rcon_mode, str(server.rcon_mode))
|
||||
|
||||
if args.prometheus:
|
||||
_print_prometheus(server.name, metrics)
|
||||
else:
|
||||
print(f"\n RCON mode : {rcon_mode_label}")
|
||||
_print_human(server.name, metrics)
|
||||
|
||||
|
||||
# ── Sub-command: validate ─────────────────────────────────────────────────────
|
||||
|
||||
def _cmd_validate(args: argparse.Namespace) -> None:
|
||||
_setup_logging(False)
|
||||
try:
|
||||
cfg = load_config(args.config)
|
||||
except ConfigError as exc:
|
||||
print(f"Invalid: {exc}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
print(f"OK — {len(cfg.servers)} server(s) configured:")
|
||||
for s in cfg.servers:
|
||||
mode_label = RCON_MODE_NAMES.get(s.rcon_mode, str(s.rcon_mode))
|
||||
print(f" {s.name:<20} {s.host}:{s.port} rcon_mode={s.rcon_mode} ({mode_label})")
|
||||
|
||||
|
||||
# ── Argument parser ───────────────────────────────────────────────────────────
|
||||
|
||||
def _build_parser() -> argparse.ArgumentParser:
|
||||
root = argparse.ArgumentParser(
|
||||
prog="xonotic-exporter",
|
||||
description="Xonotic Prometheus exporter — multi-server, TOML config",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
root.add_argument("-v", "--verbose", action="store_true", help="Debug logging")
|
||||
|
||||
sub = root.add_subparsers(dest="command", metavar="<command>")
|
||||
|
||||
# ── serve ──────────────────────────────────────────────────────────────────
|
||||
p_serve = sub.add_parser("serve", help="Start the HTTP exporter")
|
||||
p_serve.add_argument("config", metavar="CONFIG.toml", help="Path to TOML config file")
|
||||
p_serve.add_argument("-l", "--host", default=None, help="Override listen host")
|
||||
p_serve.add_argument("-p", "--port", type=int, default=None, help="Override listen port")
|
||||
|
||||
# ── query ──────────────────────────────────────────────────────────────────
|
||||
p_query = sub.add_parser(
|
||||
"query",
|
||||
help="Scrape a server and print metrics (testing)",
|
||||
description=(
|
||||
"Scrape one server.\n\n"
|
||||
"Use a name from a config file:\n"
|
||||
" xonotic-exporter query vehicles --config config.toml\n\n"
|
||||
"Or specify connection details ad-hoc:\n"
|
||||
" xonotic-exporter query --host localhost --port 26010 --password s3cr3t --mode 2"
|
||||
),
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
p_query.add_argument(
|
||||
"target", nargs="?", metavar="SERVER_NAME",
|
||||
help="Name of the server as defined in config (requires --config)"
|
||||
)
|
||||
p_query.add_argument("--config", metavar="CONFIG.toml", help="TOML config file")
|
||||
# ad-hoc flags
|
||||
p_query.add_argument("--host", help="Server hostname or IP")
|
||||
p_query.add_argument("--port", type=int, default=26000, help="Server port (default 26000)")
|
||||
p_query.add_argument("--password", default="", help="RCON password")
|
||||
p_query.add_argument(
|
||||
"--mode", type=int, default=RCON_MODE_SECURE_CHALLENGE,
|
||||
choices=list(RCON_MODE_NAMES),
|
||||
help=(
|
||||
"RCON mode: "
|
||||
+ ", ".join(f"{k}={v}" for k, v in RCON_MODE_NAMES.items())
|
||||
+ f" (default {RCON_MODE_SECURE_CHALLENGE})"
|
||||
),
|
||||
)
|
||||
p_query.add_argument(
|
||||
"--prometheus", action="store_true",
|
||||
help="Output raw Prometheus exposition format instead of human-readable",
|
||||
)
|
||||
p_query.add_argument("--retries", type=int, default=3, help="Scrape retry count (default 3)")
|
||||
p_query.add_argument("--timeout", type=float, default=5.0, help="Per-attempt timeout seconds (default 5)")
|
||||
|
||||
# ── validate ───────────────────────────────────────────────────────────────
|
||||
p_val = sub.add_parser("validate", help="Validate a config file and exit")
|
||||
p_val.add_argument("config", metavar="CONFIG.toml")
|
||||
|
||||
return root
|
||||
|
||||
|
||||
# ── Entry point ───────────────────────────────────────────────────────────────
|
||||
|
||||
def main(argv: Optional[list[str]] = None) -> None:
|
||||
parser = _build_parser()
|
||||
args = parser.parse_args(argv)
|
||||
|
||||
if args.command is None:
|
||||
parser.print_help()
|
||||
sys.exit(0)
|
||||
|
||||
dispatch = {
|
||||
"serve": _cmd_serve,
|
||||
"query": _cmd_query,
|
||||
"validate": _cmd_validate,
|
||||
}
|
||||
dispatch[args.command](args)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user