fix(godot): 修复日志 review 反馈

- 修复 DeferredLogger 格式化重载提前 string.Format 的热路径问题

- 修复 GodotLogger 默认 options 分配与结构化属性无效 key 处理

- 补充 Godot logging XML 文档、回归测试和 appsettings 接入示例

- 更新 Godot logging PR review 跟踪与验证记录
This commit is contained in:
gewuyou 2026-05-03 09:00:41 +08:00
parent b4b3538b21
commit c967b4df3d
6 changed files with 443 additions and 37 deletions

View File

@ -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;
/// <summary>
/// Verifies Godot logging configuration discovery, parsing, normalization, and live settings propagation.
/// </summary>
[TestFixture]
public sealed class GodotLoggerSettingsLoaderTests
{
/// <summary>
/// Verifies that configuration discovery honors the environment path, executable directory, and project path order.
/// </summary>
[Test]
public void DiscoverConfigurationPath_Should_Prefer_EnvironmentVariable_Then_ProcessPath_Then_ProjectPath()
{
@ -53,6 +60,9 @@ public sealed class GodotLoggerSettingsLoaderTests
}
}
/// <summary>
/// Verifies that JSON settings bind Godot logger options and category log-level overrides.
/// </summary>
[Test]
public void LoadFromJsonString_Should_Read_GodotLogger_Options_And_Category_Levels()
{
@ -89,6 +99,9 @@ public sealed class GodotLoggerSettingsLoaderTests
});
}
/// <summary>
/// Verifies that nullable JSON option fields are normalized before the runtime receives the settings snapshot.
/// </summary>
[Test]
public void LoadFromJsonString_Should_Normalize_Null_GodotLogger_Options()
{
@ -115,6 +128,9 @@ public sealed class GodotLoggerSettingsLoaderTests
});
}
/// <summary>
/// Verifies that numeric JSON log levels must map to defined <see cref="LogLevel"/> values.
/// </summary>
[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'"));
}
/// <summary>
/// Verifies that cached provider loggers read the latest settings after the provider snapshot changes.
/// </summary>
[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);
});
}
/// <summary>
/// Verifies that caller-supplied structured property keys cannot break Godot log rendering.
/// </summary>
[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"));
}
}

View File

