-
Notifications
You must be signed in to change notification settings - Fork 247
Add logging infrastructure #851
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
`Ical.Net`:
* Added package `Microsoft.Extensions.Logging.Abstractions` to `Ical.Net`
* Introduced `LoggingProvider` to support global logger factory configuration.
* `NullLoggerFactory` is the default factory
`Ical.Net.Tests`:
* Added ``TestLoggingProvider`
**Target:**
- The `TestLoggingProvider` is designed for use in testing scenarios within the iCal.NET project.
- It enables logging operations by configuring log targets, which can be either in-memory (`MemoryTarget`) or file-based (`FileTarget`).
- The target (where logs are stored) is set during construction, allowing flexibility for different test requirements.
**Usage:**
- Instantiate `TestLoggingProvider` in unit or integration tests to capture and inspect log output generated by iCal.NET components.
- You can specify logging options (like minimum/maximum log levels and logger name patterns) to control which log messages are recorded.
- Retrieve log entries via the `Logs` property, which returns the most recent logs up to a configurable maximum count (`Options.MaxLogsCount`).
- Useful for verifying that expected log messages are produced during test execution, or for debugging test failures by examining log output.
- Logging can be disabled in case no debugger is attached (`Options.DebugModeOnly`)
---
**Example Usage:**
```csharp
// Create a provider with in-memory logging
var provider = new TestLoggingProvider();
// Use provider.LoggerFactory to create loggers in your tests
var logger = provider.LoggerFactory.CreateLogger("TestLogger");
// Log something
logger.LogInformation("Test message");
// Retrieve logs for assertions
var logs = provider.Logs.ToList();
```
Codecov Report❌ Patch coverage is
@@ Coverage Diff @@
## main #851 +/- ##
=======================================
+ Coverage 67.7% 68.0% +0.3%
=======================================
Files 106 112 +6
Lines 4224 4267 +43
Branches 951 956 +5
=======================================
+ Hits 2860 2901 +41
Misses 1038 1038
- Partials 326 328 +2
🚀 New features to boost your workflow:
|
|
I'll try to take a look at the weekend. Just a short thought upfront: It is quite a benefit that Ical.Net has no dependencies except NodaTime. To avoid introducing a new dependency, maybe introduce custom types instead? And provide some simple way for creating an adapter to Microsoft.Logging? |
|
Indeed, this was a consideration. |
|
Maybe a simpler approach, where we don't need additional types, would suffice as well. E.g. Entity Framework allows specifying an LoggingProvider.SetLogger(Console.WriteLine)or, to use Microsoft Logging something similar to (probably not fully correct example) LoggingProvider.SetLogger(msg => LoggerFactory.CreateLogger("Ical.Net").LogInformation(msg));Somewhat less flexible but would avoid the additional dependency. |
|
Another aspect would be that the introduction of a new dependency would probably have to be considered a breaking change, so not something easily introduced in v5. |
As it has no influence on users code at all, and we add logging internally using Logging Delegates LoggingProvider.SetLogger((level, category, message, exception) =>
{
var msLogger = LoggerFactory.CreateLogger(category);
msLogger.Log(
MapLogLevel(level), // Map Ical LogLevel to Microsoft’s
new EventId(), // Optional
message,
exception,
(s, e) => s
);
});
// Helper to map Ical LogLevel to Microsoft.Extensions.Logging.LogLevel
private static Microsoft.Extensions.Logging.LogLevel MapLogLevel(LogLevel level) =>
level switch
{
LogLevel.Trace => Microsoft.Extensions.Logging.LogLevel.Trace,
LogLevel.Debug => Microsoft.Extensions.Logging.LogLevel.Debug,
LogLevel.Information => Microsoft.Extensions.Logging.LogLevel.Information,
LogLevel.Warning => Microsoft.Extensions.Logging.LogLevel.Warning,
LogLevel.Error => Microsoft.Extensions.Logging.LogLevel.Error,
LogLevel.Critical => Microsoft.Extensions.Logging.LogLevel.Critical,
_ => Microsoft.Extensions.Logging.LogLevel.None
};Maybe continue the discussion after you had the chance to look at the PR? |
| LoggerFactory = CreateFactory(config); | ||
|
|
||
| // Set the iCal.NET logging configuration | ||
| LoggingProvider.SetLoggerFactory(LoggerFactory); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Setting the global static logger factory as a side effect of instantiating a class introduces additional complexities. I.e. it prevents tests from running in parallel if multiple test cases instantiate the type. Maybe use something like ThreadLocal, i.e. some provider that is instantiated only once globally and that can be used to register individual listeners or sub-providers per thread.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh yes, right! AsyncLocal<ILoggerFactory> looks like the most up-to-date alternative.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I recommend not setting a global configuration (even if its AsyncLocal) in a constructor. I'd rather suggest to create the test logger in one step and set it as the current logger in a separate one. Something like this:
public void SomeTest() {
var testLoggingFactory = new TestLoggingFactory();
using var loggingReg = TestLoggingProvider.RegisterLoggingFactoryAsyncLocal(testLoggingFactory);
... test code
}The TestLoggingProvider would be something like this:
public static class TestLoggingProvider {
private class AsyncLocalLoggerFactory : ILoggerFactory {
private readonly AsyncLocal<ILoggerFactory> _loggerFactories = new();
....
}
private static readonly AsyncLocalLoggerFactory _asyncLocalLoggerFactory = new();
static TestLoggingProvider() {
LoggingProvider.SetLoggerFactory(_asyncLocalLoggerFactory);
}
static IDisposable RegisterLoggingFactoryAsyncLocal(ILoggerFactory loggerFactory) {
_loggerFactories.SetCurrent(loggerFactory);
return <some IDisposable that unregisters the current logger factory>
}
}
I had a first look. To what I see, most of the code is part of the test project. The main project contains the new
I agree that it doesn't impact the source code itself, but it could still break user projects if they have conflicting dependencies. I.e. user projects might be referenceing a different version of
Agree. The question is what the functionality is intended to provide. If it is intended to support during debugging, then to my experience the log level is not too important anyways. The user would reproduce some issue and check the whole log output to see whether there's something that points at the issue being debugged. If on the other hand it is configured in a production scenario, maybe on a web server, then it is probably a bigger, professional project. But in those cases the chance of having some legacy dependencies is high, so the additional effort of implementing a custom adapter might be neglectable compared to the challenges that come with conflicting dependencies. |
|
Mostly follow your arguments. So I added a logger implementation, as this is not too complex anyway. The |
## Ical.Net - Removed dependency `Microsoft.Extensions.Logging.Abstraction` - Introduced new logging classes and interfaces (`ILogger`, `ILoggerFactory`, `NullLogger`, `NullLoggerFactory` et al) in namespace `Ical.Net.Logging` - Refactored `LoggingProvider` to utilize `AsyncLocal<ILoggerFactory?>` for better async context management. - Added extension methods for simplified logging calls. ## Ical.Net.Tests - Added `Microsoft.Extensions.Logging` package - Enhanced `LoggingProviderUnitTests` with new tests for logger factory initialization and multi-threaded logging behavior. - Modified `TestLoggingProviderBase` to correctly use `NullTarget`. - Updated `LogLevel` mappings for compatibility with `Ical.Net.Logging` implementation.
fe16a17 to
b412cf9
Compare
minichma
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Very nice! In order to keep the code lean, I would probably have gone for simple delegates rather than individual ILoggerFactory and ILogger types, but that's probably a matter of taste. Maybe consider moving the AsyncLocal to the test project to reduce overall complexity and stay in line with what we have in TimeZoneResolver (where we also have some global configuration).
Ical.Net/Logging/LoggingProvider.cs
Outdated
| /// </summary> | ||
| internal static class LoggingProvider // Make public when logging is used in library classes | ||
| { | ||
| private static readonly AsyncLocal<ILoggerFactory?> _loggerFactory = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Whether AsyncLocal should be used is rather specific to the individual application. In Ical.Net we need it for testing, but in most applications the users might not want to configure it per async flow but rather globally. In TimeZoneResolver we have a similar situation and don't use AsyncLocal. I recommend to use the AsyncLocal only within the test project, i.e. set a specific ILoggerFactory there that wraps a AsyncLocal and lets individual test cases configure individual loggers per async flow.
In a future version it would be good to get rid of the static global altogether. Just like in TimeZoneResolver it would be good to introduce some context that holds configuration like the logger or the TZ provider. But that's not a topic for now.
| public void Dispose() | ||
| { | ||
| Dispose(true); | ||
| GC.SuppressFinalize(this); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why would we suppress the finalizer?
| LoggerFactory = CreateFactory(config); | ||
|
|
||
| // Set the iCal.NET logging configuration | ||
| LoggingProvider.SetLoggerFactory(LoggerFactory); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I recommend not setting a global configuration (even if its AsyncLocal) in a constructor. I'd rather suggest to create the test logger in one step and set it as the current logger in a separate one. Something like this:
public void SomeTest() {
var testLoggingFactory = new TestLoggingFactory();
using var loggingReg = TestLoggingProvider.RegisterLoggingFactoryAsyncLocal(testLoggingFactory);
... test code
}The TestLoggingProvider would be something like this:
public static class TestLoggingProvider {
private class AsyncLocalLoggerFactory : ILoggerFactory {
private readonly AsyncLocal<ILoggerFactory> _loggerFactories = new();
....
}
private static readonly AsyncLocalLoggerFactory _asyncLocalLoggerFactory = new();
static TestLoggingProvider() {
LoggingProvider.SetLoggerFactory(_asyncLocalLoggerFactory);
}
static IDisposable RegisterLoggingFactoryAsyncLocal(ILoggerFactory loggerFactory) {
_loggerFactories.SetCurrent(loggerFactory);
return <some IDisposable that unregisters the current logger factory>
}
}
Oh yes of course, didn't occur to me 😉 |
Updated project to remove NLog references and integrate Serilog. Added flexible logging options in the configuration.
|
After implementing As suggested Moving to |
9f0ee20 to
3fdb55c
Compare
|
|
Notice NLog supports both the When using NLog.Extensions.Logging then it will use the One can create an isolated LoggerFactory like this: var loggerFactory = new NLogLoggerFactory(new NLogProviderOptions(), new LogFactory());Or one can register NLog as Logging Provider with isolated var loggerFactory = LoggerFactory.Create(builder => builder.AddNLog(serviceProvider => new LogFactory())); |



Closes #838
Ical.Net:Added packageMicrosoft.Extensions.Logging.AbstractionstoIcal.NetILogger,ILoggerFactory,NullLogger,NullLoggerFactoryet al) in namespaceIcal.Net.LoggingLoggingProviderto support global logger factory configuration and create loggersNullLoggerFactoryis the default factoryIcal.Net.Tests.LoggingTestLoggingManagerMicrosoftLoggerAdapterandMicrosoftLoggerFactoryAdapterto wrapILoggerandILoggerFactoryforMicrosoft.Extensions.LoggingThe
TestLoggingManagersupplies two type of factories:Ical.Net.Logging.ILoggerFactory that can be used with as the factory forIcal.Net.Logging.LoggingProvider. It allows for handling log message from insideIcal.Net`.Microsoft.Extensions.Logging.ILoggerFactoryas a general usage logging factory in unit tests.The log messages of the loggers created by the factories go to the same configurable output (memory, file, console).
Under the hood the
Serilognuget package is used.Usage:
TestLoggingManagerin unit or integration tests to capture and inspect log output generated by iCal.NET components and components from inside the unit tests.Logsproperty, which returns the most recent logs up to a configurable maximum count (Options.MaxLogsCount).Options.DebugModeOnly) which istrueby default.Example Usage:
ical.net/Ical.Net.Tests/Logging.Tests/TestLoggingManagerTests.cs
Lines 62 to 99 in d659711