Skip to content
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

Allow UseLogging even when there's no ILoggerFactory #5687

Closed
wants to merge 2 commits into from

Conversation

stephentoub
Copy link
Member

@stephentoub stephentoub commented Nov 22, 2024

@SteveSandersonMS, opinions on this? I'm torn.

Microsoft Reviewers: Open in CodeFlow

@SteveSandersonMS
Copy link
Member

SteveSandersonMS commented Nov 26, 2024

Can you specify what scenario this is for? Why would someone need to no-op instead of fixing it (by adding logging services or removing UseLogging)?

To me it seems strange to no-op when the application has explicitly tried to enable logging. In general I'd expect a "required service missing" exception in these cases otherwise the application developer will struggle to understand why UseLogging isn't doing anything. For example in ASP.NET Core if you call UseAuthorization but the required DI services are missing, there's an error (rather than simply not enabling authorization).

I'm guessing some scenario prompted you to implement this so maybe there is a need for it, but it would be good to clarify the reasoning.

My best guess is it's for cases when ChatClientBuilder is used as an internal implementation detail in a library and the code author doesn't control whether or not logging is enabled in the app-wide DI container. If that's the case then I think it's pretty reasonable for the library author to call UseLogging conditionally based on the result of GetService<ILoggerFactory>() is not null. Or for the library to accept some other flag/config to control whether to enable logging, since even if you do have an ILoggerFactory, it doesn't necessarily mean you want this library to write to it.

@stephentoub
Copy link
Member Author

Any kind of component/library that's instantiating a client. In general any such component should be enabling logging if a logger factory is in DI, which means they're all effectively going to need to do the same thing: call AddLogging but only if DI contains a logger factory. So why force them to all do:

ChatClientBuilder builder = innerClient
    .WhateverA()
    .WhateverB();

if (services.GetService<ILoggerFactory>() is {} loggerFactory)
{
    builder.AddLogging(loggerFactory);
}

IChatClient client = builder.Build(services);

rather than:

IChatClient client = innerClient
    .WhateverA()
    .WhateverB()
    .AddLogging()
    .Build(services);

e.g. https://github.com/microsoft/semantic-kernel/blob/370c89a836145fbee6c239179f8d22471b71339f/dotnet/src/Connectors/Connectors.AzureAIInference/Services/AzureAIInferenceChatCompletionService.cs#L43-L53

On top of that, the logging infrastructure makes the notion of a nop-logger first class, with NullLoggerFactory.Instance being in the abstractions lib, so it's not clear to me why GetService returning null vs a null logger should result in AddLogging throwing an exception vs nop'ing. Many uses across the ecosystem end up doing the equivalent of GetService<ILoggerFactory>() ?? NullLoggerFactory.Instance.

We also have AddOpenTelemetry and AddFunctionInvocation, both of which enable logging of their pieces optionally based on whether a logger factory is available; they don't throw an exception if a logger isn't there, they just don't enable that functionality.

Effectively, AddLogging feels like it should really be AddLoggingIfEnabled, and we've just chosen a shorter name.

That's my rationale for why to do it.

The counter-argument would be "someone is asking for logging and there's no logging support available, so it's better to throw to tell them they're holding it wrong".

It really comes down to what semantics we want here.

@SteveSandersonMS
Copy link
Member

We also have AddOpenTelemetry and AddFunctionInvocation, both of which enable logging of their pieces optionally

That's true. We have a lot of prior art of framework pieces (in ASP.NET Core and Blazor at least) doing logging or secondary tasks conditionally based on what DI services are registered. However I'm not sure there's prior art when it's the primary or only task, as if is here.

Logging is an interesting case because unlike most other middleware, it has no behavioral impact so it would actually be safe to silently ignore UseLogging if the infrastructure is absent (unlike ASP.NET Core's UseAuthorization or MEAI's UseDistributedCache). Our conventions for other middleware are somewhat less applicable here. Plus in practice, most apps do have an ILoggerFactory because of our default application builders.

