feat(observability): wire OpenTelemetry nos 3 Program.cs + integration test ponta-a-ponta#368
Conversation
…n test ponta-a-ponta PR 3 (final) da entrega da Story #30. Wire concreto da extension method expandida no PR #367 nos 3 APIs do projeto + integration test com Testcontainers do OTEL Collector validando o pipeline OTLP gRPC end-to-end. Wire em Program.cs (Selecao + Ingresso + Portal) - builder.Services.AdicionarObservabilidade(nomeServico, config, env) registrado após AddRequestLogging em cada API - nomeServico declarado como const local (uniplus-{modulo}) para garantir igualdade estrita entre o Resource OTel SDK e o ResourceAttributes do sink OTLP do Serilog — drift de naming entre logs/traces seria a primeira coisa a quebrar drill-down Loki↔Tempo no Grafana - builder.Host.UseSerilog estendido para passar nomeServico à sobrecarga rotulada de ConfigurarSerilog (PR #367) 3 appsettings.json - Section Observability:Enabled=true (default seguro pra produção) - OTEL_EXPORTER_OTLP_ENDPOINT é env var standard do OTel SDK (NÃO IConfiguration); configurado via env vars do container/runtime, não em appsettings ApiFactoryBase - AddInMemoryCollection com Observability:Enabled=false ANTES dos GetConfigurationOverrides — suites HTTP-only sem Collector OTel provisionado silenciam o exporter, evitando ruído de drop batches/retry no CI - Override segue padrão de InfraHealthCheckNamesToRemoveForTests OtelCollectorContainerFixture (Tests.Fixtures/Hosting) - Sobe otel/opentelemetry-collector-contrib:0.117.0 com config minimal (receivers OTLP gRPC+HTTP, processor batch 100ms, exporter debug verbosity=detailed) - Config YAML inline via WithResourceMapping (sem arquivo temp em disco) - Collection compartilhada "OtelCollector" — pattern análogo a MinIO/Vault/Keycloak - GetLogsAsync() concatena stdout+stderr (Collector loga exporter debug em stderr) OpenTelemetryWiringTests - 1 teste end-to-end: AdicionarObservabilidade → ActivitySource → OtlpExporter gRPC → Collector real → exporter debug → asserts em ResourceSpans + service.name + service.namespace + nome do span emitido - Set/restore de OTEL_EXPORTER_OTLP_ENDPOINT via try/finally — xUnit não paraleliza dentro da mesma collection, sem race com outros testes - TestHostEnvironment stub local (sem NSubstitute — IntegrationTests project mantém deps lean: só Testcontainers + AwesomeAssertions) Build: 0 warning, 0 error. Suite COMPLETA: 629 testes passando, 0 falhas. Refs #30 Refs #105 Refs #110
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: b7cbedbe0b
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
jf2s
left a comment
There was a problem hiding this comment.
Revisão: PR #368 — feat(observability): wire OpenTelemetry nos 3 Program.cs + integration test ponta-a-ponta
Tipo de PR: wire final + integration test. Mudanças mecânicas em 3 Program.cs + 3 appsettings.json, override defensivo em ApiFactoryBase, fixture nova de Testcontainers OTEL Collector + 1 teste end-to-end fechando o ciclo da Story #30.
Diff: 284 insertions, 3 deletions em 10 arquivos.
Resumo
PR final da entrega da Story #30. Mudanças bem isoladas: cada Program.cs ganha 1 const + 1 chamada a AdicionarObservabilidade + 1 ajuste em UseSerilog. Cada appsettings.json ganha section Observability:Enabled=true. Fixture do OTEL Collector segue o pattern já estabelecido por MinIO/Vault/Keycloak fixtures. Integration test ponta-a-ponta valida ServiceCollection → AdicionarObservabilidade → ActivitySource → OtlpExporter gRPC → Collector real → exporter debug. Suite COMPLETA local: 629 testes verdes (incluindo o novo OTel em ~3.3s). CI 5/5 verde, build/test em 3m24s.
Findings
Bloqueantes (0)
Nenhum. Wire mecânico, sem mudança em runtime para testes que sobem Program.cs (ApiFactoryBase override Observability:Enabled=false por default).
Importantes (0)
Nenhum. Decisões arquiteturais bem documentadas; pattern reutiliza fixtures existentes; testes locais inclusive integration verde end-to-end.
Sugestões (3)
[S1] Tag de imagem do Collector pinada em 0.117.0 — gap pré-existente que Renovate (issue #366) vai resolver
OtelCollectorContainerFixture.Image = "otel/opentelemetry-collector-contrib:0.117.0". Tag específica é correta (reproduzibilidade), mas sem Renovate configurado no repo, ninguém vai notar quando uma versão nova com fix de CVE sair. Já registrado na issue #366 (Renovate follow-up criada no PR #365); aceitável continuar até #366 ser executada.
[S2] await Task.Delay(200ms) no integration test é heurística, não polling
tracerProvider!.ForceFlush(timeoutMilliseconds: 5_000).Should().BeTrue();
await Task.Delay(TimeSpan.FromMilliseconds(200));
string collectorOutput = await collector.GetLogsAsync();ForceFlush garante que o exporter enviou; mas o Collector ainda precisa processar o batch + escrever no stderr. 200ms é margem empírica. Mais robusto seria uma loop com timeout — algo como:
string collectorOutput = await PollUntilContainsAsync(
() => collector.GetLogsAsync(),
expected: "test-wiring-span",
timeout: TimeSpan.FromSeconds(5),
interval: TimeSpan.FromMilliseconds(50));Trade-off: em runners CI lentos, 200ms pode flakar. Vale considerar polling se o teste começar a falhar intermitentemente; no estado atual passou consistentemente em 3.3s end-to-end. Aceitável manter como está e revisitar se houver flake reportado.
[S3] Assertion de ServiceNamespaceResourceValue (uniplus) é match substring genérico
collectorOutput.Should().Contain(OpenTelemetryConfiguration.ServiceNamespaceResourceValue);"uniplus" é palavra suficientemente única no output do Collector debug; em conjunto com as outras 3 asserções (ResourceSpans, ServiceName, test-wiring-span) dá alta confiança. Em isolamento, qualquer log mencionando "uniplus-test-otel-wiring" também faria match. Aceitável dado o conjunto de asserções.
Pontos positivos
- Const local
nomeServicoem vez de string repetida —nomeServicoSelecao = \"uniplus-selecao\"declarada no topo de cadaProgram.cs, reutilizada emUseSeriloge emAdicionarObservabilidade. Garante igualdade estrita entre Resource OTel SDK e ResourceAttributes do sink Serilog (drift entre logs/traces seria a 1ª coisa a quebrar drill-down Loki↔Tempo) - Override defensivo em ApiFactoryBase com comment explícito do trade-off — coloca
Observability:Enabled=falseANTES dosGetConfigurationOverridese explica que suites HTTP-only sem Collector ficariam com ruído de drop batches. Padrão alinhado comInfraHealthCheckNamesToRemoveForTests(§ 23.5 da skillissue-driven-implementation) - Fixture do Collector segue pattern Minio/Vault/Keycloak — naming convention
{Nome}ContainerFixture,IAsyncLifetime,CollectionNameconst,WithWaitStrategyhonesto (UntilMessageIsLogged(\"Everything is ready...\")), tag pinada com comentário sobre alinhamento com Collector institucional - Config YAML inline via
WithResourceMapping— sem arquivo temp em disco, sem cleanup pendente, autocontido na fixture GetLogsAsync()com nome generalista + comentário sobre stdout vs stderr — quando descobri que Collector loga em stderr, em vez de hardcodar isso, refatorei a fixture para concatenar ambos (resiliente a mudanças futuras de qual stream o Collector emite)- TestHostEnvironment stub local em vez de NSubstitute — IntegrationTests project mantém deps lean (só Testcontainers + AwesomeAssertions); stub é 4 linhas com defaults sensatos (
AppContext.BaseDirectory,NullFileProvider) - Integration test cobre ponta-a-ponta o que unit tests não conseguem — passou em 3.3s incluindo subida do Collector, exportação real OTLP gRPC e validação no stderr. Nenhum mock no caminho crítico
- Set/restore de env var
OTEL_EXPORTER_OTLP_ENDPOINTvia try/finally — process-wide é OK pois xUnit não paraleliza dentro da mesma collection ([Collection(\"OtelCollector\")]). Restauração via finally protege contra side effect em tests que rodem na sequência
Veredicto
APROVADO
3 sugestões cosméticas/operacionais, nenhum finding importante ou bloqueante. Wire é mecânico e bem documentado, integration test fecha o ciclo da Story de fundação. Pode mergear para fechar #30, #105 e #110 de uma vez.
… ConfigureAppConfiguration) Aplica feedback Codex P2 do PR #368. Problema: o override Observability:Enabled=false via ConfigureAppConfiguration do WebApplicationFactory chega tarde demais. Em minimal hosting (WebApplication.CreateBuilder(args)), o Program.cs lê builder.Configuration e chama AdicionarObservabilidade/ConfigurarSerilog ANTES de builder.Build() rodar — momento em que o ConfigureAppConfiguration do factory é aplicado. O exporter OTLP fica tentando localhost:4317 em loop nas suites HTTP-only sem Collector provisionado, exatamente o ruído que o override pretendia evitar. Fix: setar env var Observability__Enabled=false no static ctor de ApiFactoryBase. EnvironmentVariablesConfigurationProvider é registrado por default no CreateBuilder e roda imediatamente — Program.cs vê o valor false quando lê builder.Configuration["Observability:Enabled"]. Process-wide aceitável porque (a) idempotente, (b) único test que exercita observabilidade real (OpenTelemetryWiringTests) constrói ServiceCollection manualmente sem CreateBuilder, então não é afetado, (c) esse mesmo test sobrescreve OTEL_EXPORTER_OTLP_ENDPOINT via try/finally, demonstrando o padrão para suites que queiram OTel ligado. Bloco ConfigureAppConfiguration mantido para os GetConfigurationOverrides das subclasses (Postgres connection string etc.); apenas o override ineficaz de Observability:Enabled foi removido com comment explicando. Build: 0 warning, 0 error. Suite COMPLETA: 629 testes passando, 0 falhas. Refs #30
jf2s
left a comment
There was a problem hiding this comment.
Aprovado.
Findings de review aplicados:
- [Codex P2]
Observability:Enabled=falseagora via env var no static ctor deApiFactoryBase(commit 8c2c41c).ConfigureAppConfigurationchegava apósProgram.cslerbuilder.Configurationem minimal hosting;EnvironmentVariablesConfigurationProviderdefault chega a tempo. Thread inline resolvido. - [S1] Renovate — gap pré-existente já tracked em #366.
- [S2] Polling vs Task.Delay 200ms — aceitável até teste flakar; revisitar se acontecer.
- [S3] Match substring "uniplus" — alta confiança em conjunto com as outras 3 asserções.
CI 5/5 verde após fix. Suite COMPLETA local: 629 testes passando, 0 warning, 0 error. Pode mergear para fechar #30, #105 e #110 de uma vez.
Resumo
PR 3 (final) da entrega da Story #30. Wire concreto da extension method expandida no PR #367 (
AdicionarObservabilidade) nos 3 APIs do projeto (Selecao + Ingresso + Portal) + integration test com Testcontainers do OTEL Collector validando o pipeline OTLP gRPC ponta-a-ponta. Fecha definitivamente #30, #105 e #110.Fluxo da entrega Story #30
chore(shared): declarar pacotes NuGet OpenTelemetryfeat(observability): expandir AdicionarObservabilidade + OTLP sink Serilog + correlation_id span tagfeat(observability): wire nos 3 Program.cs + integration testMudanças
Wire em Program.cs (Selecao + Ingresso + Portal)
builder.Services.AdicionarObservabilidade(nomeServico, config, env)registrado apósAddRequestLoggingem cada APInomeServicodeclarado comoconstlocal (uniplus-{modulo}) para garantir igualdade estrita entre o Resource OTel SDK e oResourceAttributesdo sink OTLP do Serilog — drift de naming entre logs/traces seria a primeira coisa a quebrar drill-down Loki↔Tempo no Grafanabuilder.Host.UseSerilogestendido para passarnomeServicoà sobrecarga rotulada deConfigurarSerilog(introduzida no PR feat(observability): expandir AdicionarObservabilidade + OTLP sink Serilog + correlation_id span tag #367)3
appsettings.jsonObservability:Enabled=true(default seguro pra produção)OTEL_EXPORTER_OTLP_ENDPOINTé env var standard do OTel SDK (NÃOIConfiguration); configurado via env vars do container/runtime, não em appsettingsApiFactoryBase(Tests.Fixtures)AddInMemoryCollectioncomObservability:Enabled=falseANTES dosGetConfigurationOverrides— suites HTTP-only sem Collector OTel provisionado silenciam o exporter, evitando ruído de drop batches/retry no CIInfraHealthCheckNamesToRemoveForTests(memóriaboot-time changesda skill)OtelCollectorContainerFixture(Tests.Fixtures/Hosting) — novootel/opentelemetry-collector-contrib:0.117.0com config minimal (receivers OTLP gRPC+HTTP, processor batch 100ms, exporterdebugverbosity=detailed)WithResourceMapping(sem arquivo temp em disco)\"OtelCollector\"— pattern análogo a MinIO/Vault/Keycloak já estabelecido no projetoGetLogsAsync()concatenastdout+stderr— Collector loga exporterdebugem stderr (descoberta via teste primeiro com stdout vazio)OpenTelemetryWiringTests(Infrastructure.Core.IntegrationTests/Observability) — novoAdicionarObservabilidade→ActivitySource→OtlpExportergRPC → Collector real → exporterdebug→ asserts emResourceSpans+service.name+service.namespace+ nome do span emitidoOTEL_EXPORTER_OTLP_ENDPOINTviatry/finally— xUnit não paraleliza dentro da mesma collection, sem race com outros testesTestHostEnvironmentstub local (sem NSubstitute — IntegrationTests project mantém deps lean: só Testcontainers + AwesomeAssertions)Validação local
```bash
dotnet build UniPlus.slnx # 0 warning, 0 error
dotnet test UniPlus.slnx # 629 testes verdes, 0 falhas
# - 540 unit + arch (~3s)
# - 8 Ingresso.IntegrationTests
# - 10 Infrastructure.Core.IntegrationTests (inclui OTel ponta-a-ponta — 3.3s)
# - 71 Selecao.IntegrationTests (~1m)
```
ADRs respeitadas
PiiMaskingEnrichercontinua antes do sink OTLP do Serilog (ordem natural). CPF mascarado antes de qualquer egressDecisões deferidas
/health/observabilityvalidando exporter OTLP — fora do escopo da Story story(observability): wiring de OpenTelemetry nos Program.cs #30; abrir issue dedicada se a operação pedirtail_sampling_processorno Collector, não da APIIssues fechadas
Closes #30
Closes #105
Closes #110