From 9d84a8df5e076e2cc4f7268f6908580ed6658862 Mon Sep 17 00:00:00 2001 From: Dudi Keleti Date: Thu, 13 Feb 2025 18:27:58 +0100 Subject: [PATCH] [Dynamic Instrumentation] DEBUG-3132 Add Memoization Cache for Redaction Logic (#6292) ## Summary of changes This PR introduces caching functionality to optimize redaction decisions by implementing memoization using our Adaptive Cache system. ## Reason for change Redaction decision-making can be computationally expensive. --- .../ExceptionDebugging.cs | 2 +- .../ProbeExpressionParser.Collection.cs | 2 +- .../ProbeExpressionParser.General.cs | 4 +- .../Datadog.Trace/Debugger/LiveDebugger.cs | 2 +- .../Snapshots/DebuggerSnapshotSerializer.cs | 2 +- .../Debugger/Snapshots/Redaction.cs | 255 ++++++++++-------- .../Debugger/RedactionTests.cs | 52 ++-- 7 files changed, 181 insertions(+), 138 deletions(-) diff --git a/tracer/src/Datadog.Trace/Debugger/ExceptionAutoInstrumentation/ExceptionDebugging.cs b/tracer/src/Datadog.Trace/Debugger/ExceptionAutoInstrumentation/ExceptionDebugging.cs index af603c57eb08..908ece85c332 100644 --- a/tracer/src/Datadog.Trace/Debugger/ExceptionAutoInstrumentation/ExceptionDebugging.cs +++ b/tracer/src/Datadog.Trace/Debugger/ExceptionAutoInstrumentation/ExceptionDebugging.cs @@ -65,7 +65,7 @@ private static void InitSnapshotsSink() // Set configs relevant for DI and Exception Debugging, using DI's environment keys. DebuggerSnapshotSerializer.SetConfig(debuggerSettings); - Redaction.SetConfig(debuggerSettings.RedactedIdentifiers, debuggerSettings.RedactedExcludedIdentifiers, debuggerSettings.RedactedTypes); + Redaction.Instance.SetConfig(debuggerSettings.RedactedIdentifiers, debuggerSettings.RedactedExcludedIdentifiers, debuggerSettings.RedactedTypes); // Set up the snapshots sink. var snapshotSlicer = SnapshotSlicer.Create(debuggerSettings); diff --git a/tracer/src/Datadog.Trace/Debugger/Expressions/ProbeExpressionParser.Collection.cs b/tracer/src/Datadog.Trace/Debugger/Expressions/ProbeExpressionParser.Collection.cs index 990084fdc617..d5970eac2ff8 100644 --- a/tracer/src/Datadog.Trace/Debugger/Expressions/ProbeExpressionParser.Collection.cs +++ b/tracer/src/Datadog.Trace/Debugger/Expressions/ProbeExpressionParser.Collection.cs @@ -109,7 +109,7 @@ private Expression GetItemAtIndex(JsonTextReader reader, List var constantValue = constant.Value?.ToString(); - if (Redaction.ShouldRedact(constantValue, constant.Type, out _)) + if (Redaction.Instance.ShouldRedact(constantValue, constant.Type, out _)) { AddError(reader.Value?.ToString() ?? "N/A", "The property or field is redacted."); return RedactedValue(); @@ -242,7 +242,7 @@ private Expression MemberPathExpression(Expression expression, ConstantExpressio try { - if (Redaction.ShouldRedact(propertyOrFieldValue, propertyOrField.Type, out _)) + if (Redaction.Instance.ShouldRedact(propertyOrFieldValue, propertyOrField.Type, out _)) { AddError($"{expression}.{propertyOrFieldValue}", "The property or field is redacted."); return RedactedValue(); diff --git a/tracer/src/Datadog.Trace/Debugger/LiveDebugger.cs b/tracer/src/Datadog.Trace/Debugger/LiveDebugger.cs index e007eac55120..018da891f15c 100644 --- a/tracer/src/Datadog.Trace/Debugger/LiveDebugger.cs +++ b/tracer/src/Datadog.Trace/Debugger/LiveDebugger.cs @@ -142,7 +142,7 @@ public async Task InitializeAsync() _subscriptionManager.SubscribeToChanges(_subscription); DebuggerSnapshotSerializer.SetConfig(Settings); - Redaction.SetConfig(Settings.RedactedIdentifiers, Settings.RedactedExcludedIdentifiers, Settings.RedactedTypes); + Redaction.Instance.SetConfig(Settings.RedactedIdentifiers, Settings.RedactedExcludedIdentifiers, Settings.RedactedTypes); AppDomain.CurrentDomain.AssemblyLoad += (sender, args) => CheckUnboundProbes(); await StartAsync().ConfigureAwait(false); diff --git a/tracer/src/Datadog.Trace/Debugger/Snapshots/DebuggerSnapshotSerializer.cs b/tracer/src/Datadog.Trace/Debugger/Snapshots/DebuggerSnapshotSerializer.cs index 2b6cde4bbb22..65fb59a5cd94 100644 --- a/tracer/src/Datadog.Trace/Debugger/Snapshots/DebuggerSnapshotSerializer.cs +++ b/tracer/src/Datadog.Trace/Debugger/Snapshots/DebuggerSnapshotSerializer.cs @@ -61,7 +61,7 @@ private static bool SerializeInternal( { try { - if (Redaction.ShouldRedact(variableName, type, out var redactionReason)) + if (Redaction.Instance.ShouldRedact(variableName, type, out var redactionReason)) { if (variableName != null) { diff --git a/tracer/src/Datadog.Trace/Debugger/Snapshots/Redaction.cs b/tracer/src/Datadog.Trace/Debugger/Snapshots/Redaction.cs index 9fc21a3240f6..c9538d7e7a2a 100644 --- a/tracer/src/Datadog.Trace/Debugger/Snapshots/Redaction.cs +++ b/tracer/src/Datadog.Trace/Debugger/Snapshots/Redaction.cs @@ -12,6 +12,7 @@ using System.Runtime.CompilerServices; using System.Security; using System.Text; +using Datadog.Trace.Debugger.Caching; using Datadog.Trace.Debugger.Configurations; using Datadog.Trace.Logging; using TypeExtensions = Datadog.Trace.Debugger.Helpers.TypeExtensions; @@ -27,9 +28,10 @@ internal enum RedactionReason Type } - internal static class Redaction + internal class Redaction { private const int MaxStackAlloc = 512; + private static readonly IDatadogLogger Log = DatadogLogging.GetLoggerFor(typeof(Redaction)); private static readonly Type[] AllowedCollectionTypes = @@ -62,6 +64,11 @@ internal static class Redaction private static readonly string[] AllowedSpecialCasedCollectionTypeNames = []; // "RangeIterator" + private static readonly Type[] DeniedTypes = + [ + typeof(SecureString) + ]; + internal static readonly Type[] AllowedTypesSafeToCallToString = [ typeof(DateTime), @@ -74,103 +81,115 @@ internal static class Redaction typeof(StringBuilder) ]; - private static readonly Type[] DeniedTypes = - [ - typeof(SecureString) - ]; + private static Redaction _instnace = new(); - private static readonly Trie TypeTrie = new(); + private readonly Trie _typeTrie; - private static readonly HashSet RedactedTypes = []; + private readonly HashSet _redactedTypes; - private static readonly HashSet RedactKeywords = - [ - "2fa", - "accesstoken", - "aiohttpsession", - "apikey", - "appkey", - "apisecret", - "apisignature", - "applicationkey", - "auth", - "authorization", - "authtoken", - "ccnumber", - "certificatepin", - "cipher", - "clientid", - "clientsecret", - "connectionstring", - "connectsid", - "cookie", - "credentials", - "creditcard", - "csrf", - "csrftoken", - "cvv", - "databaseurl", - "dburl", - "encryptionkey", - "encryptionkeyid", - "geolocation", - "gpgkey", - "ipaddress", - "jti", - "jwt", - "licensekey", - "masterkey", - "mysqlpwd", - "nonce", - "oauth", - "oauthtoken", - "otp", - "passhash", - "passwd", - "password", - "passwordb", - "pemfile", - "pgpkey", - "phpsessid", - "pin", - "pincode", - "pkcs8", - "privatekey", - "publickey", - "pwd", - "recaptchakey", - "refreshtoken", - "routingnumber", - "salt", - "secret", - "secretkey", - "secrettoken", - "securityanswer", - "securitycode", - "securityquestion", - "serviceaccountcredentials", - "session", - "sessionid", - "sessionkey", - "setcookie", - "signature", - "signaturekey", - "sshkey", - "ssn", - "symfony", - "token", - "transactionid", - "twiliotoken", - "usersession", - "voterid", - "xapikey", - "xauthtoken", - "xcsrftoken", - "xforwardedfor", - "xrealip", - "xsrf", - "xsrftoken" - ]; + private readonly HashSet _redactKeywords; + + private readonly ConcurrentAdaptiveCache _redactedTypesCache; + + private readonly ConcurrentAdaptiveCache _redactedKeywordsCache; + + private Redaction() + { + _typeTrie = new Trie(); + _redactedTypes = []; + _redactKeywords = + [ + "2fa", + "accesstoken", + "aiohttpsession", + "apikey", + "appkey", + "apisecret", + "apisignature", + "applicationkey", + "auth", + "authorization", + "authtoken", + "ccnumber", + "certificatepin", + "cipher", + "clientid", + "clientsecret", + "connectionstring", + "connectsid", + "cookie", + "credentials", + "creditcard", + "csrf", + "csrftoken", + "cvv", + "databaseurl", + "dburl", + "encryptionkey", + "encryptionkeyid", + "geolocation", + "gpgkey", + "ipaddress", + "jti", + "jwt", + "licensekey", + "masterkey", + "mysqlpwd", + "nonce", + "oauth", + "oauthtoken", + "otp", + "passhash", + "passwd", + "password", + "passwordb", + "pemfile", + "pgpkey", + "phpsessid", + "pin", + "pincode", + "pkcs8", + "privatekey", + "publickey", + "pwd", + "recaptchakey", + "refreshtoken", + "routingnumber", + "salt", + "secret", + "secretkey", + "secrettoken", + "securityanswer", + "securitycode", + "securityquestion", + "serviceaccountcredentials", + "session", + "sessionid", + "sessionkey", + "setcookie", + "signature", + "signaturekey", + "sshkey", + "ssn", + "symfony", + "token", + "transactionid", + "twiliotoken", + "usersession", + "voterid", + "xapikey", + "xauthtoken", + "xcsrftoken", + "xforwardedfor", + "xrealip", + "xsrf", + "xsrftoken" + ]; + _redactedTypesCache = new(evictionPolicyKind: EvictionPolicy.Lfu); + _redactedKeywordsCache = new(evictionPolicyKind: EvictionPolicy.Lfu); + } + + internal static Redaction Instance => _instnace; internal static bool IsSafeToCallToString(Type type) { @@ -216,13 +235,18 @@ internal static bool IsSupportedCollection(Type? type) AllowedSpecialCasedCollectionTypeNames.Any(white => white.Equals(type.Name, StringComparison.OrdinalIgnoreCase)); } - internal static bool IsRedactedType(Type? type) + internal bool IsRedactedType(Type? type) { if (type == null) { return false; } + return _redactedTypesCache.GetOrAdd(type, CheckForRedactedType); + } + + private bool CheckForRedactedType(Type type) + { Type? genericDefinition = null; if (type.IsGenericType) { @@ -245,31 +269,36 @@ internal static bool IsRedactedType(Type? type) return false; } - if (RedactedTypes.Contains(typeFullName)) + if (_redactedTypes.Contains(typeFullName)) { return true; } - if (TypeTrie.HasMatchingPrefix(typeFullName)) + if (_typeTrie.HasMatchingPrefix(typeFullName)) { - var stringStartsWith = TypeTrie.GetStringStartingWith(typeFullName); + var stringStartsWith = _typeTrie.GetStringStartingWith(typeFullName); return string.IsNullOrEmpty(stringStartsWith) || stringStartsWith.Length == typeFullName.Length; } return false; } - internal static bool IsRedactedKeyword(string name) + internal bool IsRedactedKeyword(string name) { if (string.IsNullOrEmpty(name)) { return false; } - return !TryNormalize(name, out var result) || RedactKeywords.Contains(result); + return _redactedKeywordsCache.GetOrAdd(name, this.CheckForRedactedKeyword); } - internal static bool ShouldRedact(string name, Type type, out RedactionReason redactionReason) + internal bool CheckForRedactedKeyword(string keyword) + { + return TryNormalize(keyword, out var result) && _redactKeywords.Contains(result); + } + + internal bool ShouldRedact(string name, Type type, out RedactionReason redactionReason) { if (IsRedactedKeyword(name)) { @@ -338,16 +367,16 @@ private static bool IsRemovableChar(char c) return c is '_' or '-' or '$' or '@'; } - internal static void SetConfig(HashSet redactedIdentifiers, HashSet redactedExcludedIdentifiers, HashSet redactedTypes) + internal void SetConfig(HashSet redactedIdentifiers, HashSet redactedExcludedIdentifiers, HashSet redactedTypes) { #if NET6_0_OR_GREATER - RedactKeywords.EnsureCapacity(RedactKeywords.Count + redactedIdentifiers.Count); + _redactKeywords.EnsureCapacity(_redactKeywords.Count + redactedIdentifiers.Count); #endif foreach (var identifier in redactedIdentifiers) { if (TryNormalize(identifier, out var result)) { - RedactKeywords.Add(result); + _redactKeywords.Add(result); } else { @@ -359,7 +388,7 @@ internal static void SetConfig(HashSet redactedIdentifiers, HashSet redactedIdentifiers, HashSet + /// For unit tests only! + /// + internal void ResetInstance() + { + System.Threading.Interlocked.Exchange(ref _instnace, new()); + } } } diff --git a/tracer/test/Datadog.Trace.Tests/Debugger/RedactionTests.cs b/tracer/test/Datadog.Trace.Tests/Debugger/RedactionTests.cs index 26a0a8552f4f..87046fd492a6 100644 --- a/tracer/test/Datadog.Trace.Tests/Debugger/RedactionTests.cs +++ b/tracer/test/Datadog.Trace.Tests/Debugger/RedactionTests.cs @@ -12,7 +12,7 @@ namespace Datadog.Trace.Tests.Debugger { [UsesVerify] - public class RedactionTests + public class RedactionTests : IDisposable { public static IEnumerable GetLongStringTestData() { @@ -62,6 +62,11 @@ public static IEnumerable GetLongRedactedIdentifiers() yield return new string('y', 256) + "-token"; } + public void Dispose() + { + Redaction.Instance.ResetInstance(); + } + [Theory] [InlineData(null, false)] [InlineData("", false)] @@ -83,7 +88,7 @@ public static IEnumerable GetLongRedactedIdentifiers() [InlineData("!Password", false)] public void RedactedKeywordsTest(string keyword, bool shouldYield) { - Assert.Equal(shouldYield, Redaction.IsRedactedKeyword(keyword)); + Assert.Equal(shouldYield, Redaction.Instance.IsRedactedKeyword(keyword)); } [Theory] @@ -95,7 +100,7 @@ public void RedactedKeywordsTest(string keyword, bool shouldYield) [InlineData("x_key", false)] public void ShouldRedactKeywordsTest(string keyword, bool shouldRedacted) { - Assert.Equal(shouldRedacted, Redaction.ShouldRedact(keyword, typeof(string), out _)); + Assert.Equal(shouldRedacted, Redaction.Instance.ShouldRedact(keyword, typeof(string), out _)); } [Theory] @@ -113,10 +118,10 @@ public void ShouldRedactKeywordsTest(string keyword, bool shouldRedacted) public void RedactedKeywords_WithExclusions_Test(string keyword, string[] excludedKeywords, bool shouldRedact) { // Arrange - Redaction.SetConfig(["password", "x-api-key"], [.. excludedKeywords], new HashSet()); + Redaction.Instance.SetConfig(["password", "x-api-key"], [.. excludedKeywords], new HashSet()); // Act - var isRedacted = Redaction.IsRedactedKeyword(keyword); + var isRedacted = Redaction.Instance.IsRedactedKeyword(keyword); // Assert Assert.Equal(shouldRedact, isRedacted); @@ -128,10 +133,10 @@ public void RedactedKeywords_LongStrings_Test(string keyword, string[] excludedK { // Arrange var redactedIdentifiers = new HashSet(GetLongRedactedIdentifiers()); - Redaction.SetConfig(redactedIdentifiers, [.. excludedKeywords], new HashSet()); + Redaction.Instance.SetConfig(redactedIdentifiers, [.. excludedKeywords], new HashSet()); // Act - var isRedacted = Redaction.IsRedactedKeyword(keyword); + var isRedacted = Redaction.Instance.IsRedactedKeyword(keyword); // Assert Assert.Equal(shouldRedact, isRedacted); @@ -143,10 +148,10 @@ public void RedactedKeywords_SpecialChars_Test(string keyword, string[] excluded { // Arrange var redactedIdentifiers = new HashSet { "test@keyword", "$special-key", "api@token" }; - Redaction.SetConfig(redactedIdentifiers, [.. excludedKeywords], new HashSet()); + Redaction.Instance.SetConfig(redactedIdentifiers, [.. excludedKeywords], new HashSet()); // Act - var isRedacted = Redaction.IsRedactedKeyword(keyword); + var isRedacted = Redaction.Instance.IsRedactedKeyword(keyword); // Assert Assert.Equal(shouldRedact, isRedacted); @@ -158,33 +163,33 @@ public void RedactedKeywords_SpecialChars_Test(string keyword, string[] excluded [InlineData(null, false)] public void IsRedactedType_BasicTypes_Test(Type type, bool expected) { - Assert.Equal(expected, Redaction.IsRedactedType(type)); + Assert.Equal(expected, Redaction.Instance.IsRedactedType(type)); } [Fact] public void IsRedactedType_WithConfiguredTypes_Test() { // Arrange - Redaction.SetConfig( + Redaction.Instance.SetConfig( new HashSet(), new HashSet(), new HashSet { "System.Security.SecureString", "Namespace.Sensitive*" }); // Act & Assert - Assert.True(Redaction.IsRedactedType(typeof(System.Security.SecureString))); + Assert.True(Redaction.Instance.IsRedactedType(typeof(System.Security.SecureString))); } [Fact] public void IsRedactedType_WithWildcardMatch_Test() { // Arrange - Redaction.SetConfig( + Redaction.Instance.SetConfig( new HashSet(), new HashSet(), new HashSet { "System.Security.*" }); // Act & Assert - Assert.True(Redaction.IsRedactedType(typeof(System.Security.SecureString))); + Assert.True(Redaction.Instance.IsRedactedType(typeof(System.Security.SecureString))); } [Fact] @@ -194,7 +199,7 @@ public void SetConfig_EmptyConfigurations_Test() var emptySet = new HashSet(); // Act & Assert (should not throw) - Redaction.SetConfig(emptySet, emptySet, emptySet); + Redaction.Instance.SetConfig(emptySet, emptySet, emptySet); } [Fact] @@ -205,29 +210,30 @@ public void SetConfig_DuplicateEntries_Test() var excludedIds = new HashSet { "good-password", "GOOD-PASSWORD" }; // Act (should not throw) - Redaction.SetConfig(redactedIds, excludedIds, new HashSet()); + Redaction.Instance.SetConfig(redactedIds, excludedIds, new HashSet()); // Assert (all normalized versions should be treated the same) - Assert.True(Redaction.IsRedactedKeyword("password")); - Assert.True(Redaction.IsRedactedKeyword("PASSWORD")); - Assert.False(Redaction.IsRedactedKeyword("good-password")); - Assert.False(Redaction.IsRedactedKeyword("GOOD-PASSWORD")); + Assert.True(Redaction.Instance.IsRedactedKeyword("password")); + Assert.True(Redaction.Instance.IsRedactedKeyword("PASSWORD")); + Assert.False(Redaction.Instance.IsRedactedKeyword("good-password")); + Assert.False(Redaction.Instance.IsRedactedKeyword("GOOD-PASSWORD")); } [Theory] - [InlineData("password", typeof(System.Security.SecureString), RedactionReason.Type)] + [InlineData("password", typeof(System.Security.SecureString), RedactionReason.Identifier)] + [InlineData("no-password", typeof(System.Security.SecureString), RedactionReason.Type)] [InlineData("api_key", typeof(string), RedactionReason.Identifier)] [InlineData("normal", typeof(string), RedactionReason.None)] internal void ShouldRedact_CombinedScenarios_Test(string name, Type type, RedactionReason expectedReason) { // Arrange - Redaction.SetConfig( + Redaction.Instance.SetConfig( new HashSet { "api_key" }, new HashSet(), new HashSet { "System.Security.SecureString" }); // Act - bool result = Redaction.ShouldRedact(name, type, out var reason); + bool result = Redaction.Instance.ShouldRedact(name, type, out var reason); // Assert Assert.Equal(expectedReason, reason);