CodeGym /Cursos /ChatGPT Apps /Logs estruturados e correlação de requisições

Logs estruturados e correlação de requisições

ChatGPT Apps
Nível 17 , Lição 0
Disponível

1. Por que você precisa de logs estruturados no ChatGPT App

Imagine que o product manager lhe escreva: “Os usuários reclamam que, ao escolher um presente, às vezes aparece uma lista vazia e às vezes o checkout cai. Dá para consertar até a demo de amanhã?”. Você tem:

  • O ChatGPT, que às vezes chama seu App — e às vezes não.
  • Um widget no sandbox.
  • Um servidor MCP que consulta uma base externa de produtos e a ACP.
  • Webhooks do provedor de pagamentos.

E apenas logs textuais dispersos como “something went wrong” em algum lugar no MCP e “order failed” em algum lugar no backend. Com requisições em paralelo, isso vira caos: é impossível entender qual log corresponde a qual usuário e a qual requisição.

Logs JSON estruturados e um único trace_id são exatamente o que você precisa para:

  • ver toda a cadeia por um único identificador: da requisição do ChatGPT até o webhook "order.created";
  • filtrar logs por serviço, ferramenta, usuário, cenário;
  • responder rapidamente às perguntas “por que o checkout falhou” e “o que o agente fez antes de começar a alucinar”.

Ou seja, o objetivo é simples: fazer com que o GiftGenius em produção possa ser debugado e monitorado tão bem quanto um aplicativo de microsserviços comum.

2. Logs textuais vs. estruturados: por que console.log("ops") já não resolve

No desenvolvimento comum de Next.js, muitos se limitam a logs textuais: imprimem uma frase legível para humanos e, às vezes, alguns valores. Em um serviço único, isso ainda é tolerável. Mas no stack do ChatGPT App, esses logs viram uma bagunça muito rápido.

Um log textual é apenas uma linha no arquivo ou no console. Por exemplo:


console.error(`Error in suggestGifts for user ${userId}: ${error.message}`);

Quando há cem mil mensagens dessas, achar “todos os erros do MCP no checkout com userId=… de ontem” já não é fácil. E construir um dashboard automaticamente para erros de ferramentas — quase impossível.

Um log estruturado é um objeto JSON no qual, além do texto da mensagem, há um conjunto de campos: nível, horário, serviço, identificadores, contexto técnico e de negócio. Análogo ao anterior:

logger.error({
    message: "suggest_gifts failed",
    user_id: userId,
    trace_id,
    service: "mcp",
    tool_name: "suggest_gifts",
    error_message: error.message,
});

Cada campo é indexado pelo sistema de logging (ELK, Loki, Better Stack, Datadog etc.), e então você pode escrever consultas como service="mcp" AND level="error" AND tool_name="suggest_gifts" ou simplesmente buscar por trace_id="...".

Para visualizar — uma pequena tabela.

O que comparamos Logs textuais Logs estruturados (JSON)
Parsing Manual, via regex Automático por campos
Busca por campos Consultas regexp complexas Expressões simples field=value
Agregações e dashboards Difícil, muitos “workarounds” Trivial: count() , group by field
Enriquecimento de contexto Como texto na mensagem Com novos campos sem mudar o schema
Correlação de requisições Quase impossível com requisições paralelas Busca normal por trace_id/request_id

No mundo de aplicativos com LLM, onde metade dos problemas não é “erro 500”, mas “o modelo chamou a ferramenta errada”, sem logs estruturados você fica literalmente cego.

3. Anatomia de um log JSON para o ChatGPT App

Vamos definir um “padrão mínimo” de registro de log que você usará em todas as camadas do GiftGenius. Ele não é perfeito, mas cobre 80 % das tarefas.

Vamos dividir os campos de logs em alguns grupos.

Campos técnicos

Campos técnicos servem para que as ferramentas de observabilidade entendam de onde veio o registro.

Podemos descrevê-los com um tipo TypeScript:

type LogLevel = "debug" | "info" | "warn" | "error";

interface BaseLogFields {
    timestamp: string;    // ISO 8601 UTC
    level: LogLevel;      // "info", "error"...
    service: string;      // "app-widget", "mcp", "agent", "commerce", "webhook"
    env: "dev" | "staging" | "prod";
    message: string;      // Descrição breve do evento
}

