diff --git a/README.md b/README.md index ca61ee7..2cf5257 100644 --- a/README.md +++ b/README.md @@ -110,7 +110,8 @@ internal static class LoggerStartup // 设置日志级别为 Debug。 .WithLevel(LogLevel.Debug) // 添加一个控制台日志写入器,这样控制台里就可以看到日志输出了。 - .AddWriter(new ConsoleLogger() + .AddConsoleLogger(b => b + .WithThreadSafe(LogWritingThreadMode.ProducerConsumer) .FilterConsoleTagsFromCommandLineArgs(args)) // 如果有一些库使用了本日志框架(使用源生成器,不带依赖的那种),那么可以通过这个方法将它们的日志桥接到本日志框架中。 .AddBridge(LoggerBridgeLinker.Default) diff --git a/samples/LoggerSample.MainApp/LoggerSample.MainApp.csproj b/samples/LoggerSample.MainApp/LoggerSample.MainApp.csproj index 7788b98..c2e7e96 100644 --- a/samples/LoggerSample.MainApp/LoggerSample.MainApp.csproj +++ b/samples/LoggerSample.MainApp/LoggerSample.MainApp.csproj @@ -1,7 +1,7 @@  - WinExe + Exe net8.0 preferReference diff --git a/samples/LoggerSample.MainApp/Program.cs b/samples/LoggerSample.MainApp/Program.cs index d3684d8..fec6569 100644 --- a/samples/LoggerSample.MainApp/Program.cs +++ b/samples/LoggerSample.MainApp/Program.cs @@ -1,4 +1,8 @@ -using dotnetCampus.Logging.Attributes; +using System; +using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; +using dotnetCampus.Logging.Attributes; using dotnetCampus.Logging.Configurations; using dotnetCampus.Logging.Writers; @@ -21,16 +25,27 @@ public static void Main(string[] args) { LogLevel = LogLevel.Debug, }) - .AddWriter(new ConsoleLogger - { - // Options = new ConsoleLoggerOptions - // { - // IncludeScopes = true, - // }, - }) + .AddConsoleLogger(b => b + .WithThreadSafe(LogWritingThreadMode.ProducerConsumer) + .FilterConsoleTagsFromCommandLineArgs(args)) .AddBridge(LoggerBridgeLinker.Default) .Build() .IntoGlobalStaticLog(); + + Run(); + Thread.Sleep(5000); + } + + private static void Run() + { + var stopwatch = Stopwatch.StartNew(); + Log.Debug($"[TEST] 开始 {stopwatch.ElapsedMilliseconds}ms"); + Parallel.For(0, 0x00004000, i => + { + Thread.Sleep(0); + Log.Debug($"[TEST] {DateTime.Now:HH:mm:ss}"); + }); + Log.Debug($"[TEST] 完成 {stopwatch.ElapsedMilliseconds}ms"); } } diff --git a/src/dotnetCampus.Logger/LoggerBuilder.cs b/src/dotnetCampus.Logger/LoggerBuilder.cs index 3fcc2cb..945e32c 100644 --- a/src/dotnetCampus.Logger/LoggerBuilder.cs +++ b/src/dotnetCampus.Logger/LoggerBuilder.cs @@ -1,15 +1,13 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using dotnetCampus.Logging.Bridges; using dotnetCampus.Logging.Configurations; -using dotnetCampus.Logging.Writers; namespace dotnetCampus.Logging; /// /// 辅助创建日志记录器的构建器。 /// -public class LoggerBuilder +public sealed class LoggerBuilder { private LogOptions? _options; private readonly List _writers = []; diff --git a/src/dotnetCampus.Logger/Writers/ConsoleLogger.cs b/src/dotnetCampus.Logger/Writers/ConsoleLogger.cs index 24cc3ad..db38b54 100644 --- a/src/dotnetCampus.Logger/Writers/ConsoleLogger.cs +++ b/src/dotnetCampus.Logger/Writers/ConsoleLogger.cs @@ -8,6 +8,9 @@ namespace dotnetCampus.Logging.Writers; +/// +/// 在控制台输出日志的日志记录器。 +/// public class ConsoleLogger : ILogger { /// @@ -16,18 +19,40 @@ public class ConsoleLogger : ILogger private int _isCursorMovementEnabled = 3; private readonly RepeatLoggerDetector _repeat; - private TagFilterManager? _tagFilterManager; /// - /// 高于或等于此级别的日志才会被记录。 + /// 创建一个 的新实例。 /// - public LogLevel Level { get; set; } + /// 指定控制台日志的线程安全模式。 + /// Main 方法的参数。 + public ConsoleLogger(LogWritingThreadMode threadMode = LogWritingThreadMode.NotThreadSafe, string[]? mainArgs = null) + : this(threadMode.CreateCoreLogWriter(), TagFilterManager.FromCommandLineArgs(mainArgs ?? [])) + { + } - public ConsoleLogger() + internal ConsoleLogger(ICoreLogWriter coreWriter, TagFilterManager? tagManager) { - _repeat = new(ClearAndMoveToLastLine); + _repeat = new RepeatLoggerDetector(ClearAndMoveToLastLine); + CoreWriter = coreWriter; + TagManager = tagManager; } + /// + /// 高于或等于此级别的日志才会被记录。 + /// + public LogLevel Level { get; init; } + + /// + /// 最终日志写入器。 + /// + private ICoreLogWriter CoreWriter { get; } + + /// + /// 管理控制台日志的标签过滤。 + /// + private TagFilterManager? TagManager { get; } + + /// public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) { if (logLevel < Level) @@ -36,24 +61,37 @@ public void Log(LogLevel logLevel, EventId eventId, TState state, Except } var message = formatter(state, exception); - if (_tagFilterManager?.IsTagEnabled(message) is false) + if (TagManager?.IsTagEnabled(message) is false) { return; } + var traceTag = TraceTag; + var debugTag = DebugTag; + var informationTag = InformationTag; + var warningTag = WarningTag; + var errorTag = ErrorTag; + var criticalTag = CriticalTag; LogCore(logLevel, exception, message, m => logLevel switch { - LogLevel.Trace => $"{TraceTag} {TraceText}{m}{Reset}", - LogLevel.Debug => $"{DebugTag} {DebugText}{m}{Reset}", - LogLevel.Information => $"{InformationTag} {InformationText}{m}{Reset}", - LogLevel.Warning => $"{WarningTag} {WarningText}{m}{Reset}", - LogLevel.Error => $"{ErrorTag} {ErrorText}{m}{Reset}", - LogLevel.Critical => $"{CriticalTag} {CriticalText}{m}{Reset}", + LogLevel.Trace => $"{traceTag} {TraceText}{m}{Reset}", + LogLevel.Debug => $"{debugTag} {DebugText}{m}{Reset}", + LogLevel.Information => $"{informationTag} {InformationText}{m}{Reset}", + LogLevel.Warning => $"{warningTag} {WarningText}{m}{Reset}", + LogLevel.Error => $"{errorTag} {ErrorText}{m}{Reset}", + LogLevel.Critical => $"{criticalTag} {CriticalText}{m}{Reset}", _ => null, }); } - private void LogCore(LogLevel logLevel, Exception? exception, string message, Func formatter) + /// + /// 记录日志。在必要的情况下会保证线程安全。 + /// + /// + /// + /// + /// + private void LogCore(LogLevel logLevel, Exception? exception, string message, Func formatter) => CoreWriter.Do(() => { if (_repeat.RepeatOrResetLastLog(logLevel, message, exception) is var count and > 1) { @@ -77,9 +115,15 @@ private void LogCore(LogLevel logLevel, Exception? exception, string message, Fu {tag}{exception} """, formatter); } - } + }); - private static void ConsoleMultilineMessage(string message, Func formatter, bool forceSingleLine = false) + /// + /// 记录多行日志。 + /// + /// + /// + /// + private void ConsoleMultilineMessage(string message, Func formatter, bool forceSingleLine = false) { if (forceSingleLine || !message.Contains('\n')) { @@ -96,46 +140,34 @@ private static void ConsoleMultilineMessage(string message, Func - /// 高于或等于此级别的日志才会被记录。 + /// 清空当前行并移动光标到上一行。 /// - public ConsoleLogger UseLevel(LogLevel level) - { - Level = level; - return this; - } - - /// - /// 从命令行参数中提取过滤标签。 - /// - /// 命令行参数。 - public ConsoleLogger FilterConsoleTagsFromCommandLineArgs(string[] args) - { - _tagFilterManager = TagFilterManager.FromCommandLineArgs(args); - return this; - } - + /// 此移动光标,是因为日志已重复第几次。 private void ClearAndMoveToLastLine(int repeatCount) { - if (_isCursorMovementEnabled > 0 && repeatCount > 2) + if (_isCursorMovementEnabled <= 0 || repeatCount <= 2) { - try - { - var desiredY = Console.CursorTop - 1; - var y = Math.Clamp(desiredY, 0, Console.WindowHeight - 1); - Console.SetCursorPosition(0, y); - Console.Write(new string(' ', Console.WindowWidth)); - Console.SetCursorPosition(0, y); - } - catch (IOException) - { - // 日志记录时,如果无法移动光标,说明可能当前输出位置不在缓冲区内。 - // 如果多次尝试失败,则认为当前控制台缓冲区不支持光标移动,遂放弃。 - _isCursorMovementEnabled--; - } - catch (ArgumentException) - { - // 日志记录时,有可能已经移动到头了,就不要移动了。 - } + // 如果光标控制不可用,或者还没有重复次数,则不尝试移动光标。 + return; + } + + try + { + var desiredY = Console.CursorTop - 1; + var y = Math.Clamp(desiredY, 0, Console.WindowHeight - 1); + Console.SetCursorPosition(0, y); + Console.Write(new string(' ', Console.WindowWidth)); + Console.SetCursorPosition(0, y); + } + catch (IOException) + { + // 日志记录时,如果无法移动光标,说明可能当前输出位置不在缓冲区内。 + // 如果多次尝试失败,则认为当前控制台缓冲区不支持光标移动,遂放弃。 + _isCursorMovementEnabled--; + } + catch (ArgumentException) + { + // 日志记录时,有可能已经移动到头了,就不要移动了。 } } diff --git a/src/dotnetCampus.Logger/Writers/ConsoleLoggerBuilder.cs b/src/dotnetCampus.Logger/Writers/ConsoleLoggerBuilder.cs new file mode 100644 index 0000000..5907290 --- /dev/null +++ b/src/dotnetCampus.Logger/Writers/ConsoleLoggerBuilder.cs @@ -0,0 +1,84 @@ +using System; +using dotnetCampus.Logging.Writers.Helpers; + +namespace dotnetCampus.Logging.Writers; + +/// +/// 辅助创建控制台日志记录器的构建器。 +/// +public sealed class ConsoleLoggerBuilder +{ + private TagFilterManager? _tagFilterManager; + private ICoreLogWriter _coreWriter = new NotThreadSafeLogWriter(); + + /// + /// 高于或等于此级别的日志才会被记录。 + /// + public LogLevel Level { get; set; } + + /// + /// 高于或等于此级别的日志才会被记录。 + /// + public ConsoleLoggerBuilder WithLevel(LogLevel level) + { + Level = level; + return this; + } + + /// + /// 指定控制台日志的线程安全模式。 + /// + /// 线程安全模式。 + /// 构造器模式。 + /// 线程安全模式不支持。 + public ConsoleLoggerBuilder WithThreadSafe(LogWritingThreadMode threadMode) + { + _coreWriter = threadMode switch + { + LogWritingThreadMode.NotThreadSafe => new NotThreadSafeLogWriter(), + LogWritingThreadMode.Lock => new LockLogWriter(), + LogWritingThreadMode.ProducerConsumer => new ProducerConsumerLogWriter(), + _ => throw new ArgumentOutOfRangeException(nameof(threadMode)), + }; + return this; + } + + /// + /// 从命令行参数中提取过滤标签,使得控制台日志支持过滤标签行为。 + /// + /// 命令行参数。 + /// 构造器模式。 + public ConsoleLoggerBuilder FilterConsoleTagsFromCommandLineArgs(string[] args) + { + _tagFilterManager = TagFilterManager.FromCommandLineArgs(args); + return this; + } + + /// + /// 创建控制台日志记录器。 + /// + /// 控制台日志记录器。 + internal ConsoleLogger Build() => new(_coreWriter, _tagFilterManager) + { + Level = Level, + }; +} + +/// +/// 辅助创建控制台日志记录器。 +/// +public static class ConsoleLoggerBuilderExtensions +{ + /// + /// 添加控制台日志记录器。 + /// + /// 日志构建器。 + /// 配置控制台日志记录器。 + /// 日志构建器。 + public static LoggerBuilder AddConsoleLogger(this LoggerBuilder builder, Action configure) + { + var consoleLoggerBuilder = new ConsoleLoggerBuilder(); + configure(consoleLoggerBuilder); + return builder.AddWriter(consoleLoggerBuilder.Build()); + } +} diff --git a/src/dotnetCampus.Logger/Writers/Helpers/ICoreLogWriter.cs b/src/dotnetCampus.Logger/Writers/Helpers/ICoreLogWriter.cs new file mode 100644 index 0000000..4d28ac1 --- /dev/null +++ b/src/dotnetCampus.Logger/Writers/Helpers/ICoreLogWriter.cs @@ -0,0 +1,123 @@ +using System; +using System.Collections.Concurrent; +using System.Threading.Tasks; + +namespace dotnetCampus.Logging.Writers.Helpers; + +/// +/// 提供各种不同线程安全方式的最终日志写入功能。 +/// +internal interface ICoreLogWriter +{ + /// + /// 写入日志。 + /// + /// 日志消息。为 时不写入。 + void Write(string? message); + + /// + /// 插入一个动作。 + /// + /// 动作。 + void Do(Action action); +} + +/// +/// 不处理线程安全问题的日志写入器。 +/// +internal sealed class NotThreadSafeLogWriter : ICoreLogWriter +{ + /// + public void Write(string? message) + { + if (message is not null) + { + Console.WriteLine(message); + } + } + + /// + public void Do(Action action) + { + action(); + } +} + +/// +/// 使用锁来保证线程安全的日志写入器。 +/// +internal sealed class LockLogWriter : ICoreLogWriter +{ + private readonly object _lock = new(); + + /// + public void Write(string? message) + { + if (message is not null) + { + lock (_lock) + { + Console.WriteLine(message); + } + } + } + + /// + public void Do(Action action) + { + lock (_lock) + { + action(); + } + } +} + +/// +/// 使用生产者消费者模式的日志写入器。 +/// +internal sealed class ProducerConsumerLogWriter : ICoreLogWriter +{ + private readonly BlockingCollection _queue = new(); + + /// + /// 创建 的新实例,并启动消费线程。 + /// + public ProducerConsumerLogWriter() + { + new Task(Consume, TaskCreationOptions.LongRunning).Start(); + } + + /// + public void Write(string? message) + { + if (message is not null) + { + _queue.Add(message); + } + } + + /// + public void Do(Action action) + { + _queue.Add(action); + } + + /// + /// 消费队列中的元素。 + /// + private void Consume() + { + foreach (var item in _queue.GetConsumingEnumerable()) + { + switch (item) + { + case string message: + Console.WriteLine(message); + break; + case Action action: + action(); + break; + } + } + } +} diff --git a/src/dotnetCampus.Logger/Writers/Helpers/TagFilterManager.cs b/src/dotnetCampus.Logger/Writers/Helpers/TagFilterManager.cs index 3b3f2ba..c6b5ecd 100644 --- a/src/dotnetCampus.Logger/Writers/Helpers/TagFilterManager.cs +++ b/src/dotnetCampus.Logger/Writers/Helpers/TagFilterManager.cs @@ -5,6 +5,9 @@ namespace dotnetCampus.Logging.Writers.Helpers; +/// +/// 管理控制台日志的标签过滤。 +/// internal class TagFilterManager { public const string LogTagParameterName = "--log-console-tags"; diff --git a/src/dotnetCampus.Logger/Writers/LogWritingThreadMode.cs b/src/dotnetCampus.Logger/Writers/LogWritingThreadMode.cs new file mode 100644 index 0000000..072db49 --- /dev/null +++ b/src/dotnetCampus.Logger/Writers/LogWritingThreadMode.cs @@ -0,0 +1,48 @@ +using System; +using dotnetCampus.Logging.Writers.Helpers; + +namespace dotnetCampus.Logging.Writers; + +/// +/// 表示如何管理日志的写入线程。 +/// +public enum LogWritingThreadMode +{ + /// + /// 在哪个线程调用日志,就在哪个线程写入日志。不处理线程安全问题。 + /// + NotThreadSafe, + + /// + /// 使用锁来保证线程安全。 + /// + Lock, + + /// + /// 使用生产者消费者模式,将日志写入到队列中,由后台线程消费。 + /// + /// + /// 截至目前,使用此方法难以保证在程序退出时,所有日志都能被写入。 + /// + ProducerConsumer, +} + +/// +/// 包含 的扩展方法。 +/// +internal static class LogWritingThreadModeExtensions +{ + /// + /// 根据 创建对应的 实例。 + /// + /// 线程安全模式。 + /// 最终日志写入器。 + /// 当线程安全模式不支持时抛出。 + public static ICoreLogWriter CreateCoreLogWriter(this LogWritingThreadMode threadMode) => threadMode switch + { + LogWritingThreadMode.NotThreadSafe => new NotThreadSafeLogWriter(), + LogWritingThreadMode.Lock => new LockLogWriter(), + LogWritingThreadMode.ProducerConsumer => new ProducerConsumerLogWriter(), + _ => throw new ArgumentOutOfRangeException(nameof(threadMode)), + }; +}