136 lines
4.9 KiB
Python
136 lines
4.9 KiB
Python
"""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)
|