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