From ff6e407e4c31a4ee8f4c65ad6296e72a2178012b Mon Sep 17 00:00:00 2001 From: maliming Date: Wed, 15 Apr 2026 14:32:23 +0800 Subject: [PATCH] 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() { } + } + } +}