Browse Source

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[...]]
pull/25267/head
maliming 3 weeks ago
parent
commit
ff6e407e4c
No known key found for this signature in database GPG Key ID: A646B9CB645ECEA4
  1. 26
      framework/src/Volo.Abp.Core/Microsoft/Extensions/Logging/AbpLoggerExtensions.cs
  2. 155
      framework/test/Volo.Abp.Core.Tests/Microsoft/Extensions/Logging/AbpLoggerExtensions_Tests.cs

26
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<IExceptionWithSelfLogging>();

155
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<string, object>
{
{ "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<Dictionary<string, object>>
{
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<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
{
LastLoggedMessage = formatter(state, exception);
}
public bool IsEnabled(LogLevel logLevel) => true;
public IDisposable BeginScope<TState>(TState state) where TState : notnull => NullDisposable.Instance;
private class NullDisposable : IDisposable
{
public static readonly NullDisposable Instance = new();
public void Dispose() { }
}
}
}
Loading…
Cancel
Save