feat(logging): 增强日志配置加载器功能

- 添加 JsonStringEnumConverter 支持枚举的驼峰命名转换
- 实现 ConfigurableLoggerFactory 的 IDisposable 接口确保资源正确释放
- 支持日志级别配置的前缀匹配功能(命名空间层级匹配)
- 优化测试代码中的资源管理,使用 using 语句确保对象正确释放
- 修复 JsonLogFormatter 测试中的属性访问逻辑,使用 TryGetProperty 安全访问
- 将测试中的异常断言从 ArgumentNullException 更新为 ArgumentException
This commit is contained in:
GeWuYou 2026-02-26 19:14:18 +08:00 committed by gewuyou
parent abdf4cc690
commit 466aae49ec
7 changed files with 106 additions and 29 deletions

View File

@ -11,9 +11,9 @@ namespace GFramework.Core.Tests.logging;
public class CompositeFilterTests public class CompositeFilterTests
{ {
[Test] [Test]
public void Constructor_WithNullFilters_ShouldThrowArgumentNullException() public void Constructor_WithNullFilters_ShouldThrowArgumentException()
{ {
Assert.Throws<ArgumentNullException>(() => new CompositeFilter(null!)); Assert.Throws<ArgumentException>(() => new CompositeFilter(null!));
} }
[Test] [Test]

View File

@ -159,11 +159,13 @@ public class FileAppenderTests
[Test] [Test]
public void Flush_ShouldEnsureDataWritten() public void Flush_ShouldEnsureDataWritten()
{ {
using var appender = new FileAppender(_testFilePath); using (var appender = new FileAppender(_testFilePath))
var entry = new LogEntry(DateTime.Now, LogLevel.Info, "TestLogger", "Test message", null, null); {
var entry = new LogEntry(DateTime.Now, LogLevel.Info, "TestLogger", "Test message", null, null);
appender.Append(entry); appender.Append(entry);
appender.Flush(); appender.Flush();
}
// 立即读取文件应该能看到内容 // 立即读取文件应该能看到内容
var content = File.ReadAllText(_testFilePath); var content = File.ReadAllText(_testFilePath);

View File

@ -65,11 +65,29 @@ public class JsonLogFormatterTests
var result = _formatter.Format(entry); var result = _formatter.Format(entry);
var doc = JsonDocument.Parse(result); var doc = JsonDocument.Parse(result);
var propsObj = doc.RootElement.GetProperty("properties"); if (doc.RootElement.TryGetProperty("properties", out var propsObj))
{
// 使用 TryGetProperty 来安全访问属性
Assert.That(
propsObj.TryGetProperty("userId", out var userIdProp) ||
propsObj.TryGetProperty("UserId", out userIdProp), Is.True,
$"userId/UserId not found. Available properties: {string.Join(", ", propsObj.EnumerateObject().Select(p => p.Name))}");
Assert.That(userIdProp.GetInt32(), Is.EqualTo(12345));
Assert.That(propsObj.GetProperty("userId").GetInt32(), Is.EqualTo(12345)); Assert.That(
Assert.That(propsObj.GetProperty("userName").GetString(), Is.EqualTo("TestUser")); propsObj.TryGetProperty("userName", out var userNameProp) ||
Assert.That(propsObj.GetProperty("isActive").GetBoolean(), Is.True); propsObj.TryGetProperty("UserName", out userNameProp), Is.True);
Assert.That(userNameProp.GetString(), Is.EqualTo("TestUser"));
Assert.That(
propsObj.TryGetProperty("isActive", out var isActiveProp) ||
propsObj.TryGetProperty("IsActive", out isActiveProp), Is.True);
Assert.That(isActiveProp.GetBoolean(), Is.True);
}
else
{
Assert.Fail($"Properties object should be present when properties are provided. JSON: {result}");
}
} }
[Test] [Test]
@ -77,16 +95,32 @@ public class JsonLogFormatterTests
{ {
var properties = new Dictionary<string, object?> var properties = new Dictionary<string, object?>
{ {
["Key1"] = null ["Key1"] = null,
["Key2"] = "value"
}; };
var entry = new LogEntry(DateTime.Now, LogLevel.Info, "TestLogger", "Test message", null, properties); var entry = new LogEntry(DateTime.Now, LogLevel.Info, "TestLogger", "Test message", null, properties);
var result = _formatter.Format(entry); var result = _formatter.Format(entry);
var doc = JsonDocument.Parse(result); var doc = JsonDocument.Parse(result);
var propsObj = doc.RootElement.GetProperty("properties"); if (doc.RootElement.TryGetProperty("properties", out var propsObj))
{
// 使用 TryGetProperty 来安全访问属性
Assert.That(
propsObj.TryGetProperty("key1", out var key1Prop) || propsObj.TryGetProperty("Key1", out key1Prop),
Is.True,
$"key1/Key1 not found. Available properties: {string.Join(", ", propsObj.EnumerateObject().Select(p => p.Name))}");
Assert.That(key1Prop.ValueKind, Is.EqualTo(JsonValueKind.Null));
Assert.That(propsObj.GetProperty("key1").ValueKind, Is.EqualTo(JsonValueKind.Null)); Assert.That(
propsObj.TryGetProperty("key2", out var key2Prop) || propsObj.TryGetProperty("Key2", out key2Prop),
Is.True);
Assert.That(key2Prop.GetString(), Is.EqualTo("value"));
}
else
{
Assert.Fail($"Properties object should be present when properties are provided. JSON: {result}");
}
} }
[Test] [Test]

View File

@ -183,11 +183,23 @@ public class LoggingConfigurationTests
}}"; }}";
var config = LoggingConfigurationLoader.LoadFromJsonString(json); var config = LoggingConfigurationLoader.LoadFromJsonString(json);
var factory = LoggingConfigurationLoader.CreateFactory(config); ILoggerFactory? factory = null;
try
{
factory = LoggingConfigurationLoader.CreateFactory(config);
var logger = factory.GetLogger("TestLogger"); var logger = factory.GetLogger("TestLogger");
logger.Info("Info message"); logger.Info("Info message");
logger.Warn("Warning message"); logger.Warn("Warning message");
}
finally
{
// 确保释放 factory 和所有 appenders
if (factory is IDisposable disposable)
{
disposable.Dispose();
}
}
// 只有 Warning 应该被写入 // 只有 Warning 应该被写入
var content = File.ReadAllText(testFile); var content = File.ReadAllText(testFile);