É melhor escrever o timestamp no formato ISO em UTC ("2025-11-21T10:15:30.123Z"), assim serviços diferentes podem ser ordenados por tempo sem confusão de timezones. service e env ajudam a separar, por exemplo, logs do MCP de produção dos logs do widget em dev. Isso é especialmente relevante se, mais tarde, você quiser integrar com OpenTelemetry e usar convenções comuns como service.name, service.version etc.

Campos de correlação

Este é o ponto mais importante desta aula. Sem eles, você não conseguirá relacionar eventos entre si.

Adicionemos ao nosso interface:

interface CorrelationFields {
    trace_id: string;        // ID de ponta a ponta de todo o cenário
    span_id?: string;        // (opcional) ID da operação específica
    parent_span_id?: string; // (opcional) Operação pai
    request_id?: string;     // ID local da requisição HTTP ou tool-call
    agent_run_id?: string;   // ID da execução do agente (se houver)
    tool_call_id?: string;   // ID da chamada da ferramenta específica
    checkout_session_id?: string; // ID da sessão ACP/pagamento
}

trace_id é o protagonista. Ele deve ser o mesmo em todos os logs relativos ao cenário “O usuário pediu recomendações de presentes, nós recomendamos, criamos o pedido, recebemos o webhook”. span_id e parent_span_id permitem depois construir uma “árvore de operações” no estilo de distributed tracing, mas, para começar, você pode se virar apenas com trace_id e request_id.

Contexto de negócio

Um log técnico sem contexto de negócio vira “algo aconteceu, em algum lugar, em algum momento”. Precisamos entender qual usuário e em qual etapa do cenário foi afetado.

Vamos estender a interface:

interface BusinessFields {
    user_id?: string;     // ID anônimo, NÃO e-mail
    tenant_id?: string;   // Organização/conta, se B2B
    flow?: string;        // Por exemplo, "gift_recommendation" ou "checkout"
    step?: string;        // Por exemplo, "collect_requirements" ou "create_checkout"
}

O princípio aqui é muito simples: os identificadores podem ser internos (UUID do seu banco), mas não devem conter PII (e‑mail, telefone, nome completo). Falaremos disso mais no bloco de segurança.

Campos de erro

Erros são um capítulo à parte. Um log típico de erro vale a pena dividir pelo menos em tipo, código e texto:

interface ErrorFields {
    error_type?: "validation" | "upstream" | "timeout" | "system";
    error_code?: string;       // Status HTTP, código do BD ou enum próprio
    error_message?: string;    // Curto e seguro
    stack?: string;            // Stack, cuidado com volume e PII
}

É importante que error_message não contenha dados sensíveis (tipo “failed for card 4111 1111 1111 1111”). Melhor "payment provider declined card" e algum código seguro.

Interface completa de log

Vamos juntar tudo:

export interface LogEvent
    extends BaseLogFields,
        CorrelationFields,
        BusinessFields,
        ErrorFields {
    // deixamos espaço para campos adicionais
    [key: string]: unknown;
}

Você pode usar essa interface tanto no servidor MCP quanto no backend de commerce e no agente. Assim, todos os serviços vão escrever logs no mesmo formato, e a correlação vira um passeio agradável, não uma caça ao tesouro.

4. O logger JSON mais simples para o GiftGenius (servidor MCP)

Vamos começar com algo bem minimalista. Suponha que seu servidor MCP seja um aplicativo Node.js/TypeScript. Faremos a utilidade logger:

// mcp/logging.ts
import { LogEvent, LogLevel } from "./types";

function log(level: LogLevel, event: Omit<LogEvent, "level" | "timestamp">) {
    const enriched: LogEvent = {
        timestamp: new Date().toISOString(),
        level,
        env: process.env.NODE_ENV === "production" ? "prod" : "dev",
        ...event,
    };

    // Imprimimos o JSON no stdout — o sistema de logs vai coletá-lo depois
    console.log(JSON.stringify(enriched));
}

export const logger = {
    debug: (event: Omit<LogEvent, "level" | "timestamp">) =>
        log("debug", event),
    info: (event: Omit<LogEvent, "level" | "timestamp">) =>
        log("info", event),
    warn: (event: Omit<LogEvent, "level" | "timestamp">) =>
        log("warn", event),
    error: (event: Omit<LogEvent, "level" | "timestamp">) =>
        log("error", event),
};

