feat(logging): 实现结构化日志记录和异步日志输出功能

- 将 AbstractLogger 实现从 ILogger 扩展为 IStructuredLogger 接口
- 添加通用日志方法 Log(LogLevel, string, params object[]) 支持格式化参数
- 实现结构化日志方法支持属性键值对记录
- 添加 ConsoleAppender、FileAppender 和 AsyncLogAppender 日志输出器
- 实现 CompositeFilter 过滤器和 DefaultLogFormatter、JsonLogFormatter 格式化器
- 在 ConsoleLogger 和 GodotLogger 中使用预缓存的日志级别字符串提升性能
- 使用 ANSI 颜色代码替代 ConsoleColor 实现跨平台日志着色
- 在 ConsoleLoggerFactoryProvider 和 GodotLoggerFactoryProvider 中添加日志工厂缓存
- 优化 FileStorage 中目录遍历使用 OfType<string>() 类型转换
- 添加 LogContext 支持异步流中的结构化属性传递
This commit is contained in:
GeWuYou 2026-02-26 16:33:33 +08:00 committed by gewuyou
parent 445513b784
commit 1ba771e13a
28 changed files with 1648 additions and 47 deletions

View File

@ -0,0 +1,18 @@
namespace GFramework.Core.Abstractions.logging;
/// <summary>
/// 日志输出器接口,负责将日志条目写入特定目标
/// </summary>
public interface ILogAppender
{
/// <summary>
/// 追加日志条目
/// </summary>
/// <param name="entry">日志条目</param>
void Append(LogEntry entry);
/// <summary>
/// 刷新缓冲区,确保所有日志已写入
/// </summary>
void Flush();
}

View File

@ -0,0 +1,14 @@
namespace GFramework.Core.Abstractions.logging;
/// <summary>
/// 日志过滤器接口,用于决定是否应该记录某条日志
/// </summary>
public interface ILogFilter
{
/// <summary>
/// 判断是否应该记录该日志条目
/// </summary>
/// <param name="entry">日志条目</param>
/// <returns>如果应该记录返回 true否则返回 false</returns>
bool ShouldLog(LogEntry entry);
}

View File

@ -0,0 +1,14 @@
namespace GFramework.Core.Abstractions.logging;
/// <summary>
/// 日志格式化器接口,用于将日志条目格式化为字符串
/// </summary>
public interface ILogFormatter
{
/// <summary>
/// 将日志条目格式化为字符串
/// </summary>
/// <param name="entry">日志条目</param>
/// <returns>格式化后的日志字符串</returns>
string Format(LogEntry entry);
}

View File

@ -309,4 +309,48 @@ public interface ILogger
void Fatal(string msg, Exception t);
#endregion
#region Generic Log Methods
/// <summary>
/// 使用指定的日志级别记录消息
/// </summary>
/// <param name="level">日志级别</param>
/// <param name="message">要记录的消息字符串</param>
void Log(LogLevel level, string message);
/// <summary>
/// 使用指定的日志级别根据格式和参数记录消息
/// </summary>
/// <param name="level">日志级别</param>
/// <param name="format">格式字符串</param>
/// <param name="arg">参数</param>
void Log(LogLevel level, string format, object arg);
/// <summary>
/// 使用指定的日志级别根据格式和参数记录消息
/// </summary>
/// <param name="level">日志级别</param>
/// <param name="format">格式字符串</param>
/// <param name="arg1">第一个参数</param>
/// <param name="arg2">第二个参数</param>
void Log(LogLevel level, string format, object arg1, object arg2);
/// <summary>
/// 使用指定的日志级别根据格式和参数数组记录消息
/// </summary>
/// <param name="level">日志级别</param>
/// <param name="format">格式字符串</param>
/// <param name="arguments">参数数组</param>
void Log(LogLevel level, string format, params object[] arguments);
/// <summary>
/// 使用指定的日志级别记录消息和异常
/// </summary>
/// <param name="level">日志级别</param>
/// <param name="message">伴随异常的消息</param>
/// <param name="exception">要记录的异常</param>
void Log(LogLevel level, string message, Exception exception);
#endregion
}

View File

@ -0,0 +1,24 @@
namespace GFramework.Core.Abstractions.logging;
/// <summary>
/// 支持结构化日志的日志记录器接口
/// </summary>
public interface IStructuredLogger : ILogger
{
/// <summary>
/// 使用指定的日志级别记录消息和结构化属性
/// </summary>
/// <param name="level">日志级别</param>
/// <param name="message">日志消息</param>
/// <param name="properties">结构化属性键值对</param>
void Log(LogLevel level, string message, params (string Key, object? Value)[] properties);
/// <summary>
/// 使用指定的日志级别记录消息、异常和结构化属性
/// </summary>
/// <param name="level">日志级别</param>
/// <param name="message">日志消息</param>
/// <param name="exception">异常对象</param>
/// <param name="properties">结构化属性键值对</param>
void Log(LogLevel level, string message, Exception? exception, params (string Key, object? Value)[] properties);
}

View File

@ -0,0 +1,124 @@
namespace GFramework.Core.Abstractions.logging;
/// <summary>
/// 日志上下文,用于在异步流中传递结构化属性
/// </summary>
public sealed class LogContext : IDisposable
{
private static readonly AsyncLocal<Dictionary<string, object?>?> _context = new();
private readonly bool _hadPreviousValue;
private readonly string _key;
private readonly object? _previousValue;
private LogContext(string key, object? value)
{
_key = key;
var current = _context.Value;
if (current != null && current.TryGetValue(key, out var prev))
{
_previousValue = prev;
_hadPreviousValue = true;
}
EnsureContext();
_context.Value![key] = value;
}
/// <summary>
/// 获取当前上下文中的所有属性
/// </summary>
public static IReadOnlyDictionary<string, object?> Current
{
get
{
var context = _context.Value;
return context ??
(IReadOnlyDictionary<string, object?>)new Dictionary<string, object?>(StringComparer.Ordinal);
}
}
/// <summary>
/// 释放上下文,恢复之前的值
/// </summary>
public void Dispose()
{
var current = _context.Value;
if (current == null) return;
if (_hadPreviousValue)
{
current[_key] = _previousValue;
}
else
{
current.Remove(_key);
if (current.Count == 0)
{
_context.Value = null;
}
}
}
/// <summary>
/// 向当前上下文添加一个属性
/// </summary>
/// <param name="key">属性键</param>
/// <param name="value">属性值</param>
/// <returns>可释放的上下文对象,释放时会恢复之前的值</returns>
public static IDisposable Push(string key, object? value)
{
if (string.IsNullOrWhiteSpace(key))
throw new ArgumentException("Key cannot be null or whitespace.", nameof(key));
return new LogContext(key, value);
}
/// <summary>
/// 向当前上下文添加多个属性
/// </summary>
/// <param name="properties">属性键值对</param>
/// <returns>可释放的上下文对象,释放时会恢复之前的值</returns>
public static IDisposable PushProperties(params (string Key, object? Value)[] properties)
{
if (properties == null || properties.Length == 0)
throw new ArgumentException("Properties cannot be null or empty.", nameof(properties));
return new CompositeDisposable(properties.Select(p => Push(p.Key, p.Value)).ToArray());
}
/// <summary>
/// 清除当前上下文中的所有属性
/// </summary>
public static void Clear()
{
_context.Value = null;
}
private static void EnsureContext()
{
_context.Value ??= new Dictionary<string, object?>(StringComparer.Ordinal);
}
/// <summary>
/// 组合多个可释放对象
/// </summary>
private sealed class CompositeDisposable : IDisposable
{
private readonly IDisposable[] _disposables;
public CompositeDisposable(IDisposable[] disposables)
{
_disposables = disposables;
}
public void Dispose()
{
// 按相反顺序释放
for (int i = _disposables.Length - 1; i >= 0; i--)
{
_disposables[i].Dispose();
}
}
}
}

