From 466aae49ecf66d20e0873792b78a62d3101b60fa Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Thu, 26 Feb 2026 19:14:18 +0800 Subject: [PATCH] =?UTF-8?q?feat(logging):=20=E5=A2=9E=E5=BC=BA=E6=97=A5?= =?UTF-8?q?=E5=BF=97=E9=85=8D=E7=BD=AE=E5=8A=A0=E8=BD=BD=E5=99=A8=E5=8A=9F?= =?UTF-8?q?=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加 JsonStringEnumConverter 支持枚举的驼峰命名转换 - 实现 ConfigurableLoggerFactory 的 IDisposable 接口确保资源正确释放 - 支持日志级别配置的前缀匹配功能(命名空间层级匹配) - 优化测试代码中的资源管理,使用 using 语句确保对象正确释放 - 修复 JsonLogFormatter 测试中的属性访问逻辑,使用 TryGetProperty 安全访问 - 将测试中的异常断言从 ArgumentNullException 更新为 ArgumentException --- .../logging/CompositeFilterTests.cs | 4 +- .../logging/FileAppenderTests.cs | 10 ++-- .../logging/JsonLogFormatterTests.cs | 48 ++++++++++++++++--- .../logging/LoggingConfigurationTests.cs | 20 ++++++-- .../logging/NamespaceFilterTests.cs | 4 +- .../logging/RollingFileAppenderTests.cs | 10 ++-- .../logging/LoggingConfigurationLoader.cs | 39 ++++++++++++--- 7 files changed, 106 insertions(+), 29 deletions(-) diff --git a/GFramework.Core.Tests/logging/CompositeFilterTests.cs b/GFramework.Core.Tests/logging/CompositeFilterTests.cs index a14462f..ead293e 100644 --- a/GFramework.Core.Tests/logging/CompositeFilterTests.cs +++ b/GFramework.Core.Tests/logging/CompositeFilterTests.cs @@ -11,9 +11,9 @@ namespace GFramework.Core.Tests.logging; public class CompositeFilterTests { [Test] - public void Constructor_WithNullFilters_ShouldThrowArgumentNullException() + public void Constructor_WithNullFilters_ShouldThrowArgumentException() { - Assert.Throws(() => new CompositeFilter(null!)); + Assert.Throws(() => new CompositeFilter(null!)); } [Test] diff --git a/GFramework.Core.Tests/logging/FileAppenderTests.cs b/GFramework.Core.Tests/logging/FileAppenderTests.cs index 9c2cd24..c7d51e5 100644 --- a/GFramework.Core.Tests/logging/FileAppenderTests.cs +++ b/GFramework.Core.Tests/logging/FileAppenderTests.cs @@ -159,11 +159,13 @@ public class FileAppenderTests [Test] public void Flush_ShouldEnsureDataWritten() { - using var appender = new FileAppender(_testFilePath); - var entry = new LogEntry(DateTime.Now, LogLevel.Info, "TestLogger", "Test message", null, null); + using (var appender = new FileAppender(_testFilePath)) + { + var entry = new LogEntry(DateTime.Now, LogLevel.Info, "TestLogger", "Test message", null, null); - appender.Append(entry); - appender.Flush(); + appender.Append(entry); + appender.Flush(); + } // 立即读取文件应该能看到内容 var content = File.ReadAllText(_testFilePath); diff --git a/GFramework.Core.Tests/logging/JsonLogFormatterTests.cs b/GFramework.Core.Tests/logging/JsonLogFormatterTests.cs index 2cf8ffb..f056335 100644 --- a/GFramework.Core.Tests/logging/JsonLogFormatterTests.cs +++ b/GFramework.Core.Tests/logging/JsonLogFormatterTests.cs @@ -65,11 +65,29 @@ public class JsonLogFormatterTests var result = _formatter.Format(entry); 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(propsObj.GetProperty("userName").GetString(), Is.EqualTo("TestUser")); - Assert.That(propsObj.GetProperty("isActive").GetBoolean(), Is.True); + Assert.That( + propsObj.TryGetProperty("userName", out var userNameProp) || + 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] @@ -77,16 +95,32 @@ public class JsonLogFormatterTests { var properties = new Dictionary { - ["Key1"] = null + ["Key1"] = null, + ["Key2"] = "value" }; var entry = new LogEntry(DateTime.Now, LogLevel.Info, "TestLogger", "Test message", null, properties); var result = _formatter.Format(entry); 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] diff --git a/GFramework.Core.Tests/logging/LoggingConfigurationTests.cs b/GFramework.Core.Tests/logging/LoggingConfigurationTests.cs index 69afccf..ae2910d 100644 --- a/GFramework.Core.Tests/logging/LoggingConfigurationTests.cs +++ b/GFramework.Core.Tests/logging/LoggingConfigurationTests.cs @@ -183,11 +183,23 @@ public class LoggingConfigurationTests }}"; var config = LoggingConfigurationLoader.LoadFromJsonString(json); - var factory = LoggingConfigurationLoader.CreateFactory(config); + ILoggerFactory? factory = null; + try + { + factory = LoggingConfigurationLoader.CreateFactory(config); - var logger = factory.GetLogger("TestLogger"); - logger.Info("Info message"); - logger.Warn("Warning message"); + var logger = factory.GetLogger("TestLogger"); + logger.Info("Info message"); + logger.Warn("Warning message"); + } + finally + { + // 确保释放 factory 和所有 appenders + if (factory is IDisposable disposable) + { + disposable.Dispose(); + } + } // 只有 Warning 应该被写入 var content = File.ReadAllText(testFile); diff --git a/GFramework.Core.Tests/logging/NamespaceFilterTests.cs b/GFramework.Core.Tests/logging/NamespaceFilterTests.cs index 3e7dd4f..2765046 100644 --- a/GFramework.Core.Tests/logging/NamespaceFilterTests.cs +++ b/GFramework.Core.Tests/logging/NamespaceFilterTests.cs @@ -11,9 +11,9 @@ namespace GFramework.Core.Tests.logging; public class NamespaceFilterTests { [Test] - public void Constructor_WithNullNamespaces_ShouldThrowArgumentNullException() + public void Constructor_WithNullNamespaces_ShouldThrowArgumentException() { - Assert.Throws(() => new NamespaceFilter(null!)); + Assert.Throws(() => new NamespaceFilter(null!)); } [Test] diff --git a/GFramework.Core.Tests/logging/RollingFileAppenderTests.cs b/GFramework.Core.Tests/logging/RollingFileAppenderTests.cs index 8fdab20..cdeeb7c 100644 --- a/GFramework.Core.Tests/logging/RollingFileAppenderTests.cs +++ b/GFramework.Core.Tests/logging/RollingFileAppenderTests.cs @@ -151,11 +151,13 @@ public class RollingFileAppenderTests [Test] public void Flush_ShouldEnsureDataWritten() { - using var appender = new RollingFileAppender(_testFilePath); - var entry = new LogEntry(DateTime.Now, LogLevel.Info, "TestLogger", "Test message", null, null); + using (var appender = new RollingFileAppender(_testFilePath)) + { + var entry = new LogEntry(DateTime.Now, LogLevel.Info, "TestLogger", "Test message", null, null); - appender.Append(entry); - appender.Flush(); + appender.Append(entry); + appender.Flush(); + } Assert.That(File.Exists(_testFilePath), Is.True); var content = File.ReadAllText(_testFilePath); diff --git a/GFramework.Core/logging/LoggingConfigurationLoader.cs b/GFramework.Core/logging/LoggingConfigurationLoader.cs index 1110ecc..293f0f1 100644 --- a/GFramework.Core/logging/LoggingConfigurationLoader.cs +++ b/GFramework.Core/logging/LoggingConfigurationLoader.cs @@ -1,5 +1,6 @@ using System.IO; using System.Text.Json; +using System.Text.Json.Serialization; using GFramework.Core.Abstractions.logging; using GFramework.Core.logging.appenders; using GFramework.Core.logging.filters; @@ -16,7 +17,8 @@ public static class LoggingConfigurationLoader { PropertyNameCaseInsensitive = true, ReadCommentHandling = JsonCommentHandling.Skip, - AllowTrailingCommas = true + AllowTrailingCommas = true, + Converters = { new JsonStringEnumConverter(JsonNamingPolicy.CamelCase, allowIntegerValues: true) } }; /// @@ -129,10 +131,11 @@ public static class LoggingConfigurationLoader /// /// 可配置的 Logger 工厂 /// -internal sealed class ConfigurableLoggerFactory : ILoggerFactory +internal sealed class ConfigurableLoggerFactory : ILoggerFactory, IDisposable { private readonly ILogAppender[] _appenders; private readonly LoggingConfiguration _config; + private bool _disposed; public ConfigurableLoggerFactory(LoggingConfiguration config) { @@ -140,12 +143,36 @@ internal sealed class ConfigurableLoggerFactory : ILoggerFactory _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) { - // 检查是否有特定 Logger 的级别配置 - var effectiveLevel = _config.LoggerLevels.TryGetValue(name, out var level) - ? level - : _config.MinLevel; + // 检查是否有特定 Logger 的级别配置(支持前缀匹配) + var effectiveLevel = _config.MinLevel; + + foreach (var kvp in _config.LoggerLevels) + { + // 精确匹配或前缀匹配(命名空间层级) + if (name == kvp.Key || name.StartsWith(kvp.Key + ".", StringComparison.Ordinal)) + { + effectiveLevel = kvp.Value; + break; + } + } // 如果没有 Appender,返回简单的 ConsoleLogger if (_appenders.Length == 0)