Isso não é Pino nem Winston, mas, para o curso, o que importa é a ideia: tudo é escrito como JSON com campos decentes.

Agora vamos usá-lo no handler da ferramenta MCP suggest_gifts.

5. Logging de uma ferramenta MCP: da entrada à saída

Suponha que você já tenha um handler para a ferramenta suggest_gifts, que recebe preferências do usuário e retorna uma lista de SKUs. Vamos adicionar logs a ele.

Vamos supor que já tenhamos obtido o trace_id do cabeçalho HTTP x-trace-id (como colocá-lo lá — vamos ver no próximo bloco sobre correlação).

// mcp/tools/suggestGifts.ts
import { logger } from "../logging";

export async function suggestGiftsTool(args: SuggestGiftsArgs, ctx: {
  traceId: string;
  userId?: string;
}) {
  logger.info({
    message: "suggest_gifts called",
    service: "mcp",
    trace_id: ctx.traceId,
    user_id: ctx.userId,
    tool_name: "suggest_gifts",
    flow: "gift_recommendation",
    step: "fetch_candidates",
  });

  try {
    const gifts = await fetchGiftsFromCatalog(args);

    logger.info({
      message: "suggest_gifts succeeded",
      service: "mcp",
      trace_id: ctx.traceId,
      user_id: ctx.userId,
      tool_name: "suggest_gifts",
      flow: "gift_recommendation",
      step: "rank_candidates",
      result_count: gifts.length,
    });

    return gifts;
  } catch (error: any) {
    logger.error({
      message: "suggest_gifts failed",
      service: "mcp",
      trace_id: ctx.traceId,
      user_id: ctx.userId,
      tool_name: "suggest_gifts",
      flow: "gift_recommendation",
      step: "fetch_candidates",
      error_type: "upstream",
      error_message: error.message,
    });
    throw error;
  }
}

Agora, por um único trace_id, será possível ver:

  • que a ferramenta foi chamada;
  • quantos candidatos foram encontrados;
  • em qual etapa ela falhou.

E em nenhum lugar aparece e‑mail ou nome do usuário — apenas o user_id interno.

6. Onde nasce o trace_id no ChatGPT App

Vamos entender onde o trace_id deve nascer. É importante entender que ele não está atrelado a uma requisição específica. trace_id é o identificador de uma operação de negócio. Portanto, é preciso separar duas situações típicas:

MCP tool “estreito”

É quando a ferramenta executa uma operação pequena e retorna o resultado imediatamente (sem UI interativa):

  • get_gifts_for_budget
  • calculate_price
  • save_lead etc.

Nesse caso, é conveniente considerar: uma chamada de ferramenta MCP = uma requisição de negócio = um trace. O trace_id de ponta a ponta nasce no lado do gateway/servidor MCP na entrada do tool‑call (ou é obtido de um contexto de tracing já existente, se você usa OpenTelemetry). Depois, esse trace_id é usado em todas as chamadas internas (serviços REST, bancos, filas) e aparece nos logs como campo trace_id.

O ChatGPT e o Apps SDK não interferem nisso: eles apenas enviam o tool‑call JSON‑RPC, e o tracing começa do seu lado, na zona sob seu controle.

MCP tool “amplo” (retorna um widget)

Aqui, a ferramenta não conclui a operação de negócio por completo, mas inicia uma cena interativa: retorna um widget que, no sandbox, faz dezenas de fetch() (carregar a lista de presentes, filtros, checkout etc.).

Nesse cenário, o tracing de ponta a ponta funciona de outra forma:

  • as principais operações de negócio vivem nas requisições HTTP do widget ao backend;
  • por isso, cada fetch() significativo do widget para o seu backend recebe seu próprio trace_id, que nasce no backend/gateway (o primeiro hop de servidor para esse fetch).

Nem o ChatGPT, nem o próprio widget são a “fonte da verdade” para o trace_id: eles apenas podem passar no request alguns identificadores auxiliares (session_id, widget_id, user_id), mas a criação e o gerenciamento do trace_id acontecem no servidor.

MCP tool “estreito”: um trace por tool‑call

Vamos ver como fica o fluxo para uma ferramenta “estreita” sem widget:

