MÓDULO 10 · CONCEITO 12 DE 12

Debugging Orientado a Dados

Investigação estruturada com métricas, traces, logs e profiling — exemplars como ponte, RCA com dados e a mentalidade de observabilidade-first

Tempo de leitura ~20 min Pré-requisito M10C04 — Distributed Tracing Próximo Módulo 11 — Segurança

A Mudança de Mentalidade

Debugging tradicional é SSH no servidor, tail nos logs, adicionar print statements. Funciona para sistemas simples e problemas reproduzíveis. Em sistemas distribuídos com dezenas de serviços, esse approach falha: você não sabe em qual dos 50 pods o problema está, os logs já foram rotacionados, e o problema acontece em 0.1% dos requests sob condições específicas que você não consegue reproduzir localmente.

Debugging orientado a dados é a mudança de paradigma: o sistema já contém toda a informação necessária para diagnosticar qualquer problema — traces, logs com trace_id, métricas com exemplars, profiling histórico. O trabalho do engenheiro é saber como navegar esses dados e formular hipóteses testáveis a partir deles. Você não adiciona observabilidade após detectar um problema — você já tem a observabilidade necessária antes do problema ocorrer.

Observability-driven development Charity Majors (Honeycomb) formulou o princípio: antes de fazer um deploy, você deve conseguir responder "como vou saber se essa mudança está funcionando corretamente em produção?". Se você não consegue responder, adicione a instrumentação antes do deploy — não depois. A observabilidade é parte do Definition of Done, não um add-on pós-lançamento.

Fluxo Estruturado de Investigação

Em vez de investigação ad-hoc e intuitiva, um fluxo estruturado reduz o tempo de diagnóstico e garante que você não pula etapas importantes. O fluxo tem cinco fases sequenciais — cada fase reduz o espaço de possibilidades até a causa raiz.

Fase 1 — Caracterizar o Problema

Antes de mergulhar em traces individuais, caracterize o problema: qual serviço? qual endpoint? qual percentil de usuários é afetado? Desde quando? Correlaciona com algum deploy?

# Perguntas que as métricas respondem na fase 1:

# 1. Qual serviço está afetado?
# Dashboard de golden signals — erro rate ou latência anormal?
increase(http_requests_total{status=~"5.."}[15m]) by (service)
# Serviço com maior aumento de erros nos últimos 15min

# 2. Qual endpoint específico?
sum(rate(http_requests_total{status=~"5..", service="order-service"}[5m]))
  by (http_route)
# Agrupa por route — identifica se é um endpoint específico ou global

# 3. Qual percentil é afetado? Latência?
histogram_quantile(0.99,
  sum(rate(http_request_duration_seconds_bucket{service="order-service"}[5m]))
  by (le, http_route)
)
# P99 por endpoint — discrimina "tudo lento" de "alguns endpoints lentos"

# 4. Correlaciona com deploy? (via anotações no Grafana)
# changes() ou increase() com corte no timestamp do deploy
# blast radius por versão de app:
sum(rate(http_requests_total{status=~"5.."}[5m])) by (service_version)

Fase 2 — Identificar Traces Representativos

Com o problema caracterizado, você precisa de traces concretos para investigar. Duas rotas:

# Grafana Explore — buscar traces com erro em janela de incidente
{service.name="order-service"} | status=ERROR

# No Tempo com TraceQL:
{ status = error && resource.service.name = "order-service" }
  | select(span.http.target, duration, rootSpan)
# ordenar por duration desc — ver os piores casos primeiro

# Filtrar por atributo de negócio (ex: só um tenant específico)
{ resource.service.name = "order-service"
  && span.tenant_id = "acme-corp" }
  | select(duration, span.order.failure_reason)

Fase 3 — Analisar o Trace

Com um trace em mãos, leia a cascata de spans top-down:

  1. Critical path: qual caminho de spans determina a latência total?
  2. Span mais lento: qual span filho tem a maior duração absoluta? É o bottleneck?
  3. Gap entre spans: há tempo "perdido" entre o fim de um span filho e o início do próximo? (serialização, thread scheduling, network hop não instrumentado)
  4. Span com erro: qual span tem status=Error? Qual a mensagem? O erro se propaga para o span pai?
  5. Span events: há eventos de retry, cache miss, ou exceção capturada dentro de spans?

