From ce45dff9945dd27fd8e90a0eb74cab1624d7b38a Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Thu, 26 Feb 2026 15:46:14 -0500 Subject: [PATCH] =?UTF-8?q?feat:=20port=20sessions=2012=20&=2013=20?= =?UTF-8?q?=E2=80=94=20Events/Monitoring/MsgTrace=20+=20Config=20Reload?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Session 12 (218 features, IDs 854-950, 2166-2251, 2405-2439): - EventTypes: system subjects, event message types, InternalState, ConnectEventMsg, DisconnectEventMsg, AccountNumConns, ServerIdentity, DataStats - MonitorTypes: Connz, ConnInfo, ConnzOptions, ConnState, ProxyInfo, TlsPeerCert - MonitorSortOptions: SortOpt, ConnInfos, all 13 sort comparers - MsgTraceTypes: IMsgTrace, MsgTraceBase + 6 concrete types, custom JSON converter Session 13 (89 features, IDs 2800-2888): - ReloadOptions: IReloadOption interface, NoopReloadOption base, 50 option classes covering logging, TLS, auth, cluster, JetStream, MQTT, OCSP, misc --- .../ZB.MOM.NatsNet.Server/Accounts/Account.cs | 36 +- .../Config/ReloadOptions.cs | 996 ++++++++++++++++++ .../Events/EventTypes.cs | 778 ++++++++++++++ .../MessageTrace/MsgTraceTypes.cs | 465 ++++++++ .../Monitor/MonitorSortOptions.cs | 294 ++++++ .../Monitor/MonitorTypes.cs | 387 +++++++ .../src/ZB.MOM.NatsNet.Server/NatsServer.cs | 2 +- .../ZB.MOM.NatsNet.Server/NatsServerTypes.cs | 13 +- porting.db | Bin 2473984 -> 2473984 bytes reports/current.md | 8 +- reports/report_12a14ec.md | 39 + 11 files changed, 2971 insertions(+), 47 deletions(-) create mode 100644 dotnet/src/ZB.MOM.NatsNet.Server/Config/ReloadOptions.cs create mode 100644 dotnet/src/ZB.MOM.NatsNet.Server/Events/EventTypes.cs create mode 100644 dotnet/src/ZB.MOM.NatsNet.Server/MessageTrace/MsgTraceTypes.cs create mode 100644 dotnet/src/ZB.MOM.NatsNet.Server/Monitor/MonitorSortOptions.cs create mode 100644 dotnet/src/ZB.MOM.NatsNet.Server/Monitor/MonitorTypes.cs create mode 100644 reports/report_12a14ec.md diff --git a/dotnet/src/ZB.MOM.NatsNet.Server/Accounts/Account.cs b/dotnet/src/ZB.MOM.NatsNet.Server/Accounts/Account.cs index 4a9c55a..ab7eb64 100644 --- a/dotnet/src/ZB.MOM.NatsNet.Server/Accounts/Account.cs +++ b/dotnet/src/ZB.MOM.NatsNet.Server/Accounts/Account.cs @@ -19,38 +19,6 @@ using ZB.MOM.NatsNet.Server.Internal.DataStructures; namespace ZB.MOM.NatsNet.Server; -// ============================================================================ -// AccountNumConns — remote server connection/leafnode count message -// Mirrors Go `AccountNumConns` struct used in updateRemoteServer. -// ============================================================================ - -/// -/// Carries the number of client connections and leaf nodes that a remote server -/// has for a given account, along with the remote server's identity. -/// Mirrors Go AccountNumConns in server/accounts.go. -/// -internal sealed class AccountNumConns -{ - /// Remote server identity. Mirrors Go Server ServerInfo. - public ServerIdentity Server { get; set; } = new(); - - /// Number of client connections on the remote server. Mirrors Go Conns int. - public int Conns { get; set; } - - /// Number of leaf nodes on the remote server. Mirrors Go LeafNodes int. - public int LeafNodes { get; set; } -} - -/// -/// Minimal remote server identity stub used by . -/// Full implementation lives with the server cluster sessions. -/// -internal sealed class ServerIdentity -{ - /// Unique server ID. Mirrors Go ID string. - public string ID { get; set; } = string.Empty; -} - // ============================================================================ // Account — full implementation // Mirrors Go `Account` struct in server/accounts.go lines 52-119. @@ -666,8 +634,8 @@ public sealed class Account : INatsAccount { _strack ??= new Dictionary(); - _strack.TryGetValue(m.Server.ID, out var prev); - _strack[m.Server.ID] = new SConns + _strack.TryGetValue(m.Server.Id, out var prev); + _strack[m.Server.Id] = new SConns { Conns = m.Conns, Leafs = m.LeafNodes, diff --git a/dotnet/src/ZB.MOM.NatsNet.Server/Config/ReloadOptions.cs b/dotnet/src/ZB.MOM.NatsNet.Server/Config/ReloadOptions.cs new file mode 100644 index 0000000..32f16b1 --- /dev/null +++ b/dotnet/src/ZB.MOM.NatsNet.Server/Config/ReloadOptions.cs @@ -0,0 +1,996 @@ +// Copyright 2017-2026 The NATS Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// Adapted from server/reload.go in the NATS server Go source. + +namespace ZB.MOM.NatsNet.Server; + +// ============================================================================= +// IReloadOption — mirrors Go `option` interface in reload.go +// ============================================================================= + +/// +/// Represents a hot-swappable configuration setting that can be applied to a +/// running server. Mirrors Go option interface in server/reload.go. +/// +public interface IReloadOption +{ + /// Apply this option to the running server. + void Apply(NatsServer server); + + /// Returns true if this option requires reloading the logger. + bool IsLoggingChange(); + + /// + /// Returns true if this option requires reloading the cached trace level. + /// Clients store trace level separately. + /// + bool IsTraceLevelChange(); + + /// Returns true if this option requires reloading authorization. + bool IsAuthChange(); + + /// Returns true if this option requires reloading TLS. + bool IsTlsChange(); + + /// Returns true if this option requires reloading cluster permissions. + bool IsClusterPermsChange(); + + /// + /// Returns true if this option requires special handling for changes in + /// cluster pool size or accounts list. + /// + bool IsClusterPoolSizeOrAccountsChange(); + + /// + /// Returns true if this option indicates a change in the server's JetStream config. + /// Account changes are handled separately in reloadAuthorization. + /// + bool IsJetStreamChange(); + + /// Returns true if this change requires publishing the server's statz. + bool IsStatszChange(); +} + +// ============================================================================= +// NoopReloadOption — mirrors Go `noopOption` struct in reload.go +// ============================================================================= + +/// +/// Base class providing no-op implementations for all +/// methods. Concrete option types override only the methods relevant to them. +/// Mirrors Go noopOption struct in server/reload.go. +/// +public abstract class NoopReloadOption : IReloadOption +{ + /// + public virtual void Apply(NatsServer server) { } + + /// + public virtual bool IsLoggingChange() => false; + + /// + public virtual bool IsTraceLevelChange() => false; + + /// + public virtual bool IsAuthChange() => false; + + /// + public virtual bool IsTlsChange() => false; + + /// + public virtual bool IsClusterPermsChange() => false; + + /// + public virtual bool IsClusterPoolSizeOrAccountsChange() => false; + + /// + public virtual bool IsJetStreamChange() => false; + + /// + public virtual bool IsStatszChange() => false; +} + +// ============================================================================= +// Intermediate base classes (mirrors Go loggingOption / traceLevelOption) +// ============================================================================= + +/// +/// Base for all logging-related reload options. +/// Mirrors Go loggingOption struct. +/// +internal abstract class LoggingReloadOption : NoopReloadOption +{ + public override bool IsLoggingChange() => true; +} + +/// +/// Base for all trace-level reload options. +/// Mirrors Go traceLevelOption struct. +/// +internal abstract class TraceLevelReloadOption : LoggingReloadOption +{ + public override bool IsTraceLevelChange() => true; +} + +/// +/// Base for all authorization-related reload options. +/// Mirrors Go authOption struct. +/// +internal abstract class AuthReloadOption : NoopReloadOption +{ + public override bool IsAuthChange() => true; +} + +/// +/// Base for TLS reload options. +/// Mirrors Go tlsOption (as a base, not the concrete type). +/// +internal abstract class TlsBaseReloadOption : NoopReloadOption +{ + public override bool IsTlsChange() => true; +} + +// ============================================================================= +// Logging & Trace option types +// ============================================================================= + +/// +/// Reload option for the trace setting. +/// Mirrors Go traceOption struct in reload.go. +/// +internal sealed class TraceReloadOption : TraceLevelReloadOption +{ + private readonly bool _newValue; + public TraceReloadOption(bool newValue) => _newValue = newValue; + + public override void Apply(NatsServer server) + => server.Noticef("Reloaded: trace = {0}", _newValue); +} + +/// +/// Reload option for the trace_verbose setting. +/// Mirrors Go traceVerboseOption struct in reload.go. +/// +internal sealed class TraceVerboseReloadOption : TraceLevelReloadOption +{ + private readonly bool _newValue; + public TraceVerboseReloadOption(bool newValue) => _newValue = newValue; + + public override void Apply(NatsServer server) + => server.Noticef("Reloaded: trace_verbose = {0}", _newValue); +} + +/// +/// Reload option for the trace_headers setting. +/// Mirrors Go traceHeadersOption struct in reload.go. +/// +internal sealed class TraceHeadersReloadOption : TraceLevelReloadOption +{ + private readonly bool _newValue; + public TraceHeadersReloadOption(bool newValue) => _newValue = newValue; + + public override void Apply(NatsServer server) + => server.Noticef("Reloaded: trace_headers = {0}", _newValue); +} + +/// +/// Reload option for the debug setting. +/// Mirrors Go debugOption struct in reload.go. +/// +internal sealed class DebugReloadOption : LoggingReloadOption +{ + private readonly bool _newValue; + public DebugReloadOption(bool newValue) => _newValue = newValue; + + public override void Apply(NatsServer server) + { + server.Noticef("Reloaded: debug = {0}", _newValue); + // TODO: session 13 — call server.ReloadDebugRaftNodes(_newValue) + } +} + +/// +/// Reload option for the logtime setting. +/// Mirrors Go logtimeOption struct in reload.go. +/// +internal sealed class LogtimeReloadOption : LoggingReloadOption +{ + private readonly bool _newValue; + public LogtimeReloadOption(bool newValue) => _newValue = newValue; + + public override void Apply(NatsServer server) + => server.Noticef("Reloaded: logtime = {0}", _newValue); +} + +/// +/// Reload option for the logtime_utc setting. +/// Mirrors Go logtimeUTCOption struct in reload.go. +/// +internal sealed class LogtimeUtcReloadOption : LoggingReloadOption +{ + private readonly bool _newValue; + public LogtimeUtcReloadOption(bool newValue) => _newValue = newValue; + + public override void Apply(NatsServer server) + => server.Noticef("Reloaded: logtime_utc = {0}", _newValue); +} + +/// +/// Reload option for the log_file setting. +/// Mirrors Go logfileOption struct in reload.go. +/// +internal sealed class LogFileReloadOption : LoggingReloadOption +{ + private readonly string _newValue; + public LogFileReloadOption(string newValue) => _newValue = newValue; + + public override void Apply(NatsServer server) + => server.Noticef("Reloaded: log_file = {0}", _newValue); +} + +/// +/// Reload option for the syslog setting. +/// Mirrors Go syslogOption struct in reload.go. +/// +internal sealed class SyslogReloadOption : LoggingReloadOption +{ + private readonly bool _newValue; + public SyslogReloadOption(bool newValue) => _newValue = newValue; + + public override void Apply(NatsServer server) + => server.Noticef("Reloaded: syslog = {0}", _newValue); +} + +/// +/// Reload option for the remote_syslog setting. +/// Mirrors Go remoteSyslogOption struct in reload.go. +/// +internal sealed class RemoteSyslogReloadOption : LoggingReloadOption +{ + private readonly string _newValue; + public RemoteSyslogReloadOption(string newValue) => _newValue = newValue; + + public override void Apply(NatsServer server) + => server.Noticef("Reloaded: remote_syslog = {0}", _newValue); +} + +// ============================================================================= +// TLS option types +// ============================================================================= + +/// +/// Reload option for the tls setting. +/// Mirrors Go tlsOption struct in reload.go. +/// The TLS config is stored as object? because the full +/// TlsConfig type is not yet ported. +/// TODO: session 13 — replace object? with the ported TlsConfig type. +/// +internal sealed class TlsReloadOption : NoopReloadOption +{ + // TODO: session 13 — replace object? with ported TlsConfig type + private readonly object? _newValue; + public TlsReloadOption(object? newValue) => _newValue = newValue; + + public override bool IsTlsChange() => true; + + public override void Apply(NatsServer server) + { + var message = _newValue is null ? "disabled" : "enabled"; + server.Noticef("Reloaded: tls = {0}", message); + // TODO: session 13 — update server.Info.TLSRequired / TLSVerify + } +} + +/// +/// Reload option for the TLS timeout setting. +/// Mirrors Go tlsTimeoutOption struct in reload.go. +/// +internal sealed class TlsTimeoutReloadOption : NoopReloadOption +{ + private readonly double _newValue; + public TlsTimeoutReloadOption(double newValue) => _newValue = newValue; + + public override void Apply(NatsServer server) + => server.Noticef("Reloaded: tls timeout = {0}", _newValue); +} + +/// +/// Reload option for the TLS pinned_certs setting. +/// Mirrors Go tlsPinnedCertOption struct in reload.go. +/// The pinned cert set is stored as object? pending the port +/// of the PinnedCertSet type. +/// TODO: session 13 — replace object? with ported PinnedCertSet type. +/// +internal sealed class TlsPinnedCertReloadOption : NoopReloadOption +{ + // TODO: session 13 — replace object? with ported PinnedCertSet type + private readonly object? _newValue; + public TlsPinnedCertReloadOption(object? newValue) => _newValue = newValue; + + public override void Apply(NatsServer server) + => server.Noticef("Reloaded: pinned_certs"); +} + +/// +/// Reload option for the TLS handshake_first setting. +/// Mirrors Go tlsHandshakeFirst struct in reload.go. +/// +internal sealed class TlsHandshakeFirstReloadOption : NoopReloadOption +{ + private readonly bool _newValue; + public TlsHandshakeFirstReloadOption(bool newValue) => _newValue = newValue; + + public override void Apply(NatsServer server) + => server.Noticef("Reloaded: Client TLS handshake first: {0}", _newValue); +} + +/// +/// Reload option for the TLS handshake_first_fallback delay setting. +/// Mirrors Go tlsHandshakeFirstFallback struct in reload.go. +/// +internal sealed class TlsHandshakeFirstFallbackReloadOption : NoopReloadOption +{ + private readonly TimeSpan _newValue; + public TlsHandshakeFirstFallbackReloadOption(TimeSpan newValue) => _newValue = newValue; + + public override void Apply(NatsServer server) + => server.Noticef("Reloaded: Client TLS handshake first fallback delay: {0}", _newValue); +} + +// ============================================================================= +// Authorization option types +// ============================================================================= + +/// +/// Reload option for the username authorization setting. +/// Mirrors Go usernameOption struct in reload.go. +/// +internal sealed class UsernameReloadOption : AuthReloadOption +{ + public override void Apply(NatsServer server) + => server.Noticef("Reloaded: authorization username"); +} + +/// +/// Reload option for the password authorization setting. +/// Mirrors Go passwordOption struct in reload.go. +/// +internal sealed class PasswordReloadOption : AuthReloadOption +{ + public override void Apply(NatsServer server) + => server.Noticef("Reloaded: authorization password"); +} + +/// +/// Reload option for the token authorization setting. +/// Mirrors Go authorizationOption struct in reload.go. +/// +internal sealed class AuthorizationReloadOption : AuthReloadOption +{ + public override void Apply(NatsServer server) + => server.Noticef("Reloaded: authorization token"); +} + +/// +/// Reload option for the authorization timeout setting. +/// Note: this is a NoopReloadOption (not auth) because authorization +/// will be reloaded with options separately. +/// Mirrors Go authTimeoutOption struct in reload.go. +/// +internal sealed class AuthTimeoutReloadOption : NoopReloadOption +{ + private readonly double _newValue; + public AuthTimeoutReloadOption(double newValue) => _newValue = newValue; + + public override void Apply(NatsServer server) + => server.Noticef("Reloaded: authorization timeout = {0}", _newValue); +} + +/// +/// Reload option for the tags setting. +/// Mirrors Go tagsOption struct in reload.go. +/// +internal sealed class TagsReloadOption : NoopReloadOption +{ + public override void Apply(NatsServer server) + => server.Noticef("Reloaded: tags"); + + public override bool IsStatszChange() => true; +} + +/// +/// Reload option for the metadata setting. +/// Mirrors Go metadataOption struct in reload.go. +/// +internal sealed class MetadataReloadOption : NoopReloadOption +{ + public override void Apply(NatsServer server) + => server.Noticef("Reloaded: metadata"); + + public override bool IsStatszChange() => true; +} + +/// +/// Reload option for the authorization users setting. +/// Mirrors Go usersOption struct in reload.go. +/// +internal sealed class UsersReloadOption : AuthReloadOption +{ + public override void Apply(NatsServer server) + => server.Noticef("Reloaded: authorization users"); +} + +/// +/// Reload option for the authorization nkeys setting. +/// Mirrors Go nkeysOption struct in reload.go. +/// +internal sealed class NkeysReloadOption : AuthReloadOption +{ + public override void Apply(NatsServer server) + => server.Noticef("Reloaded: authorization nkey users"); +} + +/// +/// Reload option for the accounts setting. +/// Mirrors Go accountsOption struct in reload.go. +/// +internal sealed class AccountsReloadOption : AuthReloadOption +{ + public override void Apply(NatsServer server) + => server.Noticef("Reloaded: accounts"); +} + +// ============================================================================= +// Cluster option types +// ============================================================================= + +/// +/// Reload option for the cluster setting. +/// Stores cluster options as object? pending the port of ClusterOpts. +/// Mirrors Go clusterOption struct in reload.go. +/// TODO: session 13 — replace object? with ported ClusterOpts type. +/// +internal sealed class ClusterReloadOption : AuthReloadOption +{ + // TODO: session 13 — replace object? with ported ClusterOpts type + private readonly object? _newValue; + private readonly bool _permsChanged; + private readonly bool _poolSizeChanged; + private readonly bool _compressChanged; + private readonly string[] _accsAdded; + private readonly string[] _accsRemoved; + + public ClusterReloadOption( + object? newValue, + bool permsChanged, + bool poolSizeChanged, + bool compressChanged, + string[] accsAdded, + string[] accsRemoved) + { + _newValue = newValue; + _permsChanged = permsChanged; + _poolSizeChanged = poolSizeChanged; + _compressChanged = compressChanged; + _accsAdded = accsAdded; + _accsRemoved = accsRemoved; + } + + public override bool IsClusterPermsChange() + => _permsChanged; + + public override bool IsClusterPoolSizeOrAccountsChange() + => _poolSizeChanged || _accsAdded.Length > 0 || _accsRemoved.Length > 0; + + public override void Apply(NatsServer server) + { + // TODO: session 13 — full cluster apply logic (TLS, route info, compression) + server.Noticef("Reloaded: cluster"); + } +} + +/// +/// Reload option for the cluster routes setting. +/// Routes to add/remove are stored as object[] pending the port of URL handling. +/// Mirrors Go routesOption struct in reload.go. +/// TODO: session 13 — replace object[] with Uri[] when route types are ported. +/// +internal sealed class RoutesReloadOption : NoopReloadOption +{ + // TODO: session 13 — replace object[] with Uri[] when route URL types are ported + private readonly object[] _add; + private readonly object[] _remove; + + public RoutesReloadOption(object[] add, object[] remove) + { + _add = add; + _remove = remove; + } + + public override void Apply(NatsServer server) + { + // TODO: session 13 — add/remove routes, update varzUpdateRouteURLs + server.Noticef("Reloaded: cluster routes"); + } +} + +// ============================================================================= +// Connection limit & network option types +// ============================================================================= + +/// +/// Reload option for the max_connections setting. +/// Mirrors Go maxConnOption struct in reload.go. +/// +internal sealed class MaxConnReloadOption : NoopReloadOption +{ + private readonly int _newValue; + public MaxConnReloadOption(int newValue) => _newValue = newValue; + + public override void Apply(NatsServer server) + { + // TODO: session 13 — close random connections if over limit + server.Noticef("Reloaded: max_connections = {0}", _newValue); + } +} + +/// +/// Reload option for the pid_file setting. +/// Mirrors Go pidFileOption struct in reload.go. +/// +internal sealed class PidFileReloadOption : NoopReloadOption +{ + private readonly string _newValue; + public PidFileReloadOption(string newValue) => _newValue = newValue; + + public override void Apply(NatsServer server) + { + if (string.IsNullOrEmpty(_newValue)) + return; + // TODO: session 13 — call server.LogPid() + server.Noticef("Reloaded: pid_file = {0}", _newValue); + } +} + +/// +/// Reload option for the ports_file_dir setting. +/// Mirrors Go portsFileDirOption struct in reload.go. +/// +internal sealed class PortsFileDirReloadOption : NoopReloadOption +{ + private readonly string _oldValue; + private readonly string _newValue; + + public PortsFileDirReloadOption(string oldValue, string newValue) + { + _oldValue = oldValue; + _newValue = newValue; + } + + public override void Apply(NatsServer server) + { + // TODO: session 13 — call server.DeletePortsFile(_oldValue) and server.LogPorts() + server.Noticef("Reloaded: ports_file_dir = {0}", _newValue); + } +} + +/// +/// Reload option for the max_control_line setting. +/// Mirrors Go maxControlLineOption struct in reload.go. +/// +internal sealed class MaxControlLineReloadOption : NoopReloadOption +{ + private readonly int _newValue; + public MaxControlLineReloadOption(int newValue) => _newValue = newValue; + + public override void Apply(NatsServer server) + { + // TODO: session 13 — update mcl on each connected client + server.Noticef("Reloaded: max_control_line = {0}", _newValue); + } +} + +/// +/// Reload option for the max_payload setting. +/// Mirrors Go maxPayloadOption struct in reload.go. +/// +internal sealed class MaxPayloadReloadOption : NoopReloadOption +{ + private readonly int _newValue; + public MaxPayloadReloadOption(int newValue) => _newValue = newValue; + + public override void Apply(NatsServer server) + { + // TODO: session 13 — update server info and mpay on each client + server.Noticef("Reloaded: max_payload = {0}", _newValue); + } +} + +/// +/// Reload option for the ping_interval setting. +/// Mirrors Go pingIntervalOption struct in reload.go. +/// +internal sealed class PingIntervalReloadOption : NoopReloadOption +{ + private readonly TimeSpan _newValue; + public PingIntervalReloadOption(TimeSpan newValue) => _newValue = newValue; + + public override void Apply(NatsServer server) + => server.Noticef("Reloaded: ping_interval = {0}", _newValue); +} + +/// +/// Reload option for the ping_max setting. +/// Mirrors Go maxPingsOutOption struct in reload.go. +/// +internal sealed class MaxPingsOutReloadOption : NoopReloadOption +{ + private readonly int _newValue; + public MaxPingsOutReloadOption(int newValue) => _newValue = newValue; + + public override void Apply(NatsServer server) + => server.Noticef("Reloaded: ping_max = {0}", _newValue); +} + +/// +/// Reload option for the write_deadline setting. +/// Mirrors Go writeDeadlineOption struct in reload.go. +/// +internal sealed class WriteDeadlineReloadOption : NoopReloadOption +{ + private readonly TimeSpan _newValue; + public WriteDeadlineReloadOption(TimeSpan newValue) => _newValue = newValue; + + public override void Apply(NatsServer server) + => server.Noticef("Reloaded: write_deadline = {0}", _newValue); +} + +/// +/// Reload option for the client_advertise setting. +/// Mirrors Go clientAdvertiseOption struct in reload.go. +/// +internal sealed class ClientAdvertiseReloadOption : NoopReloadOption +{ + private readonly string _newValue; + public ClientAdvertiseReloadOption(string newValue) => _newValue = newValue; + + public override void Apply(NatsServer server) + { + // TODO: session 13 — call server.SetInfoHostPort() + server.Noticef("Reload: client_advertise = {0}", _newValue); + } +} + +// ============================================================================= +// JetStream option type +// ============================================================================= + +/// +/// Reload option for the jetstream setting. +/// Mirrors Go jetStreamOption struct in reload.go. +/// +internal sealed class JetStreamReloadOption : NoopReloadOption +{ + private readonly bool _newValue; + public JetStreamReloadOption(bool newValue) => _newValue = newValue; + + public override bool IsJetStreamChange() => true; + public override bool IsStatszChange() => true; + + public override void Apply(NatsServer server) + => server.Noticef("Reloaded: JetStream"); +} + +// ============================================================================= +// Miscellaneous option types +// ============================================================================= + +/// +/// Reload option for the default_sentinel setting. +/// Mirrors Go defaultSentinelOption struct in reload.go. +/// +internal sealed class DefaultSentinelReloadOption : NoopReloadOption +{ + private readonly string _newValue; + public DefaultSentinelReloadOption(string newValue) => _newValue = newValue; + + public override void Apply(NatsServer server) + => server.Noticef("Reloaded: default_sentinel = {0}", _newValue); +} + +/// +/// Reload option for the OCSP setting. +/// The new value is stored as object? pending the port of OCSPConfig. +/// Mirrors Go ocspOption struct in reload.go. +/// TODO: session 13 — replace object? with ported OcspConfig type. +/// +internal sealed class OcspReloadOption : TlsBaseReloadOption +{ + // TODO: session 13 — replace object? with ported OcspConfig type + private readonly object? _newValue; + public OcspReloadOption(object? newValue) => _newValue = newValue; + + public override void Apply(NatsServer server) + => server.Noticef("Reloaded: OCSP"); +} + +/// +/// Reload option for the OCSP response cache setting. +/// The new value is stored as object? pending the port of +/// OCSPResponseCacheConfig. +/// Mirrors Go ocspResponseCacheOption struct in reload.go. +/// TODO: session 13 — replace object? with ported OcspResponseCacheConfig type. +/// +internal sealed class OcspResponseCacheReloadOption : TlsBaseReloadOption +{ + // TODO: session 13 — replace object? with ported OcspResponseCacheConfig type + private readonly object? _newValue; + public OcspResponseCacheReloadOption(object? newValue) => _newValue = newValue; + + public override void Apply(NatsServer server) + => server.Noticef("Reloaded OCSP peer cache"); +} + +/// +/// Reload option for the connect_error_reports setting. +/// Mirrors Go connectErrorReports struct in reload.go. +/// +internal sealed class ConnectErrorReportsReloadOption : NoopReloadOption +{ + private readonly int _newValue; + public ConnectErrorReportsReloadOption(int newValue) => _newValue = newValue; + + public override void Apply(NatsServer server) + => server.Noticef("Reloaded: connect_error_reports = {0}", _newValue); +} + +/// +/// Reload option for the reconnect_error_reports setting. +/// Mirrors Go reconnectErrorReports struct in reload.go. +/// +internal sealed class ReconnectErrorReportsReloadOption : NoopReloadOption +{ + private readonly int _newValue; + public ReconnectErrorReportsReloadOption(int newValue) => _newValue = newValue; + + public override void Apply(NatsServer server) + => server.Noticef("Reloaded: reconnect_error_reports = {0}", _newValue); +} + +/// +/// Reload option for the max_traced_msg_len setting. +/// Mirrors Go maxTracedMsgLenOption struct in reload.go. +/// +internal sealed class MaxTracedMsgLenReloadOption : NoopReloadOption +{ + private readonly int _newValue; + public MaxTracedMsgLenReloadOption(int newValue) => _newValue = newValue; + + public override void Apply(NatsServer server) + { + // TODO: session 13 — update server.Opts.MaxTracedMsgLen under lock + server.Noticef("Reloaded: max_traced_msg_len = {0}", _newValue); + } +} + +// ============================================================================= +// MQTT option types +// ============================================================================= + +/// +/// Reload option for the MQTT ack_wait setting. +/// Mirrors Go mqttAckWaitReload struct in reload.go. +/// +internal sealed class MqttAckWaitReloadOption : NoopReloadOption +{ + private readonly TimeSpan _newValue; + public MqttAckWaitReloadOption(TimeSpan newValue) => _newValue = newValue; + + public override void Apply(NatsServer server) + => server.Noticef("Reloaded: MQTT ack_wait = {0}", _newValue); +} + +/// +/// Reload option for the MQTT max_ack_pending setting. +/// Mirrors Go mqttMaxAckPendingReload struct in reload.go. +/// +internal sealed class MqttMaxAckPendingReloadOption : NoopReloadOption +{ + private readonly ushort _newValue; + public MqttMaxAckPendingReloadOption(ushort newValue) => _newValue = newValue; + + public override void Apply(NatsServer server) + { + // TODO: session 13 — call server.MqttUpdateMaxAckPending(_newValue) + server.Noticef("Reloaded: MQTT max_ack_pending = {0}", _newValue); + } +} + +/// +/// Reload option for the MQTT stream_replicas setting. +/// Mirrors Go mqttStreamReplicasReload struct in reload.go. +/// +internal sealed class MqttStreamReplicasReloadOption : NoopReloadOption +{ + private readonly int _newValue; + public MqttStreamReplicasReloadOption(int newValue) => _newValue = newValue; + + public override void Apply(NatsServer server) + => server.Noticef("Reloaded: MQTT stream_replicas = {0}", _newValue); +} + +/// +/// Reload option for the MQTT consumer_replicas setting. +/// Mirrors Go mqttConsumerReplicasReload struct in reload.go. +/// +internal sealed class MqttConsumerReplicasReloadOption : NoopReloadOption +{ + private readonly int _newValue; + public MqttConsumerReplicasReloadOption(int newValue) => _newValue = newValue; + + public override void Apply(NatsServer server) + => server.Noticef("Reloaded: MQTT consumer_replicas = {0}", _newValue); +} + +/// +/// Reload option for the MQTT consumer_memory_storage setting. +/// Mirrors Go mqttConsumerMemoryStorageReload struct in reload.go. +/// +internal sealed class MqttConsumerMemoryStorageReloadOption : NoopReloadOption +{ + private readonly bool _newValue; + public MqttConsumerMemoryStorageReloadOption(bool newValue) => _newValue = newValue; + + public override void Apply(NatsServer server) + => server.Noticef("Reloaded: MQTT consumer_memory_storage = {0}", _newValue); +} + +/// +/// Reload option for the MQTT consumer_inactive_threshold setting. +/// Mirrors Go mqttInactiveThresholdReload struct in reload.go. +/// +internal sealed class MqttInactiveThresholdReloadOption : NoopReloadOption +{ + private readonly TimeSpan _newValue; + public MqttInactiveThresholdReloadOption(TimeSpan newValue) => _newValue = newValue; + + public override void Apply(NatsServer server) + => server.Noticef("Reloaded: MQTT consumer_inactive_threshold = {0}", _newValue); +} + +// ============================================================================= +// Profiling option type +// ============================================================================= + +/// +/// Reload option for the prof_block_rate setting. +/// Mirrors Go profBlockRateReload struct in reload.go. +/// +internal sealed class ProfBlockRateReloadOption : NoopReloadOption +{ + private readonly int _newValue; + public ProfBlockRateReloadOption(int newValue) => _newValue = newValue; + + public override void Apply(NatsServer server) + { + // TODO: session 13 — call server.SetBlockProfileRate(_newValue) + server.Noticef("Reloaded: prof_block_rate = {0}", _newValue); + } +} + +// ============================================================================= +// LeafNode option type +// ============================================================================= + +/// +/// Reload option for leaf-node settings (TLS handshake-first, compression, disabled). +/// Mirrors Go leafNodeOption struct in reload.go. +/// +internal sealed class LeafNodeReloadOption : NoopReloadOption +{ + private readonly bool _tlsFirstChanged; + private readonly bool _compressionChanged; + private readonly bool _disabledChanged; + + public LeafNodeReloadOption(bool tlsFirstChanged, bool compressionChanged, bool disabledChanged) + { + _tlsFirstChanged = tlsFirstChanged; + _compressionChanged = compressionChanged; + _disabledChanged = disabledChanged; + } + + public override void Apply(NatsServer server) + { + // TODO: session 13 — full leaf-node apply logic from Go leafNodeOption.Apply() + if (_tlsFirstChanged) + server.Noticef("Reloaded: LeafNode TLS HandshakeFirst settings"); + if (_compressionChanged) + server.Noticef("Reloaded: LeafNode compression settings"); + if (_disabledChanged) + server.Noticef("Reloaded: LeafNode disabled/enabled state"); + } +} + +// ============================================================================= +// NoFastProducerStall option type +// ============================================================================= + +/// +/// Reload option for the no_fast_producer_stall setting. +/// Mirrors Go noFastProdStallReload struct in reload.go. +/// +internal sealed class NoFastProducerStallReloadOption : NoopReloadOption +{ + private readonly bool _noStall; + public NoFastProducerStallReloadOption(bool noStall) => _noStall = noStall; + + public override void Apply(NatsServer server) + { + var not = _noStall ? "not " : string.Empty; + server.Noticef("Reloaded: fast producers will {0}be stalled", not); + } +} + +// ============================================================================= +// Proxies option type +// ============================================================================= + +/// +/// Reload option for the proxies trusted keys setting. +/// Mirrors Go proxiesReload struct in reload.go. +/// +internal sealed class ProxiesReloadOption : NoopReloadOption +{ + private readonly string[] _add; + private readonly string[] _del; + + public ProxiesReloadOption(string[] add, string[] del) + { + _add = add; + _del = del; + } + + public override void Apply(NatsServer server) + { + // TODO: session 13 — disconnect proxied clients for removed keys, + // call server.ProcessProxiesTrustedKeys() + if (_del.Length > 0) + server.Noticef("Reloaded: proxies trusted keys {0} were removed", string.Join(", ", _del)); + if (_add.Length > 0) + server.Noticef("Reloaded: proxies trusted keys {0} were added", string.Join(", ", _add)); + } +} + +// ============================================================================= +// ConfigReloader — stub for server/reload.go Reload() / ReloadOptions() +// ============================================================================= + +/// +/// Stub for the configuration reloader. +/// Full reload logic (diffOptions, applyOptions, recheckPinnedCerts) will be +/// implemented in a future session. +/// Mirrors Go Server.Reload() and Server.ReloadOptions() in +/// server/reload.go. +/// +internal sealed class ConfigReloader +{ + // TODO: session 13 — full reload logic + // Mirrors Go server.Reload() / server.ReloadOptions() in server/reload.go + + /// + /// Stub: read and apply the server config file. + /// Returns null on success; a non-null Exception describes the failure. + /// + public Exception? Reload(NatsServer server) => null; +} diff --git a/dotnet/src/ZB.MOM.NatsNet.Server/Events/EventTypes.cs b/dotnet/src/ZB.MOM.NatsNet.Server/Events/EventTypes.cs new file mode 100644 index 0000000..540ac72 --- /dev/null +++ b/dotnet/src/ZB.MOM.NatsNet.Server/Events/EventTypes.cs @@ -0,0 +1,778 @@ +// Copyright 2018-2026 The NATS Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// Adapted from server/events.go in the NATS server Go source. + +using System.Text.Json.Serialization; +using ZB.MOM.NatsNet.Server.Auth.CertificateIdentityProvider; +using ZB.MOM.NatsNet.Server.Internal; + +namespace ZB.MOM.NatsNet.Server; + +// ============================================================================ +// System subject constants +// Mirrors the const block at the top of server/events.go. +// ============================================================================ + +/// +/// System-account subject templates and constants used for internal NATS server +/// event routing. All format-string fields use with +/// the appropriate server/account ID substituted at call time. +/// Mirrors the const block in server/events.go. +/// +public static class SystemSubjects +{ + // Account lookup / claims + public const string AccLookupReqSubj = "$SYS.REQ.ACCOUNT.{0}.CLAIMS.LOOKUP"; + public const string AccPackReqSubj = "$SYS.REQ.CLAIMS.PACK"; + public const string AccListReqSubj = "$SYS.REQ.CLAIMS.LIST"; + public const string AccClaimsReqSubj = "$SYS.REQ.CLAIMS.UPDATE"; + public const string AccDeleteReqSubj = "$SYS.REQ.CLAIMS.DELETE"; + + // Connection events + public const string ConnectEventSubj = "$SYS.ACCOUNT.{0}.CONNECT"; + public const string DisconnectEventSubj = "$SYS.ACCOUNT.{0}.DISCONNECT"; + + // Direct request routing + public const string AccDirectReqSubj = "$SYS.REQ.ACCOUNT.{0}.{1}"; + public const string AccPingReqSubj = "$SYS.REQ.ACCOUNT.PING.{0}"; + + // Account update events (both old and new forms kept for backward compatibility) + public const string AccUpdateEventSubjOld = "$SYS.ACCOUNT.{0}.CLAIMS.UPDATE"; + public const string AccUpdateEventSubjNew = "$SYS.REQ.ACCOUNT.{0}.CLAIMS.UPDATE"; + + public const string ConnsRespSubj = "$SYS._INBOX_.{0}"; + public const string AccConnsEventSubjNew = "$SYS.ACCOUNT.{0}.SERVER.CONNS"; + public const string AccConnsEventSubjOld = "$SYS.SERVER.ACCOUNT.{0}.CONNS"; // backward compat + + // Server lifecycle events + public const string LameDuckEventSubj = "$SYS.SERVER.{0}.LAMEDUCK"; + public const string ShutdownEventSubj = "$SYS.SERVER.{0}.SHUTDOWN"; + + // Client control + public const string ClientKickReqSubj = "$SYS.REQ.SERVER.{0}.KICK"; + public const string ClientLdmReqSubj = "$SYS.REQ.SERVER.{0}.LDM"; + + // Auth error events + public const string AuthErrorEventSubj = "$SYS.SERVER.{0}.CLIENT.AUTH.ERR"; + public const string AuthErrorAccountEventSubj = "$SYS.ACCOUNT.CLIENT.AUTH.ERR"; + + // Stats + public const string ServerStatsSubj = "$SYS.SERVER.{0}.STATSZ"; + public const string ServerDirectReqSubj = "$SYS.REQ.SERVER.{0}.{1}"; + public const string ServerPingReqSubj = "$SYS.REQ.SERVER.PING.{0}"; + public const string ServerStatsPingReqSubj = "$SYS.REQ.SERVER.PING"; // deprecated; use STATSZ variant + public const string ServerReloadReqSubj = "$SYS.REQ.SERVER.{0}.RELOAD"; + + // Leaf node + public const string LeafNodeConnectEventSubj = "$SYS.ACCOUNT.{0}.LEAFNODE.CONNECT"; // internal only + + // Latency + public const string RemoteLatencyEventSubj = "$SYS.LATENCY.M2.{0}"; + public const string InboxRespSubj = "$SYS._INBOX.{0}.{1}"; + + // User info + public const string UserDirectInfoSubj = "$SYS.REQ.USER.INFO"; + public const string UserDirectReqSubj = "$SYS.REQ.USER.{0}.INFO"; + + // Subscription count + public const string AccNumSubsReqSubj = "$SYS.REQ.ACCOUNT.NSUBS"; + + // Debug + public const string AccSubsSubj = "$SYS.DEBUG.SUBSCRIBERS"; + + // OCSP peer events + public const string OcspPeerRejectEventSubj = "$SYS.SERVER.{0}.OCSP.PEER.CONN.REJECT"; + public const string OcspPeerChainlinkInvalidEventSubj = "$SYS.SERVER.{0}.OCSP.PEER.LINK.INVALID"; + + // Parsing constants (token indexes / counts) + public const int AccLookupReqTokens = 6; + public const int ShutdownEventTokens = 4; + public const int ServerSubjectIndex = 2; + public const int AccUpdateTokensNew = 6; + public const int AccUpdateTokensOld = 5; + public const int AccUpdateAccIdxOld = 2; + public const int AccReqTokens = 5; + public const int AccReqAccIndex = 3; +} + +// ============================================================================ +// Advisory message type schema URI constants +// Mirrors the const string variables near each struct in server/events.go. +// ============================================================================ + +public static class EventMsgTypes +{ + public const string ConnectEventMsgType = "io.nats.server.advisory.v1.client_connect"; + public const string DisconnectEventMsgType = "io.nats.server.advisory.v1.client_disconnect"; + public const string OcspPeerRejectEventMsgType = "io.nats.server.advisory.v1.ocsp_peer_reject"; + public const string OcspPeerChainlinkInvalidEventMsgType = "io.nats.server.advisory.v1.ocsp_peer_link_invalid"; + public const string AccountNumConnsMsgType = "io.nats.server.advisory.v1.account_connections"; +} + +// ============================================================================ +// Heartbeat / rate-limit intervals (mirrors package-level vars in events.go) +// ============================================================================ + +/// +/// Default timing constants for server event heartbeats and rate limiting. +/// Mirrors Go package-level var declarations in events.go. +/// +public static class EventIntervals +{ + /// Default HB interval for events. Mirrors Go eventsHBInterval = 30s. + public static readonly TimeSpan EventsHbInterval = TimeSpan.FromSeconds(30); + + /// Default HB interval for stats. Mirrors Go statsHBInterval = 10s. + public static readonly TimeSpan StatsHbInterval = TimeSpan.FromSeconds(10); + + /// Minimum interval between statsz publishes. Mirrors Go defaultStatszRateLimit = 1s. + public static readonly TimeSpan DefaultStatszRateLimit = TimeSpan.FromSeconds(1); +} + +// ============================================================================ +// SysMsgHandler — delegate for internal system message dispatch +// Mirrors Go sysMsgHandler func type in events.go. +// ============================================================================ + +/// +/// Callback invoked when an internal system message is dispatched. +/// Mirrors Go sysMsgHandler in server/events.go. +/// +public delegate void SysMsgHandler( + Subscription sub, + NatsClient client, + Account acc, + string subject, + string reply, + byte[] hdr, + byte[] msg); + +// ============================================================================ +// InSysMsg — queued internal system message +// Mirrors Go inSysMsg struct in server/events.go. +// ============================================================================ + +/// +/// Holds a system message queued for internal delivery, avoiding the +/// route/gateway path. +/// Mirrors Go inSysMsg struct in server/events.go. +/// +internal sealed class InSysMsg +{ + public Subscription? Sub { get; set; } + public NatsClient? Client { get; set; } + public Account? Acc { get; set; } + public string Subject { get; set; } = string.Empty; + public string Reply { get; set; } = string.Empty; + public byte[]? Hdr { get; set; } + public byte[]? Msg { get; set; } + public SysMsgHandler? Cb { get; set; } +} + +// ============================================================================ +// InternalState — server internal/sys state +// Mirrors Go internal struct in server/events.go. +// Uses Monitor lock (lock(this)) in place of Go's embedded sync.Mutex. +// ============================================================================ + +/// +/// Holds all internal state used by the server's system-account event +/// machinery: account reference, client, send/receive queues, timers, +/// reply handlers, and heartbeat configuration. +/// Mirrors Go internal struct in server/events.go. +/// +internal sealed class InternalState +{ + // ---- identity / sequencing ---- + public Account? Account { get; set; } + public NatsClient? Client { get; set; } + public ulong Seq { get; set; } + public int Sid { get; set; } + + // ---- remote server tracking ---- + /// Map of server ID → serverUpdate. Mirrors Go servers map[string]*serverUpdate. + public Dictionary Servers { get; set; } = new(); + + // ---- timers ---- + /// Sweeper timer. Mirrors Go sweeper *time.Timer. + public System.Threading.Timer? Sweeper { get; set; } + + /// Stats heartbeat timer. Mirrors Go stmr *time.Timer. + public System.Threading.Timer? StatsMsgTimer { get; set; } + + // ---- reply handlers ---- + /// + /// Pending reply subject → handler map. + /// Mirrors Go replies map[string]msgHandler. + /// + public Dictionary> Replies { get; set; } = new(); + + // ---- queues ---- + /// Outbound message send queue. Mirrors Go sendq *ipQueue[*pubMsg]. + public IpQueue? SendQueue { get; set; } + + /// Inbound receive queue. Mirrors Go recvq *ipQueue[*inSysMsg]. + public IpQueue? RecvQueue { get; set; } + + /// Priority receive queue for STATSZ/Pings. Mirrors Go recvqp *ipQueue[*inSysMsg]. + public IpQueue? RecvQueuePriority { get; set; } + + /// Reset channel used to restart the send loop. Mirrors Go resetCh chan struct{}. + public System.Threading.Channels.Channel? ResetChannel { get; set; } + + // ---- durations ---- + /// Maximum time before an orphaned server entry is removed. Mirrors Go orphMax. + public TimeSpan OrphanMax { get; set; } + + /// Interval at which orphan checks run. Mirrors Go chkOrph. + public TimeSpan CheckOrphan { get; set; } + + /// Interval between statsz publishes. Mirrors Go statsz. + public TimeSpan StatszInterval { get; set; } + + /// Client-facing statsz interval. Mirrors Go cstatsz. + public TimeSpan ClientStatszInterval { get; set; } + + // ---- misc ---- + /// Short hash used for shared-inbox routing. Mirrors Go shash string. + public string ShortHash { get; set; } = string.Empty; + + /// Inbox prefix for this server's internal client. Mirrors Go inboxPre string. + public string InboxPrefix { get; set; } = string.Empty; + + /// Subscription for remote stats. Mirrors Go remoteStatsSub *subscription. + public Subscription? RemoteStatsSub { get; set; } + + /// Time of the last statsz publish. Mirrors Go lastStatsz time.Time. + public DateTime LastStatsz { get; set; } +} + +// ============================================================================ +// ServerUpdate — remote server heartbeat tracking +// Mirrors Go serverUpdate struct in server/events.go. +// ============================================================================ + +/// +/// Tracks the sequence number and last-seen timestamp of a remote server's +/// system heartbeat. Used to detect orphaned servers. +/// Mirrors Go serverUpdate struct in server/events.go. +/// +internal sealed class ServerUpdate +{ + /// Last sequence number received from the remote server. + public ulong Seq { get; set; } + + /// Wall-clock time of the last heartbeat. + public DateTime LTime { get; set; } +} + +// ============================================================================ +// PubMsg — internally-queued outbound publish message +// Mirrors Go pubMsg struct in server/events.go. +// ============================================================================ + +/// +/// Holds an outbound message that the server wants to publish via the internal +/// send loop, avoiding direct route/gateway writes. +/// Mirrors Go pubMsg struct in server/events.go. +/// +internal sealed class PubMsg +{ + public NatsClient? Client { get; set; } + public string Subject { get; set; } = string.Empty; + public string Reply { get; set; } = string.Empty; + public ServerInfo? Si { get; set; } + public byte[]? Hdr { get; set; } + public object? Msg { get; set; } + + /// Compression type. TODO: session 12 — wire up compressionType enum. + public int Oct { get; set; } + + public bool Echo { get; set; } + public bool Last { get; set; } + + // TODO: session 12 — add pool return helper (returnToPool). +} + +// ============================================================================ +// DataStats — message/byte counter pair (sent or received) +// Mirrors Go DataStats struct in server/events.go. +// ============================================================================ + +/// +/// Reports how many messages and bytes were sent or received. +/// Optionally breaks out gateway, route, and leaf-node traffic. +/// Mirrors Go DataStats struct in server/events.go. +/// +public sealed class DataStats +{ + [JsonPropertyName("msgs")] + public long Msgs { get; set; } + + [JsonPropertyName("bytes")] + public long Bytes { get; set; } + + [JsonPropertyName("gateways")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public MsgBytes? Gateways { get; set; } + + [JsonPropertyName("routes")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public MsgBytes? Routes { get; set; } + + [JsonPropertyName("leafs")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public MsgBytes? Leafs { get; set; } +} + +// ============================================================================ +// MsgBytes — simple message+byte pair used inside DataStats +// Mirrors Go MsgBytes struct in server/events.go. +// ============================================================================ + +/// +/// A simple pair of message and byte counts, used as a nested breakdown +/// inside . +/// Mirrors Go MsgBytes struct in server/events.go. +/// +public sealed class MsgBytes +{ + [JsonPropertyName("msgs")] + public long Msgs { get; set; } + + [JsonPropertyName("bytes")] + public long Bytes { get; set; } +} + +// ============================================================================ +// RouteStat / GatewayStat — per-route and per-gateway stat snapshots +// Mirrors Go RouteStat and GatewayStat in server/events.go. +// ============================================================================ + +/// +/// Statistics snapshot for a single cluster route connection. +/// Mirrors Go RouteStat in server/events.go. +/// +public sealed class RouteStat +{ + [JsonPropertyName("rid")] + public ulong Id { get; set; } + + [JsonPropertyName("name")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Name { get; set; } + + [JsonPropertyName("sent")] + public DataStats Sent { get; set; } = new(); + + [JsonPropertyName("received")] + public DataStats Received { get; set; } = new(); + + [JsonPropertyName("pending")] + public int Pending { get; set; } +} + +/// +/// Statistics snapshot for a gateway connection. +/// Mirrors Go GatewayStat in server/events.go. +/// +public sealed class GatewayStat +{ + [JsonPropertyName("gwid")] + public ulong Id { get; set; } + + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; + + [JsonPropertyName("sent")] + public DataStats Sent { get; set; } = new(); + + [JsonPropertyName("received")] + public DataStats Received { get; set; } = new(); + + [JsonPropertyName("inbound_connections")] + public int NumInbound { get; set; } +} + +// ============================================================================ +// ServerStatsMsg — periodic stats advisory published on $SYS.SERVER.{id}.STATSZ +// Mirrors Go ServerStatsMsg struct in server/events.go. +// ============================================================================ + +/// +/// Periodic advisory message containing the current server statistics. +/// Mirrors Go ServerStatsMsg struct in server/events.go. +/// +public sealed class ServerStatsMsg +{ + [JsonPropertyName("server")] + public ServerInfo Server { get; set; } = new(); + + [JsonPropertyName("statsz")] + public ServerStatsAdvisory Stats { get; set; } = new(); +} + +// ============================================================================ +// ServerStatsAdvisory — the statsz payload inside ServerStatsMsg +// Mirrors Go ServerStats struct (advisory form) in server/events.go. +// NOTE: distinct from the internal ServerStats in NatsServerTypes.cs. +// ============================================================================ + +/// +/// The JSON-serialisable statistics payload included inside . +/// Mirrors Go ServerStats struct (advisory form) in server/events.go. +/// +public sealed class ServerStatsAdvisory +{ + [JsonPropertyName("start")] + public DateTime Start { get; set; } + + [JsonPropertyName("mem")] + public long Mem { get; set; } + + [JsonPropertyName("cores")] + public int Cores { get; set; } + + [JsonPropertyName("cpu")] + public double Cpu { get; set; } + + [JsonPropertyName("connections")] + public int Connections { get; set; } + + [JsonPropertyName("total_connections")] + public ulong TotalConnections { get; set; } + + [JsonPropertyName("active_accounts")] + public int ActiveAccounts { get; set; } + + [JsonPropertyName("subscriptions")] + public uint NumSubs { get; set; } + + [JsonPropertyName("sent")] + public DataStats Sent { get; set; } = new(); + + [JsonPropertyName("received")] + public DataStats Received { get; set; } = new(); + + [JsonPropertyName("slow_consumers")] + public long SlowConsumers { get; set; } + + [JsonPropertyName("slow_consumer_stats")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public SlowConsumersStats? SlowConsumersStats { get; set; } + + [JsonPropertyName("stale_connections")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public long StaleConnections { get; set; } + + [JsonPropertyName("stale_connection_stats")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public StaleConnectionStats? StaleConnectionStats { get; set; } + + [JsonPropertyName("stalled_clients")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public long StalledClients { get; set; } + + [JsonPropertyName("routes")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public List? Routes { get; set; } + + [JsonPropertyName("gateways")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public List? Gateways { get; set; } + + [JsonPropertyName("active_servers")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public int ActiveServers { get; set; } + + /// JetStream stats. TODO: session 19 — wire JetStreamVarz type. + [JsonPropertyName("jetstream")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public object? JetStream { get; set; } + + [JsonPropertyName("gomemlimit")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public long MemLimit { get; set; } + + [JsonPropertyName("gomaxprocs")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public int MaxProcs { get; set; } +} + +// ============================================================================ +// SlowConsumersStats / StaleConnectionStats — advisory-layer per-kind counters +// These are the JSON-serialisable variants used in ServerStatsAdvisory. +// The internal atomic counters live in NatsServerTypes.cs (SlowConsumerStats / +// StaleConnectionStats with different casing). +// ============================================================================ + +/// +/// Per-kind slow-consumer counters included in stats advisories. +/// Mirrors Go SlowConsumersStats in server/monitor.go. +/// +public sealed class SlowConsumersStats +{ + [JsonPropertyName("clients")] + public ulong Clients { get; set; } + + [JsonPropertyName("routes")] + public ulong Routes { get; set; } + + [JsonPropertyName("gateways")] + public ulong Gateways { get; set; } + + [JsonPropertyName("leafs")] + public ulong Leafs { get; set; } +} + +/// +/// Per-kind stale-connection counters included in stats advisories. +/// Mirrors Go StaleConnectionStats in server/monitor.go. +/// +public sealed class StaleConnectionStats +{ + [JsonPropertyName("clients")] + public ulong Clients { get; set; } + + [JsonPropertyName("routes")] + public ulong Routes { get; set; } + + [JsonPropertyName("gateways")] + public ulong Gateways { get; set; } + + [JsonPropertyName("leafs")] + public ulong Leafs { get; set; } +} + +// ============================================================================ +// ConnectEventMsg / DisconnectEventMsg — client lifecycle advisories +// Mirrors Go structs in server/events.go. +// ============================================================================ + +/// +/// Advisory published on $SYS.ACCOUNT.{acc}.CONNECT when a new +/// client connection is established within a tracked account. +/// Mirrors Go ConnectEventMsg in server/events.go. +/// +public sealed class ConnectEventMsg : TypedEvent +{ + [JsonPropertyName("server")] + public ServerInfo Server { get; set; } = new(); + + [JsonPropertyName("client")] + public ClientInfo Client { get; set; } = new(); +} + +/// +/// Advisory published on $SYS.ACCOUNT.{acc}.DISCONNECT when a +/// previously-tracked client connection closes. +/// Mirrors Go DisconnectEventMsg in server/events.go. +/// +public sealed class DisconnectEventMsg : TypedEvent +{ + [JsonPropertyName("server")] + public ServerInfo Server { get; set; } = new(); + + [JsonPropertyName("client")] + public ClientInfo Client { get; set; } = new(); + + [JsonPropertyName("sent")] + public DataStats Sent { get; set; } = new(); + + [JsonPropertyName("received")] + public DataStats Received { get; set; } = new(); + + [JsonPropertyName("reason")] + public string Reason { get; set; } = string.Empty; +} + +// ============================================================================ +// OCSPPeerRejectEventMsg / OCSPPeerChainlinkInvalidEventMsg +// Mirrors Go structs in server/events.go. +// ============================================================================ + +/// +/// Advisory published when a peer TLS handshake is rejected due to OCSP +/// invalidation of the peer's leaf certificate. +/// Mirrors Go OCSPPeerRejectEventMsg in server/events.go. +/// +public sealed class OcspPeerRejectEventMsg : TypedEvent +{ + [JsonPropertyName("kind")] + public string Kind { get; set; } = string.Empty; + + [JsonPropertyName("peer")] + public CertInfo Peer { get; set; } = new(); + + [JsonPropertyName("server")] + public ServerInfo Server { get; set; } = new(); + + [JsonPropertyName("reason")] + public string Reason { get; set; } = string.Empty; +} + +/// +/// Advisory published when a certificate in a valid TLS chain is found to be +/// OCSP-invalid during a peer handshake. Both the invalid link and the +/// peer's leaf cert are included. +/// Mirrors Go OCSPPeerChainlinkInvalidEventMsg in server/events.go. +/// +public sealed class OcspPeerChainlinkInvalidEventMsg : TypedEvent +{ + [JsonPropertyName("link")] + public CertInfo Link { get; set; } = new(); + + [JsonPropertyName("peer")] + public CertInfo Peer { get; set; } = new(); + + [JsonPropertyName("server")] + public ServerInfo Server { get; set; } = new(); + + [JsonPropertyName("reason")] + public string Reason { get; set; } = string.Empty; +} + +// ============================================================================ +// AccountNumConns / AccountStat — account connection count advisories +// Mirrors Go structs in server/events.go. +// ============================================================================ + +/// +/// Advisory heartbeat published when the connection count for a tracked +/// account changes, or on a periodic schedule. +/// Mirrors Go AccountNumConns struct in server/events.go. +/// +public sealed class AccountNumConns : TypedEvent +{ + [JsonPropertyName("server")] + public ServerInfo Server { get; set; } = new(); + + // Embedded AccountStat fields are inlined via composition. + [JsonPropertyName("acc")] + public string Account { get; set; } = string.Empty; + + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; + + [JsonPropertyName("conns")] + public int Conns { get; set; } + + [JsonPropertyName("leafnodes")] + public int LeafNodes { get; set; } + + [JsonPropertyName("total_conns")] + public int TotalConns { get; set; } + + [JsonPropertyName("num_subscriptions")] + public uint NumSubs { get; set; } + + [JsonPropertyName("sent")] + public DataStats Sent { get; set; } = new(); + + [JsonPropertyName("received")] + public DataStats Received { get; set; } = new(); + + [JsonPropertyName("slow_consumers")] + public long SlowConsumers { get; set; } +} + +/// +/// Statistic data common to and account-level +/// monitoring responses. +/// Mirrors Go AccountStat struct in server/events.go. +/// +public sealed class AccountStat +{ + [JsonPropertyName("acc")] + public string Account { get; set; } = string.Empty; + + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; + + [JsonPropertyName("conns")] + public int Conns { get; set; } + + [JsonPropertyName("leafnodes")] + public int LeafNodes { get; set; } + + [JsonPropertyName("total_conns")] + public int TotalConns { get; set; } + + [JsonPropertyName("num_subscriptions")] + public uint NumSubs { get; set; } + + [JsonPropertyName("sent")] + public DataStats Sent { get; set; } = new(); + + [JsonPropertyName("received")] + public DataStats Received { get; set; } = new(); + + [JsonPropertyName("slow_consumers")] + public long SlowConsumers { get; set; } +} + +/// +/// Internal request payload sent when this server first starts tracking an +/// account, asking peer servers for their local connection counts. +/// Mirrors Go accNumConnsReq struct in server/events.go. +/// +internal sealed class AccNumConnsReq +{ + [JsonPropertyName("server")] + public ServerInfo Server { get; set; } = new(); + + [JsonPropertyName("acc")] + public string Account { get; set; } = string.Empty; +} + +// ============================================================================ +// ServerCapability / ServerID — server identity and capability flags +// Mirrors Go types in server/events.go. +// ============================================================================ + +/// +/// Bit-flag capability set for a remote server. +/// Mirrors Go ServerCapability uint64 in server/events.go. +/// +[Flags] +public enum ServerCapability : ulong +{ + /// No capabilities. + None = 0, + + /// Server has JetStream enabled. Mirrors Go JetStreamEnabled. + JetStreamEnabled = 1UL << 0, + + /// New stream snapshot capability. Mirrors Go BinaryStreamSnapshot. + BinaryStreamSnapshot = 1UL << 1, + + /// Move NRG traffic out of system account. Mirrors Go AccountNRG. + AccountNrg = 1UL << 2, +} + +/// +/// Minimal static identity for a remote server (name, host, ID). +/// Mirrors Go ServerID struct in server/events.go. +/// +public sealed class ServerIdentity +{ + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; + + [JsonPropertyName("host")] + public string Host { get; set; } = string.Empty; + + [JsonPropertyName("id")] + public string Id { get; set; } = string.Empty; +} diff --git a/dotnet/src/ZB.MOM.NatsNet.Server/MessageTrace/MsgTraceTypes.cs b/dotnet/src/ZB.MOM.NatsNet.Server/MessageTrace/MsgTraceTypes.cs new file mode 100644 index 0000000..da75fa9 --- /dev/null +++ b/dotnet/src/ZB.MOM.NatsNet.Server/MessageTrace/MsgTraceTypes.cs @@ -0,0 +1,465 @@ +// Copyright 2024-2026 The NATS Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// Adapted from server/msgtrace.go in the NATS server Go source. + +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace ZB.MOM.NatsNet.Server; + +// ============================================================================ +// Message-trace header name constants +// Mirrors Go const block at top of server/msgtrace.go. +// ============================================================================ + +/// +/// NATS message-trace header names and special sentinel values. +/// Mirrors Go const block in server/msgtrace.go. +/// +public static class MsgTraceHeaders +{ + /// Header that carries the trace destination subject. Mirrors Go MsgTraceDest. + public const string MsgTraceDest = "Nats-Trace-Dest"; + + /// + /// Sentinel value placed in the trace-dest header to disable tracing + /// (must be an invalid NATS subject). Mirrors Go MsgTraceDestDisabled. + /// + public const string MsgTraceDestDisabled = "trace disabled"; + + /// Header used for hop-count tracking across servers. Mirrors Go MsgTraceHop. + public const string MsgTraceHop = "Nats-Trace-Hop"; + + /// Header that carries the originating account name. Mirrors Go MsgTraceOriginAccount. + public const string MsgTraceOriginAccount = "Nats-Trace-Origin-Account"; + + /// + /// When set to a truthy value, the message is consumed only for tracing + /// and not delivered to subscribers. Mirrors Go MsgTraceOnly. + /// + public const string MsgTraceOnly = "Nats-Trace-Only"; + + /// + /// W3C trace-context parent header. NATS no longer lower-cases this but + /// accepts it in any case. Mirrors Go traceParentHdr (internal). + /// + public const string TraceParentHdr = "traceparent"; +} + +// ============================================================================ +// MsgTraceType — discriminator string for polymorphic trace event lists +// Mirrors Go MsgTraceType string in server/msgtrace.go. +// ============================================================================ + +/// +/// Discriminator string identifying the concrete type of a trace event +/// within a list. +/// Mirrors Go MsgTraceType string and its constants in server/msgtrace.go. +/// +public sealed class MsgTraceType +{ + private readonly string _value; + private MsgTraceType(string value) => _value = value; + + /// + public override string ToString() => _value; + + public static implicit operator MsgTraceType(string value) => new(value); + public static implicit operator string(MsgTraceType t) => t._value; + + public override bool Equals(object? obj) => + obj is MsgTraceType other && _value == other._value; + + public override int GetHashCode() => _value.GetHashCode(); + + // ---- Well-known type constants (mirror Go const block) ---- + + /// Ingress event. Mirrors Go MsgTraceIngressType = "in". + public static readonly MsgTraceType Ingress = new("in"); + + /// Subject-mapping event. Mirrors Go MsgTraceSubjectMappingType = "sm". + public static readonly MsgTraceType SubjectMapping = new("sm"); + + /// Stream-export event. Mirrors Go MsgTraceStreamExportType = "se". + public static readonly MsgTraceType StreamExport = new("se"); + + /// Service-import event. Mirrors Go MsgTraceServiceImportType = "si". + public static readonly MsgTraceType ServiceImport = new("si"); + + /// JetStream storage event. Mirrors Go MsgTraceJetStreamType = "js". + public static readonly MsgTraceType JetStream = new("js"); + + /// Egress (delivery) event. Mirrors Go MsgTraceEgressType = "eg". + public static readonly MsgTraceType Egress = new("eg"); +} + +// ============================================================================ +// IMsgTrace — interface for polymorphic trace events +// Mirrors Go MsgTrace interface in server/msgtrace.go. +// ============================================================================ + +/// +/// Marker interface implemented by all concrete message-trace event types. +/// Enables polymorphic handling of the list. +/// Mirrors Go MsgTrace interface in server/msgtrace.go. +/// +public interface IMsgTrace +{ + /// Returns the discriminator type string for this trace event. + string Typ(); +} + +// ============================================================================ +// MsgTraceBase — shared fields present in every trace event +// Mirrors Go MsgTraceBase struct in server/msgtrace.go. +// ============================================================================ + +/// +/// Common base fields shared by all concrete message-trace event types. +/// Mirrors Go MsgTraceBase struct in server/msgtrace.go. +/// +public class MsgTraceBase : IMsgTrace +{ + [JsonPropertyName("type")] + public string Type { get; set; } = string.Empty; + + [JsonPropertyName("ts")] + public DateTime Timestamp { get; set; } + + /// + public virtual string Typ() => Type; +} + +// ============================================================================ +// MsgTraceIngress — client / route / gateway / leaf connection ingress event +// Mirrors Go MsgTraceIngress struct in server/msgtrace.go. +// ============================================================================ + +/// +/// Records the point at which a message was received by the server from a +/// client, route, gateway, or leaf connection. +/// Mirrors Go MsgTraceIngress struct in server/msgtrace.go. +/// +public sealed class MsgTraceIngress : MsgTraceBase +{ + [JsonPropertyName("kind")] + public int Kind { get; set; } + + [JsonPropertyName("cid")] + public ulong Cid { get; set; } + + [JsonPropertyName("name")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Name { get; set; } + + [JsonPropertyName("acc")] + public string Account { get; set; } = string.Empty; + + [JsonPropertyName("subj")] + public string Subject { get; set; } = string.Empty; + + [JsonPropertyName("error")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Error { get; set; } +} + +// ============================================================================ +// MsgTraceSubjectMapping — subject-mapping rewrite event +// Mirrors Go MsgTraceSubjectMapping struct in server/msgtrace.go. +// ============================================================================ + +/// +/// Records a subject-mapping rewrite applied to an in-flight message. +/// Mirrors Go MsgTraceSubjectMapping struct in server/msgtrace.go. +/// +public sealed class MsgTraceSubjectMapping : MsgTraceBase +{ + [JsonPropertyName("to")] + public string MappedTo { get; set; } = string.Empty; +} + +// ============================================================================ +// MsgTraceStreamExport — stream export / cross-account delivery event +// Mirrors Go MsgTraceStreamExport struct in server/msgtrace.go. +// ============================================================================ + +/// +/// Records delivery of a message to a stream-export destination account. +/// Mirrors Go MsgTraceStreamExport struct in server/msgtrace.go. +/// +public sealed class MsgTraceStreamExport : MsgTraceBase +{ + [JsonPropertyName("acc")] + public string Account { get; set; } = string.Empty; + + [JsonPropertyName("to")] + public string To { get; set; } = string.Empty; +} + +// ============================================================================ +// MsgTraceServiceImport — service import routing event +// Mirrors Go MsgTraceServiceImport struct in server/msgtrace.go. +// ============================================================================ + +/// +/// Records routing of a message via a service-import from one account to +/// another. +/// Mirrors Go MsgTraceServiceImport struct in server/msgtrace.go. +/// +public sealed class MsgTraceServiceImport : MsgTraceBase +{ + [JsonPropertyName("acc")] + public string Account { get; set; } = string.Empty; + + [JsonPropertyName("from")] + public string From { get; set; } = string.Empty; + + [JsonPropertyName("to")] + public string To { get; set; } = string.Empty; +} + +// ============================================================================ +// MsgTraceJetStream — JetStream storage event +// Mirrors Go MsgTraceJetStream struct in server/msgtrace.go. +// ============================================================================ + +/// +/// Records the attempt (and outcome) of storing or delivering a message +/// to a JetStream stream. +/// Mirrors Go MsgTraceJetStream struct in server/msgtrace.go. +/// +public sealed class MsgTraceJetStream : MsgTraceBase +{ + [JsonPropertyName("stream")] + public string Stream { get; set; } = string.Empty; + + [JsonPropertyName("subject")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Subject { get; set; } + + [JsonPropertyName("nointerest")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public bool NoInterest { get; set; } + + [JsonPropertyName("error")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Error { get; set; } +} + +// ============================================================================ +// MsgTraceEgress — outbound delivery event +// Mirrors Go MsgTraceEgress struct in server/msgtrace.go. +// ============================================================================ + +/// +/// Records the outbound delivery of a message to a subscriber, route, +/// gateway, or leaf connection. +/// Mirrors Go MsgTraceEgress struct in server/msgtrace.go. +/// +public sealed class MsgTraceEgress : MsgTraceBase +{ + [JsonPropertyName("kind")] + public int Kind { get; set; } + + [JsonPropertyName("cid")] + public ulong Cid { get; set; } + + [JsonPropertyName("name")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Name { get; set; } + + [JsonPropertyName("hop")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Hop { get; set; } + + [JsonPropertyName("acc")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Account { get; set; } + + [JsonPropertyName("sub")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Subscription { get; set; } + + [JsonPropertyName("queue")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Queue { get; set; } + + [JsonPropertyName("error")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Error { get; set; } + + /// + /// Optional link to the produced by the remote + /// server that received this egress message (route/leaf/gateway hop). + /// Not serialised. Mirrors Go Link *MsgTraceEvent. + /// + [JsonIgnore] + public MsgTraceEvent? Link { get; set; } +} + +// ============================================================================ +// MsgTraceEvents — polymorphic list with custom JSON deserialiser +// Mirrors Go MsgTraceEvents []MsgTrace and its UnmarshalJSON in msgtrace.go. +// ============================================================================ + +/// +/// Custom JSON converter that deserialises a MsgTraceEvents JSON array +/// into the correct concrete subtype, using the +/// "type" discriminator field. +/// Mirrors Go MsgTraceEvents.UnmarshalJSON in server/msgtrace.go. +/// +public sealed class MsgTraceEventsConverter : JsonConverter> +{ + private static readonly Dictionary> Factories = new() + { + ["in"] = e => e.Deserialize()!, + ["sm"] = e => e.Deserialize()!, + ["se"] = e => e.Deserialize()!, + ["si"] = e => e.Deserialize()!, + ["js"] = e => e.Deserialize()!, + ["eg"] = e => e.Deserialize()!, + }; + + public override List Read( + ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options) + { + var result = new List(); + using var doc = JsonDocument.ParseValue(ref reader); + + foreach (var element in doc.RootElement.EnumerateArray()) + { + if (!element.TryGetProperty("type", out var typeProp)) + throw new JsonException("MsgTrace element missing 'type' field."); + + var typeStr = typeProp.GetString() ?? string.Empty; + + if (!Factories.TryGetValue(typeStr, out var factory)) + throw new JsonException($"Unknown MsgTrace type '{typeStr}'."); + + result.Add(factory(element)); + } + + return result; + } + + public override void Write( + Utf8JsonWriter writer, + List value, + JsonSerializerOptions options) + { + writer.WriteStartArray(); + foreach (var item in value) + JsonSerializer.Serialize(writer, item, item.GetType(), options); + writer.WriteEndArray(); + } +} + +// ============================================================================ +// MsgTraceRequest — the original request metadata included in a trace event +// Mirrors Go MsgTraceRequest struct in server/msgtrace.go. +// ============================================================================ + +/// +/// Captures the headers and size of the original message that triggered a +/// trace event. +/// Mirrors Go MsgTraceRequest struct in server/msgtrace.go. +/// +public sealed class MsgTraceRequest +{ + /// + /// Original message headers, preserving header-name casing. + /// Mirrors Go Header map[string][]string (not http.Header, so casing is preserved). + /// + [JsonPropertyName("header")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public Dictionary>? Header { get; set; } + + [JsonPropertyName("msgsize")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public int MsgSize { get; set; } +} + +// ============================================================================ +// MsgTraceEvent — top-level trace event published to the trace destination +// Mirrors Go MsgTraceEvent struct in server/msgtrace.go. +// ============================================================================ + +/// +/// The top-level message-trace advisory published to the trace destination +/// subject. Contains server identity, the original request metadata, the +/// hop count, and the ordered list of trace events. +/// Mirrors Go MsgTraceEvent struct in server/msgtrace.go. +/// +public sealed class MsgTraceEvent +{ + [JsonPropertyName("server")] + public ServerInfo Server { get; set; } = new(); + + [JsonPropertyName("request")] + public MsgTraceRequest Request { get; set; } = new(); + + [JsonPropertyName("hops")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public int Hops { get; set; } + + [JsonPropertyName("events")] + [JsonConverter(typeof(MsgTraceEventsConverter))] + public List Events { get; set; } = []; + + // ---- Convenience accessors (mirrors Go helper methods on MsgTraceEvent) ---- + + /// + /// Returns the first event if it is a , else null. + /// Mirrors Go MsgTraceEvent.Ingress(). + /// + public MsgTraceIngress? Ingress() => + Events.Count > 0 ? Events[0] as MsgTraceIngress : null; + + /// + /// Returns the first in the event list, or null. + /// Mirrors Go MsgTraceEvent.SubjectMapping(). + /// + public MsgTraceSubjectMapping? SubjectMapping() => + Events.OfType().FirstOrDefault(); + + /// + /// Returns all events. + /// Mirrors Go MsgTraceEvent.StreamExports(). + /// + public IReadOnlyList StreamExports() => + Events.OfType().ToList(); + + /// + /// Returns all events. + /// Mirrors Go MsgTraceEvent.ServiceImports(). + /// + public IReadOnlyList ServiceImports() => + Events.OfType().ToList(); + + /// + /// Returns the first event, or null. + /// Mirrors Go MsgTraceEvent.JetStream(). + /// + public MsgTraceJetStream? JetStream() => + Events.OfType().FirstOrDefault(); + + /// + /// Returns all events. + /// Mirrors Go MsgTraceEvent.Egresses(). + /// + public IReadOnlyList Egresses() => + Events.OfType().ToList(); +} diff --git a/dotnet/src/ZB.MOM.NatsNet.Server/Monitor/MonitorSortOptions.cs b/dotnet/src/ZB.MOM.NatsNet.Server/Monitor/MonitorSortOptions.cs new file mode 100644 index 0000000..9cc1167 --- /dev/null +++ b/dotnet/src/ZB.MOM.NatsNet.Server/Monitor/MonitorSortOptions.cs @@ -0,0 +1,294 @@ +// Copyright 2013-2026 The NATS Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// Adapted from server/monitor_sort_opts.go in the NATS server Go source. + +using System.Text.Json.Serialization; + +namespace ZB.MOM.NatsNet.Server; + +// ============================================================================ +// SortOpt — string wrapper type for connection-list sort options +// Mirrors Go SortOpt string in server/monitor_sort_opts.go. +// ============================================================================ + +/// +/// A strongly-typed sort option for . +/// Wraps a raw string value corresponding to the JSON sort key. +/// Mirrors Go SortOpt string in server/monitor_sort_opts.go. +/// +public sealed class SortOpt +{ + private readonly string _value; + + private SortOpt(string value) => _value = value; + + /// Returns the raw sort-option string value. + public override string ToString() => _value; + + /// Allows implicit conversion from a string literal. + public static implicit operator SortOpt(string value) => new(value); + + /// Allows implicit conversion back to a plain string. + public static implicit operator string(SortOpt opt) => opt._value; + + public override bool Equals(object? obj) => + obj is SortOpt other && _value == other._value; + + public override int GetHashCode() => _value.GetHashCode(); + + // ---- Well-known sort-option constants ---- + // Mirrors Go const block in monitor_sort_opts.go. + + /// Sort by connection ID (ascending). Mirrors Go ByCid = "cid". + public static readonly SortOpt ByCid = new("cid"); + + /// Sort by connection start time (same as ByCid). Mirrors Go ByStart = "start". + public static readonly SortOpt ByStart = new("start"); + + /// Sort by number of subscriptions (descending). Mirrors Go BySubs = "subs". + public static readonly SortOpt BySubs = new("subs"); + + /// Sort by pending bytes waiting to be sent (descending). Mirrors Go ByPending = "pending". + public static readonly SortOpt ByPending = new("pending"); + + /// Sort by number of outbound messages (descending). Mirrors Go ByOutMsgs = "msgs_to". + public static readonly SortOpt ByOutMsgs = new("msgs_to"); + + /// Sort by number of inbound messages (descending). Mirrors Go ByInMsgs = "msgs_from". + public static readonly SortOpt ByInMsgs = new("msgs_from"); + + /// Sort by bytes sent (descending). Mirrors Go ByOutBytes = "bytes_to". + public static readonly SortOpt ByOutBytes = new("bytes_to"); + + /// Sort by bytes received (descending). Mirrors Go ByInBytes = "bytes_from". + public static readonly SortOpt ByInBytes = new("bytes_from"); + + /// Sort by last activity time (descending). Mirrors Go ByLast = "last". + public static readonly SortOpt ByLast = new("last"); + + /// Sort by idle duration (descending). Mirrors Go ByIdle = "idle". + public static readonly SortOpt ByIdle = new("idle"); + + /// Sort by uptime (descending). Mirrors Go ByUptime = "uptime". + public static readonly SortOpt ByUptime = new("uptime"); + + /// Sort by stop time — only valid on closed connections. Mirrors Go ByStop = "stop". + public static readonly SortOpt ByStop = new("stop"); + + /// Sort by close reason — only valid on closed connections. Mirrors Go ByReason = "reason". + public static readonly SortOpt ByReason = new("reason"); + + /// Sort by round-trip time (descending). Mirrors Go ByRTT = "rtt". + public static readonly SortOpt ByRtt = new("rtt"); + + private static readonly HashSet ValidValues = + [ + "", "cid", "start", "subs", "pending", + "msgs_to", "msgs_from", "bytes_to", "bytes_from", + "last", "idle", "uptime", "stop", "reason", "rtt" + ]; + + /// + /// Returns true if this sort option is a recognised value. + /// Mirrors Go SortOpt.IsValid() in monitor_sort_opts.go. + /// + public bool IsValid() => ValidValues.Contains(_value); +} + +// ============================================================================ +// ConnInfos — sortable list wrapper for ConnInfo pointers +// Mirrors Go ConnInfos []*ConnInfo in monitor_sort_opts.go. +// ============================================================================ + +/// +/// A list of objects that can be sorted using one of +/// the SortBy* comparers defined in this file. +/// Mirrors Go ConnInfos []*ConnInfo in server/monitor_sort_opts.go. +/// +public sealed class ConnInfos : List +{ + public ConnInfos() { } + public ConnInfos(IEnumerable items) : base(items) { } +} + +// ============================================================================ +// IComparer implementations — one per sort option +// Each class mirrors the corresponding Less() method in monitor_sort_opts.go. +// ============================================================================ + +/// Sort by connection ID (ascending). Mirrors Go SortByCid. +public sealed class SortByCid : IComparer +{ + public static readonly SortByCid Instance = new(); + public int Compare(ConnInfo? x, ConnInfo? y) + { + if (x is null || y is null) return 0; + return x.Cid.CompareTo(y.Cid); + } +} + +/// Sort by number of subscriptions (ascending for underlying sort; caller reverses if needed). +/// Mirrors Go SortBySubs. +public sealed class SortBySubs : IComparer +{ + public static readonly SortBySubs Instance = new(); + public int Compare(ConnInfo? x, ConnInfo? y) + { + if (x is null || y is null) return 0; + return x.NumSubs.CompareTo(y.NumSubs); + } +} + +/// Sort by pending bytes. Mirrors Go SortByPending. +public sealed class SortByPending : IComparer +{ + public static readonly SortByPending Instance = new(); + public int Compare(ConnInfo? x, ConnInfo? y) + { + if (x is null || y is null) return 0; + return x.Pending.CompareTo(y.Pending); + } +} + +/// Sort by outbound message count. Mirrors Go SortByOutMsgs. +public sealed class SortByOutMsgs : IComparer +{ + public static readonly SortByOutMsgs Instance = new(); + public int Compare(ConnInfo? x, ConnInfo? y) + { + if (x is null || y is null) return 0; + return x.OutMsgs.CompareTo(y.OutMsgs); + } +} + +/// Sort by inbound message count. Mirrors Go SortByInMsgs. +public sealed class SortByInMsgs : IComparer +{ + public static readonly SortByInMsgs Instance = new(); + public int Compare(ConnInfo? x, ConnInfo? y) + { + if (x is null || y is null) return 0; + return x.InMsgs.CompareTo(y.InMsgs); + } +} + +/// Sort by outbound bytes. Mirrors Go SortByOutBytes. +public sealed class SortByOutBytes : IComparer +{ + public static readonly SortByOutBytes Instance = new(); + public int Compare(ConnInfo? x, ConnInfo? y) + { + if (x is null || y is null) return 0; + return x.OutBytes.CompareTo(y.OutBytes); + } +} + +/// Sort by inbound bytes. Mirrors Go SortByInBytes. +public sealed class SortByInBytes : IComparer +{ + public static readonly SortByInBytes Instance = new(); + public int Compare(ConnInfo? x, ConnInfo? y) + { + if (x is null || y is null) return 0; + return x.InBytes.CompareTo(y.InBytes); + } +} + +/// Sort by last activity timestamp. Mirrors Go SortByLast. +public sealed class SortByLast : IComparer +{ + public static readonly SortByLast Instance = new(); + public int Compare(ConnInfo? x, ConnInfo? y) + { + if (x is null || y is null) return 0; + return x.LastActivity.CompareTo(y.LastActivity); + } +} + +/// +/// Sort by idle duration (time since last activity), relative to a supplied +/// reference time. Mirrors Go SortByIdle. +/// +public sealed class SortByIdle : IComparer +{ + private readonly DateTime _now; + + public SortByIdle(DateTime now) => _now = now; + + public int Compare(ConnInfo? x, ConnInfo? y) + { + if (x is null || y is null) return 0; + var idleX = _now - x.LastActivity; + var idleY = _now - y.LastActivity; + return idleX.CompareTo(idleY); + } +} + +/// +/// Sort by uptime (time the connection has been open), relative to a supplied +/// reference time. Mirrors Go SortByUptime. +/// +public sealed class SortByUptime : IComparer +{ + private readonly DateTime _now; + + public SortByUptime(DateTime now) => _now = now; + + public int Compare(ConnInfo? x, ConnInfo? y) + { + if (x is null || y is null) return 0; + var uptimeX = (x.Stop is null || x.Stop == default) ? _now - x.Start : x.Stop.Value - x.Start; + var uptimeY = (y.Stop is null || y.Stop == default) ? _now - y.Start : y.Stop.Value - y.Start; + return uptimeX.CompareTo(uptimeY); + } +} + +/// Sort by stop time (closed connections only). Mirrors Go SortByStop. +public sealed class SortByStop : IComparer +{ + public static readonly SortByStop Instance = new(); + public int Compare(ConnInfo? x, ConnInfo? y) + { + if (x is null || y is null) return 0; + // If either stop is null treat as zero (shouldn't happen for closed-only queries) + var stopX = x.Stop ?? DateTime.MinValue; + var stopY = y.Stop ?? DateTime.MinValue; + return stopX.CompareTo(stopY); + } +} + +/// Sort by close reason string. Mirrors Go SortByReason. +public sealed class SortByReason : IComparer +{ + public static readonly SortByReason Instance = new(); + public int Compare(ConnInfo? x, ConnInfo? y) + { + if (x is null || y is null) return 0; + return string.Compare(x.Reason, y.Reason, StringComparison.Ordinal); + } +} + +/// +/// Sort by round-trip time (nanoseconds, internal field). +/// Mirrors Go SortByRTT. +/// +public sealed class SortByRtt : IComparer +{ + public static readonly SortByRtt Instance = new(); + public int Compare(ConnInfo? x, ConnInfo? y) + { + if (x is null || y is null) return 0; + return x.RttNanos.CompareTo(y.RttNanos); + } +} diff --git a/dotnet/src/ZB.MOM.NatsNet.Server/Monitor/MonitorTypes.cs b/dotnet/src/ZB.MOM.NatsNet.Server/Monitor/MonitorTypes.cs new file mode 100644 index 0000000..0135ada --- /dev/null +++ b/dotnet/src/ZB.MOM.NatsNet.Server/Monitor/MonitorTypes.cs @@ -0,0 +1,387 @@ +// Copyright 2013-2026 The NATS Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// Adapted from server/monitor.go in the NATS server Go source. + +using System.Text.Json.Serialization; + +namespace ZB.MOM.NatsNet.Server; + +// ============================================================================ +// Monitor list size defaults +// Mirrors Go const block near top of monitor.go. +// ============================================================================ + +/// +/// Default sizes for monitoring API response lists. +/// Mirrors Go constants in server/monitor.go. +/// +public static class MonitorDefaults +{ + /// Default maximum number of connection entries returned. Mirrors Go DefaultConnListSize = 1024. + public const int DefaultConnListSize = 1024; + + /// Default maximum number of subscription entries returned. Mirrors Go DefaultSubListSize = 1024. + public const int DefaultSubListSize = 1024; +} + +// ============================================================================ +// ConnState — connection state filter for Connz queries +// Mirrors Go ConnState and its iota constants in monitor.go. +// ============================================================================ + +/// +/// Filter applied to connection-list queries to select open, closed, or +/// all connections. +/// Mirrors Go ConnState in server/monitor.go. +/// +public enum ConnState +{ + /// Only return open (active) connections. Mirrors Go ConnOpen = 0. + ConnOpen = 0, + + /// Only return closed connections. Mirrors Go ConnClosed. + ConnClosed = 1, + + /// Return all connections, open or closed. Mirrors Go ConnAll. + ConnAll = 2, +} + +// ============================================================================ +// ConnzOptions — query options for the Connz endpoint +// Mirrors Go ConnzOptions struct in server/monitor.go. +// ============================================================================ + +/// +/// Options that control the output of a Connz monitoring query. +/// Mirrors Go ConnzOptions struct in server/monitor.go. +/// +public sealed class ConnzOptions +{ + /// + /// How to sort results. Only ByCid is ascending; all others are + /// descending. Mirrors Go Sort SortOpt. + /// + [JsonPropertyName("sort")] + public SortOpt Sort { get; set; } = SortOpt.ByCid; + + /// When true, usernames are included in results. Mirrors Go Username bool. + [JsonPropertyName("auth")] + public bool Username { get; set; } + + /// When true, subscription subjects are listed. Mirrors Go Subscriptions bool. + [JsonPropertyName("subscriptions")] + public bool Subscriptions { get; set; } + + /// When true, verbose subscription detail is included. Mirrors Go SubscriptionsDetail bool. + [JsonPropertyName("subscriptions_detail")] + public bool SubscriptionsDetail { get; set; } + + /// Zero-based offset for pagination. Mirrors Go Offset int. + [JsonPropertyName("offset")] + public int Offset { get; set; } + + /// Maximum number of connections to return. Mirrors Go Limit int. + [JsonPropertyName("limit")] + public int Limit { get; set; } + + /// Filter for a specific client connection by CID. Mirrors Go CID uint64. + [JsonPropertyName("cid")] + public ulong Cid { get; set; } + + /// Filter for a specific MQTT client ID. Mirrors Go MQTTClient string. + [JsonPropertyName("mqtt_client")] + public string MqttClient { get; set; } = string.Empty; + + /// Connection state filter. Mirrors Go State ConnState. + [JsonPropertyName("state")] + public ConnState State { get; set; } = ConnState.ConnOpen; + + /// Filter by username. Mirrors Go User string. + [JsonPropertyName("user")] + public string User { get; set; } = string.Empty; + + /// Filter by account name. Mirrors Go Account string. + [JsonPropertyName("acc")] + public string Account { get; set; } = string.Empty; + + /// Filter by subject interest (requires Account filter). Mirrors Go FilterSubject string. + [JsonPropertyName("filter_subject")] + public string FilterSubject { get; set; } = string.Empty; +} + +// ============================================================================ +// Connz — top-level connection list monitoring response +// Mirrors Go Connz struct in server/monitor.go. +// ============================================================================ + +/// +/// Top-level response type for the /connz monitoring endpoint. +/// Contains the current connection list and pagination metadata. +/// Mirrors Go Connz struct in server/monitor.go. +/// +public sealed class Connz +{ + [JsonPropertyName("server_id")] + public string Id { get; set; } = string.Empty; + + [JsonPropertyName("now")] + public DateTime Now { get; set; } + + [JsonPropertyName("num_connections")] + public int NumConns { get; set; } + + [JsonPropertyName("total")] + public int Total { get; set; } + + [JsonPropertyName("offset")] + public int Offset { get; set; } + + [JsonPropertyName("limit")] + public int Limit { get; set; } + + [JsonPropertyName("connections")] + public List Conns { get; set; } = []; +} + +// ============================================================================ +// ConnInfo — per-connection detail record +// Mirrors Go ConnInfo struct in server/monitor.go. +// ============================================================================ + +/// +/// Detailed information about a single client connection, as returned by the +/// /connz monitoring endpoint. +/// Mirrors Go ConnInfo struct in server/monitor.go. +/// +public sealed class ConnInfo +{ + [JsonPropertyName("cid")] + public ulong Cid { get; set; } + + [JsonPropertyName("kind")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Kind { get; set; } + + [JsonPropertyName("type")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Type { get; set; } + + [JsonPropertyName("ip")] + public string Ip { get; set; } = string.Empty; + + [JsonPropertyName("port")] + public int Port { get; set; } + + [JsonPropertyName("start")] + public DateTime Start { get; set; } + + [JsonPropertyName("last_activity")] + public DateTime LastActivity { get; set; } + + [JsonPropertyName("stop")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public DateTime? Stop { get; set; } + + [JsonPropertyName("reason")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Reason { get; set; } + + [JsonPropertyName("rtt")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Rtt { get; set; } + + [JsonPropertyName("uptime")] + public string Uptime { get; set; } = string.Empty; + + [JsonPropertyName("idle")] + public string Idle { get; set; } = string.Empty; + + [JsonPropertyName("pending_bytes")] + public int Pending { get; set; } + + [JsonPropertyName("in_msgs")] + public long InMsgs { get; set; } + + [JsonPropertyName("out_msgs")] + public long OutMsgs { get; set; } + + [JsonPropertyName("in_bytes")] + public long InBytes { get; set; } + + [JsonPropertyName("out_bytes")] + public long OutBytes { get; set; } + + [JsonPropertyName("stalls")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public long Stalls { get; set; } + + [JsonPropertyName("subscriptions")] + public uint NumSubs { get; set; } + + [JsonPropertyName("name")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Name { get; set; } + + [JsonPropertyName("lang")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Lang { get; set; } + + [JsonPropertyName("version")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Version { get; set; } + + [JsonPropertyName("tls_version")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? TlsVersion { get; set; } + + [JsonPropertyName("tls_cipher_suite")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? TlsCipher { get; set; } + + [JsonPropertyName("tls_peer_certs")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public List? TlsPeerCerts { get; set; } + + [JsonPropertyName("tls_first")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public bool TlsFirst { get; set; } + + [JsonPropertyName("authorized_user")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? AuthorizedUser { get; set; } + + [JsonPropertyName("account")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Account { get; set; } + + [JsonPropertyName("subscriptions_list")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public List? Subs { get; set; } + + [JsonPropertyName("subscriptions_list_detail")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public List? SubsDetail { get; set; } + + [JsonPropertyName("jwt")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Jwt { get; set; } + + [JsonPropertyName("issuer_key")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? IssuerKey { get; set; } + + [JsonPropertyName("name_tag")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? NameTag { get; set; } + + [JsonPropertyName("tags")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string[]? Tags { get; set; } + + [JsonPropertyName("mqtt_client")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? MqttClient { get; set; } + + [JsonPropertyName("proxy")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public ProxyInfo? Proxy { get; set; } + + /// + /// Internal field used for fast RTT-based sorting. + /// Mirrors Go rtt int64 unexported field in ConnInfo. + /// Not serialised. + /// + [JsonIgnore] + internal long RttNanos { get; set; } +} + +// ============================================================================ +// ProxyInfo — proxy connection metadata +// Mirrors Go ProxyInfo struct in server/monitor.go. +// ============================================================================ + +/// +/// Information about a proxied connection (e.g. HAProxy PROXY protocol). +/// Mirrors Go ProxyInfo struct in server/monitor.go. +/// +public sealed class ProxyInfo +{ + [JsonPropertyName("key")] + public string Key { get; set; } = string.Empty; +} + +// ============================================================================ +// TlsPeerCert — TLS peer certificate summary +// Mirrors Go TLSPeerCert struct in server/monitor.go. +// ============================================================================ + +/// +/// Basic information about a TLS peer certificate. +/// Mirrors Go TLSPeerCert struct in server/monitor.go. +/// +public sealed class TlsPeerCert +{ + [JsonPropertyName("subject")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Subject { get; set; } + + [JsonPropertyName("spki_sha256")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? SubjectPkiSha256 { get; set; } + + [JsonPropertyName("cert_sha256")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? CertSha256 { get; set; } +} + +// ============================================================================ +// SubDetail — verbose subscription information +// Mirrors Go SubDetail struct in server/monitor.go (line ~961). +// ============================================================================ + +/// +/// Verbose information about a single subscription, included in detailed +/// connection or account monitoring responses. +/// Mirrors Go SubDetail struct in server/monitor.go. +/// +public sealed class SubDetail +{ + [JsonPropertyName("account")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Account { get; set; } + + [JsonPropertyName("account_tag")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? AccountTag { get; set; } + + [JsonPropertyName("subject")] + public string Subject { get; set; } = string.Empty; + + [JsonPropertyName("qgroup")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Queue { get; set; } + + [JsonPropertyName("sid")] + public string Sid { get; set; } = string.Empty; + + [JsonPropertyName("msgs")] + public long Msgs { get; set; } + + [JsonPropertyName("max")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public long Max { get; set; } + + [JsonPropertyName("cid")] + public ulong Cid { get; set; } +} diff --git a/dotnet/src/ZB.MOM.NatsNet.Server/NatsServer.cs b/dotnet/src/ZB.MOM.NatsNet.Server/NatsServer.cs index bcef42a..73b5dfe 100644 --- a/dotnet/src/ZB.MOM.NatsNet.Server/NatsServer.cs +++ b/dotnet/src/ZB.MOM.NatsNet.Server/NatsServer.cs @@ -57,7 +57,7 @@ public sealed partial class NatsServer : INatsServer private readonly ServerStats _stats = new(); private readonly SlowConsumerStats _scStats = new(); - private readonly StaleConnectionStats _staleStats = new(); + private readonly InternalStaleStats _staleStats = new(); // ========================================================================= // Core identity diff --git a/dotnet/src/ZB.MOM.NatsNet.Server/NatsServerTypes.cs b/dotnet/src/ZB.MOM.NatsNet.Server/NatsServerTypes.cs index 5f73d89..29daa35 100644 --- a/dotnet/src/ZB.MOM.NatsNet.Server/NatsServerTypes.cs +++ b/dotnet/src/ZB.MOM.NatsNet.Server/NatsServerTypes.cs @@ -122,10 +122,12 @@ internal sealed class SlowConsumerStats } /// -/// Per-kind stale-connection counters (atomic). +/// Per-kind stale-connection counters (atomic, internal use only). /// Mirrors Go embedded staleStats in server.go. +/// NOTE: The public JSON-serialisable monitoring equivalent is StaleConnectionStats +/// in Events/EventTypes.cs. /// -internal sealed class StaleConnectionStats +internal sealed class InternalStaleStats { public long Clients; public long Routes; @@ -204,12 +206,7 @@ public static class CompressionMode // These stubs will be replaced with full implementations in later sessions. // They are declared here to allow the NatsServer class to compile. -/// Stub for the system/internal messaging state (session 12). -internal sealed class InternalState -{ - public Account? Account { get; set; } - // Full implementation in session 12 (events.go) -} +// InternalState is now fully defined in Events/EventTypes.cs (session 12). /// Stub for JetStream state pointer (session 19). internal sealed class JetStreamState { } diff --git a/porting.db b/porting.db index 685e0db9dfd825148ddd6eba7b2e31948fc6ebed..22ef20afedbd71fe8de49b2a846b20f5fe23a68b 100644 GIT binary patch delta 21199 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 delta 15203 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 diff --git a/reports/current.md b/reports/current.md index b456a35..3fd230f 100644 --- a/reports/current.md +++ b/reports/current.md @@ -1,6 +1,6 @@ # NATS .NET Porting Status Report -Generated: 2026-02-26 20:37:09 UTC +Generated: 2026-02-26 20:46:14 UTC ## Modules (12 total) @@ -13,9 +13,9 @@ Generated: 2026-02-26 20:37:09 UTC | Status | Count | |--------|-------| -| complete | 1075 | +| complete | 1382 | | n_a | 82 | -| not_started | 2423 | +| not_started | 2116 | | stub | 93 | ## Unit Tests (3257 total) @@ -36,4 +36,4 @@ Generated: 2026-02-26 20:37:09 UTC ## Overall Progress -**1668/6942 items complete (24.0%)** +**1975/6942 items complete (28.5%)** diff --git a/reports/report_12a14ec.md b/reports/report_12a14ec.md new file mode 100644 index 0000000..3fd230f --- /dev/null +++ b/reports/report_12a14ec.md @@ -0,0 +1,39 @@ +# NATS .NET Porting Status Report + +Generated: 2026-02-26 20:46:14 UTC + +## Modules (12 total) + +| Status | Count | +|--------|-------| +| complete | 11 | +| not_started | 1 | + +## Features (3673 total) + +| Status | Count | +|--------|-------| +| complete | 1382 | +| n_a | 82 | +| not_started | 2116 | +| stub | 93 | + +## Unit Tests (3257 total) + +| Status | Count | +|--------|-------| +| complete | 319 | +| n_a | 181 | +| not_started | 2533 | +| stub | 224 | + +## Library Mappings (36 total) + +| Status | Count | +|--------|-------| +| mapped | 36 | + + +## Overall Progress + +**1975/6942 items complete (28.5%)**