From b79b5f6222927a81aa4b66ade832c9fa625649e8 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Sat, 28 Feb 2026 08:01:04 -0500 Subject: [PATCH] feat(batch4-task3): implement error and rate-limit logging helpers --- .../NatsServer.Logging.cs | 77 ++++++++++++++++++ .../Internal/ServerLoggerTests.cs | 70 ++++++++++++++++ porting.db | Bin 6365184 -> 6369280 bytes 3 files changed, 147 insertions(+) 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 80d6d4341ab3c2cdb3d10343d259f0841bb05ebc..93e7709ed332d2c93a166b85995f41c028918876 100644 GIT binary patch delta 2196 zcmZ|PdrVVT7y$5lPH)eB^*(6JqYtDYQa~py3@qqSeD0w_(aDsD7A=`Zg&{g;7GpCr zqihKzo5vaFU|X_3mN;E9&t~|8J+dX5ZDc{>7AMZQXv}o#_6HcH`&wp^Q2zKO-`D${ z?|kPzdZ7QH>A=;4rY@JcisOi_+p`c=o!PXg2t9bCw}|x?p_U#*m^gq^bYxKHFmhut zod$+cDf)-)97gqKB(d|?(P{H?)s%@hx9(`$9_|Q>x^kWiBr*-M!bGM*=1yb^WLhGV zS^q7xG|cy5jiYTnMkQTdgFmNy5Pwg5g4n{&2k{8+Ay+tUOSmQ6-X7i}lGx6VGvN+p77*ap$uZ;dn}j0kT+?19)WJQRN83Ln>t8f0#EtPbb$yk(GUX8rZpS-U_o zrQ%&3yPHK_Yv79Gkp@wIJW?T=5swsz?D0s3NRCGmOFM<{WQ%Jw8yCB&)G~lS#+r@x z&d-?7ZV%u*mB8=T&bC92D;Ssn&m33GckVe%mK^yVd;|SXMx`IcRwQy&!a1_ce2{qt z$X3krC%7hh?-AL+Zu~;NK)hPZ-K0H(q*9NRcPS|xDhPWWE{sy?r-P)4j}1h7kD0Ql z?;=T~CkDw5+WhtmZ!DcD1w8(e3ZJjSPa7_h=EMavy>*df%89FfIyywYrYS>YW1@!8 z_lJo4i5j;35;>7hWS1uBYnXPI?3H-+Huo+)GD^DTNT5rF;g+agn%!rz(NL6B(r?19 zeELI_6loJ%Me@3A^yC^pD z-oxZqwqQhm9buG5PxawhYT^Zy~2CK?OR*6!V9fU%G61jLSMEU1@wZq-mS}% zu1NGq9_-UZL}Hc(NrVAnjh*P*aUo>y9yr{TWJ_yAz?pm!_D|C z)vWNTd4@LGsncq#r7PAtKo;)$Hf?O~QRX z7OVkl!HZxWr~@y7^$(m^6O+L$++@#nyO_ROJ znyAT}_@{_|HWAv~cnkYoNaByrp6~aZv*$eL z*-pnqK-V!8&~>fV=SdO|Kjz*_^3K=gC6mc_dXq(OGHL7~MqwBsVJfkLbDX#+{~5N7 zlXP-VygE)Q^@NJy+vJ?S$UH0+z1*_5b&r3a-{Q6EqUpnCu;E@rm(XzkH*p{Rs@X5nLUvD_?o%ZJUwFjGRm6~kUS!`$*|Nonvm`rF$44F(?C z^I?;J4>h>8o9=V5ML`||D zag0%*E+W*-Rm2ydc{Uy*8H`X3$%dmr#j z@ym67nXt~BE;ID}$aBGcgKq-gC4Dq>Wv(|v%MBg@9o4!hV@j7POd=ky2Vy>P*}yl- z3s$YWI?7|9wOeO~uYck-t0wRTdMFBV7ibQh$t-^*WrRy4{m`2pm_Dzw!Mi~o1+%04 zl4dQHKL2mZ;h`h~4hH$hq9w?Wui9C7C{z3mBa0l8077e9V{a&EfqTN z^E|k|!yFAmC%Fx#@AG)*YG-BImeB#`2mkz{&>S8 zFNV`MjUH%UjG`r$3R77u6bngZY1wZbe~h@%Jg``eltoBR#509)%y z+m6k*(zanMTWR?sdq#bmP}&2112kTY&#FcO_Cb0OdIxDD+!>@ru=65Spz|Va7rq7c zlub4&(qiZ~q@+@JoUVlZ!Gdg<4X{4e!``C@Sb(-tAMK}S=n1x;y}~MlLDRk@#0T!t zIy*SSwK6y~uSUb!dDRMyag{3M%&RVd4V6|H3D-J6TTq?QY}2M7DZbJo8tvKzMossG z%c1?5!M+J?er|%)irNw_jlglQc8Zy+_49Yvr> zv<5{X2Xdmd$c3WOI