From 21d4a0b904633250b5bd6f8f2a0fc5681a0df4b8 Mon Sep 17 00:00:00 2001 From: Abdullah hamza Date: Wed, 8 Apr 2026 19:57:45 +0300 Subject: [PATCH 1/3] feat(Logger): add ReplaceAppenders for atomic appender swap Add method to atomically replace all appenders with a new collection. --- src/log4net/Repository/Hierarchy/Logger.cs | 35 +++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/src/log4net/Repository/Hierarchy/Logger.cs b/src/log4net/Repository/Hierarchy/Logger.cs index 3fd13cc6..cd7f005d 100644 --- a/src/log4net/Repository/Hierarchy/Logger.cs +++ b/src/log4net/Repository/Hierarchy/Logger.cs @@ -594,4 +594,37 @@ protected virtual void ForcedLog(LoggingEvent logEvent) CallAppenders(logEvent); } -} \ No newline at end of file + + /// + /// Atomically replaces all appenders with the provided collection. + /// + /// The new set of appenders to attach. + /// + /// + /// This method removes the existing appenders and attaches all new + /// appenders inside a single writer lock, minimizing the window + /// during which the logger has no appenders. This reduces silent log + /// event loss during reconfiguration. + /// + /// + public virtual void ReplaceAppenders(IEnumerable appenders) + { + _appenderLock.AcquireWriterLock(); + try + { + _appenderAttachedImpl?.RemoveAllAppenders(); + _appenderAttachedImpl = null; + + foreach (IAppender appender in appenders) + { + _appenderAttachedImpl ??= new(); + _appenderAttachedImpl.AddAppender(appender); + } + } + finally + { + _appenderLock.ReleaseWriterLock(); + } + } + + } From 5034762fcac0ef3b80e02a0221fd3a30b52ef5c1 Mon Sep 17 00:00:00 2001 From: Abdullah hamza Date: Wed, 8 Apr 2026 20:08:39 +0300 Subject: [PATCH 2/3] Enable diagnostics by resetting Emfix(Hierarchy): reset EmittedNoAppenderWarning in ResetConfigurationittedNoAppenderWarning Reset the warning flag to allow diagnostics during reconfiguration. --- src/log4net/Repository/Hierarchy/Hierarchy.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/log4net/Repository/Hierarchy/Hierarchy.cs b/src/log4net/Repository/Hierarchy/Hierarchy.cs index 24be509d..db2c9a18 100644 --- a/src/log4net/Repository/Hierarchy/Hierarchy.cs +++ b/src/log4net/Repository/Hierarchy/Hierarchy.cs @@ -283,7 +283,11 @@ public override void ResetConfiguration() { Root.Level = LevelMap.LookupWithDefault(Level.Debug); Threshold = LevelMap.LookupWithDefault(Level.All); - + // Reset the warning flag so diagnostics can fire during the next + // reconfiguration cycle. Without this, the "no appender" warning is + // silenced permanently after the first configuration. + EmittedNoAppenderWarning = false; + Shutdown(); // nested locks are OK foreach (Logger logger in GetCurrentLoggers().OfType()) @@ -775,4 +779,4 @@ public override string ToString() /// internal void AddProperty(PropertyEntry propertyEntry) => Properties[propertyEntry.Key.EnsureNotNull()] = propertyEntry.EnsureNotNull().Value; -} \ No newline at end of file +} From 908daacbe68b1b88935d5ae7b5066e1a0cdc5a15 Mon Sep 17 00:00:00 2001 From: N0tre3l Date: Wed, 8 Apr 2026 20:50:09 +0300 Subject: [PATCH 3/3] fix(XmlHierarchyConfigurator): collect appenders before swap to minimize null window Previously, ParseChildrenOfLoggerElement called RemoveAllAppenders() before adding new ones one-by-one, creating a window (equal to the full XML parse duration, typically 10-50 ms) during which the logger had no appenders and log events were silently dropped. This change resolves the race by: 1. Collecting all incoming appenders into a local List first. 2. Calling Logger.ReplaceAppenders() to perform the swap in a single writer lock, reducing the zero-appender window to microseconds. --- .../Hierarchy/XmlHierarchyConfigurator.cs | 59 ++++++++++++------- 1 file changed, 37 insertions(+), 22 deletions(-) diff --git a/src/log4net/Repository/Hierarchy/XmlHierarchyConfigurator.cs b/src/log4net/Repository/Hierarchy/XmlHierarchyConfigurator.cs index 0e2f45eb..50b758e8 100644 --- a/src/log4net/Repository/Hierarchy/XmlHierarchyConfigurator.cs +++ b/src/log4net/Repository/Hierarchy/XmlHierarchyConfigurator.cs @@ -389,43 +389,58 @@ protected void ParseRoot(XmlElement rootElement) /// /// Parse the child elements of a <logger> element. /// + /// + /// Appenders are collected from XML first, then applied atomically via + /// , minimizing the window during + /// which the logger has no appenders and silent log event loss can occur. + /// /// protected void ParseChildrenOfLoggerElement(XmlElement catElement, Logger log, bool isRoot) { - // Remove all existing appenders from log. They will be - // reconstructed if need be. - log.EnsureNotNull().RemoveAllAppenders(); + log.EnsureNotNull(); + catElement.EnsureNotNull(); + + // Phase 1: resolve all new appenders from XML *before* touching the + // live logger. This avoids the window where the logger has no appenders. + var newAppenders = new List(); - foreach (XmlNode currentNode in catElement.EnsureNotNull().ChildNodes) + foreach (XmlNode currentNode in catElement.ChildNodes) { - if (currentNode.NodeType == XmlNodeType.Element) + if (currentNode.NodeType != XmlNodeType.Element) { - XmlElement currentElement = (XmlElement)currentNode; + continue; + } - if (currentElement.LocalName == AppenderRefTag) - { - string refName = currentElement.GetAttribute(RefAttr); - if (FindAppenderByReference(currentElement) is IAppender appender) - { - LogLog.Debug(_declaringType, $"Adding appender named [{refName}] to logger [{log.Name}]."); - log.AddAppender(appender); - } - else - { - LogLog.Error(_declaringType, $"Appender named [{refName}] not found."); - } - } - else if (currentElement.LocalName is LevelTag or PriorityTag) + XmlElement currentElement = (XmlElement)currentNode; + + if (currentElement.LocalName == AppenderRefTag) + { + string refName = currentElement.GetAttribute(RefAttr); + if (FindAppenderByReference(currentElement) is IAppender appender) { - ParseLevel(currentElement, log, isRoot); + LogLog.Debug(_declaringType, $"Resolved appender [{refName}] for logger [{log.Name}]."); + newAppenders.Add(appender); } else { - SetParameter(currentElement, log); + LogLog.Error(_declaringType, $"Appender named [{refName}] not found."); } } + else if (currentElement.LocalName is LevelTag or PriorityTag) + { + ParseLevel(currentElement, log, isRoot); + } + else + { + SetParameter(currentElement, log); + } } + // Phase 2: atomic swap — replace all appenders in one writer lock so + // the logger is never in a zero-appender state for longer than it takes + // to acquire and release the lock (microseconds, not milliseconds). + log.ReplaceAppenders(newAppenders); + if (log is IOptionHandler optionHandler) { optionHandler.ActivateOptions();