feat(hosts): driver diagnostics channel on /hosts, S7 negotiated PDU size first (#640)
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
@@ -59,6 +59,12 @@ namespace ZB.MOM.WW.OtOpcUa.Commons.Messages.Drivers;
|
||||
/// <para><b>Value-equality caveat</b> — the same one <paramref name="HostStatuses"/> carries: the
|
||||
/// record's generated <c>Equals</c> compares the sample list by REFERENCE.</para>
|
||||
/// </param>
|
||||
/// <param name="Diagnostics">
|
||||
/// Operator-facing driver diagnostics from <c>IDriverDiagnostics.GetDiagnostics()</c> (e.g. S7's
|
||||
/// negotiated PDU size), bounded and in ordinal key order; null when the driver does not implement the
|
||||
/// capability (Gitea #640). Additive, never a state change. Same by-reference value-equality caveat as
|
||||
/// <paramref name="HostStatuses"/>.
|
||||
/// </param>
|
||||
public sealed record DriverHealthChanged(
|
||||
string ClusterId,
|
||||
string DriverInstanceId,
|
||||
@@ -70,7 +76,8 @@ public sealed record DriverHealthChanged(
|
||||
DateTime? RediscoveryNeededUtc = null,
|
||||
string? RediscoveryReason = null,
|
||||
IReadOnlyList<HostConnectivityStatus>? HostStatuses = null,
|
||||
SkippedTagReport? SkippedTags = null)
|
||||
SkippedTagReport? SkippedTags = null,
|
||||
IReadOnlyDictionary<string, string>? Diagnostics = null)
|
||||
{
|
||||
/// <summary>
|
||||
/// DPS topic name. Both the runtime <c>AkkaDriverHealthPublisher</c> and the AdminUI
|
||||
|
||||
@@ -82,6 +82,12 @@ message DriverHealth {
|
||||
// proto3, which a repeated field does not. So no companion has_ flag (and one would collide: protoc
|
||||
// already generates a HasSkippedTags property for this field).
|
||||
SkippedTags skipped_tags = 12;
|
||||
// Driver diagnostics bag (Gitea #640), e.g. S7's negotiated PDU size. Additive: an old node never sets
|
||||
// these and an old central ignores them. A map cannot distinguish absent from empty, and the two differ
|
||||
// ("driver has no IDriverDiagnostics" vs "it has one and currently has nothing to say"), so the presence
|
||||
// flag carries that bit - the same shape as has_host_statuses.
|
||||
bool has_diagnostics = 13;
|
||||
map<string, string> diagnostics = 14;
|
||||
}
|
||||
|
||||
// Mirrors ZB.MOM.WW.OtOpcUa.Core.Abstractions.SkippedTagReport.
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
namespace ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
||||
|
||||
/// <summary>
|
||||
/// Optional driver capability: expose a small bag of operator-facing diagnostic key/value pairs
|
||||
/// (e.g. S7's negotiated PDU size) that ride the driver-health snapshot to the AdminUI <c>/hosts</c>
|
||||
/// page (Gitea #640).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// <b>Why a bag rather than fields on <see cref="DriverHealth"/>.</b> The facts worth showing are
|
||||
/// driver-specific and do not change the driver's <i>state</i>; a typed field per driver would
|
||||
/// widen the core record and the telemetry proto for every driver. Same additive-never-a-state-change
|
||||
/// rule as <see cref="ISkippedTagReporter"/>.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Values are strings so the telemetry proto stays a plain <c>map<string,string></c>. Keep the
|
||||
/// bag small (<see cref="DriverDiagnostics.MaxEntries"/> entries, text truncated to
|
||||
/// <see cref="DriverDiagnostics.MaxValueLength"/>): it is published on every state change.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public interface IDriverDiagnostics
|
||||
{
|
||||
/// <summary>
|
||||
/// Snapshot of the driver's diagnostics. Must be a pure in-memory read with no I/O — it is called on
|
||||
/// the health-publish path, deliberately NOT through the resilience pipeline.
|
||||
/// </summary>
|
||||
/// <returns>The key/value pairs; empty when there is nothing to show yet.</returns>
|
||||
IReadOnlyDictionary<string, string> GetDiagnostics();
|
||||
}
|
||||
|
||||
/// <summary>Bounds and helpers shared by every consumer of <see cref="IDriverDiagnostics"/>.</summary>
|
||||
public static class DriverDiagnostics
|
||||
{
|
||||
/// <summary>Maximum number of entries carried on a health snapshot; extras are dropped (ordinal key order).</summary>
|
||||
public const int MaxEntries = 16;
|
||||
|
||||
/// <summary>Maximum length of a key or value carried on a health snapshot; longer text is truncated.</summary>
|
||||
public const int MaxValueLength = 200;
|
||||
|
||||
/// <summary>
|
||||
/// Normalises a driver's bag for publishing: ordinal key order, capped entry count, truncated text.
|
||||
/// Deterministic so the same facts always produce the same fingerprint digest.
|
||||
/// </summary>
|
||||
/// <param name="raw">The bag the driver returned.</param>
|
||||
/// <returns>A bounded, ordered copy.</returns>
|
||||
public static IReadOnlyDictionary<string, string> Bound(IReadOnlyDictionary<string, string> raw)
|
||||
{
|
||||
var result = new SortedDictionary<string, string>(StringComparer.Ordinal);
|
||||
foreach (var kv in raw.OrderBy(k => k.Key, StringComparer.Ordinal))
|
||||
{
|
||||
if (result.Count >= MaxEntries) break;
|
||||
result[Truncate(kv.Key ?? "")] = Truncate(kv.Value ?? "");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static string Truncate(string s) => s.Length <= MaxValueLength ? s : s[..MaxValueLength];
|
||||
}
|
||||
@@ -34,6 +34,10 @@ public interface IDriverHealthPublisher
|
||||
/// <see cref="ISkippedTagReporter"/>. Contradicts the "Healthy, 0 errors, reads advancing" reading
|
||||
/// of a driver that is in fact ignoring part or all of its configuration (Gitea #541).
|
||||
/// </param>
|
||||
/// <param name="diagnostics">
|
||||
/// Bounded driver diagnostics from <see cref="IDriverDiagnostics.GetDiagnostics"/>; null when the
|
||||
/// driver is not an <see cref="IDriverDiagnostics"/> (Gitea #640).
|
||||
/// </param>
|
||||
void Publish(
|
||||
string clusterId,
|
||||
string driverInstanceId,
|
||||
@@ -42,7 +46,8 @@ public interface IDriverHealthPublisher
|
||||
DateTime? rediscoveryNeededUtc = null,
|
||||
string? rediscoveryReason = null,
|
||||
IReadOnlyList<HostConnectivityStatus>? hostStatuses = null,
|
||||
SkippedTagReport? skippedTags = null);
|
||||
SkippedTagReport? skippedTags = null,
|
||||
IReadOnlyDictionary<string, string>? diagnostics = null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -65,6 +70,7 @@ public sealed class NullDriverHealthPublisher : IDriverHealthPublisher
|
||||
DateTime? rediscoveryNeededUtc = null,
|
||||
string? rediscoveryReason = null,
|
||||
IReadOnlyList<HostConnectivityStatus>? hostStatuses = null,
|
||||
SkippedTagReport? skippedTags = null)
|
||||
SkippedTagReport? skippedTags = null,
|
||||
IReadOnlyDictionary<string, string>? diagnostics = null)
|
||||
{ /* no-op */ }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user