MÓDULO 10 · CONCEITO 04 DE 12

Distributed Tracing

Anatomia de um trace, W3C Trace Context, head-based vs tail-based sampling, Jaeger e Zipkin como backends open source

Tempo de leitura ~26 min Pré-requisito 03 · Métricas e Prometheus Próximo 05 · OpenTelemetry

Distributed tracing nasceu no Google com o paper Dapper, a Large-Scale Distributed Systems Tracing Infrastructure (2010). O problema era simples de enunciar: uma request ao search.google.com tocava centenas de microserviços — quando havia lentidão, como identificar qual serviço era responsável? A solução do Dapper definiu as abstrações que o mundo usa até hoje: trace (a jornada completa de uma request), span (uma operação individual dentro do trace), e context propagation (passar os IDs entre serviços via headers HTTP, mensagens RPC). O paper introduziu também o conceito de sampling como necessidade de produção — impossível registrar 100% dos traces em escala. O modelo Dapper inspirou Zipkin (Twitter, 2012), Jaeger (Uber, 2017), e eventualmente o OpenTelemetry (CNCF, 2019).

contexto Logs são eventos isolados. Tracing conecta esses eventos através de serviços, processos e redes, revelando a causalidade. Sem tracing, você sabe que o serviço A demorou 800ms e o serviço B demorou 400ms — mas não sabe se B foi chamado por A, se foram chamados em paralelo, ou qual span filho causou a latência.

Anatomia de um Trace

Trace

Um trace representa a execução completa de uma operação distribuída — do request inicial até a resposta final. É identificado por um Trace ID único e globalmente único (128 bits no OTel; 64 bits no Zipkin v1). Todos os spans de uma mesma request compartilham o mesmo Trace ID.

Span

Um span é uma unidade de trabalho com início e fim bem definidos. Cada span contém:

Span Events

Span events são timestamps dentro de um span que marcam ocorrências importantes. Diferem de atributos por serem pontuais no tempo, não estáticos. Use para: exceções capturadas, retries, cache misses, checkpoints de lógica de negócio.

// OTel — adicionando span event para exception capturada
span.AddEvent("exception", new SpanEventAttributes
{
    { "exception.type", ex.GetType().FullName },
    { "exception.message", ex.Message },
    { "exception.stacktrace", ex.StackTrace }
});

// Equivalente semântico: RecordException é o helper padrão
span.RecordException(ex);
span.SetStatus(SpanStatusCode.Error, ex.Message);

Baggage

Baggage é um mecanismo de propagação de contexto cross-cutting: key-value pairs que viajam com o trace por toda a cadeia de serviços. Use com moderação — baggage aumenta o tamanho de cada request/mensagem e pode vazar informação sensível. Casos válidos: tenant ID, feature flag ativo, ambiente de teste (canary). Casos inválidos: dados de usuário, JWTs, qualquer coisa grande.

atenção Baggage viaja para fora do seu controle. Um serviço downstream pode logar headers, ou repassar baggage para sistemas de terceiros. Nunca coloque dados sensíveis em baggage — ele é propagado por toda a cadeia de chamadas, incluindo serviços que você não controla.

Visualização em cascata (Gantt)

A visualização padrão de um trace é um diagrama em cascata: o eixo X é o tempo, cada linha é um span, e a identação representa a hierarquia pai-filho. Permite identificar visualmente:

W3C Trace Context (RFC 9532)

Antes do W3C Trace Context, cada vendor propagava contexto com headers proprietários: X-B3-TraceId (Zipkin/B3), uber-trace-id (Jaeger), X-Cloud-Trace-Context (GCP). Sistemas que cruzavam vendors quebravam o trace. O W3C Trace Context (RFC 9532, publicado como recomendação em 2023) padroniza dois headers HTTP:

traceparent

traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
             |  |                                |                | |
             |  trace-id (128 bits hex)          span-id (64 bits) sampled flag
             version (sempre 00)

# sampled flag: 01 = sampled, 00 = não sampled
# trace-id: todos os serviços na cadeia compartilham este valor
# span-id: o span do serviço UPSTREAM (quem enviou o request)

tracestate

# Vendor-specific state, propagado opcionalmente
tracestate: vendor1=value1,vendor2=value2

# Exemplo com Datadog + W3C juntos
tracestate: dd=s:1;p:00f067aa0ba902b7,rojo=00f067aa0ba902b7

