5ef27fd9a3
Adds four instruments on the already-allow-listed ZB.MOM.WW.ScadaBridge meter:
scadabridge.deployments.failed{reason}, scadabridge.store_and_forward.activity{action,category},
scadabridge.site.script.runs{outcome} + scadabridge.site.script.duration{outcome} (ms),
scadabridge.site.data_connections.transitions{state}. Queue depth, DCL connection state and
deploy-applied already existed. No new Meter, so ZbTelemetryOptions.Meters needs no change.
Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
559 lines
32 KiB
C#
559 lines
32 KiB
C#
using System.Diagnostics.Metrics;
|
|
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
|
|
|
|
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 deployment that ends in a terminal failure (ScadaBridge#81), tagged
|
|
/// <c>reason</c>: <c>timeout</c> (the site round-trip timed out), <c>error</c> (any other fault
|
|
/// in the send or post-send persistence path) or <c>site_failed</c> (the site answered with a
|
|
/// non-Success status). Pairs with <c>scadabridge.deployments.applied</c> to give the deploy
|
|
/// success ratio; the rate-limited log lines cannot be alerted on.
|
|
/// </summary>
|
|
private static readonly Counter<long> _deploymentsFailed =
|
|
Meter.CreateCounter<long>("scadabridge.deployments.failed", unit: "1",
|
|
description: "Deployments that ended in a terminal failure, tagged by reason (timeout|error|site_failed).");
|
|
|
|
/// <summary>
|
|
/// Incremented for each store-and-forward lifecycle event (ScadaBridge#81), tagged <c>action</c>
|
|
/// (<c>Queued</c>, <c>Delivered</c>, <c>Retried</c>, <c>Parked</c>, <c>Retry</c>, <c>Discard</c>)
|
|
/// and <c>category</c>. A sustained <c>Parked</c> rate is the alertable signal that messages are
|
|
/// exhausting retries; the queue-depth gauge alone cannot distinguish a drain from a park.
|
|
/// </summary>
|
|
private static readonly Counter<long> _sfActivity =
|
|
Meter.CreateCounter<long>("scadabridge.store_and_forward.activity", unit: "1",
|
|
description: "Store-and-forward lifecycle events, tagged by action and category.");
|
|
|
|
/// <summary>
|
|
/// Incremented for each finished site script run (ScadaBridge#81), tagged <c>outcome</c>
|
|
/// (<c>completed</c> | <c>failed</c> | <c>timed_out</c>). Deliberately NOT tagged by instance or
|
|
/// script name: that cardinality is unbounded across a site's instances.
|
|
/// </summary>
|
|
private static readonly Counter<long> _scriptRuns =
|
|
Meter.CreateCounter<long>("scadabridge.site.script.runs", unit: "1",
|
|
description: "Finished site script runs, tagged by outcome (completed|failed|timed_out).");
|
|
|
|
/// <summary>
|
|
/// Incremented each time a data connection actually CHANGES state (ScadaBridge#81), tagged
|
|
/// <c>state</c> (the new <see cref="ConnectionHealth"/>). The data_connections gauges show the
|
|
/// current picture; this shows flapping that a scrape interval would otherwise average away.
|
|
/// </summary>
|
|
private static readonly Counter<long> _dataConnectionTransitions =
|
|
Meter.CreateCounter<long>("scadabridge.site.data_connections.transitions", unit: "1",
|
|
description: "Data connection state changes, tagged by the new state.");
|
|
|
|
/// <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.");
|
|
|
|
/// <summary>Incremented each time an inbound execution is abandoned (handler outlived its request), tagged with the API method.</summary>
|
|
private static readonly Counter<long> _inboundAbandonedExecutions =
|
|
Meter.CreateCounter<long>("scadabridge.inbound_api.abandoned_executions", unit: "1",
|
|
description: "Inbound API executions abandoned after timeout/abort (handler still running), tagged by method.");
|
|
|
|
/// <summary>Incremented each time an S&F buffer replication op fails to dispatch/deliver to the peer.</summary>
|
|
private static readonly Counter<long> _replicationFailures =
|
|
Meter.CreateCounter<long>("scadabridge.store_and_forward.replication.failures", unit: "1",
|
|
description: "S&F buffer replication operations that failed to dispatch/deliver to the peer node");
|
|
|
|
private static readonly Counter<long> _sfResyncCompleted =
|
|
Meter.CreateCounter<long>("scadabridge.store_and_forward.resync.completed", unit: "1",
|
|
description: "S&F anti-entropy resyncs the standby acknowledged as applied");
|
|
|
|
private static readonly Counter<long> _sfResyncAckMissing =
|
|
Meter.CreateCounter<long>("scadabridge.store_and_forward.resync.ack_missing", unit: "1",
|
|
description: "S&F resyncs answered by the active node but never acknowledged by the standby within the ack window (lost chunks / dead peer)");
|
|
|
|
/// <summary>
|
|
/// Incremented each time a per-site live-alarm aggregator re-establishes its site-wide
|
|
/// gRPC stream — a NodeA↔NodeB failover flip or a reconcile-driven reopen after the
|
|
/// stream was given up. A sustained climb signals a flapping site link.
|
|
/// </summary>
|
|
private static readonly Counter<long> _liveAlarmStreamReconnects =
|
|
Meter.CreateCounter<long>("scadabridge.site.alarm_cache.reconnects", unit: "1",
|
|
description: "Live-alarm aggregator site-wide gRPC stream reconnects (NodeA↔NodeB flip or reconcile-driven reopen).");
|
|
|
|
/// <summary>
|
|
/// Incremented for each live delta evicted from a per-site live-alarm aggregator's
|
|
/// bounded pre-seed buffer (drop-oldest). Non-zero means a seed/reconcile
|
|
/// fan-out ran long enough for the delta storm behind it to exceed the cap — the
|
|
/// dropped transitions are recovered by the fan-out's authoritative snapshot, but a
|
|
/// sustained climb points at a slow site.
|
|
/// </summary>
|
|
private static readonly Counter<long> _liveAlarmBufferDrops =
|
|
Meter.CreateCounter<long>("scadabridge.site.alarm_cache.buffer_dropped", unit: "1",
|
|
description: "Live deltas evicted from a live-alarm aggregator's bounded pre-seed buffer (drop-oldest).");
|
|
|
|
/// <summary>
|
|
/// Incremented by the number of entries the site script compile cache evicts when it crosses its
|
|
/// bound (ScadaBridge#68). Eviction does not unload the Roslyn assembly, so each recompile of an
|
|
/// evicted body loads another permanent copy: a sustained climb means compile-cache thrash and
|
|
/// monotonic working-set growth - raise <c>ScadaBridge:SiteRuntime:ScriptCompileCacheMaxEntries</c>.
|
|
/// Alert on this instead of the rate-limited Warning log.
|
|
/// </summary>
|
|
private static readonly Counter<long> _scriptCompileCacheEvictions =
|
|
Meter.CreateCounter<long>("scadabridge.site.script_compile_cache.evictions", unit: "1",
|
|
description: "Entries evicted from the site script compile cache at its bound (each recompile of an evicted body loads another permanent Roslyn assembly).");
|
|
|
|
/// <summary>
|
|
/// Incremented for each debug event evicted from a central debug session's bounded
|
|
/// pre-snapshot buffer (drop-oldest). The Debug View is lossy-under-backpressure
|
|
/// by design; this makes the loss measurable instead of unbounded memory growth.
|
|
/// </summary>
|
|
private static readonly Counter<long> _debugPreSnapshotDrops =
|
|
Meter.CreateCounter<long>("scadabridge.central.debug_view.presnapshot_dropped", unit: "1",
|
|
description: "Debug events evicted from a central debug session's bounded pre-snapshot buffer (drop-oldest).");
|
|
|
|
/// <summary>
|
|
/// Incremented for each event evicted from a site-hosted gRPC stream's bounded send
|
|
/// channel, tagged by stream kind (<c>instance</c> = Debug View, <c>site-alarms</c> =
|
|
/// the site-wide alarm feed behind the operator Alarm Summary).
|
|
/// </summary>
|
|
private static readonly Counter<long> _siteStreamEventDrops =
|
|
Meter.CreateCounter<long>("scadabridge.site.stream.events_dropped", unit: "1",
|
|
description: "Events evicted from a site gRPC stream's bounded send channel, tagged by stream kind.");
|
|
|
|
/// <summary>
|
|
/// Register row 66: incremented each time <c>BundleSessionStore.Open</c> refuses a
|
|
/// bundle-import session because <c>TransportOptions.MaxConcurrentImportSessions</c>
|
|
/// (default 8) is genuinely exhausted — i.e. the store was still at the cap AFTER
|
|
/// expired sessions were reclaimed. Reclaim-then-succeed and re-opening an already
|
|
/// open session id are deliberately NOT counted: neither turns an operator away.
|
|
///
|
|
/// Row 53 shipped the companion open-sessions gauge and a Warning log, but a log line
|
|
/// is not alertable. The pair is what distinguishes the two failure modes: rejections
|
|
/// climbing while the gauge oscillates below the cap is genuine concurrency (raise the
|
|
/// cap), whereas rejections climbing while the gauge sits pinned at the cap is a
|
|
/// session leak like register row 43's (find the leak — raising the cap only defers it).
|
|
/// </summary>
|
|
private static readonly Counter<long> _transportImportSessionsRejected =
|
|
Meter.CreateCounter<long>("scadabridge.central.transport.import_sessions.rejected", unit: "1",
|
|
description: "Bundle-import sessions rejected because the concurrent-import-session cap was exhausted.");
|
|
|
|
/// <summary>
|
|
/// Register row 34: incremented once per site-event-log purge tick that stopped at
|
|
/// <c>SiteEventLogOptions.MaxPurgedRowsPerTick</c> with expired rows still to delete.
|
|
///
|
|
/// A single hit is routine (a backlog is being worked through on accelerated catch-up
|
|
/// ticks) — what the series is for is the SHAPE. A short burst of hits that stops is a
|
|
/// backlog converging; hits that never stop mean the event log is being written faster
|
|
/// than one paced tick can purge, i.e. the ceiling or the catch-up interval is now
|
|
/// mis-sized for this site's event rate. The paired log line is rate-limited and so
|
|
/// cannot answer that question on its own.
|
|
/// </summary>
|
|
private static readonly Counter<long> _siteEventLogPurgeCeilingHits =
|
|
Meter.CreateCounter<long>("scadabridge.site.event_log.purge_ceiling_hits", unit: "1",
|
|
description: "Site event log purge ticks that stopped at the per-tick delete ceiling with work remaining.");
|
|
|
|
// ---------------- Histograms ----------------
|
|
|
|
/// <summary>
|
|
/// Distribution of how many events each site→central stream frame carried (R2 — gRPC
|
|
/// event batching), tagged by stream kind. Recorded ONLY for subscriptions that
|
|
/// negotiated batching, so the series' very existence says "this central speaks the
|
|
/// batched wire". A distribution pinned at 1 means the coalescing window never sees a
|
|
/// backlog (the site is quiet, or the window is too small to be earning anything);
|
|
/// mass at the size cap means the cap, not the window, is the binding constraint.
|
|
/// </summary>
|
|
private static readonly Histogram<int> _siteStreamBatchSize =
|
|
Meter.CreateHistogram<int>("scadabridge.site.stream.batch_size", unit: "1",
|
|
description: "Events per site gRPC stream frame (1 = unbatched frame), tagged by stream kind.");
|
|
|
|
/// <summary>Duration of finished site script runs in milliseconds (ScadaBridge#81), tagged by outcome.</summary>
|
|
private static readonly Histogram<long> _scriptRunDuration =
|
|
Meter.CreateHistogram<long>("scadabridge.site.script.duration", unit: "ms",
|
|
description: "Site script run duration in milliseconds, tagged by outcome (completed|failed|timed_out).");
|
|
|
|
// ---------------- Observable gauges ----------------
|
|
|
|
/// <summary>
|
|
/// Current count of open central→site gRPC subscriber streams, mutated via
|
|
/// <see cref="Interlocked"/>. See <see cref="_siteStreamSubscribersActive"/>'s gauge for
|
|
/// why this is NOT a data-connection count.
|
|
/// </summary>
|
|
private static long _siteStreamSubscriberCount;
|
|
|
|
/// <summary>
|
|
/// Register row 54: last-known state of every data connection on this site node, keyed by
|
|
/// connection name. Written by the DCL at its four state-transition points and read by the
|
|
/// data-connection gauges below.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// A static registry, matching <see cref="_siteStreamSubscriberCount"/>'s precedent, rather
|
|
/// than a DI-injected holder: the DCL actors already receive <c>ISiteHealthCollector</c>
|
|
/// through their constructors, and threading a second dependency through both actor
|
|
/// constructors would ripple into every test that builds them. A site node hosts exactly
|
|
/// one DCL, so a process-global registry and a per-node one are the same thing here.
|
|
/// </remarks>
|
|
private static readonly System.Collections.Concurrent.ConcurrentDictionary<string, ConnectionHealth>
|
|
_dataConnectionStates = new(StringComparer.Ordinal);
|
|
|
|
/// <summary>Current count of running per-site live-alarm aggregators, mutated via <see cref="Interlocked"/>.</summary>
|
|
private static long _liveAlarmAggregatorsActive;
|
|
|
|
/// <summary>Provider that yields the live StoreAndForward queue depth; set by a later task.</summary>
|
|
private static Func<long>? _queueDepthProvider;
|
|
|
|
/// <summary>
|
|
/// Register row 53: provider that yields the number of currently open (unexpired)
|
|
/// Transport bundle-import sessions, registered by the Transport component's
|
|
/// session-eviction hosted service. Null on a site node (Transport is central-only),
|
|
/// where the gauge simply reports 0.
|
|
/// </summary>
|
|
private static Func<long>? _importSessionCountProvider;
|
|
|
|
#pragma warning disable IDE0052 // Held to keep the observable gauges alive for the meter's lifetime.
|
|
/// <summary>
|
|
/// Gauge reporting the number of open central→site gRPC subscriber streams.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Register row 54 RENAMED this from <c>scadabridge.site.connection.up</c>
|
|
/// (<c>scadabridge_site_connection_up</c> in Prometheus). The old name read as a
|
|
/// data-connection health signal, but it has only ever counted open subscriber streams
|
|
/// on <c>SiteStreamGrpcServer</c> — Debug View and the site-wide alarm feed. It therefore
|
|
/// sits at 0 on a perfectly healthy site that nobody is watching, and an alert written
|
|
/// against the old name fired on an idle operator console rather than a broken device.
|
|
/// For real data-connection health use the <c>scadabridge.site.data_connections.*</c>
|
|
/// gauges below. Counts both stream kinds together (no <c>stream</c> tag).
|
|
/// </remarks>
|
|
private static readonly ObservableGauge<long> _siteStreamSubscribersActive =
|
|
Meter.CreateObservableGauge<long>("scadabridge.site.stream.subscribers.active",
|
|
() => Interlocked.Read(ref _siteStreamSubscriberCount),
|
|
unit: "1",
|
|
description: "Open central→site gRPC subscriber streams (Debug View + site-wide alarm feed). NOT data connections.");
|
|
|
|
/// <summary>
|
|
/// Register row 54: gauge reporting how many of this site node's data connections are
|
|
/// currently in <see cref="ConnectionHealth.Connected"/> — the real data-connection
|
|
/// signal the misnamed stream-subscriber gauge was being mistaken for.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Read together with <see cref="_dataConnectionsTotal"/>: <c>connected < total</c> is the
|
|
/// alertable condition, and neither number alone distinguishes "one device down" from
|
|
/// "nothing configured". Expect **0 on the standby node** — connections are opened by the
|
|
/// deployment path on the active node only, so a standby reporting 0 is healthy, not a
|
|
/// fault. Alert on the active node, or on <c>max()</c> across the pair.
|
|
/// </remarks>
|
|
private static readonly ObservableGauge<long> _dataConnectionsConnected =
|
|
Meter.CreateObservableGauge<long>("scadabridge.site.data_connections.connected",
|
|
CountDataConnections,
|
|
unit: "1",
|
|
description: "Data connections on this site node currently in the Connected state.");
|
|
|
|
/// <summary>
|
|
/// Register row 54: gauge reporting how many data connections this site node knows about,
|
|
/// in any state. The denominator for <see cref="_dataConnectionsConnected"/>.
|
|
/// </summary>
|
|
private static readonly ObservableGauge<long> _dataConnectionsTotal =
|
|
Meter.CreateObservableGauge<long>("scadabridge.site.data_connections.total",
|
|
() => (long)_dataConnectionStates.Count,
|
|
unit: "1",
|
|
description: "Data connections known to this site node, in any state.");
|
|
|
|
/// <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.");
|
|
|
|
/// <summary>Gauge reporting the number of currently running per-site live-alarm aggregators.</summary>
|
|
private static readonly ObservableGauge<long> _liveAlarmAggregatorsActiveGauge =
|
|
Meter.CreateObservableGauge<long>("scadabridge.site.alarm_cache.aggregators.active",
|
|
() => Interlocked.Read(ref _liveAlarmAggregatorsActive),
|
|
unit: "1",
|
|
description: "Number of per-site live-alarm aggregators currently running on the active central node.");
|
|
|
|
/// <summary>
|
|
/// Register row 53: gauge reporting the number of open Transport bundle-import
|
|
/// sessions on this central node. Each open session pins a fully decrypted bundle
|
|
/// in memory and consumes one of <c>TransportOptions.MaxConcurrentImportSessions</c>
|
|
/// (default 8), so a series that plateaus at the cap is the signature of leaked
|
|
/// sessions — the failure mode register row 43 could only be inferred from rotating
|
|
/// test failures because nothing counted them.
|
|
/// </summary>
|
|
private static readonly ObservableGauge<long> _transportImportSessionsOpen =
|
|
Meter.CreateObservableGauge<long>("scadabridge.central.transport.import_sessions.open",
|
|
() => Volatile.Read(ref _importSessionCountProvider) is { } p ? p() : 0L,
|
|
unit: "1",
|
|
description: "Number of currently open bundle-import sessions on this central node.");
|
|
#pragma warning restore IDE0052
|
|
|
|
// ---------------- Emit helpers ----------------
|
|
|
|
/// <summary>Records that a deployment was applied.</summary>
|
|
public static void RecordDeploymentApplied() => _deploymentsApplied.Add(1);
|
|
|
|
/// <summary>Records one deployment that ended in a terminal failure (ScadaBridge#81).</summary>
|
|
/// <param name="reason">One of <c>timeout</c>, <c>error</c>, <c>site_failed</c>.</param>
|
|
public static void RecordDeploymentFailed(string reason) =>
|
|
_deploymentsFailed.Add(1, new KeyValuePair<string, object?>("reason", reason));
|
|
|
|
/// <summary>Records one store-and-forward lifecycle event (ScadaBridge#81).</summary>
|
|
/// <param name="action">The activity action, e.g. <c>Parked</c>.</param>
|
|
/// <param name="category">The message category name.</param>
|
|
public static void RecordStoreAndForwardActivity(string action, string category) =>
|
|
_sfActivity.Add(1,
|
|
new KeyValuePair<string, object?>("action", action),
|
|
new KeyValuePair<string, object?>("category", category));
|
|
|
|
/// <summary>Records one finished site script run and its duration (ScadaBridge#81).</summary>
|
|
/// <param name="outcome">One of <c>completed</c>, <c>failed</c>, <c>timed_out</c>.</param>
|
|
/// <param name="durationMs">The run duration in milliseconds.</param>
|
|
public static void RecordScriptRun(string outcome, long durationMs)
|
|
{
|
|
var tag = new KeyValuePair<string, object?>("outcome", outcome);
|
|
_scriptRuns.Add(1, tag);
|
|
_scriptRunDuration.Record(durationMs < 0 ? 0 : durationMs, tag);
|
|
}
|
|
|
|
/// <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 an inbound execution was abandoned (handler outlived its request) for the given <paramref name="method"/>.</summary>
|
|
/// <param name="method">The API method whose execution was abandoned.</param>
|
|
public static void RecordInboundAbandonedExecution(string method) =>
|
|
_inboundAbandonedExecutions.Add(1, new KeyValuePair<string, object?>("method", method));
|
|
|
|
/// <summary>Records one failed S&F replication dispatch.</summary>
|
|
public static void RecordReplicationFailure() => _replicationFailures.Add(1);
|
|
|
|
/// <summary>Records one acknowledged (applied) S&F buffer resync.</summary>
|
|
public static void RecordSfResyncCompleted() => _sfResyncCompleted.Add(1);
|
|
|
|
/// <summary>Records one resync whose ack window expired.</summary>
|
|
public static void RecordSfResyncAckMissing() => _sfResyncAckMissing.Add(1);
|
|
|
|
/// <summary>
|
|
/// Records one bundle-import session rejected on a genuine concurrent-session cap hit
|
|
/// (register row 66). Call ONLY from the post-eviction recheck in
|
|
/// <c>BundleSessionStore.Open</c> — counting the pre-eviction check would inflate the
|
|
/// series with reclaim-then-succeed cases that never turned an operator away.
|
|
/// </summary>
|
|
public static void RecordImportSessionRejected() => _transportImportSessionsRejected.Add(1);
|
|
|
|
/// <summary>
|
|
/// Records one site-event-log purge tick that stopped at its per-tick delete ceiling
|
|
/// (register row 34). Call ONLY when rows genuinely remain to be purged — a tick that
|
|
/// finished the work and happened to land exactly on the ceiling has nothing to report.
|
|
/// </summary>
|
|
public static void RecordSiteEventLogPurgeCeilingHit() => _siteEventLogPurgeCeilingHits.Add(1);
|
|
|
|
/// <summary>
|
|
/// Records that a central→site gRPC subscriber stream opened (increments
|
|
/// <c>scadabridge.site.stream.subscribers.active</c>). Renamed from
|
|
/// <c>SiteConnectionOpened</c> by register row 54 — it never counted data connections.
|
|
/// </summary>
|
|
public static void SiteStreamSubscriberOpened() => Interlocked.Increment(ref _siteStreamSubscriberCount);
|
|
|
|
/// <summary>
|
|
/// Records that a central→site gRPC subscriber stream closed (decrements
|
|
/// <c>scadabridge.site.stream.subscribers.active</c>). Renamed from
|
|
/// <c>SiteConnectionClosed</c> by register row 54.
|
|
/// </summary>
|
|
public static void SiteStreamSubscriberClosed() => Interlocked.Decrement(ref _siteStreamSubscriberCount);
|
|
|
|
/// <summary>
|
|
/// Records the current state of one data connection, feeding the
|
|
/// <c>scadabridge.site.data_connections.*</c> gauges (register row 54). Called by the DCL
|
|
/// at each connection state transition; safe to call repeatedly with the same value.
|
|
/// </summary>
|
|
/// <param name="connectionName">The data connection's name (unique per site node).</param>
|
|
/// <param name="state">The connection's new state.</param>
|
|
public static void RecordDataConnectionState(string connectionName, ConnectionHealth state)
|
|
{
|
|
if (string.IsNullOrEmpty(connectionName))
|
|
{
|
|
return;
|
|
}
|
|
|
|
var previous = _dataConnectionStates.TryGetValue(connectionName, out var prior) ? prior : (ConnectionHealth?)null;
|
|
_dataConnectionStates[connectionName] = state;
|
|
|
|
if (previous != state)
|
|
{
|
|
_dataConnectionTransitions.Add(1, new KeyValuePair<string, object?>("state", state.ToString()));
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Drops a data connection from the gauges (register row 54). Called when the DCL removes
|
|
/// a connection, so an undeployed connection stops counting toward the total rather than
|
|
/// lingering as a permanently-not-connected row that would alert forever.
|
|
/// </summary>
|
|
/// <param name="connectionName">The data connection's name.</param>
|
|
public static void RemoveDataConnection(string connectionName)
|
|
{
|
|
if (string.IsNullOrEmpty(connectionName))
|
|
{
|
|
return;
|
|
}
|
|
|
|
_dataConnectionStates.TryRemove(connectionName, out _);
|
|
}
|
|
|
|
/// <summary>Counts data connections currently in <see cref="ConnectionHealth.Connected"/>.</summary>
|
|
/// <returns>The connected count.</returns>
|
|
private static long CountDataConnections()
|
|
{
|
|
long connected = 0;
|
|
foreach (var state in _dataConnectionStates.Values)
|
|
{
|
|
if (state == ConnectionHealth.Connected)
|
|
{
|
|
connected++;
|
|
}
|
|
}
|
|
|
|
return connected;
|
|
}
|
|
|
|
/// <summary>Records that a per-site live-alarm aggregator started (increments the active-aggregator gauge).</summary>
|
|
public static void LiveAlarmAggregatorStarted() => Interlocked.Increment(ref _liveAlarmAggregatorsActive);
|
|
|
|
/// <summary>Records that a per-site live-alarm aggregator stopped (decrements the active-aggregator gauge).</summary>
|
|
public static void LiveAlarmAggregatorStopped() => Interlocked.Decrement(ref _liveAlarmAggregatorsActive);
|
|
|
|
/// <summary>Records that a per-site live-alarm aggregator re-established its site-wide gRPC stream.</summary>
|
|
public static void RecordLiveAlarmStreamReconnect() => _liveAlarmStreamReconnects.Add(1);
|
|
|
|
/// <summary>Records live deltas evicted from a live-alarm aggregator's bounded pre-seed buffer.</summary>
|
|
/// <param name="count">Number of deltas evicted.</param>
|
|
public static void RecordLiveAlarmBufferDrop(long count = 1) => _liveAlarmBufferDrops.Add(count);
|
|
|
|
/// <summary>Records entries evicted from the site script compile cache.</summary>
|
|
/// <param name="count">Number of entries evicted by one sweep.</param>
|
|
public static void RecordScriptCompileCacheEvictions(long count) => _scriptCompileCacheEvictions.Add(count);
|
|
|
|
/// <summary>Records debug events evicted from a debug session's bounded pre-snapshot buffer.</summary>
|
|
/// <param name="count">Number of events evicted.</param>
|
|
public static void RecordDebugPreSnapshotDrop(long count = 1) => _debugPreSnapshotDrops.Add(count);
|
|
|
|
/// <summary>Records an event evicted from a site gRPC stream's bounded send channel.</summary>
|
|
/// <param name="streamKind">Stream kind tag (<c>instance</c> or <c>site-alarms</c>).</param>
|
|
public static void RecordSiteStreamEventDropped(string streamKind) =>
|
|
_siteStreamEventDrops.Add(1, new KeyValuePair<string, object?>("stream", streamKind));
|
|
|
|
/// <summary>
|
|
/// Records how many events one site gRPC stream frame carried. Called once per emitted
|
|
/// frame on a batching-negotiated subscription (never on an un-negotiated one, where it
|
|
/// would degenerate into a per-event instrument on the hottest path in the product).
|
|
/// </summary>
|
|
/// <param name="streamKind">Stream kind tag (<c>instance</c> or <c>site-alarms</c>).</param>
|
|
/// <param name="events">Events packed into the frame; 1 for a plain unbatched frame.</param>
|
|
public static void RecordSiteStreamBatchSize(string streamKind, int events) =>
|
|
_siteStreamBatchSize.Record(events, new KeyValuePair<string, object?>("stream", streamKind));
|
|
|
|
/// <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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Clears the StoreAndForward queue-depth provider, but only if the currently
|
|
/// registered provider is the exact <paramref name="provider"/> delegate passed in
|
|
/// (reference-equal compare-and-clear). This lets a StoreAndForward service deregister
|
|
/// its own provider on graceful stop without stomping a newer instance that already
|
|
/// re-registered into the process-global slot: if a late stop of the old instance
|
|
/// passes its (now-superseded) delegate, the identity check fails and the newer
|
|
/// provider is preserved. After a successful clear the gauge falls back to reporting 0.
|
|
/// Mirrors <see cref="SetQueueDepthProvider"/>'s signature and <see cref="Volatile"/>
|
|
/// access pattern.
|
|
/// </summary>
|
|
/// <param name="provider">The provider delegate to remove; ignored unless it is the
|
|
/// one currently registered.</param>
|
|
public static void ClearQueueDepthProvider(Func<long> provider)
|
|
{
|
|
if (provider is null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Compare-and-clear: only null the slot when it still holds the caller's
|
|
// delegate, so a stale stop cannot clobber a successor's provider.
|
|
Interlocked.CompareExchange(ref _queueDepthProvider, null, provider);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Register row 53: registers the provider the open-import-sessions gauge reads on
|
|
/// each observation. Called by the Transport component's session-eviction hosted
|
|
/// service on start. A null provider is ignored so the gauge falls back to 0.
|
|
/// The callback is invoked synchronously on the metrics collection thread, so it
|
|
/// must stay cheap — the Transport implementation reads a
|
|
/// <see cref="System.Collections.Concurrent.ConcurrentDictionary{TKey,TValue}"/> it
|
|
/// already owns.
|
|
/// </summary>
|
|
/// <param name="provider">A callback returning the current open-session count.</param>
|
|
public static void SetImportSessionCountProvider(Func<long> provider)
|
|
{
|
|
if (provider is null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
Volatile.Write(ref _importSessionCountProvider, provider);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Clears the open-import-sessions provider, but only if the currently registered
|
|
/// provider is the exact <paramref name="provider"/> delegate passed in
|
|
/// (reference-equal compare-and-clear), so a late stop of a superseded instance
|
|
/// cannot stomp a newer registrant. Mirrors <see cref="ClearQueueDepthProvider"/>.
|
|
/// </summary>
|
|
/// <param name="provider">The provider delegate to remove; ignored unless it is the
|
|
/// one currently registered.</param>
|
|
public static void ClearImportSessionCountProvider(Func<long> provider)
|
|
{
|
|
if (provider is null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
Interlocked.CompareExchange(ref _importSessionCountProvider, null, provider);
|
|
}
|
|
}
|