mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-05-11 12:14:30 +08:00
feat(godot): add configurable logger templates
This commit is contained in:
parent
6aa741114f
commit
36e1ae5f32
110
GFramework.Godot.Tests/Logging/GodotLogTemplateTests.cs
Normal file
110
GFramework.Godot.Tests/Logging/GodotLogTemplateTests.cs
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
using System;
|
||||||
|
using GFramework.Core.Abstractions.Logging;
|
||||||
|
using GFramework.Godot.Logging;
|
||||||
|
|
||||||
|
namespace GFramework.Godot.Tests.Logging;
|
||||||
|
|
||||||
|
[TestFixture]
|
||||||
|
public sealed class GodotLogTemplateTests
|
||||||
|
{
|
||||||
|
[Test]
|
||||||
|
public void Render_Should_Format_Timestamp_Level_Color_Category_And_Message()
|
||||||
|
{
|
||||||
|
var template = GodotLogTemplate.Parse("[{timestamp:yyyyMMdd}] [color={color}]{level:u3}[/color] [{category:l16}] {message}");
|
||||||
|
var context = new GodotLogRenderContext(
|
||||||
|
new DateTime(2026, 5, 2, 1, 2, 3, DateTimeKind.Utc),
|
||||||
|
LogLevel.Warning,
|
||||||
|
"Game.Services.Inventory",
|
||||||
|
"Loaded",
|
||||||
|
"orange");
|
||||||
|
|
||||||
|
var result = template.Render(context);
|
||||||
|
|
||||||
|
Assert.That(result, Is.EqualTo("[20260502] [color=orange]WRN[/color] [G.S.Inventory ] Loaded"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Render_Should_Support_Lowercase_Level_Format()
|
||||||
|
{
|
||||||
|
var template = GodotLogTemplate.Parse("{level:l3}:{message}");
|
||||||
|
var context = new GodotLogRenderContext(
|
||||||
|
new DateTime(2026, 5, 2, 1, 2, 3, DateTimeKind.Utc),
|
||||||
|
LogLevel.Debug,
|
||||||
|
"Game",
|
||||||
|
"Ready",
|
||||||
|
"cyan");
|
||||||
|
|
||||||
|
var result = template.Render(context);
|
||||||
|
|
||||||
|
Assert.That(result, Is.EqualTo("dbg:Ready"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Render_Should_Right_Align_Category()
|
||||||
|
{
|
||||||
|
var template = GodotLogTemplate.Parse("[{category:r10}]");
|
||||||
|
var context = new GodotLogRenderContext(
|
||||||
|
new DateTime(2026, 5, 2, 1, 2, 3, DateTimeKind.Utc),
|
||||||
|
LogLevel.Info,
|
||||||
|
"UI",
|
||||||
|
"Ready",
|
||||||
|
"white");
|
||||||
|
|
||||||
|
var result = template.Render(context);
|
||||||
|
|
||||||
|
Assert.That(result, Is.EqualTo("[ UI]"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Render_Should_Preserve_Unknown_Placeholders()
|
||||||
|
{
|
||||||
|
var template = GodotLogTemplate.Parse("{message} {unknown}");
|
||||||
|
var context = new GodotLogRenderContext(
|
||||||
|
new DateTime(2026, 5, 2, 1, 2, 3, DateTimeKind.Utc),
|
||||||
|
LogLevel.Info,
|
||||||
|
"Game",
|
||||||
|
"Ready",
|
||||||
|
"white");
|
||||||
|
|
||||||
|
var result = template.Render(context);
|
||||||
|
|
||||||
|
Assert.That(result, Is.EqualTo("Ready {unknown}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Render_Should_Format_Padded_Level()
|
||||||
|
{
|
||||||
|
var template = GodotLogTemplate.Parse("{level:padded}");
|
||||||
|
var context = new GodotLogRenderContext(
|
||||||
|
new DateTime(2026, 5, 2, 1, 2, 3, DateTimeKind.Utc),
|
||||||
|
LogLevel.Info,
|
||||||
|
"Game",
|
||||||
|
"Ready",
|
||||||
|
"white");
|
||||||
|
|
||||||
|
var result = template.Render(context);
|
||||||
|
|
||||||
|
Assert.That(result, Is.EqualTo("INFO "));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Options_ForMinimumLevel_Should_Preserve_Fixed_Minimum_Level()
|
||||||
|
{
|
||||||
|
var options = GodotLoggerOptions.ForMinimumLevel(LogLevel.Warning);
|
||||||
|
|
||||||
|
Assert.That(options.Mode, Is.EqualTo(GodotLoggerMode.Debug));
|
||||||
|
Assert.That(options.DebugMinLevel, Is.EqualTo(LogLevel.Warning));
|
||||||
|
Assert.That(options.ReleaseMinLevel, Is.EqualTo(LogLevel.Warning));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Options_Should_Use_Default_Color_When_Configured_Color_Is_Missing()
|
||||||
|
{
|
||||||
|
var options = new GodotLoggerOptions();
|
||||||
|
options.Colors.Remove(LogLevel.Error);
|
||||||
|
|
||||||
|
var result = options.GetColor(LogLevel.Error);
|
||||||
|
|
||||||
|
Assert.That(result, Is.EqualTo("red"));
|
||||||
|
}
|
||||||
|
}
|
||||||
11
GFramework.Godot/Logging/GodotLogRenderContext.cs
Normal file
11
GFramework.Godot/Logging/GodotLogRenderContext.cs
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
using System;
|
||||||
|
using GFramework.Core.Abstractions.Logging;
|
||||||
|
|
||||||
|
namespace GFramework.Godot.Logging;
|
||||||
|
|
||||||
|
internal readonly record struct GodotLogRenderContext(
|
||||||
|
DateTime Timestamp,
|
||||||
|
LogLevel Level,
|
||||||
|
string Category,
|
||||||
|
string Message,
|
||||||
|
string Color);
|
||||||
226
GFramework.Godot/Logging/GodotLogTemplate.cs
Normal file
226
GFramework.Godot/Logging/GodotLogTemplate.cs
Normal file
@ -0,0 +1,226 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Text;
|
||||||
|
using GFramework.Core.Abstractions.Logging;
|
||||||
|
|
||||||
|
namespace GFramework.Godot.Logging;
|
||||||
|
|
||||||
|
internal sealed class GodotLogTemplate
|
||||||
|
{
|
||||||
|
private static readonly ConcurrentDictionary<string, GodotLogTemplate> Cache = new(StringComparer.Ordinal);
|
||||||
|
|
||||||
|
private readonly ConcurrentDictionary<string, string> _categoryCache = new(StringComparer.Ordinal);
|
||||||
|
private readonly int _literalLength;
|
||||||
|
private readonly Action<StringBuilder, GodotLogRenderContext>[] _segments;
|
||||||
|
|
||||||
|
private GodotLogTemplate(string template)
|
||||||
|
{
|
||||||
|
(_segments, _literalLength) = ParseCore(template);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static GodotLogTemplate Parse(string template)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(template);
|
||||||
|
return Cache.GetOrAdd(template, static value => new GodotLogTemplate(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Render(GodotLogRenderContext context)
|
||||||
|
{
|
||||||
|
var builder = new StringBuilder(_literalLength + context.Category.Length + context.Message.Length + 48);
|
||||||
|
foreach (var segment in _segments)
|
||||||
|
{
|
||||||
|
segment(builder, context);
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private (Action<StringBuilder, GodotLogRenderContext>[] Segments, int LiteralLength) ParseCore(string template)
|
||||||
|
{
|
||||||
|
var segments = new List<Action<StringBuilder, GodotLogRenderContext>>();
|
||||||
|
var literalLength = 0;
|
||||||
|
var position = 0;
|
||||||
|
|
||||||
|
while (position < template.Length)
|
||||||
|
{
|
||||||
|
var open = template.IndexOf('{', position);
|
||||||
|
if (open < 0)
|
||||||
|
{
|
||||||
|
AddLiteral(template[position..]);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (open > position)
|
||||||
|
{
|
||||||
|
AddLiteral(template[position..open]);
|
||||||
|
}
|
||||||
|
|
||||||
|
var close = template.IndexOf('}', open + 1);
|
||||||
|
if (close < 0)
|
||||||
|
{
|
||||||
|
AddLiteral(template[open..]);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
var key = template.Substring(open + 1, close - open - 1);
|
||||||
|
segments.Add(CreateSegment(key));
|
||||||
|
position = close + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ([.. segments], literalLength);
|
||||||
|
|
||||||
|
void AddLiteral(string literal)
|
||||||
|
{
|
||||||
|
if (literal.Length == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
literalLength += literal.Length;
|
||||||
|
segments.Add((builder, _) => builder.Append(literal));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Action<StringBuilder, GodotLogRenderContext> CreateSegment(string key)
|
||||||
|
{
|
||||||
|
return key switch
|
||||||
|
{
|
||||||
|
"category" => static (builder, context) => builder.Append(context.Category),
|
||||||
|
"color" => static (builder, context) => builder.Append(context.Color),
|
||||||
|
"level" => static (builder, context) => builder.Append(context.Level),
|
||||||
|
"message" => static (builder, context) => builder.Append(context.Message),
|
||||||
|
"timestamp" => static (builder, context) => builder.Append(context.Timestamp.ToString(
|
||||||
|
"yyyy-MM-dd HH:mm:ss.fff",
|
||||||
|
CultureInfo.InvariantCulture)),
|
||||||
|
not null when key.StartsWith("category:", StringComparison.Ordinal) => CreateCategorySegment(key[9..]),
|
||||||
|
not null when key.StartsWith("level:", StringComparison.Ordinal) => CreateLevelSegment(key[6..]),
|
||||||
|
not null when key.StartsWith("timestamp:", StringComparison.Ordinal) => CreateTimestampSegment(key[10..]),
|
||||||
|
_ => (builder, _) => builder.Append('{').Append(key).Append('}')
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private Action<StringBuilder, GodotLogRenderContext> CreateTimestampSegment(string format)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(format))
|
||||||
|
{
|
||||||
|
return static (builder, context) => builder.Append(context.Timestamp.ToString(
|
||||||
|
"yyyy-MM-dd HH:mm:ss.fff",
|
||||||
|
CultureInfo.InvariantCulture));
|
||||||
|
}
|
||||||
|
|
||||||
|
return (builder, context) => builder.Append(context.Timestamp.ToString(format, CultureInfo.InvariantCulture));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Action<StringBuilder, GodotLogRenderContext> CreateLevelSegment(string format)
|
||||||
|
{
|
||||||
|
return format switch
|
||||||
|
{
|
||||||
|
"u3" or "U3" => static (builder, context) => builder.Append(ToShortLevel(context.Level, upper: true)),
|
||||||
|
"l3" or "L3" => static (builder, context) => builder.Append(ToShortLevel(context.Level, upper: false)),
|
||||||
|
"padded" or "Padded" => static (builder, context) => builder.Append(ToPaddedLevel(context.Level)),
|
||||||
|
_ => static (builder, context) => builder.Append(context.Level)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private Action<StringBuilder, GodotLogRenderContext> CreateCategorySegment(string format)
|
||||||
|
{
|
||||||
|
if (format.Length < 2)
|
||||||
|
{
|
||||||
|
return static (builder, context) => builder.Append(context.Category);
|
||||||
|
}
|
||||||
|
|
||||||
|
var alignment = format[0];
|
||||||
|
if (alignment is not 'l' and not 'r')
|
||||||
|
{
|
||||||
|
return static (builder, context) => builder.Append(context.Category);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!int.TryParse(format[1..], NumberStyles.None, CultureInfo.InvariantCulture, out var width) || width <= 0)
|
||||||
|
{
|
||||||
|
return static (builder, context) => builder.Append(context.Category);
|
||||||
|
}
|
||||||
|
|
||||||
|
return alignment == 'l'
|
||||||
|
? (builder, context) => builder.Append(GetFormattedCategory(context.Category, format, width, padLeft: false))
|
||||||
|
: (builder, context) => builder.Append(GetFormattedCategory(context.Category, format, width, padLeft: true));
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetFormattedCategory(string category, string format, int width, bool padLeft)
|
||||||
|
{
|
||||||
|
var cacheKey = string.Concat(format, "\0", category);
|
||||||
|
return _categoryCache.GetOrAdd(cacheKey, _ =>
|
||||||
|
{
|
||||||
|
var abbreviated = AbbreviateCategory(category, width);
|
||||||
|
return padLeft ? abbreviated.PadLeft(width) : abbreviated.PadRight(width);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string AbbreviateCategory(string category, int maxLength)
|
||||||
|
{
|
||||||
|
if (category.Length <= maxLength)
|
||||||
|
{
|
||||||
|
return category;
|
||||||
|
}
|
||||||
|
|
||||||
|
var parts = category.Split('.');
|
||||||
|
if (parts.Length == 1)
|
||||||
|
{
|
||||||
|
return category[..maxLength];
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var i = 0; i < parts.Length - 1; i++)
|
||||||
|
{
|
||||||
|
if (parts[i].Length > 1)
|
||||||
|
{
|
||||||
|
parts[i] = parts[i][..1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var start = 0;
|
||||||
|
while (start < parts.Length - 1)
|
||||||
|
{
|
||||||
|
var joined = string.Join(".", parts, start, parts.Length - start);
|
||||||
|
if (joined.Length <= maxLength)
|
||||||
|
{
|
||||||
|
return joined;
|
||||||
|
}
|
||||||
|
|
||||||
|
start++;
|
||||||
|
}
|
||||||
|
|
||||||
|
var last = parts[^1];
|
||||||
|
return last.Length > maxLength ? last[..maxLength] : last;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ToShortLevel(LogLevel level, bool upper)
|
||||||
|
{
|
||||||
|
var value = level switch
|
||||||
|
{
|
||||||
|
LogLevel.Trace => "trc",
|
||||||
|
LogLevel.Debug => "dbg",
|
||||||
|
LogLevel.Info => "inf",
|
||||||
|
LogLevel.Warning => "wrn",
|
||||||
|
LogLevel.Error => "err",
|
||||||
|
LogLevel.Fatal => "ftl",
|
||||||
|
_ => "unk"
|
||||||
|
};
|
||||||
|
|
||||||
|
return upper ? value.ToUpperInvariant() : value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ToPaddedLevel(LogLevel level)
|
||||||
|
{
|
||||||
|
return level switch
|
||||||
|
{
|
||||||
|
LogLevel.Trace => "TRACE ",
|
||||||
|
LogLevel.Debug => "DEBUG ",
|
||||||
|
LogLevel.Info => "INFO ",
|
||||||
|
LogLevel.Warning => "WARNING",
|
||||||
|
LogLevel.Error => "ERROR ",
|
||||||
|
LogLevel.Fatal => "FATAL ",
|
||||||
|
_ => level.ToString()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,4 +1,4 @@
|
|||||||
using System.Globalization;
|
using System;
|
||||||
using GFramework.Core.Abstractions.Logging;
|
using GFramework.Core.Abstractions.Logging;
|
||||||
using GFramework.Core.Logging;
|
using GFramework.Core.Logging;
|
||||||
using Godot;
|
using Godot;
|
||||||
@ -6,69 +6,79 @@ using Godot;
|
|||||||
namespace GFramework.Godot.Logging;
|
namespace GFramework.Godot.Logging;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Godot平台的日志记录器实现。
|
/// Godot platform logger implementation.
|
||||||
/// 该类继承自 <see cref="AbstractLogger"/>,用于在 Godot 引擎中输出日志信息。
|
|
||||||
/// 支持不同日志级别的输出,并根据级别调用 Godot 的相应方法。
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="name">日志记录器的名称,默认为根日志记录器名称。</param>
|
public sealed class GodotLogger : AbstractLogger
|
||||||
/// <param name="minLevel">最低日志级别,默认为 <see cref="LogLevel.Info"/>。</param>
|
|
||||||
public sealed class GodotLogger(
|
|
||||||
string? name = null,
|
|
||||||
LogLevel minLevel = LogLevel.Info) : AbstractLogger(name ?? RootLoggerName, minLevel)
|
|
||||||
{
|
{
|
||||||
// 静态缓存日志级别字符串,避免重复格式化
|
private readonly GodotLoggerOptions _options;
|
||||||
private static readonly string[] LevelStrings =
|
|
||||||
[
|
|
||||||
"TRACE ",
|
|
||||||
"DEBUG ",
|
|
||||||
"INFO ",
|
|
||||||
"WARNING",
|
|
||||||
"ERROR ",
|
|
||||||
"FATAL "
|
|
||||||
];
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 写入日志的核心方法。
|
/// Initializes a logger that preserves the historical fixed-format template.
|
||||||
/// 格式化日志消息并根据日志级别调用 Godot 的输出方法。
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="level">日志级别。</param>
|
/// <param name="name">The logger name.</param>
|
||||||
/// <param name="message">日志消息内容。</param>
|
/// <param name="minLevel">The minimum enabled log level.</param>
|
||||||
/// <param name="exception">可选的异常信息。</param>
|
public GodotLogger(string? name = null, LogLevel minLevel = LogLevel.Info)
|
||||||
|
: this(name, GodotLoggerOptions.ForMinimumLevel(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)
|
||||||
|
: base(name ?? RootLoggerName, (options ?? throw new ArgumentNullException(nameof(options))).GetEffectiveMinLevel())
|
||||||
|
{
|
||||||
|
_options = options;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <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)
|
protected override void Write(LogLevel level, string message, Exception? exception)
|
||||||
{
|
{
|
||||||
// 构造时间戳和日志前缀
|
var templateText = _options.Mode == GodotLoggerMode.Debug
|
||||||
var timestamp = DateTime.UtcNow.ToString("yyyy-MM-dd HH:mm:ss.fff", CultureInfo.InvariantCulture);
|
? _options.DebugOutputTemplate
|
||||||
var levelStr = LevelStrings[(int)level];
|
: _options.ReleaseOutputTemplate;
|
||||||
var logPrefix = $"[{timestamp}] {levelStr} [{Name()}]";
|
var context = new GodotLogRenderContext(
|
||||||
|
DateTime.UtcNow,
|
||||||
|
level,
|
||||||
|
Name(),
|
||||||
|
message,
|
||||||
|
_options.GetColor(level));
|
||||||
|
var rendered = GodotLogTemplate.Parse(templateText).Render(context);
|
||||||
|
|
||||||
// 添加异常信息到日志消息中
|
if (_options.Mode == GodotLoggerMode.Debug)
|
||||||
if (exception != null) message += "\n" + exception;
|
{
|
||||||
|
WriteDebug(level, rendered);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
GD.Print(rendered);
|
||||||
|
}
|
||||||
|
|
||||||
var logMessage = $"{logPrefix} {message}";
|
if (exception != null)
|
||||||
|
{
|
||||||
|
GD.PrintErr(exception.ToString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void WriteDebug(LogLevel level, string rendered)
|
||||||
|
{
|
||||||
|
GD.PrintRich(rendered);
|
||||||
|
|
||||||
// 根据日志级别选择 Godot 输出方法
|
|
||||||
switch (level)
|
switch (level)
|
||||||
{
|
{
|
||||||
case LogLevel.Fatal:
|
case LogLevel.Fatal:
|
||||||
GD.PushError(logMessage);
|
|
||||||
break;
|
|
||||||
case LogLevel.Error:
|
case LogLevel.Error:
|
||||||
GD.PrintErr(logMessage);
|
GD.PushError(rendered);
|
||||||
break;
|
break;
|
||||||
case LogLevel.Warning:
|
case LogLevel.Warning:
|
||||||
GD.PushWarning(logMessage);
|
GD.PushWarning(rendered);
|
||||||
break;
|
|
||||||
case LogLevel.Trace:
|
|
||||||
GD.PrintRich($"[color=gray]{logMessage}[/color]");
|
|
||||||
break;
|
|
||||||
case LogLevel.Debug:
|
|
||||||
GD.PrintRich($"[color=cyan]{logMessage}[/color]");
|
|
||||||
break;
|
|
||||||
case LogLevel.Info:
|
|
||||||
GD.Print(logMessage);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
GD.Print(logMessage);
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,20 +1,46 @@
|
|||||||
using GFramework.Core.Abstractions.Logging;
|
using System;
|
||||||
|
using GFramework.Core.Abstractions.Logging;
|
||||||
|
|
||||||
namespace GFramework.Godot.Logging;
|
namespace GFramework.Godot.Logging;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Godot日志工厂类,用于创建Godot平台专用的日志记录器实例
|
/// Creates Godot platform logger instances.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class GodotLoggerFactory : ILoggerFactory
|
public sealed class GodotLoggerFactory : ILoggerFactory
|
||||||
{
|
{
|
||||||
|
private readonly GodotLoggerOptions? _options;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 获取指定名称的日志记录器实例
|
/// Initializes a factory that preserves the historical fixed-format logger behavior.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="name">日志记录器的名称</param>
|
public GodotLoggerFactory()
|
||||||
/// <param name="minLevel">日志记录器的最小日志级别</param>
|
{
|
||||||
/// <returns>返回GodotLogger类型的日志记录器实例</returns>
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a factory with Godot-specific formatting options.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="options">The logger options.</param>
|
||||||
|
public GodotLoggerFactory(GodotLoggerOptions options)
|
||||||
|
{
|
||||||
|
_options = options ?? throw new ArgumentNullException(nameof(options));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a logger with the specified name.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="name">The logger name.</param>
|
||||||
|
/// <param name="minLevel">The minimum enabled level.</param>
|
||||||
|
/// <returns>A Godot logger instance.</returns>
|
||||||
public ILogger GetLogger(string name, LogLevel minLevel = LogLevel.Info)
|
public ILogger GetLogger(string name, LogLevel minLevel = LogLevel.Info)
|
||||||
{
|
{
|
||||||
return new GodotLogger(name, minLevel);
|
ArgumentNullException.ThrowIfNull(name);
|
||||||
|
|
||||||
|
if (_options == null)
|
||||||
|
{
|
||||||
|
return new GodotLogger(name, minLevel);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new GodotLogger(name, _options.WithMinimumLevelFloor(minLevel));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,35 +1,51 @@
|
|||||||
using GFramework.Core.Abstractions.Logging;
|
using System;
|
||||||
|
using GFramework.Core.Abstractions.Logging;
|
||||||
using GFramework.Core.Logging;
|
using GFramework.Core.Logging;
|
||||||
|
|
||||||
namespace GFramework.Godot.Logging;
|
namespace GFramework.Godot.Logging;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Godot日志工厂提供程序,用于创建Godot日志记录器实例
|
/// Provides cached Godot logger instances.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class GodotLoggerFactoryProvider : ILoggerFactoryProvider
|
public sealed class GodotLoggerFactoryProvider : ILoggerFactoryProvider
|
||||||
{
|
{
|
||||||
private readonly ILoggerFactory _cachedFactory;
|
private readonly ILoggerFactory _cachedFactory;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 初始化Godot日志记录器工厂提供程序
|
/// Initializes a Godot logger provider with the default logger factory.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public GodotLoggerFactoryProvider()
|
public GodotLoggerFactoryProvider()
|
||||||
{
|
{
|
||||||
_cachedFactory = new CachedLoggerFactory(new GodotLoggerFactory());
|
_cachedFactory = CreateCachedFactory(new GodotLoggerFactory());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 获取或设置最小日志级别
|
/// Initializes a Godot logger provider with Godot-specific formatting options.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="options">The logger options.</param>
|
||||||
|
public GodotLoggerFactoryProvider(GodotLoggerOptions options)
|
||||||
|
{
|
||||||
|
_cachedFactory = CreateCachedFactory(new GodotLoggerFactory(options));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the provider minimum level.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public LogLevel MinLevel { get; set; }
|
public LogLevel MinLevel { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 创建指定名称的日志记录器实例(带缓存)
|
/// Creates a cached logger with the specified name.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="name">日志记录器的名称</param>
|
/// <param name="name">The logger name.</param>
|
||||||
/// <returns>返回配置了最小日志级别的Godot日志记录器实例</returns>
|
/// <returns>A logger configured with <see cref="MinLevel"/>.</returns>
|
||||||
public ILogger CreateLogger(string name)
|
public ILogger CreateLogger(string name)
|
||||||
{
|
{
|
||||||
return _cachedFactory.GetLogger(name, MinLevel);
|
return _cachedFactory.GetLogger(name, MinLevel);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
private static ILoggerFactory CreateCachedFactory(ILoggerFactory innerFactory)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(innerFactory);
|
||||||
|
return new CachedLoggerFactory(innerFactory);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
17
GFramework.Godot/Logging/GodotLoggerMode.cs
Normal file
17
GFramework.Godot/Logging/GodotLoggerMode.cs
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
namespace GFramework.Godot.Logging;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Selects the Godot logger output behavior.
|
||||||
|
/// </summary>
|
||||||
|
public enum GodotLoggerMode
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Uses rich BBCode console output and mirrors warnings/errors to the Godot debugger panel.
|
||||||
|
/// </summary>
|
||||||
|
Debug,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Uses plain console output without rich text or debugger panel mirroring.
|
||||||
|
/// </summary>
|
||||||
|
Release
|
||||||
|
}
|
||||||
115
GFramework.Godot/Logging/GodotLoggerOptions.cs
Normal file
115
GFramework.Godot/Logging/GodotLoggerOptions.cs
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using GFramework.Core.Abstractions.Logging;
|
||||||
|
|
||||||
|
namespace GFramework.Godot.Logging;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Godot logger formatting and routing options.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class GodotLoggerOptions
|
||||||
|
{
|
||||||
|
private static readonly IReadOnlyDictionary<LogLevel, string> DefaultColors = new Dictionary<LogLevel, string>
|
||||||
|
{
|
||||||
|
[LogLevel.Trace] = "gray",
|
||||||
|
[LogLevel.Debug] = "cyan",
|
||||||
|
[LogLevel.Info] = "white",
|
||||||
|
[LogLevel.Warning] = "orange",
|
||||||
|
[LogLevel.Error] = "red",
|
||||||
|
[LogLevel.Fatal] = "deep_pink"
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the output mode.
|
||||||
|
/// </summary>
|
||||||
|
public GodotLoggerMode Mode { get; set; } = GodotLoggerMode.Debug;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the minimum level used by <see cref="GodotLoggerMode.Debug"/>.
|
||||||
|
/// </summary>
|
||||||
|
public LogLevel DebugMinLevel { get; set; } = LogLevel.Debug;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the minimum level used by <see cref="GodotLoggerMode.Release"/>.
|
||||||
|
/// </summary>
|
||||||
|
public LogLevel ReleaseMinLevel { get; set; } = LogLevel.Info;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the BBCode-capable template used by <see cref="GodotLoggerMode.Debug"/>.
|
||||||
|
/// </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}";
|
||||||
|
#pragma warning restore MA0016
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the plain text template used by <see cref="GodotLoggerMode.Release"/>.
|
||||||
|
/// </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}";
|
||||||
|
#pragma warning restore MA0016
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets Godot named colors by log level.
|
||||||
|
/// </summary>
|
||||||
|
#pragma warning disable MA0016 // Keep configuration mutable for object initializer and serializer scenarios.
|
||||||
|
public Dictionary<LogLevel, string> Colors { get; set; } = new(DefaultColors);
|
||||||
|
#pragma warning restore MA0016
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates options that preserve the previous Godot logger defaults for a fixed minimum level.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="minLevel">The minimum enabled level.</param>
|
||||||
|
/// <returns>Options equivalent to the previous fixed-format logger behavior.</returns>
|
||||||
|
public static GodotLoggerOptions ForMinimumLevel(LogLevel minLevel)
|
||||||
|
{
|
||||||
|
return new 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}",
|
||||||
|
Colors = new Dictionary<LogLevel, string>(DefaultColors)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the configured color for the specified level.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="level">The level.</param>
|
||||||
|
/// <returns>The Godot named color.</returns>
|
||||||
|
public string GetColor(LogLevel level)
|
||||||
|
{
|
||||||
|
if (Colors.TryGetValue(level, out var color) && !string.IsNullOrWhiteSpace(color))
|
||||||
|
{
|
||||||
|
return color;
|
||||||
|
}
|
||||||
|
|
||||||
|
return DefaultColors[level];
|
||||||
|
}
|
||||||
|
|
||||||
|
internal LogLevel GetEffectiveMinLevel()
|
||||||
|
{
|
||||||
|
return Mode == GodotLoggerMode.Debug ? DebugMinLevel : ReleaseMinLevel;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal GodotLoggerOptions WithMinimumLevelFloor(LogLevel minLevel)
|
||||||
|
{
|
||||||
|
return new GodotLoggerOptions
|
||||||
|
{
|
||||||
|
Mode = Mode,
|
||||||
|
DebugMinLevel = Max(DebugMinLevel, minLevel),
|
||||||
|
ReleaseMinLevel = Max(ReleaseMinLevel, minLevel),
|
||||||
|
DebugOutputTemplate = DebugOutputTemplate,
|
||||||
|
ReleaseOutputTemplate = ReleaseOutputTemplate,
|
||||||
|
Colors = new Dictionary<LogLevel, string>(Colors)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static LogLevel Max(LogLevel left, LogLevel right)
|
||||||
|
{
|
||||||
|
return left > right ? left : right;
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user