feat(kpi): K1 — KpiSample + IKpiSampleSource + IKpiHistoryRepository contracts (Commons)
This commit is contained in:
@@ -0,0 +1,55 @@
|
||||
namespace ZB.MOM.WW.ScadaBridge.Commons.Entities.Kpi;
|
||||
|
||||
/// <summary>
|
||||
/// A single point-in-time KPI measurement in the central KPI-history backbone
|
||||
/// (M6 "KPI History & Trends"). One row per (Source, Metric, Scope, ScopeKey)
|
||||
/// captured by the recorder singleton at a sampling instant — a tall / EAV row in
|
||||
/// the central <c>KpiSample</c> table.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Persistence-ignorant POCO; the EF Core mapping lives in the Configuration
|
||||
/// Database component. <see cref="Source"/> draws from
|
||||
/// <see cref="ZB.MOM.WW.ScadaBridge.Commons.Types.Kpi.KpiSources"/> and
|
||||
/// <see cref="Scope"/> from
|
||||
/// <see cref="ZB.MOM.WW.ScadaBridge.Commons.Types.Kpi.KpiScopes"/>; <see cref="Metric"/>
|
||||
/// is drawn from each owning source's own metric catalog.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <see cref="Value"/> is a <see cref="double"/> that carries counts exactly and
|
||||
/// ages as seconds. <see cref="CapturedAtUtc"/> is UTC, like every timestamp in
|
||||
/// the system.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public sealed class KpiSample
|
||||
{
|
||||
/// <summary>Surrogate identity key assigned by the store.</summary>
|
||||
public long Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Owning component / source — a value from
|
||||
/// <see cref="ZB.MOM.WW.ScadaBridge.Commons.Types.Kpi.KpiSources"/>.
|
||||
/// </summary>
|
||||
public required string Source { get; set; }
|
||||
|
||||
/// <summary>Metric name, drawn from the owning source's metric catalog.</summary>
|
||||
public required string Metric { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Scope discriminator — a value from
|
||||
/// <see cref="ZB.MOM.WW.ScadaBridge.Commons.Types.Kpi.KpiScopes"/>.
|
||||
/// </summary>
|
||||
public required string Scope { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Scope qualifier — the site id or node name the sample belongs to;
|
||||
/// <c>null</c> for <see cref="ZB.MOM.WW.ScadaBridge.Commons.Types.Kpi.KpiScopes.Global"/>.
|
||||
/// </summary>
|
||||
public string? ScopeKey { get; set; }
|
||||
|
||||
/// <summary>Measured value — counts are exact; ages are expressed in seconds.</summary>
|
||||
public double Value { get; set; }
|
||||
|
||||
/// <summary>The UTC instant at which the sample was captured.</summary>
|
||||
public DateTime CapturedAtUtc { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Kpi;
|
||||
|
||||
namespace ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Kpi;
|
||||
|
||||
/// <summary>
|
||||
/// A DI-registered provider of KPI samples (M6 "KPI History & Trends"),
|
||||
/// implemented by each owning component and enumerated by the central recorder
|
||||
/// singleton at every sampling interval. The recorder stamps a single
|
||||
/// <c>capturedAtUtc</c>, fans out to every source's <see cref="CollectAsync"/>,
|
||||
/// and bulk-writes the combined samples.
|
||||
/// </summary>
|
||||
public interface IKpiSampleSource
|
||||
{
|
||||
/// <summary>
|
||||
/// The source this provider reports for — a value from
|
||||
/// <see cref="ZB.MOM.WW.ScadaBridge.Commons.Types.Kpi.KpiSources"/>. Every
|
||||
/// <see cref="KpiSample"/> returned by <see cref="CollectAsync"/> carries this value
|
||||
/// in <see cref="KpiSample.Source"/>.
|
||||
/// </summary>
|
||||
string Source { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Compute this source's KPIs as-of-now, stamping each returned sample's
|
||||
/// <see cref="KpiSample.CapturedAtUtc"/> with <paramref name="capturedAtUtc"/>.
|
||||
/// </summary>
|
||||
/// <param name="capturedAtUtc">The shared UTC capture instant for this sampling pass.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>
|
||||
/// A task resolving to the samples computed for this source; empty when there is
|
||||
/// nothing to report.
|
||||
/// </returns>
|
||||
Task<IReadOnlyList<KpiSample>> CollectAsync(DateTime capturedAtUtc, CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Kpi;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Types.Kpi;
|
||||
|
||||
namespace ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// Data access for the central KPI-history backbone (M6 "KPI History & Trends")
|
||||
/// — the tall / EAV <c>KpiSample</c> table in central MS SQL. Backs the recorder
|
||||
/// singleton's bulk write, the bucketed query path that feeds the reusable trend
|
||||
/// chart, and the retention purge. Implementation lives in the Configuration
|
||||
/// Database component.
|
||||
/// </summary>
|
||||
public interface IKpiHistoryRepository
|
||||
{
|
||||
/// <summary>
|
||||
/// Bulk-inserts a batch of captured samples (one sampling pass across all
|
||||
/// registered sources).
|
||||
/// </summary>
|
||||
/// <param name="samples">The samples to record.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>A task that represents the asynchronous write.</returns>
|
||||
Task RecordSamplesAsync(IReadOnlyCollection<KpiSample> samples, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Returns the raw points for one series in <c>[fromUtc, toUtc]</c>, ordered by
|
||||
/// <see cref="KpiSample.CapturedAtUtc"/> ascending.
|
||||
/// </summary>
|
||||
/// <param name="source">Source identifier — a value from <see cref="KpiSources"/>.</param>
|
||||
/// <param name="metric">Metric name from the source's catalog.</param>
|
||||
/// <param name="scope">Scope discriminator — a value from <see cref="KpiScopes"/>.</param>
|
||||
/// <param name="scopeKey">Scope qualifier (site id / node name); <c>null</c> for the Global scope.</param>
|
||||
/// <param name="fromUtc">Inclusive lower bound of the time window (UTC).</param>
|
||||
/// <param name="toUtc">Inclusive upper bound of the time window (UTC).</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>
|
||||
/// A task resolving to the raw series points in ascending capture order; empty when
|
||||
/// the series has no samples in the window.
|
||||
/// </returns>
|
||||
Task<IReadOnlyList<KpiSeriesPoint>> GetRawSeriesAsync(
|
||||
string source, string metric, string scope, string? scopeKey,
|
||||
DateTime fromUtc, DateTime toUtc, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Bulk-deletes rows whose <see cref="KpiSample.CapturedAtUtc"/> is strictly older
|
||||
/// than <paramref name="before"/> (retention purge).
|
||||
/// </summary>
|
||||
/// <param name="before">UTC cut-off; rows captured before this instant are deleted.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>A task resolving to the number of rows deleted.</returns>
|
||||
Task<int> PurgeOlderThanAsync(DateTime before, CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
namespace ZB.MOM.WW.ScadaBridge.Commons.Types.Kpi;
|
||||
|
||||
/// <summary>
|
||||
/// Canonical KPI scope discriminators (M6 "KPI History & Trends") — the value of
|
||||
/// <see cref="ZB.MOM.WW.ScadaBridge.Commons.Entities.Kpi.KpiSample.Scope"/>. Each
|
||||
/// constant's value equals its name. The companion
|
||||
/// <see cref="ZB.MOM.WW.ScadaBridge.Commons.Entities.Kpi.KpiSample.ScopeKey"/> qualifies
|
||||
/// the scope: <c>null</c> for <see cref="Global"/>, the site id for <see cref="Site"/>,
|
||||
/// the node name for <see cref="Node"/>.
|
||||
/// </summary>
|
||||
public static class KpiScopes
|
||||
{
|
||||
/// <summary>System-wide sample; <c>ScopeKey</c> is <c>null</c>.</summary>
|
||||
public const string Global = "Global";
|
||||
|
||||
/// <summary>Per-site sample; <c>ScopeKey</c> is the site id.</summary>
|
||||
public const string Site = "Site";
|
||||
|
||||
/// <summary>Per-node sample; <c>ScopeKey</c> is the node name.</summary>
|
||||
public const string Node = "Node";
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace ZB.MOM.WW.ScadaBridge.Commons.Types.Kpi;
|
||||
|
||||
/// <summary>
|
||||
/// A single bucketed point of a KPI series (M6 "KPI History & Trends") — the
|
||||
/// aggregated value of one series over the bucket starting at
|
||||
/// <see cref="BucketStartUtc"/>. Returned by the bucketed query path and the
|
||||
/// reusable trend chart.
|
||||
/// </summary>
|
||||
/// <param name="BucketStartUtc">UTC start of the time bucket this point covers.</param>
|
||||
/// <param name="Value">Aggregated value for the bucket — counts exact; ages as seconds.</param>
|
||||
public readonly record struct KpiSeriesPoint(DateTime BucketStartUtc, double Value);
|
||||
@@ -0,0 +1,22 @@
|
||||
namespace ZB.MOM.WW.ScadaBridge.Commons.Types.Kpi;
|
||||
|
||||
/// <summary>
|
||||
/// Canonical KPI source identifiers (M6 "KPI History & Trends") — the value of
|
||||
/// <see cref="ZB.MOM.WW.ScadaBridge.Commons.Entities.Kpi.KpiSample.Source"/> and the
|
||||
/// <see cref="ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Kpi.IKpiSampleSource.Source"/>
|
||||
/// each owning component reports. Each constant's value equals its name.
|
||||
/// </summary>
|
||||
public static class KpiSources
|
||||
{
|
||||
/// <summary>Notification Outbox (#21) delivery KPIs.</summary>
|
||||
public const string NotificationOutbox = "NotificationOutbox";
|
||||
|
||||
/// <summary>Site Call Audit (#22) cached-call KPIs.</summary>
|
||||
public const string SiteCallAudit = "SiteCallAudit";
|
||||
|
||||
/// <summary>Audit Log (#23) ingest / backlog KPIs.</summary>
|
||||
public const string AuditLog = "AuditLog";
|
||||
|
||||
/// <summary>Site Health (#11) monitoring KPIs.</summary>
|
||||
public const string SiteHealth = "SiteHealth";
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Kpi;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Types.Kpi;
|
||||
|
||||
namespace ZB.MOM.WW.ScadaBridge.Commons.Tests.Kpi;
|
||||
|
||||
public class KpiSampleTests
|
||||
{
|
||||
[Fact]
|
||||
public void KpiSample_ConstructedWithRequiredMembers_RoundTripsValues()
|
||||
{
|
||||
var capturedAt = new DateTime(2026, 6, 15, 12, 30, 0, DateTimeKind.Utc);
|
||||
|
||||
var sample = new KpiSample
|
||||
{
|
||||
Id = 42,
|
||||
Source = KpiSources.NotificationOutbox,
|
||||
Metric = "QueueDepth",
|
||||
Scope = KpiScopes.Global,
|
||||
ScopeKey = null,
|
||||
Value = 17,
|
||||
CapturedAtUtc = capturedAt,
|
||||
};
|
||||
|
||||
Assert.Equal(42, sample.Id);
|
||||
Assert.Equal("NotificationOutbox", sample.Source);
|
||||
Assert.Equal("QueueDepth", sample.Metric);
|
||||
Assert.Equal("Global", sample.Scope);
|
||||
Assert.Null(sample.ScopeKey);
|
||||
Assert.Equal(17, sample.Value);
|
||||
Assert.Equal(capturedAt, sample.CapturedAtUtc);
|
||||
Assert.Equal(DateTimeKind.Utc, sample.CapturedAtUtc.Kind);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void KpiSample_ScopedSample_CarriesScopeKey()
|
||||
{
|
||||
var sample = new KpiSample
|
||||
{
|
||||
Source = KpiSources.SiteHealth,
|
||||
Metric = "OldestPendingAgeSeconds",
|
||||
Scope = KpiScopes.Site,
|
||||
ScopeKey = "site-a",
|
||||
Value = 12.5,
|
||||
CapturedAtUtc = DateTime.UtcNow,
|
||||
};
|
||||
|
||||
Assert.Equal(KpiScopes.Site, sample.Scope);
|
||||
Assert.Equal("site-a", sample.ScopeKey);
|
||||
Assert.Equal(12.5, sample.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void KpiScopesAndSources_ConstantValues_EqualTheirNames()
|
||||
{
|
||||
Assert.Equal("NotificationOutbox", KpiSources.NotificationOutbox);
|
||||
Assert.Equal("SiteCallAudit", KpiSources.SiteCallAudit);
|
||||
Assert.Equal("AuditLog", KpiSources.AuditLog);
|
||||
Assert.Equal("SiteHealth", KpiSources.SiteHealth);
|
||||
|
||||
Assert.Equal("Global", KpiScopes.Global);
|
||||
Assert.Equal("Site", KpiScopes.Site);
|
||||
Assert.Equal("Node", KpiScopes.Node);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void KpiSeriesPoint_IsValueRecordStruct_WithComponentEquality()
|
||||
{
|
||||
var bucket = new DateTime(2026, 6, 15, 0, 0, 0, DateTimeKind.Utc);
|
||||
|
||||
var a = new KpiSeriesPoint(bucket, 3.0);
|
||||
var b = new KpiSeriesPoint(bucket, 3.0);
|
||||
|
||||
Assert.Equal(bucket, a.BucketStartUtc);
|
||||
Assert.Equal(3.0, a.Value);
|
||||
Assert.Equal(a, b);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user