Skip to content

Commit

Permalink
Use native .NET 9 traces and metrics for HTTP instrumentation (#213)
Browse files Browse the repository at this point in the history
On .NET 9, the BCL includes OTel-compliant instrumentation for HTTP,
which is now preferred over using the contrib instrumentation. For .NET
9+ targets, we check if the contrib library is referenced, and if so,
load that via reflection. Otherwise, we register the System.Net.Http
source.

Closes #195
  • Loading branch information
stevejgordon authored Feb 17, 2025
1 parent d0ee33c commit ade4cd4
Show file tree
Hide file tree
Showing 8 changed files with 114 additions and 22 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,11 @@ public class HomeController(IHttpClientFactory httpClientFactory) : Controller
public const string ActivitySourceName = "CustomActivitySource";
private static readonly ActivitySource ActivitySource = new(ActivitySourceName, "1.0.0");

private readonly IHttpClientFactory _httpClientFactory = httpClientFactory;

public async Task<IActionResult> Index()
{
using var client = httpClientFactory.CreateClient();
using var client = _httpClientFactory.CreateClient();

// ReSharper disable once ExplicitCallerInfoArgument
using var activity = ActivitySource.StartActivity("DoingStuff", ActivityKind.Internal);
Expand Down
11 changes: 5 additions & 6 deletions examples/Example.AspNetCore.Mvc/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,12 @@
// Add services to the container.
builder.Services
.AddHttpClient()
.AddOpenTelemetry()
.ConfigureResource(r => r.AddService("MyNewService1"))
.WithElasticDefaults(builder.Configuration);
.AddElasticOpenTelemetry(builder.Configuration, logger)
.ConfigureResource(r => r.AddService("MyNewService1"));

builder.Services.AddOpenTelemetry()
.ConfigureResource(r => r.AddService("MyNewService2"))
.WithElasticDefaults(builder.Configuration);
//builder.Services.AddOpenTelemetry()
// .ConfigureResource(r => r.AddService("MyNewService2"))
// .WithElasticDefaults(builder.Configuration);

//OpenTelemetrySdk.Create(b => b.WithElasticDefaults(builder.Configuration));

Expand Down
5 changes: 2 additions & 3 deletions examples/ServiceDefaults/ServiceDefaults.csproj
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsAspireSharedProject>true</IsAspireSharedProject>
Expand All @@ -16,7 +16,6 @@
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.11.1" />
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.9.0" />
<PackageReference Include="OpenTelemetry.Instrumentation.GrpcNetClient" Version="1.11.0-beta.1" />
<PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.11.0" />
<PackageReference Include="OpenTelemetry.Instrumentation.Process" Version="1.11.0-beta.1" />
<PackageReference Include="OpenTelemetry.Instrumentation.Runtime" Version="1.11.0" />
</ItemGroup>
Expand Down
1 change: 0 additions & 1 deletion src/Elastic.OpenTelemetry/Core/SignalBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ public static bool ConfigureBuilder<T>(
// code in this particular case by shortcutting and returning early.
if (options is not null && components is not null)
{
ValidateGlobalCallCount(methodName, builderName, options, components, callCount);
configure(builder, components);
return true;
}
Expand Down
4 changes: 4 additions & 0 deletions src/Elastic.OpenTelemetry/Diagnostics/LoggerMessages.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ internal static partial class LoggerMessages
[LoggerMessage(EventId = 9, EventName = "AddedInstrumentation", Level = LogLevel.Information, Message = "Added {InstrumentationName} to {Provider}.")]
public static partial void LogAddedInstrumentation(this ILogger logger, string instrumentationName, string provider);

[LoggerMessage(EventId = 10, EventName = "HttpInstrumentationFound", Level = LogLevel.Information, Message = "The HTTP instrumentation library was located at '{AssemblyPath}'. " +
"Skipping adding native {InstrumentationType} instrumentation from the 'System.Net.Http' ActivitySource.")]
public static partial void LogHttpInstrumentationFound(this ILogger logger, string assemblyPath, string instrumentationType);

// We explictly reuse the same event ID and this is the same log message, but with different types for the structured data
[LoggerMessage(EventId = 9, Level = LogLevel.Debug, Message = "{ProcessorName} found `{AttributeName}` attribute with value '{AttributeValue}' on the span.")]
internal static partial void FoundTag(this ILogger logger, string processorName, string attributeName, string attributeValue);
Expand Down
8 changes: 4 additions & 4 deletions src/Elastic.OpenTelemetry/Elastic.OpenTelemetry.csproj
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Library</OutputType>
Expand All @@ -24,23 +24,23 @@
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.11.1" />
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.11.1" />
<PackageReference Include="OpenTelemetry.Instrumentation.ElasticsearchClient" Version="1.0.0-beta.5" />
<PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.11.0" />
<PackageReference Include="OpenTelemetry.Instrumentation.GrpcNetClient" Version="1.9.0-beta.1" />
<PackageReference Include="OpenTelemetry.Instrumentation.EntityFrameworkCore" Version="1.0.0-beta.12" />
<PackageReference Include="OpenTelemetry.Instrumentation.Process" Version="0.5.0-beta.6" />
<PackageReference Include="OpenTelemetry.Instrumentation.SqlClient" Version="1.9.0-beta.1" />
<PackageReference Include="OpenTelemetry.Resources.Host" Version="1.11.0-beta.1" />
<PackageReference Include="Polyfill" Version="7.16.1" PrivateAssets="all" IncludeAssets="runtime; build; native; contentfiles; analyzers; buildtransitive" />
<PackageReference Include="NetEscapades.EnumGenerators" Version="1.0.0-beta09" PrivateAssets="all" ExcludeAssets="runtime" />
<PackageReference Include="System.Text.Json" Version="9.0.1" />
</ItemGroup>

<ItemGroup Condition="$(TargetFramework.StartsWith('netstandard2')) OR $(TargetFramework.StartsWith('net4'))">
<PackageReference Include="System.Threading.Channels" Version="9.0.1" />
<PackageReference Include="System.Threading.Channels" Version="9.0.2" />
</ItemGroup>

<ItemGroup Condition="'$(TargetFramework)' != 'net9.0'">
<PackageReference Include="System.Text.Json" Version="9.0.2" />
<PackageReference Include="OpenTelemetry.Instrumentation.Runtime" Version="1.11.0" />
<PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.11.0" />
</ItemGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,21 @@ private static InstrumentationAssemblyInfo[] GetReflectionInstrumentationAssembl
Filename = "OpenTelemetry.Instrumentation.AspNetCore.dll",
FullyQualifiedType = "OpenTelemetry.Metrics.AspNetCoreInstrumentationMeterProviderBuilderExtensions",
InstrumentationMethod = "AddAspNetCoreInstrumentation"
}
},
#if NET9_0_OR_GREATER
// On .NET 9, we add the `System.Net.Http` source for native instrumentation, rather than referencing
// the contrib instrumentation. However, if the consuming application has their own reference to
// `OpenTelemetry.Instrumentation.Http`, then we use that since it signals the consumer prefers the
// contrib instrumentation. Therefore, on .NET 9+ targets, we attempt to dynamically load the contrib
// instrumentation, when available.
new()
{
Name = "Http",
Filename = "OpenTelemetry.Instrumentation.Http.dll",
FullyQualifiedType = "OpenTelemetry.Metrics.HttpClientInstrumentationMeterProviderBuilderExtensions",
InstrumentationMethod = "AddHttpClientInstrumentation"
},
#endif
];

