Skip to content

feat(observability): wire OpenTelemetry nos 3 Program.cs + integration test ponta-a-ponta#368

Merged
marmota-alpina merged 2 commits into
mainfrom
feat/30-otel-wire
May 10, 2026
Merged

feat(observability): wire OpenTelemetry nos 3 Program.cs + integration test ponta-a-ponta#368
marmota-alpina merged 2 commits into
mainfrom
feat/30-otel-wire

Conversation

@marmota-alpina
Copy link
Copy Markdown
Contributor

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

PR Escopo Status
#365 chore(shared): declarar pacotes NuGet OpenTelemetry ✅ mergeado
#367 feat(observability): expandir AdicionarObservabilidade + OTLP sink Serilog + correlation_id span tag ✅ mergeado
Este feat(observability): wire nos 3 Program.cs + integration test 🔄 review

Mudanças

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 (introduzida no PR feat(observability): expandir AdicionarObservabilidade + OTLP sink Serilog + correlation_id span tag #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 (Tests.Fixtures)

  • 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 (memória boot-time changes da skill)

OtelCollectorContainerFixture (Tests.Fixtures/Hosting) — novo

  • 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 já estabelecido no projeto
  • GetLogsAsync() concatena stdout+stderr — Collector loga exporter debug em stderr (descoberta via teste primeiro com stdout vazio)

OpenTelemetryWiringTests (Infrastructure.Core.IntegrationTests/Observability) — novo

  • 1 teste end-to-end: AdicionarObservabilidadeActivitySourceOtlpExporter 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)

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

Decisões deferidas

  • Health check /health/observability validando exporter OTLP — fora do escopo da Story story(observability): wiring de OpenTelemetry nos Program.cs #30; abrir issue dedicada se a operação pedir
  • Métricas custom de negócio (Kafka consumer lag, classificação, homologação) — entregam junto com cada feature owner (não é responsabilidade desta Story de fundação)
  • Tail-based sampling adaptativo — responsabilidade do tail_sampling_processor no Collector, não da API

Issues fechadas

Closes #30
Closes #105
Closes #110

…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
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread tests/Unifesspa.UniPlus.IntegrationTests.Fixtures/Hosting/ApiFactoryBase.cs Outdated
Copy link
Copy Markdown
Contributor

@jf2s jf2s left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Revisão: PR #368feat(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 nomeServico em vez de string repetidanomeServicoSelecao = \"uniplus-selecao\" declarada no topo de cada Program.cs, reutilizada em UseSerilog e em AdicionarObservabilidade. 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=false ANTES dos GetConfigurationOverrides e explica que suites HTTP-only sem Collector ficariam com ruído de drop batches. Padrão alinhado com InfraHealthCheckNamesToRemoveForTests (§ 23.5 da skill issue-driven-implementation)
  • Fixture do Collector segue pattern Minio/Vault/Keycloak — naming convention {Nome}ContainerFixture, IAsyncLifetime, CollectionName const, WithWaitStrategy honesto (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_ENDPOINT via 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
Copy link
Copy Markdown
Contributor

@jf2s jf2s left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aprovado.

Findings de review aplicados:

  • [Codex P2] Observability:Enabled=false agora via env var no static ctor de ApiFactoryBase (commit 8c2c41c). ConfigureAppConfiguration chegava após Program.cs ler builder.Configuration em minimal hosting; EnvironmentVariablesConfigurationProvider default 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.

@marmota-alpina marmota-alpina merged commit 064022a into main May 10, 2026
5 checks passed
@marmota-alpina marmota-alpina deleted the feat/30-otel-wire branch May 10, 2026 19:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants