Ao terminar este artigo você vai ter uma aplicação Flask expondo métricas, um Prometheus coletando e um Grafana pronto para desenhar painéis. Também vai saber escrever as consultas PromQL dos quatro sinais dourados, criar alertas que acordam alguém só quando vale a pena e evitar o erro de cardinalidade que derruba o Prometheus.
O problema real
Dez serviços rodando e a queixa chega pelo chat: "está lento". Sem métricas, ninguém sabe qual serviço, desde quando, nem se é erro ou lentidão. A investigação vira adivinhação. Com métricas, um gráfico mostra que as chamadas a uma API externa estão falhando em 5% e o problema se resolve rápido.
O Prometheus coleta números em intervalos regulares (modelo pull: ele busca as métricas nos seus serviços). O Grafana desenha os gráficos e o Alertmanager envia os alertas.
Os quatro tipos de métrica
Counter: só cresce
Conta eventos: requisições, erros, mensagens processadas. Zera quando o processo reinicia, e o rate() sabe lidar com isso.
Gauge: sobe e desce
Valor instantâneo: memória, conexões abertas, tamanho de fila.
Histogram: distribuição em faixas
Guarda quantas observações caíram em cada faixa (bucket), mais a soma e a contagem. É o tipo certo para latência, porque permite calcular percentis no servidor Prometheus e agregar entre várias instâncias.
Summary: quantis calculados no cliente
No cliente Python, o Summary só expõe contagem e soma, sem quantis. Em outros clientes ele pode expor quantis, mas esses valores não podem ser agregados entre instâncias. Por isso, para latência, use Histogram na maioria dos casos.
Instrumentando uma aplicação
Estrutura do projeto: app.py, prometheus.yml, docker-compose.yml, Dockerfile e requirements.txt (com flask e prometheus-client).
# app.py
import time
from flask import Flask, Response, request
from prometheus_client import (
CONTENT_TYPE_LATEST, Counter, Gauge, Histogram, generate_latest
)
app = Flask(__name__)
REQUISICOES = Counter(
"http_requests_total", "Total de requisições HTTP",
["method", "route", "status"],
)
DURACAO = Histogram(
"http_request_duration_seconds", "Latência das requisições",
["route"],
buckets=(0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5),
)
EM_ANDAMENTO = Gauge("http_requests_in_progress", "Requisições em andamento")
@app.before_request
def antes():
request.inicio = time.perf_counter()
EM_ANDAMENTO.inc()
@app.after_request
def depois(resposta):
# url_rule é o padrão da rota (/users/<id>), não o caminho real.
rota = request.url_rule.rule if request.url_rule else "desconhecida"
DURACAO.labels(rota).observe(time.perf_counter() - request.inicio)
REQUISICOES.labels(request.method, rota, resposta.status_code).inc()
EM_ANDAMENTO.dec()
return resposta
@app.route("/metrics")
def metrics():
return Response(generate_latest(), mimetype=CONTENT_TYPE_LATEST)
@app.route("/users")
def users():
time.sleep(0.05)
return {"users": ["alice", "bob"]}
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5000)
Note que o rótulo usa o padrão da rota e não request.path. Se usasse o caminho real, cada /users/123 viraria uma série nova, que é o problema de cardinalidade tratado adiante.
Com docker-compose.yml:
services:
app:
build: .
ports: ["5000:5000"]
prometheus:
image: prom/prometheus
ports: ["9090:9090"]
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml:ro
- prometheus_data:/prometheus
command:
- --config.file=/etc/prometheus/prometheus.yml
- --storage.tsdb.retention.time=30d
grafana:
image: grafana/grafana
ports: ["3000:3000"]
volumes:
- grafana_data:/var/lib/grafana
volumes:
prometheus_data:
grafana_data:
E prometheus.yml. Dentro do Compose, o alvo é o nome do serviço, não localhost:
global:
scrape_interval: 15s
scrape_configs:
- job_name: app
static_configs:
- targets: ["app:5000"]
Suba com docker compose up -d --build, abra http://localhost:9090/targets e confira se o alvo está UP. O Grafana fica em http://localhost:3000, com login inicial admin e senha admin (ele pede para trocar no primeiro acesso). Adicione o Prometheus como fonte de dados usando a URL http://prometheus:9090. Imagens sem tag usam a versão mais recente, o que é prático para estudar, mas em produção fixe uma versão.
PromQL na prática
Os exemplos usam as métricas definidas acima.
Requisições por segundo, na média dos últimos 5 minutos:
sum(rate(http_requests_total[5m]))
Proporção de erros 5xx:
sum(rate(http_requests_total{status=~"5.."}[5m]))
/
sum(rate(http_requests_total[5m]))
Latência p95 por rota. Agregue os buckets com sum ... by (le) antes do histogram_quantile:
histogram_quantile(
0.95,
sum by (le, route) (rate(http_request_duration_seconds_bucket[5m]))
)
A precisão do percentil depende dos buckets que você definiu. Se quase toda a latência cai num bucket só, o resultado é uma estimativa grosseira, então ajuste as faixas em volta do seu objetivo de latência.
Por que não confiar na média: com latências de 10 ms, 10 ms, 10 ms, 10 ms e 10 segundos, a média é cerca de 2 segundos e não descreve ninguém. Percentis mostram a cauda, que é onde o usuário sofre.
Os quatro sinais dourados
O livro de SRE do Google propõe acompanhar quatro sinais por serviço.
- Latência: o p95 ou p99 da consulta acima, separando requisições com sucesso das com erro.
- Tráfego:
sum(rate(http_requests_total[5m])). - Erros: a proporção de 5xx acima.
- Saturação: o quanto o recurso mais apertado está cheio. Para máquinas, o
node_exporterfornecenode_cpu_seconds_totalenode_memory_MemAvailable_bytes. Para contêineres, o cAdvisor ou o kubelet.
Não existem limiares universais. Defina cada um a partir do que o seu negócio tolera.
Alertas que não viram ruído
Alerte sobre sintomas que o usuário sente, como erros e lentidão, e não sobre causas possíveis, como memória alta. Um alerta de causa dispara com frequência sem que nada esteja errado e as pessoas aprendem a ignorá-lo.
groups:
- name: api
rules:
- alert: TaxaDeErrosAlta
expr: |
sum(rate(http_requests_total{status=~"5.."}[5m]))
/ sum(rate(http_requests_total[5m])) > 0.01
for: 5m
labels:
severity: page
annotations:
summary: "Mais de 1% de erros 5xx nos últimos 5 minutos"
runbook_url: "https://wiki.exemplo.com/runbooks/taxa-de-erros"
- alert: LatenciaP95Alta
expr: |
histogram_quantile(0.95,
sum by (le) (rate(http_request_duration_seconds_bucket[5m]))) > 1
for: 10m
labels:
severity: ticket
annotations:
summary: "p95 acima de 1s por 10 minutos"
A cláusula for evita alertas por picos curtos. O runbook_url diz a quem está de plantão o que fazer, e sem isso o alerta só gera ansiedade. Carregue esse arquivo em rule_files no prometheus.yml e configure o Alertmanager para entregar por e-mail, Slack ou outro canal.
SLI, SLO e SLA
O SLI é a medida, o SLO é a meta interna e o SLA é o contrato com consequência financeira.
Um SLI de disponibilidade simples:
sum(rate(http_requests_total{status!~"5.."}[30d]))
/
sum(rate(http_requests_total[30d]))
Um SLO plausível seria "99,5% das requisições sem erro 5xx em 30 dias". A diferença para 100% é o orçamento de erro: nesse exemplo, 0,5% das requisições podem falhar sem violar a meta. Quando o orçamento acaba, a equipe prioriza confiabilidade em vez de funcionalidades novas. Valores de SLA e multas dependem do contrato de cada empresa.
Consultas de 30 dias são pesadas. O costume é criar uma recording rule que calcule a taxa em janelas curtas e agregue a partir dela.
Cardinalidade
Cada combinação única de valores de rótulos cria uma série temporal, e cada série ocupa memória. Rótulos com muitos valores possíveis, como user_id, e-mail ou ID de requisição, geram milhões de séries e derrubam o servidor.
# Evite: um valor novo por usuário
Counter("requests_by_user_total", "…", ["user_id"])
Se precisa saber o que um usuário fez, isso é trabalho para logs ou traces, não para métricas. Em métricas, mantenha rótulos com poucos valores conhecidos, como método, rota (o padrão, não o caminho) e classe de status. Para saber quantas séries você tem, consulte:
count({__name__=~".+"})
Em instâncias grandes essa consulta é cara, e a página /tsdb-status do Prometheus mostra as métricas com mais séries de forma mais barata. Você também pode limitar com sample_limit na configuração de scrape.
Armadilhas comuns
- Scrape a cada 1 segundo sem necessidade. Os 15s costumam bastar, e intervalos curtos aumentam muito o armazenamento.
- Esquecer a retenção. O padrão é 15 dias, ajuste com
--storage.tsdb.retention.timee dimensione o disco. - Painéis sem contexto. Um gráfico de erros sem a escala de tráfego ao lado engana.
- Alerta sem dono nem runbook.
Quando o Prometheus não é a ferramenta
Para logs use Loki ou Elasticsearch. Para traces distribuídos use Tempo ou Jaeger, de preferência com OpenTelemetry. Para eventos de alta resolução e armazenamento de longo prazo, olhe soluções como Mimir, Thanos ou VictoriaMetrics por cima do Prometheus. E para métricas de produtos com LLM, veja o artigo de observabilidade de LLM.
Próximos passos
Suba o Compose acima, gere carga com hey -z 30s http://localhost:5000/users (ou ab -n 1000 -c 10 ...) e crie no Grafana quatro painéis, um para cada sinal dourado. Para expor métricas do banco, combine com o postgres_exporter e veja o artigo de Postgres para desenvolvedores.
Quer aplicar isso na sua empresa? Marque uma conversa de 45 minutos em https://iauaicloud.com.br/consultoria