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 the library's primary diagnostics sink. /// - By default references a (no-op). Callers /// may replace with any implementation to /// redirect logs (tests, hosts, custom integrations). /// - Logging is strictly best-effort: implementations of MUST avoid /// throwing from logging calls because logging must not change application control flow. /// - Assignment to is atomic for the reference type, but there is no /// internal synchronization for concurrent set/get operations. Replace the global logger /// early during application startup or from a single thread to avoid races. For fine-grained /// testability or per-component logging prefer injecting an instance. /// 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 => _current; set => _current = value ?? new NullLogger(); } /// /// Enable logging to a rolling file using log4net with configurable log level. /// /// Path to the log file (e.g., "logs/run.log"). /// /// Minimum log level threshold. Supported values (case-insensitive): /// "All" (default), "Debug", "Info", "Warn", "Error", "Fatal", "Off". /// Invalid values default to "All". /// /// /// - This method configures a programmatically and replaces /// the global logger with a that writes /// to the configured file. /// - Directory creation: the method ensures the parent 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 /// or locked-path issues. It closes the probe stream immediately; the appender re-opens the /// file when logging. /// - Locking model: is used by default to allow concurrent /// readers. If exclusive locking is required change the locking model accordingly. /// - Encoding: the appender is configured to use UTF-8 so log files support international text /// and emoji. /// - Log levels: the provided controls the root threshold for emitted /// messages. Passing "Off" disables emission; invalid values fall back to "All". /// - Exceptions: /// - Throws when is null or empty. /// - Throws when the file cannot be created or written (wraps the 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. /// /// /// /// // Log all levels (default) /// Log.SetFile("logs/app.log"); /// /// // Log only INFO and above /// Log.SetFile("logs/app.log", "Info"); /// /// // Log only errors /// Log.SetFile("logs/errors.log", "Error"); /// /// public static void SetFile(string logFilePath, string logLevel = "All") { 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 Encoding = System.Text.Encoding.UTF8 // Explicit UTF-8 for international characters and emoji }; 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); if (hierarchy != null) { var normalizedLevel = (logLevel ?? "All").Trim().ToUpperInvariant(); var levelName = normalizedLevel switch { "ALL" => "ALL", "DEBUG" => "DEBUG", "INFO" => "INFO", "WARN" => "WARN", "ERROR" => "ERROR", "FATAL" => "FATAL", "OFF" => "OFF", _ => "ALL" }; var level = hierarchy.LevelMap[levelName]; if (level != null) { hierarchy.Root.Level = level; } else { 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); } }