# Observabilidade pra Agentes de AI em Produção: do Zero ao Avançado

Software tradicional quebra de formas previsíveis. Um timeout, um null pointer, um 500. Você olha o log, acha a linha, corrige. Agentes de AI quebram de formas criativas. Eles alucinam, chamam a ferramenta errada, entram em loop, gastam tokens sem parar, e o pior: às vezes parecem funcionar quando na verdade estão fazendo besteira.

Se você tá colocando agentes em produção sem observabilidade, você tá voando cego. Este artigo é o guia completo pra resolver isso.

## O problema é diferente

Quando um microsserviço falha, o erro é binário: funcionou ou não. Com agentes de AI, o espectro é muito mais amplo:

- O agente respondeu, mas a resposta tá errada (alucinação)
- O agente chamou 15 ferramentas quando precisava de 2 (custo explodindo)
- O agente entrou em loop entre duas ferramentas e nunca convergiu
- A latência da resposta subiu de 2s pra 45s sem motivo aparente
- O prompt de sistema foi ignorado parcialmente

Nenhum desses problemas aparece num health check HTTP 200. Você precisa de **traces**, **métricas** e **logs** específicos pra GenAI.

## Os três pilares aplicados a agentes

Observabilidade tem três pilares clássicos: traces, métricas e logs. Pra agentes de AI, cada um ganha um significado próprio.

### Traces

Um trace de agente não é só "request → response". É uma árvore de decisões:

```
[Agent Run] ─── duração total: 12.3s
  ├── [LLM Call #1] ─── modelo: claude-sonnet-4-6, tokens: 1.2k in / 340 out
  │     └── decisão: chamar ferramenta "buscar_pedido"
  ├── [Tool Call: buscar_pedido] ─── duração: 230ms
  │     └── resultado: {pedido_id: 4521, status: "enviado"}
  ├── [LLM Call #2] ─── modelo: claude-sonnet-4-6, tokens: 1.8k in / 120 out
  │     └── decisão: chamar ferramenta "rastrear_envio"
  ├── [Tool Call: rastrear_envio] ─── duração: 890ms
  │     └── resultado: {codigo: "BR123456789", status: "em_transito"}
  └── [LLM Call #3] ─── modelo: claude-sonnet-4-6, tokens: 2.1k in / 250 out
        └── resposta final ao usuário
```

Cada nó dessa árvore é um **span**. O trace completo mostra o fluxo de execução do agente, passo a passo: chamadas ao modelo, uso de ferramentas, latência, tokens e resposta final. Isso vale independente do provider: OpenAI, Anthropic, Google, Mistral.

### Métricas

As métricas que importam pra agentes são diferentes das tradicionais:

- **Token usage** (input + output, por chamada e por execução completa)
- **Custo estimado** (tokens × preço do modelo)
- **Latência por etapa** (LLM call vs tool call vs total)
- **Número de steps** por execução (agente convergiu rápido ou ficou rodando?)
- **Taxa de fallback** (quantas vezes o agente não conseguiu resolver)
- **Tool call distribution** (quais ferramentas são mais usadas)

### Logs (eventos)

Logs estruturados capturam o conteúdo das interações:

- Prompt de sistema enviado
- Mensagens do usuário
- Respostas do modelo (incluindo tool calls)
- Argumentos e retornos de ferramentas
- Erros e exceções

Isso é sensível. Por padrão, a maioria das ferramentas **não captura o conteúdo** das mensagens. Você liga isso explicitamente quando precisa debuggar.

## OpenTelemetry: a fundação

OpenTelemetry (OTel) é o padrão open-source pra instrumentação. É vendor-agnostic: você instrumenta uma vez e exporta pra Jaeger, Grafana, Datadog, Honeycomb, ou qualquer backend.

O OTel já tem **semantic conventions específicas pra GenAI**, cobrindo spans de inferência, métricas de token usage e eventos de input/output. Também existe uma seção separada para **agent spans**. Detalhe importante: em março de 2026, essas convenções ainda estão em **status Development**, o que significa que nomes de atributos e estruturas podem evoluir em versões futuras.

