"""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)