Files
wwtools/mbproxy/src/Mbproxy/Admin/StatusDto.cs
T
Joseph Doherty 0868613890 mbproxy: add keepalive / connection monitoring
The DL205/DL260 ECOM emits no TCP keepalives, so an idle backend socket
can be silently dropped by a middlebox (switch, firewall, NAT) after
2-5 minutes. Enable OS SO_KEEPALIVE on backend and accepted upstream
sockets, and drive a periodic synthetic FC03 heartbeat on each idle
backend socket so a dead path is detected before a real client request
hits it. Controlled by Connection.Keepalive (ON by default).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 09:40:54 -04:00

139 lines
4.9 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Text.Json.Serialization;
namespace Mbproxy.Admin;
// ── Wire DTOs for GET /status.json ───────────────────────────────────────────
// Field names must match docs/Operations/StatusPage.md tables EXACTLY (camelCase via
// JsonKnownNamingPolicy.CamelCase on the source-gen context).
/// <summary>
/// Top-level response envelope for <c>GET /status.json</c>.
/// </summary>
public sealed record StatusResponse(
ServiceFields Service,
ListenersAggregate Listeners,
IReadOnlyList<PlcStatus> Plcs);
/// <summary>Service-wide identity and reload counters.</summary>
public sealed record ServiceFields(
long UptimeSeconds,
string Version,
DateTimeOffset? ConfigLastReloadUtc,
int ConfigReloadCount,
int ConfigReloadRejectedCount);
/// <summary>Aggregate listener state across all configured PLCs.</summary>
public sealed record ListenersAggregate(int Bound, int Configured);
/// <summary>Per-PLC status row.</summary>
public sealed record PlcStatus(
string Name,
string Host,
int ListenPort,
PlcListenerStatus Listener,
PlcClientsStatus Clients,
PlcPdusStatus Pdus,
PlcBackendStatus Backend,
PlcBytesStatus Bytes);
/// <summary>Listener state sub-object.</summary>
public sealed record PlcListenerStatus(
string State,
string? LastBindError,
int RecoveryAttempts);
/// <summary>Connected-clients sub-object.</summary>
public sealed record PlcClientsStatus(
int Connected,
IReadOnlyList<ClientSnapshot> RemoteEndpoints);
/// <summary>Per-connection-pair snapshot for the status page.</summary>
public sealed record ClientSnapshot(
string Remote,
DateTimeOffset ConnectedAtUtc,
long PdusForwarded);
/// <summary>PDU counters sub-object.</summary>
public sealed record PlcPdusStatus(
long Forwarded,
FcCounts ByFc,
long RewrittenSlots,
long PartialBcdWarnings,
/// <summary>
/// Count of BCD-rewriter slot decisions where the wire value was not a valid BCD
/// nibble pattern (e.g. <c>0xABCD</c> at a tag address). The slot passes through
/// unrewritten and this counter increments.
/// </summary>
long InvalidBcdWarnings);
/// <summary>Per-function-code request counts.</summary>
public sealed record FcCounts(
long Fc03,
long Fc04,
long Fc06,
long Fc16,
long Other);
/// <summary>
/// Backend connect, exception, and multiplexer telemetry, including the in-flight
/// multiplexer fields (<c>InFlight</c>, <c>MaxInFlight</c>, <c>TxIdWraps</c>,
/// <c>DisconnectCascades</c>, <c>QueueDepth</c>), the read-coalescing counters
/// (<c>CoalescedHitCount</c>, <c>CoalescedMissCount</c>, <c>CoalescedResponseToDeadUpstream</c>),
/// and the response-cache counters (<c>CacheHitCount</c>, <c>CacheMissCount</c>,
/// <c>CacheInvalidations</c>, <c>CacheEntryCount</c>, <c>CacheBytes</c>).
///
/// <para>The dashboard-side derived ratios <c>coalescingRatio</c> and <c>cacheHitRatio</c>
/// are intentionally NOT carried on the wire — consumers compute <c>Hit / (Hit + Miss)</c>
/// from the raw counters.</para>
/// </summary>
public sealed record PlcBackendStatus(
long ConnectsSuccess,
long ConnectsFailed,
ExceptionCounts ExceptionsByCode,
double LastRoundTripMs,
long InFlight,
long MaxInFlight,
long TxIdWraps,
long DisconnectCascades,
long QueueDepth,
long CoalescedHitCount,
long CoalescedMissCount,
long CoalescedResponseToDeadUpstream,
long CacheHitCount,
long CacheMissCount,
long CacheInvalidations,
long CacheEntryCount,
long CacheBytes,
/// <summary>Backend keepalive heartbeat probes issued on idle backend sockets.</summary>
long BackendHeartbeatsSent,
/// <summary>Keepalive heartbeat probes that timed out (backend not answering).</summary>
long BackendHeartbeatsFailed,
/// <summary>Backend teardowns triggered by a failed keepalive heartbeat.</summary>
long BackendIdleDisconnects);
/// <summary>Modbus exception counts by code.</summary>
public sealed record ExceptionCounts(
long Code01,
long Code02,
long Code03,
long Code04,
/// <summary>
/// Backend exceptions whose response code is not 0104 (e.g. 0x06 Server Device
/// Busy, 0x0B Gateway Target Failed To Respond, vendor-specific codes).
/// </summary>
long CodeOther);
/// <summary>Byte-transfer counters.</summary>
public sealed record PlcBytesStatus(
long UpstreamIn,
long UpstreamOut);
// ── Source-generation context ─────────────────────────────────────────────────
// TreatWarningsAsErrors is on, so the context must include every reachable type.
[JsonSerializable(typeof(StatusResponse))]
[JsonSourceGenerationOptions(
WriteIndented = false,
PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)]
internal partial class StatusJsonContext : JsonSerializerContext;