using Serilog;
using Serilog.Enrichers.Span; // Nuget: Serilog.Enrichers.Span
using Serilog.Events;
using Serilog.Exceptions; // Nuget: Serilog.Exceptions
using Serilog.Exceptions.Core;
using System;
using System.IO;
using System.Text;
using System.Threading.Tasks;
namespace Ayay.SerilogLogs
{
///
/// 日志引导程序
/// 负责初始化 Serilog 全局配置,包括文件策略、控制台输出、Seq 连接以及上下文丰富化。
/// 实现了按日期分文件夹、按模块分文件、以及主次日志分离的复杂策略。
///
public static class LogBootstrapper
{
///
/// 初始化日志系统 (通常在程序启动最开始调用)
///
/// 配置选项
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);
builder.MinimumLevel.Override("Microsoft.Hosting.Lifetime", LogEventLevel.Warning);
// 2.3 注入全套元数据 (Enrichers) - 让日志更聪明
builder
// 注入全套元数据 (Enrichers) - 让日志更聪明
.Enrich.FromLogContext() // 允许使用 .ForContext() 注入上下文
.Enrich.WithProperty("AppId", opts.AppId) // 注入应用标识
.Enrich.WithProperty("PcCode", opts.PcCode) // 注入应用标识
.Enrich.WithMachineName() // [环境] 区分是哪台工控机 (建议加上)
.Enrich.WithThreadId() // 线程ID
.Enrich.WithProcessId() // 进程ID (用于识别重启)
.Enrich.WithExceptionDetails() // 结构化异常堆栈
// [异常] 结构化异常拆解 (非常强大)
// 它能把 ex.Data 和 InnerException 自动转成 JSON,而不是单纯的一堆字符串
.Enrich.WithExceptionDetails(new DestructuringOptionsBuilder()
.WithDefaultDestructurers())
//.WithDestructurers(new[] { new SqlExceptionDestructurer() })) // 如果有数据库操作,这行很关键
.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}] {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();
// 支持 Emoji 显示
Console.OutputEncoding = Encoding.UTF8;
// --------------------------------------------------------
// 5. 启动后台清理任务 (LogCleaner)
// --------------------------------------------------------
// 延迟 5 秒执行,避免在程序刚启动的高负载时刻争抢 IO 资源
Task.Delay(5000).ContinueWith(_ =>
{
// 调用我们在 LogCleaner.cs 中定义的静态方法
LogCleaner.RunAsync(
opts
);
});
}
///
/// 关闭日志系统,确保缓冲区数据写入磁盘/网络
/// 请在程序退出 (OnExit) 时调用
///
public static void Close()
{
Log.CloseAndFlush();
}
}
}