Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion src/ingresso/Unifesspa.UniPlus.Ingresso.API/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using Unifesspa.UniPlus.Infrastructure.Core.Logging;
using Unifesspa.UniPlus.Infrastructure.Core.Messaging;
using Unifesspa.UniPlus.Infrastructure.Core.Middleware;
using Unifesspa.UniPlus.Infrastructure.Core.Observability;
using Unifesspa.UniPlus.Infrastructure.Core.Profile;
using Unifesspa.UniPlus.Infrastructure.Core.Smoke;
using Unifesspa.UniPlus.Ingresso.API.Errors;
Expand All @@ -15,8 +16,11 @@

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);

// service.name canônico — ver explicação em Selecao.API/Program.cs.
const string nomeServicoIngresso = "uniplus-ingresso";

builder.Host.UseSerilog((context, loggerConfig) =>
loggerConfig.ConfigurarSerilog(context.Configuration));
loggerConfig.ConfigurarSerilog(context.Configuration, nomeServicoIngresso));

builder.Services.AddControllers()
.AddJsonOptions(options =>
Expand All @@ -42,6 +46,10 @@
builder.Services.AddOidcAuthentication(builder.Configuration, builder.Environment);
builder.Services.AddCorrelationIdAccessor();
builder.Services.AddRequestLogging(builder.Configuration);

// Observabilidade (ADR-0018) — ver explicação em Selecao.API/Program.cs.
builder.Services.AdicionarObservabilidade(nomeServicoIngresso, builder.Configuration, builder.Environment);

// AddIngressoInfrastructure agora resolve a connection string via
// IConfiguration injetada no factory do AddDbContext (issue #204) —
// simetria com UseWolverineOutboxCascading e com Selecao.
Expand Down
3 changes: 3 additions & 0 deletions src/ingresso/Unifesspa.UniPlus.Ingresso.API/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@
"Authority": "",
"Audience": "uniplus"
},
"Observability": {
"Enabled": true
},
"Serilog": {
"Using": ["Serilog.Sinks.Console"],
"MinimumLevel": {
Expand Down
10 changes: 9 additions & 1 deletion src/portal/Unifesspa.UniPlus.Portal.API/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
using Unifesspa.UniPlus.Infrastructure.Core.Logging;
using Unifesspa.UniPlus.Infrastructure.Core.Messaging;
using Unifesspa.UniPlus.Infrastructure.Core.Middleware;
using Unifesspa.UniPlus.Infrastructure.Core.Observability;
using Unifesspa.UniPlus.Infrastructure.Core.Profile;
using Unifesspa.UniPlus.Infrastructure.Core.Smoke;
using Unifesspa.UniPlus.Portal.API.Errors;
Expand All @@ -17,8 +18,11 @@

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);

// service.name canônico — ver explicação em Selecao.API/Program.cs.
const string nomeServicoPortal = "uniplus-portal";

builder.Host.UseSerilog((context, loggerConfig) =>
loggerConfig.ConfigurarSerilog(context.Configuration));
loggerConfig.ConfigurarSerilog(context.Configuration, nomeServicoPortal));

