feat(api): роутеры API v1 и WebSocket-поток метрик (Refs #19)
- ops: health/ready/metrics без аутентификации (503 = схема Error). - auth: login/logout/session, cookie-сессия + CSRF-токен. - status/config/routing/devices+logs: все эндпоинты контракта. - ws/metrics: /api/v1/metrics/stream, токен в заголовке или query.
This commit is contained in:
@@ -0,0 +1,47 @@
|
||||
"""WebSocket-поток метрик: GET /api/v1/metrics/stream."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
|
||||
from fastapi import APIRouter, WebSocket, WebSocketDisconnect
|
||||
|
||||
from awg_api.deps import SESSION_COOKIE_NAME, token_matches
|
||||
from awg_api.state import MockState
|
||||
|
||||
router = APIRouter(tags=["metrics"])
|
||||
|
||||
WS_ACCESS_DENIED = 4401
|
||||
MIN_INTERVAL_MS = 250
|
||||
|
||||
|
||||
def _authorized(websocket: WebSocket, token: str | None) -> bool:
|
||||
expected: str = websocket.app.state.api_token
|
||||
header = websocket.headers.get("authorization", "")
|
||||
scheme, _, credentials = header.partition(" ")
|
||||
if scheme.lower() == "bearer" and token_matches(credentials.strip(), expected):
|
||||
return True
|
||||
if token_matches(token, expected):
|
||||
return True
|
||||
session_id = websocket.cookies.get(SESSION_COOKIE_NAME)
|
||||
state: MockState = websocket.app.state.mock_state
|
||||
return token_matches(session_id, state.session_token)
|
||||
|
||||
|
||||
@router.websocket("/api/v1/metrics/stream")
|
||||
async def stream_metrics(
|
||||
websocket: WebSocket, token: str | None = None, interval_ms: int = 1000
|
||||
) -> None:
|
||||
"""Отправляет кадры ``WsFrame``: метрики до отключения клиента."""
|
||||
if not _authorized(websocket, token):
|
||||
await websocket.close(code=WS_ACCESS_DENIED)
|
||||
return
|
||||
state: MockState = websocket.app.state.mock_state
|
||||
await websocket.accept()
|
||||
interval_s = max(MIN_INTERVAL_MS, interval_ms) / 1000
|
||||
try:
|
||||
while True:
|
||||
await websocket.send_json(state.metrics_frame())
|
||||
await asyncio.sleep(interval_s)
|
||||
except (WebSocketDisconnect, RuntimeError):
|
||||
return
|
||||
Reference in New Issue
Block a user