Fase 4 — Correlacionar com Logs

Do trace, navegue para os logs do mesmo Trace ID no mesmo período. Os logs revelam contexto que os spans não capturam: mensagens de erro detalhadas, stack traces completas, decisões de lógica de negócio.

# Loki — buscar logs pelo trace_id de um span específico
{service="order-service"} | json | trace_id="4bf92f3577b34da6a3ce929d0e0e4736"

# Filtrar por contexto adicional:
{service="order-service"}
  | json
  | trace_id="4bf92f3577b34da6a3ce929d0e0e4736"
  | level="error"
  | line_format "{{.timestamp}} [{{.level}}] {{.message}} — err={{.error}}"

# Grafana: link automático trace → logs via Derived Fields
# (configurar no datasource Loki: campo trace_id → link para Tempo)

Fase 5 — Hipótese e Validação

Com o evidence do trace + logs, forme uma hipótese específica e falsificável: "o problema é que a query de banco está fazendo full-table-scan porque o índice no campo order_status foi removido no último deploy". Valide a hipótese com mais dados:

Correlação não é causalidade "O deploy aconteceu às 14:30 e o erro começou às 14:32" é uma correlação forte — mas pode ser coincidência (tráfego aumentou, um job pesado iniciou, a quota do banco atingiu o limite). Antes de declarar causa raiz, refute a hipótese: o que seria observável se não fosse o deploy? Existe outro evento simultâneo que explica o mesmo padrão?

Exemplars — A Ponte entre Métricas e Traces

Exemplars são amostras de traces embutidas nas métricas Prometheus. Quando um histograma é incrementado, você pode opcionalmente incluir o Trace ID do request que gerou aquele incremento. O Prometheus armazena esse Trace ID junto com o valor da amostra, e o Grafana exibe esses exemplars como diamantes clicáveis na série temporal — clicar abre o trace correspondente no Tempo.

Sem exemplars, o fluxo de debug é: ver anomalia na métrica → ir ao backend de traces → buscar manualmente por traces lentos/com erro naquele período → filtrar por serviço e timerange → escolher um trace candidato. Com exemplars: ver diamante no pico da métrica → clicar → o trace que estava exatamente no P99 naquele instante abre automaticamente. A busca manual é eliminada.

// C# — prometheus-net captura exemplars automaticamente
// do Activity.Current (span OTel ativo) quando UseHttpMetrics() está configurado

// Para instrumentação manual com exemplar:
private static readonly Histogram _orderDuration = Metrics.CreateHistogram(
    "order_processing_duration_seconds",
    "Duration of order processing",
    new HistogramConfiguration
    {
        Buckets = Histogram.ExponentialBuckets(0.001, 2, 15)
    });

public async Task<Order> ProcessOrderAsync(CreateOrderRequest req)
{
    using var timer = _orderDuration.NewTimer();
    // Exemplar é capturado automaticamente de Activity.Current ao Dispose()
    // O trace_id e span_id do span OTel ativo são embutidos na amostra
    return await DoProcessAsync(req);
}

// Prometheus — habilitar armazenamento de exemplars
# --enable-feature=exemplar-storage
# kube-prometheus-stack values.yaml:
# prometheusSpec:
#   enableFeatures:
#     - exemplar-storage
# Grafana — ativar exemplars no painel
# Edit panel → Query → selecionar "Enable exemplars"
# Os diamantes aparecem automaticamente se o Prometheus tem exemplars

histogram_quantile(0.99,
  sum(rate(http_request_duration_seconds_bucket[5m])) by (le, service)
)
# Diamante no pico das 14:32 → clique → trace do P99 naquele instante

# Grafana Explore — modo dual: lado esquerdo Prometheus, lado direito Tempo
# Split view permite navegar trace sem sair do gráfico de métricas

Root Cause Analysis com Dados

RCA estruturado com dados de observabilidade é mais rápido e mais preciso do que RCA baseado em intuição ou experiência passada. O processo:

1. Timeline de eventos

