From 1ba771e13a1272019ec08d77d105308a46160049 Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Thu, 26 Feb 2026 16:33:33 +0800 Subject: [PATCH] =?UTF-8?q?feat(logging):=20=E5=AE=9E=E7=8E=B0=E7=BB=93?= =?UTF-8?q?=E6=9E=84=E5=8C=96=E6=97=A5=E5=BF=97=E8=AE=B0=E5=BD=95=E5=92=8C?= =?UTF-8?q?=E5=BC=82=E6=AD=A5=E6=97=A5=E5=BF=97=E8=BE=93=E5=87=BA=E5=8A=9F?= =?UTF-8?q?=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将 AbstractLogger 实现从 ILogger 扩展为 IStructuredLogger 接口 - 添加通用日志方法 Log(LogLevel, string, params object[]) 支持格式化参数 - 实现结构化日志方法支持属性键值对记录 - 添加 ConsoleAppender、FileAppender 和 AsyncLogAppender 日志输出器 - 实现 CompositeFilter 过滤器和 DefaultLogFormatter、JsonLogFormatter 格式化器 - 在 ConsoleLogger 和 GodotLogger 中使用预缓存的日志级别字符串提升性能 - 使用 ANSI 颜色代码替代 ConsoleColor 实现跨平台日志着色 - 在 ConsoleLoggerFactoryProvider 和 GodotLoggerFactoryProvider 中添加日志工厂缓存 - 优化 FileStorage 中目录遍历使用 OfType() 类型转换 - 添加 LogContext 支持异步流中的结构化属性传递 --- .../logging/ILogAppender.cs | 18 ++ .../logging/ILogFilter.cs | 14 ++ .../logging/ILogFormatter.cs | 14 ++ .../logging/ILogger.cs | 44 ++++ .../logging/IStructuredLogger.cs | 24 ++ .../logging/LogContext.cs | 124 +++++++++++ .../logging/LogEntry.cs | 37 ++++ GFramework.Core/extensions/SpanExtensions.cs | 2 +- GFramework.Core/functional/Result.T.cs | 14 +- GFramework.Core/logging/AbstractLogger.cs | 107 +++++++-- .../logging/CachedLoggerFactory.cs | 34 +++ GFramework.Core/logging/CompositeLogger.cs | 134 ++++++++++++ GFramework.Core/logging/ConsoleLogger.cs | 47 ++-- .../logging/ConsoleLoggerFactoryProvider.cs | 14 +- .../logging/LoggingConfiguration.cs | 101 +++++++++ .../logging/LoggingConfigurationLoader.cs | 165 ++++++++++++++ .../logging/appenders/AsyncLogAppender.cs | 150 +++++++++++++ .../logging/appenders/ConsoleAppender.cs | 91 ++++++++ .../logging/appenders/FileAppender.cs | 105 +++++++++ .../logging/appenders/RollingFileAppender.cs | 207 ++++++++++++++++++ .../logging/filters/CompositeFilter.cs | 33 +++ .../logging/filters/LogLevelFilter.cs | 30 +++ .../logging/filters/NamespaceFilter.cs | 33 +++ .../logging/formatters/DefaultLogFormatter.cs | 56 +++++ .../logging/formatters/JsonLogFormatter.cs | 52 +++++ GFramework.Game/storage/FileStorage.cs | 6 +- GFramework.Godot/logging/GodotLogger.cs | 24 +- .../logging/GodotLoggerFactoryProvider.cs | 15 +- 28 files changed, 1648 insertions(+), 47 deletions(-) create mode 100644 GFramework.Core.Abstractions/logging/ILogAppender.cs create mode 100644 GFramework.Core.Abstractions/logging/ILogFilter.cs create mode 100644 GFramework.Core.Abstractions/logging/ILogFormatter.cs create mode 100644 GFramework.Core.Abstractions/logging/IStructuredLogger.cs create mode 100644 GFramework.Core.Abstractions/logging/LogContext.cs create mode 100644 GFramework.Core.Abstractions/logging/LogEntry.cs create mode 100644 GFramework.Core/logging/CachedLoggerFactory.cs create mode 100644 GFramework.Core/logging/CompositeLogger.cs create mode 100644 GFramework.Core/logging/LoggingConfiguration.cs create mode 100644 GFramework.Core/logging/LoggingConfigurationLoader.cs create mode 100644 GFramework.Core/logging/appenders/AsyncLogAppender.cs create mode 100644 GFramework.Core/logging/appenders/ConsoleAppender.cs create mode 100644 GFramework.Core/logging/appenders/FileAppender.cs create mode 100644 GFramework.Core/logging/appenders/RollingFileAppender.cs create mode 100644 GFramework.Core/logging/filters/CompositeFilter.cs create mode 100644 GFramework.Core/logging/filters/LogLevelFilter.cs create mode 100644 GFramework.Core/logging/filters/NamespaceFilter.cs create mode 100644 GFramework.Core/logging/formatters/DefaultLogFormatter.cs create mode 100644 GFramework.Core/logging/formatters/JsonLogFormatter.cs 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