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 { }
}
}
}
}