
Sessions, traces et spans en observabilité LLM : l'un d'eux n'est pas un niveau structurel
La page de terminologie de Datadog, premier résultat Google sur « sessions traces spans observabilité LLM », définit deux de ces trois mots. Pas trois. Le manquant correspond à gen_ai.conversation.id, et s'il manque, c'est que la spécification OpenTelemetry n'en a jamais fait un niveau structurel. Si vous cherchez encore les arguments en faveur de l'observabilité elle-même, commencez ici. Cet article prend le relais là où celui-ci s'arrête : le modèle de données.
Points clés
- Les spans s'imbriquent dans les traces ; les traces se regroupent en sessions. L'imbrication va de l'intérieur vers l'extérieur : span, puis trace, puis session.
- Un span est une opération chronométrée. Une trace est une requête de bout en bout. Une session est une conversation multi-tours.
- Les conventions GenAI d'OpenTelemetry définissent les spans et l'attribut
gen_ai.conversation.id. Elles ne définissent pas de niveau session. - Les identifiants de trace et de span se propagent automatiquement via le contexte. L'identifiant de session, non. Vous le définissez vous-même, à chaque tour.
Sessions, traces et spans en un coup d'œil
En observabilité LLM, un span est une opération chronométrée (un appel de modèle, une étape de récupération), une trace est l'arbre des spans produits par une requête, et une session regroupe plusieurs traces issues de la même conversation. L'imbrication va vers l'intérieur : les spans dans les traces, les traces dans les sessions. Le troisième regroupement est celui qui n'est pas ce qu'il semble être.
| Niveau | Ce qu'il englobe | Sa durée de vie | Qui définit l'ID | La question à laquelle il répond | Nombre typique par conversation |
|---|---|---|---|---|---|
| Session | Plusieurs traces issues d'une même conversation utilisateur | De quelques minutes à plusieurs jours ; se termine par un délai d'inactivité ou une fermeture explicite (défini par le fournisseur) | Vous, manuellement, à chaque tour | Cette conversation dans son ensemble a-t-elle réussi ? | 1 |
| Trace | Une requête ou un tour de bout en bout | De quelques millisecondes à quelques secondes | Automatique (SDK / OTel) | Que s'est-il passé pendant ce tour ? | Généralement 5 à 20 |
| Span | Une opération : une récupération, un appel de modèle, un appel d'outil | De la sous-milliseconde à quelques secondes | Automatique (SDK / OTel) | Quelle étape était lente, fausse ou coûteuse ? | Environ 3 à 30 par trace |
Ces ordres de grandeur, en nombre comme en durée de vie, sont les fourchettes typiques d'un chatbot RAG ou d'une boucle d'agent, pas des mesures issues d'un test contrôlé. Vos chiffres seront différents. Ce qui ne changera pas : la ligne Session est celle qui n'est pas un niveau structurel de la spec, et la section « La session : le niveau que votre outil a probablement inventé » le prouve.
Qu'est-ce qu'un span, et qu'est-ce qu'un type de span ?
Un span est une opération chronométrée dotée d'un nom, d'un horodatage de début, d'un horodatage de fin, d'un code de statut et d'un sac d'attributs clé-valeur. En tracing LLM, c'est dans les attributs que vivent les données utiles : gen_ai.usage.input_tokens, gen_ai.usage.output_tokens et gen_ai.request.model vous disent ce que l'opération a coûté et quel modèle l'a exécutée.
Un span est une opération, pas un appel de fonction
Chaque span porte un pointeur vers l'ID de span parent (vide pour le span racine) qui construit l'arbre. Le sac d'attributs est ouvert : vous y attachez le contexte dont vous avez besoin. Les conventions de span GenAI d'OpenTelemetry (statut : Development) exigent gen_ai.operation.name et gen_ai.provider.name sur chaque span GenAI, et recommandent les attributs de consommation de tokens ci-dessus.
Une règle pratique tirée de la page de terminologie de Datadog : les spans LLM, Workflow et Agent peuvent servir de span racine ; les spans Tool, Task, Embedding et Retrieval, non. C'est la règle de Datadog, pas une règle universelle, mais c'est le seul fournisseur qui l'énonce, et elle vous évite de construire une trace qui commence sur un appel d'outil sans parent.
Types de span : la même idée, cinq vocabulaires
Chaque outil a besoin d'une façon de dire « ce span est un appel de modèle » par opposition à « ce span est une récupération ». Ils ne s'accordent tout simplement pas sur le mot :
| Outil | Son mot pour « type d'opération » | Valeurs |
|---|---|---|
| OpenTelemetry GenAI | Attribut gen_ai.operation.name | 15 valeurs connues (chat, embeddings, execute_tool, invoke_agent, retrieval et 10 autres) ; l'une d'elles DOIT être utilisée si elle s'applique, valeurs personnalisées autorisées quand aucune ne convient |
| Datadog | Span kind | LLM, Workflow, Agent, Tool, Task, Embedding, Retrieval |
| OpenInference / Phoenix | Span kind | CHAIN, LLM, TOOL, RETRIEVER, RERANKER, EMBEDDING, AGENT, GUARDRAIL, EVALUATOR, PROMPT |
| Langfuse | Observation type | generation, span, event |
| LangSmith | Run type | LLM, chain, tool, retriever |
La spec OpenInference liste dix types. Datadog en liste sept. OTel prend une troisième voie : son registre d'attributs GenAI publie 15 valeurs connues pour gen_ai.operation.name (chat, create_agent, create_memory, create_memory_store, delete_memory, delete_memory_store, embeddings, execute_tool, generate_content, invoke_agent, invoke_workflow, plan, retrieval, search_memory, text_completion) et précise que si l'une d'elles s'applique, cette valeur DOIT être utilisée ; une valeur personnalisée PEUT être utilisée seulement quand aucune ne convient. C'est donc une enum semi-ouverte, pas l'absence d'enum. Trois listes, trois longueurs, et aucun alignement entre elles. Si vous choisissez un outil, cet écart de vocabulaire compte plus que la liste de fonctionnalités, car c'est sur lui que vos tableaux de bord et vos filtres d'alerte seront indexés.
Qu'est-ce qu'une trace, et pourquoi la forme d'arbre compte-t-elle ?
Une trace est l'arbre des spans produits par une requête. Un span racine siège au sommet ; tous les autres spans pendent en dessous via des arêtes d'ID de span parent. La forme d'arbre est tout l'intérêt : un log plat vous dit que quelque chose était lent, mais l'arbre vous dit quelle étape était lente et quelle étape a produit la mauvaise sortie.
chat_request (root) 2,340ms
├── retrieval 410ms
│ └── rerank 85ms
├── chat gpt-4o 1,720ms
└── tool_call: search_calendar 190msLisez cet arbre et le diagnostic est immédiat : 74 % de la latence se trouvait dans l'appel de modèle, pas dans la récupération. Un log plat de cinq horodatages vous donne le même total, mais aucune attribution.
Une boucle d'agent rend cet arbre plus profond et plus large qu'une requête RAG simple. Chaque appel d'outil engendre son propre sous-arbre ; un tour d'agent en cinq étapes peut facilement produire plus de 30 spans sous une seule racine. C'est normal, et c'est la raison d'être de la question sur la granularité des spans plus bas.
La distinction entre tracing et logging compte ici aussi : le logging enregistre des événements, le tracing enregistre la causalité. Si vous hésitez encore entre ce qu'il faut logger et ce qu'il faut tracer, notre article sur les bonnes pratiques de logging LLM trace cette frontière.
La session : le niveau que votre outil a probablement inventé
Non. Une session n'est pas un niveau structurel des conventions GenAI d'OpenTelemetry. La spec définit les spans et l'attribut gen_ai.conversation.id (requis conditionnellement, « quand disponible », statut : Development), décrit comme l'identifiant unique d'une conversation ou d'un fil servant à corréler les messages. Les fournisseurs construisent ensuite leur propre objet session au-dessus de cet attribut. Personne d'autre sur cette SERP n'énonce clairement le statut de la spec, alors le voici.
La conséquence tient dans la phrase que cet article existe tout entier pour livrer :
Une session est une clé de regroupement, pas un span parent. Elle ne se propage pas comme un ID de trace ; c'est vous qui la définissez, à chaque tour.
Oubliez un tour et ce tour sort de la session. Il n'existe aucune propagation automatique de contexte pour elle.
Quand une session commence-t-elle et finit-elle ?
C'est défini par le fournisseur. Certains outils ouvrent une session à la première trace portant un nouvel identifiant de conversation et la ferment après un délai d'inactivité (Langfuse utilise par défaut une fenêtre configurable). D'autres exigent un appel de fermeture explicite. La spec ne dit rien du cycle de vie, car la spec ne modélise pas la session comme un objet.
Qu'est-ce qui se transmet d'un tour à l'autre, et qu'est-ce qui ne se transmet pas ?
La fenêtre de contexte du modèle n'est pas la session. La session est une clé de regroupement posée sur des traces indépendantes. Chaque tour reçoit sa propre trace, son propre span racine, ses propres décomptes de tokens. Ce qui se transmet, c'est l'attribut d'identifiant de conversation que vous avez estampillé sur chaque span racine. Ce qui ne se transmet pas : la latence, la consommation de tokens, la structure des spans. Tout cela est propre à chaque trace.
Que mesure une métrique au niveau session ?
Ce qu'une trace seule ne peut pas mesurer : le taux de résolution (la conversation a-t-elle résolu le problème de l'utilisateur ?), le nombre de tours avant réponse (combien de traces avant que l'utilisateur obtienne ce qu'il lui fallait ?) et les conversations abandonnées (des sessions sans signal de clôture). Lancer des évaluations sur les traces en production au niveau session, c'est ainsi que vous attrapez les défaillances multi-tours qui semblent normales tour par tour.
Le code, neutre vis-à-vis des fournisseurs
Cet extrait n'utilise que des primitives OTel stables. Aucun SDK fournisseur. Il crée un span racine pour un tour, un span enfant pour la récupération, un enfant pour l'appel de modèle, et définit gen_ai.conversation.id pour que trois tours atterrissent dans une même session :
from opentelemetry import trace
tracer = trace.get_tracer("my-llm-app")
SESSION_ID = "conv-8f3a2c" # same value on every turn
def handle_turn(user_message: str):
with tracer.start_as_current_span("chat_request") as root:
# You set this. It does not propagate automatically.
root.set_attribute("gen_ai.conversation.id", SESSION_ID)
with tracer.start_as_current_span("retrieval") as ret:
ret.set_attribute("gen_ai.operation.name", "retrieval")
docs = retrieve(user_message)
with tracer.start_as_current_span("chat gpt-4o") as llm:
llm.set_attribute("gen_ai.operation.name", "chat")
llm.set_attribute("gen_ai.provider.name", "openai")
llm.set_attribute("gen_ai.request.model", "gpt-4o")
response = call_model(user_message, docs)
llm.set_attribute("gen_ai.usage.input_tokens", 1_204)
llm.set_attribute("gen_ai.usage.output_tokens", 312)
return responseAppelez handle_turn trois fois avec le même SESSION_ID et les trois traces se regroupent sous une seule session dans n'importe quel backend qui lit l'attribut. Changez l'ID et vous avez démarré une nouvelle session. C'est tout le mécanisme.
Nous avons lu cinq documentations fournisseurs côte à côte. Elles ne sont pas d'accord.
Le 30 juillet 2026, nous avons lu côte à côte la documentation actuelle du modèle de données de Langfuse, LangSmith, OpenInference / Phoenix et Datadog, ainsi que la spec de span GenAI d'OpenTelemetry. Quatre des cinq appellent le même objet différemment. Un seul traite la session comme un objet de première classe plutôt que comme un attribut. La page de terminologie de Datadog, premier résultat Google sur cette requête, ne définit pas du tout la session.
| Concept | Semconv GenAI OTel | Langfuse | LangSmith | OpenInference / Phoenix | Datadog |
|---|---|---|---|---|---|
| Conversation entière | Attribut gen_ai.conversation.id | Session (regroupement optionnel de traces) | Thread (via les métadonnées session_id / thread_id) | Attribut de span session.id | Non défini sur la page de terminologie |
| Une requête | Trace | Trace | Trace (« une collection de runs ») | Trace | Trace |
| Une opération | Span | Observation (span / generation / event) | Run (« un span représentant une unité de travail unique ») | Span avec un type de span | Span avec un type de span |
Une note de source sur cette première ligne : le session.id d'OpenInference ne figure pas dans la spec traces liée ci-dessus, qui couvre les dix types de span. Il est défini dans le fichier voisin des conventions sémantiques OpenInference comme l'identifiant unique d'une session. Deux fichiers, une seule spec.
Nous n'avons pas inventé la comparaison inter-fournisseurs ; FutureAGI publie aussi un tableau OTel contre fournisseurs. Nos deux ajouts sont la ligne session (FutureAGI la saute) et le piège du même mot pour des sens différents : l'« observation » de Langfuse et le « run » de LangSmith sont le même objet qu'un span, tandis que les types de span de Datadog et d'OpenInference sont des vocabulaires différents pour la même idée.
Langfuse l'appelle observation, LangSmith l'appelle run, Datadog l'appelle span. Le même objet, trois tableaux de bord qui cassent le jour où vous migrez.
C'est notre lecture du coût de migration, pas une affirmation d'un fournisseur. Mais c'est la raison pour laquelle les filtres sauvegardés, les configurations d'évaluation et les règles d'alerte indexés sur « observation » ou « run » cessent de fonctionner le jour où vous changez d'outil. Vous ne renommez pas un champ. Vous renommez un niveau. Si vous hésitez entre ces deux outils précisément, notre comparatif Langfuse vs LangSmith creuse cet écart.
Les lecteurs ont peut-être déjà Opik, PostHog, Sentry ou Weights & Biases dans leur stack ; Google associe les quatre au tracing LLM, et chacun mappe ces concepts légèrement différemment. Vous cherchez le bon ? Notre comparatif des plateformes d'observabilité couvre le terrain.
Une note de fraîcheur : les conventions GenAI ont déménagé dans leur propre dépôt, hors du dépôt principal semantic-conventions. L'ancien chemin opentelemetry.io/docs/specs/semconv/gen-ai/ ne porte plus qu'un pointeur.
Quel identifiant va où ?
Un ID de trace identifie une requête et se propage automatiquement via le contexte. Un ID de span identifie une opération au sein de cette trace, lui aussi automatiquement. Un ID de corrélation (ou ID de requête) provient de votre couche web avant que le tracing ne commence, et c'est celui que les gens confondent le plus souvent avec l'ID de trace. L'ID de session est l'exception : c'est à vous de le définir, manuellement, à chaque tour.
| ID | Défini par | Portée | Souvent confondu avec |
|---|---|---|---|
| ID de trace | Automatique | Une requête ; se propage via le contexte | L'ID de corrélation de votre couche web |
| ID de span | Automatique | Une opération | , |
| ID de span parent | Automatique | Construit l'arbre ; vide pour le span racine | , |
| ID de session / conversation | Vous, manuellement, à chaque tour | Plusieurs traces | On suppose qu'il se propage. Il ne se propage pas. |
| ID d'utilisateur | Vous, manuellement | Plusieurs sessions | L'ID de session |
| ID de requête / corrélation | Votre couche web, avant le début du tracing | Une requête HTTP | L'ID de trace (c'est la grande confusion) |
La règle pratique : attachez gen_ai.conversation.id comme attribut de span sur le span racine de chaque tour, et estampillez l'ID utilisateur à côté. Sautez un tour et vos métriques au niveau session perdent ce tour en silence.
Un avertissement sur la cardinalité : les ID utilisateur et les ID de session sont des valeurs à haute cardinalité. Cela compte pour la facture d'indexation de votre backend, qui est le problème de la section suivante.
Quelle granularité pour un span ?
Deux modes d'échec, tous deux courants :
Trop de spans. Un span par appel de fonction vous donne une trace de 400 spans que personne ne peut lire et une facture au span que personne n'a validée. Les backends hébergés (Datadog, Langfuse Cloud) facturent au volume de spans. Une boucle d'agent bavarde qui instrumente chaque concaténation de chaînes grillera un niveau gratuit en un après-midi.
Pas assez de spans. Un seul span pour « toute la chaîne » vous dit qu'elle était lente, mais pas où. Vous finissez par rajouter des instructions print, ce que le tracing était censé remplacer.
La règle empirique (et c'est une règle empirique, pas une mesure) : posez des spans aux frontières où une décision ou un appel externe se produit.
- Étape de récupération : un span.
- Appel de rerank : un span.
- Chaque appel de modèle : un span.
- Chaque appel d'outil : un span.
- Chaque vérification de garde-fou : un span.
- Les transformations pures en processus (formatage de chaînes, parsing JSON, assemblage de prompt) : des attributs sur le span parent, pas leurs propres spans.
Sur la cardinalité, l'échantillonnage et la rétention :
- Les attributs à haute cardinalité (ID utilisateur, prompts complets) gonflent les coûts de stockage. Échantillonnez-les ou tronquez-les.
- La plupart des backends permettent d'échantillonner au niveau de la trace. Gardez 100 % des traces en erreur ; échantillonnez le chemin nominal.
- Les fenêtres de rétention varient : 7 jours sur les niveaux gratuits, 30 à 90 jours sur les niveaux payants. Décidez avant d'avoir besoin des données.
Pour le modèle de coût réel derrière le volume de spans et la facturation au span, voyez notre guide de suivi des coûts LLM. Nous n'allons pas le reconstruire ici.
Comment Techsy aborde le sujet
Pour nos missions d'agents chez nos clients, nous nous tenons à trois règles :
- Une trace par tour. Ne fusionnez jamais deux tours utilisateur en une seule trace, même si l'agent boucle en interne.
- Un ID de session estampillé sur chaque span racine, défini dans le code applicatif, jamais supposé se propager.
- Des types de spans limités à un petit ensemble fixe (récupération, inférence, outil, garde-fou) pour que les tableaux de bord survivent à un changement de fournisseur.
Cette troisième règle est celle que les équipes sautent, et c'est celle qui sauve une migration. Si votre vocabulaire de spans est lié à l'enum d'un fournisseur, chaque alerte et chaque vue sauvegardée casse le jour où vous changez.
Si vous construisez un système d'agents et voulez un deuxième avis sur l'architecture de tracing, demandez une consultation gratuite.
À propos de l'auteur
Mert Batur est cofondateur de Techsy.io, où l'équipe livre des agents IA, des systèmes d'automatisation et des pipelines vocaux/SDR pour des clients B2B. Il écrit sur la stack d'outillage LLM que l'équipe Techsy utilise réellement en production. Retrouvez-le sur LinkedIn.
Questions fréquemment posées
Qu'est-ce qu'un span en tracing distribué ?
Un span est une unité de travail chronométrée : il possède un nom, une heure de début, une heure de fin, un statut et un jeu d'attributs. Les spans se relient entre eux par des références d'ID de span parent, formant un arbre. Dans les applications LLM, un span enveloppe typiquement un appel de modèle, une récupération ou une invocation d'outil.
Qu'est-ce qu'un span dans Datadog ?
Dans LLM Observability de Datadog, un span est la même opération chronométrée, mais Datadog y ajoute une taxonomie de types de span : LLM, Workflow, Agent, Tool, Task, Embedding et Retrieval. Seuls les types LLM, Workflow et Agent peuvent servir de span racine. Cette taxonomie est propre à Datadog ; elle ne fait pas partie du standard OpenTelemetry.
Quels sont les quatre piliers de l'observabilité ?
Les quatre piliers sont les logs, les métriques, les traces et (selon le cadre de référence) les profils ou les événements. Les traces sont le pilier où vit cet article. Le cas LLM ajoute une subtilité : la consommation de tokens et l'identité du modèle sont des attributs sur les spans de trace, pas des flux de métriques séparés, ce qui fusionne en une seule requête ce qui serait deux piliers.
Quels sont les quatre signaux d'or de l'observabilité ?
La latence, le trafic, les erreurs et la saturation. Pour les systèmes LLM, la latence désigne le délai avant le premier token et le temps de génération total ; le trafic désigne les requêtes par seconde et par modèle ; les erreurs désignent les spans en échec (code de statut ERROR) ; la saturation désigne l'épuisement du budget de tokens ou la profondeur de file d'attente. Les signaux sont les mêmes ; les unités diffèrent.
La session fait-elle partie de la spécification OpenTelemetry ?
Pas comme niveau structurel. Les conventions de span GenAI d'OTel définissent gen_ai.conversation.id comme un attribut requis conditionnellement (« quand disponible ») pour corréler les messages d'une conversation ou d'un fil. Il se place sur les spans. Des fournisseurs comme Langfuse et LangSmith construisent leurs propres objets session ou thread au-dessus.
Quelle différence entre un ID de trace, un ID de span et un ID de corrélation ?
Un ID de trace identifie une requête et se propage automatiquement à tous les services en aval. Un ID de span identifie une opération au sein de cette trace. Un ID de corrélation (ou ID de requête) est généré par votre couche web avant le début du tracing, et c'est la valeur qu'on prend le plus souvent pour l'ID de trace. Leurs portées se chevauchent, mais leurs origines diffèrent.
Combien de spans une trace doit-elle contenir ?
Il n'y a pas de réponse fixe, mais les fourchettes typiques sont de 3 à 30 pour une requête RAG et de 10 à plus de 50 pour une boucle d'agent avec plusieurs appels d'outils. La règle empirique : des spans pour les appels externes et les points de décision, pas pour les transformations internes au processus. Si votre trace dépasse 100 spans, vous instrumentez probablement trop.
Les « observations » de Langfuse sont-elles la même chose que des spans ?
Oui. Une observation Langfuse est le même objet qu'un span OTel : une opération chronométrée avec des attributs. Langfuse divise les observations en trois types (generation, span, event) là où OTel utilise gen_ai.operation.name. Si vous évaluez des outils qui lisent vos traces, notre comparatif des outils d'évaluation LLM couvre ceux qui acceptent les deux vocabulaires.
Comment regrouper une conversation de chatbot multi-tours en une seule session ?
Définissez le même identifiant de conversation sur le span racine de chaque tour. En termes OTel, c'est gen_ai.conversation.id. Dans Langfuse, vous passez un session_id à la création des traces. Dans LangSmith, vous définissez les métadonnées session_id ou thread_id. Oubliez un tour et ce tour sort du regroupement.
Ai-je besoin de sessions si je ne traite que des requêtes mono-tour ?
Probablement pas. Les sessions existent pour corréler plusieurs traces en une seule conversation. Si chaque requête est indépendante (une API de classification, un résumé en un seul coup), les métriques au niveau trace suffisent. Ajoutez des sessions quand vous avez besoin de métriques inter-tours : taux de résolution, nombre de tours avant réponse, ou coût au niveau conversation. Notre guide d'évaluation LLM couvre le moment où les évaluations au niveau session valent leur coût.
La version courte
Les spans s'imbriquent dans les traces ; les traces se regroupent en sessions. L'imbrication est réelle, mais la spec ne structure que deux des trois niveaux. gen_ai.conversation.id est un attribut que vous définissez vous-même, pas un span parent qui se propage. Et le fournisseur que vous choisissez aujourd'hui nomme ces objets différemment de celui vers lequel vous migrerez dans 18 mois, alors gardez votre vocabulaire de spans petit et portable.
Si vous choisissez une plateforme, commencez par notre comparatif des plateformes d'observabilité. Si vous construisez des évaluations au-dessus de vos traces, le guide d'évaluation LLM prend le relais à partir d'ici.