using System.Collections.Concurrent; using System.Diagnostics; namespace MeloongCore; /// /// 日志等级。 /// public enum LogLevels { /// /// 追踪。 /// 只在调试状态下输出日志信息。 /// Trace, /// /// 信息。 /// Info, /// /// 警告。 /// Warning, /// /// 错误。 /// Error } /// /// 输出日志时执行的错误汇报行为。 /// public enum LogBehaviors { /// /// 无提示。 /// None, /// /// 只在调试状态下给出错误提示信息。 /// ToastIfDebug, /// /// 给出错误提示信息。 /// Toast, /// /// 弹出错误弹窗,不要求反馈。 /// Alert, /// /// 弹出错误弹窗,要求提交反馈。 /// AlertThenFeedback, /// /// 弹出系统原生样式的错误弹窗,要求提交反馈,然后使程序崩溃。 /// AlertThenCrash } public static class Logger { public static BaseLogger Instance { get; set; } = new(); // 转发给实例的方法调用 public static void Log(string message, LogLevels level = LogLevels.Info, LogBehaviors behavior = LogBehaviors.None, [CallerFilePath] string filePath = "") => Instance.Log(message, level, behavior, filePath); public static void Log(Exception ex, string? message = null, LogLevels level = LogLevels.Warning, LogBehaviors behavior = LogBehaviors.ToastIfDebug, [CallerFilePath] string filePath = "") => Instance.Log(ex, message, level, behavior, filePath); public static void Trace(Func message, LogBehaviors behavior = LogBehaviors.None, [CallerFilePath] string filePath = "") { if (Instance.MinLevel <= LogLevels.Trace) Log(message(), LogLevels.Trace, behavior, filePath); } public static void Trace(string message, LogBehaviors behavior = LogBehaviors.None, [CallerFilePath] string filePath = "") => Log(message, LogLevels.Trace, behavior, filePath); public static void Info(string message, LogBehaviors behavior = LogBehaviors.None, [CallerFilePath] string filePath = "") => Log(message, LogLevels.Info, behavior, filePath); public static void Warning(string message, LogBehaviors behavior = LogBehaviors.ToastIfDebug, [CallerFilePath] string filePath = "") => Log(message, LogLevels.Warning, behavior, filePath); public static void Error(string message, LogBehaviors behavior = LogBehaviors.AlertThenFeedback, [CallerFilePath] string filePath = "") => Log(message, LogLevels.Error, behavior, filePath); public static void Trace(Exception ex, string? message = null, LogBehaviors behavior = LogBehaviors.None, [CallerFilePath] string filePath = "") => Log(ex, message, LogLevels.Trace, behavior, filePath); public static void Info(Exception ex, string? message = null, LogBehaviors behavior = LogBehaviors.None, [CallerFilePath] string filePath = "") => Log(ex, message, LogLevels.Info, behavior, filePath); public static void Warning(Exception ex, string? message = null, LogBehaviors behavior = LogBehaviors.ToastIfDebug, [CallerFilePath] string filePath = "") => Log(ex, message, LogLevels.Warning, behavior, filePath); public static void Error(Exception ex, string? message = null, LogBehaviors behavior = LogBehaviors.AlertThenFeedback, [CallerFilePath] string filePath = "") => Log(ex, message, LogLevels.Error, behavior, filePath); } /// /// 最基础的日志实现。 /// 仅将日志输出到 ,不实现 。 /// public class BaseLogger { /// /// 最低日志输出等级,低于此等级的日志将被忽略。 /// public LogLevels MinLevel { get; set; } = LogLevels.Trace; // 核心方法 public virtual void Log(string message, LogLevels level, LogBehaviors behavior, string filePath) { if (MinLevel > level) return; try { var formattedMessage = Format(message, level, filePath, null); Output(formattedMessage, level); HandleBehavior(message, formattedMessage, behavior, null); } catch {} } public virtual void Log(Exception ex, string? message, LogLevels level, LogBehaviors behavior, string filePath) { if (MinLevel > level) return; if (ex is ThreadInterruptedException) return; try { var formattedMessage = (message is null ? "" : $"{message}:") + ex.GetDisplay(true); formattedMessage = Format(formattedMessage, level, filePath, ex); Output(formattedMessage, level); HandleBehavior(message, formattedMessage, behavior, ex); } catch { } } /// /// 格式化日志文本。 /// protected virtual string Format(string text, LogLevels level, string filePath, Exception? ex) { string prefix = $"{DateTime.Now:HH':'mm':'ss'.'fff} {level.ToString().First()} {(Thread.CurrentThread.Name is null ? "" : $"<{Thread.CurrentThread.Name}> ")}[{filePath.AfterLast(@"\").BeforeFirst(".")}] "; return text .ReplaceLineEndings("\n", mergeMultiple: true).Split(['\n'], StringSplitOptions.RemoveEmptyEntries) .Select(t => prefix + t) .Join("\r\n"); } /// /// 输出格式化后的日志文本。 /// protected virtual void Output(string formattedMessage, LogLevels level) { Debug.WriteLine(formattedMessage); } /// /// 在调用 Log 方法的线程执行 。 /// protected virtual void HandleBehavior(string? rawMessage, string formattedMessage, LogBehaviors behavior, Exception? ex) { } } /// /// 将日志输出到 并写入指定的文件夹,最多保留 5 个文件。 /// 不实现 。 /// public class FileLogger : BaseLogger { /// protected override void Output(string formattedText, LogLevels level) { base.Output(formattedText, level); if (!writerAvaliable && queue.Count >= 100) return; // 在 writer 就绪前,最多缓存 100 条日志 queue.Enqueue(formattedText); queuedEvent.Set(); } private readonly ConcurrentQueue queue = new(); private readonly AutoResetEvent queuedEvent = new(false); private StreamWriter? writer; private volatile bool writerAvaliable = false; /// /// 将日志立即写入文件。 /// 不会抛出异常。 /// public void Flush() { try { if (writer is null || !writerAvaliable) return; lock (flushLock) while (queue.TryDequeue(out string line)) writer.WriteLine(line); // writer 指定了 AutoFlush } catch { } } private readonly object flushLock = new(); /// /// 初始化此 Logger。 /// 会在新线程中,将之前的日志文件依次后移,最多保留 5 个文件。 /// public FileLogger(string logFolder, string fileNamePrefix = "Log", string fileNameSuffix = ".txt") { var thread = new Thread(() => { // 轮转日志文件,将 Log1.txt 留空 Logger.Info("日志初始化开始"); try { for (int i = 4; i >= 1; i--) { string newerFile = Path.Combine(logFolder, $"{fileNamePrefix}{i + 1}{fileNameSuffix}"); string olderFile = Path.Combine(logFolder, $"{fileNamePrefix}{i}{fileNameSuffix}"); if (!FileUtils.Exists(olderFile)) continue; FileUtils.Copy(olderFile, newerFile); FileUtils.Delete(olderFile); } } catch (IOException ex) { Logger.Warning(ex, "可能同时开启了多个程序,这或许会导致未知问题", LogBehaviors.Toast); } catch (Exception ex) { Logger.Warning(ex, "整理日志文件失败"); } // 写入新日志文件 try { writer = new StreamWriter(PathUtils.WithLongPath(Path.Combine(logFolder, $"{fileNamePrefix}1{fileNameSuffix}")), append: true) { AutoFlush = true }; writerAvaliable = true; Logger.Info("日志初始化成功"); while (true) { queuedEvent.WaitOne(); Flush(); } } catch (Exception ex) { Logger.Warning(ex, "写入日志文件失败", LogBehaviors.Toast); writerAvaliable = false; } }) { Name = nameof(FileLogger), Priority = ThreadPriority.Lowest, IsBackground = true }; thread.Start(); } }