Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Instrumentation.AspNet] Update semantic conventions for metrics #1429

Merged
merged 21 commits into from
Dec 12, 2023
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions src/OpenTelemetry.Instrumentation.AspNet/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,24 @@
* New overload of `AddAspNetInstrumentation` now accepts a configuration delegate.
* The `Enrich` can be used to add additional metric attributes.

* HTTP server metrics now follow stable
qhris marked this conversation as resolved.
Show resolved Hide resolved
[semantic conventions](https://github.com/open-telemetry/semantic-conventions/blob/v1.23.0/docs/http/http-metrics.md#http-server).

The `http.request.duration` metric is replaced with `http.server.request.duration`.
Note that the unit changes from milliseconds to seconds.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also mention the updated buckets. Check here

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm so glad you posted this, I just realized I didn't check this properly when testing.

The buckets have not been updated, the implementation here does not actually take OpenTelemetry.Instrumentation.AspNet into consideration.

I updated the CHANGELOG, fixed it, and added tests for it locally now. But if we want it fixed upstream this PR will have to wait.
I also had to pull in OpenTelemetry as a dependency as that's the only way to access AddView.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@qhris Good catch. We should not be adding SDK adding dependency to instrumentation as Cijo mentioned. Could you remove that? and send a PR upstream to include ASPNET in the list?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I made the suggested changes, and I also updated the buckets to remove the zero bucket as was done in open-telemetry/opentelemetry-dotnet#5021.
I posted a link to the upstream PR in response to Cijo.

([#1429](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/1429)):

The following metric attributes has been added:

* `http.request.method` (previously `http.method`)
* `http.response.status_code` (previously `http.status_code`)
* `url.scheme` (previously `http.scheme`)
* `server.address`
* `server.port`
* `network.protocol.name` (http)
* `network.protocol.version` (`1.1`, `2`, `3`)
* `http.route`

## 1.6.0-beta.2

Released 2023-Nov-06
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
using System;
using System.Diagnostics;
using System.Web;
using System.Web.Routing;
using OpenTelemetry.Context.Propagation;
using OpenTelemetry.Internal;
using OpenTelemetry.Trace;
Expand All @@ -26,8 +25,7 @@ namespace OpenTelemetry.Instrumentation.AspNet.Implementation;

internal sealed class HttpInListener : IDisposable
{
private readonly PropertyFetcher<object> routeFetcher = new("Route");
private readonly PropertyFetcher<string> routeTemplateFetcher = new("RouteTemplate");
private readonly HttpRequestRouteHelper routeHelper = new();
private readonly AspNetInstrumentationOptions options;

public HttpInListener(AspNetInstrumentationOptions options)
Expand Down Expand Up @@ -133,26 +131,7 @@ private void OnStopActivity(Activity activity, HttpContext context)
activity.SetStatus(SpanHelper.ResolveActivityStatusForHttpStatusCode(activity.Kind, response.StatusCode));
}

var routeData = context.Request.RequestContext.RouteData;

string? template = null;
if (routeData.Values.TryGetValue("MS_SubRoutes", out object msSubRoutes))
{
// WebAPI attribute routing flows here. Use reflection to not take a dependency on microsoft.aspnet.webapi.core\[version]\lib\[framework]\System.Web.Http.

if (msSubRoutes is Array attributeRouting && attributeRouting.Length == 1)
{
var subRouteData = attributeRouting.GetValue(0);

_ = this.routeFetcher.TryFetch(subRouteData, out var route);
_ = this.routeTemplateFetcher.TryFetch(route, out template);
}
}
else if (routeData.Route is Route route)
{
// MVC + WebAPI traditional routing & MVC attribute routing flow here.
template = route.Url;
}
var template = this.routeHelper.GetRouteTemplate(context.Request);

if (!string.IsNullOrEmpty(template))
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,17 @@ namespace OpenTelemetry.Instrumentation.AspNet.Implementation;

internal sealed class HttpInMetricsListener : IDisposable
{
private const string NetworkProtocolName = "http";
private readonly HttpRequestRouteHelper routeHelper = new();
private readonly Histogram<double> httpServerDuration;
private readonly AspNetMetricsInstrumentationOptions options;

public HttpInMetricsListener(Meter meter, AspNetMetricsInstrumentationOptions options)
{
this.httpServerDuration = meter.CreateHistogram<double>("http.server.duration", "ms", "Measures the duration of inbound HTTP requests.");
this.httpServerDuration = meter.CreateHistogram<double>(
"http.server.request.duration",
unit: "s",
description: "Measures the duration of inbound HTTP requests.");
TelemetryHttpModule.Options.OnRequestStoppedCallback += this.OnStopActivity;
this.options = options;
}
Expand All @@ -39,17 +44,44 @@ public void Dispose()
TelemetryHttpModule.Options.OnRequestStoppedCallback -= this.OnStopActivity;
}

private static string GetHttpProtocolVersion(HttpRequest request)
{
var protocol = request.ServerVariables["SERVER_PROTOCOL"];
return protocol switch
{
"HTTP/1.1" => "1.1",
"HTTP/2" => "2",
"HTTP/3" => "3",
_ => protocol,
};
}

private void OnStopActivity(Activity activity, HttpContext context)
{
// TODO: This is just a minimal set of attributes. See the spec for additional attributes:
// https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/semantic_conventions/http-metrics.md#http-server
var request = context.Request;
var url = request.Url;
var tags = new TagList
{
{ SemanticConventions.AttributeHttpMethod, context.Request.HttpMethod },
{ SemanticConventions.AttributeHttpScheme, context.Request.Url.Scheme },
{ SemanticConventions.AttributeHttpStatusCode, context.Response.StatusCode },
{ SemanticConventions.AttributeServerAddress, url.Host },
{ SemanticConventions.AttributeServerPort, url.Port },
{ SemanticConventions.AttributeNetworkProtocolName, NetworkProtocolName },
qhris marked this conversation as resolved.
Show resolved Hide resolved
{ SemanticConventions.AttributeHttpRequestMethod, request.HttpMethod },
qhris marked this conversation as resolved.
Show resolved Hide resolved
{ SemanticConventions.AttributeUrlScheme, url.Scheme },
{ SemanticConventions.AttributeHttpResponseStatusCode, context.Response.StatusCode },
};

var protocolVersion = GetHttpProtocolVersion(request);
if (!string.IsNullOrEmpty(protocolVersion))
{
tags.Add(SemanticConventions.AttributeNetworkProtocolVersion, protocolVersion);
}

var template = this.routeHelper.GetRouteTemplate(request);
if (!string.IsNullOrEmpty(template))
{
tags.Add(SemanticConventions.AttributeHttpRoute, template);
}

if (this.options.Enrich is not null)
{
try
Expand All @@ -62,6 +94,6 @@ private void OnStopActivity(Activity activity, HttpContext context)
}
}

this.httpServerDuration.Record(activity.Duration.TotalMilliseconds, tags);
this.httpServerDuration.Record(activity.Duration.TotalSeconds, tags);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// <copyright file="HttpRequestRouteHelper.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>

using System;
using System.Web;
using System.Web.Routing;

namespace OpenTelemetry.Instrumentation.AspNet.Implementation;

/// <summary>
/// Helper class for processing http requests.
/// </summary>
internal class HttpRequestRouteHelper
qhris marked this conversation as resolved.
Show resolved Hide resolved
{
private readonly PropertyFetcher<object> routeFetcher = new("Route");
private readonly PropertyFetcher<string> routeTemplateFetcher = new("RouteTemplate");

/// <summary>
/// Extracts the route template from the <see cref="HttpRequest"/>.
/// </summary>
/// <param name="request">The <see cref="HttpRequest"/> being processed.</param>
/// <returns>The route template or <see langword="null"/>.</returns>
internal string? GetRouteTemplate(HttpRequest request)
{
var routeData = request.RequestContext.RouteData;

string? template = null;
if (routeData.Values.TryGetValue("MS_SubRoutes", out object msSubRoutes))
{
// WebAPI attribute routing flows here. Use reflection to not take a dependency on microsoft.aspnet.webapi.core\[version]\lib\[framework]\System.Web.Http.

if (msSubRoutes is Array attributeRouting && attributeRouting.Length == 1)
{
var subRouteData = attributeRouting.GetValue(0);

_ = this.routeFetcher.TryFetch(subRouteData, out var route);
_ = this.routeTemplateFetcher.TryFetch(route, out template);
}
}
else if (routeData.Route is Route route)
{
// MVC + WebAPI traditional routing & MVC attribute routing flow here.
template = route.Url;
}

return template;
}
}
7 changes: 7 additions & 0 deletions src/Shared/SemanticConventions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -114,5 +114,12 @@ internal static class SemanticConventions
public const string AttributeHttpRequestMethod = "http.request.method"; // replaces: "http.method" (AttributeHttpMethod)
public const string AttributeHttpResponseStatusCode = "http.response.status_code"; // replaces: "http.status_code" (AttributeHttpStatusCode)
public const string AttributeUrlScheme = "url.scheme"; // replaces: "http.scheme" (AttributeHttpScheme)

// v1.23.0
// https://github.com/open-telemetry/semantic-conventions/blob/v1.23.0/docs/http/http-metrics.md#http-server
public const string AttributeNetworkProtocolName = "network.protocol.name"; // replaces: "net.transport" (AttributeNetTransport)
qhris marked this conversation as resolved.
Show resolved Hide resolved
public const string AttributeNetworkProtocolVersion = "network.protocol.version"; // replaces: "http.flavor" (AttributeHttpFlavor)
public const string AttributeServerAddress = "server.address"; // replaces: "net.host.name" (AttributeNetHostName)
public const string AttributeServerPort = "server.port"; // replaces: "net.host.port" (AttributeNetHostPort)
#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,27 +29,33 @@ namespace OpenTelemetry.Instrumentation.AspNet.Tests;
public class HttpInMetricsListenerTests
{
[Theory]
[InlineData("http://localhost/", 0, null, null, "http")]
[InlineData("https://localhost/", 0, null, null, "https")]
[InlineData("http://localhost/api/value", 0, null, null, "http")]
[InlineData("http://localhost/api/value", 1, "{controller}/{action}", null, "http")]
[InlineData("http://localhost/api/value", 2, "{controller}/{action}", null, "http")]
[InlineData("http://localhost/api/value", 3, "{controller}/{action}", null, "http")]
[InlineData("http://localhost/api/value", 4, "{controller}/{action}", null, "http")]
[InlineData("http://localhost:8080/api/value", 0, null, null, "http")]
[InlineData("http://localhost:8080/api/value", 1, "{controller}/{action}", null, "http")]
[InlineData("http://localhost:8080/api/value", 3, "{controller}/{action}", "enrich", "http")]
[InlineData("http://localhost:8080/api/value", 3, "{controller}/{action}", "throw", "http")]
[InlineData("http://localhost:8080/api/value", 3, "{controller}/{action}", null, "http")]
[InlineData("http://localhost/", 0, null, null, "http", "localhost", null, 80, 200)]
[InlineData("https://localhost/", 0, null, null, "https", "localhost", null, 443, 200)]
[InlineData("http://localhost/api/value", 0, null, null, "http", "localhost", null, 80, 200)]
[InlineData("http://localhost/api/value", 1, "{controller}/{action}", null, "http", "localhost", "{controller}/{action}", 80, 200)]
[InlineData("http://localhost/api/value", 2, "{controller}/{action}", null, "http", "localhost", "{controller}/{action}", 80, 201)]
[InlineData("http://localhost/api/value", 3, "{controller}/{action}", null, "http", "localhost", "{controller}/{action}", 80, 200)]
[InlineData("http://localhost/api/value", 4, "{controller}/{action}", null, "http", "localhost", "{controller}/{action}", 80, 200)]
[InlineData("http://localhost/api/value", 1, "{controller}/{action}", null, "http", "localhost", "{controller}/{action}", 80, 500)]
[InlineData("http://localhost:8080/api/value", 0, null, null, "http", "localhost", null, 8080, 200)]
[InlineData("http://localhost:8080/api/value", 1, "{controller}/{action}", null, "http", "localhost", "{controller}/{action}", 8080, 200)]
[InlineData("http://localhost:8080/api/value", 3, "{controller}/{action}", "enrich", "http", "localhost", "{controller}/{action}", 8080, 200)]
[InlineData("http://localhost:8080/api/value", 3, "{controller}/{action}", "throw", "http", "localhost", "{controller}/{action}", 8080, 200)]
[InlineData("http://localhost:8080/api/value", 3, "{controller}/{action}", null, "http", "localhost", "{controller}/{action}", 8080, 200)]
public void AspNetMetricTagsAreCollectedSuccessfully(
string url,
int routeType,
string routeTemplate,
string enrichMode,
string expectedScheme)
string expectedScheme,
string expectedHost,
string expectedRoute,
int? expectedPort,
int expectedStatus)
{
double duration = 0;
HttpContext.Current = RouteTestHelper.BuildHttpContext(url, routeType, routeTemplate);
HttpContext.Current.Response.StatusCode = expectedStatus;

// This is to enable activity creation
// as it is created using ActivitySource inside TelemetryHttpModule
Expand All @@ -60,7 +66,7 @@ public void AspNetMetricTagsAreCollectedSuccessfully(
{
if (eventName.Equals("OnStopActivity"))
{
duration = activity.Duration.TotalMilliseconds;
duration = activity.Duration.TotalSeconds;
}
})
.Build();
Expand Down Expand Up @@ -107,12 +113,19 @@ public void AspNetMetricTagsAreCollectedSuccessfully(
var sum = metricPoint.GetHistogramSum();

Assert.Equal(MetricType.Histogram, exportedItems[0].MetricType);
Assert.Equal("http.server.duration", exportedItems[0].Name);
Assert.Equal("http.server.request.duration", exportedItems[0].Name);
Assert.Equal("s", exportedItems[0].Unit);
Assert.Equal(1L, count);
Assert.Equal(duration, sum);
Assert.True(duration > 0, "Metric duration should be set.");

var expectedTagCount = 3;
var expectedTagCount = 6;

if (!string.IsNullOrEmpty(expectedRoute))
{
expectedTagCount++;
}

if (enrichMode == "enrich")
{
expectedTagCount++;
Expand All @@ -130,9 +143,17 @@ public void AspNetMetricTagsAreCollectedSuccessfully(
ExpectTag("true", "enriched");
}

ExpectTag("GET", SemanticConventions.AttributeHttpMethod);
ExpectTag(200, SemanticConventions.AttributeHttpStatusCode);
ExpectTag(expectedScheme, SemanticConventions.AttributeHttpScheme);
// Do not use constants from SemanticConventions here in order to detect mistakes.
// https://github.com/open-telemetry/semantic-conventions/blob/v1.23.0/docs/http/http-metrics.md#http-server
// Unable to check for "network.protocol.version" because we can't set server variables due to the accessability
qhris marked this conversation as resolved.
Show resolved Hide resolved
// of the ServerVariables property.
ExpectTag("GET", "http.request.method");
ExpectTag(expectedStatus, "http.response.status_code");
ExpectTag(expectedRoute, "http.route");
ExpectTag("http", "network.protocol.name");
ExpectTag(expectedHost, "server.address");
ExpectTag(expectedPort, "server.port");
ExpectTag(expectedScheme, "url.scheme");

void ExpectTag<T>(T? expected, string tagName)
{
Expand Down
Loading