diff --git a/dotnet/src/ZB.MOM.NatsNet.Server/NatsServer.Logging.cs b/dotnet/src/ZB.MOM.NatsNet.Server/NatsServer.Logging.cs
index fb76d92..44380eb 100644
--- a/dotnet/src/ZB.MOM.NatsNet.Server/NatsServer.Logging.cs
+++ b/dotnet/src/ZB.MOM.NatsNet.Server/NatsServer.Logging.cs
@@ -171,6 +171,83 @@ public sealed partial class NatsServer
action(logger);
}
+ ///
+ /// Logs an error with a scope.
+ /// Mirrors Go Server.Errors().
+ ///
+ public void Errors(object scope, Exception e)
+ {
+ ExecuteLogCall(l => l.Errorf("{0} - {1}", scope, ErrorContextHelper.UnpackIfErrorCtx(e)));
+ }
+
+ ///
+ /// Logs an error with context.
+ /// Mirrors Go Server.Errorc().
+ ///
+ public void Errorc(string ctx, Exception e)
+ {
+ ExecuteLogCall(l => l.Errorf("{0}: {1}", ctx, ErrorContextHelper.UnpackIfErrorCtx(e)));
+ }
+
+ ///
+ /// Logs an error with scope and context.
+ /// Mirrors Go Server.Errorsc().
+ ///
+ public void Errorsc(object scope, string ctx, Exception e)
+ {
+ ExecuteLogCall(l => l.Errorf("{0} - {1}: {2}", scope, ctx, ErrorContextHelper.UnpackIfErrorCtx(e)));
+ }
+
+ ///
+ /// Rate-limited warning based on the raw format string.
+ /// Mirrors Go Server.rateLimitFormatWarnf().
+ ///
+ internal void RateLimitFormatWarnf(string format, params object[] args)
+ {
+ if (!_rateLimitLogging.TryAdd(format, DateTime.UtcNow))
+ {
+ return;
+ }
+
+ var statement = string.Format(format, args);
+ ExecuteLogCall(l => l.Warnf("{0}", statement));
+ }
+
+ ///
+ /// Rate-limited warning based on rendered statement.
+ /// Mirrors Go Server.RateLimitWarnf().
+ ///
+ public void RateLimitWarnf(string format, params object[] args)
+ {
+ var statement = string.Format(format, args);
+ if (!_rateLimitLogging.TryAdd(statement, DateTime.UtcNow))
+ {
+ return;
+ }
+
+ ExecuteLogCall(l => l.Warnf("{0}", statement));
+ }
+
+ ///
+ /// Rate-limited debug logging based on rendered statement.
+ /// Mirrors Go Server.RateLimitDebugf().
+ ///
+ public void RateLimitDebugf(string format, params object[] args)
+ {
+ var statement = string.Format(format, args);
+ if (!_rateLimitLogging.TryAdd(statement, DateTime.UtcNow))
+ {
+ return;
+ }
+
+ if (Interlocked.CompareExchange(ref _debugEnabled, 0, 0) == 0)
+ {
+ return;
+ }
+
+ ExecuteLogCall(l => l.Debugf("{0}", statement));
+ }
+
private static ILogger ToMicrosoftLogger(INatsLogger? logger)
{
return logger switch
diff --git a/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/Internal/ServerLoggerTests.cs b/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/Internal/ServerLoggerTests.cs
index 42943b3..a7968c8 100644
--- a/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/Internal/ServerLoggerTests.cs
+++ b/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/Internal/ServerLoggerTests.cs
@@ -220,6 +220,76 @@ public class ServerLoggerTests
content.ShouldContain("message-after-reopen");
}
+ [Fact]
+ public void ErrorsVariants_ShouldUseExpectedFormatting()
+ {
+ var (server, err) = NatsServer.NewServer(new ServerOptions());
+ err.ShouldBeNull();
+ server.ShouldNotBeNull();
+
+ var setLogger = GetRequiredServerMethod("SetLogger", typeof(INatsLogger), typeof(bool), typeof(bool));
+ var errors = GetRequiredServerMethod("Errors", typeof(object), typeof(Exception));
+ var errorc = GetRequiredServerMethod("Errorc", typeof(string), typeof(Exception));
+ var errorsc = GetRequiredServerMethod("Errorsc", typeof(object), typeof(string), typeof(Exception));
+
+ var logger = new CapturingLogger();
+ setLogger.Invoke(server, [logger, false, false]);
+
+ var wrapped = ErrorContextHelper.NewErrorCtx(new Exception("connection reset"), "leaf reconnect");
+ errors.Invoke(server, ["client", wrapped]);
+ errorc.Invoke(server, ["tls", wrapped]);
+ errorsc.Invoke(server, ["route", "cluster", wrapped]);
+
+ logger.Messages.Count.ShouldBe(3);
+ logger.Messages[0].ShouldContain("client - connection reset: leaf reconnect");
+ logger.Messages[1].ShouldContain("tls: connection reset: leaf reconnect");
+ logger.Messages[2].ShouldContain("route - cluster: connection reset: leaf reconnect");
+ }
+
+ [Fact]
+ public void RateLimitHelpers_ShouldRespectDedupeSemanticsAndDebugFlag()
+ {
+ var (server, err) = NatsServer.NewServer(new ServerOptions());
+ err.ShouldBeNull();
+ server.ShouldNotBeNull();
+
+ var setLogger = GetRequiredServerMethod("SetLogger", typeof(INatsLogger), typeof(bool), typeof(bool));
+ var rateLimitFormatWarnf = GetRequiredServerMethod("RateLimitFormatWarnf", typeof(string), typeof(object[]));
+ var rateLimitWarnf = GetRequiredServerMethod("RateLimitWarnf", typeof(string), typeof(object[]));
+ var rateLimitDebugf = GetRequiredServerMethod("RateLimitDebugf", typeof(string), typeof(object[]));
+
+ var logger = new CapturingLogger();
+ setLogger.Invoke(server, [logger, false, false]);
+
+ // Dedupe by format string (same format, different args => single warning).
+ rateLimitFormatWarnf.Invoke(server, ["format {0}", new object[] { "one" }]);
+ rateLimitFormatWarnf.Invoke(server, ["format {0}", new object[] { "two" }]);
+ rateLimitFormatWarnf.Invoke(server, ["other {0}", new object[] { "three" }]);
+ logger.Messages.Count.ShouldBe(2);
+ logger.Messages.ShouldContain("format one");
+ logger.Messages.ShouldContain("other three");
+
+ // Dedupe by rendered statement.
+ logger.Messages.Clear();
+ rateLimitWarnf.Invoke(server, ["warn {0}", new object[] { "same" }]);
+ rateLimitWarnf.Invoke(server, ["warn {0}", new object[] { "same" }]);
+ rateLimitWarnf.Invoke(server, ["warn {0}", new object[] { "other" }]);
+ logger.Messages.Count.ShouldBe(2);
+ logger.Messages.ShouldContain("warn same");
+ logger.Messages.ShouldContain("warn other");
+
+ // Debug dedupe + debug-flag gating.
+ logger.Messages.Clear();
+ rateLimitDebugf.Invoke(server, ["debug {0}", new object[] { "suppressed" }]);
+ logger.Messages.ShouldBeEmpty();
+
+ setLogger.Invoke(server, [logger, true, false]);
+ rateLimitDebugf.Invoke(server, ["debug {0}", new object[] { "visible" }]);
+ rateLimitDebugf.Invoke(server, ["debug {0}", new object[] { "visible" }]);
+ logger.Messages.Count.ShouldBe(1);
+ logger.Messages[0].ShouldContain("debug visible");
+ }
+
private static int GetPrivateIntField(object target, string fieldName)
{
var field = target.GetType().GetField(fieldName, BindingFlags.Instance | BindingFlags.NonPublic);
diff --git a/porting.db b/porting.db
index 80d6d43..93e7709 100644
Binary files a/porting.db and b/porting.db differ