Răsfoiți Sursa

带缓冲区的高性能 BaseLogger

龙腾猫跃 5 luni în urmă
părinte
comite
b4c10ce79d
1 a modificat fișierele cu 103 adăugiri și 90 ștergeri
  1. 103 90
      Shared/General/Logger.cs

+ 103 - 90
Shared/General/Logger.cs

@@ -43,19 +43,17 @@ public static class Logger {
 
     // 转发给 BaseLogger 实例的对外方法
 
-    public static void Log(string message, LogLevel level = LogLevel.Info, LogBehavior behavior = LogBehavior.None, [CallerFilePath] string filePath = "")
-        => Instance.Log(message, level, behavior, filePath);
     public static void Trace(string message, LogBehavior behavior = LogBehavior.None, [CallerFilePath] string filePath = "")
-        => Log(message, LogLevel.Trace, behavior, filePath);
+        => Log(null, message, LogLevel.Trace, behavior, filePath);
     public static void Info(string message, LogBehavior behavior = LogBehavior.None, [CallerFilePath] string filePath = "")
-        => Log(message, LogLevel.Info, behavior, filePath);
+        => Log(null, message, LogLevel.Info, behavior, filePath);
     public static void Warn(string message, LogBehavior behavior = LogBehavior.ToastIfDebug, [CallerFilePath] string filePath = "")
-        => Log(message, LogLevel.Warn, behavior, filePath);
+        => Log(null, message, LogLevel.Warn, behavior, filePath);
     public static void Error(string message, LogBehavior behavior = LogBehavior.AlertThenFeedback, [CallerFilePath] string filePath = "")
-        => Log(message, LogLevel.Error, behavior, filePath);
+        => Log(null, message, LogLevel.Error, behavior, filePath);
+    public static void Log(string message, LogLevel level = LogLevel.Info, LogBehavior behavior = LogBehavior.None, [CallerFilePath] string filePath = "")
+        => Log(null, message, level, behavior, filePath);
 
-    public static void Log(Exception ex, string? message = null, LogLevel level = LogLevel.Warn, LogBehavior behavior = LogBehavior.ToastIfDebug, [CallerFilePath] string filePath = "")
-        => Instance.Log(ex, message, level, behavior, filePath);
     public static void Trace(Exception ex, string? message = null, LogBehavior behavior = LogBehavior.None, [CallerFilePath] string filePath = "")
         => Log(ex, message, LogLevel.Trace, behavior, filePath);
     public static void Info(Exception ex, string? message = null, LogBehavior behavior = LogBehavior.None, [CallerFilePath] string filePath = "")
@@ -64,6 +62,8 @@ public static class Logger {
         => Log(ex, message, LogLevel.Warn, behavior, filePath);
     public static void Error(Exception ex, string? message = null, LogBehavior behavior = LogBehavior.AlertThenFeedback, [CallerFilePath] string filePath = "")
         => Log(ex, message, LogLevel.Error, behavior, filePath);
+    public static void Log(Exception? ex, string? message = null, LogLevel level = LogLevel.Warn, LogBehavior behavior = LogBehavior.ToastIfDebug, [CallerFilePath] string filePath = "")
+        => Instance.Log(ex, message, level, behavior, filePath);
 
     /// <summary>
     /// 在不调用 Logger 的情况下,将信息直接发送到输出窗口。
@@ -84,7 +84,7 @@ public static class Logger {
 
     public static void Log(Func<string?> messageFactory, LogLevel level = LogLevel.Info, LogBehavior behavior = LogBehavior.None, [CallerFilePath] string filePath = "") {
         if (Instance.MinLevel > level) return;
-        Instance.Log(messageFactory() ?? string.Empty, level, behavior, filePath);
+        Instance.Log(null, messageFactory() ?? string.Empty, level, behavior, filePath);
     }
     public static void Trace(Func<string?> messageFactory, LogBehavior behavior = LogBehavior.None, [CallerFilePath] string filePath = "")
         => Log(messageFactory, LogLevel.Trace, behavior, filePath);
@@ -98,19 +98,19 @@ public static class Logger {
     // C#:仅当实际需要输出日志时,才格式化内插字符串,以优化性能
 
     public static void Log(LogLevel level, [InterpolatedStringHandlerArgument("level")] ref LogInterpolatedStringHandler handler, LogBehavior behavior = LogBehavior.None, [CallerFilePath] string filePath = "") {
-        if (handler.IsEnabled) Instance.Log(handler.ToStringAndClear(), level, behavior, filePath);
+        if (handler.IsEnabled) Instance.Log(null, handler.ToStringAndClear(), level, behavior, filePath);
     }
     public static void Trace(ref LogInterpolatedStringHandler<TraceLogLevel> handler, LogBehavior behavior = LogBehavior.None, [CallerFilePath] string filePath = "") {
-        if (handler.IsEnabled) Instance.Log(handler.ToStringAndClear(), LogLevel.Trace, behavior, filePath);
+        if (handler.IsEnabled) Instance.Log(null, handler.ToStringAndClear(), LogLevel.Trace, behavior, filePath);
     }
     public static void Info(ref LogInterpolatedStringHandler<InfoLogLevel> handler, LogBehavior behavior = LogBehavior.None, [CallerFilePath] string filePath = "") {
-        if (handler.IsEnabled) Instance.Log(handler.ToStringAndClear(), LogLevel.Info, behavior, filePath);
+        if (handler.IsEnabled) Instance.Log(null, handler.ToStringAndClear(), LogLevel.Info, behavior, filePath);
     }
     public static void Warn(ref LogInterpolatedStringHandler<WarnLogLevel> handler, LogBehavior behavior = LogBehavior.ToastIfDebug, [CallerFilePath] string filePath = "") {
-        if (handler.IsEnabled) Instance.Log(handler.ToStringAndClear(), LogLevel.Warn, behavior, filePath);
+        if (handler.IsEnabled) Instance.Log(null, handler.ToStringAndClear(), LogLevel.Warn, behavior, filePath);
     }
     public static void Error(ref LogInterpolatedStringHandler<ErrorLogLevel> handler, LogBehavior behavior = LogBehavior.AlertThenFeedback, [CallerFilePath] string filePath = "") {
-        if (handler.IsEnabled) Instance.Log(handler.ToStringAndClear(), LogLevel.Error, behavior, filePath);
+        if (handler.IsEnabled) Instance.Log(null, handler.ToStringAndClear(), LogLevel.Error, behavior, filePath);
     }
 
     [InterpolatedStringHandler]
@@ -184,26 +184,24 @@ public class BaseLogger {
     public LogLevel MinLevel { get; set; } = LogLevel.Trace;
 
     // 核心方法
-    public virtual void Log(string message, LogLevel level, LogBehavior behavior, string filePath) {
-        if (MinLevel > level) return;
-        try {
-            var formattedMessage = Format(message, level, filePath, null);
-            Output(formattedMessage, level);
-            HandleBehavior(message, formattedMessage, behavior, null);
-        } catch (Exception logEx) {
-            Logger.SendToDebug(logEx, "打印日志失败");
-        }
-    }
-    public virtual void Log(Exception ex, string? message, LogLevel level, LogBehavior behavior, string filePath) {
-        if (MinLevel > level) return;
+    public virtual void Log(Exception? ex, string? message, LogLevel level, LogBehavior behavior, string filePath) {
         if (ex is ThreadInterruptedException) {
             Thread.CurrentThread.Interrupt();
             return;
         }
+        if (MinLevel > level) return;
         try {
-            var formattedMessage = (message is null ? "" : $"{message}:") + ex.GetDisplay(true);
+            // 格式化文本
+            var formattedMessage = ex is null ?
+                (message ?? "") :
+                (message is null ? "" : $"{message}:") + ex.GetDisplay(true);
             formattedMessage = Format(formattedMessage, level, filePath, ex);
-            Output(formattedMessage, level);
+            // 将格式化后的日志文本置入缓冲区
+            if (avaliable || queue.Count < 100) { // 在初始化前保留至多 100 条日志
+                queue.Enqueue(new(level, formattedMessage));
+                queuedEvent.Set();
+            }
+            // 执行事件
             HandleBehavior(message, formattedMessage, behavior, ex);
         } catch (Exception logEx) {
             Logger.SendToDebug(logEx, "打印日志失败");
@@ -224,96 +222,111 @@ public class BaseLogger {
         => $"{DateTime.Now:HH':'mm':'ss'.'fff} {level.ToString().First()} {(Thread.CurrentThread.Name is null ? "" : $"<{Thread.CurrentThread.Name}> ")}[{PathUtils.GetLastPart(filePath).BeforeFirst(".")}] ";
 
     /// <summary>
-    /// 输出格式化后的日志文本。
+    /// 执行 <see cref="LogBehavior"/>。
     /// </summary>
-    public virtual void Output(string formattedMessage, LogLevel level) {
-        Debug.WriteLine(formattedMessage);
+    public virtual void HandleBehavior(string? rawMessage, string formattedMessage, LogBehavior behavior, Exception? ex) {
     }
 
     /// <summary>
-    /// 在调用 Log 方法的线程执行 <see cref="LogBehavior"/>。
+    /// 输出格式化后的日志文本。
     /// </summary>
-    public virtual void HandleBehavior(string? rawMessage, string formattedMessage, LogBehavior behavior, Exception? ex) {
+    public virtual void Output(LogEntry entry) {
+        Debug.WriteLine(entry.message);
     }
 
-}
+    public virtual void Init() { }
 
-/// <summary>
-/// 将日志输出到 <see cref="Debug"/> 并写入指定的文件夹,最多保留 5 个文件。
-/// 不实现 <see cref="LogBehavior"/>。
-/// </summary>
-public class FileLogger : BaseLogger, IDisposable {
+    #region 缓冲区与初始化
 
-    /// <inheritdoc/>
-    public override void Output(string formattedText, LogLevel level) {
-        base.Output(formattedText, level);
-        if (!writerAvailable && queue.Count >= 100) return; // 在 writer 就绪前,最多缓存 100 条日志
-        queue.Enqueue(formattedText);
-        queuedEvent.Set();
-    }
+    internal volatile bool avaliable = false;
+    public record struct LogEntry(LogLevel Level, string message);
+    internal readonly ConcurrentQueue<LogEntry> queue = new();
+    internal readonly AutoResetEvent queuedEvent = new(false);
 
-    private readonly ConcurrentQueue<string> queue = new();
-    private readonly AutoResetEvent queuedEvent = new(false);
-    private StreamWriter? writer;
-    private volatile bool writerAvailable = false;
+    /// <summary>
+    /// 初始化此 Logger。
+    /// 会在新线程中,将之前的日志文件依次后移,最多保留 5 个文件。
+    /// </summary>
+    public BaseLogger() {
+        var thread = new Thread(() => {
+            try {
+                Init();
+                avaliable = true;
+                Logger.Info($"{GetType().Name} 日志初始化结束");
+                while (avaliable) {
+                    queuedEvent.WaitOne();
+                    Flush();
+                }
+            } catch (Exception ex) {
+                avaliable = false;
+                Logger.SendToDebug(ex, "基础日志线程出错");
+            }
+        }) { Name = nameof(Logger), Priority = ThreadPriority.Lowest, IsBackground = true };
+        thread.Start();
+    }
 
     /// <summary>
-    /// 将日志立即写入文件。
+    /// 立即输出缓冲区的日志。
     /// 不会抛出异常。
     /// </summary>
     public void Flush() {
         try {
-            if (writer is null || !writerAvailable) return;
+            if (!avaliable) return;
             lock (flushLock)
-                while (queue.TryDequeue(out string line)) writer.WriteLine(line); // writer 指定了 AutoFlush
+                while (queue.TryDequeue(out LogEntry entry)) Output(entry); // writer 指定了 AutoFlush
         } catch (Exception logEx) {
-            Logger.SendToDebug(logEx, "写入日志失败");
+            Logger.SendToDebug(logEx, "输出日志失败");
         }
     }
     private readonly object flushLock = new();
 
-    /// <summary>
-    /// 初始化此 Logger。
-    /// 会在新线程中,将之前的日志文件依次后移,最多保留 5 个文件。
-    /// </summary>
-    public FileLogger(string logFolder, string fileNamePrefix = "Log", string fileNameSuffix = ".txt") {
-        var thread = new Thread(() => {
-            // 轮转日志文件,将 Log1.txt 留空
-            Logger.Info("日志初始化开始");
-            try {
-                for (int i = 4; i >= 1; i--) {
-                    string newerFile = Path.Combine(logFolder, $"{fileNamePrefix}{i + 1}{fileNameSuffix}");
-                    string olderFile = Path.Combine(logFolder, $"{fileNamePrefix}{i}{fileNameSuffix}");
-                    if (!FileUtils.Exists(olderFile)) continue;
-                    FileUtils.Copy(olderFile, newerFile);
-                    FileUtils.Delete(olderFile);
-                }
-            } catch (IOException ex) {
-                Logger.Warn(ex, "可能同时开启了多个程序,这或许会导致未知问题", LogBehavior.Toast);
-            } catch (Exception ex) {
-                Logger.Warn(ex, "整理日志文件失败");
-            }
-            // 写入新日志文件
-            try {
-                writer = new StreamWriter(PathUtils.WithLongPath(Path.Combine(logFolder, $"{fileNamePrefix}1{fileNameSuffix}")), append: true) { AutoFlush = true };
-                writerAvailable = true;
-                Logger.Info("日志初始化成功");
-                while (writerAvailable) {
-                    queuedEvent.WaitOne();
-                    Flush();
-                }
-            } catch (Exception ex) {
-                Logger.Warn(ex, "写入日志文件失败", LogBehavior.Toast);
-                writerAvailable = false;
+    #endregion
+
+}
+
+/// <summary>
+/// 将日志输出到 <see cref="Debug"/> 并写入指定的文件夹,最多保留 5 个文件。
+/// 不实现 <see cref="LogBehavior"/>。
+/// </summary>
+public class FileLogger : BaseLogger, IDisposable {
+
+    public string logFolder = Path.Combine(PathUtils.CurrentFolder, "logs");
+    public string fileNamePrefix = "Log";
+    public string fileNameSuffix = ".txt";
+    private StreamWriter? writer;
+
+    /// <inheritdoc/>
+    public override void Init() {
+        // 轮转日志文件,将 Log1.txt 留空
+        Logger.Info("开始轮转日志文件");
+        try {
+            for (int i = 4; i >= 1; i--) {
+                string newerFile = Path.Combine(logFolder, $"{fileNamePrefix}{i + 1}{fileNameSuffix}");
+                string olderFile = Path.Combine(logFolder, $"{fileNamePrefix}{i}{fileNameSuffix}");
+                if (!FileUtils.Exists(olderFile)) continue;
+                FileUtils.Copy(olderFile, newerFile);
+                FileUtils.Delete(olderFile);
             }
-        }) { Name = nameof(FileLogger), Priority = ThreadPriority.Lowest, IsBackground = true };
-        thread.Start();
+        } catch (IOException ex) {
+            Logger.Warn(ex, "可能同时开启了多个程序,这或许会导致未知问题", LogBehavior.Toast);
+        } catch (Exception ex) {
+            Logger.Warn(ex, "整理日志文件失败");
+        }
+        // 新建 writer
+        writer = new StreamWriter(PathUtils.WithLongPath(Path.Combine(logFolder, $"{fileNamePrefix}1{fileNameSuffix}")), append: true) { AutoFlush = true };
+    }
+
+    /// <inheritdoc/>
+    public override void Output(LogEntry entry) {
+        base.Output(entry);
+        writer?.WriteLine(entry.message); // writer 指定了 AutoFlush
     }
 
     public void Dispose() {
-        writerAvailable = false;
+        avaliable = false;
         if (writer is not null) ((IDisposable) writer).Dispose();
         queuedEvent.Dispose();
         GC.SuppressFinalize(this);
     }
+
 }