mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-05-06 16:16:44 +08:00
- 修复 DeferredLogger 格式化重载提前 string.Format 的热路径问题 - 修复 GodotLogger 默认 options 分配与结构化属性无效 key 处理 - 补充 Godot logging XML 文档、回归测试和 appsettings 接入示例 - 更新 Godot logging PR review 跟踪与验证记录
225 lines
7.0 KiB
C#
225 lines
7.0 KiB
C#
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>
|
|
public sealed class GodotLogger : AbstractLogger
|
|
{
|
|
private readonly Func<GodotLoggerOptions> _optionsProvider;
|
|
|
|
/// <summary>
|
|
/// Initializes a logger that preserves the historical fixed-format template.
|
|
/// </summary>
|
|
/// <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 ?? RootLoggerName,
|
|
CreateFixedOptionsProvider(minLevel),
|
|
() => minLevel)
|
|
{
|
|
}
|
|
|
|
/// <summary>
|
|
/// Initializes a logger with Godot-specific formatting options.
|
|
/// </summary>
|
|
/// <param name="name">The logger name.</param>
|
|
/// <param name="options">The logger options.</param>
|
|
public GodotLogger(string? name, GodotLoggerOptions options)
|
|
: this(
|
|
name ?? RootLoggerName,
|
|
CreateOptionsProvider(options),
|
|
CreateMinLevelProvider(options))
|
|
{
|
|
}
|
|
|
|
/// <summary>
|
|
/// Initializes the core logger with dynamic options and level providers.
|
|
/// </summary>
|
|
/// <param name="name">The resolved logger name used in rendered output.</param>
|
|
/// <param name="optionsProvider">The provider that supplies the latest rendering options for each write.</param>
|
|
/// <param name="minLevelProvider">The provider that supplies the latest effective minimum level.</param>
|
|
/// <remarks>
|
|
/// The Godot factory uses this constructor so cached logger instances can observe hot-reloaded settings without
|
|
/// being recreated. The default public constructor supplies a fixed provider to avoid allocation on the log path.
|
|
/// </remarks>
|
|
internal GodotLogger(
|
|
string name,
|
|
Func<GodotLoggerOptions> optionsProvider,
|
|
Func<LogLevel> minLevelProvider)
|
|
: base(name, minLevelProvider ?? throw new ArgumentNullException(nameof(minLevelProvider)))
|
|
{
|
|
_optionsProvider = optionsProvider ?? throw new ArgumentNullException(nameof(optionsProvider));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Writes a log entry to Godot.
|
|
/// </summary>
|
|
/// <param name="level">The log level.</param>
|
|
/// <param name="message">The rendered message body.</param>
|
|
/// <param name="exception">The optional exception.</param>
|
|
protected override void Write(LogLevel level, string message, Exception? exception)
|
|
{
|
|
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 (!IsEnabled(level))
|
|
{
|
|
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 (!IsEnabled(level))
|
|
{
|
|
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),
|
|
FormatProperties(properties));
|
|
var rendered = GodotLogTemplate.Parse(templateText).Render(context);
|
|
|
|
if (options.Mode == GodotLoggerMode.Debug)
|
|
{
|
|
WriteDebug(level, rendered);
|
|
}
|
|
else
|
|
{
|
|
GD.Print(rendered);
|
|
}
|
|
|
|
if (exception != null)
|
|
{
|
|
GD.PrintErr(exception.ToString());
|
|
}
|
|
}
|
|
|
|
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)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(property.Key))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
merged[property.Key.Trim()] = property.Value;
|
|
}
|
|
}
|
|
|
|
return merged;
|
|
}
|
|
|
|
private static readonly IReadOnlyDictionary<string, object?> EmptyProperties =
|
|
new Dictionary<string, object?>(StringComparer.Ordinal);
|
|
|
|
private static Func<GodotLoggerOptions> CreateFixedOptionsProvider(LogLevel minLevel)
|
|
{
|
|
var options = GodotLoggerOptions.ForMinimumLevel(minLevel);
|
|
return () => options;
|
|
}
|
|
|
|
private static Func<GodotLoggerOptions> CreateOptionsProvider(GodotLoggerOptions options)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(options);
|
|
return () => options;
|
|
}
|
|
|
|
private static Func<LogLevel> CreateMinLevelProvider(GodotLoggerOptions options)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(options);
|
|
return () => options.GetEffectiveMinLevel();
|
|
}
|
|
|
|
private static string FormatValue(object? value)
|
|
{
|
|
if (value == null)
|
|
{
|
|
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;
|
|
}
|
|
}
|
|
}
|