Prometheus em Kubernetes: Operator, ServiceMonitor e PromQL Avançado na Prática Já leu

Introdução ao Prometheus em Kubernetes Prometheus é uma solução open-source de monitoramento e alertas que se tornou padrão na indústria para ambientes Cloud Native e Kubernetes. Diferentemente de ferramentas legadas que usam polling ou agentes push, Prometheus funciona sob o modelo pull: ele periodicamente conecta nos endpoints de suas aplicações e coleta métricas no formato Prometheus Text Format. Em um cluster Kubernetes, a força real do Prometheus emerge quando você o combina com o Operator e o ServiceMonitor, criando um sistema declarativo, dinâmico e autoexplicativo. O maior diferencial do Prometheus em Kubernetes não é apenas a coleta de métricas, mas sua integração profunda com o modelo declarativo do próprio Kubernetes. Você define ServiceMonitor como um Custom Resource Definition (CRD), e o Prometheus Operator automaticamente sincroniza a configuração sem necessidade de recarregar o Prometheus. Isso elimina o tradicional problema de editar arquivos YAML de scrape configs e fazer rolling restart de pods. Prometheus Operator e ServiceMonitor O que é o Prometheus

Introdução ao Prometheus em Kubernetes

Prometheus é uma solução open-source de monitoramento e alertas que se tornou padrão na indústria para ambientes Cloud Native e Kubernetes. Diferentemente de ferramentas legadas que usam polling ou agentes push, Prometheus funciona sob o modelo pull: ele periodicamente conecta nos endpoints de suas aplicações e coleta métricas no formato Prometheus Text Format. Em um cluster Kubernetes, a força real do Prometheus emerge quando você o combina com o Operator e o ServiceMonitor, criando um sistema declarativo, dinâmico e autoexplicativo.

O maior diferencial do Prometheus em Kubernetes não é apenas a coleta de métricas, mas sua integração profunda com o modelo declarativo do próprio Kubernetes. Você define ServiceMonitor como um Custom Resource Definition (CRD), e o Prometheus Operator automaticamente sincroniza a configuração sem necessidade de recarregar o Prometheus. Isso elimina o tradicional problema de editar arquivos YAML de scrape configs e fazer rolling restart de pods.

Prometheus Operator e ServiceMonitor

O que é o Prometheus Operator?

O Prometheus Operator é um controlador Kubernetes que gerencia a lifecycle completa do Prometheus através de Custom Resources. Em vez de gerenciar ConfigMaps e StatefulSets manualmente, você descreve o estado desejado via CRDs como Prometheus, ServiceMonitor, PrometheusRule e AlertManager. O Operator observa essas definições e automaticamente gera e atualiza os objetos Kubernetes necessários, incluindo o arquivo de configuração do Prometheus.

A instalação do Operator é feita tipicamente via Helm. Aqui está um exemplo prático:

# Adicionar o repositório Prometheus Community
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm repo update

# Instalar o kube-prometheus-stack (que inclui Operator, Prometheus, Grafana, etc)
helm install prometheus prometheus-community/kube-prometheus-stack \
  --namespace monitoring \
  --create-namespace \
  --values values.yaml

Aqui está um arquivo values.yaml mínimo e funcional:

# values.yaml para kube-prometheus-stack
prometheus:
  prometheusSpec:
    retention: 15d
    storageSpec:
      volumeClaimTemplate:
        spec:
          accessModes: ["ReadWriteOnce"]
          resources:
            requests:
              storage: 50Gi
    # Importante: define qual ServiceMonitor o Prometheus vai scrappear
    serviceMonitorSelectorNilUsesHelmValues: false

grafana:
  adminPassword: "admin123"
  persistence:
    enabled: true
    size: 10Gi

alertmanager:
  enabled: true

Entendendo ServiceMonitor

ServiceMonitor é um CRD que funciona como um contrato entre sua aplicação e o Prometheus. Em vez de adicionar endereços IP ou nomes de serviço diretamente na configuração do Prometheus, você cria um ServiceMonitor que diz: "monitore todos os pods com o label app=minha-app na porta 8080 do endpoint /metrics". O Prometheus Operator descobre esses ServiceMonitors e dinamicamente adiciona as configurações de scrape.

