"""Minimal sysmonstm gateway - standalone mode without dependencies.""" import asyncio import json import logging import os from pathlib import Path from fastapi import FastAPI, Query, WebSocket, WebSocketDisconnect from fastapi.requests import Request from fastapi.responses import HTMLResponse from fastapi.templating import Jinja2Templates # 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") # Templates templates_path = Path(__file__).parent / "templates" templates = Jinja2Templates(directory=str(templates_path)) # Store connected websockets connections: list[WebSocket] = [] # Store latest metrics from collectors machines: dict = {} @app.get("/", response_class=HTMLResponse) async def index(request: Request): return templates.TemplateResponse("dashboard.html", {"request": request}) @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) # 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)