.NET OpenTelemetry Distributed Tracing Observabilidade Microsserviços Jaeger Grafana

Distributed Tracing no .NET com OpenTelemetry: rastreando requisições entre microsserviços

Guia completo de distributed tracing no .NET com OpenTelemetry: ActivitySource para traces customizados, propagação de contexto entre serviços HTTP e.

N
Neryx Digital Architects
22 de outubro de 2025
13 min de leitura
210 profissionais leram
Categoria: Arquitetura Público: Times de plataforma e operação Etapa: Decisão

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.

Quer sair do modo reativo e priorizar o que mais importa?

O diagnóstico de maturidade ajuda a transformar sintomas operacionais em um plano mais claro de evolução.

Avaliar maturidade

Newsletter

Receba artigos como este no seu e-mail

Conteúdo técnico sobre arquitetura de software, .NET, IA e gestão de produto. Sem spam.