L'observabilité LLM commence par une question simple : où vont les traces.
La réponse intelligente, c'est OpenTelemetry — pas un format propriétaire. Ce
guide vous montre comment instrumenter un appel LLM avec OpenLLMetry
(le SDK Traceloop) et envoyer les traces vers Grafana Tempo, sans toucher
au code applicatif. On utilise le SDK OpenAI officiel pointé sur Ollama
(via son endpoint compatible OpenAI) — ce qui couvre à la fois les modèles
managés et le self-hosting. À la fin, chaque appel produit un span avec
prompt, complétion, modèle, tokens et latence, attribué
selon les conventions sémantiques gen_ai.* — donc portable vers n'importe
quel backend OTel.
Ce que vous allez apprendre
Section intitulée « Ce que vous allez apprendre »- Comprendre les conventions sémantiques OpenTelemetry GenAI (
gen_ai.*) - Auto-instrumenter un appel LLM en deux lignes avec OpenLLMetry
- Brancher l'instrumentation OpenAI sur Ollama via l'endpoint
/v1 - Exporter les traces vers Tempo en OTLP/HTTP
- Visualiser prompts, complétions et tokens dans Grafana
- Maîtriser les pièges : volume de données, prompts sensibles, batching
Prérequis
Section intitulée « Prérequis »- Avoir lu le guide Observabilité des LLM : pourquoi c'est différent pour le cadre général.
- Connaître les bases d'OpenTelemetry et savoir que Tempo est un backend de traces OTLP.
- Avoir un backend LLM joignable : Ollama local (le plus simple), la passerelle LiteLLM proxy ou un provider managé.
- Docker + docker compose v2, Python ≥ 3.12, uv.
Pourquoi gen_ai.* et pas un format maison
Section intitulée « Pourquoi gen_ai.* et pas un format maison »Avant 2024, chaque outil d'observabilité LLM avait son schéma maison — LangSmith, Helicone, Phoenix, Langfuse… autant de structures différentes pour la même donnée. Migrer d'un outil à l'autre demandait de tout ré-instrumenter.
Le groupe de travail OpenTelemetry GenAI a publié des conventions sémantiques qui standardisent les attributs d'un span LLM :
| Attribut | Type | Exemple |
|---|---|---|
gen_ai.system | string | "openai", "ollama", "anthropic" |
gen_ai.request.model | string | "gpt-4o-mini", "qwen2.5" |
gen_ai.request.temperature | double | 0.7 |
gen_ai.usage.input_tokens | int | 184 |
gen_ai.usage.output_tokens | int | 47 |
gen_ai.response.finish_reasons | string[] | ["stop"] |
À ces attributs standards s'ajoutent souvent les events (un par message de la conversation) qui portent le contenu des prompts et des complétions.
Conséquence opérationnelle : un agent instrumenté avec ces conventions s'envoie indifféremment à Tempo, Jaeger, Langfuse, Phoenix, Honeycomb ou un OTel Collector qui redistribue vers plusieurs backends. Vous ne vous verrouillez pas.
OpenLLMetry : l'auto-instrumentation
Section intitulée « OpenLLMetry : l'auto-instrumentation »Écrire les spans à la main est faisable mais fastidieux. OpenLLMetry (projet de Traceloop, open source) propose une auto-instrumentation qui détecte les SDK LLM courants et émet automatiquement des spans conformes aux conventions GenAI.
Le SDK Python s'installe en un paquet : traceloop-sdk. Une fois
initialisé, il monkey-patche les SDK suivants au chargement :
- OpenAI, Anthropic, Google Generative AI, Cohere, Bedrock, Mistral…
- Ollama (SDK natif), Groq, Replicate, Together…
- LangChain, LangGraph, LlamaIndex, Haystack, CrewAI
Côté code applicatif, rien à changer : un client.chat.completions.create(...)
reste un client.chat.completions.create(...). C'est ce qu'on veut —
l'instrumentation ne doit pas polluer la logique métier.
Le lab : Tempo + Grafana + OpenAI SDK vers Ollama
Section intitulée « Le lab : Tempo + Grafana + OpenAI SDK vers Ollama »Le lab complet est dans lab-ia-mcp/observabilite/otel-genai/.
Voici sa structure :
observabilite/otel-genai/├── compose.yml # Tempo + Grafana├── tempo/tempo.yaml # config Tempo (receivers OTLP)├── grafana/provisioning/... # datasource Tempo auto-provisionnée├── pyproject.toml # deps Python pinned├── .env.example # endpoint OTLP, modèle, backend LLM├── src/app.py # 3 appels LLM instrumentés└── README.mdStack de réception : Tempo + Grafana
Section intitulée « Stack de réception : Tempo + Grafana »Le compose.yml lance deux services, exposés sur 127.0.0.1 uniquement
(pas d'accès externe) :
services: tempo: image: grafana/tempo:2.7.0@sha256:12e904cc509a8e0bd87815e0bc719b6167933bc3f2532716a85811af967780f6 command: ["-config.file=/etc/tempo.yaml"] ports: - "127.0.0.1:4317:4317" # OTLP gRPC - "127.0.0.1:4318:4318" # OTLP HTTP
grafana: image: grafana/grafana:11.5.1@sha256:5781759b3d27734d4d548fcbaf60b1180dbf4290e708f01f292faa6ae764c5e6 environment: GF_AUTH_ANONYMOUS_ENABLED: "true" GF_AUTH_ANONYMOUS_ORG_ROLE: "Admin" ports: - "127.0.0.1:3000:3000" depends_on: [tempo]Trois points à noter :
- Les images sont épinglées par digest, pas par tag. C'est la convention du dépôt — un build reproductible à l'identique, immune aux republications.
- Tempo accepte OTLP gRPC sur 4317 et OTLP HTTP sur 4318. Le script Python utilise HTTP (4318) — c'est le défaut de l'exporteur OTLP du SDK Python, et c'est plus simple à déboguer (curl-able).
- Grafana est configuré en login anonyme (lab uniquement). Sa
datasource Tempo est provisionnée au démarrage via
grafana/provisioning/datasources/tempo.yaml, donc aucune configuration manuelle à faire dans l'UI.
Script Python instrumenté
Section intitulée « Script Python instrumenté »Le cœur du lab tient en quelques lignes. src/app.py extrait :
from openai import OpenAIfrom opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporterfrom traceloop.sdk import Traceloop
# Exporter OTLP/HTTP explicite vers Tempo (le param api_endpoint de# Traceloop pointe leur SaaS — on l'évite pour rester OTel-natif).exporter = OTLPSpanExporter(endpoint="http://localhost:4318/v1/traces")
Traceloop.init( app_name="otel-genai-lab", exporter=exporter, disable_batch=True, # spans envoyés immédiatement — pratique en lab)
# SDK OpenAI pointé sur Ollama : c'est ce client qui est auto-instrumenté.client = OpenAI( base_url="http://localhost:11434/v1", api_key="ollama", # valeur factice — Ollama ne vérifie pas la clé)
response = client.chat.completions.create( model="qwen2.5", messages=[{"role": "user", "content": "Qu'est-ce que l'observabilité d'un LLM ?"}], temperature=0,)Ce qui se passe sous le capot :
Traceloop.init(...)charge l'auto-instrumentation OpenLLMetry et configure l'exporteur OTLP/HTTP vers Tempo.client.chat.completions.create(...)est intercepté par l'instrumentation OpenAI : un spanopenai.chatest créé, enrichi avec le modèle, la température, et le contenu des messages.- La complétion revient : le span est complété avec les tokens d'entrée et
de sortie, la
finish_reason, la latence totale, puis poussé vers Tempo.
disable_batch=True force l'envoi immédiat — utile pour voir une trace
apparaître dans Grafana dès la fin du script. En production, laissez le
batching par défaut : il réduit la pression réseau et la charge sur le
backend.
Lancer le lab
Section intitulée « Lancer le lab »-
Démarrer la stack :
Fenêtre de terminal cd lab-ia-mcp/observabilite/otel-genaidocker compose up -dVérifier que Tempo écoute :
docker compose logs tempodoit mentionner les receivers OTLP gRPC/HTTP en écoute. -
Préparer l'environnement Python :
Fenêtre de terminal cp .env.example .env$EDITOR .env # ajuster OLLAMA_API_BASE et LAB_MODEL si besoinuv sync -
Émettre des appels instrumentés :
Fenêtre de terminal uv run python src/app.pyLa sortie montre les trois prompts et les premières lignes des réponses, avec le compte de tokens. À la fin :
Traces envoyées. Ouvre Grafana → Explore → Tempo → Search,filtre sur service.name = 'otel-genai-lab'. -
Visualiser dans Grafana :
Ouvrir http://localhost:3000, cliquer Explore, datasource Tempo, onglet Search. Filtrer sur
service.name = "otel-genai-lab".Une trace = un appel LLM. Cliquer dessus déplie le span avec les attributs
gen_ai.*et les events qui portent les prompts et les complétions.
Ce que vous devez voir
Section intitulée « Ce que vous devez voir »Un span LLM, dans le détail (vérifié sur le lab) :
- Service :
otel-genai-lab - Span name :
openai.chat - Attributs :
gen_ai.system = OpenAIgen_ai.request.model = qwen2.5gen_ai.request.temperature = 0gen_ai.response.model = qwen2.5gen_ai.openai.api_base = http://localhost:11434/v1/gen_ai.usage.prompt_tokens = 41gen_ai.usage.completion_tokens = 354gen_ai.prompt.0.role = usergen_ai.prompt.0.content = "Quelle est la différence entre…"gen_ai.completion.0.role = assistantgen_ai.completion.0.content = "La différence entre…"gen_ai.completion.0.finish_reason = stopllm.usage.total_tokens = 395
- Duration : la latence end-to-end de l'appel
Si vous lancez plusieurs appels consécutifs, Grafana les montre dans une timeline ; si vous orchestrez avec LangGraph ou un agent, vous verrez les spans s'imbriquer pour former l'arbre complet de l'exécution.
Sécurité et prudence opérationnelle
Section intitulée « Sécurité et prudence opérationnelle »Les prompts contiennent des données sensibles
Section intitulée « Les prompts contiennent des données sensibles »OpenLLMetry capture le contenu des messages dans les events. Si vos prompts incluent des PII, des secrets, ou des données client, ces données finissent dans Tempo. Trois mitigations possibles :
- Désactiver la capture du contenu :
Traceloop.init(traceloop_telemetry=False)ou la variableTRACELOOP_TRACE_CONTENT=false. Les attributs structurés (modèle, tokens, latence) restent, mais le texte des prompts n'est plus exporté. - Filtrer/masquer côté SDK avant l'appel — votre application reste responsable de ne pas envoyer de données qu'elle ne veut pas tracer.
- Restreindre l'accès à Tempo et Grafana : RBAC, auth en frontal, réseau interne.
Volume de traces
Section intitulée « Volume de traces »Un span LLM avec contenu peut peser plusieurs ko. Sur un agent qui appelle le modèle 10 fois par requête utilisateur, à 100 requêtes/jour, vous générez ~10 Mo de traces/jour — gérable. Mais multipliez par 100 utilisateurs, et le backend devient à dimensionner sérieusement.
Réflexes :
- Sampling côté SDK pour les déploiements à fort volume (Traceloop supporte les samplers OTel standards).
- Rétention courte sur Tempo (7 à 30 jours), avec archivage S3 pour les besoins de conformité.
Le batching en production
Section intitulée « Le batching en production »Dans le lab, disable_batch=True simplifie le debug. En production,
gardez le batching par défaut : il accumule les spans et les envoie par
lots, ce qui réduit la latence apparente et la pression réseau. Si Tempo
devient indisponible, le SDK retente — un coupure passagère ne perd pas les
traces.
Aller au-delà : OTel Collector et stack hybride
Section intitulée « Aller au-delà : OTel Collector et stack hybride »Pour une stack production, intercaler un OpenTelemetry Collector entre les applications et Tempo donne de la souplesse :
- Multi-backends : envoyer les mêmes traces à Tempo et à Langfuse, ou à un SaaS de secours.
- Échantillonnage centralisé sans toucher au code applicatif.
- Enrichissement : ajouter des attributs déduits (région, version applicative, identifiant tenant).
- Filtrage : drop les spans qui correspondent à un pattern (par exemple les health checks).
Le guide OpenTelemetry Collector détaille cette pièce ; les guides suivants de cette sous-section (Langfuse, Phoenix) montrent comment brancher des outils dédiés en plus de Tempo, sur la même source OTel.
À retenir
Section intitulée « À retenir »- Les conventions
gen_ai.*standardisent les attributs d'un span LLM, ce qui rend votre instrumentation portable entre backends. - OpenLLMetry (SDK Traceloop) auto-instrumente LiteLLM et les SDK populaires en deux lignes — sans toucher au code applicatif.
- Tempo + Grafana suffit largement comme stack de réception : c'est de l'OTel standard, vous gardez tout le reste de votre observabilité cohérente.
- Attention au contenu des prompts dans les traces — données sensibles, volume de stockage, sampling à prévoir.
- Pour une stack production, OTel Collector entre les apps et les backends ouvre le multi-backend et le sampling centralisé.