Skip to content

Observabilidade

rodrigosdk edited this page Nov 11, 2026 · 2 revisions

Observabilidade: enxergando o sistema por dentro

Para quem é este artigo: para quem precisa investigar um problema em produção, e para quem quer aprender logs estruturados, traces distribuídos, métricas e OpenTelemetry.

Pré-requisitos: nenhum além de ter rodado o projeto (Primeiros Passos).

O que você vai aprender: os três sinais da observabilidade, o que é OpenTelemetry, como o PolicyDesk registra logs sem vazar dados pessoais, como seguir uma requisição de ponta a ponta pelo traceId e como usar o Aspire Dashboard.

Código: src/Backend/PolicyDesk.Api/Observability/ e as pastas Logging/ de cada projeto.


O problema: "deu erro"

Um usuário liga para o suporte: "tentei aprovar um sinistro e deu erro". Sem observabilidade, as perguntas não têm resposta: Que sinistro? Que erro? Foi o banco? Foi lento? Acontece com todo mundo ou só com ele?

Observabilidade é a capacidade de responder perguntas sobre o que o sistema está fazendo só olhando o que ele emite, sem precisar adicionar código novo nem reproduzir o problema. Um sistema observável deixa rastros que permitem reconstruir o que aconteceu.

Esses rastros vêm em três formas, os chamados três sinais:

Sinal Pergunta que responde Analogia Exemplo no PolicyDesk
Logs O que aconteceu? O diário de bordo "Sinistro 7c6b... aprovado"
Traces Por onde a requisição passou, e quanto tempo levou cada parte? O rastreamento de uma encomenda POST /aprovacao → SELECT claims (3 ms) → UPDATE claims (2 ms)
Métricas Como o sistema se comporta ao longo do tempo? O painel do carro Requisições por segundo, tempo de resposta, conexões abertas

Os três se complementam. A métrica avisa que algo está errado ("o tempo de resposta triplicou"). O trace mostra onde ("a consulta de sinistros está lenta"). O log conta o quê ("timeout na conexão com o banco").


OpenTelemetry: um padrão para todos os sinais

Durante anos, cada ferramenta de monitoramento tinha sua própria biblioteca. Trocar de ferramenta significava reescrever a instrumentação do sistema inteiro.

O OpenTelemetry (OTel) é um padrão aberto, mantido pela Cloud Native Computing Foundation, que resolve isso: você instrumenta o código uma vez, com as bibliotecas do OTel, e envia os dados por um protocolo padrão, o OTLP (OpenTelemetry Protocol). Qualquer ferramenta compatível recebe: Aspire Dashboard, Jaeger, Grafana, Datadog, Honeycomb, Azure Monitor...

flowchart LR
    API[PolicyDesk.Api<br/>instrumentada com OpenTelemetry] -->|OTLP| COL[Aspire Dashboard<br/>ou qualquer ferramenta OTLP]
    API -->|stdout| CON[Console<br/>texto ou JSON]
    COL --> L[Logs]
    COL --> T[Traces]
    COL --> M[Métricas]
Loading

A configuração no PolicyDesk

var openTelemetry = builder.Services.AddOpenTelemetry()
    .ConfigureResource(resource => resource.AddService(builder.Environment.ApplicationName))
    .WithTracing(tracing => tracing
        .AddAspNetCoreInstrumentation(options => options.RecordException = true)
        .AddHttpClientInstrumentation()
        .AddNpgsql())
    .WithMetrics(metrics => metrics
        .AddAspNetCoreInstrumentation()
        .AddHttpClientInstrumentation()
        .AddRuntimeInstrumentation()
        .AddMeter("Microsoft.EntityFrameworkCore")
        .AddMeter("Npgsql"));

