SPB Git forge

spb/api-ka

Public

API-KA — plateforme centrale : collecte quotidienne des 8 services KA, historisation append-only et API publique sur www.api-ka.com

48commits 1branches 0releases
5.9 MBsize
maindefault branch
19 days agolast push
Python 60.9% HTML 21% TypeScript 7.3% JavaScript 5.2% CSS 4.8% Shell 0.8%

api: HEAD / accepté, endpoint compact GET /api/stats, KPI erreurs sans le bruit des scanners

- main.py : la route racine accepte HEAD (les moniteurs recevaient 405, 633 fois/7 j)
- routes/stats.py : nouveau GET /api/stats (résumé léger, contrat commun KA) —
  les moniteurs qui l appelaient recevaient 404 (1752 fois/7 j)
- routes/stats.py : les endpoints bruit (autre) / api/v1/(autre) — scans de bots —
  sont exclus des métriques du tableau de bord (ils restent journalisés)
- reqstats.py : familles /api/auth|agent|ios|monitoring repliées sous <préfixe>/*

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Simon-Pierre Boucher committed 1 mo ago (Aug 23, 2026) parent a874bb0

3 changed files +68 −1

modified src/api/main.py +1 −0
@@ -111,6 +111,7 @@ if KA_DIR.is_dir():
111 111
112 112
113 113 @app.get("/", tags=["root"])
114 +@app.head("/", include_in_schema=False)
114 115 def root(request: Request) -> Any:
115 116 """Statut de la plateforme + version.
116 117
modified src/api/reqstats.py +8 −0
@@ -49,6 +49,7 @@ _KNOWN_PATHS = frozenset(
49 49 "/apple-touch-icon.png",
50 50 "/og.png",
51 51 "/api/v1/runs",
52 + "/api/stats",
52 53 "/api/stats/dashboard",
53 54 "/api/stats/report",
54 55 "/api/stats/ecosystem-report",
@@ -56,6 +57,10 @@ _KNOWN_PATHS = frozenset(
56 57 )
57 58 _SERVICE_SUBROUTES = frozenset({"latest", "stats"})
58 59
60 +# Familles de routes API repliées sous « <préfixe>/* » (auth, agent, iOS,
61 +# monitoring) : cardinalité bornée sans les noyer dans « (autre) ».
62 +_API_FAMILIES = ("/api/auth", "/api/agent", "/api/ios", "/api/monitoring")
63 +
59 64
60 65 def normalize_endpoint(path: str) -> str:
61 66 """Replie un chemin de requête vers un endpoint à cardinalité bornée.
@@ -81,6 +86,9 @@ def normalize_endpoint(path: str) -> str:
81 86 if sub in _SERVICE_SUBROUTES and len(parts) == 5:
82 87 return f"/api/v1/{service}/{sub}"
83 88 return "/api/v1/(autre)"
89 + for family in _API_FAMILIES:
90 + if path == family or path.startswith(family + "/"):
91 + return f"{family}/*"
84 92 return "(autre)"
85 93
86 94
modified src/api/routes/stats.py +59 −1
@@ -177,6 +177,13 @@ def _utc_bounds(
177 177
178 178
179 179 # ---------------------------------------------------------------- agrégats
180 +# Endpoints « bruit » exclus des métriques du tableau de bord : chemins
181 +# inconnus sondés par les scanners/bots (404, .env, etc.). Ils restent
182 +# journalisés dans api_requests pour la visibilité sécurité, mais ne doivent
183 +# pas polluer le taux d'erreur qui mesure la santé réelle de l'API.
184 +NOISE_ENDPOINTS = ("(autre)", "/api/v1/(autre)")
185 +
186 +
180 187 def _fetch_requests(
181 188 db: Session, d_from: datetime.date, d_to: datetime.date
182 189 ) -> list[tuple[datetime.datetime, str, str, int, float]]:
@@ -189,7 +196,11 @@ def _fetch_requests(
189 196 ApiRequest.endpoint,
190 197 ApiRequest.status,
191 198 ApiRequest.duration_ms,
192 − ).where(ApiRequest.ts >= lo, ApiRequest.ts < hi)
199 + ).where(
200 + ApiRequest.ts >= lo,
201 + ApiRequest.ts < hi,
202 + ApiRequest.endpoint.notin_(NOISE_ENDPOINTS),
203 + )
193 204 ).all()
194 205 out = []
195 206 for ts, method, endpoint, status, duration in rows:
@@ -1138,6 +1149,53 @@ def _normalize_mode(mode: str | None) -> str:
1138 1149
1139 1150
1140 1151 # ---------------------------------------------------------------- routes
1152 +@router.get("")
1153 +def stats_summary(db: Session = Depends(get_db)) -> dict[str, Any]:
1154 + """Résumé compact de la plateforme (contrat léger /api/stats commun KA).
1155 +
1156 + Version allégée du tableau de bord pour les moniteurs et les apps sœurs :
1157 + activité API 7 jours, runs de collecte, dernier run par service et santé
1158 + des connecteurs supervisés.
1159 + """
1160 + today = datetime.datetime.now(TZ).date()
1161 + d_from = today - datetime.timedelta(days=6)
1162 + reqs = _fetch_requests(db, d_from, today)
1163 + runs = _fetch_runs(db, d_from, today)
1164 + health = _fetch_health(db)
1165 + errors = sum(1 for r in reqs if r[3] >= 400)
1166 + ok_runs = [r for r in runs if r.status in ("success", "retried")]
1167 + last_run: dict[str, Any] = {}
1168 + for r in runs: # triés par started_at décroissant
1169 + if r.service not in last_run:
1170 + last_run[r.service] = {
1171 + "date": r.date_key.isoformat(),
1172 + "status": r.status,
1173 + "records": r.records_count,
1174 + }
1175 + connectors = dict.fromkeys(("ok", "degraded", "broken", "stale"), 0)
1176 + for h in health:
1177 + if h.status in connectors:
1178 + connectors[h.status] += 1
1179 + return envelope(
1180 + {
1181 + "platform": "API-KA",
1182 + "period": {"from": d_from.isoformat(), "to": today.isoformat()},
1183 + "api": {
1184 + "calls_7d": len(reqs),
1185 + "error_rate_7d": round(errors / len(reqs) * 100, 2) if reqs else 0.0,
1186 + },
1187 + "runs_7d": {
1188 + "total": len(runs),
1189 + "ok": len(ok_runs),
1190 + "failed": sum(1 for r in runs if r.status == "failed"),
1191 + "records": sum(r.records_count for r in ok_runs),
1192 + },
1193 + "last_run": last_run,
1194 + "connectors": connectors,
1195 + }
1196 + )
1197 +
1198 +
1141 1199 @router.get("/dashboard")
1142 1200 def stats_dashboard(
1143 1201 period: str = Query("30j", description="auj, 7j, 30j, 3m, 6m, 12m, annee, tout"),
1144 1202