03 — Traces distribuées : spans, context propagation, sampling
Ce que tu vas apprendre
- Ce qu'est une trace et un span — la structure de base
- Comment le contexte se propage entre services (W3C TraceContext)
- Les stratégies de sampling : ne pas tout tracer
- Les backends : Jaeger, Tempo, Zipkin
- Exemples TypeScript et Python manuels (avant OpenTelemetry)
Prérequis
Trace et span : les concepts de base
Une trace représente le parcours complet d'une requête à travers le système — de la réception par l'API jusqu'à la réponse finale. Elle est identifiée par un traceId unique.
Un span est une unité de travail dans cette trace. Chaque appel de service, chaque requête BDD, chaque opération significative est un span. Un span a :
- Un
spanIdunique - Un
traceId(partagé avec tous les spans de la même trace) - Un
parentSpanId(sauf le span racine) - Un timestamp de début et de fin
- Des attributs (clés-valeurs)
- Un statut (
OK,ERROR)
Trace "abc123" — requête POST /orders (durée : 234ms)
│
├── Span "api-gateway" [0ms → 8ms]
│ method: POST, route: /orders
│
├── Span "order-service.create" [8ms → 180ms] ← span racine du service
│ orderId: ord-456
│ ├── Span "postgres.query" [10ms → 130ms] ← goulot d'étranglement
│ │ db.statement: "INSERT INTO orders..."
│ │ db.rows_affected: 1
│ └── Span "payment-service.charge" [131ms → 178ms]
│ rpc.method: Charge
│ payment.status: success
│
└── Span "notification.send" [180ms → 234ms]
notification.type: email
notification.recipient: alice@exemple.fr
En regardant cette trace, tu identifies immédiatement que la requête PostgreSQL de 120ms est responsable de 51% de la latence totale.
Le contexte de trace et sa propagation
Pour qu'un span dans le service B soit rattaché à la trace du service A, le traceId et le spanId doivent être transmis entre les deux.
Le standard W3C TraceContext (RFC 7528) définit deux headers HTTP :
traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
^ ^ ^ ^
version traceId (128 bits) parentSpanId flags
(64 bits)
tracestate: vendorA=value1,vendorB=value2
Propagation manuelle (avant OpenTelemetry) :
typescript// Service A — injecter le contexte dans la requête sortante
async function callOrderService(
traceId: string,
spanId: string,
payload: unknown
): Promise<unknown> {
const traceparent = `00-${traceId}-${spanId}-01`;
return fetch("http://order-service/orders", {
method: "POST",
headers: {
"Content-Type": "application/json",
"traceparent": traceparent,
},
body: JSON.stringify(payload),
});
}
// Service B — extraire le contexte de la requête entrante
function extractTraceContext(req: Request): { traceId: string; parentSpanId: string } | null {
const traceparent = req.headers["traceparent"] as string;
if (!traceparent) return null;
const parts = traceparent.split("-");
if (parts.length !== 4) return null;
return { traceId: parts[1], parentSpanId: parts[2] };
}
En Python :
pythonimport re
def extract_trace_context(headers: dict) -> dict | None:
traceparent = headers.get("traceparent")
if not traceparent:
return None
parts = traceparent.split("-")
if len(parts) != 4:
return None
return {"trace_id": parts[1], "parent_span_id": parts[2]}
def inject_trace_context(headers: dict, trace_id: str, span_id: str) -> dict:
return {
**headers,
"traceparent": f"00-{trace_id}-{span_id}-01",
}
En pratique, tu n'implémentes pas ça manuellement — OpenTelemetry le fait pour toi (voir l'article 04). Mais comprendre le mécanisme sous-jacent est indispensable pour déboguer les traces cassées.
Pourquoi les traces "cassent"
Une trace est cassée quand un span n'est pas rattaché au bon parent. Causes fréquentes :
1. Header non propagé. Un service intermédiaire (proxy, gateway) supprime le header traceparent au lieu de le transmettre.
2. Bibliothèque non instrumentée. Un appel HTTP via urllib (Python) ou node-fetch (Node.js) sans instrumentation OTel ne propage pas le contexte.
3. Contexte perdu dans un worker async. Un task queue (BullMQ, Celery) ne transmet pas automatiquement le contexte de trace à la tâche exécutée plus tard.
typescript// Problème : le contexte de trace est perdu quand la tâche est exécutée
queue.add("send-email", { orderId, email });
// Solution : sérialiser le contexte avec la tâche
import { context, propagation } from "@opentelemetry/api";
const carrier: Record<string, string> = {};
propagation.inject(context.active(), carrier);
queue.add("send-email", { orderId, email, traceContext: carrier });
// Dans le worker : restaurer le contexte
queue.process("send-email", async (job) => {
const ctx = propagation.extract(context.active(), job.data.traceContext);
await context.with(ctx, async () => {
// La trace est restaurée ici
await sendEmail(job.data.email);
});
});
Les stratégies de sampling
Tracer 100% des requêtes en production est rarement souhaitable — le coût en stockage et en performance devient prohibitif au-delà de quelques milliers de requêtes par seconde.
Head-based sampling (décision à l'entrée)
La décision de tracer ou non est prise au démarrage de la trace, avant de connaître son issue.
typescript// Tracer 10% des requêtes aléatoirement
const sampler = new TraceIdRatioBasedSampler(0.1);
// Toujours tracer les requêtes d'erreur — impossible avec head-based seul
Avantage : simple, faible overhead. Inconvénient : tu peux rater les traces d'erreur si elles ne sont pas samplées.
Tail-based sampling (décision à la fin)
La décision est prise après la complétion de la trace. Le collecteur peut décider de garder toutes les traces d'erreur ou celles dont la latence dépasse un seuil.
yaml# Configuration OpenTelemetry Collector — tail sampling
processors:
tail_sampling:
decision_wait: 10s
policies:
- name: errors-policy
type: status_code
status_code: { status_codes: [ERROR] }
- name: slow-requests-policy
type: latency
latency: { threshold_ms: 1000 }
- name: probabilistic-policy
type: probabilistic
probabilistic: { sampling_percentage: 5 }
Avantage : garde les traces intéressantes (erreurs, lenteur). Inconvénient : nécessite un collecteur avec mémoire (le collecteur doit attendre la fin de la trace).
Règle pratique
- Développement : 100% (tout tracer)
- Production basse charge (< 100 req/s) : 100%
- Production moyenne charge (100-1000 req/s) : tail-based sampling, toujours garder les erreurs
- Production haute charge (> 1000 req/s) : head-based 5-10% + tail-based pour les erreurs
Les backends de traces
| Backend | Open source | Hébergé | Point fort |
|---|---|---|---|
| Jaeger | Oui | Oui (Jaegertracing.io) | Simple à déployer, UI intuitive |
| Grafana Tempo | Oui | Oui (Grafana Cloud) | Intégration Grafana + Loki |
| Zipkin | Oui | Non | Léger, historique |
| Datadog APM | Non | Oui | Corrélation logs+métriques+traces |
| Honeycomb | Non | Oui | Observability-driven development |
En self-hosted : Jaeger ou Grafana Tempo (s'intègre avec Prometheus + Loki pour une stack complète).
Attributs sémantiques standard
OpenTelemetry définit des attributs sémantiques standardisés — les utiliser permet aux outils d'interpréter automatiquement les traces.
typescript// HTTP
span.setAttribute("http.method", "POST");
span.setAttribute("http.url", "https://api.exemple.fr/orders");
span.setAttribute("http.status_code", 201);
// Base de données
span.setAttribute("db.system", "postgresql");
span.setAttribute("db.name", "orders");
span.setAttribute("db.statement", "INSERT INTO orders (...)");
span.setAttribute("db.rows_affected", 1);
// Messaging (Kafka, RabbitMQ)
span.setAttribute("messaging.system", "kafka");
span.setAttribute("messaging.destination", "orders");
span.setAttribute("messaging.operation", "publish");
// Métier (custom)
span.setAttribute("app.order.id", orderId);
span.setAttribute("app.order.total_cents", totalCents);
Résumé
| Concept | Définition |
|---|---|
| Trace | Parcours complet d'une requête — identifiée par un traceId |
| Span | Une opération dans la trace — spanId, parentSpanId, timestamps, attributs |
| Context propagation | Transmission du traceId entre services via traceparent (W3C) |
| Head sampling | Décision à l'entrée — simple mais perd les erreurs non samplées |
| Tail sampling | Décision à la fin — garde les traces intéressantes, plus coûteux |
Étape suivante : 04 — OpenTelemetry — instrumenter automatiquement logs, métriques et traces avec un seul SDK.
Sources
- OpenTelemetry Authors. Traces. opentelemetry.io/docs/concepts/signals/traces/
- W3C. Trace Context Recommendation. w3.org/TR/trace-context/ (Standard traceparent)
- Majors, C., Fong-Jones, L., & Miranda, G. (2022). Observability Engineering, Chapitre 4 "Distributed Tracing". O'Reilly.
- Shkuro, Y. (2019). Mastering Distributed Tracing. Packt Publishing.
- Beyer, B., et al. (2016). Site Reliability Engineering, Chapitre 6. O'Reilly.