diff --git a/tracer/src/Datadog.Trace.Trimming/build/Datadog.Trace.Trimming.xml b/tracer/src/Datadog.Trace.Trimming/build/Datadog.Trace.Trimming.xml
index f84d7c74c99f..6ef9b033ae30 100644
--- a/tracer/src/Datadog.Trace.Trimming/build/Datadog.Trace.Trimming.xml
+++ b/tracer/src/Datadog.Trace.Trimming/build/Datadog.Trace.Trimming.xml
@@ -870,6 +870,7 @@
+
diff --git a/tracer/src/Datadog.Trace/Debugger/Expressions/ProbeExpressionParser.Dump.cs b/tracer/src/Datadog.Trace/Debugger/Expressions/ProbeExpressionParser.Dump.cs
index b22a234ad178..f1f243a075fa 100644
--- a/tracer/src/Datadog.Trace/Debugger/Expressions/ProbeExpressionParser.Dump.cs
+++ b/tracer/src/Datadog.Trace/Debugger/Expressions/ProbeExpressionParser.Dump.cs
@@ -23,6 +23,18 @@ internal partial class ProbeExpressionParser
private const string RedactedValueDump = "{redacted}";
private const string BackingFieldSuffix = ">k__BackingField";
+ // A type in CoreLib is never customer code, and a subclass of a type outside CoreLib is never in CoreLib.
+ private static readonly Assembly CoreLibAssembly = typeof(object).Assembly;
+
+ // CoreLib exceptions whose Message formats values that can be customer objects: ReflectionTypeLoadException reads
+ // each loader exception's Message, and SwitchExpressionException formats the unmatched value. .NET Framework has no
+ // SwitchExpressionException, so it's looked up by name and can be null.
+ private static readonly Type ReflectionTypeLoadExceptionType = typeof(ReflectionTypeLoadException);
+ private static readonly Type SwitchExpressionExceptionType = CoreLibAssembly.GetType("System.Runtime.CompilerServices.SwitchExpressionException");
+
+ // Read as a field, so a Message override never runs.
+ private static readonly FieldInfo ExceptionMessageField = typeof(Exception).GetField("_message", BindingFlags.Instance | BindingFlags.NonPublic);
+
private static FieldInfo[] GetSafeFieldsForDump(FieldInfo[] fields)
{
var writeIndex = 0;
@@ -103,60 +115,202 @@ private static bool IsRedactedRuntimeValue(object value, Type declaredType)
return runtimeType != declaredType && ShouldRedactDumpValue(null, runtimeType);
}
+ // Only called when the declared type is safe to call ToString on. A value type or an exact match is
+ // decided by the declared type. No subclass of a non-sealed allowed type is allowed itself, so any
+ // other runtime type is a subclass whose override must not run.
+ private static bool CanCallToStringOnRuntimeType(object value, Type declaredType)
+ {
+ return value.GetType() == declaredType || declaredType.IsValueType;
+ }
+
+ // Message and StackTrace are virtual, so they are only called on a CoreLib exception whose Message doesn't
+ // format customer values.
+ private static bool CanCallExceptionMembers(Type exceptionType)
+ {
+ return ReferenceEquals(exceptionType.Assembly, CoreLibAssembly) &&
+ !ReferenceEquals(exceptionType, ReflectionTypeLoadExceptionType) &&
+ !ReferenceEquals(exceptionType, SwitchExpressionExceptionType);
+ }
+
+ private static string DumpException(Exception exception, string prefix)
+ {
+ var runtimeType = exception.GetType();
+ return CanCallExceptionMembers(runtimeType)
+ ? $"{prefix}{runtimeType.FullName}, {exception.Message}, {exception.StackTrace}"
+ : DumpExceptionMessageField(exception, prefix);
+ }
+
+ // Shows the runtime type and the message the exception was constructed with, without calling any member.
+ private static string DumpExceptionMessageField(Exception exception, string prefix)
+ {
+ var typeName = exception.GetType().FullName;
+ return ExceptionMessageField?.GetValue(exception) is string message
+ ? $"{prefix}{typeName}, {message}"
+ : $"{prefix}{typeName}";
+ }
+
+ private static Expression IsNullExpression(Expression expression)
+ {
+ // Expression.Equal could bind to a customer equality operator
+ if (!expression.Type.IsValueType)
+ {
+ return Expression.ReferenceEqual(expression, Expression.Constant(null));
+ }
+
+ return Nullable.GetUnderlyingType(expression.Type) is null
+ ? Expression.Constant(false)
+ : Expression.Not(Expression.Property(expression, nameof(Nullable.HasValue)));
+ }
+
private Expression DumpExpression(Expression expression, List scopeMembers)
{
- if (ShouldRedactDumpValue(null, expression.Type))
+ var type = expression.Type;
+ if (ShouldRedactDumpValue(null, type))
{
return Expression.Constant(RedactedValueDump);
}
- if (expression.Type == ProbeExpressionParserHelper.UndefinedValueType)
+ if (type == ProbeExpressionParserHelper.UndefinedValueType)
{
return Expression.Constant(UndefinedValueDump);
}
- if (Datadog.Trace.Debugger.Helpers.TypeExtensions.IsSimple(expression.Type) ||
- Redaction.AllowedTypesSafeToCallToString.Contains(expression.Type))
+ // A sealed or value type is its own runtime type, so its dump is decided here and needs no runtime check.
+ var isExactType = type.IsValueType || type.IsSealed;
+ var getTypeMethod = GetMethodByReflection(typeof(object), nameof(object.GetType), Type.EmptyTypes);
+ var toStringMethod = GetMethodByReflection(typeof(object), nameof(object.ToString), Type.EmptyTypes);
+
+ if (Datadog.Trace.Debugger.Helpers.TypeExtensions.IsSimple(type) ||
+ Redaction.AllowedTypesSafeToCallToString.Contains(type))
{
- return Expression.Call(expression, GetMethodByReflection(typeof(object), nameof(object.ToString), Type.EmptyTypes));
- }
+ if (isExactType)
+ {
+ return Expression.Call(expression, toStringMethod);
+ }
- var ifNull = Expression.Equal(expression, Expression.Constant(null));
+ // No subclass of a non-sealed allowed type is allowed itself, so only an exact match may run ToString.
+ return DumpRuntimeValueExpression(
+ expression,
+ Expression.Constant("null"),
+ value => DumpIfRuntimeTypeAllows(
+ Expression.ReferenceEqual(Expression.Call(value, getTypeMethod), Expression.Constant(type, typeof(Type))),
+ Expression.Call(value, toStringMethod),
+ value,
+ scopeMembers),
+ scopeMembers);
+ }
- if (IsSafeException(expression.Type))
+ if (typeof(Exception).IsAssignableFrom(type))
{
- // for known Exception types we can assume it's safe to call .Message
- // whereas the others might have overriden it in a way that could cause side effects
- var stringConcat = GetMethodByReflection(typeof(string), nameof(string.Concat), new[] { typeof(object[]) });
- var typeNameExpression = Expression.Constant(expression.Type.FullName, typeof(string));
- var exceptionAsString = Expression.Call(
- stringConcat,
- Expression.NewArrayInit(
- typeof(string),
- typeNameExpression,
- Expression.Constant(", "),
- Expression.Property(expression, nameof(Exception.Message)),
- Expression.Constant(", "),
- Expression.Property(expression, nameof(Exception.StackTrace))));
-
- return Expression.Condition(ifNull, typeNameExpression, exceptionAsString);
+ var typeNameExpression = Expression.Constant(type.FullName, typeof(string));
+
+ // Message and StackTrace are virtual, so DumpException calls them only when CanCallExceptionMembers allows it.
+ // A type outside CoreLib has no subclass in CoreLib, so that check only runs for a non-sealed CoreLib type.
+ if (isExactType && CanCallExceptionMembers(type))
+ {
+ var stringConcat = GetMethodByReflection(typeof(string), nameof(string.Concat), new[] { typeof(object[]) });
+ var exceptionAsString = Expression.Call(
+ stringConcat,
+ Expression.NewArrayInit(
+ typeof(string),
+ typeNameExpression,
+ Expression.Constant(", "),
+ Expression.Property(expression, nameof(Exception.Message)),
+ Expression.Constant(", "),
+ Expression.Property(expression, nameof(Exception.StackTrace))));
+
+ return Expression.Condition(IsNullExpression(expression), typeNameExpression, exceptionAsString);
+ }
+
+ var dumpExceptionMethod = GetMethodByReflection(
+ typeof(ProbeExpressionParser),
+ !isExactType && ReferenceEquals(type.Assembly, CoreLibAssembly) ? nameof(DumpException) : nameof(DumpExceptionMessageField),
+ [typeof(Exception), typeof(string)]);
+ Func dumpException = exception => Expression.Call(
+ dumpExceptionMethod,
+ Expression.Convert(exception, typeof(Exception)),
+ Expression.Constant(null, typeof(string)));
+
+ return isExactType
+ ? Expression.Condition(IsNullExpression(expression), typeNameExpression, dumpException(expression))
+ : DumpRuntimeValueExpression(expression, typeNameExpression, dumpException, scopeMembers);
}
- Expression dumpExpression;
- if (IsDictionaryDumpType(expression.Type))
+ if (IsDictionaryDumpType(type))
{
- dumpExpression = DumpDictionaryExpression(expression, scopeMembers);
+ return Expression.Condition(IsNullExpression(expression), Expression.Constant("null"), DumpDictionaryExpression(expression, scopeMembers));
}
- else if (IsSafeCollection(expression.Type))
+
+ if (type.IsArray)
{
- dumpExpression = DumpCollectionExpression(expression, scopeMembers);
+ // An array's runtime type is always an array, whose enumerator can't be customer code.
+ return Expression.Condition(IsNullExpression(expression), Expression.Constant("null"), DumpCollectionExpression(expression, scopeMembers));
}
- else
+
+ if (IsSafeCollection(type))
{
- dumpExpression = DumpFieldsExpression(expression, scopeMembers);
+ // Enumerate only the collection types snapshot capture supports, so a customer enumerator never runs.
+ if (isExactType)
+ {
+ return Expression.Condition(
+ IsNullExpression(expression),
+ Expression.Constant("null"),
+ Redaction.IsSupportedCollection(type) ? DumpCollectionExpression(expression, scopeMembers) : DumpFieldsExpression(expression, scopeMembers));
+ }
+
+ // Every type on the collection allowlist other than arrays, which are handled above, derives directly from
+ // object, so a declared class can only hold a supported runtime type by holding exactly itself. Only an
+ // interface needs the runtime allowlist.
+ if (!type.IsInterface && !Redaction.IsSupportedCollection(type))
+ {
+ return DumpRuntimeValueExpression(expression, Expression.Constant("null"), value => DumpFieldsExpression(value, scopeMembers), scopeMembers);
+ }
+
+ return DumpRuntimeValueExpression(
+ expression,
+ Expression.Constant("null"),
+ value => DumpIfRuntimeTypeAllows(
+ type.IsInterface
+ ? Expression.Call(GetMethodByReflection(typeof(Redaction), nameof(Redaction.IsSupportedCollection), [typeof(object)]), value)
+ : Expression.ReferenceEqual(Expression.Call(value, getTypeMethod), Expression.Constant(type, typeof(Type))),
+ DumpCollectionExpression(Expression.Convert(value, type), scopeMembers),
+ value,
+ scopeMembers),
+ scopeMembers);
}
- return Expression.Condition(ifNull, Expression.Constant("null"), dumpExpression);
+ return isExactType
+ ? Expression.Condition(IsNullExpression(expression), Expression.Constant("null"), DumpFieldsExpression(expression, scopeMembers))
+ : DumpRuntimeValueExpression(expression, Expression.Constant("null"), value => DumpFieldsExpression(value, scopeMembers), scopeMembers);
+ }
+
+ // Reads the value once and redacts it when its runtime type, which a non-sealed declared type can't tell, is
+ // redacted. IsRedactedRuntimeValue skips the lookup when the runtime type is the declared type.
+ private BlockExpression DumpRuntimeValueExpression(
+ Expression expression,
+ Expression nullDump,
+ Func dumpValue,
+ List scopeMembers)
+ {
+ var value = Expression.Variable(typeof(object), "dumpValue");
+ scopeMembers.Add(value);
+ var isRedacted = Expression.Call(
+ GetMethodByReflection(typeof(ProbeExpressionParser), nameof(IsRedactedRuntimeValue), [typeof(object), typeof(Type)]),
+ value,
+ Expression.Constant(expression.Type, typeof(Type)));
+
+ return Expression.Block(
+ Expression.Assign(value, Expression.Convert(expression, typeof(object))),
+ Expression.Condition(
+ Expression.ReferenceEqual(value, Expression.Constant(null)),
+ nullDump,
+ Expression.Condition(isRedacted, Expression.Constant(RedactedValueDump), dumpValue(value))));
+ }
+
+ // Any runtime type that isAllowed rejects gets its fields dumped, the same output a root local of that type gets.
+ private ConditionalExpression DumpIfRuntimeTypeAllows(Expression isAllowed, Expression dumpAllowed, Expression value, List scopeMembers)
+ {
+ return Expression.Condition(isAllowed, dumpAllowed, DumpFieldsExpression(value, scopeMembers));
}
private BlockExpression DumpDictionaryExpression(Expression dictionary, List scopeMembers)
@@ -181,7 +335,7 @@ private BlockExpression DumpDictionaryExpression(Expression dictionary, List), nameof(DumpCollection), [typeof(object), typeof(Type)]),
value,
Expression.Constant(typeof(IDictionary)));
- var dumpFields = DumpFieldsExpression(Expression.Convert(value, dictionary.Type), scopeMembers);
+ var dumpFields = DumpFieldsExpression(value, scopeMembers);
var dump = Expression.Condition(isSupportedDictionary, dumpEntries, dumpFields);
return Expression.Block(
@@ -365,22 +519,20 @@ private string DumpObject(object value, Type type, string name, int depth = 0)
}
// only one level depth of collection
- if (depth == 0 && (IsTypeSupportIndex(type, out var assignableFrom) || IsGenericDictionaryInterface(type)))
+ // The declared type doesn't tell which collection the field holds, so enumerate only the runtime types
+ // snapshot capture supports and a customer or wrapped enumerator never runs. Those allow lists are
+ // cheap, so check them before the declared type, whose check looks up interfaces.
+ if (depth == 0 &&
+ (value is null || Redaction.IsSupportedDictionary(value) || Redaction.IsSupportedCollection(value)) &&
+ (IsTypeSupportIndex(type, out var assignableFrom) || IsGenericDictionaryInterface(type)))
{
- assignableFrom ??= typeof(IDictionary);
-
// The declared type may be an interface. Redact from the runtime type before enumerating.
if (IsRedactedRuntimeValue(value, type))
{
return hasName ? $"{name}={RedactedValueDump}" : RedactedValueDump;
}
- // The declared type doesn't tell which dictionary the field holds. Enumerate only the dictionary
- // types snapshot capture supports, so a customer or wrapped enumerator never runs.
- if (assignableFrom != typeof(IDictionary) || value is null || Redaction.IsSupportedDictionary(value))
- {
- return DumpCollection(value, assignableFrom);
- }
+ return DumpCollection(value, assignableFrom ?? typeof(IDictionary));
}
if (hasName)
@@ -393,15 +545,19 @@ private string DumpObject(object value, Type type, string name, int depth = 0)
return $"{name}{UndefinedValueDump}";
}
- if (IsSafeException(type))
+ if (typeof(Exception).IsAssignableFrom(type))
{
- return value is not Exception ex
- ? $"{name}{type?.FullName}"
- : $"{name}{ex.GetType().FullName}, {ex.Message}, {ex.StackTrace}";
+ if (value is not Exception ex)
+ {
+ return $"{name}{type.FullName}";
+ }
+
+ // The declared type may be a non-sealed exception. Redact from the runtime type before reading the message.
+ return IsRedactedRuntimeValue(value, type) ? $"{name}{RedactedValueDump}" : DumpException(ex, name);
}
- return Redaction.IsSafeToCallToString(type) ?
- $"{name}{value?.ToString() ?? type.FullName}" :
+ return value is not null && Redaction.IsSafeToCallToString(type) && CanCallToStringOnRuntimeType(value, type) ?
+ $"{name}{value.ToString()}" :
$"{name}{type?.FullName}";
}
diff --git a/tracer/src/Datadog.Trace/Debugger/Expressions/ProbeExpressionParser.General.cs b/tracer/src/Datadog.Trace/Debugger/Expressions/ProbeExpressionParser.General.cs
index 7d4197f9c210..2d8b748f3117 100644
--- a/tracer/src/Datadog.Trace/Debugger/Expressions/ProbeExpressionParser.General.cs
+++ b/tracer/src/Datadog.Trace/Debugger/Expressions/ProbeExpressionParser.General.cs
@@ -593,11 +593,6 @@ private ParameterExpression GetParameterExpression(List par
}
}
- private bool IsSafeException(Type type)
- {
- return typeof(Exception).IsAssignableFrom(type) && IsMicrosoftType(type);
- }
-
private bool IsMicrosoftType(Type type)
{
var @namespace = type.Namespace;
diff --git a/tracer/test/Datadog.Trace.Tests/Debugger/DebuggerExpressionLanguageTests.cs b/tracer/test/Datadog.Trace.Tests/Debugger/DebuggerExpressionLanguageTests.cs
index 76af369ef7a3..e39f083f363e 100644
--- a/tracer/test/Datadog.Trace.Tests/Debugger/DebuggerExpressionLanguageTests.cs
+++ b/tracer/test/Datadog.Trace.Tests/Debugger/DebuggerExpressionLanguageTests.cs
@@ -15,6 +15,7 @@
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
+using System.Xml;
using AgileObjects.ReadableExpressions;
using Datadog.Trace.Debugger;
using Datadog.Trace.Debugger.Configurations.Models;
@@ -42,6 +43,8 @@ public class DebuggerExpressionLanguageTests
private const string DefaultJsonTemplate = @"{""Ignore"": ""Ignore""}";
+ private const string CustomerDumpMarker = "DD_CUSTOMER_DUMP_MARKER";
+
private const string ConditionsFolder = "Conditions";
private const string TemplatesFolder = "Templates";
@@ -3459,6 +3462,239 @@ public void ProbeExpressionEvaluator_TemplateDump_DoesNotEnumerateCustomDictiona
result.Errors.Should().BeNullOrEmpty();
}
+ [Theory]
+ [InlineData("_items")]
+ [InlineData("_itemsInterface")]
+ [InlineData("_readOnlyItems")]
+ [InlineData("_headers")]
+ [InlineData("_uri")]
+ public void ProbeExpressionEvaluator_TemplateDump_GetMemberDumpsFieldsOfCustomerRuntimeType(string memberName)
+ {
+ // getmember keeps the declared type, which allows enumeration or ToString, but the runtime
+ // type is a customer subclass whose members throw, so only its fields are dumped.
+ var scopeMembers = CreateScopeMembers();
+ scopeMembers.AddMember(new ScopeMember("DumpHolderLocal", typeof(CustomerRuntimeTypesHolder), new CustomerRuntimeTypesHolder(), ScopeMemberKind.Local));
+
+ var evaluator = CreateEvaluator(
+ templates: [new DebuggerExpression(string.Empty, $@"{{""getmember"":[{{""ref"":""DumpHolderLocal""}},""{memberName}""]}}", null)],
+ condition: null,
+ metric: null,
+ spanDecorations: null,
+ captureExpressions: null);
+
+ var result = evaluator.Evaluate(scopeMembers);
+
+ result.Template.Should().Be("k__BackingField=1");
+ result.Errors.Should().BeNullOrEmpty();
+ }
+
+ [Fact]
+ public void ProbeExpressionEvaluator_TemplateDump_FieldsOfCustomerRuntimeTypeDumpDeclaredTypeName()
+ {
+ var scopeMembers = CreateScopeMembers();
+ scopeMembers.AddMember(new ScopeMember("DumpHolderLocal", typeof(CustomerRuntimeTypesHolder), new CustomerRuntimeTypesHolder(), ScopeMemberKind.Local));
+
+ var evaluator = CreateEvaluator(
+ templates: [new DebuggerExpression(string.Empty, @"{""ref"":""DumpHolderLocal""}", null)],
+ condition: null,
+ metric: null,
+ spanDecorations: null,
+ captureExpressions: null);
+
+ var result = evaluator.Evaluate(scopeMembers);
+
+ result.Template.Should().StartWith("_items=System.Collections.Generic.List`1");
+ result.Template.Should().Contain(", _itemsInterface=System.Collections.Generic.IList`1");
+ result.Template.Should().Contain(", _readOnlyItems=System.Collections.Generic.IReadOnlyList`1");
+ result.Template.Should().Contain(", _headers=System.Collections.Generic.Dictionary`2");
+ result.Template.Should().EndWith(", _uri=System.Uri");
+ result.Template.Should().NotContain(CustomerDumpMarker);
+ result.Errors.Should().BeNullOrEmpty();
+ }
+
+ [Theory]
+ [InlineData("items", "[a, b]")]
+ [InlineData("uri", "http://localhost/")]
+ [InlineData("xmlError", "System.Xml.XmlException, bad xml")]
+ public void ProbeExpressionEvaluator_TemplateDump_GetMemberDumpsAllowedRuntimeType(string memberName, string expected)
+ {
+ var scopeMembers = CreateScopeMembers();
+ scopeMembers.AddMember(new ScopeMember("DumpHolderLocal", typeof(AllowedRuntimeTypesHolder), new AllowedRuntimeTypesHolder(), ScopeMemberKind.Local));
+
+ var evaluator = CreateEvaluator(
+ templates: [new DebuggerExpression(string.Empty, $@"{{""getmember"":[{{""ref"":""DumpHolderLocal""}},""{memberName}""]}}", null)],
+ condition: null,
+ metric: null,
+ spanDecorations: null,
+ captureExpressions: null);
+
+ var result = evaluator.Evaluate(scopeMembers);
+
+ result.Template.Should().Be(expected);
+ result.Errors.Should().BeNullOrEmpty();
+ }
+
+ [Theory]
+ [InlineData("coreLib", "System.InvalidOperationException, operation failed, ")]
+ [InlineData("customer", "Datadog.Trace.Tests.Debugger.DebuggerExpressionLanguageTests+ThrowingMessageException, customer message")]
+ [InlineData("customerWithoutMessage", "Datadog.Trace.Tests.Debugger.DebuggerExpressionLanguageTests+CustomerException")]
+ [InlineData("outsideCoreLib", "System.Xml.XmlException, bad xml")]
+ public void ProbeExpressionEvaluator_TemplateDump_GetMemberExceptionField(string memberName, string expected)
+ {
+ // getmember keeps the declared Exception type. Only a CoreLib runtime type may run Message and
+ // StackTrace; any other shows its runtime type and the message field, so the throwing overrides never run.
+ var scopeMembers = CreateScopeMembers();
+ scopeMembers.AddMember(new ScopeMember("DumpHolderLocal", typeof(DumpExceptionFieldsHolder), new DumpExceptionFieldsHolder(), ScopeMemberKind.Local));
+
+ var evaluator = CreateEvaluator(
+ templates: [new DebuggerExpression(string.Empty, $@"{{""getmember"":[{{""ref"":""DumpHolderLocal""}},""{memberName}""]}}", null)],
+ condition: null,
+ metric: null,
+ spanDecorations: null,
+ captureExpressions: null);
+
+ var result = evaluator.Evaluate(scopeMembers);
+
+ result.Template.Should().Be(expected);
+ result.Errors.Should().BeNullOrEmpty();
+ }
+
+ [Fact]
+ public void ProbeExpressionEvaluator_TemplateDump_ExceptionFieldsCallMembersOnlyOnCoreLibRuntimeType()
+ {
+ var scopeMembers = CreateScopeMembers();
+ scopeMembers.AddMember(new ScopeMember("DumpHolderLocal", typeof(DumpExceptionFieldsHolder), new DumpExceptionFieldsHolder(), ScopeMemberKind.Local));
+
+ var evaluator = CreateEvaluator(
+ templates: [new DebuggerExpression(string.Empty, @"{""ref"":""DumpHolderLocal""}", null)],
+ condition: null,
+ metric: null,
+ spanDecorations: null,
+ captureExpressions: null);
+
+ var result = evaluator.Evaluate(scopeMembers);
+
+ result.Template.Should().Be(
+ "coreLib=System.InvalidOperationException, operation failed, , " +
+ "customer=Datadog.Trace.Tests.Debugger.DebuggerExpressionLanguageTests+ThrowingMessageException, customer message, " +
+ "customerWithoutMessage=Datadog.Trace.Tests.Debugger.DebuggerExpressionLanguageTests+CustomerException, " +
+ "outsideCoreLib=System.Xml.XmlException, bad xml");
+ result.Errors.Should().BeNullOrEmpty();
+ }
+
+ [Fact]
+ public void ProbeExpressionEvaluator_TemplateDump_CustomerExceptionLocalShowsMessageField()
+ {
+ var scopeMembers = CreateScopeMembers();
+ scopeMembers.AddMember(new ScopeMember("CustomerExceptionLocal", typeof(ThrowingMessageException), new ThrowingMessageException("customer message"), ScopeMemberKind.Local));
+
+ var evaluator = CreateEvaluator(
+ templates: [new DebuggerExpression(string.Empty, @"{""ref"":""CustomerExceptionLocal""}", null)],
+ condition: null,
+ metric: null,
+ spanDecorations: null,
+ captureExpressions: null);
+
+ var result = evaluator.Evaluate(scopeMembers);
+
+ result.Template.Should().Be("Datadog.Trace.Tests.Debugger.DebuggerExpressionLanguageTests+ThrowingMessageException, customer message");
+ result.Errors.Should().BeNullOrEmpty();
+ }
+
+ [Fact]
+ public void ProbeExpressionEvaluator_TemplateDump_ReflectionTypeLoadExceptionDoesNotReadLoaderExceptionMessages()
+ {
+ // On .NET Core, ReflectionTypeLoadException.Message reads each loader exception's Message.
+ var exception = new ReflectionTypeLoadException(Type.EmptyTypes, [new ThrowingMessageException("loader")], "loader failed");
+ var scopeMembers = CreateScopeMembers();
+ scopeMembers.AddMember(new ScopeMember("ExceptionLocal", typeof(ReflectionTypeLoadException), exception, ScopeMemberKind.Local));
+ scopeMembers.AddMember(new ScopeMember("DumpHolderLocal", typeof(DumpExceptionFieldHolder), new DumpExceptionFieldHolder(exception), ScopeMemberKind.Local));
+
+ var evaluator = CreateEvaluator(
+ templates:
+ [
+ new DebuggerExpression(string.Empty, @"{""ref"":""ExceptionLocal""}", null),
+ new DebuggerExpression(null, null, " | "),
+ new DebuggerExpression(string.Empty, @"{""getmember"":[{""ref"":""DumpHolderLocal""},""error""]}", null),
+ new DebuggerExpression(null, null, " | "),
+ new DebuggerExpression(string.Empty, @"{""ref"":""DumpHolderLocal""}", null)
+ ],
+ condition: null,
+ metric: null,
+ spanDecorations: null,
+ captureExpressions: null);
+
+ var result = evaluator.Evaluate(scopeMembers);
+
+ const string expected = "System.Reflection.ReflectionTypeLoadException, loader failed";
+ result.Template.Should().Be($"{expected} | {expected} | error={expected}");
+ result.Errors.Should().BeNullOrEmpty();
+ }
+
+#if NETCOREAPP3_0_OR_GREATER
+ [Fact]
+ public void ProbeExpressionEvaluator_TemplateDump_SwitchExpressionExceptionDoesNotFormatUnmatchedValue()
+ {
+ // SwitchExpressionException.Message formats the unmatched value, which calls its ToString.
+ var exception = new SwitchExpressionException(new ThrowingToStringValue());
+ var scopeMembers = CreateScopeMembers();
+ scopeMembers.AddMember(new ScopeMember("DumpHolderLocal", typeof(DumpExceptionFieldHolder), new DumpExceptionFieldHolder(exception), ScopeMemberKind.Local));
+
+ var evaluator = CreateEvaluator(
+ templates: [new DebuggerExpression(string.Empty, @"{""getmember"":[{""ref"":""DumpHolderLocal""},""error""]}", null)],
+ condition: null,
+ metric: null,
+ spanDecorations: null,
+ captureExpressions: null);
+
+ var result = evaluator.Evaluate(scopeMembers);
+
+ result.Template.Should().StartWith("System.Runtime.CompilerServices.SwitchExpressionException, ");
+ result.Errors.Should().BeNullOrEmpty();
+ }
+#endif
+
+ [Fact]
+ public void ProbeExpressionEvaluator_TemplateDump_GetMemberCollectionClassOutsideAllowlistDumpsFields()
+ {
+ // Collection isn't on the snapshot allowlist, and no allowlisted type derives from it, so the
+ // dump is decided when the expression compiles: its fields, not its items.
+ var scopeMembers = CreateScopeMembers();
+ scopeMembers.AddMember(new ScopeMember("DumpHolderLocal", typeof(AllowedRuntimeTypesHolder), new AllowedRuntimeTypesHolder(), ScopeMemberKind.Local));
+
+ var evaluator = CreateEvaluator(
+ templates: [new DebuggerExpression(string.Empty, @"{""getmember"":[{""ref"":""DumpHolderLocal""},""collection""]}", null)],
+ condition: null,
+ metric: null,
+ spanDecorations: null,
+ captureExpressions: null);
+
+ var result = evaluator.Evaluate(scopeMembers);
+
+ result.Template.Should().StartWith("items=");
+ result.Template.Should().NotContain("[a, b]");
+ result.Errors.Should().BeNullOrEmpty();
+ }
+
+ [Fact]
+ public void ProbeExpressionEvaluator_TemplateDump_NullCheckDoesNotInvokeEqualityOperator()
+ {
+ var scopeMembers = CreateScopeMembers();
+ scopeMembers.AddMember(new ScopeMember("CustomerValueLocal", typeof(ThrowingEqualityOperator), new ThrowingEqualityOperator(), ScopeMemberKind.Local));
+
+ var evaluator = CreateEvaluator(
+ templates: [new DebuggerExpression(string.Empty, @"{""ref"":""CustomerValueLocal""}", null)],
+ condition: null,
+ metric: null,
+ spanDecorations: null,
+ captureExpressions: null);
+
+ var result = evaluator.Evaluate(scopeMembers);
+
+ result.Template.Should().Be("_value=1");
+ result.Errors.Should().BeNullOrEmpty();
+ }
+
[Fact]
public void ProbeExpressionEvaluator_SpanDecorationDump_RedactsSensitiveObjectFields()
{
@@ -4224,6 +4460,77 @@ public void ProbeExpressionEvaluator_TemplateDump_IDictionaryField_RedactsConfig
Redaction.Instance.ResetInstance();
}
}
+
+ [Theory]
+ [InlineData("value", "{redacted}")]
+ [InlineData("items", "{redacted}")]
+ [InlineData("other", "_data=visible")]
+ [InlineData("error", "{redacted}")]
+ public void ProbeExpressionEvaluator_TemplateDump_GetMemberNonSealedField_RedactsConfiguredRuntimeType(string memberName, string expected)
+ {
+ // getmember keeps the declared type (object, IList, Exception), so only the runtime
+ // type shows that the value is a redacted type.
+ var scopeMembers = _tests.CreateScopeMembers();
+ scopeMembers.AddMember(new ScopeMember("DumpHolderLocal", typeof(DumpNonSealedFieldsHolder), new DumpNonSealedFieldsHolder(), ScopeMemberKind.Local));
+
+ try
+ {
+ Redaction.Instance.ResetInstance();
+ Redaction.Instance.SetConfig(
+ new HashSet(),
+ new HashSet(),
+ new HashSet { typeof(RedactedRuntimeTypeValue).FullName, typeof(List).FullName, typeof(InvalidOperationException).FullName });
+
+ var evaluator = _tests.CreateEvaluator(
+ templates: [new DebuggerExpression(string.Empty, $@"{{""getmember"":[{{""ref"":""DumpHolderLocal""}},""{memberName}""]}}", null)],
+ condition: null,
+ metric: null,
+ spanDecorations: null,
+ captureExpressions: null);
+
+ var result = evaluator.Evaluate(scopeMembers);
+
+ result.Template.Should().Be(expected);
+ result.Errors.Should().BeNullOrEmpty();
+ }
+ finally
+ {
+ Redaction.Instance.ResetInstance();
+ }
+ }
+
+ [Fact]
+ public void ProbeExpressionEvaluator_TemplateDump_ExceptionField_RedactsConfiguredRuntimeType()
+ {
+ var scopeMembers = _tests.CreateScopeMembers();
+ scopeMembers.AddMember(new ScopeMember("DumpHolderLocal", typeof(DumpNonSealedFieldsHolder), new DumpNonSealedFieldsHolder(), ScopeMemberKind.Local));
+
+ try
+ {
+ Redaction.Instance.ResetInstance();
+ Redaction.Instance.SetConfig(
+ new HashSet(),
+ new HashSet(),
+ new HashSet { typeof(InvalidOperationException).FullName });
+
+ var evaluator = _tests.CreateEvaluator(
+ templates: [new DebuggerExpression(string.Empty, @"{""ref"":""DumpHolderLocal""}", null)],
+ condition: null,
+ metric: null,
+ spanDecorations: null,
+ captureExpressions: null);
+
+ var result = evaluator.Evaluate(scopeMembers);
+
+ result.Template.Should().EndWith(", error={redacted}");
+ result.Template.Should().NotContain("TOP_SECRET_VALUE");
+ result.Errors.Should().BeNullOrEmpty();
+ }
+ finally
+ {
+ Redaction.Instance.ResetInstance();
+ }
+ }
}
internal class GenericReferenceTypeTarget
@@ -4484,6 +4791,150 @@ public DumpRedactionDictionaryHolder(string authorizationSecret, string publicVa
}
}
+ // Each field's declared type allows enumeration or ToString, but its runtime type is a customer
+ // subclass whose members throw if called.
+ private sealed class CustomerRuntimeTypesHolder
+ {
+ private readonly List _items = new ThrowingEnumerationList { CustomerDumpMarker };
+
+ private readonly IList _itemsInterface = new ThrowingEnumerationList { CustomerDumpMarker };
+
+ private readonly IReadOnlyList _readOnlyItems = new ThrowingEnumerationList { CustomerDumpMarker };
+
+ private readonly Dictionary _headers = new ThrowingEnumerationDictionary { { "public", CustomerDumpMarker } };
+
+ private readonly Uri _uri = new ThrowingToStringUri();
+ }
+
+ private sealed class DumpNonSealedFieldsHolder
+ {
+ private readonly object value = new RedactedRuntimeTypeValue("TOP_SECRET_VALUE");
+
+ private readonly IList items = new List { "TOP_SECRET_VALUE" };
+
+ private readonly object other = new VisibleRuntimeTypeValue("visible");
+
+ private readonly Exception error = new InvalidOperationException("TOP_SECRET_VALUE");
+ }
+
+ private sealed class DumpExceptionFieldsHolder
+ {
+ private readonly Exception coreLib = new InvalidOperationException("operation failed");
+
+ private readonly Exception customer = new ThrowingMessageException("customer message");
+
+ private readonly Exception customerWithoutMessage = new CustomerException();
+
+ private readonly Exception outsideCoreLib = new XmlException("bad xml");
+ }
+
+ private sealed class ThrowingMessageException : InvalidOperationException
+ {
+ public ThrowingMessageException(string message)
+ : base(message)
+ {
+ }
+
+ public override string Message => throw new InvalidOperationException("Customer Message should not be called.");
+
+ public override string StackTrace => throw new InvalidOperationException("Customer StackTrace should not be called.");
+ }
+
+ private sealed class CustomerException : Exception
+ {
+ }
+
+ private sealed class DumpExceptionFieldHolder
+ {
+ private readonly Exception error;
+
+ public DumpExceptionFieldHolder(Exception error)
+ {
+ this.error = error;
+ }
+ }
+
+ private sealed class ThrowingToStringValue
+ {
+ public override string ToString() => throw new InvalidOperationException("Customer ToString should not be called.");
+ }
+
+ private sealed class RedactedRuntimeTypeValue
+ {
+ private readonly string _data;
+
+ public RedactedRuntimeTypeValue(string data)
+ {
+ _data = data;
+ }
+ }
+
+ private sealed class VisibleRuntimeTypeValue
+ {
+ private readonly string _data;
+
+ public VisibleRuntimeTypeValue(string data)
+ {
+ _data = data;
+ }
+ }
+
+ private sealed class AllowedRuntimeTypesHolder
+ {
+ private readonly IList items = new List { "a", "b" };
+
+ private readonly Uri uri = new("http://localhost/");
+
+ private readonly XmlException xmlError = new("bad xml");
+
+ private readonly Collection collection = new() { "a", "b" };
+ }
+
+ private sealed class ThrowingEnumerationList : List, IEnumerable
+ {
+ public int CustomerField { get; } = 1;
+
+ IEnumerator IEnumerable.GetEnumerator() => throw new InvalidOperationException("Customer enumerator should not be called.");
+
+ IEnumerator IEnumerable.GetEnumerator() => throw new InvalidOperationException("Customer enumerator should not be called.");
+ }
+
+ private sealed class ThrowingEnumerationDictionary : Dictionary, IDictionary, IEnumerable>
+ {
+ public int CustomerField { get; } = 1;
+
+ IDictionaryEnumerator IDictionary.GetEnumerator() => throw new InvalidOperationException("Customer enumerator should not be called.");
+
+ IEnumerator> IEnumerable>.GetEnumerator() => throw new InvalidOperationException("Customer enumerator should not be called.");
+
+ IEnumerator IEnumerable.GetEnumerator() => throw new InvalidOperationException("Customer enumerator should not be called.");
+ }
+
+ private sealed class ThrowingToStringUri : Uri
+ {
+ public ThrowingToStringUri()
+ : base("http://localhost/")
+ {
+ }
+
+ public int CustomerField { get; } = 1;
+
+ public override string ToString() => throw new InvalidOperationException("Customer ToString should not be called.");
+ }
+
+ private sealed class ThrowingEqualityOperator
+ {
+ private readonly int _value = 1;
+
+ public static bool operator ==(ThrowingEqualityOperator left, object right) => throw new InvalidOperationException("Customer equality operator should not be called.");
+
+ public static bool operator !=(ThrowingEqualityOperator left, object right) => throw new InvalidOperationException("Customer equality operator should not be called.");
+
+ public override bool Equals(object obj) => ReferenceEquals(this, obj);
+
+ public override int GetHashCode() => _value;
+ }
+
private sealed class DumpCustomIDictionaryFieldHolder
{
private readonly IDictionary customMap;
diff --git a/tracer/test/Datadog.Trace.Tests/Debugger/ProbeExpressionsResources/Approvals/DebuggerExpressionLanguageTests.AccessNullableStruct.verified.txt b/tracer/test/Datadog.Trace.Tests/Debugger/ProbeExpressionsResources/Approvals/DebuggerExpressionLanguageTests.AccessNullableStruct.verified.txt
index cc3c7de55910..8b810ec58d83 100644
--- a/tracer/test/Datadog.Trace.Tests/Debugger/ProbeExpressionsResources/Approvals/DebuggerExpressionLanguageTests.AccessNullableStruct.verified.txt
+++ b/tracer/test/Datadog.Trace.Tests/Debugger/ProbeExpressionsResources/Approvals/DebuggerExpressionLanguageTests.AccessNullableStruct.verified.txt
@@ -46,7 +46,7 @@ Expressions: (
var StringArg = (string)scopeMemberArray[18].Value;
var CollectionArg = (List)scopeMemberArray[19].Value;
var NestedObjectArg = (DebuggerExpressionLanguageTests.TestStruct.NestedObject)scopeMemberArray[20].Value;
- var $dd_el_result = (this.NullableNullValue == null)
+ var $dd_el_result = !this.NullableNullValue.HasValue
? "null"
: {
fieldsArray = ProbeExpressionParser
diff --git a/tracer/test/Datadog.Trace.Tests/Debugger/ProbeExpressionsResources/Approvals/DebuggerExpressionLanguageTests.AccessNullableStructNotNull.verified.txt b/tracer/test/Datadog.Trace.Tests/Debugger/ProbeExpressionsResources/Approvals/DebuggerExpressionLanguageTests.AccessNullableStructNotNull.verified.txt
index d87bbc426523..c3f340b0915a 100644
--- a/tracer/test/Datadog.Trace.Tests/Debugger/ProbeExpressionsResources/Approvals/DebuggerExpressionLanguageTests.AccessNullableStructNotNull.verified.txt
+++ b/tracer/test/Datadog.Trace.Tests/Debugger/ProbeExpressionsResources/Approvals/DebuggerExpressionLanguageTests.AccessNullableStructNotNull.verified.txt
@@ -46,7 +46,7 @@ Expressions: (
var StringArg = (string)scopeMemberArray[18].Value;
var CollectionArg = (List)scopeMemberArray[19].Value;
var NestedObjectArg = (DebuggerExpressionLanguageTests.TestStruct.NestedObject)scopeMemberArray[20].Value;
- var $dd_el_result = (this.NullableNotNullValue == null)
+ var $dd_el_result = !this.NullableNotNullValue.HasValue
? "null"
: {
fieldsArray = ProbeExpressionParser
diff --git a/tracer/test/Datadog.Trace.Tests/Debugger/ProbeExpressionsResources/Approvals/DebuggerExpressionLanguageTests.Collection.verified.txt b/tracer/test/Datadog.Trace.Tests/Debugger/ProbeExpressionsResources/Approvals/DebuggerExpressionLanguageTests.Collection.verified.txt
index 380da9118c74..adb5fab1e084 100644
--- a/tracer/test/Datadog.Trace.Tests/Debugger/ProbeExpressionsResources/Approvals/DebuggerExpressionLanguageTests.Collection.verified.txt
+++ b/tracer/test/Datadog.Trace.Tests/Debugger/ProbeExpressionsResources/Approvals/DebuggerExpressionLanguageTests.Collection.verified.txt
@@ -13,8 +13,9 @@ Expressions: (
{
string loopItem;
IEnumerator enumerator;
- int index;
- StringBuilder itemValues;
+ int index, index2;
+ StringBuilder itemValues, fieldValues;
+ FieldInfo[] fieldsArray;
var this = (scopeMember.Value == null)
? default(DebuggerExpressionLanguageTests.TestStruct)
: (DebuggerExpressionLanguageTests.TestStruct)scopeMember.Value;
@@ -42,49 +43,96 @@ Expressions: (
var StringArg = (string)scopeMemberArray[18].Value;
var CollectionArg = (List)scopeMemberArray[19].Value;
var NestedObjectArg = (DebuggerExpressionLanguageTests.TestStruct.NestedObject)scopeMemberArray[20].Value;
- var $dd_el_result = (this.Collection == null)
- ? "null"
- : {
- enumerator = this.Collection.GetEnumerator();
- index = 0;
- itemValues = new StringBuilder();
- itemValues.Append("[");
- while (true)
- {
- if (enumerator.MoveNext())
- {
- string loopItem;
+ var $dd_el_result =
+ {
+ var dumpValue = (object)this.Collection;
- if (index < 3)
- {
- if (index > 0)
+ return (dumpValue == null)
+ ? "null"
+ : ProbeExpressionParser.IsRedactedRuntimeValue(dumpValue, typeof(List))
+ ? "{redacted}"
+ : (dumpValue.GetType() == typeof(List))
+ ? {
+ enumerator = ((List)dumpValue).GetEnumerator();
+ index = 0;
+ itemValues = new StringBuilder();
+ itemValues.Append("[");
+ while (true)
{
- return itemValues.Append(", ");
+ if (enumerator.MoveNext())
+ {
+ string loopItem;
+
+ if (index < 3)
+ {
+ if (index > 0)
+ {
+ return itemValues.Append(", ");
+ }
+
+ loopItem = enumerator.Current;
+ itemValues.Append(ProbeExpressionParser.DumpObject(
+ loopItem,
+ typeof(string),
+ "",
+ 1));
+ index++;
+ }
+ else
+ {
+ itemValues.Append(", ...");
+ break;
+ }
+ }
+ else
+ {
+ break;
+ }
}
+ itemValues.Append("]");
- loopItem = enumerator.Current;
- itemValues.Append(ProbeExpressionParser.DumpObject(
- loopItem,
- typeof(string),
- "",
- 1));
- index++;
+ return itemValues.ToString();
}
- else
- {
- itemValues.Append(", ...");
- break;
- }
- }
- else
- {
- break;
- }
- }
- itemValues.Append("]");
+ : {
+ fieldsArray = ProbeExpressionParser
+ .GetSafeFieldsForDump(
+ dumpValue.GetType().GetFields(
+ BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic))
+ .OrderBy(fieldInfo => fieldInfo.MetadataToken)
+ .ToArray();
+ fieldValues = new StringBuilder();
+ index2 = 0;
+ while (true)
+ {
+ if ((index2 < fieldsArray.Length) && (index2 < 5))
+ {
+ fieldValues.Append(
+ ProbeExpressionParser.DumpObject(
+ ProbeExpressionParser.GetFieldValueForDump(fieldsArray[index2], (object)dumpValue),
+ fieldsArray[index2].FieldType,
+ fieldsArray[index2].Name,
+ 0));
+ index2++;
+
+ if ((index2 < fieldsArray.Length) && (index2 < 5))
+ {
+ return fieldValues.Append(", ");
+ }
+ }
+ else
+ {
+ if (index2 < fieldsArray.Length)
+ {
+ return fieldValues.Append(", ...");
+ }
+
+ break;
+ }
+ }
- return itemValues.ToString();
- };
+ return fieldValues.ToString();
+ };
+ };
return $dd_el_result;
}
diff --git a/tracer/test/Datadog.Trace.Tests/Debugger/ProbeExpressionsResources/Approvals/DebuggerExpressionLanguageTests.CustomArrayAtIndex.verified.txt b/tracer/test/Datadog.Trace.Tests/Debugger/ProbeExpressionsResources/Approvals/DebuggerExpressionLanguageTests.CustomArrayAtIndex.verified.txt
index d6d446c475ce..eb9fa9e85cad 100644
--- a/tracer/test/Datadog.Trace.Tests/Debugger/ProbeExpressionsResources/Approvals/DebuggerExpressionLanguageTests.CustomArrayAtIndex.verified.txt
+++ b/tracer/test/Datadog.Trace.Tests/Debugger/ProbeExpressionsResources/Approvals/DebuggerExpressionLanguageTests.CustomArrayAtIndex.verified.txt
@@ -46,49 +46,56 @@ Expressions: (
var StringArg = (string)scopeMemberArray[18].Value;
var CollectionArg = (List)scopeMemberArray[19].Value;
var NestedObjectArg = (DebuggerExpressionLanguageTests.TestStruct.NestedObject)scopeMemberArray[20].Value;
- var $dd_el_result = (((DebuggerExpressionLanguageTests.TestStruct.NestedObject)CustomArrayLocal[0]) == null)
- ? "null"
- : {
- fieldsArray = ProbeExpressionParser
- .GetSafeFieldsForDump(
- ((DebuggerExpressionLanguageTests.TestStruct.NestedObject)CustomArrayLocal[0]).GetType().GetFields(
- BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic))
- .OrderBy(fieldInfo => fieldInfo.MetadataToken)
- .ToArray();
- fieldValues = new StringBuilder();
- index = 0;
- while (true)
- {
- if ((index < fieldsArray.Length) && (index < 5))
- {
- fieldValues.Append(
- ProbeExpressionParser.DumpObject(
- ProbeExpressionParser.GetFieldValueForDump(
- fieldsArray[index],
- (object)((DebuggerExpressionLanguageTests.TestStruct.NestedObject)CustomArrayLocal[0])),
- fieldsArray[index].FieldType,
- fieldsArray[index].Name,
- 0));
- index++;
+ var $dd_el_result =
+ {
+ var dumpValue = (object)((DebuggerExpressionLanguageTests.TestStruct.NestedObject)CustomArrayLocal[0]);
- if ((index < fieldsArray.Length) && (index < 5))
+ return (dumpValue == null)
+ ? "null"
+ : ProbeExpressionParser.IsRedactedRuntimeValue(
+ dumpValue,
+ typeof(DebuggerExpressionLanguageTests.TestStruct.NestedObject))
+ ? "{redacted}"
+ : {
+ fieldsArray = ProbeExpressionParser
+ .GetSafeFieldsForDump(
+ dumpValue.GetType().GetFields(
+ BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic))
+ .OrderBy(fieldInfo => fieldInfo.MetadataToken)
+ .ToArray();
+ fieldValues = new StringBuilder();
+ index = 0;
+ while (true)
{
- return fieldValues.Append(", ");
- }
- }
- else
- {
- if (index < fieldsArray.Length)
- {
- return fieldValues.Append(", ...");
- }
+ if ((index < fieldsArray.Length) && (index < 5))
+ {
+ fieldValues.Append(
+ ProbeExpressionParser.DumpObject(
+ ProbeExpressionParser.GetFieldValueForDump(fieldsArray[index], (object)dumpValue),
+ fieldsArray[index].FieldType,
+ fieldsArray[index].Name,
+ 0));
+ index++;
- break;
- }
- }
+ if ((index < fieldsArray.Length) && (index < 5))
+ {
+ return fieldValues.Append(", ");
+ }
+ }
+ else
+ {
+ if (index < fieldsArray.Length)
+ {
+ return fieldValues.Append(", ...");
+ }
+
+ break;
+ }
+ }
- return fieldValues.ToString();
- };
+ return fieldValues.ToString();
+ };
+ };
return $dd_el_result;
}
diff --git a/tracer/test/Datadog.Trace.Tests/Debugger/ProbeExpressionsResources/Approvals/DebuggerExpressionLanguageTests.Exception.verified.txt b/tracer/test/Datadog.Trace.Tests/Debugger/ProbeExpressionsResources/Approvals/DebuggerExpressionLanguageTests.Exception.verified.txt
index 930e5e3dfea0..3a6c57919197 100644
--- a/tracer/test/Datadog.Trace.Tests/Debugger/ProbeExpressionsResources/Approvals/DebuggerExpressionLanguageTests.Exception.verified.txt
+++ b/tracer/test/Datadog.Trace.Tests/Debugger/ProbeExpressionsResources/Approvals/DebuggerExpressionLanguageTests.Exception.verified.txt
@@ -38,10 +38,17 @@ Expressions: (
var StringArg = (string)scopeMemberArray[18].Value;
var CollectionArg = (List)scopeMemberArray[19].Value;
var NestedObjectArg = (DebuggerExpressionLanguageTests.TestStruct.NestedObject)scopeMemberArray[20].Value;
- var $dd_el_result = (@exception == null)
- ? "System.Exception"
- : "System.Exception" + ", " + @exception.Message + ", " + @exception.StackTrace;
+ var $dd_el_result =
+ {
+ var dumpValue = (object)@exception;
+
+ return (dumpValue == null)
+ ? "System.Exception"
+ : ProbeExpressionParser.IsRedactedRuntimeValue(dumpValue, typeof(Exception))
+ ? "{redacted}"
+ : ProbeExpressionParser.DumpException((Exception)dumpValue, null);
+ };
return $dd_el_result;
}
-Result: The result of the expression is: System.Exception, Can not cast X to Y,
+Result: The result of the expression is: System.InvalidCastException, Can not cast X to Y,
diff --git a/tracer/test/Datadog.Trace.Tests/Debugger/ProbeExpressionsResources/Approvals/DebuggerExpressionLanguageTests.ToStringNotSupported.verified.txt b/tracer/test/Datadog.Trace.Tests/Debugger/ProbeExpressionsResources/Approvals/DebuggerExpressionLanguageTests.ToStringNotSupported.verified.txt
index 3a3625bd63cc..efa3e6e2fe67 100644
--- a/tracer/test/Datadog.Trace.Tests/Debugger/ProbeExpressionsResources/Approvals/DebuggerExpressionLanguageTests.ToStringNotSupported.verified.txt
+++ b/tracer/test/Datadog.Trace.Tests/Debugger/ProbeExpressionsResources/Approvals/DebuggerExpressionLanguageTests.ToStringNotSupported.verified.txt
@@ -41,47 +41,56 @@ Expressions: (
var StringArg = (string)scopeMemberArray[18].Value;
var CollectionArg = (List)scopeMemberArray[19].Value;
var NestedObjectArg = (DebuggerExpressionLanguageTests.TestStruct.NestedObject)scopeMemberArray[20].Value;
- var $dd_el_result = (this.Nested == null)
- ? "null"
- : {
- fieldsArray = ProbeExpressionParser
- .GetSafeFieldsForDump(
- this.Nested.GetType().GetFields(
- BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic))
- .OrderBy(fieldInfo => fieldInfo.MetadataToken)
- .ToArray();
- fieldValues = new StringBuilder();
- index = 0;
- while (true)
- {
- if ((index < fieldsArray.Length) && (index < 5))
- {
- fieldValues.Append(
- ProbeExpressionParser.DumpObject(
- ProbeExpressionParser.GetFieldValueForDump(fieldsArray[index], (object)this.Nested),
- fieldsArray[index].FieldType,
- fieldsArray[index].Name,
- 0));
- index++;
+ var $dd_el_result =
+ {
+ var dumpValue = (object)this.Nested;
- if ((index < fieldsArray.Length) && (index < 5))
+ return (dumpValue == null)
+ ? "null"
+ : ProbeExpressionParser.IsRedactedRuntimeValue(
+ dumpValue,
+ typeof(DebuggerExpressionLanguageTests.TestStruct.NestedObject))
+ ? "{redacted}"
+ : {
+ fieldsArray = ProbeExpressionParser
+ .GetSafeFieldsForDump(
+ dumpValue.GetType().GetFields(
+ BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic))
+ .OrderBy(fieldInfo => fieldInfo.MetadataToken)
+ .ToArray();
+ fieldValues = new StringBuilder();
+ index = 0;
+ while (true)
{
- return fieldValues.Append(", ");
- }
- }
- else
- {
- if (index < fieldsArray.Length)
- {
- return fieldValues.Append(", ...");
- }
+ if ((index < fieldsArray.Length) && (index < 5))
+ {
+ fieldValues.Append(
+ ProbeExpressionParser.DumpObject(
+ ProbeExpressionParser.GetFieldValueForDump(fieldsArray[index], (object)dumpValue),
+ fieldsArray[index].FieldType,
+ fieldsArray[index].Name,
+ 0));
+ index++;
- break;
- }
- }
+ if ((index < fieldsArray.Length) && (index < 5))
+ {
+ return fieldValues.Append(", ");
+ }
+ }
+ else
+ {
+ if (index < fieldsArray.Length)
+ {
+ return fieldValues.Append(", ...");
+ }
+
+ break;
+ }
+ }
- return fieldValues.ToString();
- };
+ return fieldValues.ToString();
+ };
+ };
return $dd_el_result;
}