Logger.cs 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207
  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 interface ILogger {
  56. void Log(string message, LogLevels level, LogBehaviors behavior, string filePath);
  57. void Log(Exception ex, string? message, LogLevels level, LogBehaviors behavior, string filePath);
  58. }
  59. public static class Logger {
  60. public static ILogger Instance { get; set; } = new ConsoleLogger();
  61. // 转发给实例的方法调用
  62. // TODO: Trace
  63. public static void Log(string message, LogLevels level = LogLevels.Info, LogBehaviors behavior = LogBehaviors.None, [CallerFilePath] string filePath = "")
  64. => Instance.Log(message, level, behavior, filePath);
  65. public static void Log(Exception ex, string? message = null, LogLevels level = LogLevels.Warning, LogBehaviors behavior = LogBehaviors.ToastIfDebug, [CallerFilePath] string filePath = "")
  66. => Instance.Log(ex, message, level, behavior, filePath);
  67. public static void Trace(string message, LogBehaviors behavior = LogBehaviors.None, [CallerFilePath] string filePath = "") => Log(message, LogLevels.Trace, behavior, filePath);
  68. public static void Info(string message, LogBehaviors behavior = LogBehaviors.None, [CallerFilePath] string filePath = "") => Log(message, LogLevels.Info, behavior, filePath);
  69. public static void Warning(string message, LogBehaviors behavior = LogBehaviors.None, [CallerFilePath] string filePath = "") => Log(message, LogLevels.Warning, behavior, filePath);
  70. public static void Error(string message, LogBehaviors behavior = LogBehaviors.None, [CallerFilePath] string filePath = "") => Log(message, LogLevels.Error, 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. /// 将日志输出到 <see cref="Debug"/> 的 <see cref="ILogger"/> 基础实现。
  78. /// </summary>
  79. public class ConsoleLogger : ILogger {
  80. /// <summary>
  81. /// 在写入日志前对文本进行预处理的处理器。
  82. /// </summary>
  83. public event Func<string, string>? TextProcessor;
  84. /// <summary>
  85. /// 最低输出等级,低于此等级的日志将被忽略。默认为 <see cref="LogLevels.Trace"/>。
  86. /// </summary>
  87. public LogLevels MinLevel { get; set; } = LogLevels.Trace;
  88. public virtual void Log(string message, LogLevels level, LogBehaviors behavior, string filePath) {
  89. if (level < MinLevel) return;
  90. try {
  91. Emit(message, level, filePath);
  92. } catch { }
  93. }
  94. public virtual void Log(Exception ex, string? message, LogLevels level, LogBehaviors behavior, string filePath) {
  95. if (level < MinLevel) return;
  96. if (ex is ThreadInterruptedException) return;
  97. try {
  98. Emit((message is null ? "" : $"{message}:") + ex.GetDetail(true), level, filePath);
  99. } catch { }
  100. }
  101. private void Emit(string body, LogLevels level, string filePath) {
  102. // 构造原始文本
  103. string threadName = Thread.CurrentThread.Name is { Length: > 0 } n ? n : "主线程";
  104. string file = filePath.AfterLast("\\").BeforeFirst(".");
  105. if (file.StartsWithF("Mod")) file = file.AfterFirst("Mod");
  106. string text = $"<{threadName}> [{file}] {body}\r\n";
  107. // 应用处理器
  108. if (TextProcessor is not null) foreach (var h in TextProcessor.GetInvocationList().OfType<Func<string, string>>()) text = h(text);
  109. string timestamp = $"{DateTime.Now:HH':'mm':'ss'.'fff} | ";
  110. string debugPrefix = level switch { LogLevels.Trace => "T ", LogLevels.Warning => "W ", LogLevels.Error => "E ", _ => "I " };
  111. string formatted = text
  112. .ReplaceLineEndings("\n", mergeMultiple: true).Split(['\n'], StringSplitOptions.RemoveEmptyEntries)
  113. .Select(t => { t = debugPrefix + timestamp + t; return t; })
  114. .Join("\r\n");
  115. WriteOutput(formatted, level);
  116. }
  117. /// <summary>
  118. /// 将格式化后的日志文本写入额外目标。
  119. /// <see cref="ConsoleLogger"/> 不执行额外操作(已由 <see cref="Emit"/> 写入调试控制台);
  120. /// 子类可重写此方法以追加写入更多目标。
  121. /// </summary>
  122. protected virtual void WriteOutput(string formattedText, LogLevels level) {
  123. Debug.WriteLine(formattedText);
  124. }
  125. }
  126. /// <summary>
  127. /// 在 <see cref="ConsoleLogger"/> 基础上将日志缓冲写入文件的 <see cref="ILogger"/> 实现。
  128. /// </summary>
  129. public class FileLogger : ConsoleLogger {
  130. private readonly ConcurrentQueue<string> logQueue = new();
  131. private StreamWriter? logWriter;
  132. /// <inheritdoc/>
  133. protected override void WriteOutput(string formattedText, LogLevels level) {
  134. base.WriteOutput(formattedText, level);
  135. logQueue.Enqueue(formattedText);
  136. }
  137. /// <summary>
  138. /// 启动日志文件写入。轮换历史日志文件,随后在后台线程持续将缓冲区内容写入 Log1.txt。
  139. /// </summary>
  140. /// <param name="logFolder">日志文件夹路径,以 \ 结尾。</param>
  141. /// <param name="onInitFailed">初始化失败时的回调,参数为捕获到的异常。</param>
  142. public void LogStart(string logFolder, Action<Exception>? onInitFailed = null) {
  143. var thread = new Thread(() => {
  144. bool isInitSuccess = true;
  145. try {
  146. for (int i = 4; i >= 1; i--) {
  147. if (FileUtils.Exists(logFolder + $"Log{i}.txt")) {
  148. if (FileUtils.Exists(logFolder + $"Log{i + 1}.txt"))
  149. FileUtils.Delete(logFolder + $"Log{i + 1}.txt");
  150. FileUtils.Copy(logFolder + $"Log{i}.txt", logFolder + $"Log{i + 1}.txt");
  151. }
  152. }
  153. FileUtils.CreateAsStream(logFolder + "Log1.txt").Dispose();
  154. } catch (Exception ex) {
  155. isInitSuccess = false;
  156. onInitFailed?.Invoke(ex);
  157. }
  158. try {
  159. logWriter = new StreamWriter(logFolder + "Log1.txt", append: true) { AutoFlush = true };
  160. } catch {
  161. logWriter = null;
  162. }
  163. while (true) {
  164. if (isInitSuccess)
  165. LogFlush();
  166. else
  167. while (logQueue.TryDequeue(out _)) { } // 清空队列避免内存爆炸
  168. Thread.Sleep(50);
  169. }
  170. }) {
  171. Name = "Log Writer",
  172. Priority = ThreadPriority.Lowest,
  173. IsBackground = true,
  174. };
  175. thread.Start();
  176. }
  177. /// <summary>
  178. /// 将日志队列中的内容立即写入文件。
  179. /// </summary>
  180. public void LogFlush() {
  181. try {
  182. if (logWriter is null || logQueue.IsEmpty) return;
  183. var sb = new StringBuilder();
  184. while (logQueue.TryDequeue(out string? line))
  185. sb.Append(line).Append("\r\n");
  186. logWriter.Write(sb);
  187. } catch { }
  188. }
  189. }