@ -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.
/// </remarks>
/// <param name="category">The category passed to the provider when the real logger is first needed.</param>
/// <param name="providerAccessor">The accessor that returns the current provider at first use.</param>
internal sealed class DeferredLogger(string category, Func<ILoggerFactoryProvider> providerAccessor) : IStructuredLogger
{
private ILogger? _inner;
/// <summary>
/// Gets the resolved inner logger, creating and atomically publishing it on first use.
/// </summary>
/// <remarks>
/// The property is intentionally the single resolution gate so all delegated members share the same thread-safe
/// lazy initialization behavior.
/// </remarks>
private ILogger Inner
{
get
@ -35,221 +44,440 @@ internal sealed class DeferredLogger(string category, Func<ILoggerFactoryProvide
}
}
/// <summary>
/// Gets the category name reported by the resolved logger.
/// </summary>
/// <returns>The logger category name.</returns>
public string Name()
{
return Inner.Name();
}
/// <summary>
/// Returns whether trace messages are enabled by the current provider settings.
/// </summary>
/// <returns>true when trace messages should be emitted; otherwise false.</returns>
public bool IsTraceEnabled()
{
return Inner.IsTraceEnabled();
}
/// <summary>
/// Returns whether debug messages are enabled by the current provider settings.
/// </summary>
/// <returns>true when debug messages should be emitted; otherwise false.</returns>
public bool IsDebugEnabled()
{
return Inner.IsDebugEnabled();
}
/// <summary>
/// Returns whether informational messages are enabled by the current provider settings.
/// </summary>
/// <returns>true when informational messages should be emitted; otherwise false.</returns>
public bool IsInfoEnabled()
{
return Inner.IsInfoEnabled();
}
/// <summary>
/// Returns whether warning messages are enabled by the current provider settings.
/// </summary>
/// <returns>true when warning messages should be emitted; otherwise false.</returns>
public bool IsWarnEnabled()
{
return Inner.IsWarnEnabled();
}
/// <summary>
/// Returns whether error messages are enabled by the current provider settings.
/// </summary>
/// <returns>true when error messages should be emitted; otherwise false.</returns>
public bool IsErrorEnabled()
{
return Inner.IsErrorEnabled();
}
/// <summary>
/// Returns whether fatal messages are enabled by the current provider settings.
/// </summary>
/// <returns>true when fatal messages should be emitted; otherwise false.</returns>
public bool IsFatalEnabled()
{
return Inner.IsFatalEnabled();
}
/// <summary>
/// Returns whether the specified log level is enabled by the current provider settings.
/// </summary>
/// <param name="level">The level to check.</param>
/// <returns>true when the level should be emitted; otherwise false.</returns>
public bool IsEnabledForLevel(LogLevel level)
{
return Inner.IsEnabledForLevel(level);
}
/// <summary>
/// Writes a trace message through the resolved logger.
/// </summary>
/// <param name="msg">The message to write.</param>
public void Trace(string msg)
{
Inner.Trace(msg);
}
/// <summary>
/// Writes a formatted trace message through the resolved logger.
/// </summary>
/// <param name="format">The format string interpreted by the resolved logger.</param>
/// <param name="arg">The first format argument.</param>
public void Trace(string format, object arg)
{
Inner.Trace(format, arg);
}
/// <summary>
/// Writes a formatted trace message through the resolved logger.
/// </summary>
/// <param name="format">The format string interpreted by the resolved logger.</param>
/// <param name="arg1">The first format argument.</param>
/// <param name="arg2">The second format argument.</param>
public void Trace(string format, object arg1, object arg2)
{
Inner.Trace(format, arg1, arg2);
}
/// <summary>
/// Writes a formatted trace message through the resolved logger.
/// </summary>
/// <param name="format">The format string interpreted by the resolved logger.</param>
/// <param name="arguments">The format arguments.</param>
public void Trace(string format, params object[] arguments)
{
Inner.Trace(format, arguments);
}
/// <summary>
/// Writes a trace message and exception through the resolved logger.
/// </summary>
/// <param name="msg">The message to write.</param>
/// <param name="t">The exception to attach.</param>
public void Trace(string msg, Exception t)
{
Inner.Trace(msg, t);
}
/// <summary>
/// Writes a debug message through the resolved logger.
/// </summary>
/// <param name="msg">The message to write.</param>
public void Debug(string msg)
{
Inner.Debug(msg);
}
/// <summary>
/// Writes a formatted debug message through the resolved logger.
/// </summary>
/// <param name="format">The format string interpreted by the resolved logger.</param>
/// <param name="arg">The first format argument.</param>
public void Debug(string format, object arg)
{
Inner.Debug(format, arg);
}
/// <summary>
/// Writes a formatted debug message through the resolved logger.
/// </summary>
/// <param name="format">The format string interpreted by the resolved logger.</param>
/// <param name="arg1">The first format argument.</param>
/// <param name="arg2">The second format argument.</param>
public void Debug(string format, object arg1, object arg2)
{
Inner.Debug(format, arg1, arg2);
}
/// <summary>
/// Writes a formatted debug message through the resolved logger.
/// </summary>
/// <param name="format">The format string interpreted by the resolved logger.</param>
/// <param name="arguments">The format arguments.</param>
public void Debug(string format, params object[] arguments)
{
Inner.Debug(format, arguments);
}
/// <summary>
/// Writes a debug message and exception through the resolved logger.
/// </summary>
/// <param name="msg">The message to write.</param>
/// <param name="t">The exception to attach.</param>
public void Debug(string msg, Exception t)
{
Inner.Debug(msg, t);
}
/// <summary>
/// Writes an informational message through the resolved logger.
/// </summary>
/// <param name="msg">The message to write.</param>
public void Info(string msg)
{
Inner.Info(msg);
}
/// <summary>
/// Writes a formatted informational message through the resolved logger.
/// </summary>
/// <param name="format">The format string interpreted by the resolved logger.</param>
/// <param name="arg">The first format argument.</param>
public void Info(string format, object arg)
{
Inner.Info(format, arg);
}
/// <summary>
/// Writes a formatted informational message through the resolved logger.
/// </summary>
/// <param name="format">The format string interpreted by the resolved logger.</param>
/// <param name="arg1">The first format argument.</param>
/// <param name="arg2">The second format argument.</param>
public void Info(string format, object arg1, object arg2)
{
Inner.Info(format, arg1, arg2);
}
/// <summary>
/// Writes a formatted informational message through the resolved logger.
/// </summary>
/// <param name="format">The format string interpreted by the resolved logger.</param>
/// <param name="arguments">The format arguments.</param>
public void Info(string format, params object[] arguments)
{
Inner.Info(format, arguments);
}
/// <summary>
/// Writes an informational message and exception through the resolved logger.
/// </summary>
/// <param name="msg">The message to write.</param>
/// <param name="t">The exception to attach.</param>
public void Info(string msg, Exception t)
{
Inner.Info(msg, t);
}
/// <summary>
/// Writes a warning message through the resolved logger.
/// </summary>
/// <param name="msg">The message to write.</param>
public void Warn(string msg)
{
Inner.Warn(msg);
}
/// <summary>
/// Writes a formatted warning message through the resolved logger.
/// </summary>
/// <param name="format">The format string interpreted by the resolved logger.</param>
/// <param name="arg">The first format argument.</param>
public void Warn(string format, object arg)
{
Inner.Warn(format, arg);
}
/// <summary>
/// Writes a formatted warning message through the resolved logger.
/// </summary>
/// <param name="format">The format string interpreted by the resolved logger.</param>
/// <param name="arg1">The first format argument.</param>
/// <param name="arg2">The second format argument.</param>
public void Warn(string format, object arg1, object arg2)
{
Inner.Warn(format, arg1, arg2);
}
/// <summary>
/// Writes a formatted warning message through the resolved logger.
/// </summary>
/// <param name="format">The format string interpreted by the resolved logger.</param>
/// <param name="arguments">The format arguments.</param>
public void Warn(string format, params object[] arguments)
{
Inner.Warn(format, arguments);
}
/// <summary>
/// Writes a warning message and exception through the resolved logger.
/// </summary>
/// <param name="msg">The message to write.</param>
/// <param name="t">The exception to attach.</param>
public void Warn(string msg, Exception t)
{
Inner.Warn(msg, t);
}
/// <summary>
/// Writes an error message through the resolved logger.
/// </summary>
/// <param name="msg">The message to write.</param>
public void Error(string msg)
{
Inner.Error(msg);
}
/// <summary>
/// Writes a formatted error message through the resolved logger.
/// </summary>
/// <param name="format">The format string interpreted by the resolved logger.</param>
/// <param name="arg">The first format argument.</param>
public void Error(string format, object arg)
{
Inner.Error(format, arg);
}
/// <summary>
/// Writes a formatted error message through the resolved logger.
/// </summary>
/// <param name="format">The format string interpreted by the resolved logger.</param>
/// <param name="arg1">The first format argument.</param>
/// <param name="arg2">The second format argument.</param>
public void Error(string format, object arg1, object arg2)
{
Inner.Error(format, arg1, arg2);
}
/// <summary>
/// Writes a formatted error message through the resolved logger.
/// </summary>
/// <param name="format">The format string interpreted by the resolved logger.</param>
/// <param name="arguments">The format arguments.</param>
public void Error(string format, params object[] arguments)
{
Inner.Error(format, arguments);
}
/// <summary>
/// Writes an error message and exception through the resolved logger.
/// </summary>
/// <param name="msg">The message to write.</param>
/// <param name="t">The exception to attach.</param>
public void Error(string msg, Exception t)
{
Inner.Error(msg, t);
}
/// <summary>
/// Writes a fatal message through the resolved logger.
/// </summary>
/// <param name="msg">The message to write.</param>
public void Fatal(string msg)
{
Inner.Fatal(msg);
}
/// <summary>
/// Writes a formatted fatal message through the resolved logger.
/// </summary>
/// <param name="format">The format string interpreted by the resolved logger.</param>
/// <param name="arg">The first format argument.</param>
public void Fatal(string format, object arg)
{
Inner.Fatal(format, arg);
}
/// <summary>
/// Writes a formatted fatal message through the resolved logger.
/// </summary>
/// <param name="format">The format string interpreted by the resolved logger.</param>
/// <param name="arg1">The first format argument.</param>
/// <param name="arg2">The second format argument.</param>
public void Fatal(string format, object arg1, object arg2)
{
Inner.Fatal(format, arg1, arg2);
}
/// <summary>
/// Writes a formatted fatal message through the resolved logger.
/// </summary>
/// <param name="format">The format string interpreted by the resolved logger.</param>
/// <param name="arguments">The format arguments.</param>
public void Fatal(string format, params object[] arguments)
{
Inner.Fatal(format, arguments);
}
/// <summary>
/// Writes a fatal message and exception through the resolved logger.
/// </summary>
/// <param name="msg">The message to write.</param>
/// <param name="t">The exception to attach.</param>
public void Fatal(string msg, Exception t)
{
Inner.Fatal(msg, t);
}
/// <summary>
/// Writes a message at the specified level through the resolved logger.
/// </summary>
/// <param name="level">The level to write.</param>
/// <param name="message">The message to write.</param>
public void Log(LogLevel level, string message)
{
LogFallback(level, message, exception: null);
}
/// <summary>
/// Writes a formatted message at the specified level while preserving deferred formatting semantics.
/// </summary>
/// <param name="level">The level to write.</param>
/// <param name="format">The format string interpreted by the resolved logger.</param>
/// <param name="arg">The first format argument.</param>
public void Log(LogLevel level, string format, object arg)
{
LogFallback(level, string.Format(format, arg), exception: null);
Inner.Log(level, format, arg);
}
/// <summary>
/// Writes a formatted message at the specified level while preserving deferred formatting semantics.
/// </summary>
/// <param name="level">The level to write.</param>
/// <param name="format">The format string interpreted by the resolved logger.</param>
/// <param name="arg1">The first format argument.</param>
/// <param name="arg2">The second format argument.</param>
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);
}
/// <summary>
/// Writes a formatted message at the specified level while preserving deferred formatting semantics.
/// </summary>
/// <param name="level">The level to write.</param>
/// <param name="format">The format string interpreted by the resolved logger.</param>
/// <param name="arguments">The format arguments.</param>
public void Log(LogLevel level, string format, params object[] arguments)
{
LogFallback(level, string.Format(format, arguments), exception: null);
Inner.Log(level, format, arguments);
}
/// <summary>
/// Writes a message and exception at the specified level through the resolved logger.
/// </summary>
/// <param name="level">The level to write.</param>
/// <param name="message">The message to write.</param>
/// <param name="exception">The exception to attach.</param>
public void Log(LogLevel level, string message, Exception exception)
{
LogFallback(level, message, exception);
}
/// <summary>
/// Writes a structured message through the resolved logger when it supports structured properties.
/// </summary>
/// <param name="level">The level to write.</param>
/// <param name="message">The message to write.</param>
/// <param name="properties">The structured properties to attach.</param>
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<ILoggerFactoryProvide
LogFallback(level, message, exception: null, properties);
}
/// <summary>
/// Writes a structured message and exception through the resolved logger when it supports structured properties.
/// </summary>
/// <param name="level">The level to write.</param>
/// <param name="message">The message to write.</param>
/// <param name="exception">The optional exception to attach.</param>
/// <param name="properties">The structured properties to attach.</param>
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<ILoggerFactoryProvide
LogFallback(level, message, exception, properties);
}
/// <summary>
/// Resolves the real logger from the current provider for the deferred category.
/// </summary>
/// <returns>The logger created by the current provider.</returns>
private ILogger ResolveLogger()
{
return providerAccessor().CreateLogger(category);
}
/// <summary>
/// Routes a message through the non-structured logger surface when the resolved logger lacks structured support.
/// </summary>
/// <param name="level">The level to write.</param>
/// <param name="message">The message to write.</param>
/// <param name="exception">The optional exception to attach.</param>
/// <param name="properties">The structured properties rendered into a suffix for fallback loggers.</param>
/// <exception cref="ArgumentOutOfRangeException">Thrown when <paramref name="level"/> is not supported.</exception>
private void LogFallback(
LogLevel level,
string message,
@ -313,11 +560,25 @@ internal sealed class DeferredLogger(string category, Func<ILoggerFactoryProvide
}
}
/// <summary>
/// Routes a non-structured message through the fallback path.
/// </summary>
/// <param name="level">The level to write.</param>
/// <param name="message">The message to write.</param>
/// <param name="exception">The optional exception to attach.</param>
/// <exception cref="ArgumentOutOfRangeException">Thrown when <paramref name="level"/> is not supported.</exception>
private void LogFallback(LogLevel level, string message, Exception? exception)
{
LogFallback(level, message, exception, []);
}
/// <summary>
/// Chooses the message-only or exception-aware write delegate for fallback logging.
/// </summary>
/// <param name="message">The rendered fallback message.</param>
/// <param name="exception">The optional exception to attach.</param>
/// <param name="writeMessage">The delegate used when no exception is present.</param>
/// <param name="writeException">The delegate used when an exception is present.</param>
private static void WriteFallback(
string message,
Exception? exception,

View File

@ -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);
}
/// <summary>
/// Initializes the core logger with dynamic options and level providers.
/// </summary>
/// <param name="name">The resolved logger name used in rendered output.</param>
/// <param name="optionsProvider">The provider that supplies the latest rendering options for each write.</param>
/// <param name="minLevelProvider">The provider that supplies the latest effective minimum level.</param>
/// <remarks>
/// 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.
/// </remarks>
internal GodotLogger(
string name,
Func<GodotLoggerOptions> 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<string, object?> EmptyProperties =
new Dictionary<string, object?>(StringComparer.Ordinal);
private static Func<GodotLoggerOptions> CreateFixedOptionsProvider(LogLevel minLevel)
{
var options = GodotLoggerOptions.ForMinimumLevel(minLevel);
return () => options;
}
private static Func<GodotLoggerOptions> CreateOptionsProvider(GodotLoggerOptions options)
{
ArgumentNullException.ThrowIfNull(options);
return () => options;
}
private static Func<LogLevel> CreateMinLevelProvider(GodotLoggerOptions options)
{
ArgumentNullException.ThrowIfNull(options);
return () => options.GetEffectiveMinLevel();
}
private static string FormatValue(object? value)
{
if (value == null)

View File

@ -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 环境,而不是继续改本地格式

View File

@ -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-upRP-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 环境而不是扩大本地格式清理范围

View File

@ -95,6 +95,47 @@ var logger = GodotLog.CreateLogger<Main>();
`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