Prometheus e Grafana na prática: modelo de dados, scrape, PromQL e alertas
Um guia conceitual e prático de monitoração com Prometheus e Grafana: séries temporais, scrape, exporters, PromQL, recording e alerting rules.
Neste artigo
Quando um serviço começa a responder devagar às três da manhã, você não quer estar adivinhando. Quer olhar um gráfico, ver a latência subindo, correlacionar com o consumo de CPU de um pod específico e descobrir que uma fila de mensagens travou. Essa capacidade de perguntar ao sistema "o que estava acontecendo naquele instante" é o coração da monitoração, e a combinação Prometheus + Grafana virou o padrão de fato para respondê-la em ambientes de infraestrutura e DevOps.
O problema é que muita gente instala os dois, importa um dashboard pronto da internet e para por aí, sem entender o modelo de dados que sustenta tudo. Aí, quando precisa escrever uma consulta que ninguém já escreveu, ou quando o Prometheus começa a consumir dezenas de gigabytes de memória sem explicação aparente, falta base para diagnosticar. Este artigo é sobre essa base: como o Prometheus armazena dados, como ele os coleta, como você os interroga com PromQL e como transforma consultas em alertas confiáveis.
A proposta aqui é conceitual e prática ao mesmo tempo. Você vai entender por que o modelo pull existe, por que cardinalidade é o grande vilão da operação, e como escrever consultas que respondem perguntas reais. Não é um tutorial de instalação — é o mapa mental que faz você usar a ferramenta com intenção, e não por imitação.
O modelo de dados: séries temporais com labels#
Tudo no Prometheus é uma série temporal: uma sequência de valores numéricos indexados pelo tempo. Cada ponto é um par (timestamp, valor de ponto flutuante). Mas o que dá poder ao modelo não é o valor em si — é como as séries são identificadas.
Uma série é identificada por um nome de métrica mais um conjunto de labels (rótulos), que são pares chave-valor. Considere esta amostra:
`` http_requests_total{method="POST", handler="/api/checkout", status="500"} 42 ``
O nome da métrica é http_requests_total. Os labels method, handler e status qualificam essa medição específica. O valor 42 é a leitura mais recente. O ponto crucial é este: cada combinação distinta de nome e labels é uma série temporal independente. Se você tem 5 métodos HTTP, 20 handlers e 15 códigos de status possíveis, potencialmente tem até 1.500 séries só nessa métrica. Guarde esse número — vamos voltar a ele quando falarmos de cardinalidade.
Internamente, o Prometheus trata o nome da métrica como apenas mais um label, com a chave especial __name__. Ou seja, aquela amostra é conceitualmente um conjunto de labels só. Essa uniformidade é o que permite a PromQL filtrar e agregar por qualquer dimensão de forma consistente. Você não escolhe entre "consultar por nome" ou "consultar por rótulo" — é sempre a mesma operação de casamento de labels.
Os labels são a fonte da flexibilidade e também do perigo. Eles permitem que você fatie a mesma métrica por instância, por região, por versão da aplicação, por endpoint. Mas cada novo valor de label multiplica o número de séries. Um label bem escolhido tem um conjunto pequeno e estável de valores possíveis (o método HTTP, por exemplo). Um label mal escolhido tem valores ilimitados — como um ID de usuário ou um caminho de URL com parâmetros embutidos — e esse é o caminho mais rápido para derrubar seu Prometheus.
O modelo PULL: por que o Prometheus faz scrape#
Diferente de sistemas em que a aplicação empurra métricas para um coletor central, o Prometheus opera por pull (puxar). Em intervalos regulares — o scrape_interval, tipicamente 15 ou 30 segundos — o servidor faz uma requisição HTTP a cada target (alvo) e lê um endpoint, geralmente /metrics, que devolve o estado atual de todas as métricas em texto plano. Esse ato de coletar é o scrape.
Por que puxar em vez de empurrar? A escolha tem consequências operacionais concretas:
- Descoberta de saúde embutida. Se o scrape falha, o Prometheus sabe imediatamente que o target está inacessível — ele expõe isso na métrica sintética
up, que vale1para sucesso e0para falha. Você ganha um health check de graça, sem nenhuma lógica extra. - O servidor controla a carga. É o Prometheus que decide a frequência de coleta. Uma aplicação sob pressão não pode inundar o sistema de monitoração com um dilúvio de métricas, porque não é ela quem dita o ritmo.
- Configuração centralizada. A lista de quem monitorar vive no Prometheus, não espalhada por cada aplicação. Isso facilita auditar o que está e o que não está sendo observado.
Há exceções legítimas. Jobs curtos (batch, cron) morrem antes que o Prometheus consiga fazer scrape deles. Para esses casos existe o Pushgateway, um intermediário para onde o job empurra suas métricas e de onde o Prometheus depois puxa. É uma exceção deliberada, não a regra — usá-lo para serviços de longa duração é um antipadrão clássico, porque quebra justamente a detecção de saúde que o pull oferece.
Uma configuração de scrape mínima se parece com isto:
```yaml global: scrape_interval: 15s scrape_timeout: 10s
scrape_configs:
- job_name: "node"
static_configs:
- targets:
- "10.0.1.10:9100"
- "10.0.1.11:9100"
labels: env: "producao"
- job_name: "api"
metrics_path: "/metrics" scrape_interval: 30s static_configs:
- targets:
- "api-1.internal:8080"
- "api-2.internal:8080"
```
Cada bloco scrape_config define um job_name, que vira automaticamente o label job em toda série coletada daquele grupo. Repare que definimos targets estáticos, mas em ambientes dinâmicos você raramente escreve IPs à mão.
Exporters, targets e service discovery#
Nem toda aplicação sabe falar o dialeto do Prometheus nativamente. Para isso existem os exporters: processos que traduzem o estado de um sistema para o formato de métricas do Prometheus. O mais conhecido é o node_exporter, que roda em cada máquina e expõe centenas de métricas de sistema operacional — uso de CPU (node_cpu_seconds_total), memória (node_memory_MemAvailable_bytes), disco, rede, carga. Há exporters para praticamente tudo: PostgreSQL, Redis, RabbitMQ, JMX, hardware via SNMP.
Aplicações que você mesmo escreve não precisam de exporter separado — você instrumenta o código com uma biblioteca cliente (para Go, Python, Java, Rust e outras linguagens) que expõe o próprio endpoint /metrics. A aplicação vira o seu próprio target.
Em ambientes estáticos, listar IPs no static_configs funciona. Mas em Kubernetes, onde pods nascem e morrem o tempo todo, endereços fixos são inviáveis. Aí entra o service discovery (descoberta de serviços): o Prometheus consulta uma fonte dinâmica — a API do Kubernetes, o Consul, tags de instâncias na nuvem — e monta a lista de targets automaticamente. Quando um pod novo sobe, ele passa a ser monitorado sem nenhuma edição manual de configuração.
O service discovery vem acompanhado de relabeling, um mecanismo poderoso (e confuso à primeira vista) para reescrever, filtrar e enriquecer labels antes do scrape. É com relabeling que você diz "só monitore pods com a anotação prometheus.io/scrape: true" ou "transforme o nome do namespace em um label". Vale investir tempo entendendo essa etapa, porque é onde a maioria dos problemas de "meu target não aparece" se resolve.
Os quatro tipos de métrica#
O Prometheus define quatro tipos de métrica. Eles não mudam o armazenamento — no fim, tudo é série temporal —, mas expressam intenções diferentes e mudam como você consulta.
- Counter (contador). Um valor que só cresce, reiniciando a zero quando o processo reinicia. Serve para contar coisas cumulativas: requisições atendidas, erros ocorridos, bytes enviados. Você quase nunca olha o valor absoluto de um counter — olha a taxa de crescimento com
rate(). Exemplos:http_requests_total,node_network_receive_bytes_total. A convenção é o sufixo_total. - Gauge (medidor). Um valor que sobe e desce livremente. Serve para estados instantâneos: temperatura, memória em uso, número de conexões ativas, tamanho de uma fila. Aqui o valor absoluto interessa, e faz sentido perguntar o máximo, o mínimo, a média. Exemplos:
node_memory_MemAvailable_bytes,node_load1. - Histogram (histograma). Amostra observações (tipicamente durações ou tamanhos) e as distribui em buckets cumulativos configuráveis. Cada bucket é um counter que conta quantas observações ficaram abaixo de um certo limite. O histograma expõe várias séries: os buckets (
_bucketcom o labelle, de less or equal), a soma total (_sum) e a contagem (_count). É a base para calcular quantis (percentis) do lado do servidor. - Summary (sumário). Parecido com o histograma, mas calcula os quantis do lado do cliente, no momento da coleta. Ele expõe quantis pré-configurados diretamente. A diferença crucial: quantis de summary não são agregáveis entre instâncias — você não pode somar o p99 de dez servidores para achar o p99 do conjunto. Histogramas, por serem baseados em buckets somáveis, permitem essa agregação. Por isso, na dúvida, prefira histogram.
A escolha entre histogram e summary é uma das decisões de instrumentação que mais confunde iniciantes. A regra prática: se você precisa agregar percentis entre múltiplas instâncias (quase sempre precisa), use histogram. Summary só compensa quando você quer um quantil exato de uma única instância e não vai agregar.
PromQL: interrogando as séries#
Toda a força do Prometheus se realiza na PromQL, sua linguagem de consulta. Ela opera sobre séries temporais e devolve, dependendo da expressão, um valor instantâneo por série ou um intervalo de valores ao longo do tempo. Vamos do simples ao útil.
A consulta mais básica é o nome de uma métrica, que devolve o valor mais recente de cada série correspondente:
``promql node_memory_MemAvailable_bytes ``
Você filtra por labels com chaves. O operador = casa exato, != exclui, =~ casa por expressão regular e !~ exclui por regex:
``promql node_cpu_seconds_total{mode="idle", instance=~"10.0.1..*"} ``
rate() e a taxa dos counters#
Aqui está o conceito mais importante da PromQL prática. Um counter cru é quase inútil de olhar — ele só cresce. O que você quer saber é com que velocidade ele cresce. A função rate() calcula a taxa média de crescimento por segundo ao longo de uma janela de tempo:
``promql rate(http_requests_total[5m]) ``
Isso lê "a taxa média de requisições por segundo, calculada sobre os últimos 5 minutos, para cada série". O [5m] é um range selector: em vez de um ponto, ele seleciona todos os pontos dos últimos 5 minutos, e rate() calcula a inclinação sobre eles. A janela precisa conter pelo menos dois pontos de scrape; uma regra segura é usar pelo menos quatro vezes o scrape_interval.
O rate() também lida elegantemente com reinícios: quando o counter volta a zero porque o processo reiniciou, ele detecta a queda e a compensa, em vez de reportar uma taxa negativa absurda. Para alertas que reagem rápido a picos, existe o irate(), que usa só os dois últimos pontos — ótimo para gráficos de alta resolução, ruim para alertas, porque é ruidoso. A recomendação padrão é rate() para alertas e visões estáveis.
Agregação por label#
O segundo pilar da PromQL é a agregação. Operadores como sum, avg, max, min e count colapsam múltiplas séries em menos séries. Sozinhos, eles colapsam tudo em um único valor; com as cláusulas by e without, você controla quais dimensões preservar:
``promql sum by (handler) (rate(http_requests_total[5m])) ``
Essa consulta calcula a taxa por série e depois soma agrupando por handler, descartando todos os outros labels. O resultado é uma série por endpoint, com o total de requisições por segundo de cada um, independente de método ou status. A cláusula by diz quais labels manter; without faz o inverso, dizendo quais descartar. Dominar essas duas cláusulas é dominar 80% da PromQL do dia a dia.
Um exemplo clássico: a proporção de erros. Você divide a taxa de respostas 5xx pela taxa total:
``promql sum by (job) (rate(http_requests_total{status=~"5.."}[5m])) / sum by (job) (rate(http_requests_total[5m])) ``
Repare que a divisão casa séries pelos labels que sobraram após o sum by (job) — as duas metades precisam ter o mesmo conjunto de labels para se alinharem. Esse casamento vetorial é o que dá coerência às operações binárias da PromQL.
Quantis com histogram_quantile#
Para extrair percentis de um histograma, use histogram_quantile() sobre a taxa dos buckets. Percentis de latência são a métrica de experiência mais honesta que existe — a média esconde as caudas, o p99 as revela:
``promql histogram_quantile( 0.99, sum by (le) (rate(http_request_duration_seconds_bucket[5m])) ) ``
Leia de dentro para fora: rate(..._bucket[5m]) dá a taxa de cada bucket, sum by (le) agrega preservando o label le (o limite superior de cada bucket, essencial para o cálculo), e histogram_quantile(0.99, ...) estima o valor abaixo do qual 99% das observações caem. O resultado é uma estimativa — a precisão depende de quão bem escolhidos são os buckets em torno da região de interesse. Buckets mal distribuídos dão percentis imprecisos, então vale calibrá-los conforme a latência real do serviço.
Recording rules e alerting rules#
PromQL é poderosa, mas consultas pesadas rodadas repetidamente — num dashboard que vários engenheiros abrem, por exemplo — custam CPU. As recording rules (regras de gravação) resolvem isso: o Prometheus avalia uma expressão em intervalos regulares e grava o resultado como uma nova série temporal. Consultas caras viram baratas, porque o trabalho já foi feito e persistido.
```yaml groups:
- name: http_agg
interval: 30s rules:
- record: job:http_requests:rate5m
expr: sum by (job) (rate(http_requests_total[5m])) ```
A convenção de nomes nivel:metrica:operacao (aqui job:http_requests:rate5m) sinaliza o nível de agregação, e é uma disciplina que vale seguir. Dashboards e alertas passam a consultar a série pré-computada, respondendo instantaneamente.
As alerting rules (regras de alerta) têm estrutura parecida, mas em vez de gravar uma série, disparam quando uma condição se sustenta por um tempo. O campo for é o que evita alarme falso: a condição precisa ser verdadeira continuamente durante esse período antes que o alerta passe de pending para firing.
```yaml groups:
- name: disponibilidade
rules:
- alert: AltaTaxaDeErro5xx
expr: | sum by (job) (rate(http_requests_total{status=~"5.."}[5m])) / sum by (job) (rate(http_requests_total[5m])) > 0.05 for: 10m labels: severity: critical annotations: summary: "Taxa de erro 5xx acima de 5% no job {{ $labels.job }}" description: "O job {{ $labels.job }} está com {{ $value | humanizePercentage }} de erros há 10 minutos." ```
Esse alerta dispara quando mais de 5% das requisições falham por 10 minutos seguidos. Os labels classificam o alerta (a severidade guiará o roteamento); as annotations são texto para humanos, com templates que injetam valores reais. O for: 10m é sua defesa contra o pânico de um pico transitório — sem ele, cada soluço momentâneo acordaria alguém.
Alertmanager: roteamento, silêncio e deduplicação#
O Prometheus decide quando um alerta dispara, mas não o entrega. Quem faz isso é o Alertmanager, um serviço separado que recebe os alertas em estado firing e cuida do que acontece depois. Essa separação é deliberada: o Prometheus avalia, o Alertmanager notifica.
O Alertmanager resolve três problemas que, se ignorados, transformam alertas em ruído inútil:
- Roteamento. Uma árvore de rotas decide para onde cada alerta vai, com base em seus labels. Alertas
severity: criticaldo time de banco de dados vão para o PagerDuty daquele time; os de severidade menor vão para um canal do Slack. O roteamento é o que garante que o alerta certo chega à pessoa certa. - Agrupamento (grouping). Quando um rack inteiro cai e cinquenta serviços disparam ao mesmo tempo, você não quer cinquenta notificações. O agrupamento junta alertas relacionados numa única mensagem, usando labels comuns. Menos barulho, mais sinal.
- Deduplicação. Se você roda dois Prometheus em alta disponibilidade, ambos mandam o mesmo alerta. O Alertmanager percebe a duplicata e notifica uma vez só.
Além disso, ele oferece silenciamentos (silences): durante uma manutenção planejada, você silencia os alertas correspondentes por um período, casando por labels, para não ser incomodado por algo que você mesmo causou. E oferece inibição (inhibition): um alerta de "datacenter inteiro inacessível" pode suprimir os alertas de "serviço X fora do ar" que são apenas sintoma da mesma causa raiz. Um Alertmanager bem configurado é a diferença entre um time que confia nos seus alertas e um time que aprendeu a ignorá-los.
Grafana: a camada de visualização#
O Prometheus tem uma interface própria de consulta, útil para depurar PromQL, mas ninguém constrói painéis operacionais nela. Essa é a função do Grafana: conectar-se ao Prometheus como um datasource e transformar consultas em dashboards ricos — séries temporais, medidores, tabelas, mapas de calor.
O fluxo é direto. Você registra o Prometheus como datasource, cria um dashboard, adiciona painéis e, em cada painel, escreve uma consulta PromQL. O Grafana executa a consulta contra o Prometheus no intervalo de tempo que você selecionou na tela e desenha o resultado. Um detalhe importante: é o Grafana quem manda o intervalo — quando você navega de "última hora" para "últimos 7 dias", ele reescreve as consultas para pedir a resolução adequada.
O recurso que eleva um dashboard de rígido a reutilizável são as variáveis de template. Em vez de fixar um instance específico numa consulta, você define uma variável $instance cujos valores o Grafana popula dinamicamente — consultando o próprio Prometheus por label_values(up, instance), por exemplo. A consulta do painel vira rate(node_cpu_seconds_total{instance="$instance"}[5m]), e um menu suspenso no topo deixa você trocar de máquina sem tocar em nada. Um único dashboard bem construído com variáveis serve para toda a frota.
Vale reforçar a divisão de responsabilidades: o Grafana não armazena suas métricas. Ele é uma janela sobre os dados que vivem no Prometheus (ou em outra fonte). Toda a lógica de coleta, retenção e alerta baseado em métrica mora do lado do Prometheus. Confundir isso leva a decisões erradas de arquitetura, como esperar que o Grafana resolva um problema de retenção que é, na verdade, do backend de armazenamento.
Cardinalidade: o grande vilão#
Chegamos ao conceito que separa quem opera Prometheus de quem apenas o instala. Cardinalidade é o número total de séries temporais distintas que seu Prometheus mantém. Lembra do cálculo lá do começo — 5 métodos × 20 handlers × 15 status = 1.500 séries de uma métrica só? Multiplique isso por dezenas de métricas e centenas de instâncias e você entende por que a coisa escala rápido.
O Prometheus mantém em memória um índice de todas as séries ativas. Cada série ativa custa memória, e o índice inteiro precisa caber na RAM. Quando a cardinalidade explode — normalmente porque alguém colocou num label um valor de alta cardinalidade — o consumo de memória dispara e o servidor pode ser morto pelo OOM killer. Os culpados clássicos:
- IDs de usuário, sessão ou requisição como label. Cada usuário vira uma série nova. Milhões de usuários, milhões de séries.
- URLs completas com parâmetros de query embutidos, em vez do padrão da rota.
/api/checkouté um bom label;/api/checkout?cart=abc123&t=1699999999é uma bomba. - Timestamps, endereços de e-mail, UUIDs — qualquer coisa com um espaço de valores praticamente ilimitado.
A regra de ouro é simples: labels devem ter um conjunto pequeno, delimitado e estável de valores possíveis. Antes de adicionar um label, pergunte-se quantos valores distintos ele pode assumir ao longo do tempo. Se a resposta é "ilimitado" ou "cresce com o tráfego", esse dado não pertence a um label — pertence a um log, a um trace, a um sistema feito para alta cardinalidade. Métrica é para agregados de baixa cardinalidade, não para identificar eventos individuais. Internalizar essa fronteira previne a maioria dos incidentes de capacidade do Prometheus.
Retenção e armazenamento de longo prazo#
Por padrão, o Prometheus guarda dados localmente por um período limitado — comumente 15 dias. Isso é intencional: ele foi desenhado para ser confiável e autossuficiente para monitoração operacional recente, não para ser um data warehouse histórico. Para a pergunta "por que o alerta disparou nos últimos dias?", o armazenamento local basta.
Mas há perguntas que exigem meses ou anos de dados: planejamento de capacidade, análise de tendências sazonais, relatórios de SLA de longo prazo. Guardar tudo isso num único Prometheus local é impraticável — o disco e a memória não acompanham. É aqui que entram as soluções de long-term storage.
Duas se destacam como conceito, ambas construídas em torno do Prometheus, não como substitutos:
- Thanos. Adiciona uma camada que envia os blocos de dados do Prometheus para armazenamento de objetos (S3, GCS e afins), barato e praticamente infinito. Componentes de consulta permitem interrogar simultaneamente vários Prometheus e o histórico no object storage como se fossem um só, com deduplicação para configurações de alta disponibilidade.
- Mimir. Uma solução de armazenamento horizontalmente escalável que ingere as métricas via remote write do Prometheus e as guarda também em object storage, projetada para altíssima escala — muitos tenants, bilhões de séries.
O que ambos têm em comum é a filosofia: você mantém o Prometheus fazendo o que ele faz bem — scrape, avaliação de regras, alerta local — e delega a durabilidade e a escala de longo prazo a uma camada separada, apoiada em armazenamento de objetos. Não troque o Prometheus por elas; estenda-o com elas quando a necessidade de retenção realmente aparecer.
Fechando o ciclo#
Vale amarrar as peças num fluxo mental único. As aplicações e os exporters expõem métricas num endpoint. O Prometheus faz scrape desses targets — estáticos ou descobertos dinamicamente —, armazena tudo como séries temporais identificadas por nome e labels, e avalia recording rules para pré-computar agregações e alerting rules para vigiar condições. Alertas em disparo vão ao Alertmanager, que roteia, agrupa, deduplica e notifica. O Grafana lê o Prometheus como datasource e transforma PromQL em painéis navegáveis. E, quando a retenção precisa ir além do curto prazo, Thanos ou Mimir estendem o alcance para o armazenamento de objetos.
Se você tirar uma única lição prática daqui, que seja esta: pense em labels antes de instrumentar. A qualidade da sua monitoração — e a sanidade da sua conta de memória — se decide na escolha dos rótulos, muito antes de você escrever a primeira consulta. Métrica é para agregado; identificador individual é para log e trace. Respeite essa fronteira e o Prometheus será um aliado silencioso e confiável. Ignore-a, e você vai passar madrugadas depurando o próprio sistema que deveria estar depurando os outros.
Comece pequeno: instale um node_exporter, faça o scrape, escreva um rate(), monte um painel no Grafana com uma variável de template. Depois adicione um histograma na sua aplicação e extraia um p99 com histogram_quantile(). Cada peça que você entende de dentro para fora é uma peça a menos para adivinhar às três da manhã.