Files
lmxopcua/tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests/ObservabilityAllowlistTests.cs
T
Joseph Doherty 0b90379f70 fix(telemetry): export the Galaxy driver's spans and galaxy.events.* counters (#892)
ZbTelemetryOptions.Meters/ActivitySources are strict allowlists. The host named only
ZB.MOM.WW.OtOpcUa and the LocalDb meter, so the in-process Galaxy driver's galaxy.* spans
and its event pump's galaxy.events.* counters (both on "ZB.MOM.WW.OtOpcUa.Driver.Galaxy")
were silently dropped from /metrics and OTLP. Found exercising OTLP end to end against a
collector (scadaproj#13).

- Galaxy driver: public GalaxyDriverTelemetryNames (MeterName / ActivitySourceName);
  GalaxyTelemetry and EventPump reference it.
- Host: ObservabilityExtensions.ObservedMeters / ObservedActivitySources include it.
- Host.IntegrationTests: ObservabilityAllowlistTests assert on the listener side (span
  sampled, instrument enabled) with an unregistered-name negative control; dropping the
  Galaxy entries fails 3 of 8.

Claude-Session: https://claude.ai/code/session_01GT67ACmpMR7b9LGRPLijMi
2026-11-12 20:31:14 -05:00

88 lines
3.6 KiB
C#

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;
/// <summary>
/// scadaproj#13 — <c>ZbTelemetryOptions.Meters</c> / <c>.ActivitySources</c> are strict allowlists,
/// so a meter or activity source the host forgets to name is silently never exported (the Galaxy
/// driver's <c>galaxy.*</c> spans and <c>galaxy.events.*</c> counters were). These tests build the
/// real <see cref="ObservabilityExtensions.AddOtOpcUaObservability"/> 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.
/// </summary>
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<TracerProvider>();
_app.Services.GetRequiredService<MeterProvider>();
}
[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<long>("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<long>("allowlist.probe").Enabled.ShouldBeFalse();
}
public async ValueTask DisposeAsync() => await _app.DisposeAsync();
}