This commit is contained in:
Ano-sys
2026-09-20 15:00:59 +02:00
commit 02834f81e1
5 changed files with 528 additions and 0 deletions
Executable
+3
View File
@@ -0,0 +1,3 @@
<Solution>
<Project Path="NETLogger/NETLogger.csproj" />
</Solution>
+123
View File
@@ -0,0 +1,123 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace NETLogger.Services
{
/// <summary>
/// A service that allows for the registration of loggers and controls their enabled state.
/// </summary>
public interface ILoggerControlService
{
bool IsEnabled { get; set; }
IDisposable Register(string name, Action<bool> setEnabled);
}
/// <summary>
/// A service that allows for the registration of loggers and controls their enabled state.
/// </summary>
public sealed class LoggerControlService : ILoggerControlService
{
private readonly object _syncRoot = new object();
private readonly Dictionary<Guid, Registration> _registrations = new Dictionary<Guid, Registration>();
private bool _isEnabled = true;
public bool IsEnabled
{
get { lock (_syncRoot) return _isEnabled; }
set
{
Action<bool>[] setters;
lock (_syncRoot)
{
if (_isEnabled == value) return;
_isEnabled = value;
setters = _registrations.Values.Select(x => x.SetEnabled).ToArray();
}
foreach (Action<bool> setter in setters) setter(value);
}
}
/// <summary>
/// Registers a logger with the control service, allowing it to be enabled or disabled.
/// </summary>
/// <param name="name"></param>
/// <param name="setEnabled"></param>
/// <returns></returns>
/// <exception cref="ArgumentException"></exception>
/// <exception cref="ArgumentNullException"></exception>
public IDisposable Register(string name, Action<bool> 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<bool> setEnabled)
{
Name = name;
SetEnabled = setEnabled;
}
public string Name { get; }
public Action<bool> 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();
}
}
/// <summary>
/// Creates and registers a logger of type T with the provided ILoggerControlService.
/// The logger's enabled state will be controlled by the ILoggerControlService.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="loggerControlService"></param>
/// <param name="registration"></param>
/// <returns></returns>
/// <exception cref="ArgumentNullException"></exception>
public static ILogger<T> CreateAndRegisterNETLogger<T>(ILoggerControlService loggerControlService, out IDisposable registration)
{
if (loggerControlService == null) throw new ArgumentNullException(nameof(loggerControlService));
var logger = new Logger<T>();
registration = loggerControlService.Register(typeof(T).FullName, isEnabled =>
{
if (isEnabled) logger.Enable();
else logger.Disable();
});
return logger;
}
}
}
+223
View File
@@ -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<T> : ILogger { }
/// <summary>
/// Configuration for all Logger Instances.
/// This class is static and should be configured at application startup.
/// </summary>
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; }
/// <summary>
/// Generates a new log file path based on the current timestamp in the specified directory.
/// </summary>
/// <param name="directory"></param>
/// <returns></returns>
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);
}
/// <summary>
/// Sets the log file path. If the directory does not exist, it will be created.
/// </summary>
/// <param name="path"></param>
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);
}
}
/// <summary>
/// Convenience method to get a new log file path and set it as the current log file
/// </summary>
/// <param name="directory"></param>
public static void GetAndSetNewLogFile(string directory) => SetLogFile(GetNewLogFilePath(directory));
/// <summary>
/// Creates a default logging path in the user's application data folder for the specified application name.
/// </summary>
/// <param name="appName"></param>
/// <returns></returns>
/// <exception cref="ArgumentException"></exception>
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
}
}
}
}
}
/// <summary>
/// Utility class for clearing log files in the log directory.
/// </summary>
internal class LogClearer
{
public static string DirectoryPath => Directory.GetParent(LogConfiguration.LogFilePath).FullName;
/// <summary>
/// Clears all log files in the log directory.
/// </summary>
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 */ }
}
/// <summary>
/// Clears old log files in the log directory, keeping only the specified number of most recent logs.
/// </summary>
/// <param name="logsToKeep"></param>
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 */ }
}
}
/// <summary>
/// typed generic logger class that implements ILogger<T> interface
/// </summary>
public class Logger<T> : ILogger<T>
{
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.");
}
}
}
+12
View File
@@ -0,0 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Version>1.1.0</Version>
<Authors>Timo Niemann</Authors>
<Company>Timo Niemann</Company>
<Description>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.</Description>
<TargetFramework>netstandard2.0</TargetFramework>
<LangVersion>latest</LangVersion>
</PropertyGroup>
</Project>
Executable
+167
View File
@@ -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<T>`. The type name is automatically included in each log entry as its category:
```csharp
public class Service
{
private readonly ILogger _logger = new Logger<Service>();
public void Run()
{
_logger.Info("Service started.");
try
{
// Application code
}
catch (Exception exception)
{
_logger.Error("Service failed.", exception);
}
}
}
```
A directly created `Logger<T>` 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<ApplicationController> _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<ApplicationController>(
_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<BackgroundService> _logger;
private readonly IDisposable _loggerRegistration;
public BackgroundService(ILoggerControlService loggerControlService)
{
_logger = LoggerControlService.CreateAndRegisterNETLogger<BackgroundService>(
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<ConfigurationCache> _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<ConfigurationCache>(
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.