Merge branch 'feature/sections-7-10-gaps' into main
This commit is contained in:
@@ -34,6 +34,13 @@ public sealed class AuthService
|
||||
var nonceRequired = false;
|
||||
Dictionary<string, User>? usersMap = null;
|
||||
|
||||
// TLS certificate mapping (highest priority when enabled)
|
||||
if (options.TlsMap && options.TlsVerify && options.Users is { Count: > 0 })
|
||||
{
|
||||
authenticators.Add(new TlsMapAuthenticator(options.Users));
|
||||
authRequired = true;
|
||||
}
|
||||
|
||||
// Priority order (matching Go): NKeys > Users > Token > SimpleUserPassword
|
||||
|
||||
if (options.NKeys is { Count: > 0 })
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using NATS.Server.Protocol;
|
||||
|
||||
namespace NATS.Server.Auth;
|
||||
@@ -11,4 +12,5 @@ public sealed class ClientAuthContext
|
||||
{
|
||||
public required ClientOptions Opts { get; init; }
|
||||
public required byte[] Nonce { get; init; }
|
||||
public X509Certificate2? ClientCertificate { get; init; }
|
||||
}
|
||||
|
||||
67
src/NATS.Server/Auth/TlsMapAuthenticator.cs
Normal file
67
src/NATS.Server/Auth/TlsMapAuthenticator.cs
Normal file
@@ -0,0 +1,67 @@
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
|
||||
namespace NATS.Server.Auth;
|
||||
|
||||
/// <summary>
|
||||
/// Authenticates clients by mapping TLS certificate subject DN to configured users.
|
||||
/// Corresponds to Go server/auth.go checkClientTLSCertSubject.
|
||||
/// </summary>
|
||||
public sealed class TlsMapAuthenticator : IAuthenticator
|
||||
{
|
||||
private readonly Dictionary<string, User> _usersByDn;
|
||||
private readonly Dictionary<string, User> _usersByCn;
|
||||
|
||||
public TlsMapAuthenticator(IReadOnlyList<User> users)
|
||||
{
|
||||
_usersByDn = new Dictionary<string, User>(StringComparer.OrdinalIgnoreCase);
|
||||
_usersByCn = new Dictionary<string, User>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var user in users)
|
||||
{
|
||||
_usersByDn[user.Username] = user;
|
||||
_usersByCn[user.Username] = user;
|
||||
}
|
||||
}
|
||||
|
||||
public AuthResult? Authenticate(ClientAuthContext context)
|
||||
{
|
||||
var cert = context.ClientCertificate;
|
||||
if (cert == null)
|
||||
return null;
|
||||
|
||||
var dn = cert.SubjectName;
|
||||
var dnString = dn.Name; // RFC 2253 format
|
||||
|
||||
// Try exact DN match first
|
||||
if (_usersByDn.TryGetValue(dnString, out var user))
|
||||
return BuildResult(user);
|
||||
|
||||
// Try CN extraction
|
||||
var cn = ExtractCn(dn);
|
||||
if (cn != null && _usersByCn.TryGetValue(cn, out user))
|
||||
return BuildResult(user);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static string? ExtractCn(X500DistinguishedName dn)
|
||||
{
|
||||
var dnString = dn.Name;
|
||||
foreach (var rdn in dnString.Split(',', StringSplitOptions.TrimEntries))
|
||||
{
|
||||
if (rdn.StartsWith("CN=", StringComparison.OrdinalIgnoreCase))
|
||||
return rdn[3..];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static AuthResult BuildResult(User user)
|
||||
{
|
||||
return new AuthResult
|
||||
{
|
||||
Identity = user.Username,
|
||||
AccountName = user.Account,
|
||||
Permissions = user.Permissions,
|
||||
Expiry = user.ConnectionDeadline,
|
||||
};
|
||||
}
|
||||
}
|
||||
25
src/NATS.Server/Monitoring/ClosedClient.cs
Normal file
25
src/NATS.Server/Monitoring/ClosedClient.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
namespace NATS.Server.Monitoring;
|
||||
|
||||
/// <summary>
|
||||
/// Snapshot of a closed client connection for /connz reporting.
|
||||
/// </summary>
|
||||
public sealed record ClosedClient
|
||||
{
|
||||
public required ulong Cid { get; init; }
|
||||
public string Ip { get; init; } = "";
|
||||
public int Port { get; init; }
|
||||
public DateTime Start { get; init; }
|
||||
public DateTime Stop { get; init; }
|
||||
public string Reason { get; init; } = "";
|
||||
public string Name { get; init; } = "";
|
||||
public string Lang { get; init; } = "";
|
||||
public string Version { get; init; } = "";
|
||||
public long InMsgs { get; init; }
|
||||
public long OutMsgs { get; init; }
|
||||
public long InBytes { get; init; }
|
||||
public long OutBytes { get; init; }
|
||||
public uint NumSubs { get; init; }
|
||||
public TimeSpan Rtt { get; init; }
|
||||
public string TlsVersion { get; init; } = "";
|
||||
public string TlsCipherSuite { get; init; } = "";
|
||||
}
|
||||
@@ -168,6 +168,9 @@ public enum SortOpt
|
||||
ByLast,
|
||||
ByIdle,
|
||||
ByUptime,
|
||||
ByRtt,
|
||||
ByStop,
|
||||
ByReason,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -12,9 +12,25 @@ public sealed class ConnzHandler(NatsServer server)
|
||||
{
|
||||
var opts = ParseQueryParams(ctx);
|
||||
var now = DateTime.UtcNow;
|
||||
var clients = server.GetClients().ToArray();
|
||||
|
||||
var connInfos = clients.Select(c => BuildConnInfo(c, now, opts)).ToList();
|
||||
var connInfos = new List<ConnInfo>();
|
||||
|
||||
// Collect open connections
|
||||
if (opts.State is ConnState.Open or ConnState.All)
|
||||
{
|
||||
var clients = server.GetClients().ToArray();
|
||||
connInfos.AddRange(clients.Select(c => BuildConnInfo(c, now, opts)));
|
||||
}
|
||||
|
||||
// Collect closed connections
|
||||
if (opts.State is ConnState.Closed or ConnState.All)
|
||||
{
|
||||
connInfos.AddRange(server.GetClosedClients().Select(c => BuildClosedConnInfo(c, now, opts)));
|
||||
}
|
||||
|
||||
// Validate sort options that require closed state
|
||||
if (opts.Sort is SortOpt.ByStop or SortOpt.ByReason && opts.State == ConnState.Open)
|
||||
opts.Sort = SortOpt.ByCid; // Fallback
|
||||
|
||||
// Sort
|
||||
connInfos = opts.Sort switch
|
||||
@@ -30,6 +46,9 @@ public sealed class ConnzHandler(NatsServer server)
|
||||
SortOpt.ByLast => connInfos.OrderByDescending(c => c.LastActivity).ToList(),
|
||||
SortOpt.ByIdle => connInfos.OrderByDescending(c => now - c.LastActivity).ToList(),
|
||||
SortOpt.ByUptime => connInfos.OrderByDescending(c => now - c.Start).ToList(),
|
||||
SortOpt.ByStop => connInfos.OrderByDescending(c => c.Stop ?? DateTime.MinValue).ToList(),
|
||||
SortOpt.ByReason => connInfos.OrderBy(c => c.Reason).ToList(),
|
||||
SortOpt.ByRtt => connInfos.OrderBy(c => c.Rtt).ToList(),
|
||||
_ => connInfos.OrderBy(c => c.Cid).ToList(),
|
||||
};
|
||||
|
||||
@@ -73,6 +92,7 @@ public sealed class ConnzHandler(NatsServer server)
|
||||
Reason = client.CloseReason.ToReasonString(),
|
||||
TlsVersion = client.TlsState?.TlsVersion ?? "",
|
||||
TlsCipherSuite = client.TlsState?.CipherSuite ?? "",
|
||||
Rtt = FormatRtt(client.Rtt),
|
||||
};
|
||||
|
||||
if (opts.Subscriptions)
|
||||
@@ -96,6 +116,35 @@ public sealed class ConnzHandler(NatsServer server)
|
||||
return info;
|
||||
}
|
||||
|
||||
private static ConnInfo BuildClosedConnInfo(ClosedClient closed, DateTime now, ConnzOptions opts)
|
||||
{
|
||||
return new ConnInfo
|
||||
{
|
||||
Cid = closed.Cid,
|
||||
Kind = "Client",
|
||||
Type = "Client",
|
||||
Ip = closed.Ip,
|
||||
Port = closed.Port,
|
||||
Start = closed.Start,
|
||||
Stop = closed.Stop,
|
||||
LastActivity = closed.Stop,
|
||||
Uptime = FormatDuration(closed.Stop - closed.Start),
|
||||
Idle = FormatDuration(now - closed.Stop),
|
||||
InMsgs = closed.InMsgs,
|
||||
OutMsgs = closed.OutMsgs,
|
||||
InBytes = closed.InBytes,
|
||||
OutBytes = closed.OutBytes,
|
||||
NumSubs = closed.NumSubs,
|
||||
Name = closed.Name,
|
||||
Lang = closed.Lang,
|
||||
Version = closed.Version,
|
||||
Reason = closed.Reason,
|
||||
Rtt = FormatRtt(closed.Rtt),
|
||||
TlsVersion = closed.TlsVersion,
|
||||
TlsCipherSuite = closed.TlsCipherSuite,
|
||||
};
|
||||
}
|
||||
|
||||
private static ConnzOptions ParseQueryParams(HttpContext ctx)
|
||||
{
|
||||
var q = ctx.Request.Query;
|
||||
@@ -116,6 +165,9 @@ public sealed class ConnzHandler(NatsServer server)
|
||||
"last" => SortOpt.ByLast,
|
||||
"idle" => SortOpt.ByIdle,
|
||||
"uptime" => SortOpt.ByUptime,
|
||||
"rtt" => SortOpt.ByRtt,
|
||||
"stop" => SortOpt.ByStop,
|
||||
"reason" => SortOpt.ByReason,
|
||||
_ => SortOpt.ByCid,
|
||||
};
|
||||
}
|
||||
@@ -128,6 +180,17 @@ public sealed class ConnzHandler(NatsServer server)
|
||||
opts.Subscriptions = true;
|
||||
}
|
||||
|
||||
if (q.TryGetValue("state", out var state))
|
||||
{
|
||||
opts.State = state.ToString().ToLowerInvariant() switch
|
||||
{
|
||||
"open" => ConnState.Open,
|
||||
"closed" => ConnState.Closed,
|
||||
"all" => ConnState.All,
|
||||
_ => ConnState.Open,
|
||||
};
|
||||
}
|
||||
|
||||
if (q.TryGetValue("offset", out var offset) && int.TryParse(offset, out var o))
|
||||
opts.Offset = o;
|
||||
|
||||
@@ -137,6 +200,16 @@ public sealed class ConnzHandler(NatsServer server)
|
||||
return opts;
|
||||
}
|
||||
|
||||
private static string FormatRtt(TimeSpan rtt)
|
||||
{
|
||||
if (rtt == TimeSpan.Zero) return "";
|
||||
if (rtt.TotalMilliseconds < 1)
|
||||
return $"{rtt.TotalMicroseconds:F3}\u00b5s";
|
||||
if (rtt.TotalSeconds < 1)
|
||||
return $"{rtt.TotalMilliseconds:F3}ms";
|
||||
return $"{rtt.TotalSeconds:F3}s";
|
||||
}
|
||||
|
||||
private static string FormatDuration(TimeSpan ts)
|
||||
{
|
||||
if (ts.TotalDays >= 1)
|
||||
|
||||
@@ -15,6 +15,7 @@ public sealed class MonitorServer : IAsyncDisposable
|
||||
private readonly ILogger<MonitorServer> _logger;
|
||||
private readonly VarzHandler _varzHandler;
|
||||
private readonly ConnzHandler _connzHandler;
|
||||
private readonly SubszHandler _subszHandler;
|
||||
|
||||
public MonitorServer(NatsServer server, NatsOptions options, ServerStats stats, ILoggerFactory loggerFactory)
|
||||
{
|
||||
@@ -29,6 +30,7 @@ public sealed class MonitorServer : IAsyncDisposable
|
||||
|
||||
_varzHandler = new VarzHandler(server, options);
|
||||
_connzHandler = new ConnzHandler(server);
|
||||
_subszHandler = new SubszHandler(server);
|
||||
|
||||
_app.MapGet(basePath + "/", () =>
|
||||
{
|
||||
@@ -75,15 +77,15 @@ public sealed class MonitorServer : IAsyncDisposable
|
||||
stats.HttpReqStats.AddOrUpdate("/leafz", 1, (_, v) => v + 1);
|
||||
return Results.Ok(new { });
|
||||
});
|
||||
_app.MapGet(basePath + "/subz", () =>
|
||||
_app.MapGet(basePath + "/subz", (HttpContext ctx) =>
|
||||
{
|
||||
stats.HttpReqStats.AddOrUpdate("/subz", 1, (_, v) => v + 1);
|
||||
return Results.Ok(new { });
|
||||
return Results.Ok(_subszHandler.HandleSubsz(ctx));
|
||||
});
|
||||
_app.MapGet(basePath + "/subscriptionsz", () =>
|
||||
_app.MapGet(basePath + "/subscriptionsz", (HttpContext ctx) =>
|
||||
{
|
||||
stats.HttpReqStats.AddOrUpdate("/subscriptionsz", 1, (_, v) => v + 1);
|
||||
return Results.Ok(new { });
|
||||
return Results.Ok(_subszHandler.HandleSubsz(ctx));
|
||||
});
|
||||
_app.MapGet(basePath + "/accountz", () =>
|
||||
{
|
||||
|
||||
45
src/NATS.Server/Monitoring/Subsz.cs
Normal file
45
src/NATS.Server/Monitoring/Subsz.cs
Normal file
@@ -0,0 +1,45 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace NATS.Server.Monitoring;
|
||||
|
||||
/// <summary>
|
||||
/// Subscription information response. Corresponds to Go server/monitor.go Subsz struct.
|
||||
/// </summary>
|
||||
public sealed class Subsz
|
||||
{
|
||||
[JsonPropertyName("server_id")]
|
||||
public string Id { get; set; } = "";
|
||||
|
||||
[JsonPropertyName("now")]
|
||||
public DateTime Now { get; set; }
|
||||
|
||||
[JsonPropertyName("num_subscriptions")]
|
||||
public uint NumSubs { get; set; }
|
||||
|
||||
[JsonPropertyName("num_cache")]
|
||||
public int NumCache { get; set; }
|
||||
|
||||
[JsonPropertyName("total")]
|
||||
public int Total { get; set; }
|
||||
|
||||
[JsonPropertyName("offset")]
|
||||
public int Offset { get; set; }
|
||||
|
||||
[JsonPropertyName("limit")]
|
||||
public int Limit { get; set; }
|
||||
|
||||
[JsonPropertyName("subscriptions")]
|
||||
public SubDetail[] Subs { get; set; } = [];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Options passed to Subsz() for filtering.
|
||||
/// </summary>
|
||||
public sealed class SubszOptions
|
||||
{
|
||||
public int Offset { get; set; }
|
||||
public int Limit { get; set; } = 1024;
|
||||
public bool Subscriptions { get; set; }
|
||||
public string Account { get; set; } = "";
|
||||
public string Test { get; set; } = "";
|
||||
}
|
||||
93
src/NATS.Server/Monitoring/SubszHandler.cs
Normal file
93
src/NATS.Server/Monitoring/SubszHandler.cs
Normal file
@@ -0,0 +1,93 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using NATS.Server.Subscriptions;
|
||||
|
||||
namespace NATS.Server.Monitoring;
|
||||
|
||||
/// <summary>
|
||||
/// Handles /subz endpoint requests, returning subscription information.
|
||||
/// Corresponds to Go server/monitor.go handleSubsz.
|
||||
/// </summary>
|
||||
public sealed class SubszHandler(NatsServer server)
|
||||
{
|
||||
public Subsz HandleSubsz(HttpContext ctx)
|
||||
{
|
||||
var opts = ParseQueryParams(ctx);
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
// Collect subscriptions from all accounts (or filtered)
|
||||
var allSubs = new List<Subscription>();
|
||||
foreach (var account in server.GetAccounts())
|
||||
{
|
||||
if (!string.IsNullOrEmpty(opts.Account) && account.Name != opts.Account)
|
||||
continue;
|
||||
allSubs.AddRange(account.SubList.GetAllSubscriptions());
|
||||
}
|
||||
|
||||
// Filter by test subject if provided
|
||||
if (!string.IsNullOrEmpty(opts.Test))
|
||||
{
|
||||
allSubs = allSubs.Where(s => SubjectMatch.MatchLiteral(opts.Test, s.Subject)).ToList();
|
||||
}
|
||||
|
||||
var total = allSubs.Count;
|
||||
var numSubs = server.GetAccounts()
|
||||
.Where(a => string.IsNullOrEmpty(opts.Account) || a.Name == opts.Account)
|
||||
.Aggregate(0u, (sum, a) => sum + a.SubList.Count);
|
||||
var numCache = server.GetAccounts()
|
||||
.Where(a => string.IsNullOrEmpty(opts.Account) || a.Name == opts.Account)
|
||||
.Sum(a => a.SubList.CacheCount);
|
||||
|
||||
SubDetail[] details = [];
|
||||
if (opts.Subscriptions)
|
||||
{
|
||||
details = allSubs
|
||||
.Skip(opts.Offset)
|
||||
.Take(opts.Limit)
|
||||
.Select(s => new SubDetail
|
||||
{
|
||||
Subject = s.Subject,
|
||||
Queue = s.Queue ?? "",
|
||||
Sid = s.Sid,
|
||||
Msgs = Interlocked.Read(ref s.MessageCount),
|
||||
Max = s.MaxMessages,
|
||||
Cid = s.Client?.Id ?? 0,
|
||||
})
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
return new Subsz
|
||||
{
|
||||
Id = server.ServerId,
|
||||
Now = now,
|
||||
NumSubs = numSubs,
|
||||
NumCache = numCache,
|
||||
Total = total,
|
||||
Offset = opts.Offset,
|
||||
Limit = opts.Limit,
|
||||
Subs = details,
|
||||
};
|
||||
}
|
||||
|
||||
private static SubszOptions ParseQueryParams(HttpContext ctx)
|
||||
{
|
||||
var q = ctx.Request.Query;
|
||||
var opts = new SubszOptions();
|
||||
|
||||
if (q.TryGetValue("subs", out var subs))
|
||||
opts.Subscriptions = subs == "true" || subs == "1" || subs == "detail";
|
||||
|
||||
if (q.TryGetValue("offset", out var offset) && int.TryParse(offset, out var o))
|
||||
opts.Offset = o;
|
||||
|
||||
if (q.TryGetValue("limit", out var limit) && int.TryParse(limit, out var l))
|
||||
opts.Limit = l;
|
||||
|
||||
if (q.TryGetValue("acc", out var acc))
|
||||
opts.Account = acc.ToString();
|
||||
|
||||
if (q.TryGetValue("test", out var test))
|
||||
opts.Test = test.ToString();
|
||||
|
||||
return opts;
|
||||
}
|
||||
}
|
||||
@@ -157,6 +157,12 @@ public sealed class Varz
|
||||
[JsonPropertyName("slow_consumer_stats")]
|
||||
public SlowConsumersStats SlowConsumerStats { get; set; } = new();
|
||||
|
||||
[JsonPropertyName("stale_connections")]
|
||||
public long StaleConnections { get; set; }
|
||||
|
||||
[JsonPropertyName("stale_connection_stats")]
|
||||
public StaleConnectionStats StaleConnectionStatsDetail { get; set; } = new();
|
||||
|
||||
[JsonPropertyName("subscriptions")]
|
||||
public uint Subscriptions { get; set; }
|
||||
|
||||
@@ -219,6 +225,25 @@ public sealed class SlowConsumersStats
|
||||
public ulong Leafs { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Statistics about stale connections by connection type.
|
||||
/// Corresponds to Go server/monitor.go StaleConnectionStats struct.
|
||||
/// </summary>
|
||||
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; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cluster configuration monitoring information.
|
||||
/// Corresponds to Go server/monitor.go ClusterOptsVarz struct.
|
||||
|
||||
@@ -91,6 +91,14 @@ public sealed class VarzHandler : IDisposable
|
||||
Gateways = (ulong)Interlocked.Read(ref stats.SlowConsumerGateways),
|
||||
Leafs = (ulong)Interlocked.Read(ref stats.SlowConsumerLeafs),
|
||||
},
|
||||
StaleConnections = Interlocked.Read(ref stats.StaleConnections),
|
||||
StaleConnectionStatsDetail = new StaleConnectionStats
|
||||
{
|
||||
Clients = (ulong)Interlocked.Read(ref stats.StaleConnectionClients),
|
||||
Routes = (ulong)Interlocked.Read(ref stats.StaleConnectionRoutes),
|
||||
Gateways = (ulong)Interlocked.Read(ref stats.StaleConnectionGateways),
|
||||
Leafs = (ulong)Interlocked.Read(ref stats.StaleConnectionLeafs),
|
||||
},
|
||||
Subscriptions = _server.SubList.Count,
|
||||
ConfigLoadTime = _server.StartTime,
|
||||
HttpReqStats = stats.HttpReqStats.ToDictionary(kv => kv.Key, kv => (ulong)kv.Value),
|
||||
|
||||
@@ -74,6 +74,11 @@ public sealed class NatsClient : IDisposable
|
||||
private int _pingsOut;
|
||||
private long _lastIn;
|
||||
|
||||
// RTT tracking
|
||||
private long _rttStartTicks;
|
||||
private long _rtt;
|
||||
public TimeSpan Rtt => new(Interlocked.Read(ref _rtt));
|
||||
|
||||
public TlsConnectionState? TlsState { get; set; }
|
||||
public bool InfoAlreadySent { get; set; }
|
||||
|
||||
@@ -322,6 +327,14 @@ public sealed class NatsClient : IDisposable
|
||||
|
||||
case CommandType.Pong:
|
||||
Interlocked.Exchange(ref _pingsOut, 0);
|
||||
var rttStart = Interlocked.Read(ref _rttStartTicks);
|
||||
if (rttStart > 0)
|
||||
{
|
||||
var elapsed = DateTime.UtcNow.Ticks - rttStart;
|
||||
if (elapsed <= 0) elapsed = 1; // min 1 tick for Windows granularity
|
||||
Interlocked.Exchange(ref _rtt, elapsed);
|
||||
}
|
||||
_flags.SetFlag(ClientFlags.FirstPongSent);
|
||||
break;
|
||||
|
||||
case CommandType.Sub:
|
||||
@@ -356,6 +369,7 @@ public sealed class NatsClient : IDisposable
|
||||
{
|
||||
Opts = ClientOpts,
|
||||
Nonce = _nonce ?? [],
|
||||
ClientCertificate = TlsState?.PeerCert,
|
||||
};
|
||||
|
||||
authResult = _authService.Authenticate(context);
|
||||
@@ -733,6 +747,13 @@ public sealed class NatsClient : IDisposable
|
||||
{
|
||||
while (await timer.WaitForNextTickAsync(ct))
|
||||
{
|
||||
// Delay first PING until client has responded with PONG or 2 seconds elapsed
|
||||
if (!_flags.HasFlag(ClientFlags.FirstPongSent)
|
||||
&& (DateTime.UtcNow - StartTime).TotalSeconds < 2)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var elapsed = Environment.TickCount64 - Interlocked.Read(ref _lastIn);
|
||||
if (elapsed < (long)_options.PingInterval.TotalMilliseconds)
|
||||
{
|
||||
@@ -744,6 +765,8 @@ public sealed class NatsClient : IDisposable
|
||||
if (Volatile.Read(ref _pingsOut) + 1 > _options.MaxPingsOut)
|
||||
{
|
||||
_logger.LogDebug("Client {ClientId} stale connection -- closing", Id);
|
||||
Interlocked.Increment(ref _serverStats.StaleConnections);
|
||||
Interlocked.Increment(ref _serverStats.StaleConnectionClients);
|
||||
await SendErrAndCloseAsync(NatsProtocol.ErrStaleConnection, ClientClosedReason.StaleConnection);
|
||||
return;
|
||||
}
|
||||
@@ -751,6 +774,7 @@ public sealed class NatsClient : IDisposable
|
||||
var currentPingsOut = Interlocked.Increment(ref _pingsOut);
|
||||
_logger.LogDebug("Client {ClientId} sending PING ({PingsOut}/{MaxPingsOut})",
|
||||
Id, currentPingsOut, _options.MaxPingsOut);
|
||||
Interlocked.Exchange(ref _rttStartTicks, DateTime.UtcNow.Ticks);
|
||||
WriteProtocol(NatsProtocol.PingBytes);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,12 +20,6 @@ public sealed class NatsOptions
|
||||
public int MaxSubs { get; set; } // 0 = unlimited (per-connection)
|
||||
public int MaxSubTokens { get; set; } // 0 = unlimited
|
||||
|
||||
// Logging / diagnostics
|
||||
public bool Debug { get; set; }
|
||||
public bool Trace { get; set; }
|
||||
public string? LogFile { get; set; }
|
||||
public long LogSizeLimit { get; set; }
|
||||
|
||||
// Server tags (exposed via /varz)
|
||||
public Dictionary<string, string>? Tags { get; set; }
|
||||
|
||||
@@ -63,6 +57,17 @@ public sealed class NatsOptions
|
||||
public string? PortsFileDir { get; set; }
|
||||
public string? ConfigFile { get; set; }
|
||||
|
||||
// Logging
|
||||
public string? LogFile { get; set; }
|
||||
public long LogSizeLimit { get; set; }
|
||||
public int LogMaxFiles { get; set; }
|
||||
public bool Debug { get; set; }
|
||||
public bool Trace { get; set; }
|
||||
public bool Logtime { get; set; } = true;
|
||||
public bool LogtimeUTC { get; set; }
|
||||
public bool Syslog { get; set; }
|
||||
public string? RemoteSyslog { get; set; }
|
||||
|
||||
// Profiling (0 = disabled)
|
||||
public int ProfPort { get; set; }
|
||||
|
||||
|
||||
@@ -19,6 +19,8 @@ public sealed class NatsServer : IMessageRouter, ISubListAccess, IDisposable
|
||||
{
|
||||
private readonly NatsOptions _options;
|
||||
private readonly ConcurrentDictionary<ulong, NatsClient> _clients = new();
|
||||
private readonly ConcurrentQueue<ClosedClient> _closedClients = new();
|
||||
private const int MaxClosedClients = 10_000;
|
||||
private readonly ServerInfo _serverInfo;
|
||||
private readonly ILogger<NatsServer> _logger;
|
||||
private readonly ILoggerFactory _loggerFactory;
|
||||
@@ -64,8 +66,13 @@ public sealed class NatsServer : IMessageRouter, ISubListAccess, IDisposable
|
||||
public string ServerNKey { get; }
|
||||
public bool IsShuttingDown => Volatile.Read(ref _shutdown) != 0;
|
||||
public bool IsLameDuckMode => Volatile.Read(ref _lameDuck) != 0;
|
||||
public Action? ReOpenLogFile { get; set; }
|
||||
public IEnumerable<NatsClient> GetClients() => _clients.Values;
|
||||
|
||||
public IEnumerable<ClosedClient> GetClosedClients() => _closedClients;
|
||||
|
||||
public IEnumerable<Auth.Account> GetAccounts() => _accounts.Values;
|
||||
|
||||
public Task WaitForReadyAsync() => _listeningStarted.Task;
|
||||
|
||||
public void WaitForShutdown() => _shutdownComplete.Task.GetAwaiter().GetResult();
|
||||
@@ -195,7 +202,7 @@ public sealed class NatsServer : IMessageRouter, ISubListAccess, IDisposable
|
||||
|
||||
/// <summary>
|
||||
/// Registers Unix signal handlers.
|
||||
/// SIGTERM → shutdown, SIGUSR2 → lame duck, SIGUSR1 → log reopen (stub), SIGHUP → reload (stub).
|
||||
/// SIGTERM → shutdown, SIGUSR2 → lame duck, SIGUSR1 → log reopen, SIGHUP → reload (stub).
|
||||
/// </summary>
|
||||
public void HandleSignals()
|
||||
{
|
||||
@@ -225,7 +232,8 @@ public sealed class NatsServer : IMessageRouter, ISubListAccess, IDisposable
|
||||
_signalRegistrations.Add(PosixSignalRegistration.Create((PosixSignal)10, ctx =>
|
||||
{
|
||||
ctx.Cancel = true;
|
||||
_logger.LogWarning("Trapped SIGUSR1 signal — log reopen not yet supported");
|
||||
_logger.LogInformation("Trapped SIGUSR1 signal — reopening log file");
|
||||
ReOpenLogFile?.Invoke();
|
||||
}));
|
||||
|
||||
_signalRegistrations.Add(PosixSignalRegistration.Create((PosixSignal)12, ctx =>
|
||||
@@ -615,6 +623,33 @@ public sealed class NatsServer : IMessageRouter, ISubListAccess, IDisposable
|
||||
{
|
||||
_clients.TryRemove(client.Id, out _);
|
||||
_logger.LogDebug("Removed client {ClientId}", client.Id);
|
||||
|
||||
// Snapshot for closed-connections tracking
|
||||
_closedClients.Enqueue(new ClosedClient
|
||||
{
|
||||
Cid = client.Id,
|
||||
Ip = client.RemoteIp ?? "",
|
||||
Port = client.RemotePort,
|
||||
Start = client.StartTime,
|
||||
Stop = DateTime.UtcNow,
|
||||
Reason = client.CloseReason.ToReasonString(),
|
||||
Name = client.ClientOpts?.Name ?? "",
|
||||
Lang = client.ClientOpts?.Lang ?? "",
|
||||
Version = client.ClientOpts?.Version ?? "",
|
||||
InMsgs = Interlocked.Read(ref client.InMsgs),
|
||||
OutMsgs = Interlocked.Read(ref client.OutMsgs),
|
||||
InBytes = Interlocked.Read(ref client.InBytes),
|
||||
OutBytes = Interlocked.Read(ref client.OutBytes),
|
||||
NumSubs = (uint)client.Subscriptions.Count,
|
||||
Rtt = client.Rtt,
|
||||
TlsVersion = client.TlsState?.TlsVersion ?? "",
|
||||
TlsCipherSuite = client.TlsState?.CipherSuite ?? "",
|
||||
});
|
||||
|
||||
// Cap closed clients list
|
||||
while (_closedClients.Count > MaxClosedClients)
|
||||
_closedClients.TryDequeue(out _);
|
||||
|
||||
var subList = client.Account?.SubList ?? _globalAccount.SubList;
|
||||
client.RemoveAllSubscriptions(subList);
|
||||
client.Account?.RemoveClient(client.Id);
|
||||
|
||||
@@ -16,5 +16,9 @@ public sealed class ServerStats
|
||||
public long SlowConsumerRoutes;
|
||||
public long SlowConsumerLeafs;
|
||||
public long SlowConsumerGateways;
|
||||
public long StaleConnectionClients;
|
||||
public long StaleConnectionRoutes;
|
||||
public long StaleConnectionLeafs;
|
||||
public long StaleConnectionGateways;
|
||||
public readonly ConcurrentDictionary<string, long> HttpReqStats = new();
|
||||
}
|
||||
|
||||
@@ -40,6 +40,62 @@ public sealed class SubList : IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns all subscriptions in the trie. For monitoring only.
|
||||
/// </summary>
|
||||
public List<Subscription> GetAllSubscriptions()
|
||||
{
|
||||
_lock.EnterReadLock();
|
||||
try
|
||||
{
|
||||
var result = new List<Subscription>();
|
||||
CollectAll(_root, result);
|
||||
return result;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_lock.ExitReadLock();
|
||||
}
|
||||
}
|
||||
|
||||
private static void CollectAll(TrieLevel level, List<Subscription> result)
|
||||
{
|
||||
foreach (var (_, node) in level.Nodes)
|
||||
{
|
||||
foreach (var sub in node.PlainSubs) result.Add(sub);
|
||||
foreach (var (_, qset) in node.QueueSubs)
|
||||
foreach (var sub in qset) result.Add(sub);
|
||||
if (node.Next != null) CollectAll(node.Next, result);
|
||||
}
|
||||
if (level.Pwc != null)
|
||||
{
|
||||
foreach (var sub in level.Pwc.PlainSubs) result.Add(sub);
|
||||
foreach (var (_, qset) in level.Pwc.QueueSubs)
|
||||
foreach (var sub in qset) result.Add(sub);
|
||||
if (level.Pwc.Next != null) CollectAll(level.Pwc.Next, result);
|
||||
}
|
||||
if (level.Fwc != null)
|
||||
{
|
||||
foreach (var sub in level.Fwc.PlainSubs) result.Add(sub);
|
||||
foreach (var (_, qset) in level.Fwc.QueueSubs)
|
||||
foreach (var sub in qset) result.Add(sub);
|
||||
if (level.Fwc.Next != null) CollectAll(level.Fwc.Next, result);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the current number of entries in the cache.
|
||||
/// </summary>
|
||||
public int CacheCount
|
||||
{
|
||||
get
|
||||
{
|
||||
_lock.EnterReadLock();
|
||||
try { return _cache?.Count ?? 0; }
|
||||
finally { _lock.ExitReadLock(); }
|
||||
}
|
||||
}
|
||||
|
||||
public void Insert(Subscription sub)
|
||||
{
|
||||
var subject = sub.Subject;
|
||||
|
||||
Reference in New Issue
Block a user