DevOps

Alertas Inteligentes e Cultura de Resposta a Incidentes Já leu

16 min de leitura

Alertas Inteligentes e Cultura de Resposta a Incidentes
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íti

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:

  1. Identificar o commit que introduziu o problema: bash git log --oneline --since="2 hours ago"
  2. Executar rollback: bash gh workflow run rollback.yml \ -f versao=<SHA_ANTERIOR> \ -f ambiente=production

Se o problema for sobrecarga do banco de dados:

  1. Reiniciar o pool de conexões (sem downtime): bash kubectl rollout restart deployment/minha-api
  2. 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:

  1. Verificar status dos pods: bash kubectl get pods -l app=minha-api kubectl describe pod <pod-com-problema>
  2. 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

  1. Ausência de testes de performance no pipeline: O pipeline de CI executa testes funcionais mas não detecta regressões de performance.

  2. Falta de análise de queries no processo de PR: Não há checklist ou ferramenta que verifique queries SQL em PRs.

  3. 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.

  4. 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.

Comentários

Mais em DevOps

Introdução ao Kubernetes: Orquestrando Containers em Escala
Introdução ao Kubernetes: Orquestrando Containers em Escala

O Docker resolve o problema de empacotar e executar uma aplicação em um único...

Resiliência e Chaos Engineering
Resiliência e Chaos Engineering

Todo engenheiro que opera sistemas em produção por tempo suficiente aprende u...

Docker Compose: Orquestrando Múltiplos Serviços Localmente
Docker Compose: Orquestrando Múltiplos Serviços Localmente

O artigo anterior terminou com um conjunto de comandos&nbsp;docker run que co...