MÓDULO 09 · CONCEITO 14 DE 14

Observabilidade em Comunicação Distribuída

Distributed tracing, correlação entre serviços, métricas de comunicação e debugging em sistemas assíncronos

Tempo de leitura ~25 min Pré-requisito Todos os conceitos anteriores do Módulo 09 Próximo Módulo 10 — Bancos de Dados em Sistemas Distribuídos

Em um monólito, um bug tem um stack trace. Em um sistema distribuído, a mesma operação atravessa dez serviços, três filas de mensagem, dois bancos de dados e um cache. O stack trace de cada serviço individual conta apenas um capítulo da história. Observabilidade em sistemas distribuídos é a capacidade de responder "o que aconteceu com a requisição X?" olhando os dados coletados — sem precisar reproduzir o bug ou adicionar instrumentação depois do fato.

Os três pilares clássicos de observabilidade são logs, métricas e traces. Em comunicação distribuída, os três precisam estar correlacionados: um trace ID que conecta o log do Order Service ao log do Payment Service ao trace do Kafka consumer. Sem correlação, você tem três conjuntos de dados independentes que não contam a mesma história.

Distributed Tracing — conceitos fundamentais

Um trace representa a jornada completa de uma operação pelo sistema distribuído. Um trace é composto de spans — unidades de trabalho com início, fim, nome e atributos. Spans têm relações pai-filho que formam uma árvore (ou DAG, em casos de fan-out paralelo).

Trace ID: 4bf92f3577b34da6

Span: api-gateway (root)                    [0ms ─────────────────── 245ms]
  Span: auth-service                        [2ms ── 15ms]
  Span: order-service                       [18ms ───────────────── 243ms]
    Span: postgres query (SELECT)           [20ms ─ 28ms]
    Span: kafka produce (OrderPlaced)       [30ms ─ 35ms]
    Span: inventory-service (async)         [35ms ──────── 180ms]
      Span: postgres query (UPDATE)         [40ms ─ 55ms]
      Span: redis set (cache)               [57ms ─ 60ms]
    Span: payment-service (async)           [35ms ────────────── 240ms]
      Span: external payment API            [40ms ──────────── 235ms]

Cada span carrega: trace_id (identifica o trace completo), span_id (identifica o span), parent_span_id (relaciona ao span pai), timestamps de início e fim, nome da operação, status (OK/ERROR), e atributos (chave-valor: http.method, db.statement, messaging.destination).

W3C TraceContext — o padrão de propagação

Para que um trace atravesse serviços, o contexto precisa ser propagado junto com cada chamada. O W3C TraceContext (RFC publicado em 2021) padroniza os headers HTTP de propagação:

traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
             ^^  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^ ^^
             version   trace-id (128 bits hex)    parent-span-id   flags
                                                  (64 bits hex)    (sampled=1)

tracestate: vendor1=value1,vendor2=value2
            (extensões proprietárias opcionais)

Todo instrumentado correto deve: ao receber uma requisição, extrair o contexto do header traceparent; ao fazer uma requisição downstream, injetar o contexto como header. O OpenTelemetry SDK faz isso automaticamente para HTTP, gRPC, e muitos clientes de banco de dados.

Propagação em mensageria assíncrona

Mensageria quebra o modelo de propagação de contexto por headers HTTP — a mensagem viaja assincronamente, possivelmente horas depois da publicação. A solução: injetar o contexto de trace como metadado/header da mensagem, e o consumidor extrai e cria um novo span filho (ou, mais corretamente, um span com link para o span original).

// Produtor: injeta o trace context como header da mensagem Kafka
var propagator = new TraceContextPropagator();
var headers = new Dictionary<string, string>();
propagator.Inject(
    new PropagationContext(Activity.Current?.Context ?? default, Baggage.Current),
    headers,
    (carrier, key, value) => carrier[key] = value
);

// Headers viajam com a mensagem:
// "traceparent": "00-4bf92f3577b34da6...-01"

// Consumidor: extrai e cria span filho
var parentContext = propagator.Extract(
    default,
    messageHeaders,
    (carrier, key) => carrier.TryGetValue(key, out var v) ? [v] : []
);

using var activity = ActivitySource.StartActivity(
    "process OrderPlaced",
    ActivityKind.Consumer,
    parentContext.ActivityContext  // span filho do produtor
);
nota Em mensageria assíncrona, o span do consumidor não é tecnicamente "filho" do span do produtor no sentido de tempo — o consumidor executa depois, possivelmente em um sistema diferente. A semântica correta é um link (não parent-child) entre o span de publicação e o span de consumo. OpenTelemetry suporta links explicitamente. Muitos sistemas, porém, usam parent-child por simplicidade — o trace fica menos correto mas mais legível.

OpenTelemetry — a camada de abstração universal

OpenTelemetry (OTel) é o padrão aberto para instrumentação de observabilidade — uma API, SDK, e protocolo de exportação (OTLP) que funciona independentemente do backend (Jaeger, Zipkin, Tempo, Datadog, Honeycomb). Instrumentar uma vez com OTel permite mudar de backend sem mudar o código.