Construa uma timeline precisa com dados — não "acho que o problema começou lá pelas 14h" mas "às 14:32:07 UTC o error rate do order-service cruzou 1%, conforme increase(http_requests_total{status=~"5.."}[5m]). Às 14:29:52 UTC houve um deploy, conforme anotação no Grafana". Timestamps exatos eliminam ambiguidade e permitem que outra pessoa reproduza a investigação.

2. Blast radius

Qual é o escopo do problema? Todos os usuários? Apenas uma região? Apenas um tenant? Apenas chamadas a partir de um serviço específico? O escopo frequentemente aponta diretamente para a causa — um problema regional sugere infraestrutura; um problema em um tenant específico sugere dados ou configuração.

# Erro rate por região — identificar se é regional
sum(rate(http_requests_total{status=~"5.."}[5m])) by (cloud_region)

# Erro rate por versão de app — identificar se é específico de um deploy
sum(rate(http_requests_total{status=~"5.."}[5m])) by (service_version)

# Erro rate por tenant (alta cardinalidade — use logs ou Honeycomb, não Prometheus)
{service="order-service"} | json | status >= 500
  | count by (tenant_id)  # LogQL — baixo custo, alta cardinalidade ok

# Traçar a origem no trace — qual span filho falhou primeiro?
{ status = error && resource.service.name = "order-service" }
  | select(span.db.statement, span.http.url, rootSpan.name)

3. Mudanças recentes

A maioria dos incidentes em produção é causada por uma mudança recente: deploy de código, mudança de configuração, migração de banco, escalonamento de infra, mudança de dependência externa. Com Grafana, adicione anotações de deploy automaticamente via CI/CD — elas aparecem como linhas verticais nos dashboards, correlacionando visualmente eventos de deploy com mudanças nas métricas.

# Grafana API — criar anotação de deploy via CI/CD (GitHub Actions, etc.)
curl -X POST \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $GRAFANA_API_TOKEN" \
  -d '{
    "tags": ["deploy", "order-service"],
    "text": "Deploy order-service v1.2.0 (PR #1842)",
    "time": '"$(date +%s000)"'
  }' \
  http://grafana.internal/api/annotations

# No dashboard: View → Annotations → filtrar por tag "deploy"
# Linha vertical aparece no exato timestamp do deploy

4. Evidence trail no postmortem

Um bom postmortem documenta o evidence trail: links para os traces específicos que foram analisados, screenshots dos dashboards no momento do incidente, as queries PromQL exatas que confirmaram a hipótese. Isso serve dois propósitos: (1) validação — qualquer membro do time pode reproduzir a investigação e verificar a conclusão; (2) treinamento — engenheiros menos experientes aprendem a investigar seguindo o evidence trail de incidentes passados. O RCA só é considerado completo quando o evidence trail permite reprodutibilidade independente.

Instrumentação para Debugging por Linguagem

C# — Spans de negócio com atributos de debugging
// Padrão: enriquecer spans com dados de negócio suficientes
// para debugging sem necessidade de logs adicionais

public class OrderService
{
    private static readonly ActivitySource _tracer = new("OrderService");
    private readonly ILogger<OrderService> _logger;

    public async Task<OrderResult> ProcessAsync(
        OrderRequest req, CancellationToken ct)
    {
        using var activity = _tracer.StartActivity("ProcessOrder");
        activity?.SetTag("order.customer_id", req.CustomerId);
        activity?.SetTag("order.item_count", req.Items.Count);
        activity?.SetTag("order.total_cents", req.TotalCents);
        activity?.SetTag("order.payment_method", req.PaymentMethod);
        // NÃO: dados sensíveis (PAN, CVV, SSN, senha)

        _logger.LogInformation(
            "Processing order {OrderId} for customer {CustomerId}",
            req.OrderId, req.CustomerId);
        // ILogger com OTel bridge injeta trace_id automaticamente

        try
        {
            var inv = await _inventory.CheckAsync(req.Items, ct);

            activity?.AddEvent(new ActivityEvent("inventory.checked",
                tags: new ActivityTagsCollection
                {
                    ["inventory.available"] = inv.AllAvailable,
                    ["inventory.missing_count"] = inv.MissingCount,
                }));

            if (!inv.AllAvailable)
            {
                activity?.SetTag("order.failure_reason", "insufficient_inventory");
                activity?.SetStatus(ActivityStatusCode.Error,
                    "Insufficient inventory");
                return OrderResult.InsufficientInventory(inv);
            }

            var payment = await _payments.ChargeAsync(req, ct);
            activity?.SetTag("payment.transaction_id", payment.TransactionId);
            return OrderResult.Success(payment);
        }
        catch (TimeoutException ex)
        {
            activity?.SetTag("order.failure_reason", "timeout");
            activity?.RecordException(ex);
            activity?.SetStatus(ActivityStatusCode.Error, "Timeout");
            _logger.LogError(ex,
                "Timeout processing order {OrderId}", req.OrderId);
            throw;
        }
    }
}

