
Sessões, Traces e Spans na Observabilidade de LLM: Um Destes Não É um Nível Estrutural
A página de termos da Datadog, o resultado número 1 do Google para "LLM observability sessions traces spans", define duas dessas três palavras. Não três. A que falta corresponde ao gen_ai.conversation.id, e o motivo de ela estar ausente é que a spec do OpenTelemetry nunca a transformou em um nível estrutural. Se você precisa dos argumentos a favor da observabilidade em si, comece aqui. Este post continua de onde aquele para: o modelo de dados.
Principais Conclusões
- Spans se aninham dentro de traces; traces se agrupam em sessões. O aninhamento vai de dentro para fora: span, depois trace, depois sessão.
- Um span é uma operação cronometrada. Um trace é uma requisição de ponta a ponta. Uma sessão é uma conversa com múltiplos turnos.
- As convenções GenAI do OpenTelemetry definem os spans e o atributo
gen_ai.conversation.id. Elas não definem um nível de sessão. - IDs de trace e de span se propagam automaticamente pelo contexto. O ID de sessão, não. Você o define, a cada turno.
Sessões vs Traces vs Spans, de Relance
Na observabilidade de LLM, um span é uma operação cronometrada (uma chamada de modelo, uma etapa de recuperação), um trace é a árvore de spans que uma requisição produz, e uma sessão agrupa vários traces de uma mesma conversa. O aninhamento vai para dentro: spans dentro de traces, traces dentro de sessões. O terceiro agrupamento é aquele que não é o que parece.
| Nível | O que ele envolve | Quanto tempo vive | Quem define o ID | O que ele responde | Contagem típica por conversa |
|---|---|---|---|---|---|
| Sessão | Vários traces de uma conversa de usuário | Minutos a dias; termina por timeout de inatividade ou por um fechamento explícito (definido pelo vendor) | Você, manualmente, a cada turno | Esta conversa inteira teve sucesso? | 1 |
| Trace | Uma requisição ou turno de ponta a ponta | Milissegundos a segundos | Automático (SDK / OTel) | O que aconteceu neste turno? | Geralmente 5–20 |
| Span | Uma operação: uma recuperação, uma chamada de modelo, uma chamada de ferramenta | Sub-milissegundo a segundos | Automático (SDK / OTel) | Qual etapa foi lenta, errada ou cara? | Aproximadamente 3–30 por trace |
Esses números de contagem e de tempo de vida são faixas típicas que você esperaria em um chatbot RAG ou em um loop de agente, não medições de um teste controlado. Seus números vão variar. O que não vai variar: a linha da Sessão é aquela que não é um nível estrutural na spec, e a seção "Sessões: O Nível Que Sua Ferramenta Provavelmente Inventou" prova isso.
O Que É um Span, e O Que É um Kind de Span?
Um span é uma operação cronometrada com um nome, um timestamp de início, um timestamp de fim, um código de status e um conjunto de atributos chave-valor. No tracing de LLM, os atributos são onde vivem os dados úteis: gen_ai.usage.input_tokens, gen_ai.usage.output_tokens e gen_ai.request.model dizem quanto a operação custou e qual modelo a executou.
Um span é uma operação, não uma chamada de função
Todo span carrega um ponteiro de ID de span pai (vazio no span raiz) que constrói a árvore. O conjunto de atributos é aberto: você anexa o contexto de que precisar. As convenções de span GenAI do OpenTelemetry (status: Development) exigem gen_ai.operation.name e gen_ai.provider.name em todo span GenAI, e recomendam os atributos de uso de tokens acima.
Uma regra prática da página de termos da Datadog: spans de LLM, Workflow e Agent podem servir como span raiz; spans de Tool, Task, Embedding e Retrieval não podem. Essa é a regra da Datadog, não uma regra universal, mas ela é o único vendor que a declara, e isso poupa você de construir um trace que começa em uma chamada de ferramenta sem pai.
Kinds de span: a mesma ideia, cinco vocabulários
Toda ferramenta precisa de uma forma de dizer "este span é uma chamada de modelo" versus "este span é uma recuperação". Elas só não concordam na palavra:
| Ferramenta | A palavra dela para "tipo de operação" | Valores |
|---|---|---|
| OpenTelemetry GenAI | atributo gen_ai.operation.name | 15 valores bem conhecidos (chat, embeddings, execute_tool, invoke_agent, retrieval e mais 10); um DEVE ser usado se aplicável, valores customizados são permitidos quando nenhum se aplica |
| 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 |
A spec OpenInference lista dez kinds. A Datadog lista sete. O OTel segue um terceiro caminho: seu registro de atributos GenAI publica 15 valores bem conhecidos para 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) e afirma que, se um deles se aplicar, esse valor DEVE ser usado; um valor customizado PODE ser usado apenas quando nenhum se encaixa. Então é um enum semiaberto, não a ausência de um. Três listas, três tamanhos, e nenhum alinhamento entre elas. Se você está escolhendo uma ferramenta, essa lacuna de vocabulário importa mais do que a lista de funcionalidades, porque é nela que seus dashboards e filtros de alerta vão se basear.
O Que É um Trace, e Por Que o Formato de Árvore Importa?
Um trace é a árvore de spans produzida por uma requisição. Um span raiz fica no topo; todo outro span pendura abaixo dele por meio de arestas de ID de span pai. O formato de árvore é o ponto central: um log plano diz que algo foi lento, mas a árvore diz qual etapa foi lenta e qual etapa produziu a saída ruim.
chat_request (root) 2,340ms
├── retrieval 410ms
│ └── rerank 85ms
├── chat gpt-4o 1,720ms
└── tool_call: search_calendar 190msLeia essa árvore e o diagnóstico é imediato: 74% da latência estava na chamada de modelo, não na recuperação. Um log plano de cinco timestamps dá o mesmo total, mas nenhuma da atribuição.
Um loop de agente torna essa árvore mais profunda e mais larga do que uma requisição RAG simples. Cada chamada de ferramenta gera sua própria subárvore; um turno de agente com cinco etapas pode facilmente produzir mais de 30 spans sob uma única raiz. Isso é normal, e é o motivo de a pergunta sobre granularidade de span abaixo existir.
A distinção entre tracing e logging também importa aqui: logging registra eventos, tracing registra causalidade. Se você ainda está decidindo o que logar versus o que tracear, nosso post sobre boas práticas de logging de LLM traça essa linha.
Sessões: O Nível Que Sua Ferramenta Provavelmente Inventou
Não. Uma sessão não é um nível estrutural nas convenções GenAI do OpenTelemetry. A spec define os spans e o atributo gen_ai.conversation.id (condicionalmente obrigatório, "quando disponível", status: Development), descrito como o identificador único de uma conversa ou thread usado para correlacionar mensagens. Os vendors então constroem seu próprio objeto de sessão em cima desse atributo. Ninguém mais nesta SERP declara o status da spec abertamente, então aqui está.
A consequência é a frase que este post inteiro existe para entregar:
Uma sessão é uma chave de agrupamento, não um span pai. Ela não se propaga da forma que um ID de trace se propaga; você mesmo a define, a cada turno.
Perca um turno e esse turno cai fora da sessão. Não existe propagação automática de contexto para ela.
Quando uma sessão começa e termina?
Definido pelo vendor. Algumas ferramentas abrem uma sessão no primeiro trace que carrega um novo ID de conversa e a fecham por timeout de inatividade (o Langfuse usa por padrão uma janela configurável). Outras exigem uma chamada explícita de fechamento. A spec não diz nada sobre ciclo de vida porque a spec não modela uma sessão como um objeto.
O que se mantém entre turnos, e o que não se mantém?
A janela de contexto do modelo não é a sessão. A sessão é uma chave de agrupamento sobre traces independentes. Cada turno recebe seu próprio trace, seu próprio span raiz, suas próprias contagens de tokens. O que se mantém é o atributo de ID de conversa que você carimbou em cada span raiz. O que não se mantém: latência, uso de tokens, estrutura de spans. Isso é por trace.
O que uma métrica de nível de sessão mede?
Coisas que um único trace não consegue: taxa de resolução (a conversa resolveu o problema do usuário?), turnos até a resposta (quantos traces antes de o usuário conseguir o que precisava?) e conversas abandonadas (sessões sem sinal de fechamento). Rodar evals sobre traces ao vivo no nível da sessão é como você captura falhas multi-turno que parecem normais turno a turno.
O código, neutro em relação a vendor
Este snippet usa apenas primitivas estáveis do OTel. Nenhum SDK de vendor. Ele cria um span raiz para um turno, um span filho para a recuperação, um filho para a chamada de modelo, e define gen_ai.conversation.id para que três turnos caiam em uma sessão:
from opentelemetry import trace
tracer = trace.get_tracer("my-llm-app")
SESSION_ID = "conv-8f3a2c" # mesmo valor em todo turno
def handle_turn(user_message: str):
with tracer.start_as_current_span("chat_request") as root:
# Você define isso. Não se propaga automaticamente.
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 responseChame handle_turn três vezes com o mesmo SESSION_ID e todos os três traces se agrupam sob uma sessão em qualquer backend que leia o atributo. Mude o ID e você iniciou uma nova sessão. Esse é o mecanismo inteiro.
Lemos a Documentação de Cinco Vendors Lado a Lado. Eles Não Concordam.
Em 30/07/2026 lemos a documentação atual de modelo de dados de Langfuse, LangSmith, OpenInference / Phoenix e Datadog lado a lado, além da spec de span GenAI do OpenTelemetry. Quatro dos cinco chamam o mesmo objeto por um nome diferente. Apenas um trata uma sessão como um objeto de primeira classe, e não como um atributo. A página de termos da Datadog, o resultado número 1 do Google para essa consulta, não define sessão alguma.
| Conceito | OTel GenAI semconv | Langfuse | LangSmith | OpenInference / Phoenix | Datadog |
|---|---|---|---|---|---|
| Conversa inteira | atributo gen_ai.conversation.id | Session (agrupamento opcional de traces) | Thread (via metadados session_id / thread_id) | atributo de span session.id | Não definido na página de termos |
| Uma requisição | Trace | Trace | Trace ("a collection of runs") | Trace | Trace |
| Uma operação | Span | Observation (span / generation / event) | Run ("a span representing a single unit of work") | Span com um span kind | Span com um span kind |
Uma nota de fonte sobre aquela primeira linha: o session.id do OpenInference não está na spec de traces linkada acima, que cobre os dez kinds de span. Ele é definido no arquivo vizinho de convenções semânticas do OpenInference como o identificador único de uma sessão. Dois arquivos, uma spec.
Não inventamos a comparação entre vendors; a FutureAGI também publica uma tabela OTel-vs-vendor. Nossos dois acréscimos são a linha da sessão (a FutureAGI a ignora) e a armadilha da mesma-palavra-significado-diferente: a "observation" do Langfuse e o "run" do LangSmith são o mesmo objeto que um span, enquanto os kinds de span da Datadog e do OpenInference são vocabulários diferentes para a mesma ideia.
O Langfuse chama de observation, o LangSmith chama de run, a Datadog chama de span. O mesmo objeto, três dashboards que quebram quando você migra.
Essa é a nossa leitura do custo de migração, não uma afirmação de vendor. Mas é o motivo pelo qual filtros salvos, configurações de eval e regras de alerta baseados em "observation" ou "run" param de funcionar no dia em que você troca de ferramenta. Você não está renomeando um campo. Está renomeando um nível. Se você está pesando essas duas ferramentas específicas, nosso comparativo Langfuse vs LangSmith se aprofunda na divergência.
Leitores também podem já ter Opik, PostHog, Sentry ou Weights & Biases em sua stack; o Google associa todos os quatro a llm tracing, e cada um mapeia esses conceitos de forma ligeiramente diferente. Escolhendo o certo? Nosso panorama de plataformas de observabilidade cobre o campo.
Uma nota de atualização: as convenções GenAI se mudaram para seu próprio repositório, fora do repositório principal semantic-conventions. O antigo caminho opentelemetry.io/docs/specs/semconv/gen-ai/ agora carrega apenas um ponteiro.
Qual ID Vai Onde?
Um ID de trace identifica uma requisição e se propaga pelo contexto automaticamente. Um ID de span identifica uma operação dentro daquele trace, também automaticamente. Um ID de correlação (ou ID de requisição) vem da sua camada web antes de o tracing começar, e é o que as pessoas mais confundem com o ID de trace. O ID de sessão é o diferente: cabe a você defini-lo, manualmente, a cada turno.
| ID | Definido por | Escopo | Confundido com |
|---|---|---|---|
| ID de trace | Automático | Uma requisição; propaga-se pelo contexto | O ID de correlação da sua camada web |
| ID de span | Automático | Uma operação | , |
| ID de span pai | Automático | Constrói a árvore; vazio no span raiz | , |
| ID de sessão / conversa | Você, manualmente, a cada turno | Vários traces | Assume-se que se propaga. Não se propaga. |
| ID de usuário | Você, manualmente | Várias sessões | O ID de sessão |
| ID de requisição / correlação | Sua camada web, antes de o tracing começar | Uma requisição HTTP | O ID de trace (este é o grande) |
A regra prática: anexe gen_ai.conversation.id como um atributo de span no span raiz de cada turno, e carimbe o ID de usuário junto com ele. Pule um turno e suas métricas de nível de sessão silenciosamente perdem aquele turno.
Um aviso sobre cardinalidade: IDs de usuário e IDs de sessão são valores de alta cardinalidade. Isso importa para a conta de indexação do seu backend, que é o problema da próxima seção.
Quão Granular um Span Deve Ser?
Dois modos de falha, ambos comuns:
Spans em excesso. Um span por chamada de função dá a você um trace de 400 spans que ninguém consegue ler e uma conta por span que ninguém aprovou. Backends hospedados (Datadog, Langfuse Cloud) cobram por volume de spans. Um loop de agente tagarela que instrumenta cada concatenação de strings vai queimar um plano gratuito em uma tarde.
Spans de menos. Um span para "a cadeia inteira" diz que foi lento, mas não onde. Você acaba recolocando print statements, que é exatamente o que o tracing deveria substituir.
A regra de ouro (e é uma regra de ouro, não uma medição): traceie os limites onde uma decisão ou uma chamada externa acontece.
- Etapa de recuperação: traceie.
- Chamada de rerank: traceie.
- Cada chamada de modelo: traceie.
- Cada chamada de ferramenta: traceie.
- Cada verificação de guardrail: traceie.
- Transformações puramente internas (formatação de strings, parsing de JSON, montagem de prompt): atributos no span pai, não spans próprios.
Sobre cardinalidade, amostragem e retenção:
- Atributos de alta cardinalidade (IDs de usuário, prompts completos) inflam os custos de armazenamento. Faça amostragem ou trunque-os.
- A maioria dos backends permite amostrar no nível do trace. Mantenha 100% dos traces de erro; faça amostragem do caminho feliz.
- Janelas de retenção variam: 7 dias em planos gratuitos, 30–90 dias em pagos. Decida antes de precisar dos dados.
Para o modelo de custo real por trás do volume de spans e do preço por span, veja nosso guia de monitoramento de custos de LLM. Não vamos reconstruí-lo aqui.
Como a Techsy Aborda Isso
Para trabalho com agentes de clientes, padronizamos em três regras:
- Um trace por turno. Nunca junte dois turnos de usuário em um trace, mesmo que o agente faça loops internamente.
- Um ID de sessão carimbado em cada span raiz, definido no código da aplicação, nunca assumido como propagado.
- Kinds de span mantidos em um conjunto pequeno e fixo (retrieval, inference, tool, guardrail) para que os dashboards sobrevivam a uma troca de vendor.
Essa terceira regra é a que as equipes pulam, e é a que salva uma migração. Se o seu vocabulário de spans está amarrado ao enum de um vendor, cada alerta e cada view salva quebra no dia em que você troca.
Se você está construindo um sistema de agente e quer uma segunda opinião sobre a arquitetura de tracing, receba uma consulta gratuita.
Sobre o Autor
Mert Batur é Co-Fundador da Techsy.io, onde a equipe entrega agentes de IA, sistemas de automação e pipelines de voz/SDR para clientes B2B. Ele escreve sobre a stack de ferramentas de LLM que a equipe da Techsy realmente usa em produção. Conecte-se no LinkedIn.
Perguntas Frequentes
O que é um span em tracing distribuído?
Um span é uma unidade de trabalho cronometrada: ele tem um nome, um horário de início, um horário de fim, um status e um conjunto de atributos. Spans se ligam uns aos outros por meio de referências de ID de span pai, formando uma árvore. Em aplicações de LLM, um span tipicamente envolve uma chamada de modelo, uma recuperação ou uma invocação de ferramenta.
O que é um span na Datadog?
No LLM Observability da Datadog, um span é a mesma operação cronometrada, mas a Datadog adiciona uma taxonomia de span kind: LLM, Workflow, Agent, Tool, Task, Embedding e Retrieval. Apenas os kinds LLM, Workflow e Agent podem servir como span raiz. A taxonomia é específica da Datadog; ela não faz parte do padrão OpenTelemetry.
Quais são os quatro pilares da observabilidade?
Os quatro pilares são logs, métricas, traces e (dependendo do enquadramento) profiles ou eventos. Traces são o pilar em que este post vive. O caso de LLM adiciona uma complicação: uso de tokens e identidade do modelo são atributos nos spans do trace, não fluxos de métricas separados, o que colapsa o que seriam dois pilares em uma única query.
Quais são os quatro sinais dourados da observabilidade?
Latência, tráfego, erros e saturação. Para sistemas de LLM, latência significa tempo até o primeiro token e tempo total de geração; tráfego significa requisições por segundo por modelo; erros significam spans com falha (código de status ERROR); saturação significa esgotamento do orçamento de tokens ou profundidade de fila. Os sinais são os mesmos; as unidades diferem.
Uma sessão faz parte da especificação do OpenTelemetry?
Não como um nível estrutural. As convenções de span GenAI do OTel definem gen_ai.conversation.id como um atributo condicionalmente obrigatório ("quando disponível") para correlacionar mensagens em uma conversa ou thread. Ele fica nos spans. Vendors como Langfuse e LangSmith constroem seus próprios objetos de sessão ou thread em cima dele.
Qual é a diferença entre um ID de trace, um ID de span e um ID de correlação?
Um ID de trace identifica uma requisição e se propaga automaticamente por todos os serviços downstream. Um ID de span identifica uma operação dentro daquele trace. Um ID de correlação (ou ID de requisição) é gerado pela sua camada web antes de o tracing começar e é o valor que as pessoas mais confundem com o ID de trace. Eles se sobrepõem em escopo, mas têm origens diferentes.
Quantos spans um trace deve ter?
Não existe uma resposta fixa, mas faixas típicas são 3–30 para uma requisição RAG e 10–50+ para um loop de agente com múltiplas chamadas de ferramenta. A regra de ouro: traceie chamadas externas e pontos de decisão, não transformações internas. Se o seu trace ultrapassa 100 spans, você provavelmente está instrumentando em excesso.
As "observations" do Langfuse são a mesma coisa que spans?
Sim. Uma observation do Langfuse é o mesmo objeto que um span do OTel: uma operação cronometrada com atributos. O Langfuse divide as observations em três tipos (generation, span, event) onde o OTel usa gen_ai.operation.name. Se você está avaliando ferramentas que leem seus traces, nosso panorama de ferramentas de avaliação de LLM cobre quais delas aceitam ambos os vocabulários.
Como você agrupa uma conversa de chatbot multi-turno em uma sessão?
Defina o mesmo identificador de conversa no span raiz de cada turno. Em termos de OTel, isso é gen_ai.conversation.id. No Langfuse, você passa um session_id ao criar traces. No LangSmith, você define os metadados session_id ou thread_id. Perca um turno e esse turno cai fora do agrupamento.
Preciso de sessões se só lido com requisições de turno único?
Provavelmente não. Sessões existem para correlacionar múltiplos traces em uma conversa. Se cada requisição é independente (uma API de classificação, um sumarizador one-shot), métricas de nível de trace são suficientes. Adicione sessões quando precisar de métricas entre turnos: taxa de resolução, turnos até a resposta ou custo de nível de conversa. Nosso guia de avaliação de LLM cobre quando evals de nível de sessão valem o investimento.
A Versão Curta
Spans se aninham dentro de traces; traces se agrupam em sessões. O aninhamento é real, mas a spec só estrutura dois dos três níveis. gen_ai.conversation.id é um atributo que você mesmo define, não um span pai que se propaga. E o vendor que você escolhe hoje nomeia esses objetos de forma diferente do vendor para o qual você vai migrar em 18 meses, então mantenha seu vocabulário de spans pequeno e portável.
Se você está escolhendo uma plataforma, comece com nosso comparativo de plataformas de observabilidade. Se você está construindo evals em cima dos seus traces, o guia de avaliação de LLM continua a partir daqui.