A arquitetura OTel tem três camadas: API (interfaces estáveis que o código da aplicação usa — nada muda se você mudar de SDK), SDK (implementação: sampling, batching, exportação), e Collector (processo separado que recebe dados via OTLP, processa e exporta para backends).

┌──────────────────────────────────────────────────────┐
│  Aplicação                                           │
│  ┌─────────────────┐  ┌────────────────────────────┐ │
│  │  Código próprio │  │  Instrumentação automática │ │
│  │  (manual spans) │  │  (HTTP, DB, gRPC, etc.)    │ │
│  └────────┬────────┘  └────────────┬───────────────┘ │
│           └──────────────┬─────────┘                 │
│                    OTel SDK                           │
│                    (sampling, batching)               │
└──────────────────────┬───────────────────────────────┘
                       │ OTLP (gRPC ou HTTP)
                  OTel Collector
                  (recebe, processa, exporta)
                       │
          ┌────────────┼────────────┐
          ▼            ▼            ▼
        Jaeger      Prometheus    Datadog
       (traces)     (metrics)    (all-in-one)

Auto-instrumentação: o OTel SDK intercepta chamadas HTTP, consultas de banco, operações de fila e adiciona spans automaticamente sem mudança no código de aplicação. Em .NET, registra-se com AddAspNetCoreInstrumentation(), AddEntityFrameworkCoreInstrumentation(), etc. Em Python, opentelemetry-instrument python app.py injeta instrumentação via monkey-patching. Em Go, bibliotecas precisam de instrumentação explícita ou wrappers.

Métricas de comunicação — RED method

Para APIs e serviços, o método RED define as três métricas essenciais por serviço:

Rate: requisições por segundo. É o sinal de volume — quantas coisas estão acontecendo.

Errors: taxa de erros (% de requisições que falharam). É o sinal de qualidade — quantas coisas estão falhando.

Duration: distribuição de latência (P50, P95, P99). É o sinal de velocidade — quão rápido as coisas estão acontecendo para os usuários na cauda da distribuição.

# Prometheus — métricas RED para um serviço HTTP
# Rate: requisições por segundo
rate(http_requests_total[5m])

# Errors: taxa de erro nos últimos 5 minutos
rate(http_requests_total{status=~"5.."}[5m])
  /
rate(http_requests_total[5m])

# Duration: P99 de latência
histogram_quantile(0.99, rate(http_request_duration_seconds_bucket[5m]))

# Exemplo de instrumentação manual (Go + Prometheus)
var (
    requestsTotal = prometheus.NewCounterVec(
        prometheus.CounterOpts{Name: "http_requests_total"},
        []string{"method", "path", "status"},
    )
    requestDuration = prometheus.NewHistogramVec(
        prometheus.HistogramOpts{
            Name:    "http_request_duration_seconds",
            Buckets: []float64{0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5},
        },
        []string{"method", "path"},
    )
)

Para mensageria, as métricas equivalentes: Rate = mensagens processadas/segundo, Errors = taxa de mensagens para DLQ, Duration = lag do consumer (diferença entre quando a mensagem foi produzida e quando foi processada — o P99 do lag é crítico em sistemas event-driven).

Correlação de logs com traces

Logs sem contexto de trace são uma coleção de strings. Logs com trace_id e span_id permitem navegar de um log para o trace completo — ou de uma linha de trace para todos os logs gerados por aquele span. A correlação requer injetar o contexto atual nos campos estruturados do log.

// C# — injeção automática de trace context nos logs via ILogger + OTel
// Adicionar o OTel Log Bridge no Program.cs:
builder.Logging.AddOpenTelemetry(o => {
    o.IncludeScopes = true;
    o.ParseStateValues = true;
    // OTel automaticamente adiciona trace_id e span_id aos registros
});

// Log com contexto automático:
// {"timestamp":"...","level":"INFO","message":"Pedido criado",
//  "trace_id":"4bf92f3577b34da6...","span_id":"00f067aa0ba902b7",
//  "order_id":"ord-123"}

// Python — injeção manual com structlog
import structlog
from opentelemetry import trace

def add_trace_context(logger, method, event_dict):
    span = trace.get_current_span()
    if span.is_recording():
        ctx = span.get_span_context()
        event_dict["trace_id"] = format(ctx.trace_id, "032x")
        event_dict["span_id"] = format(ctx.span_id, "016x")
    return event_dict

structlog.configure(
    processors=[
        add_trace_context,
        structlog.processors.JSONRenderer(),
    ]
)

Debugging de sistemas assíncronos

Sistemas assíncronos introduzem desafios únicos de debugging: a causa e o efeito são separados no tempo, a ordem de eventos varia entre execuções, e um único evento pode disparar múltiplos consumidores em paralelo.

Estratégias específicas para sistemas baseados em eventos:

Correlation ID persistido na mensagem: além do trace context, inclua um correlation_id estável que identifica a operação de negócio original (ex: o ID do pedido que disparou toda a cadeia). Diferente do trace_id que muda a cada trace, o correlation_id permite conectar logs de dias diferentes que fazem parte do mesmo fluxo de negócio.

