batchCooking/services/tech-step-intent-service/intent_service/logging_config.py
Nicolas 74a0052431 feat(recipes): journalise chaque input/output du pipeline NLP
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>
2026-08-25 21:34:40 +02:00

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)