A seleção funciona através de label matching. Você define em qual namespace buscar, quais labels de Service selecionar e quais labels de Pod selecionar. Isso é poderoso porque permite que diferentes times criem seus próprios ServiceMonitors sem precisar acessar a configuração central do Prometheus.

Vamos a um exemplo prático. Suponha que você tem uma aplicação Node.js que expõe métricas em localhost:3000/metrics. Primeiro, você cria um Deployment:

# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: api-app
  namespace: production
spec:
  replicas: 2
  selector:
    matchLabels:
      app: api-app
  template:
    metadata:
      labels:
        app: api-app
        version: v1
    spec:
      containers:
      - name: api
        image: myregistry/api-app:1.0.0
        ports:
        - name: metrics
          containerPort: 3000
        livenessProbe:
          httpGet:
            path: /health
            port: 3000
          initialDelaySeconds: 10

Depois, você cria um Service:

# service.yaml
apiVersion: v1
kind: Service
metadata:
  name: api-app
  namespace: production
  labels:
    app: api-app
spec:
  ports:
  - name: metrics
    port: 3000
    targetPort: metrics
    protocol: TCP
  selector:
    app: api-app

E finalmente, o ServiceMonitor que conecta tudo:

# servicemonitor.yaml
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  name: api-app-monitor
  namespace: production
  labels:
    release: prometheus  # Importante: deve corresponder ao seletor do Prometheus
spec:
  selector:
    matchLabels:
      app: api-app
  endpoints:
  - port: metrics
    interval: 30s
    path: /metrics
    scrapeTimeout: 10s

Note o label release: prometheus no ServiceMonitor. Isso é crucial: no spec do Prometheus (via values.yaml do Helm), você configurou serviceMonitorSelector para selecionar ServiceMonitors com esse label. Sem isso, o Prometheus nunca vai descobrir seu ServiceMonitor.

PromQL Avançado

Conceitos Fundamentais de PromQL

PromQL é a linguagem de consulta do Prometheus, e dominar PromQL avançado é o que separa um operador iniciante de um especialista. PromQL trabalha com séries temporais, onde cada métrica é identificada por seu nome e um conjunto de labels. Uma consulta PromQL retorna um conjunto de pontos de dados em um intervalo de tempo.

Antes de mergulhar em construções complexas, é essencial entender os tipos de dados: vetores instantâneos (um valor por série no momento atual), vetores de intervalo (múltiplos valores por série em um período) e escalares (um único número). Funções diferentes operam sobre esses tipos e, frequentemente, o erro em uma consulta PromQL ocorre quando você tenta aplicar uma função esperando um tipo de dado que ela não retorna.

Operadores e Funções Essenciais

A maioria das queries PromQL começa simples: http_requests_total retorna todas as séries com esse nome. Mas conforme você monitora ambientes complexos, você precisa filtrar, agrupar e agregar. Aqui estão as operações mais poderosas:

Filtros: http_requests_total{job="api-app", method="GET"} seleciona apenas requisições GET.

Operadores Aritméticos: rate(http_requests_total[5m]) calcula a taxa de requisições por segundo nos últimos 5 minutos. Este é talvez o operador mais importante em PromQL.

Operadores Booleanos: http_requests_total{status=~"5.."} usa regex para selecionar status 5xx.

Funções de Agregação: sum(rate(http_requests_total[5m])) by (job) soma as taxas agrupadas por job.

Vamos a um exemplo real. Suponha que você quer alertar quando a latência P95 de uma API fica acima de 500ms. Primeiro, sua aplicação precisa exportar um histograma:

# Em sua aplicação (exemplo com Prometheus client Python)
from prometheus_client import Histogram, start_http_server

request_latency = Histogram(
    'http_request_duration_seconds',
    'HTTP request latency in seconds',
    buckets=(0.01, 0.025, 0.05, 0.075, 0.1, 0.25, 0.5, 0.75, 1.0, 2.5, 5.0)
)

@app.route('/api/users')
def get_users():
    with request_latency.time():
        # seu código
        return jsonify(users)