Event timeline reconstruction: para debugar um fluxo assíncrono, colete todos os eventos com o mesmo correlation_id e ordene por timestamp. Isso reconstrói a sequência real de acontecimentos, mesmo que tenham acontecido em serviços diferentes com clocks ligeiramente diferentes.

Dead letter queue como diagnóstico: toda mensagem na DLQ tem uma história. Os headers de retry e erro são dados de observabilidade — colete-os em um sistema de análise (Elasticsearch, Loki) para identificar padrões: qual tipo de mensagem falha mais? Em qual serviço? Com qual erro?

Kafka consumer lag monitoring: o lag do consumer (diferença entre o offset mais recente e o offset commitado pelo consumer) é a métrica mais importante de saúde de um sistema baseado em Kafka. Lag crescente indica que o consumer não está acompanhando a produção — pode ser lentidão de processamento, instâncias mortas, ou rebalancing excessivo.

# Kafka consumer lag via CLI
kafka-consumer-groups.sh --bootstrap-server kafka:9092 \
  --describe --group order-processing-group

# Output:
# GROUP                  TOPIC      PARTITION  CURRENT-OFFSET  LOG-END-OFFSET  LAG
# order-processing-group orders     0          1000            1000            0
# order-processing-group orders     1          850             1200            350  ← lag!
# order-processing-group orders     2          1100            1100            0

# Prometheus com kafka-exporter:
# kafka_consumergroup_lag{consumergroup="order-processing-group",topic="orders"}

SLOs em comunicação distribuída

Service Level Objectives (SLOs) em sistemas de comunicação precisam capturar tanto o componente síncrono quanto o assíncrono:

# SLO para API síncrona
# Disponibilidade: 99.9% das requisições retornam 2xx em 30 dias
# Latência: 95% das requisições respondem em <200ms

# SLO para pipeline assíncrona
# Disponibilidade: 99.9% das mensagens são processadas com sucesso
# Freshness: 99% das mensagens são processadas em <30 segundos após publicação
# (P99 de lag < 30s)

# Error budget: 0.1% de 30 dias = 43 minutos de downtime aceitável
# Ou: 0.1% de 1M mensagens/dia = 1000 mensagens com falha aceitável por dia

Comparativo entre linguagens — instrumentação OTel

C# — OpenTelemetry .NET SDK
// C# — OpenTelemetry completo: traces + métricas + logs

// Program.cs
var serviceName = "order-service";
var serviceVersion = "1.0.0";

builder.Services.AddOpenTelemetry()
    .ConfigureResource(r => r
        .AddService(serviceName, serviceVersion: serviceVersion)
        .AddAttributes([
            new("deployment.environment", "production"),
            new("host.name", Environment.MachineName),
        ])
    )
    .WithTracing(t => t
        .AddAspNetCoreInstrumentation(o => {
            o.RecordException = true;
            o.Filter = ctx => !ctx.Request.Path.StartsWithSegments("/health");
        })
        .AddEntityFrameworkCoreInstrumentation(o => o.SetDbStatementForText = true)
        .AddHttpClientInstrumentation()
        .AddSource("order-service")        // ActivitySource próprio
        .AddOtlpExporter(o => o.Endpoint = new Uri("http://otel-collector:4317"))
    )
    .WithMetrics(m => m
        .AddAspNetCoreInstrumentation()
        .AddRuntimeInstrumentation()       // GC, ThreadPool, etc.
        .AddMeter("order-service")
        .AddOtlpExporter()
    );

builder.Logging.AddOpenTelemetry(o => {
    o.IncludeScopes = true;
    o.AddOtlpExporter();
});

// Uso: spans manuais para lógica de negócio
public class OrderService(ILogger<OrderService> log) {
    private static readonly ActivitySource _source = new("order-service");
    private static readonly Meter _meter = new("order-service");
    private static readonly Counter<long> _ordersCreated =
        _meter.CreateCounter<long>("orders.created.total");

    public async Task<Order> PlaceOrderAsync(PlaceOrderCommand cmd) {
        using var activity = _source.StartActivity("PlaceOrder");
        activity?.SetTag("order.customer_id", cmd.CustomerId);
        activity?.SetTag("order.item_count", cmd.Items.Count);

        try {
            var order = await CreateOrderAsync(cmd);
            _ordersCreated.Add(1, new TagList {
                { "payment.method", cmd.PaymentMethod },
            });
            activity?.SetTag("order.id", order.Id);
            return order;
        } catch (Exception ex) {
            activity?.SetStatus(ActivityStatusCode.Error, ex.Message);
            activity?.RecordException(ex);
            log.LogError(ex, "Falha ao criar pedido para cliente {CustomerId}", cmd.CustomerId);
            throw;
        }
    }
}

O OTel SDK do .NET auto-instrumenta ASP.NET Core, EF Core e HttpClient sem código adicional. O ActivitySource próprio permite criar spans de negócio com atributos semânticos relevantes. O bridge de logs injeta trace_id e span_id automaticamente em todos os logs emitidos dentro de um span ativo.