So altogether I'm coming round towards being persuaded that falling back on NullLoggerFactory.Instance would be reasonable.

@stephentoub
Copy link
Member Author

Ok, thanks. I'll go ahead with this then.

@dotnet-comment-bot
Copy link
Collaborator

🎉 Good job! The coverage increased 🎉
Update MinCodeCoverage in the project files.

Project Expected Actual
Microsoft.Extensions.Diagnostics.Probes 70 76
Microsoft.Extensions.AI.Ollama 0 80
Microsoft.Extensions.AI.AzureAIInference 0 77
Microsoft.Extensions.AI.Abstractions 0 83
Microsoft.Extensions.AI 0 84
Microsoft.Extensions.AI.OpenAI 0 66
Microsoft.Extensions.Caching.Hybrid 75 77

Full code coverage report: https://dev.azure.com/dnceng-public/public/_build/results?buildId=880030&view=codecoverage-tab

@eiriktsarpalis
Copy link
Member

I tend to agree with @SteveSandersonMS's initial comment. While I get that it unblocks certain usage patterns in intermediate libraries, falling back to the null logger in the context of an application's composition root is almost certainly an indication of misconfiguration being silently ignored. Would exposing an additional bool throwIfNotFound = true parameter (or corresponding overload) be a reasonable compromise?

@stephentoub
Copy link
Member Author

I thought of that, or of renaming the method AddLoggingIfAvailable, or of having two differently named methods, but I keep coming back to the idea that:
a) it's strange to me that we'd fail if null was returned but not if a null logger was returned, and logging burns the concept of a null logger being perfectly valid into the public surface area with NullLoggerFactory.Instance and NullLogger.Instance.
b) it doesn't feel like the place of the chat client builder or embedding generation builder to blow up if no logger is provided. It really feels like this is asking to add logging if there's a logger configured, and we've just shortened the name to "add logging".
c) there's optional logging performed as part of AddOpenTelemetry and AddFunctionInvocation, and they rightfully do not blow up if no logger is available. It's strange then that it's non optional for AddLogging, which also adds logging just like the others, just logging different things.
d) it'll be common to then special-case AddLogging. If someone really wants it to blow up if not registered, we can invert who needs to do work to achieve that, and have someone that wants it to blow up call GetRequiredService, rather than having someone who wants it to be optional be forced to call GetService and conditionally call AddLogging for it. Having a throwIfNotFound doesn't really add much value, then, since that's already achievable just by doing AddLogging(services.GetRequiredService<ILoggerFactory>()).

If we really don't like the idea of AddLogging behaving as if it were AddLoggingIfAvailable, then I'd still want to add the optimization to AddLogging that checks whether the provided logger factory is NullLoggerFactory.Instance, and then a consumer that wants that optional behavior can do AddLogging(services.GetService<ILoggerFactory>() ?? NullLoggerFactory.Instance). But given all of the above, I'm still inclined to sayy AddLogging should be implicitly conditional on whether a logger factory was registered. In the 99% case of applications, there will be a logger factory registered, and if there isn't, that's going to be discovered right quick via other means. As such, it behooves us to optimize for the case of components that want to log if logging is available but not blow up if it isn't.

That's my thinking at least.

@eiriktsarpalis
Copy link
Member

and if there isn't, that's going to be discovered right quick via other means.

I'm not too sure about that. The presence of logs in an application isn't something you can easily test for in an automated fashion, so this makes it easier for logs in prod to be lost. I don't object to this behaviour being made available, but I just think it should either use an explicit name or have an opt in flag.

@stephentoub
Copy link
Member Author

I don't think logging associated with this subsystem should implicitly be the canary for the whole application. I think this method really is AddLoggingIfAvailable just with a simpler name, just like AddOpenTelemetry is really AddLoggingIfAvailableAndCreateActivityAndMetricsIfEnabled. If someone wants to validate that there is a logger and pass that in, it's just passing GetRequiredService to the factory parameter.

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

Successfully merging this pull request may close these issues.

4 participants