using System; using System.Globalization; using System.IO; using System.Text; namespace TeamAAS.Logging { /// /// 插件日志路径/行为配置(挂在 SystemConfiguration.PluginLog 下统一配置管理)。 /// 路径公式支持的占位符:{root} {flow} {task} {year} {month} {day} {time} {timestamp} /// {time}=yyyy-MM-dd {timestamp}=yyyyMMdd_HHmmss {flow}=流程名 {task}=任务名 /// [Serializable] public class PluginLogConfig { /// 日志根目录:程序目录下的相对子目录(默认 Logs),也可填绝对路径。 public string RootPath { get; set; } = "Logs"; /// 公共日志相对路径公式(1/2 级写入;默认 年/时间.log)。 public string CommonPattern { get; set; } = @"{year}\{time}.log"; /// 任务日志相对路径公式(3/4 级写入;默认 流程名\任务名\年\时间.log)。 public string TaskPattern { get; set; } = @"{year}\{time}\{flow}\{task}\{time}.log"; /// 新建插件未显式设置时的默认「存储详细度」:粗略=只写 L1/L2 公共日志;详细=额外写 L3/L4 到独立路径。 public LogVerbosity DefaultStoreVerbosity { get; set; } = LogVerbosity.粗略; /// 新建插件未显式设置时的默认「显示详细度」:粗略=实时日志只显 L1/L2;详细=额外显 L3/L4。 public LogVerbosity DefaultDisplayVerbosity { get; set; } = LogVerbosity.粗略; /// 单个日志文件最大 MB,超过后由 NLog 自动归档(0 或负数=不按大小切分)。 public int MaxFileMB { get; set; } = 10; /// 日志保留天数,超期的归档文件由 NLog 自动删除(MaxArchiveDays;0 或负数=不删除)。 public int RetentionDays { get; set; } = 3650; public PluginLogConfig Clone() => (PluginLogConfig)MemberwiseClone(); } /// /// 插件日志核心:全局配置 + 阈值过滤 + 等级路由,文件写入统一交给 NLog(NLogHub)。 /// /// 路由规则(等级即详细度档位,与 Debug/Info 严重级别无关): /// - 1 低级 / 2 中级:写公共日志(CommonPattern,与 AppLogger 合并到同一个文件); /// - 3 高级 / 4 终极:写任务日志(TaskPattern,按 流程名/任务名 分目录)。 /// /// 两道独立闸门:存储(level ≤ 节点存储详细度才写盘)与显示(level ≤ 节点显示详细度才进实时控件),互不影响。 /// 单文件大小归档与保留天数清理均由 NLog 原生实现(见 NLogHub)。 /// 写失败静默吞掉(回退 Debug 输出),绝不影响业务流程。 /// public static class PluginLogger { private static PluginLogConfig _config = new PluginLogConfig(); /// 应用全局配置(启动加载/保存系统配置时调用)。 public static void Configure(PluginLogConfig config) { _config = config ?? new PluginLogConfig(); NLogHub.Configure(_config); } /// 当前生效配置(只读快照语义,UI 展示用)。 public static PluginLogConfig Current => _config; /// /// 写一条插件日志(唯一入口,静态直调)。两道相互独立的闸门: /// flow/task:所属流程名/任务名(3/4 级任务日志分目录用,1/2 级忽略); /// storeVerbosity:该节点「存储详细度」——level ≤ 它才写盘(L1/L2→公共日志,L3/L4→流程\任务 独立路径); /// displayVerbosity:该节点「显示详细度」——level ≤ 它才进实时日志控件(与是否落盘无关,可显而不存); /// level:本条日志的详细度标签(1低级/2中级/3高级/4终极); /// category:日志类别,默认 Info,只作为条目标记不影响存/显。 /// public static void Log(string flow, string task, LogVerbosity storeVerbosity, LogVerbosity displayVerbosity, int level, string message, LogLevel category = LogLevel.Info) { if (level <= 0) return; // 闸门A·存储:level ≤ 存储详细度 才写文件(1/2 公共日志,3/4 流程/任务 独立路径) if (level <= (int)storeVerbosity) { var line = FormatLine(flow, task, level, category, message); try { NLogHub.Apply(); if (level <= (int)PluginLogLevel.中级) { // 1/2 级:公共日志(与 AppLogger 写入同一个 CommonPattern 文件) NLogHub.Common.Log(MapNLogLevel(category), line); } else { // 3/4 级:任务日志,按 流程名/任务名 分目录(路径由 NLog 依据 TaskPattern 解析) var ev = NLog.LogEventInfo.Create(MapNLogLevel(category), NLogHub.TaskLoggerName, line); ev.Properties[NLogHub.FlowPropertyName] = Sanitize(flow, "未命名流程"); ev.Properties[NLogHub.TaskPropertyName] = Sanitize(task, "未命名任务"); NLogHub.Task.Log(ev); } } catch (Exception ex) { System.Diagnostics.Debug.WriteLine("[PluginLogger] write failed: " + ex.Message); } } // 闸门B·显示:level ≤ 显示详细度 才联动实时日志控件(独立于存储,可显而不存) if (level <= (int)displayVerbosity) { LogStream.Publish(new LogEntry( DateTime.Now, LogSource.Plugin, category, level: level, flow: flow, task: task, message: message)); } } private static NLog.LogLevel MapNLogLevel(LogLevel category) { switch (category) { case LogLevel.Warning: return NLog.LogLevel.Warn; case LogLevel.Error: return NLog.LogLevel.Error; default: return NLog.LogLevel.Info; } } private static string FormatLine(string flow, string task, int level, LogLevel category, string message) { var sb = new StringBuilder(); sb.Append(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff", CultureInfo.InvariantCulture)); sb.Append(" [L").Append(level).Append(']'); sb.Append(" [").Append(category.ToString().ToUpperInvariant()).Append(']'); sb.Append(" [T").Append(Environment.CurrentManagedThreadId).Append(']'); if (!string.IsNullOrEmpty(flow)) sb.Append('[').Append(flow).Append(']'); if (!string.IsNullOrEmpty(task)) sb.Append('[').Append(task).Append(']'); sb.Append(' ').Append((message ?? "").Replace(Environment.NewLine, " | ")); return sb.ToString(); } /// 清洗流程/任务名中的文件路径非法字符(作为 NLog 事件属性写入路径前调用)。 private static string Sanitize(string name, string fallback) { if (string.IsNullOrWhiteSpace(name)) return fallback; var sb = new StringBuilder(name.Length); foreach (var c in name) { if (Array.IndexOf(Path.GetInvalidFileNameChars(), c) >= 0 || c == '\\' || c == '/') sb.Append('_'); else sb.Append(c); } return sb.ToString(); } } }