Ao receber um request com traceparent: extraia o Trace ID e o Span ID do upstream. Crie um novo span com Parent Span ID = Span ID do upstream. Propague o traceparent atualizado (com seu novo Span ID) nas chamadas downstream.

nota B3 Propagation ainda é amplamente usado (Zipkin, Spring, algumas versões do Istio). O OTel suporta ambos via propagators. Para sistemas novos, use W3C Trace Context. Para interoperabilidade com sistemas legados B3, configure o OTel Composite Propagator com ambos.

Sampling: Head-based vs Tail-based

Em produção, capturar 100% dos traces é inviável — um sistema com 10k req/s geraria volumes enormes de dados. Sampling é a decisão de quais traces registrar.

Head-based Sampling

A decisão de amostrar é tomada no início do trace, no primeiro serviço que recebe o request. A decisão é propagada via traceparent (o sampled flag) para todos os downstream. Vantagens: baixo overhead, sem buffer, decisão simples. Desvantagens: você não sabe na entrada se o trace será interessante — a maioria dos traces descartados são normais, mas alguns descartados seriam erros ou outliers que você queria ver.

Tail-based Sampling

A decisão de amostrar é tomada após o trace completar, com base em critérios como presença de erro, latência acima do P99, ou span de banco de dados lento. Requer um collector que mantenha todos os spans em buffer até o trace completar (ou timeout). Vantagens: você sempre captura 100% dos traces com erros ou latência anormal — os traces que realmente importam. Desvantagens: overhead de memória no collector, complexidade de implementação, risco de perder spans se o trace não completar antes do timeout.

Estratégias Combinadas

processors:
  tail_sampling:
    decision_wait: 30s        # aguarda até 30s para o trace completar
    num_traces: 100000        # buffer de traces em memória
    expected_new_traces_per_sec: 1000
    policies:
      - name: errors-policy
        type: status_code
        status_code: { status_codes: [ERROR] }
      - name: slow-traces
        type: latency
        latency: { threshold_ms: 1000 }
      - name: probabilistic-sample
        type: probabilistic
        probabilistic: { sampling_percentage: 5 }
    # decisão final: OR entre políticas (qualquer match = sample)

Backends Open Source: Jaeger, Zipkin e Tempo

Zipkin — criado pelo Twitter em 2012. O mais antigo backend de tracing open source. Usa o formato B3 para propagação. Armazenamento: in-memory (dev), Cassandra, Elasticsearch, MySQL. UI simples e estável. Quando usar: sistemas legados que já usam B3 headers, equipes pequenas que querem simplicidade, integração com Spring Cloud Sleuth.

Jaeger — criado pelo Uber em 2017, doado à CNCF em 2017, graduated em 2019. Arquitetura mais elaborada: Agent (sidecar), Collector, Query, UI. Suporte nativo a Kafka como buffer. Armazenamento: Cassandra, Elasticsearch, Badger (embedded), OpenSearch. Jaeger v2 (2024) adotou o OTel Collector como base, substituindo o Jaeger Agent pelo OTel Collector com exportador Jaeger. Suporte nativo ao protocolo OTLP.

Aplicações → OTel SDK → OTLP → OTel Collector → Kafka → Jaeger Ingester → Elasticsearch
                                                  ↓
                                            Jaeger Query + UI (leitura)

# OTel Collector: recebe OTLP, aplica tail sampling, batcheia, exporta
# Kafka: buffer para picos de tráfego (Jaeger Ingester consome)
# Elasticsearch: armazenamento com TTL de 7-30 dias típico

Grafana Tempo — backend de tracing minimalista: armazena traces no object storage (S3, GCS) sem indexação completa. Baixíssimo custo de storage — a troca é que você não pode buscar por atributos arbitrários (apenas por Trace ID). Ideal quando você chega ao trace via exemplar de uma métrica Prometheus ou link de um log no Loki.

Span Status Codes e Span Kind

OTel define três status codes para spans: Unset — padrão; a operação completou sem indicação explícita de resultado. Instrumentações automáticas geralmente deixam Unset para HTTP 2xx/3xx. Ok — a operação completou com sucesso explicitamente marcado. Use em operações de negócio concluídas com sucesso. Nunca downgrade de Ok para Unset. Error — a operação falhou. Deve ser acompanhado de RecordException() ou um span event com detalhes do erro.

