Você já automatizou alertas e runbooks, definiu SLIs, SLOs e Error Budgets e validou resiliência com Engenharia de Caos. Mas quando o incidente termina, o que acontece? Na maioria das organizações, nada. O alerta é resolvido e todo mundo segue a vida. Até o mesmo problema voltar.

Sem uma análise pós-incidente decente, a organização repete os mesmos erros. O postmortem blameless transforma falhas em aprendizado e evita que incidentes recorrentes continuem corroendo seu Error Budget.

Aqui, o foco é montar um processo completo de postmortem no Azure: coleta automatizada com KQL, templates estruturados, automação com Logic Apps e integração com Azure DevOps.


Por que postmortems falham na maioria das organizações

Anti-padrãoConsequênciaSintoma visível
Cultura de culpaPessoas escondem informações por medo de puniçãoTimeline do incidente tem lacunas inexplicáveis
Postmortem tardioDetalhes críticos são esquecidos após dias ou semanasDocumento superficial, sem insights acionáveis
Sem action itemsRevisão vira exercício burocrático sem resultadoMesmo incidente se repete em semanas
Action items sem donoTarefas são criadas mas nunca acompanhadasLista de melhorias cresce infinitamente, nada é implementado
Escopo excessivoTentativa de resolver todos os problemas da organizaçãoReunião de 3 horas sem conclusão clara
Foco em “o que deu errado”Ignora fatores sistêmicos e contribuintesCorreção pontual que não endereça a causa raiz
Sem dados concretosAnálise baseada em memória e percepçõesConclusões divergentes entre os participantes

O Google SRE Book define postmortem como “um registro escrito de um incidente, seu impacto, as ações tomadas para mitigá-lo, a(s) causa(s) raiz, e as ações para evitar recorrência”. Mas o aspecto mais importante é a filosofia: blameless, sem culpa.


O que é cultura blameless e por que isso importa

Cultura blameless não significa ausência de responsabilidade. Significa que o foco está no sistema, não no indivíduo. Se uma pessoa cometeu um erro, o sistema deixou esse erro avançar até virar impacto.

Comparação: abordagem tradicional vs. blameless

AspectoAbordagem tradicionalAbordagem blameless
Pergunta central“Quem causou isso?”“O que permitiu que isso acontecesse?”
Foco da análiseAção individualFatores sistêmicos e contribuintes
Resultado esperadoPunição ou advertênciaMelhorias no sistema, processos e ferramentas
Efeito na equipeMedo, ocultação de errosSegurança psicológica, transparência
Timeline do incidenteIncompleta (pessoas omitem detalhes)Completa (pessoas compartilham sem medo)
Probabilidade de recorrênciaAlta (causa raiz não endereçada)Baixa (fatores sistêmicos corrigidos)
Aprendizado organizacionalMínimoMáximo

A segurança psicológica é o alicerce. Quando engenheiros sabem que não serão punidos, contam coisas que normalmente ficariam de fora, inclusive decisões que pareciam razoáveis na hora e depois se mostraram problemáticas. Esse contexto ajuda a evitar novos incidentes.

Os cinco princípios de um postmortem blameless

  1. Assumir boa-fé: ninguém acorda querendo derrubar a produção.
  2. Falhas são aprendizado: cada incidente revela lacunas não visíveis antes.
  3. Foco em fatores contribuintes: incidentes raramente têm uma causa raiz isolada. Normalmente existe uma combinação de fatores.
  4. Ações corretivas sistêmicas: melhorias tornam o sistema mais resiliente, não apenas impedem a repetição exata.
  5. Transparência total: conclusões acessíveis a toda a organização.

Anatomia de um postmortem eficaz

Template baseado nas práticas do Google SRE, adaptado para o ecossistema Azure.

Template completo de postmortem

# Postmortem: [Título descritivo do incidente]

