opentelemetry-dotnet: NotSupportedException is thrown during ConfigureBuilder
Bug Report
Packages:
<PackageReference Include="OpenTelemetry.Exporter.Console" Version="1.4.0-beta.1" />
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.3.1" />
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.0.0-rc9.7" />
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.0.0-rc9.7" />
<PackageReference Include="OpenTelemetry.Instrumentation.ElasticsearchClient" Version="1.0.0-beta.3" />
<PackageReference Include="OpenTelemetry.Instrumentation.Hangfire" Version="1.0.0-beta.2" />
<PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.0.0-rc9.7" />
<PackageReference Include="OpenTelemetry.Instrumentation.StackExchangeRedis" Version="1.0.0-rc9.7" />
Runtime version: .NET 7 RC 1
Symptom
I use a combination of AddOpenTelemetryTracing
and ConfigureBuilder
to emulate a missing overload mentioned in #3086 (I need an access to IServiceProvider
to register tracing). It works fine and I can add all exporters and instrumentions. Hovewer, the approach fails if I pass Action<AspNetCoreInstrumentationOptions>
to AddAspNetCoreInstrumentation
.
What is the expected behavior? No exceptions are thrown
What is the actual behavior?
NotSupporteException
is thrown. I see it only in Debug
, in Release
it’s swallowed, i.e. there’re no logs, no errors and no externally visible exceptions. Open telemetry just disables itself (no traces are exported, the application continues handling requests).
Reproduce
services.AddOpenTelemetryTracing(builder =>
{
builder.ConfigureBuilder(static (provider, nestedBuilder) =>
{
var tracingOptions = provider.GetRequiredService<IOptions<TracingOptions>>().Value;
if (!tracingOptions.Enabled)
{
return;
}
nestedBuilder.AddConsoleExporter();
var resourceBuilder = ResourceBuilder
.CreateDefault()
.AddService("name");
nestedBuilder
.AddSource("name")
.SetResourceBuilder(resourceBuilder)
.AddAspNetCoreInstrumentation(options => // .AddAspNetCoreInstrumentation() works fine
{
options.Filter = context => true;
});
});
});
As a workaround I can call .AddAspNetCoreInstrumentation()
and add
services.Configure<AspNetCoreInstrumentationOptions>(options =>
{
options.Filter = context => true;
});
Additional Context
System.NotSupportedException: 'Services cannot be configured after ServiceProvider has been created.'
at OpenTelemetry.Trace.TracerProviderBuilderBase.ConfigureServices(Action`1 configure)
at OpenTelemetry.Trace.TracerProviderBuilderExtensions.ConfigureServices(TracerProviderBuilder tracerProviderBuilder, Action`1 configure)
at OpenTelemetry.Trace.TracerProviderBuilderExtensions.AddAspNetCoreInstrumentation(TracerProviderBuilder builder, String name, Action`1 configureAspNetCoreInstrumentationOptions)
at OpenTelemetry.Trace.TracerProviderBuilderExtensions.AddAspNetCoreInstrumentation(TracerProviderBuilder builder, Action`1 configureAspNetCoreInstrumentationOptions)
at Host.Tracing.Extensions.ServiceCollectionExtensions.<>c.<AddTracing>b__0_5(IServiceProvider provider, TracerProviderBuilder nestedBuilder) in C:\Users\Hello\source\repos\common\src\Host\Tracing\Extensions\ServiceCollectionExtensions.cs:line 52
at OpenTelemetry.Trace.TracerProviderBuilderServiceCollectionHelper.<>c__DisplayClass0_0.<RegisterConfigureBuilderCallback>b__0(IServiceProvider sp, TracerProviderBuilderState state)
at OpenTelemetry.Trace.TracerProviderBuilderServiceCollectionHelper.ConfigureTracerProviderBuilderStateCallbackRegistration.Configure(IServiceProvider serviceProvider, TracerProviderBuilderState state)
at OpenTelemetry.Trace.TracerProviderBuilderServiceCollectionHelper.InvokeRegisteredConfigureStateCallbacks(IServiceProvider serviceProvider, TracerProviderBuilderState state)
at OpenTelemetry.Trace.TracerProviderSdk..ctor(IServiceProvider serviceProvider, Boolean ownsServiceProvider)
at OpenTelemetry.Trace.TracerProviderBuilderBase.<>c.<.ctor>b__4_0(IServiceProvider sp)
at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitRootCache(ServiceCallSite callSite, RuntimeResolverContext context)
at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSite(ServiceCallSite callSite, TArgument argument)
at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.Resolve(ServiceCallSite callSite, ServiceProviderEngineScope scope)
at Microsoft.Extensions.DependencyInjection.ServiceProvider.CreateServiceAccessor(Type serviceType)
at System.Collections.Concurrent.ConcurrentDictionary`2.GetOrAdd(TKey key, Func`2 valueFactory)
at Microsoft.Extensions.DependencyInjection.ServiceProvider.GetService(Type serviceType, ServiceProviderEngineScope serviceProviderEngineScope)
at Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetService[T](IServiceProvider provider)
at OpenTelemetry.Extensions.Hosting.Implementation.TelemetryHostedService.Initialize(IServiceProvider serviceProvider)
at OpenTelemetry.Extensions.Hosting.Implementation.TelemetryHostedService.StartAsync(CancellationToken cancellationToken)
About this issue
- Original URL
- State: open
- Created 2 years ago
- Reactions: 2
- Comments: 17 (8 by maintainers)
@CodeBlanch is there any particular reason why the team decided not to follow proper semantic versioning for these libraries? Minor version bumps shouldn’t contain breaking API changes ideally, only deprecations at most.
Would certainly be nice if you were following semantic versioning as that gives more information about actual breaking changes to consumers by just looking at the version numbers.
@maciejw , adding HostedServices to the ServicesCollection automatically starts them up when the application is run. This is how the hosting extensions work by definition.
What this library does is exactly that: it adds a hosted service that listens to the telemetry and then exports them.
Again, it relies on a hosted service. You are just misunderstanding how it works fundamentally.
I don’t quite follow this one. Can you clarify?
This should work just fine though. Did you run into any issues trying to move configuration into
IConfigureOptions
?My understanding is that this works as well.
What conditional registration are you alluding to? In general I agree, but I’m not sure what you are referring to here in particular.
This looks like an exaggeration… what aspects do you think would need to be testable exactly?
See first points. This relies on hosted services, the same way AspNetCore itself does. Similarly for AspNetCore, you don’t “see” the hosted service being registered anywhere but it is done behind the scenes by one of the extensions or builders.
🤔
Hello everyone.
It looks like this library has its own way of doing things, does anyone read aspnetcore source code or documentation here? because this library ignores all widely know conventions from dotnet ecosystem.
A summary:
ConfigureServices
should not start anything, this is only for DI configuration, registeringServiceDescriptor
sConfigureServices
, because its a configuration for DI to make composition root work. Aspnet will compose a graph and will give us an instance of what we ask later.IServiceConfigration
extension methods plus useIConfigureOptions
for more advanced stuffIConfigureOptions<T>
(using this we can inject stuff from DI, and configure something fromIConfiguration
)ConfigureServices
when we have an app that does something more thenWeatherForecast
fromdotnet new webapi
Startup.Configure(ApplicationBuilder)
method,IStartupFilter
orIHostingService
If we forget about points I’ve mentioned we end up with problems described here. This method from the title is a manifestation of how not to do stuff, but this is only the tip of the iceberg. The way configuration API of this library works is an abomination.
I know that we cannot change current API in a drastic way for now, but I hope we can do better in version 2.0, how can I help? together we could build something truly amazing 😃
@julealgon I hear you. My thoughts on this…
Define breaking API change for me 😄
There shouldn’t be any source breaks. You should be able to upgrade and compile. A lot of time went into making that possible! If you ran into one of these please reply or open an issue ASAP.
There shouldn’t be any binary breaks either, meaning you can drop in a newer version at runtime, but there is one that I know about. We should probably fix that.
Behavior breaks, the goal is to avoid them, but you know the saying about omelets and broken eggs right? 😄 If we were to do a 2.0 we would have made a lot bigger changes which would have been very disruptive and might have caused people to stick on 1.3 (which would be bad for the growth of the ecosystem). A few breaking behavioral things so long as the impact is low is a lesser evil IMO.
ConfigureBuilder
is a really advanced API I expect only advanced users to be messing with so while these changes are annoying, I think the good outweighs the bad.All that being said I’m sure I’m going to get flamed on this issue hard once 1.4 comes out 🤣
@alefcarlos Thanks for sharing!
Agree with this. The
AddOtlpExporter
extension changed a lot from 1.3.0 to 1.4.0. It now requires theIServiceCollection
to do its work. Couple of reasons for this. 1) Tighter integration with Options API. If you pass a delegate to configure the options it should play nice with other actions that are registered (eg usingservices.Configure<OtlpExporterOptions>
). To do that it has to register the delegate into services. 2) Tighter integration with Configuration API. Previously we loaded environment variables through the static class. Now we load them throughIConfiguration
. That also requires theIServiceCollection
.So this is a breaking behavioral change, but I think the new features/tighter integrations are worth it. I don’t think most users will run into this.
The code above extending
WebApplicationBuilder
looks good to me. It is very close to what our AspNetCore example does. For dynamically choosing an exporter, this is a good solution.You could get your original code up and running by avoiding the
AddOtlpExporter
extension and usingbuilder.AddProcessor
method with the public ctor exposed on the exporter classes… but I’m not going to provide that code because I think the above is a better solution.Folks… please, you gotta be careful with these random breaking changes…
I had code since May that was relying on
GetServices
to register extra services in the container, and suddenly (I actually don’t know when precisely) that just doesn’t work anymore?I just spent nearly 2 entire days debugging why my traces were not showing up for a specific project. Doing all sorts of crazy tests, trying to eliminate as many factors as possible. I thought it was something in the Azure AppService environment that might have changed with the shift to .NET7, but no… a breaking change on this library started to silently throw an exception that caused my traces to vanish completely.
Took me a very long time to finally be able to reproduce the issue locally, because it was related to a conditional call based on “Environment != ‘Development’”. I even set up an ubuntu distro locally using WSL to see if running in that environment would reproduce the problem I was seeing…
When I finally changed the environment to “Production” locally, I then immediately saw this exception. I thought “huh maybe this new
ConfigureServices
method from the newer version is not really equivalent to the oldGetServices()
”? But nay… switching back toGetServices()
with a disposal warning suppression resulted in the exact same exception. On something that hasn’t changed at all in several months besides package updates and .NET 6 to 7 migration 😭To top it all of, you decide to fucking swallow exceptions like this… why would you do that, that’s an incredibly bad practice. This is a critical mistake (the exception message clearly says “you shouldn’t do this”), by swallowing this exception you make it nearly impossible to detect the problem. I totally understand ignoring some configuration things, using the standard path if things are not setup, etc, but you can’t just swallow critical exceptions like this.
So frustrating… I just can’t believe I wasted so much time on something that would be trivially found if the exception was just thrown and the app crashed.
I’ve mentioned earlier how terrible I thought the EventSource-based custom diagnostics thing was (I still hope you’ll abandon all of that at some point and migrate towards
ILogger
), but this here takes the cake.Linking to: https://github.com/open-telemetry/opentelemetry-dotnet/issues/3881
@julealgon, thank you !
But that used to work using 1.3.0 version 😦
Thats the exactly behaviour I have.
To solve that I changed my extension, now it uses
WebApplicationBuilder
@alefcarlos OTEL already defines a well-known configuration setting that you can use to change the URL,
OTEL_EXPORTER_OTLP_ENDPOINT
. I’d strongly recommend relying on that instead of creating a separate configuration setting for it.As for how to conditionally add the exporter based on configuration, you might have to pass the
IConfiguration
instead of attempting to resolve it from the container (which is what I ended up doing withIHostEnvironment
as well).I’m aware that the OTEL spec is very sensitive in this regard, but as you said, this type of exception is different: it is not an OTEL/collection issue, it is a preliminary setup problem that is part of the library and not of OTEL itself.
Agreed with having the exception always happen here. This would’ve allowed us to catch the problem in our case months earlier than we did.
Definitely looking forward to that.
This is absolutely fantastic! Thank you so much for sharing it. I’ll for sure be using this in all of our services until official
ILogger
support is added.Sadly, this is working as designed!
ConfigureBuilder
fires after theIServiceProvider
has been created fromIServiceCollection
. Services can’t be added to theIServiceProvider
once is has been created. The way the Options API works is configuration delegates are added to theIServiceCollection
and then executed back when you access the final instance. That’s why it is blowing up.AddAspNetCoreInstrumentation
is trying to register a “service” (in this case configuration delegate) too late in the flow. Not sure what we could do about that.The OP has hinted at a potential solution, but let me add it to the full snippet:
@hankovich It looks like you want to disable tracing based on your runtime configuration. This really isn’t the use case
ConfigureBuilder
was intended to solve. Would something like this work instead?Just FYI we would like to have a built-in mechanism for disabling the SDK, but we’re not there yet! We’re kind of kicking around ideas for example: #3639