using log4net; using log4net.Appender; using log4net.Layout; using log4net.Repository; using System; using System.IO; using System.Reflection; namespace GitConverter.Lib.Logging { /// /// Static entry point for logging. /// Default is a no-op logger; call SetFile(...) to configure log4net-based file logging. /// /// /// - The static logger is used by the library to emit diagnostics. /// - By default points to a (no-op). Callers can replace /// with any implementation to redirect logs (tests, hosts). /// - Logging is best-effort: implementations of should avoid throwing from log calls /// because logging must not change application control flow. /// - Assignment to is atomic for the reference type, but code does not synchronize concurrent /// set/get operations. Replace the global logger early during startup or from a single thread to avoid races. /// 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). /// - Prefer setting this at application startup to avoid races; assignments are atomic but not synchronized. /// - Setting to null will implicitly replace the active logger with a to preserve /// no-op behavior. /// public static IAppLogger Current { get { return _current; } set { _current = value ?? new NullLogger(); } } /// /// Enable logging to a rolling file using log4net. /// Logs all levels (DEBUG, INFO, WARN, ERROR, FATAL). /// /// Path to the log file (e.g., "logs/run.log"). /// /// - This method configures a RollingFileAppender programmatically and replaces the global /// logger with a that writes to the configured file. /// - Directory creation: the method ensures the directory exists because log4net will create the file but not the directory. /// - Write probing: the method attempts to open the file for append to fail fast on permission/locked-path issues. /// It immediately closes the file; the actual appender will reopen it when logging. /// - Locking model: we select to allow concurrent readers. If your /// environment requires exclusive locking, change the locking model accordingly. /// - Exceptions: /// - Throws when is null/empty. /// - Throws on path or permission errors (wraps underlying exception). /// - Thread-safety: call from single-threaded startup; concurrent calls may race to replace repository configuration. /// - Tests: in unit tests prefer injecting a test logger (set directly) instead of configuring file logging. /// public static void SetFile(string logFilePath) { if (string.IsNullOrWhiteSpace(logFilePath)) throw new ArgumentException("logFilePath cannot be null or empty.", nameof(logFilePath)); // Resolve full path and ensure directory exists. log4net will create the file if not exists. var fullPath = Path.GetFullPath(logFilePath); var directory = Path.GetDirectoryName(fullPath); if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory)) { Directory.CreateDirectory(directory); } // Probe write access by opening the file for append. This will create the file if it doesn't exist. // We close immediately; log4net will open the file when logging. try { using (var fs = new FileStream(fullPath, FileMode.Append, FileAccess.Write, FileShare.Read)) { // no-op; just testing access } } catch (Exception ex) { throw new IOException($"Cannot write to log file path '{fullPath}'. Check permissions and that the path is valid.", ex); } 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() // better for concurrent reads }; roller.ActivateOptions(); // Select repository associated with the entry assembly if available; otherwise use executing assembly. var entry = Assembly.GetEntryAssembly(); var repoAssembly = (object)entry ?? (object)Assembly.GetExecutingAssembly(); ILoggerRepository repo = LogManager.GetRepository((Assembly)repoAssembly); // Reset configuration when using the hierarchy to avoid duplicate appenders across multiple SetFile calls. var hierarchy = repo as log4net.Repository.Hierarchy.Hierarchy; if (hierarchy != null) { hierarchy.ResetConfiguration(); } // Configure basic logger with the rolling appender log4net.Config.BasicConfigurator.Configure(repo, roller); // Always enable all levels on the root logger for maximum diagnostic coverage if (hierarchy != null) { hierarchy.Root.Level = hierarchy.LevelMap["ALL"]; hierarchy.Configured = true; } // Replace the current logger with a log4net-backed adapter var logger = LogManager.GetLogger(typeof(Log)); Current = new Log4NetAdapter(logger); } /// /// Enable logging using the existing log4net configuration. /// /// /// - This method does not change log4net appenders or layout; it simply sets to a /// that forwards library log calls to log4net. /// - Use when log4net is configured externally (XML config, host wiring) and you want the library to emit logs. /// - Does not throw for normal operation; if log4net is not configured the adapter will still forward calls but /// no output may be produced. /// - For unit tests prefer setting directly to a test logger. /// 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 does not modify log4net's global repository configuration (appenders remain registered). To fully /// reset log4net you would need to call into the repository (not performed here). /// - Useful in tests to suppress noisy logs or to temporarily disable file logging. /// 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); } } }