From 072265bbb2eef14daf299b51770b78eb6852abad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Kie=C5=82kowicz?= Date: Tue, 7 May 2024 08:37:30 +0200 Subject: [PATCH 01/26] Feed Instrumentation.AspNetCore from main repository https://github.com/open-telemetry/opentelemetry-dotnet/tree/ac0d1d1fab24db657008e853e2b882096c41a974 --- .../.publicApi/PublicAPI.Shipped.txt | 18 + .../.publicApi/PublicAPI.Unshipped.txt | 0 .../netstandard2.0/PublicAPI.Shipped.txt | 0 .../netstandard2.0/PublicAPI.Unshipped.txt | 0 .../AspNetCoreInstrumentation.cs | 38 + ...mentationMeterProviderBuilderExtensions.cs | 52 + ...entationTracerProviderBuilderExtensions.cs | 123 ++ .../AspNetCoreMetrics.cs | 41 + .../AspNetCoreTraceInstrumentationOptions.cs | 115 ++ .../AssemblyInfo.cs | 10 + .../CHANGELOG.md | 638 +++++++++ .../AspNetCoreInstrumentationEventSource.cs | 94 ++ .../Implementation/HttpInListener.cs | 397 +++++ .../Implementation/HttpInMetricsListener.cs | 128 ++ .../Implementation/HttpTagHelper.cs | 33 + .../Implementation/TelemetryHelper.cs | 28 + ...elemetry.Instrumentation.AspNetCore.csproj | 48 + .../README.md | 331 +++++ .../AttributesExtensions.cs | 12 + .../BasicTests.cs | 1273 +++++++++++++++++ .../DependencyInjectionConfigTests.cs | 54 + .../EventSourceTest.cs | 17 + ...stsCollectionsIsAccordingToTheSpecTests.cs | 171 +++ .../MetricTests.cs | 419 ++++++ ...ry.Instrumentation.AspNetCore.Tests.csproj | 47 + .../RouteTests/README.md | 204 +++ .../RouteTests/README.net6.0.md | 612 ++++++++ .../RouteTests/README.net7.0.md | 654 +++++++++ .../RouteTests/README.net8.0.md | 654 +++++++++ .../RouteTests/RoutingTestCases.cs | 69 + .../RouteTests/RoutingTestCases.json | 211 +++ .../RouteTests/RoutingTestFixture.cs | 108 ++ .../RouteTests/RoutingTestResult.cs | 33 + .../RouteTests/RoutingTests.cs | 139 ++ .../Controllers/AnotherAreaController.cs | 14 + .../ControllerForMyAreaController.cs | 16 + .../Controllers/AttributeRouteController.cs | 23 + .../ConventionalRouteController.cs | 17 + .../TestApplication/Pages/Index.cshtml | 2 + .../Pages/PageThatThrowsException.cshtml | 4 + .../RouteTests/TestApplication/RouteInfo.cs | 138 ++ .../RouteInfoDiagnosticObserver.cs | 110 ++ .../TestApplication/TestApplicationFactory.cs | 199 +++ .../TestApplication/wwwroot/js/site.js | 4 + test/TestApp.AspNetCore/ActivityMiddleware.cs | 44 + test/TestApp.AspNetCore/CallbackMiddleware.cs | 32 + .../Controllers/ChildActivityController.cs | 47 + .../Controllers/ErrorController.cs | 17 + .../Controllers/ValuesController.cs | 42 + .../Filters/ExceptionFilter1.cs | 14 + .../Filters/ExceptionFilter2.cs | 14 + test/TestApp.AspNetCore/Program.cs | 54 + .../Properties/launchSettings.json | 12 + .../TestApp.AspNetCore.csproj | 19 + test/TestApp.AspNetCore/TestMiddleware.cs | 24 + .../appsettings.Development.json | 8 + test/TestApp.AspNetCore/appsettings.json | 9 + 57 files changed, 7634 insertions(+) create mode 100644 src/OpenTelemetry.Instrumentation.AspNetCore/.publicApi/PublicAPI.Shipped.txt create mode 100644 src/OpenTelemetry.Instrumentation.AspNetCore/.publicApi/PublicAPI.Unshipped.txt create mode 100644 src/OpenTelemetry.Instrumentation.AspNetCore/.publicApi/netstandard2.0/PublicAPI.Shipped.txt create mode 100644 src/OpenTelemetry.Instrumentation.AspNetCore/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt create mode 100644 src/OpenTelemetry.Instrumentation.AspNetCore/AspNetCoreInstrumentation.cs create mode 100644 src/OpenTelemetry.Instrumentation.AspNetCore/AspNetCoreInstrumentationMeterProviderBuilderExtensions.cs create mode 100644 src/OpenTelemetry.Instrumentation.AspNetCore/AspNetCoreInstrumentationTracerProviderBuilderExtensions.cs create mode 100644 src/OpenTelemetry.Instrumentation.AspNetCore/AspNetCoreMetrics.cs create mode 100644 src/OpenTelemetry.Instrumentation.AspNetCore/AspNetCoreTraceInstrumentationOptions.cs create mode 100644 src/OpenTelemetry.Instrumentation.AspNetCore/AssemblyInfo.cs create mode 100644 src/OpenTelemetry.Instrumentation.AspNetCore/CHANGELOG.md create mode 100644 src/OpenTelemetry.Instrumentation.AspNetCore/Implementation/AspNetCoreInstrumentationEventSource.cs create mode 100644 src/OpenTelemetry.Instrumentation.AspNetCore/Implementation/HttpInListener.cs create mode 100644 src/OpenTelemetry.Instrumentation.AspNetCore/Implementation/HttpInMetricsListener.cs create mode 100644 src/OpenTelemetry.Instrumentation.AspNetCore/Implementation/HttpTagHelper.cs create mode 100644 src/OpenTelemetry.Instrumentation.AspNetCore/Implementation/TelemetryHelper.cs create mode 100644 src/OpenTelemetry.Instrumentation.AspNetCore/OpenTelemetry.Instrumentation.AspNetCore.csproj create mode 100644 src/OpenTelemetry.Instrumentation.AspNetCore/README.md create mode 100644 test/OpenTelemetry.Instrumentation.AspNetCore.Tests/AttributesExtensions.cs create mode 100644 test/OpenTelemetry.Instrumentation.AspNetCore.Tests/BasicTests.cs create mode 100644 test/OpenTelemetry.Instrumentation.AspNetCore.Tests/DependencyInjectionConfigTests.cs create mode 100644 test/OpenTelemetry.Instrumentation.AspNetCore.Tests/EventSourceTest.cs create mode 100644 test/OpenTelemetry.Instrumentation.AspNetCore.Tests/IncomingRequestsCollectionsIsAccordingToTheSpecTests.cs create mode 100644 test/OpenTelemetry.Instrumentation.AspNetCore.Tests/MetricTests.cs create mode 100644 test/OpenTelemetry.Instrumentation.AspNetCore.Tests/OpenTelemetry.Instrumentation.AspNetCore.Tests.csproj 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 create mode 100644 test/TestApp.AspNetCore/ActivityMiddleware.cs create mode 100644 test/TestApp.AspNetCore/CallbackMiddleware.cs create mode 100644 test/TestApp.AspNetCore/Controllers/ChildActivityController.cs create mode 100644 test/TestApp.AspNetCore/Controllers/ErrorController.cs create mode 100644 test/TestApp.AspNetCore/Controllers/ValuesController.cs create mode 100644 test/TestApp.AspNetCore/Filters/ExceptionFilter1.cs create mode 100644 test/TestApp.AspNetCore/Filters/ExceptionFilter2.cs create mode 100644 test/TestApp.AspNetCore/Program.cs create mode 100644 test/TestApp.AspNetCore/Properties/launchSettings.json create mode 100644 test/TestApp.AspNetCore/TestApp.AspNetCore.csproj create mode 100644 test/TestApp.AspNetCore/TestMiddleware.cs create mode 100644 test/TestApp.AspNetCore/appsettings.Development.json create mode 100644 test/TestApp.AspNetCore/appsettings.json diff --git a/src/OpenTelemetry.Instrumentation.AspNetCore/.publicApi/PublicAPI.Shipped.txt b/src/OpenTelemetry.Instrumentation.AspNetCore/.publicApi/PublicAPI.Shipped.txt new file mode 100644 index 0000000000..fc47928891 --- /dev/null +++ b/src/OpenTelemetry.Instrumentation.AspNetCore/.publicApi/PublicAPI.Shipped.txt @@ -0,0 +1,18 @@ +OpenTelemetry.Instrumentation.AspNetCore.AspNetCoreTraceInstrumentationOptions +OpenTelemetry.Instrumentation.AspNetCore.AspNetCoreTraceInstrumentationOptions.AspNetCoreTraceInstrumentationOptions() -> void +OpenTelemetry.Instrumentation.AspNetCore.AspNetCoreTraceInstrumentationOptions.EnrichWithException.get -> System.Action +OpenTelemetry.Instrumentation.AspNetCore.AspNetCoreTraceInstrumentationOptions.EnrichWithException.set -> void +OpenTelemetry.Instrumentation.AspNetCore.AspNetCoreTraceInstrumentationOptions.EnrichWithHttpRequest.get -> System.Action +OpenTelemetry.Instrumentation.AspNetCore.AspNetCoreTraceInstrumentationOptions.EnrichWithHttpRequest.set -> void +OpenTelemetry.Instrumentation.AspNetCore.AspNetCoreTraceInstrumentationOptions.EnrichWithHttpResponse.get -> System.Action +OpenTelemetry.Instrumentation.AspNetCore.AspNetCoreTraceInstrumentationOptions.EnrichWithHttpResponse.set -> void +OpenTelemetry.Instrumentation.AspNetCore.AspNetCoreTraceInstrumentationOptions.Filter.get -> System.Func +OpenTelemetry.Instrumentation.AspNetCore.AspNetCoreTraceInstrumentationOptions.Filter.set -> void +OpenTelemetry.Instrumentation.AspNetCore.AspNetCoreTraceInstrumentationOptions.RecordException.get -> bool +OpenTelemetry.Instrumentation.AspNetCore.AspNetCoreTraceInstrumentationOptions.RecordException.set -> void +OpenTelemetry.Metrics.AspNetCoreInstrumentationMeterProviderBuilderExtensions +OpenTelemetry.Trace.AspNetCoreInstrumentationTracerProviderBuilderExtensions +static OpenTelemetry.Metrics.AspNetCoreInstrumentationMeterProviderBuilderExtensions.AddAspNetCoreInstrumentation(this OpenTelemetry.Metrics.MeterProviderBuilder builder) -> OpenTelemetry.Metrics.MeterProviderBuilder +static OpenTelemetry.Trace.AspNetCoreInstrumentationTracerProviderBuilderExtensions.AddAspNetCoreInstrumentation(this OpenTelemetry.Trace.TracerProviderBuilder builder) -> OpenTelemetry.Trace.TracerProviderBuilder +static OpenTelemetry.Trace.AspNetCoreInstrumentationTracerProviderBuilderExtensions.AddAspNetCoreInstrumentation(this OpenTelemetry.Trace.TracerProviderBuilder builder, string name, System.Action configureAspNetCoreTraceInstrumentationOptions) -> OpenTelemetry.Trace.TracerProviderBuilder +static OpenTelemetry.Trace.AspNetCoreInstrumentationTracerProviderBuilderExtensions.AddAspNetCoreInstrumentation(this OpenTelemetry.Trace.TracerProviderBuilder builder, System.Action configureAspNetCoreTraceInstrumentationOptions) -> OpenTelemetry.Trace.TracerProviderBuilder diff --git a/src/OpenTelemetry.Instrumentation.AspNetCore/.publicApi/PublicAPI.Unshipped.txt b/src/OpenTelemetry.Instrumentation.AspNetCore/.publicApi/PublicAPI.Unshipped.txt new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/OpenTelemetry.Instrumentation.AspNetCore/.publicApi/netstandard2.0/PublicAPI.Shipped.txt b/src/OpenTelemetry.Instrumentation.AspNetCore/.publicApi/netstandard2.0/PublicAPI.Shipped.txt new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/OpenTelemetry.Instrumentation.AspNetCore/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt b/src/OpenTelemetry.Instrumentation.AspNetCore/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/OpenTelemetry.Instrumentation.AspNetCore/AspNetCoreInstrumentation.cs b/src/OpenTelemetry.Instrumentation.AspNetCore/AspNetCoreInstrumentation.cs new file mode 100644 index 0000000000..d309679262 --- /dev/null +++ b/src/OpenTelemetry.Instrumentation.AspNetCore/AspNetCoreInstrumentation.cs @@ -0,0 +1,38 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using OpenTelemetry.Instrumentation.AspNetCore.Implementation; + +namespace OpenTelemetry.Instrumentation.AspNetCore; + +/// +/// Asp.Net Core Requests instrumentation. +/// +internal sealed class AspNetCoreInstrumentation : IDisposable +{ + private static readonly HashSet DiagnosticSourceEvents = new() + { + "Microsoft.AspNetCore.Hosting.HttpRequestIn", + "Microsoft.AspNetCore.Hosting.HttpRequestIn.Start", + "Microsoft.AspNetCore.Hosting.HttpRequestIn.Stop", + "Microsoft.AspNetCore.Diagnostics.UnhandledException", + "Microsoft.AspNetCore.Hosting.UnhandledException", + }; + + private readonly Func isEnabled = (eventName, _, _) + => DiagnosticSourceEvents.Contains(eventName); + + private readonly DiagnosticSourceSubscriber diagnosticSourceSubscriber; + + public AspNetCoreInstrumentation(HttpInListener httpInListener) + { + this.diagnosticSourceSubscriber = new DiagnosticSourceSubscriber(httpInListener, this.isEnabled, AspNetCoreInstrumentationEventSource.Log.UnknownErrorProcessingEvent); + this.diagnosticSourceSubscriber.Subscribe(); + } + + /// + public void Dispose() + { + this.diagnosticSourceSubscriber?.Dispose(); + } +} diff --git a/src/OpenTelemetry.Instrumentation.AspNetCore/AspNetCoreInstrumentationMeterProviderBuilderExtensions.cs b/src/OpenTelemetry.Instrumentation.AspNetCore/AspNetCoreInstrumentationMeterProviderBuilderExtensions.cs new file mode 100644 index 0000000000..3da8c1de59 --- /dev/null +++ b/src/OpenTelemetry.Instrumentation.AspNetCore/AspNetCoreInstrumentationMeterProviderBuilderExtensions.cs @@ -0,0 +1,52 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#if !NET8_0_OR_GREATER +using OpenTelemetry.Instrumentation.AspNetCore; +using OpenTelemetry.Instrumentation.AspNetCore.Implementation; +#endif +using OpenTelemetry.Internal; + +namespace OpenTelemetry.Metrics; + +/// +/// Extension methods to simplify registering of ASP.NET Core request instrumentation. +/// +public static class AspNetCoreInstrumentationMeterProviderBuilderExtensions +{ + /// + /// Enables the incoming requests automatic data collection for ASP.NET Core. + /// + /// being configured. + /// The instance of to chain the calls. + public static MeterProviderBuilder AddAspNetCoreInstrumentation( + this MeterProviderBuilder builder) + { + Guard.ThrowIfNull(builder); + +#if NET8_0_OR_GREATER + return builder.ConfigureMeters(); +#else + // Note: Warm-up the status code and method mapping. + _ = TelemetryHelper.BoxedStatusCodes; + _ = RequestMethodHelper.KnownMethods; + + builder.AddMeter(HttpInMetricsListener.InstrumentationName); + + builder.AddInstrumentation(new AspNetCoreMetrics()); + + return builder; +#endif + } + + internal static MeterProviderBuilder ConfigureMeters(this MeterProviderBuilder builder) + { + return builder + .AddMeter("Microsoft.AspNetCore.Hosting") + .AddMeter("Microsoft.AspNetCore.Server.Kestrel") + .AddMeter("Microsoft.AspNetCore.Http.Connections") + .AddMeter("Microsoft.AspNetCore.Routing") + .AddMeter("Microsoft.AspNetCore.Diagnostics") + .AddMeter("Microsoft.AspNetCore.RateLimiting"); + } +} diff --git a/src/OpenTelemetry.Instrumentation.AspNetCore/AspNetCoreInstrumentationTracerProviderBuilderExtensions.cs b/src/OpenTelemetry.Instrumentation.AspNetCore/AspNetCoreInstrumentationTracerProviderBuilderExtensions.cs new file mode 100644 index 0000000000..8ec35718cc --- /dev/null +++ b/src/OpenTelemetry.Instrumentation.AspNetCore/AspNetCoreInstrumentationTracerProviderBuilderExtensions.cs @@ -0,0 +1,123 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using System.Diagnostics; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using OpenTelemetry.Instrumentation.AspNetCore; +using OpenTelemetry.Instrumentation.AspNetCore.Implementation; +using OpenTelemetry.Internal; + +namespace OpenTelemetry.Trace; + +/// +/// Extension methods to simplify registering of ASP.NET Core request instrumentation. +/// +public static class AspNetCoreInstrumentationTracerProviderBuilderExtensions +{ + /// + /// Enables the incoming requests automatic data collection for ASP.NET Core. + /// + /// being configured. + /// The instance of to chain the calls. + public static TracerProviderBuilder AddAspNetCoreInstrumentation(this TracerProviderBuilder builder) + => AddAspNetCoreInstrumentation(builder, name: null, configureAspNetCoreTraceInstrumentationOptions: null); + + /// + /// Enables the incoming requests automatic data collection for ASP.NET Core. + /// + /// being configured. + /// Callback action for configuring . + /// The instance of to chain the calls. + public static TracerProviderBuilder AddAspNetCoreInstrumentation( + this TracerProviderBuilder builder, + Action configureAspNetCoreTraceInstrumentationOptions) + => AddAspNetCoreInstrumentation(builder, name: null, configureAspNetCoreTraceInstrumentationOptions); + + /// + /// Enables the incoming requests automatic data collection for ASP.NET Core. + /// + /// being configured. + /// Name which is used when retrieving options. + /// Callback action for configuring . + /// The instance of to chain the calls. + public static TracerProviderBuilder AddAspNetCoreInstrumentation( + this TracerProviderBuilder builder, + string name, + Action configureAspNetCoreTraceInstrumentationOptions) + { + Guard.ThrowIfNull(builder); + + // Note: Warm-up the status code and method mapping. + _ = TelemetryHelper.BoxedStatusCodes; + _ = RequestMethodHelper.KnownMethods; + + name ??= Options.DefaultName; + + builder.ConfigureServices(services => + { + if (configureAspNetCoreTraceInstrumentationOptions != null) + { + services.Configure(name, configureAspNetCoreTraceInstrumentationOptions); + } + + services.RegisterOptionsFactory(configuration => new AspNetCoreTraceInstrumentationOptions(configuration)); + }); + + if (builder is IDeferredTracerProviderBuilder deferredTracerProviderBuilder) + { + deferredTracerProviderBuilder.Configure((sp, builder) => + { + AddAspNetCoreInstrumentationSources(builder, sp); + }); + } + + return builder.AddInstrumentation(sp => + { + var options = sp.GetRequiredService>().Get(name); + + return new AspNetCoreInstrumentation( + new HttpInListener(options)); + }); + } + + // Note: This is used by unit tests. + internal static TracerProviderBuilder AddAspNetCoreInstrumentation( + this TracerProviderBuilder builder, + HttpInListener listener) + { + builder.AddAspNetCoreInstrumentationSources(); + + return builder.AddInstrumentation( + new AspNetCoreInstrumentation(listener)); + } + + private static void AddAspNetCoreInstrumentationSources( + this TracerProviderBuilder builder, + IServiceProvider serviceProvider = null) + { + // For .NET7.0 onwards activity will be created using activitySource. + // https://github.com/dotnet/aspnetcore/blob/bf3352f2422bf16fa3ca49021f0e31961ce525eb/src/Hosting/Hosting/src/Internal/HostingApplicationDiagnostics.cs#L327 + // For .NET6.0 and below, we will continue to use legacy way. + if (HttpInListener.Net7OrGreater) + { + // TODO: Check with .NET team to see if this can be prevented + // as this allows user to override the ActivitySource. + var activitySourceService = serviceProvider?.GetService(); + if (activitySourceService != null) + { + builder.AddSource(activitySourceService.Name); + } + else + { + // For users not using hosting package? + builder.AddSource(HttpInListener.AspNetCoreActivitySourceName); + } + } + else + { + builder.AddSource(HttpInListener.ActivitySourceName); + builder.AddLegacySource(HttpInListener.ActivityOperationName); // for the activities created by AspNetCore + } + } +} diff --git a/src/OpenTelemetry.Instrumentation.AspNetCore/AspNetCoreMetrics.cs b/src/OpenTelemetry.Instrumentation.AspNetCore/AspNetCoreMetrics.cs new file mode 100644 index 0000000000..a819d561a9 --- /dev/null +++ b/src/OpenTelemetry.Instrumentation.AspNetCore/AspNetCoreMetrics.cs @@ -0,0 +1,41 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#if !NET8_0_OR_GREATER +using OpenTelemetry.Instrumentation.AspNetCore.Implementation; + +namespace OpenTelemetry.Instrumentation.AspNetCore; + +/// +/// Asp.Net Core Requests instrumentation. +/// +internal sealed class AspNetCoreMetrics : IDisposable +{ + private static readonly HashSet DiagnosticSourceEvents = new() + { + "Microsoft.AspNetCore.Hosting.HttpRequestIn", + "Microsoft.AspNetCore.Hosting.HttpRequestIn.Start", + "Microsoft.AspNetCore.Hosting.HttpRequestIn.Stop", + "Microsoft.AspNetCore.Diagnostics.UnhandledException", + "Microsoft.AspNetCore.Hosting.UnhandledException", + }; + + private readonly Func isEnabled = (eventName, _, _) + => DiagnosticSourceEvents.Contains(eventName); + + private readonly DiagnosticSourceSubscriber diagnosticSourceSubscriber; + + internal AspNetCoreMetrics() + { + var metricsListener = new HttpInMetricsListener("Microsoft.AspNetCore"); + this.diagnosticSourceSubscriber = new DiagnosticSourceSubscriber(metricsListener, this.isEnabled, AspNetCoreInstrumentationEventSource.Log.UnknownErrorProcessingEvent); + this.diagnosticSourceSubscriber.Subscribe(); + } + + /// + public void Dispose() + { + this.diagnosticSourceSubscriber?.Dispose(); + } +} +#endif diff --git a/src/OpenTelemetry.Instrumentation.AspNetCore/AspNetCoreTraceInstrumentationOptions.cs b/src/OpenTelemetry.Instrumentation.AspNetCore/AspNetCoreTraceInstrumentationOptions.cs new file mode 100644 index 0000000000..f5ffb7962f --- /dev/null +++ b/src/OpenTelemetry.Instrumentation.AspNetCore/AspNetCoreTraceInstrumentationOptions.cs @@ -0,0 +1,115 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using System.Diagnostics; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Configuration; +using OpenTelemetry.Instrumentation.AspNetCore.Implementation; + +namespace OpenTelemetry.Instrumentation.AspNetCore; + +/// +/// Options for requests instrumentation. +/// +public class AspNetCoreTraceInstrumentationOptions +{ + /// + /// Initializes a new instance of the class. + /// + public AspNetCoreTraceInstrumentationOptions() + : this(new ConfigurationBuilder().AddEnvironmentVariables().Build()) + { + } + + internal AspNetCoreTraceInstrumentationOptions(IConfiguration configuration) + { + Debug.Assert(configuration != null, "configuration was null"); + + if (configuration.TryGetBoolValue( + AspNetCoreInstrumentationEventSource.Log, + "OTEL_DOTNET_EXPERIMENTAL_ASPNETCORE_ENABLE_GRPC_INSTRUMENTATION", + out var enableGrpcInstrumentation)) + { + this.EnableGrpcAspNetCoreSupport = enableGrpcInstrumentation; + } + + if (configuration.TryGetBoolValue( + AspNetCoreInstrumentationEventSource.Log, + "OTEL_DOTNET_EXPERIMENTAL_ASPNETCORE_DISABLE_URL_QUERY_REDACTION", + out var disableUrlQueryRedaction)) + { + this.DisableUrlQueryRedaction = disableUrlQueryRedaction; + } + } + + /// + /// Gets or sets a filter function that determines whether or not to + /// collect telemetry on a per request basis. + /// + /// + /// Notes: + /// + /// The return value for the filter function is interpreted as: + /// + /// If filter returns , the request is + /// collected. + /// If filter returns or throws an + /// exception the request is NOT collected. + /// + /// + /// + public Func Filter { get; set; } + + /// + /// Gets or sets an action to enrich an Activity. + /// + /// + /// : the activity being enriched. + /// : the HttpRequest object from which additional information can be extracted to enrich the activity. + /// + public Action EnrichWithHttpRequest { get; set; } + + /// + /// Gets or sets an action to enrich an Activity. + /// + /// + /// : the activity being enriched. + /// : the HttpResponse object from which additional information can be extracted to enrich the activity. + /// + public Action EnrichWithHttpResponse { get; set; } + + /// + /// Gets or sets an action to enrich an Activity. + /// + /// + /// : the activity being enriched. + /// : the Exception object from which additional information can be extracted to enrich the activity. + /// + public Action EnrichWithException { get; set; } + + /// + /// Gets or sets a value indicating whether the exception will be recorded as ActivityEvent or not. + /// + /// + /// https://github.com/open-telemetry/semantic-conventions/blob/main/docs/exceptions/exceptions-spans.md. + /// + public bool RecordException { get; set; } + + /// + /// Gets or sets a value indicating whether RPC attributes are added to an Activity when using Grpc.AspNetCore. + /// + /// + /// https://github.com/open-telemetry/semantic-conventions/blob/main/docs/rpc/rpc-spans.md. + /// + internal bool EnableGrpcAspNetCoreSupport { get; set; } + + /// + /// Gets or sets a value indicating whether the url query value should be redacted or not. + /// + /// + /// The query parameter values are redacted with value set as Redacted. + /// e.g. `?key1=value1` is set as `?key1=Redacted`. + /// The redaction can be disabled by setting this property to . + /// + internal bool DisableUrlQueryRedaction { get; set; } +} diff --git a/src/OpenTelemetry.Instrumentation.AspNetCore/AssemblyInfo.cs b/src/OpenTelemetry.Instrumentation.AspNetCore/AssemblyInfo.cs new file mode 100644 index 0000000000..2cd1a339fd --- /dev/null +++ b/src/OpenTelemetry.Instrumentation.AspNetCore/AssemblyInfo.cs @@ -0,0 +1,10 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using System.Runtime.CompilerServices; + +#if SIGNED +[assembly: InternalsVisibleTo("OpenTelemetry.Instrumentation.AspNetCore.Tests, PublicKey=002400000480000094000000060200000024000052534131000400000100010051c1562a090fb0c9f391012a32198b5e5d9a60e9b80fa2d7b434c9e5ccb7259bd606e66f9660676afc6692b8cdc6793d190904551d2103b7b22fa636dcbb8208839785ba402ea08fc00c8f1500ccef28bbf599aa64ffb1e1d5dc1bf3420a3777badfe697856e9d52070a50c3ea5821c80bef17ca3acffa28f89dd413f096f898")] +#else +[assembly: InternalsVisibleTo("OpenTelemetry.Instrumentation.AspNetCore.Tests")] +#endif diff --git a/src/OpenTelemetry.Instrumentation.AspNetCore/CHANGELOG.md b/src/OpenTelemetry.Instrumentation.AspNetCore/CHANGELOG.md new file mode 100644 index 0000000000..d8f74ef6cc --- /dev/null +++ b/src/OpenTelemetry.Instrumentation.AspNetCore/CHANGELOG.md @@ -0,0 +1,638 @@ +# Changelog + +## 1.8.1 + +Released 2024-Apr-12 + +* **Breaking Change**: Fixed tracing instrumentation so that by default any + values detected in the query string component of requests are replaced with + the text `Redacted` when building the `url.query` tag. For example, + `?key1=value1&key2=value2` becomes `?key1=Redacted&key2=Redacted`. You can + disable this redaction by setting the environment variable + `OTEL_DOTNET_EXPERIMENTAL_ASPNETCORE_DISABLE_URL_QUERY_REDACTION` to `true`. + ([#5532](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5532)) + +## 1.8.0 + +Released 2024-Apr-04 + +* Fixed an issue for spans when `server.port` attribute was not set with + `server.address` when it has default values (`80` for `HTTP` and + `443` for `HTTPS` protocol). + ([#5419](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5419)) + +* Fixed an issue where the `http.request.method_original` attribute was not set + on activity. Now, when `http.request.method` is set and the original method + is converted to its canonical form (e.g., `Get` is converted to `GET`), + the original value `Get` will be stored in `http.request.method_original`. + ([#5471](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5471)) + +* Fixed the name of spans that have `http.request.method` attribute set to `_OTHER`. + The span name will be set as `HTTP {http.route}` as per the [specification](https://github.com/open-telemetry/semantic-conventions/blob/v1.24.0/docs/http/http-spans.md#name). + ([#5484](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5484)) + +## 1.7.1 + +Released 2024-Feb-09 + +* Fixed issue + [#4466](https://github.com/open-telemetry/opentelemetry-dotnet/issues/4466) + where the activity instance returned by `Activity.Current` was different than + instance obtained from `IHttpActivityFeature.Activity`. + ([#5136](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5136)) + +* Fixed an issue where the `http.route` attribute was not set on either the + `Activity` or `http.server.request.duration` metric generated from a + request when an exception handling middleware is invoked. One caveat is that + this fix does not address the problem for the `http.server.request.duration` + metric when running ASP.NET Core 8. ASP.NET Core 8 contains an equivalent fix + which should ship in version 8.0.2 + (see: [dotnet/aspnetcore#52652](https://github.com/dotnet/aspnetcore/pull/52652)). + ([#5135](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5135)) + +* Fixes scenario when the `net6.0` target of this library is loaded into a + .NET 7+ process and the instrumentation does not behave as expected. This + is an unusual scenario that does not affect users consuming this package + normally. This fix is primarily to support the + [opentelemetry-dotnet-instrumentation](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5252) + project. + ([#5252](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5252)) + +## 1.7.0 + +Released 2023-Dec-13 + +## 1.6.0 - First stable release of this library + +Released 2023-Dec-13 + +* Re-introduced support for gRPC instrumentation as an opt-in experimental + feature. From now onwards, gRPC can be enabled by setting + `OTEL_DOTNET_EXPERIMENTAL_ASPNETCORE_ENABLE_GRPC_INSTRUMENTATION` flag to + `True`. `OTEL_DOTNET_EXPERIMENTAL_ASPNETCORE_ENABLE_GRPC_INSTRUMENTATION` can + be set as an environment variable or via IConfiguration. The change is + introduced in order to support stable release of `http` instrumentation. + Semantic conventions for RPC is still + [experimental](https://github.com/open-telemetry/semantic-conventions/tree/main/docs/rpc) + and hence the package will only support it as an opt-in experimental feature. + Note that the support was removed in `1.6.0-rc.1` version of the package and + versions released before `1.6.0-rc.1` had gRPC instrumentation enabled by + default. + ([#5130](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5130)) + +## 1.6.0-rc.1 + +Released 2023-Dec-01 + +* Removed support for `OTEL_SEMCONV_STABILITY_OPT_IN` environment variable. The + library will now emit only the + [stable](https://github.com/open-telemetry/semantic-conventions/tree/v1.23.0/docs/http) + semantic conventions. + ([#5066](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5066)) + +* Removed `netstandard2.1` target. + ([#5094](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5094)) + +* Removed support for grpc instrumentation to unblock stable release of http + instrumentation. For details, see issue + [#5098](https://github.com/open-telemetry/opentelemetry-dotnet/issues/5098) + ([#5097](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5097)) + +* **Breaking Change** : Renamed `AspNetCoreInstrumentationOptions` to + `AspNetCoreTraceInstrumentationOptions`. + ([#5108](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5108)) + +## 1.6.0-beta.3 + +Released 2023-Nov-17 + +* 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) + then the request method will be set on an additional tag + `http.request.method.original` and `http.request.method` will be set to + `_OTHER`. + * For metrics, if the original method does not belong to one of the [known + values](https://github.com/open-telemetry/semantic-conventions/blob/v1.22.0/docs/http/http-spans.md#:~:text=http.request.method%20has%20the%20following%20list%20of%20well%2Dknown%20values) + then `http.request.method` on `http.server.request.duration` metric will be + set to `_OTHER` + + `http.request.method` is set on `http.server.request.duration` metric or + activity when `OTEL_SEMCONV_STABILITY_OPT_IN` environment variable is set to + `http` or `http/dup`. + ([#5001](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5001)) + +* An additional attribute `error.type` will be added to activity and +`http.server.request.duration` metric when the request results in unhandled +exception. The attribute value will be set to full name of exception type. + + The attribute will only be added when `OTEL_SEMCONV_STABILITY_OPT_IN` + environment variable is set to `http` or `http/dup`. + ([#4986](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4986)) + +* Fixed `network.protocol.version` attribute values to match the specification. + ([#5007](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5007)) + +* Calls to `/metrics` will now be included in the `http.server.request.duration` + metric. This change may affect Prometheus pull scenario if the Prometheus + server sends request to the scraping endpoint that contains `/metrics` in + path. + ([#5044](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5044)) + +* Fixes the `http.route` attribute for scenarios in which it was + previously missing or incorrect. Additionally, the `http.route` attribute + is now the same for both the metric and `Activity` emitted for a request. + Lastly, the `Activity.DisplayName` has been adjusted to have the format + `{http.request.method} {http.route}` to conform with [the specification](https://github.com/open-telemetry/semantic-conventions/blob/main/docs/http/http-spans.md#name). + There remain scenarios when using conventional routing or Razor pages where + `http.route` is still incorrect. See [#5056](https://github.com/open-telemetry/opentelemetry-dotnet/issues/5056) + and [#5057](https://github.com/open-telemetry/opentelemetry-dotnet/issues/5057) + for more details. + ([#5026](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5026)) + +* Removed `network.protocol.name` from `http.server.request.duration` metric as + per spec. + ([#5049](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5049)) + +## 1.6.0-beta.2 + +Released 2023-Oct-26 + +* Introduced a new metric, `http.server.request.duration` measured in seconds. + The OTel SDK (starting with version 1.6.0) + [applies custom histogram buckets](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4820) + for this metric to comply with the + [Semantic Convention for Http Metrics](https://github.com/open-telemetry/semantic-conventions/blob/2bad9afad58fbd6b33cc683d1ad1f006e35e4a5d/docs/http/http-metrics.md). + This new metric is only available for users who opt-in to the new + semantic convention by configuring the `OTEL_SEMCONV_STABILITY_OPT_IN` + environment variable to either `http` (to emit only the new metric) or + `http/dup` (to emit both the new and old metrics). + ([#4802](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4802)) + * New metric: `http.server.request.duration` + * Unit: `s` (seconds) + * Histogram Buckets: `0, 0.005, 0.01, 0.025, 0.05, 0.075, 0.1, 0.25, 0.5, + 0.75, 1, 2.5, 5, 7.5, 10` + * Old metric: `http.server.duration` + * Unit: `ms` (milliseconds) + * Histogram Buckets: `0, 5, 10, 25, 50, 75, 100, 250, 500, 750, 1000, 2500, + 5000, 7500, 10000` + + Note: the older `http.server.duration` metric and + `OTEL_SEMCONV_STABILITY_OPT_IN` environment variable will eventually be + removed after the HTTP semantic conventions are marked stable. + At which time this instrumentation can publish a stable release. Refer to + the specification for more information regarding the new HTTP semantic + conventions for both + [spans](https://github.com/open-telemetry/semantic-conventions/blob/2bad9afad58fbd6b33cc683d1ad1f006e35e4a5d/docs/http/http-spans.md) + and + [metrics](https://github.com/open-telemetry/semantic-conventions/blob/2bad9afad58fbd6b33cc683d1ad1f006e35e4a5d/docs/http/http-metrics.md). + +* Following metrics will now be enabled by default when targeting `.NET8.0` or + newer framework: + + * **Meter** : `Microsoft.AspNetCore.Hosting` + * `http.server.request.duration` + * `http.server.active_requests` + + * **Meter** : `Microsoft.AspNetCore.Server.Kestrel` + * `kestrel.active_connections` + * `kestrel.connection.duration` + * `kestrel.rejected_connections` + * `kestrel.queued_connections` + * `kestrel.queued_requests` + * `kestrel.upgraded_connections` + * `kestrel.tls_handshake.duration` + * `kestrel.active_tls_handshakes` + + * **Meter** : `Microsoft.AspNetCore.Http.Connections` + * `signalr.server.connection.duration` + * `signalr.server.active_connections` + + * **Meter** : `Microsoft.AspNetCore.Routing` + * `aspnetcore.routing.match_attempts` + + * **Meter** : `Microsoft.AspNetCore.Diagnostics` + * `aspnetcore.diagnostics.exceptions` + + * **Meter** : `Microsoft.AspNetCore.RateLimiting` + * `aspnetcore.rate_limiting.active_request_leases` + * `aspnetcore.rate_limiting.request_lease.duration` + * `aspnetcore.rate_limiting.queued_requests` + * `aspnetcore.rate_limiting.request.time_in_queue` + * `aspnetcore.rate_limiting.requests` + + For details about each individual metric check [ASP.NET Core + docs + page](https://learn.microsoft.com/dotnet/core/diagnostics/built-in-metrics-aspnetcore). + + **NOTES**: + * When targeting `.NET8.0` framework or newer, `http.server.request.duration` metric + will only follow + [v1.22.0](https://github.com/open-telemetry/semantic-conventions/blob/v1.22.0/docs/http/http-metrics.md#metric-httpclientrequestduration) + semantic conventions specification. Ability to switch behavior to older + conventions using `OTEL_SEMCONV_STABILITY_OPT_IN` environment variable is + not available. + * Users can opt-out of metrics that are not required using + [views](https://github.com/open-telemetry/opentelemetry-dotnet/tree/main/docs/metrics/customizing-the-sdk#drop-an-instrument). + + ([#4934](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4934)) + +* Added `network.protocol.name` dimension to `http.server.request.duration` +metric. This change only affects users setting `OTEL_SEMCONV_STABILITY_OPT_IN` +to `http` or `http/dup`. +([#4934](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4934)) + +* **Breaking**: Removed `Enrich` and `Filter` support for **metrics** + instrumentation. With this change, `AspNetCoreMetricsInstrumentationOptions` + is no longer available. + ([#4981](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4981)) + + * `Enrich` migration: + + An enrichment API for the `http.server.request.duration` metric is available + inside AspNetCore for users targeting .NET 8.0 (or newer). For details see: + [Enrich the ASP.NET Core request + metric](https://learn.microsoft.com/aspnet/core/log-mon/metrics/metrics?view=aspnetcore-8.0#enrich-the-aspnet-core-request-metric). + + * `Filter` migration: + + There is no comparable filter mechanism currently available for any .NET + version. Please [share your + feedback](https://github.com/open-telemetry/opentelemetry-dotnet/issues/4982) + if you are impacted by this feature gap. + + > **Note** + > The [View API](https://github.com/open-telemetry/opentelemetry-dotnet/tree/main/docs/metrics/customizing-the-sdk#select-specific-tags) + may be used to drop dimensions. + +* Updated description for `http.server.request.duration` metrics to match spec + definition. + ([#4990](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4990)) + +## 1.5.1-beta.1 + +Released 2023-Jul-20 + +* The new HTTP and network semantic conventions can be opted in to by setting + the `OTEL_SEMCONV_STABILITY_OPT_IN` environment variable. This allows for a + transition period for users to experiment with the new semantic conventions + and adapt as necessary. The environment variable supports the following + values: + * `http` - emit the new, frozen (proposed for stable) HTTP and networking + attributes, and stop emitting the old experimental HTTP and networking + attributes that the instrumentation emitted previously. + * `http/dup` - emit both the old and the frozen (proposed for stable) HTTP + and networking attributes, allowing for a more seamless transition. + * The default behavior (in the absence of one of these values) is to continue + emitting the same HTTP and network semantic conventions that were emitted in + `1.5.0-beta.1`. + * Note: this option will eventually be removed after the new HTTP and + network semantic conventions are marked stable. At which time this + instrumentation can receive a stable release, and the old HTTP and + network semantic conventions will no longer be supported. Refer to the + specification for more information regarding the new HTTP and network + semantic conventions for both + [spans](https://github.com/open-telemetry/semantic-conventions/blob/v1.21.0/docs/http/http-spans.md) + and + [metrics](https://github.com/open-telemetry/semantic-conventions/blob/v1.21.0/docs/http/http-metrics.md). + ([#4537](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4537), + [#4606](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4606), + [#4660](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4660)) + +* Fixed an issue affecting NET 7.0+. If custom propagation is being used + and tags are added to an Activity during sampling then that Activity would be dropped. + ([#4637](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4637)) + +## 1.5.0-beta.1 + +Released 2023-Jun-05 + +* Bumped the package version to `1.5.0-beta.1` to keep its major and minor + version in sync with that of the core packages. This would make it more + intuitive for users to figure out what version of core packages would work + with a given version of this package. The pre-release identifier has also been + changed from `rc` to `beta` as we believe this more accurately reflects the + status of this package. We believe the `rc` identifier will be more + appropriate as semantic conventions reach stability. + +* Fix issue where baggage gets cleared when the ASP.NET Core Activity + is stopped. The instrumentation no longer clears baggage. One problem + this caused was that it prevented Activity processors from accessing baggage + during their `OnEnd` call. +([#4274](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4274)) + +* Added direct reference to `System.Text.Encodings.Web` with minimum version of +`4.7.2` due to [CVE-2021-26701](https://github.com/dotnet/runtime/issues/49377). +This impacts target frameworks `netstandard2.0` and `netstandard2.1` which has a +reference to `Microsoft.AspNetCore.Http.Abstractions` that depends on +`System.Text.Encodings.Web` >= 4.5.0. +([#4399](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4399)) + +* Improve perf by avoiding boxing of common status codes values. + ([#4360](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4360), + [#4363](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4363)) + +## 1.0.0-rc9.14 + +Released 2023-Feb-24 + +* Updated OTel SDK dependency to 1.4.0 + +## 1.4.0-rc9.13 + +Released 2023-Feb-10 + +## 1.0.0-rc9.12 + +Released 2023-Feb-01 + +## 1.0.0-rc9.11 + +Released 2023-Jan-09 + +## 1.0.0-rc9.10 + +Released 2022-Dec-12 + +* **Users migrating from version `1.0.0-rc9.9` will see the following breaking + changes:** + * Updated `http.status_code` dimension type from string to int for + `http.server.duration` metric. + ([#3930](https://github.com/open-telemetry/opentelemetry-dotnet/pull/3930)) + * `http.host` will no longer be populated on `http.server.duration` metric. + `net.host.name` and `net.host.port` attributes will be populated instead. +([#3928](https://github.com/open-telemetry/opentelemetry-dotnet/pull/3928)) + + * The `http.server.duration` metric's `http.target` attribute is replaced with +`http.route` attribute. +([#3903](https://github.com/open-telemetry/opentelemetry-dotnet/pull/3903)) + + * `http.host` will no longer be populated on activity. `net.host.name` and + `net.host.port` attributes will be populated instead. + ([#3858](https://github.com/open-telemetry/opentelemetry-dotnet/pull/3858)) + +* Extension method `AddAspNetCoreInstrumentation` on `MeterProviderBuilder` now + supports `AspNetCoreMetricsInstrumentationOptions`. This option class exposes + configuration properties for metric filtering and tag enrichment. + ([#3948](https://github.com/open-telemetry/opentelemetry-dotnet/pull/3948), + [#3982](https://github.com/open-telemetry/opentelemetry-dotnet/pull/3982)) + +## 1.0.0-rc9.9 + +Released 2022-Nov-07 + +* **Breaking change** The `Enrich` callback option has been removed. + For better usability, it has been replaced by three separate options: + `EnrichWithHttpRequest`, `EnrichWithHttpResponse` and `EnrichWithException`. + Previously, the single `Enrich` callback required the consumer to detect + which event triggered the callback to be invoked (e.g., request start, + response end, or an exception) and then cast the object received to the + appropriate type: `HttpRequest`, `HttpResponse`, or `Exception`. The separate + callbacks make it clear what event triggers them and there is no longer the + need to cast the argument to the expected type. + ([#3749](https://github.com/open-telemetry/opentelemetry-dotnet/pull/3749)) + +* Added back `netstandard2.0` and `netstandard2.1` targets. +([#3755](https://github.com/open-telemetry/opentelemetry-dotnet/pull/3755)) + +## 1.0.0-rc9.8 + +Released 2022-Oct-17 + +## 1.0.0-rc9.7 + +Released 2022-Sep-29 + +* Performance improvement (Reduced memory allocation) - Updated DiagnosticSource +event subscription to specific set of events. +([#3519](https://github.com/open-telemetry/opentelemetry-dotnet/pull/3519)) + +* Added overloads which accept a name to the `TracerProviderBuilder` + `AddAspNetCoreInstrumentation` extension to allow for more fine-grained + options management + ([#3661](https://github.com/open-telemetry/opentelemetry-dotnet/pull/3661)) + +* Fix issue where when an application has an ExceptionFilter, the exception data + wouldn't be collected. + ([#3475](https://github.com/open-telemetry/opentelemetry-dotnet/pull/3475)) + +## 1.0.0-rc9.6 + +Released 2022-Aug-18 + +* Removed `netstandard2.0` and `netstandard2.1` targets. .NET 5 reached EOL + in May 2022 and .NET Core 3.1 reaches EOL in December 2022. End of support + dates for .NET are published + [here](https://dotnet.microsoft.com/download/dotnet). The + instrumentation for ASP.NET Core now requires .NET 6 or later. + ([#3567](https://github.com/open-telemetry/opentelemetry-dotnet/pull/3567)) + +* Fixed an issue where activity started within middleware was modified by + instrumentation library. + ([#3498](https://github.com/open-telemetry/opentelemetry-dotnet/pull/3498)) + +* Updated to use Activity native support from + `System.Diagnostics.DiagnosticSource` to set activity status. + ([#3118](https://github.com/open-telemetry/opentelemetry-dotnet/issues/3118)) + ([#3555](https://github.com/open-telemetry/opentelemetry-dotnet/pull/3555)) + +## 1.0.0-rc9.5 + +Released 2022-Aug-02 + +* Fix Remote IP Address - NULL reference exception. + ([#3481](https://github.com/open-telemetry/opentelemetry-dotnet/pull/3481)) +* Metrics instrumentation to correctly populate `http.flavor` tag. + (1.1 instead of HTTP/1.1 etc.) + ([#3379](https://github.com/open-telemetry/opentelemetry-dotnet/pull/3379)) +* Tracing instrumentation to populate `http.flavor` tag. + ([#3372](https://github.com/open-telemetry/opentelemetry-dotnet/pull/3372)) +* Tracing instrumentation to populate `http.scheme` tag. + ([#3392](https://github.com/open-telemetry/opentelemetry-dotnet/pull/3392)) + +## 1.0.0-rc9.4 + +Released 2022-Jun-03 + +* Added additional metric dimensions. + ([#3247](https://github.com/open-telemetry/opentelemetry-dotnet/pull/3247)) +* Removes net5.0 target as .NET 5.0 is going out + of support. The package keeps netstandard2.1 target, so it + can still be used with .NET5.0 apps. + ([#3147](https://github.com/open-telemetry/opentelemetry-dotnet/issues/3147)) + +## 1.0.0-rc9.3 + +Released 2022-Apr-15 + +## 1.0.0-rc9.2 + +Released 2022-Apr-12 + +## 1.0.0-rc9.1 + +Released 2022-Mar-30 + +* Fix: Http server span status is now unset for `400`-`499`. + ([#2904](https://github.com/open-telemetry/opentelemetry-dotnet/pull/2904)) +* Fix: drop direct reference of the `Microsoft.AspNetCore.Http.Features` from + net5 & net6 targets (already part of the FrameworkReference since the net5). + ([#2860](https://github.com/open-telemetry/opentelemetry-dotnet/pull/2860)) +* Reduce allocations calculating the http.url tag. + ([#2947](https://github.com/open-telemetry/opentelemetry-dotnet/pull/2947)) + +## 1.0.0-rc10 (broken. use 1.0.0-rc9.1 and newer) + +Released 2022-Mar-04 + +## 1.0.0-rc9 + +Released 2022-Feb-02 + +## 1.0.0-rc8 + +Released 2021-Oct-08 + +* Replaced `http.path` tag on activity with `http.target`. + ([#2266](https://github.com/open-telemetry/opentelemetry-dotnet/pull/2266)) + +## 1.0.0-rc7 + +Released 2021-Jul-12 + +## 1.0.0-rc6 + +Released 2021-Jun-25 + +## 1.0.0-rc5 + +Released 2021-Jun-09 + +* Fixes bug + [#1740](https://github.com/open-telemetry/opentelemetry-dotnet/issues/1740): + Instrumentation.AspNetCore for gRPC services omits ALL rpc.* attributes under + certain conditions + ([#1879](https://github.com/open-telemetry/opentelemetry-dotnet/pull/1879)) + +## 1.0.0-rc4 + +Released 2021-Apr-23 + +* When using OpenTelemetry.Extensions.Hosting you can now bind + `AspNetCoreInstrumentationOptions` from DI. + ([#1997](https://github.com/open-telemetry/opentelemetry-dotnet/pull/1997)) + +## 1.0.0-rc3 + +Released 2021-Mar-19 + +* Leverages added AddLegacySource API from OpenTelemetry SDK to trigger Samplers + and ActivityProcessors. Samplers, ActivityProcessor.OnStart will now get the + Activity before any enrichment done by the instrumentation. + ([#1836](https://github.com/open-telemetry/opentelemetry-dotnet/pull/1836)) +* Performance optimization by leveraging sampling decision and short circuiting + activity enrichment. `Filter` and `Enrich` are now only called if + `activity.IsAllDataRequested` is `true` + ([#1899](https://github.com/open-telemetry/opentelemetry-dotnet/pull/1899)) + +## 1.0.0-rc2 + +Released 2021-Jan-29 + +## 1.0.0-rc1.1 + +Released 2020-Nov-17 + +* AspNetCoreInstrumentation sets ActivitySource to activities created outside + ActivitySource. + ([#1515](https://github.com/open-telemetry/opentelemetry-dotnet/pull/1515/)) +* For gRPC invocations, leading forward slash is trimmed from span name in order + to conform to the specification. + ([#1551](https://github.com/open-telemetry/opentelemetry-dotnet/pull/1551)) + +## 0.8.0-beta.1 + +Released 2020-Nov-5 + +* Record `Exception` in AspNetCore instrumentation based on `RecordException` in + `AspNetCoreInstrumentationOptions` + ([#1408](https://github.com/open-telemetry/opentelemetry-dotnet/issues/1408)) +* Added configuration option `EnableGrpcAspNetCoreSupport` to enable or disable + support for adding OpenTelemetry RPC attributes when using + [Grpc.AspNetCore](https://www.nuget.org/packages/Grpc.AspNetCore/). This + option is enabled by default. + ([#1423](https://github.com/open-telemetry/opentelemetry-dotnet/pull/1423)) +* Renamed TextMapPropagator to TraceContextPropagator, CompositePropagator to + CompositeTextMapPropagator. IPropagator is renamed to TextMapPropagator and + changed from interface to abstract class. + ([#1427](https://github.com/open-telemetry/opentelemetry-dotnet/pull/1427)) +* Propagators.DefaultTextMapPropagator will be used as the default Propagator + ([#1427](https://github.com/open-telemetry/opentelemetry-dotnet/pull/1428)) +* Removed Propagator from Instrumentation Options. Instrumentation now always + respect the Propagator.DefaultTextMapPropagator. + ([#1448](https://github.com/open-telemetry/opentelemetry-dotnet/pull/1448)) + +## 0.7.0-beta.1 + +Released 2020-Oct-16 + +* Instrumentation no longer store raw objects like `HttpRequest` in + Activity.CustomProperty. To enrich activity, use the Enrich action on the + instrumentation. + ([#1261](https://github.com/open-telemetry/opentelemetry-dotnet/pull/1261)) +* Span Status is populated as per new spec + ([#1313](https://github.com/open-telemetry/opentelemetry-dotnet/pull/1313)) + +## 0.6.0-beta.1 + +Released 2020-Sep-15 + +* For gRPC invocations, the `grpc.method` and `grpc.status_code` attributes + added by the library are removed from the span. The information from these + attributes is contained in other attributes that follow the conventions of + OpenTelemetry. + ([#1260](https://github.com/open-telemetry/opentelemetry-dotnet/pull/1260)) + +## 0.5.0-beta.2 + +Released 2020-08-28 + +* Added Filter public API on AspNetCoreInstrumentationOptions to allow filtering + of instrumentation based on HttpContext. + +* Asp.Net Core Instrumentation automatically populates HttpRequest, HttpResponse + in Activity custom property + +* Changed the default propagation to support W3C Baggage + ([#1048](https://github.com/open-telemetry/opentelemetry-dotnet/pull/1048)) + * The default ITextFormat is now `CompositePropagator(TraceContextFormat, + BaggageFormat)`. Baggage sent via the [W3C + Baggage](https://github.com/w3c/baggage/blob/master/baggage/HTTP_HEADER_FORMAT.md) + header will now be parsed and set on incoming Http spans. +* Introduced support for Grpc.AspNetCore (#803). + * Attributes are added to gRPC invocations: `rpc.system`, `rpc.service`, + `rpc.method`. These attributes are added to an existing span generated by + the instrumentation. This is unlike the instrumentation for client-side gRPC + calls where one span is created for the gRPC call and a separate span is + created for the underlying HTTP call in the event both gRPC and HTTP + instrumentation are enabled. +* Renamed `ITextPropagator` to `IPropagator` + ([#1190](https://github.com/open-telemetry/opentelemetry-dotnet/pull/1190)) + +## 0.4.0-beta.2 + +Released 2020-07-24 + +* First beta release + +## 0.3.0-beta + +Released 2020-07-23 + +* Initial release diff --git a/src/OpenTelemetry.Instrumentation.AspNetCore/Implementation/AspNetCoreInstrumentationEventSource.cs b/src/OpenTelemetry.Instrumentation.AspNetCore/Implementation/AspNetCoreInstrumentationEventSource.cs new file mode 100644 index 0000000000..cafd0141d9 --- /dev/null +++ b/src/OpenTelemetry.Instrumentation.AspNetCore/Implementation/AspNetCoreInstrumentationEventSource.cs @@ -0,0 +1,94 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#if NET6_0_OR_GREATER +using System.Diagnostics.CodeAnalysis; +#endif +using System.Diagnostics.Tracing; +using Microsoft.Extensions.Configuration; +using OpenTelemetry.Internal; + +namespace OpenTelemetry.Instrumentation.AspNetCore.Implementation; + +/// +/// EventSource events emitted from the project. +/// +[EventSource(Name = "OpenTelemetry-Instrumentation-AspNetCore")] +internal sealed class AspNetCoreInstrumentationEventSource : EventSource, IConfigurationExtensionsLogger +{ + public static AspNetCoreInstrumentationEventSource Log = new(); + + [NonEvent] + public void RequestFilterException(string handlerName, string eventName, string operationName, Exception ex) + { + if (this.IsEnabled(EventLevel.Error, EventKeywords.All)) + { + this.RequestFilterException(handlerName, eventName, operationName, ex.ToInvariantString()); + } + } + + [NonEvent] + public void EnrichmentException(string handlerName, string eventName, string operationName, Exception ex) + { + if (this.IsEnabled(EventLevel.Error, EventKeywords.All)) + { + this.EnrichmentException(handlerName, eventName, operationName, ex.ToInvariantString()); + } + } + + [NonEvent] + public void UnknownErrorProcessingEvent(string handlerName, string eventName, Exception ex) + { + if (this.IsEnabled(EventLevel.Error, EventKeywords.All)) + { + this.UnknownErrorProcessingEvent(handlerName, eventName, ex.ToInvariantString()); + } + } + + [Event(1, Message = "Payload is NULL, span will not be recorded. HandlerName: '{0}', EventName: '{1}', OperationName: '{2}'.", Level = EventLevel.Warning)] + public void NullPayload(string handlerName, string eventName, string operationName) + { + this.WriteEvent(1, handlerName, eventName, operationName); + } + + [Event(2, Message = "Request is filtered out. HandlerName: '{0}', EventName: '{1}', OperationName: '{2}'.", Level = EventLevel.Verbose)] + public void RequestIsFilteredOut(string handlerName, string eventName, string operationName) + { + this.WriteEvent(2, handlerName, eventName, operationName); + } + +#if NET6_0_OR_GREATER + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:RequiresUnreferencedCode", Justification = "Parameters to this method are primitive and are trimmer safe.")] +#endif + [Event(3, Message = "Filter threw exception, request will not be collected. HandlerName: '{0}', EventName: '{1}', OperationName: '{2}', Exception: {3}.", Level = EventLevel.Error)] + public void RequestFilterException(string handlerName, string eventName, string operationName, string exception) + { + this.WriteEvent(3, handlerName, eventName, operationName, exception); + } + +#if NET6_0_OR_GREATER + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:RequiresUnreferencedCode", Justification = "Parameters to this method are primitive and are trimmer safe.")] +#endif + [Event(4, Message = "Enrich threw exception. HandlerName: '{0}', EventName: '{1}', OperationName: '{2}', Exception: {3}.", Level = EventLevel.Warning)] + public void EnrichmentException(string handlerName, string eventName, string operationName, string exception) + { + this.WriteEvent(4, handlerName, eventName, operationName, exception); + } + + [Event(5, Message = "Unknown error processing event '{1}' from handler '{0}', Exception: {2}", Level = EventLevel.Error)] + public void UnknownErrorProcessingEvent(string handlerName, string eventName, string ex) + { + this.WriteEvent(5, handlerName, eventName, ex); + } + + [Event(6, Message = "Configuration key '{0}' has an invalid value: '{1}'", Level = EventLevel.Warning)] + public void InvalidConfigurationValue(string key, string value) + { + this.WriteEvent(6, key, value); + } + + void IConfigurationExtensionsLogger.LogInvalidConfigurationValue(string key, string value) + { + this.InvalidConfigurationValue(key, value); + } +} diff --git a/src/OpenTelemetry.Instrumentation.AspNetCore/Implementation/HttpInListener.cs b/src/OpenTelemetry.Instrumentation.AspNetCore/Implementation/HttpInListener.cs new file mode 100644 index 0000000000..7b5942a108 --- /dev/null +++ b/src/OpenTelemetry.Instrumentation.AspNetCore/Implementation/HttpInListener.cs @@ -0,0 +1,397 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using System.Diagnostics; +#if NET6_0_OR_GREATER +using System.Diagnostics.CodeAnalysis; +#endif +using System.Reflection; +using System.Runtime.CompilerServices; +using Microsoft.AspNetCore.Http; +#if !NETSTANDARD +using Microsoft.AspNetCore.Diagnostics; +using Microsoft.AspNetCore.Routing; +#endif +using OpenTelemetry.Context.Propagation; +using OpenTelemetry.Instrumentation.GrpcNetClient; +using OpenTelemetry.Internal; +using OpenTelemetry.Trace; + +namespace OpenTelemetry.Instrumentation.AspNetCore.Implementation; + +internal class HttpInListener : ListenerHandler +{ + internal const string ActivityOperationName = "Microsoft.AspNetCore.Hosting.HttpRequestIn"; + internal const string OnStartEvent = "Microsoft.AspNetCore.Hosting.HttpRequestIn.Start"; + internal const string OnStopEvent = "Microsoft.AspNetCore.Hosting.HttpRequestIn.Stop"; + internal const string OnUnhandledHostingExceptionEvent = "Microsoft.AspNetCore.Hosting.UnhandledException"; + internal const string OnUnHandledDiagnosticsExceptionEvent = "Microsoft.AspNetCore.Diagnostics.UnhandledException"; + + // https://github.com/dotnet/aspnetcore/blob/8d6554e655b64da75b71e0e20d6db54a3ba8d2fb/src/Hosting/Hosting/src/GenericHost/GenericWebHostBuilder.cs#L85 + internal static readonly string AspNetCoreActivitySourceName = "Microsoft.AspNetCore"; + + internal static readonly AssemblyName AssemblyName = typeof(HttpInListener).Assembly.GetName(); + internal static readonly string ActivitySourceName = AssemblyName.Name; + internal static readonly Version Version = AssemblyName.Version; + internal static readonly ActivitySource ActivitySource = new(ActivitySourceName, Version.ToString()); + internal static readonly bool Net7OrGreater = Environment.Version.Major >= 7; + + private const string DiagnosticSourceName = "Microsoft.AspNetCore"; + + private static readonly Func> HttpRequestHeaderValuesGetter = (request, name) => + { + if (request.Headers.TryGetValue(name, out var value)) + { + // This causes allocation as the `StringValues` struct has to be casted to an `IEnumerable` object. + return value; + } + + return Enumerable.Empty(); + }; + + private static readonly PropertyFetcher ExceptionPropertyFetcher = new("Exception"); + + private readonly AspNetCoreTraceInstrumentationOptions options; + + public HttpInListener(AspNetCoreTraceInstrumentationOptions options) + : base(DiagnosticSourceName) + { + Guard.ThrowIfNull(options); + + this.options = options; + } + + public override void OnEventWritten(string name, object payload) + { + switch (name) + { + case OnStartEvent: + { + this.OnStartActivity(Activity.Current, payload); + } + + break; + case OnStopEvent: + { + this.OnStopActivity(Activity.Current, payload); + } + + break; + case OnUnhandledHostingExceptionEvent: + case OnUnHandledDiagnosticsExceptionEvent: + { + this.OnException(Activity.Current, payload); + } + + break; + } + } + + public void OnStartActivity(Activity activity, object payload) + { + // The overall flow of what AspNetCore library does is as below: + // Activity.Start() + // DiagnosticSource.WriteEvent("Start", payload) + // DiagnosticSource.WriteEvent("Stop", payload) + // Activity.Stop() + + // This method is in the WriteEvent("Start", payload) path. + // By this time, samplers have already run and + // activity.IsAllDataRequested populated accordingly. + + var context = payload as HttpContext; + if (context == null) + { + AspNetCoreInstrumentationEventSource.Log.NullPayload(nameof(HttpInListener), nameof(this.OnStartActivity), activity.OperationName); + return; + } + + // Ensure context extraction irrespective of sampling decision + var request = context.Request; + var textMapPropagator = Propagators.DefaultTextMapPropagator; + if (textMapPropagator is not TraceContextPropagator) + { + var ctx = textMapPropagator.Extract(default, request, HttpRequestHeaderValuesGetter); + if (ctx.ActivityContext.IsValid() + && !((ctx.ActivityContext.TraceId == activity.TraceId) + && (ctx.ActivityContext.SpanId == activity.ParentSpanId) + && (ctx.ActivityContext.TraceState == activity.TraceStateString))) + { + // Create a new activity with its parent set from the extracted context. + // This makes the new activity as a "sibling" of the activity created by + // Asp.Net Core. + Activity newOne; + if (Net7OrGreater) + { + // For NET7.0 onwards activity is created using ActivitySource so, + // we will use the source of the activity to create the new one. + newOne = activity.Source.CreateActivity(ActivityOperationName, ActivityKind.Server, ctx.ActivityContext); + } + else + { + newOne = new Activity(ActivityOperationName); + newOne.SetParentId(ctx.ActivityContext.TraceId, ctx.ActivityContext.SpanId, ctx.ActivityContext.TraceFlags); + } + + newOne.TraceStateString = ctx.ActivityContext.TraceState; + + newOne.SetTag("IsCreatedByInstrumentation", bool.TrueString); + + // Starting the new activity make it the Activity.Current one. + newOne.Start(); + + // Set IsAllDataRequested to false for the activity created by the framework to only export the sibling activity and not the framework activity + activity.IsAllDataRequested = false; + activity = newOne; + } + + Baggage.Current = ctx.Baggage; + } + + // enrich Activity from payload only if sampling decision + // is favorable. + if (activity.IsAllDataRequested) + { + try + { + if (this.options.Filter?.Invoke(context) == false) + { + AspNetCoreInstrumentationEventSource.Log.RequestIsFilteredOut(nameof(HttpInListener), nameof(this.OnStartActivity), activity.OperationName); + activity.IsAllDataRequested = false; + activity.ActivityTraceFlags &= ~ActivityTraceFlags.Recorded; + return; + } + } + catch (Exception ex) + { + AspNetCoreInstrumentationEventSource.Log.RequestFilterException(nameof(HttpInListener), nameof(this.OnStartActivity), activity.OperationName, ex); + activity.IsAllDataRequested = false; + activity.ActivityTraceFlags &= ~ActivityTraceFlags.Recorded; + return; + } + + if (!Net7OrGreater) + { + ActivityInstrumentationHelper.SetActivitySourceProperty(activity, ActivitySource); + ActivityInstrumentationHelper.SetKindProperty(activity, ActivityKind.Server); + } + + var path = (request.PathBase.HasValue || request.Path.HasValue) ? (request.PathBase + request.Path).ToString() : "/"; + RequestMethodHelper.SetActivityDisplayName(activity, request.Method); + + // see the spec https://github.com/open-telemetry/semantic-conventions/blob/v1.23.0/docs/http/http-spans.md + + if (request.Host.HasValue) + { + activity.SetTag(SemanticConventions.AttributeServerAddress, request.Host.Host); + + if (request.Host.Port.HasValue) + { + activity.SetTag(SemanticConventions.AttributeServerPort, request.Host.Port.Value); + } + } + + if (request.QueryString.HasValue) + { + if (this.options.DisableUrlQueryRedaction) + { + activity.SetTag(SemanticConventions.AttributeUrlQuery, request.QueryString.Value); + } + else + { + activity.SetTag(SemanticConventions.AttributeUrlQuery, RedactionHelper.GetRedactedQueryString(request.QueryString.Value)); + } + } + + RequestMethodHelper.SetHttpMethodTag(activity, request.Method); + + activity.SetTag(SemanticConventions.AttributeUrlScheme, request.Scheme); + activity.SetTag(SemanticConventions.AttributeUrlPath, path); + activity.SetTag(SemanticConventions.AttributeNetworkProtocolVersion, HttpTagHelper.GetFlavorTagValueFromProtocol(request.Protocol)); + + if (request.Headers.TryGetValue("User-Agent", out var values)) + { + var userAgent = values.Count > 0 ? values[0] : null; + if (!string.IsNullOrEmpty(userAgent)) + { + activity.SetTag(SemanticConventions.AttributeUserAgentOriginal, userAgent); + } + } + + try + { + this.options.EnrichWithHttpRequest?.Invoke(activity, request); + } + catch (Exception ex) + { + AspNetCoreInstrumentationEventSource.Log.EnrichmentException(nameof(HttpInListener), nameof(this.OnStartActivity), activity.OperationName, ex); + } + } + } + + public void OnStopActivity(Activity activity, object payload) + { + if (activity.IsAllDataRequested) + { + HttpContext context = payload as HttpContext; + if (context == null) + { + AspNetCoreInstrumentationEventSource.Log.NullPayload(nameof(HttpInListener), nameof(this.OnStopActivity), activity.OperationName); + return; + } + + var response = context.Response; + +#if !NETSTANDARD + var routePattern = (context.Features.Get()?.Endpoint as RouteEndpoint ?? + context.GetEndpoint() as RouteEndpoint)?.RoutePattern.RawText; + if (!string.IsNullOrEmpty(routePattern)) + { + RequestMethodHelper.SetActivityDisplayName(activity, context.Request.Method, routePattern); + activity.SetTag(SemanticConventions.AttributeHttpRoute, routePattern); + } +#endif + + activity.SetTag(SemanticConventions.AttributeHttpResponseStatusCode, TelemetryHelper.GetBoxedStatusCode(response.StatusCode)); + + if (this.options.EnableGrpcAspNetCoreSupport && TryGetGrpcMethod(activity, out var grpcMethod)) + { + AddGrpcAttributes(activity, grpcMethod, context); + } + + if (activity.Status == ActivityStatusCode.Unset) + { + activity.SetStatus(SpanHelper.ResolveSpanStatusForHttpStatusCode(activity.Kind, response.StatusCode)); + } + + try + { + this.options.EnrichWithHttpResponse?.Invoke(activity, response); + } + catch (Exception ex) + { + AspNetCoreInstrumentationEventSource.Log.EnrichmentException(nameof(HttpInListener), nameof(this.OnStopActivity), activity.OperationName, ex); + } + } + + object tagValue; + if (Net7OrGreater) + { + tagValue = activity.GetTagValue("IsCreatedByInstrumentation"); + } + else + { + _ = activity.TryCheckFirstTag("IsCreatedByInstrumentation", out tagValue); + } + + if (ReferenceEquals(tagValue, bool.TrueString)) + { + // If instrumentation started a new Activity, it must + // be stopped here. + activity.SetTag("IsCreatedByInstrumentation", null); + activity.Stop(); + + // After the activity.Stop() code, Activity.Current becomes null. + // If Asp.Net Core uses Activity.Current?.Stop() - it'll not stop the activity + // it created. + // Currently Asp.Net core does not use Activity.Current, instead it stores a + // reference to its activity, and calls .Stop on it. + + // TODO: Should we still restore Activity.Current here? + // If yes, then we need to store the asp.net core activity inside + // the one created by the instrumentation. + // And retrieve it here, and set it to Current. + } + } + + public void OnException(Activity activity, object payload) + { + if (activity.IsAllDataRequested) + { + // We need to use reflection here as the payload type is not a defined public type. + if (!TryFetchException(payload, out Exception exc)) + { + AspNetCoreInstrumentationEventSource.Log.NullPayload(nameof(HttpInListener), nameof(this.OnException), activity.OperationName); + return; + } + + activity.SetTag(SemanticConventions.AttributeErrorType, exc.GetType().FullName); + + if (this.options.RecordException) + { + activity.RecordException(exc); + } + + activity.SetStatus(ActivityStatusCode.Error); + + try + { + this.options.EnrichWithException?.Invoke(activity, exc); + } + catch (Exception ex) + { + AspNetCoreInstrumentationEventSource.Log.EnrichmentException(nameof(HttpInListener), nameof(this.OnException), activity.OperationName, ex); + } + } + + // See https://github.com/dotnet/aspnetcore/blob/690d78279e940d267669f825aa6627b0d731f64c/src/Hosting/Hosting/src/Internal/HostingApplicationDiagnostics.cs#L252 + // and https://github.com/dotnet/aspnetcore/blob/690d78279e940d267669f825aa6627b0d731f64c/src/Middleware/Diagnostics/src/DeveloperExceptionPage/DeveloperExceptionPageMiddlewareImpl.cs#L174 + // this makes sure that top-level properties on the payload object are always preserved. +#if NET6_0_OR_GREATER + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "The event source guarantees that top level properties are preserved")] +#endif + static bool TryFetchException(object payload, out Exception exc) + => ExceptionPropertyFetcher.TryFetch(payload, out exc) && exc != null; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool TryGetGrpcMethod(Activity activity, out string grpcMethod) + { + grpcMethod = GrpcTagHelper.GetGrpcMethodFromActivity(activity); + return !string.IsNullOrEmpty(grpcMethod); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void AddGrpcAttributes(Activity activity, string grpcMethod, HttpContext context) + { + // The RPC semantic conventions indicate the span name + // should not have a leading forward slash. + // https://github.com/open-telemetry/semantic-conventions/blob/main/docs/rpc/rpc-spans.md#span-name + activity.DisplayName = grpcMethod.TrimStart('/'); + + activity.SetTag(SemanticConventions.AttributeRpcSystem, GrpcTagHelper.RpcSystemGrpc); + + // see the spec https://github.com/open-telemetry/semantic-conventions/blob/v1.23.0/docs/rpc/rpc-spans.md + + if (context.Connection.RemoteIpAddress != null) + { + activity.SetTag(SemanticConventions.AttributeClientAddress, context.Connection.RemoteIpAddress.ToString()); + } + + activity.SetTag(SemanticConventions.AttributeClientPort, context.Connection.RemotePort); + + bool validConversion = GrpcTagHelper.TryGetGrpcStatusCodeFromActivity(activity, out int status); + if (validConversion) + { + activity.SetStatus(GrpcTagHelper.ResolveSpanStatusForGrpcStatusCode(status)); + } + + if (GrpcTagHelper.TryParseRpcServiceAndRpcMethod(grpcMethod, out var rpcService, out var rpcMethod)) + { + activity.SetTag(SemanticConventions.AttributeRpcService, rpcService); + activity.SetTag(SemanticConventions.AttributeRpcMethod, rpcMethod); + + // Remove the grpc.method tag added by the gRPC .NET library + activity.SetTag(GrpcTagHelper.GrpcMethodTagName, null); + + // Remove the grpc.status_code tag added by the gRPC .NET library + activity.SetTag(GrpcTagHelper.GrpcStatusCodeTagName, null); + + if (validConversion) + { + // setting rpc.grpc.status_code + activity.SetTag(SemanticConventions.AttributeRpcGrpcStatusCode, status); + } + } + } +} diff --git a/src/OpenTelemetry.Instrumentation.AspNetCore/Implementation/HttpInMetricsListener.cs b/src/OpenTelemetry.Instrumentation.AspNetCore/Implementation/HttpInMetricsListener.cs new file mode 100644 index 0000000000..e41cd5dc25 --- /dev/null +++ b/src/OpenTelemetry.Instrumentation.AspNetCore/Implementation/HttpInMetricsListener.cs @@ -0,0 +1,128 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using System.Diagnostics; +using System.Diagnostics.Metrics; +using System.Reflection; +using Microsoft.AspNetCore.Http; +using OpenTelemetry.Internal; + +#if NET6_0_OR_GREATER +using System.Diagnostics.CodeAnalysis; +using Microsoft.AspNetCore.Diagnostics; +using Microsoft.AspNetCore.Routing; +#endif +using OpenTelemetry.Trace; + +namespace OpenTelemetry.Instrumentation.AspNetCore.Implementation; + +internal sealed class HttpInMetricsListener : ListenerHandler +{ + internal const string HttpServerRequestDurationMetricName = "http.server.request.duration"; + + internal const string OnUnhandledHostingExceptionEvent = "Microsoft.AspNetCore.Hosting.UnhandledException"; + internal const string OnUnhandledDiagnosticsExceptionEvent = "Microsoft.AspNetCore.Diagnostics.UnhandledException"; + + internal static readonly AssemblyName AssemblyName = typeof(HttpInListener).Assembly.GetName(); + internal static readonly string InstrumentationName = AssemblyName.Name; + internal static readonly string InstrumentationVersion = AssemblyName.Version.ToString(); + internal static readonly Meter Meter = new(InstrumentationName, InstrumentationVersion); + + private const string OnStopEvent = "Microsoft.AspNetCore.Hosting.HttpRequestIn.Stop"; + + private static readonly PropertyFetcher ExceptionPropertyFetcher = new("Exception"); + private static readonly PropertyFetcher HttpContextPropertyFetcher = new("HttpContext"); + private static readonly object ErrorTypeHttpContextItemsKey = new(); + + private static readonly Histogram HttpServerRequestDuration = Meter.CreateHistogram(HttpServerRequestDurationMetricName, "s", "Duration of HTTP server requests."); + + internal HttpInMetricsListener(string name) + : base(name) + { + } + + public static void OnExceptionEventWritten(string name, object payload) + { + // We need to use reflection here as the payload type is not a defined public type. + if (!TryFetchException(payload, out Exception exc) || !TryFetchHttpContext(payload, out HttpContext ctx)) + { + AspNetCoreInstrumentationEventSource.Log.NullPayload(nameof(HttpInMetricsListener), nameof(OnExceptionEventWritten), HttpServerRequestDurationMetricName); + return; + } + + ctx.Items.Add(ErrorTypeHttpContextItemsKey, exc.GetType().FullName); + + // See https://github.com/dotnet/aspnetcore/blob/690d78279e940d267669f825aa6627b0d731f64c/src/Hosting/Hosting/src/Internal/HostingApplicationDiagnostics.cs#L252 + // and https://github.com/dotnet/aspnetcore/blob/690d78279e940d267669f825aa6627b0d731f64c/src/Middleware/Diagnostics/src/DeveloperExceptionPage/DeveloperExceptionPageMiddlewareImpl.cs#L174 + // this makes sure that top-level properties on the payload object are always preserved. +#if NET6_0_OR_GREATER + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "The ASP.NET Core framework guarantees that top level properties are preserved")] +#endif + static bool TryFetchException(object payload, out Exception exc) + => ExceptionPropertyFetcher.TryFetch(payload, out exc) && exc != null; +#if NET6_0_OR_GREATER + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "The ASP.NET Core framework guarantees that top level properties are preserved")] +#endif + static bool TryFetchHttpContext(object payload, out HttpContext ctx) + => HttpContextPropertyFetcher.TryFetch(payload, out ctx) && ctx != null; + } + + public static void OnStopEventWritten(string name, object payload) + { + var context = payload as HttpContext; + if (context == null) + { + AspNetCoreInstrumentationEventSource.Log.NullPayload(nameof(HttpInMetricsListener), nameof(OnStopEventWritten), HttpServerRequestDurationMetricName); + return; + } + + TagList tags = default; + + // see the spec https://github.com/open-telemetry/semantic-conventions/blob/v1.21.0/docs/http/http-spans.md + tags.Add(new KeyValuePair(SemanticConventions.AttributeNetworkProtocolVersion, HttpTagHelper.GetFlavorTagValueFromProtocol(context.Request.Protocol))); + tags.Add(new KeyValuePair(SemanticConventions.AttributeUrlScheme, context.Request.Scheme)); + tags.Add(new KeyValuePair(SemanticConventions.AttributeHttpResponseStatusCode, TelemetryHelper.GetBoxedStatusCode(context.Response.StatusCode))); + + var httpMethod = RequestMethodHelper.GetNormalizedHttpMethod(context.Request.Method); + tags.Add(new KeyValuePair(SemanticConventions.AttributeHttpRequestMethod, httpMethod)); + +#if NET6_0_OR_GREATER + // Check the exception handler feature first in case the endpoint was overwritten + var route = (context.Features.Get()?.Endpoint as RouteEndpoint ?? + context.GetEndpoint() as RouteEndpoint)?.RoutePattern.RawText; + if (!string.IsNullOrEmpty(route)) + { + tags.Add(new KeyValuePair(SemanticConventions.AttributeHttpRoute, route)); + } +#endif + if (context.Items.TryGetValue(ErrorTypeHttpContextItemsKey, out var errorType)) + { + tags.Add(new KeyValuePair(SemanticConventions.AttributeErrorType, errorType)); + } + + // We are relying here on ASP.NET Core to set duration before writing the stop event. + // https://github.com/dotnet/aspnetcore/blob/d6fa351048617ae1c8b47493ba1abbe94c3a24cf/src/Hosting/Hosting/src/Internal/HostingApplicationDiagnostics.cs#L449 + // TODO: Follow up with .NET team if we can continue to rely on this behavior. + HttpServerRequestDuration.Record(Activity.Current.Duration.TotalSeconds, tags); + } + + public override void OnEventWritten(string name, object payload) + { + switch (name) + { + case OnUnhandledDiagnosticsExceptionEvent: + case OnUnhandledHostingExceptionEvent: + { + OnExceptionEventWritten(name, payload); + } + + break; + case OnStopEvent: + { + OnStopEventWritten(name, payload); + } + + break; + } + } +} diff --git a/src/OpenTelemetry.Instrumentation.AspNetCore/Implementation/HttpTagHelper.cs b/src/OpenTelemetry.Instrumentation.AspNetCore/Implementation/HttpTagHelper.cs new file mode 100644 index 0000000000..90f37eba3c --- /dev/null +++ b/src/OpenTelemetry.Instrumentation.AspNetCore/Implementation/HttpTagHelper.cs @@ -0,0 +1,33 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +namespace OpenTelemetry.Instrumentation.AspNetCore.Implementation; + +/// +/// A collection of helper methods to be used when building Http activities. +/// +internal static class HttpTagHelper +{ + /// + /// Gets the OpenTelemetry standard version tag value for a span based on its protocol/>. + /// + /// . + /// Span flavor value. + public static string GetFlavorTagValueFromProtocol(string protocol) + { + switch (protocol) + { + case "HTTP/2": + return "2"; + + case "HTTP/3": + return "3"; + + case "HTTP/1.1": + return "1.1"; + + default: + return protocol; + } + } +} diff --git a/src/OpenTelemetry.Instrumentation.AspNetCore/Implementation/TelemetryHelper.cs b/src/OpenTelemetry.Instrumentation.AspNetCore/Implementation/TelemetryHelper.cs new file mode 100644 index 0000000000..6f2e1fae8e --- /dev/null +++ b/src/OpenTelemetry.Instrumentation.AspNetCore/Implementation/TelemetryHelper.cs @@ -0,0 +1,28 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +namespace OpenTelemetry.Instrumentation.AspNetCore.Implementation; + +internal static class TelemetryHelper +{ + public static readonly object[] BoxedStatusCodes; + + static TelemetryHelper() + { + BoxedStatusCodes = new object[500]; + for (int i = 0, c = 100; i < BoxedStatusCodes.Length; i++, c++) + { + BoxedStatusCodes[i] = c; + } + } + + public static object GetBoxedStatusCode(int statusCode) + { + if (statusCode >= 100 && statusCode < 600) + { + return BoxedStatusCodes[statusCode - 100]; + } + + return statusCode; + } +} diff --git a/src/OpenTelemetry.Instrumentation.AspNetCore/OpenTelemetry.Instrumentation.AspNetCore.csproj b/src/OpenTelemetry.Instrumentation.AspNetCore/OpenTelemetry.Instrumentation.AspNetCore.csproj new file mode 100644 index 0000000000..15aa86638d --- /dev/null +++ b/src/OpenTelemetry.Instrumentation.AspNetCore/OpenTelemetry.Instrumentation.AspNetCore.csproj @@ -0,0 +1,48 @@ + + + + $(TargetFrameworksForAspNetCoreInstrumentation) + ASP.NET Core instrumentation for OpenTelemetry .NET + $(PackageTags);distributed-tracing;AspNetCore + Instrumentation.AspNetCore- + true + 1.8.1 + + + disable + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/OpenTelemetry.Instrumentation.AspNetCore/README.md b/src/OpenTelemetry.Instrumentation.AspNetCore/README.md new file mode 100644 index 0000000000..f8ef36ef2d --- /dev/null +++ b/src/OpenTelemetry.Instrumentation.AspNetCore/README.md @@ -0,0 +1,331 @@ +# ASP.NET Core Instrumentation for OpenTelemetry .NET + +[![NuGet](https://img.shields.io/nuget/v/OpenTelemetry.Instrumentation.AspNetCore.svg)](https://www.nuget.org/packages/OpenTelemetry.Instrumentation.AspNetCore) +[![NuGet](https://img.shields.io/nuget/dt/OpenTelemetry.Instrumentation.AspNetCore.svg)](https://www.nuget.org/packages/OpenTelemetry.Instrumentation.AspNetCore) + +This is an [Instrumentation +Library](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/glossary.md#instrumentation-library), +which instruments [ASP.NET Core](https://docs.microsoft.com/aspnet/core) and +collect metrics and traces about incoming web requests. This instrumentation +also collects traces from incoming gRPC requests using +[Grpc.AspNetCore](https://www.nuget.org/packages/Grpc.AspNetCore). +Instrumentation support for gRPC server requests is supported via an +[experimental](#experimental-support-for-grpc-requests) feature flag. + +This component is based on the +[v1.23](https://github.com/open-telemetry/semantic-conventions/tree/v1.23.0/docs/http) +of http semantic conventions. For details on the default set of attributes that +are added, checkout [Traces](#traces) and [Metrics](#metrics) sections below. + +## Steps to enable OpenTelemetry.Instrumentation.AspNetCore + +### Step 1: Install Package + +Add a reference to the +[`OpenTelemetry.Instrumentation.AspNetCore`](https://www.nuget.org/packages/OpenTelemetry.Instrumentation.AspNetCore) +package. Also, add any other instrumentations & exporters you will need. + +```shell +dotnet add package OpenTelemetry.Instrumentation.AspNetCore +``` + +### Step 2: Enable ASP.NET Core Instrumentation at application startup + +ASP.NET Core instrumentation must be enabled at application startup. This is +typically done in the `ConfigureServices` of your `Startup` class. Both examples +below enables OpenTelemetry by calling `AddOpenTelemetry()` on `IServiceCollection`. + This extension method requires adding the package +[`OpenTelemetry.Extensions.Hosting`](../OpenTelemetry.Extensions.Hosting/README.md) +to the application. This ensures instrumentations are disposed when the host +is shutdown. + +#### Traces + +The following example demonstrates adding ASP.NET Core instrumentation with the +extension method `WithTracing()` on `OpenTelemetryBuilder`. +then extension method `AddAspNetCoreInstrumentation()` on `TracerProviderBuilder` +to the application. This example also sets up the Console Exporter, +which requires adding the package [`OpenTelemetry.Exporter.Console`](../OpenTelemetry.Exporter.Console/README.md) +to the application. + +```csharp +using Microsoft.Extensions.DependencyInjection; +using OpenTelemetry.Trace; + +public void ConfigureServices(IServiceCollection services) +{ + services.AddOpenTelemetry() + .WithTracing(builder => builder + .AddAspNetCoreInstrumentation() + .AddConsoleExporter()); +} +``` + +Following list of attributes are added by default on activity. See +[http-spans](https://github.com/open-telemetry/semantic-conventions/tree/v1.23.0/docs/http/http-spans.md) +for more details about each individual attribute: + +* `error.type` +* `http.request.method` +* `http.request.method_original` +* `http.response.status_code` +* `http.route` +* `network.protocol.version` +* `user_agent.original` +* `server.address` +* `server.port` +* `url.path` +* `url.query` - By default, the values in the query component are replaced with + the text `Redacted`. For example, `?key1=value1&key2=value2` becomes + `?key1=Redacted&key2=Redacted`. You can disable this redaction by setting the + environment variable + `OTEL_DOTNET_EXPERIMENTAL_ASPNETCORE_DISABLE_URL_QUERY_REDACTION` to `true`. +* `url.scheme` + +[Enrich Api](#enrich) can be used if any additional attributes are +required on activity. + +#### Metrics + +The following example demonstrates adding ASP.NET Core instrumentation with the +extension method `WithMetrics()` on `OpenTelemetryBuilder` +then extension method `AddAspNetCoreInstrumentation()` on `MeterProviderBuilder` +to the application. This example also sets up the Console Exporter, +which requires adding the package [`OpenTelemetry.Exporter.Console`](../OpenTelemetry.Exporter.Console/README.md) +to the application. + +```csharp +using Microsoft.Extensions.DependencyInjection; +using OpenTelemetry.Metrics; + +public void ConfigureServices(IServiceCollection services) +{ + services.AddOpenTelemetry() + .WithMetrics(builder => builder + .AddAspNetCoreInstrumentation() + .AddConsoleExporter()); +} +``` + +Following list of attributes are added by default on +`http.server.request.duration` metric. See +[http-metrics](https://github.com/open-telemetry/semantic-conventions/tree/v1.23.0/docs/http/http-metrics.md) +for more details about each individual attribute. `.NET8.0` and above supports +additional metrics, see [list of metrics produced](#list-of-metrics-produced) for +more details. + +* `error.type` +* `http.response.status_code` +* `http.request.method` +* `http.route` +* `network.protocol.version` +* `url.scheme` + +#### List of metrics produced + +When the application targets `.NET6.0` or `.NET7.0`, the instrumentation emits +the following metric: + +| Name | Details | +|-----------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------| +| `http.server.request.duration` | [Specification](https://github.com/open-telemetry/semantic-conventions/blob/release/v1.23.x/docs/http/http-metrics.md#metric-httpserverrequestduration) | + +Starting from `.NET8.0`, metrics instrumentation is natively implemented, and +the ASP.NET Core library has incorporated support for [built-in +metrics](https://learn.microsoft.com/dotnet/core/diagnostics/built-in-metrics-aspnetcore) +following the OpenTelemetry semantic conventions. The library includes additional +metrics beyond those defined in the +[specification](https://github.com/open-telemetry/semantic-conventions/blob/v1.23.0/docs/http/http-metrics.md), +covering additional scenarios for ASP.NET Core users. When the application +targets `.NET8.0` and newer versions, the instrumentation library automatically +enables all `built-in` metrics by default. + +Note that the `AddAspNetCoreInstrumentation()` extension simplifies the process +of enabling all built-in metrics via a single line of code. Alternatively, for +more granular control over emitted metrics, you can utilize the `AddMeter()` +extension on `MeterProviderBuilder` for meters listed in +[built-in-metrics-aspnetcore](https://learn.microsoft.com/dotnet/core/diagnostics/built-in-metrics-aspnetcore). +Using `AddMeter()` for metrics activation eliminates the need to take dependency +on the instrumentation library package and calling +`AddAspNetCoreInstrumentation()`. + +If you utilize `AddAspNetCoreInstrumentation()` and wish to exclude unnecessary +metrics, you can utilize +[Views](https://github.com/open-telemetry/opentelemetry-dotnet/tree/main/docs/metrics/customizing-the-sdk#drop-an-instrument) +to achieve this. + +> [!NOTE] +> There is no difference in features or emitted metrics when enabling metrics +using `AddMeter()` or `AddAspNetCoreInstrumentation()` on `.NET8.0` and newer +versions. + +> [!NOTE] +> The `http.server.request.duration` metric is emitted in `seconds` as per the +semantic convention. While the convention [recommends using custom histogram +buckets](https://github.com/open-telemetry/semantic-conventions/blob/release/v1.23.x/docs/http/http-metrics.md) +, this feature is not yet available via .NET Metrics API. A +[workaround](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4820) +has been included in OTel SDK starting version `1.6.0` which applies recommended +buckets by default for `http.server.request.duration`. This applies to all +targeted frameworks. + +## Advanced configuration + +### Tracing + +This instrumentation can be configured to change the default behavior by using +`AspNetCoreTraceInstrumentationOptions`, which allows adding [`Filter`](#filter), +[`Enrich`](#enrich) as explained below. + +// TODO: This section could be refined. +When used with +[`OpenTelemetry.Extensions.Hosting`](../OpenTelemetry.Extensions.Hosting/README.md), +all configurations to `AspNetCoreTraceInstrumentationOptions` can be done in the +`ConfigureServices` +method of you applications `Startup` class as shown below. + +```csharp +// Configure +services.Configure(options => +{ + options.Filter = (httpContext) => + { + // only collect telemetry about HTTP GET requests + return httpContext.Request.Method.Equals("GET"); + }; +}); + +services.AddOpenTelemetry() + .WithTracing(builder => builder + .AddAspNetCoreInstrumentation() + .AddConsoleExporter()); +``` + +#### Filter + +This instrumentation by default collects all the incoming http requests. It +allows filtering of requests by using the `Filter` function in +`AspNetCoreTraceInstrumentationOptions`. This defines the condition for allowable +requests. The Filter receives the `HttpContext` of the incoming +request, and does not collect telemetry about the request if the Filter +returns false or throws exception. + +The following code snippet shows how to use `Filter` to only allow GET +requests. + +```csharp +services.AddOpenTelemetry() + .WithTracing(builder => builder + .AddAspNetCoreInstrumentation((options) => options.Filter = httpContext => + { + // only collect telemetry about HTTP GET requests + return httpContext.Request.Method.Equals("GET"); + }) + .AddConsoleExporter()); +``` + +It is important to note that this `Filter` option is specific to this +instrumentation. OpenTelemetry has a concept of a +[Sampler](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/sdk.md#sampling), +and the `Filter` option does the filtering *after* the Sampler is invoked. + +#### Enrich + +This instrumentation library provides `EnrichWithHttpRequest`, +`EnrichWithHttpResponse` and `EnrichWithException` options that can be used to +enrich the activity with additional information from the raw `HttpRequest`, +`HttpResponse` and `Exception` objects respectively. These actions are called +only when `activity.IsAllDataRequested` is `true`. It contains the activity +itself (which can be enriched) and the actual raw object. + +The following code snippet shows how to enrich the activity using all 3 +different options. + +```csharp +services.AddOpenTelemetry() + .WithTracing(builder => builder + .AddAspNetCoreInstrumentation(o => + { + o.EnrichWithHttpRequest = (activity, httpRequest) => + { + activity.SetTag("requestProtocol", httpRequest.Protocol); + }; + o.EnrichWithHttpResponse = (activity, httpResponse) => + { + activity.SetTag("responseLength", httpResponse.ContentLength); + }; + o.EnrichWithException = (activity, exception) => + { + activity.SetTag("exceptionType", exception.GetType().ToString()); + }; + })); +``` + +[Processor](../../docs/trace/extending-the-sdk/README.md#processor), +is the general extensibility point to add additional properties to any activity. +The `Enrich` option is specific to this instrumentation, and is provided to +get access to `HttpRequest` and `HttpResponse`. + +#### RecordException + +This instrumentation automatically sets Activity Status to Error if an unhandled +exception is thrown. Additionally, `RecordException` feature may be turned on, +to store the exception to the Activity itself as ActivityEvent. + +## Activity duration and http.server.request.duration metric calculation + +`Activity.Duration` and `http.server.request.duration` values represents the +time used to handle an inbound HTTP request as measured at the hosting layer of +ASP.NET Core. The time measurement starts once the underlying web host has: + +* Sufficiently parsed the HTTP request headers on the inbound network stream to + identify the new request. +* Initialized the context data structures such as the + [HttpContext](https://learn.microsoft.com/dotnet/api/microsoft.aspnetcore.http.httpcontext). + +The time ends when: + +* The ASP.NET Core handler pipeline is finished executing. +* All response data has been sent. +* The context data structures for the request are being disposed. + +## Experimental support for gRPC requests + +gRPC instrumentation can be enabled by setting +`OTEL_DOTNET_EXPERIMENTAL_ASPNETCORE_ENABLE_GRPC_INSTRUMENTATION` flag to +`True`. The flag can be set as an environment variable or via IConfiguration as +shown below. + +```csharp +var appBuilder = WebApplication.CreateBuilder(args); + +appBuilder.Configuration.AddInMemoryCollection( + new Dictionary + { + ["OTEL_DOTNET_EXPERIMENTAL_ASPNETCORE_ENABLE_GRPC_INSTRUMENTATION"] = "true", + }); + +appBuilder.Services.AddOpenTelemetry() + .WithTracing(tracing => tracing + .AddAspNetCoreInstrumentation()); +``` + + Semantic conventions for RPC are still + [experimental](https://github.com/open-telemetry/semantic-conventions/tree/main/docs/rpc) + and hence the instrumentation only offers it as an experimental feature. + +## Troubleshooting + +This component uses an +[EventSource](https://docs.microsoft.com/dotnet/api/system.diagnostics.tracing.eventsource) +with the name "OpenTelemetry-Instrumentation-AspNetCore" for its internal +logging. Please refer to [SDK +troubleshooting](../OpenTelemetry/README.md#troubleshooting) for instructions on +seeing these internal logs. + +## References + +* [Introduction to ASP.NET + Core](https://docs.microsoft.com/aspnet/core/introduction-to-aspnet-core) +* [gRPC services using ASP.NET Core](https://docs.microsoft.com/aspnet/core/grpc/aspnetcore) +* [OpenTelemetry Project](https://opentelemetry.io/) diff --git a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/AttributesExtensions.cs b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/AttributesExtensions.cs new file mode 100644 index 0000000000..7ad43bdaea --- /dev/null +++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/AttributesExtensions.cs @@ -0,0 +1,12 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +namespace OpenTelemetry.Instrumentation.AspNetCore.Tests; + +internal static class AttributesExtensions +{ + public static object GetValue(this IEnumerable> attributes, string key) + { + return attributes.FirstOrDefault(kvp => kvp.Key == key).Value; + } +} diff --git a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/BasicTests.cs b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/BasicTests.cs new file mode 100644 index 0000000000..03f6f4c162 --- /dev/null +++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/BasicTests.cs @@ -0,0 +1,1273 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using System.Diagnostics; +using System.Text.Json; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using OpenTelemetry.Context.Propagation; +using OpenTelemetry.Instrumentation.AspNetCore.Implementation; +using OpenTelemetry.Tests; +using OpenTelemetry.Trace; +using TestApp.AspNetCore; +using TestApp.AspNetCore.Filters; +using Xunit; + +namespace OpenTelemetry.Instrumentation.AspNetCore.Tests; + +// See https://github.com/aspnet/Docs/tree/master/aspnetcore/test/integration-tests/samples/2.x/IntegrationTestsSample +public sealed class BasicTests + : IClassFixture>, IDisposable +{ + private readonly WebApplicationFactory factory; + private TracerProvider tracerProvider = null; + + public BasicTests(WebApplicationFactory factory) + { + this.factory = factory; + } + + [Fact] + public void AddAspNetCoreInstrumentation_BadArgs() + { + TracerProviderBuilder builder = null; + Assert.Throws(() => builder.AddAspNetCoreInstrumentation()); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task StatusIsUnsetOn200Response(bool disableLogging) + { + var exportedItems = new List(); + void ConfigureTestServices(IServiceCollection services) + { + this.tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddAspNetCoreInstrumentation() + .AddInMemoryExporter(exportedItems) + .Build(); + } + + // Arrange + using (var client = this.factory + .WithWebHostBuilder(builder => + { + builder.ConfigureTestServices(ConfigureTestServices); + if (disableLogging) + { + builder.ConfigureLogging(loggingBuilder => loggingBuilder.ClearProviders()); + } + }) + .CreateClient()) + { + // Act + using var response = await client.GetAsync("/api/values"); + + // Assert + response.EnsureSuccessStatusCode(); // Status Code 200-299 + + WaitForActivityExport(exportedItems, 1); + } + + Assert.Single(exportedItems); + var activity = exportedItems[0]; + + Assert.Equal(200, activity.GetTagValue(SemanticConventions.AttributeHttpResponseStatusCode)); + Assert.Equal(ActivityStatusCode.Unset, activity.Status); + ValidateAspNetCoreActivity(activity, "/api/values"); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task SuccessfulTemplateControllerCallGeneratesASpan(bool shouldEnrich) + { + var exportedItems = new List(); + void ConfigureTestServices(IServiceCollection services) + { + this.tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddAspNetCoreInstrumentation(options => + { + if (shouldEnrich) + { + options.EnrichWithHttpRequest = (activity, request) => { activity.SetTag("enrichedOnStart", "yes"); }; + options.EnrichWithHttpResponse = (activity, response) => { activity.SetTag("enrichedOnStop", "yes"); }; + } + }) + .AddInMemoryExporter(exportedItems) + .Build(); + } + + // Arrange + using (var client = this.factory + .WithWebHostBuilder(builder => + { + builder.ConfigureTestServices(ConfigureTestServices); + builder.ConfigureLogging(loggingBuilder => loggingBuilder.ClearProviders()); + }) + .CreateClient()) + { + // Act + using var response = await client.GetAsync("/api/values"); + + // Assert + response.EnsureSuccessStatusCode(); // Status Code 200-299 + + WaitForActivityExport(exportedItems, 1); + } + + Assert.Single(exportedItems); + var activity = exportedItems[0]; + + if (shouldEnrich) + { + Assert.NotEmpty(activity.Tags.Where(tag => tag.Key == "enrichedOnStart" && tag.Value == "yes")); + Assert.NotEmpty(activity.Tags.Where(tag => tag.Key == "enrichedOnStop" && tag.Value == "yes")); + } + + ValidateAspNetCoreActivity(activity, "/api/values"); + } + + [Fact] + public async Task SuccessfulTemplateControllerCallUsesParentContext() + { + var exportedItems = new List(); + var expectedTraceId = ActivityTraceId.CreateRandom(); + var expectedSpanId = ActivitySpanId.CreateRandom(); + + // Arrange + using (var testFactory = this.factory + .WithWebHostBuilder(builder => + { + builder.ConfigureTestServices(services => + { + this.tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddAspNetCoreInstrumentation() + .AddInMemoryExporter(exportedItems) + .Build(); + }); + + builder.ConfigureLogging(loggingBuilder => loggingBuilder.ClearProviders()); + })) + { + using var client = testFactory.CreateClient(); + var request = new HttpRequestMessage(HttpMethod.Get, "/api/values/2"); + request.Headers.Add("traceparent", $"00-{expectedTraceId}-{expectedSpanId}-01"); + + // Act + var response = await client.SendAsync(request); + + // Assert + response.EnsureSuccessStatusCode(); // Status Code 200-299 + + WaitForActivityExport(exportedItems, 1); + } + + Assert.Single(exportedItems); + var activity = exportedItems[0]; + + Assert.Equal("Microsoft.AspNetCore.Hosting.HttpRequestIn", activity.OperationName); + + Assert.Equal(expectedTraceId, activity.Context.TraceId); + Assert.Equal(expectedSpanId, activity.ParentSpanId); + + ValidateAspNetCoreActivity(activity, "/api/values/2"); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task CustomPropagator(bool addSampler) + { + try + { + var exportedItems = new List(); + var expectedTraceId = ActivityTraceId.CreateRandom(); + var expectedSpanId = ActivitySpanId.CreateRandom(); + + var propagator = new CustomTextMapPropagator + { + TraceId = expectedTraceId, + SpanId = expectedSpanId, + }; + + // Arrange + using (var testFactory = this.factory + .WithWebHostBuilder(builder => + { + builder.ConfigureTestServices(services => + { + Sdk.SetDefaultTextMapPropagator(propagator); + var tracerProviderBuilder = Sdk.CreateTracerProviderBuilder(); + + if (addSampler) + { + tracerProviderBuilder + .SetSampler(new TestSampler(SamplingDecision.RecordAndSample, new Dictionary { { "SomeTag", "SomeKey" }, })); + } + + this.tracerProvider = tracerProviderBuilder + .AddAspNetCoreInstrumentation() + .AddInMemoryExporter(exportedItems) + .Build(); + }); + builder.ConfigureLogging(loggingBuilder => loggingBuilder.ClearProviders()); + })) + { + using var client = testFactory.CreateClient(); + using var response = await client.GetAsync("/api/values/2"); + response.EnsureSuccessStatusCode(); // Status Code 200-299 + + WaitForActivityExport(exportedItems, 1); + } + + Assert.Single(exportedItems); + var activity = exportedItems[0]; + + Assert.True(activity.Duration != TimeSpan.Zero); + + Assert.Equal(expectedTraceId, activity.Context.TraceId); + Assert.Equal(expectedSpanId, activity.ParentSpanId); + + ValidateAspNetCoreActivity(activity, "/api/values/2"); + } + finally + { + Sdk.SetDefaultTextMapPropagator(new CompositeTextMapPropagator(new TextMapPropagator[] + { + new TraceContextPropagator(), + new BaggagePropagator(), + })); + } + } + + [Fact] + public async Task RequestNotCollectedWhenFilterIsApplied() + { + var exportedItems = new List(); + + void ConfigureTestServices(IServiceCollection services) + { + this.tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddAspNetCoreInstrumentation((opt) => opt.Filter = (ctx) => ctx.Request.Path != "/api/values/2") + .AddInMemoryExporter(exportedItems) + .Build(); + } + + // Arrange + using (var testFactory = this.factory + .WithWebHostBuilder(builder => + { + builder.ConfigureTestServices(ConfigureTestServices); + builder.ConfigureLogging(loggingBuilder => loggingBuilder.ClearProviders()); + })) + { + using var client = testFactory.CreateClient(); + + // Act + using var response1 = await client.GetAsync("/api/values"); + using var response2 = await client.GetAsync("/api/values/2"); + + // Assert + response1.EnsureSuccessStatusCode(); // Status Code 200-299 + response2.EnsureSuccessStatusCode(); // Status Code 200-299 + + WaitForActivityExport(exportedItems, 1); + } + + Assert.Single(exportedItems); + var activity = exportedItems[0]; + + ValidateAspNetCoreActivity(activity, "/api/values"); + } + + [Fact] + public async Task RequestNotCollectedWhenFilterThrowException() + { + var exportedItems = new List(); + + void ConfigureTestServices(IServiceCollection services) + { + this.tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddAspNetCoreInstrumentation((opt) => opt.Filter = (ctx) => + { + if (ctx.Request.Path == "/api/values/2") + { + throw new Exception("from InstrumentationFilter"); + } + else + { + return true; + } + }) + .AddInMemoryExporter(exportedItems) + .Build(); + } + + // Arrange + using (var testFactory = this.factory + .WithWebHostBuilder(builder => + { + builder.ConfigureTestServices(ConfigureTestServices); + builder.ConfigureLogging(loggingBuilder => loggingBuilder.ClearProviders()); + })) + { + using var client = testFactory.CreateClient(); + + // Act + using (var inMemoryEventListener = new InMemoryEventListener(AspNetCoreInstrumentationEventSource.Log)) + { + using var response1 = await client.GetAsync("/api/values"); + using var response2 = await client.GetAsync("/api/values/2"); + + response1.EnsureSuccessStatusCode(); // Status Code 200-299 + response2.EnsureSuccessStatusCode(); // Status Code 200-299 + Assert.Single(inMemoryEventListener.Events.Where((e) => e.EventId == 3)); + } + + WaitForActivityExport(exportedItems, 1); + } + + // As InstrumentationFilter threw, we continue as if the + // InstrumentationFilter did not exist. + + Assert.Single(exportedItems); + var activity = exportedItems[0]; + ValidateAspNetCoreActivity(activity, "/api/values"); + } + + [Theory] + [InlineData(SamplingDecision.Drop)] + [InlineData(SamplingDecision.RecordOnly)] + [InlineData(SamplingDecision.RecordAndSample)] + public async Task ExtractContextIrrespectiveOfSamplingDecision(SamplingDecision samplingDecision) + { + try + { + var expectedTraceId = ActivityTraceId.CreateRandom(); + var expectedParentSpanId = ActivitySpanId.CreateRandom(); + var expectedTraceState = "rojo=1,congo=2"; + var activityContext = new ActivityContext(expectedTraceId, expectedParentSpanId, ActivityTraceFlags.Recorded, expectedTraceState, true); + var expectedBaggage = Baggage.SetBaggage("key1", "value1").SetBaggage("key2", "value2"); + Sdk.SetDefaultTextMapPropagator(new ExtractOnlyPropagator(activityContext, expectedBaggage)); + + // Arrange + using var testFactory = this.factory + .WithWebHostBuilder(builder => + { + builder.ConfigureTestServices(services => { this.tracerProvider = Sdk.CreateTracerProviderBuilder().SetSampler(new TestSampler(samplingDecision)).AddAspNetCoreInstrumentation().Build(); }); + builder.ConfigureLogging(loggingBuilder => loggingBuilder.ClearProviders()); + }); + using var client = testFactory.CreateClient(); + + // Test TraceContext Propagation + var request = new HttpRequestMessage(HttpMethod.Get, "/api/GetChildActivityTraceContext"); + var response = await client.SendAsync(request); + var childActivityTraceContext = JsonSerializer.Deserialize>(await response.Content.ReadAsStringAsync()); + + response.EnsureSuccessStatusCode(); + + Assert.Equal(expectedTraceId.ToString(), childActivityTraceContext["TraceId"]); + Assert.Equal(expectedTraceState, childActivityTraceContext["TraceState"]); + Assert.NotEqual(expectedParentSpanId.ToString(), childActivityTraceContext["ParentSpanId"]); // there is a new activity created in instrumentation therefore the ParentSpanId is different that what is provided in the headers + + // Test Baggage Context Propagation + request = new HttpRequestMessage(HttpMethod.Get, "/api/GetChildActivityBaggageContext"); + + response = await client.SendAsync(request); + var childActivityBaggageContext = JsonSerializer.Deserialize>(await response.Content.ReadAsStringAsync()); + + response.EnsureSuccessStatusCode(); + + Assert.Single(childActivityBaggageContext, item => item.Key == "key1" && item.Value == "value1"); + Assert.Single(childActivityBaggageContext, item => item.Key == "key2" && item.Value == "value2"); + } + finally + { + Sdk.SetDefaultTextMapPropagator(new CompositeTextMapPropagator(new TextMapPropagator[] + { + new TraceContextPropagator(), + new BaggagePropagator(), + })); + } + } + + [Fact] + public async Task ExtractContextIrrespectiveOfTheFilterApplied() + { + try + { + var expectedTraceId = ActivityTraceId.CreateRandom(); + var expectedParentSpanId = ActivitySpanId.CreateRandom(); + var expectedTraceState = "rojo=1,congo=2"; + var activityContext = new ActivityContext(expectedTraceId, expectedParentSpanId, ActivityTraceFlags.Recorded, expectedTraceState); + var expectedBaggage = Baggage.SetBaggage("key1", "value1").SetBaggage("key2", "value2"); + Sdk.SetDefaultTextMapPropagator(new ExtractOnlyPropagator(activityContext, expectedBaggage)); + + // Arrange + bool isFilterCalled = false; + using var testFactory = this.factory + .WithWebHostBuilder(builder => + { + builder.ConfigureTestServices(services => + { + this.tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddAspNetCoreInstrumentation(options => + { + options.Filter = context => + { + isFilterCalled = true; + return false; + }; + }) + .Build(); + }); + builder.ConfigureLogging(loggingBuilder => loggingBuilder.ClearProviders()); + }); + using var client = testFactory.CreateClient(); + + // Test TraceContext Propagation + var request = new HttpRequestMessage(HttpMethod.Get, "/api/GetChildActivityTraceContext"); + var response = await client.SendAsync(request); + + // Ensure that filter was called + Assert.True(isFilterCalled); + + var childActivityTraceContext = JsonSerializer.Deserialize>(await response.Content.ReadAsStringAsync()); + + response.EnsureSuccessStatusCode(); + + Assert.Equal(expectedTraceId.ToString(), childActivityTraceContext["TraceId"]); + Assert.Equal(expectedTraceState, childActivityTraceContext["TraceState"]); + Assert.NotEqual(expectedParentSpanId.ToString(), childActivityTraceContext["ParentSpanId"]); // there is a new activity created in instrumentation therefore the ParentSpanId is different that what is provided in the headers + + // Test Baggage Context Propagation + request = new HttpRequestMessage(HttpMethod.Get, "/api/GetChildActivityBaggageContext"); + + response = await client.SendAsync(request); + var childActivityBaggageContext = JsonSerializer.Deserialize>(await response.Content.ReadAsStringAsync()); + + response.EnsureSuccessStatusCode(); + + Assert.Single(childActivityBaggageContext, item => item.Key == "key1" && item.Value == "value1"); + Assert.Single(childActivityBaggageContext, item => item.Key == "key2" && item.Value == "value2"); + } + finally + { + Sdk.SetDefaultTextMapPropagator(new CompositeTextMapPropagator(new TextMapPropagator[] + { + new TraceContextPropagator(), + new BaggagePropagator(), + })); + } + } + + [Fact] + public async Task BaggageIsNotClearedWhenActivityStopped() + { + int? baggageCountAfterStart = null; + int? baggageCountAfterStop = null; + using EventWaitHandle stopSignal = new EventWaitHandle(false, EventResetMode.ManualReset); + + void ConfigureTestServices(IServiceCollection services) + { + this.tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddAspNetCoreInstrumentation( + new TestHttpInListener(new AspNetCoreTraceInstrumentationOptions()) + { + OnEventWrittenCallback = (name, payload) => + { + switch (name) + { + case HttpInListener.OnStartEvent: + { + baggageCountAfterStart = Baggage.Current.Count; + } + + break; + case HttpInListener.OnStopEvent: + { + baggageCountAfterStop = Baggage.Current.Count; + stopSignal.Set(); + } + + break; + } + }, + }) + .Build(); + } + + // Arrange + using (var client = this.factory + .WithWebHostBuilder(builder => + { + builder.ConfigureTestServices(ConfigureTestServices); + builder.ConfigureLogging(loggingBuilder => loggingBuilder.ClearProviders()); + }) + .CreateClient()) + { + using var request = new HttpRequestMessage(HttpMethod.Get, "/api/values"); + + request.Headers.TryAddWithoutValidation("baggage", "TestKey1=123,TestKey2=456"); + + // Act + using var response = await client.SendAsync(request); + } + + stopSignal.WaitOne(5000); + + // Assert + Assert.NotNull(baggageCountAfterStart); + Assert.Equal(2, baggageCountAfterStart); + Assert.NotNull(baggageCountAfterStop); + Assert.Equal(2, baggageCountAfterStop); + } + + [Theory] + [InlineData(SamplingDecision.Drop, false, false)] + [InlineData(SamplingDecision.RecordOnly, true, true)] + [InlineData(SamplingDecision.RecordAndSample, true, true)] + public async Task FilterAndEnrichAreOnlyCalledWhenSampled(SamplingDecision samplingDecision, bool shouldFilterBeCalled, bool shouldEnrichBeCalled) + { + bool filterCalled = false; + bool enrichWithHttpRequestCalled = false; + bool enrichWithHttpResponseCalled = false; + void ConfigureTestServices(IServiceCollection services) + { + this.tracerProvider = Sdk.CreateTracerProviderBuilder() + .SetSampler(new TestSampler(samplingDecision)) + .AddAspNetCoreInstrumentation(options => + { + options.Filter = (context) => + { + filterCalled = true; + return true; + }; + options.EnrichWithHttpRequest = (activity, request) => + { + enrichWithHttpRequestCalled = true; + }; + options.EnrichWithHttpResponse = (activity, request) => + { + enrichWithHttpResponseCalled = true; + }; + }) + .Build(); + } + + // Arrange + using var client = this.factory + .WithWebHostBuilder(builder => + { + builder.ConfigureTestServices(ConfigureTestServices); + builder.ConfigureLogging(loggingBuilder => loggingBuilder.ClearProviders()); + }) + .CreateClient(); + + // Act + using var response = await client.GetAsync("/api/values"); + + // Assert + Assert.Equal(shouldFilterBeCalled, filterCalled); + Assert.Equal(shouldEnrichBeCalled, enrichWithHttpRequestCalled); + Assert.Equal(shouldEnrichBeCalled, enrichWithHttpResponseCalled); + } + + [Fact] + public async Task ActivitiesStartedInMiddlewareShouldNotBeUpdated() + { + var exportedItems = new List(); + + var activitySourceName = "TestMiddlewareActivitySource"; + var activityName = "TestMiddlewareActivity"; + + void ConfigureTestServices(IServiceCollection services) + { + services.AddSingleton(new TestActivityMiddlewareImpl(activitySourceName, activityName)); + this.tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddAspNetCoreInstrumentation() + .AddSource(activitySourceName) + .AddInMemoryExporter(exportedItems) + .Build(); + } + + // Arrange + using (var client = this.factory + .WithWebHostBuilder(builder => + { + builder.ConfigureTestServices(ConfigureTestServices); + builder.ConfigureLogging(loggingBuilder => loggingBuilder.ClearProviders()); + }) + .CreateClient()) + { + using var response = await client.GetAsync("/api/values/2"); + response.EnsureSuccessStatusCode(); + WaitForActivityExport(exportedItems, 2); + } + + Assert.Equal(2, exportedItems.Count); + + var middlewareActivity = exportedItems[0]; + + var aspnetcoreframeworkactivity = exportedItems[1]; + + // Middleware activity name should not be changed + Assert.Equal(ActivityKind.Internal, middlewareActivity.Kind); + Assert.Equal(activityName, middlewareActivity.OperationName); + Assert.Equal(activityName, middlewareActivity.DisplayName); + + // tag http.method should be added on activity started by asp.net core + Assert.Equal("GET", aspnetcoreframeworkactivity.GetTagValue(SemanticConventions.AttributeHttpRequestMethod) as string); + Assert.Equal("Microsoft.AspNetCore.Hosting.HttpRequestIn", aspnetcoreframeworkactivity.OperationName); + } + + [Theory] + [InlineData("CONNECT", "CONNECT", null, "CONNECT")] + [InlineData("DELETE", "DELETE", null, "DELETE")] + [InlineData("GET", "GET", null, "GET")] + [InlineData("PUT", "PUT", null, "PUT")] + [InlineData("HEAD", "HEAD", null, "HEAD")] + [InlineData("OPTIONS", "OPTIONS", null, "OPTIONS")] + [InlineData("PATCH", "PATCH", null, "PATCH")] + [InlineData("Get", "GET", "Get", "GET")] + [InlineData("POST", "POST", null, "POST")] + [InlineData("TRACE", "TRACE", null, "TRACE")] + [InlineData("CUSTOM", "_OTHER", "CUSTOM", "HTTP")] + public async Task HttpRequestMethodAndActivityDisplayIsSetAsPerSpec(string originalMethod, string expectedMethod, string expectedOriginalMethod, string expectedDisplayName) + { + var exportedItems = new List(); + + void ConfigureTestServices(IServiceCollection services) + { + this.tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddAspNetCoreInstrumentation() + .AddInMemoryExporter(exportedItems) + .Build(); + } + + // Arrange + using var client = this.factory + .WithWebHostBuilder(builder => + { + builder.ConfigureTestServices(ConfigureTestServices); + builder.ConfigureLogging(loggingBuilder => loggingBuilder.ClearProviders()); + }) + .CreateClient(); + + var message = new HttpRequestMessage(); + + message.Method = new HttpMethod(originalMethod); + + try + { + using var response = await client.SendAsync(message); + response.EnsureSuccessStatusCode(); + } + catch + { + // ignore error. + } + + WaitForActivityExport(exportedItems, 1); + + Assert.Single(exportedItems); + + var activity = exportedItems[0]; + + Assert.Equal(expectedMethod, activity.GetTagValue(SemanticConventions.AttributeHttpRequestMethod)); + Assert.Equal(expectedOriginalMethod, activity.GetTagValue(SemanticConventions.AttributeHttpRequestMethodOriginal)); + Assert.Equal(expectedDisplayName, activity.DisplayName); + } + + [Fact] + public async Task ActivitiesStartedInMiddlewareBySettingHostActivityToNullShouldNotBeUpdated() + { + var exportedItems = new List(); + + var activitySourceName = "TestMiddlewareActivitySource"; + var activityName = "TestMiddlewareActivity"; + + // Arrange + using (var client = this.factory + .WithWebHostBuilder(builder => + { + builder.ConfigureTestServices((IServiceCollection services) => + { + services.AddSingleton(new TestNullHostActivityMiddlewareImpl(activitySourceName, activityName)); + services.AddOpenTelemetry() + .WithTracing(builder => builder + .AddAspNetCoreInstrumentation() + .AddSource(activitySourceName) + .AddInMemoryExporter(exportedItems)); + }); + builder.ConfigureLogging(loggingBuilder => loggingBuilder.ClearProviders()); + }) + .CreateClient()) + { + using var response = await client.GetAsync("/api/values/2"); + response.EnsureSuccessStatusCode(); + WaitForActivityExport(exportedItems, 2); + } + + Assert.Equal(2, exportedItems.Count); + + var middlewareActivity = exportedItems[0]; + + var aspnetcoreframeworkactivity = exportedItems[1]; + + // Middleware activity name should not be changed + Assert.Equal(ActivityKind.Internal, middlewareActivity.Kind); + Assert.Equal(activityName, middlewareActivity.OperationName); + Assert.Equal(activityName, middlewareActivity.DisplayName); + + // tag http.method should be added on activity started by asp.net core + Assert.Equal("GET", aspnetcoreframeworkactivity.GetTagValue(SemanticConventions.AttributeHttpRequestMethod) as string); + Assert.Equal("Microsoft.AspNetCore.Hosting.HttpRequestIn", aspnetcoreframeworkactivity.OperationName); + } + +#if NET7_0_OR_GREATER + [Fact] + public async Task UserRegisteredActivitySourceIsUsedForActivityCreationByAspNetCore() + { + var exportedItems = new List(); + void ConfigureTestServices(IServiceCollection services) + { + services.AddOpenTelemetry() + .WithTracing(builder => builder + .AddAspNetCoreInstrumentation() + .AddInMemoryExporter(exportedItems)); + + // Register ActivitySource here so that it will be used + // by ASP.NET Core to create activities + // https://github.com/dotnet/aspnetcore/blob/0e5cbf447d329a1e7d69932c3decd1c70a00fbba/src/Hosting/Hosting/src/Internal/WebHost.cs#L152 + services.AddSingleton(sp => new ActivitySource("UserRegisteredActivitySource")); + } + + // Arrange + using (var client = this.factory + .WithWebHostBuilder(builder => + { + builder.ConfigureTestServices(ConfigureTestServices); + builder.ConfigureLogging(loggingBuilder => loggingBuilder.ClearProviders()); + }) + .CreateClient()) + { + // Act + using var response = await client.GetAsync("/api/values"); + + // Assert + response.EnsureSuccessStatusCode(); // Status Code 200-299 + + WaitForActivityExport(exportedItems, 1); + } + + Assert.Single(exportedItems); + var activity = exportedItems[0]; + + Assert.Equal("UserRegisteredActivitySource", activity.Source.Name); + } +#endif + + [Theory] + [InlineData(1)] + [InlineData(2)] + public async Task ShouldExportActivityWithOneOrMoreExceptionFilters(int mode) + { + var exportedItems = new List(); + + // Arrange + using (var client = this.factory + .WithWebHostBuilder(builder => + { + builder.ConfigureTestServices( + (s) => this.ConfigureExceptionFilters(s, mode, ref exportedItems)); + builder.ConfigureLogging(loggingBuilder => loggingBuilder.ClearProviders()); + }) + .CreateClient()) + { + // Act + using var response = await client.GetAsync("/api/error"); + + WaitForActivityExport(exportedItems, 1); + } + + // Assert + AssertException(exportedItems); + } + + [Fact] + public async Task DiagnosticSourceCallbacksAreReceivedOnlyForSubscribedEvents() + { + int numberOfUnSubscribedEvents = 0; + int numberofSubscribedEvents = 0; + + this.tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddAspNetCoreInstrumentation( + new TestHttpInListener(new AspNetCoreTraceInstrumentationOptions()) + { + OnEventWrittenCallback = (name, payload) => + { + switch (name) + { + case HttpInListener.OnStartEvent: + { + numberofSubscribedEvents++; + } + + break; + case HttpInListener.OnStopEvent: + { + numberofSubscribedEvents++; + } + + break; + default: + { + numberOfUnSubscribedEvents++; + } + + break; + } + }, + }) + .Build(); + + // Arrange + using (var client = this.factory + .WithWebHostBuilder(builder => + { + builder.ConfigureLogging(loggingBuilder => loggingBuilder.ClearProviders()); + }) + .CreateClient()) + { + using var request = new HttpRequestMessage(HttpMethod.Get, "/api/values"); + + // Act + using var response = await client.SendAsync(request); + } + + Assert.Equal(0, numberOfUnSubscribedEvents); + Assert.Equal(2, numberofSubscribedEvents); + } + + [Fact] + public async Task DiagnosticSourceExceptionCallbackIsReceivedForUnHandledException() + { + int numberOfUnSubscribedEvents = 0; + int numberofSubscribedEvents = 0; + int numberOfExceptionCallbacks = 0; + + this.tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddAspNetCoreInstrumentation( + new TestHttpInListener(new AspNetCoreTraceInstrumentationOptions()) + { + OnEventWrittenCallback = (name, payload) => + { + switch (name) + { + case HttpInListener.OnStartEvent: + { + numberofSubscribedEvents++; + } + + break; + case HttpInListener.OnStopEvent: + { + numberofSubscribedEvents++; + } + + break; + + // TODO: Add test case for validating name for both the types + // of exception event. + case HttpInListener.OnUnhandledHostingExceptionEvent: + case HttpInListener.OnUnHandledDiagnosticsExceptionEvent: + { + numberofSubscribedEvents++; + numberOfExceptionCallbacks++; + } + + break; + default: + { + numberOfUnSubscribedEvents++; + } + + break; + } + }, + }) + .Build(); + + // Arrange + using (var client = this.factory + .WithWebHostBuilder(builder => + { + builder.ConfigureLogging(loggingBuilder => loggingBuilder.ClearProviders()); + }) + .CreateClient()) + { + try + { + using var request = new HttpRequestMessage(HttpMethod.Get, "/api/error"); + + // Act + using var response = await client.SendAsync(request); + } + catch + { + // ignore exception + } + } + + Assert.Equal(1, numberOfExceptionCallbacks); + Assert.Equal(0, numberOfUnSubscribedEvents); + Assert.Equal(3, numberofSubscribedEvents); + } + + [Fact] + public async Task DiagnosticSourceExceptionCallBackIsNotReceivedForExceptionsHandledInMiddleware() + { + int numberOfUnSubscribedEvents = 0; + int numberOfSubscribedEvents = 0; + int numberOfExceptionCallbacks = 0; + bool exceptionHandled = false; + + // configure SDK + this.tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddAspNetCoreInstrumentation( + new TestHttpInListener(new AspNetCoreTraceInstrumentationOptions()) + { + OnEventWrittenCallback = (name, payload) => + { + switch (name) + { + case HttpInListener.OnStartEvent: + { + numberOfSubscribedEvents++; + } + + break; + case HttpInListener.OnStopEvent: + { + numberOfSubscribedEvents++; + } + + break; + + // TODO: Add test case for validating name for both the types + // of exception event. + case HttpInListener.OnUnhandledHostingExceptionEvent: + case HttpInListener.OnUnHandledDiagnosticsExceptionEvent: + { + numberOfSubscribedEvents++; + numberOfExceptionCallbacks++; + } + + break; + default: + { + numberOfUnSubscribedEvents++; + } + + break; + } + }, + }) + .Build(); + + TestMiddleware.Create(builder => builder + .UseExceptionHandler(handler => + handler.Run(async (ctx) => + { + exceptionHandled = true; + await ctx.Response.WriteAsync("handled"); + }))); + + using (var client = this.factory + .WithWebHostBuilder(builder => + { + builder.ConfigureLogging(loggingBuilder => loggingBuilder.ClearProviders()); + }) + .CreateClient()) + { + try + { + using var request = new HttpRequestMessage(HttpMethod.Get, "/api/error"); + using var response = await client.SendAsync(request); + } + catch + { + // ignore exception + } + } + + Assert.Equal(0, numberOfExceptionCallbacks); + Assert.Equal(0, numberOfUnSubscribedEvents); + Assert.Equal(2, numberOfSubscribedEvents); + Assert.True(exceptionHandled); + } + +#if NET6_0_OR_GREATER + [Fact] + public async Task NoSiblingActivityCreatedWhenTraceFlagsNone() + { + this.tracerProvider = Sdk.CreateTracerProviderBuilder() + .SetSampler(new AlwaysOnSampler()) + .AddAspNetCoreInstrumentation() + .Build(); + + using var testFactory = this.factory + .WithWebHostBuilder(builder => + { + builder.ConfigureTestServices(services => + { + this.tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddAspNetCoreInstrumentation() + .Build(); + }); + + builder.ConfigureLogging(loggingBuilder => loggingBuilder.ClearProviders()); + }); + using var client = testFactory.CreateClient(); + var request = new HttpRequestMessage(HttpMethod.Get, "/api/GetActivityEquality"); + var traceId = ActivityTraceId.CreateRandom(); + var spanId = ActivitySpanId.CreateRandom(); + request.Headers.Add("traceparent", $"00-{traceId}-{spanId}-00"); + + var response = await client.SendAsync(request); + var result = bool.Parse(await response.Content.ReadAsStringAsync()); + + Assert.True(response.IsSuccessStatusCode); + + // Confirm that Activity.Current and IHttpActivityFeature activity are same + Assert.True(result); + } +#endif + + [Theory] + [InlineData("?a", "?a", false)] + [InlineData("?a=bdjdjh", "?a=Redacted", false)] + [InlineData("?a=b&", "?a=Redacted&", false)] + [InlineData("?c=b&", "?c=Redacted&", false)] + [InlineData("?c=a", "?c=Redacted", false)] + [InlineData("?a=b&c", "?a=Redacted&c", false)] + [InlineData("?a=b&c=1123456&", "?a=Redacted&c=Redacted&", false)] + [InlineData("?a=b&c=1&a1", "?a=Redacted&c=Redacted&a1", false)] + [InlineData("?a=ghgjgj&c=1deedd&a1=", "?a=Redacted&c=Redacted&a1=Redacted", false)] + [InlineData("?a=b&c=11&a1=&", "?a=Redacted&c=Redacted&a1=Redacted&", false)] + [InlineData("?c&c&c&", "?c&c&c&", false)] + [InlineData("?a&a&a&a", "?a&a&a&a", false)] + [InlineData("?&&&&&&&", "?&&&&&&&", false)] + [InlineData("?c", "?c", false)] + [InlineData("?a", "?a", true)] + [InlineData("?a=bdfdfdf", "?a=bdfdfdf", true)] + [InlineData("?a=b&", "?a=b&", true)] + [InlineData("?c=b&", "?c=b&", true)] + [InlineData("?c=a", "?c=a", true)] + [InlineData("?a=b&c", "?a=b&c", true)] + [InlineData("?a=b&c=111111&", "?a=b&c=111111&", true)] + [InlineData("?a=b&c=1&a1", "?a=b&c=1&a1", true)] + [InlineData("?a=b&c=1&a1=", "?a=b&c=1&a1=", true)] + [InlineData("?a=b123&c=11&a1=&", "?a=b123&c=11&a1=&", true)] + [InlineData("?c&c&c&", "?c&c&c&", true)] + [InlineData("?a&a&a&a", "?a&a&a&a", true)] + [InlineData("?&&&&&&&", "?&&&&&&&", true)] + [InlineData("?c", "?c", true)] + [InlineData("?c=%26&", "?c=Redacted&", false)] + public async Task ValidateUrlQueryRedaction(string urlQuery, string expectedUrlQuery, bool disableQueryRedaction) + { + var exportedItems = new List(); + + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary { ["OTEL_DOTNET_EXPERIMENTAL_ASPNETCORE_DISABLE_URL_QUERY_REDACTION"] = disableQueryRedaction.ToString() }) + .Build(); + + var path = "/api/values" + urlQuery; + + // Arrange + using var traceprovider = Sdk.CreateTracerProviderBuilder() + .ConfigureServices(services => services.AddSingleton(configuration)) + .AddAspNetCoreInstrumentation() + .AddInMemoryExporter(exportedItems) + .Build(); + + using (var client = this.factory + .WithWebHostBuilder(builder => + { + builder.ConfigureLogging(loggingBuilder => loggingBuilder.ClearProviders()); + }) + .CreateClient()) + { + try + { + using var response = await client.GetAsync(path); + } + catch (Exception) + { + // ignore errors + } + + WaitForActivityExport(exportedItems, 1); + } + + Assert.Single(exportedItems); + var activity = exportedItems[0]; + + Assert.Equal(expectedUrlQuery, activity.GetTagValue(SemanticConventions.AttributeUrlQuery)); + } + + public void Dispose() + { + this.tracerProvider?.Dispose(); + } + + private static void WaitForActivityExport(List exportedItems, int count) + { + // We need to let End callback execute as it is executed AFTER response was returned. + // In unit tests environment there may be a lot of parallel unit tests executed, so + // giving some breezing room for the End callback to complete + Assert.True(SpinWait.SpinUntil( + () => + { + Thread.Sleep(10); + return exportedItems.Count >= count; + }, + TimeSpan.FromSeconds(1))); + } + + private static void ValidateAspNetCoreActivity(Activity activityToValidate, string expectedHttpPath) + { + Assert.Equal(ActivityKind.Server, activityToValidate.Kind); +#if NET7_0_OR_GREATER + Assert.Equal(HttpInListener.AspNetCoreActivitySourceName, activityToValidate.Source.Name); + Assert.Empty(activityToValidate.Source.Version); +#else + Assert.Equal(HttpInListener.ActivitySourceName, activityToValidate.Source.Name); + Assert.Equal(HttpInListener.Version.ToString(), activityToValidate.Source.Version); +#endif + Assert.Equal(expectedHttpPath, activityToValidate.GetTagValue(SemanticConventions.AttributeUrlPath) as string); + } + + private static void AssertException(List exportedItems) + { + Assert.Single(exportedItems); + var activity = exportedItems[0]; + + var exMessage = "something's wrong!"; + Assert.Single(activity.Events); + Assert.Equal("System.Exception", activity.Events.First().Tags.FirstOrDefault(t => t.Key == SemanticConventions.AttributeExceptionType).Value); + Assert.Equal(exMessage, activity.Events.First().Tags.FirstOrDefault(t => t.Key == SemanticConventions.AttributeExceptionMessage).Value); + + ValidateAspNetCoreActivity(activity, "/api/error"); + } + + private void ConfigureExceptionFilters(IServiceCollection services, int mode, ref List exportedItems) + { + switch (mode) + { + case 1: + services.AddMvc(x => x.Filters.Add()); + break; + case 2: + services.AddMvc(x => x.Filters.Add()); + services.AddMvc(x => x.Filters.Add()); + break; + default: + break; + } + + this.tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddAspNetCoreInstrumentation(x => x.RecordException = true) + .AddInMemoryExporter(exportedItems) + .Build(); + } + + private class ExtractOnlyPropagator(ActivityContext activityContext, Baggage baggage) : TextMapPropagator + { + private readonly ActivityContext activityContext = activityContext; + private readonly Baggage baggage = baggage; + + public override ISet Fields => throw new NotImplementedException(); + + public override PropagationContext Extract(PropagationContext context, T carrier, Func> getter) + { + return new PropagationContext(this.activityContext, this.baggage); + } + + public override void Inject(PropagationContext context, T carrier, Action setter) + { + throw new NotImplementedException(); + } + } + + private class TestSampler(SamplingDecision samplingDecision, IEnumerable> attributes = null) : Sampler + { + private readonly SamplingDecision samplingDecision = samplingDecision; + private readonly IEnumerable> attributes = attributes; + + public override SamplingResult ShouldSample(in SamplingParameters samplingParameters) + { + return new SamplingResult(this.samplingDecision, this.attributes); + } + } + + private class TestHttpInListener(AspNetCoreTraceInstrumentationOptions options) : HttpInListener(options) + { + public Action OnEventWrittenCallback; + + public override void OnEventWritten(string name, object payload) + { + base.OnEventWritten(name, payload); + + this.OnEventWrittenCallback?.Invoke(name, payload); + } + } + + private class TestNullHostActivityMiddlewareImpl(string activitySourceName, string activityName) : ActivityMiddleware.ActivityMiddlewareImpl + { + private readonly ActivitySource activitySource = new(activitySourceName); + private readonly string activityName = activityName; + private Activity activity; + + public override void PreProcess(HttpContext context) + { + // Setting the host activity i.e. activity started by asp.net core + // to null here will have no impact on middleware activity. + // This also means that asp.net core activity will not be found + // during OnEventWritten event. + Activity.Current = null; + this.activity = this.activitySource.StartActivity(this.activityName); + } + + public override void PostProcess(HttpContext context) + { + this.activity?.Stop(); + } + } + + private class TestActivityMiddlewareImpl(string activitySourceName, string activityName) : ActivityMiddleware.ActivityMiddlewareImpl + { + private readonly ActivitySource activitySource = new(activitySourceName); + private readonly string activityName = activityName; + private Activity activity; + + public override void PreProcess(HttpContext context) + { + this.activity = this.activitySource.StartActivity(this.activityName); + } + + public override void PostProcess(HttpContext context) + { + this.activity?.Stop(); + } + } +} diff --git a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/DependencyInjectionConfigTests.cs b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/DependencyInjectionConfigTests.cs new file mode 100644 index 0000000000..766a81b40d --- /dev/null +++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/DependencyInjectionConfigTests.cs @@ -0,0 +1,54 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using OpenTelemetry.Metrics; +using OpenTelemetry.Trace; +using Xunit; + +namespace OpenTelemetry.Instrumentation.AspNetCore.Tests; + +public class DependencyInjectionConfigTests + : IClassFixture> +{ + private readonly WebApplicationFactory factory; + + public DependencyInjectionConfigTests(WebApplicationFactory factory) + { + this.factory = factory; + } + + [Theory] + [InlineData(null)] + [InlineData("CustomName")] + public void TestTracingOptionsDIConfig(string name) + { + name ??= Options.DefaultName; + + bool optionsPickedFromDI = false; + void ConfigureTestServices(IServiceCollection services) + { + services.AddOpenTelemetry() + .WithTracing(builder => builder + .AddAspNetCoreInstrumentation(name, configureAspNetCoreTraceInstrumentationOptions: null)); + + services.Configure(name, options => + { + optionsPickedFromDI = true; + }); + } + + // Arrange + using (var client = this.factory + .WithWebHostBuilder(builder => + builder.ConfigureTestServices(ConfigureTestServices)) + .CreateClient()) + { + } + + Assert.True(optionsPickedFromDI); + } +} diff --git a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/EventSourceTest.cs b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/EventSourceTest.cs new file mode 100644 index 0000000000..5bae1e25f4 --- /dev/null +++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/EventSourceTest.cs @@ -0,0 +1,17 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using OpenTelemetry.Instrumentation.AspNetCore.Implementation; +using OpenTelemetry.Tests; +using Xunit; + +namespace OpenTelemetry.Instrumentation.AspNetCore.Tests; + +public class EventSourceTest +{ + [Fact] + public void EventSourceTest_AspNetCoreInstrumentationEventSource() + { + EventSourceTestHelper.MethodsAreImplementedConsistentlyWithTheirAttributes(AspNetCoreInstrumentationEventSource.Log); + } +} diff --git a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/IncomingRequestsCollectionsIsAccordingToTheSpecTests.cs b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/IncomingRequestsCollectionsIsAccordingToTheSpecTests.cs new file mode 100644 index 0000000000..d5777dbc6f --- /dev/null +++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/IncomingRequestsCollectionsIsAccordingToTheSpecTests.cs @@ -0,0 +1,171 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using System.Diagnostics; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using OpenTelemetry.Trace; +using TestApp.AspNetCore; +using Xunit; + +namespace OpenTelemetry.Instrumentation.AspNetCore.Tests; + +public class IncomingRequestsCollectionsIsAccordingToTheSpecTests + : IClassFixture> +{ + private readonly WebApplicationFactory factory; + + public IncomingRequestsCollectionsIsAccordingToTheSpecTests(WebApplicationFactory factory) + { + this.factory = factory; + } + + [Theory] + [InlineData("/api/values", null, "user-agent", 200, null)] + [InlineData("/api/values", null, null, 200, null)] + [InlineData("/api/exception", null, null, 503, null)] + [InlineData("/api/exception", null, null, 503, null, true)] + public async Task SuccessfulTemplateControllerCallGeneratesASpan_New( + string urlPath, + string query, + string userAgent, + int statusCode, + string reasonPhrase, + bool recordException = false) + { + var exportedItems = new List(); + + // Arrange + using (var client = this.factory + .WithWebHostBuilder(builder => + { + builder.ConfigureTestServices((IServiceCollection services) => + { + services.AddSingleton(new TestCallbackMiddlewareImpl(statusCode, reasonPhrase)); + services.AddOpenTelemetry() + .WithTracing(builder => builder + .AddAspNetCoreInstrumentation(options => + { + options.RecordException = recordException; + }) + .AddInMemoryExporter(exportedItems)); + }); + builder.ConfigureLogging(loggingBuilder => loggingBuilder.ClearProviders()); + }) + .CreateClient()) + { + try + { + if (!string.IsNullOrEmpty(userAgent)) + { + client.DefaultRequestHeaders.Add("User-Agent", userAgent); + } + + // Act + var path = urlPath; + if (query != null) + { + path += query; + } + + using var response = await client.GetAsync(path); + } + catch (Exception) + { + // ignore errors + } + + for (var i = 0; i < 10; i++) + { + if (exportedItems.Count == 1) + { + break; + } + + // We need to let End callback execute as it is executed AFTER response was returned. + // In unit tests environment there may be a lot of parallel unit tests executed, so + // giving some breezing room for the End callback to complete + await Task.Delay(TimeSpan.FromSeconds(1)); + } + } + + Assert.Single(exportedItems); + var activity = exportedItems[0]; + + Assert.Equal(ActivityKind.Server, activity.Kind); + Assert.Equal("localhost", activity.GetTagValue(SemanticConventions.AttributeServerAddress)); + Assert.Equal("GET", activity.GetTagValue(SemanticConventions.AttributeHttpRequestMethod)); + Assert.Equal("1.1", activity.GetTagValue(SemanticConventions.AttributeNetworkProtocolVersion)); + Assert.Equal("http", activity.GetTagValue(SemanticConventions.AttributeUrlScheme)); + Assert.Equal(urlPath, activity.GetTagValue(SemanticConventions.AttributeUrlPath)); + Assert.Equal(query, activity.GetTagValue(SemanticConventions.AttributeUrlQuery)); + Assert.Equal(statusCode, activity.GetTagValue(SemanticConventions.AttributeHttpResponseStatusCode)); + + if (statusCode == 503) + { + Assert.Equal(ActivityStatusCode.Error, activity.Status); + Assert.Equal("System.Exception", activity.GetTagValue(SemanticConventions.AttributeErrorType)); + } + else + { + Assert.Equal(ActivityStatusCode.Unset, activity.Status); + } + + // Instrumentation is not expected to set status description + // as the reason can be inferred from SemanticConventions.AttributeHttpStatusCode + Assert.Null(activity.StatusDescription); + + if (recordException) + { + Assert.Single(activity.Events); + Assert.Equal("exception", activity.Events.First().Name); + } + + ValidateTagValue(activity, SemanticConventions.AttributeUserAgentOriginal, userAgent); + + activity.Dispose(); + } + + private static void ValidateTagValue(Activity activity, string attribute, string expectedValue) + { + if (string.IsNullOrEmpty(expectedValue)) + { + Assert.Null(activity.GetTagValue(attribute)); + } + else + { + Assert.Equal(expectedValue, activity.GetTagValue(attribute)); + } + } + + public class TestCallbackMiddlewareImpl : CallbackMiddleware.CallbackMiddlewareImpl + { + private readonly int statusCode; + private readonly string reasonPhrase; + + public TestCallbackMiddlewareImpl(int statusCode, string reasonPhrase) + { + this.statusCode = statusCode; + this.reasonPhrase = reasonPhrase; + } + + public override async Task ProcessAsync(HttpContext context) + { + context.Response.StatusCode = this.statusCode; + context.Response.HttpContext.Features.Get().ReasonPhrase = this.reasonPhrase; + await context.Response.WriteAsync("empty"); + + if (context.Request.Path.Value.EndsWith("exception")) + { + throw new Exception("exception description"); + } + + return false; + } + } +} diff --git a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/MetricTests.cs b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/MetricTests.cs new file mode 100644 index 0000000000..ddec198dc4 --- /dev/null +++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/MetricTests.cs @@ -0,0 +1,419 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#if NET8_0_OR_GREATER +using System.Threading.RateLimiting; +using Microsoft.AspNetCore.Builder; +#endif +using Microsoft.AspNetCore.Hosting; +#if NET8_0_OR_GREATER +using Microsoft.AspNetCore.Http; +#endif +using Microsoft.AspNetCore.Mvc.Testing; +#if NET8_0_OR_GREATER +using Microsoft.AspNetCore.RateLimiting; +#endif +#if NET8_0_OR_GREATER +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +#endif +using Microsoft.Extensions.Logging; +using OpenTelemetry.Metrics; +using OpenTelemetry.Trace; +using Xunit; + +namespace OpenTelemetry.Instrumentation.AspNetCore.Tests; + +public class MetricTests(WebApplicationFactory factory) + : IClassFixture>, IDisposable +{ + private const int StandardTagsCount = 6; + + private readonly WebApplicationFactory factory = factory; + private MeterProvider meterProvider; + + [Fact] + public void AddAspNetCoreInstrumentation_BadArgs() + { + MeterProviderBuilder builder = null; + Assert.Throws(builder.AddAspNetCoreInstrumentation); + } + +#if NET8_0_OR_GREATER + [Fact] + public async Task ValidateNet8MetricsAsync() + { + var exportedItems = new List(); + this.meterProvider = Sdk.CreateMeterProviderBuilder() + .AddAspNetCoreInstrumentation() + .AddInMemoryExporter(exportedItems) + .Build(); + + var builder = WebApplication.CreateBuilder(); + builder.WebHost.UseUrls("http://*:0"); + var app = builder.Build(); + + app.MapGet("/", () => "Hello"); + + _ = app.RunAsync(); + + var url = app.Urls.ToArray()[0]; + var portNumber = url.Substring(url.LastIndexOf(':') + 1); + + using var client = new HttpClient(); + var res = await client.GetAsync($"http://localhost:{portNumber}/"); + Assert.True(res.IsSuccessStatusCode); + + // We need to let metric callback execute as it is executed AFTER response was returned. + // In unit tests environment there may be a lot of parallel unit tests executed, so + // giving some breezing room for the callbacks to complete + await Task.Delay(TimeSpan.FromSeconds(1)); + + this.meterProvider.Dispose(); + + var requestDurationMetric = exportedItems + .Count(item => item.Name == "http.server.request.duration"); + + var activeRequestsMetric = exportedItems. + Count(item => item.Name == "http.server.active_requests"); + + var routeMatchingMetric = exportedItems. + Count(item => item.Name == "aspnetcore.routing.match_attempts"); + + var kestrelActiveConnectionsMetric = exportedItems. + Count(item => item.Name == "kestrel.active_connections"); + + var kestrelQueuedConnectionMetric = exportedItems. + Count(item => item.Name == "kestrel.queued_connections"); + + Assert.Equal(1, requestDurationMetric); + Assert.Equal(1, activeRequestsMetric); + Assert.Equal(1, routeMatchingMetric); + Assert.Equal(1, kestrelActiveConnectionsMetric); + Assert.Equal(1, kestrelQueuedConnectionMetric); + + // TODO + // kestrel.queued_requests + // kestrel.upgraded_connections + // kestrel.rejected_connections + // kestrel.tls_handshake.duration + // kestrel.active_tls_handshakes + + await app.DisposeAsync(); + } + + [Fact] + public async Task ValidateNet8RateLimitingMetricsAsync() + { + var exportedItems = new List(); + + void ConfigureTestServices(IServiceCollection services) + { + this.meterProvider = Sdk.CreateMeterProviderBuilder() + .AddAspNetCoreInstrumentation() + .AddInMemoryExporter(exportedItems) + .Build(); + + services.AddRateLimiter(_ => _ + .AddFixedWindowLimiter(policyName: "fixed", options => + { + options.PermitLimit = 4; + options.Window = TimeSpan.FromSeconds(12); + options.QueueProcessingOrder = QueueProcessingOrder.OldestFirst; + options.QueueLimit = 2; + })); + } + + var builder = WebApplication.CreateBuilder(); + builder.WebHost.UseUrls("http://*:0"); + ConfigureTestServices(builder.Services); + + builder.Logging.ClearProviders(); + var app = builder.Build(); + + app.UseRateLimiter(); + + static string GetTicks() => (DateTime.Now.Ticks & 0x11111).ToString("00000"); + + app.MapGet("/", () => Results.Ok($"Hello {GetTicks()}")) + .RequireRateLimiting("fixed"); + + _ = app.RunAsync(); + + var url = app.Urls.ToArray()[0]; + var portNumber = url.Substring(url.LastIndexOf(':') + 1); + + using var client = new HttpClient(); + var res = await client.GetAsync($"http://localhost:{portNumber}/"); + Assert.NotNull(res); + + // We need to let metric callback execute as it is executed AFTER response was returned. + // In unit tests environment there may be a lot of parallel unit tests executed, so + // giving some breezing room for the callbacks to complete + await Task.Delay(TimeSpan.FromSeconds(1)); + + this.meterProvider.Dispose(); + + var activeRequestLeasesMetric = exportedItems + .Where(item => item.Name == "aspnetcore.rate_limiting.active_request_leases") + .ToArray(); + + var requestLeaseDurationMetric = exportedItems. + Where(item => item.Name == "aspnetcore.rate_limiting.request_lease.duration") + .ToArray(); + + var limitingRequestsMetric = exportedItems. + Where(item => item.Name == "aspnetcore.rate_limiting.requests") + .ToArray(); + + Assert.Single(activeRequestLeasesMetric); + Assert.Single(requestLeaseDurationMetric); + Assert.Single(limitingRequestsMetric); + + // TODO + // aspnetcore.rate_limiting.request.time_in_queue + // aspnetcore.rate_limiting.queued_requests + + await app.DisposeAsync(); + } +#endif + + [Theory] + [InlineData("/api/values/2", "api/Values/{id}", null, 200)] + [InlineData("/api/Error", "api/Error", "System.Exception", 500)] + public async Task RequestMetricIsCaptured(string api, string expectedRoute, string expectedErrorType, int expectedStatusCode) + { + var metricItems = new List(); + + this.meterProvider = Sdk.CreateMeterProviderBuilder() + .AddAspNetCoreInstrumentation() + .AddInMemoryExporter(metricItems) + .Build(); + + using (var client = this.factory + .WithWebHostBuilder(builder => + { + builder.ConfigureLogging(loggingBuilder => loggingBuilder.ClearProviders()); + }) + .CreateClient()) + { + try + { + using var response = await client.GetAsync(api); + response.EnsureSuccessStatusCode(); + } + catch + { + // ignore error. + } + } + + // We need to let End callback execute as it is executed AFTER response was returned. + // In unit tests environment there may be a lot of parallel unit tests executed, so + // giving some breezing room for the End callback to complete + await Task.Delay(TimeSpan.FromSeconds(1)); + + this.meterProvider.Dispose(); + + var requestMetrics = metricItems + .Where(item => item.Name == "http.server.request.duration") + .ToArray(); + + var metric = Assert.Single(requestMetrics); + + Assert.Equal("s", metric.Unit); + var metricPoints = GetMetricPoints(metric); + Assert.Single(metricPoints); + + AssertMetricPoints( + metricPoints: metricPoints, + expectedRoutes: new List { expectedRoute }, + expectedErrorType, + expectedStatusCode, + expectedTagsCount: expectedErrorType == null ? 5 : 6); + } + + [Theory] + [InlineData("CONNECT", "CONNECT")] + [InlineData("DELETE", "DELETE")] + [InlineData("GET", "GET")] + [InlineData("PUT", "PUT")] + [InlineData("HEAD", "HEAD")] + [InlineData("OPTIONS", "OPTIONS")] + [InlineData("PATCH", "PATCH")] + [InlineData("Get", "GET")] + [InlineData("POST", "POST")] + [InlineData("TRACE", "TRACE")] + [InlineData("CUSTOM", "_OTHER")] + public async Task HttpRequestMethodIsCapturedAsPerSpec(string originalMethod, string expectedMethod) + { + var metricItems = new List(); + + this.meterProvider = Sdk.CreateMeterProviderBuilder() + .AddAspNetCoreInstrumentation() + .AddInMemoryExporter(metricItems) + .Build(); + + using var client = this.factory + .WithWebHostBuilder(builder => + { + builder.ConfigureLogging(loggingBuilder => loggingBuilder.ClearProviders()); + }) + .CreateClient(); + + var message = new HttpRequestMessage(); + message.Method = new HttpMethod(originalMethod); + + try + { + using var response = await client.SendAsync(message); + } + catch + { + // ignore error. + } + + // We need to let End callback execute as it is executed AFTER response was returned. + // In unit tests environment there may be a lot of parallel unit tests executed, so + // giving some breezing room for the End callback to complete + await Task.Delay(TimeSpan.FromSeconds(1)); + + this.meterProvider.Dispose(); + + var requestMetrics = metricItems + .Where(item => item.Name == "http.server.request.duration") + .ToArray(); + + var metric = Assert.Single(requestMetrics); + + Assert.Equal("s", metric.Unit); + var metricPoints = GetMetricPoints(metric); + Assert.Single(metricPoints); + + var mp = metricPoints[0]; + + // Inspect Metric Attributes + var attributes = new Dictionary(); + foreach (var tag in mp.Tags) + { + attributes[tag.Key] = tag.Value; + } + + Assert.Contains(attributes, kvp => kvp.Key == SemanticConventions.AttributeHttpRequestMethod && kvp.Value.ToString() == expectedMethod); + + Assert.DoesNotContain(attributes, t => t.Key == SemanticConventions.AttributeHttpRequestMethodOriginal); + } + + public void Dispose() + { + this.meterProvider?.Dispose(); + GC.SuppressFinalize(this); + } + + private static List GetMetricPoints(Metric metric) + { + Assert.NotNull(metric); + Assert.True(metric.MetricType == MetricType.Histogram); + var metricPoints = new List(); + foreach (var p in metric.GetMetricPoints()) + { + metricPoints.Add(p); + } + + return metricPoints; + } + + private static void AssertMetricPoints( + List metricPoints, + List expectedRoutes, + string expectedErrorType, + int expectedStatusCode, + int expectedTagsCount) + { + // Assert that one MetricPoint exists for each ExpectedRoute + foreach (var expectedRoute in expectedRoutes) + { + MetricPoint? metricPoint = null; + + foreach (var mp in metricPoints) + { + foreach (var tag in mp.Tags) + { + if (tag.Key == SemanticConventions.AttributeHttpRoute && tag.Value.ToString() == expectedRoute) + { + metricPoint = mp; + } + } + } + + if (metricPoint.HasValue) + { + AssertMetricPoint(metricPoint.Value, expectedStatusCode, expectedRoute, expectedErrorType, expectedTagsCount); + } + else + { + Assert.Fail($"A metric for route '{expectedRoute}' was not found"); + } + } + } + + private static void AssertMetricPoint( + MetricPoint metricPoint, + int expectedStatusCode, + string expectedRoute, + string expectedErrorType, + int expectedTagsCount) + { + var count = metricPoint.GetHistogramCount(); + var sum = metricPoint.GetHistogramSum(); + + Assert.Equal(1L, count); + Assert.True(sum > 0); + + var attributes = new KeyValuePair[metricPoint.Tags.Count]; + int i = 0; + foreach (var tag in metricPoint.Tags) + { + attributes[i++] = tag; + } + + // Inspect Attributes + Assert.Equal(expectedTagsCount, attributes.Length); + + var method = new KeyValuePair(SemanticConventions.AttributeHttpRequestMethod, "GET"); + var scheme = new KeyValuePair(SemanticConventions.AttributeUrlScheme, "http"); + var statusCode = new KeyValuePair(SemanticConventions.AttributeHttpResponseStatusCode, expectedStatusCode); + var flavor = new KeyValuePair(SemanticConventions.AttributeNetworkProtocolVersion, "1.1"); + var route = new KeyValuePair(SemanticConventions.AttributeHttpRoute, expectedRoute); + Assert.Contains(method, attributes); + Assert.Contains(scheme, attributes); + Assert.Contains(statusCode, attributes); + Assert.Contains(flavor, attributes); + Assert.Contains(route, attributes); + + if (expectedErrorType != null) + { + var errorType = new KeyValuePair(SemanticConventions.AttributeErrorType, expectedErrorType); + + Assert.Contains(errorType, attributes); + } + + // Inspect Histogram Bounds + var histogramBuckets = metricPoint.GetHistogramBuckets(); + var histogramBounds = new List(); + foreach (var t in histogramBuckets) + { + histogramBounds.Add(t.ExplicitBound); + } + + // TODO: Remove the check for the older bounds once 1.7.0 is released. This is a temporary fix for instrumentation libraries CI workflow. + + var expectedHistogramBoundsOld = new List { 0, 0.005, 0.01, 0.025, 0.05, 0.075, 0.1, 0.25, 0.5, 0.75, 1, 2.5, 5, 7.5, 10, double.PositiveInfinity }; + var expectedHistogramBoundsNew = new List { 0.005, 0.01, 0.025, 0.05, 0.075, 0.1, 0.25, 0.5, 0.75, 1, 2.5, 5, 7.5, 10, double.PositiveInfinity }; + + var histogramBoundsMatchCorrectly = Enumerable.SequenceEqual(expectedHistogramBoundsOld, histogramBounds) || + Enumerable.SequenceEqual(expectedHistogramBoundsNew, histogramBounds); + + Assert.True(histogramBoundsMatchCorrectly); + } +} diff --git a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/OpenTelemetry.Instrumentation.AspNetCore.Tests.csproj b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/OpenTelemetry.Instrumentation.AspNetCore.Tests.csproj new file mode 100644 index 0000000000..26722a5dd5 --- /dev/null +++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/OpenTelemetry.Instrumentation.AspNetCore.Tests.csproj @@ -0,0 +1,47 @@ + + + Unit test project for OpenTelemetry ASP.NET Core instrumentation + $(TargetFrameworksForAspNetCoreTests) + + disable + + + + + + + + + runtime; build; native; contentfiles; analyzers + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 0000000000..38ae9f93fd --- /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 0000000000..6582c75715 --- /dev/null +++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/README.net6.0.md @@ -0,0 +1,612 @@ +# Test results for ASP.NET Core 6 + +| http.route | App | Test Name | +| - | - | - | +| :broken_heart: | ConventionalRouting | [Root path](#conventionalrouting-root-path) | +| :broken_heart: | ConventionalRouting | [Non-default action with route parameter and query string](#conventionalrouting-non-default-action-with-route-parameter-and-query-string) | +| :broken_heart: | ConventionalRouting | [Non-default action with query string](#conventionalrouting-non-default-action-with-query-string) | +| :green_heart: | ConventionalRouting | [Not Found (404)](#conventionalrouting-not-found-404) | +| :green_heart: | ConventionalRouting | [Route template with parameter constraint](#conventionalrouting-route-template-with-parameter-constraint) | +| :green_heart: | ConventionalRouting | [Path that does not match parameter constraint](#conventionalrouting-path-that-does-not-match-parameter-constraint) | +| :broken_heart: | ConventionalRouting | [Area using `area:exists`, default controller/action](#conventionalrouting-area-using-areaexists-default-controlleraction) | +| :broken_heart: | ConventionalRouting | [Area using `area:exists`, non-default action](#conventionalrouting-area-using-areaexists-non-default-action) | +| :broken_heart: | ConventionalRouting | [Area w/o `area:exists`, default controller/action](#conventionalrouting-area-wo-areaexists-default-controlleraction) | +| :green_heart: | AttributeRouting | [Default action](#attributerouting-default-action) | +| :green_heart: | AttributeRouting | [Action without parameter](#attributerouting-action-without-parameter) | +| :green_heart: | AttributeRouting | [Action with parameter](#attributerouting-action-with-parameter) | +| :green_heart: | AttributeRouting | [Action with parameter before action name in template](#attributerouting-action-with-parameter-before-action-name-in-template) | +| :green_heart: | AttributeRouting | [Action invoked resulting in 400 Bad Request](#attributerouting-action-invoked-resulting-in-400-bad-request) | +| :broken_heart: | RazorPages | [Root path](#razorpages-root-path) | +| :broken_heart: | RazorPages | [Index page](#razorpages-index-page) | +| :broken_heart: | RazorPages | [Throws exception](#razorpages-throws-exception) | +| :green_heart: | RazorPages | [Static content](#razorpages-static-content) | +| :green_heart: | MinimalApi | [Action without parameter](#minimalapi-action-without-parameter) | +| :green_heart: | MinimalApi | [Action with parameter](#minimalapi-action-with-parameter) | +| :green_heart: | ExceptionMiddleware | [Exception Handled by Exception Handler Middleware](#exceptionmiddleware-exception-handled-by-exception-handler-middleware) | + +## ConventionalRouting: Root path + +```json +{ + "IdealHttpRoute": "ConventionalRoute/Default/{id?}", + "ActivityDisplayName": "GET {controller=ConventionalRoute}/{action=Default}/{id?}", + "ActivityHttpRoute": "{controller=ConventionalRoute}/{action=Default}/{id?}", + "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": "GET {controller=ConventionalRoute}/{action=Default}/{id?}", + "ActivityHttpRoute": "{controller=ConventionalRoute}/{action=Default}/{id?}", + "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": "GET {controller=ConventionalRoute}/{action=Default}/{id?}", + "ActivityHttpRoute": "{controller=ConventionalRoute}/{action=Default}/{id?}", + "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": "GET", + "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": "GET SomePath/{id}/{num:int}", + "ActivityHttpRoute": "SomePath/{id}/{num:int}", + "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": "GET", + "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": "GET {area:exists}/{controller=ControllerForMyArea}/{action=Default}/{id?}", + "ActivityHttpRoute": "{area:exists}/{controller=ControllerForMyArea}/{action=Default}/{id?}", + "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": "GET {area:exists}/{controller=ControllerForMyArea}/{action=Default}/{id?}", + "ActivityHttpRoute": "{area:exists}/{controller=ControllerForMyArea}/{action=Default}/{id?}", + "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": "GET SomePrefix/{controller=AnotherArea}/{action=Index}/{id?}", + "ActivityHttpRoute": "SomePrefix/{controller=AnotherArea}/{action=Index}/{id?}", + "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": "GET 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": "GET 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": "GET 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": "GET 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": "GET 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": "GET", + "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": "GET 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": "GET 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": "GET", + "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": "GET /MinimalApi", + "ActivityHttpRoute": "/MinimalApi", + "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": "GET /MinimalApi/{id}", + "ActivityHttpRoute": "/MinimalApi/{id}", + "MetricHttpRoute": "/MinimalApi/{id}", + "RouteInfo": { + "HttpMethod": "GET", + "Path": "/MinimalApi/123", + "RoutePattern.RawText": "/MinimalApi/{id}", + "IRouteDiagnosticsMetadata.Route": null, + "HttpContext.GetRouteData()": { + "id": "123" + }, + "ActionDescriptor": null + } +} +``` + +## ExceptionMiddleware: Exception Handled by Exception Handler Middleware + +```json +{ + "IdealHttpRoute": "/Exception", + "ActivityDisplayName": "GET /Exception", + "ActivityHttpRoute": "/Exception", + "MetricHttpRoute": "/Exception", + "RouteInfo": { + "HttpMethod": "GET", + "Path": "/Exception", + "RoutePattern.RawText": null, + "IRouteDiagnosticsMetadata.Route": null, + "HttpContext.GetRouteData()": {}, + "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 0000000000..49d8224155 --- /dev/null +++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/README.net7.0.md @@ -0,0 +1,654 @@ +# Test results for ASP.NET Core 7 + +| http.route | App | Test Name | +| - | - | - | +| :broken_heart: | ConventionalRouting | [Root path](#conventionalrouting-root-path) | +| :broken_heart: | ConventionalRouting | [Non-default action with route parameter and query string](#conventionalrouting-non-default-action-with-route-parameter-and-query-string) | +| :broken_heart: | ConventionalRouting | [Non-default action with query string](#conventionalrouting-non-default-action-with-query-string) | +| :green_heart: | ConventionalRouting | [Not Found (404)](#conventionalrouting-not-found-404) | +| :green_heart: | ConventionalRouting | [Route template with parameter constraint](#conventionalrouting-route-template-with-parameter-constraint) | +| :green_heart: | ConventionalRouting | [Path that does not match parameter constraint](#conventionalrouting-path-that-does-not-match-parameter-constraint) | +| :broken_heart: | ConventionalRouting | [Area using `area:exists`, default controller/action](#conventionalrouting-area-using-areaexists-default-controlleraction) | +| :broken_heart: | ConventionalRouting | [Area using `area:exists`, non-default action](#conventionalrouting-area-using-areaexists-non-default-action) | +| :broken_heart: | ConventionalRouting | [Area w/o `area:exists`, default controller/action](#conventionalrouting-area-wo-areaexists-default-controlleraction) | +| :green_heart: | AttributeRouting | [Default action](#attributerouting-default-action) | +| :green_heart: | AttributeRouting | [Action without parameter](#attributerouting-action-without-parameter) | +| :green_heart: | AttributeRouting | [Action with parameter](#attributerouting-action-with-parameter) | +| :green_heart: | AttributeRouting | [Action with parameter before action name in template](#attributerouting-action-with-parameter-before-action-name-in-template) | +| :green_heart: | AttributeRouting | [Action invoked resulting in 400 Bad Request](#attributerouting-action-invoked-resulting-in-400-bad-request) | +| :broken_heart: | RazorPages | [Root path](#razorpages-root-path) | +| :broken_heart: | RazorPages | [Index page](#razorpages-index-page) | +| :broken_heart: | RazorPages | [Throws exception](#razorpages-throws-exception) | +| :green_heart: | RazorPages | [Static content](#razorpages-static-content) | +| :green_heart: | MinimalApi | [Action without parameter](#minimalapi-action-without-parameter) | +| :green_heart: | MinimalApi | [Action with parameter](#minimalapi-action-with-parameter) | +| :green_heart: | MinimalApi | [Action without parameter (MapGroup)](#minimalapi-action-without-parameter-mapgroup) | +| :green_heart: | MinimalApi | [Action with parameter (MapGroup)](#minimalapi-action-with-parameter-mapgroup) | +| :green_heart: | ExceptionMiddleware | [Exception Handled by Exception Handler Middleware](#exceptionmiddleware-exception-handled-by-exception-handler-middleware) | + +## ConventionalRouting: Root path + +```json +{ + "IdealHttpRoute": "ConventionalRoute/Default/{id?}", + "ActivityDisplayName": "GET {controller=ConventionalRoute}/{action=Default}/{id?}", + "ActivityHttpRoute": "{controller=ConventionalRoute}/{action=Default}/{id?}", + "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": "GET {controller=ConventionalRoute}/{action=Default}/{id?}", + "ActivityHttpRoute": "{controller=ConventionalRoute}/{action=Default}/{id?}", + "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": "GET {controller=ConventionalRoute}/{action=Default}/{id?}", + "ActivityHttpRoute": "{controller=ConventionalRoute}/{action=Default}/{id?}", + "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": "GET", + "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": "GET SomePath/{id}/{num:int}", + "ActivityHttpRoute": "SomePath/{id}/{num:int}", + "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": "GET", + "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": "GET {area:exists}/{controller=ControllerForMyArea}/{action=Default}/{id?}", + "ActivityHttpRoute": "{area:exists}/{controller=ControllerForMyArea}/{action=Default}/{id?}", + "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": "GET {area:exists}/{controller=ControllerForMyArea}/{action=Default}/{id?}", + "ActivityHttpRoute": "{area:exists}/{controller=ControllerForMyArea}/{action=Default}/{id?}", + "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": "GET SomePrefix/{controller=AnotherArea}/{action=Index}/{id?}", + "ActivityHttpRoute": "SomePrefix/{controller=AnotherArea}/{action=Index}/{id?}", + "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": "GET 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": "GET 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": "GET 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": "GET 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": "GET 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": "GET", + "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": "GET 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": "GET 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": "GET", + "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": "GET /MinimalApi", + "ActivityHttpRoute": "/MinimalApi", + "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": "GET /MinimalApi/{id}", + "ActivityHttpRoute": "/MinimalApi/{id}", + "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": "GET /MinimalApiUsingMapGroup/", + "ActivityHttpRoute": "/MinimalApiUsingMapGroup/", + "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": "GET /MinimalApiUsingMapGroup/{id}", + "ActivityHttpRoute": "/MinimalApiUsingMapGroup/{id}", + "MetricHttpRoute": "/MinimalApiUsingMapGroup/{id}", + "RouteInfo": { + "HttpMethod": "GET", + "Path": "/MinimalApiUsingMapGroup/123", + "RoutePattern.RawText": "/MinimalApiUsingMapGroup/{id}", + "IRouteDiagnosticsMetadata.Route": null, + "HttpContext.GetRouteData()": { + "id": "123" + }, + "ActionDescriptor": null + } +} +``` + +## ExceptionMiddleware: Exception Handled by Exception Handler Middleware + +```json +{ + "IdealHttpRoute": "/Exception", + "ActivityDisplayName": "GET /Exception", + "ActivityHttpRoute": "/Exception", + "MetricHttpRoute": "/Exception", + "RouteInfo": { + "HttpMethod": "GET", + "Path": "/Exception", + "RoutePattern.RawText": null, + "IRouteDiagnosticsMetadata.Route": null, + "HttpContext.GetRouteData()": {}, + "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 0000000000..40b63a1ca4 --- /dev/null +++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/README.net8.0.md @@ -0,0 +1,654 @@ +# Test results for ASP.NET Core 8 + +| http.route | App | Test Name | +| - | - | - | +| :broken_heart: | ConventionalRouting | [Root path](#conventionalrouting-root-path) | +| :broken_heart: | ConventionalRouting | [Non-default action with route parameter and query string](#conventionalrouting-non-default-action-with-route-parameter-and-query-string) | +| :broken_heart: | ConventionalRouting | [Non-default action with query string](#conventionalrouting-non-default-action-with-query-string) | +| :green_heart: | ConventionalRouting | [Not Found (404)](#conventionalrouting-not-found-404) | +| :green_heart: | ConventionalRouting | [Route template with parameter constraint](#conventionalrouting-route-template-with-parameter-constraint) | +| :green_heart: | ConventionalRouting | [Path that does not match parameter constraint](#conventionalrouting-path-that-does-not-match-parameter-constraint) | +| :broken_heart: | ConventionalRouting | [Area using `area:exists`, default controller/action](#conventionalrouting-area-using-areaexists-default-controlleraction) | +| :broken_heart: | ConventionalRouting | [Area using `area:exists`, non-default action](#conventionalrouting-area-using-areaexists-non-default-action) | +| :broken_heart: | ConventionalRouting | [Area w/o `area:exists`, default controller/action](#conventionalrouting-area-wo-areaexists-default-controlleraction) | +| :green_heart: | AttributeRouting | [Default action](#attributerouting-default-action) | +| :green_heart: | AttributeRouting | [Action without parameter](#attributerouting-action-without-parameter) | +| :green_heart: | AttributeRouting | [Action with parameter](#attributerouting-action-with-parameter) | +| :green_heart: | AttributeRouting | [Action with parameter before action name in template](#attributerouting-action-with-parameter-before-action-name-in-template) | +| :green_heart: | AttributeRouting | [Action invoked resulting in 400 Bad Request](#attributerouting-action-invoked-resulting-in-400-bad-request) | +| :broken_heart: | RazorPages | [Root path](#razorpages-root-path) | +| :broken_heart: | RazorPages | [Index page](#razorpages-index-page) | +| :broken_heart: | RazorPages | [Throws exception](#razorpages-throws-exception) | +| :green_heart: | RazorPages | [Static content](#razorpages-static-content) | +| :green_heart: | MinimalApi | [Action without parameter](#minimalapi-action-without-parameter) | +| :green_heart: | MinimalApi | [Action with parameter](#minimalapi-action-with-parameter) | +| :green_heart: | MinimalApi | [Action without parameter (MapGroup)](#minimalapi-action-without-parameter-mapgroup) | +| :green_heart: | MinimalApi | [Action with parameter (MapGroup)](#minimalapi-action-with-parameter-mapgroup) | +| :green_heart: | ExceptionMiddleware | [Exception Handled by Exception Handler Middleware](#exceptionmiddleware-exception-handled-by-exception-handler-middleware) | + +## ConventionalRouting: Root path + +```json +{ + "IdealHttpRoute": "ConventionalRoute/Default/{id?}", + "ActivityDisplayName": "GET {controller=ConventionalRoute}/{action=Default}/{id?}", + "ActivityHttpRoute": "{controller=ConventionalRoute}/{action=Default}/{id?}", + "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": "GET {controller=ConventionalRoute}/{action=Default}/{id?}", + "ActivityHttpRoute": "{controller=ConventionalRoute}/{action=Default}/{id?}", + "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": "GET {controller=ConventionalRoute}/{action=Default}/{id?}", + "ActivityHttpRoute": "{controller=ConventionalRoute}/{action=Default}/{id?}", + "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": "GET", + "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": "GET SomePath/{id}/{num:int}", + "ActivityHttpRoute": "SomePath/{id}/{num:int}", + "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": "GET", + "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": "GET {area:exists}/{controller=ControllerForMyArea}/{action=Default}/{id?}", + "ActivityHttpRoute": "{area:exists}/{controller=ControllerForMyArea}/{action=Default}/{id?}", + "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": "GET {area:exists}/{controller=ControllerForMyArea}/{action=Default}/{id?}", + "ActivityHttpRoute": "{area:exists}/{controller=ControllerForMyArea}/{action=Default}/{id?}", + "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": "GET SomePrefix/{controller=AnotherArea}/{action=Index}/{id?}", + "ActivityHttpRoute": "SomePrefix/{controller=AnotherArea}/{action=Index}/{id?}", + "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": "GET 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": "GET 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": "GET 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": "GET 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": "GET 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": "GET", + "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": "GET 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": "GET 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": "GET", + "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": "GET /MinimalApi", + "ActivityHttpRoute": "/MinimalApi", + "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": "GET /MinimalApi/{id}", + "ActivityHttpRoute": "/MinimalApi/{id}", + "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": "GET /MinimalApiUsingMapGroup/", + "ActivityHttpRoute": "/MinimalApiUsingMapGroup/", + "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": "GET /MinimalApiUsingMapGroup/{id}", + "ActivityHttpRoute": "/MinimalApiUsingMapGroup/{id}", + "MetricHttpRoute": "/MinimalApiUsingMapGroup/{id}", + "RouteInfo": { + "HttpMethod": "GET", + "Path": "/MinimalApiUsingMapGroup/123", + "RoutePattern.RawText": "/MinimalApiUsingMapGroup/{id}", + "IRouteDiagnosticsMetadata.Route": "/MinimalApiUsingMapGroup/{id}", + "HttpContext.GetRouteData()": { + "id": "123" + }, + "ActionDescriptor": null + } +} +``` + +## ExceptionMiddleware: Exception Handled by Exception Handler Middleware + +```json +{ + "IdealHttpRoute": "/Exception", + "ActivityDisplayName": "GET /Exception", + "ActivityHttpRoute": "/Exception", + "MetricHttpRoute": "/Exception", + "RouteInfo": { + "HttpMethod": "GET", + "Path": "/Exception", + "RoutePattern.RawText": "/Exception", + "IRouteDiagnosticsMetadata.Route": "/Exception", + "HttpContext.GetRouteData()": {}, + "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 0000000000..a3ec381888 --- /dev/null +++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/RoutingTestCases.cs @@ -0,0 +1,69 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#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 }); + } + + 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? CurrentHttpRoute { 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 0000000000..2d1fa584ee --- /dev/null +++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/RoutingTestCases.json @@ -0,0 +1,211 @@ +[ + { + "name": "Root path", + "testApplicationScenario": "ConventionalRouting", + "httpMethod": "GET", + "path": "/", + "expectedStatusCode": 200, + "currentHttpRoute": "{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, + "currentHttpRoute": "{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, + "currentHttpRoute": "{controller=ConventionalRoute}/{action=Default}/{id?}", + "expectedHttpRoute": "ConventionalRoute/ActionWithStringParameter/{id?}" + }, + { + "name": "Not Found (404)", + "testApplicationScenario": "ConventionalRouting", + "httpMethod": "GET", + "path": "/ConventionalRoute/NotFound", + "expectedStatusCode": 404, + "currentHttpRoute": null, + "expectedHttpRoute": "" + }, + { + "name": "Route template with parameter constraint", + "testApplicationScenario": "ConventionalRouting", + "httpMethod": "GET", + "path": "/SomePath/SomeString/2", + "expectedStatusCode": 200, + "currentHttpRoute": null, + "expectedHttpRoute": "SomePath/{id}/{num:int}" + }, + { + "name": "Path that does not match parameter constraint", + "testApplicationScenario": "ConventionalRouting", + "httpMethod": "GET", + "path": "/SomePath/SomeString/NotAnInt", + "expectedStatusCode": 404, + "currentHttpRoute": null, + "expectedHttpRoute": "" + }, + { + "name": "Area using `area:exists`, default controller/action", + "testApplicationScenario": "ConventionalRouting", + "httpMethod": "GET", + "path": "/MyArea", + "expectedStatusCode": 200, + "currentHttpRoute": "{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, + "currentHttpRoute": "{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, + "currentHttpRoute": "SomePrefix/{controller=AnotherArea}/{action=Index}/{id?}", + "expectedHttpRoute": "SomePrefix/AnotherArea/Index/{id?}" + }, + { + "name": "Default action", + "testApplicationScenario": "AttributeRouting", + "httpMethod": "GET", + "path": "/AttributeRoute", + "expectedStatusCode": 200, + "currentHttpRoute": null, + "expectedHttpRoute": "AttributeRoute" + }, + { + "name": "Action without parameter", + "testApplicationScenario": "AttributeRouting", + "httpMethod": "GET", + "path": "/AttributeRoute/Get", + "expectedStatusCode": 200, + "currentHttpRoute": null, + "expectedHttpRoute": "AttributeRoute/Get" + }, + { + "name": "Action with parameter", + "testApplicationScenario": "AttributeRouting", + "httpMethod": "GET", + "path": "/AttributeRoute/Get/12", + "expectedStatusCode": 200, + "currentHttpRoute": null, + "expectedHttpRoute": "AttributeRoute/Get/{id}" + }, + { + "name": "Action with parameter before action name in template", + "testApplicationScenario": "AttributeRouting", + "httpMethod": "GET", + "path": "/AttributeRoute/12/GetWithActionNameInDifferentSpotInTemplate", + "expectedStatusCode": 200, + "currentHttpRoute": null, + "expectedHttpRoute": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate" + }, + { + "name": "Action invoked resulting in 400 Bad Request", + "testApplicationScenario": "AttributeRouting", + "httpMethod": "GET", + "path": "/AttributeRoute/NotAnInt/GetWithActionNameInDifferentSpotInTemplate", + "expectedStatusCode": 400, + "currentHttpRoute": null, + "expectedHttpRoute": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate" + }, + { + "name": "Root path", + "testApplicationScenario": "RazorPages", + "httpMethod": "GET", + "path": "/", + "expectedStatusCode": 200, + "currentHttpRoute": "", + "expectedHttpRoute": "/Index" + }, + { + "name": "Index page", + "testApplicationScenario": "RazorPages", + "httpMethod": "GET", + "path": "/Index", + "expectedStatusCode": 200, + "currentHttpRoute": "Index", + "expectedHttpRoute": "/Index" + }, + { + "name": "Throws exception", + "testApplicationScenario": "RazorPages", + "httpMethod": "GET", + "path": "/PageThatThrowsException", + "expectedStatusCode": 500, + "currentHttpRoute": "PageThatThrowsException", + "expectedHttpRoute": "/PageThatThrowsException" + }, + { + "name": "Static content", + "testApplicationScenario": "RazorPages", + "httpMethod": "GET", + "path": "/js/site.js", + "expectedStatusCode": 200, + "currentHttpRoute": null, + "expectedHttpRoute": "" + }, + { + "name": "Action without parameter", + "testApplicationScenario": "MinimalApi", + "httpMethod": "GET", + "path": "/MinimalApi", + "expectedStatusCode": 200, + "currentHttpRoute": null, + "expectedHttpRoute": "/MinimalApi" + }, + { + "name": "Action with parameter", + "testApplicationScenario": "MinimalApi", + "httpMethod": "GET", + "path": "/MinimalApi/123", + "expectedStatusCode": 200, + "currentHttpRoute": null, + "expectedHttpRoute": "/MinimalApi/{id}" + }, + { + "name": "Action without parameter (MapGroup)", + "minimumDotnetVersion": 7, + "testApplicationScenario": "MinimalApi", + "httpMethod": "GET", + "path": "/MinimalApiUsingMapGroup", + "expectedStatusCode": 200, + "currentHttpRoute": null, + "expectedHttpRoute": "/MinimalApiUsingMapGroup/" + }, + { + "name": "Action with parameter (MapGroup)", + "minimumDotnetVersion": 7, + "testApplicationScenario": "MinimalApi", + "httpMethod": "GET", + "path": "/MinimalApiUsingMapGroup/123", + "expectedStatusCode": 200, + "currentHttpRoute": null, + "expectedHttpRoute": "/MinimalApiUsingMapGroup/{id}" + }, + { + "name": "Exception Handled by Exception Handler Middleware", + "testApplicationScenario": "ExceptionMiddleware", + "httpMethod": "GET", + "path": "/Exception", + "expectedStatusCode": 500, + "currentHttpRoute": null, + "expectedHttpRoute": "/Exception" + } +] 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 0000000000..1b949e2674 --- /dev/null +++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/RoutingTestFixture.cs @@ -0,0 +1,108 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#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); + } + + 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("| http.route | App | Test Name |"); + sb.AppendLine("| - | - | - |"); + + for (var i = 0; i < this.testResults.Count; ++i) + { + var result = this.testResults[i]; + var emoji = result.TestCase.CurrentHttpRoute == null ? ":green_heart:" : ":broken_heart:"; + sb.AppendLine($"| {emoji} | {result.TestCase.TestApplicationScenario} | [{result.TestCase.Name}]({GenerateLinkFragment(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()); + + // Generates a link fragment that should comply with markdownlint rule MD051 + // https://github.com/DavidAnson/markdownlint/blob/main/doc/md051.md + static string GenerateLinkFragment(TestApplicationScenario scenario, string name) + { + var chars = name.ToCharArray() + .Where(c => (!char.IsPunctuation(c) && 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 0000000000..8217fa0987 --- /dev/null +++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/RoutingTestResult.cs @@ -0,0 +1,33 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#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 0000000000..6098aaa08b --- /dev/null +++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/RoutingTests.cs @@ -0,0 +1,139 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#nullable enable + +using System.Diagnostics; +using OpenTelemetry; +using OpenTelemetry.Metrics; +using OpenTelemetry.Trace; +using RouteTests.TestApplication; +using Xunit; + +namespace RouteTests; + +public class RoutingTests : IClassFixture +{ + 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) + { + using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddAspNetCoreInstrumentation() + .AddInMemoryExporter(this.exportedActivities) + .Build()!; + + using var meterProvider = Sdk.CreateMeterProviderBuilder() + .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)); + } + + 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(activity, out var activityHttpStatusCode, out var activityHttpMethod, out var activityHttpRoute); + GetTagsFromMetricPoint(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 CurrentHttpRoute property will go away. It They only serve to capture status quo. + // If CurrentHttpRoute is null, then that means we already conform to the correct behavior. + var expectedHttpRoute = testCase.CurrentHttpRoute != null ? testCase.CurrentHttpRoute : testCase.ExpectedHttpRoute; + Assert.Equal(expectedHttpRoute, activityHttpRoute); + Assert.Equal(expectedHttpRoute, metricHttpRoute); + + // 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(expectedHttpRoute) + ? testCase.HttpMethod + : $"{testCase.HttpMethod} {expectedHttpRoute}"; + + Assert.Equal(expectedActivityDisplayName, activity.DisplayName); + + 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(Activity activity, out int httpStatusCode, out string httpMethod, out string? httpRoute) + { + var expectedStatusCodeKey = HttpStatusCode; + var expectedHttpMethodKey = 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 = HttpStatusCode; + var expectedHttpMethodKey = 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 0000000000..7754255edf --- /dev/null +++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/TestApplication/Areas/AnotherArea/Controllers/AnotherAreaController.cs @@ -0,0 +1,14 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#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 0000000000..762f4a95e8 --- /dev/null +++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/TestApplication/Areas/MyArea/Controllers/ControllerForMyAreaController.cs @@ -0,0 +1,16 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#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 0000000000..b1e5783b0a --- /dev/null +++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/TestApplication/Controllers/AttributeRouteController.cs @@ -0,0 +1,23 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#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 0000000000..977ee36a13 --- /dev/null +++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/TestApplication/Controllers/ConventionalRouteController.cs @@ -0,0 +1,17 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#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 0000000000..51c350f956 --- /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 0000000000..cf6ac0d5b8 --- /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 0000000000..7858721039 --- /dev/null +++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/TestApplication/RouteInfo.cs @@ -0,0 +1,138 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#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 0000000000..3b3feb59e1 --- /dev/null +++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/TestApplication/RouteInfoDiagnosticObserver.cs @@ -0,0 +1,110 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#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 0000000000..b030ab7f42 --- /dev/null +++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/TestApplication/TestApplicationFactory.cs @@ -0,0 +1,199 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#nullable enable + +using System.Diagnostics; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Diagnostics; +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, + + /// + /// Application with Exception Handling Middleware. + /// + ExceptionMiddleware, +} + +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(); + case TestApplicationScenario.ExceptionMiddleware: + return CreateExceptionHandlerApplication(); + 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; + } + + private static WebApplication CreateExceptionHandlerApplication() + { + var builder = WebApplication.CreateBuilder(); + builder.Logging.ClearProviders(); + + var app = builder.Build(); + + app.UseExceptionHandler(exceptionHandlerApp => + { + exceptionHandlerApp.Run(async context => + { + context.Response.StatusCode = StatusCodes.Status500InternalServerError; + var exceptionHandlerPathFeature = context.Features.Get(); + await context.Response.WriteAsync(exceptionHandlerPathFeature?.Error.Message ?? "An exception was thrown."); + }); + }); + + app.Urls.Clear(); + app.Urls.Add("http://[::1]:0"); + + // TODO: Remove this condition once ASP.NET Core 8.0.2. + // Currently, .NET 8 has a different behavior than .NET 6 and 7. + // This is because ASP.NET Core 8+ has native metric instrumentation. + // When ASP.NET Core 8.0.2 is released then its behavior will align with .NET 6/7. + // See: https://github.com/dotnet/aspnetcore/issues/52648#issuecomment-1853432776 +#if !NET8_0_OR_GREATER + app.MapGet("/Exception", (ctx) => throw new ApplicationException()); +#else + app.MapGet("/Exception", () => Results.Content(content: "Error", contentType: null, contentEncoding: null, statusCode: 500)); +#endif + + 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 0000000000..0937657353 --- /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. diff --git a/test/TestApp.AspNetCore/ActivityMiddleware.cs b/test/TestApp.AspNetCore/ActivityMiddleware.cs new file mode 100644 index 0000000000..591bc388f3 --- /dev/null +++ b/test/TestApp.AspNetCore/ActivityMiddleware.cs @@ -0,0 +1,44 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +namespace TestApp.AspNetCore; + +public class ActivityMiddleware +{ + private readonly ActivityMiddlewareImpl impl; + private readonly RequestDelegate next; + + public ActivityMiddleware(RequestDelegate next, ActivityMiddlewareImpl impl) + { + this.next = next; + this.impl = impl; + } + + public async Task InvokeAsync(HttpContext context) + { + if (this.impl != null) + { + this.impl.PreProcess(context); + } + + await this.next(context); + + if (this.impl != null) + { + this.impl.PostProcess(context); + } + } + + public class ActivityMiddlewareImpl + { + public virtual void PreProcess(HttpContext context) + { + // Do nothing + } + + public virtual void PostProcess(HttpContext context) + { + // Do nothing + } + } +} diff --git a/test/TestApp.AspNetCore/CallbackMiddleware.cs b/test/TestApp.AspNetCore/CallbackMiddleware.cs new file mode 100644 index 0000000000..fee8569d5d --- /dev/null +++ b/test/TestApp.AspNetCore/CallbackMiddleware.cs @@ -0,0 +1,32 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +namespace TestApp.AspNetCore; + +public class CallbackMiddleware +{ + private readonly CallbackMiddlewareImpl impl; + private readonly RequestDelegate next; + + public CallbackMiddleware(RequestDelegate next, CallbackMiddlewareImpl impl) + { + this.next = next; + this.impl = impl; + } + + public async Task InvokeAsync(HttpContext context) + { + if (this.impl == null || await this.impl.ProcessAsync(context)) + { + await this.next(context); + } + } + + public class CallbackMiddlewareImpl + { + public virtual async Task ProcessAsync(HttpContext context) + { + return await Task.FromResult(true); + } + } +} diff --git a/test/TestApp.AspNetCore/Controllers/ChildActivityController.cs b/test/TestApp.AspNetCore/Controllers/ChildActivityController.cs new file mode 100644 index 0000000000..b55927000f --- /dev/null +++ b/test/TestApp.AspNetCore/Controllers/ChildActivityController.cs @@ -0,0 +1,47 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using System.Diagnostics; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Mvc; +using OpenTelemetry; + +namespace TestApp.AspNetCore.Controllers; + +public class ChildActivityController : Controller +{ + [HttpGet] + [Route("api/GetChildActivityTraceContext")] + public Dictionary GetChildActivityTraceContext() + { + var result = new Dictionary(); + var activity = new Activity("ActivityInsideHttpRequest"); + activity.Start(); + result["TraceId"] = activity.Context.TraceId.ToString(); + result["ParentSpanId"] = activity.ParentSpanId.ToString(); + if (activity.Context.TraceState != null) + { + result["TraceState"] = activity.Context.TraceState; + } + + activity.Stop(); + return result; + } + + [HttpGet] + [Route("api/GetChildActivityBaggageContext")] + public IReadOnlyDictionary GetChildActivityBaggageContext() + { + var result = Baggage.Current.GetBaggage(); + return result; + } + + [HttpGet] + [Route("api/GetActivityEquality")] + public bool GetActivityEquality() + { + var activity = this.HttpContext.Features.Get()?.Activity; + var equal = Activity.Current == activity; + return equal; + } +} diff --git a/test/TestApp.AspNetCore/Controllers/ErrorController.cs b/test/TestApp.AspNetCore/Controllers/ErrorController.cs new file mode 100644 index 0000000000..24c904cfe9 --- /dev/null +++ b/test/TestApp.AspNetCore/Controllers/ErrorController.cs @@ -0,0 +1,17 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using Microsoft.AspNetCore.Mvc; + +namespace TestApp.AspNetCore.Controllers; + +[Route("api/[controller]")] +public class ErrorController : Controller +{ + // GET api/error + [HttpGet] + public string Get() + { + throw new Exception("something's wrong!"); + } +} diff --git a/test/TestApp.AspNetCore/Controllers/ValuesController.cs b/test/TestApp.AspNetCore/Controllers/ValuesController.cs new file mode 100644 index 0000000000..27a9ab0d2d --- /dev/null +++ b/test/TestApp.AspNetCore/Controllers/ValuesController.cs @@ -0,0 +1,42 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using Microsoft.AspNetCore.Mvc; + +namespace TestApp.AspNetCore.Controllers; + +[Route("api/[controller]")] +public class ValuesController : Controller +{ + // GET api/values + [HttpGet] + public IEnumerable Get() + { + return new string[] { "value1", "value2" }; + } + + // GET api/values/5 + [HttpGet("{id}")] + public string Get(int id) + { + return "value"; + } + + // POST api/values + [HttpPost] + public void Post([FromBody] string value) + { + } + + // PUT api/values/5 + [HttpPut("{id}")] + public void Put(int id, [FromBody] string value) + { + } + + // DELETE api/values/5 + [HttpDelete("{id}")] + public void Delete(int id) + { + } +} diff --git a/test/TestApp.AspNetCore/Filters/ExceptionFilter1.cs b/test/TestApp.AspNetCore/Filters/ExceptionFilter1.cs new file mode 100644 index 0000000000..1f05886069 --- /dev/null +++ b/test/TestApp.AspNetCore/Filters/ExceptionFilter1.cs @@ -0,0 +1,14 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using Microsoft.AspNetCore.Mvc.Filters; + +namespace TestApp.AspNetCore.Filters; + +public class ExceptionFilter1 : IExceptionFilter +{ + public void OnException(ExceptionContext context) + { + // test the behaviour when an application has two ExceptionFilters defined + } +} diff --git a/test/TestApp.AspNetCore/Filters/ExceptionFilter2.cs b/test/TestApp.AspNetCore/Filters/ExceptionFilter2.cs new file mode 100644 index 0000000000..fc81905c65 --- /dev/null +++ b/test/TestApp.AspNetCore/Filters/ExceptionFilter2.cs @@ -0,0 +1,14 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using Microsoft.AspNetCore.Mvc.Filters; + +namespace TestApp.AspNetCore.Filters; + +public class ExceptionFilter2 : IExceptionFilter +{ + public void OnException(ExceptionContext context) + { + // test the behaviour when an application has two ExceptionFilters defined + } +} diff --git a/test/TestApp.AspNetCore/Program.cs b/test/TestApp.AspNetCore/Program.cs new file mode 100644 index 0000000000..06071eab4b --- /dev/null +++ b/test/TestApp.AspNetCore/Program.cs @@ -0,0 +1,54 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using TestApp.AspNetCore; + +public class Program +{ + public static void Main(string[] args) + { + var builder = WebApplication.CreateBuilder(args); + + // Add services to the container. + + builder.Services.AddControllers(); + + // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle + builder.Services.AddEndpointsApiExplorer(); + + builder.Services.AddSwaggerGen(); + + builder.Services.AddMvc(); + + builder.Services.AddSingleton(); + + builder.Services.AddSingleton( + new CallbackMiddleware.CallbackMiddlewareImpl()); + + builder.Services.AddSingleton( + new ActivityMiddleware.ActivityMiddlewareImpl()); + + var app = builder.Build(); + + // Configure the HTTP request pipeline. + if (app.Environment.IsDevelopment()) + { + app.UseSwagger(); + app.UseSwaggerUI(); + } + + app.UseHttpsRedirection(); + + app.UseAuthorization(); + + app.MapControllers(); + + app.UseMiddleware(); + + app.UseMiddleware(); + + app.AddTestMiddleware(); + + app.Run(); + } +} diff --git a/test/TestApp.AspNetCore/Properties/launchSettings.json b/test/TestApp.AspNetCore/Properties/launchSettings.json new file mode 100644 index 0000000000..f627182e41 --- /dev/null +++ b/test/TestApp.AspNetCore/Properties/launchSettings.json @@ -0,0 +1,12 @@ +{ + "profiles": { + "TestApp.AspNetCore": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "https://localhost:58211;http://localhost:58212" + } + } +} \ No newline at end of file diff --git a/test/TestApp.AspNetCore/TestApp.AspNetCore.csproj b/test/TestApp.AspNetCore/TestApp.AspNetCore.csproj new file mode 100644 index 0000000000..93a3a0a972 --- /dev/null +++ b/test/TestApp.AspNetCore/TestApp.AspNetCore.csproj @@ -0,0 +1,19 @@ + + + + $(TargetFrameworksForAspNetCoreTests) + + + + + + + + + + + + + + + diff --git a/test/TestApp.AspNetCore/TestMiddleware.cs b/test/TestApp.AspNetCore/TestMiddleware.cs new file mode 100644 index 0000000000..39acf58db3 --- /dev/null +++ b/test/TestApp.AspNetCore/TestMiddleware.cs @@ -0,0 +1,24 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +namespace TestApp.AspNetCore; + +public static class TestMiddleware +{ + private static readonly AsyncLocal?> Current = new(); + + public static IApplicationBuilder AddTestMiddleware(this IApplicationBuilder builder) + { + if (Current.Value is { } configure) + { + configure(builder); + } + + return builder; + } + + public static void Create(Action action) + { + Current.Value = action; + } +} diff --git a/test/TestApp.AspNetCore/appsettings.Development.json b/test/TestApp.AspNetCore/appsettings.Development.json new file mode 100644 index 0000000000..0c208ae918 --- /dev/null +++ b/test/TestApp.AspNetCore/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/test/TestApp.AspNetCore/appsettings.json b/test/TestApp.AspNetCore/appsettings.json new file mode 100644 index 0000000000..10f68b8c8b --- /dev/null +++ b/test/TestApp.AspNetCore/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} From b04febb9c25d93eb92b55770e4bf709401127ccb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Kie=C5=82kowicz?= Date: Tue, 7 May 2024 09:23:43 +0200 Subject: [PATCH 02/26] include projects into solution --- opentelemetry-dotnet-contrib.sln | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/opentelemetry-dotnet-contrib.sln b/opentelemetry-dotnet-contrib.sln index 1830e85346..34bd25ab17 100644 --- a/opentelemetry-dotnet-contrib.sln +++ b/opentelemetry-dotnet-contrib.sln @@ -369,6 +369,12 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenTelemetry.Instrumentati EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenTelemetry.Instrumentation.GrpcNetClient.Tests", "test\OpenTelemetry.Instrumentation.GrpcNetClient.Tests\OpenTelemetry.Instrumentation.GrpcNetClient.Tests.csproj", "{2E1A5759-1431-4724-8885-3E9447FBF617}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenTelemetry.Instrumentation.AspNetCore", "src\OpenTelemetry.Instrumentation.AspNetCore\OpenTelemetry.Instrumentation.AspNetCore.csproj", "{A8FF0DEB-F371-42FC-8A53-A8C25FE408FC}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenTelemetry.Instrumentation.AspNetCore.Tests", "test\OpenTelemetry.Instrumentation.AspNetCore.Tests\OpenTelemetry.Instrumentation.AspNetCore.Tests.csproj", "{917AEC46-816C-4E05-913E-F0F44C24C437}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestApp.AspNetCore", "test\TestApp.AspNetCore\TestApp.AspNetCore.csproj", "{1E743561-B1D4-4100-B6AD-1FD25FA8659B}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -747,6 +753,18 @@ Global {2E1A5759-1431-4724-8885-3E9447FBF617}.Debug|Any CPU.Build.0 = Debug|Any CPU {2E1A5759-1431-4724-8885-3E9447FBF617}.Release|Any CPU.ActiveCfg = Release|Any CPU {2E1A5759-1431-4724-8885-3E9447FBF617}.Release|Any CPU.Build.0 = Release|Any CPU + {A8FF0DEB-F371-42FC-8A53-A8C25FE408FC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A8FF0DEB-F371-42FC-8A53-A8C25FE408FC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A8FF0DEB-F371-42FC-8A53-A8C25FE408FC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A8FF0DEB-F371-42FC-8A53-A8C25FE408FC}.Release|Any CPU.Build.0 = Release|Any CPU + {917AEC46-816C-4E05-913E-F0F44C24C437}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {917AEC46-816C-4E05-913E-F0F44C24C437}.Debug|Any CPU.Build.0 = Debug|Any CPU + {917AEC46-816C-4E05-913E-F0F44C24C437}.Release|Any CPU.ActiveCfg = Release|Any CPU + {917AEC46-816C-4E05-913E-F0F44C24C437}.Release|Any CPU.Build.0 = Release|Any CPU + {1E743561-B1D4-4100-B6AD-1FD25FA8659B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1E743561-B1D4-4100-B6AD-1FD25FA8659B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1E743561-B1D4-4100-B6AD-1FD25FA8659B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1E743561-B1D4-4100-B6AD-1FD25FA8659B}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -858,6 +876,9 @@ Global {1156D564-2E3C-47D6-97C1-FF3ADEDC41C8} = {2097345F-4DD3-477D-BC54-A922F9B2B402} {0156E342-CE63-46F5-992D-691A7CCB50F8} = {22DF5DC0-1290-4E83-A9D8-6BB7DE3B3E63} {2E1A5759-1431-4724-8885-3E9447FBF617} = {2097345F-4DD3-477D-BC54-A922F9B2B402} + {A8FF0DEB-F371-42FC-8A53-A8C25FE408FC} = {22DF5DC0-1290-4E83-A9D8-6BB7DE3B3E63} + {917AEC46-816C-4E05-913E-F0F44C24C437} = {2097345F-4DD3-477D-BC54-A922F9B2B402} + {1E743561-B1D4-4100-B6AD-1FD25FA8659B} = {2097345F-4DD3-477D-BC54-A922F9B2B402} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {B0816796-CDB3-47D7-8C3C-946434DE3B66} From c7589dd76934e13ac9c60fa0525933d5b06dad80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Kie=C5=82kowicz?= Date: Tue, 7 May 2024 09:29:17 +0200 Subject: [PATCH 03/26] Build fixes --- ...mentationMeterProviderBuilderExtensions.cs | 3 +- ...entationTracerProviderBuilderExtensions.cs | 2 + .../Implementation/HttpInListener.cs | 4 +- .../Implementation/HttpInMetricsListener.cs | 1 - ...elemetry.Instrumentation.AspNetCore.csproj | 38 +++++++++------- src/Shared/ActivityHelperExtensions.cs | 43 +++++++++++++++++- ...ry.Instrumentation.AspNetCore.Tests.csproj | 45 ++++++++++--------- .../TestApp.AspNetCore.csproj | 14 ++---- 8 files changed, 95 insertions(+), 55 deletions(-) diff --git a/src/OpenTelemetry.Instrumentation.AspNetCore/AspNetCoreInstrumentationMeterProviderBuilderExtensions.cs b/src/OpenTelemetry.Instrumentation.AspNetCore/AspNetCoreInstrumentationMeterProviderBuilderExtensions.cs index 3da8c1de59..dbd7664abc 100644 --- a/src/OpenTelemetry.Instrumentation.AspNetCore/AspNetCoreInstrumentationMeterProviderBuilderExtensions.cs +++ b/src/OpenTelemetry.Instrumentation.AspNetCore/AspNetCoreInstrumentationMeterProviderBuilderExtensions.cs @@ -29,11 +29,12 @@ public static MeterProviderBuilder AddAspNetCoreInstrumentation( #else // Note: Warm-up the status code and method mapping. _ = TelemetryHelper.BoxedStatusCodes; - _ = RequestMethodHelper.KnownMethods; builder.AddMeter(HttpInMetricsListener.InstrumentationName); +#pragma warning disable CA2000 builder.AddInstrumentation(new AspNetCoreMetrics()); +#pragma warning restore CA2000 return builder; #endif diff --git a/src/OpenTelemetry.Instrumentation.AspNetCore/AspNetCoreInstrumentationTracerProviderBuilderExtensions.cs b/src/OpenTelemetry.Instrumentation.AspNetCore/AspNetCoreInstrumentationTracerProviderBuilderExtensions.cs index 8ec35718cc..0de402f070 100644 --- a/src/OpenTelemetry.Instrumentation.AspNetCore/AspNetCoreInstrumentationTracerProviderBuilderExtensions.cs +++ b/src/OpenTelemetry.Instrumentation.AspNetCore/AspNetCoreInstrumentationTracerProviderBuilderExtensions.cs @@ -88,8 +88,10 @@ internal static TracerProviderBuilder AddAspNetCoreInstrumentation( { builder.AddAspNetCoreInstrumentationSources(); +#pragma warning disable CA2000 return builder.AddInstrumentation( new AspNetCoreInstrumentation(listener)); +#pragma warning restore CA2000 } private static void AddAspNetCoreInstrumentationSources( diff --git a/src/OpenTelemetry.Instrumentation.AspNetCore/Implementation/HttpInListener.cs b/src/OpenTelemetry.Instrumentation.AspNetCore/Implementation/HttpInListener.cs index 7b5942a108..db0767841c 100644 --- a/src/OpenTelemetry.Instrumentation.AspNetCore/Implementation/HttpInListener.cs +++ b/src/OpenTelemetry.Instrumentation.AspNetCore/Implementation/HttpInListener.cs @@ -28,7 +28,7 @@ internal class HttpInListener : ListenerHandler internal const string OnUnHandledDiagnosticsExceptionEvent = "Microsoft.AspNetCore.Diagnostics.UnhandledException"; // https://github.com/dotnet/aspnetcore/blob/8d6554e655b64da75b71e0e20d6db54a3ba8d2fb/src/Hosting/Hosting/src/GenericHost/GenericWebHostBuilder.cs#L85 - internal static readonly string AspNetCoreActivitySourceName = "Microsoft.AspNetCore"; + internal const string AspNetCoreActivitySourceName = "Microsoft.AspNetCore"; internal static readonly AssemblyName AssemblyName = typeof(HttpInListener).Assembly.GetName(); internal static readonly string ActivitySourceName = AssemblyName.Name; @@ -261,7 +261,7 @@ public void OnStopActivity(Activity activity, object payload) if (activity.Status == ActivityStatusCode.Unset) { - activity.SetStatus(SpanHelper.ResolveSpanStatusForHttpStatusCode(activity.Kind, response.StatusCode)); + activity.SetStatus(SpanHelper.ResolveActivityStatusForHttpStatusCode(activity.Kind, response.StatusCode)); } try diff --git a/src/OpenTelemetry.Instrumentation.AspNetCore/Implementation/HttpInMetricsListener.cs b/src/OpenTelemetry.Instrumentation.AspNetCore/Implementation/HttpInMetricsListener.cs index e41cd5dc25..d2f1a58c11 100644 --- a/src/OpenTelemetry.Instrumentation.AspNetCore/Implementation/HttpInMetricsListener.cs +++ b/src/OpenTelemetry.Instrumentation.AspNetCore/Implementation/HttpInMetricsListener.cs @@ -5,7 +5,6 @@ using System.Diagnostics.Metrics; using System.Reflection; using Microsoft.AspNetCore.Http; -using OpenTelemetry.Internal; #if NET6_0_OR_GREATER using System.Diagnostics.CodeAnalysis; diff --git a/src/OpenTelemetry.Instrumentation.AspNetCore/OpenTelemetry.Instrumentation.AspNetCore.csproj b/src/OpenTelemetry.Instrumentation.AspNetCore/OpenTelemetry.Instrumentation.AspNetCore.csproj index 15aa86638d..c415de0ab1 100644 --- a/src/OpenTelemetry.Instrumentation.AspNetCore/OpenTelemetry.Instrumentation.AspNetCore.csproj +++ b/src/OpenTelemetry.Instrumentation.AspNetCore/OpenTelemetry.Instrumentation.AspNetCore.csproj @@ -1,13 +1,13 @@ - $(TargetFrameworksForAspNetCoreInstrumentation) + net8.0;net7.0;net6.0;netstandard2.0 ASP.NET Core instrumentation for OpenTelemetry .NET $(PackageTags);distributed-tracing;AspNetCore Instrumentation.AspNetCore- - true 1.8.1 + enable disable @@ -17,28 +17,32 @@ + + + + + - - - + + + + + + + - - - - - - - + + - - - - - + + + + + diff --git a/src/Shared/ActivityHelperExtensions.cs b/src/Shared/ActivityHelperExtensions.cs index 6a85d45527..7425c97edc 100644 --- a/src/Shared/ActivityHelperExtensions.cs +++ b/src/Shared/ActivityHelperExtensions.cs @@ -4,14 +4,24 @@ #nullable enable using System.Diagnostics; +using System.Runtime.CompilerServices; namespace OpenTelemetry.Trace; internal static class ActivityHelperExtensions { - public static object? GetTagValue(this Activity activity, string tagName) + /// + /// Gets the value of a specific tag on an . + /// + /// Activity instance. + /// Case-sensitive tag name to retrieve. + /// Tag value or null if a match was not found. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static object? GetTagValue(this Activity activity, string? tagName) { - foreach (var tag in activity.TagObjects) + Debug.Assert(activity != null, "Activity should not be null"); + + foreach (ref readonly var tag in activity!.EnumerateTagObjects()) { if (tag.Key == tagName) { @@ -21,4 +31,33 @@ internal static class ActivityHelperExtensions return null; } + + /// + /// Checks if the user provided tag name is the first tag of the and retrieves the tag value. + /// + /// Activity instance. + /// Tag name. + /// Tag value. + /// if the first tag of the supplied Activity matches the user provide tag name. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool TryCheckFirstTag(this Activity activity, string tagName, out object? tagValue) + { + Debug.Assert(activity != null, "Activity should not be null"); + + var enumerator = activity!.EnumerateTagObjects(); + + if (enumerator.MoveNext()) + { + ref readonly var tag = ref enumerator.Current; + + if (tag.Key == tagName) + { + tagValue = tag.Value; + return true; + } + } + + tagValue = null; + return false; + } } 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 26722a5dd5..4371b4f6ab 100644 --- a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/OpenTelemetry.Instrumentation.AspNetCore.Tests.csproj +++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/OpenTelemetry.Instrumentation.AspNetCore.Tests.csproj @@ -1,41 +1,42 @@ Unit test project for OpenTelemetry ASP.NET Core instrumentation - $(TargetFrameworksForAspNetCoreTests) + net8.0;net7.0;net6.0 + enable disable - - - - - - - runtime; build; native; contentfiles; analyzers - + + + - - - + + + + + + + + - - - + + + - - - + + + - - - - + + + + diff --git a/test/TestApp.AspNetCore/TestApp.AspNetCore.csproj b/test/TestApp.AspNetCore/TestApp.AspNetCore.csproj index 93a3a0a972..ce4a4b6852 100644 --- a/test/TestApp.AspNetCore/TestApp.AspNetCore.csproj +++ b/test/TestApp.AspNetCore/TestApp.AspNetCore.csproj @@ -1,19 +1,13 @@ - $(TargetFrameworksForAspNetCoreTests) + net8.0;net7.0;net6.0 + enable - - - - - - - - - + + From c0310cb9aa7aac6a62b8ffe8b51916ec39f7dbc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Kie=C5=82kowicz?= Date: Tue, 7 May 2024 11:12:54 +0200 Subject: [PATCH 04/26] Fix CA1810 --- .../Implementation/TelemetryHelper.cs | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/OpenTelemetry.Instrumentation.AspNetCore/Implementation/TelemetryHelper.cs b/src/OpenTelemetry.Instrumentation.AspNetCore/Implementation/TelemetryHelper.cs index 6f2e1fae8e..f8ad607d86 100644 --- a/src/OpenTelemetry.Instrumentation.AspNetCore/Implementation/TelemetryHelper.cs +++ b/src/OpenTelemetry.Instrumentation.AspNetCore/Implementation/TelemetryHelper.cs @@ -5,24 +5,26 @@ namespace OpenTelemetry.Instrumentation.AspNetCore.Implementation; internal static class TelemetryHelper { - public static readonly object[] BoxedStatusCodes; + public static readonly object[] BoxedStatusCodes = InitializeBoxedStatusCodes(); - static TelemetryHelper() + public static object GetBoxedStatusCode(int statusCode) { - BoxedStatusCodes = new object[500]; - for (int i = 0, c = 100; i < BoxedStatusCodes.Length; i++, c++) + if (statusCode >= 100 && statusCode < 600) { - BoxedStatusCodes[i] = c; + return BoxedStatusCodes[statusCode - 100]; } + + return statusCode; } - public static object GetBoxedStatusCode(int statusCode) + private static object[] InitializeBoxedStatusCodes() { - if (statusCode >= 100 && statusCode < 600) + var boxedStatusCodes = new object[500]; + for (int i = 0, c = 100; i < boxedStatusCodes.Length; i++, c++) { - return BoxedStatusCodes[statusCode - 100]; + boxedStatusCodes[i] = c; } - return statusCode; + return boxedStatusCodes; } } From d5cabfaf42cc1381a1d7bf023f302c593206d7c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Kie=C5=82kowicz?= Date: Tue, 7 May 2024 11:56:38 +0200 Subject: [PATCH 05/26] Use shared version of RequestDataHelper --- ...mentationMeterProviderBuilderExtensions.cs | 1 + ...entationTracerProviderBuilderExtensions.cs | 2 +- .../Implementation/HttpInListener.cs | 8 ++--- .../Implementation/HttpInMetricsListener.cs | 5 +-- .../Implementation/HttpTagHelper.cs | 33 ------------------- .../Implementation/TelemetryHelper.cs | 3 ++ 6 files changed, 12 insertions(+), 40 deletions(-) delete mode 100644 src/OpenTelemetry.Instrumentation.AspNetCore/Implementation/HttpTagHelper.cs diff --git a/src/OpenTelemetry.Instrumentation.AspNetCore/AspNetCoreInstrumentationMeterProviderBuilderExtensions.cs b/src/OpenTelemetry.Instrumentation.AspNetCore/AspNetCoreInstrumentationMeterProviderBuilderExtensions.cs index dbd7664abc..a9d7ef7c8a 100644 --- a/src/OpenTelemetry.Instrumentation.AspNetCore/AspNetCoreInstrumentationMeterProviderBuilderExtensions.cs +++ b/src/OpenTelemetry.Instrumentation.AspNetCore/AspNetCoreInstrumentationMeterProviderBuilderExtensions.cs @@ -29,6 +29,7 @@ public static MeterProviderBuilder AddAspNetCoreInstrumentation( #else // Note: Warm-up the status code and method mapping. _ = TelemetryHelper.BoxedStatusCodes; + _ = TelemetryHelper.RequestDataHelper; builder.AddMeter(HttpInMetricsListener.InstrumentationName); diff --git a/src/OpenTelemetry.Instrumentation.AspNetCore/AspNetCoreInstrumentationTracerProviderBuilderExtensions.cs b/src/OpenTelemetry.Instrumentation.AspNetCore/AspNetCoreInstrumentationTracerProviderBuilderExtensions.cs index 0de402f070..00fa0d9435 100644 --- a/src/OpenTelemetry.Instrumentation.AspNetCore/AspNetCoreInstrumentationTracerProviderBuilderExtensions.cs +++ b/src/OpenTelemetry.Instrumentation.AspNetCore/AspNetCoreInstrumentationTracerProviderBuilderExtensions.cs @@ -50,7 +50,7 @@ public static TracerProviderBuilder AddAspNetCoreInstrumentation( // Note: Warm-up the status code and method mapping. _ = TelemetryHelper.BoxedStatusCodes; - _ = RequestMethodHelper.KnownMethods; + _ = TelemetryHelper.RequestDataHelper; name ??= Options.DefaultName; diff --git a/src/OpenTelemetry.Instrumentation.AspNetCore/Implementation/HttpInListener.cs b/src/OpenTelemetry.Instrumentation.AspNetCore/Implementation/HttpInListener.cs index db0767841c..ebadf377e6 100644 --- a/src/OpenTelemetry.Instrumentation.AspNetCore/Implementation/HttpInListener.cs +++ b/src/OpenTelemetry.Instrumentation.AspNetCore/Implementation/HttpInListener.cs @@ -177,7 +177,7 @@ public void OnStartActivity(Activity activity, object payload) } var path = (request.PathBase.HasValue || request.Path.HasValue) ? (request.PathBase + request.Path).ToString() : "/"; - RequestMethodHelper.SetActivityDisplayName(activity, request.Method); + TelemetryHelper.RequestDataHelper.SetActivityDisplayName(activity, request.Method); // see the spec https://github.com/open-telemetry/semantic-conventions/blob/v1.23.0/docs/http/http-spans.md @@ -203,11 +203,11 @@ public void OnStartActivity(Activity activity, object payload) } } - RequestMethodHelper.SetHttpMethodTag(activity, request.Method); + TelemetryHelper.RequestDataHelper.SetHttpMethodTag(activity, request.Method); activity.SetTag(SemanticConventions.AttributeUrlScheme, request.Scheme); activity.SetTag(SemanticConventions.AttributeUrlPath, path); - activity.SetTag(SemanticConventions.AttributeNetworkProtocolVersion, HttpTagHelper.GetFlavorTagValueFromProtocol(request.Protocol)); + activity.SetTag(SemanticConventions.AttributeNetworkProtocolVersion, RequestDataHelper.GetHttpProtocolVersion(request.Protocol)); if (request.Headers.TryGetValue("User-Agent", out var values)) { @@ -247,7 +247,7 @@ public void OnStopActivity(Activity activity, object payload) context.GetEndpoint() as RouteEndpoint)?.RoutePattern.RawText; if (!string.IsNullOrEmpty(routePattern)) { - RequestMethodHelper.SetActivityDisplayName(activity, context.Request.Method, routePattern); + TelemetryHelper.RequestDataHelper.SetActivityDisplayName(activity, context.Request.Method, routePattern); activity.SetTag(SemanticConventions.AttributeHttpRoute, routePattern); } #endif diff --git a/src/OpenTelemetry.Instrumentation.AspNetCore/Implementation/HttpInMetricsListener.cs b/src/OpenTelemetry.Instrumentation.AspNetCore/Implementation/HttpInMetricsListener.cs index d2f1a58c11..cf9682d1fa 100644 --- a/src/OpenTelemetry.Instrumentation.AspNetCore/Implementation/HttpInMetricsListener.cs +++ b/src/OpenTelemetry.Instrumentation.AspNetCore/Implementation/HttpInMetricsListener.cs @@ -5,6 +5,7 @@ using System.Diagnostics.Metrics; using System.Reflection; using Microsoft.AspNetCore.Http; +using OpenTelemetry.Internal; #if NET6_0_OR_GREATER using System.Diagnostics.CodeAnalysis; @@ -78,11 +79,11 @@ public static void OnStopEventWritten(string name, object payload) TagList tags = default; // see the spec https://github.com/open-telemetry/semantic-conventions/blob/v1.21.0/docs/http/http-spans.md - tags.Add(new KeyValuePair(SemanticConventions.AttributeNetworkProtocolVersion, HttpTagHelper.GetFlavorTagValueFromProtocol(context.Request.Protocol))); + tags.Add(new KeyValuePair(SemanticConventions.AttributeNetworkProtocolVersion, RequestDataHelper.GetHttpProtocolVersion(context.Request.Protocol))); tags.Add(new KeyValuePair(SemanticConventions.AttributeUrlScheme, context.Request.Scheme)); tags.Add(new KeyValuePair(SemanticConventions.AttributeHttpResponseStatusCode, TelemetryHelper.GetBoxedStatusCode(context.Response.StatusCode))); - var httpMethod = RequestMethodHelper.GetNormalizedHttpMethod(context.Request.Method); + var httpMethod = TelemetryHelper.RequestDataHelper.GetNormalizedHttpMethod(context.Request.Method); tags.Add(new KeyValuePair(SemanticConventions.AttributeHttpRequestMethod, httpMethod)); #if NET6_0_OR_GREATER diff --git a/src/OpenTelemetry.Instrumentation.AspNetCore/Implementation/HttpTagHelper.cs b/src/OpenTelemetry.Instrumentation.AspNetCore/Implementation/HttpTagHelper.cs deleted file mode 100644 index 90f37eba3c..0000000000 --- a/src/OpenTelemetry.Instrumentation.AspNetCore/Implementation/HttpTagHelper.cs +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright The OpenTelemetry Authors -// SPDX-License-Identifier: Apache-2.0 - -namespace OpenTelemetry.Instrumentation.AspNetCore.Implementation; - -/// -/// A collection of helper methods to be used when building Http activities. -/// -internal static class HttpTagHelper -{ - /// - /// Gets the OpenTelemetry standard version tag value for a span based on its protocol/>. - /// - /// . - /// Span flavor value. - public static string GetFlavorTagValueFromProtocol(string protocol) - { - switch (protocol) - { - case "HTTP/2": - return "2"; - - case "HTTP/3": - return "3"; - - case "HTTP/1.1": - return "1.1"; - - default: - return protocol; - } - } -} diff --git a/src/OpenTelemetry.Instrumentation.AspNetCore/Implementation/TelemetryHelper.cs b/src/OpenTelemetry.Instrumentation.AspNetCore/Implementation/TelemetryHelper.cs index f8ad607d86..4e8cd55524 100644 --- a/src/OpenTelemetry.Instrumentation.AspNetCore/Implementation/TelemetryHelper.cs +++ b/src/OpenTelemetry.Instrumentation.AspNetCore/Implementation/TelemetryHelper.cs @@ -1,11 +1,14 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 +using OpenTelemetry.Internal; + namespace OpenTelemetry.Instrumentation.AspNetCore.Implementation; internal static class TelemetryHelper { public static readonly object[] BoxedStatusCodes = InitializeBoxedStatusCodes(); + internal static readonly RequestDataHelper RequestDataHelper = new(); public static object GetBoxedStatusCode(int statusCode) { From bb59d88dbfd973a3ac4d2c7cb970cf4705f11e28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Kie=C5=82kowicz?= Date: Tue, 7 May 2024 12:04:46 +0200 Subject: [PATCH 06/26] Ignore CA2000 --- .../Implementation/HttpInListener.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/OpenTelemetry.Instrumentation.AspNetCore/Implementation/HttpInListener.cs b/src/OpenTelemetry.Instrumentation.AspNetCore/Implementation/HttpInListener.cs index ebadf377e6..89e8c1e278 100644 --- a/src/OpenTelemetry.Instrumentation.AspNetCore/Implementation/HttpInListener.cs +++ b/src/OpenTelemetry.Instrumentation.AspNetCore/Implementation/HttpInListener.cs @@ -129,7 +129,9 @@ public void OnStartActivity(Activity activity, object payload) } else { +#pragma warning disable CA2000 newOne = new Activity(ActivityOperationName); +#pragma warning restore CA2000 newOne.SetParentId(ctx.ActivityContext.TraceId, ctx.ActivityContext.SpanId, ctx.ActivityContext.TraceFlags); } From 3e409a24c450318d3fe89cdbd8578d57848330c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Kie=C5=82kowicz?= Date: Tue, 7 May 2024 12:14:37 +0200 Subject: [PATCH 07/26] Fix CA1711 --- .../BasicTests.cs | 8 ++--- ...stsCollectionsIsAccordingToTheSpecTests.cs | 6 ++-- test/TestApp.AspNetCore/ActivityMiddleware.cs | 29 +++++-------------- test/TestApp.AspNetCore/CallbackMiddleware.cs | 16 +++------- test/TestApp.AspNetCore/Program.cs | 4 +-- .../TestActivityMiddleware.cs | 17 +++++++++++ .../TestCallbackMiddleware.cs | 12 ++++++++ 7 files changed, 50 insertions(+), 42 deletions(-) create mode 100644 test/TestApp.AspNetCore/TestActivityMiddleware.cs create mode 100644 test/TestApp.AspNetCore/TestCallbackMiddleware.cs diff --git a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/BasicTests.cs b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/BasicTests.cs index 03f6f4c162..7f54724dfa 100644 --- a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/BasicTests.cs +++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/BasicTests.cs @@ -591,7 +591,7 @@ public async Task ActivitiesStartedInMiddlewareShouldNotBeUpdated() void ConfigureTestServices(IServiceCollection services) { - services.AddSingleton(new TestActivityMiddlewareImpl(activitySourceName, activityName)); + services.AddSingleton(new TestTestActivityMiddleware(activitySourceName, activityName)); this.tracerProvider = Sdk.CreateTracerProviderBuilder() .AddAspNetCoreInstrumentation() .AddSource(activitySourceName) @@ -701,7 +701,7 @@ public async Task ActivitiesStartedInMiddlewareBySettingHostActivityToNullShould { builder.ConfigureTestServices((IServiceCollection services) => { - services.AddSingleton(new TestNullHostActivityMiddlewareImpl(activitySourceName, activityName)); + services.AddSingleton(new TestNullHostActivityMiddlewareImpl(activitySourceName, activityName)); services.AddOpenTelemetry() .WithTracing(builder => builder .AddAspNetCoreInstrumentation() @@ -1232,7 +1232,7 @@ public override void OnEventWritten(string name, object payload) } } - private class TestNullHostActivityMiddlewareImpl(string activitySourceName, string activityName) : ActivityMiddleware.ActivityMiddlewareImpl + private class TestNullHostActivityMiddlewareImpl(string activitySourceName, string activityName) : TestActivityMiddleware { private readonly ActivitySource activitySource = new(activitySourceName); private readonly string activityName = activityName; @@ -1254,7 +1254,7 @@ public override void PostProcess(HttpContext context) } } - private class TestActivityMiddlewareImpl(string activitySourceName, string activityName) : ActivityMiddleware.ActivityMiddlewareImpl + private class TestTestActivityMiddleware(string activitySourceName, string activityName) : TestActivityMiddleware { private readonly ActivitySource activitySource = new(activitySourceName); private readonly string activityName = activityName; diff --git a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/IncomingRequestsCollectionsIsAccordingToTheSpecTests.cs b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/IncomingRequestsCollectionsIsAccordingToTheSpecTests.cs index d5777dbc6f..ff725b0da6 100644 --- a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/IncomingRequestsCollectionsIsAccordingToTheSpecTests.cs +++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/IncomingRequestsCollectionsIsAccordingToTheSpecTests.cs @@ -46,7 +46,7 @@ public async Task SuccessfulTemplateControllerCallGeneratesASpan_New( { builder.ConfigureTestServices((IServiceCollection services) => { - services.AddSingleton(new TestCallbackMiddlewareImpl(statusCode, reasonPhrase)); + services.AddSingleton(new TestTestCallbackMiddleware(statusCode, reasonPhrase)); services.AddOpenTelemetry() .WithTracing(builder => builder .AddAspNetCoreInstrumentation(options => @@ -143,12 +143,12 @@ private static void ValidateTagValue(Activity activity, string attribute, string } } - public class TestCallbackMiddlewareImpl : CallbackMiddleware.CallbackMiddlewareImpl + public class TestTestCallbackMiddleware : TestCallbackMiddleware { private readonly int statusCode; private readonly string reasonPhrase; - public TestCallbackMiddlewareImpl(int statusCode, string reasonPhrase) + public TestTestCallbackMiddleware(int statusCode, string reasonPhrase) { this.statusCode = statusCode; this.reasonPhrase = reasonPhrase; diff --git a/test/TestApp.AspNetCore/ActivityMiddleware.cs b/test/TestApp.AspNetCore/ActivityMiddleware.cs index 591bc388f3..99f70a5aa3 100644 --- a/test/TestApp.AspNetCore/ActivityMiddleware.cs +++ b/test/TestApp.AspNetCore/ActivityMiddleware.cs @@ -3,42 +3,29 @@ namespace TestApp.AspNetCore; -public class ActivityMiddleware +internal class ActivityMiddleware { - private readonly ActivityMiddlewareImpl impl; + private readonly TestActivityMiddleware testActivityMiddleware; private readonly RequestDelegate next; - public ActivityMiddleware(RequestDelegate next, ActivityMiddlewareImpl impl) + public ActivityMiddleware(RequestDelegate next, TestActivityMiddleware testActivityMiddleware) { this.next = next; - this.impl = impl; + this.testActivityMiddleware = testActivityMiddleware; } public async Task InvokeAsync(HttpContext context) { - if (this.impl != null) + if (this.testActivityMiddleware != null) { - this.impl.PreProcess(context); + this.testActivityMiddleware.PreProcess(context); } await this.next(context); - if (this.impl != null) + if (this.testActivityMiddleware != null) { - this.impl.PostProcess(context); - } - } - - public class ActivityMiddlewareImpl - { - public virtual void PreProcess(HttpContext context) - { - // Do nothing - } - - public virtual void PostProcess(HttpContext context) - { - // Do nothing + this.testActivityMiddleware.PostProcess(context); } } } diff --git a/test/TestApp.AspNetCore/CallbackMiddleware.cs b/test/TestApp.AspNetCore/CallbackMiddleware.cs index fee8569d5d..9ee236845d 100644 --- a/test/TestApp.AspNetCore/CallbackMiddleware.cs +++ b/test/TestApp.AspNetCore/CallbackMiddleware.cs @@ -5,28 +5,20 @@ namespace TestApp.AspNetCore; public class CallbackMiddleware { - private readonly CallbackMiddlewareImpl impl; + private readonly TestCallbackMiddleware testCallbackMiddleware; private readonly RequestDelegate next; - public CallbackMiddleware(RequestDelegate next, CallbackMiddlewareImpl impl) + public CallbackMiddleware(RequestDelegate next, TestCallbackMiddleware testCallbackMiddleware) { this.next = next; - this.impl = impl; + this.testCallbackMiddleware = testCallbackMiddleware; } public async Task InvokeAsync(HttpContext context) { - if (this.impl == null || await this.impl.ProcessAsync(context)) + if (this.testCallbackMiddleware == null || await this.testCallbackMiddleware.ProcessAsync(context)) { await this.next(context); } } - - public class CallbackMiddlewareImpl - { - public virtual async Task ProcessAsync(HttpContext context) - { - return await Task.FromResult(true); - } - } } diff --git a/test/TestApp.AspNetCore/Program.cs b/test/TestApp.AspNetCore/Program.cs index 06071eab4b..5cbe2b5e3a 100644 --- a/test/TestApp.AspNetCore/Program.cs +++ b/test/TestApp.AspNetCore/Program.cs @@ -23,10 +23,10 @@ public static void Main(string[] args) builder.Services.AddSingleton(); builder.Services.AddSingleton( - new CallbackMiddleware.CallbackMiddlewareImpl()); + new TestCallbackMiddleware()); builder.Services.AddSingleton( - new ActivityMiddleware.ActivityMiddlewareImpl()); + new TestActivityMiddleware()); var app = builder.Build(); diff --git a/test/TestApp.AspNetCore/TestActivityMiddleware.cs b/test/TestApp.AspNetCore/TestActivityMiddleware.cs new file mode 100644 index 0000000000..be1e2e5a1e --- /dev/null +++ b/test/TestApp.AspNetCore/TestActivityMiddleware.cs @@ -0,0 +1,17 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +namespace TestApp.AspNetCore; + +public class TestActivityMiddleware +{ + public virtual void PreProcess(HttpContext context) + { + // Do nothing + } + + public virtual void PostProcess(HttpContext context) + { + // Do nothing + } +} diff --git a/test/TestApp.AspNetCore/TestCallbackMiddleware.cs b/test/TestApp.AspNetCore/TestCallbackMiddleware.cs new file mode 100644 index 0000000000..ba11577ff0 --- /dev/null +++ b/test/TestApp.AspNetCore/TestCallbackMiddleware.cs @@ -0,0 +1,12 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +namespace TestApp.AspNetCore; + +public class TestCallbackMiddleware +{ + public virtual async Task ProcessAsync(HttpContext context) + { + return await Task.FromResult(true); + } +} From a2d496868c5f59d78b9b3bbe0a4de12fa4219f18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Kie=C5=82kowicz?= Date: Tue, 7 May 2024 12:21:33 +0200 Subject: [PATCH 08/26] Fix CA1805 --- .../BasicTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/BasicTests.cs b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/BasicTests.cs index 7f54724dfa..92fdedf1c8 100644 --- a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/BasicTests.cs +++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/BasicTests.cs @@ -27,7 +27,7 @@ public sealed class BasicTests : IClassFixture>, IDisposable { private readonly WebApplicationFactory factory; - private TracerProvider tracerProvider = null; + private TracerProvider tracerProvider; public BasicTests(WebApplicationFactory factory) { From ce6bd54b6ce32352cfbd03e38614658fb0a63e47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Kie=C5=82kowicz?= Date: Tue, 7 May 2024 12:24:18 +0200 Subject: [PATCH 09/26] Fix CA2234 --- .../BasicTests.cs | 27 ++++++++++--------- ...stsCollectionsIsAccordingToTheSpecTests.cs | 2 +- .../MetricTests.cs | 6 ++--- .../RouteTests/RoutingTestFixture.cs | 2 +- 4 files changed, 19 insertions(+), 18 deletions(-) diff --git a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/BasicTests.cs b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/BasicTests.cs index 92fdedf1c8..b16ea48ea9 100644 --- a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/BasicTests.cs +++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/BasicTests.cs @@ -19,6 +19,7 @@ using TestApp.AspNetCore; using TestApp.AspNetCore.Filters; using Xunit; +using Uri = System.Uri; namespace OpenTelemetry.Instrumentation.AspNetCore.Tests; @@ -68,7 +69,7 @@ void ConfigureTestServices(IServiceCollection services) .CreateClient()) { // Act - using var response = await client.GetAsync("/api/values"); + using var response = await client.GetAsync(new Uri("/api/values", UriKind.Relative)); // Assert response.EnsureSuccessStatusCode(); // Status Code 200-299 @@ -115,7 +116,7 @@ void ConfigureTestServices(IServiceCollection services) .CreateClient()) { // Act - using var response = await client.GetAsync("/api/values"); + using var response = await client.GetAsync(new Uri("/api/values", UriKind.Relative)); // Assert response.EnsureSuccessStatusCode(); // Status Code 200-299 @@ -222,7 +223,7 @@ public async Task CustomPropagator(bool addSampler) })) { using var client = testFactory.CreateClient(); - using var response = await client.GetAsync("/api/values/2"); + using var response = await client.GetAsync(new Uri("/api/values/2", UriKind.Relative)); response.EnsureSuccessStatusCode(); // Status Code 200-299 WaitForActivityExport(exportedItems, 1); @@ -272,8 +273,8 @@ void ConfigureTestServices(IServiceCollection services) using var client = testFactory.CreateClient(); // Act - using var response1 = await client.GetAsync("/api/values"); - using var response2 = await client.GetAsync("/api/values/2"); + using var response1 = await client.GetAsync(new Uri("/api/values", UriKind.Relative)); + using var response2 = await client.GetAsync(new Uri("/api/values/2", UriKind.Relative)); // Assert response1.EnsureSuccessStatusCode(); // Status Code 200-299 @@ -324,8 +325,8 @@ void ConfigureTestServices(IServiceCollection services) // Act using (var inMemoryEventListener = new InMemoryEventListener(AspNetCoreInstrumentationEventSource.Log)) { - using var response1 = await client.GetAsync("/api/values"); - using var response2 = await client.GetAsync("/api/values/2"); + using var response1 = await client.GetAsync(new Uri("/api/values", UriKind.Relative)); + using var response2 = await client.GetAsync(new Uri("/api/values/2", UriKind.Relative)); response1.EnsureSuccessStatusCode(); // Status Code 200-299 response2.EnsureSuccessStatusCode(); // Status Code 200-299 @@ -573,7 +574,7 @@ void ConfigureTestServices(IServiceCollection services) .CreateClient(); // Act - using var response = await client.GetAsync("/api/values"); + using var response = await client.GetAsync(new Uri("/api/values", UriKind.Relative)); // Assert Assert.Equal(shouldFilterBeCalled, filterCalled); @@ -608,7 +609,7 @@ void ConfigureTestServices(IServiceCollection services) }) .CreateClient()) { - using var response = await client.GetAsync("/api/values/2"); + using var response = await client.GetAsync(new Uri("/api/values/2", UriKind.Relative)); response.EnsureSuccessStatusCode(); WaitForActivityExport(exportedItems, 2); } @@ -712,7 +713,7 @@ public async Task ActivitiesStartedInMiddlewareBySettingHostActivityToNullShould }) .CreateClient()) { - using var response = await client.GetAsync("/api/values/2"); + using var response = await client.GetAsync(new Uri("/api/values/2", UriKind.Relative)); response.EnsureSuccessStatusCode(); WaitForActivityExport(exportedItems, 2); } @@ -761,7 +762,7 @@ void ConfigureTestServices(IServiceCollection services) .CreateClient()) { // Act - using var response = await client.GetAsync("/api/values"); + using var response = await client.GetAsync(new Uri("/api/values", UriKind.Relative)); // Assert response.EnsureSuccessStatusCode(); // Status Code 200-299 @@ -794,7 +795,7 @@ public async Task ShouldExportActivityWithOneOrMoreExceptionFilters(int mode) .CreateClient()) { // Act - using var response = await client.GetAsync("/api/error"); + using var response = await client.GetAsync(new Uri("/api/error", UriKind.Relative)); WaitForActivityExport(exportedItems, 1); } @@ -1109,7 +1110,7 @@ public async Task ValidateUrlQueryRedaction(string urlQuery, string expectedUrlQ { try { - using var response = await client.GetAsync(path); + using var response = await client.GetAsync(new Uri(path, UriKind.Relative)); } catch (Exception) { diff --git a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/IncomingRequestsCollectionsIsAccordingToTheSpecTests.cs b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/IncomingRequestsCollectionsIsAccordingToTheSpecTests.cs index ff725b0da6..cc91ac509d 100644 --- a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/IncomingRequestsCollectionsIsAccordingToTheSpecTests.cs +++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/IncomingRequestsCollectionsIsAccordingToTheSpecTests.cs @@ -73,7 +73,7 @@ public async Task SuccessfulTemplateControllerCallGeneratesASpan_New( path += query; } - using var response = await client.GetAsync(path); + using var response = await client.GetAsync(new Uri(path, UriKind.Relative)); } catch (Exception) { diff --git a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/MetricTests.cs b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/MetricTests.cs index ddec198dc4..8b69a8a8cf 100644 --- a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/MetricTests.cs +++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/MetricTests.cs @@ -61,7 +61,7 @@ public async Task ValidateNet8MetricsAsync() var portNumber = url.Substring(url.LastIndexOf(':') + 1); using var client = new HttpClient(); - var res = await client.GetAsync($"http://localhost:{portNumber}/"); + var res = await client.GetAsync(new Uri($"http://localhost:{portNumber}/")); Assert.True(res.IsSuccessStatusCode); // We need to let metric callback execute as it is executed AFTER response was returned. @@ -144,7 +144,7 @@ void ConfigureTestServices(IServiceCollection services) var portNumber = url.Substring(url.LastIndexOf(':') + 1); using var client = new HttpClient(); - var res = await client.GetAsync($"http://localhost:{portNumber}/"); + var res = await client.GetAsync(new Uri($"http://localhost:{portNumber}/")); Assert.NotNull(res); // We need to let metric callback execute as it is executed AFTER response was returned. @@ -199,7 +199,7 @@ public async Task RequestMetricIsCaptured(string api, string expectedRoute, stri { try { - using var response = await client.GetAsync(api); + using var response = await client.GetAsync(new Uri(api, UriKind.Relative)); response.EnsureSuccessStatusCode(); } catch diff --git a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/RoutingTestFixture.cs b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/RoutingTestFixture.cs index 1b949e2674..31f32ac1ae 100644 --- a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/RoutingTestFixture.cs +++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/RoutingTestFixture.cs @@ -38,7 +38,7 @@ 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); + await HttpClient.GetAsync(new Uri(url)); } public void AddTestResult(RoutingTestResult result) From 7f2a7426462773e297c7aecde024ddb55f15c9e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Kie=C5=82kowicz?= Date: Tue, 7 May 2024 12:25:59 +0200 Subject: [PATCH 10/26] Fix - do not nest public classes --- .../IncomingRequestsCollectionsIsAccordingToTheSpecTests.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/IncomingRequestsCollectionsIsAccordingToTheSpecTests.cs b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/IncomingRequestsCollectionsIsAccordingToTheSpecTests.cs index cc91ac509d..72141730cd 100644 --- a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/IncomingRequestsCollectionsIsAccordingToTheSpecTests.cs +++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/IncomingRequestsCollectionsIsAccordingToTheSpecTests.cs @@ -46,7 +46,7 @@ public async Task SuccessfulTemplateControllerCallGeneratesASpan_New( { builder.ConfigureTestServices((IServiceCollection services) => { - services.AddSingleton(new TestTestCallbackMiddleware(statusCode, reasonPhrase)); + services.AddSingleton(new ExceptionTestCallbackMiddleware(statusCode, reasonPhrase)); services.AddOpenTelemetry() .WithTracing(builder => builder .AddAspNetCoreInstrumentation(options => @@ -143,12 +143,12 @@ private static void ValidateTagValue(Activity activity, string attribute, string } } - public class TestTestCallbackMiddleware : TestCallbackMiddleware + internal class ExceptionTestCallbackMiddleware : TestCallbackMiddleware { private readonly int statusCode; private readonly string reasonPhrase; - public TestTestCallbackMiddleware(int statusCode, string reasonPhrase) + public ExceptionTestCallbackMiddleware(int statusCode, string reasonPhrase) { this.statusCode = statusCode; this.reasonPhrase = reasonPhrase; From 00aab8418c203fb204658a3136d800c268366115 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Kie=C5=82kowicz?= Date: Tue, 7 May 2024 12:26:56 +0200 Subject: [PATCH 11/26] Fix CA1310 --- .../IncomingRequestsCollectionsIsAccordingToTheSpecTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/IncomingRequestsCollectionsIsAccordingToTheSpecTests.cs b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/IncomingRequestsCollectionsIsAccordingToTheSpecTests.cs index 72141730cd..0060373f8f 100644 --- a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/IncomingRequestsCollectionsIsAccordingToTheSpecTests.cs +++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/IncomingRequestsCollectionsIsAccordingToTheSpecTests.cs @@ -160,7 +160,7 @@ public override async Task ProcessAsync(HttpContext context) context.Response.HttpContext.Features.Get().ReasonPhrase = this.reasonPhrase; await context.Response.WriteAsync("empty"); - if (context.Request.Path.Value.EndsWith("exception")) + if (context.Request.Path.Value.EndsWith("exception", StringComparison.Ordinal)) { throw new Exception("exception description"); } From 2072f9b82105e75954a556d9f9079b2e5c9ef666 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Kie=C5=82kowicz?= Date: Tue, 7 May 2024 12:29:30 +0200 Subject: [PATCH 12/26] Fix CA1823 --- .../MetricTests.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/MetricTests.cs b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/MetricTests.cs index 8b69a8a8cf..97bba603b9 100644 --- a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/MetricTests.cs +++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/MetricTests.cs @@ -27,8 +27,6 @@ namespace OpenTelemetry.Instrumentation.AspNetCore.Tests; public class MetricTests(WebApplicationFactory factory) : IClassFixture>, IDisposable { - private const int StandardTagsCount = 6; - private readonly WebApplicationFactory factory = factory; private MeterProvider meterProvider; From 1cddb8c2768e2751e0a008264a6a7e8f18421e48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Kie=C5=82kowicz?= Date: Tue, 7 May 2024 12:30:59 +0200 Subject: [PATCH 13/26] Fix CA1869 --- .../RouteTests/RoutingTestCases.cs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/RoutingTestCases.cs b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/RoutingTestCases.cs index a3ec381888..10ac5dc695 100644 --- a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/RoutingTestCases.cs +++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/RoutingTestCases.cs @@ -12,16 +12,18 @@ namespace RouteTests; public static class RoutingTestCases { + private static readonly JsonSerializerOptions JsonSerializerOptions = new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + Converters = { new JsonStringEnumConverter() }, + }; + public static IEnumerable GetTestCases() { var assembly = Assembly.GetExecutingAssembly(); var input = JsonSerializer.Deserialize( assembly.GetManifestResourceStream("RoutingTestCases.json")!, - new JsonSerializerOptions - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - Converters = { new JsonStringEnumConverter() }, - }); + JsonSerializerOptions); return GetArgumentsFromTestCaseObject(input!); } From 2e0d42b280e59479de601193c864ae6eb3e9b219 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Kie=C5=82kowicz?= Date: Tue, 7 May 2024 12:31:28 +0200 Subject: [PATCH 14/26] Fix CA1859 --- .../RouteTests/RoutingTestCases.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/RoutingTestCases.cs b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/RoutingTestCases.cs index 10ac5dc695..24a2ee9f9a 100644 --- a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/RoutingTestCases.cs +++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/RoutingTestCases.cs @@ -27,7 +27,7 @@ public static IEnumerable GetTestCases() return GetArgumentsFromTestCaseObject(input!); } - private static IEnumerable GetArgumentsFromTestCaseObject(IEnumerable input) + private static List GetArgumentsFromTestCaseObject(IEnumerable input) { var result = new List(); From 1f8094247374017b05998c017887ba9680ef800f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Kie=C5=82kowicz?= Date: Tue, 7 May 2024 12:33:36 +0200 Subject: [PATCH 15/26] Fix CA1304 --- .../RouteTests/RoutingTestFixture.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/RoutingTestFixture.cs b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/RoutingTestFixture.cs index 31f32ac1ae..4b11dc5166 100644 --- a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/RoutingTestFixture.cs +++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/RoutingTestFixture.cs @@ -3,6 +3,7 @@ #nullable enable +using System.Globalization; using System.Text; using Microsoft.AspNetCore.Builder; using RouteTests.TestApplication; @@ -98,11 +99,11 @@ static string GenerateLinkFragment(TestApplicationScenario scenario, string name { '-' => '-', ' ' => '-', - _ => char.ToLower(c), + _ => char.ToLower(c, CultureInfo.InvariantCulture), }) .ToArray(); - return $"#{scenario.ToString().ToLower()}-{new string(chars)}"; + return $"#{scenario.ToString().ToLower(CultureInfo.CurrentCulture)}-{new string(chars)}"; } } } From 1e32d93144e422d5465baafc398c17184a751305 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Kie=C5=82kowicz?= Date: Tue, 7 May 2024 12:47:13 +0200 Subject: [PATCH 16/26] Fix 1034 --- .../RouteTests/RoutingTestCases.cs | 26 ------ .../TestApplication/ActionDescriptorInfo.cs | 50 ++++++++++++ .../ControllerActionDescriptorInfo.cs | 26 ++++++ .../PageActionDescriptorInfo.cs | 26 ++++++ .../RouteTests/TestApplication/RouteInfo.cs | 80 ------------------- .../RouteTests/TestCase.cs | 32 ++++++++ 6 files changed, 134 insertions(+), 106 deletions(-) create mode 100644 test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/TestApplication/ActionDescriptorInfo.cs create mode 100644 test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/TestApplication/ControllerActionDescriptorInfo.cs create mode 100644 test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/TestApplication/PageActionDescriptorInfo.cs create mode 100644 test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/TestCase.cs diff --git a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/RoutingTestCases.cs b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/RoutingTestCases.cs index 24a2ee9f9a..44d0e84e43 100644 --- a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/RoutingTestCases.cs +++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/RoutingTestCases.cs @@ -6,7 +6,6 @@ using System.Reflection; using System.Text.Json; using System.Text.Json.Serialization; -using RouteTests.TestApplication; namespace RouteTests; @@ -43,29 +42,4 @@ private static List GetArgumentsFromTestCaseObject(IEnumerable(); + 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; } +} diff --git a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/TestApplication/ControllerActionDescriptorInfo.cs b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/TestApplication/ControllerActionDescriptorInfo.cs new file mode 100644 index 0000000000..ae5ef6b907 --- /dev/null +++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/TestApplication/ControllerActionDescriptorInfo.cs @@ -0,0 +1,26 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#nullable enable +using System.Text.Json.Serialization; + +namespace RouteTests.TestApplication; + +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; +} diff --git a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/TestApplication/PageActionDescriptorInfo.cs b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/TestApplication/PageActionDescriptorInfo.cs new file mode 100644 index 0000000000..dda48ae462 --- /dev/null +++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/TestApplication/PageActionDescriptorInfo.cs @@ -0,0 +1,26 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#nullable enable +using System.Text.Json.Serialization; + +namespace RouteTests.TestApplication; + +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/RouteInfo.cs b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/TestApplication/RouteInfo.cs index 7858721039..bb16574018 100644 --- a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/TestApplication/RouteInfo.cs +++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/TestApplication/RouteInfo.cs @@ -9,8 +9,6 @@ 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; @@ -57,82 +55,4 @@ public void SetValues(ActionDescriptor actionDescriptor) 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/TestCase.cs b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/TestCase.cs new file mode 100644 index 0000000000..36710fe4a9 --- /dev/null +++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/TestCase.cs @@ -0,0 +1,32 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#nullable enable +using RouteTests.TestApplication; + +namespace RouteTests; + +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? CurrentHttpRoute { 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}"; + } +} From 9c57768f82fceada7f9387bf7ec170d69dc0a73e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Kie=C5=82kowicz?= Date: Tue, 7 May 2024 12:49:01 +0200 Subject: [PATCH 17/26] Ignore CA2227 --- .../RouteTests/RoutingTestResult.cs | 2 +- .../RouteTests/RoutingTests.cs | 2 +- .../RouteTests/TestApplication/ActionDescriptorInfo.cs | 2 ++ .../RouteTests/TestApplication/RouteInfo.cs | 2 ++ 4 files changed, 6 insertions(+), 2 deletions(-) diff --git a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/RoutingTestResult.cs b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/RoutingTestResult.cs index 8217fa0987..f1df77ba10 100644 --- a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/RoutingTestResult.cs +++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/RoutingTestResult.cs @@ -24,7 +24,7 @@ public class RoutingTestResult public RouteInfo RouteInfo { get; set; } = new RouteInfo(); [JsonIgnore] - public RoutingTestCases.TestCase TestCase { get; set; } = new RoutingTestCases.TestCase(); + public TestCase TestCase { get; set; } = new TestCase(); public override string ToString() { diff --git a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/RoutingTests.cs b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/RoutingTests.cs index 6098aaa08b..9ab58450b4 100644 --- a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/RoutingTests.cs +++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/RoutingTests.cs @@ -31,7 +31,7 @@ public RoutingTests(RoutingTestFixture fixture) [Theory] [MemberData(nameof(TestData))] - public async Task TestHttpRoute(RoutingTestCases.TestCase testCase) + public async Task TestHttpRoute(TestCase testCase) { using var tracerProvider = Sdk.CreateTracerProviderBuilder() .AddAspNetCoreInstrumentation() diff --git a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/TestApplication/ActionDescriptorInfo.cs b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/TestApplication/ActionDescriptorInfo.cs index 19356e291f..20fc1f281b 100644 --- a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/TestApplication/ActionDescriptorInfo.cs +++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/TestApplication/ActionDescriptorInfo.cs @@ -40,7 +40,9 @@ public ActionDescriptorInfo(ActionDescriptor actionDescriptor) public string? AttributeRouteInfo { get; set; } [JsonPropertyName("Parameters")] +#pragma warning disable CA2227 public IList? ActionParameters { get; set; } +#pragma warning restore CA2227 [JsonPropertyName("ControllerActionDescriptor")] public ControllerActionDescriptorInfo? ControllerActionDescriptorSummary { get; set; } diff --git a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/TestApplication/RouteInfo.cs b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/TestApplication/RouteInfo.cs index bb16574018..08433fc666 100644 --- a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/TestApplication/RouteInfo.cs +++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/TestApplication/RouteInfo.cs @@ -28,7 +28,9 @@ public class RouteInfo public string? RouteDiagnosticMetadata { get; set; } [JsonPropertyName("HttpContext.GetRouteData()")] +#pragma warning disable CA2227 public IDictionary? RouteData { get; set; } +#pragma warning restore CA2227 public ActionDescriptorInfo? ActionDescriptor { get; set; } From edff8c9b5cc309efe72713f9bbed959c34f507ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Kie=C5=82kowicz?= Date: Tue, 7 May 2024 13:00:22 +0200 Subject: [PATCH 18/26] Fix CA2012 --- .../RouteTests/RoutingTestFixture.cs | 32 +++++++++++-------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/RoutingTestFixture.cs b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/RoutingTestFixture.cs index 4b11dc5166..40d6d51209 100644 --- a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/RoutingTestFixture.cs +++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/RoutingTestFixture.cs @@ -7,10 +7,11 @@ using System.Text; using Microsoft.AspNetCore.Builder; using RouteTests.TestApplication; +using Xunit; namespace RouteTests; -public class RoutingTestFixture : IDisposable +public class RoutingTestFixture : IAsyncLifetime { private static readonly HttpClient HttpClient = new(); private readonly Dictionary apps = new(); @@ -34,24 +35,16 @@ public RoutingTestFixture() } } - 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(new Uri(url)); - } - - public void AddTestResult(RoutingTestResult result) + public Task InitializeAsync() { - this.testResults.Add(result); + return Task.CompletedTask; } - public void Dispose() + public async Task DisposeAsync() { foreach (var app in this.apps) { - app.Value.DisposeAsync().GetAwaiter().GetResult(); + await app.Value.DisposeAsync(); } HttpClient.Dispose(); @@ -60,6 +53,19 @@ public void Dispose() this.GenerateReadme(); } + 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(new Uri(url)); + } + + public void AddTestResult(RoutingTestResult result) + { + this.testResults.Add(result); + } + private void GenerateReadme() { var sb = new StringBuilder(); From c0b712f886296df4eb958beb39b7edcc6279d337 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Kie=C5=82kowicz?= Date: Tue, 7 May 2024 13:26:02 +0200 Subject: [PATCH 19/26] cleanup usings --- .../OpenTelemetry.Instrumentation.AspNetCore.Tests/BasicTests.cs | 1 - .../DependencyInjectionConfigTests.cs | 1 - 2 files changed, 2 deletions(-) diff --git a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/BasicTests.cs b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/BasicTests.cs index b16ea48ea9..3ede00dcd4 100644 --- a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/BasicTests.cs +++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/BasicTests.cs @@ -10,7 +10,6 @@ using Microsoft.AspNetCore.TestHost; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using OpenTelemetry.Context.Propagation; using OpenTelemetry.Instrumentation.AspNetCore.Implementation; diff --git a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/DependencyInjectionConfigTests.cs b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/DependencyInjectionConfigTests.cs index 766a81b40d..23791d421c 100644 --- a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/DependencyInjectionConfigTests.cs +++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/DependencyInjectionConfigTests.cs @@ -5,7 +5,6 @@ using Microsoft.AspNetCore.TestHost; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; -using OpenTelemetry.Metrics; using OpenTelemetry.Trace; using Xunit; From 48199742ab46e16ee90e7fb409cf1de06ad9ee19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Kie=C5=82kowicz?= Date: Tue, 7 May 2024 14:50:15 +0200 Subject: [PATCH 20/26] remove unnecessary conditional compilation --- .../BasicTests.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/BasicTests.cs b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/BasicTests.cs index 3ede00dcd4..eee0af980e 100644 --- a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/BasicTests.cs +++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/BasicTests.cs @@ -1016,7 +1016,6 @@ public async Task DiagnosticSourceExceptionCallBackIsNotReceivedForExceptionsHan Assert.True(exceptionHandled); } -#if NET6_0_OR_GREATER [Fact] public async Task NoSiblingActivityCreatedWhenTraceFlagsNone() { @@ -1051,7 +1050,6 @@ public async Task NoSiblingActivityCreatedWhenTraceFlagsNone() // Confirm that Activity.Current and IHttpActivityFeature activity are same Assert.True(result); } -#endif [Theory] [InlineData("?a", "?a", false)] From 36740a429abafd8636bd50e529262533a1632042 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Kie=C5=82kowicz?= Date: Wed, 8 May 2024 08:14:49 +0200 Subject: [PATCH 21/26] Dispose both instances of tracer provider in NoSiblingActivityCreatedWhenTraceFlagsNone One of the instance was not closed, it affected other tests --- .../BasicTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/BasicTests.cs b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/BasicTests.cs index eee0af980e..f3c11b9297 100644 --- a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/BasicTests.cs +++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/BasicTests.cs @@ -1019,7 +1019,7 @@ public async Task DiagnosticSourceExceptionCallBackIsNotReceivedForExceptionsHan [Fact] public async Task NoSiblingActivityCreatedWhenTraceFlagsNone() { - this.tracerProvider = Sdk.CreateTracerProviderBuilder() + using var localTracerProvider = Sdk.CreateTracerProviderBuilder() .SetSampler(new AlwaysOnSampler()) .AddAspNetCoreInstrumentation() .Build(); From e6d0c996c53bf2dc722cf238c6b3e273605b4c35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Kie=C5=82kowicz?= Date: Wed, 8 May 2024 08:32:42 +0200 Subject: [PATCH 22/26] Infrastructure code --- .../comp_instrumentation_aspnetcore.md | 41 +++++++++++++++++++ .github/codecov.yml | 5 +++ .github/workflows/ci.yml | 21 +++++++++- .../package-Instrumentation.AspNetCore.yml | 21 ++++++++++ ...nTelemetry.Instrumentation.AspNetCore.proj | 34 +++++++++++++++ opentelemetry-dotnet-contrib.sln | 2 + ...nTelemetry.AotCompatibility.TestApp.csproj | 1 + 7 files changed, 123 insertions(+), 2 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/comp_instrumentation_aspnetcore.md create mode 100644 .github/workflows/package-Instrumentation.AspNetCore.yml create mode 100644 build/Projects/OpenTelemetry.Instrumentation.AspNetCore.proj diff --git a/.github/ISSUE_TEMPLATE/comp_instrumentation_aspnetcore.md b/.github/ISSUE_TEMPLATE/comp_instrumentation_aspnetcore.md new file mode 100644 index 0000000000..bd5d9c2550 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/comp_instrumentation_aspnetcore.md @@ -0,0 +1,41 @@ +--- +name: OpenTelemetry.Instrumentation.AspNetCore +about: Issue with OpenTelemetry.Instrumentation.AspNetCore +labels: comp:instrumentation.aspnetcore +--- + +# Issue with OpenTelemetry.Instrumentation.AspNetCore + +List of [all OpenTelemetry NuGet +packages](https://www.nuget.org/profiles/OpenTelemetry) and version that you are +using (e.g. `OpenTelemetry 1.3.2`): + +* TBD + +Runtime version (e.g. `net462`, `net48`, `net6.0`, `net7.0` etc. You can +find this information from the `*.csproj` file): + +* TBD + +**Is this a feature request or a bug?** + +* [ ] Feature Request +* [ ] Bug + +**What is the expected behavior?** + +What do you expect to see? + +**What is the actual behavior?** + +What did you see instead? If you are reporting a bug, create a self-contained +project using the template of your choice and apply the minimum required code to +result in the issue you're observing. We will close this issue if: + +* The repro project you share with us is complex. We can't investigate custom + projects, so don't point us to such, please. +* If we can not reproduce the behavior you're reporting. + +## Additional Context + +Add any other context about the feature request here. diff --git a/.github/codecov.yml b/.github/codecov.yml index 3198fe49bb..93c6e5eb08 100644 --- a/.github/codecov.yml +++ b/.github/codecov.yml @@ -58,6 +58,11 @@ flags: - src/OpenTelemetry.Instrumentation.AspNet - src/OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule + unittests-Instrumentation.AspNetCore: + carryforward: true + paths: + - src/OpenTelemetry.Instrumentation.AspNetCore + unittests-Instrumentation.EventCounters: carryforward: true paths: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1e01fa622b..a6e0614a40 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,7 +23,8 @@ jobs: code: ['**.cs', '**.csproj', '.editorconfig'] aot: ['src/OpenTelemetry.Extensions.Enrichment/**'] aottestapp: ['test/OpenTelemetry.AotCompatibility.TestApp/**'] - aspnet: ['*/OpenTelemetry.Instrumentation.AspNet*/**', 'examples/AspNet/**', '!**/*.md'] + aspnet: ['*/OpenTelemetry.Instrumentation.AspNet.*/**', 'examples/AspNet/**', '!**/*.md'] + aspnetcore: ['*/OpenTelemetry.Instrumentation.AspNetCore*/**', '!**/*.md'] aws: ['*/OpenTelemetry.*.AWS*/**', '!**/*.md'] azure: ['*/OpenTelemetry.ResourceDetectors.Azure*/**', '!**/*.md'] eventcounters: ['*/OpenTelemetry.Instrumentation.EventCounters*/**', 'examples/event-counters/**', '!**/*.md'] @@ -49,7 +50,8 @@ jobs: 'test/**', 'examples/**', '!test/OpenTelemetry.AotCompatibility.TestApp/**', - '!*/OpenTelemetry.Instrumentation.AspNet*/**', + '!*/OpenTelemetry.Instrumentation.AspNet.*/**', + '!*/OpenTelemetry.Instrumentation.AspNetCore*/**', '!examples/AspNet/**', '!*/OpenTelemetry.ResourceDetectors.Azure*/**', '!*/OpenTelemetry.ResourceDetectors.Host*/**', @@ -108,6 +110,18 @@ jobs: os-list: '[ "windows-latest" ]' tfm-list: '[ "net462" ]' + build-test-aspnetcore: + needs: detect-changes + if: | + contains(needs.detect-changes.outputs.changes, 'aspnetcore') + || contains(needs.detect-changes.outputs.changes, 'build') + || contains(needs.detect-changes.outputs.changes, 'shared') + uses: ./.github/workflows/Component.BuildTest.yml + with: + project-name: OpenTelemetry.Instrumentation.AspNetCore + code-cov-name: Instrumentation.AspNetCore + tfm-list: '[ "net6.0", "net7.0", "net8.0" ]' + build-test-azure: needs: detect-changes if: | @@ -351,6 +365,7 @@ jobs: OpenTelemetry.Extensions.Tests.csproj, OpenTelemetry.Instrumentation.AspNet.Tests.csproj, OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule.Tests.csproj, + OpenTelemetry.Instrumentation.AspNetCore.Tests.csproj, OpenTelemetry.Instrumentation.EventCounters.Tests.csproj, OpenTelemetry.Instrumentation.GrpcNetClient.Tests.csproj, OpenTelemetry.Instrumentation.Http.Tests.csproj, @@ -407,6 +422,7 @@ jobs: if: | contains(needs.detect-changes.outputs.changes, 'eventcounters') || contains(needs.detect-changes.outputs.changes, 'runtime') + || contains(needs.detect-changes.outputs.changes, 'aspnetcore') || contains(needs.detect-changes.outputs.changes, 'aws') || contains(needs.detect-changes.outputs.changes, 'azure') || contains(needs.detect-changes.outputs.changes, 'extensions') @@ -433,6 +449,7 @@ jobs: lint-md, lint-dotnet-format, build-test-aspnet, + build-test-aspnetcore, build-test-azure, build-test-eventcounters, build-test-extensions, diff --git a/.github/workflows/package-Instrumentation.AspNetCore.yml b/.github/workflows/package-Instrumentation.AspNetCore.yml new file mode 100644 index 0000000000..9861c3a06d --- /dev/null +++ b/.github/workflows/package-Instrumentation.AspNetCore.yml @@ -0,0 +1,21 @@ +name: Pack OpenTelemetry.Instrumentation.AspNetCore + +on: + workflow_dispatch: + inputs: + logLevel: + description: 'Log level' + required: true + default: 'warning' + push: + tags: + - 'Instrumentation.AspNetCore-*' # trigger when we create a tag with prefix "Instrumentation.AspNetCore-" + +jobs: + call-build-test-pack: + permissions: + contents: write + uses: ./.github/workflows/Component.Package.yml + with: + project-name: OpenTelemetry.Instrumentation.AspNetCore + secrets: inherit diff --git a/build/Projects/OpenTelemetry.Instrumentation.AspNetCore.proj b/build/Projects/OpenTelemetry.Instrumentation.AspNetCore.proj new file mode 100644 index 0000000000..28396920e9 --- /dev/null +++ b/build/Projects/OpenTelemetry.Instrumentation.AspNetCore.proj @@ -0,0 +1,34 @@ + + + + $([System.IO.Directory]::GetParent($(MSBuildThisFileDirectory)).Parent.Parent.FullName) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/opentelemetry-dotnet-contrib.sln b/opentelemetry-dotnet-contrib.sln index 34bd25ab17..822638cdaa 100644 --- a/opentelemetry-dotnet-contrib.sln +++ b/opentelemetry-dotnet-contrib.sln @@ -45,6 +45,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{ .github\workflows\package-Extensions.Enrichment.yml = .github\workflows\package-Extensions.Enrichment.yml .github\workflows\package-Extensions.yml = .github\workflows\package-Extensions.yml .github\workflows\package-Instrumentation.AspNet.yml = .github\workflows\package-Instrumentation.AspNet.yml + .github\workflows\package-Instrumentation.AspNetCore.yml = .github\workflows\package-Instrumentation.AspNetCore.yml .github\workflows\package-Instrumentation.AWS.yml = .github\workflows\package-Instrumentation.AWS.yml .github\workflows\package-Instrumentation.AWSLambda.yml = .github\workflows\package-Instrumentation.AWSLambda.yml .github\workflows\package-Instrumentation.Cassandra.yml = .github\workflows\package-Instrumentation.Cassandra.yml @@ -322,6 +323,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Projects", "Projects", "{04 build\Projects\OpenTelemetry.Exporter.OneCollector.proj = build\Projects\OpenTelemetry.Exporter.OneCollector.proj build\Projects\OpenTelemetry.Extensions.proj = build\Projects\OpenTelemetry.Extensions.proj build\Projects\OpenTelemetry.Instrumentation.AspNet.proj = build\Projects\OpenTelemetry.Instrumentation.AspNet.proj + build\Projects\OpenTelemetry.Instrumentation.AspNetCore.proj = build\Projects\OpenTelemetry.Instrumentation.AspNetCore.proj build\Projects\OpenTelemetry.Instrumentation.EventCounters.proj = build\Projects\OpenTelemetry.Instrumentation.EventCounters.proj build\Projects\OpenTelemetry.Instrumentation.GrpcNetClient.proj = build\Projects\OpenTelemetry.Instrumentation.GrpcNetClient.proj build\Projects\OpenTelemetry.Instrumentation.Http.proj = build\Projects\OpenTelemetry.Instrumentation.Http.proj diff --git a/test/OpenTelemetry.AotCompatibility.TestApp/OpenTelemetry.AotCompatibility.TestApp.csproj b/test/OpenTelemetry.AotCompatibility.TestApp/OpenTelemetry.AotCompatibility.TestApp.csproj index 934b0d638c..169dbd0936 100644 --- a/test/OpenTelemetry.AotCompatibility.TestApp/OpenTelemetry.AotCompatibility.TestApp.csproj +++ b/test/OpenTelemetry.AotCompatibility.TestApp/OpenTelemetry.AotCompatibility.TestApp.csproj @@ -18,6 +18,7 @@ + From c1b6a6aa19346226961766b1964fcbef46d87841 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Kie=C5=82kowicz?= Date: Wed, 8 May 2024 09:08:05 +0200 Subject: [PATCH 23/26] Avoid tests concurrency --- .../OpenTelemetry.Instrumentation.AspNetCore.Tests/BasicTests.cs | 1 + .../DependencyInjectionConfigTests.cs | 1 + .../IncomingRequestsCollectionsIsAccordingToTheSpecTests.cs | 1 + .../MetricTests.cs | 1 + .../RouteTests/RoutingTests.cs | 1 + 5 files changed, 5 insertions(+) diff --git a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/BasicTests.cs b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/BasicTests.cs index f3c11b9297..5590a2fb3e 100644 --- a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/BasicTests.cs +++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/BasicTests.cs @@ -23,6 +23,7 @@ namespace OpenTelemetry.Instrumentation.AspNetCore.Tests; // See https://github.com/aspnet/Docs/tree/master/aspnetcore/test/integration-tests/samples/2.x/IntegrationTestsSample +[Collection("AspNetCore")] public sealed class BasicTests : IClassFixture>, IDisposable { diff --git a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/DependencyInjectionConfigTests.cs b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/DependencyInjectionConfigTests.cs index 23791d421c..a877d060a3 100644 --- a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/DependencyInjectionConfigTests.cs +++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/DependencyInjectionConfigTests.cs @@ -10,6 +10,7 @@ namespace OpenTelemetry.Instrumentation.AspNetCore.Tests; +[Collection("AspNetCore")] public class DependencyInjectionConfigTests : IClassFixture> { diff --git a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/IncomingRequestsCollectionsIsAccordingToTheSpecTests.cs b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/IncomingRequestsCollectionsIsAccordingToTheSpecTests.cs index 0060373f8f..f6bc29a386 100644 --- a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/IncomingRequestsCollectionsIsAccordingToTheSpecTests.cs +++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/IncomingRequestsCollectionsIsAccordingToTheSpecTests.cs @@ -15,6 +15,7 @@ namespace OpenTelemetry.Instrumentation.AspNetCore.Tests; +[Collection("AspNetCore")] public class IncomingRequestsCollectionsIsAccordingToTheSpecTests : IClassFixture> { diff --git a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/MetricTests.cs b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/MetricTests.cs index 97bba603b9..21743e0faf 100644 --- a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/MetricTests.cs +++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/MetricTests.cs @@ -24,6 +24,7 @@ namespace OpenTelemetry.Instrumentation.AspNetCore.Tests; +[Collection("AspNetCore")] public class MetricTests(WebApplicationFactory factory) : IClassFixture>, IDisposable { diff --git a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/RoutingTests.cs b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/RoutingTests.cs index 9ab58450b4..e140a3bb60 100644 --- a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/RoutingTests.cs +++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/RoutingTests.cs @@ -12,6 +12,7 @@ namespace RouteTests; +[Collection("AspNetCore")] public class RoutingTests : IClassFixture { private const string HttpStatusCode = "http.response.status_code"; From 4c8fd4ccb9e98d41874214a878e23d0ff2fca85f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Kie=C5=82kowicz?= Date: Wed, 8 May 2024 09:08:37 +0200 Subject: [PATCH 24/26] Remove unused class --- .../AttributesExtensions.cs | 12 ------------ 1 file changed, 12 deletions(-) delete mode 100644 test/OpenTelemetry.Instrumentation.AspNetCore.Tests/AttributesExtensions.cs diff --git a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/AttributesExtensions.cs b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/AttributesExtensions.cs deleted file mode 100644 index 7ad43bdaea..0000000000 --- a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/AttributesExtensions.cs +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright The OpenTelemetry Authors -// SPDX-License-Identifier: Apache-2.0 - -namespace OpenTelemetry.Instrumentation.AspNetCore.Tests; - -internal static class AttributesExtensions -{ - public static object GetValue(this IEnumerable> attributes, string key) - { - return attributes.FirstOrDefault(kvp => kvp.Key == key).Value; - } -} From 4d97822dbaed5026684e4e9276f0baf763e8782e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Kie=C5=82kowicz?= Date: Wed, 8 May 2024 10:17:18 +0200 Subject: [PATCH 25/26] Migrate Benchmarks --- ...nTelemetry.Instrumentation.AspNetCore.proj | 1 + opentelemetry-dotnet-contrib.sln | 7 + .../AspNetCoreInstrumentationBenchmarks.cs | 186 ++++++++++++++++ .../AspNetCoreInstrumentationNewBenchmarks.cs | 207 ++++++++++++++++++ ...nstrumentation.AspNetCore.Benchmark.csproj | 21 ++ .../Program.cs | 11 + .../README.md | 11 + .../README.md | 2 +- 8 files changed, 445 insertions(+), 1 deletion(-) create mode 100644 test/OpenTelemetry.Instrumentation.AspNetCore.Benchmark/Instrumentation/AspNetCoreInstrumentationBenchmarks.cs create mode 100644 test/OpenTelemetry.Instrumentation.AspNetCore.Benchmark/Instrumentation/AspNetCoreInstrumentationNewBenchmarks.cs create mode 100644 test/OpenTelemetry.Instrumentation.AspNetCore.Benchmark/OpenTelemetry.Instrumentation.AspNetCore.Benchmark.csproj create mode 100644 test/OpenTelemetry.Instrumentation.AspNetCore.Benchmark/Program.cs create mode 100644 test/OpenTelemetry.Instrumentation.AspNetCore.Benchmark/README.md diff --git a/build/Projects/OpenTelemetry.Instrumentation.AspNetCore.proj b/build/Projects/OpenTelemetry.Instrumentation.AspNetCore.proj index 28396920e9..0e86e80ccf 100644 --- a/build/Projects/OpenTelemetry.Instrumentation.AspNetCore.proj +++ b/build/Projects/OpenTelemetry.Instrumentation.AspNetCore.proj @@ -6,6 +6,7 @@ + diff --git a/opentelemetry-dotnet-contrib.sln b/opentelemetry-dotnet-contrib.sln index 822638cdaa..0809cbac97 100644 --- a/opentelemetry-dotnet-contrib.sln +++ b/opentelemetry-dotnet-contrib.sln @@ -377,6 +377,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenTelemetry.Instrumentati EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestApp.AspNetCore", "test\TestApp.AspNetCore\TestApp.AspNetCore.csproj", "{1E743561-B1D4-4100-B6AD-1FD25FA8659B}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenTelemetry.Instrumentation.AspNetCore.Benchmark", "test\OpenTelemetry.Instrumentation.AspNetCore.Benchmark\OpenTelemetry.Instrumentation.AspNetCore.Benchmark.csproj", "{92CD1B60-74B8-4E6E-9E7F-83AC3C792980}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -767,6 +769,10 @@ Global {1E743561-B1D4-4100-B6AD-1FD25FA8659B}.Debug|Any CPU.Build.0 = Debug|Any CPU {1E743561-B1D4-4100-B6AD-1FD25FA8659B}.Release|Any CPU.ActiveCfg = Release|Any CPU {1E743561-B1D4-4100-B6AD-1FD25FA8659B}.Release|Any CPU.Build.0 = Release|Any CPU + {92CD1B60-74B8-4E6E-9E7F-83AC3C792980}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {92CD1B60-74B8-4E6E-9E7F-83AC3C792980}.Debug|Any CPU.Build.0 = Debug|Any CPU + {92CD1B60-74B8-4E6E-9E7F-83AC3C792980}.Release|Any CPU.ActiveCfg = Release|Any CPU + {92CD1B60-74B8-4E6E-9E7F-83AC3C792980}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -881,6 +887,7 @@ Global {A8FF0DEB-F371-42FC-8A53-A8C25FE408FC} = {22DF5DC0-1290-4E83-A9D8-6BB7DE3B3E63} {917AEC46-816C-4E05-913E-F0F44C24C437} = {2097345F-4DD3-477D-BC54-A922F9B2B402} {1E743561-B1D4-4100-B6AD-1FD25FA8659B} = {2097345F-4DD3-477D-BC54-A922F9B2B402} + {92CD1B60-74B8-4E6E-9E7F-83AC3C792980} = {2097345F-4DD3-477D-BC54-A922F9B2B402} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {B0816796-CDB3-47D7-8C3C-946434DE3B66} diff --git a/test/OpenTelemetry.Instrumentation.AspNetCore.Benchmark/Instrumentation/AspNetCoreInstrumentationBenchmarks.cs b/test/OpenTelemetry.Instrumentation.AspNetCore.Benchmark/Instrumentation/AspNetCoreInstrumentationBenchmarks.cs new file mode 100644 index 0000000000..1175922312 --- /dev/null +++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Benchmark/Instrumentation/AspNetCoreInstrumentationBenchmarks.cs @@ -0,0 +1,186 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using BenchmarkDotNet.Attributes; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using OpenTelemetry.Metrics; +using OpenTelemetry.Trace; + +/* +BenchmarkDotNet=v0.13.5, OS=Windows 11 (10.0.22621.1702/22H2/2022Update/SunValley2) +Intel Core i7-8850H CPU 2.60GHz (Coffee Lake), 1 CPU, 12 logical and 6 physical cores +.NET SDK=7.0.203 + [Host] : .NET 7.0.5 (7.0.523.17405), X64 RyuJIT AVX2 + DefaultJob : .NET 7.0.5 (7.0.523.17405), X64 RyuJIT AVX2 + + +| Method | EnableInstrumentation | Mean | Error | StdDev | Gen0 | Allocated | +|--------------------------- |---------------------- |---------:|--------:|--------:|-------:|----------:| +| GetRequestForAspNetCoreApp | None | 136.8 us | 1.56 us | 1.46 us | 0.4883 | 2.45 KB | +| GetRequestForAspNetCoreApp | Traces | 148.1 us | 0.88 us | 0.82 us | 0.7324 | 3.57 KB | +| GetRequestForAspNetCoreApp | Metrics | 144.4 us | 1.16 us | 1.08 us | 0.4883 | 2.92 KB | +| GetRequestForAspNetCoreApp | Traces, Metrics | 163.0 us | 1.60 us | 1.49 us | 0.7324 | 3.63 KB | + +Allocation details for .NET 7: + +// Traces +* Activity creation + `Activity.Start()` = 416 B +* Casting of the struct `Microsoft.Extensions.Primitives.StringValues` to `IEnumerable` by `HttpRequestHeaderValuesGetter` + - `TraceContextPropagator.Extract` = 24 B + - `BaggageContextPropagator.Extract` = 24 B +* String creation for `HttpRequest.HostString.Host` = 40 B +* `Activity.TagsLinkedList` (this is allocated on the first Activity.SetTag call) = 40 B +* Boxing of `Port` number when adding it as a tag = 24 B +* String creation in `GetUri` method for adding the http url tag = 66 B +* Setting `Baggage` (Setting AsyncLocal values causes allocation) + - `BaggageHolder` creation = 24 B + - `System.Threading.AsyncLocalValueMap.TwoElementAsyncLocalValueMap` = 48 B + - `System.Threading.ExecutionContext` = 40 B +* `DiagNode>` + - This is allocated eight times for the eight tags that are added = 8 * 40 = 320 B +* `Activity.Stop()` trying to set `Activity.Current` (This happens because of setting another AsyncLocal variable which is `Baggage` + - System.Threading.AsyncLocalValueMap.OneElementAsyncLocalValueMap = 32 B + - System.Threading.ExecutionContext = 40 B + +Baseline = 2.45 KB +With Traces = 2.45 + (1138 / 1024) = 2.45 + 1.12 = 3.57 KB + + +// Metrics +* Activity creation + `Activity.Start()` = 416 B +* Boxing of `Port` number when adding it as a tag = 24 B +* String creation for `HttpRequest.HostString.Host` = 40 B + +Baseline = 2.45 KB +With Metrics = 2.45 + (416 + 40 + 24) / 1024 = 2.45 + 0.47 = 2.92 KB + +// With Traces and Metrics + +Baseline = 2.45 KB +With Traces and Metrics = Baseline + With Traces + (With Metrics - (Activity creation + `Acitivity.Stop()`)) (they use the same activity) + = 2.45 + (1138 + 64) / 1024 = 2.45 + 1.17 = ~3.63KB +*/ + +namespace OpenTelemetry.Instrumentation.AspNetCore.Benchmark.Instrumentation; + +public class AspNetCoreInstrumentationBenchmarks +{ + private HttpClient httpClient; + private WebApplication app; + private TracerProvider tracerProvider; + private MeterProvider meterProvider; + + [Flags] + public enum EnableInstrumentationOption + { + /// + /// Instrumentation is not enabled for any signal. + /// + None = 0, + + /// + /// Instrumentation is enbled only for Traces. + /// + Traces = 1, + + /// + /// Instrumentation is enbled only for Metrics. + /// + Metrics = 2, + } + + [Params(0, 1, 2, 3)] + public EnableInstrumentationOption EnableInstrumentation { get; set; } + + [GlobalSetup(Target = nameof(GetRequestForAspNetCoreApp))] + public void GetRequestForAspNetCoreAppGlobalSetup() + { + if (this.EnableInstrumentation == EnableInstrumentationOption.None) + { + this.StartWebApplication(); + this.httpClient = new HttpClient(); + } + else if (this.EnableInstrumentation == EnableInstrumentationOption.Traces) + { + this.StartWebApplication(); + this.httpClient = new HttpClient(); + + this.tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddAspNetCoreInstrumentation() + .Build(); + } + else if (this.EnableInstrumentation == EnableInstrumentationOption.Metrics) + { + this.StartWebApplication(); + this.httpClient = new HttpClient(); + + this.meterProvider = Sdk.CreateMeterProviderBuilder() + .AddAspNetCoreInstrumentation() + .Build(); + } + else if (this.EnableInstrumentation.HasFlag(EnableInstrumentationOption.Traces) && + this.EnableInstrumentation.HasFlag(EnableInstrumentationOption.Metrics)) + { + this.StartWebApplication(); + this.httpClient = new HttpClient(); + + this.tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddAspNetCoreInstrumentation() + .Build(); + + this.meterProvider = Sdk.CreateMeterProviderBuilder() + .AddAspNetCoreInstrumentation() + .Build(); + } + } + + [GlobalCleanup(Target = nameof(GetRequestForAspNetCoreApp))] + public async Task GetRequestForAspNetCoreAppGlobalCleanup() + { + if (this.EnableInstrumentation == EnableInstrumentationOption.None) + { + this.httpClient.Dispose(); + await this.app.DisposeAsync(); + } + else if (this.EnableInstrumentation == EnableInstrumentationOption.Traces) + { + this.httpClient.Dispose(); + await this.app.DisposeAsync(); + this.tracerProvider.Dispose(); + } + else if (this.EnableInstrumentation == EnableInstrumentationOption.Metrics) + { + this.httpClient.Dispose(); + await this.app.DisposeAsync(); + this.meterProvider.Dispose(); + } + else if (this.EnableInstrumentation.HasFlag(EnableInstrumentationOption.Traces) && + this.EnableInstrumentation.HasFlag(EnableInstrumentationOption.Metrics)) + { + this.httpClient.Dispose(); + await this.app.DisposeAsync(); + this.tracerProvider.Dispose(); + this.meterProvider.Dispose(); + } + } + + [Benchmark] + public async Task GetRequestForAspNetCoreApp() + { + var httpResponse = await this.httpClient.GetAsync(new Uri("http://localhost:5000")).ConfigureAwait(false); + httpResponse.EnsureSuccessStatusCode(); + } + + private void StartWebApplication() + { + var builder = WebApplication.CreateBuilder(); + builder.Logging.ClearProviders(); + var app = builder.Build(); + app.MapGet("/", async context => await context.Response.WriteAsync($"Hello World!")); + app.RunAsync(); + + this.app = app; + } +} diff --git a/test/OpenTelemetry.Instrumentation.AspNetCore.Benchmark/Instrumentation/AspNetCoreInstrumentationNewBenchmarks.cs b/test/OpenTelemetry.Instrumentation.AspNetCore.Benchmark/Instrumentation/AspNetCoreInstrumentationNewBenchmarks.cs new file mode 100644 index 0000000000..9ed7771cd0 --- /dev/null +++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Benchmark/Instrumentation/AspNetCoreInstrumentationNewBenchmarks.cs @@ -0,0 +1,207 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using BenchmarkDotNet.Attributes; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using OpenTelemetry.Metrics; +using OpenTelemetry.Trace; + +/* +// * Summary * + +BenchmarkDotNet=v0.13.5, OS=Windows 11 (10.0.22621.1992/22H2/2022Update/SunValley2), VM=Hyper-V +AMD EPYC 7763, 1 CPU, 16 logical and 8 physical cores +.NET SDK=7.0.306 + [Host] : .NET 7.0.9 (7.0.923.32018), X64 RyuJIT AVX2 + DefaultJob : .NET 7.0.9 (7.0.923.32018), X64 RyuJIT AVX2 + + +| Method | EnableInstrumentation | Mean | Error | StdDev | Allocated | +|--------------------------- |---------------------- |---------:|--------:|--------:|----------:| +| GetRequestForAspNetCoreApp | None | 150.7 us | 1.68 us | 1.57 us | 2.45 KB | +| GetRequestForAspNetCoreApp | Traces | 156.6 us | 3.12 us | 6.37 us | 3.46 KB | +| GetRequestForAspNetCoreApp | Metrics | 148.8 us | 2.87 us | 2.69 us | 2.92 KB | +| GetRequestForAspNetCoreApp | Traces, Metrics | 164.0 us | 3.19 us | 6.22 us | 3.52 KB | + +Allocation details for .NET 7: + +// Traces +* Activity creation + `Activity.Start()` = 416 B +* Casting of the struct `Microsoft.Extensions.Primitives.StringValues` to `IEnumerable` by `HttpRequestHeaderValuesGetter` + - `TraceContextPropagator.Extract` = 24 B + - `BaggageContextPropagator.Extract` = 24 B +* String creation for `HttpRequest.HostString.Host` = 40 B +* `Activity.TagsLinkedList` (this is allocated on the first Activity.SetTag call) = 40 B +* Boxing of `Port` number when adding it as a tag = 24 B +* Setting `Baggage` (Setting AsyncLocal values causes allocation) + - `BaggageHolder` creation = 24 B + - `System.Threading.AsyncLocalValueMap.TwoElementAsyncLocalValueMap` = 48 B + - `System.Threading.ExecutionContext` = 40 B +* `DiagNode>` + - This is allocated seven times for the seven (eight if query string is available) tags that are added = 7 * 40 = 280 B +* `Activity.Stop()` trying to set `Activity.Current` (This happens because of setting another AsyncLocal variable which is `Baggage` + - System.Threading.AsyncLocalValueMap.OneElementAsyncLocalValueMap = 32 B + - System.Threading.ExecutionContext = 40 B + +Baseline = 2.45 KB +With Traces = 2.45 + (1032 / 1024) = 2.45 + 1.01 = 3.46 KB + + +// Metrics +* Activity creation + `Activity.Start()` = 416 B +* Boxing of `Port` number when adding it as a tag = 24 B +* String creation for `HttpRequest.HostString.Host` = 40 B + +Baseline = 2.45 KB +With Metrics = 2.45 + (416 + 40 + 24) / 1024 = 2.45 + 0.47 = 2.92 KB + +// With Traces and Metrics + +Baseline = 2.45 KB +With Traces and Metrics = Baseline + With Traces + (With Metrics - (Activity creation + `Acitivity.Stop()`)) (they use the same activity) + = 2.45 + (1032 + 64) / 1024 = 2.45 + 1.07 = ~3.52KB +*/ +namespace OpenTelemetry.Instrumentation.AspNetCore.Benchmark.Instrumentation; + +public class AspNetCoreInstrumentationNewBenchmarks +{ + private HttpClient httpClient; + private WebApplication app; + private TracerProvider tracerProvider; + private MeterProvider meterProvider; + + [Flags] + public enum EnableInstrumentationOption + { + /// + /// Instrumentation is not enabled for any signal. + /// + None = 0, + + /// + /// Instrumentation is enbled only for Traces. + /// + Traces = 1, + + /// + /// Instrumentation is enbled only for Metrics. + /// + Metrics = 2, + } + + [Params(0, 1, 2, 3)] + public EnableInstrumentationOption EnableInstrumentation { get; set; } + + [GlobalSetup(Target = nameof(GetRequestForAspNetCoreApp))] + public void GetRequestForAspNetCoreAppGlobalSetup() + { + KeyValuePair[] config = new KeyValuePair[] { new KeyValuePair("OTEL_SEMCONV_STABILITY_OPT_IN", "http") }; + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(config) + .Build(); + + if (this.EnableInstrumentation == EnableInstrumentationOption.None) + { + this.StartWebApplication(); + this.httpClient = new HttpClient(); + } + else if (this.EnableInstrumentation == EnableInstrumentationOption.Traces) + { + this.StartWebApplication(); + this.httpClient = new HttpClient(); + + this.tracerProvider = Sdk.CreateTracerProviderBuilder() + .ConfigureServices(services => services.AddSingleton(configuration)) + .AddAspNetCoreInstrumentation() + .Build(); + } + else if (this.EnableInstrumentation == EnableInstrumentationOption.Metrics) + { + this.StartWebApplication(); + this.httpClient = new HttpClient(); + + var exportedItems = new List(); + this.meterProvider = Sdk.CreateMeterProviderBuilder() + .ConfigureServices(services => services.AddSingleton(configuration)) + .AddAspNetCoreInstrumentation() + .AddInMemoryExporter(exportedItems, metricReaderOptions => + { + metricReaderOptions.PeriodicExportingMetricReaderOptions.ExportIntervalMilliseconds = 1000; + }) + .Build(); + } + else if (this.EnableInstrumentation.HasFlag(EnableInstrumentationOption.Traces) && + this.EnableInstrumentation.HasFlag(EnableInstrumentationOption.Metrics)) + { + this.StartWebApplication(); + this.httpClient = new HttpClient(); + + this.tracerProvider = Sdk.CreateTracerProviderBuilder() + .ConfigureServices(services => services.AddSingleton(configuration)) + .AddAspNetCoreInstrumentation() + .Build(); + + var exportedItems = new List(); + this.meterProvider = Sdk.CreateMeterProviderBuilder() + .ConfigureServices(services => services.AddSingleton(configuration)) + .AddAspNetCoreInstrumentation() + .AddInMemoryExporter(exportedItems, metricReaderOptions => + { + metricReaderOptions.PeriodicExportingMetricReaderOptions.ExportIntervalMilliseconds = 1000; + }) + .Build(); + } + } + + [GlobalCleanup(Target = nameof(GetRequestForAspNetCoreApp))] + public async Task GetRequestForAspNetCoreAppGlobalCleanup() + { + if (this.EnableInstrumentation == EnableInstrumentationOption.None) + { + this.httpClient.Dispose(); + await this.app.DisposeAsync(); + } + else if (this.EnableInstrumentation == EnableInstrumentationOption.Traces) + { + this.httpClient.Dispose(); + await this.app.DisposeAsync(); + this.tracerProvider.Dispose(); + } + else if (this.EnableInstrumentation == EnableInstrumentationOption.Metrics) + { + this.httpClient.Dispose(); + await this.app.DisposeAsync(); + this.meterProvider.Dispose(); + } + else if (this.EnableInstrumentation.HasFlag(EnableInstrumentationOption.Traces) && + this.EnableInstrumentation.HasFlag(EnableInstrumentationOption.Metrics)) + { + this.httpClient.Dispose(); + await this.app.DisposeAsync(); + this.tracerProvider.Dispose(); + this.meterProvider.Dispose(); + } + } + + [Benchmark] + public async Task GetRequestForAspNetCoreApp() + { + var httpResponse = await this.httpClient.GetAsync(new Uri("http://localhost:5000")).ConfigureAwait(false); + httpResponse.EnsureSuccessStatusCode(); + } + + private void StartWebApplication() + { + var builder = WebApplication.CreateBuilder(); + builder.Logging.ClearProviders(); + var app = builder.Build(); + app.MapGet("/", async context => await context.Response.WriteAsync($"Hello World!")); + app.RunAsync(); + + this.app = app; + } +} diff --git a/test/OpenTelemetry.Instrumentation.AspNetCore.Benchmark/OpenTelemetry.Instrumentation.AspNetCore.Benchmark.csproj b/test/OpenTelemetry.Instrumentation.AspNetCore.Benchmark/OpenTelemetry.Instrumentation.AspNetCore.Benchmark.csproj new file mode 100644 index 0000000000..84fe25b212 --- /dev/null +++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Benchmark/OpenTelemetry.Instrumentation.AspNetCore.Benchmark.csproj @@ -0,0 +1,21 @@ + + + Exe + + $(SupportedNetTargets) + enable + disable + + + + + + + + + + + + + + diff --git a/test/OpenTelemetry.Instrumentation.AspNetCore.Benchmark/Program.cs b/test/OpenTelemetry.Instrumentation.AspNetCore.Benchmark/Program.cs new file mode 100644 index 0000000000..612690cb98 --- /dev/null +++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Benchmark/Program.cs @@ -0,0 +1,11 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using BenchmarkDotNet.Running; + +namespace OpenTelemetry.Instrumentation.AspNetCore.Benchmark; + +internal class Program +{ + private static void Main(string[] args) => BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args); +} diff --git a/test/OpenTelemetry.Instrumentation.AspNetCore.Benchmark/README.md b/test/OpenTelemetry.Instrumentation.AspNetCore.Benchmark/README.md new file mode 100644 index 0000000000..075e40772c --- /dev/null +++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Benchmark/README.md @@ -0,0 +1,11 @@ +# OpenTelemetry ASP.NET Core Instrumentation Benchmarks + +Navigate to `./test/OpenTelemetry.Instrumentation.AspNetCore.Benchmark` directory +and run the following command: + +```sh +dotnet run -c Release -f net8.0 -- -m +`` + +Then choose the benchmark class that you want to run by entering the required +option number from the list of options shown on the Console window. diff --git a/test/OpenTelemetry.Instrumentation.Http.Benchmark/README.md b/test/OpenTelemetry.Instrumentation.Http.Benchmark/README.md index b855fb74fd..2cc3e82fbc 100644 --- a/test/OpenTelemetry.Instrumentation.Http.Benchmark/README.md +++ b/test/OpenTelemetry.Instrumentation.Http.Benchmark/README.md @@ -1,4 +1,4 @@ -# OpenTelemetry GenevaExporter Benchmarks +# OpenTelemetry HTTP Instrumentation Benchmarks Navigate to `./test/OpenTelemetry.Instrumentation.Http.Benchmark` directory and run the following command: From 91063ef66219e151503e9fe68ee6862a769465cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Kie=C5=82kowicz?= Date: Wed, 8 May 2024 16:17:01 +0200 Subject: [PATCH 26/26] changelog --- src/OpenTelemetry.Instrumentation.AspNetCore/CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/OpenTelemetry.Instrumentation.AspNetCore/CHANGELOG.md b/src/OpenTelemetry.Instrumentation.AspNetCore/CHANGELOG.md index d8f74ef6cc..2b31a759e3 100644 --- a/src/OpenTelemetry.Instrumentation.AspNetCore/CHANGELOG.md +++ b/src/OpenTelemetry.Instrumentation.AspNetCore/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Unreleased + +* Update `OpenTelemetry.Api.ProviderBuilderExtensions` to `1.8.1`. + * Update `OpenTelemetry.Api` to `1.8.1`. + ([#1668](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/1668)) + ## 1.8.1 Released 2024-Apr-12