mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-05-07 00:39:00 +08:00
feat(godot): 收敛 GodotLogger 宿主能力
- 新增 GodotLog、DeferredLogger 和配置自动发现、热重载接线。 - 修复已缓存 logger 的级别判定与输出路径,使动态配置生效。 - 更新文档与追踪记录,明确当前收敛边界和恢复点。
This commit is contained in:
parent
36e1ae5f32
commit
748bb714fb
@ -6,16 +6,42 @@ namespace GFramework.Core.Logging;
|
||||
/// 日志抽象基类,封装日志级别判断、格式化与异常处理逻辑。
|
||||
/// 平台日志器只需实现 Write 方法即可。
|
||||
/// </summary>
|
||||
public abstract class AbstractLogger(
|
||||
string? name = null,
|
||||
LogLevel minLevel = LogLevel.Info) : IStructuredLogger
|
||||
public abstract class AbstractLogger : IStructuredLogger
|
||||
{
|
||||
/// <summary>
|
||||
/// 根日志记录器的名称常量
|
||||
/// </summary>
|
||||
public const string RootLoggerName = "ROOT";
|
||||
|
||||
private readonly string _name = name ?? RootLoggerName;
|
||||
private readonly Func<LogLevel> _minLevelProvider;
|
||||
private readonly string _name;
|
||||
|
||||
/// <summary>
|
||||
/// 使用固定最小日志级别初始化日志器。
|
||||
/// </summary>
|
||||
/// <param name="name">日志器名称。</param>
|
||||
/// <param name="minLevel">最小日志级别。</param>
|
||||
protected AbstractLogger(
|
||||
string? name = null,
|
||||
LogLevel minLevel = LogLevel.Info)
|
||||
: this(name, () => minLevel)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 使用动态最小日志级别提供器初始化日志器。
|
||||
/// </summary>
|
||||
/// <param name="name">日志器名称。</param>
|
||||
/// <param name="minLevelProvider">最小日志级别提供器。</param>
|
||||
protected AbstractLogger(
|
||||
string? name,
|
||||
Func<LogLevel> minLevelProvider)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(minLevelProvider);
|
||||
|
||||
_name = name ?? RootLoggerName;
|
||||
_minLevelProvider = minLevelProvider;
|
||||
}
|
||||
|
||||
#region Metadata
|
||||
|
||||
@ -47,7 +73,7 @@ public abstract class AbstractLogger(
|
||||
/// <returns>如果指定级别大于等于最小级别则返回true,否则返回false</returns>
|
||||
protected bool IsEnabled(LogLevel level)
|
||||
{
|
||||
return level >= minLevel;
|
||||
return level >= _minLevelProvider();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -568,4 +594,4 @@ public abstract class AbstractLogger(
|
||||
#region Core Pipeline (Private)
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
@ -16,7 +16,8 @@ public sealed class GodotLogTemplateTests
|
||||
LogLevel.Warning,
|
||||
"Game.Services.Inventory",
|
||||
"Loaded",
|
||||
"orange");
|
||||
"orange",
|
||||
string.Empty);
|
||||
|
||||
var result = template.Render(context);
|
||||
|
||||
@ -32,7 +33,8 @@ public sealed class GodotLogTemplateTests
|
||||
LogLevel.Debug,
|
||||
"Game",
|
||||
"Ready",
|
||||
"cyan");
|
||||
"cyan",
|
||||
string.Empty);
|
||||
|
||||
var result = template.Render(context);
|
||||
|
||||
@ -48,7 +50,8 @@ public sealed class GodotLogTemplateTests
|
||||
LogLevel.Info,
|
||||
"UI",
|
||||
"Ready",
|
||||
"white");
|
||||
"white",
|
||||
string.Empty);
|
||||
|
||||
var result = template.Render(context);
|
||||
|
||||
@ -64,7 +67,8 @@ public sealed class GodotLogTemplateTests
|
||||
LogLevel.Info,
|
||||
"Game",
|
||||
"Ready",
|
||||
"white");
|
||||
"white",
|
||||
string.Empty);
|
||||
|
||||
var result = template.Render(context);
|
||||
|
||||
@ -80,13 +84,31 @@ public sealed class GodotLogTemplateTests
|
||||
LogLevel.Info,
|
||||
"Game",
|
||||
"Ready",
|
||||
"white");
|
||||
"white",
|
||||
string.Empty);
|
||||
|
||||
var result = template.Render(context);
|
||||
|
||||
Assert.That(result, Is.EqualTo("INFO "));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Render_Should_Append_Properties_Placeholder()
|
||||
{
|
||||
var template = GodotLogTemplate.Parse("{message}{properties}");
|
||||
var context = new GodotLogRenderContext(
|
||||
new DateTime(2026, 5, 2, 1, 2, 3, DateTimeKind.Utc),
|
||||
LogLevel.Info,
|
||||
"Game",
|
||||
"Ready",
|
||||
"white",
|
||||
" | Scene=Boot");
|
||||
|
||||
var result = template.Render(context);
|
||||
|
||||
Assert.That(result, Is.EqualTo("Ready | Scene=Boot"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Options_ForMinimumLevel_Should_Preserve_Fixed_Minimum_Level()
|
||||
{
|
||||
|
||||
@ -8,4 +8,5 @@ internal readonly record struct GodotLogRenderContext(
|
||||
LogLevel Level,
|
||||
string Category,
|
||||
string Message,
|
||||
string Color);
|
||||
string Color,
|
||||
string Properties);
|
||||
|
||||
@ -91,6 +91,7 @@ internal sealed class GodotLogTemplate
|
||||
"color" => static (builder, context) => builder.Append(context.Color),
|
||||
"level" => static (builder, context) => builder.Append(context.Level),
|
||||
"message" => static (builder, context) => builder.Append(context.Message),
|
||||
"properties" => static (builder, context) => builder.Append(context.Properties),
|
||||
"timestamp" => static (builder, context) => builder.Append(context.Timestamp.ToString(
|
||||
"yyyy-MM-dd HH:mm:ss.fff",
|
||||
CultureInfo.InvariantCulture)),
|
||||
|
||||
@ -1,4 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using GFramework.Core.Abstractions.Logging;
|
||||
using GFramework.Core.Logging;
|
||||
using Godot;
|
||||
@ -10,7 +13,8 @@ namespace GFramework.Godot.Logging;
|
||||
/// </summary>
|
||||
public sealed class GodotLogger : AbstractLogger
|
||||
{
|
||||
private readonly GodotLoggerOptions _options;
|
||||
private readonly Func<LogLevel> _minLevelProvider;
|
||||
private readonly Func<GodotLoggerOptions> _optionsProvider;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a logger that preserves the historical fixed-format template.
|
||||
@ -18,7 +22,10 @@ public sealed class GodotLogger : AbstractLogger
|
||||
/// <param name="name">The logger name.</param>
|
||||
/// <param name="minLevel">The minimum enabled log level.</param>
|
||||
public GodotLogger(string? name = null, LogLevel minLevel = LogLevel.Info)
|
||||
: this(name, GodotLoggerOptions.ForMinimumLevel(minLevel))
|
||||
: this(
|
||||
name ?? RootLoggerName,
|
||||
() => GodotLoggerOptions.ForMinimumLevel(minLevel),
|
||||
() => minLevel)
|
||||
{
|
||||
}
|
||||
|
||||
@ -28,9 +35,21 @@ public sealed class GodotLogger : AbstractLogger
|
||||
/// <param name="name">The logger name.</param>
|
||||
/// <param name="options">The logger options.</param>
|
||||
public GodotLogger(string? name, GodotLoggerOptions options)
|
||||
: base(name ?? RootLoggerName, (options ?? throw new ArgumentNullException(nameof(options))).GetEffectiveMinLevel())
|
||||
: this(
|
||||
name ?? RootLoggerName,
|
||||
() => options ?? throw new ArgumentNullException(nameof(options)),
|
||||
() => (options ?? throw new ArgumentNullException(nameof(options))).GetEffectiveMinLevel())
|
||||
{
|
||||
_options = options;
|
||||
}
|
||||
|
||||
internal GodotLogger(
|
||||
string name,
|
||||
Func<GodotLoggerOptions> optionsProvider,
|
||||
Func<LogLevel> minLevelProvider)
|
||||
: base(name, minLevelProvider ?? throw new ArgumentNullException(nameof(minLevelProvider)))
|
||||
{
|
||||
_optionsProvider = optionsProvider ?? throw new ArgumentNullException(nameof(optionsProvider));
|
||||
_minLevelProvider = minLevelProvider;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -41,18 +60,59 @@ public sealed class GodotLogger : AbstractLogger
|
||||
/// <param name="exception">The optional exception.</param>
|
||||
protected override void Write(LogLevel level, string message, Exception? exception)
|
||||
{
|
||||
var templateText = _options.Mode == GodotLoggerMode.Debug
|
||||
? _options.DebugOutputTemplate
|
||||
: _options.ReleaseOutputTemplate;
|
||||
WriteEntry(level, message, exception, properties: null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Uses Godot-aware structured rendering instead of the base string concatenation fallback.
|
||||
/// </summary>
|
||||
public override void Log(LogLevel level, string message, params (string Key, object? Value)[] properties)
|
||||
{
|
||||
if (level < _minLevelProvider())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
WriteEntry(level, message, exception: null, properties);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Uses Godot-aware structured rendering instead of the base string concatenation fallback.
|
||||
/// </summary>
|
||||
public override void Log(
|
||||
LogLevel level,
|
||||
string message,
|
||||
Exception? exception,
|
||||
params (string Key, object? Value)[] properties)
|
||||
{
|
||||
if (level < _minLevelProvider())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
WriteEntry(level, message, exception, properties);
|
||||
}
|
||||
|
||||
private void WriteEntry(
|
||||
LogLevel level,
|
||||
string message,
|
||||
Exception? exception,
|
||||
(string Key, object? Value)[]? properties)
|
||||
{
|
||||
var options = _optionsProvider();
|
||||
var templateText = options.Mode == GodotLoggerMode.Debug
|
||||
? options.DebugOutputTemplate
|
||||
: options.ReleaseOutputTemplate;
|
||||
var context = new GodotLogRenderContext(
|
||||
DateTime.UtcNow,
|
||||
level,
|
||||
Name(),
|
||||
message,
|
||||
_options.GetColor(level));
|
||||
options.GetColor(level),
|
||||
FormatProperties(properties));
|
||||
var rendered = GodotLogTemplate.Parse(templateText).Render(context);
|
||||
|
||||
if (_options.Mode == GodotLoggerMode.Debug)
|
||||
if (options.Mode == GodotLoggerMode.Debug)
|
||||
{
|
||||
WriteDebug(level, rendered);
|
||||
}
|
||||
@ -67,6 +127,54 @@ public sealed class GodotLogger : AbstractLogger
|
||||
}
|
||||
}
|
||||
|
||||
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)}"));
|
||||
}
|
||||
|
||||
private static IReadOnlyDictionary<string, object?> MergeProperties((string Key, object? Value)[]? properties)
|
||||
{
|
||||
var contextProperties = LogContext.Current;
|
||||
if ((properties == null || properties.Length == 0) && contextProperties.Count == 0)
|
||||
{
|
||||
return EmptyProperties;
|
||||
}
|
||||
|
||||
var merged = new Dictionary<string, object?>(contextProperties, StringComparer.Ordinal);
|
||||
if (properties != null)
|
||||
{
|
||||
foreach (var property in properties)
|
||||
{
|
||||
merged[property.Key] = property.Value;
|
||||
}
|
||||
}
|
||||
|
||||
return merged;
|
||||
}
|
||||
|
||||
private static readonly IReadOnlyDictionary<string, object?> EmptyProperties =
|
||||
new Dictionary<string, object?>(StringComparer.Ordinal);
|
||||
|
||||
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);
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using GFramework.Core.Abstractions.Logging;
|
||||
using GFramework.Core.Logging;
|
||||
|
||||
namespace GFramework.Godot.Logging;
|
||||
|
||||
@ -9,14 +9,15 @@ namespace GFramework.Godot.Logging;
|
||||
/// </summary>
|
||||
public sealed class GodotLoggerFactoryProvider : ILoggerFactoryProvider
|
||||
{
|
||||
private readonly ILoggerFactory _cachedFactory;
|
||||
private readonly ConcurrentDictionary<string, ILogger> _loggers = new(StringComparer.Ordinal);
|
||||
private readonly Func<GodotLoggerSettings> _settingsProvider;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a Godot logger provider with the default logger factory.
|
||||
/// </summary>
|
||||
public GodotLoggerFactoryProvider()
|
||||
: this(static () => GodotLoggerSettings.Default)
|
||||
{
|
||||
_cachedFactory = CreateCachedFactory(new GodotLoggerFactory());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -24,8 +25,13 @@ public sealed class GodotLoggerFactoryProvider : ILoggerFactoryProvider
|
||||
/// </summary>
|
||||
/// <param name="options">The logger options.</param>
|
||||
public GodotLoggerFactoryProvider(GodotLoggerOptions options)
|
||||
: this(CreateStaticSettingsProvider(options))
|
||||
{
|
||||
_cachedFactory = CreateCachedFactory(new GodotLoggerFactory(options));
|
||||
}
|
||||
|
||||
internal GodotLoggerFactoryProvider(Func<GodotLoggerSettings> settingsProvider)
|
||||
{
|
||||
_settingsProvider = settingsProvider ?? throw new ArgumentNullException(nameof(settingsProvider));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -40,12 +46,31 @@ public sealed class GodotLoggerFactoryProvider : ILoggerFactoryProvider
|
||||
/// <returns>A logger configured with <see cref="MinLevel"/>.</returns>
|
||||
public ILogger CreateLogger(string name)
|
||||
{
|
||||
return _cachedFactory.GetLogger(name, MinLevel);
|
||||
ArgumentNullException.ThrowIfNull(name);
|
||||
|
||||
return _loggers.GetOrAdd(
|
||||
name,
|
||||
static (loggerName, provider) => new GodotLogger(
|
||||
loggerName,
|
||||
provider.GetOptions,
|
||||
() => provider.GetEffectiveMinLevel(loggerName)),
|
||||
this);
|
||||
}
|
||||
|
||||
private static ILoggerFactory CreateCachedFactory(ILoggerFactory innerFactory)
|
||||
private GodotLoggerOptions GetOptions()
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(innerFactory);
|
||||
return new CachedLoggerFactory(innerFactory);
|
||||
return _settingsProvider().Options;
|
||||
}
|
||||
|
||||
private LogLevel GetEffectiveMinLevel(string categoryName)
|
||||
{
|
||||
return _settingsProvider().GetEffectiveMinLevel(categoryName, MinLevel);
|
||||
}
|
||||
|
||||
private static Func<GodotLoggerSettings> CreateStaticSettingsProvider(GodotLoggerOptions options)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(options);
|
||||
var settings = GodotLoggerSettings.FromOptions(options);
|
||||
return () => settings;
|
||||
}
|
||||
}
|
||||
|
||||
@ -39,7 +39,7 @@ public sealed class GodotLoggerOptions
|
||||
/// </summary>
|
||||
#pragma warning disable MA0016 // Keep configuration mutable for object initializer and serializer scenarios.
|
||||
public string DebugOutputTemplate { get; set; } =
|
||||
"[{timestamp:yyyy-MM-dd HH:mm:ss.fff}] [color={color}][{level:u3}][/color] [{category:l16}] {message}";
|
||||
"[{timestamp:yyyy-MM-dd HH:mm:ss.fff}] [color={color}][{level:u3}][/color] [{category:l16}] {message}{properties}";
|
||||
#pragma warning restore MA0016
|
||||
|
||||
/// <summary>
|
||||
@ -47,7 +47,7 @@ public sealed class GodotLoggerOptions
|
||||
/// </summary>
|
||||
#pragma warning disable MA0016 // Keep configuration mutable for object initializer and serializer scenarios.
|
||||
public string ReleaseOutputTemplate { get; set; } =
|
||||
"[{timestamp:yyyy-MM-dd HH:mm:ss.fff}] [{level:u3}] [{category:l16}] {message}";
|
||||
"[{timestamp:yyyy-MM-dd HH:mm:ss.fff}] [{level:u3}] [{category:l16}] {message}{properties}";
|
||||
#pragma warning restore MA0016
|
||||
|
||||
/// <summary>
|
||||
@ -69,8 +69,8 @@ public sealed class GodotLoggerOptions
|
||||
Mode = GodotLoggerMode.Debug,
|
||||
DebugMinLevel = minLevel,
|
||||
ReleaseMinLevel = minLevel,
|
||||
DebugOutputTemplate = "[{timestamp:yyyy-MM-dd HH:mm:ss.fff}] {level:padded} [{category}] {message}",
|
||||
ReleaseOutputTemplate = "[{timestamp:yyyy-MM-dd HH:mm:ss.fff}] {level:padded} [{category}] {message}",
|
||||
DebugOutputTemplate = "[{timestamp:yyyy-MM-dd HH:mm:ss.fff}] {level:padded} [{category}] {message}{properties}",
|
||||
ReleaseOutputTemplate = "[{timestamp:yyyy-MM-dd HH:mm:ss.fff}] {level:padded} [{category}] {message}{properties}",
|
||||
Colors = new Dictionary<LogLevel, string>(DefaultColors)
|
||||
};
|
||||
}
|
||||
|
||||
@ -38,6 +38,10 @@ 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`
|
||||
@ -59,6 +63,9 @@ 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`
|
||||
|
||||
@ -5,11 +5,16 @@ description: 以当前 GFramework.Godot.Logging 源码与 CoreGrid 接线为准
|
||||
|
||||
# Godot 日志系统
|
||||
|
||||
`GFramework.Godot` 当前的日志能力很收敛:它不是一套独立于 Core 的新日志框架,而是把现有 `ILogger` 调用面接到
|
||||
Godot 控制台。
|
||||
`GFramework.Godot` 当前的日志能力仍然以 Core 的 `ILogger` 调用面为中心,但已经不再只是一个薄输出适配层。
|
||||
除了把日志写到 Godot 控制台,它现在还补上了 Godot 宿主常见的接入便利层:
|
||||
|
||||
换句话说,Godot 侧真正新增的是 provider / factory / logger 这层输出适配,而不是新的日志 API。业务代码仍然继续使用
|
||||
`LoggerFactoryResolver.Provider.CreateLogger(...)` 或 `[Log]` 生成的 `ILogger` 字段。
|
||||
- `GodotLog` 静态入口
|
||||
- 配置文件自动发现
|
||||
- 运行期配置热重载
|
||||
- 延迟 logger 解析,适合 `static readonly` 字段
|
||||
|
||||
业务代码仍然继续使用 `LoggerFactoryResolver.Provider.CreateLogger(...)`、`GodotLog.CreateLogger(...)` 或 `[Log]`
|
||||
生成的 `ILogger` 字段;Godot 侧没有额外引入第二套业务日志 API。
|
||||
|
||||
## 当前公开入口
|
||||
|
||||
@ -57,11 +62,34 @@ public sealed class GodotLoggerFactoryProvider : ILoggerFactoryProvider
|
||||
}
|
||||
```
|
||||
|
||||
它内部用 `CachedLoggerFactory` 包装 `GodotLoggerFactory`。缓存 key 由 `name` 和 `MinLevel` 共同组成,所以:
|
||||
当前 provider 会按 logger 名称缓存实例,但 logger 本身会在写入时读取当前配置快照,所以:
|
||||
|
||||
- 同名、同 `MinLevel` 的 logger 会复用实例
|
||||
- 调整 `MinLevel` 后,新创建的 logger 会走新的缓存 key
|
||||
- 已经持有的旧 logger 不会被原地改写
|
||||
- 同名 logger 会复用实例
|
||||
- 调整 provider 最小级别或热更新配置后,已持有的 logger 会立即看到新行为
|
||||
- 不需要为了刷新模板、颜色或级别而重新创建 logger
|
||||
|
||||
### `GodotLog`
|
||||
|
||||
`GodotLog` 是新增的 Godot 宿主友好入口:
|
||||
|
||||
```csharp
|
||||
using GFramework.Godot.Logging;
|
||||
|
||||
GodotLog.Configure(options =>
|
||||
{
|
||||
options.Mode = GodotLoggerMode.Debug;
|
||||
});
|
||||
|
||||
GodotLog.UseAsDefaultProvider();
|
||||
|
||||
var logger = GodotLog.CreateLogger<Main>();
|
||||
```
|
||||
|
||||
它提供三件事:
|
||||
|
||||
- 在第一次真正创建 provider 前允许代码覆写 `GodotLoggerOptions`
|
||||
- 自动按 `GODOT_LOGGER_CONFIG` -> 可执行目录 `appsettings.json` -> `res://appsettings.json` 顺序发现配置
|
||||
- 返回延迟解析 logger,避免 `static readonly` 字段过早锁死配置
|
||||
|
||||
## 最小接入路径
|
||||
|
||||
@ -121,7 +149,7 @@ public partial class SettingsPanel : Control
|
||||
```
|
||||
|
||||
如果你已经在用 `GFramework.Core.SourceGenerators`,也可以继续让 `[Log]` 生成字段。Godot provider 只改变输出落点,
|
||||
不会改变 `[Log]` 的生成契约。
|
||||
不会改变 `[Log]` 的生成契约。需要静态字段延迟初始化时,也可以直接用 `GodotLog.CreateLogger<T>()`。
|
||||
|
||||
### 3. Scene / UI 迁移日志会自动复用同一套 provider
|
||||
|
||||
@ -152,7 +180,9 @@ RegisterHandler(new LoggingTransitionHandler());
|
||||
| `Error` | `GD.PrintErr(...)` | 输出到错误流 |
|
||||
| `Fatal` | `GD.PushError(...)` | 进入 Godot 错误通道 |
|
||||
|
||||
异常追加格式也来自当前实现本身:
|
||||
结构化属性如果通过 `IStructuredLogger` 或 `LogContext` 传入,也会追加到模板里的 `{properties}` 占位符。
|
||||
|
||||
异常追加格式仍然来自当前实现本身:
|
||||
|
||||
```text
|
||||
[2026-04-22 10:30:47.012] ERROR [SaveSystem] 保存游戏失败
|
||||
@ -182,14 +212,16 @@ System.IO.IOException: ...
|
||||
|
||||
## 当前边界
|
||||
|
||||
- 当前推荐接法是把 `GodotLoggerFactoryProvider` 放进 `ArchitectureConfiguration.LoggerProperties`;直接赋值
|
||||
`LoggerFactoryResolver.Provider` 仍然可用,但不该再写成默认采用路径
|
||||
- 当前推荐接法仍然是把 `GodotLoggerFactoryProvider` 放进 `ArchitectureConfiguration.LoggerProperties`;如果项目是纯
|
||||
Godot 宿主,也可以在入口直接调用 `GodotLog.UseAsDefaultProvider()`
|
||||
- `GFramework.Godot.Logging` 只解决 Godot 控制台输出,不提供文件落盘、JSON formatter、异步 appender 或按 namespace
|
||||
的复杂过滤
|
||||
- `GodotLogger` 只改变输出方式,不改变 `ILogger` 接口本身;业务代码不需要切换到 Godot 专用日志 API
|
||||
- `[Log]`、`[ContextAware]` 这类字段注入能力不属于 `GFramework.Godot.Logging`
|
||||
- Scene / UI 的 `LoggingTransitionHandler` 位于 `GFramework.Game`,Godot 侧只是通过 provider 让它们输出到 Godot 控制台
|
||||
- 当前 `GodotLogger` 使用的是 UTC 时间戳;如果项目需要本地时区展示,需要自定义 provider / logger,而不是假定当前实现会自动转换
|
||||
- 当前配置热重载只覆盖 Godot logger 自身的模板、颜色、模式和级别;它没有把 `Microsoft.Extensions.Logging` 的整个
|
||||
options / builder 模型搬进来
|
||||
|
||||
## 继续阅读
|
||||
|
||||
|
||||
@ -1,154 +1,154 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
更新所有 C# 文件中的命名空间声明和 using 语句
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
ROOT_DIR = "/mnt/f/gewuyou/System/Documents/WorkSpace/GameDev/GFramework"
|
||||
|
||||
# 命名空间替换规则(按优先级排序,长的先匹配)
|
||||
NAMESPACE_RULES = [
|
||||
# CQRS 子命名空间
|
||||
(r'\.cqrs\.notification\b', '.CQRS.Notification'),
|
||||
(r'\.cqrs\.command\b', '.CQRS.Command'),
|
||||
(r'\.cqrs\.request\b', '.CQRS.Request'),
|
||||
(r'\.cqrs\.query\b', '.CQRS.Query'),
|
||||
(r'\.cqrs\.behaviors\b', '.CQRS.Behaviors'),
|
||||
(r'\.cqrs\b', '.CQRS'),
|
||||
|
||||
# 嵌套命名空间
|
||||
(r'\.coroutine\.instructions\b', '.Coroutine.Instructions'),
|
||||
(r'\.coroutine\.extensions\b', '.Coroutine.Extensions'),
|
||||
(r'\.coroutine\b', '.Coroutine'),
|
||||
|
||||
(r'\.events\.filters\b', '.Events.Filters'),
|
||||
(r'\.events\b', '.Events'),
|
||||
|
||||
(r'\.logging\.appenders\b', '.Logging.Appenders'),
|
||||
(r'\.logging\.filters\b', '.Logging.Filters'),
|
||||
(r'\.logging\.formatters\b', '.Logging.Formatters'),
|
||||
(r'\.logging\b', '.Logging'),
|
||||
|
||||
(r'\.functional\.async\b', '.Functional.Async'),
|
||||
(r'\.functional\.control\b', '.Functional.Control'),
|
||||
(r'\.functional\.functions\b', '.Functional.Functions'),
|
||||
(r'\.functional\.pipe\b', '.Functional.Pipe'),
|
||||
(r'\.functional\.result\b', '.Functional.Result'),
|
||||
(r'\.functional\b', '.Functional'),
|
||||
|
||||
(r'\.services\.modules\b', '.Services.Modules'),
|
||||
(r'\.services\b', '.Services'),
|
||||
|
||||
(r'\.extensions\.signal\b', '.Extensions.Signal'),
|
||||
(r'\.extensions\b', '.Extensions'),
|
||||
|
||||
(r'\.setting\.data\b', '.Setting.Data'),
|
||||
(r'\.setting\.events\b', '.Setting.Events'),
|
||||
(r'\.setting\b', '.Setting'),
|
||||
|
||||
(r'\.scene\.handler\b', '.Scene.Handler'),
|
||||
(r'\.scene\b', '.Scene'),
|
||||
|
||||
(r'\.ui\.handler\b', '.UI.Handler'),
|
||||
(r'\.ui\b', '.UI'),
|
||||
|
||||
(r'\.data\.events\b', '.Data.Events'),
|
||||
(r'\.data\b', '.Data'),
|
||||
|
||||
# 单层命名空间
|
||||
(r'\.architecture\b', '.Architecture'),
|
||||
(r'\.bases\b', '.Bases'),
|
||||
(r'\.command\b', '.Command'),
|
||||
(r'\.configuration\b', '.Configuration'),
|
||||
(r'\.constants\b', '.Constants'),
|
||||
(r'\.enums\b', '.Enums'),
|
||||
(r'\.environment\b', '.Environment'),
|
||||
(r'\.internals\b', '.Internals'),
|
||||
(r'\.ioc\b', '.IoC'),
|
||||
(r'\.lifecycle\b', '.Lifecycle'),
|
||||
(r'\.model\b', '.Model'),
|
||||
(r'\.pause\b', '.Pause'),
|
||||
(r'\.pool\b', '.Pool'),
|
||||
(r'\.properties\b', '.Properties'),
|
||||
(r'\.property\b', '.Property'),
|
||||
(r'\.query\b', '.Query'),
|
||||
(r'\.registries\b', '.Registries'),
|
||||
(r'\.resource\b', '.Resource'),
|
||||
(r'\.rule\b', '.Rule'),
|
||||
(r'\.serializer\b', '.Serializer'),
|
||||
(r'\.state\b', '.State'),
|
||||
(r'\.storage\b', '.Storage'),
|
||||
(r'\.system\b', '.System'),
|
||||
(r'\.time\b', '.Time'),
|
||||
(r'\.utility\b', '.Utility'),
|
||||
(r'\.versioning\b', '.Versioning'),
|
||||
(r'\.asset\b', '.Asset'),
|
||||
(r'\.components\b', '.Components'),
|
||||
(r'\.systems\b', '.Systems'),
|
||||
(r'\.ecs\b', '.ECS'),
|
||||
(r'\.integration\b', '.Integration'),
|
||||
(r'\.mediator\b', '.Mediator'),
|
||||
(r'\.tests\b', '.Tests'),
|
||||
(r'\.analyzers\b', '.Analyzers'),
|
||||
(r'\.diagnostics\b', '.Diagnostics'),
|
||||
(r'\.generator\b', '.Generator'),
|
||||
(r'\.info\b', '.Info'),
|
||||
]
|
||||
|
||||
def update_file(file_path):
|
||||
"""更新单个文件中的命名空间"""
|
||||
try:
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
|
||||
original_content = content
|
||||
replacements = 0
|
||||
|
||||
for pattern, replacement in NAMESPACE_RULES:
|
||||
matches = re.findall(pattern, content, re.IGNORECASE)
|
||||
if matches:
|
||||
content = re.sub(pattern, replacement, content, flags=re.IGNORECASE)
|
||||
replacements += len(matches)
|
||||
|
||||
if content != original_content:
|
||||
with open(file_path, 'w', encoding='utf-8') as f:
|
||||
f.write(content)
|
||||
return replacements
|
||||
|
||||
return 0
|
||||
except Exception as e:
|
||||
print(f"错误处理文件 {file_path}: {e}")
|
||||
return 0
|
||||
|
||||
def main():
|
||||
print("开始更新命名空间...")
|
||||
|
||||
# 查找所有 C# 文件
|
||||
cs_files = []
|
||||
for root, dirs, files in os.walk(ROOT_DIR):
|
||||
# 跳过 bin, obj, Generated 目录
|
||||
dirs[:] = [d for d in dirs if d not in ['bin', 'obj', 'Generated', '.git', 'node_modules']]
|
||||
|
||||
for file in files:
|
||||
if file.endswith('.cs'):
|
||||
cs_files.append(os.path.join(root, file))
|
||||
|
||||
print(f"找到 {len(cs_files)} 个 C# 文件")
|
||||
|
||||
updated_files = 0
|
||||
total_replacements = 0
|
||||
|
||||
for file_path in cs_files:
|
||||
replacements = update_file(file_path)
|
||||
if replacements > 0:
|
||||
updated_files += 1
|
||||
total_replacements += replacements
|
||||
print(f"更新: {os.path.basename(file_path)} ({replacements} 处替换)")
|
||||
|
||||
print(f"\n完成!更新了 {updated_files} 个文件,共 {total_replacements} 处替换")
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
更新所有 C# 文件中的命名空间声明和 using 语句
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
ROOT_DIR = "/mnt/f/gewuyou/System/Documents/WorkSpace/GameDev/GFramework"
|
||||
|
||||
# 命名空间替换规则(按优先级排序,长的先匹配)
|
||||
NAMESPACE_RULES = [
|
||||
# CQRS 子命名空间
|
||||
(r'\.cqrs\.notification\b', '.CQRS.Notification'),
|
||||
(r'\.cqrs\.command\b', '.CQRS.Command'),
|
||||
(r'\.cqrs\.request\b', '.CQRS.Request'),
|
||||
(r'\.cqrs\.query\b', '.CQRS.Query'),
|
||||
(r'\.cqrs\.behaviors\b', '.CQRS.Behaviors'),
|
||||
(r'\.cqrs\b', '.CQRS'),
|
||||
|
||||
# 嵌套命名空间
|
||||
(r'\.coroutine\.instructions\b', '.Coroutine.Instructions'),
|
||||
(r'\.coroutine\.extensions\b', '.Coroutine.Extensions'),
|
||||
(r'\.coroutine\b', '.Coroutine'),
|
||||
|
||||
(r'\.events\.filters\b', '.Events.Filters'),
|
||||
(r'\.events\b', '.Events'),
|
||||
|
||||
(r'\.logging\.appenders\b', '.Logging.Appenders'),
|
||||
(r'\.logging\.filters\b', '.Logging.Filters'),
|
||||
(r'\.logging\.formatters\b', '.Logging.Formatters'),
|
||||
(r'\.logging\b', '.Logging'),
|
||||
|
||||
(r'\.functional\.async\b', '.Functional.Async'),
|
||||
(r'\.functional\.control\b', '.Functional.Control'),
|
||||
(r'\.functional\.functions\b', '.Functional.Functions'),
|
||||
(r'\.functional\.pipe\b', '.Functional.Pipe'),
|
||||
(r'\.functional\.result\b', '.Functional.Result'),
|
||||
(r'\.functional\b', '.Functional'),
|
||||
|
||||
(r'\.services\.modules\b', '.Services.Modules'),
|
||||
(r'\.services\b', '.Services'),
|
||||
|
||||
(r'\.extensions\.signal\b', '.Extensions.Signal'),
|
||||
(r'\.extensions\b', '.Extensions'),
|
||||
|
||||
(r'\.setting\.data\b', '.Setting.Data'),
|
||||
(r'\.setting\.events\b', '.Setting.Events'),
|
||||
(r'\.setting\b', '.Setting'),
|
||||
|
||||
(r'\.scene\.handler\b', '.Scene.Handler'),
|
||||
(r'\.scene\b', '.Scene'),
|
||||
|
||||
(r'\.ui\.handler\b', '.UI.Handler'),
|
||||
(r'\.ui\b', '.UI'),
|
||||
|
||||
(r'\.data\.events\b', '.Data.Events'),
|
||||
(r'\.data\b', '.Data'),
|
||||
|
||||
# 单层命名空间
|
||||
(r'\.architecture\b', '.Architecture'),
|
||||
(r'\.bases\b', '.Bases'),
|
||||
(r'\.command\b', '.Command'),
|
||||
(r'\.configuration\b', '.Configuration'),
|
||||
(r'\.constants\b', '.Constants'),
|
||||
(r'\.enums\b', '.Enums'),
|
||||
(r'\.environment\b', '.Environment'),
|
||||
(r'\.internals\b', '.Internals'),
|
||||
(r'\.ioc\b', '.IoC'),
|
||||
(r'\.lifecycle\b', '.Lifecycle'),
|
||||
(r'\.model\b', '.Model'),
|
||||
(r'\.pause\b', '.Pause'),
|
||||
(r'\.pool\b', '.Pool'),
|
||||
(r'\.properties\b', '.Properties'),
|
||||
(r'\.property\b', '.Property'),
|
||||
(r'\.query\b', '.Query'),
|
||||
(r'\.registries\b', '.Registries'),
|
||||
(r'\.resource\b', '.Resource'),
|
||||
(r'\.rule\b', '.Rule'),
|
||||
(r'\.serializer\b', '.Serializer'),
|
||||
(r'\.state\b', '.State'),
|
||||
(r'\.storage\b', '.Storage'),
|
||||
(r'\.system\b', '.System'),
|
||||
(r'\.time\b', '.Time'),
|
||||
(r'\.utility\b', '.Utility'),
|
||||
(r'\.versioning\b', '.Versioning'),
|
||||
(r'\.asset\b', '.Asset'),
|
||||
(r'\.components\b', '.Components'),
|
||||
(r'\.systems\b', '.Systems'),
|
||||
(r'\.ecs\b', '.ECS'),
|
||||
(r'\.integration\b', '.Integration'),
|
||||
(r'\.mediator\b', '.Mediator'),
|
||||
(r'\.tests\b', '.Tests'),
|
||||
(r'\.analyzers\b', '.Analyzers'),
|
||||
(r'\.diagnostics\b', '.Diagnostics'),
|
||||
(r'\.generator\b', '.Generator'),
|
||||
(r'\.info\b', '.Info'),
|
||||
]
|
||||
|
||||
def update_file(file_path):
|
||||
"""更新单个文件中的命名空间"""
|
||||
try:
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
|
||||
original_content = content
|
||||
replacements = 0
|
||||
|
||||
for pattern, replacement in NAMESPACE_RULES:
|
||||
matches = re.findall(pattern, content, re.IGNORECASE)
|
||||
if matches:
|
||||
content = re.sub(pattern, replacement, content, flags=re.IGNORECASE)
|
||||
replacements += len(matches)
|
||||
|
||||
if content != original_content:
|
||||
with open(file_path, 'w', encoding='utf-8') as f:
|
||||
f.write(content)
|
||||
return replacements
|
||||
|
||||
return 0
|
||||
except Exception as e:
|
||||
print(f"错误处理文件 {file_path}: {e}")
|
||||
return 0
|
||||
|
||||
def main():
|
||||
print("开始更新命名空间...")
|
||||
|
||||
# 查找所有 C# 文件
|
||||
cs_files = []
|
||||
for root, dirs, files in os.walk(ROOT_DIR):
|
||||
# 跳过 bin, obj, Generated 目录
|
||||
dirs[:] = [d for d in dirs if d not in ['bin', 'obj', 'Generated', '.git', 'node_modules']]
|
||||
|
||||
for file in files:
|
||||
if file.endswith('.cs'):
|
||||
cs_files.append(os.path.join(root, file))
|
||||
|
||||
print(f"找到 {len(cs_files)} 个 C# 文件")
|
||||
|
||||
updated_files = 0
|
||||
total_replacements = 0
|
||||
|
||||
for file_path in cs_files:
|
||||
replacements = update_file(file_path)
|
||||
if replacements > 0:
|
||||
updated_files += 1
|
||||
total_replacements += replacements
|
||||
print(f"更新: {os.path.basename(file_path)} ({replacements} 处替换)")
|
||||
|
||||
print(f"\n完成!更新了 {updated_files} 个文件,共 {total_replacements} 处替换")
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user