feat: add ServerStats counters and NatsClient metadata for monitoring
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
using System.Buffers;
|
using System.Buffers;
|
||||||
using System.IO.Pipelines;
|
using System.IO.Pipelines;
|
||||||
|
using System.Net;
|
||||||
using System.Net.Sockets;
|
using System.Net.Sockets;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
@@ -32,11 +33,17 @@ public sealed class NatsClient : IDisposable
|
|||||||
private CancellationTokenSource? _clientCts;
|
private CancellationTokenSource? _clientCts;
|
||||||
private readonly Dictionary<string, Subscription> _subs = new();
|
private readonly Dictionary<string, Subscription> _subs = new();
|
||||||
private readonly ILogger _logger;
|
private readonly ILogger _logger;
|
||||||
|
private readonly ServerStats _serverStats;
|
||||||
|
|
||||||
public ulong Id { get; }
|
public ulong Id { get; }
|
||||||
public ClientOptions? ClientOpts { get; private set; }
|
public ClientOptions? ClientOpts { get; private set; }
|
||||||
public IMessageRouter? Router { get; set; }
|
public IMessageRouter? Router { get; set; }
|
||||||
public bool ConnectReceived { get; private set; }
|
public bool ConnectReceived { get; private set; }
|
||||||
|
public DateTime StartTime { get; }
|
||||||
|
private long _lastActivityTicks;
|
||||||
|
public DateTime LastActivity => new(Interlocked.Read(ref _lastActivityTicks), DateTimeKind.Utc);
|
||||||
|
public string? RemoteIp { get; }
|
||||||
|
public int RemotePort { get; }
|
||||||
|
|
||||||
// Stats
|
// Stats
|
||||||
public long InMsgs;
|
public long InMsgs;
|
||||||
@@ -50,7 +57,8 @@ public sealed class NatsClient : IDisposable
|
|||||||
|
|
||||||
public IReadOnlyDictionary<string, Subscription> Subscriptions => _subs;
|
public IReadOnlyDictionary<string, Subscription> Subscriptions => _subs;
|
||||||
|
|
||||||
public NatsClient(ulong id, Socket socket, NatsOptions options, ServerInfo serverInfo, ILogger logger)
|
public NatsClient(ulong id, Socket socket, NatsOptions options, ServerInfo serverInfo,
|
||||||
|
ILogger logger, ServerStats serverStats)
|
||||||
{
|
{
|
||||||
Id = id;
|
Id = id;
|
||||||
_socket = socket;
|
_socket = socket;
|
||||||
@@ -58,7 +66,15 @@ public sealed class NatsClient : IDisposable
|
|||||||
_options = options;
|
_options = options;
|
||||||
_serverInfo = serverInfo;
|
_serverInfo = serverInfo;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
|
_serverStats = serverStats;
|
||||||
_parser = new NatsParser(options.MaxPayload);
|
_parser = new NatsParser(options.MaxPayload);
|
||||||
|
StartTime = DateTime.UtcNow;
|
||||||
|
_lastActivityTicks = StartTime.Ticks;
|
||||||
|
if (socket.RemoteEndPoint is IPEndPoint ep)
|
||||||
|
{
|
||||||
|
RemoteIp = ep.Address.ToString();
|
||||||
|
RemotePort = ep.Port;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task RunAsync(CancellationToken ct)
|
public async Task RunAsync(CancellationToken ct)
|
||||||
@@ -147,6 +163,7 @@ public sealed class NatsClient : IDisposable
|
|||||||
|
|
||||||
private async ValueTask DispatchCommandAsync(ParsedCommand cmd, CancellationToken ct)
|
private async ValueTask DispatchCommandAsync(ParsedCommand cmd, CancellationToken ct)
|
||||||
{
|
{
|
||||||
|
Interlocked.Exchange(ref _lastActivityTicks, DateTime.UtcNow.Ticks);
|
||||||
switch (cmd.Type)
|
switch (cmd.Type)
|
||||||
{
|
{
|
||||||
case CommandType.Connect:
|
case CommandType.Connect:
|
||||||
@@ -226,6 +243,8 @@ public sealed class NatsClient : IDisposable
|
|||||||
{
|
{
|
||||||
Interlocked.Increment(ref InMsgs);
|
Interlocked.Increment(ref InMsgs);
|
||||||
Interlocked.Add(ref InBytes, cmd.Payload.Length);
|
Interlocked.Add(ref InBytes, cmd.Payload.Length);
|
||||||
|
Interlocked.Increment(ref _serverStats.InMsgs);
|
||||||
|
Interlocked.Add(ref _serverStats.InBytes, cmd.Payload.Length);
|
||||||
|
|
||||||
// Max payload validation (always, hard close)
|
// Max payload validation (always, hard close)
|
||||||
if (cmd.Payload.Length > _options.MaxPayload)
|
if (cmd.Payload.Length > _options.MaxPayload)
|
||||||
@@ -268,6 +287,8 @@ public sealed class NatsClient : IDisposable
|
|||||||
{
|
{
|
||||||
Interlocked.Increment(ref OutMsgs);
|
Interlocked.Increment(ref OutMsgs);
|
||||||
Interlocked.Add(ref OutBytes, payload.Length + headers.Length);
|
Interlocked.Add(ref OutBytes, payload.Length + headers.Length);
|
||||||
|
Interlocked.Increment(ref _serverStats.OutMsgs);
|
||||||
|
Interlocked.Add(ref _serverStats.OutBytes, payload.Length + headers.Length);
|
||||||
|
|
||||||
byte[] line;
|
byte[] line;
|
||||||
if (headers.Length > 0)
|
if (headers.Length > 0)
|
||||||
|
|||||||
@@ -16,11 +16,19 @@ public sealed class NatsServer : IMessageRouter, ISubListAccess, IDisposable
|
|||||||
private readonly ServerInfo _serverInfo;
|
private readonly ServerInfo _serverInfo;
|
||||||
private readonly ILogger<NatsServer> _logger;
|
private readonly ILogger<NatsServer> _logger;
|
||||||
private readonly ILoggerFactory _loggerFactory;
|
private readonly ILoggerFactory _loggerFactory;
|
||||||
|
private readonly ServerStats _stats = new();
|
||||||
private readonly TaskCompletionSource _listeningStarted = new(TaskCreationOptions.RunContinuationsAsynchronously);
|
private readonly TaskCompletionSource _listeningStarted = new(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||||
private Socket? _listener;
|
private Socket? _listener;
|
||||||
private ulong _nextClientId;
|
private ulong _nextClientId;
|
||||||
|
private long _startTimeTicks;
|
||||||
|
|
||||||
public SubList SubList => _subList;
|
public SubList SubList => _subList;
|
||||||
|
public ServerStats Stats => _stats;
|
||||||
|
public DateTime StartTime => new(Interlocked.Read(ref _startTimeTicks), DateTimeKind.Utc);
|
||||||
|
public string ServerId => _serverInfo.ServerId;
|
||||||
|
public string ServerName => _serverInfo.ServerName;
|
||||||
|
public int ClientCount => _clients.Count;
|
||||||
|
public IEnumerable<NatsClient> GetClients() => _clients.Values;
|
||||||
|
|
||||||
public Task WaitForReadyAsync() => _listeningStarted.Task;
|
public Task WaitForReadyAsync() => _listeningStarted.Task;
|
||||||
|
|
||||||
@@ -47,6 +55,7 @@ public sealed class NatsServer : IMessageRouter, ISubListAccess, IDisposable
|
|||||||
_listener.Bind(new IPEndPoint(
|
_listener.Bind(new IPEndPoint(
|
||||||
_options.Host == "0.0.0.0" ? IPAddress.Any : IPAddress.Parse(_options.Host),
|
_options.Host == "0.0.0.0" ? IPAddress.Any : IPAddress.Parse(_options.Host),
|
||||||
_options.Port));
|
_options.Port));
|
||||||
|
Interlocked.Exchange(ref _startTimeTicks, DateTime.UtcNow.Ticks);
|
||||||
_listener.Listen(128);
|
_listener.Listen(128);
|
||||||
_listeningStarted.TrySetResult();
|
_listeningStarted.TrySetResult();
|
||||||
|
|
||||||
@@ -84,11 +93,12 @@ public sealed class NatsServer : IMessageRouter, ISubListAccess, IDisposable
|
|||||||
}
|
}
|
||||||
|
|
||||||
var clientId = Interlocked.Increment(ref _nextClientId);
|
var clientId = Interlocked.Increment(ref _nextClientId);
|
||||||
|
Interlocked.Increment(ref _stats.TotalConnections);
|
||||||
|
|
||||||
_logger.LogDebug("Client {ClientId} connected from {RemoteEndpoint}", clientId, socket.RemoteEndPoint);
|
_logger.LogDebug("Client {ClientId} connected from {RemoteEndpoint}", clientId, socket.RemoteEndPoint);
|
||||||
|
|
||||||
var clientLogger = _loggerFactory.CreateLogger($"NATS.Server.NatsClient[{clientId}]");
|
var clientLogger = _loggerFactory.CreateLogger($"NATS.Server.NatsClient[{clientId}]");
|
||||||
var client = new NatsClient(clientId, socket, _options, _serverInfo, clientLogger);
|
var client = new NatsClient(clientId, socket, _options, _serverInfo, clientLogger, _stats);
|
||||||
client.Router = this;
|
client.Router = this;
|
||||||
_clients[clientId] = client;
|
_clients[clientId] = client;
|
||||||
|
|
||||||
|
|||||||
20
src/NATS.Server/ServerStats.cs
Normal file
20
src/NATS.Server/ServerStats.cs
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
using System.Collections.Concurrent;
|
||||||
|
|
||||||
|
namespace NATS.Server;
|
||||||
|
|
||||||
|
public sealed class ServerStats
|
||||||
|
{
|
||||||
|
public long InMsgs;
|
||||||
|
public long OutMsgs;
|
||||||
|
public long InBytes;
|
||||||
|
public long OutBytes;
|
||||||
|
public long TotalConnections;
|
||||||
|
public long SlowConsumers;
|
||||||
|
public long StaleConnections;
|
||||||
|
public long Stalls;
|
||||||
|
public long SlowConsumerClients;
|
||||||
|
public long SlowConsumerRoutes;
|
||||||
|
public long SlowConsumerLeafs;
|
||||||
|
public long SlowConsumerGateways;
|
||||||
|
public readonly ConcurrentDictionary<string, long> HttpReqStats = new();
|
||||||
|
}
|
||||||
@@ -39,7 +39,7 @@ public class ClientTests : IAsyncDisposable
|
|||||||
Port = 4222,
|
Port = 4222,
|
||||||
};
|
};
|
||||||
|
|
||||||
_natsClient = new NatsClient(1, _serverSocket, new NatsOptions(), serverInfo, NullLogger.Instance);
|
_natsClient = new NatsClient(1, _serverSocket, new NatsOptions(), serverInfo, NullLogger.Instance, new ServerStats());
|
||||||
}
|
}
|
||||||
|
|
||||||
public async ValueTask DisposeAsync()
|
public async ValueTask DisposeAsync()
|
||||||
|
|||||||
84
tests/NATS.Server.Tests/ServerStatsTests.cs
Normal file
84
tests/NATS.Server.Tests/ServerStatsTests.cs
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
using System.Net;
|
||||||
|
using System.Net.Sockets;
|
||||||
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
|
using NATS.Server;
|
||||||
|
|
||||||
|
namespace NATS.Server.Tests;
|
||||||
|
|
||||||
|
public class ServerStatsTests : IAsyncLifetime
|
||||||
|
{
|
||||||
|
private readonly NatsServer _server;
|
||||||
|
private readonly int _port;
|
||||||
|
private readonly CancellationTokenSource _cts = new();
|
||||||
|
|
||||||
|
public ServerStatsTests()
|
||||||
|
{
|
||||||
|
_port = GetFreePort();
|
||||||
|
_server = new NatsServer(new NatsOptions { Port = _port }, NullLoggerFactory.Instance);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task InitializeAsync()
|
||||||
|
{
|
||||||
|
_ = _server.StartAsync(_cts.Token);
|
||||||
|
await _server.WaitForReadyAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task DisposeAsync()
|
||||||
|
{
|
||||||
|
_cts.Cancel();
|
||||||
|
_server.Dispose();
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Server_has_start_time()
|
||||||
|
{
|
||||||
|
_server.StartTime.ShouldNotBe(default);
|
||||||
|
_server.StartTime.ShouldBeLessThanOrEqualTo(DateTime.UtcNow);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Server_tracks_total_connections()
|
||||||
|
{
|
||||||
|
using var sock = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
|
||||||
|
await sock.ConnectAsync(new IPEndPoint(IPAddress.Loopback, _port));
|
||||||
|
await Task.Delay(100);
|
||||||
|
_server.Stats.TotalConnections.ShouldBeGreaterThanOrEqualTo(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Server_stats_track_messages()
|
||||||
|
{
|
||||||
|
using var pub = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
|
||||||
|
await pub.ConnectAsync(new IPEndPoint(IPAddress.Loopback, _port));
|
||||||
|
|
||||||
|
var buf = new byte[4096];
|
||||||
|
await pub.ReceiveAsync(buf, SocketFlags.None); // INFO
|
||||||
|
|
||||||
|
await pub.SendAsync("CONNECT {}\r\nSUB test 1\r\nPUB test 5\r\nhello\r\n"u8.ToArray());
|
||||||
|
await Task.Delay(200);
|
||||||
|
|
||||||
|
_server.Stats.InMsgs.ShouldBeGreaterThanOrEqualTo(1);
|
||||||
|
_server.Stats.InBytes.ShouldBeGreaterThanOrEqualTo(5);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Client_has_metadata()
|
||||||
|
{
|
||||||
|
using var sock = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
|
||||||
|
await sock.ConnectAsync(new IPEndPoint(IPAddress.Loopback, _port));
|
||||||
|
await Task.Delay(100);
|
||||||
|
|
||||||
|
var client = _server.GetClients().First();
|
||||||
|
client.RemoteIp.ShouldNotBeNullOrEmpty();
|
||||||
|
client.RemotePort.ShouldBeGreaterThan(0);
|
||||||
|
client.StartTime.ShouldNotBe(default);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int GetFreePort()
|
||||||
|
{
|
||||||
|
using var sock = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
|
||||||
|
sock.Bind(new IPEndPoint(IPAddress.Loopback, 0));
|
||||||
|
return ((IPEndPoint)sock.LocalEndPoint!).Port;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user