if (!string.IsNullOrWhiteSpace(builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"]))
    openTelemetry.UseOtlpExporter();

Lendo em português: "identifique os dados com o nome da aplicação; colete traces das requisições HTTP recebidas, das chamadas HTTP feitas e dos comandos enviados ao PostgreSQL; colete métricas de HTTP, do runtime do .NET, do EF Core e do Npgsql; e, se houver um endereço configurado, envie tudo por OTLP".

Cada Add...Instrumentation é uma instrumentação automática: uma biblioteca que se acopla a outra e gera os dados sem você escrever nada.

Instrumentação Sinal O que captura
ASP.NET Core Traces, métricas Um registro por requisição, com rota, status, duração e exceções
HttpClient Traces, métricas Chamadas HTTP de saída (preparado para integrações futuras)
Npgsql Traces Um registro por comando SQL, com o texto da consulta
Runtime Métricas Coleta de lixo, memória, threads, exceções
EF Core Métricas Consultas, gravações, cache de consultas compiladas
Npgsql Métricas Conexões em uso e ociosas, comandos executados

Só envia se houver para onde

O envio só é ligado se a variável OTEL_EXPORTER_OTLP_ENDPOINT estiver definida. Sem ela (nos testes, por exemplo), nada é enviado, e a aplicação não fica tentando conectar em um coletor que não existe e registrando falhas.

Essa variável é padrão da especificação OpenTelemetry. O mesmo programa funciona com qualquer ferramenta compatível; muda só o valor da variável.


Logs: o diário de bordo

Logs estruturados

Um log tradicional é uma linha de texto:

2026-11-03 14:22:10 Sinistro 7c6b5a49 aprovado

Para encontrar todos os logs de um sinistro, você faria uma busca por texto, frágil e lenta. Um log estruturado guarda os dados como campos separados:

{
  "Timestamp": "2026-11-03T14:22:10.512Z",
  "EventId": 1003,
  "LogLevel": "Information",
  "Message": "Sinistro 7c6b5a49-... aprovado",
  "ClaimId": "7c6b5a49-3827-4615-9504-f3e2d1c0b9a8",
  "TraceId": "4bf92f3577b34da6a3ce929d0e0e4736"
}

Agora dá para filtrar com precisão: ClaimId = 7c6b..., EventId = 1003, TraceId = 4bf9....

Logs de requisição

O ASP.NET registra uma linha por requisição, com o mínimo necessário:

builder.Services.AddHttpLogging(options =>
{
    options.LoggingFields = HttpLoggingFields.RequestMethod
                            | HttpLoggingFields.RequestPath
                            | HttpLoggingFields.ResponseStatusCode
                            | HttpLoggingFields.Duration;
    options.CombineLogs = true;
});

Método, caminho, status e duração. O corpo, os parâmetros de consulta e os cabeçalhos ficam de fora, de propósito. Eles podem conter CPF, e-mail e telefone, que são dados pessoais protegidos pela LGPD (Lei Geral de Proteção de Dados). Logs costumam ser guardados por muito tempo e lidos por muita gente; um dado pessoal em log é um vazamento esperando para acontecer.

Logs de negócio: um catálogo de eventos

Além das requisições, os casos de uso registram eventos de negócio. Eles não são escritos com logger.LogInformation("...") espalhado pelo código; ficam em catálogos, um por projeto, com cada evento declarado como um método:

[LoggerMessage(EventId = 1003, Level = LogLevel.Information, Message = "Sinistro {ClaimId} aprovado")]
public static partial void ClaimApproved(this ILogger logger, Guid claimId);

O atributo [LoggerMessage] faz o compilador gerar o código do log (veja Design Patterns › Source-Generated Logging). O caso de uso só chama logger.ClaimApproved(claim.Id).

EventId Nível Mensagem
1001 Information Sinistro {ClaimId} registrado na apólice {PolicyId} no valor de {Amount}
1002 Information Sinistro {ClaimId} em análise
1003 Information Sinistro {ClaimId} aprovado
1004 Information Sinistro {ClaimId} negado
1005 Information Pagamento do sinistro {ClaimId} registrado no valor de {Amount}
2001 Information Apólice {PolicyId} registrada para o segurado {PolicyholderId}
2002 Information Apólice {PolicyId} cancelada
3001 Information Segurado cadastrado ou reativado
3002 Information Segurado {PolicyholderId} atualizado
3003 Information Segurado {PolicyholderId} inativado
9001 Error Erro não tratado em {Method} {Path}
9002 Information Requisição cancelada pelo cliente em {Method} {Path}

Os números seguem faixas: 1xxx sinistros, 2xxx apólices, 3xxx segurados, 9xxx a própria API. Um número fixo por evento permite criar alertas e painéis que não quebram se o texto mudar.

As regras do catálogo:

  1. O texto é um modelo fixo e não é traduzido. As ferramentas agrupam os logs pelo modelo ("Sinistro {ClaimId} aprovado"), não pelo texto final. Traduzir quebraria esses agrupamentos.
  2. Só identificadores e valores, nunca dados pessoais. Nada de nome, CPF ou e-mail. O evento 3001 não registra nem o id, porque, no cadastro com reativação, o id da entidade nova pode não ser o do registro gravado.
  3. O log vem depois da gravação. O evento é emitido depois do CommitAsync. Se o log diz "sinistro aprovado", o sinistro foi de fato gravado como aprovado.

O formato muda com o ambiente

Ambiente Formato Por quê
Development Texto em uma linha, com HH:mm:ss Para uma pessoa ler no terminal
Os demais JSON, com escopos e horário UTC no padrão ISO 8601 Para ferramentas lerem (Loki, Elasticsearch, Azure Monitor...)

Em Development, os comandos SQL gerados pelo EF Core também aparecem no console, o que é ótimo para estudar o que o ORM está fazendo.


Traces: seguindo uma requisição

Trace e span

Um trace é o registro completo de uma requisição. Ele é formado por spans: cada span é uma etapa, com início, fim e detalhes. Um span pode ter spans filhos, formando uma árvore:

POST api/sinistros/{id:guid}/aprovacao  ─────────────────────── 18 ms  204
 ├─ postgresql SELECT claims ...         ───── 3 ms
 └─ postgresql UPDATE claims ...         ──── 2 ms

Lendo: a requisição levou 18 ms no total; o SELECT levou 3 ms e o UPDATE, 2 ms. Os 13 ms restantes foram gastos no código da aplicação. Se a requisição estivesse lenta, o trace mostraria na hora qual parte é a culpada.

Em sistemas com vários serviços, o mesmo trace atravessa todos eles (por isso o nome tracing distribuído). O identificador viaja de um serviço para o outro em um cabeçalho padronizado pelo W3C, o Trace Context.

O traceId: um fio que liga tudo

Cada trace tem um TraceId, um identificador de 32 caracteres hexadecimais. No PolicyDesk, o mesmo valor:

  • volta no cabeçalho X-Trace-Id de toda resposta;
  • aparece no campo traceId de toda resposta de erro;
  • é anexado a todos os logs emitidos durante a requisição.

O código que garante o cabeçalho:

public static string Get(HttpContext httpContext) =>
    Activity.Current?.TraceId.ToHexString() ?? httpContext.TraceIdentifier;

app.Use((context, next) =>
{
    var traceId = Get(context);
    context.Response.OnStarting(() =>
    {
        context.Response.Headers[HeaderName] = traceId;
        return Task.CompletedTask;
    });

    return next(context);
});

Um detalhe que só se aprende tropeçando: o cabeçalho é gravado no OnStarting, um gancho chamado logo antes de a resposta começar a ser enviada. Por quê? Porque, quando acontece um erro, o tratamento de exceções limpa os cabeçalhos antes de escrever a resposta de erro. Se o cabeçalho fosse gravado antes, ele sumiria justamente nas respostas em que é mais necessário.

Esse identificador compartilhado se chama Correlation ID: ele correlaciona tudo o que pertence à mesma requisição.

Investigando um problema, do começo ao fim

Voltando ao usuário que ligou para o suporte:

sequenceDiagram
    participant U as Usuário
    participant S as Suporte
    participant D as Aspire Dashboard

    U->>S: "Deu erro ao aprovar. traceId 4bf92f35..."
    S->>D: Busca o trace 4bf92f35...
    D-->>S: Span HTTP → span SQL → exceção registrada
    S->>D: Filtra os logs pelo mesmo TraceId
    D-->>S: EventId 9001 com o stack trace completo
Loading

O front-end mostra o traceId na tela de erro; o usuário informa; o suporte encontra, em segundos, o caminho da requisição e o erro exato. Sem adivinhação.


Métricas: o painel do carro

Logs e traces descrevem requisições individuais. Métricas são números agregados ao longo do tempo: quantas requisições por segundo, quanto tempo levam (a média e os casos mais lentos), quanta memória está em uso.

Um termo que aparece muito: percentil. O "p95 do tempo de resposta" é o tempo abaixo do qual estão 95% das requisições. A média esconde problemas; se 5% das requisições levam 10 segundos, a média pode parecer ótima, mas o p95 denuncia.

Algumas métricas úteis que o PolicyDesk já emite:

Métrica O que mostra
http.server.request.duration Tempo das requisições, por rota e status
Conexões do Npgsql em uso e ociosas Se o pool de conexões com o banco está se esgotando
Métricas do EF Core Consultas e gravações por segundo
Métricas do runtime Memória, coleta de lixo, threads

Usando o Aspire Dashboard

O Aspire Dashboard é uma ferramenta da Microsoft que recebe OTLP e mostra os três sinais em uma interface web. É ótima para desenvolvimento.

docker compose -f docker/compose.yml -f docker/compose.dev.yml up -d

Abra http://localhost:18888. (Só com o docker/compose.yml, a API sobe sem enviar telemetria.)

Aba O que ver
Structured Logs Os logs, filtráveis por EventId, nível ou TraceId. Clique em um para ver todos os campos
Traces As requisições, com a duração. Abra uma para ver a árvore de spans e o SQL de cada consulta
Metrics Os gráficos de cada métrica

⚠️ O Compose liga o dashboard sem autenticação (ASPIRE_DASHBOARD_UNSECURED_ALLOW_ANONYMOUS=true). Isso é aceitável só na sua máquina.

Exercícios

  1. Faça uma requisição que dê 400 (por exemplo, pagar um sinistro pendente). Copie o X-Trace-Id da resposta e encontre o trace e os logs no dashboard.
  2. Abra o trace de uma listagem de apólices e encontre as duas consultas da paginação: o COUNT e o SELECT com LIMIT/OFFSET. Compare com e sem o filtro status.
  3. Dispare 50 requisições ao mesmo tempo (por exemplo, com um laço de curl & no terminal) e observe as métricas de conexões do Npgsql.

Health check: "você está vivo?"

GET /health responde se a API está no ar e consegue falar com o banco. Por baixo, ele executa AddDbContextCheck<PolicyDeskDbContext>, que tenta uma conexão.

Estado HTTP Corpo
Saudável 200 Healthy
Banco fora 503 Unhealthy

Quem usa esse endpoint são as máquinas: balanceadores de carga e orquestradores perguntam periodicamente, e param de mandar tráfego para uma instância doente. Um exemplo no Kubernetes:

readinessProbe:
  httpGet:
    path: /health
    port: 8080
  periodSeconds: 10

No PolicyDesk, o /health também é o smoke test do pipeline: logo depois do deploy, o job chama o endpoint várias vezes e falha se o banco não responder. Como o plano F1 da Azure não tem Always On, a primeira chamada depois de um tempo parado pode levar alguns segundos.


Em produção (Azure App Service)

Sinal Onde ver
Logs do console (JSON) Em Log stream, no portal do Web App, ou com az webapp log tail -g rg-policydesk -n policydesk-api-rodrigo
Traces e métricas Defina otlpEndpoint no infra/main.bicep (vira a variável OTEL_EXPORTER_OTLP_ENDPOINT) para enviar a um coletor OTLP. Sem ele, nada é enviado

Resumo

  • Observabilidade é responder perguntas sobre o sistema só olhando o que ele emite: logs, traces e métricas.
  • O OpenTelemetry instrumenta uma vez e envia para qualquer ferramenta compatível, pelo protocolo OTLP.
  • Os logs são estruturados, vêm de um catálogo de eventos numerados e nunca contêm dados pessoais.
  • O traceId liga a resposta, os logs e o trace de cada requisição.
  • O /health informa às máquinas se a instância pode receber tráfego.

Para aprofundar

  • Documentação do OpenTelemetry: conceitos de signals, traces e spans (opentelemetry.io).
  • Observability Engineering, Charity Majors, Liz Fong-Jones e George Miranda.
  • Especificação W3C Trace Context.
  • Próximo artigo sugerido: Estratégia de Testes.

Clone this wiki locally