sequenceDiagram
    participant ChatGPT as ChatGPT / Agent
    participant MCP as Servidor MCP
    participant GiftAPI as Gift API
    participant Pricing as Pricing API

    ChatGPT->>MCP: JSON-RPC tools.call get_gifts
    MCP->>MCP: start trace (trace_id = T-123)
    MCP->>GiftAPI: GET /gifts (x-trace-id = T-123)
    GiftAPI-->>MCP: 200 OK (trace_id = T-123)
    MCP->>Pricing: GET /price (x-trace-id = T-123)
    Pricing-->>MCP: 200 OK (trace_id = T-123)
    MCP-->>ChatGPT: tool result (opcional com trace_id)

Padrão:

  • na entrada do tool‑call no MCP você cria o trace (ou pega um já existente de traceparent/x-trace-id);
  • todo o caminho desse tool‑call (chamadas a serviços, banco, caches) é logado com o mesmo trace_id;
  • não há participação do widget nos logs, porque não há widget.

Esse approach oferece:

  • um “instantâneo” claro de uma operação: “MCP‑tool suggest_gifts → Gift API → Pricing API → resposta”;
  • um trace_id por chamada de ferramenta.

MCP tool “amplo”: widget e vários traces

Agora o cenário do GiftGenius, no qual a ferramenta MCP retorna um widget:

  1. O ChatGPT chama a ferramenta MCP, por exemplo open_gift_widget.
  2. A ferramenta MCP gera a descrição do widget (layout, estado inicial) e a retorna.
  3. O widget é montado no sandbox e passa a viver sua própria vida:
    • GET /api/gifts?budget=50&page=1
    • GET /api/gifts?budget=50&filter=for_developers
    • POST /api/checkout
    • POST /api/save-lead
  4. Cada uma dessas requisições HTTP chega ao seu backend Next.js / gateway — e é que você cria um novo trace:
fetch #1  -> trace_id = T-501  (carregar a primeira página de presentes)
fetch #2  -> trace_id = T-502  (aplicar o filtro “para desenvolvedores”)
fetch #3  -> trace_id = T-503  (criar o checkout)
...

Ou seja:

  • a ferramenta MCP é “ampla”: sua principal tarefa é abrir o widget, e não executar toda a cadeia de negócio;
  • a lógica de negócio real (lista de presentes, escolha do top presente, checkout) vive no backend, que processa os fetch() do widget;
  • o grupo de requisições fetch() unidas por um único cenário de negócio tem seu trace_id único, que você gera no servidor na entrada da requisição HTTP.

Adicionalmente, você pode incluir em cada trace:

  • session_id (ID da sessão do ChatGPT, se houver),
  • widget_id,
  • user_id,
  • tool_run_id ou qualquer outro contexto.

Com trace_id você observa a operação específica (“checkout #3”); com session_id / widget_id — tudo o que aconteceu no escopo de um único widget/sessão.

7. Correlação de requisições: como o trace_id percorre App, MCP, widget e backend

Vamos à parte mais interessante: como fazer com que os identificadores necessários passem por todas as camadas — ChatGPT, servidor MCP, widget, backend de commerce e webhooks.

Fluxo de requisições com trace_id (diagrama do caso “amplo”)

Um pequeno esquema de como isso fica no GiftGenius:

sequenceDiagram
    participant ChatGPT as ChatGPT UI
    participant MCP as Servidor MCP
    participant Widget as Widget GiftGenius
    participant Backend as Backend Next.js
    participant ACP as Commerce API
    participant WH as Manipulador de Webhook

    ChatGPT->>MCP: tools.call open_gift_widget
    MCP-->>ChatGPT: Widget description (layout, config)
    ChatGPT->>Widget: Renderizar o widget no sandbox

    Widget->>Backend: GET /api/gifts (trace_id = T-501, nasce no Backend)
    Backend->>ACP: GET /gifts (x-trace-id = T-501)
    ACP-->>Backend: 200 OK (trace_id = T-501)
    Backend-->>Widget: JSON com presentes (trace_id = T-501 nos logs)

    Widget->>Backend: POST /api/checkout (trace_id = T-503, nasce no Backend)
    Backend->>ACP: POST /checkout (x-trace-id = T-503)
    ACP-->>Backend: 200 OK (trace_id = T-503)
    ACP-->>WH: webhook order.created (x-trace-id = T-503)
    WH->>WH: Registra o evento (trace_id = T-503)

Observe:

  • neste esquema, o trace_id não é gerado pelo widget;
  • ele aparece no ponto de entrada da requisição HTTP ao seu backend (handler de rota do Next.js, API‑gateway etc.);
  • depois esse trace_id é propagado:
    • nos logs do backend,
    • no cabeçalho x-trace-id ao chamar a ACP,
    • nos webhooks, se a ACP o retornar/propagar adiante.

6.5. Gerando e propagando o trace_id no backend para chamadas vindas do widget

Vamos reescrever o exemplo para deixar explícito: o trace_id nasce no backend, não no widget.

// app/api/mcp/tools/call/route.ts (Backend Next.js, proxy para o MCP)
import { NextRequest, NextResponse } from "next/server";
import { v4 as uuidv4 } from "uuid";
import { logger } from "@/mcp/logging";

export async function POST(req: NextRequest) {
  // Se veio um trace_id do mundo externo (por exemplo, do gateway), usamos ele.
  // Caso contrário, geramos um novo na entrada do backend.
  const incomingTraceId = req.headers.get("x-trace-id");
  const traceId = incomingTraceId ?? uuidv4();
  const requestId = uuidv4();

  logger.info({
    message: "mcp.tools.call received from widget",
    service: "backend",
    trace_id: traceId,
    request_id: requestId,
  });

  const body = await req.json();

  const res = await fetch(process.env.MCP_SERVER_URL!, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "x-trace-id": traceId,
    },
    body: JSON.stringify(body),
  });

  const json = await res.json();

  logger.info({
    message: "mcp.tools.call completed",
    service: "backend",
    trace_id: traceId,
    request_id: requestId,
  });

  return NextResponse.json(json);
}

