From 40cce565e6537f894f57d44b19f8a0fd7bb0b5d4 Mon Sep 17 00:00:00 2001
From: gewuyou <95328647+GeWuYou@users.noreply.github.com>
Date: Sun, 3 May 2026 10:51:16 +0800
Subject: [PATCH 1/4] =?UTF-8?q?docs(ai-plan):=20=E5=90=AF=E5=8A=A8=20Godot?=
=?UTF-8?q?=20logging=20Core=20sink=20=E4=B8=BB=E9=A2=98?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 归档 Godot logging 合规收尾主题并保留验证结果
- 新增 Godot logging Core sink active topic 恢复入口
- 更新 public boot 索引和当前分支映射
---
ai-plan/public/README.md | 19 +++++---
...odot-logging-compliance-polish-tracking.md | 26 ++++++-----
.../godot-logging-compliance-polish-trace.md | 21 +++++++++
.../todos/godot-logging-core-sink-tracking.md | 43 +++++++++++++++++++
.../traces/godot-logging-core-sink-trace.md | 31 +++++++++++++
5 files changed, 122 insertions(+), 18 deletions(-)
rename ai-plan/public/{ => archive}/godot-logging-compliance-polish/todos/godot-logging-compliance-polish-tracking.md (85%)
rename ai-plan/public/{ => archive}/godot-logging-compliance-polish/traces/godot-logging-compliance-polish-trace.md (87%)
create mode 100644 ai-plan/public/godot-logging-core-sink/todos/godot-logging-core-sink-tracking.md
create mode 100644 ai-plan/public/godot-logging-core-sink/traces/godot-logging-core-sink-trace.md
diff --git a/ai-plan/public/README.md b/ai-plan/public/README.md
index 447d2d23..98876db1 100644
--- a/ai-plan/public/README.md
+++ b/ai-plan/public/README.md
@@ -38,14 +38,15 @@ help the current worktree land on the right recovery documents without scanning
- Purpose: continue the data repository persistence hardening plus the settings / serialization follow-up backlog.
- Tracking: `ai-plan/public/data-repository-persistence/todos/data-repository-persistence-tracking.md`
- Trace: `ai-plan/public/data-repository-persistence/traces/data-repository-persistence-trace.md`
-- `godot-logging-compliance-polish`
- - Purpose: continue Godot logging host integration, configuration reload, structured-output polish, and follow-up work without forking the Core logging model.
- - Tracking: `ai-plan/public/godot-logging-compliance-polish/todos/godot-logging-compliance-polish-tracking.md`
- - Trace: `ai-plan/public/godot-logging-compliance-polish/traces/godot-logging-compliance-polish-trace.md`
- `semantic-release-versioning`
- Purpose: migrate release version calculation from fixed patch bumps to semantic-release while keeping the existing tag-driven NuGet publish flow.
- Tracking: `ai-plan/public/semantic-release-versioning/todos/semantic-release-versioning-tracking.md`
- Trace: `ai-plan/public/semantic-release-versioning/traces/semantic-release-versioning-trace.md`
+- `godot-logging-core-sink`
+ - Purpose: evaluate and implement the next Godot logging stage by unifying Godot output with the Core logging
+ appender / sink model instead of expanding a separate Godot-only logging pipeline.
+ - Tracking: `ai-plan/public/godot-logging-core-sink/todos/godot-logging-core-sink-tracking.md`
+ - Trace: `ai-plan/public/godot-logging-core-sink/traces/godot-logging-core-sink-trace.md`
## Worktree To Active Topic Map
@@ -63,15 +64,16 @@ help the current worktree land on the right recovery documents without scanning
- Branch: `feat/data-repository-persistence`
- Worktree hint: `GFramework-data-repository-persistence`
- Priority 1: `data-repository-persistence`
-- Branch: `feat/godot-logging-compliance-polish`
- - Worktree hint: `GFramework`
- - Priority 1: `godot-logging-compliance-polish`
- Branch: `feat/semantic-release-versioning`
- Worktree hint: `GFramework`
- Priority 1: `semantic-release-versioning`
+- Branch: `feat/godot-logging-core-sink`
+ - Worktree hint: `GFramework`
+ - Priority 1: `godot-logging-core-sink`
- Branch: `docs/sdk-update-documentation`
- Worktree hint: `GFramework-update-documentation`
- Priority 1: `documentation-full-coverage-governance`
+
## Archived Topics
- `analyzer-warning-reduction`
@@ -83,3 +85,6 @@ help the current worktree land on the right recovery documents without scanning
- `documentation-governance-and-refresh`
- Archive root: `ai-plan/public/archive/documentation-governance-and-refresh/`
- Note: PR #268 已合并;文档治理与 Godot 栏目刷新阶段已完成,后续仅作为历史恢复材料保留。
+- `godot-logging-compliance-polish`
+ - Archive root: `ai-plan/public/archive/godot-logging-compliance-polish/`
+ - Note: PR #314 已合并到 `origin/main`;Godot logging 宿主集成、热重载、结构化输出和 review follow-up 已收尾,后续 Core appender / sink 统一评估应新建 active topic。
diff --git a/ai-plan/public/godot-logging-compliance-polish/todos/godot-logging-compliance-polish-tracking.md b/ai-plan/public/archive/godot-logging-compliance-polish/todos/godot-logging-compliance-polish-tracking.md
similarity index 85%
rename from ai-plan/public/godot-logging-compliance-polish/todos/godot-logging-compliance-polish-tracking.md
rename to ai-plan/public/archive/godot-logging-compliance-polish/todos/godot-logging-compliance-polish-tracking.md
index b2b18dcd..506d0425 100644
--- a/ai-plan/public/godot-logging-compliance-polish/todos/godot-logging-compliance-polish-tracking.md
+++ b/ai-plan/public/archive/godot-logging-compliance-polish/todos/godot-logging-compliance-polish-tracking.md
@@ -5,17 +5,17 @@
继续把 `GFramework.Godot.Logging` 从“基础可用的 Godot 输出适配”收敛成“对齐 `GodotLogger` 优点、但保持
GFramework 自身日志抽象不分叉”的稳定宿主层,并为后续 Godot / Core 日志统一留下清晰恢复点。
-## 当前恢复点
+## 完成状态
- 恢复点编号:`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 中仍适用的 XML docs、热路径分配、结构化属性兜底、文档示例和 tracking 精简问题
- - 下一轮优先刷新 PR review / CI 反馈,避免继续扩大 Godot logging API 面
+ - PR #314 已合并到 `origin/main`,当前主题从默认 boot 路径移入归档
## 当前状态摘要
@@ -31,7 +31,8 @@ GFramework 自身日志抽象不分叉”的稳定宿主层,并为后续 Godot
## 当前活跃事实
-- 当前主题由分支 `feat/godot-logging-compliance-polish` 驱动,并已在 `ai-plan/public/README.md` 建立映射
+- 本主题归档前由分支 `feat/godot-logging-compliance-polish` 驱动,PR #314 合并后已从
+ `ai-plan/public/README.md` 的 active topic 映射移除
- `ai-libs/GodotLogger` 的 MIT 许可证已复制到 `third-party-licenses/GodotLogger/LICENSE`
- `GodotLog` 当前的配置发现顺序为:
- `GODOT_LOGGER_CONFIG`
@@ -53,12 +54,12 @@ GFramework 自身日志抽象不分叉”的稳定宿主层,并为后续 Godot
- PR #314 最新 follow-up 中,`DeferredLogger` 格式化重载现在委托给 inner logger,`GodotLogger` 默认 options
provider 已改为构造时缓存,结构化属性会跳过空白 key 并使用 trimmed key
-## 当前风险
+## 收尾风险
- 双入口生命周期风险:如果同一宿主同时混用 `LoggerFactoryResolver.Provider` 与 `GodotLog`,需要明确谁是最终默认 provider
- 缓解措施:当前文档与实现都保留 `GodotLog.UseAsDefaultProvider()`,并继续把 `ArchitectureConfiguration` 方式写成默认推荐路径
- Core / Godot 管线分离风险:Godot 侧虽然已有热重载与配置发现,但还没有变成 Core 可组合 appender
- - 缓解措施:下一轮只评估“Godot sink / appender 化”,不再继续扩张独立的 Godot logging 面
+ - 缓解措施:若后续重启本方向,应新建独立 topic 评估“Godot sink / appender 化”,不要在已归档主题继续扩张独立的 Godot logging 面
- 配置热重载的宿主差异风险:Godot 编辑器、导出包和测试宿主的文件系统语义不完全一致
- 缓解措施:active 入口先锁定 discovery / reload 语义,后续若遇到平台差异,再用定向回归和文档补充收口
- `GodotLog.ConfigurationPath` 的“不会 materialize”语义没有加入自动化测试
@@ -80,11 +81,14 @@ GFramework 自身日志抽象不分叉”的稳定宿主层,并为后续 Godot
- `dotnet format GFramework.Godot.Tests/GFramework.Godot.Tests.csproj --verify-no-changes --no-restore --include ...`
- 结果:通过
- 备注:include 范围为本轮修改的 C# 文件;全项目 format 仍命中既有行尾 / 编码问题,详见 trace
+- `dotnet build GFramework.sln -c Release`
+ - 结果:通过,`0 warning / 0 error`
+ - 备注:2026-05-03 在归档维护分支补跑仓库级 Release build,验证归档改动不会影响解决方案构建
- 历史验证明细已保留在 [执行 trace](../traces/godot-logging-compliance-polish-trace.md) 的 `RP-001 验证` 与
`RP-002 验证` 小节,active tracking 入口只保留当前恢复点相关结果
-## 下一步
+## 归档说明
-1. 提交 RP-003 review follow-up 改动
-2. 刷新 PR review / CI 状态,确认最新 head 上 CodeRabbit 与 Greptile 线程是否关闭或变为 stale
-3. 若 CI 仍报 MegaLinter `dotnet-format` restore 失败,优先复核 Actions restore 环境,而不是继续改本地格式
+1. 本主题已随 PR #314 合并到 `origin/main`
+2. 默认 boot 索引不再指向本主题
+3. 后续若继续做 Godot logging 与 Core appender / sink 的统一设计,应建立新的 active topic
diff --git a/ai-plan/public/godot-logging-compliance-polish/traces/godot-logging-compliance-polish-trace.md b/ai-plan/public/archive/godot-logging-compliance-polish/traces/godot-logging-compliance-polish-trace.md
similarity index 87%
rename from ai-plan/public/godot-logging-compliance-polish/traces/godot-logging-compliance-polish-trace.md
rename to ai-plan/public/archive/godot-logging-compliance-polish/traces/godot-logging-compliance-polish-trace.md
index 915585c8..9823b69e 100644
--- a/ai-plan/public/godot-logging-compliance-polish/traces/godot-logging-compliance-polish-trace.md
+++ b/ai-plan/public/archive/godot-logging-compliance-polish/traces/godot-logging-compliance-polish-trace.md
@@ -117,3 +117,24 @@
1. 提交 RP-003 review follow-up 改动
2. 刷新 PR review,确认 CodeRabbit / Greptile 线程是否关闭或 stale
3. 若 CI 仍只有 MegaLinter `dotnet-format` restore 失败,继续定位 Actions restore 环境而不是扩大本地格式清理范围
+
+### 阶段:主题归档(RP-004)
+
+- PR #314 已合并,当前分支 head 与 `origin/main` 同为 merge commit `918a61f3`
+- 旧 upstream branch `origin/feat/godot-logging-compliance-polish` 已不存在
+- 当前 batch stop condition 使用 `origin/main` 作为 baseline;归档前分支累计 diff 为 `0` 个文件
+- 接受的收尾动作:
+ - 将 `godot-logging-compliance-polish` 从默认 boot active topic 中移除
+ - 将主题恢复文档移动到 `ai-plan/public/archive/godot-logging-compliance-polish/`
+ - 在 public index 的 archived topics 中保留主题位置和合并结论
+
+### RP-004 验证
+
+- `dotnet build GFramework.sln -c Release`
+ - 结果:通过,`0 warning / 0 error`
+ - 备注:归档维护只触及 `ai-plan/public/**`,本次 build 用于满足仓库完成标准并确认解决方案仍可构建
+
+### RP-004 下一步
+
+1. 若继续推进 Godot logging 与 Core 的统一输出管线,建立新的 active topic
+2. 当前归档维护已完成;后续只需提交并发布归档分支
diff --git a/ai-plan/public/godot-logging-core-sink/todos/godot-logging-core-sink-tracking.md b/ai-plan/public/godot-logging-core-sink/todos/godot-logging-core-sink-tracking.md
new file mode 100644
index 00000000..e1ac8d93
--- /dev/null
+++ b/ai-plan/public/godot-logging-core-sink/todos/godot-logging-core-sink-tracking.md
@@ -0,0 +1,43 @@
+# Godot Logging Core Sink 跟踪
+
+## 目标
+
+在 `GFramework.Godot.Logging` 已完成宿主便利层收口后,评估并推进 Godot 输出与 `GFramework.Core` 日志扩展点的统一。
+本主题优先判断是否应把 Godot 输出沉淀为 Core 可组合的 appender / sink,而不是继续扩张 Godot-only logging 管线。
+
+## 当前恢复点
+
+- 恢复点编号:`GODOT-LOGGING-CORE-SINK-RP-001`
+- 当前阶段:`启动与边界确认`
+- 当前焦点:
+ - 复查 `GFramework.Core` 当前日志抽象、provider、appender / sink 能力与扩展边界
+ - 对照已归档的 `godot-logging-compliance-polish` 结论,确认哪些能力应迁移为 Core 通用扩展,哪些能力应保留在 Godot 宿主层
+ - 形成最小实现路径,避免同时引入第二套日志 API 或破坏现有 `GodotLog` 入口
+
+## 已知输入
+
+- `godot-logging-compliance-polish` 已归档,PR #314 已合并到 `origin/main`
+- 归档主题确认:
+ - `GFramework.Core` 仍是主日志框架
+ - `GFramework.Godot.Logging` 已补齐 `GodotLog`、延迟 logger、配置发现、热重载和结构化属性渲染
+ - 下一阶段应新建 topic 评估 Godot sink / appender 化,而不是继续在归档主题内扩张
+- 当前分支同时承载归档收尾与本 active topic 启动,避免为纯归档维护单独开 PR
+
+## 待办
+
+1. 盘点 `GFramework.Core` 日志扩展点与 Godot 侧 logger/provider 的实际耦合点
+2. 判断 Core appender / sink 抽象是否已足够承载 Godot 输出,还是需要先补齐抽象层能力
+3. 制定兼容路径:保留 `GodotLog` 用户入口,同时让底层输出走 Core 可组合管线
+4. 为选定方案补充 targeted tests 与 `docs/zh-CN/` adoption guidance
+
+## 验证
+
+- `dotnet build GFramework.sln -c Release`
+ - 结果:通过,`0 warning / 0 error`
+ - 备注:2026-05-03 在创建本 active topic 前已验证归档收尾分支;后续实现改动需要按受影响项目重新验证
+
+## 下一步
+
+1. 从 `GFramework.Core` 与 `GFramework.Godot.Logging` 源码开始做只读盘点
+2. 在 trace 中记录候选设计和不采用的扩张路径
+3. 确认实现边界后再修改代码与文档
diff --git a/ai-plan/public/godot-logging-core-sink/traces/godot-logging-core-sink-trace.md b/ai-plan/public/godot-logging-core-sink/traces/godot-logging-core-sink-trace.md
new file mode 100644
index 00000000..e487900b
--- /dev/null
+++ b/ai-plan/public/godot-logging-core-sink/traces/godot-logging-core-sink-trace.md
@@ -0,0 +1,31 @@
+# Godot Logging Core Sink Trace
+
+## 2026-05-03
+
+### RP-001 启动
+
+- 新建 active topic:`godot-logging-core-sink`
+- 当前分支:`feat/godot-logging-core-sink`
+- 启动背景:
+ - `godot-logging-compliance-polish` 已随 PR #314 合并并归档
+ - 用户明确要求归档收尾不要作为独立分支推进,而是跟下一 active topic 一起提交
+ - 本分支因此同时包含归档索引收口和新 topic 启动入口
+
+### 初始边界
+
+- 本主题要评估 Godot 输出是否应进入 Core appender / sink 模型
+- 不把 `Microsoft.Extensions.Logging` 生态原样搬入 GFramework
+- 不新增第二套业务日志 API;`GodotLog` 应保持为 Godot 宿主便利入口
+- 不在已归档的 `godot-logging-compliance-polish` topic 中继续扩张新需求
+
+### 验证
+
+- `dotnet build GFramework.sln -c Release`
+ - 结果:通过,`0 warning / 0 error`
+ - 备注:本次 build 在创建 active topic 前执行,用于验证归档维护对解决方案无影响;实现阶段需要重新跑受影响项目验证
+
+### 下一步
+
+1. 只读盘点 Core logging 抽象与 Godot logger/provider 的耦合点
+2. 记录候选设计,明确哪些能力进入 Core,哪些保留在 Godot 宿主层
+3. 确认方案后进入实现与文档更新
From 1009fee4a40efb441526670423614bf8efa881cf Mon Sep 17 00:00:00 2001
From: gewuyou <95328647+GeWuYou@users.noreply.github.com>
Date: Sun, 3 May 2026 11:03:58 +0800
Subject: [PATCH 2/4] =?UTF-8?q?feat(godot):=20=E6=96=B0=E5=A2=9E=20Godot?=
=?UTF-8?q?=20=E6=97=A5=E5=BF=97=20Appender?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
新增 GodotLogAppender 作为 Core ILogAppender 的 Godot 控制台落点
重构 GodotLogger 输出路径以复用 appender 管线并保持现有 ILogger 入口
补充 Godot appender 渲染测试、文档说明与 active topic 恢复记录
---
.../Logging/GodotLogAppenderTests.cs | 80 +++++++
GFramework.Godot/Logging/GodotLogAppender.cs | 211 ++++++++++++++++++
GFramework.Godot/Logging/GodotLogger.cs | 118 +++-------
GFramework.Godot/README.md | 2 +-
.../todos/godot-logging-core-sink-tracking.md | 31 ++-
.../traces/godot-logging-core-sink-trace.md | 29 +++
docs/zh-CN/core/logging.md | 5 +
docs/zh-CN/godot/index.md | 2 +-
docs/zh-CN/godot/logging.md | 50 +++--
9 files changed, 418 insertions(+), 110 deletions(-)
create mode 100644 GFramework.Godot.Tests/Logging/GodotLogAppenderTests.cs
create mode 100644 GFramework.Godot/Logging/GodotLogAppender.cs
diff --git a/GFramework.Godot.Tests/Logging/GodotLogAppenderTests.cs b/GFramework.Godot.Tests/Logging/GodotLogAppenderTests.cs
new file mode 100644
index 00000000..d16e73b9
--- /dev/null
+++ b/GFramework.Godot.Tests/Logging/GodotLogAppenderTests.cs
@@ -0,0 +1,80 @@
+using System;
+using System.Collections.Generic;
+using GFramework.Core.Abstractions.Logging;
+using GFramework.Godot.Logging;
+
+namespace GFramework.Godot.Tests.Logging;
+
+///
+/// Verifies the Godot appender edge that adapts Core log entries to Godot output rendering.
+///
+[TestFixture]
+public sealed class GodotLogAppenderTests
+{
+ ///
+ /// Verifies that the appender renders Core log entry data and merged structured properties.
+ ///
+ [Test]
+ public void Render_Should_Use_Core_LogEntry_And_Merged_Properties()
+ {
+ LogContext.Clear();
+ using var sceneContext = LogContext.Push("Scene", "Boot");
+ var appender = new GodotLogAppender(new GodotLoggerOptions
+ {
+ Mode = GodotLoggerMode.Release,
+ ReleaseOutputTemplate = "{timestamp:yyyyMMdd}|{level:u3}|{category}|{message}{properties}"
+ });
+ var entry = new LogEntry(
+ new DateTime(2026, 5, 3, 4, 5, 6, DateTimeKind.Utc),
+ LogLevel.Info,
+ "Game.Services.Inventory",
+ "Ready",
+ null,
+ new Dictionary(StringComparer.Ordinal)
+ {
+ [" "] = "ignored",
+ ["Score"] = 12.5m
+ });
+
+ var result = appender.Render(entry);
+
+ Assert.That(result, Is.EqualTo("20260503|INF|Game.Services.Inventory|Ready | Scene=Boot, Score=12.5"));
+ }
+
+ ///
+ /// Verifies that dynamic option providers are evaluated for each rendered log entry.
+ ///
+ [Test]
+ public void Render_Should_Use_Latest_Options_From_Provider()
+ {
+ var options = new GodotLoggerOptions
+ {
+ Mode = GodotLoggerMode.Release,
+ ReleaseOutputTemplate = "[release] {message}"
+ };
+ var appender = new GodotLogAppender(() => options);
+ var entry = new LogEntry(
+ DateTime.UtcNow,
+ LogLevel.Warning,
+ "Game",
+ "Reloaded",
+ null,
+ null);
+
+ var releaseResult = appender.Render(entry);
+
+ options = new GodotLoggerOptions
+ {
+ Mode = GodotLoggerMode.Debug,
+ DebugOutputTemplate = "[debug] {message}"
+ };
+
+ var debugResult = appender.Render(entry);
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(releaseResult, Is.EqualTo("[release] Reloaded"));
+ Assert.That(debugResult, Is.EqualTo("[debug] Reloaded"));
+ });
+ }
+}
diff --git a/GFramework.Godot/Logging/GodotLogAppender.cs b/GFramework.Godot/Logging/GodotLogAppender.cs
new file mode 100644
index 00000000..dd41b8df
--- /dev/null
+++ b/GFramework.Godot/Logging/GodotLogAppender.cs
@@ -0,0 +1,211 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using GFramework.Core.Abstractions.Logging;
+using Godot;
+
+namespace GFramework.Godot.Logging;
+
+///
+/// Writes Core instances to the Godot output APIs.
+///
+///
+/// This appender is the Godot-specific edge of the Core logging pipeline. It keeps formatting, color selection, and
+/// Godot debugger routing in the host package while allowing consumers to compose Godot output with Core
+/// features such as CompositeLogger, filters, and async appenders. The appender
+/// does not own unmanaged resources; and are therefore no-op lifecycle
+/// hooks that satisfy the shared appender contract.
+///
+public sealed class GodotLogAppender : ILogAppender
+{
+ private static readonly IReadOnlyDictionary EmptyProperties =
+ new Dictionary(StringComparer.Ordinal);
+
+ private readonly Func _optionsProvider;
+
+ ///
+ /// Initializes a Godot appender with default Godot logger options.
+ ///
+ public GodotLogAppender()
+ : this(new GodotLoggerOptions())
+ {
+ }
+
+ ///
+ /// Initializes a Godot appender with fixed Godot logger options.
+ ///
+ /// The formatting and routing options used for every appended entry.
+ /// is .
+ public GodotLogAppender(GodotLoggerOptions options)
+ : this(CreateFixedOptionsProvider(options))
+ {
+ }
+
+ ///
+ /// Initializes a Godot appender with a dynamic options provider.
+ ///
+ ///
+ /// Provides the latest formatting and routing options for each append operation.
+ ///
+ ///
+ /// The Godot logger provider uses this constructor so cached loggers observe hot-reloaded settings without
+ /// being recreated. The provider must be fast and thread-safe because it is called on the logging path.
+ ///
+ internal GodotLogAppender(Func optionsProvider)
+ {
+ _optionsProvider = optionsProvider ?? throw new ArgumentNullException(nameof(optionsProvider));
+ }
+
+ ///
+ /// Appends one Core log entry to Godot's console and debugger output.
+ ///
+ /// The Core log entry to render.
+ /// is .
+ public void Append(LogEntry entry)
+ {
+ ArgumentNullException.ThrowIfNull(entry);
+
+ var options = _optionsProvider();
+ var rendered = Render(entry, options);
+
+ if (options.Mode == GodotLoggerMode.Debug)
+ {
+ WriteDebug(entry.Level, rendered);
+ }
+ else
+ {
+ GD.Print(rendered);
+ }
+
+ if (entry.Exception != null)
+ {
+ GD.PrintErr(entry.Exception.ToString());
+ }
+ }
+
+ ///
+ /// Completes pending writes.
+ ///
+ ///
+ /// Godot output APIs are synchronous from this appender's point of view, so there is no buffered state to
+ /// flush.
+ ///
+ public void Flush()
+ {
+ }
+
+ ///
+ /// Releases appender resources.
+ ///
+ ///
+ /// The appender does not own disposable Godot resources. This method exists to honor the Core appender
+ /// lifecycle contract and to remain composable with factories that dispose appenders uniformly.
+ ///
+ public void Dispose()
+ {
+ }
+
+ ///
+ /// Formats structured properties for the {properties} template placeholder.
+ ///
+ /// The already-merged property set from a Core .
+ ///
+ /// A leading separator plus formatted properties, or an empty string when no valid properties exist.
+ ///
+ ///
+ /// Blank keys are ignored because they cannot produce useful structured output and can come from
+ /// caller-provided tuples. Valid keys are trimmed at render time so the appender never mutates the original
+ /// property dictionary.
+ ///
+ internal static string FormatProperties(IReadOnlyDictionary? properties)
+ {
+ if (properties == null || properties.Count == 0)
+ {
+ return string.Empty;
+ }
+
+ var formattedProperties = properties
+ .Where(static pair => !string.IsNullOrWhiteSpace(pair.Key))
+ .Select(static pair => $"{pair.Key.Trim()}={FormatValue(pair.Value)}")
+ .ToArray();
+
+ return formattedProperties.Length == 0
+ ? string.Empty
+ : " | " + string.Join(", ", formattedProperties);
+ }
+
+ ///
+ /// Renders a Core log entry without writing it to Godot.
+ ///
+ /// The Core log entry to render.
+ /// The line that would be sent to the selected Godot output API.
+ ///
+ /// Tests use this method to verify template and structured-property behavior without depending on Godot's
+ /// static output APIs.
+ ///
+ internal string Render(LogEntry entry)
+ {
+ ArgumentNullException.ThrowIfNull(entry);
+
+ return Render(entry, _optionsProvider());
+ }
+
+ private static Func CreateFixedOptionsProvider(GodotLoggerOptions options)
+ {
+ ArgumentNullException.ThrowIfNull(options);
+ return () => options;
+ }
+
+ private static string Render(LogEntry entry, GodotLoggerOptions options)
+ {
+ var templateText = options.Mode == GodotLoggerMode.Debug
+ ? options.DebugOutputTemplate
+ : options.ReleaseOutputTemplate;
+ var context = new GodotLogRenderContext(
+ entry.Timestamp,
+ entry.Level,
+ entry.LoggerName,
+ entry.Message,
+ options.GetColor(entry.Level),
+ FormatProperties(GetMergedProperties(entry)));
+
+ return GodotLogTemplate.Parse(templateText).Render(context);
+ }
+
+ private static IReadOnlyDictionary GetMergedProperties(LogEntry entry)
+ {
+ var allProperties = entry.GetAllProperties();
+ return allProperties.Count == 0 ? EmptyProperties : allProperties;
+ }
+
+ private static string FormatValue(object? value)
+ {
+ if (value == null)
+ {
+ return "null";
+ }
+
+ return value switch
+ {
+ IFormattable formattable => formattable.ToString(null, CultureInfo.InvariantCulture),
+ _ => value.ToString() ?? string.Empty
+ };
+ }
+
+ private static void WriteDebug(LogLevel level, string rendered)
+ {
+ GD.PrintRich(rendered);
+
+ switch (level)
+ {
+ case LogLevel.Fatal:
+ case LogLevel.Error:
+ GD.PushError(rendered);
+ break;
+ case LogLevel.Warning:
+ GD.PushWarning(rendered);
+ break;
+ }
+ }
+}
diff --git a/GFramework.Godot/Logging/GodotLogger.cs b/GFramework.Godot/Logging/GodotLogger.cs
index d6dbe974..763d480e 100644
--- a/GFramework.Godot/Logging/GodotLogger.cs
+++ b/GFramework.Godot/Logging/GodotLogger.cs
@@ -1,19 +1,23 @@
using System;
using System.Collections.Generic;
-using System.Globalization;
-using System.Linq;
using GFramework.Core.Abstractions.Logging;
using GFramework.Core.Logging;
-using Godot;
namespace GFramework.Godot.Logging;
///
/// Godot platform logger implementation.
///
+///
+/// This logger preserves the existing entry point while delegating output to
+/// so Godot rendering remains compatible with the Core appender pipeline.
+///
public sealed class GodotLogger : AbstractLogger
{
- private readonly Func _optionsProvider;
+ private static readonly IReadOnlyDictionary EmptyProperties =
+ new Dictionary(StringComparer.Ordinal);
+
+ private readonly GodotLogAppender _appender;
///
/// Initializes a logger that preserves the historical fixed-format template.
@@ -45,11 +49,14 @@ public sealed class GodotLogger : AbstractLogger
/// 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 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.
+ /// being recreated. The default public constructor supplies a fixed provider to avoid allocation on the log
+ /// path.
///
internal GodotLogger(
string name,
@@ -57,7 +64,8 @@ public sealed class GodotLogger : AbstractLogger
Func minLevelProvider)
: base(name, minLevelProvider ?? throw new ArgumentNullException(nameof(minLevelProvider)))
{
- _optionsProvider = optionsProvider ?? throw new ArgumentNullException(nameof(optionsProvider));
+ _appender = new GodotLogAppender(
+ optionsProvider ?? throw new ArgumentNullException(nameof(optionsProvider)));
}
///
@@ -74,6 +82,9 @@ public sealed class GodotLogger : AbstractLogger
///
/// Uses Godot-aware structured rendering instead of the base string concatenation fallback.
///
+ /// The log level.
+ /// The message body before Godot template rendering.
+ /// Structured properties appended through the configured Godot template.
public override void Log(LogLevel level, string message, params (string Key, object? Value)[] properties)
{
if (!IsEnabled(level))
@@ -87,6 +98,10 @@ public sealed class GodotLogger : AbstractLogger
///
/// Uses Godot-aware structured rendering instead of the base string concatenation fallback.
///
+ /// The log level.
+ /// The message body before Godot template rendering.
+ /// The optional exception written after the rendered message.
+ /// Structured properties appended through the configured Godot template.
public override void Log(
LogLevel level,
string message,
@@ -107,73 +122,44 @@ public sealed class GodotLogger : AbstractLogger
Exception? exception,
(string Key, object? Value)[]? properties)
{
- var options = _optionsProvider();
- var templateText = options.Mode == GodotLoggerMode.Debug
- ? options.DebugOutputTemplate
- : options.ReleaseOutputTemplate;
- var context = new GodotLogRenderContext(
+ var entry = new LogEntry(
DateTime.UtcNow,
level,
Name(),
message,
- options.GetColor(level),
- FormatProperties(properties));
- var rendered = GodotLogTemplate.Parse(templateText).Render(context);
+ exception,
+ ToPropertiesDictionary(properties));
- if (options.Mode == GodotLoggerMode.Debug)
- {
- WriteDebug(level, rendered);
- }
- else
- {
- GD.Print(rendered);
- }
-
- if (exception != null)
- {
- GD.PrintErr(exception.ToString());
- }
+ _appender.Append(entry);
}
private static string FormatProperties((string Key, object? Value)[]? properties)
{
- var merged = MergeProperties(properties);
- if (merged.Count == 0)
- {
- return string.Empty;
- }
-
- return " | " + string.Join(", ", merged.Select(static pair => $"{pair.Key}={FormatValue(pair.Value)}"));
+ return GodotLogAppender.FormatProperties(ToPropertiesDictionary(properties));
}
- private static IReadOnlyDictionary MergeProperties((string Key, object? Value)[]? properties)
+ private static IReadOnlyDictionary ToPropertiesDictionary(
+ (string Key, object? Value)[]? properties)
{
- var contextProperties = LogContext.Current;
- if ((properties == null || properties.Length == 0) && contextProperties.Count == 0)
+ if (properties == null || properties.Length == 0)
{
return EmptyProperties;
}
- var merged = new Dictionary(contextProperties, StringComparer.Ordinal);
- if (properties != null)
+ var result = new Dictionary(StringComparer.Ordinal);
+ foreach (var property in properties)
{
- foreach (var property in properties)
+ if (string.IsNullOrWhiteSpace(property.Key))
{
- if (string.IsNullOrWhiteSpace(property.Key))
- {
- continue;
- }
-
- merged[property.Key.Trim()] = property.Value;
+ continue;
}
+
+ result[property.Key.Trim()] = property.Value;
}
- return merged;
+ return result.Count == 0 ? EmptyProperties : result;
}
- private static readonly IReadOnlyDictionary EmptyProperties =
- new Dictionary(StringComparer.Ordinal);
-
private static Func CreateFixedOptionsProvider(LogLevel minLevel)
{
var options = GodotLoggerOptions.ForMinimumLevel(minLevel);
@@ -191,34 +177,4 @@ public sealed class GodotLogger : AbstractLogger
ArgumentNullException.ThrowIfNull(options);
return () => options.GetEffectiveMinLevel();
}
-
- private static string FormatValue(object? value)
- {
- if (value == null)
- {
- return "null";
- }
-
- return value switch
- {
- IFormattable formattable => formattable.ToString(null, CultureInfo.InvariantCulture),
- _ => value.ToString() ?? string.Empty
- };
- }
-
- private static void WriteDebug(LogLevel level, string rendered)
- {
- GD.PrintRich(rendered);
-
- switch (level)
- {
- case LogLevel.Fatal:
- case LogLevel.Error:
- GD.PushError(rendered);
- break;
- case LogLevel.Warning:
- GD.PushWarning(rendered);
- break;
- }
- }
}
diff --git a/GFramework.Godot/README.md b/GFramework.Godot/README.md
index 3e42f487..7f89c45f 100644
--- a/GFramework.Godot/README.md
+++ b/GFramework.Godot/README.md
@@ -72,7 +72,7 @@ Scene / UI、配置、存储、设置、日志与协程能力接到 `Node`、`Sc
- 节点扩展与 `Signal(...)` fluent API
- `GodotTimeSource` 与协程时间分段
-- Godot 日志 provider
+- Godot 日志 provider 与 `GodotLogAppender`
- 暂停处理、节点池与富文本效果支持
这些目录都是“宿主适配层”,不是新的 gameplay 抽象层。
diff --git a/ai-plan/public/godot-logging-core-sink/todos/godot-logging-core-sink-tracking.md b/ai-plan/public/godot-logging-core-sink/todos/godot-logging-core-sink-tracking.md
index e1ac8d93..0c04f2bf 100644
--- a/ai-plan/public/godot-logging-core-sink/todos/godot-logging-core-sink-tracking.md
+++ b/ai-plan/public/godot-logging-core-sink/todos/godot-logging-core-sink-tracking.md
@@ -7,12 +7,12 @@
## 当前恢复点
-- 恢复点编号:`GODOT-LOGGING-CORE-SINK-RP-001`
-- 当前阶段:`启动与边界确认`
+- 恢复点编号:`GODOT-LOGGING-CORE-SINK-RP-002`
+- 当前阶段:`Godot appender 最小实现已验证`
- 当前焦点:
- - 复查 `GFramework.Core` 当前日志抽象、provider、appender / sink 能力与扩展边界
- - 对照已归档的 `godot-logging-compliance-polish` 结论,确认哪些能力应迁移为 Core 通用扩展,哪些能力应保留在 Godot 宿主层
- - 形成最小实现路径,避免同时引入第二套日志 API 或破坏现有 `GodotLog` 入口
+ - `GFramework.Godot.Logging.GodotLogAppender` 已作为 Core `ILogAppender` 的 Godot 宿主落点落地
+ - `GodotLogger` 保留原有 `ILogger` 入口,但底层输出委托给 appender
+ - Godot / Core logging 文档已说明 provider 与 appender 的组合边界
## 已知输入
@@ -25,19 +25,26 @@
## 待办
-1. 盘点 `GFramework.Core` 日志扩展点与 Godot 侧 logger/provider 的实际耦合点
-2. 判断 Core appender / sink 抽象是否已足够承载 Godot 输出,还是需要先补齐抽象层能力
-3. 制定兼容路径:保留 `GodotLog` 用户入口,同时让底层输出走 Core 可组合管线
-4. 为选定方案补充 targeted tests 与 `docs/zh-CN/` adoption guidance
+1. 已完成:盘点 `GFramework.Core` 日志扩展点与 Godot 侧 logger/provider 的实际耦合点
+2. 已完成:确认现有 Core `ILogAppender` 足够承载 Godot 输出,无需新增第二套 sink API
+3. 已完成:保留 `GodotLog` / `GodotLoggerFactoryProvider` 入口,并让 `GodotLogger` 底层走 `GodotLogAppender`
+4. 已完成:补充 `GodotLogAppender` targeted tests 与 `docs/zh-CN/` adoption guidance
+5. 待确认:是否还需要在后续阶段补一个配置化 factory 示例,把 `GodotLogAppender` 与文件 / async appender 显式组合
## 验证
- `dotnet build GFramework.sln -c Release`
- 结果:通过,`0 warning / 0 error`
- 备注:2026-05-03 在创建本 active topic 前已验证归档收尾分支;后续实现改动需要按受影响项目重新验证
+- `dotnet test GFramework.Godot.Tests -c Release`
+ - 结果:通过,`75 passed / 0 failed / 0 skipped`
+ - 备注:覆盖 `GodotLogAppender` 渲染、动态 options provider、既有 Godot logging tests
+- `dotnet build GFramework.Godot -c Release`
+ - 结果:通过,`0 warning / 0 error`
+ - 备注:验证受影响运行时项目
## 下一步
-1. 从 `GFramework.Core` 与 `GFramework.Godot.Logging` 源码开始做只读盘点
-2. 在 trace 中记录候选设计和不采用的扩张路径
-3. 确认实现边界后再修改代码与文档
+1. 提交当前 appender 实现、测试、文档与 tracking 更新
+2. 如继续扩展本主题,优先评估是否需要示例化 `CompositeLogger + GodotLogAppender + FileAppender`,而不是新增 API
+3. 若无新增需求,本主题可在 PR 验证通过后归档
diff --git a/ai-plan/public/godot-logging-core-sink/traces/godot-logging-core-sink-trace.md b/ai-plan/public/godot-logging-core-sink/traces/godot-logging-core-sink-trace.md
index e487900b..78abfb76 100644
--- a/ai-plan/public/godot-logging-core-sink/traces/godot-logging-core-sink-trace.md
+++ b/ai-plan/public/godot-logging-core-sink/traces/godot-logging-core-sink-trace.md
@@ -29,3 +29,32 @@
1. 只读盘点 Core logging 抽象与 Godot logger/provider 的耦合点
2. 记录候选设计,明确哪些能力进入 Core,哪些保留在 Godot 宿主层
3. 确认方案后进入实现与文档更新
+
+### RP-002 Godot Appender 最小实现
+
+- 盘点结论:
+ - Core 已有 `ILogAppender`、`LogEntry`、`CompositeLogger`、filter、formatter 与 async appender
+ - Godot 侧主要耦合点是 `GodotLogger` 直接持有模板渲染和 `GD.*` 输出逻辑
+ - 不需要先新增 Core sink 抽象;把 Godot 输出沉淀为 Godot 包内的 `ILogAppender` 已能复用 Core 管线
+- 已实施:
+ - 新增 `GFramework.Godot.Logging.GodotLogAppender`
+ - `GodotLogger` 保留原有 public API,并把输出委托给 `GodotLogAppender`
+ - 新增 `GFramework.Godot.Tests/Logging/GodotLogAppenderTests.cs`
+ - 更新 `GFramework.Godot/README.md`、`docs/zh-CN/core/logging.md`、`docs/zh-CN/godot/index.md`、
+ `docs/zh-CN/godot/logging.md`
+- 采用的兼容边界:
+ - `GodotLog`、`GodotLoggerFactory`、`GodotLoggerFactoryProvider` 不改用户调用方式
+ - Godot 输出可作为 Core appender 被自定义 factory / `CompositeLogger` 组合
+ - 文件、JSON、namespace filter、async 等仍由 Core logging 组件负责,Godot 包只提供宿主控制台落点
+
+### 验证
+
+- `dotnet test GFramework.Godot.Tests -c Release`
+ - 结果:通过,`75 passed / 0 failed / 0 skipped`
+- `dotnet build GFramework.Godot -c Release`
+ - 结果:通过,`0 warning / 0 error`
+
+### 下一步
+
+1. 提交当前 appender 实现与文档更新
+2. 若继续推进本主题,优先补充组合示例或归档 topic,不新增第二套日志 API
diff --git a/docs/zh-CN/core/logging.md b/docs/zh-CN/core/logging.md
index 0b46635c..9382cf37 100644
--- a/docs/zh-CN/core/logging.md
+++ b/docs/zh-CN/core/logging.md
@@ -79,6 +79,10 @@ using (LogContext.Push("RequestId", requestId))
`LoggingConfigurationLoader` 读取 `LoggingConfiguration`,再把自定义 `ILoggerFactoryProvider` 挂到
`ArchitectureConfiguration.LoggerProperties.LoggerFactoryProvider` 或 `LoggerFactoryResolver.Provider`。
+宿主包也可以提供自己的 appender。Godot 项目如果需要把 Core 日志管线输出到 Godot 控制台,可以引用
+`GFramework.Godot.Logging.GodotLogAppender`,再用 `CompositeLogger` 或自定义 factory 把它和文件、JSON、异步输出等
+Core 组件组合在同一条调用面下。
+
## 什么时候该换 provider
下面这些场景通常不该只靠改 `MinLevel`:
@@ -87,5 +91,6 @@ using (LogContext.Push("RequestId", requestId))
- 需要按 namespace / level 做过滤
- 需要 JSON 格式日志
- 需要组合多个 appender
+- 需要把输出落到 Godot、Unity 或其他宿主控制台
这时更合理的做法是保留 `ILogger` 调用面不变,只替换 provider / factory / formatter / appender 组合。
diff --git a/docs/zh-CN/godot/index.md b/docs/zh-CN/godot/index.md
index e3b6d1f0..5b0bfe34 100644
--- a/docs/zh-CN/godot/index.md
+++ b/docs/zh-CN/godot/index.md
@@ -16,7 +16,7 @@ description: 以当前 GFramework.Godot 源码、测试与 CoreGrid 接线为准
- 节点运行时辅助:`WaitUntilReadyAsync()`、`AddChildXAsync()`、`QueueFreeX()`、`UnRegisterWhenNodeExitTree(...)`
- Godot 风格的 Scene / UI 工厂与 registry:`GodotSceneFactory`、`GodotUiFactory`
- Godot 特化的存储、设置与配置加载:`GodotFileStorage`、`GodotAudioSettings`、`GodotYamlConfigLoader`
-- 少量面向运行时交互的扩展:`Signal(...)` fluent API、暂停处理、富文本效果、协程时间源
+- 少量面向运行时交互的扩展:`Signal(...)` fluent API、`GodotLogAppender`、暂停处理、富文本效果、协程时间源
它不是 `[GetNode]`、`[BindNodeSignal]`、`AutoLoads`、`InputActions` 的来源。这些能力属于
`GFramework.Godot.SourceGenerators`。
diff --git a/docs/zh-CN/godot/logging.md b/docs/zh-CN/godot/logging.md
index b6a71b44..a850d247 100644
--- a/docs/zh-CN/godot/logging.md
+++ b/docs/zh-CN/godot/logging.md
@@ -9,6 +9,7 @@ description: 以当前 GFramework.Godot.Logging 源码与 CoreGrid 接线为准
除了把日志写到 Godot 控制台,它现在还补上了 Godot 宿主常见的接入便利层:
- `GodotLog` 静态入口
+- `GodotLogAppender`,用于接入 Core appender 管线
- 配置文件自动发现
- 运行期配置热重载
- 延迟 logger 解析,适合 `static readonly` 字段
@@ -20,7 +21,9 @@ description: 以当前 GFramework.Godot.Logging 源码与 CoreGrid 接线为准
### `GodotLogger`
-`GodotLogger` 继承自 `AbstractLogger`,负责把日志写到 Godot 的输出 API:
+`GodotLogger` 继承自 `AbstractLogger`,保留原有 `ILogger` 使用面。它现在把实际输出委托给
+`GodotLogAppender`,所以 `GodotLoggerFactoryProvider` 继续可用,同时 Godot 输出也能作为 Core appender 管线的一个
+可组合目标:
```csharp
public sealed class GodotLogger(
@@ -32,10 +35,27 @@ public sealed class GodotLogger(
当前实现里的几个关键语义:
- 时间戳使用 `DateTime.UtcNow`
-- 输出前缀格式是 `[yyyy-MM-dd HH:mm:ss.fff] LEVEL [LoggerName]`
-- `exception` 不会被单独结构化处理,而是直接追加到消息后面
-- `Trace` / `Debug` 走 `GD.PrintRich(...)`
-- `Info` / `Warning` / `Error` / `Fatal` 分别走 Godot 自身的普通、警告和错误输出通道
+- 模板、级别、颜色仍由 `GodotLoggerOptions` 或配置文件控制
+- 结构化属性来自 `IStructuredLogger` 参数和 `LogContext`
+- `exception` 会在渲染后的主消息之后写入 Godot 错误输出
+
+### `GodotLogAppender`
+
+`GodotLogAppender` 实现 Core 的 `ILogAppender`:
+
+```csharp
+public sealed class GodotLogAppender : ILogAppender
+{
+ public GodotLogAppender();
+ public GodotLogAppender(GodotLoggerOptions options);
+ public void Append(LogEntry entry);
+ public void Flush();
+}
+```
+
+它适合在已经使用 `CompositeLogger`、`AsyncLogAppender`、filter 或自定义 factory 的项目里,把 Godot 控制台输出作为
+其中一个落点,而不是为 Godot 重新定义一套业务日志 API。`Flush()` 和 `Dispose()` 没有额外副作用,因为 Godot 输出 API
+对这个 appender 来说没有持有的缓冲区或外部资源。
### `GodotLoggerFactory`
@@ -109,7 +129,7 @@ var logger = GodotLog.CreateLogger();
"Mode": "Debug",
"DebugMinLevel": "Debug",
"ReleaseMinLevel": "Info",
- "DebugOutputTemplate": "[{timestamp:HH:mm:ss.fff}] [color={color}][{level:u3}][/color] [{category:l16}] {message}{properties}",
+ "DebugOutputTemplate": "[{timestamp:HH:mm:ss.fff}] [color={color}]{level:u3}[/color] {message}{properties}",
"ReleaseOutputTemplate": "[{timestamp:HH:mm:ss.fff}] [{level:u3}] [{category:l16}] {message}{properties}",
"Colors": {
"Info": "white",
@@ -214,16 +234,16 @@ RegisterHandler(new LoggingTransitionHandler());
## Godot 控制台输出语义
-当前 `GodotLogger.Write(...)` 的级别映射如下:
+当前 `GodotLogAppender.Append(...)` 的级别映射如下:
| 日志级别 | Godot 输出 API | 当前行为 |
| --- | --- | --- |
-| `Trace` | `GD.PrintRich(...)` | 使用灰色富文本输出 |
-| `Debug` | `GD.PrintRich(...)` | 使用青色富文本输出 |
-| `Info` | `GD.Print(...)` | 普通控制台输出 |
-| `Warning` | `GD.PushWarning(...)` | 进入 Godot 警告通道 |
-| `Error` | `GD.PrintErr(...)` | 输出到错误流 |
-| `Fatal` | `GD.PushError(...)` | 进入 Godot 错误通道 |
+| `Trace` | `GD.PrintRich(...)` 或 `GD.Print(...)` | Debug 模式使用富文本,Release 模式使用普通输出 |
+| `Debug` | `GD.PrintRich(...)` 或 `GD.Print(...)` | Debug 模式使用富文本,Release 模式使用普通输出 |
+| `Info` | `GD.PrintRich(...)` 或 `GD.Print(...)` | Debug 模式使用富文本,Release 模式使用普通输出 |
+| `Warning` | `GD.PrintRich(...)` + `GD.PushWarning(...)` 或 `GD.Print(...)` | Debug 模式同时进入 Godot 警告通道 |
+| `Error` | `GD.PrintRich(...)` + `GD.PushError(...)` 或 `GD.Print(...)` | Debug 模式同时进入 Godot 错误通道 |
+| `Fatal` | `GD.PrintRich(...)` + `GD.PushError(...)` 或 `GD.Print(...)` | Debug 模式同时进入 Godot 错误通道 |
结构化属性如果通过 `IStructuredLogger` 或 `LogContext` 传入,也会追加到模板里的 `{properties}` 占位符。
@@ -259,8 +279,8 @@ System.IO.IOException: ...
- 当前推荐接法仍然是把 `GodotLoggerFactoryProvider` 放进 `ArchitectureConfiguration.LoggerProperties`;如果项目是纯
Godot 宿主,也可以在入口直接调用 `GodotLog.UseAsDefaultProvider()`
-- `GFramework.Godot.Logging` 只解决 Godot 控制台输出,不提供文件落盘、JSON formatter、异步 appender 或按 namespace
- 的复杂过滤
+- `GFramework.Godot.Logging` 只提供 Godot 控制台 appender;文件落盘、JSON formatter、异步 appender 或按 namespace
+ 的复杂过滤继续使用 Core 日志组件组合
- `GodotLogger` 只改变输出方式,不改变 `ILogger` 接口本身;业务代码不需要切换到 Godot 专用日志 API
- `[Log]`、`[ContextAware]` 这类字段注入能力不属于 `GFramework.Godot.Logging`
- Scene / UI 的 `LoggingTransitionHandler` 位于 `GFramework.Game`,Godot 侧只是通过 provider 让它们输出到 Godot 控制台
From 3ced56be8b1b76ad7865cd0d2f291e9c08215a7f Mon Sep 17 00:00:00 2001
From: gewuyou <95328647+GeWuYou@users.noreply.github.com>
Date: Sun, 3 May 2026 13:24:24 +0800
Subject: [PATCH 3/4] =?UTF-8?q?chore(godot):=20=E5=A4=84=E7=90=86=20Godot?=
=?UTF-8?q?=20=E6=97=A5=E5=BF=97=20PR=20=E5=8F=8D=E9=A6=88?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 修复 GodotLogAppender 测试对结构化属性顺序的依赖
- 移除 GodotLogger 未使用的私有格式化包装方法
- 更新 ai-plan 默认索引和 trace 恢复记录,避免归档主题与重复标题干扰 boot
---
.../Logging/GodotLogAppenderTests.cs | 7 +++-
.../Logging/GodotLoggerSettingsLoaderTests.cs | 7 ++--
GFramework.Godot/Logging/GodotLogger.cs | 5 ---
ai-plan/public/README.md | 15 -------
.../todos/godot-logging-core-sink-tracking.md | 21 +++++++---
.../traces/godot-logging-core-sink-trace.md | 41 +++++++++++++++++--
6 files changed, 62 insertions(+), 34 deletions(-)
diff --git a/GFramework.Godot.Tests/Logging/GodotLogAppenderTests.cs b/GFramework.Godot.Tests/Logging/GodotLogAppenderTests.cs
index d16e73b9..dd98e173 100644
--- a/GFramework.Godot.Tests/Logging/GodotLogAppenderTests.cs
+++ b/GFramework.Godot.Tests/Logging/GodotLogAppenderTests.cs
@@ -38,7 +38,12 @@ public sealed class GodotLogAppenderTests
var result = appender.Render(entry);
- Assert.That(result, Is.EqualTo("20260503|INF|Game.Services.Inventory|Ready | Scene=Boot, Score=12.5"));
+ Assert.Multiple(() =>
+ {
+ Assert.That(result, Does.StartWith("20260503|INF|Game.Services.Inventory|Ready | "));
+ Assert.That(result, Does.Contain("Scene=Boot"));
+ Assert.That(result, Does.Contain("Score=12.5"));
+ });
}
///
diff --git a/GFramework.Godot.Tests/Logging/GodotLoggerSettingsLoaderTests.cs b/GFramework.Godot.Tests/Logging/GodotLoggerSettingsLoaderTests.cs
index 749ac715..74db1f6b 100644
--- a/GFramework.Godot.Tests/Logging/GodotLoggerSettingsLoaderTests.cs
+++ b/GFramework.Godot.Tests/Logging/GodotLoggerSettingsLoaderTests.cs
@@ -192,8 +192,8 @@ public sealed class GodotLoggerSettingsLoaderTests
[Test]
public void StructuredProperties_Should_Skip_Blank_Keys_And_Trim_Valid_Keys()
{
- var formatProperties = typeof(GodotLogger).GetMethod(
- "FormatProperties",
+ var toPropertiesDictionary = typeof(GodotLogger).GetMethod(
+ "ToPropertiesDictionary",
BindingFlags.NonPublic | BindingFlags.Static);
var properties = new (string Key, object? Value)[]
{
@@ -202,7 +202,8 @@ public sealed class GodotLoggerSettingsLoaderTests
(" Player ", 42)
};
- var result = formatProperties?.Invoke(null, [properties]);
+ var dictionary = toPropertiesDictionary?.Invoke(null, [properties]) as IReadOnlyDictionary;
+ var result = GodotLogAppender.FormatProperties(dictionary);
Assert.That(result, Is.EqualTo(" | Player=42"));
}
diff --git a/GFramework.Godot/Logging/GodotLogger.cs b/GFramework.Godot/Logging/GodotLogger.cs
index 763d480e..87adeebe 100644
--- a/GFramework.Godot/Logging/GodotLogger.cs
+++ b/GFramework.Godot/Logging/GodotLogger.cs
@@ -133,11 +133,6 @@ public sealed class GodotLogger : AbstractLogger
_appender.Append(entry);
}
- private static string FormatProperties((string Key, object? Value)[]? properties)
- {
- return GodotLogAppender.FormatProperties(ToPropertiesDictionary(properties));
- }
-
private static IReadOnlyDictionary ToPropertiesDictionary(
(string Key, object? Value)[]? properties)
{
diff --git a/ai-plan/public/README.md b/ai-plan/public/README.md
index 98876db1..53b6415f 100644
--- a/ai-plan/public/README.md
+++ b/ai-plan/public/README.md
@@ -73,18 +73,3 @@ help the current worktree land on the right recovery documents without scanning
- Branch: `docs/sdk-update-documentation`
- Worktree hint: `GFramework-update-documentation`
- Priority 1: `documentation-full-coverage-governance`
-
-## Archived Topics
-
-- `analyzer-warning-reduction`
- - Archive root: `ai-plan/public/archive/analyzer-warning-reduction/`
- - Note: 长期 warning-reduction 分支已收尾;PR #301 的最终 review follow-up 已本地闭环,后续仅作为历史恢复材料保留。
-- `cqrs-cache-docs-hardening`
- - Archive root: `ai-plan/public/archive/cqrs-cache-docs-hardening/`
- - Note: archived topics stay outside the default `boot` context until a user explicitly requests historical review.
-- `documentation-governance-and-refresh`
- - Archive root: `ai-plan/public/archive/documentation-governance-and-refresh/`
- - Note: PR #268 已合并;文档治理与 Godot 栏目刷新阶段已完成,后续仅作为历史恢复材料保留。
-- `godot-logging-compliance-polish`
- - Archive root: `ai-plan/public/archive/godot-logging-compliance-polish/`
- - Note: PR #314 已合并到 `origin/main`;Godot logging 宿主集成、热重载、结构化输出和 review follow-up 已收尾,后续 Core appender / sink 统一评估应新建 active topic。
diff --git a/ai-plan/public/godot-logging-core-sink/todos/godot-logging-core-sink-tracking.md b/ai-plan/public/godot-logging-core-sink/todos/godot-logging-core-sink-tracking.md
index 0c04f2bf..af8ac022 100644
--- a/ai-plan/public/godot-logging-core-sink/todos/godot-logging-core-sink-tracking.md
+++ b/ai-plan/public/godot-logging-core-sink/todos/godot-logging-core-sink-tracking.md
@@ -7,12 +7,13 @@
## 当前恢复点
-- 恢复点编号:`GODOT-LOGGING-CORE-SINK-RP-002`
-- 当前阶段:`Godot appender 最小实现已验证`
+- 恢复点编号:`GODOT-LOGGING-CORE-SINK-RP-003`
+- 当前阶段:`PR review follow-up 已验证`
- 当前焦点:
- `GFramework.Godot.Logging.GodotLogAppender` 已作为 Core `ILogAppender` 的 Godot 宿主落点落地
- `GodotLogger` 保留原有 `ILogger` 入口,但底层输出委托给 appender
- Godot / Core logging 文档已说明 provider 与 appender 的组合边界
+ - PR #315 最新 AI review 中仍适用的测试稳定性、dead private wrapper、boot index 与 trace heading 问题已处理
## 已知输入
@@ -29,7 +30,8 @@
2. 已完成:确认现有 Core `ILogAppender` 足够承载 Godot 输出,无需新增第二套 sink API
3. 已完成:保留 `GodotLog` / `GodotLoggerFactoryProvider` 入口,并让 `GodotLogger` 底层走 `GodotLogAppender`
4. 已完成:补充 `GodotLogAppender` targeted tests 与 `docs/zh-CN/` adoption guidance
-5. 待确认:是否还需要在后续阶段补一个配置化 factory 示例,把 `GodotLogAppender` 与文件 / async appender 显式组合
+5. 已完成:处理 PR #315 最新 review follow-up,移除默认 boot index 的 archived topics 区块并消除 trace 重复 heading
+6. 待确认:是否还需要在后续阶段补一个配置化 factory 示例,把 `GodotLogAppender` 与文件 / async appender 显式组合
## 验证
@@ -42,9 +44,16 @@
- `dotnet build GFramework.Godot -c Release`
- 结果:通过,`0 warning / 0 error`
- 备注:验证受影响运行时项目
+- `dotnet format GFramework.Godot --verify-no-changes --no-restore --include GFramework.Godot/Logging/GodotLogger.cs`
+ - 结果:通过
+- `dotnet format GFramework.Godot.Tests --verify-no-changes --no-restore --include GFramework.Godot.Tests/Logging/GodotLogAppenderTests.cs GFramework.Godot.Tests/Logging/GodotLoggerSettingsLoaderTests.cs`
+ - 结果:通过
+- `dotnet format GFramework.sln --verify-no-changes --no-restore`
+ - 结果:失败
+ - 备注:失败集中在仓库既有的 whitespace、final newline 与 charset 诊断,跨 `GFramework.Core`、`GFramework.Cqrs`、`GFramework.Game.Abstractions` 等未触碰项目;本轮改动用 scoped format 验证
## 下一步
-1. 提交当前 appender 实现、测试、文档与 tracking 更新
-2. 如继续扩展本主题,优先评估是否需要示例化 `CompositeLogger + GodotLogAppender + FileAppender`,而不是新增 API
-3. 若无新增需求,本主题可在 PR 验证通过后归档
+1. 提交当前 PR review follow-up
+2. 等待 PR #315 复查并确认 CodeRabbit / Greptile 线程是否关闭
+3. 如继续扩展本主题,优先评估是否需要示例化 `CompositeLogger + GodotLogAppender + FileAppender`,而不是新增 API
diff --git a/ai-plan/public/godot-logging-core-sink/traces/godot-logging-core-sink-trace.md b/ai-plan/public/godot-logging-core-sink/traces/godot-logging-core-sink-trace.md
index 78abfb76..45ae1e36 100644
--- a/ai-plan/public/godot-logging-core-sink/traces/godot-logging-core-sink-trace.md
+++ b/ai-plan/public/godot-logging-core-sink/traces/godot-logging-core-sink-trace.md
@@ -18,13 +18,13 @@
- 不新增第二套业务日志 API;`GodotLog` 应保持为 Godot 宿主便利入口
- 不在已归档的 `godot-logging-compliance-polish` topic 中继续扩张新需求
-### 验证
+### RP-001 验证
- `dotnet build GFramework.sln -c Release`
- 结果:通过,`0 warning / 0 error`
- 备注:本次 build 在创建 active topic 前执行,用于验证归档维护对解决方案无影响;实现阶段需要重新跑受影响项目验证
-### 下一步
+### RP-001 下一步
1. 只读盘点 Core logging 抽象与 Godot logger/provider 的耦合点
2. 记录候选设计,明确哪些能力进入 Core,哪些保留在 Godot 宿主层
@@ -47,14 +47,47 @@
- Godot 输出可作为 Core appender 被自定义 factory / `CompositeLogger` 组合
- 文件、JSON、namespace filter、async 等仍由 Core logging 组件负责,Godot 包只提供宿主控制台落点
-### 验证
+### RP-002 验证
- `dotnet test GFramework.Godot.Tests -c Release`
- 结果:通过,`75 passed / 0 failed / 0 skipped`
- `dotnet build GFramework.Godot -c Release`
- 结果:通过,`0 warning / 0 error`
-### 下一步
+### RP-002 下一步
1. 提交当前 appender 实现与文档更新
2. 若继续推进本主题,优先补充组合示例或归档 topic,不新增第二套日志 API
+
+### RP-003 PR Review Follow-up
+
+- 使用 `$gframework-pr-review` 抓取 PR #315 最新 review payload:
+ - CodeRabbit:3 个 open thread,分别指向 appender test 顺序依赖、默认 boot index 包含 archived topic、trace 重复 heading
+ - Greptile:1 个 open thread,指出 `GodotLogger.FormatProperties` 为 dead private wrapper
+ - Gemini Code Assist:无 open thread
+ - GitHub Test Reporter:`2264 passed / 0 failed`
+ - MegaLinter:`dotnet-format` 报 restore failure;本地进一步验证时发现 solution-wide format 还有既有 repo-wide 诊断
+- 已实施:
+ - `GodotLogAppenderTests` 改为验证固定前缀与结构化属性集合内容,不再依赖 `Dictionary` 枚举顺序
+ - 移除 `GodotLogger.FormatProperties` private wrapper,并把既有结构化属性测试改为验证生产路径使用的 `ToPropertiesDictionary` 与 `GodotLogAppender.FormatProperties`
+ - 从 `ai-plan/public/README.md` 移除 archived topics 区块,默认 boot index 只保留 active topic 与 worktree map
+ - 将 trace 中重复的 `### 验证` / `### 下一步` 改为 `RP-001` 与 `RP-002` 前缀,避免 MD024 anchor 冲突
+
+### RP-003 验证
+
+- `dotnet test GFramework.Godot.Tests -c Release`
+ - 结果:通过,`75 passed / 0 failed / 0 skipped`
+- `dotnet build GFramework.Godot -c Release`
+ - 结果:通过,`0 warning / 0 error`
+- `dotnet format GFramework.Godot --verify-no-changes --no-restore --include GFramework.Godot/Logging/GodotLogger.cs`
+ - 结果:通过
+- `dotnet format GFramework.Godot.Tests --verify-no-changes --no-restore --include GFramework.Godot.Tests/Logging/GodotLogAppenderTests.cs GFramework.Godot.Tests/Logging/GodotLoggerSettingsLoaderTests.cs`
+ - 结果:通过
+- `dotnet format GFramework.sln --verify-no-changes --no-restore`
+ - 结果:失败
+ - 备注:失败为仓库既有的跨项目 whitespace、final newline 与 charset 诊断;本轮改动文件已通过 scoped format 验证
+
+### RP-003 下一步
+
+1. 提交 PR review follow-up
+2. 等待 PR #315 复查,确认 CodeRabbit / Greptile open threads 是否关闭
From 64e5d8d11d9e72cd182165f093e11ed908d387f7 Mon Sep 17 00:00:00 2001
From: gewuyou <95328647+GeWuYou@users.noreply.github.com>
Date: Sun, 3 May 2026 14:04:59 +0800
Subject: [PATCH 4/4] =?UTF-8?q?test(godot):=20=E8=A1=A5=E5=BC=BA=E6=97=A5?=
=?UTF-8?q?=E5=BF=97=E5=8F=8D=E5=B0=84=E6=96=AD=E8=A8=80?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 补充 GodotLogger 结构化属性反射目标的显式断言
- 优化 反射返回类型不匹配时的测试失败定位
- 更新 godot logging core sink 跟踪与执行 trace
---
.../Logging/GodotLoggerSettingsLoaderTests.cs | 6 ++++-
.../todos/godot-logging-core-sink-tracking.md | 18 ++++++++++----
.../traces/godot-logging-core-sink-trace.md | 24 +++++++++++++++++++
3 files changed, 42 insertions(+), 6 deletions(-)
diff --git a/GFramework.Godot.Tests/Logging/GodotLoggerSettingsLoaderTests.cs b/GFramework.Godot.Tests/Logging/GodotLoggerSettingsLoaderTests.cs
index 74db1f6b..71f8ff18 100644
--- a/GFramework.Godot.Tests/Logging/GodotLoggerSettingsLoaderTests.cs
+++ b/GFramework.Godot.Tests/Logging/GodotLoggerSettingsLoaderTests.cs
@@ -195,6 +195,8 @@ public sealed class GodotLoggerSettingsLoaderTests
var toPropertiesDictionary = typeof(GodotLogger).GetMethod(
"ToPropertiesDictionary",
BindingFlags.NonPublic | BindingFlags.Static);
+ Assert.That(toPropertiesDictionary, Is.Not.Null, "Unable to reflect GodotLogger.ToPropertiesDictionary.");
+
var properties = new (string Key, object? Value)[]
{
(null!, "ignored"),
@@ -202,7 +204,9 @@ public sealed class GodotLoggerSettingsLoaderTests
(" Player ", 42)
};
- var dictionary = toPropertiesDictionary?.Invoke(null, [properties]) as IReadOnlyDictionary;
+ var dictionary = toPropertiesDictionary!.Invoke(null, [properties]) as IReadOnlyDictionary;
+ Assert.That(dictionary, Is.Not.Null, "ToPropertiesDictionary should return structured log properties.");
+
var result = GodotLogAppender.FormatProperties(dictionary);
Assert.That(result, Is.EqualTo(" | Player=42"));
diff --git a/ai-plan/public/godot-logging-core-sink/todos/godot-logging-core-sink-tracking.md b/ai-plan/public/godot-logging-core-sink/todos/godot-logging-core-sink-tracking.md
index af8ac022..a87c1b70 100644
--- a/ai-plan/public/godot-logging-core-sink/todos/godot-logging-core-sink-tracking.md
+++ b/ai-plan/public/godot-logging-core-sink/todos/godot-logging-core-sink-tracking.md
@@ -7,13 +7,14 @@
## 当前恢复点
-- 恢复点编号:`GODOT-LOGGING-CORE-SINK-RP-003`
-- 当前阶段:`PR review follow-up 已验证`
+- 恢复点编号:`GODOT-LOGGING-CORE-SINK-RP-004`
+- 当前阶段:`PR review follow-up 复查已验证`
- 当前焦点:
- `GFramework.Godot.Logging.GodotLogAppender` 已作为 Core `ILogAppender` 的 Godot 宿主落点落地
- `GodotLogger` 保留原有 `ILogger` 入口,但底层输出委托给 appender
- Godot / Core logging 文档已说明 provider 与 appender 的组合边界
- PR #315 最新 AI review 中仍适用的测试稳定性、dead private wrapper、boot index 与 trace heading 问题已处理
+ - 最新 CodeRabbit outside-diff 复查指出的反射测试诊断不清晰问题已处理
## 已知输入
@@ -31,7 +32,8 @@
3. 已完成:保留 `GodotLog` / `GodotLoggerFactoryProvider` 入口,并让 `GodotLogger` 底层走 `GodotLogAppender`
4. 已完成:补充 `GodotLogAppender` targeted tests 与 `docs/zh-CN/` adoption guidance
5. 已完成:处理 PR #315 最新 review follow-up,移除默认 boot index 的 archived topics 区块并消除 trace 重复 heading
-6. 待确认:是否还需要在后续阶段补一个配置化 factory 示例,把 `GodotLogAppender` 与文件 / async appender 显式组合
+6. 已完成:处理最新 CodeRabbit outside-diff 反馈,显式断言反射目标与返回类型以改善测试失败定位
+7. 待确认:是否还需要在后续阶段补一个配置化 factory 示例,把 `GodotLogAppender` 与文件 / async appender 显式组合
## 验证
@@ -48,12 +50,18 @@
- 结果:通过
- `dotnet format GFramework.Godot.Tests --verify-no-changes --no-restore --include GFramework.Godot.Tests/Logging/GodotLogAppenderTests.cs GFramework.Godot.Tests/Logging/GodotLoggerSettingsLoaderTests.cs`
- 结果:通过
+- `dotnet test GFramework.Godot.Tests -c Release`
+ - 结果:通过,`75 passed / 0 failed / 0 skipped`
+ - 备注:2026-05-03 最新 PR review outside-diff 复查后重新验证
+- `dotnet format GFramework.Godot.Tests --verify-no-changes --no-restore --include GFramework.Godot.Tests/Logging/GodotLoggerSettingsLoaderTests.cs`
+ - 结果:通过
+ - 备注:覆盖最新改动的测试文件
- `dotnet format GFramework.sln --verify-no-changes --no-restore`
- 结果:失败
- 备注:失败集中在仓库既有的 whitespace、final newline 与 charset 诊断,跨 `GFramework.Core`、`GFramework.Cqrs`、`GFramework.Game.Abstractions` 等未触碰项目;本轮改动用 scoped format 验证
## 下一步
-1. 提交当前 PR review follow-up
-2. 等待 PR #315 复查并确认 CodeRabbit / Greptile 线程是否关闭
+1. 提交最新 PR review follow-up
+2. 等待 PR #315 复查并确认 CodeRabbit outside-diff 反馈是否关闭
3. 如继续扩展本主题,优先评估是否需要示例化 `CompositeLogger + GodotLogAppender + FileAppender`,而不是新增 API
diff --git a/ai-plan/public/godot-logging-core-sink/traces/godot-logging-core-sink-trace.md b/ai-plan/public/godot-logging-core-sink/traces/godot-logging-core-sink-trace.md
index 45ae1e36..dea0f040 100644
--- a/ai-plan/public/godot-logging-core-sink/traces/godot-logging-core-sink-trace.md
+++ b/ai-plan/public/godot-logging-core-sink/traces/godot-logging-core-sink-trace.md
@@ -91,3 +91,27 @@
1. 提交 PR review follow-up
2. 等待 PR #315 复查,确认 CodeRabbit / Greptile open threads 是否关闭
+
+### RP-004 PR Review Outside-Diff 复查
+
+- 使用 `$gframework-pr-review` 重新抓取 PR #315 最新 review payload:
+ - CodeRabbit:无 open thread,但 latest review body 仍有 1 条 outside-diff 反馈
+ - Greptile:无 open thread
+ - Gemini Code Assist:无 open thread
+ - GitHub Test Reporter:最新 run 显示 `2264 passed / 0 failed`
+ - MegaLinter:仍为 `dotnet-format` restore failure,未提供具体源文件格式诊断
+- 已实施:
+ - `GodotLoggerSettingsLoaderTests.StructuredProperties_Should_Skip_Blank_Keys_And_Trim_Valid_Keys` 在调用反射目标前显式断言 `ToPropertiesDictionary` 存在
+ - 同一测试在交给 `GodotLogAppender.FormatProperties` 前显式断言反射返回值类型符合预期
+
+### RP-004 验证
+
+- `dotnet test GFramework.Godot.Tests -c Release`
+ - 结果:通过,`75 passed / 0 failed / 0 skipped`
+- `dotnet format GFramework.Godot.Tests --verify-no-changes --no-restore --include GFramework.Godot.Tests/Logging/GodotLoggerSettingsLoaderTests.cs`
+ - 结果:通过
+
+### RP-004 下一步
+
+1. 提交最新 PR review follow-up
+2. 等待 PR #315 复查,确认 CodeRabbit outside-diff 反馈是否关闭