"""Minimal sysmonstm gateway - standalone mode without dependencies.""" import asyncio import json import logging import os from datetime import datetime from fastapi import FastAPI, Query, WebSocket, WebSocketDisconnect from fastapi.responses import HTMLResponse # Configuration API_KEY = os.environ.get("API_KEY", "") LOG_LEVEL = os.environ.get("LOG_LEVEL", "INFO") # Logging setup logging.basicConfig( level=getattr(logging, LOG_LEVEL.upper(), logging.INFO), format="%(asctime)s [%(levelname)s] %(message)s", datefmt="%Y-%m-%d %H:%M:%S", ) log = logging.getLogger("gateway") app = FastAPI(title="sysmonstm") # Store connected websockets connections: list[WebSocket] = [] # Store latest metrics from collectors machines: dict = {} HTML = """ System Monitor Dashboard

System Monitor

Connecting...

No machines connected

Waiting for collectors to send metrics...

""" @app.get("/", response_class=HTMLResponse) async def index(): return HTML @app.get("/health") async def health(): return {"status": "ok", "machines": len(machines)} @app.get("/api/machines") async def get_machines(): return machines @app.websocket("/ws") async def websocket_endpoint(websocket: WebSocket, key: str = Query(default="")): # API key validation for collectors (browsers don't need key) # Check if this looks like a collector (will send metrics) or browser (will receive) # We validate key only when metrics are received, allowing browsers to connect freely await websocket.accept() connections.append(websocket) client = websocket.client.host if websocket.client else "unknown" log.info(f"WebSocket connected: {client}") try: # Send current state to new connection for machine_id, data in machines.items(): await websocket.send_json( {"type": "metrics", "machine_id": machine_id, **data} ) # Main loop while True: try: msg = await asyncio.wait_for(websocket.receive_text(), timeout=30) data = json.loads(msg) if data.get("type") == "metrics": # Validate API key for metric submissions if API_KEY and key != API_KEY: log.warning(f"Invalid API key from {client}") await websocket.close(code=4001, reason="Invalid API key") return # Handle both formats: # 1. Direct: {"type": "metrics", "machine_id": "...", "cpu": ...} # 2. Nested (from gateway): {"type": "metrics", "data": {...}, "timestamp": "..."} if "data" in data and isinstance(data["data"], dict): # Nested format from gateway forwarding payload = data["data"] machine_id = payload.get("machine_id", "unknown") # Extract metrics from nested structure metrics = payload.get("metrics", {}) metric_data = { "type": "metrics", "machine_id": machine_id, "hostname": payload.get("hostname", ""), "timestamp": data.get("timestamp"), } # Flatten metrics for dashboard display for key_name, value in metrics.items(): metric_data[key_name.lower()] = value machines[machine_id] = metric_data log.debug(f"Metrics (forwarded) from {machine_id}") else: # Direct format from collector machine_id = data.get("machine_id", "unknown") machines[machine_id] = data log.debug(f"Metrics from {machine_id}: cpu={data.get('cpu')}%") # Broadcast to all connected clients broadcast_data = machines[machine_id] for conn in connections: try: await conn.send_json(broadcast_data) except Exception: pass except asyncio.TimeoutError: # Send ping to keep connection alive await websocket.send_json({"type": "ping"}) except WebSocketDisconnect: log.info(f"WebSocket disconnected: {client}") except Exception as e: log.error(f"WebSocket error: {e}") finally: if websocket in connections: connections.remove(websocket) if __name__ == "__main__": import uvicorn log.info("Starting sysmonstm gateway") log.info(f" API key: {'configured' if API_KEY else 'not set (open)'}") uvicorn.run(app, host="0.0.0.0", port=8080)