164 lines
6.8 KiB
C#
164 lines
6.8 KiB
C#
using Microsoft.Extensions.Options;
|
|
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Kpi;
|
|
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Kpi;
|
|
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories;
|
|
using ZB.MOM.WW.ScadaBridge.Commons.Types.Audit;
|
|
using ZB.MOM.WW.ScadaBridge.Commons.Types.Kpi;
|
|
|
|
namespace ZB.MOM.WW.ScadaBridge.SiteCallAudit.Kpi;
|
|
|
|
/// <summary>
|
|
/// Site Call Audit (#22) <see cref="IKpiSampleSource"/> for the M6 "KPI History
|
|
/// & Trends" backbone. Each sampling pass the central recorder enumerates this
|
|
/// source and calls <see cref="CollectAsync"/>, which snapshots the same
|
|
/// point-in-time <c>SiteCalls</c> KPIs the live Health-dashboard tiles show —
|
|
/// global, per-source-site, and per-originating-node — into flat
|
|
/// <see cref="KpiSample"/> rows.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <para>
|
|
/// The cutoffs are derived from <see cref="SiteCallAuditOptions"/> exactly as the
|
|
/// live <c>SiteCallAuditActor</c> KPI handlers derive them
|
|
/// (<c>stuckCutoff = capturedAtUtc - StuckAgeThreshold</c>,
|
|
/// <c>intervalSince = capturedAtUtc - KpiInterval</c>). The COUNT metrics
|
|
/// (buffered, parked, failedLastInterval, deliveredLastInterval, stuck) equal the
|
|
/// live tile at the same instant; the <c>oldestPendingAgeSeconds</c> metric is
|
|
/// computed against the repository's internal clock and may differ from the live
|
|
/// tile by the query-execution latency. The recorder's <c>capturedAtUtc</c> is
|
|
/// the single anchor for both cutoffs.
|
|
/// </para>
|
|
/// <para>
|
|
/// Registered DI-scoped (next to the rest of the Site Call Audit composition) so
|
|
/// each sampling pass resolves a fresh repository scope, mirroring the actor's
|
|
/// scope-per-message repository access.
|
|
/// </para>
|
|
/// </remarks>
|
|
public sealed class SiteCallAuditKpiSampleSource : IKpiSampleSource
|
|
{
|
|
// ── Metric catalog (the M6-agreed metric-name strings for this source) ──
|
|
// Declaration order matches the emission order in AddSnapshot.
|
|
private const string MetricBuffered = "buffered";
|
|
private const string MetricParked = "parked";
|
|
private const string MetricFailedLastInterval = "failedLastInterval";
|
|
private const string MetricDeliveredLastInterval = "deliveredLastInterval";
|
|
private const string MetricStuck = "stuck";
|
|
private const string MetricOldestPendingAgeSeconds = "oldestPendingAgeSeconds";
|
|
|
|
private readonly ISiteCallAuditRepository _repository;
|
|
private readonly SiteCallAuditOptions _options;
|
|
|
|
/// <summary>
|
|
/// Creates the sample source over the central <c>SiteCalls</c> repository and
|
|
/// the Site Call Audit options that define the stuck-age + KPI-interval
|
|
/// windows.
|
|
/// </summary>
|
|
/// <param name="repository">The central <c>SiteCalls</c> operational-state repository.</param>
|
|
/// <param name="options">Site Call Audit windowing options (stuck-age + KPI interval).</param>
|
|
public SiteCallAuditKpiSampleSource(
|
|
ISiteCallAuditRepository repository,
|
|
IOptions<SiteCallAuditOptions> options)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(repository);
|
|
ArgumentNullException.ThrowIfNull(options);
|
|
|
|
_repository = repository;
|
|
_options = options.Value;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public string Source => KpiSources.SiteCallAudit;
|
|
|
|
/// <inheritdoc />
|
|
public async Task<IReadOnlyList<KpiSample>> CollectAsync(
|
|
DateTime capturedAtUtc, CancellationToken cancellationToken = default)
|
|
{
|
|
// Match the live SiteCallAuditActor KPI handlers: stuck cutoff and
|
|
// interval window are both anchored on the single capture instant.
|
|
var stuckCutoff = capturedAtUtc - _options.StuckAgeThreshold;
|
|
var intervalSince = capturedAtUtc - _options.KpiInterval;
|
|
|
|
var global = await _repository
|
|
.ComputeKpisAsync(stuckCutoff, intervalSince, cancellationToken)
|
|
.ConfigureAwait(false);
|
|
var perSite = await _repository
|
|
.ComputePerSiteKpisAsync(stuckCutoff, intervalSince, cancellationToken)
|
|
.ConfigureAwait(false);
|
|
var perNode = await _repository
|
|
.ComputePerNodeKpisAsync(stuckCutoff, intervalSince, cancellationToken)
|
|
.ConfigureAwait(false);
|
|
|
|
var samples = new List<KpiSample>();
|
|
|
|
// Global scope (null ScopeKey).
|
|
AddSnapshot(
|
|
samples, capturedAtUtc, KpiScopes.Global, scopeKey: null,
|
|
global.BufferedCount, global.ParkedCount,
|
|
global.FailedLastInterval, global.DeliveredLastInterval,
|
|
global.OldestPendingAge, global.StuckCount);
|
|
|
|
// Per-site scope (ScopeKey = source site id).
|
|
foreach (var site in perSite)
|
|
{
|
|
AddSnapshot(
|
|
samples, capturedAtUtc, KpiScopes.Site, scopeKey: site.SourceSite,
|
|
site.BufferedCount, site.ParkedCount,
|
|
site.FailedLastInterval, site.DeliveredLastInterval,
|
|
site.OldestPendingAge, site.StuckCount);
|
|
}
|
|
|
|
// Per-node scope (ScopeKey = node name).
|
|
foreach (var node in perNode)
|
|
{
|
|
AddSnapshot(
|
|
samples, capturedAtUtc, KpiScopes.Node, scopeKey: node.SourceNode,
|
|
node.BufferedCount, node.ParkedCount,
|
|
node.FailedLastInterval, node.DeliveredLastInterval,
|
|
node.OldestPendingAge, node.StuckCount);
|
|
}
|
|
|
|
return samples;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Appends the six-metric catalog for one snapshot at the given scope. The
|
|
/// oldest-pending-age metric is omitted when the snapshot's age is
|
|
/// <c>null</c> (no non-terminal rows) rather than written as zero.
|
|
/// </summary>
|
|
private void AddSnapshot(
|
|
List<KpiSample> samples,
|
|
DateTime capturedAtUtc,
|
|
string scope,
|
|
string? scopeKey,
|
|
int buffered,
|
|
int parked,
|
|
int failedLastInterval,
|
|
int deliveredLastInterval,
|
|
TimeSpan? oldestPendingAge,
|
|
int stuck)
|
|
{
|
|
samples.Add(Sample(capturedAtUtc, MetricBuffered, scope, scopeKey, buffered));
|
|
samples.Add(Sample(capturedAtUtc, MetricParked, scope, scopeKey, parked));
|
|
samples.Add(Sample(capturedAtUtc, MetricFailedLastInterval, scope, scopeKey, failedLastInterval));
|
|
samples.Add(Sample(capturedAtUtc, MetricDeliveredLastInterval, scope, scopeKey, deliveredLastInterval));
|
|
samples.Add(Sample(capturedAtUtc, MetricStuck, scope, scopeKey, stuck));
|
|
|
|
if (oldestPendingAge is { } age)
|
|
{
|
|
samples.Add(Sample(
|
|
capturedAtUtc, MetricOldestPendingAgeSeconds, scope, scopeKey, age.TotalSeconds));
|
|
}
|
|
}
|
|
|
|
private KpiSample Sample(
|
|
DateTime capturedAtUtc, string metric, string scope, string? scopeKey, double value) =>
|
|
new()
|
|
{
|
|
Source = KpiSources.SiteCallAudit,
|
|
Metric = metric,
|
|
Scope = scope,
|
|
ScopeKey = scopeKey,
|
|
Value = value,
|
|
CapturedAtUtc = capturedAtUtc,
|
|
};
|
|
}
|