using System.Diagnostics; namespace MeloongCore; /// /// 日志等级。 /// public enum LogLevel { Trace, Info, Warn, Error } /// /// 输出日志时执行的错误汇报行为。 /// public enum LogBehavior { /// /// 无提示。 /// None, /// /// 只在调试状态下给出错误提示信息。 /// ToastIfDebug, /// /// 给出错误提示信息。 /// Toast, /// /// 弹出错误弹窗,不要求反馈。 /// Alert, /// /// 弹出错误弹窗,要求提交反馈。 /// AlertThenFeedback, /// /// 弹出系统原生样式的错误弹窗,要求提交反馈,然后使程序崩溃。 /// AlertThenCrash } public static class Logger { public static BaseLogger Instance { get; set; } = new(); // 转发给 BaseLogger 实例的对外方法 public static void Trace(string message, LogBehavior behavior = LogBehavior.None, [CallerFilePath] string filePath = "") => Log(null, message, LogLevel.Trace, behavior, filePath); public static void Info(string message, LogBehavior behavior = LogBehavior.None, [CallerFilePath] string filePath = "") => Log(null, message, LogLevel.Info, behavior, filePath); public static void Warn(string message, LogBehavior behavior = LogBehavior.ToastIfDebug, [CallerFilePath] string filePath = "") => Log(null, message, LogLevel.Warn, behavior, filePath); public static void Error(string message, LogBehavior behavior = LogBehavior.AlertThenFeedback, [CallerFilePath] string filePath = "") => Log(null, message, LogLevel.Error, behavior, filePath); public static void Log(string message, LogLevel level = LogLevel.Info, LogBehavior behavior = LogBehavior.None, [CallerFilePath] string filePath = "") => Log(null, message, level, behavior, filePath); public static void Trace(Exception ex, string? message = null, LogBehavior behavior = LogBehavior.None, [CallerFilePath] string filePath = "") => Log(ex, message, LogLevel.Trace, behavior, filePath); public static void Info(Exception ex, string? message = null, LogBehavior behavior = LogBehavior.None, [CallerFilePath] string filePath = "") => Log(ex, message, LogLevel.Info, behavior, filePath); public static void Warn(Exception ex, string? message = null, LogBehavior behavior = LogBehavior.ToastIfDebug, [CallerFilePath] string filePath = "") => Log(ex, message, LogLevel.Warn, behavior, filePath); public static void Error(Exception ex, string? message = null, LogBehavior behavior = LogBehavior.AlertThenFeedback, [CallerFilePath] string filePath = "") => Log(ex, message, LogLevel.Error, behavior, filePath); public static void Log(Exception? ex, string? message = null, LogLevel level = LogLevel.Warn, LogBehavior behavior = LogBehavior.ToastIfDebug, [CallerFilePath] string filePath = "") => Instance.Log(ex, message, level, behavior, filePath); /// /// 在不调用 Logger 的情况下,将信息直接发送到输出窗口。 /// [Conditional("DEBUG")] public static void SendToDebug(string message, LogLevel level = LogLevel.Error, [CallerFilePath] string filePath = "") => Debug.WriteLine($"{BaseLogger.GetLogPrefix(level, filePath)}{message}"); /// /// 在不调用 Logger 的情况下,将信息直接发送到输出窗口。 /// [Conditional("DEBUG")] public static void SendToDebug(Exception ex, string? message = null, LogLevel level = LogLevel.Error, [CallerFilePath] string filePath = "") => Debug.WriteLine($"{BaseLogger.GetLogPrefix(level, filePath)}{(message is null ? "" : $"{message}:")}{ex.GetDisplay(true)}"); /// /// 记录当前堆栈信息。 /// public static void StackTrace(LogLevel level = LogLevel.Trace, [CallerFilePath] string filePath = "") => Instance.Log(null, new StackTrace(skipFrames: 1, fNeedFileInfo: true).ToString(), level, LogBehavior.None, filePath); #region 内插优化 // VB.NET:由于不兼容 InterpolatedStringHandler,只能手动使用 Lambda public static void Log(Func messageFactory, LogLevel level = LogLevel.Info, LogBehavior behavior = LogBehavior.None, [CallerFilePath] string filePath = "") { if (Instance.MinLevel > level) return; Instance.Log(null, messageFactory() ?? string.Empty, level, behavior, filePath); } public static void Trace(Func messageFactory, LogBehavior behavior = LogBehavior.None, [CallerFilePath] string filePath = "") => Log(messageFactory, LogLevel.Trace, behavior, filePath); public static void Info(Func messageFactory, LogBehavior behavior = LogBehavior.None, [CallerFilePath] string filePath = "") => Log(messageFactory, LogLevel.Info, behavior, filePath); public static void Warn(Func messageFactory, LogBehavior behavior = LogBehavior.ToastIfDebug, [CallerFilePath] string filePath = "") => Log(messageFactory, LogLevel.Warn, behavior, filePath); public static void Error(Func messageFactory, LogBehavior behavior = LogBehavior.AlertThenFeedback, [CallerFilePath] string filePath = "") => Log(messageFactory, LogLevel.Error, behavior, filePath); // C#:仅当实际需要输出日志时,才格式化内插字符串,以优化性能 public static void Log(LogLevel level, [InterpolatedStringHandlerArgument("level")] ref LogInterpolatedStringHandler handler, LogBehavior behavior = LogBehavior.None, [CallerFilePath] string filePath = "") { if (handler.isEnabled) Instance.Log(null, handler.ToStringAndClear(), level, behavior, filePath); } public static void Trace(ref LogInterpolatedStringHandler handler, LogBehavior behavior = LogBehavior.None, [CallerFilePath] string filePath = "") { if (handler.IsEnabled) Instance.Log(null, handler.ToStringAndClear(), LogLevel.Trace, behavior, filePath); } public static void Info(ref LogInterpolatedStringHandler handler, LogBehavior behavior = LogBehavior.None, [CallerFilePath] string filePath = "") { if (handler.IsEnabled) Instance.Log(null, handler.ToStringAndClear(), LogLevel.Info, behavior, filePath); } public static void Warn(ref LogInterpolatedStringHandler handler, LogBehavior behavior = LogBehavior.ToastIfDebug, [CallerFilePath] string filePath = "") { if (handler.IsEnabled) Instance.Log(null, handler.ToStringAndClear(), LogLevel.Warn, behavior, filePath); } public static void Error(ref LogInterpolatedStringHandler handler, LogBehavior behavior = LogBehavior.AlertThenFeedback, [CallerFilePath] string filePath = "") { if (handler.IsEnabled) Instance.Log(null, handler.ToStringAndClear(), LogLevel.Error, behavior, filePath); } [InterpolatedStringHandler] public ref struct LogInterpolatedStringHandler { private StringBuilder? _sb; public readonly bool isEnabled; public LogInterpolatedStringHandler(int literalLength, int formattedCount, LogLevel level, out bool isEnabled) { this.isEnabled = isEnabled = Logger.Instance.MinLevel <= level; _sb = this.isEnabled ? new StringBuilder(literalLength) : null; } public void AppendLiteral(string value) => _sb?.Append(value); public void AppendFormatted(T value) => _sb?.Append(value?.ToString()); public void AppendFormatted(T value, string? format) => _sb?.Append(value is IFormattable formattable ? formattable.ToString(format, null) : value?.ToString()); public void AppendFormatted(T value, int alignment) { if (_sb is null) return; var s = value?.ToString() ?? string.Empty; _sb.Append(alignment > 0 ? s.PadLeft(alignment) : alignment < 0 ? s.PadRight(-alignment) : s); } public void AppendFormatted(T value, int alignment, string? format) { if (_sb is null) return; var s = value is IFormattable formattable ? formattable.ToString(format, null) : value?.ToString() ?? string.Empty; _sb.Append(alignment > 0 ? s.PadLeft(alignment) : alignment < 0 ? s.PadRight(-alignment) : s); } public void AppendFormatted(string? value) => _sb?.Append(value); public void AppendFormatted(object? value, int alignment = 0, string? format = null) { if (_sb is null) return; var s = value is IFormattable formattable ? formattable.ToString(format, null) : value?.ToString() ?? string.Empty; _sb.Append(alignment > 0 ? s.PadLeft(alignment) : alignment < 0 ? s.PadRight(-alignment) : s); } internal string ToStringAndClear() { var result = _sb?.ToString() ?? string.Empty; _sb = null; return result; } } [InterpolatedStringHandler] public ref struct LogInterpolatedStringHandler where TLevel : struct, ILogLevelProvider { private LogInterpolatedStringHandler _inner; public bool IsEnabled => _inner.isEnabled; public LogInterpolatedStringHandler(int literalLength, int formattedCount, out bool isEnabled) => _inner = new(literalLength, formattedCount, default(TLevel).Level, out isEnabled); public void AppendLiteral(string value) => _inner.AppendLiteral(value); public void AppendFormatted(T value) => _inner.AppendFormatted(value); public void AppendFormatted(T value, string? format) => _inner.AppendFormatted(value, format); public void AppendFormatted(T value, int alignment) => _inner.AppendFormatted(value, alignment); public void AppendFormatted(T value, int alignment, string? format) => _inner.AppendFormatted(value, alignment, format); public void AppendFormatted(string? value) => _inner.AppendFormatted(value); public void AppendFormatted(object? value, int alignment = 0, string? format = null) => _inner.AppendFormatted(value, alignment, format); internal string ToStringAndClear() => _inner.ToStringAndClear(); } public readonly struct TraceLogLevel : ILogLevelProvider { public LogLevel Level => LogLevel.Trace; } public readonly struct InfoLogLevel : ILogLevelProvider { public LogLevel Level => LogLevel.Info; } public readonly struct WarnLogLevel : ILogLevelProvider { public LogLevel Level => LogLevel.Warn; } public readonly struct ErrorLogLevel : ILogLevelProvider { public LogLevel Level => LogLevel.Error; } public interface ILogLevelProvider { LogLevel Level { get; } } #endregion } /// /// 最基础的日志实现。 /// 仅将日志输出到 ,不实现 。 /// public class BaseLogger { /// /// 最低日志输出等级,低于此等级的日志将被忽略。 /// public LogLevel MinLevel { get; set; } = LogLevel.Trace; // 核心方法 public virtual void Log(Exception? ex, string? message, LogLevel level, LogBehavior behavior, string filePath) { if (ex?.IsCanceled() == true) { Log(null, $"操作已取消({message})", LogLevel.Info, LogBehavior.None, filePath); return; } if (MinLevel > level) return; try { // 格式化文本 var formattedMessage = ex is null ? (message ?? "") : (message is null ? "" : $"{message}:") + ex.GetDisplay(true); formattedMessage = Format(formattedMessage, level, filePath, ex); // 将格式化后的日志文本置入缓冲区 if (avaliable || queue.Count < 100) { // 在初始化前保留至多 100 条日志 queue.Enqueue(new(level, formattedMessage)); queuedEvent.Set(); } // 执行事件 HandleBehavior(message, formattedMessage, behavior, ex); } catch (Exception logEx) { Logger.SendToDebug(logEx, "打印日志失败"); } } /// /// 格式化日志文本。 /// public virtual string Format(string text, LogLevel level, string filePath, Exception? ex) { string prefix = GetLogPrefix(level, filePath); if (string.IsNullOrEmpty(text)) return prefix; return text .ReplaceLineEndings("\n", mergeMultiple: true).Split('\n', true) .Select(t => prefix + t) .Join("\r\n"); } public static string GetLogPrefix(LogLevel level, string filePath) => $"{DateTime.Now:HH':'mm':'ss'.'fff} {level.ToString().First()} { (Thread.CurrentThread.Name is not null ? $"<{Thread.CurrentThread.ManagedThreadId} · {Thread.CurrentThread.Name}> " : (Thread.CurrentThread.IsThreadPoolThread ? " " : ""))}[{ PathUtils.GetLastPart(filePath).BeforeFirst(".")}] "; /// /// 执行 。 /// public virtual void HandleBehavior(string? rawMessage, string formattedMessage, LogBehavior behavior, Exception? ex) { } /// /// 输出格式化后的日志文本。 /// public virtual void Output(LogEntry entry) { Debug.WriteLine(entry.message); } public virtual void Init() { } #region 缓冲区与初始化 internal volatile bool avaliable = false; public record struct LogEntry(LogLevel Level, string message); internal readonly ConcurrentQueue queue = new(); internal readonly AutoResetEvent queuedEvent = new(false); /// /// 初始化此 Logger。 /// 会在新线程中,将之前的日志文件依次后移,最多保留 5 个文件。 /// public BaseLogger() { var thread = new Thread(() => { try { Init(); avaliable = true; Logger.Info($"{GetType().Name} 日志初始化结束"); while (avaliable) { queuedEvent.WaitOne(); Flush(); } } catch (Exception ex) { avaliable = false; Logger.SendToDebug(ex, "基础日志线程出错"); } }) { Name = nameof(Logger), Priority = ThreadPriority.Lowest, IsBackground = true }; thread.Start(); } /// /// 立即输出缓冲区的日志。 /// 不会抛出异常。 /// public void Flush() { try { if (!avaliable) return; lock (flushLock) { while (queue.TryDequeue(out LogEntry entry)) Output(entry); // writer 指定了 AutoFlush } } catch (Exception logEx) { Logger.SendToDebug(logEx, "输出日志失败"); } } private readonly object flushLock = new(); #endregion } /// /// 将日志输出到 并写入指定的文件夹,最多保留 5 个文件。 /// 不实现 。 /// public class FileLogger : BaseLogger, IDisposable { public string logFolder = Path.Combine(Paths.Base, "logs"); public string fileNamePrefix = "Log"; public string fileNameSuffix = ".txt"; private StreamWriter? writer; /// public override void Init() { // 轮转日志文件,将 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.Warn(ex, "可能同时开启了多个程序,这或许会导致未知问题", LogBehavior.Toast); } catch (Exception ex) { Logger.Warn(ex, "整理日志文件失败"); } // 新建 writer string logFilePath = PathUtils.ForApi(Path.Combine(logFolder, $"{fileNamePrefix}1{fileNameSuffix}")); DirectoryUtils.Create(logFolder); writer = new StreamWriter(logFilePath, append: true) { AutoFlush = true }; } /// public override void Output(LogEntry entry) { base.Output(entry); writer?.WriteLine(entry.message); // writer 指定了 AutoFlush } public void Dispose() { avaliable = false; if (writer is not null) ((IDisposable) writer).Dispose(); queuedEvent.Dispose(); GC.SuppressFinalize(this); } }