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).
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 ID — identificador único do span (64 bits)
- Parent Span ID — referência ao span pai (ausente no root span)
- Trace ID — a qual trace pertence
- Operation name — nome da operação (ex:
HTTP GET /users/{id}) - Start timestamp + Duration
- Attributes — key-value pairs com contexto (ex:
http.status_code=200) - Events — pontos no tempo dentro do span (ex: cache miss, retry attempt)
- Status — Unset, Ok, ou Error
- Kind — Server, Client, Producer, Consumer, Internal
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.
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:
- Serial vs paralelo — spans filhos sequenciais ou sobrepostos
- Critical path — o caminho mais longo que determina a latência total
- Gap spans — tempo entre o fim de um span filho e início do próximo (serialization overhead, network)
- Long tail — spans que demoram muito mais que a mediana
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.
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
- Always-on para erros: head-based a 1%, mas sempre amostrar quando há flag de erro ou usuário em beta
- Priority sampling: Datadog e Jaeger combinam head-based com ajuste dinâmico baseado em throughput e presença de erros
- OTel Tail Sampling Processor: no OTel Collector, permite regras como "sempre amostrar se status=Error ou latency > 1s, amostrar 5% do resto"
- Exemplars: mesmo com alto sampling rate, armazene exemplars nas métricas apontando para traces específicos — permite investigação cirúrgica
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.
Span Kind afeta como os backends calculam latência e como as UIs exibem o trace:
- Server — lado servidor de uma chamada (HTTP server, gRPC server). Root span típico.
- Client — chamada saindo do processo (HTTP client, DB query, Redis call)
- Producer — publicação em mensageria (Kafka produce, RabbitMQ publish)
- Consumer — consumo de mensageria (Kafka consume, SQS receive)
- Internal — operação interna ao processo (função de negócio, processamento)
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.
// 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.
# 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.
// 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.
Decisões de engenharia
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: 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: 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: 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
-
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. -
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. -
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. -
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. -
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
ctxexplicitamente (Go) ou usandoActivity.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
- paper Dapper, a Large-Scale Distributed Systems Tracing Infrastructure — Sigelman et al., Google (2010).
- docs W3C Trace Context — RFC 9532 — W3C (2023).
- docs OpenTelemetry Traces — Conceitos oficiais — CNCF.
- docs Jaeger Architecture — Jaeger Project, CNCF.
- docs Grafana Tempo — Grafana Labs.
- docs OpenTelemetry Semantic Conventions — CNCF.
- docs OTel Collector — Tail Sampling Processor — opentelemetry.io.
- livro Observability Engineering — Charity Majors, Liz Fong-Jones & George Miranda (O'Reilly, 2022).
- paper Canopy: An End-to-End Performance Tracing and Analysis System at Facebook — Kaldor et al. (SOSP, 2017).
- blog Distributed Tracing — We've Been Doing It Wrong — Cindy Sridharan.
- padrão W3C Baggage Specification — W3C.
- docs OpenTelemetry Instrumentation Libraries — opentelemetry.io.