feat(historian): drain/capacity/retention config knobs + startup config-warning validation

This commit is contained in:
Joseph Doherty
2026-06-11 13:04:16 -04:00
parent 61b230d79a
commit f215982b93
4 changed files with 84 additions and 3 deletions
@@ -15,6 +15,9 @@
"Enabled": false,
"DatabasePath": "alarm-historian.db",
"PipeName": "OtOpcUaHistorian",
"SharedSecret": ""
"SharedSecret": "",
"DrainIntervalSeconds": 5,
"Capacity": 1000000,
"DeadLetterRetentionDays": 30
}
}
@@ -1,3 +1,6 @@
using System.Collections.Generic;
using System.IO;
namespace ZB.MOM.WW.OtOpcUa.Runtime.Historian;
/// <summary>
@@ -29,4 +32,28 @@ public sealed class AlarmHistorianOptions
/// <summary>Maximum number of queued rows the drain worker forwards in a single batch.</summary>
public int BatchSize { get; init; } = 100;
/// <summary>Seconds between drain-worker ticks. Defaults to 5.</summary>
public int DrainIntervalSeconds { get; init; } = 5;
/// <summary>Maximum queued rows before the sink evicts the oldest. Defaults to 1,000,000
/// (matches <c>SqliteStoreAndForwardSink</c>'s <c>DefaultCapacity</c>).</summary>
public long Capacity { get; init; } = 1_000_000;
/// <summary>Days to retain dead-lettered rows before purge. Defaults to 30.</summary>
public int DeadLetterRetentionDays { get; init; } = 30;
/// <summary>Returns operator-facing misconfiguration warnings for an <c>Enabled</c> historian
/// (empty when disabled or correctly configured). Pure — the registration logs each entry.</summary>
/// <returns>Zero or more human-readable warning messages.</returns>
public IReadOnlyList<string> Validate()
{
var warnings = new List<string>();
if (!Enabled) return warnings;
if (string.IsNullOrWhiteSpace(SharedSecret))
warnings.Add("AlarmHistorian:SharedSecret is empty while the historian is enabled — the Wonderware sidecar Hello frame will carry an empty secret.");
if (!Path.IsPathRooted(DatabasePath))
warnings.Add($"AlarmHistorian:DatabasePath '{DatabasePath}' is relative — it resolves against the process working directory (e.g. System32 for a Windows service). Set an absolute path.");
return warnings;
}
}
@@ -72,6 +72,9 @@ public static class ServiceCollectionExtensions
var opts = configuration.GetSection(AlarmHistorianOptions.SectionName).Get<AlarmHistorianOptions>();
if (opts is not { Enabled: true }) return services; // leave the Null default from AddOtOpcUaRuntime
foreach (var warning in opts.Validate())
Serilog.Log.Logger.ForContext<SqliteStoreAndForwardSink>().Warning("{HistorianConfigWarning}", warning);
services.AddSingleton<IAlarmHistorianSink>(sp =>
{
// SqliteStoreAndForwardSink takes a Serilog ILogger (not Microsoft.Extensions.Logging).
@@ -81,8 +84,10 @@ public static class ServiceCollectionExtensions
opts.DatabasePath,
writerFactory(opts, sp),
Serilog.Log.Logger.ForContext<SqliteStoreAndForwardSink>(),
batchSize: opts.BatchSize);
sink.StartDrainLoop(TimeSpan.FromSeconds(5));
batchSize: opts.BatchSize,
capacity: opts.Capacity,
deadLetterRetention: TimeSpan.FromDays(opts.DeadLetterRetentionDays));
sink.StartDrainLoop(TimeSpan.FromSeconds(opts.DrainIntervalSeconds));
return sink;
});
return services;