DevOps

Grafana: Dashboards e Alertas que Fazem Sentido Já leu

13 min de leitura

Grafana: Dashboards e Alertas que Fazem Sentido
Um dashboard mal projetado é quase tão ruim quanto não ter dashboard. Quando um engenheiro de plantão recebe um alerta às três da manhã e abre o Grafana, ele precisa entender o estado do sistema em segundos — não em minu

Um dashboard mal projetado é quase tão ruim quanto não ter dashboard. Quando um engenheiro de plantão recebe um alerta às três da manhã e abre o Grafana, ele precisa entender o estado do sistema em segundos — não em minutos. Se o dashboard exige interpretação, se os painéis não têm contexto, se os alertas não apontam para onde olhar, o tempo de resposta ao incidente aumenta e a confiança na ferramenta diminui.

Bons dashboards não são feitos de acúmulo de gráficos. São feitos de perguntas respondidas de forma visual. A pergunta vem primeiro — "o sistema está saudável?", "qual serviço está causando lentidão?", "o deploy de hoje afetou a performance?" — e o gráfico é a resposta.

Este artigo aborda como construir dashboards que comunicam, como configurar alertas que chegam ao lugar certo com o contexto certo, e como organizar o Grafana para que seja útil tanto no dia a dia quanto durante incidentes.


Provisionamento Declarativo do Grafana

Em vez de configurar datasources e dashboards manualmente pela interface, o Grafana suporta provisionamento declarativo — arquivos YAML e JSON que definem toda a configuração. Isso permite versionar a observabilidade junto com o código da aplicação.

Provisionando datasources:

# observabilidade/grafana/provisioning/datasources/datasources.yml
apiVersion: 1

datasources:
  - name: Prometheus
    type: prometheus
    access: proxy
    url: http://prometheus:9090
    isDefault: true
    editable: false
    jsonData:
      timeInterval: "15s"
      queryTimeout: "60s"
      httpMethod: POST

  - name: Loki
    type: loki
    access: proxy
    url: http://loki:3100
    editable: false
    jsonData:
      maxLines: 1000
      derivedFields:
        # Cria link automático entre logs e traces a partir do request_id
        - matcherRegex: '"requestId":"([^"]+)"'
          name: RequestID
          url: '/explore?orgId=1&left={"datasource":"Tempo","queries":[{"query":"${__value.raw}"}]}'
          urlDisplayLabel: Ver trace

Provisionando dashboards:

# observabilidade/grafana/provisioning/dashboards/dashboards.yml
apiVersion: 1

providers:
  - name: 'Dashboards do Projeto'
    orgId: 1
    type: file
    disableDeletion: false
    updateIntervalSeconds: 30
    allowUiUpdates: true
    options:
      path: /etc/grafana/provisioning/dashboards
      foldersFromFilesStructure: true

Construindo o Dashboard de Visão Geral

O primeiro dashboard que todo sistema precisa é a visão geral — o painel que responde em segundos se o sistema está saudável. Ele é o destino padrão quando alguém quer saber "está tudo bem?".

O JSON abaixo define um dashboard completo com os quatro painéis essenciais. Salvo em observabilidade/grafana/provisioning/dashboards/visao-geral.json:

