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%
3.2 KB · 94 lines python
Raw Blame History
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