feat: add structured logging, Shouldly assertions, CPM, and project documentation

- Add Microsoft.Extensions.Logging + Serilog to NatsServer and NatsClient
- Convert all test assertions from xUnit Assert to Shouldly
- Add NSubstitute package for future mocking needs
- Introduce Central Package Management via Directory.Packages.props
- Add documentation_rules.md with style guide, generation/update rules, component map
- Generate 10 documentation files across 5 component folders (GettingStarted, Protocol, Subscriptions, Server, Configuration/Operations)
- Update CLAUDE.md with logging, testing, porting, agent model, CPM, and documentation guidance
This commit is contained in:
Joseph Doherty
2026-02-22 21:05:53 -05:00
parent b9f4dec523
commit 539b2b7588
25 changed files with 2734 additions and 110 deletions

View File

@@ -8,4 +8,9 @@
<ProjectReference Include="..\NATS.Server\NATS.Server.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Serilog.Extensions.Hosting" />
<PackageReference Include="Serilog.Sinks.Console" />
</ItemGroup>
</Project>

View File

@@ -1,5 +1,11 @@
// src/NATS.Server.Host/Program.cs
using NATS.Server;
using Serilog;
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Debug()
.Enrich.FromLogContext()
.WriteTo.Console(outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}")
.CreateLogger();
var options = new NatsOptions();
@@ -20,7 +26,8 @@ for (int i = 0; i < args.Length; i++)
}
}
var server = new NatsServer(options);
using var loggerFactory = new Serilog.Extensions.Logging.SerilogLoggerFactory(Log.Logger);
var server = new NatsServer(options, loggerFactory);
var cts = new CancellationTokenSource();
Console.CancelKeyPress += (_, e) =>
@@ -29,12 +36,12 @@ Console.CancelKeyPress += (_, e) =>
cts.Cancel();
};
Console.WriteLine($"[NATS] Listening on {options.Host}:{options.Port}");
try
{
await server.StartAsync(cts.Token);
}
catch (OperationCanceledException) { }
Console.WriteLine("[NATS] Server stopped.");
finally
{
Log.CloseAndFlush();
}

View File

@@ -1,3 +1,5 @@
<Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
</ItemGroup>
</Project>

View File

@@ -3,6 +3,7 @@ using System.IO.Pipelines;
using System.Net.Sockets;
using System.Text;
using System.Text.Json;
using Microsoft.Extensions.Logging;
using NATS.Server.Protocol;
using NATS.Server.Subscriptions;
@@ -29,6 +30,7 @@ public sealed class NatsClient : IDisposable
private readonly NatsParser _parser;
private readonly SemaphoreSlim _writeLock = new(1, 1);
private readonly Dictionary<string, Subscription> _subs = new();
private readonly ILogger _logger;
public ulong Id { get; }
public ClientOptions? ClientOpts { get; private set; }
@@ -43,13 +45,14 @@ public sealed class NatsClient : IDisposable
public IReadOnlyDictionary<string, Subscription> Subscriptions => _subs;
public NatsClient(ulong id, Socket socket, NatsOptions options, ServerInfo serverInfo)
public NatsClient(ulong id, Socket socket, NatsOptions options, ServerInfo serverInfo, ILogger logger)
{
Id = id;
_socket = socket;
_stream = new NetworkStream(socket, ownsSocket: false);
_options = options;
_serverInfo = serverInfo;
_logger = logger;
_parser = new NatsParser(options.MaxPayload);
}
@@ -68,7 +71,10 @@ public sealed class NatsClient : IDisposable
await Task.WhenAny(fillTask, processTask);
}
catch (OperationCanceledException) { }
catch (Exception) { /* connection error -- clean up */ }
catch (Exception ex)
{
_logger.LogDebug(ex, "Client {ClientId} connection error", Id);
}
finally
{
Router?.RemoveClient(this);
@@ -160,6 +166,7 @@ public sealed class NatsClient : IDisposable
ClientOpts = JsonSerializer.Deserialize<ClientOptions>(cmd.Payload.Span)
?? new ClientOptions();
ConnectReceived = true;
_logger.LogDebug("CONNECT received from client {ClientId}, name={ClientName}", Id, ClientOpts?.Name);
}
private void ProcessSub(ParsedCommand cmd)
@@ -174,12 +181,16 @@ public sealed class NatsClient : IDisposable
_subs[cmd.Sid!] = sub;
sub.Client = this;
_logger.LogDebug("SUB {Subject} {Sid} from client {ClientId}", cmd.Subject, cmd.Sid, Id);
if (Router is ISubListAccess sl)
sl.SubList.Insert(sub);
}
private void ProcessUnsub(ParsedCommand cmd)
{
_logger.LogDebug("UNSUB {Sid} from client {ClientId}", cmd.Sid, Id);
if (!_subs.TryGetValue(cmd.Sid!, out var sub))
return;

View File

@@ -1,6 +1,7 @@
using System.Collections.Concurrent;
using System.Net;
using System.Net.Sockets;
using Microsoft.Extensions.Logging;
using NATS.Server.Protocol;
using NATS.Server.Subscriptions;
@@ -12,14 +13,18 @@ public sealed class NatsServer : IMessageRouter, ISubListAccess, IDisposable
private readonly ConcurrentDictionary<ulong, NatsClient> _clients = new();
private readonly SubList _subList = new();
private readonly ServerInfo _serverInfo;
private readonly ILogger<NatsServer> _logger;
private readonly ILoggerFactory _loggerFactory;
private Socket? _listener;
private ulong _nextClientId;
public SubList SubList => _subList;
public NatsServer(NatsOptions options)
public NatsServer(NatsOptions options, ILoggerFactory loggerFactory)
{
_options = options;
_loggerFactory = loggerFactory;
_logger = loggerFactory.CreateLogger<NatsServer>();
_serverInfo = new ServerInfo
{
ServerId = Guid.NewGuid().ToString("N")[..20].ToUpperInvariant(),
@@ -40,6 +45,8 @@ public sealed class NatsServer : IMessageRouter, ISubListAccess, IDisposable
_options.Port));
_listener.Listen(128);
_logger.LogInformation("Listening on {Host}:{Port}", _options.Host, _options.Port);
try
{
while (!ct.IsCancellationRequested)
@@ -47,7 +54,10 @@ public sealed class NatsServer : IMessageRouter, ISubListAccess, IDisposable
var socket = await _listener.AcceptAsync(ct);
var clientId = Interlocked.Increment(ref _nextClientId);
var client = new NatsClient(clientId, socket, _options, _serverInfo);
_logger.LogDebug("Client {ClientId} connected from {RemoteEndpoint}", clientId, socket.RemoteEndPoint);
var clientLogger = _loggerFactory.CreateLogger($"NATS.Server.NatsClient[{clientId}]");
var client = new NatsClient(clientId, socket, _options, _serverInfo, clientLogger);
client.Router = this;
_clients[clientId] = client;
@@ -69,6 +79,7 @@ public sealed class NatsServer : IMessageRouter, ISubListAccess, IDisposable
}
finally
{
_logger.LogDebug("Client {ClientId} disconnected", client.Id);
RemoveClient(client);
}
}
@@ -127,6 +138,7 @@ public sealed class NatsServer : IMessageRouter, ISubListAccess, IDisposable
public void RemoveClient(NatsClient client)
{
_clients.TryRemove(client.Id, out _);
_logger.LogDebug("Removed client {ClientId}", client.Id);
client.RemoveAllSubscriptions(_subList);
}