nota HTTP 4xx não é sempre Error no nível do span. Se seu serviço retorna 404 porque o recurso genuinamente não existe (comportamento correto), o span não deve ser marcado como Error — o sistema funcionou corretamente. Error é para falhas do sistema, não para resultados de negócio esperados. Porém, um 503 ou 500 inesperado deve sempre ser Error.

Span Kind afeta como os backends calculam latência e como as UIs exibem o trace:

Instrumentação Manual por Linguagem

OTel auto-instrumentation cobre HTTP, DB, gRPC automaticamente. Para spans de lógica de negócio, instrumentação manual é necessária.

C# — OpenTelemetry .NET com ActivitySource
// Program.cs — configuração completa
builder.Services.AddOpenTelemetry()
    .WithTracing(tracing => tracing
        .SetResourceBuilder(ResourceBuilder.CreateDefault()
            .AddService("order-service", serviceVersion: "1.2.0"))
        .AddAspNetCoreInstrumentation(opts =>
        {
            opts.RecordException = true;
            opts.Filter = ctx => !ctx.Request.Path.StartsWithSegments("/health");
        })
        .AddHttpClientInstrumentation()
        .AddEntityFrameworkCoreInstrumentation()
        .AddSource("OrderService.*")
        .AddOtlpExporter(otlp =>
        {
            otlp.Endpoint = new Uri("http://otel-collector:4317");
            otlp.Protocol = OtlpExportProtocol.Grpc;
        }));

// OrderService.cs — instrumentação manual
public class OrderService
{
    private static readonly ActivitySource _tracer =
        new("OrderService.Orders");

    public async Task<Order> ProcessOrderAsync(CreateOrderRequest req)
    {
        using var activity = _tracer.StartActivity("ProcessOrder",
            ActivityKind.Internal);

        activity?.SetTag("order.customer_id", req.CustomerId);
        activity?.SetTag("order.item_count", req.Items.Count);
        activity?.SetTag("order.total_value", req.TotalValue);

        try
        {
            var inventory = await CheckInventoryAsync(req);

            activity?.AddEvent(new ActivityEvent("inventory.checked",
                tags: new ActivityTagsCollection
                {
                    { "inventory.all_available", inventory.AllAvailable }
                }));

            if (!inventory.AllAvailable)
            {
                activity?.SetStatus(ActivityStatusCode.Error, "Insufficient inventory");
                throw new InsufficientInventoryException(inventory.MissingItems);
            }

            var order = await _repo.CreateAsync(req);
            activity?.SetTag("order.id", order.Id.ToString());
            activity?.SetStatus(ActivityStatusCode.Ok);
            return order;
        }
        catch (Exception ex) when (ex is not InsufficientInventoryException)
        {
            activity?.RecordException(ex);
            activity?.SetStatus(ActivityStatusCode.Error, ex.Message);
            throw;
        }
    }
}

ActivitySource é o wrapper .NET para OTel Tracer. ActivityKind mapeia para SpanKind. AddSource() registra ActivitySources customizados no pipeline. O filtro de health check evita poluir o trace com liveness probes.

Python — OpenTelemetry Python SDK
# setup.py / app factory
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
from opentelemetry.sdk.resources import Resource
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
from opentelemetry.instrumentation.sqlalchemy import SQLAlchemyInstrumentor
from opentelemetry.instrumentation.httpx import HTTPXClientInstrumentor

resource = Resource.create({
    "service.name": "order-service",
    "service.version": "1.2.0",
    "deployment.environment": os.getenv("ENV", "production"),
})

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

FastAPIInstrumentor.instrument_app(app)
SQLAlchemyInstrumentor().instrument(engine=engine)
HTTPXClientInstrumentor().instrument()

# order_service.py — instrumentação manual
tracer = trace.get_tracer("order_service.orders")

async def process_order(req: CreateOrderRequest) -> Order:
    with tracer.start_as_current_span(
        "ProcessOrder",
        kind=trace.SpanKind.INTERNAL,
        attributes={
            "order.customer_id": req.customer_id,
            "order.item_count": len(req.items),
        }
    ) as span:
        try:
            inventory = await check_inventory(req)

            span.add_event("inventory.checked", attributes={
                "inventory.all_available": inventory.all_available
            })

            if not inventory.all_available:
                span.set_status(
                    trace.StatusCode.ERROR,
                    "Insufficient inventory"
                )
                raise InsufficientInventoryError(inventory.missing_items)

            order = await repo.create(req)
            span.set_attribute("order.id", str(order.id))
            span.set_status(trace.StatusCode.OK)
            return order

        except InsufficientInventoryError:
            raise
        except Exception as exc:
            span.record_exception(exc)
            span.set_status(trace.StatusCode.ERROR, str(exc))
            raise