Python — OpenTelemetry Python SDK
# Python — OpenTelemetry com FastAPI, auto-instrumentação e spans manuais

from opentelemetry import trace, metrics
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.sdk.metrics import MeterProvider
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import OTLPMetricExporter
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
from opentelemetry.instrumentation.sqlalchemy import SQLAlchemyInstrumentor
from opentelemetry.instrumentation.httpx import HTTPXClientInstrumentor
from opentelemetry.sdk.resources import SERVICE_NAME, Resource

# Setup do provider
resource = Resource(attributes={
    SERVICE_NAME: "order-service",
    "service.version": "1.0.0",
    "deployment.environment": "production",
})

tracer_provider = TracerProvider(resource=resource)
tracer_provider.add_span_processor(
    BatchSpanProcessor(OTLPSpanExporter(endpoint="http://otel-collector:4317"))
)
trace.set_tracer_provider(tracer_provider)

meter_provider = MeterProvider(resource=resource)
metrics.set_meter_provider(meter_provider)

# Instrumentação automática
FastAPIInstrumentor.instrument_app(app)
SQLAlchemyInstrumentor().instrument(engine=engine)
HTTPXClientInstrumentor().instrument()

# Uso manual
tracer = trace.get_tracer("order-service")
meter = metrics.get_meter("order-service")
orders_counter = meter.create_counter("orders.created.total")

async def place_order(cmd: PlaceOrderCommand) -> Order:
    with tracer.start_as_current_span("PlaceOrder") as span:
        span.set_attribute("order.customer_id", cmd.customer_id)
        span.set_attribute("order.item_count", len(cmd.items))

        try:
            order = await create_order_in_db(cmd)
            orders_counter.add(1, {"payment.method": cmd.payment_method})
            span.set_attribute("order.id", order.id)
            return order
        except Exception as e:
            span.set_status(trace.StatusCode.ERROR, str(e))
            span.record_exception(e)
            raise


# Propagação em Kafka consumer
from opentelemetry.propagate import extract

async def consume_message(msg: aio_pika.IncomingMessage) -> None:
    # Extrai trace context dos headers da mensagem
    headers = {k: v.decode() if isinstance(v, bytes) else v
               for k, v in (msg.headers or {}).items()}
    ctx = extract(headers)

    with tracer.start_as_current_span(
        "process OrderPlaced",
        context=ctx,
        kind=trace.SpanKind.CONSUMER,
    ) as span:
        span.set_attribute("messaging.system", "rabbitmq")
        span.set_attribute("messaging.destination", msg.routing_key)
        span.set_attribute("messaging.message_id", msg.message_id)
        await handle_order_placed(msg)

Python OTel tem auto-instrumentação via monkey-patching para FastAPI, SQLAlchemy, HTTPX, requests, e outros. Para Kafka com confluent-kafka, instrumentação manual via extract()/inject() é necessária. A propagação em mensagens async usa os mesmos mecanismos de propagação HTTP — só o carrier muda (headers do mensaje ao invés de headers HTTP).

Go — OpenTelemetry Go SDK
// Go — OpenTelemetry com net/http, spans manuais e propagação Kafka

package main

import (
    "context"
    "log/slog"

    "go.opentelemetry.io/otel"
    "go.opentelemetry.io/otel/attribute"
    "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
    "go.opentelemetry.io/otel/propagation"
    "go.opentelemetry.io/otel/sdk/resource"
    sdktrace "go.opentelemetry.io/otel/sdk/trace"
    semconv "go.opentelemetry.io/otel/semconv/v1.26.0"
    "go.opentelemetry.io/otel/trace"
    "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
)

var tracer trace.Tracer

func initOTel(ctx context.Context) func() {
    exp, _ := otlptracegrpc.New(ctx,
        otlptracegrpc.WithEndpoint("otel-collector:4317"),
        otlptracegrpc.WithInsecure(),
    )

    res, _ := resource.New(ctx,
        resource.WithAttributes(
            semconv.ServiceName("order-service"),
            semconv.ServiceVersion("1.0.0"),
            attribute.String("deployment.environment", "production"),
        ),
    )

    tp := sdktrace.NewTracerProvider(
        sdktrace.WithBatcher(exp),
        sdktrace.WithResource(res),
        sdktrace.WithSampler(sdktrace.ParentBased(sdktrace.TraceIDRatioBased(0.1))),
    )
    otel.SetTracerProvider(tp)
    otel.SetTextMapPropagator(propagation.TraceContext{})

    tracer = otel.Tracer("order-service")
    return func() { tp.Shutdown(ctx) }
}

// HTTP handler auto-instrumentado com otelhttp
func main() {
    shutdown := initOTel(context.Background())
    defer shutdown()

    mux := http.NewServeMux()
    mux.Handle("/api/orders", otelhttp.NewHandler(
        http.HandlerFunc(PlaceOrderHandler),
        "PlaceOrder",
    ))
    http.ListenAndServe(":8080", mux)
}