A métrica exportada terá nomes como http_request_duration_seconds_bucket, http_request_duration_seconds_sum e http_request_duration_seconds_count. Para calcular o P95:

histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[5m]))

Essa função calcula o percentil 95 dos latências nos últimos 5 minutos. Mas isso agrega globalmente. Se você quer P95 por endpoint:

histogram_quantile(0.95, rate(http_request_duration_seconds_bucket{job="api-app"}[5m])) by (le)

Não, espera. O by (le) está errado aqui. Você quer agrupar por endpoint, não por bucket label. A query correta é:

histogram_quantile(0.95, sum(rate(http_request_duration_seconds_bucket{job="api-app"}[5m])) by (handler, le))

Isso é PromQL avançado: você primeiro agrega os buckets por handler (nome do endpoint), mantendo o label le (limite de bucket) necessário para a função histogram_quantile funcionar, e depois calcula o percentil.

Operações com Offset e Comparações Temporais

Um padrão poderoso é comparar o comportamento atual com o comportamento passado. Se você quer verificar se o tráfego atual está 50% acima da média dos últimos 7 dias:

rate(http_requests_total[5m]) > (avg_over_time(rate(http_requests_total[5m])[7d:5m]) * 1.5)

Quebra-se assim: avg_over_time(...[7d:5m]) calcula a média de pontos de 5 minutos ao longo de 7 dias. O :5m é essencial — ele diz "use pontos a cada 5 minutos" em vez de recuperar milhões de pontos.

Outra técnica é o offset:

rate(http_requests_total[5m]) - rate(http_requests_total[5m] offset 1h)

Isso compara o tráfego atual com o de 1 hora atrás. Útil para detectar mudanças súbitas.

Subqueries e Funções Complexas

PromQL 2.7+ suporta subqueries, que permitem usar o resultado de uma query como entrada para outra. Por exemplo, para encontrar jobs com mais de 100 requisições por segundo no P99:

topk(3, (histogram_quantile(0.99, sum(rate(http_request_duration_seconds_bucket[5m])) by (job, le))) > 0.5)

Aqui, histogram_quantile é a "subquery" interna, e topk(3, ...) retorna os 3 piores jobs.

Funções de utilidade incluem predict_linear(v range-vector, t scalar) que extrapola uma tendência linear, útil para alertas proativos:

predict_linear(disk_free_bytes[1h], 3600) < 1073741824  # Alerta se disco vai encher em 1 hora

PrometheusRule e Alertas Práticos

Estrutura de PrometheusRule

PrometheusRule é o CRD que define regras de gravação (recording rules) e alertas. Uma recording rule pré-calcula uma expressão PromQL e a armazena como uma nova série temporal, reduzindo a carga de queries custosas. Um alerta define uma condição PromQL e a ação correspondente (enviar para AlertManager).

Aqui está um exemplo completo de PrometheusRule para uma aplicação em produção:

# prometheusrule.yaml
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
  name: api-app-rules
  namespace: production
  labels:
    prometheus: prometheus-stack
spec:
  groups:
  - name: api-app.rules
    interval: 30s
    rules:
    # Recording rule: pré-calcula taxa de requisições por endpoint
    - record: job:http_requests:rate5m
      expr: sum(rate(http_requests_total[5m])) by (job, handler)

    # Alert: taxa de erro acima de 5%
    - alert: HighErrorRate
      expr: |
        (
          sum(rate(http_requests_total{status=~"5.."}[5m])) by (job)
          /
          sum(rate(http_requests_total[5m])) by (job)
        ) > 0.05
      for: 5m
      labels:
        severity: critical
        team: backend
      annotations:
        summary: "High error rate detected for {{ $labels.job }}"
        description: "Error rate is {{ $value | humanizePercentage }} for job {{ $labels.job }}"

    # Alert: latência P95 acima de 500ms
    - alert: HighLatencyP95
      expr: |
        histogram_quantile(0.95, 
          sum(rate(http_request_duration_seconds_bucket[5m])) by (job, le)
        ) > 0.5
      for: 10m
      labels:
        severity: warning
        team: backend
      annotations:
        summary: "High P95 latency for {{ $labels.job }}"
        description: "P95 latency is {{ $value | humanizeDuration }}"

    # Alert: tráfego anormalmente baixo (possível outage)
    - alert: AbnormallyLowTraffic
      expr: |
        rate(http_requests_total[5m]) 
        < (avg_over_time(rate(http_requests_total[5m])[7d:5m]) * 0.5)
      for: 15m
      labels:
        severity: warning
      annotations:
        summary: "Traffic is abnormally low for {{ $labels.job }}"
        description: "Current rate: {{ $value | humanize }}/s"

