mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-03-22 10:34:30 +08:00
feat(logging): 实现结构化日志记录和异步日志输出功能
- 将 AbstractLogger 实现从 ILogger 扩展为 IStructuredLogger 接口 - 添加通用日志方法 Log(LogLevel, string, params object[]) 支持格式化参数 - 实现结构化日志方法支持属性键值对记录 - 添加 ConsoleAppender、FileAppender 和 AsyncLogAppender 日志输出器 - 实现 CompositeFilter 过滤器和 DefaultLogFormatter、JsonLogFormatter 格式化器 - 在 ConsoleLogger 和 GodotLogger 中使用预缓存的日志级别字符串提升性能 - 使用 ANSI 颜色代码替代 ConsoleColor 实现跨平台日志着色 - 在 ConsoleLoggerFactoryProvider 和 GodotLoggerFactoryProvider 中添加日志工厂缓存 - 优化 FileStorage 中目录遍历使用 OfType<string>() 类型转换 - 添加 LogContext 支持异步流中的结构化属性传递
This commit is contained in:
parent
445513b784
commit
1ba771e13a
18
GFramework.Core.Abstractions/logging/ILogAppender.cs
Normal file
18
GFramework.Core.Abstractions/logging/ILogAppender.cs
Normal file
@ -0,0 +1,18 @@
|
||||
namespace GFramework.Core.Abstractions.logging;
|
||||
|
||||
/// <summary>
|
||||
/// 日志输出器接口,负责将日志条目写入特定目标
|
||||
/// </summary>
|
||||
public interface ILogAppender
|
||||
{
|
||||
/// <summary>
|
||||
/// 追加日志条目
|
||||
/// </summary>
|
||||
/// <param name="entry">日志条目</param>
|
||||
void Append(LogEntry entry);
|
||||
|
||||
/// <summary>
|
||||
/// 刷新缓冲区,确保所有日志已写入
|
||||
/// </summary>
|
||||
void Flush();
|
||||
}
|
||||
14
GFramework.Core.Abstractions/logging/ILogFilter.cs
Normal file
14
GFramework.Core.Abstractions/logging/ILogFilter.cs
Normal file
@ -0,0 +1,14 @@
|
||||
namespace GFramework.Core.Abstractions.logging;
|
||||
|
||||
/// <summary>
|
||||
/// 日志过滤器接口,用于决定是否应该记录某条日志
|
||||
/// </summary>
|
||||
public interface ILogFilter
|
||||
{
|
||||
/// <summary>
|
||||
/// 判断是否应该记录该日志条目
|
||||
/// </summary>
|
||||
/// <param name="entry">日志条目</param>
|
||||
/// <returns>如果应该记录返回 true,否则返回 false</returns>
|
||||
bool ShouldLog(LogEntry entry);
|
||||
}
|
||||
14
GFramework.Core.Abstractions/logging/ILogFormatter.cs
Normal file
14
GFramework.Core.Abstractions/logging/ILogFormatter.cs
Normal file
@ -0,0 +1,14 @@
|
||||
namespace GFramework.Core.Abstractions.logging;
|
||||
|
||||
/// <summary>
|
||||
/// 日志格式化器接口,用于将日志条目格式化为字符串
|
||||
/// </summary>
|
||||
public interface ILogFormatter
|
||||
{
|
||||
/// <summary>
|
||||
/// 将日志条目格式化为字符串
|
||||
/// </summary>
|
||||
/// <param name="entry">日志条目</param>
|
||||
/// <returns>格式化后的日志字符串</returns>
|
||||
string Format(LogEntry entry);
|
||||
}
|
||||
@ -309,4 +309,48 @@ public interface ILogger
|
||||
void Fatal(string msg, Exception t);
|
||||
|
||||
#endregion
|
||||
|
||||
#region Generic Log Methods
|
||||
|
||||
/// <summary>
|
||||
/// 使用指定的日志级别记录消息
|
||||
/// </summary>
|
||||
/// <param name="level">日志级别</param>
|
||||
/// <param name="message">要记录的消息字符串</param>
|
||||
void Log(LogLevel level, string message);
|
||||
|
||||
/// <summary>
|
||||
/// 使用指定的日志级别根据格式和参数记录消息
|
||||
/// </summary>
|
||||
/// <param name="level">日志级别</param>
|
||||
/// <param name="format">格式字符串</param>
|
||||
/// <param name="arg">参数</param>
|
||||
void Log(LogLevel level, string format, object arg);
|
||||
|
||||
/// <summary>
|
||||
/// 使用指定的日志级别根据格式和参数记录消息
|
||||
/// </summary>
|
||||
/// <param name="level">日志级别</param>
|
||||
/// <param name="format">格式字符串</param>
|
||||
/// <param name="arg1">第一个参数</param>
|
||||
/// <param name="arg2">第二个参数</param>
|
||||
void Log(LogLevel level, string format, object arg1, object arg2);
|
||||
|
||||
/// <summary>
|
||||
/// 使用指定的日志级别根据格式和参数数组记录消息
|
||||
/// </summary>
|
||||
/// <param name="level">日志级别</param>
|
||||
/// <param name="format">格式字符串</param>
|
||||
/// <param name="arguments">参数数组</param>
|
||||
void Log(LogLevel level, string format, params object[] arguments);
|
||||
|
||||
/// <summary>
|
||||
/// 使用指定的日志级别记录消息和异常
|
||||
/// </summary>
|
||||
/// <param name="level">日志级别</param>
|
||||
/// <param name="message">伴随异常的消息</param>
|
||||
/// <param name="exception">要记录的异常</param>
|
||||
void Log(LogLevel level, string message, Exception exception);
|
||||
|
||||
#endregion
|
||||
}
|
||||
24
GFramework.Core.Abstractions/logging/IStructuredLogger.cs
Normal file
24
GFramework.Core.Abstractions/logging/IStructuredLogger.cs
Normal file
@ -0,0 +1,24 @@
|
||||
namespace GFramework.Core.Abstractions.logging;
|
||||
|
||||
/// <summary>
|
||||
/// 支持结构化日志的日志记录器接口
|
||||
/// </summary>
|
||||
public interface IStructuredLogger : ILogger
|
||||
{
|
||||
/// <summary>
|
||||
/// 使用指定的日志级别记录消息和结构化属性
|
||||
/// </summary>
|
||||
/// <param name="level">日志级别</param>
|
||||
/// <param name="message">日志消息</param>
|
||||
/// <param name="properties">结构化属性键值对</param>
|
||||
void Log(LogLevel level, string message, params (string Key, object? Value)[] properties);
|
||||
|
||||
/// <summary>
|
||||
/// 使用指定的日志级别记录消息、异常和结构化属性
|
||||
/// </summary>
|
||||
/// <param name="level">日志级别</param>
|
||||
/// <param name="message">日志消息</param>
|
||||
/// <param name="exception">异常对象</param>
|
||||
/// <param name="properties">结构化属性键值对</param>
|
||||
void Log(LogLevel level, string message, Exception? exception, params (string Key, object? Value)[] properties);
|
||||
}
|
||||
124
GFramework.Core.Abstractions/logging/LogContext.cs
Normal file
124
GFramework.Core.Abstractions/logging/LogContext.cs
Normal file
@ -0,0 +1,124 @@
|
||||
namespace GFramework.Core.Abstractions.logging;
|
||||
|
||||
/// <summary>
|
||||
/// 日志上下文,用于在异步流中传递结构化属性
|
||||
/// </summary>
|
||||
public sealed class LogContext : IDisposable
|
||||
{
|
||||
private static readonly AsyncLocal<Dictionary<string, object?>?> _context = new();
|
||||
private readonly bool _hadPreviousValue;
|
||||
private readonly string _key;
|
||||
private readonly object? _previousValue;
|
||||
|
||||
private LogContext(string key, object? value)
|
||||
{
|
||||
_key = key;
|
||||
|
||||
var current = _context.Value;
|
||||
if (current != null && current.TryGetValue(key, out var prev))
|
||||
{
|
||||
_previousValue = prev;
|
||||
_hadPreviousValue = true;
|
||||
}
|
||||
|
||||
EnsureContext();
|
||||
_context.Value![key] = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前上下文中的所有属性
|
||||
/// </summary>
|
||||
public static IReadOnlyDictionary<string, object?> Current
|
||||
{
|
||||
get
|
||||
{
|
||||
var context = _context.Value;
|
||||
return context ??
|
||||
(IReadOnlyDictionary<string, object?>)new Dictionary<string, object?>(StringComparer.Ordinal);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 释放上下文,恢复之前的值
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
var current = _context.Value;
|
||||
if (current == null) return;
|
||||
|
||||
if (_hadPreviousValue)
|
||||
{
|
||||
current[_key] = _previousValue;
|
||||
}
|
||||
else
|
||||
{
|
||||
current.Remove(_key);
|
||||
if (current.Count == 0)
|
||||
{
|
||||
_context.Value = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 向当前上下文添加一个属性
|
||||
/// </summary>
|
||||
/// <param name="key">属性键</param>
|
||||
/// <param name="value">属性值</param>
|
||||
/// <returns>可释放的上下文对象,释放时会恢复之前的值</returns>
|
||||
public static IDisposable Push(string key, object? value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(key))
|
||||
throw new ArgumentException("Key cannot be null or whitespace.", nameof(key));
|
||||
|
||||
return new LogContext(key, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 向当前上下文添加多个属性
|
||||
/// </summary>
|
||||
/// <param name="properties">属性键值对</param>
|
||||
/// <returns>可释放的上下文对象,释放时会恢复之前的值</returns>
|
||||
public static IDisposable PushProperties(params (string Key, object? Value)[] properties)
|
||||
{
|
||||
if (properties == null || properties.Length == 0)
|
||||
throw new ArgumentException("Properties cannot be null or empty.", nameof(properties));
|
||||
|
||||
return new CompositeDisposable(properties.Select(p => Push(p.Key, p.Value)).ToArray());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清除当前上下文中的所有属性
|
||||
/// </summary>
|
||||
public static void Clear()
|
||||
{
|
||||
_context.Value = null;
|
||||
}
|
||||
|
||||
private static void EnsureContext()
|
||||
{
|
||||
_context.Value ??= new Dictionary<string, object?>(StringComparer.Ordinal);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 组合多个可释放对象
|
||||
/// </summary>
|
||||
private sealed class CompositeDisposable : IDisposable
|
||||
{
|
||||
private readonly IDisposable[] _disposables;
|
||||
|
||||
public CompositeDisposable(IDisposable[] disposables)
|
||||
{
|
||||
_disposables = disposables;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
// 按相反顺序释放
|
||||
for (int i = _disposables.Length - 1; i >= 0; i--)
|
||||
{
|
||||
_disposables[i].Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
37
GFramework.Core.Abstractions/logging/LogEntry.cs
Normal file
37
GFramework.Core.Abstractions/logging/LogEntry.cs
Normal file
@ -0,0 +1,37 @@
|
||||
namespace GFramework.Core.Abstractions.logging;
|
||||
|
||||
/// <summary>
|
||||
/// 日志条目,包含完整的日志信息
|
||||
/// </summary>
|
||||
public sealed record LogEntry(
|
||||
DateTime Timestamp,
|
||||
LogLevel Level,
|
||||
string LoggerName,
|
||||
string Message,
|
||||
Exception? Exception,
|
||||
IReadOnlyDictionary<string, object?>? Properties)
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取合并了上下文属性的所有属性
|
||||
/// </summary>
|
||||
/// <returns>包含日志属性和上下文属性的字典</returns>
|
||||
public IReadOnlyDictionary<string, object?> GetAllProperties()
|
||||
{
|
||||
var contextProps = LogContext.Current;
|
||||
|
||||
if (Properties == null || Properties.Count == 0)
|
||||
return contextProps;
|
||||
|
||||
if (contextProps.Count == 0)
|
||||
return Properties;
|
||||
|
||||
// 合并属性,日志属性优先
|
||||
var merged = new Dictionary<string, object?>(contextProps, StringComparer.Ordinal);
|
||||
foreach (var prop in Properties)
|
||||
{
|
||||
merged[prop.Key] = prop.Value;
|
||||
}
|
||||
|
||||
return merged;
|
||||
}
|
||||
}
|
||||
@ -21,7 +21,7 @@ public static class SpanExtensions
|
||||
/// }
|
||||
/// </code>
|
||||
/// </example>
|
||||
public static bool TryParseValue<T>(this ReadOnlySpan<char> span, out T result) where T : ISpanParsable<T>
|
||||
public static bool TryParseValue<T>(this ReadOnlySpan<char> span, out T? result) where T : ISpanParsable<T>
|
||||
{
|
||||
return T.TryParse(span, null, out result);
|
||||
}
|
||||
|
||||
@ -26,13 +26,23 @@ public readonly struct Result<A> : IEquatable<Result<A>>, IComparable<Result<A>>
|
||||
// ------------------------------------------------------------------ 状态枚举
|
||||
|
||||
/// <summary>
|
||||
/// 结果状态枚举,表示结果的不同状态
|
||||
/// 排序: Bottom < Faulted < Success
|
||||
/// 表示 Result 结构体的内部状态
|
||||
/// </summary>
|
||||
private enum ResultState : byte
|
||||
{
|
||||
/// <summary>
|
||||
/// 未初始化状态,表示 Result 尚未被赋值
|
||||
/// </summary>
|
||||
Bottom,
|
||||
|
||||
/// <summary>
|
||||
/// 失败状态,表示操作执行失败并包含异常信息
|
||||
/// </summary>
|
||||
Faulted,
|
||||
|
||||
/// <summary>
|
||||
/// 成功状态,表示操作执行成功并包含返回值
|
||||
/// </summary>
|
||||
Success
|
||||
}
|
||||
|
||||
|
||||
@ -8,7 +8,7 @@ namespace GFramework.Core.logging;
|
||||
/// </summary>
|
||||
public abstract class AbstractLogger(
|
||||
string? name = null,
|
||||
LogLevel minLevel = LogLevel.Info) : ILogger
|
||||
LogLevel minLevel = LogLevel.Info) : IStructuredLogger
|
||||
{
|
||||
/// <summary>
|
||||
/// 根日志记录器的名称常量
|
||||
@ -451,42 +451,121 @@ public abstract class AbstractLogger(
|
||||
|
||||
#endregion
|
||||
|
||||
#region Core Pipeline
|
||||
#region Generic Log Methods
|
||||
|
||||
/// <summary>
|
||||
/// 核心日志记录方法(无参数)
|
||||
/// 使用指定的日志级别记录消息
|
||||
/// </summary>
|
||||
/// <param name="level">日志级别</param>
|
||||
/// <param name="message">日志消息</param>
|
||||
private void Log(LogLevel level, string message)
|
||||
/// <param name="message">要记录的消息字符串</param>
|
||||
public void Log(LogLevel level, string message)
|
||||
{
|
||||
if (!IsEnabled(level)) return;
|
||||
Write(level, message, null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 核心日志记录方法(带参数格式化)
|
||||
/// 使用指定的日志级别根据格式和参数记录消息
|
||||
/// </summary>
|
||||
/// <param name="level">日志级别</param>
|
||||
/// <param name="format">格式化字符串</param>
|
||||
/// <param name="args">格式化参数数组</param>
|
||||
private void Log(LogLevel level, string format, params object[] args)
|
||||
/// <param name="format">格式字符串</param>
|
||||
/// <param name="arg">参数</param>
|
||||
public void Log(LogLevel level, string format, object arg)
|
||||
{
|
||||
if (!IsEnabled(level)) return;
|
||||
Write(level, string.Format(format, args), null);
|
||||
Write(level, string.Format(format, arg), null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 核心日志记录方法(带异常)
|
||||
/// 使用指定的日志级别根据格式和参数记录消息
|
||||
/// </summary>
|
||||
/// <param name="level">日志级别</param>
|
||||
/// <param name="message">日志消息</param>
|
||||
/// <param name="exception">异常对象</param>
|
||||
private void Log(LogLevel level, string message, Exception exception)
|
||||
/// <param name="format">格式字符串</param>
|
||||
/// <param name="arg1">第一个参数</param>
|
||||
/// <param name="arg2">第二个参数</param>
|
||||
public void Log(LogLevel level, string format, object arg1, object arg2)
|
||||
{
|
||||
if (!IsEnabled(level)) return;
|
||||
Write(level, string.Format(format, arg1, arg2), null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 使用指定的日志级别根据格式和参数数组记录消息
|
||||
/// </summary>
|
||||
/// <param name="level">日志级别</param>
|
||||
/// <param name="format">格式字符串</param>
|
||||
/// <param name="arguments">参数数组</param>
|
||||
public void Log(LogLevel level, string format, params object[] arguments)
|
||||
{
|
||||
if (!IsEnabled(level)) return;
|
||||
Write(level, string.Format(format, arguments), null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 使用指定的日志级别记录消息和异常
|
||||
/// </summary>
|
||||
/// <param name="level">日志级别</param>
|
||||
/// <param name="message">伴随异常的消息</param>
|
||||
/// <param name="exception">要记录的异常</param>
|
||||
public void Log(LogLevel level, string message, Exception exception)
|
||||
{
|
||||
if (!IsEnabled(level)) return;
|
||||
Write(level, message, exception);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Structured Log Methods
|
||||
|
||||
/// <summary>
|
||||
/// 使用指定的日志级别记录消息和结构化属性
|
||||
/// </summary>
|
||||
/// <param name="level">日志级别</param>
|
||||
/// <param name="message">日志消息</param>
|
||||
/// <param name="properties">结构化属性键值对</param>
|
||||
public virtual void Log(LogLevel level, string message, params (string Key, object? Value)[] properties)
|
||||
{
|
||||
if (!IsEnabled(level)) return;
|
||||
|
||||
// 默认实现:将属性附加到消息后面
|
||||
if (properties.Length > 0)
|
||||
{
|
||||
var propsStr = string.Join(", ", properties.Select(p => $"{p.Key}={p.Value}"));
|
||||
Write(level, $"{message} | {propsStr}", null);
|
||||
}
|
||||
else
|
||||
{
|
||||
Write(level, message, null);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 使用指定的日志级别记录消息、异常和结构化属性
|
||||
/// </summary>
|
||||
/// <param name="level">日志级别</param>
|
||||
/// <param name="message">日志消息</param>
|
||||
/// <param name="exception">异常对象</param>
|
||||
/// <param name="properties">结构化属性键值对</param>
|
||||
public virtual void Log(LogLevel level, string message, Exception? exception,
|
||||
params (string Key, object? Value)[] properties)
|
||||
{
|
||||
if (!IsEnabled(level)) return;
|
||||
|
||||
// 默认实现:将属性附加到消息后面
|
||||
if (properties.Length > 0)
|
||||
{
|
||||
var propsStr = string.Join(", ", properties.Select(p => $"{p.Key}={p.Value}"));
|
||||
Write(level, $"{message} | {propsStr}", exception);
|
||||
}
|
||||
else
|
||||
{
|
||||
Write(level, message, exception);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Core Pipeline (Private)
|
||||
|
||||
#endregion
|
||||
}
|
||||
34
GFramework.Core/logging/CachedLoggerFactory.cs
Normal file
34
GFramework.Core/logging/CachedLoggerFactory.cs
Normal file
@ -0,0 +1,34 @@
|
||||
using System.Collections.Concurrent;
|
||||
using GFramework.Core.Abstractions.logging;
|
||||
|
||||
namespace GFramework.Core.logging;
|
||||
|
||||
/// <summary>
|
||||
/// 带缓存的日志工厂包装器,避免重复创建相同名称的日志记录器实例
|
||||
/// </summary>
|
||||
public sealed class CachedLoggerFactory : ILoggerFactory
|
||||
{
|
||||
private readonly ConcurrentDictionary<string, ILogger> _cache = new();
|
||||
private readonly ILoggerFactory _innerFactory;
|
||||
|
||||
/// <summary>
|
||||
/// 创建缓存日志工厂实例
|
||||
/// </summary>
|
||||
/// <param name="innerFactory">内部日志工厂</param>
|
||||
public CachedLoggerFactory(ILoggerFactory innerFactory)
|
||||
{
|
||||
_innerFactory = innerFactory ?? throw new ArgumentNullException(nameof(innerFactory));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取或创建指定名称的日志记录器(带缓存)
|
||||
/// </summary>
|
||||
/// <param name="name">日志记录器名称</param>
|
||||
/// <param name="minLevel">最小日志级别</param>
|
||||
/// <returns>日志记录器实例</returns>
|
||||
public ILogger GetLogger(string name, LogLevel minLevel = LogLevel.Info)
|
||||
{
|
||||
var cacheKey = $"{name}:{minLevel}";
|
||||
return _cache.GetOrAdd(cacheKey, _ => _innerFactory.GetLogger(name, minLevel));
|
||||
}
|
||||
}
|
||||
134
GFramework.Core/logging/CompositeLogger.cs
Normal file
134
GFramework.Core/logging/CompositeLogger.cs
Normal file
@ -0,0 +1,134 @@
|
||||
using GFramework.Core.Abstractions.logging;
|
||||
|
||||
namespace GFramework.Core.logging;
|
||||
|
||||
/// <summary>
|
||||
/// 组合日志记录器,支持同时输出到多个 Appender
|
||||
/// </summary>
|
||||
public sealed class CompositeLogger : AbstractLogger, IDisposable
|
||||
{
|
||||
private readonly ILogAppender[] _appenders;
|
||||
|
||||
/// <summary>
|
||||
/// 创建组合日志记录器
|
||||
/// </summary>
|
||||
/// <param name="name">日志记录器名称</param>
|
||||
/// <param name="minLevel">最小日志级别</param>
|
||||
/// <param name="appenders">日志输出器列表</param>
|
||||
public CompositeLogger(
|
||||
string name,
|
||||
LogLevel minLevel,
|
||||
params ILogAppender[] appenders)
|
||||
: base(name, minLevel)
|
||||
{
|
||||
if (appenders == null || appenders.Length == 0)
|
||||
throw new ArgumentException("At least one appender must be provided.", nameof(appenders));
|
||||
|
||||
_appenders = appenders;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 释放所有 Appender 资源
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
foreach (var appender in _appenders)
|
||||
{
|
||||
if (appender is IDisposable disposable)
|
||||
{
|
||||
disposable.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 写入日志到所有 Appender
|
||||
/// </summary>
|
||||
/// <param name="level">日志级别</param>
|
||||
/// <param name="message">日志消息</param>
|
||||
/// <param name="exception">异常对象</param>
|
||||
protected override void Write(LogLevel level, string message, Exception? exception)
|
||||
{
|
||||
var entry = new LogEntry(
|
||||
DateTime.Now,
|
||||
level,
|
||||
Name(),
|
||||
message,
|
||||
exception,
|
||||
null);
|
||||
|
||||
foreach (var appender in _appenders)
|
||||
{
|
||||
appender.Append(entry);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 使用指定的日志级别记录消息和结构化属性
|
||||
/// </summary>
|
||||
/// <param name="level">日志级别</param>
|
||||
/// <param name="message">日志消息</param>
|
||||
/// <param name="properties">结构化属性键值对</param>
|
||||
public override void Log(LogLevel level, string message, params (string Key, object? Value)[] properties)
|
||||
{
|
||||
if (!IsEnabled(level)) return;
|
||||
|
||||
var propsDict = properties.Length > 0
|
||||
? properties.ToDictionary(p => p.Key, p => p.Value)
|
||||
: null;
|
||||
|
||||
var entry = new LogEntry(
|
||||
DateTime.Now,
|
||||
level,
|
||||
Name(),
|
||||
message,
|
||||
null,
|
||||
propsDict);
|
||||
|
||||
foreach (var appender in _appenders)
|
||||
{
|
||||
appender.Append(entry);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 使用指定的日志级别记录消息、异常和结构化属性
|
||||
/// </summary>
|
||||
/// <param name="level">日志级别</param>
|
||||
/// <param name="message">日志消息</param>
|
||||
/// <param name="exception">异常对象</param>
|
||||
/// <param name="properties">结构化属性键值对</param>
|
||||
public override void Log(LogLevel level, string message, Exception? exception,
|
||||
params (string Key, object? Value)[] properties)
|
||||
{
|
||||
if (!IsEnabled(level)) return;
|
||||
|
||||
var propsDict = properties.Length > 0
|
||||
? properties.ToDictionary(p => p.Key, p => p.Value)
|
||||
: null;
|
||||
|
||||
var entry = new LogEntry(
|
||||
DateTime.Now,
|
||||
level,
|
||||
Name(),
|
||||
message,
|
||||
exception,
|
||||
propsDict);
|
||||
|
||||
foreach (var appender in _appenders)
|
||||
{
|
||||
appender.Append(entry);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 刷新所有 Appender 的缓冲区
|
||||
/// </summary>
|
||||
public void Flush()
|
||||
{
|
||||
foreach (var appender in _appenders)
|
||||
{
|
||||
appender.Flush();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -12,6 +12,17 @@ public sealed class ConsoleLogger(
|
||||
TextWriter? writer = null,
|
||||
bool useColors = true) : AbstractLogger(name ?? RootLoggerName, minLevel)
|
||||
{
|
||||
// 静态缓存日志级别字符串,避免重复格式化
|
||||
private static readonly string[] LevelStrings =
|
||||
[
|
||||
"TRACE ",
|
||||
"DEBUG ",
|
||||
"INFO ",
|
||||
"WARNING",
|
||||
"ERROR ",
|
||||
"FATAL "
|
||||
];
|
||||
|
||||
private readonly bool _useColors = useColors && writer == Console.Out;
|
||||
private readonly TextWriter _writer = writer ?? Console.Out;
|
||||
|
||||
@ -24,7 +35,7 @@ public sealed class ConsoleLogger(
|
||||
protected override void Write(LogLevel level, string message, Exception? exception)
|
||||
{
|
||||
var timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff");
|
||||
var levelStr = level.ToString().ToUpper().PadRight(7);
|
||||
var levelStr = LevelStrings[(int)level];
|
||||
var log = $"[{timestamp}] {levelStr} [{Name()}] {message}";
|
||||
|
||||
// 添加异常信息到日志
|
||||
@ -39,40 +50,32 @@ public sealed class ConsoleLogger(
|
||||
#region Internal Core
|
||||
|
||||
/// <summary>
|
||||
/// 以指定颜色写入日志消息
|
||||
/// 以指定颜色写入日志消息(使用 ANSI 转义码)
|
||||
/// </summary>
|
||||
/// <param name="level">日志级别</param>
|
||||
/// <param name="message">日志消息</param>
|
||||
private void WriteColored(LogLevel level, string message)
|
||||
{
|
||||
var original = Console.ForegroundColor;
|
||||
try
|
||||
{
|
||||
Console.ForegroundColor = GetColor(level);
|
||||
_writer.WriteLine(message);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Console.ForegroundColor = original;
|
||||
}
|
||||
var colorCode = GetAnsiColorCode(level);
|
||||
_writer.WriteLine($"\x1b[{colorCode}m{message}\x1b[0m");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据日志级别获取对应的颜色
|
||||
/// 根据日志级别获取对应的 ANSI 颜色代码
|
||||
/// </summary>
|
||||
/// <param name="level">日志级别</param>
|
||||
/// <returns>控制台颜色</returns>
|
||||
private static ConsoleColor GetColor(LogLevel level)
|
||||
/// <returns>ANSI 颜色代码</returns>
|
||||
private static string GetAnsiColorCode(LogLevel level)
|
||||
{
|
||||
return level switch
|
||||
{
|
||||
LogLevel.Trace => ConsoleColor.DarkGray,
|
||||
LogLevel.Debug => ConsoleColor.Cyan,
|
||||
LogLevel.Info => ConsoleColor.White,
|
||||
LogLevel.Warning => ConsoleColor.Yellow,
|
||||
LogLevel.Error => ConsoleColor.Red,
|
||||
LogLevel.Fatal => ConsoleColor.Magenta,
|
||||
_ => ConsoleColor.White
|
||||
LogLevel.Trace => "90", // 暗灰色
|
||||
LogLevel.Debug => "36", // 青色
|
||||
LogLevel.Info => "37", // 白色
|
||||
LogLevel.Warning => "33", // 黄色
|
||||
LogLevel.Error => "31", // 红色
|
||||
LogLevel.Fatal => "35", // 洋红色
|
||||
_ => "37"
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -7,18 +7,28 @@ namespace GFramework.Core.logging;
|
||||
/// </summary>
|
||||
public sealed class ConsoleLoggerFactoryProvider : ILoggerFactoryProvider
|
||||
{
|
||||
private readonly ILoggerFactory _cachedFactory;
|
||||
|
||||
/// <summary>
|
||||
/// 初始化控制台日志记录器工厂提供程序
|
||||
/// </summary>
|
||||
public ConsoleLoggerFactoryProvider()
|
||||
{
|
||||
_cachedFactory = new CachedLoggerFactory(new ConsoleLoggerFactory());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置日志记录器的最小日志级别,低于此级别的日志将被忽略
|
||||
/// </summary>
|
||||
public LogLevel MinLevel { get; set; } = LogLevel.Info;
|
||||
|
||||
/// <summary>
|
||||
/// 创建一个日志记录器实例
|
||||
/// 创建一个日志记录器实例(带缓存)
|
||||
/// </summary>
|
||||
/// <param name="name">日志记录器的名称,用于标识特定的日志源</param>
|
||||
/// <returns>配置了指定名称和最小日志级别的ILogger实例</returns>
|
||||
public ILogger CreateLogger(string name)
|
||||
{
|
||||
return new ConsoleLoggerFactory().GetLogger(name, MinLevel);
|
||||
return _cachedFactory.GetLogger(name, MinLevel);
|
||||
}
|
||||
}
|
||||
101
GFramework.Core/logging/LoggingConfiguration.cs
Normal file
101
GFramework.Core/logging/LoggingConfiguration.cs
Normal file
@ -0,0 +1,101 @@
|
||||
using GFramework.Core.Abstractions.logging;
|
||||
|
||||
namespace GFramework.Core.logging;
|
||||
|
||||
/// <summary>
|
||||
/// 日志配置类
|
||||
/// </summary>
|
||||
public sealed class LoggingConfiguration
|
||||
{
|
||||
/// <summary>
|
||||
/// 全局最小日志级别
|
||||
/// </summary>
|
||||
public LogLevel MinLevel { get; set; } = LogLevel.Info;
|
||||
|
||||
/// <summary>
|
||||
/// Appender 配置列表
|
||||
/// </summary>
|
||||
public List<AppenderConfiguration> Appenders { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// 特定 Logger 的日志级别配置
|
||||
/// </summary>
|
||||
public Dictionary<string, LogLevel> LoggerLevels { get; set; } = new();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Appender 配置
|
||||
/// </summary>
|
||||
public sealed class AppenderConfiguration
|
||||
{
|
||||
/// <summary>
|
||||
/// Appender 类型(Console, File, RollingFile, Async)
|
||||
/// </summary>
|
||||
public string Type { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 格式化器类型(Default, Json)
|
||||
/// </summary>
|
||||
public string Formatter { get; set; } = "Default";
|
||||
|
||||
/// <summary>
|
||||
/// 文件路径(仅用于 File 和 RollingFile)
|
||||
/// </summary>
|
||||
public string? FilePath { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否使用颜色(仅用于 Console)
|
||||
/// </summary>
|
||||
public bool UseColors { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// 缓冲区大小(仅用于 Async)
|
||||
/// </summary>
|
||||
public int BufferSize { get; set; } = 10000;
|
||||
|
||||
/// <summary>
|
||||
/// 最大文件大小(仅用于 RollingFile,字节)
|
||||
/// </summary>
|
||||
public long MaxFileSize { get; set; } = 10 * 1024 * 1024; // 10MB
|
||||
|
||||
/// <summary>
|
||||
/// 最大文件数量(仅用于 RollingFile)
|
||||
/// </summary>
|
||||
public int MaxFileCount { get; set; } = 5;
|
||||
|
||||
/// <summary>
|
||||
/// 过滤器配置
|
||||
/// </summary>
|
||||
public FilterConfiguration? Filter { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 内部 Appender 配置(仅用于 Async)
|
||||
/// </summary>
|
||||
public AppenderConfiguration? InnerAppender { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 过滤器配置
|
||||
/// </summary>
|
||||
public sealed class FilterConfiguration
|
||||
{
|
||||
/// <summary>
|
||||
/// 过滤器类型(LogLevel, Namespace, Composite)
|
||||
/// </summary>
|
||||
public string Type { get; set; } = "LogLevel";
|
||||
|
||||
/// <summary>
|
||||
/// 最小日志级别(用于 LogLevel 过滤器)
|
||||
/// </summary>
|
||||
public LogLevel? MinLevel { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 命名空间前缀列表(用于 Namespace 过滤器)
|
||||
/// </summary>
|
||||
public List<string>? Namespaces { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 子过滤器列表(用于 Composite 过滤器)
|
||||
/// </summary>
|
||||
public List<FilterConfiguration>? Filters { get; set; }
|
||||
}
|
||||
165
GFramework.Core/logging/LoggingConfigurationLoader.cs
Normal file
165
GFramework.Core/logging/LoggingConfigurationLoader.cs
Normal file
@ -0,0 +1,165 @@
|
||||
using System.IO;
|
||||
using System.Text.Json;
|
||||
using GFramework.Core.Abstractions.logging;
|
||||
using GFramework.Core.logging.appenders;
|
||||
using GFramework.Core.logging.filters;
|
||||
using GFramework.Core.logging.formatters;
|
||||
|
||||
namespace GFramework.Core.logging;
|
||||
|
||||
/// <summary>
|
||||
/// 日志配置加载器
|
||||
/// </summary>
|
||||
public static class LoggingConfigurationLoader
|
||||
{
|
||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||
{
|
||||
PropertyNameCaseInsensitive = true,
|
||||
ReadCommentHandling = JsonCommentHandling.Skip,
|
||||
AllowTrailingCommas = true
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// 从 JSON 文件加载配置
|
||||
/// </summary>
|
||||
/// <param name="filePath">配置文件路径</param>
|
||||
/// <returns>日志配置对象</returns>
|
||||
public static LoggingConfiguration LoadFromJson(string filePath)
|
||||
{
|
||||
if (!File.Exists(filePath))
|
||||
throw new FileNotFoundException($"Configuration file not found: {filePath}");
|
||||
|
||||
var json = File.ReadAllText(filePath);
|
||||
var config = JsonSerializer.Deserialize<LoggingConfiguration>(json, JsonOptions);
|
||||
|
||||
return config ?? throw new InvalidOperationException("Failed to deserialize configuration.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从 JSON 字符串加载配置
|
||||
/// </summary>
|
||||
/// <param name="json">JSON 字符串</param>
|
||||
/// <returns>日志配置对象</returns>
|
||||
public static LoggingConfiguration LoadFromJsonString(string json)
|
||||
{
|
||||
var config = JsonSerializer.Deserialize<LoggingConfiguration>(json, JsonOptions);
|
||||
return config ?? throw new InvalidOperationException("Failed to deserialize configuration.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据配置创建 Logger 工厂
|
||||
/// </summary>
|
||||
/// <param name="config">日志配置</param>
|
||||
/// <returns>Logger 工厂</returns>
|
||||
public static ILoggerFactory CreateFactory(LoggingConfiguration config)
|
||||
{
|
||||
return new ConfigurableLoggerFactory(config);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据配置创建 Appender
|
||||
/// </summary>
|
||||
internal static ILogAppender CreateAppender(AppenderConfiguration config)
|
||||
{
|
||||
var formatter = CreateFormatter(config.Formatter);
|
||||
var filter = config.Filter != null ? CreateFilter(config.Filter) : null;
|
||||
|
||||
return config.Type.ToLowerInvariant() switch
|
||||
{
|
||||
"console" => new ConsoleAppender(formatter, useColors: config.UseColors, filter: filter),
|
||||
|
||||
"file" => new FileAppender(
|
||||
config.FilePath ?? throw new InvalidOperationException("FilePath is required for File appender."),
|
||||
formatter,
|
||||
filter),
|
||||
|
||||
"rollingfile" => new RollingFileAppender(
|
||||
config.FilePath ??
|
||||
throw new InvalidOperationException("FilePath is required for RollingFile appender."),
|
||||
config.MaxFileSize,
|
||||
config.MaxFileCount,
|
||||
formatter,
|
||||
filter),
|
||||
|
||||
"async" => new AsyncLogAppender(
|
||||
CreateAppender(config.InnerAppender ??
|
||||
throw new InvalidOperationException("InnerAppender is required for Async appender.")),
|
||||
config.BufferSize),
|
||||
|
||||
_ => throw new NotSupportedException($"Appender type '{config.Type}' is not supported.")
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据配置创建格式化器
|
||||
/// </summary>
|
||||
internal static ILogFormatter CreateFormatter(string formatterType)
|
||||
{
|
||||
return formatterType.ToLowerInvariant() switch
|
||||
{
|
||||
"default" => new DefaultLogFormatter(),
|
||||
"json" => new JsonLogFormatter(),
|
||||
_ => throw new NotSupportedException($"Formatter type '{formatterType}' is not supported.")
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据配置创建过滤器
|
||||
/// </summary>
|
||||
internal static ILogFilter CreateFilter(FilterConfiguration config)
|
||||
{
|
||||
return config.Type.ToLowerInvariant() switch
|
||||
{
|
||||
"loglevel" => new LogLevelFilter(
|
||||
config.MinLevel ?? throw new InvalidOperationException("MinLevel is required for LogLevel filter.")),
|
||||
|
||||
"namespace" => new NamespaceFilter(
|
||||
config.Namespaces?.ToArray() ??
|
||||
throw new InvalidOperationException("Namespaces is required for Namespace filter.")),
|
||||
|
||||
"composite" => new CompositeFilter(
|
||||
config.Filters?.Select(CreateFilter).ToArray() ??
|
||||
throw new InvalidOperationException("Filters is required for Composite filter.")),
|
||||
|
||||
_ => throw new NotSupportedException($"Filter type '{config.Type}' is not supported.")
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 可配置的 Logger 工厂
|
||||
/// </summary>
|
||||
internal sealed class ConfigurableLoggerFactory : ILoggerFactory
|
||||
{
|
||||
private readonly ILogAppender[] _appenders;
|
||||
private readonly LoggingConfiguration _config;
|
||||
|
||||
public ConfigurableLoggerFactory(LoggingConfiguration config)
|
||||
{
|
||||
_config = config ?? throw new ArgumentNullException(nameof(config));
|
||||
_appenders = config.Appenders.Select(LoggingConfigurationLoader.CreateAppender).ToArray();
|
||||
}
|
||||
|
||||
public ILogger GetLogger(string name, LogLevel minLevel = LogLevel.Info)
|
||||
{
|
||||
// 检查是否有特定 Logger 的级别配置
|
||||
var effectiveLevel = _config.LoggerLevels.TryGetValue(name, out var level)
|
||||
? level
|
||||
: _config.MinLevel;
|
||||
|
||||
// 如果没有 Appender,返回简单的 ConsoleLogger
|
||||
if (_appenders.Length == 0)
|
||||
{
|
||||
return new ConsoleLogger(name, effectiveLevel);
|
||||
}
|
||||
|
||||
// 如果只有一个 Appender 且是 ConsoleAppender,优化为 ConsoleLogger
|
||||
if (_appenders.Length == 1 && _appenders[0] is ConsoleAppender)
|
||||
{
|
||||
return new ConsoleLogger(name, effectiveLevel);
|
||||
}
|
||||
|
||||
// 返回 CompositeLogger
|
||||
return new CompositeLogger(name, effectiveLevel, _appenders);
|
||||
}
|
||||
}
|
||||
150
GFramework.Core/logging/appenders/AsyncLogAppender.cs
Normal file
150
GFramework.Core/logging/appenders/AsyncLogAppender.cs
Normal file
@ -0,0 +1,150 @@
|
||||
using System.Threading.Channels;
|
||||
using GFramework.Core.Abstractions.logging;
|
||||
|
||||
namespace GFramework.Core.logging.appenders;
|
||||
|
||||
/// <summary>
|
||||
/// 异步日志输出器,使用 Channel 实现非阻塞日志写入
|
||||
/// </summary>
|
||||
public sealed class AsyncLogAppender : ILogAppender, IDisposable
|
||||
{
|
||||
private readonly Channel<LogEntry> _channel;
|
||||
private readonly CancellationTokenSource _cts;
|
||||
private readonly ILogAppender _innerAppender;
|
||||
private readonly Task _processingTask;
|
||||
private bool _disposed;
|
||||
|
||||
/// <summary>
|
||||
/// 创建异步日志输出器
|
||||
/// </summary>
|
||||
/// <param name="innerAppender">内部日志输出器</param>
|
||||
/// <param name="bufferSize">缓冲区大小(默认 10000)</param>
|
||||
public AsyncLogAppender(ILogAppender innerAppender, int bufferSize = 10000)
|
||||
{
|
||||
_innerAppender = innerAppender ?? throw new ArgumentNullException(nameof(innerAppender));
|
||||
|
||||
if (bufferSize <= 0)
|
||||
throw new ArgumentException("Buffer size must be greater than 0.", nameof(bufferSize));
|
||||
|
||||
// 创建有界 Channel
|
||||
var options = new BoundedChannelOptions(bufferSize)
|
||||
{
|
||||
FullMode = BoundedChannelFullMode.Wait, // 缓冲区满时等待
|
||||
SingleReader = true,
|
||||
SingleWriter = false
|
||||
};
|
||||
|
||||
_channel = Channel.CreateBounded<LogEntry>(options);
|
||||
_cts = new CancellationTokenSource();
|
||||
|
||||
// 启动后台处理任务
|
||||
_processingTask = Task.Run(() => ProcessLogsAsync(_cts.Token));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前缓冲区中的日志数量
|
||||
/// </summary>
|
||||
public int PendingCount => _channel.Reader.Count;
|
||||
|
||||
/// <summary>
|
||||
/// 获取是否已完成处理
|
||||
/// </summary>
|
||||
public bool IsCompleted => _channel.Reader.Completion.IsCompleted;
|
||||
|
||||
/// <summary>
|
||||
/// 释放资源
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed) return;
|
||||
|
||||
// 标记 Channel 为完成状态
|
||||
_channel.Writer.Complete();
|
||||
|
||||
// 等待处理任务完成(最多等待 5 秒)
|
||||
if (!_processingTask.Wait(TimeSpan.FromSeconds(5)))
|
||||
{
|
||||
_cts.Cancel();
|
||||
}
|
||||
|
||||
// 释放内部 Appender
|
||||
if (_innerAppender is IDisposable disposable)
|
||||
{
|
||||
disposable.Dispose();
|
||||
}
|
||||
|
||||
_cts.Dispose();
|
||||
_disposed = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 追加日志条目(非阻塞)
|
||||
/// </summary>
|
||||
/// <param name="entry">日志条目</param>
|
||||
public void Append(LogEntry entry)
|
||||
{
|
||||
if (_disposed)
|
||||
throw new ObjectDisposedException(nameof(AsyncLogAppender));
|
||||
|
||||
// 尝试非阻塞写入,如果失败则丢弃(避免阻塞调用线程)
|
||||
_channel.Writer.TryWrite(entry);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 刷新缓冲区,等待所有日志写入完成
|
||||
/// </summary>
|
||||
public void Flush()
|
||||
{
|
||||
if (_disposed) return;
|
||||
|
||||
// 等待 Channel 中的所有消息被处理
|
||||
while (_channel.Reader.Count > 0)
|
||||
{
|
||||
Thread.Sleep(10);
|
||||
}
|
||||
|
||||
_innerAppender.Flush();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 后台处理日志的异步方法
|
||||
/// </summary>
|
||||
private async Task ProcessLogsAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
await foreach (var entry in _channel.Reader.ReadAllAsync(cancellationToken))
|
||||
{
|
||||
try
|
||||
{
|
||||
_innerAppender.Append(entry);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// 记录内部错误到控制台(避免递归)
|
||||
await Console.Error.WriteLineAsync($"[AsyncLogAppender] Error processing log entry: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// 正常取消,忽略
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await Console.Error.WriteLineAsync($"[AsyncLogAppender] Fatal error in processing task: {ex}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
// 确保最后刷新
|
||||
try
|
||||
{
|
||||
_innerAppender.Flush();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 忽略刷新错误
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
91
GFramework.Core/logging/appenders/ConsoleAppender.cs
Normal file
91
GFramework.Core/logging/appenders/ConsoleAppender.cs
Normal file
@ -0,0 +1,91 @@
|
||||
using System.IO;
|
||||
using GFramework.Core.Abstractions.logging;
|
||||
|
||||
namespace GFramework.Core.logging.appenders;
|
||||
|
||||
/// <summary>
|
||||
/// 控制台日志输出器
|
||||
/// </summary>
|
||||
public sealed class ConsoleAppender : ILogAppender, IDisposable
|
||||
{
|
||||
private readonly ILogFilter? _filter;
|
||||
private readonly ILogFormatter _formatter;
|
||||
private readonly bool _useColors;
|
||||
private readonly TextWriter _writer;
|
||||
|
||||
/// <summary>
|
||||
/// 创建控制台日志输出器
|
||||
/// </summary>
|
||||
/// <param name="formatter">日志格式化器</param>
|
||||
/// <param name="writer">文本写入器(默认为 Console.Out)</param>
|
||||
/// <param name="useColors">是否使用颜色(默认为 true)</param>
|
||||
/// <param name="filter">日志过滤器(可选)</param>
|
||||
public ConsoleAppender(
|
||||
ILogFormatter formatter,
|
||||
TextWriter? writer = null,
|
||||
bool useColors = true,
|
||||
ILogFilter? filter = null)
|
||||
{
|
||||
_formatter = formatter ?? throw new ArgumentNullException(nameof(formatter));
|
||||
_writer = writer ?? Console.Out;
|
||||
_useColors = useColors && _writer == Console.Out;
|
||||
_filter = filter;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 释放资源
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
_writer.Flush();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 追加日志条目到控制台
|
||||
/// </summary>
|
||||
/// <param name="entry">日志条目</param>
|
||||
public void Append(LogEntry entry)
|
||||
{
|
||||
if (_filter != null && !_filter.ShouldLog(entry))
|
||||
return;
|
||||
|
||||
var message = _formatter.Format(entry);
|
||||
|
||||
if (_useColors)
|
||||
{
|
||||
WriteColored(entry.Level, message);
|
||||
}
|
||||
else
|
||||
{
|
||||
_writer.WriteLine(message);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 刷新控制台输出
|
||||
/// </summary>
|
||||
public void Flush()
|
||||
{
|
||||
_writer.Flush();
|
||||
}
|
||||
|
||||
private void WriteColored(LogLevel level, string message)
|
||||
{
|
||||
var colorCode = GetAnsiColorCode(level);
|
||||
_writer.WriteLine($"\x1b[{colorCode}m{message}\x1b[0m");
|
||||
}
|
||||
|
||||
private static string GetAnsiColorCode(LogLevel level)
|
||||
{
|
||||
return level switch
|
||||
{
|
||||
LogLevel.Trace => "90", // 暗灰色
|
||||
LogLevel.Debug => "36", // 青色
|
||||
LogLevel.Info => "37", // 白色
|
||||
LogLevel.Warning => "33", // 黄色
|
||||
LogLevel.Error => "31", // 红色
|
||||
LogLevel.Fatal => "35", // 洋红色
|
||||
_ => "37"
|
||||
};
|
||||
}
|
||||
}
|
||||
105
GFramework.Core/logging/appenders/FileAppender.cs
Normal file
105
GFramework.Core/logging/appenders/FileAppender.cs
Normal file
@ -0,0 +1,105 @@
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using GFramework.Core.Abstractions.logging;
|
||||
using GFramework.Core.logging.formatters;
|
||||
|
||||
namespace GFramework.Core.logging.appenders;
|
||||
|
||||
/// <summary>
|
||||
/// 文件日志输出器(线程安全)
|
||||
/// </summary>
|
||||
public sealed class FileAppender : ILogAppender, IDisposable
|
||||
{
|
||||
private readonly string _filePath;
|
||||
private readonly ILogFilter? _filter;
|
||||
private readonly ILogFormatter _formatter;
|
||||
private readonly object _lock = new();
|
||||
private bool _disposed;
|
||||
private StreamWriter? _writer;
|
||||
|
||||
/// <summary>
|
||||
/// 创建文件日志输出器
|
||||
/// </summary>
|
||||
/// <param name="filePath">日志文件路径</param>
|
||||
/// <param name="formatter">日志格式化器</param>
|
||||
/// <param name="filter">日志过滤器(可选)</param>
|
||||
public FileAppender(
|
||||
string filePath,
|
||||
ILogFormatter? formatter = null,
|
||||
ILogFilter? filter = null)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(filePath))
|
||||
throw new ArgumentException("File path cannot be null or whitespace.", nameof(filePath));
|
||||
|
||||
_filePath = filePath;
|
||||
_formatter = formatter ?? new DefaultLogFormatter();
|
||||
_filter = filter;
|
||||
|
||||
EnsureDirectoryExists();
|
||||
InitializeWriter();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 释放资源
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed) return;
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
_writer?.Flush();
|
||||
_writer?.Dispose();
|
||||
_writer = null;
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 追加日志条目到文件
|
||||
/// </summary>
|
||||
/// <param name="entry">日志条目</param>
|
||||
public void Append(LogEntry entry)
|
||||
{
|
||||
if (_disposed)
|
||||
throw new ObjectDisposedException(nameof(FileAppender));
|
||||
|
||||
if (_filter != null && !_filter.ShouldLog(entry))
|
||||
return;
|
||||
|
||||
var message = _formatter.Format(entry);
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
_writer?.WriteLine(message);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 刷新文件缓冲区
|
||||
/// </summary>
|
||||
public void Flush()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
_writer?.Flush();
|
||||
}
|
||||
}
|
||||
|
||||
private void EnsureDirectoryExists()
|
||||
{
|
||||
var directory = Path.GetDirectoryName(_filePath);
|
||||
if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
|
||||
{
|
||||
Directory.CreateDirectory(directory);
|
||||
}
|
||||
}
|
||||
|
||||
private void InitializeWriter()
|
||||
{
|
||||
_writer = new StreamWriter(_filePath, append: true, Encoding.UTF8)
|
||||
{
|
||||
AutoFlush = true
|
||||
};
|
||||
}
|
||||
}
|
||||
207
GFramework.Core/logging/appenders/RollingFileAppender.cs
Normal file
207
GFramework.Core/logging/appenders/RollingFileAppender.cs
Normal file
@ -0,0 +1,207 @@
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using GFramework.Core.Abstractions.logging;
|
||||
using GFramework.Core.logging.formatters;
|
||||
|
||||
namespace GFramework.Core.logging.appenders;
|
||||
|
||||
/// <summary>
|
||||
/// 滚动文件日志输出器,支持按大小自动轮转日志文件
|
||||
/// </summary>
|
||||
public sealed class RollingFileAppender : ILogAppender, IDisposable
|
||||
{
|
||||
private readonly string _baseFilePath;
|
||||
private readonly ILogFilter? _filter;
|
||||
private readonly ILogFormatter _formatter;
|
||||
private readonly object _lock = new();
|
||||
private readonly int _maxFileCount;
|
||||
private readonly long _maxFileSize;
|
||||
private long _currentSize;
|
||||
private bool _disposed;
|
||||
private StreamWriter? _writer;
|
||||
|
||||
/// <summary>
|
||||
/// 创建滚动文件日志输出器
|
||||
/// </summary>
|
||||
/// <param name="baseFilePath">基础文件路径(例如: logs/app.log)</param>
|
||||
/// <param name="maxFileSize">单个文件最大大小(字节),默认 10MB</param>
|
||||
/// <param name="maxFileCount">保留的文件数量,默认 5</param>
|
||||
/// <param name="formatter">日志格式化器</param>
|
||||
/// <param name="filter">日志过滤器(可选)</param>
|
||||
public RollingFileAppender(
|
||||
string baseFilePath,
|
||||
long maxFileSize = 10 * 1024 * 1024,
|
||||
int maxFileCount = 5,
|
||||
ILogFormatter? formatter = null,
|
||||
ILogFilter? filter = null)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(baseFilePath))
|
||||
throw new ArgumentException("Base file path cannot be null or whitespace.", nameof(baseFilePath));
|
||||
|
||||
if (maxFileSize <= 0)
|
||||
throw new ArgumentException("Max file size must be greater than 0.", nameof(maxFileSize));
|
||||
|
||||
if (maxFileCount <= 0)
|
||||
throw new ArgumentException("Max file count must be greater than 0.", nameof(maxFileCount));
|
||||
|
||||
_baseFilePath = baseFilePath;
|
||||
_maxFileSize = maxFileSize;
|
||||
_maxFileCount = maxFileCount;
|
||||
_formatter = formatter ?? new DefaultLogFormatter();
|
||||
_filter = filter;
|
||||
|
||||
EnsureDirectoryExists();
|
||||
InitializeWriter();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 释放资源
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed) return;
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
_writer?.Flush();
|
||||
_writer?.Dispose();
|
||||
_writer = null;
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 追加日志条目到文件
|
||||
/// </summary>
|
||||
/// <param name="entry">日志条目</param>
|
||||
public void Append(LogEntry entry)
|
||||
{
|
||||
if (_disposed)
|
||||
throw new ObjectDisposedException(nameof(RollingFileAppender));
|
||||
|
||||
if (_filter != null && !_filter.ShouldLog(entry))
|
||||
return;
|
||||
|
||||
var message = _formatter.Format(entry);
|
||||
var messageBytes = Encoding.UTF8.GetByteCount(message) + Environment.NewLine.Length;
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
// 检查是否需要轮转
|
||||
if (_currentSize + messageBytes > _maxFileSize)
|
||||
{
|
||||
RollFiles();
|
||||
}
|
||||
|
||||
_writer?.WriteLine(message);
|
||||
_currentSize += messageBytes;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 刷新文件缓冲区
|
||||
/// </summary>
|
||||
public void Flush()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
_writer?.Flush();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 轮转日志文件
|
||||
/// </summary>
|
||||
private void RollFiles()
|
||||
{
|
||||
// 关闭当前文件
|
||||
_writer?.Flush();
|
||||
_writer?.Dispose();
|
||||
_writer = null;
|
||||
|
||||
// 删除最旧的文件(如果存在)
|
||||
var oldestFile = GetRolledFileName(_maxFileCount - 1);
|
||||
if (File.Exists(oldestFile))
|
||||
{
|
||||
try
|
||||
{
|
||||
File.Delete(oldestFile);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 忽略删除错误
|
||||
}
|
||||
}
|
||||
|
||||
// 重命名现有文件: app.log -> app.1.log -> app.2.log -> ...
|
||||
for (int i = _maxFileCount - 2; i >= 0; i--)
|
||||
{
|
||||
var sourceFile = i == 0 ? _baseFilePath : GetRolledFileName(i);
|
||||
var targetFile = GetRolledFileName(i + 1);
|
||||
|
||||
if (File.Exists(sourceFile))
|
||||
{
|
||||
try
|
||||
{
|
||||
if (File.Exists(targetFile))
|
||||
{
|
||||
File.Delete(targetFile);
|
||||
}
|
||||
|
||||
File.Move(sourceFile, targetFile);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 忽略移动错误
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 重新初始化写入器
|
||||
InitializeWriter();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取轮转后的文件名
|
||||
/// </summary>
|
||||
/// <param name="index">文件索引</param>
|
||||
/// <returns>轮转后的文件路径</returns>
|
||||
private string GetRolledFileName(int index)
|
||||
{
|
||||
var directory = Path.GetDirectoryName(_baseFilePath);
|
||||
var fileNameWithoutExt = Path.GetFileNameWithoutExtension(_baseFilePath);
|
||||
var extension = Path.GetExtension(_baseFilePath);
|
||||
|
||||
var rolledFileName = $"{fileNameWithoutExt}.{index}{extension}";
|
||||
|
||||
return string.IsNullOrEmpty(directory)
|
||||
? rolledFileName
|
||||
: Path.Combine(directory, rolledFileName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 确保目录存在
|
||||
/// </summary>
|
||||
private void EnsureDirectoryExists()
|
||||
{
|
||||
var directory = Path.GetDirectoryName(_baseFilePath);
|
||||
if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
|
||||
{
|
||||
Directory.CreateDirectory(directory);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 初始化写入器
|
||||
/// </summary>
|
||||
private void InitializeWriter()
|
||||
{
|
||||
_writer = new StreamWriter(_baseFilePath, append: true, Encoding.UTF8)
|
||||
{
|
||||
AutoFlush = true
|
||||
};
|
||||
|
||||
// 获取当前文件大小
|
||||
_currentSize = File.Exists(_baseFilePath) ? new FileInfo(_baseFilePath).Length : 0;
|
||||
}
|
||||
}
|
||||
33
GFramework.Core/logging/filters/CompositeFilter.cs
Normal file
33
GFramework.Core/logging/filters/CompositeFilter.cs
Normal file
@ -0,0 +1,33 @@
|
||||
using GFramework.Core.Abstractions.logging;
|
||||
|
||||
namespace GFramework.Core.logging.filters;
|
||||
|
||||
/// <summary>
|
||||
/// 组合多个过滤器的过滤器(AND 逻辑)
|
||||
/// </summary>
|
||||
public sealed class CompositeFilter : ILogFilter
|
||||
{
|
||||
private readonly ILogFilter[] _filters;
|
||||
|
||||
/// <summary>
|
||||
/// 创建组合过滤器
|
||||
/// </summary>
|
||||
/// <param name="filters">要组合的过滤器列表</param>
|
||||
public CompositeFilter(params ILogFilter[] filters)
|
||||
{
|
||||
if (filters == null || filters.Length == 0)
|
||||
throw new ArgumentException("At least one filter must be provided.", nameof(filters));
|
||||
|
||||
_filters = filters;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 判断日志是否通过所有过滤器(AND 逻辑)
|
||||
/// </summary>
|
||||
/// <param name="entry">日志条目</param>
|
||||
/// <returns>如果所有过滤器都返回 true 则返回 true</returns>
|
||||
public bool ShouldLog(LogEntry entry)
|
||||
{
|
||||
return _filters.All(filter => filter.ShouldLog(entry));
|
||||
}
|
||||
}
|
||||
30
GFramework.Core/logging/filters/LogLevelFilter.cs
Normal file
30
GFramework.Core/logging/filters/LogLevelFilter.cs
Normal file
@ -0,0 +1,30 @@
|
||||
using GFramework.Core.Abstractions.logging;
|
||||
|
||||
namespace GFramework.Core.logging.filters;
|
||||
|
||||
/// <summary>
|
||||
/// 按日志级别过滤的过滤器
|
||||
/// </summary>
|
||||
public sealed class LogLevelFilter : ILogFilter
|
||||
{
|
||||
private readonly LogLevel _minLevel;
|
||||
|
||||
/// <summary>
|
||||
/// 创建日志级别过滤器
|
||||
/// </summary>
|
||||
/// <param name="minLevel">最小日志级别</param>
|
||||
public LogLevelFilter(LogLevel minLevel)
|
||||
{
|
||||
_minLevel = minLevel;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 判断日志级别是否满足最小级别要求
|
||||
/// </summary>
|
||||
/// <param name="entry">日志条目</param>
|
||||
/// <returns>如果日志级别大于等于最小级别返回 true</returns>
|
||||
public bool ShouldLog(LogEntry entry)
|
||||
{
|
||||
return entry.Level >= _minLevel;
|
||||
}
|
||||
}
|
||||
33
GFramework.Core/logging/filters/NamespaceFilter.cs
Normal file
33
GFramework.Core/logging/filters/NamespaceFilter.cs
Normal file
@ -0,0 +1,33 @@
|
||||
using GFramework.Core.Abstractions.logging;
|
||||
|
||||
namespace GFramework.Core.logging.filters;
|
||||
|
||||
/// <summary>
|
||||
/// 按命名空间前缀过滤的过滤器
|
||||
/// </summary>
|
||||
public sealed class NamespaceFilter : ILogFilter
|
||||
{
|
||||
private readonly string[] _allowedPrefixes;
|
||||
|
||||
/// <summary>
|
||||
/// 创建命名空间过滤器
|
||||
/// </summary>
|
||||
/// <param name="allowedPrefixes">允许的命名空间前缀列表</param>
|
||||
public NamespaceFilter(params string[] allowedPrefixes)
|
||||
{
|
||||
if (allowedPrefixes == null || allowedPrefixes.Length == 0)
|
||||
throw new ArgumentException("At least one namespace prefix must be provided.", nameof(allowedPrefixes));
|
||||
|
||||
_allowedPrefixes = allowedPrefixes;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 判断日志记录器名称是否匹配允许的命名空间前缀
|
||||
/// </summary>
|
||||
/// <param name="entry">日志条目</param>
|
||||
/// <returns>如果匹配任一前缀返回 true</returns>
|
||||
public bool ShouldLog(LogEntry entry)
|
||||
{
|
||||
return _allowedPrefixes.Any(prefix => entry.LoggerName.StartsWith(prefix, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
}
|
||||
56
GFramework.Core/logging/formatters/DefaultLogFormatter.cs
Normal file
56
GFramework.Core/logging/formatters/DefaultLogFormatter.cs
Normal file
@ -0,0 +1,56 @@
|
||||
using System.Text;
|
||||
using GFramework.Core.Abstractions.logging;
|
||||
|
||||
namespace GFramework.Core.logging.formatters;
|
||||
|
||||
/// <summary>
|
||||
/// 默认日志格式化器,保持与现有格式兼容
|
||||
/// </summary>
|
||||
public sealed class DefaultLogFormatter : ILogFormatter
|
||||
{
|
||||
private static readonly string[] LevelStrings =
|
||||
[
|
||||
"TRACE ",
|
||||
"DEBUG ",
|
||||
"INFO ",
|
||||
"WARNING",
|
||||
"ERROR ",
|
||||
"FATAL "
|
||||
];
|
||||
|
||||
/// <summary>
|
||||
/// 将日志条目格式化为默认格式
|
||||
/// </summary>
|
||||
/// <param name="entry">日志条目</param>
|
||||
/// <returns>格式化后的日志字符串</returns>
|
||||
public string Format(LogEntry entry)
|
||||
{
|
||||
var timestamp = entry.Timestamp.ToString("yyyy-MM-dd HH:mm:ss.fff");
|
||||
var levelStr = LevelStrings[(int)entry.Level];
|
||||
var sb = new StringBuilder();
|
||||
|
||||
sb.Append('[').Append(timestamp).Append("] ")
|
||||
.Append(levelStr).Append(" [")
|
||||
.Append(entry.LoggerName).Append("] ")
|
||||
.Append(entry.Message);
|
||||
|
||||
// 添加结构化属性
|
||||
var properties = entry.GetAllProperties();
|
||||
if (properties.Count > 0)
|
||||
{
|
||||
sb.Append(" |");
|
||||
foreach (var prop in properties)
|
||||
{
|
||||
sb.Append(' ').Append(prop.Key).Append('=').Append(prop.Value);
|
||||
}
|
||||
}
|
||||
|
||||
// 添加异常信息
|
||||
if (entry.Exception != null)
|
||||
{
|
||||
sb.Append(Environment.NewLine).Append(entry.Exception);
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
52
GFramework.Core/logging/formatters/JsonLogFormatter.cs
Normal file
52
GFramework.Core/logging/formatters/JsonLogFormatter.cs
Normal file
@ -0,0 +1,52 @@
|
||||
using System.Text.Json;
|
||||
using GFramework.Core.Abstractions.logging;
|
||||
|
||||
namespace GFramework.Core.logging.formatters;
|
||||
|
||||
/// <summary>
|
||||
/// JSON 格式化器,将日志输出为 JSON 格式
|
||||
/// </summary>
|
||||
public sealed class JsonLogFormatter : ILogFormatter
|
||||
{
|
||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||
{
|
||||
WriteIndented = false,
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// 将日志条目格式化为 JSON 格式
|
||||
/// </summary>
|
||||
/// <param name="entry">日志条目</param>
|
||||
/// <returns>JSON 格式的日志字符串</returns>
|
||||
public string Format(LogEntry entry)
|
||||
{
|
||||
var logObject = new Dictionary<string, object?>
|
||||
{
|
||||
["timestamp"] = entry.Timestamp.ToString("O"), // ISO 8601 格式
|
||||
["level"] = entry.Level.ToString().ToUpper(),
|
||||
["logger"] = entry.LoggerName,
|
||||
["message"] = entry.Message
|
||||
};
|
||||
|
||||
// 添加结构化属性
|
||||
var properties = entry.GetAllProperties();
|
||||
if (properties.Count > 0)
|
||||
{
|
||||
logObject["properties"] = properties;
|
||||
}
|
||||
|
||||
// 添加异常信息
|
||||
if (entry.Exception != null)
|
||||
{
|
||||
logObject["exception"] = new
|
||||
{
|
||||
type = entry.Exception.GetType().FullName,
|
||||
message = entry.Exception.Message,
|
||||
stackTrace = entry.Exception.StackTrace
|
||||
};
|
||||
}
|
||||
|
||||
return JsonSerializer.Serialize(logObject, JsonOptions);
|
||||
}
|
||||
}
|
||||
@ -239,11 +239,12 @@ public sealed class FileStorage : IFileStorage
|
||||
{
|
||||
var fullPath = string.IsNullOrEmpty(path) ? _rootPath : Path.Combine(_rootPath, path);
|
||||
if (!Directory.Exists(fullPath))
|
||||
return Task.FromResult<IReadOnlyList<string>>(Array.Empty<string>());
|
||||
return Task.FromResult<IReadOnlyList<string>>([]);
|
||||
|
||||
var dirs = Directory.GetDirectories(fullPath)
|
||||
.Select(Path.GetFileName)
|
||||
.Where(name => !string.IsNullOrEmpty(name) && !name.StartsWith(".", StringComparison.Ordinal))
|
||||
.OfType<string>()
|
||||
.Where(name => !string.IsNullOrEmpty(name) && !name.StartsWith('.'))
|
||||
.ToList();
|
||||
|
||||
return Task.FromResult<IReadOnlyList<string>>(dirs);
|
||||
@ -262,6 +263,7 @@ public sealed class FileStorage : IFileStorage
|
||||
|
||||
var files = Directory.GetFiles(fullPath)
|
||||
.Select(Path.GetFileName)
|
||||
.OfType<string>()
|
||||
.Where(name => !string.IsNullOrEmpty(name))
|
||||
.ToList();
|
||||
|
||||
|
||||
@ -15,6 +15,17 @@ public sealed class GodotLogger(
|
||||
string? name = null,
|
||||
LogLevel minLevel = LogLevel.Info) : AbstractLogger(name ?? RootLoggerName, minLevel)
|
||||
{
|
||||
// 静态缓存日志级别字符串,避免重复格式化
|
||||
private static readonly string[] LevelStrings =
|
||||
[
|
||||
"TRACE ",
|
||||
"DEBUG ",
|
||||
"INFO ",
|
||||
"WARNING",
|
||||
"ERROR ",
|
||||
"FATAL "
|
||||
];
|
||||
|
||||
/// <summary>
|
||||
/// 写入日志的核心方法。
|
||||
/// 格式化日志消息并根据日志级别调用 Godot 的输出方法。
|
||||
@ -26,7 +37,7 @@ public sealed class GodotLogger(
|
||||
{
|
||||
// 构造时间戳和日志前缀
|
||||
var timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff");
|
||||
var levelStr = level.ToString().ToUpper().PadRight(7);
|
||||
var levelStr = LevelStrings[(int)level];
|
||||
var logPrefix = $"[{timestamp}] {levelStr} [{Name()}]";
|
||||
|
||||
// 添加异常信息到日志消息中
|
||||
@ -46,7 +57,16 @@ public sealed class GodotLogger(
|
||||
case LogLevel.Warning:
|
||||
GD.PushWarning(logMessage);
|
||||
break;
|
||||
default: // Trace / Debug / Info
|
||||
case LogLevel.Trace:
|
||||
GD.PrintRich($"[color=gray]{logMessage}[/color]");
|
||||
break;
|
||||
case LogLevel.Debug:
|
||||
GD.PrintRich($"[color=cyan]{logMessage}[/color]");
|
||||
break;
|
||||
case LogLevel.Info:
|
||||
GD.Print(logMessage);
|
||||
break;
|
||||
default:
|
||||
GD.Print(logMessage);
|
||||
break;
|
||||
}
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
using GFramework.Core.Abstractions.logging;
|
||||
using GFramework.Core.logging;
|
||||
|
||||
namespace GFramework.Godot.logging;
|
||||
|
||||
@ -7,18 +8,28 @@ namespace GFramework.Godot.logging;
|
||||
/// </summary>
|
||||
public sealed class GodotLoggerFactoryProvider : ILoggerFactoryProvider
|
||||
{
|
||||
private readonly ILoggerFactory _cachedFactory;
|
||||
|
||||
/// <summary>
|
||||
/// 初始化Godot日志记录器工厂提供程序
|
||||
/// </summary>
|
||||
public GodotLoggerFactoryProvider()
|
||||
{
|
||||
_cachedFactory = new CachedLoggerFactory(new GodotLoggerFactory());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置最小日志级别
|
||||
/// </summary>
|
||||
public LogLevel MinLevel { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 创建指定名称的日志记录器实例
|
||||
/// 创建指定名称的日志记录器实例(带缓存)
|
||||
/// </summary>
|
||||
/// <param name="name">日志记录器的名称</param>
|
||||
/// <returns>返回配置了最小日志级别的Godot日志记录器实例</returns>
|
||||
public ILogger CreateLogger(string name)
|
||||
{
|
||||
return new GodotLoggerFactory().GetLogger(name, MinLevel);
|
||||
return _cachedFactory.GetLogger(name, MinLevel);
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user