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..0e86e80ccf
--- /dev/null
+++ b/build/Projects/OpenTelemetry.Instrumentation.AspNetCore.proj
@@ -0,0 +1,35 @@
+
+
+
+ $([System.IO.Directory]::GetParent($(MSBuildThisFileDirectory)).Parent.Parent.FullName)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/opentelemetry-dotnet-contrib.sln b/opentelemetry-dotnet-contrib.sln
index 1830e85346..0809cbac97 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
@@ -369,6 +371,14 @@ 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
+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
@@ -747,6 +757,22 @@ 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
+ {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
@@ -858,6 +884,10 @@ 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}
+ {92CD1B60-74B8-4E6E-9E7F-83AC3C792980} = {2097345F-4DD3-477D-BC54-A922F9B2B402}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {B0816796-CDB3-47D7-8C3C-946434DE3B66}
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..a9d7ef7c8a
--- /dev/null
+++ b/src/OpenTelemetry.Instrumentation.AspNetCore/AspNetCoreInstrumentationMeterProviderBuilderExtensions.cs
@@ -0,0 +1,54 @@
+// 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;
+ _ = TelemetryHelper.RequestDataHelper;
+
+ builder.AddMeter(HttpInMetricsListener.InstrumentationName);
+
+#pragma warning disable CA2000
+ builder.AddInstrumentation(new AspNetCoreMetrics());
+#pragma warning restore CA2000
+
+ 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..00fa0d9435
--- /dev/null
+++ b/src/OpenTelemetry.Instrumentation.AspNetCore/AspNetCoreInstrumentationTracerProviderBuilderExtensions.cs
@@ -0,0 +1,125 @@
+// 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;
+ _ = TelemetryHelper.RequestDataHelper;
+
+ 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();
+
+#pragma warning disable CA2000
+ return builder.AddInstrumentation(
+ new AspNetCoreInstrumentation(listener));
+#pragma warning restore CA2000
+ }
+
+ 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..2b31a759e3
--- /dev/null
+++ b/src/OpenTelemetry.Instrumentation.AspNetCore/CHANGELOG.md
@@ -0,0 +1,644 @@
+# 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
+
+* **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..89e8c1e278
--- /dev/null
+++ b/src/OpenTelemetry.Instrumentation.AspNetCore/Implementation/HttpInListener.cs
@@ -0,0 +1,399 @@
+// 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 const 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
+ {
+#pragma warning disable CA2000
+ newOne = new Activity(ActivityOperationName);
+#pragma warning restore CA2000
+ 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() : "/";
+ 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
+
+ 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));
+ }
+ }
+
+ TelemetryHelper.RequestDataHelper.SetHttpMethodTag(activity, request.Method);
+
+ activity.SetTag(SemanticConventions.AttributeUrlScheme, request.Scheme);
+ activity.SetTag(SemanticConventions.AttributeUrlPath, path);
+ activity.SetTag(SemanticConventions.AttributeNetworkProtocolVersion, RequestDataHelper.GetHttpProtocolVersion(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))
+ {
+ TelemetryHelper.RequestDataHelper.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.ResolveActivityStatusForHttpStatusCode(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..cf9682d1fa
--- /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, 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 = TelemetryHelper.RequestDataHelper.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/TelemetryHelper.cs b/src/OpenTelemetry.Instrumentation.AspNetCore/Implementation/TelemetryHelper.cs
new file mode 100644
index 0000000000..4e8cd55524
--- /dev/null
+++ b/src/OpenTelemetry.Instrumentation.AspNetCore/Implementation/TelemetryHelper.cs
@@ -0,0 +1,33 @@
+// 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)
+ {
+ if (statusCode >= 100 && statusCode < 600)
+ {
+ return BoxedStatusCodes[statusCode - 100];
+ }
+
+ return statusCode;
+ }
+
+ private static object[] InitializeBoxedStatusCodes()
+ {
+ var boxedStatusCodes = new object[500];
+ for (int i = 0, c = 100; i < boxedStatusCodes.Length; i++, c++)
+ {
+ boxedStatusCodes[i] = c;
+ }
+
+ return boxedStatusCodes;
+ }
+}
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..c415de0ab1
--- /dev/null
+++ b/src/OpenTelemetry.Instrumentation.AspNetCore/OpenTelemetry.Instrumentation.AspNetCore.csproj
@@ -0,0 +1,52 @@
+
+
+
+ net8.0;net7.0;net6.0;netstandard2.0
+ ASP.NET Core instrumentation for OpenTelemetry .NET
+ $(PackageTags);distributed-tracing;AspNetCore
+ Instrumentation.AspNetCore-
+ 1.8.1
+
+ enable
+
+ 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/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.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 @@
+
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.AspNetCore.Tests/BasicTests.cs b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/BasicTests.cs
new file mode 100644
index 0000000000..5590a2fb3e
--- /dev/null
+++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/BasicTests.cs
@@ -0,0 +1,1272 @@
+// 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.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;
+using Uri = System.Uri;
+
+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
+{
+ private readonly WebApplicationFactory factory;
+ private TracerProvider tracerProvider;
+
+ 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(new Uri("/api/values", UriKind.Relative));
+
+ // 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(new Uri("/api/values", UriKind.Relative));
+
+ // 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(new Uri("/api/values/2", UriKind.Relative));
+ 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(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
+ 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(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
+ 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(new Uri("/api/values", UriKind.Relative));
+
+ // 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 TestTestActivityMiddleware(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(new Uri("/api/values/2", UriKind.Relative));
+ 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(new Uri("/api/values/2", UriKind.Relative));
+ 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(new Uri("/api/values", UriKind.Relative));
+
+ // 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(new Uri("/api/error", UriKind.Relative));
+
+ 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);
+ }
+
+ [Fact]
+ public async Task NoSiblingActivityCreatedWhenTraceFlagsNone()
+ {
+ using var localTracerProvider = 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);
+ }
+
+ [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(new Uri(path, UriKind.Relative));
+ }
+ 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) : TestActivityMiddleware
+ {
+ 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 TestTestActivityMiddleware(string activitySourceName, string activityName) : TestActivityMiddleware
+ {
+ 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..a877d060a3
--- /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.Trace;
+using Xunit;
+
+namespace OpenTelemetry.Instrumentation.AspNetCore.Tests;
+
+[Collection("AspNetCore")]
+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..f6bc29a386
--- /dev/null
+++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/IncomingRequestsCollectionsIsAccordingToTheSpecTests.cs
@@ -0,0 +1,172 @@
+// 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;
+
+[Collection("AspNetCore")]
+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 ExceptionTestCallbackMiddleware(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(new Uri(path, UriKind.Relative));
+ }
+ 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));
+ }
+ }
+
+ internal class ExceptionTestCallbackMiddleware : TestCallbackMiddleware
+ {
+ private readonly int statusCode;
+ private readonly string reasonPhrase;
+
+ public ExceptionTestCallbackMiddleware(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", StringComparison.Ordinal))
+ {
+ 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..21743e0faf
--- /dev/null
+++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/MetricTests.cs
@@ -0,0 +1,418 @@
+// 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;
+
+[Collection("AspNetCore")]
+public class MetricTests(WebApplicationFactory factory)
+ : IClassFixture>, IDisposable
+{
+ 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(new Uri($"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(new Uri($"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(new Uri(api, UriKind.Relative));
+ 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..4371b4f6ab
--- /dev/null
+++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/OpenTelemetry.Instrumentation.AspNetCore.Tests.csproj
@@ -0,0 +1,48 @@
+
+
+ Unit test project for OpenTelemetry ASP.NET Core instrumentation
+ net8.0;net7.0;net6.0
+ enable
+
+ disable
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 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..44d0e84e43
--- /dev/null
+++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/RoutingTestCases.cs
@@ -0,0 +1,45 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+#nullable enable
+
+using System.Reflection;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace RouteTests;
+
+public static class RoutingTestCases
+{
+ private static readonly JsonSerializerOptions JsonSerializerOptions = new JsonSerializerOptions
+ {
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
+ Converters = { new JsonStringEnumConverter() },
+ };
+
+ public static IEnumerable