// Span manual de negócio
func PlaceOrderHandler(w http.ResponseWriter, r *http.Request) {
    ctx, span := tracer.Start(r.Context(), "PlaceOrder",
        trace.WithAttributes(
            attribute.String("order.customer_id", r.Header.Get("X-Customer-Id")),
        ),
    )
    defer span.End()

    order, err := placeOrder(ctx, parseRequest(r))
    if err != nil {
        span.RecordError(err)
        span.SetStatus(codes.Error, err.Error())
        http.Error(w, err.Error(), 500)
        return
    }
    span.SetAttributes(attribute.String("order.id", order.ID))
    writeJSON(w, order)
}

// Propagação em Kafka com franz-go
type kafkaHeaderCarrier []kgo.RecordHeader

func (c *kafkaHeaderCarrier) Get(key string) string {
    for _, h := range *c {
        if h.Key == key {
            return string(h.Value)
        }
    }
    return ""
}
func (c *kafkaHeaderCarrier) Set(key, val string) {
    *c = append(*c, kgo.RecordHeader{Key: key, Value: []byte(val)})
}
func (c kafkaHeaderCarrier) Keys() []string {
    keys := make([]string, len(c))
    for i, h := range c {
        keys[i] = h.Key
    }
    return keys
}

func processKafkaRecord(ctx context.Context, record *kgo.Record) {
    carrier := kafkaHeaderCarrier(record.Headers)
    parentCtx := otel.GetTextMapPropagator().Extract(ctx, carrier)

    ctx, span := tracer.Start(parentCtx, "process "+record.Topic,
        trace.WithSpanKind(trace.SpanKindConsumer),
        trace.WithAttributes(
            semconv.MessagingSystemKafka,
            semconv.MessagingDestinationName(record.Topic),
            attribute.Int64("messaging.kafka.partition", int64(record.Partition)),
            attribute.Int64("messaging.kafka.offset", record.Offset),
        ),
    )
    defer span.End()

    slog.InfoContext(ctx, "processando mensagem",
        "topic", record.Topic,
        "partition", record.Partition,
        "offset", record.Offset,
    )
    // slog com OTel bridge injeta trace_id/span_id no structured log automaticamente
}

Go não tem auto-instrumentação por monkey-patching — cada biblioteca precisa de um wrapper ou instrumentação explícita. otelhttp.NewHandler instrumenta handlers HTTP automaticamente. Para Kafka, o kafkaHeaderCarrier implementa a interface TextMapCarrier do OTel, permitindo usar o propagador padrão com qualquer biblioteca Kafka.

Checklist de observabilidade para comunicação distribuída

Antes de colocar um serviço em produção, verifique:

Traces: toda requisição HTTP de entrada gera um trace. Spans são criados para chamadas externas (HTTP downstream, banco de dados, cache, fila). O trace context é propagado em headers HTTP e metadados de mensagens. Erros são registrados no span com RecordError.

Métricas: métricas RED estão disponíveis por endpoint (rate, errors, duration). Consumer lag está sendo exportado para o Prometheus. Métricas de negócio relevantes (pedidos criados, pagamentos processados) estão sendo coletadas com atributos de dimensão.

Logs: logs são estruturados (JSON). Todo log emitido dentro de um span ativo inclui trace_id e span_id. Mensagens de log incluem correlation_id de negócio. Logs de erro incluem o stack trace completo.

Alertas: alerta de consumer lag crescente. Alerta de taxa de erro acima do SLO. Alerta de mensagens na DLQ. Alerta de P99 de latência acima do threshold.

Decisões de engenharia

Sampling: Head-based vs Tail-based

Head-based sampling: a decisão de amostrar ou não é tomada na entrada do sistema (ao receber a primeira requisição), antes de conhecer o resultado. É propagada via traceparent para todos os spans filhos — ou o trace inteiro é coletado ou nenhum span é. Simples de implementar, baixo overhead, mas não pode priorizar traces com erros ou alta latência (a decisão é tomada antes do resultado ser conhecido).

Tail-based sampling: a decisão é tomada após o trace completo ser coletado. O Collector acumula todos os spans do trace e então decide — priorizando traces com erros, alta latência, ou outros atributos de interesse. Resulta em uma coleção muito mais útil (100% dos traces com erro, 1% dos traces bem-sucedidos). O custo é maior complexidade no Collector (estado distribuído) e maior uso de memória. Prefira tail-based em produção com alto volume — é a abordagem do Grafana Tempo e do OpenTelemetry Collector com tail-sampling processor.

OpenTelemetry vs SDKs proprietários (Datadog, New Relic)

OpenTelemetry: padrão aberto suportado por todos os vendors. A instrumentação é portável — você pode trocar o backend (Jaeger → Tempo → Datadog) sem mudar o código da aplicação. É o caminho para evitar vendor lock-in na camada de instrumentação. Custo: o padrão é mais genérico, algumas integrações vendor-specific (profiling, APM avançado) não são cobertas.

