mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-05-06 16:16:44 +08:00
feat(godot): 新增 Godot 日志 Appender
新增 GodotLogAppender 作为 Core ILogAppender 的 Godot 控制台落点 重构 GodotLogger 输出路径以复用 appender 管线并保持现有 ILogger 入口 补充 Godot appender 渲染测试、文档说明与 active topic 恢复记录
This commit is contained in:
parent
40cce565e6
commit
1009fee4a4
80
GFramework.Godot.Tests/Logging/GodotLogAppenderTests.cs
Normal file
80
GFramework.Godot.Tests/Logging/GodotLogAppenderTests.cs
Normal file
@ -0,0 +1,80 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using GFramework.Core.Abstractions.Logging;
|
||||
using GFramework.Godot.Logging;
|
||||
|
||||
namespace GFramework.Godot.Tests.Logging;
|
||||
|
||||
/// <summary>
|
||||
/// Verifies the Godot appender edge that adapts Core log entries to Godot output rendering.
|
||||
/// </summary>
|
||||
[TestFixture]
|
||||
public sealed class GodotLogAppenderTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Verifies that the appender renders Core log entry data and merged structured properties.
|
||||
/// </summary>
|
||||
[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<string, object?>(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"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that dynamic option providers are evaluated for each rendered log entry.
|
||||
/// </summary>
|
||||
[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"));
|
||||
});
|
||||
}
|
||||
}
|
||||
211
GFramework.Godot/Logging/GodotLogAppender.cs
Normal file
211
GFramework.Godot/Logging/GodotLogAppender.cs
Normal file
@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Writes Core <see cref="LogEntry"/> instances to the Godot output APIs.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 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
|
||||
/// <see cref="ILogAppender"/> features such as <c>CompositeLogger</c>, filters, and async appenders. The appender
|
||||
/// does not own unmanaged resources; <see cref="Flush"/> and <see cref="Dispose"/> are therefore no-op lifecycle
|
||||
/// hooks that satisfy the shared appender contract.
|
||||
/// </remarks>
|
||||
public sealed class GodotLogAppender : ILogAppender
|
||||
{
|
||||
private static readonly IReadOnlyDictionary<string, object?> EmptyProperties =
|
||||
new Dictionary<string, object?>(StringComparer.Ordinal);
|
||||
|
||||
private readonly Func<GodotLoggerOptions> _optionsProvider;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a Godot appender with default Godot logger options.
|
||||
/// </summary>
|
||||
public GodotLogAppender()
|
||||
: this(new GodotLoggerOptions())
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a Godot appender with fixed Godot logger options.
|
||||
/// </summary>
|
||||
/// <param name="options">The formatting and routing options used for every appended entry.</param>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="options"/> is <see langword="null"/>.</exception>
|
||||
public GodotLogAppender(GodotLoggerOptions options)
|
||||
: this(CreateFixedOptionsProvider(options))
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a Godot appender with a dynamic options provider.
|
||||
/// </summary>
|
||||
/// <param name="optionsProvider">
|
||||
/// Provides the latest formatting and routing options for each append operation.
|
||||
/// </param>
|
||||
/// <remarks>
|
||||
/// 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.
|
||||
/// </remarks>
|
||||
internal GodotLogAppender(Func<GodotLoggerOptions> optionsProvider)
|
||||
{
|
||||
_optionsProvider = optionsProvider ?? throw new ArgumentNullException(nameof(optionsProvider));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Appends one Core log entry to Godot's console and debugger output.
|
||||
/// </summary>
|
||||
/// <param name="entry">The Core log entry to render.</param>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="entry"/> is <see langword="null"/>.</exception>
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Completes pending writes.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Godot output APIs are synchronous from this appender's point of view, so there is no buffered state to
|
||||
/// flush.
|
||||
/// </remarks>
|
||||
public void Flush()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Releases appender resources.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 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.
|
||||
/// </remarks>
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Formats structured properties for the <c>{properties}</c> template placeholder.
|
||||
/// </summary>
|
||||
/// <param name="properties">The already-merged property set from a Core <see cref="LogEntry"/>.</param>
|
||||
/// <returns>
|
||||
/// A leading separator plus formatted properties, or an empty string when no valid properties exist.
|
||||
/// </returns>
|
||||
/// <remarks>
|
||||
/// 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.
|
||||
/// </remarks>
|
||||
internal static string FormatProperties(IReadOnlyDictionary<string, object?>? 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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Renders a Core log entry without writing it to Godot.
|
||||
/// </summary>
|
||||
/// <param name="entry">The Core log entry to render.</param>
|
||||
/// <returns>The line that would be sent to the selected Godot output API.</returns>
|
||||
/// <remarks>
|
||||
/// Tests use this method to verify template and structured-property behavior without depending on Godot's
|
||||
/// static output APIs.
|
||||
/// </remarks>
|
||||
internal string Render(LogEntry entry)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(entry);
|
||||
|
||||
return Render(entry, _optionsProvider());
|
||||
}
|
||||
|
||||
private static Func<GodotLoggerOptions> 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<string, object?> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Godot platform logger implementation.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This logger preserves the existing <see cref="ILogger"/> entry point while delegating output to
|
||||
/// <see cref="GodotLogAppender"/> so Godot rendering remains compatible with the Core appender pipeline.
|
||||
/// </remarks>
|
||||
public sealed class GodotLogger : AbstractLogger
|
||||
{
|
||||
private readonly Func<GodotLoggerOptions> _optionsProvider;
|
||||
private static readonly IReadOnlyDictionary<string, object?> EmptyProperties =
|
||||
new Dictionary<string, object?>(StringComparer.Ordinal);
|
||||
|
||||
private readonly GodotLogAppender _appender;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </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="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.
|
||||
/// being recreated. The default public constructor supplies a fixed provider to avoid allocation on the log
|
||||
/// path.
|
||||
/// </remarks>
|
||||
internal GodotLogger(
|
||||
string name,
|
||||
@ -57,7 +64,8 @@ public sealed class GodotLogger : AbstractLogger
|
||||
Func<LogLevel> 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)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -74,6 +82,9 @@ public sealed class GodotLogger : AbstractLogger
|
||||
/// <summary>
|
||||
/// Uses Godot-aware structured rendering instead of the base string concatenation fallback.
|
||||
/// </summary>
|
||||
/// <param name="level">The log level.</param>
|
||||
/// <param name="message">The message body before Godot template rendering.</param>
|
||||
/// <param name="properties">Structured properties appended through the configured Godot template.</param>
|
||||
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
|
||||
/// <summary>
|
||||
/// Uses Godot-aware structured rendering instead of the base string concatenation fallback.
|
||||
/// </summary>
|
||||
/// <param name="level">The log level.</param>
|
||||
/// <param name="message">The message body before Godot template rendering.</param>
|
||||
/// <param name="exception">The optional exception written after the rendered message.</param>
|
||||
/// <param name="properties">Structured properties appended through the configured Godot template.</param>
|
||||
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<string, object?> MergeProperties((string Key, object? Value)[]? properties)
|
||||
private static IReadOnlyDictionary<string, object?> 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<string, object?>(contextProperties, StringComparer.Ordinal);
|
||||
if (properties != null)
|
||||
var result = new Dictionary<string, object?>(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<string, object?> EmptyProperties =
|
||||
new Dictionary<string, object?>(StringComparer.Ordinal);
|
||||
|
||||
private static Func<GodotLoggerOptions> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -72,7 +72,7 @@ Scene / UI、配置、存储、设置、日志与协程能力接到 `Node`、`Sc
|
||||
|
||||
- 节点扩展与 `Signal(...)` fluent API
|
||||
- `GodotTimeSource` 与协程时间分段
|
||||
- Godot 日志 provider
|
||||
- Godot 日志 provider 与 `GodotLogAppender`
|
||||
- 暂停处理、节点池与富文本效果支持
|
||||
|
||||
这些目录都是“宿主适配层”,不是新的 gameplay 抽象层。
|
||||
|
||||
@ -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 验证通过后归档
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 组合。
|
||||
|
||||
@ -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`。
|
||||
|
||||
@ -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<Main>();
|
||||
"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 控制台
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user