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