12-01-02 — Tracing: spans, runs, threads (OpenTelemetry GenAI conventions)
TL;DR
Tracing distribuido e a espinha dorsal da observabilidade em IA. Com o padrao OpenTelemetry GenAI Semantic Conventions (estabilizado em 2024), voce instrumenta qualquer LLM call, tool call e chain com spans padronizados — e exporta para qualquer backend (Langfuse, Jaeger, Azure Monitor). Run e a execucao completa de um agente. Thread e a conversa (multiplas runs). Span e cada etapa atomica dentro de uma run.
Conceitos fundamentais
Hierarquia: Thread → Run → Span
graph TD
T["Thread (conversa completa)\nID: thread_abc123"] --> R1["Run 1\nmensagem: 'Quais vendas?'"]
T --> R2["Run 2\nmensagem: 'Filtra por SP'"]
R1 --> S1["Span: retrieval\n45ms"]
R1 --> S2["Span: llm.chat\n820ms"]
R2 --> S3["Span: llm.chat\n610ms"]
S2 --> S4["Span: tool_call: query_fabric\n350ms"]
| Conceito | O que representa | Analogia .NET |
|---|---|---|
| Thread | Sessao de conversa com um usuario | HTTP Session |
| Run | Uma invocacao do agente (uma mensagem → resposta) | HTTP Request |
| Span | Uma operacao atomica dentro da run | Method call em trace |
| Trace | Arvore completa de spans de uma run | Distributed trace |
OpenTelemetry GenAI Semantic Conventions
A spec define atributos padronizados para spans de IA. Os principais:
Atributos de LLM Call (gen_ai.client.*)
# Atributos padrao em um span de LLM call
{
"gen_ai.system": "openai", # ou "azure_openai", "anthropic"
"gen_ai.request.model": "gpt-4o",
"gen_ai.request.max_tokens": 4096,
"gen_ai.request.temperature": 0.7,
"gen_ai.response.model": "gpt-4o-2024-08-06",
"gen_ai.response.finish_reasons": ["stop"],
"gen_ai.usage.input_tokens": 1234,
"gen_ai.usage.output_tokens": 456,
"gen_ai.usage.total_tokens": 1690
}
Atributos de Tool Call
{
"gen_ai.tool.name": "query_fabric",
"gen_ai.tool.call.id": "call_abc123",
"gen_ai.tool.description": "Executa query SQL no Fabric Lakehouse"
}
Instrumentacao com OpenTelemetry (Python)
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
from opentelemetry.instrumentation.openai import OpenAIInstrumentor
# Configurar provider e exporter (aqui exportando para Langfuse via OTLP)
provider = TracerProvider()
exporter = OTLPSpanExporter(
endpoint="https://cloud.langfuse.com/api/public/otel/v1/traces",
headers={
"Authorization": "Basic "
}
)
provider.add_span_processor(BatchSpanProcessor(exporter))
trace.set_tracer_provider(provider)
# Auto-instrumentacao: captura todos os LLM calls automaticamente
OpenAIInstrumentor().instrument()
# A partir daqui, qualquer chamada ao OpenAI SDK gera spans automaticamente
from openai import AzureOpenAI
client = AzureOpenAI(
azure_endpoint=os.environ["AZURE_OPENAI_ENDPOINT"],
api_key=os.environ["AZURE_OPENAI_KEY"],
api_version="2024-08-01-preview"
)
# Esta chamada gera um span com todos os atributos GenAI automaticamente
response = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": "Quais vendas no Q1?"}]
)
Instrumentacao manual para logica customizada
tracer = trace.get_tracer("sales-agent", "1.0.0")
def executar_agente(user_message: str, thread_id: str) -> str:
with tracer.start_as_current_span("agent.run") as run_span:
run_span.set_attribute("gen_ai.thread.id", thread_id)
run_span.set_attribute("gen_ai.request.input", user_message)
# Etapa de retrieval
with tracer.start_as_current_span("retrieval.fabric_query") as ret_span:
docs = query_fabric(user_message)
ret_span.set_attribute("retrieval.docs_count", len(docs))
ret_span.set_attribute("retrieval.latency_ms", 320)
# Etapa de geracao
with tracer.start_as_current_span("gen_ai.chat") as gen_span:
response = chamar_llm(user_message, docs)
gen_span.set_attribute("gen_ai.usage.input_tokens", response.usage.prompt_tokens)
gen_span.set_attribute("gen_ai.usage.output_tokens", response.usage.completion_tokens)
run_span.set_attribute("gen_ai.response.output", response.content)
return response.content
Instrumentacao em .NET (Semantic Kernel)
using OpenTelemetry;
using OpenTelemetry.Trace;
using Microsoft.SemanticKernel;
// Configurar OpenTelemetry com exportador OTLP
var tracerProvider = Sdk.CreateTracerProviderBuilder()
.AddSource("Microsoft.SemanticKernel*") // captura todos os spans do SK
.AddOtlpExporter(opt =>
{
opt.Endpoint = new Uri("https://your-langfuse-endpoint/api/public/otel/v1/traces");
opt.Headers = "Authorization=Basic ";
})
.Build();
// Habilitar telemetria detalhada no Semantic Kernel
var builder = Kernel.CreateBuilder();
builder.AddAzureOpenAIChatCompletion(deploymentName, endpoint, apiKey);
builder.Services.AddLogging(l => l.SetMinimumLevel(LogLevel.Trace));
// AppContext para habilitar conteudo sensivel nos logs (use apenas em dev/staging)
AppContext.SetSwitch("Microsoft.SemanticKernel.Experimental.GenAI.EnableOTelDiagnosticsSensitive", true);
var kernel = builder.Build();
// A partir daqui, todas as invocacoes do kernel geram spans automaticamente
Visualizando traces
Com os spans chegando no backend de observabilidade, voce consegue:
- Waterfall view: Ver exatamente onde o tempo foi gasto (retrieval? LLM? tool call?)
- Token breakdown: Input vs output tokens por etapa
- Error traces: Identificar em qual span uma falha ocorreu
- Session replay: Ver toda a conversa de um usuario com o trace completo de cada mensagem
💡 Regra pratica
Qualquer operacao que demora mais de 10ms merece um span proprio. Isso inclui: chamadas LLM, queries de banco/Fabric, chamadas de tools externas, embedding calls e etapas de pos-processamento.
Qualquer operacao que demora mais de 10ms merece um span proprio. Isso inclui: chamadas LLM, queries de banco/Fabric, chamadas de tools externas, embedding calls e etapas de pos-processamento.
Como isso se conecta
- 12-02-01 LangSmith/Langfuse: Backends que consomem esses traces OTLP e os tornam navegaveis
- 12-02-02 Azure Monitor: Destino alternativo para times full Microsoft stack
- 08 Semantic Kernel: SK tem instrumentacao OTEL nativa — o setup acima funciona out of the box
- 05 Agentes: Cada invocacao de agente e uma Run; cada step e um Span