using System; using System.IO; using System.Text; namespace WxOrder.Framework.Helpers { /// 日志消息级别。 public enum LogMessageLevel { Trace = 0, Debug = 1, Info = 2, Warn = 3, Error = 4, Fatal = 5, None = 6 } /// /// 调试输出桥接。默认输出到 System.Diagnostics.Debug, /// 可在外部重新赋值 DebugPrint 以接管控件台/UI 输出。 /// public static class DebugLog { /// 调试输出回调。默认写到 Debug 输出窗口。 public static Action? DebugPrint { get; set; } = msg => System.Diagnostics.Debug.WriteLine(msg); } /// /// 默认日志逻辑类(不依赖任何第三方日志库)。 /// 日志文件按日期分目录存放,并在跨天时自动切换: /// {logDirectory}/yyyy-MM/yyyy-MM-dd.log,例如 log/2022-12/2022-12-12.log /// public sealed class NLogger : IDisposable, ILogger { private const string DefaultName = "App"; private static readonly object _lock = new object(); private static NLogger? _instance; /// /// 同一进程内所有实例共用的写入锁。 /// 多个实例(不同 Name)会写入同一个按日期命名的文件,需要串行化写入。 /// private static readonly object _fileLock = new object(); /// 获取 NLogger 的唯一单例实例(线程安全)。 public static NLogger Instance { get { if (_instance == null) { lock (_lock) { if (_instance == null) _instance = new NLogger(DefaultName); } } return _instance; } } /// 静态工厂:根据名称创建日志实例。 public static NLogger Create(string name, string? logDirectory = null) { if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("日志名称不能为空。", nameof(name)); return new NLogger(name, logDirectory); } /// 静态工厂:创建使用默认名称的日志实例。 public static NLogger Create() => new NLogger(DefaultName); /// 日志名称(写入每行日志中,用于区分来源,不再决定文件名)。 public string Name { get; } public bool IsDebugEnabled { get; set; } = false; public bool IsDebugWriteLineEnabled { get; set; } = false; public LogMessageLevel MinimumLevel { get; set; } = LogMessageLevel.Trace; /// 日志根目录。 public string LogDirectory { get; } private string? _currentFilePath; private StreamWriter? _writer; private bool _disposed; private NLogger(string name, string? logDirectory = null) { Name = name; LogDirectory = logDirectory ?? Path.Combine(AppContext.BaseDirectory, "log"); // 文件在第一次写入时按当前日期惰性创建,构造阶段不打开文件。 } #region 日志方法 public void Trace(string message, Exception? exception = null) => Execute(LogMessageLevel.Trace, message, exception); public void Debug(string message, Exception? exception = null) => Execute(LogMessageLevel.Debug, message, exception); public void Info(string message, Exception? exception = null) => Execute(LogMessageLevel.Info, message, exception); public void Warn(string message, Exception? exception = null) => Execute(LogMessageLevel.Warn, message, exception); public void Error(string message, Exception? exception = null) => Execute(LogMessageLevel.Error, message, exception); public void Fatal(string message, Exception? exception = null) => Execute(LogMessageLevel.Fatal, message, exception); public void Log(string message, LogMessageLevel level, Exception? exception = null) => Execute(level, message, exception); #endregion #region 内部逻辑 private bool CheckEnabled(LogMessageLevel level) { if (level == LogMessageLevel.None) return false; if (level < MinimumLevel) return false; if (level == LogMessageLevel.Trace || level == LogMessageLevel.Debug) return IsDebugEnabled; return true; } private void Execute(LogMessageLevel level, string message, Exception? exception) { if (CheckEnabled(level)) { WriteToFile(level, message, exception); } if (IsDebugWriteLineEnabled) { string logOutput = "[" + level + "] " + message; if (exception != null) { logOutput += Environment.NewLine + exception; } Action? handler = DebugLog.DebugPrint; if (handler != null) { handler(logOutput); } } } /// 根据日期生成日志文件路径:{根目录}/yyyy-MM/yyyy-MM-dd.log private string GetLogFilePath(DateTime time) { return Path.Combine( LogDirectory, time.ToString("yyyy-MM"), time.ToString("yyyy-MM-dd") + ".log"); } /// 写入当天的日志文件,跨天时自动切换到新文件。 private void WriteToFile(LogMessageLevel level, string message, Exception? exception) { DateTime now = DateTime.Now; var sb = new StringBuilder(); sb.Append('[') .Append(now.ToString("yyyy-MM-dd HH:mm:ss.fff")) .Append("] [") .Append(level) .Append("] [") .Append(Name) .Append("] ") .AppendLine(message); if (exception != null) { sb.AppendLine(exception.ToString()); } string line = sb.ToString(); lock (_fileLock) { if (_disposed) return; try { EnsureWriter(GetLogFilePath(now)); _writer!.Write(line); _writer.Flush(); } catch { // 日志写入失败不应影响主流程,这里吞掉异常。 // 重置写入器,下次写入时重新尝试打开文件。 CloseWriter(); } } } /// 确保写入器指向指定路径;路径变化(跨天/跨月)时关闭旧文件并打开新文件。 private void EnsureWriter(string path) { if (_writer != null && string.Equals(_currentFilePath, path, StringComparison.Ordinal)) return; CloseWriter(); string? dir = Path.GetDirectoryName(path); if (!string.IsNullOrEmpty(dir)) { Directory.CreateDirectory(dir); } var fs = new FileStream( path, FileMode.Append, FileAccess.Write, FileShare.ReadWrite); _writer = new StreamWriter(fs, new UTF8Encoding(false)) { AutoFlush = false }; _currentFilePath = path; } private void CloseWriter() { try { if (_writer != null) { _writer.Flush(); _writer.Dispose(); } } catch { /* ignore */ } finally { _writer = null; _currentFilePath = null; } } #endregion #region IDisposable public void Dispose() { lock (_fileLock) { if (_disposed) return; _disposed = true; CloseWriter(); } } #endregion } }