Atributo order.failure_reason permite no Tempo/Jaeger filtrar por tipo de falha sem ler logs individuais — TraceQL: { span.order.failure_reason = "timeout" }.

Python — structlog com contexto persistente
# Padrão: structlog.bind() para correlação global no contexto
# + span events para checkpoints de decisão

import structlog
from opentelemetry import trace
from contextvars import ContextVar

correlation_id: ContextVar[str] = ContextVar("correlation_id", default="")
logger = structlog.get_logger()

async def process_order(req: OrderRequest) -> OrderResult:
    tracer = trace.get_tracer("order_service")
    with tracer.start_as_current_span("ProcessOrder",
        attributes={
            "order.customer_id": req.customer_id,
            "order.item_count": len(req.items),
            "order.total_cents": req.total_cents,
            "order.payment_method": req.payment_method,
        }) as span:

        # Todos os logs desta função terão esses campos — sem passar explicitamente
        log = logger.bind(
            order_id=str(req.order_id),
            customer_id=req.customer_id,
            correlation_id=correlation_id.get(),
        )

        log.info("processing.started")

        try:
            inventory = await check_inventory(req.items)
            span.add_event("inventory.checked", attributes={
                "inventory.available": inventory.all_available,
                "inventory.missing": len(inventory.missing_items),
            })

            if not inventory.all_available:
                span.set_attribute("order.failure_reason",
                    "insufficient_inventory")
                span.set_status(trace.StatusCode.ERROR,
                    "Insufficient inventory")
                log.warning("processing.failed",
                    reason="insufficient_inventory",
                    missing=[str(i) for i in inventory.missing_items])
                return OrderResult.insufficient_inventory(inventory)

            payment = await charge_payment(req)
            span.set_attribute("payment.transaction_id",
                payment.transaction_id)
            log.info("processing.completed",
                transaction_id=payment.transaction_id)
            return OrderResult.success(payment)

        except asyncio.TimeoutError as e:
            span.set_attribute("order.failure_reason", "timeout")
            span.record_exception(e)
            span.set_status(trace.StatusCode.ERROR, "Timeout")
            log.error("processing.timeout", exc_info=True)
            raise

structlog.bind() adiciona campos ao logger para toda a execução do contexto — todos os logs emitidos durante o processamento da ordem terão order_id e customer_id sem passá-los explicitamente em cada chamada.

Go — context propagation com slog e OTel
// Padrão: context carrega span OTel + logger com campos de negócio
// slog com traceHandler injeta trace_id automaticamente em todo log

package order

import (
    "context"
    "fmt"
    "log/slog"

    "go.opentelemetry.io/otel"
    "go.opentelemetry.io/otel/attribute"
    "go.opentelemetry.io/otel/codes"
    "go.opentelemetry.io/otel/trace"
)

var tracer = otel.Tracer("order_service")

