| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153 |
- using System;
- using System.Globalization;
- using System.IO;
- using System.Text;
- namespace TeamAAS.Logging
- {
- /// <summary>
- /// 插件日志路径/行为配置(挂在 SystemConfiguration.PluginLog 下统一配置管理)。
- /// 路径公式支持的占位符:{root} {flow} {task} {year} {month} {day} {time} {timestamp}
- /// {time}=yyyy-MM-dd {timestamp}=yyyyMMdd_HHmmss {flow}=流程名 {task}=任务名
- /// </summary>
- [Serializable]
- public class PluginLogConfig
- {
- /// <summary>日志根目录:程序目录下的相对子目录(默认 Logs),也可填绝对路径。</summary>
- public string RootPath { get; set; } = "Logs";
- /// <summary>公共日志相对路径公式(1/2 级写入;默认 年/时间.log)。</summary>
- public string CommonPattern { get; set; } = @"{year}\{time}.log";
- /// <summary>任务日志相对路径公式(3/4 级写入;默认 流程名\任务名\年\时间.log)。</summary>
- public string TaskPattern { get; set; } = @"{year}\{time}\{flow}\{task}\{time}.log";
- /// <summary>新建插件未显式设置时的默认「存储详细度」:粗略=只写 L1/L2 公共日志;详细=额外写 L3/L4 到独立路径。</summary>
- public LogVerbosity DefaultStoreVerbosity { get; set; } = LogVerbosity.粗略;
- /// <summary>新建插件未显式设置时的默认「显示详细度」:粗略=实时日志只显 L1/L2;详细=额外显 L3/L4。</summary>
- public LogVerbosity DefaultDisplayVerbosity { get; set; } = LogVerbosity.粗略;
- /// <summary>单个日志文件最大 MB,超过后由 NLog 自动归档(0 或负数=不按大小切分)。</summary>
- public int MaxFileMB { get; set; } = 10;
- /// <summary>日志保留天数,超期的归档文件由 NLog 自动删除(MaxArchiveDays;0 或负数=不删除)。</summary>
- public int RetentionDays { get; set; } = 3650;
- public PluginLogConfig Clone()
- => (PluginLogConfig)MemberwiseClone();
- }
- /// <summary>
- /// 插件日志核心:全局配置 + 阈值过滤 + 等级路由,文件写入统一交给 NLog(NLogHub)。
- ///
- /// 路由规则(等级即详细度档位,与 Debug/Info 严重级别无关):
- /// - 1 低级 / 2 中级:写公共日志(CommonPattern,与 AppLogger 合并到同一个文件);
- /// - 3 高级 / 4 终极:写任务日志(TaskPattern,按 流程名/任务名 分目录)。
- ///
- /// 两道独立闸门:存储(level ≤ 节点存储详细度才写盘)与显示(level ≤ 节点显示详细度才进实时控件),互不影响。
- /// 单文件大小归档与保留天数清理均由 NLog 原生实现(见 NLogHub)。
- /// 写失败静默吞掉(回退 Debug 输出),绝不影响业务流程。
- /// </summary>
- public static class PluginLogger
- {
- private static PluginLogConfig _config = new PluginLogConfig();
- /// <summary>应用全局配置(启动加载/保存系统配置时调用)。</summary>
- public static void Configure(PluginLogConfig config)
- {
- _config = config ?? new PluginLogConfig();
- NLogHub.Configure(_config);
- }
- /// <summary>当前生效配置(只读快照语义,UI 展示用)。</summary>
- public static PluginLogConfig Current => _config;
- /// <summary>
- /// 写一条插件日志(唯一入口,静态直调)。两道相互独立的闸门:
- /// flow/task:所属流程名/任务名(3/4 级任务日志分目录用,1/2 级忽略);
- /// storeVerbosity:该节点「存储详细度」——level ≤ 它才写盘(L1/L2→公共日志,L3/L4→流程\任务 独立路径);
- /// displayVerbosity:该节点「显示详细度」——level ≤ 它才进实时日志控件(与是否落盘无关,可显而不存);
- /// level:本条日志的详细度标签(1低级/2中级/3高级/4终极);
- /// category:日志类别,默认 Info,只作为条目标记不影响存/显。
- /// </summary>
- 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();
- }
- /// <summary>清洗流程/任务名中的文件路径非法字符(作为 NLog 事件属性写入路径前调用)。</summary>
- 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();
- }
- }
- }
|