using log4net;
using log4net.Appender;
using log4net.Layout;
using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
namespace GitConverter.Lib.Logging
{
///
/// Static entry point for library diagnostics.
///
///
///
/// The Log class provides a simple, test-friendly logging façade used throughout the
/// library. It exposes a single mutable logger reference (an
/// ) and a set of convenience forwarding methods (,
/// , , ) so callers do not need to
/// depend on a specific logging implementation.
///
///
/// Important behavior and guidance:
/// - Default behavior: when the process starts the active logger is a ,
/// which silently ignores log calls. Tests or hosts can replace with a
/// custom implementation (for example a TestLogger in unit tests or a file-based logger
/// in production) to capture diagnostics.
/// - Non-throwing contract: implementations of MUST avoid throwing
/// from log methods. Logging is strictly best-effort and must not change application control flow.
/// - Assignment semantics: the reference is assigned atomically but there is
/// no internal synchronization for concurrent set/get operations. Replace the global logger
/// early during startup or within a single thread. For per-component control prefer injecting
/// an instance into constructors instead of relying on the static.
/// - Testability: unit tests should set to a deterministic test logger rather
/// than configure file-based logging. Use to restore no-op behavior.
///
///
/// File-based logging
/// - Use to configure a log4net rolling file appender programmatically.
/// - The method performs defensive checks (path resolution, directory creation, permission probe) and
/// throws a clear if the path is not writable.
/// - When file logging is configured the global logger is set to a
/// that forwards calls into log4net.
///
///
/// When to call these helpers
/// - Hosts should configure logging once at startup (before library calls) and rely on the injected
/// for any per-component needs. Tests should replace
/// with a lightweight test logger and avoid call sites that depend on file system state.
///
///
public static class Log
{
private static IAppLogger _current = new NullLogger();
///
/// Gets or sets the active logger. If set to null, a no-op logger is used.
///
///
///
/// Consumers (tests, hosts) may set this property to supply a custom logger (for example a
/// test logger). Assigning null will restore a instance to preserve
/// no-op behavior.
///
///
/// Assignments are atomic for the reference; however concurrent read/write races are not
/// synchronized. Set the global logger early during initialization to avoid race conditions.
///
///
public static IAppLogger Current
{
get => _current;
set => _current = value ?? new NullLogger();
}
///
/// Configure file-based logging using a programmatically-created log4net rolling appender.
///
/// Target log file path. The method will create containing directories as needed.
/// Optional log level name (All/Debug/Info/Warn/Error/Fatal/Off). Defaults to "All".
///
///
/// This helper performs the following defensive steps before enabling file logging:
/// - Resolves and normalizes the supplied path.
/// - Ensures the target directory exists.
/// - Probes write access by opening the file for append and immediately closing it.
/// - Creates a rolling file appender with a UTF-8 pattern layout and configures the log4net
/// repository for the current entry assembly.
///
///
/// The method throws an for invalid arguments and an
/// when the path is not writable. Callers should catch and handle
/// these exceptions during host startup; tests should avoid calling this method and instead
/// set directly to a deterministic test logger.
///
///
public static void SetFile(string logFilePath, string logLevel = "All")
{
if (string.IsNullOrWhiteSpace(logFilePath))
throw new ArgumentException("logFilePath cannot be null or empty.", nameof(logFilePath));
var fullPath = ResolvePath(logFilePath);
EnsureDirectoryExists(fullPath);
ProbeWriteAccess(fullPath);
var appender = CreateRollingAppender(fullPath);
var level = ParseLogLevel(logLevel);
ConfigureRepository(appender, level);
Current = new Log4NetAdapter(LogManager.GetLogger(typeof(Log)));
}
private static string ResolvePath(string logFilePath)
{
return Path.GetFullPath(logFilePath);
}
private static void EnsureDirectoryExists(string fullPath)
{
var directory = Path.GetDirectoryName(fullPath);
if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
{
Directory.CreateDirectory(directory);
}
}
private static void ProbeWriteAccess(string fullPath)
{
try
{
using (var fs = new FileStream(fullPath, FileMode.Append, FileAccess.Write, FileShare.Read))
{
// just testing write access
}
}
catch (Exception ex)
{
throw new IOException($"Cannot write to log file path '{fullPath}'. Check permissions and that the path is valid.", ex);
}
}
private static RollingFileAppender CreateRollingAppender(string fullPath)
{
var layout = new PatternLayout
{
ConversionPattern = "%date %-5level %logger - %message%newline%exception"
};
layout.ActivateOptions();
var roller = new RollingFileAppender
{
AppendToFile = true,
File = fullPath,
Layout = layout,
MaxSizeRollBackups = 5,
MaximumFileSize = "10MB",
RollingStyle = RollingFileAppender.RollingMode.Size,
StaticLogFileName = true,
LockingModel = new FileAppender.MinimalLock(),
Encoding = System.Text.Encoding.UTF8
};
roller.ActivateOptions();
return roller;
}
private static log4net.Core.Level ParseLogLevel(string logLevel)
{
var levelMap = new Dictionary(StringComparer.OrdinalIgnoreCase)
{
["ALL"] = "ALL",
["DEBUG"] = "DEBUG",
["INFO"] = "INFO",
["WARN"] = "WARN",
["ERROR"] = "ERROR",
["FATAL"] = "FATAL",
["OFF"] = "OFF"
};
var normalizedLevel = (logLevel ?? "ALL").Trim().ToUpperInvariant();
var levelName = levelMap.ContainsKey(normalizedLevel) ? levelMap[normalizedLevel] : "ALL";
var entry = Assembly.GetEntryAssembly();
var repoAssembly = (Assembly)(entry ?? Assembly.GetExecutingAssembly());
var repo = LogManager.GetRepository(repoAssembly);
var hierarchy = repo as log4net.Repository.Hierarchy.Hierarchy;
return hierarchy?.LevelMap[levelName] ?? hierarchy?.LevelMap["ALL"];
}
private static void ConfigureRepository(RollingFileAppender appender, log4net.Core.Level level)
{
var entry = Assembly.GetEntryAssembly();
var repoAssembly = (Assembly)(entry ?? Assembly.GetExecutingAssembly());
var repo = LogManager.GetRepository(repoAssembly);
var hierarchy = repo as log4net.Repository.Hierarchy.Hierarchy;
if (hierarchy != null)
{
hierarchy.ResetConfiguration();
}
log4net.Config.BasicConfigurator.Configure(repo, appender);
if (hierarchy != null)
{
hierarchy.Root.Level = level;
hierarchy.Configured = true;
}
}
///
/// Enable logging using the existing log4net configuration.
///
///
///
/// Sets to a that forwards all calls to the
/// configured log4net repository. This is useful when the host configures log4net externally (for
/// example via XML configuration or host wiring) and the library should piggy-back on the host's
/// logging setup.
///
///
/// For unit tests prefer setting directly to a test logger to keep tests
/// deterministic and avoid file/IO side-effects.
///
///
public static void Enable()
{
var logger = LogManager.GetLogger(typeof(Log));
Current = new Log4NetAdapter(logger);
}
///
/// Disable logging and revert to a no-op logger.
///
///
///
/// Sets to so subsequent library log calls are no-ops.
/// This is useful in tests to suppress noisy logs or to temporarily disable file logging.
///
///
/// This method does not modify the log4net repository configuration; appenders remain registered. To fully
/// reset log4net one would need to interact with the repository API directly (not performed here).
///
///
public static void Disable()
{
Current = new NullLogger();
}
///
/// Log a debug message via the active logger.
///
///
/// Forwarding method to avoid callers referencing directly. Implementations of
/// should be non-throwing and lightweight.
///
public static void Debug(string message) => _current.Debug(message);
///
/// Log an informational message via the active logger.
///
///
/// Intended for high-level lifecycle and milestone messages.
///
public static void Info(string message) => _current.Info(message);
///
/// Log a warning via the active logger.
///
///
/// Warnings indicate recoverable/unexpected conditions that merit attention.
///
public static void Warn(string message) => _current.Warn(message);
///
/// Log an error via the active logger, optionally with an exception.
///
///
/// Error logging should be used for failures that affect operation. Avoid passing large exception
/// objects across process boundaries; prefer serializing minimal diagnostic info when logs are
/// collected by external systems.
///
public static void Error(string message, Exception ex = null) => _current.Error(message, ex);
}
}