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
);
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 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 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 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
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: 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.
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 (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.
-
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.idao 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. -
Implemente propagação de trace através de uma fila RabbitMQ: o produtor injeta o
traceparentnos headers da mensagem usando o propagador W3C TraceContext, e o consumidor extrai e cria um span filho comCONSUMERkind. 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. -
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. -
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. -
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
- docs OpenTelemetry Documentation — opentelemetry.io.
- artigo W3C TraceContext Specification — W3C.
- livro Observability Engineering — Charity Majors, Liz Fong-Jones & George Miranda (O'Reilly, 2022).
- artigo Dapper, a Large-Scale Distributed Systems Tracing Infrastructure — Google (2010).
- artigo RED Method — Tom Wilkie — Grafana Blog.
- docs OpenTelemetry Collector — Tail Sampling Processor — opentelemetry.io.
- blog Always-on Profiling and the Four Keys to Observability — Charity Majors (Honeycomb).
- docs Prometheus Exemplars — Prometheus Documentation.
- paper Canopy: An End-to-End Performance Tracing And Analysis System — Facebook (SOSP 2017).
- artigo USE Method — Systems Performance Methodology — Brendan Gregg.
- blog How to Monitor Kafka Consumer Lag — Confluent Blog.
- padrão W3C Trace Context — Level 2 (tracestate) — W3C.