Projeto: MBA Arquitetura Full Cycle - Observabilidade e DevOps/SRE Fase do projeto: Do sintoma ao post-mortem
Uma instrumentação inacabada, uma queixa do financeiro, e um documento no fim que explica o que aconteceu
Você vai receber uma aplicação em TypeScript com dois processos e uma instrumentação pela metade. Alguém do time começou a instrumentar meses atrás e parou justamente antes das partes que dão sentido ao resto.
Seu trabalho tem três movimentos. Terminar o que foi começado: ligar o log ao trace, fazer o contexto atravessar a fila, consertar um erro de cardinalidade que já está plantado nas métricas, criar as métricas de negócio que faltam e montar o dashboard. Definir o que é qualidade de serviço aqui, transformando isso num SLO e derivando dele o alerta. E usar tudo isso para investigar a queixa que chegou do financeiro e fechar o incidente com um post-mortem.
Este desafio cobre dois módulos do MBA: Observabilidade e DevOps e SRE.
Você entrou no time de plataforma de uma loja online. O sistema roda em produção, o Jaeger mostra spans, o log sai em JSON, o Prometheus está coletando. Parece que tem tudo.
Aí o financeiro fechou o mês e faltou dinheiro. Alguém abriu o Grafana, não achou nada, e a resposta que circula pelo Slack é que deve ser problema do banco. Você foi contratado para acabar com o palpite.
O foco é observabilidade e resposta a incidente, não desenvolvimento de produto e não performance. A aplicação vem pronta e funcional, e o seu trabalho é completar a instrumentação dela e configurar o que falta na stack.
Você não deve corrigir o defeito da aplicação. Ele é o objeto de estudo: é o que a sua instrumentação precisa revelar. Corrigir faz o cenário de carga parar de reproduzir a queixa, e o alerta que deveria disparar não dispara mais.
O curso ensinou observabilidade em Java, com Actuator, Micrometer e o bridge do OpenTelemetry. Este desafio é em TypeScript, e isso é deliberado: observabilidade é especificação, não framework. Span continua sendo span, propagação continua sendo W3C Trace Context, e cardinalidade alta continua derrubando Prometheus em qualquer stack. O que muda é o nome do pacote e a sintaxe, e descobrir esses equivalentes na documentação é parte do trabalho, tanto que o README tem uma tabela de equivalências como entregável. Você não precisa saber TypeScript de antemão: a aplicação vem funcionando e você completa código que já existe.
Do lado de SRE, o post-mortem que você vai escrever no fim segue o template do módulo de DevOps e SRE, disponível em https://github.com/devfullcycle/MBA-fundamentos-devops-sre. Vale abrir antes de começar, porque saber quais perguntas o documento faz muda o que você presta atenção durante a investigação.
https://github.com/devfullcycle/mba-arch-desafio-observabilidade
Faça o fork e trabalhe nele. A entrega final fica na branch main do seu fork.
Pré-requisitos: Docker, Docker Compose v2 e curl. Nada além disso precisa estar instalado na sua máquina.
cp .env.example .env
docker compose up -d
curl -s localhost:8080/health
Isso sobe a api, o worker, o Postgres, o Redis e a stack de observabilidade inteira, já parcialmente configurada.
A pasta src/ é montada como volume nos dois processos, então o container roda o código que está no seu disco e você não precisa reconstruir a imagem para testar uma mudança de código. Em compensação, o processo não recarrega sozinho: depois de editar qualquer arquivo em src/, rode docker compose restart api worker. Vale o mesmo para git checkout, git stash e git pull, que mudam o código do disco sem mexer no que já está rodando. A exceção é o package.json: as dependências ficam dentro da imagem, então acrescentar uma biblioteca exige docker compose up -d --build.
Um único projeto TypeScript, dois processos com o mesmo código-fonte e entrypoints diferentes, um Postgres e um Redis.
api na porta 8080, com quatro rotas de negócio:
GET /produtoslista produtos eGET /produtos/:iddevolve um produto, ou 404POST /pedidosrecebe{cliente_id, itens: [{produto_id, quantidade}]}, grava o pedido comopendente, publica na filapedidosdo Redis e devolve 202 com{pedido_id, status}GET /pedidos/:iddevolve o pedido, ou 404
Além delas, GET /health e GET /metrics.
worker na porta 8081, que expõe apenas /health e /metrics. Consome a fila pedidos, chama o processador de pagamento simulado e atualiza o pedido para confirmado ou recusado. O processador é uma função local, sem chamada externa, e o resultado depende do cliente: ele pode aprovar, recusar ou falhar.
O Postgres sobe com produtos e com alguns pedidos antigos já carregados. Use GET /produtos para obter identificadores válidos.
Este é o ponto de partida. Confira cada item com os próprios olhos antes de começar, porque o desafio inteiro parte daqui.
- Tracing automático funcionando. O SDK do OpenTelemetry já sobe nos dois processos, com auto instrumentação de HTTP e de banco, exportando por OTLP para o Jaeger com amostragem em 100%. Uma requisição já vira trace com spans de rota e de consulta
- Log em JSON. Os dois processos já escrevem uma linha JSON por evento no stdout, com os campos
timestamp,level,serviceemsg. Não existe nenhum campo que ligue a linha a um trace - Rota
/metricscomprom-client. Já existem as métricas padrão do processo e um histograma de latência HTTP chamadohttp_request_duration_seconds. Não existe nenhuma métrica de negócio - Prometheus coletando. Os dois processos já aparecem como
UPemhttp://localhost:9090/targets, e oprometheus.ymljá declara de onde ler as regras de alerta. A pasta de regras é que está vazia - Grafana com datasources e um dashboard começado. Prometheus e Jaeger já provisionados por arquivo, e existe um dashboard provisionado com um painel só, para você não precisar descobrir o formato do JSON do zero
- Alertmanager ligado ao receptor. A rota e o receiver já apontam para o
receptor-alertas, com o agrupamento já ajustado para o disparo ser rápido. Não existe nenhuma regra de alerta
Nada liga o log ao trace, e não existe nenhum span de negócio. O contexto não atravessa a fila, então o que a api faz e o que o worker faz aparecem hoje como dois traces separados, sem relação entre si. Não existe métrica de negócio, nem SLO declarado, nem regra de alerta, e o dashboard tem um painel só.
Além disso, tem um problema plantado na instrumentação de métricas que já existe, que o requisito 3 trata.
"Fechamos o mês e faltou dinheiro." O financeiro cruzou os pedidos que aparecem como confirmados para o cliente com o que efetivamente entrou, e encontrou pedidos confirmados que nunca foram cobrados. Reproduz com cenario-a.
Pista: do lado de fora está tudo 2xx, o cliente recebe sucesso e o pedido aparece confirmado. Monitoramento de caixa preta não enxerga isso, e é por isso que o Grafana não mostrou nada. A resposta está no que o código sabe e hoje não conta a ninguém.
docker compose run --rm carga normal
docker compose run --rm carga cenario-a
Cada cenário roda até você interromper com Ctrl+C. Para deixar rodando em segundo plano, a flag vem antes do nome do serviço, como em docker compose run --rm -d carga normal, e para parar o que ficou rodando assim use docker stop $(docker ps -q --filter name=carga-run). O normal é o tráfego saudável, com aprovações e recusas legítimas, e a queixa não se manifesta nele: é contra ele que você calibra o alerta. O cenario-a reproduz a queixa, e leva menos de um minuto para isso acontecer.
http://localhost:9099, um serviço mínimo que recebe webhook do Alertmanager e registra no stdout tudo que chega. É a sua prova de que o alerta disparou.
Por quê. Log e trace hoje são duas ilhas: você tem o trace de uma operação e não acha as linhas de log dela, e tem a linha de um erro e não acha o trace onde ele aconteceu. Essa ponte é a espinha da observabilidade.
Tarefa. Acrescente a toda linha de log dos dois processos os campos trace_id e span_id, com esses nomes exatos, preenchidos a partir do span ativo no momento em que a linha é escrita. Linhas relacionadas a um pedido carregam também pedido_id.
Os dois campos existem em toda linha, sempre. Quando não houver span ativo, como na subida do processo, eles vão vazios. Campo que aparece e some conforme o contexto quebra qualquer filtro automático em cima do log.
O identificador tem que ser o mesmo que aparece no Jaeger. Identificador próprio, gerado pela aplicação e desconectado do trace, não atende.
Por quê. A auto instrumentação entrega os limites técnicos, que são rota e consulta, e não entrega os limites de negócio. E ela não atravessa fila, então hoje não existe forma de olhar um pedido confirmado e ver o que aconteceu na cobrança dele.
Tarefa. Duas coisas.
- Crie dois spans manuais, com estes nomes exatos:
pedido.criarnoPOST /pedidosepedido.processarno consumo da mensagem pelo worker. Ambos com atributos úteis para diagnóstico, incluindo o identificador do pedido - Faça o contexto de trace atravessar a fila, injetando na publicação e extraindo no consumo, de modo que um
POST /pedidose o processamento dele formem um único trace com um únicotrace_id
Além disso, toda exceção capturada pelo código passa a ser registrada no span, com o status do span indo para erro, e a produzir uma linha de log de nível error com o motivo. O fluxo da aplicação continua exatamente o mesmo: você só passa a contar o que acontece.
Um detalhe do Express que atrapalha exatamente aqui: a versão 4 não captura exceção de handler async, então hoje um erro dentro do POST /pedidos não chega ao middleware de erro e derruba o processo da api. Se você envolver o handler em try/catch para registrar a exceção no span, relance o erro no fim do catch. Capturar a exceção e responder 500 onde antes o processo caía é mudar comportamento, e o que se pede aqui é instrumentar sem mudar.
Por quê. O histograma que já existe comete o erro de cardinalidade mais clássico que existe, e o custo dele não aparece em teste: aparece em produção, semanas depois, quando o Prometheus começa a consumir memória sem explicação. E métrica de infraestrutura sozinha não conta a história do negócio: o sistema pode estar com 100% de disponibilidade e sangrando dinheiro.
Tarefa. Três coisas.
- Encontre o erro de cardinalidade na instrumentação de métricas existente, corrija, e explique no README em uma frase por que aquilo derrubaria um Prometheus em produção
- Crie três métricas de negócio, com estes nomes e tipos exatos:
pedidos_criados_totalepedidos_confirmados_total, contadores, ecobrancas_processadas_total, contador com o labelresultado, que assume os valoresaprovada,recusadaefalha - Inicialize os três contadores em zero na subida, incluindo cada valor do label
resultado. Série que só nasce no primeiro evento some do gráfico enquanto está tudo bem e quebra o alerta que dependia dela
A proibição de cardinalidade vale para métrica. Em span e em log o identificador é bem-vindo e necessário, porque lá ele custa barato e é o que permite achar o caso individual.
Por quê. Dashboard construído na mão dentro do Grafana morre com o container: não é versionado, não é revisado, ninguém sabe quem mudou o quê. E dashboard eficaz não é o que mostra tudo, é o que cabe numa tela e responde as perguntas que alguém vai fazer às três da manhã.
Tarefa. Complete o dashboard Pedidos, que já vem provisionado em grafana/provisioning/dashboards/pedidos.json e hoje tem um painel só. No fim ele tem no máximo quatro painéis e responde estas três perguntas:
- o sistema está com erro?
- o sistema está lento?
- o dinheiro está entrando?
Quais painéis usar para responder é decisão sua, e o teto de quatro é parte do exercício. Todo painel com título que diz o que ele mostra, e o README diz qual das três perguntas cada painel responde.
O painel que já vem no arquivo é exemplo de formato, não peça obrigatória: mantenha, retitule ou apague, como preferir. O que conta no fim é que todo painel que sobrar responda uma das três perguntas e que o seu README diga qual.
Por quê. Limiar escolhido no olho é chute com aparência de rigor, e é assim que nasce o alerta que ninguém respeita. SLO inverte a ordem: primeiro você declara quanta falha o serviço pode ter sem que ninguém precise ser acordado, e o limiar do alerta vira consequência aritmética disso, não opinião.
Tarefa. Declare o objetivo de serviço e derive o alerta dele.
- Defina o SLI: a medida de saúde do processamento de cobrança, em uma frase, acompanhada da expressão PromQL que a calcula a partir das suas métricas
- Defina o SLO: o alvo numérico para esse SLI, com a janela de tempo a que ele se refere, e o error budget que esse alvo implica
- Escreva no Prometheus uma regra chamada exatamente
FalhaEmCobrancas, cujo limiar saia do SLO, com a conta explicada no README. Regra com número redondo escolhido no olho não atende, ainda que dispare na hora certa forentre 30s e 1m, inclusive nos extremos. É um limite do desafio, para o disparo ser observável durante a correção- O Alertmanager agrupa alertas e espera um intervalo antes de disparar o webhook. O starter já vem com esse intervalo curto, então some ao seu
formais alguns segundos e saiba que isso existe antes de achar que a sua regra está quebrada - A janela do seu
rate()também entra no tempo de detecção, e esse é o detalhe que mais confunde. O alerta compara uma proporção, então, se ocenario-asobe logo depois donormal, a janela ainda está cheia de sucesso e a proporção precisa primeiro diluir o que estava limpo para só então cruzar o limiar. Quanto mais tráfego saudável cabe na sua janela, e quanto mais alto o seu limiar, mais isso demora. Reiniciarapieworkernão adianta: o Prometheus compensa o reset do contador e o histórico continua contando dentro da janela. Isso não é defeito da sua regra, e o critério de aceite já dá margem para isso. Só não meça o tempo de disparo no primeiro minuto e conclua que nada funciona - Comportamento exigido: a regra não dispara no cenário
normale dispara nocenario-a
Não existe SLO certo neste desafio. Existe SLO justificado: o que vale é a coerência entre o número que você escolheu, o que ele significa para o negócio e o limiar que sai dele.
Por quê. Instrumentação existe para responder pergunta, e num time de verdade a resposta vira um documento que outras pessoas leem: o que aconteceu, quanto custou, como foi descoberto e o que muda para não repetir. Sem isso, o incidente é resolvido e esquecido, e volta em três meses com outra roupa.
Tarefa. Investigue a queixa usando a sua instrumentação e escreva reports/post-mortem.md, numa versão enxuta do template do módulo de DevOps e SRE, com exatamente estas oito seções, com estes títulos:
## Resumo: o que aconteceu, em duas ou três frases, para quem só vai ler isto## Impacto: no período que você reproduziu, quantos pedidos foram afetados e qual o valor envolvido, dizendo de onde saiu cada número. Repare que a sua métrica responde uma dessas perguntas e não responde a outra## Detecção: como o problema foi descoberto hoje, quanto tempo o seu alerta leva para detectar o mesmo problema, medido na sua reprodução, e o que essa diferença significa## Causa raiz: o arquivo e a função onde o defeito mora, com o trecho de código colado## Evidências: umtrace_idreal observado no Jaeger, a query PromQL usada e o comando de busca no log usado, cada um com o resultado que você obteve## Lições aprendidas: as três perguntas do template, que são o que correu bem, o que correu mal e onde tivemos sorte## Itens de ação: tabela com no mínimo três ações, cada uma com o tipo, sendo evitar ou mitigar, e a prioridade. A correção definitiva do defeito é uma delas, descrita e não executada## Timeline: a sequência do que aconteceu e do que você fez, reconstruída a partir da evidência
Um aviso sobre a contagem de pedidos afetados: o increase() do Prometheus extrapola a janela e devolve estimativa, não contagem exata, então é normal que ele divirja em alguns pedidos do que você contar no log ou no banco. Isso não é erro seu. Diga de onde veio cada número e, se as fontes divergirem, diga isso também: post-mortem que declara a origem e a precisão de cada número vale mais do que um que apresenta estimativa como se fosse contagem.
Escreva sem procurar culpado. Post-mortem que aponta pessoa não gera aprendizado, gera silêncio no próximo incidente.
Por quê. A entrega é lida por alguém que nunca viu o seu repositório e que vai executar exatamente o que estiver escrito. Se o roteiro não funcionar do zero, a entrega não existe.
Tarefa. Substitua o README.md do projeto base por um com estas quatro seções, com estes títulos:
## Como rodar: do clone à stack no ar## Equivalências com o curso: tabela com quatro linhas, uma para métricas, uma para tracing, uma para logs estruturados e uma para propagação de contexto, dizendo o que o curso usou em Java e o que você usou em TypeScript## Decisões técnicas: a explicação do erro de cardinalidade, o que cada métrica de negócio responde, e qual das três perguntas cada painel do dashboard responde## SLO e alerta: o SLI com a expressão que o calcula, o SLO com a janela, o error budget que ele implica, e a conta que leva do SLO ao limiar da regra
- Você completa a instrumentação. Não altera comportamento funcional e não muda o contrato das rotas existentes
- Escrever dentro de um bloco que hoje não registra nada é instrumentação, não correção, e é esperado que você faça isso
- Não altere a pasta
carga/nem a pastareceptor-alertas/ - Nenhum componente da stack pode ser substituído por serviço de terceiros, porque a entrega roda inteira na máquina do avaliador
- Dashboard entra por arquivo versionado. Você pode montar o painel pela interface do Grafana para experimentar, mas o que conta é o JSON versionado, e o ambiente subindo do zero tem que trazer tudo pronto sem nenhum clique
- Nenhuma credencial nova em arquivo versionado. O que precisar de valor vem do
.env
- Corrigir o defeito da aplicação
- Agregação centralizada de log. O log fica em JSON no stdout e a correlação se prova por linha de comando
- OpenTelemetry Collector e Pushgateway. A exportação é direta e a coleta é por pull
- Exporters de infraestrutura. O foco é a instrumentação da aplicação
- Instrumentar o
receptor-alertasou o gerador de carga - Integração com Slack, e-mail ou qualquer canal externo. O
receptor-alertasé o destino - Pipeline de CI, publicação de imagem e infraestrutura como código. Nada de GitHub Actions aqui
Todos os critérios são eliminatórios: qualquer item não atendido reprova a entrega. Cada um traz o comando ou a tela que dá o check, a partir de um clone limpo do seu fork, com a stack no ar e com o cenário de carga que o próprio critério indicar.
Logs
☐ Toda linha de log dos dois processos é JSON e traz timestamp, level, service, msg, trace_id e span_id, mesmo quando os dois últimos estão vazios. Confira com docker compose logs api | tail -20 e docker compose logs worker | tail -20
☐ Linhas relacionadas a um pedido trazem pedido_id
☐ Rodando cenario-a, aparecem linhas de nível error com o motivo da falha
Tracing
☐ Um POST /pedidos aparece no Jaeger como um trace único que contém spans da api e do worker
☐ Esse mesmo trace contém os spans pedido.criar e pedido.processar
☐ Rodando cenario-a, existe no Jaeger ao menos um trace com span marcado como erro
☐ O trace_id de um trace do Jaeger, buscado com docker compose logs api worker, devolve linhas dos dois processos
Métricas
☐ Nenhuma série de http_request_duration_seconds usa caminho concreto no lugar do template. /produtos/:id está correto, /produtos/42 reprova
☐ Nenhuma série usa identificador de pedido ou de cliente como label
☐ As saídas de /metrics dos dois processos, somadas, contêm pedidos_criados_total, pedidos_confirmados_total e cobrancas_processadas_total, esta última com o label resultado nos valores aprovada, recusada e falha
☐ Os três contadores existem já na subida, antes de qualquer tráfego
Dashboard
☐ Existe exatamente um dashboard provisionado por arquivo JSON versionado, e ele abre no Grafana logo após a subida, sem nenhum clique de configuração
☐ Tem no máximo quatro painéis, nenhum com título vazio ou igual a Panel Title
☐ O README diz, para cada painel, qual das três perguntas do requisito 4 ele responde
SLO e alerta
☐ O README declara o SLI com a expressão PromQL que o calcula, o SLO com alvo e janela, o error budget que ele implica, e a conta que leva do SLO ao limiar da regra
☐ http://localhost:9090/rules exibe FalhaEmCobrancas, carregada sem erro e com for entre 30s e 1m
☐ Com o cenário normal rodando por 3 minutos após docker compose restart receptor-alertas, nenhum alerta aparece em docker compose logs receptor-alertas
☐ Rodando cenario-a, FalhaEmCobrancas aparece no log do receptor-alertas em até 5 minutos. A margem é bem maior que o for de propósito: quando o cenario-a sobe logo depois do normal, a proporção ainda precisa diluir o tráfego saudável que está na janela do rate() antes de cruzar o limiar, e quanto mais alto o seu limiar, mais isso demora
Post-mortem
☐ Existe reports/post-mortem.md com as oito seções exigidas, com os títulos exatos
☐ ## Impacto quantifica pedidos afetados e valor envolvido, dizendo a origem de cada número
☐ ## Detecção diz como o problema foi descoberto, quanto tempo o alerta leva para detectá-lo e o que a diferença significa
☐ ## Causa raiz cita o arquivo e a função onde o defeito mora, e ambos coincidem com o gabarito de correção
☐ ## Evidências traz um trace_id, uma query PromQL e um comando de busca no log, cada um com o resultado obtido
☐ ## Itens de ação é uma tabela com no mínimo três ações, cada uma com tipo e prioridade
README
☐ O README.md tem as quatro seções exigidas, com os títulos exatos, e a tabela de equivalências está preenchida com o que foi usado de fato no código
Integridade
☐ git diff contra o repositório base não mostra alteração em carga/ nem em receptor-alertas/
☐ git diff -w contra o repositório base em src/ mostra apenas linhas de instrumentação, sem alteração no fluxo da aplicação. A opção -w existe porque envolver um bloco em um span reindenta o código sem mudar nada
.
├── README.md (substituído por você)
├── compose.yaml
├── .env.example
├── src/
│ ├── api/ você completa a instrumentação
│ ├── worker/ você completa a instrumentação
│ ├── telemetria/ bootstrap do OTel, logger e métricas
│ └── ...
├── carga/ (não alterar)
├── receptor-alertas/ (não alterar)
├── prometheus/
│ ├── prometheus.yml já configurado, inclusive de onde ler as regras
│ └── regras/ (você preenche)
├── alertmanager/
│ └── alertmanager.yml já configurado
├── grafana/
│ └── provisioning/
│ ├── datasources/ já configurado
│ └── dashboards/
│ └── pedidos.json um painel pronto, você completa
└── reports/
└── post-mortem.md esqueleto pronto, você preenche
- Link do fork público no GitHub, com tudo consolidado na branch
main - README com as quatro seções obrigatórias, verificado do zero
- O post-mortem em
reports/ - A base é obrigatória. Entregas que reescrevem a aplicação, trocam a stack ou corrigem o defeito não serão aceitas
1. Suba o ambiente e explore o que já existe antes de escrever qualquer linha. Faça um pedido, abra o trace no Jaeger, leia uma linha de log, olhe a saída de /metrics e os alvos no Prometheus. Metade do desafio é entender onde a instrumentação atual para.
2. Comece pela correlação de log com trace. É a peça mais barata e a que mais muda o seu dia dali em diante: sem ela, você vai depurar o resto no escuro. Só considere pronto quando conseguir pegar um trace_id no Jaeger e achar as linhas dos dois processos por aquele identificador.
3. Ataque as métricas: conserte a cardinalidade primeiro, porque ela suja tudo que vem depois, e só então crie as de negócio. Feito isso, suba os dois cenários e passe alguns minutos só olhando os números subirem. Métrica de negócio serve para levantar pergunta, e é bem provável que você saia daqui com uma.
4. Faça os spans manuais e depois a travessia da fila. Este é o ponto mais difícil do desafio. O contexto de trace não é mágica do framework, é dado que precisa viajar junto com a mensagem, e o OpenTelemetry tem uma API própria de propagação para isso. Se ao consumir a mensagem você começar um trace novo em vez de continuar o que existia, o sintoma é claro: dois traces curtos no Jaeger em vez de um completo. E preste atenção em qual contexto você usa como base ao extrair, porque usar o contexto ativo do worker em vez de um contexto raiz é o erro que faz o span nascer no lugar errado.
5. Monte o dashboard por arquivo. Derrube tudo com docker compose down -v, suba de novo e confirme que os painéis voltaram sozinhos.
6. Declare o SLI e o SLO antes de escrever a regra. A ordem importa: quem escreve a regra primeiro acaba inventando um SLO que justifica o número que já tinha escolhido. Depois prove os dois comportamentos, silêncio no normal e disparo no cenario-a.
7. Investigue e escreva o post-mortem. Abra o esqueleto do documento antes de começar a investigar, porque as perguntas dele dizem que evidência você precisa guardar pelo caminho. Reconstruir evidência depois é mais caro do que salvar na hora, e resista ao impulso de procurar a resposta lendo o código: se ela vier do código e não da telemetria, você não provou nada, só confirmou um palpite, que é o que a empresa já fazia antes de você chegar.
8. Percorra os critérios de aceite item a item, do zero, seguindo só o seu README, antes do push final.