mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-05-06 16:16:44 +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.Logging;
|
||||
using Godot;
|
||||
@ -6,69 +6,79 @@ using Godot;
|
||||
namespace GFramework.Godot.Logging;
|
||||
|
||||
/// <summary>
|
||||
/// Godot平台的日志记录器实现。
|
||||
/// 该类继承自 <see cref="AbstractLogger"/>,用于在 Godot 引擎中输出日志信息。
|
||||
/// 支持不同日志级别的输出,并根据级别调用 Godot 的相应方法。
|
||||
/// Godot platform logger implementation.
|
||||
/// </summary>
|
||||
/// <param name="name">日志记录器的名称,默认为根日志记录器名称。</param>
|
||||
/// <param name="minLevel">最低日志级别,默认为 <see cref="LogLevel.Info"/>。</param>
|
||||
public sealed class GodotLogger(
|
||||
string? name = null,
|
||||
LogLevel minLevel = LogLevel.Info) : AbstractLogger(name ?? RootLoggerName, minLevel)
|
||||
public sealed class GodotLogger : AbstractLogger
|
||||
{
|
||||
// 静态缓存日志级别字符串,避免重复格式化
|
||||
private static readonly string[] LevelStrings =
|
||||
[
|
||||
"TRACE ",
|
||||
"DEBUG ",
|
||||
"INFO ",
|
||||
"WARNING",
|
||||
"ERROR ",
|
||||
"FATAL "
|
||||
];
|
||||
private readonly GodotLoggerOptions _options;
|
||||
|
||||
/// <summary>
|
||||
/// 写入日志的核心方法。
|
||||
/// 格式化日志消息并根据日志级别调用 Godot 的输出方法。
|
||||
/// Initializes a logger that preserves the historical fixed-format template.
|
||||
/// </summary>
|
||||
/// <param name="level">日志级别。</param>
|
||||
/// <param name="message">日志消息内容。</param>
|
||||
/// <param name="exception">可选的异常信息。</param>
|
||||
/// <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))
|
||||
{
|
||||
}
|
||||
|
||||
/// <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)
|
||||
{
|
||||
// 构造时间戳和日志前缀
|
||||
var timestamp = DateTime.UtcNow.ToString("yyyy-MM-dd HH:mm:ss.fff", CultureInfo.InvariantCulture);
|
||||
var levelStr = LevelStrings[(int)level];
|
||||
var logPrefix = $"[{timestamp}] {levelStr} [{Name()}]";
|
||||
var templateText = _options.Mode == GodotLoggerMode.Debug
|
||||
? _options.DebugOutputTemplate
|
||||
: _options.ReleaseOutputTemplate;
|
||||
var context = new GodotLogRenderContext(
|
||||
DateTime.UtcNow,
|
||||
level,
|
||||
Name(),
|
||||
message,
|
||||
_options.GetColor(level));
|
||||
var rendered = GodotLogTemplate.Parse(templateText).Render(context);
|
||||
|
||||
// 添加异常信息到日志消息中
|
||||
if (exception != null) message += "\n" + exception;
|
||||
if (_options.Mode == GodotLoggerMode.Debug)
|
||||
{
|
||||
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)
|
||||
{
|
||||
case LogLevel.Fatal:
|
||||
GD.PushError(logMessage);
|
||||
break;
|
||||
case LogLevel.Error:
|
||||
GD.PrintErr(logMessage);
|
||||
GD.PushError(rendered);
|
||||
break;
|
||||
case LogLevel.Warning:
|
||||
GD.PushWarning(logMessage);
|
||||
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);
|
||||
GD.PushWarning(rendered);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,20 +1,46 @@
|
||||
using GFramework.Core.Abstractions.Logging;
|
||||
using System;
|
||||
using GFramework.Core.Abstractions.Logging;
|
||||
|
||||
namespace GFramework.Godot.Logging;
|
||||
|
||||
/// <summary>
|
||||
/// Godot日志工厂类,用于创建Godot平台专用的日志记录器实例
|
||||
/// Creates Godot platform logger instances.
|
||||
/// </summary>
|
||||
public class GodotLoggerFactory : ILoggerFactory
|
||||
public sealed class GodotLoggerFactory : ILoggerFactory
|
||||
{
|
||||
private readonly GodotLoggerOptions? _options;
|
||||
|
||||
/// <summary>
|
||||
/// 获取指定名称的日志记录器实例
|
||||
/// Initializes a factory that preserves the historical fixed-format logger behavior.
|
||||
/// </summary>
|
||||
/// <param name="name">日志记录器的名称</param>
|
||||
/// <param name="minLevel">日志记录器的最小日志级别</param>
|
||||
/// <returns>返回GodotLogger类型的日志记录器实例</returns>
|
||||
public GodotLoggerFactory()
|
||||
{
|
||||
}
|
||||
|
||||
/// <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)
|
||||
{
|
||||
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;
|
||||
|
||||
namespace GFramework.Godot.Logging;
|
||||
|
||||
/// <summary>
|
||||
/// Godot日志工厂提供程序,用于创建Godot日志记录器实例
|
||||
/// Provides cached Godot logger instances.
|
||||
/// </summary>
|
||||
public sealed class GodotLoggerFactoryProvider : ILoggerFactoryProvider
|
||||
{
|
||||
private readonly ILoggerFactory _cachedFactory;
|
||||
|
||||
/// <summary>
|
||||
/// 初始化Godot日志记录器工厂提供程序
|
||||
/// Initializes a Godot logger provider with the default logger factory.
|
||||
/// </summary>
|
||||
public GodotLoggerFactoryProvider()
|
||||
{
|
||||
_cachedFactory = new CachedLoggerFactory(new GodotLoggerFactory());
|
||||
_cachedFactory = CreateCachedFactory(new GodotLoggerFactory());
|
||||
}
|
||||
|
||||
/// <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>
|
||||
public LogLevel MinLevel { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 创建指定名称的日志记录器实例(带缓存)
|
||||
/// Creates a cached logger with the specified name.
|
||||
/// </summary>
|
||||
/// <param name="name">日志记录器的名称</param>
|
||||
/// <returns>返回配置了最小日志级别的Godot日志记录器实例</returns>
|
||||
/// <param name="name">The logger name.</param>
|
||||
/// <returns>A logger configured with <see cref="MinLevel"/>.</returns>
|
||||
public ILogger CreateLogger(string name)
|
||||
{
|
||||
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