[api] Nullable annotations for the trace folder#4873
[api] Nullable annotations for the trace folder#4873CodeBlanch merged 15 commits intoopen-telemetry:mainfrom
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.
|
|
|
||
| /// <inheritdoc /> | ||
| public override MeterProviderBuilder AddInstrumentation<TInstrumentation>(Func<TInstrumentation> instrumentationFactory) | ||
| public override MeterProviderBuilder AddInstrumentation<TInstrumentation>(Func<TInstrumentation?> instrumentationFactory) |
There was a problem hiding this comment.
Is it meaningful for instrumentation factories to return null? This code suggests that we expect them to always return a non-null value
There was a problem hiding this comment.
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.
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.
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.
| instrumentationName, | ||
| instrumentationVersion, | ||
| instrumentationFactory); | ||
| instrumentationFactory()); |
There was a problem hiding this comment.
This is really a bugfix. I'll add to CHANGELOG.
| #endif | ||
| [MethodImpl(MethodImplOptions.AggressiveInlining)] | ||
| public static TelemetrySpan WithSpan(TelemetrySpan span) | ||
| public static TelemetrySpan? WithSpan(TelemetrySpan? span) |
There was a problem hiding this comment.
@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 🤣
| foreach (var instrumentation in state.Instrumentation) | ||
| { | ||
| if (instrumentation.Instance != null) | ||
| if (instrumentation.Instance is not null) |
There was a problem hiding this comment.
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.
| ([#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.
It seems there are other entries in the CHANGELOG which need to be fixed.
There was a problem hiding this comment.
Updated the one I added and all the ones at the ~top using /issues/ path instead of /pull/.
Changes
Tracefolder inside the API projectTracerProviderBuilderBasewas not invoking theinstrumentationFactorydelegate passed to theprotectedAddInstrumentationmethod.Merge requirement checklist
CHANGELOG.mdfiles updated for non-trivial changes