Logger.cs 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208
  1. using System.Collections.Concurrent;
  2. using System.Diagnostics;
  3. namespace MeloongCore;
  4. /// <summary>
  5. /// 日志等级。
  6. /// </summary>
  7. public enum LogLevels {
  8. /// <summary>
  9. /// 追踪。
  10. /// 只在调试状态下输出日志信息。
  11. /// </summary>
  12. Trace,
  13. /// <summary>
  14. /// 信息。
  15. /// </summary>
  16. Info,
  17. /// <summary>
  18. /// 警告。
  19. /// </summary>
  20. Warning,
  21. /// <summary>
  22. /// 错误。
  23. /// </summary>
  24. Error
  25. }
  26. /// <summary>
  27. /// 输出日志时执行的错误汇报行为。
  28. /// </summary>
  29. public enum LogBehaviors {
  30. /// <summary>
  31. /// 无提示。
  32. /// </summary>
  33. None,
  34. /// <summary>
  35. /// 只在调试状态下给出错误提示信息。
  36. /// </summary>
  37. ToastIfDebug,
  38. /// <summary>
  39. /// 给出错误提示信息。
  40. /// </summary>
  41. Toast,
  42. /// <summary>
  43. /// 弹出错误弹窗,不要求反馈。
  44. /// </summary>
  45. Alert,
  46. /// <summary>
  47. /// 弹出错误弹窗,要求提交反馈。
  48. /// </summary>
  49. AlertThenFeedback,
  50. /// <summary>
  51. /// 弹出系统原生样式的错误弹窗,要求提交反馈,然后使程序崩溃。
  52. /// </summary>
  53. AlertThenCrash
  54. }
  55. public static class Logger {
  56. public static BaseLogger Instance { get; set; } = new();
  57. // 转发给实例的方法调用
  58. public static void Log(string message, LogLevels level = LogLevels.Info, LogBehaviors behavior = LogBehaviors.None, [CallerFilePath] string filePath = "")
  59. => Instance.Log(message, level, behavior, filePath);
  60. public static void Log(Exception ex, string? message = null, LogLevels level = LogLevels.Warning, LogBehaviors behavior = LogBehaviors.ToastIfDebug, [CallerFilePath] string filePath = "")
  61. => Instance.Log(ex, message, level, behavior, filePath);
  62. public static void Trace(Func<string> message, LogBehaviors behavior = LogBehaviors.None, [CallerFilePath] string filePath = "") { if (Instance.MinLevel <= LogLevels.Trace) Log(message(), LogLevels.Trace, behavior, filePath); }
  63. public static void Trace(string message, LogBehaviors behavior = LogBehaviors.None, [CallerFilePath] string filePath = "") => Log(message, LogLevels.Trace, behavior, filePath);
  64. public static void Info(string message, LogBehaviors behavior = LogBehaviors.None, [CallerFilePath] string filePath = "") => Log(message, LogLevels.Info, behavior, filePath);
  65. public static void Warning(string message, LogBehaviors behavior = LogBehaviors.ToastIfDebug, [CallerFilePath] string filePath = "") => Log(message, LogLevels.Warning, behavior, filePath);
  66. public static void Error(string message, LogBehaviors behavior = LogBehaviors.AlertThenFeedback, [CallerFilePath] string filePath = "") => Log(message, LogLevels.Error, behavior, filePath);
  67. public static void Trace(Exception ex, string? message = null, LogBehaviors behavior = LogBehaviors.None, [CallerFilePath] string filePath = "") => Log(ex, message, LogLevels.Trace, behavior, filePath);
  68. public static void Info(Exception ex, string? message = null, LogBehaviors behavior = LogBehaviors.None, [CallerFilePath] string filePath = "") => Log(ex, message, LogLevels.Info, behavior, filePath);
  69. public static void Warning(Exception ex, string? message = null, LogBehaviors behavior = LogBehaviors.ToastIfDebug, [CallerFilePath] string filePath = "") => Log(ex, message, LogLevels.Warning, behavior, filePath);
  70. public static void Error(Exception ex, string? message = null, LogBehaviors behavior = LogBehaviors.AlertThenFeedback, [CallerFilePath] string filePath = "") => Log(ex, message, LogLevels.Error, behavior, filePath);
  71. }
  72. /// <summary>
  73. /// 最基础的日志实现。
  74. /// 仅将日志输出到 <see cref="Debug"/>,不实现 <see cref="LogBehaviors"/>。
  75. /// </summary>
  76. public class BaseLogger {
  77. /// <summary>
  78. /// 最低日志输出等级,低于此等级的日志将被忽略。
  79. /// </summary>
  80. public LogLevels MinLevel { get; set; } = LogLevels.Trace;
  81. // 核心方法
  82. public virtual void Log(string message, LogLevels level, LogBehaviors behavior, string filePath) {
  83. if (MinLevel > level) return;
  84. try {
  85. var formattedMessage = Format(message, level, filePath, null);
  86. Output(formattedMessage, level);
  87. HandleBehavior(message, formattedMessage, behavior, null);
  88. } catch {}
  89. }
  90. public virtual void Log(Exception ex, string? message, LogLevels level, LogBehaviors behavior, string filePath) {
  91. if (MinLevel > level) return;
  92. if (ex is ThreadInterruptedException) return;
  93. try {
  94. var formattedMessage = (message is null ? "" : $"{message}:") + ex.GetDisplay(true);
  95. formattedMessage = Format(formattedMessage, level, filePath, ex);
  96. Output(formattedMessage, level);
  97. HandleBehavior(message, formattedMessage, behavior, ex);
  98. } catch { }
  99. }
  100. /// <summary>
  101. /// 格式化日志文本。
  102. /// </summary>
  103. protected virtual string Format(string text, LogLevels level, string filePath, Exception? ex) {
  104. string prefix = $"{DateTime.Now:HH':'mm':'ss'.'fff} {level.ToString().First()} {(Thread.CurrentThread.Name is null ? "" : $"<{Thread.CurrentThread.Name}> ")}[{filePath.AfterLast(@"\").BeforeFirst(".")}] ";
  105. return text
  106. .ReplaceLineEndings("\n", mergeMultiple: true).Split(['\n'], StringSplitOptions.RemoveEmptyEntries)
  107. .Select(t => prefix + t)
  108. .Join("\r\n");
  109. }
  110. /// <summary>
  111. /// 输出格式化后的日志文本。
  112. /// </summary>
  113. protected virtual void Output(string formattedMessage, LogLevels level) {
  114. Debug.WriteLine(formattedMessage);
  115. }
  116. /// <summary>
  117. /// 在调用 Log 方法的线程执行 <see cref="LogBehaviors"/>。
  118. /// </summary>
  119. protected virtual void HandleBehavior(string? rawMessage, string formattedMessage, LogBehaviors behavior, Exception? ex) {
  120. }
  121. }
  122. /// <summary>
  123. /// 将日志输出到 <see cref="Debug"/> 并写入指定的文件夹,最多保留 5 个文件。
  124. /// 不实现 <see cref="LogBehaviors"/>。
  125. /// </summary>
  126. public class FileLogger : BaseLogger {
  127. /// <inheritdoc/>
  128. protected override void Output(string formattedText, LogLevels level) {
  129. base.Output(formattedText, level);
  130. if (!writerAvaliable && queue.Count >= 100) return; // 在 writer 就绪前,最多缓存 100 条日志
  131. queue.Enqueue(formattedText);
  132. queuedEvent.Set();
  133. }
  134. private readonly ConcurrentQueue<string> queue = new();
  135. private readonly AutoResetEvent queuedEvent = new(false);
  136. private StreamWriter? writer;
  137. private volatile bool writerAvaliable = false;
  138. /// <summary>
  139. /// 将日志立即写入文件。
  140. /// 不会抛出异常。
  141. /// </summary>
  142. public void Flush() {
  143. try {
  144. if (writer is null || !writerAvaliable) return;
  145. lock (flushLock)
  146. while (queue.TryDequeue(out string line)) writer.WriteLine(line); // writer 指定了 AutoFlush
  147. } catch { }
  148. }
  149. private readonly object flushLock = new();
  150. /// <summary>
  151. /// 初始化此 Logger。
  152. /// 会在新线程中,将之前的日志文件依次后移,最多保留 5 个文件。
  153. /// </summary>
  154. public FileLogger(string logFolder, string fileNamePrefix = "Log", string fileNameSuffix = ".txt") {
  155. var thread = new Thread(() => {
  156. // 轮转日志文件,将 Log1.txt 留空
  157. Logger.Info("日志初始化开始");
  158. try {
  159. for (int i = 4; i >= 1; i--) {
  160. string newerFile = Path.Combine(logFolder, $"{fileNamePrefix}{i + 1}{fileNameSuffix}");
  161. string olderFile = Path.Combine(logFolder, $"{fileNamePrefix}{i}{fileNameSuffix}");
  162. if (!FileUtils.Exists(olderFile)) continue;
  163. FileUtils.Copy(olderFile, newerFile);
  164. FileUtils.Delete(olderFile);
  165. }
  166. } catch (IOException ex) {
  167. Logger.Warning(ex, "可能同时开启了多个程序,这或许会导致未知问题", LogBehaviors.Toast);
  168. } catch (Exception ex) {
  169. Logger.Warning(ex, "整理日志文件失败");
  170. }
  171. // 写入新日志文件
  172. try {
  173. writer = new StreamWriter(PathUtils.WithLongPath(Path.Combine(logFolder, $"{fileNamePrefix}1{fileNameSuffix}")), append: true) { AutoFlush = true };
  174. writerAvaliable = true;
  175. Logger.Info("日志初始化成功");
  176. while (true) {
  177. queuedEvent.WaitOne();
  178. Flush();
  179. }
  180. } catch (Exception ex) {
  181. Logger.Warning(ex, "写入日志文件失败", LogBehaviors.Toast);
  182. writerAvaliable = false;
  183. }
  184. }) { Name = nameof(FileLogger), Priority = ThreadPriority.Lowest, IsBackground = true };
  185. thread.Start();
  186. }
  187. }