As convenções cobrem providers específicos com documentação dedicada: [OpenAI](https://opentelemetry.io/docs/specs/semconv/gen-ai/openai/), [Anthropic](https://opentelemetry.io/docs/specs/semconv/gen-ai/anthropic/), [AWS Bedrock](https://opentelemetry.io/docs/specs/semconv/gen-ai/aws-bedrock/) e [Azure AI Inference](https://opentelemetry.io/docs/specs/semconv/gen-ai/azure-ai-inference/).

Os atributos principais:

| Atributo | O que captura |
|---|---|
| `gen_ai.operation.name` | Tipo de operação (`chat`, `invoke_agent`, `execute_tool`, etc.) |
| `gen_ai.request.model` | Modelo solicitado (`gpt-4.1`, `claude-sonnet-4-6`, etc.) |
| `gen_ai.response.model` | Modelo que respondeu (pode diferir do solicitado) |
| `gen_ai.usage.input_tokens` | Tokens consumidos no input |
| `gen_ai.usage.output_tokens` | Tokens consumidos no output |
| `gen_ai.provider.name` | Provider (`openai`, `anthropic`, etc.) |
| `gen_ai.response.finish_reasons` | Motivo do fim da geração (`stop`, `length`, `tool_calls`, `end_turn`) |

Pra fluxos agentic, o OTel documenta operações padronizadas como `create_agent` (criação do agente), `invoke_agent` (invocação) e `execute_tool` (execução de ferramenta), além dos spans tradicionais de inferência. Na prática, isso permite separar o trace do "agente como orquestrador" do trace das chamadas LLM e das ferramentas executadas.

## Instrumentações disponíveis no ecossistema OTel

O ecossistema de instrumentação OTel pra GenAI cresceu rápido. Aqui tá o que existe e funciona:

### Repositório oficial opentelemetry-python-contrib

Estas vivem no repositório oficial do OpenTelemetry ([opentelemetry-python-contrib](https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/instrumentation-genai)):

| Pacote | SDK suportado |
|---|---|
| `opentelemetry-instrumentation-openai-v2` | `openai >= 1.26.0` |
| `opentelemetry-instrumentation-anthropic` | `anthropic >= 0.16.0` |
| `opentelemetry-instrumentation-google-genai` | `google-genai >= 1.32.0` |
| `opentelemetry-instrumentation-langchain` | `langchain >= 0.3.21` |
| `opentelemetry-instrumentation-vertexai` | `google-cloud-aiplatform >= 1.64` |

Os pacotes acima oferecem níveis diferentes de suporte a traces, eventos/logs e, em alguns casos, métricas. Como esse detalhe varia por pacote e versão, vale conferir a documentação do instrumentor exato antes de assumir cobertura completa.

Note também que pra o ecossistema Google existem pacotes distintos dependendo do SDK que você usa (`google-genai` vs `google-cloud-aiplatform`). Valide o nome do pacote contra o SDK exato do seu projeto.

### OpenLLMetry (Traceloop)

Projeto open-source (Apache 2.0) que oferece instrumentação automática pra vários providers: OpenAI, Anthropic, Azure OpenAI, Google AI, Mistral, Cohere, Amazon Bedrock, além de vector DBs como Pinecone, Chroma e Weaviate. Disponível pra Python e Node.js/TypeScript.

A grande vantagem do OpenLLMetry: um `init()` e ele detecta e instrumenta automaticamente os SDKs compatíveis instalados.

## Stack prática: montando do zero

Três opções, todas com exemplos multi-provider.

### Opção 1: OpenTelemetry puro + Jaeger

Pra quem já usa OTel na infra ou quer controle total.

#### Setup base (compartilhado entre providers)

```python
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter

def setup_telemetry(endpoint: str = "localhost:4317") -> None:
    provider = TracerProvider()
    processor = BatchSpanProcessor(
        OTLPSpanExporter(endpoint=endpoint, insecure=True)
    )
    provider.add_span_processor(processor)
    trace.set_tracer_provider(provider)
```

O `insecure=True` é pra desenvolvimento local (sem TLS). Em produção, configure certificados.

#### Com OpenAI

```bash
pip install openai opentelemetry-api opentelemetry-sdk \
  opentelemetry-exporter-otlp \
  opentelemetry-instrumentation-openai-v2
```

```python
from opentelemetry.instrumentation.openai_v2 import OpenAIInstrumentor
import openai

setup_telemetry()
OpenAIInstrumentor().instrument()

client = openai.OpenAI()
response = client.chat.completions.create(
    model="gpt-4.1",
    messages=[{"role": "user", "content": "Qual a capital da França?"}]
)
```

#### Com Anthropic (Claude)

```bash
pip install anthropic opentelemetry-api opentelemetry-sdk \
  opentelemetry-exporter-otlp \
  opentelemetry-instrumentation-anthropic
```

```python
from opentelemetry.instrumentation.anthropic import AnthropicInstrumentor
import anthropic

setup_telemetry()
AnthropicInstrumentor().instrument()

client = anthropic.Anthropic()
response = client.messages.create(
    model="claude-sonnet-4-6",
    max_tokens=1024,
    messages=[{"role": "user", "content": "Qual a capital da França?"}]
)
```

O setup do OTel é idêntico. Só muda o instrumentor e o client. Os traces gerados seguem as mesmas semantic conventions, com `gen_ai.provider.name` setado pra `"anthropic"` ou `"openai"`.

#### Múltiplos providers no mesmo projeto

```python
from opentelemetry.instrumentation.openai_v2 import OpenAIInstrumentor
from opentelemetry.instrumentation.anthropic import AnthropicInstrumentor

setup_telemetry()
OpenAIInstrumentor().instrument()
AnthropicInstrumentor().instrument()

# A partir daqui, qualquer chamada a qualquer provider gera traces
```

Útil quando seu agente usa modelos diferentes pra tarefas diferentes (ex: Claude pra raciocínio complexo, GPT-4.1-nano pra classificação rápida).

#### Capturando conteúdo das mensagens

A captura de conteúdo varia por instrumentação. Nas instrumentações GenAI mais novas (como `openai-v2` e `google-genai`), prompts e respostas normalmente **não são capturados por padrão** e precisam ser habilitados explicitamente.

O utilitário `opentelemetry-util-genai` define os seguintes modos via variável de ambiente:

```bash
# Não captura conteúdo (padrão)
export OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT=NO_CONTENT

# Captura conteúdo apenas em atributos de span
export OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT=SPAN_ONLY

# Captura conteúdo apenas como eventos/logs
export OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT=EVENT_ONLY

# Captura em ambos (spans e eventos)
export OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT=SPAN_AND_EVENT
```

Pra habilitar as features experimentais das semantic conventions, você também pode precisar de:

```bash
export OTEL_SEMCONV_STABILITY_OPT_IN=gen_ai_latest_experimental
```

Já alguns pacotes do ecossistema Traceloop, como a instrumentação do Anthropic via OpenLLMetry, podem **capturar conteúdo por padrão** e exigir disable explícito:

```bash
export TRACELOOP_TRACE_CONTENT=false  # pra desabilitar
```

Sempre confira a documentação do pacote exato que você instalou. O comportamento padrão e os valores aceitos variam.

#### Subindo Jaeger local

```bash
docker run -d --name jaeger \
  -p 16686:16686 \
  -p 4317:4317 \
  jaegertracing/jaeger:latest
```

Acesse `http://localhost:16686` e você vai ver os traces de cada chamada LLM, independente do provider.

### Opção 2: OpenLLMetry (Traceloop)

Pra quem quer instrumentação automática de múltiplos providers com o mínimo de código. O OpenLLMetry detecta os SDKs compatíveis instalados e instrumenta automaticamente.

#### Python

```bash
pip install traceloop-sdk openai anthropic
```

```python
from traceloop.sdk import Traceloop
import openai
import anthropic

Traceloop.init(
    app_name="meu-agente",
    disable_batch=True  # pra ver traces imediatamente em dev
)

# OpenAI
oai_client = openai.OpenAI()
oai_response = oai_client.chat.completions.create(
    model="gpt-4.1",
    messages=[{"role": "user", "content": "Explique observabilidade."}]
)

# Anthropic
claude_client = anthropic.Anthropic()
claude_response = claude_client.messages.create(
    model="claude-sonnet-4-6",
    max_tokens=1024,
    messages=[{"role": "user", "content": "Explique observabilidade."}]
)

# Ambas as chamadas geram traces automaticamente
```

#### Node.js / TypeScript

```bash
npm install @traceloop/node-server-sdk openai @anthropic-ai/sdk
```

```typescript
import * as traceloop from "@traceloop/node-server-sdk";

// IMPORTANTE: inicializar ANTES de importar os clients
traceloop.initialize({ disableBatch: true });

import OpenAI from "openai";
import Anthropic from "@anthropic-ai/sdk";

const openaiClient = new OpenAI();
const anthropicClient = new Anthropic();

// Ambos são instrumentados automaticamente
```

Pra exportar traces, configure `TRACELOOP_BASE_URL` e, quando necessário, `TRACELOOP_HEADERS`, apontando para um collector ou backend OTLP compatível.

### Opção 3: Langfuse (plataforma completa)

Langfuse é uma plataforma open-source de LLM engineering. Diferente das opções anteriores que são "só" instrumentação, o Langfuse entrega UI pronta: dashboard de custos, visualização de traces em árvore, sessões multi-turno, avaliação de qualidade e prompt management.

#### Self-hosted com Docker Compose

```bash
git clone https://github.com/langfuse/langfuse.git
cd langfuse
docker compose up -d
```

Acesse `http://localhost:3000`, crie um projeto e pegue as chaves da API.

#### Integração via OpenTelemetry (recomendada)

O Langfuse aceita traces via protocolo OTLP no endpoint `/api/public/otel`.

**Importante:** o endpoint OTLP do Langfuse é **HTTP, não gRPC**. Se você estiver usando o exportador `otlp/http`, funciona direto. Se estiver usando o exportador gRPC (como no setup base com Jaeger), vai precisar trocar o exporter ou colocar um OTel Collector no meio fazendo a conversão.

Configuração via variáveis de ambiente:

```bash
export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:3000/api/public/otel
export OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
export OTEL_EXPORTER_OTLP_HEADERS="Authorization=Basic $(echo -n 'public_key:secret_key' | base64)"
```

Ou no código Python, usando o exportador HTTP:

```python
import base64
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter

credentials = base64.b64encode(b"public_key:secret_key").decode()
exporter = OTLPSpanExporter(
    endpoint="http://localhost:3000/api/public/otel/v1/traces",
    headers={"Authorization": f"Basic {credentials}"}
)
```

Pra usar o exportador HTTP, instale:

```bash
pip install opentelemetry-exporter-otlp-proto-http
```

Com isso, você pode usar o OpenLLMetry ou as instrumentações oficiais (OpenAI, Anthropic, Google) e enviar tudo pro Langfuse. Não importa qual provider.

## Instrumentando um agente completo

As opções acima funcionam pra chamadas individuais. Mas um agente faz múltiplas chamadas em sequência, com decisões entre elas. Você precisa agrupar tudo num trace pai.

Vou usar a API da Anthropic neste exemplo pra mostrar que o padrão é o mesmo independente do provider.

### Com OpenTelemetry manual + Claude

```python
from opentelemetry import trace
import anthropic
import json

tracer = trace.get_tracer("meu-agente")
client = anthropic.Anthropic()

def executar_agente(pergunta: str):
    with tracer.start_as_current_span(
        "invoke_agent assistente-pedidos",
        attributes={
            "gen_ai.operation.name": "invoke_agent",
            "gen_ai.agent.name": "assistente-pedidos",
            "gen_ai.provider.name": "anthropic"
        }
    ) as agent_span:
        messages = [{"role": "user", "content": pergunta}]
        system_prompt = "Você ajuda com pedidos. Use as ferramentas disponíveis."

        tools = [
            {
                "name": "buscar_pedido",
                "description": "Busca info de um pedido pelo ID",
                "input_schema": {
                    "type": "object",
                    "properties": {
                        "pedido_id": {"type": "string"}
                    },
                    "required": ["pedido_id"]
                }
            }
        ]

        max_steps = 10
        for step in range(max_steps):
            # Cada chamada LLM gera um span filho automaticamente
            # via AnthropicInstrumentor
            response = client.messages.create(
                model="claude-sonnet-4-6",
                max_tokens=1024,
                system=system_prompt,
                messages=messages,
                tools=tools
            )

            # Claude retorna stop_reason: "end_turn" ou "tool_use"
            if response.stop_reason == "end_turn":
                texto_final = next(
                    (b.text for b in response.content if b.type == "text"), ""
                )
                agent_span.set_attribute("agent.steps", step + 1)
                return texto_final

            if response.stop_reason == "tool_use":
                # Adiciona a resposta do assistant ao histórico
                # no formato esperado pela SDK/API da Anthropic
                messages.append({
                    "role": "assistant",
                    "content": response.to_dict()["content"]
                })

                tool_results = []
                for block in response.content:
                    if block.type == "tool_use":
                        with tracer.start_as_current_span(
                            f"execute_tool {block.name}",
                            attributes={
                                "gen_ai.operation.name": "execute_tool",
                                "tool.name": block.name,
                                "tool.arguments": json.dumps(block.input)
                            }
                        ):
                            result = executar_ferramenta(
                                block.name, block.input
                            )
                            tool_results.append({
                                "type": "tool_result",
                                "tool_use_id": block.id,
                                "content": result
                            })

                messages.append({"role": "user", "content": tool_results})

        agent_span.set_attribute("agent.steps", max_steps)
        agent_span.set_attribute("agent.converged", False)
        return "Não consegui resolver."


def executar_ferramenta(nome: str, argumentos: dict) -> str:
    if nome == "buscar_pedido":
        return json.dumps({"status": "enviado", "previsao": "2025-03-15"})
    return json.dumps({"error": "ferramenta desconhecida"})
```

Algumas diferenças importantes entre os SDKs da Anthropic e OpenAI que afetam a instrumentação:

- **Stop reason:** Anthropic usa `stop_reason` com valores `"end_turn"` e `"tool_use"`. OpenAI usa `finish_reason` com `"stop"` e `"tool_calls"`.
- **Tool results:** No Anthropic, tool results vão como conteúdo do role `"user"` com `type: "tool_result"`. No OpenAI, vão como role `"tool"`.
- **System prompt:** No Anthropic, vai no parâmetro `system` separado. No OpenAI, vai como primeira mensagem com role `"system"`.

Mas nos traces OTel, ambos geram os mesmos atributos padronizados. Essa é a beleza: a observabilidade é normalizada.

## Métricas que você precisa ter em dashboard

Com traces funcionando, é hora de extrair métricas.

### 1. Custo por execução

Preços de API mudam frequentemente. Não faça hardcode de valores no código. Uma abordagem melhor:

```python
from dataclasses import dataclass
from pathlib import Path
import json
import os


@dataclass
class ModelPricing:
    input_per_mtok: float   # USD por 1M tokens de input
    output_per_mtok: float  # USD por 1M tokens de output


class CostCalculator:
    """Calcula custo de chamadas LLM a partir de um arquivo de config.

    Preços mudam frequentemente. Mantenha o arquivo de config atualizado
    consultando as páginas oficiais:
    - OpenAI: https://openai.com/api/pricing
    - Anthropic: https://docs.anthropic.com/en/docs/about-claude/pricing
    """

    def __init__(self, config_path: str | None = None):
        path = config_path or os.environ.get(
            "LLM_PRICING_CONFIG", "pricing.json"
        )
        self._pricing: dict[str, ModelPricing] = {}
        self._load(path)

    def _load(self, path: str) -> None:
        config_file = Path(path)
        if not config_file.exists():
            return
        data = json.loads(config_file.read_text())
        for model, prices in data.items():
            self._pricing[model] = ModelPricing(
                input_per_mtok=prices["input"],
                output_per_mtok=prices["output"]
            )

    def calculate(
        self, model: str, input_tokens: int, output_tokens: int
    ) -> float | None:
        pricing = self._pricing.get(model)
        if not pricing:
            return None
        return (
            input_tokens * pricing.input_per_mtok
            + output_tokens * pricing.output_per_mtok
        ) / 1_000_000
```

Com um `pricing.json` separado:

```json
{
  "gpt-5.4": {"input": 2.50, "output": 15.00},
  "gpt-5-mini": {"input": 0.25, "output": 2.00},
  "gpt-4.1": {"input": 2.00, "output": 8.00},
  "gpt-4.1-mini": {"input": 0.40, "output": 1.60},
  "gpt-4.1-nano": {"input": 0.10, "output": 0.40},
  "claude-opus-4-6": {"input": 5.00, "output": 25.00},
  "claude-sonnet-4-6": {"input": 3.00, "output": 15.00},
  "claude-haiku-4-5": {"input": 1.00, "output": 5.00}
}
```

Os valores acima são referência de março/2026. **Consulte sempre as páginas oficiais** ([OpenAI](https://openai.com/api/pricing), [Anthropic](https://docs.anthropic.com/en/docs/about-claude/pricing)) antes de usar.

Vantagens dessa abordagem: o arquivo de preços pode ser atualizado sem tocar no código, versionado separadamente, e até carregado de uma URL se quiser automatizar.

### 2. Token acumulado por conversa

A cada step do agente, o context window cresce. Se você não monitora, um agente de 5 steps pode estar mandando 50k tokens no último call porque acumula todo o histórico.

Capture `gen_ai.usage.input_tokens` em cada chamada e plote a progressão. Se o crescimento é linear, tudo certo. Se é exponencial, tem algo errado no gerenciamento de contexto.

Isso é especialmente relevante em modelos com janelas de contexto muito grandes (200k+ tokens). Só porque cabe, não significa que deveria estar lá.

### 3. Steps até convergência

Histograma de quantos passos o agente leva pra resolver uma tarefa. Se a maioria resolve em 2-3 steps mas tem um tail de 10+, investigue esses casos.

### 4. Taxa de uso de ferramentas

Qual ferramenta é mais chamada? Alguma nunca é usada? Alguma falha frequentemente? Isso mostra se o agente tá usando bem o que tem disponível.

### 5. Latência decomposta

Não basta saber que a execução total levou 15s. Decompor:

- Tempo em chamadas LLM (latência do provider)
- Tempo em tool calls (latência da sua infra)
- Tempo de processamento local

Se 80% do tempo tá em tool calls, o gargalo é a sua API, não o modelo.

## Alertas que salvam

Configure alertas pra cenários críticos:

```yaml
# Exemplo conceitual (adapte pro seu sistema de alertas)
alertas:
  - nome: agente_em_loop
    condição: agent.steps > 8
    ação: matar execução + notificar

  - nome: custo_alto
    condição: custo_execucao > 0.50  # USD
    ação: notificar

  - nome: latencia_alta
    condição: duracao_total > 30s
    ação: notificar

  - nome: token_explosion
    condição: input_tokens_ultima_chamada > 100000
    ação: matar execução + notificar
```

Na prática, implemente isso com métricas OTel + Prometheus + Alertmanager, ou use os alertas nativos do Grafana/Datadog/seu backend.

## Grafana: visualizando tudo

Se você usa Grafana (e deveria, é grátis e open-source), aqui vai uma stack completa:

```yaml
# docker-compose.yml
services:
  jaeger:
    image: jaegertracing/jaeger:latest
    ports:
      - "16686:16686"  # UI
      - "4317:4317"    # OTLP gRPC

  prometheus:
    image: prom/prometheus:latest
    ports:
      - "9090:9090"
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml

  grafana:
    image: grafana/grafana:latest
    ports:
      - "3001:3000"
    environment:
      - GF_AUTH_ANONYMOUS_ENABLED=true
```

Configurando o Prometheus pra coletar métricas OTel, você consegue dashboards com:

- Custo acumulado por hora/dia, segmentado por provider e modelo
- Latência P50/P95/P99 por modelo (compare Claude vs GPT lado a lado)
- Token usage ao longo do tempo
- Top ferramentas mais chamadas
- Execuções que excederam o limite de steps

## Considerações de segurança

Capturar prompts e respostas em produção levanta questões sérias:

1. **PII (dados pessoais):** se o usuário manda dados pessoais pro agente e você loga tudo, agora você tem PII no seu sistema de observabilidade. Defina políticas de retenção e considere mascaramento.

2. **Prompts de sistema:** seu prompt de sistema é propriedade intelectual. Cuidado com quem tem acesso aos traces completos.

3. **Retenção:** traces de LLM são grandes (tokens de input/output). Defina TTL agressivo pra dados de conteúdo (7-30 dias) e mantenha métricas agregadas por mais tempo.

4. **Ambiente:** capture conteúdo de mensagens **só em staging/debug**. Em produção, capture apenas métricas e metadados.

## Comparativo rápido

Pra fechar, quando usar o quê:

**OTel puro + Jaeger/Grafana:** você já tem infra de observabilidade OTel. Quer controle total. Escolhe o instrumentor específico do provider que usa.

**OpenLLMetry:** quer instrumentação automática de múltiplos providers e frameworks sem escrever código de instrumentação. Instala, inicializa, e tudo que passa pelo SDK é trackeado.

**Langfuse:** quer uma plataforma completa com UI pronta, dashboard de custos, prompt management e avaliação. Self-hosted ou cloud. Aceita dados via OTLP (HTTP), então combina com qualquer instrumentação.

As três opções não são excludentes. Você pode usar OpenLLMetry pra instrumentar tanto OpenAI quanto Anthropic e exportar pro Langfuse via OTLP. Ou usar instrumentações oficiais do OTel e mandar pro Jaeger + Grafana. Mix and match.

## Conclusão

Agentes de AI em produção sem observabilidade é receita pra dor de cabeça. O ecossistema já tem ferramentas maduras o suficiente pra resolver isso, e todas suportam os principais providers.

Comece simples: instrumente suas chamadas LLM com o instrumentor do seu provider, exporte pra um Jaeger local, e veja os traces. Depois adicione métricas de custo e alertas. Quando sentir necessidade, suba pra uma plataforma como Langfuse.

O investimento é pequeno. A alternativa é descobrir que seu agente gastou $200 em tokens fazendo loop às 3 da manhã.