View File

@ -0,0 +1,37 @@
namespace GFramework.Core.Abstractions.logging;
/// <summary>
/// 日志条目,包含完整的日志信息
/// </summary>
public sealed record LogEntry(
DateTime Timestamp,
LogLevel Level,
string LoggerName,
string Message,
Exception? Exception,
IReadOnlyDictionary<string, object?>? Properties)
{
/// <summary>
/// 获取合并了上下文属性的所有属性
/// </summary>
/// <returns>包含日志属性和上下文属性的字典</returns>
public IReadOnlyDictionary<string, object?> GetAllProperties()
{
var contextProps = LogContext.Current;
if (Properties == null || Properties.Count == 0)
return contextProps;
if (contextProps.Count == 0)
return Properties;
// 合并属性,日志属性优先
var merged = new Dictionary<string, object?>(contextProps, StringComparer.Ordinal);
foreach (var prop in Properties)
{
merged[prop.Key] = prop.Value;
}
return merged;
}
}

View File

@ -21,7 +21,7 @@ public static class SpanExtensions
/// }
/// </code>
/// </example>
public static bool TryParseValue<T>(this ReadOnlySpan<char> span, out T result) where T : ISpanParsable<T>
public static bool TryParseValue<T>(this ReadOnlySpan<char> span, out T? result) where T : ISpanParsable<T>
{
return T.TryParse(span, null, out result);
}

View File

@ -26,13 +26,23 @@ public readonly struct Result<A> : IEquatable<Result<A>>, IComparable<Result<A>>
// ------------------------------------------------------------------ 状态枚举
/// <summary>
/// 结果状态枚举,表示结果的不同状态
/// 排序: Bottom < Faulted < Success
/// 表示 Result 结构体的内部状态
/// </summary>
private enum ResultState : byte
{
/// <summary>
/// 未初始化状态,表示 Result 尚未被赋值
/// </summary>
Bottom,
/// <summary>
/// 失败状态,表示操作执行失败并包含异常信息
/// </summary>
Faulted,
/// <summary>
/// 成功状态,表示操作执行成功并包含返回值
/// </summary>
Success
}

View File

