Notificações, Logs e Monitoramento do Pipeline

[292] Notificações, Logs e Monitoramento do Pipeline

O pipeline que falha em silêncio e o alerta genérico que ninguém lê: notificações no Slack, email e Teams roteadas por contexto, logs estruturados nos jobs, o cálculo das métricas de entrega, badges de status no README e a integração com ferramentas de observabilidade.
DevOps

19 min de leitura

Um pipeline de CI/CD que falha sem avisar ninguém é pior do que não ter pipeline. A automação cria uma falsa sensação de segurança: o time assume que, se nada foi dito, tudo correu bem. Quando a falha finalmente é descoberta — geralmente por um cliente reclamando de algo que não funciona — o tempo de resposta é muito maior do que seria se o alerta tivesse chegado imediatamente.

Monitoramento de pipeline não é um detalhe de acabamento. É parte integral do sistema de entrega. Um bom sistema de notificações garante que a pessoa certa recebe a informação certa no momento certo, sem criar ruído que leve a equipe a ignorar os alertas.

Este artigo cobre três dimensões complementares: como estruturar notificações úteis, como interpretar e centralizar logs de pipeline, e como monitorar a saúde do pipeline como um sistema em si.

O Problema do Alerta Genérico

Antes de configurar qualquer notificação, vale refletir sobre o que torna um alerta útil versus um alerta que se torna ruído.

Um alerta genérico do tipo "pipeline falhou" obriga o receptor a abrir o GitHub, encontrar o workflow, identificar qual job falhou, ler os logs e então entender o que aconteceu. Se esse processo acontece dezenas de vezes por semana, a equipe começa a ignorar os alertas.

Um alerta útil responde imediatamente a quatro perguntas: o quê falhou, onde falhou, quem causou a falha e o que fazer a seguir. Com essa informação disponível diretamente na notificação, o tempo de resposta cai drasticamente e a equipe mantém o hábito de agir sobre os alertas.

Notificações no Slack

O Slack é a ferramenta de comunicação mais comum em equipes de tecnologia. A integração com GitHub Actions pode ser feita de diferentes formas, com diferentes níveis de riqueza de informação.

A forma mais simples usa webhooks do Slack diretamente:

- name: Notifica falha no Slack
  if: failure()
  uses: slackapi/slack-github-action@v1.26.0
  with:
    payload: |
      {
        "blocks": [
          {
            "type": "header",
            "text": {
              "type": "plain_text",
              "text": "❌ Pipeline Falhou"
            }
          },
          {
            "type": "section",
            "fields": [
              {
                "type": "mrkdwn",
                "text": "*Repositório:*\n${{ github.repository }}"
              },
              {
                "type": "mrkdwn",
                "text": "*Branch:*\n${{ github.ref_name }}"
              },
              {
                "type": "mrkdwn",
                "text": "*Job Falhou:*\n${{ github.job }}"
              },
              {
                "type": "mrkdwn",
                "text": "*Autor do Commit:*\n${{ github.actor }}"
              }
            ]
          },
          {
            "type": "section",

            "text": {
              "type": "mrkdwn",
              "text": "*Mensagem do Commit:*\n${{ github.event.head_commit.message }}"
            }
          },
          {
            "type": "actions",
            "elements": [
              {
                "type": "button",
                "text": {
                  "type": "plain_text",
                  "text": "Ver Pipeline"
                },
                "url": "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
              }
            ]
          }
        ]
      }
  env:
    SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
    SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK

Para notificações mais sofisticadas que incluem o diff de cobertura de testes, o tamanho do PR ou métricas de build, usa-se a API do Slack diretamente com um token de bot:

- name: Notifica resultado completo do pipeline
  if: always()
  uses: slackapi/slack-github-action@v1.26.0
  with:
    channel-id: 'deploys'
    slack-message: |
      ${{ job.status == 'success' && '✅' || '❌' }} *${{ github.repository }}*
      *Status:* ${{ job.status }}
      *Branch:* `${{ github.ref_name }}`
      *Commit:* `${{ github.sha }}`
      *Autor:* ${{ github.actor }}
      *Duração:* ${{ steps.duracao.outputs.tempo }}
      <${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|Ver detalhes>
  env:
    SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}

