API-KA — plateforme centrale : collecte quotidienne des 8 services KA, historisation append-only et API publique sur www.api-ka.com
Python 60.9%
HTML 21%
TypeScript 7.3%
JavaScript 5.2%
CSS 4.8%
Shell 0.8%
1# ============================================2# Projet : API-KA3# Fichier : scripts/backfill_api_requests.py4# Node : m3u96b5# Author : Simon-Pierre Boucher6# Contact : contact@spboucher.ai7# Date : 2026-08-178# ============================================9"""Rattrapage : importe l'historique des requêtes HTTP déjà journalisées dans10``logs/apika.log*`` (lignes JSON ``http_request`` du middleware) vers la table11``api_requests`` qui alimente la page /stats.1213Idempotent : seules les lignes STRICTEMENT antérieures au plus ancien14enregistrement déjà présent dans ``api_requests`` sont insérées (à la première15exécution, tout l'historique des logs est repris ; aux suivantes, rien).1617Usage : venv/bin/python -m scripts.backfill_api_requests18"""1920from __future__ import annotations2122import datetime23import json2425from sqlalchemy import func, select2627from src.api.reqstats import normalize_endpoint28from src.config import get_settings29from src.database.db import init_db, session_scope30from src.database.models import ApiRequest31from src.utils.logger import get_logger323334def main() -> None:35 logger = get_logger("apika.backfill_requests")36 init_db()37 settings = get_settings()3839 with session_scope() as session:40 cutoff = session.execute(select(func.min(ApiRequest.ts))).scalar()41 if cutoff is not None and cutoff.tzinfo is None:42 cutoff = cutoff.replace(tzinfo=datetime.UTC)43 if cutoff is None:44 cutoff = datetime.datetime.now(tz=datetime.UTC)4546 entries: list[dict] = []47 parsed = skipped = 048 for log_file in sorted(settings.logs_dir.glob("apika.log*")):49 with open(log_file, encoding="utf-8") as fh:50 for line in fh:51 line = line.strip()52 if not line or '"http_request"' not in line:53 continue54 try:55 rec = json.loads(line)56 except json.JSONDecodeError:57 continue58 if rec.get("message") != "http_request":59 continue60 parsed += 161 try:62 ts = datetime.datetime.fromisoformat(rec["timestamp"])63 except (KeyError, ValueError):64 skipped += 165 continue66 if ts.tzinfo is None:67 ts = ts.replace(tzinfo=datetime.UTC)68 if ts >= cutoff:69 skipped += 170 continue71 entries.append(72 {73 "ts": ts,74 "method": rec.get("method", "GET"),75 "endpoint": normalize_endpoint(rec.get("path", "(autre)")),76 "status": int(rec.get("status_code", 0)),77 "duration_ms": round(float(rec.get("duration_ms", 0.0)), 2),78 }79 )8081 if entries:82 with session_scope() as session:83 session.bulk_insert_mappings(ApiRequest.__mapper__, entries)8485 logger.info(86 "Backfill api_requests terminé",87 extra={"inserted": len(entries), "parsed": parsed, "skipped": skipped},88 )89 print(f"Backfill : {len(entries)} requêtes insérées ({parsed} lues, {skipped} ignorées)")909192if __name__ == "__main__":93 main()94