SDKs proprietários (Datadog APM, New Relic, Dynatrace): melhor experiência out-of-the-box, auto-instrumentação mais ampla, features únicas como code profiling contínuo e análise de anomalias. O custo é acoplamento: migrar de Datadog para Grafana Cloud requer re-instrumentar todos os serviços. Em ambientes greenfield, prefira OTel. Em ambientes existentes com Datadog, considere o OTel exporter para Datadog — você instrumenta com OTel mas continua no Datadog.

Qual sinal usar para debugging: Trace vs Log vs Métrica

Métricas: para "o sistema está saudável?" e alertas. Rate, errors, duration — você sabe que algo está errado mas não sabe o que. São agregadas e não revelam o contexto específico de uma falha. Use para monitoramento contínuo e alertas.

Traces: para "o que aconteceu com esta requisição específica?" — debugging de latência, onde o tempo foi gasto, qual serviço falhou em um fluxo específico. São o sinal mais poderoso para sistemas distribuídos porque mostram causalidade entre serviços. Use quando o alerta de métrica disparou e você precisa entender a causa.

Logs: para contexto de negócio e debugging de lógica interna de um serviço específico. São o sinal mais detalhado mas o mais difícil de correlacionar entre serviços. Com trace_id nos logs, você pode navegar do trace para os logs do span específico que falhou. Use logs para o "porquê" depois que traces mostram o "onde".

Consumer lag como SLI vs latência de mensagem end-to-end

Consumer lag (número de mensagens não processadas): é uma métrica operacional indireta — lag alto indica que o consumidor está atrasado, mas não diz o impacto no usuário. É fácil de medir (kafka-consumer-groups, Prometheus exporter) e alertar. Problema: lag alto em partições de baixo volume não é necessariamente problemático; lag de 10.000 mensagens com throughput de 1M/s é 10ms de atraso.

Latência end-to-end da mensagem: timestamp de produção vs timestamp de processamento. É o SLI correto para comunicação assíncrona — mede o impacto real no SLO de negócio (ex: "eventos devem ser processados em menos de 5 segundos"). É mais difícil de medir (requer timestamp no payload da mensagem e OTel). Prefira latência end-to-end como SLI primário; use consumer lag como métrica operacional secundária para diagnosticar a causa do atraso.

  1. Instrumente um serviço simples com OpenTelemetry (na linguagem de sua preferência), exporte para Jaeger local (Docker), e visualize um trace completo incluindo um span de banco de dados e um span de chamada HTTP externa. Adicione o atributo de negócio order.id ao span raiz e encontre o trace no Jaeger filtrando por esse atributo.
    Critério: trace visível no Jaeger com pelo menos 3 spans (http.server, db.query, http.client); atributo order.id aparece no span raiz e é pesquisável; erro simulado em um span aparece com vermelho e mensagem de erro no trace.
  2. Implemente propagação de trace através de uma fila RabbitMQ: o produtor injeta o traceparent nos headers da mensagem usando o propagador W3C TraceContext, e o consumidor extrai e cria um span filho com CONSUMER kind. Visualize no Jaeger que o trace é contínuo — o span do consumidor aparece como filho do span de publicação.
    Critério: um único trace no Jaeger mostrando produtor → publicação na fila → consumo, com o span de consumo sendo filho do span de publicação; o gap de tempo entre publicação e consumo aparece claramente no trace como tempo de enfileiramento.
  3. Configure métricas RED com Prometheus e crie um dashboard no Grafana com: request rate (req/s), taxa de erros (%), e heatmap de latência (p50/p95/p99). Crie um alerta que dispara quando o P99 ultrapassa 500ms por mais de 2 minutos consecutivos.
    Critério: dashboard com os 3 painéis funcionando com dados reais; alerta dispara corretamente ao simular latência alta (sleep no handler); alerta resolve automaticamente quando a latência volta ao normal.
  4. Configure consumer lag monitoring para um consumer Kafka: use o kafka-exporter ou burrow para expor o lag por partição no Prometheus. Crie um alerta que dispara quando o lag de qualquer partição excede 1000 mensagens por mais de 5 minutos. Demonstre o alerta parando o consumer e produzindo mensagens.
    Critério: lag por partição visível no Grafana em tempo real; alerta dispara em menos de 6 minutos após lag exceder 1000; ao reiniciar o consumer, lag diminui e alerta resolve.
  5. Debugging exercise: em um sistema com 3 serviços e uma fila RabbitMQ, introduza um bug de falha intermitente (10% das mensagens falham com erro aleatório de negócio). Usando traces (Jaeger), logs estruturados (com trace_id) e métricas (taxa de erro por fila), identifique: qual serviço falha, com qual erro, e qual atributo das mensagens causa a falha.
    Critério: causa raiz identificada usando os três sinais; processo documentado — qual sinal revelou o problema, em que ordem, e o que cada sinal contribuiu; tempo de diagnóstico medido (do alerta à causa raiz).

