Lab 51 — Os três pilares no .NET com OpenTelemetry
O problema, e a empresa que o tem
A Cadência cresceu. Além da API de Pedidos que o L08 deixou rastreável, existe agora Faturamento — um segundo serviço .NET 8 em ECS Fargate, atrás do mesmo ALB, na mesma VPC do L01. Foi escrito por um squad terceirizado que entrou há dois meses e processa cerca de 40 mil cobranças por dia.
Faturamento também tem log, métrica e trace. Os três, funcionando, sem erro nenhum: Serilog grava JSON estruturado no console, uma classe própria publica duração e contagem de erro com PutMetricData, e o SDK do X-Ray abre um segmento por requisição. Cada peça, isolada, passaria numa revisão de código.
O problema apareceu quando um cliente reclamou de cobrança duplicada. O plantão tinha três fontes bem alimentadas — e nenhuma pergunta respondível. O RequestId do Serilog é um GUID que o próprio middleware inventa; o trace id do segmento X-Ray é outro, criado pelo SDK sem saber que o anterior existia; a métrica não carrega identificador nenhum, só um contador que reinicia a cada deploy. Cruzar os três exigiu a mesma tática frágil de antes do L08: alinhar por hora aproximada — e sob 40 mil cobranças/dia distribuídas em várias tasks, duas requisições caem no mesmo segundo com frequência suficiente para produzir conclusão errada.
O que este laboratório ataca não é falta de instrumentação — é excesso dela sem modelo comum. Três bibliotecas bem configuradas continuam sendo três identificadores que não se conhecem. A correção não é "instrumentar mais", é trocar as três por uma só.
O que este laboratório NÃO é
Não é definição de SLO nem alarme acionável — isso é o L52. Não é o painel que responde perguntas de operação — isso é o L53. E não cobre serviço em outra linguagem além de .NET, embora o coletor e o protocolo OTLP sejam os mesmos para qualquer SDK. O que este módulo HABILITA e não trata é o L96: um agente de operação só pode produzir hipótese com evidência se a evidência já estiver correlacionada — sem o que este laboratório constrói, o agente teria as mesmas três fontes desconexas que o plantão humano tinha antes dele.
O que você vai conseguir fazer
Cada objetivo tem uma prova com número na seção de implantação. Nenhum se verifica com a sensação de ter entendido.
- Explicar por que três bibliotecas de instrumentação bem configuradas ainda produzem três identificadores que não se cruzam.
- Configurar um único builder OpenTelemetry que emite trace, métrica E log a partir do mesmo Resource, sem costura manual entre eles.
- Distinguir instrumentação automática por biblioteca, instrumentação automática sem código (zero-code) e span manual — e escolher a certa para cada caso.
- Escrever um span manual para uma unidade de lógica de negócio que nenhuma instrumentação automática cobre.
- Configurar o coletor ADOT para exportar o mesmo fluxo OTLP para X-Ray, métrica derivada e CloudWatch Logs.
- Provar, com um trace id só, que log, span automático e span manual de UMA requisição são a mesma requisição.
- Explicar o que trocar de fornecedor de observação exigiria mudar — e o que NÃO exigiria tocar.
- Diagnosticar uma segunda biblioteca reintroduzindo um identificador de correlação paralelo.
- Avaliar quando um componente experimental é aceitável em produção, e o que monitorar enquanto ele não amadurece.
O que a certificação cobra disto
| Conceito | Certificação | Como aparece aqui | O que dominar |
|---|---|---|---|
| Modelo de dados unificado do OpenTelemetry | DVA-C02, SAP-C02 | um Resource, três providers (Tracer/Meter/Logger) | que correlação é propriedade estrutural do SDK, não convenção entre times |
| ADOT como distribuição AWS do projeto CNCF | SAP-C02, DOP-C02 | coletor com receptor OTLP e exportadores AWS | que ADOT não é produto à parte — é OpenTelemetry testado e suportado pela AWS |
| Instrumentação automática por biblioteca vs. manual | DVA-C02 | `AddAspNetCoreInstrumentation()` vs `ActivitySource.StartActivity()` | quando a automática não cobre, e o custo de escrever manual |
| Instrumentação automática sem código (zero-code) | SAP-C02 | citada como alternativa para app legado; não usada neste laboratório | quando ela se justifica: não poder tocar no código-fonte |
| Neutralidade de fornecedor via OTLP | SAP-C02, DOP-C02 | trocar o exportador do coletor sem tocar na aplicação | que a aplicação fala só OTLP; quem decide o destino é o coletor |
| Propagação de contexto dentro do processo | DVA-C02 | HttpClient e Npgsql instrumentados propagam o mesmo contexto | que o contexto anda sozinho via AsyncLocal, sem parâmetro passado a mão |
| Cardinalidade de dimensão de métrica | SOA-C02, DOP-C02 | atributo de span de negócio vs. dimensão de métrica, retomando o L08 | mesma regra do L08, agora aplicada a um segundo serviço com volume próprio |
| Maturidade de componente como critério de decisão | SAP-C02 | o exportador de log do coletor é o mais novo dos três | que decisão de produção pesa maturidade, não só funcionalidade |
Onde isto costuma ser cobrado errado
A pergunta clássica descreve um sistema com log, métrica e trace implementados e pede por que a correlação falha. A armadilha é procurar erro de CONFIGURAÇÃO em cada peça — todas podem estar corretas isoladamente. A resposta esperada é estrutural: sinais que nascem de bibliotecas diferentes carregam identificadores diferentes por definição, e nenhuma quantidade de configuração individual conserta isso. É preciso uma fonte comum.
Requisitos, e como cada um muda o desenho
Requisito de observabilidade que não vira linha de configuração é aspiração. A coluna da direita é onde cada um deixou marca.
| Requisito | Valor declarado | O que ele decide no desenho |
|---|---|---|
| Um único identificador em log, métrica e trace de uma requisição | obrigatório | remove as três bibliotecas concorrentes; exige um único builder OpenTelemetry |
| Nenhuma equipe nova decide sozinha qual biblioteca usar | requisito organizacional | pacote interno compartilhado (`AddObservabilidadeCadencia`), não documentação a seguir |
| Lógica de negócio de faturamento tem span próprio | obrigatório | span manual com `ActivitySource`, registrado no `AddSource()` do builder |
| Trocar de fornecedor de observação sem tocar em código de aplicação | prova de conceito exigida | fan-out no coletor via exportador adicional; aplicação inalterada |
| Falha do coletor não derruba a API que processa cobrança | requisito absoluto | `essential = false` no contêiner do coletor, herdado do L08, com dinheiro em jogo agora |
| Log continua consultável por campo | obrigatório | log emitido pelo `LoggerProvider` do próprio OTel, não por Serilog paralelo |
| Custo de observação sob controle mesmo com dois serviços | até 8% da fatura de cada um | amostragem por cauda também em Faturamento, herdada do L08 |
| Componente experimental é aceitável nesta fase, mas rotulado | decisão explícita do time | exportador de log entra com aviso, monitoramento de self-telemetry e plano de saída |
| Evidência de auditoria de cobrança não pode se perder em silêncio | requisito de conformidade | monitorar métricas internas do coletor; não confiar em "sem erro visível" como prova |
Arquitetura mínima: três bibliotecas, três identificadores
Este é o desenho que Faturamento tem hoje, e ele é enganoso justamente por parecer maduro: não falta nenhum dos três pilares. O defeito não aparece numa revisão de código isolada — só aparece quando alguém tenta seguir UMA requisição pelas três fontes ao mesmo tempo.
- → POST /faturas — a cobrança que saiu duplicada
- → X-Amzn-Trace-Id chega, e nenhuma das três bibliotecas o lê
- → linha JSON com RequestId do middleware do Serilog
- → segmento X-Ray aberto pelo SDK, com trace id próprio
- → PutMetricData com CorrelationTag = contador local
- → filtra por horário aproximado, sem campo de correlação
- → abre o console do X-Ray e procura por horário
- → olha o gráfico, sem conseguir isolar a requisição
- Fora da AWS
- Rede e entrega
- Compute
- Gestão e governança
- Conceito de arquitetura
Este desenho não tem lacuna de instrumentação: log, métrica e trace existem os três, funcionando, sem erro. O defeito não é ausência — é que cada seta que sai da task carrega um identificador que nasceu numa biblioteca diferente, e nenhuma delas leu o que a outra escreveu. Percorra os passos e repare que o cabeçalho do ALB, a mesma peça que o L08 já tinha resolvido, chega aqui e é ignorado três vezes.
- O cabeçalho que o L08 resolveu chega aqui e é ignorado. O ALB continua inserindo `X-Amzn-Trace-Id` em toda requisição, exatamente como no L08. A diferença é que aqui NENHUMA das três bibliotecas foi escrita para lê-lo: Serilog gera o próprio `RequestId` no middleware, o SDK do X-Ray abre segmento sem semente externa, e a classe de métricas nunca sequer olhou para um cabeçalho HTTP.
- O log tem um identificador que só ele conhece. O `RequestId` do Serilog é um GUID novo a cada requisição, gerado no middleware de log — não veio de fora e não vai para fora. Ele resolve "linhas desta requisição se juntam no CloudWatch Logs" e não resolve nada além disso.
- O trace tem outro. Quando o código chama `AWSXRayRecorder.Instance.BeginSegment(...)` sem receber um contexto de trace já existente, o SDK cria um trace id NOVO. Ele não sabe que o ALB já tinha um, porque ninguém passou esse valor para o construtor do segmento.
- A métrica não tem identificador nenhum, e reinicia sozinha. `CorrelationTag` é um `int` incrementado em memória dentro do processo. Ele nem tenta ser um identificador de correlação — é um contador de chamadas que zera a cada deploy e é diferente em cada task do serviço, porque cada task tem a própria memória.
- Cruzar os três exige a mesma faixa de horário — e falha sob concorrência. Sem campo comum, a única correlação possível é "essas três coisas aconteceram por volta da mesma hora". Com 40 mil cobranças por dia distribuídas em várias tasks, dezenas de requisições caem na mesma janela de um segundo, e a correlação por proximidade de horário passa a produzir pares errados — pior que não ter resposta.
- Por que alguém instrumenta assim. A equipe da Faturamento é terceirizada e chegou há dois meses, sob prazo de entrega. Serilog e o SDK do X-Ray são o que o time já sabia usar do emprego anterior; a classe de métricas é código que "já existia" num projeto interno antigo. Cada escolha, isolada, é razoável. Ninguém decidiu, como GRUPO, que as três teriam de nascer da mesma biblioteca — e por isso ninguém é o culpado.
Antes de mudar qualquer coisa, meça a mesma grandeza que o L08 mediu: quanto tempo se leva para confirmar que uma linha de log, um segmento de trace e um ponto de métrica são a mesma requisição.
# Rode ANTES de instrumentar nada. O numero daqui e a linha de base.
# Gere uma cobranca conhecida e anote o horario exato.
curl -s -X POST "https://.../api/faturas/8f3a..." | jq .
# Tente achar, no CloudWatch Logs, no console do X-Ray e no grafico de
# metrica customizada, os TRES registros da MESMA requisicao — usando so
# o horario. Cronometre.
# Na Faturamento: entre 20 e 40 minutos, e sem garantia — o resultado e uma
# CRENCA de que os tres registros achados sao da mesma requisicao, nao uma prova.O defeito aqui é mais caro de detectar do que o do L08
No L08, a ausência de qualquer correlação era óbvia no primeiro incidente: quatro linhas de texto soltas. Aqui, cada pilar RESPONDE quando consultado isoladamente — "sim, há log da requisição", "sim, há trace", "sim, há métrica" — e essa resposta positiva engana quem revisa. O problema só aparece quando se tenta atravessar os três, e é exatamente esse o cenário de um incidente real.
Arquitetura para produção: um SDK, um Resource, um coletor
Cada peça abaixo rastreia a uma linha da tabela de requisitos. A diferença estrutural em relação ao desenho anterior não é "mais uma biblioteca boa" — é que a APLICAÇÃO para de saber quantos backends de observação existem, porque ela só fala um protocolo.
- → POST /faturas, com traceparent se o cliente enviar
- → X-Amzn-Trace-Id — lido pelo único propagador do SDK
- → OTLP/gRPC na 4317: um span, um log e uma métrica do mesmo contexto
- → log exportado via awscloudwatchlogs, TraceId como campo
- → span exportado via awsxray, mesmo trace id do log
- → métrica agregada via EMF, sem chamada extra de API
- → mesmo OTLP, outro exportador — ilustrativo
- → filter @TraceId = … — um campo, não um grep
- → abre o trace, vê o span manual do cálculo da fatura
- Fora da AWS
- Rede e entrega
- Compute
- Conceito de arquitetura
- Gestão e governança
A task deixa de conter três bibliotecas e passa a conter DOIS contêineres: a aplicação, com um único SDK falando um único protocolo, e o coletor, que decide os três destinos. A diferença estrutural não é "mais uma peça" — é que a aplicação para de saber onde a telemetria acaba indo. Percorra os passos: cada peça rastreia a um requisito da seção anterior.
- O SDK único lê o cabeçalho, uma vez. O ALB continua fazendo o que já fazia no L08: inserir ou atualizar `X-Amzn-Trace-Id`. A mudança está em quem lê — não são mais três candidatos concorrentes, é um propagador composto registrado uma única vez no builder do OpenTelemetry, que adota o contexto em vez de disputar com outras bibliotecas.
- Um Resource, três providers, um envio. `WithTracing`, `WithMetrics` e `WithLogging` são chamados no MESMO `AddOpenTelemetry()`, sobre o MESMO `ConfigureResource`. É essa estrutura compartilhada — não uma convenção entre desenvolvedores — que garante que os três sinais carregam o mesmo `service.name` e o mesmo contexto de trace corrente.
- O span manual cobre o que a automática não vê. `AddAspNetCoreInstrumentation` e `AddNpgsql` cobrem HTTP e banco sozinhos. Mas "calcular o valor da fatura, aplicando desconto e imposto" não é uma chamada de rede — é lógica de negócio pura, e nenhuma instrumentação automática cria um span para ela. O span manual, aberto com o `ActivitySource` registrado no mesmo builder, herda o `TraceId` corrente por estar dentro do mesmo fluxo de execução.
- O coletor decide o destino, a aplicação nunca soube. A partir daqui, a responsabilidade sai do processo da aplicação. O coletor recebe um único fluxo OTLP e o distribui: X-Ray para o span, `awscloudwatchlogs` para o log, EMF para a métrica agregada. A aplicação nunca importou um pacote da AWS para nenhum dos três — só falou o protocolo aberto.
- Trocar de fornecedor é uma seção nova no coletor.yaml. O nó de backend de terceiros aqui é ILUSTRATIVO — não está implantado neste laboratório. Ele existe para mostrar o tamanho real da mudança: acrescentar um exportador `otlphttp` apontando para outro provedor de observação, sem tocar em uma linha do Program.cs. É essa propriedade — e só ela — que o termo "vendor-neutral" descreve.
- O plantão entra pelo trace id, não pela hora aproximada. Contraste direto com a arquitetura anterior: em vez de cruzar três fontes por proximidade de horário, o plantão parte do `X-Correlation-Id` devolvido ao cliente na resposta e faz DUAS consultas — uma no log, outra no trace — e as duas batem, porque nasceram do mesmo contexto.
- O exportador de log é o mais novo dos três, e isso pesa na decisão. Ao contrário de `awsxray` e `awsemf`, que são maduros, `awscloudwatchlogs` está rotulado como experimental pelo próprio projeto. Isso não impede o uso — impede tratá-lo como equivalente aos outros dois sem monitorar a exportação. É o assunto da seção de segurança e da terceira falha provocada.
A decisão que define este desenho, em uma frase
Log, métrica e trace nascem do mesmo Resource porque nascem do mesmo SDK — a correlação deixa de ser algo que se CONFIGURA em cada pilar e passa a ser algo que se HERDA por construção. Quem adiciona um quarto sinal, ou um quarto serviço, não reaprende a correlacionar: chama o mesmo pacote interno e ganha a propriedade de graça.
Como o contexto viaja, ponta a ponta
O identificador não nasce no seu código — nasce no ALB, exatamente como no L08. O que muda aqui é tudo o que acontece DEPOIS dele entrar no processo: em vez de um propagador lido por acaso e um `ILogger` costurado por fora, existe uma árvore só, com uma raiz só.
O payload abaixo não é o formato de fio exato do OTLP (que é Protobuf binário) — é a estrutura conceitual que o SDK preenche UMA vez, no `Resource`, e que os três sinais carregam consigo sem precisar declará-la de novo.
// Forma SIMPLIFICADA do que o coletor recebe via OTLP — não é o wire format
// exato (que é Protobuf binário), é a estrutura conceitual que o SDK preenche
// UMA vez e anexa aos três sinais. É o que a seção anterior chama de "mesmo
// Resource": nenhum dos três teve de declarar service.name por conta própria.
{
"resource": {
"attributes": {
"service.name": "faturamento",
"service.version": "a1b2c3d", // SHA da imagem — o mesmo padrao do L03
"deployment.environment": "producao",
"cloud.platform": "aws_ecs_fargate",
"cloud.region": "us-east-1"
}
},
"trace_id": "5f2c9a1e4b3d8f7601a2c3d4e5f60718",
"log_record": {
"severity_text": "INFORMATION",
"body": "Fatura calculada para {PedidoId}",
"attributes": { "pedido.id": "8f3a...", "fatura.valor_centavos": 148900 },
"span_id": "3c7e9b0a1d2f4e56" // o mesmo span do cálculo, abaixo
},
"span": {
"name": "calcular-fatura", // o span MANUAL da seção "Construir"
"span_id": "3c7e9b0a1d2f4e56",
"parent_span_id": "1a2b3c4d5e6f7081",
"attributes": { "fatura.desconto_aplicado": true }
},
"metric_data_point": {
"name": "http.server.request.duration",
"attributes": { "http.route": "/api/faturas/{id}", "http.status_code": 200 }
// repare no que NAO esta aqui: nenhum trace_id no ponto de metrica. Metrica
// agregada nao carrega identidade de requisicao — e por isso ela e barata
// em volume e nao serve para achar UMA cobranca especifica.
}
}
O que NÃO está no ponto de métrica, e por quê
Repare que o `metric_data_point` do payload acima não carrega `trace_id`. Métrica agregada não guarda identidade de requisição — é o que a torna barata em volume. Para achar a métrica de UMA cobrança específica não existe caminho: o caminho é log e trace, que carregam identidade; métrica só diz "como o sistema se comporta em média", nunca "o que aconteceu com esta requisição".
As decisões, e o que se perde em cada uma
📋 Cadência tem duas APIs .NET 8 em ECS Fargate — Pedidos, instrumentada seguindo o L08, e Faturamento, com três bibliotecas de instrumentação que não se cruzam. Uma terceira equipe está prestes a começar um terceiro serviço, e a empresa tem duas pessoas de plataforma, sem tempo para revisar código de instrumentação de cada time.
O problema não é falta de conhecimento técnico — a Faturamento instrumentou os três pilares corretamente, cada um isolado. O problema é ausência de PADRÃO ORGANIZACIONAL: sem uma decisão de "toda equipe usa esta biblioteca", cada squad reproduz a experiência do emprego anterior. Um pacote interno com um método de extensão (`AddObservabilidadeCadencia()`) resolve isso sem depender de revisão manual: o padrão vira a linha de menor esforço, porque é mais rápido chamar um método do que escrever três integrações. O coletor, por sua vez, resolve o problema seguinte — que backend cada sinal alcança — sem a aplicação precisar saber a resposta.
Alt: Revisão manual de PR para cada novo serviço — Não escala com duas pessoas de plataforma revisando N squads, e falha do mesmo jeito que falhou aqui: revisão humana detecta o que já sabe procurar, e ninguém prescreveu "biblioteca de instrumentação" como item de checklist antes da Faturamento existir.
Alt: Manter as três bibliotecas e amarrar por convenção de nome de campo — Foi exatamente a tentativa que não funcionou: pedir para o RequestId do Serilog "ser igual" ao trace id do X-Ray exige que alguém copie um valor de uma biblioteca para outra manualmente, em todo ponto de entrada — a mesma disciplina frágil do L08, agora multiplicada por três bibliotecas em vez de uma.
Alt: Usar só o SDK do X-Ray, sem OpenTelemetry — Resolveria trace sozinho, bem, dentro do ecossistema AWS. Mas não dá log nem métrica correlacionados de graça — seria preciso reinventar a parte de log e métrica com a mesma disciplina manual do L08, e a aplicação ficaria acoplada a uma API proprietária em vez de um protocolo aberto.
Alt: Instrumentação automática sem código (zero-code) em todo serviço — Cobre bem serviço legado que não se pode alterar — não é o caso aqui, o time controla o código-fonte. Zero-code também não cria o span manual do cálculo de fatura, porque não existe automação que descubra ONDE, dentro da sua lógica de negócio, um span deveria começar.
| Alternativa | Por que perde para o pacote interno + coletor |
|---|---|
| Revisão manual de PR para cada novo serviço | Não escala com duas pessoas de plataforma revisando N squads, e falha do mesmo jeito que falhou aqui: revisão humana detecta o que já sabe procurar, e ninguém prescreveu "biblioteca de instrumentação" como item de checklist antes da Faturamento existir. |
| Manter as três bibliotecas e amarrar por convenção de nome de campo | Foi exatamente a tentativa que não funcionou: pedir para o RequestId do Serilog "ser igual" ao trace id do X-Ray exige que alguém copie um valor de uma biblioteca para outra manualmente, em todo ponto de entrada — a mesma disciplina frágil do L08, agora multiplicada por três bibliotecas em vez de uma. |
| Usar só o SDK do X-Ray, sem OpenTelemetry | Resolveria trace sozinho, bem, dentro do ecossistema AWS. Mas não dá log nem métrica correlacionados de graça — seria preciso reinventar a parte de log e métrica com a mesma disciplina manual do L08, e a aplicação ficaria acoplada a uma API proprietária em vez de um protocolo aberto. |
| Instrumentação automática sem código (zero-code) em todo serviço | Cobre bem serviço legado que não se pode alterar — não é o caso aqui, o time controla o código-fonte. Zero-code também não cria o span manual do cálculo de fatura, porque não existe automação que descubra ONDE, dentro da sua lógica de negócio, um span deveria começar. |
Construir: um builder único, e o span que a automática não cobre
O pacote interno é o que torna o padrão a linha de MENOR esforço — mais rápido chamar um método do que integrar três bibliotecas. É código pequeno de propósito: quanto mais ele fizer, mais caro fica atualizá-lo quando o OpenTelemetry lançar uma versão nova.
// ObservabilidadeCadencia/Extensoes.cs — o pacote interno que resolve o requisito
// organizacional. Uma equipe nova chama UM metodo; nao decide biblioteca nenhuma.
using OpenTelemetry.Logs;
using OpenTelemetry.Metrics;
using OpenTelemetry.Resources;
using OpenTelemetry.Trace;
namespace Cadencia.Observabilidade;
public static class Extensoes
{
// O ActivitySource compartilhado para spans MANUAIS de logica de negocio.
// Fica aqui, no pacote interno, para toda equipe registrar o MESMO nome —
// um ActivitySource com nome diferente do registrado no builder produz span
// que o SDK descarta silenciosamente, e essa e a causa da Falha 2 adiante.
public static readonly System.Diagnostics.ActivitySource FonteDeSpansDeNegocio =
new("Cadencia.Negocio");
public static IServiceCollection AddObservabilidadeCadencia(
this IServiceCollection services, IConfiguration config, string nomeServico)
{
var recurso = ResourceBuilder.CreateDefault().AddService(
serviceName: nomeServico,
serviceVersion: config["SHA_DA_IMAGEM"] ?? "dev");
services.AddOpenTelemetry()
.ConfigureResource(r => r.AddAttributes(new Dictionary<string, object>
{
["deployment.environment"] = config["ASPNETCORE_ENVIRONMENT"] ?? "producao",
}).AddService(nomeServico, serviceVersion: config["SHA_DA_IMAGEM"] ?? "dev"))
// ── Os TRES sinais saem do MESMO builder — e e essa estrutura, nao
// convencao entre desenvolvedores, que garante a correlacao. ──────────
.WithTracing(t => t
.AddSource(FonteDeSpansDeNegocio.Name) // registra o span MANUAL
.AddAspNetCoreInstrumentation(o =>
o.Filter = ctx => !ctx.Request.Path.StartsWithSegments("/health"))
.AddHttpClientInstrumentation()
.AddNpgsql()
.AddOtlpExporter())
.WithMetrics(m => m
.AddAspNetCoreInstrumentation()
.AddRuntimeInstrumentation()
.AddOtlpExporter())
// ── A peca que o L08 nao tinha: o log nasce do MESMO LoggerProvider,
// nao de um ILogger nativo costurado por fora com ActivityTrackingOptions.
.WithLogging(l => l.AddOtlpExporter());
return services;
}
}
Nome do ActivitySource errado é span perdido em silêncio
Um span manual só é exportado se o NOME do `ActivitySource` que o criou estiver registrado em `.AddSource(...)` no builder. Errar essa string — copiar e colar de outro serviço, por exemplo — não gera exceção nenhuma: o span simplesmente não aparece no trace, e a evidência do cálculo de uma cobrança específica desaparece sem log de erro algum. É a Falha 2 desta seção, e é por isso que o nome vive no pacote compartilhado em vez de ser digitado de novo em cada serviço.
Com o pacote pronto, o Program.cs de Faturamento fica menor do que a versão com três bibliotecas — não maior. É um efeito colateral que vale nomear: o caminho certo aqui também é o mais curto.
// Program.cs — Faturamento, depois da migracao. Uma chamada ao pacote interno
// substitui as tres bibliotecas concorrentes da arquitetura minima.
using Cadencia.Observabilidade;
using System.Diagnostics;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContextPool<AppDb>(o =>
o.UseNpgsql(builder.Configuration.GetConnectionString("Padrao")));
// Uma linha. Nao ha Serilog, nao ha AWSXRayRecorder, nao ha MetricsPublisher
// caseiro — os tres foram removidos do .csproj. E o requisito organizacional
// da tabela anterior, em codigo: nenhuma equipe decide sozinha como instrumentar.
builder.Services.AddObservabilidadeCadencia(builder.Configuration, nomeServico: "faturamento");
// Devolve o identificador ao cliente — a mesma pratica do L08. Sem isto, o
// ticket volta a chegar so com um horario aproximado.
var app0 = builder.Build();
app0.Use(async (ctx, next) =>
{
var trace = Activity.Current?.TraceId.ToString();
if (trace is not null) ctx.Response.Headers["X-Correlation-Id"] = trace;
await next();
});
var app = app0;
app.MapPost("/api/faturas/{pedidoId:guid}", async (
Guid pedidoId, AppDb db, ILogger<Program> log) =>
{
// Sem TraceId passado a mao: o escopo ambiental do Activity ja garante que
// este evento sai carregando o contexto corrente — herdado do WithLogging.
log.LogInformation("Calculando fatura para {PedidoId}", pedidoId);
var pedido = await db.Pedidos.FindAsync(pedidoId);
if (pedido is null) return Results.NotFound();
// ── O SPAN MANUAL: nenhuma instrumentacao automatica cobre "calcular valor
// com desconto e imposto", porque nao e uma chamada de rede nem de banco —
// e logica de negocio pura. StartActivity herda o Activity.Current como pai
// por padrao: o span aparece na MESMA arvore, com o MESMO trace id.
using var span = Extensoes.FonteDeSpansDeNegocio.StartActivity("calcular-fatura");
var valor = CalcularValorComDescontoEImposto(pedido);
span?.SetTag("fatura.valor_centavos", valor);
span?.SetTag("fatura.desconto_aplicado", pedido.TemDesconto);
var fatura = new Fatura { PedidoId = pedidoId, ValorCentavos = valor };
db.Faturas.Add(fatura);
await db.SaveChangesAsync();
log.LogInformation("Fatura {FaturaId} criada para {PedidoId}: {ValorCentavos}",
fatura.Id, pedidoId, valor);
return Results.Created($"/api/faturas/{fatura.Id}", fatura);
});
app.MapHealthChecks("/health/ready");
app.Run();
static long CalcularValorComDescontoEImposto(Pedido pedido) =>
(long)(pedido.SubtotalCentavos * (pedido.TemDesconto ? 0.9m : 1m) * 1.08m);
Instrumentação automática, zero-code e manual — as três, lado a lado
`AddAspNetCoreInstrumentation` e `AddNpgsql` são instrumentação automática POR BIBLIOTECA: você adiciona uma linha, e HTTP e banco passam a gerar span sozinhos. A instrumentação SEM CÓDIGO (zero-code, via agente/injeção) vai um passo além — nem essa linha é necessária, útil quando não se pode alterar o código-fonte de um serviço legado. Nenhuma das duas cobre "calcular o valor da fatura": isso é lógica de negócio, não uma chamada de rede, e por isso recebe o span MANUAL do bloco acima.
Construir: o coletor com fan-out para os três backends
O coletor recebe um protocolo — OTLP — e decide três destinos. É essa fronteira que torna a aplicação neutra de fornecedor: ela nunca importou um SDK da AWS diretamente para nenhum dos três pilares.
# coletor.yaml — Faturamento. Mesma base do L08, com o exportador de log que
# fecha a lacuna: la, log saia pelo driver awslogs do conteiner, fora do OTLP.
receivers:
otlp:
protocols:
grpc:
endpoint: 127.0.0.1:4317 # so a propria task alcanca; awsvpc compartilha rede
http:
endpoint: 127.0.0.1:4318
processors:
batch/traces:
timeout: 5s
send_batch_size: 128
# Amostragem por cauda, herdada do L08: decide DEPOIS de o trace terminar.
# Aqui pesa mais ainda — Faturamento mexe com dinheiro, e "erro raro que a
# amostra de cabeca perderia" e exatamente o tipo de evento que a auditoria
# de uma cobranca precisa conseguir encontrar.
tailsampling:
decision_wait: 10s
policies:
- name: erros-sempre
type: status_code
status_code: { status_codes: [ERROR] }
- name: lentas-sempre
type: latency
latency: { threshold_ms: 2000 }
- name: resto-amostrado
type: probabilistic
probabilistic: { sampling_percentage: 5 }
resourcedetection:
detectors: [env, ecs]
exporters:
awsxray:
indexed_annotations: [fatura.desconto_aplicado, deployment.environment]
awsemf:
namespace: Faturamento
log_group_name: /ecs/faturamento/emf
dimension_rollup_option: NoDimensionRollup
metric_declarations:
# Mesma decisao de custo do L08: rota x status, nunca id de cliente ou de
# requisicao — cada combinacao distinta e uma serie nova.
- dimensions: [[http.route, http.status_code]]
metric_name_selectors: ['http.server.request.duration']
# NOVO em relacao ao L08: log tambem sai pelo coletor, nao pelo driver do
# conteiner. INCERTEZA DECLARADA: o projeto rotula este exportador como
# experimental e sujeito a mudanca de campo — confira o README do
# `awscloudwatchlogsexporter` na versao do ADOT Collector que voce fixar
# antes de depender dele para auditoria. E por isso que a secao de
# Seguranca trata perda silenciosa deste exportador como risco nomeado, e
# a Falha 3 provoca exatamente esse cenario.
awscloudwatchlogs:
log_group_name: /otel/faturamento/app
log_stream_name: faturamento-app
# Exportador ILUSTRATIVO do diagrama de producao — NAO ativo no pipeline
# abaixo. Mostra o tamanho real de "trocar de fornecedor": esta secao, e
# nenhuma linha em Program.cs.
# otlphttp/terceiro:
# endpoint: https://otel.exemplo-de-terceiro.com:4318
service:
# Telemetria do PROPRIO coletor — e o que a Prova 5 e a Falha 3 consultam
# para saber se o exportador experimental esta de fato entregando.
telemetry:
metrics:
level: detailed
pipelines:
traces:
receivers: [otlp]
processors: [resourcedetection, tailsampling, batch/traces]
exporters: [awsxray]
metrics:
receivers: [otlp]
processors: [resourcedetection]
exporters: [awsemf]
logs:
receivers: [otlp]
processors: [resourcedetection]
exporters: [awscloudwatchlogs]
O exportador de log é o único dos três que não está maduro
`awsxray` e `awsemf` são exportadores estáveis, usados desde o L08. `awscloudwatchlogs` está rotulado pelo próprio projeto OpenTelemetry Collector Contrib como suporte experimental, sujeito a mudança de campo entre versões. Isso não é motivo para não usá-lo — é motivo para monitorar a self-telemetry do coletor em vez de assumir que "sem erro visível na aplicação" significa "o log chegou". A Prova 5 e a Falha 3 desta seção tratam exatamente disso.
Construir: Terraform — o delta de IAM e os dois grupos de log
O serviço ECS, o ALB e o RDS de Faturamento seguem o mesmo padrão do L01/L03 e não são repetidos aqui. O que muda de verdade em relação ao L08 é o IAM do coletor — que agora escreve em DOIS destinos além do X-Ray — e a definição da task, que ganha as variáveis que apontam a aplicação para o coletor local.
# observabilidade-faturamento.tf — o DELTA sobre o L08: dois grupos de log
# novos e o IAM que falta para o coletor escrever log via OTLP, nao so metrica
# e trace. O servico ECS, o ALB e o RDS de Faturamento seguem o mesmo padrao
# do L01/L03 e nao sao repetidos aqui.
resource "aws_cloudwatch_log_group" "faturamento_app" {
name = "/otel/faturamento/app"
retention_in_days = 14
kms_key_id = aws_kms_key.app.arn
}
resource "aws_cloudwatch_log_group" "faturamento_emf" {
# Grupo SEPARADO do de log de aplicacao, com retencao mais curta: e metrica
# derivada, nao evidencia de auditoria — a mesma distincao de dois grupos do L08.
name = "/ecs/faturamento/emf"
retention_in_days = 3
}
data "aws_iam_policy_document" "coletor_faturamento" {
statement {
sid = "Tracos"
effect = "Allow"
actions = ["xray:PutTraceSegments", "xray:PutTelemetryRecords"]
# X-Ray nao aceita Resource especifico nestas duas acoes — sao de conta,
# nao de recurso. E o mesmo padrao de justificativa do ecr:GetAuthorizationToken no L03.
resources = ["*"]
}
statement {
sid = "MetricaEmbutidaELogEstruturado"
effect = "Allow"
actions = [
"logs:CreateLogStream",
"logs:PutLogEvents",
"logs:DescribeLogStreams",
]
# Ao contrario do X-Ray, PutLogEvents ACEITA arn de recurso — entao aqui o
# "*" nao se justifica. Escopar aos dois grupos e o que impede o coletor de
# escrever em QUALQUER log group da conta.
resources = [
aws_cloudwatch_log_group.faturamento_app.arn,
"${aws_cloudwatch_log_group.faturamento_app.arn}:*",
aws_cloudwatch_log_group.faturamento_emf.arn,
"${aws_cloudwatch_log_group.faturamento_emf.arn}:*",
]
}
}
resource "aws_iam_role_policy" "coletor_faturamento" {
name = "coletor-otel-faturamento"
role = aws_iam_role.task_faturamento.id # task role, NAO execution role — quem fala com X-Ray/Logs e a APLICACAO/coletor
policy = data.aws_iam_policy_document.coletor_faturamento.json
}
# Delta na definicao de task: segundo conteiner nao-essencial, e as variaveis
# que apontam a aplicacao para o coletor local em vez de para um SDK proprietario.
resource "aws_ecs_task_definition" "faturamento" {
family = "faturamento"
requires_compatibilities = ["FARGATE"]
network_mode = "awsvpc"
cpu = 512
memory = 1024
execution_role_arn = aws_iam_role.execucao_faturamento.arn
task_role_arn = aws_iam_role.task_faturamento.arn
container_definitions = jsonencode([
{
name = "api"
image = "${aws_ecr_repository.faturamento.repository_url}:${var.sha_da_imagem}"
essential = true
portMappings = [{ containerPort = 8080, protocol = "tcp" }]
environment = [
{ name = "OTEL_EXPORTER_OTLP_ENDPOINT", value = "http://localhost:4317" },
{ name = "OTEL_SERVICE_NAME", value = "faturamento" },
{ name = "SHA_DA_IMAGEM", value = var.sha_da_imagem },
]
secrets = [{
name = "ConnectionStrings__Padrao"
valueFrom = aws_secretsmanager_secret.banco.arn
}]
# SEM logConfiguration apontando para o driver awslogs: o log sai pelo
# coletor agora. Repare no que isso significa — se o coletor cair, o log
# da aplicacao para de sair tambem, e e por isso que o proximo bloco
# marca o coletor como nao-essencial em vez de deixar o log em silencio.
dependsOn = [{ containerName = "coletor", condition = "START" }]
},
{
name = "coletor"
image = "public.ecr.aws/aws-observability/aws-otel-collector:latest"
essential = false # falha do coletor NAO derruba a API que processa cobranca
command = ["--config=/etc/coletor.yaml"]
logConfiguration = {
logDriver = "awslogs"
options = {
"awslogs-group" = aws_cloudwatch_log_group.faturamento_emf.name
"awslogs-region" = var.regiao
"awslogs-stream-prefix" = "coletor"
}
}
},
])
}
Por que o X-Ray usa "*" e o CloudWatch Logs não
`xray:PutTraceSegments` e `xray:PutTelemetryRecords` são operações de CONTA — não aceitam ARN de recurso, então escrevê-las com um ARN específico simplesmente não autoriza nada. `logs:PutLogEvents` aceita ARN de grupo, e por isso o recurso é restrito aos dois grupos que este módulo criou. A regra continua a mesma do L03: todo `*` carrega uma frase explicando por que não pode ser mais estreito.
Implantar, e provar que a requisição é a mesma nos três pilares
Cinco provas. Nenhuma aceita "os três pilares existem" como resultado — cada uma compara um identificador contra outro, ou mede uma contagem.
# provas.sh — cinco medicoes; nenhuma conclusao vem de "os tres pilares existem"
PROJETO=faturamento; REGIAO=us-east-1
# ── Prova 1: UM trace id aparece no log, no span e no atributo de negocio ────
TID=$(curl -s -o /dev/null -D - -X POST "https://$(terraform output -raw dominio)/api/faturas/8f3a..." \
| grep -i x-correlation-id | tr -d '\r' | cut -d' ' -f2)
echo "trace id devolvido ao cliente: $TID"
aws logs start-query --log-group-name /otel/faturamento/app \
--start-time $(($(date +%s)-120)) --end-time $(date +%s) \
--query-string "fields @message | filter TraceId = \"$TID\" | stats count() as achados" \
--query queryId --output text
# Esperado: achados >= 1. Zero significa que WithLogging nao esta ligado ao
# mesmo Resource — e a Falha 1 desta secao.
aws xray get-trace-summaries --time-range-type TraceId --trace-ids "$TID" \
--query 'TraceSummaries[0].Id' --output text
# Esperado: o MESMO id do log. IDs diferentes e a Falha 2: uma segunda
# biblioteca abriu segmento proprio.
# ── Prova 2: o span manual esta na arvore, nao e um trace a parte ───────────
aws xray batch-get-traces --trace-ids "$TID" \
--query 'Traces[0].Segments[].Document' --output text | grep -c "calcular-fatura"
# Esperado: 1. Zero significa ActivitySource nao registrado no AddSource() do
# builder — o span existe no processo e o SDK descarta antes de exportar.
# ── Prova 3: falha do coletor nao derruba a API que processa cobranca ───────
aws ecs stop-task --cluster $PROJETO \
--task "$(aws ecs list-tasks --cluster $PROJETO --service-name $PROJETO \
--query 'taskArns[0]' --output text)" --reason "prova de essential=false"
sleep 5
for i in 1 2 3 4 5; do
curl -s -o /dev/null -w '%{http_code} ' "https://$(terraform output -raw dominio)/health/ready"
done
echo
# Esperado: cinco 200. Qualquer coisa diferente de 200 significa que o
# coletor foi marcado essential=true por engano.
# ── Prova 4: a cardinalidade da metrica continua baixa em 40 mil req/dia ────
aws cloudwatch list-metrics --namespace Faturamento \
--query 'length(Metrics)' --output text
# Esperado: dezenas, nao milhares. Se subir na casa de milhares, alguma
# dimensao de alta cardinalidade entrou no metric_declarations — e o
# Anti-padrao 5 desta secao.
# ── Prova 5: o exportador de log esta de fato entregando (self-telemetry) ───
aws logs filter-log-events --log-group-name /ecs/faturamento/emf \
--filter-pattern '"otelcol_exporter_send_failed_log_records"' \
--start-time $(( ($(date +%s) - 300) * 1000 )) \
--query 'events[].message' --output text
# Esperado: vazio. Qualquer linha aqui significa que o exportador
# experimental esta descartando log silenciosamente — e a Falha 3.
| Prova | Compara | Resultado que aprova | O que reprova, e o que significa |
|---|---|---|---|
| 1 · Log encontra o trace id | TraceId devolvido ao cliente vs. campo no log | ao menos 1 evento de log com aquele TraceId | zero significa `WithLogging` fora do builder — o log ainda está solto |
| 2 · Trace id bate entre log e X-Ray | trace id do log vs. `Id` do X-Ray | os dois valores são idênticos | valores diferentes indicam uma segunda biblioteca criando trace próprio |
| 3 · O span manual está na árvore | contagem de `calcular-fatura` no documento do trace | exatamente 1 | zero significa `ActivitySource` não registrado em `AddSource()` — span descartado |
| 4 · Coletor cai, API sobrevive | HTTP 200 em `/health/ready` com o coletor parado | cinco de cinco respostas 200 | qualquer falha indica `essential = true` por engano no contêiner do coletor |
| 5 · Exportador de log não está falhando | métrica interna `otelcol_exporter_send_failed_log_records` | nenhuma linha nos últimos 5 minutos | qualquer linha é perda silenciosa de log — a Falha 3 desta seção |
Quebrar de propósito: três falhas e o diagnóstico
As três reproduzem defeitos reais deste padrão — não do padrão anterior. Provocar cada uma e ver o sintoma é o que torna a prova da seção anterior mais do que um comando decorado.
| Falha | Como provocar | Sintoma | Onde olhar | Correção |
|---|---|---|---|---|
| Log sem TraceId, mesmo com trace funcionando | remova `.WithLogging(...)` do builder, mantendo `WithTracing` e `WithMetrics` | span aparece no X-Ray normalmente; o log volta a não ter campo de correlação | o `LoggerProvider` do processo — confirme se ele está registrado no builder do OTel | registrar `WithLogging` no MESMO `AddOpenTelemetry()` que já tem os outros dois sinais |
| Dois trace ids para a mesma requisição | adicione, por engano, um pacote NuGet transitivo que também chama `Activity.Start()` com um `ActivitySource` fora do registrado | a Prova 2 falha: o trace id do log não bate com o `Id` do X-Ray | contagem de `ActivitySource` distintos em uso no processo, e as dependências transitivas do `.csproj` | remover a biblioteca duplicada; instrumentação nova entra só pelo pacote interno padrão |
| Log perdido em silêncio pelo exportador experimental | configure `log_group_name` do `awscloudwatchlogs` para um grupo que a task role NÃO tem permissão de escrever | span e métrica continuam chegando normalmente; log some, sem erro na aplicação | métrica interna `otelcol_exporter_send_failed_log_records`, exposta pela self-telemetry do coletor | corrigir o IAM; e, em produção, alarmar sobre essa métrica interna em vez de confiar em "sem erro visível" |
A Falha 3 é a mais séria das três, porque não tem sintoma no seu lado
Nas duas primeiras falhas, alguém percebe: o log não correlaciona, ou os dois trace ids não batem — sinais visíveis na Prova 1 e na Prova 2. Na terceira, a aplicação continua respondendo 200, o trace continua completo, a métrica continua subindo. O ÚNICO lugar onde o defeito aparece é dentro da telemetria do próprio coletor. Para um serviço que processa cobrança, log perdido em silêncio é evidência de auditoria que deixou de existir sem ninguém saber — e é por isso que este risco é rotulado como perda irreversível, não como inconveniência.
Segurança: telemetria de cobrança é dado sensível e é vetor
Instrumentar mais superfície cria mais lugar para dado sensível vazar por engano, e Faturamento lida com valor monetário e identificador de pedido — não é hipotético.
| Risco | Probabilidade | Impacto | Prevenção | Detecção | Resposta |
|---|---|---|---|---|---|
| Atributo de span carrega dado pessoal do cliente sem intenção | média | alto | lista fechada de atributos permitidos em `SetTag`; revisão de `EnrichWith` | varredura de padrão (CPF, e-mail) nos atributos indexados do X-Ray | purgar o span/log afetado; revisar o ponto de instrumentação que o gerou |
| Token ou dado de pagamento em atributo de span | baixa | alto | nunca colocar dado de cartão em `SetTag` — só identificador de referência interno | varredura do mesmo padrão do risco anterior, com regra específica de PAN | rotacionar o que foi exposto; é o mesmo protocolo de segredo vazado do L03 |
| Exportador de log experimental falha e mascara perda de evidência | média | alto | monitorar `otelcol_exporter_send_failed_log_records` (self-telemetry do coletor) | alarme sobre a métrica interna, não sobre ausência de erro na aplicação | plano de saída: manter fallback de log via driver do contêiner enquanto o exportador amadurece |
| IAM do coletor mais amplo que o necessário | baixa | médio | `Resource` escopado aos dois grupos de log específicos, nunca `*` onde a ação aceita ARN | IAM Access Analyzer sobre uso real do papel do coletor | derivar a política do uso medido — é o L41 |
| Amostragem por cauda descarta o trace de uma cobrança com erro | baixa | alto | regra "erros-sempre" e "lentas-sempre" no `tailsampling`, herdada do L08 | comparar contagem de erro na métrica com contagem de trace de erro indexado | se divergir, revisar as políticas de amostragem antes de qualquer outra coisa |
| Backend de terceiros ilustrativo vira real sem revisão de conformidade | baixa | alto | mudança de exportador no coletor passa por revisão de arquitetura como qualquer novo destino de dado | CloudTrail/Config em alteração da configuração do coletor | reverter a configuração; auditar o que já foi exportado antes da reversão |
O paradoxo desta seção
O mesmo coletor que RESOLVE a correlação também CENTRALIZA o risco: antes, um vazamento de dado sensível exigiria erro em três lugares diferentes; agora, um erro na configuração do coletor afeta os três sinais de uma vez. Centralizar não é grátis — é trocar um risco disperso por um risco concentrado, e por isso a revisão do `coletor.yaml` merece o mesmo rigor que a revisão de código de aplicação.
A equipe de Faturamento tem log estruturado (Serilog), trace (SDK do X-Ray) e métrica (`PutMetricData`) funcionando sem erro — e ainda assim não consegue correlacionar uma requisição entre os três. Qual é a causa raiz?
Observabilidade: as perguntas que o painel tem de responder
| Pergunta | Métrica ou consulta | O que significa mudar | Limiar inicial |
|---|---|---|---|
| Esta cobrança específica foi processada corretamente? | Logs Insights por `TraceId` | a pergunta que a arquitetura mínima não conseguia responder em menos de 20 minutos | sempre respondível, não é alarme |
| Quantas cobranças falharam na última hora? | métrica EMF de erro por rota | taxa de erro subindo é o primeiro sinal de algo sistêmico | > 1% em 5 min |
| O cálculo da fatura está lento, ou é a chamada ao banco? | duração do span manual vs. subsegmento do banco | reparte o tempo entre lógica de negócio e I/O — sem isso, "está lento" não diz onde | span manual > 200 ms investiga-se separado do banco |
| O coletor está exportando tudo, ou perdendo telemetria? | `otelcol_exporter_send_failed_*` (self-telemetry) | sem esta métrica, perda de log é invisível — é a Falha 3 | qualquer valor > 0 |
| Alguém reintroduziu uma segunda biblioteca de instrumentação? | contagem de trace roots por requisição | mais de uma raiz por requisição é o sintoma exato da arquitetura mínima voltando | > 1 raiz para a mesma requisição |
| O custo de observação está dentro do orçamento? | ingestão de log + spans indexados / fatura total do serviço | ultrapassar 8% é sinal de amostragem mal calibrada ou dimensão de alta cardinalidade | > 8% da fatura do serviço |
A métrica que não existia antes deste laboratório
A self-telemetry do coletor — métricas SOBRE o próprio pipeline de observação — não tem equivalente na arquitetura mínima, porque lá não havia coletor. Ignorá-la é o erro mais fácil de cometer: ela é a ÚNICA fonte que expõe a Falha 3, já que a aplicação, o trace e a métrica de negócio continuam todos aparentemente saudáveis.
Escala: 10, 10 mil, 1 milhão
| Volume | O que acontece com o pipeline de telemetria | O que passa a doer | O que fazer |
|---|---|---|---|
| 10 cobranças/dia, 1 task | o sidecar entrega sem esforço perceptível | nada | nada — é o cenário de teste deste laboratório |
| 40 mil cobranças/dia, poucas tasks (Faturamento hoje) | cada task carrega o próprio coletor, com CPU/memória reservados | ainda desprezível: poucas tasks, pouco overhead agregado | nada — o modelo sidecar continua sendo a escolha certa |
| 500 mil requisições/dia somando Pedidos e Faturamento, dezenas de tasks | o overhead do coletor escala LINEARMENTE com o número de tasks, não com o volume por task | dezenas de coletores parcialmente ociosos reservando CPU/memória que a aplicação não usa | considerar o modelo "service" (gateway): poucos coletores compartilhados atrás de um balanceador — é o nível 3 da evolução adiante |
| 1 milhão de requisições/dia, centenas de tasks | centenas de sidecars, cada um pequeno, somam uma linha visível na fatura de Fargate só de telemetria | reserva de CPU/memória "para o caso de pico" multiplicada por centenas de tasks | gateway com `memory_limiter` dimensionado pelo volume real, e sidecar local só enfileirando e repassando |
| Uma requisição que atravessa Pedidos → Faturamento (chamada HTTP entre serviços) | CADA sidecar decide a amostragem por cauda de forma independente, vendo só os spans que passaram por ele | o span de um serviço pode ser retido e o do outro descartado — o mesmo trace, cortado ao meio | amostragem por cauda cross-service exige o modelo gateway, que vê todos os spans do trace antes de decidir |
| Falha de AZ | a aplicação sobrevive de qualquer forma — o coletor já é `essential = false` | se o gateway do nível 3 estiver concentrado numa AZ só, a telemetria da AZ inteira some, mesmo com a aplicação respondendo | gateway replicado em pelo menos duas AZs, atrás de um balanceador de rede |
A amostragem por cauda cross-service tem um buraco que este laboratório não fecha
Quando uma requisição atravessa dois serviços — Pedidos chamando Faturamento, por exemplo — cada sidecar decide, sozinho, se retém ou descarta os SEUS spans daquele trace. Não existe coordenação entre os dois coletores. O resultado possível é um trace pela metade: o segmento de um serviço retido porque viu um erro, e o do outro descartado porque, isolado, pareceu uma chamada comum. Para telemetria que serve de evidência de cobrança, span cortado é evidência incompleta — é por isso que este risco entra como perda, e não como inconveniência. A correção estrutural é o modelo gateway do próximo nível de evolução, que recebe todos os spans do trace antes de decidir amostrar.
Custo: o que a correlação estruturada muda na fatura
A mudança de arquitetura deste laboratório não introduz recurso novo em peso — introduz um segundo contêiner por task e reorganiza como os três sinais chegam aos backends. O efeito no custo é mais sutil do que "mais uma peça", e vale medir cada dimensão separadamente.
| Cenário | Volume | O que acrescenta | Tendência | Otimização |
|---|---|---|---|---|
| Protótipo | 1 task de Faturamento, poucas cobranças/dia | CPU/memória mínimos do sidecar; log e span de volume desprezível | desprezível | nenhuma — otimizar aqui é gastar atenção onde não há dinheiro |
| Produção pequena | Faturamento sozinho, 40 mil cobranças/dia, sidecar por task | ingestão de log estruturado, span amostrado por cauda, métrica via EMF — sem chamada extra de API | baixa e previsível | os dois grupos de log com retenção diferenciada (14 e 3 dias), herdados do L08 |
| Alta escala | Pedidos e Faturamento juntos, dezenas de tasks, 500 mil+ requisições/dia | o overhead do coletor por task some do lado da aplicação e vira linha própria na fatura de Fargate | passa a ser visível | consolidar em gateway compartilhado (nível 3 da evolução), reduzindo a contagem de coletores |
| Dimensão | Cobra por | Cuidado |
|---|---|---|
| CPU/memória reservado do coletor sidecar | vCPU-segundo e GB-segundo por task, mesmo ocioso | reservar memória "por segurança" em centenas de tasks é fatura de Fargate só de telemetria — dimensione pelo `memory_limiter` medido, não por um chute |
| Ingestão e retenção do CloudWatch Logs | GB ingerido e retido, em dois grupos (app e EMF) | log de negócio (14 dias) e log derivado de métrica (3 dias) têm ciclos diferentes por propósito diferente — a mesma decisão do L08 |
| Spans indexados no X-Ray | span retido pela política de amostragem por cauda | "erros sempre, lentas sempre" mantém 100% do que importa; a alavanca de custo real está no percentual do "resto amostrado" |
| `PutMetricData` (arquitetura MÍNIMA, para contraste) | chamada de API, além do free tier | é a diferença de custo mais direta entre as duas arquiteturas — ver o callout abaixo |
| Tráfego OTLP entre aplicação e coletor | nada — fica dentro da própria task | localhost dentro da mesma rede `awsvpc`; não atravessa o ENI da task nem paga transferência |
O ganho que não aparece comparando linha por linha
Substituir `PutMetricData` por métrica derivada de EMF não é só arquitetura mais limpa: é economia direta de chamada de API, porque a métrica nasce do MESMO evento de log que já ia ser ingerido. Em 40 mil cobranças/dia, a arquitetura mínima fazia até uma chamada de `PutMetricData` por evento medido; a de produção faz zero — o custo migrou de "por chamada" para "incluído na ingestão que já acontecia".
O coletor sidecar cobra mesmo parado, e a soma some da vista em poucas tasks
Reservar CPU/memória para o coletor é reserva de CAPACIDADE, não de uso: a task paga pelo vCPU e pela memória alocados ao contêiner do coletor mesmo nos segundos em que ele não está processando nada. Com duas ou três tasks isso é ruído. Com centenas — a escala de "1 milhão de requisições/dia" da seção anterior — a soma dessas reservas ociosas vira uma linha própria na fatura de Fargate, e é exatamente o motivo pelo qual a seção de Escala recomenda migrar para o modelo gateway antes de chegar lá.
Well-Architected nos seis pilares
| Pilar | Situação ao fim deste laboratório | Risco que fica | Melhoria | Prioridade |
|---|---|---|---|---|
| Excelência operacional | requisição de Faturamento rastreável por um único trace id, presente em log, span e métrica | a correlação existe, mas ninguém acorda por causa dela — não há SLO nem alarme acionável | error budget e alarme ligado a plantão (L52) | alta |
| Segurança | IAM do coletor escopado por recurso onde a ação aceita, segredo por referência de ARN | exportador de log experimental sem critério formal de quando trocar ou desativar | plano de saída explícito para o `awscloudwatchlogsexporter`, revisado por versão do ADOT | média |
| Confiabilidade | coletor não-essencial: falha dele não derruba a API que processa cobrança | amostragem por cauda decidida isoladamente por sidecar corta trace que atravessa dois serviços | gateway compartilhado que decide sobre o trace inteiro (nível 3 da evolução) | alta |
| Eficiência de performance | telemetria sai localmente por OTLP dentro da task, sem round-trip externo no caminho da requisição | sidecar por task não é o modelo mais eficiente em frota grande | gateway dimensionado por volume, não por contagem de tasks | média |
| Otimização de custos | métrica derivada de EMF eliminou a chamada de API por medição que a arquitetura mínima pagava | reserva de CPU/memória do coletor cresce linearmente com a frota, mesmo ociosa | consolidar em gateway antes de a frota crescer mais uma ordem de grandeza | média |
| Sustentabilidade | um SDK e um coletor por task, em vez de três bibliotecas concorrentes fazendo trabalho redundante | centenas de sidecars processando pouco volume cada um é uso pouco eficiente de CPU reservada | o gateway do nível 3 concentra processamento e reduz a contagem de processos ociosos | baixa |
Evolução em níveis: da instrumentação isolada ao dado que um agente usa
A terceira arquitetura não é um desenho: é a resposta a QUANDO trocar de desenho. Cada nível resolve um risco e compra outro, e é a segunda coluna que raramente se escreve.
Cada serviço instrumenta sozinho, com a biblioteca que a equipe já conhece — Serilog, SDK do X-Ray, classe própria de métrica. É onde Faturamento estava.Um SDK OpenTelemetry, um Resource, um coletor ADOT sidecar por task, com fan-out para X-Ray, CloudWatch Logs e EMF.Modelo "service": poucos coletores compartilhados atrás de um balanceador de rede, recebendo de sidecars locais que passam a só enfileirar e repassar. A decisão de amostragem por cauda vê o trace inteiro, cross-service.SLO por serviço, error budget e alarme ligado a plantão em vez de painel que ninguém olha (L52), construído sobre o trace id que este laboratório entrega correlacionado.O pacote interno (`AddObservabilidadeCadencia`) vira módulo publicado e versionado, com dono; mudança no `coletor.yaml` passa por revisão de arquitetura como qualquer novo destino de dado; painel multi-serviço (L53).O histórico de trace, log e métrica — já correlacionado pelo mesmo trace id — vira DADO consultável em lote, servindo de base para um MODELO de detecção de anomalia sobre a métrica agregada, ou para um agente de IA (L96) que usa o trace id como chave para buscar evidência automaticamente antes de acordar alguém às três da manhã.A ordem não é negociável, e o motivo é concreto
Um agente de operação (L96) que usa o trace id para buscar evidência automaticamente depende de a evidência já estar correlacionada — que depende do coletor decidir sobre o trace inteiro (nível 3) — que depende do SDK único deste laboratório (nível 2). Pular para "IA que diagnostica incidente" sem os níveis anteriores dá a um modelo três fontes desconexas, exatamente o problema que o plantão humano tinha antes deste laboratório existir.
Onde IA entra nesta arquitetura, e onde não entra
Neste módulo, IA não resolve o problema central. "Log, métrica e trace não se correlacionam" tem causa estrutural e determinística — três bibliotecas geram três identificadores — e a correção também é determinística: um SDK, um Resource. Um modelo não decide melhor do que o builder qual identificador usar; ele simplesmente não participa dessa decisão.
Há um lugar honesto onde IA acrescentaria valor, e ele só existe PORQUE este laboratório correlacionou os três pilares primeiro: um agente ou um modelo que usa o trace id como chave para reunir evidência — log, span, métrica — antes de um humano ser acordado. Sem a correlação, o agente teria as mesmas três fontes desconexas que o plantão tinha; é o L96 que constrói esse agente, e ele lista este laboratório como dependência.
| Pergunta | Resposta honesta para este módulo |
|---|---|
| Qual problema a IA resolveria? | reunir e resumir evidência já correlacionada de um incidente antes de acordar alguém — não decidir a correlação em si |
| Por que uma regra não bastaria? | para triagem simples, uma regra basta: "5xx do alvo + trace de erro → abrir incidente com o link pronto" cobre a maior parte dos casos. IA se justifica quando o volume de sinais correlacionados é grande demais para uma pessoa, ou uma regra, revisar um por um |
| De onde viriam os dados? | do mesmo trace id que este laboratório já grava em log, span e métrica — nenhum dado novo precisa ser capturado, só consultado em lote |
| Qual o risco? | um modelo treinado sobre poucos incidentes históricos (Faturamento tem dois meses de vida) aprender padrão que não generaliza, e sugerir causa errada com confiança — exige avaliação com dado retido e revisão humana sempre disponível |
| Por que não agora? | porque a correlação — pré-requisito de qualquer um dos dois usos — é exatamente o que este laboratório está construindo. Primeiro o dado fica confiável; depois um modelo o consome |
O uso de IA que parece atraente e é armadilha aqui
Pedir a um modelo para "olhar os três painéis separados e adivinhar se são a mesma requisição" é tentador porque parece resolver o problema deste laboratório sem reescrever nenhuma instrumentação. É a armadilha: você trocaria um identificador determinístico — o mesmo trace id em log, span e métrica, garantido por construção — por uma inferência probabilística sobre dados que já deveriam estar correlacionados. Onde existe identidade exata, um modelo só acrescenta latência e a chance de juntar duas requisições que não são a mesma. IA sobre observabilidade vale a partir do dado correlacionado, nunca no lugar da correlação.
Anti-padrões deste laboratório
| Anti-padrão | Por que alguém faz | Por que é problema | Sintoma em produção | Forma correta | Quando é aceitável |
|---|---|---|---|---|---|
| Segunda biblioteca de instrumentação entrando por dependência transitiva | um pacote NuGet trazido por outro motivo já chama `Activity.Start()` com fonte própria, e ninguém percebeu | cria um segundo trace id para a mesma requisição, revertendo a correlação sem nenhuma linha de código intencional | a Prova 2 falha: o trace id do log não bate com o do X-Ray | contagem periódica de `ActivitySource` em uso; instrumentação nova só entra pelo pacote interno padrão | nunca — mesmo em teste, mascara o comportamento real |
| `ActivitySource` criado ad-hoc, sem registrar em `AddSource()` | copiar um trecho de exemplo da documentação sem adaptar ao builder do serviço | o span é criado no processo e o SDK o descarta antes de exportar, sem erro nenhum | evidência de uma etapa específica simplesmente não aparece no trace, sem log de causa | nome do `ActivitySource` centralizado no pacote interno compartilhado, nunca digitado à mão em cada serviço | nunca |
| Confiar em "sem erro na aplicação" como prova de que a telemetria chegou | é o sinal mais fácil de observar, e na maioria dos dias está certo | ignora que o exportador de log é experimental e pode descartar sem propagar exceção | log ausente só é percebido quando alguém precisa dele para auditoria — tarde demais | alarme sobre a self-telemetry do coletor (`otelcol_exporter_send_failed_*`), não sobre ausência de erro | aceitável só para sinais com exportador maduro (X-Ray, EMF), nunca para o experimental |
| Marcar o contêiner do coletor como `essential = true` | parece garantir que a telemetria "sempre" vai existir | inverte a prioridade: um problema de observação passa a poder derrubar o que observa | a API de cobrança para de responder por causa de uma falha no coletor de telemetria | `essential = false`, sempre — falha de observação nunca é motivo para indisponibilidade de negócio | nunca |
| Usar o SDK do X-Ray direto "porque já sei usar", ignorando o pacote interno | é o que o desenvolvedor aprendeu no emprego anterior, e funciona sozinho | quebra a neutralidade de fornecedor e reintroduz o problema original: um pilar com identificador próprio | trace id do X-Ray não aparece no log — a Falha 2 deste módulo, de novo | todo serviço novo chama o pacote interno; nenhuma equipe decide biblioteca sozinha | nunca em serviço novo; código legado que já usa o SDK do X-Ray é migração, não reescrita imediata |
| Identificador de negócio de alta cardinalidade (id do cliente, id do pedido) como dimensão de métrica | parece útil poder filtrar a métrica por cliente específico | cada valor distinto vira uma série de métrica nova — com milhares de clientes, a fatura de métrica explode | a contagem de métricas do namespace cresce na casa de milhares (a Prova 4 reprova) | identificador de negócio vai em atributo de SPAN ou campo de LOG, nunca em dimensão de métrica | nunca — é a mesma regra de cardinalidade do L08, aplicada de novo |
Quando algo não funciona
| Sintoma | Causa provável | Como investigar | Onde olhar | Correção |
|---|---|---|---|---|
| Nenhum log carrega TraceId, mas o trace aparece no X-Ray normalmente | `WithLogging` não está registrado no mesmo `AddOpenTelemetry()` | confira se o `LoggerProvider` do processo é o do OTel ou ainda um `ILogger` nativo à parte | a chamada de `.WithLogging(...)` no builder do pacote interno | registrar `WithLogging` no MESMO builder que já tem `WithTracing`/`WithMetrics` |
| Trace id do log diverge do trace id do X-Ray para a mesma requisição | uma segunda biblioteca (SDK do X-Ray direto, ou dependência transitiva) abriu segmento/Activity próprio | liste os pacotes NuGet do serviço e procure chamada direta a `AWSXRayRecorder` ou `Activity.StartActivity` fora do `ActivitySource` registrado | contagem de `ActivitySource`/recorders distintos em uso no processo | remover a biblioteca concorrente; toda instrumentação nova entra pelo pacote interno |
| Span do cálculo de fatura não aparece na árvore do trace | `ActivitySource` usado no código não bate com o nome registrado em `AddSource()` | compare a string literal nos dois lugares — é o erro mais fácil de digitar errado | o documento do trace no X-Ray, contando ocorrências do nome do span | usar a constante compartilhada do pacote interno em vez de digitar o nome de novo |
| A task de Faturamento reinicia repetidamente logo após o deploy | o contêiner do coletor está `essential = true` e falhou ao subir (imagem, config inválida) | os eventos do serviço ECS mostram qual contêiner causou a parada | `describe-services` → `events`, e o `command`/config do coletor.yaml | validar `coletor.yaml` com o binário localmente antes do deploy; manter `essential = false` |
| Métrica de Faturamento não aparece no CloudWatch, mesmo com a aplicação respondendo | o exportador `awsemf` não está no pipeline `metrics` do coletor, ou o Resource não tem `service.name` preenchido | confira a seção `service.pipelines.metrics.exporters` do coletor.yaml | coletor.yaml, seção `service` | garantir `awsemf` no pipeline de métricas e `ConfigureResource` preenchido no builder |
| Log chega ao CloudWatch com atraso de vários minutos, ou não chega | o exportador `awscloudwatchlogs` é experimental e pode estar descartando ou represando | consulte a self-telemetry do coletor por falhas de exportação | `otelcol_exporter_send_failed_log_records` no grupo de log do EMF | corrigir IAM/config; considerar fallback temporário ao driver `awslogs` do contêiner enquanto o exportador amadurece |
| O número de métricas no namespace cresce sem explicação | uma dimensão de alta cardinalidade (id de cliente, id de pedido) entrou em `metric_declarations` | liste as métricas do namespace e procure dimensão com muitos valores distintos | `list-metrics` no namespace Faturamento | mover o identificador de negócio para atributo de span/log; a dimensão de métrica fica só em rota/status |
| Trace de uma requisição que atravessa Pedidos e Faturamento aparece cortado pela metade | os dois sidecars decidiram a amostragem por cauda de forma independente | confira se o span do outro serviço existe no X-Ray para o mesmo trace id | X-Ray, buscando pelo mesmo trace id nos dois serviços | é o limite estrutural do modelo sidecar — migrar para gateway compartilhado (nível 3 da evolução) resolve na raiz |
A pergunta que resolve metade destes casos
Antes de mexer em qualquer configuração, pergunte: o defeito está DENTRO do processo da aplicação (o SDK não gerou o dado) ou DEPOIS dele (o coletor não exportou o dado que o SDK gerou)? A Prova 1 e a Prova 3 respondem a primeira metade; a self-telemetry do coletor (Prova 5) responde a segunda. Confundir as duas leva a mexer no `Program.cs` quando o problema está no `coletor.yaml`, ou vice-versa.
Limpeza: o que o destroy não leva
Este laboratório acrescenta pouco recurso administrado pelo Terraform sobre o que o L08 já tinha — mas dois grupos de log novos sobrevivem ao destroy, e um deles sustenta a evidência de auditoria da fatura.
#!/usr/bin/env bash
# limpar.sh — o delta deste laboratorio sobre o que o L08 ja limpava.
set -euo pipefail
# 1. Derrube o que o Terraform administra (task definition, IAM, os dois grupos novos).
terraform destroy -auto-approve
# 2. GRUPOS DE LOG NOVOS: se `skip_destroy` estiver ligado em algum deles —
# pratica comum para nao perder log de incidente por engano — o destroy
# NAO os remove. Confira os dois.
aws logs describe-log-groups --log-group-name-prefix /otel/faturamento \
--query "logGroups[].{grupo:logGroupName,bytes:storedBytes}" --output table
aws logs describe-log-groups --log-group-name-prefix /ecs/faturamento/emf \
--query "logGroups[].{grupo:logGroupName,bytes:storedBytes}" --output table
aws logs delete-log-group --log-group-name /otel/faturamento/app 2>/dev/null || true
aws logs delete-log-group --log-group-name /ecs/faturamento/emf 2>/dev/null || true
# 3. Repositorio ECR de Faturamento, se este laboratorio foi seu primeiro
# contato com o servico (nao vinha do L08).
aws ecr describe-repositories --repository-names faturamento \
--query "repositories[0].repositoryName" --output text 2>/dev/null && \
aws ecr delete-repository --repository-name faturamento --force
# 4. O que o L01/L03 ja cobravam por hora continua cobrando: ALB, RDS, NAT
# Gateway. Se voce nao segue para o L52, rode a limpeza deles tambem.
# 5. Prova final: nada com o nome do servico de pe.
aws resourcegroupstaggingapi get-resources \
--tag-filters Key=Servico,Values=faturamento \
--query "ResourceTagMappingList[].ResourceARN" --output table
| Recurso | Sai no destroy? | Cobra parado? | Por que fica |
|---|---|---|---|
| Serviço, task definition e IAM de Faturamento | sim | não | administrado pelo Terraform integralmente |
| Grupo de log `/otel/faturamento/app` | só sem `skip_destroy` | sim, ingestão retida | prática comum é proteger este grupo especificamente — é a evidência de auditoria da cobrança |
| Grupo de log `/ecs/faturamento/emf` | só sem `skip_destroy` | sim, ingestão retida | origem da métrica derivada; retenção curta (3 dias) já limita o acúmulo |
| Spans retidos no X-Ray | não se aplica — retenção fixa do serviço | sim, incluído no serviço | não é recurso Terraform; expira sozinho pela retenção padrão do X-Ray |
| Repositório ECR de Faturamento | só com `force_delete` | sim, GB-mês | o mesmo comportamento do L03: o destroy falha se houver imagem dentro |
| ALB, RDS, NAT Gateway (herdados do L01/L08) | sim, se administrados aqui | sim, por hora | não repetidos neste módulo; rode a limpeza deles se não for seguir para o L52 |
O grupo que vale proteger de exclusão acidental
Entre os dois grupos de log novos, `/otel/faturamento/app` é o que carrega a evidência de cada cobrança processada — o outro é derivado de métrica e recriável a partir do histórico de requisições. Se a sua política de retenção para auditoria for mais longa do que os 14 dias configurados aqui, ajuste antes de rodar `terraform destroy` num ambiente que já processou cobrança real.
Resumo: problema, peça e motivo
| Problema | Peça | Por que ela, e não outra |
|---|---|---|
| Três bibliotecas geram três identificadores que não se cruzam | um SDK OpenTelemetry único, `WithTracing`+`WithMetrics`+`WithLogging` no mesmo builder | correlação vira propriedade estrutural do mesmo Resource, não convenção entre times |
| Nenhuma equipe nova decide sozinha como instrumentar | pacote interno compartilhado (`AddObservabilidadeCadencia`) | o padrão vira a linha de menor esforço — mais rápido chamar um método do que integrar três bibliotecas |
| Lógica de negócio sem cobertura de instrumentação automática | span manual via `ActivitySource` registrado em `AddSource()` | herda o `Activity.Current` como pai por padrão — entra na mesma árvore sem costura |
| Aplicação acoplada a três SDKs de fornecedor | coletor ADOT falando só OTLP com a aplicação | trocar de backend é seção nova no coletor.yaml, zero linha em Program.cs |
| Falha de observação não pode virar indisponibilidade de negócio | `essential = false` no contêiner do coletor | mesmo requisito do L03 aplicado à telemetria em vez de ao deploy |
| Log perdido em silêncio pelo exportador experimental | alarme sobre a self-telemetry do coletor | é a única fonte que expõe a Falha 3 — a aplicação continua parecendo saudável |
| Trace cortado quando a requisição atravessa dois serviços | reconhecido como limite do modelo sidecar; resolvido no nível 3 da evolução (gateway) | tail sampling coordenado exige ver o trace inteiro antes de decidir |
| Falha | O que a protege | O que ela NÃO protege |
|---|---|---|
| Identificador de correlação desencontrado | um SDK, um Resource | segunda biblioteca entrando por dependência transitiva — exige vigilância periódica |
| Span de negócio ausente | `ActivitySource` no pacote interno compartilhado | nome digitado errado fora do pacote — por isso ele não deveria ser digitado de novo |
| API de cobrança indisponível por causa do coletor | `essential = false` | degradação de desempenho da aplicação por CPU/memória disputada com o coletor na mesma task |
| Log perdido silenciosamente | monitoramento da self-telemetry do coletor | perda que ocorreu ANTES de o alarme existir — não há como recuperar log que nunca chegou |
| Custo de métrica fora de controle | EMF com dimensão restrita a rota+status | alguém acrescentar dimensão nova sem revisão — é o Anti-padrão 6 |
| Trace cross-service cortado | nada neste laboratório | é o risco declarado que empurra para o gateway do nível 3 |
- O ALB insere `X-Amzn-Trace-Id` na requisição, como já fazia no L08.
- O propagador do OpenTelemetry, registrado uma única vez no builder, lê o cabeçalho e cria o `Activity` raiz dentro daquele contexto.
- Instrumentação automática de HTTP e banco cria spans filhos, herdando o contexto por `Activity.Current`.
- O span manual do cálculo de fatura nasce como filho do span HTTP, porque `ActivitySource.StartActivity` herda o contexto corrente.
- Toda chamada a `ILogger` dentro da requisição carrega TraceId/SpanId, porque o `LoggerProvider` é o mesmo do trace e da métrica.
- Ao fim da requisição, o SDK empacota os três sinais em OTLP e envia ao coletor local, dentro da própria task.
- O coletor aplica amostragem por cauda e detecção de recurso, e distribui: X-Ray para o span, CloudWatch Logs para o log, EMF para a métrica.
- O cliente recebe o trace id de volta no cabeçalho `X-Correlation-Id`.
- O plantão, de posse do trace id, faz duas consultas — uma no log, outra no trace — e as duas batem.
- Se o coletor falhar em qualquer ponto desse caminho, a API de cobrança continua respondendo — a telemetria é observadora, nunca dependência.
Desafio — sem roteiro
O requisito
Adicione um atributo customizado relevante ao negócio — por exemplo, o valor do pedido — a um span existente, e confirme que ele aparece correlacionado nos três pilares (trace, métrica e log) para a MESMA requisição.
Critério de aceite — executável, não "verifique se funciona"
Buscando pelo trace ID de uma requisição específica, o atributo de valor do pedido aparece no span do trace E no log estruturado daquela requisição E contribui para uma métrica agregada (soma ou histograma) visível no painel.
- Dica 1: `Activity.SetTag()` no .NET adiciona o atributo ao span atual — mas ele só correlaciona com o log se o log também estiver usando o `ActivitySource`/trace context ativo, não um logger desconectado.
- Dica 2: Para a métrica, um `Histogram<double>` do `System.Diagnostics.Metrics` é o instrumento certo para "valor do pedido" — um `Counter` simples não guardaria a distribuição de valores, só a contagem de eventos.
- Dica 3: Se o atributo aparecer no trace mas não no log, o motivo mais comum é o logger não estar configurado para incluir `TraceId`/`SpanId` automaticamente no escopo — isso é configuração do provider de logging, não do span.
Lembrete de limpeza
Todo recurso que este desafio criar entra no mesmo `terraform destroy` do laboratório — nada fica para trás cobrando sozinho.
Perguntas frequentes
❓ Por que log, métrica e trace instrumentados corretamente ainda não se correlacionam?
❓ O que o ADOT Collector faz que a aplicação não poderia fazer sozinha?
❓ Instrumentação automática cobre span manual, ou preciso escrever os dois?
❓ Por que o exportador de log do coletor é tratado como um risco à parte?
❓ Um trace que atravessa dois serviços é correlacionado de ponta a ponta neste desenho?
❓ Métrica derivada de EMF é mais barata que métrica publicada via PutMetricData?
❓ Por que o contêiner do coletor é marcado como não-essencial?
❓ Preciso trocar o SDK do X-Ray por OpenTelemetry em todo serviço legado imediatamente?
Fixando
Uma requisição do Faturamento chama a API de Pedidos internamente. No X-Ray, o trace aparece com o span de Pedidos, mas sem nenhum span de Faturamento — mesmo com os dois serviços instrumentados com OpenTelemetry e coletor sidecar. Qual é a explicação mais provável?
O contêiner do coletor está configurado com `essential = false`. Durante um incidente, ele passa a descartar eventos de log silenciosamente por um problema de IAM, mas a API de Faturamento continua respondendo 200 normalmente e o span/métrica continuam chegando. Qual sinal detecta esse problema?
Conhecimentos, próximo módulo e documentação
| Item | Conteúdo |
|---|---|
| Conhecimentos anteriores necessários | L08 no ar (API Pedidos rastreável por trace id), noção de task definition do ECS, C# e injeção de dependência do .NET 8 |
| Conhecimentos adquiridos | por que três bibliotecas corretas ainda não correlacionam; como um Resource comum estrutura a correlação; instrumentação automática por biblioteca vs. zero-code vs. span manual; o coletor como camada de neutralidade de fornecedor; o limite do modelo sidecar na amostragem por cauda cross-service |
| Limitação que fica | amostragem por cauda decidida isoladamente por sidecar corta trace que atravessa dois serviços; e o exportador de log do coletor continua rotulado como experimental |
| Próximo exemplo recomendado | L52 — SLO, error budget e alarme que acorda alguém. Reutiliza o trace id correlacionado deste laboratório como base do alarme acionável |
| Também habilitado por este módulo | L53 (dashboard que responde pergunta de operação) e L96 (agente que diagnostica incidente usando evidência correlacionada) dependem da correlação construída aqui |
| Data da última validação técnica | 7 de agosto de 2026 |
Documentação oficial consultada: AWS Distro for OpenTelemetry and AWS X-Ray — o papel do ADOT como distribuição suportada pela AWS do projeto OpenTelemetry; OpenTelemetry .NET — Getting started with logs, ASP.NET Core — a estrutura de `WithLogging` sobre o mesmo builder de `WithTracing`/`WithMetrics`; OpenTelemetry .NET Automatic Instrumentation — a distinção entre instrumentação automática por biblioteca e zero-code; opentelemetry-collector-contrib, exporter/awscloudwatchlogsexporter — o rótulo de suporte experimental citado nas seções de Construir e Segurança; e AWS Distro for OpenTelemetry Collector Deployment Types (sidecar vs. service) — os dois modelos usados nas seções de Escala e Evolução, incluindo a recomendação de sempre usar o processador `memory_limiter` no coletor. Os valores de preço não aparecem neste módulo por decisão: use o AWS Pricing Calculator, porque preço varia por região e envelhece mais rápido que o conteúdo.
O que não foi verificado, e você deve conferir na sua conta
O volume de 40 mil cobranças/dia e o overhead de CPU/memória do coletor sidecar são ordens de grandeza da aplicação de exemplo, não medições da AWS. Meça o overhead real do seu coletor com `memory_limiter` configurado e o `check_interval` de 1 segundo recomendado pelo projeto antes de dimensionar CPU/memória da task em produção. O rótulo de "suporte experimental" do `awscloudwatchlogsexporter` também muda entre versões do OpenTelemetry Collector Contrib — confira o README da versão do ADOT Collector que você fixar antes de depender dele para evidência de auditoria.
Terminou de ler?
Marcar como concluído registra o XP, mantém sua sequência e coloca 3 cartas deste módulo na fila de revisão espaçada.
Próximos passos sugeridos
Temas deste módulo
Discussão
Carregando comentários…