Aller au contenu principal

ADR-021 : Observabilité pilotée par UAC — Debug Chirurgical sans tcpdump

Décision : Implémenter un système d'observabilité pilotée par le contrat UAC, avec activation dynamique du debug réseau bas niveau sans polluer Prometheus/Loki.

Statut : Brouillon

Date : 2026-01-23

Linear : CAB-892


Contexte — La Douleur Réelle

Situation actuelle chez les clients :

  1. Erreur aléatoire en prod sur un client spécifique
  2. Ops planifie un tcpdump en HNO (3h du matin)
  3. Capture 50 Go de trafic pendant 2h
  4. Télécharge le pcap, ouvre Wireshark, cherche l'aiguille
  5. Disque plein → incident secondaire
  6. Au final : 3 paquets utiles sur 10 millions

Coût : MTTR multiplié par 10, stress équipes, risque incident secondaire.


Décision

Implémenter "Observability as Contract" dans le UAC avec :

  1. Niveaux déclaratifs : metrics → traces → debug → full
  2. Activation dynamique : via Console/API, ciblée par client/endpoint
  3. Auto-extinction : sécurité anti-oubli après durée définie
  4. Export OTel : données filtrées vers le backend approprié

Architecture — Isolation Tenant via JWT

┌─────────────────────────────────────────────────────────────────┐
│ Request entrant │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ Authorization: Bearer eyJhbGc... │ │
│ └───────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ JWT Decode (no verify, juste peek pour tagging) │ │
│ │ → tenant_id: "acme-corp" │ │
│ │ → client_id: "app-payment-v2" │ │
│ │ → sub: "service-account-xyz" │ │
│ └───────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ OTel Span Tags (automatiques) │ │
│ │ tenant_id = "acme-corp" │ │
│ │ client_id = "app-payment-v2" │ │
│ │ correlation_id = "req-abc-123" │ │
│ └───────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ Grafana Query Filter │ │
│ │ WHERE tenant_id = "acme-corp" │ │
│ │ → Isolation garantie, pas de leak cross-tenant │ │
│ └───────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘

Sources de corrélation tenant :

  • JWT claims : tenant_id, azp (authorized party), client_id
  • mTLS : CN du certificat client
  • API Key : lookup vers tenant associé

Niveaux d'Observabilité

ModeCe qui remonteBackendCoûtRétention
metricslatency, status, countPrometheus~015j
tracesTrace ID + spansTempo/JaegerFaible7j
debugTCP timing, headers, TLSOTel → LokiTemporaire24h
fullTout (équivalent tcpdump)Export fichierPonctuelSession

Extension du Schéma UAC

# Dans le contrat UAC
observability:
default_level: "metrics" # Prometheus léger (toujours)
on_error: "traces" # OTel traces auto si 5xx

debug_mode:
enabled: false # Activable via Console/API
auto_disable_after: "10m" # Sécurité anti-oubli
max_duration: "1h" # Hard limit

filters: # Ciblage chirurgical
- client_id: "client-xyz"
- endpoint: "/payments/*"
- status_code: [500, 502, 503]
- header_match:
X-Request-ID: "req-*"

capture: # Ce qu'on capture
- tcp_timing # Connect, TTFB, total
- tls_handshake # Durée, cipher, cert info
- headers # Request + Response
- body_hash # SHA-256, pas le body (RGPD)

exclude: # Ce qu'on ne capture JAMAIS
- headers: ["Authorization", "Cookie", "X-API-Key"]
- body: true # Jamais le body en clair

API d'Activation Dynamique

# Activer debug pour un client spécifique
POST /v1/debug/sessions
{
"scope": {
"tenant_id": "acme-corp",
"client_id": "payment-service",
"endpoint": "/api/v1/payments/*"
},
"capture": ["tcp_timing", "headers", "tls_handshake"],
"duration": "10m",
"reason": "Investigating intermittent 502 errors"
}