Perguntas de entrevista

    O que é distributed tracing e por que context propagation é fundamental para que funcione?

    Distributed tracing: um mecanismo para rastrear o caminho de uma requisição através de múltiplos serviços. Um trace é um grafo de spans — cada span representa uma operação com início, fim, e atributos. Os spans têm relações pai-filho que formam a árvore de chamadas. O trace ID é o identificador único que une todos os spans de uma requisição em um único trace.

    Por que context propagation é fundamental: sem propagação, cada serviço gera seus próprios spans com trace IDs diferentes e independentes — você tem N logs de spans sem nenhuma conexão entre eles. Para que um span no Payment Service seja filho de um span no Order Service, o trace ID (e span ID pai) precisa ser transmitido na chamada. Em HTTP, via header traceparent: 00-{trace_id}-{span_id}-{flags} (W3C TraceContext). Em Kafka, via headers da mensagem. Em gRPC, via metadata.

    O que pode interromper a propagação: qualquer camada que não propaga o header quebra o trace — um HTTP client que não copia os headers de entrada para os de saída, uma fila que não preserva os headers de mensagem, ou um trabalho assíncrono que não passa o contexto para a goroutine/thread. O trace fica partido em dois traces sem conexão aparente.

    Qual a diferença entre head-based e tail-based sampling? Quando cada uma é adequada?

    Head-based sampling: a decisão de amostrar é tomada na entrada do sistema (no primeiro span do trace), antes de conhecer o resultado. A decisão é propagada para todos os spans filhos via sampled flag no traceparent. Simples e eficiente — zero overhead para traces não amostrados. Limitação crítica: você não pode decidir manter um trace porque ele teve erro se o erro ocorre depois da decisão inicial.

    Tail-based sampling: o Collector acumula todos os spans de um trace e toma a decisão no final. Permite regras como "100% de traces com erro, 100% de traces com P99 > 2s, 1% dos demais". Resulta em uma coleção muito mais útil para debugging. Custo: o Collector precisa manter estado de traces em andamento (memória), e traces que demoram muito podem ser descartados por timeout antes de chegar ao Collector.

    Quando usar: head-based para sistemas com volume muito alto onde tail-based seria proibitivo em recursos (>100k req/s), ou quando a complexidade operacional do tail-based Collector não é justificável. Tail-based quando debugging de produção é frequente e você precisa garantir que traces relevantes (com erros ou latência alta) sempre sejam coletados. Muitos sistemas usam híbrido: head-based alto (10-20% de amostragem), mas com tail-based sobreposto que garante 100% dos traces com erro.

    Como você correlaciona logs, traces e métricas na prática em um sistema distribuído?

    Correlação Logs ↔ Traces: inclua trace_id e span_id em todo log estruturado emitido dentro de um span ativo. O OpenTelemetry oferece bridges para os principais frameworks de logging — o span ativo é automaticamente injetado. Assim, ao ver um trace com erro no Jaeger, você pode clicar no span e ir direto aos logs daquele span específico.

    Correlação Métricas ↔ Traces: use exemplares — o Prometheus suporta exemplars: pontos de dados de métrica com um trace_id associado (o trace que gerou aquele ponto de dado). No Grafana, você pode clicar em um ponto do gráfico de P99 alto e ir direto para o trace daquela requisição que foi lenta. É a navegação ideal: alerta dispara (métrica) → encontra P99 alto no gráfico → clica no exemplar → chega no trace específico → navega para os logs.

    Correlação cross-service por correlation_id de negócio: adicione um ID de negócio (order_id, payment_id) como atributo nos spans e como campo nos logs. Isso permite buscar "todos os logs e traces relacionados ao pedido X" mesmo que o trace tenha sido partido em múltiplos traces por sampling ou fila.

    O que são os métodos RED e USE de instrumentação e quando cada um é mais útil?

    RED (Rate, Errors, Duration): criado por Tom Wilkie (Grafana/Weave). Foca em serviços: (R) taxa de requisições por segundo, (E) taxa de erros (requisições com falha / total), (D) distribuição de duração (latência, histograma p50/p95/p99). É a perspectiva do usuário do serviço — o que o serviço está entregando. Ideal para APIs, microsserviços, qualquer componente que processa requisições.

    USE (Utilization, Saturation, Errors): criado por Brendan Gregg. Foca em recursos: (U) utilização (% do tempo que o recurso está ocupado), (S) saturação (quanto trabalho está esperando, ex: tamanho da fila), (E) erros de recurso. É a perspectiva do recurso de infraestrutura — CPU, memória, disco, conexões de banco. Ideal para diagnosticar gargalos de infraestrutura.

    Quando usar: RED para SLOs de usuário — "95% das requisições em menos de 200ms com taxa de erro <0.1%". USE para debugging de infraestrutura — "por que o serviço está lento? CPU saturada? Pool de conexões esgotado?". Use RED para alertas de SLO; quando o alerta dispara, use USE para diagnosticar o recurso bottleneck.

    Como você projetaria o monitoramento de consumer lag e latência para um pipeline Kafka em produção?

    Consumer lag como métrica operacional: lag = último offset do tópico − último offset commitado pelo consumer group. Exportado via kafka-consumer-groups API, Burrow, ou Kafka Exporter para Prometheus. Monitore por consumer group + tópico + partição para identificar partições com lag desproporcional (indicativo de hot partition ou consumer morto). Alerta: lag crescendo por mais de N minutos, não apenas lag absoluto (lag estável não é necessariamente problema).

    Latência end-to-end como SLI primário: adicione produced_at (timestamp) ao payload da mensagem no produtor. O consumer calcula processing_latency = now - produced_at e exporta como histograma. É o SLI correto para SLOs de pipeline — "99% das mensagens devem ser processadas em menos de 5 segundos". Alerta nessa métrica, não no lag bruto.

    Tópico de DLQ e taxa de erro: monitore a taxa de mensagens indo para a DLQ. Um aumento súbito indica bug no consumer ou mudança de esquema incompatível. Alerta imediato em qualquer mensagem na DLQ é o padrão.

    Dashboard ideal: lag por partição (calor map), latência end-to-end p50/p95/p99, throughput (mensagens/s), taxa de DLQ, consumer group rebalances (sinal de instabilidade). Combine com traces que cruzam o boundary kafka usando propagação de contexto via message headers.

