Skip to content

Commit 7576078

Browse files
authored
Enhance assembly scanning (#253)
We now register AspNetCore as a special-case to ensure we can enable the enrich with exceptions. We now prefer assembly scanning even for dependencies such as HTTP. This allows consumers to disable them with the assembly scanning switch.
1 parent cecc659 commit 7576078

File tree

4 files changed

+118
-55
lines changed

4 files changed

+118
-55
lines changed

src/Elastic.OpenTelemetry/Elastic.OpenTelemetry.csproj

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<Project Sdk="Microsoft.NET.Sdk">
1+
<Project Sdk="Microsoft.NET.Sdk">
22

33
<PropertyGroup>
44
<OutputType>Library</OutputType>
@@ -30,21 +30,25 @@
3030
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.11.2" />
3131
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.11.2" />
3232

33+
<!-- Required to add necessary resource attributes to drive the Observability UI -->
3334
<PackageReference Include="OpenTelemetry.Resources.Host" Version="1.11.0-beta.1" />
3435
<PackageReference Include="OpenTelemetry.Resources.ProcessRuntime" Version="1.11.0-beta.2" />
35-
36-
<PackageReference Include="OpenTelemetry.Instrumentation.GrpcNetClient" Version="1.9.0-beta.1" />
36+
37+
<!-- Required to produce metrics used on the curated dashboard -->
3738
<PackageReference Include="OpenTelemetry.Instrumentation.Process" Version="0.5.0-beta.6" />
39+
40+
<!-- Not, required but we bring these in for a simpler OOB -->
41+
<PackageReference Include="OpenTelemetry.Instrumentation.GrpcNetClient" Version="1.9.0-beta.1" />
3842
<PackageReference Include="OpenTelemetry.Instrumentation.SqlClient" Version="1.9.0-beta.1" />
3943

4044
<PackageReference Include="Polyfill" Version="7.21.0" PrivateAssets="all" IncludeAssets="runtime; build; native; contentfiles; analyzers; buildtransitive" />
4145
<PackageReference Include="NetEscapades.EnumGenerators" Version="1.0.0-beta09" PrivateAssets="all" ExcludeAssets="runtime" />
4246
</ItemGroup>
4347

4448
<ItemGroup Condition="'$(TargetFramework)' != 'net9.0'">
45-
<!-- We skip this on .NET since we prefer to use the native instrumentation from the built in Meter -->
49+
<!-- We skip this on .NET 9, since we prefer to use the native instrumentation from the built in Meter -->
4650
<PackageReference Include="OpenTelemetry.Instrumentation.Runtime" Version="1.11.1" />
47-
<!-- We skip this on .NET since we prefer to use the native instrumentation from the built in ActivitySource -->
51+
<!-- We skip this on .NET 9, since we prefer to use the native instrumentation from the built in ActivitySource -->
4852
<PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.11.1" />
4953
</ItemGroup>
5054

src/Elastic.OpenTelemetry/Extensions/TracerProviderBuilderExtensions.cs

Lines changed: 86 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
33
// See the LICENSE file in the project root for more information
44

5+
using System.Diagnostics;
56
using System.Diagnostics.CodeAnalysis;
7+
using System.Reflection;
68
using System.Runtime.CompilerServices;
79
using Elastic.OpenTelemetry;
810
using Elastic.OpenTelemetry.Configuration;
@@ -135,9 +137,9 @@ private static TracerProviderBuilder WithElasticDefaultsCore(
135137
return SignalBuilder.WithElasticDefaults(builder, Signals.Traces, options, components, services, ConfigureBuilder);
136138
}
137139

138-
[UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026", Justification = "The calls to `AddSqlClientInstrumentation` and " +
139-
"`AssemblyScanning.AddInstrumentationViaReflection` are guarded by a RuntimeFeature.IsDynamicCodeSupported` check and therefore " +
140-
"this method is safe to call in AoT scenarios.")]
140+
[UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026", Justification = "The call to AssemblyScanning.AddInstrumentationViaReflection` " +
141+
"is guarded by a RuntimeFeature.IsDynamicCodeSupported` check and, therefore, this method is safe to call in AoT scenarios.")]
142+
141143
private static void ConfigureBuilder(TracerProviderBuilder builder, BuilderState builderState, IServiceCollection? services)
142144
{
143145
const string tracerProviderBuilderName = nameof(TracerProviderBuilder);
@@ -153,38 +155,28 @@ private static void ConfigureBuilder(TracerProviderBuilder builder, BuilderState
153155
builder.ConfigureServices(sc => sc.Configure<OtlpExporterOptions>(OtlpExporterDefaults.OtlpExporterOptions));
154156

155157
#if NET9_0_OR_GREATER
156-
if (SignalBuilder.InstrumentationAssemblyExists("OpenTelemetry.Instrumentation.Http.dll"))
157-
{
158-
logger.LogHttpInstrumentationFound("trace", tracerProviderBuilderName, builderState.InstanceIdentifier);
159-
160-
if (!RuntimeFeature.IsDynamicCodeSupported)
161-
logger.LogWarning("The OpenTelemetry.Instrumentation.Http.dll was found alongside the executing assembly. " +
162-
"When using Native AOT publishing on .NET, the trace instrumentation is not registered automatically. Either register it manually, " +
163-
"or remove the dependency so that the native `System.Net.Http` instrumentation (available in .NET 9) is observed instead.");
164-
}
165-
else
158+
// .NET 9 introduced semantic convention compatible instrumentation in System.Net.Http so it's recommended to no longer
159+
// use the contrib instrumentation. We don't bring in the dependency for .NET 9+. However, if the consuming app depends
160+
// on it, it will be assumed that the user prefers it and therefore we allow the assembly scanning to add it. We don't
161+
// add the native source to avoid doubling up on spans.
162+
if (!SignalBuilder.InstrumentationAssemblyExists("OpenTelemetry.Instrumentation.Http.dll"))
166163
{
167164
TracerProvderBuilderExtensions.AddActivitySourceWithLogging(builder, logger, "System.Net.Http", builderState.InstanceIdentifier);
168165
}
169-
#else
170-
AddWithLogging(builder, logger, "HTTP", b => b.AddHttpClientInstrumentation(), builderState.InstanceIdentifier);
171166
#endif
172167

173-
AddWithLogging(builder, logger, "GrpcClient", b => b.AddGrpcClientInstrumentation(), builderState.InstanceIdentifier);
174-
175168
TracerProvderBuilderExtensions.AddActivitySourceWithLogging(builder, logger, "Elastic.Transport", builderState.InstanceIdentifier);
176169

177-
// NOTE: Despite them having no dependencies. We cannot add the OpenTelemetry.Instrumentation.ElasticsearchClient or
178-
// OpenTelemetry.Instrumentation.EntityFrameworkCore instrumentations here, as including the package references causes
179-
// trimming warnings. We can still add them via reflection.
180-
181170
#if NET
182171
if (RuntimeFeature.IsDynamicCodeSupported)
183172
#endif
184173
{
185-
// This instrumentation is not currently compatible for AoT scenarios.
186-
AddWithLogging(builder, logger, "SqlClient", b => b.AddSqlClientInstrumentation(), builderState.InstanceIdentifier);
187174
SignalBuilder.AddInstrumentationViaReflection(builder, components, ContribTraceInstrumentation.GetReflectionInstrumentationAssemblies(), builderState.InstanceIdentifier);
175+
176+
// This is special-cased because we need to register additional options to ensure we capture exceptions by default
177+
// This improves the UI experience as requests which cause an exception are highlighted in the UI and users can view the
178+
// log generated from the span event.
179+
AddAspNetCoreInstrumentation(builder, builderState);
188180
}
189181

190182
TracerProvderBuilderExtensions.AddElasticProcessorsCore(builder, builderState, null, services);
@@ -199,12 +191,79 @@ private static void ConfigureBuilder(TracerProviderBuilder builder, BuilderState
199191
}
200192

201193
logger.LogConfiguredSignalProvider(nameof(Signals.Traces), nameof(TracerProviderBuilder), builderState.InstanceIdentifier);
194+
}
195+
196+
[UnconditionalSuppressMessage("DynamicCode", "IL2026", Justification = "The call to this method is guarded by a RuntimeFeature.IsDynamicCodeSupported` " +
197+
"check and therefore this method is safe to call in AoT scenarios.")]
198+
[UnconditionalSuppressMessage("DynamicCode", "IL3050", Justification = "The call to this method is guarded by a RuntimeFeature.IsDynamicCodeSupported` " +
199+
"check and therefore this method is safe to call in AoT scenarios.")]
200+
[UnconditionalSuppressMessage("DynamicallyAccessMembers", "IL2075", Justification = "The call to this method is guarded by a RuntimeFeature.IsDynamicCodeSupported` " +
201+
"check and therefore this method is safe to call in AoT scenarios.")]
202+
private static void AddAspNetCoreInstrumentation(TracerProviderBuilder builder, BuilderState builderState)
203+
{
204+
if (builderState.Components.Options.SkipInstrumentationAssemblyScanning)
205+
return;
206+
207+
var logger = builderState.Components.Logger;
202208

203-
[MethodImpl(MethodImplOptions.AggressiveInlining)]
204-
static void AddWithLogging(TracerProviderBuilder builder, ILogger logger, string name, Action<TracerProviderBuilder> add, string builderIdentifier)
209+
const string tracerProviderBuilderExtensionsTypeName = "OpenTelemetry.Trace.AspNetCoreInstrumentationTracerProviderBuilderExtensions";
210+
const string aspNetCoreTraceInstrumentationOptionsTypeName = "OpenTelemetry.Instrumentation.AspNetCore.AspNetCoreTraceInstrumentationOptions";
211+
const string extensionMethodName = "AddAspNetCoreInstrumentation";
212+
const string assemblyName = "OpenTelemetry.Instrumentation.AspNetCore";
213+
214+
var builderTypeName = builder.GetType().Name;
215+
216+
try
217+
{
218+
var tracerProviderBuilderExtensionsType = Type.GetType($"{tracerProviderBuilderExtensionsTypeName}, {assemblyName}");
219+
var optionsType = Type.GetType($"{aspNetCoreTraceInstrumentationOptionsTypeName}, {assemblyName}");
220+
221+
if (tracerProviderBuilderExtensionsType is null)
222+
{
223+
logger.LogUnableToFindTypeWarning(tracerProviderBuilderExtensionsTypeName, assemblyName);
224+
return;
225+
}
226+
227+
if (optionsType is null)
228+
{
229+
logger.LogUnableToFindTypeWarning(aspNetCoreTraceInstrumentationOptionsTypeName, assemblyName);
230+
return;
231+
}
232+
233+
Action<object> configureOptions = options =>
234+
{
235+
var enrichWithExceptionProperty = options.GetType().GetProperty("EnrichWithException");
236+
if (enrichWithExceptionProperty is not null)
237+
{
238+
var enrichWithExceptionDelegate = (Action<Activity, Exception>)((activity, ex) =>
239+
{
240+
activity.AddException(ex);
241+
242+
if (ex.Source is not null)
243+
{
244+
activity.SetTag("exception.source", ex.Source);
245+
}
246+
});
247+
248+
enrichWithExceptionProperty.SetValue(options, enrichWithExceptionDelegate);
249+
}
250+
};
251+
252+
var methodInfo = tracerProviderBuilderExtensionsType.GetMethod(extensionMethodName, BindingFlags.Static | BindingFlags.Public,
253+
Type.DefaultBinder, [typeof(TracerProviderBuilder), typeof(Action<>).MakeGenericType(optionsType)], null);
254+
255+
if (methodInfo is null)
256+
{
257+
logger.LogUnableToFindMethodWarning(tracerProviderBuilderExtensionsTypeName, extensionMethodName, assemblyName);
258+
return;
259+
}
260+
261+
methodInfo.Invoke(null, [builder, configureOptions]);
262+
}
263+
catch (Exception ex)
205264
{
206-
add.Invoke(builder);
207-
logger.LogAddedInstrumentation(name, nameof(TracerProviderBuilder), builderIdentifier);
265+
logger.LogError(new EventId(503, "DynamicInstrumentaionFailed"), ex, "Failed to dynamically enable " +
266+
"{InstrumentationName} on {Provider}.", assemblyName, builderTypeName);
208267
}
209268
}
210269
}

src/Elastic.OpenTelemetry/Instrumentation/ContribTraceInstrumentation.cs

Lines changed: 22 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,7 @@ public static InstrumentationAssemblyInfo[] GetReflectionInstrumentationAssembli
2323
InstrumentationMethod = "AddAspNetInstrumentation"
2424
},
2525

