diff --git a/GFramework.Core.Abstractions/logging/ILogAppender.cs b/GFramework.Core.Abstractions/logging/ILogAppender.cs
new file mode 100644
index 0000000..a667d17
--- /dev/null
+++ b/GFramework.Core.Abstractions/logging/ILogAppender.cs
@@ -0,0 +1,18 @@
+namespace GFramework.Core.Abstractions.logging;
+
+///
+/// 日志输出器接口,负责将日志条目写入特定目标
+///
+public interface ILogAppender
+{
+ ///
+ /// 追加日志条目
+ ///
+ /// 日志条目
+ void Append(LogEntry entry);
+
+ ///
+ /// 刷新缓冲区,确保所有日志已写入
+ ///
+ void Flush();
+}
\ No newline at end of file
diff --git a/GFramework.Core.Abstractions/logging/ILogFilter.cs b/GFramework.Core.Abstractions/logging/ILogFilter.cs
new file mode 100644
index 0000000..c5e8e5c
--- /dev/null
+++ b/GFramework.Core.Abstractions/logging/ILogFilter.cs
@@ -0,0 +1,14 @@
+namespace GFramework.Core.Abstractions.logging;
+
+///
+/// 日志过滤器接口,用于决定是否应该记录某条日志
+///
+public interface ILogFilter
+{
+ ///
+ /// 判断是否应该记录该日志条目
+ ///
+ /// 日志条目
+ /// 如果应该记录返回 true,否则返回 false
+ bool ShouldLog(LogEntry entry);
+}
\ No newline at end of file
diff --git a/GFramework.Core.Abstractions/logging/ILogFormatter.cs b/GFramework.Core.Abstractions/logging/ILogFormatter.cs
new file mode 100644
index 0000000..00792e1
--- /dev/null
+++ b/GFramework.Core.Abstractions/logging/ILogFormatter.cs
@@ -0,0 +1,14 @@
+namespace GFramework.Core.Abstractions.logging;
+
+///
+/// 日志格式化器接口,用于将日志条目格式化为字符串
+///
+public interface ILogFormatter
+{
+ ///
+ /// 将日志条目格式化为字符串
+ ///
+ /// 日志条目
+ /// 格式化后的日志字符串
+ string Format(LogEntry entry);
+}
\ No newline at end of file
diff --git a/GFramework.Core.Abstractions/logging/ILogger.cs b/GFramework.Core.Abstractions/logging/ILogger.cs
index 185d604..72efefc 100644
--- a/GFramework.Core.Abstractions/logging/ILogger.cs
+++ b/GFramework.Core.Abstractions/logging/ILogger.cs
@@ -309,4 +309,48 @@ public interface ILogger
void Fatal(string msg, Exception t);
#endregion
+
+ #region Generic Log Methods
+
+ ///
+ /// 使用指定的日志级别记录消息
+ ///
+ /// 日志级别
+ /// 要记录的消息字符串
+ void Log(LogLevel level, string message);
+
+ ///
+ /// 使用指定的日志级别根据格式和参数记录消息
+ ///
+ /// 日志级别
+ /// 格式字符串
+ /// 参数
+ void Log(LogLevel level, string format, object arg);
+
+ ///
+ /// 使用指定的日志级别根据格式和参数记录消息
+ ///
+ /// 日志级别
+ /// 格式字符串
+ /// 第一个参数
+ /// 第二个参数
+ void Log(LogLevel level, string format, object arg1, object arg2);
+
+ ///
+ /// 使用指定的日志级别根据格式和参数数组记录消息
+ ///
+ /// 日志级别
+ /// 格式字符串
+ /// 参数数组
+ void Log(LogLevel level, string format, params object[] arguments);
+
+ ///
+ /// 使用指定的日志级别记录消息和异常
+ ///
+ /// 日志级别
+ /// 伴随异常的消息
+ /// 要记录的异常
+ void Log(LogLevel level, string message, Exception exception);
+
+ #endregion
}
\ No newline at end of file
diff --git a/GFramework.Core.Abstractions/logging/IStructuredLogger.cs b/GFramework.Core.Abstractions/logging/IStructuredLogger.cs
new file mode 100644
index 0000000..eee7092
--- /dev/null
+++ b/GFramework.Core.Abstractions/logging/IStructuredLogger.cs
@@ -0,0 +1,24 @@
+namespace GFramework.Core.Abstractions.logging;
+
+///
+/// 支持结构化日志的日志记录器接口
+///
+public interface IStructuredLogger : ILogger
+{
+ ///
+ /// 使用指定的日志级别记录消息和结构化属性
+ ///
+ /// 日志级别
+ /// 日志消息
+ /// 结构化属性键值对
+ void Log(LogLevel level, string message, params (string Key, object? Value)[] properties);
+
+ ///
+ /// 使用指定的日志级别记录消息、异常和结构化属性
+ ///
+ /// 日志级别
+ /// 日志消息
+ /// 异常对象
+ /// 结构化属性键值对
+ void Log(LogLevel level, string message, Exception? exception, params (string Key, object? Value)[] properties);
+}
\ No newline at end of file
diff --git a/GFramework.Core.Abstractions/logging/LogContext.cs b/GFramework.Core.Abstractions/logging/LogContext.cs
new file mode 100644
index 0000000..0f54d16
--- /dev/null
+++ b/GFramework.Core.Abstractions/logging/LogContext.cs
@@ -0,0 +1,124 @@
+namespace GFramework.Core.Abstractions.logging;
+
+///
+/// 日志上下文,用于在异步流中传递结构化属性
+///
+public sealed class LogContext : IDisposable
+{
+ private static readonly AsyncLocal?> _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;
+ }
+
+ ///
+ /// 获取当前上下文中的所有属性
+ ///
+ public static IReadOnlyDictionary Current
+ {
+ get
+ {
+ var context = _context.Value;
+ return context ??
+ (IReadOnlyDictionary)new Dictionary(StringComparer.Ordinal);
+ }
+ }
+
+ ///
+ /// 释放上下文,恢复之前的值
+ ///
+ 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;
+ }
+ }
+ }
+
+ ///
+ /// 向当前上下文添加一个属性
+ ///
+ /// 属性键
+ /// 属性值
+ /// 可释放的上下文对象,释放时会恢复之前的值
+ 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);
+ }
+
+ ///
+ /// 向当前上下文添加多个属性
+ ///
+ /// 属性键值对
+ /// 可释放的上下文对象,释放时会恢复之前的值
+ 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());
+ }
+
+ ///
+ /// 清除当前上下文中的所有属性
+ ///
+ public static void Clear()
+ {
+ _context.Value = null;
+ }
+
+ private static void EnsureContext()
+ {
+ _context.Value ??= new Dictionary(StringComparer.Ordinal);
+ }
+
+ ///
+ /// 组合多个可释放对象
+ ///
+ 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();
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/GFramework.Core.Abstractions/logging/LogEntry.cs b/GFramework.Core.Abstractions/logging/LogEntry.cs
new file mode 100644
index 0000000..740353e
--- /dev/null
+++ b/GFramework.Core.Abstractions/logging/LogEntry.cs
@@ -0,0 +1,37 @@
+namespace GFramework.Core.Abstractions.logging;
+
+///
+/// 日志条目,包含完整的日志信息
+///
+public sealed record LogEntry(
+ DateTime Timestamp,
+ LogLevel Level,
+ string LoggerName,
+ string Message,
+ Exception? Exception,
+ IReadOnlyDictionary? Properties)
+{
+ ///
+ /// 获取合并了上下文属性的所有属性
+ ///
+ /// 包含日志属性和上下文属性的字典
+ public IReadOnlyDictionary GetAllProperties()
+ {
+ var contextProps = LogContext.Current;
+
+ if (Properties == null || Properties.Count == 0)
+ return contextProps;
+
+ if (contextProps.Count == 0)
+ return Properties;
+
+ // 合并属性,日志属性优先
+ var merged = new Dictionary(contextProps, StringComparer.Ordinal);
+ foreach (var prop in Properties)
+ {
+ merged[prop.Key] = prop.Value;
+ }
+
+ return merged;
+ }
+}
\ No newline at end of file
diff --git a/GFramework.Core/extensions/SpanExtensions.cs b/GFramework.Core/extensions/SpanExtensions.cs
index d24145f..575d42f 100644
--- a/GFramework.Core/extensions/SpanExtensions.cs
+++ b/GFramework.Core/extensions/SpanExtensions.cs
@@ -21,7 +21,7 @@ public static class SpanExtensions
/// }
///
///
- public static bool TryParseValue(this ReadOnlySpan span, out T result) where T : ISpanParsable
+ public static bool TryParseValue(this ReadOnlySpan span, out T? result) where T : ISpanParsable
{
return T.TryParse(span, null, out result);
}
diff --git a/GFramework.Core/functional/Result.T.cs b/GFramework.Core/functional/Result.T.cs
index a65c505..637900c 100644
--- a/GFramework.Core/functional/Result.T.cs
+++ b/GFramework.Core/functional/Result.T.cs
@@ -26,13 +26,23 @@ public readonly struct Result : IEquatable>, IComparable>
// ------------------------------------------------------------------ 状态枚举
///
- /// 结果状态枚举,表示结果的不同状态
- /// 排序: Bottom < Faulted < Success
+ /// 表示 Result 结构体的内部状态
///
private enum ResultState : byte
{
+ ///
+ /// 未初始化状态,表示 Result 尚未被赋值
+ ///
Bottom,
+
+ ///
+ /// 失败状态,表示操作执行失败并包含异常信息
+ ///
Faulted,
+
+ ///
+ /// 成功状态,表示操作执行成功并包含返回值
+ ///
Success
}
diff --git a/GFramework.Core/logging/AbstractLogger.cs b/GFramework.Core/logging/AbstractLogger.cs
index f924af2..e9f8ff7 100644
--- a/GFramework.Core/logging/AbstractLogger.cs
+++ b/GFramework.Core/logging/AbstractLogger.cs
@@ -8,7 +8,7 @@ namespace GFramework.Core.logging;
///
public abstract class AbstractLogger(
string? name = null,
- LogLevel minLevel = LogLevel.Info) : ILogger
+ LogLevel minLevel = LogLevel.Info) : IStructuredLogger
{
///
/// 根日志记录器的名称常量
@@ -451,42 +451,121 @@ public abstract class AbstractLogger(
#endregion
- #region Core Pipeline
+ #region Generic Log Methods
///
- /// 核心日志记录方法(无参数)
+ /// 使用指定的日志级别记录消息
///
/// 日志级别
- /// 日志消息
- private void Log(LogLevel level, string message)
+ /// 要记录的消息字符串
+ public void Log(LogLevel level, string message)
{
if (!IsEnabled(level)) return;
Write(level, message, null);
}
///
- /// 核心日志记录方法(带参数格式化)
+ /// 使用指定的日志级别根据格式和参数记录消息
///
/// 日志级别
- /// 格式化字符串
- /// 格式化参数数组
- private void Log(LogLevel level, string format, params object[] args)
+ /// 格式字符串
+ /// 参数
+ 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);
}
///
- /// 核心日志记录方法(带异常)
+ /// 使用指定的日志级别根据格式和参数记录消息
///
/// 日志级别
- /// 日志消息
- /// 异常对象
- private void Log(LogLevel level, string message, Exception exception)
+ /// 格式字符串
+ /// 第一个参数
+ /// 第二个参数
+ public void Log(LogLevel level, string format, object arg1, object arg2)
+ {
+ if (!IsEnabled(level)) return;
+ Write(level, string.Format(format, arg1, arg2), null);
+ }
+
+ ///
+ /// 使用指定的日志级别根据格式和参数数组记录消息
+ ///
+ /// 日志级别
+ /// 格式字符串
+ /// 参数数组
+ public void Log(LogLevel level, string format, params object[] arguments)
+ {
+ if (!IsEnabled(level)) return;
+ Write(level, string.Format(format, arguments), null);
+ }
+
+ ///
+ /// 使用指定的日志级别记录消息和异常
+ ///
+ /// 日志级别
+ /// 伴随异常的消息
+ /// 要记录的异常
+ public void Log(LogLevel level, string message, Exception exception)
{
if (!IsEnabled(level)) return;
Write(level, message, exception);
}
#endregion
+
+ #region Structured Log Methods
+
+ ///
+ /// 使用指定的日志级别记录消息和结构化属性
+ ///
+ /// 日志级别
+ /// 日志消息
+ /// 结构化属性键值对
+ 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);
+ }
+ }
+
+ ///
+ /// 使用指定的日志级别记录消息、异常和结构化属性
+ ///
+ /// 日志级别
+ /// 日志消息
+ /// 异常对象
+ /// 结构化属性键值对
+ 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
}
\ No newline at end of file
diff --git a/GFramework.Core/logging/CachedLoggerFactory.cs b/GFramework.Core/logging/CachedLoggerFactory.cs
new file mode 100644
index 0000000..9aea055
--- /dev/null
+++ b/GFramework.Core/logging/CachedLoggerFactory.cs
@@ -0,0 +1,34 @@
+using System.Collections.Concurrent;
+using GFramework.Core.Abstractions.logging;
+
+namespace GFramework.Core.logging;
+
+///
+/// 带缓存的日志工厂包装器,避免重复创建相同名称的日志记录器实例
+///
+public sealed class CachedLoggerFactory : ILoggerFactory
+{
+ private readonly ConcurrentDictionary _cache = new();
+ private readonly ILoggerFactory _innerFactory;
+
+ ///
+ /// 创建缓存日志工厂实例
+ ///
+ /// 内部日志工厂
+ public CachedLoggerFactory(ILoggerFactory innerFactory)
+ {
+ _innerFactory = innerFactory ?? throw new ArgumentNullException(nameof(innerFactory));
+ }
+
+ ///
+ /// 获取或创建指定名称的日志记录器(带缓存)
+ ///
+ /// 日志记录器名称
+ /// 最小日志级别
+ /// 日志记录器实例
+ public ILogger GetLogger(string name, LogLevel minLevel = LogLevel.Info)
+ {
+ var cacheKey = $"{name}:{minLevel}";
+ return _cache.GetOrAdd(cacheKey, _ => _innerFactory.GetLogger(name, minLevel));
+ }
+}
\ No newline at end of file
diff --git a/GFramework.Core/logging/CompositeLogger.cs b/GFramework.Core/logging/CompositeLogger.cs
new file mode 100644
index 0000000..06dba6c
--- /dev/null
+++ b/GFramework.Core/logging/CompositeLogger.cs
@@ -0,0 +1,134 @@
+using GFramework.Core.Abstractions.logging;
+
+namespace GFramework.Core.logging;
+
+///
+/// 组合日志记录器,支持同时输出到多个 Appender
+///
+public sealed class CompositeLogger : AbstractLogger, IDisposable
+{
+ private readonly ILogAppender[] _appenders;
+
+ ///
+ /// 创建组合日志记录器
+ ///
+ /// 日志记录器名称
+ /// 最小日志级别
+ /// 日志输出器列表
+ 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;
+ }
+
+ ///
+ /// 释放所有 Appender 资源
+ ///
+ public void Dispose()
+ {
+ foreach (var appender in _appenders)
+ {
+ if (appender is IDisposable disposable)
+ {
+ disposable.Dispose();
+ }
+ }
+ }
+
+ ///
+ /// 写入日志到所有 Appender
+ ///
+ /// 日志级别
+ /// 日志消息
+ /// 异常对象
+ 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);
+ }
+ }
+
+ ///
+ /// 使用指定的日志级别记录消息和结构化属性
+ ///
+ /// 日志级别
+ /// 日志消息
+ /// 结构化属性键值对
+ 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);
+ }
+ }
+
+ ///
+ /// 使用指定的日志级别记录消息、异常和结构化属性
+ ///
+ /// 日志级别
+ /// 日志消息
+ /// 异常对象
+ /// 结构化属性键值对
+ 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);
+ }
+ }
+
+ ///
+ /// 刷新所有 Appender 的缓冲区
+ ///
+ public void Flush()
+ {
+ foreach (var appender in _appenders)
+ {
+ appender.Flush();
+ }
+ }
+}
\ No newline at end of file
diff --git a/GFramework.Core/logging/ConsoleLogger.cs b/GFramework.Core/logging/ConsoleLogger.cs
index 1645f18..468a35b 100644
--- a/GFramework.Core/logging/ConsoleLogger.cs
+++ b/GFramework.Core/logging/ConsoleLogger.cs
@@ -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
///
- /// 以指定颜色写入日志消息
+ /// 以指定颜色写入日志消息(使用 ANSI 转义码)
///
/// 日志级别
/// 日志消息
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");
}
///
- /// 根据日志级别获取对应的颜色
+ /// 根据日志级别获取对应的 ANSI 颜色代码
///
/// 日志级别
- /// 控制台颜色
- private static ConsoleColor GetColor(LogLevel level)
+ /// ANSI 颜色代码
+ 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"
};
}
diff --git a/GFramework.Core/logging/ConsoleLoggerFactoryProvider.cs b/GFramework.Core/logging/ConsoleLoggerFactoryProvider.cs
index 9751f55..52583a3 100644
--- a/GFramework.Core/logging/ConsoleLoggerFactoryProvider.cs
+++ b/GFramework.Core/logging/ConsoleLoggerFactoryProvider.cs
@@ -7,18 +7,28 @@ namespace GFramework.Core.logging;
///
public sealed class ConsoleLoggerFactoryProvider : ILoggerFactoryProvider
{
+ private readonly ILoggerFactory _cachedFactory;
+
+ ///
+ /// 初始化控制台日志记录器工厂提供程序
+ ///
+ public ConsoleLoggerFactoryProvider()
+ {
+ _cachedFactory = new CachedLoggerFactory(new ConsoleLoggerFactory());
+ }
+
///
/// 获取或设置日志记录器的最小日志级别,低于此级别的日志将被忽略
///
public LogLevel MinLevel { get; set; } = LogLevel.Info;
///
- /// 创建一个日志记录器实例
+ /// 创建一个日志记录器实例(带缓存)
///
/// 日志记录器的名称,用于标识特定的日志源
/// 配置了指定名称和最小日志级别的ILogger实例
public ILogger CreateLogger(string name)
{
- return new ConsoleLoggerFactory().GetLogger(name, MinLevel);
+ return _cachedFactory.GetLogger(name, MinLevel);
}
}
\ No newline at end of file
diff --git a/GFramework.Core/logging/LoggingConfiguration.cs b/GFramework.Core/logging/LoggingConfiguration.cs
new file mode 100644
index 0000000..37f7ff8
--- /dev/null
+++ b/GFramework.Core/logging/LoggingConfiguration.cs
@@ -0,0 +1,101 @@
+using GFramework.Core.Abstractions.logging;
+
+namespace GFramework.Core.logging;
+
+///
+/// 日志配置类
+///
+public sealed class LoggingConfiguration
+{
+ ///
+ /// 全局最小日志级别
+ ///
+ public LogLevel MinLevel { get; set; } = LogLevel.Info;
+
+ ///
+ /// Appender 配置列表
+ ///
+ public List Appenders { get; set; } = new();
+
+ ///
+ /// 特定 Logger 的日志级别配置
+ ///
+ public Dictionary LoggerLevels { get; set; } = new();
+}
+
+///
+/// Appender 配置
+///
+public sealed class AppenderConfiguration
+{
+ ///
+ /// Appender 类型(Console, File, RollingFile, Async)
+ ///
+ public string Type { get; set; } = string.Empty;
+
+ ///
+ /// 格式化器类型(Default, Json)
+ ///
+ public string Formatter { get; set; } = "Default";
+
+ ///
+ /// 文件路径(仅用于 File 和 RollingFile)
+ ///
+ public string? FilePath { get; set; }
+
+ ///
+ /// 是否使用颜色(仅用于 Console)
+ ///
+ public bool UseColors { get; set; } = true;
+
+ ///
+ /// 缓冲区大小(仅用于 Async)
+ ///
+ public int BufferSize { get; set; } = 10000;
+
+ ///
+ /// 最大文件大小(仅用于 RollingFile,字节)
+ ///
+ public long MaxFileSize { get; set; } = 10 * 1024 * 1024; // 10MB
+
+ ///
+ /// 最大文件数量(仅用于 RollingFile)
+ ///
+ public int MaxFileCount { get; set; } = 5;
+
+ ///
+ /// 过滤器配置
+ ///
+ public FilterConfiguration? Filter { get; set; }
+
+ ///
+ /// 内部 Appender 配置(仅用于 Async)
+ ///
+ public AppenderConfiguration? InnerAppender { get; set; }
+}
+
+///
+/// 过滤器配置
+///
+public sealed class FilterConfiguration
+{
+ ///
+ /// 过滤器类型(LogLevel, Namespace, Composite)
+ ///
+ public string Type { get; set; } = "LogLevel";
+
+ ///
+ /// 最小日志级别(用于 LogLevel 过滤器)
+ ///
+ public LogLevel? MinLevel { get; set; }
+
+ ///
+ /// 命名空间前缀列表(用于 Namespace 过滤器)
+ ///
+ public List? Namespaces { get; set; }
+
+ ///
+ /// 子过滤器列表(用于 Composite 过滤器)
+ ///
+ public List? Filters { get; set; }
+}
\ No newline at end of file
diff --git a/GFramework.Core/logging/LoggingConfigurationLoader.cs b/GFramework.Core/logging/LoggingConfigurationLoader.cs
new file mode 100644
index 0000000..1110ecc
--- /dev/null
+++ b/GFramework.Core/logging/LoggingConfigurationLoader.cs
@@ -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;
+
+///
+/// 日志配置加载器
+///
+public static class LoggingConfigurationLoader
+{
+ private static readonly JsonSerializerOptions JsonOptions = new()
+ {
+ PropertyNameCaseInsensitive = true,
+ ReadCommentHandling = JsonCommentHandling.Skip,
+ AllowTrailingCommas = true
+ };
+
+ ///
+ /// 从 JSON 文件加载配置
+ ///
+ /// 配置文件路径
+ /// 日志配置对象
+ 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(json, JsonOptions);
+
+ return config ?? throw new InvalidOperationException("Failed to deserialize configuration.");
+ }
+
+ ///
+ /// 从 JSON 字符串加载配置
+ ///
+ /// JSON 字符串
+ /// 日志配置对象
+ public static LoggingConfiguration LoadFromJsonString(string json)
+ {
+ var config = JsonSerializer.Deserialize(json, JsonOptions);
+ return config ?? throw new InvalidOperationException("Failed to deserialize configuration.");
+ }
+
+ ///
+ /// 根据配置创建 Logger 工厂
+ ///
+ /// 日志配置
+ /// Logger 工厂
+ public static ILoggerFactory CreateFactory(LoggingConfiguration config)
+ {
+ return new ConfigurableLoggerFactory(config);
+ }
+
+ ///
+ /// 根据配置创建 Appender
+ ///
+ 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.")
+ };
+ }
+
+ ///
+ /// 根据配置创建格式化器
+ ///
+ 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.")
+ };
+ }
+
+ ///
+ /// 根据配置创建过滤器
+ ///
+ 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.")
+ };
+ }
+}
+
+///
+/// 可配置的 Logger 工厂
+///
+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);
+ }
+}
\ No newline at end of file
diff --git a/GFramework.Core/logging/appenders/AsyncLogAppender.cs b/GFramework.Core/logging/appenders/AsyncLogAppender.cs
new file mode 100644
index 0000000..1c864ee
--- /dev/null
+++ b/GFramework.Core/logging/appenders/AsyncLogAppender.cs
@@ -0,0 +1,150 @@
+using System.Threading.Channels;
+using GFramework.Core.Abstractions.logging;
+
+namespace GFramework.Core.logging.appenders;
+
+///
+/// 异步日志输出器,使用 Channel 实现非阻塞日志写入
+///
+public sealed class AsyncLogAppender : ILogAppender, IDisposable
+{
+ private readonly Channel _channel;
+ private readonly CancellationTokenSource _cts;
+ private readonly ILogAppender _innerAppender;
+ private readonly Task _processingTask;
+ private bool _disposed;
+
+ ///
+ /// 创建异步日志输出器
+ ///
+ /// 内部日志输出器
+ /// 缓冲区大小(默认 10000)
+ 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(options);
+ _cts = new CancellationTokenSource();
+
+ // 启动后台处理任务
+ _processingTask = Task.Run(() => ProcessLogsAsync(_cts.Token));
+ }
+
+ ///
+ /// 获取当前缓冲区中的日志数量
+ ///
+ public int PendingCount => _channel.Reader.Count;
+
+ ///
+ /// 获取是否已完成处理
+ ///
+ public bool IsCompleted => _channel.Reader.Completion.IsCompleted;
+
+ ///
+ /// 释放资源
+ ///
+ 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;
+ }
+
+ ///
+ /// 追加日志条目(非阻塞)
+ ///
+ /// 日志条目
+ public void Append(LogEntry entry)
+ {
+ if (_disposed)
+ throw new ObjectDisposedException(nameof(AsyncLogAppender));
+
+ // 尝试非阻塞写入,如果失败则丢弃(避免阻塞调用线程)
+ _channel.Writer.TryWrite(entry);
+ }
+
+ ///
+ /// 刷新缓冲区,等待所有日志写入完成
+ ///
+ public void Flush()
+ {
+ if (_disposed) return;
+
+ // 等待 Channel 中的所有消息被处理
+ while (_channel.Reader.Count > 0)
+ {
+ Thread.Sleep(10);
+ }
+
+ _innerAppender.Flush();
+ }
+
+ ///
+ /// 后台处理日志的异步方法
+ ///
+ 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
+ {
+ // 忽略刷新错误
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/GFramework.Core/logging/appenders/ConsoleAppender.cs b/GFramework.Core/logging/appenders/ConsoleAppender.cs
new file mode 100644
index 0000000..b7aebe5
--- /dev/null
+++ b/GFramework.Core/logging/appenders/ConsoleAppender.cs
@@ -0,0 +1,91 @@
+using System.IO;
+using GFramework.Core.Abstractions.logging;
+
+namespace GFramework.Core.logging.appenders;
+
+///
+/// 控制台日志输出器
+///
+public sealed class ConsoleAppender : ILogAppender, IDisposable
+{
+ private readonly ILogFilter? _filter;
+ private readonly ILogFormatter _formatter;
+ private readonly bool _useColors;
+ private readonly TextWriter _writer;
+
+ ///
+ /// 创建控制台日志输出器
+ ///
+ /// 日志格式化器
+ /// 文本写入器(默认为 Console.Out)
+ /// 是否使用颜色(默认为 true)
+ /// 日志过滤器(可选)
+ 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;
+ }
+
+ ///
+ /// 释放资源
+ ///
+ public void Dispose()
+ {
+ _writer.Flush();
+ }
+
+ ///
+ /// 追加日志条目到控制台
+ ///
+ /// 日志条目
+ 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);
+ }
+ }
+
+ ///
+ /// 刷新控制台输出
+ ///
+ 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"
+ };
+ }
+}
\ No newline at end of file
diff --git a/GFramework.Core/logging/appenders/FileAppender.cs b/GFramework.Core/logging/appenders/FileAppender.cs
new file mode 100644
index 0000000..a92ea25
--- /dev/null
+++ b/GFramework.Core/logging/appenders/FileAppender.cs
@@ -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;
+
+///
+/// 文件日志输出器(线程安全)
+///
+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;
+
+ ///
+ /// 创建文件日志输出器
+ ///
+ /// 日志文件路径
+ /// 日志格式化器
+ /// 日志过滤器(可选)
+ 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();
+ }
+
+ ///
+ /// 释放资源
+ ///
+ public void Dispose()
+ {
+ if (_disposed) return;
+
+ lock (_lock)
+ {
+ _writer?.Flush();
+ _writer?.Dispose();
+ _writer = null;
+ _disposed = true;
+ }
+ }
+
+ ///
+ /// 追加日志条目到文件
+ ///
+ /// 日志条目
+ 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);
+ }
+ }
+
+ ///
+ /// 刷新文件缓冲区
+ ///
+ 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
+ };
+ }
+}
\ No newline at end of file
diff --git a/GFramework.Core/logging/appenders/RollingFileAppender.cs b/GFramework.Core/logging/appenders/RollingFileAppender.cs
new file mode 100644
index 0000000..34564b1
--- /dev/null
+++ b/GFramework.Core/logging/appenders/RollingFileAppender.cs
@@ -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;
+
+///
+/// 滚动文件日志输出器,支持按大小自动轮转日志文件
+///
+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;
+
+ ///
+ /// 创建滚动文件日志输出器
+ ///
+ /// 基础文件路径(例如: logs/app.log)
+ /// 单个文件最大大小(字节),默认 10MB
+ /// 保留的文件数量,默认 5
+ /// 日志格式化器
+ /// 日志过滤器(可选)
+ 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();
+ }
+
+ ///
+ /// 释放资源
+ ///
+ public void Dispose()
+ {
+ if (_disposed) return;
+
+ lock (_lock)
+ {
+ _writer?.Flush();
+ _writer?.Dispose();
+ _writer = null;
+ _disposed = true;
+ }
+ }
+
+ ///
+ /// 追加日志条目到文件
+ ///
+ /// 日志条目
+ 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;
+ }
+ }
+
+ ///
+ /// 刷新文件缓冲区
+ ///
+ public void Flush()
+ {
+ lock (_lock)
+ {
+ _writer?.Flush();
+ }
+ }
+
+ ///
+ /// 轮转日志文件
+ ///
+ 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();
+ }
+
+ ///
+ /// 获取轮转后的文件名
+ ///
+ /// 文件索引
+ /// 轮转后的文件路径
+ 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);
+ }
+
+ ///
+ /// 确保目录存在
+ ///
+ private void EnsureDirectoryExists()
+ {
+ var directory = Path.GetDirectoryName(_baseFilePath);
+ if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
+ {
+ Directory.CreateDirectory(directory);
+ }
+ }
+
+ ///
+ /// 初始化写入器
+ ///
+ private void InitializeWriter()
+ {
+ _writer = new StreamWriter(_baseFilePath, append: true, Encoding.UTF8)
+ {
+ AutoFlush = true
+ };
+
+ // 获取当前文件大小
+ _currentSize = File.Exists(_baseFilePath) ? new FileInfo(_baseFilePath).Length : 0;
+ }
+}
\ No newline at end of file
diff --git a/GFramework.Core/logging/filters/CompositeFilter.cs b/GFramework.Core/logging/filters/CompositeFilter.cs
new file mode 100644
index 0000000..b07f50c
--- /dev/null
+++ b/GFramework.Core/logging/filters/CompositeFilter.cs
@@ -0,0 +1,33 @@
+using GFramework.Core.Abstractions.logging;
+
+namespace GFramework.Core.logging.filters;
+
+///
+/// 组合多个过滤器的过滤器(AND 逻辑)
+///
+public sealed class CompositeFilter : ILogFilter
+{
+ private readonly ILogFilter[] _filters;
+
+ ///
+ /// 创建组合过滤器
+ ///
+ /// 要组合的过滤器列表
+ 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;
+ }
+
+ ///
+ /// 判断日志是否通过所有过滤器(AND 逻辑)
+ ///
+ /// 日志条目
+ /// 如果所有过滤器都返回 true 则返回 true
+ public bool ShouldLog(LogEntry entry)
+ {
+ return _filters.All(filter => filter.ShouldLog(entry));
+ }
+}
\ No newline at end of file
diff --git a/GFramework.Core/logging/filters/LogLevelFilter.cs b/GFramework.Core/logging/filters/LogLevelFilter.cs
new file mode 100644
index 0000000..7241d18
--- /dev/null
+++ b/GFramework.Core/logging/filters/LogLevelFilter.cs
@@ -0,0 +1,30 @@
+using GFramework.Core.Abstractions.logging;
+
+namespace GFramework.Core.logging.filters;
+
+///
+/// 按日志级别过滤的过滤器
+///
+public sealed class LogLevelFilter : ILogFilter
+{
+ private readonly LogLevel _minLevel;
+
+ ///
+ /// 创建日志级别过滤器
+ ///
+ /// 最小日志级别
+ public LogLevelFilter(LogLevel minLevel)
+ {
+ _minLevel = minLevel;
+ }
+
+ ///
+ /// 判断日志级别是否满足最小级别要求
+ ///
+ /// 日志条目
+ /// 如果日志级别大于等于最小级别返回 true
+ public bool ShouldLog(LogEntry entry)
+ {
+ return entry.Level >= _minLevel;
+ }
+}
\ No newline at end of file
diff --git a/GFramework.Core/logging/filters/NamespaceFilter.cs b/GFramework.Core/logging/filters/NamespaceFilter.cs
new file mode 100644
index 0000000..2d678d4
--- /dev/null
+++ b/GFramework.Core/logging/filters/NamespaceFilter.cs
@@ -0,0 +1,33 @@
+using GFramework.Core.Abstractions.logging;
+
+namespace GFramework.Core.logging.filters;
+
+///
+/// 按命名空间前缀过滤的过滤器
+///
+public sealed class NamespaceFilter : ILogFilter
+{
+ private readonly string[] _allowedPrefixes;
+
+ ///
+ /// 创建命名空间过滤器
+ ///
+ /// 允许的命名空间前缀列表
+ 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;
+ }
+
+ ///
+ /// 判断日志记录器名称是否匹配允许的命名空间前缀
+ ///
+ /// 日志条目
+ /// 如果匹配任一前缀返回 true
+ public bool ShouldLog(LogEntry entry)
+ {
+ return _allowedPrefixes.Any(prefix => entry.LoggerName.StartsWith(prefix, StringComparison.OrdinalIgnoreCase));
+ }
+}
\ No newline at end of file
diff --git a/GFramework.Core/logging/formatters/DefaultLogFormatter.cs b/GFramework.Core/logging/formatters/DefaultLogFormatter.cs
new file mode 100644
index 0000000..48aef88
--- /dev/null
+++ b/GFramework.Core/logging/formatters/DefaultLogFormatter.cs
@@ -0,0 +1,56 @@
+using System.Text;
+using GFramework.Core.Abstractions.logging;
+
+namespace GFramework.Core.logging.formatters;
+
+///
+/// 默认日志格式化器,保持与现有格式兼容
+///
+public sealed class DefaultLogFormatter : ILogFormatter
+{
+ private static readonly string[] LevelStrings =
+ [
+ "TRACE ",
+ "DEBUG ",
+ "INFO ",
+ "WARNING",
+ "ERROR ",
+ "FATAL "
+ ];
+
+ ///
+ /// 将日志条目格式化为默认格式
+ ///
+ /// 日志条目
+ /// 格式化后的日志字符串
+ 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();
+ }
+}
\ No newline at end of file
diff --git a/GFramework.Core/logging/formatters/JsonLogFormatter.cs b/GFramework.Core/logging/formatters/JsonLogFormatter.cs
new file mode 100644
index 0000000..83e6820
--- /dev/null
+++ b/GFramework.Core/logging/formatters/JsonLogFormatter.cs
@@ -0,0 +1,52 @@
+using System.Text.Json;
+using GFramework.Core.Abstractions.logging;
+
+namespace GFramework.Core.logging.formatters;
+
+///
+/// JSON 格式化器,将日志输出为 JSON 格式
+///
+public sealed class JsonLogFormatter : ILogFormatter
+{
+ private static readonly JsonSerializerOptions JsonOptions = new()
+ {
+ WriteIndented = false,
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase
+ };
+
+ ///
+ /// 将日志条目格式化为 JSON 格式
+ ///
+ /// 日志条目
+ /// JSON 格式的日志字符串
+ public string Format(LogEntry entry)
+ {
+ var logObject = new Dictionary
+ {
+ ["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);
+ }
+}
\ No newline at end of file
diff --git a/GFramework.Game/storage/FileStorage.cs b/GFramework.Game/storage/FileStorage.cs
index 003ebcd..a5dcc61 100644
--- a/GFramework.Game/storage/FileStorage.cs
+++ b/GFramework.Game/storage/FileStorage.cs
@@ -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>(Array.Empty());
+ return Task.FromResult>([]);
var dirs = Directory.GetDirectories(fullPath)
.Select(Path.GetFileName)
- .Where(name => !string.IsNullOrEmpty(name) && !name.StartsWith(".", StringComparison.Ordinal))
+ .OfType()
+ .Where(name => !string.IsNullOrEmpty(name) && !name.StartsWith('.'))
.ToList();
return Task.FromResult>(dirs);
@@ -262,6 +263,7 @@ public sealed class FileStorage : IFileStorage
var files = Directory.GetFiles(fullPath)
.Select(Path.GetFileName)
+ .OfType()
.Where(name => !string.IsNullOrEmpty(name))
.ToList();
diff --git a/GFramework.Godot/logging/GodotLogger.cs b/GFramework.Godot/logging/GodotLogger.cs
index ab1cc95..e4642bc 100644
--- a/GFramework.Godot/logging/GodotLogger.cs
+++ b/GFramework.Godot/logging/GodotLogger.cs
@@ -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 "
+ ];
+
///
/// 写入日志的核心方法。
/// 格式化日志消息并根据日志级别调用 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;
}
diff --git a/GFramework.Godot/logging/GodotLoggerFactoryProvider.cs b/GFramework.Godot/logging/GodotLoggerFactoryProvider.cs
index cf9aa98..d753805 100644
--- a/GFramework.Godot/logging/GodotLoggerFactoryProvider.cs
+++ b/GFramework.Godot/logging/GodotLoggerFactoryProvider.cs
@@ -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;
///
public sealed class GodotLoggerFactoryProvider : ILoggerFactoryProvider
{
+ private readonly ILoggerFactory _cachedFactory;
+
+ ///
+ /// 初始化Godot日志记录器工厂提供程序
+ ///
+ public GodotLoggerFactoryProvider()
+ {
+ _cachedFactory = new CachedLoggerFactory(new GodotLoggerFactory());
+ }
+
///
/// 获取或设置最小日志级别
///
public LogLevel MinLevel { get; set; }
///
- /// 创建指定名称的日志记录器实例
+ /// 创建指定名称的日志记录器实例(带缓存)
///
/// 日志记录器的名称
/// 返回配置了最小日志级别的Godot日志记录器实例
public ILogger CreateLogger(string name)
{
- return new GodotLoggerFactory().GetLogger(name, MinLevel);
+ return _cachedFactory.GetLogger(name, MinLevel);
}
}
\ No newline at end of file