Lab 08 — Enxergar o que quebrou
O problema, e a empresa que o tem
A Cadência tem um ticket aberto há três dias: "às 14h22 de terça, tentei abrir um pedido e apareceu erro". A aplicação é a mesma do L01 — .NET 8 em ECS Fargate, RDS em sub-rede privada, ALB na frente e CloudFront na borda — e ela registra log. Registra bastante: 3,4 GB por dia. Nada disso ajudou.
A investigação foi a de sempre. Abrir o grupo de logs, filtrar por error, recortar a janela entre 14h20 e 14h25, e ler. Nesse intervalo há 2.100 eventos de quatro tasks diferentes, e onze deles contêm a palavra. Nenhum tem o identificador do pedido. Dois têm um GUID que pode ser o pedido ou pode ser outra coisa. As linhas de requisições distintas se intercalam no mesmo milissegundo, então ler em sequência produz histórias que não aconteceram.
O diagnóstico que se chegou foi "provavelmente o banco estava lento". Provavelmente. Depois de três dias, a conclusão é um advérbio. E aqui está o ponto que este laboratório existe para atacar: o problema não é falta de log, é que log em texto é um documento para humano ler, e ninguém pode ler 3,4 GB. O que se precisa é de um dado que a máquina consulte — e o que separa uma coisa da outra é um identificador que apareça em todas as camadas, em todas as linhas.
O que este laboratório NÃO é
Não é painel, não é SLO e não é plantão. Definir objetivo de nível de serviço, orçamento de erro e política de acionamento é o L51; correlacionar métrica de negócio com métrica técnica é o L52. Aqui o alvo é mais estreito e vem antes dos dois: fazer com que UMA requisição tenha resposta. Sem isso, um SLO violado avisa que algo está errado e não diz o quê — e a plataforma que você constrói em cima herda o ponto cego.
O que você vai conseguir fazer
Cada objetivo abaixo tem uma prova com número na seção de implantação. Nenhum deles se verifica com a sensação de ter entendido.
- Explicar de onde vem o identificador de correlação antes de o seu código rodar, e nomear os dois campos que o ALB usa.
- Distinguir o identificador de borda do CloudFront do identificador de trace, e dizer por que gravar os dois é necessário.
- Configurar log estruturado em .NET 8 de modo que TraceId e SpanId entrem em toda mensagem sem serem passados de método em método.
- Derivar por que o trace id precisa estar TAMBÉM no log, e o que exatamente se perde quando ele está só no span.
- Escolher entre métrica customizada, métrica derivada de log e filtro de métrica a partir de cardinalidade e de quem controla o formato.
- Calcular quantos traces uma regra de amostragem produz para um volume dado, e dizer quais perguntas deixam de ter resposta.
- Escrever a consulta do Logs Insights que devolve uma requisição inteira em ordem, partindo apenas do identificador que o cliente tem na mão.
- Diagnosticar um trace partido em dois e apontar qual propagador estava faltando.
- Identificar, num arquivo de configuração, a linha que produziria explosão de cardinalidade de métrica.
O que a certificação cobra disto
| Conceito | Certificação | Como aparece aqui | O que dominar |
|---|---|---|---|
| Log estruturado vs. texto | DVA-C02, SOA-C02 | JSON de uma linha por evento, consultado por campo | que a consulta por campo é o que o formato compra, e que o driver awslogs quebra por linha |
| Correlação por identificador | DVA-C02, SOA-C02 | `X-Amzn-Trace-Id` do ALB lido por propagador e gravado no log | que o id chega no cabeçalho e não é a aplicação que o inventa |
| Trace distribuído e propagação de contexto | DVA-C02, SAP-C02 | propagador composto: formato AWS e `traceparent` do W3C | que instalar só um dos dois parte o trace em dois, sem erro nenhum |
| Amostragem do X-Ray | DVA-C02, SOA-C02 | primeira de cada segundo mais 5% das adicionais, no padrão do SDK | que amostragem é escolha de perguntas futuras, não configuração de volume |
| Anotação vs. metadado | DVA-C02 | anotação indexada para filtro, metadado só para leitura | que anotação é pesquisável e limitada a 50 por trace, e metadado não é pesquisável |
| Métrica derivada de log | SOA-C02, DOP-C02 | formato embutido pelo coletor e filtro de métrica no grupo de logs | quando cada um cabe: você controla o formato, ou não controla |
| Cardinalidade de dimensão | SOA-C02, DOP-C02 | a linha de `dimensions` que decide o custo do arquivo inteiro | que cada combinação distinta é uma métrica nova, e id único é a pior escolha |
| Métrica, log e trace: qual pergunta cada um responde | SOA-C02, SAP-C02 | três planos, três custos, três perguntas | que métrica não responde "esta requisição" e log não responde "onde o tempo foi" |
| Retenção de grupo de logs | SOA-C02 | dois grupos com retenções diferentes por propósito | que grupo sem retenção declarada guarda indefinidamente |
| Sidecar de telemetria em ECS | DVA-C02, SAP-C02 | contêiner do coletor com `essential = false` | por que marcar o coletor como essencial troca incidente de observação por incidente de disponibilidade |
Onde isto costuma ser cobrado errado
A pergunta clássica apresenta uma aplicação em contêiner com log em texto e pede a forma mais econômica de alarmar sobre uma condição que aparece na mensagem. A armadilha é escolher "publicar métrica customizada da aplicação", que funciona e acrescenta uma chamada de API por medição. A resposta esperada é derivar a métrica do log que já está sendo ingerido — por filtro de métrica quando você não controla o formato, ou pelo formato embutido quando controla. A segunda armadilha é a distinção entre 5xx do balanceador e 5xx do alvo, que aparece junto e é do L03.
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 no desenho.
| Requisito | Valor declarado | O que ele decide no desenho |
|---|---|---|
| Do ticket ao salto que falhou | abaixo de 2 min, sem adivinhar horário | obriga identificador devolvido ao cliente e presente como CAMPO em toda linha — é o que descarta log de texto |
| O trace da requisição reclamada tem de existir | 100% dos erros e das lentas | obriga amostragem por CAUDA no coletor: a decisão por cabeça não sabe ainda se houve erro |
| Custo de observabilidade | até 8% da fatura da aplicação | proíbe 100% de span indexado e proíbe dimensão de métrica de alta cardinalidade; empurra a métrica para ser derivada do log |
| Detalhe da requisição individual | sempre disponível | log estruturado em 100%, não amostrado — é o plano barato o suficiente para não amostrar |
| Latência acrescentada pela telemetria | desprezível na resposta ao cliente | coletor em sidecar, e não exportação direta do processo da API: lote e repetição saem do caminho da requisição |
| Falha da telemetria não derruba a API | requisito absoluto | `essential = false` no contêiner do coletor, e exportador que descarta em vez de bloquear |
| Repartir o tempo entre os saltos | obrigatório | instrumentação do cliente de banco e do cliente HTTP: sem subsegmento não há como separar lentidão de banco de lentidão de código |
| Log não pode conter dado pessoal | exigência de conformidade | campo de negócio é identificador, nunca corpo de requisição; e a rota de saúde é filtrada da instrumentação |
| Retenção diferenciada | 14 dias para aplicação, 3 para o log que virou métrica | dois grupos de logs distintos, cada um com a sua retenção — não um grupo grande |
| Ligar latência a deploy | desejável | o SHA da imagem entra como atributo de recurso na telemetria, reaproveitando o L03 |
Arquitetura mínima: muitos logs, nenhuma resposta
Este é o desenho que a Cadência tem, e ele é legítimo como ponto de partida: registra de verdade, custa pouco em atenção e resolve o problema de depurar na própria máquina. O laboratório começa medindo o defeito, porque um número o torna discutível e "o log é ruim" não.
- → GET /pedidos/8f3 — e o 500 que ele viu
- → repassa, acrescentando o identificador de borda
- → X-Amzn-Trace-Id: Root=1-67891233-abcdef…
- → SELECT do pedido, sem duração medida
- → quatro linhas de texto por requisição
- → filter-log-events com "error" e um horário aproximado
- Fora da AWS
- Rede e entrega
- Compute
- Banco de dados
- Gestão e governança
Este desenho está em produção e registra tudo: nada aqui está desligado. O defeito não é ausência de log — é que nenhuma das quatro linhas que a requisição gerou sabe da existência das outras três. Percorra os passos e repare que o identificador de correlação JÁ CHEGOU no cabeçalho, e foi descartado pela própria aplicação.
- O identificador já existe, e a aplicação o joga fora. O ALB acrescenta ou atualiza `X-Amzn-Trace-Id` em toda requisição que recebe — não é opcional e não custa nada. O valor tem a forma `Root=1-<época em 8 hex>-<24 hex>`. Nada na aplicação lê esse cabeçalho, então o dado que ligaria as camadas chega e é descartado no primeiro salto.
- A borda tem OUTRO identificador, e ele não é o mesmo. O CloudFront identifica cada requisição de borda com o seu próprio valor, devolvido no cabeçalho da resposta e registrado no log de acesso dele. Esse valor não é o do ALB nem o da aplicação. Quem só guarda um dos três não consegue partir do que o cliente tem na mão e chegar ao código.
- Quatro linhas soltas em vez de um evento. "recebi pedido", "consultando banco", "erro ao serializar", "devolvendo 500" são quatro eventos independentes para o CloudWatch. Nenhum campo os une. Reconstruir a requisição é trabalho humano de leitura, e cresce linearmente com o volume — que é o oposto do que se quer de um sistema de consulta.
- O grep só sabe casar substring e recortar por hora. Com log de texto, as duas únicas dimensões de busca são "contém isso" e "entre estes instantes". Não há "onde a rota é X e o status é 500 e o cliente é Y", porque rota, status e cliente não são campos — são pedaços de frase. É a diferença entre um arquivo e um índice.
- Sob concorrência, o horário deixa de correlacionar. Com uma requisição por segundo, ordenar por horário quase funciona. Com trinta simultâneas em quatro tasks, as linhas de requisições diferentes se intercalam no mesmo milissegundo e a leitura por proximidade temporal passa a produzir histórias falsas — que é pior do que não ter resposta.
- A chamada ao banco não deixou rastro de tempo. A causa real era uma consulta de 4 s que estourou o tempo do cliente. Como ninguém mediu a duração da chamada, o log registra que houve erro e não registra ONDE o tempo foi gasto. A pergunta "a lentidão é do banco ou da aplicação?" não tem dado que a responda.
- Por que alguém instrumenta assim. Porque `Console.WriteLine($"erro ao processar {id}")` é uma linha, aparece na hora e resolve o problema de ontem — depurar na própria máquina. O custo só aparece quando o log deixa de ser lido por quem o escreveu e passa a ser consultado por quem está de plantão às três da manhã.
Antes de mudar qualquer coisa, meça duas grandezas: quanto tempo se leva para achar uma requisição específica, e qual fração dos eventos permite filtro por campo. As duas viram a linha de base.
# Rode ANTES de instrumentar nada. Os numeros daqui sao a linha de base.
GRUPO=/ecs/ffv-lab-api
# 1. Quantos eventos por dia, e quantos GB. Da a escala do problema.
aws logs describe-log-groups --log-group-name-prefix "$GRUPO" \
--query "logGroups[].{grupo:logGroupName,bytes:storedBytes,ret:retentionInDays}" \
--output table
# Na Cadencia: 3,4 GB/dia e retentionInDays NULO — guardando para sempre.
# 2. Qual fracao dos eventos e JSON valido, ou seja, consultavel por campo.
# Com log de texto a resposta e zero, e o zero e o argumento deste modulo.
aws logs start-query --log-group-name "$GRUPO" \
--start-time $(( $(date +%s) - 3600 )) --end-time $(date +%s) \
--query-string 'fields @message
| filter ispresent(TraceId)
| stats count() as com_campo' --query queryId --output text
# Na Cadencia: 0 de 87.412 eventos na ultima hora.
# 3. Cronometre a busca de UMA requisicao conhecida. Faca voce mesmo, com
# relogio, partindo so de "erro por volta das 14h22".
# Na Cadencia: 3 dias, e a conclusao foi o adverbio "provavelmente".Os dois defeitos deste desenho são diferentes, e confundi-los custa caro
O primeiro é de FORMATO: texto interpolado não tem campo, então a única busca possível é substring. O segundo é de CORRELAÇÃO: nenhum evento carrega um identificador comum, então nem com campos você ligaria as linhas de uma mesma requisição. Trocar texto por JSON resolve só o primeiro — e é o meio-caminho mais comum, porque parece a mudança grande. Você fica com eventos bonitos e inconectáveis, e conclui que "structured logging não ajudou".
Arquitetura para produção
Cada peça abaixo rastreia a uma linha da tabela de requisitos. A diferença estrutural em relação ao desenho anterior não é uma caixa a mais: a unidade de implantação passa a ter dois contêineres, e a telemetria sai por dois caminhos com preços diferentes.
- → GET /pedidos/8f3, com traceparent se o cliente tiver
- → repassa com o identificador de borda no cabeçalho
- → X-Amzn-Trace-Id: Root=… (ou Self=…;Root=…)
- → linha de acesso com o campo de trace e o tempo de alvo
- → consulta instrumentada: subsegmento com duração
- → JSON de uma linha por evento, com TraceId em todos
- → OTLP em gRPC na 4317, dentro da própria task
- → span exportado: 100% ingerido, fração indexada
- → EMF no log: a métrica nasce do evento que já ia ser escrito
- → p99 e taxa de falha por rota, não por requisição
- → aviso com a consulta pronta, não com "erro em produção"
- → filter @TraceId = … — um campo, não um grep
- → abre o trace e vê em qual salto os 4 s foram
- Fora da AWS
- Rede e entrega
- Armazenamento
- Compute
- Conceito de arquitetura
- Banco de dados
- Gestão e governança
A diferença estrutural não é uma caixa a mais: a task passa a ter DOIS contêineres, e a telemetria sai por dois caminhos com custos diferentes — log estruturado indexável e span amostrado. O que os une é o mesmo identificador, agora presente em toda linha e em todo span. Percorra os passos: cada peça rastreia a um requisito da tabela anterior.
- O identificador entra antes do seu código, e agora é lido. O ALB continua fazendo o que já fazia: `Root=` quando o cabeçalho não vinha, `Self=` inserido na frente quando já vinha um `Root`. A mudança está na aplicação, que extrai esse valor com um propagador e o adota como contexto do trace em vez de gerar um id novo — que é o que quebra a cadeia em dois.
- Três identificadores, e você grava os três no mesmo evento. O de borda vem do CloudFront, o de balanceador vem do `X-Amzn-Trace-Id` e o de negócio é o seu (id do pedido, do cliente). Gravar os três como campos do log estruturado é o que permite entrar pela ponta que o reclamante tem na mão. O access log do ALB fecha a lacuna de tempo que antecede o código.
- O mesmo id vai para o span E para a linha de log. Esta é a peça que costuma faltar. Trace sem log dá o mapa e não dá o detalhe; log sem trace dá o detalhe e não dá o mapa. Com o id nos dois, o span diz que o salto do banco levou 4 s e o log diz qual consulta era e com qual parâmetro. Um sem o outro é meio diagnóstico.
- A amostragem decide o que você poderá perguntar depois. Amostrar não é economizar bytes: é escolher, hoje, quais perguntas terão resposta amanhã. Com 5% de amostragem, o trace da requisição que o cliente reclamou tem 95% de chance de não existir. Por isso o laboratório separa os dois planos: log estruturado em 100% e span amostrado, com regra que força 100% no que é erro.
- A métrica nasce do log, não de uma segunda chamada de API. O evento que já ia para o CloudWatch Logs sai no formato de métrica embutida, e o CloudWatch extrai o valor numérico dele. Você paga a ingestão do log — que ia pagar de todo modo — em vez de somar uma chamada por medição. A conta muda de sinal quando a dimensão tem cardinalidade alta, e a seção de custo mostra onde.
- O banco deixa de ser caixa preta. A instrumentação do cliente de banco cria um subsegmento por consulta, com início e fim. É isso que responde "a lentidão é do banco ou da aplicação?" sem adivinhação — a soma dos subsegmentos contra a duração do segmento pai reparte o tempo entre os saltos.
- A pergunta do plantão é sobre UMA requisição, e ela tem resposta. O alarme dispara sobre a métrica agregada, porque alarme sobre requisição individual é ruído. Mas o corpo do aviso traz o filtro pronto: duas consultas depois — uma no log por `TraceId`, outra no trace — o plantão sabe qual salto falhou e com qual parâmetro. É o entregável deste laboratório.
Repare no que NÃO mudou: continua sendo uma task em Fargate, com o mesmo ALB e o mesmo banco. Observabilidade não é uma arquitetura nova — é um plano de dados acrescentado ao lado do plano de aplicação. O erro de leitura mais comum aqui é achar que o coletor está no caminho da requisição; ele não está, e é justamente por isso que uma falha dele não devolve erro ao cliente.
A decisão que define este desenho, em uma frase
Log estruturado em 100% e span amostrado. As duas perguntas têm preços diferentes: "o que aconteceu nesta requisição" precisa de todas as requisições, e o log é barato o suficiente para não amostrar; "como o sistema se comporta" precisa de uma amostra representativa, e o span é caro o suficiente para amostrar. Quem amostra os dois perde a requisição reclamada; quem não amostra nenhum estoura o orçamento de 8%.
A vida de um identificador, ponta a ponta
O identificador não nasce no seu código, e essa é a informação mais útil desta seção. O ALB acrescenta ou atualiza X-Amzn-Trace-Id em toda requisição que recebe, com o formato Campo=versão-tempo-id: versão sempre 1, tempo em 8 dígitos hexadecimais representando a época em segundos, e id em 24 dígitos hexadecimais. Quando o cabeçalho não vinha, ele cria um campo Root. Quando já vinha com um Root, ele insere um campo Self na frente e preserva o resto — inclusive campos arbitrários que a sua aplicação tenha acrescentado.
// Os tres identificadores que atravessam UMA requisicao, e por que sao tres.
// ── 1. O que o cliente ve na resposta ────────────────────────────────────────
HTTP/1.1 500 Internal Server Error
X-Amz-Cf-Id: SLxU6dEQ4o8-Xz1kA9wV3rN7pQ== // identificador da requisicao de borda
X-Correlation-Id: 4bf92f3577b34da6a3ce929d0e0e4736 // devolvido pelo nosso middleware
// ── 2. O que o ALB coloca a caminho do alvo ─────────────────────────────────
// Cabecalho AUSENTE na entrada: o balanceador cria um campo Root.
X-Amzn-Trace-Id: Root=1-67891233-abcdef012345678912345678
// ^versao 1 ^epoca em 8 digitos hex ^id em 24 digitos hex
// Cabecalho PRESENTE com Root: o balanceador INSERE Self na frente e preserva
// o resto, inclusive campos que a sua aplicacao tenha acrescentado.
X-Amzn-Trace-Id: Self=1-67891233-12456789abcdef012345678;Root=1-67891233-abcdef012345678912345678;CalledFrom=app
// Ja com Sampled e Parent, quando vem de um servico instrumentado:
X-Amzn-Trace-Id: Root=1-5759e988-bd862e3fe1be46a994272793;Parent=53995c3f42cd8ad8;Sampled=1
// ── 3. O que um cliente moderno ou outro servico instrumentado manda ─────────
// Formato W3C. Repare que ele NAO tem tempo embutido: e 32 hex aleatorios.
traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
// ^versao ^trace-id (32 hex) ^span-id (16 hex) ^flags: 01 = amostrado
// A diferenca de formato tem consequencia pratica: o id do X-Ray carrega a epoca
// nos seus 8 primeiros digitos hexadecimais e o do W3C nao carrega nada. E por
// isso que existe um gerador de id "no formato X-Ray" nas extensoes AWS do
// OpenTelemetry, e por isso que aceitar id em formato W3C foi uma capacidade que
// precisou ser acrescentada ao lado do X-Ray, nao algo que ja vinha de graca.
Três limites do rastreamento do ALB que a documentação declara e quase ninguém lê
O balanceador atualiza o cabeçalho na ENTRADA da requisição, nunca na resposta — então não espere correlacionar pelo caminho de volta. Se os cabeçalhos HTTP passarem de 7 KB, ele REESCREVE o `X-Amzn-Trace-Id` com um campo `Root` novo, o que significa perda silenciosa de correlação em requisição com muito cabeçalho ou cookie grande. E com WebSocket só há rastreamento até o upgrade ser bem-sucedido; depois disso o túnel não produz mais requisições para rastrear.
O mesmo evento que vai para o log pode carregar a métrica embutida, e é assim que a série temporal nasce sem uma segunda chamada de API. O documento abaixo é um evento de log válido E a fonte de duas métricas: o objeto _aws é o que instrui o CloudWatch a extrair os valores numéricos declarados.
{
"_aws": {
"Timestamp": 1786000951418,
"CloudWatchMetrics": [
{
"Namespace": "ApiPedidos",
"Dimensions": [ [ "http.route", "http.status_code" ] ],
"Metrics": [
{ "Name": "http.server.request.duration", "Unit": "Milliseconds" },
{ "Name": "db.query.duration", "Unit": "Milliseconds" }
]
}
]
},
"http.route": "/api/pedidos/{id}",
"http.status_code": "500",
"http.server.request.duration": 4213,
"db.query.duration": 4098,
"TraceId": "4bf92f3577b34da6a3ce929d0e0e4736",
"PedidoId": "8f3a1c22-0d41-4d9e-9c17-0b2f4a6e5d10",
"AwsCloudFrontRequestId": "SLxU6dEQ4o8-Xz1kA9wV3rN7pQ=="
}
Leia a linha de dimensões como uma linha de custo
Cada DimensionSet usado cria uma métrica nova no CloudWatch. Com `http.route` e `http.status_code`, o número de séries é rota vezes status — dezenas. Acrescente `PedidoId` à lista de dimensões e passa a ser uma série por pedido: a documentação do formato embutido avisa esse caso com nome e tudo, usando `requestId` como exemplo. Repare que `TraceId` e `PedidoId` estão no evento como campos comuns, e é onde eles devem estar: consultáveis no log, filtráveis, e fora da identidade da métrica.
As decisões, e o que se perde em cada uma
📋 Uma API .NET 8 em ECS Fargate atrás de ALB e CloudFront, com 500 mil requisições por dia, dois engenheiros em plantão alternado e um erro 500 relatado pelo cliente que ninguém consegue localizar no log de texto.
A pergunta que precisa de resposta é sobre UMA requisição, e só um identificador compartilhado a responde. A escolha de instrumentar com OpenTelemetry em vez do SDK do X-Ray não é preferência de moda: é o que permite trocar o destino da telemetria mudando a configuração do coletor, sem tocar no código da aplicação — e a decisão de para onde mandar telemetria muda mais vezes do que a de como produzi-la. O sidecar existe para que amostragem, lote e repetição de envio saiam do processo que atende o cliente. E o log fica em 100% enquanto o span é amostrado porque as duas perguntas têm preços diferentes: "o que aconteceu nesta requisição" precisa de todas, "como o sistema se comporta" precisa de uma amostra representativa.
Alt: SDK do X-Ray direto na aplicação — Menos peças e funciona. O custo é acoplamento: o formato de span, o destino e a semântica de atributo passam a estar no seu código, e trocar de backend de observabilidade vira refatoração. Legítimo quando o compromisso com a AWS é explícito e a equipe é pequena.
Alt: Coletor central compartilhado, em vez de sidecar por task — Economiza CPU e memória duplicadas e centraliza a configuração. Em troca, passa a ser ponto único de falha e de saturação para toda a frota, e uma perda dele apaga a telemetria de todo mundo ao mesmo tempo — exatamente quando você mais precisa dela.
Alt: Exportar direto da aplicação, sem coletor — Uma peça a menos, e é o desenho mínimo deste módulo levado a sério. O problema é que a amostragem, o lote e a repetição de envio passam a rodar no mesmo processo que atende o cliente: uma lentidão no backend de telemetria vira latência na resposta.
Alt: Log em texto com padrão de linha e um filtro de métrica — É o caminho mais curto de sair do zero, e continua sendo a resposta certa para uma aplicação com um único componente. Ele não sobrevive ao segundo serviço: sem contexto propagado, não existe trace distribuído, e a correlação volta a ser por horário.
| Decisão | Escolha | Alternativas | Motivo | O que se perde |
|---|---|---|---|---|
| Como instrumentar | OpenTelemetry na aplicação | SDK do X-Ray; agente sem código; nada | o destino da telemetria muda mais vezes que a forma de produzi-la, e o coletor absorve essa mudança | mais peças e uma curva de aprendizado de conceito (span, propagador, exportador) |
| Onde amostrar | no coletor, por cauda | no SDK, por cabeça; amostragem remota do X-Ray | decidir depois de o trace fechar é o que permite guardar 100% dos erros | o coletor precisa manter o trace em memória até decidir, e traces muito longos podem expirar |
| Onde o coletor roda | sidecar por task | serviço central compartilhado; sem coletor | falha e saturação ficam contidas na task, e a configuração viaja com a aplicação | CPU e memória duplicadas em cada task, e uma alteração de configuração exige novo deploy |
| Como o log sai | JSON de uma linha no stdout, driver awslogs | agente de log próprio; escrita direta na API do CloudWatch | o contêiner não precisa saber onde o log vai parar, e a plataforma cuida do transporte | o formato de uma linha é obrigatório, e JSON identado quebraria cada evento em vários |
| De onde vem a métrica | derivada do log em formato embutido | métrica customizada por chamada de API; filtro de métrica | aproveita a ingestão que já ia acontecer, sem somar chamada por medição | a métrica só existe se o log for ingerido: retenção zero no grupo mataria a fonte |
| Quanto do span indexar | fração pequena, com 100% ingerido | indexar 100%; não usar busca por transação | ver um trace específico e procurar traces com um padrão são perguntas de preços diferentes | a busca por atributo só alcança a fração indexada — o trace existe, mas não é encontrável por filtro |
| Quais atributos indexar como anotação | loja e ambiente | indexar o id do pedido; não indexar nada | anotação serve para AGRUPAR traces, e o limite é de 50 por trace | filtrar traces por pedido específico deixa de ser possível — e para isso existe o log |
| O que devolver ao cliente | o trace id, em cabeçalho de resposta | nada; um id interno próprio | transforma "deu erro no seu site" em uma chave de busca exata | nada de relevante: o trace id não contém dado do usuário, e expô-lo não abre superfície |
A dívida que este desenho cria, e que ele não paga
Com o trace id na resposta e nos dois planos de dados, você passa a ter capacidade de reconstruir o comportamento de um usuário identificado. Isso é poder de diagnóstico e é risco de privacidade ao mesmo tempo. A regra que fica: identificador de recurso (pedido, loja) pode entrar como campo; conteúdo de requisição, cabeçalho de autorização e dado pessoal, não. Quem trata isso a sério faz mascaramento na saída do coletor, e isso é assunto do L52 junto com governança de dado.
Construir: log estruturado com escopo de requisição
A parte que costuma faltar não é o JSON — é o escopo. Sem ele, você escreve eventos estruturados que não sabem a qual requisição pertencem, e o defeito de correlação sobrevive ao upgrade de formato. No .NET a ligação se faz com ActivityTrackingOptions, que instrui a fábrica de log a anexar o TraceId e o SpanId do Activity corrente a toda mensagem escrita dentro daquele fluxo de execução.
O mecanismo importa mais que a linha: o Activity.Current é ambiental — vive no contexto de execução assíncrono, e não num parâmetro. É por isso que a rota não precisa receber nem repassar identificador nenhum, e é por isso que a correlação sobrevive a await e a chamadas em profundidade.
// Program.cs — a configuração real do log estruturado com escopo de requisição.
// Não é "use Serilog": é a fábrica de log do proprio .NET anexando o contexto do
// Activity corrente a TODA mensagem escrita dentro da requisicao.
using System.Diagnostics;
using OpenTelemetry.Trace;
using OpenTelemetry.Metrics;
using OpenTelemetry.Resources;
var builder = WebApplication.CreateBuilder(args);
// ── 1. O elo que quase todo mundo esquece ────────────────────────────────────
// ActivityTrackingOptions faz a fabrica de log ler o Activity.Current e anexar
// TraceId e SpanId como ESCOPO de cada mensagem. Sem esta linha voce tem trace
// bonito no X-Ray e log sem trace id nenhum — o pior dos dois mundos, porque
// cada metade responde a uma pergunta e nenhuma responde a sua.
builder.Logging.Configure(o =>
o.ActivityTrackingOptions = ActivityTrackingOptions.TraceId
| ActivityTrackingOptions.SpanId
| ActivityTrackingOptions.ParentId);
// ── 2. Uma linha de JSON por evento, e por que UMA linha ─────────────────────
// O driver awslogs quebra a saida por salto de linha: um JSON identado viraria
// N eventos invalidos no CloudWatch, cada um com um fragmento do objeto. Indented
// = false nao e estetica, e requisito do transporte.
builder.Logging.ClearProviders();
builder.Logging.AddJsonConsole(o =>
{
o.IncludeScopes = true; // sem isto, TraceId fica no escopo e nao no evento
o.UseUtcTimestamp = true; // fuso local em log distribuido e fonte de erro de leitura
o.JsonWriterOptions = new System.Text.Json.JsonWriterOptions { Indented = false };
});
// ── 3. Propagador COMPOSTO: aceita os dois formatos que chegam ───────────────
// O ALB fala X-Amzn-Trace-Id; um cliente moderno ou outro servico instrumentado
// fala traceparent (W3C). Instalar so um dos dois propagadores e a causa numero
// um de "trace quebrado em dois": o contexto que chegou nao e reconhecido, um id
// novo e criado, e o mapa perde a aresta.
//
// INCERTEZA DECLARADA: o pacote NuGet que traz o propagador da AWS e o gerador de
// id no formato do X-Ray mudou de nome entre versoes (ja se chamou
// OpenTelemetry.Contrib.Extensions.AWSXRay). Confira o nome atual no NuGet antes
// de copiar o `using` — o MECANISMO abaixo nao muda, o pacote sim.
builder.Services.AddOpenTelemetry()
.ConfigureResource(r => r
.AddService(serviceName: "api-pedidos",
serviceVersion: builder.Configuration["SHA_DA_IMAGEM"] ?? "dev")
// O SHA como versao do recurso e o que permite perguntar "esta latencia
// apareceu com qual deploy?" — liga observabilidade a entrega (L03).
.AddAttributes(new Dictionary<string, object>
{
["deployment.environment"] = builder.Environment.EnvironmentName,
}))
.WithTracing(t => t
.AddAspNetCoreInstrumentation(o =>
{
// Health check gera trace inutil em volume alto: e a maior fonte de
// span sem valor numa API atras de ALB verificando a cada 5 s.
o.Filter = ctx => !ctx.Request.Path.StartsWithSegments("/health");
// Enriquecer o span com o que o cliente tem na mao. Sem isto, nao ha
// como partir do ticket e chegar no trace.
o.EnrichWithHttpRequest = (activity, req) =>
{
var borda = req.Headers["X-Amz-Cf-Id"].ToString();
if (!string.IsNullOrEmpty(borda))
activity.SetTag("aws.cloudfront.request_id", borda);
};
})
.AddHttpClientInstrumentation()
.AddNpgsql() // subsegmento por consulta, com duracao
.AddOtlpExporter()) // destino vem de OTEL_EXPORTER_OTLP_ENDPOINT
.WithMetrics(m => m
.AddAspNetCoreInstrumentation()
.AddRuntimeInstrumentation()
.AddOtlpExporter());
var app = builder.Build();
// ── 4. Devolver o identificador ao cliente ───────────────────────────────────
// E a peca que transforma "deu erro no seu site" em "deu erro, o codigo e este".
// Sem ela, o ticket chega com um horario aproximado e a investigacao comeca por
// adivinhacao. Devolver trace id NAO e vazamento: ele nao contem dado do usuario.
app.Use(async (ctx, next) =>
{
var trace = Activity.Current?.TraceId.ToString();
if (trace is not null)
ctx.Response.Headers["X-Correlation-Id"] = trace;
await next();
});
app.MapGet("/api/pedidos/{id:guid}", async (
Guid id, AppDb db, ILogger<Program> log) =>
{
// Repare no que NAO esta aqui: nenhum trace id passado a mao. O escopo
// ambiental do Activity ja garante que este evento sai com TraceId e SpanId.
// Repare tambem no formato: {PedidoId} e um NOME DE CAMPO, nao interpolacao.
// `$"pedido {id}"` produziria uma cadeia de caracteres opaca, e a consulta
// por pedido voltaria a ser substring.
log.LogInformation("Consultando pedido {PedidoId}", id);
var pedido = await db.Pedidos.FindAsync(id);
if (pedido is null)
{
// 404 e resultado esperado, nao falha: registrar como erro polui a taxa
// de erro e faz o alarme perder credibilidade.
log.LogInformation("Pedido {PedidoId} inexistente", id);
return Results.NotFound();
}
// Atributo de negocio no span: e o que permite filtrar traces por cliente
// sem criar uma metrica por cliente. Atributo de span e barato; dimensao de
// metrica com a mesma cardinalidade seria ruinosa.
Activity.Current?.SetTag("pedido.id", id.ToString());
Activity.Current?.SetTag("pedido.loja", pedido.LojaId.ToString());
return Results.Ok(pedido);
});
app.MapHealthChecks("/health/ready");
app.Run();
A pegadinha do JSON identado
O driver de log `awslogs` delimita eventos por salto de linha. Um JSON formatado com indentação vira N eventos no CloudWatch, cada um com um fragmento do objeto, e nenhum deles é JSON válido — então a consulta por campo falha em silêncio, sem erro, sem alarme e sem nada no console além de linhas soltas. `Indented = false` não é preferência de estilo: é requisito do transporte.
Sobre o nome do pacote, e o que não foi verificado
O mecanismo descrito acima — propagador composto, gerador de id em formato X-Ray e instrumentação do ASP.NET Core — é estável. O NOME dos pacotes NuGet das extensões AWS do OpenTelemetry mudou de lugar entre versões; um deles já se chamou `OpenTelemetry.Contrib.Extensions.AWSXRay`. Confira o nome atual no NuGet antes de copiar os `using`, e prefira configurar o propagador pela variável de ambiente `OTEL_PROPAGATORS`, que é padronizada e não depende do nome do pacote.
Construir: o mesmo evento, antes e depois
Vale ver os dois lado a lado, porque a diferença não é de qualidade de redação. É que o segundo tem nomes, e nome é o que uma consulta pode referenciar.
// O MESMO evento, nos dois formatos. A diferenca nao e estetica.
// ── Antes: uma cadeia de caracteres. Legivel, inconsultavel. ─────────────────
// 2026-08-07 14:22:31 ERRO ao processar pedido 8f3a-... para loja 42: timeout
//
// Para achar isto voce precisa saber a substring exata. Para contar quantos
// aconteceram por loja, precisa de expressao regular sobre texto livre. Para
// ligar a outra linha da mesma requisicao, precisa do horario e de sorte.
// ── Depois: um objeto com campos. Um evento, N perguntas. ───────────────────
{
"Timestamp": "2026-08-07T14:22:31.4180000Z",
"LogLevel": "Error",
"Category": "Api.Pedidos",
"Message": "Falha ao consultar pedido",
"State": {
"PedidoId": "8f3a1c22-0d41-4d9e-9c17-0b2f4a6e5d10",
"LojaId": 42
},
// Injetados pelo ActivityTrackingOptions: nao aparecem no codigo da rota.
"Scopes": [
{ "TraceId": "4bf92f3577b34da6a3ce929d0e0e4736",
"SpanId": "00f067aa0ba902b7",
"ParentId": "0000000000000000" }
],
// O que o cliente tem na mao, gravado como campo — e a porta de entrada da
// investigacao quando o ticket nao traz horario preciso.
"AwsCloudFrontRequestId": "SLxU6dEQ4o8-Xz1kA9wV",
"http.route": "/api/pedidos/{id}",
"http.status_code": 500
}
// A consulta que o segundo formato permite e o primeiro nao, no Logs Insights:
//
// fields @timestamp, Message, State.PedidoId
// | filter Scopes.0.TraceId = "4bf92f3577b34da6a3ce929d0e0e4736"
// | sort @timestamp asc
//
// Devolve a requisicao INTEIRA, em ordem, sem depender de horario. E o mesmo
// dado de antes; o que mudou e que agora ele tem nome.
| Pergunta que se quer fazer | Com log de texto | Com log estruturado e correlacionado |
|---|---|---|
| Todos os eventos desta requisição, em ordem | impossível: nada os liga além do horário aproximado | um filtro por `TraceId`, e a resposta vem ordenada |
| Quantos erros por loja na última hora | expressão regular sobre texto livre, quebrando a cada mudança de redação | `stats count() by LojaId`, sobre um campo |
| Este pedido específico já falhou antes | busca por substring do GUID, se ele estiver escrito na linha | filtro por `State.PedidoId` em qualquer janela |
| Onde os 4 s foram gastos | não há dado: ninguém mediu duração de nada | o trace reparte entre os saltos, e o log diz qual consulta era |
| A latência piorou com qual deploy | nenhuma relação entre log e versão | o SHA está como atributo de recurso em toda telemetria |
| Quantos eventos não têm correlação | a pergunta não faz sentido: nenhum tem | `filter ispresent(...) = 0 | stats count()` — e a meta é zero |
A regra prática que resume esta seção
Sempre que você escrever interpolação de cadeia de caracteres dentro de uma chamada de log, você acabou de transformar um dado em texto. `log.LogInformation("pedido " + id)` produz uma frase; `log.LogInformation("Consultando pedido {PedidoId}", id)` produz um campo chamado `PedidoId` mais um modelo de mensagem reutilizável. O esforço é o mesmo e a diferença é permanente, porque log velho não se reescreve.
Construir: o coletor, a amostragem e a métrica derivada de log
A amostragem é a decisão mais consequente deste arquivo, e ela não é uma configuração de volume: é a escolha de quais perguntas terão resposta depois. Com 5% uniforme, o trace da requisição que o cliente reclamou tem 95% de chance de não existir — e nenhuma ferramenta recupera o que não foi gravado.
A saída é decidir DEPOIS. A amostragem por cabeça escolhe na primeira requisição do trace, quando ainda não se sabe se houve erro; a amostragem por cauda espera o trace fechar e usa o resultado na decisão. O preço é que o coletor mantém o trace em memória durante a espera, e traces que demoram mais que essa janela são decididos incompletos.
# coletor.yaml — configuracao do ADOT collector rodando como sidecar da task.
# Ele existe para que amostragem, lote e repeticao de envio NAO rodem no processo
# que atende o cliente.
receivers:
otlp:
protocols:
grpc:
# 127.0.0.1 e nao 0.0.0.0: em modo awsvpc os dois conteineres da task
# compartilham a interface de rede, entao localhost basta — e expor em
# 0.0.0.0 abriria o receptor para qualquer coisa na sub-rede.
endpoint: 127.0.0.1:4317
http:
endpoint: 127.0.0.1:4318
processors:
# Lote existe para trocar N chamadas pequenas por uma maior. Timeout curto
# mantem a telemetria util em incidente; lote grande economiza chamada.
batch/traces:
timeout: 5s
send_batch_size: 128
# Amostragem por CAUDA: decide DEPOIS de o trace terminar, entao pode usar o
# resultado na decisao. E a diferenca central em relacao a amostragem por
# cabeca, que decide na primeira requisicao e nao sabe ainda se houve erro.
tailsampling:
decision_wait: 10s
policies:
# 1. Tudo o que falhou: 100%. E a regra que garante que o trace da
# requisicao reclamada pelo cliente EXISTE.
- name: erros-sempre
type: status_code
status_code: { status_codes: [ERROR] }
# 2. Tudo o que passou de 2 s: 100%. Lentidao e o segundo motivo de ticket.
- name: lentas-sempre
type: latency
latency: { threshold_ms: 2000 }
# 3. O resto: 5%. E a linha de base do comportamento normal — suficiente
# para p99 e para o mapa de servicos, e e onde o custo mora.
- name: resto-amostrado
type: probabilistic
probabilistic: { sampling_percentage: 5 }
resourcedetection:
detectors: [env, ecs] # preenche cluster, servico e task sem codigo
exporters:
awsxray:
indexed_annotations: [pedido.loja, deployment.environment]
# Anotacao e indexada e serve de filtro; atributo comum vira metadado e nao
# e pesquisavel. Indexar `pedido.id` seria tentador e errado: o limite e de
# 50 anotacoes por trace, e id unico nao agrupa nada.
awsemf:
namespace: ApiPedidos
log_group_name: /ecs/api-pedidos/emf
dimension_rollup_option: NoDimensionRollup
metric_declarations:
# A escolha de dimensao E a decisao de custo deste arquivo. Rota x status
# sao dezenas de series. Trocar por id de cliente seriam milhares; por id
# de requisicao, uma serie por requisicao — a documentacao do formato
# embutido avisa exatamente isso.
- dimensions: [[http.route, http.status_code]]
metric_name_selectors: ['http.server.request.duration']
service:
pipelines:
traces:
receivers: [otlp]
processors: [resourcedetection, tailsampling, batch/traces]
exporters: [awsxray]
metrics:
receivers: [otlp]
processors: [resourcedetection]
exporters: [awsemf]
| Estratégia de amostragem | Quando decide | O que ela garante | O que ela custa |
|---|---|---|---|
| Por cabeça, taxa fixa | na primeira requisição do trace | volume previsível e nenhuma memória de estado | o trace do erro reclamado provavelmente não existe |
| Por cabeça, com reservatório | na primeira requisição, com piso por segundo | que serviço de baixo tráfego não fique invisível | ainda não distingue erro de sucesso; é o padrão do SDK do X-Ray |
| Por cauda, por política | após o trace fechar, na janela de espera | 100% de erro e de lentidão, com amostra do resto | memória no coletor e traces mais longos que a janela decididos sem tudo |
| Remota, por regra no serviço | no SDK, consultando regras centralizadas | mudar a taxa sem novo deploy da aplicação | a decisão continua por cabeça: não sabe se houve erro |
| Nenhuma (100%) | não há decisão | todas as perguntas de trace têm resposta | é a linha da fatura que estoura o orçamento de 8% deste laboratório |
Formato embutido, filtro de métrica ou métrica customizada: como escolher
A pergunta que decide é quem controla o formato do log. Se é você, o formato embutido ganha: o valor numérico viaja no evento que já ia ser ingerido, sem chamada de API a mais, e você escolhe unidade e resolução. Se o log é de terceiro, de biblioteca ou de agente — formato que você não muda — o filtro de métrica sobre o grupo de logs é a ferramenta certa, e ela funciona por campo quando o log é JSON e por padrão de texto quando não é. A métrica customizada por chamada direta continua fazendo sentido em um caso: quando o valor não vem de um evento de log, como uma contagem periódica de estado que ninguém registraria de outro modo.
Construir: a task com dois contêineres, e o IAM de cada um
Duas coisas aqui merecem atenção antes do código. A primeira é essential = false no contêiner do coletor: com true, uma falha da telemetria mata a task e você troca um incidente de observação por um incidente de disponibilidade. A segunda é que quem envia telemetria é a APLICAÇÃO, então a permissão vai no papel da task — não no papel de execução, que é o do agente do ECS. Trocar os dois é o erro mais comum desta banda.
# observabilidade.tf — a task com dois conteineres, e o IAM que cada um precisa
# ── Grupos de logs SEPARADOS, com retencoes diferentes ───────────────────────
# Um grupo por proposito, porque retencao e uma decisao por proposito. Grupo
# criado sem retention_in_days guarda PARA SEMPRE, e e a linha esquecida mais
# comum da fatura de CloudWatch.
resource "aws_cloudwatch_log_group" "app" {
name = "/ecs/${var.projeto}-api"
retention_in_days = 14 # log de aplicacao raramente serve depois de 2 semanas
kms_key_id = aws_kms_key.logs.arn
}
resource "aws_cloudwatch_log_group" "emf" {
name = "/ecs/${var.projeto}-api/emf"
retention_in_days = 3 # o valor virou metrica; o log fonte nao precisa durar
kms_key_id = aws_kms_key.logs.arn
}
resource "aws_ecs_task_definition" "api" {
family = "${var.projeto}-api"
requires_compatibilities = ["FARGATE"]
network_mode = "awsvpc"
cpu = 1024 # +256 em relacao ao L01: o sidecar consome
memory = 2048
execution_role_arn = aws_iam_role.execucao.arn
task_role_arn = aws_iam_role.task.arn
container_definitions = jsonencode([
{
name = "api"
image = "${var.repositorio}:${var.sha_da_imagem}"
essential = true
portMappings = [{ containerPort = 8080, protocol = "tcp" }]
environment = [
{ name = "ASPNETCORE_URLS", value = "http://+:8080" },
# localhost porque, em awsvpc, os dois conteineres da task compartilham
# a interface de rede. Nao ha salto de rede entre eles.
{ name = "OTEL_EXPORTER_OTLP_ENDPOINT", value = "http://127.0.0.1:4317" },
# Propagador COMPOSTO. Manter so um dos dois e a causa numero um de
# trace partido: o contexto que chega nao e reconhecido e um id novo
# nasce no meio do caminho.
{ name = "OTEL_PROPAGATORS", value = "tracecontext,baggage,xray" },
{ name = "OTEL_SERVICE_NAME", value = "${var.projeto}-api" },
# O SHA entra na telemetria: e o que liga latencia a deploy (L03).
{ name = "SHA_DA_IMAGEM", value = var.sha_da_imagem },
]
logConfiguration = {
logDriver = "awslogs"
options = {
"awslogs-group" = aws_cloudwatch_log_group.app.name
"awslogs-region" = var.regiao
"awslogs-stream-prefix" = "api"
}
}
# A API nao sobe antes do coletor. Sem isto, os primeiros segundos de
# telemetria sao perdidos com recusa de conexao — e e justamente a partida
# que voce quer ver quando algo quebra no deploy.
dependsOn = [{ containerName = "coletor", condition = "START" }]
},
{
name = "coletor"
# Fixe a versao em vez de :latest, pelo mesmo motivo do L03: telemetria
# que muda de comportamento sozinha e pior que telemetria ausente.
image = "public.ecr.aws/aws-observability/aws-otel-collector:v0.43.0"
# essential = false E A DECISAO CENTRAL DESTE BLOCO. Com true, uma falha do
# coletor mata a task inteira e voce troca um incidente de observabilidade
# por um incidente de disponibilidade. Telemetria nao pode ser mais critica
# que o servico que ela observa.
essential = false
cpu = 256
memory = 512
secrets = [{
# A configuracao vem por referencia de ARN, nunca em environment.
name = "AOT_CONFIG_CONTENT"
valueFrom = aws_ssm_parameter.coletor.arn
}]
logConfiguration = {
logDriver = "awslogs"
options = {
"awslogs-group" = aws_cloudwatch_log_group.app.name
"awslogs-region" = var.regiao
"awslogs-stream-prefix" = "coletor"
}
}
},
])
}
# ── IAM: quem escreve telemetria e a APLICACAO, nao o agente do ECS ──────────
data "aws_iam_policy_document" "telemetria" {
statement {
sid = "EnviarTraceParaXRay"
effect = "Allow"
actions = [
"xray:PutTraceSegments",
"xray:PutTelemetryRecords",
"xray:GetSamplingRules",
"xray:GetSamplingTargets",
"xray:GetSamplingStatisticSummaries",
]
# Nenhuma destas cinco acoes aceita recurso especifico: o X-Ray nao tem ARN
# de segmento nem de regra de amostragem a que se restringir. Escrever um ARN
# aqui nao estreitaria nada — nao autorizaria nada. E `*` por natureza da
# API, e esta frase e o que separa decisao de preguica.
resources = ["*"]
}
statement {
sid = "EscreverLogEEmf"
effect = "Allow"
actions = [
"logs:CreateLogStream",
"logs:PutLogEvents",
"logs:DescribeLogStreams",
]
# Aqui SIM ha recurso: os dois grupos, e nada mais. O exportador de metrica
# embutida escreve num deles, e nao tem por que alcancar outro grupo da conta.
resources = [
"${aws_cloudwatch_log_group.app.arn}:*",
"${aws_cloudwatch_log_group.emf.arn}:*",
]
}
}
# Repare no que NAO esta na politica: cloudwatch:PutMetricData. A metrica deste
# desenho nasce do log, entao a permissao de publicar metrica e desnecessaria.
# Se um dia for preciso, ela tambem nao aceita recurso — mas aceita a chave de
# condicao cloudwatch:namespace, que e como se estreita esse caso.
resource "aws_iam_role_policy" "telemetria" {
role = aws_iam_role.task.id
policy = data.aws_iam_policy_document.telemetria.json
}
# ── Alarme sobre a metrica DERIVADA do log ───────────────────────────────────
# O namespace e o nome vem do exportador de metrica embutida do coletor. Se o
# alarme ficar em INSUFFICIENT_DATA para sempre, a causa quase certa e o par
# namespace/dimensao nao casar com o que o coletor declarou.
resource "aws_cloudwatch_metric_alarm" "p99_rota_pedidos" {
alarm_name = "${var.projeto}-p99-pedidos"
namespace = "ApiPedidos"
metric_name = "http.server.request.duration"
extended_statistic = "p99"
period = 60
evaluation_periods = 3
threshold = 2000
comparison_operator = "GreaterThanThreshold"
treat_missing_data = "notBreaching"
dimensions = {
"http.route" = "/api/pedidos/{id}"
"http.status_code" = "200"
}
# O corpo do aviso tem de trazer a consulta pronta. Alarme que diz "erro em
# producao" transfere ao plantao o trabalho de descobrir por onde comecar.
alarm_description = "p99 da rota de pedidos acima de 2 s. Investigue com: fields @timestamp, Message | filter http.route = '/api/pedidos/{id}' | filter http.status_code >= 500 | sort @timestamp desc"
alarm_actions = [aws_sns_topic.alertas.arn]
}
# Filtro de metrica: o OUTRO jeito de derivar metrica de log, e ele serve para o
# caso em que voce NAO controla o formato — log de terceiro, de biblioteca, de
# agente. Aqui cabe porque a excecao nao passa pelo coletor.
resource "aws_cloudwatch_log_metric_filter" "excecao_nao_tratada" {
name = "${var.projeto}-excecao-nao-tratada"
log_group_name = aws_cloudwatch_log_group.app.name
# Padrao de JSON: consulta por CAMPO, nao por substring. Com log de texto este
# filtro so poderia casar cadeia de caracteres, e mudanca de redacao da
# mensagem o quebraria em silencio.
pattern = "{ $.LogLevel = \"Critical\" }"
metric_transformation {
name = "ExcecaoNaoTratada"
namespace = "ApiPedidos"
value = "1"
default_value = "0" # sem isto, ausencia de erro vira ausencia de dado
}
}
| Onde | Parâmetro | Aqui | Por quê |
|---|---|---|---|
| Contêiner do coletor | `essential` | `false` | telemetria não pode ser mais crítica que o serviço que ela observa |
| Contêiner da API | `dependsOn` do coletor | condição `START` | sem isso, os primeiros segundos de telemetria somem em recusa de conexão — e é a partida que você quer ver |
| Endpoint OTLP | destino do exportador | `127.0.0.1:4317` | em modo `awsvpc` os dois contêineres compartilham a interface de rede: não há salto |
| Propagadores | `OTEL_PROPAGATORS` | `tracecontext,baggage,xray` | aceita o `traceparent` do W3C e o formato da AWS; só um dos dois parte o trace |
| Papel | quem tem permissão de X-Ray | papel da TASK | quem envia span é a aplicação; o papel de execução é do agente, para imagem e log |
| Grupo de logs da aplicação | `retention_in_days` | 14 | sem declarar, o grupo guarda indefinidamente — a linha esquecida mais comum |
| Grupo de logs do formato embutido | `retention_in_days` | 3 | o valor já virou métrica, com retenção própria; o log fonte é rastro de curto prazo |
| CPU e memória da task | acréscimo | +256 / +512 | o sidecar consome de verdade; dimensionar a task igual à do L01 causa reinício por memória |
O `*` da política, e por que ele não pode ser mais estreito
As cinco ações de X-Ray usadas aqui não aceitam recurso específico: não existe ARN de segmento nem de regra de amostragem ao qual restringir a permissão. Escrever um ARN nessa declaração não estreitaria nada — não autorizaria nada. Já a declaração de log é o caso oposto e mostra o contraste: ela nomeia os dois grupos e nenhum outro. A regra não é "nunca use `*`"; é que todo `*` vem acompanhado da frase que explica por que não pode ser mais estreito. E note o que a política NÃO tem: `cloudwatch:PutMetricData`, porque a métrica deste desenho nasce do log. Se um dia fizer falta, ela também não aceita recurso, mas aceita a chave de condição `cloudwatch:namespace` — que é como se estreita esse caso.
Implantar, e provar que a requisição é rastreável
Cinco provas. Todas têm número, e a última é o entregável do laboratório: partir do que um reclamante tem na mão e chegar ao salto que falhou em menos de dois minutos.
# provas.sh — cinco medicoes. Nenhuma conclusao vem de "parece que funcionou".
PROJETO=ffv-lab; REGIAO=us-east-1
GRUPO="/ecs/${PROJETO}-api"
URL="https://$(terraform output -raw dominio)/api/pedidos/8f3a1c22-0d41-4d9e-9c17-0b2f4a6e5d10"
# ── Prova 1: o identificador chega ao cliente e existe nos DOIS planos ───────
# E o entregavel do laboratorio. Partimos so do que um reclamante teria na mao.
CORR=$(curl -s -D - -o /dev/null "$URL" | awk -F': ' '/^[Xx]-[Cc]orrelation-[Ii]d/{print $2}' | tr -d '\r')
echo "identificador devolvido ao cliente: $CORR"
# Esperado: 32 caracteres hexadecimais. Se vier vazio, o middleware nao rodou ou
# nao ha Activity corrente — a instrumentacao do ASP.NET Core nao esta ativa.
[ "${#CORR}" -eq 32 ] || echo "FALHA DA PROVA 1: esperado 32 chars, veio ${#CORR}"
# ── Prova 2: TODA linha de log tem o identificador. A meta e 0 sem. ──────────
# Esta e a prova que separa "instrumentei" de "correlacionei". Um unico evento
# sem TraceId e um ponto cego permanente naquela requisicao.
CONSULTA_ID=$(aws logs start-query --log-group-name "$GRUPO" \
--start-time "$(($(date +%s) - 900))" --end-time "$(date +%s)" \
--query-string 'fields @message
| filter ispresent(Scopes.0.TraceId) = 0
| stats count() as sem_trace' \
--query queryId --output text)
sleep 8
aws logs get-query-results --query-id "$CONSULTA_ID" --output table
# Esperado: sem_trace = 0 num total de centenas. Qualquer valor > 0 aponta log
# escrito FORA do escopo da requisicao — tipicamente na partida ou num
# BackgroundService, e nesses casos e legitimo: confira a categoria antes.
# ── Prova 3: a amostragem faz aritmeticamente o que ela diz ─────────────────
# 600 requisicoes em ~60 s, das quais 60 forcadas a erro. Com a regra por cauda
# do coletor (100% de erro, 5% do resto), a conta esperada e:
# 60 (erros) + 0,05 x 540 (~27) = ~87 traces, nao 600.
for i in $(seq 1 540); do curl -s -o /dev/null "$URL"; done &
for i in $(seq 1 60); do curl -s -o /dev/null "${URL}?forcar_erro=1"; done
wait
aws xray get-trace-summaries --region "$REGIAO" \
--start-time "$(($(date +%s) - 300))" --end-time "$(date +%s)" \
--query 'length(TraceSummaries)' --output text
# Esperado: da ordem de 87, com tolerancia — a amostragem e estatistica, nao
# cota. O que REPROVA e sair ~600 (amostragem nao aplicada, e a fatura vem
# junto) ou sair ~30 (a regra de erro nao casou, e o trace que interessa nao
# existe). Confira tambem que 100% dos 60 erros aparecem:
aws xray get-trace-summaries --region "$REGIAO" \
--start-time "$(($(date +%s) - 300))" --end-time "$(date +%s)" \
--filter-expression 'error = true OR fault = true' \
--query 'length(TraceSummaries)' --output text
# Esperado: 60. Menos que isso significa que a decisao por cauda expirou antes
# de o trace fechar — aumente decision_wait.
# ── Prova 4: a metrica derivada casa com a contagem no log ──────────────────
# Duas fontes, um numero. Divergencia acima de 1% indica que o exportador de
# metrica embutida esta descartando evento — quase sempre por dimensao ausente
# no registro, que invalida o documento inteiro em silencio.
aws cloudwatch get-metric-statistics --namespace ApiPedidos \
--metric-name 'http.server.request.duration' --statistics SampleCount \
--start-time "$(date -u -v-15M +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -u -d '15 min ago' +%Y-%m-%dT%H:%M:%SZ)" \
--end-time "$(date -u +%Y-%m-%dT%H:%M:%SZ)" --period 900 \
--dimensions Name=http.route,Value='/api/pedidos/{id}' Name=http.status_code,Value=200 \
--query 'Datapoints[0].SampleCount' --output text
# Compare com o count() do mesmo intervalo no grupo de logs. Diferenca < 1%.
# Confira tambem se houve documento recusado: o CloudWatch publica falha de
# extracao no namespace AWS/Logs.
# ── Prova 5: do ticket ao span, em duas consultas e menos de 2 minutos ──────
# A prova de que o LABORATORIO cumpriu o objetivo. Cronometre de verdade.
inicio=$(date +%s)
aws logs start-query --log-group-name "$GRUPO" \
--start-time "$(($(date +%s) - 3600))" --end-time "$(date +%s)" \
--query-string "fields @timestamp, Message, State.PedidoId, http.status_code
| filter Scopes.0.TraceId = '${CORR}'
| sort @timestamp asc" --query queryId --output text
aws xray batch-get-traces --trace-ids "$(aws xray get-trace-summaries \
--start-time "$(($(date +%s) - 3600))" --end-time "$(date +%s)" \
--query 'TraceSummaries[0].Id' --output text)" \
--query 'Traces[0].Segments[].Document' --output text | head -5
echo "segundos do ticket ao span: $(( $(date +%s) - inicio ))"
# Esperado: abaixo de 120 s, e nenhuma das duas consultas usa horario como
# filtro principal. Se voce precisou adivinhar a janela, a correlacao nao esta
# no lugar — esta na sua cabeca.
| Prova | Comando | Resultado que aprova | O que reprova, e o que significa |
|---|---|---|---|
| 1 · O id chega ao cliente | ler o cabeçalho de resposta | 32 caracteres hexadecimais em `X-Correlation-Id` | vazio significa que não havia `Activity` corrente: a instrumentação do ASP.NET Core não está ativa |
| 2 · Toda linha tem correlação | contagem de eventos sem `TraceId` | 0 de centenas | qualquer valor acima de zero é log escrito fora do escopo da requisição — confira a categoria antes de tratar como defeito |
| 3 · A amostragem faz a aritmética | 600 requisições, 60 com erro | da ordem de 87 traces, e 60 deles com falha | ~600 significa amostragem não aplicada, e a fatura vem junto; ~30 significa que a regra de erro não casou, e o trace que importa não existe |
| 4 · A métrica casa com o log | contagem de amostras vs. `count()` no log | divergência abaixo de 1% | divergência maior indica documento de métrica embutida recusado, quase sempre por dimensão declarada e ausente no evento |
| 5 · Do ticket ao span | duas consultas, cronometradas | abaixo de 120 s, sem usar horário como filtro principal | se você precisou adivinhar a janela de tempo, a correlação está na sua cabeça e não no dado |
A prova 2 é a que separa instrumentar de correlacionar
É comum passar na prova 1 e falhar na 2. O trace existe, o span está no X-Ray, o cliente recebeu um id — e metade das linhas de log não tem `TraceId`, porque foram escritas por um serviço em segundo plano, por um manipulador de exceção fora do escopo da requisição, ou por uma biblioteca que usa o seu próprio provedor de log. Cada uma dessas linhas é um ponto cego permanente naquela requisição, e ela some exatamente na investigação em que faria falta.
Quebrar de propósito: três falhas e o diagnóstico
As três acontecem de verdade, e nenhuma delas gera erro. É a característica que as torna caras: a instrumentação parece funcionando até o dia em que você precisa dela.
| Falha | Como provocar | Sintoma | Onde olhar | Correção |
|---|---|---|---|---|
| Trace partido em dois | remova `xray` de `OTEL_PROPAGATORS` e chame a API passando por um serviço que só fala o formato da AWS | dois traces de um span cada, sem aresta entre eles; o mapa de serviços fica com nós soltos | o mapa de serviços e a contagem de spans por trace: um trace de um span numa chamada que teve dois saltos é o indício | propagador composto com os dois formatos; a variável `OTEL_PROPAGATORS` é o lugar mais estável de configurá-lo |
| Span com trace, log sem trace | remova a linha de `ActivityTrackingOptions` e mantenha todo o resto | o X-Ray mostra o trace e a consulta por `TraceId` no log devolve zero eventos | a prova 2: a contagem de eventos sem o campo passa de 0 para 100% | reativar o rastreamento de atividade na fábrica de log; é uma linha e é o elo do módulo inteiro |
| Explosão de cardinalidade de métrica | acrescente `TraceId` à lista de `dimensions` do exportador de métrica embutida e gere 1.000 requisições | nenhum erro; a fatura de métrica customizada cresce e o painel fica com mil séries de um ponto cada | `list-metrics` no namespace devolve número da ordem do número de requisições, e o namespace `AWS/Logs` acusa o processamento | dimensão é para AGRUPAR: rota, status, ambiente. Identificador único vira campo do evento, nunca dimensão |
A falha silenciosa que nenhuma das três cobre
Um documento de métrica embutida com dimensão DECLARADA e ausente no evento é descartado inteiro — sem erro na aplicação, sem erro no coletor, sem métrica. O log continua lá, legível, e a série temporal simplesmente não existe, o que aparece como alarme preso em dados insuficientes. É por isso que a prova 4 compara duas fontes: a única maneira de detectar isso é conferir que o número da métrica casa com o número do log. O CloudWatch publica falha de extração no namespace `AWS/Logs`, e vale alarmar sobre ela.
Uma API .NET 8 em Fargate tem OpenTelemetry ativo, os traces aparecem completos no X-Ray e o log já sai em JSON. Mesmo assim, a consulta `filter Scopes.0.TraceId = "..."` no Logs Insights devolve zero eventos. Qual é a causa mais provável?
Segurança: telemetria é dado sensível e é vetor
Observabilidade cria duas superfícies novas ao mesmo tempo. Uma é de vazamento: o log passa a ser o lugar mais provável de um dado pessoal aparecer sem intenção. A outra é de abuso: o cabeçalho de rastreamento vem do cliente, e cliente é entrada não confiável.
| Risco | Probabilidade | Impacto | Controle preventivo | Detecção | Resposta |
|---|---|---|---|---|---|
| Cliente forja o cabeçalho de rastreamento para forçar amostragem de 100% | baixa | médio | remover `X-Amzn-Trace-Id` de entrada na borda quando a origem não é confiável — a própria documentação do X-Ray recomenda isso | volume de traces desproporcional ao volume de requisições | descartar o cabeçalho de entrada e reamostrar no coletor, que decide por conta própria |
| Dado pessoal em campo de log | alta | alto | registrar identificador de recurso, nunca corpo de requisição nem cabeçalho de autorização; revisão de código no que se loga | busca por padrão de documento, cartão e endereço de e-mail no grupo de logs | apagar o fluxo de log afetado, encurtar retenção e mascarar na saída do coletor |
| Segredo em variável de ambiente aparecendo na telemetria | média | alto | configuração do coletor por referência de ARN em `secrets`, nunca em `environment`; nada de despejar o ambiente no log de partida | CloudTrail em leitura de parâmetro e busca por prefixo de credencial no log | rotacionar o segredo — presumir que log lido é log copiado |
| Log sem cifragem em repouso | média | médio | chave gerenciada pelo cliente no grupo de logs, com política que separa quem lê de quem administra | auditar grupos sem `kmsKeyId` na conta | associar a chave; o histórico já ingerido continua com a cifragem anterior |
| Trace id como identificador de sessão | baixa | alto | nunca usar o trace id em decisão de autorização: ele é público por desenho e vai no cabeçalho da resposta | revisão de código em qualquer uso de trace id fora de log e telemetria | remover o uso; tratar como se o valor tivesse sido publicado |
| Grupo de logs legível por toda a organização | média | médio | permissão de leitura por grupo, não por conta; separar log de aplicação de log de auditoria | Access Analyzer sobre políticas com `logs:FilterLogEvents` amplo | estreitar a política e derivá-la do uso real medido — é o L41 |
| Consulta de log varrendo 30 dias sem necessidade | alta | baixo | janela de tempo padrão curta nos painéis; treinar a equipe a filtrar por campo | custo de consulta do Logs Insights por identidade | consultas salvas com janela definida, e o filtro por `TraceId` como caminho padrão |
Por que o trace id pode ir para o cliente e o log não pode ir para o cliente
O trace id é um número aleatório sem semântica: conhecê-lo não autoriza nada e não revela nada sobre outra requisição. É por isso que devolvê-lo no cabeçalho da resposta é seguro e útil. O conteúdo do log é o oposto: ele carrega parâmetro, identificador de recurso e às vezes mensagem de exceção com detalhe de implementação. A assimetria é o desenho: um identificador opaco atravessa a fronteira, o dado que ele indexa não.
Observabilidade: as três perguntas e os três preços
Log, métrica e trace não são três formas de ver a mesma coisa. São três respostas a perguntas diferentes, com custos e cardinalidades diferentes — e usar um no lugar do outro é a origem tanto de fatura alta quanto de ponto cego.
| Plano | Pergunta que ele responde | Pergunta que ele NÃO responde | Como o custo cresce | Cardinalidade que ele suporta |
|---|---|---|---|---|
| Métrica | o sistema está bem AGORA, e desde quando piorou | o que aconteceu com a requisição do cliente Y | por série temporal, ou seja, por combinação de dimensões | baixa: dezenas ou centenas de séries; identificador único é proibitivo |
| Log estruturado | o que exatamente aconteceu nesta requisição, com quais parâmetros | onde o tempo foi gasto entre os serviços | por byte ingerido, por byte retido e por byte varrido em consulta | alta: campo com valor único é normal e desejável |
| Trace | em qual salto o tempo foi, e qual serviço causou a falha | com qual parâmetro a consulta rodou | por span ingerido e, separadamente, pela fração indexada | média: atributo de span é livre, mas anotação indexada tem limite de 50 por trace |
| Pergunta que o painel tem de responder | Métrica ou consulta | O que significa mudar | Limiar inicial |
|---|---|---|---|
| A rota de pedidos está lenta? | p99 de `http.server.request.duration` por rota | regressão de latência que não gera erro | acima de 2 s por 3 períodos de 1 min |
| Estamos devolvendo erro? | contagem de eventos com status maior ou igual a 500 | falha da aplicação, distinta de falha de capacidade do ALB (L03) | acima de 1% das requisições da rota |
| Quantos eventos perderam correlação? | contagem de eventos sem `TraceId` | código escrevendo log fora do escopo da requisição | qualquer valor acima de zero |
| A telemetria está chegando? | contagem de spans ingeridos por minuto | coletor caído — e como ele é não essencial, a API continua de pé em silêncio | queda para zero por 5 min |
| O documento de métrica está válido? | métricas de falha de extração no namespace `AWS/Logs` | dimensão declarada e ausente descartando o documento inteiro | qualquer valor acima de zero |
| O tempo é de banco ou de código? | soma dos subsegmentos contra a duração do segmento | a repartição mudou de lado, e a correção é em lugar diferente | banco acima de 60% do tempo total da requisição |
| A observabilidade caiu no orçamento? | custo de CloudWatch e X-Ray contra o da aplicação | ou o volume cresceu, ou alguém acrescentou uma dimensão | acima de 8% da fatura da aplicação |
| Quantas séries de métrica existem no namespace? | contagem de métricas listadas | crescimento sem deploy é sinal de cardinalidade escapando | crescimento acima de 20% em uma semana |
O alarme que ninguém cria e que é o mais importante desta seção
Alarme sobre a própria telemetria. Como o contêiner do coletor é não essencial — e tem de ser —, uma falha dele deixa a aplicação funcionando perfeitamente e cega. Não há erro, não há 5xx, não há reinício: só param de aparecer spans. Sem um alarme sobre a contagem de spans ingeridos, você descobre isso no próximo incidente, que é o pior momento possível.
Escala: 10, 10 mil, 1 milhão
| Volume | O que acontece com a observabilidade | O que passa a doer | O que fazer |
|---|---|---|---|
| 10 req/dia, 1 task | log em 100% e trace em 100% caberiam sem esforço | nada; e a amostragem por taxa fixa torna o serviço invisível | reservatório com piso por segundo, ou nenhuma amostragem — em volume baixo, guardar tudo é a decisão barata |
| 500 mil req/dia, 4 tasks | é o cenário deste laboratório: 3,4 GB de log/dia | a retenção indefinida do grupo de logs, que ninguém percebe até o terceiro mês | retenção declarada por propósito, amostragem por cauda e métrica derivada do log |
| 50 milhões req/dia, 200 tasks | a ingestão de log passa a ser linha grande da fatura | o custo de ingestão e a memória do coletor mantendo traces até decidir | reduzir o log de nível informativo por amostragem própria, exportar log frio para S3 e consultar com Athena (L64); manter 100% só do log de erro |
| Pico de 20× em promoção | volume de telemetria acompanha o de requisição, linearmente | a ingestão dispara junto com o incidente que você quer investigar | não é para impedir; é para saber. Orçamento de custo com alerta, e limite de ingestão no grupo de logs se o risco financeiro for maior que o de cegueira |
| Falha de uma AZ | metade das tasks para, e o log delas para com elas | os últimos segundos antes da queda são justamente os que interessam | o driver `awslogs` envia em fluxo contínuo, não no fim: o que se perde é a fração em trânsito. Não escreva log em arquivo dentro do contêiner esperando recuperá-lo |
| Segundo serviço entra no desenho | o trace deixa de ser local e passa a ser distribuído | a propagação entre serviços, que é onde o trace parte em dois | propagador composto em TODOS os serviços, e um teste que verifica a contagem de spans por trace no caminho crítico |
O limite que aparece quando o sistema fica grande de verdade
A busca por transação suporta trace com até 10 mil spans, e um documento de segmento tem limite de 64 kB. Nenhum dos dois incomoda numa API de dois saltos. Os dois passam a incomodar quando alguém instrumenta um laço: um trace com um span por item de uma lista de 50 mil não é observabilidade, é um jeito caro de guardar a lista. Span representa unidade de trabalho com significado, não iteração.
Custo: as dimensões, e o único termo que explode
Observabilidade tem seis dimensões de cobrança e apenas uma delas cresce de forma multiplicativa. Saber qual é resolve a maior parte das surpresas de fatura.
| Cenário | Volume | O que domina | Tendência | Otimização |
|---|---|---|---|---|
| Protótipo | 10 mil req/dia, 1 task | nada domina; a fatura é ruído | desprezível | nenhuma. Otimizar aqui é gastar atenção onde não há dinheiro — e amostrar em volume baixo só cria pontos cegos |
| Produção pequena | 500 mil req/dia, 4 tasks | ingestão e retenção de log, nessa ordem | linear e previsível | retenção por propósito, métrica derivada do log em vez de chamada por medição, e amostragem por cauda no span |
| Alta escala | 50 milhões req/dia, 200 tasks | ingestão de log; e cardinalidade de métrica, se alguém escorregar | a de log é linear; a de cardinalidade é degrau súbito | amostrar o log informativo e manter 100% do log de erro, exportar log frio para consulta por varredura, e revisar dimensões a cada deploy |
| Dimensão | Cobra por | Cuidado |
|---|---|---|
| Ingestão no CloudWatch Logs | GB entregue ao serviço | JSON é mais verboso que texto, e o corte que funciona é remover campo que ninguém consulta — não voltar para texto |
| Retenção de log | GB armazenado por mês | grupo sem `retention_in_days` guarda indefinidamente; é a linha esquecida mais comum |
| Consulta no Logs Insights | GB varrido pela consulta | a janela de tempo da consulta é decisão de custo; filtro por campo em janela curta é barato, varredura de 30 dias não |
| Spans ingeridos | volume de span entregue | na busca por transação, 100% é ingerido como log estruturado — este termo não é reduzido por amostragem de indexação |
| Spans indexados | porcentagem configurada de indexação | o padrão indexa 1% sem cobrança adicional e vai de 0 a 100; é o que separa "ver este trace" de "procurar traces assim" |
| Métricas customizadas | por série temporal ativa | é o termo multiplicativo. Uma dimensão de alta cardinalidade transforma dezenas de séries em milhares, sem nenhum erro e sem aviso |
| Alarmes | por alarme e por período de avaliação | valor pequeno e fixo; alarme sobre estatística estendida como p99 tem preço próprio, que continua modesto |
O custo oculto deste laboratório, e ele não está na AWS
É a atenção. Cada campo que você acrescenta ao log estruturado, cada dimensão de métrica e cada alarme é uma coisa que alguém vai ler numa madrugada. Painel com quarenta gráficos não responde mais perguntas que um com seis: responde menos, porque ninguém acha o gráfico certo. O critério para acrescentar sinal é o mesmo do módulo inteiro — que pergunta ele responde, e o que se decide diferente por causa dele. Sem as duas respostas, é decoração que cobra ingestão. Para os valores em moeda, use o AWS Pricing Calculator: preço varia por região e envelhece mais rápido que este texto.
Well-Architected nos seis pilares
| Pilar | Situação ao fim deste laboratório | Risco que fica | Melhoria | Prioridade |
|---|---|---|---|---|
| Excelência operacional | toda requisição é rastreável por identificador, com log correlacionado e trace amostrado com regra de erro | não há objetivo de nível de serviço nem política de acionamento: o alarme avisa e ninguém sabe se é aceitável | SLO e orçamento de erro (L51) | alta |
| Segurança | log cifrado com chave gerenciada, telemetria por papel da task com privilégio justificado, segredo por referência | o log pode receber dado pessoal por descuido de quem escreve a próxima rota | mascaramento na saída do coletor e revisão do que se loga (L52) | alta |
| Confiabilidade | falha da telemetria não derruba a API, e há alarme sobre a própria telemetria | o coletor em sidecar é ponto único por task: se ele saturar, aquela task fica cega | alarme por task na contagem de spans, e limite de memória no processador de amostragem | média |
| Eficiência de performance | exportação em lote fora do processo da API; instrumentação de saúde filtrada | nenhuma medição do custo em CPU da própria instrumentação | comparar p99 com e sem instrumentação em teste de carga (L07) | média |
| Otimização de custos | métrica derivada do log, retenção por propósito, span amostrado por cauda | cardinalidade de dimensão não tem controle automático: depende de revisão humana | gate no pipeline que reprova dimensão nova sem justificativa, e orçamento com alerta (L09) | alta |
| Sustentabilidade | só se transmite e se guarda o que responde a uma pergunta declarada | log de nível informativo em 100% move bytes que quase nunca são lidos | amostrar o informativo mantendo 100% do erro, quando o volume justificar | baixa |
Evolução em níveis: o que muda, e o que passa a doer
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 parte que raramente se escreve.
Texto no stdout, um grupo de logs, `grep` quando alguém reclama. É onde a Cadência estava, e continua legítimo num serviço com um componente e pouco tráfego.JSON de uma linha por evento, retenção declarada, consulta por campo no Logs Insights. Ainda sem trace e sem propagação.Propagador composto, trace id em toda linha e em todo span, coletor em sidecar, amostragem por cauda, métrica derivada do log e o id devolvido ao cliente.O log de nível informativo passa a ser amostrado, o log de erro continua em 100%, o log frio vai para o S3 e é consultado por varredura. Painel por serviço, não por recurso.Objetivo de nível de serviço por jornada de usuário, orçamento de erro que governa o ritmo de deploy, e acionamento só sobre sintoma percebido pelo cliente (L51).A telemetria vira conjunto de dados: detecção de anomalia sobre métrica, agrupamento automático de exceções semelhantes e sumarização do trace de um incidente para acelerar a primeira hipótese (L52 e a banda de IA).A ordem não é negociável, e o motivo é concreto
SLO no nível 5 depende de medir a jornada do cliente ponta a ponta, que depende de correlação — o nível 3. Quem define objetivo de nível de serviço antes de ter correlação mede o que é fácil de medir, tipicamente saúde de recurso, e passa a perseguir um número que não corresponde ao que o cliente sente. E detecção de anomalia no nível 6 sobre métrica de cardinalidade descontrolada aprende ruído. Cada nível é pré-requisito de dado, não de maturidade.
Onde IA entra nesta arquitetura, e onde não entra
No problema central deste laboratório, IA não entra, e forçá-la seria o antipadrão que a série critica. "Qual foi o caminho desta requisição" é uma pergunta com resposta exata: existe um identificador, ele está no dado, e uma consulta o devolve. Trocar isso por um modelo que infere correlação a partir de horário e semelhança de texto é substituir uma resposta certa por uma provável — e mais caro.
Há dois lugares onde IA acrescentaria valor real, e os dois são posteriores à correlação, não substitutos dela. O primeiro é agrupar exceções semanticamente equivalentes: mil eventos que dizem a mesma coisa com mensagens diferentes viram um grupo com mil ocorrências, e isso é trabalho de similaridade que regra por padrão de texto faz mal. O segundo é a primeira hipótese num incidente: dado um trace com o salto lento e os eventos de log daquela requisição, resumir o que provavelmente aconteceu economiza os primeiros minutos — que são os mais caros.
| Pergunta | Resposta honesta para este módulo |
|---|---|
| Qual problema a IA resolveria? | reduzir mil eventos a um punhado de grupos com significado, e propor a primeira hipótese de causa a partir de trace mais log |
| Por que uma regra não bastaria? | para agrupar, regra por padrão de texto quebra a cada mudança de redação da mensagem e não reconhece equivalência semântica. Para a hipótese, regra bastaria nos casos conhecidos — e é onde se deve começar |
| De onde viriam os dados? | do que este laboratório produziu: log estruturado com campos, spans com duração por salto e o histórico de incidentes. Nada de fonte nova |
| Qual o risco? | hipótese plausível e errada, apresentada com confiança, que desvia a investigação. O antídoto é o resumo sempre vir com o link para o dado, e a decisão nunca ser tomada só sobre o resumo |
| Por que não substituir a correlação por IA? | porque a correlação é exata e a inferência é probabilística. Onde existe identificador, modelo só acrescenta latência e a chance de errar com convicção |
O uso de IA que parece atraente e é armadilha aqui
Pedir a um modelo para "ler o log e achar a requisição que o cliente reclamou". É exatamente o trabalho que a correlação torna trivial e que a inferência torna caro e incerto: sem identificador comum, o modelo vai correlacionar por horário e por semelhança — que é o mesmo método falho do desenho mínimo, agora com uma fatura de inferência por cima. IA sobre observabilidade tem lugar quando o sinal é volumoso ou ambíguo demais para regra. Um identificador presente em toda linha não é nem uma coisa nem outra.
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 |
|---|---|---|---|---|---|
| Interpolar valores na mensagem de log | é o que a linguagem faz por padrão, e o resultado é legível na hora | transforma dado em texto: o que era campo passa a ser pedaço de frase, e a consulta volta a ser substring | não se consegue contar erros por loja sem expressão regular sobre texto livre | modelo de mensagem com nome de campo, e o valor como parâmetro | em log de partida da aplicação, que ninguém consulta por campo |
| Trace no X-Ray e log sem trace id | a instrumentação automática dá o trace de graça, e o log parece já estar resolvido porque virou JSON | é o pior dos dois mundos: o trace mostra ONDE o tempo foi e não com qual parâmetro; o log tem o parâmetro e não se liga ao trace | a investigação tem duas metades que não se encontram, e cada uma parece quase suficiente | rastreamento de atividade na fábrica de log, anexando TraceId e SpanId ao escopo | nunca; é uma linha de configuração e é o elo do módulo |
| Amostrar 100% em produção "para não perder nada" | em desenvolvimento é o que funciona, e a intuição é que mais dado é sempre melhor | a fatura cresce com o tráfego e o painel fica com volume que ninguém consegue ler; em incidente, o excesso atrapalha | o custo de observabilidade passa o de computação e alguém corta tudo de uma vez, trocando desperdício por cegueira | amostragem por cauda: 100% de erro e de lentidão, fração pequena do resto | em volume genuinamente baixo, onde amostrar só cria ponto cego |
| Usar identificador único como dimensão de métrica | dá um gráfico por requisição, e por um momento parece o auge da visibilidade | cada combinação distinta é uma métrica nova: a contagem de séries iguala a de requisições, e a documentação avisa esse caso pelo nome | nenhum erro, e a fatura de métrica customizada multiplicada; painel com mil séries de um ponto | dimensão agrupa (rota, status, ambiente); identificador vira campo do evento de log | nunca em métrica. Em log e em atributo de span, é normal e desejável |
| Correlacionar por horário | com pouco tráfego funciona, e não exige mudar nada no código | sob concorrência, linhas de requisições diferentes se intercalam no mesmo milissegundo e a leitura em sequência produz histórias falsas | diagnóstico confiante e errado, que é pior que diagnóstico ausente | identificador comum em toda linha, e o horário só para ordenar dentro do trace | para ordenar eventos que você JÁ sabe pertencerem à mesma requisição |
| Logar o corpo inteiro da requisição "para ter contexto" | garante que nada vai faltar na hora do aperto, e é uma linha | multiplica a ingestão por um fator que ninguém estimou e é o caminho mais curto para dado pessoal no log | fatura de ingestão alta e um achado de conformidade meses depois | campos escolhidos: identificador de recurso, rota, status, duração | em depuração de curta duração, com prazo declarado e retenção curta |
| Marcar o contêiner do coletor como essencial | parece rigor: "se a telemetria caiu, quero saber" | inverte a hierarquia — uma falha da observação passa a derrubar o serviço observado | a API fica indisponível por causa de um problema no backend de telemetria | `essential = false`, mais um alarme sobre a contagem de spans ingeridos | nunca em serviço que atende cliente. Em job cuja saída É a telemetria, faz sentido |
| Um grupo de logs para tudo, sem retenção | é o que acontece por omissão: o driver cria o grupo e ninguém volta lá | guarda indefinidamente e mistura propósitos com necessidades de retenção diferentes | a maior linha da fatura de CloudWatch é armazenamento de log de 2023 | um grupo por propósito, cada um com retenção declarada em código | nunca em conta que fica de pé; em conta de laboratório que será apagada, é indiferente |
Quando algo não funciona
| Sintoma | Causa provável | Como investigar | Onde olhar | Correção |
|---|---|---|---|---|
| Nenhum trace aparece no X-Ray | papel da TASK sem permissão de X-Ray, ou permissão posta no papel de execução | leia o log do contêiner do coletor: ele registra a recusa explicitamente | fluxo de log do coletor no grupo da aplicação | as cinco ações de X-Ray no papel da task; o papel de execução é do agente do ECS |
| Trace aparece com um span só, numa chamada de dois saltos | propagador incompleto: falta o formato da AWS ou falta o `traceparent` | compare o trace id do log do chamador com o do chamado — se diferem, o contexto não foi reconhecido | mapa de serviços com nós soltos; `OTEL_PROPAGATORS` | propagador composto: `tracecontext,baggage,xray` |
| Consulta por `TraceId` no log devolve zero eventos | rastreamento de atividade não configurado na fábrica de log | abra um evento cru e procure o campo dentro de `Scopes` | a prova 2, que conta eventos sem o campo | a linha de `ActivityTrackingOptions`, mais `IncludeScopes = true` no formatador |
| Cada linha do log virou vários eventos truncados | JSON identado: o driver `awslogs` quebra por salto de linha | olhe um evento no console — ele terá uma chave e nada mais | fluxo de log da aplicação | `Indented = false` nas opções do escritor JSON |
| Alarme preso em dados insuficientes | namespace, nome de métrica ou dimensão não casam com o que o coletor declarou | liste as métricas do namespace e compare nome e dimensões, caractere por caractere | `list-metrics` contra o `metric_declarations` do coletor | alinhar os três; e conferir se algum documento foi recusado, no namespace `AWS/Logs` |
| A métrica existe e o número não bate com o log | documento de métrica embutida recusado por dimensão declarada e ausente no evento | a prova 4: comparar contagem de amostras com `count()` no log | métricas de falha de extração em `AWS/Logs` | garantir que todo campo listado em `Dimensions` exista na raiz do evento |
| O trace da requisição reclamada não existe | amostragem por cabeça, que decidiu antes de saber que houve erro | confira qual estratégia está ativa; se a decisão está no SDK, é por cabeça | configuração do coletor e variáveis de amostragem no contêiner | amostragem por cauda com política de status de erro em 100% |
| Só metade dos erros de um teste aparece como trace | a janela de decisão por cauda expirou antes de o trace fechar | compare a duração dos traces perdidos com `decision_wait` | log do coletor, que registra decisão sobre trace incompleto | aumentar a janela de espera, ciente de que ela custa memória no coletor |
| Correlação some em requisições grandes | cabeçalhos acima de 7 KB: o ALB reescreve o `X-Amzn-Trace-Id` com um `Root` novo | meça o tamanho total dos cabeçalhos das requisições afetadas | access log do ALB comparado ao trace id gravado no log da aplicação | reduzir cabeçalho e cookie; e aceitar que, acima do limite, a correlação é refeita pelo balanceador |
| A contagem de séries do namespace cresceu sem deploy | dimensão de alta cardinalidade entrou por configuração do coletor | liste as métricas e olhe as dimensões que existem, não as que você acha que existem | `list-metrics` no namespace | remover a dimensão; a série já criada expira por desuso, e o mês já foi cobrado |
A pergunta que resolve metade destes casos
Antes de mexer em configuração, pergunte: o dado não foi PRODUZIDO, não foi TRANSPORTADO ou não foi CONSULTADO corretamente? Ausência de span aponta produção ou permissão; evento truncado aponta transporte; alarme em dados insuficientes com log presente aponta consulta ou nome que não casa. São três lugares diferentes, e a maioria das horas perdidas vem de investigar o segundo quando o problema é no terceiro.
Limpeza: o que o destroy não leva
Três coisas aqui sobrevivem ao terraform destroy e uma delas não se apaga de jeito nenhum: série de métrica customizada expira por desuso, não por comando. É o motivo de a explosão de cardinalidade ser didática uma vez e caríssima duas.
# limpar.sh — a ordem importa, e tres coisas aqui cobram depois do destroy.
# 1. O que o Terraform administra.
terraform destroy -auto-approve
# 2. GRUPOS DE LOGS: sobrevivem se skip_destroy estiver ligado, e sobrevivem
# sempre se voce os criou implicitamente (o driver awslogs cria o grupo
# quando ele nao existe, e esse nao entra no estado do Terraform).
for g in /ecs/ffv-lab-api /ecs/ffv-lab-api/emf; do
aws logs describe-log-groups --log-group-name-prefix "$g" \
--query 'logGroups[].{nome:logGroupName,bytes:storedBytes,ret:retentionInDays}' \
--output table
aws logs delete-log-group --log-group-name "$g" 2>/dev/null || true
done
# 3. GRUPO DE SPANS: se voce habilitou o Transaction Search, os spans vao para um
# grupo gerenciado chamado aws/spans, e ele NAO pertence ao seu Terraform.
# Desabilitar a busca por transacao e a acao que para a ingestao; apagar o
# grupo sem desabilitar apenas o recria.
aws logs describe-log-groups --log-group-name-prefix "aws/spans" \
--query 'logGroups[].{nome:logGroupName,bytes:storedBytes}' --output table
# 4. METRICAS CUSTOMIZADAS nao se apagam. Elas expiram por desuso (15 meses de
# retencao de serie no CloudWatch). Se voce criou serie de alta cardinalidade
# por engano, o custo do mes ja foi — a correcao e parar de produzir, e a
# licao e a secao de custo deste modulo.
aws cloudwatch list-metrics --namespace ApiPedidos \
--query 'length(Metrics)' --output text
# Se este numero for da ordem do numero de requisicoes do teste, voce reproduziu
# a explosao de cardinalidade. E didatico uma vez; e caro duas.
# 5. ALARMES e filtro de metrica, se criados fora do Terraform.
aws cloudwatch delete-alarms --alarm-names ffv-lab-p99-pedidos 2>/dev/null || true
aws logs delete-metric-filter --log-group-name /ecs/ffv-lab-api \
--filter-name ffv-lab-excecao-nao-tratada 2>/dev/null || true
# 6. REGRAS DE AMOSTRAGEM do X-Ray criadas a mao permanecem na conta. Nao cobram,
# mas se aplicam ao proximo servico que voce subir — e isso surpreende.
aws xray get-sampling-rules \
--query 'SamplingRuleRecords[].SamplingRule.RuleName' --output table
# 7. O que veio do L01 e cobra POR HORA, ligado ou nao: ALB, RDS, NAT Gateway e o
# Elastic IP dele, e endpoint de interface de VPC. Se voce nao vai seguir para
# o L09, rode a limpeza do L01 tambem.
aws ec2 describe-addresses --query "Addresses[?AssociationId==null].PublicIp" --output table
# 8. Prova final: nada com a etiqueta do projeto de pe.
aws resourcegroupstaggingapi get-resources \
--tag-filters Key=Projeto,Values=ffv-lab \
--query "ResourceTagMappingList[].ResourceARN" --output table
| Recurso | Sai no destroy? | Cobra parado? | Por que fica |
|---|---|---|---|
| Definição de task com os dois contêineres | sim | não | as revisões permanecem registradas, sem custo |
| Grupos de logs declarados no Terraform | sim, se sem `skip_destroy` | sim, por GB retido | e se o driver `awslogs` criou o grupo implicitamente, ele nunca entrou no estado |
| Grupo gerenciado de spans | não | sim, por GB retido | pertence à busca por transação, não ao seu Terraform; apagar sem desabilitar apenas recria |
| Séries de métrica customizada | não | enquanto ativas | não existe comando para apagar métrica: ela expira por desuso, e o mês corrente já foi cobrado |
| Filtro de métrica | sim se em Terraform | não | criado no console, não aparece no estado — e continua extraindo do grupo |
| Alarmes | sim se em Terraform | centavos por alarme | alarme sobre estatística estendida tem preço próprio, ainda modesto |
| Regras de amostragem do X-Ray | sim se em Terraform | não | regra criada à mão continua na conta e se aplica ao PRÓXIMO serviço que você subir |
| Chave do KMS dos logs | entra em espera de exclusão | sim, por chave-mês | a exclusão tem período de espera obrigatório; até lá, cobra |
| ALB, RDS, NAT e Elastic IP (L01) | sim | sim, por hora | vêm do laboratório anterior; o Elastic IP cobra justamente por NÃO estar associado |
Resumo: problema, peça e motivo
| Problema | Peça | Por que ela, e não outra |
|---|---|---|
| Não sei qual requisição é a do cliente | trace id devolvido no cabeçalho da resposta | transforma um relato em chave de busca exata, e o valor é opaco: não revela nada |
| As linhas não se ligam | rastreamento de atividade na fábrica de log | o contexto é ambiental: nenhuma rota precisa receber ou repassar identificador |
| Não consigo filtrar por rota ou por loja | JSON de uma linha com nome de campo | nome é o que uma consulta pode referenciar; frase não é |
| O contexto que chega não é reconhecido | propagador composto AWS e W3C | é a única causa de trace partido que não gera erro nenhum |
| Não sei onde o tempo foi gasto | instrumentação do cliente de banco e de HTTP | subsegmento com duração reparte o tempo entre os saltos, em vez de somá-lo |
| O trace do erro reclamado não existe | amostragem por cauda com 100% de erro | a decisão por cabeça acontece antes de haver resultado para usar |
| Métrica custa uma chamada por medição | métrica embutida no evento de log | aproveita a ingestão que já ia acontecer; e o filtro de métrica cobre o log que você não formata |
| A telemetria não pode derrubar a API | sidecar com `essential = false` | observação não pode ser mais crítica que o observado — mais alarme sobre a própria telemetria |
| A fatura de log cresce sem controle | retenção declarada por propósito | grupo sem retenção guarda indefinidamente, e é a linha esquecida mais comum |
| Falha | O que a protege | O que ela NÃO protege |
|---|---|---|
| Requisição impossível de localizar | trace id em toda linha e no cabeçalho de resposta | evento escrito fora do escopo da requisição, que continua sem correlação |
| Trace partido entre serviços | propagador composto | serviço a jusante que não propaga nada — a cadeia quebra lá |
| Trace do erro ausente | amostragem por cauda com política de erro | trace mais longo que a janela de decisão, que é avaliado incompleto |
| Cegueira por falha do coletor | alarme sobre spans ingeridos | perda dos poucos segundos em trânsito quando a task morre |
| Explosão de custo de métrica | revisão da linha de dimensões | nada automático: não há proteção técnica contra dimensão nova mal escolhida |
| Alarme que não dispara nunca | prova que compara métrica com log | limiar mal escolhido, que é decisão e não defeito |
- O cliente pede uma página; o CloudFront identifica a requisição de borda.
- O ALB acrescenta ou atualiza `X-Amzn-Trace-Id` a caminho do alvo.
- O propagador composto adota o contexto que chegou, em vez de criar um novo.
- O rastreamento de atividade injeta TraceId e SpanId no escopo de todo evento de log.
- Cada chamada de saída vira um span filho com duração medida, e leva o contexto adiante.
- O sidecar recebe OTLP, decide por cauda o que amostrar e exporta em lote.
- O span vai para o X-Ray; a métrica sai embutida no evento de log e o CloudWatch a extrai.
- O middleware devolve o trace id ao cliente no cabeçalho da resposta.
- O alarme dispara sobre a métrica agregada e traz a consulta pronta no corpo do aviso.
- Duas consultas depois — uma por campo no log, uma no trace — o salto que falhou tem nome.
Desafio — sem roteiro
O requisito
Um terceiro serviço entrou na cadeia — o serviço de pedidos agora chama, além do de estoque, um serviço de precificação externo. Instrumente essa chamada nova para aparecer no MESMO traço.
Critério de aceite — executável, não "verifique se funciona"
Uma requisição de ponta a ponta produz um único trace ID no X-Ray com TRÊS spans visíveis — pedidos, estoque e precificação — e o mapa de serviço mostra as três caixas conectadas.
- Dica 1: O contexto de rastreamento (trace ID) precisa ser PROPAGADO no cabeçalho HTTP da chamada ao serviço de precificação — sem isso, ele abre um trace novo e desconectado.
- Dica 2: O SDK do X-Ray tem um middleware para instrumentar chamada HTTP de saída automaticamente — verifique se ele está registrado antes de instrumentar manualmente.
- Dica 3: Se o terceiro serviço estiver fora do seu controle (de verdade externo), ele não vai propagar o contexto de volta — o span dele aparece como uma "caixa preta" com duração, sem sub-spans, e isso é o resultado ESPERADO nesse caso.
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
❓ Qual header o ALB usa para rastrear requisições?
❓ Por que meu trace no X-Ray aparece quebrado em dois?
❓ Como colocar o trace id em todas as linhas de log no .NET 8?
❓ Qual é a taxa de amostragem padrão do X-Ray?
❓ Embedded Metric Format ou PutMetricData: qual é mais barato?
❓ Como encontrar todos os logs de uma requisição específica no CloudWatch Logs Insights?
❓ Log, métrica e trace: qual usar para quê?
❓ O X-Amz-Cf-Id do CloudFront serve como trace id?
Fixando
Você configura o exportador de métrica embutida do coletor com `dimensions: [[http.route, http.status_code, TraceId]]` e roda um teste de 10 mil requisições. O que acontece?
Um cliente reclama de erro numa requisição de ontem. Você tem amostragem no SDK com o padrão do X-Ray — a primeira requisição de cada segundo mais 5% das adicionais — e log estruturado correlacionado em 100%. O trace daquela requisição não existe. Qual mudança resolve o problema de raiz?
Conhecimentos, próximo módulo e documentação
| Item | Conteúdo |
|---|---|
| Conhecimentos anteriores necessários | L01 no ar (ECS Fargate, RDS em sub-rede privada, ALB, CloudFront), Terraform, .NET 8 e noção de cabeçalho HTTP |
| Conhecimentos adquiridos | de onde vem o identificador de correlação antes do seu código; por que o trace id tem de estar no log e não só no span; a diferença entre amostragem por cabeça e por cauda e o que cada uma permite perguntar depois; quando métrica derivada de log é mais barata que métrica customizada; e por que cardinalidade é o único termo multiplicativo da fatura de observabilidade |
| Limitação que fica | não há objetivo de nível de serviço, orçamento de erro nem política de acionamento: o alarme avisa e ninguém sabe se aquilo é aceitável. E a hipótese declarada de API síncrona sem fila continua valendo — com trabalho assíncrono, a propagação precisa entrar no cabeçalho da mensagem |
| Próximo exemplo recomendado | L51 — objetivo de nível de serviço e orçamento de erro. Ele consome exatamente a correlação construída aqui, porque medir a jornada do cliente exige ligar as camadas |
| Também habilitado por este módulo | L52 (correlacionar métrica de negócio com métrica técnica) depende do log estruturado com campo de negócio; e o L07 de teste de carga passa a poder medir onde o tempo vai, em vez de só quanto |
| Data da última validação técnica | 7 de agosto de 2026 |
Documentação oficial consultada: Request tracing for your Application Load Balancer — o nome do cabeçalho, o formato dos campos Root e Self, a preservação de campos próprios e os três limites (atualização só na entrada, reescrita acima de 7 KB de cabeçalhos, WebSocket só até o upgrade); AWS X-Ray concepts — a amostragem padrão de uma requisição por segundo mais 5% das adicionais, a distinção entre anotação indexada e metadado não indexado com o limite de 50 anotações por trace, a retenção de 30 dias e o limite de 64 kB por documento de segmento; CloudWatch Transaction Search — a ingestão de 100% dos spans como log estruturado com indexação de uma porcentagem configurável de 0 a 100 (1% por padrão), o suporte a identificador de trace em formato W3C além do formato X-Ray e o teto de 10 mil spans por trace; e Specification: embedded metric format — a estrutura do objeto _aws, o limite de 30 chaves por conjunto de dimensões e de 100 definições de métrica, o teto de 1 MB por evento e o aviso explícito de que cada conjunto de dimensões cria uma métrica nova, com dimensão de alta cardinalidade como caso a evitar. Nenhum preço absoluto aparece 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 no seu ambiente
Os volumes citados — 3,4 GB de log por dia, 500 mil requisições, p99 de 380 ms, 87 traces esperados na prova 3 — são da aplicação de exemplo e servem como ordem de grandeza, não como referência: derive os seus da sua medição. Os nomes dos pacotes NuGet das extensões AWS do OpenTelemetry mudaram de lugar entre versões, e por isso o módulo configura o propagador pela variável `OTEL_PROPAGATORS`, que é padronizada; confira o nome atual no NuGet antes de copiar qualquer `using`. A versão fixada da imagem do coletor também envelhece — confira a atual no repositório público antes de implantar, e continue fixando em vez de usar `:latest`, pelo mesmo motivo do L03. Por fim, a porcentagem gratuita de indexação da busca por transação é um valor de produto e pode mudar: confirme no console antes de dimensionar o orçamento em cima dela.
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…