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;
///
/// Site Call Audit (#22) for the M6 "KPI History
/// & Trends" backbone. Each sampling pass the central recorder enumerates this
/// source and calls , which snapshots the same
/// point-in-time SiteCalls KPIs the live Health-dashboard tiles show —
/// global, per-source-site, and per-originating-node — into flat
/// rows.
///
///
///
/// The cutoffs are derived from exactly as the
/// live SiteCallAuditActor KPI handlers derive them
/// (stuckCutoff = capturedAtUtc - StuckAgeThreshold,
/// intervalSince = capturedAtUtc - KpiInterval). The COUNT metrics
/// (buffered, parked, failedLastInterval, deliveredLastInterval, stuck) equal the
/// live tile at the same instant; the oldestPendingAgeSeconds metric is
/// computed against the repository's internal clock and may differ from the live
/// tile by the query-execution latency. The recorder's capturedAtUtc is
/// the single anchor for both cutoffs.
///
///
/// 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.
///
///
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;
///
/// Creates the sample source over the central SiteCalls repository and
/// the Site Call Audit options that define the stuck-age + KPI-interval
/// windows.
///
/// The central SiteCalls operational-state repository.
/// Site Call Audit windowing options (stuck-age + KPI interval).
public SiteCallAuditKpiSampleSource(
ISiteCallAuditRepository repository,
IOptions options)
{
ArgumentNullException.ThrowIfNull(repository);
ArgumentNullException.ThrowIfNull(options);
_repository = repository;
_options = options.Value;
}
///
public string Source => KpiSources.SiteCallAudit;
///
public async Task> 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();
// 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;
}
///
/// 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
/// null (no non-terminal rows) rather than written as zero.
///
private void AddSnapshot(
List 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,
};
}