diff --git a/tracer/src/Datadog.Trace/Debugger/SpanCodeOrigin/EndpointDetector.cs b/tracer/src/Datadog.Trace/Debugger/SpanCodeOrigin/EndpointDetector.cs index 5d97b4b1742c..e0f90409ae11 100644 --- a/tracer/src/Datadog.Trace/Debugger/SpanCodeOrigin/EndpointDetector.cs +++ b/tracer/src/Datadog.Trace/Debugger/SpanCodeOrigin/EndpointDetector.cs @@ -165,10 +165,24 @@ private static bool HasAttributeFromSet(CustomAttributeHandleCollection attribut { foreach (var attributeHandle in attributes) { + string fullName; var attribute = reader.GetCustomAttribute(attributeHandle); - var ctor = reader.GetMemberReference((MemberReferenceHandle)attribute.Constructor); - var attributeType = reader.GetTypeReference((TypeReferenceHandle)ctor.Parent); - var fullName = GetFullTypeName(attributeType.Namespace, attributeType.Name, reader); + if (attribute.Constructor.Kind == HandleKind.MemberReference) + { + var ctor = reader.GetMemberReference((MemberReferenceHandle)attribute.Constructor); + var attributeType = reader.GetTypeReference((TypeReferenceHandle)ctor.Parent); + fullName = GetFullTypeName(attributeType.Namespace, attributeType.Name, reader); + } + else if (attribute.Constructor.Kind == HandleKind.MethodDefinition) + { + var ctor = reader.GetMethodDefinition((MethodDefinitionHandle)attribute.Constructor); + var attributeType = reader.GetTypeDefinition(ctor.GetDeclaringType()); + fullName = GetFullTypeName(attributeType.Namespace, attributeType.Name, reader); + } + else + { + continue; + } if (attributeNames.Contains(fullName)) { diff --git a/tracer/src/Datadog.Trace/Debugger/SpanCodeOrigin/SpanCodeOriginManager.cs b/tracer/src/Datadog.Trace/Debugger/SpanCodeOrigin/SpanCodeOriginManager.cs index db494319bf77..5e521e8b1c16 100644 --- a/tracer/src/Datadog.Trace/Debugger/SpanCodeOrigin/SpanCodeOriginManager.cs +++ b/tracer/src/Datadog.Trace/Debugger/SpanCodeOrigin/SpanCodeOriginManager.cs @@ -21,24 +21,24 @@ internal class SpanCodeOriginManager { private static readonly IDatadogLogger Log = DatadogLogging.GetLoggerFor(typeof(SpanCodeOriginManager)); - private readonly int _maxUserFrames; - - private readonly bool _isCodeOriginEnabled; - private readonly ConcurrentDictionary _assemblyPdbCache = new(); private readonly CodeOriginTags _tags; + private readonly int _maxUserFrames; + + private readonly bool _isCodeOriginEnabled; + private readonly ImmutableHashSet _thirdPartyDetectionExcludes; private readonly ImmutableHashSet _thirdPartyDetectionIncludes; private SpanCodeOriginManager() { + _tags = new CodeOriginTags(_maxUserFrames); var settings = LiveDebugger.Instance?.Settings ?? DebuggerSettings.FromDefaultSource(); _maxUserFrames = settings.CodeOriginMaxUserFrames; _isCodeOriginEnabled = settings.CodeOriginForSpansEnabled; - _tags = new CodeOriginTags(_maxUserFrames); _thirdPartyDetectionExcludes = settings.ThirdPartyDetectionExcludes; _thirdPartyDetectionIncludes = settings.ThirdPartyDetectionIncludes; } diff --git a/tracer/test/Datadog.Trace.Tests/Debugger/SpanCodeOriginTests.cs b/tracer/test/Datadog.Trace.Tests/Debugger/SpanCodeOriginTests.cs index 8488fc732b6e..234f4d83b2bc 100644 --- a/tracer/test/Datadog.Trace.Tests/Debugger/SpanCodeOriginTests.cs +++ b/tracer/test/Datadog.Trace.Tests/Debugger/SpanCodeOriginTests.cs @@ -5,15 +5,16 @@ using System; using System.Collections.Generic; -using System.Collections.Specialized; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; -using Datadog.Trace.Configuration; -using Datadog.Trace.Configuration.Telemetry; -using Datadog.Trace.Debugger; +using System.Threading.Tasks; using Datadog.Trace.Debugger.SpanCodeOrigin; +using Datadog.Trace.VendoredMicrosoftCode.System.Collections.Immutable; using FluentAssertions; +#if !NETFRAMEWORK +using Microsoft.AspNetCore.Mvc; +#endif using Xunit; namespace Datadog.Trace.Tests.Debugger @@ -22,130 +23,336 @@ public class SpanCodeOriginTests { private const string CodeOriginTag = "_dd.code_origin"; - [Fact] - public void SetCodeOrigin_WhenSpanIsNull_DoesNotThrow() + private static IDisposable SetCodeOriginManagerSettings(bool isEnable = false, int numberOfFrames = 8, string excludeFromFilter = "Datadog.Trace.Tests") { - // Should not throw - SpanCodeOriginManager.Instance.SetCodeOriginForExitSpan(null); + var setter = new CodeOriginSettingsSetter(); + setter.Set(isEnable, numberOfFrames, excludeFromFilter); + return setter; } - [Fact] - public void SetCodeOrigin_WhenDisabled_DoesNotSetTags() + public class EntrySpanTests { - // Arrange - using var settingsSetter = SetCodeOriginManagerSettings(); + [Fact] + public void SetCodeOriginForEntrySpan_WhenSpanIsNull_ShouldNotThrow() + { + var action = () => SpanCodeOriginManager.Instance.SetCodeOriginForEntrySpan(null, typeof(string), typeof(string).GetMethod(nameof(string.ToString), Type.EmptyTypes)); - var span = new Span(new SpanContext(1, 2, SamplingPriority.UserKeep), DateTimeOffset.UtcNow); + action.Should().NotThrow(); + } - // Act - SpanCodeOriginManager.Instance.SetCodeOriginForExitSpan(span); + [Fact] + public void SetCodeOriginForEntrySpan_WhenTypeIsNull_ShouldNotThrow() + { + var span = CreateSpan(); + var action = () => SpanCodeOriginManager.Instance.SetCodeOriginForEntrySpan(span, null, typeof(string).GetMethod(nameof(string.ToString), Type.EmptyTypes)); - // Assert - span.Tags.GetTag(CodeOriginTag + ".type").Should().BeNull(); - } + action.Should().NotThrow(); + span.GetTag($"{CodeOriginTag}.type").Should().BeNull(); + } - [Fact] - public void SetCodeOrigin_WhenEnabled_SetsCorrectTags() - { - // Arrange - using var settingsSetter = SetCodeOriginManagerSettings(true); - - var span = new Span(new SpanContext(1, 2, SamplingPriority.UserKeep), DateTimeOffset.UtcNow); - - // Act - TestMethod(span); - - // Assert - var codeOriginType = span.Tags.GetTag($"{CodeOriginTag}.type"); - codeOriginType.Should().Be("exit"); - var frame0Method = span.Tags.GetTag($"{CodeOriginTag}.frames.0.method"); - frame0Method.Should().Be(nameof(TestMethod)); - var frame0Type = span.Tags.GetTag($"{CodeOriginTag}.frames.0.type"); - frame0Type.Should().Be(GetType().FullName); - var file = span.Tags.GetTag($"{CodeOriginTag}.frames.0.file"); - file.Should().EndWith($"{nameof(SpanCodeOriginTests)}.cs"); - var line = span.Tags.GetTag($"{CodeOriginTag}.frames.0.line"); - line.Should().NotBeNullOrEmpty(); - var column = span.Tags.GetTag($"{CodeOriginTag}.frames.0.column"); - column.Should().NotBeNullOrEmpty(); - } + [Fact] + public void SetCodeOriginForEntrySpan_WhenMethodIsNull_ShouldNotThrow() + { + var span = CreateSpan(); + var action = () => SpanCodeOriginManager.Instance.SetCodeOriginForEntrySpan(span, typeof(string), null); - [Fact] - public void SetCodeOrigin_WithMaxFramesLimit_RespectsLimit() - { - // Arrange - using var settingsSetter = SetCodeOriginManagerSettings(true, 2); + action.Should().NotThrow(); + span.GetTag($"{CodeOriginTag}.type").Should().BeNull(); + } - var span = new Span(new SpanContext(1, 2, SamplingPriority.UserKeep), DateTimeOffset.UtcNow); + [Fact] + public void SetCodeOriginForEntrySpan_WhenSpanAlreadyHasCodeOrigin_ShouldNotModifyTags() + { + // Arrange + using var settingsSetter = SetCodeOriginManagerSettings(true); + var span = CreateSpan(); + span.SetTag($"{CodeOriginTag}.type", "existing"); - // Act - DeepTestMethod1(span); + // Act + SpanCodeOriginManager.Instance.SetCodeOriginForEntrySpan(span, GetType(), GetType().GetMethod(nameof(SetCodeOriginForEntrySpan_WhenSpanAlreadyHasCodeOrigin_ShouldNotModifyTags))); - // Assert - var tags = ((List>)(typeof(Datadog.Trace.Tagging.TagsList).GetField("_tags", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(span.Tags))).Select(i => i.Key).ToList(); - tags.Should().Contain(s => s.StartsWith($"{CodeOriginTag}.frames.0")); - tags.Should().Contain(s => s.StartsWith($"{CodeOriginTag}.frames.1")); - tags.Should().NotContain(s => s.StartsWith($"{CodeOriginTag}.frames.2")); - } + // Assert + span.GetTag($"{CodeOriginTag}.type").Should().BeEquivalentTo("existing"); + } - private static IDisposable SetCodeOriginManagerSettings(bool isEnable = false, int numberOfFrames = 8, string excludeFromFilter = "Datadog.Trace.Tests") - { - var setter = new CodeOriginSettingsSetter(); - setter.Set(isEnable, numberOfFrames, excludeFromFilter); - return setter; - } + [Fact] + public void SetCodeOriginForEntrySpan_WithValidInputs_ShouldSetCorrectTags() + { + // Arrange + using var settingsSetter = SetCodeOriginManagerSettings(true); + var span = CreateSpan(); + var type = GetType(); + var method = type.GetMethod(nameof(this.TestMethod), BindingFlags.Instance | BindingFlags.NonPublic); - [MethodImpl(MethodImplOptions.NoInlining)] - private void TestMethod(Span span) - { - SpanCodeOriginManager.Instance.SetCodeOriginForExitSpan(span); - } + // Act + SpanCodeOriginManager.Instance.SetCodeOriginForEntrySpan(span, type, method); - [MethodImpl(MethodImplOptions.NoInlining)] - private void DeepTestMethod1(Span span) - { - DeepTestMethod2(span); - } + // Assert + span.GetTag($"{CodeOriginTag}.type").Should().Be("entry"); + span.GetTag($"{CodeOriginTag}.frames.0.index").Should().Be("0"); + span.GetTag($"{CodeOriginTag}.frames.0.method").Should().Be(nameof(this.TestMethod)); + span.GetTag($"{CodeOriginTag}.frames.0.type").Should().Be(type.FullName); + } - [MethodImpl(MethodImplOptions.NoInlining)] - private void DeepTestMethod2(Span span) - { - DeepTestMethod3(span); + [Fact] + public void SetCodeOriginForEntrySpan_WithThirdPartyAssembly_ShouldNotSetTags() + { + // Arrange + using var settingsSetter = SetCodeOriginManagerSettings(true); + var span = CreateSpan(); + var method = typeof(string).GetMethod(nameof(string.ToString), Type.EmptyTypes); + var type = method.DeclaringType; + + // Act + SpanCodeOriginManager.Instance.SetCodeOriginForEntrySpan(span, type, method); + + // Assert + span.GetTag($"{CodeOriginTag}.type").Should().BeNull(); + } + + [Fact] + public void SetCodeOriginForEntrySpan_ForNonControllerAction_ShouldNotSetTags() + { + // Arrange + using var settingsSetter = SetCodeOriginManagerSettings(true); + var span = CreateSpan(); + var type = GetType(); + var method = type.GetMethod(nameof(this.TestMethod), BindingFlags.Instance | BindingFlags.NonPublic); + + // Act + SpanCodeOriginManager.Instance.SetCodeOriginForEntrySpan(span, type, method); + + // Assert + span.GetTag($"{CodeOriginTag}.type").Should().Be("entry"); + span.GetTag($"{CodeOriginTag}.frames.0.method").Should().Be(nameof(this.TestMethod)); + span.GetTag($"{CodeOriginTag}.frames.{0}.file").Should().BeNull(); + } + + [Fact] + public void SetCodeOriginForEntrySpan_ForNonControllerAction_WithAsyncMethod_ShouldSetCorrectTags() + { + // Arrange + using var settingsSetter = SetCodeOriginManagerSettings(true); + var span = CreateSpan(); + var method = GetType().GetMethod(nameof(AsyncTestMethod), BindingFlags.Instance | BindingFlags.NonPublic); + var type = method.DeclaringType; + + // Act + SpanCodeOriginManager.Instance.SetCodeOriginForEntrySpan(span, type, method); + + // Assert + span.GetTag($"{CodeOriginTag}.type").Should().Be("entry"); + span.GetTag($"{CodeOriginTag}.frames.0.method").Should().Be(nameof(AsyncTestMethod)); + span.GetTag($"{CodeOriginTag}.frames.{0}.file").Should().BeNull(); + } + + [Fact] + public void SetCodeOriginForEntrySpan_ForNonControllerAction_WithGenericMethod_ShouldSetCorrectTags() + { + // Arrange + using var settingsSetter = SetCodeOriginManagerSettings(true); + var span = CreateSpan(); + var method = GetType().GetMethod(nameof(GenericTestMethod), BindingFlags.Instance | BindingFlags.NonPublic); + var type = method.DeclaringType; + + // Act + SpanCodeOriginManager.Instance.SetCodeOriginForEntrySpan(span, type, method); + + // Assert + span.GetTag($"{CodeOriginTag}.type").Should().Be("entry"); + span.GetTag($"{CodeOriginTag}.frames.0.method").Should().Be(nameof(GenericTestMethod)); + span.GetTag($"{CodeOriginTag}.frames.{0}.file").Should().BeNull(); + } + +#if !NETFRAMEWORK + [Fact] + public void SetCodeOriginForEntrySpan_ForControllerAction_ShouldSetTags() + { + // Arrange + using var settingsSetter = SetCodeOriginManagerSettings(true); + var span = CreateSpan(); + var controllerType = typeof(TestController); + var method = controllerType.GetMethod(nameof(TestController.Get)); + + // Act + SpanCodeOriginManager.Instance.SetCodeOriginForEntrySpan(span, controllerType, method); + + // Assert + span.GetTag("_dd.code_origin.type").Should().Be("entry"); + span.GetTag("_dd.code_origin.frames.0.method").Should().Be(nameof(TestController.Get)); + span.GetTag("_dd.code_origin.frames.0.type").Should().Be("Datadog.Trace.Tests.Debugger.SpanCodeOriginTests+TestController"); + span.GetTag($"{CodeOriginTag}.frames.{0}.file").Should().EndWithEquivalentOf("SpanCodeOriginTests.cs"); + } +#endif + + private Span CreateSpan() + { + var spanContext = new SpanContext(1234, 5678); + return new Span(spanContext, DateTimeOffset.UtcNow); + } + + private int TestMethod() => 42; + + private async Task AsyncTestMethod() + { + await Task.Delay(1); + } + + private T GenericTestMethod(T input) + where T : class + { + return input; + } } - [MethodImpl(MethodImplOptions.NoInlining)] - private void DeepTestMethod3(Span span) + public class ExitSpanTests { - SpanCodeOriginManager.Instance.SetCodeOriginForExitSpan(span); + [Fact] + public void SetCodeOrigin_WhenSpanIsNull_DoesNotThrow() + { + // Should not throw + SpanCodeOriginManager.Instance.SetCodeOriginForExitSpan(null); + } + + [Fact] + public void SetCodeOrigin_WhenDisabled_DoesNotSetTags() + { + // Arrange + using var settingsSetter = SetCodeOriginManagerSettings(); + + var span = new Span(new SpanContext(1, 2, SamplingPriority.UserKeep), DateTimeOffset.UtcNow); + + // Act + SpanCodeOriginManager.Instance.SetCodeOriginForExitSpan(span); + + // Assert + span.Tags.GetTag(CodeOriginTag + ".type").Should().BeNull(); + } + + [Fact] + public void SetCodeOrigin_WhenEnabled_SetsCorrectTags() + { + // Arrange + using var settingsSetter = SetCodeOriginManagerSettings(true); + + var span = new Span(new SpanContext(1, 2, SamplingPriority.UserKeep), DateTimeOffset.UtcNow); + + // Act + TestMethod(span); + + // Assert + var codeOriginType = span.Tags.GetTag($"{CodeOriginTag}.type"); + codeOriginType.Should().Be("exit"); + var frame0Method = span.Tags.GetTag($"{CodeOriginTag}.frames.0.method"); + frame0Method.Should().Be(nameof(TestMethod)); + var frame0Type = span.Tags.GetTag($"{CodeOriginTag}.frames.0.type"); + frame0Type.Should().Be(GetType().FullName); + var file = span.Tags.GetTag($"{CodeOriginTag}.frames.0.file"); + file.Should().EndWith($"{nameof(SpanCodeOriginTests)}.cs"); + var line = span.Tags.GetTag($"{CodeOriginTag}.frames.0.line"); + line.Should().NotBeNullOrEmpty(); + var column = span.Tags.GetTag($"{CodeOriginTag}.frames.0.column"); + column.Should().NotBeNullOrEmpty(); + } + + [Fact] + public void SetCodeOrigin_WithMaxFramesLimit_RespectsLimit() + { + // Arrange + using var settingsSetter = SetCodeOriginManagerSettings(true, 2); + + var span = new Span(new SpanContext(1, 2, SamplingPriority.UserKeep), DateTimeOffset.UtcNow); + + // Act + DeepTestMethod1(span); + + // Assert + var tags = ((List>)(typeof(Datadog.Trace.Tagging.TagsList).GetField("_tags", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(span.Tags))).Select(i => i.Key).ToList(); + tags.Should().Contain(s => s.StartsWith($"{CodeOriginTag}.frames.0")); + tags.Should().Contain(s => s.StartsWith($"{CodeOriginTag}.frames.1")); + tags.Should().NotContain(s => s.StartsWith($"{CodeOriginTag}.frames.2")); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private void TestMethod(Span span) + { + SpanCodeOriginManager.Instance.SetCodeOriginForExitSpan(span); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private void DeepTestMethod1(Span span) + { + DeepTestMethod2(span); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private void DeepTestMethod2(Span span) + { + DeepTestMethod3(span); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private void DeepTestMethod3(Span span) + { + SpanCodeOriginManager.Instance.SetCodeOriginForExitSpan(span); + } } internal class CodeOriginSettingsSetter : IDisposable { - private DebuggerSettings _original; + private int _maxUserFrames; + + private bool _isCodeOriginEnabled; + + private ImmutableHashSet _thirdPartyDetectionExcludes; + + private SpanCodeOriginManager.CodeOriginTags _tags; public void Dispose() { var instance = SpanCodeOriginManager.Instance; - instance.GetType().GetField("_settings", BindingFlags.NonPublic | BindingFlags.Instance).SetValue(instance, _original); + var maxUserFrameField = instance.GetType().GetField("_maxUserFrames", BindingFlags.NonPublic | BindingFlags.Instance); + var isCodeOriginEnabledField = instance.GetType().GetField("_isCodeOriginEnabled", BindingFlags.NonPublic | BindingFlags.Instance); + var thirdPartyDetectionExcludesField = instance.GetType().GetField("_thirdPartyDetectionExcludes", BindingFlags.NonPublic | BindingFlags.Instance); + var tagsField = instance.GetType().GetField("_tags", BindingFlags.NonPublic | BindingFlags.Instance); + maxUserFrameField.SetValue(instance, this._maxUserFrames); + isCodeOriginEnabledField.SetValue(instance, _isCodeOriginEnabled); + thirdPartyDetectionExcludesField.SetValue(instance, _thirdPartyDetectionExcludes); + tagsField.SetValue(instance, _tags); } internal void Set(bool isEnable, int numberOfFrames, string excludeFromFilter) { var instance = SpanCodeOriginManager.Instance; - _original = (DebuggerSettings)instance.GetType().GetField("_settings", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(instance); + var maxUserFrameField = instance.GetType().GetField("_maxUserFrames", BindingFlags.NonPublic | BindingFlags.Instance); + var isCodeOriginEnabledField = instance.GetType().GetField("_isCodeOriginEnabled", BindingFlags.NonPublic | BindingFlags.Instance); + var thirdPartyDetectionExcludesField = instance.GetType().GetField("_thirdPartyDetectionExcludes", BindingFlags.NonPublic | BindingFlags.Instance); + var tagsField = instance.GetType().GetField("_tags", BindingFlags.NonPublic | BindingFlags.Instance); - var overrideSettings = DebuggerSettings.FromSource( - new NameValueConfigurationSource( - new NameValueCollection - { - { ConfigurationKeys.Debugger.CodeOriginForSpansEnabled, isEnable.ToString() }, - { ConfigurationKeys.Debugger.CodeOriginMaxUserFrames, numberOfFrames.ToString() }, - { ConfigurationKeys.Debugger.ThirdPartyDetectionExcludes, excludeFromFilter } - }), - NullConfigurationTelemetry.Instance); + // save to restore later + _maxUserFrames = (int)maxUserFrameField.GetValue(instance); + _isCodeOriginEnabled = (bool)isCodeOriginEnabledField.GetValue(instance); + _thirdPartyDetectionExcludes = (ImmutableHashSet)thirdPartyDetectionExcludesField.GetValue(instance); + _tags = (SpanCodeOriginManager.CodeOriginTags)tagsField.GetValue(instance); - instance.GetType().GetField("_settings", BindingFlags.NonPublic | BindingFlags.Instance).SetValue(instance, overrideSettings); + // set test values + maxUserFrameField.SetValue(instance, numberOfFrames); + isCodeOriginEnabledField.SetValue(instance, isEnable); + thirdPartyDetectionExcludesField.SetValue(instance, new[] { excludeFromFilter }.ToImmutableHashSet()); + tagsField.SetValue(instance, new SpanCodeOriginManager.CodeOriginTags(numberOfFrames)); } } + + internal class GenericType + { + } + +#if !NETFRAMEWORK + internal class TestController : Microsoft.AspNetCore.Mvc.ControllerBase + { + [HttpGet] + public object Get() => null; + } +#endif } }