OperationLogger.cs 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333
  1. using System;
  2. using System.Diagnostics;
  3. namespace Aivfo.OperationLog
  4. {
  5. /// <summary>
  6. /// 操作日志组件门面(14§9)。
  7. /// 用法:
  8. /// 启动时一次:OperationLogger.Init(o =&gt; { o.Project="operate"; o.KafkaBootstrapServers="localhost:9092"; });
  9. /// 记一条: OperationLogger.Log("串口", "打开端口", input: new{port="COM3"}, result:"成功");
  10. /// 计时 scope: using (OperationLogger.Begin("串口","打开端口")) { ... } // 自动耗时 + 成功/失败 + 异常→error
  11. /// 所有 API 全 try 兜底:日志失败绝不影响业务。
  12. /// </summary>
  13. public static class OperationLogger
  14. {
  15. private static OperationLogOptions _options;
  16. private static OperationLogPipeline _pipeline;
  17. private static LocalFileWriter _local;
  18. private static OperationLogConfigWatcher _configWatcher; // G3-3:配置热加载 watcher(ConfigFilePath 非空才起)
  19. private static readonly object _initLock = new object();
  20. private static volatile bool _initialized;
  21. /// <summary>当前配置(运行时可改模块开关:OperationLogger.Options.SetModule(...))。</summary>
  22. public static OperationLogOptions Options => _options;
  23. /// <summary>是否已初始化。</summary>
  24. public static bool Initialized => _initialized;
  25. /// <summary>用默认 Kafka 传输初始化。</summary>
  26. public static void Init(Action<OperationLogOptions> configure)
  27. {
  28. lock (_initLock)
  29. {
  30. if (_initialized) return;
  31. var opts = new OperationLogOptions();
  32. configure?.Invoke(opts);
  33. var local = new LocalFileWriter(opts.LocalLogDir);
  34. IOplogTransport transport;
  35. try
  36. {
  37. // G3-3 / 14§11:投递失败(broker 宕/超时/队列满)把整条 JSON 落本地兜底,
  38. // 由 pipeline 的补送定时器在 Kafka 恢复后补回,不静默丢。
  39. transport = new KafkaOplogTransport(opts.KafkaBootstrapServers, opts.Topic,
  40. onError: local.WriteSelfError, onDeliveryFailed: local.WriteFallback);
  41. }
  42. catch (Exception ex)
  43. {
  44. // Kafka 客户端建不起来:降级为只写本地,不抛。
  45. local.WriteSelfError("Kafka transport init failed, fallback to local-only: " + ex.Message);
  46. transport = new NullTransport();
  47. }
  48. InitCore(opts, transport, local);
  49. }
  50. }
  51. /// <summary>用自定义传输初始化(测试/离线降级用)。</summary>
  52. public static void Init(OperationLogOptions opts, IOplogTransport transport)
  53. {
  54. lock (_initLock)
  55. {
  56. if (_initialized) return;
  57. var local = new LocalFileWriter(opts.LocalLogDir);
  58. InitCore(opts, transport ?? new NullTransport(), local);
  59. }
  60. }
  61. private static void InitCore(OperationLogOptions opts, IOplogTransport transport, LocalFileWriter local)
  62. {
  63. _options = opts;
  64. _local = local;
  65. _pipeline = new OperationLogPipeline(opts, transport, local);
  66. // G3-3 / 14§10:配置集中下发——指定了 ConfigFilePath 才起后台热加载 watcher
  67. // (日志微服务/运维把统一配置文件下发覆盖到该路径即热生效,无需重编)。
  68. if (!string.IsNullOrWhiteSpace(opts.ConfigFilePath))
  69. {
  70. try { _configWatcher = new OperationLogConfigWatcher(opts, local.WriteSelfError); }
  71. catch (Exception ex) { local.WriteSelfError("ConfigWatcher init failed: " + ex.Message); }
  72. }
  73. _initialized = true;
  74. }
  75. /// <summary>
  76. /// 记一条操作日志。input/output 任意对象,内部安全序列化。
  77. /// level 默认 Info(操作级,发 Kafka);Debug 走本地文件、不入 Kafka。
  78. /// </summary>
  79. public static void Log(
  80. string module,
  81. string operation,
  82. object input = null,
  83. object output = null,
  84. string result = null,
  85. string error = null,
  86. long? elapsedMs = null,
  87. OpLogLevel level = OpLogLevel.Info,
  88. string operatorName = null,
  89. int? houseSn = null,
  90. int? wellSn = null,
  91. string tlSn = null)
  92. {
  93. try
  94. {
  95. if (!_initialized) return;
  96. // 模块整体关闭则什么都不记(含调试级本地文件)。
  97. if (!_options.IsModuleEnabled(module)) return;
  98. var msg = new OperationLogMessage
  99. {
  100. TraceId = OperationLogContext.TraceId ?? OperationLogContext.NewTraceId(),
  101. ParentId = OperationLogContext.ParentId,
  102. Time = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
  103. Project = _options.Project,
  104. Module = module,
  105. Operation = operation,
  106. Operator = operatorName ?? OperationLogContext.Operator,
  107. Input = SafeSerializer.Serialize(input),
  108. Output = SafeSerializer.Serialize(output),
  109. Result = result,
  110. Error = error,
  111. ElapsedMs = elapsedMs,
  112. Level = level == OpLogLevel.Debug ? "DEBUG" : "INFO",
  113. HouseSn = houseSn ?? OperationLogContext.HouseSn,
  114. WellSn = wellSn ?? OperationLogContext.WellSn,
  115. TlSn = tlSn ?? _options.TlSn,
  116. Host = _options.Host
  117. };
  118. if (level == OpLogLevel.Debug)
  119. {
  120. // 调试级(14§4):写本地文件、不入 Kafka(除非该模块 MinLevel 显式调到 Debug)。
  121. _local.WriteDebug(msg);
  122. if (_options.ShouldSendKafka(module, level))
  123. _pipeline.Enqueue(msg);
  124. }
  125. else
  126. {
  127. // 操作级:达到 Kafka 门槛才发;否则也落本地兜底,避免静默丢失。
  128. if (_options.ShouldSendKafka(module, level))
  129. _pipeline.Enqueue(msg);
  130. else
  131. _local.WriteDebug(msg);
  132. }
  133. }
  134. catch (Exception ex)
  135. {
  136. try { _local?.WriteSelfError("Log() failed: " + ex.Message); } catch { }
  137. }
  138. }
  139. /// <summary>
  140. /// 开启一次带计时的操作 scope。释放时自动记录(耗时、成功/失败、异常→error)。
  141. /// using (var op = OperationLogger.Begin("串口","打开端口")) { op.Input(x); ...; op.Output(y); }
  142. /// 块内抛异常时自动记 result=失败 + error=异常摘要,再向上抛。
  143. /// </summary>
  144. public static OperationScope Begin(
  145. string module,
  146. string operation,
  147. OpLogLevel level = OpLogLevel.Info,
  148. string operatorName = null,
  149. int? houseSn = null,
  150. int? wellSn = null,
  151. string tlSn = null)
  152. {
  153. return new OperationScope(module, operation, level, operatorName, houseSn, wellSn, tlSn);
  154. }
  155. /// <summary>flush(测试/退出用)。</summary>
  156. public static void Flush(TimeSpan? timeout = null)
  157. {
  158. try { _pipeline?.Flush(timeout ?? TimeSpan.FromSeconds(5)); } catch { }
  159. }
  160. /// <summary>
  161. /// 包装执行一个操作:自动计时 + 异常自动记 result=失败/error,然后向上抛。
  162. /// 这是带异常捕获的推荐写法(Dispose 无法探测异常)。
  163. /// </summary>
  164. public static void Run(string module, string operation, Action body,
  165. object input = null, OpLogLevel level = OpLogLevel.Info)
  166. {
  167. var sw = Stopwatch.StartNew();
  168. using (OperationLogContext.BeginScope())
  169. {
  170. try
  171. {
  172. body?.Invoke();
  173. sw.Stop();
  174. Log(module, operation, input: input, result: "成功", elapsedMs: sw.ElapsedMilliseconds, level: level);
  175. }
  176. catch (Exception ex)
  177. {
  178. sw.Stop();
  179. Log(module, operation, input: input, result: "失败",
  180. error: ex.GetType().Name + ": " + ex.Message,
  181. elapsedMs: sw.ElapsedMilliseconds, level: level);
  182. throw;
  183. }
  184. }
  185. }
  186. /// <summary>Run 的有返回值版本。</summary>
  187. public static T Run<T>(string module, string operation, Func<T> body,
  188. object input = null, OpLogLevel level = OpLogLevel.Info)
  189. {
  190. var sw = Stopwatch.StartNew();
  191. using (OperationLogContext.BeginScope())
  192. {
  193. try
  194. {
  195. var r = body != null ? body() : default;
  196. sw.Stop();
  197. Log(module, operation, input: input, output: r, result: "成功", elapsedMs: sw.ElapsedMilliseconds, level: level);
  198. return r;
  199. }
  200. catch (Exception ex)
  201. {
  202. sw.Stop();
  203. Log(module, operation, input: input, result: "失败",
  204. error: ex.GetType().Name + ": " + ex.Message,
  205. elapsedMs: sw.ElapsedMilliseconds, level: level);
  206. throw;
  207. }
  208. }
  209. }
  210. /// <summary>关停(退出时调用,flush + 释放 Kafka)。</summary>
  211. public static void Shutdown()
  212. {
  213. lock (_initLock)
  214. {
  215. if (!_initialized) return;
  216. try { _configWatcher?.Dispose(); } catch { }
  217. try { _pipeline?.Dispose(); } catch { }
  218. _initialized = false;
  219. }
  220. }
  221. /// <summary>空传输:纯本地降级时用,Send 直接落兜底由调用方决定(这里丢弃,已在 init 记错误)。</summary>
  222. private sealed class NullTransport : IOplogTransport
  223. {
  224. public void Send(string json) { }
  225. public void Flush(TimeSpan timeout) { }
  226. public void Dispose() { }
  227. }
  228. }
  229. /// <summary>
  230. /// 一次操作的计时 scope。Dispose 时落一条日志。块内异常自动记失败。
  231. /// </summary>
  232. public sealed class OperationScope : IDisposable
  233. {
  234. private readonly string _module;
  235. private readonly string _operation;
  236. private readonly OpLogLevel _level;
  237. private readonly string _operator;
  238. private readonly int? _houseSn;
  239. private readonly int? _wellSn;
  240. private readonly string _tlSn;
  241. private readonly Stopwatch _sw;
  242. private readonly IDisposable _ctxScope;
  243. private object _input;
  244. private object _output;
  245. private string _result;
  246. private string _error;
  247. private bool _disposed;
  248. internal OperationScope(string module, string operation, OpLogLevel level,
  249. string operatorName, int? houseSn, int? wellSn, string tlSn)
  250. {
  251. _module = module;
  252. _operation = operation;
  253. _level = level;
  254. _operator = operatorName;
  255. _houseSn = houseSn;
  256. _wellSn = wellSn;
  257. _tlSn = tlSn;
  258. // 进入 scope 即建立/继承 traceId(父子链)。
  259. _ctxScope = OperationLogContext.BeginScope();
  260. _sw = Stopwatch.StartNew();
  261. }
  262. /// <summary>记录入参。</summary>
  263. public OperationScope Input(object input) { _input = input; return this; }
  264. /// <summary>记录出参。</summary>
  265. public OperationScope Output(object output) { _output = output; return this; }
  266. /// <summary>显式标记成功。</summary>
  267. public OperationScope Success(object output = null)
  268. {
  269. _result = "成功";
  270. if (output != null) _output = output;
  271. return this;
  272. }
  273. /// <summary>显式标记失败 + 错误信息。</summary>
  274. public OperationScope Fail(string error)
  275. {
  276. _result = "失败";
  277. _error = error;
  278. return this;
  279. }
  280. public void Dispose()
  281. {
  282. if (_disposed) return;
  283. _disposed = true;
  284. try
  285. {
  286. _sw.Stop();
  287. // .NET 无法在 Dispose 内可靠探测正在传播的异常。
  288. // 异常路径请用 Fail()(或 Run/RunAsync 包装自动捕获);未显式标记则默认成功。
  289. if (_result == null) _result = "成功";
  290. OperationLogger.Log(
  291. _module, _operation,
  292. input: _input, output: _output,
  293. result: _result, error: _error,
  294. elapsedMs: _sw.ElapsedMilliseconds,
  295. level: _level,
  296. operatorName: _operator,
  297. houseSn: _houseSn, wellSn: _wellSn, tlSn: _tlSn);
  298. }
  299. catch { /* 兜底 */ }
  300. finally
  301. {
  302. try { _ctxScope?.Dispose(); } catch { }
  303. }
  304. }
  305. }
  306. }