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 GetTestCases() + { + var assembly = Assembly.GetExecutingAssembly(); + var input = JsonSerializer.Deserialize( + assembly.GetManifestResourceStream("RoutingTestCases.json")!, + new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + Converters = { new JsonStringEnumConverter() }, + }); + return GetArgumentsFromTestCaseObject(input!); + } + + private static IEnumerable GetArgumentsFromTestCaseObject(IEnumerable input) + { + var result = new List(); + + foreach (var testCase in input) + { + if (testCase.MinimumDotnetVersion.HasValue && Environment.Version.Major < testCase.MinimumDotnetVersion.Value) + { + continue; + } + + result.Add(new object[] { testCase, true }); + result.Add(new object[] { testCase, false }); + } + + return result; + } + + public class TestCase + { + public string Name { get; set; } = string.Empty; + + public int? MinimumDotnetVersion { get; set; } + + public TestApplicationScenario TestApplicationScenario { get; set; } + + public string? HttpMethod { get; set; } + + public string Path { get; set; } = string.Empty; + + public int ExpectedStatusCode { get; set; } + + public string? ExpectedHttpRoute { get; set; } + + public string? CurrentActivityDisplayName { get; set; } + + public string? CurrentActivityHttpRoute { get; set; } + + public string? CurrentMetricHttpRoute { get; set; } + + public override string ToString() + { + // This is used by Visual Studio's test runner to identify the test case. + return $"{this.TestApplicationScenario}: {this.Name}"; + } + } +} diff --git a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/RoutingTestCases.json b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/RoutingTestCases.json new file mode 100644 index 00000000000..c5c8febb77b --- /dev/null +++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/RoutingTestCases.json @@ -0,0 +1,246 @@ +[ + { + "name": "Root path", + "testApplicationScenario": "ConventionalRouting", + "httpMethod": "GET", + "path": "/", + "expectedStatusCode": 200, + "currentActivityDisplayName": "/", + "currentActivityHttpRoute": "", + "currentMetricHttpRoute": "{controller=ConventionalRoute}/{action=Default}/{id?}", + "expectedHttpRoute": "ConventionalRoute/Default/{id?}" + }, + { + "name": "Non-default action with route parameter and query string", + "testApplicationScenario": "ConventionalRouting", + "httpMethod": "GET", + "path": "/ConventionalRoute/ActionWithStringParameter/2?num=3", + "expectedStatusCode": 200, + "currentActivityDisplayName": "/ConventionalRoute/ActionWithStringParameter/2", + "currentActivityHttpRoute": "", + "currentMetricHttpRoute": "{controller=ConventionalRoute}/{action=Default}/{id?}", + "expectedHttpRoute": "ConventionalRoute/ActionWithStringParameter/{id?}" + }, + { + "name": "Non-default action with query string", + "testApplicationScenario": "ConventionalRouting", + "httpMethod": "GET", + "path": "/ConventionalRoute/ActionWithStringParameter?num=3", + "expectedStatusCode": 200, + "currentActivityDisplayName": "/ConventionalRoute/ActionWithStringParameter", + "currentActivityHttpRoute": "", + "currentMetricHttpRoute": "{controller=ConventionalRoute}/{action=Default}/{id?}", + "expectedHttpRoute": "ConventionalRoute/ActionWithStringParameter/{id?}" + }, + { + "name": "Not Found (404)", + "testApplicationScenario": "ConventionalRouting", + "httpMethod": "GET", + "path": "/ConventionalRoute/NotFound", + "expectedStatusCode": 404, + "currentActivityDisplayName": "/ConventionalRoute/NotFound", + "currentActivityHttpRoute": "", + "currentMetricHttpRoute": "", + "expectedHttpRoute": "" + }, + { + "name": "Route template with parameter constraint", + "testApplicationScenario": "ConventionalRouting", + "httpMethod": "GET", + "path": "/SomePath/SomeString/2", + "expectedStatusCode": 200, + "currentActivityDisplayName": "/SomePath/SomeString/2", + "currentActivityHttpRoute": "", + "currentMetricHttpRoute": null, + "expectedHttpRoute": "SomePath/{id}/{num:int}" + }, + { + "name": "Path that does not match parameter constraint", + "testApplicationScenario": "ConventionalRouting", + "httpMethod": "GET", + "path": "/SomePath/SomeString/NotAnInt", + "expectedStatusCode": 404, + "currentActivityDisplayName": "/SomePath/SomeString/NotAnInt", + "currentActivityHttpRoute": "", + "currentMetricHttpRoute": null, + "expectedHttpRoute": "" + }, + { + "name": "Area using area:exists, default controller/action", + "testApplicationScenario": "ConventionalRouting", + "httpMethod": "GET", + "path": "/MyArea", + "expectedStatusCode": 200, + "currentActivityDisplayName": "/MyArea", + "currentActivityHttpRoute": "", + "currentMetricHttpRoute": "{area:exists}/{controller=ControllerForMyArea}/{action=Default}/{id?}", + "expectedHttpRoute": "{area:exists}/ControllerForMyArea/Default/{id?}" + }, + { + "name": "Area using area:exists, non-default action", + "testApplicationScenario": "ConventionalRouting", + "httpMethod": "GET", + "path": "/MyArea/ControllerForMyArea/NonDefault", + "expectedStatusCode": 200, + "currentActivityDisplayName": "/MyArea/ControllerForMyArea/NonDefault", + "currentActivityHttpRoute": "", + "currentMetricHttpRoute": "{area:exists}/{controller=ControllerForMyArea}/{action=Default}/{id?}", + "expectedHttpRoute": "{area:exists}/ControllerForMyArea/NonDefault/{id?}" + }, + { + "name": "Area w/o area:exists, default controller/action", + "testApplicationScenario": "ConventionalRouting", + "httpMethod": "GET", + "path": "/SomePrefix", + "expectedStatusCode": 200, + "currentActivityDisplayName": "/SomePrefix", + "currentActivityHttpRoute": "", + "currentMetricHttpRoute": "SomePrefix/{controller=AnotherArea}/{action=Index}/{id?}", + "expectedHttpRoute": "SomePrefix/AnotherArea/Index/{id?}" + }, + { + "name": "Default action", + "testApplicationScenario": "AttributeRouting", + "httpMethod": "GET", + "path": "/AttributeRoute", + "expectedStatusCode": 200, + "currentActivityDisplayName": "AttributeRoute", + "currentActivityHttpRoute": null, + "currentMetricHttpRoute": null, + "expectedHttpRoute": "AttributeRoute" + }, + { + "name": "Action without parameter", + "testApplicationScenario": "AttributeRouting", + "httpMethod": "GET", + "path": "/AttributeRoute/Get", + "expectedStatusCode": 200, + "currentActivityDisplayName": "AttributeRoute/Get", + "currentActivityHttpRoute": null, + "currentMetricHttpRoute": null, + "expectedHttpRoute": "AttributeRoute/Get" + }, + { + "name": "Action with parameter", + "testApplicationScenario": "AttributeRouting", + "httpMethod": "GET", + "path": "/AttributeRoute/Get/12", + "expectedStatusCode": 200, + "currentActivityDisplayName": "AttributeRoute/Get/{id}", + "currentActivityHttpRoute": null, + "currentMetricHttpRoute": null, + "expectedHttpRoute": "AttributeRoute/Get/{id}" + }, + { + "name": "Action with parameter before action name in template", + "testApplicationScenario": "AttributeRouting", + "httpMethod": "GET", + "path": "/AttributeRoute/12/GetWithActionNameInDifferentSpotInTemplate", + "expectedStatusCode": 200, + "currentActivityDisplayName": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate", + "currentActivityHttpRoute": null, + "currentMetricHttpRoute": null, + "expectedHttpRoute": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate" + }, + { + "name": "Action invoked resulting in 400 Bad Request", + "testApplicationScenario": "AttributeRouting", + "httpMethod": "GET", + "path": "/AttributeRoute/NotAnInt/GetWithActionNameInDifferentSpotInTemplate", + "expectedStatusCode": 400, + "currentActivityDisplayName": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate", + "currentActivityHttpRoute": null, + "currentMetricHttpRoute": null, + "expectedHttpRoute": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate" + }, + { + "name": "Root path", + "testApplicationScenario": "RazorPages", + "httpMethod": "GET", + "path": "/", + "expectedStatusCode": 200, + "currentActivityDisplayName": "/", + "currentActivityHttpRoute": "", + "currentMetricHttpRoute": "", + "expectedHttpRoute": "/Index" + }, + { + "name": "Index page", + "testApplicationScenario": "RazorPages", + "httpMethod": "GET", + "path": "/Index", + "expectedStatusCode": 200, + "currentActivityDisplayName": "Index", + "currentActivityHttpRoute": "Index", + "currentMetricHttpRoute": "Index", + "expectedHttpRoute": "/Index" + }, + { + "name": "Throws exception", + "testApplicationScenario": "RazorPages", + "httpMethod": "GET", + "path": "/PageThatThrowsException", + "expectedStatusCode": 500, + "currentActivityDisplayName": "PageThatThrowsException", + "currentActivityHttpRoute": "PageThatThrowsException", + "currentMetricHttpRoute": "PageThatThrowsException", + "expectedHttpRoute": "/PageThatThrowsException" + }, + { + "name": "Static content", + "testApplicationScenario": "RazorPages", + "httpMethod": "GET", + "path": "/js/site.js", + "expectedStatusCode": 200, + "currentActivityDisplayName": "/js/site.js", + "currentActivityHttpRoute": null, + "currentMetricHttpRoute": null, + "expectedHttpRoute": "" + }, + { + "name": "Action without parameter", + "testApplicationScenario": "MinimalApi", + "httpMethod": "GET", + "path": "/MinimalApi", + "expectedStatusCode": 200, + "currentActivityDisplayName": "/MinimalApi", + "currentActivityHttpRoute": "", + "currentMetricHttpRoute": null, + "expectedHttpRoute": "/MinimalApi" + }, + { + "name": "Action with parameter", + "testApplicationScenario": "MinimalApi", + "httpMethod": "GET", + "path": "/MinimalApi/123", + "expectedStatusCode": 200, + "currentActivityDisplayName": "/MinimalApi/123", + "currentActivityHttpRoute": "", + "currentMetricHttpRoute": null, + "expectedHttpRoute": "/MinimalApi/{id}" + }, + { + "name": "Action without parameter (MapGroup)", + "minimumDotnetVersion": 7, + "testApplicationScenario": "MinimalApi", + "httpMethod": "GET", + "path": "/MinimalApiUsingMapGroup", + "expectedStatusCode": 200, + "currentActivityDisplayName": "/MinimalApiUsingMapGroup", + "currentActivityHttpRoute": "", + "currentMetricHttpRoute": null, + "expectedHttpRoute": "/MinimalApiUsingMapGroup/" + }, + { + "name": "Action with parameter (MapGroup)", + "minimumDotnetVersion": 7, + "testApplicationScenario": "MinimalApi", + "httpMethod": "GET", + "path": "/MinimalApiUsingMapGroup/123", + "expectedStatusCode": 200, + "currentActivityDisplayName": "/MinimalApiUsingMapGroup/123", + "currentActivityHttpRoute": "", + "currentMetricHttpRoute": null, + "expectedHttpRoute": "/MinimalApiUsingMapGroup/{id}" + } +] diff --git a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/RoutingTestFixture.cs b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/RoutingTestFixture.cs new file mode 100644 index 00000000000..1addda01cb6 --- /dev/null +++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/RoutingTestFixture.cs @@ -0,0 +1,121 @@ +// +// 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.Text; +using Microsoft.AspNetCore.Builder; +using RouteTests.TestApplication; + +namespace RouteTests; + +public class RoutingTestFixture : IDisposable +{ + private static readonly HttpClient HttpClient = new(); + private readonly Dictionary apps = new(); + private readonly RouteInfoDiagnosticObserver diagnostics = new(); + private readonly List testResults = new(); + + public RoutingTestFixture() + { + foreach (var scenario in Enum.GetValues()) + { + var app = TestApplicationFactory.CreateApplication(scenario); + if (app != null) + { + this.apps.Add(scenario, app); + } + } + + foreach (var app in this.apps) + { + app.Value.RunAsync(); + } + } + + public async Task MakeRequest(TestApplicationScenario scenario, string path) + { + var app = this.apps[scenario]; + var baseUrl = app.Urls.First(); + var url = $"{baseUrl}{path}"; + await HttpClient.GetAsync(url).ConfigureAwait(false); + } + + public void AddTestResult(RoutingTestResult result) + { + this.testResults.Add(result); + } + + public void Dispose() + { + foreach (var app in this.apps) + { + app.Value.DisposeAsync().GetAwaiter().GetResult(); + } + + HttpClient.Dispose(); + this.diagnostics.Dispose(); + + this.GenerateReadme(); + } + + private void GenerateReadme() + { + var sb = new StringBuilder(); + sb.AppendLine($"# Test results for ASP.NET Core {Environment.Version.Major}"); + sb.AppendLine(); + sb.AppendLine("| Span http.route | Metric http.route | App | Test Name |"); + sb.AppendLine("| - | - | - | - |"); + + for (var i = 0; i < this.testResults.Count; ++i) + { + var result = this.testResults[i]; + var emoji1 = result.TestCase.CurrentActivityHttpRoute == null ? ":green_heart:" : ":broken_heart:"; + var emoji2 = result.TestCase.CurrentMetricHttpRoute == null ? ":green_heart:" : ":broken_heart:"; + sb.Append($"| {emoji1} | {emoji2} "); + sb.AppendLine($"| {result.TestCase.TestApplicationScenario} | [{result.TestCase.Name}]({MakeAnchorTag(result.TestCase.TestApplicationScenario, result.TestCase.Name)}) |"); + } + + for (var i = 0; i < this.testResults.Count; ++i) + { + var result = this.testResults[i]; + sb.AppendLine(); + sb.AppendLine($"## {result.TestCase.TestApplicationScenario}: {result.TestCase.Name}"); + sb.AppendLine(); + sb.AppendLine("```json"); + sb.AppendLine(result.ToString()); + sb.AppendLine("```"); + } + + var readmeFileName = $"README.net{Environment.Version.Major}.0.md"; + File.WriteAllText(Path.Combine("..", "..", "..", "RouteTests", readmeFileName), sb.ToString()); + + static string MakeAnchorTag(TestApplicationScenario scenario, string name) + { + var chars = name.ToCharArray() + .Where(c => !char.IsPunctuation(c) || c == '-') + .Select(c => c switch + { + '-' => '-', + ' ' => '-', + _ => char.ToLower(c), + }) + .ToArray(); + + return $"#{scenario.ToString().ToLower()}-{new string(chars)}"; + } + } +} diff --git a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/RoutingTestResult.cs b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/RoutingTestResult.cs new file mode 100644 index 00000000000..584812f4c97 --- /dev/null +++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/RoutingTestResult.cs @@ -0,0 +1,46 @@ +// +// 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.Text.Json; +using System.Text.Json.Serialization; +using RouteTests.TestApplication; + +namespace RouteTests; + +public class RoutingTestResult +{ + private static readonly JsonSerializerOptions JsonSerializerOptions = new() { WriteIndented = true }; + + public string? IdealHttpRoute { get; set; } + + public string ActivityDisplayName { get; set; } = string.Empty; + + public string? ActivityHttpRoute { get; set; } + + public string? MetricHttpRoute { get; set; } + + public RouteInfo RouteInfo { get; set; } = new RouteInfo(); + + [JsonIgnore] + public RoutingTestCases.TestCase TestCase { get; set; } = new RoutingTestCases.TestCase(); + + public override string ToString() + { + return JsonSerializer.Serialize(this, JsonSerializerOptions); + } +} diff --git a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/RoutingTests.cs b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/RoutingTests.cs new file mode 100644 index 00000000000..d8983db9d4d --- /dev/null +++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/RoutingTests.cs @@ -0,0 +1,191 @@ +// +// 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.Diagnostics; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using OpenTelemetry; +using OpenTelemetry.Metrics; +using OpenTelemetry.Trace; +using RouteTests.TestApplication; +using Xunit; +using static OpenTelemetry.Internal.HttpSemanticConventionHelper; + +namespace RouteTests; + +public class RoutingTests : IClassFixture +{ + private const string OldHttpStatusCode = "http.status_code"; + private const string OldHttpMethod = "http.method"; + private const string HttpStatusCode = "http.response.status_code"; + private const string HttpMethod = "http.request.method"; + private const string HttpRoute = "http.route"; + + private readonly RoutingTestFixture fixture; + private readonly List exportedActivities = new(); + private readonly List exportedMetrics = new(); + + public RoutingTests(RoutingTestFixture fixture) + { + this.fixture = fixture; + } + + public static IEnumerable TestData => RoutingTestCases.GetTestCases(); + + [Theory] + [MemberData(nameof(TestData))] + public async Task TestHttpRoute(RoutingTestCases.TestCase testCase, bool useLegacyConventions) + { + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary { [SemanticConventionOptInKeyName] = useLegacyConventions ? null : "http" }) + .Build(); + + using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .ConfigureServices(services => services.AddSingleton(configuration)) + .AddAspNetCoreInstrumentation() + .AddInMemoryExporter(this.exportedActivities) + .Build()!; + + using var meterProvider = Sdk.CreateMeterProviderBuilder() + .ConfigureServices(services => services.AddSingleton(configuration)) + .AddAspNetCoreInstrumentation() + .AddInMemoryExporter(this.exportedMetrics) + .Build()!; + + await this.fixture.MakeRequest(testCase.TestApplicationScenario, testCase.Path); + + for (var i = 0; i < 10; i++) + { + if (this.exportedActivities.Count > 0) + { + break; + } + + await Task.Delay(TimeSpan.FromSeconds(1)).ConfigureAwait(false); + } + + meterProvider.ForceFlush(); + + var durationMetric = this.exportedMetrics.Single(x => x.Name == "http.server.request.duration" || x.Name == "http.server.duration"); + var metricPoints = new List(); + foreach (var mp in durationMetric.GetMetricPoints()) + { + metricPoints.Add(mp); + } + + var activity = Assert.Single(this.exportedActivities); + var metricPoint = Assert.Single(metricPoints); + + GetTagsFromActivity(useLegacyConventions, activity, out var activityHttpStatusCode, out var activityHttpMethod, out var activityHttpRoute); + GetTagsFromMetricPoint(useLegacyConventions && Environment.Version.Major < 8, metricPoint, out var metricHttpStatusCode, out var metricHttpMethod, out var metricHttpRoute); + + Assert.Equal(testCase.ExpectedStatusCode, activityHttpStatusCode); + Assert.Equal(testCase.ExpectedStatusCode, metricHttpStatusCode); + Assert.Equal(testCase.HttpMethod, activityHttpMethod); + Assert.Equal(testCase.HttpMethod, metricHttpMethod); + + // TODO: The CurrentActivityDisplayName, CurrentActivityHttpRoute, and CurrentMetricHttpRoute + // properties will go away. They only serve to capture status quo. The "else" blocks are the real + // asserts that we ultimately want. + // If any of the current properties are null, then that means we already conform to the + // correct behavior. + if (testCase.CurrentActivityDisplayName != null) + { + Assert.Equal(testCase.CurrentActivityDisplayName, activity.DisplayName); + } + else + { + // Activity.DisplayName should be a combination of http.method + http.route attributes, see: + // https://github.com/open-telemetry/semantic-conventions/blob/main/docs/http/http-spans.md#name + var expectedActivityDisplayName = string.IsNullOrEmpty(testCase.ExpectedHttpRoute) + ? testCase.HttpMethod + : $"{testCase.HttpMethod} {testCase.ExpectedHttpRoute}"; + + Assert.Equal(expectedActivityDisplayName, activity.DisplayName); + } + + if (testCase.CurrentActivityHttpRoute != null) + { + Assert.Equal(testCase.CurrentActivityHttpRoute, activityHttpRoute); + } + else + { + Assert.Equal(testCase.ExpectedHttpRoute, activityHttpRoute); + } + + if (testCase.CurrentMetricHttpRoute != null) + { + Assert.Equal(testCase.CurrentMetricHttpRoute, metricHttpRoute); + } + else + { + Assert.Equal(testCase.ExpectedHttpRoute, metricHttpRoute); + } + + // Only produce README files based on final semantic conventions + if (!useLegacyConventions) + { + var testResult = new RoutingTestResult + { + IdealHttpRoute = testCase.ExpectedHttpRoute, + ActivityDisplayName = activity.DisplayName, + ActivityHttpRoute = activityHttpRoute, + MetricHttpRoute = metricHttpRoute, + TestCase = testCase, + RouteInfo = RouteInfo.Current, + }; + + this.fixture.AddTestResult(testResult); + } + } + + private static void GetTagsFromActivity(bool useLegacyConventions, Activity activity, out int httpStatusCode, out string httpMethod, out string? httpRoute) + { + var expectedStatusCodeKey = useLegacyConventions ? OldHttpStatusCode : HttpStatusCode; + var expectedHttpMethodKey = useLegacyConventions ? OldHttpMethod : HttpMethod; + httpStatusCode = Convert.ToInt32(activity.GetTagItem(expectedStatusCodeKey)); + httpMethod = (activity.GetTagItem(expectedHttpMethodKey) as string)!; + httpRoute = activity.GetTagItem(HttpRoute) as string ?? string.Empty; + } + + private static void GetTagsFromMetricPoint(bool useLegacyConventions, MetricPoint metricPoint, out int httpStatusCode, out string httpMethod, out string? httpRoute) + { + var expectedStatusCodeKey = useLegacyConventions ? OldHttpStatusCode : HttpStatusCode; + var expectedHttpMethodKey = useLegacyConventions ? OldHttpMethod : HttpMethod; + + httpStatusCode = 0; + httpMethod = string.Empty; + httpRoute = string.Empty; + + foreach (var tag in metricPoint.Tags) + { + if (tag.Key.Equals(expectedStatusCodeKey)) + { + httpStatusCode = Convert.ToInt32(tag.Value); + } + else if (tag.Key.Equals(expectedHttpMethodKey)) + { + httpMethod = (tag.Value as string)!; + } + else if (tag.Key.Equals(HttpRoute)) + { + httpRoute = tag.Value as string; + } + } + } +} diff --git a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/TestApplication/Areas/AnotherArea/Controllers/AnotherAreaController.cs b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/TestApplication/Areas/AnotherArea/Controllers/AnotherAreaController.cs new file mode 100644 index 00000000000..e9a2565304c --- /dev/null +++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/TestApplication/Areas/AnotherArea/Controllers/AnotherAreaController.cs @@ -0,0 +1,27 @@ +// +// 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 disable + +using Microsoft.AspNetCore.Mvc; + +namespace RouteTests.Controllers; + +[Area("AnotherArea")] +public class AnotherAreaController : Controller +{ + public IActionResult Index() => this.Ok(); +} diff --git a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/TestApplication/Areas/MyArea/Controllers/ControllerForMyAreaController.cs b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/TestApplication/Areas/MyArea/Controllers/ControllerForMyAreaController.cs new file mode 100644 index 00000000000..99933dd34cf --- /dev/null +++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/TestApplication/Areas/MyArea/Controllers/ControllerForMyAreaController.cs @@ -0,0 +1,29 @@ +// +// 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 disable + +using Microsoft.AspNetCore.Mvc; + +namespace RouteTests.Controllers; + +[Area("MyArea")] +public class ControllerForMyAreaController : Controller +{ + public IActionResult Default() => this.Ok(); + + public IActionResult NonDefault() => this.Ok(); +} diff --git a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/TestApplication/Controllers/AttributeRouteController.cs b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/TestApplication/Controllers/AttributeRouteController.cs new file mode 100644 index 00000000000..28271f9553e --- /dev/null +++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/TestApplication/Controllers/AttributeRouteController.cs @@ -0,0 +1,36 @@ +// +// 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 disable + +using Microsoft.AspNetCore.Mvc; + +namespace RouteTests.Controllers; + +[ApiController] +[Route("[controller]")] +public class AttributeRouteController : ControllerBase +{ + [HttpGet] + [HttpGet("[action]")] + public IActionResult Get() => this.Ok(); + + [HttpGet("[action]/{id}")] + public IActionResult Get(int id) => this.Ok(); + + [HttpGet("{id}/[action]")] + public IActionResult GetWithActionNameInDifferentSpotInTemplate(int id) => this.Ok(); +} diff --git a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/TestApplication/Controllers/ConventionalRouteController.cs b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/TestApplication/Controllers/ConventionalRouteController.cs new file mode 100644 index 00000000000..32fc1a6223b --- /dev/null +++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/TestApplication/Controllers/ConventionalRouteController.cs @@ -0,0 +1,30 @@ +// +// 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 disable + +using Microsoft.AspNetCore.Mvc; + +namespace RouteTests.Controllers; + +public class ConventionalRouteController : Controller +{ + public IActionResult Default() => this.Ok(); + + public IActionResult ActionWithParameter(int id) => this.Ok(); + + public IActionResult ActionWithStringParameter(string id, int num) => this.Ok(); +} diff --git a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/TestApplication/Pages/Index.cshtml b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/TestApplication/Pages/Index.cshtml new file mode 100644 index 00000000000..51c350f9565 --- /dev/null +++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/TestApplication/Pages/Index.cshtml @@ -0,0 +1,2 @@ +@page +Hello, OpenTelemetry! diff --git a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/TestApplication/Pages/PageThatThrowsException.cshtml b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/TestApplication/Pages/PageThatThrowsException.cshtml new file mode 100644 index 00000000000..cf6ac0d5b81 --- /dev/null +++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/TestApplication/Pages/PageThatThrowsException.cshtml @@ -0,0 +1,4 @@ +@page +@{ + throw new Exception("Oops."); +} diff --git a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/TestApplication/RouteInfo.cs b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/TestApplication/RouteInfo.cs new file mode 100644 index 00000000000..fdb652a0069 --- /dev/null +++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/TestApplication/RouteInfo.cs @@ -0,0 +1,151 @@ +// +// 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.Text.Json.Serialization; +using Microsoft.AspNetCore.Http; +#if NET8_0_OR_GREATER +using Microsoft.AspNetCore.Http.Metadata; +#endif +using Microsoft.AspNetCore.Mvc.Abstractions; +using Microsoft.AspNetCore.Mvc.Controllers; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.AspNetCore.Routing; + +namespace RouteTests.TestApplication; + +public class RouteInfo +{ + public static RouteInfo Current { get; set; } = new(); + + public string? HttpMethod { get; set; } + + public string? Path { get; set; } + + [JsonPropertyName("RoutePattern.RawText")] + public string? RawText { get; set; } + + [JsonPropertyName("IRouteDiagnosticsMetadata.Route")] + public string? RouteDiagnosticMetadata { get; set; } + + [JsonPropertyName("HttpContext.GetRouteData()")] + public IDictionary? RouteData { get; set; } + + public ActionDescriptorInfo? ActionDescriptor { get; set; } + + public void SetValues(HttpContext context) + { + this.HttpMethod = context.Request.Method; + this.Path = $"{context.Request.Path}{context.Request.QueryString}"; + var endpoint = context.GetEndpoint(); + this.RawText = (endpoint as RouteEndpoint)?.RoutePattern.RawText; +#if NET8_0_OR_GREATER + this.RouteDiagnosticMetadata = endpoint?.Metadata.GetMetadata()?.Route; +#endif + this.RouteData = new Dictionary(); + foreach (var value in context.GetRouteData().Values) + { + this.RouteData[value.Key] = value.Value?.ToString(); + } + } + + public void SetValues(ActionDescriptor actionDescriptor) + { + if (this.ActionDescriptor == null) + { + this.ActionDescriptor = new ActionDescriptorInfo(actionDescriptor); + } + } + + public class ActionDescriptorInfo + { + public ActionDescriptorInfo() + { + } + + public ActionDescriptorInfo(ActionDescriptor actionDescriptor) + { + this.AttributeRouteInfo = actionDescriptor.AttributeRouteInfo?.Template; + + this.ActionParameters = new List(); + foreach (var item in actionDescriptor.Parameters) + { + this.ActionParameters.Add(item.Name); + } + + if (actionDescriptor is PageActionDescriptor pad) + { + this.PageActionDescriptorSummary = new PageActionDescriptorInfo(pad.RelativePath, pad.ViewEnginePath); + } + + if (actionDescriptor is ControllerActionDescriptor cad) + { + this.ControllerActionDescriptorSummary = new ControllerActionDescriptorInfo(cad.ControllerName, cad.ActionName); + } + } + + [JsonPropertyName("AttributeRouteInfo.Template")] + public string? AttributeRouteInfo { get; set; } + + [JsonPropertyName("Parameters")] + public IList? ActionParameters { get; set; } + + [JsonPropertyName("ControllerActionDescriptor")] + public ControllerActionDescriptorInfo? ControllerActionDescriptorSummary { get; set; } + + [JsonPropertyName("PageActionDescriptor")] + public PageActionDescriptorInfo? PageActionDescriptorSummary { get; set; } + } + + public class ControllerActionDescriptorInfo + { + public ControllerActionDescriptorInfo() + { + } + + public ControllerActionDescriptorInfo(string controllerName, string actionName) + { + this.ControllerActionDescriptorControllerName = controllerName; + this.ControllerActionDescriptorActionName = actionName; + } + + [JsonPropertyName("ControllerName")] + public string ControllerActionDescriptorControllerName { get; set; } = string.Empty; + + [JsonPropertyName("ActionName")] + public string ControllerActionDescriptorActionName { get; set; } = string.Empty; + } + + public class PageActionDescriptorInfo + { + public PageActionDescriptorInfo() + { + } + + public PageActionDescriptorInfo(string relativePath, string viewEnginePath) + { + this.PageActionDescriptorRelativePath = relativePath; + this.PageActionDescriptorViewEnginePath = viewEnginePath; + } + + [JsonPropertyName("RelativePath")] + public string PageActionDescriptorRelativePath { get; set; } = string.Empty; + + [JsonPropertyName("ViewEnginePath")] + public string PageActionDescriptorViewEnginePath { get; set; } = string.Empty; + } +} diff --git a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/TestApplication/RouteInfoDiagnosticObserver.cs b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/TestApplication/RouteInfoDiagnosticObserver.cs new file mode 100644 index 00000000000..7ca0998d0e8 --- /dev/null +++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/TestApplication/RouteInfoDiagnosticObserver.cs @@ -0,0 +1,123 @@ +// +// 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.Diagnostics; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc.Diagnostics; + +namespace RouteTests.TestApplication; + +/// +/// This observer captures all the available route information for a request. +/// This route information is used for generating a README file for analyzing +/// what information is available in different scenarios. +/// +internal sealed class RouteInfoDiagnosticObserver : IDisposable, IObserver, IObserver> +{ + internal const string OnStartEvent = "Microsoft.AspNetCore.Hosting.HttpRequestIn.Start"; + internal const string OnStopEvent = "Microsoft.AspNetCore.Hosting.HttpRequestIn.Stop"; + internal const string OnMvcBeforeActionEvent = "Microsoft.AspNetCore.Mvc.BeforeAction"; + + private readonly List listenerSubscriptions = new(); + private IDisposable? allSourcesSubscription; + private long disposed; + + public RouteInfoDiagnosticObserver() + { + this.allSourcesSubscription = DiagnosticListener.AllListeners.Subscribe(this); + } + + public void OnNext(DiagnosticListener value) + { + if (value.Name == "Microsoft.AspNetCore") + { + var subscription = value.Subscribe(this); + + lock (this.listenerSubscriptions) + { + this.listenerSubscriptions.Add(subscription); + } + } + } + + public void OnNext(KeyValuePair value) + { + HttpContext? context; + BeforeActionEventData? actionMethodEventData; + RouteInfo? info; + + switch (value.Key) + { + case OnStartEvent: + context = value.Value as HttpContext; + Debug.Assert(context != null, "HttpContext was null"); + info = new RouteInfo(); + info.SetValues(context); + RouteInfo.Current = info; + break; + case OnMvcBeforeActionEvent: + actionMethodEventData = value.Value as BeforeActionEventData; + Debug.Assert(actionMethodEventData != null, $"expected {nameof(BeforeActionEventData)}"); + RouteInfo.Current.SetValues(actionMethodEventData.HttpContext); + RouteInfo.Current.SetValues(actionMethodEventData.ActionDescriptor); + break; + case OnStopEvent: + context = value.Value as HttpContext; + Debug.Assert(context != null, "HttpContext was null"); + RouteInfo.Current.SetValues(context); + break; + default: + break; + } + } + + public void OnCompleted() + { + } + + public void OnError(Exception error) + { + } + + public void Dispose() + { + this.Dispose(true); + GC.SuppressFinalize(this); + } + + private void Dispose(bool disposing) + { + if (Interlocked.CompareExchange(ref this.disposed, 1, 0) == 1) + { + return; + } + + lock (this.listenerSubscriptions) + { + foreach (var listenerSubscription in this.listenerSubscriptions) + { + listenerSubscription?.Dispose(); + } + + this.listenerSubscriptions.Clear(); + } + + this.allSourcesSubscription?.Dispose(); + this.allSourcesSubscription = null; + } +} diff --git a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/TestApplication/TestApplicationFactory.cs b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/TestApplication/TestApplicationFactory.cs new file mode 100644 index 00000000000..b2fc1cdfba3 --- /dev/null +++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/TestApplication/TestApplicationFactory.cs @@ -0,0 +1,170 @@ +// +// 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.Diagnostics; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.FileProviders; +using Microsoft.Extensions.Logging; + +namespace RouteTests.TestApplication; + +public enum TestApplicationScenario +{ + /// + /// An application that uses conventional routing. + /// + ConventionalRouting, + + /// + /// An application that uses attribute routing. + /// + AttributeRouting, + + /// + /// A Minimal API application. + /// + MinimalApi, + + /// + /// An Razor Pages application. + /// + RazorPages, +} + +internal class TestApplicationFactory +{ + private static readonly string AspNetCoreTestsPath = new FileInfo(typeof(RoutingTests)!.Assembly!.Location)!.Directory!.Parent!.Parent!.Parent!.FullName; + private static readonly string ContentRootPath = Path.Combine(AspNetCoreTestsPath, "RouteTests", "TestApplication"); + + public static WebApplication? CreateApplication(TestApplicationScenario config) + { + Debug.Assert(Directory.Exists(ContentRootPath), $"Cannot find ContentRootPath: {ContentRootPath}"); + switch (config) + { + case TestApplicationScenario.ConventionalRouting: + return CreateConventionalRoutingApplication(); + case TestApplicationScenario.AttributeRouting: + return CreateAttributeRoutingApplication(); + case TestApplicationScenario.MinimalApi: + return CreateMinimalApiApplication(); + case TestApplicationScenario.RazorPages: + return CreateRazorPagesApplication(); + default: + throw new ArgumentException($"Invalid {nameof(TestApplicationScenario)}"); + } + } + + private static WebApplication CreateConventionalRoutingApplication() + { + var builder = WebApplication.CreateBuilder(new WebApplicationOptions { ContentRootPath = ContentRootPath }); + builder.Logging.ClearProviders(); + + builder.Services + .AddControllersWithViews() + .AddApplicationPart(typeof(RoutingTests).Assembly); + + var app = builder.Build(); + app.Urls.Clear(); + app.Urls.Add("http://[::1]:0"); + app.UseStaticFiles(); + app.UseRouting(); + + app.MapAreaControllerRoute( + name: "AnotherArea", + areaName: "AnotherArea", + pattern: "SomePrefix/{controller=AnotherArea}/{action=Index}/{id?}"); + + app.MapControllerRoute( + name: "MyArea", + pattern: "{area:exists}/{controller=ControllerForMyArea}/{action=Default}/{id?}"); + + app.MapControllerRoute( + name: "FixedRouteWithConstraints", + pattern: "SomePath/{id}/{num:int}", + defaults: new { controller = "ConventionalRoute", action = "ActionWithStringParameter" }); + + app.MapControllerRoute( + name: "default", + pattern: "{controller=ConventionalRoute}/{action=Default}/{id?}"); + + return app; + } + + private static WebApplication CreateAttributeRoutingApplication() + { + var builder = WebApplication.CreateBuilder(); + builder.Logging.ClearProviders(); + + builder.Services + .AddControllers() + .AddApplicationPart(typeof(RoutingTests).Assembly); + + var app = builder.Build(); + app.Urls.Clear(); + app.Urls.Add("http://[::1]:0"); + app.MapControllers(); + + return app; + } + + private static WebApplication CreateMinimalApiApplication() + { + var builder = WebApplication.CreateBuilder(); + builder.Logging.ClearProviders(); + + var app = builder.Build(); + app.Urls.Clear(); + app.Urls.Add("http://[::1]:0"); + + app.MapGet("/MinimalApi", () => Results.Ok()); + app.MapGet("/MinimalApi/{id}", (int id) => Results.Ok()); + +#if NET7_0_OR_GREATER + var api = app.MapGroup("/MinimalApiUsingMapGroup"); + api.MapGet("/", () => Results.Ok()); + api.MapGet("/{id}", (int id) => Results.Ok()); +#endif + + return app; + } + + private static WebApplication CreateRazorPagesApplication() + { + var builder = WebApplication.CreateBuilder(new WebApplicationOptions { ContentRootPath = ContentRootPath }); + builder.Logging.ClearProviders(); + + builder.Services + .AddRazorPages() + .AddRazorRuntimeCompilation(options => + { + options.FileProviders.Add(new PhysicalFileProvider(ContentRootPath)); + }) + .AddApplicationPart(typeof(RoutingTests).Assembly); + + var app = builder.Build(); + app.Urls.Clear(); + app.Urls.Add("http://[::1]:0"); + app.UseStaticFiles(); + app.UseRouting(); + app.MapRazorPages(); + + return app; + } +} diff --git a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/TestApplication/wwwroot/js/site.js b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/TestApplication/wwwroot/js/site.js new file mode 100644 index 00000000000..dcc7262061a --- /dev/null +++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/TestApplication/wwwroot/js/site.js @@ -0,0 +1,4 @@ +// Please see documentation at https://learn.microsoft.com/aspnet/core/client-side/bundling-and-minification +// for details on configuring this project to bundle and minify static web assets. + +// Write your JavaScript code.