Implement ISupportExternalScope#116
Conversation
We don't target that version of .NET Core anymore, so this is unreachable code.
|
@huorswords This is pretty much ready so feel free to check out the code already, it is a draft because there are tiny things I wanna think about left:
|
|
Thank you very much by your amazing contribution. I'll try to answer the questions...
I have not tested at all, but yes, I think so, I feel comfortable with your proposal. After reading the post you included as documentation in code, I think that could be very interesting to manage it as you have defined, specially when the scenario involves a lot of concurrent threads writing all to the Log4NetProvider. All actions that we can do to improve memory usage, are welcome.
In this case, everything we can do to let the library consumers adapt the code to fulfil their needs, the better. So I think that it is a reasonable approach, always considering that the default behavior should be kept in place.
My original idea (probably wrong) when I have decided to set the
In this case, and in order to avoid big breaking change, I think that the better option is 2: to change What are your thoughts on that?
I have considered it in the past, but at last I didn't found a good reason to include it yet. For me it is an extra. Do you think that could be important or maybe it could be considered just a configuration bonus? Again, THANK YOU by your effort. This is a high level PR. |
7e0e943 to
7bfe9c5
Compare
Allocating is the old way with a class, non allocation the readonly struct. Result is that it uses less allocations and therefore does less Gen 0 garbadge collections and is pretty much the same in speed. BenchmarkDotNet=v0.13.1, OS=Windows 10.0.22000
Intel Core i7-8559U CPU 2.70GHz (Coffee Lake), 1 CPU, 8 logical and 4 physical cores
.NET SDK=6.0.101
[Host] : .NET Core 3.1.22 (CoreCLR 4.700.21.56803, CoreFX 4.700.21.57101), X64 RyuJIT
.NET 5.0 : .NET 5.0.13 (5.0.1321.56516), X64 RyuJIT
.NET 6.0 : .NET 6.0.1 (6.0.121.56705), X64 RyuJIT
.NET Core 3.1 : .NET Core 3.1.22 (CoreCLR 4.700.21.56803, CoreFX 4.700.21.57101), X64 RyuJIT
.NET Framework 4.6.1 : .NET Framework 4.8 (4.8.4420.0), X64 RyuJIT
.NET Framework 4.6.2 : .NET Framework 4.8 (4.8.4420.0), X64 RyuJIT
.NET Framework 4.7 : .NET Framework 4.8 (4.8.4420.0), X64 RyuJIT
.NET Framework 4.7.1 : .NET Framework 4.8 (4.8.4420.0), X64 RyuJIT
.NET Framework 4.7.2 : .NET Framework 4.8 (4.8.4420.0), X64 RyuJIT
.NET Framework 4.8 : .NET Framework 4.8 (4.8.4420.0), X64 RyuJIT
|
I made it virtual.
I saw that all the classes in the other subfolders already have a different namespace starting with
I saw the alias for the default loggers used with filter rules for specific providers or something similar, but it seems to be possible to use the full type name so it's probably not a problem and there is no real need to add it [0]. I also made another tiny breaking change: If the logging scope is provided as |
7bfe9c5 to
7b1d757
Compare
|
@huorswords I think i have everything ready now. But before you merge you should read my last comment, because there is another tiny breaking change, that you should be aware of/approve. |
Supporting the ExternalScopeProvider grants performance benefits and
allows to access scopes that are by default added to all loggers (like
Activity.Current data).
A Microsoft.Extensions.Logging.ILoggerProvider can signal to the logging
infrastructure, that it supports external scopes by implementing the
interface ISupportExternalScope. Afterwards the infrastructure will
provide the logging provider with an IExternalScopeProvider
implementation, which can be used to discover the currently active
scopes.
If the logger implements this interface it will be handled differently
by the infrastructure. Previously it received the scopes somewhat like
this:
1. A call to ILogger<A>.BeginScope("Test") would result in a call to the
global logger [see 0] of <A> in the logging infrastructure.
2. This global logger holds all scope loggers registered with the
logging infrastructure and calls BeginScope("Test") on each one.
3. The loggers save the scope in their own AsyncLocal<?>.
This pattern requires every logger to carry their own AsyncLocal and is
therefore suboptimal for performance if many loggers are registered. So
with ISupportExternalScope the infrastructure handels it differently:
1. A call to ILogger<A>.BeginScope("Test") results in a call to the
global logger of <A> in the logging infrastructure.
2. The global logger adds the scope to Async of the
IExternalScopeProvider.
3. All connected loggers received the IExternalScopeProvider on
creation, so they can query for the scopes on every log message with
the following method:
IExternalScopeProvider.ForEachScope<T>(Action<object, T>, T)
This pattern requires only one AsyncLocal at the ExternalScopeProvider,
meaning additional performance. Additionally the external scope provider
is used to inject certain default scopes into the Loggers. Starting with
.NET 5 the default scope provider automatically adds the data of the
current Activity to the scope of all log messages [1][2].
[0]: https://github.com/dotnet/runtime/blob/57bfe474518ab5b7cfe6bf7424a79ce3af9d6657/src/libraries/Microsoft.Extensions.Logging/src/Logger.cs
[1]: dotnet/runtime#34305
[2]: https://github.com/dotnet/runtime/blob/b8a8c7a4c94c02d18c0224bd94108ec5887d77f2/src/libraries/Microsoft.Extensions.Logging/src/LoggerFactoryScopeProvider.cs#L35
7b1d757 to
f082461
Compare
|
Perfect. I will put an additional side note on Release notes in order to let the consumers know that the conversion of numeric values now is managed through InvariantCulture. |
|
Great job @JustusGreiberORGADATA! |
Fixes: #113
Implement ISupportExternalScope for the provider. This will cause the logging infrastructure to manage the scope externally and provide it to the logger via IExternalScopeProvider. This has 2 benefits:
The Pull Request is done in 3 steps:
1. Unify the test project
The test project was duplicated for .NET 5 and .NET 6 targeting. This pretty much meant writing every test twice if you wanted to add any functionality. Instead we can multi target a single test project to all target frameworks. This makes it easier to add new frameworks and allowed to easily expand the tested frameworks to the following values, giving a full test coverage to all target frameworks:
(Some test can't be run against every framework so they are taken out with the preprocessor. Also because not that many people likely have that many .NET Framework Versions installed one should maybe look to instead do those test on Github Actions or something like that in the future.)
2. Remove old .NET Core 1.1 exclusive code
Although the project only targets .NET Standard 2.0 and up there was still exclusive code for 1.1 so I removed that.
3. Switch to ISupportExternalScope
This is a breaking change, because external scope can't be used with the scope factories anymore. To still allow a consumer of this library to modify the scope data, the scope provider is passed to the configured ILog4NetLoggingEventFactory. The default Log4NetLoggingEventFactory has been modified to replicate the behaviour of the standard scope factories (I hope atleast there weren't a lot of tests sadly). Because this is a breaking change anyway, I also made MessageCandidate a readonly struct instead, that is passed by reference. This should allow for less allocations (no need to create a class on the heap, instead the values can be just pushed to the stack) and therefore be more resource friendly (still need to do a measurement on that one to be sure :)).
See:
datalust/seq-extensions-logging#36
dotnet/runtime#31372
dotnet/aspnetcore#22376
https://docs.microsoft.com/en-us/dotnet/core/diagnostics/distributed-tracing-instrumentation-walkthroughs