{
  "title": "Visão Geral — Minha API",
  "uid": "minha-api-overview",
  "tags": ["minha-api", "overview"],
  "time": { "from": "now-1h", "to": "now" },
  "refresh": "30s",
  "panels": [
    {
      "id": 1,
      "title": "Status do Sistema",
      "type": "stat",
      "gridPos": { "h": 4, "w": 6, "x": 0, "y": 0 },
      "options": {
        "reduceOptions": { "calcs": ["lastNotNull"] },
        "colorMode": "background",
        "graphMode": "none",
        "textMode": "auto"
      },
      "targets": [
        {
          "datasource": "Prometheus",
          "expr": "up{job=\"minha-api\"}",
          "legendFormat": "{{instance}}"
        }
      ],
      "fieldConfig": {
        "defaults": {
          "mappings": [
            { "type": "value", "options": { "0": { "text": "DOWN", "color": "red" } } },
            { "type": "value", "options": { "1": { "text": "UP", "color": "green" } } }
          ]
        }
      }
    },
    {
      "id": 2,
      "title": "Requisições por Segundo",
      "type": "timeseries",
      "gridPos": { "h": 8, "w": 12, "x": 0, "y": 4 },
      "targets": [
        {
          "datasource": "Prometheus",
          "expr": "sum(rate(http_requests_total{job=\"minha-api\"}[5m])) by (status)",
          "legendFormat": "HTTP {{status}}"
        }
      ],
      "fieldConfig": {
        "defaults": {
          "unit": "reqps",
          "custom": { "lineWidth": 2, "fillOpacity": 10 }
        },
        "overrides": [
          {
            "matcher": { "id": "byRegexp", "options": "HTTP 5.*" },
            "properties": [{ "id": "color", "value": { "fixedColor": "red", "mode": "fixed" } }]
          },
          {
            "matcher": { "id": "byRegexp", "options": "HTTP 4.*" },
            "properties": [{ "id": "color", "value": { "fixedColor": "orange", "mode": "fixed" } }]
          },
          {
            "matcher": { "id": "byRegexp", "options": "HTTP 2.*" },
            "properties": [{ "id": "color", "value": { "fixedColor": "green", "mode": "fixed" } }]
          }
        ]
      }
    },
    {
      "id": 3,
      "title": "Latência (P50 / P95 / P99)",
      "type": "timeseries",
      "gridPos": { "h": 8, "w": 12, "x": 12, "y": 4 },
      "targets": [
        {
          "datasource": "Prometheus",
          "expr": "histogram_quantile(0.50, sum(rate(http_request_duration_seconds_bucket{job=\"minha-api\"}[5m])) by (le))",
          "legendFormat": "P50"
        },
        {
          "datasource": "Prometheus",
          "expr": "histogram_quantile(0.95, sum(rate(http_request_duration_seconds_bucket{job=\"minha-api\"}[5m])) by (le))",
          "legendFormat": "P95"
        },
        {
          "datasource": "Prometheus",
          "expr": "histogram_quantile(0.99, sum(rate(http_request_duration_seconds_bucket{job=\"minha-api\"}[5m])) by (le))",
          "legendFormat": "P99"
        }
      ],
      "fieldConfig": {
        "defaults": {
          "unit": "s",
          "custom": { "lineWidth": 2 },
          "thresholds": {
            "mode": "absolute",
            "steps": [
              { "color": "green", "value": null },
              { "color": "yellow", "value": 0.25 },
              { "color": "red", "value": 0.5 }
            ]
          }
        }
      }
    },
    {
      "id": 4,
      "title": "Taxa de Erros (%)",
      "type": "timeseries",
      "gridPos": { "h": 8, "w": 12, "x": 0, "y": 12 },
      "targets": [
        {
          "datasource": "Prometheus",
          "expr": "sum(rate(http_requests_total{job=\"minha-api\",status=~\"5..\"}[5m])) / sum(rate(http_requests_total{job=\"minha-api\"}[5m])) * 100",
          "legendFormat": "Erros 5xx (%)"
        },
        {
          "datasource": "Prometheus",
          "expr": "sum(rate(http_requests_total{job=\"minha-api\",status=~\"4..\"}[5m])) / sum(rate(http_requests_total{job=\"minha-api\"}[5m])) * 100",
          "legendFormat": "Erros 4xx (%)"
        }
      ],
      "fieldConfig": {
        "defaults": {
          "unit": "percent",
          "min": 0,
          "custom": { "lineWidth": 2, "fillOpacity": 15 }
        }
      }
    },
    {
      "id": 5,
      "title": "Uso de Memória do Processo",
      "type": "timeseries",
      "gridPos": { "h": 8, "w": 12, "x": 12, "y": 12 },
      "targets": [
        {
          "datasource": "Prometheus",
          "expr": "process_resident_memory_bytes{job=\"minha-api\"} / 1024 / 1024",
          "legendFormat": "{{instance}} — RSS"
        },
        {
          "datasource": "Prometheus",
          "expr": "nodejs_heap_size_used_bytes{job=\"minha-api\"} / 1024 / 1024",
          "legendFormat": "{{instance}} — Heap Used"
        }
      ],
      "fieldConfig": {
        "defaults": {
          "unit": "decmbytes",
          "custom": { "lineWidth": 2 }
        }
      }
    }
  ]
}

O Dashboard RED: A Metodologia de Monitoramento de Serviços

A metodologia RED — Rate, Errors, Duration — define as três métricas fundamentais para qualquer serviço que processa requisições. Foi popularizada por Tom Wilkie e é complementar aos quatro sinais de ouro do Google SRE.

Rate — quantas requisições por segundo o serviço está processando. É o indicador de tráfego e demanda.