Notificações por Email e Microsoft Teams

Para equipes que usam email como canal principal de comunicação:

- name: Envia notificação por email
  if: failure()
  uses: dawidd6/action-send-mail@v3
  with:
    server_address: smtp.gmail.com
    server_port: 465
    secure: true
    username: ${{ secrets.EMAIL_USERNAME }}
    password: ${{ secrets.EMAIL_PASSWORD }}
    subject: "❌ Pipeline falhou — ${{ github.repository }} (${{ github.ref_name }})"
    to: time-devops@empresa.com
    from: pipeline@empresa.com
    html_body: |
      <h2>Pipeline Falhou</h2>
      <table>
        <tr><td><strong>Repositório</strong></td><td>${{ github.repository }}</td></tr>
        <tr><td><strong>Branch</strong></td><td>${{ github.ref_name }}</td></tr>
        <tr><td><strong>Commit</strong></td><td>${{ github.sha }}</td></tr>
        <tr><td><strong>Autor</strong></td><td>${{ github.actor }}</td></tr>
        <tr><td><strong>Job</strong></td><td>${{ github.job }}</td></tr>
      </table>
      <p>
        <a href="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}">
          Ver detalhes do pipeline
        </a>
      </p>

Para Microsoft Teams, o formato usa Adaptive Cards:

- name: Notifica no Microsoft Teams
  if: failure()
  uses: jdcargile/ms-teams-notification@v1.4
  with:
    github-token: ${{ secrets.GITHUB_TOKEN }}
    ms-teams-webhook-uri: ${{ secrets.TEAMS_WEBHOOK_URL }}
    notification-summary: "Pipeline falhou em ${{ github.repository }}"
    notification-color: "FF0000"
    timezone: America/Sao_Paulo

Estratégia de Notificação por Contexto

Notificar sempre e para todos é tão ruim quanto não notificar. A estratégia mais eficiente diferencia o contexto:

jobs:
  testes:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm test

      # Notifica o autor do commit diretamente quando os testes falham
      - name: Notifica autor sobre falha nos testes
        if: failure() && github.event_name == 'pull_request'
        uses: slackapi/slack-github-action@v1.26.0
        with:
          channel-id: ${{ secrets.SLACK_CHANNEL_DEVS }}
          slack-message: |
            <@${{ github.actor }}> seus testes falharam no PR #${{ github.event.pull_request.number }}.
            <${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|Ver logs>
        env:
          SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}

  deploy-producao:
    runs-on: ubuntu-latest
    environment: production
    steps:
      - name: Deploy
        run: ./scripts/deploy.sh

      # Deploy bem-sucedido — notifica canal geral
      - name: Anuncia deploy bem-sucedido
        if: success()
        uses: slackapi/slack-github-action@v1.26.0
        with:
          channel-id: ${{ secrets.SLACK_CHANNEL_GERAL }}
          slack-message: |
            🚀 *Deploy em produção concluído*
            Versão `${{ github.sha }}` está no ar.
            Autor: ${{ github.actor }}
        env:
          SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}

      # Deploy falhou — acorda o plantão
      - name: Alerta crítico de falha em produção
        if: failure()
        uses: slackapi/slack-github-action@v1.26.0
        with:
          channel-id: ${{ secrets.SLACK_CHANNEL_ALERTAS }}
          slack-message: |
            🚨 *ALERTA: Deploy em produção FALHOU*
            <!channel> verificar imediatamente.
            Commit: `${{ github.sha }}`
            Autor: ${{ github.actor }}
            <${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|Ver pipeline>
        env:
          SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}

Logs Estruturados no Pipeline

Logs são o principal instrumento de diagnóstico quando algo dá errado. A diferença entre logs úteis e logs inúteis é a estrutura e a consistência.

O GitHub Actions oferece comandos especiais chamados workflow commands que permitem estruturar a saída dos logs:

