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