No lado do servidor MCP, nós apenas lemos esse cabeçalho e usamos o trace_id nos nossos logs (como nos exemplos da seção 5).

O widget pode nem saber da existência do trace_id — basta chamar /api/mcp/tools/call. Mas, se for útil exibir ou logar ações de UI vinculadas à rastreabilidade, você pode retornar o trace_id na resposta e escrever, por exemplo, service: "app-widget" nos seus próprios logs JSON (no cliente ou via alguma análise SaaS).

Exemplo de chamada ao MCP no cliente a partir do widget

// app/lib/mcpClient.ts (widget)
export async function callMcpTool(toolName: string, args: unknown) {
  const res = await fetch("/api/mcp/tools/call", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      // NÃO geramos trace_id aqui — ele vai nascer no backend
    },
    body: JSON.stringify({ toolName, args }),
  });

  // Se o backend devolver trace_id no corpo, você pode salvá-lo:
  const data = await res.json();
  return data;
}

Se quiser, você pode estender o handler do backend para que ele adicione o trace_id na resposta JSON; assim, o widget poderá:

  • logar eventos como "service": "app-widget", "trace_id": "...",
  • exibir links de trace para desenvolvedores.

Mas o princípio segue o mesmo: a fonte do trace_id é o servidor, não o widget.

Propagando o trace_id adiante para a ACP/commerce

Agora, dentro da ferramenta MCP create_checkout_session, chamamos seu commerce API e continuamos levando o trace_id nos cabeçalhos:

// mcp/tools/createCheckout.ts
import { logger } from "../logging";

export async function createCheckoutTool(
  args: CreateCheckoutArgs,
  ctx: { traceId: string; userId?: string }
) {
  logger.info({
    message: "create_checkout called",
    service: "mcp",
    trace_id: ctx.traceId,
    user_id: ctx.userId,
    tool_name: "create_checkout_session",
    flow: "checkout",
    step: "create_session",
  });

  const res = await fetch(process.env.COMMERCE_URL + "/checkout", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "x-trace-id": ctx.traceId,
    },
    body: JSON.stringify({
      userId: ctx.userId,
      ...args,
    }),
  });

  if (!res.ok) {
    logger.error({
      message: "checkout API failed",
      service: "mcp",
      trace_id: ctx.traceId,
      user_id: ctx.userId,
      flow: "checkout",
      step: "create_session",
      error_type: "upstream",
      error_code: String(res.status),
    });
    throw new Error("Checkout API failed");
  }

  const data = await res.json();

  logger.info({
    message: "checkout session created",
    service: "mcp",
    trace_id: ctx.traceId,
    user_id: ctx.userId,
    flow: "checkout",
    step: "create_session",
    checkout_session_id: data.sessionId,
  });

  return data;
}

