2026-01-15 18:56:39 +08:00
|
|
|
|
using Serilog;
|
2026-01-16 07:23:56 +08:00
|
|
|
|
using Serilog.Enrichers.Span; // Nuget: Serilog.Enrichers.Span
|
2026-01-15 18:56:39 +08:00
|
|
|
|
using Serilog.Events;
|
|
|
|
|
|
using Serilog.Exceptions; // Nuget: Serilog.Exceptions
|
2026-01-16 07:23:56 +08:00
|
|
|
|
using Serilog.Exceptions.Core;
|
2026-01-15 18:56:39 +08:00
|
|
|
|
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
|
2026-01-16 07:23:56 +08:00
|
|
|
|
// 注入全套元数据 (Enrichers) - 让日志更聪明
|
2026-01-15 18:56:39 +08:00
|
|
|
|
.Enrich.FromLogContext() // 允许使用 .ForContext() 注入上下文
|
|
|
|
|
|
.Enrich.WithProperty("AppId", opts.AppId) // 注入应用标识
|
2026-01-16 07:23:56 +08:00
|
|
|
|
.Enrich.WithProperty("PcCode", opts.PcCode) // 注入应用标识
|
|
|
|
|
|
.Enrich.WithMachineName() // [环境] 区分是哪台工控机 (建议加上)
|
2026-01-15 18:56:39 +08:00
|
|
|
|
.Enrich.WithThreadId() // 线程ID
|
|
|
|
|
|
.Enrich.WithProcessId() // 进程ID (用于识别重启)
|
|
|
|
|
|
.Enrich.WithExceptionDetails() // 结构化异常堆栈
|
2026-01-16 07:23:56 +08:00
|
|
|
|
// [异常] 结构化异常拆解 (非常强大)
|
|
|
|
|
|
// 它能把 ex.Data 和 InnerException 自动转成 JSON,而不是单纯的一堆字符串
|
|
|
|
|
|
.Enrich.WithExceptionDetails(new DestructuringOptionsBuilder()
|
|
|
|
|
|
.WithDefaultDestructurers())
|
|
|
|
|
|
//.WithDestructurers(new[] { new SqlExceptionDestructurer() })) // 如果有数据库操作,这行很关键
|
2026-01-15 18:56:39 +08:00
|
|
|
|
.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();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|