commit 02834f81e13055467e89576b54de4a38cb5cf650 Author: Ano-sys Date: Sun Sep 20 15:00:59 2026 +0200 Upload diff --git a/NETLogger.slnx b/NETLogger.slnx new file mode 100755 index 0000000..7c4bff7 --- /dev/null +++ b/NETLogger.slnx @@ -0,0 +1,3 @@ + + + diff --git a/NETLogger/LoggerControlService.cs b/NETLogger/LoggerControlService.cs new file mode 100755 index 0000000..205b28e --- /dev/null +++ b/NETLogger/LoggerControlService.cs @@ -0,0 +1,123 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace NETLogger.Services +{ + /// + /// A service that allows for the registration of loggers and controls their enabled state. + /// + public interface ILoggerControlService + { + bool IsEnabled { get; set; } + + IDisposable Register(string name, Action setEnabled); + } + + /// + /// A service that allows for the registration of loggers and controls their enabled state. + /// + public sealed class LoggerControlService : ILoggerControlService + { + private readonly object _syncRoot = new object(); + private readonly Dictionary _registrations = new Dictionary(); + + private bool _isEnabled = true; + + public bool IsEnabled + { + get { lock (_syncRoot) return _isEnabled; } + set + { + Action[] setters; + + lock (_syncRoot) + { + if (_isEnabled == value) return; + + _isEnabled = value; + setters = _registrations.Values.Select(x => x.SetEnabled).ToArray(); + } + + foreach (Action setter in setters) setter(value); + } + } + + /// + /// Registers a logger with the control service, allowing it to be enabled or disabled. + /// + /// + /// + /// + /// + /// + public IDisposable Register(string name, Action setEnabled) + { + if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("Logger name is required.", nameof(name)); + if (setEnabled == null) throw new ArgumentNullException(nameof(setEnabled)); + + var id = Guid.NewGuid(); + + lock (_syncRoot) + { + _registrations.Add(id, new Registration(name, setEnabled)); + setEnabled(_isEnabled); + } + + return new UnregisterAction(() => + { + lock (_syncRoot) _registrations.Remove(id); + }); + } + + private sealed class Registration + { + public Registration(string name, Action setEnabled) + { + Name = name; + SetEnabled = setEnabled; + } + + public string Name { get; } + public Action SetEnabled { get; } + } + + private sealed class UnregisterAction : IDisposable + { + private Action _unregister; + + public UnregisterAction(Action unregister) => _unregister = unregister; + + public void Dispose() + { + var unregister = _unregister; + _unregister = null; + unregister?.Invoke(); + } + } + + /// + /// Creates and registers a logger of type T with the provided ILoggerControlService. + /// The logger's enabled state will be controlled by the ILoggerControlService. + /// + /// + /// + /// + /// + /// + public static ILogger CreateAndRegisterNETLogger(ILoggerControlService loggerControlService, out IDisposable registration) + { + if (loggerControlService == null) throw new ArgumentNullException(nameof(loggerControlService)); + + var logger = new Logger(); + + registration = loggerControlService.Register(typeof(T).FullName, isEnabled => + { + if (isEnabled) logger.Enable(); + else logger.Disable(); + }); + + return logger; + } + } +} diff --git a/NETLogger/NETLogger.cs b/NETLogger/NETLogger.cs new file mode 100755 index 0000000..d3e86f5 --- /dev/null +++ b/NETLogger/NETLogger.cs @@ -0,0 +1,223 @@ +using System; +using System.IO; +using System.Runtime.InteropServices; +using System.Text; + +namespace NETLogger +{ + public enum LogLevel + { + Trace = 0, + Debug = 1, + Info = 2, + Warn = 3, + Error = 4, + Fatal = 5 + } + + public interface ILogger + { + void Log(LogLevel level, string message, Exception ex = null); + void Trace(string message); + void Debug(string message); + void Info(string message); + void Warn(string message, Exception ex = null); + void Error(string message, Exception ex = null); + void Fatal(string message, Exception ex = null); + } + + public interface ILogger : ILogger { } + + /// + /// Configuration for all Logger Instances. + /// This class is static and should be configured at application startup. + /// + public static class LogConfiguration + { + public static LogLevel MinimumLevel { get; set; } = LogLevel.Debug; + public static bool EnableDebugView { get; set; } = true; + public static bool EnableConsole { get; set; } = false; + + public static string LogFilePath { get; private set; } + + /// + /// Generates a new log file path based on the current timestamp in the specified directory. + /// + /// + /// + public static string GetNewLogFilePath(string directory) + { + if (string.IsNullOrEmpty(directory)) return null; + + var fileName = $"log_{DateTime.Now:yyyyMMdd_HHmmss_fff}.log"; + return Path.Combine(directory, fileName); + } + + /// + /// Sets the log file path. If the directory does not exist, it will be created. + /// + /// + public static void SetLogFile(string path) + { + LogFilePath = path; + if (!string.IsNullOrEmpty(path)) + { + var dir = Path.GetDirectoryName(path); + if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir)) Directory.CreateDirectory(dir); + } + } + + /// + /// Convenience method to get a new log file path and set it as the current log file + /// + /// + public static void GetAndSetNewLogFile(string directory) => SetLogFile(GetNewLogFilePath(directory)); + + /// + /// Creates a default logging path in the user's application data folder for the specified application name. + /// + /// + /// + /// + public static string CreateAndGetDefaultLoggingPath(string appName) + { + if (string.IsNullOrWhiteSpace(appName)) throw new ArgumentException("Application name is required.", nameof(appName)); + var dir = System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), appName, "Logs"); + if (!Directory.Exists(dir)) Directory.CreateDirectory(dir); + return dir; + } + + public static void RemoveLoggingPath() + { + if (!string.IsNullOrEmpty(LogFilePath)) + { + try { File.Delete(LogFilePath); } + catch { /* silently ignore */ } + LogFilePath = null; + } + } + } + + internal static class LogWriter + { + private static readonly object _fileLock = new object(); + private static readonly bool IsWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); + + // P/Invoke for Windows OutputDebugString (DbgView64) + [DllImport("kernel32.dll", CharSet = CharSet.Unicode, EntryPoint = "OutputDebugStringW")] + private static extern void OutputDebugString(string lpOutputString); + + internal static void Write(LogLevel level, string category, string message, Exception ex) + { + if (level < LogConfiguration.MinimumLevel) return; + + string timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff"); + string levelStr = level.ToString().ToUpperInvariant(); + string fullMessage = ex == null ? $"[{timestamp}] [{levelStr}] [{category}] {message}" : $"[{timestamp}] [{levelStr}] [{category}] {message} --> Exception: {ex}"; + + if (LogConfiguration.EnableDebugView && IsWindows) OutputDebugString(fullMessage + Environment.NewLine); + if (LogConfiguration.EnableConsole) Console.WriteLine(fullMessage); + if (!string.IsNullOrEmpty(LogConfiguration.LogFilePath)) + { + lock (_fileLock) + { + try { File.AppendAllText(LogConfiguration.LogFilePath, fullMessage + Environment.NewLine, Encoding.UTF8); } + catch + { + // logging should under no circumstances crash the application, so we silently ignore any exceptions here + } + } + } + } + } + + /// + /// Utility class for clearing log files in the log directory. + /// + internal class LogClearer + { + public static string DirectoryPath => Directory.GetParent(LogConfiguration.LogFilePath).FullName; + + /// + /// Clears all log files in the log directory. + /// + public static void ClearDirectory() + { + var dir = DirectoryPath; + if (string.IsNullOrEmpty(dir) || !Directory.Exists(dir)) return; + try + { + foreach (var file in Directory.GetFiles(dir)) + { + try { File.Delete(file); } + catch { /* silently ignore */ } + } + } + catch { /* silently ignore */ } + } + + /// + /// Clears old log files in the log directory, keeping only the specified number of most recent logs. + /// + /// + public static void ClearOldLogs(int logsToKeep = 30) + { + var dir = DirectoryPath; + if (string.IsNullOrEmpty(dir) || !Directory.Exists(dir)) return; + try + { + var logFiles = new DirectoryInfo(dir).GetFiles("log_*.log"); + Array.Sort(logFiles, (x, y) => y.CreationTime.CompareTo(x.CreationTime)); + + for (int i = logsToKeep; i < logFiles.Length; i++) + { + try { logFiles[i].Delete(); } + catch { /* silently ignore */ } + } + } + catch + { /* silently ignore */ } + } + } + + /// + /// typed generic logger class that implements ILogger interface + /// + public class Logger : ILogger + { + private readonly string _categoryName; + + public bool Enabled { get; private set; } = true; + + public Logger() => _categoryName = typeof(T).Name; + public Logger(string customCategory) => _categoryName = customCategory; + + public void Enable() => Enabled = true; + public void Disable() => Enabled = false; + + public void Log(LogLevel level, string message, Exception ex = null) + { + if (!Enabled) return; + LogWriter.Write(level, _categoryName, message, ex); + } + + public void Trace(string message) => Log(LogLevel.Trace, message); + public void Debug(string message) => Log(LogLevel.Debug, message); + public void Info(string message) => Log(LogLevel.Info, message); + public void Warn(string message, Exception ex = null) => Log(LogLevel.Warn, message, ex); + public void Error(string message, Exception ex = null) => Log(LogLevel.Error, message, ex); + public void Fatal(string message, Exception ex = null) => Log(LogLevel.Fatal, message, ex); + + public void ClearLogs() + { + LogClearer.ClearDirectory(); + Info("Clearing all log files in the log directory."); + } + + public void ClearOldLogs(int logsToKeep = 30) + { + LogClearer.ClearOldLogs(logsToKeep); + Info($"Clearing old log files in the log directory, keeping only the {logsToKeep} most recent logs."); + } + } +} diff --git a/NETLogger/NETLogger.csproj b/NETLogger/NETLogger.csproj new file mode 100755 index 0000000..743b32e --- /dev/null +++ b/NETLogger/NETLogger.csproj @@ -0,0 +1,12 @@ + + + + 1.1.0 + Timo Niemann + Timo Niemann + NETLogger is a logging library for .NET applications. With support for multiple logging providers and easy configuration, it simplifies the process of adding robust logging to your app. + netstandard2.0 + latest + + + diff --git a/README.md b/README.md new file mode 100755 index 0000000..5e5515f --- /dev/null +++ b/README.md @@ -0,0 +1,167 @@ +# NETLogger + +NETLogger is a small logging library for .NET applications targeting .NET Standard 2.0. + +## Installation + +Add `NETLogger/NETLogger.csproj` as a project reference or install the NuGet package generated during the build. + +## Configure the application + +Configure the global logger settings once during application startup. The following example stores log files in the current user's application data directory: + +```csharp +using NETLogger; + +LogConfiguration.MinimumLevel = LogLevel.Info; +LogConfiguration.EnableConsole = true; +LogConfiguration.EnableDebugView = true; // Windows only + +string logDirectory = LogConfiguration.CreateAndGetDefaultLoggingPath("MyApplication"); +LogConfiguration.GetAndSetNewLogFile(logDirectory); +``` + +Alternatively, use `LogConfiguration.SetLogFile(path)` to specify a complete file path. Without a configured file path, NETLogger writes only to the enabled console and Windows debug outputs. + +## Use a logger directly + +For an independent class, create a typed `Logger`. The type name is automatically included in each log entry as its category: + +```csharp +public class Service +{ + private readonly ILogger _logger = new Logger(); + + public void Run() + { + _logger.Info("Service started."); + + try + { + // Application code + } + catch (Exception exception) + { + _logger.Error("Service failed.", exception); + } + } +} +``` + +A directly created `Logger` can be disabled and enabled independently with `Disable()` and `Enable()`. `ClearOldLogs(logsToKeep)` removes old log files; without an argument, it keeps the 30 most recent files. `ClearLogs()` removes all log files from the configured directory. + +## Control multiple loggers centrally + +Use a shared `LoggerControlService` when multiple loggers need to be enabled or disabled together. An owning class creates the service and registers its logger: + +```csharp +using NETLogger; +using NETLogger.Services; + +public sealed class ApplicationController : IDisposable +{ + private readonly ILoggerControlService _loggerControlService; + private readonly ILogger _logger; + private readonly IDisposable _loggerRegistration; + + public ILoggerControlService LoggerControlService => _loggerControlService; + + public ApplicationController() : this(new LoggerControlService()) { } + + public ApplicationController(ILoggerControlService loggerControlService) + { + _loggerControlService = loggerControlService + ?? throw new ArgumentNullException(nameof(loggerControlService)); + + _logger = NETLogger.Services.LoggerControlService.CreateAndRegisterNETLogger( + _loggerControlService, + out _loggerRegistration); + + _logger.Info("Application controller initialized."); + } + + public void Dispose() + { + _loggerRegistration.Dispose(); + } +} +``` + +Dependent classes receive the same service and register their own typed logger. Dispose the `IDisposable` registration when the owning instance is disposed: + +```csharp +public sealed class BackgroundService : IDisposable +{ + private readonly ILogger _logger; + private readonly IDisposable _loggerRegistration; + + public BackgroundService(ILoggerControlService loggerControlService) + { + _logger = LoggerControlService.CreateAndRegisterNETLogger( + loggerControlService, + out _loggerRegistration); + + _logger.Info("Background service initialized."); + } + + public void Dispose() + { + _loggerRegistration.Dispose(); + } +} +``` + +All registered loggers can be toggled together: + +```csharp +loggerControlService.IsEnabled = false; // Disable all registered loggers +loggerControlService.IsEnabled = true; // Enable all registered loggers +``` + +## Initialize loggers for static members + +Classes with static logging members can use explicit initialization to participate in the shared `LoggerControlService`: + +```csharp +public sealed class ConfigurationCache +{ + private static ILogger _logger; + private static IDisposable _loggerRegistration; + + private ConfigurationCache() { } + + public static void InitializeLogger(ILoggerControlService loggerControlService) + { + if (loggerControlService == null) + throw new ArgumentNullException(nameof(loggerControlService)); + + _loggerRegistration?.Dispose(); + _logger = LoggerControlService.CreateAndRegisterNETLogger( + loggerControlService, + out _loggerRegistration); + } +} +``` + +The owning class initializes the logger with its shared service: + +```csharp +ConfigurationCache.InitializeLogger(_loggerControlService); +``` + +## Log levels and outputs + +The available levels are `Trace`, `Debug`, `Info`, `Warn`, `Error`, and `Fatal`. Messages below `LogConfiguration.MinimumLevel` are ignored. `Warn`, `Error`, and `Fatal` optionally accept an exception: + +```csharp +try +{ + // Application code +} +catch (Exception exception) +{ + _logger.Error("Operation failed.", exception); +} +``` + +NETLogger can write to the Windows debug output, the console, and a UTF-8 log file at the same time. \ No newline at end of file