diff --git a/tracer/build/supported_calltargets.g.json b/tracer/build/supported_calltargets.g.json
index e88f055d8947..cde0f5b56113 100644
--- a/tracer/build/supported_calltargets.g.json
+++ b/tracer/build/supported_calltargets.g.json
@@ -1569,7 +1569,7 @@
"InstrumentationTypeName": "Datadog.Trace.ClrProfiler.AutoInstrumentation.AspNet.ControllerActionInvoker_InvokeAction_Integration",
"IntegrationKind": 0,
"IsAdoNetIntegration": false,
- "InstrumentationCategory": 6
+ "InstrumentationCategory": 7
},
{
"IntegrationName": "AspNetWebApi2",
@@ -1620,7 +1620,7 @@
"InstrumentationTypeName": "Datadog.Trace.ClrProfiler.AutoInstrumentation.AspNet.ReflectedHttpActionDescriptor_ExecuteAsync_Integration",
"IntegrationKind": 0,
"IsAdoNetIntegration": false,
- "InstrumentationCategory": 6
+ "InstrumentationCategory": 7
},
{
"IntegrationName": "AspNetWebApi2",
diff --git a/tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/AspNet/ActionDescriptorWithMethodInfo.cs b/tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/AspNet/ActionDescriptorWithMethodInfo.cs
new file mode 100644
index 000000000000..7cfe5eac00aa
--- /dev/null
+++ b/tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/AspNet/ActionDescriptorWithMethodInfo.cs
@@ -0,0 +1,25 @@
+//
+// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License.
+// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc.
+//
+
+#if NETFRAMEWORK
+#nullable enable
+
+using System.Reflection;
+using Datadog.Trace.DuckTyping;
+
+namespace Datadog.Trace.ClrProfiler.AutoInstrumentation.AspNet
+{
+ ///
+ /// Duck type for MVC/WebApi2 descriptors that expose a MethodInfo.
+ ///
+ [DuckCopy]
+ internal struct ActionDescriptorWithMethodInfo
+ {
+ [Duck]
+ public MethodInfo MethodInfo;
+ }
+}
+#endif
+
diff --git a/tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/AspNet/ControllerActionInvoker_InvokeAction_Integration.cs b/tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/AspNet/ControllerActionInvoker_InvokeAction_Integration.cs
index 370917f4cd3b..e611bbdc1439 100644
--- a/tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/AspNet/ControllerActionInvoker_InvokeAction_Integration.cs
+++ b/tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/AspNet/ControllerActionInvoker_InvokeAction_Integration.cs
@@ -8,14 +8,18 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
+using System.Reflection;
using System.Web;
using Datadog.Trace.AppSec;
using Datadog.Trace.AppSec.Coordinator;
using Datadog.Trace.AspNet;
using Datadog.Trace.ClrProfiler.CallTarget;
using Datadog.Trace.Configuration;
+using Datadog.Trace.Debugger;
+using Datadog.Trace.Debugger.SpanCodeOrigin;
using Datadog.Trace.DuckTyping;
using Datadog.Trace.Logging;
+using Datadog.Trace.Vendors.Serilog.Events;
namespace Datadog.Trace.ClrProfiler.AutoInstrumentation.AspNet
{
@@ -31,7 +35,7 @@ namespace Datadog.Trace.ClrProfiler.AutoInstrumentation.AspNet
MinimumVersion = "4",
MaximumVersion = "5",
IntegrationName = IntegrationName,
- InstrumentationCategory = InstrumentationCategory.AppSec | InstrumentationCategory.Iast)]
+ InstrumentationCategory = InstrumentationCategory.Tracing | InstrumentationCategory.AppSec | InstrumentationCategory.Iast)]
// ReSharper disable once InconsistentNaming
[Browsable(false)]
[EditorBrowsable(EditorBrowsableState.Never)]
@@ -70,9 +74,62 @@ internal static CallTargetState OnMethodBegin(TActionDescriptor actionDescriptor, SpanCodeOrigin codeOrigin)
+ {
+ if (actionDescriptor is null)
+ {
+ return;
+ }
+
+ var httpContext = HttpContext.Current;
+ if (SharedItems.TryPeekScope(httpContext, AspNetMvcIntegration.HttpContextKey) is not { Root.Span: { } rootSpan })
+ {
+ if (Log.IsEnabled(LogEventLevel.Debug))
+ {
+ Log.Debug(
+ "Code origin is enabled but scope was not found in HttpContext (key: {HttpContextKey}, httpContextNull: {HttpContextIsNull}, itemsCount: {HttpContextItemsCount}, actionDescriptorType: {ActionDescriptorType}).",
+ AspNetMvcIntegration.HttpContextKey,
+ httpContext is null,
+ httpContext?.Items?.Count ?? 0,
+ actionDescriptor.GetType());
+ }
+
+ return;
+ }
+
+ if (!actionDescriptor.TryDuckCast(out var reflected)
+ || reflected.MethodInfo is not { } actionMethod
+ || actionMethod.DeclaringType is not { } actionType)
+ {
+ if (Log.IsEnabled(LogEventLevel.Debug))
+ {
+ Log.Debug(
+ "Code origin is enabled but could not extract action from ActionDescriptor type {ActionDescriptorType} or action MethodInfo has no DeclaringType.",
+ actionDescriptor.GetType());
+ }
+
+ return;
+ }
+
+ codeOrigin.SetCodeOriginForEntrySpan(rootSpan, actionType, actionMethod);
+ }
+
///
/// OnMethodEnd callback
///
diff --git a/tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/AspNet/ReflectedHttpActionDescriptor_ExecuteAsync_Integration.cs b/tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/AspNet/ReflectedHttpActionDescriptor_ExecuteAsync_Integration.cs
index 10634ea0386f..43e3d8d4ba1e 100644
--- a/tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/AspNet/ReflectedHttpActionDescriptor_ExecuteAsync_Integration.cs
+++ b/tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/AspNet/ReflectedHttpActionDescriptor_ExecuteAsync_Integration.cs
@@ -16,8 +16,11 @@
using Datadog.Trace.AspNet;
using Datadog.Trace.ClrProfiler.CallTarget;
using Datadog.Trace.Configuration;
+using Datadog.Trace.Debugger;
+using Datadog.Trace.Debugger.SpanCodeOrigin;
using Datadog.Trace.DuckTyping;
using Datadog.Trace.Logging;
+using Datadog.Trace.Vendors.Serilog.Events;
namespace Datadog.Trace.ClrProfiler.AutoInstrumentation.AspNet
{
@@ -33,7 +36,7 @@ namespace Datadog.Trace.ClrProfiler.AutoInstrumentation.AspNet
MinimumVersion = "5.1",
MaximumVersion = "5",
IntegrationName = IntegrationName,
- InstrumentationCategory = InstrumentationCategory.AppSec | InstrumentationCategory.Iast)]
+ InstrumentationCategory = InstrumentationCategory.Tracing | InstrumentationCategory.AppSec | InstrumentationCategory.Iast)]
// ReSharper disable once InconsistentNaming
[Browsable(false)]
[EditorBrowsable(EditorBrowsableState.Never)]
@@ -70,9 +73,62 @@ internal static CallTargetState OnMethodBegin(TTarget inst
Log.Error(ex, "Error instrumenting method {MethodName}", "System.Web.Http.Controllers.ReflectedHttpActionDescriptor.ExecuteAsync()");
}
+ try
+ {
+ var codeOrigin = DebuggerManager.Instance.CodeOrigin;
+ if (codeOrigin is { Settings.CodeOriginForSpansEnabled: true })
+ {
+ AddSpanCodeOrigin(instance, codeOrigin);
+ }
+ }
+ catch (Exception ex) when (BlockException.GetBlockException(ex) is null)
+ {
+ Log.Error(ex, "Error adding code origin for spans in {MethodName}", "System.Web.Http.Controllers.ReflectedHttpActionDescriptor.ExecuteAsync()");
+ }
+
return CallTargetState.GetDefault();
}
+ private static void AddSpanCodeOrigin(TTarget instance, SpanCodeOrigin codeOrigin)
+ {
+ if (instance == null)
+ {
+ return;
+ }
+
+ var httpContext = HttpContext.Current;
+ if (SharedItems.TryPeekScope(httpContext, AspNetWebApi2Integration.HttpContextKey) is not { Root.Span: { } rootSpan })
+ {
+ if (Log.IsEnabled(LogEventLevel.Debug))
+ {
+ Log.Debug(
+ "Code origin is enabled but scope was not found in HttpContext (key: {HttpContextKey}, httpContextNull: {HttpContextIsNull}, itemsCount: {HttpContextItemsCount}, actionDescriptorType: {ActionDescriptorType}).",
+ AspNetWebApi2Integration.HttpContextKey,
+ httpContext is null,
+ httpContext?.Items?.Count ?? 0,
+ instance.GetType());
+ }
+
+ return;
+ }
+
+ if (!instance.TryDuckCast(out var reflected)
+ || reflected.MethodInfo is not { } actionMethod
+ || actionMethod.DeclaringType is not { } actionType)
+ {
+ if (Log.IsEnabled(LogEventLevel.Debug))
+ {
+ Log.Debug(
+ "Code origin is enabled but could not extract action from HttpActionDescriptor type {ActionDescriptorType} or action has no DeclaringType.",
+ instance.GetType());
+ }
+
+ return;
+ }
+
+ codeOrigin.SetCodeOriginForEntrySpan(rootSpan, actionType, actionMethod);
+ }
+
internal static TResponse? OnAsyncMethodEnd(TTarget instance, TResponse? response, Exception? exception, in CallTargetState state)
{
var security = Security.Instance;
diff --git a/tracer/src/Datadog.Trace/Debugger/SpanCodeOrigin/EndpointDetector.cs b/tracer/src/Datadog.Trace/Debugger/SpanCodeOrigin/EndpointDetector.cs
index 1dba428e478d..363a5741b09a 100644
--- a/tracer/src/Datadog.Trace/Debugger/SpanCodeOrigin/EndpointDetector.cs
+++ b/tracer/src/Datadog.Trace/Debugger/SpanCodeOrigin/EndpointDetector.cs
@@ -1,4 +1,4 @@
-//
+//
// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License.
// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc.
//
@@ -18,20 +18,20 @@ namespace Datadog.Trace.Debugger.SpanCodeOrigin;
internal static class EndpointDetector
{
- private static readonly HashSet ControllerAttributes =
+ private static readonly HashSet AspNetCoreControllerAttributes =
[
"Microsoft.AspNetCore.Mvc.ApiControllerAttribute",
"Microsoft.AspNetCore.Mvc.ControllerAttribute",
"Microsoft.AspNetCore.Mvc.RouteAttribute"
];
- private static readonly HashSet ControllerBaseNames =
+ private static readonly HashSet AspNetCoreControllerBaseNames =
[
"Microsoft.AspNetCore.Mvc.Controller",
"Microsoft.AspNetCore.Mvc.ControllerBase"
];
- private static readonly HashSet ActionAttributes =
+ private static readonly HashSet AspNetCoreActionAttributes =
[
"Microsoft.AspNetCore.Mvc.HttpGetAttribute",
"Microsoft.AspNetCore.Mvc.HttpPostAttribute",
@@ -43,6 +43,25 @@ internal static class EndpointDetector
"Microsoft.AspNetCore.Mvc.Routing.HttpMethodAttribute"
];
+ private static readonly HashSet NetFrameworkControllerBaseNames =
+ [
+ // MVC 4/5
+ "System.Web.Mvc.Controller",
+ "System.Web.Mvc.ControllerBase",
+
+ // Web API 2
+ "System.Web.Http.ApiController"
+ ];
+
+ private static readonly HashSet NetFrameworkNonActionAttributes =
+ [
+ // MVC 4/5
+ "System.Web.Mvc.NonActionAttribute",
+
+ // Web API 2
+ "System.Web.Http.NonActionAttribute"
+ ];
+
private static readonly HashSet SignalRHubBaseNames =
[
"Microsoft.AspNetCore.SignalR.Hub",
@@ -74,7 +93,9 @@ internal static ImmutableHashSet GetEndpointMethodTokens(DatadogMetadataRea
bool isPageModel = false;
bool isSignalRHub = false;
bool isCompilerGeneratedType = false;
- var isController = IsInheritFromTypesOrHasAttribute(typeDef, metadataReader, ControllerAttributes, ControllerBaseNames);
+ var isAspNetCoreController = IsInheritFromTypesOrHasAttribute(typeDef, metadataReader, AspNetCoreControllerAttributes, AspNetCoreControllerBaseNames);
+ var isNetFrameworkController = !isAspNetCoreController && IsInheritFromTypes(typeDef, metadataReader, NetFrameworkControllerBaseNames);
+ var isController = isAspNetCoreController || isNetFrameworkController;
if (!isController)
{
isPageModel = IsInheritFromTypes(typeDef, metadataReader, PageModelBaseNames);
@@ -101,7 +122,13 @@ internal static ImmutableHashSet GetEndpointMethodTokens(DatadogMetadataRea
continue;
}
- if (isController && HasAttributeFromSet(methodDef.GetCustomAttributes(), metadataReader, ActionAttributes))
+ if (isAspNetCoreController && HasAttributeFromSet(methodDef.GetCustomAttributes(), metadataReader, AspNetCoreActionAttributes))
+ {
+ builder.Add(metadataReader.GetToken(methodHandle));
+ continue;
+ }
+
+ if (isNetFrameworkController && !HasAttributeFromSet(methodDef.GetCustomAttributes(), metadataReader, NetFrameworkNonActionAttributes))
{
builder.Add(metadataReader.GetToken(methodHandle));
continue;
@@ -141,7 +168,8 @@ private static bool IsValidMethod(MethodDefinition methodDef)
{
var attributes = methodDef.Attributes;
return (attributes & MethodAttributes.Public) != 0 &&
- (attributes & MethodAttributes.Static) == 0;
+ (attributes & MethodAttributes.Static) == 0 &&
+ (attributes & MethodAttributes.SpecialName) == 0;
}
private static bool IsInheritFromTypesOrHasAttribute(TypeDefinition typeDef, MetadataReader reader, HashSet attributesNames, HashSet baseTypeNames)
diff --git a/tracer/src/Datadog.Tracer.Native/Generated/generated_calltargets.g.cpp b/tracer/src/Datadog.Tracer.Native/Generated/generated_calltargets.g.cpp
index dff661caa48c..6cbb5e6e48fb 100644
--- a/tracer/src/Datadog.Tracer.Native/Generated/generated_calltargets.g.cpp
+++ b/tracer/src/Datadog.Tracer.Native/Generated/generated_calltargets.g.cpp
@@ -483,9 +483,9 @@ std::vector callTargets =
#if _WIN32
{(WCHAR*)WStr("System.Web.Mvc"),(WCHAR*)WStr("System.Web.Mvc.Async.AsyncControllerActionInvoker"),(WCHAR*)WStr("BeginInvokeAction"),sig145,5,4,0,0,5,65535,65535,assemblyName,(WCHAR*)WStr("Datadog.Trace.ClrProfiler.AutoInstrumentation.AspNet.AsyncControllerActionInvoker_BeginInvokeAction_Integration"),CallTargetKind::Default,1,1},
{(WCHAR*)WStr("System.Web.Mvc"),(WCHAR*)WStr("System.Web.Mvc.Async.AsyncControllerActionInvoker"),(WCHAR*)WStr("EndInvokeAction"),sig124,2,4,0,0,5,65535,65535,assemblyName,(WCHAR*)WStr("Datadog.Trace.ClrProfiler.AutoInstrumentation.AspNet.AsyncControllerActionInvoker_EndInvokeAction_Integration"),CallTargetKind::Default,1,1},
-{(WCHAR*)WStr("System.Web.Mvc"),(WCHAR*)WStr("System.Web.Mvc.ControllerActionInvoker"),(WCHAR*)WStr("InvokeActionMethod"),sig394,4,4,0,0,5,65535,65535,assemblyName,(WCHAR*)WStr("Datadog.Trace.ClrProfiler.AutoInstrumentation.AspNet.ControllerActionInvoker_InvokeAction_Integration"),CallTargetKind::Default,6,1},
+{(WCHAR*)WStr("System.Web.Mvc"),(WCHAR*)WStr("System.Web.Mvc.ControllerActionInvoker"),(WCHAR*)WStr("InvokeActionMethod"),sig394,4,4,0,0,5,65535,65535,assemblyName,(WCHAR*)WStr("Datadog.Trace.ClrProfiler.AutoInstrumentation.AspNet.ControllerActionInvoker_InvokeAction_Integration"),CallTargetKind::Default,7,1},
{(WCHAR*)WStr("System.Web.Http"),(WCHAR*)WStr("System.Web.Http.ApiController"),(WCHAR*)WStr("ExecuteAsync"),sig299,3,5,1,0,5,65535,65535,assemblyName,(WCHAR*)WStr("Datadog.Trace.ClrProfiler.AutoInstrumentation.AspNet.ApiController_ExecuteAsync_Integration"),CallTargetKind::Default,1,1},
-{(WCHAR*)WStr("System.Web.Http"),(WCHAR*)WStr("System.Web.Http.Controllers.ReflectedHttpActionDescriptor"),(WCHAR*)WStr("ExecuteAsync"),sig298,4,5,1,0,5,65535,65535,assemblyName,(WCHAR*)WStr("Datadog.Trace.ClrProfiler.AutoInstrumentation.AspNet.ReflectedHttpActionDescriptor_ExecuteAsync_Integration"),CallTargetKind::Default,6,1},
+{(WCHAR*)WStr("System.Web.Http"),(WCHAR*)WStr("System.Web.Http.Controllers.ReflectedHttpActionDescriptor"),(WCHAR*)WStr("ExecuteAsync"),sig298,4,5,1,0,5,65535,65535,assemblyName,(WCHAR*)WStr("Datadog.Trace.ClrProfiler.AutoInstrumentation.AspNet.ReflectedHttpActionDescriptor_ExecuteAsync_Integration"),CallTargetKind::Default,7,1},
{(WCHAR*)WStr("System.Web.Http"),(WCHAR*)WStr("System.Web.Http.ExceptionHandling.ExceptionHandlerExtensions"),(WCHAR*)WStr("HandleAsync"),sig300,4,5,1,0,5,65535,65535,assemblyName,(WCHAR*)WStr("Datadog.Trace.ClrProfiler.AutoInstrumentation.AspNet.ExceptionHandlerExtensions_HandleAsync_Integration"),CallTargetKind::Default,1,1},
#endif
{(WCHAR*)WStr("AWSSDK.DynamoDBv2"),(WCHAR*)WStr("Amazon.DynamoDBv2.AmazonDynamoDBClient"),(WCHAR*)WStr("BatchGetItem"),sig008,2,3,0,0,4,65535,65535,assemblyName,(WCHAR*)WStr("Datadog.Trace.ClrProfiler.AutoInstrumentation.AWS.DynamoDb.BatchGetItemIntegration"),CallTargetKind::Default,1,15},
diff --git a/tracer/test/Datadog.Trace.ClrProfiler.IntegrationTests/AspNet/AspNetMvc5CodeOriginTests.cs b/tracer/test/Datadog.Trace.ClrProfiler.IntegrationTests/AspNet/AspNetMvc5CodeOriginTests.cs
new file mode 100644
index 000000000000..59dbb681016c
--- /dev/null
+++ b/tracer/test/Datadog.Trace.ClrProfiler.IntegrationTests/AspNet/AspNetMvc5CodeOriginTests.cs
@@ -0,0 +1,114 @@
+//
+// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License.
+// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc.
+//
+
+#if NETFRAMEWORK
+
+using System;
+using System.Collections.Generic;
+using System.Net;
+using System.Text.RegularExpressions;
+using System.Threading.Tasks;
+using Datadog.Trace.Configuration;
+using Datadog.Trace.ExtensionMethods;
+using Datadog.Trace.TestHelpers;
+using VerifyTests;
+using VerifyXunit;
+using Xunit;
+using Xunit.Abstractions;
+#pragma warning disable SA1402
+
+namespace Datadog.Trace.ClrProfiler.IntegrationTests
+{
+ [UsesVerify]
+ public abstract class AspNetMvc5CodeOriginTests : TracingIntegrationTest, IClassFixture, IAsyncLifetime
+ {
+ private readonly IisFixture _iisFixture;
+ private readonly string _testName;
+ private readonly bool _classicMode;
+
+ protected AspNetMvc5CodeOriginTests(IisFixture iisFixture, ITestOutputHelper output, bool classicMode)
+ : base("AspNetMvc5", @"test\test-applications\aspnet", output)
+ {
+ _iisFixture = iisFixture;
+ _iisFixture.ShutdownPath = "/home/shutdown";
+ _classicMode = classicMode;
+ _testName = $"{nameof(AspNetMvc5CodeOriginTests)}.{(classicMode ? "Classic" : "Integrated")}";
+
+ SetServiceVersion("1.0.0");
+ SetEnvironmentVariable(ConfigurationKeys.Debugger.CodeOriginForSpansEnabled, "true");
+ }
+
+ public override Result ValidateIntegrationSpan(MockSpan span, string metadataSchemaVersion) =>
+ span.Name switch
+ {
+ "aspnet.request" => span.IsAspNet(metadataSchemaVersion),
+ "aspnet-mvc.request" => span.IsAspNetMvc(metadataSchemaVersion),
+ _ => Result.DefaultSuccess,
+ };
+
+ [SkippableFact]
+ [Trait("Category", "EndToEnd")]
+ [Trait("RunOnWindows", "True")]
+ [Trait("LoadFromGAC", "True")]
+ public async Task AddsCodeOriginToAspNetRequestSpan()
+ {
+ const string path = "/Home/Index";
+ const int statusCode = 200;
+
+ var spans = await GetWebServerSpans(
+ path: _iisFixture.VirtualApplicationPath + path,
+ agent: _iisFixture.Agent,
+ httpPort: _iisFixture.HttpPort,
+ expectedHttpStatusCode: HttpStatusCode.OK,
+ expectedSpanCount: 2);
+
+ var sanitisedPath = VerifyHelper.SanitisePathsForVerify(path);
+ var settings = VerifyHelper.GetSpanVerifierSettings(sanitisedPath, statusCode);
+
+ AddCodeOriginScrubbers(settings);
+
+ await Verifier.Verify(spans, settings)
+ .UseFileName($"{_testName}.__path={sanitisedPath}_statusCode={statusCode}");
+ }
+
+ public Task InitializeAsync() => _iisFixture.TryStartIis(this, _classicMode ? IisAppType.AspNetClassic : IisAppType.AspNetIntegrated);
+
+ public Task DisposeAsync() => Task.CompletedTask;
+
+ private static void AddCodeOriginScrubbers(VerifySettings settings)
+ {
+ // File paths are machine dependent, and line/column numbers are sensitive to unrelated edits.
+ // We scrub the values but keep the tags present in the snapshot.
+ settings.AddRegexScrubber(
+ new Regex(@"_dd\.code_origin\.frames\.(\d+)\.file: .*", VerifyHelper.RegOptions),
+ "_dd.code_origin.frames.$1.file: ");
+
+ settings.AddRegexScrubber(
+ new Regex(@"_dd\.code_origin\.frames\.(\d+)\.(line|column): \d+", VerifyHelper.RegOptions),
+ "_dd.code_origin.frames.$1.$2: 0");
+ }
+ }
+
+ [Collection("IisTests")]
+ public class AspNetMvc5CodeOriginTestsClassic : AspNetMvc5CodeOriginTests
+ {
+ public AspNetMvc5CodeOriginTestsClassic(IisFixture iisFixture, ITestOutputHelper output)
+ : base(iisFixture, output, classicMode: true)
+ {
+ }
+ }
+
+ [Collection("IisTests")]
+ public class AspNetMvc5CodeOriginTestsIntegrated : AspNetMvc5CodeOriginTests
+ {
+ public AspNetMvc5CodeOriginTestsIntegrated(IisFixture iisFixture, ITestOutputHelper output)
+ : base(iisFixture, output, classicMode: false)
+ {
+ }
+ }
+}
+
+#endif
+
diff --git a/tracer/test/Datadog.Trace.ClrProfiler.IntegrationTests/AspNet/AspNetWebApi2CodeOriginTests.cs b/tracer/test/Datadog.Trace.ClrProfiler.IntegrationTests/AspNet/AspNetWebApi2CodeOriginTests.cs
new file mode 100644
index 000000000000..e9528be983d6
--- /dev/null
+++ b/tracer/test/Datadog.Trace.ClrProfiler.IntegrationTests/AspNet/AspNetWebApi2CodeOriginTests.cs
@@ -0,0 +1,113 @@
+//
+// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License.
+// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc.
+//
+
+#if NETFRAMEWORK
+#pragma warning disable SA1402 // File may only contain a single class
+#pragma warning disable SA1649 // File name must match first type name
+
+using System;
+using System.Collections.Generic;
+using System.Net;
+using System.Text.RegularExpressions;
+using System.Threading.Tasks;
+using Datadog.Trace.Configuration;
+using Datadog.Trace.ExtensionMethods;
+using Datadog.Trace.TestHelpers;
+using VerifyTests;
+using VerifyXunit;
+using Xunit;
+using Xunit.Abstractions;
+
+namespace Datadog.Trace.ClrProfiler.IntegrationTests
+{
+ [Collection("IisTests")]
+ public class AspNetWebApi2CodeOriginTestsClassic : AspNetWebApi2CodeOriginTests
+ {
+ public AspNetWebApi2CodeOriginTestsClassic(IisFixture iisFixture, ITestOutputHelper output)
+ : base(iisFixture, output, classicMode: true)
+ {
+ }
+ }
+
+ [Collection("IisTests")]
+ public class AspNetWebApi2CodeOriginTestsIntegrated : AspNetWebApi2CodeOriginTests
+ {
+ public AspNetWebApi2CodeOriginTestsIntegrated(IisFixture iisFixture, ITestOutputHelper output)
+ : base(iisFixture, output, classicMode: false)
+ {
+ }
+ }
+
+ [UsesVerify]
+ public abstract class AspNetWebApi2CodeOriginTests : TracingIntegrationTest, IClassFixture, IAsyncLifetime
+ {
+ private readonly IisFixture _iisFixture;
+ private readonly string _testName;
+ private readonly bool _classicMode;
+
+ protected AspNetWebApi2CodeOriginTests(IisFixture iisFixture, ITestOutputHelper output, bool classicMode)
+ : base("AspNetMvc5", @"test\test-applications\aspnet", output)
+ {
+ _iisFixture = iisFixture;
+ _iisFixture.ShutdownPath = "/home/shutdown";
+ _classicMode = classicMode;
+ _testName = $"{nameof(AspNetWebApi2CodeOriginTests)}.{(classicMode ? "Classic" : "Integrated")}";
+
+ SetServiceVersion("1.0.0");
+ SetEnvironmentVariable(ConfigurationKeys.Debugger.CodeOriginForSpansEnabled, "true");
+ }
+
+ public override Result ValidateIntegrationSpan(MockSpan span, string metadataSchemaVersion) =>
+ span.Name switch
+ {
+ "aspnet.request" => span.IsAspNet(metadataSchemaVersion),
+ "aspnet-webapi.request" => span.IsAspNetWebApi2(metadataSchemaVersion),
+ _ => Result.DefaultSuccess,
+ };
+
+ [SkippableFact]
+ [Trait("Category", "EndToEnd")]
+ [Trait("RunOnWindows", "True")]
+ [Trait("LoadFromGAC", "True")]
+ public async Task AddsCodeOriginToAspNetRequestSpan()
+ {
+ const string path = "/api/environment";
+ const int statusCode = 200;
+
+ var spans = await GetWebServerSpans(
+ path: _iisFixture.VirtualApplicationPath + path,
+ agent: _iisFixture.Agent,
+ httpPort: _iisFixture.HttpPort,
+ expectedHttpStatusCode: HttpStatusCode.OK,
+ expectedSpanCount: 2);
+
+ var sanitisedPath = VerifyHelper.SanitisePathsForVerify(path);
+ var settings = VerifyHelper.GetSpanVerifierSettings(sanitisedPath, statusCode);
+
+ AddCodeOriginScrubbers(settings);
+
+ await Verifier.Verify(spans, settings)
+ .UseFileName($"{_testName}.__path={sanitisedPath}_statusCode={statusCode}");
+ }
+
+ public Task InitializeAsync() => _iisFixture.TryStartIis(this, _classicMode ? IisAppType.AspNetClassic : IisAppType.AspNetIntegrated);
+
+ public Task DisposeAsync() => Task.CompletedTask;
+
+ private static void AddCodeOriginScrubbers(VerifySettings settings)
+ {
+ settings.AddRegexScrubber(
+ new Regex(@"_dd\.code_origin\.frames\.(\d+)\.file: .*", VerifyHelper.RegOptions),
+ "_dd.code_origin.frames.$1.file: ");
+
+ settings.AddRegexScrubber(
+ new Regex(@"_dd\.code_origin\.frames\.(\d+)\.(line|column): \d+", VerifyHelper.RegOptions),
+ "_dd.code_origin.frames.$1.$2: 0");
+ }
+ }
+}
+
+#endif
+
diff --git a/tracer/test/Datadog.Trace.Security.IntegrationTests/AspNetWebApi.cs b/tracer/test/Datadog.Trace.Security.IntegrationTests/AspNetWebApi.cs
index 7aaca72b8f8a..739038c21c17 100644
--- a/tracer/test/Datadog.Trace.Security.IntegrationTests/AspNetWebApi.cs
+++ b/tracer/test/Datadog.Trace.Security.IntegrationTests/AspNetWebApi.cs
@@ -105,7 +105,10 @@ public async Task TestBlockedRequest(string test)
var settings = VerifyHelper.GetSpanVerifierSettings(test);
FilterConnectionHeader(settings);
- await TestAppSecRequestWithVerifyAsync(_iisFixture.Agent, url, null, 5, 1, settings, userAgent: "Hello/V");
+ // When AppSec is enabled, the request is blocked early and only the ASP.NET root span is created.
+ // When AppSec is disabled, the request completes normally and we also get the Web API span.
+ var spansPerRequest = SecurityEnabled ? 1 : 2;
+ await TestAppSecRequestWithVerifyAsync(_iisFixture.Agent, url, null, 5, spansPerRequest, settings, userAgent: "Hello/V");
}
[Trait("Category", "EndToEnd")]
diff --git a/tracer/test/Datadog.Trace.Tests/Debugger/EndpointDetectorTests.cs b/tracer/test/Datadog.Trace.Tests/Debugger/EndpointDetectorTests.cs
index 8a28d0ba6400..eb9122f61726 100644
--- a/tracer/test/Datadog.Trace.Tests/Debugger/EndpointDetectorTests.cs
+++ b/tracer/test/Datadog.Trace.Tests/Debugger/EndpointDetectorTests.cs
@@ -102,6 +102,16 @@ public void GetEndpointMethodTokens_FindsAllEndpoints()
isExpectedEndpoint = true;
}
+ // .NET Framework MVC/WebApi2 controllers (convention-based)
+ else if (type.Name == "NetFxMvcController" && method.Name == "Index")
+ {
+ isExpectedEndpoint = true;
+ }
+ else if (type.Name == "NetFxWebApiController" && method.Name == "Get")
+ {
+ isExpectedEndpoint = true;
+ }
+
// PageModel handlers
else if (type.Name == "TestPageModel" &&
method.Name is "OnGet" or "OnGetAsync" or "OnPost" or "OnPostAsync" or "OnPut" or "OnPutAsync" or "OnDelete" or "OnDeleteAsync" or "OnHead" or "OnHeadAsync" or "OnPatch" or "OnPatchAsync" or "OnOptions" or "OnOptionsAsync" &&
@@ -191,6 +201,12 @@ public void GetEndpointMethodTokens_DoesNotDetectNonEndpoints()
shouldNotBeEndpoint = true;
}
+ // .NET Framework MVC/WebApi2 non-actions
+ else if (type.Name is "NetFxMvcController" or "NetFxWebApiController" && method.Name == "Helper")
+ {
+ shouldNotBeEndpoint = true;
+ }
+
// PageModel methods with NonHandler attribute
else if (type.Name == "TestPageModel" && method.Name == "OnGetWithNonHandlerAttribute")
{
@@ -414,6 +430,24 @@ public static void MapPost(this object app, string pattern, Delegate handler) {
}
}
+namespace System.Web.Mvc
+{
+ [AttributeUsage(AttributeTargets.Method)]
+ public class NonActionAttribute : Attribute { }
+
+ public class ControllerBase { }
+
+ public class Controller : ControllerBase { }
+}
+
+namespace System.Web.Http
+{
+ [AttributeUsage(AttributeTargets.Method)]
+ public class NonActionAttribute : Attribute { }
+
+ public class ApiController { }
+}
+
namespace EndpointDetectorTestNamespace
{
// Controller with ControllerAttribute
@@ -577,6 +611,28 @@ public static void UseHandlers(object app, RequestHandler handler1, RequestHandl
// This empty method ensures the delegates are used
}
}
+
+ // .NET Framework MVC controller
+ public class NetFxMvcController : System.Web.Mvc.Controller
+ {
+ public object Index() => null;
+
+ [System.Web.Mvc.NonAction]
+ public object Helper() => null;
+
+ private object PrivateMethod() => null;
+
+ public static object StaticMethod() => null;
+ }
+
+ // .NET Framework Web API 2 controller
+ public class NetFxWebApiController : System.Web.Http.ApiController
+ {
+ public object Get() => null;
+
+ [System.Web.Http.NonAction]
+ public object Helper() => null;
+ }
}");
}
}
diff --git a/tracer/test/snapshots/AspNetMvc5CodeOriginTests.Classic.__path=_Home_Index_statusCode=200.verified.txt b/tracer/test/snapshots/AspNetMvc5CodeOriginTests.Classic.__path=_Home_Index_statusCode=200.verified.txt
new file mode 100644
index 000000000000..c8131e6839db
--- /dev/null
+++ b/tracer/test/snapshots/AspNetMvc5CodeOriginTests.Classic.__path=_Home_Index_statusCode=200.verified.txt
@@ -0,0 +1,61 @@
+[
+ {
+ TraceId: Id_1,
+ SpanId: Id_2,
+ Name: aspnet-mvc.request,
+ Resource: GET /home/index,
+ Service: sample,
+ Type: web,
+ ParentId: Id_3,
+ Tags: {
+ aspnet.action: index,
+ aspnet.controller: home,
+ aspnet.route: {controller}/{action}/{id},
+ component: aspnet,
+ env: integration_tests,
+ http.method: GET,
+ http.request.headers.host: localhost:00000,
+ http.status_code: 200,
+ http.url: http://localhost:00000/Home/Index,
+ http.useragent: testhelper,
+ language: dotnet,
+ span.kind: server,
+ version: 1.0.0
+ }
+ },
+ {
+ TraceId: Id_1,
+ SpanId: Id_3,
+ Name: aspnet.request,
+ Resource: GET /home/index,
+ Service: sample,
+ Type: web,
+ Tags: {
+ component: aspnet,
+ env: integration_tests,
+ http.method: GET,
+ http.request.headers.host: localhost:00000,
+ http.route: {controller}/{action}/{id},
+ http.status_code: 200,
+ http.url: http://localhost:00000/Home/Index,
+ http.useragent: testhelper,
+ language: dotnet,
+ runtime-id: Guid_1,
+ span.kind: server,
+ version: 1.0.0,
+ _dd.code_origin.frames.0.column: 0,
+ _dd.code_origin.frames.0.file:
+ _dd.code_origin.frames.0.index: 0,
+ _dd.code_origin.frames.0.line: 0,
+ _dd.code_origin.frames.0.method: Index,
+ _dd.code_origin.frames.0.type: Samples.AspNetMvc5.Controllers.HomeController,
+ _dd.code_origin.type: entry
+ },
+ Metrics: {
+ process_id: 0,
+ _dd.top_level: 1.0,
+ _dd.tracer_kr: 1.0,
+ _sampling_priority_v1: 1.0
+ }
+ }
+]
\ No newline at end of file
diff --git a/tracer/test/snapshots/AspNetMvc5CodeOriginTests.Integrated.__path=_Home_Index_statusCode=200.verified.txt b/tracer/test/snapshots/AspNetMvc5CodeOriginTests.Integrated.__path=_Home_Index_statusCode=200.verified.txt
new file mode 100644
index 000000000000..c8131e6839db
--- /dev/null
+++ b/tracer/test/snapshots/AspNetMvc5CodeOriginTests.Integrated.__path=_Home_Index_statusCode=200.verified.txt
@@ -0,0 +1,61 @@
+[
+ {
+ TraceId: Id_1,
+ SpanId: Id_2,
+ Name: aspnet-mvc.request,
+ Resource: GET /home/index,
+ Service: sample,
+ Type: web,
+ ParentId: Id_3,
+ Tags: {
+ aspnet.action: index,
+ aspnet.controller: home,
+ aspnet.route: {controller}/{action}/{id},
+ component: aspnet,
+ env: integration_tests,
+ http.method: GET,
+ http.request.headers.host: localhost:00000,
+ http.status_code: 200,
+ http.url: http://localhost:00000/Home/Index,
+ http.useragent: testhelper,
+ language: dotnet,
+ span.kind: server,
+ version: 1.0.0
+ }
+ },
+ {
+ TraceId: Id_1,
+ SpanId: Id_3,
+ Name: aspnet.request,
+ Resource: GET /home/index,
+ Service: sample,
+ Type: web,
+ Tags: {
+ component: aspnet,
+ env: integration_tests,
+ http.method: GET,
+ http.request.headers.host: localhost:00000,
+ http.route: {controller}/{action}/{id},
+ http.status_code: 200,
+ http.url: http://localhost:00000/Home/Index,
+ http.useragent: testhelper,
+ language: dotnet,
+ runtime-id: Guid_1,
+ span.kind: server,
+ version: 1.0.0,
+ _dd.code_origin.frames.0.column: 0,
+ _dd.code_origin.frames.0.file:
+ _dd.code_origin.frames.0.index: 0,
+ _dd.code_origin.frames.0.line: 0,
+ _dd.code_origin.frames.0.method: Index,
+ _dd.code_origin.frames.0.type: Samples.AspNetMvc5.Controllers.HomeController,
+ _dd.code_origin.type: entry
+ },
+ Metrics: {
+ process_id: 0,
+ _dd.top_level: 1.0,
+ _dd.tracer_kr: 1.0,
+ _sampling_priority_v1: 1.0
+ }
+ }
+]
\ No newline at end of file
diff --git a/tracer/test/snapshots/AspNetWebApi2CodeOriginTests.Classic.__path=_api_environment_statusCode=200.verified.txt b/tracer/test/snapshots/AspNetWebApi2CodeOriginTests.Classic.__path=_api_environment_statusCode=200.verified.txt
new file mode 100644
index 000000000000..940a619c1ad3
--- /dev/null
+++ b/tracer/test/snapshots/AspNetWebApi2CodeOriginTests.Classic.__path=_api_environment_statusCode=200.verified.txt
@@ -0,0 +1,59 @@
+[
+ {
+ TraceId: Id_1,
+ SpanId: Id_2,
+ Name: aspnet-webapi.request,
+ Resource: GET /api/environment,
+ Service: sample,
+ Type: web,
+ ParentId: Id_3,
+ Tags: {
+ aspnet.route: api/environment,
+ component: aspnet,
+ env: integration_tests,
+ http.method: GET,
+ http.request.headers.host: localhost:00000,
+ http.status_code: 200,
+ http.url: http://localhost:00000/api/environment,
+ http.useragent: testhelper,
+ language: dotnet,
+ span.kind: server,
+ version: 1.0.0
+ }
+ },
+ {
+ TraceId: Id_1,
+ SpanId: Id_3,
+ Name: aspnet.request,
+ Resource: GET /api/environment,
+ Service: sample,
+ Type: web,
+ Tags: {
+ component: aspnet,
+ env: integration_tests,
+ http.method: GET,
+ http.request.headers.host: localhost:00000,
+ http.route: api/environment,
+ http.status_code: 200,
+ http.url: http://localhost:00000/api/environment,
+ http.useragent: testhelper,
+ language: dotnet,
+ runtime-id: Guid_1,
+ span.kind: server,
+ version: 1.0.0,
+ _dd.code_origin.frames.0.column: 0,
+ _dd.code_origin.frames.0.file:
+ _dd.code_origin.frames.0.index: 0,
+ _dd.code_origin.frames.0.line: 0,
+ _dd.code_origin.frames.0.method: Environment,
+ _dd.code_origin.frames.0.type: Samples.AspNetMvc5.Controllers.ApiController,
+ _dd.code_origin.type: entry
+ },
+ Metrics: {
+ process_id: 0,
+ _dd.top_level: 1.0,
+ _dd.tracer_kr: 1.0,
+ _sampling_priority_v1: 1.0
+ }
+ }
+]
\ No newline at end of file
diff --git a/tracer/test/snapshots/AspNetWebApi2CodeOriginTests.Integrated.__path=_api_environment_statusCode=200.verified.txt b/tracer/test/snapshots/AspNetWebApi2CodeOriginTests.Integrated.__path=_api_environment_statusCode=200.verified.txt
new file mode 100644
index 000000000000..940a619c1ad3
--- /dev/null
+++ b/tracer/test/snapshots/AspNetWebApi2CodeOriginTests.Integrated.__path=_api_environment_statusCode=200.verified.txt
@@ -0,0 +1,59 @@
+[
+ {
+ TraceId: Id_1,
+ SpanId: Id_2,
+ Name: aspnet-webapi.request,
+ Resource: GET /api/environment,
+ Service: sample,
+ Type: web,
+ ParentId: Id_3,
+ Tags: {
+ aspnet.route: api/environment,
+ component: aspnet,
+ env: integration_tests,
+ http.method: GET,
+ http.request.headers.host: localhost:00000,
+ http.status_code: 200,
+ http.url: http://localhost:00000/api/environment,
+ http.useragent: testhelper,
+ language: dotnet,
+ span.kind: server,
+ version: 1.0.0
+ }
+ },
+ {
+ TraceId: Id_1,
+ SpanId: Id_3,
+ Name: aspnet.request,
+ Resource: GET /api/environment,
+ Service: sample,
+ Type: web,
+ Tags: {
+ component: aspnet,
+ env: integration_tests,
+ http.method: GET,
+ http.request.headers.host: localhost:00000,
+ http.route: api/environment,
+ http.status_code: 200,
+ http.url: http://localhost:00000/api/environment,
+ http.useragent: testhelper,
+ language: dotnet,
+ runtime-id: Guid_1,
+ span.kind: server,
+ version: 1.0.0,
+ _dd.code_origin.frames.0.column: 0,
+ _dd.code_origin.frames.0.file:
+ _dd.code_origin.frames.0.index: 0,
+ _dd.code_origin.frames.0.line: 0,
+ _dd.code_origin.frames.0.method: Environment,
+ _dd.code_origin.frames.0.type: Samples.AspNetMvc5.Controllers.ApiController,
+ _dd.code_origin.type: entry
+ },
+ Metrics: {
+ process_id: 0,
+ _dd.top_level: 1.0,
+ _dd.tracer_kr: 1.0,
+ _sampling_priority_v1: 1.0
+ }
+ }
+]
\ No newline at end of file