OpenTelemetry em .NET: seguindo uma requisição pelo sistema
Uma requisição que atravessa um controller, chama dois serviços internos e faz uma consulta ao banco deixa, sem instrumentação nenhuma, só o resultado final visível. O caminho que ela percorreu por dentro do sistema, quanto tempo cada etapa levou e onde exatamente algo desacelerou, tudo isso desaparece assim que a resposta é enviada, a menos que alguém tenha instrumentado esse caminho de propósito.
ActivitySource: a peça que já vem embutida no .NET
O .NET já inclui suporte nativo a tracing distribuído através de System.Diagnostics.ActivitySource, sem depender de nenhuma biblioteca externa só para criar os spans. Um span representa uma unidade de trabalho com início, fim e duração, e spans podem ser aninhados para representar a hierarquia de chamadas dentro de uma mesma requisição.
private static readonly ActivitySource ActivitySource = new("MinhaAplicacao.Pedidos");
public async Task<Pedido> ProcessarPedidoAsync(int pedidoId)
{
using var activity = ActivitySource.StartActivity("ProcessarPedido");
activity?.SetTag("pedido.id", pedidoId);
var pedido = await _repository.BuscarAsync(pedidoId);
using (ActivitySource.StartActivity("ValidarEstoque"))
{
await _estoqueService.ValidarAsync(pedido);
}
return pedido;
}
Cada StartActivity cria um span, automaticamente vinculado ao span pai ativo no contexto (nesse exemplo, ValidarEstoque aparece aninhado dentro de ProcessarPedido), formando a árvore de chamadas que representa o caminho real da requisição por dentro da aplicação.
Meter: a mesma lógica aplicada a métricas
Para números agregados ao longo do tempo (quantas requisições, qual latência média, quantos erros), o .NET expõe System.Diagnostics.Metrics.Meter, seguindo o mesmo princípio de instrumentação nativa sem acoplamento a um fornecedor específico de monitoramento:
private static readonly Meter Meter = new("MinhaAplicacao.Pedidos");
private static readonly Counter<long> PedidosProcessados =
Meter.CreateCounter<long>("pedidos.processados");
PedidosProcessados.Add(1, new KeyValuePair<string, object?>("status", "sucesso"));
ActivitySource e Meter produzem os dados brutos. Nenhum dos dois, por conta própria, decide para onde esses dados vão. Essa decisão fica isolada na camada de exportação, configurada separadamente.
OTLP, não mais o exportador dedicado de Jaeger
Um ponto que já mudou desde versões mais antigas de tutorial sobre o assunto: o pacote OpenTelemetry.Exporter.Jaeger, dedicado especificamente a exportar para Jaeger, está descontinuado. A recomendação atual é usar o exportador OTLP padrão (OpenTelemetry.Exporter.OpenTelemetryProtocol), já que o Jaeger passou a aceitar OTLP nativamente. Isso simplifica a configuração: o mesmo exportador funciona com Jaeger, com outros backends compatíveis com OTLP, e com um Collector do OpenTelemetry no meio do caminho, sem precisar trocar de pacote ao trocar de backend de observabilidade.
builder.Services.AddOpenTelemetry()
.WithTracing(tracing => tracing
.AddSource("MinhaAplicacao.Pedidos")
.AddAspNetCoreInstrumentation()
.AddOtlpExporter())
.WithMetrics(metrics => metrics
.AddMeter("MinhaAplicacao.Pedidos")
.AddOtlpExporter());
AddAspNetCoreInstrumentation() adiciona automaticamente spans para cada requisição HTTP recebida pela aplicação, sem precisar instrumentar manualmente o pipeline de middleware inteiro. AddOtlpExporter() envia tudo (traces e métricas) para o endpoint configurado, seja um Collector local, seja diretamente um backend compatível.
Vendor neutrality não é detalhe filosófico
A vantagem prática de instrumentar contra ActivitySource e Meter, exportando via OTLP, é poder trocar de backend de observabilidade (Jaeger, um Collector redirecionando para outro destino, uma plataforma comercial) alterando só a configuração de exportação, sem tocar em nenhuma linha de código que gera os spans e métricas. Isso evita o cenário comum de uma aplicação inteira instrumentada com SDK proprietário de um fornecedor específico, onde trocar de ferramenta de observabilidade significa reinstrumentar a aplicação do zero.
Seguir uma requisição de ponta a ponta é o objetivo final
O valor de tudo isso só aparece quando uma requisição lenta ou com erro precisa ser investigada: com tracing instrumentado corretamente, dá para abrir o trace daquela requisição específica e ver exatamente qual etapa levou mais tempo, ou onde a falha realmente aconteceu, em vez de reconstruir isso por hipótese a partir de log espalhado sem correlação entre si.