ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

从零搭建灌装监控系统(十四):Serilog四路日志输出

从零搭建灌装监控系统(十四):Serilog四路日志输出 Serilog 四路日志输出这是「从零搭建灌装监控系统」系列第14篇。报警系统需要日志设备通信需要日志配置保存也需要日志。真正让人头疼的不是写一条日志而是同一条日志如何同时服务现场操作员、开发调试、历史查询和问题定位。这篇用 Serilog 设计文件、SQLite、控制台和 UI 四路输出。出了问题日志却只在窗口里调试阶段最方便的写法是把信息追加到 RichTextBox。程序运行时看起来很直观出问题时却发现窗口早就被刷新掉了如果程序在启动阶段崩溃UI 还没有创建连第一行日志都没有。后来又加了文件日志但业务代码里到处是File.AppendAllText。不同模块的时间格式不一致异常堆栈被截断多个线程同时写文件还会碰到占用异常。日志应该是基础设施。业务代码只需要说“记录一条警告”至于写到哪里由日志配置决定。Serilog 的价值就在这里一个统一的日志事件可以挂接多个 Sink。四路日志各自解决什么问题输出主要读者适合内容文件滚动现场维护、开发人员完整运行日志和异常堆栈SQLite日志查看器、统计功能可筛选、可分页的历史日志控制台调试人员、自动化测试启动参数和实时诊断UI操作员当前运行状态、重要警告四路输出不等于四份不同的日志代码。业务层只调用LogService.Info、Warn、ErrorSerilog 负责分发。LogService 统一入口Serilog Logger文件滚动SQLite Sink控制台UI SinkViewModel 订阅事件Dispatcher 更新集合从最小配置开始Log.LoggernewLoggerConfiguration().MinimumLevel.Debug().Enrich.FromLogContext().WriteTo.Console().WriteTo.File(Path.Combine(logDirectory,app-.log),rollingInterval:RollingInterval.Day,retainedFileCountLimit:14,shared:true).CreateLogger();文件名使用日期滚动保留数量要有限制。现场机器磁盘空间通常不是无限的日志如果没有清理策略几个月后就会变成一个隐蔽的部署问题。shared: true能改善多个进程或工具读取文件时的兼容性但它不能替代完整的并发设计。不要让业务代码自己打开同一个文件。SQLite Sink让日志可以查询文件适合追查完整异常SQLite 适合做日志查看器。日志表至少需要时间、级别、消息和异常信息publicclassSystemLog{[Column(IsIdentitytrue,IsPrimarytrue)]publiclongId{get;set;}publicDateTimeTimestamp{get;set;}publicstringLevel{get;set;};publicstringMessage{get;set;};publicstring?Exception{get;set;}}可以使用现成的 SQLite Sink也可以写一个轻量的自定义 Sink把LogEvent映射成SystemLogpublicsealedclassSqliteLogSink:ILogEventSink{publicvoidEmit(LogEventlogEvent){varrownewSystemLog{TimestamplogEvent.Timestamp.LocalDateTime,LevellogEvent.Level.ToString(),MessagelogEvent.RenderMessage(),ExceptionlogEvent.Exception?.ToString()};_DbProvider.Fsql.Insert(row).ExecuteAffrowsAsync();}}自定义 Sink 里启动异步写入要谨慎。Emit本身是同步调用如果每一条日志都阻塞等待数据库业务线程会被日志拖慢如果完全 fire-and-forget又要考虑程序退出时未写完的日志。中小型项目可以使用有界 Channel 做日志队列退出时等待队列排空。如果日志量不大直接使用成熟 Sink 更省心。自定义 Sink 适合确实需要特殊字段、批量写入或统一队列的场景。UI Sink不要在后台线程直接改控件UI 日志是展示层能力不应该让 Serilog 知道 RichTextBox 的存在。可以定义一个事件或 ChannelpublicsealedclassUiLogSink:ILogEventSink{publicstaticeventActionUiLogItem?Published;publicvoidEmit(LogEventlogEvent){varitemnewUiLogItem(logEvent.Timestamp.LocalDateTime,logEvent.Level,logEvent.RenderMessage());Published?.Invoke(item);}}ViewModel 订阅后切换到 DispatcherprivatevoidOnUiLog(UiLogItemitem){Application.Current.Dispatcher.BeginInvoke((){VisibleLogs.Insert(0,item);while(VisibleLogs.Count200)VisibleLogs.RemoveAt(VisibleLogs.Count-1);});}UI 只显示最近一段不要把一天的日志全部塞进 ObservableCollection。历史日志在 SQLite当前窗口只承担实时反馈。另外UI 日志最好做级别过滤。操作员通常关心信息、警告和错误SQL Verbose、调试轮询和心跳消息会干扰判断。统一 LogService业务层不应该依赖 Serilog 的所有 API。封装一层的好处是以后替换日志库、增加上下文或统一脱敏时只改一个地方publicstaticclassLogService{publicstaticvoidInfo(stringmessage)Log.Information(message);publicstaticvoidWarn(stringmessage)Log.Warning(message);publicstaticvoidDebug(stringmessage)Log.Debug(message);publicstaticvoidVerbose(stringmessage)Log.Verbose(message);publicstaticvoidError(stringmessage,Exception?exnull){if(exnull)Log.Error(message);elseLog.Error(ex,message);}}日志模板尽量使用结构化参数Log.Information(设备连接成功模式{Mode}地址{Address},settings.Mode,settings.Address);不要把密码、完整连接字符串、Cookie 或内部路径写进日志。异常对象本身可能包含路径和参数生产环境也要评估日志的可见范围。启动顺序很重要日志初始化必须早于大部分服务初始化否则启动阶段的异常没有去处protectedoverridevoidOnStartup(StartupEventArgse){varlogDirectoryPath.Combine(AppContext.BaseDirectory,Logs);Directory.CreateDirectory(logDirectory);LogBootstrapper.Initialize(logDirectory);try{InitializeCoreServices();ConfigureDeviceServices();base.OnStartup(e);}catch(Exceptionex){LogService.Fatal(应用启动失败,ex);Log.CloseAndFlush();throw;}}Logs目录先创建日志配置再加载核心服务最后初始化。这样数据库、设备和配置初始化失败时至少能留下错误。应用退出时调用Log.CloseAndFlush()给异步 Sink 一个排空机会protectedoverridevoidOnExit(ExitEventArgse){Log.Information(应用退出);Log.CloseAndFlush();base.OnExit(e);}如果进程被强制终止任何异步日志方案都不能保证最后一条一定落盘。所以关键动作不能只依赖日志真正重要的数据还应该写入业务表。级别不是越详细越好常见级别可以这样使用VerboseSQL、轮询细节只在调试时开启Debug连接尝试、状态转换等诊断信息Information正常启动、保存成功、记录入库Warning自动重连、配置恢复、非致命异常Error一次操作失败但程序还能继续Fatal应用无法继续运行的严重错误。如果所有事情都写Error真正的错误会失去优先级如果所有轮询都写Information日志会很快失去可读性。级别是给人筛选用的不是给代码作者发泄用的。脱敏和日志保留日志比文章更容易泄露现场信息因为它往往包含设备地址、路径和异常参数。发布前检查文章和示例日志时要去掉真实设备 IP、端口和串口号公司目录、用户名和部署盘符用户名、密码和令牌真实产品批次号和客户信息。文章里的日志示例使用127.0.0.1、COM1和泛化后的批次号只为解释格式不代表真实现场配置。踩坑记录日志初始化太晚启动异常没有记录排查只能靠猜。日志引导程序要放在核心服务之前。UI Sink 直接引用控件日志基础设施一旦依赖窗口后台测试和无界面启动都会变麻烦。Sink 发布事件ViewModel 决定怎么显示。SQLite 日志无限增长日志表也需要保留策略可以按时间清理或限制数据库大小。不要因为“数据库便宜”就永远不清理。把异常消息当作用户提示异常消息可能包含技术细节和路径。日志保留完整异常UI 显示可理解的简短提示。本篇小结目标做法统一入口LogService封装 Serilog现场排查日期滚动文件历史筛选SQLite Sink实时提示UI Sink Dispatcher调试诊断控制台和 Verbose 级别启动可靠先初始化日志再初始化业务服务四路日志接通以后下一篇把 SQLite 里的日志展示出来按级别、日期和关键字筛选并使用固定分页避免一次加载太多数据。下期预告第15篇日志查看器筛选与分页下一篇完成LogsViewModel实现 50 条一页、条件查询、页码切换和 CSV 导出入口。
返回列表