Observabilidade .NET OpenTelemetry DevOps Monitoramento

Observabilidade com OpenTelemetry no .NET: traces, métricas e logs em produção

Como implementar observabilidade completa em aplicações .NET com OpenTelemetry: distributed tracing, métricas customizadas.

N
Neryx Digital Architects
7 de janeiro de 2026
14 min de leitura
240 profissionais leram
Categoria: DevOps & Cloud Público: Times de plataforma e backend buscando visibilidade operacional Etapa: Decisão

Você subiu o deploy, o usuário reclama que "está lento", e você abre o painel de monitoramento. Se a resposta for só um gráfico de CPU e memória, você tem monitoramento — mas não tem observabilidade. A diferença é que observabilidade permite responder: qual endpoint está lento? qual query específica está demorando? qual microsserviço está causando o problema?

OpenTelemetry é o padrão open source que unifica os três pilares da observabilidade — traces, métricas e logs — em uma única instrumentação que exporta para qualquer backend.

Os três pilares da observabilidade

Traces distribuídos: rastreiam uma requisição do início ao fim, atravessando múltiplos serviços. Mostram exatamente onde o tempo foi gasto — no banco, numa chamada HTTP externa, num processamento específico.

Métricas: valores numéricos agregados ao longo do tempo — latência p95/p99, taxa de erros, throughput, tamanho de filas. Ótimas para alertas e dashboards de saúde.

Logs estruturados: eventos com contexto estruturado (campos nomeados, não texto livre). Permitem filtrar por tenant_id, trace_id, usuario_id sem depender de regex.

Setup: pacotes necessários

# Pacotes principais
dotnet add package OpenTelemetry
dotnet add package OpenTelemetry.Extensions.Hosting
dotnet add package OpenTelemetry.Instrumentation.AspNetCore
dotnet add package OpenTelemetry.Instrumentation.Http
dotnet add package OpenTelemetry.Instrumentation.EntityFrameworkCore

# Exportadores (escolha conforme seu stack)
dotnet add package OpenTelemetry.Exporter.Console          # dev/debug
dotnet add package OpenTelemetry.Exporter.Jaeger           # Jaeger (self-hosted)
dotnet add package OpenTelemetry.Exporter.OpenTelemetryProtocol  # OTLP (Grafana, etc)

# Métricas
dotnet add package OpenTelemetry.Instrumentation.Runtime   # métricas do runtime .NET

Configuração em Program.cs

var serviceName = "pedidos-api";
var serviceVersion = "1.0.0";