builder.Services.AddControllers()
.AddJsonOptions(options =>
Expand Down Expand Up @@ -53,6 +57,10 @@
builder.Services.AddOidcAuthentication(builder.Configuration, builder.Environment);
builder.Services.AddCorrelationIdAccessor();
builder.Services.AddRequestLogging(builder.Configuration);

// Observabilidade (ADR-0018) — ver explicação em Selecao.API/Program.cs.
builder.Services.AdicionarObservabilidade(nomeServicoPortal, builder.Configuration, builder.Environment);

// AddPortalInfrastructure resolve a connection string via IConfiguration
// injetada no factory do AddDbContext — simetria com Selecao/Ingresso (#204).
builder.Services.AddPortalInfrastructure();
Expand Down
3 changes: 3 additions & 0 deletions src/portal/Unifesspa.UniPlus.Portal.API/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@
"Authority": "",
"Audience": "uniplus"
},
"Observability": {
"Enabled": true
},
"Serilog": {
"Using": ["Serilog.Sinks.Console"],
"MinimumLevel": {
Expand Down
16 changes: 15 additions & 1 deletion src/selecao/Unifesspa.UniPlus.Selecao.API/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
using Unifesspa.UniPlus.Infrastructure.Core.Messaging;
using Unifesspa.UniPlus.Infrastructure.Core.Messaging.SchemaRegistry;
using Unifesspa.UniPlus.Infrastructure.Core.Middleware;
using Unifesspa.UniPlus.Infrastructure.Core.Observability;
using Unifesspa.UniPlus.Infrastructure.Core.Profile;
using Unifesspa.UniPlus.Infrastructure.Core.Smoke;
using Unifesspa.UniPlus.Selecao.API.Errors;
Expand All @@ -28,8 +29,14 @@

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);

// service.name canônico para Resource OTel (tracing/metrics) e ResourceAttributes
// do sink OTLP do Serilog (logs). Mantido em const local para garantir igualdade
// estrita entre os 2 pipelines — drift de naming entre logs e traces seria a
// 1ª coisa a quebrar drill-down Loki↔Tempo no Grafana.
const string nomeServicoSelecao = "uniplus-selecao";

builder.Host.UseSerilog((context, loggerConfig) =>
loggerConfig.ConfigurarSerilog(context.Configuration));
loggerConfig.ConfigurarSerilog(context.Configuration, nomeServicoSelecao));

