Aller au contenu
Développement medium

OpenTelemetry GenAI : tracer ses LLM jusqu'à Grafana Tempo

12 min de lecture

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.

  • 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

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 :

AttributTypeExemple
gen_ai.systemstring"openai", "ollama", "anthropic"
gen_ai.request.modelstring"gpt-4o-mini", "qwen2.5"
gen_ai.request.temperaturedouble0.7
gen_ai.usage.input_tokensint184
gen_ai.usage.output_tokensint47
gen_ai.response.finish_reasonsstring[]["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.

É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 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.md

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.

Le cœur du lab tient en quelques lignes. src/app.py extrait :

from openai import OpenAI
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
from 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 :

  1. Traceloop.init(...) charge l'auto-instrumentation OpenLLMetry et configure l'exporteur OTLP/HTTP vers Tempo.
  2. client.chat.completions.create(...) est intercepté par l'instrumentation OpenAI : un span openai.chat est créé, enrichi avec le modèle, la température, et le contenu des messages.
  3. 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.

  1. Démarrer la stack :

    Fenêtre de terminal
    cd lab-ia-mcp/observabilite/otel-genai
    docker compose up -d

    Vérifier que Tempo écoute : docker compose logs tempo doit mentionner les receivers OTLP gRPC/HTTP en écoute.

  2. Préparer l'environnement Python :

    Fenêtre de terminal
    cp .env.example .env
    $EDITOR .env # ajuster OLLAMA_API_BASE et LAB_MODEL si besoin
    uv sync
  3. Émettre des appels instrumentés :

    Fenêtre de terminal
    uv run python src/app.py

    La 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'.
  4. 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.

Un span LLM, dans le détail (vérifié sur le lab) :

  • Service : otel-genai-lab
  • Span name : openai.chat
  • Attributs :
    • gen_ai.system = OpenAI
    • gen_ai.request.model = qwen2.5
    • gen_ai.request.temperature = 0
    • gen_ai.response.model = qwen2.5
    • gen_ai.openai.api_base = http://localhost:11434/v1/
    • gen_ai.usage.prompt_tokens = 41
    • gen_ai.usage.completion_tokens = 354
    • gen_ai.prompt.0.role = user
    • gen_ai.prompt.0.content = "Quelle est la différence entre…"
    • gen_ai.completion.0.role = assistant
    • gen_ai.completion.0.content = "La différence entre…"
    • gen_ai.completion.0.finish_reason = stop
    • llm.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.

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 variable TRACELOOP_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.

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

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.

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.

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

Ce site vous est utile ?

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

Je maintiens +700 guides gratuits, sans pub ni tracing. Aujourd'hui, ce site ne couvre même pas mes frais d'hébergement, d'électricité, de matériel, de logiciels, mais surtout de cafés.

Un soutien régulier, même symbolique, m'aide à garder ces ressources gratuites et à continuer de produire des guides de qualité. Merci pour votre appui.

Abonnez-vous et suivez mon actualité DevSecOps sur LinkedIn