OpenTelemetry en production : traces, contexte, et ce qui compte vraiment
Patterns OpenTelemetry en production. Propagation de contexte à travers les files et workers, traçage des appels LLM, stratégies d'échantillonnage pour les charges IA, spans respectueux de la vie privée, et l'API Baggage.
Pourquoi OpenTelemetry a gagné
Il y a trois ans, le paysage de l'observabilité était fragmenté. Jaeger pour le traçage, Prometheus pour les métriques, Fluentd pour les logs, chacun avec son propre SDK, son propre protocole, son propre verrouillage fournisseur. OpenTelemetry a tout unifié en un seul standard : un SDK, un protocole (OTLP), un collector qui route vers n'importe quel backend.
Nous avons adopté OpenTelemetry dans nos systèmes de production. Cet article couvre les patterns qui comptent vraiment en production, pas le tutoriel d'installation. Pour la stratégie d'observabilité plus large (quand alerter, quoi logger, comment structurer les métriques), consulte notre guide d'observabilité IA. Cet article va plus en profondeur sur l'implémentation spécifique à OpenTelemetry.
Propagation de contexte : la partie la plus difficile
Une seule requête utilisateur peut traverser 5 services, 3 files de messages, 2 processus worker, et un appel LLM. La propagation de contexte garantit que la trace suit la requête à travers tous ces éléments.
Propagation HTTP (facile)
OpenTelemetry auto-instrumente les clients et serveurs HTTP. Le contexte de trace se propage via les headers traceparent et tracestate. Ça fonctionne sans configuration.
// Auto-instrumenté : pas de code nécessaire pour la propagation HTTP
// Le SDK ajoute le header traceparent aux requêtes sortantes
// Le service destinataire l'extrait et continue la trace
Propagation via files de messages (difficile)
Les files de messages cassent la propagation automatique. Quand tu mets un message en file, le contexte de trace doit être sérialisé dans les headers du message. Quand un worker le défile, le contexte doit être extrait et la trace poursuivie.
// Producteur : injecter le contexte de trace dans les headers du message
import { context, propagation } from '@opentelemetry/api';
async function enqueueMessage(queue: string, payload: any) {
const carrier: Record<string, string> = {};
propagation.inject(context.active(), carrier);
await messageQueue.send(queue, {
body: payload,
headers: carrier, // Contient traceparent, tracestate
});
}
// Consommateur : extraire le contexte de trace des headers du message
async function processMessage(message: QueueMessage) {
const parentContext = propagation.extract(context.active(), message.headers);
await context.with(parentContext, async () => {
const span = tracer.startSpan('process_message', {
attributes: {
'messaging.system': 'rabbitmq',
'messaging.operation': 'process',
'messaging.destination': message.queue,
},
});
try {
await handleMessage(message.body);
span.setStatus({ code: SpanStatusCode.OK });
} catch (error) {
span.setStatus({ code: SpanStatusCode.ERROR, message: error.message });
throw error;
} finally {
span.end();
}
});
}
Ce pattern fonctionne pour RabbitMQ, Kafka, BullMQ, SQS, et Symfony Messenger. Les headers du message transportent le contexte de trace. Le consommateur l'extrait et crée des spans enfants sous la trace originale.
Propagation dans les processus worker
Pour les workers BullMQ de Vendure, les workers Symfony Messenger de Pimcore, et les systèmes de jobs en arrière-plan similaires, le pattern est le même : sérialiser le contexte dans le payload du job, l'extraire côté worker.
// BullMQ : ajouter le contexte de trace aux données du job
async function addJob(queue: Queue, data: any) {
const carrier: Record<string, string> = {};
propagation.inject(context.active(), carrier);
await queue.add('process', {
...data,
_traceContext: carrier,
});
}
// BullMQ : extraire le contexte de trace dans le worker
worker.on('process', async (job) => {
const parentContext = propagation.extract(context.active(), job.data._traceContext || {});
await context.with(parentContext, async () => {
const span = tracer.startSpan(`job:${job.name}`);
try {
await processJob(job.data);
} finally {
span.end();
}
});
});
Tracer les appels LLM
Les appels LLM sont les opérations les plus coûteuses dans les systèmes IA. Les tracer avec les bons attributs permet le suivi des coûts, l'analyse de latence, et le monitoring de qualité.
async function tracedLlmCall(prompt: string, options: LlmOptions): Promise<string> {
const span = tracer.startSpan('llm.generate', {
attributes: {
'llm.provider': options.provider, // "openai", "anthropic"
'llm.model': options.model, // "gpt-4o", "claude-sonnet-4-20250514"
'llm.temperature': options.temperature,
'llm.max_tokens': options.maxTokens,
'llm.prompt_tokens': estimateTokens(prompt), // estimation avant l'appel
},
});
try {
const response = await llmClient.generate(prompt, options);
span.setAttributes({
'llm.response_tokens': response.usage.completionTokens,
'llm.total_tokens': response.usage.totalTokens,
'llm.finish_reason': response.finishReason,
'llm.cost_usd': calculateCost(response.usage, options.model),
});
span.setStatus({ code: SpanStatusCode.OK });
return response.text;
} catch (error) {
span.setStatus({ code: SpanStatusCode.ERROR, message: error.message });
span.setAttribute('llm.error_type', error.constructor.name);
throw error;
} finally {
span.end();
}
}
Ne mets PAS le texte du prompt dans les attributs de span. Les prompts contiennent des données personnelles. Les attributs de span sont envoyés à ton backend d'observabilité (Jaeger, Grafana Tempo, Datadog). Logue plutôt le hash du prompt ou le nombre de tokens. Pour l'architecture complète de logging respectueux des données personnelles, consulte notre guide de prévention des fuites de données IA.
Convention d'attributs pour les spans LLM
| Attribut | Type | Exemple |
|---|---|---|
llm.provider | string | "openai" |
llm.model | string | "gpt-4o" |
llm.temperature | float | 0.7 |
llm.prompt_tokens | int | 1250 |
llm.response_tokens | int | 340 |
llm.total_tokens | int | 1590 |
llm.finish_reason | string | "stop" |
llm.cost_usd | float | 0.023 |
llm.error_type | string | "RateLimitError" |
llm.cache_hit | boolean | false |
Stratégies d'échantillonnage
Dans les systèmes IA à haut volume, tracer chaque requête est trop coûteux. L'échantillonnage réduit le volume tout en préservant la visibilité sur les traces importantes.
Échantillonnage en tête de chaîne (head-based)
La décision est prise au début de la trace : échantillonner ou non. Simple mais avec pertes.
// Échantillonner 10% de toutes les traces
const sampler = new TraceIdRatioBased(0.1);
// Toujours échantillonner les erreurs (surcharge du ratio pour les traces en erreur)
const compositeSampler = new ParentBasedSampler({
root: new TraceIdRatioBased(0.1),
// Les erreurs sont toujours échantillonnées via le span processor
});
Échantillonnage en fin de chaîne (tail-based, recommandé pour l'IA)
La décision est prise après que la trace est terminée : la garder ou non. Conserve toutes les traces intéressantes (erreurs, réponses lentes, coûts élevés) et supprime les routinières.
// OpenTelemetry Collector : configuration de l'échantillonnage tail-based
processors:
tail_sampling:
decision_wait: 10s
policies:
# Garder toutes les erreurs
- name: errors
type: status_code
status_code: { status_codes: [ERROR] }
# Garder les traces lentes (> 5 secondes)
- name: slow
type: latency
latency: { threshold_ms: 5000 }
# Garder les appels LLM coûteux (> 0,10 $)
- name: expensive_llm
type: string_attribute
string_attribute:
key: llm.cost_usd
values: [] # Custom : filtrage dans le pipeline
enabled_regex_matching: true
# Échantillonner 5% du reste
- name: baseline
type: probabilistic
probabilistic: { sampling_percentage: 5 }
L'échantillonnage tail-based nécessite l'OpenTelemetry Collector. Le collector met en buffer les traces complètes, évalue les politiques, et ne transmet que les traces échantillonnées au backend. Cela ajoute de la latence (la période decision_wait) mais réduit considérablement les coûts de stockage tout en conservant toutes les données intéressantes.
Spans respectueux de la vie privée
Les attributs de span, les noms de span, et les événements de span sont tous envoyés à ton backend d'observabilité. Si l'un d'entre eux contient des données personnelles, ton infrastructure de traçage devient un risque en matière de protection des données.
// MAUVAIS : données personnelles dans les attributs de span
span.setAttribute('user.email', 'sara.mustermann@beispiel.de');
span.setAttribute('user.name', 'Sara Mustermann');
span.setAttribute('request.body', JSON.stringify(requestBody)); // Contient des données personnelles
// BON : identifiants opaques et données agrégées uniquement
span.setAttribute('user.id', 'usr_abc123'); // ID opaque, pas de données personnelles
span.setAttribute('entities.detected', 3);
span.setAttribute('entities.types', ['person', 'email', 'phone']);
span.setAttribute('policy.applied', 'german-support');
Règles pour un traçage respectueux de la vie privée :
- Identifiants utilisateur : uniquement des identifiants opaques (pas d'emails, pas de noms)
- Corps de requête : ne jamais inclure le contenu brut. Logue les comptages d'entités et les types.
- Prompts LLM : ne jamais inclure. Logue les comptages de tokens et le hash du prompt.
- Messages d'erreur : assainis avant de les attacher aux spans. Supprime toute donnée utilisateur.
L'API Baggage
L'API Baggage d'OpenTelemetry transporte des paires clé-valeur à travers les frontières de services. Contrairement aux attributs de span (qui restent sur le span), le baggage se propage automatiquement à tous les services en aval.
import { propagation, context, baggage } from '@opentelemetry/api';
// Définir le baggage à la passerelle API
const bag = propagation.createBaggage({
'tenant.id': { value: 'tenant_acme' },
'request.priority': { value: 'high' },
'feature.flags': { value: 'new-checkout,beta-search' },
});
const ctx = propagation.setBaggage(context.active(), bag);
// Les services en aval peuvent lire le baggage
const tenantId = propagation.getBaggage(context.active())?.getEntry('tenant.id')?.value;
Utile pour :
- Propagation de l'ID tenant (chaque service en aval sait quel tenant)
- Feature flags (propager les assignations d'expériences entre services)
- Routage par priorité (les requêtes haute priorité reçoivent un traitement de file différent)
- Marqueurs de debug (marquer des requêtes spécifiques pour un logging verbeux)
Le baggage voyage avec le contexte de trace dans les headers HTTP et les métadonnées des messages. Chaque service qui extrait le contexte de trace reçoit aussi le baggage.
Architecture du Collector
L'OpenTelemetry Collector est la couche de routage centrale entre tes applications et tes backends d'observabilité.
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Service A │ │ Service B │ │ Worker C │
│ (OTLP gRPC) │ │ (OTLP HTTP) │ │ (OTLP gRPC) │
└──────┬───────┘ └──────┬───────┘ └──────┬───────┘
│ │ │
▼ ▼ ▼
┌─────────────────────────────────────────────────┐
│ OTel Collector │
│ │
│ Receivers: OTLP (gRPC + HTTP) │
│ Processors: batch, tail_sampling, attributes │
│ Exporters: Tempo, Prometheus, Loki │
└─────────────────────────────────────────────────┘
│ │ │
▼ ▼ ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Grafana │ │ Prometheus │ │ Grafana │
│ Tempo │ │ │ │ Loki │
│ (traces) │ │ (métriques) │ │ (logs) │
└──────────────┘ └──────────────┘ └──────────────┘
Le collector gère le batching (réduit les appels réseau), l'échantillonnage (réduit le stockage), le traitement des attributs (ajoute/supprime des attributs), et le routage (différents signaux vers différents backends). Déploie-le en sidecar ou en service central selon ton infrastructure.
Pour les patterns de déploiement cloud incluant l'infrastructure d'observabilité, cette page couvre notre approche.
Pièges courants
-
Pas de propagation de contexte à travers les files. La propagation HTTP est automatique. La propagation des files ne l'est pas. Si tu n'injectes/extrais pas le contexte dans les headers des messages, les traces se cassent à chaque frontière de file.
-
Des données personnelles dans les attributs de span. Ton backend de traçage indexe tout. Si les spans contiennent des emails, des noms, ou des corps de requête, ton cluster Grafana Tempo est un store de données personnelles.
-
Tracer chaque requête en production. A 1000 RPS, le traçage complet génère des téraoctets de données. Utilise l'échantillonnage tail-based pour garder les erreurs, les traces lentes, et les opérations coûteuses.
-
Pas d'attributs spécifiques aux LLM. Sans comptages de tokens, coût, ID de modèle, et raison de fin sur les spans LLM, tu ne peux pas suivre les coûts IA ni diagnostiquer les problèmes de qualité.
-
L'échantillonnage head-based qui supprime les erreurs. Si tu échantillonnes 10% des traces et qu'une erreur survient dans les 90% que tu supprimes, tu ne la vois jamais. Utilise l'échantillonnage tail-based ou des politiques de capture systématique des erreurs.
-
Le baggage pour de gros payloads. Le baggage voyage avec chaque requête. Des valeurs volumineuses augmentent la taille des headers sur chaque appel HTTP. Garde les valeurs de baggage petites (IDs, flags, priorités).
Points clés à retenir
-
La propagation de contexte à travers les files est la partie la plus difficile. HTTP est automatique. Les files nécessitent l'injection/extraction manuelle du contexte de trace dans les headers des messages. C'est là que la plupart des implémentations de traçage distribué cassent.
-
Trace les appels LLM avec les attributs de coût et de tokens. Modèle, fournisseur, comptages de tokens, coût, raison de fin. Ces attributs permettent les dashboards de coûts IA et le monitoring de qualité.
-
Échantillonnage tail-based pour les charges IA. Garde toutes les erreurs, les traces lentes, et les opérations coûteuses. Supprime les traces routinières. Réduit le stockage de 90%+ tout en conservant toutes les données intéressantes.
-
Pas de données personnelles dans les spans. IDs utilisateur opaques, comptages d'entités, types de tokens. Jamais de contenu brut, d'emails, de noms, ou de corps de requête.
-
Le baggage propage le contexte tenant. Définis l'ID tenant, les feature flags, et la priorité en bordure de réseau. Chaque service en aval le lit depuis le baggage sans passage de paramètres explicite.
Nous implémentons OpenTelemetry dans nos services IA, nos projets de développement logiciel sur mesure, et notre infrastructure cloud. Si tu construis l'observabilité d'un système distribué, parle à notre équipe ou demande un devis.
Sujets couverts
Guides connexes
Guide Entreprise des Systèmes d'IA Agentiques
Guide technique des systemes d'IA agentiques en entreprise. Decouvre l'architecture, les capacites et les applications des agents IA autonomes.
Lire le guideCommerce Agentique : Comment laisser les agents IA acheter en toute securite
Comment concevoir un commerce agentique gouverne. Moteurs de politiques, portes d'approbation HITL, reçus HMAC, idempotence, isolation multi-tenant et le protocole Agentic Checkout complet.
Lire le guideLes 9 endroits où ton système IA laisse fuir des données (et comment colmater chacun)
Cartographie systématique de chaque point de fuite de données dans les systèmes IA. Prompts, embeddings, logs, appels d'outils, mémoire d'agent, messages d'erreur, cache, données de fine-tuning et transferts entre agents.
Lire le guidePrêt à construire des systèmes IA prêts pour la production ?
Notre équipe est spécialisée dans les systèmes IA prêts pour la production. Discutons de comment nous pouvons aider.
Démarrer une conversation