diff --git a/GFramework.Godot.Tests/Logging/GodotLoggerSettingsLoaderTests.cs b/GFramework.Godot.Tests/Logging/GodotLoggerSettingsLoaderTests.cs index defd50f8..749ac715 100644 --- a/GFramework.Godot.Tests/Logging/GodotLoggerSettingsLoaderTests.cs +++ b/GFramework.Godot.Tests/Logging/GodotLoggerSettingsLoaderTests.cs @@ -1,15 +1,22 @@ using System; using System.Collections.Generic; using System.IO; +using System.Reflection; using System.Text.Json; using GFramework.Core.Abstractions.Logging; using GFramework.Godot.Logging; namespace GFramework.Godot.Tests.Logging; +/// +/// Verifies Godot logging configuration discovery, parsing, normalization, and live settings propagation. +/// [TestFixture] public sealed class GodotLoggerSettingsLoaderTests { + /// + /// Verifies that configuration discovery honors the environment path, executable directory, and project path order. + /// [Test] public void DiscoverConfigurationPath_Should_Prefer_EnvironmentVariable_Then_ProcessPath_Then_ProjectPath() { @@ -53,6 +60,9 @@ public sealed class GodotLoggerSettingsLoaderTests } } + /// + /// Verifies that JSON settings bind Godot logger options and category log-level overrides. + /// [Test] public void LoadFromJsonString_Should_Read_GodotLogger_Options_And_Category_Levels() { @@ -89,6 +99,9 @@ public sealed class GodotLoggerSettingsLoaderTests }); } + /// + /// Verifies that nullable JSON option fields are normalized before the runtime receives the settings snapshot. + /// [Test] public void LoadFromJsonString_Should_Normalize_Null_GodotLogger_Options() { @@ -115,6 +128,9 @@ public sealed class GodotLoggerSettingsLoaderTests }); } + /// + /// Verifies that numeric JSON log levels must map to defined values. + /// [Test] public void LoadFromJsonString_Should_Reject_Invalid_Numeric_LogLevel() { @@ -133,6 +149,9 @@ public sealed class GodotLoggerSettingsLoaderTests Assert.That(error?.Message, Does.Contain("Unsupported numeric LogLevel value '999'")); } + /// + /// Verifies that cached provider loggers read the latest settings after the provider snapshot changes. + /// [Test] public void Provider_Should_Apply_Updated_Settings_To_Existing_Loggers() { @@ -166,4 +185,25 @@ public sealed class GodotLoggerSettingsLoaderTests Assert.That(logger.IsDebugEnabled(), Is.False); }); } + + /// + /// Verifies that caller-supplied structured property keys cannot break Godot log rendering. + /// + [Test] + public void StructuredProperties_Should_Skip_Blank_Keys_And_Trim_Valid_Keys() + { + var formatProperties = typeof(GodotLogger).GetMethod( + "FormatProperties", + BindingFlags.NonPublic | BindingFlags.Static); + var properties = new (string Key, object? Value)[] + { + (null!, "ignored"), + (" ", "ignored"), + (" Player ", 42) + }; + + var result = formatProperties?.Invoke(null, [properties]); + + Assert.That(result, Is.EqualTo(" | Player=42")); + } } diff --git a/GFramework.Godot/Logging/DeferredLogger.cs b/GFramework.Godot/Logging/DeferredLogger.cs index d37d4ccb..35215030 100644 --- a/GFramework.Godot/Logging/DeferredLogger.cs +++ b/GFramework.Godot/Logging/DeferredLogger.cs @@ -14,10 +14,19 @@ namespace GFramework.Godot.Logging; /// exchange so concurrent first-use calls converge on one cached instance without relying on the non-atomic /// null-coalescing assignment pattern. /// +/// The category passed to the provider when the real logger is first needed. +/// The accessor that returns the current provider at first use. internal sealed class DeferredLogger(string category, Func providerAccessor) : IStructuredLogger { private ILogger? _inner; + /// + /// Gets the resolved inner logger, creating and atomically publishing it on first use. + /// + /// + /// The property is intentionally the single resolution gate so all delegated members share the same thread-safe + /// lazy initialization behavior. + /// private ILogger Inner { get @@ -35,221 +44,440 @@ internal sealed class DeferredLogger(string category, Func + /// Gets the category name reported by the resolved logger. + /// + /// The logger category name. public string Name() { return Inner.Name(); } + /// + /// Returns whether trace messages are enabled by the current provider settings. + /// + /// true when trace messages should be emitted; otherwise false. public bool IsTraceEnabled() { return Inner.IsTraceEnabled(); } + /// + /// Returns whether debug messages are enabled by the current provider settings. + /// + /// true when debug messages should be emitted; otherwise false. public bool IsDebugEnabled() { return Inner.IsDebugEnabled(); } + /// + /// Returns whether informational messages are enabled by the current provider settings. + /// + /// true when informational messages should be emitted; otherwise false. public bool IsInfoEnabled() { return Inner.IsInfoEnabled(); } + /// + /// Returns whether warning messages are enabled by the current provider settings. + /// + /// true when warning messages should be emitted; otherwise false. public bool IsWarnEnabled() { return Inner.IsWarnEnabled(); } + /// + /// Returns whether error messages are enabled by the current provider settings. + /// + /// true when error messages should be emitted; otherwise false. public bool IsErrorEnabled() { return Inner.IsErrorEnabled(); } + /// + /// Returns whether fatal messages are enabled by the current provider settings. + /// + /// true when fatal messages should be emitted; otherwise false. public bool IsFatalEnabled() { return Inner.IsFatalEnabled(); } + /// + /// Returns whether the specified log level is enabled by the current provider settings. + /// + /// The level to check. + /// true when the level should be emitted; otherwise false. public bool IsEnabledForLevel(LogLevel level) { return Inner.IsEnabledForLevel(level); } + /// + /// Writes a trace message through the resolved logger. + /// + /// The message to write. public void Trace(string msg) { Inner.Trace(msg); } + /// + /// Writes a formatted trace message through the resolved logger. + /// + /// The format string interpreted by the resolved logger. + /// The first format argument. public void Trace(string format, object arg) { Inner.Trace(format, arg); } + /// + /// Writes a formatted trace message through the resolved logger. + /// + /// The format string interpreted by the resolved logger. + /// The first format argument. + /// The second format argument. public void Trace(string format, object arg1, object arg2) { Inner.Trace(format, arg1, arg2); } + /// + /// Writes a formatted trace message through the resolved logger. + /// + /// The format string interpreted by the resolved logger. + /// The format arguments. public void Trace(string format, params object[] arguments) { Inner.Trace(format, arguments); } + /// + /// Writes a trace message and exception through the resolved logger. + /// + /// The message to write. + /// The exception to attach. public void Trace(string msg, Exception t) { Inner.Trace(msg, t); } + /// + /// Writes a debug message through the resolved logger. + /// + /// The message to write. public void Debug(string msg) { Inner.Debug(msg); } + /// + /// Writes a formatted debug message through the resolved logger. + /// + /// The format string interpreted by the resolved logger. + /// The first format argument. public void Debug(string format, object arg) { Inner.Debug(format, arg); } + /// + /// Writes a formatted debug message through the resolved logger. + /// + /// The format string interpreted by the resolved logger. + /// The first format argument. + /// The second format argument. public void Debug(string format, object arg1, object arg2) { Inner.Debug(format, arg1, arg2); } + /// + /// Writes a formatted debug message through the resolved logger. + /// + /// The format string interpreted by the resolved logger. + /// The format arguments. public void Debug(string format, params object[] arguments) { Inner.Debug(format, arguments); } + /// + /// Writes a debug message and exception through the resolved logger. + /// + /// The message to write. + /// The exception to attach. public void Debug(string msg, Exception t) { Inner.Debug(msg, t); } + /// + /// Writes an informational message through the resolved logger. + /// + /// The message to write. public void Info(string msg) { Inner.Info(msg); } + /// + /// Writes a formatted informational message through the resolved logger. + /// + /// The format string interpreted by the resolved logger. + /// The first format argument. public void Info(string format, object arg) { Inner.Info(format, arg); } + /// + /// Writes a formatted informational message through the resolved logger. + /// + /// The format string interpreted by the resolved logger. + /// The first format argument. + /// The second format argument. public void Info(string format, object arg1, object arg2) { Inner.Info(format, arg1, arg2); } + /// + /// Writes a formatted informational message through the resolved logger. + /// + /// The format string interpreted by the resolved logger. + /// The format arguments. public void Info(string format, params object[] arguments) { Inner.Info(format, arguments); } + /// + /// Writes an informational message and exception through the resolved logger. + /// + /// The message to write. + /// The exception to attach. public void Info(string msg, Exception t) { Inner.Info(msg, t); } + /// + /// Writes a warning message through the resolved logger. + /// + /// The message to write. public void Warn(string msg) { Inner.Warn(msg); } + /// + /// Writes a formatted warning message through the resolved logger. + /// + /// The format string interpreted by the resolved logger. + /// The first format argument. public void Warn(string format, object arg) { Inner.Warn(format, arg); } + /// + /// Writes a formatted warning message through the resolved logger. + /// + /// The format string interpreted by the resolved logger. + /// The first format argument. + /// The second format argument. public void Warn(string format, object arg1, object arg2) { Inner.Warn(format, arg1, arg2); } + /// + /// Writes a formatted warning message through the resolved logger. + /// + /// The format string interpreted by the resolved logger. + /// The format arguments. public void Warn(string format, params object[] arguments) { Inner.Warn(format, arguments); } + /// + /// Writes a warning message and exception through the resolved logger. + /// + /// The message to write. + /// The exception to attach. public void Warn(string msg, Exception t) { Inner.Warn(msg, t); } + /// + /// Writes an error message through the resolved logger. + /// + /// The message to write. public void Error(string msg) { Inner.Error(msg); } + /// + /// Writes a formatted error message through the resolved logger. + /// + /// The format string interpreted by the resolved logger. + /// The first format argument. public void Error(string format, object arg) { Inner.Error(format, arg); } + /// + /// Writes a formatted error message through the resolved logger. + /// + /// The format string interpreted by the resolved logger. + /// The first format argument. + /// The second format argument. public void Error(string format, object arg1, object arg2) { Inner.Error(format, arg1, arg2); } + /// + /// Writes a formatted error message through the resolved logger. + /// + /// The format string interpreted by the resolved logger. + /// The format arguments. public void Error(string format, params object[] arguments) { Inner.Error(format, arguments); } + /// + /// Writes an error message and exception through the resolved logger. + /// + /// The message to write. + /// The exception to attach. public void Error(string msg, Exception t) { Inner.Error(msg, t); } + /// + /// Writes a fatal message through the resolved logger. + /// + /// The message to write. public void Fatal(string msg) { Inner.Fatal(msg); } + /// + /// Writes a formatted fatal message through the resolved logger. + /// + /// The format string interpreted by the resolved logger. + /// The first format argument. public void Fatal(string format, object arg) { Inner.Fatal(format, arg); } + /// + /// Writes a formatted fatal message through the resolved logger. + /// + /// The format string interpreted by the resolved logger. + /// The first format argument. + /// The second format argument. public void Fatal(string format, object arg1, object arg2) { Inner.Fatal(format, arg1, arg2); } + /// + /// Writes a formatted fatal message through the resolved logger. + /// + /// The format string interpreted by the resolved logger. + /// The format arguments. public void Fatal(string format, params object[] arguments) { Inner.Fatal(format, arguments); } + /// + /// Writes a fatal message and exception through the resolved logger. + /// + /// The message to write. + /// The exception to attach. public void Fatal(string msg, Exception t) { Inner.Fatal(msg, t); } + /// + /// Writes a message at the specified level through the resolved logger. + /// + /// The level to write. + /// The message to write. public void Log(LogLevel level, string message) { LogFallback(level, message, exception: null); } + /// + /// Writes a formatted message at the specified level while preserving deferred formatting semantics. + /// + /// The level to write. + /// The format string interpreted by the resolved logger. + /// The first format argument. public void Log(LogLevel level, string format, object arg) { - LogFallback(level, string.Format(format, arg), exception: null); + Inner.Log(level, format, arg); } + /// + /// Writes a formatted message at the specified level while preserving deferred formatting semantics. + /// + /// The level to write. + /// The format string interpreted by the resolved logger. + /// The first format argument. + /// The second format argument. public void Log(LogLevel level, string format, object arg1, object arg2) { - LogFallback(level, string.Format(format, arg1, arg2), exception: null); + Inner.Log(level, format, arg1, arg2); } + /// + /// Writes a formatted message at the specified level while preserving deferred formatting semantics. + /// + /// The level to write. + /// The format string interpreted by the resolved logger. + /// The format arguments. public void Log(LogLevel level, string format, params object[] arguments) { - LogFallback(level, string.Format(format, arguments), exception: null); + Inner.Log(level, format, arguments); } + /// + /// Writes a message and exception at the specified level through the resolved logger. + /// + /// The level to write. + /// The message to write. + /// The exception to attach. public void Log(LogLevel level, string message, Exception exception) { LogFallback(level, message, exception); } + /// + /// Writes a structured message through the resolved logger when it supports structured properties. + /// + /// The level to write. + /// The message to write. + /// The structured properties to attach. public void Log(LogLevel level, string message, params (string Key, object? Value)[] properties) { if (Inner is IStructuredLogger structuredLogger) @@ -261,6 +489,13 @@ internal sealed class DeferredLogger(string category, Func + /// Writes a structured message and exception through the resolved logger when it supports structured properties. + /// + /// The level to write. + /// The message to write. + /// The optional exception to attach. + /// The structured properties to attach. public void Log(LogLevel level, string message, Exception? exception, params (string Key, object? Value)[] properties) { if (Inner is IStructuredLogger structuredLogger) @@ -272,11 +507,23 @@ internal sealed class DeferredLogger(string category, Func + /// Resolves the real logger from the current provider for the deferred category. + /// + /// The logger created by the current provider. private ILogger ResolveLogger() { return providerAccessor().CreateLogger(category); } + /// + /// Routes a message through the non-structured logger surface when the resolved logger lacks structured support. + /// + /// The level to write. + /// The message to write. + /// The optional exception to attach. + /// The structured properties rendered into a suffix for fallback loggers. + /// Thrown when is not supported. private void LogFallback( LogLevel level, string message, @@ -313,11 +560,25 @@ internal sealed class DeferredLogger(string category, Func + /// Routes a non-structured message through the fallback path. + /// + /// The level to write. + /// The message to write. + /// The optional exception to attach. + /// Thrown when is not supported. private void LogFallback(LogLevel level, string message, Exception? exception) { LogFallback(level, message, exception, []); } + /// + /// Chooses the message-only or exception-aware write delegate for fallback logging. + /// + /// The rendered fallback message. + /// The optional exception to attach. + /// The delegate used when no exception is present. + /// The delegate used when an exception is present. private static void WriteFallback( string message, Exception? exception, diff --git a/GFramework.Godot/Logging/GodotLogger.cs b/GFramework.Godot/Logging/GodotLogger.cs index 7a519b4d..d6dbe974 100644 --- a/GFramework.Godot/Logging/GodotLogger.cs +++ b/GFramework.Godot/Logging/GodotLogger.cs @@ -23,7 +23,7 @@ public sealed class GodotLogger : AbstractLogger public GodotLogger(string? name = null, LogLevel minLevel = LogLevel.Info) : this( name ?? RootLoggerName, - () => GodotLoggerOptions.ForMinimumLevel(minLevel), + CreateFixedOptionsProvider(minLevel), () => minLevel) { } @@ -36,12 +36,21 @@ public sealed class GodotLogger : AbstractLogger public GodotLogger(string? name, GodotLoggerOptions options) : this( name ?? RootLoggerName, - () => options, - () => options.GetEffectiveMinLevel()) + CreateOptionsProvider(options), + CreateMinLevelProvider(options)) { - ArgumentNullException.ThrowIfNull(options); } + /// + /// Initializes the core logger with dynamic options and level providers. + /// + /// The resolved logger name used in rendered output. + /// The provider that supplies the latest rendering options for each write. + /// The provider that supplies the latest effective minimum level. + /// + /// The Godot factory uses this constructor so cached logger instances can observe hot-reloaded settings without + /// being recreated. The default public constructor supplies a fixed provider to avoid allocation on the log path. + /// internal GodotLogger( string name, Func optionsProvider, @@ -150,7 +159,12 @@ public sealed class GodotLogger : AbstractLogger { foreach (var property in properties) { - merged[property.Key] = property.Value; + if (string.IsNullOrWhiteSpace(property.Key)) + { + continue; + } + + merged[property.Key.Trim()] = property.Value; } } @@ -160,6 +174,24 @@ public sealed class GodotLogger : AbstractLogger private static readonly IReadOnlyDictionary EmptyProperties = new Dictionary(StringComparer.Ordinal); + private static Func CreateFixedOptionsProvider(LogLevel minLevel) + { + var options = GodotLoggerOptions.ForMinimumLevel(minLevel); + return () => options; + } + + private static Func CreateOptionsProvider(GodotLoggerOptions options) + { + ArgumentNullException.ThrowIfNull(options); + return () => options; + } + + private static Func CreateMinLevelProvider(GodotLoggerOptions options) + { + ArgumentNullException.ThrowIfNull(options); + return () => options.GetEffectiveMinLevel(); + } + private static string FormatValue(object? value) { if (value == null) diff --git a/ai-plan/public/godot-logging-compliance-polish/todos/godot-logging-compliance-polish-tracking.md b/ai-plan/public/godot-logging-compliance-polish/todos/godot-logging-compliance-polish-tracking.md index 00865126..b2b18dcd 100644 --- a/ai-plan/public/godot-logging-compliance-polish/todos/godot-logging-compliance-polish-tracking.md +++ b/ai-plan/public/godot-logging-compliance-polish/todos/godot-logging-compliance-polish-tracking.md @@ -7,15 +7,15 @@ GFramework 自身日志抽象不分叉”的稳定宿主层,并为后续 Godot ## 当前恢复点 -- 恢复点编号:`GODOT-LOGGING-COMPLIANCE-POLISH-RP-002` -- 当前阶段:`PR review hardening` +- 恢复点编号:`GODOT-LOGGING-COMPLIANCE-POLISH-RP-003` +- 当前阶段:`PR review follow-up` - 当前焦点: - 已补齐 `GodotLog` 静态入口、延迟 logger 解析、配置自动发现与热重载 - 已让 `GodotLoggerFactoryProvider` 对已缓存 logger 生效动态配置,而不是只在新建 logger 时读快照 - 已让 `GodotLogger` 支持 `{properties}` 占位符,并把 `IStructuredLogger` / `LogContext` 属性落到 Godot 输出 - 已兼容 `GodotLogger` 风格配置值,如 `Information` / `Critical` - - 已处理 PR #314 最新 AI review 中仍适用的生命周期、配置输入、缓存边界、注释和脚本健壮性问题 - - 下一轮优先只复核 CI 反馈是否已收敛,避免继续扩大 Godot logging API 面 + - 已处理 PR #314 最新 AI review 中仍适用的 XML docs、热路径分配、结构化属性兜底、文档示例和 tracking 精简问题 + - 下一轮优先刷新 PR review / CI 反馈,避免继续扩大 Godot logging API 面 ## 当前状态摘要 @@ -48,6 +48,10 @@ GFramework 自身日志抽象不分叉”的稳定宿主层,并为后续 Godot - 配置 JSON 会先归一化模板和颜色字典,并拒绝未定义的数字 `LogLevel` - `GodotLogTemplate` 的模板缓存和分类格式缓存已改为有界并发缓存,避免热重载或动态 category 长期单向增长 - `refactor-scripts/update-namespaces.py` 已移除本机绝对路径默认值,并会把文件处理失败汇总成非零退出码 +- PR #314 最新 review 中,GodotLog watcher 释放、热重载回调阻塞、配置归一化、数字 `LogLevel` 校验、 + 模板缓存和脚本健壮性问题已在当前 head 验证为已处理;本轮只继续修仍适用的问题 +- PR #314 最新 follow-up 中,`DeferredLogger` 格式化重载现在委托给 inner logger,`GodotLogger` 默认 options + provider 已改为构造时缓存,结构化属性会跳过空白 key 并使用 trimmed key ## 当前风险 @@ -69,25 +73,18 @@ GFramework 自身日志抽象不分叉”的稳定宿主层,并为后续 Godot - `dotnet test GFramework.Godot.Tests/GFramework.Godot.Tests.csproj -c Release --filter FullyQualifiedName~GodotLog -nologo` - 结果:通过 - - 备注:定向新增 Godot logging 配置 / 模板回归共 `11` 项通过 + - 备注:RP-003 follow-up 后 Godot logging 定向测试共 `15` 项通过 - `dotnet test GFramework.Godot.Tests/GFramework.Godot.Tests.csproj -c Release -nologo` - 结果:通过 - - 备注:Godot 测试项目共 `69` 项通过 -- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter FullyQualifiedName~Logging -nologo` + - 备注:RP-003 follow-up 后 Godot 测试项目共 `73` 项通过 +- `dotnet format GFramework.Godot.Tests/GFramework.Godot.Tests.csproj --verify-no-changes --no-restore --include ...` - 结果:通过 - - 备注:Core logging 相关测试共 `214` 项通过,覆盖 `AbstractLogger` 动态最小级别改造回归 -- `dotnet test GFramework.Godot.Tests/GFramework.Godot.Tests.csproj -c Release --filter FullyQualifiedName~GodotLog -nologo` - - 结果:通过 - - 备注:PR review hardening 后 Godot logging 定向测试共 `14` 项通过 -- `dotnet test GFramework.Godot.Tests/GFramework.Godot.Tests.csproj -c Release -nologo` - - 结果:通过 - - 备注:PR review hardening 后 Godot 测试项目共 `72` 项通过 -- `python3 -B refactor-scripts/update-namespaces.py --help` - - 结果:通过 - - 备注:确认脚本 CLI 参数解析可用 + - 备注:include 范围为本轮修改的 C# 文件;全项目 format 仍命中既有行尾 / 编码问题,详见 trace +- 历史验证明细已保留在 [执行 trace](../traces/godot-logging-compliance-polish-trace.md) 的 `RP-001 验证` 与 + `RP-002 验证` 小节,active tracking 入口只保留当前恢复点相关结果 ## 下一步 -1. 刷新 PR review / CI 状态,确认最新 head 上 CodeRabbit 与 Greptile 线程是否关闭或变为 stale -2. 若 CI 仍报 MegaLinter `dotnet-format` restore 失败,优先复核 Actions restore 环境,而不是继续改本地格式 -3. 后续若继续推进能力设计,再评估 Godot 输出是否应变成 Core 可组合 sink / appender +1. 提交 RP-003 review follow-up 改动 +2. 刷新 PR review / CI 状态,确认最新 head 上 CodeRabbit 与 Greptile 线程是否关闭或变为 stale +3. 若 CI 仍报 MegaLinter `dotnet-format` restore 失败,优先复核 Actions restore 环境,而不是继续改本地格式 diff --git a/ai-plan/public/godot-logging-compliance-polish/traces/godot-logging-compliance-polish-trace.md b/ai-plan/public/godot-logging-compliance-polish/traces/godot-logging-compliance-polish-trace.md index 6afb0b79..915585c8 100644 --- a/ai-plan/public/godot-logging-compliance-polish/traces/godot-logging-compliance-polish-trace.md +++ b/ai-plan/public/godot-logging-compliance-polish/traces/godot-logging-compliance-polish-trace.md @@ -38,7 +38,7 @@ - 同步更新 `docs/zh-CN/godot/logging.md`,把文档结论从“只有薄适配层”刷新成“已具备宿主便利层和热重载语义” - 已从 `ai-libs/GodotLogger` 复制 MIT 许可证到 `third-party-licenses/GodotLogger/LICENSE` -### 验证 +### RP-001 验证 - `dotnet test GFramework.Godot.Tests/GFramework.Godot.Tests.csproj -c Release --filter FullyQualifiedName~GodotLog -nologo` - 结果:通过(11/11) @@ -47,7 +47,7 @@ - `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter FullyQualifiedName~Logging -nologo` - 结果:通过(214/214) -### 下一步 +### RP-001 下一步 1. 若继续推进本主题,优先评估 Godot 输出是否应变成 Core 可组合 appender / sink 2. 若出现后续 review 反馈,直接在本 topic 追加 RP-002,而不是重新开临时 local-plan @@ -68,17 +68,52 @@ - 同步补充 Godot logging 内部类型和关键方法 XML 文档,说明热重载、快照发布、分类匹配和模板缓存语义 - 同步更新 `docs/zh-CN/godot/logging.md`,记录 `ConfigurationPath` 的诊断语义和 `Shutdown()` teardown 用法 -### 验证 +### RP-002 验证 + +- `dotnet test GFramework.Godot.Tests/GFramework.Godot.Tests.csproj -c Release --filter FullyQualifiedName~GodotLog -nologo` + - 结果:通过(15/15) +- `dotnet test GFramework.Godot.Tests/GFramework.Godot.Tests.csproj -c Release -nologo` + - 结果:通过(73/73) +- `python3 -B refactor-scripts/update-namespaces.py --help` + - 结果:通过 + +### RP-002 下一步 + +1. 提交 RP-002 review hardening 改动 +2. 刷新 PR review / CI,确认最新 head 是否关闭已处理线程 +3. 若 CI 仍只有 MegaLinter `dotnet-format` restore 失败,优先定位 Actions restore 环境 + +## 2026-05-03 + +### 阶段:PR review follow-up(RP-003) + +- 再次使用 `$gframework-pr-review` 抓取 PR #314 最新 review payload,确认当前 head 上仍有 CodeRabbit 与 + Greptile 未解决线程 +- 本轮验证后接受并处理仍适用的 review 结论: + - `GodotLoggerSettingsLoaderTests` 公开测试类型与公开测试方法需要 XML 文档 + - `DeferredLogger` 的公开接口成员需要 XML 文档,并且格式化 `Log(...)` 重载不应提前执行 `string.Format` + - `GodotLogger` 默认构造器不应在每条日志上重新创建 options,结构化属性 key 需要跳过空白并做 trim + - Godot logging 文档需要给出最小 `appsettings.json` 示例、放置约定和热重载覆盖说明 + - active tracking 不应同时保留 RP-001 与 RP-002 的详细验证计数,trace 重复标题需要消除 +- 本轮验证后确认以下旧 review 结论在当前 head 已处理,无需重复改动: + - `GodotLog.Shutdown()` 已可释放 materialized configuration source 的 watcher + - hot-reload callback 已走无 `Thread.Sleep` 的 `LoadSettings()`,`Thread.Sleep` 只保留在 startup strict load retry + - JSON options 归一化、数字 `LogLevel` 校验、GodotLogTemplate 缓存和 namespace 脚本健壮性已在当前 head 存在 + +### RP-003 验证 - `dotnet test GFramework.Godot.Tests/GFramework.Godot.Tests.csproj -c Release --filter FullyQualifiedName~GodotLog -nologo` - 结果:通过(14/14) - `dotnet test GFramework.Godot.Tests/GFramework.Godot.Tests.csproj -c Release -nologo` - 结果:通过(72/72) -- `python3 -B refactor-scripts/update-namespaces.py --help` - - 结果:通过 +- `dotnet format GFramework.Godot.Tests/GFramework.Godot.Tests.csproj --verify-no-changes --no-restore --include ...` + - 结果:通过,include 范围为本轮修改的三个 C# 文件 +- `dotnet format GFramework.Godot.Tests/GFramework.Godot.Tests.csproj --verify-no-changes --no-restore` + - 结果:未通过;命中既有 `GFramework.Godot.Tests/Coroutine/GodotTimeSourceTests.cs` 行尾与 + `GFramework.Godot.Tests/GlobalUsings.cs` 编码问题,本轮未把该历史格式清理并入 PR review follow-up -### 下一步 +### RP-003 下一步 -1. 提交 RP-002 review hardening 改动 -2. 刷新 PR review / CI,确认最新 head 是否关闭已处理线程 -3. 若 CI 仍只有 MegaLinter `dotnet-format` restore 失败,优先定位 Actions restore 环境 +1. 提交 RP-003 review follow-up 改动 +2. 刷新 PR review,确认 CodeRabbit / Greptile 线程是否关闭或 stale +3. 若 CI 仍只有 MegaLinter `dotnet-format` restore 失败,继续定位 Actions restore 环境而不是扩大本地格式清理范围 diff --git a/docs/zh-CN/godot/logging.md b/docs/zh-CN/godot/logging.md index 0ae7f9f0..b6a71b44 100644 --- a/docs/zh-CN/godot/logging.md +++ b/docs/zh-CN/godot/logging.md @@ -95,6 +95,47 @@ var logger = GodotLog.CreateLogger
(); `GodotLog.Configure(...)` 失效。长生命周期服务器或测试宿主如果需要在退出时主动释放配置文件 watcher,可以调用 `GodotLog.Shutdown()`;它会停止热重载监听,已创建 logger 仍然继续使用最后一次成功发布的配置快照。 +最小可复制的 `appsettings.json` 可以只包含 `Logging` 根节点。`LogLevel` 使用 `Default` 和类别名控制过滤阈值, +`GodotLogger` 控制 Godot 输出模式、模板和颜色: + +```json +{ + "Logging": { + "LogLevel": { + "Default": "Info", + "Game.Services": "Debug" + }, + "GodotLogger": { + "Mode": "Debug", + "DebugMinLevel": "Debug", + "ReleaseMinLevel": "Info", + "DebugOutputTemplate": "[{timestamp:HH:mm:ss.fff}] [color={color}][{level:u3}][/color] [{category:l16}] {message}{properties}", + "ReleaseOutputTemplate": "[{timestamp:HH:mm:ss.fff}] [{level:u3}] [{category:l16}] {message}{properties}", + "Colors": { + "Info": "white", + "Warning": "orange", + "Error": "red" + } + } + } +} +``` + +配置文件发现顺序固定为: + +1. `GODOT_LOGGER_CONFIG` 指向的文件 +2. 导出程序或测试进程所在目录的 `appsettings.json` +3. Godot 项目资源根目录的 `res://appsettings.json` + +在编辑器项目里,`res://appsettings.json` 放在项目根目录;在导出包或专用服务器里,优先把 +`appsettings.json` 放到可执行文件同目录,便于运维脚本替换。运行中修改已发现的配置文件会热重载 +`Logging:LogLevel` 与 `Logging:GodotLogger` 下的模式、最小级别、模板和颜色;已创建 logger 不会重新实例化, +但下一次级别判定和写入会读取最新成功发布的配置快照。热重载解析失败或文件被短暂锁定时会保留上一份可用配置。 + +`GodotLog.Configure(...)` 适合在没有配置文件或需要代码覆盖默认值时使用,并且必须在首次创建 provider 或配置源前调用。 +`GodotLog.ConfigurationPath` 适合启动诊断和测试断言;`GodotLog.Shutdown()` 适合测试 teardown 或长生命周期服务器退出时释放 +文件 watcher,不会清空已经发布给 logger 的最后一份配置。 + ## 最小接入路径 ### 1. 在 `ArchitectureConfiguration` 中挂上 Godot provider