From ff6e407e4c31a4ee8f4c65ad6296e72a2178012b Mon Sep 17 00:00:00 2001 From: maliming Date: Wed, 15 Apr 2026 14:32:23 +0800 Subject: [PATCH 1/5] Serialize complex objects in exception data logging Use JsonSerializer for non-primitive types in AbpLoggerExtensions.LogData to output meaningful JSON instead of type names like List`1[Dictionary`2[...]] --- .../Extensions/Logging/AbpLoggerExtensions.cs | 26 ++- .../Logging/AbpLoggerExtensions_Tests.cs | 155 ++++++++++++++++++ 2 files changed, 180 insertions(+), 1 deletion(-) create mode 100644 framework/test/Volo.Abp.Core.Tests/Microsoft/Extensions/Logging/AbpLoggerExtensions_Tests.cs diff --git a/framework/src/Volo.Abp.Core/Microsoft/Extensions/Logging/AbpLoggerExtensions.cs b/framework/src/Volo.Abp.Core/Microsoft/Extensions/Logging/AbpLoggerExtensions.cs index e1899f7efc..fb2d619f97 100644 --- a/framework/src/Volo.Abp.Core/Microsoft/Extensions/Logging/AbpLoggerExtensions.cs +++ b/framework/src/Volo.Abp.Core/Microsoft/Extensions/Logging/AbpLoggerExtensions.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Text; +using System.Text.Json; using Volo.Abp.ExceptionHandling; using Volo.Abp.Logging; @@ -92,12 +93,35 @@ public static class AbpLoggerExtensions exceptionData.AppendLine("---------- Exception Data ----------"); foreach (var key in exception.Data.Keys) { - exceptionData.AppendLine($"{key} = {exception.Data[key]}"); + exceptionData.AppendLine($"{key} = {FormatDataValue(exception.Data[key])}"); } logger.LogWithLevel(logLevel, exceptionData.ToString()); } + private static string FormatDataValue(object? value) + { + if (value == null) + { + return string.Empty; + } + + var type = value.GetType(); + if (value is string || type.IsPrimitive || value is decimal || value is DateTime || value is DateTimeOffset || value is Guid || type.IsEnum) + { + return value.ToString()!; + } + + try + { + return JsonSerializer.Serialize(value); + } + catch + { + return value.ToString()!; + } + } + private static void LogSelfLogging(ILogger logger, Exception exception) { var loggingExceptions = new List(); diff --git a/framework/test/Volo.Abp.Core.Tests/Microsoft/Extensions/Logging/AbpLoggerExtensions_Tests.cs b/framework/test/Volo.Abp.Core.Tests/Microsoft/Extensions/Logging/AbpLoggerExtensions_Tests.cs new file mode 100644 index 0000000000..7d79136388 --- /dev/null +++ b/framework/test/Volo.Abp.Core.Tests/Microsoft/Extensions/Logging/AbpLoggerExtensions_Tests.cs @@ -0,0 +1,155 @@ +using System; +using System.Collections.Generic; +using Shouldly; +using Xunit; + +namespace Microsoft.Extensions.Logging; + +public class AbpLoggerExtensions_Tests +{ + [Fact] + public void LogException_Should_Format_String_Data() + { + var logger = new FakeLogger(); + var exception = new Exception("test"); + exception.Data["Name"] = "John"; + + logger.LogException(exception); + + logger.LastLoggedMessage.ShouldContain("Name = John"); + } + + [Fact] + public void LogException_Should_Format_Primitive_Data() + { + var logger = new FakeLogger(); + var exception = new Exception("test"); + exception.Data["Count"] = 42; + exception.Data["IsActive"] = true; + + logger.LogException(exception); + + logger.LastLoggedMessage.ShouldContain("Count = 42"); + logger.LastLoggedMessage.ShouldContain("IsActive = True"); + } + + [Fact] + public void LogException_Should_Format_Complex_Object_As_Json() + { + var logger = new FakeLogger(); + var exception = new Exception("test"); + exception.Data["Details"] = new Dictionary + { + { "RuleName", "FixedWindow" }, + { "Limit", 10 } + }; + + logger.LogException(exception); + + logger.LastLoggedMessage.ShouldContain("\"RuleName\":\"FixedWindow\""); + logger.LastLoggedMessage.ShouldContain("\"Limit\":10"); + } + + [Fact] + public void LogException_Should_Format_List_Of_Complex_Objects_As_Json() + { + var logger = new FakeLogger(); + var exception = new Exception("test"); + exception.Data["RuleDetails"] = new List> + { + new() { { "RuleName", "FixedWindow" }, { "Limit", 10 } }, + new() { { "RuleName", "SlidingWindow" }, { "Limit", 20 } } + }; + + logger.LogException(exception); + + var message = logger.LastLoggedMessage; + message.ShouldNotContain("System.Collections.Generic.List"); + message.ShouldContain("FixedWindow"); + message.ShouldContain("SlidingWindow"); + } + + [Fact] + public void LogException_Should_Format_Null_Data_As_Empty() + { + var logger = new FakeLogger(); + var exception = new Exception("test"); + exception.Data["NullKey"] = null; + + logger.LogException(exception); + + logger.LastLoggedMessage.ShouldContain("NullKey = "); + } + + [Fact] + public void LogException_Should_Format_Enum_Data() + { + var logger = new FakeLogger(); + var exception = new Exception("test"); + exception.Data["Level"] = LogLevel.Warning; + + logger.LogException(exception); + + logger.LastLoggedMessage.ShouldContain("Level = Warning"); + } + + [Fact] + public void LogException_Should_Format_DateTime_Data() + { + var logger = new FakeLogger(); + var exception = new Exception("test"); + var now = new DateTime(2025, 1, 15, 10, 30, 0); + exception.Data["Timestamp"] = now; + + logger.LogException(exception); + + logger.LastLoggedMessage.ShouldContain($"Timestamp = {now}"); + } + + [Fact] + public void LogException_Should_Format_Guid_Data() + { + var logger = new FakeLogger(); + var exception = new Exception("test"); + var id = Guid.NewGuid(); + exception.Data["Id"] = id; + + logger.LogException(exception); + + logger.LastLoggedMessage.ShouldContain($"Id = {id}"); + } + + [Fact] + public void LogException_Should_Format_Anonymous_Object_As_Json() + { + var logger = new FakeLogger(); + var exception = new Exception("test"); + exception.Data["Info"] = new { Name = "Test", Value = 123 }; + + logger.LogException(exception); + + var message = logger.LastLoggedMessage; + message.ShouldContain("\"Name\":\"Test\""); + message.ShouldContain("\"Value\":123"); + } + + private class FakeLogger : ILogger + { + public string? LastLoggedMessage { get; private set; } + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) + { + LastLoggedMessage = formatter(state, exception); + } + + public bool IsEnabled(LogLevel logLevel) => true; + + public IDisposable BeginScope(TState state) where TState : notnull => NullDisposable.Instance; + + private class NullDisposable : IDisposable + { + public static readonly NullDisposable Instance = new(); + public void Dispose() { } + } + } +} From 262543a9e4308f76693e7693a5966a01ea5313fd Mon Sep 17 00:00:00 2001 From: maliming Date: Wed, 15 Apr 2026 14:43:46 +0800 Subject: [PATCH 2/5] Add truncation for large JSON output and fallback test coverage --- .../Extensions/Logging/AbpLoggerExtensions.cs | 10 ++++- .../Logging/AbpLoggerExtensions_Tests.cs | 42 +++++++++++++++++++ 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/framework/src/Volo.Abp.Core/Microsoft/Extensions/Logging/AbpLoggerExtensions.cs b/framework/src/Volo.Abp.Core/Microsoft/Extensions/Logging/AbpLoggerExtensions.cs index fb2d619f97..972fb3f92e 100644 --- a/framework/src/Volo.Abp.Core/Microsoft/Extensions/Logging/AbpLoggerExtensions.cs +++ b/framework/src/Volo.Abp.Core/Microsoft/Extensions/Logging/AbpLoggerExtensions.cs @@ -99,6 +99,8 @@ public static class AbpLoggerExtensions logger.LogWithLevel(logLevel, exceptionData.ToString()); } + private const int MaxDataValueLength = 4096; + private static string FormatDataValue(object? value) { if (value == null) @@ -114,7 +116,13 @@ public static class AbpLoggerExtensions try { - return JsonSerializer.Serialize(value); + var json = JsonSerializer.Serialize(value); + if (json.Length > MaxDataValueLength) + { + return json.Substring(0, MaxDataValueLength) + "...(truncated)"; + } + + return json; } catch { diff --git a/framework/test/Volo.Abp.Core.Tests/Microsoft/Extensions/Logging/AbpLoggerExtensions_Tests.cs b/framework/test/Volo.Abp.Core.Tests/Microsoft/Extensions/Logging/AbpLoggerExtensions_Tests.cs index 7d79136388..a4c52a2e6f 100644 --- a/framework/test/Volo.Abp.Core.Tests/Microsoft/Extensions/Logging/AbpLoggerExtensions_Tests.cs +++ b/framework/test/Volo.Abp.Core.Tests/Microsoft/Extensions/Logging/AbpLoggerExtensions_Tests.cs @@ -133,6 +133,48 @@ public class AbpLoggerExtensions_Tests message.ShouldContain("\"Value\":123"); } + [Fact] + public void LogException_Should_Fallback_To_ToString_For_Non_Serializable_Object() + { + var logger = new FakeLogger(); + var exception = new Exception("test"); + var selfRef = new SelfReferencingObject { Name = "Loop" }; + selfRef.Self = selfRef; + exception.Data["BadObject"] = selfRef; + + logger.LogException(exception); + + logger.LastLoggedMessage.ShouldNotBeNull(); + logger.LastLoggedMessage.ShouldContain("BadObject = "); + } + + [Fact] + public void LogException_Should_Truncate_Large_Json_Output() + { + var logger = new FakeLogger(); + var exception = new Exception("test"); + var largeList = new List>(); + for (var i = 0; i < 500; i++) + { + largeList.Add(new Dictionary + { + { "Key", new string('x', 100) } + }); + } + exception.Data["LargeData"] = largeList; + + logger.LogException(exception); + + logger.LastLoggedMessage.ShouldNotBeNull(); + logger.LastLoggedMessage.ShouldContain("...(truncated)"); + } + + private class SelfReferencingObject + { + public string Name { get; set; } = default!; + public SelfReferencingObject? Self { get; set; } + } + private class FakeLogger : ILogger { public string? LastLoggedMessage { get; private set; } From aa3c7723f05eff800a6bf47abc72e1f49edf07ed Mon Sep 17 00:00:00 2001 From: maliming Date: Wed, 15 Apr 2026 15:06:06 +0800 Subject: [PATCH 3/5] Fix truncation length and strengthen fallback test assertion --- .../Microsoft/Extensions/Logging/AbpLoggerExtensions.cs | 3 ++- .../Microsoft/Extensions/Logging/AbpLoggerExtensions_Tests.cs | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/framework/src/Volo.Abp.Core/Microsoft/Extensions/Logging/AbpLoggerExtensions.cs b/framework/src/Volo.Abp.Core/Microsoft/Extensions/Logging/AbpLoggerExtensions.cs index 972fb3f92e..a9dbe57edb 100644 --- a/framework/src/Volo.Abp.Core/Microsoft/Extensions/Logging/AbpLoggerExtensions.cs +++ b/framework/src/Volo.Abp.Core/Microsoft/Extensions/Logging/AbpLoggerExtensions.cs @@ -100,6 +100,7 @@ public static class AbpLoggerExtensions } private const int MaxDataValueLength = 4096; + private const string TruncationSuffix = "...(truncated)"; private static string FormatDataValue(object? value) { @@ -119,7 +120,7 @@ public static class AbpLoggerExtensions var json = JsonSerializer.Serialize(value); if (json.Length > MaxDataValueLength) { - return json.Substring(0, MaxDataValueLength) + "...(truncated)"; + return json.Substring(0, MaxDataValueLength - TruncationSuffix.Length) + TruncationSuffix; } return json; diff --git a/framework/test/Volo.Abp.Core.Tests/Microsoft/Extensions/Logging/AbpLoggerExtensions_Tests.cs b/framework/test/Volo.Abp.Core.Tests/Microsoft/Extensions/Logging/AbpLoggerExtensions_Tests.cs index a4c52a2e6f..8e49afe16d 100644 --- a/framework/test/Volo.Abp.Core.Tests/Microsoft/Extensions/Logging/AbpLoggerExtensions_Tests.cs +++ b/framework/test/Volo.Abp.Core.Tests/Microsoft/Extensions/Logging/AbpLoggerExtensions_Tests.cs @@ -145,7 +145,8 @@ public class AbpLoggerExtensions_Tests logger.LogException(exception); logger.LastLoggedMessage.ShouldNotBeNull(); - logger.LastLoggedMessage.ShouldContain("BadObject = "); + logger.LastLoggedMessage.ShouldContain("BadObject = SelfRef:Loop"); + logger.LastLoggedMessage.ShouldNotContain("\"Name\""); } [Fact] @@ -172,6 +173,7 @@ public class AbpLoggerExtensions_Tests private class SelfReferencingObject { public string Name { get; set; } = default!; + public override string ToString() => $"SelfRef:{Name}"; public SelfReferencingObject? Self { get; set; } } From 02c7dae2a1272b29805de44aa6cb4830587a835e Mon Sep 17 00:00:00 2001 From: maliming Date: Wed, 15 Apr 2026 15:14:52 +0800 Subject: [PATCH 4/5] Use TruncateWithPostfix for JSON truncation in FormatDataValue --- .../Microsoft/Extensions/Logging/AbpLoggerExtensions.cs | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/framework/src/Volo.Abp.Core/Microsoft/Extensions/Logging/AbpLoggerExtensions.cs b/framework/src/Volo.Abp.Core/Microsoft/Extensions/Logging/AbpLoggerExtensions.cs index a9dbe57edb..bf52211a39 100644 --- a/framework/src/Volo.Abp.Core/Microsoft/Extensions/Logging/AbpLoggerExtensions.cs +++ b/framework/src/Volo.Abp.Core/Microsoft/Extensions/Logging/AbpLoggerExtensions.cs @@ -100,7 +100,6 @@ public static class AbpLoggerExtensions } private const int MaxDataValueLength = 4096; - private const string TruncationSuffix = "...(truncated)"; private static string FormatDataValue(object? value) { @@ -117,13 +116,7 @@ public static class AbpLoggerExtensions try { - var json = JsonSerializer.Serialize(value); - if (json.Length > MaxDataValueLength) - { - return json.Substring(0, MaxDataValueLength - TruncationSuffix.Length) + TruncationSuffix; - } - - return json; + return JsonSerializer.Serialize(value).TruncateWithPostfix(MaxDataValueLength, "...(truncated)")!; } catch { From f3042d42f532177bdfd9d977902ab32ec0395731 Mon Sep 17 00:00:00 2001 From: maliming Date: Wed, 15 Apr 2026 15:26:08 +0800 Subject: [PATCH 5/5] Fix culture-sensitive DateTime assertion in test --- .../Microsoft/Extensions/Logging/AbpLoggerExtensions_Tests.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/framework/test/Volo.Abp.Core.Tests/Microsoft/Extensions/Logging/AbpLoggerExtensions_Tests.cs b/framework/test/Volo.Abp.Core.Tests/Microsoft/Extensions/Logging/AbpLoggerExtensions_Tests.cs index 8e49afe16d..dbaabd3a63 100644 --- a/framework/test/Volo.Abp.Core.Tests/Microsoft/Extensions/Logging/AbpLoggerExtensions_Tests.cs +++ b/framework/test/Volo.Abp.Core.Tests/Microsoft/Extensions/Logging/AbpLoggerExtensions_Tests.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Globalization; using Shouldly; using Xunit; @@ -103,7 +104,7 @@ public class AbpLoggerExtensions_Tests logger.LogException(exception); - logger.LastLoggedMessage.ShouldContain($"Timestamp = {now}"); + logger.LastLoggedMessage.ShouldContain($"Timestamp = {now.ToString(CultureInfo.CurrentCulture)}"); } [Fact]