Quando uma requisição falha em um sistema com dez microsserviços, você precisa saber: em qual serviço aconteceu o erro? Qual era o caminho da requisição até lá? Quanto tempo cada etapa levou? Sem distributed tracing, você navega por logs separados de cada serviço tentando montar o puzzle manualmente. Com tracing, você tem uma linha do tempo completa da requisição — do browser até o banco de dados — em uma única visualização.
Como o distributed tracing funciona
Distributed tracing se baseia em dois conceitos: trace (a requisição completa, do início ao fim) e span (uma operação individual dentro do trace — uma chamada HTTP, uma query SQL, uma publicação de mensagem). Cada span tem um ID único, um ID do trace pai, timestamps de início e fim, e atributos (metadados).
A propagação de contexto é o mecanismo que conecta os spans entre serviços: quando o Serviço A chama o Serviço B via HTTP, ele inclui o TraceId e o SpanId nos headers da requisição. O Serviço B lê esses headers e cria seus spans como filhos do span do Serviço A. Assim, mesmo que A e B sejam processos separados em máquinas diferentes, os spans acabam no mesmo trace.
Setup: OpenTelemetry no .NET
dotnet add package OpenTelemetry.Extensions.Hosting
dotnet add package OpenTelemetry.Instrumentation.AspNetCore
dotnet add package OpenTelemetry.Instrumentation.Http
dotnet add package OpenTelemetry.Instrumentation.EntityFrameworkCore
dotnet add package OpenTelemetry.Exporter.Jaeger # para Jaeger
dotnet add package OpenTelemetry.Exporter.OpenTelemetryProtocol # para Grafana/OTEL Collector
// Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddOpenTelemetry()
.ConfigureResource(resource => resource
.AddService(
serviceName: "order-service",
serviceVersion: "2.1.0",
serviceInstanceId: Environment.MachineName))
.WithTracing(tracing => tracing
// Instrumentação automática — sem código extra nos controllers
.AddAspNetCoreInstrumentation(opt =>
{
// Inclui o body da requisição nos atributos (cuidado com dados sensíveis)
opt.RecordException = true;
opt.Filter = httpContext =>
// Exclui health checks do tracing para não poluir
!httpContext.Request.Path.StartsWithSegments("/health");
})
.AddHttpClientInstrumentation(opt =>
{
opt.RecordException = true;
})
.AddEntityFrameworkCoreInstrumentation(opt =>
{
opt.SetDbStatementForText = true; // inclui o SQL nos spans (só em dev/staging)
})
// Source customizado para spans de negócio (ver abaixo)
.AddSource("OrderService.Business")
.AddSource("OrderService.Messaging")
// Exporta para Jaeger (desenvolvimento)
.AddJaegerExporter(opt =>
{
opt.AgentHost = builder.Configuration["Jaeger:Host"] ?? "localhost";
opt.AgentPort = 6831;
})
// Exporta via OTLP (Grafana Tempo, Honeycomb, Datadog etc. em produção)
.AddOtlpExporter(opt =>
{
opt.Endpoint = new Uri(
builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"]
?? "http://localhost:4317");
}));
ActivitySource: spans customizados para operações de negócio
// A instrumentação automática cobre HTTP e SQL.
// Para operações de negócio relevantes, crie spans customizados com ActivitySource.
// Declare o ActivitySource como static — um por serviço/módulo
public static class Telemetry
{
// Nome deve ser registrado no AddSource() acima
public static readonly ActivitySource Business =
new ActivitySource("OrderService.Business", "2.1.0");
public static readonly ActivitySource Messaging =
new ActivitySource("OrderService.Messaging", "2.1.0");
}
// Uso em um caso de uso de negócio
public class CreateOrderHandler : IRequestHandler<CreateOrderCommand, CreateOrderResult>
{
public async Task<CreateOrderResult> Handle(
CreateOrderCommand command, CancellationToken ct)
{
// Cria um span para o caso de uso completo
using var activity = Telemetry.Business.StartActivity("CreateOrder");
// Atributos enriquecendo o span com contexto de negócio
activity?.SetTag("order.customer_id", command.CustomerId.ToString());
activity?.SetTag("order.item_count", command.Items.Count);
try
{
// Span filho para validação
using (var validationSpan = Telemetry.Business.StartActivity("ValidateOrder"))
{
await ValidateOrderAsync(command, ct);
validationSpan?.SetTag("validation.passed", true);
}
var order = Order.Create(command.CustomerId, command.Items);
// Span filho para persistência
using (var persistSpan = Telemetry.Business.StartActivity("PersistOrder"))
{
persistSpan?.SetTag("db.table", "orders");
_context.Orders.Add(order);
await _context.SaveChangesAsync(ct);
persistSpan?.SetTag("order.id", order.Id.ToString());
}
// Span filho para publicação de evento
using (var eventSpan = Telemetry.Messaging.StartActivity("PublishOrderCreated"))
{
eventSpan?.SetTag("messaging.destination", "order-events");
await _bus.Publish(new OrderCreatedEvent { OrderId = order.Id }, ct);
}
// Enriquece o span raiz com o resultado
activity?.SetTag("order.id", order.Id.ToString());
activity?.SetTag("order.total", order.Total);
activity?.SetStatus(ActivityStatusCode.Ok);
return new CreateOrderResult(order.Id, order.Total);
}
catch (Exception ex)
{
// Marca o span como erro com detalhes da exceção
activity?.SetStatus(ActivityStatusCode.Error, ex.Message);
activity?.RecordException(ex);
throw;
}
}
}
Correlação com logs: TraceId nos logs automaticamente
// Com OpenTelemetry + Serilog, o TraceId e SpanId são automaticamente
// incluídos em cada log emitido dentro de um span ativo.
// appsettings.json — Serilog com enriquecimento de tracing
{
"Serilog": {
"Using": ["Serilog.Sinks.Console", "Serilog.Sinks.Elasticsearch"],
"Enrich": ["FromLogContext", "WithMachineName", "WithThreadId",
"WithSpanId", "WithTraceId"], // ← correlação automática
"WriteTo": [
{
"Name": "Console",
"Args": {
"outputTemplate": "[{Timestamp:HH:mm:ss} {Level:u3}] {TraceId} {Message:lj}{NewLine}{Exception}"
}
}
]
}
}
// No Elasticsearch/Kibana ou Grafana Loki, você pode buscar por TraceId
// e ver todos os logs relacionados àquela requisição — em todos os serviços.
// Clicar no TraceId abre o trace no Jaeger/Grafana Tempo.
Propagação de contexto via mensageria
// A propagação automática funciona para HTTP.
// Para mensageria (RabbitMQ, Kafka), você precisa propagar manualmente
// ou usar uma biblioteca que faça isso (MassTransit faz automaticamente).
// Publicador: injeta o contexto de tracing nos headers da mensagem
public async Task PublishWithTracingAsync<T>(T message, CancellationToken ct)
{
using var activity = Telemetry.Messaging.StartActivity(
$"publish {typeof(T).Name}",
ActivityKind.Producer);
var headers = new Dictionary<string, string>();
// Propaga o contexto W3C TraceContext nos headers da mensagem
Propagators.DefaultTextMapPropagator.Inject(
new PropagationContext(
Activity.Current?.Context ?? default,
Baggage.Current),
headers,
(carrier, key, value) => carrier[key] = value);
await _channel.BasicPublishAsync(
exchange: "orders",
routingKey: typeof(T).Name,
body: JsonSerializer.SerializeToUtf8Bytes(new
{
Payload = message,
TraceHeaders = headers // headers de propagação junto com o payload
}),
cancellationToken: ct);
activity?.SetTag("messaging.destination", "orders");
}
// Consumidor: extrai o contexto de tracing e continua o trace
public async Task ConsumeAsync(BasicDeliverEventArgs args)
{
var envelope = JsonSerializer.Deserialize<MessageEnvelope>(args.Body.ToArray());
// Extrai o contexto de propagação dos headers
var parentContext = Propagators.DefaultTextMapPropagator.Extract(
default,
envelope!.TraceHeaders,
(carrier, key) => carrier.TryGetValue(key, out var value)
? [value] : []);
// Cria o span do consumer como filho do span do publicador
using var activity = Telemetry.Messaging.StartActivity(
$"consume {args.RoutingKey}",
ActivityKind.Consumer,
parentContext.ActivityContext);
activity?.SetTag("messaging.source", args.Exchange);
await ProcessMessageAsync(envelope.Payload);
}
Baggage: dados de negócio propagados por todo o trace
// Baggage é um mecanismo para propagar dados de negócio por toda a cadeia
// de chamadas — sem precisar passar parâmetros em cada método.
// Útil para TenantId, UserId, CorrelationId de negócio.
// No início da requisição (middleware ou filtro):
public class TenantTracingMiddleware
{
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
var tenantId = context.User.FindFirst("tenant_id")?.Value;
if (tenantId is not null)
{
// Adiciona ao baggage — propagado automaticamente para todos os serviços
Baggage.SetBaggage("tenant.id", tenantId);
// Também como atributo do span atual para visualização no Jaeger
Activity.Current?.SetTag("tenant.id", tenantId);
}
await next(context);
}
}
// Em qualquer serviço downstream que recebe a requisição:
var tenantId = Baggage.GetBaggage("tenant.id");
// tenantId está disponível sem precisar passar via parâmetro
Jaeger e Grafana Tempo: backends de tracing
# docker-compose.yml para desenvolvimento local
services:
jaeger:
image: jaegertracing/all-in-one:latest
ports:
- "6831:6831/udp" # agent (UDP — recebe spans dos serviços)
- "16686:16686" # UI (http://localhost:16686)
- "14268:14268" # HTTP collector
environment:
- COLLECTOR_OTLP_ENABLED=true
# Alternativa: Grafana Tempo (melhor integração com Grafana/Loki/Prometheus)
tempo:
image: grafana/tempo:latest
ports:
- "3200:3200" # query frontend
- "4317:4317" # OTLP gRPC
- "4318:4318" # OTLP HTTP
volumes:
- ./tempo-config.yaml:/etc/tempo.yaml
command: ["-config.file=/etc/tempo.yaml"]
grafana:
image: grafana/grafana:latest
ports:
- "3000:3000"
environment:
- GF_AUTH_ANONYMOUS_ENABLED=true
volumes:
- ./grafana-datasources.yaml:/etc/grafana/provisioning/datasources/ds.yaml
Em produção, a escolha entre Jaeger, Grafana Tempo, Honeycomb e Datadog depende do seu stack de observabilidade. Se já usa Grafana para métricas e Loki para logs, Grafana Tempo é a escolha natural — você correlaciona logs, métricas e traces dentro do mesmo Grafana. Se a equipe prefere uma ferramenta dedicada a tracing, Jaeger é open source e maduro.
O ponto mais importante: o código .NET é o mesmo para qualquer backend — apenas muda o exporter configurado no AddJaegerExporter() ou AddOtlpExporter(). Trocar o backend é uma mudança de configuração, não de código.
Distributed tracing é a diferença entre "há um problema em produção" e "o problema está no método X do serviço Y, na chamada Z". Se você quer implementar observabilidade completa no seu sistema .NET, a Neryx pode ajudar.