diff --git a/src/NATS.Server/ClientTraceInfo.cs b/src/NATS.Server/ClientTraceInfo.cs
new file mode 100644
index 0000000..5653da1
--- /dev/null
+++ b/src/NATS.Server/ClientTraceInfo.cs
@@ -0,0 +1,83 @@
+namespace NATS.Server;
+
+///
+/// Per-client trace configuration and echo control.
+/// Go reference: server/client.go — c.trace, c.echo fields.
+///
+public sealed class ClientTraceInfo
+{
+ private bool _traceEnabled;
+ private bool _echoEnabled = true; // default: echo is enabled
+ private readonly List _traceLog = new();
+ private readonly Lock _lock = new();
+
+ /// Whether message delivery tracing is enabled for this client.
+ public bool TraceEnabled
+ {
+ get => Volatile.Read(ref _traceEnabled);
+ set => Volatile.Write(ref _traceEnabled, value);
+ }
+
+ /// Whether this client echoes its own published messages back.
+ public bool EchoEnabled
+ {
+ get => Volatile.Read(ref _echoEnabled);
+ set => Volatile.Write(ref _echoEnabled, value);
+ }
+
+ ///
+ /// Records a message delivery trace if tracing is enabled.
+ /// Go reference: server/client.go — traceMsg / TraceMsgDelivery.
+ ///
+ public void TraceMsgDelivery(string subject, string destination, int payloadSize)
+ {
+ if (!TraceEnabled) return;
+ lock (_lock)
+ {
+ _traceLog.Add(new TraceRecord
+ {
+ Subject = subject,
+ Destination = destination,
+ PayloadSize = payloadSize,
+ TimestampUtc = DateTime.UtcNow,
+ });
+ }
+ }
+
+ ///
+ /// Returns whether a message from this client should be delivered back to it.
+ /// When echo is disabled, messages published by this client are not delivered to
+ /// subscriptions on the same client.
+ /// Go reference: server/client.go — c.echo check in deliverMsg.
+ ///
+ public bool ShouldEcho(string publisherClientId, string subscriberClientId)
+ {
+ if (EchoEnabled) return true;
+ return !string.Equals(publisherClientId, subscriberClientId, StringComparison.Ordinal);
+ }
+
+ /// Returns all trace records and clears the log.
+ public IReadOnlyList DrainTraceLog()
+ {
+ lock (_lock)
+ {
+ var copy = _traceLog.ToList();
+ _traceLog.Clear();
+ return copy;
+ }
+ }
+
+ /// Current trace log count.
+ public int TraceLogCount
+ {
+ get { lock (_lock) return _traceLog.Count; }
+ }
+}
+
+public sealed record TraceRecord
+{
+ public string Subject { get; init; } = string.Empty;
+ public string Destination { get; init; } = string.Empty;
+ public int PayloadSize { get; init; }
+ public DateTime TimestampUtc { get; init; }
+}
diff --git a/tests/NATS.Server.Tests/Auth/AccountGoParityTests.cs b/tests/NATS.Server.Tests/Auth/AccountGoParityTests.cs
index 1419e21..fffbdd1 100644
--- a/tests/NATS.Server.Tests/Auth/AccountGoParityTests.cs
+++ b/tests/NATS.Server.Tests/Auth/AccountGoParityTests.cs
@@ -3,7 +3,7 @@
using NATS.Server.Auth;
using NATS.Server.Imports;
-using NATS.Server.Subscriptions;
+using ServerSubscriptions = NATS.Server.Subscriptions;
namespace NATS.Server.Tests.Auth;
@@ -28,7 +28,7 @@ public class AccountGoParityTests
using var accB = new Account("B");
// Add subscriptions to account A's SubList
- var subA = new Subscription { Subject = "foo", Sid = "1" };
+ var subA = new ServerSubscriptions.Subscription { Subject = "foo", Sid = "1" };
accA.SubList.Insert(subA);
// Account B should not see account A's subscriptions
@@ -52,8 +52,8 @@ public class AccountGoParityTests
// Go: TestAccountWildcardRouteMapping — wildcards work per-account.
using var acc = new Account("TEST");
- var sub1 = new Subscription { Subject = "orders.*", Sid = "1" };
- var sub2 = new Subscription { Subject = "orders.>", Sid = "2" };
+ var sub1 = new ServerSubscriptions.Subscription { Subject = "orders.*", Sid = "1" };
+ var sub2 = new ServerSubscriptions.Subscription { Subject = "orders.>", Sid = "2" };
acc.SubList.Insert(sub1);
acc.SubList.Insert(sub2);
diff --git a/tests/NATS.Server.Tests/ClientTraceTests.cs b/tests/NATS.Server.Tests/ClientTraceTests.cs
new file mode 100644
index 0000000..9b4ba7c
--- /dev/null
+++ b/tests/NATS.Server.Tests/ClientTraceTests.cs
@@ -0,0 +1,136 @@
+using NATS.Server;
+using Shouldly;
+
+namespace NATS.Server.Tests;
+
+///
+/// Tests for per-client trace delivery and echo control.
+/// Go reference: server/client.go — c.trace, c.echo fields and TraceMsgDelivery / deliverMsg logic.
+///
+public class ClientTraceTests
+{
+ // 1. TraceEnabled defaults to false
+ [Fact]
+ public void TraceEnabled_defaults_to_false()
+ {
+ var info = new ClientTraceInfo();
+ info.TraceEnabled.ShouldBeFalse();
+ }
+
+ // 2. EchoEnabled defaults to true
+ [Fact]
+ public void EchoEnabled_defaults_to_true()
+ {
+ var info = new ClientTraceInfo();
+ info.EchoEnabled.ShouldBeTrue();
+ }
+
+ // 3. TraceMsgDelivery records when enabled
+ [Fact]
+ public void TraceMsgDelivery_records_when_enabled()
+ {
+ var info = new ClientTraceInfo();
+ info.TraceEnabled = true;
+
+ info.TraceMsgDelivery("foo.bar", "client-1", 42);
+
+ info.TraceLogCount.ShouldBe(1);
+ }
+
+ // 4. TraceMsgDelivery skips when disabled
+ [Fact]
+ public void TraceMsgDelivery_skips_when_disabled()
+ {
+ var info = new ClientTraceInfo();
+ // TraceEnabled is false by default
+
+ info.TraceMsgDelivery("foo.bar", "client-1", 42);
+
+ info.TraceLogCount.ShouldBe(0);
+ }
+
+ // 5. TraceMsgDelivery captures subject, destination and payload size correctly
+ [Fact]
+ public void TraceMsgDelivery_captures_subject_destination_size()
+ {
+ var info = new ClientTraceInfo();
+ info.TraceEnabled = true;
+ var before = DateTime.UtcNow;
+
+ info.TraceMsgDelivery("orders.new", "client-42", 128);
+
+ var records = info.DrainTraceLog();
+ records.Count.ShouldBe(1);
+ var rec = records[0];
+ rec.Subject.ShouldBe("orders.new");
+ rec.Destination.ShouldBe("client-42");
+ rec.PayloadSize.ShouldBe(128);
+ rec.TimestampUtc.ShouldBeGreaterThanOrEqualTo(before);
+ rec.TimestampUtc.ShouldBeLessThanOrEqualTo(DateTime.UtcNow);
+ }
+
+ // 6. ShouldEcho returns true when echo is enabled (same client)
+ [Fact]
+ public void ShouldEcho_true_when_echo_enabled()
+ {
+ var info = new ClientTraceInfo();
+ info.EchoEnabled = true;
+
+ info.ShouldEcho("client-1", "client-1").ShouldBeTrue();
+ }
+
+ // 7. ShouldEcho returns false when echo is disabled and same client
+ [Fact]
+ public void ShouldEcho_false_when_echo_disabled_same_client()
+ {
+ var info = new ClientTraceInfo();
+ info.EchoEnabled = false;
+
+ info.ShouldEcho("client-1", "client-1").ShouldBeFalse();
+ }
+
+ // 8. ShouldEcho returns true when echo is disabled but different client
+ [Fact]
+ public void ShouldEcho_true_when_echo_disabled_different_client()
+ {
+ var info = new ClientTraceInfo();
+ info.EchoEnabled = false;
+
+ info.ShouldEcho("client-1", "client-2").ShouldBeTrue();
+ }
+
+ // 9. DrainTraceLog returns records and clears the log
+ [Fact]
+ public void DrainTraceLog_returns_and_clears()
+ {
+ var info = new ClientTraceInfo();
+ info.TraceEnabled = true;
+ info.TraceMsgDelivery("a", "dest-a", 10);
+ info.TraceMsgDelivery("b", "dest-b", 20);
+
+ var drained = info.DrainTraceLog();
+
+ drained.Count.ShouldBe(2);
+ info.TraceLogCount.ShouldBe(0);
+ }
+
+ // 10. TraceLogCount reflects current entries before and after drain
+ [Fact]
+ public void TraceLogCount_reflects_current_entries()
+ {
+ var info = new ClientTraceInfo();
+ info.TraceEnabled = true;
+
+ info.TraceLogCount.ShouldBe(0);
+
+ info.TraceMsgDelivery("x", "dest-x", 5);
+ info.TraceMsgDelivery("y", "dest-y", 15);
+ info.TraceMsgDelivery("z", "dest-z", 25);
+
+ info.TraceLogCount.ShouldBe(3);
+
+ info.DrainTraceLog();
+
+ info.TraceLogCount.ShouldBe(0);
+ }
+}