# Agrupa linhas relacionadas em uma seção recolhível
echo "::group::Instalando dependências"
npm ci
echo "::endgroup::"

# Marca uma linha como aviso — aparece destacado nos logs
echo "::warning::Cobertura de testes abaixo de 85%"

# Marca uma linha como erro — aparece em vermelho
echo "::error::Falha na conexão com o banco de dados"

# Adiciona uma anotação vinculada a um arquivo e linha específica
echo "::error file=src/auth.js,line=42::Token de autenticação não validado"

# Define uma variável disponível nos steps seguintes
echo "BUILD_VERSION=1.2.3" >> $GITHUB_ENV

# Define um output do step disponível nos jobs seguintes
echo "image-tag=sha-abc1234" >> $GITHUB_OUTPUT

# Mascara um valor para que não apareça nos logs
echo "::add-mask::$SENHA_SECRETA"

Exemplo de um script de deploy com logs bem estruturados:

#!/bin/bash
set -euo pipefail

# Funções de log com timestamp e nível
log_info() {
  echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] [INFO] $*"
}

log_warn() {
  echo "::warning::[$(date -u +%Y-%m-%dT%H:%M:%SZ)] [WARN] $*"
}

log_error() {
  echo "::error::[$(date -u +%Y-%m-%dT%H:%M:%SZ)] [ERROR] $*"
}

log_group() {
  echo "::group::$1"
}

log_endgroup() {
  echo "::endgroup::"
}

# Uso no script de deploy
log_group "Verificando pré-requisitos"
  log_info "Verificando Docker..."
  docker version || { log_error "Docker não está acessível"; exit 1; }

  log_info "Verificando conectividade com o servidor..."
  nc -zv "$DEPLOY_HOST" 22 || { log_error "Servidor inacessível na porta 22"; exit 1; }
log_endgroup

log_group "Executando deploy"
  log_info "Imagem: $IMAGE"
  log_info "Servidor: $DEPLOY_HOST"

  docker pull "$IMAGE" && log_info "Imagem baixada com sucesso" || {
    log_error "Falha ao baixar imagem $IMAGE"
    exit 1
  }
log_endgroup

log_group "Verificando saúde pós-deploy"
  for i in $(seq 1 10); do
    STATUS=$(curl -s -o /dev/null -w "%{http_code}" \
      "http://$DEPLOY_HOST:3000/health" || echo "000")

    if [ "$STATUS" = "200" ]; then
      log_info "Aplicação saudável após ${i}s"
      break
    fi

    if [ "$i" = "10" ]; then
      log_error "Aplicação não ficou saudável após 10s — status $STATUS"
      exit 1
    fi

    log_warn "Aguardando... tentativa $i/10 — status atual: $STATUS"
    sleep 1
  done
log_endgroup

Calculando e Reportando Métricas do Pipeline

Acompanhar métricas do pipeline ao longo do tempo permite identificar degradações de performance antes que se tornem problemas críticos.

Um step que calcula a duração do pipeline e a reporta:

jobs:
  pipeline:
    runs-on: ubuntu-latest

    steps:
      - name: Registra hora de início
        id: inicio
        run: echo "timestamp=$(date +%s)" >> $GITHUB_OUTPUT

      # ... steps do pipeline ...

      - name: Calcula e reporta métricas
        if: always()
        run: |
          INICIO=${{ steps.inicio.outputs.timestamp }}
          FIM=$(date +%s)
          DURACAO=$((FIM - INICIO))
          MINUTOS=$((DURACAO / 60))
          SEGUNDOS=$((DURACAO % 60))

          echo "::notice::Duração total do pipeline: ${MINUTOS}m ${SEGUNDOS}s"

          # Alerta se o pipeline demorou mais que 10 minutos
          if [ $DURACAO -gt 600 ]; then
            echo "::warning::Pipeline acima de 10 minutos — considere otimização"
          fi

          # Envia métrica para o Datadog (se disponível)
          if [ -n "${{ secrets.DD_API_KEY }}" ]; then
            curl -s -X POST "https://api.datadoghq.com/api/v2/series" \
              -H "Content-Type: application/json" \
              -H "DD-API-KEY: ${{ secrets.DD_API_KEY }}" \
              -d "{
                \"series\": [{
                  \"metric\": \"ci.pipeline.duration\",
                  \"points\": [[$(date +%s), $DURACAO]],
                  \"tags\": [
                    \"repo:${{ github.repository }}\",
                    \"branch:${{ github.ref_name }}\",
                    \"status:${{ job.status }}\"
                  ]
                }]
              }"
          fi