## Metadados
- **Data do incidente**: YYYY-MM-DD
- **Duração**: HH:MM (do início da detecção até a resolução)
- **Severidade**: SEV-1 / SEV-2 / SEV-3 / SEV-4
- **Autor do postmortem**: [Nome]
- **Facilitador da reunião**: [Nome]
- **Data da revisão**: YYYY-MM-DD
- **Status**: Rascunho / Em Revisão / Aprovado

## Resumo executivo
[2-3 parágrafos: o que aconteceu, impacto e resolução.]

## Impacto
- **Usuários afetados**: [número ou percentual]
- **Duração do impacto**: [tempo em que usuários foram afetados]
- **Receita impactada**: [estimativa, se aplicável]
- **SLO afetado**: [qual SLO foi violado e em quanto]
- **Error Budget consumido**: [percentual do budget mensal consumido]
- **Tickets de suporte**: [quantidade gerada]

## Timeline detalhada
| Hora (UTC) | Evento | Fonte |
|------------|--------|-------|
| HH:MM | Primeiro sinal de degradação nos logs | Azure Monitor |
| HH:MM | Alerta disparado | Action Group |
| HH:MM | Engenheiro de plantão acionado | PagerDuty/Opsgenie |
| HH:MM | Início da investigação | - |
| HH:MM | Causa identificada | Log Analytics |
| HH:MM | Ação de mitigação aplicada | - |
| HH:MM | Serviço restaurado | Azure Monitor |
| HH:MM | Confirmação de normalidade | Dashboard SLO |

## Causa raiz e fatores contribuintes
### Causa raiz
[Descrição técnica detalhada da causa raiz]

### Fatores contribuintes
1. [Fator 1]
2. [Fator 2]
3. [Fator 3]

### Diagrama de causa e efeito
[Se aplicável, incluir diagrama de Ishikawa ou árvore de falhas]

## O que funcionou bem
- [Item 1]
- [Item 2]
- [Item 3]

## O que pode ser melhorado
- [Item 1]
- [Item 2]
- [Item 3]

## Onde tivemos sorte
- [Item 1]
- [Item 2]

## Action items
| ID | Ação | Prioridade | Responsável | Prazo | Status |
|----|------|-----------|-------------|-------|--------|
| AI-001 | Ajustar threshold do alerta X | P1 | @fulano | 7 dias | Pendente |
| AI-002 | Criar runbook para cenário Y | P2 | @ciclano | 14 dias | Pendente |
| AI-003 | Adicionar teste de caos para componente Z | P3 | @beltrano | 30 dias | Pendente |

## Lições aprendidas
[Insights que transcendem este incidente específico e se aplicam à organização]

Classificação de severidade

A severidade determina o rigor do postmortem, participantes e prazos.

SeveridadeCritérioPrazo do postmortemParticipantes obrigatórios
SEV-1Indisponibilidade total ou perda de dados48 horas após resoluçãoEngenharia, Gestão, Produto, Suporte
SEV-2Degradação significativa ou funcionalidade crítica indisponível5 dias úteisEngenharia, Gestão
SEV-3Impacto parcial ou degradação moderada10 dias úteisEngenharia
SEV-4Impacto mínimo, detectado internamente15 dias úteisEngenheiros envolvidos

Regra: todo incidente SEV-1 e SEV-2 deve gerar postmortem. SEV-3/SEV-4 são recomendados mas podem ser simplificados.


Coletando dados para o postmortem com Azure Monitor e KQL

Reconstruir a timeline é a parte mais trabalhosa do postmortem. Com Azure Monitor configurado, os dados já estão lá. O trabalho é puxar isso com KQL.

Reconstruindo a timeline de falhas com Application Insights

Identificando quando as falhas começaram e como evoluíram:

// Timeline de falhas por minuto durante o período do incidente
AppRequests
| where TimeGenerated between (datetime("2026-04-15 14:00") .. datetime("2026-04-15 18:00"))
| summarize 
    totalRequests = count(),
    failedRequests = countif(Success == false),
    failureRate = round(100.0 * countif(Success == false) / count(), 2),
    avgDuration = round(avg(DurationMs), 2),
    p95Duration = round(percentile(DurationMs, 95), 2)
  by bin(TimeGenerated, 1m)