Note alguns detalhes importantes:

  • for: 5m significa que o alerta só dispara se a condição permanecer verdadeira por 5 minutos. Isso evita alertas fluttuantes causados por picos momentâneos.
  • labels são adicionados ao alerta e usados pelo AlertManager para roteamento e silenciamento.
  • annotations usam templates Golang com $labels e $value para contextualizar o alerta.
  • humanizePercentage e humanizeDuration são funções de formatação que transformam valores brutos em formatos legíveis.

Correlacionando Múltiplas Métricas

Um passo além é correlacionar múltiplas métricas para reduzir false positives. Por exemplo, em vez de alertar por alto uso de CPU simplesmente, você pode alertar apenas se CPU está alta E memória está alta E I/O de disco está elevado:

- alert: SystemUnderStress
  expr: |
    (node_cpu_seconds_total > 0.8)
    and (node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes < 0.2)
    and (rate(node_disk_io_time_seconds_total[5m]) > 0.5)
  for: 10m
  labels:
    severity: critical

Isso reduz significativamente alertas falsos porque é muito mais provável que um nó realmente tenha problemas se múltiplas métricas estão ruins simultaneamente.

Integração Prática: Do Zero ao Monitoramento

Passo a Passo Completo

Vamos criar um cenário real: você tem uma aplicação Python Flask e quer monitore-a com Prometheus em Kubernetes. Começa-se pelos pré-requisitos:

  1. Cluster Kubernetes rodando com kube-prometheus-stack instalado (conforme mostrado antes).
  2. Sua aplicação exportando métricas Prometheus.

Aqui está a aplicação Flask instrumentada:

# app.py
from flask import Flask, jsonify
from prometheus_client import Counter, Histogram, generate_latest, CONTENT_TYPE_LATEST
import time

app = Flask(__name__)

# Métricas
request_count = Counter(
    'http_requests_total',
    'Total HTTP requests',
    ['method', 'endpoint', 'status']
)

request_duration = Histogram(
    'http_request_duration_seconds',
    'HTTP request duration in seconds',
    ['method', 'endpoint'],
    buckets=(0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0)
)

db_query_duration = Histogram(
    'db_query_duration_seconds',
    'Database query duration',
    ['query_type']
)

@app.before_request
def before_request():
    app.request_start_time = time.time()

@app.after_request
def after_request(response):
    duration = time.time() - app.request_start_time
    request_duration.labels(
        method=request.method,
        endpoint=request.path
    ).observe(duration)
    request_count.labels(
        method=request.method,
        endpoint=request.path,
        status=response.status_code
    ).inc()
    return response

@app.route('/health')
def health():
    return jsonify({'status': 'ok'}), 200

@app.route('/api/users')
def get_users():
    # Simular query ao banco
    with db_query_duration.labels(query_type='select').time():
        time.sleep(0.05)
    return jsonify([{'id': 1, 'name': 'Alice'}, {'id': 2, 'name': 'Bob'}]), 200

@app.route('/metrics')
def metrics():
    return generate_latest(), 200, {'Content-Type': CONTENT_TYPE_LATEST}

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=3000)

Dockerfile:

FROM python:3.11-slim

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY app.py .

EXPOSE 3000
CMD ["python", "app.py"]

requirements.txt:

Flask==3.0.0
prometheus-client==0.19.0

Agora, os manifests Kubernetes:

# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: flask-app
  namespace: production
  labels:
    app: flask-app
