JetBrains Tracy: Observabilidade Pragmática de IA para Kotlin
JetBrains Tracy é uma biblioteca Kotlin que conecta tracing ciente de LLM na sua aplicação em cima do OpenTelemetry. Este post percorre como eu integrei no serviço Spring Boot, as decisões de design que importam, e os modos de falha que times encontram quando chamadas de LLM se tornam o caminho mais quente do sistema.
Em algum ponto dos últimos dezoito meses, a maioria dos experimentos que rodei deixou de tratar chamadas de LLM como uma curiosidade e passou a tratá-las como infraestrutura estrutural. Essa mudança não se anuncia — um experimento ganha um passo "summarize", outro adiciona retrieval, depois algo evolui para um agente — e em pouco tempo, uma fatia significativa do seu p99 de latência, da sua conta mensal de cloud e do seu pager de on-call está amarrada em chamadas HTTP opacas para provedores como OpenAI ou Anthropic.
A tooling tradicional de observabilidade não cobre bem essa superfície. O Datadog vai te mostrar alegremente que uma chamada HTTP levou 4,2 segundos, mas não pode te dizer qual modelo você chamou, se o prompt foi truncado, quantos tokens voltaram, ou quanto aquela única requisição custou. Essa lacuna é o que o Tracy — a nova biblioteca open-source Kotlin da JetBrains — foi construído para preencher.
Este post é o registro da integração do Tracy em um pequeno serviço de agente Spring Boot e uma visão de onde ele encaixa no stack de observabilidade Kotlin. Se você roda Kotlin em produção e está começando a se importar com custo, latência e debugabilidade de LLM, há um takeaway concreto aqui: Tracy não é um novo backend de observabilidade, é uma camada de instrumentação bem projetada em cima do OpenTelemetry — e essa escolha de design é o que o torna valioso de adotar.
O Problema
Aplicações com LLM quebram as suposições com as quais a maior parte da nossa tooling de observabilidade cresceu.
Um microserviço tradicional produz saída estruturada e determinística: um status code, um body, talvez um punhado de atributos customizados. Um serviço movido por LLM produz um grafo de execução ramificado que parece mais com isso:
- Seu endpoint recebe uma requisição.
- Você enriquece a partir de um vector store ou banco de dados.
- Você envia um prompt para um modelo. Ele responde com uma chamada de tool.
- Você executa a tool (que pode, ela mesma, chamar outro serviço).
- Você envia o resultado da tool de volta ao modelo.
- Repita até o modelo produzir uma resposta final, ou você bater num guardrail.
Cada um desses passos tem seu próprio custo, perfil de latência e modo de falha. Algumas falhas são silenciosas — o modelo "tem sucesso" com uma chamada de tool alucinada que seu código executa alegremente. Algumas falhas são falhas de custo — um loop de retry silenciosamente queima 40k tokens em uma única requisição de usuário. Algumas são falhas de drift — uma mudança de prompt há três semanas degradou a acurácia em 12%, mas ninguém percebe porque não há sinal de regressão.
APMs stand-alone não sabem de nada disso. Tracing específico de SDK (a telemetria built-in do SDK OpenAI, por exemplo) só cobre a chamada ao modelo, não a lógica de aplicação em volta. Frameworks como LangChain ou Koog te dão tracing, mas só se cada chamada LLM fluir através das abstrações deles — uma restrição que não sobrevive ao contato com codebases reais.
O que você realmente quer é algo que capture o grafo inteiro com uma única API composável. Esse é o nicho que o Tracy preenche.
A Abordagem
O design do Tracy pode ser resumido em uma frase: toda interação LLM interessante se torna um span do OpenTelemetry, e você tem três maneiras ergonômicas de criar esses spans.
As três APIs são:
1. Spans com escopo via withSpan. Uma construção com escopo de bloco que abre um span na entrada e fecha na saída. O aninhamento é automático, então você pega um trace hierárquico sem malabarizar referências de span.
2. Instrumentação de cliente. Você entrega ao Tracy seu cliente HTTP ou SDK, ele o envolve, e cada chamada por esse cliente produz um span ciente de LLM com modelo, provider, contagens de token e latência anexadas. Crucialmente, por default ele captura só metadata — prompts e completions ficam desligados a menos que você explicitamente opte por eles.
3. Anotações @Trace. Coloque @Trace em um método de interface e cada implementação é automaticamente envolvida em um span. Isso importa mais do que soa — instrumentar chamadas de tool à mão é o tipo de trabalho tedioso que é pulado sob pressão de prazo, o que significa que os exatos momentos em que você precisa de visibilidade são aqueles em que ela está ausente.
Como tudo é emitido como spans OTel, você aponta o Tracy para qualquer backend que já rode: Jaeger, Zipkin, Grafana Tempo, ou um produto específico para LLM como Langfuse ou W&B Weave. Você não compra uma nova UI; você enriquece a que já tem.

