Registro Estruturado de Prompts, Respostas e Contagens de Tokens
Um log estruturado é um registro consistente e analisável por máquina de uma chamada da API Claude: o prompt, a resposta, as contagens de tokens, o modelo, a latência e um ID de solicitação.
Sem essa consistência, cada local de chamada registra algo ligeiramente diferente, e responder "quantos tokens usamos ontem" se transforma em uma busca por texto inconsistente em vez de executar uma consulta.
O objetivo do registro estruturado é um esquema fixo aplicado a cada chamada, não uma mensagem de log de melhor esforço.
Cada campo no esquema ganha seu lugar: o prompt e a resposta para depurar problemas de qualidade, as contagens de tokens para custos, a latência para desempenho, o nome do modelo para comparar entre versões de modelo e um ID de solicitação para correlacionar com traces ou tickets de suporte.
Um logging.Logger do Python com um formatador JSON, ou uma função dedicada de adaptador de log, é suficiente para impor isso sem adotar um novo framework.
O padrão se generaliza além de uma única chamada messages.create: respostas em streaming, conversas com vários turnos e loops de agente que usam ferramentas precisam que o mesmo esquema seja aplicado por chamada, não por conversa.
Esta página constrói um adaptador de log reutilizável, e depois o estende para casos de múltiplos turnos e streaming.
import jsonimport loggingimport timeimport uuidimport anthropiclogging.basicConfig(level=logging.INFO, format="%(message)s")logger = logging.getLogger("claude.calls")client = anthropic.Anthropic()def log_entry(**fields) -> None: """Emite uma linha de log JSON estruturada, descartando qualquer campo deixado como None.""" logger.info(json.dumps({k: v for k, v in fields.items() if v is not None}))def call_claude(prompt: str, model: str = "claude-sonnet-5") -> str: """Faz uma chamada Claude e registra uma entrada estruturada para ela, sucesso ou falha.""" request_id = str(uuid.uuid4()) start = time.monotonic() try: response = client.messages.create( model=model, max_tokens=500, messages=[{"role": "user", "content": prompt}], ) except anthropic.APIStatusError as exc: log_entry( request_id=request_id, model=model, prompt=prompt, response=None, input_tokens=None, output_tokens=None, latency_ms=round((time.monotonic() - start) * 1000, 1), status="error", error_type=exc.__class__.__name__, status_code=exc.status_code, ) raise response_text = response.content[0].text log_entry( request_id=request_id, model=response.model, prompt=prompt, response=response_text, input_tokens=response.usage.input_tokens, output_tokens=response.usage.output_tokens, cache_read_input_tokens=getattr(response.usage, "cache_read_input_tokens", None), latency_ms=round((time.monotonic() - start) * 1000, 1), status="ok", ) return response_textdef call_claude_multi_turn(messages: list[dict], model: str = "claude-sonnet-5") -> dict: """Mesmo esquema, aplicado a um turno de uma conversa com vários turnos.""" request_id = str(uuid.uuid4()) start = time.monotonic() response = client.messages.create( model=model, max_tokens=500, messages=messages, ) log_entry( request_id=request_id, model=response.model, prompt=messages[-1]["content"], response=response.content[0].text if response.content else "", input_tokens=response.usage.input_tokens, output_tokens=response.usage.output_tokens, latency_ms=round((time.monotonic() - start) * 1000, 1), status="ok", turn_count=len(messages), ) return {"request_id": request_id, "response": response}if __name__ == "__main__": call_claude("Resuma os trade-offs dos microsserviços em duas frases.")
O que isso demonstra:
Uma única função log_entry aplicando um esquema, chamada tanto do caminho de sucesso quanto do de erro.
Descartar campos None evita que linhas de log de erro carreguem campos de response/token vazios enganosos.
request_id gerado antes da chamada, para que exista mesmo quando a chamada falha.
cache_read_input_tokens capturado com getattr, pois só está presente quando o cache de prompt se aplica.
O mesmo esquema reutilizado para uma chamada com vários turnos, com um campo turn_count adicionado, mostrando que o esquema se estende em vez de ser substituído por tipo de chamada.
O esquema é apenas um dicionário Python serializado para JSON; nenhuma biblioteca especial é necessária, embora uma biblioteca dedicada de log JSON (python-json-logger, structlog) remova o boilerplate manual de json.dumps em escala.
Centralizar a chamada de log em uma única função (log_entry) é o que realmente impõe o esquema; se cada local de chamada constrói seu próprio dicionário inline, os campos se afastam ao longo do tempo.
logging.Logger (não print) é usado porque se integra com filtragem de nível de log, manipuladores e a maioria dos agentes de agregação de log (Datadog Agent, Fluent Bit, CloudWatch Logs agent) prontos para uso.
Os caminhos de erro e sucesso usam o mesmo conjunto de campos com status os distinguindo, para que uma única consulta como status:error ou uma agregação sobre input_tokens funcione independentemente do resultado.
Prefira logging.Logger em vez de print, mesmo para scripts; não custa nada extra e significa que o mesmo código funciona sem modificações assim que você adiciona manipuladores para um agregador de log real.
Use logger.info(json.dumps(...)) para uma configuração mínima; para serviços de alto rendimento, uma subclasse logging.Formatter que codifica em JSON todo o LogRecord evita serialização dupla e mantém os rastros de pilha intactos em erros.
Redija ou trunque os campos prompt/response antes de registrar se sua aplicação lida com dados de usuário regulamentados ou sensíveis; um esquema estruturado facilita isso, pois a redação se torna uma função, não uma decisão por local de chamada.
Construir o dicionário de log inline em cada local de chamada. Sem uma função log_entry compartilhada, um local de chamada adiciona um campo e outro não, e seu esquema se desvia silenciosamente. Correção: centralize a construção do log em uma função ou uma pequena classe adaptadora de log.
Registrar campos usage antes de verificar uma exceção. Se a chamada falhar, response nunca é atribuído, e referenciar response.usage em um bloco finally causa um erro NameError. Correção: calcule os campos de token apenas dentro do ramo de sucesso e registre None para eles no caminho de erro.
cache_read_input_tokens ausente quebrando a matemática de custos downstream. Campos de uso relacionados ao cache nem sempre estão presentes no objeto de resposta, dependendo da versão do SDK e se o cache se aplicou. Correção: acesse-os com getattr(response.usage, "cache_read_input_tokens", None) em vez de acesso direto ao atributo.
Registrar o texto completo do prompt/resposta em um nível de log muito alto em produção. Isso pode inflar os custos de armazenamento de log e, se os prompts contiverem PII do usuário, cria uma superfície de conformidade que você não pretendia. Correção: aplique redação/truncamento antes de registrar e confirme sua política de retenção de log para este fluxo de acordo com seus requisitos de manuseio de dados.
Reutilizar print(json.dumps(...)) em vez do módulo de log. Isso ignora a filtragem de nível de log e a maioria dos agentes de agregação de log, que esperam linhas de log no stdout/stderr via um framework de log ou esperam um formato de arquivo específico. Correção: sempre roteie através de logging.Logger, mesmo para um script rápido.
Por que não usar apenas `print()` para registrar chamadas Claude durante o desenvolvimento?
A saída print não é estruturada, não se integra com a filtragem de nível de log, e a maioria das ferramentas de agregação de log (Datadog Agent, CloudWatch Logs, Fluent Bit) esperam saída através de um framework de log para analisá-la de forma confiável. Usar logging.Logger desde o início não custa nada extra e significa que o mesmo código funciona assim que você adiciona manipuladores reais.
Devo registrar o texto completo do prompt e da resposta, ou apenas um resumo?
Registre o texto completo quando puder, pois logs truncados tornam a depuração de problemas de qualidade muito mais difícil. Se seus prompts ou respostas contiverem dados sensíveis do usuário, adicione redação antes de registrar em vez de omitir os campos inteiramente, para manter a capacidade de depuração sem o risco de conformidade.
Qual é o conjunto mínimo de campos que uma entrada de log estruturada precisa?
request_id, model, prompt, response, input_tokens, output_tokens, latency_ms e status.
Todo o resto (tokens de cache, detalhes de erro, contagem de turnos) é aditivo em cima desse esquema base.
Como manter o esquema consistente em vários locais de chamada em uma base de código grande?
Centralize o registro em uma função ou uma pequena classe adaptadora que todos os locais de chamada usam, em vez de deixar cada local de chamada construir seu próprio dicionário de log. Esta é a maior alavanca para a consistência do esquema; sem ela, os campos se desviam em semanas.
O que acontece com `response.usage` se a chamada da API gerar uma exceção?
try: response = client.messages.create(...)except anthropic.APIStatusError as exc: # response nunca foi atribuído; registre None para os campos de uso em vez disso log_entry(status="error", error_type=exc.__class__.__name__, input_tokens=None) raise
Não há objeto response para ler usage dele, então o ramo de erro deve registrar None (ou omitir) os campos de uso em vez de tentar referenciar uma variável não atribuída.
As respostas em streaming precisam de uma abordagem de log diferente?
O esquema permanece o mesmo, mas você registra uma vez após a conclusão do stream, usando stream.get_final_message() para os números de uso finais, em vez de registrar por fragmento. Registrar dentro do loop de fragmentos produziria uma linha de log por fragmento de token em vez de uma por chamada.
Por que incluir um `request_id` se eu ainda não estou usando rastreamento?
Isso prepara seus logs para correlação com rastreamento quando você o adicionar.
Também é útil por si só para correlacionar o relatório de bug de um usuário com a chamada exata que produziu o resultado dele.
Gerá-lo não custa nada e é uma única chamada uuid.uuid4().
O `cache_read_input_tokens` está sempre presente na resposta?
Não, ele só é preenchido de forma significativa quando o cache de prompt está em uso para essa chamada. Acesse-o defensivamente com getattr(response.usage, "cache_read_input_tokens", None) para que seu código de log não falhe em chamadas que não usam cache.
Os logs de erro devem usar exatamente o mesmo esquema que os logs de sucesso?
Sim, com status como o campo que os distingue e os campos de token/resposta definidos como None em caso de erro. Manter o esquema compartilhado significa que uma consulta (por exemplo, latência média ou contagem agrupada por status) funciona em ambos os resultados sem formatos de log separados.
Como este esquema se conecta a um dashboard de custos?
Os campos input_tokens, output_tokens e model são exatamente o que um cálculo de custo precisa; um remetente de log ou um trabalho em lote lê essas linhas de log estruturadas, multiplica pelo preço por modelo e encaminha o resultado para um dashboard. Sem um esquema consistente, esse cálculo não tem nada confiável para analisar.
Qual é um nível de log razoável para essas entradas?
INFO para chamadas bem-sucedidas é típico, pois são eventos esperados e de alto volume que você ainda deseja reter para análise de custos e uso. ERROR (ou WARNING, dependendo da gravidade) se encaixa no caminho de falha, o que permite filtrar dashboards e alertas por nível, além do campo status.
Versões de Stack: Escrito contra a linha de modelos Claude atual em ~junho de 2026 - Claude Fable 5, Claude Opus 4.8, Claude Sonnet 5 (o padrão), e Claude Haiku 4.5 - e o SDK oficial anthropic Python (última versão 0.x). Nomes de modelos, preços e versões de SDK mudam rapidamente - verifique os detalhes atuais em platform.claude.com/docs antes de confiar neles.
Revisado por Chris St. John·Última atualização: 13 de jul. de 2026