[ASP.NET Core] Set `http.request.method` as per spec (#5001)
This commit is contained in:
parent
b45b8a9a12
commit
5cb7a3fca9
|
|
@ -269,6 +269,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Shared", "Shared", "{A49299
|
|||
src\Shared\PeriodicExportingMetricReaderHelper.cs = src\Shared\PeriodicExportingMetricReaderHelper.cs
|
||||
src\Shared\PooledList.cs = src\Shared\PooledList.cs
|
||||
src\Shared\ResourceSemanticConventions.cs = src\Shared\ResourceSemanticConventions.cs
|
||||
src\Shared\RequestMethodHelper.cs = src\Shared\RequestMethodHelper.cs
|
||||
src\Shared\SemanticConventions.cs = src\Shared\SemanticConventions.cs
|
||||
src\Shared\SpanAttributeConstants.cs = src\Shared\SpanAttributeConstants.cs
|
||||
src\Shared\SpanHelper.cs = src\Shared\SpanHelper.cs
|
||||
|
|
|
|||
|
|
@ -2,6 +2,22 @@
|
|||
|
||||
## Unreleased
|
||||
|
||||
* Updated `http.request.method` to match specification guidelines.
|
||||
* For activity, if the method does not belong to one of the [known
|
||||
values](https://github.com/open-telemetry/semantic-conventions/blob/v1.22.0/docs/http/http-spans.md#:~:text=http.request.method%20has%20the%20following%20list%20of%20well%2Dknown%20values)
|
||||
then the request method will be set on an additional tag
|
||||
`http.request.method.original` and `http.request.method` will be set to
|
||||
`_OTHER`.
|
||||
* For metrics, if the original method does not belong to one of the [known
|
||||
values](https://github.com/open-telemetry/semantic-conventions/blob/v1.22.0/docs/http/http-spans.md#:~:text=http.request.method%20has%20the%20following%20list%20of%20well%2Dknown%20values)
|
||||
then `http.request.method` on `http.server.request.duration` metric will be
|
||||
set to `_OTHER`
|
||||
|
||||
`http.request.method` is set on `http.server.request.duration` metric or
|
||||
activity when `OTEL_SEMCONV_STABILITY_OPT_IN` environment variable is set to
|
||||
`http` or `http/dup`.
|
||||
([#5001](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5001))
|
||||
|
||||
## 1.6.0-beta.2
|
||||
|
||||
Released 2023-Oct-26
|
||||
|
|
|
|||
|
|
@ -252,7 +252,16 @@ internal class HttpInListener : ListenerHandler
|
|||
activity.SetTag(SemanticConventions.AttributeUrlQuery, request.QueryString.Value);
|
||||
}
|
||||
|
||||
activity.SetTag(SemanticConventions.AttributeHttpRequestMethod, request.Method);
|
||||
if (RequestMethodHelper.TryResolveHttpMethod(request.Method, out var httpMethod))
|
||||
{
|
||||
activity.SetTag(SemanticConventions.AttributeHttpRequestMethod, httpMethod);
|
||||
}
|
||||
else
|
||||
{
|
||||
activity.SetTag(SemanticConventions.AttributeHttpRequestMethod, httpMethod);
|
||||
activity.SetTag(SemanticConventions.AttributeHttpRequestMethodOriginal, request.Method);
|
||||
}
|
||||
|
||||
activity.SetTag(SemanticConventions.AttributeUrlScheme, request.Scheme);
|
||||
activity.SetTag(SemanticConventions.AttributeUrlPath, path);
|
||||
activity.SetTag(SemanticConventions.AttributeNetworkProtocolVersion, HttpTagHelper.GetFlavorTagValueFromProtocol(request.Protocol));
|
||||
|
|
|
|||
|
|
@ -18,6 +18,8 @@
|
|||
using System.Diagnostics;
|
||||
using System.Diagnostics.Metrics;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using OpenTelemetry.Internal;
|
||||
|
||||
#if NET6_0_OR_GREATER
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
#endif
|
||||
|
|
@ -150,8 +152,9 @@ internal sealed class HttpInMetricsListener : ListenerHandler
|
|||
tags.Add(new KeyValuePair<string, object>(SemanticConventions.AttributeNetworkProtocolName, NetworkProtocolName));
|
||||
tags.Add(new KeyValuePair<string, object>(SemanticConventions.AttributeNetworkProtocolVersion, HttpTagHelper.GetFlavorTagValueFromProtocol(context.Request.Protocol)));
|
||||
tags.Add(new KeyValuePair<string, object>(SemanticConventions.AttributeUrlScheme, context.Request.Scheme));
|
||||
tags.Add(new KeyValuePair<string, object>(SemanticConventions.AttributeHttpRequestMethod, context.Request.Method));
|
||||
tags.Add(new KeyValuePair<string, object>(SemanticConventions.AttributeHttpResponseStatusCode, TelemetryHelper.GetBoxedStatusCode(context.Response.StatusCode)));
|
||||
RequestMethodHelper.TryResolveHttpMethod(context.Request.Method, out var httpMethod);
|
||||
tags.Add(new KeyValuePair<string, object>(SemanticConventions.AttributeHttpRequestMethod, httpMethod));
|
||||
|
||||
#if NET6_0_OR_GREATER
|
||||
var route = (context.GetEndpoint() as RouteEndpoint)?.RoutePattern.RawText;
|
||||
|
|
|
|||
|
|
@ -42,8 +42,9 @@ public static class MeterProviderBuilderExtensions
|
|||
#if NET8_0_OR_GREATER
|
||||
return builder.ConfigureMeters();
|
||||
#else
|
||||
// Note: Warm-up the status code mapping.
|
||||
// Note: Warm-up the status code and method mapping.
|
||||
_ = TelemetryHelper.BoxedStatusCodes;
|
||||
_ = RequestMethodHelper.KnownMethods;
|
||||
|
||||
builder.ConfigureServices(services =>
|
||||
{
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@
|
|||
<Compile Include="$(RepoRoot)\src\OpenTelemetry.Instrumentation.GrpcNetClient\GrpcTagHelper.cs" Link="Includes\GrpcTagHelper.cs" />
|
||||
<Compile Include="$(RepoRoot)\src\OpenTelemetry.Instrumentation.GrpcNetClient\StatusCanonicalCode.cs" Link="Includes\StatusCanonicalCode.cs" />
|
||||
<Compile Include="$(RepoRoot)\src\Shared\Guard.cs" Link="Includes\Guard.cs" />
|
||||
<Compile Include="$(RepoRoot)\src\Shared\RequestMethodHelper.cs" Link="Includes\RequestMethodHelper.cs" />
|
||||
<Compile Include="$(RepoRoot)\src\Shared\EnvironmentVariables\*.cs" Link="Includes\EnvironmentVariables\%(Filename).cs" Condition="'$(TargetFramework)' == 'netstandard2.0' OR '$(TargetFramework)' == 'netstandard2.1'" />
|
||||
<Compile Include="$(RepoRoot)\src\Shared\HttpSemanticConventionHelper.cs" Link="Includes\HttpSemanticConventionHelper.cs" />
|
||||
<Compile Include="$(RepoRoot)\src\Shared\Options\*.cs" Link="Includes\Options\%(Filename).cs" />
|
||||
|
|
|
|||
|
|
@ -63,8 +63,9 @@ public static class TracerProviderBuilderExtensions
|
|||
{
|
||||
Guard.ThrowIfNull(builder);
|
||||
|
||||
// Note: Warm-up the status code mapping.
|
||||
// Note: Warm-up the status code and method mapping.
|
||||
_ = TelemetryHelper.BoxedStatusCodes;
|
||||
_ = RequestMethodHelper.KnownMethods;
|
||||
|
||||
name ??= Options.DefaultName;
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,77 @@
|
|||
// <copyright file="RequestMethodHelper.cs" company="OpenTelemetry Authors">
|
||||
// Copyright The OpenTelemetry Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
// </copyright>
|
||||
|
||||
#if NET8_0_OR_GREATER
|
||||
using System.Collections.Frozen;
|
||||
#endif
|
||||
|
||||
namespace OpenTelemetry.Internal;
|
||||
|
||||
internal static class RequestMethodHelper
|
||||
{
|
||||
#if NET8_0_OR_GREATER
|
||||
internal static readonly FrozenDictionary<string, string> KnownMethods;
|
||||
#else
|
||||
internal static readonly Dictionary<string, string> KnownMethods;
|
||||
#endif
|
||||
|
||||
static RequestMethodHelper()
|
||||
{
|
||||
#if NET8_0_OR_GREATER
|
||||
KnownMethods = FrozenDictionary.ToFrozenDictionary(
|
||||
new[]
|
||||
{
|
||||
KeyValuePair.Create("GET", "GET"),
|
||||
KeyValuePair.Create("PUT", "PUT"),
|
||||
KeyValuePair.Create("POST", "POST"),
|
||||
KeyValuePair.Create("DELETE", "DELETE"),
|
||||
KeyValuePair.Create("HEAD", "HEAD"),
|
||||
KeyValuePair.Create("OPTIONS", "OPTIONS"),
|
||||
KeyValuePair.Create("TRACE", "TRACE"),
|
||||
KeyValuePair.Create("PATCH", "PATCH"),
|
||||
KeyValuePair.Create("CONNECT", "CONNECT"),
|
||||
},
|
||||
StringComparer.OrdinalIgnoreCase);
|
||||
#else
|
||||
KnownMethods = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
{ "GET", "GET" },
|
||||
{ "PUT", "PUT" },
|
||||
{ "POST", "POST" },
|
||||
{ "DELETE", "DELETE" },
|
||||
{ "HEAD", "HEAD" },
|
||||
{ "OPTIONS", "OPTIONS" },
|
||||
{ "TRACE", "TRACE" },
|
||||
{ "PATCH", "PATCH" },
|
||||
{ "CONNECT", "CONNECT" },
|
||||
};
|
||||
#endif
|
||||
}
|
||||
|
||||
public static bool TryResolveHttpMethod(string method, out string resolvedMethod)
|
||||
{
|
||||
if (KnownMethods.TryGetValue(method, out resolvedMethod))
|
||||
{
|
||||
// KnownMethods ignores case. Use the value returned by the dictionary to have a consistent case.
|
||||
return true;
|
||||
}
|
||||
|
||||
// Set to default "_OTHER" as per spec.
|
||||
// https://github.com/open-telemetry/semantic-conventions/blob/v1.22.0/docs/http/http-spans.md#common-attributes
|
||||
resolvedMethod = "_OTHER";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
@ -129,4 +129,5 @@ internal static class SemanticConventions
|
|||
public const string AttributeUrlScheme = "url.scheme"; // replaces: "http.scheme" (AttributeHttpScheme)
|
||||
public const string AttributeUrlQuery = "url.query";
|
||||
public const string AttributeUserAgentOriginal = "user_agent.original"; // replaces: "http.user_agent" (AttributeHttpUserAgent)
|
||||
public const string AttributeHttpRequestMethodOriginal = "http.request.method_original";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ using Microsoft.AspNetCore.Hosting;
|
|||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc.Testing;
|
||||
using Microsoft.AspNetCore.TestHost;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Moq;
|
||||
|
|
@ -32,6 +33,8 @@ using TestApp.AspNetCore;
|
|||
using TestApp.AspNetCore.Filters;
|
||||
using Xunit;
|
||||
|
||||
using static OpenTelemetry.Internal.HttpSemanticConventionHelper;
|
||||
|
||||
namespace OpenTelemetry.Instrumentation.AspNetCore.Tests;
|
||||
|
||||
// See https://github.com/aspnet/Docs/tree/master/aspnetcore/test/integration-tests/samples/2.x/IntegrationTestsSample
|
||||
|
|
@ -647,6 +650,78 @@ public sealed class BasicTests
|
|||
Assert.Equal("api/Values/{id}", aspnetcoreframeworkactivity.DisplayName);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("CONNECT", "CONNECT")]
|
||||
[InlineData("DELETE", "DELETE")]
|
||||
[InlineData("GET", "GET")]
|
||||
[InlineData("PUT", "PUT")]
|
||||
[InlineData("HEAD", "HEAD")]
|
||||
[InlineData("OPTIONS", "OPTIONS")]
|
||||
[InlineData("PATCH", "PATCH")]
|
||||
[InlineData("Get", "GET")]
|
||||
[InlineData("POST", "POST")]
|
||||
[InlineData("TRACE", "TRACE")]
|
||||
[InlineData("CUSTOM", "_OTHER")]
|
||||
public async Task HttpRequestMethodIsSetAsPerSpec(string originalMethod, string expectedMethod)
|
||||
{
|
||||
var exportedItems = new List<Activity>();
|
||||
|
||||
var configuration = new ConfigurationBuilder()
|
||||
.AddInMemoryCollection(new Dictionary<string, string> { [SemanticConventionOptInKeyName] = "http" })
|
||||
.Build();
|
||||
|
||||
void ConfigureTestServices(IServiceCollection services)
|
||||
{
|
||||
this.tracerProvider = Sdk.CreateTracerProviderBuilder()
|
||||
.ConfigureServices(services => services.AddSingleton<IConfiguration>(configuration))
|
||||
.AddAspNetCoreInstrumentation()
|
||||
.AddInMemoryExporter(exportedItems)
|
||||
.Build();
|
||||
}
|
||||
|
||||
// Arrange
|
||||
using var client = this.factory
|
||||
.WithWebHostBuilder(builder =>
|
||||
{
|
||||
builder.ConfigureTestServices(ConfigureTestServices);
|
||||
builder.ConfigureLogging(loggingBuilder => loggingBuilder.ClearProviders());
|
||||
})
|
||||
.CreateClient();
|
||||
|
||||
var message = new HttpRequestMessage();
|
||||
|
||||
message.Method = new HttpMethod(originalMethod);
|
||||
|
||||
try
|
||||
{
|
||||
using var response = await client.SendAsync(message).ConfigureAwait(false);
|
||||
response.EnsureSuccessStatusCode();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignore error.
|
||||
}
|
||||
|
||||
WaitForActivityExport(exportedItems, 1);
|
||||
|
||||
Assert.Single(exportedItems);
|
||||
|
||||
var activity = exportedItems[0];
|
||||
|
||||
Assert.Contains(activity.TagObjects, t => t.Key == SemanticConventions.AttributeHttpRequestMethod);
|
||||
|
||||
if (originalMethod.Equals(expectedMethod, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
Assert.DoesNotContain(activity.TagObjects, t => t.Key == SemanticConventions.AttributeHttpRequestMethodOriginal);
|
||||
}
|
||||
else
|
||||
{
|
||||
Assert.Equal(originalMethod, activity.GetTagValue(SemanticConventions.AttributeHttpRequestMethodOriginal) as string);
|
||||
}
|
||||
|
||||
Assert.Equal(expectedMethod, activity.GetTagValue(SemanticConventions.AttributeHttpRequestMethod) as string);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ActivitiesStartedInMiddlewareBySettingHostActivityToNullShouldNotBeUpdated()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -237,6 +237,82 @@ public class MetricTests
|
|||
expectedTagsCount: 6);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("CONNECT", "CONNECT")]
|
||||
[InlineData("DELETE", "DELETE")]
|
||||
[InlineData("GET", "GET")]
|
||||
[InlineData("PUT", "PUT")]
|
||||
[InlineData("HEAD", "HEAD")]
|
||||
[InlineData("OPTIONS", "OPTIONS")]
|
||||
[InlineData("PATCH", "PATCH")]
|
||||
[InlineData("Get", "GET")]
|
||||
[InlineData("POST", "POST")]
|
||||
[InlineData("TRACE", "TRACE")]
|
||||
[InlineData("CUSTOM", "_OTHER")]
|
||||
public async Task HttpRequestMethodIsCapturedAsPerSpec(string originalMethod, string expectedMethod)
|
||||
{
|
||||
var configuration = new ConfigurationBuilder()
|
||||
.AddInMemoryCollection(new Dictionary<string, string> { [SemanticConventionOptInKeyName] = "http" })
|
||||
.Build();
|
||||
|
||||
var metricItems = new List<Metric>();
|
||||
|
||||
this.meterProvider = Sdk.CreateMeterProviderBuilder()
|
||||
.ConfigureServices(services => services.AddSingleton<IConfiguration>(configuration))
|
||||
.AddAspNetCoreInstrumentation()
|
||||
.AddInMemoryExporter(metricItems)
|
||||
.Build();
|
||||
|
||||
using var client = this.factory
|
||||
.WithWebHostBuilder(builder =>
|
||||
{
|
||||
builder.ConfigureLogging(loggingBuilder => loggingBuilder.ClearProviders());
|
||||
})
|
||||
.CreateClient();
|
||||
|
||||
var message = new HttpRequestMessage();
|
||||
message.Method = new HttpMethod(originalMethod);
|
||||
|
||||
try
|
||||
{
|
||||
using var response = await client.SendAsync(message).ConfigureAwait(false);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignore error.
|
||||
}
|
||||
|
||||
// We need to let End callback execute as it is executed AFTER response was returned.
|
||||
// In unit tests environment there may be a lot of parallel unit tests executed, so
|
||||
// giving some breezing room for the End callback to complete
|
||||
await Task.Delay(TimeSpan.FromSeconds(1)).ConfigureAwait(false);
|
||||
|
||||
this.meterProvider.Dispose();
|
||||
|
||||
var requestMetrics = metricItems
|
||||
.Where(item => item.Name == "http.server.request.duration")
|
||||
.ToArray();
|
||||
|
||||
var metric = Assert.Single(requestMetrics);
|
||||
|
||||
Assert.Equal("s", metric.Unit);
|
||||
var metricPoints = GetMetricPoints(metric);
|
||||
Assert.Single(metricPoints);
|
||||
|
||||
var mp = metricPoints[0];
|
||||
|
||||
// Inspect Metric Attributes
|
||||
var attributes = new Dictionary<string, object>();
|
||||
foreach (var tag in mp.Tags)
|
||||
{
|
||||
attributes[tag.Key] = tag.Value;
|
||||
}
|
||||
|
||||
Assert.Contains(attributes, kvp => kvp.Key == SemanticConventions.AttributeHttpRequestMethod && kvp.Value.ToString() == expectedMethod);
|
||||
|
||||
Assert.DoesNotContain(attributes, t => t.Key == SemanticConventions.AttributeHttpRequestMethodOriginal);
|
||||
}
|
||||
|
||||
#if !NET8_0_OR_GREATER
|
||||
[Fact]
|
||||
public async Task RequestMetricIsCaptured_Old()
|
||||
|
|
|
|||
Loading…
Reference in New Issue