| order by TimeGenerated asc
| render timechart

Identificando as exceções que causaram as falhas

Com o período mapeado, identificamos as exceções por trás dos erros:

// Top exceções durante o período do incidente
AppExceptions
| where TimeGenerated between (datetime("2026-04-15 14:00") .. datetime("2026-04-15 18:00"))
| summarize 
    occurrences = count(),
    firstSeen = min(TimeGenerated),
    lastSeen = max(TimeGenerated),
    affectedOperations = dcount(OperationId)
  by ProblemId, ExceptionType = tostring(split(Type, ".")[-1]), OuterMessage
| order by occurrences desc
| take 20

Correlacionando exceções com requests falhos

Cruzando requests falhos com exceções para revelar qual operação foi afetada e por quê:

// Correlação entre requests falhos e exceções
AppRequests
| where TimeGenerated between (datetime("2026-04-15 14:00") .. datetime("2026-04-15 18:00"))
| where Success == false
| join kind=inner (
    AppExceptions
    | where TimeGenerated between (datetime("2026-04-15 14:00") .. datetime("2026-04-15 18:00"))
) on OperationId
| summarize 
    failureCount = count(),
    avgDuration = round(avg(DurationMs), 2)
  by RequestName = Name, ExceptionType = Type, ExceptionMessage = OuterMessage
| order by failureCount desc

Analisando dependências que falharam

Muitos incidentes vêm de dependências externas (bancos, APIs, cache). Esta query identifica quais falharam:

// Dependências com falha durante o incidente
AppDependencies
| where TimeGenerated between (datetime("2026-04-15 14:00") .. datetime("2026-04-15 18:00"))
| where Success == false
| summarize 
    failures = count(),
    avgDuration = round(avg(DurationMs), 2),
    p99Duration = round(percentile(DurationMs, 99), 2),
    firstFailure = min(TimeGenerated),
    lastFailure = max(TimeGenerated)
  by DependencyType = Type, DependencyName = Name, Target, ResultCode
| order by failures desc

Verificando alterações no ambiente (Activity Log)

“Algo mudou antes do incidente?” O Activity Log registra todas as operações de gerenciamento:

// Alterações no ambiente nas 6 horas anteriores ao incidente
AzureActivity
| where TimeGenerated between (datetime("2026-04-15 08:00") .. datetime("2026-04-15 14:30"))
| where OperationNameValue has_any ("write", "delete", "action")
| where ActivityStatusValue == "Success"
| project 
    TimeGenerated,
    Caller,
    OperationNameValue,
    ResourceGroup,
    Resource = tostring(split(_ResourceId, "/")[-1]),
    Properties = ActivitySubstatusValue
| order by TimeGenerated desc

Verificando a saúde dos recursos (Resource Health)

O Resource Health revela problemas de infraestrutura não visíveis nos logs da aplicação:

// Eventos de saúde dos recursos durante o incidente
AzureActivity
| where TimeGenerated between (datetime("2026-04-15 13:00") .. datetime("2026-04-15 19:00"))
| where CategoryValue == "ResourceHealth"
| project 
    TimeGenerated,
    ResourceType = tostring(split(_ResourceId, "/")[-2]),
    Resource = tostring(split(_ResourceId, "/")[-1]),
    Status = ActivityStatusValue,
    Detail = Properties
| order by TimeGenerated asc

Query consolidada: timeline completa do incidente

Esta query unifica eventos de múltiplas fontes em uma visão cronológica:

