LLM Security

Logging et observabilité d'un système LLM en production

Logs et observabilité LLM production : OpenTelemetry, traces, prompts/outputs, tokens, coûts, RGPD. Stack Langfuse/Phoenix/Arize. Patterns SRE et incident response.

Naim Aouaichia
12 min de lecture
  • logging
  • observabilité
  • SRE
  • OpenTelemetry
  • Langfuse

L'observabilité d'un système LLM en production est un sujet distinct de l'observabilité applicative classique. Quatre raisons : le contenu (prompt + réponse) est la donnée critique mais sensible (PII, secrets) ; le coût est directement traçable et explosif (chaque token coûte de l'argent réel) ; le non-déterminisme rend la reproduction d'incident difficile sans contexte complet ; les agents génèrent des traces multi-step (5-50 LLM calls par requête utilisateur) qui imposent un tracing distribué structuré. Cet article documente la stack observabilité LLM 2026 : conventions OpenTelemetry GenAI (publiées 2024), schéma de log minimum (identification, modèle, contenu, métriques, sécurité, conformité), gestion RGPD (redaction PII, pseudonymisation, EU AI Act Art. 12), stack open-source recommandée (OTel + Tempo + Loki + Prometheus + Langfuse + Grafana), instrumentation FastAPI + LangChain en 4 étapes, 6 exploitations concrètes pour réduire incidents et coûts (-20% à -40% coûts en 3-6 mois). Cible : SRE / DevOps / AI engineers / RSSI structurant l'observabilité d'apps LLM enterprise.

Pour la couche audit production : auditer un LLM en production. Pour la sécurité API en amont : sécuriser les API LLM : rate limiting, quotas, anti-abuse.

Pourquoi l'observabilité LLM est différente

Le contenu est la donnée critique

