diff --git a/Ical.Net.Tests/Ical.Net.Tests.csproj b/Ical.Net.Tests/Ical.Net.Tests.csproj index 90065b7a4..964dc2ff5 100644 --- a/Ical.Net.Tests/Ical.Net.Tests.csproj +++ b/Ical.Net.Tests/Ical.Net.Tests.csproj @@ -11,8 +11,7 @@ $(NoWarn);CS8600;CS8601;CS8602;CS8603;CS8604;CS8618;CS8620;CS8714 - - + diff --git a/Ical.Net.Tests/Logging/Adapters/MicrosoftLoggerAdapter.cs b/Ical.Net.Tests/Logging/Adapters/MicrosoftLoggerAdapter.cs new file mode 100644 index 000000000..17184f7e9 --- /dev/null +++ b/Ical.Net.Tests/Logging/Adapters/MicrosoftLoggerAdapter.cs @@ -0,0 +1,38 @@ +// +// Copyright ical.net project maintainers and contributors. +// Licensed under the MIT license. +// +#nullable enable +using System; +using Microsoft.Extensions.Logging; + +namespace Ical.Net.Tests.Logging.Adapters; + +#pragma warning disable CA2254 // template should be a static expression + +internal class MicrosoftLoggerAdapter(ILogger msLogger) : Ical.Net.Logging.ILogger +{ + public void Log(Ical.Net.Logging.LogLevel level, string messageTemplate, params object[] args) + => msLogger.Log(ConvertLogLevel(level), messageTemplate, args); + + public void Log(Ical.Net.Logging.LogLevel level, Exception exception, string messageTemplate, params object[] args) + => msLogger.Log(ConvertLogLevel(level), exception, messageTemplate, args); + + public bool IsEnabled(Ical.Net.Logging.LogLevel level) => + msLogger.IsEnabled(ConvertLogLevel(level)); + + private static LogLevel ConvertLogLevel(Ical.Net.Logging.LogLevel level) + { + return level switch + { + Ical.Net.Logging.LogLevel.Trace => LogLevel.Trace, + Ical.Net.Logging.LogLevel.Debug => LogLevel.Debug, + Ical.Net.Logging.LogLevel.Information => LogLevel.Information, + Ical.Net.Logging.LogLevel.Warning => LogLevel.Warning, + Ical.Net.Logging.LogLevel.Error => LogLevel.Error, + Ical.Net.Logging.LogLevel.Critical => LogLevel.Critical, + _ => LogLevel.None + }; + } +} +#pragma warning restore CA2254 diff --git a/Ical.Net.Tests/Logging/Adapters/MicrosoftLoggerFactoryAdapter.cs b/Ical.Net.Tests/Logging/Adapters/MicrosoftLoggerFactoryAdapter.cs new file mode 100644 index 000000000..52c5430fc --- /dev/null +++ b/Ical.Net.Tests/Logging/Adapters/MicrosoftLoggerFactoryAdapter.cs @@ -0,0 +1,67 @@ +// +// Copyright ical.net project maintainers and contributors. +// Licensed under the MIT license. +// +#nullable enable +using System; + +namespace Ical.Net.Tests.Logging.Adapters; + +/// +/// Adapts an +/// to the interface. +/// +/// This class provides a bridge between the framework and the +/// abstraction. It allows the creation of +/// instances using the underlying +/// . +/// +internal class MicrosoftLoggerFactoryAdapter : Ical.Net.Logging.ILoggerFactory +{ + private readonly Microsoft.Extensions.Logging.ILoggerFactory _msLoggerFactory; + private bool _disposed; + + /// + /// Initializes a new instance of the class, using the specified + /// Microsoft.Extensions.Logging . + /// + /// The instance used to create loggers. + /// This parameter cannot be . + /// + public MicrosoftLoggerFactoryAdapter(Microsoft.Extensions.Logging.ILoggerFactory msLoggerFactory) + { + _msLoggerFactory = msLoggerFactory; + } + + /// + /// Creates a logger instance for the specified category name. + /// + /// The name of the category for the logger. + /// A instance configured to log messages for the specified category. + public Ical.Net.Logging.ILogger CreateLogger(string categoryName) + { + var msLogger = _msLoggerFactory.CreateLogger(categoryName); + return new MicrosoftLoggerAdapter(msLogger); + } + + protected virtual void Dispose(bool disposing) + { + if (_disposed) return; + + if (disposing) + _msLoggerFactory.Dispose(); + _disposed = true; + } + + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + ~MicrosoftLoggerFactoryAdapter() + { + Dispose(false); + } +} diff --git a/Ical.Net.Tests/Logging/LoggingProviderUnitTests.cs b/Ical.Net.Tests/Logging/LoggingProviderUnitTests.cs index 34bb9a3ff..5b0564aad 100644 --- a/Ical.Net.Tests/Logging/LoggingProviderUnitTests.cs +++ b/Ical.Net.Tests/Logging/LoggingProviderUnitTests.cs @@ -6,16 +6,49 @@ using System; using System.IO; using System.Linq; +using System.Threading; using Ical.Net.DataTypes; using Ical.Net.Logging; -using Microsoft.Extensions.Logging; +using Ical.Net.Logging.Internal; using NUnit.Framework; - namespace Ical.Net.Tests.Logging; +// LoggingProvider.SetLoggerFactory is static and gets set in each test, +// so we need to ensure that the tests are not run in parallel. +[Parallelizable(ParallelScope.None)] internal class LoggingProviderUnitTests { + [Test] + public void InitializingLoggerFactoryTwice_ShouldThrow() + { + using var logging1 = new TestLoggingProvider(new Options { DebugModeOnly = false }); + + using (Assert.EnterMultipleScope()) + { + Assert.That(LoggingProvider.FactoryIsSet, Is.True); + Assert.That(() => + { + using var logging2 = new TestLoggingProvider(new Options { DebugModeOnly = false }); + }, Throws.InvalidOperationException); + } + } + + [Test] + public void LoggerFactoryNotSet_ShouldFallBackToNullLoggerFactory() + { + using var logging1 = new TestLoggingProvider(new Options { DebugModeOnly = false }); + LoggingProvider.SetLoggerFactory(null, true); + var logger = LoggingProvider.CreateLogger(); + using (Assert.EnterMultipleScope()) + { + Assert.That(LoggingProvider.LoggerFactory, Is.InstanceOf()); + Assert.That(logger, Is.InstanceOf>()); + Assert.That(logger.IsEnabled(LogLevel.Information), Is.False); + Assert.That(() => LoggingProvider.LoggerFactory.Dispose(), Throws.Nothing); + } + } + [TestCase(true)] [TestCase(false)] public void SubmittedLogMessages_ShouldBeInTheLogs(bool useFileTarget) @@ -35,7 +68,7 @@ public void SubmittedLogMessages_ShouldBeInTheLogs(bool useFileTarget) var catLogger = LoggingProvider.CreateLogger(typeof(LoggingProviderUnitTests).FullName!); catLogger.LogInformation("This is an info message."); - genLogger.LogError(new ArgumentException(), "This is an error message."); + genLogger.LogError(new ArgumentException(),"This is an error message."); genLogger.LogWarning("This is a warning message with structured data. {Data}", new { Key1 = "Value1", Key2 = "Value2" }); var logs = logging.Logs.ToList(); @@ -89,6 +122,34 @@ public void OnlyLatestMaxLogsCount_ShouldBeReturned(bool useFileTarget) } } + [TestCase(true)] + [TestCase(false)] + public void DebugModeOnly_IsTrue_ShouldOnlyLogWhenDebugging(bool useFileTarget) + { + var logFile = useFileTarget + ? Path.ChangeExtension(Path.GetTempFileName(), ".log") + : null; + + using var logging = logFile is not null + ? new TestLoggingProvider(logFile) // Use file target for logging + : new TestLoggingProvider(); // Use in-memory target for logging + + var logger = LoggingProvider.CreateLogger(); + logger.LogInformation("##{Counter}##", 123); + + var logs = logging.Logs.ToList(); + + if (System.Diagnostics.Debugger.IsAttached) + { + Assert.That(logs, Has.Count.EqualTo(1)); + } + else + { + // If not in debug mode, no logs should be present + Assert.That(logs, Is.Empty); + } + } + [Test] public void MissingFileTargetToReadFrom_ShouldThrow() { @@ -126,8 +187,8 @@ public void AllLogLevels_CanBeUsedAsFilters(bool unrecognizedPattern) DebugModeOnly = false, Filters = allLevels.Select(l => new Filter { - MinLogLevel = l, - MaxLogLevel = l, + MinLogLevel = (Microsoft.Extensions.Logging.LogLevel) l, + MaxLogLevel = (Microsoft.Extensions.Logging.LogLevel) l, LoggerNamePattern = pattern }).ToList() }; @@ -138,31 +199,38 @@ public void AllLogLevels_CanBeUsedAsFilters(bool unrecognizedPattern) var logger = LoggingProvider.CreateLogger(); // Act: log one message for each level + var exception = new InvalidOperationException("Test exception"); foreach (var level in allLevels) switch (level) { case LogLevel.Trace: logger.LogTrace("Trace message"); + logger.LogTrace(exception, string.Empty); break; case LogLevel.Debug: logger.LogDebug("Debug message"); + logger.LogDebug(exception, string.Empty); break; case LogLevel.Information: logger.LogInformation("Info message"); + logger.LogInformation(exception, string.Empty); break; case LogLevel.Warning: logger.LogWarning("Warning message"); + logger.LogWarning(exception, string.Empty); break; case LogLevel.Error: logger.LogError("Error message"); + logger.LogError(exception, string.Empty); break; case LogLevel.Critical: logger.LogCritical("Critical message"); + logger.LogCritical(exception, string.Empty); break; } var logs = logging.Logs.ToList(); - Assert.That(logs, unrecognizedPattern ? Has.Count.EqualTo(6) : Has.Count.EqualTo(0)); + Assert.That(logs, unrecognizedPattern ? Has.Count.EqualTo(12) : Has.Count.EqualTo(0)); } [Test] @@ -175,8 +243,8 @@ public void UsingFilters_ShouldExcludeLogMessages() [ new Filter { - MinLogLevel = LogLevel.Error, - MaxLogLevel = LogLevel.Error, + MinLogLevel = (Microsoft.Extensions.Logging.LogLevel) LogLevel.Error, + MaxLogLevel = (Microsoft.Extensions.Logging.LogLevel) LogLevel.Error, LoggerNamePattern = $"{typeof(LoggingProviderUnitTests).FullName}" } ] @@ -217,6 +285,74 @@ public void LoggingForDebugModeOnly_ShouldLog() : Has.Count.EqualTo(0)); } + [Test] + public void ConcurrentLogging_ShouldLogAllMessagesCorrectly() + { + // Set up in-memory logging with (using 1 ILoggerFactory) + using var logging = new TestLoggingProvider(new Options { DebugModeOnly = false }); + + const int threadCount = 10; + const int logsPerThread = 20; + var runningThreads = 0; + var maxParallelThreads = 0; + + // Create and start multiple threads to log messages concurrently + + var threads = Enumerable.Range(0, threadCount) + .Select(t => new Thread(() => LogThread(t))) + .ToArray(); + + foreach (var thread in threads) + thread.Start(); + foreach (var thread in threads) + thread.Join(); + + var logs = logging.Logs.ToList(); + + using (Assert.EnterMultipleScope()) + { + Assert.That(logs, Has.Count.EqualTo(threadCount * logsPerThread)); + Assert.That(maxParallelThreads, Is.GreaterThan(1), "Threads did not run in parallel"); + for (var t = 0; t < threadCount; t++) + { + for (var i = 0; i < logsPerThread; i++) + { + var expected = $"Log {i:D3} - Thread {t:D2}"; + Assert.That(logs.Any(log => log.Contains(expected)), Is.True, $"Expected log message '{expected}' not found"); + } + } + } + + return; + + // Local functions + + void LogThread(int threadNo) + { + var current = Interlocked.Increment(ref runningThreads); + UpdateMaxParallelThreads(current); + + var logger = LoggingProvider.CreateLogger(); + for (var i = 0; i < logsPerThread; i++) + { + logger.LogInformation("Log {Log:D3} - Thread {Thread:D2}", i, threadNo); + } + Interlocked.Decrement(ref runningThreads); + } + + void UpdateMaxParallelThreads(int current) + { + do + { + var prevMax = maxParallelThreads; + if (current > prevMax) + { + Interlocked.CompareExchange(ref maxParallelThreads, current, prevMax); + } + } while (current > maxParallelThreads); + } + } + [Test] public void DemoForOccurrences() { diff --git a/Ical.Net.Tests/Logging/TestLoggingProviderBase.cs b/Ical.Net.Tests/Logging/TestLoggingProviderBase.cs index beb4770d8..fd6494b21 100644 --- a/Ical.Net.Tests/Logging/TestLoggingProviderBase.cs +++ b/Ical.Net.Tests/Logging/TestLoggingProviderBase.cs @@ -4,8 +4,8 @@ #nullable enable using System; using Ical.Net.Logging; +using Ical.Net.Tests.Logging.Adapters; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; using NLog.Config; using NLog.Extensions.Logging; using NLog.Targets; @@ -13,7 +13,7 @@ namespace Ical.Net.Tests.Logging; /// -/// Provides logging operations by configuring and providing a +/// Provides logging operations by configuring and providing a /// and a for log storage. /// /// @@ -40,18 +40,16 @@ protected TestLoggingProviderBase(Target target, Options? options) { Options = options ?? new Options(); + Target = target; if (Options.DebugModeOnly && !System.Diagnostics.Debugger.IsAttached) { - Target = new NullTarget(); - LoggerFactory = NullLoggerFactory.Instance; - return; + Target = new NullTarget("null"); } - - Target = target; - var config = Configure(target, Options); + var config = Configure(Target, Options); LoggerFactory = CreateFactory(config); // Set the iCal.NET logging configuration + // Don't force the logger factory to be set if it already exists! LoggingProvider.SetLoggerFactory(LoggerFactory); } @@ -61,20 +59,21 @@ protected TestLoggingProviderBase(Target target, Options? options) protected internal Target Target { get; } /// - /// Gets the instance used for creating logger instances. + /// Gets the instance used for creating logger instances. /// - public ILoggerFactory LoggerFactory { get; protected set; } + public Ical.Net.Logging.ILoggerFactory LoggerFactory { get; protected set; } - private static ILoggerFactory CreateFactory(LoggingConfiguration config) + private static Ical.Net.Logging.ILoggerFactory CreateFactory(LoggingConfiguration config) { // Create a LoggerFactory with NLog as the provider - var factory = Microsoft.Extensions.Logging.LoggerFactory.Create(builder => + var msLoggerFactory = Microsoft.Extensions.Logging.LoggerFactory.Create(builder => { builder.ClearProviders(); builder.AddNLog(config); }); - return factory; + // Wrap the Microsoft logger factory with the Ical adapter + return new MicrosoftLoggerFactoryAdapter(msLoggerFactory); } private static LoggingConfiguration Configure(Target target, Options options) @@ -109,7 +108,7 @@ private void Dispose(bool disposing) Target.Dispose(); LoggerFactory.Dispose(); NLog.LogManager.Configuration = null; - LoggingProvider.SetLoggerFactory(null); + LoggingProvider.SetLoggerFactory(null, true); } _isDisposed = true; @@ -125,17 +124,17 @@ public void Dispose() /// ~TestLoggingProviderBase() => Dispose(disposing: false); - private static NLog.LogLevel MapLogLevel(LogLevel logLevel) + private static NLog.LogLevel MapLogLevel(Microsoft.Extensions.Logging.LogLevel logLevel) { #pragma warning disable CS8509 // The switch expression does not handle all possible values of its input type (it is not exhaustive). return logLevel switch { - LogLevel.Trace => NLog.LogLevel.Trace, - LogLevel.Debug => NLog.LogLevel.Debug, - LogLevel.Information => NLog.LogLevel.Info, - LogLevel.Warning => NLog.LogLevel.Warn, - LogLevel.Error => NLog.LogLevel.Error, - LogLevel.Critical => NLog.LogLevel.Fatal, + Microsoft.Extensions.Logging.LogLevel.Trace => NLog.LogLevel.Trace, + Microsoft.Extensions.Logging.LogLevel.Debug => NLog.LogLevel.Debug, + Microsoft.Extensions.Logging.LogLevel.Information => NLog.LogLevel.Info, + Microsoft.Extensions.Logging.LogLevel.Warning => NLog.LogLevel.Warn, + Microsoft.Extensions.Logging.LogLevel.Error => NLog.LogLevel.Error, + Microsoft.Extensions.Logging.LogLevel.Critical => NLog.LogLevel.Fatal, }; #pragma warning restore CS8509 // The switch expression does not handle all possible values of its input type (it is not exhaustive). } diff --git a/Ical.Net.Tests/Logging/ToLogExtensions.cs b/Ical.Net.Tests/Logging/ToLogExtensions.cs index a5e1ccbd1..564134c02 100644 --- a/Ical.Net.Tests/Logging/ToLogExtensions.cs +++ b/Ical.Net.Tests/Logging/ToLogExtensions.cs @@ -10,6 +10,7 @@ using Ical.Net.DataTypes; namespace Ical.Net.Tests.Logging; + internal static class ToLogExtensions { public static string ToLog(this Exception? exception) diff --git a/Ical.Net/Ical.Net.csproj b/Ical.Net/Ical.Net.csproj index 4a4cf3f1a..2d4e5b641 100644 --- a/Ical.Net/Ical.Net.csproj +++ b/Ical.Net/Ical.Net.csproj @@ -7,7 +7,6 @@ latest - diff --git a/Ical.Net/Logging/ILogger.cs b/Ical.Net/Logging/ILogger.cs new file mode 100644 index 000000000..fcc5b31d4 --- /dev/null +++ b/Ical.Net/Logging/ILogger.cs @@ -0,0 +1,47 @@ +// +// Copyright ical.net project maintainers and contributors. +// Licensed under the MIT license. +// + +using System; + +namespace Ical.Net.Logging; + +/// +/// Represents a logging interface for writing log entries with varying severity levels. +/// +internal interface ILogger // Make public when logging is used in library classes +{ + /// + /// Writes a log entry. + /// + /// The severity level of the log entry. + /// The message template. Ex: "Processed {OrderId} in {ElapsedTime}ms". + /// The objects to format into the message template. + void Log(LogLevel level, string messageTemplate, params object[] args); + + /// + /// Writes a log entry that includes an exception. + /// + /// The severity level of the log entry. + /// The exception to log. + /// The message template. + /// The objects to format into the message template. + void Log(LogLevel level, Exception exception, string messageTemplate, params object[] args); + + /// + /// Determines whether logging is enabled for the specified log level. + /// + /// This method can be used to check if a particular log level is enabled before performing logging + /// operations. It is useful for optimizing performance by avoiding unnecessary log message generation when logging is + /// disabled for the given level. + /// + /// + /// The log level to check. Must be a valid value. + /// + /// + /// if logging is enabled for the specified + /// level; otherwise, . + /// + bool IsEnabled(LogLevel level); +} diff --git a/Ical.Net/Logging/ILoggerFactory.cs b/Ical.Net/Logging/ILoggerFactory.cs new file mode 100644 index 000000000..a5f13b2e0 --- /dev/null +++ b/Ical.Net/Logging/ILoggerFactory.cs @@ -0,0 +1,25 @@ +// +// Copyright ical.net project maintainers and contributors. +// Licensed under the MIT license. +// + +using System; + +namespace Ical.Net.Logging; + +/// +/// Represents a factory for creating instances. +/// +internal interface ILoggerFactory : IDisposable // Make public when logging is used in library classes +{ + /// + /// Creates a new logger instance for the specified category name. + /// + /// + /// The name of the category for the logger. + /// + /// + /// An instance configured for the specified category name. + /// + ILogger CreateLogger(string categoryName); +} diff --git a/Ical.Net/Logging/ILoggerT.cs b/Ical.Net/Logging/ILoggerT.cs new file mode 100644 index 000000000..f0444b812 --- /dev/null +++ b/Ical.Net/Logging/ILoggerT.cs @@ -0,0 +1,10 @@ +namespace Ical.Net.Logging +{ + /// + /// Represents a type-specific logger. + /// + /// The type for which the logger is created. + internal interface ILogger : ILogger // Make public when logging is used in library classes + { + } +} diff --git a/Ical.Net/Logging/Internal/Logger.cs b/Ical.Net/Logging/Internal/Logger.cs new file mode 100644 index 000000000..b977edbbf --- /dev/null +++ b/Ical.Net/Logging/Internal/Logger.cs @@ -0,0 +1,70 @@ +// +// Copyright ical.net project maintainers and contributors. +// Licensed under the MIT license. +// + +using System; + +namespace Ical.Net.Logging.Internal; + +/// +/// Provides logging functionality by delegating to an +/// underlying instance. +/// +internal class Logger : ILogger +{ + private readonly ILogger _logger; + + /// + /// Initializes a new instance of the class + /// using the specified logger factory and category name. + /// + /// + /// The factory used to create the logger instance. + /// + /// The category name used to identify the logger. + /// + public Logger(ILoggerFactory loggerFactory, string categoryName) + { + _logger = loggerFactory.CreateLogger(categoryName); + } + + /// + /// Determines whether logging is enabled for the specified . + /// + /// The log level to check. + /// + /// if logging is enabled for the specified ; + /// otherwise, . + /// + public bool IsEnabled(LogLevel level) => _logger.IsEnabled(level); + + /// + /// Logs a message at the specified log level using the provided message template and arguments. + /// + /// The severity level of the log message. Must be a valid . + /// The message template containing placeholders for arguments. + /// An array of objects to format into the placeholders in the message template. + public void Log(LogLevel level, string messageTemplate, params object[] args) + { + if (IsEnabled(level)) + { + _logger.Log(level, messageTemplate, args); + } + } + + /// + /// Logs a message at the specified log level using the provided message template and arguments. + /// + /// The severity level of the log message. Must be a valid . + /// The to add to the log. + /// The message template containing placeholders for arguments. + /// An array of objects to format into the placeholders in the message template. + public void Log(LogLevel level, Exception exception, string messageTemplate, params object[] args) + { + if (IsEnabled(level)) + { + _logger.Log(level, exception, messageTemplate, args); + } + } +} diff --git a/Ical.Net/Logging/Internal/LoggerT.cs b/Ical.Net/Logging/Internal/LoggerT.cs new file mode 100644 index 000000000..871c05795 --- /dev/null +++ b/Ical.Net/Logging/Internal/LoggerT.cs @@ -0,0 +1,20 @@ +// +// Copyright ical.net project maintainers and contributors. +// Licensed under the MIT license. +// + +namespace Ical.Net.Logging.Internal; + +/// +internal class Logger : Logger +{ + /// + /// Initializes a new instance of the class + /// for the specified type T, using the specified logger factory. + /// + public Logger(ILoggerFactory loggerFactory) + : base(loggerFactory, typeof(T).FullName ?? typeof(T).Name) + { + // This constructor is used to create a logger for a specific type T. + } +} diff --git a/Ical.Net/Logging/Internal/NullLogger.cs b/Ical.Net/Logging/Internal/NullLogger.cs new file mode 100644 index 000000000..03d70ba17 --- /dev/null +++ b/Ical.Net/Logging/Internal/NullLogger.cs @@ -0,0 +1,36 @@ +// +// Copyright ical.net project maintainers and contributors. +// Licensed under the MIT license. +// + +using System; +using System.Diagnostics.CodeAnalysis; + +namespace Ical.Net.Logging.Internal; + +/// +/// Represents a no-operation implementation of the interface. +/// +internal class NullLogger : ILogger +{ + public static readonly NullLogger Instance = new(); + + private NullLogger() { } + + /// + [ExcludeFromCodeCoverage] + public void Log(LogLevel level, string messageTemplate, params object[] args) + { + // will never get called because all LogLevels are disabled + } + + /// + [ExcludeFromCodeCoverage] + public void Log(LogLevel level, Exception exception, string messageTemplate, params object[] args) + { + // will never get called because all LogLevels are disabled + } + + /// + public bool IsEnabled(LogLevel level) => false; +} diff --git a/Ical.Net/Logging/Internal/NullLoggerFactory.cs b/Ical.Net/Logging/Internal/NullLoggerFactory.cs new file mode 100644 index 000000000..4606436c0 --- /dev/null +++ b/Ical.Net/Logging/Internal/NullLoggerFactory.cs @@ -0,0 +1,33 @@ +// +// Copyright ical.net project maintainers and contributors. +// Licensed under the MIT license. +// + +namespace Ical.Net.Logging.Internal; + +/// +/// Provides a no-operation implementation of that produces +/// instances. This factory is intended for scenarios where logging is not required or should be disabled. +/// +internal class NullLoggerFactory : ILoggerFactory +{ + /// + /// Provides a singleton instance of a that produces no-op loggers. + /// + public static readonly NullLoggerFactory Instance = new(); + + private NullLoggerFactory() { } + + /// + /// Creates a . + /// + /// The parameter is not used. + /// A + public ILogger CreateLogger(string categoryName) => NullLogger.Instance; + + /// + public void Dispose() + { + // no-op + } +} diff --git a/Ical.Net/Logging/LogLevel.cs b/Ical.Net/Logging/LogLevel.cs new file mode 100644 index 000000000..2aefa4c23 --- /dev/null +++ b/Ical.Net/Logging/LogLevel.cs @@ -0,0 +1,46 @@ +// +// Copyright ical.net project maintainers and contributors. +// Licensed under the MIT license. +// + +namespace Ical.Net.Logging; + +/// +/// Specifies the severity level of a log message. +/// +internal enum LogLevel // Make public when logging is used in library classes +{ + /// + /// Logs that contain the most detailed messages. These messages may contain sensitive application data. + /// These messages are disabled by default and should never be enabled in a production environment. + /// + Trace, + /// + /// Logs that are used for interactive investigation during development. These logs should primarily contain + /// information useful for debugging and have no long-term value. + /// + Debug, + /// + /// Logs that track the general flow of the application. These logs should have long-term value. + /// + Information, + /// + /// Logs that highlight an abnormal or unexpected event in the application flow, but do not otherwise cause the + /// application execution to stop. + /// + Warning, + /// + /// Logs that highlight when the current flow of execution is stopped due to a failure. These should indicate a + /// failure in the current activity, not an application-wide failure. + /// + Error, + /// + /// Logs that describe an unrecoverable application or system crash, or a catastrophic failure that requires + /// immediate attention. + /// + Critical, + /// + /// Not used for writing log messages. Specifies that a logging category should not write any messages. + /// + None +} diff --git a/Ical.Net/Logging/LoggerExtensions.cs b/Ical.Net/Logging/LoggerExtensions.cs new file mode 100644 index 000000000..a595daaf5 --- /dev/null +++ b/Ical.Net/Logging/LoggerExtensions.cs @@ -0,0 +1,42 @@ +using System; + +namespace Ical.Net.Logging; + +internal static class LoggerExtensions // Make public when logging is used in library classes +{ + public static void LogTrace(this ILogger logger, string messageTemplate, params object[] args) + => logger.Log(LogLevel.Trace, messageTemplate, args); + + public static void LogTrace(this ILogger logger, Exception exception, string messageTemplate, params object[] args) + => logger.Log(LogLevel.Trace, exception, messageTemplate, args); + + public static void LogDebug(this ILogger logger, string messageTemplate, params object[] args) + => logger.Log(LogLevel.Debug, messageTemplate, args); + + public static void LogDebug(this ILogger logger, Exception exception, string messageTemplate, params object[] args) + => logger.Log(LogLevel.Debug, exception, messageTemplate, args); + + public static void LogInformation(this ILogger logger, string messageTemplate, params object[] args) + => logger.Log(LogLevel.Information, messageTemplate, args); + + public static void LogInformation(this ILogger logger, Exception exception, string messageTemplate, params object[] args) + => logger.Log(LogLevel.Information, exception, messageTemplate, args); + + public static void LogWarning(this ILogger logger, string messageTemplate, params object[] args) + => logger.Log(LogLevel.Warning, messageTemplate, args); + + public static void LogWarning(this ILogger logger, Exception exception, string messageTemplate, params object[] args) + => logger.Log(LogLevel.Warning, exception, messageTemplate, args); + + public static void LogError(this ILogger logger, string messageTemplate, params object[] args) + => logger.Log(LogLevel.Error, messageTemplate, args); + + public static void LogError(this ILogger logger, Exception exception, string messageTemplate, params object[] args) + => logger.Log(LogLevel.Error, exception, messageTemplate, args); + + public static void LogCritical(this ILogger logger, string messageTemplate, params object[] args) + => logger.Log(LogLevel.Critical, messageTemplate, args); + + public static void LogCritical(this ILogger logger, Exception exception, string messageTemplate, params object[] args) + => logger.Log(LogLevel.Critical, exception, messageTemplate, args); +} diff --git a/Ical.Net/Logging/LoggingProvider.cs b/Ical.Net/Logging/LoggingProvider.cs index bc880f8e7..ae0e84562 100644 --- a/Ical.Net/Logging/LoggingProvider.cs +++ b/Ical.Net/Logging/LoggingProvider.cs @@ -3,8 +3,9 @@ // Licensed under the MIT license. // -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; +using System; +using System.Threading; +using Ical.Net.Logging.Internal; namespace Ical.Net.Logging; @@ -15,34 +16,54 @@ namespace Ical.Net.Logging; /// internal static class LoggingProvider // Make public when logging is used in library classes { + private static readonly AsyncLocal _loggerFactory = + new() { Value = null }; + + /// + /// Returns if a implementation + /// has been set (using ). + /// + public static bool FactoryIsSet => _loggerFactory.Value is not null; + /// /// Sets the global used by Ical.Net for creating instances. - /// This should be called once at application startup by the consuming application. - /// If , will be used, resulting in no-op logging. + /// This should be called once at application startup by the consuming application or thread that initializes logging. + /// + /// Before calling this method, or setting a no-op logger factory will be used. + /// + /// Check to determine if a logger factory has already been set. /// /// The instance from the consuming application's DI container or direct creation. - public static void SetLoggerFactory(ILoggerFactory? loggerFactory) - => LoggerFactory = loggerFactory ?? NullLoggerFactory.Instance; + /// If , an existing will be overwritten; else the method will throw. + public static void SetLoggerFactory(ILoggerFactory? loggerFactory, bool force = false) + { + if (!force && _loggerFactory.Value != null) + throw new InvalidOperationException( + "LoggerFactory has already been set. To override, set force to true."); + _loggerFactory.Value = loggerFactory; + } + /// /// Gets an instance for the specified category name. /// /// The category name for the logger (typically the full type name). /// An instance. internal static ILogger CreateLogger(string categoryName) - => LoggerFactory.CreateLogger(categoryName); + => (_loggerFactory.Value ?? NullLoggerFactory.Instance).CreateLogger(categoryName); /// /// Gets an instance for the specified type. /// The category name will be the full name of the type T. /// /// The type for which to create the logger. - /// An instance. - internal static ILogger CreateLogger() - => new Logger(LoggerFactory); + /// An instance. + internal static ILogger CreateLogger() + => new Logger(_loggerFactory.Value ?? NullLoggerFactory.Instance); /// /// Gets the current instance of the logger factory for Ical.Net loggers. /// - internal static ILoggerFactory LoggerFactory { get; private set; } = NullLoggerFactory.Instance; + internal static ILoggerFactory LoggerFactory + => _loggerFactory.Value ?? NullLoggerFactory.Instance; }