Monitoramento do Pipeline como Sistema

Além de monitorar execuções individuais, é valioso monitorar o pipeline como um sistema — identificar padrões de falha, jobs lentos e tendências ao longo do tempo.

O GitHub fornece uma API para consultar dados históricos de workflows:

#!/bin/bash
# scripts/analise-pipeline.sh
# Analisa as últimas 50 execuções do workflow de CI

REPO="meu-usuario/meu-repositorio"
WORKFLOW="ci.yml"
TOKEN="$GITHUB_TOKEN"

# Busca as últimas execuções
RUNS=$(curl -s \
  -H "Authorization: Bearer $TOKEN" \
  -H "Accept: application/vnd.github+json" \
  "https://api.github.com/repos/$REPO/actions/workflows/$WORKFLOW/runs?per_page=50")

# Calcula taxa de sucesso
TOTAL=$(echo "$RUNS" | jq '.workflow_runs | length')
SUCESSO=$(echo "$RUNS" | jq '[.workflow_runs[] | select(.conclusion == "success")] | length')
FALHA=$(echo "$RUNS" | jq '[.workflow_runs[] | select(.conclusion == "failure")] | length')
TAXA=$(echo "scale=1; $SUCESSO * 100 / $TOTAL" | bc)

echo "=== Análise do Pipeline: últimas $TOTAL execuções ==="
echo "Sucesso: $SUCESSO ($TAXA%)"
echo "Falha: $FALHA"