func (s *Service) Process(
    ctx context.Context, req OrderRequest) (*OrderResult, error) {

    ctx, span := tracer.Start(ctx, "ProcessOrder",
        trace.WithAttributes(
            attribute.String("order.customer_id", req.CustomerID),
            attribute.Int("order.item_count", len(req.Items)),
            attribute.Int64("order.total_cents", req.TotalCents),
            attribute.String("order.payment_method", req.PaymentMethod),
        ),
    )
    defer span.End()

    log := slog.Default().With(
        slog.String("order_id", req.OrderID.String()),
        slog.String("customer_id", req.CustomerID),
    )

    log.InfoContext(ctx, "processing.started")

    inventory, err := s.inventory.Check(ctx, req.Items)
    if err != nil {
        span.RecordError(err)
        span.SetStatus(codes.Error, err.Error())
        log.ErrorContext(ctx, "inventory.check.failed", slog.Any("error", err))
        return nil, fmt.Errorf("inventory check: %w", err)
    }

    span.AddEvent("inventory.checked", trace.WithAttributes(
        attribute.Bool("inventory.available", inventory.AllAvailable),
        attribute.Int("inventory.missing", len(inventory.Missing)),
    ))

    if !inventory.AllAvailable {
        span.SetAttributes(
            attribute.String("order.failure_reason", "insufficient_inventory"),
        )
        span.SetStatus(codes.Error, "insufficient inventory")
        log.WarnContext(ctx, "processing.failed",
            slog.String("reason", "insufficient_inventory"),
            slog.Int("missing_count", len(inventory.Missing)))
        return &OrderResult{Status: StatusInsufficientInventory}, nil
    }

    payment, err := s.payments.Charge(ctx, req)
    if err != nil {
        span.RecordError(err)
        span.SetStatus(codes.Error, err.Error())
        log.ErrorContext(ctx, "payment.failed", slog.Any("error", err))
        return nil, fmt.Errorf("payment: %w", err)
    }

    span.SetAttributes(
        attribute.String("payment.transaction_id", payment.TransactionID),
    )
    log.InfoContext(ctx, "processing.completed",
        slog.String("transaction_id", payment.TransactionID))

    return &OrderResult{Status: StatusSuccess, Payment: payment}, nil
}

Passar ctx em todas as chamadas é o contrato fundamental do Go — carrega o span OTel e o logger com campos de contexto através de toda a cadeia de chamadas, sem variáveis globais.

Observabilidade como Skill de Engenheiro Sênior

Um engenheiro sênior não apenas usa as ferramentas de observabilidade — ele toma decisões que tornam o sistema observável desde o início. Isso se manifesta em code reviews ("esse novo endpoint não tem instrumentação de latência"), em design decisions ("vamos precisar rastrear o tenant_id nesse fluxo para debugging futuro"), e em definição de critérios de pronto ("DoD inclui: novos fluxos têm spans de negócio com atributos suficientes para debugging em produção").

Dimensões de Maturidade em Observabilidade

A pergunta de ouro Antes de fazer um deploy: "Se esse código causar um bug sutil em 0.1% dos requests, como eu vou saber? Quanto tempo vou levar para encontrar a causa raiz?" Se a resposta é "horas ou dias", a instrumentação é insuficiente. Se a resposta é "minutos — vou ver o trace, o span de negócio vai mostrar o valor do campo que causou o problema", você está no nível certo de observabilidade.

Decisões de Engenharia

Quanto contexto de negócio colocar nos spans?

Suficiente para responder "qual request causou este problema?" sem precisar buscar nos logs. Para uma ordem: order_id, customer_id, total_cents, payment_method. Não: dados de cartão, PAN, CVV, SSN, senha. O princípio: se você precisaria de SSH nos logs para encontrar um dado que te ajudaria a debugar, considere adicioná-lo ao span — com cuidado sobre privacidade e custo de cardinaldade. Spans com alta cardinaldade (user_id em sistemas de milhões de usuários) funcionam em Honeycomb/Tempo, mas não como labels do Prometheus.

Como equilibrar volume de logs vs custo de storage?

Logs de sucesso de alta frequência (HTTP 200, cache hits) têm baixíssimo valor de debugging — amostre 1-5%. Logs de erro e warning: sempre 100%. Logs de operações de negócio críticas (pagamento, criação de conta): sempre. Logs de debug em produção: nunca (use span events para pontos de decisão dentro de spans). Filtragem no Fluent Bit antes da ingestão é mais eficaz que filtrar no backend — bytes não ingeridos custam zero.

Span de negócio manual vs confiar na auto-instrumentation?

Auto-instrumentation cobre: HTTP handling, DB queries, cache calls, gRPC — o "esqueleto" do trace. Adicione span manual quando: a operação tem semântica de negócio (ProcessOrder, CalculateDiscount, ValidateRisk), quando você quer atributos de negócio que a auto-instrumentation não captura, ou quando a operação agrega múltiplas chamadas que você quer ver juntas no trace. Não adicione span para cada função individual — ruído sem valor de debugging.

