Métricas¶
O SDK oferece dois caminhos complementares de métricas:
- Prometheus de RED/USE para HTTP (
PrometheusMiddleware+make_prometheus_router) — escuta cada request, calculahttp_requests_total+ histograma de latência +http_requests_in_progress, expõe tudo numGET /metricsno formato texto do Prometheus pronto pra ser raspado pelo seu Prometheus / Grafana / Datadog. - Snapshots de sistema sob demanda (
MetricsUtils) — coleta CPU / memória / disco / GPU NVIDIA pra um endpoint custom (debug page interna, /oncall, etc.). Sem export Prometheus integrado — o objetivo é dar a foto instantânea.
Use o #1 em produção sempre. Adicione o #2 quando precisar inspecionar o host onde a app roda.
#1 Prometheus HTTP — extra [prometheus]¶
Instale com [prometheus] (puxa prometheus-client). O middleware mede toda request; o router serve o endpoint de scrape.
# src/api/app.py
from fastapi import FastAPI
from tempest_fastapi_sdk import (
PrometheusMiddleware,
make_prometheus_registry,
make_prometheus_router,
)
def create_app() -> FastAPI:
app = FastAPI(title="my-service")
# Registry isolado por app — evita colisões com outros prometheus-client globais.
registry = make_prometheus_registry()
app.add_middleware(PrometheusMiddleware, registry=registry)
app.include_router(make_prometheus_router(registry=registry))
return app
Pronto. GET /metrics agora devolve algo como:
# HELP http_requests_total Total HTTP requests
# TYPE http_requests_total counter
http_requests_total{method="GET",path="/api/users",status="200"} 142.0
http_requests_total{method="POST",path="/auth/signup",status="201"} 7.0
# HELP http_request_duration_seconds HTTP request latency
# TYPE http_request_duration_seconds histogram
http_request_duration_seconds_bucket{le="0.005",method="GET",path="/api/users"} 89.0
...
http_requests_in_progress{method="GET"} 2.0
Buckets padrão (DEFAULT_LATENCY_BUCKETS) cobrem 5ms → 10s — adequado pra APIs típicas. Sobrescreva com PrometheusMiddleware(registry=..., latency_buckets=(0.001, 0.005, 0.025, 0.1, 0.5, 2, 10)) quando seu workload é mais granular.
Path normalization
O path label usa o template da rota (/api/users/{user_id}), não o path concreto, pra não explodir a cardinalidade com UUIDs únicos. Isso vem do FastAPI/Starlette — você não precisa configurar nada.
Scrape config¶
prometheus.yml:
scrape_configs:
- job_name: my-service
metrics_path: /metrics
static_configs:
- targets: ["my-service:8000"]
Ou em compose:
services:
my-service:
image: ...
ports: ["8000:8000"]
prometheus:
image: prom/prometheus:latest
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
ports: ["9090:9090"]
Métricas de negócio — BusinessMetrics¶
O middleware cobre HTTP (RED). Pras métricas do seu domínio — pedidos,
profundidade de fila, duração de job — use BusinessMetrics: uma fábrica
tipada de Counter / Gauge / Histogram ligada ao mesmo registry,
então tudo sai no mesmo GET /metrics.
import asyncio
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
from tempest_fastapi_sdk import (
BaseRepository,
BusinessMetrics,
make_prometheus_registry,
)
from src.db.models import UserModel
# Num serviço, a sessão real vem de `db.get_session_context()`; aqui, do SQLite.
session = AsyncSession(create_async_engine("sqlite+aiosqlite:///:memory:"))
elapsed = 0.42
repo = BaseRepository(session, model=UserModel)
registry = make_prometheus_registry()
metrics = BusinessMetrics(registry, namespace="shop")
orders = metrics.counter("orders_total", "Pedidos criados", labelnames=["status"])
queue = metrics.gauge("queue_depth", "Itens na fila")
job = metrics.histogram("job_seconds", "Duração do job", buckets=[0.1, 1, 10])
orders.labels(status="paid").inc()
async def main() -> None:
"""Run this example."""
queue.set(await repo.count({"status": "pending"}))
job.observe(elapsed)
asyncio.run(main())
Sai no /metrics como shop_orders_total{status="paid"}, shop_queue_depth,
shop_job_seconds_bucket. Criar duas vezes com o mesmo nome devolve a
mesma métrica (sem Duplicated timeseries em reload/testes).
Sem mágica
Os objetos retornados são os Counter/Gauge/Histogram de verdade do
prometheus_client — .inc() / .set() / .observe() / .labels(...)
funcionam como na doc oficial. O BusinessMetrics só amarra ao registry
e ao namespace, sem esconder nada.
#2 Snapshot de sistema — extra [metrics]¶
MetricsUtils coleta uso de CPU, memória, disco e GPU NVIDIA via psutil + pynvml. Todo método tem variante sync e async (o wrapper async roda o mesmo código via asyncio.to_thread). A amostragem de GPU degrada graciosamente para [] quando pynvml ou os drivers da NVIDIA estão ausentes.
Instale com [metrics].
# src/api/routers/system.py
from typing import Any
from fastapi import APIRouter
from tempest_fastapi_sdk import MetricsUtils
router = APIRouter()
@router.get("/system-metrics")
async def system_metrics() -> dict[str, Any]:
"""JSON snapshot. NOT the Prometheus endpoint — that one is /metrics."""
snapshot = await MetricsUtils.snapshot_async(disk_paths=["/", "/data"])
return snapshot.to_dict()
Não monte no path /metrics
Esse endpoint não é o do Prometheus — montar no mesmo path conflita com o make_prometheus_router quando os dois estão ativos. Use /system-metrics, /admin/sysinfo, ou algum prefixo restrito de oncall.
MetricsUtils.cpu(interval=...) bloqueia o event loop
A chamada sync gasta interval segundos amostrando — o wrapper cpu_async evita o bloqueio rodando em thread. Sempre prefira MetricsUtils.snapshot_async() em handlers.
Coletores individuais¶
import asyncio
from tempest_fastapi_sdk import MetricsUtils
async def main() -> None:
"""Run this example."""
snapshot = await MetricsUtils.snapshot_async(disk_paths=["/"])
print(snapshot.cpu.percent, snapshot.memory.percent)
for disk in snapshot.disks:
print(disk.path, disk.percent)
for gpu in snapshot.gpus:
print(gpu.name, gpu.utilization_percent, gpu.memory_used_bytes)
asyncio.run(main())
Os coletores individuais também estão disponíveis: MetricsUtils.cpu(interval=...), MetricsUtils.memory(), MetricsUtils.disk(path), MetricsUtils.disks(paths), MetricsUtils.gpus() — e suas variantes *_async. Cada um retorna uma dataclass tipada (CPUMetrics, MemoryMetrics, DiskMetrics, GPUMetrics, SystemMetrics) com um helper to_dict() para serialização JSON.
Um disco: use disk_async, não disks_async([path])
As duas variantes tratam erro de caminho de formas opostas, e a diferença aparece justamente num endpoint de métrica:
import asyncio
from tempest_fastapi_sdk import DiskMetrics, MetricsUtils
async def main() -> None:
"""Compare as duas leituras do mesmo caminho inexistente."""
vazio = await MetricsUtils.disks_async(["/mnt/sumiu"])
print(vazio) # [] — o erro virou linha de log, e o payload sai sem o bloco
try:
disco: DiskMetrics = await MetricsUtils.disk_async("/mnt/sumiu")
except FileNotFoundError as exc:
print(f"propagou: {exc}")
asyncio.run(main())
disks_async loga e pula porque a lista é plural — um caminho ruim não
deveria derrubar os outros quatro. Para um caminho só isso vira ausência
silenciosa: o dashboard mostra "sem disco" onde havia falha de permissão ou
mount que sumiu. Quem passa vários caminhos e quer falhar pede
disks(paths, strict=True) (ou disks_async(paths, strict=True)).
Recap¶
- Caminho #1 (
[prometheus]) —PrometheusMiddleware+make_prometheus_routerexpõem séries RED/USE (http_requests_total,http_request_duration_seconds,http_requests_in_progress) numGET /metricspronto pra scrape. É o que você liga em produção. - Caminho #2 (
[metrics]) —MetricsUtilsentrega um snapshot instantâneo de CPU / memória / disco / GPU num endpoint custom. Sem exporter Prometheus — é a foto sob demanda do host.
Próximos passos¶
- Observabilidade — como combinar métricas com request-id, logging estruturado e o
HTTPClienttipado. - Logging — logging estruturado, arquivos por nível e o endpoint
/logs.