// Timeline unificada do incidente
let incidentStart = datetime("2026-04-15 14:00");
let incidentEnd = datetime("2026-04-15 18:00");
let lookbackWindow = 2h;
//
// 1. Primeiros sinais de falha (requests)
let requestFailures = AppRequests
| where TimeGenerated between ((incidentStart - lookbackWindow) .. incidentEnd)
| where Success == false
| summarize Count = count() by bin(TimeGenerated, 1m)
| where Count > 5 // threshold de ruído
| extend EventType = "RequestFailure", Detail = strcat(Count, " requests falharam");
//
// 2. Exceções novas
let exceptions = AppExceptions
| where TimeGenerated between ((incidentStart - lookbackWindow) .. incidentEnd)
| summarize Count = count(), Types = make_set(Type) by bin(TimeGenerated, 1m)
| where Count > 0
| extend EventType = "Exception", Detail = strcat(Count, " exceções: ", tostring(Types));
//
// 3. Alterações no ambiente
let changes = AzureActivity
| where TimeGenerated between ((incidentStart - lookbackWindow) .. incidentEnd)
| where OperationNameValue has_any ("write", "delete")
| where ActivityStatusValue == "Success"
| extend EventType = "EnvironmentChange", 
         Detail = strcat(Caller, " executou ", OperationNameValue, " em ", tostring(split(_ResourceId, "/")[-1]));
//
// Alert data requires Azure Resource Graph - query separately if needed
//
// Unificação
requestFailures
| union exceptions
| union changes
| project TimeGenerated, EventType, Detail
| order by TimeGenerated asc

Automatizando a coleta de dados do postmortem com Logic Apps

Podemos automatizar a geração do rascunho com Logic Apps que disparam quando um incidente é resolvido.

Arquitetura da automação

Configurando o trigger no Azure Monitor

Crie um Action Group que dispara quando um alerta é resolvido:

# Criar Action Group para postmortem
az monitor action-group create \
  --resource-group rg-monitoring \
  --name ag-postmortem-trigger \
  --short-name postmortem \
  --action logicapp "PostmortemGenerator" \
    "/subscriptions/{sub-id}/resourceGroups/rg-automation/providers/Microsoft.Logic/workflows/postmortem-generator" \
    "https://prod-XX.brazilsouth.logic.azure.com:443/workflows/{workflow-id}/triggers/manual/paths/invoke?api-version=2016-10-01&sp=%2Ftriggers%2Fmanual%2Frun&sv=1.0&sig={signature}" \
  --use-common-alert-schema true

Criando a Logic App para geração automática

A Logic App executa queries KQL via API do Log Analytics e monta o rascunho:

