File size: 8,172 Bytes
6993919 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 | """
RMI Analytics API Router
==========================
REST API for real-time analytics and dashboard data.
Endpoints:
GET /api/v1/analytics/metrics β List all metrics
GET /api/v1/analytics/metrics/{name} β Get metric data
POST /api/v1/analytics/metrics/{name} β Record metric
GET /api/v1/analytics/dashboards β List dashboards
GET /api/v1/analytics/dashboards/{id} β Get dashboard data
POST /api/v1/analytics/dashboards β Create dashboard
GET /api/v1/analytics/trends/{metric} β Get trend analysis
GET /api/v1/analytics/stats β System stats
GET /api/v1/analytics/prometheus β Prometheus export
GET /api/v1/analytics/export/{metric} β Export metric
GET /api/v1/analytics/realtime/{dashboard} β WebSocket-compatible data
"""
import logging
import os
import time
from typing import Any
from fastapi import APIRouter, HTTPException, Query, Request
from pydantic import BaseModel, Field
from app.admin_backend import AuditLogger, require_admin
from app.analytics_engine import AnalyticsEngine, DashboardWidget, get_analytics_engine
logger = logging.getLogger("analytics_api")
router = APIRouter(prefix="/api/v1/analytics", tags=["analytics"])
# ββ Models ββββββββββββββββββββββββββββββββββββββββββββββββββββ
class RecordMetricRequest(BaseModel):
value: float
labels: dict[str, str] | None = None
class CreateDashboardRequest(BaseModel):
name: str
description: str = ""
class AddWidgetRequest(BaseModel):
widget_type: str = Field(..., description="line, bar, gauge, counter, table, pie")
title: str
metric_name: str
width: int = 6
height: int = 4
refresh_interval: int = 30
config: dict[str, Any] | None = None
# ββ Helper ββββββββββββββββββββββββββββββββββββββββββββββββββββ
def _get_engine() -> AnalyticsEngine:
return get_analytics_engine()
# ββ Endpoints βββββββββββββββββββββββββββββββββββββββββββββββββ
@router.get("/metrics")
async def list_metrics(request: Request):
"""List all tracked metrics."""
await require_admin(request, "analytics.read")
engine = _get_engine()
metrics = []
for name in engine.get_metric_names():
metric = engine.get_metric(name)
if metric:
metrics.append(metric.to_dict())
return {"metrics": metrics, "total": len(metrics)}
@router.get("/metrics/{name}")
async def get_metric(request: Request, name: str, limit: int = 100):
"""Get metric data points."""
await require_admin(request, "analytics.read")
engine = _get_engine()
metric = engine.get_metric(name)
if not metric:
raise HTTPException(status_code=404, detail="Metric not found")
points = [{"timestamp": p.timestamp, "value": p.value, "labels": p.labels} for p in metric.points[-limit:]]
return {
"name": metric.name,
"description": metric.description,
"unit": metric.unit,
"latest": metric.latest(),
"avg_1m": metric.avg(60),
"trend": metric.trend(),
"points": points,
}
@router.post("/metrics/{name}")
async def record_metric(request: Request, name: str, body: RecordMetricRequest):
"""Record a metric data point."""
# Allow public recording for system metrics (from middleware)
engine = _get_engine()
engine.record_metric(name, body.value, body.labels or {})
return {"success": True}
@router.get("/dashboards")
async def list_dashboards(request: Request):
"""List all dashboards."""
await require_admin(request, "analytics.read")
engine = _get_engine()
dashboards = engine.list_dashboards()
return {
"dashboards": [
{
"dashboard_id": d.dashboard_id,
"name": d.name,
"description": d.description,
"widget_count": len(d.widgets),
"is_default": d.is_default,
}
for d in dashboards
]
}
@router.get("/dashboards/{dashboard_id}")
async def get_dashboard_data(request: Request, dashboard_id: str):
"""Get current data for a dashboard."""
await require_admin(request, "analytics.read")
engine = _get_engine()
data = engine.get_dashboard_data(dashboard_id)
if "error" in data:
raise HTTPException(status_code=404, detail=data["error"])
return data
@router.post("/dashboards")
async def create_dashboard(request: Request, body: CreateDashboardRequest):
"""Create a new dashboard."""
auth = await require_admin(request, "analytics.read")
admin = auth["admin"]
engine = _get_engine()
dashboard = engine.create_dashboard(body.name, body.description, admin["id"])
await AuditLogger.log(
admin_id=admin["id"],
admin_email=admin["email"],
action="dashboard.create",
resource_type="dashboard",
resource_id=dashboard.dashboard_id,
ip_address=request.client.host if request.client else "",
user_agent=request.headers.get("user-agent", ""),
)
return {
"success": True,
"dashboard": {"dashboard_id": dashboard.dashboard_id, "name": dashboard.name},
}
@router.post("/dashboards/{dashboard_id}/widgets")
async def add_widget(request: Request, dashboard_id: str, body: AddWidgetRequest):
"""Add widget to dashboard."""
auth = await require_admin(request, "analytics.read")
auth["admin"]
engine = _get_engine()
widget = DashboardWidget(
widget_id=f"wid_{int(time.time())}_{os.urandom(4).hex()}",
widget_type=body.widget_type,
title=body.title,
metric_name=body.metric_name,
width=body.width,
height=body.height,
refresh_interval=body.refresh_interval,
config=body.config or {},
)
result = engine.add_widget(dashboard_id, widget)
if not result:
raise HTTPException(status_code=404, detail="Dashboard not found")
return {"success": True, "widget": {"widget_id": widget.widget_id, "title": widget.title}}
@router.get("/trends/{metric_name}")
async def get_trends(request: Request, metric_name: str, window: int = 60):
"""Get trend analysis for a metric."""
await require_admin(request, "analytics.read")
engine = _get_engine()
trends = engine.detect_trends(metric_name, window)
return trends
@router.get("/stats")
async def get_stats(request: Request):
"""Get analytics system statistics."""
await require_admin(request, "analytics.read")
engine = _get_engine()
return engine.get_system_stats()
@router.get("/prometheus")
async def prometheus_export(request: Request):
"""Export metrics in Prometheus format."""
# Public endpoint for Prometheus scraping
engine = _get_engine()
return engine.to_prometheus()
@router.get("/export/{metric_name}")
async def export_metric(
request: Request,
metric_name: str,
format: str = Query("json", regex="^(json|csv)$"),
):
"""Export metric data."""
await require_admin(request, "analytics.read")
engine = _get_engine()
data = engine.export_metric(metric_name, format)
if data is None:
raise HTTPException(status_code=404, detail="Metric not found")
if format == "csv":
return {"data": data, "format": "csv"}
return data
@router.get("/realtime/{dashboard_id}")
async def realtime_data(request: Request, dashboard_id: str):
"""Get real-time dashboard data (WebSocket-compatible)."""
await require_admin(request, "analytics.read")
engine = _get_engine()
data = engine.get_dashboard_data(dashboard_id)
if "error" in data:
raise HTTPException(status_code=404, detail=data["error"])
# Add timestamp for client sync
data["server_time"] = time.time()
data["refresh_interval"] = 30
return data
|