Repository navigation
Observabilidade
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
traceIde como usar o Aspire Dashboard.
Código: src/Backend/PolicyDesk.Api/Observability/ e as pastas Logging/ de cada projeto.
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").
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]
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 |
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.
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....
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.
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:
-
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. - 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.
-
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.
| 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.
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.
Cada trace tem um TraceId, um identificador de 32 caracteres hexadecimais. No PolicyDesk, o mesmo valor:
- volta no cabeçalho
X-Trace-Idde toda resposta; - aparece no campo
traceIdde 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.
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
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.
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 |
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 -dAbra 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.
- Faça uma requisição que dê 400 (por exemplo, pagar um sinistro pendente). Copie o
X-Trace-Idda resposta e encontre o trace e os logs no dashboard. - Abra o trace de uma listagem de apólices e encontre as duas consultas da paginação: o
COUNTe oSELECTcomLIMIT/OFFSET. Compare com e sem o filtrostatus. - 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.
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: 10No 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.
| 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 |
- 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
traceIdliga a resposta, os logs e o trace de cada requisição. - O
/healthinforma às máquinas se a instância pode receber tráfego.
- 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.
PolicyDesk · Distribuído sob a licença MIT · Repositório
Comece aqui
Como o sistema é feito
A API
Engenharia
Prática
Consulta