diff --git a/Ayay.SerilogLogs/LogBootstrapper.cs b/Ayay.SerilogLogs/LogBootstrapper.cs index 3b8c876..6500d3f 100644 --- a/Ayay.SerilogLogs/LogBootstrapper.cs +++ b/Ayay.SerilogLogs/LogBootstrapper.cs @@ -83,7 +83,7 @@ namespace Ayay.SerilogLogs // 定义通用模板:包含了 SourceContext (模块名), TraceId, AppId // 示例: 2026-01-15 12:00:01 [INF] [Algorithm] [Dev01] 计算完成 - string outputTemplate = "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] [{SourceContext}] [{AppId}] {Message:lj}{NewLine}{Exception}"; + string outputTemplate = "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] [{SourceContext}] {Message:lj}{NewLine}{Exception}"; // 3.1 控制台输出 (开发调试用) builder.WriteTo.Async(a => a.Console( @@ -167,6 +167,9 @@ namespace Ayay.SerilogLogs // -------------------------------------------------------- Log.Logger = builder.CreateLogger(); + // 支持 Emoji 显示 + Console.OutputEncoding = Encoding.UTF8; + // -------------------------------------------------------- // 5. 启动后台清理任务 (LogCleaner) // -------------------------------------------------------- diff --git a/Ayay.SerilogLogs/LogModules.cs b/Ayay.SerilogLogs/LogModules.cs index 79e765e..ff22ea4 100644 --- a/Ayay.SerilogLogs/LogModules.cs +++ b/Ayay.SerilogLogs/LogModules.cs @@ -10,7 +10,7 @@ public static string Core { get; } = "Core"; // 系统主逻辑/启动关闭 public static string Network { get; } = "Network"; // 底层网络通讯 (TCP/UDP) public static string WebApi { get; } = "WebAPI"; // 对外 HTTP 接口 - public static string gRpc { get; } = "gRPC"; // 对外 gRPC 接口 + public static string gRpc { get; } = "gRpc"; // 对外 gRpc 接口 public static string WebSocket { get; } = "WebSocket"; // 实时通讯 public static string Ping { get; } = "Ping"; // 心跳/Ping包 (通常量大且不重要) diff --git a/SHH.CameraSdk/Abstractions/IStorageService.cs b/SHH.CameraSdk/Abstractions/IStorageService.cs deleted file mode 100644 index d0dcf3f..0000000 --- a/SHH.CameraSdk/Abstractions/IStorageService.cs +++ /dev/null @@ -1,28 +0,0 @@ -namespace SHH.CameraSdk; - -public interface IStorageService -{ - // 1. 基础属性 - int ProcessId { get; } - - // 2. 设备配置管理 - // 保存:接收 VideoSourceConfig 集合 - Task SaveDevicesAsync(IEnumerable configs); - - // 加载:返回 VideoSourceConfig 列表 - Task> LoadDevicesAsync(); - - // 3. 系统日志 - // 记录系统操作 (如 POST /api/cameras) - Task AppendSystemLogAsync(string action, string ip, string path); - - // 获取系统日志 - Task> GetSystemLogsAsync(int count); - - // 4. 设备审计日志 - // 记录单设备日志 (统一使用 int deviceId) - Task AppendDeviceLogAsync(int deviceId, string message); - - // 获取单设备日志 - Task> GetDeviceLogsAsync(int deviceId, int count); -} \ No newline at end of file diff --git a/SHH.CameraSdk/Controllers/MonitorController.cs b/SHH.CameraSdk/Controllers/MonitorController.cs index c610c9d..2306e07 100644 --- a/SHH.CameraSdk/Controllers/MonitorController.cs +++ b/SHH.CameraSdk/Controllers/MonitorController.cs @@ -14,16 +14,14 @@ public class MonitorController : ControllerBase #region --- 依赖注入 (Dependency Injection) --- private readonly CameraManager _cameraManager; - private readonly IStorageService _storage; // [新增] 存储服务引用 private readonly ProcessingConfigManager _configManager; /// /// 构造函数:注入 CameraManager 和 IStorageService /// - public MonitorController(CameraManager cameraManager, IStorageService storage, ProcessingConfigManager configManager) + public MonitorController(CameraManager cameraManager, ProcessingConfigManager configManager) { _cameraManager = cameraManager; - _storage = storage; _configManager = configManager; } @@ -131,7 +129,6 @@ public class MonitorController : ControllerBase // [修正] 改为从 StorageService 读取文件日志 // 这样即使重启程序,历史日志也能查到 - var logs = await _storage.GetDeviceLogsAsync((int)id, 50); return Ok(new { @@ -151,27 +148,11 @@ public class MonitorController : ControllerBase device.Width, device.Height }, - - // [关键] 持久化日志 - AuditLogs = logs }); } #endregion - /// - /// 获取系统操作日志(读取最新的 50 条) - /// - [HttpGet("system-logs")] - public async Task GetSystemLogs() - { - // [修正] 彻底废弃手动读文件,改用 Service - // Service 内部会自动处理锁、路径 (App_Data/Process_X/system.log) 和异常 - var logs = await _storage.GetSystemLogsAsync(50); - - return Ok(logs); - } - [HttpPost("update-processing")] public IActionResult UpdateProcessing([FromBody] UpdateProcessingRequest request) { diff --git a/SHH.CameraSdk/Core/Features/FrameConsumer.cs b/SHH.CameraSdk/Core/Features/FrameConsumer.cs index 7609baa..881aeea 100644 --- a/SHH.CameraSdk/Core/Features/FrameConsumer.cs +++ b/SHH.CameraSdk/Core/Features/FrameConsumer.cs @@ -1,4 +1,6 @@ -using OpenCvSharp; +using Ayay.SerilogLogs; +using OpenCvSharp; +using Serilog; namespace SHH.CameraSdk; @@ -13,6 +15,8 @@ public class FrameConsumer : IDisposable { #region --- 私有资源与状态 (Private Resources & States) --- + private static ILogger _sysLog = Log.ForContext("SourceContext", LogModules.Core); + /// 帧缓冲队列(容量1):仅存储最新一帧,保证零延迟渲染 /// BlockingCollection 封装线程安全操作,GetConsumingEnumerable 支持取消令牌 private readonly BlockingCollection _frameBuffer = new(1); @@ -49,7 +53,7 @@ public class FrameConsumer : IDisposable // 启动长期运行的渲染任务,提升线程调度优先级 _renderTask = Task.Factory.StartNew(RenderLoop, TaskCreationOptions.LongRunning); - Console.WriteLine($"[Consumer] 渲染线程启动成功,窗口名称: {_windowName}"); + _sysLog.Information($"[Consumer] 渲染线程启动成功,窗口名称: {_windowName}"); } /// @@ -75,7 +79,7 @@ public class FrameConsumer : IDisposable residualFrame.Dispose(); } - Console.WriteLine($"[Consumer] 渲染线程已停止,窗口: {_windowName}"); + _sysLog.Information($"[Consumer] 渲染线程已停止,窗口: {_windowName}"); } #endregion diff --git a/SHH.CameraSdk/Core/Manager/CameraManager.cs b/SHH.CameraSdk/Core/Manager/CameraManager.cs index 67efd07..a49329e 100644 --- a/SHH.CameraSdk/Core/Manager/CameraManager.cs +++ b/SHH.CameraSdk/Core/Manager/CameraManager.cs @@ -1,4 +1,7 @@ -namespace SHH.CameraSdk; +using Ayay.SerilogLogs; +using Serilog; + +namespace SHH.CameraSdk; /// /// [管理层] 视频源总控管理器 (V3.5 持久化集成版) @@ -8,6 +11,8 @@ public class CameraManager : IDisposable, IAsyncDisposable { #region --- 1. 核心资源与状态 (Fields & States) --- + private static ILogger _sysLog = Log.ForContext("SourceContext", LogModules.Core); + /// 全局设备实例池(线程安全),Key = 设备唯一标识 private readonly ConcurrentDictionary _cameraPool = new(); @@ -26,17 +31,13 @@ public class CameraManager : IDisposable, IAsyncDisposable /// private volatile bool _isEngineStarted = false; - // [新增] 存储服务引用 - private readonly IStorageService _storage; - #endregion #region --- 构造函数 (Constructor) --- // [修改] 注入 IStorageService - public CameraManager(IStorageService storage) + public CameraManager() { - _storage = storage; } #endregion @@ -59,17 +60,15 @@ public class CameraManager : IDisposable, IAsyncDisposable { // 如果添加失败(ID冲突),由于 device 还没被使用,直接释放掉 device.DisposeAsync().AsTask().Wait(); + + _sysLog.Warning($"[Core] 设备 ID:{config.Id} 已存在"); + _sysLog.Debug($"[Core] 设备 ID:{config.Id} => 明细:" + "{@cfg}.", config); throw new InvalidOperationException($"设备 ID {config.Id} 已存在"); } // 动态激活逻辑:引擎已启动时,新设备直接标记为运行状态 if (_isEngineStarted) - { device.IsRunning = true; - } - - // [新增] 自动保存到文件 - SaveChanges(); } /// @@ -94,7 +93,7 @@ public class CameraManager : IDisposable, IAsyncDisposable if (_cameraPool.TryRemove(id, out var device)) { // 记录日志 - Console.WriteLine($"[Manager] 正在移除设备 {id}..."); + _sysLog.Information("[Core] 正在移除设备, ID {0} ", id); // 1. 停止物理连接 await device.StopAsync(); @@ -102,10 +101,7 @@ public class CameraManager : IDisposable, IAsyncDisposable // 2. 释放资源 await device.DisposeAsync(); - Console.WriteLine($"[Manager] 设备 {id} 已彻底移除"); - - // [新增] 自动保存到文件 - SaveChanges(); + _sysLog.Warning("[Core] 设备已彻底移除, ID {0} ", id); } } @@ -125,36 +121,7 @@ public class CameraManager : IDisposable, IAsyncDisposable if (_isEngineStarted) return; // ========================================================= - // 1. [新增] 从文件加载设备配置 - // ========================================================= - try - { - Console.WriteLine("[Manager] 正在检查本地配置文件..."); - var savedConfigs = await _storage.LoadDevicesAsync(); - - int loadedCount = 0; - foreach (var config in savedConfigs) - { - // 防止ID冲突(虽然文件里理论上不重复) - if (!_cameraPool.ContainsKey(config.Id)) - { - var device = CreateDeviceInstance(config); - // 默认设为运行状态,让协调器稍后去连接 - //device.IsRunning = true; - _cameraPool.TryAdd(config.Id, device); - loadedCount++; - } - } - if (loadedCount > 0) - Console.WriteLine($"[Manager] 已从文件恢复 {loadedCount} 台设备配置"); - } - catch (Exception ex) - { - Console.WriteLine($"[Manager] 加载配置文件警告: {ex.Message}"); - } - - // ========================================================= - // 2. 全局驱动环境预初始化 + // 1. 全局驱动环境预初始化 // ========================================================= HikSdkManager.Initialize(); @@ -162,7 +129,7 @@ public class CameraManager : IDisposable, IAsyncDisposable _isEngineStarted = true; // ========================================================= - // 3. 启动协调器后台自愈循环 + // 2. 启动协调器后台自愈循环 // ========================================================= _ = Task.Factory.StartNew( () => _coordinator.RunCoordinationLoopAsync(_globalCts.Token), @@ -175,7 +142,7 @@ public class CameraManager : IDisposable, IAsyncDisposable // *注意*:如果 Coordinator 需要显式注册,请在这里补上: foreach (var dev in _cameraPool.Values) _coordinator.Register(dev); - Console.WriteLine($"[CameraManager] 引擎启动成功,当前管理 {_cameraPool.Count} 路相机设备。"); + _sysLog.Warning($"[Core] 设备管理引擎启动成功, 当前管理 {_cameraPool.Count} 路设备"); await Task.CompletedTask; } @@ -191,20 +158,6 @@ public class CameraManager : IDisposable, IAsyncDisposable #region --- 4. 监控数据采集 (Telemetry Collection) --- - /// - /// 获取所有相机的健康度报告 - /// - public IEnumerable GetDetailedTelemetry() - { - return _cameraPool.Values.Select(cam => new CameraHealthReport - { - DeviceId = cam.Id, - Ip = cam.Config.IpAddress, - Status = cam.Status.ToString(), - LastError = cam.Status == VideoSourceStatus.Faulted ? "设备故障或网络中断" : "运行正常" - }); - } - /// /// 获取全量相机实时遥测数据快照 (MonitorController 使用) /// @@ -250,10 +203,13 @@ public class CameraManager : IDisposable, IAsyncDisposable public async Task UpdateDeviceConfigAsync(long deviceId, DeviceUpdateDto dto) { if (!_cameraPool.TryGetValue(deviceId, out var device)) - throw new KeyNotFoundException($"设备 {deviceId} 不存在"); + { + _sysLog.Warning($"[Core] 设备更新制作, ID:{deviceId} 不存在."); + throw new KeyNotFoundException($"设备 ID:{deviceId} 不存在."); + } // 1. 审计 - device.AddAuditLog("收到配置更新请求"); + _sysLog.Debug($"[Core] 响应设备配置更新请求, ID:{deviceId}."); // 2. 创建副本进行对比 var oldConfig = device.Config; @@ -287,7 +243,7 @@ public class CameraManager : IDisposable, IAsyncDisposable if (needColdRestart) { - device.AddAuditLog($"检测到核心参数变更,执行冷重启 (Reboot)"); + _sysLog.Debug($"[Core] 检测到核心参数变更, 执行冷重启, ID:{deviceId}."); bool wasRunning = device.IsRunning; // A. 彻底停止 @@ -301,7 +257,7 @@ public class CameraManager : IDisposable, IAsyncDisposable } else { - device.AddAuditLog($"检测到运行时参数变更,执行热更新 (HotSwap)"); + _sysLog.Debug($"[Core] 检测到运行时参数变更, 执行热更新, ID:{deviceId}."); // A. 更新配置数据 device.UpdateConfig(newConfig); @@ -317,47 +273,6 @@ public class CameraManager : IDisposable, IAsyncDisposable device.ApplyOptions(options); } } - - // [新增] 保存文件 - SaveChanges(); - } - - /// - /// 全量替换更新 (兼容接口) - /// - public async Task UpdateDeviceAsync(int id, VideoSourceConfig newConfig) - { - if (!_cameraPool.TryGetValue(id, out var oldDevice)) - throw new KeyNotFoundException($"设备 #{id} 不存在"); - - bool wasRunning = oldDevice.IsRunning || - oldDevice.Status == VideoSourceStatus.Playing || - oldDevice.Status == VideoSourceStatus.Connecting; - - Console.WriteLine($"[Manager] 正在更新设备 #{id},配置变更中..."); - - try - { - await oldDevice.StopAsync(); - await oldDevice.DisposeAsync(); - } - catch (Exception ex) - { - Console.WriteLine($"[Manager] 销毁旧设备时警告: {ex.Message}"); - } - - var newDevice = CreateDeviceInstance(newConfig); - _cameraPool[id] = newDevice; - - Console.WriteLine($"[Manager] 设备 #{id} 实例已重建。"); - - if (wasRunning) - { - await newDevice.StartAsync(); - } - - // [新增] 保存文件 - SaveChanges(); } #endregion @@ -366,6 +281,10 @@ public class CameraManager : IDisposable, IAsyncDisposable public void Dispose() => DisposeAsync().AsTask().GetAwaiter().GetResult(); + /// + /// 释放资源 + /// + /// public async ValueTask DisposeAsync() { if (_isDisposed) return; @@ -402,31 +321,38 @@ public class CameraManager : IDisposable, IAsyncDisposable #region --- 7. 内部辅助 (Helpers) --- + /// + /// 创建设备实例 + /// + /// + /// private BaseVideoSource CreateDeviceInstance(VideoSourceConfig config) { return config.Brand switch { DeviceBrand.HikVision => new HikVideoSource(config), - _ => throw new NotSupportedException($"不支持的设备品牌: {config.Brand}") + + // 使用模式匹配获取不匹配的值,记录详细的 DTO 上下文 + _ => HandleUnsupportedBrand(config) }; } /// - /// [新增] 触发异步保存 (Fire-and-Forget) - /// 不阻塞当前 API 线程,让后台存储服务去排队写入 + /// 处理不支持的设备品牌 /// - private void SaveChanges() + /// + /// + /// + private BaseVideoSource HandleUnsupportedBrand(VideoSourceConfig config) { - try - { - var allConfigs = _cameraPool.Values.Select(d => d.Config).ToList(); - // 异步调用存储服务,不使用 await 以免阻塞 API 响应 - _ = _storage.SaveDevicesAsync(allConfigs); - } - catch (Exception ex) - { - Console.WriteLine($"[Manager] 触发保存失败: {ex.Message}"); - } + // 1. 构造错误消息 + string errorMsg = $"❌ 不支持的设备品牌: {config.Brand} (ID: {config.Id}, Name: {config.Name})"; + + // 2. 写入日志 - 建议带上 config 的解构信息,方便排查是否是前端传参错误 + _sysLog.Error($"[Core] {errorMsg} | 配置详情: " + "{@Config}", config); + + // 3. 抛出异常,阻止程序进入不确定状态 + throw new NotSupportedException(errorMsg); } #endregion diff --git a/SHH.CameraSdk/Core/Pipeline/GlobalStreamDispatcher.cs b/SHH.CameraSdk/Core/Pipeline/GlobalStreamDispatcher.cs index 1a70ea5..97d7b33 100644 --- a/SHH.CameraSdk/Core/Pipeline/GlobalStreamDispatcher.cs +++ b/SHH.CameraSdk/Core/Pipeline/GlobalStreamDispatcher.cs @@ -1,4 +1,7 @@ -namespace SHH.CameraSdk; +using Ayay.SerilogLogs; +using Serilog; + +namespace SHH.CameraSdk; /// /// 全局流分发器(静态类 | 线程安全) @@ -14,6 +17,8 @@ public static class GlobalStreamDispatcher { #region --- 1. 预设订阅通道 (Predefined Subscription Channels) --- + private static ILogger _sysLog = Log.ForContext("SourceContext", LogModules.Core); + /// /// UI 预览订阅通道:供 UI 模块订阅帧数据,用于实时画面显示 /// 回调参数:(设备唯一标识, 处理后的智能帧数据) @@ -50,7 +55,7 @@ public static class GlobalStreamDispatcher } catch (Exception ex) { - Console.WriteLine($"[GlobalBus Error] 广播异常: {ex.Message}"); + _sysLog.Error($"[GlobalBus] 帧广播分发异常: {ex.Message}"); } // B. 执行你原有的定向分发逻辑 (给处理链用) @@ -151,7 +156,7 @@ public static class GlobalStreamDispatcher if (deviceMap.TryRemove(specificDeviceId, out _)) { // 可选:如果该 AppId 下没设备了,是否清理外层字典?(为了性能通常不清理,或者定期清理) - // Console.WriteLine($"[Dispatcher] {appId} 已停止订阅设备 {specificDeviceId}"); + _sysLog.Information($"[Dispatcher] {appId} 已停止订阅设备 ID:{specificDeviceId }"); } } } @@ -223,7 +228,7 @@ public static class GlobalStreamDispatcher { // 单个订阅者异常隔离,不影响其他分发流程 task.Context.AddLog($"帧任务 [Seq:{sequence}] 投递到 AppId:{appId} 失败:{ex.Message}"); - Console.WriteLine($"[DispatchError] AppId={appId}, DeviceId={deviceId}, Error={ex.Message}"); + _sysLog.Error($"[Dispatch] AppId={appId}, DeviceId={deviceId}, Error={ex.Message}"); } } @@ -242,7 +247,7 @@ public static class GlobalStreamDispatcher } catch (Exception ex) { - Console.WriteLine($"[DispatchError] DeviceSpecific AppId={appId}, Dev={deviceId}: {ex.Message}"); + _sysLog.Error($"[Dispatch] DeviceSpecific AppId={appId}, Dev={deviceId}: {ex.Message}"); } } } @@ -285,7 +290,7 @@ public static class GlobalStreamDispatcher } catch (Exception ex) { - Console.WriteLine($"[SidecarDispatchError] App={sidecarAppId}, Dev={deviceId}: {ex.Message}"); + _sysLog.Error($"[Dispatch] App={sidecarAppId}, Dev={deviceId}: {ex.Message}"); } } } @@ -317,7 +322,7 @@ public static class GlobalStreamDispatcher // TryRemove 是原子的、线程安全的 if (_routingTable.TryRemove(appId, out _)) { - Console.WriteLine($"[Dispatcher] 已强制移除 AppId [{appId}] 的所有订阅路由"); + _sysLog.Error($"[Dispatcher] 已强制移除 AppId [{appId}] 的所有订阅路由."); } } diff --git a/SHH.CameraSdk/Core/Pipeline/ProcessingPipeline.cs b/SHH.CameraSdk/Core/Pipeline/ProcessingPipeline.cs index 19abac1..60671ad 100644 --- a/SHH.CameraSdk/Core/Pipeline/ProcessingPipeline.cs +++ b/SHH.CameraSdk/Core/Pipeline/ProcessingPipeline.cs @@ -1,4 +1,7 @@ -namespace SHH.CameraSdk; +using Ayay.SerilogLogs; +using Serilog; + +namespace SHH.CameraSdk; /// /// 帧处理管道(后台处理核心) @@ -12,6 +15,8 @@ public class ProcessingPipeline { #region --- 私有资源与状态 (Private Resources & States) --- + private static ILogger _sysLog = Log.ForContext("SourceContext", LogModules.Core); + /// 任务队列(有界通道):存储待处理的帧任务 private readonly Channel _queue; @@ -135,7 +140,7 @@ public class ProcessingPipeline catch (Exception ex) { // 捕获处理过程中的异常,避免影响后续任务执行 - Console.WriteLine($"[PipelineError] 帧处理失败 (DeviceId: {task.DeviceId}, Seq: {task.Decision.Sequence}): {ex.Message}"); + _sysLog.Error($"[Pipeline] 帧处理失败 (DeviceId: {task.DeviceId}, Seq: {task.Decision.Sequence}): {ex.Message}"); } finally { diff --git a/SHH.CameraSdk/Core/Resilience/CameraCoordinator.cs b/SHH.CameraSdk/Core/Resilience/CameraCoordinator.cs index 55d7fa1..399e9da 100644 --- a/SHH.CameraSdk/Core/Resilience/CameraCoordinator.cs +++ b/SHH.CameraSdk/Core/Resilience/CameraCoordinator.cs @@ -1,4 +1,6 @@ -using System.Net.NetworkInformation; +using Ayay.SerilogLogs; +using Serilog; +using System.Net.NetworkInformation; using System.Net.Sockets; namespace SHH.CameraSdk; @@ -13,6 +15,8 @@ public class CameraCoordinator { #region --- 私有资源与配置 (Private Resources & Configurations) --- + private static ILogger _sysLog = Log.ForContext("SourceContext", LogModules.Core); + /// 已注册的相机设备集合(线程安全,支持并发添加与遍历) private readonly ConcurrentBag _cameras = new(); @@ -90,7 +94,7 @@ public class CameraCoordinator catch (Exception ex) { // 捕获调度层全局异常,避免循环终止 - Console.WriteLine($"[CoordinatorCritical] 调度循环异常: {ex.Message}"); + _sysLog.Error($"[Coordinator] 调度循环异常: {ex.Message}"); } try @@ -186,7 +190,7 @@ public class CameraCoordinator // 【关键修复】:增加了 && cam.IsRunning 判定,防止待机状态下被误复位 else if (isPhysicalOk && cam.IsOnline && !isFlowing && cam.IsRunning) // [cite: 504] { - Console.WriteLine($"[自愈] 设备 {cam.Id} 僵死({secondsSinceLastFrame:F1}秒无帧),复位中..."); + _sysLog.Warning($"[Coordinator] [自愈] 设备 {cam.Id} 僵死({secondsSinceLastFrame:F1}秒无帧),复位中..."); await cam.StopAsync().ConfigureAwait(false); } } diff --git a/SHH.CameraSdk/Core/Scheduling/FrameController.cs b/SHH.CameraSdk/Core/Scheduling/FrameController.cs index fc61b75..5005d41 100644 --- a/SHH.CameraSdk/Core/Scheduling/FrameController.cs +++ b/SHH.CameraSdk/Core/Scheduling/FrameController.cs @@ -1,4 +1,7 @@ -namespace SHH.CameraSdk; +using Ayay.SerilogLogs; +using Serilog; + +namespace SHH.CameraSdk; /// /// 帧控制器(混合模式最终版) @@ -8,6 +11,8 @@ /// public class FrameController { + private static ILogger _sysLog = Log.ForContext("SourceContext", LogModules.Core); + // 需求字典 private readonly ConcurrentDictionary _requirements = new(); @@ -68,7 +73,7 @@ public class FrameController // 2. 从积分累加器中移除(防止内存泄漏) _accumulators.TryRemove(appId, out _); - Console.WriteLine($"[Scheduler] 已从调度中心彻底移除 AppId: {appId}"); + _sysLog.Warning($"[Core] 帧控制器已从调度中心彻底移除 AppId: {appId}"); } // --------------------------------------------------------- diff --git a/SHH.CameraSdk/Core/ServiceExtensions.cs b/SHH.CameraSdk/Core/ServiceExtensions.cs index b62670f..5660173 100644 --- a/SHH.CameraSdk/Core/ServiceExtensions.cs +++ b/SHH.CameraSdk/Core/ServiceExtensions.cs @@ -22,46 +22,42 @@ namespace SHH.CameraSdk services.AddSingleton(); // ============================================================= - // 2. 图像处理流水线编排 (Pipeline) + // 2. 图像处理流水线编排 (Pipeline) - 修复版 // ============================================================= - // 这里我们利用 Factory 模式在注册时完成链条组装,保持了你原有的逻辑 + + // 1. 先注册下游节点 (Enhance) + // 这样整个系统(包括 Controller 和 Scale)都共享这唯一的一个实例 + services.AddSingleton(sp => + { + var configMgr = sp.GetRequiredService(); + return new ImageEnhanceCluster(4, configMgr); + }); + + // 2. 再注册上游节点 (Scale) 并完成组装 services.AddSingleton(sp => { var configMgr = sp.GetRequiredService(); - // 手动创建实例 + // 创建 Scale 实例 var scale = new ImageScaleCluster(4, configMgr); - var enhance = new ImageEnhanceCluster(4, configMgr); - // ★ 编排流水线:缩放 -> 增亮 + // ★ 关键修复:从容器中获取已经在上面注册好的 Enhance 实例 + // 而不是 new 一个新的 + var enhance = sp.GetRequiredService(); + + // ★ 编排流水线:缩放 -> 增亮 (现在引用的是同一个对象了) scale.SetNext(enhance); - // ★ 全局路由挂载 (兼容旧驱动层) + // ★ 全局路由挂载 GlobalPipelineRouter.SetProcessor(scale); return scale; }); - // 注册 EnhanceCluster,以防 Controller 单独请求它 - // 注意:这里我们通过从 Scale 中获取 Next 来保证是同一个实例链条 - services.AddSingleton(sp => - { - var scale = sp.GetRequiredService(); - // 这里假设链条没变,或者你可以重新 new 一个,但为了保持引用一致性, - // 建议尽量通过主入口访问,或者在这里重新创建独立的(取决于业务需求)。 - // 按照你之前的逻辑,这里为了简单,我们重新注册一个新的或沿用上一个逻辑。 - // *最佳实践*:如果 enhancing 是依附于 scaling 的,通常只注册 Head。 - // 但为了兼容你原代码的 DI 注册: - return new ImageEnhanceCluster(4, sp.GetRequiredService()); - }); - // ============================================================= // 3. 核心业务服务 // ============================================================= - // 文件存储服务 (依赖 processId) - services.AddSingleton(sp => new FileStorageService(processId)); - // 核心设备管理器 (自动注入 IStorageService) services.AddSingleton(); diff --git a/SHH.CameraSdk/Core/Services/BaseFrameProcessor.cs b/SHH.CameraSdk/Core/Services/BaseFrameProcessor.cs index 40e9bea..2f9339b 100644 --- a/SHH.CameraSdk/Core/Services/BaseFrameProcessor.cs +++ b/SHH.CameraSdk/Core/Services/BaseFrameProcessor.cs @@ -1,4 +1,7 @@ -namespace SHH.CameraSdk +using Ayay.SerilogLogs; +using Serilog; + +namespace SHH.CameraSdk { #region --- 架构基类:帧处理集群 (Frame Processor Cluster) --- @@ -15,6 +18,9 @@ where TWorker : BaseWorker { #region --- 受保护成员 --- + + private static ILogger _sysLog = Log.ForContext("SourceContext", LogModules.Core); + /// Worker 线程池,负责具体的帧处理任务 protected readonly List _workers = new List(); @@ -39,7 +45,10 @@ { // 校验并行度参数,避免无效配置 if (workerCount < 1) - throw new ArgumentOutOfRangeException(nameof(workerCount), "Worker数量必须大于0"); + { + _sysLog.Error("[Core] 帧处理集群初始化失败, 线程数必须 > 0."); + throw new ArgumentOutOfRangeException(nameof(workerCount), "帧处理集群初始化失败, 线程数必须 > 0."); + } _configManager = configManager; // 先赋值配置管理器 _workerCount = workerCount; @@ -50,7 +59,7 @@ _workers.Add(CreateWorker(i)); } - Console.WriteLine($"[{serviceName}] 服务已初始化 (并行度: {workerCount})"); + _sysLog.Information($"[Core] 帧处理集群初始化成功, {serviceName} 并行数 {workerCount}. 注: 不能大于CPU核心数."); } #endregion @@ -156,6 +165,8 @@ { #region --- 私有成员 --- + private static ILogger _gRpcLog = Log.ForContext("SourceContext", LogModules.gRpc); + /// 线程内任务队列,容量限制100,防止内存溢出 private readonly BlockingCollection<(long DeviceId, SmartFrame Frame, FrameDecision Decision)> _taskQueue = new BlockingCollection<(long, SmartFrame, FrameDecision)>(100); @@ -195,7 +206,7 @@ { // 背压处理:丢弃当前帧,释放引用计数 frame.Dispose(); - Console.WriteLine($"[Worker] 任务队列已满,丢弃设备 {deviceId} 的帧 (引用计数已释放)"); + _gRpcLog.Debug($"[gRpc] 任务队列已满,BaseWorker 丢弃设备 {deviceId} 的帧."); } } @@ -228,7 +239,7 @@ } catch (Exception ex) { - Console.WriteLine($"[Worker] 帧处理异常: {ex.Message}"); + _gRpcLog.Information($"[gRpc] 帧处理异常,BaseWorker 异常消息: {ex.Message}."); // 异常保底策略:即使处理失败,也透传帧到下一个环节,保证流水线不中断 NotifyFinished(taskItem.DeviceId, frame, taskItem.Decision); @@ -239,11 +250,11 @@ catch (OperationCanceledException) { // 正常取消:线程退出,无需报错 - Console.WriteLine("[Worker] 处理循环已正常终止"); + _gRpcLog.Information($"[gRpc] BaseWorker 处理循环已正常终止."); } catch (Exception ex) { - Console.WriteLine($"[Worker] 处理循环异常终止: {ex.Message}"); + _gRpcLog.Error($"[gRpc] BaseWorker 处理循环异常终止."); } } diff --git a/SHH.CameraSdk/Core/Services/DisplayWindowManager.cs b/SHH.CameraSdk/Core/Services/DisplayWindowManager.cs index 2aa4b95..3ab373f 100644 --- a/SHH.CameraSdk/Core/Services/DisplayWindowManager.cs +++ b/SHH.CameraSdk/Core/Services/DisplayWindowManager.cs @@ -1,4 +1,6 @@ -using OpenCvSharp; +using Ayay.SerilogLogs; +using OpenCvSharp; +using Serilog; namespace SHH.CameraSdk { @@ -17,6 +19,9 @@ namespace SHH.CameraSdk public class DisplayWindowManager : IDisposable { #region --- 内部窗口上下文类 --- + + private static ILogger _sysLog = Log.ForContext("SourceContext", LogModules.Core); + /// /// 单个窗口的上下文信息载体 /// 存储设备关联、运行状态、回调函数等核心数据 @@ -64,7 +69,7 @@ namespace SHH.CameraSdk _cameraManager = cameraManager; // 启动长驻UI线程,设置 LongRunning 提升调度优先级 _uiThread = Task.Factory.StartNew(UILoop, TaskCreationOptions.LongRunning); - Console.WriteLine("[DisplayManager] 渲染引擎就绪 (防僵尸窗口终极版)"); + _sysLog.Information("[DisplayManager] 渲染引擎就绪 (防僵尸窗口终极版)"); } /// @@ -77,7 +82,7 @@ namespace SHH.CameraSdk // 2. 等待 UI 线程退出(最多等待1秒,防止卡死) try { _uiThread.Wait(1000); } - catch (Exception ex) { Console.WriteLine($"[DisplayManager] 线程退出异常: {ex.Message}"); } + catch (Exception ex) { _sysLog.Error($"[DisplayManager] 线程退出异常: {ex.Message}"); } // 3. 强制清理所有活跃窗口 foreach (var appId in _activeWindows.Keys) @@ -95,7 +100,7 @@ namespace SHH.CameraSdk _uiActionQueue.Dispose(); _cts.Dispose(); - Console.WriteLine("[DisplayManager] 渲染引擎已安全销毁"); + _sysLog.Information("[DisplayManager] 渲染引擎已安全销毁"); } #endregion @@ -110,7 +115,7 @@ namespace SHH.CameraSdk // 防重入:已存在该窗口则直接返回 if (_activeWindows.ContainsKey(appId)) return; - Console.WriteLine($"[DisplayManager] 正在启动窗口: {appId} -> Device {deviceId}"); + _sysLog.Information($"[DisplayManager] 正在启动窗口: {appId} -> Device {deviceId}"); // 初始化窗口上下文 var context = new WindowContext @@ -126,7 +131,7 @@ namespace SHH.CameraSdk if (mouseEvent == MouseEventTypes.LButtonDown) { context.IsPaused = !context.IsPaused; - Console.WriteLine($"[DisplayManager] 窗口 {appId} 状态切换: {(context.IsPaused ? "暂停" : "恢复")}"); + _sysLog.Information($"[DisplayManager] 窗口 {appId} 状态切换: {(context.IsPaused ? "暂停" : "恢复")}"); } }; @@ -148,7 +153,7 @@ namespace SHH.CameraSdk } catch (Exception ex) { - Console.WriteLine($"[DisplayManager] 窗口 {appId} 初始化失败: {ex.Message}"); + _sysLog.Error($"[DisplayManager] 窗口 {appId} 初始化失败: {ex.Message}"); } }); @@ -179,7 +184,7 @@ namespace SHH.CameraSdk } catch (Exception ex) { - Console.WriteLine($"[DisplayManager] 帧克隆失败: {ex.Message}"); + _sysLog.Error($"[DisplayManager] 帧克隆失败: {ex.Message}"); return; } @@ -223,7 +228,7 @@ namespace SHH.CameraSdk // 从注册表中移除窗口上下文 if (_activeWindows.TryRemove(appId, out var context)) { - Console.WriteLine($"[DisplayManager] 正在清理窗口资源: {appId}"); + _sysLog.Information($"[DisplayManager] 正在清理窗口资源: {appId}"); // 步骤1:立即取消帧数据流订阅 GlobalStreamDispatcher.Unsubscribe(appId); @@ -237,7 +242,7 @@ namespace SHH.CameraSdk } catch (Exception ex) { - Console.WriteLine($"[DisplayManager] 窗口 {appId} 销毁失败: {ex.Message}"); + _sysLog.Error($"[DisplayManager] 窗口 {appId} 销毁失败: {ex.Message}"); } }); @@ -257,7 +262,7 @@ namespace SHH.CameraSdk if (_activeWindows.TryGetValue(appId, out var ctx)) { ctx.IsPaused = true; - Console.WriteLine($"[DisplayManager] 窗口 {appId} 已暂停"); + _sysLog.Information($"[DisplayManager] 窗口 {appId} 已暂停"); } } @@ -270,7 +275,7 @@ namespace SHH.CameraSdk if (_activeWindows.TryGetValue(appId, out var ctx)) { ctx.IsPaused = false; - Console.WriteLine($"[DisplayManager] 窗口 {appId} 已恢复"); + _sysLog.Information($"[DisplayManager] 窗口 {appId} 已恢复"); } } #endregion @@ -290,7 +295,7 @@ namespace SHH.CameraSdk var device = _cameraManager.GetDevice(deviceId); if (device == null) { - Console.WriteLine($"[策略联动] 设备 {deviceId} 不存在"); + _sysLog.Information($"[策略联动] 设备 {deviceId} 不存在"); return; } @@ -298,7 +303,7 @@ namespace SHH.CameraSdk var frameController = device.Controller; if (frameController == null) { - Console.WriteLine($"[策略联动] 设备 {deviceId} 未配置帧控制器"); + _sysLog.Information($"[策略联动] 设备 {deviceId} 未配置帧控制器"); return; } @@ -306,12 +311,12 @@ namespace SHH.CameraSdk if (fps > 0) { frameController.Register(appId, fps); - Console.WriteLine($"[策略联动] ✅ 已注册流控: {appId} -> {fps} FPS"); + _sysLog.Information($"[策略联动] ✅ 已注册流控: {appId} -> {fps} FPS"); } else { frameController.Unregister(appId); - Console.WriteLine($"[策略联动] 🗑️ 已注销流控: {appId}"); + _sysLog.Information($"[策略联动] 🗑️ 已注销流控: {appId}"); } // 记录审计日志,用于前端排查问题 @@ -319,7 +324,7 @@ namespace SHH.CameraSdk } catch (Exception ex) { - Console.WriteLine($"[策略联动] ❌ 联动失败: {ex.Message}"); + _sysLog.Error($"[策略联动] ❌ 联动失败: {ex.Message}"); } } #endregion @@ -360,7 +365,7 @@ namespace SHH.CameraSdk } catch (Exception ex) { - Console.WriteLine($"[UI] 渲染循环异常: {ex.Message}"); + _sysLog.Error($"[UI] 渲染循环异常: {ex.Message}"); } } @@ -385,7 +390,7 @@ namespace SHH.CameraSdk // Visible < 1.0 表示窗口已被用户手动关闭 if (Cv2.GetWindowProperty(appId, WindowPropertyFlags.Visible) < 1.0) { - Console.WriteLine($"[UI] 检测到窗口 {appId} 已被手动关闭,触发清理..."); + _sysLog.Information($"[UI] 检测到窗口 {appId} 已被手动关闭,触发清理..."); // 异步清理:避免 StopDisplay 内部的队列操作阻塞 UI 线程 Task.Run(() => StopDisplay(appId)); } diff --git a/SHH.CameraSdk/Core/Services/FileStorageService.cs b/SHH.CameraSdk/Core/Services/FileStorageService.cs deleted file mode 100644 index 64c88ca..0000000 --- a/SHH.CameraSdk/Core/Services/FileStorageService.cs +++ /dev/null @@ -1,172 +0,0 @@ -using System.Text.Json; - -namespace SHH.CameraSdk; - -public class FileStorageService : IStorageService -{ - public int ProcessId { get; } - private readonly string _baseDir; - private readonly string _devicesPath; - private readonly string _systemLogPath; // 系统日志路径 - private readonly string _logsDir; // 设备日志文件夹 - - // 【关键优化】双锁分离:配置读写和日志读写互不干扰 - private readonly SemaphoreSlim _configLock = new SemaphoreSlim(1, 1); - private readonly SemaphoreSlim _logLock = new SemaphoreSlim(1, 1); - - // JSON 配置 (保持不变) - private readonly JsonSerializerOptions _jsonOptions = new JsonSerializerOptions - { - WriteIndented = true, - IncludeFields = true, - PropertyNameCaseInsensitive = true, - NumberHandling = JsonNumberHandling.AllowReadingFromString - }; - - public FileStorageService(int processId) - { - ProcessId = processId; - - // 目录结构: - // App_Data/Process_1/ - // ├── devices.json - // ├── system.log - // └── logs/ - // ├── device_101.log - // └── device_102.log - - _baseDir = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "App_Data", $"Process_{processId}"); - _devicesPath = Path.Combine(_baseDir, "devices.json"); - _systemLogPath = Path.Combine(_baseDir, "system.log"); - _logsDir = Path.Combine(_baseDir, "logs"); - - if (!Directory.Exists(_baseDir)) Directory.CreateDirectory(_baseDir); - if (!Directory.Exists(_logsDir)) Directory.CreateDirectory(_logsDir); - - Console.WriteLine($"[Storage] 服务就绪 | 日志路径: {_systemLogPath}"); - } - - // ================================================================== - // 1. 设备配置管理 (使用 _configLock) - // ================================================================== - - public async Task SaveDevicesAsync(IEnumerable configs) - { - await _configLock.WaitAsync(); - try - { - if (SdkGlobal.SaveCameraConfigEnable) - { - var json = JsonSerializer.Serialize(configs, _jsonOptions); - await File.WriteAllTextAsync(_devicesPath, json); - } - } - catch (Exception ex) - { - Console.WriteLine($"[Storage] ❌ 保存配置失败: {ex.Message}"); - } - finally { _configLock.Release(); } - } - - public async Task> LoadDevicesAsync() - { - if (!File.Exists(_devicesPath)) return new List(); - - await _configLock.WaitAsync(); - try - { - if (!SdkGlobal.SaveCameraConfigEnable) - return new List(); - - var json = await File.ReadAllTextAsync(_devicesPath); - if (string.IsNullOrWhiteSpace(json)) return new List(); - - var list = JsonSerializer.Deserialize>(json, _jsonOptions); - return list ?? new List(); - //return new List(); - } - catch (Exception ex) - { - Console.WriteLine($"[Storage] ❌ 读取配置失败: {ex.Message}"); - return new List(); - } - finally { _configLock.Release(); } - } - - // ================================================================== - // 2. 系统操作日志 (使用 _logLock) - // ================================================================== - - public async Task AppendSystemLogAsync(string action, string ip, string path) - { - // 格式: [时间] | IP | 动作 路径 - var time = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"); - var line = $"[{time}] | {ip} | {action} {path}"; - - await _logLock.WaitAsync(); // 等待日志锁 - try - { - // 追加写入 (Async) - await File.AppendAllTextAsync(_systemLogPath, line + Environment.NewLine); - } - catch { /* 忽略日志写入错误,别崩了主程序 */ } - finally { _logLock.Release(); } - } - - public async Task> GetSystemLogsAsync(int count) - { - if (!File.Exists(_systemLogPath)) return new List { "暂无日志" }; - - await _logLock.WaitAsync(); - try - { - // 读取所有行 (如果日志文件非常大,这里建议优化为倒序读取,但几MB以内没问题) - var lines = await File.ReadAllLinesAsync(_systemLogPath); - - // 取最后 N 行,并反转(让最新的显示在最上面) - return lines.TakeLast(count).Reverse().ToList(); - } - catch (Exception ex) - { - return new List { $"读取失败: {ex.Message}" }; - } - finally { _logLock.Release(); } - } - - // ================================================================== - // 3. 设备审计日志 (使用 _logLock) - // ================================================================== - - public async Task AppendDeviceLogAsync(int deviceId, string message) - { - var path = Path.Combine(_logsDir, $"device_{deviceId}.log"); - var time = DateTime.Now.ToString("MM-dd HH:mm:ss"); - var line = $"{time} > {message}"; - - await _logLock.WaitAsync(); // 复用日志锁,防止多文件同时IO导致磁盘抖动 - try - { - await File.AppendAllTextAsync(path, line + Environment.NewLine); - } - catch { } - finally { _logLock.Release(); } - } - - public async Task> GetDeviceLogsAsync(int deviceId, int count) - { - var path = Path.Combine(_logsDir, $"device_{deviceId}.log"); - if (!File.Exists(path)) return new List(); - - await _logLock.WaitAsync(); - try - { - var lines = await File.ReadAllLinesAsync(path); - return lines.TakeLast(count).Reverse().ToList(); - } - catch - { - return new List(); - } - finally { _logLock.Release(); } - } -} \ No newline at end of file diff --git a/SHH.CameraSdk/Core/Services/ProcessingConfigManager.cs b/SHH.CameraSdk/Core/Services/ProcessingConfigManager.cs index c851d49..1d9b692 100644 --- a/SHH.CameraSdk/Core/Services/ProcessingConfigManager.cs +++ b/SHH.CameraSdk/Core/Services/ProcessingConfigManager.cs @@ -1,4 +1,7 @@ -namespace SHH.CameraSdk; +using Ayay.SerilogLogs; +using Serilog; + +namespace SHH.CameraSdk; /// /// [配置中心] 预处理参数管理器 @@ -6,6 +9,8 @@ /// public class ProcessingConfigManager { + private static ILogger _sysLog = Log.ForContext("SourceContext", LogModules.Core); + // 内存字典:Key=设备ID, Value=配置对象 private readonly ConcurrentDictionary _configs = new(); @@ -30,7 +35,7 @@ public class ProcessingConfigManager // 直接覆盖旧配置,由于是引用替换,原子性较高 _configs.AddOrUpdate(deviceId, newOptions, (key, old) => newOptions); - Console.WriteLine($"[ConfigManager] 设备 {deviceId} 预处理参数已更新: " + + _sysLog.Information($"[ConfigManager] 设备 {deviceId} 预处理参数已更新: " + $"Expand={newOptions.EnableExpand} Shrink:{newOptions.EnableShrink} 分辨率:({newOptions.TargetWidth}x{newOptions.TargetHeight}), " + $"EnableBrightness}}={newOptions.EnableBrightness}"); } diff --git a/SHH.CameraSdk/Temp/UserActionFilter.cs b/SHH.CameraSdk/Temp/UserActionFilter.cs index 16f3250..f6ea4fb 100644 --- a/SHH.CameraSdk/Temp/UserActionFilter.cs +++ b/SHH.CameraSdk/Temp/UserActionFilter.cs @@ -9,13 +9,10 @@ namespace SHH.CameraSdk; /// public class UserActionFilter : IActionFilter { - private readonly IStorageService _storage; - // 【关键点】构造函数注入 // ASP.NET Core 会自动把我们在 Program.cs 中注册的 IStorageService 实例传进来 - public UserActionFilter(IStorageService storage) + public UserActionFilter() { - _storage = storage; } /// @@ -32,11 +29,6 @@ public class UserActionFilter : IActionFilter if (method != "GET") { var ip = context.HttpContext.Connection.RemoteIpAddress?.ToString() ?? "Unknown"; - - // 3. 调用存储服务写入日志 - // 注意:这里我们不等待任务完成 (Fire-and-Forget),以免日志写入拖慢 API 响应速度 - // 因为 _storage.AppendSystemLogAsync 内部目前是空实现(Task.CompletedTask),所以这里绝对不会卡顿 - _ = _storage.AppendSystemLogAsync(method, ip, path); } } diff --git a/SHH.CameraService/Bootstrapper.cs b/SHH.CameraService/Bootstrapper.cs index f3e3e9a..1f24a74 100644 --- a/SHH.CameraService/Bootstrapper.cs +++ b/SHH.CameraService/Bootstrapper.cs @@ -38,8 +38,8 @@ public static class Bootstrapper "--uris", "localhost,9001,command,调试PC;", // 日志中心配置 (格式: IP,Port,Desc) - "--sequris", "58.216.225.5,20026,日志处置中心;", - "--seqkey", "Shine101173874928;", + "--sequris", "172.16.41.241,20026,日志处置中心;", + "--seqkey", "Shine899195994250;", // 端口策略 "--mode", "1", @@ -100,11 +100,11 @@ public static class Bootstrapper // ========================================================= if (exitCode != 0) { - sysLog.Fatal($"💀 [程序终止] {reason} (Code: {exitCode})"); + sysLog.Fatal($"[Core] 💀 [程序终止] {reason} (Code: {exitCode})"); } else { - sysLog.Information($"👋 [程序退出] {reason}"); + sysLog.Information($"[Core] 👋 [程序退出] {reason}"); } // ========================================================= @@ -113,7 +113,7 @@ public static class Bootstrapper // 防止 SDK 句柄残留导致下次启动无法连接相机 try { - sysLog.Information("正在清理 Hikvision SDK 资源..."); + sysLog.Information("[Core] 正在清理 Hikvision SDK 资源..."); // 如果你的项目中引用了 SDK,请务必解开这行注释 HikNativeMethods.NET_DVR_Cleanup(); @@ -162,7 +162,7 @@ public static class Bootstrapper /// public static int ScanForAvailablePort(ServiceConfig config, ILogger logger) { - logger.Information($"🔍 开始端口检测: 起始={config.BasePort}, 范围={config.MaxPortRange}"); + logger.Information($"[Core] 🔍 开始端口检测: 起始={config.BasePort}, 范围={config.MaxPortRange}"); for (int i = 0; i <= config.MaxPortRange; i++) { @@ -171,15 +171,15 @@ public static class Bootstrapper { if (currentPort != config.BasePort) { - logger.Warning($"⚙️ 端口自动漂移: {config.BasePort} -> {currentPort}"); + logger.Warning($"[Core] ⚙️ 端口自动漂移: {config.BasePort} -> {currentPort}"); } else { - logger.Information($"✅ 端口检测通过: {currentPort}"); + logger.Information($"[Core] ✅ 端口检测通过: {currentPort}"); } return currentPort; } - logger.Debug($"⚠️ 端口 {currentPort} 被占用,尝试下一个..."); + logger.Debug($"[Core] ⚠️ 端口 {currentPort} 被占用,尝试下一个..."); } return -1; } @@ -217,16 +217,16 @@ public static class Bootstrapper /// public static void WarmUpHardware(ILogger logger) { - logger.Information("Hik Sdk 开始预热."); + logger.Information("[Core] Hik Sdk 开始预热."); try { HikNativeMethods.NET_DVR_Init(); HikSdkManager.ForceWarmUp(); - logger.Information("💡Hik Sdk 预热成功."); + logger.Information("[Core] 💡Hik Sdk 预热成功."); } catch (Exception ex) { - logger.Error(ex, "⚠️ Hik Sdk 预热失败."); + logger.Error(ex, "[Core] ⚠️ Hik Sdk 预热失败."); } } @@ -244,13 +244,13 @@ public static class Bootstrapper try { - // 将 tcp:// 转换为 http:// 以适配 gRPC + // 将 tcp:// 转换为 http:// 以适配 gRpc string targetUrl = config.CommandEndpoints.First().Uri.Replace("tcp://", "http://"); using var channel = GrpcChannel.ForAddress(targetUrl); var client = new GatewayProvider.GatewayProviderClient(channel); - gRpcLog.Information($"[gRPC] 正在执行预注册: {targetUrl}"); + gRpcLog.Information($"[gRpc] 正在执行预注册: {targetUrl}"); var resp = await client.RegisterInstanceAsync(new RegisterRequest { InstanceId = config.AppId, @@ -261,11 +261,11 @@ public static class Bootstrapper ProcessId = Environment.ProcessId, Description = "" }); - gRpcLog.Information($"💡[gRPC] 预注册成功: {resp.Message}"); + gRpcLog.Information($"[gRpc] 💡预注册成功: {resp.Message}"); } catch (Exception ex) { - gRpcLog.Error($"⚠️ [gRPC] 预注册尝试失败: {ex.Message}"); + gRpcLog.Error($"[gRpc] ⚠️ 预注册尝试失败: {ex.Message}"); } } diff --git a/SHH.CameraService/Core/CommandDispatcher.cs b/SHH.CameraService/Core/CommandDispatcher.cs index 917e2e3..8a26f09 100644 --- a/SHH.CameraService/Core/CommandDispatcher.cs +++ b/SHH.CameraService/Core/CommandDispatcher.cs @@ -6,7 +6,7 @@ using SHH.Contracts.Grpc; namespace SHH.CameraService; /// -/// gRPC 指令分发器 +/// gRpc 指令分发器 /// 职责:接收从 GrpcCommandReceiverWorker 传入的 Proto 消息,解析参数并路由至具体的 Handler。 /// public class CommandDispatcher @@ -29,14 +29,14 @@ public class CommandDispatcher /// /// 执行指令分发 /// - /// 从 gRPC Server Streaming 接收到的原始 Proto 指令对象 + /// 从 gRpc Server Streaming 接收到的原始 Proto 指令对象 public async Task DispatchAsync(CommandPayloadProto protoMsg) { if (protoMsg == null) return; string cmdCode = protoMsg.CmdCode; // 例如 "Sync_Camera" - _gRpcLog.Information($"[gRPC] 响应请求, 业务:{protoMsg.CmdCode}, 请求ID:{protoMsg.RequestId}, 业务分发."); - _gRpcLog.Debug($"[gRPC] 响应请求, {protoMsg.CmdCode}, 请求ID:{protoMsg.RequestId}, 业务分发 => {protoMsg}"); + _gRpcLog.Information($"[gRpc] 响应请求, 业务:{protoMsg.CmdCode}, 请求ID:{protoMsg.RequestId}, 业务分发."); + _gRpcLog.Debug($"[gRpc] 响应请求, {protoMsg.CmdCode}, 请求ID:{protoMsg.RequestId}, 业务分发 => {protoMsg}"); try { @@ -51,20 +51,20 @@ public class CommandDispatcher // 3. 调用具体业务执行 await handler.ExecuteAsync(token); - _gRpcLog.Information($"[gRPC] 业务:{protoMsg.CmdCode}, 请求ID:{protoMsg.RequestId}, 执行成功."); + _gRpcLog.Information($"[gRpc] 业务:{protoMsg.CmdCode}, 请求ID:{protoMsg.RequestId}, 执行成功."); } else { - _gRpcLog.Warning($"[gRPC] 业务:{protoMsg.CmdCode}, 请求ID:{protoMsg.RequestId}, 未找到指令处理器."); + _gRpcLog.Warning($"[gRpc] 业务:{protoMsg.CmdCode}, 请求ID:{protoMsg.RequestId}, 未找到指令处理器."); } } catch (Exception ex) { - _gRpcLog.Error($"[gRPC] 业务:{protoMsg.CmdCode}, 请求ID:{protoMsg.RequestId}, 执行指令处理异常: {ex.Message}."); + _gRpcLog.Error($"[gRpc] 业务:{protoMsg.CmdCode}, 请求ID:{protoMsg.RequestId}, 执行指令处理异常: {ex.Message}."); } // 注意:关于 ACK (require_ack) - // 在 NetMQ 时代需要手动回发结果,在 gRPC Server Streaming 模式下, + // 在 NetMQ 时代需要手动回发结果,在 gRpc Server Streaming 模式下, // 建议通过 Unary RPC (例如另设一个 ReportCommandResult 方法) 异步上报执行结果。 } } \ No newline at end of file diff --git a/SHH.CameraService/Core/ParentProcessSentinel.cs b/SHH.CameraService/Core/ParentProcessSentinel.cs index 5b68170..80af562 100644 --- a/SHH.CameraService/Core/ParentProcessSentinel.cs +++ b/SHH.CameraService/Core/ParentProcessSentinel.cs @@ -1,44 +1,52 @@ -using System.Diagnostics; +using Ayay.SerilogLogs; using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; +using Serilog; using SHH.CameraSdk; +using System.Diagnostics; namespace SHH.CameraService; +/// +/// 父进程守护服务 (BackgroundService) +/// 核心逻辑:定期检查启动本服务的父进程是否存活,若父进程退出(如 UI 崩溃),则触发本服务自动退出,避免孤儿进程占用相机硬件资源。 +/// public class ParentProcessSentinel : BackgroundService { private readonly ServiceConfig _config; private readonly IHostApplicationLifetime _lifetime; - private readonly ILogger _logger; + private static ILogger _sysLog = Log.ForContext("SourceContext", LogModules.Core); + /// + /// 使用统一的结构化日志记录器,SourceContext 设置为 Core 模块 + /// public ParentProcessSentinel( ServiceConfig config, - IHostApplicationLifetime lifetime, - ILogger logger) + IHostApplicationLifetime lifetime) { _config = config; _lifetime = lifetime; - _logger = logger; } + /// + /// 执行后台守护逻辑 + /// protected override async Task ExecuteAsync(CancellationToken stoppingToken) { int pid = _config.ParentPid; - - // 如果 PID 为 0 或负数,说明不需要守护(可能是手动启动调试) + // 1. 验证 PID 合法性。如果 PID 为 0 或负数,可能是手动启动调试模式,不执行守护逻辑 if (pid <= 0) { - _logger.LogInformation("未指定有效的父进程 PID,守护模式已禁用。"); + _sysLog.Warning("[Sentinel] 未指定有效的父进程 PID ({ParentPid}),守护模式已禁用,服务将持续运行.", pid); return; } - _logger.LogInformation($"父进程守护已启动,正在监控 PID: {pid}"); + _sysLog.Information("[Sentinel] 父进程守护已启动,正在监控目标 PID: {ParentPid}", pid); while (!stoppingToken.IsCancellationRequested) { if (!IsParentRunning(pid)) { - _logger.LogWarning($"[ALERT] 检测到父进程 (PID:{pid}) 已退出!正在终止当前服务..."); + _sysLog.Warning("[Sentinel] ### ALERT ### 检测到父进程 (PID:{ParentPid}) 已退出!正在下发系统终止信号...", pid); // 触发程序优雅退出 _lifetime.StopApplication(); @@ -52,6 +60,11 @@ public class ParentProcessSentinel : BackgroundService } } + /// + /// 核心状态判定:通过 PID 获取进程快照并检查存活状态 + /// + /// 父进程 ID + /// 存活返回 True,已消亡返回 False private bool IsParentRunning(int pid) { try @@ -72,7 +85,7 @@ public class ParentProcessSentinel : BackgroundService } catch (Exception ex) { - _logger.LogError(ex, "检查父进程状态时发生未知错误,默认为存活"); + _sysLog.Debug("[Sentinel] 无法定位 PID 为 {ParentPid} 的进程,判定为已退出.", pid); return true; // 发生未知错误时,保守起见认为它还活着 } } diff --git a/SHH.CameraService/GrpcImpls/Handlers/DeviceConfigHandler.cs b/SHH.CameraService/GrpcImpls/Handlers/DeviceConfigHandler.cs index f7b68e8..10334b3 100644 --- a/SHH.CameraService/GrpcImpls/Handlers/DeviceConfigHandler.cs +++ b/SHH.CameraService/GrpcImpls/Handlers/DeviceConfigHandler.cs @@ -12,6 +12,7 @@ namespace SHH.CameraService; public class DeviceConfigHandler : ICommandHandler { private static ILogger _sysLog = Log.ForContext("SourceContext", LogModules.Core); + private readonly CameraManager _cameraManager; /// @@ -42,12 +43,15 @@ public class DeviceConfigHandler : ICommandHandler // 2. 尝试获取现有设备 var device = _cameraManager.GetDevice(dto.Id); + string op = device != null ? "更新" : "新增"; + _sysLog.Warning($"[Sync] 即将{op}设备配置 => ID:{dto.Id} Name:{dto.Name} IP:{dto.IpAddress} Port:{dto.Port} Brand:{(DeviceBrand)dto.Brand} Rtsp:{dto.RtspPath}"); + _sysLog.Debug($"[Sync] 即将{op}设备配置 => ID:{dto.Id} Name:{dto.Name} IP:{dto.IpAddress} 详情:" + "{@dto}", dto, dto.AutoSubscriptions); + if (device != null) { // ========================================================= // 场景 A: 设备已存在 -> 执行智能更新 (Smart Update) // ========================================================= - Console.WriteLine($"[Sync] 更新设备配置: {dto.Id} ({dto.Name})"); // 将全量配置映射为部分更新 DTO var updateDto = new DeviceUpdateDto @@ -86,7 +90,6 @@ public class DeviceConfigHandler : ICommandHandler // ========================================================= // 场景 B: 设备不存在 -> 执行新增 (Add New) // ========================================================= - Console.WriteLine($"[Sync] 新增设备: {dto.Id} ({dto.Name})"); // 构造全新的设备配置 var newConfig = new VideoSourceConfig @@ -126,7 +129,7 @@ public class DeviceConfigHandler : ICommandHandler // 情况 1: 收到“启动”指令 if (!device.IsOnline) // 只有没在线时才点火 { - Console.WriteLine($"[Sync] 指令:立即启动设备 {dto.Id}"); + _sysLog.Warning($"[Sync] 设备立即启动 => ID:{dto.Id} Name:{dto.Name} IP:{dto.IpAddress} Port:{dto.Port} Brand:{(DeviceBrand)dto.Brand} Rtsp:{dto.RtspPath}"); _ = device.StartAsync(); } } @@ -135,7 +138,7 @@ public class DeviceConfigHandler : ICommandHandler // 情况 2: 收到“停止”指令 (即 ImmediateExecution = false) if (device.IsOnline) // 只有在线时才熄火 { - Console.WriteLine($"[Sync] 指令:立即停止设备 {dto.Id}"); + _sysLog.Warning($"[Sync] 设备立即停止 {dto.Id}"); _ = device.StopAsync(); } } diff --git a/SHH.CameraService/GrpcImpls/Handlers/DeviceStatusHandler.cs b/SHH.CameraService/GrpcImpls/Handlers/DeviceStatusHandler.cs index 5653510..d324e99 100644 --- a/SHH.CameraService/GrpcImpls/Handlers/DeviceStatusHandler.cs +++ b/SHH.CameraService/GrpcImpls/Handlers/DeviceStatusHandler.cs @@ -1,7 +1,8 @@ -using Grpc.Core; +using Ayay.SerilogLogs; +using Grpc.Core; using Grpc.Net.Client; using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; +using Serilog; using SHH.CameraSdk; using SHH.Contracts; using SHH.Contracts.Grpc; @@ -10,14 +11,15 @@ using System.Collections.Concurrent; namespace SHH.CameraService; /// -/// 设备状态监控工作者 (gRPC 版) -/// 职责:监控相机状态并在状态变更或心跳周期内,通过 gRPC 批量上报至所有配置的端点 +/// 设备状态监控工作者 (gRpc 版) +/// 职责:监控相机状态并在状态变更或心跳周期内,通过 gRpc 批量上报至所有配置的端点 /// public class DeviceStatusHandler : BackgroundService { + private static ILogger _gRpcLog = Log.ForContext("SourceContext", LogModules.gRpc); + private readonly CameraManager _manager; private readonly ServiceConfig _config; - private readonly ILogger _logger; // 状态存储:CameraId -> 状态载荷 private readonly ConcurrentDictionary _stateStore = new(); @@ -27,12 +29,10 @@ public class DeviceStatusHandler : BackgroundService public DeviceStatusHandler( CameraManager manager, - ServiceConfig config, - ILogger logger) + ServiceConfig config) { _manager = manager; _config = config; - _logger = logger; } protected override async Task ExecuteAsync(CancellationToken stoppingToken) @@ -46,7 +46,7 @@ public class DeviceStatusHandler : BackgroundService // 2. 订阅 SDK 状态变更事件 _manager.OnDeviceStatusChanged += OnSdkStatusChanged; - _logger.LogInformation("[StatusWorker] gRPC 状态上报已启动,配置节点数: {Count}", _config.CommandEndpoints.Count); + _gRpcLog.Information($"[gRpc] 状态上报已启动,配置节点数: {_config.CommandEndpoints.Count}"); // 3. 定时循环 (1秒1次检查) var timer = new PeriodicTimer(TimeSpan.FromSeconds(1)); @@ -60,7 +60,7 @@ public class DeviceStatusHandler : BackgroundService catch (OperationCanceledException) { /* 正常退出 */ } catch (Exception ex) { - _logger.LogError(ex, "[StatusWorker] 运行异常"); + _gRpcLog.Error($"[gRpc] 状态上报运行异常"); } finally { @@ -96,12 +96,12 @@ public class DeviceStatusHandler : BackgroundService { long now = Environment.TickCount64; - // 策略: 有变更(Dirty) 或 超过5秒(强制心跳) - bool shouldSend = _isDirty || (now - _lastSendTick > 5000); + // 策略: 有变更(Dirty) 或 超过 2 秒(强制心跳) + bool shouldSend = _isDirty || (now - _lastSendTick > 2000); if (shouldSend && _config.CommandEndpoints.Any()) { - // 1. 构建 gRPC 请求包 + // 1. 构建 gRpc 请求包 var request = new StatusBatchRequest { Timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() @@ -129,13 +129,12 @@ public class DeviceStatusHandler : BackgroundService using var channel = GrpcChannel.ForAddress(grpcUrl); var client = new GatewayProvider.GatewayProviderClient(channel); - // 获取 gRPC 内部生成的服务全称 + // 获取 gRpc 内部生成的服务全称 // 这就是客户端尝试调用的真实路径:/包名.服务名/方法名 - var methodName = "ReportStatusBatch"; var serviceName = client.GetType().DeclaringType?.Name ?? "Unknown"; - _logger.LogInformation("[gRPC Debug] 准备调用端点: {Url}", grpcUrl); - _logger.LogInformation("[gRPC Debug] 客户端契约服务名: {Service}", serviceName); + _gRpcLog.Debug("[gRpc] 准备调用端点: {Url}", grpcUrl); + _gRpcLog.Debug("[gRpc] 客户端契约服务名: {Service}", serviceName); // 执行调用 var response = await client.ReportStatusBatchAsync(request, @@ -143,7 +142,8 @@ public class DeviceStatusHandler : BackgroundService if (response.Success) { - _logger.LogInformation("[gRPC Success] 上报成功"); + _gRpcLog.Information("[gRpc] 设备状态上报成功, 共计: {Count} 个, Url: {Url}", request.Items.Count, grpcUrl); + _gRpcLog.Debug("[gRpc] 设备状态上报成功: {Url} Items:{Items}", grpcUrl, request.Items); _isDirty = false; _lastSendTick = Environment.TickCount64; } @@ -151,17 +151,17 @@ public class DeviceStatusHandler : BackgroundService catch (RpcException ex) { // 这里是关键:打印 RpcException 的详细状态 - _logger.LogError("[gRPC Error] StatusCode: {Code}, Detail: {Detail}", ex.StatusCode, ex.Status.Detail); + _gRpcLog.Error("[gRpc] StatusCode: {Code}, Detail: {Detail}", ex.StatusCode, ex.Status.Detail); // 如果是 Unimplemented,通常意味着路径不对 if (ex.StatusCode == StatusCode.Unimplemented) { - _logger.LogError("[gRPC Fix] 请检查服务端是否注册了名为 'GatewayProvider' 的服务,且其 package 声明与客户端一致。"); + _gRpcLog.Error("[gRpc] 请检查服务端是否注册了名为 'GatewayProvider' 的服务,且其 package 声明与客户端一致。"); } } catch (Exception ex) { - _logger.LogError("[gRPC Fatal] 非 RPC 异常: {Msg}", ex.Message); + _gRpcLog.Error("[gRpc] 非 RPC 异常: {Msg}", ex.Message); } } } diff --git a/SHH.CameraService/GrpcImpls/Handlers/GatewayService.cs b/SHH.CameraService/GrpcImpls/Handlers/GatewayService.cs index 525aea4..8ab1962 100644 --- a/SHH.CameraService/GrpcImpls/Handlers/GatewayService.cs +++ b/SHH.CameraService/GrpcImpls/Handlers/GatewayService.cs @@ -9,9 +9,9 @@ using SHH.Contracts.Grpc; // 引用 Proto 生成的命名空间 namespace SHH.CameraService { /// - /// gRPC 指令接收后台服务 + /// gRpc 指令接收后台服务 /// 职责: - /// 1. 维护与 AiVideo 的 gRPC 长连接。 + /// 1. 维护与 AiVideo 的 gRpc 长连接。 /// 2. 完成节点逻辑注册。 /// 3. 监听 Server Streaming 指令流并移交给 Dispatcher。 /// @@ -33,7 +33,7 @@ namespace SHH.CameraService var gRpcLog = Log.ForContext("SourceContext", LogModules.gRpc); // 预留系统启动缓冲时间,确保数据库和 SDK 已就绪 - gRpcLog.Information("[gRPC] 指令接收服务启动,等待环境预热..."); + gRpcLog.Information("[gRpc] 指令接收服务启动,等待环境预热..."); await Task.Delay(3000, stoppingToken); while (!stoppingToken.IsCancellationRequested) @@ -48,7 +48,7 @@ namespace SHH.CameraService var client = new GatewayProvider.GatewayProviderClient(channel); // --- 第一步:发起节点逻辑注册 (Unary) --- - gRpcLog.Information("[gRPC] 正在发起逻辑注册: {Url}", targetUrl); + gRpcLog.Information("[gRpc] 正在发起逻辑注册: {Url}", targetUrl); var regResp = await client.RegisterInstanceAsync(new RegisterRequest { InstanceId = _config.AppId, @@ -59,7 +59,7 @@ namespace SHH.CameraService if (regResp.Success) { - gRpcLog.Information("[gRPC] 注册成功, 正在建立双向指令通道..."); + gRpcLog.Information("[gRpc] 注册成功, 正在建立双向指令通道..."); // --- 第二步:开启 Server Streaming 指令流 --- using var call = client.OpenCommandChannel(new CommandStreamRequest @@ -86,14 +86,14 @@ namespace SHH.CameraService } catch (RpcException ex) { - gRpcLog.Debug("[gRPC] RPC 异常 (Status: {Code}): {Msg}", ex.StatusCode, ex.Message); + gRpcLog.Debug("[gRpc] RPC 异常 (Status: {Code}): {Msg}", ex.StatusCode, ex.Message); // 链路异常,进入重连等待阶段 await Task.Delay(5000, stoppingToken); } catch (Exception ex) { - gRpcLog.Debug("[gRPC] 非预期链路异常: {Msg},5秒后尝试重连", ex.Message); + gRpcLog.Debug("[gRpc] 非预期链路异常: {Msg},5秒后尝试重连", ex.Message); await Task.Delay(5000, stoppingToken); } } diff --git a/SHH.CameraService/GrpcImpls/Handlers/RemoveCameraHandler.cs b/SHH.CameraService/GrpcImpls/Handlers/RemoveCameraHandler.cs index 43fec85..58b9efe 100644 --- a/SHH.CameraService/GrpcImpls/Handlers/RemoveCameraHandler.cs +++ b/SHH.CameraService/GrpcImpls/Handlers/RemoveCameraHandler.cs @@ -1,4 +1,6 @@ -using Newtonsoft.Json.Linq; +using Ayay.SerilogLogs; +using Newtonsoft.Json.Linq; +using Serilog; using SHH.CameraSdk; using SHH.Contracts; @@ -9,6 +11,8 @@ namespace SHH.CameraService /// public class RemoveCameraHandler : ICommandHandler { + private static ILogger _sysLog = Log.ForContext("SourceContext", LogModules.Core); + private readonly CameraManager _cameraManager; /// @@ -50,7 +54,7 @@ namespace SHH.CameraService if (deviceId <= 0) { - Console.WriteLine($"[{ActionName}] 收到无效指令: ID解析失败 ({payload})"); + _sysLog.Warning($"[Sync] 收到无效指令, ID解析失败 ({payload})"); return; } @@ -58,26 +62,25 @@ namespace SHH.CameraService var device = _cameraManager.GetDevice(deviceId); if (device == null) { - Console.WriteLine($"[{ActionName}] 设备 {deviceId} 已经不在管理池中,无需操作。"); + _sysLog.Warning($"[Sync] 设备 {deviceId} 已经不在管理池中,无需操作."); return; } // 3. 安全移除 // 这里建议增加审计日志,记录谁触发了删除(如果协议里有用户信息的话) - device.AddAuditLog("收到远程指令:彻底移除设备"); - Console.WriteLine($"[{ActionName}] 正在安全移除设备: {deviceId} ({device.Config.Name})"); + _sysLog.Debug($"[Sync] 收到远程指令, 正在安全移除设备, ID:{deviceId} Name:{device.Config.Name} ."); // CameraManager 内部会:StopAsync -> DisposeAsync -> TryRemove -> SaveChanges await _cameraManager.RemoveDeviceAsync(deviceId); - Console.WriteLine($"[{ActionName}] 设备 {deviceId} 已彻底清理并从持久化库中移除。"); + _sysLog.Information($"[Sync] 收到远程指令, 设备, ID:{deviceId} Name:{device.Config.Name}已彻底清理并从持久化库中移除 ."); // 4. (可选) 此处可以调用 CommandDispatcher 发送 Success ACK } catch (Exception ex) { // 捕获异常,防止影响全局 Socket 轮询 - Console.WriteLine($"[{ActionName}] 移除设备 {deviceId} 过程中发生致命错误: {ex.Message}"); + _sysLog.Error($"[Sync] 移除设备, ID:{deviceId} 过程中发生致命错误, {ex.Message}."); } } } diff --git a/SHH.CameraService/GrpcImpls/ImageFactory/ImageMonitorController.cs b/SHH.CameraService/GrpcImpls/ImageFactory/ImageMonitorController.cs index 3558cd7..6d828e1 100644 --- a/SHH.CameraService/GrpcImpls/ImageFactory/ImageMonitorController.cs +++ b/SHH.CameraService/GrpcImpls/ImageFactory/ImageMonitorController.cs @@ -1,13 +1,22 @@ -using Microsoft.Extensions.Hosting; +using Ayay.SerilogLogs; +using Microsoft.Extensions.Hosting; using OpenCvSharp; +using Serilog; using SHH.CameraSdk; // 引用 SDK 核心 using SHH.Contracts; using System.Diagnostics; namespace SHH.CameraService; +/// +/// 图像监控采集控制器 (流媒体分发引擎) +/// 功能:监听全局图像采集总线,对图像进行实时 JPG 编码,并动态分发至云端、大屏等订阅目标。 +/// 设计模式:发布-订阅模式 + 扇出 (Fan-out) 分发。 +/// public class ImageMonitorController : BackgroundService { + private static ILogger _sysLog = Log.ForContext("SourceContext", LogModules.Core); + // 注入所有注册的目标(云端、大屏等),实现动态分发 private readonly IEnumerable _targets; @@ -16,29 +25,35 @@ public class ImageMonitorController : BackgroundService // 如果您确实需要 100,请注意带宽压力。此处我保留您要求的 100,但建议未来调优。 private readonly int[] _encodeParams = { (int)ImwriteFlags.JpegQuality, 100 }; + /// + /// 构造函数 + /// + /// public ImageMonitorController(IEnumerable targets) { _targets = targets; } - + + /// + /// 启动后台服务:挂载事件总线 + /// protected override Task ExecuteAsync(CancellationToken stoppingToken) { - Console.WriteLine("[StreamWorker] 启动流媒体采集引擎..."); + _sysLog.Information("[Core] 启动流媒体采集引擎..."); // ========================================================= // 订阅逻辑:接入 "上帝模式" (God Mode) // ========================================================= - // 理由:NetMQ 网关需要无差别地获取所有设备的图像。 + // 理由:gRpc 需要无差别地获取所有设备的图像。 GlobalStreamDispatcher.OnGlobalFrame += ProcessFrame; - - //Console.WriteLine($"[StreamWorker] 已挂载至全局广播总线,正在监听 {GlobalStreamDispatcher.OnGlobalFrame?.GetInvocationList().Length ?? 0} 个订阅者..."); + _sysLog.Information($"[StreamWorker] 已挂载至全局广播总线,正在监听帧信息."); var tcs = new TaskCompletionSource(); stoppingToken.Register(() => { // 停止时反注册,防止静态事件内存泄漏 GlobalStreamDispatcher.OnGlobalFrame -= ProcessFrame; - Console.WriteLine("[StreamWorker] 已断开全局广播连接"); + _sysLog.Information("[Core] 流媒体采集引擎已断开全局广播连接."); tcs.SetResult(); }); @@ -46,9 +61,11 @@ public class ImageMonitorController : BackgroundService } /// - /// [回调函数] 处理每一帧图像 - /// 注意:此方法运行在 SDK 的采集线程池中,必须极速处理,严禁阻塞! + /// [回调函数] 处理实时帧 + /// 注意:此方法由 SDK 采集线程池触发,必须保持极速处理,严禁在内部执行 IO 等耗时阻塞操作。 /// + /// 设备唯一标识 ID + /// 包含原始图像(InternalMat)和处理后图像(TargetMat)的帧数据 private void ProcessFrame(long deviceId, SmartFrame frame) { try @@ -110,20 +127,22 @@ public class ImageMonitorController : BackgroundService if (!ok) { // 如果这里打印,说明管道由于某种原因被关闭了(通常是程序正在退出) - Console.WriteLine($"[DEBUG] 管道写入失败,目标: {target.Config.Name}"); + _sysLog.Warning($"[ImageMonitor] 管道写入失败,目标: {target.Config.Name}"); } } } catch (Exception ex) { // 极少发生的内存错误,打印日志但不抛出,避免崩溃 SDK 线程 - Console.WriteLine($"[StreamWorker] 采集处理异常: {ex.Message}"); + _sysLog.Error($"[ImageMonitor] 采集处理异常: {ex.Message}"); } } /// - /// 辅助:OpenCV 内存编码 + /// 调用 OpenCV 进行内存级图片编码 /// + /// 待编码的 OpenCV Mat 矩阵 + /// JPG 字节数组 private byte[] EncodeImage(Mat mat) { // ImEncode 将 Mat 编码为一维字节数组 (托管内存) diff --git a/SHH.CameraService/GrpcImpls/ImageFactory/PipelineConfigurator.cs b/SHH.CameraService/GrpcImpls/ImageFactory/PipelineConfigurator.cs index 1f41f07..3da083a 100644 --- a/SHH.CameraService/GrpcImpls/ImageFactory/PipelineConfigurator.cs +++ b/SHH.CameraService/GrpcImpls/ImageFactory/PipelineConfigurator.cs @@ -1,4 +1,6 @@ -using Microsoft.Extensions.Hosting; +using Ayay.SerilogLogs; +using Microsoft.Extensions.Hosting; +using Serilog; using SHH.CameraSdk; namespace SHH.CameraService; @@ -65,8 +67,9 @@ public class PipelineConfigurator : IHostedService GlobalPipelineRouter.SetProcessor(_scale); // 启动日志:打印管道组装结果,便于运维排查 - Console.WriteLine("[Pipeline] 图像处理链组装完成: ImageScaleCluster -> ImageEnhanceCluster"); - Console.WriteLine("[Pipeline] 提示:帧数据将按 '缩放 → 增强' 顺序处理,可通过 GlobalPipelineRouter 调整流程"); + Log.ForContext("SourceContext", LogModules.Core) + .Information(@"[Pipeline] 图像处理链组装完成: ImageScaleCluster -> ImageEnhanceCluster + 提示:帧数据将按 '缩放 → 增强' 顺序处理,可通过 GlobalPipelineRouter 调整流程"); return Task.CompletedTask; } diff --git a/SHH.CameraService/GrpcImpls/ImageProcs/GrpcSenderWorker.cs b/SHH.CameraService/GrpcImpls/ImageProcs/GrpcSenderWorker.cs index f677483..cc0dce1 100644 --- a/SHH.CameraService/GrpcImpls/ImageProcs/GrpcSenderWorker.cs +++ b/SHH.CameraService/GrpcImpls/ImageProcs/GrpcSenderWorker.cs @@ -1,25 +1,26 @@ -using Google.Protobuf; +using Ayay.SerilogLogs; +using Google.Protobuf; using Grpc.Net.Client; using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; +using Serilog; using SHH.Contracts.Grpc; namespace SHH.CameraService; /// -/// gRPC 视频流发送工作者 -/// 职责:监听特定的 StreamTarget 队列,建立 gRPC 客户端流并持续推送图片 +/// gRpc 视频流发送工作者 +/// 职责:监听特定的 StreamTarget 队列,建立 gRpc 客户端流并持续推送图片 /// public class GrpcSenderWorker : BackgroundService { - private readonly StreamTarget _target; - private readonly ILogger _logger; + private static ILogger _gRpcLog = Log.ForContext("SourceContext", LogModules.gRpc); + + private readonly StreamTarget _target; private readonly string _grpcUrl; - public GrpcSenderWorker(StreamTarget target, ILogger logger) + public GrpcSenderWorker(StreamTarget target) { _target = target; - _logger = logger; // 自动适配地址:将配置的 tcp://localhost:9001 转换为 http://localhost:9001 // 并且严格使用你验证成功的 localhost @@ -28,7 +29,7 @@ public class GrpcSenderWorker : BackgroundService protected override async Task ExecuteAsync(CancellationToken stoppingToken) { - _logger.LogInformation($"[gRPC Worker] 启动。目标: {_target.Config.Name}, 地址: {_grpcUrl}"); + _gRpcLog.Information($"[gRpc] 视频流发送业务启动, 目标: {_target.Config.Name}, 地址: {_grpcUrl}"); while (!stoppingToken.IsCancellationRequested) { @@ -41,10 +42,10 @@ public class GrpcSenderWorker : BackgroundService // 2. 开启客户端流 (UploadVideoStream 是在 proto 中定义的) using var call = client.UploadVideoStream(cancellationToken: stoppingToken); - _logger.LogInformation($"[gRPC Worker] 已开启视频推送流: {_target.Config.Name}"); + _gRpcLog.Information($"[gRpc] 已开启视频推送流, 目标: {_target.Config.Name}, 地址: {_grpcUrl}"); - // 3. 核心搬运循环:从内存队列 (Channel) 读取数据 - await foreach (var payload in _target.Channel.Reader.ReadAllAsync(stoppingToken)) + // 3. 核心搬运循环:从内存队列 (Channel) 读取数据 + await foreach (var payload in _target.Channel.Reader.ReadAllAsync(stoppingToken)) { // 【畅通保障】检查数据时效性:丢弃超过 1 秒的积压帧 var delay = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() - payload.CaptureTimestamp; @@ -53,7 +54,7 @@ public class GrpcSenderWorker : BackgroundService continue; } - // 将业务 DTO 转换为 gRPC 原生 Request + // 将业务 DTO 转换为 gRpc 原生 Request var request = new VideoFrameRequest { CameraId = payload.CameraId ?? "0", @@ -63,7 +64,7 @@ public class GrpcSenderWorker : BackgroundService HasOriginalImage = payload.HasOriginalImage, HasTargetImage = payload.HasTargetImage, - // ★ 核心:将 byte[] 转换为 gRPC 的 ByteString (高性能) + // ★ 核心:将 byte[] 转换为 gRpc 的 ByteString (高性能) OriginalImageBytes = payload.OriginalImageBytes != null ? ByteString.CopyFrom(payload.OriginalImageBytes) : ByteString.Empty, @@ -94,9 +95,9 @@ public class GrpcSenderWorker : BackgroundService catch (OperationCanceledException) { break; } catch (Exception ex) { - _logger.LogError($"[gRPC Worker] 推送链路异常,5秒后重连: {ex.Message}"); + _gRpcLog.Warning($"[gRpc] 视频推送流链路异常, 目标: {_target.Config.Name}, 地址: {_grpcUrl}, 5秒后重连: {ex.Message}."); await Task.Delay(5000, stoppingToken); - } + } } } } \ No newline at end of file diff --git a/SHH.CameraService/Program.cs b/SHH.CameraService/Program.cs index 8c129fb..f907384 100644 --- a/SHH.CameraService/Program.cs +++ b/SHH.CameraService/Program.cs @@ -25,13 +25,13 @@ public class Program // ============================================================= // 1. 启动日志 // ============================================================= - sysLog.Warning($"🚀 视频取流进程启动, 日志组件初始化完毕 => 进程: {opts.AppId}"); + sysLog.Warning($"[Core] 🚀 视频取流进程启动, 日志组件初始化完毕 => 进程: {opts.AppId}"); string argString = string.Join(" ", args); - sysLog.Debug($"🚀 启动参数({(isDebugArgs ? "调试环境" : "生产环境")}): {argString}"); + sysLog.Debug($"[Core] 🚀 启动参数({(isDebugArgs ? "调试环境" : "生产环境")}): {argString}"); // ============================================================= - // 2. 硬件预热、端口扫描、gRPC链接 + // 2. 硬件预热、端口扫描、gRpc链接 // ============================================================= Bootstrapper.WarmUpHardware(sysLog); @@ -39,13 +39,13 @@ public class Program int activePort = Bootstrapper.ScanForAvailablePort(config, sysLog); if (activePort == -1) { - sysLog.Fatal("💀 无法启动:配置范围内无可用端口"); + sysLog.Fatal("[Core] 💀 无法启动:配置范围内无可用端口"); Bootstrapper.Shutdown("无法启动:配置范围内无可用端口", exitCode: 1); return; } config.UpdateActualPort(activePort); // 回填端口 - // 具体的 gRPC 链接逻辑封装在 Bootstrapper 中,保持 Main 清爽但逻辑可见 + // 具体的 gRpc 链接逻辑封装在 Bootstrapper 中,保持 Main 清爽但逻辑可见 await Bootstrapper.RegisterToGatewayAsync(config); // ============================================================= @@ -53,7 +53,7 @@ public class Program // ============================================================= var builder = WebApplication.CreateBuilder(args); - // ★ 核心改动:一行代码注册所有业务 (SDK, Workers, gRPC, 视频流) + // ★ 核心改动:一行代码注册所有业务 (SDK, Workers, gRpc, 视频流) builder.Services.AddCameraBusinessServices(config, sysLog); // ★ 核心改动:注册 Web 基础 (Controller, Swagger, Cors) @@ -72,7 +72,7 @@ public class Program // 启动监听 string url = $"http://0.0.0.0:{config.BasePort}"; - sysLog.Information($"🚀 [WebApi] 服务启动,监听: {url}"); + sysLog.Information($"[WebApi] 🚀 服务启动,监听: {url}"); await app.RunAsync(url); } @@ -90,7 +90,9 @@ public class Program _ = app.Services.GetRequiredService(); await manager.StartAsync(); - Console.WriteLine("✅[System] 核心业务逻辑已激活。"); + + var sysLog = Log.ForContext("SourceContext", LogModules.Core); + sysLog.Information($"[Core] 🚀 核心业务逻辑已激活, 设备管理器已就绪."); } } diff --git a/SHH.CameraService/Utils/ServiceCollectionExtensions.cs b/SHH.CameraService/Utils/ServiceCollectionExtensions.cs index a5e662d..cc9e802 100644 --- a/SHH.CameraService/Utils/ServiceCollectionExtensions.cs +++ b/SHH.CameraService/Utils/ServiceCollectionExtensions.cs @@ -1,6 +1,5 @@ using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; // 用于泛型 ILogger<> using Microsoft.OpenApi.Models; using SHH.CameraSdk; @@ -84,9 +83,12 @@ public static class ServiceCollectionExtensions } } - logger.Information("📋 加载视频流目标: {Count} 个", netTargets.Count); + logger.Information("[Core] 📋 加载视频流目标: {Count} 个", netTargets.Count); if (netTargets.Count > 0) - logger.Debug("🔍 视频流目标详情: {@Targets}", netTargets); + { + foreach (var item in netTargets) + logger.Debug("[Core] 🔍 视频流目标详情: {@Targets}", new { item.Config }); + } services.AddSingleton>(netTargets); services.AddHostedService(); @@ -96,7 +98,7 @@ public static class ServiceCollectionExtensions { // 注意:这里需要使用 Microsoft.Extensions.Logging.ILogger 来适配构造函数 services.AddHostedService(sp => - new GrpcSenderWorker(target, sp.GetRequiredService>())); + new GrpcSenderWorker(target)); } }