[API REST classique]
GET /users/42 → 200 OK, 12ms
└── Suffit. Le contenu est implicite (l'objet User #42).

[API LLM]
POST /chat → 200 OK, 1850ms, 482 tokens, 0.02 €
├── Prompt : "résume ces 5 derniers emails et identifie les actions urgentes"
├── System : "Tu es Eva, assistante exécutive..."  
├── RAG retrieved : [doc_42, doc_91, doc_156]
├── Tool calls : [{name: search_email, args: {...}}]
└── Response : "3 actions urgentes identifiées : ..."

Pour debug, qualité, sécurité : tout le contexte est nécessaire. Mais ce contexte contient PII, secrets, données métier sensibles. Tension permanente entre observabilité et confidentialité.

Le coût directement traçable

Chaque requête LLM a un coût mesurable en $. C'est :

  • Une dimension SRE (capacity planning, FinOps).
  • Un vecteur d'attaque (Denial of Wallet, cf article précédent).
  • Un levier d'optimisation (caching, model right-sizing).

cost_usd doit être un first-class metric, au même titre que latency_ms.

Le non-déterminisme

Un même prompt → réponses différentes. Sans logging du contexte complet (température, seed, version modèle, retrieval RAG, tool history), impossible de reproduire un incident ou de comparer deux exécutions.

Multi-step traces

Un agent IA :

  1. User request → 1 LLM call (planning)
  2. Tool selection → 1 LLM call
  3. Tool exec → résultat
  4. Réflexion → 1 LLM call
  5. Sub-task → 5 LLM calls
  6. Synthèse → 1 LLM call

Total : 9 LLM calls pour 1 requête. Sans tracing distribué structuré, impossible de comprendre où une boucle s'est formée ou où le coût a explosé.

OpenTelemetry tracing obligatoire, pas optionnel.

Schéma de log minimum recommandé

Structure JSON

{
  "ts": "2026-05-02T11:23:45.123Z",
  "request_id": "req_8f3a2b1c4e5d6f7g",
  "trace_id": "0123456789abcdef0123456789abcdef",
  "span_id": "abcdef0123456789",
  "parent_span_id": "0123456789abcdef",
  
  "user_id_hash": "sha256_abcd1234",
  "org_id": "org_zeroday",
  "session_id": "sess_xyz",
  
  "model": {
    "provider": "openai",
    "name": "gpt-4o",
    "version": "2026-04-15",
    "temperature": 0.3,
    "max_tokens": 1500
  },
  
  "content": {
    "system_prompt_hash": "sha256_efgh5678",
    "user_message_redacted": "Résume mes [EMAIL_REDACTED] et identifie...",
    "response_redacted": "3 actions urgentes : ...",
    "rag_doc_ids": ["doc_42", "doc_91", "doc_156"],
    "tool_calls": [
      {"name": "search_email", "args_hash": "sha256_..."}
    ]
  },
  
  "metrics": {
    "tokens_input": 1240,
    "tokens_output": 482,
    "tokens_cached": 800,
    "cost_usd": 0.018,
    "latency_ms_total": 1850,
    "latency_ms_breakdown": {
      "guardrail_input": 78,
      "rag_retrieval": 145,
      "llm_call": 1320,
      "tool_exec": 250,
      "guardrail_output": 32,
      "other": 25
    }
  },
  
  "security": {
    "ip_source_hash": "sha256_ijkl9012",
    "user_agent": "Chrome/120.0",
    "rate_limit_remaining": {"req_min": 18, "tokens_min": 28000},
    "guardrail_scores": {
      "input_classifier": 0.05,
      "output_filter_alerts": []
    },
    "anomaly_flags": []
  },
  
  "compliance": {
    "data_classification": "internal",
    "retention_class": "security_1y",
    "deletion_eligible_after": "2027-05-02T11:23:45Z"
  },
  
  "result": {
    "status": "ok",
    "error_type": null,
    "guardrail_action": "allow"
  }
}

Règles de remplissage

  • Hash plutôt que clair quand possible : user_id, IP, args sensibles → SHA-256 avec sel par environnement.
  • Redaction automatique : prompt et response passent par un PII detector avant log.
  • Schéma versionné : "schema_version": "1.3" dans chaque entrée pour gérer évolution.
  • Volume estimé : 5-50 KB par appel. Pour 10M req/mois → 50-500 GB logs/mois. Plan stockage et rotation.

RGPD et EU AI Act : ce qu'il faut savoir

Niveau 1, Redaction PII automatique

from presidio_analyzer import AnalyzerEngine
from presidio_anonymizer import AnonymizerEngine
 
analyzer = AnalyzerEngine()
anonymizer = AnonymizerEngine()
 
PII_ENTITIES = [
    "EMAIL_ADDRESS",
    "PHONE_NUMBER",
    "CREDIT_CARD",
    "IBAN_CODE",
    "PERSON",
    "FR_NIR",  # Numéro de sécurité sociale FR
    "FR_SIREN",
]
 
def redact_for_logging(text: str, language: str = "fr") -> str:
    results = analyzer.analyze(text=text, language=language, entities=PII_ENTITIES)
    anonymized = anonymizer.anonymize(text=text, analyzer_results=results)
    return anonymized.text
 
 
# Wrapper pour log structuré
def log_llm_call(prompt: str, response: str, **kwargs):
    logger.info({
        "user_message_redacted": redact_for_logging(prompt),
        "response_redacted": redact_for_logging(response),
        **kwargs,
    })

Niveau 2, Pseudonymisation user_id

import hmac
import hashlib
import os
 
PSEUDO_KEY = os.environ["PSEUDO_HMAC_KEY"].encode()  # secret env-specific
 
def pseudonymize_user_id(user_id: str) -> str:
    return hmac.new(PSEUDO_KEY, user_id.encode(), hashlib.sha256).hexdigest()
 
# Permet d'agréger par user_id_hash sans révéler l'identité,
# tout en gardant la stabilité (même user → même hash).

Niveau 3, Encryption at rest + access control

  • Logs stockés sur S3/Azure Blob avec chiffrement KMS.
  • Accès journalisé (CloudTrail / Audit Logs).
  • IAM policies strictes : seuls SRE/AppSec/RSSI ont accès brut. Devs ont accès aux logs anonymisés.

EU AI Act Art. 12, logging obligations

L'EU AI Act impose pour les systèmes IA à haut risque (annex III : RH, scoring crédit, justice, infrastructures critiques, éducation) un logging :

  • Automatique tout au long du cycle de vie.
  • Conservé minimum 6 mois (selon catégorie).
  • Permettant traçabilité et audit ex-post.

→ Si votre app rentre dans le périmètre haut risque, l'observabilité n'est plus optionnelle mais une obligation réglementaire.

Cas particulier, domaines ultra-sensibles

Pour conversations médicales, légales, RH : considérer ne pas logger le contenu du tout. Logger uniquement métadonnées (tokens, cost, latency, scores). Replay possible côté client si l'utilisateur consent au support.

Stack open-source recommandée 2026

Vue d'ensemble

[Application FastAPI / Express / ...]
    │
    │ (OTel SDK auto-instrumentation)
    ▼
[OTel Collector]  ← agrège, sample, redact
    │
    ├─► Tempo / Jaeger    : traces distribuées
    ├─► Loki              : logs structurés
    ├─► Prometheus        : métriques agrégées
    └─► Langfuse / Phoenix: UI prompts/conversations
    │
    ▼
[Grafana]  ← dashboards unifiés
    │
    ▼
[Alertmanager]  ← règles d'alerte
    │
    ▼
[PagerDuty / Slack / SNS]  ← notification on-call

Composants

ComposantRôleCoût indicatif
OpenTelemetry SDKInstrumentation appGratuit
OTel CollectorAggregation + routageGratuit (auto-host)
TempoBackend tracesGratuit (auto-host)
LokiBackend logsGratuit (auto-host)
PrometheusMétriquesGratuit (auto-host)
Langfuse / Arize PhoenixUI promptsGratuit (self-host) ou cloud
GrafanaDashboardsGratuit
AlertmanagerAlertingGratuit

Coût infra réel pour 10M req/mois : ~50-300€/mois (compute + storage). À comparer aux solutions cloud all-in-one (Datadog LLM Observability, New Relic AI Monitoring) à 1000-3000€/mois.

OpenTelemetry GenAI semantic conventions

Publiées par CNCF 2024. Attributs standards :

# Attributes spans LLM
gen_ai.system = "openai"
gen_ai.request.model = "gpt-4o"
gen_ai.request.temperature = 0.3
gen_ai.request.max_tokens = 1500
gen_ai.response.model = "gpt-4o-2026-04-15"
gen_ai.response.finish_reasons = ["stop"]
gen_ai.usage.prompt_tokens = 1240
gen_ai.usage.completion_tokens = 482
gen_ai.usage.total_tokens = 1722

Toute lib LLM moderne (OpenAI SDK, Anthropic SDK, LangChain, LlamaIndex) instrumente via ces conventions. Permet une observabilité vendor-agnostic.

Instrumentation FastAPI + LangChain en 4 étapes

Étape 1, Installation

pip install \
    opentelemetry-sdk \
    opentelemetry-exporter-otlp \
    opentelemetry-instrumentation-fastapi \
    opentelemetry-instrumentation-langchain \
    langfuse

Étape 2, Configuration tracer

# observability.py
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
from opentelemetry.sdk.resources import Resource
from opentelemetry.semconv.resource import ResourceAttributes
 
resource = Resource.create({
    ResourceAttributes.SERVICE_NAME: "zeroday-llm-app",
    ResourceAttributes.SERVICE_VERSION: "1.4.2",
    ResourceAttributes.DEPLOYMENT_ENVIRONMENT: "production",
})
 
provider = TracerProvider(resource=resource)
exporter = OTLPSpanExporter(endpoint="http://otel-collector:4317", insecure=True)
provider.add_span_processor(BatchSpanProcessor(exporter))
trace.set_tracer_provider(provider)
 
tracer = trace.get_tracer(__name__)

Étape 3, Auto-instrumentation

# main.py
from fastapi import FastAPI
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
from opentelemetry.instrumentation.langchain import LangchainInstrumentor
 
import observability  # configure tracer
 
app = FastAPI()
FastAPIInstrumentor.instrument_app(app)
LangchainInstrumentor().instrument()

Étape 4, Spans custom métier

from observability import tracer
 
@app.post("/chat")
async def chat(req: ChatReq):
    with tracer.start_as_current_span("chat_endpoint") as span:
        span.set_attribute("user.id_hash", pseudonymize(req.user_id))
        span.set_attribute("org.id", req.org_id)
        
        # Sub-span guardrail input
        with tracer.start_as_current_span("guardrail_input") as gspan:
            score = await classify_input(req.message)
            gspan.set_attribute("guardrail.score", score)
            if score > 0.95:
                gspan.set_attribute("guardrail.action", "block")
                return {"answer": "Désolé..."}
        
        # Sub-span RAG
        with tracer.start_as_current_span("rag_retrieval") as rspan:
            docs = await rag.query(req.message, tenant=req.org_id)
            rspan.set_attribute("rag.docs_count", len(docs))
            rspan.set_attribute("rag.doc_ids", [d.id for d in docs])
        
        # LLM call (auto-instrumented par LangchainInstrumentor)
        response = await chain.ainvoke({"input": req.message, "context": docs})
        
        # Sub-span guardrail output
        with tracer.start_as_current_span("guardrail_output") as ospan:
            filtered = filter_output(response.content, req.message)
            ospan.set_attribute("guardrail.alerts_count", len(get_alerts()))
        
        # Logger métier
        log_llm_call(
            request_id=span.get_span_context().trace_id,
            user_id_hash=pseudonymize(req.user_id),
            tokens_in=response.usage.prompt_tokens,
            tokens_out=response.usage.completion_tokens,
            cost_usd=compute_cost(response.usage, "gpt-4o"),
            input_redacted=redact_for_logging(req.message),
            output_redacted=redact_for_logging(filtered),
        )
        
        return {"answer": filtered}

Avantage clé

Un trace montre tout le pipeline avec timing par étape :

[Trace req_8f3a2b1c, 1850ms]
└── chat_endpoint                  1850ms
    ├── guardrail_input              78ms  (score=0.05, allow)
    ├── rag_retrieval               145ms  (3 docs)
    ├── langchain.ChatOpenAI       1320ms  (tokens=1722, cost=0.02 €)
    │   └── openai.chat.completions 1290ms
    ├── tool_call:search_email      250ms
    ├── guardrail_output             32ms  (no alerts)
    └── log_write                    25ms

Indispensable pour debug et optimisation.

Langfuse / Arize Phoenix, UI prompts dédiée

Pourquoi pas seulement Grafana

Grafana est excellent pour métriques agrégées. Pas adapté pour :

  • Voir une conversation complète d'un user spécifique
  • Comparer côte à côte 2 versions d'un même prompt
  • Annoter manuellement des outputs (qualité, hallucination)
  • Datasets curatés pour eval / fine-tune

Pour ces cas, UI prompts dédiée : Langfuse, Arize Phoenix, Helicone, LangSmith (commercial).

Setup Langfuse self-hosted

# docker-compose.yml fragment
services:
  langfuse:
    image: langfuse/langfuse:latest
    ports: ["3000:3000"]
    environment:
      - DATABASE_URL=postgresql://langfuse:${PG_PASS}@postgres/langfuse
      - NEXTAUTH_SECRET=${NEXTAUTH_SECRET}
      - NEXTAUTH_URL=https://langfuse.internal
    depends_on: [postgres]
  
  postgres:
    image: postgres:16
    environment:
      - POSTGRES_USER=langfuse
      - POSTGRES_PASSWORD=${PG_PASS}
      - POSTGRES_DB=langfuse
    volumes: [pg-data:/var/lib/postgresql/data]
 
volumes: { pg-data: }

Instrumentation app

from langfuse import Langfuse
 
langfuse = Langfuse(
    public_key=os.environ["LANGFUSE_PUBLIC_KEY"],
    secret_key=os.environ["LANGFUSE_SECRET_KEY"],
    host="https://langfuse.internal",
)
 
# Wrapper sur appel LLM
@langfuse.observe()
async def llm_call_with_observability(prompt: str, user_id: str):
    langfuse.update_current_observation(
        user_id=pseudonymize(user_id),
        metadata={"app_version": "1.4.2"},
    )
    
    response = await openai_client.chat.completions.create(...)
    
    langfuse.update_current_observation(
        usage={"input": response.usage.prompt_tokens, "output": response.usage.completion_tokens},
        cost=compute_cost(response.usage, "gpt-4o"),
    )
    
    return response

Cas d'usage typiques

  • Sample 1% du trafic vers UI prompts (volume gérable, vue qualitative).
  • 100% trafic flagged (guardrail high score, anomaly) → debug immédiat.
  • Datasets eval curés à partir des conversations production → réutilisés pour Promptfoo / fine-tune.

Métriques Prometheus + dashboards Grafana

Métriques essentielles

from prometheus_client import Counter, Histogram, Gauge
 
# Volumes
llm_requests = Counter("llm_requests_total", "LLM requests", ["model", "status", "org"])
llm_tokens = Counter("llm_tokens_total", "Tokens", ["model", "direction", "org"])
llm_cost = Counter("llm_cost_usd_total", "EUR cost", ["model", "org"])
 
# Latence
llm_duration = Histogram(
    "llm_duration_seconds",
    "Request duration",
    ["model"],
    buckets=[0.1, 0.5, 1, 2, 5, 10, 30, 60],
)
 
# Guardrails
guardrail_blocks = Counter("guardrail_blocks_total", "Blocks", ["layer", "reason"])
guardrail_score = Histogram("guardrail_score", "Score distribution", ["layer"])
 
# Rate limit
rate_limit_hits = Counter("rate_limit_hits_total", "Hits", ["dimension", "user"])
 
# Anomalies
anomaly_flags = Counter("anomaly_flags_total", "Anomalies", ["type"])

Dashboards Grafana, panneaux critiques

Dashboard 1, Cost & Usage

  • Cost $/min en temps réel (par org)
  • Tokens/min in/out
  • Top 10 users par cost (24h, 7j, 30j)
  • Cost par modèle (split GPT-4o vs Claude vs local)

Dashboard 2, Performance

  • p50 / p95 / p99 latence par endpoint
  • Latency breakdown (guardrail / RAG / LLM / tools)
  • Error rate par type
  • Cache hit rate (si caching prompts)

Dashboard 3, Security

  • Guardrail blocks par layer / reason
  • Rate limit hits par dimension
  • Anomaly flags par type
  • Failed auth attempts (signal credential stuffing)

Dashboard 4, Quality (sample-based)

  • LLM-as-judge score sur sample 1%
  • Hallucination detection rate
  • Refusal rate (guardrail trop strict ?)
  • User feedback (👍 / 👎) si exposé

6 exploitations concrètes

1. Anomaly detection cost

# alertmanager
- alert: UserCostAnomaly
  expr: |
    rate(llm_cost_usd_total{user!=""}[1h])
    > 3 * avg_over_time(rate(llm_cost_usd_total{user!=""}[1h])[7d:1h])
  for: 10m
  annotations:
    summary: "User {{ $labels.user }} cost > 3× baseline"

Détecte abus, recursive bug, compromise.

2. Latency hotspots

Analyser les spans, identifier l'étape lente. Patterns observés :

  • RAG retrieval > 500ms → indexer mieux, réduire top-k
  • LLM call > 5s → context trop long, réduire history
  • Tool exec > 2s → cache result ou parallelize

3. Quality regression

Sample 1% trafic, scoring LLM-as-judge, alerter si score moyen drop > 5% post-deploy.

4. Prompt optimization

Top 10 prompts par cost, sont-ils nécessaires ? Peuvent-ils être plus courts ? Caché ?

5. Failure pattern analysis

Group error_type par fréquence. Top 3 = priorités fixe.

6. Capacity planning

Linear regression sur cost / req-volume / latency p95 sur 30j. Projection 90j. Anticipe scaling et budget.

ROI et erreurs courantes

ROI typique

Une équipe AI engineering qui adopte observabilité LLM mature constate sur 3-6 mois :

  • -20% à -40% coûts LLM (caching, model right-sizing, prompt optimization, dead RAG retrieval)
  • MTTR incidents divisé par 3-5× (debug guidé par traces vs investigation cold)
  • Velocity feature maintenue (problèmes détectés tôt)
  • Conformité : capacité à répondre à audits / DPIA / EU AI Act sans crise

Erreurs courantes

Erreur 1, Logger contenu en clair RGPD violation directe. Toujours redaction PII avant log.

Erreur 2, Pas de tracing distribué Logs séparés sans corrélation. Impossible de débugger un agent multi-step. OTel obligatoire.

Erreur 3, Coût pas tracké comme métrique first-class "On verra à la fin du mois sur la facture". Trop tard. cost_usd en métrique temps réel.

Erreur 4, Logger 100% sans sampling sur volume élevé Storage explose, requêtes Loki/Tempo lentes. Sample 100% sur erreurs/anomalies, 1-10% sur succès.

Erreur 5, Pas d'UI prompts dédiée Grafana seul ne suffit pas pour debug qualitatif des conversations. Langfuse / Phoenix obligatoire.

Erreur 6, Logs gardés ad vitam aeternam Coût + risque RGPD. Rotation et rétention par classe de log.

Ce que ça change pour votre dispositif

Une observabilité LLM mature 2026 :

  • OTel + Tempo + Loki + Prometheus + Langfuse comme standard
  • Schéma log structuré avec redaction PII et pseudonymisation
  • Conformité RGPD + EU AI Act intégrée by design
  • Cost first-class dans les métriques
  • Tracing distribué sur tous les agents
  • 6 exploitations continues (anomaly, latency, quality, prompt, failure, capacity)

C'est l'infrastructure invisible qui rend tout le reste (guardrails, audit, incident response) opérable. Sans elle, on pilote à l'aveugle.

ROI : 1-2 ETP × 1-2 mois pour mise en place initiale, ensuite 0.2 ETP en routine. Bénéfice mesuré : -20-40% coûts + MTTR / 3-5×.


Pour aller plus loin : la suite naturelle est de détecter activement les abus en temps réel sur ces logs, signaux faibles, patterns d'attaque émergents, anomaly detection ML, au-delà des seuils statiques. À découvrir dans le prochain article du cluster.

Questions fréquentes

  • Pourquoi l'observabilité LLM est-elle un sujet à part de l'observabilité applicative classique ?
    Quatre raisons. (1) **Le contenu importe** : un log API REST classique ('GET /users/42 → 200 OK 12ms') suffit. Pour un LLM, le **prompt** et la **réponse** sont les données critiques pour debug, qualité, sécurité, mais aussi les plus sensibles (PII, secrets, contenu utilisateur). (2) **Coût directement traçable** : chaque appel LLM coûte de l'argent réel. Tracker tokens in/out + coût $ par requête est une exigence SRE/FinOps native. (3) **Non-déterminisme** : un même prompt peut donner une réponse différente. Reproduction d'incident nécessite logger TOUT le contexte (prompt, paramètres, version modèle, RAG retrieval, tool calls). (4) **Multi-step traces** : un agent fait 5-50 LLM calls par requête utilisateur. Sans tracing structuré (OpenTelemetry, équivalent), impossible de comprendre une boucle ou un échec. **Conséquence** : stack observabilité LLM = OTel + collector dédié + UI prompts (Langfuse, Arize Phoenix, LangSmith) + redaction PII + RGPD compliance.
  • Quelles données doit-on logger pour chaque appel LLM ?
    Schéma minimum recommandé. **Identification** : request_id, span_id (OTel), parent_span_id, user_id (hashé ou pseudonymisé), org_id, session_id. **Modèle** : provider (openai/anthropic/local), model_name (gpt-4o-2026-...), version, temperature, max_tokens. **Contenu** : prompt (system + user + history, redacted PII), response (raw + post-guardrails). **Métriques** : tokens_input, tokens_output, tokens_cached, cost_usd, latency_ms (p50/p95 par étape). **Contexte applicatif** : rag_documents_retrieved, tool_calls (avec args + results), guardrail_scores (input classifier, output filter), error_type. **Sécurité** : ip_source, user_agent, rate_limit_remaining, anomaly_flags. **Conformité** : data_classification (public/internal/confidential), retention_class, deletion_eligible_after. **Format** : JSON structuré, schéma versionné. **Volume typique** : 5-50 KB par appel LLM (~100× plus qu'un log REST classique). Plan storage en conséquence.
  • Comment gérer les contraintes RGPD sur les logs prompts/outputs ?
    Trois niveaux de durcissement. **Niveau 1, Redaction automatique au log** : avant écriture log, passer prompt et output dans un PII detector (Presidio, AWS Comprehend, regex). Remplacer email, phone, IBAN, NIR, noms par `[REDACTED]`. Ne JAMAIS logger en clair des secrets / passwords. **Niveau 2, Pseudonymisation** : user_id remplacé par hash HMAC stable, permet d'agréger par user sans le re-identifier directement. **Niveau 3, Encryption at rest + access control** : logs chiffrés (KMS), accès journalisé, principe du moindre privilège. **Rétention** : aligner sur durées RGPD du contexte produit. Typique : logs sécurité 1 an, logs débogage 30j, logs analytique agrégés (sans contenu) plus longtemps. **Right to erasure** : prévoir mécanisme delete par user_id pseudonymisé. **DPIA** : si l'app traite données sensibles, l'observabilité fait partie du périmètre, documenter dans la DPIA. **Cas particulier conversation médicale/légale/RH** : envisager **ne pas logger contenu prompt/output** du tout (uniquement métadonnées), avec replay possible côté client si l'utilisateur consent au support. **Référence** : EU AI Act Art. 12 (logging obligations pour high-risk AI systems).
  • Quelle stack open-source 2026 pour observabilité LLM ?
    Trois layers à empiler. **Layer 1, Tracing distribué** : OpenTelemetry (OTel) + collector + backend (Jaeger, Tempo, Honeycomb). Sémantique LLM : OTel a publié les conventions GenAI 2024 (`gen_ai.system`, `gen_ai.request.model`, `gen_ai.usage.prompt_tokens`, etc.). Toute lib LLM moderne (OpenAI SDK, Anthropic SDK, LangChain, LlamaIndex) instrumente via OTel. **Layer 2, UI prompts dédiée** : Langfuse (open-source self-host gratuit + cloud), Arize Phoenix (open-source), Helicone (commercial). Permettent de visualiser conversations complètes, filtrer par user/score/cost, A/B comparer prompts, alerter. **Layer 3, Métriques + alerting** : Prometheus + Grafana + Alertmanager. Standard pour métriques agrégées (cost/min, latency p95, error rate). **Stack typique 2026** : OTel SDK dans l'app → OTel Collector → Tempo (traces) + Loki (logs) + Prometheus (métriques) + Langfuse (UI prompts) + Grafana (dashboards). Coût infra : ~50-300€/mois pour app moyenne (10M requêtes/mois). Alternative cloud all-in-one : Datadog LLM Observability, New Relic AI Monitoring, Honeycomb (avec semantic conventions GenAI).
  • Comment instrumenter une app FastAPI + LangChain avec OpenTelemetry pour LLM ?
    Setup en 4 étapes. (1) **Installer SDK OTel + LangChain instrumentor** : `pip install opentelemetry-sdk opentelemetry-exporter-otlp opentelemetry-instrumentation-langchain`. (2) **Configurer le tracer** : exporter OTLP vers le collector, resource attributes service_name/version/environment. (3) **Auto-instrument LangChain** : `LangchainInstrumentor().instrument()` capture tous les appels LLM, retrieval RAG, tool calls, avec spans hiérarchiques. (4) **Spans custom** pour la logique métier : `with tracer.start_as_current_span('rag_retrieval') as span: span.set_attribute('rag.docs_count', n)`. **Avantage clé** : un trace montre tout le pipeline (user request → guardrail → LLM call → RAG → tool exec → guardrail output → response) avec timing par étape. Indispensable pour debug agents complexes. **Bonus** : OTel s'intègre naturellement avec Langfuse, Arize Phoenix qui consomment le format. Pas besoin de double instrumentation. **Limite à connaître** : auto-instrumentation peut logger les prompts en clair par défaut, vérifier que la PII redaction est en place AVANT de l'activer en prod (cf section RGPD).
  • Comment exploiter l'observabilité pour réduire incidents et coûts ?
    Six exploitations concrètes. (1) **Anomaly detection cost** : alerte si user dépasse 3× son baseline coût/heure. Détecte abus, bug recursif, compromise. (2) **Latency hotspots** : analyser les spans, identifier l'étape la plus lente (souvent retrieval RAG mal indexé ou LLM call avec context trop long). Optimiser. (3) **Quality regression** : comparer scoring (LLM-as-judge sur sample) avant/après changement de prompt ou modèle. Si score chute → rollback. (4) **Prompt optimization** : analyser les prompts les plus utilisés vs ceux qui produisent les meilleurs outputs. Refactoriser les prompts producteurs de bonnes réponses, déprécier les mauvais. (5) **Failure pattern analysis** : grouper les errors par cause (rate limit, timeout, content policy violation, hallucination détectée). Identifier les top 3 et les fixer en priorité. (6) **Capacity planning** : projection coûts/charge sur 30/90j basée sur croissance observée. Anti-cipe DoW potentiel. **Cadence** : review hebdo des dashboards principaux par lead AI eng + review mensuel de fond avec FinOps. ROI typique : -20% à -40% coûts LLM en 3-6 mois grâce à l'observabilité (caching, prompt optimization, model right-sizing).

Écrit par

Naim Aouaichia

Cyber Security Engineer et fondateur de Zeroday Cyber Academy

Ingénieur cybersécurité avec un parcours hybride : développement, DevOps Capgemini, DevSecOps IN Groupe (sécurité des documents d'identité régaliens), audits CAC 40. Fondateur de Hash24Security et Zeroday Cyber Academy. Présence LinkedIn 43 000 abonnés, Substack Zeroday Notes 23 000 abonnés.