diff --git a/Datadog.Trace.sln b/Datadog.Trace.sln index 5791973d2eeb..01f229af0fe7 100644 --- a/Datadog.Trace.sln +++ b/Datadog.Trace.sln @@ -612,14 +612,14 @@ Global Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {91B6272F-5780-4C94-8071-DBBA7B4F67F3}.Debug|Any CPU.ActiveCfg = Debug|x64 - {91B6272F-5780-4C94-8071-DBBA7B4F67F3}.Debug|Any CPU.Build.0 = Debug|x64 - {91B6272F-5780-4C94-8071-DBBA7B4F67F3}.Release|Any CPU.ActiveCfg = Release|x64 - {91B6272F-5780-4C94-8071-DBBA7B4F67F3}.Release|Any CPU.Build.0 = Release|x64 - {C0C8D381-D6B9-4C76-9428-F40F2FA93A9A}.Debug|Any CPU.ActiveCfg = Debug|x64 - {C0C8D381-D6B9-4C76-9428-F40F2FA93A9A}.Debug|Any CPU.Build.0 = Debug|x64 - {C0C8D381-D6B9-4C76-9428-F40F2FA93A9A}.Release|Any CPU.ActiveCfg = Release|x64 - {C0C8D381-D6B9-4C76-9428-F40F2FA93A9A}.Release|Any CPU.Build.0 = Release|x64 + {91B6272F-5780-4C94-8071-DBBA7B4F67F3}.Debug|Any CPU.ActiveCfg = Debug|Win32 + {91B6272F-5780-4C94-8071-DBBA7B4F67F3}.Debug|Any CPU.Build.0 = Debug|Win32 + {91B6272F-5780-4C94-8071-DBBA7B4F67F3}.Release|Any CPU.ActiveCfg = Release|Win32 + {91B6272F-5780-4C94-8071-DBBA7B4F67F3}.Release|Any CPU.Build.0 = Release|Win32 + {C0C8D381-D6B9-4C76-9428-F40F2FA93A9A}.Debug|Any CPU.ActiveCfg = Debug|Win32 + {C0C8D381-D6B9-4C76-9428-F40F2FA93A9A}.Debug|Any CPU.Build.0 = Debug|Win32 + {C0C8D381-D6B9-4C76-9428-F40F2FA93A9A}.Release|Any CPU.ActiveCfg = Release|Win32 + {C0C8D381-D6B9-4C76-9428-F40F2FA93A9A}.Release|Any CPU.Build.0 = Release|Win32 {5DFDF781-F24C-45B1-82EF-9125875A80A4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {5DFDF781-F24C-45B1-82EF-9125875A80A4}.Debug|Any CPU.Build.0 = Debug|Any CPU {5DFDF781-F24C-45B1-82EF-9125875A80A4}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -648,10 +648,10 @@ Global {3C6DD42E-9214-4747-92BA-78DE29AACE59}.Debug|Any CPU.Build.0 = Debug|Any CPU {3C6DD42E-9214-4747-92BA-78DE29AACE59}.Release|Any CPU.ActiveCfg = Release|Any CPU {3C6DD42E-9214-4747-92BA-78DE29AACE59}.Release|Any CPU.Build.0 = Release|Any CPU - {5728056A-51AA-4FF5-AD0C-E86E44E36102}.Debug|Any CPU.ActiveCfg = Debug|x64 - {5728056A-51AA-4FF5-AD0C-E86E44E36102}.Debug|Any CPU.Build.0 = Debug|x64 - {5728056A-51AA-4FF5-AD0C-E86E44E36102}.Release|Any CPU.ActiveCfg = Release|x64 - {5728056A-51AA-4FF5-AD0C-E86E44E36102}.Release|Any CPU.Build.0 = Release|x64 + {5728056A-51AA-4FF5-AD0C-E86E44E36102}.Debug|Any CPU.ActiveCfg = Debug|Win32 + {5728056A-51AA-4FF5-AD0C-E86E44E36102}.Debug|Any CPU.Build.0 = Debug|Win32 + {5728056A-51AA-4FF5-AD0C-E86E44E36102}.Release|Any CPU.ActiveCfg = Release|Win32 + {5728056A-51AA-4FF5-AD0C-E86E44E36102}.Release|Any CPU.Build.0 = Release|Win32 {FDB5C8D0-018D-4FF9-9680-C6A5078F819B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {FDB5C8D0-018D-4FF9-9680-C6A5078F819B}.Debug|Any CPU.Build.0 = Debug|Any CPU {FDB5C8D0-018D-4FF9-9680-C6A5078F819B}.Release|Any CPU.ActiveCfg = Release|Any CPU diff --git a/exploration-tests/protobuf b/exploration-tests/protobuf deleted file mode 160000 index 7c40b2df1fdf..000000000000 --- a/exploration-tests/protobuf +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 7c40b2df1fdf6f414c1c18c789715a9c948a0725 diff --git a/tracer/src/Datadog.Trace/Debugger/ExceptionAutoInstrumentation/ExceptionCaseInstrumentationManager.cs b/tracer/src/Datadog.Trace/Debugger/ExceptionAutoInstrumentation/ExceptionCaseInstrumentationManager.cs index a05f9806be52..36f10fba3487 100644 --- a/tracer/src/Datadog.Trace/Debugger/ExceptionAutoInstrumentation/ExceptionCaseInstrumentationManager.cs +++ b/tracer/src/Datadog.Trace/Debugger/ExceptionAutoInstrumentation/ExceptionCaseInstrumentationManager.cs @@ -38,8 +38,8 @@ internal static ExceptionCase Instrument(ExceptionIdentifier exceptionId, string { Log.Information("Instrumenting {ExceptionId}", exceptionId); - var parsedFramesFromExceptionToString = ExceptionNormalizer.Instance.ParseFrames(exceptionToString).ToArray(); - var stackTrace = exceptionId.StackTrace.Where(frame => parsedFramesFromExceptionToString.Any(f => f.Contains(frame.Method.Name))).ToArray(); + var parsedFramesFromExceptionToString = StackTraceProcessor.ParseFrames(exceptionToString).ToArray(); + var stackTrace = exceptionId.StackTrace.Where(frame => parsedFramesFromExceptionToString.Any(f => MethodMatcher.IsMethodMatch(f, frame.Method))).ToArray(); var participatingUserMethods = GetMethodsToRejit(stackTrace); var uniqueMethods = participatingUserMethods @@ -84,53 +84,11 @@ bool ShouldInstrumentFrameAtIndex(int i) } } - private static bool ContainsExceptionDispatchInfoThrow(MethodBase method) - { - var methodBody = method.GetMethodBody(); - if (methodBody == null) - { - return false; - } - - byte[] ilBytes = methodBody.GetILAsByteArray(); - - if (ilBytes == null) - { - return false; - } - - for (int i = 0; i < ilBytes.Length; i++) - { - if (ilBytes[i] == (byte)OpCodes.Call.Value || ilBytes[i] == (byte)OpCodes.Callvirt.Value) - { - // The next 4 bytes after a call instruction contain the metadata token - if (i + 4 < ilBytes.Length) - { - int metadataToken = BitConverter.ToInt32(ilBytes, i + 1); - try - { - MethodInfo calledMethod = (MethodInfo)method.Module.ResolveMethod(metadataToken); - if (calledMethod.DeclaringType == typeof(System.Runtime.ExceptionServices.ExceptionDispatchInfo) && - calledMethod.Name == "Throw") - { - return true; - } - } - catch (ArgumentException) - { - // If we can't resolve the method, just continue - continue; - } - } - } - } - - return false; - } - private static List GetMethodsToRejit(ParticipatingFrame[] allFrames) { var methodsToRejit = new List(); + MethodUniqueIdentifier? lastMethod = null; + var wasLastMisleading = false; foreach (var frame in allFrames) { @@ -149,11 +107,24 @@ private static List GetMethodsToRejit(ParticipatingFrame continue; } - methodsToRejit.Add(frame.MethodIdentifier); + var currentMethod = frame.MethodIdentifier; + var isCurrentMisleading = currentMethod.IsMisleadMethod(); + + // Add the method if either: + // 1. It's not misleading (we keep all non-misleading methods) + // 2. It's misleading but different from the last misleading method we saw + // 3. It's the first misleading method after non-misleading methods + if (!isCurrentMisleading || currentMethod != lastMethod || !wasLastMisleading) + { + methodsToRejit.Add(currentMethod); + } + + lastMethod = currentMethod; + wasLastMisleading = isCurrentMisleading; } catch (Exception ex) { - Log.Error(ex, "Failed to instrument frame the frame: {FrameToRejit}", frame); + Log.Error(ex, "Failed to instrument the frame: {FrameToRejit}", frame); } } diff --git a/tracer/src/Datadog.Trace/Debugger/ExceptionAutoInstrumentation/ExceptionDebuggingProcessor.cs b/tracer/src/Datadog.Trace/Debugger/ExceptionAutoInstrumentation/ExceptionDebuggingProcessor.cs index 2ef2cbd4ff13..93350c0e397a 100644 --- a/tracer/src/Datadog.Trace/Debugger/ExceptionAutoInstrumentation/ExceptionDebuggingProcessor.cs +++ b/tracer/src/Datadog.Trace/Debugger/ExceptionAutoInstrumentation/ExceptionDebuggingProcessor.cs @@ -19,6 +19,7 @@ internal class ExceptionDebuggingProcessor : IProbeProcessor private static readonly IDatadogLogger Log = DatadogLogging.GetLoggerFor(typeof(ExceptionDebuggingProcessor)); private readonly object _lock = new(); private readonly int _maxFramesToCapture; + private readonly bool _isMisleadingMethod; private ExceptionProbeProcessor[] _processors; internal ExceptionDebuggingProcessor(string probeId, MethodUniqueIdentifier method) @@ -27,6 +28,7 @@ internal ExceptionDebuggingProcessor(string probeId, MethodUniqueIdentifier meth ProbeId = probeId; Method = method; _maxFramesToCapture = ExceptionDebugging.Settings.MaximumFramesToCapture; + _isMisleadingMethod = method.IsMisleadMethod(); } public string ProbeId { get; } @@ -68,7 +70,17 @@ public bool Process(ref CaptureInfo info, IDebuggerSnapshotC case MethodState.EntryStart: case MethodState.EntryAsync: shadowStack = ShadowStackHolder.EnsureShadowStackEnabled(); - snapshotCreator.EnterHash = shadowStack.CurrentStackFrameNode?.EnterSequenceHash ?? Fnv1aHash.FnvOffsetBias; + var currentFrame = shadowStack.CurrentStackFrameNode; + snapshotCreator.EnterHash = Fnv1aHash.Combine(info.Method.MetadataToken, shadowStack.CurrentStackFrameNode?.EnterSequenceHash ?? Fnv1aHash.FnvOffsetBias); + + if (currentFrame?.Method == info.Method && _isMisleadingMethod) + { + // Methods marked as `misleading` are methods we tolerate being in the shadow stack multiple times. + // We flatten those methods due to `ExceptionDispatchInfo.Capture(X).Throw()` API causing frames to appear twice + // while in reality they were involved only once. + snapshotCreator.TrackedStackFrameNode = shadowStack.EnterFake(info.Method); + return true; + } var shouldProcess = false; foreach (var processor in snapshotCreator.Processors) @@ -113,7 +125,15 @@ public bool Process(ref CaptureInfo info, IDebuggerSnapshotC var exception = info.Value as Exception; snapshotCreator.TrackedStackFrameNode.LeavingException = exception; - snapshotCreator.LeaveHash = shadowStack.CurrentStackFrameNode?.LeaveSequenceHash ?? Fnv1aHash.FnvOffsetBias; + snapshotCreator.LeaveHash = shadowStack.CurrentStackFrameNode!.LeaveSequenceHash; + + if (snapshotCreator.TrackedStackFrameNode is FakeTrackedStackFrameNode) + { + shadowStack.Leave(snapshotCreator.TrackedStackFrameNode, exception); + snapshotCreator.TrackedStackFrameNode.CapturingStrategy = SnapshotCapturingStrategy.FullSnapshot; + snapshotCreator.TrackedStackFrameNode.AddScopeMember(info.Name, info.Type, info.Value, info.MemberKind); + return true; + } var leavingExceptionType = info.Value.GetType(); diff --git a/tracer/src/Datadog.Trace/Debugger/ExceptionAutoInstrumentation/ExceptionNormalizer.cs b/tracer/src/Datadog.Trace/Debugger/ExceptionAutoInstrumentation/ExceptionNormalizer.cs index 1869b3d84ecb..6a39312b15c4 100644 --- a/tracer/src/Datadog.Trace/Debugger/ExceptionAutoInstrumentation/ExceptionNormalizer.cs +++ b/tracer/src/Datadog.Trace/Debugger/ExceptionAutoInstrumentation/ExceptionNormalizer.cs @@ -104,54 +104,83 @@ protected virtual int HashLine(VendoredMicrosoftCode.System.ReadOnlySpan l } [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal IEnumerable ParseFrames(string exceptionString) + internal List ParseFrames(string exceptionString) { if (string.IsNullOrEmpty(exceptionString)) { throw new ArgumentException(@"Exception string cannot be null or empty", nameof(exceptionString)); } - var exceptionSpan = VendoredMicrosoftCode.System.MemoryExtensions.AsSpan(exceptionString); - var inSpan = VendoredMicrosoftCode.System.MemoryExtensions.AsSpan(" in "); - var atSpan = VendoredMicrosoftCode.System.MemoryExtensions.AsSpan("at "); - var lambdaSpan = VendoredMicrosoftCode.System.MemoryExtensions.AsSpan("lambda_"); - var datadogSpan = VendoredMicrosoftCode.System.MemoryExtensions.AsSpan("at Datadog."); + var results = new List(); + var currentSpan = VendoredMicrosoftCode.System.MemoryExtensions.AsSpan(exceptionString); - while (!exceptionSpan.IsEmpty) + while (!currentSpan.IsEmpty) { - var lineEndIndex = exceptionSpan.IndexOfAny('\r', '\n'); + var lineEndIndex = currentSpan.IndexOfAny('\r', '\n'); VendoredMicrosoftCode.System.ReadOnlySpan line; if (lineEndIndex >= 0) { - line = exceptionSpan.Slice(0, lineEndIndex); - exceptionSpan = exceptionSpan.Slice(lineEndIndex + 1); - if (!exceptionSpan.IsEmpty && exceptionSpan[0] == '\n') + line = currentSpan.Slice(0, lineEndIndex); + currentSpan = currentSpan.Slice(lineEndIndex + 1); + if (!currentSpan.IsEmpty && currentSpan[0] == '\n') { - exceptionSpan = exceptionSpan.Slice(1); + currentSpan = currentSpan.Slice(1); } } else { - line = exceptionSpan; - exceptionSpan = default; + line = currentSpan; + currentSpan = default; } - // Is frame line (starts with `in `). - if (VendoredMicrosoftCode.System.MemoryExtensions.StartsWith(line.TrimStart(), atSpan, StringComparison.Ordinal)) - { - var index = VendoredMicrosoftCode.System.MemoryExtensions.IndexOf(line, inSpan, StringComparison.Ordinal); - line = index > 0 ? line.Slice(0, index) : line; + ProcessLine(line, results); + } - if (VendoredMicrosoftCode.System.MemoryExtensions.Contains(line, lambdaSpan, StringComparison.Ordinal) || - VendoredMicrosoftCode.System.MemoryExtensions.Contains(line, datadogSpan, StringComparison.Ordinal)) - { - continue; - } + return results; + } - yield return line.ToString(); - } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void ProcessLine(VendoredMicrosoftCode.System.ReadOnlySpan line, List results) + { + line = line.TrimStart(); + if (line.IsEmpty) + { + return; + } + + // Check if it's a stack frame line (starts with "at ") + if (!VendoredMicrosoftCode.System.MemoryExtensions.StartsWith(line, VendoredMicrosoftCode.System.MemoryExtensions.AsSpan("at "), StringComparison.Ordinal)) + { + return; + } + + // Skip the "at " prefix + line = line.Slice(3); + + // Skip lambda and Datadog frames early + if (ContainsAny(line, "lambda_", "at Datadog.")) + { + return; + } + + // Find the " in " marker and truncate if found + var inIndex = VendoredMicrosoftCode.System.MemoryExtensions.IndexOf(line, VendoredMicrosoftCode.System.MemoryExtensions.AsSpan(" in "), StringComparison.Ordinal); + + if (inIndex > 0) + { + line = line.Slice(0, inIndex); } + + // Only create a string when we're sure we want to keep this frame + results.Add(line.ToString()); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool ContainsAny(VendoredMicrosoftCode.System.ReadOnlySpan source, string first, string second) + { + return VendoredMicrosoftCode.System.MemoryExtensions.Contains(source, VendoredMicrosoftCode.System.MemoryExtensions.AsSpan(first), StringComparison.Ordinal) || + VendoredMicrosoftCode.System.MemoryExtensions.Contains(source, VendoredMicrosoftCode.System.MemoryExtensions.AsSpan(second), StringComparison.Ordinal); } } } diff --git a/tracer/src/Datadog.Trace/Debugger/ExceptionAutoInstrumentation/ExceptionProbeProcessor.cs b/tracer/src/Datadog.Trace/Debugger/ExceptionAutoInstrumentation/ExceptionProbeProcessor.cs index b804dd13c525..0bffe86f733f 100644 --- a/tracer/src/Datadog.Trace/Debugger/ExceptionAutoInstrumentation/ExceptionProbeProcessor.cs +++ b/tracer/src/Datadog.Trace/Debugger/ExceptionAutoInstrumentation/ExceptionProbeProcessor.cs @@ -116,19 +116,17 @@ private bool EnsureLeaveHashComputed() Array.Reverse(installedProbes); } - if (!installedProbes.Any()) - { - return Fnv1aHash.FnvOffsetBias; - } - var hash = Fnv1aHash.FnvOffsetBias; - foreach (var probe in installedProbes) + if (installedProbes.Any()) { - hash = Fnv1aHash.Combine(probe.Method.MethodToken, hash); + foreach (var probe in installedProbes) + { + hash = Fnv1aHash.Combine(probe.Method.Method.MetadataToken, hash); + } } - return hash; + return Fnv1aHash.Combine(ExceptionDebuggingProcessor.Method.Method.MetadataToken, hash); } internal void InvalidateEnterLeave() diff --git a/tracer/src/Datadog.Trace/Debugger/ExceptionAutoInstrumentation/ExceptionReplaySnapshotCreator.cs b/tracer/src/Datadog.Trace/Debugger/ExceptionAutoInstrumentation/ExceptionReplaySnapshotCreator.cs index 1c3eb75d693c..81eaceb6ccf8 100644 --- a/tracer/src/Datadog.Trace/Debugger/ExceptionAutoInstrumentation/ExceptionReplaySnapshotCreator.cs +++ b/tracer/src/Datadog.Trace/Debugger/ExceptionAutoInstrumentation/ExceptionReplaySnapshotCreator.cs @@ -17,36 +17,32 @@ namespace Datadog.Trace.Debugger.ExceptionAutoInstrumentation { internal class ExceptionReplaySnapshotCreator : DebuggerSnapshotCreator { - private readonly string _exceptionHash; - private readonly string _exceptionCaptureId; - private readonly int _frameIndex; - - public ExceptionReplaySnapshotCreator(bool isFullSnapshot, ProbeLocation location, bool hasCondition, string[] tags, CaptureLimitInfo limitInfo, string exceptionHash, string exceptionCaptureId, int frameIndex) + public ExceptionReplaySnapshotCreator(bool isFullSnapshot, ProbeLocation location, bool hasCondition, string[] tags, CaptureLimitInfo limitInfo) : base(isFullSnapshot, location, hasCondition, tags, limitInfo) { - _exceptionHash = exceptionHash; - _exceptionCaptureId = exceptionCaptureId; - _frameIndex = frameIndex; } - public ExceptionReplaySnapshotCreator(bool isFullSnapshot, ProbeLocation location, bool hasCondition, string[] tags, MethodScopeMembers methodScopeMembers, CaptureLimitInfo limitInfo, string exceptionHash, string exceptionCaptureId, int frameIndex) + public ExceptionReplaySnapshotCreator(bool isFullSnapshot, ProbeLocation location, bool hasCondition, string[] tags, MethodScopeMembers methodScopeMembers, CaptureLimitInfo limitInfo) : base(isFullSnapshot, location, hasCondition, tags, methodScopeMembers, limitInfo) { - _exceptionHash = exceptionHash; - _exceptionCaptureId = exceptionCaptureId; - _frameIndex = frameIndex; } + internal static string ExceptionHash { get; } = Guid.NewGuid().ToString(); + + internal static string ExceptionCaptureId { get; } = Guid.NewGuid().ToString(); + + internal static string FrameIndex { get; } = Guid.NewGuid().ToString(); + internal override DebuggerSnapshotCreator EndSnapshot() { - JsonWriter.WritePropertyName("exception_hash"); - JsonWriter.WriteValue(_exceptionHash); + JsonWriter.WritePropertyName("exceptionHash"); + JsonWriter.WriteValue(ExceptionHash); - JsonWriter.WritePropertyName("exception_capture_id"); - JsonWriter.WriteValue(_exceptionCaptureId); + JsonWriter.WritePropertyName("exceptionId"); + JsonWriter.WriteValue(ExceptionCaptureId); - JsonWriter.WritePropertyName("frame_index"); - JsonWriter.WriteValue(_frameIndex); + JsonWriter.WritePropertyName("frameIndex"); + JsonWriter.WriteValue(FrameIndex); return base.EndSnapshot(); } diff --git a/tracer/src/Datadog.Trace/Debugger/ExceptionAutoInstrumentation/ExceptionTrackManager.cs b/tracer/src/Datadog.Trace/Debugger/ExceptionAutoInstrumentation/ExceptionTrackManager.cs index 834fcb9807b4..0cb95b3d279f 100644 --- a/tracer/src/Datadog.Trace/Debugger/ExceptionAutoInstrumentation/ExceptionTrackManager.cs +++ b/tracer/src/Datadog.Trace/Debugger/ExceptionAutoInstrumentation/ExceptionTrackManager.cs @@ -19,6 +19,7 @@ using Datadog.Trace.Debugger.Sink.Models; using Datadog.Trace.Debugger.Snapshots; using Datadog.Trace.Debugger.Symbols; +using Datadog.Trace.DuckTyping; using Datadog.Trace.Logging; using Datadog.Trace.Util; using Datadog.Trace.VendoredMicrosoftCode.System.Buffers; @@ -55,7 +56,16 @@ private static async Task StartExceptionProcessingAsync(CancellationToken cancel while (ExceptionProcessQueue.TryDequeue(out var exception)) { - ProcessException(exception, 0, ErrorOriginKind.HttpRequestFailure, rootSpan: null); + try + { + ProcessException(exception, 0, ErrorOriginKind.HttpRequestFailure, rootSpan: null); + } +#pragma warning disable DD0001 + catch (Exception ex) + { + Log.Error(ex, "An exception was thrown while processing an exception for tracking from background thread. Exception = {Exception}", exception.ToString()); + } +#pragma warning restore DD0001 } } } @@ -223,9 +233,9 @@ private static void ProcessException(Exception exception, int normalizedExHash, SetDiagnosticTag(rootSpan, ExceptionReplayDiagnosticTagNames.InvalidatedExceptionCase, normalizedExHash); - var allFrames = trackedExceptionCase.ExceptionCase.ExceptionId.StackTrace; + var allFrames = StackTraceProcessor.ParseFrames(exception.ToString()); var allProbes = trackedExceptionCase.ExceptionCase.Probes; - var frameIndex = allFrames.Length - 1; + var frameIndex = allFrames.Count - 1; var debugErrorPrefix = "_dd.debug.error"; var assignIndex = 0; @@ -237,11 +247,11 @@ private static void ProcessException(Exception exception, int normalizedExHash, while (frameIndex >= 0) { var participatingFrame = allFrames[frameIndex--]; - var noCaptureReason = GetNoCaptureReason(participatingFrame, allProbes.FirstOrDefault(p => p.Method.Equals(participatingFrame.MethodIdentifier))); + var noCaptureReason = GetNoCaptureReason(participatingFrame, allProbes.FirstOrDefault(p => MethodMatcher.IsMethodMatch(participatingFrame, p.Method.Method))); if (noCaptureReason != string.Empty) { - TagMissingFrame(rootSpan, $"{debugErrorPrefix}.{assignIndex}.", participatingFrame.Method, noCaptureReason); + TagMissingFrame(rootSpan, $"{debugErrorPrefix}.{assignIndex}.", participatingFrame, noCaptureReason); } assignIndex += 1; @@ -325,90 +335,142 @@ private static void ProcessException(Exception exception, int normalizedExHash, { if (rootSpan == null) { - Log.Error("The RootSpan is null in the branch of extracing snapshots. Should not happen. Exception: {Exception}", exception.ToString()); + Log.Error("The RootSpan is null in the branch of extracting snapshots. Should not happen. Exception: {Exception}", exception.ToString()); return; } + var exceptionCaptureId = Guid.NewGuid().ToString(); + // Attach tags to the root span var debugErrorPrefix = "_dd.debug.error"; rootSpan.Tags.SetTag("error.debug_info_captured", "true"); rootSpan.Tags.SetTag($"{debugErrorPrefix}.exception_hash", trackedExceptionCase.ErrorHash); - rootSpan.Tags.SetTag($"{debugErrorPrefix}.exception_id", Guid.NewGuid().ToString()); + rootSpan.Tags.SetTag($"{debugErrorPrefix}.exception_id", exceptionCaptureId); var @case = trackedExceptionCase.ExceptionCase; var capturedFrames = resultCallStackTree.Frames; - var allFrames = @case.ExceptionId.StackTrace; + var allFrames = StackTraceProcessor.ParseFrames(exception.ToString()); + var frameIndex = allFrames.Count - 1; + var uploadedHeadFrame = false; // Upload head frame - var frameIndex = 0; - - while (frameIndex < allFrames.Length && - !allFrames[frameIndex].Method.Equals(capturedFrames[0].MethodInfo.Method)) + if (capturedFrames[0].MethodInfo.Method.Equals(@case.Probes[0].Method.Method)) { - frameIndex += 1; - } - - var frame = capturedFrames[0]; - TagAndUpload(rootSpan, $"{debugErrorPrefix}.{allFrames.Length - frameIndex - 1}.", frame); - - // Upload tail frames - frameIndex = allFrames.Length - 1; - var capturedFrameIndex = capturedFrames.Count - 1; - var assignIndex = 0; + while (frameIndex >= 0 && !MethodMatcher.IsMethodMatch(allFrames[frameIndex], capturedFrames[0].MethodInfo.Method)) + { + // Just processing the frames until a match is found + frameIndex -= 1; + } - while (capturedFrameIndex >= 1 && frameIndex >= 0) + TagAndUpload(rootSpan, $"{debugErrorPrefix}.{frameIndex}.", capturedFrames[0], exceptionId: exceptionCaptureId, exceptionHash: trackedExceptionCase.ErrorHash, frameIndex: frameIndex); + uploadedHeadFrame = true; + } + else { - frame = capturedFrames[capturedFrameIndex]; - - var participatingFrame = allFrames[frameIndex--]; + // Missing head + var probe = @case.Probes[0]; + var noCaptureReason = GetNoCaptureReason(probe.Method.Method.Name, probe); - if (!participatingFrame.Method.Equals(frame.MethodInfo.Method)) + if (noCaptureReason != string.Empty) { - var noCaptureReason = GetNoCaptureReason(participatingFrame, @case.Probes.FirstOrDefault(p => p.Method.Equals(participatingFrame.MethodIdentifier))); - - if (noCaptureReason != string.Empty) + while (frameIndex >= 0 && !MethodMatcher.IsMethodMatch(allFrames[frameIndex], @case.Probes[0].Method.Method)) { - TagMissingFrame(rootSpan, $"{debugErrorPrefix}.{assignIndex}.", participatingFrame.Method, noCaptureReason); + // Just processing the frames until a match is found + frameIndex -= 1; } - assignIndex += 1; - continue; + TagMissingFrame(rootSpan, $"{debugErrorPrefix}.{frameIndex}.", probe.Method.Method.Name, noCaptureReason); } - - capturedFrameIndex -= 1; - - var prefix = $"{debugErrorPrefix}.{assignIndex++}."; - TagAndUpload(rootSpan, prefix, frame); } - // Upload missing frames - var maxFramesToCaptureIncludingHead = MaxFramesToCapture + 1; - if (capturedFrames.Count < maxFramesToCaptureIncludingHead && - allFrames.Length > capturedFrames.Count) + frameIndex = 0; + var capturedFrameIndex = capturedFrames.Count - 1; + var probeIndex = @case.Probes.Length - 1; + var capturedFrameIndexBound = uploadedHeadFrame ? 0 : -1; + var uploadFramesBound = MaxFramesToCapture; + var uploadedFrames = 0; + while (frameIndex < allFrames.Count && uploadedFrames < uploadFramesBound && probeIndex >= 0) { - frameIndex = allFrames.Length - 1; - var probesIndex = @case.Probes.Length - 1; - assignIndex = 0; - - while (frameIndex >= 0 && maxFramesToCaptureIncludingHead > 0) + if (capturedFrameIndex <= capturedFrameIndexBound) { - maxFramesToCaptureIncludingHead -= 1; - var participatingFrame = allFrames[frameIndex--]; + // No 'captured frames' left for matching + while (probeIndex >= 0) + { + var noCaptureReason = GetNoCaptureReason(@case.Probes[probeIndex].Method.Method.Name, @case.Probes[probeIndex]); - var noCaptureReason = GetNoCaptureReasonForFrame(participatingFrame); + if (noCaptureReason != string.Empty) + { + while (frameIndex < allFrames.Count && !MethodMatcher.IsMethodMatch(allFrames[frameIndex], @case.Probes[probeIndex].Method.Method)) + { + // Just processing the frames until a match is found + frameIndex += 1; + } + + if (frameIndex >= allFrames.Count) + { + // Nothing left to match + break; + } + + TagMissingFrame(rootSpan, $"{debugErrorPrefix}.{frameIndex}.", @case.Probes[probeIndex].Method.Method.Name, noCaptureReason); + } + + probeIndex -= 1; + } - if (noCaptureReason == string.Empty && probesIndex >= 0) + break; + } + + while (probeIndex >= 0 && !capturedFrames[capturedFrameIndex].MethodInfo.Method.Equals(@case.Probes[probeIndex].Method.Method)) + { + // Determine if the frame is misleading (e.g duplicated on the stack thanks for ExceptionCaptureInfo.Throw) + var prevIndex = probeIndex + 1; + if (prevIndex < @case.Probes.Length && capturedFrames[capturedFrameIndex].MethodInfo.Method.Equals(@case.Probes[prevIndex].Method.Method) && @case.Probes[prevIndex].Method.IsMisleadMethod()) { - noCaptureReason = GetNoCaptureReason(participatingFrame, @case.Probes[probesIndex--]); + // The current captured frame is marked as misleading. Consider the previous probe as the 'current' probe for a proper matching in this extremely rare case + Log.Warning("Encountered misleading frame that is also recursive with exception: {Exception}, Method: {MethodName}", exception.ToString(), capturedFrames[capturedFrameIndex].MethodInfo.Method.GetFullName()); + probeIndex += 1; + break; } + var noCaptureReason = GetNoCaptureReason(@case.Probes[probeIndex].Method.Method.Name, @case.Probes[probeIndex]); + if (noCaptureReason != string.Empty) { - TagMissingFrame(rootSpan, $"{debugErrorPrefix}.{assignIndex}.", participatingFrame.Method, noCaptureReason); + while (frameIndex < allFrames.Count && !MethodMatcher.IsMethodMatch(allFrames[frameIndex], @case.Probes[probeIndex].Method.Method)) + { + // Just processing the frames until a match is found + frameIndex += 1; + } + + TagMissingFrame(rootSpan, $"{debugErrorPrefix}.{frameIndex}.", @case.Probes[probeIndex].Method.Method.Name, noCaptureReason); } - assignIndex++; + probeIndex -= 1; } + + if (probeIndex < 0) + { + // We exhausted the probes array, nothing left to match + break; + } + + while (frameIndex < allFrames.Count && !MethodMatcher.IsMethodMatch(allFrames[frameIndex], capturedFrames[capturedFrameIndex].MethodInfo.Method)) + { + frameIndex += 1; + } + + if (frameIndex >= allFrames.Count) + { + // We exhausted the whole frames array, nothing left to match + break; + } + + TagAndUpload(rootSpan, $"{debugErrorPrefix}.{frameIndex}.", capturedFrames[capturedFrameIndex], exceptionId: exceptionCaptureId, exceptionHash: trackedExceptionCase.ErrorHash, frameIndex: frameIndex); + capturedFrameIndex -= 1; + probeIndex -= 1; + frameIndex += 1; + uploadedFrames += 1; } Log.Information("Reverting an exception case for exception: {Name}, Message: {Message}, StackTrace: {StackTrace}", exception.GetType().Name, exception.Message, exception.StackTrace); @@ -432,31 +494,30 @@ private static void ProcessException(Exception exception, int normalizedExHash, } } - private static string GetNoCaptureReason(ParticipatingFrame frame, ExceptionDebuggingProbe? probe) + private static string GetNoCaptureReason(string methodName, ExceptionDebuggingProbe? probe) { - var noCaptureReason = GetNoCaptureReasonForFrame(frame); - - if (noCaptureReason != string.Empty) - { - return noCaptureReason; - } + var noCaptureReason = string.Empty; if (probe != null) { if (probe.MayBeOmittedFromCallStack) { // The process is spawned with `COMPLUS_ForceEnc` & the module of the method is non-optimized. - noCaptureReason = $"The method {frame.Method.GetFullyQualifiedName()} could not be captured because the process is spawned with Edit and Continue feature turned on and the module is compiled as Debug. Set the environment variable `COMPLUS_ForceEnc` to `0`. For further info, visit: https://github.com/dotnet/runtime/issues/91963."; + noCaptureReason = $"The method {methodName} could not be captured because the process is spawned with Edit and Continue feature turned on and the module is compiled as Debug. Set the environment variable `COMPLUS_ForceEnc` to `0`. For further info, visit: https://github.com/dotnet/runtime/issues/91963."; } else if (probe.ProbeStatus == Status.ERROR) { // Frame is failed to instrument. - noCaptureReason = $"The method {frame.Method.GetFullyQualifiedName()} has failed in instrumentation. Failure reason: {probe.ErrorMessage}"; + noCaptureReason = $"The method {methodName} has failed in instrumentation. Failure reason: {probe.ErrorMessage}"; } else if (probe.ProbeStatus == Status.RECEIVED) { // Frame is failed to instrument. - noCaptureReason = $"The method {frame.Method.GetFullyQualifiedName()} could not be found."; + noCaptureReason = $"The method {methodName} could not be found."; + } + else if (probe.Method.IsMisleadMethod()) + { + noCaptureReason = $"This frame of {methodName} is a duplication, due to `ExceptionDispatchInfo.Throw()`."; } } @@ -480,7 +541,7 @@ private static string GetNoCaptureReasonForFrame(ParticipatingFrame frame) return noCaptureReason; } - private static void TagAndUpload(Span span, string tagPrefix, ExceptionStackNodeRecord record) + private static void TagAndUpload(Span span, string tagPrefix, ExceptionStackNodeRecord record, string exceptionId, string exceptionHash, int frameIndex) { var method = record.MethodInfo.Method; var snapshotId = record.SnapshotId; @@ -491,13 +552,16 @@ private static void TagAndUpload(Span span, string tagPrefix, ExceptionStackNode span.Tags.SetTag(tagPrefix + "frame_data.class_name", method.DeclaringType?.Name); span.Tags.SetTag(tagPrefix + "snapshot_id", snapshotId); + snapshot = snapshot + .Replace(ExceptionReplaySnapshotCreator.ExceptionCaptureId, exceptionId) + .Replace(ExceptionReplaySnapshotCreator.ExceptionHash, exceptionHash) + .Replace(ExceptionReplaySnapshotCreator.FrameIndex, frameIndex.ToString()); ExceptionDebugging.AddSnapshot(probeId, snapshot); } - private static void TagMissingFrame(Span span, string tagPrefix, MethodBase method, string reason) + private static void TagMissingFrame(Span span, string tagPrefix, string method, string reason) { - span.Tags.SetTag(tagPrefix + "frame_data.function", method.Name); - span.Tags.SetTag(tagPrefix + "frame_data.class_name", method.DeclaringType?.Name); + span.Tags.SetTag(tagPrefix + "frame_data.name", method); span.Tags.SetTag(tagPrefix + "no_capture_reason", reason); } diff --git a/tracer/src/Datadog.Trace/Debugger/ExceptionAutoInstrumentation/FakeTrackedStackFrameNode.cs b/tracer/src/Datadog.Trace/Debugger/ExceptionAutoInstrumentation/FakeTrackedStackFrameNode.cs new file mode 100644 index 000000000000..b122752c57a2 --- /dev/null +++ b/tracer/src/Datadog.Trace/Debugger/ExceptionAutoInstrumentation/FakeTrackedStackFrameNode.cs @@ -0,0 +1,47 @@ +// +// 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. +// + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using Datadog.Trace.Debugger.Expressions; +using Datadog.Trace.Debugger.Instrumentation.Collections; +using Datadog.Trace.Debugger.Snapshots; +using Fnv1aHash = Datadog.Trace.VendoredMicrosoftCode.System.Reflection.Internal.Hash; + +#nullable enable +namespace Datadog.Trace.Debugger.ExceptionAutoInstrumentation +{ + internal class FakeTrackedStackFrameNode(TrackedStackFrameNode? parent, MethodBase method) + : TrackedStackFrameNode(parent, method, isInvalidPath: false) + { + protected override int ComputeEnterSequenceHash() + { + return Parent!.EnterSequenceHash; + } + + protected override int ComputeLeaveSequenceHash() + { + lock (this) + { + ClearNonRelevantChildNodes(); + + if (ActiveChildNodes?.Any() == true) + { + var firstChild = ActiveChildNodes.First(); + return firstChild.LeaveSequenceHash; + } + + return Fnv1aHash.Combine(Method.MetadataToken, Fnv1aHash.FnvOffsetBias); + } + } + + public override string ToString() + { + return $"{nameof(FakeTrackedStackFrameNode)}(Child Count = {ActiveChildNodes?.Count})"; + } + } +} diff --git a/tracer/src/Datadog.Trace/Debugger/ExceptionAutoInstrumentation/ILAnalyzer.cs b/tracer/src/Datadog.Trace/Debugger/ExceptionAutoInstrumentation/ILAnalyzer.cs new file mode 100644 index 000000000000..b488a6f690be --- /dev/null +++ b/tracer/src/Datadog.Trace/Debugger/ExceptionAutoInstrumentation/ILAnalyzer.cs @@ -0,0 +1,117 @@ +// +// 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. +// + +using System; +using System.Reflection; +using System.Reflection.Emit; +using System.Runtime.CompilerServices; + +#nullable enable +namespace Datadog.Trace.Debugger.ExceptionAutoInstrumentation +{ + internal static class ILAnalyzer + { + public static bool HasDirectCallTo(MethodBase method, Type targetType, string methodName) + { + try + { + var effectiveMethod = GetEffectiveMethod(method); + return AnalyzeMethodBody(effectiveMethod, targetType, methodName); + } + catch + { + // If we can't analyze the method for any reason, assume it might not contain the call + return false; + } + } + + private static MethodBase GetEffectiveMethod(MethodBase method) + { + // For async methods, analyze the MoveNext method + if (method.GetCustomAttribute() is AsyncStateMachineAttribute asyncAttribute) + { + var stateMachineType = asyncAttribute.StateMachineType; + return stateMachineType.GetMethod( + "MoveNext", + BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public) ?? method; + } + + return method; + } + + private static bool AnalyzeMethodBody(MethodBase methodToAnalyze, Type targetType, string methodName) + { + var methodBody = methodToAnalyze.GetMethodBody(); + if (methodBody == null) + { + return false; + } + + var il = methodBody.GetILAsByteArray(); + if (il == null) + { + return false; + } + + int position = 0; + while (position < il.Length) + { + // Read opcode + int opcode = il[position++]; + if (opcode == 0xFE && position < il.Length) + { + opcode = 0xFE00 | il[position++]; + } + + // Check for call instructions + if (opcode == OpCodes.Call.Value || opcode == OpCodes.Callvirt.Value) + { + // Ensure we have enough bytes for the token + if (position + 3 >= il.Length) + { + break; + } + + int token = il[position] | + (il[position + 1] << 8) | + (il[position + 2] << 16) | + (il[position + 3] << 24); + + position += 4; + + try + { + var calledMethod = methodToAnalyze?.Module?.ResolveMethod(token); + if (calledMethod?.DeclaringType == targetType && + calledMethod.Name == methodName) + { + return true; + } + } + catch + { + // If we can't resolve this specific method, continue checking others + continue; + } + } + else + { + // Skip operands for other opcodes + position += GetOperandSize(opcode); + } + } + + return false; + } + + private static int GetOperandSize(int opcode) => opcode switch + { + 0x28 or 0x6F or 0x70 or 0x11 or 0x20 => 4, // Various 4-byte operand instructions + 0x73 or 0x6E => 4, // Newobj, Ldtoken + 0x1B or 0x31 or 0x15 or 0x2A => 1, // Various 1-byte operand instructions + _ => 0 + }; + } +} diff --git a/tracer/src/Datadog.Trace/Debugger/ExceptionAutoInstrumentation/MethodMatcher.cs b/tracer/src/Datadog.Trace/Debugger/ExceptionAutoInstrumentation/MethodMatcher.cs new file mode 100644 index 000000000000..5067077d6792 --- /dev/null +++ b/tracer/src/Datadog.Trace/Debugger/ExceptionAutoInstrumentation/MethodMatcher.cs @@ -0,0 +1,454 @@ +// +// 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. +// + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; + +#nullable enable +namespace Datadog.Trace.Debugger.ExceptionAutoInstrumentation; + +internal static class MethodMatcher +{ + private static readonly ConcurrentDictionary AsyncMethodCache = new(); + + /// + /// Attempts to match a method name from a stack trace with a MethodBase, + /// handling special cases like async methods, iterators, and nested methods. + /// + internal static bool IsMethodMatch(string stackTraceMethodText, MethodBase methodBase) + { + // First try to resolve special method types (async, iterator, etc.) + var resolvedMethod = ResolveSpecialMethod(methodBase); + + // Use resolved method if available, otherwise use original + methodBase = resolvedMethod ?? methodBase; + + // Normalize and parse both representations + var normalizedStackTrace = NormalizeMethodText(stackTraceMethodText); + var methodBaseName = BuildMethodSignature(methodBase); + + var stackTraceParts = ParseMethodName(normalizedStackTrace); + var methodBaseParts = ParseMethodName(methodBaseName); + + if (stackTraceParts == null || methodBaseParts == null) + { + return normalizedStackTrace.Equals(methodBaseName, StringComparison.Ordinal); + } + + return DoMethodPartsMatch(stackTraceParts, methodBaseParts); + } + + private static MethodBase? ResolveSpecialMethod(MethodBase method) + { + // Check cache first + if (AsyncMethodCache.TryGetValue(method, out var cachedMethod)) + { + return cachedMethod; + } + + MethodBase? resolvedMethod = null; + + if (IsAsyncStateMachine(method)) + { + resolvedMethod = ResolveAsyncMethod(method); + } + else if (IsIteratorStateMachine(method)) + { + resolvedMethod = ResolveIteratorMethod(method); + } + else if (IsLocalFunction(method)) + { + resolvedMethod = ResolveLocalFunction(method); + } + + // Cache the result (null is a valid cache result) + AsyncMethodCache.TryAdd(method, resolvedMethod); + return resolvedMethod; + } + + private static bool IsAsyncStateMachine(MethodBase method) + { + if (method.Name != "MoveNext") + { + return false; + } + + var declaringType = method.DeclaringType; + if (declaringType == null) + { + return false; + } + + // Check both the type name pattern and implemented interfaces + return declaringType.Name.Contains(">d__") && + declaringType.GetInterfaces() + .Any(i => i.FullName == "System.Runtime.CompilerServices.IAsyncStateMachine"); + } + + private static bool IsIteratorStateMachine(MethodBase method) + { + if (method.Name != "MoveNext") + { + return false; + } + + var declaringType = method.DeclaringType; + if (declaringType == null) + { + return false; + } + + return declaringType.GetInterfaces() + .Any(i => i.FullName == "System.Collections.IEnumerator" || + (i.FullName?.StartsWith("System.Collections.Generic.IEnumerator`") ?? false)); + } + + private static bool IsLocalFunction(MethodBase method) + { + // Local functions have a generated name starting with "<" + return method.Name.StartsWith("<") && method.Name.Contains(">"); + } + + private static MethodBase? ResolveAsyncMethod(MethodBase moveNextMethod) + { + var stateMachineType = moveNextMethod.DeclaringType; + if (stateMachineType == null) + { + return null; + } + + // Handle nested types and generic type parameters + var containingType = ResolveContainingType(stateMachineType); + if (containingType == null) + { + return null; + } + + var originalMethodName = ExtractMethodNameFromStateMachine(stateMachineType.Name); + if (originalMethodName == null) + { + return null; + } + + // Find all methods with matching name + var candidates = containingType.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | + BindingFlags.Static | BindingFlags.Instance | + BindingFlags.DeclaredOnly) + .Where(m => m.Name == originalMethodName) + .ToList(); + + // If we have multiple candidates, try to match by async method builder attribute + if (candidates.Count > 1) + { + var matchingMethod = candidates.FirstOrDefault(HasMatchingAsyncMethodBuilder); + if (matchingMethod != null) + { + return matchingMethod; + } + } + + return candidates.FirstOrDefault(); + } + + private static Type? ResolveContainingType(Type stateMachineType) + { + var containingType = stateMachineType.DeclaringType; + if (containingType == null) + { + return null; + } + + // Handle nested generic types + if (containingType.IsGenericType && !containingType.IsGenericTypeDefinition) + { + // Get the generic type definition + containingType = containingType.GetGenericTypeDefinition(); + } + + return containingType; + } + + private static string? ExtractMethodNameFromStateMachine(string typeName) + { + var methodNameStart = typeName.IndexOf('<'); + var methodNameEnd = typeName.IndexOf('>'); + + if (methodNameStart < 0 || methodNameEnd <= methodNameStart) + { + return null; + } + + return typeName.Substring(methodNameStart + 1, methodNameEnd - methodNameStart - 1); + } + + private static bool HasMatchingAsyncMethodBuilder(MethodInfo method) + { + return method.GetCustomAttributes() + .Any(attr => attr.GetType().Name.EndsWith("AsyncStateMachineAttribute")); + } + + private static MethodBase? ResolveIteratorMethod(MethodBase moveNextMethod) + { + // Similar to async method resolution but looking for iterator patterns + var stateMachineType = moveNextMethod.DeclaringType; + if (stateMachineType == null) + { + return null; + } + + var containingType = ResolveContainingType(stateMachineType); + if (containingType == null) + { + return null; + } + + var originalMethodName = ExtractMethodNameFromStateMachine(stateMachineType.Name); + if (originalMethodName == null) + { + return null; + } + + return containingType.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | + BindingFlags.Static | BindingFlags.Instance) + .FirstOrDefault(m => m.Name == originalMethodName && + IsIteratorMethod(m)); + } + + private static bool IsIteratorMethod(MethodInfo method) + { + var returnType = method.ReturnType; + return typeof(System.Collections.IEnumerable).IsAssignableFrom(returnType) || + (returnType.IsGenericType && returnType.GetGenericTypeDefinition() == typeof(IEnumerable<>)); + } + + private static MethodBase? ResolveLocalFunction(MethodBase method) + { + // Extract the original method name from the local function name + var localFunctionName = method.Name; + var parentMethodStart = localFunctionName.IndexOf('<'); + var parentMethodEnd = localFunctionName.IndexOf('>', parentMethodStart); + + if (parentMethodStart < 0 || parentMethodEnd <= parentMethodStart) + { + return null; + } + + var parentMethodName = localFunctionName.Substring(parentMethodStart + 1, parentMethodEnd - parentMethodStart - 1); + + var declaringType = method.DeclaringType?.DeclaringType; + if (declaringType == null) + { + return null; + } + + return declaringType.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | + BindingFlags.Static | BindingFlags.Instance) + .FirstOrDefault(m => m.Name == parentMethodName); + } + + private static MethodNameParts? ParseMethodName(string fullName) + { + var parts = fullName.Split('.'); + if (parts.Length < 2) + { + return null; + } + + // The last part is always the method name + var methodName = NormalizeMethodName(parts[parts.Length - 1]); + + // Find where the type name starts (looking for capital letter after namespace) + var typeStartIndex = -1; + for (var i = 0; i < parts.Length - 1; i++) + { + if (parts[i].Length > 0 && char.IsUpper(parts[i][0])) + { + typeStartIndex = i; + break; + } + } + + if (typeStartIndex == -1) + { + return null; + } + + var namespaceParts = new string[typeStartIndex]; + Array.Copy(parts, 0, namespaceParts, 0, typeStartIndex); + + var typeParts = new string[parts.Length - typeStartIndex - 1]; + Array.Copy(parts, typeStartIndex, typeParts, 0, parts.Length - typeStartIndex - 1); + + return new MethodNameParts(namespaceParts, typeParts, methodName); + } + + private static bool DoMethodPartsMatch(MethodNameParts stackTrace, MethodNameParts methodBase) + { + // Namespace must match exactly + if (!AreArraysEqual(stackTrace.NamespaceParts, methodBase.NamespaceParts)) + { + return false; + } + + // Type parts must match exactly + if (!AreArraysEqual(stackTrace.TypeParts, methodBase.TypeParts)) + { + return false; + } + + // For method names, we need special comparison due to compiler-generated names + return AreMethodNamesEquivalent(stackTrace.MethodName, methodBase.MethodName); + } + + private static bool AreArraysEqual(string[] arr1, string[] arr2) + { + if (arr1.Length != arr2.Length) + { + return false; + } + + for (var i = 0; i < arr1.Length; i++) + { + if (!arr1[i].Equals(arr2[i], StringComparison.Ordinal)) + { + return false; + } + } + + return true; + } + + private static bool AreMethodNamesEquivalent(string stackTraceMethod, string methodBaseMethod) + { + // If they're exactly equal, we're done + if (stackTraceMethod.Equals(methodBaseMethod, StringComparison.Ordinal)) + { + return true; + } + + // For compiler-generated async methods + if (stackTraceMethod == "MoveNext" && + (methodBaseMethod.Contains("Async") || methodBaseMethod.Contains("<"))) + { + return true; + } + + // For compiler-generated methods, we need exact matches of the base name + var baseStackTraceName = RemoveAsyncSuffix(RemoveCompilerGeneratedParts(stackTraceMethod)); + var baseMethodName = RemoveAsyncSuffix(RemoveCompilerGeneratedParts(methodBaseMethod)); + + return baseStackTraceName.Equals(baseMethodName, StringComparison.Ordinal); + } + + private static string RemoveCompilerGeneratedParts(string methodName) + { + // Remove everything between < and > including the brackets + var startBracket = methodName.IndexOf('<'); + var endBracket = methodName.LastIndexOf('>'); + + if (startBracket >= 0 && endBracket > startBracket) + { + methodName = methodName.Substring(0, startBracket) + + methodName.Substring(endBracket + 1); + } + + // Remove any remaining compiler-generated suffixes (like "b__1") + var index = methodName.IndexOf("b__", StringComparison.Ordinal); + if (index > 0) + { + methodName = methodName.Substring(0, index); + } + + return methodName; + } + + private static string RemoveAsyncSuffix(string methodName) + { + return methodName.EndsWith("Async", StringComparison.Ordinal) + ? methodName.Substring(0, methodName.Length - 5) + : methodName; + } + + private static string NormalizeMethodText(string methodText) + { + // Remove parameter list if present (content within parentheses) + var parenthesesIndex = methodText.IndexOf('('); + if (parenthesesIndex > 0) + { + methodText = methodText.Substring(0, parenthesesIndex); + } + + // Special handling for generic method type arguments in stack trace format [T1,T2] + var genericIndex = methodText.IndexOf('['); + if (genericIndex > 0) + { + var closeIndex = methodText.IndexOf(']', genericIndex); + if (closeIndex > genericIndex) + { + // Count the number of type parameters + var typeParams = methodText.Substring(genericIndex + 1, closeIndex - genericIndex - 1) + .Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) + .Length; + + // Replace with a normalized form + methodText = methodText.Substring(0, genericIndex) + "`" + typeParams; + } + } + + return methodText; + } + + private static string NormalizeMethodName(string methodName) + { + // Remove generic arity if present + var backtickIndex = methodName.IndexOf('`'); + if (backtickIndex > 0) + { + methodName = methodName.Substring(0, backtickIndex); + } + + return methodName; + } + + private static string BuildMethodSignature(MethodBase method) + { + var sb = new StringBuilder(); + + // Add declaring type's full name + if (method.DeclaringType != null) + { + var typeFullName = method.DeclaringType.FullName ?? method.DeclaringType.Name; + sb.Append(typeFullName.Replace("+", ".")); + } + + sb.Append('.'); + sb.Append(method.Name); + + return sb.ToString(); + } + + /// + /// Represents the parts of a method's full name + /// + private class MethodNameParts + { + public MethodNameParts(string[] namespaceParts, string[] typeParts, string methodName) + { + NamespaceParts = namespaceParts; + TypeParts = typeParts; + MethodName = methodName; + } + + public string[] NamespaceParts { get; } + + public string[] TypeParts { get; } + + public string MethodName { get; } + } +} diff --git a/tracer/src/Datadog.Trace/Debugger/ExceptionAutoInstrumentation/MethodUniqueIdentifier.cs b/tracer/src/Datadog.Trace/Debugger/ExceptionAutoInstrumentation/MethodUniqueIdentifier.cs index 409a9b56615e..adf71905fded 100644 --- a/tracer/src/Datadog.Trace/Debugger/ExceptionAutoInstrumentation/MethodUniqueIdentifier.cs +++ b/tracer/src/Datadog.Trace/Debugger/ExceptionAutoInstrumentation/MethodUniqueIdentifier.cs @@ -8,16 +8,29 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; +using System.Runtime.CompilerServices; #nullable enable namespace Datadog.Trace.Debugger.ExceptionAutoInstrumentation { internal readonly record struct MethodUniqueIdentifier(Guid Mvid, int MethodToken, MethodBase Method) { + internal static readonly ConcurrentDictionary MightMisleadStacktrace = new(); + public override int GetHashCode() { return HashCode.Combine(Mvid, MethodToken); } + + public bool IsMisleadMethod() + { + return MightMisleadStacktrace.GetOrAdd( + this, + identifier => ILAnalyzer.HasDirectCallTo( + identifier.Method, + typeof(System.Runtime.ExceptionServices.ExceptionDispatchInfo), + "Throw")); + } } internal readonly struct ExceptionCase : IEquatable diff --git a/tracer/src/Datadog.Trace/Debugger/ExceptionAutoInstrumentation/ShadowStackTree.cs b/tracer/src/Datadog.Trace/Debugger/ExceptionAutoInstrumentation/ShadowStackTree.cs index 349bc3c0cb32..5370933c5949 100644 --- a/tracer/src/Datadog.Trace/Debugger/ExceptionAutoInstrumentation/ShadowStackTree.cs +++ b/tracer/src/Datadog.Trace/Debugger/ExceptionAutoInstrumentation/ShadowStackTree.cs @@ -46,6 +46,12 @@ public TrackedStackFrameNode Enter(MethodBase method, bool isInvalidPath = false return _trackedStackFrameActiveNode.Value; } + public TrackedStackFrameNode EnterFake(MethodBase method) + { + _trackedStackFrameActiveNode.Value = new FakeTrackedStackFrameNode(_trackedStackFrameActiveNode.Value, method); + return _trackedStackFrameActiveNode.Value; + } + public bool Leave(TrackedStackFrameNode trackedStackFrameNode, Exception? exception) { var currentActiveNode = _trackedStackFrameActiveNode.Value; diff --git a/tracer/src/Datadog.Trace/Debugger/ExceptionAutoInstrumentation/StackTraceProcessor.cs b/tracer/src/Datadog.Trace/Debugger/ExceptionAutoInstrumentation/StackTraceProcessor.cs new file mode 100644 index 000000000000..e8189bf98e8a --- /dev/null +++ b/tracer/src/Datadog.Trace/Debugger/ExceptionAutoInstrumentation/StackTraceProcessor.cs @@ -0,0 +1,83 @@ +// +// 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. +// + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; +using Datadog.Trace.Debugger.Helpers; +using Datadog.Trace.VendoredMicrosoftCode.System; + +#nullable enable +namespace Datadog.Trace.Debugger.ExceptionAutoInstrumentation; + +internal static class StackTraceProcessor +{ + internal static List ParseFrames(string exceptionString) + { + if (string.IsNullOrEmpty(exceptionString)) + { + throw new ArgumentException(@"Exception string cannot be null or empty", nameof(exceptionString)); + } + + var results = new List(); + var currentSpan = VendoredMicrosoftCode.System.MemoryExtensions.AsSpan(exceptionString); + + while (!currentSpan.IsEmpty) + { + var lineEndIndex = currentSpan.IndexOfAny('\r', '\n'); + VendoredMicrosoftCode.System.ReadOnlySpan line; + + if (lineEndIndex >= 0) + { + line = currentSpan.Slice(0, lineEndIndex); + currentSpan = currentSpan.Slice(lineEndIndex + 1); + if (!currentSpan.IsEmpty && currentSpan[0] == '\n') + { + currentSpan = currentSpan.Slice(1); + } + } + else + { + line = currentSpan; + currentSpan = default; + } + + ProcessLine(line, results); + } + + return results; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void ProcessLine(VendoredMicrosoftCode.System.ReadOnlySpan line, List results) + { + line = line.TrimStart(); + if (line.IsEmpty) + { + return; + } + + // Check if it's a stack frame line (starts with "at ") + if (!VendoredMicrosoftCode.System.MemoryExtensions.StartsWith(line, VendoredMicrosoftCode.System.MemoryExtensions.AsSpan("at "), StringComparison.Ordinal)) + { + return; + } + + // Skip the "at " prefix + line = line.Slice(3); + + // Find the " in " marker and truncate if found + var inIndex = VendoredMicrosoftCode.System.MemoryExtensions.IndexOf(line, VendoredMicrosoftCode.System.MemoryExtensions.AsSpan(" in "), StringComparison.Ordinal); + + if (inIndex > 0) + { + line = line.Slice(0, inIndex); + } + + results.Add(line.ToString()); + } +} diff --git a/tracer/src/Datadog.Trace/Debugger/ExceptionAutoInstrumentation/TrackedStackFrameNode.cs b/tracer/src/Datadog.Trace/Debugger/ExceptionAutoInstrumentation/TrackedStackFrameNode.cs index 7afccd2d91ca..a3854ff49027 100644 --- a/tracer/src/Datadog.Trace/Debugger/ExceptionAutoInstrumentation/TrackedStackFrameNode.cs +++ b/tracer/src/Datadog.Trace/Debugger/ExceptionAutoInstrumentation/TrackedStackFrameNode.cs @@ -18,7 +18,6 @@ namespace Datadog.Trace.Debugger.ExceptionAutoInstrumentation internal class TrackedStackFrameNode { private TrackedStackFrameNode? _parent; - private List? _activeChildNodes; private bool _disposed; private int? _enterSequenceHash; private int? _leaveSequenceHash; @@ -59,6 +58,8 @@ public int SequenceHash } } + protected List? ActiveChildNodes { get; private set; } + public TrackedStackFrameNode? Parent => _parent; public Exception? LeavingException { get; set; } @@ -89,7 +90,7 @@ public string SnapshotId public bool IsInvalidPath { get; } - public IEnumerable ChildNodes => _activeChildNodes?.ToList() ?? Enumerable.Empty(); + public IEnumerable ChildNodes => ActiveChildNodes?.ToList() ?? Enumerable.Empty(); public MethodBase Method { get; } @@ -133,7 +134,7 @@ private string CreateSnapshot() MaxFieldCount: DebuggerSettings.DefaultMaxNumberOfFieldsToCopy, MaxLength: DebuggerSettings.DefaultMaxStringLength); - using var snapshotCreator = new DebuggerSnapshotCreator(isFullSnapshot: true, location: ProbeLocation.Method, hasCondition: false, Array.Empty(), members, limitInfo: limitInfo); + using var snapshotCreator = new ExceptionReplaySnapshotCreator(isFullSnapshot: true, location: ProbeLocation.Method, hasCondition: false, Array.Empty(), members, limitInfo: limitInfo); _snapshotId = snapshotCreator.SnapshotId; @@ -219,7 +220,7 @@ internal void AddScopeMember(string name, Type type, T value, ScopeMemberKind } } - private int ComputeEnterSequenceHash() + protected virtual int ComputeEnterSequenceHash() { return Fnv1aHash.Combine(Method.MetadataToken, _parent?.EnterSequenceHash ?? Fnv1aHash.FnvOffsetBias); } @@ -227,19 +228,19 @@ private int ComputeEnterSequenceHash() /// /// TODO take not only first child. /// - private int ComputeLeaveSequenceHash() + protected virtual int ComputeLeaveSequenceHash() { lock (this) { ClearNonRelevantChildNodes(); - if (_activeChildNodes?.Any() == true) + if (ActiveChildNodes?.Any() == true) { - var firstChild = _activeChildNodes.First(); - return Fnv1aHash.Combine(firstChild.Method.MetadataToken, firstChild.LeaveSequenceHash); + var firstChild = ActiveChildNodes.First(); + return Fnv1aHash.Combine(Method.MetadataToken, firstChild.LeaveSequenceHash); } - return Fnv1aHash.FnvOffsetBias; + return Fnv1aHash.Combine(Method.MetadataToken, Fnv1aHash.FnvOffsetBias); } } @@ -260,14 +261,14 @@ private int ComputeLeaveSequenceHash() lock (_parent) { - _parent._activeChildNodes ??= new List(); - _parent._activeChildNodes.Add(this); + _parent.ActiveChildNodes ??= new List(); + _parent.ActiveChildNodes.Add(this); } lock (this) { // TODO For AggregateException, first/default might no be the most suitable way to tackle it. - var childCapturingCount = _activeChildNodes?.FirstOrDefault()?.NumOfChildren ?? 0; + var childCapturingCount = ActiveChildNodes?.FirstOrDefault()?.NumOfChildren ?? 0; NumOfChildren = childCapturingCount + 1; } @@ -289,23 +290,23 @@ public void Dispose() } _parent = null; - if (_activeChildNodes != null) + if (ActiveChildNodes != null) { - foreach (var node in _activeChildNodes) + foreach (var node in ActiveChildNodes) { node.Dispose(); } } - _activeChildNodes = null; + ActiveChildNodes = null; MarkAsUnwound(); _disposed = true; } - private void ClearNonRelevantChildNodes() + protected void ClearNonRelevantChildNodes() { // ReSharper disable once InconsistentlySynchronizedField - if (_activeChildNodes == null || _childNodesAlreadyCleansed) + if (ActiveChildNodes == null || _childNodesAlreadyCleansed) { return; } @@ -317,18 +318,18 @@ private void ClearNonRelevantChildNodes() return; } - if (!_activeChildNodes.Any()) + if (!ActiveChildNodes.Any()) { - _activeChildNodes = null; + ActiveChildNodes = null; return; } - for (var i = _activeChildNodes.Count - 1; i >= 0; i--) + for (var i = ActiveChildNodes.Count - 1; i >= 0; i--) { - var frame = _activeChildNodes[i]; + var frame = ActiveChildNodes[i]; if (frame.LeavingException == null || !HasChildException(frame.LeavingException)) { - _activeChildNodes.RemoveAt(i); + ActiveChildNodes.RemoveAt(i); frame.Dispose(); } } @@ -355,7 +356,7 @@ public bool HasChildException(Exception? exception) public override string ToString() { - return $"{nameof(TrackedStackFrameNode)}(Child Count = {_activeChildNodes?.Count})"; + return $"{nameof(TrackedStackFrameNode)}(Child Count = {ActiveChildNodes?.Count})"; } } } diff --git a/tracer/src/Datadog.Trace/Debugger/Helpers/MethodExtensions.cs b/tracer/src/Datadog.Trace/Debugger/Helpers/MethodExtensions.cs index c76121e2860f..ca05a232f98d 100644 --- a/tracer/src/Datadog.Trace/Debugger/Helpers/MethodExtensions.cs +++ b/tracer/src/Datadog.Trace/Debugger/Helpers/MethodExtensions.cs @@ -19,6 +19,11 @@ namespace Datadog.Trace.Debugger.Helpers { internal static class MethodExtensions { + internal static string GetFullName(this MethodBase mb) + { + return mb.DeclaringType?.FullName + "." + mb.Name; + } + /// /// Gets fully qualified name of a method with parameters and generics. For example SkyApm.Sample.ConsoleApp.Program.Main(String[] args). /// Code was copied from System.Diagnostics.StackTrace.ToString() - .NET Standard implementation, not .NET Framework diff --git a/tracer/src/Datadog.Trace/Debugger/Helpers/ProbeStatusPollerMock.cs b/tracer/src/Datadog.Trace/Debugger/Helpers/ProbeStatusPollerMock.cs index 4001e22d5733..4314341cfd1a 100644 --- a/tracer/src/Datadog.Trace/Debugger/Helpers/ProbeStatusPollerMock.cs +++ b/tracer/src/Datadog.Trace/Debugger/Helpers/ProbeStatusPollerMock.cs @@ -10,6 +10,7 @@ using System.Threading.Tasks; using Datadog.Trace.Debugger.ProbeStatuses; +#nullable enable namespace Datadog.Trace.Debugger.Helpers { internal class ProbeStatusPollerMock : IProbeStatusPoller diff --git a/tracer/src/Datadog.Tracer.Native/debugger_constants.h b/tracer/src/Datadog.Tracer.Native/debugger_constants.h index 849de952309d..57bc9886e650 100644 --- a/tracer/src/Datadog.Tracer.Native/debugger_constants.h +++ b/tracer/src/Datadog.Tracer.Native/debugger_constants.h @@ -48,6 +48,11 @@ inline WSTRING GetGenericErrorMessageWithErrorCode(short errorCode) return general_error_message + WStr(" [Error Code: ") + shared::ToWSTRING(errorCode) + WStr("]"); } +inline WSTRING GetGenericErrorMessageWithHr(HRESULT hr) +{ + return general_error_message + WStr(" [hr: ") + shared::ToWSTRING(Hex(hr)) + WStr("]"); +} + const WSTRING invalid_probe_method_already_instrumented = WStr("Dynamic Instrumentation failed to install the probe because the corresponding method is already instrumented by another product."); const WSTRING invalid_method_probe_probe_is_not_supported = @@ -69,7 +74,7 @@ const WSTRING non_supported_compiled_bytecode = const WSTRING type_contains_invalid_symbol = WStr("The type is not supported."); const WSTRING async_method_could_not_load_this = WStr("Instrumentation of async method in a generic class is not yet supported."); -const WSTRING invalid_probe_failed_to_instrument_method_probe = +const WSTRING invalid_probe_failed_to_instrument_method_probe = GetGenericErrorMessageWithErrorCode(1); const WSTRING invalid_probe_failed_to_instrument_line_probe = GetGenericErrorMessageWithErrorCode(2); diff --git a/tracer/src/Datadog.Tracer.Native/debugger_probes_instrumentation_requester.cpp b/tracer/src/Datadog.Tracer.Native/debugger_probes_instrumentation_requester.cpp index 770cb1f085f4..19b7e6e3f33d 100644 --- a/tracer/src/Datadog.Tracer.Native/debugger_probes_instrumentation_requester.cpp +++ b/tracer/src/Datadog.Tracer.Native/debugger_probes_instrumentation_requester.cpp @@ -1038,7 +1038,7 @@ HRESULT DebuggerProbesInstrumentationRequester::NotifyReJITError(ModuleID module { Logger::Info("Marking ", probeId, " as Error."); ProbesMetadataTracker::Instance()->SetErrorProbeStatus(probeId, - invalid_probe_failed_to_instrument_method_probe); + GetGenericErrorMessageWithHr(hrStatus)); } } diff --git a/tracer/test/Datadog.Trace.TestHelpers/MockTracerAgent.cs b/tracer/test/Datadog.Trace.TestHelpers/MockTracerAgent.cs index c6ae3f777de4..cbe051949bca 100644 --- a/tracer/test/Datadog.Trace.TestHelpers/MockTracerAgent.cs +++ b/tracer/test/Datadog.Trace.TestHelpers/MockTracerAgent.cs @@ -135,7 +135,7 @@ public static TcpUdpAgent Create(ITestOutputHelper output, int? port = null, int /// The list of spans. public IImmutableList WaitForSpans( int count, - int timeoutInMilliseconds = int.MaxValue, + int timeoutInMilliseconds = 20000, string operationName = null, DateTimeOffset? minDateTime = null, bool returnAllOperations = false, diff --git a/tracer/test/test-applications/debugger/dependency-libs/Samples.Probes.TestRuns/ExceptionReplay/RethrowTest.cs b/tracer/test/test-applications/debugger/dependency-libs/Samples.Probes.TestRuns/ExceptionReplay/RethrowTest.cs index 1a13d3885e0a..a3bc6202a9f0 100644 --- a/tracer/test/test-applications/debugger/dependency-libs/Samples.Probes.TestRuns/ExceptionReplay/RethrowTest.cs +++ b/tracer/test/test-applications/debugger/dependency-libs/Samples.Probes.TestRuns/ExceptionReplay/RethrowTest.cs @@ -12,8 +12,12 @@ namespace Samples.Probes.TestRuns.ExceptionReplay [ExceptionReplayTestData(expectedNumberOfSnapshotsDefault: 4, expectedNumberOfSnaphotsFull: 4)] internal class RethrowTest : IAsyncRun { + private string _tempMethodName; + public async Task RunAsync() { + var methodName = nameof(RunAsync); + await Task.Yield(); try @@ -37,10 +41,13 @@ public async Task RunAsync() await Task.Yield(); } + _tempMethodName = methodName; } public async Task RunAsync2() { + var methodName = nameof(RunAsync2); + await Task.Yield(); try @@ -65,10 +72,13 @@ public async Task RunAsync2() await Task.Yield(); } + _tempMethodName = methodName; } public async Task InTheMiddle() { + var methodName = nameof(InTheMiddle); + int num = 3; try { @@ -104,10 +114,14 @@ public async Task InTheMiddle() { throw new NotImplementedException("Outer", ex); } + + _tempMethodName = methodName; } public async Task InTheMiddle2() { + var methodName = nameof(InTheMiddle2); + try { await Task.Yield(); @@ -118,6 +132,8 @@ public async Task InTheMiddle2() { throw new NotImplementedException("Outer", inner: e); } + + _tempMethodName = methodName; } public class RelationalDataReader : IAsyncDisposable, IDisposable @@ -171,12 +187,16 @@ protected virtual async ValueTask DisposeAsyncCore() private async ValueTask AwaitUsingFunc() { + var methodName = nameof(AwaitUsingFunc); await Task.Yield(); + _tempMethodName = methodName; return new RelationalDataReader(); } private async Task Foo() { + var methodName = nameof(Foo); + await Task.Yield(); try @@ -188,22 +208,76 @@ private async Task Foo() { await Task.Yield(); } + + _tempMethodName = methodName; } void CaptureAndThrow() { + var methodName = nameof(CaptureAndThrow); + try { - Bar(); + RecursiveThrow(); } catch (Exception e) { System.Runtime.ExceptionServices.ExceptionDispatchInfo.Capture(e).Throw(); } - } + _tempMethodName = methodName; + } + + void RecursiveThrow(int depth = 5) + { + var methodName = nameof(RecursiveThrow); + + try + { + if (depth > 0) + { + RecursiveThrow(depth - 1); + } + else + { + RecursiveCaptureAndThrow(); + } + } + catch (Exception e) + { + throw; + } + + _tempMethodName = methodName; + } + + void RecursiveCaptureAndThrow(int depth = 5) + { + var methodName = nameof(RecursiveCaptureAndThrow); + + try + { + if (depth > 0) + { + RecursiveCaptureAndThrow(depth - 1); + } + else + { + Bar(); + } + } + catch (Exception e) + { + System.Runtime.ExceptionServices.ExceptionDispatchInfo.Capture(e).Throw(); + } + + _tempMethodName = methodName; + } + private void Bar() { + var methodName = nameof(Bar); + try { throw new NotImplementedException(); @@ -213,6 +287,8 @@ private void Bar() Console.WriteLine(nameof(RunAsync)); throw; } + + _tempMethodName = methodName; } } }