using System.Diagnostics; using System.Diagnostics.Metrics; using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; using OpenTelemetry.Metrics; using OpenTelemetry.Trace; using Shouldly; using Xunit; using ZB.MOM.WW.LocalDb.Replication; using ZB.MOM.WW.OtOpcUa.Commons.Observability; using ZB.MOM.WW.OtOpcUa.Driver.Galaxy; using ZB.MOM.WW.OtOpcUa.Host.Observability; namespace ZB.MOM.WW.OtOpcUa.Host.IntegrationTests; /// /// scadaproj#13 — ZbTelemetryOptions.Meters / .ActivitySources are strict allowlists, /// so a meter or activity source the host forgets to name is silently never exported (the Galaxy /// driver's galaxy.* spans and galaxy.events.* counters were). These tests build the /// real container, start its /// providers, and assert on the listener side: a span from a named source is sampled, and an /// instrument on a named meter is enabled. An unregistered name is the negative control proving /// the assertion can fail. /// public sealed class ObservabilityAllowlistTests : IAsyncDisposable { private readonly WebApplication _app; public ObservabilityAllowlistTests() { var builder = WebApplication.CreateBuilder(new WebApplicationOptions { Args = [] }); builder.Services.AddOtOpcUaObservability(builder.Configuration); _app = builder.Build(); // Resolving the providers is what attaches their ActivityListener / MeterListener. _app.Services.GetRequiredService(); _app.Services.GetRequiredService(); } [Fact] public void ObservedLists_NameTheGalaxyDriverMeterAndSource() { ObservabilityExtensions.ObservedMeters.ShouldBe( [OtOpcUaTelemetry.MeterName, LocalDbMetrics.MeterName, GalaxyDriverTelemetryNames.MeterName], ignoreOrder: true); ObservabilityExtensions.ObservedActivitySources.ShouldBe( [OtOpcUaTelemetry.ActivitySourceName, GalaxyDriverTelemetryNames.ActivitySourceName], ignoreOrder: true); } [Theory] [InlineData(OtOpcUaTelemetry.ActivitySourceName)] [InlineData(GalaxyDriverTelemetryNames.ActivitySourceName)] public void SpanFromAnObservedSource_IsRecorded(string sourceName) { using var source = new ActivitySource(sourceName); using var activity = source.StartActivity("allowlist-probe"); activity.ShouldNotBeNull(); activity.IsAllDataRequested.ShouldBeTrue(); } [Fact] public void SpanFromAnUnregisteredSource_IsDropped_NegativeControl() { using var source = new ActivitySource("ZB.MOM.WW.OtOpcUa.Unregistered." + Guid.NewGuid().ToString("N")); source.StartActivity("allowlist-probe").ShouldBeNull(); } [Theory] [InlineData(OtOpcUaTelemetry.MeterName)] [InlineData(LocalDbMetrics.MeterName)] [InlineData(GalaxyDriverTelemetryNames.MeterName)] public void InstrumentOnAnObservedMeter_IsEnabled(string meterName) { using var meter = new Meter(meterName); var counter = meter.CreateCounter("allowlist.probe." + Guid.NewGuid().ToString("N")); counter.Enabled.ShouldBeTrue(); } [Fact] public void InstrumentOnAnUnregisteredMeter_IsNotEnabled_NegativeControl() { using var meter = new Meter("ZB.MOM.WW.OtOpcUa.Unregistered." + Guid.NewGuid().ToString("N")); meter.CreateCounter("allowlist.probe").Enabled.ShouldBeFalse(); } public async ValueTask DisposeAsync() => await _app.DisposeAsync(); }