/// <summary>
Expand Down Expand Up @@ -156,8 +170,41 @@ static void ConfigureBuilder(MeterProviderBuilder builder, ElasticOpenTelemetryC
{
builder.ConfigureResource(r => r.AddElasticDistroAttributes());

AddWithLogging(builder, components.Logger, "HttpClient", b => b.AddHttpClientInstrumentation());
#if NET9_0_OR_GREATER
try
{
// This first check determines whether OpenTelemetry.Instrumentation.Http.dll is present, in which case,
// it will be registered on the builder via reflection. If it's not present, we can safely add the native
// source which is OTel compliant since .NET 9.
var assemblyLocation = Path.GetDirectoryName(typeof(ElasticOpenTelemetry).Assembly.Location);
if (assemblyLocation is not null)
{
var assemblyPath = Path.Combine(assemblyLocation, "OpenTelemetry.Instrumentation.Http.dll");

if (!File.Exists(assemblyPath))
{
AddWithLogging(builder, components.Logger, "Http (via native instrumentation)", b => b.AddMeter("System.Net.Http"));
}
else
{
components.Logger.LogHttpInstrumentationFound(assemblyPath, "metric");
}
}
}
catch (Exception ex)
{
components.Logger.LogError(ex, "An exception occurred while checking for the presence of `OpenTelemetry.Instrumentation.Http.dll`.");
}
#else
AddWithLogging(builder, components.Logger, "Http (via contrib instrumentation)", b => b.AddHttpClientInstrumentation());
#endif

AddWithLogging(builder, components.Logger, "Process", b => b.AddProcessInstrumentation());
#if NET9_0_OR_GREATER
AddWithLogging(builder, components.Logger, "Runtime", b => b.AddMeter("System.Runtime"));
#else
AddWithLogging(builder, components.Logger, "Runtime", b => b.AddRuntimeInstrumentation());
#endif

// TODO - Guard this behind runtime checks e.g. RuntimeFeature.IsDynamicCodeSupported to support AoT users.
// see https://github.com/elastic/elastic-otel-dotnet/issues/198
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,21 @@ private static InstrumentationAssemblyInfo[] GetReflectionInstrumentationAssembl
Filename = "OpenTelemetry.Instrumentation.AspNetCore.dll",
FullyQualifiedType = "OpenTelemetry.Trace.AspNetCoreInstrumentationTracerProviderBuilderExtensions",
InstrumentationMethod = "AddAspNetCoreInstrumentation"
}
},
#if NET9_0_OR_GREATER
// On .NET 9, we add the `System.Net.Http` source for native instrumentation, rather than referencing
// the contrib instrumentation. However, if the consuming application has their own reference to
// `OpenTelemetry.Instrumentation.Http`, then we use that since it signals the consumer prefers the
// contrib instrumentation. Therefore, on .NET 9+ targets, we attempt to dynamically load the contrib
// instrumentation, when available.
new()
{
Name = "Http",
Filename = "OpenTelemetry.Instrumentation.Http.dll",
FullyQualifiedType = "OpenTelemetry.Trace.HttpClientInstrumentationTracerProviderBuilderExtensions",
InstrumentationMethod = "AddHttpClientInstrumentation"
},
#endif
];

