feat(scadabridge): add ScadaBridgeTelemetry meter + 4 instruments; register with OTel
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
using System.Diagnostics.Metrics;
|
||||
|
||||
namespace ZB.MOM.WW.ScadaBridge.Commons.Observability;
|
||||
|
||||
/// <summary>
|
||||
/// Central <see cref="Meter"/> + instrument definitions for ScadaBridge's application
|
||||
/// telemetry, modelled on OtOpcUa's <c>OtOpcUaTelemetry</c>. Modules emit through these
|
||||
/// pre-created instruments so a single OpenTelemetry / Prometheus binding in
|
||||
/// <c>Host</c> (registered via <c>AddZbTelemetry</c> with this meter named in
|
||||
/// <c>ZbTelemetryOptions.Meters</c>) catches everything. No exporter is required —
|
||||
/// instruments are no-op until a listener attaches, so tests and dev hosts pay nothing
|
||||
/// for instrumentation that nobody scrapes.
|
||||
///
|
||||
/// Instrument names follow the OpenTelemetry semantic convention pattern
|
||||
/// <c>scadabridge.<subsystem>.<event></c>. This task defines the instruments and
|
||||
/// their emit helpers; four later tasks wire the actual emit points. Until those land the
|
||||
/// helpers are dormant but inert — calling them is safe and simply records against a meter
|
||||
/// that nothing observes.
|
||||
/// </summary>
|
||||
public static class ScadaBridgeTelemetry
|
||||
{
|
||||
/// <summary>The meter name registered with OTel via <c>ZbTelemetryOptions.Meters</c>.</summary>
|
||||
public const string MeterName = "ZB.MOM.WW.ScadaBridge";
|
||||
|
||||
/// <summary>Singleton <see cref="Meter"/> all instruments hang off.</summary>
|
||||
private static readonly Meter Meter = new(MeterName);
|
||||
|
||||
// ---------------- Counters ----------------
|
||||
|
||||
/// <summary>Incremented each time a deployment is successfully applied.</summary>
|
||||
private static readonly Counter<long> _deploymentsApplied =
|
||||
Meter.CreateCounter<long>("scadabridge.deployments.applied", unit: "1",
|
||||
description: "Deployments applied.");
|
||||
|
||||
/// <summary>Incremented for each inbound API request, tagged with the API method.</summary>
|
||||
private static readonly Counter<long> _inboundApiRequests =
|
||||
Meter.CreateCounter<long>("scadabridge.inbound_api.requests", unit: "1",
|
||||
description: "Inbound API requests, tagged by method.");
|
||||
|
||||
// ---------------- Observable gauges ----------------
|
||||
|
||||
/// <summary>Current count of open site connections, mutated via <see cref="Interlocked"/>.</summary>
|
||||
private static long _siteConnectionsUp;
|
||||
|
||||
/// <summary>Provider that yields the live StoreAndForward queue depth; set by a later task.</summary>
|
||||
private static Func<long>? _queueDepthProvider;
|
||||
|
||||
#pragma warning disable IDE0052 // Held to keep the observable gauges alive for the meter's lifetime.
|
||||
/// <summary>Gauge reporting the number of currently open site connections.</summary>
|
||||
private static readonly ObservableGauge<long> _siteConnectionUp =
|
||||
Meter.CreateObservableGauge<long>("scadabridge.site.connection.up",
|
||||
() => Interlocked.Read(ref _siteConnectionsUp),
|
||||
description: "Number of currently open site connections.");
|
||||
|
||||
/// <summary>Gauge reporting the current StoreAndForward queue depth via the registered provider.</summary>
|
||||
private static readonly ObservableGauge<long> _storeAndForwardQueueDepth =
|
||||
Meter.CreateObservableGauge<long>("scadabridge.store_and_forward.queue.depth",
|
||||
() => Volatile.Read(ref _queueDepthProvider) is { } p ? p() : 0L,
|
||||
unit: "items",
|
||||
description: "Current StoreAndForward queue depth.");
|
||||
#pragma warning restore IDE0052
|
||||
|
||||
// ---------------- Emit helpers ----------------
|
||||
|
||||
/// <summary>Records that a deployment was applied.</summary>
|
||||
public static void RecordDeploymentApplied() => _deploymentsApplied.Add(1);
|
||||
|
||||
/// <summary>Records an inbound API request for the given <paramref name="method"/>.</summary>
|
||||
/// <param name="method">The API method the request targeted.</param>
|
||||
public static void RecordInboundApiRequest(string method) =>
|
||||
_inboundApiRequests.Add(1, new KeyValuePair<string, object?>("method", method));
|
||||
|
||||
/// <summary>Records that a site connection opened (increments the up-count gauge).</summary>
|
||||
public static void SiteConnectionOpened() => Interlocked.Increment(ref _siteConnectionsUp);
|
||||
|
||||
/// <summary>Records that a site connection closed (decrements the up-count gauge).</summary>
|
||||
public static void SiteConnectionClosed() => Interlocked.Decrement(ref _siteConnectionsUp);
|
||||
|
||||
/// <summary>
|
||||
/// Registers the provider the StoreAndForward queue-depth gauge reads on each observation.
|
||||
/// A later task supplies a provider that reads the real StoreAndForward depth. A null
|
||||
/// provider is ignored so the gauge falls back to reporting 0.
|
||||
/// </summary>
|
||||
/// <param name="provider">A callback returning the current queue depth.</param>
|
||||
public static void SetQueueDepthProvider(Func<long> provider)
|
||||
{
|
||||
if (provider is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Volatile.Write(ref _queueDepthProvider, provider);
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ using ZB.MOM.WW.ScadaBridge.AuditLog;
|
||||
using ZB.MOM.WW.ScadaBridge.ClusterInfrastructure;
|
||||
using ZB.MOM.WW.ScadaBridge.Communication;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Observability;
|
||||
using ZB.MOM.WW.ScadaBridge.DataConnectionLayer;
|
||||
using ZB.MOM.WW.ScadaBridge.ExternalSystemGateway;
|
||||
using ZB.MOM.WW.ScadaBridge.HealthMonitoring;
|
||||
@@ -122,12 +123,14 @@ public static class SiteServiceRegistration
|
||||
// the always-on Prometheus exporter. Mount the /metrics scrape endpoint per role
|
||||
// with app.MapZbMetrics(). The same `?? "central"` SiteId default Program.cs uses
|
||||
// is applied here so the Resource attribute matches the log-enricher value.
|
||||
// Meters left empty — application instruments are a deferred follow-on.
|
||||
// The application meter is named so OTel observes its instruments; emit points are
|
||||
// wired by follow-on tasks (the instruments are no-op until a listener attaches).
|
||||
services.AddZbTelemetry(o =>
|
||||
{
|
||||
o.ServiceName = "scadabridge";
|
||||
o.SiteId = config["ScadaBridge:Node:SiteId"] ?? "central";
|
||||
o.NodeRole = config["ScadaBridge:Node:Role"];
|
||||
o.Meters = [ScadaBridgeTelemetry.MeterName];
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Observability;
|
||||
|
||||
namespace ZB.MOM.WW.ScadaBridge.Host.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Infrastructure-free guards for <see cref="ScadaBridgeTelemetry"/>: the meter name is the
|
||||
/// stable value registered with OTel, and the emit helpers are safe to call (they are no-op
|
||||
/// until follow-on tasks wire real emit points and a listener attaches).
|
||||
/// </summary>
|
||||
public class ScadaBridgeTelemetryTests
|
||||
{
|
||||
[Fact]
|
||||
public void MeterName_IsStableValue()
|
||||
{
|
||||
Assert.Equal("ZB.MOM.WW.ScadaBridge", ScadaBridgeTelemetry.MeterName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EmitHelpers_DoNotThrow()
|
||||
{
|
||||
var ex = Record.Exception(() =>
|
||||
{
|
||||
ScadaBridgeTelemetry.RecordDeploymentApplied();
|
||||
ScadaBridgeTelemetry.RecordInboundApiRequest("X");
|
||||
ScadaBridgeTelemetry.SiteConnectionOpened();
|
||||
ScadaBridgeTelemetry.SiteConnectionClosed();
|
||||
ScadaBridgeTelemetry.SetQueueDepthProvider(() => 5);
|
||||
});
|
||||
|
||||
Assert.Null(ex);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user