Merge #892 export the Galaxy driver's spans and galaxy.events.* counters
v2-ci / unit-tests (push) Has been cancelled
v2-ci / unit-tests (push) Has been cancelled
Claude-Session: https://claude.ai/code/session_01GT67ACmpMR7b9LGRPLijMi
This commit is contained in:
@@ -0,0 +1,17 @@
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy;
|
||||
|
||||
/// <summary>
|
||||
/// The Galaxy driver's OpenTelemetry instrument names, public so the host can hand them to
|
||||
/// <c>AddZbTelemetry</c>. <c>ZbTelemetryOptions.Meters</c> and <c>.ActivitySources</c> are
|
||||
/// STRICT allow-lists: a name missing there is silently never exported — the driver's
|
||||
/// <c>galaxy.*</c> spans and <c>galaxy.events.*</c> counters were dropped this way until
|
||||
/// scadaproj#13 exercised the OTLP path end to end.
|
||||
/// </summary>
|
||||
public static class GalaxyDriverTelemetryNames
|
||||
{
|
||||
/// <summary>The meter the event pump's <c>galaxy.events.*</c> counters are created on.</summary>
|
||||
public const string MeterName = "ZB.MOM.WW.OtOpcUa.Driver.Galaxy";
|
||||
|
||||
/// <summary>The activity source the traced gateway decorators open <c>galaxy.*</c> spans on.</summary>
|
||||
public const string ActivitySourceName = "ZB.MOM.WW.OtOpcUa.Driver.Galaxy";
|
||||
}
|
||||
@@ -34,7 +34,7 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Runtime;
|
||||
/// </remarks>
|
||||
internal sealed class EventPump : IAsyncDisposable
|
||||
{
|
||||
public const string MeterName = "ZB.MOM.WW.OtOpcUa.Driver.Galaxy";
|
||||
public const string MeterName = GalaxyDriverTelemetryNames.MeterName;
|
||||
private const int DefaultChannelCapacity = 50_000;
|
||||
|
||||
// Single static meter so a host-level MeterListener catches all pump instances.
|
||||
|
||||
@@ -17,7 +17,7 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Runtime;
|
||||
/// </summary>
|
||||
internal static class GalaxyTelemetry
|
||||
{
|
||||
public const string ActivitySourceName = "ZB.MOM.WW.OtOpcUa.Driver.Galaxy";
|
||||
public const string ActivitySourceName = GalaxyDriverTelemetryNames.ActivitySourceName;
|
||||
|
||||
public static readonly ActivitySource ActivitySource = new(ActivitySourceName);
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using ZB.MOM.WW.LocalDb.Replication;
|
||||
using ZB.MOM.WW.OtOpcUa.Commons.Observability;
|
||||
using ZB.MOM.WW.OtOpcUa.Driver.Galaxy;
|
||||
using ZB.MOM.WW.Telemetry;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Host.Observability;
|
||||
@@ -14,6 +15,31 @@ namespace ZB.MOM.WW.OtOpcUa.Host.Observability;
|
||||
/// </summary>
|
||||
public static class ObservabilityExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Every meter the host exports. <c>ZbTelemetryOptions.Meters</c> is a STRICT allowlist (no
|
||||
/// wildcard): the LocalDb replication meter publishes its <c>localdb.sync.*</c> /
|
||||
/// <c>localdb.oplog.depth</c> series on driver nodes, and the in-process Galaxy driver its
|
||||
/// <c>galaxy.events.*</c> counters — each is silently absent from <c>/metrics</c> and OTLP unless
|
||||
/// named here. Adding a meter anywhere in the product means adding it here.
|
||||
/// </summary>
|
||||
public static readonly string[] ObservedMeters =
|
||||
[
|
||||
OtOpcUaTelemetry.MeterName,
|
||||
LocalDbMetrics.MeterName,
|
||||
GalaxyDriverTelemetryNames.MeterName,
|
||||
];
|
||||
|
||||
/// <summary>
|
||||
/// Every activity source the host exports over OTLP. Also a strict allowlist: the Galaxy
|
||||
/// driver's <c>galaxy.*</c> gateway-call spans were dropped until scadaproj#13 because its source
|
||||
/// was missing here.
|
||||
/// </summary>
|
||||
public static readonly string[] ObservedActivitySources =
|
||||
[
|
||||
OtOpcUaTelemetry.ActivitySourceName,
|
||||
GalaxyDriverTelemetryNames.ActivitySourceName,
|
||||
];
|
||||
|
||||
/// <summary>Adds OtOpcUa observability (metrics and tracing) to the service collection.</summary>
|
||||
/// <param name="services">The service collection to add observability services to.</param>
|
||||
/// <param name="configuration">
|
||||
@@ -28,12 +54,9 @@ public static class ObservabilityExtensions
|
||||
return services.AddZbTelemetry(o =>
|
||||
{
|
||||
o.ServiceName = "otopcua";
|
||||
// o.Meters is a STRICT allowlist — ZbTelemetry adds only the named meters, with no
|
||||
// wildcard. The LocalDb replication engine publishes under its own meter name, so
|
||||
// without this entry its localdb.sync.* / localdb.oplog.depth series are silently absent
|
||||
// from /metrics on driver nodes (the exact omission a ScadaBridge live gate caught).
|
||||
o.Meters = [OtOpcUaTelemetry.MeterName, LocalDbMetrics.MeterName];
|
||||
o.ActivitySources = [OtOpcUaTelemetry.ActivitySourceName];
|
||||
// Strict allowlists — see ObservedMeters / ObservedActivitySources.
|
||||
o.Meters = ObservedMeters;
|
||||
o.ActivitySources = ObservedActivitySources;
|
||||
if (Enum.TryParse<ZbExporter>(configuration["OtOpcUa:Telemetry:Exporter"], ignoreCase: true, out var exporter))
|
||||
o.Exporter = exporter;
|
||||
var otlp = configuration["OtOpcUa:Telemetry:OtlpEndpoint"];
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
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();
|
||||
}
|
||||
Reference in New Issue
Block a user