builder.Services.AddControllers()
.AddJsonOptions(options =>
Expand Down Expand Up @@ -71,6 +78,13 @@
builder.Services.AddOidcAuthentication(builder.Configuration, builder.Environment);
builder.Services.AddCorrelationIdAccessor();
builder.Services.AddRequestLogging(builder.Configuration);

// Observabilidade (ADR-0018) — tracing + metrics via OpenTelemetry SDK para o Collector
// institucional. Logs já fluem via Serilog OTLP sink configurado em UseSerilog acima.
// Toggle Observability:Enabled em appsettings; default true. Em test factories sem
// Collector provisionado, sobrescrever para false em InMemoryCollection.
builder.Services.AdicionarObservabilidade(nomeServicoSelecao, builder.Configuration, builder.Environment);

builder.Services.AddSelecaoApplication();
// AddSelecaoInfrastructure agora resolve a connection string via
// IConfiguration injetada no factory do AddDbContext (issue #204) —
Expand Down
3 changes: 3 additions & 0 deletions src/selecao/Unifesspa.UniPlus.Selecao.API/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@
"Authority": "",
"Audience": "uniplus"
},
"Observability": {
"Enabled": true
},
"Serilog": {
"Using": ["Serilog.Sinks.Console"],
"MinimumLevel": {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
namespace Unifesspa.UniPlus.Infrastructure.Core.IntegrationTests.Observability;

using System.Collections.Generic;
using System.Diagnostics;

using AwesomeAssertions;

using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Hosting;

using OpenTelemetry.Trace;

using Unifesspa.UniPlus.Infrastructure.Core.Observability;
using Unifesspa.UniPlus.IntegrationTests.Fixtures.Hosting;

[Collection(OtelCollectorContainerFixture.CollectionName)]
public sealed class OpenTelemetryWiringTests(OtelCollectorContainerFixture collector)
{
private const string ServiceName = "uniplus-test-otel-wiring";
private const string EnvVarName = "OTEL_EXPORTER_OTLP_ENDPOINT";

[Fact]
public async Task AdicionarObservabilidade_PipelineEndToEnd_ExportaSpanRotuladoParaCollector()
{
// O OtlpExporter padrão lê o endpoint da env var OTEL_EXPORTER_OTLP_ENDPOINT
// (sem opção de override via DI sem mudar a assinatura pública). Setamos
// process-wide e restauramos no finally — xUnit não paraleliza dentro da
// mesma collection, então não há race com outros testes.
string? endpointAnterior = Environment.GetEnvironmentVariable(EnvVarName);
try
{
Environment.SetEnvironmentVariable(EnvVarName, collector.GrpcEndpoint);

ServiceCollection services = new();
services.AddLogging();
IConfiguration configuration = new ConfigurationBuilder().Build();
IHostEnvironment environment = new TestHostEnvironment("Development");

services.AdicionarObservabilidade(ServiceName, configuration, environment);

await using ServiceProvider provider = services.BuildServiceProvider();

// Resolver o TracerProvider força o startup do pipeline OTel — exporter
// OTLP abre a conexão gRPC com o Collector neste ponto.
TracerProvider? tracerProvider = provider.GetService<TracerProvider>();
tracerProvider.Should().NotBeNull("AdicionarObservabilidade deve registrar um TracerProvider quando Observability:Enabled é o default true");

// ActivitySource compartilha o nome do serviço — registrado em
// AddSource(nomeServico) na pipeline. Sem isso, listeners do OTel
// SDK ignoram os spans emitidos.
using ActivitySource source = new(ServiceName);
using (Activity? span = source.StartActivity("test-wiring-span"))
{
span.Should().NotBeNull("o sampler em Development é AlwaysOn — span não pode ser dropado");
span!.SetTag("test.scenario", "wiring-end-to-end");
}

// ForceFlush garante que o batch processor envia imediatamente em vez
// de esperar o intervalo (5s default). Timeout 5s é margem para gRPC
// handshake + envio em runners CI mais lentos.
tracerProvider!.ForceFlush(timeoutMilliseconds: 5_000).Should().BeTrue();

// Buffer extra (200ms) para o Collector processar o batch recebido e
// emitir no exporter debug. Heurística: na prática 50ms basta, 200ms
// dá folga sem aumentar tempo do teste materialmente.
await Task.Delay(TimeSpan.FromMilliseconds(200));

string collectorOutput = await collector.GetLogsAsync();

collectorOutput.Should().Contain("ResourceSpans", because: "o exporter debug do Collector escreve o nome do tipo OTLP recebido em stderr");
collectorOutput.Should().Contain(ServiceName, because: $"o Resource attribute service.name precisa chegar até o Collector — confirma que ConfigureResource(...AddService(\"{ServiceName}\")) está wired");
collectorOutput.Should().Contain("test-wiring-span", because: "o nome do span emitido localmente precisa aparecer no output do Collector — confirma que AddOtlpExporter() está exportando de fato");
collectorOutput.Should().Contain(OpenTelemetryConfiguration.ServiceNamespaceResourceValue, because: "service.namespace=uniplus deve chegar no Resource exportado");
}
finally
{
Environment.SetEnvironmentVariable(EnvVarName, endpointAnterior);
}
}

/// <summary>
/// Stub minimal de <see cref="IHostEnvironment"/> — só popula
/// <c>EnvironmentName</c> (única propriedade lida por
/// <see cref="OpenTelemetryConfiguration.SelecionarSampler"/> e
/// <see cref="OpenTelemetryConfiguration.AdicionarObservabilidade"/>).
/// Evita NSubstitute aqui porque o test project não referencia o pacote
/// (kept lean — IntegrationTests usa Testcontainers + AwesomeAssertions
/// e nada além).
/// </summary>
private sealed class TestHostEnvironment(string environmentName) : IHostEnvironment
{
public string EnvironmentName { get; set; } = environmentName;

public string ApplicationName { get; set; } = "Unifesspa.UniPlus.Infrastructure.Core.IntegrationTests";

public string ContentRootPath { get; set; } = AppContext.BaseDirectory;

public IFileProvider ContentRootFileProvider { get; set; } = new NullFileProvider();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
namespace Unifesspa.UniPlus.Infrastructure.Core.IntegrationTests.Observability;

using System.Diagnostics.CodeAnalysis;

using Unifesspa.UniPlus.IntegrationTests.Fixtures.Hosting;

[CollectionDefinition(OtelCollectorContainerFixture.CollectionName)]
[SuppressMessage(
"Naming",
"CA1711:Identifiers should not have incorrect suffix",
Justification = "Convenção xUnit: o sufixo 'Collection' identifica a classe como CollectionDefinition.")]
[SuppressMessage(
"Performance",
"CA1515:Consider making public types internal",
Justification = "xUnit 2.x exige que CollectionDefinitions sejam públicas para que o runner as descubra.")]
public sealed class OtelCollectorCollection : ICollectionFixture<OtelCollectorContainerFixture>
{
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,37 @@ namespace Unifesspa.UniPlus.IntegrationTests.Fixtures.Hosting;
public abstract class ApiFactoryBase<TEntryPoint> : WebApplicationFactory<TEntryPoint>
where TEntryPoint : class
{
/// <summary>
/// Static initializer que silencia o pipeline OpenTelemetry para todas as suites
/// HTTP-only que usam <see cref="ApiFactoryBase{T}"/>.
/// </summary>
/// <remarks>
/// <para><strong>Por que env var em vez de <c>ConfigureAppConfiguration</c>?</strong>
/// Em minimal hosting (<c>WebApplication.CreateBuilder(args)</c>), o
/// <c>Program.cs</c> lê <c>builder.Configuration</c> e chama
/// <c>AdicionarObservabilidade</c>/<c>ConfigurarSerilog</c> ANTES de
/// <c>builder.Build()</c> rodar — momento em que o
/// <c>ConfigureAppConfiguration</c> do <see cref="WebApplicationFactory{T}"/>
/// é aplicado. Override via <c>AddInMemoryCollection</c> chega tarde demais
/// e o exporter OTLP fica tentando <c>localhost:4317</c> em loop.</para>
/// <para>Env var (<c>Observability__Enabled=false</c>) é lida pelo
/// <c>EnvironmentVariablesConfigurationProvider</c> default do
/// <c>CreateBuilder</c> — chega A TEMPO da leitura no <c>Program.cs</c>.</para>
/// <para><strong>Process-wide:</strong> setar env var afeta o processo inteiro.
/// Aceitável aqui porque (a) é idempotente, (b) o único test no projeto que
/// exercita observabilidade real
/// (<c>OpenTelemetryWiringTests</c>) constrói <see cref="IServiceCollection"/>
/// manualmente sem <c>CreateBuilder</c>, então não é afetado pela env var, e
/// (c) o <c>OpenTelemetryWiringTests</c> também sobrescreve
/// <c>OTEL_EXPORTER_OTLP_ENDPOINT</c> via <c>try/finally</c>, demonstrando o
/// padrão para testes que queiram OTel ligado.</para>
/// </remarks>
static ApiFactoryBase()
{
Environment.SetEnvironmentVariable("Observability__Enabled", "false");
}


/// <summary>
/// Quando <c>true</c> (default), o factory remove o
/// <see cref="WolverineRuntime"/> da lista de <see cref="IHostedService"/>
Expand Down Expand Up @@ -81,6 +112,10 @@ protected override void ConfigureWebHost(IWebHostBuilder builder)

builder.ConfigureAppConfiguration((_, configurationBuilder) =>
{
// Observability:Enabled=false é injetado via env var no static ctor
// desta classe (ver explicação no XML doc) — ConfigureAppConfiguration
// roda em builder.Build() e seria tarde demais para o Program.cs ler
// antes de chamar AdicionarObservabilidade.
configurationBuilder.AddInMemoryCollection(GetConfigurationOverrides());
});

Expand Down
Loading
Loading