-
Notifications
You must be signed in to change notification settings - Fork 246
Conversation
Idea is to move |
We should add an AddProvider extension method off ILoggerbuilder |
@davidfowl done |
I've mentioned it before, but I'll say it again; I'm really not a fan of changing the return type (to IMO, It's a much better alternative to use Other than that, I think this looks pretty awesome! |
@khellang I think it makes sense to take delegate if it's execution is deferred. In case of logging we would have to immediately execute it because the only thing it does is mutating service collection. So in case of logging we decided to go the same way MVC did ( |
It also makes sense for scoping, i.e. var provider = new ServiceCollection()
.AddLogging(logging => { /* providers can only be added in here */ })
.BuildServiceProvider(); as opposed to var services = new ServiceCollection();
var loggingBuilder = services.AddLogging();
// providers can be added here...
var provider = services.BuildServiceProvider();
// what happens if you try to add a provider here?
Yeah, I realize there's a mixture of
There's even new APIs in this PR, like Anyway, it's pretty easy to wrap these kinds of APIs in extension methods to achieve what I want, like public static class ServiceCollectionExtensions
{
public static IServiceCollection AddLogging(this IServiceCollection services, Action<ILoggingBuilder> configure)
{
configure(services.AddLogging());
return services;
}
} So it's not really a big deal 😄 |
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 added a few notes as I read through the changeset; guessing most of these are due to WIP, but recorded them in case anything is useful :-)
public static LoggerFactory AddTraceSource( | ||
this LoggerFactory factory, | ||
public static ILoggerBuilder AddTraceSource( | ||
this ILoggerBuilder collection, |
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.
This argument is called collection
in some, and builder
in other similar methods.
} | ||
} | ||
|
||
public string ExpandLoggerAlias(string name) |
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.
This doesn't seem like a scalable API; it'll also deter users from stepping beyond these basic providers because the natural guess will be wrong. I.e., if I'm a user looking at an existing configuration, and want to add a new one for, say, a file, I'll try "File"
and nothing will happen... It also seems a bit questionable to favor, say, Azure App Services, but not Seq, Splunk, Elasticsearch, CloudWatch....
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.
Yep, it's temporary, we would way to extend the list.
|
||
namespace Microsoft.Extensions.Logging | ||
{ | ||
public interface ILoggerBuilder |
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 introduce ILoggerBuilder
at all, here? If it's just a wrapper for IServiceCollection
, and all the provider extension methods do is dot through to Services
, isn't this unnecessary ceremony?
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.
We want to prevent all logging related methods appearing on IServiceCollection
. If we keep them on IServiceCollection
they would also need to be named in more verbose way (AddConsoleLogger, AddLoggingFilter, etc. )
@@ -0,0 +1,4 @@ | |||
namespace Microsoft.Extensions.Logging | |||
{ | |||
public delegate bool LogMessageFilter(string loggerType, string categoryName, LogLevel level); |
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.
This seems more like a LogCategoryFilter
or something like that - it doesn't touch the log message itself.
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'm thinking about removing it all together because most AddFilter
overloads are forced to use Action<*> as filter delegates.
{ | ||
return; | ||
} | ||
|
||
List<Exception> exceptions = null; | ||
foreach (var loggerInfo in _loggers) | ||
foreach (var loggerInfo in Loggers) |
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.
Race condition; Loggers
is public and might have been set to null between this and the check above.
I think it'd be good to state the goals of this PR a bit more clearly. Is there really much benefit building logging atop DI? (Apologies if I've overlooked an earlier discussion around this.) In most apps I've worked on, we've gone to great pains to configure logging at start-up and stand it up independently of anything as complex as DI. Configuration errors in happen all the time, and often they're environment specific, so getting log events out even if DI configuration crashes is pretty useful. |
@nblumhardt I agree with you about
I've been bitten by the app failing on startup, but how would you go about resolving this issue I brought up #620 |
There are also some questions about how you can use logging outside of ASP.NET Core, like with EF in a UWP application, that we need to consider. |
Thanks for the follow-ups @epignosisx @glennc.
This is a good point, and I think it works if you use Serilog. From the perspective of the library as a whole it still seems like there should be a way for developers to get start-up errors through their logging pipeline using only MEL. Having to go to a separate collection point does leaves the problem on the end-user developer's plate, it would seem. #620 is an interesting problem; there are ways around it by using static state (put the I wish I could add more useful ideas here - it's a challenging API to get right! :-) |
/// <param name="services">The <see cref="IServiceCollection" /> to add services to.</param> | ||
/// <param name="configure">The <see cref="ILoggerBuilder"/> configuration delegate.</param> | ||
/// <returns>The <see cref="IServiceCollection"/> so that additional calls can be chained.</returns> | ||
public static IServiceCollection AddLogging(this IServiceCollection services, Action<ILoggerBuilder> configure) |
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.
😍😍😍
{ | ||
if (WebAppContext.Default.IsRunningInAzureWebApp) | ||
{ | ||
// Only add the provider if we're in Azure WebApp. That cannot change once the apps started | ||
factory.AddProvider("AzureAppServices", new AzureAppServicesDiagnosticsLoggerProvider(WebAppContext.Default, settings)); | ||
builder.Services.AddSingleton(WebAppContext.Default); |
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.
Do we need to add this to DI if we pass a singleton instance of AzureAppServicesDiagnosticsLoggerProvider
with WebAppContext.Default
already in it?
|
||
namespace Microsoft.Extensions.Logging.Console | ||
{ | ||
public class ConfigurationConsoleLoggerConfigureOptions : ConfigureOptions<ConsoleLoggerOptions> |
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.
This name is a bit much?
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 need suggestions :)
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.
ConfigureConsole[Logger]Options?
{ | ||
_settings = new ConsoleLoggerSettings(); | ||
logger.Filter = GetFilter(logger.Name, _settings); |
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 think this can be removed, _settings
is only used for 1.X ConsoleLogger
methods
{ | ||
foreach (var section in configurationSection.AsEnumerable(true)) | ||
{ | ||
if (TryGetSwitch(section.Value, out var level)) |
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.
Where would the error propagate to for an invalid config value?
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.
Whoever tries to inject ILogger
first would get it.
|
||
public static ILoggerBuilder AddFilter(this ILoggerBuilder builder, Func<string, string, LogLevel, bool> filter) | ||
{ | ||
return AddRule(builder, new LoggerFilterRule(null, null, null, filter)); |
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 think adding named parameters would be useful here for readability. (Here = all of the new LoggerFilterRule(...)
calls)
|
||
// checks config and filters set on the LoggerFactory | ||
if (!_loggerFactory.IsEnabled(loggerInfo.ProviderNames, _categoryName, logLevel)) | ||
if (!loggerInfo.IsEnabled(logLevel)) |
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.
This function is supposed to check if at least 1 logger is enabled, and if 1 is found you can return true immediately. Currently you are checking if any are not enabled and returning false.
@@ -0,0 +1,17 @@ | |||
// Copyright (c) .NET Foundation. All rights reserved. |
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.
Should probably move this into an internal folder
} | ||
|
||
AddProvider(name, provider); | ||
_providerRegistrations = providers.Select(provider => new ProviderRegistration { Provider = provider}).ToList(); |
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.
nit: space after provider
// Filter rule selection: | ||
// 1. Select rules for current logger type, if there is none, select ones without logger type specified | ||
// 2. Select rules with longest matching categories | ||
// 3. If there no category |
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.
Something doesn't look right here.
public static ILoggerBuilder AddFilter(this ILoggerBuilder builder, Func<string, string, LogLevel, bool> filter) | ||
{ | ||
return AddRule(builder, new LoggerFilterRule(null, null, null, filter)); | ||
} |
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.
nit: newline
/// Adds an <see cref="ILoggerProvider"/> to the logging system. | ||
/// </summary> | ||
/// <param name="provider">The <see cref="ILoggerProvider"/>.</param> | ||
[Obsolete("This method is obsolete and will be removed in a future version. The recommended alternative is to call the Microsoft.Extensions.Logging.AddProvider() method on the Microsoft.Extensions.Logging.LoggerFactory instance.")] |
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.
Did we decide to un-obsolete everything?
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.
Yep, EF uses AddProvider and there is no time to fix it there.
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.
Obsolete doesn't mean it wont work, calling AddProvider should still be fine they just have to ignore the warning.
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.
Issues is there is no "right" way for them to do it right now, so showing obsolete warning is not entirely correct.
|
||
namespace Microsoft.Extensions.Logging | ||
{ | ||
internal class LoggerBuilder : ILoggerBuilder |
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.
Pubternal?
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.
How do we decide? Other .Logging stuff was internal.
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.
If we want to preserve the ability to new this up without an explicit service collection then it should be public and have a parameters less ctor that news up a service collection internally. But meh.
// Filter rule selection: | ||
// 1. Select rules for current logger type, if there is none, select ones without logger type specified | ||
// 2. Select rules with longest matching categories | ||
// 3. If there no category |
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.
Did you forget to
🆙📅 I'd like to get it merged tomorrow |
b7b3e58
to
027db2b
Compare
public static ILoggerBuilder AddConfiguration(this ILoggerBuilder builder, IConfiguration configuration) | ||
{ | ||
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IConfigureOptions<LoggerFilterOptions>>(new LoggerFilterConfigureOptions(configuration))); | ||
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IOptionsChangeTokenSource<LoggerFilterOptions>>(new ConfigurationChangeTokenSource<LoggerFilterOptions>(configuration))); |
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.
Does the "TryAddEnumerable" mean that calling AddConfiguration a second time with a different configuration source is a no-op? If so, there would be quite a mix of behaviors here (IIUC):
- AddConfiguration can only be called once and further calls are ignored
- AddProvider can be called multiple times - the same provider can even be added multiple times
- SetMinimumLevel can be called multiple times and overwrites previous values
Not sure if it's a valid scenario to call "AddConfiguration" with different configuration sources but would it be a problem if it were allowed? The IConfigureOptions system can handle this anyway, right?
/// <param name="services">The <see cref="IServiceCollection" /> to add services to.</param> | ||
/// <param name="configure">The <see cref="ILoggerBuilder"/> configuration delegate.</param> | ||
/// <returns>The <see cref="IServiceCollection"/> so that additional calls can be chained.</returns> | ||
public static IServiceCollection AddLogging(this IServiceCollection services, Action<ILoggerBuilder> configure) |
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.
Nitpick: Wouldn't ILoggingBuilder
be better since the "Logging" module is configured with it and not a single logger. (just a suggestion/question, probably really not important)
@@ -17,6 +17,8 @@ | |||
<ItemGroup> | |||
<PackageReference Include="Microsoft.Extensions.Configuration" Version="$(AspNetCoreVersion)" /> | |||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="$(AspNetCoreVersion)" /> | |||
<PackageReference Include="Microsoft.Extensions.Options" Version="$(AspNetCoreVersion)" /> | |||
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="$(AspNetCoreVersion)" /> |
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.
"M.E.O.ConfigurationExtensions" brings in some of the above as transitive dependencies. I think it's more declarative to have those here as well - but not sure how you handle these things?!?
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.
These dependencies do kinda suck. Should we have a logging.configuration package?
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 think the most important thing is for M.E.L.Abstractions
to be clean because that's what libraries will depend on. M.E.L
will be used by hosts and they have all of these dependencies anyway, so it's probably not necessary to split 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.
@davidfowl it should align with our IDefaultConfigureOptions
story
/// <summary> | ||
/// Adds a console logger named 'Console' to the factory. | ||
/// </summary> | ||
/// <param name="builder">The <see cref="LoggerFactory"/> to use.</param> |
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.
The <see cref="LoggerFactory"/> to use.
is no longer a good comment here, is it? (applies to all of the ILoggerBuilder based methods)
Provider aliases support
Use ProviderAliasAttribute Fix ProviderAliasAttribute usage rules
Regarding the general issue of logging crashes before/on DI: We also tried to setup the whole logging system in program.cs before the WebHost is built. However we ended up still needing the environment name, some configuration values etc - so we would have duplicated a lot of WebHostBuilder logic and gave up in the end. One thing that should still be possible though is to create your own mini DI-provider just for logging in Program.Main and use this one to catch startup exceptions (with CaptureStartupErrors=false). This one could then be environment-independent and log everything to e.g. Console/EventLog. I'm also not really a friend of |
} | ||
if (output != null) | ||
{ | ||
builder.AddXunit(output, LogLevel.Debug); |
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.
Should we add a provider name to the XunitLoggerProvider
?
#346