/// <summary>
Expand Down Expand Up @@ -147,7 +161,35 @@ static void ConfigureBuilder(TracerProviderBuilder builder, ElasticOpenTelemetry
{
builder.ConfigureResource(r => r.AddElasticDistroAttributes());

AddWithLogging(builder, components.Logger, "HttpClient", b => b.AddHttpClientInstrumentation());
#if NET9_0_OR_GREATER
try
{
// This first check determines whether OpenTelemetry.Instrumentation.Http.dll is present, in which case,
// it will be registered on the builder via reflection. If it's not present, we can safely add the native
// source which is OTel compliant since .NET 9.
var assemblyLocation = Path.GetDirectoryName(typeof(ElasticOpenTelemetry).Assembly.Location);
if (assemblyLocation is not null)
{
var assemblyPath = Path.Combine(assemblyLocation, "OpenTelemetry.Instrumentation.Http.dll");

if (!File.Exists(assemblyPath))
{
AddWithLogging(builder, components.Logger, "Http (via native instrumentation)", b => b.AddSource("System.Net.Http"));
}
else
{
components.Logger.LogHttpInstrumentationFound(assemblyPath, "trace");
}
}
}
catch (Exception ex)
{
components.Logger.LogError(ex, "An exception occurred while checking for the presence of `OpenTelemetry.Instrumentation.Http.dll`.");
}
#else
AddWithLogging(builder, components.Logger, "Http (via contrib instrumentation)", b => b.AddHttpClientInstrumentation());
#endif

AddWithLogging(builder, components.Logger, "GrpcClient", b => b.AddGrpcClientInstrumentation());
AddWithLogging(builder, components.Logger, "EntityFrameworkCore", b => b.AddEntityFrameworkCoreInstrumentation());
AddWithLogging(builder, components.Logger, "NEST", b => b.AddElasticsearchClientInstrumentation());
Expand Down Expand Up @@ -192,9 +234,9 @@ static void AddInstrumentationViaReflection(TracerProviderBuilder builder, ILogg
AddInstrumentationLibraryViaReflection(builder, logger, assemblyLocation, assembly);
}
}
catch
catch (Exception ex)
{
// TODO - Logging
logger.LogError(ex, "An exception occurred while adding instrumentation via reflection.");
}
}

Expand All @@ -207,7 +249,7 @@ static void AddInstrumentationLibraryViaReflection(
try
{
var assemblyPath = Path.Combine(assemblyLocation, info.Filename);
if (File.Exists(Path.Combine(assemblyLocation, info.Filename)))
if (File.Exists(assemblyPath))
{
logger.LogLocatedInstrumentationAssembly(info.Filename, assemblyLocation);

Expand Down

0 comments on commit ade4cd4

Please sign in to comment.