start_as_current_span é context manager que define o span como current — chamadas downstream herdam automaticamente o contexto via ContextVar. A auto-instrumentação do FastAPI cria spans para cada request sem código adicional.

Go — OpenTelemetry Go SDK
// setup/tracing.go
func InitTracer(ctx context.Context) (*sdktrace.TracerProvider, error) {
    res, err := resource.New(ctx,
        resource.WithAttributes(
            semconv.ServiceName("order-service"),
            semconv.ServiceVersion("1.2.0"),
            attribute.String("deployment.environment", os.Getenv("ENV")),
        ),
    )
    if err != nil {
        return nil, fmt.Errorf("creating resource: %w", err)
    }

    conn, err := grpc.DialContext(ctx, "otel-collector:4317",
        grpc.WithTransportCredentials(insecure.NewCredentials()))
    if err != nil {
        return nil, fmt.Errorf("connecting to collector: %w", err)
    }

    exporter, err := otlptracegrpc.New(ctx,
        otlptracegrpc.WithGRPCConn(conn))
    if err != nil {
        return nil, err
    }

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

// order_service.go — instrumentação manual
var tracer = otel.Tracer("order_service.orders")

func (s *OrderService) ProcessOrder(
    ctx context.Context, req CreateOrderRequest,
) (*Order, error) {
    ctx, span := tracer.Start(ctx, "ProcessOrder",
        trace.WithSpanKind(trace.SpanKindInternal),
        trace.WithAttributes(
            attribute.String("order.customer_id", req.CustomerID),
            attribute.Int("order.item_count", len(req.Items)),
        ),
    )
    defer span.End()

    inventory, err := s.checkInventory(ctx, req)
    if err != nil {
        span.RecordError(err)
        span.SetStatus(codes.Error, err.Error())
        return nil, err
    }

    span.AddEvent("inventory.checked", trace.WithAttributes(
        attribute.Bool("inventory.all_available", inventory.AllAvailable),
    ))

    if !inventory.AllAvailable {
        err = fmt.Errorf("insufficient inventory: %v", inventory.MissingItems)
        span.SetStatus(codes.Error, err.Error())
        return nil, err
    }

    order, err := s.repo.Create(ctx, req)
    if err != nil {
        span.RecordError(err)
        span.SetStatus(codes.Error, err.Error())
        return nil, err
    }

    span.SetAttributes(attribute.String("order.id", order.ID.String()))
    span.SetStatus(codes.Ok, "")
    return order, nil
}

ctx propaga o span automaticamente — passar ctx em todas as chamadas downstream é o contrato do Go para propagação de contexto. otel.SetTextMapPropagator configura W3C Trace Context globalmente.

Padrões Avançados

Trace Context em Mensageria

Em sistemas assíncronos, o trace deve ser propagado via headers da mensagem (Kafka headers, AMQP message headers). O consumer cria um novo span com o Parent Span ID do producer — mas o trace pode ter um "gap" de horas ou dias entre producer e consumer, o que é intencional e correto.

// Kafka producer — propagar contexto via headers
var headers = new Headers();
var carrier = new DictionaryCarrier();
Propagators.DefaultTextMapPropagator.Inject(
    new PropagationContext(Activity.Current!.Context, Baggage.Current),
    carrier,
    (dict, key, value) => headers.Add(key, Encoding.UTF8.GetBytes(value))
);
await producer.ProduceAsync(topic, new Message<string, string>
{
    Key = key,
    Value = JsonSerializer.Serialize(payload),
    Headers = headers
});

// Kafka consumer — extrair contexto e criar span filho
var propagationContext = Propagators.DefaultTextMapPropagator.Extract(
    default,
    message.Headers,
    (headers, key) => new[] { Encoding.UTF8.GetString(headers.GetLastBytes(key)) }
);
using var activity = tracer.StartActivity(
    "ProcessOrderEvent",
    ActivityKind.Consumer,
    propagationContext.ActivityContext
);

Correlation entre Trace e Logs

O valor máximo do tracing aparece quando logs são correlacionados com traces. Injete o Trace ID e Span ID em cada log gerado durante o processamento de um span — ferramentas como Grafana permitem clicar em um log e ver o trace completo.

# Python — injetar trace context em structlog automaticamente
import structlog
from opentelemetry import trace

def inject_trace_context(logger, method, event_dict):
    span = trace.get_current_span()
    if span and 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")
        event_dict["trace_sampled"] = ctx.trace_flags.sampled
    return event_dict

structlog.configure(processors=[inject_trace_context, ...])

Exemplars — Ponte Metrics → Traces

Exemplars são amostras de traces embutidas nas métricas. Quando um bucket do histogram é incrementado, o exemplar armazena o Trace ID do request que gerou aquele valor. No Grafana, clicar no P99 de uma métrica salta diretamente para um trace representativo daquele percentil.

quando usar cada backend Jaeger: quando você precisa buscar traces por atributos (ex: "todos os traces com customer_id=X"). Tempo: quando você chega ao trace via métrica ou log (trace ID já conhecido), e quer armazenamento barato. Em stacks Grafana (Loki + Tempo + Mimir), Tempo é a escolha natural — correlation entre as três ferramentas é nativa.

Decisões de engenharia

Head-based vs Tail-based sampling

Head-based sampling: a decisão é tomada na borda do sistema, no primeiro span do trace, antes de qualquer processamento. É propagada via flag no traceparent. Zero overhead de armazenamento para traces não amostrados — spans descartados imediatamente. Limitação crítica: você não pode priorizar traces com erro porque a decisão precede o resultado.

Tail-based sampling: o OTel Collector acumula todos os spans de um trace e decide no final — priorizando erros (100%), alta latência (>1s: 100%), e amostrando o restante (1-10%). Resultado: coleção muito mais útil. Custo: o Collector precisa de memória para bufferizar spans em andamento, e traces que excedem o timeout de decisão são descartados. Use tail-based em produção quando debugging de traces com erros é frequente — é o único modo que garante capturar 100% dos traces problemáticos.

Jaeger vs Grafana Tempo como backend

Jaeger: backend com índice de spans — você pode buscar traces por atributo (customer.id=123, http.status_code=500). Adequado quando você precisa encontrar traces específicos sem saber o trace ID. Requer storage de índice (Elasticsearch ou Cassandra em escala), o que aumenta custo e complexidade operacional.

Grafana Tempo: armazena traces em object storage (S3) sem índice de spans. Muito mais barato e simples de operar. A limitação: você só encontra traces pelo trace ID — não pode buscar por atributo. Na prática, isso não é problema quando você chega ao trace via exemplar de métrica (trace ID no gráfico) ou via link de log (trace ID no log). Em stacks Grafana (Loki + Tempo + Mimir), Tempo é a escolha natural pela integração nativa entre os três sinais. Tempo 2.0 adicionou suporte a TraceQL (query language para spans), reduzindo a diferença com Jaeger.

Auto-instrumentação vs instrumentação manual

Auto-instrumentação: o SDK OTel injeta spans automaticamente para frameworks conhecidos (HTTP servers, HTTP clients, gRPC, SQL, Redis, Kafka) sem modificar o código da aplicação. Em Go, requer wrappers explícitos (otelhttp.NewHandler, otelsql). Em Java e Python, agentes de auto-instrumentação fazem bytecode injection. Cobre o plumbing técnico sem esforço, mas não sabe nada do domínio de negócio.

Instrumentação manual: você cria spans explicitamente para operações de negócio (ProcessPayment, ValidateInventory) e adiciona atributos de domínio (order.id, customer.tier, payment.method). É o que transforma tracing técnico em observabilidade de negócio — permite filtrar "todos os traces de clientes premium" ou "traces de pagamentos com cartão de crédito". A combinação das duas abordagens é o padrão: auto para o plumbing, manual para o contexto de negócio.

Span attributes vs Baggage vs Log fields

Span attributes: metadados do span específico — http.method, db.statement, order.id. Visíveis apenas naquele span, não propagados para spans filhos de outros serviços. São o lugar certo para contexto técnico e de negócio específico à operação.

Baggage: pares chave-valor propagados em todos os spans filhos, através de todos os serviços, automaticamente via context. Útil para IDs de correlação que devem aparecer em toda a cadeia: session.id, feature.flag. Custo: aumenta o tamanho dos headers HTTP em cada chamada. Nunca coloque dados sensíveis ou de alta cardinalidade no Baggage — ele é propagado em toda a rede.

Log fields correlacionados: trace_id + span_id injetados nos logs estruturados do serviço. Não são parte do trace em si, mas permitem navegar do trace para os logs detalhados daquele span. Use quando o contexto é verboso demais para atributo de span (stack traces, payloads detalhados).

Como praticar

  1. Configure OTel em um serviço HTTP (qualquer linguagem) com auto-instrumentação + um span manual de negócio com atributo order.id. Exporte para Jaeger local via Docker. Faça requests (incluindo alguns com erro) e explore a UI do Jaeger: identifique o critical path, os gap spans, e os traces com erro.
    Critério: trace completo visível no Jaeger com spans de HTTP + banco de dados + span manual de negócio; o atributo order.id é pesquisável na UI do Jaeger; traces com erro aparecem em vermelho e o span de erro tem mensagem de erro visível.
  2. Implemente tail-based sampling no OTel Collector: sempre capturar traces com status Error ou latência acima de 1s, amostrar 5% do restante. Valide que 100% dos traces com erro aparecem no Jaeger mesmo quando o sampling rate geral é baixo.
    Critério: com 1000 requests (10% com erro, 90% normais), o Jaeger mostra ~100% dos traces com erro e ~5% dos normais; traces lentos (>1s) aparecem 100% independente do sampling geral; configuração de tail sampler no Collector documentada.
  3. Propague trace context via Kafka: um producer HTTP recebe um request, injeta os headers W3C na mensagem Kafka usando o propagador OTel. Um consumer extrai o contexto e cria um span filho. Verifique no Jaeger que producer e consumer aparecem no mesmo trace com o gap de tempo de enfileiramento visível.
    Critério: trace no Jaeger mostra producer → gap (tempo na fila) → consumer como sequência contínua; o span do consumer é filho do span de publicação; o tempo de enfileiramento é calculável subtraindo os timestamps dos spans.
  4. Injete trace_id e span_id em todos os logs do serviço via bridge OTel-logger. Configure o Grafana com Loki (logs) e Tempo (traces) correlacionados. Em um log de erro, clique no link de trace e navegue diretamente para o span que gerou o erro.
    Critério: todo log emitido dentro de um span tem trace_id e span_id corretos; clique no trace_id no Grafana Explore abre o trace no Tempo; o span corresponde exatamente ao momento do log.
  5. Demonstre context loss: crie um cenário onde uma goroutine (Go) ou Task (C#) é criada sem passar o contexto OTel. Mostre no Jaeger que os spans criados dentro da goroutine não aparecem no trace correto — são traces órfãos. Corrija passando o ctx explicitamente (Go) ou usando Activity.Current (C#).
    Critério: sem propagação: spans da goroutine aparecem como traces separados sem conexão com o trace pai; com propagação: aparecem como spans filhos do span correto; a diferença é demonstrável no Jaeger lado a lado.

Perguntas de entrevista

    O que é context propagation em distributed tracing e o que acontece quando é perdido?

    Context propagation: o mecanismo pelo qual o trace ID e o span ID pai são transmitidos entre serviços. Sem propagação, cada serviço geraria spans com trace IDs independentes — você teria N logs de spans sem nenhuma conexão. Com propagação, todos os spans de uma request compartilham o mesmo trace ID e formam uma árvore de causalidade.

    Mecanismo HTTP: o header traceparent: 00-{trace_id}-{parent_span_id}-{flags} (W3C TraceContext). O serviço downstream extrai os IDs, cria um span filho com aquele parent_span_id, e propaga para suas próprias chamadas. O SDK OTel faz isso automaticamente para HTTP quando auto-instrumentado.

    O que acontece quando é perdido: o trace fica partido em múltiplos traces sem conexão. Sintomas: você vê o trace do serviço A terminando normalmente, mas não vê o que aconteceu no serviço B que ele chamou. No Jaeger, aparece como dois traces separados sem link entre eles. Causas comuns: (1) HTTP client que não copia headers de entrada para saída; (2) fila de mensagens que não preserva headers; (3) thread pool ou goroutine criada sem propagar o contexto OTel; (4) serialização/deserialização de contexto mal implementada em chamadas gRPC.

    Como você instrumentaria uma chamada assíncrona (goroutine, Task, thread pool) sem perder o contexto OTel?

    O problema: o contexto OTel é armazenado em variável de contexto local à goroutine/thread (context.Context em Go, AsyncLocal<T> em .NET, contextvars.ContextVar em Python). Ao criar uma nova goroutine ou Task, a nova unidade de execução começa com contexto vazio — o span ativo do caller não é automaticamente herdado.

    Go: sempre passe o ctx explicitamente para a goroutine. O contexto OTel vive dentro do context.Context:

    ctx, span := tracer.Start(ctx, "process-order")
    defer span.End()
    
    go func(ctx context.Context) {  // passa ctx, não captura por closure
        _, childSpan := tracer.Start(ctx, "async-work")
        defer childSpan.End()
    }(ctx)
    

    .NET: Activity.Current é propagado automaticamente para Tasks via AsyncLocal. Se você usa Task.Run, a Activity atual é capturada. O problema ocorre com ThreadPool.QueueUserWorkItem legado ou callbacks sem await. Para garantir: capture a Activity antes de enfileirar e restaure no callback.

    Python: use contextvars.copy_context().run(fn) para executar uma função em uma cópia do contexto atual, incluindo o span OTel ativo.

    O que são atributos semânticos do OpenTelemetry e por que padronizá-los importa?

    Atributos semânticos: um vocabulário padrão de nomes de atributos para operações comuns, definido pela OpenTelemetry Semantic Conventions. Exemplos: http.method, http.status_code, http.url, db.system, db.statement, messaging.system, messaging.destination, rpc.method.

    Por que padronizar importa: (1) interoperabilidade — ferramentas como Jaeger, Tempo, Datadog entendem os atributos padrão e exibem UIs especializadas (ex: queries de banco com db.statement aparecem formatadas); (2) correlação cross-service — se todos os serviços usam http.status_code, você pode buscar "todos os traces com HTTP 503" independente de linguagem ou time; (3) alertas reutilizáveis — regras de alerta baseadas em atributos semânticos funcionam para qualquer serviço que siga a convenção.

    Atributos de domínio customizados: adicione atributos de negócio com namespace do seu domínio — order.id, customer.tier, payment.method. Use snake_case com prefixo de domínio para evitar conflitos com futuras convenções OTel. Esses atributos são o que torna o tracing realmente útil para debugging de negócio — sem eles, você tem apenas plumbing técnico.

    Como você escolheria entre Jaeger e Grafana Tempo para uma nova stack de observabilidade?

    Escolha Tempo quando: (1) você já usa Grafana, Loki (logs) e Mimir/Prometheus (métricas) — a integração nativa entre os três é o maior diferencial do Tempo; (2) custo de storage é importante — Tempo no S3 é significativamente mais barato que Jaeger com Elasticsearch/Cassandra; (3) você chega ao trace principalmente via exemplar de métrica ou link de log (o trace ID já é conhecido, não precisa de busca por atributo); (4) você quer zero operação de storage (S3 é serverless).

    Escolha Jaeger quando: (1) você precisa buscar traces por atributo sem saber o trace ID — "todos os traces de customer.id=X das últimas 2 horas"; (2) você não usa Grafana como ferramenta principal de observabilidade; (3) investigação ad-hoc por atributo é frequente no workflow da equipe.

    TraceQL no Tempo 2.0: adiciona query language que permite buscar spans por atributo, reduzindo significativamente a diferença com Jaeger. Se você está decidindo hoje, Tempo 2.0+ com TraceQL é competitivo com Jaeger na maioria dos casos de uso, com custo de operação menor.

    O que é um "gap span" no trace e o que ele indica sobre o sistema?

    Gap span: um período de tempo no trace que não está coberto por nenhum span filho — o span pai está ativo, mas não há spans filhos registrados naquele intervalo. No diagrama do Jaeger, aparece como um espaço vazio horizontal dentro de um span.

    O que indica: (1) trabalho não instrumentado — há código rodando (locks, serialização, processamento em memória) que não tem spans; (2) overhead de framework não visível — tempo gasto em middleware, interceptors, ou plumbing de framework antes de chegar ao código da aplicação; (3) espera por lock ou resource — a goroutine está bloqueada aguardando algo que não é rastreável; (4) instrumentation bug — um span filho foi criado mas seu contexto não foi passado corretamente para o código que faz o trabalho real.

    Como investigar: um gap grande (ex: 200ms num trace de 250ms) é o sinal mais valioso — indica onde está o tempo "perdido". Adicione spans manuais na seção de código coberta pelo gap para identificar a causa. Em sistemas com pools de threads, o gap entre criar a Task e ela começar a executar pode ser significativo — é o tempo de espera na fila do pool, que indica saturação.

Referências

  1. paper Dapper, a Large-Scale Distributed Systems Tracing Infrastructure — Sigelman et al., Google (2010). research.google/pubs/dapper — O paper fundacional de distributed tracing. Define trace, span, context propagation e sampling — as abstrações que Zipkin, Jaeger e OTel ainda usam hoje.
  2. docs W3C Trace Context — RFC 9532 — W3C (2023). w3.org/TR/trace-context — Especificação dos headers traceparent e tracestate. Leitura obrigatória para entender o formato de propagação e como integrar vendors diferentes.
  3. docs OpenTelemetry Traces — Conceitos oficiais — CNCF. opentelemetry.io/docs/concepts/signals/traces — Referência canônica dos conceitos de tracing no OTel: span kinds, status codes, attributes semânticos, baggage e exemplars.
  4. docs Jaeger Architecture — Jaeger Project, CNCF. jaegertracing.io/docs/architecture — Documentação da arquitetura Jaeger v2 com OTel Collector integrado. Explica o pipeline de ingestão, storage backends e tail sampling.
  5. docs Grafana Tempo — Grafana Labs. grafana.com/oss/tempo — Documentação do Tempo como backend de tracing em object storage. Explica a integração com Loki (logs) e Mimir/Prometheus (métricas) via exemplars.
  6. docs OpenTelemetry Semantic Conventions — CNCF. opentelemetry.io/docs/specs/semconv — Vocabulário padrão de atributos de span para HTTP, gRPC, banco de dados, mensageria, cloud e mais. Seguir as convenções garante interoperabilidade entre ferramentas e permite queries cross-service por atributo padronizado.
  7. docs OTel Collector — Tail Sampling Processor — opentelemetry.io. opentelemetry.io/docs/collector/configuration/#tail_sampling — Configuração de tail sampling no Collector: políticas por status de erro, latência, probabilístico, e combinações AND/OR. Inclui considerações de memória e timeout para traces de longa duração.
  8. livro Observability Engineering — Charity Majors, Liz Fong-Jones & George Miranda (O'Reilly, 2022). Capítulos 8-11 cobrem distributed tracing em profundidade: como projetar spans úteis, atributos de domínio, sampling strategies, e a diferença entre tracing técnico e observabilidade real. O capítulo sobre "instrumentation as a practice" é especialmente relevante.
  9. paper Canopy: An End-to-End Performance Tracing and Analysis System at Facebook — Kaldor et al. (SOSP, 2017). Descreve o sistema de tracing do Facebook em escala de bilhões de requests/dia. Apresenta soluções para causal tracing (além do modelo simples de span-pai), tail-based sampling distribuído, e análise de trace causal. Muito relevante para quem opera tracing em escala.
  10. blog Distributed Tracing — We've Been Doing It Wrong — Cindy Sridharan. medium.com/@copyconstruct/distributed-tracing-weve-been-doing-it-wrong-39fc92a857df — Análise crítica das limitações do modelo de tracing baseado em spans: o problema de traces que não capturam causalidade real em sistemas com fan-out massivo, e como pensar em tracing além do modelo Dapper.
  11. padrão W3C Baggage Specification — W3C. w3.org/TR/baggage — Especificação do header baggage para propagação de contexto arbitrário entre serviços. Define o formato, limites de tamanho (8kb máximo), e semântica de merge quando múltiplos valores são combinados. Complementa o traceparent para propagação de IDs de negócio.
  12. docs OpenTelemetry Instrumentation Libraries — opentelemetry.io. opentelemetry.io/ecosystem/registry — Registro de bibliotecas de instrumentação OTel por linguagem: wrappers para net/http, database/sql, gRPC, Kafka, Redis, e outros. O ponto de partida para adicionar auto-instrumentação sem modificar o código da aplicação.