OperationLogger.cs 12 KB

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