From fc96b6eb438ad2102f99f0b9b98312a12160a9f6 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Mon, 23 Feb 2026 05:29:40 -0500 Subject: [PATCH] feat: add system event DTOs and JSON source generator context --- src/NATS.Server/Events/EventJsonContext.cs | 12 + src/NATS.Server/Events/EventTypes.cs | 270 ++++++++++++++++++++ src/NATS.Server/NATS.Server.csproj | 3 + tests/NATS.Server.Tests/EventSystemTests.cs | 68 +++++ 4 files changed, 353 insertions(+) create mode 100644 src/NATS.Server/Events/EventJsonContext.cs create mode 100644 src/NATS.Server/Events/EventTypes.cs create mode 100644 tests/NATS.Server.Tests/EventSystemTests.cs diff --git a/src/NATS.Server/Events/EventJsonContext.cs b/src/NATS.Server/Events/EventJsonContext.cs new file mode 100644 index 0000000..7ac4ed2 --- /dev/null +++ b/src/NATS.Server/Events/EventJsonContext.cs @@ -0,0 +1,12 @@ +using System.Text.Json.Serialization; + +namespace NATS.Server.Events; + +[JsonSerializable(typeof(ConnectEventMsg))] +[JsonSerializable(typeof(DisconnectEventMsg))] +[JsonSerializable(typeof(AccountNumConns))] +[JsonSerializable(typeof(ServerStatsMsg))] +[JsonSerializable(typeof(ShutdownEventMsg))] +[JsonSerializable(typeof(LameDuckEventMsg))] +[JsonSerializable(typeof(AuthErrorEventMsg))] +internal partial class EventJsonContext : JsonSerializerContext; diff --git a/src/NATS.Server/Events/EventTypes.cs b/src/NATS.Server/Events/EventTypes.cs new file mode 100644 index 0000000..9da36bb --- /dev/null +++ b/src/NATS.Server/Events/EventTypes.cs @@ -0,0 +1,270 @@ +using System.Text.Json.Serialization; + +namespace NATS.Server.Events; + +/// +/// Server identity block embedded in all system events. +/// +public sealed class EventServerInfo +{ + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; + + [JsonPropertyName("host")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Host { get; set; } + + [JsonPropertyName("id")] + public string Id { get; set; } = string.Empty; + + [JsonPropertyName("cluster")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Cluster { get; set; } + + [JsonPropertyName("domain")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Domain { get; set; } + + [JsonPropertyName("ver")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Version { get; set; } + + [JsonPropertyName("seq")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public ulong Seq { get; set; } + + [JsonPropertyName("tags")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public Dictionary? Tags { get; set; } +} + +/// +/// Client identity block for connect/disconnect events. +/// +public sealed class EventClientInfo +{ + [JsonPropertyName("start")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public DateTime Start { get; set; } + + [JsonPropertyName("stop")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public DateTime Stop { get; set; } + + [JsonPropertyName("host")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Host { get; set; } + + [JsonPropertyName("id")] + public ulong Id { get; set; } + + [JsonPropertyName("acc")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Account { 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("ver")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Version { get; set; } + + [JsonPropertyName("rtt")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public long RttNanos { get; set; } +} + +public sealed class DataStats +{ + [JsonPropertyName("msgs")] + public long Msgs { get; set; } + + [JsonPropertyName("bytes")] + public long Bytes { get; set; } +} + +/// Client connect advisory. Go events.go:155-160. +public sealed class ConnectEventMsg +{ + public const string EventType = "io.nats.server.advisory.v1.client_connect"; + + [JsonPropertyName("type")] + public string Type { get; set; } = EventType; + + [JsonPropertyName("id")] + public string Id { get; set; } = string.Empty; + + [JsonPropertyName("timestamp")] + public DateTime Time { get; set; } + + [JsonPropertyName("server")] + public EventServerInfo Server { get; set; } = new(); + + [JsonPropertyName("client")] + public EventClientInfo Client { get; set; } = new(); +} + +/// Client disconnect advisory. Go events.go:167-174. +public sealed class DisconnectEventMsg +{ + public const string EventType = "io.nats.server.advisory.v1.client_disconnect"; + + [JsonPropertyName("type")] + public string Type { get; set; } = EventType; + + [JsonPropertyName("id")] + public string Id { get; set; } = string.Empty; + + [JsonPropertyName("timestamp")] + public DateTime Time { get; set; } + + [JsonPropertyName("server")] + public EventServerInfo Server { get; set; } = new(); + + [JsonPropertyName("client")] + public EventClientInfo 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; +} + +/// Account connection count heartbeat. Go events.go:210-214. +public sealed class AccountNumConns +{ + public const string EventType = "io.nats.server.advisory.v1.account_connections"; + + [JsonPropertyName("type")] + public string Type { get; set; } = EventType; + + [JsonPropertyName("id")] + public string Id { get; set; } = string.Empty; + + [JsonPropertyName("timestamp")] + public DateTime Time { get; set; } + + [JsonPropertyName("server")] + public EventServerInfo Server { get; set; } = new(); + + [JsonPropertyName("acc")] + public string AccountName { get; set; } = string.Empty; + + [JsonPropertyName("conns")] + public int Connections { get; set; } + + [JsonPropertyName("total_conns")] + public long TotalConnections { get; set; } + + [JsonPropertyName("subs")] + public int Subscriptions { get; set; } + + [JsonPropertyName("sent")] + public DataStats Sent { get; set; } = new(); + + [JsonPropertyName("received")] + public DataStats Received { get; set; } = new(); +} + +/// Server stats broadcast. Go events.go:150-153. +public sealed class ServerStatsMsg +{ + [JsonPropertyName("server")] + public EventServerInfo Server { get; set; } = new(); + + [JsonPropertyName("statsz")] + public ServerStatsData Stats { get; set; } = new(); +} + +public sealed class ServerStatsData +{ + [JsonPropertyName("start")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public DateTime Start { get; set; } + + [JsonPropertyName("mem")] + public long Mem { get; set; } + + [JsonPropertyName("cores")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public int Cores { get; set; } + + [JsonPropertyName("connections")] + public int Connections { get; set; } + + [JsonPropertyName("total_connections")] + public long TotalConnections { get; set; } + + [JsonPropertyName("active_accounts")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public int ActiveAccounts { get; set; } + + [JsonPropertyName("subscriptions")] + public long Subscriptions { 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("slow_consumers")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public long SlowConsumers { get; set; } +} + +/// Server shutdown notification. +public sealed class ShutdownEventMsg +{ + [JsonPropertyName("server")] + public EventServerInfo Server { get; set; } = new(); + + [JsonPropertyName("reason")] + public string Reason { get; set; } = string.Empty; +} + +/// Lame duck mode notification. +public sealed class LameDuckEventMsg +{ + [JsonPropertyName("server")] + public EventServerInfo Server { get; set; } = new(); +} + +/// Auth error advisory. +public sealed class AuthErrorEventMsg +{ + public const string EventType = "io.nats.server.advisory.v1.client_auth"; + + [JsonPropertyName("type")] + public string Type { get; set; } = EventType; + + [JsonPropertyName("id")] + public string Id { get; set; } = string.Empty; + + [JsonPropertyName("timestamp")] + public DateTime Time { get; set; } + + [JsonPropertyName("server")] + public EventServerInfo Server { get; set; } = new(); + + [JsonPropertyName("client")] + public EventClientInfo Client { get; set; } = new(); + + [JsonPropertyName("reason")] + public string Reason { get; set; } = string.Empty; +} diff --git a/src/NATS.Server/NATS.Server.csproj b/src/NATS.Server/NATS.Server.csproj index d85e688..390f283 100644 --- a/src/NATS.Server/NATS.Server.csproj +++ b/src/NATS.Server/NATS.Server.csproj @@ -1,4 +1,7 @@ + + + diff --git a/tests/NATS.Server.Tests/EventSystemTests.cs b/tests/NATS.Server.Tests/EventSystemTests.cs new file mode 100644 index 0000000..ab6e501 --- /dev/null +++ b/tests/NATS.Server.Tests/EventSystemTests.cs @@ -0,0 +1,68 @@ +using System.Text.Json; +using NATS.Server.Events; + +namespace NATS.Server.Tests; + +public class EventSystemTests +{ + [Fact] + public void ConnectEventMsg_serializes_with_correct_type() + { + var evt = new ConnectEventMsg + { + Type = ConnectEventMsg.EventType, + Id = "test123", + Time = new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc), + Server = new EventServerInfo { Name = "test-server", Id = "SRV1" }, + Client = new EventClientInfo { Id = 1, Account = "$G" }, + }; + + var json = JsonSerializer.Serialize(evt, EventJsonContext.Default.ConnectEventMsg); + json.ShouldContain("\"type\":\"io.nats.server.advisory.v1.client_connect\""); + json.ShouldContain("\"server\":"); + json.ShouldContain("\"client\":"); + } + + [Fact] + public void DisconnectEventMsg_serializes_with_reason() + { + var evt = new DisconnectEventMsg + { + Type = DisconnectEventMsg.EventType, + Id = "test456", + Time = DateTime.UtcNow, + Server = new EventServerInfo { Name = "test-server", Id = "SRV1" }, + Client = new EventClientInfo { Id = 2, Account = "myacc" }, + Reason = "Client Closed", + Sent = new DataStats { Msgs = 10, Bytes = 1024 }, + Received = new DataStats { Msgs = 5, Bytes = 512 }, + }; + + var json = JsonSerializer.Serialize(evt, EventJsonContext.Default.DisconnectEventMsg); + json.ShouldContain("\"reason\":\"Client Closed\""); + } + + [Fact] + public void ServerStatsMsg_serializes() + { + var evt = new ServerStatsMsg + { + Server = new EventServerInfo { Name = "srv1", Id = "ABC" }, + Stats = new ServerStatsData + { + Connections = 10, + TotalConnections = 100, + InMsgs = 5000, + OutMsgs = 4500, + InBytes = 1_000_000, + OutBytes = 900_000, + Mem = 50 * 1024 * 1024, + Subscriptions = 42, + }, + }; + + var json = JsonSerializer.Serialize(evt, EventJsonContext.Default.ServerStatsMsg); + json.ShouldContain("\"connections\":10"); + json.ShouldContain("\"in_msgs\":5000"); + } +}