ITSkillsCenter
Business Digital

Instrumenter une application Python avec OpenTelemetry SDK pas-a-pas

12 دقائق للقراءة

📍 Article principal : Observabilité applicative en 2026 : OpenTelemetry, traces distribuées et stack LGTM — pour le contexte conceptuel et l’architecture d’ensemble.

Pourquoi instrumenter Python avec OpenTelemetry

Une API Python en production — Flask, FastAPI ou Django — a souvent une vie modeste : quelques routes, une base de données, une intégration externe ou deux. Tant que tout va bien, on s’en sort avec les logs. Le jour où la latence p99 grimpe sans raison apparente, on regrette de ne pas avoir branché des traces dès le départ. La force d’OpenTelemetry côté Python est qu’on peut instrumenter une application existante sans toucher au code, en quelques minutes, grâce à l’auto-instrumentation pilotée par le binaire opentelemetry-instrument.

Ce tutoriel construit l’instrumentation d’une application FastAPI minimaliste en deux temps. D’abord la voie zéro-code, qui activera traces, métriques et logs sans changer une ligne. Puis l’ajout d’instrumentation manuelle pour exposer des spans et des métriques métier que l’auto-instrumentation ne peut pas deviner.

Prérequis

  • Python 3.9+ (3.11 ou 3.12 recommandé)
  • pip 23+ ou poetry/uv récent
  • Un OpenTelemetry Collector local en écoute sur 127.0.0.1:4317 (gRPC)
  • Connaissances Python intermédiaires
  • Temps estimé : 30 à 40 minutes

Étape 1 — Préparer un environnement isolé

Comme tout projet Python sérieux, on travaille dans un environnement virtuel. C’est l’occasion de figer la version Python utilisée et d’éviter les conflits de dépendances avec d’autres outils installés globalement. La commande python -m venv est suffisante ; les outils plus modernes comme uv ou poetry font la même chose en plus rapide.

mkdir otel-python-demo && cd otel-python-demo
python -m venv .venv
source .venv/bin/activate    # Linux/macOS
# .venv\Scripts\activate     # Windows PowerShell

Une fois l’environnement activé, le prompt du shell affiche (.venv). Toute installation pip qui suit reste cantonnée au projet, ce qui rendra les versions reproductibles et la désinstallation triviale par simple suppression du dossier.

Étape 2 — Installer le SDK et les outils OTel

L’écosystème Python OTel fournit un méta-paquet, opentelemetry-distro, qui installe à la fois l’API, le SDK et l’outil en ligne de commande opentelemetry-instrument. Couplé à opentelemetry-bootstrap, qui détecte les bibliothèques présentes et installe les instrumentations correspondantes, on obtient en deux commandes une couverture automatique très large.

pip install fastapi uvicorn[standard] httpx
pip install opentelemetry-distro \
            opentelemetry-exporter-otlp
opentelemetry-bootstrap -a install

La dernière commande inspecte les paquets installés (FastAPI, httpx, etc.) et installe automatiquement les paquets opentelemetry-instrumentation-* correspondants. Si plus tard on ajoute SQLAlchemy ou redis-py, il suffira de relancer opentelemetry-bootstrap -a install pour étendre la couverture sans rien modifier au code.

Étape 3 — Écrire l’application FastAPI

L’application sert deux endpoints : un retour trivial pour valider la chaîne et un endpoint qui appelle une API externe via httpx. Comme dans le cas Node, le code applicatif n’a aucune référence à OpenTelemetry — c’est l’auto-instrumentation qui se charge de tout.

# app.py
from fastapi import FastAPI
import httpx

app = FastAPI()

@app.get("/hello")
async def hello():
    return {"message": "hello"}

@app.get("/joke")
async def joke():
    async with httpx.AsyncClient(timeout=5.0) as client:
        r = await client.get("https://icanhazdadjoke.com/", headers={"Accept": "application/json"})
        return r.json()