Como priorizar investimento em observabilidade?

Comece pelos serviços no critical path do usuário — os que, se falharem, causam impacto direto. Instrumentação básica (golden signals + logs estruturados) tem o maior ROI por hora investida. Profiling contínuo e correlação avançada são investimentos de segundo estágio, quando o debugging de primeiro estágio se torna o bottleneck. Um serviço interno sem SLA crítico não precisa do mesmo nível de observabilidade que o serviço de checkout.

Perguntas de Entrevista

    Como você usaria o stack de observabilidade para investigar um aumento de latência P99 que apareceu às 14h de ontem?

    Investigação estruturada em cinco fases: (1) Confirmar o escopo: acessar o dashboard de golden signals no Grafana, ajustar o time range para 12h–16h de ontem. Confirmar qual serviço, qual endpoint, qual percentil. P50 normal mas P99 alto sugere long tail, não degradação geral — buscar por outliers, não degradação média. (2) Identificar o trace representativo: na série temporal do P99, procurar exemplars (diamantes) — clicar no exemplar no pico de latência para abrir o trace correspondente no Tempo. Sem exemplars: buscar no Tempo por { resource.service.name = "order-service" } | select(duration) | sort(desc) no time range. (3) Analisar o trace: identificar o span mais lento no critical path — DB? Cache miss? Chamada externa? (4) Correlacionar com logs: do trace, pegar o trace_id e buscar no Loki. Procurar warnings, erros, ou mensagens que expliquem o comportamento lento. (5) Verificar correlação com deploy: houve deploy às 13h? A anotação no Grafana deve mostrar. Se sim, comparar traces before/after do deploy para isolar a mudança.

    O que são exemplars e como eles funcionam como ponte entre métricas e traces?

    Exemplars são amostras de traces embutidas em métricas Prometheus. Quando você incrementa um histograma, pode incluir opcionalmente o Trace ID do request que causou aquele incremento. O Prometheus armazena esse Trace ID junto com o valor da amostra. No Grafana, quando você visualiza um histograma com exemplars habilitados, o Grafana exibe diamantes na série temporal — cada diamante representa um exemplar. Clicar no diamante abre o trace correspondente no Tempo.

    Por que é poderoso: sem exemplars, você vê "P99 foi de 200ms para 800ms às 14:30" e precisa ir ao backend de traces e buscar manualmente por traces lentos naquele período. Com exemplars, um clique no pico do gráfico abre diretamente o trace que estava no P99 naquele momento exato — sem busca manual. É a ponte que elimina o gap entre "eu vejo uma anomalia na métrica" e "eu tenho o trace específico que causou essa anomalia". Para funcionar: o servidor Prometheus precisa de --enable-feature=exemplar-storage, e o Grafana precisa com "Enable exemplars" no panel.

    Como você instrumentaria uma nova feature para garantir que problemas sejam detectáveis em produção?

    Antes do deploy, respondo: "se este código falhar em 0.1% dos requests, como vou saber e quanto tempo leva para encontrar a causa?" Com isso em mente: (1) Span de negócio: criar um span para a operação principal, com atributos de negócio suficientes — IDs, valores, parâmetros de decisão; (2) failure_reason: em cada caminho de erro, adicionar um atributo ao span identificando o tipo de falha, para filtrar por tipo de erro no backend de traces sem ler logs individualmente; (3) Span events para checkpoints: pontos de decisão, cache misses, retries; (4) Counter de negócio: se a feature tem uma taxa de sucesso/falha de negócio, adicionar um Counter com labels de resultado; (5) Log correlacionado: garantir que logs têm o trace_id injetado; (6) Alerta de sanity: se a feature muda comportamento de uma métrica existente, adicionar alerta de burn rate. O critério de pronto inclui: após o deploy, verificar no Grafana/Tempo que spans aparecem com atributos corretos em requests reais.

    Qual a diferença de debugging em um monolito vs sistema distribuído, e como a observabilidade muda a abordagem?

    Em um monolito: quando um erro acontece, o stack trace completo está disponível no processo, o debugger pode ser anexado, os logs estão em um único lugar, a causalidade é local (função A chamou função B). Debugging é relativamente direto: reproduzir localmente, adicionar breakpoint, inspecionar estado.

    Em um sistema distribuído: uma request passa por 5-10 serviços; o stack trace de um serviço não mostra o que aconteceu nos outros; falhas podem ser causadas por latência de rede, retries assíncronos, ou timeouts em cascata. Reproduzir localmente é muitas vezes impossível (o bug só ocorre com tráfego real e estado de banco específico). A causalidade é distribuída — o erro em um serviço pode ser causado por um timeout em outro que iniciou o problema.

    Como a observabilidade muda a abordagem: traces substituem stack traces (mostram causalidade entre serviços com timestamps precisos); exemplars substituem breakpoints (permitem encontrar o request específico que falhou sem reproduzir o problema); TraceQL substitui inspeção de estado local (permite filtrar por atributos de negócio across todo o sistema). O paradigma muda de "reproduce and debug" para "observe and diagnose" — o sistema em produção é o único lugar onde o bug existe.

    Como você garantiria que o postmortem de um incidente serve como material de treinamento eficaz para o time?

    Um postmortem eficaz como material de treinamento precisa de um evidence trail reproduzível: qualquer pessoa do time deve conseguir seguir a investigação e chegar à mesma conclusão usando os dados disponíveis. Isso requer: (1) Links para traces específicos — não "analisamos um trace lento", mas um link para o trace ID exato no Tempo/Jaeger; (2) Queries PromQL/LogQL exatas que confirmaram a hipótese, com o time range do incidente; (3) Screenshots dos dashboards no momento do incidente, pois os dados podem expirar; (4) Timeline com timestamps exatos de cada evento e descoberta — "14:32:07 UTC error rate cruzou 1%, identificado pela métrica X; 14:45:22 UTC trace ID Y mostrou full-table-scan no span de DB"; (5) Hipóteses descartadas e por quê — mostrar o raciocínio de eliminação é tão valioso quanto mostrar o que funcionou; (6) Seção de aprendizados de debugging separada da seção de ações corretivas — "como detectamos esse problema mais rápido na próxima vez" vs "como evitamos que o problema aconteça de novo".