O backend de commerce, por sua vez, também lê x-trace-id e o escreve nos seus logs JSON. Assim, com um único trace_id você verá:

  • a requisição HTTP de entrada do widget no backend (onde o trace nasceu);
  • o proxy para o MCP (se houver);
  • a chamada interna create_checkout_session;
  • a chamada ao commerce API;
  • a resposta do backend de commerce;
  • e, se ele também propagar o cabeçalho, o webhook order.created.

8. Níveis de log: DEBUG, INFO, WARN, ERROR no contexto de um app com LLM

Os níveis de log ajudam a não se afogar em informação. No ChatGPT App, é conveniente tratá-los assim:

  • DEBUG — informação técnica detalhada, útil em dev/staging. Por exemplo, prompts resumidos, estados intermediários do agente, respostas “cruas” de APIs externas (sem PII). Em produção, é preciso muito cuidado.
  • INFO — eventos normais de negócio: “suggest_gifts succeeded, 10 candidates”, “checkout session created”, “webhook order.created processed”. Esses logs podem ficar ligados em produção.
  • WARN — algo saiu do padrão, mas o sistema continuou funcionando. Por exemplo: “fallback to cached catalog because upstream timeout”, “model returned invalid tool args, retry with different schema”.
  • ERROR — falha explícita: o cenário não concluiu como deveria. Por exemplo: “checkout API failed”, “failed to persist order”, “tool crashed with unhandled exception”.

Para conveniência, você pode adicionar um helper simples, para não escrever as regras na mão:

type LogLevel = "debug" | "info" | "warn" | "error";

function isProd() {
  return process.env.NODE_ENV === "production";
}

export function shouldLogLevel(level: LogLevel): boolean {
  if (isProd()) {
    return level === "info" || level === "warn" || level === "error";
  }
  return true; // em dev, logamos tudo
}

E chamar logger.debug apenas quando shouldLogLevel("debug") retornar true.

É especialmente perigoso em produção escrever logs de DEBUG com o prompt completo e a resposta do modelo: facilmente podem aparecer senhas, chaves, qualquer PII que o usuário tenha colado por engano no chat.

9. Segurança dos logs: PII‑scrub e segredos

É fácil exagerar nos logs. Se você escrever “tudo”, vai:

  • violar leis de proteção de dados;
  • facilitar a vida de invasores (segredos e tokens podem ser extraídos dos logs);
  • ter medo de conceder acesso ao sistema de logs para outras pessoas.

Por isso, vale a regra simples: há informação suficiente nos logs para entender o que aconteceu, mas insuficiente para roubar dados.

Boas práticas:

  1. Logue user_id, não e‑mail ou telefone. Se você realmente precisar do e‑mail nos logs para debug, logue o hash ou use máscara ("a***@gmail.com").
  2. Nunca escreva tokens completos ("sk-..."), refresh tokens, client_secret, senhas. Se for muito necessário — apenas os 4 primeiros/últimos caracteres e o tipo (“sk-***1234”).
  3. Cuidado com tool_input e tool_output. Eles podem conter tudo o que o usuário escreveu. Em produção, ou não os logue por completo ou:
    • logue apenas campos tipados, já validados;
    • corte para um tamanho razoável e aplique scrub — mascaramento por regex (e‑mail, números de cartão etc.).

Exemplo simples de sanitizador (bem simplificado):

export function sanitize(text: string): string {
  return text
    .replace(/sk-[a-zA-Z0-9]{20,}/g, "sk-***redacted***")
    .replace(/\b\d{16}\b/g, "****-****-****-****"); // cartões
}

E, ao logar a entrada do usuário:

logger.debug({
  message: "raw_user_message",
  service: "app-widget",
  trace_id,
  user_id,
  raw: sanitize(userMessage),
});

Esse código está longe do nível de produção, mas mostra bem a ideia: primeiro higienizamos, depois logamos.

10. Prática: evento gift_recommended para o GiftGenius

Agora vamos fazer o exercício: projetar o evento de log gift_recommended, registrado quando o GiftGenius escolhe finalmente o “top presente” para o usuário.

O evento deve permitir responder às perguntas:

  • qual usuário (ID interno);
  • qual presente (SKU);
  • por qual cenário e em qual etapa;
  • qual trace_id, para relacionar com os demais logs.

E, ao mesmo tempo, não deve conter PII nem segredos.

Exemplo:

