From 81790f5c191cfddd9a3fc79dfd6976d23ad88430 Mon Sep 17 00:00:00 2001 From: Nils Gruson Date: Thu, 21 Dec 2023 18:46:20 +0100 Subject: [PATCH] Add support for OTEL_INSTRUMENTATION_HTTP_KNOWN_METHODS --- ...mentationMeterProviderBuilderExtensions.cs | 3 +- ...entationTracerProviderBuilderExtensions.cs | 1 - .../AspNetCoreTraceInstrumentationOptions.cs | 14 +++ .../Implementation/HttpInListener.cs | 18 +-- .../Implementation/HttpInMetricsListener.cs | 3 +- ...mentationMeterProviderBuilderExtensions.cs | 3 +- ...entationTracerProviderBuilderExtensions.cs | 3 +- .../HttpClientTraceInstrumentationOptions.cs | 10 ++ .../HttpHandlerDiagnosticListener.cs | 14 +-- .../HttpWebRequestActivitySource.netfx.cs | 4 +- src/Shared/RequestMethodHelper.cs | 92 +++++++++++---- .../BasicTests.cs | 107 ++++++++++++++++++ 12 files changed, 224 insertions(+), 48 deletions(-) diff --git a/src/OpenTelemetry.Instrumentation.AspNetCore/AspNetCoreInstrumentationMeterProviderBuilderExtensions.cs b/src/OpenTelemetry.Instrumentation.AspNetCore/AspNetCoreInstrumentationMeterProviderBuilderExtensions.cs index 3da8c1de59d..35d9f85793f 100644 --- a/src/OpenTelemetry.Instrumentation.AspNetCore/AspNetCoreInstrumentationMeterProviderBuilderExtensions.cs +++ b/src/OpenTelemetry.Instrumentation.AspNetCore/AspNetCoreInstrumentationMeterProviderBuilderExtensions.cs @@ -27,9 +27,8 @@ public static MeterProviderBuilder AddAspNetCoreInstrumentation( #if NET8_0_OR_GREATER return builder.ConfigureMeters(); #else - // Note: Warm-up the status code and method mapping. + // Note: Warm-up the status code. _ = TelemetryHelper.BoxedStatusCodes; - _ = RequestMethodHelper.KnownMethods; builder.AddMeter(HttpInMetricsListener.InstrumentationName); diff --git a/src/OpenTelemetry.Instrumentation.AspNetCore/AspNetCoreInstrumentationTracerProviderBuilderExtensions.cs b/src/OpenTelemetry.Instrumentation.AspNetCore/AspNetCoreInstrumentationTracerProviderBuilderExtensions.cs index d37d77ae3fa..0091729c70a 100644 --- a/src/OpenTelemetry.Instrumentation.AspNetCore/AspNetCoreInstrumentationTracerProviderBuilderExtensions.cs +++ b/src/OpenTelemetry.Instrumentation.AspNetCore/AspNetCoreInstrumentationTracerProviderBuilderExtensions.cs @@ -52,7 +52,6 @@ public static TracerProviderBuilder AddAspNetCoreInstrumentation( // Note: Warm-up the status code and method mapping. _ = TelemetryHelper.BoxedStatusCodes; - _ = RequestMethodHelper.KnownMethods; name ??= Options.DefaultName; diff --git a/src/OpenTelemetry.Instrumentation.AspNetCore/AspNetCoreTraceInstrumentationOptions.cs b/src/OpenTelemetry.Instrumentation.AspNetCore/AspNetCoreTraceInstrumentationOptions.cs index f66a5e04344..3603da27c55 100644 --- a/src/OpenTelemetry.Instrumentation.AspNetCore/AspNetCoreTraceInstrumentationOptions.cs +++ b/src/OpenTelemetry.Instrumentation.AspNetCore/AspNetCoreTraceInstrumentationOptions.cs @@ -1,6 +1,9 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 +#if NET8_0_OR_GREATER +using System.Collections.Frozen; +#endif using System.Diagnostics; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Configuration; @@ -29,6 +32,15 @@ internal AspNetCoreTraceInstrumentationOptions(IConfiguration configuration) { this.EnableGrpcAspNetCoreSupport = enableGrpcInstrumentation; } + + if (configuration.TryGetStringValue("OTEL_INSTRUMENTATION_HTTP_KNOWN_METHODS", out var knownHttpMethods)) + { + this.KnownHttpMethods = knownHttpMethods + .Split(',') + .Select(x => x.Trim()) + .Where(x => !string.IsNullOrEmpty(x)) + .ToList(); + } } /// @@ -91,4 +103,6 @@ internal AspNetCoreTraceInstrumentationOptions(IConfiguration configuration) /// https://github.com/open-telemetry/semantic-conventions/blob/main/docs/rpc/rpc-spans.md. /// internal bool EnableGrpcAspNetCoreSupport { get; set; } + + internal List KnownHttpMethods { get; set; } = new(); } diff --git a/src/OpenTelemetry.Instrumentation.AspNetCore/Implementation/HttpInListener.cs b/src/OpenTelemetry.Instrumentation.AspNetCore/Implementation/HttpInListener.cs index a9aefacc005..12c1afeef78 100644 --- a/src/OpenTelemetry.Instrumentation.AspNetCore/Implementation/HttpInListener.cs +++ b/src/OpenTelemetry.Instrumentation.AspNetCore/Implementation/HttpInListener.cs @@ -58,6 +58,7 @@ internal class HttpInListener : ListenerHandler private readonly PropertyFetcher beforeActionTemplateFetcher = new("Template"); #endif private readonly AspNetCoreTraceInstrumentationOptions options; + private readonly RequestMethodHelper requestMethodHelper; public HttpInListener(AspNetCoreTraceInstrumentationOptions options) : base(DiagnosticSourceName) @@ -65,6 +66,7 @@ public HttpInListener(AspNetCoreTraceInstrumentationOptions options) Guard.ThrowIfNull(options); this.options = options; + this.requestMethodHelper = new RequestMethodHelper(this.options.KnownHttpMethods); } public override void OnEventWritten(string name, object payload) @@ -105,8 +107,7 @@ public void OnStartActivity(Activity activity, object payload) // By this time, samplers have already run and // activity.IsAllDataRequested populated accordingly. - HttpContext context = payload as HttpContext; - if (context == null) + if (payload is not HttpContext context) { AspNetCoreInstrumentationEventSource.Log.NullPayload(nameof(HttpInListener), nameof(this.OnStartActivity), activity.OperationName); return; @@ -176,7 +177,7 @@ public void OnStartActivity(Activity activity, object payload) #endif var path = (request.PathBase.HasValue || request.Path.HasValue) ? (request.PathBase + request.Path).ToString() : "/"; - activity.DisplayName = GetDisplayName(request.Method); + activity.DisplayName = this.GetDisplayName(request.Method); // see the spec https://github.com/open-telemetry/semantic-conventions/blob/v1.23.0/docs/http/http-spans.md @@ -196,7 +197,7 @@ public void OnStartActivity(Activity activity, object payload) activity.SetTag(SemanticConventions.AttributeUrlQuery, request.QueryString.Value); } - RequestMethodHelper.SetHttpMethodTag(activity, request.Method); + this.requestMethodHelper.SetHttpMethodTag(activity, request.Method); activity.SetTag(SemanticConventions.AttributeUrlScheme, request.Scheme); activity.SetTag(SemanticConventions.AttributeUrlPath, path); @@ -226,8 +227,7 @@ public void OnStopActivity(Activity activity, object payload) { if (activity.IsAllDataRequested) { - HttpContext context = payload as HttpContext; - if (context == null) + if (payload is not HttpContext context) { AspNetCoreInstrumentationEventSource.Log.NullPayload(nameof(HttpInListener), nameof(this.OnStopActivity), activity.OperationName); return; @@ -239,7 +239,7 @@ public void OnStopActivity(Activity activity, object payload) var routePattern = (context.GetEndpoint() as RouteEndpoint)?.RoutePattern.RawText; if (!string.IsNullOrEmpty(routePattern)) { - activity.DisplayName = GetDisplayName(context.Request.Method, routePattern); + activity.DisplayName = this.GetDisplayName(context.Request.Method, routePattern); activity.SetTag(SemanticConventions.AttributeHttpRoute, routePattern); } #endif @@ -382,9 +382,9 @@ private static void AddGrpcAttributes(Activity activity, string grpcMethod, Http } } - private static string GetDisplayName(string httpMethod, string httpRoute = null) + private string GetDisplayName(string httpMethod, string httpRoute = null) { - var normalizedMethod = RequestMethodHelper.GetNormalizedHttpMethod(httpMethod); + var normalizedMethod = this.requestMethodHelper.GetNormalizedHttpMethod(httpMethod); return string.IsNullOrEmpty(httpRoute) ? normalizedMethod diff --git a/src/OpenTelemetry.Instrumentation.AspNetCore/Implementation/HttpInMetricsListener.cs b/src/OpenTelemetry.Instrumentation.AspNetCore/Implementation/HttpInMetricsListener.cs index c2afcd02a81..2577d0ba8a7 100644 --- a/src/OpenTelemetry.Instrumentation.AspNetCore/Implementation/HttpInMetricsListener.cs +++ b/src/OpenTelemetry.Instrumentation.AspNetCore/Implementation/HttpInMetricsListener.cs @@ -84,7 +84,8 @@ public static void OnStopEventWritten(string name, object payload) tags.Add(new KeyValuePair(SemanticConventions.AttributeUrlScheme, context.Request.Scheme)); tags.Add(new KeyValuePair(SemanticConventions.AttributeHttpResponseStatusCode, TelemetryHelper.GetBoxedStatusCode(context.Response.StatusCode))); - var httpMethod = RequestMethodHelper.GetNormalizedHttpMethod(context.Request.Method); + var requestMethodHelper = new RequestMethodHelper(string.Empty); + var httpMethod = requestMethodHelper.GetNormalizedHttpMethod(context.Request.Method); tags.Add(new KeyValuePair(SemanticConventions.AttributeHttpRequestMethod, httpMethod)); #if NET6_0_OR_GREATER diff --git a/src/OpenTelemetry.Instrumentation.Http/HttpClientInstrumentationMeterProviderBuilderExtensions.cs b/src/OpenTelemetry.Instrumentation.Http/HttpClientInstrumentationMeterProviderBuilderExtensions.cs index 24b9524696f..6629d61b654 100644 --- a/src/OpenTelemetry.Instrumentation.Http/HttpClientInstrumentationMeterProviderBuilderExtensions.cs +++ b/src/OpenTelemetry.Instrumentation.Http/HttpClientInstrumentationMeterProviderBuilderExtensions.cs @@ -32,9 +32,8 @@ public static MeterProviderBuilder AddHttpClientInstrumentation( .AddMeter("System.Net.Http") .AddMeter("System.Net.NameResolution"); #else - // Note: Warm-up the status code and method mapping. + // Note: Warm-up the status code. _ = TelemetryHelper.BoxedStatusCodes; - _ = RequestMethodHelper.KnownMethods; #if NETFRAMEWORK builder.AddMeter(HttpWebRequestActivitySource.MeterName); diff --git a/src/OpenTelemetry.Instrumentation.Http/HttpClientInstrumentationTracerProviderBuilderExtensions.cs b/src/OpenTelemetry.Instrumentation.Http/HttpClientInstrumentationTracerProviderBuilderExtensions.cs index 6015c183f2c..6dd8acb772c 100644 --- a/src/OpenTelemetry.Instrumentation.Http/HttpClientInstrumentationTracerProviderBuilderExtensions.cs +++ b/src/OpenTelemetry.Instrumentation.Http/HttpClientInstrumentationTracerProviderBuilderExtensions.cs @@ -47,9 +47,8 @@ public static TracerProviderBuilder AddHttpClientInstrumentation( { Guard.ThrowIfNull(builder); - // Note: Warm-up the status code and method mapping. + // Note: Warm-up the status code. _ = TelemetryHelper.BoxedStatusCodes; - _ = RequestMethodHelper.KnownMethods; name ??= Options.DefaultName; diff --git a/src/OpenTelemetry.Instrumentation.Http/HttpClientTraceInstrumentationOptions.cs b/src/OpenTelemetry.Instrumentation.Http/HttpClientTraceInstrumentationOptions.cs index 95bb6dab7b0..c472790ac29 100644 --- a/src/OpenTelemetry.Instrumentation.Http/HttpClientTraceInstrumentationOptions.cs +++ b/src/OpenTelemetry.Instrumentation.Http/HttpClientTraceInstrumentationOptions.cs @@ -1,6 +1,9 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 +#if NET8_0_OR_GREATER +using System.Collections.Frozen; +#endif using System.Diagnostics; using System.Net; #if NETFRAMEWORK @@ -8,6 +11,7 @@ #endif using System.Runtime.CompilerServices; using OpenTelemetry.Instrumentation.Http.Implementation; +using OpenTelemetry.Internal; namespace OpenTelemetry.Instrumentation.Http; @@ -125,6 +129,12 @@ public class HttpClientTraceInstrumentationOptions /// public bool RecordException { get; set; } +#if NET8_0_OR_GREATER + internal FrozenDictionary KnownHttpMethods { get; set; } +#else + internal Dictionary KnownHttpMethods { get; set; } = RequestMethodHelper.GetKnownMethods(null); +#endif + [MethodImpl(MethodImplOptions.AggressiveInlining)] internal bool EventFilterHttpRequestMessage(string activityName, object arg1) { diff --git a/src/OpenTelemetry.Instrumentation.Http/Implementation/HttpHandlerDiagnosticListener.cs b/src/OpenTelemetry.Instrumentation.Http/Implementation/HttpHandlerDiagnosticListener.cs index e0625b420db..278f4d48670 100644 --- a/src/OpenTelemetry.Instrumentation.Http/Implementation/HttpHandlerDiagnosticListener.cs +++ b/src/OpenTelemetry.Instrumentation.Http/Implementation/HttpHandlerDiagnosticListener.cs @@ -15,7 +15,7 @@ namespace OpenTelemetry.Instrumentation.Http.Implementation; -internal sealed class HttpHandlerDiagnosticListener : ListenerHandler +internal sealed class HttpHandlerDiagnosticListener(HttpClientTraceInstrumentationOptions options) : ListenerHandler("HttpHandlerDiagnosticListener") { internal static readonly AssemblyName AssemblyName = typeof(HttpHandlerDiagnosticListener).Assembly.GetName(); internal static readonly bool IsNet7OrGreater; @@ -34,7 +34,7 @@ internal sealed class HttpHandlerDiagnosticListener : ListenerHandler private static readonly PropertyFetcher StopResponseFetcher = new("Response"); private static readonly PropertyFetcher StopExceptionFetcher = new("Exception"); private static readonly PropertyFetcher StopRequestStatusFetcher = new("RequestTaskStatus"); - private readonly HttpClientTraceInstrumentationOptions options; + private readonly HttpClientTraceInstrumentationOptions options = options; static HttpHandlerDiagnosticListener() { @@ -48,12 +48,6 @@ static HttpHandlerDiagnosticListener() } } - public HttpHandlerDiagnosticListener(HttpClientTraceInstrumentationOptions options) - : base("HttpHandlerDiagnosticListener") - { - this.options = options; - } - public override void OnEventWritten(string name, object payload) { switch (name) @@ -135,7 +129,7 @@ public void OnStartActivity(Activity activity, object payload) return; } - RequestMethodHelper.SetHttpClientActivityDisplayName(activity, request.Method.Method); + RequestMethodHelper.SetHttpClientActivityDisplayName(activity, request.Method.Method, this.options.KnownHttpMethods); if (!IsNet7OrGreater) { @@ -144,7 +138,7 @@ public void OnStartActivity(Activity activity, object payload) } // see the spec https://github.com/open-telemetry/semantic-conventions/blob/v1.23.0/docs/http/http-spans.md - RequestMethodHelper.SetHttpMethodTag(activity, request.Method.Method); + RequestMethodHelper.SetHttpMethodTag(activity, request.Method.Method, this.options.KnownHttpMethods); activity.SetTag(SemanticConventions.AttributeServerAddress, request.RequestUri.Host); if (!request.RequestUri.IsDefaultPort) diff --git a/src/OpenTelemetry.Instrumentation.Http/Implementation/HttpWebRequestActivitySource.netfx.cs b/src/OpenTelemetry.Instrumentation.Http/Implementation/HttpWebRequestActivitySource.netfx.cs index ea8e88abb5f..ed8f97b5237 100644 --- a/src/OpenTelemetry.Instrumentation.Http/Implementation/HttpWebRequestActivitySource.netfx.cs +++ b/src/OpenTelemetry.Instrumentation.Http/Implementation/HttpWebRequestActivitySource.netfx.cs @@ -95,12 +95,12 @@ internal static HttpClientTraceInstrumentationOptions TracingOptions [MethodImpl(MethodImplOptions.AggressiveInlining)] private static void AddRequestTagsAndInstrumentRequest(HttpWebRequest request, Activity activity) { - RequestMethodHelper.SetHttpClientActivityDisplayName(activity, request.Method); + RequestMethodHelper.SetHttpClientActivityDisplayName(activity, request.Method, tracingOptions.KnownHttpMethods); if (activity.IsAllDataRequested) { // see the spec https://github.com/open-telemetry/semantic-conventions/blob/v1.23.0/docs/http/http-spans.md - RequestMethodHelper.SetHttpMethodTag(activity, request.Method); + RequestMethodHelper.SetHttpMethodTag(activity, request.Method, tracingOptions.KnownHttpMethods); activity.SetTag(SemanticConventions.AttributeServerAddress, request.RequestUri.Host); if (!request.RequestUri.IsDefaultPort) diff --git a/src/Shared/RequestMethodHelper.cs b/src/Shared/RequestMethodHelper.cs index 05a3f022511..556e3893b37 100644 --- a/src/Shared/RequestMethodHelper.cs +++ b/src/Shared/RequestMethodHelper.cs @@ -9,21 +9,13 @@ namespace OpenTelemetry.Internal; -internal static class RequestMethodHelper +internal class RequestMethodHelper { // The value "_OTHER" is used for non-standard HTTP methods. // https://github.com/open-telemetry/semantic-conventions/blob/v1.22.0/docs/http/http-spans.md#common-attributes public const string OtherHttpMethod = "_OTHER"; -#if NET8_0_OR_GREATER - internal static readonly FrozenDictionary KnownMethods; -#else - internal static readonly Dictionary KnownMethods; -#endif - - static RequestMethodHelper() - { - var knownMethodSet = new Dictionary(StringComparer.OrdinalIgnoreCase) + private static readonly Dictionary DefaultKnownMethods = new(StringComparer.OrdinalIgnoreCase) { { "GET", "GET" }, { "PUT", "PUT" }, @@ -36,24 +28,45 @@ static RequestMethodHelper() { "CONNECT", "CONNECT" }, }; - // KnownMethods ignores case. Use the value returned by the dictionary to have a consistent case. #if NET8_0_OR_GREATER - KnownMethods = FrozenDictionary.ToFrozenDictionary(knownMethodSet, StringComparer.OrdinalIgnoreCase); + private readonly FrozenDictionary knownMethods; #else - KnownMethods = knownMethodSet; + private Dictionary knownMethods; #endif + + public RequestMethodHelper(string configuredKnownMethods) + { + var splitArray = configuredKnownMethods.Split(',') + .Select(x => x.Trim()) + .Where(x => !string.IsNullOrEmpty(x)) + .ToList(); + + this.knownMethods = GetKnownMethods(splitArray); + } + + public RequestMethodHelper(List configuredKnownMethods) + { + this.knownMethods = GetKnownMethods(configuredKnownMethods); } - public static string GetNormalizedHttpMethod(string method) +#if NET8_0_OR_GREATER + public string GetNormalizedHttpMethod(string method) +#else + public string GetNormalizedHttpMethod(string method, Dictionary knownMethods = null) +#endif { - return KnownMethods.TryGetValue(method, out var normalizedMethod) + return this.knownMethods.TryGetValue(method, out var normalizedMethod) ? normalizedMethod : OtherHttpMethod; } - public static void SetHttpMethodTag(Activity activity, string method) +#if NET8_0_OR_GREATER + public void SetHttpMethodTag(Activity activity, string method) +#else + public void SetHttpMethodTag(Activity activity, string method) +#endif { - if (KnownMethods.TryGetValue(method, out var normalizedMethod)) + if (this.knownMethods.TryGetValue(method, out var normalizedMethod)) { activity?.SetTag(SemanticConventions.AttributeHttpRequestMethod, normalizedMethod); } @@ -64,9 +77,50 @@ public static void SetHttpMethodTag(Activity activity, string method) } } - public static void SetHttpClientActivityDisplayName(Activity activity, string method) +#if NET8_0_OR_GREATER + public void SetHttpClientActivityDisplayName(Activity activity, string method) +#else + public void SetHttpClientActivityDisplayName(Activity activity, string method, Dictionary knownMethods) +#endif { // https://github.com/open-telemetry/semantic-conventions/blob/v1.23.0/docs/http/http-spans.md#name - activity.DisplayName = KnownMethods.TryGetValue(method, out var httpMethod) ? httpMethod : "HTTP"; + activity.DisplayName = this.knownMethods.TryGetValue(method, out var httpMethod) ? httpMethod : "HTTP"; + } + +#if NET8_0_OR_GREATER + private static FrozenDictionary GetKnownMethods(string configuredKnownMethods) +#else + private static Dictionary GetKnownMethods(string configuredKnownMethods) +#endif + { + var splitArray = configuredKnownMethods.Split(',') + .Select(x => x.Trim()) + .Where(x => !string.IsNullOrEmpty(x)) + .ToList(); + + return GetKnownMethods(splitArray); + } + +#if NET8_0_OR_GREATER + private static FrozenDictionary GetKnownMethods(List configuredKnownMethods) +#else + private static Dictionary GetKnownMethods(List configuredKnownMethods) +#endif + { + IEnumerable> knownMethods = DefaultKnownMethods; + + if (configuredKnownMethods != null) + { + if (configuredKnownMethods.Count > 0) + { + knownMethods = DefaultKnownMethods.Where(x => configuredKnownMethods.Contains(x.Key, StringComparer.InvariantCultureIgnoreCase)); + } + } + +#if NET8_0_OR_GREATER + return FrozenDictionary.ToFrozenDictionary(knownMethods, StringComparer.OrdinalIgnoreCase); +#else + return knownMethods.ToDictionary(x => x.Key, x => x.Value); +#endif } } diff --git a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/BasicTests.cs b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/BasicTests.cs index 2d3f6d2c21a..649815e5ae1 100644 --- a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/BasicTests.cs +++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/BasicTests.cs @@ -8,6 +8,7 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; @@ -1024,6 +1025,112 @@ public async Task DiagnosticSourceExceptionCallBackIsNotReceivedForExceptionsHan Assert.True(exceptionHandled); } + [Theory] + [InlineData("GET", null)] + public async Task KnownHttpMethodsAreBeingRespected_Defaults(string expectedMethod, string expectedOriginalMethod) + { + var exportedItems = new List(); + + using var client = this.factory + .WithWebHostBuilder(builder => + { + builder.ConfigureLogging(loggingBuilder => loggingBuilder.ClearProviders()); + }) + .CreateClient(); + + this.tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddAspNetCoreInstrumentation() + .AddInMemoryExporter(exportedItems) + .Build(); + + using var request = new HttpRequestMessage(HttpMethod.Get, "/api/values"); + using var response = await client.SendAsync(request); + + Assert.Single(exportedItems); + var activity = exportedItems[0]; + + Assert.Equal(expectedMethod, activity.GetTagValue(SemanticConventions.AttributeHttpRequestMethod) as string); + Assert.Equal(expectedOriginalMethod, activity.GetTagValue(SemanticConventions.AttributeHttpRequestMethodOriginal) as string); + } + + [Theory] + [InlineData("GET,POST,PUT", "GET", null)] + [InlineData("POST,PUT", "_OTHER", "GET")] + [InlineData("fooBar", "_OTHER", "GET")] + [InlineData("", "GET", null)] + [InlineData(",", "GET", null)] + public async Task KnownHttpMethodsAreBeingRespected_EnvVar(string knownMethods, string expectedMethod, string expectedOriginalMethod) + { + var config = new KeyValuePair[] { new("OTEL_INSTRUMENTATION_HTTP_KNOWN_METHODS", knownMethods) }; + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(config) + .Build(); + + var exportedItems = new List(); + + using var client = this.factory + .WithWebHostBuilder(builder => + { + builder.ConfigureLogging(loggingBuilder => loggingBuilder.ClearProviders()); + }) + .CreateClient(); + + this.tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddAspNetCoreInstrumentation() + .ConfigureServices(services => services.AddSingleton(configuration)) + .AddInMemoryExporter(exportedItems) + .Build(); + + using var request = new HttpRequestMessage(HttpMethod.Get, "/api/values"); + using var response = await client.SendAsync(request); + + Assert.Single(exportedItems); + var activity = exportedItems[0]; + + Assert.Equal(expectedMethod, activity.GetTagValue(SemanticConventions.AttributeHttpRequestMethod) as string); + Assert.Equal(expectedOriginalMethod, activity.GetTagValue(SemanticConventions.AttributeHttpRequestMethodOriginal) as string); + } + + [Theory] + [InlineData("GET,POST,PUT", "GET", null)] + [InlineData("POST,PUT", "_OTHER", "GET")] + [InlineData("fooBar", "_OTHER", "GET")] + public async Task KnownHttpMethodsAreBeingRespected_Programmatically(string knownMethods, string expectedMethod, string expectedOriginalMethod) + { + var exportedItems = new List(); + + using var client = this.factory + .WithWebHostBuilder(builder => + { + builder.ConfigureLogging(loggingBuilder => loggingBuilder.ClearProviders()); + }) + .CreateClient(); + + this.tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddAspNetCoreInstrumentation(options => + { + var splitArray = knownMethods.Split(',') + .Select(x => x.Trim()) + .Where(x => !string.IsNullOrEmpty(x)); + + foreach (var item in splitArray) + { + options.KnownHttpMethods.Add(item); + } + }) + .AddInMemoryExporter(exportedItems) + .Build(); + + using var request = new HttpRequestMessage(HttpMethod.Get, "/api/values"); + using var response = await client.SendAsync(request); + + Assert.Single(exportedItems); + var activity = exportedItems[0]; + + Assert.Equal(expectedMethod, activity.GetTagValue(SemanticConventions.AttributeHttpRequestMethod) as string); + Assert.Equal(expectedOriginalMethod, activity.GetTagValue(SemanticConventions.AttributeHttpRequestMethodOriginal) as string); + } + public void Dispose() { this.tracerProvider?.Dispose();