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); } }