# Calcula duração média das execuções bem-sucedidas
MEDIA=$(echo "$RUNS" | jq '
  [.workflow_runs[]
   | select(.conclusion == "success")
   | (.updated_at | fromdateiso8601) - (.created_at | fromdateiso8601)
  ] | add / length | . / 60 | floor')

echo "Duração média (sucesso): ${MEDIA} minutos"

# Identifica os jobs que mais falham
echo ""
echo "=== Analisando jobs com maior taxa de falha ==="
for RUN_ID in $(echo "$RUNS" | jq -r '.workflow_runs[].id' | head -20); do
  curl -s \
    -H "Authorization: Bearer $TOKEN" \
    "https://api.github.com/repos/$REPO/actions/runs/$RUN_ID/jobs" \
    | jq -r '.jobs[] | select(.conclusion == "failure") | .name'
done | sort | uniq -c | sort -rn | head -10

Dashboard de Status com GitHub Badges

Uma forma simples e eficaz de tornar o status do pipeline visível para toda a equipe é adicionar badges ao README do repositório:

# Minha API

[![CI](https://github.com/usuario/minha-api/actions/workflows/ci.yml/badge.svg)](https://github.com/usuario/minha-api/actions/workflows/ci.yml)
[![Deploy](https://github.com/usuario/minha-api/actions/workflows/deploy.yml/badge.svg)](https://github.com/usuario/minha-api/actions/workflows/deploy.yml)
[![Cobertura](https://codecov.io/gh/usuario/minha-api/branch/main/graph/badge.svg)](https://codecov.io/gh/usuario/minha-api)

Esses badges atualizam automaticamente e são visíveis para qualquer pessoa que abrir o repositório — sem precisar entrar na aba de Actions.

Integrando com Ferramentas de Observabilidade

Para times com ferramentas de APM como Datadog ou New Relic, integrar os eventos de deploy ao sistema de observabilidade cria uma linha do tempo unificada que correlaciona deploys com mudanças de performance:

- name: Registra evento de deploy no Datadog
  if: success()
  run: |
    curl -s -X POST "https://api.datadoghq.com/api/v1/events" \
      -H "Content-Type: application/json" \
      -H "DD-API-KEY: ${{ secrets.DD_API_KEY }}" \
      -d '{
        "title": "Deploy em produção",
        "text": "Versão ${{ github.sha }} implantada por ${{ github.actor }}",
        "alert_type": "info",
        "tags": [
          "env:production",
          "service:minha-api",
          "version:${{ github.sha }}"
        ],
        "source_type_name": "github"
      }'

Com esse evento registrado, qualquer anomalia de performance que ocorra após um deploy fica visualmente correlacionada no dashboard do Datadog — simplificando enormemente a investigação de regressões.

Encerrando o Módulo 4

Com este artigo conclui-se o Módulo 4. Foram cobertos os fundamentos conceituais do CI/CD, a construção de pipelines completos com GitHub Actions, testes automatizados em suas diferentes camadas, gerenciamento seguro de secrets, deploy automático com zero downtime e rollback, e o monitoramento do próprio pipeline como sistema.

O Módulo 5 entra no território da Infraestrutura como Código — a disciplina que aplica os mesmos princípios de versionamento, revisão e automação que foram aplicados ao código da aplicação à própria infraestrutura que a sustenta.

Referências para Aprofundamento

Documentação oficial

Ferramentas de notificação

Observabilidade e métricas

  • Datadog CI Visibility — Documentação do módulo de visibilidade de CI/CD do Datadog, que agrega métricas de múltiplos pipelines em dashboards unificados.
  • Codecov Documentation — Documentação do Codecov para integração de relatórios de cobertura de testes ao pipeline do GitHub Actions.

Exercícios

Exercício 1

Por que um alerta do tipo "pipeline falhou" acaba sendo pior do que útil? Quais são as quatro perguntas que um alerta bem construído responde de imediato, e onde cada uma delas aparece no payload de blocos do Slack mostrado no artigo?

Ver resposta

✓ Resposta: Porque ele obriga quem recebe a fazer todo o trabalho de diagnóstico: abrir o GitHub, achar o workflow, identificar o job que falhou e ler os logs. Quando isso se repete dezenas de vezes por semana, a equipe passa a ignorar os alertas — e aí o canal deixa de funcionar justamente na hora em que a falha importa. Alerta ruidoso não é neutro: ele treina o time a não olhar.

As quatro perguntas são o quê falhou, onde falhou, quem causou e o que fazer a seguir. No payload elas aparecem, respectivamente, em: Job Falhou com ${{ github.job }}; Repositório e Branch com ${{ github.repository }} e ${{ github.ref_name }}; Autor do Commit com ${{ github.actor }}, complementado pela mensagem do commit; e o bloco actions com o botão "Ver Pipeline" apontando para actions/runs/${{ github.run_id }}, que é o próximo passo concreto.

Exercício 2

O artigo configura três notificações diferentes para o mesmo pipeline. Descreva o gatilho, o destino e a razão de cada uma — e explique por que o alerta de falha em produção usa <!channel> enquanto o de teste em PR menciona apenas <@autor>.

Ver resposta

✓ Resposta: São três combinações de contexto:

  • Falha de teste em PRif: failure() && github.event_name == 'pull_request', vai para o canal dos desenvolvedores mencionando <@${{ github.actor }}>. É um problema de uma pessoa só, e ela ainda está trabalhando naquilo.
  • Deploy bem-sucedidoif: success(), vai para o canal geral. É informação de interesse amplo (a versão está no ar), mas não exige ação de ninguém.
  • Falha em produçãoif: failure(), vai para o canal de alertas com <!channel>. Aqui há impacto em usuários e alguém precisa agir agora.

A diferença entre <!channel> e <@autor> é exatamente a dosagem do incômodo. O <!channel> notifica todo mundo do canal, inclusive fora do horário — é um recurso caro, que perde o efeito se for gasto em toda falha de teste. Um teste que quebrou num PR não justifica acordar a equipe; produção fora do ar justifica. É o mesmo princípio do exercício anterior aplicado ao alcance: notificar todos sempre é tão ruim quanto não notificar.

Exercício 3

Qual a diferença entre escrever em $GITHUB_ENV e em $GITHUB_OUTPUT? E o que ::group:: e ::error file=src/auth.js,line=42:: produzem na interface do GitHub?

Ver resposta

✓ Resposta: $GITHUB_ENV define uma variável de ambiente que passa a existir nos steps seguintes do mesmo job — dali em diante ela é lida como $BUILD_VERSION, como qualquer variável de shell. $GITHUB_OUTPUT define um output identificado do step, referenciado como ${{ steps.<id>.outputs.<nome> }}; é o mecanismo que permite declarar outputs no job e passar valores para outros jobs, via needs. Resumindo: GITHUB_ENV é ambiente e não atravessa jobs; GITHUB_OUTPUT é dado nomeado e atravessa.

O ::group:: cria uma seção recolhível no log (fechada por ::endgroup::), o que mantém a saída navegável em pipelines longos. Já ::error file=src/auth.js,line=42:: gera uma anotação: além de aparecer em vermelho no log, o GitHub a exibe ancorada naquele arquivo e linha, inclusive sobre o diff do pull request — quem revisa vê o erro no ponto exato do código, sem abrir o log.

Exercício 4

O step que calcula a duração do pipeline usa if: always(). O que aconteceria se ele usasse if: success(), e por que isso comprometeria justamente o objetivo de acompanhar métricas?

Ver resposta

✓ Resposta: Com if: success() o step só rodaria quando todos os anteriores tivessem passado — ou seja, nenhuma execução com falha seria medida. A métrica ci.pipeline.duration enviada ao Datadog passaria a conter apenas os casos de sucesso.

Isso enviesa exatamente o dado que interessa. Pipelines que falham costumam ter perfil de tempo diferente — ou muito curtos, porque quebraram logo no início, ou muito longos, porque travaram até estourar timeout. Ao descartá-los, a série histórica passa a mostrar uma saúde melhor do que a real, e a tag status:${{ job.status }} perde o sentido, já que só existiria com um valor. O always() garante que a medição aconteça em qualquer desfecho, que é a condição para comparar sucesso e falha ao longo do tempo.

Lembrando que always() é também o que permite ao step de notificação com job.status reportar tanto ✅ quanto ❌ — a mesma razão.

Exercício 5

O script de deploy começa com set -euo pipefail. Explique o que cada uma das três opções faz e descreva uma falha concreta que passaria despercebida se a linha fosse removida.

Ver resposta

✓ Resposta: -e encerra o script no primeiro comando que retornar status diferente de zero. -u transforma o uso de variável não definida em erro, em vez de substituí-la por string vazia. -o pipefail faz um pipeline retornar o status do primeiro comando que falhou, e não o do último da cadeia.

Sem essa linha, um caso concreto: se docker pull "$IMAGE" falhasse — registro fora do ar, tag inexistente — o script seguiria adiante e reiniciaria o container com a imagem antiga que já estava em cache. O deploy terminaria com status de sucesso, a notificação anunciaria "versão no ar" e a versão nova simplesmente não teria sido publicada. É a pior categoria de falha: silenciosa e com aparência de êxito.

Sem -u, um erro de digitação em $DEPLOY_HOST viraria string vazia e o nc -zv "" 22 falharia com uma mensagem confusa, em vez de apontar a variável ausente. Sem pipefail, algo como comando_que_falha | tee deploy.log devolveria o status do tee — quase sempre zero — mascarando a falha real mesmo com o -e ligado.

Comentários

Mais em DevOps

Escrevendo um Dockerfile do Zero
Escrevendo um Dockerfile do Zero

Escrevendo um Dockerfile do zero: o papel de cada instrução, por que a ordem…

Ansible: Configurando Servidores de Forma Declarativa
Ansible: Configurando Servidores de Forma Declarativa

Configuração declarativa de servidores por SSH, sem agente instalado: o…

O Que é CI/CD e Por Que Sua Empresa Precisa Disso
O Que é CI/CD e Por Que Sua Empresa Precisa Disso

O que é CI/CD e por que ele existe: o integration hell que o antecedeu, a…