1. Retomando: da stack vazia à primeira fonte de dados
No primeiro artigo desta série, subimos Prometheus, Loki e Grafana com Docker Compose, mas sem nenhuma aplicação real enviando dados — o Prometheus só monitorava a si mesmo. Este artigo resolve isso: vamos entender como o Prometheus efetivamente coleta métricas de uma aplicação (scraping) e construir uma mini aplicação em Python puro, sem nenhum framework, que expõe métricas nesse formato.
2. Pull vs push: o modelo de scraping do Prometheus
Existem duas formas de uma ferramenta de métricas obter dados de uma aplicação:
- Push — a aplicação envia ativamente os valores para um servidor central sempre que quiser (ou em intervalos definidos por ela mesma). StatsD funciona assim.
- Pull (scraping) — a aplicação apenas expõe seus valores atuais em um endpoint HTTP, e é o servidor de métricas que periodicamente visita esse endpoint e lê o estado atual. É o modelo do Prometheus.
O Prometheus escolheu pull deliberadamente, por algumas razões práticas:
-
Descoberta centralizada de falhas — se o Prometheus não consegue fazer scraping de um alvo, ele mesmo já sabe e registra isso (métrica
up == 0), sem depender da aplicação perceber que parou de enviar dados. -
Facilidade de testar manualmente — como o endpoint é só HTTP, dá para abrir
/metricsnum navegador ou comcurle ver exatamente o que será coletado, sem precisar simular o lado do Prometheus. - Controle central do intervalo de coleta — quem decide a frequência é quem consome os dados (o Prometheus), não cada aplicação individualmente.
A exceção fica por conta de jobs de curta duração (um script batch que roda e termina em segundos, sem ficar no ar para ser "scrapeado") — para esses casos existe o Pushgateway, um componente auxiliar que aceita push e fica exposto para o Prometheus fazer pull dele. Para aplicações de vida longa como as desta série (servidores HTTP), o modelo pull padrão é suficiente.
3. O formato de exposição: o que vive em /metrics
Um endpoint /metrics do Prometheus é simplesmente texto plano, em um formato específico chamado exposition format. Cada métrica segue esta estrutura:
# HELP app_requests_total Total de requisições recebidas
# TYPE app_requests_total counter
app_requests_total 42
- A linha
# HELPé uma descrição legível por humanos (opcional, mas boa prática). - A linha
# TYPEdeclara o tipo da métrica —counter,gauge,histogramousummary(o artigo 3 desta série detalha cada tipo). - A linha de dados é
nome_da_metrica{labels_opcionais="valor"} valor.
Nenhuma biblioteca é estritamente necessária para gerar esse texto — é só formatação de string. Isso torna o formato fácil de implementar em qualquer linguagem, mesmo sem um cliente oficial, o que é exatamente o que esta mini aplicação vai fazer.
4. Uma mini app em Python puro com http.server
Usando apenas a biblioteca padrão do Python (módulo http.server, sem Flask, Django ou qualquer outro framework), uma aplicação mínima que conta requisições e expõe essa contagem em /metrics:
# app.py
import time
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
start_time = time.time()
request_count = 0
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
global request_count
if self.path == "/metrics":
uptime = time.time() - start_time
body = (
"# HELP app_requests_total Total de requisicoes recebidas\n"
"# TYPE app_requests_total counter\n"
f"app_requests_total {request_count}\n"
"# HELP app_uptime_seconds Tempo desde que o processo iniciou\n"
"# TYPE app_uptime_seconds gauge\n"
f"app_uptime_seconds {uptime:.2f}\n"
)
self.send_response(200)
self.send_header("Content-Type", "text/plain; version=0.0.4")
self.end_headers()
self.wfile.write(body.encode("utf-8"))
return
request_count += 1
self.send_response(200)
self.send_header("Content-Type", "text/plain")
self.end_headers()
self.wfile.write(b"ola do app em python\n")
def log_message(self, format, *args):
pass # silencia o log padrao do http.server no stdout
if __name__ == "__main__":
server = ThreadingHTTPServer(("0.0.0.0", 8000), Handler)
print("app rodando em http://0.0.0.0:8000")
server.serve_forever()
Rodando localmente:
python3 app.py
Cada requisição a qualquer path diferente de /metrics incrementa o contador app_requests_total — importante notar que a própria requisição ao /metrics não conta como requisição de negócio, propositalmente, para não distorcer a métrica. Testando:
curl http://localhost:8000/
curl http://localhost:8000/
curl http://localhost:8000/metrics
A última chamada deve mostrar app_requests_total 2.
5. Configurando o prometheus.yml para coletar a nova app
Com o endpoint funcionando, o próximo passo é adicionar um job ao prometheus.yml do artigo anterior, apontando para essa aplicação:
# prometheus.yml
global:
scrape_interval: 15s
scrape_configs:
- job_name: "prometheus"
static_configs:
- targets: ["localhost:9090"]
- job_name: "python-app"
static_configs:
- targets: ["python-app:8000"]
E incluir a aplicação no docker-compose.yml, na mesma rede dos outros serviços (por isso o hostname python-app funciona no target acima — é o nome do serviço no Compose):
# docker-compose.yml (trecho adicionado ao do artigo anterior)
services:
python-app:
image: python:3.12-slim
working_dir: /app
volumes:
- ./app.py:/app/app.py:ro
command: ["python3", "app.py"]
ports:
- "8000:8000"
Depois de docker compose up -d, o Prometheus passa a fazer scraping da aplicação a cada 15 segundos (o scrape_interval global). Para confirmar que o alvo está saudável, em http://localhost:9090/targets o job python-app deve aparecer com estado UP.
6. Consultando os dados no Prometheus
Com alguns minutos de coleta (gerando tráfego manualmente com curl em loop, se necessário), já é possível consultar no campo de expressão do Prometheus (http://localhost:9090/graph):
# valor bruto do contador, acumulado desde que o processo subiu
app_requests_total
# taxa de requisições por segundo, calculada sobre a janela de 1 minuto
rate(app_requests_total[1m])
rate() é a função mais usada sobre contadores no PromQL: como um contador só cresce (e zera se o processo reiniciar), o valor bruto não diz muito sozinho — o que importa é a velocidade de crescimento, que é exatamente o que rate() calcula, já lidando corretamente com o caso de reinício do contador.
7. Conclusão e próximos passos
Vimos por que o Prometheus escolhe pull em vez de push, como o formato de exposição em /metrics é apenas texto simples — implementável sem biblioteca alguma —, construímos uma mini aplicação Python que expõe uma métrica real, e configuramos o Prometheus para coletá-la periodicamente. No próximo artigo, a mini aplicação passa a ser em PHP puro, e o foco vai para os três tipos de métrica — counters, gauges e histograms — aplicados a métricas de negócio, não só contagem de requisições.
Imagem de capa: Logo oficial do Prometheus — repositório prometheus/prometheus, licença Apache 2.0 (convertido para PNG via wsrv.nl)
Referências:
Top comments (0)