Como Praticar

  1. Exercite o fluxo completo de correlação metrics → exemplar → trace → logs. Configure um serviço simples com prometheus-net (C#), opentelemetry-go ou opentelemetry-python, expondo um histograma com exemplars. Faça requests e encontre o trace do P99 via clique no exemplar no Grafana, sem nenhuma busca manual no Tempo.
    Critério: Trace encontrado via clique no diamante (exemplar), não via busca no Jaeger/Tempo; o trace aberto tem o trace_id correspondente à amostra da métrica; no mesmo trace, é possível navegar para os logs Loki pelo trace_id via Derived Fields.
  2. Simule uma regressão de P99 e use o fluxo de investigação para identificar a causa. Implante duas versões de um serviço: a primeira saudável, a segunda com um delay artificial em 2% dos requests. Usando apenas ferramentas de observabilidade (sem inspecionar o código), identifique a versão problemática e o tipo de lentidão.
    Critério: Causa identificada (versão com delay) em menos de 15 minutos; blast radius confirmado via label service_version na métrica; trace do P99 mostra o span com delay anômalo; nenhum acesso ao código-fonte durante a investigação.
  3. Instrumente uma feature nova seguindo o princípio observability-first. Antes de escrever qualquer lógica de negócio, defina: quais atributos o span principal terá, qual será o failure_reason de cada caminho de erro, quais span events marcarão checkpoints. Depois, em produção (ou staging), confirme que a instrumentação é suficiente para responder "qual customer_id gerou o erro?" apenas a partir do trace.
    Critério: Pergunta "qual customer_id gerou o erro?" respondível em menos de 2 minutos usando apenas o trace, sem abrir logs; failure_reason distingue pelo menos 3 caminhos de erro diferentes; span events visíveis para pelo menos 2 checkpoints de decisão.
  4. Configure anotações automáticas de deploy no Grafana via CI/CD. Adicione um step no pipeline (GitHub Actions ou similar) que faz POST para a API de anotações do Grafana após cada deploy bem-sucedido, com tags de serviço e versão. Valide que a anotação aparece nos dashboards de golden signals no timestamp correto.
    Critério: Anotação visível no dashboard como linha vertical no timestamp exato do deploy; tag de serviço filtrável no painel de anotações do Grafana; em um deploy intencional com regressão, a correlação visual entre a anotação e a mudança na métrica é imediata.
  5. Escreva um postmortem com evidence trail completo para um incidente simulado. Simule um incidente (ex: um serviço de banco de dados com alta latência por 30 minutos), investigue usando o fluxo de cinco fases, e documente o postmortem incluindo: links para traces específicos, queries PromQL exatas, timeline com timestamps, hipóteses descartadas.
    Critério: Outro engenheiro do time consegue reproduzir a investigação seguindo os links e queries do postmortem sem assistência; a timeline tem timestamps em UTC com granularidade de segundos; pelo menos uma hipótese descartada está documentada com a evidência que a refutou.

Referências

  1. book Majors, Fong-Jones, Miranda — Observability Engineering O'Reilly, 2022. O livro definitivo sobre observabilidade em sistemas distribuídos: o caso para high-cardinality events, como explorar dados de produção, e a mentalidade de observabilidade-first. Base conceitual para este conceito.
  2. blog Charity Majors — Observability is a many-splendored thing charity.wtf. Ensaio fundacional que distingue observabilidade de monitoring clássico, introduz o papel do cardinality-first event e explica por que o modelo de três pilares (logs/metrics/traces) é incompleto sem a perspectiva do evento.
  3. blog Honeycomb — Observability-Driven Development Blog da Honeycomb. Define ODD como uma prática de desenvolvimento onde instrumentação é adicionada antes do deploy, e a produção é o ambiente de debugging primário — não apenas staging. Introduz a "pergunta de ouro" antes de cada deploy.
  4. docs Google SRE Workbook — Debugging Simple Memory Leaks sre.google. Capítulo de debugging do Google SRE Workbook: metodologia de investigação estruturada, formação de hipóteses testáveis, e uso de dados de observabilidade para RCA. Aplicável além de memory leaks.
  5. paper Sigelman et al. — Dapper, a Large-Scale Distributed Systems Tracing Infrastructure Google, 2010. O paper original que definiu distributed tracing como disciplina: propagação de contexto, sampling, span model. A base conceitual para Jaeger, Zipkin, Tempo e OpenTelemetry.
  6. article Peter Bourgon — Metrics, tracing, and logging peter.bourgon.org, 2017. O artigo que cunhou o modelo dos "três pilares da observabilidade" e definiu como métricas, traces e logs se complementam — quando usar cada um e onde cada sinal tem vantagem comparativa.
  7. docs Prometheus — Exemplars (OpenMetrics) Documentação oficial do Prometheus sobre exemplars: formato OpenMetrics, configuração de --enable-feature=exemplar-storage, limites de armazenamento, e como o Grafana os consome para navegação trace-from-metric.
  8. book Alex Hidalgo — Implementing Service Level Objectives O'Reilly, 2020. Guia prático de SLOs, SLIs e error budgets: como definir SLIs mensuráveis, como usar error budget para decisões de priorização de debugging vs features, e como SLOs mudam a cultura de incidentes.
  9. article Cindy Sridharan — Distributed Systems Observability O'Reilly (free report), 2018. Uma das primeiras sínteses acessíveis dos três pilares em contexto de sistemas cloud-native: como logs estruturados, métricas e traces funcionam juntos, e as limitações de cada abordagem isolada.
  10. docs Grafana Tempo — TraceQL Documentação do Tempo sobre TraceQL: sintaxe de busca por atributos de span, filtros por duração e status, agregações de traces, e como usar para blast radius analysis filtrando por service_version ou tenant_id.
  11. article Honeycomb — What is Observability-Driven Development? Honeycomb.io. Aprofunda o princípio de ODD com exemplos práticos: como formular a "pergunta de ouro" antes de cada deploy, o papel dos span events como substituto de logs de debug, e como usar produção como ambiente de debugging primário.
  12. blog Grafana Labs — Correlating Logs, Metrics, and Traces Blog da Grafana Labs sobre correlação entre os três sinais na stack LGTM: Derived Fields no Loki para trace-to-logs, exemplars para metric-to-trace, e trace-to-metrics no Tempo. Referência técnica para configurar a correlação end-to-end.