feat(historian): backend registry, ServerHistorian:Backend, Host wiring via provider

Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
Joseph Doherty
2026-10-09 13:05:53 -04:00
parent 65e6352dad
commit 31d342eff3
7 changed files with 186 additions and 9 deletions
@@ -37,6 +37,11 @@ public static class ContinuousHistorizationMetrics
ObserveDropped,
unit: "{entry}",
description: "Lifetime entries the continuous-historization outbox dropped on capacity overflow.");
OtOpcUaTelemetry.Meter.CreateObservableGauge(
"otopcua.historian.outbox.unreadable",
() => _outbox?.UnreadableCount ?? 0L,
unit: "{entry}",
description: "Lifetime undecodable records the continuous-historization outbox skipped.");
}
/// <summary>Binds the process outbox the gauges observe. Called once by the Host when continuous
@@ -35,6 +35,20 @@ public sealed class ServerHistorianOptions
/// The scheme selects the transport: <c>https://</c> = TLS, <c>http://</c> = h2c plaintext.
/// Required when <see cref="Enabled"/> is <c>true</c>.
/// </summary>
/// <summary>
/// Selects the historian backend provider (case-insensitive). Default <c>"Gateway"</c>; the Host's
/// <c>HistorianBackendRegistry</c> owns the set of valid names. Gateway-specific keys
/// (<see cref="Endpoint"/>, <see cref="ApiKey"/>, <see cref="UseTls"/>) are only required for <c>Gateway</c>.
/// </summary>
public string Backend { get; init; } = GatewayBackendName;
/// <summary>The default backend name.</summary>
public const string GatewayBackendName = "Gateway";
/// <summary>True when <see cref="Backend"/> selects the gateway backend (case-insensitive).</summary>
public bool IsGatewayBackend =>
string.Equals(Backend?.Trim(), GatewayBackendName, StringComparison.OrdinalIgnoreCase);
public string Endpoint { get; init; } = "";
/// <summary>
@@ -112,6 +126,9 @@ public sealed class ServerHistorianOptions
{
var warnings = new List<string>();
if (!Enabled) return warnings;
// Gateway-specific checks apply only to the Gateway backend (other backends carry their own options).
if (IsGatewayBackend)
{
if (string.IsNullOrWhiteSpace(Endpoint))
warnings.Add("ServerHistorian:Endpoint is empty while the historian is enabled — the read client has no gateway address to dial.");
// archreview 06/S-11 warning parity: a non-empty but unparseable / non-absolute-http(s) Endpoint
@@ -123,6 +140,7 @@ public sealed class ServerHistorianOptions
warnings.Add($"ServerHistorian:Endpoint ('{Endpoint}') is not an absolute http(s) URI while the historian is enabled — the read client cannot dial it (e.g. https://host:5222).");
if (string.IsNullOrWhiteSpace(ApiKey))
warnings.Add("ServerHistorian:ApiKey is empty while the historian is enabled — the gateway gRPC surface will reject unauthenticated calls.");
}
// MaxTieClusterOverfetch is intentionally checked AFTER the Enabled early-return above:
// the over-fetch code path only runs when a real IHistorianDataSource is wired in,
// so a zero/negative value is harmless (and noise-free) when the historian is disabled.