Ajoute un logging JSON structure (meme convention que LoggerService cote apps/api) a services/tech-step-intent-service : chaque appel POST /v1/process journalise locale/texte en entree et entites/intent/score en sortie, chaque POST /v1/train journalise les uid entraines et les compteurs resultants. Chatter interne de spaCy mis a WARNING pour ne pas noyer ces lignes. Bug trouve et corrige en verifiant les octets bruts d'un log reel (pas juste son affichage terminal) : l'encodage par defaut de sys.stdout sur Windows produisait de vrais octets UTF-8 invalides pour tout texte accentue journalise (le francais des etapes de recette) — corrige par sys.stdout.reconfigure(encoding="utf-8") au demarrage. LOG_LEVEL configurable (INFO par defaut), documente dans le README du service et .env.example. Verifie : 30/30 pytest (3 nouveaux tests sur le formateur JSON), smoke test HTTP reel confirmant au niveau des octets que les caracteres accentues sont preserves, lint complet du monorepo. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
76 lines
3.8 KiB
Python
76 lines
3.8 KiB
Python
"""Logging structuré — même convention que `LoggerService` côté `apps/api`
|
|
(`apps/api/src/lib/logger.service.ts`) : une ligne JSON par évènement
|
|
(`timestamp`, `level`, `message`, + le reste des champs fournis fusionné),
|
|
jamais du texte libre, pour rester grep/parse-able par `docker logs`/
|
|
Portainer ou un agrégateur de logs — cohérent avec le reste du repo plutôt
|
|
qu'un format propre à ce seul service.
|
|
|
|
Configuré une fois au démarrage (`main.py`) plutôt que par un `print()` ad
|
|
hoc dans chaque route — `routes/process.py`/`routes/train.py` appellent
|
|
`logging.getLogger(__name__)` normalement, ce module ne fait que brancher le
|
|
formateur JSON sur la racine du logging Python.
|
|
"""
|
|
|
|
import json
|
|
import logging
|
|
import sys
|
|
from datetime import UTC, datetime
|
|
from typing import Any
|
|
|
|
|
|
class _JsonFormatter(logging.Formatter):
|
|
"""Sérialise chaque `LogRecord` en une ligne JSON. Les champs
|
|
supplémentaires passés via `logger.info(msg, extra={...})` sont fusionnés
|
|
tels quels dans l'objet — c'est ce que `routes/process.py` utilise pour
|
|
joindre `locale`/`text`/`entities`/`intent`/`score` à la ligne."""
|
|
|
|
# Attributs standards de `LogRecord` — tout le reste posé sur le record
|
|
# (via `extra=`) est un champ métier ajouté par l'appelant, à fusionner
|
|
# dans la sortie JSON.
|
|
_STANDARD_ATTRS = frozenset(logging.LogRecord("", 0, "", 0, "", None, None).__dict__.keys())
|
|
|
|
def format(self, record: logging.LogRecord) -> str:
|
|
payload: dict[str, Any] = {
|
|
"timestamp": datetime.fromtimestamp(record.created, tz=UTC).isoformat(),
|
|
"level": record.levelname.lower(),
|
|
"message": record.getMessage(),
|
|
}
|
|
extra_fields = {
|
|
key: value for key, value in record.__dict__.items() if key not in self._STANDARD_ATTRS
|
|
}
|
|
payload.update(extra_fields)
|
|
if record.exc_info:
|
|
payload["error"] = self.formatException(record.exc_info)
|
|
return json.dumps(payload, ensure_ascii=False, default=str)
|
|
|
|
|
|
def configure_logging(level: str) -> None:
|
|
"""Branche le formateur JSON sur la racine du logging Python — appelé
|
|
une fois au démarrage (`main.py`), avant que `routes/*` ne journalisent
|
|
quoi que ce soit."""
|
|
# L'encodage par défaut de `sys.stdout` suit la locale de l'OS/console,
|
|
# pas forcément UTF-8 — sur Windows en particulier, garder ce défaut
|
|
# produit de vrais octets invalides (pas juste un affichage terminal
|
|
# trompeur) pour tout texte accentué journalisé par `routes/process.py`
|
|
# (le texte réel des étapes de recette, en français) — trouvé en
|
|
# vérifiant les octets bruts d'un log réel, pas juste son affichage.
|
|
# `reconfigure` existe sur `sys.stdout` dans toute exécution Python
|
|
# normale (pas dans certains contextes embarqués/redirigés exotiques) —
|
|
# protégé par `hasattr` pour ne jamais faire planter le démarrage pour un
|
|
# souci de confort d'affichage.
|
|
if hasattr(sys.stdout, "reconfigure"):
|
|
sys.stdout.reconfigure(encoding="utf-8")
|
|
handler = logging.StreamHandler(sys.stdout)
|
|
handler.setFormatter(_JsonFormatter())
|
|
root = logging.getLogger()
|
|
root.handlers = [handler]
|
|
root.setLevel(level)
|
|
|
|
# spaCy/thinc journalisent leur propre chatter interne ("Created
|
|
# vocabulary", "Finished initializing nlp object"...) sur le logger
|
|
# `"spacy"`, qui propage jusqu'à la racine et se retrouverait donc
|
|
# mélangé aux lignes input/output de `routes/process.py`/`routes/train.py`
|
|
# — ce sont ces dernières que ce service existe pour rendre visibles, pas
|
|
# le détail interne de spaCy. `WARNING` laisse quand même remonter un
|
|
# vrai problème (dépréciation, échec partiel) sans le bruit `INFO`.
|
|
logging.getLogger("spacy").setLevel(logging.WARNING)
|