
Sessies, traces & spans in LLM observability: één hiervan is geen structureel niveau
De terms-pagina van Datadog, het eerste Google-resultaat voor LLM observability sessies traces spans, definieert twee van die drie woorden. Niet drie. Het ontbrekende woord hoort bij gen_ai.conversation.id, en de reden dat het ontbreekt is dat de OpenTelemetry-spec er nooit een structureel niveau van heeft gemaakt. Heb je eerst de onderbouwing voor observability zelf nodig, begin dan hier. Dit artikel pakt het op waar dat stuk stopt: het datamodel.
Belangrijkste punten
- Spans nesten in traces; traces groeperen in sessies. De nesting loopt van binnen naar buiten: span, dan trace, dan sessie.
- Een span is één getimede operatie. Een trace is één end-to-end request. Een sessie is één gesprek met meerdere turns.
- De GenAI-conventies van OpenTelemetry definiëren spans en het attribuut
gen_ai.conversation.id. Een sessieniveau definiëren ze niet. - Trace- en span-ID's propageren automatisch via context. De sessie-ID niet. Die zet je zelf, bij elke turn.
Sessies vs traces vs spans in één oogopslag
Binnen LLM observability is een span één getimede operatie (een model call, een retrieval-stap), een trace de boom van spans die één request produceert, en een sessie groepeert vele traces uit hetzelfde gesprek. De nesting loopt naar binnen: spans in traces, traces in sessies. De derde groepering is degene die niet is wat ze lijkt.
| Niveau | Wat het omvat | Hoe lang het leeft | Wie de ID zet | Wat het beantwoordt | Typisch aantal per gesprek |
|---|---|---|---|---|---|
| Sessie | Veel traces uit één gebruikersgesprek | Minuten tot dagen; eindigt op een inactiviteitstimeout of een expliciete close (vendor-specifiek) | Jij, handmatig, bij elke turn | Is dit hele gesprek geslaagd? | 1 |
| Trace | Eén end-to-end request of turn | Milliseconden tot seconden | Automatisch (SDK / OTel) | Wat gebeurde er in deze turn? | Meestal 5–20 |
| Span | Eén operatie: een retrieval, een model call, een tool call | Submilliseconde tot seconden | Automatisch (SDK / OTel) | Welke stap was traag, fout of duur? | Ruwweg 3–30 per trace |
Die aantallen en leeftijden zijn typische ranges die je verwacht in een RAG-chatbot of een agent-loop, geen metingen uit een gecontroleerde test. Jouw getallen zullen anders zijn. Wat niet anders zal zijn: de sessie-rij is degene die in de spec geen structureel niveau is, en het onderdeel 'Sessies: het niveau dat je tool waarschijnlijk zelf heeft verzonnen' bewijst dat.
Wat is een span, en wat is een span kind?
Een span is één getimede operatie met een naam, een start-timestamp, een eind-timestamp, een statuscode en een zak sleutel-waarde-attributen. Bij LLM tracing zitten de nuttige gegevens in de attributen: gen_ai.usage.input_tokens, gen_ai.usage.output_tokens en gen_ai.request.model vertellen je wat de operatie kostte en welk model hem uitvoerde.
Een span is één operatie, niet één functieaanroep
Elke span draagt een pointer naar een parent span ID (leeg op de root span) die de boom opbouwt. De attributenzak is open: je koppelt welke context je nodig hebt. De OpenTelemetry GenAI span-conventies (status: Development) vereisen gen_ai.operation.name en gen_ai.provider.name op elke GenAI-span, en bevelen de token-gebruiksattributen hierboven aan.
Eén praktische regel van de terms-pagina van Datadog: LLM-, Workflow- en Agent-spans mogen dienen als root span; Tool-, Task-, Embedding- en Retrieval-spans niet. Dat is de regel van Datadog, geen universele, maar het is de enige vendor die het zo stelt, en het behoedt je voor een trace die begint op een tool call zonder parent.
Span kinds: hetzelfde idee, vijf woordenschatten
Elke tool moet kunnen zeggen 'deze span is een model call' versus 'deze span is een retrieval'. Ze zijn het alleen niet eens over het woord:
| Tool | Zijn woord voor 'soort operatie' | Waarden |
|---|---|---|
| OpenTelemetry GenAI | gen_ai.operation.name-attribuut | 15 bekende waarden (chat, embeddings, execute_tool, invoke_agent, retrieval, en nog 10); één MOET worden gebruikt als hij van toepassing is, eigen waarden mogen als geen enkele past |
| 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 |
De OpenInference-spec noemt tien kinds. Datadog noemt er zeven. OTel kiest een derde route: zijn GenAI-attributenregister publiceert 15 bekende waarden voor 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) en stelt dat als één ervan van toepassing is, die waarde MOET worden gebruikt; een eigen waarde MAG alleen als geen enkele past. Het is dus een halfopen enum, niet de afwezigheid ervan. Drie lijsten, drie lengtes, en geen enkele afstemming ertussen. Als je een tool kiest, doet deze woordenschatkloof er meer toe dan de featurelijst, want hierop worden je dashboards en alertfilters afgestemd.
Wat is een trace, en waarom doet de boomvorm ertoe?
Een trace is de boom van spans die één request produceert. Eén root span staat bovenaan; elke andere span hangt eronder via parent-span-ID-verbindingen. De boomvorm is het hele punt: een plat logboek vertelt je dat iets traag was, maar de boom vertelt je welke stap traag was en welke stap de foute output produceerde.
chat_request (root) 2,340ms
├── retrieval 410ms
│ └── rerank 85ms
├── chat gpt-4o 1,720ms
└── tool_call: search_calendar 190msLees die boom en de diagnose is direct: 74% van de latentie zat in de model call, niet in de retrieval. Een plat logboek met vijf timestamps geeft je hetzelfde totaal maar geen van de toerekening.
Een agent-loop maakt deze boom dieper en breder dan een gewoon RAG-request. Elke tool call spawnt zijn eigen deelboom; een agent-turn van vijf stappen kan makkelijk 30+ spans onder één root produceren. Dat is normaal, en het is de reden dat de vraag over span-granulariteit hieronder bestaat.
Het verschil tussen tracing en logging doet hier ook ertoe: logging legt events vast, tracing legt causaliteit vast. Als je nog afweegt wat je logt versus wat je tracet, trekt ons artikel over beste praktijken voor LLM logging die grens.
Sessies: het niveau dat je tool waarschijnlijk zelf heeft verzonnen
Nee. Een sessie is geen structureel niveau in de OpenTelemetry GenAI-conventies. De spec definieert spans en het attribuut gen_ai.conversation.id (conditioneel vereist, 'when available', status: Development), beschreven als de unieke identifier voor een gesprek of thread waarmee berichten worden gecorreleerd. Vendors bouwen daar vervolgens hun eigen sessie-object bovenop. Niemand anders op deze SERP zegt de spec-status zo vlakaf, dus hier is hij.
Het gevolg is de zin waar dit hele artikel voor bestaat:
Een sessie is een groeperingssleutel, geen parent span. Hij propagateert niet zoals een trace-ID dat doet; je zet hem zelf bij elke turn.
Sla één turn over en die turn valt uit de sessie. Er is geen automatische context-propagatie voor.
Wanneer begint en eindigt een sessie?
Vendor-specifiek. Sommige tools openen een sessie bij de eerste trace met een nieuwe conversation-ID en sluiten hem op een inactiviteitstimeout (Langfuse gebruikt standaard een instelbaar venster). Andere vereisen een expliciete close-call. De spec zegt niets over levenscyclus omdat de spec een sessie niet als object modelleert.
Wat gaat mee over turns heen, en wat niet?
Het contextvenster van het model is niet de sessie. De sessie is een groeperingssleutel over onafhankelijke traces. Elke turn krijgt zijn eigen trace, zijn eigen root span, zijn eigen tokentellingen. Wat meegaat is het conversation-ID-attribuut dat je op elke root span hebt gezet. Wat niet meegaat: latentie, tokengebruik, spanstructuur. Die zijn per trace.
Wat meet een metriek op sessieniveau?
Dingen die een enkele trace niet kan: resolution rate (loste het gesprek het probleem van de gebruiker op?), turns-to-answer (hoeveel traces voordat de gebruiker kreeg wat hij nodig had?) en abandoned conversations (sessies zonder sluitsignaal). Evals op live traces draaien op sessieniveau is hoe je fouten over meerdere turns vangt die er per turn prima uitzien.
De code, vendor-neutraal
Deze snippet gebruikt alleen stabiele OTel-primitieven. Geen vendor-SDK. Hij maakt een root span voor één turn, een child span voor retrieval, een child voor de model call, en zet gen_ai.conversation.id zodat drie turns in één sessie landen:
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 responseRoep handle_turn drie keer aan met dezelfde SESSION_ID en alle drie traces groeperen onder één sessie in elke backend die het attribuut leest. Verander de ID en je bent een nieuwe sessie begonnen. Dat is het hele mechanisme.
We lazen de docs van vijf vendors naast elkaar. Ze zijn het niet eens.
Op 30-07-2026 lazen we de huidige datamodel-docs van Langfuse, LangSmith, OpenInference / Phoenix en Datadog naast elkaar, plus de OpenTelemetry GenAI span-spec. Vier van de vijf noemen hetzelfde object iets anders. Slechts één behandelt een sessie als een first-class object in plaats van een attribuut. De terms-pagina van Datadog, het eerste Google-resultaat voor deze zoekopdracht, definieert een sessie helemaal niet.
| Concept | OTel GenAI semconv | Langfuse | LangSmith | OpenInference / Phoenix | Datadog |
|---|---|---|---|---|---|
| Hele gesprek | gen_ai.conversation.id-attribuut | Session (optionele groepering van traces) | Thread (via session_id / thread_id metadata) | session.id span-attribuut | Niet gedefinieerd op de terms-pagina |
| Eén request | Trace | Trace | Trace ('a collection of runs') | Trace | Trace |
| Eén operatie | Span | Observation (span / generation / event) | Run ('a span representing a single unit of work') | Span met een span kind | Span met een span kind |
Eén bronnotitie bij die eerste rij: session.id van OpenInference staat niet in de traces-spec die hierboven gelinkt is, die de tien span kinds dekt. Hij is gedefinieerd in het zusterbestand OpenInference semantic conventions als de unieke identifier voor een sessie. Twee bestanden, één spec.
We hebben de cross-vendor vergelijking niet zelf verzonnen; FutureAGI publiceert ook een OTel-vs-vendor tabel. Onze twee toevoegingen zijn de sessie-rij (FutureAGI slaat hem over) en de zelfde-woord-andere-betekenis-val: Langfuse's 'observation' en LangSmith's 'run' zijn hetzelfde object als een span, terwijl de span kinds van Datadog en OpenInference verschillende woordenschatten zijn voor hetzelfde idee.
Langfuse noemt het een observation, LangSmith noemt het een run, Datadog noemt het een span. Zelfde object, drie dashboards die breken wanneer je migreert.
Dat is onze lezing van de migratiekosten, geen bewering van een vendor. Maar het is de reden dat opgeslagen filters, eval-configs en alertregels die op 'observation' of 'run' zijn afgestemd stoppen met werken op de dag dat je van tool wisselt. Je hernoemt geen veld. Je hernoemt een niveau. Als je die twee specifieke tools afweegt, gaat onze Langfuse vs LangSmith vergelijking dieper op de verschillen.
Lezers hebben mogelijk ook Opik, PostHog, Sentry of Weights & Biases al in hun stack; Google associeert alle vier met llm tracing, en elk brengt deze concepten net iets anders in kaart. De juiste kiezen? Ons observability platformoverzicht dekt het veld.
Eén versheidsnotitie: de GenAI-conventies zijn naar hun eigen repository verhuisd, uit de hoofdrepo semantic-conventions. Het oude pad opentelemetry.io/docs/specs/semconv/gen-ai/ bevat nu alleen een verwijzing.
Welke ID hoort waar?
Een trace ID identificeert één request en propagateert automatisch via context. Een span ID identificeert één operatie binnen die trace, ook automatisch. Een correlation ID (of request ID) komt van je weblaag voordat tracing begint, en is degene die mensen het vaakst verwarren met de trace-ID. De sessie-ID is de buitenbeent: die zet jij, handmatig, bij elke turn.
| ID | Gezet door | Bereik | Verward met |
|---|---|---|---|
| Trace-ID | Automatisch | Eén request; propagateert via context | De correlation-ID van je weblaag |
| Span-ID | Automatisch | Eén operatie | , |
| Parent span-ID | Automatisch | Bouwt de boom; leeg op de root span | , |
| Sessie- / conversation-ID | Jij, handmatig, elke turn | Veel traces | Aangenomen dat hij propagateert. Doet hij niet. |
| User-ID | Jij, handmatig | Veel sessies | De sessie-ID |
| Request- / correlation-ID | Je weblaag, voordat tracing begint | Eén HTTP-request | De trace-ID (dit is de grote) |
De praktische regel: koppel gen_ai.conversation.id als span-attribuut op de root span van elke turn, en zet de user-ID ernaast. Sla één turn over en je metrieken op sessieniveau verliezen die turn stilzwijgend.
Eén waarschuwing over cardinaliteit: user-ID's en sessie-ID's zijn high-cardinality waarden. Dat doet ertoe voor de indexeringsfactuur van je backend, wat het probleem van de volgende sectie is.
Hoe granulair moet een span zijn?
Twee faalmodi, beide gebruikelijk:
Over-spanning. Een span per functieaanroep geeft je een trace van 400 spans die niemand kan lezen en een per-span-factuur die niemand heeft goedgekeurd. Gehoste backends (Datadog, Langfuse Cloud) prijzen op spanvolume. Een praatgrage agent-loop die elke string-concatenatie instrumenteert, brandt een free tier in een middag op.
Under-spanning. Eén span voor 'de hele chain' vertelt je dat hij traag was maar niet waar. Je eindigt met het weer toevoegen van print statements, wat nu net is wat tracing had moeten vervangen.
De vuistregel (en het is een vuistregel, geen meting): span de grenzen waar een besluit of een externe call plaatsvindt.
- Retrieval-stap: spannen.
- Rerank-call: spannen.
- Elke model call: spannen.
- Elke tool call: spannen.
- Elke guardrail-check: spannen.
- Pure in-proces transformaties (string formatting, JSON parsing, prompt-assemblage): attributen op de parent span, geen eigen spans.
Over cardinaliteit, sampling en retentie:
- High-cardinality attributen (user-ID's, volledige prompts) blazen de opslagkosten op. Sample of truncate ze.
- De meeste backends laten je samplen op traceniveau. Houd 100% van de fout-traces; sample het happy path.
- Retentievensters lopen uiteen: 7 dagen op free tiers, 30–90 dagen op betaald. Beslis voordat je de data nodig hebt.
Voor het eigenlijke kostenmodel achter spanvolume en per-span-prijzen, zie onze LLM kostenmonitoring gids. Dat bouwen we hier niet opnieuw.
Hoe Techsy dit aanpakt
Voor agent-werk bij klanten standaardiseren we op drie regels:
- Eén trace per turn. Voeg nooit twee gebruikers-turns samen in één trace, zelfs niet als de agent intern loopt.
- Een sessie-ID op elke root span, gezet in applicatiecode, nooit aangenomen dat hij propagateert.
- Span kinds beperkt tot een kleine vaste set (retrieval, inference, tool, guardrail) zodat dashboards een vendorwisseling overleven.
Die derde regel is degene die teams overslaan, en het is degene die een migratie redt. Als je span-woordenschat aan de enum van één vendor hangt, breekt elke alert en elke opgeslagen view op de dag dat je wisselt.
Bouw je een agentsysteem en wil je een second opinion over de tracing-architectuur, neem dan contact op voor een gratis adviesgesprek.
Over de auteur
Mert Batur is Co-Founder van Techsy.io, waar het team AI-agents, automatiseringssystemen en voice/SDR-pipelines oplevert voor B2B-klanten. Hij schrijft over de LLM-toolingstack die het Techsy-team daadwerkelijk in productie gebruikt. Verbind op LinkedIn.
Veelgestelde vragen
Wat is een span in distributed tracing?
Een span is één getimede eenheid werk: hij heeft een naam, een starttijd, een eindtijd, een status en een set attributen. Spans koppelen aan elkaar via parent-span-ID-referenties en vormen zo een boom. In LLM-applicaties omvat een span doorgaans één model call, één retrieval of één tool-aanroep.
Wat is een span in Datadog?
In Datadog's LLM Observability is een span dezelfde getimede operatie, maar Datadog voegt een span kind-taxonomie toe: LLM, Workflow, Agent, Tool, Task, Embedding en Retrieval. Alleen LLM-, Workflow- en Agent-kinds mogen dienen als root span. De taxonomie is Datadog-specifiek; hij is geen onderdeel van de OpenTelemetry-standaard.
Wat zijn de vier pijlers van observability?
De vier pijlers zijn logs, metrics, traces en (afhankelijk van wiens indeling) profiles of events. Traces zijn de pijler waar dit artikel in leeft. De LLM-case voegt een plooi toe: tokengebruik en modelidentiteit zijn attributen op trace-spans, geen aparte metrische streams, wat twee pijlers samenvouwt tot één query.
Wat zijn de vier gouden signalen voor observability?
Latentie, verkeer, fouten en verzadiging. Voor LLM-systemen betekent latentie time-to-first-token en totale generatietijd; verkeer betekent requests per seconde per model; fouten betekent gefaalde spans (statuscode ERROR); verzadiging betekent uitputting van het tokenbudget of wachtrijdiepte. De signalen zijn hetzelfde; de eenheden verschillen.
Is een sessie onderdeel van de OpenTelemetry-specificatie?
Niet als structureel niveau. De OTel GenAI span-conventies definiëren gen_ai.conversation.id als een conditioneel vereist attribuut ('when available') voor het correleren van berichten in een gesprek of thread. Hij zit op spans. Vendors zoals Langfuse en LangSmith bouwen hun eigen sessie- of thread-objecten er bovenop.
Wat is het verschil tussen een trace-ID, een span-ID en een correlation-ID?
Een trace-ID identificeert één request en propagateert automatisch door alle downstream services. Een span-ID identificeert één operatie binnen die trace. Een correlation-ID (of request-ID) wordt gegenereerd door je weblaag voordat tracing begint en is de waarde die mensen het vaakst aanzien voor de trace-ID. Ze overlappen in bereik maar ontstaan anders.
Hoeveel spans moet één trace hebben?
Er is geen vast antwoord, maar typische ranges zijn 3–30 voor een RAG-request en 10–50+ voor een agent-loop met meerdere tool calls. De vuistregel: span externe calls en beslispunten, geen in-proces transformaties. Als je trace meer dan 100 spans heeft, instrumenteer je waarschijnlijk te veel.
Zijn Langfuse 'observations' hetzelfde als spans?
Ja. Een Langfuse observation is hetzelfde object als een OTel-span: één getimede operatie met attributen. Langfuse splitst observations in drie typen (generation, span, event) waar OTel gen_ai.operation.name gebruikt. Als je tools evalueert die je traces lezen, dekt ons LLM evaluatietools overzicht welke beide woordenschatten accepteren.
Hoe groepeer je een chatbotgesprek met meerdere turns in één sessie?
Zet dezelfde conversation-identifier op de root span van elke turn. In OTel-termen is dat gen_ai.conversation.id. In Langfuse geef je een session_id mee bij het aanmaken van traces. In LangSmith zet je session_id of thread_id metadata. Sla één turn over en die turn valt uit de groepering.
Heb ik sessies nodig als ik alleen single-turn requests afhandel?
Waarschijnlijk niet. Sessies bestaan om meerdere traces tot één gesprek te correleren. Als elk request onafhankelijk is (een classificatie-API, een one-shot samenvatter), volstaan metrieken op traceniveau. Voeg sessies toe wanneer je cross-turn metrieken nodig hebt: resolution rate, turns-to-answer of kosten op gespreksniveau. Onze LLM evaluatie gids dekt wanneer evals op sessieniveau hun kosten waard zijn.
De korte versie
Spans nesten in traces; traces groeperen in sessies. De nesting is echt, maar de spec structureert slechts twee van de drie niveaus. gen_ai.conversation.id is een attribuut dat je zelf zet, geen parent span die propagateert. En de vendor die je vandaag kiest noemt deze objecten anders dan de vendor waar je over 18 maanden naar overstapt, dus houd je span-woordenschat klein en overdraagbaar.
Als je een platform kiest, begin dan bij onze observability platformvergelijking. Als je evals bovenop je traces bouwt, pakt de LLM evaluatie gids het hier op.