Aller au contenu
Outils medium

OpenTelemetry : Instrumentation universelle

22 min de lecture

logo opentelemetry

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.

  • 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.

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.

QuestionRé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...

Avant de coder, comprenez les 5 couches :

Couches OpenTelemetry : du code applicatif aux backends

CoucheCe qu'elle faitOù elle vit
APIInterface pour créer spans, métriques, logsDans votre code
SDKImplémentation : buffering, sampling, exportRuntime de l'app
Auto-instrumentationInstrumente automatiquement les frameworksAgent/wrapper autour de l'app
Exporter OTLPEnvoie les signaux en OTLPDans l'app (SDK)
CollectorReçoit, transforme, routeInfrastructure (container/daemon)

Objectif : voir une trace passer avant de coder quoi que ce soit.

  1. 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:4317
    http:
    endpoint: 0.0.0.0:4318
    exporters:
    debug:
    verbosity: detailed
    service:
    pipelines:
    traces:
    receivers: [otlp]
    exporters: [debug]
    '

    Le Collector affiche les traces reçues dans la console.

  2. 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
  3. Vérifiez la sortie

    Vous devez voir dans les logs du Collector :

    Span #0
    Name : lets-go
    Kind : Client
    Span #1
    Name : okey-dokey-0
    Kind : Server

    Ces deux noms sont codés en dur dans telemetrygen : lets-go est le span racine de type client, okey-dokey-0 le 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.

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.

ModeQuand l'utiliserCe que ça couvre
Auto-instrumentationDémarrage rapide, legacy, frameworks standardsHTTP, SQL, Redis, Kafka, gRPC...
Instrumentation manuelleLogique métier, spans personnalisésCheckout, paiement, génération PDF...
Les deuxProduction réelleAuto = "plomberie", manuel = "métier"

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).

Fenêtre de terminal
# Installer la distro (inclut SDK + instrumentations courantes)
pip install opentelemetry-distro
opentelemetry-bootstrap -a install
# Lancer en auto-instrumentation
opentelemetry-instrument python app.py

Configuration via env vars (recommandé) :

Fenêtre de terminal
export OTEL_SERVICE_NAME=checkout-api
export OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4317
# Optionnel : debug local
export OTEL_TRACES_EXPORTER=console

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()

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)
raise

Ne hardcodez pas les endpoints. Utilisez les variables d'environnement standard OTel :

Fenêtre de terminal
# Identité du service (obligatoire)
OTEL_SERVICE_NAME=checkout-api
OTEL_RESOURCE_ATTRIBUTES=service.version=1.2.0,deployment.environment=production
# Export OTLP
OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4317
OTEL_EXPORTER_OTLP_PROTOCOL=grpc # ou http/protobuf
# Sampling (production)
OTEL_TRACES_SAMPLER=parentbased_traceidratio
OTEL_TRACES_SAMPLER_ARG=0.1 # 10% des traces
# Propagation
OTEL_PROPAGATORS=tracecontext,baggage
# Debug (dev uniquement)
OTEL_TRACES_EXPORTER=console # affiche dans stdout

Une 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é.

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.

SamplerDescription
always_onTout (dev uniquement)
traceidratioX% des traces
parentbased_traceidratioX% sauf si parent a décidé (recommandé)
Fenêtre de terminal
OTEL_TRACES_SAMPLER=parentbased_traceidratio
OTEL_TRACES_SAMPLER_ARG=0.1 # 10%

La propagation transmet le trace-id entre services. Sans elle, vos traces sont fragmentées.

traceparent: 00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01

Par défaut, OTel utilise W3C Trace Context. Si vous avez du legacy B3 (Zipkin) :

Fenêtre de terminal
OTEL_PROPAGATORS=tracecontext,baggage,b3multi

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.

DomaineAttributs
HTTPhttp.request.method, url.path, http.response.status_code
Databasedb.system.name, db.namespace, db.operation.name, db.query.text
Messagingmessaging.system, messaging.destination.name

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"
}

En production, ne pas exporter directement vers les backends. Passez par un Collector :

App (SDK) → OTLP → Collector → Backends

Pourquoi ?

  • 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

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-patternConséquenceSolution
IDs uniques en attributsExplosion cardinalité, coûtsUtiliser des catégories (status, region)
Tracer 100% en prodOverhead, coûts stockageSampling 10-20%
PII/secrets en attributsFuite de donnéesRedaction via Collector
Exporter direct vers backendVendor lock-in, pas de gouvernancePasser par Collector
Pas de service.nameTraces anonymes, inutilisablesToujours définir OTEL_SERVICE_NAME
Ignorer la propagationSpans orphelins, traces fragmentéesVérifier propagators + headers
Spans trop verbeuxBruit, difficile à exploiter5-10 attributs max par span

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.

  1. 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
  2. Le Collector reçoit-il les données ?

    Activez le debug exporter dans le Collector :

    exporters:
    debug:
    verbosity: detailed
  3. Le réseau est-il OK ?

    Fenêtre de terminal
    # Depuis le container de l'app
    curl -v http://otel-collector:4318/v1/traces
    # → Doit répondre (même en erreur 405)
  4. Les ports sont-ils corrects ?

    • 4317 = gRPC
    • 4318 = HTTP

    Vérifiez que OTEL_EXPORTER_OTLP_PROTOCOL correspond.

  5. La propagation est-elle configurée ?

    Sans propagation, les services ne partagent pas le trace-id.

    Fenêtre de terminal
    OTEL_PROPAGATORS=tracecontext,baggage

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ômeCause probableSolution
Connection refused :4317Collector non démarré ou mauvais portVérifier le container/service
Spans orphelins (pas de parent)Propagation manquanteVérifier OTEL_PROPAGATORS
Traces fragmentéesContexte perdu entre appels asyncTransmettre le contexte explicitement (trace.use_span() ou trace.set_span_in_context() en Python, passage du ctx en Go)
Unknown service dans le backendOTEL_SERVICE_NAME non définiDéfinir la variable

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.

TypeUsageExemple
CounterValeurs cumulativesRequêtes, erreurs
GaugeValeur instantanéeMémoire, connexions actives
HistogramDistributionLatence (p50, p95, p99)
from opentelemetry import metrics
meter = metrics.get_meter(__name__)
# Counter
request_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"})

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 = 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

Ce site vous est utile ?

Sachez que moins de 1% des lecteurs soutiennent ce site.

Je maintiens +700 guides gratuits, sans pub ni tracking. Un soutien, même symbolique, m'aide à couvrir l'hébergement et à garder ces ressources gratuites. Merci pour votre appui.

Le formulaire ne s'affiche pas ? Ouvrir Ko-fi dans un onglet.

Abonnez-vous et suivez mon actualité DevSecOps sur LinkedIn