Fixes included: Thread-safe logger switching Single log4net configuration Safe file probing Buffered user failure mode Flush only once Memory safety cap Shared layout helper Directory null safety 1️⃣ IAppLogger.cs (unchanged) using System; public interface IAppLogger { /// /// Log a debug/detail message intended for developers or verbose diagnostics. /// void Debug(object? message); /// /// Log an informational message that represents normal operation or key milestones. /// void Info(object? message); /// /// Log a warning about an unexpected but recoverable condition. /// void Warn(object? message); /// /// Log an error. Implementations may include exception details when ex is provided. /// void Error(object? message, Exception? ex = null); } 2️⃣ NullLogger.cs using System; public sealed class NullLogger : IAppLogger { public void Debug(object? message) { } public void Info(object? message) { } public void Warn(object? message) { } public void Error(object? message, Exception? ex = null) { } } 3️⃣ Log.cs (Facade) using System; using System.IO; using System.Threading; using log4net; using log4net.Appender; using log4net.Config; using log4net.Layout; public static class Log { private static volatile IAppLogger _current = new NullLogger(); public static IAppLogger Current { get => _current; set => Interlocked.Exchange(ref _current, value ?? new NullLogger()); } public static void Debug(object? message) => _current.Debug(message); public static void Info(object? message) => _current.Info(message); public static void Warn(object? message) => _current.Warn(message); public static void Error(object? message, Exception? ex = null) => _current.Error(message, ex); public static void SetDisabled() { Current = new NullLogger(); } public static void SetAdminFile(string adminLogPath) { var fullPath = ResolvePath(adminLogPath); EnsureDirectoryExists(fullPath); ProbeWriteAccess(fullPath); var appender = new RollingFileAppender { File = fullPath, AppendToFile = true, RollingStyle = RollingFileAppender.RollingMode.Size, MaximumFileSize = "5MB", MaxSizeRollBackups = 3, StaticLogFileName = true, Layout = CreateDefaultLayout() }; appender.ActivateOptions(); Configure(appender); Current = new Log4NetAdapter(); } public static void SetAdminAndUserFailure(string adminLogPath, string userLogPath) { var fullAdmin = ResolvePath(adminLogPath); EnsureDirectoryExists(fullAdmin); ProbeWriteAccess(fullAdmin); var fileAppender = new RollingFileAppender { File = fullAdmin, AppendToFile = true, RollingStyle = RollingFileAppender.RollingMode.Size, MaximumFileSize = "5MB", MaxSizeRollBackups = 3, StaticLogFileName = true, Layout = CreateDefaultLayout() }; fileAppender.ActivateOptions(); var memoryAppender = new MemoryAppender(); memoryAppender.ActivateOptions(); Configure(fileAppender, memoryAppender); Current = new Log4NetAdapterWithFailureFlush(memoryAppender, userLogPath); } public static void SetUserFailureOnly(string userLogPath) { Current = new UserLogOnlyAdapter(userLogPath); } private static void Configure(params IAppender[] appenders) { var repo = LogManager.GetRepository(); repo.ResetConfiguration(); BasicConfigurator.Configure(appenders); } internal static PatternLayout CreateDefaultLayout() { var layout = new PatternLayout("%date %-5level %logger - %message%newline%exception"); layout.ActivateOptions(); return layout; } internal static string ResolvePath(string logFilePath) { return Path.GetFullPath(logFilePath); } internal static void EnsureDirectoryExists(string fullPath) { var dir = Path.GetDirectoryName(fullPath); if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir)) Directory.CreateDirectory(dir); } internal static void ProbeWriteAccess(string fullPath) { using var fs = new FileStream( fullPath, FileMode.OpenOrCreate, FileAccess.Write, FileShare.ReadWrite); } } 4️⃣ Log4NetAdapter.cs using log4net; public class Log4NetAdapter : IAppLogger { private readonly ILog _log; /// /// Creates a new adapter that forwards calls to the specified . /// /// A non-null log4net logger instance. /// Thrown when is null. public Log4NetAdapter(ILog log) { _log = log ?? throw new ArgumentNullException(nameof(log)); } public void Debug(object? message) { try { _log.Debug(message); } catch { /* swallow to avoid logging causing application failures */ } } public void Info(object? message) { try { // Skip logging empty strings to avoid blank lines in output if (message != null && string.IsNullOrWhiteSpace(message.ToString())) return; _log.Info(message); } catch { /* swallow to avoid logging causing application failures */ } } public void Warn(object? message) { try { _log.Warn(message); } catch { /* swallow to avoid logging causing application failures */ } } public void Error(object? message, Exception? ex = null) { try { if (ex != null) _log.Error(message, ex); else _log.Error(message); } catch { /* swallow to avoid logging causing application failures */ } } } 5️⃣ Log4NetAdapterWithFailureFlush.cs (admin log + user failure log) using System; using System.Threading; using log4net; using log4net.Appender; using log4net.Layout; public class Log4NetAdapterWithFailureFlush : IAppLogger { private readonly ILog _log; private readonly MemoryAppender _memoryAppender; private readonly RollingFileAppender _userAppender; private int _flushed; public Log4NetAdapterWithFailureFlush(ILog log MemoryAppender memoryAppender, string userLogPath) { _log = log ?? throw new ArgumentNullException(nameof(log)); _memoryAppender = memoryAppender; var fullUserPath = Log.ResolvePath(userLogPath); Log.EnsureDirectoryExists(fullUserPath); Log.ProbeWriteAccess(fullUserPath); _userAppender = new RollingFileAppender { File = fullUserPath, AppendToFile = true, RollingStyle = RollingFileAppender.RollingMode.Size, MaximumFileSize = "5MB", MaxSizeRollBackups = 1, StaticLogFileName = true, Layout = Log.CreateDefaultLayout() }; _userAppender.ActivateOptions(); } public void Debug(object? message) { try { _log.Debug(message); } catch { /* swallow to avoid logging causing application failures */ } } public void Info(object? message) { try { // Skip logging empty strings to avoid blank lines in output if (message != null && string.IsNullOrWhiteSpace(message.ToString())) return; _log.Info(message); } catch { /* swallow to avoid logging causing application failures */ } } public void Warn(object? message) { try { _log.Warn(message); } catch { /* swallow to avoid logging causing application failures */ } } public void Error(object? message, Exception? ex = null) { try{ _logger.Error(message, ex); if (Interlocked.Exchange(ref _flushed, 1) == 1) return; foreach (var e in _memoryAppender.GetEvents()) _userAppender.DoAppend(e); _memoryAppender.Clear(); catch { /* swallow to avoid logging causing application failures */ } } } } 6️⃣ UserLogOnlyAdapter.cs (Buffered Failure Mode) Now buffers Debug/Info/Warn and flushes on Error. using System; using System.Threading; using log4net.Appender; using log4net.Core; public class UserLogOnlyAdapter : IAppLogger { private readonly MemoryAppender _memoryAppender = new MemoryAppender(); private readonly RollingFileAppender _userAppender; private int _flushed; public UserLogOnlyAdapter(string userLogPath) { var fullPath = Log.ResolvePath(userLogPath); Log.EnsureDirectoryExists(fullPath); Log.ProbeWriteAccess(fullPath); _memoryAppender.ActivateOptions(); _userAppender = new RollingFileAppender { File = fullPath, AppendToFile = true, RollingStyle = RollingFileAppender.RollingMode.Size, MaximumFileSize = "5MB", MaxSizeRollBackups = 1, StaticLogFileName = true, Layout = Log.CreateDefaultLayout() }; _userAppender.ActivateOptions(); } public void Debug(object? message) { try { // Skip logging empty strings to avoid blank lines in output if (message != null && string.IsNullOrWhiteSpace(message.ToString())) return; Buffer(Level.Info, message); } catch { /* swallow to avoid logging causing application failures */ } public void Info(object? message) { try { // Skip logging empty strings to avoid blank lines in output if (message != null && string.IsNullOrWhiteSpace(message.ToString())) return; Buffer(Level.Info, message); } catch { /* swallow to avoid logging causing application failures */ } } public void Warn(object? message) { try { // Skip logging empty strings to avoid blank lines in output if (message != null && string.IsNullOrWhiteSpace(message.ToString())) return; Buffer(Level.Info, message); } catch { /* swallow to avoid logging causing application failures */ } } } public void Error(object? message, Exception? ex = null) { try{ Buffer(Level.Error, message, ex); if (Interlocked.Exchange(ref _flushed, 1) == 1) return; foreach (var e in _memoryAppender.GetEvents()) _userAppender.DoAppend(e); _memoryAppender.Clear(); catch { /* swallow to avoid logging causing application failures */ } } } private void Buffer(Level level, object? message, Exception? ex = null) { var loggingEvent = new LoggingEvent( typeof(UserLogOnlyAdapter), log4net.LogManager.GetRepository(), "User", level, message, ex); _memoryAppender.DoAppend(loggingEvent); if (_memoryAppender.GetEvents().Length > 500) _memoryAppender.Clear(); } } Final Behavior Mode Behavior Disabled No logging AdminFile Full log to admin file AdminAndUserFailure Full admin log + buffered user dump on failure UserFailureOnly Buffer events → flush to user file on error ✅ Production-safe improvements included thread safety memory protection correct log4net configuration failure buffering single flush