builder.Services.AddOpenTelemetry() .ConfigureResource(resource => resource .AddService(serviceName, serviceVersion: serviceVersion) .AddAttributes(new Dictionary<string, object> { [“deployment.environment”] = builder.Environment.EnvironmentName.ToLower(), [“host.name”] = Environment.MachineName })) .WithTracing(tracing => tracing .AddAspNetCoreInstrumentation(opt => { // Filtrar health checks do trace opt.Filter = ctx => !ctx.Request.Path.StartsWithSegments(“/health”); opt.RecordException = true; }) .AddHttpClientInstrumentation() .AddEntityFrameworkCoreInstrumentation(opt => { opt.SetDbStatementForText = true; // inclui a query SQL no trace }) .AddSource(serviceName) // para traces manuais (ActivitySource) .AddJaegerExporter(opt => { opt.AgentHost = builder.Configuration[“Jaeger:Host”] ?? “localhost”; opt.AgentPort = 6831; }) .AddOtlpExporter(opt => // para Grafana Tempo / AWS X-Ray OTLP { opt.Endpoint = new Uri(builder.Configuration[“Otlp:Endpoint”] ?? “http://localhost:4317”); })) .WithMetrics(metrics => metrics .AddAspNetCoreInstrumentation() .AddHttpClientInstrumentation() .AddRuntimeInstrumentation() .AddMeter(serviceName) // para métricas customizadas .AddPrometheusExporter()) // expõe /metrics para o Prometheus coletar .WithLogging(logging => logging .AddOtlpExporter());

Traces manuais: instrumentando código de negócio

A instrumentação automática cobre HTTP e banco — mas para lógica de negócio crítica você quer criar spans próprios.

public class PedidoService
{
    // ActivitySource é thread-safe, crie uma instância estática por classe
    private static readonly ActivitySource _activitySource = new("pedidos-api");
private readonly IProcessadorPagamento _pagamento;
private readonly IEstoqueService _estoque;

public async Task&lt;PedidoConfirmadoResult&gt; ProcessarPedidoAsync(
    Pedido pedido, CancellationToken ct)
{
    // Span raiz para toda a operação
    using var activity = _activitySource.StartActivity("ProcessarPedido");
    activity?.SetTag("pedido.id", pedido.Id.ToString());
    activity?.SetTag("pedido.total", pedido.CalcularTotal());
    activity?.SetTag("pedido.itens", pedido.Itens.Count);

    try
    {
        // Sub-span para verificação de estoque
        using (var estoqueActivity = _activitySource.StartActivity("VerificarEstoque"))
        {
            var estoqueOk = await _estoque.VerificarDisponibilidadeAsync(pedido.Itens, ct);
            estoqueActivity?.SetTag("estoque.disponivel", estoqueOk);

            if (!estoqueOk)
            {
                estoqueActivity?.SetStatus(ActivityStatusCode.Error, "Estoque insuficiente");
                throw new EstoqueInsuficienteException(pedido.Id);
            }
        }

        // Sub-span para processamento de pagamento
        using (var pagamentoActivity = _activitySource.StartActivity("ProcessarPagamento"))
        {
            var resultado = await _pagamento.ProcessarAsync(pedido, ct);
            pagamentoActivity?.SetTag("pagamento.gateway", resultado.Gateway);
            pagamentoActivity?.SetTag("pagamento.codigo", resultado.CodigoTransacao);
            return new PedidoConfirmadoResult(pedido.Id, resultado.CodigoTransacao);
        }
    }
    catch (Exception ex)
    {
        activity?.SetStatus(ActivityStatusCode.Error, ex.Message);
        activity?.RecordException(ex);
        throw;
    }
}

}

Métricas customizadas

public class PedidoMetrics
{
    private readonly Counter<long> _pedidosConfirmados;
    private readonly Counter<long> _pedidosCancelados;
    private readonly Histogram<double> _tempoProcessamento;
    private readonly UpDownCounter<int> _pedidosPendentes;
public PedidoMetrics(IMeterFactory meterFactory)
{
    var meter = meterFactory.Create("pedidos-api");

    _pedidosConfirmados = meter.CreateCounter&lt;long&gt;(
        "pedidos.confirmados.total",
        description: "Total de pedidos confirmados");

    _pedidosCancelados = meter.CreateCounter&lt;long&gt;(
        "pedidos.cancelados.total",
        description: "Total de pedidos cancelados");

    _tempoProcessamento = meter.CreateHistogram&lt;double&gt;(
        "pedidos.processamento.duracao",
        unit: "ms",
        description: "Duração do processamento de pedidos");

    _pedidosPendentes = meter.CreateUpDownCounter&lt;int&gt;(
        "pedidos.pendentes.atual",
        description: "Pedidos aguardando processamento no momento");
}

public void PedidoConfirmado(string canal) =>
    _pedidosConfirmados.Add(1, new KeyValuePair&lt;string, object?&gt;("canal", canal));

public void PedidoCancelado(string motivo) =>
    _pedidosCancelados.Add(1, new KeyValuePair&lt;string, object?&gt;("motivo", motivo));

public void RegistrarTempoProcessamento(double ms) =>
    _tempoProcessamento.Record(ms);

public void PedidoEntrou() => _pedidosPendentes.Add(1);
public void PedidoSaiu() => _pedidosPendentes.Add(-1);

}

// Registrar no DI builder.Services.AddSingleton<PedidoMetrics>(); // Usar no handler public class ConfirmarPedidoHandler : IRequestHandler<ConfirmarPedidoCommand, ConfirmarPedidoResult> { private readonly PedidoMetrics _metrics; // …

public async Task&lt;ConfirmarPedidoResult&gt; Handle(ConfirmarPedidoCommand req, CancellationToken ct)
{
    var sw = Stopwatch.StartNew();
    try
    {
        var result = await ProcessarAsync(req, ct);
        _metrics.PedidoConfirmado("api");
        return result;
    }
    finally
    {
        sw.Stop();
        _metrics.RegistrarTempoProcessamento(sw.Elapsed.TotalMilliseconds);
    }
}

}

Logs estruturados com contexto de trace

Quando você usa OpenTelemetry + logging estruturado, o trace_id e span_id são automaticamente injetados em cada log — isso permite correlacionar um log de erro com o trace exato que o gerou.

// Serilog com sink OTLP (exporta logs para o mesmo backend dos traces)
dotnet add package Serilog.AspNetCore
dotnet add package Serilog.Sinks.OpenTelemetry

// Program.cs Log.Logger = new LoggerConfiguration() .Enrich.FromLogContext() .Enrich.WithProperty(“service_name”, “pedidos-api”) .WriteTo.Console(new RenderedCompactJsonFormatter()) // JSON estruturado .WriteTo.OpenTelemetry(opt => { opt.Endpoint = “http://localhost:4317”; opt.ResourceAttributes = new Dictionary<string, object> { [“service.name”] = “pedidos-api” }; }) .CreateLogger();

// Uso nos handlers — campos nomeados, não interpolação de string _logger.LogInformation(“Pedido {PedidoId} confirmado para cliente {ClienteId} — total {Total:C}”, pedido.Id, pedido.ClienteId, pedido.CalcularTotal());

// O log gerado em JSON terá: // { “PedidoId”: ”…”, “ClienteId”: ”…”, “Total”: 299.90, // “TraceId”: “abc123”, “SpanId”: “def456”, … }

Stack de observabilidade self-hosted com Docker Compose

# docker-compose.observabilidade.yml
services:
  jaeger:
    image: jaegertracing/all-in-one:1.57
    ports:
      - "16686:16686"  # UI do Jaeger
      - "6831:6831/udp"
      - "4317:4317"    # OTLP gRPC

prometheus: image: prom/prometheus:v2.51.0 ports: - “9090:9090” volumes: - ./prometheus.yml:/etc/prometheus/prometheus.yml

grafana: image: grafana/grafana:10.4.0 ports: - “3000:3000” environment: GF_SECURITY_ADMIN_PASSWORD: admin volumes: - grafana_data:/var/lib/grafana

volumes: grafana_data:

Com essa stack rodando localmente você tem: Jaeger em localhost:16686 para traces, Prometheus em localhost:9090 para métricas, e Grafana em localhost:3000 para dashboards unificados.

Observabilidade na AWS

Para quem usa AWS, o stack equivalente é: AWS X-Ray para traces (exportador OTLP compatível), CloudWatch Metrics para métricas e CloudWatch Logs para logs estruturados. O OpenTelemetry exporta para todos com o exportador OTLP — você troca o endpoint de destino sem mudar o código de instrumentação.

Conclusão

Observabilidade com OpenTelemetry transforma a forma como você diagnostica problemas em produção. Em vez de adivinhar onde está o gargalo, você tem o caminho exato da requisição, com tempos por etapa, queries executadas e contexto de negócio em cada span.

O investimento na instrumentação se paga na primeira vez que você precisa diagnosticar um problema de performance em produção sem acesso SSH ao servidor — que é exatamente o cenário de um sistema cloud-native moderno.

Se você quer implementar observabilidade em uma aplicação .NET existente ou está desenhando a stack de monitoramento de um novo projeto, a Neryx pode ajudar. Consultoria inicial gratuita.

Leitura complementar:

Quer medir o nível de maturidade atual?

Comece pelo diagnóstico e veja rapidamente onde estão os gargalos de deploy, testes, observabilidade e infraestrutura.

Fazer diagnóstico

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.