26-
new()
27-
{
28-
Name = "AspNetCore",
29-
Filename = "OpenTelemetry.Instrumentation.AspNetCore.dll",
30-
FullyQualifiedType = "OpenTelemetry.Trace.AspNetCoreInstrumentationTracerProviderBuilderExtensions",
31-
InstrumentationMethod = "AddAspNetCoreInstrumentation"
32-
},
26+
// NOTE: We don't add ASP.NET Core here as we special-case it and handle it manually
3327

3428
new()
3529
{
@@ -41,26 +35,26 @@ public static InstrumentationAssemblyInfo[] GetReflectionInstrumentationAssembli
4135

4236
new()
4337
{
44-
Name = "Kafka (Producer)",
45-
Filename = "OpenTelemetry.Instrumentation.ConfluentKafka.dll",
38+
Name = "ElasticsearchClient (NEST)",
39+
Filename = "OpenTelemetry.Instrumentation.ElasticsearchClient.dll",
4640
FullyQualifiedType = "OpenTelemetry.Trace.TracerProviderBuilderExtensions",
47-
InstrumentationMethod = "AddKafkaProducerInstrumentation"
41+
InstrumentationMethod = "AddElasticsearchClientInstrumentation"
4842
},
4943

5044
new()
5145
{
52-
Name = "Kafka (Consumer)",
53-
Filename = "OpenTelemetry.Instrumentation.ConfluentKafka.dll",
46+
Name = "EntityFrameworkCore",
47+
Filename = "OpenTelemetry.Instrumentation.EntityFrameworkCore.dll",
5448
FullyQualifiedType = "OpenTelemetry.Trace.TracerProviderBuilderExtensions",
55-
InstrumentationMethod = "AddKafkaConsumerInstrumentation"
49+
InstrumentationMethod = "AddEntityFrameworkCoreInstrumentation"
5650
},
5751

5852
new()
5953
{
60-
Name = "EntityFrameworkCore",
61-
Filename = "OpenTelemetry.Instrumentation.EntityFrameworkCore.dll",
54+
Name = "GrpcNetClient",
55+
Filename = "OpenTelemetry.Instrumentation.GrpcNetClient.dll",
6256
FullyQualifiedType = "OpenTelemetry.Trace.TracerProviderBuilderExtensions",
63-
InstrumentationMethod = "AddEntityFrameworkCoreInstrumentation"
57+
InstrumentationMethod = "AddGrpcClientInstrumentation"
6458
},
6559

6660
new()
@@ -79,11 +73,10 @@ public static InstrumentationAssemblyInfo[] GetReflectionInstrumentationAssembli
7973
InstrumentationMethod = "AddHangfireInstrumentation"
8074
},
8175