spec:
  replicas: 3
  selector:
    matchLabels:
      app: flask-app
  template:
    metadata:
      labels:
        app: flask-app
      annotations:
        prometheus.io/scrape: "true"
        prometheus.io/port: "3000"
        prometheus.io/path: "/metrics"
    spec:
      containers:
      - name: app
        image: myregistry/flask-app:1.0.0
        imagePullPolicy: Always
        ports:
        - name: http
          containerPort: 3000
        livenessProbe:
          httpGet:
            path: /health
            port: http
          initialDelaySeconds: 10
          periodSeconds: 10
        readinessProbe:
          httpGet:
            path: /health
            port: http
          initialDelaySeconds: 5
          periodSeconds: 5
        resources:
          requests:
            cpu: 100m
            memory: 128Mi
          limits:
            cpu: 500m
            memory: 512Mi
---
apiVersion: v1
kind: Service
metadata:
  name: flask-app
  namespace: production
  labels:
    app: flask-app
spec:
  type: ClusterIP
  ports:
  - name: http
    port: 3000
    targetPort: http
    protocol: TCP
  selector:
    app: flask-app
---
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  name: flask-app
  namespace: production
  labels:
    release: prometheus
spec:
  selector:
    matchLabels:
      app: flask-app
  endpoints:
  - port: http
    interval: 30s
    path: /metrics
    scrapeTimeout: 10s
---
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
  name: flask-app-alerts
  namespace: production
  labels:
    prometheus: prometheus-stack
spec:
  groups:
  - name: flask-app.rules
    interval: 30s
    rules:
    - record: flask:request_rate:5m
      expr: sum(rate(http_requests_total{job="flask-app"}[5m])) by (endpoint, status)

    - alert: HighRequestErrorRate
      expr: |
        (
          sum(rate(http_requests_total{job="flask-app", status=~"5.."}[5m])) by (endpoint)
          /
          sum(rate(http_requests_total{job="flask-app"}[5m])) by (endpoint)
        ) > 0.1
      for: 5m
      labels:
        severity: warning
        app: flask-app
      annotations:
        summary: "High error rate on {{ $labels.endpoint }}"
        description: "Error rate is {{ $value | humanizePercentage }}"

    - alert: HighP95Latency
      expr: |
        histogram_quantile(0.95,
          sum(rate(http_request_duration_seconds_bucket{job="flask-app"}[5m])) by (endpoint, le)
        ) > 0.5
      for: 10m
      labels:
        severity: warning
        app: flask-app
      annotations:
        summary: "Slow responses on {{ $labels.endpoint }}"
        description: "P95 latency is {{ $value | humanizeDuration }}"

Após aplicar esses manifests com kubectl apply -f, o Prometheus Operator detectará o ServiceMonitor e o PrometheusRule, sincronizará com o Prometheus, e os alertas estarão ativos automaticamente.

Verificação e Debug

Para verificar se tudo está funcionando:

# Verificar se o ServiceMonitor foi descoberto
kubectl get servicemonitor -n production

# Ver targets descobertos no Prometheus
kubectl port-forward -n monitoring svc/prometheus-operated 9090:9090
# Acesso em http://localhost:9090/targets

# Consultar logs do Prometheus Operator
kubectl logs -n monitoring -l app.kubernetes.io/name=prometheus-operator -f

# Testar a aplicação
kubectl port-forward -n production svc/flask-app 3000:3000
curl http://localhost:3000/metrics

Conclusão

Dominar Prometheus em Kubernetes significa entender três pilares: o Prometheus Operator, que torna a configuração declarativa e dinâmica; o ServiceMonitor, que permite descoberta automática e desacoplamento entre aplicações e monitoramento; e PromQL avançado, que transforma dados brutos em insights acionáveis. O verdadeiro valor emerge quando você combina esses três: usar PromQL para escrever queries que capturam o comportamento real de suas aplicações, expressar essas queries como PrometheusRules para automatizar detecção de problemas, e usar ServiceMonitor para garantir que novas aplicações sejam monitoradas sem necessidade de reconfiguração manual. A chave é começar simples, com métricas bem instrumentadas e alertas bem baseados em dados, e iterativamente aumentar a sofisticação conforme você entende os padrões de seu sistema.

Referências


Artigos relacionados