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