增加日志组件
This commit is contained in:
21
Ayay.SerilogLogs/Ayay.SerilogLogs.csproj
Normal file
21
Ayay.SerilogLogs/Ayay.SerilogLogs.csproj
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>netstandard2.0</TargetFramework>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Serilog" Version="4.3.0" />
|
||||||
|
<PackageReference Include="Serilog.Enrichers.Environment" Version="3.0.1" />
|
||||||
|
<PackageReference Include="Serilog.Enrichers.Process" Version="3.0.0" />
|
||||||
|
<PackageReference Include="Serilog.Enrichers.Span" Version="3.1.0" />
|
||||||
|
<PackageReference Include="Serilog.Enrichers.Thread" Version="4.0.0" />
|
||||||
|
<PackageReference Include="Serilog.Exceptions" Version="8.4.0" />
|
||||||
|
<PackageReference Include="Serilog.Sinks.Async" Version="2.1.0" />
|
||||||
|
<PackageReference Include="Serilog.Sinks.Console" Version="6.1.1" />
|
||||||
|
<PackageReference Include="Serilog.Sinks.File" Version="7.0.0" />
|
||||||
|
<PackageReference Include="Serilog.Sinks.Map" Version="2.0.0" />
|
||||||
|
<PackageReference Include="Serilog.Sinks.Seq" Version="9.0.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
183
Ayay.SerilogLogs/LogBootstrapper.cs
Normal file
183
Ayay.SerilogLogs/LogBootstrapper.cs
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
using Serilog;
|
||||||
|
using Serilog.Events;
|
||||||
|
using Serilog.Exceptions; // Nuget: Serilog.Exceptions
|
||||||
|
using Serilog.Enrichers.Span; // Nuget: Serilog.Enrichers.Span
|
||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Ayay.SerilogLogs
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 日志引导程序
|
||||||
|
/// <para>负责初始化 Serilog 全局配置,包括文件策略、控制台输出、Seq 连接以及上下文丰富化。</para>
|
||||||
|
/// <para>实现了按日期分文件夹、按模块分文件、以及主次日志分离的复杂策略。</para>
|
||||||
|
/// </summary>
|
||||||
|
public static class LogBootstrapper
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 初始化日志系统 (通常在程序启动最开始调用)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="opts">配置选项</param>
|
||||||
|
public static void Init(LogOptions opts)
|
||||||
|
{
|
||||||
|
// --------------------------------------------------------
|
||||||
|
// 1. 目录容错处理 (防止因为 D 盘不存在导致程序崩溃)
|
||||||
|
// --------------------------------------------------------
|
||||||
|
string finalLogPath = opts.LogRootPath;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!Directory.Exists(finalLogPath))
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(finalLogPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
// 如果创建失败(比如没有 D 盘权限),回退到程序运行目录下的 Logs 文件夹
|
||||||
|
finalLogPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Logs");
|
||||||
|
}
|
||||||
|
|
||||||
|
// --------------------------------------------------------
|
||||||
|
// 2. 构建 Serilog 配置
|
||||||
|
// --------------------------------------------------------
|
||||||
|
var builder = new LoggerConfiguration();
|
||||||
|
|
||||||
|
// 2.1 设置全局最低门槛 (兜底策略)
|
||||||
|
builder.MinimumLevel.Is(opts.GlobalMinimumLevel);
|
||||||
|
|
||||||
|
// 2.2 应用模块级别的特殊配置 (关键:处理 Algorithm=Debug, Ping=Fatal 等)
|
||||||
|
if (opts.ModuleLevels != null)
|
||||||
|
{
|
||||||
|
foreach (var module in opts.ModuleLevels)
|
||||||
|
{
|
||||||
|
builder.MinimumLevel.Override(module.Key, module.Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 强制覆盖微软自带的啰嗦日志
|
||||||
|
builder.MinimumLevel.Override("Microsoft", LogEventLevel.Warning);
|
||||||
|
builder.MinimumLevel.Override("System", LogEventLevel.Warning);
|
||||||
|
|
||||||
|
// 2.3 注入全套元数据 (Enrichers) - 让日志更聪明
|
||||||
|
builder
|
||||||
|
.Enrich.FromLogContext() // 允许使用 .ForContext() 注入上下文
|
||||||
|
.Enrich.WithProperty("AppId", opts.AppId) // 注入应用标识
|
||||||
|
.Enrich.WithThreadId() // 线程ID
|
||||||
|
.Enrich.WithProcessId() // 进程ID (用于识别重启)
|
||||||
|
.Enrich.WithExceptionDetails() // 结构化异常堆栈
|
||||||
|
.Enrich.WithSpan(); // 全链路追踪 ID
|
||||||
|
|
||||||
|
// --------------------------------------------------------
|
||||||
|
// 3. 配置输出端 (Sinks)
|
||||||
|
// --------------------------------------------------------
|
||||||
|
|
||||||
|
// 定义通用模板:包含了 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}";
|
||||||
|
|
||||||
|
// 3.1 控制台输出 (开发调试用)
|
||||||
|
builder.WriteTo.Async(a => a.Console(
|
||||||
|
restrictedToMinimumLevel: opts.ConsoleLevel,
|
||||||
|
outputTemplate: outputTemplate
|
||||||
|
));
|
||||||
|
|
||||||
|
// =================================================================
|
||||||
|
// 3.2 [核心逻辑] 双重 Map 实现: 日期文件夹 -> 模块文件
|
||||||
|
// =================================================================
|
||||||
|
|
||||||
|
// 第一层 Map:根据【日期】分发 (key = "2026-01-15")
|
||||||
|
// 目的:实现 D:\Logs\App\2026-01-15\ 这样的目录结构
|
||||||
|
builder.WriteTo.Map(le => le.Timestamp.ToString("yyyy-MM-dd"), (dateKey, dailyConfig) =>
|
||||||
|
{
|
||||||
|
// 动态计算当天的文件夹路径
|
||||||
|
var dailyPath = Path.Combine(finalLogPath, dateKey);
|
||||||
|
|
||||||
|
// 第二层 Map:根据【模块 SourceContext】分发
|
||||||
|
// 目的:在日期文件夹下,区分 System.txt, Network.txt
|
||||||
|
dailyConfig.Map("SourceContext", (moduleKey, moduleConfig) =>
|
||||||
|
{
|
||||||
|
// 如果没填 SourceContext,默认归为 General
|
||||||
|
var moduleName = string.IsNullOrEmpty(moduleKey) ? "General" : moduleKey;
|
||||||
|
|
||||||
|
// --- A. 配置【主要数据】文件 (Main) ---
|
||||||
|
// 规则: 只存 Information 及以上
|
||||||
|
// 路径: .../2026-01-15/System-20260115.txt
|
||||||
|
moduleConfig.File(
|
||||||
|
path: Path.Combine(dailyPath, $"{moduleName}-.txt"), // RollingInterval.Day 会自动把 - 替换为 -yyyyMMdd
|
||||||
|
restrictedToMinimumLevel: LogEventLevel.Information, // 👈 过滤掉 Debug
|
||||||
|
rollingInterval: RollingInterval.Day,
|
||||||
|
|
||||||
|
// 文件大小限制 (超过 10MB 切割 Main_001.txt)
|
||||||
|
fileSizeLimitBytes: opts.FileSizeLimitBytes,
|
||||||
|
rollOnFileSizeLimit: opts.RollOnFileSizeLimit,
|
||||||
|
|
||||||
|
// ⚠️ 设为 null,因为我们有自定义的 LogCleaner 接管清理工作,避免 Serilog 内部逻辑冲突
|
||||||
|
retainedFileCountLimit: null,
|
||||||
|
|
||||||
|
encoding: Encoding.UTF8,
|
||||||
|
outputTemplate: outputTemplate,
|
||||||
|
shared: true // 允许跨进程/多线程共享
|
||||||
|
);
|
||||||
|
|
||||||
|
// --- B. 配置【细节数据】文件 (Detail) ---
|
||||||
|
// 规则: 存 Debug 及以上 (包含 Main 的数据,是最全的备份)
|
||||||
|
// 路径: .../2026-01-15/SystemDetail-20260115.txt
|
||||||
|
moduleConfig.File(
|
||||||
|
path: Path.Combine(dailyPath, $"{moduleName}Detail-.txt"),
|
||||||
|
restrictedToMinimumLevel: LogEventLevel.Debug, // 👈 包含 Debug
|
||||||
|
rollingInterval: RollingInterval.Day,
|
||||||
|
|
||||||
|
// 文件大小限制 (与 Main 保持一致)
|
||||||
|
fileSizeLimitBytes: opts.FileSizeLimitBytes,
|
||||||
|
rollOnFileSizeLimit: opts.RollOnFileSizeLimit,
|
||||||
|
|
||||||
|
retainedFileCountLimit: null,
|
||||||
|
|
||||||
|
encoding: Encoding.UTF8,
|
||||||
|
outputTemplate: outputTemplate,
|
||||||
|
shared: true
|
||||||
|
);
|
||||||
|
|
||||||
|
}, sinkMapCountLimit: 20); // 限制模块数量,防止 Context 乱填导致句柄爆炸
|
||||||
|
|
||||||
|
}, sinkMapCountLimit: 2); // 限制日期 Sink 数量 (只需要保持今天和昨天,防止跨天运行时内存不释放)
|
||||||
|
|
||||||
|
// 3.3 Seq 远程输出 (生产监控)
|
||||||
|
if (!string.IsNullOrWhiteSpace(opts.SeqServerUrl))
|
||||||
|
{
|
||||||
|
builder.WriteTo.Async(a => a.Seq(
|
||||||
|
serverUrl: opts.SeqServerUrl,
|
||||||
|
apiKey: opts.SeqApiKey,
|
||||||
|
restrictedToMinimumLevel: opts.SeqLevel
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// --------------------------------------------------------
|
||||||
|
// 4. 生成 Logger 并赋值给全局静态变量
|
||||||
|
// --------------------------------------------------------
|
||||||
|
Log.Logger = builder.CreateLogger();
|
||||||
|
|
||||||
|
// --------------------------------------------------------
|
||||||
|
// 5. 启动后台清理任务 (LogCleaner)
|
||||||
|
// --------------------------------------------------------
|
||||||
|
// 延迟 5 秒执行,避免在程序刚启动的高负载时刻争抢 IO 资源
|
||||||
|
Task.Delay(5000).ContinueWith(_ =>
|
||||||
|
{
|
||||||
|
// 调用我们在 LogCleaner.cs 中定义的静态方法
|
||||||
|
LogCleaner.RunAsync(
|
||||||
|
opts
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 关闭日志系统,确保缓冲区数据写入磁盘/网络
|
||||||
|
/// <para>请在程序退出 (OnExit) 时调用</para>
|
||||||
|
/// </summary>
|
||||||
|
public static void Close()
|
||||||
|
{
|
||||||
|
Log.CloseAndFlush();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
157
Ayay.SerilogLogs/LogCleaner.cs
Normal file
157
Ayay.SerilogLogs/LogCleaner.cs
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
using Serilog;
|
||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Ayay.SerilogLogs
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 日志清理工具
|
||||||
|
/// <para>弥补 Serilog 原生清理功能的不足,支持“按天数”和“按总大小”进行全局清理。</para>
|
||||||
|
/// <para>✅ 已适配多级目录结构,会自动清理删除文件后留下的空文件夹。</para>
|
||||||
|
/// </summary>
|
||||||
|
public static class LogCleaner
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 异步执行清理任务
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="opts">配置选项</param>
|
||||||
|
public static void RunAsync(LogOptions opts)
|
||||||
|
{
|
||||||
|
string rootPath = opts.LogRootPath; // 日志根目录
|
||||||
|
int maxDays = opts.MaxRetentionDays; // 最大保留天数
|
||||||
|
long maxBytes = opts.MaxTotalLogSize; // 最大总字节数
|
||||||
|
|
||||||
|
Task.Run(() =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
CleanUp(rootPath, maxDays, maxBytes);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Log.Error(ex, "[LogCleaner] 日志自动清理任务执行失败");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void CleanUp(string rootPath, int maxDays, long maxBytes)
|
||||||
|
{
|
||||||
|
var dirInfo = new DirectoryInfo(rootPath);
|
||||||
|
if (!dirInfo.Exists) return;
|
||||||
|
|
||||||
|
// =========================================================
|
||||||
|
// 第一步:清理文件 (递归查找所有子目录)
|
||||||
|
// =========================================================
|
||||||
|
|
||||||
|
// 获取所有 .txt 文件,按最后修改时间升序排列 (最旧的在前面)
|
||||||
|
// SearchOption.AllDirectories 确保了能扫描到 2026-01-15 这种子文件夹里的内容
|
||||||
|
var allFiles = dirInfo.GetFiles("*.txt", SearchOption.AllDirectories)
|
||||||
|
.OrderBy(f => f.LastWriteTime)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (allFiles.Count == 0) return;
|
||||||
|
|
||||||
|
long currentTotalSize = 0;
|
||||||
|
var cutOffDate = DateTime.Now.Date.AddDays(-maxDays); // 只保留到今天之前的 N 天
|
||||||
|
int deletedCount = 0;
|
||||||
|
|
||||||
|
// --- 策略 A: 按时间清理 ---
|
||||||
|
for (int i = allFiles.Count - 1; i >= 0; i--)
|
||||||
|
{
|
||||||
|
var file = allFiles[i];
|
||||||
|
|
||||||
|
if (file.LastWriteTime.Date < cutOffDate)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
file.Delete();
|
||||||
|
allFiles.RemoveAt(i);
|
||||||
|
deletedCount++;
|
||||||
|
}
|
||||||
|
catch { /* 忽略占用 */ }
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
currentTotalSize += file.Length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (deletedCount > 0)
|
||||||
|
{
|
||||||
|
Log.ForContext("SourceContext", LogModules.Core)
|
||||||
|
.Information("[LogCleaner] 时间策略: 已删除 {Count} 个过期文件", deletedCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 策略 B: 按总大小清理 ---
|
||||||
|
if (currentTotalSize > maxBytes)
|
||||||
|
{
|
||||||
|
int sizeDeletedCount = 0;
|
||||||
|
long freedBytes = 0;
|
||||||
|
|
||||||
|
foreach (var file in allFiles)
|
||||||
|
{
|
||||||
|
if (currentTotalSize <= maxBytes) break;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
long len = file.Length;
|
||||||
|
file.Delete();
|
||||||
|
currentTotalSize -= len;
|
||||||
|
freedBytes += len;
|
||||||
|
sizeDeletedCount++;
|
||||||
|
}
|
||||||
|
catch { /* 忽略占用 */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sizeDeletedCount > 0)
|
||||||
|
{
|
||||||
|
Log.ForContext("SourceContext", LogModules.Core)
|
||||||
|
.Warning("[LogCleaner] 空间策略: 已删除 {Count} 个旧文件, 释放 {SizeMB:F2} MB",
|
||||||
|
sizeDeletedCount, freedBytes / 1024.0 / 1024.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================
|
||||||
|
// 第二步:清理空文件夹 (新增逻辑)
|
||||||
|
// =========================================================
|
||||||
|
// 目的:当 2026-01-01 文件夹里的文件都被删光后,这个文件夹本身也应该被删掉
|
||||||
|
DeleteEmptyDirectories(dirInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 递归删除空文件夹
|
||||||
|
/// </summary>
|
||||||
|
private static void DeleteEmptyDirectories(DirectoryInfo dir)
|
||||||
|
{
|
||||||
|
// 1. 先递归处理子文件夹
|
||||||
|
foreach (var subDir in dir.GetDirectories())
|
||||||
|
{
|
||||||
|
DeleteEmptyDirectories(subDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 检查当前文件夹是否为空 (且不是根目录自己)
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// 重新刷新状态
|
||||||
|
dir.Refresh();
|
||||||
|
|
||||||
|
// 如果没有文件 且 没有子文件夹
|
||||||
|
if (dir.GetFiles().Length == 0 && dir.GetDirectories().Length == 0)
|
||||||
|
{
|
||||||
|
// 注意:不要删除根目录 LogRootPath
|
||||||
|
// 这里虽然逻辑上递归会删到底,但通常外层调用时传入的是 Logs 根目录,
|
||||||
|
// 只要 LogBootstrapper 里保证 Logs 存在,这里删掉子目录没问题。
|
||||||
|
// 为了安全,可以判断一下是否是根目录的直接子级,或者简单地 try catch 忽略根目录无法删除的异常。
|
||||||
|
|
||||||
|
dir.Delete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// 忽略异常 (比如文件夹正被打开,或者试图删除根目录但被占用)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
28
Ayay.SerilogLogs/LogModules.cs
Normal file
28
Ayay.SerilogLogs/LogModules.cs
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
namespace Ayay.SerilogLogs
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 统一管理系统中的日志模块名称(SourceContext)
|
||||||
|
/// <para>使用常量可以避免硬编码字符串带来的拼写错误</para>
|
||||||
|
/// </summary>
|
||||||
|
public static class LogModules
|
||||||
|
{
|
||||||
|
// --- 核心架构层 ---
|
||||||
|
public const string Core = "Core"; // 系统主逻辑/启动关闭
|
||||||
|
public const string Network = "Network"; // 底层网络通讯 (TCP/UDP)
|
||||||
|
public const string WebApi = "WebAPI"; // 对外 HTTP 接口
|
||||||
|
public const string WebSocket = "WebSocket"; // 实时通讯
|
||||||
|
public const string Ping = "Ping"; // 心跳/Ping包 (通常量大且不重要)
|
||||||
|
|
||||||
|
// --- 业务逻辑层 ---
|
||||||
|
public const string UserSystem = "UserSystem"; // 用户账户/权限系统
|
||||||
|
public const string UserAction = "UserAction"; // 用户操作行为 (审计日志)
|
||||||
|
public const string DeviceOps = "DeviceOps"; // 设备运行/控制指令
|
||||||
|
|
||||||
|
// --- 核心算法层 ---
|
||||||
|
public const string Algorithm = "Algorithm"; // 算法分析/AI识别 (需要上下文追踪)
|
||||||
|
public const string Observation = "Observation"; // 观察点/埋点 (用于调试或统计)
|
||||||
|
|
||||||
|
// --- 第三方集成 ---
|
||||||
|
public const string Sdk = "SDK"; // 第三方 SDK 调用封装
|
||||||
|
}
|
||||||
|
}
|
||||||
154
Ayay.SerilogLogs/LogOptions.cs
Normal file
154
Ayay.SerilogLogs/LogOptions.cs
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
using Serilog.Events;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace Ayay.SerilogLogs
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 日志组件配置选项
|
||||||
|
/// <para>包含身份标识、存储路径、分级策略以及自动清理策略。</para>
|
||||||
|
/// </summary>
|
||||||
|
public class LogOptions
|
||||||
|
{
|
||||||
|
// ==========================================
|
||||||
|
// 1. 基础身份标识
|
||||||
|
// ==========================================
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 应用名称/服务ID。
|
||||||
|
/// <para>例如: "VideoServer-01", "Gatekeeper-Api"。</para>
|
||||||
|
/// <para>在 Seq 中对应 {AppId} 属性,用于区分多服务环境下的日志来源。</para>
|
||||||
|
/// </summary>
|
||||||
|
public string AppId { get; set; } = "DefaultApp";
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// 2. 存储路径配置
|
||||||
|
// ==========================================
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 本地日志文件的存储根目录。
|
||||||
|
/// <para>默认: @"D:\Logs"</para>
|
||||||
|
/// <para>程序会自动在此目录下按模块创建子文件夹(如 System, Network)。</para>
|
||||||
|
/// <para>注意:如果该目录无写入权限,组件会自动降级到程序运行目录。</para>
|
||||||
|
/// </summary>
|
||||||
|
public string LogRootPath { get; set; } = @"D:\Logs";
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// 3. Seq 集中式日志配置
|
||||||
|
// ==========================================
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Seq 服务器地址。
|
||||||
|
/// <para>例如: "http://192.168.1.100:5341"</para>
|
||||||
|
/// <para>如果留空 (null/empty),则不启用 Seq 投递。</para>
|
||||||
|
/// </summary>
|
||||||
|
public string SeqServerUrl { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Seq API Key (令牌)。
|
||||||
|
/// <para>建议在 Seq 后台申请仅具有 [Ingest] 权限的 Key。</para>
|
||||||
|
/// <para>配置 Key 后支持动态调整日志级别 (Dynamic Level Control)。</para>
|
||||||
|
/// </summary>
|
||||||
|
public string SeqApiKey { get; set; }
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// 4. 输出端级别控制 (Sink Levels)
|
||||||
|
// 用于控制不同媒介的“过滤网”疏密程度
|
||||||
|
// ==========================================
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 控制台输出的最低级别。
|
||||||
|
/// <para>默认: Information (开发调试时可改为 Debug)</para>
|
||||||
|
/// </summary>
|
||||||
|
public LogEventLevel ConsoleLevel { get; set; } = LogEventLevel.Information;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 本地文件记录的最低级别。
|
||||||
|
/// <para>默认: Debug (保留详细案底,便于事后追溯)</para>
|
||||||
|
/// <para>注意:具体写入哪个文件(Main/Detail)由内部逻辑决定,此属性控制总开关。</para>
|
||||||
|
/// </summary>
|
||||||
|
public LogEventLevel FileLevel { get; set; } = LogEventLevel.Verbose;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Seq 网络传输的最低级别。
|
||||||
|
/// <para>默认: Information (减少网络带宽和服务器存储压力)</para>
|
||||||
|
/// <para>生产环境建议设为 Information 或 Warning,除非需要在线排错。</para>
|
||||||
|
/// </summary>
|
||||||
|
public LogEventLevel SeqLevel { get; set; } = LogEventLevel.Verbose;
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// 5. 业务模块级别控制 (Context Levels)
|
||||||
|
// 用于精细化控制特定业务模块的日志开关
|
||||||
|
// ==========================================
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 全局默认最低级别。
|
||||||
|
/// <para>如果某个日志没有指定模块,或者模块不在 ModuleLevels 列表中,则使用此级别。</para>
|
||||||
|
/// </summary>
|
||||||
|
public LogEventLevel GlobalMinimumLevel { get; set; } = LogEventLevel.Verbose;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 针对特定业务模块的日志级别覆盖 (Override)。
|
||||||
|
/// <para>Key: 模块名称 (建议使用 LogModules 常量字符串)</para>
|
||||||
|
/// <para>Value: 该模块允许记录的最低级别</para>
|
||||||
|
/// </summary>
|
||||||
|
public Dictionary<string, LogEventLevel> ModuleLevels { get; set; } = new Dictionary<string, LogEventLevel>
|
||||||
|
{
|
||||||
|
// --- 系统层 ---
|
||||||
|
{ LogModules.Core, LogEventLevel.Debug }, // 系统主逻辑
|
||||||
|
{ LogModules.Network, LogEventLevel.Debug }, // 网络通讯:平时只看警告,防止心跳刷屏
|
||||||
|
{ LogModules.WebApi, LogEventLevel.Debug }, // WebAPI:记录请求响应
|
||||||
|
|
||||||
|
// --- 业务层 ---
|
||||||
|
{ LogModules.UserSystem, LogEventLevel.Debug }, // 用户系统
|
||||||
|
{ LogModules.UserAction, LogEventLevel.Debug }, // 用户操作:必须记录,用于审计
|
||||||
|
{ LogModules.DeviceOps, LogEventLevel.Debug }, // 设备操作:记录关键指令
|
||||||
|
|
||||||
|
// --- 核心/高频数据 ---
|
||||||
|
{ LogModules.Algorithm, LogEventLevel.Debug }, // 算法:核心业务,开启 Debug 以记录全过程
|
||||||
|
{ LogModules.Observation, LogEventLevel.Debug }, // 观察点:最详细的埋点
|
||||||
|
|
||||||
|
// --- 降噪区 (垃圾数据屏蔽) ---
|
||||||
|
{ LogModules.WebSocket, LogEventLevel.Debug }, // WS:数据量极大,除非报错否则不记
|
||||||
|
{ LogModules.Ping, LogEventLevel.Debug }, // Ping:几乎不记,除非完全断连
|
||||||
|
{ LogModules.Sdk, LogEventLevel.Debug } // SDK:屏蔽第三方的废话日志
|
||||||
|
};
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// 6. 文件滚动策略 (Rolling Policy)
|
||||||
|
// 控制单个日志文件的大小和生成频率
|
||||||
|
// ==========================================
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 单个日志文件大小限制 (单位:字节)。
|
||||||
|
/// <para>默认: 10MB (10 * 1024 * 1024)</para>
|
||||||
|
/// <para>当文件超过此大小时,会自动创建新文件 (例如 Main_001.txt)。</para>
|
||||||
|
/// <para>建议不要设置过大,否则记事本打开会很卡。</para>
|
||||||
|
/// </summary>
|
||||||
|
public long FileSizeLimitBytes { get; set; } = 10 * 1024 * 1024;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 超过大小限制后是否创建新文件。
|
||||||
|
/// <para>默认: true (推荐)</para>
|
||||||
|
/// </summary>
|
||||||
|
public bool RollOnFileSizeLimit { get; set; } = true;
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// 7. 自动清理策略 (Auto Cleanup)
|
||||||
|
// 由后台 LogCleaner 任务执行,满足任意条件即清理
|
||||||
|
// ==========================================
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 历史日志最大保留天数 (时间策略)。
|
||||||
|
/// <para>默认: 30天</para>
|
||||||
|
/// <para>系统会强制删除最后修改时间超过此天数的文件。</para>
|
||||||
|
/// </summary>
|
||||||
|
public int MaxRetentionDays { get; set; } = 30;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 日志目录总大小上限 (空间策略)。
|
||||||
|
/// <para>默认: 1GB (1024 * 1024 * 1024)</para>
|
||||||
|
/// <para>如果所有日志文件总和超过此大小,系统会按时间倒序(从旧到新)删除文件,直到空间低于此值。</para>
|
||||||
|
/// </summary>
|
||||||
|
public long MaxTotalLogSize { get; set; } = 1024L * 1024 * 1024;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
|
|
||||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
# Visual Studio Version 18
|
# Visual Studio Version 17
|
||||||
VisualStudioVersion = 18.1.11312.151
|
VisualStudioVersion = 17.14.36623.8 d17.14
|
||||||
MinimumVisualStudioVersion = 10.0.40219.1
|
MinimumVisualStudioVersion = 10.0.40219.1
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SHH.CameraSdk", "SHH.CameraSdk\SHH.CameraSdk.csproj", "{21B70A94-43FC-4D17-AB83-9E4B5178397E}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SHH.CameraSdk", "SHH.CameraSdk\SHH.CameraSdk.csproj", "{21B70A94-43FC-4D17-AB83-9E4B5178397E}"
|
||||||
EndProject
|
EndProject
|
||||||
@@ -15,6 +15,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SHH.ProcessLaunchers", "SHH
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SHH.Contracts.Grpc", "SHH.Contracts.Grpc\SHH.Contracts.Grpc.csproj", "{5CBDD688-1CD0-4E63-81C5-8E18750D891A}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SHH.Contracts.Grpc", "SHH.Contracts.Grpc\SHH.Contracts.Grpc.csproj", "{5CBDD688-1CD0-4E63-81C5-8E18750D891A}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ayay.SerilogLogs", "Ayay.SerilogLogs\Ayay.SerilogLogs.csproj", "{0AC10F89-F938-4EA2-BC9F-63CB02BFB067}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
@@ -45,6 +47,10 @@ Global
|
|||||||
{5CBDD688-1CD0-4E63-81C5-8E18750D891A}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{5CBDD688-1CD0-4E63-81C5-8E18750D891A}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{5CBDD688-1CD0-4E63-81C5-8E18750D891A}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{5CBDD688-1CD0-4E63-81C5-8E18750D891A}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{5CBDD688-1CD0-4E63-81C5-8E18750D891A}.Release|Any CPU.Build.0 = Release|Any CPU
|
{5CBDD688-1CD0-4E63-81C5-8E18750D891A}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{0AC10F89-F938-4EA2-BC9F-63CB02BFB067}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{0AC10F89-F938-4EA2-BC9F-63CB02BFB067}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{0AC10F89-F938-4EA2-BC9F-63CB02BFB067}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{0AC10F89-F938-4EA2-BC9F-63CB02BFB067}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
|
|||||||
@@ -6,13 +6,32 @@ public class ServiceConfig
|
|||||||
// 1. 基础属性
|
// 1. 基础属性
|
||||||
// ==========================================
|
// ==========================================
|
||||||
public int ParentPid { get; private set; }
|
public int ParentPid { get; private set; }
|
||||||
|
|
||||||
public string AppId { get; private set; } = "Unknown_01";
|
public string AppId { get; private set; } = "Unknown_01";
|
||||||
|
|
||||||
public int NumericId { get; private set; } = 1;
|
public int NumericId { get; private set; } = 1;
|
||||||
|
|
||||||
public int BasePort { get; private set; } = 5000;
|
public int BasePort { get; private set; } = 5000;
|
||||||
|
|
||||||
public int MaxPortRange { get; private set; } = 100;
|
public int MaxPortRange { get; private set; } = 100;
|
||||||
|
|
||||||
public NetworkMode Mode { get; private set; } = NetworkMode.Passive;
|
public NetworkMode Mode { get; private set; } = NetworkMode.Passive;
|
||||||
|
|
||||||
public bool ShouldConnect => Mode == NetworkMode.Active || Mode == NetworkMode.Hybrid;
|
public bool ShouldConnect => Mode == NetworkMode.Active || Mode == NetworkMode.Hybrid;
|
||||||
|
|
||||||
|
public string SeqServerUrl { get; private set; } = string.Empty;
|
||||||
|
|
||||||
|
public string SeqApiKey { get; private set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 更新实际端口
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="port"></param>
|
||||||
|
public void UpdateActualPort(int port)
|
||||||
|
{
|
||||||
|
this.BasePort = port;
|
||||||
|
}
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// 2. 目标地址列表 (类型变了!)
|
// 2. 目标地址列表 (类型变了!)
|
||||||
// ==========================================
|
// ==========================================
|
||||||
@@ -31,12 +50,16 @@ public class ServiceConfig
|
|||||||
for (int i = 0; i < args.Length; i++)
|
for (int i = 0; i < args.Length; i++)
|
||||||
{
|
{
|
||||||
var key = args[i].ToLower().Trim();
|
var key = args[i].ToLower().Trim();
|
||||||
|
|
||||||
|
// 确保不越界且下一个参数不是 key (防止 value 为空的情况)
|
||||||
var value = (i + 1 < args.Length && !args[i + 1].StartsWith("--")) ? args[i + 1] : string.Empty;
|
var value = (i + 1 < args.Length && !args[i + 1].StartsWith("--")) ? args[i + 1] : string.Empty;
|
||||||
bool consumed = !string.IsNullOrEmpty(value);
|
bool consumed = !string.IsNullOrEmpty(value);
|
||||||
|
|
||||||
switch (key)
|
switch (key)
|
||||||
{
|
{
|
||||||
case "--pid": if (int.TryParse(value, out int pid)) config.ParentPid = pid; break;
|
case "--pid":
|
||||||
|
if (int.TryParse(value, out int pid)) config.ParentPid = pid;
|
||||||
|
break;
|
||||||
case "--appid":
|
case "--appid":
|
||||||
if (!string.IsNullOrWhiteSpace(value))
|
if (!string.IsNullOrWhiteSpace(value))
|
||||||
{
|
{
|
||||||
@@ -49,12 +72,14 @@ public class ServiceConfig
|
|||||||
break;
|
break;
|
||||||
case "--mode": if (int.TryParse(value, out int m)) config.Mode = (NetworkMode)m; break;
|
case "--mode": if (int.TryParse(value, out int m)) config.Mode = (NetworkMode)m; break;
|
||||||
case "--ports":
|
case "--ports":
|
||||||
if (!string.IsNullOrWhiteSpace(value) && value.Contains(","))
|
ParsePortConfig(config, value);
|
||||||
{
|
break;
|
||||||
var parts = value.Split(',');
|
case "--sequris":
|
||||||
if (parts.Length >= 1 && int.TryParse(parts[0], out int p)) config.BasePort = p;
|
config.SeqServerUrl = ParseSeqUri(value);
|
||||||
if (parts.Length >= 2 && int.TryParse(parts[1], out int r)) config.MaxPortRange = r;
|
break;
|
||||||
}
|
case "--seqkey":
|
||||||
|
// 去掉可能存在的分号
|
||||||
|
config.SeqApiKey = value.Replace(";", "").Trim();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if (consumed) i++;
|
if (consumed) i++;
|
||||||
@@ -62,6 +87,54 @@ public class ServiceConfig
|
|||||||
return config;
|
return config;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 解析端口配置
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="config"></param>
|
||||||
|
/// <param name="value"></param>
|
||||||
|
private static void ParsePortConfig(ServiceConfig config, string value)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(value) && value.Contains(","))
|
||||||
|
{
|
||||||
|
var parts = value.Split(',');
|
||||||
|
if (parts.Length >= 1 && int.TryParse(parts[0], out int p)) config.BasePort = p;
|
||||||
|
if (parts.Length >= 2 && int.TryParse(parts[1], out int r)) config.MaxPortRange = r;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 解析 Seq URI
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="rawValue"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
private static string ParseSeqUri(string rawValue)
|
||||||
|
{
|
||||||
|
// 格式: 172.16.41.241,20026,日志处置中心;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
rawValue = rawValue.Replace("\"", "").TrimEnd(';'); // 清理引号和末尾分号
|
||||||
|
var parts = rawValue.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
|
||||||
|
|
||||||
|
if (parts.Length >= 2)
|
||||||
|
{
|
||||||
|
string ip = parts[0].Trim();
|
||||||
|
string port = parts[1].Trim();
|
||||||
|
// 组装成标准 HTTP 格式
|
||||||
|
return $"http://{ip}:{port}";
|
||||||
|
}
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 从 AppId 中解析 ID
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="appId"></param>
|
||||||
|
/// <returns></returns>
|
||||||
private static int ParseIdFromAppId(string appId)
|
private static int ParseIdFromAppId(string appId)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(appId)) return 1;
|
if (string.IsNullOrWhiteSpace(appId)) return 1;
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
namespace SHH.CameraSdk;
|
using Ayay.SerilogLogs;
|
||||||
|
using Serilog;
|
||||||
|
|
||||||
|
namespace SHH.CameraSdk;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// [驱动支持层] 海康 SDK 全局资源管理器 (V3.3.1 修复版)
|
/// [驱动支持层] 海康 SDK 全局资源管理器 (V3.3.1 修复版)
|
||||||
@@ -103,7 +106,8 @@ public static class HikSdkManager
|
|||||||
// 已预热过则直接返回,避免重复执行
|
// 已预热过则直接返回,避免重复执行
|
||||||
if (_isWarmedUp) return;
|
if (_isWarmedUp) return;
|
||||||
|
|
||||||
Console.WriteLine($"[{DateTime.Now:HH:mm:ss.fff}] 正在进行播放库硬件探测预热,请稍候...");
|
Log.ForContext("SourceContext", LogModules.Core)
|
||||||
|
.Debug($"正在进行播放库硬件探测预热,请稍候...");
|
||||||
|
|
||||||
Stopwatch sw = Stopwatch.StartNew();
|
Stopwatch sw = Stopwatch.StartNew();
|
||||||
int tempPort = -1;
|
int tempPort = -1;
|
||||||
@@ -118,7 +122,8 @@ public static class HikSdkManager
|
|||||||
sw.Stop();
|
sw.Stop();
|
||||||
_isWarmedUp = true;
|
_isWarmedUp = true;
|
||||||
|
|
||||||
Console.WriteLine($"[{DateTime.Now:HH:mm:ss.fff}] 预热完成!耗时: {sw.ElapsedMilliseconds}ms。后续调用将恢复正常。");
|
Log.ForContext("SourceContext", LogModules.Core)
|
||||||
|
.Debug($"预热完成!耗时: {sw.ElapsedMilliseconds}ms.");
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Serilog" Version="4.3.0" />
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
|
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
|
||||||
<PackageReference Include="OpenCvSharp4" Version="4.11.0.20250507" />
|
<PackageReference Include="OpenCvSharp4" Version="4.11.0.20250507" />
|
||||||
<PackageReference Include="OpenCvSharp4.Extensions" Version="4.11.0.20250507" />
|
<PackageReference Include="OpenCvSharp4.Extensions" Version="4.11.0.20250507" />
|
||||||
@@ -22,6 +23,7 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\Ayay.SerilogLogs\Ayay.SerilogLogs.csproj" />
|
||||||
<ProjectReference Include="..\SHH.Contracts\SHH.Contracts.csproj" />
|
<ProjectReference Include="..\SHH.Contracts\SHH.Contracts.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|||||||
269
SHH.CameraService/Bootstrapper.cs
Normal file
269
SHH.CameraService/Bootstrapper.cs
Normal file
@@ -0,0 +1,269 @@
|
|||||||
|
using System.Net.Sockets;
|
||||||
|
using System.Net;
|
||||||
|
using Ayay.SerilogLogs;
|
||||||
|
using Grpc.Net.Client;
|
||||||
|
using Serilog;
|
||||||
|
using SHH.CameraSdk;
|
||||||
|
using SHH.Contracts.Grpc;
|
||||||
|
|
||||||
|
namespace SHH.CameraService;
|
||||||
|
|
||||||
|
public static class Bootstrapper
|
||||||
|
{
|
||||||
|
#region ParseConfigAndInitLogger
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 解析参数并初始化 Serilog
|
||||||
|
/// </summary>
|
||||||
|
public static (ServiceConfig Config, LogOptions opts, bool IsDebug) ParseConfigAndInitLogger(string[] args)
|
||||||
|
{
|
||||||
|
bool isDebugArgs = args.Length == 0;
|
||||||
|
|
||||||
|
// =================================================================================
|
||||||
|
// 1. 模拟调试参数
|
||||||
|
// 原理:直接构造 string[],完全模拟 OS 传递给 Main 的参数结构,避免 Split 带来的空格解析风险
|
||||||
|
// =================================================================================
|
||||||
|
if (isDebugArgs)
|
||||||
|
{
|
||||||
|
args = new[]
|
||||||
|
{
|
||||||
|
"--appid", "CameraApp_01",
|
||||||
|
|
||||||
|
// 视频流地址 (格式: IP,Port,Type,Desc)
|
||||||
|
"--uris", "localhost,9001,video,调试PC;",
|
||||||
|
"--uris", "localhost,9002,video,调试PC-2;",
|
||||||
|
|
||||||
|
// 指令通道
|
||||||
|
"--uris", "localhost,9001,command,调试PC;",
|
||||||
|
|
||||||
|
// 日志中心配置 (格式: IP,Port,Desc)
|
||||||
|
"--sequris", "172.16.41.241,20026,日志处置中心;",
|
||||||
|
"--seqkey", "Shine899195994250;",
|
||||||
|
|
||||||
|
// 端口策略
|
||||||
|
"--mode", "1",
|
||||||
|
"--ports", "5000,100"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
var config = ServiceConfig.BuildFromArgs(args);
|
||||||
|
|
||||||
|
var ops = new LogOptions
|
||||||
|
{
|
||||||
|
AppId = config.AppId,
|
||||||
|
LogRootPath = $@"D:\Logs\{config.AppId}",
|
||||||
|
|
||||||
|
// ★这里改为从 config 读取,如果没配则留空或给个默认值
|
||||||
|
SeqServerUrl = config.SeqServerUrl,
|
||||||
|
SeqApiKey = config.SeqApiKey,
|
||||||
|
|
||||||
|
MaxRetentionDays = 10,
|
||||||
|
FileSizeLimitBytes = 1024L * 1024 * 1024,
|
||||||
|
|
||||||
|
// 动态设置日志级别
|
||||||
|
ModuleLevels = new Dictionary<string, Serilog.Events.LogEventLevel>
|
||||||
|
{
|
||||||
|
// 确保 Core 模块在调试时能输出 Debug,在生产时输出 Info
|
||||||
|
{ LogModules.Core, isDebugArgs ? Serilog.Events.LogEventLevel.Debug : Serilog.Events.LogEventLevel.Information },
|
||||||
|
{ LogModules.Network, Serilog.Events.LogEventLevel.Warning }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
LogBootstrapper.Init(ops);
|
||||||
|
|
||||||
|
return (config, ops, isDebugArgs);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Shutdown
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 统一的资源释放与关闭方法
|
||||||
|
/// </summary>
|
||||||
|
/// <summary>
|
||||||
|
/// 统一的资源释放与关闭方法
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="reason">退出原因</param>
|
||||||
|
/// <param name="exitCode">退出码 (0=正常, 非0=异常)</param>
|
||||||
|
public static void Shutdown(string reason, int exitCode = 0)
|
||||||
|
{
|
||||||
|
// 创建一个临时的上下文 Logger,防止全局 Logger 已被部分释放
|
||||||
|
var sysLog = Log.ForContext("SourceContext", LogModules.Core);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// =========================================================
|
||||||
|
// 1. 写日志
|
||||||
|
// =========================================================
|
||||||
|
if (exitCode != 0)
|
||||||
|
{
|
||||||
|
sysLog.Fatal($"💀 [程序终止] {reason} (Code: {exitCode})");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
sysLog.Information($"👋 [程序退出] {reason}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================
|
||||||
|
// 2. 核心硬件资源释放 (关键!)
|
||||||
|
// =========================================================
|
||||||
|
// 防止 SDK 句柄残留导致下次启动无法连接相机
|
||||||
|
try
|
||||||
|
{
|
||||||
|
sysLog.Information("正在清理 Hikvision SDK 资源...");
|
||||||
|
|
||||||
|
// 如果你的项目中引用了 SDK,请务必解开这行注释
|
||||||
|
HikNativeMethods.NET_DVR_Cleanup();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
sysLog.Error(ex, "⚠️ SDK 资源清理失败");
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================
|
||||||
|
// 3. 日志强制落盘
|
||||||
|
// =========================================================
|
||||||
|
// Environment.Exit 是暴力退出,finally 块不会执行,
|
||||||
|
// 必须手动 Flush 确保最后一条日志写入磁盘。
|
||||||
|
Log.CloseAndFlush();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// 忽略所有清理过程中的错误,确保一定要执行到 Environment.Exit
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================
|
||||||
|
// 4. 开发环境交互 (生产环境自动跳过)
|
||||||
|
// =========================================================
|
||||||
|
// 只有在调试器挂载时才暂停,防止 Docker/Service 环境卡死
|
||||||
|
if (System.Diagnostics.Debugger.IsAttached)
|
||||||
|
{
|
||||||
|
Console.ForegroundColor = exitCode == 0 ? ConsoleColor.Green : ConsoleColor.Red;
|
||||||
|
Console.WriteLine($"\n[Debug模式] 按下任意键退出... (ExitCode: {exitCode})");
|
||||||
|
Console.ResetColor();
|
||||||
|
Console.ReadKey();
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================
|
||||||
|
// 5. 暴力且彻底地结束进程
|
||||||
|
// =========================================================
|
||||||
|
Environment.Exit(exitCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region ScanForAvailablePort
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 扫描可用端口
|
||||||
|
/// </summary>
|
||||||
|
public static int ScanForAvailablePort(ServiceConfig config, ILogger logger)
|
||||||
|
{
|
||||||
|
logger.Information($"🔍 开始端口检测: 起始={config.BasePort}, 范围={config.MaxPortRange}");
|
||||||
|
|
||||||
|
for (int i = 0; i <= config.MaxPortRange; i++)
|
||||||
|
{
|
||||||
|
int currentPort = config.BasePort + i;
|
||||||
|
if (CheckPortAvailable(currentPort))
|
||||||
|
{
|
||||||
|
if (currentPort != config.BasePort)
|
||||||
|
{
|
||||||
|
logger.Warning($"⚙️ 端口自动漂移: {config.BasePort} -> {currentPort}");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
logger.Information($"✅ 端口检测通过: {currentPort}");
|
||||||
|
}
|
||||||
|
return currentPort;
|
||||||
|
}
|
||||||
|
logger.Debug($"⚠️ 端口 {currentPort} 被占用,尝试下一个...");
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region CheckPortAvailable
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 检查端口是否可用
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="port"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
private static bool CheckPortAvailable(int port)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var listener = new TcpListener(IPAddress.Any, port);
|
||||||
|
listener.Start();
|
||||||
|
listener.Stop();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region WarmUpHardware
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 硬件预热
|
||||||
|
/// </summary>
|
||||||
|
public static void WarmUpHardware(ILogger logger)
|
||||||
|
{
|
||||||
|
logger.Information("Hik Sdk 开始预热.");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
HikNativeMethods.NET_DVR_Init();
|
||||||
|
HikSdkManager.ForceWarmUp();
|
||||||
|
logger.Information("💡Hik Sdk 预热成功.");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.Error(ex, "⚠️ Hik Sdk 预热失败.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region RegisterToGatewayAsync
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 向网关注册实例
|
||||||
|
/// </summary>
|
||||||
|
public static async Task RegisterToGatewayAsync(ServiceConfig config, ILogger logger)
|
||||||
|
{
|
||||||
|
if (!config.CommandEndpoints.Any()) return;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// 将 tcp:// 转换为 http:// 以适配 gRPC
|
||||||
|
string targetUrl = config.CommandEndpoints.First().Uri.Replace("tcp://", "http://");
|
||||||
|
|
||||||
|
using var channel = GrpcChannel.ForAddress(targetUrl);
|
||||||
|
var client = new GatewayProvider.GatewayProviderClient(channel);
|
||||||
|
|
||||||
|
logger.Information($"[Grpc] 正在执行预注册: {targetUrl}");
|
||||||
|
var resp = await client.RegisterInstanceAsync(new RegisterRequest
|
||||||
|
{
|
||||||
|
InstanceId = config.AppId,
|
||||||
|
Version = "2.0.0-grpc",
|
||||||
|
ServerIp = "127.0.0.1",
|
||||||
|
WebapiPort = config.BasePort, // 使用扫描后的新端口
|
||||||
|
StartTimeTicks = DateTime.Now.Ticks,
|
||||||
|
ProcessId = Environment.ProcessId,
|
||||||
|
Description = ""
|
||||||
|
});
|
||||||
|
logger.Information($"💡[Grpc] 预注册成功: {resp.Message}");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.Error($"⚠️ [Grpc] 预注册尝试失败: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
@@ -1,121 +1,63 @@
|
|||||||
using Grpc.Net.Client;
|
using Ayay.SerilogLogs;
|
||||||
using Microsoft.AspNetCore.Builder;
|
using Microsoft.AspNetCore.Builder;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.OpenApi.Models;
|
using Serilog;
|
||||||
using SHH.CameraSdk;
|
using SHH.CameraSdk;
|
||||||
using SHH.Contracts.Grpc;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
|
|
||||||
namespace SHH.CameraService;
|
namespace SHH.CameraService;
|
||||||
|
|
||||||
public class Program
|
public class Program
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 主程序
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="args"></param>
|
||||||
|
/// <returns></returns>
|
||||||
public static async Task Main(string[] args)
|
public static async Task Main(string[] args)
|
||||||
{
|
{
|
||||||
// 2. 硬件预热 (静态方法保留)
|
|
||||||
HikNativeMethods.NET_DVR_Init();
|
|
||||||
HikSdkManager.ForceWarmUp();
|
|
||||||
|
|
||||||
// 1. [核心环境] 必须在所有网络操作前开启
|
// 1. [核心环境] 必须在所有网络操作前开启
|
||||||
AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true);
|
AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true);
|
||||||
|
|
||||||
// 2. 模拟/解析配置
|
// 2. 解析配置与初始化日志
|
||||||
if (args.Length == 0)
|
var (config, opts, isDebugArgs) = Bootstrapper.ParseConfigAndInitLogger(args);
|
||||||
|
var sysLog = Log.ForContext("SourceContext", LogModules.Core);
|
||||||
|
|
||||||
|
// =============================================================
|
||||||
|
// 1. 启动日志
|
||||||
|
// =============================================================
|
||||||
|
sysLog.Warning($"🚀 视频取流进程启动, 日志组件初始化完毕 => 进程: {opts.AppId}");
|
||||||
|
|
||||||
|
string argString = string.Join(" ", args);
|
||||||
|
sysLog.Debug($"🚀 启动参数({(isDebugArgs ? "调试环境" : "生产环境")}): {argString}");
|
||||||
|
|
||||||
|
// =============================================================
|
||||||
|
// 2. 硬件预热、端口扫描、gRPC链接
|
||||||
|
// =============================================================
|
||||||
|
Bootstrapper.WarmUpHardware(sysLog);
|
||||||
|
|
||||||
|
// 端口自动扫描 (必须做,否则端口冲突)
|
||||||
|
int activePort = Bootstrapper.ScanForAvailablePort(config, sysLog);
|
||||||
|
if (activePort == -1)
|
||||||
{
|
{
|
||||||
string serviceArgs = "--appid CameraApp_01 " +
|
sysLog.Fatal("💀 无法启动:配置范围内无可用端口");
|
||||||
"--uris localhost,9001,video,调试PC; " +
|
Bootstrapper.Shutdown("无法启动:配置范围内无可用端口", exitCode: 1);
|
||||||
"--uris localhost,9001,command,调试PC; " +
|
return;
|
||||||
"--mode 1 --ports 5000,100";
|
|
||||||
args = serviceArgs.Split(' ', StringSplitOptions.RemoveEmptyEntries);
|
|
||||||
}
|
}
|
||||||
var config = ServiceConfig.BuildFromArgs(args);
|
config.UpdateActualPort(activePort); // 回填端口
|
||||||
|
|
||||||
|
// 具体的 gRPC 链接逻辑封装在 Bootstrapper 中,保持 Main 清爽但逻辑可见
|
||||||
|
await Bootstrapper.RegisterToGatewayAsync(config, sysLog);
|
||||||
|
|
||||||
// =============================================================
|
// =============================================================
|
||||||
// 3. 【强行复刻成功逻辑】 在 Web 容器启动前直接执行注册
|
// 3. 构建 Web 主机环境
|
||||||
// =============================================================
|
|
||||||
if (config.CommandEndpoints.Any())
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// 将 tcp:// 转换为 http:// 以适配 gRPC
|
|
||||||
string targetUrl = config.CommandEndpoints.First().Uri.Replace("tcp://", "http://");
|
|
||||||
|
|
||||||
using var channel = GrpcChannel.ForAddress(targetUrl);
|
|
||||||
var client = new GatewayProvider.GatewayProviderClient(channel);
|
|
||||||
|
|
||||||
Console.WriteLine($"[gRPC] 正在执行预注册 (环境: 纯净): {targetUrl}");
|
|
||||||
var resp = await client.RegisterInstanceAsync(new RegisterRequest
|
|
||||||
{
|
|
||||||
InstanceId = config.AppId,
|
|
||||||
Version = "2.0.0-grpc",
|
|
||||||
ServerIp = "127.0.0.1",
|
|
||||||
WebapiPort = config.BasePort,
|
|
||||||
StartTimeTicks = DateTime.Now.Ticks,
|
|
||||||
ProcessId = Environment.ProcessId,
|
|
||||||
Description = "Camera Service"
|
|
||||||
});
|
|
||||||
Console.WriteLine($"[gRPC] 预注册成功: {resp.Message}");
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Console.WriteLine($"[gRPC] 预注册尝试失败 (不影响启动): {ex.Message}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// =============================================================
|
|
||||||
// 4. 构建 Web 主机环境
|
|
||||||
// =============================================================
|
// =============================================================
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
// 基础业务单例注册
|
// ★ 核心改动:一行代码注册所有业务 (SDK, Workers, gRPC, 视频流)
|
||||||
builder.Services.AddSingleton(config);
|
builder.Services.AddCameraBusinessServices(config, sysLog);
|
||||||
builder.Services.AddSingleton<ProcessingConfigManager>();
|
|
||||||
builder.Services.AddSingleton(sp => new ImageScaleCluster(4, sp.GetRequiredService<ProcessingConfigManager>()));
|
|
||||||
builder.Services.AddSingleton(sp => new ImageEnhanceCluster(4, sp.GetRequiredService<ProcessingConfigManager>()));
|
|
||||||
builder.Services.AddHostedService<PipelineConfigurator>();
|
|
||||||
|
|
||||||
// 接入 SDK 核心逻辑
|
// ★ 核心改动:注册 Web 基础 (Controller, Swagger, Cors)
|
||||||
builder.Services.AddCameraSdk(config.NumericId);
|
builder.Services.AddWebSupport(config);
|
||||||
builder.Services.AddHostedService<CameraEngineWorker>();
|
|
||||||
|
|
||||||
// ★ 注册 gRPC 版本的状态监控工作者 (不讲道理,直接注册)
|
|
||||||
builder.Services.AddHostedService<DeviceStatusHandler>();
|
|
||||||
builder.Services.AddHostedService<ParentProcessSentinel>();
|
|
||||||
builder.Services.AddHostedService<GatewayService>();
|
|
||||||
|
|
||||||
// =============================================================
|
|
||||||
// 5. 视频流 Target 注册 (gRPC 模式)
|
|
||||||
// =============================================================
|
|
||||||
var netTargets = new List<StreamTarget>();
|
|
||||||
if (config.VideoEndpoints != null)
|
|
||||||
{
|
|
||||||
foreach (var cfgVideo in config.VideoEndpoints)
|
|
||||||
{
|
|
||||||
netTargets.Add(new StreamTarget(new PushTargetConfig
|
|
||||||
{
|
|
||||||
Name = cfgVideo.Description,
|
|
||||||
Endpoint = cfgVideo.Uri,
|
|
||||||
QueueCapacity = 10,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
builder.Services.AddSingleton<IEnumerable<StreamTarget>>(netTargets);
|
|
||||||
builder.Services.AddHostedService<ImageMonitorController>();
|
|
||||||
|
|
||||||
// 为每个 Target 绑定一个 gRPC 流发送者
|
|
||||||
foreach (var target in netTargets)
|
|
||||||
{
|
|
||||||
builder.Services.AddHostedService(sp =>
|
|
||||||
new GrpcSenderWorker(target, sp.GetRequiredService<ILogger<GrpcSenderWorker>>()));
|
|
||||||
}
|
|
||||||
|
|
||||||
// 注册指令分发 (不再使用 NetMQ 的 CommandClientWorker)
|
|
||||||
builder.Services.AddSingleton<InterceptorPipeline>();
|
|
||||||
builder.Services.AddSingleton<CommandDispatcher>();
|
|
||||||
builder.Services.AddSingleton<ICommandHandler, DeviceConfigHandler>();
|
|
||||||
builder.Services.AddSingleton<ICommandHandler, RemoveCameraHandler>();
|
|
||||||
|
|
||||||
ConfigureWebServices(builder, config);
|
|
||||||
|
|
||||||
// =============================================================
|
// =============================================================
|
||||||
// 6. 启动服务
|
// 6. 启动服务
|
||||||
@@ -123,51 +65,66 @@ public class Program
|
|||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|
||||||
// 激活 SDK 管理器并启动业务点火
|
// 激活 SDK 管理器并启动业务点火
|
||||||
await StartBusinessLogic(app);
|
await StartBusinessLogic(app, sysLog);
|
||||||
|
|
||||||
app.UseSwagger();
|
// ★ 核心改动:配置 HTTP 管道 (Swagger, MapControllers 等)
|
||||||
app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", $"SHH Gateway #{config.AppId}"));
|
app.ConfigurePipeline(config);
|
||||||
|
|
||||||
app.MapGet("/", () => $"SHH Gateway {config.AppId} is running (gRPC Mode).");
|
// 启动监听
|
||||||
app.UseCors("AllowAll");
|
string url = $"http://0.0.0.0:{config.BasePort}";
|
||||||
app.MapControllers();
|
sysLog.Information($"🚀 [WebApi] 服务启动,监听: {url}");
|
||||||
|
|
||||||
Console.WriteLine($"[System] 正在启动 Web 服务,监听端口: {config.BasePort}");
|
await app.RunAsync(url);
|
||||||
await app.RunAsync($"http://0.0.0.0:{config.BasePort}");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 激活单例并启动相机管理器
|
/// 激活单例并启动相机管理器
|
||||||
/// </summary>
|
/// </summary>
|
||||||
static async Task StartBusinessLogic(WebApplication app)
|
/// <param name="app"></param>
|
||||||
|
/// <param name="logger"></param>
|
||||||
|
static async Task StartBusinessLogic(WebApplication app, Serilog.ILogger logger)
|
||||||
{
|
{
|
||||||
var manager = app.Services.GetRequiredService<CameraManager>();
|
var manager = app.Services.GetRequiredService<CameraManager>();
|
||||||
|
|
||||||
|
// 激活哨兵
|
||||||
_ = app.Services.GetRequiredService<ConnectivitySentinel>();
|
_ = app.Services.GetRequiredService<ConnectivitySentinel>();
|
||||||
|
|
||||||
await manager.StartAsync();
|
await manager.StartAsync();
|
||||||
Console.WriteLine("[System] 核心业务逻辑已激活。");
|
Console.WriteLine("✅[System] 核心业务逻辑已激活。");
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 注册 Web API 支持
|
|
||||||
/// </summary>
|
|
||||||
static void ConfigureWebServices(WebApplicationBuilder builder, ServiceConfig cfg)
|
|
||||||
{
|
|
||||||
builder.Services.AddCors(options =>
|
|
||||||
{
|
|
||||||
options.AddPolicy("AllowAll", p => p.AllowAnyOrigin().AllowAnyHeader().AllowAnyMethod());
|
|
||||||
});
|
|
||||||
|
|
||||||
builder.Services.AddControllers(options =>
|
|
||||||
{
|
|
||||||
options.Filters.Add<UserActionFilter>();
|
|
||||||
})
|
|
||||||
.AddApplicationPart(typeof(CamerasController).Assembly)
|
|
||||||
.AddApplicationPart(typeof(MonitorController).Assembly);
|
|
||||||
|
|
||||||
builder.Services.AddEndpointsApiExplorer();
|
|
||||||
builder.Services.AddSwaggerGen(c =>
|
|
||||||
{
|
|
||||||
c.SwaggerDoc("v1", new OpenApiInfo { Title = $"SHH Gateway #{cfg.AppId}", Version = "v1" });
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
🚀 启动/发布 程序启动、服务预热、开始监听
|
||||||
|
🏁 结束/终点 批量任务全部完成、程序正常退出
|
||||||
|
🔄 重试/循环 正在重试连接、定时任务触发、同步数据中
|
||||||
|
⏳ 等待/耗时 长耗时操作开始、排队中
|
||||||
|
💤 休眠/闲置 线程挂起、服务进入待机模式
|
||||||
|
🌐 网络/HTTP HTTP 请求、API 调用、Web 服务
|
||||||
|
🔌 连接 数据库连接建立、Socket 连接
|
||||||
|
📡 信号/广播 发送 MQ 消息、广播通知、取流
|
||||||
|
💾 存储/磁盘 写入文件、数据库落盘、缓存读写
|
||||||
|
🔒 安全/锁 加密、解密、登录成功、获取分布式锁
|
||||||
|
⚙️ 配置/系统 加载配置、系统底层操作
|
||||||
|
🐞 Bug/调试 捕捉到的异常、临时调试信息
|
||||||
|
🧪 测试/实验 单元测试环境、灰度测试代码
|
||||||
|
🔍 搜索/检查 查询数据库、检查文件是否存在
|
||||||
|
💡 提示/发现 逻辑分支提示、参数值打印
|
||||||
|
🔴 红:致命/严重 (Fatal/Error)
|
||||||
|
🟡 黄:警告 (Warning)
|
||||||
|
🟢 绿:正常/成功 (Info/Success)
|
||||||
|
🔵 蓝:数据/网络 (Data/Network)
|
||||||
|
⚪ 灰:细节/忽略 (Debug/Verbose)
|
||||||
|
✅ Check Mark Button \u{2705} ✅
|
||||||
|
🆗 OK Button \u{1F197} 🆗
|
||||||
|
🔚 END Arrow (逻辑结束) \u{1F51A} 🔚
|
||||||
|
💯 Hundred Points (完美结束) \u{1F4AF} 💯
|
||||||
|
🛑 Stop Sign (最强提示) \u{1F6D1} 🛑
|
||||||
|
⛔ No Entry (禁止/中断) \u{26D4} ⛔
|
||||||
|
🚫 Prohibited (非法操作终止) \u{1F6AB} 🚫
|
||||||
|
⏹️ Stop Button (播放器风格) \u{23F9} ⏹
|
||||||
|
❌ Cross Mark (任务失败结束) \u{274C} ❌
|
||||||
|
💀 Skull (进程被 Kill) \u{1F480} 💀
|
||||||
|
⚰️ Coffin (彻底销毁) \u{26B0} ⚰
|
||||||
|
👻 Ghost (变成孤儿进程) \u{1F47B} 👻
|
||||||
|
*/
|
||||||
@@ -22,6 +22,7 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\Ayay.SerilogLogs\Ayay.SerilogLogs.csproj" />
|
||||||
<ProjectReference Include="..\SHH.CameraSdk\SHH.CameraSdk.csproj" />
|
<ProjectReference Include="..\SHH.CameraSdk\SHH.CameraSdk.csproj" />
|
||||||
<ProjectReference Include="..\SHH.Contracts.Grpc\SHH.Contracts.Grpc.csproj" />
|
<ProjectReference Include="..\SHH.Contracts.Grpc\SHH.Contracts.Grpc.csproj" />
|
||||||
<ProjectReference Include="..\SHH.Contracts\SHH.Contracts.csproj" />
|
<ProjectReference Include="..\SHH.Contracts\SHH.Contracts.csproj" />
|
||||||
|
|||||||
155
SHH.CameraService/Utils/ServiceCollectionExtensions.cs
Normal file
155
SHH.CameraService/Utils/ServiceCollectionExtensions.cs
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
using Microsoft.AspNetCore.Builder;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Logging; // 用于泛型 ILogger<>
|
||||||
|
using Microsoft.OpenApi.Models;
|
||||||
|
using SHH.CameraSdk;
|
||||||
|
|
||||||
|
// 确保 namespace 与 Program.cs 的引用一致
|
||||||
|
namespace SHH.CameraService;
|
||||||
|
|
||||||
|
public static class ServiceCollectionExtensions
|
||||||
|
{
|
||||||
|
// ========================================================================
|
||||||
|
// 1. 核心聚合方法 (对应 Program.cs 中的调用)
|
||||||
|
// ========================================================================
|
||||||
|
|
||||||
|
#region AddCameraBusinessServices
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// [聚合] 注册所有核心业务 (包含逻辑处理、SDK、Gateway、视频流)
|
||||||
|
/// </summary>
|
||||||
|
public static void AddCameraBusinessServices(this IServiceCollection services, ServiceConfig config, Serilog.ILogger logger)
|
||||||
|
{
|
||||||
|
// 1.1 注册基础业务逻辑
|
||||||
|
services.AddBusinessServices(config);
|
||||||
|
|
||||||
|
// 1.2 注册视频流相关 (因为需要 Logger,所以单独传)
|
||||||
|
services.AddStreamTargets(config, logger);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region AddBusinessServices
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 集中注册 SDK、Workers、Gateway 等纯业务服务
|
||||||
|
/// </summary>
|
||||||
|
private static void AddBusinessServices(this IServiceCollection services, ServiceConfig config)
|
||||||
|
{
|
||||||
|
// 基础单例
|
||||||
|
services.AddSingleton(config);
|
||||||
|
services.AddSingleton<ProcessingConfigManager>();
|
||||||
|
|
||||||
|
// 图像处理集群
|
||||||
|
services.AddSingleton(sp => new ImageScaleCluster(4, sp.GetRequiredService<ProcessingConfigManager>()));
|
||||||
|
services.AddSingleton(sp => new ImageEnhanceCluster(4, sp.GetRequiredService<ProcessingConfigManager>()));
|
||||||
|
services.AddHostedService<PipelineConfigurator>();
|
||||||
|
|
||||||
|
// SDK 与核心引擎
|
||||||
|
services.AddCameraSdk(config.NumericId);
|
||||||
|
services.AddHostedService<CameraEngineWorker>();
|
||||||
|
|
||||||
|
// 监控与网关
|
||||||
|
services.AddHostedService<DeviceStatusHandler>();
|
||||||
|
services.AddHostedService<ParentProcessSentinel>();
|
||||||
|
services.AddHostedService<GatewayService>();
|
||||||
|
|
||||||
|
// 指令分发系统
|
||||||
|
services.AddSingleton<InterceptorPipeline>();
|
||||||
|
services.AddSingleton<CommandDispatcher>();
|
||||||
|
services.AddSingleton<ICommandHandler, DeviceConfigHandler>();
|
||||||
|
services.AddSingleton<ICommandHandler, RemoveCameraHandler>();
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region AddStreamTargets
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 注册视频流目标 (StreamTargets & GrpcSender)
|
||||||
|
/// </summary>
|
||||||
|
private static void AddStreamTargets(this IServiceCollection services, ServiceConfig config, Serilog.ILogger logger)
|
||||||
|
{
|
||||||
|
var netTargets = new List<StreamTarget>();
|
||||||
|
if (config.VideoEndpoints != null)
|
||||||
|
{
|
||||||
|
foreach (var cfgVideo in config.VideoEndpoints)
|
||||||
|
{
|
||||||
|
netTargets.Add(new StreamTarget(new PushTargetConfig
|
||||||
|
{
|
||||||
|
Name = cfgVideo.Description,
|
||||||
|
Endpoint = cfgVideo.Uri,
|
||||||
|
QueueCapacity = 10,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Information("📋 加载视频流目标: {Count} 个", netTargets.Count);
|
||||||
|
|
||||||
|
services.AddSingleton<IEnumerable<StreamTarget>>(netTargets);
|
||||||
|
services.AddHostedService<ImageMonitorController>();
|
||||||
|
|
||||||
|
// 动态注册 Sender Worker
|
||||||
|
foreach (var target in netTargets)
|
||||||
|
{
|
||||||
|
// 注意:这里需要使用 Microsoft.Extensions.Logging.ILogger 来适配构造函数
|
||||||
|
services.AddHostedService(sp =>
|
||||||
|
new GrpcSenderWorker(target, sp.GetRequiredService<ILogger<GrpcSenderWorker>>()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
|
// 2. Web 支持与管道 (Program.cs 调用的另外两个方法)
|
||||||
|
// ========================================================================
|
||||||
|
|
||||||
|
#region AddWebSupport
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 注册 Controller, Swagger, Cors
|
||||||
|
/// </summary>
|
||||||
|
public static void AddWebSupport(this IServiceCollection services, ServiceConfig config)
|
||||||
|
{
|
||||||
|
services.AddCors(options =>
|
||||||
|
{
|
||||||
|
options.AddPolicy("AllowAll", p => p.AllowAnyOrigin().AllowAnyHeader().AllowAnyMethod());
|
||||||
|
});
|
||||||
|
|
||||||
|
// 注册 Controller 并添加过滤器
|
||||||
|
services.AddControllers(options =>
|
||||||
|
{
|
||||||
|
options.Filters.Add<UserActionFilter>();
|
||||||
|
})
|
||||||
|
.AddApplicationPart(typeof(CamerasController).Assembly) // 确保能扫描到 Controller
|
||||||
|
.AddApplicationPart(typeof(MonitorController).Assembly);
|
||||||
|
|
||||||
|
services.AddEndpointsApiExplorer();
|
||||||
|
services.AddSwaggerGen(c =>
|
||||||
|
{
|
||||||
|
c.SwaggerDoc("v1", new OpenApiInfo { Title = $"SHH Gateway #{config.AppId}", Version = "v1" });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region ConfigurePipeline
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 配置 HTTP 中间件管道
|
||||||
|
/// </summary>
|
||||||
|
public static void ConfigurePipeline(this WebApplication app, ServiceConfig config)
|
||||||
|
{
|
||||||
|
// 开启 Swagger
|
||||||
|
app.UseSwagger();
|
||||||
|
app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", $"SHH Gateway #{config.AppId}"));
|
||||||
|
|
||||||
|
// 简单健康检查端点
|
||||||
|
app.MapGet("/", () => $"SHH Gateway {config.AppId} is running.");
|
||||||
|
|
||||||
|
app.UseCors("AllowAll");
|
||||||
|
app.MapControllers();
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user