153 lines
6.9 KiB
C#
153 lines
6.9 KiB
C#
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Kpi;
|
|
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Kpi;
|
|
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
|
|
using ZB.MOM.WW.ScadaBridge.Commons.Types.Kpi;
|
|
|
|
namespace ZB.MOM.WW.ScadaBridge.HealthMonitoring.Kpi;
|
|
|
|
/// <summary>
|
|
/// Site Health <see cref="IKpiSampleSource"/> for the "KPI History &
|
|
/// Trends" backbone. Unlike the other sample sources — which each query a
|
|
/// central repository — this one reads the in-memory
|
|
/// <see cref="ICentralHealthAggregator"/> state, snapshotting the latest
|
|
/// <see cref="ZB.MOM.WW.ScadaBridge.Commons.Messages.Health.SiteHealthReport"/>
|
|
/// each tracked site has reported into per-site (<see cref="KpiScopes.Site"/>)
|
|
/// <see cref="KpiSample"/> rows.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <para>
|
|
/// Each sampling pass the central recorder enumerates this source and calls
|
|
/// <see cref="CollectAsync"/>, which iterates
|
|
/// <see cref="ICentralHealthAggregator.GetAllSiteStates"/> and, for every site
|
|
/// whose <see cref="SiteHealthState.LatestReport"/> is non-null, emits the
|
|
/// fixed metric catalog below. Sites known only via heartbeats (null
|
|
/// <see cref="SiteHealthState.LatestReport"/>) contribute nothing.
|
|
/// </para>
|
|
/// <para>
|
|
/// The aggregator is registered as a singleton (its state is shared, immutable,
|
|
/// and safe to hand straight to callers); this source is nonetheless registered
|
|
/// DI-scoped for consistency with the recorder's per-tick scope and the other
|
|
/// sample sources. A scoped consumer resolving a singleton dependency is the
|
|
/// normal DI lifetime relationship and introduces no captive-dependency issue.
|
|
/// </para>
|
|
/// </remarks>
|
|
public sealed class SiteHealthKpiSampleSource : IKpiSampleSource
|
|
{
|
|
// ── Metric catalog (the agreed metric-name strings for this source) ──
|
|
// Declaration order matches the emission order in AddSiteSnapshot. Charted metrics
|
|
// share the public Commons catalog so source + UI trend page key off one symbol;
|
|
// the uncharted internal metrics stay private here.
|
|
private const string MetricConnectionsUp = "connectionsUp";
|
|
private const string MetricConnectionsDown = KpiMetrics.SiteHealth.ConnectionsDown;
|
|
private const string MetricScriptErrors = KpiMetrics.SiteHealth.ScriptErrors;
|
|
private const string MetricAlarmEvalErrors = KpiMetrics.SiteHealth.AlarmEvalErrors;
|
|
private const string MetricSfBufferDepth = KpiMetrics.SiteHealth.SfBufferDepth;
|
|
private const string MetricDeadLetters = KpiMetrics.SiteHealth.DeadLetters;
|
|
private const string MetricParkedMessages = "parkedMessages";
|
|
private const string MetricDeployedInstances = "deployedInstances";
|
|
private const string MetricEnabledInstances = "enabledInstances";
|
|
private const string MetricDisabledInstances = "disabledInstances";
|
|
private const string MetricAuditBacklogPending = "auditBacklogPending";
|
|
private const string MetricEventLogWriteFailures = KpiMetrics.SiteHealth.EventLogWriteFailures;
|
|
|
|
private readonly ICentralHealthAggregator _aggregator;
|
|
|
|
/// <summary>
|
|
/// Creates the sample source over the central in-memory health aggregator.
|
|
/// </summary>
|
|
/// <param name="aggregator">The central health aggregator holding per-site state.</param>
|
|
public SiteHealthKpiSampleSource(ICentralHealthAggregator aggregator)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(aggregator);
|
|
_aggregator = aggregator;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public string Source => KpiSources.SiteHealth;
|
|
|
|
/// <inheritdoc />
|
|
public Task<IReadOnlyList<KpiSample>> CollectAsync(
|
|
DateTime capturedAtUtc, CancellationToken cancellationToken = default)
|
|
{
|
|
var samples = new List<KpiSample>();
|
|
|
|
foreach (var (siteId, state) in _aggregator.GetAllSiteStates())
|
|
{
|
|
// Sites known only via heartbeats (no report yet) contribute nothing.
|
|
if (state.LatestReport is not { } report)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
AddSiteSnapshot(samples, capturedAtUtc, siteId, report);
|
|
}
|
|
|
|
return Task.FromResult<IReadOnlyList<KpiSample>>(samples);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Appends the per-site metric catalog for one site's latest report,
|
|
/// mapping each metric to the corresponding
|
|
/// <see cref="ZB.MOM.WW.ScadaBridge.Commons.Messages.Health.SiteHealthReport"/>
|
|
/// field.
|
|
/// </summary>
|
|
private static void AddSiteSnapshot(
|
|
List<KpiSample> samples,
|
|
DateTime capturedAtUtc,
|
|
string siteId,
|
|
ZB.MOM.WW.ScadaBridge.Commons.Messages.Health.SiteHealthReport report)
|
|
{
|
|
// Connection statuses: Connected counts as up, everything else
|
|
// (Disconnected / Connecting / Error) counts as down.
|
|
var connectionsUp = 0;
|
|
var connectionsDown = 0;
|
|
foreach (var status in report.DataConnectionStatuses.Values)
|
|
{
|
|
if (status == ConnectionHealth.Connected)
|
|
{
|
|
connectionsUp++;
|
|
}
|
|
else
|
|
{
|
|
connectionsDown++;
|
|
}
|
|
}
|
|
|
|
// Sum of the store-and-forward buffer depths across all buffers.
|
|
long sfBufferDepth = 0;
|
|
foreach (var depth in report.StoreAndForwardBufferDepths.Values)
|
|
{
|
|
sfBufferDepth += depth;
|
|
}
|
|
|
|
samples.Add(Sample(capturedAtUtc, MetricConnectionsUp, siteId, connectionsUp));
|
|
samples.Add(Sample(capturedAtUtc, MetricConnectionsDown, siteId, connectionsDown));
|
|
samples.Add(Sample(capturedAtUtc, MetricScriptErrors, siteId, report.ScriptErrorCount));
|
|
samples.Add(Sample(capturedAtUtc, MetricAlarmEvalErrors, siteId, report.AlarmEvaluationErrorCount));
|
|
samples.Add(Sample(capturedAtUtc, MetricSfBufferDepth, siteId, sfBufferDepth));
|
|
samples.Add(Sample(capturedAtUtc, MetricDeadLetters, siteId, report.DeadLetterCount));
|
|
samples.Add(Sample(capturedAtUtc, MetricParkedMessages, siteId, report.ParkedMessageCount));
|
|
samples.Add(Sample(capturedAtUtc, MetricDeployedInstances, siteId, report.DeployedInstanceCount));
|
|
samples.Add(Sample(capturedAtUtc, MetricEnabledInstances, siteId, report.EnabledInstanceCount));
|
|
samples.Add(Sample(capturedAtUtc, MetricDisabledInstances, siteId, report.DisabledInstanceCount));
|
|
// Nested audit-backlog object may be null (no data yet) — treat as 0.
|
|
samples.Add(Sample(
|
|
capturedAtUtc, MetricAuditBacklogPending, siteId,
|
|
report.SiteAuditBacklog?.PendingCount ?? 0));
|
|
samples.Add(Sample(
|
|
capturedAtUtc, MetricEventLogWriteFailures, siteId, report.SiteEventLogWriteFailures));
|
|
}
|
|
|
|
private static KpiSample Sample(
|
|
DateTime capturedAtUtc, string metric, string scopeKey, double value) =>
|
|
new()
|
|
{
|
|
Source = KpiSources.SiteHealth,
|
|
Metric = metric,
|
|
Scope = KpiScopes.Site,
|
|
ScopeKey = scopeKey,
|
|
Value = value,
|
|
CapturedAtUtc = capturedAtUtc,
|
|
};
|
|
}
|