Errors — qual a taxa de requisições que estão falhando. É o indicador mais direto de problemas que afetam usuários.

Duration — quanto tempo as requisições estão levando. É o indicador de performance e experiência do usuário.

Um dashboard RED para a API:

{
  "title": "RED — Minha API",
  "uid": "minha-api-red",
  "panels": [
    {
      "id": 1,
      "title": "R — Requisições por Segundo por Endpoint",
      "type": "timeseries",
      "gridPos": { "h": 8, "w": 24, "x": 0, "y": 0 },
      "targets": [
        {
          "datasource": "Prometheus",
          "expr": "topk(10, sum(rate(http_requests_total{job=\"minha-api\",status=~\"2..\"}[5m])) by (route))",
          "legendFormat": "{{route}}"
        }
      ],
      "fieldConfig": {
        "defaults": { "unit": "reqps" }
      }
    },
    {
      "id": 2,
      "title": "E — Erros por Endpoint (últimos 30 min)",
      "type": "table",
      "gridPos": { "h": 8, "w": 12, "x": 0, "y": 8 },
      "targets": [
        {
          "datasource": "Prometheus",
          "expr": "sort_desc(sum by (route, status) (increase(http_requests_total{job=\"minha-api\",status=~\"[45]..\"}[30m])))",
          "legendFormat": "",
          "instant": true,
          "format": "table"
        }
      ],
      "options": {
        "sortBy": [{ "displayName": "Value", "desc": true }]
      }
    },
    {
      "id": 3,
      "title": "D — Latência P95 por Endpoint (heatmap)",
      "type": "heatmap",
      "gridPos": { "h": 8, "w": 12, "x": 12, "y": 8 },
      "targets": [
        {
          "datasource": "Prometheus",
          "expr": "sum(rate(http_request_duration_seconds_bucket{job=\"minha-api\"}[5m])) by (le)",
          "legendFormat": "{{le}}"
        }
      ],
      "options": {
        "calculate": false,
        "yAxis": { "unit": "s" }
      }
    }
  ]
}

Configurando o Alertmanager

O Prometheus detecta quando uma condição de alerta é satisfeita, mas é o Alertmanager que gerencia o roteamento, agrupamento e envio das notificações. Essa separação é importante: o Alertmanager evita que a mesma notificação seja enviada dezenas de vezes durante um incidente prolongado, e garante que alertas relacionados sejam agrupados em uma única notificação.

# observabilidade/alertmanager/alertmanager.yml
global:
  # Intervalo de resolução — quanto tempo após o alerta ser resolvido
  # antes de enviar a notificação de resolução
  resolve_timeout: 5m

  # Configurações padrão do Slack
  slack_api_url: 'https://hooks.slack.com/services/TOKEN/TOKEN/TOKEN'

# Roteamento de alertas
route:
  # Agrupamento — alertas com as mesmas labels são agrupados
  group_by: ['alertname', 'cluster', 'service']

  # Aguarda este tempo antes de enviar o primeiro alerta do grupo
  group_wait: 30s

  # Aguarda este tempo antes de enviar novos alertas do mesmo grupo
  group_interval: 5m

  # Reenvia o alerta após este intervalo se ainda estiver ativo
  repeat_interval: 4h

  # Receptor padrão
  receiver: 'slack-ops'

  # Rotas específicas por severidade e equipe
  routes:
    # Alertas críticos de produção — alerta imediato + PagerDuty
    - matchers:
        - severity = critical
        - environment = production
      receiver: 'pagerduty-critico'
      group_wait: 0s
      repeat_interval: 1h
      routes:
        # Alertas de banco de dados — rota para equipe de dados
        - matchers:
            - team = dados
          receiver: 'slack-dados'

    # Alertas de warning — apenas Slack
    - matchers:
        - severity = warning
      receiver: 'slack-ops'
      group_wait: 1m
      repeat_interval: 8h

    # Alertas de CI/CD — canal separado
    - matchers:
        - alertname =~ "Pipeline.*"
      receiver: 'slack-cicd'

# Inibição — suprime alertas menos graves quando um grave está ativo
inhibit_rules:
  # Se um alerta critical está ativo, suprime o warning correspondente
  - source_matchers:
      - severity = critical
    target_matchers:
      - severity = warning
    equal: ['alertname', 'instance']

