113 lines
4.0 KiB
Python
113 lines
4.0 KiB
Python
|
|
#!/usr/bin/env python3
|
||
|
|
"""Prometheus exporter for the labsim connectivity matrix.
|
||
|
|
|
||
|
|
Runs the same sweep as labsim-matrix.py on an interval and exposes it as
|
||
|
|
metrics, so Grafana can show the mesh as a heatmap and — more usefully — a
|
||
|
|
history of exactly when a cell flipped after a firewall change.
|
||
|
|
|
||
|
|
labsim_reachable{src,dst,proto} 1 = reachable, 0 = blocked
|
||
|
|
labsim_sweep_seconds how long the last sweep took
|
||
|
|
labsim_sweep_total sweeps completed since start
|
||
|
|
labsim_up 1 while the exporter is alive
|
||
|
|
|
||
|
|
Deliberately stdlib-only (http.server + threads): this runs on the workstation
|
||
|
|
next to libvirt, and adding a dependency to watch a lab network is silly.
|
||
|
|
|
||
|
|
./labsim-exporter.py --port 9101 --interval 15
|
||
|
|
"""
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
import argparse
|
||
|
|
import http.server
|
||
|
|
import threading
|
||
|
|
import time
|
||
|
|
|
||
|
|
import labsim_matrix_lib as m # thin import shim, see below
|
||
|
|
|
||
|
|
|
||
|
|
class Collector:
|
||
|
|
def __init__(self, interval: int, timeout: int) -> None:
|
||
|
|
self.interval = interval
|
||
|
|
self.timeout = timeout
|
||
|
|
self.vlans = m.load_vlans()
|
||
|
|
self.lock = threading.Lock()
|
||
|
|
self.results: dict = {}
|
||
|
|
self.duration = 0.0
|
||
|
|
self.sweeps = 0
|
||
|
|
|
||
|
|
def loop(self) -> None:
|
||
|
|
while True:
|
||
|
|
started = time.time()
|
||
|
|
try:
|
||
|
|
results = m.sweep(self.vlans, self.timeout)
|
||
|
|
with self.lock:
|
||
|
|
self.results = results
|
||
|
|
self.duration = time.time() - started
|
||
|
|
self.sweeps += 1
|
||
|
|
except Exception: # noqa: BLE001 - never let the loop die
|
||
|
|
pass
|
||
|
|
time.sleep(max(1.0, self.interval - (time.time() - started)))
|
||
|
|
|
||
|
|
def render(self) -> str:
|
||
|
|
with self.lock:
|
||
|
|
results, duration, sweeps = dict(self.results), self.duration, self.sweeps
|
||
|
|
|
||
|
|
out = [
|
||
|
|
"# HELP labsim_reachable 1 if dst is reachable from src over proto",
|
||
|
|
"# TYPE labsim_reachable gauge",
|
||
|
|
]
|
||
|
|
for src, data in results.items():
|
||
|
|
if "__error__" in data:
|
||
|
|
continue
|
||
|
|
for dst, protos in data.items():
|
||
|
|
for proto, ok in protos.items():
|
||
|
|
out.append(
|
||
|
|
f'labsim_reachable{{src="{src}",dst="{dst}",proto="{proto}"}} {1 if ok else 0}')
|
||
|
|
out += [
|
||
|
|
"# HELP labsim_sweep_seconds duration of the last sweep",
|
||
|
|
"# TYPE labsim_sweep_seconds gauge",
|
||
|
|
f"labsim_sweep_seconds {duration:.3f}",
|
||
|
|
"# HELP labsim_sweep_total sweeps completed",
|
||
|
|
"# TYPE labsim_sweep_total counter",
|
||
|
|
f"labsim_sweep_total {sweeps}",
|
||
|
|
"# HELP labsim_up exporter liveness",
|
||
|
|
"# TYPE labsim_up gauge",
|
||
|
|
"labsim_up 1",
|
||
|
|
]
|
||
|
|
return "\n".join(out) + "\n"
|
||
|
|
|
||
|
|
|
||
|
|
def main() -> int:
|
||
|
|
ap = argparse.ArgumentParser()
|
||
|
|
ap.add_argument("--port", type=int, default=9101)
|
||
|
|
ap.add_argument("--interval", type=int, default=15)
|
||
|
|
ap.add_argument("--timeout", type=int, default=30)
|
||
|
|
args = ap.parse_args()
|
||
|
|
|
||
|
|
collector = Collector(args.interval, args.timeout)
|
||
|
|
threading.Thread(target=collector.loop, daemon=True).start()
|
||
|
|
|
||
|
|
class Handler(http.server.BaseHTTPRequestHandler):
|
||
|
|
def do_GET(self) -> None: # noqa: N802 - stdlib API
|
||
|
|
if self.path.rstrip("/") not in ("", "/metrics"):
|
||
|
|
self.send_error(404)
|
||
|
|
return
|
||
|
|
body = collector.render().encode()
|
||
|
|
self.send_response(200)
|
||
|
|
self.send_header("Content-Type", "text/plain; version=0.0.4")
|
||
|
|
self.send_header("Content-Length", str(len(body)))
|
||
|
|
self.end_headers()
|
||
|
|
self.wfile.write(body)
|
||
|
|
|
||
|
|
def log_message(self, *_args) -> None: # keep the console quiet
|
||
|
|
return
|
||
|
|
|
||
|
|
srv = http.server.ThreadingHTTPServer(("0.0.0.0", args.port), Handler)
|
||
|
|
print(f"labsim exporter on :{args.port}/metrics (sweep every {args.interval}s)")
|
||
|
|
srv.serve_forever()
|
||
|
|
return 0
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
raise SystemExit(main())
|