{
  "definition": {
    "$schema": "https://schema.management.azure.com/providers/Microsoft.Logic/schemas/2016-06-01/workflowdefinition.json#",
    "triggers": {
      "Quando_alerta_resolvido": {
        "type": "Request",
        "kind": "Http",
        "inputs": {
          "schema": {
            "type": "object",
            "properties": {
              "data": {
                "type": "object",
                "properties": {
                  "essentials": {
                    "type": "object",
                    "properties": {
                      "alertId": { "type": "string" },
                      "alertRule": { "type": "string" },
                      "severity": { "type": "string" },
                      "monitorCondition": { "type": "string" },
                      "firedDateTime": { "type": "string" },
                      "resolvedDateTime": { "type": "string" }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "actions": {
      "Consultar_Log_Analytics": {
        "type": "ApiConnection",
        "inputs": {
          "host": {
            "connection": {
              "name": "@parameters('$connections')['azuremonitorlogs']['connectionId']"
            }
          },
          "method": "post",
          "path": "/queryData",
          "body": {
            "query": "AppRequests | where TimeGenerated between (datetime('@{triggerBody()?['data']?['essentials']?['firedDateTime']}') .. datetime('@{triggerBody()?['data']?['essentials']?['resolvedDateTime']}')) | where Success == false | summarize failedCount=count(), avgDuration=avg(DurationMs) by bin(TimeGenerated, 5m), Name | order by TimeGenerated asc",
            "workspaces": [
              "<workspace-id>"
            ],
            "timeRange": {
              "start": "@triggerBody()?['data']?['essentials']?['firedDateTime']",
              "end": "@triggerBody()?['data']?['essentials']?['resolvedDateTime']"
            }
          }
        }
      },
      "Criar_Work_Item_DevOps": {
        "type": "ApiConnection",
        "runAfter": { "Consultar_Log_Analytics": ["Succeeded"] },
        "inputs": {
          "host": {
            "connection": {
              "name": "@parameters('$connections')['visualstudioteamservices']['connectionId']"
            }
          },
          "method": "post",
          "path": "/{project}/_apis/wit/workitems/$Bug",
          "body": [
            {
              "op": "add",
              "path": "/fields/System.Title",
              "value": "Postmortem: @{triggerBody()?['data']?['essentials']?['alertRule']}"
            },
            {
              "op": "add",
              "path": "/fields/System.Tags",
              "value": "postmortem;incident-review"
            }
          ]
        }
      }
    }
  }
}

Script Azure CLI para consultar dados do postmortem

Para gerar o rascunho via CLI em vez de Logic Apps:

#!/bin/bash
# Script para gerar rascunho de postmortem via CLI
# Uso: ./gerar-postmortem.sh <workspace-id> <inicio> <fim>

WORKSPACE_ID=$1
INCIDENT_START=$2
INCIDENT_END=$3

echo "# Postmortem - Rascunho Automático"
echo "Gerado em: $(date -u +%Y-%m-%dT%H:%M:%SZ)"
echo ""
echo "## Período do incidente"
echo "- Início: $INCIDENT_START"
echo "- Fim: $INCIDENT_END"
echo ""

# Timeline de falhas
echo "## Timeline de falhas (requests)"
az monitor log-analytics query \
  --workspace "$WORKSPACE_ID" \
  --analytics-query "
    AppRequests
    | where TimeGenerated between (datetime('$INCIDENT_START') .. datetime('$INCIDENT_END'))
    | where Success == false
    | summarize failedCount=count() by bin(TimeGenerated, 5m), Name
    | order by TimeGenerated asc
  " \
  --output table

# Top exceções
echo ""
echo "## Exceções identificadas"
az monitor log-analytics query \
  --workspace "$WORKSPACE_ID" \
  --analytics-query "
    AppExceptions
    | where TimeGenerated between (datetime('$INCIDENT_START') .. datetime('$INCIDENT_END'))
    | summarize count() by ProblemId, Type, OuterMessage
    | order by count_ desc
    | take 10
  " \
  --output table

# Alterações no ambiente
echo ""
echo "## Alterações no ambiente (2h antes do incidente)"
az monitor log-analytics query \
  --workspace "$WORKSPACE_ID" \
  --analytics-query "
    AzureActivity
    | where TimeGenerated between (datetime_add('hour', -2, datetime('$INCIDENT_START')) .. datetime('$INCIDENT_START'))
    | where OperationNameValue has_any ('write', 'delete')
    | where ActivityStatusValue == 'Success'
    | project TimeGenerated, Caller, OperationNameValue, ResourceGroup
    | order by TimeGenerated desc
  " \
  --output table

# Dependências com falha
echo ""
echo "## Dependências com falha"
az monitor log-analytics query \
  --workspace "$WORKSPACE_ID" \
  --analytics-query "
    AppDependencies
    | where TimeGenerated between (datetime('$INCIDENT_START') .. datetime('$INCIDENT_END'))
    | where Success == false
    | summarize failures=count() by Type, Name, Target, ResultCode
    | order by failures desc
  " \
  --output table

Conclusão da Parte 1

Até aqui, você já tem a base para rodar postmortems blameless: filosofia sem culpa, template estruturado, coleta automatizada com KQL e rascunhos gerados com Logic Apps. Isso já basta para reconstruir a timeline de um incidente sem depender só de memória.

Na Parte 2, o foco passa para Azure DevOps, métricas de eficácia e cenários avançados, incluindo postmortems cross-cloud.