# Receptores
receivers:
  - name: 'slack-ops'
    slack_configs:
      - channel: '#ops-alertas'
        send_resolved: true
        title: '{{ template "slack.title" . }}'
        text: '{{ template "slack.text" . }}'
        color: '{{ if eq .Status "firing" }}{{ if eq (index .Alerts 0).Labels.severity "critical" }}danger{{ else }}warning{{ end }}{{ else }}good{{ end }}'
        actions:
          - type: button
            text: 'Ver no Grafana'
            url: '{{ (index .Alerts 0).Annotations.grafana_url }}'
          - type: button
            text: 'Runbook'
            url: '{{ (index .Alerts 0).Annotations.runbook_url }}'

  - name: 'slack-dados'
    slack_configs:
      - channel: '#time-dados-alertas'
        send_resolved: true
        title: '{{ template "slack.title" . }}'
        text: '{{ template "slack.text" . }}'

  - name: 'slack-cicd'
    slack_configs:
      - channel: '#ci-cd'
        send_resolved: true

  - name: 'pagerduty-critico'
    pagerduty_configs:
      - routing_key: '${{ secrets.PAGERDUTY_KEY }}'
        description: '{{ template "pagerduty.description" . }}'
        severity: '{{ (index .Alerts 0).Labels.severity }}'
        details:
          firing: '{{ .Alerts.Firing | len }}'
          resolved: '{{ .Alerts.Resolved | len }}'
          num_alerting: '{{ .Alerts.Firing | len }}'

# Templates de notificação
templates:
  - '/etc/alertmanager/templates/*.tmpl'

Templates de notificação customizados:

{{/* observabilidade/alertmanager/templates/slack.tmpl */}}

{{ define "slack.title" }}
{{ if eq .Status "firing" }}
  {{ if eq (index .Alerts 0).Labels.severity "critical" }}🚨{{ else }}⚠️{{ end }}
{{ else }}✅{{ end }}
[{{ .Status | toUpper }}] {{ (index .Alerts 0).Labels.alertname }}
{{ end }}

{{ define "slack.text" }}
{{ range .Alerts }}
*Alerta:* {{ .Labels.alertname }}
*Severidade:* {{ .Labels.severity }}
*Ambiente:* {{ .Labels.environment }}
*Instância:* {{ .Labels.instance }}

*Resumo:* {{ .Annotations.summary }}
*Detalhes:* {{ .Annotations.description }}

{{ if .Annotations.runbook_url }}
*Runbook:* {{ .Annotations.runbook_url }}
{{ end }}

*Início:* {{ .StartsAt | since }}
{{ end }}
{{ end }}

Alertas Baseados em SLOs

A abordagem mais madura de alertas não é baseada em thresholds arbitrários — "alerta quando CPU > 80%" — mas em SLOs: Service Level Objectives. Um SLO define um nível de serviço aceitável para o usuário final, e os alertas são disparados quando o sistema está consumindo o orçamento de erros mais rápido do que o aceitável.

Definindo um SLO para a API com 99,9% de disponibilidade:

# observabilidade/prometheus/rules/slos.yml
groups:
  - name: slo-minha-api
    rules:
      # Taxa de sucesso — base do SLO
      - record: job:http_requests_success:rate5m
        expr: |
          sum(rate(http_requests_total{job="minha-api",status!~"5.."}[5m]))
          /
          sum(rate(http_requests_total{job="minha-api"}[5m]))

      # Burn rate — velocidade de consumo do orçamento de erros
      # SLO de 99.9% = 0.1% de orçamento de erros por período
      - record: job:http_requests_error_budget_burn:rate1h
        expr: |
          (1 - job:http_requests_success:rate5m) / (1 - 0.999)

      # Alerta de burn rate alto — 1h e 5min ambos acima do threshold
      # Burn rate > 14.4x significa que o budget mensal será consumido em 2 dias
      - alert: SLOBurnRateCritico
        expr: |
          job:http_requests_error_budget_burn:rate1h > 14.4
          and
          (
            1 - (
              sum(rate(http_requests_total{job="minha-api",status!~"5.."}[5m]))
              /
              sum(rate(http_requests_total{job="minha-api"}[5m]))
            )
          ) / (1 - 0.999) > 14.4
        for: 2m
        labels:
          severity: critical
          slo: disponibilidade-api
        annotations:
          summary: "SLO em risco — burn rate crítico"
          description: |
            O burn rate do orçamento de erros está em {{ $value | humanize }}x
            a taxa aceitável. O orçamento mensal será consumido em menos de 2 dias
            se o ritmo atual continuar.
          runbook_url: "https://wiki.empresa.com/runbooks/slo-burn-rate"

