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.