# Response
{
"session_id": "debug-session-abc123",
"expires_at": "2026-01-23T22:10:00Z",
"grafana_url": "https://grafana.stoa.internal/d/debug?session=abc123"
}
# Lister sessions actives
GET /v1/debug/sessions?tenant_id=acme-corp

# Arrêter une session
DELETE /v1/debug/sessions/debug-session-abc123

Format de Sortie — JSON Interprété

Pas de pcap brut. Un JSON lisible avec diagnostic automatique :

{
"request_id": "req-abc-123",
"tenant_id": "acme-corp",
"timestamp": "2026-01-23T21:45:12.345Z",

"timeline": [
{"event": "request_received", "time_us": 0},
{"event": "tcp_connect", "target": "backend:8080", "duration_us": 1200},
{"event": "tls_handshake", "duration_us": 45000, "cipher": "TLS_AES_256_GCM_SHA384"},
{"event": "request_sent", "bytes": 2048},
{"event": "first_byte", "wait_us": 230000},
{"event": "response_complete", "bytes": 15360, "status": 200}
],

"metrics": {
"total_duration_ms": 276.2,
"tcp_connect_ms": 1.2,
"tls_handshake_ms": 45.0,
"ttfb_ms": 230.0,
"transfer_ms": 0.0
},

"diagnosis": {
"verdict": "BACKEND_SLOW",
"detail": "TTFB 230ms indicates slow backend processing",
"recommendation": "Check backend /payments endpoint performance"
}
}

Sécurité & Conformité

AspectMesure
RGPDJamais de body en clair, hash SHA-256 uniquement
SecretsHeaders sensibles exclus (Auth, Cookie, API-Key)
Multi-tenantIsolation par tenant_id, pas de leak possible
AuditToute activation loggée avec raison et utilisateur
Auto-expirySessions debug auto-désactivées
RBACSeuls les tenant-admin+ peuvent activer le debug

Implémentation Rust (MCP Gateway)

use jsonwebtoken::{decode, DecodingKey, Validation, Algorithm};
use opentelemetry::trace::Span;

// Extraire tenant_id du JWT (sans vérifier signature)
fn extract_tenant_from_jwt(token: &str) -> Option<String> &#123;
let mut validation = Validation::new(Algorithm::RS256);
validation.insecure_disable_signature_validation();
validation.validate_exp = false;

let token_data = decode::<Claims>(token, &DecodingKey::from_secret(&[]), &validation).ok()?;
token_data.claims.tenant_id.or(token_data.claims.azp)
&#125;

// Tagger le span OTel
fn tag_span_with_tenant(span: &mut impl Span, tenant_id: &str, client_id: &str) &#123;
span.set_attribute(KeyValue::new("tenant_id", tenant_id.to_string()));
span.set_attribute(KeyValue::new("client_id", client_id.to_string()));
&#125;

Positionnement Marché

CapacitéGateways TraditionnelsSTOA
Debug réseauOutillage limité ou externeTCP/TLS/HTTP natif
Activation dynamiqueGénéralement non disponibleVia contrat UAC
Multi-tenant sécuriséVariable selon l'implémentationIsolation JWT par conception
Auto-expiryNettoyage manuel requisAuto-désactivation intégrée

Différenciateur clé : Débogage en production sans tcpdump.


Métriques

# Debug sessions
stoa_debug_sessions_active{tenant_id}
stoa_debug_sessions_total{tenant_id, outcome="completed|expired|cancelled"}
stoa_debug_capture_bytes_total{tenant_id, capture_type}

# Performance impact (doit rester ~0 quand debug off)
stoa_observability_overhead_us{level="metrics|traces|debug"}

Phases d'Implémentation

PhaseScopeCycle
1UAC Schema + ParserCycle 9
2Runtime capture (Rust)Cycle 9-10
3API activation + Console UICycle 10
4Dashboard GrafanaCycle 10

Liens