Logger.cs 9.8 KB

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