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); // 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}] [{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 ); }); } /// /// 关闭日志系统,确保缓冲区数据写入磁盘/网络 /// 请在程序退出 (OnExit) 时调用 /// public static void Close() { Log.CloseAndFlush(); } } }