Le code est volontairement simple. Les bibliothèques utilisées (FastAPI, Starlette en interne, httpx) sont toutes auto-instrumentées et produiront des spans dès qu’on lancera l’application sous le wrapper opentelemetry-instrument.

Étape 4 — Lancer en mode auto-instrumentation

La magie opère ici : on remplace uvicorn app:app par opentelemetry-instrument uvicorn app:app. Le wrapper se charge avant tout import applicatif, charge les instrumentations et configure les exporters à partir des variables d’environnement standardisées par la spec OpenTelemetry. Aucune modification du code.

export OTEL_SERVICE_NAME=otel-python-demo
export OTEL_RESOURCE_ATTRIBUTES="deployment.environment=development,service.version=1.0.0"
export OTEL_EXPORTER_OTLP_ENDPOINT=http://127.0.0.1:4317
export OTEL_EXPORTER_OTLP_PROTOCOL=grpc
export OTEL_TRACES_EXPORTER=otlp
export OTEL_METRICS_EXPORTER=otlp
export OTEL_LOGS_EXPORTER=otlp

opentelemetry-instrument uvicorn app:app --host 0.0.0.0 --port 8000

Au démarrage, le wrapper liste les instrumentations actives (FastAPI, Starlette, httpx, asyncio, requests si présent). On déclenche un peu de trafic pour valider la chaîne de bout en bout. Si le Collector reçoit bien les exports, on voit dans ses logs les spans s’accumuler.

curl http://127.0.0.1:8000/hello
curl http://127.0.0.1:8000/joke

Chaque requête entrante produit une trace : un span racine pour la requête HTTP, un span FastAPI pour le routage, et pour /joke un span supplémentaire pour l’appel httpx sortant avec son propre traceparent propagé vers icanhazdadjoke. Si l’API distante était elle-même instrumentée OTel, on verrait l’arbre s’étendre côté serveur — c’est tout l’intérêt de la propagation W3C Trace Context standardisée.

Étape 5 — Ajouter un span manuel pour une opération métier

L’auto-instrumentation couvre l’infrastructure mais pas la logique métier. Si on veut voir dans Tempo une opération de calcul, de validation ou de transformation comme un span dédié, il faut l’ajouter explicitement. L’API est exactement la même que côté Node : un tracer global, un context manager pour ouvrir un span, des attributs sémantiquement nommés.

# scoring.py
from opentelemetry import trace
from opentelemetry.trace import Status, StatusCode
import random

tracer = trace.get_tracer("otel-python-demo")

def score_user(user_id: str) -> int:
    with tracer.start_as_current_span("score.compute") as span:
        try:
            span.set_attribute("user.id", user_id)
            value = random.randint(0, 1000)
            span.set_attribute("score.value", value)
            return value
        except Exception as exc:
            span.record_exception(exc)
            span.set_status(Status(StatusCode.ERROR, str(exc)))
            raise

Le pattern with tracer.start_as_current_span(...) garantit la fermeture du span même en cas d’exception. À l’intérieur, set_attribute attache du contexte métier qui sera filtrable dans Tempo. record_exception transforme une erreur Python en attribut structuré du span avec stack trace, et set_status marque le span comme erroné — ce qui le rend cliquable dans Grafana et candidat aux politiques de sampling tail-based.

On câble ce span dans une route :

# app.py (extension)
from scoring import score_user

@app.get("/score/{user_id}")
async def score(user_id: str):
    return {"user_id": user_id, "score": score_user(user_id)}

Un appel à /score/abc produit une trace à trois niveaux : le span HTTP entrant, le span FastAPI, et le span score.compute avec ses attributs métier. Cette imbrication est gratuite : start_as_current_span détecte automatiquement le span parent dans le contexte courant.

Étape 6 — Émettre des métriques applicatives

Les métriques répondent aux questions agrégées : quel est le débit, quelle est la distribution de latence, combien d’erreurs par minute. L’API Python expose les mêmes types d’instruments que les autres SDK : Counter, UpDownCounter, Histogram, Gauge observable. Pour une application web, l’instrument le plus utile est l’Histogram qui capture la distribution d’une grandeur (durée, taille).

