
OpenTelemetry (OTel) est le standard open source pour instrumenter vos applications : vous générez traces, métriques et logs une seule fois, puis vous les envoyez vers n'importe quel backend. Pas de vendor lock-in, pas de ré-instrumentation si vous changez de stack d'observabilité.
Ce guide s'adresse aux profils débutants et intermédiaires qui veulent rendre leurs services observables. Vous irez de votre première trace jusqu'à une configuration prête pour la production : quickstart, auto-instrumentation, spans métier, configuration et sampling.
Ce que vous allez apprendre
Section intitulée « Ce que vous allez apprendre »- Comprendre le modèle en couches d'OpenTelemetry (API, SDK, Collector).
- Voir passer une première trace avec un Collector de test.
- Instrumenter une application en auto puis en manuel.
- Configurer OTel proprement par variables d'environnement.
- Préparer la production : sampling, propagation, conventions.
Pourquoi OpenTelemetry ?
Section intitulée « Pourquoi OpenTelemetry ? »Avant OpenTelemetry, chaque outil d'observabilité imposait son propre agent et son propre format. Changer de backend obligeait à réinstrumenter toutes les applications. OTel casse ce couplage : une instrumentation unique, un protocole unique (OTLP), et le choix du backend devient une simple décision d'infrastructure. C'est ce qui en a fait le standard de fait.
| Question | Réponse |
|---|---|
| C'est quoi ? | Un ensemble d'APIs, SDKs et outils pour générer de la télémétrie (traces, métriques, logs) |
| Qui le maintient ? | CNCF (Cloud Native Computing Foundation), projet Graduated depuis le 11 mai 2026 |
| Pourquoi l'utiliser ? | Standard unique, multi-langage, multi-backend, communauté active |
| Quels backends ? | Jaeger, Tempo, Prometheus, Mimir, Loki, Datadog, New Relic, Splunk... |
Mental model : comment les briques s'assemblent
Section intitulée « Mental model : comment les briques s'assemblent »Avant de coder, comprenez les 5 couches :
| Couche | Ce qu'elle fait | Où elle vit |
|---|---|---|
| API | Interface pour créer spans, métriques, logs | Dans votre code |
| SDK | Implémentation : buffering, sampling, export | Runtime de l'app |
| Auto-instrumentation | Instrumente automatiquement les frameworks | Agent/wrapper autour de l'app |
| Exporter OTLP | Envoie les signaux en OTLP | Dans l'app (SDK) |
| Collector | Reçoit, transforme, route | Infrastructure (container/daemon) |
Quickstart : votre première trace (10 min)
Section intitulée « Quickstart : votre première trace (10 min) »Objectif : voir une trace passer avant de coder quoi que ce soit.
-
Lancez un Collector avec export debug
Fenêtre de terminal docker run --rm -p 4317:4317 -p 4318:4318 \otel/opentelemetry-collector-contrib:0.155.0 \--config='receivers:otlp:protocols:grpc:endpoint: 0.0.0.0:4317http:endpoint: 0.0.0.0:4318exporters:debug:verbosity: detailedservice:pipelines:traces:receivers: [otlp]exporters: [debug]'Le Collector affiche les traces reçues dans la console.
-
Envoyez une trace de test
Avec
telemetrygen(outil de test OTel) :Fenêtre de terminal docker run --rm --network host ghcr.io/open-telemetry/opentelemetry-collector-contrib/telemetrygen:0.155.0 \traces --otlp-insecure --traces 1 -
Vérifiez la sortie
Vous devez voir dans les logs du Collector :
Span #0Name : lets-goKind : ClientSpan #1Name : okey-dokey-0Kind : ServerCes deux noms sont codés en dur dans
telemetrygen:lets-goest le span racine de type client,okey-dokey-0le span enfant de type serveur qui simule le service appelé. Voir les deux confirme que le contexte de trace a bien été propagé du parent vers l'enfant.
Vous avez un pipeline fonctionnel. Maintenant, instrumentons une vraie application.
Choisir son mode : auto vs manuel
Section intitulée « Choisir son mode : auto vs manuel »La question n'est pas « auto ou manuel » mais « dans quel ordre ». Les deux approches produisent des spans dans la même trace et se combinent sans conflit : l'auto-instrumentation intercepte les bibliothèques que vous n'avez pas écrites (client HTTP, pilote SQL, consommateur Kafka), l'instrumentation manuelle décrit ce que votre code fait entre ces appels. Lisez la troisième ligne du tableau comme la cible réelle en production, les deux premières comme des étapes vers elle.
| Mode | Quand l'utiliser | Ce que ça couvre |
|---|---|---|
| Auto-instrumentation | Démarrage rapide, legacy, frameworks standards | HTTP, SQL, Redis, Kafka, gRPC... |
| Instrumentation manuelle | Logique métier, spans personnalisés | Checkout, paiement, génération PDF... |
| Les deux | Production réelle | Auto = "plomberie", manuel = "métier" |
Auto-instrumentation par langage
Section intitulée « Auto-instrumentation par langage »L'auto-instrumentation injecte des traces sans modifier votre code pour les frameworks courants (HTTP, DB, messaging). Le mécanisme diffère selon le langage et cela change ce que vous devez déployer : Java repose sur un agent JVM chargé au démarrage, Python sur un lanceur (opentelemetry-instrument) qui monkey-patche les bibliothèques, Node.js sur un module chargé avant le code applicatif avec --require, .NET sur un enregistrement explicite dans le conteneur d'injection de dépendances. Go fait exception : sa compilation statique interdit le monkey-patching, l'instrumentation passe donc par des wrappers explicites (ou par le projet séparé opentelemetry-go-instrumentation, qui s'appuie sur eBPF).
# Installer la distro (inclut SDK + instrumentations courantes)pip install opentelemetry-distroopentelemetry-bootstrap -a install
# Lancer en auto-instrumentationopentelemetry-instrument python app.pyConfiguration via env vars (recommandé) :
export OTEL_SERVICE_NAME=checkout-apiexport OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4317# Optionnel : debug localexport OTEL_TRACES_EXPORTER=console# Télécharger l'agent (vérifiez la dernière version sur opentelemetry.io/docs/languages/java/)curl -L -o opentelemetry-javaagent.jar \ https://github.com/open-telemetry/opentelemetry-java-instrumentation/releases/latest/download/opentelemetry-javaagent.jar
# Lancer avec l'agentjava -javaagent:opentelemetry-javaagent.jar \ -jar myapp.jarConfiguration via env vars :
export OTEL_SERVICE_NAME=order-serviceexport OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4317L'agent instrumente automatiquement : Spring Boot, Quarkus, JAX-RS, JDBC, Kafka, Redis...
const { NodeSDK } = require('@opentelemetry/sdk-node');const { getNodeAutoInstrumentations } = require('@opentelemetry/auto-instrumentations-node');const { OTLPTraceExporter } = require('@opentelemetry/exporter-trace-otlp-grpc');
const sdk = new NodeSDK({ traceExporter: new OTLPTraceExporter(), instrumentations: [getNodeAutoInstrumentations()],});
sdk.start();# Installernpm install @opentelemetry/sdk-node @opentelemetry/auto-instrumentations-node @opentelemetry/exporter-trace-otlp-grpc
# LancerOTEL_SERVICE_NAME=frontend \OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4317 \node --require ./tracing.js app.jsusing OpenTelemetry.Trace;using OpenTelemetry.Resources;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddOpenTelemetry() .ConfigureResource(r => r.AddService("my-service")) .WithTracing(tracing => tracing .AddAspNetCoreInstrumentation() .AddHttpClientInstrumentation() .AddOtlpExporter());Configuration via env vars :
export OTEL_SERVICE_NAME=payment-apiexport OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4317Go n'a pas d'auto-instrumentation "magique", vous devez wrapper les packages manuellement. Utilisez les instrumentations officielles pour net/http, database/sql, gRPC, etc.
import "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
// Wrapper HTTP clientclient := &http.Client{Transport: otelhttp.NewTransport(http.DefaultTransport)}
// Wrapper HTTP serverhandler := otelhttp.NewHandler(myHandler, "my-server")Instrumentation manuelle : spans métier
Section intitulée « Instrumentation manuelle : spans métier »L'auto couvre les "tuyaux" (HTTP, DB). Pour tracer la logique métier, ajoutez des spans manuels.
from opentelemetry import trace
tracer = trace.get_tracer(__name__)
def process_checkout(cart_id: str, user_id: str): with tracer.start_as_current_span("checkout.process") as span: # Attributs métier span.set_attribute("cart.id", cart_id) span.set_attribute("user.id", user_id) span.set_attribute("cart.items_count", 5)
# Span enfant with tracer.start_as_current_span("checkout.validate_payment"): validate_payment()
with tracer.start_as_current_span("checkout.reserve_stock"): reserve_stock()import io.opentelemetry.api.GlobalOpenTelemetry;import io.opentelemetry.api.trace.Span;import io.opentelemetry.api.trace.Tracer;
public class CheckoutService { private static final Tracer tracer = GlobalOpenTelemetry.getTracer("checkout-service");
public void processCheckout(String cartId) { Span span = tracer.spanBuilder("checkout.process").startSpan(); try (var scope = span.makeCurrent()) { span.setAttribute("cart.id", cartId); validatePayment(); reserveStock(); } finally { span.end(); } }}const opentelemetry = require('@opentelemetry/api');
const tracer = opentelemetry.trace.getTracer('checkout-service');
async function processCheckout(cartId, userId) { return tracer.startActiveSpan('checkout.process', async (span) => { span.setAttribute('cart.id', cartId); span.setAttribute('user.id', userId);
await tracer.startActiveSpan('checkout.validate_payment', async (paymentSpan) => { await validatePayment(); paymentSpan.end(); });
span.end(); });}import ( "context" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute")
var tracer = otel.Tracer("checkout-service")
func ProcessCheckout(ctx context.Context, cartID, userID string) error { ctx, span := tracer.Start(ctx, "checkout.process") defer span.End()
span.SetAttributes( attribute.String("cart.id", cartID), attribute.String("user.id", userID), )
if err := validatePayment(ctx); err != nil { span.RecordError(err) return err }
return reserveStock(ctx)}Gérer les erreurs
Section intitulée « Gérer les erreurs »Un span dont le code a levé une exception reste vert par défaut : le SDK ne
devine pas qu'il y a eu un incident. Deux appels distincts sont nécessaires et
ils ne font pas la même chose. set_status(StatusCode.ERROR) marque le span
comme en échec, c'est ce qui le fait ressortir en rouge dans le backend et ce
sur quoi vos alertes se déclenchent. record_exception() attache la trace de
pile au span sous forme d'événement, ce qui donne le contexte du diagnostic.
Le raise final est indispensable : sans lui, vous auriez tracé une erreur tout
en la masquant à l'appelant.
from opentelemetry.trace import Status, StatusCode
try: result = process_payment()except Exception as e: span.set_status(Status(StatusCode.ERROR, str(e))) span.record_exception(e) raiseConfiguration universelle (env vars)
Section intitulée « Configuration universelle (env vars) »Ne hardcodez pas les endpoints. Utilisez les variables d'environnement standard OTel :
# Identité du service (obligatoire)OTEL_SERVICE_NAME=checkout-apiOTEL_RESOURCE_ATTRIBUTES=service.version=1.2.0,deployment.environment=production
# Export OTLPOTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4317OTEL_EXPORTER_OTLP_PROTOCOL=grpc # ou http/protobuf
# Sampling (production)OTEL_TRACES_SAMPLER=parentbased_traceidratioOTEL_TRACES_SAMPLER_ARG=0.1 # 10% des traces
# PropagationOTEL_PROPAGATORS=tracecontext,baggage
# Debug (dev uniquement)OTEL_TRACES_EXPORTER=console # affiche dans stdoutUne nuance à connaître : OTEL_SERVICE_NAME, OTEL_RESOURCE_ATTRIBUTES,
OTEL_TRACES_SAMPLER, OTEL_PROPAGATORS et les variables OTEL_*_EXPORTER
sont spécifiées et se comportent de la même façon dans tous les langages.
Beaucoup d'autres variables que vous croiserez dans les billets de blog, à
commencer par OTEL_LOG_LEVEL, ne figurent pas dans la spécification et ne sont
reconnues que par certains SDK. Avant d'en mettre une dans un manifeste de
production, vérifiez-la dans la
configuration SDK
du langage concerné.
Production : les 5 points critiques
Section intitulée « Production : les 5 points critiques »1. Sampling : ne tracez pas 100%
Section intitulée « 1. Sampling : ne tracez pas 100% »En production, tracer 100 % des requêtes coûte cher en stockage et ajoute de la
latence sur chaque appel. Le point à comprendre est que la décision
d'échantillonnage se prend au début de la trace et se propage ensuite : c'est
tout l'intérêt de la variante parentbased_, qui garantit qu'une trace est
complète ou absente, jamais amputée de ses derniers services. Un sampler non
parentbased appliqué service par service produit des traces trouées, où le
span racine existe mais pas l'appel à la base qui a mis trois secondes.
| Sampler | Description |
|---|---|
always_on | Tout (dev uniquement) |
traceidratio | X% des traces |
parentbased_traceidratio | X% sauf si parent a décidé (recommandé) |
OTEL_TRACES_SAMPLER=parentbased_traceidratioOTEL_TRACES_SAMPLER_ARG=0.1 # 10%2. Propagation : W3C Trace Context
Section intitulée « 2. Propagation : W3C Trace Context »La propagation transmet le trace-id entre services. Sans elle, vos traces sont fragmentées.
traceparent: 00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01Par défaut, OTel utilise W3C Trace Context. Si vous avez du legacy B3 (Zipkin) :
OTEL_PROPAGATORS=tracecontext,baggage,b3multi3. Semantic Conventions : attributs standards
Section intitulée « 3. Semantic Conventions : attributs standards »Les Semantic Conventions sont la liste normalisée des noms d'attributs. Leur
intérêt n'est pas cosmétique : les tableaux de bord préconstruits, les règles
d'alerte et les fonctions de détection d'anomalie des backends reposent sur ces
noms exacts. Un attribut nommé http_status au lieu de
http.response.status_code reste visible dans la trace, mais aucun outil ne
l'exploitera automatiquement. Le tableau ci-dessous ne donne que les domaines
les plus courants ; la spécification
complète en couvre plusieurs
dizaines.
| Domaine | Attributs |
|---|---|
| HTTP | http.request.method, url.path, http.response.status_code |
| Database | db.system.name, db.namespace, db.operation.name, db.query.text |
| Messaging | messaging.system, messaging.destination.name |
4. Logs : corrélation avec les traces
Section intitulée « 4. Logs : corrélation avec les traces »Sans corrélation, un log d'erreur vous dit qu'un paiement a échoué mais pas dans
quelle requête ni après quels appels. En injectant trace_id et span_id dans
chaque ligne de log, vous rendez possible le passage direct du message d'erreur
à la trace complète, et inversement. La condition est que le log soit
structuré (JSON ou logfmt) : un message texte libre contenant l'identifiant
ne sera pas indexé comme un champ et restera inexploitable par le backend.
{ "timestamp": "2026-02-09T10:30:00Z", "level": "ERROR", "message": "Payment failed: insufficient funds", "trace_id": "abc123def456...", "span_id": "789xyz...", "service.name": "payment-service"}5. Collector : découplage et gouvernance
Section intitulée « 5. Collector : découplage et gouvernance »En production, ne pas exporter directement vers les backends. Passez par un Collector :
App (SDK) → OTLP → Collector → BackendsPourquoi ?
- Découplage : changez de backend sans recoder
- Résilience : le Collector bufferise si backend down
- Gouvernance : filtrage, redaction PII, enrichissement K8s
- Multi-destinations : traces → Tempo, métriques → Mimir
Anti-patterns (ce qu'il ne faut PAS faire)
Section intitulée « Anti-patterns (ce qu'il ne faut PAS faire) »Les deux premières lignes du tableau expliquent la quasi-totalité des factures d'observabilité qui dérapent. Un identifiant unique placé en attribut multiplie le nombre de séries distinctes par le nombre d'utilisateurs, et cette cardinalité est ce que facturent la plupart des backends. Les deux suivantes sont des risques de sécurité et d'architecture, les trois dernières dégradent l'exploitabilité sans coûter d'argent. Traitez-les dans cet ordre.
| Anti-pattern | Conséquence | Solution |
|---|---|---|
| IDs uniques en attributs | Explosion cardinalité, coûts | Utiliser des catégories (status, region) |
| Tracer 100% en prod | Overhead, coûts stockage | Sampling 10-20% |
| PII/secrets en attributs | Fuite de données | Redaction via Collector |
| Exporter direct vers backend | Vendor lock-in, pas de gouvernance | Passer par Collector |
Pas de service.name | Traces anonymes, inutilisables | Toujours définir OTEL_SERVICE_NAME |
| Ignorer la propagation | Spans orphelins, traces fragmentées | Vérifier propagators + headers |
| Spans trop verbeux | Bruit, difficile à exploiter | 5-10 attributs max par span |
Dépannage
Section intitulée « Dépannage »Checklist « je ne vois pas mes traces »
Section intitulée « Checklist « je ne vois pas mes traces » »Suivez ces cinq points dans l'ordre, du plus proche du code au plus proche
du backend. Chacun isole un maillon de la chaîne, et s'arrêter au premier qui
échoue vous évite de chercher un problème réseau alors que le SDK ne produit
rien. Le point 1 est de loin le plus rentable : si console n'affiche aucun
span, le problème est dans l'instrumentation, pas dans l'export.
-
Le SDK génère-t-il des spans ?
Fenêtre de terminal OTEL_TRACES_EXPORTER=console python app.py# → Vous devez voir des spans dans stdout -
Le Collector reçoit-il les données ?
Activez le debug exporter dans le Collector :
exporters:debug:verbosity: detailed -
Le réseau est-il OK ?
Fenêtre de terminal # Depuis le container de l'appcurl -v http://otel-collector:4318/v1/traces# → Doit répondre (même en erreur 405) -
Les ports sont-ils corrects ?
- 4317 = gRPC
- 4318 = HTTP
Vérifiez que
OTEL_EXPORTER_OTLP_PROTOCOLcorrespond. -
La propagation est-elle configurée ?
Sans propagation, les services ne partagent pas le
trace-id.Fenêtre de terminal OTEL_PROPAGATORS=tracecontext,baggage
Erreurs courantes
Section intitulée « Erreurs courantes »Ces quatre symptômes se distinguent par ce que vous voyez déjà. Si aucune donnée n'arrive, regardez la première ligne, c'est un problème de transport. Si les spans arrivent mais ne se rattachent pas les uns aux autres, ce sont les lignes 2 et 3 : le contexte de trace se perd, soit entre deux services faute de propagateur, soit à l'intérieur d'un même service quand une tâche asynchrone part sans emporter le contexte courant. Si tout arrive mais que le backend ne sait pas nommer l'émetteur, c'est la dernière ligne.
| Symptôme | Cause probable | Solution |
|---|---|---|
Connection refused :4317 | Collector non démarré ou mauvais port | Vérifier le container/service |
| Spans orphelins (pas de parent) | Propagation manquante | Vérifier OTEL_PROPAGATORS |
| Traces fragmentées | Contexte perdu entre appels async | Transmettre le contexte explicitement (trace.use_span() ou trace.set_span_in_context() en Python, passage du ctx en Go) |
Unknown service dans le backend | OTEL_SERVICE_NAME non défini | Définir la variable |
Métriques (bref aperçu)
Section intitulée « Métriques (bref aperçu) »OTel ne se limite pas aux traces : le même SDK produit aussi des métriques, avec
le même service.name et les mêmes attributs de ressource, ce qui permet de
passer d'un graphe de latence aux traces correspondantes. Le choix du type
d'instrument n'est pas une préférence de style, il détermine ce que le backend
peut calculer. Un Counter ne sait répondre qu'à « combien depuis le
démarrage », un Histogram sait répondre à « quel est le 95ᵉ centile », et
seul ce dernier permet de poser un objectif de niveau de service sur la latence.
| Type | Usage | Exemple |
|---|---|---|
| Counter | Valeurs cumulatives | Requêtes, erreurs |
| Gauge | Valeur instantanée | Mémoire, connexions actives |
| Histogram | Distribution | Latence (p50, p95, p99) |
from opentelemetry import metrics
meter = metrics.get_meter(__name__)
# Counterrequest_counter = meter.create_counter("http.server.requests")request_counter.add(1, {"method": "GET", "status": "200"})
# Histogram (latence)latency_histogram = meter.create_histogram("http.server.duration", unit="ms")latency_histogram.record(45.2, {"method": "GET", "route": "/api/orders"})FAQ : questions fréquentes
Section intitulée « FAQ : questions fréquentes »Les questions ci-dessous reviennent systématiquement lors d'une première mise en place : par où démarrer, comment doser auto et manuel, quel port OTLP choisir, et faut-il vraiment déployer un Collector.
OTEL_EXPORTER_OTLP_ENDPOINT, puis on ajoute des spans manuels pour la logique métier que l'auto ne capture pas.À retenir
Section intitulée « À retenir »- OTel = standard CNCF pour traces, métriques, logs, choisissez-le par défaut
- Auto-instrumentation couvre 80% sans modifier le code
- Spans manuels pour la logique métier (checkout, paiement...)
- Configurez via env vars (
OTEL_*), pas en dur dans le code - Passez par un Collector en production (découplage, gouvernance)
- Sampling + redaction = obligatoires en prod
Prochaines étapes
Section intitulée « Prochaines étapes »Ressources
Section intitulée « Ressources »- Documentation officielle
- SDK Configuration (env vars)
- OTLP Exporter Configuration
- Semantic Conventions
- Specification Status, maturité des signaux par langage
- Registry (instrumentations)