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
This commit is contained in:
Joseph Doherty
2026-11-12 20:31:14 -05:00
parent aaaa55e211
commit 0b90379f70
5 changed files with 135 additions and 8 deletions
@@ -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"];