Logging¶
Nesta receita você configura logs JSON estruturados com correlação por request, um arquivo por nível de severidade e um endpoint HTTP para lê-los. O objetivo é que cada linha de log seja parseável, rastreável até a requisição que a originou e inspecionável sem SSH no servidor.
configure_logging instala um handler JSON no logger raiz que emite registros JSON de uma linha carregando o request ID ativo. LogUtils é uma fachada fina que adiciona métodos por nível aceitando **fields estruturados.
from tempest_fastapi_sdk import LogUtils, configure_logging
from tempest_fastapi_sdk.core import get_request_id
from src.db.models import UserModel
def risky() -> None:
"""Blow up, so the log shows a real traceback."""
raise RuntimeError("boom")
user = UserModel(name="Ana", email="ana@example.com")
# Imperativo — chame uma vez durante o bootstrap.
configure_logging(level="INFO", json_output=True)
# Fachada — útil para singletons de serviço.
log = LogUtils("app.users", level="INFO")
log.info("user_created", user_id=str(user.id), email=user.email)
log.warning("login_throttled", ip="1.2.3.4", attempts=5)
try:
risky()
except RuntimeError:
log.exception("risky_failed", op="reconcile") # appends traceback
# Exponha o ID de correlação fora da linha de log, se necessário.
request_id = get_request_id()
Adotando num serviço que já loga em %-style
Os métodos por nível aceitam os posicionais do logging, então call site
existente entra sem reescrita — e continua com interpolação lazy e com
o template estável que a ferramenta de log agrega:
from tempest_fastapi_sdk import LogUtils
log = LogUtils("app.email", level="INFO")
log.info("Email enviado com sucesso para %s", "ana@example.com")
log.error("Falha ao enviar para %s: %s", "bruno@x.com", "timeout")
log.error("Falha ao enviar para %s: %s", "bruno@x.com", "timeout", op="send")
A última linha mostra que os dois estilos convivem: os posicionais montam
a mensagem, e o **fields continua virando chave de topo no JSON.
funcName/lineno apontam para o seu call site, não para dentro da
fachada — o default é stacklevel=2. Quem embrulha o LogUtils numa
camada própria passa stacklevel=3 (ou mais) para atravessar os frames
extras.
Saída JSON (uma linha — formatada aqui para legibilidade):
{
"timestamp": "2026-05-16T20:14:33.412Z",
"level": "INFO",
"logger": "app.users",
"message": "user_created",
"request_id": "d83e4b0c-7c2f-4bd6-aaa1-7d4f6cf5e5e9",
"user_id": "9c1a5b2d-...",
"email": "ana@example.com"
}
O middleware aceita um nome de header customizado (RequestIDMiddleware(app, header_name="X-Correlation-ID")); o mesmo header é ecoado de volta em toda resposta.
Arquivos por nível + 500.log isolado¶
Por padrão, o SDK escreve simultaneamente no stdout E em logs/ (um arquivo JSON por nível). Cada arquivo recebe apenas o seu próprio nível (correspondência exata — um ERROR nunca cai no warning.log), então toda severidade vira um fluxo isolado e fácil de inspecionar com grep.
from tempest_fastapi_sdk import configure_logging
# Defaults — stdout + logs/{debug,info,warning,error,critical,500}.log
configure_logging(level="INFO")
# Customizar diretório
configure_logging(level="INFO", log_dir="/var/log/myapp")
# Desligar arquivos (stdout puro — útil em serverless ou FS read-only)
configure_logging(level="INFO", file_output=False)
# Desligar stdout (sidecar coleta de disco)
configure_logging(level="INFO", stdout=False)
# Teto de crescimento: cada arquivo rotaciona em ~10 MB, guardando 5 gerações
configure_logging(level="INFO", max_bytes=10_000_000, backup_count=5)
# Sem rotação — quando o logrotate do host (ou um sidecar) é o dono da retenção
configure_logging(level="INFO", max_bytes=0)
Os arquivos rotacionam por padrão — e é por isso
FileHandler puro cresce sem teto. Num serviço com uma linha de log por
request, rodando em host de longa duração, o info.log é o que enche o
disco — e disco cheio derruba o serviço e o que mais dividir a
partição. Por isso o default é RotatingFileHandler com
max_bytes=10_000_000 e backup_count=5: ~60 MB por nível, teto duro.
O outro lado deste par já tinha o teto: o make_logs_router limita a
leitura a 20 mil registros por arquivo, adicionado depois que um serviço
com diretório de log em gigabytes respondeu com worker morto. Isto é o lado
de quem escreve.
Os arquivos rotacionados (info.log.1, info.log.2, …) não são lidos
pelo /logs: o endpoint lê os nomes exatos, então ele mostra a janela
corrente. Retenção mais longa é trabalho de coletor.
Não desligue os dois
configure_logging(stdout=False, file_output=False) lança
ValueError — silenciar todos os handlers deixa a aplicação
cega.
Log em arquivo é best-effort — nunca derruba o boot
Se o log_dir não puder ser criado ou seus arquivos não puderem ser
abertos (FS read-only, falta de permissão de escrita, container
endurecido, serverless, CI), o SDK pula os handlers de arquivo,
emite um aviso (no logger quando o stdout está ligado, senão direto no
stderr) e segue rodando só com stdout — em vez de morrer no import com
PermissionError: [Errno 13] ... 'logs'. Para abrir mão do log em
arquivo de forma explícita, passe file_output=False.
O resultado em disco:
logs/
├── debug.log # só registros DEBUG
├── info.log # só registros INFO
├── warning.log # só registros WARNING
├── error.log # só registros ERROR (um 500 também cai aqui)
├── critical.log # só registros CRITICAL
└── 500.log # só erros 500 não tratados (isolado)
Erros 500 são graves — por isso ganham arquivo próprio
O handler catch-all registrado por register_exception_handlers
marca toda exceção não tratada com o extra http_500=True. O
configure_logging(log_dir=...) roteia esses registros para um
500.log dedicado, além do error.log. Assim a falha mais
grave nunca fica soterrada no meio dos outros erros.
Sempre nos logs, nunca no body
O traceback vai para os arquivos/terminal via logging — não para
o corpo da resposta. O body de um 500 é só o envelope genérico
({"detail": "Internal server error", "code": "INTERNAL_SERVER_ERROR"}).
Veja Camada HTTP para os flags log_traceback /
include_traceback.
Arquivos são sempre JSON
Os handlers de arquivo usam o JSONFormatter independente de
json_output, para que o endpoint /logs consiga parseá-los. O
json_output controla apenas o formato do stdout.
No scaffold, o diretório vem de LOG_DIR (padrão "logs"; deixe vazio para desativar o log em arquivo). Adicione logs/ ao .gitignore.
Lendo logs por HTTP — make_logs_router¶
make_logs_router monta GET /logs, que lê os arquivos JSON em disco e devolve um BasePaginationSchema[LogEntrySchema] paginado (mais recentes primeiro).
from fastapi import FastAPI
from tempest_fastapi_sdk import make_logs_router
from src.core.settings import settings
app = FastAPI()
app.include_router(
make_logs_router(log_dir="logs", token_secret=settings.TOKEN_SECRET),
)
Proteja o endpoint em produção
O payload expõe tracebacks e metadados de request. O endpoint é
protegido por um header de segredo compartilhado X-Token via
make_token_dependency. Um TOKEN_SECRET vazio desativa a
checagem (apenas dev) — nunca exponha /logs sem auth em produção.
Exemplos de consulta:
# Últimos 20 registros de todos os níveis
curl -H "X-Token: $TOKEN_SECRET" "http://localhost:8000/logs"
# Só os 500 isolados, página 1, 50 por página
curl -H "X-Token: $TOKEN_SECRET" "http://localhost:8000/logs?source=500&page_size=50"
# Erros mencionando "timeout" numa janela de tempo
curl -H "X-Token: $TOKEN_SECRET" \
"http://localhost:8000/logs?source=error&q=timeout&start=2026-05-31T00:00:00Z"
Parâmetros de query:
| Parâmetro | Valores | Descrição |
|---|---|---|
source |
all (padrão), debug, info, warning, error, critical, 500 |
Qual arquivo ler. all mescla todos os níveis; 500 retorna só os 500 isolados. |
q |
texto | Substring (case-insensitive) na mensagem. |
start / end |
ISO-8601 | Limita os registros a uma janela de tempo. Um valor sem offset (2026-05-31T00:00:00, ou só a data) é lido como UTC. |
page / page_size |
inteiros | Paginação (1-indexada). |
A leitura é limitada por arquivo
Cada request lê os 20 000 registros mais recentes de cada arquivo
selecionado (DEFAULT_MAX_RECORDS_PER_FILE), não o arquivo inteiro. O
endpoint ordena do mais novo pro mais antigo e pagina, então o que ficou de
fora não era alcançável de qualquer forma — e sem o limite um diretório de
log de vários gigabytes ia inteiro pra memória a cada request, derrubando o
worker antes de responder. Ajuste com
make_logs_router(max_records_per_file=...); quando o corte acontece, um
WARNING é logado nomeando o source.
Recap
configure_logging(log_dir=...)→ stdout + um arquivo por nível.- Exatidão por nível: cada arquivo só recebe a sua severidade.
500.logisola erros 500 não tratados (marcadorhttp_500).make_logs_routerserve esses arquivos paginados e autenticados.
Recap¶
configure_loggingescreve JSON estruturado no stdout e emlogs/, um arquivo por nível, cada arquivo com apenas o seu próprio nível.500.logfica isolado de propósito: o arquivo que você abre primeiro no incidente não vem misturado com o resto.- O id da requisição entra em toda linha, então uma reclamação de usuário vira
um
grep— é isso que separa log estruturado de log bonito. make_logs_routermontaGET /logspaginado sobre esses arquivos, mais recentes primeiro, para ler sem acesso ao disco do container.