-
Notifications
You must be signed in to change notification settings - Fork 772
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
[api] Nullable annotations for the trace folder #4873
[api] Nullable annotations for the trace folder #4873
Conversation
Codecov Report
@@ Coverage Diff @@
## main #4873 +/- ##
=======================================
Coverage 82.90% 82.90%
=======================================
Files 294 294
Lines 12193 12188 -5
=======================================
- Hits 10109 10105 -4
+ Misses 2084 2083 -1
Flags with carried forward coverage won't be shown. Click here to find out more.
|
@@ -33,7 +33,8 @@ public MeterProviderServiceCollectionBuilder(IServiceCollection services) | |||
public MeterProvider? Provider => null; | |||
|
|||
/// <inheritdoc /> | |||
public override MeterProviderBuilder AddInstrumentation<TInstrumentation>(Func<TInstrumentation> instrumentationFactory) | |||
public override MeterProviderBuilder AddInstrumentation<TInstrumentation>(Func<TInstrumentation?> instrumentationFactory) |
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.
Is it meaningful for instrumentation factories to return null? This code suggests that we expect them to always return a non-null value
opentelemetry-dotnet/src/OpenTelemetry/Metrics/Builder/MeterProviderBuilderSdk.cs
Lines 125 to 138 in 535c819
public MeterProviderBuilder AddInstrumentation( | |
string instrumentationName, | |
string instrumentationVersion, | |
object instrumentation) | |
{ | |
Debug.Assert(!string.IsNullOrWhiteSpace(instrumentationName), "instrumentationName was null or whitespace"); | |
Debug.Assert(!string.IsNullOrWhiteSpace(instrumentationVersion), "instrumentationVersion was null or whitespace"); | |
Debug.Assert(instrumentation != null, "instrumentation was null"); | |
this.Instrumentation.Add( | |
new InstrumentationRegistration( | |
instrumentationName, | |
instrumentationVersion, | |
instrumentation!)); |
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.
Good catch/question!
Short answer: Turns out that assert is based on invalid assumptions.
Long answer:
Today you can do this...
using OpenTelemetry;
using OpenTelemetry.Trace;
Console.WriteLine("Starting");
using (var provider = Sdk.CreateTracerProviderBuilder()
.AddInstrumentation<object>(() =>
{
Console.WriteLine("Instrumentation created");
return null;
})
.Build())
{
Console.WriteLine("Provider created");
}
Console.WriteLine("Ending");
Everything will work fine. We don't actually validate that thing returned by the factory is non-null or access it in any way.
What will be logged out is:
Starting
Instrumentation created
Provider created
Ending
SDK logs look like:
2023-09-22T21:55:35.5402710Z:TracerProviderSdk event: '{0}'{Building TracerProvider.}
2023-09-22T21:55:35.5645778Z:TracerProviderSdk event: '{0}'{Sampler added = "OpenTelemetry.Trace.ParentBasedSampler".}
2023-09-22T21:55:35.5650944Z:TracerProviderSdk event: '{0}'{Instrumentations added = "Object".}
2023-09-22T21:55:35.5652432Z:TracerProviderSdk event: '{0}'{TracerProvider built successfully.}
2023-09-22T21:55:35.5660583Z:'{0}' Disposed.{TracerProvider}
So null
is possible today and allowed. There is that Debug.Assert
in there, but that won't do anything for real code using release builds. That is only validating the repo's code doesn't return a null
anywhere. Could actually lead us to make the wrong decisions!
It was probably an oversight to allow null
in the first place, but in order to not break anything I annotated it as supported and made sure the code handles it well. If I go the other way and annotate it as not allowing null
that could be breaking. We would get warnings in our code where we don't validate things (adding that validation would be breaking), and it may generate warnings for consumers and break their builds. Could also be binary breaking with annotations disapearing, not sure. In the end I decided just to embrace the behavior that we have in place.
Thoughts?
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.
PS: Updated tests to make sure this case is captured.
Compat issues with assembly OpenTelemetry: | ||
CannotChangeAttribute : Attribute 'System.Runtime.CompilerServices.NullableAttribute' on generic param 'TInstrumentation' on member 'OpenTelemetry.Metrics.MeterProviderBuilderBase.AddInstrumentation<TInstrumentation>(System.Func<TInstrumentation>)' changed from '[NullableAttribute((byte)0)]' in the contract to '[NullableAttribute((byte)2)]' in the implementation. | ||
CannotChangeAttribute : Attribute 'System.Runtime.CompilerServices.NullableAttribute' on generic param 'TInstrumentation' on member 'OpenTelemetry.Trace.TracerProviderBuilderBase.AddInstrumentation<TInstrumentation>(System.Func<TInstrumentation>)' changed from '[NullableAttribute((byte)0)]' in the contract to '[NullableAttribute((byte)2)]' in the implementation. | ||
Total Issues: 2 |
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 was very tricky. I had to get help from @ericstj.
Short version: ApiCompat detects that something has changed but it doesn't attempt to figure out if the change is safe.
Long version:
What is going on here is explained by Nullable Metadata.
Our API before was...
public abstract TracerProviderBuilder AddInstrumentation<TInstrumentation>(
Func<TInstrumentation> instrumentationFactory)
where TInstrumentation : class;
That is WITHOUT a nullable context so TInstrumentation
in TracerProviderBuilderBase
got [NullableAttribute((byte)0)]
in its metadata.
Now we have annotations & a nullable context for this API as:
public abstract TracerProviderBuilder AddInstrumentation<TInstrumentation>(
Func<TInstrumentation> instrumentationFactory)
where TInstrumentation : class?;
Therefore TInstrumentation
in TracerProviderBuilderBase
gets changed to [NullableAttribute((byte)2)]
.
Both are null-allowing just one is "without annotations" and one is "with annotations."
My understanding is this is safe.
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.
Thanks @ericstj!
@@ -128,7 +128,7 @@ internal TracerProvider InvokeBuild() | |||
tracerProviderBuilderState.AddInstrumentation( | |||
instrumentationName, | |||
instrumentationVersion, | |||
instrumentationFactory); | |||
instrumentationFactory()); |
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 is really a bugfix. I'll add to CHANGELOG.
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.
Also added tests.
[MethodImpl(MethodImplOptions.AggressiveInlining)] | ||
public static TelemetrySpan WithSpan(TelemetrySpan span) | ||
public static TelemetrySpan? WithSpan(TelemetrySpan? span) |
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.
@alanwest This was another weird one. Doesn't seem to really be valid to pass a null
in or get one back out but for all of these annotations I just tried to capture what was in place today and not change any behaviors. Probably would have been better initially for this to throw a NRE for null
input but it is what it is at this point 🤣
if (instrumentation.Instance != null) | ||
if (instrumentation.Instance is not null) |
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 reason for this change was not immediately obvious to me. Just commenting in case this catches anyone else's eye:
Using !=
may case an overridden operator to be called. Using is not null
will not.
src/OpenTelemetry/CHANGELOG.md
Outdated
* Fixed a bug where `TracerProviderBuilderBase` was not invoking the | ||
`instrumentationFactory` delegate passed to the `protected` | ||
`AddInstrumentation` method. | ||
([#4873](https://github.com/open-telemetry/opentelemetry-dotnet/issues/4873)) |
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.
([#4873](https://github.com/open-telemetry/opentelemetry-dotnet/issues/4873)) | |
([#4873](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4873)) |
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.
It seems there are other entries in the CHANGELOG which need to be fixed.
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.
Updated the one I added and all the ones at the ~top using /issues/
path instead of /pull/
.
Changes
Trace
folder inside the API projectTracerProviderBuilderBase
was not invoking theinstrumentationFactory
delegate passed to theprotected
AddInstrumentation
method.Merge requirement checklist
CHANGELOG.md
files updated for non-trivial changes