From f2ab97636c7d6bfbc8c9319bbfc342ed44566fef Mon Sep 17 00:00:00 2001
From: Utkarsh Umesan Pillai <66651184+utpilla@users.noreply.github.com>
Date: Mon, 6 Nov 2023 12:37:18 -0800
Subject: [PATCH 1/5] [repo] Update Microsoft.Extensions.* packages (#5015)
---
Directory.Packages.props | 20 ++++++++++---------
.../CHANGELOG.md | 4 ++++
.../CHANGELOG.md | 4 ++++
.../CHANGELOG.md | 4 ++++
4 files changed, 23 insertions(+), 9 deletions(-)
diff --git a/Directory.Packages.props b/Directory.Packages.props
index fb81a09ab03..21852b666b0 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -16,23 +16,24 @@
-
-
-
-
-
+
+
+
+
-
+
+
@@ -75,6 +76,7 @@
+
diff --git a/src/OpenTelemetry.Api.ProviderBuilderExtensions/CHANGELOG.md b/src/OpenTelemetry.Api.ProviderBuilderExtensions/CHANGELOG.md
index 8e124c39ecb..c4abeb17c12 100644
--- a/src/OpenTelemetry.Api.ProviderBuilderExtensions/CHANGELOG.md
+++ b/src/OpenTelemetry.Api.ProviderBuilderExtensions/CHANGELOG.md
@@ -2,6 +2,10 @@
## Unreleased
+* Updated `Microsoft.Extensions.DependencyInjection.Abstractions` package
+ version to `8.0.0-rc.2.23479.6`.
+ ([#5015](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5015))
+
## 1.7.0-alpha.1
Released 2023-Oct-16
diff --git a/src/OpenTelemetry.Extensions.Hosting/CHANGELOG.md b/src/OpenTelemetry.Extensions.Hosting/CHANGELOG.md
index 3739a6d6f55..3032509ec82 100644
--- a/src/OpenTelemetry.Extensions.Hosting/CHANGELOG.md
+++ b/src/OpenTelemetry.Extensions.Hosting/CHANGELOG.md
@@ -2,6 +2,10 @@
## Unreleased
+* Updated `Microsoft.Extensions.Hosting.Abstractions` package
+ version to `8.0.0-rc.2.23479.6`.
+ ([#5015](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5015))
+
## 1.7.0-alpha.1
Released 2023-Oct-16
diff --git a/src/OpenTelemetry.Instrumentation.SqlClient/CHANGELOG.md b/src/OpenTelemetry.Instrumentation.SqlClient/CHANGELOG.md
index 07d8187ca5a..d09e31da0bb 100644
--- a/src/OpenTelemetry.Instrumentation.SqlClient/CHANGELOG.md
+++ b/src/OpenTelemetry.Instrumentation.SqlClient/CHANGELOG.md
@@ -2,6 +2,10 @@
## Unreleased
+* Updated `Microsoft.Extensions.Configuration` and
+ `Microsoft.Extensions.Options` package version to `8.0.0-rc.2.23479.6`.
+ ([#5015](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5015))
+
## 1.6.0-beta.2
Released 2023-Oct-26
From 91ed41fcd972eef227eb7ac7ab70bdefd62f9c3f Mon Sep 17 00:00:00 2001
From: Cijo Thomas
Date: Wed, 8 Nov 2023 14:47:32 -0800
Subject: [PATCH 2/5] Update SIG meeting time to 9AM (#5031)
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index 996ec89fd0d..c8b3915f23a 100644
--- a/README.md
+++ b/README.md
@@ -98,7 +98,7 @@ extension scenarios:
See [CONTRIBUTING.md](CONTRIBUTING.md)
-We meet weekly on Tuesdays, and the time of the meeting alternates between 11AM
+We meet weekly on Tuesdays, and the time of the meeting alternates between 9AM
PT and 4PM PT. The meeting is subject to change depending on contributors'
availability. Check the [OpenTelemetry community
calendar](https://calendar.google.com/calendar/embed?src=google.com_b79e3e90j7bbsa2n2p5an5lf60%40group.calendar.google.com)
From 4a3c8d36b3d1dda97d0f3e0721e502619ddfe83a Mon Sep 17 00:00:00 2001
From: Vishwesh Bankwar
Date: Wed, 8 Nov 2023 15:50:42 -0800
Subject: [PATCH 3/5] [HttpClient] Add `error.type` for traces and metrics
(#5005)
Co-authored-by: Utkarsh Umesan Pillai <66651184+utpilla@users.noreply.github.com>
Co-authored-by: Timothy Mothra
Co-authored-by: Mikel Blanchard
---
.../CHANGELOG.md | 16 ++
.../HttpHandlerDiagnosticListener.cs | 48 ++++-
.../HttpHandlerMetricsDiagnosticListener.cs | 190 +++++++++++++-----
.../HttpClientTests.cs | 82 +++++++-
.../http-out-test-cases.json | 18 --
5 files changed, 273 insertions(+), 81 deletions(-)
diff --git a/src/OpenTelemetry.Instrumentation.Http/CHANGELOG.md b/src/OpenTelemetry.Instrumentation.Http/CHANGELOG.md
index 4b7bbb8cd3e..bc19fe9c4fb 100644
--- a/src/OpenTelemetry.Instrumentation.Http/CHANGELOG.md
+++ b/src/OpenTelemetry.Instrumentation.Http/CHANGELOG.md
@@ -18,6 +18,22 @@
`http` or `http/dup`.
([#5003](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5003))
+* An additional attribute `error.type` will be added to activity and
+ `http.client.request.duration` metric in case of failed requests as per the
+ [specification](https://github.com/open-telemetry/semantic-conventions/blob/v1.23.0/docs/http/http-spans.md#common-attributes).
+
+ Users moving to `net8.0` or newer frameworks from lower versions will see
+ difference in values in case of an exception. `net8.0` or newer frameworks add
+ the ability to further drill down the exceptions to a specific type through
+ [HttpRequestError](https://learn.microsoft.com/dotnet/api/system.net.http.httprequesterror?view=net-8.0)
+ enum. For lower versions, the individual types will be rolled in to a single
+ type. This could be a **breaking change** if alerts are set based on the values.
+
+ The attribute will only be added when `OTEL_SEMCONV_STABILITY_OPT_IN`
+ environment variable is set to `http` or `http/dup`.
+
+ ([#5005](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5005))
+
## 1.6.0-beta.2
Released 2023-Oct-26
diff --git a/src/OpenTelemetry.Instrumentation.Http/Implementation/HttpHandlerDiagnosticListener.cs b/src/OpenTelemetry.Instrumentation.Http/Implementation/HttpHandlerDiagnosticListener.cs
index b5d4c85fc5e..c9234e4cdcc 100644
--- a/src/OpenTelemetry.Instrumentation.Http/Implementation/HttpHandlerDiagnosticListener.cs
+++ b/src/OpenTelemetry.Instrumentation.Http/Implementation/HttpHandlerDiagnosticListener.cs
@@ -271,6 +271,11 @@ public void OnStopActivity(Activity activity, object payload)
if (TryFetchResponse(payload, out HttpResponseMessage response))
{
+ if (currentStatusCode == ActivityStatusCode.Unset)
+ {
+ activity.SetStatus(SpanHelper.ResolveSpanStatusForHttpStatusCode(activity.Kind, (int)response.StatusCode));
+ }
+
if (this.emitOldAttributes)
{
activity.SetTag(SemanticConventions.AttributeHttpStatusCode, TelemetryHelper.GetBoxedStatusCode(response.StatusCode));
@@ -279,11 +284,10 @@ public void OnStopActivity(Activity activity, object payload)
if (this.emitNewAttributes)
{
activity.SetTag(SemanticConventions.AttributeHttpResponseStatusCode, TelemetryHelper.GetBoxedStatusCode(response.StatusCode));
- }
-
- if (currentStatusCode == ActivityStatusCode.Unset)
- {
- activity.SetStatus(SpanHelper.ResolveSpanStatusForHttpStatusCode(activity.Kind, (int)response.StatusCode));
+ if (activity.Status == ActivityStatusCode.Error)
+ {
+ activity.SetTag(SemanticConventions.AttributeErrorType, TelemetryHelper.GetBoxedStatusCode(response.StatusCode));
+ }
}
try
@@ -337,6 +341,11 @@ public void OnException(Activity activity, object payload)
return;
}
+ if (this.emitNewAttributes)
+ {
+ activity.SetTag(SemanticConventions.AttributeErrorType, GetErrorType(exc));
+ }
+
if (this.options.RecordException)
{
activity.RecordException(exc);
@@ -372,4 +381,33 @@ static bool TryFetchException(object payload, out Exception exc)
return true;
}
}
+
+ private static string GetErrorType(Exception exc)
+ {
+#if NET8_0_OR_GREATER
+ // For net8.0 and above exception type can be found using HttpRequestError.
+ // https://learn.microsoft.com/dotnet/api/system.net.http.httprequesterror?view=net-8.0
+ if (exc is HttpRequestException httpRequestException)
+ {
+ return httpRequestException.HttpRequestError switch
+ {
+ HttpRequestError.NameResolutionError => "name_resolution_error",
+ HttpRequestError.ConnectionError => "connection_error",
+ HttpRequestError.SecureConnectionError => "secure_connection_error",
+ HttpRequestError.HttpProtocolError => "http_protocol_error",
+ HttpRequestError.ExtendedConnectNotSupported => "extended_connect_not_supported",
+ HttpRequestError.VersionNegotiationError => "version_negotiation_error",
+ HttpRequestError.UserAuthenticationError => "user_authentication_error",
+ HttpRequestError.ProxyTunnelError => "proxy_tunnel_error",
+ HttpRequestError.InvalidResponse => "invalid_response",
+ HttpRequestError.ResponseEnded => "response_ended",
+ HttpRequestError.ConfigurationLimitExceeded => "configuration_limit_exceeded",
+
+ // Fall back to the exception type name in case of HttpRequestError.Unknown
+ _ => exc.GetType().FullName,
+ };
+ }
+#endif
+ return exc.GetType().FullName;
+ }
}
diff --git a/src/OpenTelemetry.Instrumentation.Http/Implementation/HttpHandlerMetricsDiagnosticListener.cs b/src/OpenTelemetry.Instrumentation.Http/Implementation/HttpHandlerMetricsDiagnosticListener.cs
index eab4c6d4dc4..48e5c59320e 100644
--- a/src/OpenTelemetry.Instrumentation.Http/Implementation/HttpHandlerMetricsDiagnosticListener.cs
+++ b/src/OpenTelemetry.Instrumentation.Http/Implementation/HttpHandlerMetricsDiagnosticListener.cs
@@ -37,11 +37,18 @@ internal sealed class HttpHandlerMetricsDiagnosticListener : ListenerHandler
internal static readonly string MeterName = AssemblyName.Name;
internal static readonly string MeterVersion = AssemblyName.Version.ToString();
internal static readonly Meter Meter = new(MeterName, MeterVersion);
+ private const string OnUnhandledExceptionEvent = "System.Net.Http.Exception";
private static readonly Histogram HttpClientDuration = Meter.CreateHistogram("http.client.duration", "ms", "Measures the duration of outbound HTTP requests.");
private static readonly Histogram HttpClientRequestDuration = Meter.CreateHistogram("http.client.request.duration", "s", "Duration of HTTP client requests.");
private static readonly PropertyFetcher StopRequestFetcher = new("Request");
private static readonly PropertyFetcher StopResponseFetcher = new("Response");
+ private static readonly PropertyFetcher StopExceptionFetcher = new("Exception");
+ private static readonly PropertyFetcher RequestFetcher = new("Request");
+#if NET6_0_OR_GREATER
+ private static readonly HttpRequestOptionsKey HttpRequestOptionsErrorKey = new HttpRequestOptionsKey(SemanticConventions.AttributeErrorType);
+#endif
+
private readonly HttpClientMetricInstrumentationOptions options;
private readonly bool emitOldAttributes;
private readonly bool emitNewAttributes;
@@ -57,84 +64,118 @@ public HttpHandlerMetricsDiagnosticListener(string name, HttpClientMetricInstrum
public override void OnEventWritten(string name, object payload)
{
- if (name == OnStopEvent)
+ if (name == OnUnhandledExceptionEvent)
{
- if (Sdk.SuppressInstrumentation)
+ if (this.emitNewAttributes)
{
- return;
+ this.OnExceptionEventWritten(Activity.Current, payload);
}
+ }
+ else if (name == OnStopEvent)
+ {
+ this.OnStopEventWritten(Activity.Current, payload);
+ }
+ }
- var activity = Activity.Current;
- if (TryFetchRequest(payload, out HttpRequestMessage request))
+ public void OnStopEventWritten(Activity activity, object payload)
+ {
+ if (Sdk.SuppressInstrumentation)
+ {
+ return;
+ }
+
+ if (TryFetchRequest(payload, out HttpRequestMessage request))
+ {
+ // see the spec https://github.com/open-telemetry/opentelemetry-specification/blob/v1.20.0/specification/trace/semantic_conventions/http.md
+ if (this.emitOldAttributes)
{
- // see the spec https://github.com/open-telemetry/opentelemetry-specification/blob/v1.20.0/specification/trace/semantic_conventions/http.md
- if (this.emitOldAttributes)
+ TagList tags = default;
+
+ tags.Add(new KeyValuePair(SemanticConventions.AttributeHttpMethod, HttpTagHelper.GetNameForHttpMethod(request.Method)));
+ tags.Add(new KeyValuePair(SemanticConventions.AttributeHttpScheme, request.RequestUri.Scheme));
+ tags.Add(new KeyValuePair(SemanticConventions.AttributeHttpFlavor, HttpTagHelper.GetFlavorTagValueFromProtocolVersion(request.Version)));
+ tags.Add(new KeyValuePair(SemanticConventions.AttributeNetPeerName, request.RequestUri.Host));
+
+ if (!request.RequestUri.IsDefaultPort)
{
- TagList tags = default;
+ tags.Add(new KeyValuePair(SemanticConventions.AttributeNetPeerPort, request.RequestUri.Port));
+ }
- tags.Add(new KeyValuePair(SemanticConventions.AttributeHttpMethod, HttpTagHelper.GetNameForHttpMethod(request.Method)));
- tags.Add(new KeyValuePair(SemanticConventions.AttributeHttpScheme, request.RequestUri.Scheme));
- tags.Add(new KeyValuePair(SemanticConventions.AttributeHttpFlavor, HttpTagHelper.GetFlavorTagValueFromProtocolVersion(request.Version)));
- tags.Add(new KeyValuePair(SemanticConventions.AttributeNetPeerName, request.RequestUri.Host));
+ if (TryFetchResponse(payload, out HttpResponseMessage response))
+ {
+ tags.Add(new KeyValuePair(SemanticConventions.AttributeHttpStatusCode, TelemetryHelper.GetBoxedStatusCode(response.StatusCode)));
+ }
- if (!request.RequestUri.IsDefaultPort)
- {
- tags.Add(new KeyValuePair(SemanticConventions.AttributeNetPeerPort, request.RequestUri.Port));
- }
+ // We are relying here on HttpClient library to set duration before writing the stop event.
+ // https://github.com/dotnet/runtime/blob/90603686d314147017c8bbe1fa8965776ce607d0/src/libraries/System.Net.Http/src/System/Net/Http/DiagnosticsHandler.cs#L178
+ // TODO: Follow up with .NET team if we can continue to rely on this behavior.
+ HttpClientDuration.Record(activity.Duration.TotalMilliseconds, tags);
+ }
- if (TryFetchResponse(payload, out HttpResponseMessage response))
- {
- tags.Add(new KeyValuePair(SemanticConventions.AttributeHttpStatusCode, TelemetryHelper.GetBoxedStatusCode(response.StatusCode)));
- }
+ // see the spec https://github.com/open-telemetry/semantic-conventions/blob/v1.21.0/docs/http/http-spans.md
+ if (this.emitNewAttributes)
+ {
+ TagList tags = default;
- // We are relying here on HttpClient library to set duration before writing the stop event.
- // https://github.com/dotnet/runtime/blob/90603686d314147017c8bbe1fa8965776ce607d0/src/libraries/System.Net.Http/src/System/Net/Http/DiagnosticsHandler.cs#L178
- // TODO: Follow up with .NET team if we can continue to rely on this behavior.
- HttpClientDuration.Record(activity.Duration.TotalMilliseconds, tags);
+ if (RequestMethodHelper.KnownMethods.TryGetValue(request.Method.Method, out var httpMethod))
+ {
+ tags.Add(new KeyValuePair(SemanticConventions.AttributeHttpRequestMethod, httpMethod));
}
-
- // see the spec https://github.com/open-telemetry/semantic-conventions/blob/v1.21.0/docs/http/http-spans.md
- if (this.emitNewAttributes)
+ else
{
- TagList tags = default;
+ // 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
+ tags.Add(new KeyValuePair(SemanticConventions.AttributeHttpRequestMethod, "_OTHER"));
+ }
- if (RequestMethodHelper.KnownMethods.TryGetValue(request.Method.Method, out var httpMethod))
- {
- tags.Add(new KeyValuePair(SemanticConventions.AttributeHttpRequestMethod, httpMethod));
- }
- else
- {
- // 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
- tags.Add(new KeyValuePair(SemanticConventions.AttributeHttpRequestMethod, "_OTHER"));
- }
+ tags.Add(new KeyValuePair(SemanticConventions.AttributeServerAddress, request.RequestUri.Host));
+ tags.Add(new KeyValuePair(SemanticConventions.AttributeUrlScheme, request.RequestUri.Scheme));
+ tags.Add(new KeyValuePair(SemanticConventions.AttributeNetworkProtocolVersion, HttpTagHelper.GetFlavorTagValueFromProtocolVersion(request.Version)));
- tags.Add(new KeyValuePair(SemanticConventions.AttributeNetworkProtocolVersion, HttpTagHelper.GetFlavorTagValueFromProtocolVersion(request.Version)));
- tags.Add(new KeyValuePair(SemanticConventions.AttributeServerAddress, request.RequestUri.Host));
- tags.Add(new KeyValuePair(SemanticConventions.AttributeUrlScheme, request.RequestUri.Scheme));
+ if (!request.RequestUri.IsDefaultPort)
+ {
+ tags.Add(new KeyValuePair(SemanticConventions.AttributeServerPort, request.RequestUri.Port));
+ }
+
+ if (TryFetchResponse(payload, out HttpResponseMessage response))
+ {
+ tags.Add(new KeyValuePair(SemanticConventions.AttributeHttpResponseStatusCode, TelemetryHelper.GetBoxedStatusCode(response.StatusCode)));
- if (!request.RequestUri.IsDefaultPort)
+ // Set error.type to status code for failed requests
+ // https://github.com/open-telemetry/semantic-conventions/blob/v1.23.0/docs/http/http-spans.md#common-attributes
+ if (SpanHelper.ResolveSpanStatusForHttpStatusCode(ActivityKind.Client, (int)response.StatusCode) == ActivityStatusCode.Error)
{
- tags.Add(new KeyValuePair(SemanticConventions.AttributeServerPort, request.RequestUri.Port));
+ tags.Add(new KeyValuePair(SemanticConventions.AttributeErrorType, TelemetryHelper.GetBoxedStatusCode(response.StatusCode)));
}
+ }
- if (TryFetchResponse(payload, out HttpResponseMessage response))
+ if (response == null)
+ {
+#if !NET6_0_OR_GREATER
+ request.Properties.TryGetValue(SemanticConventions.AttributeErrorType, out var errorType);
+#else
+ request.Options.TryGetValue(HttpRequestOptionsErrorKey, out var errorType);
+#endif
+
+ // Set error.type to exception type if response was not received.
+ // https://github.com/open-telemetry/semantic-conventions/blob/v1.23.0/docs/http/http-spans.md#common-attributes
+ if (errorType != null)
{
- tags.Add(new KeyValuePair(SemanticConventions.AttributeHttpResponseStatusCode, TelemetryHelper.GetBoxedStatusCode(response.StatusCode)));
+ tags.Add(new KeyValuePair(SemanticConventions.AttributeErrorType, errorType));
}
-
- // We are relying here on HttpClient library to set duration before writing the stop event.
- // https://github.com/dotnet/runtime/blob/90603686d314147017c8bbe1fa8965776ce607d0/src/libraries/System.Net.Http/src/System/Net/Http/DiagnosticsHandler.cs#L178
- // TODO: Follow up with .NET team if we can continue to rely on this behavior.
- HttpClientRequestDuration.Record(activity.Duration.TotalSeconds, tags);
}
+
+ // We are relying here on HttpClient library to set duration before writing the stop event.
+ // https://github.com/dotnet/runtime/blob/90603686d314147017c8bbe1fa8965776ce607d0/src/libraries/System.Net.Http/src/System/Net/Http/DiagnosticsHandler.cs#L178
+ // TODO: Follow up with .NET team if we can continue to rely on this behavior.
+ HttpClientRequestDuration.Record(activity.Duration.TotalSeconds, tags);
}
}
// The AOT-annotation DynamicallyAccessedMembers in System.Net.Http library ensures that top-level properties on the payload object are always preserved.
// see https://github.com/dotnet/runtime/blob/f9246538e3d49b90b0e9128d7b1defef57cd6911/src/libraries/System.Net.Http/src/System/Net/Http/DiagnosticsHandler.cs#L325
#if NET6_0_OR_GREATER
- [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "The event source guarantees that top-level properties are preserved")]
+ [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "The System.Net.Http library guarantees that top-level properties are preserved")]
#endif
static bool TryFetchRequest(object payload, out HttpRequestMessage request) =>
StopRequestFetcher.TryFetch(payload, out request) && request != null;
@@ -142,9 +183,54 @@ static bool TryFetchRequest(object payload, out HttpRequestMessage request) =>
// The AOT-annotation DynamicallyAccessedMembers in System.Net.Http library ensures that top-level properties on the payload object are always preserved.
// see https://github.com/dotnet/runtime/blob/f9246538e3d49b90b0e9128d7b1defef57cd6911/src/libraries/System.Net.Http/src/System/Net/Http/DiagnosticsHandler.cs#L325
#if NET6_0_OR_GREATER
- [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "The event source guarantees that top-level properties are preserved")]
+ [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "The System.Net.Http library guarantees that top-level properties are preserved")]
#endif
static bool TryFetchResponse(object payload, out HttpResponseMessage response) =>
StopResponseFetcher.TryFetch(payload, out response) && response != null;
}
+
+ public void OnExceptionEventWritten(Activity activity, object payload)
+ {
+ if (!TryFetchException(payload, out Exception exc) || !TryFetchRequest(payload, out HttpRequestMessage request))
+ {
+ HttpInstrumentationEventSource.Log.NullPayload(nameof(HttpHandlerMetricsDiagnosticListener), nameof(this.OnExceptionEventWritten));
+ return;
+ }
+
+#if !NET6_0_OR_GREATER
+ request.Properties.Add(SemanticConventions.AttributeErrorType, exc.GetType().FullName);
+#else
+ request.Options.Set(HttpRequestOptionsErrorKey, exc.GetType().FullName);
+#endif
+
+ // The AOT-annotation DynamicallyAccessedMembers in System.Net.Http library ensures that top-level properties on the payload object are always preserved.
+ // see https://github.com/dotnet/runtime/blob/f9246538e3d49b90b0e9128d7b1defef57cd6911/src/libraries/System.Net.Http/src/System/Net/Http/DiagnosticsHandler.cs#L325
+#if NET6_0_OR_GREATER
+ [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "The System.Net.Http library guarantees that top-level properties are preserved")]
+#endif
+ static bool TryFetchException(object payload, out Exception exc)
+ {
+ if (!StopExceptionFetcher.TryFetch(payload, out exc) || exc == null)
+ {
+ return false;
+ }
+
+ return true;
+ }
+
+ // The AOT-annotation DynamicallyAccessedMembers in System.Net.Http library ensures that top-level properties on the payload object are always preserved.
+ // see https://github.com/dotnet/runtime/blob/f9246538e3d49b90b0e9128d7b1defef57cd6911/src/libraries/System.Net.Http/src/System/Net/Http/DiagnosticsHandler.cs#L325
+#if NET6_0_OR_GREATER
+ [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "The System.Net.Http library guarantees that top-level properties are preserved")]
+#endif
+ static bool TryFetchRequest(object payload, out HttpRequestMessage request)
+ {
+ if (!RequestFetcher.TryFetch(payload, out request) || request == null)
+ {
+ return false;
+ }
+
+ return true;
+ }
+ }
}
diff --git a/test/OpenTelemetry.Instrumentation.Http.Tests/HttpClientTests.cs b/test/OpenTelemetry.Instrumentation.Http.Tests/HttpClientTests.cs
index 314ff67ea41..9ff51ebef41 100644
--- a/test/OpenTelemetry.Instrumentation.Http.Tests/HttpClientTests.cs
+++ b/test/OpenTelemetry.Instrumentation.Http.Tests/HttpClientTests.cs
@@ -51,7 +51,7 @@ await HttpOutCallsAreCollectedSuccessfullyBodyAsync(
[Theory]
[MemberData(nameof(TestData))]
- public async Task HttpOutCallsAreCollectedSuccessfullyTracesAndMetricsNewSemanticConventionsAsync(HttpTestData.HttpOutTestCase tc)
+ public async Task HttpOutCallsAreCollectedSuccessfullyTracesAndMetricsDuplicateSemanticConventionsAsync(HttpTestData.HttpOutTestCase tc)
{
await HttpOutCallsAreCollectedSuccessfullyBodyAsync(
this.host,
@@ -59,12 +59,13 @@ await HttpOutCallsAreCollectedSuccessfullyBodyAsync(
tc,
enableTracing: true,
enableMetrics: true,
- semanticConvention: HttpSemanticConvention.New).ConfigureAwait(false);
+ semanticConvention: HttpSemanticConvention.Dupe).ConfigureAwait(false);
}
+#endif
[Theory]
[MemberData(nameof(TestData))]
- public async Task HttpOutCallsAreCollectedSuccessfullyTracesAndMetricsDuplicateSemanticConventionsAsync(HttpTestData.HttpOutTestCase tc)
+ public async Task HttpOutCallsAreCollectedSuccessfullyTracesAndMetricsNewSemanticConventionsAsync(HttpTestData.HttpOutTestCase tc)
{
await HttpOutCallsAreCollectedSuccessfullyBodyAsync(
this.host,
@@ -72,9 +73,8 @@ await HttpOutCallsAreCollectedSuccessfullyBodyAsync(
tc,
enableTracing: true,
enableMetrics: true,
- semanticConvention: HttpSemanticConvention.Dupe).ConfigureAwait(false);
+ semanticConvention: HttpSemanticConvention.New).ConfigureAwait(false);
}
-#endif
[Theory]
[MemberData(nameof(TestData))]
@@ -346,11 +346,22 @@ private static async Task HttpOutCallsAreCollectedSuccessfullyBodyAsync(
var normalizedAttributes = activity.TagObjects.Where(kv => !kv.Key.StartsWith("otel.")).ToDictionary(x => x.Key, x => x.Value.ToString());
+#if !NETFRAMEWORK
+ int numberOfNewTags = activity.Status == ActivityStatusCode.Error ? 6 : 5;
+ int numberOfDupeTags = activity.Status == ActivityStatusCode.Error ? 12 : 11;
+
+ var expectedAttributeCount = semanticConvention == HttpSemanticConvention.Dupe
+ ? numberOfDupeTags + (tc.ResponseExpected ? 2 : 0)
+ : semanticConvention == HttpSemanticConvention.New
+ ? numberOfNewTags + (tc.ResponseExpected ? 1 : 0)
+ : 6 + (tc.ResponseExpected ? 1 : 0);
+#else
var expectedAttributeCount = semanticConvention == HttpSemanticConvention.Dupe
? 11 + (tc.ResponseExpected ? 2 : 0)
: semanticConvention == HttpSemanticConvention.New
? 5 + (tc.ResponseExpected ? 1 : 0)
: 6 + (tc.ResponseExpected ? 1 : 0);
+#endif
Assert.Equal(expectedAttributeCount, normalizedAttributes.Count);
@@ -382,10 +393,26 @@ private static async Task HttpOutCallsAreCollectedSuccessfullyBodyAsync(
if (tc.ResponseExpected)
{
Assert.Contains(normalizedAttributes, kvp => kvp.Key == SemanticConventions.AttributeHttpResponseStatusCode && kvp.Value.ToString() == normalizedAttributesTestCase[SemanticConventions.AttributeHttpStatusCode]);
+
+#if !NETFRAMEWORK
+ if (tc.ResponseCode >= 400)
+ {
+ Assert.Contains(normalizedAttributes, kvp => kvp.Key == SemanticConventions.AttributeErrorType && kvp.Value.ToString() == normalizedAttributesTestCase[SemanticConventions.AttributeHttpStatusCode]);
+ }
+#endif
}
else
{
Assert.DoesNotContain(normalizedAttributes, kvp => kvp.Key == SemanticConventions.AttributeHttpResponseStatusCode);
+#if !NETFRAMEWORK
+#if !NET8_0_OR_GREATER
+ Assert.Contains(normalizedAttributes, kvp => kvp.Key == SemanticConventions.AttributeErrorType && kvp.Value.ToString() == "System.Net.Http.HttpRequestException");
+#else
+ // we are using fake address so it will be "name_resolution_error"
+ // TODO: test other error types.
+ Assert.Contains(normalizedAttributes, kvp => kvp.Key == SemanticConventions.AttributeErrorType && kvp.Value.ToString() == "name_resolution_error");
+#endif
+#endif
}
}
@@ -505,7 +532,9 @@ private static async Task HttpOutCallsAreCollectedSuccessfullyBodyAsync(
if (enableTracing)
{
var activity = Assert.Single(activities);
+#if !NET8_0_OR_GREATER
Assert.Equal(activity.Duration.TotalSeconds, sum);
+#endif
}
else
{
@@ -519,22 +548,63 @@ private static async Task HttpOutCallsAreCollectedSuccessfullyBodyAsync(
attributes[tag.Key] = tag.Value;
}
+#if !NETFRAMEWORK
+#if !NET8_0_OR_GREATER
+ var numberOfTags = 6;
+#else
+ // network.protocol.version is not emitted when response if not received.
+ // https://github.com/open-telemetry/opentelemetry-dotnet/issues/4928
+ var numberOfTags = 5;
+#endif
+ if (tc.ResponseExpected)
+ {
+ var expectedStatusCode = int.Parse(normalizedAttributesTestCase[SemanticConventions.AttributeHttpStatusCode]);
+ numberOfTags = (expectedStatusCode >= 400) ? 6 : 5;
+ }
+
+ var expectedAttributeCount = numberOfTags + (tc.ResponseExpected ? 1 : 0);
+#else
var expectedAttributeCount = 5 + (tc.ResponseExpected ? 1 : 0);
+#endif
Assert.Equal(expectedAttributeCount, attributes.Count);
Assert.Contains(attributes, kvp => kvp.Key == SemanticConventions.AttributeHttpRequestMethod && kvp.Value.ToString() == normalizedAttributesTestCase[SemanticConventions.AttributeHttpMethod]);
Assert.Contains(attributes, kvp => kvp.Key == SemanticConventions.AttributeServerAddress && kvp.Value.ToString() == normalizedAttributesTestCase[SemanticConventions.AttributeNetPeerName]);
Assert.Contains(attributes, kvp => kvp.Key == SemanticConventions.AttributeServerPort && kvp.Value.ToString() == normalizedAttributesTestCase[SemanticConventions.AttributeNetPeerPort]);
- Assert.Contains(attributes, kvp => kvp.Key == SemanticConventions.AttributeNetworkProtocolVersion && kvp.Value.ToString() == normalizedAttributesTestCase[SemanticConventions.AttributeHttpFlavor]);
Assert.Contains(attributes, kvp => kvp.Key == SemanticConventions.AttributeUrlScheme && kvp.Value.ToString() == normalizedAttributesTestCase[SemanticConventions.AttributeHttpScheme]);
+#if !NET8_0_OR_GREATER
+ Assert.Contains(attributes, kvp => kvp.Key == SemanticConventions.AttributeNetworkProtocolVersion && kvp.Value.ToString() == normalizedAttributesTestCase[SemanticConventions.AttributeHttpFlavor]);
+#endif
+
if (tc.ResponseExpected)
{
Assert.Contains(attributes, kvp => kvp.Key == SemanticConventions.AttributeHttpResponseStatusCode && kvp.Value.ToString() == normalizedAttributesTestCase[SemanticConventions.AttributeHttpStatusCode]);
+
+#if !NETFRAMEWORK
+ if (tc.ResponseCode >= 400)
+ {
+ Assert.Contains(attributes, kvp => kvp.Key == SemanticConventions.AttributeErrorType && kvp.Value.ToString() == normalizedAttributesTestCase[SemanticConventions.AttributeHttpStatusCode]);
+ }
+#endif
}
else
{
Assert.DoesNotContain(attributes, kvp => kvp.Key == SemanticConventions.AttributeHttpResponseStatusCode);
+
+#if !NETFRAMEWORK
+#if !NET8_0_OR_GREATER
+ Assert.Contains(attributes, kvp => kvp.Key == SemanticConventions.AttributeErrorType && kvp.Value.ToString() == "System.Net.Http.HttpRequestException");
+#else
+ // we are using fake address so it will be "name_resolution_error"
+ // TODO: test other error types.
+ Assert.Contains(attributes, kvp => kvp.Key == SemanticConventions.AttributeErrorType && kvp.Value.ToString() == "name_resolution_error");
+
+ // network.protocol.version is not emitted when response if not received.
+ // https://github.com/open-telemetry/opentelemetry-dotnet/issues/4928
+ Assert.DoesNotContain(attributes, kvp => kvp.Key == SemanticConventions.AttributeNetworkProtocolVersion);
+#endif
+#endif
}
// Inspect Histogram Bounds
diff --git a/test/OpenTelemetry.Instrumentation.Http.Tests/http-out-test-cases.json b/test/OpenTelemetry.Instrumentation.Http.Tests/http-out-test-cases.json
index 833e26098bb..8ebbe29ff1e 100644
--- a/test/OpenTelemetry.Instrumentation.Http.Tests/http-out-test-cases.json
+++ b/test/OpenTelemetry.Instrumentation.Http.Tests/http-out-test-cases.json
@@ -321,24 +321,6 @@
"http.url": "http://{host}:{port}/"
}
},
- {
- "name": "Response code: 600",
- "method": "GET",
- "url": "http://{host}:{port}/",
- "responseCode": 600,
- "spanName": "HTTP GET",
- "spanStatus": "Error",
- "responseExpected": true,
- "spanAttributes": {
- "http.scheme": "http",
- "http.method": "GET",
- "net.peer.name": "{host}",
- "net.peer.port": "{port}",
- "http.flavor": "{flavor}",
- "http.status_code": "600",
- "http.url": "http://{host}:{port}/"
- }
- },
{
"name": "Http version attribute populated",
"method": "GET",
From f25ff3a4f4687e7eddafdf72ce0ee0967819b503 Mon Sep 17 00:00:00 2001
From: Timothy Mothra
Date: Wed, 8 Nov 2023 17:38:55 -0800
Subject: [PATCH 4/5] [AspNetCore] [Http] remove Activity Status Description
and update unit tests (#5025)
---
.../CHANGELOG.md | 6 +++
.../Implementation/HttpInListener.cs | 2 +-
.../CHANGELOG.md | 6 +++
.../HttpHandlerDiagnosticListener.cs | 2 +-
.../HttpWebRequestActivitySource.netfx.cs | 51 +++++--------------
...stsCollectionsIsAccordingToTheSpecTests.cs | 9 +---
...llectionsIsAccordingToTheSpecTests_Dupe.cs | 9 +---
...ollectionsIsAccordingToTheSpecTests_New.cs | 9 +---
.../HttpClientTests.cs | 7 +--
.../HttpTestData.cs | 2 -
...HttpWebRequestActivitySourceTests.netfx.cs | 6 +--
.../HttpWebRequestTests.cs | 6 +--
.../http-out-test-cases.json | 2 -
13 files changed, 34 insertions(+), 83 deletions(-)
diff --git a/src/OpenTelemetry.Instrumentation.AspNetCore/CHANGELOG.md b/src/OpenTelemetry.Instrumentation.AspNetCore/CHANGELOG.md
index 4091d5b9f1f..b259c980e9d 100644
--- a/src/OpenTelemetry.Instrumentation.AspNetCore/CHANGELOG.md
+++ b/src/OpenTelemetry.Instrumentation.AspNetCore/CHANGELOG.md
@@ -2,6 +2,12 @@
## Unreleased
+* Removed the Activity Status Description that was being set during
+ exceptions. Activity Status will continue to be reported as `Error`.
+ This is a **breaking change**. `EnrichWithException` can be leveraged
+ to restore this behavior.
+ ([#5025](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5025))
+
* 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)
diff --git a/src/OpenTelemetry.Instrumentation.AspNetCore/Implementation/HttpInListener.cs b/src/OpenTelemetry.Instrumentation.AspNetCore/Implementation/HttpInListener.cs
index fbf39179482..7160ab666c2 100644
--- a/src/OpenTelemetry.Instrumentation.AspNetCore/Implementation/HttpInListener.cs
+++ b/src/OpenTelemetry.Instrumentation.AspNetCore/Implementation/HttpInListener.cs
@@ -435,7 +435,7 @@ public void OnException(Activity activity, object payload)
activity.RecordException(exc);
}
- activity.SetStatus(ActivityStatusCode.Error, exc.Message);
+ activity.SetStatus(ActivityStatusCode.Error);
try
{
diff --git a/src/OpenTelemetry.Instrumentation.Http/CHANGELOG.md b/src/OpenTelemetry.Instrumentation.Http/CHANGELOG.md
index bc19fe9c4fb..25bdfb76a51 100644
--- a/src/OpenTelemetry.Instrumentation.Http/CHANGELOG.md
+++ b/src/OpenTelemetry.Instrumentation.Http/CHANGELOG.md
@@ -2,6 +2,12 @@
## Unreleased
+* Removed the Activity Status Description that was being set during
+ exceptions. Activity Status will continue to be reported as `Error`.
+ This is a **breaking change**. `EnrichWithException` can be leveraged
+ to restore this behavior.
+ ([#5025](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5025))
+
* 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)
diff --git a/src/OpenTelemetry.Instrumentation.Http/Implementation/HttpHandlerDiagnosticListener.cs b/src/OpenTelemetry.Instrumentation.Http/Implementation/HttpHandlerDiagnosticListener.cs
index c9234e4cdcc..f8fe389a943 100644
--- a/src/OpenTelemetry.Instrumentation.Http/Implementation/HttpHandlerDiagnosticListener.cs
+++ b/src/OpenTelemetry.Instrumentation.Http/Implementation/HttpHandlerDiagnosticListener.cs
@@ -353,7 +353,7 @@ public void OnException(Activity activity, object payload)
if (exc is HttpRequestException)
{
- activity.SetStatus(ActivityStatusCode.Error, exc.Message);
+ activity.SetStatus(ActivityStatusCode.Error);
}
try
diff --git a/src/OpenTelemetry.Instrumentation.Http/Implementation/HttpWebRequestActivitySource.netfx.cs b/src/OpenTelemetry.Instrumentation.Http/Implementation/HttpWebRequestActivitySource.netfx.cs
index d2c23f6c6bf..095ace34d67 100644
--- a/src/OpenTelemetry.Instrumentation.Http/Implementation/HttpWebRequestActivitySource.netfx.cs
+++ b/src/OpenTelemetry.Instrumentation.Http/Implementation/HttpWebRequestActivitySource.netfx.cs
@@ -230,57 +230,30 @@ private static void AddExceptionTags(Exception exception, Activity activity, out
}
ActivityStatusCode status;
- string exceptionMessage = null;
- if (exception is WebException wexc)
+ if (exception is WebException wexc && wexc.Response is HttpWebResponse response)
{
- if (wexc.Response is HttpWebResponse response)
- {
- statusCode = response.StatusCode;
-
- if (tracingEmitOldAttributes)
- {
- activity.SetTag(SemanticConventions.AttributeHttpStatusCode, (int)statusCode);
- }
+ statusCode = response.StatusCode;
- if (tracingEmitNewAttributes)
- {
- activity.SetTag(SemanticConventions.AttributeHttpResponseStatusCode, (int)statusCode);
- }
-
- status = SpanHelper.ResolveSpanStatusForHttpStatusCode(activity.Kind, (int)statusCode);
+ if (tracingEmitOldAttributes)
+ {
+ activity.SetTag(SemanticConventions.AttributeHttpStatusCode, (int)statusCode);
}
- else
+
+ if (tracingEmitNewAttributes)
{
- switch (wexc.Status)
- {
- case WebExceptionStatus.Timeout:
- case WebExceptionStatus.RequestCanceled:
- status = ActivityStatusCode.Error;
- break;
- case WebExceptionStatus.SendFailure:
- case WebExceptionStatus.ConnectFailure:
- case WebExceptionStatus.SecureChannelFailure:
- case WebExceptionStatus.TrustFailure:
- case WebExceptionStatus.ServerProtocolViolation:
- case WebExceptionStatus.MessageLengthLimitExceeded:
- status = ActivityStatusCode.Error;
- exceptionMessage = exception.Message;
- break;
- default:
- status = ActivityStatusCode.Error;
- exceptionMessage = exception.Message;
- break;
- }
+ activity.SetTag(SemanticConventions.AttributeHttpResponseStatusCode, (int)statusCode);
}
+
+ status = SpanHelper.ResolveSpanStatusForHttpStatusCode(activity.Kind, (int)statusCode);
}
else
{
status = ActivityStatusCode.Error;
- exceptionMessage = exception.Message;
}
- activity.SetStatus(status, exceptionMessage);
+ activity.SetStatus(status);
+
if (TracingOptions.RecordException)
{
activity.RecordException(exception);
diff --git a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/IncomingRequestsCollectionsIsAccordingToTheSpecTests.cs b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/IncomingRequestsCollectionsIsAccordingToTheSpecTests.cs
index 58688301760..1059bee9281 100644
--- a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/IncomingRequestsCollectionsIsAccordingToTheSpecTests.cs
+++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/IncomingRequestsCollectionsIsAccordingToTheSpecTests.cs
@@ -131,14 +131,7 @@ public async Task SuccessfulTemplateControllerCallGeneratesASpan_Old(
// Instrumentation is not expected to set status description
// as the reason can be inferred from SemanticConventions.AttributeHttpStatusCode
- if (!urlPath.EndsWith("exception"))
- {
- Assert.True(string.IsNullOrEmpty(activity.StatusDescription));
- }
- else
- {
- Assert.Equal("exception description", activity.StatusDescription);
- }
+ Assert.Null(activity.StatusDescription);
if (recordException)
{
diff --git a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/IncomingRequestsCollectionsIsAccordingToTheSpecTests_Dupe.cs b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/IncomingRequestsCollectionsIsAccordingToTheSpecTests_Dupe.cs
index 622498876cd..8bfe675ed5d 100644
--- a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/IncomingRequestsCollectionsIsAccordingToTheSpecTests_Dupe.cs
+++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/IncomingRequestsCollectionsIsAccordingToTheSpecTests_Dupe.cs
@@ -138,14 +138,7 @@ public async Task SuccessfulTemplateControllerCallGeneratesASpan_Dupe(
// Instrumentation is not expected to set status description
// as the reason can be inferred from SemanticConventions.AttributeHttpStatusCode
- if (!urlPath.EndsWith("exception"))
- {
- Assert.True(string.IsNullOrEmpty(activity.StatusDescription));
- }
- else
- {
- Assert.Equal("exception description", activity.StatusDescription);
- }
+ Assert.Null(activity.StatusDescription);
if (recordException)
{
diff --git a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/IncomingRequestsCollectionsIsAccordingToTheSpecTests_New.cs b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/IncomingRequestsCollectionsIsAccordingToTheSpecTests_New.cs
index cf66df98ee7..5c56ffb6719 100644
--- a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/IncomingRequestsCollectionsIsAccordingToTheSpecTests_New.cs
+++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/IncomingRequestsCollectionsIsAccordingToTheSpecTests_New.cs
@@ -132,14 +132,7 @@ public async Task SuccessfulTemplateControllerCallGeneratesASpan_New(
// Instrumentation is not expected to set status description
// as the reason can be inferred from SemanticConventions.AttributeHttpStatusCode
- if (!urlPath.EndsWith("exception"))
- {
- Assert.True(string.IsNullOrEmpty(activity.StatusDescription));
- }
- else
- {
- Assert.Equal("exception description", activity.StatusDescription);
- }
+ Assert.Null(activity.StatusDescription);
if (recordException)
{
diff --git a/test/OpenTelemetry.Instrumentation.Http.Tests/HttpClientTests.cs b/test/OpenTelemetry.Instrumentation.Http.Tests/HttpClientTests.cs
index 9ff51ebef41..ad3344d8a29 100644
--- a/test/OpenTelemetry.Instrumentation.Http.Tests/HttpClientTests.cs
+++ b/test/OpenTelemetry.Instrumentation.Http.Tests/HttpClientTests.cs
@@ -337,12 +337,7 @@ private static async Task HttpOutCallsAreCollectedSuccessfullyBodyAsync(
// Assert.Equal(tc.SpanStatus, d[span.Status.CanonicalCode]);
Assert.Equal(tc.SpanStatus, activity.Status.ToString());
-
- if (tc.SpanStatusHasDescription.HasValue)
- {
- var desc = activity.StatusDescription;
- Assert.Equal(tc.SpanStatusHasDescription.Value, !string.IsNullOrEmpty(desc));
- }
+ Assert.Null(activity.StatusDescription);
var normalizedAttributes = activity.TagObjects.Where(kv => !kv.Key.StartsWith("otel.")).ToDictionary(x => x.Key, x => x.Value.ToString());
diff --git a/test/OpenTelemetry.Instrumentation.Http.Tests/HttpTestData.cs b/test/OpenTelemetry.Instrumentation.Http.Tests/HttpTestData.cs
index 86daf630db5..705c7c9f876 100644
--- a/test/OpenTelemetry.Instrumentation.Http.Tests/HttpTestData.cs
+++ b/test/OpenTelemetry.Instrumentation.Http.Tests/HttpTestData.cs
@@ -72,8 +72,6 @@ public class HttpOutTestCase
public string SpanStatus { get; set; }
- public bool? SpanStatusHasDescription { get; set; }
-
public Dictionary SpanAttributes { get; set; }
}
}
diff --git a/test/OpenTelemetry.Instrumentation.Http.Tests/HttpWebRequestActivitySourceTests.netfx.cs b/test/OpenTelemetry.Instrumentation.Http.Tests/HttpWebRequestActivitySourceTests.netfx.cs
index 6eca1d40eed..074ae7df55c 100644
--- a/test/OpenTelemetry.Instrumentation.Http.Tests/HttpWebRequestActivitySourceTests.netfx.cs
+++ b/test/OpenTelemetry.Instrumentation.Http.Tests/HttpWebRequestActivitySourceTests.netfx.cs
@@ -549,7 +549,7 @@ public async Task TestRequestWithException(string method)
Assert.Equal("Stop", exceptionEvent.Key);
Assert.True(activity.Status != ActivityStatusCode.Unset);
- Assert.NotNull(activity.StatusDescription);
+ Assert.Null(activity.StatusDescription);
}
///
@@ -627,7 +627,7 @@ public async Task TestSecureTransportFailureRequest(string method)
Assert.Equal("Stop", exceptionEvent.Key);
Assert.True(exceptionEvent.Value.Status != ActivityStatusCode.Unset);
- Assert.NotNull(exceptionEvent.Value.StatusDescription);
+ Assert.Null(exceptionEvent.Value.StatusDescription);
}
///
@@ -669,7 +669,7 @@ public async Task TestSecureTransportRetryFailureRequest(string method)
Assert.Equal("Stop", exceptionEvent.Key);
Assert.True(exceptionEvent.Value.Status != ActivityStatusCode.Unset);
- Assert.NotNull(exceptionEvent.Value.StatusDescription);
+ Assert.Null(exceptionEvent.Value.StatusDescription);
}
[Fact]
diff --git a/test/OpenTelemetry.Instrumentation.Http.Tests/HttpWebRequestTests.cs b/test/OpenTelemetry.Instrumentation.Http.Tests/HttpWebRequestTests.cs
index 88e5b6e3813..44d79860889 100644
--- a/test/OpenTelemetry.Instrumentation.Http.Tests/HttpWebRequestTests.cs
+++ b/test/OpenTelemetry.Instrumentation.Http.Tests/HttpWebRequestTests.cs
@@ -123,11 +123,7 @@ public void HttpOutCallsAreCollectedSuccessfully(HttpTestData.HttpOutTestCase tc
if (tag.Key == SpanAttributeConstants.StatusDescriptionKey)
{
- if (tc.SpanStatusHasDescription.HasValue)
- {
- Assert.Equal(tc.SpanStatusHasDescription.Value, !string.IsNullOrEmpty(tagValue));
- }
-
+ Assert.Null(tagValue);
continue;
}
diff --git a/test/OpenTelemetry.Instrumentation.Http.Tests/http-out-test-cases.json b/test/OpenTelemetry.Instrumentation.Http.Tests/http-out-test-cases.json
index 8ebbe29ff1e..d808d5c00f2 100644
--- a/test/OpenTelemetry.Instrumentation.Http.Tests/http-out-test-cases.json
+++ b/test/OpenTelemetry.Instrumentation.Http.Tests/http-out-test-cases.json
@@ -93,7 +93,6 @@
"url": "http://sdlfaldfjalkdfjlkajdflkajlsdjf:{port}/",
"spanName": "HTTP GET",
"spanStatus": "Error",
- "spanStatusHasDescription": true,
"responseExpected": false,
"recordException": false,
"spanAttributes": {
@@ -111,7 +110,6 @@
"url": "http://sdlfaldfjalkdfjlkajdflkajlsdjf:{port}/",
"spanName": "HTTP GET",
"spanStatus": "Error",
- "spanStatusHasDescription": true,
"responseExpected": false,
"recordException": true,
"spanAttributes": {
From 0a989a942f2b7084980bb3d88eb8011f1f2cdc21 Mon Sep 17 00:00:00 2001
From: Alan West <3676547+alanwest@users.noreply.github.com>
Date: Thu, 9 Nov 2023 09:57:55 -0800
Subject: [PATCH 5/5] Add test suite to analyze ASP.NET Core routing scenarios
(#4950)
---
Directory.Packages.props | 3 +
...ry.Instrumentation.AspNetCore.Tests.csproj | 8 +
.../RouteTests/README.md | 204 ++++++
.../RouteTests/README.net6.0.md | 592 ++++++++++++++++
.../RouteTests/README.net7.0.md | 634 ++++++++++++++++++
.../RouteTests/README.net8.0.md | 634 ++++++++++++++++++
.../RouteTests/RoutingTestCases.cs | 87 +++
.../RouteTests/RoutingTestCases.json | 246 +++++++
.../RouteTests/RoutingTestFixture.cs | 121 ++++
.../RouteTests/RoutingTestResult.cs | 46 ++
.../RouteTests/RoutingTests.cs | 191 ++++++
.../Controllers/AnotherAreaController.cs | 27 +
.../ControllerForMyAreaController.cs | 29 +
.../Controllers/AttributeRouteController.cs | 36 +
.../ConventionalRouteController.cs | 30 +
.../TestApplication/Pages/Index.cshtml | 2 +
.../Pages/PageThatThrowsException.cshtml | 4 +
.../RouteTests/TestApplication/RouteInfo.cs | 151 +++++
.../RouteInfoDiagnosticObserver.cs | 123 ++++
.../TestApplication/TestApplicationFactory.cs | 170 +++++
.../TestApplication/wwwroot/js/site.js | 4 +
21 files changed, 3342 insertions(+)
create mode 100644 test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/README.md
create mode 100644 test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/README.net6.0.md
create mode 100644 test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/README.net7.0.md
create mode 100644 test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/README.net8.0.md
create mode 100644 test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/RoutingTestCases.cs
create mode 100644 test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/RoutingTestCases.json
create mode 100644 test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/RoutingTestFixture.cs
create mode 100644 test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/RoutingTestResult.cs
create mode 100644 test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/RoutingTests.cs
create mode 100644 test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/TestApplication/Areas/AnotherArea/Controllers/AnotherAreaController.cs
create mode 100644 test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/TestApplication/Areas/MyArea/Controllers/ControllerForMyAreaController.cs
create mode 100644 test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/TestApplication/Controllers/AttributeRouteController.cs
create mode 100644 test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/TestApplication/Controllers/ConventionalRouteController.cs
create mode 100644 test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/TestApplication/Pages/Index.cshtml
create mode 100644 test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/TestApplication/Pages/PageThatThrowsException.cshtml
create mode 100644 test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/TestApplication/RouteInfo.cs
create mode 100644 test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/TestApplication/RouteInfoDiagnosticObserver.cs
create mode 100644 test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/TestApplication/TestApplicationFactory.cs
create mode 100644 test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/TestApplication/wwwroot/js/site.js
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 21852b666b0..6ca67d71ca7 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -98,13 +98,16 @@
+
+
+
diff --git a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/OpenTelemetry.Instrumentation.AspNetCore.Tests.csproj b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/OpenTelemetry.Instrumentation.AspNetCore.Tests.csproj
index f7400d295f8..186b33a8f7f 100644
--- a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/OpenTelemetry.Instrumentation.AspNetCore.Tests.csproj
+++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/OpenTelemetry.Instrumentation.AspNetCore.Tests.csproj
@@ -7,6 +7,7 @@
+
@@ -36,4 +37,11 @@
+
+
+
+ RoutingTestCases.json
+ Always
+
+
diff --git a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/README.md b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/README.md
new file mode 100644
index 00000000000..38ae9f93fd9
--- /dev/null
+++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/README.md
@@ -0,0 +1,204 @@
+# ASP.NET Core `http.route` tests
+
+This folder contains a test suite that validates the instrumentation produces
+the expected `http.route` attribute on both the activity and metric it emits.
+When available, the `http.route` is also a required component of the
+`Activity.DisplayName`.
+
+The test suite covers a variety of different routing scenarios available for
+ASP.NET Core:
+
+* [Conventional routing](https://learn.microsoft.com/aspnet/core/mvc/controllers/routing#conventional-routing)
+* [Conventional routing using areas](https://learn.microsoft.com/aspnet/core/mvc/controllers/routing#areas)
+* [Attribute routing](https://learn.microsoft.com/aspnet/core/mvc/controllers/routing#attribute-routing-for-rest-apis)
+* [Razor pages](https://learn.microsoft.com/aspnet/core/razor-pages/razor-pages-conventions)
+* [Minimal APIs](https://learn.microsoft.com/aspnet/core/fundamentals/minimal-apis/route-handlers)
+
+The individual test cases are defined in RoutingTestCases.json.
+
+The test suite is unique in that, when run, it generates README files for each
+target framework which aids in documenting how the instrumentation behaves for
+each test case. These files are source-controlled, so if the behavior of the
+instrumentation changes, the README files will be updated to reflect the change.
+
+* [.NET 6](./README.net6.0.md)
+* [.NET 7](./README.net7.0.md)
+* [.NET 8](./README.net8.0.md)
+
+For each test case a request is made to an ASP.NET Core application with a
+particular routing configuration. ASP.NET Core offers a
+[variety of APIs](#aspnet-core-apis-for-retrieving-route-information) for
+retrieving the route information of a given request. The README files include
+detailed information documenting the route information available using the
+various APIs in each test case. For example, here is the detailed result
+generated for a test case:
+
+```json
+{
+ "IdealHttpRoute": "ConventionalRoute/ActionWithStringParameter/{id?}",
+ "ActivityDisplayName": "/ConventionalRoute/ActionWithStringParameter/2",
+ "ActivityHttpRoute": "",
+ "MetricHttpRoute": "{controller=ConventionalRoute}/{action=Default}/{id?}",
+ "RouteInfo": {
+ "HttpMethod": "GET",
+ "Path": "/ConventionalRoute/ActionWithStringParameter/2?num=3",
+ "RoutePattern.RawText": "{controller=ConventionalRoute}/{action=Default}/{id?}",
+ "IRouteDiagnosticsMetadata.Route": null,
+ "HttpContext.GetRouteData()": {
+ "controller": "ConventionalRoute",
+ "action": "ActionWithStringParameter",
+ "id": "2"
+ },
+ "ActionDescriptor": {
+ "AttributeRouteInfo.Template": null,
+ "Parameters": [
+ "id",
+ "num"
+ ],
+ "ControllerActionDescriptor": {
+ "ControllerName": "ConventionalRoute",
+ "ActionName": "ActionWithStringParameter"
+ },
+ "PageActionDescriptor": null
+ }
+ }
+}
+```
+
+> [!NOTE]
+> The test result currently includes an `IdealHttpRoute` property. This is
+> temporary, and is meant to drive a conversation to determine the best way
+> for generating the `http.route` attribute under different routing scenarios.
+> In the example above, the path invoked is
+> `/ConventionalRoute/ActionWithStringParameter/2?num=3`. Currently, we see
+> that the `http.route` attribute on the metric emitted is
+> `{controller=ConventionalRoute}/{action=Default}/{id?}` which was derived
+> using `RoutePattern.RawText`. This is not ideal
+> because the route template does not include the actual action that was
+> invoked `ActionWithStringParameter`. The invoked action could be derived
+> using either the `ControllerActionDescriptor`
+> or `HttpContext.GetRouteData()`.
+
+## ASP.NET Core APIs for retrieving route information
+
+Included below are short snippets illustrating the use of the various
+APIs available for retrieving route information.
+
+### Retrieving the route template
+
+The route template can be obtained from `HttpContext` by retrieving the
+`RouteEndpoint` using the following two APIs.
+
+For attribute routing and minimal API scenarios, using the route template alone
+is sufficient for deriving `http.route` in all test cases.
+
+The route template does not well describe the `http.route` in conventional
+routing and some Razor page scenarios.
+
+#### [RoutePattern.RawText](https://learn.microsoft.com/dotnet/api/microsoft.aspnetcore.routing.patterns.routepattern.rawtext)
+
+```csharp
+(httpContext.GetEndpoint() as RouteEndpoint)?.RoutePattern.RawText;
+```
+
+#### [IRouteDiagnosticsMetadata.Route](https://learn.microsoft.com/dotnet/api/microsoft.aspnetcore.http.metadata.iroutediagnosticsmetadata.route)
+
+This API was introduced in .NET 8.
+
+```csharp
+httpContext.GetEndpoint()?.Metadata.GetMetadata()?.Route;
+```
+
+### RouteData
+
+`RouteData` can be retrieved from `HttpContext` using the `GetRouteData()`
+extension method. The values obtained from `RouteData` identify the controller/
+action or Razor page invoked by the request.
+
+#### [HttpContext.GetRouteData()](https://learn.microsoft.com/dotnet/api/microsoft.aspnetcore.routing.routinghttpcontextextensions.getroutedata)
+
+```csharp
+foreach (var value in httpContext.GetRouteData().Values)
+{
+ Console.WriteLine($"{value.Key} = {value.Value?.ToString()}");
+}
+```
+
+For example, the above code produces something like:
+
+```text
+controller = ConventionalRoute
+action = ActionWithStringParameter
+id = 2
+```
+
+### Information from the ActionDescriptor
+
+For requests that invoke an action or Razor page, the `ActionDescriptor` can
+be used to access route information.
+
+#### [AttributeRouteInfo.Template](https://learn.microsoft.com/dotnet/api/microsoft.aspnetcore.mvc.routing.attributerouteinfo.template)
+
+The `AttributeRouteInfo.Template` is equivalent to using
+[other APIs for retrieving the route template](#retrieving-the-route-template)
+when using attribute routing. For conventional routing and Razor pages it will
+be `null`.
+
+```csharp
+actionDescriptor.AttributeRouteInfo?.Template;
+```
+
+#### [ControllerActionDescriptor](https://learn.microsoft.com/dotnet/api/microsoft.aspnetcore.mvc.controllers.controlleractiondescriptor)
+
+For requests that invoke an action on a controller, the `ActionDescriptor`
+will be of type `ControllerActionDescriptor` which includes the controller and
+action name.
+
+```csharp
+(actionDescriptor as ControllerActionDescriptor)?.ControllerName;
+(actionDescriptor as ControllerActionDescriptor)?.ActionName;
+```
+
+#### [PageActionDescriptor](https://learn.microsoft.com/dotnet/api/microsoft.aspnetcore.mvc.razorpages.pageactiondescriptor)
+
+For requests that invoke a Razor page, the `ActionDescriptor`
+will be of type `PageActionDescriptor` which includes the path to the invoked
+page.
+
+```csharp
+(actionDescriptor as PageActionDescriptor)?.RelativePath;
+(actionDescriptor as PageActionDescriptor)?.ViewEnginePath;
+```
+
+#### [Parameters](https://learn.microsoft.com/dotnet/api/microsoft.aspnetcore.mvc.abstractions.actiondescriptor.parameters#microsoft-aspnetcore-mvc-abstractions-actiondescriptor-parameters)
+
+The `ActionDescriptor.Parameters` property is interesting because it describes
+the actual parameters (type and name) of an invoked action method. Some APM
+products use `ActionDescriptor.Parameters` to more precisely describe the
+method an endpoint invokes since not all parameters may be present in the
+route template.
+
+Consider the following action method:
+
+```csharp
+public IActionResult SomeActionMethod(string id, int num) { ... }
+```
+
+Using conventional routing assuming a default route template
+`{controller=ConventionalRoute}/{action=Default}/{id?}`, the `SomeActionMethod`
+may match this route template. The route template describes the `id` parameter
+but not the `num` parameter.
+
+```csharp
+foreach (var parameter in actionDescriptor.Parameters)
+{
+ Console.WriteLine($"{parameter.Name}");
+}
+```
+
+The above code produces:
+
+```text
+id
+num
+```
diff --git a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/README.net6.0.md b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/README.net6.0.md
new file mode 100644
index 00000000000..ac9ff0d5117
--- /dev/null
+++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/README.net6.0.md
@@ -0,0 +1,592 @@
+# Test results for ASP.NET Core 6
+
+| Span http.route | Metric http.route | App | Test Name |
+| - | - | - | - |
+| :broken_heart: | :broken_heart: | ConventionalRouting | [Root path](#conventionalrouting-root-path) |
+| :broken_heart: | :broken_heart: | ConventionalRouting | [Non-default action with route parameter and query string](#conventionalrouting-non-default-action-with-route-parameter-and-query-string) |
+| :broken_heart: | :broken_heart: | ConventionalRouting | [Non-default action with query string](#conventionalrouting-non-default-action-with-query-string) |
+| :broken_heart: | :broken_heart: | ConventionalRouting | [Not Found (404)](#conventionalrouting-not-found-404) |
+| :broken_heart: | :green_heart: | ConventionalRouting | [Route template with parameter constraint](#conventionalrouting-route-template-with-parameter-constraint) |
+| :broken_heart: | :green_heart: | ConventionalRouting | [Path that does not match parameter constraint](#conventionalrouting-path-that-does-not-match-parameter-constraint) |
+| :broken_heart: | :broken_heart: | ConventionalRouting | [Area using area:exists, default controller/action](#conventionalrouting-area-using-areaexists-default-controlleraction) |
+| :broken_heart: | :broken_heart: | ConventionalRouting | [Area using area:exists, non-default action](#conventionalrouting-area-using-areaexists-non-default-action) |
+| :broken_heart: | :broken_heart: | ConventionalRouting | [Area w/o area:exists, default controller/action](#conventionalrouting-area-wo-areaexists-default-controlleraction) |
+| :green_heart: | :green_heart: | AttributeRouting | [Default action](#attributerouting-default-action) |
+| :green_heart: | :green_heart: | AttributeRouting | [Action without parameter](#attributerouting-action-without-parameter) |
+| :green_heart: | :green_heart: | AttributeRouting | [Action with parameter](#attributerouting-action-with-parameter) |
+| :green_heart: | :green_heart: | AttributeRouting | [Action with parameter before action name in template](#attributerouting-action-with-parameter-before-action-name-in-template) |
+| :green_heart: | :green_heart: | AttributeRouting | [Action invoked resulting in 400 Bad Request](#attributerouting-action-invoked-resulting-in-400-bad-request) |
+| :broken_heart: | :broken_heart: | RazorPages | [Root path](#razorpages-root-path) |
+| :broken_heart: | :broken_heart: | RazorPages | [Index page](#razorpages-index-page) |
+| :broken_heart: | :broken_heart: | RazorPages | [Throws exception](#razorpages-throws-exception) |
+| :green_heart: | :green_heart: | RazorPages | [Static content](#razorpages-static-content) |
+| :broken_heart: | :green_heart: | MinimalApi | [Action without parameter](#minimalapi-action-without-parameter) |
+| :broken_heart: | :green_heart: | MinimalApi | [Action with parameter](#minimalapi-action-with-parameter) |
+
+## ConventionalRouting: Root path
+
+```json
+{
+ "IdealHttpRoute": "ConventionalRoute/Default/{id?}",
+ "ActivityDisplayName": "/",
+ "ActivityHttpRoute": "",
+ "MetricHttpRoute": "{controller=ConventionalRoute}/{action=Default}/{id?}",
+ "RouteInfo": {
+ "HttpMethod": "GET",
+ "Path": "/",
+ "RoutePattern.RawText": "{controller=ConventionalRoute}/{action=Default}/{id?}",
+ "IRouteDiagnosticsMetadata.Route": null,
+ "HttpContext.GetRouteData()": {
+ "controller": "ConventionalRoute",
+ "action": "Default"
+ },
+ "ActionDescriptor": {
+ "AttributeRouteInfo.Template": null,
+ "Parameters": [],
+ "ControllerActionDescriptor": {
+ "ControllerName": "ConventionalRoute",
+ "ActionName": "Default"
+ },
+ "PageActionDescriptor": null
+ }
+ }
+}
+```
+
+## ConventionalRouting: Non-default action with route parameter and query string
+
+```json
+{
+ "IdealHttpRoute": "ConventionalRoute/ActionWithStringParameter/{id?}",
+ "ActivityDisplayName": "/ConventionalRoute/ActionWithStringParameter/2",
+ "ActivityHttpRoute": "",
+ "MetricHttpRoute": "{controller=ConventionalRoute}/{action=Default}/{id?}",
+ "RouteInfo": {
+ "HttpMethod": "GET",
+ "Path": "/ConventionalRoute/ActionWithStringParameter/2?num=3",
+ "RoutePattern.RawText": "{controller=ConventionalRoute}/{action=Default}/{id?}",
+ "IRouteDiagnosticsMetadata.Route": null,
+ "HttpContext.GetRouteData()": {
+ "controller": "ConventionalRoute",
+ "action": "ActionWithStringParameter",
+ "id": "2"
+ },
+ "ActionDescriptor": {
+ "AttributeRouteInfo.Template": null,
+ "Parameters": [
+ "id",
+ "num"
+ ],
+ "ControllerActionDescriptor": {
+ "ControllerName": "ConventionalRoute",
+ "ActionName": "ActionWithStringParameter"
+ },
+ "PageActionDescriptor": null
+ }
+ }
+}
+```
+
+## ConventionalRouting: Non-default action with query string
+
+```json
+{
+ "IdealHttpRoute": "ConventionalRoute/ActionWithStringParameter/{id?}",
+ "ActivityDisplayName": "/ConventionalRoute/ActionWithStringParameter",
+ "ActivityHttpRoute": "",
+ "MetricHttpRoute": "{controller=ConventionalRoute}/{action=Default}/{id?}",
+ "RouteInfo": {
+ "HttpMethod": "GET",
+ "Path": "/ConventionalRoute/ActionWithStringParameter?num=3",
+ "RoutePattern.RawText": "{controller=ConventionalRoute}/{action=Default}/{id?}",
+ "IRouteDiagnosticsMetadata.Route": null,
+ "HttpContext.GetRouteData()": {
+ "controller": "ConventionalRoute",
+ "action": "ActionWithStringParameter"
+ },
+ "ActionDescriptor": {
+ "AttributeRouteInfo.Template": null,
+ "Parameters": [
+ "id",
+ "num"
+ ],
+ "ControllerActionDescriptor": {
+ "ControllerName": "ConventionalRoute",
+ "ActionName": "ActionWithStringParameter"
+ },
+ "PageActionDescriptor": null
+ }
+ }
+}
+```
+
+## ConventionalRouting: Not Found (404)
+
+```json
+{
+ "IdealHttpRoute": "",
+ "ActivityDisplayName": "/ConventionalRoute/NotFound",
+ "ActivityHttpRoute": "",
+ "MetricHttpRoute": "",
+ "RouteInfo": {
+ "HttpMethod": "GET",
+ "Path": "/ConventionalRoute/NotFound",
+ "RoutePattern.RawText": null,
+ "IRouteDiagnosticsMetadata.Route": null,
+ "HttpContext.GetRouteData()": {},
+ "ActionDescriptor": null
+ }
+}
+```
+
+## ConventionalRouting: Route template with parameter constraint
+
+```json
+{
+ "IdealHttpRoute": "SomePath/{id}/{num:int}",
+ "ActivityDisplayName": "/SomePath/SomeString/2",
+ "ActivityHttpRoute": "",
+ "MetricHttpRoute": "SomePath/{id}/{num:int}",
+ "RouteInfo": {
+ "HttpMethod": "GET",
+ "Path": "/SomePath/SomeString/2",
+ "RoutePattern.RawText": "SomePath/{id}/{num:int}",
+ "IRouteDiagnosticsMetadata.Route": null,
+ "HttpContext.GetRouteData()": {
+ "controller": "ConventionalRoute",
+ "action": "ActionWithStringParameter",
+ "id": "SomeString",
+ "num": "2"
+ },
+ "ActionDescriptor": {
+ "AttributeRouteInfo.Template": null,
+ "Parameters": [
+ "id",
+ "num"
+ ],
+ "ControllerActionDescriptor": {
+ "ControllerName": "ConventionalRoute",
+ "ActionName": "ActionWithStringParameter"
+ },
+ "PageActionDescriptor": null
+ }
+ }
+}
+```
+
+## ConventionalRouting: Path that does not match parameter constraint
+
+```json
+{
+ "IdealHttpRoute": "",
+ "ActivityDisplayName": "/SomePath/SomeString/NotAnInt",
+ "ActivityHttpRoute": "",
+ "MetricHttpRoute": "",
+ "RouteInfo": {
+ "HttpMethod": "GET",
+ "Path": "/SomePath/SomeString/NotAnInt",
+ "RoutePattern.RawText": null,
+ "IRouteDiagnosticsMetadata.Route": null,
+ "HttpContext.GetRouteData()": {},
+ "ActionDescriptor": null
+ }
+}
+```
+
+## ConventionalRouting: Area using area:exists, default controller/action
+
+```json
+{
+ "IdealHttpRoute": "{area:exists}/ControllerForMyArea/Default/{id?}",
+ "ActivityDisplayName": "/MyArea",
+ "ActivityHttpRoute": "",
+ "MetricHttpRoute": "{area:exists}/{controller=ControllerForMyArea}/{action=Default}/{id?}",
+ "RouteInfo": {
+ "HttpMethod": "GET",
+ "Path": "/MyArea",
+ "RoutePattern.RawText": "{area:exists}/{controller=ControllerForMyArea}/{action=Default}/{id?}",
+ "IRouteDiagnosticsMetadata.Route": null,
+ "HttpContext.GetRouteData()": {
+ "controller": "ControllerForMyArea",
+ "action": "Default",
+ "area": "MyArea"
+ },
+ "ActionDescriptor": {
+ "AttributeRouteInfo.Template": null,
+ "Parameters": [],
+ "ControllerActionDescriptor": {
+ "ControllerName": "ControllerForMyArea",
+ "ActionName": "Default"
+ },
+ "PageActionDescriptor": null
+ }
+ }
+}
+```
+
+## ConventionalRouting: Area using area:exists, non-default action
+
+```json
+{
+ "IdealHttpRoute": "{area:exists}/ControllerForMyArea/NonDefault/{id?}",
+ "ActivityDisplayName": "/MyArea/ControllerForMyArea/NonDefault",
+ "ActivityHttpRoute": "",
+ "MetricHttpRoute": "{area:exists}/{controller=ControllerForMyArea}/{action=Default}/{id?}",
+ "RouteInfo": {
+ "HttpMethod": "GET",
+ "Path": "/MyArea/ControllerForMyArea/NonDefault",
+ "RoutePattern.RawText": "{area:exists}/{controller=ControllerForMyArea}/{action=Default}/{id?}",
+ "IRouteDiagnosticsMetadata.Route": null,
+ "HttpContext.GetRouteData()": {
+ "controller": "ControllerForMyArea",
+ "area": "MyArea",
+ "action": "NonDefault"
+ },
+ "ActionDescriptor": {
+ "AttributeRouteInfo.Template": null,
+ "Parameters": [],
+ "ControllerActionDescriptor": {
+ "ControllerName": "ControllerForMyArea",
+ "ActionName": "NonDefault"
+ },
+ "PageActionDescriptor": null
+ }
+ }
+}
+```
+
+## ConventionalRouting: Area w/o area:exists, default controller/action
+
+```json
+{
+ "IdealHttpRoute": "SomePrefix/AnotherArea/Index/{id?}",
+ "ActivityDisplayName": "/SomePrefix",
+ "ActivityHttpRoute": "",
+ "MetricHttpRoute": "SomePrefix/{controller=AnotherArea}/{action=Index}/{id?}",
+ "RouteInfo": {
+ "HttpMethod": "GET",
+ "Path": "/SomePrefix",
+ "RoutePattern.RawText": "SomePrefix/{controller=AnotherArea}/{action=Index}/{id?}",
+ "IRouteDiagnosticsMetadata.Route": null,
+ "HttpContext.GetRouteData()": {
+ "area": "AnotherArea",
+ "controller": "AnotherArea",
+ "action": "Index"
+ },
+ "ActionDescriptor": {
+ "AttributeRouteInfo.Template": null,
+ "Parameters": [],
+ "ControllerActionDescriptor": {
+ "ControllerName": "AnotherArea",
+ "ActionName": "Index"
+ },
+ "PageActionDescriptor": null
+ }
+ }
+}
+```
+
+## AttributeRouting: Default action
+
+```json
+{
+ "IdealHttpRoute": "AttributeRoute",
+ "ActivityDisplayName": "AttributeRoute",
+ "ActivityHttpRoute": "AttributeRoute",
+ "MetricHttpRoute": "AttributeRoute",
+ "RouteInfo": {
+ "HttpMethod": "GET",
+ "Path": "/AttributeRoute",
+ "RoutePattern.RawText": "AttributeRoute",
+ "IRouteDiagnosticsMetadata.Route": null,
+ "HttpContext.GetRouteData()": {
+ "action": "Get",
+ "controller": "AttributeRoute"
+ },
+ "ActionDescriptor": {
+ "AttributeRouteInfo.Template": "AttributeRoute",
+ "Parameters": [],
+ "ControllerActionDescriptor": {
+ "ControllerName": "AttributeRoute",
+ "ActionName": "Get"
+ },
+ "PageActionDescriptor": null
+ }
+ }
+}
+```
+
+## AttributeRouting: Action without parameter
+
+```json
+{
+ "IdealHttpRoute": "AttributeRoute/Get",
+ "ActivityDisplayName": "AttributeRoute/Get",
+ "ActivityHttpRoute": "AttributeRoute/Get",
+ "MetricHttpRoute": "AttributeRoute/Get",
+ "RouteInfo": {
+ "HttpMethod": "GET",
+ "Path": "/AttributeRoute/Get",
+ "RoutePattern.RawText": "AttributeRoute/Get",
+ "IRouteDiagnosticsMetadata.Route": null,
+ "HttpContext.GetRouteData()": {
+ "action": "Get",
+ "controller": "AttributeRoute"
+ },
+ "ActionDescriptor": {
+ "AttributeRouteInfo.Template": "AttributeRoute/Get",
+ "Parameters": [],
+ "ControllerActionDescriptor": {
+ "ControllerName": "AttributeRoute",
+ "ActionName": "Get"
+ },
+ "PageActionDescriptor": null
+ }
+ }
+}
+```
+
+## AttributeRouting: Action with parameter
+
+```json
+{
+ "IdealHttpRoute": "AttributeRoute/Get/{id}",
+ "ActivityDisplayName": "AttributeRoute/Get/{id}",
+ "ActivityHttpRoute": "AttributeRoute/Get/{id}",
+ "MetricHttpRoute": "AttributeRoute/Get/{id}",
+ "RouteInfo": {
+ "HttpMethod": "GET",
+ "Path": "/AttributeRoute/Get/12",
+ "RoutePattern.RawText": "AttributeRoute/Get/{id}",
+ "IRouteDiagnosticsMetadata.Route": null,
+ "HttpContext.GetRouteData()": {
+ "action": "Get",
+ "controller": "AttributeRoute",
+ "id": "12"
+ },
+ "ActionDescriptor": {
+ "AttributeRouteInfo.Template": "AttributeRoute/Get/{id}",
+ "Parameters": [
+ "id"
+ ],
+ "ControllerActionDescriptor": {
+ "ControllerName": "AttributeRoute",
+ "ActionName": "Get"
+ },
+ "PageActionDescriptor": null
+ }
+ }
+}
+```
+
+## AttributeRouting: Action with parameter before action name in template
+
+```json
+{
+ "IdealHttpRoute": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate",
+ "ActivityDisplayName": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate",
+ "ActivityHttpRoute": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate",
+ "MetricHttpRoute": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate",
+ "RouteInfo": {
+ "HttpMethod": "GET",
+ "Path": "/AttributeRoute/12/GetWithActionNameInDifferentSpotInTemplate",
+ "RoutePattern.RawText": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate",
+ "IRouteDiagnosticsMetadata.Route": null,
+ "HttpContext.GetRouteData()": {
+ "action": "GetWithActionNameInDifferentSpotInTemplate",
+ "controller": "AttributeRoute",
+ "id": "12"
+ },
+ "ActionDescriptor": {
+ "AttributeRouteInfo.Template": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate",
+ "Parameters": [
+ "id"
+ ],
+ "ControllerActionDescriptor": {
+ "ControllerName": "AttributeRoute",
+ "ActionName": "GetWithActionNameInDifferentSpotInTemplate"
+ },
+ "PageActionDescriptor": null
+ }
+ }
+}
+```
+
+## AttributeRouting: Action invoked resulting in 400 Bad Request
+
+```json
+{
+ "IdealHttpRoute": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate",
+ "ActivityDisplayName": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate",
+ "ActivityHttpRoute": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate",
+ "MetricHttpRoute": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate",
+ "RouteInfo": {
+ "HttpMethod": "GET",
+ "Path": "/AttributeRoute/NotAnInt/GetWithActionNameInDifferentSpotInTemplate",
+ "RoutePattern.RawText": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate",
+ "IRouteDiagnosticsMetadata.Route": null,
+ "HttpContext.GetRouteData()": {
+ "action": "GetWithActionNameInDifferentSpotInTemplate",
+ "controller": "AttributeRoute",
+ "id": "NotAnInt"
+ },
+ "ActionDescriptor": {
+ "AttributeRouteInfo.Template": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate",
+ "Parameters": [
+ "id"
+ ],
+ "ControllerActionDescriptor": {
+ "ControllerName": "AttributeRoute",
+ "ActionName": "GetWithActionNameInDifferentSpotInTemplate"
+ },
+ "PageActionDescriptor": null
+ }
+ }
+}
+```
+
+## RazorPages: Root path
+
+```json
+{
+ "IdealHttpRoute": "/Index",
+ "ActivityDisplayName": "/",
+ "ActivityHttpRoute": "",
+ "MetricHttpRoute": "",
+ "RouteInfo": {
+ "HttpMethod": "GET",
+ "Path": "/",
+ "RoutePattern.RawText": "",
+ "IRouteDiagnosticsMetadata.Route": null,
+ "HttpContext.GetRouteData()": {
+ "page": "/Index"
+ },
+ "ActionDescriptor": {
+ "AttributeRouteInfo.Template": "",
+ "Parameters": [],
+ "ControllerActionDescriptor": null,
+ "PageActionDescriptor": {
+ "RelativePath": "/Pages/Index.cshtml",
+ "ViewEnginePath": "/Index"
+ }
+ }
+ }
+}
+```
+
+## RazorPages: Index page
+
+```json
+{
+ "IdealHttpRoute": "/Index",
+ "ActivityDisplayName": "Index",
+ "ActivityHttpRoute": "Index",
+ "MetricHttpRoute": "Index",
+ "RouteInfo": {
+ "HttpMethod": "GET",
+ "Path": "/Index",
+ "RoutePattern.RawText": "Index",
+ "IRouteDiagnosticsMetadata.Route": null,
+ "HttpContext.GetRouteData()": {
+ "page": "/Index"
+ },
+ "ActionDescriptor": {
+ "AttributeRouteInfo.Template": "Index",
+ "Parameters": [],
+ "ControllerActionDescriptor": null,
+ "PageActionDescriptor": {
+ "RelativePath": "/Pages/Index.cshtml",
+ "ViewEnginePath": "/Index"
+ }
+ }
+ }
+}
+```
+
+## RazorPages: Throws exception
+
+```json
+{
+ "IdealHttpRoute": "/PageThatThrowsException",
+ "ActivityDisplayName": "PageThatThrowsException",
+ "ActivityHttpRoute": "PageThatThrowsException",
+ "MetricHttpRoute": "PageThatThrowsException",
+ "RouteInfo": {
+ "HttpMethod": "GET",
+ "Path": "/PageThatThrowsException",
+ "RoutePattern.RawText": "PageThatThrowsException",
+ "IRouteDiagnosticsMetadata.Route": null,
+ "HttpContext.GetRouteData()": {
+ "page": "/PageThatThrowsException"
+ },
+ "ActionDescriptor": {
+ "AttributeRouteInfo.Template": "PageThatThrowsException",
+ "Parameters": [],
+ "ControllerActionDescriptor": null,
+ "PageActionDescriptor": {
+ "RelativePath": "/Pages/PageThatThrowsException.cshtml",
+ "ViewEnginePath": "/PageThatThrowsException"
+ }
+ }
+ }
+}
+```
+
+## RazorPages: Static content
+
+```json
+{
+ "IdealHttpRoute": "",
+ "ActivityDisplayName": "/js/site.js",
+ "ActivityHttpRoute": "",
+ "MetricHttpRoute": "",
+ "RouteInfo": {
+ "HttpMethod": "GET",
+ "Path": "/js/site.js",
+ "RoutePattern.RawText": null,
+ "IRouteDiagnosticsMetadata.Route": null,
+ "HttpContext.GetRouteData()": {},
+ "ActionDescriptor": null
+ }
+}
+```
+
+## MinimalApi: Action without parameter
+
+```json
+{
+ "IdealHttpRoute": "/MinimalApi",
+ "ActivityDisplayName": "/MinimalApi",
+ "ActivityHttpRoute": "",
+ "MetricHttpRoute": "/MinimalApi",
+ "RouteInfo": {
+ "HttpMethod": "GET",
+ "Path": "/MinimalApi",
+ "RoutePattern.RawText": "/MinimalApi",
+ "IRouteDiagnosticsMetadata.Route": null,
+ "HttpContext.GetRouteData()": {},
+ "ActionDescriptor": null
+ }
+}
+```
+
+## MinimalApi: Action with parameter
+
+```json
+{
+ "IdealHttpRoute": "/MinimalApi/{id}",
+ "ActivityDisplayName": "/MinimalApi/123",
+ "ActivityHttpRoute": "",
+ "MetricHttpRoute": "/MinimalApi/{id}",
+ "RouteInfo": {
+ "HttpMethod": "GET",
+ "Path": "/MinimalApi/123",
+ "RoutePattern.RawText": "/MinimalApi/{id}",
+ "IRouteDiagnosticsMetadata.Route": null,
+ "HttpContext.GetRouteData()": {
+ "id": "123"
+ },
+ "ActionDescriptor": null
+ }
+}
+```
diff --git a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/README.net7.0.md b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/README.net7.0.md
new file mode 100644
index 00000000000..f93e5de8ea8
--- /dev/null
+++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/README.net7.0.md
@@ -0,0 +1,634 @@
+# Test results for ASP.NET Core 7
+
+| Span http.route | Metric http.route | App | Test Name |
+| - | - | - | - |
+| :broken_heart: | :broken_heart: | ConventionalRouting | [Root path](#conventionalrouting-root-path) |
+| :broken_heart: | :broken_heart: | ConventionalRouting | [Non-default action with route parameter and query string](#conventionalrouting-non-default-action-with-route-parameter-and-query-string) |
+| :broken_heart: | :broken_heart: | ConventionalRouting | [Non-default action with query string](#conventionalrouting-non-default-action-with-query-string) |
+| :broken_heart: | :broken_heart: | ConventionalRouting | [Not Found (404)](#conventionalrouting-not-found-404) |
+| :broken_heart: | :green_heart: | ConventionalRouting | [Route template with parameter constraint](#conventionalrouting-route-template-with-parameter-constraint) |
+| :broken_heart: | :green_heart: | ConventionalRouting | [Path that does not match parameter constraint](#conventionalrouting-path-that-does-not-match-parameter-constraint) |
+| :broken_heart: | :broken_heart: | ConventionalRouting | [Area using area:exists, default controller/action](#conventionalrouting-area-using-areaexists-default-controlleraction) |
+| :broken_heart: | :broken_heart: | ConventionalRouting | [Area using area:exists, non-default action](#conventionalrouting-area-using-areaexists-non-default-action) |
+| :broken_heart: | :broken_heart: | ConventionalRouting | [Area w/o area:exists, default controller/action](#conventionalrouting-area-wo-areaexists-default-controlleraction) |
+| :green_heart: | :green_heart: | AttributeRouting | [Default action](#attributerouting-default-action) |
+| :green_heart: | :green_heart: | AttributeRouting | [Action without parameter](#attributerouting-action-without-parameter) |
+| :green_heart: | :green_heart: | AttributeRouting | [Action with parameter](#attributerouting-action-with-parameter) |
+| :green_heart: | :green_heart: | AttributeRouting | [Action with parameter before action name in template](#attributerouting-action-with-parameter-before-action-name-in-template) |
+| :green_heart: | :green_heart: | AttributeRouting | [Action invoked resulting in 400 Bad Request](#attributerouting-action-invoked-resulting-in-400-bad-request) |
+| :broken_heart: | :broken_heart: | RazorPages | [Root path](#razorpages-root-path) |
+| :broken_heart: | :broken_heart: | RazorPages | [Index page](#razorpages-index-page) |
+| :broken_heart: | :broken_heart: | RazorPages | [Throws exception](#razorpages-throws-exception) |
+| :green_heart: | :green_heart: | RazorPages | [Static content](#razorpages-static-content) |
+| :broken_heart: | :green_heart: | MinimalApi | [Action without parameter](#minimalapi-action-without-parameter) |
+| :broken_heart: | :green_heart: | MinimalApi | [Action with parameter](#minimalapi-action-with-parameter) |
+| :broken_heart: | :green_heart: | MinimalApi | [Action without parameter (MapGroup)](#minimalapi-action-without-parameter-mapgroup) |
+| :broken_heart: | :green_heart: | MinimalApi | [Action with parameter (MapGroup)](#minimalapi-action-with-parameter-mapgroup) |
+
+## ConventionalRouting: Root path
+
+```json
+{
+ "IdealHttpRoute": "ConventionalRoute/Default/{id?}",
+ "ActivityDisplayName": "/",
+ "ActivityHttpRoute": "",
+ "MetricHttpRoute": "{controller=ConventionalRoute}/{action=Default}/{id?}",
+ "RouteInfo": {
+ "HttpMethod": "GET",
+ "Path": "/",
+ "RoutePattern.RawText": "{controller=ConventionalRoute}/{action=Default}/{id?}",
+ "IRouteDiagnosticsMetadata.Route": null,
+ "HttpContext.GetRouteData()": {
+ "controller": "ConventionalRoute",
+ "action": "Default"
+ },
+ "ActionDescriptor": {
+ "AttributeRouteInfo.Template": null,
+ "Parameters": [],
+ "ControllerActionDescriptor": {
+ "ControllerName": "ConventionalRoute",
+ "ActionName": "Default"
+ },
+ "PageActionDescriptor": null
+ }
+ }
+}
+```
+
+## ConventionalRouting: Non-default action with route parameter and query string
+
+```json
+{
+ "IdealHttpRoute": "ConventionalRoute/ActionWithStringParameter/{id?}",
+ "ActivityDisplayName": "/ConventionalRoute/ActionWithStringParameter/2",
+ "ActivityHttpRoute": "",
+ "MetricHttpRoute": "{controller=ConventionalRoute}/{action=Default}/{id?}",
+ "RouteInfo": {
+ "HttpMethod": "GET",
+ "Path": "/ConventionalRoute/ActionWithStringParameter/2?num=3",
+ "RoutePattern.RawText": "{controller=ConventionalRoute}/{action=Default}/{id?}",
+ "IRouteDiagnosticsMetadata.Route": null,
+ "HttpContext.GetRouteData()": {
+ "controller": "ConventionalRoute",
+ "action": "ActionWithStringParameter",
+ "id": "2"
+ },
+ "ActionDescriptor": {
+ "AttributeRouteInfo.Template": null,
+ "Parameters": [
+ "id",
+ "num"
+ ],
+ "ControllerActionDescriptor": {
+ "ControllerName": "ConventionalRoute",
+ "ActionName": "ActionWithStringParameter"
+ },
+ "PageActionDescriptor": null
+ }
+ }
+}
+```
+
+## ConventionalRouting: Non-default action with query string
+
+```json
+{
+ "IdealHttpRoute": "ConventionalRoute/ActionWithStringParameter/{id?}",
+ "ActivityDisplayName": "/ConventionalRoute/ActionWithStringParameter",
+ "ActivityHttpRoute": "",
+ "MetricHttpRoute": "{controller=ConventionalRoute}/{action=Default}/{id?}",
+ "RouteInfo": {
+ "HttpMethod": "GET",
+ "Path": "/ConventionalRoute/ActionWithStringParameter?num=3",
+ "RoutePattern.RawText": "{controller=ConventionalRoute}/{action=Default}/{id?}",
+ "IRouteDiagnosticsMetadata.Route": null,
+ "HttpContext.GetRouteData()": {
+ "controller": "ConventionalRoute",
+ "action": "ActionWithStringParameter"
+ },
+ "ActionDescriptor": {
+ "AttributeRouteInfo.Template": null,
+ "Parameters": [
+ "id",
+ "num"
+ ],
+ "ControllerActionDescriptor": {
+ "ControllerName": "ConventionalRoute",
+ "ActionName": "ActionWithStringParameter"
+ },
+ "PageActionDescriptor": null
+ }
+ }
+}
+```
+
+## ConventionalRouting: Not Found (404)
+
+```json
+{
+ "IdealHttpRoute": "",
+ "ActivityDisplayName": "/ConventionalRoute/NotFound",
+ "ActivityHttpRoute": "",
+ "MetricHttpRoute": "",
+ "RouteInfo": {
+ "HttpMethod": "GET",
+ "Path": "/ConventionalRoute/NotFound",
+ "RoutePattern.RawText": null,
+ "IRouteDiagnosticsMetadata.Route": null,
+ "HttpContext.GetRouteData()": {},
+ "ActionDescriptor": null
+ }
+}
+```
+
+## ConventionalRouting: Route template with parameter constraint
+
+```json
+{
+ "IdealHttpRoute": "SomePath/{id}/{num:int}",
+ "ActivityDisplayName": "/SomePath/SomeString/2",
+ "ActivityHttpRoute": "",
+ "MetricHttpRoute": "SomePath/{id}/{num:int}",
+ "RouteInfo": {
+ "HttpMethod": "GET",
+ "Path": "/SomePath/SomeString/2",
+ "RoutePattern.RawText": "SomePath/{id}/{num:int}",
+ "IRouteDiagnosticsMetadata.Route": null,
+ "HttpContext.GetRouteData()": {
+ "controller": "ConventionalRoute",
+ "action": "ActionWithStringParameter",
+ "id": "SomeString",
+ "num": "2"
+ },
+ "ActionDescriptor": {
+ "AttributeRouteInfo.Template": null,
+ "Parameters": [
+ "id",
+ "num"
+ ],
+ "ControllerActionDescriptor": {
+ "ControllerName": "ConventionalRoute",
+ "ActionName": "ActionWithStringParameter"
+ },
+ "PageActionDescriptor": null
+ }
+ }
+}
+```
+
+## ConventionalRouting: Path that does not match parameter constraint
+
+```json
+{
+ "IdealHttpRoute": "",
+ "ActivityDisplayName": "/SomePath/SomeString/NotAnInt",
+ "ActivityHttpRoute": "",
+ "MetricHttpRoute": "",
+ "RouteInfo": {
+ "HttpMethod": "GET",
+ "Path": "/SomePath/SomeString/NotAnInt",
+ "RoutePattern.RawText": null,
+ "IRouteDiagnosticsMetadata.Route": null,
+ "HttpContext.GetRouteData()": {},
+ "ActionDescriptor": null
+ }
+}
+```
+
+## ConventionalRouting: Area using area:exists, default controller/action
+
+```json
+{
+ "IdealHttpRoute": "{area:exists}/ControllerForMyArea/Default/{id?}",
+ "ActivityDisplayName": "/MyArea",
+ "ActivityHttpRoute": "",
+ "MetricHttpRoute": "{area:exists}/{controller=ControllerForMyArea}/{action=Default}/{id?}",
+ "RouteInfo": {
+ "HttpMethod": "GET",
+ "Path": "/MyArea",
+ "RoutePattern.RawText": "{area:exists}/{controller=ControllerForMyArea}/{action=Default}/{id?}",
+ "IRouteDiagnosticsMetadata.Route": null,
+ "HttpContext.GetRouteData()": {
+ "controller": "ControllerForMyArea",
+ "action": "Default",
+ "area": "MyArea"
+ },
+ "ActionDescriptor": {
+ "AttributeRouteInfo.Template": null,
+ "Parameters": [],
+ "ControllerActionDescriptor": {
+ "ControllerName": "ControllerForMyArea",
+ "ActionName": "Default"
+ },
+ "PageActionDescriptor": null
+ }
+ }
+}
+```
+
+## ConventionalRouting: Area using area:exists, non-default action
+
+```json
+{
+ "IdealHttpRoute": "{area:exists}/ControllerForMyArea/NonDefault/{id?}",
+ "ActivityDisplayName": "/MyArea/ControllerForMyArea/NonDefault",
+ "ActivityHttpRoute": "",
+ "MetricHttpRoute": "{area:exists}/{controller=ControllerForMyArea}/{action=Default}/{id?}",
+ "RouteInfo": {
+ "HttpMethod": "GET",
+ "Path": "/MyArea/ControllerForMyArea/NonDefault",
+ "RoutePattern.RawText": "{area:exists}/{controller=ControllerForMyArea}/{action=Default}/{id?}",
+ "IRouteDiagnosticsMetadata.Route": null,
+ "HttpContext.GetRouteData()": {
+ "controller": "ControllerForMyArea",
+ "area": "MyArea",
+ "action": "NonDefault"
+ },
+ "ActionDescriptor": {
+ "AttributeRouteInfo.Template": null,
+ "Parameters": [],
+ "ControllerActionDescriptor": {
+ "ControllerName": "ControllerForMyArea",
+ "ActionName": "NonDefault"
+ },
+ "PageActionDescriptor": null
+ }
+ }
+}
+```
+
+## ConventionalRouting: Area w/o area:exists, default controller/action
+
+```json
+{
+ "IdealHttpRoute": "SomePrefix/AnotherArea/Index/{id?}",
+ "ActivityDisplayName": "/SomePrefix",
+ "ActivityHttpRoute": "",
+ "MetricHttpRoute": "SomePrefix/{controller=AnotherArea}/{action=Index}/{id?}",
+ "RouteInfo": {
+ "HttpMethod": "GET",
+ "Path": "/SomePrefix",
+ "RoutePattern.RawText": "SomePrefix/{controller=AnotherArea}/{action=Index}/{id?}",
+ "IRouteDiagnosticsMetadata.Route": null,
+ "HttpContext.GetRouteData()": {
+ "area": "AnotherArea",
+ "controller": "AnotherArea",
+ "action": "Index"
+ },
+ "ActionDescriptor": {
+ "AttributeRouteInfo.Template": null,
+ "Parameters": [],
+ "ControllerActionDescriptor": {
+ "ControllerName": "AnotherArea",
+ "ActionName": "Index"
+ },
+ "PageActionDescriptor": null
+ }
+ }
+}
+```
+
+## AttributeRouting: Default action
+
+```json
+{
+ "IdealHttpRoute": "AttributeRoute",
+ "ActivityDisplayName": "AttributeRoute",
+ "ActivityHttpRoute": "AttributeRoute",
+ "MetricHttpRoute": "AttributeRoute",
+ "RouteInfo": {
+ "HttpMethod": "GET",
+ "Path": "/AttributeRoute",
+ "RoutePattern.RawText": "AttributeRoute",
+ "IRouteDiagnosticsMetadata.Route": null,
+ "HttpContext.GetRouteData()": {
+ "action": "Get",
+ "controller": "AttributeRoute"
+ },
+ "ActionDescriptor": {
+ "AttributeRouteInfo.Template": "AttributeRoute",
+ "Parameters": [],
+ "ControllerActionDescriptor": {
+ "ControllerName": "AttributeRoute",
+ "ActionName": "Get"
+ },
+ "PageActionDescriptor": null
+ }
+ }
+}
+```
+
+## AttributeRouting: Action without parameter
+
+```json
+{
+ "IdealHttpRoute": "AttributeRoute/Get",
+ "ActivityDisplayName": "AttributeRoute/Get",
+ "ActivityHttpRoute": "AttributeRoute/Get",
+ "MetricHttpRoute": "AttributeRoute/Get",
+ "RouteInfo": {
+ "HttpMethod": "GET",
+ "Path": "/AttributeRoute/Get",
+ "RoutePattern.RawText": "AttributeRoute/Get",
+ "IRouteDiagnosticsMetadata.Route": null,
+ "HttpContext.GetRouteData()": {
+ "action": "Get",
+ "controller": "AttributeRoute"
+ },
+ "ActionDescriptor": {
+ "AttributeRouteInfo.Template": "AttributeRoute/Get",
+ "Parameters": [],
+ "ControllerActionDescriptor": {
+ "ControllerName": "AttributeRoute",
+ "ActionName": "Get"
+ },
+ "PageActionDescriptor": null
+ }
+ }
+}
+```
+
+## AttributeRouting: Action with parameter
+
+```json
+{
+ "IdealHttpRoute": "AttributeRoute/Get/{id}",
+ "ActivityDisplayName": "AttributeRoute/Get/{id}",
+ "ActivityHttpRoute": "AttributeRoute/Get/{id}",
+ "MetricHttpRoute": "AttributeRoute/Get/{id}",
+ "RouteInfo": {
+ "HttpMethod": "GET",
+ "Path": "/AttributeRoute/Get/12",
+ "RoutePattern.RawText": "AttributeRoute/Get/{id}",
+ "IRouteDiagnosticsMetadata.Route": null,
+ "HttpContext.GetRouteData()": {
+ "action": "Get",
+ "controller": "AttributeRoute",
+ "id": "12"
+ },
+ "ActionDescriptor": {
+ "AttributeRouteInfo.Template": "AttributeRoute/Get/{id}",
+ "Parameters": [
+ "id"
+ ],
+ "ControllerActionDescriptor": {
+ "ControllerName": "AttributeRoute",
+ "ActionName": "Get"
+ },
+ "PageActionDescriptor": null
+ }
+ }
+}
+```
+
+## AttributeRouting: Action with parameter before action name in template
+
+```json
+{
+ "IdealHttpRoute": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate",
+ "ActivityDisplayName": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate",
+ "ActivityHttpRoute": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate",
+ "MetricHttpRoute": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate",
+ "RouteInfo": {
+ "HttpMethod": "GET",
+ "Path": "/AttributeRoute/12/GetWithActionNameInDifferentSpotInTemplate",
+ "RoutePattern.RawText": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate",
+ "IRouteDiagnosticsMetadata.Route": null,
+ "HttpContext.GetRouteData()": {
+ "action": "GetWithActionNameInDifferentSpotInTemplate",
+ "controller": "AttributeRoute",
+ "id": "12"
+ },
+ "ActionDescriptor": {
+ "AttributeRouteInfo.Template": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate",
+ "Parameters": [
+ "id"
+ ],
+ "ControllerActionDescriptor": {
+ "ControllerName": "AttributeRoute",
+ "ActionName": "GetWithActionNameInDifferentSpotInTemplate"
+ },
+ "PageActionDescriptor": null
+ }
+ }
+}
+```
+
+## AttributeRouting: Action invoked resulting in 400 Bad Request
+
+```json
+{
+ "IdealHttpRoute": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate",
+ "ActivityDisplayName": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate",
+ "ActivityHttpRoute": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate",
+ "MetricHttpRoute": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate",
+ "RouteInfo": {
+ "HttpMethod": "GET",
+ "Path": "/AttributeRoute/NotAnInt/GetWithActionNameInDifferentSpotInTemplate",
+ "RoutePattern.RawText": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate",
+ "IRouteDiagnosticsMetadata.Route": null,
+ "HttpContext.GetRouteData()": {
+ "action": "GetWithActionNameInDifferentSpotInTemplate",
+ "controller": "AttributeRoute",
+ "id": "NotAnInt"
+ },
+ "ActionDescriptor": {
+ "AttributeRouteInfo.Template": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate",
+ "Parameters": [
+ "id"
+ ],
+ "ControllerActionDescriptor": {
+ "ControllerName": "AttributeRoute",
+ "ActionName": "GetWithActionNameInDifferentSpotInTemplate"
+ },
+ "PageActionDescriptor": null
+ }
+ }
+}
+```
+
+## RazorPages: Root path
+
+```json
+{
+ "IdealHttpRoute": "/Index",
+ "ActivityDisplayName": "/",
+ "ActivityHttpRoute": "",
+ "MetricHttpRoute": "",
+ "RouteInfo": {
+ "HttpMethod": "GET",
+ "Path": "/",
+ "RoutePattern.RawText": "",
+ "IRouteDiagnosticsMetadata.Route": null,
+ "HttpContext.GetRouteData()": {
+ "page": "/Index"
+ },
+ "ActionDescriptor": {
+ "AttributeRouteInfo.Template": "",
+ "Parameters": [],
+ "ControllerActionDescriptor": null,
+ "PageActionDescriptor": {
+ "RelativePath": "/Pages/Index.cshtml",
+ "ViewEnginePath": "/Index"
+ }
+ }
+ }
+}
+```
+
+## RazorPages: Index page
+
+```json
+{
+ "IdealHttpRoute": "/Index",
+ "ActivityDisplayName": "Index",
+ "ActivityHttpRoute": "Index",
+ "MetricHttpRoute": "Index",
+ "RouteInfo": {
+ "HttpMethod": "GET",
+ "Path": "/Index",
+ "RoutePattern.RawText": "Index",
+ "IRouteDiagnosticsMetadata.Route": null,
+ "HttpContext.GetRouteData()": {
+ "page": "/Index"
+ },
+ "ActionDescriptor": {
+ "AttributeRouteInfo.Template": "Index",
+ "Parameters": [],
+ "ControllerActionDescriptor": null,
+ "PageActionDescriptor": {
+ "RelativePath": "/Pages/Index.cshtml",
+ "ViewEnginePath": "/Index"
+ }
+ }
+ }
+}
+```
+
+## RazorPages: Throws exception
+
+```json
+{
+ "IdealHttpRoute": "/PageThatThrowsException",
+ "ActivityDisplayName": "PageThatThrowsException",
+ "ActivityHttpRoute": "PageThatThrowsException",
+ "MetricHttpRoute": "PageThatThrowsException",
+ "RouteInfo": {
+ "HttpMethod": "GET",
+ "Path": "/PageThatThrowsException",
+ "RoutePattern.RawText": "PageThatThrowsException",
+ "IRouteDiagnosticsMetadata.Route": null,
+ "HttpContext.GetRouteData()": {
+ "page": "/PageThatThrowsException"
+ },
+ "ActionDescriptor": {
+ "AttributeRouteInfo.Template": "PageThatThrowsException",
+ "Parameters": [],
+ "ControllerActionDescriptor": null,
+ "PageActionDescriptor": {
+ "RelativePath": "/Pages/PageThatThrowsException.cshtml",
+ "ViewEnginePath": "/PageThatThrowsException"
+ }
+ }
+ }
+}
+```
+
+## RazorPages: Static content
+
+```json
+{
+ "IdealHttpRoute": "",
+ "ActivityDisplayName": "/js/site.js",
+ "ActivityHttpRoute": "",
+ "MetricHttpRoute": "",
+ "RouteInfo": {
+ "HttpMethod": "GET",
+ "Path": "/js/site.js",
+ "RoutePattern.RawText": null,
+ "IRouteDiagnosticsMetadata.Route": null,
+ "HttpContext.GetRouteData()": {},
+ "ActionDescriptor": null
+ }
+}
+```
+
+## MinimalApi: Action without parameter
+
+```json
+{
+ "IdealHttpRoute": "/MinimalApi",
+ "ActivityDisplayName": "/MinimalApi",
+ "ActivityHttpRoute": "",
+ "MetricHttpRoute": "/MinimalApi",
+ "RouteInfo": {
+ "HttpMethod": "GET",
+ "Path": "/MinimalApi",
+ "RoutePattern.RawText": "/MinimalApi",
+ "IRouteDiagnosticsMetadata.Route": null,
+ "HttpContext.GetRouteData()": {},
+ "ActionDescriptor": null
+ }
+}
+```
+
+## MinimalApi: Action with parameter
+
+```json
+{
+ "IdealHttpRoute": "/MinimalApi/{id}",
+ "ActivityDisplayName": "/MinimalApi/123",
+ "ActivityHttpRoute": "",
+ "MetricHttpRoute": "/MinimalApi/{id}",
+ "RouteInfo": {
+ "HttpMethod": "GET",
+ "Path": "/MinimalApi/123",
+ "RoutePattern.RawText": "/MinimalApi/{id}",
+ "IRouteDiagnosticsMetadata.Route": null,
+ "HttpContext.GetRouteData()": {
+ "id": "123"
+ },
+ "ActionDescriptor": null
+ }
+}
+```
+
+## MinimalApi: Action without parameter (MapGroup)
+
+```json
+{
+ "IdealHttpRoute": "/MinimalApiUsingMapGroup/",
+ "ActivityDisplayName": "/MinimalApiUsingMapGroup",
+ "ActivityHttpRoute": "",
+ "MetricHttpRoute": "/MinimalApiUsingMapGroup/",
+ "RouteInfo": {
+ "HttpMethod": "GET",
+ "Path": "/MinimalApiUsingMapGroup",
+ "RoutePattern.RawText": "/MinimalApiUsingMapGroup/",
+ "IRouteDiagnosticsMetadata.Route": null,
+ "HttpContext.GetRouteData()": {},
+ "ActionDescriptor": null
+ }
+}
+```
+
+## MinimalApi: Action with parameter (MapGroup)
+
+```json
+{
+ "IdealHttpRoute": "/MinimalApiUsingMapGroup/{id}",
+ "ActivityDisplayName": "/MinimalApiUsingMapGroup/123",
+ "ActivityHttpRoute": "",
+ "MetricHttpRoute": "/MinimalApiUsingMapGroup/{id}",
+ "RouteInfo": {
+ "HttpMethod": "GET",
+ "Path": "/MinimalApiUsingMapGroup/123",
+ "RoutePattern.RawText": "/MinimalApiUsingMapGroup/{id}",
+ "IRouteDiagnosticsMetadata.Route": null,
+ "HttpContext.GetRouteData()": {
+ "id": "123"
+ },
+ "ActionDescriptor": null
+ }
+}
+```
diff --git a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/README.net8.0.md b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/README.net8.0.md
new file mode 100644
index 00000000000..4cacc1eac0c
--- /dev/null
+++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/README.net8.0.md
@@ -0,0 +1,634 @@
+# Test results for ASP.NET Core 8
+
+| Span http.route | Metric http.route | App | Test Name |
+| - | - | - | - |
+| :broken_heart: | :broken_heart: | ConventionalRouting | [Root path](#conventionalrouting-root-path) |
+| :broken_heart: | :broken_heart: | ConventionalRouting | [Non-default action with route parameter and query string](#conventionalrouting-non-default-action-with-route-parameter-and-query-string) |
+| :broken_heart: | :broken_heart: | ConventionalRouting | [Non-default action with query string](#conventionalrouting-non-default-action-with-query-string) |
+| :broken_heart: | :broken_heart: | ConventionalRouting | [Not Found (404)](#conventionalrouting-not-found-404) |
+| :broken_heart: | :green_heart: | ConventionalRouting | [Route template with parameter constraint](#conventionalrouting-route-template-with-parameter-constraint) |
+| :broken_heart: | :green_heart: | ConventionalRouting | [Path that does not match parameter constraint](#conventionalrouting-path-that-does-not-match-parameter-constraint) |
+| :broken_heart: | :broken_heart: | ConventionalRouting | [Area using area:exists, default controller/action](#conventionalrouting-area-using-areaexists-default-controlleraction) |
+| :broken_heart: | :broken_heart: | ConventionalRouting | [Area using area:exists, non-default action](#conventionalrouting-area-using-areaexists-non-default-action) |
+| :broken_heart: | :broken_heart: | ConventionalRouting | [Area w/o area:exists, default controller/action](#conventionalrouting-area-wo-areaexists-default-controlleraction) |
+| :green_heart: | :green_heart: | AttributeRouting | [Default action](#attributerouting-default-action) |
+| :green_heart: | :green_heart: | AttributeRouting | [Action without parameter](#attributerouting-action-without-parameter) |
+| :green_heart: | :green_heart: | AttributeRouting | [Action with parameter](#attributerouting-action-with-parameter) |
+| :green_heart: | :green_heart: | AttributeRouting | [Action with parameter before action name in template](#attributerouting-action-with-parameter-before-action-name-in-template) |
+| :green_heart: | :green_heart: | AttributeRouting | [Action invoked resulting in 400 Bad Request](#attributerouting-action-invoked-resulting-in-400-bad-request) |
+| :broken_heart: | :broken_heart: | RazorPages | [Root path](#razorpages-root-path) |
+| :broken_heart: | :broken_heart: | RazorPages | [Index page](#razorpages-index-page) |
+| :broken_heart: | :broken_heart: | RazorPages | [Throws exception](#razorpages-throws-exception) |
+| :green_heart: | :green_heart: | RazorPages | [Static content](#razorpages-static-content) |
+| :broken_heart: | :green_heart: | MinimalApi | [Action without parameter](#minimalapi-action-without-parameter) |
+| :broken_heart: | :green_heart: | MinimalApi | [Action with parameter](#minimalapi-action-with-parameter) |
+| :broken_heart: | :green_heart: | MinimalApi | [Action without parameter (MapGroup)](#minimalapi-action-without-parameter-mapgroup) |
+| :broken_heart: | :green_heart: | MinimalApi | [Action with parameter (MapGroup)](#minimalapi-action-with-parameter-mapgroup) |
+
+## ConventionalRouting: Root path
+
+```json
+{
+ "IdealHttpRoute": "ConventionalRoute/Default/{id?}",
+ "ActivityDisplayName": "/",
+ "ActivityHttpRoute": "",
+ "MetricHttpRoute": "{controller=ConventionalRoute}/{action=Default}/{id?}",
+ "RouteInfo": {
+ "HttpMethod": "GET",
+ "Path": "/",
+ "RoutePattern.RawText": "{controller=ConventionalRoute}/{action=Default}/{id?}",
+ "IRouteDiagnosticsMetadata.Route": "{controller=ConventionalRoute}/{action=Default}/{id?}",
+ "HttpContext.GetRouteData()": {
+ "controller": "ConventionalRoute",
+ "action": "Default"
+ },
+ "ActionDescriptor": {
+ "AttributeRouteInfo.Template": null,
+ "Parameters": [],
+ "ControllerActionDescriptor": {
+ "ControllerName": "ConventionalRoute",
+ "ActionName": "Default"
+ },
+ "PageActionDescriptor": null
+ }
+ }
+}
+```
+
+## ConventionalRouting: Non-default action with route parameter and query string
+
+```json
+{
+ "IdealHttpRoute": "ConventionalRoute/ActionWithStringParameter/{id?}",
+ "ActivityDisplayName": "/ConventionalRoute/ActionWithStringParameter/2",
+ "ActivityHttpRoute": "",
+ "MetricHttpRoute": "{controller=ConventionalRoute}/{action=Default}/{id?}",
+ "RouteInfo": {
+ "HttpMethod": "GET",
+ "Path": "/ConventionalRoute/ActionWithStringParameter/2?num=3",
+ "RoutePattern.RawText": "{controller=ConventionalRoute}/{action=Default}/{id?}",
+ "IRouteDiagnosticsMetadata.Route": "{controller=ConventionalRoute}/{action=Default}/{id?}",
+ "HttpContext.GetRouteData()": {
+ "controller": "ConventionalRoute",
+ "action": "ActionWithStringParameter",
+ "id": "2"
+ },
+ "ActionDescriptor": {
+ "AttributeRouteInfo.Template": null,
+ "Parameters": [
+ "id",
+ "num"
+ ],
+ "ControllerActionDescriptor": {
+ "ControllerName": "ConventionalRoute",
+ "ActionName": "ActionWithStringParameter"
+ },
+ "PageActionDescriptor": null
+ }
+ }
+}
+```
+
+## ConventionalRouting: Non-default action with query string
+
+```json
+{
+ "IdealHttpRoute": "ConventionalRoute/ActionWithStringParameter/{id?}",
+ "ActivityDisplayName": "/ConventionalRoute/ActionWithStringParameter",
+ "ActivityHttpRoute": "",
+ "MetricHttpRoute": "{controller=ConventionalRoute}/{action=Default}/{id?}",
+ "RouteInfo": {
+ "HttpMethod": "GET",
+ "Path": "/ConventionalRoute/ActionWithStringParameter?num=3",
+ "RoutePattern.RawText": "{controller=ConventionalRoute}/{action=Default}/{id?}",
+ "IRouteDiagnosticsMetadata.Route": "{controller=ConventionalRoute}/{action=Default}/{id?}",
+ "HttpContext.GetRouteData()": {
+ "controller": "ConventionalRoute",
+ "action": "ActionWithStringParameter"
+ },
+ "ActionDescriptor": {
+ "AttributeRouteInfo.Template": null,
+ "Parameters": [
+ "id",
+ "num"
+ ],
+ "ControllerActionDescriptor": {
+ "ControllerName": "ConventionalRoute",
+ "ActionName": "ActionWithStringParameter"
+ },
+ "PageActionDescriptor": null
+ }
+ }
+}
+```
+
+## ConventionalRouting: Not Found (404)
+
+```json
+{
+ "IdealHttpRoute": "",
+ "ActivityDisplayName": "/ConventionalRoute/NotFound",
+ "ActivityHttpRoute": "",
+ "MetricHttpRoute": "",
+ "RouteInfo": {
+ "HttpMethod": "GET",
+ "Path": "/ConventionalRoute/NotFound",
+ "RoutePattern.RawText": null,
+ "IRouteDiagnosticsMetadata.Route": null,
+ "HttpContext.GetRouteData()": {},
+ "ActionDescriptor": null
+ }
+}
+```
+
+## ConventionalRouting: Route template with parameter constraint
+
+```json
+{
+ "IdealHttpRoute": "SomePath/{id}/{num:int}",
+ "ActivityDisplayName": "/SomePath/SomeString/2",
+ "ActivityHttpRoute": "",
+ "MetricHttpRoute": "SomePath/{id}/{num:int}",
+ "RouteInfo": {
+ "HttpMethod": "GET",
+ "Path": "/SomePath/SomeString/2",
+ "RoutePattern.RawText": "SomePath/{id}/{num:int}",
+ "IRouteDiagnosticsMetadata.Route": "SomePath/{id}/{num:int}",
+ "HttpContext.GetRouteData()": {
+ "controller": "ConventionalRoute",
+ "action": "ActionWithStringParameter",
+ "id": "SomeString",
+ "num": "2"
+ },
+ "ActionDescriptor": {
+ "AttributeRouteInfo.Template": null,
+ "Parameters": [
+ "id",
+ "num"
+ ],
+ "ControllerActionDescriptor": {
+ "ControllerName": "ConventionalRoute",
+ "ActionName": "ActionWithStringParameter"
+ },
+ "PageActionDescriptor": null
+ }
+ }
+}
+```
+
+## ConventionalRouting: Path that does not match parameter constraint
+
+```json
+{
+ "IdealHttpRoute": "",
+ "ActivityDisplayName": "/SomePath/SomeString/NotAnInt",
+ "ActivityHttpRoute": "",
+ "MetricHttpRoute": "",
+ "RouteInfo": {
+ "HttpMethod": "GET",
+ "Path": "/SomePath/SomeString/NotAnInt",
+ "RoutePattern.RawText": null,
+ "IRouteDiagnosticsMetadata.Route": null,
+ "HttpContext.GetRouteData()": {},
+ "ActionDescriptor": null
+ }
+}
+```
+
+## ConventionalRouting: Area using area:exists, default controller/action
+
+```json
+{
+ "IdealHttpRoute": "{area:exists}/ControllerForMyArea/Default/{id?}",
+ "ActivityDisplayName": "/MyArea",
+ "ActivityHttpRoute": "",
+ "MetricHttpRoute": "{area:exists}/{controller=ControllerForMyArea}/{action=Default}/{id?}",
+ "RouteInfo": {
+ "HttpMethod": "GET",
+ "Path": "/MyArea",
+ "RoutePattern.RawText": "{area:exists}/{controller=ControllerForMyArea}/{action=Default}/{id?}",
+ "IRouteDiagnosticsMetadata.Route": "{area:exists}/{controller=ControllerForMyArea}/{action=Default}/{id?}",
+ "HttpContext.GetRouteData()": {
+ "controller": "ControllerForMyArea",
+ "action": "Default",
+ "area": "MyArea"
+ },
+ "ActionDescriptor": {
+ "AttributeRouteInfo.Template": null,
+ "Parameters": [],
+ "ControllerActionDescriptor": {
+ "ControllerName": "ControllerForMyArea",
+ "ActionName": "Default"
+ },
+ "PageActionDescriptor": null
+ }
+ }
+}
+```
+
+## ConventionalRouting: Area using area:exists, non-default action
+
+```json
+{
+ "IdealHttpRoute": "{area:exists}/ControllerForMyArea/NonDefault/{id?}",
+ "ActivityDisplayName": "/MyArea/ControllerForMyArea/NonDefault",
+ "ActivityHttpRoute": "",
+ "MetricHttpRoute": "{area:exists}/{controller=ControllerForMyArea}/{action=Default}/{id?}",
+ "RouteInfo": {
+ "HttpMethod": "GET",
+ "Path": "/MyArea/ControllerForMyArea/NonDefault",
+ "RoutePattern.RawText": "{area:exists}/{controller=ControllerForMyArea}/{action=Default}/{id?}",
+ "IRouteDiagnosticsMetadata.Route": "{area:exists}/{controller=ControllerForMyArea}/{action=Default}/{id?}",
+ "HttpContext.GetRouteData()": {
+ "controller": "ControllerForMyArea",
+ "area": "MyArea",
+ "action": "NonDefault"
+ },
+ "ActionDescriptor": {
+ "AttributeRouteInfo.Template": null,
+ "Parameters": [],
+ "ControllerActionDescriptor": {
+ "ControllerName": "ControllerForMyArea",
+ "ActionName": "NonDefault"
+ },
+ "PageActionDescriptor": null
+ }
+ }
+}
+```
+
+## ConventionalRouting: Area w/o area:exists, default controller/action
+
+```json
+{
+ "IdealHttpRoute": "SomePrefix/AnotherArea/Index/{id?}",
+ "ActivityDisplayName": "/SomePrefix",
+ "ActivityHttpRoute": "",
+ "MetricHttpRoute": "SomePrefix/{controller=AnotherArea}/{action=Index}/{id?}",
+ "RouteInfo": {
+ "HttpMethod": "GET",
+ "Path": "/SomePrefix",
+ "RoutePattern.RawText": "SomePrefix/{controller=AnotherArea}/{action=Index}/{id?}",
+ "IRouteDiagnosticsMetadata.Route": "SomePrefix/{controller=AnotherArea}/{action=Index}/{id?}",
+ "HttpContext.GetRouteData()": {
+ "area": "AnotherArea",
+ "controller": "AnotherArea",
+ "action": "Index"
+ },
+ "ActionDescriptor": {
+ "AttributeRouteInfo.Template": null,
+ "Parameters": [],
+ "ControllerActionDescriptor": {
+ "ControllerName": "AnotherArea",
+ "ActionName": "Index"
+ },
+ "PageActionDescriptor": null
+ }
+ }
+}
+```
+
+## AttributeRouting: Default action
+
+```json
+{
+ "IdealHttpRoute": "AttributeRoute",
+ "ActivityDisplayName": "AttributeRoute",
+ "ActivityHttpRoute": "AttributeRoute",
+ "MetricHttpRoute": "AttributeRoute",
+ "RouteInfo": {
+ "HttpMethod": "GET",
+ "Path": "/AttributeRoute",
+ "RoutePattern.RawText": "AttributeRoute",
+ "IRouteDiagnosticsMetadata.Route": "AttributeRoute",
+ "HttpContext.GetRouteData()": {
+ "action": "Get",
+ "controller": "AttributeRoute"
+ },
+ "ActionDescriptor": {
+ "AttributeRouteInfo.Template": "AttributeRoute",
+ "Parameters": [],
+ "ControllerActionDescriptor": {
+ "ControllerName": "AttributeRoute",
+ "ActionName": "Get"
+ },
+ "PageActionDescriptor": null
+ }
+ }
+}
+```
+
+## AttributeRouting: Action without parameter
+
+```json
+{
+ "IdealHttpRoute": "AttributeRoute/Get",
+ "ActivityDisplayName": "AttributeRoute/Get",
+ "ActivityHttpRoute": "AttributeRoute/Get",
+ "MetricHttpRoute": "AttributeRoute/Get",
+ "RouteInfo": {
+ "HttpMethod": "GET",
+ "Path": "/AttributeRoute/Get",
+ "RoutePattern.RawText": "AttributeRoute/Get",
+ "IRouteDiagnosticsMetadata.Route": "AttributeRoute/Get",
+ "HttpContext.GetRouteData()": {
+ "action": "Get",
+ "controller": "AttributeRoute"
+ },
+ "ActionDescriptor": {
+ "AttributeRouteInfo.Template": "AttributeRoute/Get",
+ "Parameters": [],
+ "ControllerActionDescriptor": {
+ "ControllerName": "AttributeRoute",
+ "ActionName": "Get"
+ },
+ "PageActionDescriptor": null
+ }
+ }
+}
+```
+
+## AttributeRouting: Action with parameter
+
+```json
+{
+ "IdealHttpRoute": "AttributeRoute/Get/{id}",
+ "ActivityDisplayName": "AttributeRoute/Get/{id}",
+ "ActivityHttpRoute": "AttributeRoute/Get/{id}",
+ "MetricHttpRoute": "AttributeRoute/Get/{id}",
+ "RouteInfo": {
+ "HttpMethod": "GET",
+ "Path": "/AttributeRoute/Get/12",
+ "RoutePattern.RawText": "AttributeRoute/Get/{id}",
+ "IRouteDiagnosticsMetadata.Route": "AttributeRoute/Get/{id}",
+ "HttpContext.GetRouteData()": {
+ "action": "Get",
+ "controller": "AttributeRoute",
+ "id": "12"
+ },
+ "ActionDescriptor": {
+ "AttributeRouteInfo.Template": "AttributeRoute/Get/{id}",
+ "Parameters": [
+ "id"
+ ],
+ "ControllerActionDescriptor": {
+ "ControllerName": "AttributeRoute",
+ "ActionName": "Get"
+ },
+ "PageActionDescriptor": null
+ }
+ }
+}
+```
+
+## AttributeRouting: Action with parameter before action name in template
+
+```json
+{
+ "IdealHttpRoute": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate",
+ "ActivityDisplayName": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate",
+ "ActivityHttpRoute": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate",
+ "MetricHttpRoute": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate",
+ "RouteInfo": {
+ "HttpMethod": "GET",
+ "Path": "/AttributeRoute/12/GetWithActionNameInDifferentSpotInTemplate",
+ "RoutePattern.RawText": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate",
+ "IRouteDiagnosticsMetadata.Route": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate",
+ "HttpContext.GetRouteData()": {
+ "action": "GetWithActionNameInDifferentSpotInTemplate",
+ "controller": "AttributeRoute",
+ "id": "12"
+ },
+ "ActionDescriptor": {
+ "AttributeRouteInfo.Template": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate",
+ "Parameters": [
+ "id"
+ ],
+ "ControllerActionDescriptor": {
+ "ControllerName": "AttributeRoute",
+ "ActionName": "GetWithActionNameInDifferentSpotInTemplate"
+ },
+ "PageActionDescriptor": null
+ }
+ }
+}
+```
+
+## AttributeRouting: Action invoked resulting in 400 Bad Request
+
+```json
+{
+ "IdealHttpRoute": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate",
+ "ActivityDisplayName": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate",
+ "ActivityHttpRoute": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate",
+ "MetricHttpRoute": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate",
+ "RouteInfo": {
+ "HttpMethod": "GET",
+ "Path": "/AttributeRoute/NotAnInt/GetWithActionNameInDifferentSpotInTemplate",
+ "RoutePattern.RawText": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate",
+ "IRouteDiagnosticsMetadata.Route": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate",
+ "HttpContext.GetRouteData()": {
+ "action": "GetWithActionNameInDifferentSpotInTemplate",
+ "controller": "AttributeRoute",
+ "id": "NotAnInt"
+ },
+ "ActionDescriptor": {
+ "AttributeRouteInfo.Template": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate",
+ "Parameters": [
+ "id"
+ ],
+ "ControllerActionDescriptor": {
+ "ControllerName": "AttributeRoute",
+ "ActionName": "GetWithActionNameInDifferentSpotInTemplate"
+ },
+ "PageActionDescriptor": null
+ }
+ }
+}
+```
+
+## RazorPages: Root path
+
+```json
+{
+ "IdealHttpRoute": "/Index",
+ "ActivityDisplayName": "/",
+ "ActivityHttpRoute": "",
+ "MetricHttpRoute": "",
+ "RouteInfo": {
+ "HttpMethod": "GET",
+ "Path": "/",
+ "RoutePattern.RawText": "",
+ "IRouteDiagnosticsMetadata.Route": "",
+ "HttpContext.GetRouteData()": {
+ "page": "/Index"
+ },
+ "ActionDescriptor": {
+ "AttributeRouteInfo.Template": "",
+ "Parameters": [],
+ "ControllerActionDescriptor": null,
+ "PageActionDescriptor": {
+ "RelativePath": "/Pages/Index.cshtml",
+ "ViewEnginePath": "/Index"
+ }
+ }
+ }
+}
+```
+
+## RazorPages: Index page
+
+```json
+{
+ "IdealHttpRoute": "/Index",
+ "ActivityDisplayName": "Index",
+ "ActivityHttpRoute": "Index",
+ "MetricHttpRoute": "Index",
+ "RouteInfo": {
+ "HttpMethod": "GET",
+ "Path": "/Index",
+ "RoutePattern.RawText": "Index",
+ "IRouteDiagnosticsMetadata.Route": "Index",
+ "HttpContext.GetRouteData()": {
+ "page": "/Index"
+ },
+ "ActionDescriptor": {
+ "AttributeRouteInfo.Template": "Index",
+ "Parameters": [],
+ "ControllerActionDescriptor": null,
+ "PageActionDescriptor": {
+ "RelativePath": "/Pages/Index.cshtml",
+ "ViewEnginePath": "/Index"
+ }
+ }
+ }
+}
+```
+
+## RazorPages: Throws exception
+
+```json
+{
+ "IdealHttpRoute": "/PageThatThrowsException",
+ "ActivityDisplayName": "PageThatThrowsException",
+ "ActivityHttpRoute": "PageThatThrowsException",
+ "MetricHttpRoute": "PageThatThrowsException",
+ "RouteInfo": {
+ "HttpMethod": "GET",
+ "Path": "/PageThatThrowsException",
+ "RoutePattern.RawText": "PageThatThrowsException",
+ "IRouteDiagnosticsMetadata.Route": "PageThatThrowsException",
+ "HttpContext.GetRouteData()": {
+ "page": "/PageThatThrowsException"
+ },
+ "ActionDescriptor": {
+ "AttributeRouteInfo.Template": "PageThatThrowsException",
+ "Parameters": [],
+ "ControllerActionDescriptor": null,
+ "PageActionDescriptor": {
+ "RelativePath": "/Pages/PageThatThrowsException.cshtml",
+ "ViewEnginePath": "/PageThatThrowsException"
+ }
+ }
+ }
+}
+```
+
+## RazorPages: Static content
+
+```json
+{
+ "IdealHttpRoute": "",
+ "ActivityDisplayName": "/js/site.js",
+ "ActivityHttpRoute": "",
+ "MetricHttpRoute": "",
+ "RouteInfo": {
+ "HttpMethod": "GET",
+ "Path": "/js/site.js",
+ "RoutePattern.RawText": null,
+ "IRouteDiagnosticsMetadata.Route": null,
+ "HttpContext.GetRouteData()": {},
+ "ActionDescriptor": null
+ }
+}
+```
+
+## MinimalApi: Action without parameter
+
+```json
+{
+ "IdealHttpRoute": "/MinimalApi",
+ "ActivityDisplayName": "/MinimalApi",
+ "ActivityHttpRoute": "",
+ "MetricHttpRoute": "/MinimalApi",
+ "RouteInfo": {
+ "HttpMethod": "GET",
+ "Path": "/MinimalApi",
+ "RoutePattern.RawText": "/MinimalApi",
+ "IRouteDiagnosticsMetadata.Route": "/MinimalApi",
+ "HttpContext.GetRouteData()": {},
+ "ActionDescriptor": null
+ }
+}
+```
+
+## MinimalApi: Action with parameter
+
+```json
+{
+ "IdealHttpRoute": "/MinimalApi/{id}",
+ "ActivityDisplayName": "/MinimalApi/123",
+ "ActivityHttpRoute": "",
+ "MetricHttpRoute": "/MinimalApi/{id}",
+ "RouteInfo": {
+ "HttpMethod": "GET",
+ "Path": "/MinimalApi/123",
+ "RoutePattern.RawText": "/MinimalApi/{id}",
+ "IRouteDiagnosticsMetadata.Route": "/MinimalApi/{id}",
+ "HttpContext.GetRouteData()": {
+ "id": "123"
+ },
+ "ActionDescriptor": null
+ }
+}
+```
+
+## MinimalApi: Action without parameter (MapGroup)
+
+```json
+{
+ "IdealHttpRoute": "/MinimalApiUsingMapGroup/",
+ "ActivityDisplayName": "/MinimalApiUsingMapGroup",
+ "ActivityHttpRoute": "",
+ "MetricHttpRoute": "/MinimalApiUsingMapGroup/",
+ "RouteInfo": {
+ "HttpMethod": "GET",
+ "Path": "/MinimalApiUsingMapGroup",
+ "RoutePattern.RawText": "/MinimalApiUsingMapGroup/",
+ "IRouteDiagnosticsMetadata.Route": "/MinimalApiUsingMapGroup/",
+ "HttpContext.GetRouteData()": {},
+ "ActionDescriptor": null
+ }
+}
+```
+
+## MinimalApi: Action with parameter (MapGroup)
+
+```json
+{
+ "IdealHttpRoute": "/MinimalApiUsingMapGroup/{id}",
+ "ActivityDisplayName": "/MinimalApiUsingMapGroup/123",
+ "ActivityHttpRoute": "",
+ "MetricHttpRoute": "/MinimalApiUsingMapGroup/{id}",
+ "RouteInfo": {
+ "HttpMethod": "GET",
+ "Path": "/MinimalApiUsingMapGroup/123",
+ "RoutePattern.RawText": "/MinimalApiUsingMapGroup/{id}",
+ "IRouteDiagnosticsMetadata.Route": "/MinimalApiUsingMapGroup/{id}",
+ "HttpContext.GetRouteData()": {
+ "id": "123"
+ },
+ "ActionDescriptor": null
+ }
+}
+```
diff --git a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/RoutingTestCases.cs b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/RoutingTestCases.cs
new file mode 100644
index 00000000000..d2b7bb730df
--- /dev/null
+++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/RoutingTestCases.cs
@@ -0,0 +1,87 @@
+//
+// 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.
+//
+
+#nullable enable
+
+using System.Reflection;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using RouteTests.TestApplication;
+
+namespace RouteTests;
+
+public static class RoutingTestCases
+{
+ public static IEnumerable