From 13828d34b369277f784248e0ed48cc12ce503bf7 Mon Sep 17 00:00:00 2001
From: Henrique Graca <999396+hjgraca@users.noreply.github.com>
Date: Tue, 30 Sep 2025 15:28:04 +0100
Subject: [PATCH 1/9] feat(tests): add comprehensive tests for XRayRecorder and
tracing functionality, including sanitization for problematic types
---
.../Internal/TracingSubsegment.cs | 101 ++-
.../Internal/XRayRecorder.cs | 588 +++++++++++++++++-
.../AWS.Lambda.Powertools.Tracing/Tracing.cs | 44 ++
.../EntityLevelSanitizationTests.cs | 156 +++++
.../Handlers/ExceptionFunctionHandler.cs | 40 ++
.../Handlers/HandlerTests.cs | 81 ++-
.../Handlers/Handlers.cs | 37 ++
.../IntegrationTests.cs | 157 +++++
.../TracingAspectTests.cs | 2 +-
.../TracingAttributeTest.cs | 213 ++++---
.../TracingSubsegmentTests.cs | 333 +++++++++-
.../TracingTestBase.cs | 182 ++++++
.../TracingTestCollectionFixture.cs | 73 +++
.../XRayRecorderSanitizationTests.cs | 261 ++++++++
.../XRayRecorderTestFixture.cs | 28 +
.../XRayRecorderTests.cs | 113 +++-
16 files changed, 2295 insertions(+), 114 deletions(-)
create mode 100644 libraries/tests/AWS.Lambda.Powertools.Tracing.Tests/EntityLevelSanitizationTests.cs
create mode 100644 libraries/tests/AWS.Lambda.Powertools.Tracing.Tests/IntegrationTests.cs
create mode 100644 libraries/tests/AWS.Lambda.Powertools.Tracing.Tests/TracingTestBase.cs
create mode 100644 libraries/tests/AWS.Lambda.Powertools.Tracing.Tests/TracingTestCollectionFixture.cs
create mode 100644 libraries/tests/AWS.Lambda.Powertools.Tracing.Tests/XRayRecorderSanitizationTests.cs
create mode 100644 libraries/tests/AWS.Lambda.Powertools.Tracing.Tests/XRayRecorderTestFixture.cs
diff --git a/libraries/src/AWS.Lambda.Powertools.Tracing/Internal/TracingSubsegment.cs b/libraries/src/AWS.Lambda.Powertools.Tracing/Internal/TracingSubsegment.cs
index 68622858f..5d73e3fde 100644
--- a/libraries/src/AWS.Lambda.Powertools.Tracing/Internal/TracingSubsegment.cs
+++ b/libraries/src/AWS.Lambda.Powertools.Tracing/Internal/TracingSubsegment.cs
@@ -1,4 +1,6 @@
+using System;
using Amazon.XRay.Recorder.Core.Internal.Entities;
+using AWS.Lambda.Powertools.Common;
namespace AWS.Lambda.Powertools.Tracing.Internal;
@@ -7,11 +9,106 @@ namespace AWS.Lambda.Powertools.Tracing.Internal;
/// It's a wrapper for Subsegment from Amazon.XRay.Recorder.Core.Internal.
///
///
-public class TracingSubsegment : Subsegment
+public class TracingSubsegment : Subsegment, IDisposable
{
+ private bool _disposed = false;
+ private readonly bool _shouldAutoEnd;
+
///
/// Wrapper constructor
///
///
- public TracingSubsegment(string name) : base(name) { }
+ public TracingSubsegment(string name) : base(name)
+ {
+ _shouldAutoEnd = false;
+ }
+
+ ///
+ /// Constructor for disposable subsegments
+ ///
+ /// The name of the subsegment
+ /// Whether this subsegment should auto-end when disposed
+ internal TracingSubsegment(string name, bool shouldAutoEnd) : base(name)
+ {
+ _shouldAutoEnd = shouldAutoEnd;
+ }
+
+ ///
+ /// Adds an annotation to the subsegment
+ ///
+ /// The annotation key
+ /// The annotation value
+ public new void AddAnnotation(string key, object value)
+ {
+ XRayRecorder.Instance.AddAnnotation(key, value);
+ }
+
+ ///
+ /// Adds metadata to the subsegment
+ ///
+ /// The metadata key
+ /// The metadata value
+ public new void AddMetadata(string key, object value)
+ {
+ XRayRecorder.Instance.AddMetadata(Namespace ?? PowertoolsConfigurations.Instance.Service, key, value);
+ }
+
+ ///
+ /// Adds metadata to the subsegment with a specific namespace
+ ///
+ /// The namespace
+ /// The metadata key
+ /// The metadata value
+ public new void AddMetadata(string nameSpace, string key, object value)
+ {
+ XRayRecorder.Instance.AddMetadata(nameSpace, key, value);
+ }
+
+ ///
+ /// Adds an exception to the subsegment
+ ///
+ /// The exception to add
+ public new void AddException(Exception exception)
+ {
+ XRayRecorder.Instance.AddException(exception);
+ }
+
+ ///
+ /// Adds HTTP information to the subsegment
+ ///
+ /// The HTTP information key
+ /// The HTTP information value
+ public void AddHttpInformation(string key, object value)
+ {
+ XRayRecorder.Instance.AddHttpInformation(key, value);
+ }
+
+ ///
+ /// Disposes the subsegment and ends it if configured to do so
+ ///
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ ///
+ /// Protected dispose method
+ ///
+ /// Whether we're disposing
+ protected virtual void Dispose(bool disposing)
+ {
+ if (!_disposed && disposing && _shouldAutoEnd)
+ {
+ try
+ {
+ XRayRecorder.Instance.EndSubsegment();
+ }
+ catch
+ {
+ // Swallow exceptions during disposal to prevent issues in using blocks
+ }
+ _disposed = true;
+ }
+ }
}
\ No newline at end of file
diff --git a/libraries/src/AWS.Lambda.Powertools.Tracing/Internal/XRayRecorder.cs b/libraries/src/AWS.Lambda.Powertools.Tracing/Internal/XRayRecorder.cs
index 0c546da22..5082610f8 100644
--- a/libraries/src/AWS.Lambda.Powertools.Tracing/Internal/XRayRecorder.cs
+++ b/libraries/src/AWS.Lambda.Powertools.Tracing/Internal/XRayRecorder.cs
@@ -14,11 +14,20 @@ namespace AWS.Lambda.Powertools.Tracing.Internal;
///
internal class XRayRecorder : IXRayRecorder
{
- private static IAWSXRayRecorder _awsxRayRecorder;
+ ///
+ /// Maximum recursion depth for sanitization to prevent infinite loops
+ ///
+ private const int MaxSanitizationDepth = 10;
+
///
/// The instance
///
private static IXRayRecorder _instance;
+
+ ///
+ /// The AWS X-Ray recorder instance
+ ///
+ private readonly IAWSXRayRecorder _awsxRayRecorder;
///
/// Gets the instance.
@@ -34,6 +43,14 @@ public XRayRecorder(IAWSXRayRecorder awsxRayRecorder, IPowertoolsConfigurations
_awsxRayRecorder = awsxRayRecorder;
}
+ ///
+ /// Resets the singleton instance. This method is intended for testing purposes only.
+ ///
+ internal static void ResetInstance()
+ {
+ _instance = null;
+ }
+
///
/// Checks whether current execution is in AWS Lambda.
///
@@ -82,7 +99,10 @@ public void SetNamespace(string value)
public void AddAnnotation(string key, object value)
{
if (_isLambda)
- _awsxRayRecorder.AddAnnotation(key, value);
+ {
+ var sanitizedValue = SanitizeValueForAnnotation(value);
+ _awsxRayRecorder.AddAnnotation(key, sanitizedValue);
+ }
}
///
@@ -94,7 +114,10 @@ public void AddAnnotation(string key, object value)
public void AddMetadata(string nameSpace, string key, object value)
{
if (_isLambda)
- _awsxRayRecorder.AddMetadata(nameSpace, key, value);
+ {
+ var sanitizedValue = SanitizeValueForMetadata(value);
+ _awsxRayRecorder.AddMetadata(nameSpace, key, sanitizedValue);
+ }
}
///
@@ -103,22 +126,114 @@ public void AddMetadata(string nameSpace, string key, object value)
public void EndSubsegment()
{
if (!_isLambda) return;
+
try
{
+ // First attempt: Sanitize the entire entity before ending the subsegment
+ SanitizeCurrentEntitySafely();
_awsxRayRecorder.EndSubsegment();
}
+ catch (Exception e) when (IsSerializationError(e))
+ {
+ // This is a JSON serialization error - handle it aggressively
+ Console.WriteLine("JSON serialization error detected in Tracing utility - attempting recovery");
+ Console.WriteLine($"Error: {e.Message}");
+
+ HandleSerializationError(e);
+ }
catch (Exception e)
{
- // if it fails at this stage the data is lost
- // so lets create a new subsegment with the error
-
+ // Handle other types of errors with the original logic
Console.WriteLine("Error in Tracing utility - see Exceptions tab in Cloudwatch Traces");
+ Console.WriteLine(e.StackTrace);
+
+ try
+ {
+ _awsxRayRecorder.TraceContext.ClearEntity();
+ _awsxRayRecorder.BeginSubsegment("Error in Tracing utility - see Exceptions tab");
+ _awsxRayRecorder.AddException(e);
+ _awsxRayRecorder.MarkError();
+ _awsxRayRecorder.EndSubsegment();
+ }
+ catch
+ {
+ // If even error handling fails, give up gracefully
+ Console.WriteLine("Failed to handle tracing error");
+ }
+ }
+ }
+ ///
+ /// Determines if an exception is related to JSON serialization
+ ///
+ private static bool IsSerializationError(Exception e)
+ {
+ if (e == null) return false;
+
+ var message = e.Message ?? string.Empty;
+ var stackTrace = e.StackTrace ?? string.Empty;
+ var typeName = e.GetType().Name ?? string.Empty;
+
+ return message.Contains("LitJson") ||
+ message.Contains("JsonMapper") ||
+ stackTrace.Contains("JsonMapper") ||
+ stackTrace.Contains("LitJson") ||
+ stackTrace.Contains("JsonSegmentMarshaller") ||
+ typeName.Contains("Json");
+ }
+
+ ///
+ /// Handles serialization errors by progressively trying different recovery strategies
+ ///
+ private void HandleSerializationError(Exception originalException)
+ {
+ try
+ {
+ // Strategy 1: Try to clear and recreate with minimal data
+ Console.WriteLine("Attempting serialization error recovery - Strategy 1: Clear and recreate");
+
_awsxRayRecorder.TraceContext.ClearEntity();
- _awsxRayRecorder.BeginSubsegment("Error in Tracing utility - see Exceptions tab");
- _awsxRayRecorder.AddException(e);
- _awsxRayRecorder.MarkError();
+ _awsxRayRecorder.BeginSubsegment("Tracing_Sanitized");
+ _awsxRayRecorder.AddAnnotation("SerializationError", true);
+ _awsxRayRecorder.AddMetadata("Error", "Type", "JSON Serialization Error");
+ _awsxRayRecorder.AddMetadata("Error", "Message", SanitizeValueForMetadata(originalException.Message));
_awsxRayRecorder.EndSubsegment();
+
+ Console.WriteLine("Serialization error recovery successful");
+ }
+ catch (Exception e2)
+ {
+ try
+ {
+ // Strategy 2: Even more minimal approach
+ Console.WriteLine("Strategy 1 failed, attempting Strategy 2: Minimal segment");
+
+ _awsxRayRecorder.TraceContext.ClearEntity();
+ _awsxRayRecorder.BeginSubsegment("Tracing_Error");
+ _awsxRayRecorder.AddAnnotation("Error", "SerializationFailed");
+ _awsxRayRecorder.EndSubsegment();
+
+ Console.WriteLine("Minimal serialization error recovery successful");
+ }
+ catch (Exception e3)
+ {
+ // Strategy 3: Complete failure - just log and give up
+ Console.WriteLine("All serialization error recovery strategies failed");
+ Console.WriteLine($"Original error: {originalException.Message}");
+ Console.WriteLine($"Recovery error 1: {e2.Message}");
+ Console.WriteLine($"Recovery error 2: {e3.Message}");
+
+ // Try one last time to clear the entity to prevent further issues
+ try
+ {
+ _awsxRayRecorder.TraceContext.ClearEntity();
+ }
+ catch
+ {
+ // If we can't even clear, there's nothing more we can do
+ Console.WriteLine("Failed to clear X-Ray entity - tracing may be in an inconsistent state");
+ }
+ }
}
}
@@ -128,9 +243,19 @@ public void EndSubsegment()
/// Entity.
public Entity GetEntity()
{
- return _isLambda
- ? _awsxRayRecorder.TraceContext.GetEntity()
- : new Subsegment("Root");
+ if (_isLambda)
+ {
+ try
+ {
+ return _awsxRayRecorder?.TraceContext?.GetEntity() ?? new Subsegment("Root");
+ }
+ catch
+ {
+ // If we can't get the entity from X-Ray context, fall back to a root subsegment
+ return new Subsegment("Root");
+ }
+ }
+ return new Subsegment("Root");
}
///
@@ -150,7 +275,19 @@ public void SetEntity(Entity entity)
public void AddException(Exception exception)
{
if (_isLambda)
- _awsxRayRecorder.AddException(exception);
+ {
+ // Sanitize exception data if it contains problematic types
+ try
+ {
+ _awsxRayRecorder.AddException(exception);
+ }
+ catch (Exception ex) when (ex.Message.Contains("LitJson") || ex.Message.Contains("JsonMapper"))
+ {
+ // If the exception itself causes serialization issues, create a sanitized version
+ var sanitizedException = new Exception($"[Sanitized Exception] {exception.GetType().Name}: {exception.Message}");
+ _awsxRayRecorder.AddException(sanitizedException);
+ }
+ }
}
///
@@ -161,6 +298,429 @@ public void AddException(Exception exception)
public void AddHttpInformation(string key, object value)
{
if (_isLambda)
- _awsxRayRecorder.AddHttpInformation(key, value);
+ {
+ var sanitizedValue = SanitizeValueForMetadata(value);
+ _awsxRayRecorder.AddHttpInformation(key, sanitizedValue);
+ }
+ }
+
+ ///
+ /// Sanitizes annotation values to ensure they are supported by X-Ray.
+ /// X-Ray annotations only support: string, int, long, double, float, bool
+ ///
+ /// The value to sanitize
+ /// A sanitized value safe for X-Ray annotations
+ private static object SanitizeValueForAnnotation(object value)
+ {
+ if (value == null)
+ return null;
+
+ var type = value.GetType();
+
+ // X-Ray supported annotation types: string, int, long, double, float, bool
+ if (type == typeof(string) ||
+ type == typeof(int) ||
+ type == typeof(long) ||
+ type == typeof(double) ||
+ type == typeof(float) ||
+ type == typeof(bool))
+ {
+ return value;
+ }
+
+ // Convert all other types to string
+ return value.ToString();
+ }
+
+ ///
+ /// Sanitizes metadata values to ensure they can be serialized by X-Ray.
+ /// This method recursively processes complex objects to handle problematic types.
+ ///
+ /// The value to sanitize
+ /// A sanitized value safe for X-Ray metadata serialization
+ private static object SanitizeValueForMetadata(object value)
+ {
+ try
+ {
+ return SanitizeValueRecursive(value, 0);
+ }
+ catch (Exception ex)
+ {
+ // If sanitization fails, return a safe string representation
+ // This ensures we don't break the tracing functionality
+ return $"[Sanitization failed: {ex.Message}] {value?.ToString() ?? "null"}";
+ }
+ }
+
+ ///
+ /// Recursively sanitizes values with depth protection to prevent infinite recursion.
+ ///
+ /// The value to sanitize
+ /// Current recursion depth
+ /// A sanitized value
+ private static object SanitizeValueRecursive(object value, int depth)
+ {
+ // Prevent infinite recursion
+ if (depth > MaxSanitizationDepth)
+ return "[Max depth reached]";
+
+ if (value == null)
+ return null;
+
+ var type = value.GetType();
+
+ // Handle primitive types and strings - only convert problematic ones
+ if (type.IsPrimitive || type == typeof(string) || type == typeof(decimal))
+ {
+ // Handle problematic numeric types that cause JSON serialization issues
+ if (type == typeof(IntPtr) || type == typeof(UIntPtr))
+ return value.ToString();
+
+ // Handle unsigned types that might cause issues with LitJson
+ if (type == typeof(uint) || type == typeof(ulong) || type == typeof(ushort) || type == typeof(byte) || type == typeof(sbyte))
+ return value.ToString();
+
+ // Keep safe primitive types as-is
+ return value;
+ }
+
+ // Handle DateTime and TimeSpan - these can cause serialization issues
+ if (type == typeof(DateTime))
+ return ((DateTime)value).ToString("O"); // ISO 8601 format
+
+ if (type == typeof(TimeSpan))
+ return ((TimeSpan)value).ToString();
+
+ // Handle Guid - convert to string for safety
+ if (type == typeof(Guid))
+ return value.ToString();
+
+ // Handle enums - convert to string for safety
+ if (type.IsEnum)
+ return value.ToString();
+
+ // Handle arrays - only sanitize if elements need sanitization
+ if (type.IsArray)
+ {
+ var array = (Array)value;
+ var elementType = type.GetElementType();
+
+ // If it's an array of safe types, return as-is
+ if (elementType != null && IsSafeType(elementType))
+ {
+ // Check if all elements are actually safe
+ bool allElementsSafe = true;
+ for (int i = 0; i < array.Length; i++)
+ {
+ var element = array.GetValue(i);
+ if (element != null && NeedsTypeSanitization(element.GetType()))
+ {
+ allElementsSafe = false;
+ break;
+ }
+ }
+
+ if (allElementsSafe)
+ return value; // Return original array
+ }
+
+ // Otherwise, sanitize to object array
+ var sanitizedArray = new object[array.Length];
+ for (int i = 0; i < array.Length; i++)
+ {
+ sanitizedArray[i] = SanitizeValueRecursive(array.GetValue(i), depth + 1);
+ }
+ return sanitizedArray;
+ }
+
+ // Handle dictionaries - always sanitize for maximum safety
+ if (value is System.Collections.IDictionary dict)
+ {
+ var sanitizedDict = new System.Collections.Generic.Dictionary();
+ foreach (System.Collections.DictionaryEntry entry in dict)
+ {
+ var key = entry.Key?.ToString() ?? "null";
+ sanitizedDict[key] = SanitizeValueRecursive(entry.Value, depth + 1);
+ }
+ return sanitizedDict;
+ }
+
+ // Handle other collections (List, etc.) - always sanitize for maximum safety
+ if (value is System.Collections.IEnumerable enumerable && !(value is string))
+ {
+ var sanitizedList = new System.Collections.Generic.List