# scoring.py (extension)
from opentelemetry import metrics

meter = metrics.get_meter("otel-python-demo")

score_counter = meter.create_counter(
    "app.score.computed",
    description="Number of scores computed",
)

score_histogram = meter.create_histogram(
    "app.score.value",
    description="Distribution of computed score values",
)

def score_user(user_id: str) -> int:
    with tracer.start_as_current_span("score.compute") as span:
        span.set_attribute("user.id", user_id)
        value = random.randint(0, 1000)
        score_counter.add(1)
        score_histogram.record(value)
        return value

Le compteur s’incrémente à chaque appel, l’histogramme accumule la distribution. La période d’export est par défaut de 60 secondes côté Python ; on peut la réduire avec la variable OTEL_METRIC_EXPORT_INTERVAL en millisecondes pour des tests interactifs. Côté Mimir, on retrouvera la métrique sous app_score_computed_total et son équivalent histogramme avec les buckets _bucket, _sum et _count.

Règle d’hygiène déjà rappelée : ne pas passer user.id en attribut de métrique. Sur un span c’est utile et bon marché ; sur une métrique, chaque utilisateur unique crée une nouvelle série temporelle, ce qui fait exploser la cardinalité.

Étape 7 — Connecter les logs à la trace

Côté Python, le logging standard est étroitement intégré à OpenTelemetry. L’auto-instrumentation patche le module logging pour injecter automatiquement trace_id et span_id dans chaque enregistrement émis dans le contexte d’un span actif. Si on configure aussi un LoggingHandler OTel, les logs sont exportés en OTLP comme les traces et métriques.

# logger.py
import logging

logger = logging.getLogger("otel-python-demo")
logger.setLevel(logging.INFO)

# format JSON simple, l'auto-instrumentation OTel ajoutera trace_id/span_id
handler = logging.StreamHandler()
formatter = logging.Formatter(
    '{"ts":"%(asctime)s","level":"%(levelname)s","msg":"%(message)s",'
    '"trace_id":"%(otelTraceID)s","span_id":"%(otelSpanID)s"}'
)
handler.setFormatter(formatter)
logger.addHandler(handler)

Avec OTEL_LOGS_EXPORTER=otlp activé, le SDK exporte aussi les logs en OTLP vers le Collector, qui les route vers Loki. Côté Grafana, un derived field sur la clé trace_id permet de cliquer une ligne de log et d’ouvrir la trace dans Tempo. Cette corrélation est ce qui transforme une investigation laborieuse en saut direct entre signaux.

Étape 8 — Vérifier la pipeline complète

Le test final consiste à provoquer un peu de trafic et à vérifier que les trois signaux atterrissent. Si le Collector logue chaque batch reçu vers stdout, on doit voir des lignes de type TracesExporter: 12 spans, MetricsExporter: 4 metrics, LogsExporter: 8 records. Si rien n’arrive, vérifier dans cet ordre : variable OTEL_EXPORTER_OTLP_ENDPOINT, écoute du Collector sur 4317, absence de pare-feu local.

for i in $(seq 1 100); do
  curl -s "http://127.0.0.1:8000/score/$i" > /dev/null
done

Cent appels génèrent suffisamment de signal pour observer une distribution dans Mimir, des traces dans Tempo et des logs dans Loki. À ce stade, la chaîne d’instrumentation est complète et l’application est prête à être déployée derrière son Collector et le pipeline d’exploitation associé. Pour la suite, le tutoriel Configurer un OpenTelemetry Collector détaille la configuration côté infrastructure.

Erreurs fréquentes

Lancer le serveur sans opentelemetry-instrument

Le wrapper est ce qui charge les instrumentations avant les imports. Lancer directement uvicorn sans le wrapper donne un service muet côté OTel : aucun span, aucune métrique. La vérification est triviale : si rien n’arrive au Collector et que le bootstrap n’a pas été chargé manuellement, c’est cette cause.