Referências

  1. docs OpenTelemetry Documentation — opentelemetry.io. opentelemetry.io/docs — Documentação oficial do OTel incluindo conceitos, especificações de API/SDK, guias de instrumentação por linguagem e referência do protocolo OTLP. O ponto de partida para qualquer implementação.
  2. artigo W3C TraceContext Specification — W3C. w3.org/TR/trace-context — Especificação do formato traceparent/tracestate. Fundamental para entender a semântica de propagação, flags de sampling e extensões via tracestate.
  3. livro Observability Engineering — Charity Majors, Liz Fong-Jones & George Miranda (O'Reilly, 2022). O livro mais completo sobre observabilidade moderna — distingue monitoring tradicional de observabilidade, explica high-cardinality events, e detalha como usar traces para debugging de sistemas distribuídos complexos.
  4. artigo Dapper, a Large-Scale Distributed Systems Tracing Infrastructure — Google (2010). research.google/pubs/pub36356 — O paper que definiu o modelo de distributed tracing moderno (trace + span + propagation). Jaeger, Zipkin e OTel são todos derivados conceituais do Dapper.
  5. artigo RED Method — Tom Wilkie — Grafana Blog. grafana.com/blog/2018/08/02/the-red-method-how-to-instrument-your-services — Explicação do método RED (Rate, Errors, Duration) por seu criador, com exemplos de queries Prometheus e discussão de quando usar RED vs USE (Utilization, Saturation, Errors).
  6. docs OpenTelemetry Collector — Tail Sampling Processor — opentelemetry.io. opentelemetry.io/docs/collector/configuration/#tail_sampling — Documentação do tail sampling processor do OTel Collector: configuração de políticas (always sample errors, sample by latency, probabilistic sampling), limites de memória e timeout para traces incompletos.
  7. blog Always-on Profiling and the Four Keys to Observability — Charity Majors (Honeycomb). charity.wtf/2020/03/03/observability-is-a-many-splendored-thing — Argumenta que observabilidade real requer high-cardinality events (não apenas métricas agregadas) e explica como traces com atributos ricos permitem debugging de problemas que métricas tradicionais nunca revelariam.
  8. docs Prometheus Exemplars — Prometheus Documentation. prometheus.io/docs/prometheus/latest/feature_flags/#exemplars-storage — Como exemplars conectam métricas a traces: um ponto de dado de métrica carrega um trace_id associado. Permite navegar de um spike de P99 no Grafana diretamente para o trace Jaeger/Tempo da requisição lenta.
  9. paper Canopy: An End-to-End Performance Tracing And Analysis System — Facebook (SOSP 2017). Descreve o sistema de tracing do Facebook em escala de bilhões de requisições/dia. Apresenta soluções para tail-based sampling distribuído, análise de trace causal, e correlação de traces com métricas de performance — problemas que sistemas menores enfrentam em escala.
  10. artigo USE Method — Systems Performance Methodology — Brendan Gregg. brendangregg.com/usemethod.html — Descrição formal do método USE por seu criador: aplicação sistemática a recursos de CPU, memória, disco, rede e outros. Inclui checklists por tipo de recurso e como combinar USE (infraestrutura) com RED (serviços) para diagnóstico completo.
  11. blog How to Monitor Kafka Consumer Lag — Confluent Blog. confluent.io/blog/monitor-kafka-consumer-group-latencies-with-burrow — Explica a diferença entre consumer lag (número de mensagens) e consumer lag time (latência em tempo real). Descreve Burrow como solução para monitoramento inteligente de lag: avalia tendência de lag, não apenas valor absoluto.
  12. padrão W3C Trace Context — Level 2 (tracestate) — W3C. w3.org/TR/trace-context-2 — Extensão do TraceContext que define o header tracestate para metadados vendor-specific propagados junto com o traceparent. Permite que vendors como Datadog e New Relic propagam sampling decisions e outros metadados sem poluir o traceparent padrão.