Aller au contenu principal

ADR-009 : Snapshots d'erreurs — Débogage « voyage dans le temps » avec masquage PII

Métadonnées

ChampValeur
StatutAccepté
Date2026-02-06
LinearCAB-397

Contexte

Lorsque des invocations d'outils MCP échouent, le débogage nécessite de comprendre le contexte complet :

  • Quelle était la requête ?
  • Quel utilisateur/tenant l'a initiée ?
  • Quel contexte LLM existait ?
  • Y a-t-il eu des retentatives ?
  • Quel était l'impact sur les coûts ?

La journalisation traditionnelle ne capture que des fragments. Les développeurs doivent corréler manuellement les logs entre les services. Cela est particulièrement problématique pour :

  • Les échecs intermittents
  • Les dépassements de rate limit
  • Les timeouts backend
  • Les erreurs de validation de schéma

Le problème

« Un agent IA a échoué en pleine conversation. L'utilisateur le signale 2 heures plus tard. Comment reconstruire exactement ce qui s'est passé ? »

Décision

Implémenter des snapshots d'erreurs — des captures complètes instantanées des requêtes échouées avec masquage automatique des données personnelles et génération de commandes cURL pour la reproduction.

Architecture

┌──────────────────────────────────────────────────────────────────────┐
│ MCP Gateway │
│ │
│ ┌────────────┐ ┌────────────────┐ ┌────────────────────────┐ │
│ │ Invocation │───▶│ Gestionnaire │───▶│ capture_mcp_error() │ │
│ │ d'outil │ │ d'erreurs │ │ │ │
│ │ (échoue) │ │ │ │ - Construire contexte │ │
│ └────────────┘ │ statut >= 400 │ │ - Masquer PII │ │
│ └────────────────┘ │ - Calculer coût │ │
│ │ - Publier async │ │
│ └───────────┬────────────┘ │
└───────────────────────────────────────────────────────┼──────────────┘


┌───────────────────────────────────────────────────────────────────────┐
│ Éditeur de snapshots │
│ │
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
│ │ Topic Kafka │ │ MinIO/S3 │ │ OpenSearch │ │
│ │ error-snapshots │ │ (compressé) │ │ (indexé) │ │
│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │
└───────────────────────────────────────────────────────────────────────┘

Modèle de snapshot

@dataclass
class MCPErrorSnapshot:
# --- Identification ---
snapshot_id: str # UUID
timestamp: datetime
environment: str # dev, staging, prod

# --- Détails de l'erreur ---
error_type: MCPErrorType # Enum : TOOL_EXECUTION, RATE_LIMIT, AUTH, etc.
error_message: str # Masqué

# --- Contexte de la requête ---
request: RequestContext # Méthode, chemin, headers (masqués), corps
response_status: int

# --- Contexte utilisateur ---
user: UserContext # tenant_id, user_id (haché), rôles

# --- Contexte MCP ---
mcp_server: MCPServerContext | None # ID serveur, version protocole
tool_invocation: ToolInvocation | None # Nom outil, paramètres (masqués)
llm_context: LLMContext | None # Tokens, modèle, coût estimé

# --- Contexte de retentative ---
retry_context: RetryContext | None # Nombre de tentatives, backoff

# --- Observabilité ---
trace_id: str | None
span_id: str | None
conversation_id: str | None

# --- Impact sur les coûts ---
total_cost_usd: float
tokens_wasted: int

# --- Suivi PII ---
masked_fields: list[str] # Champs qui ont été masqués

Types d'erreurs

class MCPErrorType(Enum):
TOOL_EXECUTION = "tool_execution" # Outil échoué pendant l'exécution
TOOL_NOT_FOUND = "tool_not_found" # Outil inexistant
VALIDATION = "validation" # Validation d'entrée échouée
AUTH = "auth" # Authentification/autorisation
RATE_LIMIT = "rate_limit" # Rate limit dépassé
TIMEOUT = "timeout" # Timeout backend
UPSTREAM = "upstream" # Erreur retournée par le backend
INTERNAL = "internal" # Erreur interne du MCP Gateway

Stratégie de masquage PII

Masquage automatique

Masquage en trois couches avant le stockage :

# 1. Headers
masked_headers = mask_headers(request.headers)
# Authorization: Bearer eyJ... → Authorization: [REDACTED]
# X-API-Key: sk-... → X-API-Key: [REDACTED]

# 2. Paramètres d'outil
masked_params = mask_tool_params(tool.input_params)
# {"password": "secret123"} → {"password": "[REDACTED]"}
# {"email": "user@example.com"} → {"email": "[REDACTED]"}

# 3. Messages d'erreur
masked_message = mask_error_message(error_message)
# "Token invalide pour l'utilisateur john@..." → "Token invalide pour l'utilisateur [REDACTED]"