82-
#if NET9_0_OR_GREATER
8376
// On .NET 9, we add the `System.Net.Http` source for native instrumentation, rather than referencing
8477
// the contrib instrumentation. However, if the consuming application has their own reference to
8578
// `OpenTelemetry.Instrumentation.Http`, then we use that since it signals the consumer prefers the
86-
// contrib instrumentation. Therefore, on .NET 9+ targets, we attempt to dynamically load the contrib
79+
// contrib instrumentation. Therefore, even on .NET 9+ targets, we attempt to dynamically load the contrib
8780
// instrumentation, when available, because we no longer take this dependency for .NET 9 targets.
8881
new()
8982
{
@@ -92,14 +85,21 @@ public static InstrumentationAssemblyInfo[] GetReflectionInstrumentationAssembli
9285
FullyQualifiedType = "OpenTelemetry.Trace.HttpClientInstrumentationTracerProviderBuilderExtensions",
9386
InstrumentationMethod = "AddHttpClientInstrumentation"
9487
},
95-
#endif
9688

9789
new()
9890
{
99-
Name = "NEST",
100-
Filename = "OpenTelemetry.Instrumentation.ElasticsearchClient.dll",
91+
Name = "Kafka (Producer)",
92+
Filename = "OpenTelemetry.Instrumentation.ConfluentKafka.dll",
10193
FullyQualifiedType = "OpenTelemetry.Trace.TracerProviderBuilderExtensions",
102-
InstrumentationMethod = "AddElasticsearchClientInstrumentation"
94+
InstrumentationMethod = "AddKafkaProducerInstrumentation"
95+
},
96+
97+
new()
98+
{
99+
Name = "Kafka (Consumer)",
100+
Filename = "OpenTelemetry.Instrumentation.ConfluentKafka.dll",
101+
FullyQualifiedType = "OpenTelemetry.Trace.TracerProviderBuilderExtensions",
102+
InstrumentationMethod = "AddKafkaConsumerInstrumentation"
103103
},
104104

105105
new()

tests/Elastic.OpenTelemetry.Tests/Aot/NativeAotCompatibilityTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ public async Task CanPublishAotApp()
1818
{
1919
var workingDir = Environment.CurrentDirectory;
2020
var indexOfSolutionFolder = workingDir.AsSpan().LastIndexOf("elastic-otel-dotnet");
21-
workingDir = workingDir.AsSpan().Slice(0, indexOfSolutionFolder + "elastic-otel-dotnet".Length).ToString();
21+
workingDir = workingDir.AsSpan()[..(indexOfSolutionFolder + "elastic-otel-dotnet".Length)].ToString();
2222
workingDir = Path.Combine(workingDir, "examples", "Example.AspNetCore.WebApiAot");
2323

2424
var rid = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "win-x64" : "linux-x64";

0 commit comments

Comments
 (0)