{
  "timestamp": "2025-11-21T10:22:33.456Z",
  "level": "info",
  "service": "agent",
  "env": "prod",
  "message": "gift_recommended",
  "trace_id": "a3b9e8c2-1f47-4ec5-9bdf-9d4e0c123abc",
  "agent_run_id": "run_7f1d2c",
  "user_id": "u_123456",
  "flow": "gift_recommendation",
  "step": "final_choice",
  "recommended_sku": "SKU-SPACE-MUG-001",
  "price_cents": 2499,
  "currency": "USD",
  "reason_summary": "recipient_likes_space_and_practical_gadgets"
}

O que importa aqui:

  • Registramos user_id, mas não e‑mail nem nome;
  • SKU e preço — são dados normais de negócio, não considerados PII;
  • reason_summary — uma etiqueta técnica curta, não a frase completa do usuário;
  • trace_id e agent_run_id, para permitir ver quais ferramentas o agente chamou no caminho até essa escolha.

O que não deve ser logado de jeito nenhum:

  • o texto completo da resposta do modelo com a explicação “humana”;
  • o prompt do usuário (“quero um presente para uma colega, o telefone dela é tal, endereço tal”);
  • quaisquer dados de pagamento.

11. Exemplos de logs: tool‑call bem-sucedido e erro da ACP

Para fixar — dois pequenos exemplos JSON.

tools.call bem-sucedido no MCP

{
  "timestamp": "2025-11-21T10:20:00.000Z",
  "level": "info",
  "service": "mcp",
  "env": "prod",
  "message": "tools.call completed",
  "trace_id": "a3b9e8c2-1f47-4ec5-9bdf-9d4e0c123abc",
  "request_id": "req_01JCQ5CZ0YQ6TM7E5W8H3N3F2Y",
  "tool_name": "suggest_gifts",
  "user_id": "u_123456",
  "flow": "gift_recommendation",
  "step": "rank_candidates",
  "result_count": 12,
  "latency_ms": 430
}

De um único log como esse já dá para ver:

  • qual ferramenta;
  • para qual usuário;
  • em qual cenário;
  • quanto tempo levou e quantos candidatos foram retornados.

Com o trace_id, você acha facilmente os logs da UI e do agente relativos à mesma requisição.

Erro da ACP/checkout

{
  "timestamp": "2025-11-21T10:21:05.789Z",
  "level": "error",
  "service": "commerce",
  "env": "prod",
  "message": "checkout failed",
  "trace_id": "a3b9e8c2-1f47-4ec5-9bdf-9d4e0c123abc",
  "checkout_session_id": "cs_test_9YpQvJH8",
  "user_id": "u_123456",
  "flow": "checkout",
  "step": "charge_customer",
  "error_type": "upstream",
  "error_code": "PAYMENT_DECLINED",
  "error_message": "payment provider declined card",
  "provider": "stripe",
  "amount_cents": 2499,
  "currency": "USD"
}

Novamente, nenhum número de cartão, apenas o código de erro e uma mensagem segura. E, mais uma vez, o mesmo trace_id, então você pode relacionar esse log ao gift_recommended e entender em que etapa a cadeia rompeu.

12. Como não transformar logs em lixo

É muito tentador: “já que conseguimos logar tudo bonitinho, vamos logar absolutamente tudo”. Assim você rapidamente terá gigabytes de ruído JSON, nos quais eventos úteis desaparecem.

Algumas dicas práticas:

  • Logs duplicados do tipo “entrei na função X” sem informação adicional são pouco úteis. É melhor logar eventos significativos: início/fim de cenário, chamada de API externa, transição de etapa de workflow, erros.
  • Para operações frequentes (por exemplo, requisições ao catálogo de produtos), você pode habilitar sampling: logar 1 de N requisições por completo e as demais — só em caso de erro.
  • Em produção, mantenha DEBUG desligado (ou muito seletivo). Se houver logging de prompts/respostas — que seja limitado e com scrub.

Falaremos de métricas e SLO em outra aula, mas já agora é importante entender: logs não são apenas “para debug”, são o alicerce da observabilidade de todo o stack do ChatGPT.

Lembra do product manager do começo da aula com a “lista vazia” e o checkout caindo? Com o esquema de logs descrito, você encontraria em poucos minutos todas as requisições com o trace_id desejado, veria suggest_gifts (quantos candidatos a ferramenta retornou, em qual etapa ela falhou) e os logs de "checkout failed" com error_code do provedor de pagamentos. Isso deixa de ser uma investigação “em sopa de logs” e vira um cenário claro “da requisição ao webhook”.