Anotações de Deploy nos Dashboards

Um recurso fundamental para correlacionar deploys com mudanças de comportamento é adicionar anotações ao Grafana no momento de cada deploy. Uma linha vertical aparece no gráfico marcando exatamente quando o deploy aconteceu:

# Script chamado ao final de cada deploy bem-sucedido
# scripts/anotar-deploy.sh

GRAFANA_URL="${GRAFANA_URL:-http://localhost:3001}"
GRAFANA_TOKEN="${GRAFANA_TOKEN}"
VERSAO="${1}"
AMBIENTE="${2:-staging}"

curl -s -X POST "$GRAFANA_URL/api/annotations" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $GRAFANA_TOKEN" \
  -d '{
    "text": "Deploy: '"$VERSAO"'",
    "tags": ["deploy", "'"$AMBIENTE"'"],
    "time": '"$(date +%s%3N)"',
    "timeEnd": '"$(date +%s%3N)"'
  }'

No GitHub Actions, este script é chamado ao final do job de deploy:

- name: Registra anotação de deploy no Grafana
  if: success()
  run: |
    bash scripts/anotar-deploy.sh \
      "${{ github.sha }}" \
      "${{ inputs.ambiente }}"
  env:
    GRAFANA_URL: ${{ secrets.GRAFANA_URL }}
    GRAFANA_TOKEN: ${{ secrets.GRAFANA_API_TOKEN }}

Organizando Dashboards por Audiência

Dashboards diferentes servem a audiências diferentes. Uma estrutura eficiente organiza os dashboards em três níveis:

Nível executivo — um único painel mostrando os SLOs dos sistemas críticos e o status geral. Sem gráficos técnicos. Sem PromQL visível. Apenas "verde" ou "vermelho" para cada sistema.

Nível operacional — os dashboards RED e USE para cada serviço. Destinados ao time de operações e engenheiros de plantão. Devem responder a "onde está o problema?" em menos de 30 segundos.

Nível de diagnóstico — dashboards detalhados por componente. Métricas de banco de dados, uso de conexões, cache hit rate, filas internas. Destinados a quem está investigando a causa raiz de um problema já identificado.

No Grafana, isso é organizado com pastas:

Grafana/
├── 📁 Executivo/
│   └── Status dos SLOs
├── 📁 Operacional/
│   ├── Visão Geral — Minha API
│   ├── RED — Minha API
│   └── Infraestrutura — Servidores
└── 📁 Diagnóstico/
    ├── PostgreSQL — Detalhado
    ├── Redis — Detalhado
    ├── Node.js — Event Loop e GC
    └── Nginx — Conexões e Cache

O Que Vem a Seguir

O próximo artigo apresenta o rastreamento distribuído com OpenTelemetry — o terceiro pilar da observabilidade. Em sistemas com múltiplos serviços, entender o caminho completo de uma requisição — quais serviços foram chamados, quanto tempo cada um levou, onde os erros ocorreram — é impossível apenas com métricas e logs.


Referências para Aprofundamento

Documentação oficial - Grafana Documentation — grafana.com — Documentação completa do Grafana, cobrindo painéis, alertas, datasources e provisionamento declarativo. - Alertmanager Documentation — prometheus.io — Referência completa do Alertmanager, incluindo configuração de rotas, receptores e templates de notificação.

Metodologias de monitoramento - The RED Method — Tom Wilkie — Artigo original de Tom Wilkie sobre a metodologia RED, com exemplos práticos de instrumentação. - Google SRE Book — Alerting on SLOs — Capítulo do SRE Workbook do Google sobre alertas baseados em SLOs e orçamento de erros, disponível gratuitamente online.

Dashboards prontos - Grafana Dashboards — grafana.com — Biblioteca de dashboards públicos prontos para importar, incluindo dashboards para Node.js, PostgreSQL, Nginx, Redis e dezenas de outras tecnologias.

Comentários

Mais em DevOps

Semantic Versioning e Tags de Release
Semantic Versioning e Tags de Release

Imagine receber uma mensagem de um colega dizendo: "o bug está na vers...

Boas Práticas de Imagens: Leveza, Segurança e Camadas
Boas Práticas de Imagens: Leveza, Segurança e Camadas

Nos artigos anteriores foram construídas imagens funcionais. Uma image...

Deploy Automático para Servidores com GitHub Actions
Deploy Automático para Servidores com GitHub Actions

Todo o trabalho construído nos artigos anteriores — testes, build de imagens,...