mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-05-06 16:16:44 +08:00
feat(godot): 补齐 GodotLogger 接入文件
- 新增 GodotLogger 的配置加载、延迟入口和宿主输出适配实现。 - 复制 ai-libs/GodotLogger 的 MIT 许可证到 third-party-licenses/GodotLogger/LICENSE。 - 补充 active tracking 与 trace,保留下一阶段对齐点。
This commit is contained in:
parent
748bb714fb
commit
a52f3c6fec
124
GFramework.Godot.Tests/Logging/GodotLoggerSettingsLoaderTests.cs
Normal file
124
GFramework.Godot.Tests/Logging/GodotLoggerSettingsLoaderTests.cs
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using GFramework.Core.Abstractions.Logging;
|
||||||
|
using GFramework.Godot.Logging;
|
||||||
|
|
||||||
|
namespace GFramework.Godot.Tests.Logging;
|
||||||
|
|
||||||
|
[TestFixture]
|
||||||
|
public sealed class GodotLoggerSettingsLoaderTests
|
||||||
|
{
|
||||||
|
[Test]
|
||||||
|
public void DiscoverConfigurationPath_Should_Prefer_EnvironmentVariable_Then_ProcessPath_Then_ProjectPath()
|
||||||
|
{
|
||||||
|
var root = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N"));
|
||||||
|
Directory.CreateDirectory(root);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var envPath = Path.Combine(root, "env.json");
|
||||||
|
File.WriteAllText(envPath, "{}");
|
||||||
|
|
||||||
|
var executableDirectory = Path.Combine(root, "bin");
|
||||||
|
Directory.CreateDirectory(executableDirectory);
|
||||||
|
var processPath = Path.Combine(executableDirectory, "game.exe");
|
||||||
|
var executableConfigPath = Path.Combine(executableDirectory, "appsettings.json");
|
||||||
|
File.WriteAllText(executableConfigPath, "{}");
|
||||||
|
|
||||||
|
var projectPath = Path.Combine(root, "project-appsettings.json");
|
||||||
|
File.WriteAllText(projectPath, "{}");
|
||||||
|
|
||||||
|
var discoveredFromEnvironment = GodotLoggerSettingsLoader.DiscoverConfigurationPath(
|
||||||
|
environmentPath: envPath,
|
||||||
|
processPath: processPath,
|
||||||
|
projectPathResolver: _ => projectPath);
|
||||||
|
var discoveredFromProcess = GodotLoggerSettingsLoader.DiscoverConfigurationPath(
|
||||||
|
environmentPath: Path.Combine(root, "missing-env.json"),
|
||||||
|
processPath: processPath,
|
||||||
|
projectPathResolver: _ => projectPath);
|
||||||
|
var discoveredFromProject = GodotLoggerSettingsLoader.DiscoverConfigurationPath(
|
||||||
|
environmentPath: Path.Combine(root, "missing-env.json"),
|
||||||
|
processPath: Path.Combine(root, "missing", "game.exe"),
|
||||||
|
projectPathResolver: _ => projectPath);
|
||||||
|
|
||||||
|
Assert.That(discoveredFromEnvironment, Is.EqualTo(envPath));
|
||||||
|
Assert.That(discoveredFromProcess, Is.EqualTo(executableConfigPath));
|
||||||
|
Assert.That(discoveredFromProject, Is.EqualTo(projectPath));
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Directory.Delete(root, recursive: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void LoadFromJsonString_Should_Read_GodotLogger_Options_And_Category_Levels()
|
||||||
|
{
|
||||||
|
const string json = """
|
||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Warning",
|
||||||
|
"Game.Services": "Error"
|
||||||
|
},
|
||||||
|
"GodotLogger": {
|
||||||
|
"Mode": "Release",
|
||||||
|
"DebugMinLevel": "Debug",
|
||||||
|
"ReleaseMinLevel": "Information",
|
||||||
|
"DebugOutputTemplate": "[dbg] {message}",
|
||||||
|
"ReleaseOutputTemplate": "[rel] {message}{properties}",
|
||||||
|
"Colors": {
|
||||||
|
"Info": "aqua"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
var settings = GodotLoggerSettingsLoader.LoadFromJsonString(json);
|
||||||
|
|
||||||
|
Assert.Multiple(() =>
|
||||||
|
{
|
||||||
|
Assert.That(settings.Options.Mode, Is.EqualTo(GodotLoggerMode.Release));
|
||||||
|
Assert.That(settings.Options.ReleaseOutputTemplate, Is.EqualTo("[rel] {message}{properties}"));
|
||||||
|
Assert.That(settings.Options.GetColor(LogLevel.Info), Is.EqualTo("aqua"));
|
||||||
|
Assert.That(settings.GetEffectiveMinLevel("Game.Services.Inventory", LogLevel.Trace), Is.EqualTo(LogLevel.Error));
|
||||||
|
Assert.That(settings.GetEffectiveMinLevel("Game.Other", LogLevel.Trace), Is.EqualTo(LogLevel.Warning));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Provider_Should_Apply_Updated_Settings_To_Existing_Loggers()
|
||||||
|
{
|
||||||
|
var settings = new GodotLoggerSettings(new GodotLoggerOptions
|
||||||
|
{
|
||||||
|
Mode = GodotLoggerMode.Debug,
|
||||||
|
DebugMinLevel = LogLevel.Error,
|
||||||
|
ReleaseMinLevel = LogLevel.Error
|
||||||
|
});
|
||||||
|
var provider = new GodotLoggerFactoryProvider(() => settings);
|
||||||
|
var logger = provider.CreateLogger("Game.Services.Inventory");
|
||||||
|
|
||||||
|
Assert.That(logger.IsInfoEnabled(), Is.False);
|
||||||
|
|
||||||
|
settings = new GodotLoggerSettings(
|
||||||
|
new GodotLoggerOptions
|
||||||
|
{
|
||||||
|
Mode = GodotLoggerMode.Debug,
|
||||||
|
DebugMinLevel = LogLevel.Info,
|
||||||
|
ReleaseMinLevel = LogLevel.Info
|
||||||
|
},
|
||||||
|
defaultLogLevel: LogLevel.Trace,
|
||||||
|
loggerLevels: new Dictionary<string, LogLevel>(StringComparer.Ordinal)
|
||||||
|
{
|
||||||
|
["Game.Services"] = LogLevel.Debug
|
||||||
|
});
|
||||||
|
|
||||||
|
Assert.Multiple(() =>
|
||||||
|
{
|
||||||
|
Assert.That(logger.IsInfoEnabled(), Is.True);
|
||||||
|
Assert.That(logger.IsDebugEnabled(), Is.False);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
311
GFramework.Godot/Logging/DeferredLogger.cs
Normal file
311
GFramework.Godot/Logging/DeferredLogger.cs
Normal file
@ -0,0 +1,311 @@
|
|||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using GFramework.Core.Abstractions.Logging;
|
||||||
|
|
||||||
|
namespace GFramework.Godot.Logging;
|
||||||
|
|
||||||
|
internal sealed class DeferredLogger(string category, Func<ILoggerFactoryProvider> providerAccessor) : IStructuredLogger
|
||||||
|
{
|
||||||
|
private ILogger? _inner;
|
||||||
|
|
||||||
|
private ILogger Inner => _inner ??= ResolveLogger();
|
||||||
|
|
||||||
|
public string Name()
|
||||||
|
{
|
||||||
|
return Inner.Name();
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsTraceEnabled()
|
||||||
|
{
|
||||||
|
return Inner.IsTraceEnabled();
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsDebugEnabled()
|
||||||
|
{
|
||||||
|
return Inner.IsDebugEnabled();
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsInfoEnabled()
|
||||||
|
{
|
||||||
|
return Inner.IsInfoEnabled();
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsWarnEnabled()
|
||||||
|
{
|
||||||
|
return Inner.IsWarnEnabled();
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsErrorEnabled()
|
||||||
|
{
|
||||||
|
return Inner.IsErrorEnabled();
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsFatalEnabled()
|
||||||
|
{
|
||||||
|
return Inner.IsFatalEnabled();
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsEnabledForLevel(LogLevel level)
|
||||||
|
{
|
||||||
|
return Inner.IsEnabledForLevel(level);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Trace(string msg)
|
||||||
|
{
|
||||||
|
Inner.Trace(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Trace(string format, object arg)
|
||||||
|
{
|
||||||
|
Inner.Trace(format, arg);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Trace(string format, object arg1, object arg2)
|
||||||
|
{
|
||||||
|
Inner.Trace(format, arg1, arg2);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Trace(string format, params object[] arguments)
|
||||||
|
{
|
||||||
|
Inner.Trace(format, arguments);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Trace(string msg, Exception t)
|
||||||
|
{
|
||||||
|
Inner.Trace(msg, t);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Debug(string msg)
|
||||||
|
{
|
||||||
|
Inner.Debug(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Debug(string format, object arg)
|
||||||
|
{
|
||||||
|
Inner.Debug(format, arg);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Debug(string format, object arg1, object arg2)
|
||||||
|
{
|
||||||
|
Inner.Debug(format, arg1, arg2);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Debug(string format, params object[] arguments)
|
||||||
|
{
|
||||||
|
Inner.Debug(format, arguments);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Debug(string msg, Exception t)
|
||||||
|
{
|
||||||
|
Inner.Debug(msg, t);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Info(string msg)
|
||||||
|
{
|
||||||
|
Inner.Info(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Info(string format, object arg)
|
||||||
|
{
|
||||||
|
Inner.Info(format, arg);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Info(string format, object arg1, object arg2)
|
||||||
|
{
|
||||||
|
Inner.Info(format, arg1, arg2);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Info(string format, params object[] arguments)
|
||||||
|
{
|
||||||
|
Inner.Info(format, arguments);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Info(string msg, Exception t)
|
||||||
|
{
|
||||||
|
Inner.Info(msg, t);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Warn(string msg)
|
||||||
|
{
|
||||||
|
Inner.Warn(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Warn(string format, object arg)
|
||||||
|
{
|
||||||
|
Inner.Warn(format, arg);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Warn(string format, object arg1, object arg2)
|
||||||
|
{
|
||||||
|
Inner.Warn(format, arg1, arg2);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Warn(string format, params object[] arguments)
|
||||||
|
{
|
||||||
|
Inner.Warn(format, arguments);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Warn(string msg, Exception t)
|
||||||
|
{
|
||||||
|
Inner.Warn(msg, t);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Error(string msg)
|
||||||
|
{
|
||||||
|
Inner.Error(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Error(string format, object arg)
|
||||||
|
{
|
||||||
|
Inner.Error(format, arg);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Error(string format, object arg1, object arg2)
|
||||||
|
{
|
||||||
|
Inner.Error(format, arg1, arg2);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Error(string format, params object[] arguments)
|
||||||
|
{
|
||||||
|
Inner.Error(format, arguments);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Error(string msg, Exception t)
|
||||||
|
{
|
||||||
|
Inner.Error(msg, t);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Fatal(string msg)
|
||||||
|
{
|
||||||
|
Inner.Fatal(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Fatal(string format, object arg)
|
||||||
|
{
|
||||||
|
Inner.Fatal(format, arg);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Fatal(string format, object arg1, object arg2)
|
||||||
|
{
|
||||||
|
Inner.Fatal(format, arg1, arg2);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Fatal(string format, params object[] arguments)
|
||||||
|
{
|
||||||
|
Inner.Fatal(format, arguments);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Fatal(string msg, Exception t)
|
||||||
|
{
|
||||||
|
Inner.Fatal(msg, t);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Log(LogLevel level, string message)
|
||||||
|
{
|
||||||
|
LogFallback(level, message, exception: null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Log(LogLevel level, string format, object arg)
|
||||||
|
{
|
||||||
|
LogFallback(level, string.Format(format, arg), exception: null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Log(LogLevel level, string format, object arg1, object arg2)
|
||||||
|
{
|
||||||
|
LogFallback(level, string.Format(format, arg1, arg2), exception: null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Log(LogLevel level, string format, params object[] arguments)
|
||||||
|
{
|
||||||
|
LogFallback(level, string.Format(format, arguments), exception: null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Log(LogLevel level, string message, Exception exception)
|
||||||
|
{
|
||||||
|
LogFallback(level, message, exception);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Log(LogLevel level, string message, params (string Key, object? Value)[] properties)
|
||||||
|
{
|
||||||
|
if (Inner is IStructuredLogger structuredLogger)
|
||||||
|
{
|
||||||
|
structuredLogger.Log(level, message, properties);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
LogFallback(level, message, exception: null, properties);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Log(LogLevel level, string message, Exception? exception, params (string Key, object? Value)[] properties)
|
||||||
|
{
|
||||||
|
if (Inner is IStructuredLogger structuredLogger)
|
||||||
|
{
|
||||||
|
structuredLogger.Log(level, message, exception, properties);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
LogFallback(level, message, exception, properties);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ILogger ResolveLogger()
|
||||||
|
{
|
||||||
|
return providerAccessor().CreateLogger(category);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void LogFallback(
|
||||||
|
LogLevel level,
|
||||||
|
string message,
|
||||||
|
Exception? exception,
|
||||||
|
params (string Key, object? Value)[] properties)
|
||||||
|
{
|
||||||
|
var suffix = properties.Length == 0
|
||||||
|
? string.Empty
|
||||||
|
: " | " + string.Join(", ", properties.Select(static property => $"{property.Key}={property.Value}"));
|
||||||
|
var rendered = message + suffix;
|
||||||
|
|
||||||
|
switch (level)
|
||||||
|
{
|
||||||
|
case LogLevel.Trace:
|
||||||
|
WriteFallback(rendered, exception, Inner.Trace, Inner.Trace);
|
||||||
|
break;
|
||||||
|
case LogLevel.Debug:
|
||||||
|
WriteFallback(rendered, exception, Inner.Debug, Inner.Debug);
|
||||||
|
break;
|
||||||
|
case LogLevel.Info:
|
||||||
|
WriteFallback(rendered, exception, Inner.Info, Inner.Info);
|
||||||
|
break;
|
||||||
|
case LogLevel.Warning:
|
||||||
|
WriteFallback(rendered, exception, Inner.Warn, Inner.Warn);
|
||||||
|
break;
|
||||||
|
case LogLevel.Error:
|
||||||
|
WriteFallback(rendered, exception, Inner.Error, Inner.Error);
|
||||||
|
break;
|
||||||
|
case LogLevel.Fatal:
|
||||||
|
WriteFallback(rendered, exception, Inner.Fatal, Inner.Fatal);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(level), level, "Unsupported log level.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void LogFallback(LogLevel level, string message, Exception? exception)
|
||||||
|
{
|
||||||
|
LogFallback(level, message, exception, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void WriteFallback(
|
||||||
|
string message,
|
||||||
|
Exception? exception,
|
||||||
|
Action<string> writeMessage,
|
||||||
|
Action<string, Exception> writeException)
|
||||||
|
{
|
||||||
|
if (exception == null)
|
||||||
|
{
|
||||||
|
writeMessage(message);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
writeException(message, exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
103
GFramework.Godot/Logging/GodotLog.cs
Normal file
103
GFramework.Godot/Logging/GodotLog.cs
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading;
|
||||||
|
using GFramework.Core.Abstractions.Logging;
|
||||||
|
|
||||||
|
namespace GFramework.Godot.Logging;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Static Godot logging entry point with auto-discovered configuration and deferred logger creation.
|
||||||
|
/// </summary>
|
||||||
|
public static class GodotLog
|
||||||
|
{
|
||||||
|
#if NET9_0_OR_GREATER
|
||||||
|
private static readonly System.Threading.Lock ConfigureLock = new();
|
||||||
|
#else
|
||||||
|
private static readonly object ConfigureLock = new();
|
||||||
|
#endif
|
||||||
|
private static Action<GodotLoggerOptions>? _configure;
|
||||||
|
|
||||||
|
private static readonly Lazy<GodotLogConfigurationSource> LazyConfigurationSource = new(
|
||||||
|
CreateConfigurationSource,
|
||||||
|
LazyThreadSafetyMode.ExecutionAndPublication);
|
||||||
|
|
||||||
|
private static readonly Lazy<GodotLoggerFactoryProvider> LazyProvider = new(
|
||||||
|
static () => new GodotLoggerFactoryProvider(() => LazyConfigurationSource.Value.CurrentSettings),
|
||||||
|
LazyThreadSafetyMode.ExecutionAndPublication);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Applies imperative option overrides before the global Godot logger provider is materialized.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="configure">The options mutator.</param>
|
||||||
|
public static void Configure(Action<GodotLoggerOptions> configure)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(configure);
|
||||||
|
|
||||||
|
lock (ConfigureLock)
|
||||||
|
{
|
||||||
|
if (LazyProvider.IsValueCreated)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
"GodotLog.Configure must be called before any GodotLog logger is materialized.");
|
||||||
|
}
|
||||||
|
|
||||||
|
_configure = configure;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the lazily-configured Godot logger provider.
|
||||||
|
/// </summary>
|
||||||
|
public static ILoggerFactoryProvider Provider
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
lock (ConfigureLock)
|
||||||
|
{
|
||||||
|
return LazyProvider.Value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the discovered configuration file path, if any.
|
||||||
|
/// </summary>
|
||||||
|
public static string? ConfigurationPath => LazyConfigurationSource.Value.ConfigurationPath;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a logger for the specified category without materializing the provider until first use.
|
||||||
|
/// </summary>
|
||||||
|
public static ILogger CreateLogger(string category)
|
||||||
|
{
|
||||||
|
ArgumentException.ThrowIfNullOrWhiteSpace(category);
|
||||||
|
return new DeferredLogger(category, static () => Provider);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a logger for the specified type without materializing the provider until first use.
|
||||||
|
/// </summary>
|
||||||
|
public static ILogger CreateLogger<T>()
|
||||||
|
{
|
||||||
|
return CreateLogger(GetCategoryName(typeof(T)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Installs the Godot provider as the current global resolver provider.
|
||||||
|
/// </summary>
|
||||||
|
public static void UseAsDefaultProvider()
|
||||||
|
{
|
||||||
|
LoggerFactoryResolver.Provider = Provider;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static GodotLogConfigurationSource CreateConfigurationSource()
|
||||||
|
{
|
||||||
|
lock (ConfigureLock)
|
||||||
|
{
|
||||||
|
return new GodotLogConfigurationSource(_configure);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetCategoryName(Type type)
|
||||||
|
{
|
||||||
|
return type.FullName ?? type.Name;
|
||||||
|
}
|
||||||
|
}
|
||||||
150
GFramework.Godot/Logging/GodotLogConfigurationSource.cs
Normal file
150
GFramework.Godot/Logging/GodotLogConfigurationSource.cs
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Threading;
|
||||||
|
using GFramework.Core.Abstractions.Logging;
|
||||||
|
|
||||||
|
namespace GFramework.Godot.Logging;
|
||||||
|
|
||||||
|
internal sealed class GodotLogConfigurationSource : IDisposable
|
||||||
|
{
|
||||||
|
private readonly Action<GodotLoggerOptions>? _configure;
|
||||||
|
private readonly FileSystemWatcher? _watcher;
|
||||||
|
private GodotLoggerSettings _currentSettings = GodotLoggerSettings.Default;
|
||||||
|
|
||||||
|
public GodotLogConfigurationSource(Action<GodotLoggerOptions>? configure)
|
||||||
|
{
|
||||||
|
_configure = configure;
|
||||||
|
ConfigurationPath = GodotLoggerSettingsLoader.DiscoverConfigurationPath();
|
||||||
|
Reload(throwOnError: true);
|
||||||
|
_watcher = CreateWatcher(ConfigurationPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string? ConfigurationPath { get; }
|
||||||
|
|
||||||
|
public GodotLoggerSettings CurrentSettings => Volatile.Read(ref _currentSettings);
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_watcher?.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
private FileSystemWatcher? CreateWatcher(string? configurationPath)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(configurationPath))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var directory = Path.GetDirectoryName(configurationPath);
|
||||||
|
var fileName = Path.GetFileName(configurationPath);
|
||||||
|
if (string.IsNullOrWhiteSpace(directory) || string.IsNullOrWhiteSpace(fileName))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var watcher = new FileSystemWatcher(directory, fileName)
|
||||||
|
{
|
||||||
|
EnableRaisingEvents = true,
|
||||||
|
NotifyFilter = NotifyFilters.CreationTime
|
||||||
|
| NotifyFilters.FileName
|
||||||
|
| NotifyFilters.LastWrite
|
||||||
|
| NotifyFilters.Size
|
||||||
|
};
|
||||||
|
|
||||||
|
watcher.Changed += OnConfigurationChanged;
|
||||||
|
watcher.Created += OnConfigurationChanged;
|
||||||
|
watcher.Deleted += OnConfigurationChanged;
|
||||||
|
watcher.Renamed += OnConfigurationRenamed;
|
||||||
|
return watcher;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnConfigurationChanged(object sender, FileSystemEventArgs e)
|
||||||
|
{
|
||||||
|
Reload(throwOnError: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnConfigurationRenamed(object sender, RenamedEventArgs e)
|
||||||
|
{
|
||||||
|
Reload(throwOnError: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Reload(bool throwOnError)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var settings = LoadSettingsWithRetry();
|
||||||
|
Volatile.Write(ref _currentSettings, settings);
|
||||||
|
}
|
||||||
|
catch when (!throwOnError)
|
||||||
|
{
|
||||||
|
// Ignore transient parse or file-lock failures during hot reload and keep the last good snapshot.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private GodotLoggerSettings LoadSettingsWithRetry()
|
||||||
|
{
|
||||||
|
Exception? lastError = null;
|
||||||
|
|
||||||
|
for (var attempt = 0; attempt < 3; attempt++)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return LoadSettings();
|
||||||
|
}
|
||||||
|
catch (IOException ex)
|
||||||
|
{
|
||||||
|
lastError = ex;
|
||||||
|
}
|
||||||
|
catch (UnauthorizedAccessException ex)
|
||||||
|
{
|
||||||
|
lastError = ex;
|
||||||
|
}
|
||||||
|
|
||||||
|
Thread.Sleep(50);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw lastError ?? new InvalidOperationException("Failed to load Godot logging configuration.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private GodotLoggerSettings LoadSettings()
|
||||||
|
{
|
||||||
|
var settings = string.IsNullOrWhiteSpace(ConfigurationPath) || !File.Exists(ConfigurationPath)
|
||||||
|
? GodotLoggerSettings.Default
|
||||||
|
: GodotLoggerSettingsLoader.LoadFromJsonFile(ConfigurationPath);
|
||||||
|
|
||||||
|
if (_configure == null)
|
||||||
|
{
|
||||||
|
return settings;
|
||||||
|
}
|
||||||
|
|
||||||
|
var configuredOptions = CloneOptions(settings.Options);
|
||||||
|
_configure(configuredOptions);
|
||||||
|
return new GodotLoggerSettings(configuredOptions, settings.DefaultLogLevel, CopyLoggerLevels(settings));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static GodotLoggerOptions CloneOptions(GodotLoggerOptions options)
|
||||||
|
{
|
||||||
|
return new GodotLoggerOptions
|
||||||
|
{
|
||||||
|
Mode = options.Mode,
|
||||||
|
DebugMinLevel = options.DebugMinLevel,
|
||||||
|
ReleaseMinLevel = options.ReleaseMinLevel,
|
||||||
|
DebugOutputTemplate = options.DebugOutputTemplate,
|
||||||
|
ReleaseOutputTemplate = options.ReleaseOutputTemplate,
|
||||||
|
Colors = new Dictionary<GFramework.Core.Abstractions.Logging.LogLevel, string>(options.Colors)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IReadOnlyDictionary<string, LogLevel> CopyLoggerLevels(
|
||||||
|
GodotLoggerSettings settings)
|
||||||
|
{
|
||||||
|
var levels = new Dictionary<string, LogLevel>(StringComparer.Ordinal);
|
||||||
|
foreach (var pair in settings.LoggerLevels)
|
||||||
|
{
|
||||||
|
levels[pair.Key] = pair.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return levels;
|
||||||
|
}
|
||||||
|
}
|
||||||
76
GFramework.Godot/Logging/GodotLoggerSettings.cs
Normal file
76
GFramework.Godot/Logging/GodotLoggerSettings.cs
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using GFramework.Core.Abstractions.Logging;
|
||||||
|
|
||||||
|
namespace GFramework.Godot.Logging;
|
||||||
|
|
||||||
|
internal sealed class GodotLoggerSettings
|
||||||
|
{
|
||||||
|
private readonly IReadOnlyDictionary<string, LogLevel> _loggerLevels;
|
||||||
|
|
||||||
|
public static GodotLoggerSettings Default { get; } = new(new GodotLoggerOptions());
|
||||||
|
|
||||||
|
public GodotLoggerSettings(
|
||||||
|
GodotLoggerOptions options,
|
||||||
|
LogLevel? defaultLogLevel = null,
|
||||||
|
IReadOnlyDictionary<string, LogLevel>? loggerLevels = null)
|
||||||
|
{
|
||||||
|
Options = options ?? throw new ArgumentNullException(nameof(options));
|
||||||
|
DefaultLogLevel = defaultLogLevel;
|
||||||
|
_loggerLevels = loggerLevels ?? new Dictionary<string, LogLevel>(StringComparer.Ordinal);
|
||||||
|
}
|
||||||
|
|
||||||
|
public LogLevel? DefaultLogLevel { get; }
|
||||||
|
|
||||||
|
public GodotLoggerOptions Options { get; }
|
||||||
|
|
||||||
|
public IReadOnlyDictionary<string, LogLevel> LoggerLevels => _loggerLevels;
|
||||||
|
|
||||||
|
public static GodotLoggerSettings FromOptions(GodotLoggerOptions options)
|
||||||
|
{
|
||||||
|
return new GodotLoggerSettings(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
public LogLevel GetEffectiveMinLevel(string categoryName, LogLevel providerMinLevel)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(categoryName);
|
||||||
|
|
||||||
|
var effective = Max(Options.GetEffectiveMinLevel(), providerMinLevel);
|
||||||
|
var configuredLevel = GetConfiguredMinLevel(categoryName);
|
||||||
|
return configuredLevel.HasValue ? Max(effective, configuredLevel.Value) : effective;
|
||||||
|
}
|
||||||
|
|
||||||
|
private LogLevel? GetConfiguredMinLevel(string categoryName)
|
||||||
|
{
|
||||||
|
if (_loggerLevels.TryGetValue(categoryName, out var exactLevel))
|
||||||
|
{
|
||||||
|
return exactLevel;
|
||||||
|
}
|
||||||
|
|
||||||
|
var bestMatchLength = -1;
|
||||||
|
LogLevel? bestMatchLevel = DefaultLogLevel;
|
||||||
|
|
||||||
|
foreach (var pair in _loggerLevels)
|
||||||
|
{
|
||||||
|
if (!categoryName.StartsWith(pair.Key + ".", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pair.Key.Length <= bestMatchLength)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
bestMatchLength = pair.Key.Length;
|
||||||
|
bestMatchLevel = pair.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return bestMatchLevel;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static LogLevel Max(LogLevel left, LogLevel right)
|
||||||
|
{
|
||||||
|
return left > right ? left : right;
|
||||||
|
}
|
||||||
|
}
|
||||||
176
GFramework.Godot/Logging/GodotLoggerSettingsLoader.cs
Normal file
176
GFramework.Godot/Logging/GodotLoggerSettingsLoader.cs
Normal file
@ -0,0 +1,176 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using GFramework.Core.Abstractions.Logging;
|
||||||
|
using Godot;
|
||||||
|
|
||||||
|
namespace GFramework.Godot.Logging;
|
||||||
|
|
||||||
|
internal static class GodotLoggerSettingsLoader
|
||||||
|
{
|
||||||
|
internal const string ConfigEnvironmentVariableName = "GODOT_LOGGER_CONFIG";
|
||||||
|
|
||||||
|
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||||
|
{
|
||||||
|
AllowTrailingCommas = true,
|
||||||
|
PropertyNameCaseInsensitive = true,
|
||||||
|
ReadCommentHandling = JsonCommentHandling.Skip,
|
||||||
|
Converters =
|
||||||
|
{
|
||||||
|
new GodotLogLevelJsonConverter(),
|
||||||
|
new JsonStringEnumConverter(JsonNamingPolicy.CamelCase, allowIntegerValues: true)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
public static string? DiscoverConfigurationPath(
|
||||||
|
string? environmentPath = null,
|
||||||
|
string? processPath = null,
|
||||||
|
Func<string, string?>? projectPathResolver = null)
|
||||||
|
{
|
||||||
|
var envPath = environmentPath ?? System.Environment.GetEnvironmentVariable(ConfigEnvironmentVariableName);
|
||||||
|
if (!string.IsNullOrWhiteSpace(envPath) && File.Exists(envPath))
|
||||||
|
{
|
||||||
|
return envPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
var resolvedProcessPath = processPath ?? System.Environment.ProcessPath;
|
||||||
|
if (!string.IsNullOrWhiteSpace(resolvedProcessPath))
|
||||||
|
{
|
||||||
|
var executableDirectory = Path.GetDirectoryName(resolvedProcessPath);
|
||||||
|
if (!string.IsNullOrWhiteSpace(executableDirectory))
|
||||||
|
{
|
||||||
|
var candidate = Path.Combine(executableDirectory, "appsettings.json");
|
||||||
|
if (File.Exists(candidate))
|
||||||
|
{
|
||||||
|
return candidate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var resolver = projectPathResolver ?? SafeGlobalizeProjectPath;
|
||||||
|
var projectCandidate = resolver("res://appsettings.json");
|
||||||
|
if (!string.IsNullOrWhiteSpace(projectCandidate) && File.Exists(projectCandidate))
|
||||||
|
{
|
||||||
|
return projectCandidate;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static GodotLoggerSettings LoadFromJsonFile(string filePath)
|
||||||
|
{
|
||||||
|
ArgumentException.ThrowIfNullOrWhiteSpace(filePath);
|
||||||
|
|
||||||
|
if (!File.Exists(filePath))
|
||||||
|
{
|
||||||
|
throw new FileNotFoundException($"Configuration file not found: {filePath}", filePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
return LoadFromJsonString(File.ReadAllText(filePath));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static GodotLoggerSettings LoadFromJsonString(string json)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(json);
|
||||||
|
|
||||||
|
var root = JsonSerializer.Deserialize<GodotLoggerSettingsDocument>(json, JsonOptions)
|
||||||
|
?? throw new InvalidOperationException("Failed to deserialize Godot logging configuration.");
|
||||||
|
|
||||||
|
var logging = root.Logging;
|
||||||
|
var options = logging?.GodotLogger ?? new GodotLoggerOptions();
|
||||||
|
|
||||||
|
LogLevel? defaultLogLevel = null;
|
||||||
|
var loggerLevels = new Dictionary<string, LogLevel>(StringComparer.Ordinal);
|
||||||
|
|
||||||
|
if (logging?.LogLevel != null)
|
||||||
|
{
|
||||||
|
foreach (var pair in logging.LogLevel)
|
||||||
|
{
|
||||||
|
if (string.Equals(pair.Key, "Default", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
defaultLogLevel = pair.Value;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
loggerLevels[pair.Key] = pair.Value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new GodotLoggerSettings(options, defaultLogLevel, loggerLevels);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? SafeGlobalizeProjectPath(string path)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return ProjectSettings.GlobalizePath(path);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class GodotLoggerSettingsDocument
|
||||||
|
{
|
||||||
|
public LoggingDocument? Logging { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class LoggingDocument
|
||||||
|
{
|
||||||
|
public GodotLoggerOptions? GodotLogger { get; set; }
|
||||||
|
|
||||||
|
public Dictionary<string, LogLevel>? LogLevel { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class GodotLogLevelJsonConverter : JsonConverter<LogLevel>
|
||||||
|
{
|
||||||
|
public override LogLevel Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
if (reader.TokenType == JsonTokenType.Number && reader.TryGetInt32(out var numericValue))
|
||||||
|
{
|
||||||
|
return (LogLevel)numericValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (reader.TokenType != JsonTokenType.String)
|
||||||
|
{
|
||||||
|
throw new JsonException($"Unexpected token {reader.TokenType} when parsing {nameof(LogLevel)}.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return Parse(reader.GetString());
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Write(Utf8JsonWriter writer, LogLevel value, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
writer.WriteStringValue(value.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
public override LogLevel ReadAsPropertyName(ref Utf8JsonReader reader, Type typeToConvert,
|
||||||
|
JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
return Parse(reader.GetString());
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void WriteAsPropertyName(Utf8JsonWriter writer, LogLevel value, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
writer.WritePropertyName(value.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static LogLevel Parse(string? value)
|
||||||
|
{
|
||||||
|
return value?.Trim() switch
|
||||||
|
{
|
||||||
|
"Trace" or "trace" => LogLevel.Trace,
|
||||||
|
"Debug" or "debug" => LogLevel.Debug,
|
||||||
|
"Info" or "info" or "Information" or "information" => LogLevel.Info,
|
||||||
|
"Warning" or "warning" or "Warn" or "warn" => LogLevel.Warning,
|
||||||
|
"Error" or "error" => LogLevel.Error,
|
||||||
|
"Fatal" or "fatal" or "Critical" or "critical" => LogLevel.Fatal,
|
||||||
|
_ => throw new JsonException($"Unsupported log level '{value}'.")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,77 @@
|
|||||||
|
# Godot Logging Compliance Polish 跟踪
|
||||||
|
|
||||||
|
## 目标
|
||||||
|
|
||||||
|
继续把 `GFramework.Godot.Logging` 从“基础可用的 Godot 输出适配”收敛成“对齐 `GodotLogger` 优点、但保持
|
||||||
|
GFramework 自身日志抽象不分叉”的稳定宿主层,并为后续 Godot / Core 日志统一留下清晰恢复点。
|
||||||
|
|
||||||
|
## 当前恢复点
|
||||||
|
|
||||||
|
- 恢复点编号:`GODOT-LOGGING-COMPLIANCE-POLISH-RP-001`
|
||||||
|
- 当前阶段:`Phase 1`
|
||||||
|
- 当前焦点:
|
||||||
|
- 已补齐 `GodotLog` 静态入口、延迟 logger 解析、配置自动发现与热重载
|
||||||
|
- 已让 `GodotLoggerFactoryProvider` 对已缓存 logger 生效动态配置,而不是只在新建 logger 时读快照
|
||||||
|
- 已让 `GodotLogger` 支持 `{properties}` 占位符,并把 `IStructuredLogger` / `LogContext` 属性落到 Godot 输出
|
||||||
|
- 已兼容 `GodotLogger` 风格配置值,如 `Information` / `Critical`
|
||||||
|
- 下一轮优先评估是否把 Godot 输出进一步并入 Core 的 appender / formatter / filter 组合管线
|
||||||
|
|
||||||
|
## 当前状态摘要
|
||||||
|
|
||||||
|
- `GFramework.Core` 仍是主日志框架;`GFramework.Godot` 没有引入第二套业务日志 API
|
||||||
|
- `GFramework.Godot.Logging` 现在已经补上原 `GodotLogger` 项目最有价值的宿主便利层:
|
||||||
|
- `GodotLog`
|
||||||
|
- `DeferredLogger`
|
||||||
|
- 配置文件自动发现
|
||||||
|
- 文件热重载
|
||||||
|
- 结构化属性渲染
|
||||||
|
- 本轮没有把 `Microsoft.Extensions.Logging` 的 `ILoggingBuilder` / `ILoggerProvider` 生态原样搬入 GFramework
|
||||||
|
- `AbstractLogger` 已支持动态最小级别提供器,为 Godot 配置热更新生效打通基础能力
|
||||||
|
|
||||||
|
## 当前活跃事实
|
||||||
|
|
||||||
|
- 当前主题由分支 `feat/godot-logging-compliance-polish` 驱动,并已在 `ai-plan/public/README.md` 建立映射
|
||||||
|
- `ai-libs/GodotLogger` 的 MIT 许可证已复制到 `third-party-licenses/GodotLogger/LICENSE`
|
||||||
|
- `GodotLog` 当前的配置发现顺序为:
|
||||||
|
- `GODOT_LOGGER_CONFIG`
|
||||||
|
- 可执行目录 `appsettings.json`
|
||||||
|
- `res://appsettings.json`
|
||||||
|
- `GodotLog.Configure(...)` 仍要求在首次 materialize provider 前调用;延迟 logger 会避免 `static readonly` 字段过早锁死配置
|
||||||
|
- `GodotLoggerFactoryProvider` 当前按 logger 名称缓存实例,但每次判定级别和写日志都会读取最新 `GodotLoggerSettings`
|
||||||
|
- Godot 模板默认已扩展为包含 `{properties}`,因此结构化属性和 `LogContext` 会进入渲染结果
|
||||||
|
- 配置加载兼容两套级别命名:
|
||||||
|
- GFramework 风格:`Info` / `Fatal`
|
||||||
|
- `GodotLogger` 风格:`Information` / `Critical`
|
||||||
|
- 现有设计仍保留 UTC 时间戳语义,没有为了对齐原项目而默认切回本地时间
|
||||||
|
|
||||||
|
## 当前风险
|
||||||
|
|
||||||
|
- 双入口生命周期风险:如果同一宿主同时混用 `LoggerFactoryResolver.Provider` 与 `GodotLog`,需要明确谁是最终默认 provider
|
||||||
|
- 缓解措施:当前文档与实现都保留 `GodotLog.UseAsDefaultProvider()`,并继续把 `ArchitectureConfiguration` 方式写成默认推荐路径
|
||||||
|
- Core / Godot 管线分离风险:Godot 侧虽然已有热重载与配置发现,但还没有变成 Core 可组合 appender
|
||||||
|
- 缓解措施:下一轮只评估“Godot sink / appender 化”,不再继续扩张独立的 Godot logging 面
|
||||||
|
- 配置热重载的宿主差异风险:Godot 编辑器、导出包和测试宿主的文件系统语义不完全一致
|
||||||
|
- 缓解措施:active 入口先锁定 discovery / reload 语义,后续若遇到平台差异,再用定向回归和文档补充收口
|
||||||
|
|
||||||
|
## 活跃文档
|
||||||
|
|
||||||
|
- 当前跟踪:[godot-logging-compliance-polish-tracking.md](./godot-logging-compliance-polish-tracking.md)
|
||||||
|
- 当前 trace:[godot-logging-compliance-polish-trace.md](../traces/godot-logging-compliance-polish-trace.md)
|
||||||
|
|
||||||
|
## 验证说明
|
||||||
|
|
||||||
|
- `dotnet test GFramework.Godot.Tests/GFramework.Godot.Tests.csproj -c Release --filter FullyQualifiedName~GodotLog -nologo`
|
||||||
|
- 结果:通过
|
||||||
|
- 备注:定向新增 Godot logging 配置 / 模板回归共 `11` 项通过
|
||||||
|
- `dotnet test GFramework.Godot.Tests/GFramework.Godot.Tests.csproj -c Release -nologo`
|
||||||
|
- 结果:通过
|
||||||
|
- 备注:Godot 测试项目共 `69` 项通过
|
||||||
|
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter FullyQualifiedName~Logging -nologo`
|
||||||
|
- 结果:通过
|
||||||
|
- 备注:Core logging 相关测试共 `214` 项通过,覆盖 `AbstractLogger` 动态最小级别改造回归
|
||||||
|
|
||||||
|
## 下一步
|
||||||
|
|
||||||
|
1. 评估是否需要把 Godot 控制台输出收敛成 Core 可组合 sink / appender,而不是继续扩张独立 provider 逻辑
|
||||||
|
2. 若继续做 Godot logger 能力,优先补真实宿主下的配置 reload / 输出行为回归,而不是再添加新的公开入口
|
||||||
|
3. 若本轮改动进入 PR,后续 review / follow-up 继续写回本 topic,而不是另开第二份 Godot logging 追踪
|
||||||
@ -0,0 +1,54 @@
|
|||||||
|
# Godot Logging Compliance Polish 追踪
|
||||||
|
|
||||||
|
## 2026-05-02
|
||||||
|
|
||||||
|
### 阶段:GodotLogger 优点吸收与宿主层补齐(RP-001)
|
||||||
|
|
||||||
|
- 复核 `GodotLogger` 与 `GFramework.Godot.Logging` 后确认:
|
||||||
|
- 模板、颜色、Debug/Release 双模式、类别缩写和 Godot 输出路由已基本吸收
|
||||||
|
- 真正缺口主要在宿主接入与运行期配置层,而不是输出格式层
|
||||||
|
- 本轮新建 `godot-logging-compliance-polish` topic,并将当前分支
|
||||||
|
`feat/godot-logging-compliance-polish` 映射到该主题
|
||||||
|
- 为 `GFramework.Godot.Logging` 新增:
|
||||||
|
- `GodotLog`
|
||||||
|
- `DeferredLogger`
|
||||||
|
- `GodotLogConfigurationSource`
|
||||||
|
- `GodotLoggerSettings`
|
||||||
|
- `GodotLoggerSettingsLoader`
|
||||||
|
- 关键实现决策:
|
||||||
|
- 不把 `Microsoft.Extensions.Logging` 的 builder / provider 生态整套移植进来
|
||||||
|
- 保持 `LoggerFactoryResolver` 与 `ArchitectureConfiguration` 仍是主接线方式
|
||||||
|
- 只吸收 `GodotLogger` 里对 GFramework 现有模型真正有价值的部分:
|
||||||
|
- 配置自动发现
|
||||||
|
- 热重载
|
||||||
|
- 延迟 logger 初始化
|
||||||
|
- 配置命名兼容
|
||||||
|
- 为让热重载作用于已缓存 logger,调整 `AbstractLogger` 支持动态最小级别提供器,并让
|
||||||
|
`GodotLoggerFactoryProvider` / `GodotLogger` 在写入和级别判定时读取最新设置
|
||||||
|
- 为让结构化日志在 Godot 侧不再退化成纯字符串,扩展:
|
||||||
|
- `GodotLogRenderContext`
|
||||||
|
- `GodotLogTemplate`
|
||||||
|
- `GodotLoggerOptions`
|
||||||
|
- `GodotLogger`
|
||||||
|
使默认模板支持 `{properties}`,并将 `IStructuredLogger` / `LogContext` 属性渲染到输出中
|
||||||
|
- 为兼容 `GodotLogger` 原项目配置习惯,在 `GodotLoggerSettingsLoader` 中补充枚举解析兼容:
|
||||||
|
- `Info` / `Information`
|
||||||
|
- `Fatal` / `Critical`
|
||||||
|
- `Warn` / `Warning`
|
||||||
|
- 同步更新 `docs/zh-CN/godot/logging.md`,把文档结论从“只有薄适配层”刷新成“已具备宿主便利层和热重载语义”
|
||||||
|
- 已从 `ai-libs/GodotLogger` 复制 MIT 许可证到 `third-party-licenses/GodotLogger/LICENSE`
|
||||||
|
|
||||||
|
### 验证
|
||||||
|
|
||||||
|
- `dotnet test GFramework.Godot.Tests/GFramework.Godot.Tests.csproj -c Release --filter FullyQualifiedName~GodotLog -nologo`
|
||||||
|
- 结果:通过(11/11)
|
||||||
|
- `dotnet test GFramework.Godot.Tests/GFramework.Godot.Tests.csproj -c Release -nologo`
|
||||||
|
- 结果:通过(69/69)
|
||||||
|
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter FullyQualifiedName~Logging -nologo`
|
||||||
|
- 结果:通过(214/214)
|
||||||
|
|
||||||
|
### 下一步
|
||||||
|
|
||||||
|
1. 若继续推进本主题,优先评估 Godot 输出是否应变成 Core 可组合 appender / sink
|
||||||
|
2. 若出现后续 review 反馈,直接在本 topic 追加 RP-002,而不是重新开临时 local-plan
|
||||||
|
3. 若本主题阶段性完成,再把详细实现 history 迁入 `archive/`,active 入口只保留恢复点与风险
|
||||||
21
third-party-licenses/GodotLogger/LICENSE
Normal file
21
third-party-licenses/GodotLogger/LICENSE
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2025 gamedo
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
Loading…
x
Reference in New Issue
Block a user