View File

@ -11,9 +11,9 @@ namespace GFramework.Core.Tests.logging;
public class NamespaceFilterTests public class NamespaceFilterTests
{ {
[Test] [Test]
public void Constructor_WithNullNamespaces_ShouldThrowArgumentNullException() public void Constructor_WithNullNamespaces_ShouldThrowArgumentException()
{ {
Assert.Throws<ArgumentNullException>(() => new NamespaceFilter(null!)); Assert.Throws<ArgumentException>(() => new NamespaceFilter(null!));
} }
[Test] [Test]

View File

@ -151,11 +151,13 @@ public class RollingFileAppenderTests
[Test] [Test]
public void Flush_ShouldEnsureDataWritten() public void Flush_ShouldEnsureDataWritten()
{ {
using var appender = new RollingFileAppender(_testFilePath); using (var appender = new RollingFileAppender(_testFilePath))
var entry = new LogEntry(DateTime.Now, LogLevel.Info, "TestLogger", "Test message", null, null); {
var entry = new LogEntry(DateTime.Now, LogLevel.Info, "TestLogger", "Test message", null, null);
appender.Append(entry); appender.Append(entry);
appender.Flush(); appender.Flush();
}
Assert.That(File.Exists(_testFilePath), Is.True); Assert.That(File.Exists(_testFilePath), Is.True);
var content = File.ReadAllText(_testFilePath); var content = File.ReadAllText(_testFilePath);

View File

@ -1,5 +1,6 @@
using System.IO; using System.IO;
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Serialization;
using GFramework.Core.Abstractions.logging; using GFramework.Core.Abstractions.logging;
using GFramework.Core.logging.appenders; using GFramework.Core.logging.appenders;
using GFramework.Core.logging.filters; using GFramework.Core.logging.filters;
@ -16,7 +17,8 @@ public static class LoggingConfigurationLoader
{ {
PropertyNameCaseInsensitive = true, PropertyNameCaseInsensitive = true,
ReadCommentHandling = JsonCommentHandling.Skip, ReadCommentHandling = JsonCommentHandling.Skip,
AllowTrailingCommas = true AllowTrailingCommas = true,
Converters = { new JsonStringEnumConverter(JsonNamingPolicy.CamelCase, allowIntegerValues: true) }
}; };
/// <summary> /// <summary>
@ -129,10 +131,11 @@ public static class LoggingConfigurationLoader
/// <summary> /// <summary>
/// 可配置的 Logger 工厂 /// 可配置的 Logger 工厂
/// </summary> /// </summary>
internal sealed class ConfigurableLoggerFactory : ILoggerFactory internal sealed class ConfigurableLoggerFactory : ILoggerFactory, IDisposable
{ {
private readonly ILogAppender[] _appenders; private readonly ILogAppender[] _appenders;
private readonly LoggingConfiguration _config; private readonly LoggingConfiguration _config;
private bool _disposed;
public ConfigurableLoggerFactory(LoggingConfiguration config) public ConfigurableLoggerFactory(LoggingConfiguration config)
{ {
@ -140,12 +143,36 @@ internal sealed class ConfigurableLoggerFactory : ILoggerFactory
_appenders = config.Appenders.Select(LoggingConfigurationLoader.CreateAppender).ToArray(); _appenders = config.Appenders.Select(LoggingConfigurationLoader.CreateAppender).ToArray();
} }
public void Dispose()
{
if (_disposed)
return;
foreach (var appender in _appenders)
{
if (appender is IDisposable disposable)
{
disposable.Dispose();
}
}
_disposed = true;
}
public ILogger GetLogger(string name, LogLevel minLevel = LogLevel.Info) public ILogger GetLogger(string name, LogLevel minLevel = LogLevel.Info)
{ {
// 检查是否有特定 Logger 的级别配置 // 检查是否有特定 Logger 的级别配置(支持前缀匹配)
var effectiveLevel = _config.LoggerLevels.TryGetValue(name, out var level) var effectiveLevel = _config.MinLevel;
? level
: _config.MinLevel; foreach (var kvp in _config.LoggerLevels)
{
// 精确匹配或前缀匹配(命名空间层级)
if (name == kvp.Key || name.StartsWith(kvp.Key + ".", StringComparison.Ordinal))
{
effectiveLevel = kvp.Value;
break;
}
}
// 如果没有 Appender返回简单的 ConsoleLogger // 如果没有 Appender返回简单的 ConsoleLogger
if (_appenders.Length == 0) if (_appenders.Length == 0)