Existe um paradoxo bem documentado em times de operações: quanto mais alertas existem, menos atenção cada um recebe. Um sistema que envia cinquenta notificações por dia treina a equipe a ignorá-las. Quando o alerta crítico chega, ele se perde no ruído.
Esse fenômeno tem nome — alert fatigue — e é uma das causas mais comuns de incidentes graves que poderiam ter sido detectados e resolvidos mais cedo. A equipe viu o alerta, mas o tratou como mais um falso positivo em uma fila interminável.
A solução não é ter menos alertas a qualquer custo — é ter alertas de alta qualidade. Um alerta de alta qualidade tem três características: é acionável (existe algo concreto a fazer quando ele chega), é preciso (dispara quando há realmente um problema, não antes), e é contextualizado (chega com informação suficiente para que o receptor saiba por onde começar).
Este artigo cobre como construir esse sistema — desde a filosofia de alertas até a cultura de resposta a incidentes que transforma cada problema em aprendizado.
Os Quatro Sinais de Ouro
O Google SRE Book define quatro sinais que, juntos, cobrem a saúde de qualquer serviço que atende requisições de usuários. Qualquer sistema de alertas eficaz começa por monitorar esses quatro sinais antes de qualquer outra métrica.
Latência — o tempo que leva para atender uma requisição. É importante distinguir latência de requisições bem-sucedidas da latência de requisições com erro — um erro rápido não é necessariamente bom. O percentil 99 é mais relevante que a média, porque a média esconde os usuários com pior experiência.
Tráfego — a demanda atual sobre o sistema. Para uma API HTTP, são requisições por segundo. Para um sistema de streaming, são bytes por segundo. Para um banco de dados, são transações por segundo. O tráfego contextualiza todos os outros sinais — uma taxa de erros de 1% com tráfego de 10 req/s é muito diferente de 1% com tráfego de 10.000 req/s.
Erros — a taxa de requisições que falham. Inclui tanto erros explícitos — HTTP 5xx — quanto erros implícitos — HTTP 200 com conteúdo incorreto ou incompleto. A definição de "erro" deve ser estabelecida em termos do que representa uma falha para o usuário, não apenas do que o servidor considera falha.
Saturação — quão "cheio" está o serviço. Mede a utilização dos recursos mais constrangidos — CPU, memória, disco, conexões de banco de dados, threads de worker. A saturação é um indicador preditivo: um serviço pode estar funcionando bem agora mas prestes a degradar porque seus recursos estão quase esgotados.
Estrutura de um Alerta Eficaz
Cada alerta deve ser construído como uma unidade completa de informação, não como um trigger que obriga o receptor a ir buscar contexto em outro lugar:
# observabilidade/prometheus/rules/alertas-gold-signals.yml
groups:
- name: sinais-de-ouro
rules:
# ── Latência ─────────────────────────────────────
- alert: LatenciaAltaP99
expr: |
histogram_quantile(0.99,
sum by (job, le) (
rate(http_request_duration_seconds_bucket{job="minha-api"}[5m])
)
) > 1.0
for: 5m
labels:
severity: warning
sinal: latencia
team: backend
annotations:
summary: "Latência P99 acima de 1s em {{ $labels.job }}"
description: |
A latência P99 está em {{ $value | humanizeDuration }}.
Threshold: 1 segundo por mais de 5 minutos consecutivos.
Possíveis causas:
- Queries lentas no banco de dados
- Serviço externo com alta latência
- GC pause no processo Node.js
- Conexões de banco esgotadas
grafana_url: "https://grafana.empresa.com/d/minha-api-red"
runbook_url: "https://wiki.empresa.com/runbooks/latencia-alta"
# ── Erros ─────────────────────────────────────────
- alert: TaxaDeErrosCritica
expr: |
(
sum(rate(http_requests_total{job="minha-api",status=~"5.."}[5m]))
/
sum(rate(http_requests_total{job="minha-api"}[5m]))
) > 0.01
for: 2m
labels:
severity: critical
sinal: erros
team: backend
annotations:
summary: "Taxa de erros 5xx acima de 1% em {{ $labels.job }}"
description: |
Taxa de erros: {{ $value | humanizePercentage }}
Threshold: 1% por mais de 2 minutos.
Impacto estimado: {{ with query "sum(rate(http_requests_total{job='minha-api'}[5m]))" }}
{{ . | first | value | humanize }} usuários/segundo afetados{{ end }}.
Verificar:
1. Logs de erro recentes: https://grafana.empresa.com/explore?...
2. Último deploy: https://github.com/empresa/repo/deployments
3. Status do banco de dados
grafana_url: "https://grafana.empresa.com/d/minha-api-red"
runbook_url: "https://wiki.empresa.com/runbooks/taxa-erros-critica"
# ── Saturação — conexões de banco ─────────────────
- alert: ConexoesBancoDeDadosEsgotando
expr: |
(
pg_stat_activity_count
/
pg_settings_max_connections
) > 0.80
for: 5m
labels:
severity: warning
sinal: saturacao
team: backend
annotations:
summary: "Pool de conexões do PostgreSQL acima de 80%"
description: |
Conexões em uso: {{ $value | humanizePercentage }} do máximo.
Em {{ with query "pg_settings_max_connections" }}{{ . | first | value | humanize }}{{ end }} conexões máximas,
{{ with query "pg_stat_activity_count" }}{{ . | first | value | humanize }}{{ end }} estão em uso.
Ações imediatas:
1. Verificar queries longas: SELECT * FROM pg_stat_activity WHERE state = 'active'
2. Verificar se há connection leaks na aplicação
3. Considerar aumentar max_connections ou usar PgBouncer
runbook_url: "https://wiki.empresa.com/runbooks/conexoes-banco"
# ── Tráfego — queda abrupta ────────────────────────
# Uma queda de tráfego pode indicar que o serviço parou de receber
# requisições — tão preocupante quanto um aumento
- alert: TrafegoBaixoAnomalo
expr: |
(
sum(rate(http_requests_total{job="minha-api"}[5m]))
<
sum(rate(http_requests_total{job="minha-api"}[5m] offset 1d)) * 0.3
)
and
sum(rate(http_requests_total{job="minha-api"}[5m] offset 1d)) > 1
for: 10m
labels:
severity: warning
sinal: trafego
team: backend
annotations:
summary: "Queda abrupta de tráfego em {{ $labels.job }}"
description: |
O tráfego atual está 70% abaixo do mesmo período de ontem.
Tráfego atual: {{ $value | humanize }} req/s
Possíveis causas: falha no load balancer, DNS, ou upstream.
Runbooks: O Manual de Resposta
Um runbook é um documento que descreve como responder a um tipo específico de incidente. Quando um alerta chega às 3h da manhã, o engenheiro de plantão não deve precisar de experiência prévia com aquele tipo de problema para responder adequadamente — o runbook deve guiá-lo passo a passo.
Estrutura de um runbook eficaz:
# Runbook: Taxa de Erros 5xx Alta
**Alerta:** TaxaDeErrosCritica
**Severidade:** Critical
**Última atualização:** 2025-03-10
**Proprietário:** Time Backend
---
## Impacto
Taxa de erros acima de 1% significa que pelo menos 1 em cada 100
requisições está falhando. Com o tráfego atual de ~500 req/s, isso
representa ~5 usuários por segundo recebendo erros.
---
## Diagnóstico Rápido (< 5 minutos)
### 1. Verificar se o serviço está de pé
```bash
curl -s https://api.empresa.com/health | jq .
Se retornar 200: o serviço está respondendo — problema provavelmente em uma rota específica.
Se não retornar: executar o passo 4 diretamente.
2. Identificar quais rotas estão errando
Abrir o Grafana → Dashboard RED → painel "Erros por Endpoint".
Ou via PromQL:
topk(10, sum by (route, status) (
rate(http_requests_total{job="minha-api",status=~"5.."}[5m])
))
3. Verificar os logs de erro
# Últimos 100 erros da aplicação
kubectl logs -l app=minha-api --tail=100 | grep '"level":"error"' | jq .
# Ou via Loki (LogQL):
# {service="minha-api"} | json | level="error" | line_format "{{.msg}} {{.err}}"
4. Verificar o status do banco de dados
# Verificar se o banco está acessível
psql $DATABASE_URL -c "SELECT 1"
# Verificar queries lentas
psql $DATABASE_URL -c "
SELECT pid, now() - pg_stat_activity.query_start AS duracao,
query, state
FROM pg_stat_activity
WHERE state != 'idle' AND query_start < now() - interval '30 seconds'
ORDER BY duracao DESC;"
Ações de Mitigação
Se o problema for uma rota específica com um bug recente:
- Identificar o commit que introduziu o problema:
bash git log --oneline --since="2 hours ago" - Executar rollback:
bash gh workflow run rollback.yml \ -f versao=<SHA_ANTERIOR> \ -f ambiente=production
Se o problema for sobrecarga do banco de dados:
- Reiniciar o pool de conexões (sem downtime):
bash kubectl rollout restart deployment/minha-api - Se as queries lentas persistirem, escalar o banco:
bash aws rds modify-db-instance \ --db-instance-identifier producao-db \ --db-instance-class db.r6g.2xlarge \ --apply-immediately
Se o serviço não estiver respondendo:
- Verificar status dos pods:
bash kubectl get pods -l app=minha-api kubectl describe pod <pod-com-problema> - Verificar eventos recentes do cluster:
bash kubectl get events --sort-by='.lastTimestamp' | tail -20
Escalonamento
Se o problema não for resolvido em 15 minutos: - Primeiro escalonamento: Líder técnico do time backend - Segundo escalonamento: CTO
Contatos de emergência: ver página de plantão no PagerDuty.
Resolução e Fechamento
Ao resolver o incidente: 1. Confirmar que a taxa de erros voltou ao normal no Grafana 2. Documentar a causa raiz no canal #incidentes do Slack 3. Criar issue para o postmortem se o incidente durou > 15 minutos
---
## Conduzindo Postmortems Sem Culpa
Um **postmortem** é uma análise retrospectiva de um incidente com o objetivo de entender o que aconteceu, por que aconteceu e como evitar que aconteça novamente. A palavra "postmortem" pode soar pesada, mas o processo deve ser construtivo — focado no sistema, não nas pessoas.
O princípio fundamental é a **ausência de culpa**: em sistemas complexos, incidentes são o resultado de múltiplos fatores sistêmicos, não de uma única pessoa que "cometeu um erro". A pergunta não é "quem errou?" mas "o que no nosso sistema tornou esse erro possível ou provável?"
**Estrutura de um postmortem:**
```markdown
# Postmortem: Indisponibilidade da API de Checkout — 2025-03-10
**Duração:** 47 minutos (15h43 – 16h30)
**Severidade:** SEV1 — serviço crítico indisponível
**Status:** Resolvido
**Autores:** João Silva, Ana Costa
---
## Resumo Executivo
A API de checkout ficou indisponível por 47 minutos após um deploy
que introduziu uma query PostgreSQL sem índice em um endpoint de
alto tráfego. O aumento de latência nas queries consumiu todas as
conexões disponíveis do pool, tornando o serviço incapaz de atender
novas requisições.
---
## Linha do Tempo
| Horário | Evento |
|---------|--------|
| 15h30 | Deploy da versão 1.8.3 concluído em produção |
| 15h43 | Primeiro alerta de latência alta disparado |
| 15h45 | Engenheiro de plantão começa investigação |
| 15h52 | Causa raiz identificada: query sem índice |
| 16h10 | Índice criado no banco de dados em produção |
| 16h20 | Pool de conexões recuperado gradualmente |
| 16h30 | Serviço normalizado, alerta resolvido |
---
## Causa Raiz
O endpoint `GET /api/checkout/historico` introduzido na versão 1.8.3
executava a seguinte query:
```sql
SELECT * FROM pedidos WHERE usuario_id = $1 ORDER BY criado_em DESC;
A coluna usuario_id não tinha índice na tabela pedidos (4,2 milhões de registros). Cada chamada ao endpoint resultava em um full table scan de ~800ms. Com o aumento de tráfego pós-deploy, o pool de 100 conexões foi consumido em menos de 2 minutos.
Fatores Contribuintes
-
Ausência de testes de performance no pipeline: O pipeline de CI executa testes funcionais mas não detecta regressões de performance.
-
Falta de análise de queries no processo de PR: Não há checklist ou ferramenta que verifique queries SQL em PRs.
-
Ambiente de staging com dados reduzidos: O banco de staging tem apenas 500 registros — a query levava 2ms lá, sem indicação do problema em produção.
-
Alerta de latência com janela longa: O alerta disparou apenas 5 minutos após o deploy. Uma janela de 1 minuto teria alertado mais cedo.
O Que Foi Bem
- O runbook foi suficientemente detalhado para guiar o diagnóstico
- A correlação de logs e traces no Grafana reduziu o tempo de identificação da causa raiz
- O rollback estava disponível mas não foi necessário — a criação do índice resolveu sem interrupção adicional
Ações Corretivas
| Ação | Responsável | Prazo | Issue |
|---|---|---|---|
| Adicionar análise de EXPLAIN para queries em PRs | João | 2025-03-17 | #1234 |
| Criar dados realistas no banco de staging | Ana | 2025-03-24 | #1235 |
| Reduzir janela do alerta de latência de 5m para 1m | Pedro | 2025-03-12 | #1236 |
| Adicionar teste de carga no pipeline (k6) | João | 2025-03-31 | #1237 |
| Documentar checklist de queries SQL para revisores | Ana | 2025-03-14 | #1238 |
---
## Automatizando a Criação de Incidentes
Para times que usam ferramentas como PagerDuty ou OpsGenie, o processo de criação de incidentes pode ser automatizado diretamente a partir dos alertas do Alertmanager:
```yaml
# Receptor PagerDuty no alertmanager.yml
receivers:
- name: 'pagerduty-critico'
pagerduty_configs:
- routing_key: '${{ secrets.PAGERDUTY_INTEGRATION_KEY }}'
severity: '{{ (index .Alerts 0).Labels.severity }}'
description: '{{ (index .Alerts 0).Annotations.summary }}'
details:
firing: '{{ .Alerts.Firing | len }}'
environment: '{{ (index .Alerts 0).Labels.environment }}'
runbook: '{{ (index .Alerts 0).Annotations.runbook_url }}'
grafana: '{{ (index .Alerts 0).Annotations.grafana_url }}'
links:
- href: '{{ (index .Alerts 0).Annotations.grafana_url }}'
text: 'Dashboard Grafana'
- href: '{{ (index .Alerts 0).Annotations.runbook_url }}'
text: 'Runbook'
Um bot de Slack que centraliza a comunicação de incidentes:
// scripts/incident-bot.js
// Integra Alertmanager → Slack com criação automática de canal de incidente
const { WebClient } = require('@slack/web-api');
const express = require('express');
const slack = new WebClient(process.env.SLACK_BOT_TOKEN);
const app = express();
app.use(express.json());
// Webhook recebido do Alertmanager
app.post('/webhook/alertmanager', async (req, res) => {
const { alerts, status } = req.body;
for (const alert of alerts) {
if (status === 'firing' && alert.labels.severity === 'critical') {
await criarIncidente(alert);
} else if (status === 'resolved') {
await resolverIncidente(alert);
}
}
res.sendStatus(200);
});
async function criarIncidente(alert) {
const nomeCanal = `incidente-${Date.now()}`;
// Cria canal dedicado para o incidente
const { channel } = await slack.conversations.create({
name: nomeCanal,
is_private: false,
});
// Convida o time de plantão
await slack.conversations.invite({
channel: channel.id,
users: process.env.ONCALL_USER_IDS,
});
// Posta a mensagem inicial estruturada
await slack.chat.postMessage({
channel: channel.id,
blocks: [
{
type: 'header',
text: {
type: 'plain_text',
text: `🚨 INCIDENTE — ${alert.annotations.summary}`,
},
},
{
type: 'section',
fields: [
{ type: 'mrkdwn', text: `*Severidade:*\n${alert.labels.severity}` },
{ type: 'mrkdwn', text: `*Ambiente:*\n${alert.labels.environment}` },
{ type: 'mrkdwn', text: `*Início:*\n${alert.startsAt}` },
{ type: 'mrkdwn', text: `*Time:*\n${alert.labels.team}` },
],
},
{
type: 'section',
text: {
type: 'mrkdwn',
text: `*Descrição:*\n${alert.annotations.description}`,
},
},
{
type: 'actions',
elements: [
{
type: 'button',
text: { type: 'plain_text', text: '📊 Grafana' },
url: alert.annotations.grafana_url,
},
{
type: 'button',
text: { type: 'plain_text', text: '📖 Runbook' },
url: alert.annotations.runbook_url,
style: 'primary',
},
],
},
],
});
// Posta no canal geral de incidentes
await slack.chat.postMessage({
channel: '#incidentes',
text: `🚨 Novo incidente crítico em andamento. Acompanhe em <#${channel.id}>.`,
});
}
Métricas de Maturidade em Observabilidade
Para avaliar a maturidade do sistema de observabilidade de um time, as métricas DORA — já introduzidas no contexto de CI/CD — são complementadas por métricas específicas de operações:
MTTD — Mean Time to Detect. Quanto tempo leva, em média, desde o início de um problema até o alerta ser disparado. Sistemas com boa instrumentação têm MTTD abaixo de 5 minutos.
MTTR — Mean Time to Recover. Quanto tempo leva, em média, desde a detecção do problema até a recuperação do serviço. O MTTR é uma função direta da qualidade dos runbooks, da clareza dos alertas e da experiência da equipe com o sistema.
Alert-to-Action Ratio — que percentual dos alertas disparados resulta em uma ação concreta. Um ratio abaixo de 50% indica excesso de ruído — muitos alertas são ignorados ou descartados como falso positivo.
Postmortem Completion Rate — que percentual de incidentes de alta severidade resulta em um postmortem publicado. Times maduros têm 100% de completion rate para incidentes SEV1 e SEV2.
Encerrando o Módulo 6
Com este artigo encerra-se o tema — Monitoramento e Observabilidade. Foram cobertos os três pilares da observabilidade, a construção de dashboards eficazes no Grafana, o rastreamento distribuído com OpenTelemetry e, neste artigo, a cultura de alertas e resposta a incidentes que transforma dados em ação.
Os próximos artigos entram no território da AWS em profundidade — os serviços gerenciados que permitem construir infraestrutura escalável, resiliente e econômica sem gerenciar servidores diretamente.
Referências para Aprofundamento
Cultura e processos - Google SRE Book — Postmortem Culture — Capítulo do SRE Book do Google sobre a cultura de postmortem sem culpa, disponível gratuitamente online. Define os princípios e o processo adotado pelo Google. - PagerDuty Incident Response Guide — response.pagerduty.com — Guia completo de resposta a incidentes mantido pelo PagerDuty, cobrindo papéis, comunicação e processos de escalonamento. Disponível gratuitamente online.
Alertas e SLOs - Google SRE Workbook — Alerting on SLOs — Capítulo técnico sobre como construir alertas baseados em orçamento de erros e burn rate, disponível gratuitamente. - Alertmanager Configuration — prometheus.io — Referência completa da configuração do Alertmanager, incluindo todos os tipos de receptores e opções de roteamento.
Ferramentas - Rootly — rootly.com — Blog da Rootly com artigos práticos sobre gestão de incidentes, postmortems e cultura SRE. - Firehydrant — firehydrant.com — Blog da FireHydrant com recursos sobre resposta a incidentes e automação de runbooks.