feat: complete system event payload fields (Gap 10.6)

Add EventBuilder static class to EventTypes.cs with helpers for constructing
fully-populated ConnectEventMsg, DisconnectEventMsg, AccountNumConns, and
ServerStatsMsg. Also add RemoteServerShutdownEvent, RemoteServerUpdateEvent,
LeafNodeConnectEvent, and LeafNodeDisconnectEvent advisory types.

Add FullEventPayloadTests.cs (10 tests) covering all builders, GenerateEventId
uniqueness, GetTimestamp ISO 8601 format, DataStats zero defaults, and
ConnectEventMsg JSON roundtrip.
This commit is contained in:
Joseph Doherty
2026-02-25 13:11:58 -05:00
parent 619acc3c08
commit a6e7778c6c
4 changed files with 862 additions and 0 deletions

View File

@@ -540,6 +540,143 @@ public sealed class OcspPeerRejectEventMsg
public string Reason { get; set; } = string.Empty;
}
/// <summary>
/// Remote server shutdown advisory.
/// Go reference: events.go — remote server lifecycle.
/// </summary>
public sealed class RemoteServerShutdownEvent
{
public const string EventType = "io.nats.server.advisory.v1.remote_shutdown";
[JsonPropertyName("type")]
public string Type { get; init; } = EventType;
[JsonPropertyName("id")]
public string Id { get; init; } = Guid.NewGuid().ToString("N");
[JsonPropertyName("time")]
public string Time { get; init; } = DateTime.UtcNow.ToString("O");
[JsonPropertyName("server")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public EventServerInfo? Server { get; set; }
[JsonPropertyName("remote_server_id")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? RemoteServerId { get; set; }
[JsonPropertyName("remote_server_name")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? RemoteServerName { get; set; }
[JsonPropertyName("reason")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? Reason { get; set; }
}
/// <summary>
/// Remote server update advisory.
/// Go reference: events.go — remote server lifecycle.
/// </summary>
public sealed class RemoteServerUpdateEvent
{
public const string EventType = "io.nats.server.advisory.v1.remote_update";
[JsonPropertyName("type")]
public string Type { get; init; } = EventType;
[JsonPropertyName("id")]
public string Id { get; init; } = Guid.NewGuid().ToString("N");
[JsonPropertyName("time")]
public string Time { get; init; } = DateTime.UtcNow.ToString("O");
[JsonPropertyName("server")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public EventServerInfo? Server { get; set; }
[JsonPropertyName("remote_server_id")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? RemoteServerId { get; set; }
[JsonPropertyName("remote_server_name")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? RemoteServerName { get; set; }
/// <summary>Update category, e.g. "routes_changed", "config_updated".</summary>
[JsonPropertyName("update_type")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? UpdateType { get; set; }
}
/// <summary>
/// Leaf node connect advisory.
/// Go reference: events.go — leaf node lifecycle.
/// </summary>
public sealed class LeafNodeConnectEvent
{
public const string EventType = "io.nats.server.advisory.v1.leafnode_connect";
[JsonPropertyName("type")]
public string Type { get; init; } = EventType;
[JsonPropertyName("id")]
public string Id { get; init; } = Guid.NewGuid().ToString("N");
[JsonPropertyName("time")]
public string Time { get; init; } = DateTime.UtcNow.ToString("O");
[JsonPropertyName("server")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public EventServerInfo? Server { get; set; }
[JsonPropertyName("leaf_node_id")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? LeafNodeId { get; set; }
[JsonPropertyName("leaf_node_name")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? LeafNodeName { get; set; }
[JsonPropertyName("remote_url")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? RemoteUrl { get; set; }
[JsonPropertyName("account")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? Account { get; set; }
}
/// <summary>
/// Leaf node disconnect advisory.
/// Go reference: events.go — leaf node lifecycle.
/// </summary>
public sealed class LeafNodeDisconnectEvent
{
public const string EventType = "io.nats.server.advisory.v1.leafnode_disconnect";
[JsonPropertyName("type")]
public string Type { get; init; } = EventType;
[JsonPropertyName("id")]
public string Id { get; init; } = Guid.NewGuid().ToString("N");
[JsonPropertyName("time")]
public string Time { get; init; } = DateTime.UtcNow.ToString("O");
[JsonPropertyName("server")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public EventServerInfo? Server { get; set; }
[JsonPropertyName("leaf_node_id")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? LeafNodeId { get; set; }
[JsonPropertyName("reason")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? Reason { get; set; }
}
/// <summary>
/// Account numeric connections request.
/// Go reference: events.go:233-236 accNumConnsReq.
@@ -552,3 +689,148 @@ public sealed class AccNumConnsReq
[JsonPropertyName("acc")]
public string Account { get; set; } = string.Empty;
}
/// <summary>
/// Factory helpers that construct fully-populated system event messages,
/// mirroring Go's inline struct initialization patterns in events.go.
/// Go reference: events.go sendConnectEventMsg, sendDisconnectEventMsg,
/// sendAccountNumConns, sendServerStats.
/// </summary>
public static class EventBuilder
{
/// <summary>
/// Build a ConnectEventMsg with all required fields.
/// Go reference: events.go sendConnectEventMsg (around line 636).
/// </summary>
public static ConnectEventMsg BuildConnectEvent(
string serverId, string serverName, string? cluster,
ulong clientId, string host, string? account, string? user, string? name,
string? lang, string? version) =>
new()
{
Id = GenerateEventId(),
Time = DateTime.UtcNow,
Server = new EventServerInfo
{
Id = serverId,
Name = serverName,
Cluster = cluster,
},
Client = new EventClientInfo
{
Id = clientId,
Host = host,
Account = account,
User = user,
Name = name,
Lang = lang,
Version = version,
Start = DateTime.UtcNow,
},
};
/// <summary>
/// Build a DisconnectEventMsg with all required fields.
/// Go reference: events.go sendDisconnectEventMsg (around line 668).
/// </summary>
public static DisconnectEventMsg BuildDisconnectEvent(
string serverId, string serverName, string? cluster,
ulong clientId, string host, string? account, string? user,
string reason, DataStats sent, DataStats received) =>
new()
{
Id = GenerateEventId(),
Time = DateTime.UtcNow,
Server = new EventServerInfo
{
Id = serverId,
Name = serverName,
Cluster = cluster,
},
Client = new EventClientInfo
{
Id = clientId,
Host = host,
Account = account,
User = user,
Stop = DateTime.UtcNow,
},
Reason = reason,
Sent = sent,
Received = received,
};
/// <summary>
/// Build an AccountNumConns event.
/// Go reference: events.go sendAccountNumConns (around line 714).
/// </summary>
public static AccountNumConns BuildAccountConnsEvent(
string serverId, string serverName,
string accountName, int connections, int leafNodes,
int totalConnections, int numSubscriptions,
DataStats sent, DataStats received, long slowConsumers) =>
new()
{
Id = GenerateEventId(),
Time = DateTime.UtcNow,
Server = new EventServerInfo
{
Id = serverId,
Name = serverName,
},
AccountName = accountName,
Connections = connections,
LeafNodes = leafNodes,
TotalConnections = totalConnections,
NumSubscriptions = (uint)numSubscriptions,
Sent = sent,
Received = received,
SlowConsumers = slowConsumers,
};
/// <summary>
/// Build a ServerStatsMsg.
/// Go reference: events.go sendStatsz (around line 742).
/// </summary>
public static ServerStatsMsg BuildServerStats(
string serverId, string serverName,
long mem, int cores, double cpu,
int connections, int totalConnections, int activeAccounts,
int subscriptions, DataStats sent, DataStats received) =>
new()
{
Server = new EventServerInfo
{
Id = serverId,
Name = serverName,
},
Stats = new ServerStatsData
{
Start = DateTime.UtcNow,
Mem = mem,
Cores = cores,
Cpu = cpu,
Connections = connections,
TotalConnections = totalConnections,
ActiveAccounts = activeAccounts,
Subscriptions = subscriptions,
Sent = sent,
Received = received,
InMsgs = received.Msgs,
OutMsgs = sent.Msgs,
InBytes = received.Bytes,
OutBytes = sent.Bytes,
},
};
/// <summary>
/// Generate a unique event ID. Go uses nuid for this.
/// Go reference: events.go — nuid.Next() for event IDs.
/// </summary>
public static string GenerateEventId() => Guid.NewGuid().ToString("N");
/// <summary>
/// Get an ISO 8601 timestamp string for embedding in events.
/// </summary>
public static string GetTimestamp() => DateTime.UtcNow.ToString("O");
}