@ -8,7 +8,7 @@ namespace GFramework.Core.logging;
/// </summary>
public abstract class AbstractLogger(
string? name = null,
LogLevel minLevel = LogLevel.Info) : ILogger
LogLevel minLevel = LogLevel.Info) : IStructuredLogger
{
/// <summary>
/// 根日志记录器的名称常量
@ -451,42 +451,121 @@ public abstract class AbstractLogger(
#endregion
#region Core Pipeline
#region Generic Log Methods
/// <summary>
/// 核心日志记录方法(无参数)
/// 使用指定的日志级别记录消息
/// </summary>
/// <param name="level">日志级别</param>
/// <param name="message">日志消息</param>
private void Log(LogLevel level, string message)
/// <param name="message">要记录的消息字符串</param>
public void Log(LogLevel level, string message)
{
if (!IsEnabled(level)) return;
Write(level, message, null);
}
/// <summary>
/// 核心日志记录方法(带参数格式化)
/// 使用指定的日志级别根据格式和参数记录消息
/// </summary>
/// <param name="level">日志级别</param>
/// <param name="format">格式字符串</param>
/// <param name="args">格式化参数数组</param>
private void Log(LogLevel level, string format, params object[] args)
/// <param name="format">格式字符串</param>
/// <param name="arg">参数</param>
public void Log(LogLevel level, string format, object arg)
{
if (!IsEnabled(level)) return;
Write(level, string.Format(format, args), null);
Write(level, string.Format(format, arg), null);
}
/// <summary>
/// 核心日志记录方法(带异常)
/// 使用指定的日志级别根据格式和参数记录消息
/// </summary>
/// <param name="level">日志级别</param>
/// <param name="message">日志消息</param>
/// <param name="exception">异常对象</param>
private void Log(LogLevel level, string message, Exception exception)
/// <param name="format">格式字符串</param>
/// <param name="arg1">第一个参数</param>
/// <param name="arg2">第二个参数</param>
public void Log(LogLevel level, string format, object arg1, object arg2)
{
if (!IsEnabled(level)) return;
Write(level, string.Format(format, arg1, arg2), null);
}
/// <summary>
/// 使用指定的日志级别根据格式和参数数组记录消息
/// </summary>
/// <param name="level">日志级别</param>
/// <param name="format">格式字符串</param>
/// <param name="arguments">参数数组</param>
public void Log(LogLevel level, string format, params object[] arguments)
{
if (!IsEnabled(level)) return;
Write(level, string.Format(format, arguments), null);
}
/// <summary>
/// 使用指定的日志级别记录消息和异常
/// </summary>
/// <param name="level">日志级别</param>
/// <param name="message">伴随异常的消息</param>
/// <param name="exception">要记录的异常</param>
public void Log(LogLevel level, string message, Exception exception)
{
if (!IsEnabled(level)) return;
Write(level, message, exception);
}
#endregion
#region Structured Log Methods
/// <summary>
/// 使用指定的日志级别记录消息和结构化属性
/// </summary>
/// <param name="level">日志级别</param>
/// <param name="message">日志消息</param>
/// <param name="properties">结构化属性键值对</param>
public virtual void Log(LogLevel level, string message, params (string Key, object? Value)[] properties)
{
if (!IsEnabled(level)) return;
// 默认实现:将属性附加到消息后面
if (properties.Length > 0)
{
var propsStr = string.Join(", ", properties.Select(p => $"{p.Key}={p.Value}"));
Write(level, $"{message} | {propsStr}", null);
}
else
{
Write(level, message, null);
}
}
/// <summary>
/// 使用指定的日志级别记录消息、异常和结构化属性
/// </summary>
/// <param name="level">日志级别</param>
/// <param name="message">日志消息</param>
/// <param name="exception">异常对象</param>
/// <param name="properties">结构化属性键值对</param>
public virtual void Log(LogLevel level, string message, Exception? exception,
params (string Key, object? Value)[] properties)
{
if (!IsEnabled(level)) return;
// 默认实现:将属性附加到消息后面
if (properties.Length > 0)
{
var propsStr = string.Join(", ", properties.Select(p => $"{p.Key}={p.Value}"));
Write(level, $"{message} | {propsStr}", exception);
}
else
{
Write(level, message, exception);
}
}
#endregion
#region Core Pipeline (Private)
#endregion
}

View File

@ -0,0 +1,34 @@
using System.Collections.Concurrent;
using GFramework.Core.Abstractions.logging;
namespace GFramework.Core.logging;
/// <summary>
/// 带缓存的日志工厂包装器,避免重复创建相同名称的日志记录器实例
/// </summary>
public sealed class CachedLoggerFactory : ILoggerFactory
{
private readonly ConcurrentDictionary<string, ILogger> _cache = new();
private readonly ILoggerFactory _innerFactory;
/// <summary>
/// 创建缓存日志工厂实例
/// </summary>
/// <param name="innerFactory">内部日志工厂</param>
public CachedLoggerFactory(ILoggerFactory innerFactory)
{
_innerFactory = innerFactory ?? throw new ArgumentNullException(nameof(innerFactory));
}
/// <summary>
/// 获取或创建指定名称的日志记录器(带缓存)
/// </summary>
/// <param name="name">日志记录器名称</param>
/// <param name="minLevel">最小日志级别</param>
/// <returns>日志记录器实例</returns>
public ILogger GetLogger(string name, LogLevel minLevel = LogLevel.Info)
{
var cacheKey = $"{name}:{minLevel}";
return _cache.GetOrAdd(cacheKey, _ => _innerFactory.GetLogger(name, minLevel));
}
}

View File

@ -0,0 +1,134 @@
using GFramework.Core.Abstractions.logging;
namespace GFramework.Core.logging;
/// <summary>
/// 组合日志记录器,支持同时输出到多个 Appender
/// </summary>
public sealed class CompositeLogger : AbstractLogger, IDisposable
{
private readonly ILogAppender[] _appenders;
/// <summary>
/// 创建组合日志记录器
/// </summary>
/// <param name="name">日志记录器名称</param>
/// <param name="minLevel">最小日志级别</param>
/// <param name="appenders">日志输出器列表</param>
public CompositeLogger(
string name,
LogLevel minLevel,
params ILogAppender[] appenders)
: base(name, minLevel)
{
if (appenders == null || appenders.Length == 0)
throw new ArgumentException("At least one appender must be provided.", nameof(appenders));
_appenders = appenders;
}
/// <summary>
/// 释放所有 Appender 资源
/// </summary>
public void Dispose()
{
foreach (var appender in _appenders)
{
if (appender is IDisposable disposable)
{
disposable.Dispose();
}
}
}
/// <summary>
/// 写入日志到所有 Appender
/// </summary>
/// <param name="level">日志级别</param>
/// <param name="message">日志消息</param>
/// <param name="exception">异常对象</param>
protected override void Write(LogLevel level, string message, Exception? exception)
{
var entry = new LogEntry(
DateTime.Now,
level,
Name(),
message,
exception,
null);
foreach (var appender in _appenders)
{
appender.Append(entry);
}
}
/// <summary>
/// 使用指定的日志级别记录消息和结构化属性
/// </summary>
/// <param name="level">日志级别</param>
/// <param name="message">日志消息</param>
/// <param name="properties">结构化属性键值对</param>
public override void Log(LogLevel level, string message, params (string Key, object? Value)[] properties)
{
if (!IsEnabled(level)) return;
var propsDict = properties.Length > 0
? properties.ToDictionary(p => p.Key, p => p.Value)
: null;
var entry = new LogEntry(
DateTime.Now,
level,
Name(),
message,
null,
propsDict);
foreach (var appender in _appenders)
{
appender.Append(entry);
}
}
/// <summary>
/// 使用指定的日志级别记录消息、异常和结构化属性
/// </summary>
/// <param name="level">日志级别</param>
/// <param name="message">日志消息</param>
/// <param name="exception">异常对象</param>
/// <param name="properties">结构化属性键值对</param>
public override void Log(LogLevel level, string message, Exception? exception,
params (string Key, object? Value)[] properties)
{
if (!IsEnabled(level)) return;
var propsDict = properties.Length > 0
? properties.ToDictionary(p => p.Key, p => p.Value)
: null;
var entry = new LogEntry(
DateTime.Now,
level,
Name(),
message,
exception,
propsDict);
foreach (var appender in _appenders)
{
appender.Append(entry);
}
}
/// <summary>
/// 刷新所有 Appender 的缓冲区
/// </summary>
public void Flush()
{
foreach (var appender in _appenders)
{
appender.Flush();
}
}
}

View File

@ -12,6 +12,17 @@ public sealed class ConsoleLogger(
TextWriter? writer = null,
bool useColors = true) : AbstractLogger(name ?? RootLoggerName, minLevel)
{
// 静态缓存日志级别字符串,避免重复格式化
private static readonly string[] LevelStrings =
[
"TRACE ",
"DEBUG ",
"INFO ",
"WARNING",
"ERROR ",
"FATAL "
];
private readonly bool _useColors = useColors && writer == Console.Out;
private readonly TextWriter _writer = writer ?? Console.Out;
@ -24,7 +35,7 @@ public sealed class ConsoleLogger(
protected override void Write(LogLevel level, string message, Exception? exception)
{
var timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff");
var levelStr = level.ToString().ToUpper().PadRight(7);
var levelStr = LevelStrings[(int)level];
var log = $"[{timestamp}] {levelStr} [{Name()}] {message}";
// 添加异常信息到日志
@ -39,40 +50,32 @@ public sealed class ConsoleLogger(
#region Internal Core
/// <summary>
/// 以指定颜色写入日志消息
/// 以指定颜色写入日志消息(使用 ANSI 转义码)
/// </summary>
/// <param name="level">日志级别</param>
/// <param name="message">日志消息</param>
private void WriteColored(LogLevel level, string message)
{
var original = Console.ForegroundColor;
try
{
Console.ForegroundColor = GetColor(level);
_writer.WriteLine(message);
}
finally
{
Console.ForegroundColor = original;
}
var colorCode = GetAnsiColorCode(level);
_writer.WriteLine($"\x1b[{colorCode}m{message}\x1b[0m");
}
/// <summary>
/// 根据日志级别获取对应的颜色
/// 根据日志级别获取对应的 ANSI 颜色代码
/// </summary>
/// <param name="level">日志级别</param>
/// <returns>控制台颜色</returns>
private static ConsoleColor GetColor(LogLevel level)
/// <returns>ANSI 颜色代码</returns>
private static string GetAnsiColorCode(LogLevel level)
{
return level switch
{
LogLevel.Trace => ConsoleColor.DarkGray,
LogLevel.Debug => ConsoleColor.Cyan,
LogLevel.Info => ConsoleColor.White,
LogLevel.Warning => ConsoleColor.Yellow,
LogLevel.Error => ConsoleColor.Red,
LogLevel.Fatal => ConsoleColor.Magenta,
_ => ConsoleColor.White
LogLevel.Trace => "90", // 暗灰色
LogLevel.Debug => "36", // 青色
LogLevel.Info => "37", // 白色
LogLevel.Warning => "33", // 黄色
LogLevel.Error => "31", // 红色
LogLevel.Fatal => "35", // 洋红色
_ => "37"
};
}

View File

@ -7,18 +7,28 @@ namespace GFramework.Core.logging;
/// </summary>
public sealed class ConsoleLoggerFactoryProvider : ILoggerFactoryProvider
{
private readonly ILoggerFactory _cachedFactory;
/// <summary>
/// 初始化控制台日志记录器工厂提供程序
/// </summary>
public ConsoleLoggerFactoryProvider()
{
_cachedFactory = new CachedLoggerFactory(new ConsoleLoggerFactory());
}
/// <summary>
/// 获取或设置日志记录器的最小日志级别,低于此级别的日志将被忽略
/// </summary>
public LogLevel MinLevel { get; set; } = LogLevel.Info;
/// <summary>
/// 创建一个日志记录器实例
/// 创建一个日志记录器实例(带缓存)
/// </summary>
/// <param name="name">日志记录器的名称,用于标识特定的日志源</param>
/// <returns>配置了指定名称和最小日志级别的ILogger实例</returns>
public ILogger CreateLogger(string name)
{
return new ConsoleLoggerFactory().GetLogger(name, MinLevel);
return _cachedFactory.GetLogger(name, MinLevel);
}
}

View File

@ -0,0 +1,101 @@
using GFramework.Core.Abstractions.logging;
namespace GFramework.Core.logging;
/// <summary>
/// 日志配置类
/// </summary>
public sealed class LoggingConfiguration
{
/// <summary>
/// 全局最小日志级别
/// </summary>
public LogLevel MinLevel { get; set; } = LogLevel.Info;
/// <summary>
/// Appender 配置列表
/// </summary>
public List<AppenderConfiguration> Appenders { get; set; } = new();
/// <summary>
/// 特定 Logger 的日志级别配置
/// </summary>
public Dictionary<string, LogLevel> LoggerLevels { get; set; } = new();
}
/// <summary>
/// Appender 配置
/// </summary>
public sealed class AppenderConfiguration
{
/// <summary>
/// Appender 类型Console, File, RollingFile, Async
/// </summary>
public string Type { get; set; } = string.Empty;
/// <summary>
/// 格式化器类型Default, Json
/// </summary>
public string Formatter { get; set; } = "Default";
/// <summary>
/// 文件路径(仅用于 File 和 RollingFile
/// </summary>
public string? FilePath { get; set; }
/// <summary>
/// 是否使用颜色(仅用于 Console
/// </summary>
public bool UseColors { get; set; } = true;
/// <summary>
/// 缓冲区大小(仅用于 Async
/// </summary>
public int BufferSize { get; set; } = 10000;
/// <summary>
/// 最大文件大小(仅用于 RollingFile字节
/// </summary>
public long MaxFileSize { get; set; } = 10 * 1024 * 1024; // 10MB
/// <summary>
/// 最大文件数量(仅用于 RollingFile
/// </summary>
public int MaxFileCount { get; set; } = 5;
/// <summary>
/// 过滤器配置
/// </summary>
public FilterConfiguration? Filter { get; set; }
/// <summary>
/// 内部 Appender 配置(仅用于 Async
/// </summary>
public AppenderConfiguration? InnerAppender { get; set; }
}
/// <summary>
/// 过滤器配置
/// </summary>
public sealed class FilterConfiguration
{
/// <summary>
/// 过滤器类型LogLevel, Namespace, Composite
/// </summary>
public string Type { get; set; } = "LogLevel";
/// <summary>
/// 最小日志级别(用于 LogLevel 过滤器)
/// </summary>
public LogLevel? MinLevel { get; set; }
/// <summary>
/// 命名空间前缀列表(用于 Namespace 过滤器)
/// </summary>
public List<string>? Namespaces { get; set; }
/// <summary>
/// 子过滤器列表(用于 Composite 过滤器)
/// </summary>
public List<FilterConfiguration>? Filters { get; set; }
}

View File

@ -0,0 +1,165 @@
using System.IO;
using System.Text.Json;
using GFramework.Core.Abstractions.logging;
using GFramework.Core.logging.appenders;
using GFramework.Core.logging.filters;
using GFramework.Core.logging.formatters;
namespace GFramework.Core.logging;
/// <summary>
/// 日志配置加载器
/// </summary>
public static class LoggingConfigurationLoader
{
private static readonly JsonSerializerOptions JsonOptions = new()
{
PropertyNameCaseInsensitive = true,
ReadCommentHandling = JsonCommentHandling.Skip,
AllowTrailingCommas = true
};
/// <summary>
/// 从 JSON 文件加载配置
/// </summary>
/// <param name="filePath">配置文件路径</param>
/// <returns>日志配置对象</returns>
public static LoggingConfiguration LoadFromJson(string filePath)
{
if (!File.Exists(filePath))
throw new FileNotFoundException($"Configuration file not found: {filePath}");
var json = File.ReadAllText(filePath);
var config = JsonSerializer.Deserialize<LoggingConfiguration>(json, JsonOptions);
return config ?? throw new InvalidOperationException("Failed to deserialize configuration.");
}
/// <summary>
/// 从 JSON 字符串加载配置
/// </summary>
/// <param name="json">JSON 字符串</param>
/// <returns>日志配置对象</returns>
public static LoggingConfiguration LoadFromJsonString(string json)
{
var config = JsonSerializer.Deserialize<LoggingConfiguration>(json, JsonOptions);
return config ?? throw new InvalidOperationException("Failed to deserialize configuration.");
}
/// <summary>
/// 根据配置创建 Logger 工厂
/// </summary>
/// <param name="config">日志配置</param>
/// <returns>Logger 工厂</returns>
public static ILoggerFactory CreateFactory(LoggingConfiguration config)
{
return new ConfigurableLoggerFactory(config);
}
/// <summary>
/// 根据配置创建 Appender
/// </summary>
internal static ILogAppender CreateAppender(AppenderConfiguration config)
{
var formatter = CreateFormatter(config.Formatter);
var filter = config.Filter != null ? CreateFilter(config.Filter) : null;
return config.Type.ToLowerInvariant() switch
{
"console" => new ConsoleAppender(formatter, useColors: config.UseColors, filter: filter),
"file" => new FileAppender(
config.FilePath ?? throw new InvalidOperationException("FilePath is required for File appender."),
formatter,
filter),
"rollingfile" => new RollingFileAppender(
config.FilePath ??
throw new InvalidOperationException("FilePath is required for RollingFile appender."),
config.MaxFileSize,
config.MaxFileCount,
formatter,
filter),
"async" => new AsyncLogAppender(
CreateAppender(config.InnerAppender ??
throw new InvalidOperationException("InnerAppender is required for Async appender.")),
config.BufferSize),
_ => throw new NotSupportedException($"Appender type '{config.Type}' is not supported.")
};
}
/// <summary>
/// 根据配置创建格式化器
/// </summary>
internal static ILogFormatter CreateFormatter(string formatterType)
{
return formatterType.ToLowerInvariant() switch
{
"default" => new DefaultLogFormatter(),
"json" => new JsonLogFormatter(),
_ => throw new NotSupportedException($"Formatter type '{formatterType}' is not supported.")
};
}
/// <summary>
/// 根据配置创建过滤器
/// </summary>
internal static ILogFilter CreateFilter(FilterConfiguration config)
{
return config.Type.ToLowerInvariant() switch
{
"loglevel" => new LogLevelFilter(
config.MinLevel ?? throw new InvalidOperationException("MinLevel is required for LogLevel filter.")),
"namespace" => new NamespaceFilter(
config.Namespaces?.ToArray() ??
throw new InvalidOperationException("Namespaces is required for Namespace filter.")),
"composite" => new CompositeFilter(
config.Filters?.Select(CreateFilter).ToArray() ??
throw new InvalidOperationException("Filters is required for Composite filter.")),
_ => throw new NotSupportedException($"Filter type '{config.Type}' is not supported.")
};
}
}
/// <summary>
/// 可配置的 Logger 工厂
/// </summary>
internal sealed class ConfigurableLoggerFactory : ILoggerFactory
{
private readonly ILogAppender[] _appenders;
private readonly LoggingConfiguration _config;
public ConfigurableLoggerFactory(LoggingConfiguration config)
{
_config = config ?? throw new ArgumentNullException(nameof(config));
_appenders = config.Appenders.Select(LoggingConfigurationLoader.CreateAppender).ToArray();
}
public ILogger GetLogger(string name, LogLevel minLevel = LogLevel.Info)
{
// 检查是否有特定 Logger 的级别配置
var effectiveLevel = _config.LoggerLevels.TryGetValue(name, out var level)
? level
: _config.MinLevel;
// 如果没有 Appender返回简单的 ConsoleLogger
if (_appenders.Length == 0)
{
return new ConsoleLogger(name, effectiveLevel);
}
// 如果只有一个 Appender 且是 ConsoleAppender优化为 ConsoleLogger
if (_appenders.Length == 1 && _appenders[0] is ConsoleAppender)
{
return new ConsoleLogger(name, effectiveLevel);
}
// 返回 CompositeLogger
return new CompositeLogger(name, effectiveLevel, _appenders);
}
}

View File

@ -0,0 +1,150 @@
using System.Threading.Channels;
using GFramework.Core.Abstractions.logging;
namespace GFramework.Core.logging.appenders;
/// <summary>
/// 异步日志输出器,使用 Channel 实现非阻塞日志写入
/// </summary>
public sealed class AsyncLogAppender : ILogAppender, IDisposable
{
private readonly Channel<LogEntry> _channel;
private readonly CancellationTokenSource _cts;
private readonly ILogAppender _innerAppender;
private readonly Task _processingTask;
private bool _disposed;
/// <summary>
/// 创建异步日志输出器
/// </summary>
/// <param name="innerAppender">内部日志输出器</param>
/// <param name="bufferSize">缓冲区大小(默认 10000</param>
public AsyncLogAppender(ILogAppender innerAppender, int bufferSize = 10000)
{
_innerAppender = innerAppender ?? throw new ArgumentNullException(nameof(innerAppender));
if (bufferSize <= 0)
throw new ArgumentException("Buffer size must be greater than 0.", nameof(bufferSize));
// 创建有界 Channel
var options = new BoundedChannelOptions(bufferSize)
{
FullMode = BoundedChannelFullMode.Wait, // 缓冲区满时等待
SingleReader = true,
SingleWriter = false
};
_channel = Channel.CreateBounded<LogEntry>(options);
_cts = new CancellationTokenSource();
// 启动后台处理任务
_processingTask = Task.Run(() => ProcessLogsAsync(_cts.Token));
}
/// <summary>
/// 获取当前缓冲区中的日志数量
/// </summary>
public int PendingCount => _channel.Reader.Count;
/// <summary>
/// 获取是否已完成处理
/// </summary>
public bool IsCompleted => _channel.Reader.Completion.IsCompleted;
/// <summary>
/// 释放资源
/// </summary>
public void Dispose()
{
if (_disposed) return;
// 标记 Channel 为完成状态
_channel.Writer.Complete();
// 等待处理任务完成(最多等待 5 秒)
if (!_processingTask.Wait(TimeSpan.FromSeconds(5)))
{
_cts.Cancel();
}
// 释放内部 Appender
if (_innerAppender is IDisposable disposable)
{
disposable.Dispose();
}
_cts.Dispose();
_disposed = true;
}
/// <summary>
/// 追加日志条目(非阻塞)
/// </summary>
/// <param name="entry">日志条目</param>
public void Append(LogEntry entry)
{
if (_disposed)
throw new ObjectDisposedException(nameof(AsyncLogAppender));
// 尝试非阻塞写入,如果失败则丢弃(避免阻塞调用线程)
_channel.Writer.TryWrite(entry);
}
/// <summary>
/// 刷新缓冲区,等待所有日志写入完成
/// </summary>
public void Flush()
{
if (_disposed) return;
// 等待 Channel 中的所有消息被处理
while (_channel.Reader.Count > 0)
{
Thread.Sleep(10);
}
_innerAppender.Flush();
}
/// <summary>
/// 后台处理日志的异步方法
/// </summary>
private async Task ProcessLogsAsync(CancellationToken cancellationToken)
{
try
{
await foreach (var entry in _channel.Reader.ReadAllAsync(cancellationToken))
{
try
{
_innerAppender.Append(entry);
}
catch (Exception ex)
{
// 记录内部错误到控制台(避免递归)
await Console.Error.WriteLineAsync($"[AsyncLogAppender] Error processing log entry: {ex.Message}");
}
}
}
catch (OperationCanceledException)
{
// 正常取消,忽略
}
catch (Exception ex)
{
await Console.Error.WriteLineAsync($"[AsyncLogAppender] Fatal error in processing task: {ex}");
}
finally
{
// 确保最后刷新
try
{
_innerAppender.Flush();
}
catch
{
// 忽略刷新错误
}
}
}
}

View File

@ -0,0 +1,91 @@
using System.IO;
using GFramework.Core.Abstractions.logging;
namespace GFramework.Core.logging.appenders;
/// <summary>
/// 控制台日志输出器
/// </summary>
public sealed class ConsoleAppender : ILogAppender, IDisposable
{
private readonly ILogFilter? _filter;
private readonly ILogFormatter _formatter;
private readonly bool _useColors;
private readonly TextWriter _writer;
/// <summary>
/// 创建控制台日志输出器
/// </summary>
/// <param name="formatter">日志格式化器</param>
/// <param name="writer">文本写入器(默认为 Console.Out</param>
/// <param name="useColors">是否使用颜色(默认为 true</param>
/// <param name="filter">日志过滤器(可选)</param>
public ConsoleAppender(
ILogFormatter formatter,
TextWriter? writer = null,
bool useColors = true,
ILogFilter? filter = null)
{
_formatter = formatter ?? throw new ArgumentNullException(nameof(formatter));
_writer = writer ?? Console.Out;
_useColors = useColors && _writer == Console.Out;
_filter = filter;
}
/// <summary>
/// 释放资源
/// </summary>
public void Dispose()
{
_writer.Flush();
}
/// <summary>
/// 追加日志条目到控制台
/// </summary>
/// <param name="entry">日志条目</param>
public void Append(LogEntry entry)
{
if (_filter != null && !_filter.ShouldLog(entry))
return;
var message = _formatter.Format(entry);
if (_useColors)
{
WriteColored(entry.Level, message);
}
else
{
_writer.WriteLine(message);
}
}
/// <summary>
/// 刷新控制台输出
/// </summary>
public void Flush()
{
_writer.Flush();
}
private void WriteColored(LogLevel level, string message)
{
var colorCode = GetAnsiColorCode(level);
_writer.WriteLine($"\x1b[{colorCode}m{message}\x1b[0m");
}
private static string GetAnsiColorCode(LogLevel level)
{
return level switch
{
LogLevel.Trace => "90", // 暗灰色
LogLevel.Debug => "36", // 青色
LogLevel.Info => "37", // 白色
LogLevel.Warning => "33", // 黄色
LogLevel.Error => "31", // 红色
LogLevel.Fatal => "35", // 洋红色
_ => "37"
};
}
}

View File

@ -0,0 +1,105 @@
using System.IO;
using System.Text;
using GFramework.Core.Abstractions.logging;
using GFramework.Core.logging.formatters;
namespace GFramework.Core.logging.appenders;
/// <summary>
/// 文件日志输出器(线程安全)
/// </summary>
public sealed class FileAppender : ILogAppender, IDisposable
{
private readonly string _filePath;
private readonly ILogFilter? _filter;
private readonly ILogFormatter _formatter;
private readonly object _lock = new();
private bool _disposed;
private StreamWriter? _writer;
/// <summary>
/// 创建文件日志输出器
/// </summary>
/// <param name="filePath">日志文件路径</param>
/// <param name="formatter">日志格式化器</param>
/// <param name="filter">日志过滤器(可选)</param>
public FileAppender(
string filePath,
ILogFormatter? formatter = null,
ILogFilter? filter = null)
{
if (string.IsNullOrWhiteSpace(filePath))
throw new ArgumentException("File path cannot be null or whitespace.", nameof(filePath));
_filePath = filePath;
_formatter = formatter ?? new DefaultLogFormatter();
_filter = filter;
EnsureDirectoryExists();
InitializeWriter();
}
/// <summary>
/// 释放资源
/// </summary>
public void Dispose()
{
if (_disposed) return;
lock (_lock)
{
_writer?.Flush();
_writer?.Dispose();
_writer = null;
_disposed = true;
}
}
/// <summary>
/// 追加日志条目到文件
/// </summary>
/// <param name="entry">日志条目</param>
public void Append(LogEntry entry)
{
if (_disposed)
throw new ObjectDisposedException(nameof(FileAppender));
if (_filter != null && !_filter.ShouldLog(entry))
return;
var message = _formatter.Format(entry);
lock (_lock)
{
_writer?.WriteLine(message);
}
}
/// <summary>
/// 刷新文件缓冲区
/// </summary>
public void Flush()
{
lock (_lock)
{
_writer?.Flush();
}
}
private void EnsureDirectoryExists()
{
var directory = Path.GetDirectoryName(_filePath);
if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
{
Directory.CreateDirectory(directory);
}
}
private void InitializeWriter()
{
_writer = new StreamWriter(_filePath, append: true, Encoding.UTF8)
{
AutoFlush = true
};
}
}

View File

@ -0,0 +1,207 @@
using System.IO;
using System.Text;
using GFramework.Core.Abstractions.logging;
using GFramework.Core.logging.formatters;
namespace GFramework.Core.logging.appenders;
/// <summary>
/// 滚动文件日志输出器,支持按大小自动轮转日志文件
/// </summary>
public sealed class RollingFileAppender : ILogAppender, IDisposable
{
private readonly string _baseFilePath;
private readonly ILogFilter? _filter;
private readonly ILogFormatter _formatter;
private readonly object _lock = new();
private readonly int _maxFileCount;
private readonly long _maxFileSize;
private long _currentSize;
private bool _disposed;
private StreamWriter? _writer;
/// <summary>
/// 创建滚动文件日志输出器
/// </summary>
/// <param name="baseFilePath">基础文件路径(例如: logs/app.log</param>
/// <param name="maxFileSize">单个文件最大大小(字节),默认 10MB</param>
/// <param name="maxFileCount">保留的文件数量,默认 5</param>
/// <param name="formatter">日志格式化器</param>
/// <param name="filter">日志过滤器(可选)</param>
public RollingFileAppender(
string baseFilePath,
long maxFileSize = 10 * 1024 * 1024,
int maxFileCount = 5,
ILogFormatter? formatter = null,
ILogFilter? filter = null)
{
if (string.IsNullOrWhiteSpace(baseFilePath))
throw new ArgumentException("Base file path cannot be null or whitespace.", nameof(baseFilePath));
if (maxFileSize <= 0)
throw new ArgumentException("Max file size must be greater than 0.", nameof(maxFileSize));
if (maxFileCount <= 0)
throw new ArgumentException("Max file count must be greater than 0.", nameof(maxFileCount));
_baseFilePath = baseFilePath;
_maxFileSize = maxFileSize;
_maxFileCount = maxFileCount;
_formatter = formatter ?? new DefaultLogFormatter();
_filter = filter;
EnsureDirectoryExists();
InitializeWriter();
}
/// <summary>
/// 释放资源
/// </summary>
public void Dispose()
{
if (_disposed) return;
lock (_lock)
{
_writer?.Flush();
_writer?.Dispose();
_writer = null;
_disposed = true;
}
}
/// <summary>
/// 追加日志条目到文件
/// </summary>
/// <param name="entry">日志条目</param>
public void Append(LogEntry entry)
{
if (_disposed)
throw new ObjectDisposedException(nameof(RollingFileAppender));
if (_filter != null && !_filter.ShouldLog(entry))
return;
var message = _formatter.Format(entry);
var messageBytes = Encoding.UTF8.GetByteCount(message) + Environment.NewLine.Length;
lock (_lock)
{
// 检查是否需要轮转
if (_currentSize + messageBytes > _maxFileSize)
{
RollFiles();
}
_writer?.WriteLine(message);
_currentSize += messageBytes;
}
}
/// <summary>
/// 刷新文件缓冲区
/// </summary>
public void Flush()
{
lock (_lock)
{
_writer?.Flush();
}
}
/// <summary>
/// 轮转日志文件
/// </summary>
private void RollFiles()
{
// 关闭当前文件
_writer?.Flush();
_writer?.Dispose();
_writer = null;
// 删除最旧的文件(如果存在)
var oldestFile = GetRolledFileName(_maxFileCount - 1);
if (File.Exists(oldestFile))
{
try
{
File.Delete(oldestFile);
}
catch
{
// 忽略删除错误
}
}
// 重命名现有文件: app.log -> app.1.log -> app.2.log -> ...
for (int i = _maxFileCount - 2; i >= 0; i--)
{
var sourceFile = i == 0 ? _baseFilePath : GetRolledFileName(i);
var targetFile = GetRolledFileName(i + 1);
if (File.Exists(sourceFile))
{
try
{
if (File.Exists(targetFile))
{
File.Delete(targetFile);
}
File.Move(sourceFile, targetFile);
}
catch
{
// 忽略移动错误
}
}
}
// 重新初始化写入器
InitializeWriter();
}
/// <summary>
/// 获取轮转后的文件名
/// </summary>
/// <param name="index">文件索引</param>
/// <returns>轮转后的文件路径</returns>
private string GetRolledFileName(int index)
{
var directory = Path.GetDirectoryName(_baseFilePath);
var fileNameWithoutExt = Path.GetFileNameWithoutExtension(_baseFilePath);
var extension = Path.GetExtension(_baseFilePath);
var rolledFileName = $"{fileNameWithoutExt}.{index}{extension}";
return string.IsNullOrEmpty(directory)
? rolledFileName
: Path.Combine(directory, rolledFileName);
}
/// <summary>
/// 确保目录存在
/// </summary>
private void EnsureDirectoryExists()
{
var directory = Path.GetDirectoryName(_baseFilePath);
if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
{
Directory.CreateDirectory(directory);
}
}
/// <summary>
/// 初始化写入器
/// </summary>
private void InitializeWriter()
{
_writer = new StreamWriter(_baseFilePath, append: true, Encoding.UTF8)
{
AutoFlush = true
};
// 获取当前文件大小
_currentSize = File.Exists(_baseFilePath) ? new FileInfo(_baseFilePath).Length : 0;
}
}

View File

@ -0,0 +1,33 @@
using GFramework.Core.Abstractions.logging;
namespace GFramework.Core.logging.filters;
/// <summary>
/// 组合多个过滤器的过滤器AND 逻辑)
/// </summary>
public sealed class CompositeFilter : ILogFilter
{
private readonly ILogFilter[] _filters;
/// <summary>
/// 创建组合过滤器
/// </summary>
/// <param name="filters">要组合的过滤器列表</param>
public CompositeFilter(params ILogFilter[] filters)
{
if (filters == null || filters.Length == 0)
throw new ArgumentException("At least one filter must be provided.", nameof(filters));
_filters = filters;
}
/// <summary>
/// 判断日志是否通过所有过滤器AND 逻辑)
/// </summary>
/// <param name="entry">日志条目</param>
/// <returns>如果所有过滤器都返回 true 则返回 true</returns>
public bool ShouldLog(LogEntry entry)
{
return _filters.All(filter => filter.ShouldLog(entry));
}
}

View File

@ -0,0 +1,30 @@
using GFramework.Core.Abstractions.logging;
namespace GFramework.Core.logging.filters;
/// <summary>
/// 按日志级别过滤的过滤器
/// </summary>
public sealed class LogLevelFilter : ILogFilter
{
private readonly LogLevel _minLevel;
/// <summary>
/// 创建日志级别过滤器
/// </summary>
/// <param name="minLevel">最小日志级别</param>
public LogLevelFilter(LogLevel minLevel)
{
_minLevel = minLevel;
}
/// <summary>
/// 判断日志级别是否满足最小级别要求
/// </summary>
/// <param name="entry">日志条目</param>
/// <returns>如果日志级别大于等于最小级别返回 true</returns>
public bool ShouldLog(LogEntry entry)
{
return entry.Level >= _minLevel;
}
}

View File

@ -0,0 +1,33 @@
using GFramework.Core.Abstractions.logging;
namespace GFramework.Core.logging.filters;
/// <summary>
/// 按命名空间前缀过滤的过滤器
/// </summary>
public sealed class NamespaceFilter : ILogFilter
{
private readonly string[] _allowedPrefixes;
/// <summary>
/// 创建命名空间过滤器
/// </summary>
/// <param name="allowedPrefixes">允许的命名空间前缀列表</param>
public NamespaceFilter(params string[] allowedPrefixes)
{
if (allowedPrefixes == null || allowedPrefixes.Length == 0)
throw new ArgumentException("At least one namespace prefix must be provided.", nameof(allowedPrefixes));
_allowedPrefixes = allowedPrefixes;
}
/// <summary>
/// 判断日志记录器名称是否匹配允许的命名空间前缀
/// </summary>
/// <param name="entry">日志条目</param>
/// <returns>如果匹配任一前缀返回 true</returns>
public bool ShouldLog(LogEntry entry)
{
return _allowedPrefixes.Any(prefix => entry.LoggerName.StartsWith(prefix, StringComparison.OrdinalIgnoreCase));
}
}

View File

@ -0,0 +1,56 @@
using System.Text;
using GFramework.Core.Abstractions.logging;
namespace GFramework.Core.logging.formatters;
/// <summary>
/// 默认日志格式化器,保持与现有格式兼容
/// </summary>
public sealed class DefaultLogFormatter : ILogFormatter
{
private static readonly string[] LevelStrings =
[
"TRACE ",
"DEBUG ",
"INFO ",
"WARNING",
"ERROR ",
"FATAL "
];
/// <summary>
/// 将日志条目格式化为默认格式
/// </summary>
/// <param name="entry">日志条目</param>
/// <returns>格式化后的日志字符串</returns>
public string Format(LogEntry entry)
{
var timestamp = entry.Timestamp.ToString("yyyy-MM-dd HH:mm:ss.fff");
var levelStr = LevelStrings[(int)entry.Level];
var sb = new StringBuilder();
sb.Append('[').Append(timestamp).Append("] ")
.Append(levelStr).Append(" [")
.Append(entry.LoggerName).Append("] ")
.Append(entry.Message);
// 添加结构化属性
var properties = entry.GetAllProperties();
if (properties.Count > 0)
{
sb.Append(" |");
foreach (var prop in properties)
{
sb.Append(' ').Append(prop.Key).Append('=').Append(prop.Value);
}
}
// 添加异常信息
if (entry.Exception != null)
{
sb.Append(Environment.NewLine).Append(entry.Exception);
}
return sb.ToString();
}
}

View File

@ -0,0 +1,52 @@
using System.Text.Json;
using GFramework.Core.Abstractions.logging;
namespace GFramework.Core.logging.formatters;
/// <summary>
/// JSON 格式化器,将日志输出为 JSON 格式
/// </summary>
public sealed class JsonLogFormatter : ILogFormatter
{
private static readonly JsonSerializerOptions JsonOptions = new()
{
WriteIndented = false,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
/// <summary>
/// 将日志条目格式化为 JSON 格式
/// </summary>
/// <param name="entry">日志条目</param>
/// <returns>JSON 格式的日志字符串</returns>
public string Format(LogEntry entry)
{
var logObject = new Dictionary<string, object?>
{
["timestamp"] = entry.Timestamp.ToString("O"), // ISO 8601 格式
["level"] = entry.Level.ToString().ToUpper(),
["logger"] = entry.LoggerName,
["message"] = entry.Message
};
// 添加结构化属性
var properties = entry.GetAllProperties();
if (properties.Count > 0)
{
logObject["properties"] = properties;
}
// 添加异常信息
if (entry.Exception != null)
{
logObject["exception"] = new
{
type = entry.Exception.GetType().FullName,
message = entry.Exception.Message,
stackTrace = entry.Exception.StackTrace
};
}
return JsonSerializer.Serialize(logObject, JsonOptions);
}
}

View File

@ -239,11 +239,12 @@ public sealed class FileStorage : IFileStorage
{
var fullPath = string.IsNullOrEmpty(path) ? _rootPath : Path.Combine(_rootPath, path);
if (!Directory.Exists(fullPath))
return Task.FromResult<IReadOnlyList<string>>(Array.Empty<string>());
return Task.FromResult<IReadOnlyList<string>>([]);
var dirs = Directory.GetDirectories(fullPath)
.Select(Path.GetFileName)
.Where(name => !string.IsNullOrEmpty(name) && !name.StartsWith(".", StringComparison.Ordinal))
.OfType<string>()
.Where(name => !string.IsNullOrEmpty(name) && !name.StartsWith('.'))
.ToList();
return Task.FromResult<IReadOnlyList<string>>(dirs);
@ -262,6 +263,7 @@ public sealed class FileStorage : IFileStorage
var files = Directory.GetFiles(fullPath)
.Select(Path.GetFileName)
.OfType<string>()
.Where(name => !string.IsNullOrEmpty(name))
.ToList();

View File

@ -15,6 +15,17 @@ public sealed class GodotLogger(
string? name = null,
LogLevel minLevel = LogLevel.Info) : AbstractLogger(name ?? RootLoggerName, minLevel)
{
// 静态缓存日志级别字符串,避免重复格式化
private static readonly string[] LevelStrings =
[
"TRACE ",
"DEBUG ",
"INFO ",
"WARNING",
"ERROR ",
"FATAL "
];
/// <summary>
/// 写入日志的核心方法。
/// 格式化日志消息并根据日志级别调用 Godot 的输出方法。
@ -26,7 +37,7 @@ public sealed class GodotLogger(
{
// 构造时间戳和日志前缀
var timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff");
var levelStr = level.ToString().ToUpper().PadRight(7);
var levelStr = LevelStrings[(int)level];
var logPrefix = $"[{timestamp}] {levelStr} [{Name()}]";
// 添加异常信息到日志消息中
@ -46,7 +57,16 @@ public sealed class GodotLogger(
case LogLevel.Warning:
GD.PushWarning(logMessage);
break;
default: // Trace / Debug / Info
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;
}

View File

@ -1,4 +1,5 @@
using GFramework.Core.Abstractions.logging;
using GFramework.Core.logging;
namespace GFramework.Godot.logging;
@ -7,18 +8,28 @@ namespace GFramework.Godot.logging;
/// </summary>
public sealed class GodotLoggerFactoryProvider : ILoggerFactoryProvider
{
private readonly ILoggerFactory _cachedFactory;
/// <summary>
/// 初始化Godot日志记录器工厂提供程序
/// </summary>
public GodotLoggerFactoryProvider()
{
_cachedFactory = new CachedLoggerFactory(new GodotLoggerFactory());
}
/// <summary>
/// 获取或设置最小日志级别
/// </summary>
public LogLevel MinLevel { get; set; }
/// <summary>
/// 创建指定名称的日志记录器实例
/// 创建指定名称的日志记录器实例(带缓存)
/// </summary>
/// <param name="name">日志记录器的名称</param>
/// <returns>返回配置了最小日志级别的Godot日志记录器实例</returns>
public ILogger CreateLogger(string name)
{
return new GodotLoggerFactory().GetLogger(name, MinLevel);
return _cachedFactory.GetLogger(name, MinLevel);
}
}