Confondre OTEL_EXPORTER_OTLP_PROTOCOL grpc et http

Si l’on positionne OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf mais que l’endpoint pointe vers le port 4317 (gRPC), le SDK échoue silencieusement à exporter. Le port HTTP est 4318. Aligner protocole et port est une erreur courante au premier passage en production.

Oublier de propager le contexte dans une tâche async détachée

Une asyncio.create_task(...) qui démarre une tâche longue va perdre le contexte de trace si l’on n’a pas pris soin de le copier explicitement. Pour les workers, une queue ou un pool, il faut utiliser contextvars et la propagation explicite via opentelemetry.context. Sans cela, les spans des tâches détachées apparaissent comme des arbres orphelins.

Mettre en place du sampling head-based avant d’avoir vu le volume

Activer OTEL_TRACES_SAMPLER=traceidratio à 0.1 paraît raisonnable mais coupe à l’aveugle 90 % des traces, y compris celles en erreur. En début de vie, garder 100 % des traces et basculer plus tard vers du tail-based dans le Collector quand le volume justifie l’effort. Le détail est creusé dans le tutoriel Tail-based sampling pour maîtriser les coûts.

Instrumenter une bibliothèque non supportée sans le savoir

Si l’application utilise un client SQL ou un broker exotique non couvert par opentelemetry-bootstrap, ces appels n’apparaissent pas dans la trace et donnent l’impression trompeuse que le service est rapide alors que ses dépendances tirent la latence. Vérifier la liste des instrumentations disponibles sur le dépôt opentelemetry-python-contrib ; pour ce qui n’est pas couvert, instrumenter manuellement.

Tutoriels associés

Ressources et références officielles

FAQ

Quelle différence entre auto-instrumentation et instrumentation manuelle ?

L’auto-instrumentation est faite par opentelemetry-instrument qui charge les paquets opentelemetry-instrumentation-* au démarrage. Elle couvre les bibliothèques (HTTP, ORM, broker) sans toucher au code. L’instrumentation manuelle utilise l’API opentelemetry.trace pour ajouter des spans métier dans le code. Les deux coexistent et se complètent.

Faut-il choisir gunicorn ou uvicorn ?

Les deux sont compatibles. Avec gunicorn en mode uvicorn.workers.UvicornWorker, lancer le master via opentelemetry-instrument gunicorn ... et utiliser le hook post_worker_init si nécessaire pour réinitialiser des composants par worker. La doc OTel Python contient un exemple complet.

Le SDK supporte-t-il Python 3.13 et 3.14 ?

Oui. Les versions 1.4x du SDK couvrent Python 3.9 à 3.14 inclus. La matrice de support est documentée sur le projet et suit le calendrier officiel CPython.

Comment instrumenter un script de batch (sans serveur web) ?

Même mécanique : on lance le script via opentelemetry-instrument python mon_script.py. L’auto-instrumentation prendra en charge requêtes HTTP, base de données, etc. Pour un span racine personnalisé qui couvre tout le job, on l’ouvre dans le main et on ferme avant sys.exit. Penser à appeler tracer_provider.shutdown() en fin de script pour vider les buffers.

Le surcoût en performance est-il sensible ?

Pour une API Python typique (~100 à 1000 req/s par worker), le surcoût mesuré est de 2 à 5 % CPU et environ 300 µs de latence ajoutée par requête, dominé par la sérialisation des spans. C’est négligeable face à la valeur diagnostique. Pour des charges très élevées, on peut réduire le coût avec un sampling tail-based dans le Collector qui ne change rien côté application.

Sponsoriser ce contenu

Cet emplacement est à vous

Position premium en fin d'article — c'est l'instant où les lecteurs sont le plus engagés. Réservez cet espace pour votre marque, votre formation ou votre offre.

Recevoir nos tarifs
Publicité