using System; using System.Diagnostics; namespace Aivfo.OperationLog { /// /// 操作日志组件门面(14§9)。 /// 用法: /// 启动时一次:OperationLogger.Init(o => { o.Project="operate"; o.KafkaBootstrapServers="localhost:9092"; }); /// 记一条: OperationLogger.Log("串口", "打开端口", input: new{port="COM3"}, result:"成功"); /// 计时 scope: using (OperationLogger.Begin("串口","打开端口")) { ... } // 自动耗时 + 成功/失败 + 异常→error /// 所有 API 全 try 兜底:日志失败绝不影响业务。 /// public static class OperationLogger { private static OperationLogOptions _options; private static OperationLogPipeline _pipeline; private static LocalFileWriter _local; private static OperationLogConfigWatcher _configWatcher; // G3-3:配置热加载 watcher(ConfigFilePath 非空才起) private static readonly object _initLock = new object(); private static volatile bool _initialized; /// 当前配置(运行时可改模块开关:OperationLogger.Options.SetModule(...))。 public static OperationLogOptions Options => _options; /// 是否已初始化。 public static bool Initialized => _initialized; /// 用默认 Kafka 传输初始化。 public static void Init(Action configure) { lock (_initLock) { if (_initialized) return; var opts = new OperationLogOptions(); configure?.Invoke(opts); var local = new LocalFileWriter(opts.LocalLogDir); IOplogTransport transport; try { // G3-3 / 14§11:投递失败(broker 宕/超时/队列满)把整条 JSON 落本地兜底, // 由 pipeline 的补送定时器在 Kafka 恢复后补回,不静默丢。 transport = new KafkaOplogTransport(opts.KafkaBootstrapServers, opts.Topic, onError: local.WriteSelfError, onDeliveryFailed: local.WriteFallback); } catch (Exception ex) { // Kafka 客户端建不起来:降级为只写本地,不抛。 local.WriteSelfError("Kafka transport init failed, fallback to local-only: " + ex.Message); transport = new NullTransport(); } InitCore(opts, transport, local); } } /// 用自定义传输初始化(测试/离线降级用)。 public static void Init(OperationLogOptions opts, IOplogTransport transport) { lock (_initLock) { if (_initialized) return; var local = new LocalFileWriter(opts.LocalLogDir); InitCore(opts, transport ?? new NullTransport(), local); } } private static void InitCore(OperationLogOptions opts, IOplogTransport transport, LocalFileWriter local) { _options = opts; _local = local; _pipeline = new OperationLogPipeline(opts, transport, local); // G3-3 / 14§10:配置集中下发——指定了 ConfigFilePath 才起后台热加载 watcher // (日志微服务/运维把统一配置文件下发覆盖到该路径即热生效,无需重编)。 if (!string.IsNullOrWhiteSpace(opts.ConfigFilePath)) { try { _configWatcher = new OperationLogConfigWatcher(opts, local.WriteSelfError); } catch (Exception ex) { local.WriteSelfError("ConfigWatcher init failed: " + ex.Message); } } _initialized = true; } /// /// 记一条操作日志。input/output 任意对象,内部安全序列化。 /// level 默认 Info(操作级,发 Kafka);Debug 走本地文件、不入 Kafka。 /// public static void Log( string module, string operation, object input = null, object output = null, string result = null, string error = null, long? elapsedMs = null, OpLogLevel level = OpLogLevel.Info, string operatorName = null, int? houseSn = null, int? wellSn = null, string tlSn = null) { try { if (!_initialized) return; // 模块整体关闭则什么都不记(含调试级本地文件)。 if (!_options.IsModuleEnabled(module)) return; var msg = new OperationLogMessage { TraceId = OperationLogContext.TraceId ?? OperationLogContext.NewTraceId(), ParentId = OperationLogContext.ParentId, Time = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(), Project = _options.Project, Module = module, Operation = operation, Operator = operatorName ?? OperationLogContext.Operator, Input = SafeSerializer.Serialize(input), Output = SafeSerializer.Serialize(output), Result = result, Error = error, ElapsedMs = elapsedMs, Level = level == OpLogLevel.Debug ? "DEBUG" : "INFO", HouseSn = houseSn ?? OperationLogContext.HouseSn, WellSn = wellSn ?? OperationLogContext.WellSn, TlSn = tlSn ?? _options.TlSn, Host = _options.Host }; if (level == OpLogLevel.Debug) { // 调试级(14§4):写本地文件、不入 Kafka(除非该模块 MinLevel 显式调到 Debug)。 _local.WriteDebug(msg); if (_options.ShouldSendKafka(module, level)) _pipeline.Enqueue(msg); } else { // 操作级:达到 Kafka 门槛才发;否则也落本地兜底,避免静默丢失。 if (_options.ShouldSendKafka(module, level)) _pipeline.Enqueue(msg); else _local.WriteDebug(msg); } } catch (Exception ex) { try { _local?.WriteSelfError("Log() failed: " + ex.Message); } catch { } } } /// /// 开启一次带计时的操作 scope。释放时自动记录(耗时、成功/失败、异常→error)。 /// using (var op = OperationLogger.Begin("串口","打开端口")) { op.Input(x); ...; op.Output(y); } /// 块内抛异常时自动记 result=失败 + error=异常摘要,再向上抛。 /// public static OperationScope Begin( string module, string operation, OpLogLevel level = OpLogLevel.Info, string operatorName = null, int? houseSn = null, int? wellSn = null, string tlSn = null) { return new OperationScope(module, operation, level, operatorName, houseSn, wellSn, tlSn); } /// flush(测试/退出用)。 public static void Flush(TimeSpan? timeout = null) { try { _pipeline?.Flush(timeout ?? TimeSpan.FromSeconds(5)); } catch { } } /// /// 包装执行一个操作:自动计时 + 异常自动记 result=失败/error,然后向上抛。 /// 这是带异常捕获的推荐写法(Dispose 无法探测异常)。 /// public static void Run(string module, string operation, Action body, object input = null, OpLogLevel level = OpLogLevel.Info) { var sw = Stopwatch.StartNew(); using (OperationLogContext.BeginScope()) { try { body?.Invoke(); sw.Stop(); Log(module, operation, input: input, result: "成功", elapsedMs: sw.ElapsedMilliseconds, level: level); } catch (Exception ex) { sw.Stop(); Log(module, operation, input: input, result: "失败", error: ex.GetType().Name + ": " + ex.Message, elapsedMs: sw.ElapsedMilliseconds, level: level); throw; } } } /// Run 的有返回值版本。 public static T Run(string module, string operation, Func body, object input = null, OpLogLevel level = OpLogLevel.Info) { var sw = Stopwatch.StartNew(); using (OperationLogContext.BeginScope()) { try { var r = body != null ? body() : default; sw.Stop(); Log(module, operation, input: input, output: r, result: "成功", elapsedMs: sw.ElapsedMilliseconds, level: level); return r; } catch (Exception ex) { sw.Stop(); Log(module, operation, input: input, result: "失败", error: ex.GetType().Name + ": " + ex.Message, elapsedMs: sw.ElapsedMilliseconds, level: level); throw; } } } /// 关停(退出时调用,flush + 释放 Kafka)。 public static void Shutdown() { lock (_initLock) { if (!_initialized) return; try { _configWatcher?.Dispose(); } catch { } try { _pipeline?.Dispose(); } catch { } _initialized = false; } } /// 空传输:纯本地降级时用,Send 直接落兜底由调用方决定(这里丢弃,已在 init 记错误)。 private sealed class NullTransport : IOplogTransport { public void Send(string json) { } public void Flush(TimeSpan timeout) { } public void Dispose() { } } } /// /// 一次操作的计时 scope。Dispose 时落一条日志。块内异常自动记失败。 /// public sealed class OperationScope : IDisposable { private readonly string _module; private readonly string _operation; private readonly OpLogLevel _level; private readonly string _operator; private readonly int? _houseSn; private readonly int? _wellSn; private readonly string _tlSn; private readonly Stopwatch _sw; private readonly IDisposable _ctxScope; private object _input; private object _output; private string _result; private string _error; private bool _disposed; internal OperationScope(string module, string operation, OpLogLevel level, string operatorName, int? houseSn, int? wellSn, string tlSn) { _module = module; _operation = operation; _level = level; _operator = operatorName; _houseSn = houseSn; _wellSn = wellSn; _tlSn = tlSn; // 进入 scope 即建立/继承 traceId(父子链)。 _ctxScope = OperationLogContext.BeginScope(); _sw = Stopwatch.StartNew(); } /// 记录入参。 public OperationScope Input(object input) { _input = input; return this; } /// 记录出参。 public OperationScope Output(object output) { _output = output; return this; } /// 显式标记成功。 public OperationScope Success(object output = null) { _result = "成功"; if (output != null) _output = output; return this; } /// 显式标记失败 + 错误信息。 public OperationScope Fail(string error) { _result = "失败"; _error = error; return this; } public void Dispose() { if (_disposed) return; _disposed = true; try { _sw.Stop(); // .NET 无法在 Dispose 内可靠探测正在传播的异常。 // 异常路径请用 Fail()(或 Run/RunAsync 包装自动捕获);未显式标记则默认成功。 if (_result == null) _result = "成功"; OperationLogger.Log( _module, _operation, input: _input, output: _output, result: _result, error: _error, elapsedMs: _sw.ElapsedMilliseconds, level: _level, operatorName: _operator, houseSn: _houseSn, wellSn: _wellSn, tlSn: _tlSn); } catch { /* 兜底 */ } finally { try { _ctxScope?.Dispose(); } catch { } } } } }