diff --git a/src/ZB.MOM.WW.ScadaBridge.Commons/Entities/Kpi/KpiSample.cs b/src/ZB.MOM.WW.ScadaBridge.Commons/Entities/Kpi/KpiSample.cs new file mode 100644 index 00000000..f73d8f4a --- /dev/null +++ b/src/ZB.MOM.WW.ScadaBridge.Commons/Entities/Kpi/KpiSample.cs @@ -0,0 +1,55 @@ +namespace ZB.MOM.WW.ScadaBridge.Commons.Entities.Kpi; + +/// +/// 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 KpiSample table. +/// +/// +/// +/// Persistence-ignorant POCO; the EF Core mapping lives in the Configuration +/// Database component. draws from +/// and +/// from +/// ; +/// is drawn from each owning source's own metric catalog. +/// +/// +/// is a that carries counts exactly and +/// ages as seconds. is UTC, like every timestamp in +/// the system. +/// +/// +public sealed class KpiSample +{ + /// Surrogate identity key assigned by the store. + public long Id { get; set; } + + /// + /// Owning component / source — a value from + /// . + /// + public required string Source { get; set; } + + /// Metric name, drawn from the owning source's metric catalog. + public required string Metric { get; set; } + + /// + /// Scope discriminator — a value from + /// . + /// + public required string Scope { get; set; } + + /// + /// Scope qualifier — the site id or node name the sample belongs to; + /// null for . + /// + public string? ScopeKey { get; set; } + + /// Measured value — counts are exact; ages are expressed in seconds. + public double Value { get; set; } + + /// The UTC instant at which the sample was captured. + public DateTime CapturedAtUtc { get; set; } +} diff --git a/src/ZB.MOM.WW.ScadaBridge.Commons/Interfaces/Kpi/IKpiSampleSource.cs b/src/ZB.MOM.WW.ScadaBridge.Commons/Interfaces/Kpi/IKpiSampleSource.cs new file mode 100644 index 00000000..1c866fda --- /dev/null +++ b/src/ZB.MOM.WW.ScadaBridge.Commons/Interfaces/Kpi/IKpiSampleSource.cs @@ -0,0 +1,33 @@ +using ZB.MOM.WW.ScadaBridge.Commons.Entities.Kpi; + +namespace ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Kpi; + +/// +/// 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 +/// capturedAtUtc, fans out to every source's , +/// and bulk-writes the combined samples. +/// +public interface IKpiSampleSource +{ + /// + /// The source this provider reports for — a value from + /// . Every + /// returned by carries this value + /// in . + /// + string Source { get; } + + /// + /// Compute this source's KPIs as-of-now, stamping each returned sample's + /// with . + /// + /// The shared UTC capture instant for this sampling pass. + /// Cancellation token. + /// + /// A task resolving to the samples computed for this source; empty when there is + /// nothing to report. + /// + Task> CollectAsync(DateTime capturedAtUtc, CancellationToken cancellationToken = default); +} diff --git a/src/ZB.MOM.WW.ScadaBridge.Commons/Interfaces/Repositories/IKpiHistoryRepository.cs b/src/ZB.MOM.WW.ScadaBridge.Commons/Interfaces/Repositories/IKpiHistoryRepository.cs new file mode 100644 index 00000000..d06fcb62 --- /dev/null +++ b/src/ZB.MOM.WW.ScadaBridge.Commons/Interfaces/Repositories/IKpiHistoryRepository.cs @@ -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; + +/// +/// Data access for the central KPI-history backbone (M6 "KPI History & Trends") +/// — the tall / EAV KpiSample 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. +/// +public interface IKpiHistoryRepository +{ + /// + /// Bulk-inserts a batch of captured samples (one sampling pass across all + /// registered sources). + /// + /// The samples to record. + /// Cancellation token. + /// A task that represents the asynchronous write. + Task RecordSamplesAsync(IReadOnlyCollection samples, CancellationToken cancellationToken = default); + + /// + /// Returns the raw points for one series in [fromUtc, toUtc], ordered by + /// ascending. + /// + /// Source identifier — a value from . + /// Metric name from the source's catalog. + /// Scope discriminator — a value from . + /// Scope qualifier (site id / node name); null for the Global scope. + /// Inclusive lower bound of the time window (UTC). + /// Inclusive upper bound of the time window (UTC). + /// Cancellation token. + /// + /// A task resolving to the raw series points in ascending capture order; empty when + /// the series has no samples in the window. + /// + Task> GetRawSeriesAsync( + string source, string metric, string scope, string? scopeKey, + DateTime fromUtc, DateTime toUtc, CancellationToken cancellationToken = default); + + /// + /// Bulk-deletes rows whose is strictly older + /// than (retention purge). + /// + /// UTC cut-off; rows captured before this instant are deleted. + /// Cancellation token. + /// A task resolving to the number of rows deleted. + Task PurgeOlderThanAsync(DateTime before, CancellationToken cancellationToken = default); +} diff --git a/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Kpi/KpiScopes.cs b/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Kpi/KpiScopes.cs new file mode 100644 index 00000000..4104b523 --- /dev/null +++ b/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Kpi/KpiScopes.cs @@ -0,0 +1,21 @@ +namespace ZB.MOM.WW.ScadaBridge.Commons.Types.Kpi; + +/// +/// Canonical KPI scope discriminators (M6 "KPI History & Trends") — the value of +/// . Each +/// constant's value equals its name. The companion +/// qualifies +/// the scope: null for , the site id for , +/// the node name for . +/// +public static class KpiScopes +{ + /// System-wide sample; ScopeKey is null. + public const string Global = "Global"; + + /// Per-site sample; ScopeKey is the site id. + public const string Site = "Site"; + + /// Per-node sample; ScopeKey is the node name. + public const string Node = "Node"; +} diff --git a/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Kpi/KpiSeriesPoint.cs b/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Kpi/KpiSeriesPoint.cs new file mode 100644 index 00000000..29f91cae --- /dev/null +++ b/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Kpi/KpiSeriesPoint.cs @@ -0,0 +1,11 @@ +namespace ZB.MOM.WW.ScadaBridge.Commons.Types.Kpi; + +/// +/// A single bucketed point of a KPI series (M6 "KPI History & Trends") — the +/// aggregated value of one series over the bucket starting at +/// . Returned by the bucketed query path and the +/// reusable trend chart. +/// +/// UTC start of the time bucket this point covers. +/// Aggregated value for the bucket — counts exact; ages as seconds. +public readonly record struct KpiSeriesPoint(DateTime BucketStartUtc, double Value); diff --git a/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Kpi/KpiSources.cs b/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Kpi/KpiSources.cs new file mode 100644 index 00000000..f173f7cb --- /dev/null +++ b/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Kpi/KpiSources.cs @@ -0,0 +1,22 @@ +namespace ZB.MOM.WW.ScadaBridge.Commons.Types.Kpi; + +/// +/// Canonical KPI source identifiers (M6 "KPI History & Trends") — the value of +/// and the +/// +/// each owning component reports. Each constant's value equals its name. +/// +public static class KpiSources +{ + /// Notification Outbox (#21) delivery KPIs. + public const string NotificationOutbox = "NotificationOutbox"; + + /// Site Call Audit (#22) cached-call KPIs. + public const string SiteCallAudit = "SiteCallAudit"; + + /// Audit Log (#23) ingest / backlog KPIs. + public const string AuditLog = "AuditLog"; + + /// Site Health (#11) monitoring KPIs. + public const string SiteHealth = "SiteHealth"; +} diff --git a/tests/ZB.MOM.WW.ScadaBridge.Commons.Tests/Kpi/KpiSampleTests.cs b/tests/ZB.MOM.WW.ScadaBridge.Commons.Tests/Kpi/KpiSampleTests.cs new file mode 100644 index 00000000..1d02c8a1 --- /dev/null +++ b/tests/ZB.MOM.WW.ScadaBridge.Commons.Tests/Kpi/KpiSampleTests.cs @@ -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); + } +}