feat(kpi): K3 — KpiHistory project + options/validator + AddKpiHistory

This commit is contained in:
Joseph Doherty
2026-06-17 19:45:07 -04:00
parent cabc557629
commit 9ffa34d3e7
7 changed files with 341 additions and 0 deletions
@@ -0,0 +1,38 @@
namespace ZB.MOM.WW.ScadaBridge.KpiHistory;
/// <summary>
/// Configuration for the KPI History (#26, M6) component. Bound from the
/// <c>ScadaBridge:KpiHistory</c> section of <c>appsettings.json</c>. Defaults
/// reflect the design: a 60-second sampling cadence for the point-in-time
/// Notification Outbox / Site Call Audit KPI snapshots, a 90-day central
/// retention window with a daily purge sweep, and a 200-point default ceiling
/// on the series returned to the Central UI trend charts.
/// </summary>
public sealed class KpiHistoryOptions
{
/// <summary>
/// How often the recorder captures a KPI sample row (default 60 s). Must be
/// strictly positive.
/// </summary>
public TimeSpan SampleInterval { get; set; } = TimeSpan.FromSeconds(60);
/// <summary>
/// Central retention window in days for recorded KPI samples (default 90,
/// range <c>[1, 3650]</c>). Rows older than this are dropped by the purge
/// sweep.
/// </summary>
public int RetentionDays { get; set; } = 90;
/// <summary>
/// How often the purge sweep drops expired sample rows (default 1 day). Must
/// be strictly positive.
/// </summary>
public TimeSpan PurgeInterval { get; set; } = TimeSpan.FromDays(1);
/// <summary>
/// Default ceiling on the number of points returned in a single trend series
/// query when the caller does not specify one (default 200, range
/// <c>[2, 5000]</c>). At least two points are required to draw a line.
/// </summary>
public int DefaultMaxSeriesPoints { get; set; } = 200;
}
@@ -0,0 +1,58 @@
using ZB.MOM.WW.Configuration;
namespace ZB.MOM.WW.ScadaBridge.KpiHistory;
/// <summary>
/// Validates <see cref="KpiHistoryOptions"/> on startup. A non-positive
/// <see cref="KpiHistoryOptions.SampleInterval"/> or
/// <see cref="KpiHistoryOptions.PurgeInterval"/> would stall the recorder /
/// purge loops, so both are required to be strictly positive.
/// <see cref="KpiHistoryOptions.RetentionDays"/> is bounded to <c>[1, 3650]</c>
/// to keep the purge window sane (too short drops trends still being viewed;
/// too long defeats the retention purpose). <see cref="KpiHistoryOptions.DefaultMaxSeriesPoints"/>
/// is bounded to <c>[2, 5000]</c> — at least two points are needed to draw a
/// line, and an unbounded ceiling would let a single trend query stream an
/// arbitrarily large series to the Central UI.
/// </summary>
public sealed class KpiHistoryOptionsValidator : OptionsValidatorBase<KpiHistoryOptions>
{
/// <summary>Inclusive lower bound for <see cref="KpiHistoryOptions.RetentionDays"/>.</summary>
public const int MinRetentionDays = 1;
/// <summary>Inclusive upper bound for <see cref="KpiHistoryOptions.RetentionDays"/>.</summary>
public const int MaxRetentionDays = 3650;
/// <summary>Inclusive lower bound for <see cref="KpiHistoryOptions.DefaultMaxSeriesPoints"/>.</summary>
public const int MinDefaultMaxSeriesPoints = 2;
/// <summary>Inclusive upper bound for <see cref="KpiHistoryOptions.DefaultMaxSeriesPoints"/>.</summary>
public const int MaxDefaultMaxSeriesPoints = 5000;
/// <inheritdoc />
protected override void Validate(ValidationBuilder builder, KpiHistoryOptions options)
{
builder.RequireThat(options.SampleInterval > TimeSpan.Zero,
$"ScadaBridge:KpiHistory:{nameof(KpiHistoryOptions.SampleInterval)} ({options.SampleInterval}) " +
"must be > 0; it is the recorder's sampling cadence.");
// Valid when RetentionDays is within [Min, Max] inclusive. The De Morgan'd
// guard !(below Min OR above Max) is equivalent to (>= Min AND <= Max).
builder.RequireThat(
!(options.RetentionDays < MinRetentionDays || options.RetentionDays > MaxRetentionDays),
$"ScadaBridge:KpiHistory:{nameof(KpiHistoryOptions.RetentionDays)} ({options.RetentionDays}) " +
$"must be in [{MinRetentionDays}, {MaxRetentionDays}] days.");
builder.RequireThat(options.PurgeInterval > TimeSpan.Zero,
$"ScadaBridge:KpiHistory:{nameof(KpiHistoryOptions.PurgeInterval)} ({options.PurgeInterval}) " +
"must be > 0; it is the purge-sweep cadence.");
// Valid when DefaultMaxSeriesPoints is within [Min, Max] inclusive. The
// De Morgan'd guard !(below Min OR above Max) is equivalent to
// (>= Min AND <= Max).
builder.RequireThat(
!(options.DefaultMaxSeriesPoints < MinDefaultMaxSeriesPoints
|| options.DefaultMaxSeriesPoints > MaxDefaultMaxSeriesPoints),
$"ScadaBridge:KpiHistory:{nameof(KpiHistoryOptions.DefaultMaxSeriesPoints)} ({options.DefaultMaxSeriesPoints}) " +
$"must be in [{MinDefaultMaxSeriesPoints}, {MaxDefaultMaxSeriesPoints}].");
}
}
@@ -0,0 +1,44 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using ZB.MOM.WW.Configuration;
namespace ZB.MOM.WW.ScadaBridge.KpiHistory;
/// <summary>
/// Composition root for the KPI History (#26, M6) component.
/// </summary>
/// <remarks>
/// <para>
/// K3 scaffolds the component: it binds and validates
/// <see cref="KpiHistoryOptions"/>. The recorder actor itself is created via
/// <c>Props</c> in the Host on the active central node (K4/K5), not registered
/// here — mirroring the Notification Outbox (#21) singleton wiring.
/// </para>
/// </remarks>
public static class ServiceCollectionExtensions
{
/// <summary>Configuration section bound to <see cref="KpiHistoryOptions"/>.</summary>
public const string OptionsSection = "ScadaBridge:KpiHistory";
/// <summary>
/// Registers the KPI History (#26) component services: the validated
/// <see cref="KpiHistoryOptions"/> binding. Idempotent re-registration of the
/// validator is handled by the shared <c>AddValidatedOptions</c> helper
/// (<c>TryAddEnumerable</c>); the options binding itself is call-once per
/// <see cref="IServiceCollection"/>.
/// </summary>
/// <param name="services">The service collection to register into.</param>
/// <param name="configuration">Application configuration used to bind <see cref="KpiHistoryOptions"/>.</param>
/// <returns>The same <see cref="IServiceCollection"/> for chaining.</returns>
public static IServiceCollection AddKpiHistory(this IServiceCollection services, IConfiguration configuration)
{
ArgumentNullException.ThrowIfNull(services);
ArgumentNullException.ThrowIfNull(configuration);
// Binds the "ScadaBridge:KpiHistory" section, registers the validator,
// and enables ValidateOnStart in one call — same shape as AddAuditLog.
services.AddValidatedOptions<KpiHistoryOptions, KpiHistoryOptionsValidator>(configuration, OptionsSection);
return services;
}
}
@@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
<PackageReference Include="Microsoft.Extensions.Options" />
<PackageReference Include="ZB.MOM.WW.Configuration" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="../ZB.MOM.WW.ScadaBridge.Commons/ZB.MOM.WW.ScadaBridge.Commons.csproj" />
</ItemGroup>
</Project>