feat(historian): drain/capacity/retention config knobs + startup config-warning validation
This commit is contained in:
@@ -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;
|
||||
|
||||
+46
@@ -4,6 +4,7 @@ using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.OtOpcUa.Core.AlarmHistorian;
|
||||
using ZB.MOM.WW.OtOpcUa.Runtime.Historian;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Runtime.Tests.Historian;
|
||||
|
||||
@@ -91,4 +92,49 @@ public sealed class AlarmHistorianRegistrationTests
|
||||
try { Directory.Delete(tempDir, recursive: true); } catch (IOException) { /* best effort */ }
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Section_binds_drain_capacity_and_retention_knobs()
|
||||
{
|
||||
var config = ConfigFrom(new Dictionary<string, string?>
|
||||
{
|
||||
["AlarmHistorian:Enabled"] = "true",
|
||||
["AlarmHistorian:DrainIntervalSeconds"] = "11",
|
||||
["AlarmHistorian:Capacity"] = "500",
|
||||
["AlarmHistorian:DeadLetterRetentionDays"] = "7",
|
||||
});
|
||||
|
||||
var opts = config.GetSection(AlarmHistorianOptions.SectionName).Get<AlarmHistorianOptions>();
|
||||
|
||||
opts.ShouldNotBeNull();
|
||||
opts.DrainIntervalSeconds.ShouldBe(11);
|
||||
opts.Capacity.ShouldBe(500);
|
||||
opts.DeadLetterRetentionDays.ShouldBe(7);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_warns_on_empty_shared_secret_when_enabled()
|
||||
{
|
||||
var opts = new AlarmHistorianOptions { Enabled = true, SharedSecret = "", DatabasePath = "/var/h.db" };
|
||||
opts.Validate().ShouldContain(w => w.Contains("SharedSecret"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_warns_on_relative_database_path_when_enabled()
|
||||
{
|
||||
var opts = new AlarmHistorianOptions { Enabled = true, SharedSecret = "s", DatabasePath = "alarm-historian.db" };
|
||||
opts.Validate().ShouldContain(w => w.Contains("DatabasePath"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_is_silent_when_correctly_configured()
|
||||
{
|
||||
new AlarmHistorianOptions { Enabled = true, SharedSecret = "s", DatabasePath = "/abs/h.db" }.Validate().ShouldBeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_is_silent_when_disabled()
|
||||
{
|
||||
new AlarmHistorianOptions { Enabled = false, SharedSecret = "" }.Validate().ShouldBeEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user