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 interface ILogger {
void Log(string message, LogLevels level, LogBehaviors behavior, string filePath);
void Log(Exception ex, string? message, LogLevels level, LogBehaviors behavior, string filePath);
}
public static class Logger {
public static ILogger Instance { get; set; } = new ConsoleLogger();
// 转发给实例的方法调用
// TODO: Trace
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(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.None, [CallerFilePath] string filePath = "") => Log(message, LogLevels.Warning, behavior, filePath);
public static void Error(string message, LogBehaviors behavior = LogBehaviors.None, [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 ConsoleLogger : ILogger {
///
/// 在写入日志前对文本进行预处理的处理器。
///
public event Func? TextProcessor;
///
/// 最低输出等级,低于此等级的日志将被忽略。默认为 。
///
public LogLevels MinLevel { get; set; } = LogLevels.Trace;
public virtual void Log(string message, LogLevels level, LogBehaviors behavior, string filePath) {
if (level < MinLevel) return;
try {
Emit(message, level, filePath);
} catch { }
}
public virtual void Log(Exception ex, string? message, LogLevels level, LogBehaviors behavior, string filePath) {
if (level < MinLevel) return;
if (ex is ThreadInterruptedException) return;
try {
Emit((message is null ? "" : $"{message}:") + ex.GetDetail(true), level, filePath);
} catch { }
}
private void Emit(string body, LogLevels level, string filePath) {
// 构造原始文本
string threadName = Thread.CurrentThread.Name is { Length: > 0 } n ? n : "主线程";
string file = filePath.AfterLast("\\").BeforeFirst(".");
if (file.StartsWithF("Mod")) file = file.AfterFirst("Mod");
string text = $"<{threadName}> [{file}] {body}\r\n";
// 应用处理器
if (TextProcessor is not null) foreach (var h in TextProcessor.GetInvocationList().OfType>()) text = h(text);
string timestamp = $"{DateTime.Now:HH':'mm':'ss'.'fff} | ";
string debugPrefix = level switch { LogLevels.Trace => "T ", LogLevels.Warning => "W ", LogLevels.Error => "E ", _ => "I " };
string formatted = text
.ReplaceLineEndings("\n", mergeMultiple: true).Split(['\n'], StringSplitOptions.RemoveEmptyEntries)
.Select(t => { t = debugPrefix + timestamp + t; return t; })
.Join("\r\n");
WriteOutput(formatted, level);
}
///
/// 将格式化后的日志文本写入额外目标。
/// 不执行额外操作(已由 写入调试控制台);
/// 子类可重写此方法以追加写入更多目标。
///
protected virtual void WriteOutput(string formattedText, LogLevels level) {
Debug.WriteLine(formattedText);
}
}
///
/// 在 基础上将日志缓冲写入文件的 实现。
///
public class FileLogger : ConsoleLogger {
private readonly ConcurrentQueue logQueue = new();
private StreamWriter? logWriter;
///
protected override void WriteOutput(string formattedText, LogLevels level) {
base.WriteOutput(formattedText, level);
logQueue.Enqueue(formattedText);
}
///
/// 启动日志文件写入。轮换历史日志文件,随后在后台线程持续将缓冲区内容写入 Log1.txt。
///
/// 日志文件夹路径,以 \ 结尾。
/// 初始化失败时的回调,参数为捕获到的异常。
public void LogStart(string logFolder, Action? onInitFailed = null) {
var thread = new Thread(() => {
bool isInitSuccess = true;
try {
for (int i = 4; i >= 1; i--) {
if (FileUtils.Exists(logFolder + $"Log{i}.txt")) {
if (FileUtils.Exists(logFolder + $"Log{i + 1}.txt"))
FileUtils.Delete(logFolder + $"Log{i + 1}.txt");
FileUtils.Copy(logFolder + $"Log{i}.txt", logFolder + $"Log{i + 1}.txt");
}
}
FileUtils.CreateAsStream(logFolder + "Log1.txt").Dispose();
} catch (Exception ex) {
isInitSuccess = false;
onInitFailed?.Invoke(ex);
}
try {
logWriter = new StreamWriter(logFolder + "Log1.txt", append: true) { AutoFlush = true };
} catch {
logWriter = null;
}
while (true) {
if (isInitSuccess)
LogFlush();
else
while (logQueue.TryDequeue(out _)) { } // 清空队列避免内存爆炸
Thread.Sleep(50);
}
}) {
Name = "Log Writer",
Priority = ThreadPriority.Lowest,
IsBackground = true,
};
thread.Start();
}
///
/// 将日志队列中的内容立即写入文件。
///
public void LogFlush() {
try {
if (logWriter is null || logQueue.IsEmpty) return;
var sb = new StringBuilder();
while (logQueue.TryDequeue(out string? line))
sb.Append(line).Append("\r\n");
logWriter.Write(sb);
} catch { }
}
}