| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319 |
- using System;
- using System.Diagnostics;
- namespace Aivfo.OperationLog
- {
- /// <summary>
- /// 操作日志组件门面(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 兜底:日志失败绝不影响业务。
- /// </summary>
- public static class OperationLogger
- {
- private static OperationLogOptions _options;
- private static OperationLogPipeline _pipeline;
- private static LocalFileWriter _local;
- private static readonly object _initLock = new object();
- private static volatile bool _initialized;
- /// <summary>当前配置(运行时可改模块开关:OperationLogger.Options.SetModule(...))。</summary>
- public static OperationLogOptions Options => _options;
- /// <summary>是否已初始化。</summary>
- public static bool Initialized => _initialized;
- /// <summary>用默认 Kafka 传输初始化。</summary>
- public static void Init(Action<OperationLogOptions> configure)
- {
- lock (_initLock)
- {
- if (_initialized) return;
- var opts = new OperationLogOptions();
- configure?.Invoke(opts);
- var local = new LocalFileWriter(opts.LocalLogDir);
- IOplogTransport transport;
- try
- {
- transport = new KafkaOplogTransport(opts.KafkaBootstrapServers, opts.Topic, local.WriteSelfError);
- }
- catch (Exception ex)
- {
- // Kafka 客户端建不起来:降级为只写本地,不抛。
- local.WriteSelfError("Kafka transport init failed, fallback to local-only: " + ex.Message);
- transport = new NullTransport();
- }
- InitCore(opts, transport, local);
- }
- }
- /// <summary>用自定义传输初始化(测试/离线降级用)。</summary>
- 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);
- _initialized = true;
- }
- /// <summary>
- /// 记一条操作日志。input/output 任意对象,内部安全序列化。
- /// level 默认 Info(操作级,发 Kafka);Debug 走本地文件、不入 Kafka。
- /// </summary>
- 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 { }
- }
- }
- /// <summary>
- /// 开启一次带计时的操作 scope。释放时自动记录(耗时、成功/失败、异常→error)。
- /// using (var op = OperationLogger.Begin("串口","打开端口")) { op.Input(x); ...; op.Output(y); }
- /// 块内抛异常时自动记 result=失败 + error=异常摘要,再向上抛。
- /// </summary>
- 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);
- }
- /// <summary>flush(测试/退出用)。</summary>
- public static void Flush(TimeSpan? timeout = null)
- {
- try { _pipeline?.Flush(timeout ?? TimeSpan.FromSeconds(5)); } catch { }
- }
- /// <summary>
- /// 包装执行一个操作:自动计时 + 异常自动记 result=失败/error,然后向上抛。
- /// 这是带异常捕获的推荐写法(Dispose 无法探测异常)。
- /// </summary>
- 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;
- }
- }
- }
- /// <summary>Run 的有返回值版本。</summary>
- public static T Run<T>(string module, string operation, Func<T> 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;
- }
- }
- }
- /// <summary>关停(退出时调用,flush + 释放 Kafka)。</summary>
- public static void Shutdown()
- {
- lock (_initLock)
- {
- if (!_initialized) return;
- try { _pipeline?.Dispose(); } catch { }
- _initialized = false;
- }
- }
- /// <summary>空传输:纯本地降级时用,Send 直接落兜底由调用方决定(这里丢弃,已在 init 记错误)。</summary>
- private sealed class NullTransport : IOplogTransport
- {
- public void Send(string json) { }
- public void Flush(TimeSpan timeout) { }
- public void Dispose() { }
- }
- }
- /// <summary>
- /// 一次操作的计时 scope。Dispose 时落一条日志。块内异常自动记失败。
- /// </summary>
- 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();
- }
- /// <summary>记录入参。</summary>
- public OperationScope Input(object input) { _input = input; return this; }
- /// <summary>记录出参。</summary>
- public OperationScope Output(object output) { _output = output; return this; }
- /// <summary>显式标记成功。</summary>
- public OperationScope Success(object output = null)
- {
- _result = "成功";
- if (output != null) _output = output;
- return this;
- }
- /// <summary>显式标记失败 + 错误信息。</summary>
- 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 { }
- }
- }
- }
- }
|