Logger.cs 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333
  1. using System.Collections.Concurrent;
  2. using System.Diagnostics;
  3. namespace MeloongCore;
  4. /// <summary>
  5. /// 日志等级。
  6. /// </summary>
  7. public enum LogLevel { Trace, Info, Warn, Error }
  8. /// <summary>
  9. /// 输出日志时执行的错误汇报行为。
  10. /// </summary>
  11. public enum LogBehavior {
  12. /// <summary>
  13. /// 无提示。
  14. /// </summary>
  15. None,
  16. /// <summary>
  17. /// 只在调试状态下给出错误提示信息。
  18. /// </summary>
  19. ToastIfDebug,
  20. /// <summary>
  21. /// 给出错误提示信息。
  22. /// </summary>
  23. Toast,
  24. /// <summary>
  25. /// 弹出错误弹窗,不要求反馈。
  26. /// </summary>
  27. Alert,
  28. /// <summary>
  29. /// 弹出错误弹窗,要求提交反馈。
  30. /// </summary>
  31. AlertThenFeedback,
  32. /// <summary>
  33. /// 弹出系统原生样式的错误弹窗,要求提交反馈,然后使程序崩溃。
  34. /// </summary>
  35. AlertThenCrash
  36. }
  37. public static class Logger {
  38. public static BaseLogger Instance { get; set; } = new();
  39. // 转发给 BaseLogger 实例的对外方法
  40. public static void Trace(string message, LogBehavior behavior = LogBehavior.None, [CallerFilePath] string filePath = "")
  41. => Log(null, message, LogLevel.Trace, behavior, filePath);
  42. public static void Info(string message, LogBehavior behavior = LogBehavior.None, [CallerFilePath] string filePath = "")
  43. => Log(null, message, LogLevel.Info, behavior, filePath);
  44. public static void Warn(string message, LogBehavior behavior = LogBehavior.ToastIfDebug, [CallerFilePath] string filePath = "")
  45. => Log(null, message, LogLevel.Warn, behavior, filePath);
  46. public static void Error(string message, LogBehavior behavior = LogBehavior.AlertThenFeedback, [CallerFilePath] string filePath = "")
  47. => Log(null, message, LogLevel.Error, behavior, filePath);
  48. public static void Log(string message, LogLevel level = LogLevel.Info, LogBehavior behavior = LogBehavior.None, [CallerFilePath] string filePath = "")
  49. => Log(null, message, level, behavior, filePath);
  50. public static void Trace(Exception ex, string? message = null, LogBehavior behavior = LogBehavior.None, [CallerFilePath] string filePath = "")
  51. => Log(ex, message, LogLevel.Trace, behavior, filePath);
  52. public static void Info(Exception ex, string? message = null, LogBehavior behavior = LogBehavior.None, [CallerFilePath] string filePath = "")
  53. => Log(ex, message, LogLevel.Info, behavior, filePath);
  54. public static void Warn(Exception ex, string? message = null, LogBehavior behavior = LogBehavior.ToastIfDebug, [CallerFilePath] string filePath = "")
  55. => Log(ex, message, LogLevel.Warn, behavior, filePath);
  56. public static void Error(Exception ex, string? message = null, LogBehavior behavior = LogBehavior.AlertThenFeedback, [CallerFilePath] string filePath = "")
  57. => Log(ex, message, LogLevel.Error, behavior, filePath);
  58. public static void Log(Exception? ex, string? message = null, LogLevel level = LogLevel.Warn, LogBehavior behavior = LogBehavior.ToastIfDebug, [CallerFilePath] string filePath = "")
  59. => Instance.Log(ex, message, level, behavior, filePath);
  60. /// <summary>
  61. /// 在不调用 Logger 的情况下,将信息直接发送到输出窗口。
  62. /// </summary>
  63. [Conditional("DEBUG")]
  64. public static void SendToDebug(string message, LogLevel level = LogLevel.Error, [CallerFilePath] string filePath = "")
  65. => Debug.WriteLine($"{BaseLogger.GetLogPrefix(level, filePath)}{message}");
  66. /// <summary>
  67. /// 在不调用 Logger 的情况下,将信息直接发送到输出窗口。
  68. /// </summary>
  69. [Conditional("DEBUG")]
  70. public static void SendToDebug(Exception ex, string? message = null, LogLevel level = LogLevel.Error, [CallerFilePath] string filePath = "")
  71. => Debug.WriteLine($"{BaseLogger.GetLogPrefix(level, filePath)}{(message is null ? "" : $"{message}:")}{ex.GetDisplay(true)}");
  72. #region 内插优化
  73. // VB.NET:由于不兼容 InterpolatedStringHandler,只能手动使用 Lambda
  74. public static void Log(Func<string?> messageFactory, LogLevel level = LogLevel.Info, LogBehavior behavior = LogBehavior.None, [CallerFilePath] string filePath = "") {
  75. if (Instance.MinLevel > level) return;
  76. Instance.Log(null, messageFactory() ?? string.Empty, level, behavior, filePath);
  77. }
  78. public static void Trace(Func<string?> messageFactory, LogBehavior behavior = LogBehavior.None, [CallerFilePath] string filePath = "")
  79. => Log(messageFactory, LogLevel.Trace, behavior, filePath);
  80. public static void Info(Func<string?> messageFactory, LogBehavior behavior = LogBehavior.None, [CallerFilePath] string filePath = "")
  81. => Log(messageFactory, LogLevel.Info, behavior, filePath);
  82. public static void Warn(Func<string?> messageFactory, LogBehavior behavior = LogBehavior.ToastIfDebug, [CallerFilePath] string filePath = "")
  83. => Log(messageFactory, LogLevel.Warn, behavior, filePath);
  84. public static void Error(Func<string?> messageFactory, LogBehavior behavior = LogBehavior.AlertThenFeedback, [CallerFilePath] string filePath = "")
  85. => Log(messageFactory, LogLevel.Error, behavior, filePath);
  86. // C#:仅当实际需要输出日志时,才格式化内插字符串,以优化性能
  87. public static void Log(LogLevel level, [InterpolatedStringHandlerArgument("level")] ref LogInterpolatedStringHandler handler, LogBehavior behavior = LogBehavior.None, [CallerFilePath] string filePath = "") {
  88. if (handler.isEnabled) Instance.Log(null, handler.ToStringAndClear(), level, behavior, filePath);
  89. }
  90. public static void Trace(ref LogInterpolatedStringHandler<TraceLogLevel> handler, LogBehavior behavior = LogBehavior.None, [CallerFilePath] string filePath = "") {
  91. if (handler.IsEnabled) Instance.Log(null, handler.ToStringAndClear(), LogLevel.Trace, behavior, filePath);
  92. }
  93. public static void Info(ref LogInterpolatedStringHandler<InfoLogLevel> handler, LogBehavior behavior = LogBehavior.None, [CallerFilePath] string filePath = "") {
  94. if (handler.IsEnabled) Instance.Log(null, handler.ToStringAndClear(), LogLevel.Info, behavior, filePath);
  95. }
  96. public static void Warn(ref LogInterpolatedStringHandler<WarnLogLevel> handler, LogBehavior behavior = LogBehavior.ToastIfDebug, [CallerFilePath] string filePath = "") {
  97. if (handler.IsEnabled) Instance.Log(null, handler.ToStringAndClear(), LogLevel.Warn, behavior, filePath);
  98. }
  99. public static void Error(ref LogInterpolatedStringHandler<ErrorLogLevel> handler, LogBehavior behavior = LogBehavior.AlertThenFeedback, [CallerFilePath] string filePath = "") {
  100. if (handler.IsEnabled) Instance.Log(null, handler.ToStringAndClear(), LogLevel.Error, behavior, filePath);
  101. }
  102. [InterpolatedStringHandler]
  103. public ref struct LogInterpolatedStringHandler {
  104. private StringBuilder? _sb;
  105. public readonly bool isEnabled;
  106. public LogInterpolatedStringHandler(int literalLength, int formattedCount, LogLevel level, out bool isEnabled) {
  107. this.isEnabled = isEnabled = Logger.Instance.MinLevel <= level;
  108. _sb = this.isEnabled ? new StringBuilder(literalLength) : null;
  109. }
  110. public void AppendLiteral(string value) => _sb?.Append(value);
  111. public void AppendFormatted<T>(T value) => _sb?.Append(value?.ToString());
  112. public void AppendFormatted<T>(T value, string? format) => _sb?.Append(value is IFormattable formattable ? formattable.ToString(format, null) : value?.ToString());
  113. public void AppendFormatted<T>(T value, int alignment) {
  114. if (_sb is null) return;
  115. var s = value?.ToString() ?? string.Empty;
  116. _sb.Append(alignment > 0 ? s.PadLeft(alignment) : alignment < 0 ? s.PadRight(-alignment) : s);
  117. }
  118. public void AppendFormatted<T>(T value, int alignment, string? format) {
  119. if (_sb is null) return;
  120. var s = value is IFormattable formattable ? formattable.ToString(format, null) : value?.ToString() ?? string.Empty;
  121. _sb.Append(alignment > 0 ? s.PadLeft(alignment) : alignment < 0 ? s.PadRight(-alignment) : s);
  122. }
  123. public void AppendFormatted(string? value) => _sb?.Append(value);
  124. public void AppendFormatted(object? value, int alignment = 0, string? format = null) {
  125. if (_sb is null) return;
  126. var s = value is IFormattable formattable ? formattable.ToString(format, null) : value?.ToString() ?? string.Empty;
  127. _sb.Append(alignment > 0 ? s.PadLeft(alignment) : alignment < 0 ? s.PadRight(-alignment) : s);
  128. }
  129. internal string ToStringAndClear() {
  130. var result = _sb?.ToString() ?? string.Empty;
  131. _sb = null;
  132. return result;
  133. }
  134. }
  135. [InterpolatedStringHandler]
  136. public ref struct LogInterpolatedStringHandler<TLevel> where TLevel : struct, ILogLevelProvider {
  137. private LogInterpolatedStringHandler _inner;
  138. public bool IsEnabled => _inner.isEnabled;
  139. public LogInterpolatedStringHandler(int literalLength, int formattedCount, out bool isEnabled) => _inner = new(literalLength, formattedCount, default(TLevel).Level, out isEnabled);
  140. public void AppendLiteral(string value) => _inner.AppendLiteral(value);
  141. public void AppendFormatted<T>(T value) => _inner.AppendFormatted(value);
  142. public void AppendFormatted<T>(T value, string? format) => _inner.AppendFormatted(value, format);
  143. public void AppendFormatted<T>(T value, int alignment) => _inner.AppendFormatted(value, alignment);
  144. public void AppendFormatted<T>(T value, int alignment, string? format) => _inner.AppendFormatted(value, alignment, format);
  145. public void AppendFormatted(string? value) => _inner.AppendFormatted(value);
  146. public void AppendFormatted(object? value, int alignment = 0, string? format = null) => _inner.AppendFormatted(value, alignment, format);
  147. internal string ToStringAndClear() => _inner.ToStringAndClear();
  148. }
  149. public readonly struct TraceLogLevel : ILogLevelProvider { public LogLevel Level => LogLevel.Trace; }
  150. public readonly struct InfoLogLevel : ILogLevelProvider { public LogLevel Level => LogLevel.Info; }
  151. public readonly struct WarnLogLevel : ILogLevelProvider { public LogLevel Level => LogLevel.Warn; }
  152. public readonly struct ErrorLogLevel : ILogLevelProvider { public LogLevel Level => LogLevel.Error; }
  153. public interface ILogLevelProvider { LogLevel Level { get; } }
  154. #endregion
  155. }
  156. /// <summary>
  157. /// 最基础的日志实现。
  158. /// 仅将日志输出到 <see cref="Debug"/>,不实现 <see cref="LogBehavior"/>。
  159. /// </summary>
  160. public class BaseLogger {
  161. /// <summary>
  162. /// 最低日志输出等级,低于此等级的日志将被忽略。
  163. /// </summary>
  164. public LogLevel MinLevel { get; set; } = LogLevel.Trace;
  165. // 核心方法
  166. public virtual void Log(Exception? ex, string? message, LogLevel level, LogBehavior behavior, string filePath) {
  167. if (ex is ThreadInterruptedException) {
  168. Thread.CurrentThread.Interrupt();
  169. return;
  170. }
  171. if (MinLevel > level) return;
  172. try {
  173. // 格式化文本
  174. var formattedMessage = ex is null ?
  175. (message ?? "") :
  176. (message is null ? "" : $"{message}:") + ex.GetDisplay(true);
  177. formattedMessage = Format(formattedMessage, level, filePath, ex);
  178. // 将格式化后的日志文本置入缓冲区
  179. if (avaliable || queue.Count < 100) { // 在初始化前保留至多 100 条日志
  180. queue.Enqueue(new(level, formattedMessage));
  181. queuedEvent.Set();
  182. }
  183. // 执行事件
  184. HandleBehavior(message, formattedMessage, behavior, ex);
  185. } catch (Exception logEx) {
  186. Logger.SendToDebug(logEx, "打印日志失败");
  187. }
  188. }
  189. /// <summary>
  190. /// 格式化日志文本。
  191. /// </summary>
  192. public virtual string Format(string text, LogLevel level, string filePath, Exception? ex) {
  193. string prefix = GetLogPrefix(level, filePath);
  194. if (string.IsNullOrEmpty(text)) return prefix;
  195. return text
  196. .ReplaceLineEndings("\n", mergeMultiple: true).Split('\n', true)
  197. .Select(t => prefix + t)
  198. .Join("\r\n");
  199. }
  200. public static string GetLogPrefix(LogLevel level, string filePath)
  201. => $"{DateTime.Now:HH':'mm':'ss'.'fff} {level.ToString().First()} {(Thread.CurrentThread.Name is null ? "" : $"<{Thread.CurrentThread.Name}> ")}[{PathUtils.GetLastPart(filePath).BeforeFirst(".")}] ";
  202. /// <summary>
  203. /// 执行 <see cref="LogBehavior"/>。
  204. /// </summary>
  205. public virtual void HandleBehavior(string? rawMessage, string formattedMessage, LogBehavior behavior, Exception? ex) {
  206. }
  207. /// <summary>
  208. /// 输出格式化后的日志文本。
  209. /// </summary>
  210. public virtual void Output(LogEntry entry) {
  211. Debug.WriteLine(entry.message);
  212. }
  213. public virtual void Init() { }
  214. #region 缓冲区与初始化
  215. internal volatile bool avaliable = false;
  216. public record struct LogEntry(LogLevel Level, string message);
  217. internal readonly ConcurrentQueue<LogEntry> queue = new();
  218. internal readonly AutoResetEvent queuedEvent = new(false);
  219. /// <summary>
  220. /// 初始化此 Logger。
  221. /// 会在新线程中,将之前的日志文件依次后移,最多保留 5 个文件。
  222. /// </summary>
  223. public BaseLogger() {
  224. var thread = new Thread(() => {
  225. try {
  226. Init();
  227. avaliable = true;
  228. Logger.Info($"{GetType().Name} 日志初始化结束");
  229. while (avaliable) {
  230. queuedEvent.WaitOne();
  231. Flush();
  232. }
  233. } catch (Exception ex) {
  234. avaliable = false;
  235. Logger.SendToDebug(ex, "基础日志线程出错");
  236. }
  237. }) { Name = nameof(Logger), Priority = ThreadPriority.Lowest, IsBackground = true };
  238. thread.Start();
  239. }
  240. /// <summary>
  241. /// 立即输出缓冲区的日志。
  242. /// 不会抛出异常。
  243. /// </summary>
  244. public void Flush() {
  245. try {
  246. if (!avaliable) return;
  247. lock (flushLock)
  248. while (queue.TryDequeue(out LogEntry entry)) Output(entry); // writer 指定了 AutoFlush
  249. } catch (Exception logEx) {
  250. Logger.SendToDebug(logEx, "输出日志失败");
  251. }
  252. }
  253. private readonly object flushLock = new();
  254. #endregion
  255. }
  256. /// <summary>
  257. /// 将日志输出到 <see cref="Debug"/> 并写入指定的文件夹,最多保留 5 个文件。
  258. /// 不实现 <see cref="LogBehavior"/>。
  259. /// </summary>
  260. public class FileLogger : BaseLogger, IDisposable {
  261. public string logFolder = Path.Combine(PathUtils.CurrentFolder, "logs");
  262. public string fileNamePrefix = "Log";
  263. public string fileNameSuffix = ".txt";
  264. private StreamWriter? writer;
  265. /// <inheritdoc/>
  266. public override void Init() {
  267. // 轮转日志文件,将 Log1.txt 留空
  268. Logger.Info("开始轮转日志文件");
  269. try {
  270. for (int i = 4; i >= 1; i--) {
  271. string newerFile = Path.Combine(logFolder, $"{fileNamePrefix}{i + 1}{fileNameSuffix}");
  272. string olderFile = Path.Combine(logFolder, $"{fileNamePrefix}{i}{fileNameSuffix}");
  273. if (!FileUtils.Exists(olderFile)) continue;
  274. FileUtils.Copy(olderFile, newerFile);
  275. FileUtils.Delete(olderFile);
  276. }
  277. } catch (IOException ex) {
  278. Logger.Warn(ex, "可能同时开启了多个程序,这或许会导致未知问题", LogBehavior.Toast);
  279. } catch (Exception ex) {
  280. Logger.Warn(ex, "整理日志文件失败");
  281. }
  282. // 新建 writer
  283. writer = new StreamWriter(PathUtils.ForApi(Path.Combine(logFolder, $"{fileNamePrefix}1{fileNameSuffix}")), append: true) { AutoFlush = true };
  284. }
  285. /// <inheritdoc/>
  286. public override void Output(LogEntry entry) {
  287. base.Output(entry);
  288. writer?.WriteLine(entry.message); // writer 指定了 AutoFlush
  289. }
  290. public void Dispose() {
  291. avaliable = false;
  292. if (writer is not null) ((IDisposable) writer).Dispose();
  293. queuedEvent.Dispose();
  294. GC.SuppressFinalize(this);
  295. }
  296. }