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 :
- User request → 1 LLM call (planning)
- Tool selection → 1 LLM call
- Tool exec → résultat
- Réflexion → 1 LLM call
- Sub-task → 5 LLM calls
- 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
| Composant | Rôle | Coût indicatif |
|---|---|---|
| OpenTelemetry SDK | Instrumentation app | Gratuit |
| OTel Collector | Aggregation + routage | Gratuit (auto-host) |
| Tempo | Backend traces | Gratuit (auto-host) |
| Loki | Backend logs | Gratuit (auto-host) |
| Prometheus | Métriques | Gratuit (auto-host) |
| Langfuse / Arize Phoenix | UI prompts | Gratuit (self-host) ou cloud |
| Grafana | Dashboards | Gratuit |
| Alertmanager | Alerting | Gratuit |
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 = 1722Toute 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 responseCas 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.