Configuration des champs sensibles

settings = MCPSnapshotSettings(
sensitive_headers=[
"authorization",
"x-api-key",
"cookie",
"x-client-secret",
],
sensitive_params=[
"password",
"secret",
"token",
"api_key",
"email",
"phone",
"ssn",
"credit_card",
],
)

Génération de commande cURL pour la reproduction

Chaque snapshot inclut une commande cURL pour rejouer la requête :

def generate_curl_command(snapshot: MCPErrorSnapshot) -> str:
"""Générer une commande cURL pour la reproduction (secrets remplacés par des placeholders)."""
cmd = f"curl -X {snapshot.request.method}"
cmd += f" '{snapshot.request.url}'"

for header, value in snapshot.request.headers.items():
if header.lower() in sensitive_headers:
cmd += f" -H '{header}: ${{YOUR_{header.upper()}_HERE}}'"
else:
cmd += f" -H '{header}: {value}'"

if snapshot.request.body:
cmd += f" -d '{json.dumps(snapshot.request.body)}'"

return cmd

Exemple de sortie :

curl -X POST '${STOA_GATEWAY_URL}/tools/call' \
-H 'Authorization: ${YOUR_AUTHORIZATION_HERE}' \
-H 'Content-Type: application/json' \
-d '{"tool": "acme:payment-api:create", "arguments": {...}}'

Stratégie de stockage

Partitionnement

Les snapshots sont partitionnés par date et tenant :

s3://stoa-error-snapshots/
├── 2026/
│ └── 02/
│ └── 06/
│ ├── tenant-acme/
│ │ ├── snap_abc123.json.gz
│ │ └── snap_def456.json.gz
│ └── tenant-beta/
│ └── snap_xyz789.json.gz

Rétention

EnvironnementRétentionJustification
Production30 joursConformité, débogage
Staging7 joursTests
Développement1 jourÉconomies

Compression

Les snapshots sont compressés avec gzip avant le stockage :

async def store_snapshot(snapshot: MCPErrorSnapshot) -> str:
"""Stocker le snapshot avec compression."""
payload = snapshot.model_dump_json()
compressed = gzip.compress(payload.encode())

key = f"{snapshot.timestamp:%Y/%m/%d}/{snapshot.user.tenant_id}/{snapshot.snapshot_id}.json.gz"
await s3.put_object(Bucket=bucket, Key=key, Body=compressed)

return key

Conditions de capture

Les snapshots sont capturés selon les critères suivants :

if not settings.enabled:
return None

if response_status < 400:
return None # Succès — pas de snapshot

if 400 <= response_status < 500 and not settings.capture_on_4xx:
return None # Erreur client — optionnel

if response_status >= 500 and not settings.capture_on_5xx:
return None # Erreur serveur — généralement capturé

Exclusions

settings = MCPSnapshotSettings(
exclude_paths=[
"/health",
"/metrics",
"/ready",
],
capture_on_4xx=True, # Capturer les erreurs client
capture_on_5xx=True, # Capturer les erreurs serveur
)

Suivi des coûts

Les snapshots calculent les ressources gaspillées :

if llm_context:
total_cost = llm_context.estimated_cost_usd
tokens_wasted = llm_context.tokens_input + llm_context.tokens_output

snapshot = MCPErrorSnapshot(
total_cost_usd=total_cost,
tokens_wasted=tokens_wasted,
...
)

Permet des tableaux de bord affichant :

  • L'impact des erreurs sur les coûts par tenant
  • Les tendances de gaspillage de tokens
  • Les types d'erreurs les plus coûteux

Conséquences

Positives

  • Débogage « voyage dans le temps » — Reconstituer l'état exact de la requête des heures plus tard
  • Sûr pour la conformité — Le masquage automatique évite les problèmes de conformité
  • Visibilité sur les coûts — Suivi des tokens et coûts API gaspillés
  • Capacité de reproduction — Commandes cURL pour la reproduction
  • Corrélation — Liens vers les traces via trace_id/span_id

Négatives

  • Coûts de stockage — Les snapshots consomment de l'espace S3
  • Latence de capture — ~5 ms de surcoût par erreur
  • Lacunes de masquage — Les champs personnalisés peuvent nécessiter une configuration
  • Volume de données — Les scénarios à forte erreur génèrent de nombreux snapshots

Atténuations

DéfiAtténuation
Coûts de stockageCompression + rétention courte
Latence de capturePublication asynchrone via Kafka
Lacunes de masquageChamps sensibles configurables
Volume de donnéesRate limiting, échantillonnage pour les 4xx

Références


Standard Marchemalo : Un architecte vétéran de 40 ans comprend en 30 secondes