Skip to content

Commit 2b89178

Browse files
authored
[Exception Replay][Backport] Normalized exception hashing for more fine-grained aggregation (#5872 -> v2) (#5890)
## Summary of changes In Exception Replay, the exception could be in one of several phases. Two of these phases are `Done` and `Invalidated`. - `Done`: The exception has already been captured and is waiting for the next epoch to be wakened up. - `Invalidated`: None of the frames could be captured, reporting tags that tend to assist in understanding the reasoning for troubleshooting. To be able to capture those phases as quick as possible, a cache is used for a lookup before performing intensive calculation on the `System.Exception` object itself, since the execution path is hot and run as part of unwinding the request with an exception. The lookup key used to be simply **Fnv1a** hashing of `exception.ToString()` of the exception reaching the service root span. Basing the hashing on `exception.ToString()` lead to scenarios where two identical exceptions seemed different based on one of various factors; non-deterministic participating frame, exception messages, PDB info (file path + line number), etc. To be able to determine quickly in which phase the exception is in without performing costly computations, a new way of hashing is required that should cleanse the exceptions in a way that two _similar_ exception should fall into the same case, even though their stack traces are not identical. Also, the new algorithm should be as performant as possible with as little temporal allocations as possible - they play part every time a service root span is finalized with an exception. ## Reason for change Improve the experience of Exception Replay where we failed to report an exception due to failure in determining if the exception is in `Done` / `Invalidated` phases, as a result of it's previous occurrence looking a bit different. ## Implementation details A new class, `ExceptionNormalizer`, has been added that takes as input the string representation of the exception alongside it's outermost exception type, and one level deep of inner exception. It cleanses the exception from the aforementioned attributes, and performs a more fine-grained hash that shall have a better distribution based on the actual exception, leaving out all the non-relevant bits that might differ. ## Test coverage [ExceptionNormalizerTests](https://github.com/DataDog/dd-trace-dotnet/blob/821e4860632a8fcb258bbbe74506249cb6865659/tracer/test/Datadog.Trace.Debugger.IntegrationTests/ExceptionNormalizerTests.cs) with approvals on the hash + string representing the cleansed stack trace. ## Other details Fixes #DEBUG-2674
1 parent 47f3013 commit 2b89178

File tree

42 files changed

+540
-60
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+540
-60
lines changed

tracer/src/Datadog.Trace/Debugger/ExceptionAutoInstrumentation/CachedDoneExceptions.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,17 +20,17 @@ internal static class CachedDoneExceptions
2020
{
2121
private static readonly CachedItems _cachedDoneExceptions = new CachedItems();
2222

23-
internal static void Add(string item)
23+
internal static void Add(int item)
2424
{
2525
_cachedDoneExceptions.Add(item);
2626
}
2727

28-
internal static bool Remove(string item)
28+
internal static bool Remove(int item)
2929
{
3030
return _cachedDoneExceptions.Remove(item);
3131
}
3232

33-
internal static bool Contains(string item)
33+
internal static bool Contains(int item)
3434
{
3535
return _cachedDoneExceptions.Contains(item);
3636
}

tracer/src/Datadog.Trace/Debugger/ExceptionAutoInstrumentation/CachedItems.cs

Lines changed: 14 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -18,48 +18,46 @@ namespace Datadog.Trace.Debugger.ExceptionAutoInstrumentation
1818
{
1919
internal class CachedItems
2020
{
21-
private readonly HashSet<int> cache = new();
22-
private readonly ReaderWriterLockSlim cacheLocker = new();
21+
private readonly HashSet<int> _cache = new();
22+
private readonly ReaderWriterLockSlim _cacheLocker = new();
2323

24-
internal void Add(string item)
24+
internal void Add(int item)
2525
{
26-
cacheLocker.EnterWriteLock();
26+
_cacheLocker.EnterWriteLock();
2727
try
2828
{
29-
cache.Add(Hash(item));
29+
_cache.Add(item);
3030
}
3131
finally
3232
{
33-
cacheLocker.ExitWriteLock();
33+
_cacheLocker.ExitWriteLock();
3434
}
3535
}
3636

37-
internal bool Remove(string item)
37+
internal bool Remove(int item)
3838
{
39-
cacheLocker.EnterWriteLock();
39+
_cacheLocker.EnterWriteLock();
4040
try
4141
{
42-
return cache.Remove(Hash(item));
42+
return _cache.Remove(item);
4343
}
4444
finally
4545
{
46-
cacheLocker.ExitWriteLock();
46+
_cacheLocker.ExitWriteLock();
4747
}
4848
}
4949

50-
internal bool Contains(string item)
50+
internal bool Contains(int item)
5151
{
52-
cacheLocker.EnterReadLock();
52+
_cacheLocker.EnterReadLock();
5353
try
5454
{
55-
return cache.Contains(Hash(item));
55+
return _cache.Contains(item);
5656
}
5757
finally
5858
{
59-
cacheLocker.ExitReadLock();
59+
_cacheLocker.ExitReadLock();
6060
}
6161
}
62-
63-
private int Hash(string item) => Fnv1aHash.GetFNVHashCode(StringEncoding.UTF8.GetBytes(item));
6462
}
6563
}
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
// <copyright file="ExceptionNormalizer.cs" company="Datadog">
2+
// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License.
3+
// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc.
4+
// </copyright>
5+
6+
using System;
7+
using System.Numerics;
8+
using System.Runtime.CompilerServices;
9+
using System.Text;
10+
using Datadog.Trace.VendoredMicrosoftCode.System;
11+
using Fnv1aHash = Datadog.Trace.VendoredMicrosoftCode.System.Reflection.Internal.Hash;
12+
using MemoryExtensions = Datadog.Trace.Debugger.Helpers.MemoryExtensions;
13+
14+
#nullable enable
15+
namespace Datadog.Trace.Debugger.ExceptionAutoInstrumentation
16+
{
17+
internal class ExceptionNormalizer
18+
{
19+
/// <summary>
20+
/// Given the string representation of an exception alongside it's FQN of the outer and (potential) inner exception,
21+
/// this function cleanse the stack trace from error messages, customized information attached to the exception and PDB line info if present.
22+
/// It returns a hash representing the resulting cleansed exception and inner exceptions.
23+
/// Used to aggregate same/similar exceptions that only differ by non-relevant bits.
24+
/// </summary>
25+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
26+
internal int NormalizeAndHashException(string exceptionString, string outerExceptionType, string? innerExceptionType)
27+
{
28+
if (string.IsNullOrEmpty(exceptionString))
29+
{
30+
throw new ArgumentException(@"Exception string cannot be null or empty", nameof(exceptionString));
31+
}
32+
33+
var fnvHashCode = HashLine(VendoredMicrosoftCode.System.MemoryExtensions.AsSpan(outerExceptionType), Fnv1aHash.FnvOffsetBias);
34+
35+
if (innerExceptionType != null)
36+
{
37+
fnvHashCode = HashLine(VendoredMicrosoftCode.System.MemoryExtensions.AsSpan(innerExceptionType), fnvHashCode);
38+
}
39+
40+
var exceptionSpan = VendoredMicrosoftCode.System.MemoryExtensions.AsSpan(exceptionString);
41+
var inSpan = VendoredMicrosoftCode.System.MemoryExtensions.AsSpan(" in ");
42+
var atSpan = VendoredMicrosoftCode.System.MemoryExtensions.AsSpan("at ");
43+
var lambdaSpan = VendoredMicrosoftCode.System.MemoryExtensions.AsSpan("lambda_");
44+
var microsoftSpan = VendoredMicrosoftCode.System.MemoryExtensions.AsSpan("at Microsoft.");
45+
var systemSpan = VendoredMicrosoftCode.System.MemoryExtensions.AsSpan("at System.");
46+
var datadogSpan = VendoredMicrosoftCode.System.MemoryExtensions.AsSpan("at Datadog.");
47+
48+
while (!exceptionSpan.IsEmpty)
49+
{
50+
var lineEndIndex = exceptionSpan.IndexOfAny('\r', '\n');
51+
VendoredMicrosoftCode.System.ReadOnlySpan<char> line;
52+
53+
if (lineEndIndex >= 0)
54+
{
55+
line = exceptionSpan.Slice(0, lineEndIndex);
56+
exceptionSpan = exceptionSpan.Slice(lineEndIndex + 1);
57+
if (!exceptionSpan.IsEmpty && exceptionSpan[0] == '\n')
58+
{
59+
exceptionSpan = exceptionSpan.Slice(1);
60+
}
61+
}
62+
else
63+
{
64+
line = exceptionSpan;
65+
exceptionSpan = default;
66+
}
67+
68+
// Is frame line (starts with `in `).
69+
if (VendoredMicrosoftCode.System.MemoryExtensions.StartsWith(line.TrimStart(), atSpan, StringComparison.Ordinal))
70+
{
71+
var index = VendoredMicrosoftCode.System.MemoryExtensions.IndexOf(line, inSpan, StringComparison.Ordinal);
72+
line = index > 0 ? line.Slice(0, index) : line;
73+
74+
if (VendoredMicrosoftCode.System.MemoryExtensions.Contains(line, lambdaSpan, StringComparison.Ordinal) ||
75+
VendoredMicrosoftCode.System.MemoryExtensions.Contains(line, microsoftSpan, StringComparison.Ordinal) ||
76+
VendoredMicrosoftCode.System.MemoryExtensions.Contains(line, datadogSpan, StringComparison.Ordinal) ||
77+
VendoredMicrosoftCode.System.MemoryExtensions.Contains(line, systemSpan, StringComparison.Ordinal))
78+
{
79+
continue;
80+
}
81+
82+
fnvHashCode = HashLine(line, fnvHashCode);
83+
}
84+
}
85+
86+
return fnvHashCode;
87+
}
88+
89+
protected virtual int HashLine(VendoredMicrosoftCode.System.ReadOnlySpan<char> line, int fnvHashCode)
90+
{
91+
for (var i = 0; i < line.Length; i++)
92+
{
93+
fnvHashCode = Fnv1aHash.Combine((uint)line[i], fnvHashCode);
94+
}
95+
96+
return fnvHashCode;
97+
}
98+
}
99+
}

tracer/src/Datadog.Trace/Debugger/ExceptionAutoInstrumentation/ExceptionReplayDiagnosticTagNames.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,16 +18,17 @@ namespace Datadog.Trace.Debugger.ExceptionAutoInstrumentation
1818
internal static class ExceptionReplayDiagnosticTagNames
1919
{
2020
public const string Eligible = nameof(Eligible);
21-
public const string EmptyShadowStack = nameof(EmptyShadowStack);
21+
public const string NotEligible = nameof(NotEligible);
2222
public const string ExceptionTrackManagerNotInitialized = nameof(ExceptionTrackManagerNotInitialized);
2323
public const string NotRootSpan = nameof(NotRootSpan);
2424
public const string ExceptionObjectIsNull = nameof(ExceptionObjectIsNull);
2525
public const string NonSupportedExceptionType = nameof(NonSupportedExceptionType);
2626
public const string CachedDoneExceptionCase = nameof(CachedDoneExceptionCase);
27+
public const string CachedInvalidatedExceptionCase = nameof(CachedInvalidatedExceptionCase);
2728
public const string InvalidatedExceptionCase = nameof(InvalidatedExceptionCase);
2829
public const string CircuitBreakerIsOpen = nameof(CircuitBreakerIsOpen);
2930
public const string NonCachedDoneExceptionCase = nameof(NonCachedDoneExceptionCase);
30-
public const string NotSupportedExceptionType = nameof(NotSupportedExceptionType);
31+
public const string NoCustomerFrames = nameof(NoCustomerFrames);
3132
public const string NoFramesToInstrument = nameof(NoFramesToInstrument);
3233
public const string EmptyCallStackTreeWhileCollecting = nameof(EmptyCallStackTreeWhileCollecting);
3334
public const string InvalidatedCase = nameof(InvalidatedCase);

0 commit comments

Comments
 (0)