Mergulho Técnico
Aqui está como a integração se parece em um serviço Spring Boot que usa o SDK OpenAI Kotlin, com um loop de agente simples na frente.
Envolvendo o cliente
@Configuration
class OpenAiConfig {
@Bean
fun openAiClient(): OpenAIClient {
val raw = OpenAIOkHttpClient.fromEnv()
// Every call through `client` now emits an OTel span
// with provider, model, prompt_tokens, completion_tokens,
// total_tokens and latency attached automatically.
return instrument(raw)
}
}O detalhe importante aqui: instrument é uma fronteira. Qualquer coisa que passa pelo cliente envolvido é traçada. Qualquer coisa que o contorna — uma chamada direta de HttpClient que alguém adicionou para uma integração pontual — é invisível. Uma boa regra é expor o cliente instrumentado como o único bean injetável e fazer o build falhar se alguém construir um bruto.
Dando escopo ao trabalho do agente
@Service
class SupportAgent(
private val client: OpenAIClient,
private val tools: List<Tool<*>>,
) {
fun handle(ticket: Ticket): AgentReply = withSpan("support-agent") {
withSpan("context.load") {
loadContext(ticket)
}.let { context ->
runAgentLoop(ticket, context)
}
}
}withSpan é mais do que um logger. Ele ancora cada chamada LLM aninhada, invocação de tool e hop de serviço downstream a um span raiz, o que significa que você pode perguntar ao seu backend coisas como: "me mostre cada ticket em que o agente entrou em loop mais de três vezes e custou mais de $0,50."
Rastreando tools sem boilerplate
interface Tool<T> {
@Trace(name = "tool.call")
fun execute(args: Map<String, Any>): T
}
class LookupOrderTool(
private val orders: OrderRepository,
) : Tool<Order?> {
override fun execute(args: Map<String, Any>): Order? =
orders.findById(args["orderId"] as String)
}Toda implementação herda o comportamento da anotação. Adicione uma nova tool, ganhe um span de graça. Essa é a peça em que mais passei a confiar em produção — a maioria dos incidentes que vi com serviços de agente tem origem em uma tool específica se comportando de forma estranha, e os dados do span são o que te leva à causa raiz sem um repro.
Capturando prompts sob demanda
if (env.isDev || featureFlag("llm.trace.content", userId)) {
TracingManager.traceSensitiveContent()
}O fato de isso ser opt-in é uma escolha de design real, não uma limitação. Prompts e completions carregam dois tipos de risco: frequentemente contêm PII e inflam o tamanho do span (um único prompt aumentado por RAG pode ser 50KB de texto). Tornar o opt-in explícito — ligável em runtime, direcionado — é o default certo.
O trade-off que importa
Tracy se apoia em OpenTelemetry, então o teto do que você pode fazer é aquilo que o OTel e seu backend suportam. Isso significa que você herda as forças do OTel (vendor-neutral, convenções semânticas padronizadas, ecossistema rico) e suas fraquezas (atributos de alta cardinalidade punem alguns backends, limites de payload de span podem truncar prompts grandes, estratégias de sampling precisam de reflexão).
A alternativa teria sido um formato wire sob medida otimizado para payloads de LLM. Isso te daria controle mais apertado sobre armazenamento de prompt e evals mais ricos mas te forçaria a um viewer proprietário. A JetBrains escolheu o caminho pragmático, e acho que é o certo para uma biblioteca que precisa encontrar times onde eles já vivem.
Armadilhas e Casos de Borda
Algumas coisas vão te morder que não são óbvias a partir do post de anúncio:
Atribuição de token é por span, não por requisição. Se seu agente entra em loop, você vai ver cinco spans LLM sob uma raiz — cada um com suas próprias contagens de token. Agregar isso em "custo por requisição de usuário" é trabalho que você faz na camada de query do seu backend, não algo que o Tracy te entrega. Construa esse dashboard cedo; é o número que seu PM vai pedir primeiro.
Captura de conteúdo sensível interage mal com limites de tamanho de span. A maioria dos backends OTel capa atributos de span em algo entre 4KB e 64KB. Ative captura de prompt em uma app RAG e você vai truncar silenciosamente. Se você precisa de logging de prompt em alta fidelidade, roteie prompts para uma store separada (S3, uma tabela Postgres llm_requests) e coloque só a referência no span.
Clientes instrumentados não propagam automaticamente contexto entre coroutines. Se você lança trabalho em um dispatcher diferente dentro de um bloco withSpan sem propagar o contexto OTel, os spans filhos ficarão órfãos. O MDCContext do Kotlin Coroutines e o Context.current().asContextElement() do OTel são as primitivas certas. Teste isso com um withContext(Dispatchers.IO) deliberado dentro de um loop de agente antes de fazer deploy.
Retries não são observabilidade gratuita. Se seu cliente OpenAI retenta em um 429, o Tracy vai te mostrar o span bem-sucedido mas a tempestade de retry vai inflar seus percentis de latência e contagens de token. Ou instrumente em uma camada acima dos retries, ou adicione um atributo retry.count para poder filtrar seus dashboards.
@Trace só funciona através da interface. Chame a classe concreta diretamente e a anotação é contornada. Se você usa Spring, garanta que suas tools são resolvidas através de seus beans de interface e que AOP está configurado para processá-los; isso me pegou uma vez em um serviço que usava singletons object do Kotlin para algumas tools.
Takeaways Práticos
- Adote o cliente instrumentado como uma única fronteira. Faça dele a única forma de entrar no provider LLM a partir da sua codebase, e faça o build falhar em construção direta.
- Coloque
withSpanno topo de cada entrada de agente ou feature. Um span raiz por interação de usuário é a unidade de análise que você vai usar para sempre. - Ligue
@Tracepara toda interface de tool desde o dia um. O custo marginal é zero e o valor marginal é enorme assim que algo se comporta mal. - Mantenha conteúdo sensível desligado por default; coloque-o atrás de uma feature flag para dev e debug direcionado de produção.
- Armazene prompts e completions fora de banda quando de fato precisar deles, e referencie-os a partir do span em vez de embuti-los.
- Construa um dashboard de custo por requisição cedo. Agregar contagens de token através de spans aninhados é a pergunta de observabilidade que retorna o maior ROI.
- Não abandone seu APM existente. Tracy te dá a visão ciente de LLM; suas ferramentas existentes ainda são donas de métricas de host, performance de DB e todo o resto. O ganho é a correlação.
Conclusão
Tracy é o menos chamativo da onda de lançamentos de tooling de IA de 2026 — e essa é sua força. Ele não tenta ser um novo backend de observabilidade, um novo framework de agente ou uma nova plataforma de avaliação. Ele silenciosamente preenche a lacuna de instrumentação entre seu código de aplicação Kotlin e os backends compatíveis com OpenTelemetry que você já roda, e faz isso com uma superfície de API que respeita como desenvolvedores Kotlin de fato escrevem código.
Use quando você tem serviços Kotlin fazendo chamadas LLM em produção, quando você quer ver seus loops de agente como traces de primeira classe ao lado de seus spans existentes, e quando você prefere aumentar seu stack de observabilidade atual em vez de substituí-lo. Passe adiante se você precisa de um produto verticalmente integrado de eval + tracing + gerenciamento de prompt — essa é uma categoria diferente, e ferramentas como Langfuse (para o qual, agradavelmente, o Tracy pode exportar) são um encaixe melhor ali.
A mensagem maior é a que a JetBrains faz de passagem no anúncio: não importa o quão bons os modelos fiquem, as aplicações que os envolvem ainda precisam ser debugadas, medidas e avaliadas. Observabilidade é a fundação que torna tudo downstream — evals, otimização de custo, engenharia de prompt — possível. Tracy é uma peça silenciosa e bem construída dessa fundação para o ecossistema Kotlin, e se você vive ali, vale trinta minutos da sua tarde.
Referências:
JetBrains Tracy - https://github.com/JetBrains/tracy
Escrito por Tiarê Balbi