From c967b4df3dd3d6a075b654e57e45f38e95180830 Mon Sep 17 00:00:00 2001
From: gewuyou <95328647+GeWuYou@users.noreply.github.com>
Date: Sun, 3 May 2026 09:00:41 +0800
Subject: [PATCH] =?UTF-8?q?fix(godot):=20=E4=BF=AE=E5=A4=8D=E6=97=A5?=
=?UTF-8?q?=E5=BF=97=20review=20=E5=8F=8D=E9=A6=88?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 修复 DeferredLogger 格式化重载提前 string.Format 的热路径问题
- 修复 GodotLogger 默认 options 分配与结构化属性无效 key 处理
- 补充 Godot logging XML 文档、回归测试和 appsettings 接入示例
- 更新 Godot logging PR review 跟踪与验证记录
---
.../Logging/GodotLoggerSettingsLoaderTests.cs | 40 +++
GFramework.Godot/Logging/DeferredLogger.cs | 267 +++++++++++++++++-
GFramework.Godot/Logging/GodotLogger.cs | 42 ++-
...odot-logging-compliance-polish-tracking.md | 37 ++-
.../godot-logging-compliance-polish-trace.md | 53 +++-
docs/zh-CN/godot/logging.md | 41 +++
6 files changed, 443 insertions(+), 37 deletions(-)
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