Logger.cs 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206
  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(string message, LogBehaviors behavior = LogBehaviors.None, [CallerFilePath] string filePath = "") => Log(message, LogLevels.Trace, behavior, filePath);
  63. public static void Trace(Func<string> message, LogBehaviors behavior = LogBehaviors.None, [CallerFilePath] string filePath = "") {
  64. if (Instance.MinLevel > LogLevels.Trace) return;
  65. Log(message(), LogLevels.Trace, behavior, filePath);
  66. }
  67. public static void Info(string message, LogBehaviors behavior = LogBehaviors.None, [CallerFilePath] string filePath = "") => Log(message, LogLevels.Info, behavior, filePath);
  68. public static void Warning(string message, LogBehaviors behavior = LogBehaviors.None, [CallerFilePath] string filePath = "") => Log(message, LogLevels.Warning, behavior, filePath);
  69. public static void Error(string message, LogBehaviors behavior = LogBehaviors.None, [CallerFilePath] string filePath = "") => Log(message, LogLevels.Error, behavior, filePath);
  70. public static void Trace(Exception ex, string? message = null, LogBehaviors behavior = LogBehaviors.None, [CallerFilePath] string filePath = "") => Log(ex, message, LogLevels.Trace, behavior, filePath);
  71. public static void Info(Exception ex, string? message = null, LogBehaviors behavior = LogBehaviors.None, [CallerFilePath] string filePath = "") => Log(ex, message, LogLevels.Info, behavior, filePath);
  72. public static void Warning(Exception ex, string? message = null, LogBehaviors behavior = LogBehaviors.ToastIfDebug, [CallerFilePath] string filePath = "") => Log(ex, message, LogLevels.Warning, behavior, filePath);
  73. public static void Error(Exception ex, string? message = null, LogBehaviors behavior = LogBehaviors.AlertThenFeedback, [CallerFilePath] string filePath = "") => Log(ex, message, LogLevels.Error, behavior, filePath);
  74. }
  75. /// <summary>
  76. /// 最基础的日志实现。
  77. /// 仅将日志输出到 <see cref="Debug"/>,不实现 <see cref="LogBehaviors"/>。
  78. /// </summary>
  79. public class BaseLogger {
  80. /// <summary>
  81. /// 最低日志输出等级,低于此等级的日志将被忽略。
  82. /// </summary>
  83. public LogLevels MinLevel { get; set; } = LogLevels.Trace;
  84. // 核心方法
  85. public virtual void Log(string message, LogLevels level, LogBehaviors behavior, string filePath) {
  86. if (MinLevel > level) return;
  87. try {
  88. var formattedMessage = Format(message, level, filePath, null);
  89. Output(formattedMessage, level);
  90. HandleBehavior(message, formattedMessage, behavior, null);
  91. } catch {}
  92. }
  93. public virtual void Log(Exception ex, string? message, LogLevels level, LogBehaviors behavior, string filePath) {
  94. if (MinLevel > level) return;
  95. if (ex is ThreadInterruptedException) return;
  96. try {
  97. var formattedMessage = (message is null ? "" : $"{message}:") + ex.GetDetail(false);
  98. formattedMessage = Format(formattedMessage, level, filePath, ex);
  99. Output(formattedMessage, level);
  100. HandleBehavior(message, formattedMessage, behavior, ex);
  101. } catch { }
  102. }
  103. /// <summary>
  104. /// 格式化日志文本。
  105. /// </summary>
  106. protected virtual string Format(string text, LogLevels level, string filePath, Exception? ex) {
  107. string prefix = $"{DateTime.Now:HH':'mm':'ss'.'fff} {level.ToString().First()} [{filePath.AfterLast(@"\").BeforeFirst(".")}{(Thread.CurrentThread.Name is null ? "" : $" @ {Thread.CurrentThread.Name}")}] ";
  108. return text
  109. .ReplaceLineEndings("\n", mergeMultiple: true).Split(['\n'], StringSplitOptions.RemoveEmptyEntries)
  110. .Select(t => prefix + t)
  111. .Join("\r\n");
  112. }
  113. /// <summary>
  114. /// 输出格式化后的日志文本。
  115. /// </summary>
  116. protected virtual void Output(string formattedMessage, LogLevels level) {
  117. Debug.WriteLine(formattedMessage);
  118. }
  119. /// <summary>
  120. /// 在调用 Log 方法的线程执行 <see cref="LogBehaviors"/>。
  121. /// </summary>
  122. protected virtual void HandleBehavior(string? rawMessage, string formattedMessage, LogBehaviors behavior, Exception? ex) {
  123. }
  124. }
  125. /// <summary>
  126. /// 将日志输出到 <see cref="Debug"/> 并写入指定的文件夹,最多保留 5 个文件。
  127. /// 不实现 <see cref="LogBehaviors"/>。
  128. /// </summary>
  129. public class FileLogger : BaseLogger {
  130. /// <inheritdoc/>
  131. protected override void Output(string formattedText, LogLevels level) {
  132. base.Output(formattedText, level);
  133. if (!writerAvaliable && queue.Count >= 100) return; // 在 writer 就绪前,最多缓存 100 条日志
  134. queue.Enqueue(formattedText);
  135. queuedEvent.Set();
  136. }
  137. private readonly ConcurrentQueue<string> queue = new();
  138. private readonly AutoResetEvent queuedEvent = new(false);
  139. private StreamWriter? writer;
  140. private volatile bool writerAvaliable = false;
  141. /// <summary>
  142. /// 将日志立即写入文件。
  143. /// 不会抛出异常。
  144. /// </summary>
  145. public void Flush() {
  146. try {
  147. if (writer is null || !writerAvaliable) return;
  148. lock (flushLock)
  149. while (queue.TryDequeue(out string line)) writer.WriteLine(line); // writer 指定了 AutoFlush
  150. } catch { }
  151. }
  152. private readonly object flushLock = new();
  153. /// <summary>
  154. /// 初始化此 Logger。
  155. /// 会在新线程中,将之前的日志文件依次后移,最多保留 5 个文件。
  156. /// </summary>
  157. public FileLogger(string logFolder, string fileNamePrefix = "Log", string fileNameSuffix = ".txt") {
  158. var thread = new Thread(() => {
  159. // 轮转日志文件,将 Log1.txt 留空
  160. try {
  161. for (int i = 4; i >= 1; i--) {
  162. string newerFile = Path.Combine(logFolder, $"{fileNamePrefix}{i + 1}{fileNameSuffix}");
  163. string olderFile = Path.Combine(logFolder, $"{fileNamePrefix}{i}{fileNameSuffix}");
  164. if (!FileUtils.Exists(olderFile)) continue;
  165. FileUtils.Copy(olderFile, newerFile);
  166. FileUtils.Delete(olderFile);
  167. }
  168. } catch (IOException ex) {
  169. Logger.Warning(ex, "可能同时开启了多个程序,这或许会导致未知问题", LogBehaviors.Toast);
  170. } catch (Exception ex) {
  171. Logger.Warning(ex, "整理日志文件失败");
  172. }
  173. // 写入新日志文件
  174. try {
  175. writer = new StreamWriter(PathUtils.WithLongPath(Path.Combine(logFolder, $"{fileNamePrefix}1{fileNameSuffix}")), append: true) { AutoFlush = true };
  176. writerAvaliable = true;
  177. while (true) {
  178. queuedEvent.WaitOne();
  179. Flush();
  180. }
  181. } catch (Exception ex) {
  182. Logger.Warning(ex, "写入日志文件失败", LogBehaviors.Toast);
  183. writerAvaliable = false;
  184. }
  185. }) { Name = nameof(FileLogger), Priority = ThreadPriority.Lowest, IsBackground = true };
  186. thread.Start();
  187. }
  188. }