Skip to content
This repository has been archived by the owner on Dec 13, 2018. It is now read-only.

Logging in DI #626

Merged
merged 20 commits into from
Jun 2, 2017
Merged

Logging in DI #626

merged 20 commits into from
Jun 2, 2017

Conversation

pakrym
Copy link
Contributor

@pakrym pakrym commented May 24, 2017

@pakrym
Copy link
Contributor Author

pakrym commented May 24, 2017

Idea is to move ILoggerProviders to DI and use existing options infrastructure to manage logger configuration/filtering.

/cc @cwe1ss @nblumhardt @khellang

@davidfowl
Copy link
Member

We should add an AddProvider extension method off ILoggerbuilder

@pakrym
Copy link
Contributor Author

pakrym commented May 24, 2017

@davidfowl done

@khellang
Copy link

khellang commented May 24, 2017

I've mentioned it before, but I'll say it again; I'm really not a fan of changing the return type (to ILoggerBuilder). It breaks chainability and forces me away from using expressions.

IMO, It's a much better alternative to use Action<ILoggerBuilder>. The pattern is already scattered around the different repos, but it's not really consistent, and I seem to remember some pushback on the pattern way back. What are the guidelines on this now?

Other than that, I think this looks pretty awesome! :shipit:

@pakrym
Copy link
Contributor Author

pakrym commented May 24, 2017

@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 (IMvcBuilder) and return a builder.

@khellang
Copy link

khellang commented May 24, 2017

I think it makes sense to take delegate if it's execution is deferred.

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?

So in case of logging we decided to go the same way MVC did (IMvcBuilder) and return a builder.

Yeah, I realize there's a mixture of I*Something*Builder and the Action<I*Something*Builder> throughout the APIs. AFAIK, Entity Framework does something similar. That's why I asked

What are the guidelines on this now?

There's even new APIs in this PR, like Action<ConsoleLoggerOptions>. That's not deferred, is it?

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 😄

Copy link
Contributor

@nblumhardt nblumhardt left a 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,
Copy link
Contributor

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)
Copy link
Contributor

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....

Copy link
Contributor Author

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
Copy link
Contributor

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?

Copy link
Contributor Author

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);
Copy link
Contributor

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.

Copy link
Contributor Author

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)
Copy link
Contributor

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.

@nblumhardt
Copy link
Contributor

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.

@epignosisx
Copy link

@nblumhardt I agree with you about

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.

I've been bitten by the app failing on startup, but how would you go about resolving this issue I brought up #620

@glennc
Copy link
Member

glennc commented May 25, 2017

@nblumhardt

  • as @epignosisx raised there are people who are already injecting services and using them with the logging system, I think we already have some other providers that need services today. Which we completely broke when we went the other way in preview1. Ideally we would provide a way to use services in DI without resorting to something like a service locator.
  • having providers in DI means that it is easy for various components to add providers and the composition and configuration model is well known and understood. This can be done either in program main or in ConfigureServices because it is understood how all the services compose and operate. Having as much of the stack as possible be configured and registered the same way is beneficial, as is the removal of special top-level APIs on WebHostBuilder.
  • we will log using EventSource before the container is built, though we discussed other things as well that are possible like a single log sink that can be set on the builder or something of that sort. You are right that this is a fundamental limitation of the design. Today we log very little before the container is built, in fact I don't think we log anything. But we would use EventSource as we added logging. For logging that an app dev wants to use they can also presumably configure Serilog statically in program main and log to Serilog directly there, registering the provider for the rest of their app. Right? Just thinking "out loud" on that last bit.

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.

@pakrym pakrym changed the title [WIP] Logging in DI Logging in DI May 25, 2017
@nblumhardt
Copy link
Contributor

nblumhardt commented May 25, 2017

Thanks for the follow-ups @epignosisx @glennc.

they can also presumably configure Serilog statically in program main and log to Serilog directly there, registering the provider for the rest of their app. Right?

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 IHttpContextAccessor in some well-known location when it becomes available, and have the enricher look for it there). Not ideal, but might be preferable to tangling up DI and logging inextricably.

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)

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);
Copy link
Member

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>
Copy link
Member

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?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I need suggestions :)

Copy link
Member

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);
Copy link
Member

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))
Copy link
Member

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?

Copy link
Contributor Author

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));
Copy link
Member

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))
Copy link
Member

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.
Copy link
Member

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();
Copy link
Member

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
Copy link
Member

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));
}
Copy link
Member

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.")]
Copy link
Member

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?

Copy link
Contributor Author

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.

Copy link
Member

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.

Copy link
Contributor Author

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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pubternal?

Copy link
Contributor Author

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.

Copy link
Member

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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you forget to

@pakrym
Copy link
Contributor Author

pakrym commented May 30, 2017

🆙📅 I'd like to get it merged tomorrow

@pakrym pakrym force-pushed the pakrym/logging-spike branch from b7b3e58 to 027db2b Compare May 30, 2017 23:58
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)));
Copy link
Contributor

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)
Copy link
Contributor

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)" />
Copy link
Contributor

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?!?

Copy link
Member

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?

Copy link
Contributor

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?!

Copy link
Contributor Author

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>
Copy link
Contributor

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)

pakrym added 3 commits May 31, 2017 09:24
Provider aliases support
Use ProviderAliasAttribute
Fix ProviderAliasAttribute usage rules
@cwe1ss
Copy link
Contributor

cwe1ss commented May 31, 2017

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 EventSource/ETW. It sounds great in theory but it's yet another thing to deal with and gets quite complex on Azure (ARM template -> Azure Diagnostics agent -> Azure Tables -> OMS). The new EventFlow library might help here but I haven't looked into it much yet.

}
if (output != null)
{
builder.AddXunit(output, LogLevel.Debug);
Copy link
Member

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?

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.