Files
Ayay/Ayay.SerilogLogs/LogBootstrapper.cs
2026-01-16 17:45:27 +08:00

196 lines
9.5 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
{
/// <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);
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
);
});
}
/// <summary>
/// 关闭日志系统,确保缓冲区数据写入磁盘/网络
/// <para>请在程序退出 (OnExit) 时调用</para>
/// </summary>
public static void Close()
{
Log.CloseAndFlush();
}
}
}