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.
Resumo
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.
Receita
import json
import logging
import time
import uuid
logger = logging.getLogger("claude.calls")
def log_call(*, request_id, model, prompt, response_text, usage, latency_ms, status="ok"):
logger.info(json.dumps({
"request_id": request_id,
"model": model,
"prompt": prompt,
"response": response_text,
"input_tokens": usage.input_tokens,
"output_tokens": usage.output_tokens,
"latency_ms": latency_ms,
"status": status,
}))Quando usar isso:
- Você tem mais de um local de chamada fazendo chamadas à API Claude e deseja um formato de log consistente e consultável.
- Você precisa responder "quantos tokens esta funcionalidade usou na semana passada" a partir dos logs, não reexecutando as solicitações.
- Você está prestes a adicionar dashboards de rastreamento ou de custos, ambos dependem da existência desse esquema primeiro.
- Você deseja logs de erro e sucesso na mesma forma para que uma consulta cubra ambos.
Exemplo de Trabalho
import json
import logging
import time
import uuid
import anthropic
logging.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_text
def 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_entryaplicando um esquema, chamada tanto do caminho de sucesso quanto do de erro. - Descartar campos
Noneevita que linhas de log de erro carreguem campos deresponse/token vazios enganosos. request_idgerado antes da chamada, para que exista mesmo quando a chamada falha.cache_read_input_tokenscapturado comgetattr, 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_countadicionado, mostrando que o esquema se estende em vez de ser substituído por tipo de chamada.
Mergulho Profundo
Como Funciona
- 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 dejson.dumpsem 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ãoprint) é 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
statusos distinguindo, para que uma única consulta comostatus:errorou uma agregação sobreinput_tokensfuncione independentemente do resultado.
Referência do Esquema
| Campo | Tipo | Sempre Presente | Notas |
|---|---|---|---|
request_id | string | sim | Gerado antes da chamada, une-se a spans de trace |
model | string | sim | O nome do modelo usado para a solicitação |
prompt | string | sim | Texto completo do prompt enviado |
response | string | apenas em caso de sucesso | Texto completo da resposta |
input_tokens / output_tokens | int | apenas em caso de sucesso | De response.usage |
cache_read_input_tokens | int | apenas quando o cache se aplica | Presente uma vez que o cache de prompt está em uso |
latency_ms | float | sim | Tempo de relógio para a chamada |
status | string | sim | "ok" ou "error" |
error_type / status_code | string / int | apenas em caso de erro | Da exceção capturada |
Notas Python
- Prefira
logging.Loggerem vez deprint, 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 subclasselogging.Formatterque codifica em JSON todo oLogRecordevita serialização dupla e mantém os rastros de pilha intactos em erros. - Redija ou trunque os campos
prompt/responseantes 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.
Armadilhas
- Construir o dicionário de log inline em cada local de chamada. Sem uma função
log_entrycompartilhada, 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
usageantes de verificar uma exceção. Se a chamada falhar,responsenunca é atribuído, e referenciarresponse.usageem um blocofinallycausa um erroNameError. Correção: calcule os campos de token apenas dentro do ramo de sucesso e registreNonepara eles no caminho de erro. cache_read_input_tokensausente 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 comgetattr(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 delogging.Logger, mesmo para um script rápido.
Alternativas
| Alternativa | Usar Quando | Não Usar Quando |
|---|---|---|
Instruções print ad hoc | Um script de depuração único que você excluirá em uma hora | Qualquer código que será executado mais de uma vez ou lido por um colega de equipe |
Uma biblioteca dedicada de log estruturado (structlog) | Você deseja vinculação de contexto automática e menos boilerplate JSON em escala | Um script pequeno onde a dependência adicionada não vale a pena |
| Atributos de span OpenTelemetry em vez de logs | Você só precisa de metadados curtos e consultáveis (tokens, modelo, latência) | Você precisa que o texto completo do prompt/resposta seja preservado, o que os spans não devem carregar |
| Uma plataforma de observabilidade de LLM gerenciada SDK | Você deseja captura de prompt/resposta com uma interface de usuário pronta e configuração mínima | Você precisa de controle total sobre o esquema ou deve evitar o envio de conteúdo de prompt para terceiros |
FAQs
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_msestatus.- 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)
raiseNã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.
Relacionado
- Noções Básicas de Observabilidade - a versão menor e de função única deste padrão.
- Como a Observabilidade Funciona para Aplicações LLM - onde o registro estruturado se encaixa em relação ao rastreamento e alertas.
- Instrumentando Loops de Agente com Rastreamento OpenTelemetry - unindo essas linhas de log a spans de trace via ID de solicitação.
- Integrando Dashboards de Uso e Custo Claude com Datadog - enviando os campos de token deste esquema para um dashboard de custos.
- Melhores Práticas de Observabilidade - uma lista de verificação consolidada nesta seção.
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
anthropicPython (ú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.