Pular para o conteúdo
19 min de leitura

Prometheus e Grafana na prática: modelo de dados, scrape, PromQL e alertas

Por Equipe Nebular ·

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 vale 1 para sucesso e 0 para 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 (_bucket com o label le, 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: critical do 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ã.

Leituras relacionadas

Nenhum comentário ainda

Seja o primeiro a comentar.

Deixe seu comentário

Entre com sua conta Canverly para comentar. Você pode usar a mesma conta em qualquer site da rede.

Entrar com Canverly