PluginLogger.cs 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153
  1. using System;
  2. using System.Globalization;
  3. using System.IO;
  4. using System.Text;
  5. namespace TeamAAS.Logging
  6. {
  7. /// <summary>
  8. /// 插件日志路径/行为配置(挂在 SystemConfiguration.PluginLog 下统一配置管理)。
  9. /// 路径公式支持的占位符:{root} {flow} {task} {year} {month} {day} {time} {timestamp}
  10. /// {time}=yyyy-MM-dd {timestamp}=yyyyMMdd_HHmmss {flow}=流程名 {task}=任务名
  11. /// </summary>
  12. [Serializable]
  13. public class PluginLogConfig
  14. {
  15. /// <summary>日志根目录:程序目录下的相对子目录(默认 Logs),也可填绝对路径。</summary>
  16. public string RootPath { get; set; } = "Logs";
  17. /// <summary>公共日志相对路径公式(1/2 级写入;默认 年/时间.log)。</summary>
  18. public string CommonPattern { get; set; } = @"{year}\{time}.log";
  19. /// <summary>任务日志相对路径公式(3/4 级写入;默认 流程名\任务名\年\时间.log)。</summary>
  20. public string TaskPattern { get; set; } = @"{year}\{time}\{flow}\{task}\{time}.log";
  21. /// <summary>新建插件未显式设置时的默认「存储详细度」:粗略=只写 L1/L2 公共日志;详细=额外写 L3/L4 到独立路径。</summary>
  22. public LogVerbosity DefaultStoreVerbosity { get; set; } = LogVerbosity.粗略;
  23. /// <summary>新建插件未显式设置时的默认「显示详细度」:粗略=实时日志只显 L1/L2;详细=额外显 L3/L4。</summary>
  24. public LogVerbosity DefaultDisplayVerbosity { get; set; } = LogVerbosity.粗略;
  25. /// <summary>单个日志文件最大 MB,超过后由 NLog 自动归档(0 或负数=不按大小切分)。</summary>
  26. public int MaxFileMB { get; set; } = 10;
  27. /// <summary>日志保留天数,超期的归档文件由 NLog 自动删除(MaxArchiveDays;0 或负数=不删除)。</summary>
  28. public int RetentionDays { get; set; } = 3650;
  29. public PluginLogConfig Clone()
  30. => (PluginLogConfig)MemberwiseClone();
  31. }
  32. /// <summary>
  33. /// 插件日志核心:全局配置 + 阈值过滤 + 等级路由,文件写入统一交给 NLog(NLogHub)。
  34. ///
  35. /// 路由规则(等级即详细度档位,与 Debug/Info 严重级别无关):
  36. /// - 1 低级 / 2 中级:写公共日志(CommonPattern,与 AppLogger 合并到同一个文件);
  37. /// - 3 高级 / 4 终极:写任务日志(TaskPattern,按 流程名/任务名 分目录)。
  38. ///
  39. /// 两道独立闸门:存储(level ≤ 节点存储详细度才写盘)与显示(level ≤ 节点显示详细度才进实时控件),互不影响。
  40. /// 单文件大小归档与保留天数清理均由 NLog 原生实现(见 NLogHub)。
  41. /// 写失败静默吞掉(回退 Debug 输出),绝不影响业务流程。
  42. /// </summary>
  43. public static class PluginLogger
  44. {
  45. private static PluginLogConfig _config = new PluginLogConfig();
  46. /// <summary>应用全局配置(启动加载/保存系统配置时调用)。</summary>
  47. public static void Configure(PluginLogConfig config)
  48. {
  49. _config = config ?? new PluginLogConfig();
  50. NLogHub.Configure(_config);
  51. }
  52. /// <summary>当前生效配置(只读快照语义,UI 展示用)。</summary>
  53. public static PluginLogConfig Current => _config;
  54. /// <summary>
  55. /// 写一条插件日志(唯一入口,静态直调)。两道相互独立的闸门:
  56. /// flow/task:所属流程名/任务名(3/4 级任务日志分目录用,1/2 级忽略);
  57. /// storeVerbosity:该节点「存储详细度」——level ≤ 它才写盘(L1/L2→公共日志,L3/L4→流程\任务 独立路径);
  58. /// displayVerbosity:该节点「显示详细度」——level ≤ 它才进实时日志控件(与是否落盘无关,可显而不存);
  59. /// level:本条日志的详细度标签(1低级/2中级/3高级/4终极);
  60. /// category:日志类别,默认 Info,只作为条目标记不影响存/显。
  61. /// </summary>
  62. public static void Log(string flow, string task, LogVerbosity storeVerbosity, LogVerbosity displayVerbosity,
  63. int level, string message, LogLevel category = LogLevel.Info)
  64. {
  65. if (level <= 0) return;
  66. // 闸门A·存储:level ≤ 存储详细度 才写文件(1/2 公共日志,3/4 流程/任务 独立路径)
  67. if (level <= (int)storeVerbosity)
  68. {
  69. var line = FormatLine(flow, task, level, category, message);
  70. try
  71. {
  72. NLogHub.Apply();
  73. if (level <= (int)PluginLogLevel.中级)
  74. {
  75. // 1/2 级:公共日志(与 AppLogger 写入同一个 CommonPattern 文件)
  76. NLogHub.Common.Log(MapNLogLevel(category), line);
  77. }
  78. else
  79. {
  80. // 3/4 级:任务日志,按 流程名/任务名 分目录(路径由 NLog 依据 TaskPattern 解析)
  81. var ev = NLog.LogEventInfo.Create(MapNLogLevel(category), NLogHub.TaskLoggerName, line);
  82. ev.Properties[NLogHub.FlowPropertyName] = Sanitize(flow, "未命名流程");
  83. ev.Properties[NLogHub.TaskPropertyName] = Sanitize(task, "未命名任务");
  84. NLogHub.Task.Log(ev);
  85. }
  86. }
  87. catch (Exception ex)
  88. {
  89. System.Diagnostics.Debug.WriteLine("[PluginLogger] write failed: " + ex.Message);
  90. }
  91. }
  92. // 闸门B·显示:level ≤ 显示详细度 才联动实时日志控件(独立于存储,可显而不存)
  93. if (level <= (int)displayVerbosity)
  94. {
  95. LogStream.Publish(new LogEntry(
  96. DateTime.Now, LogSource.Plugin, category,
  97. level: level, flow: flow, task: task, message: message));
  98. }
  99. }
  100. private static NLog.LogLevel MapNLogLevel(LogLevel category)
  101. {
  102. switch (category)
  103. {
  104. case LogLevel.Warning: return NLog.LogLevel.Warn;
  105. case LogLevel.Error: return NLog.LogLevel.Error;
  106. default: return NLog.LogLevel.Info;
  107. }
  108. }
  109. private static string FormatLine(string flow, string task, int level, LogLevel category, string message)
  110. {
  111. var sb = new StringBuilder();
  112. sb.Append(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff", CultureInfo.InvariantCulture));
  113. sb.Append(" [L").Append(level).Append(']');
  114. sb.Append(" [").Append(category.ToString().ToUpperInvariant()).Append(']');
  115. sb.Append(" [T").Append(Environment.CurrentManagedThreadId).Append(']');
  116. if (!string.IsNullOrEmpty(flow)) sb.Append('[').Append(flow).Append(']');
  117. if (!string.IsNullOrEmpty(task)) sb.Append('[').Append(task).Append(']');
  118. sb.Append(' ').Append((message ?? "").Replace(Environment.NewLine, " | "));
  119. return sb.ToString();
  120. }
  121. /// <summary>清洗流程/任务名中的文件路径非法字符(作为 NLog 事件属性写入路径前调用)。</summary>
  122. private static string Sanitize(string name, string fallback)
  123. {
  124. if (string.IsNullOrWhiteSpace(name)) return fallback;
  125. var sb = new StringBuilder(name.Length);
  126. foreach (var c in name)
  127. {
  128. if (Array.IndexOf(Path.GetInvalidFileNameChars(), c) >= 0 || c == '\\' || c == '/')
  129. sb.Append('_');
  130. else
  131. sb.Append(c);
  132. }
  133. return sb.ToString();
  134. }
  135. }
  136. }