No fim, um bom stack de logging para o ChatGPT App não é “escrevemos algo no stdout”, mas sim:

  • pontos de nascimento corretos do trace_id (no gateway/servidor MCP para ferramentas “estreitas” e na entrada do backend para os fetch() do widget em cenários “amplos”);
  • um único trace_id através de App → MCP → commerce → webhooks para cada chamada de negócio significativa;
  • um schema comum de logs JSON (service, env, user_id, flow, step, tool_name etc.);
  • cuidado com PII e segredos (scrub, mascaramento, DEBUG limitado em produção);
  • níveis de log bem definidos e ausência de ruído.

Com essa base, todas as outras ferramentas de observabilidade (métricas, SLO, alertas) se tornam muito mais úteis e ajudam não apenas a “coletar logs”, mas a gerenciar de fato a qualidade e a estabilidade do seu ChatGPT App.

13. Erros típicos ao trabalhar com logs estruturados e correlação

Erro nº 1: ausência de um trace_id único em todos os serviços.
Caso clássico: o gateway MCP gera um ID, o backend de commerce — outro, os webhooks não sabem nada sobre correlação, e nos logs do widget o trace_id nem aparece. Como resultado, a correlação vira uma busca manual “parece que o horário coincide”. A abordagem correta é gerar o trace_id nos pontos de entrada controlados (servidor MCP para ferramentas “estreitas”, backend/gateway — para fetch() vindos do widget) e carregá-lo através de todas as fronteiras: cabeçalhos HTTP, campos JSON, contexto do agente.

Erro nº 2: tentar gerar o trace_id no widget e tratá-lo como “verdade”.
Às vezes parece lógico: “vamos gerar crypto.randomUUID() direto no widget React e enviar nos cabeçalhos”. O problema é que, assim, o trace_id vive no cliente e pode não coincidir com o tracing real do servidor (OpenTelemetry, gateway, outros serviços). É muito mais confiável que o trace_id surja onde você controla todo o caminho no servidor: no backend Next.js, no API‑gateway ou no servidor MCP. O widget, se quiser, pode apenas ler esse ID e logá-lo.

Erro nº 3: logar PII e segredos “para facilitar o debug”.
No início do desenvolvimento, “é muito prático” gravar no log o corpo inteiro do prompt, tokens, números de cartão e e‑mail. Depois de alguns meses, isso vira uma bomba-relógio: o acesso aos logs fica tóxico, a auditoria de segurança começa a fazer perguntas incômodas, e você tem receio até de mostrar um screenshot do erro. Desde o começo, implemente scrub e não logue aquilo que amanhã você terá de limpar às pressas.

Erro nº 4: logs textuais sem estrutura em uma das camadas.
Às vezes, o time faz logs JSON ótimos no MCP e no commerce, mas no widget deixa console.log("step 1", data). O resultado é que o começo e o fim da cadeia ficam desconectados.

Erro nº 5: abuso do nível ERROR.
Se qualquer desvio insignificante (tipo “o modelo retornou 0 candidatos, mostramos fallback”) é logado como ERROR, os alertas de produção vão acender o tempo todo. O time logo para de reagir a alertas. Procure separar honestamente: “WARN — estranho, mas contornamos; ERROR — o cenário do usuário realmente quebrou”.

Erro nº 6: schemas de logs inconsistentes entre serviços.
Quando em um serviço o campo se chama traceId, em outro correlation_id, e no terceiro requestId, nenhum sistema de logs salva. É importante alinhar um schema único (como fizemos com LogEvent) e segui-lo em todos os componentes: widget do App, servidor MCP, agentes, ACP, webhooks. Assim, construir dashboards de ponta a ponta e investigar incidentes vira questão de minutos, não de dias.

Erro nº 7: tentar “otimizar” o tamanho dos logs descartando campos-chave.
Às vezes, na pressa de economizar espaço, alguém decide: “vamos remover user_id ou flow, isso é detalhe”. Depois, de repente, é preciso responder “para quais usuários o checkout falha com mais frequência?” — e descobre-se que não há informação. Se for escolher o que descartar, descarte payloads textuais longos (corpos de requests/respostas) e campos de debug, não identificadores e atributos de contexto essenciais.

Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION