news 2026/10/7 2:43:16

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
从零搭建灌装监控系统(十四):Serilog四路日志输出

Serilog 四路日志输出

这是「从零搭建灌装监控系统」系列第14篇。报警系统需要日志,设备通信需要日志,配置保存也需要日志。真正让人头疼的不是写一条日志,而是同一条日志如何同时服务现场操作员、开发调试、历史查询和问题定位。这篇用 Serilog 设计文件、SQLite、控制台和 UI 四路输出。


出了问题,日志却只在窗口里

调试阶段,最方便的写法是把信息追加到 RichTextBox。程序运行时看起来很直观,出问题时却发现窗口早就被刷新掉了;如果程序在启动阶段崩溃,UI 还没有创建,连第一行日志都没有。

后来又加了文件日志,但业务代码里到处是File.AppendAllText。不同模块的时间格式不一致,异常堆栈被截断,多个线程同时写文件还会碰到占用异常。

日志应该是基础设施。业务代码只需要说“记录一条警告”,至于写到哪里,由日志配置决定。Serilog 的价值就在这里:一个统一的日志事件,可以挂接多个 Sink。


四路日志各自解决什么问题

输出主要读者适合内容
文件滚动现场维护、开发人员完整运行日志和异常堆栈
SQLite日志查看器、统计功能可筛选、可分页的历史日志
控制台调试人员、自动化测试启动参数和实时诊断
UI操作员当前运行状态、重要警告

四路输出不等于四份不同的日志代码。业务层只调用LogService.Info、Warn、Error,Serilog 负责分发。


LogService 统一入口

Serilog Logger

文件滚动

SQLite Sink

控制台

UI Sink

ViewModel 订阅事件

Dispatcher 更新集合

从最小配置开始

Log.Logger=newLoggerConfiguration().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(IsIdentity=true,IsPrimary=true)]publiclongId{get;set;}publicDateTimeTimestamp{get;set;}publicstringLevel{get;set;}="";publicstringMessage{get;set;}="";publicstring?Exception{get;set;}}

可以使用现成的 SQLite Sink,也可以写一个轻量的自定义 Sink,把LogEvent映射成SystemLog:

publicsealedclassSqliteLogSink:ILogEventSink{publicvoidEmit(LogEventlogEvent){varrow=newSystemLog{Timestamp=logEvent.Timestamp.LocalDateTime,Level=logEvent.Level.ToString(),Message=logEvent.RenderMessage(),Exception=logEvent.Exception?.ToString()};_=DbProvider.Fsql.Insert(row).ExecuteAffrowsAsync();}}

自定义 Sink 里启动异步写入要谨慎。Emit本身是同步调用,如果每一条日志都阻塞等待数据库,业务线程会被日志拖慢;如果完全 fire-and-forget,又要考虑程序退出时未写完的日志。中小型项目可以使用有界 Channel 做日志队列,退出时等待队列排空。

如果日志量不大,直接使用成熟 Sink 更省心。自定义 Sink 适合确实需要特殊字段、批量写入或统一队列的场景。


UI Sink:不要在后台线程直接改控件

UI 日志是展示层能力,不应该让 Serilog 知道 RichTextBox 的存在。可以定义一个事件或 Channel:

publicsealedclassUiLogSink:ILogEventSink{publicstaticeventAction<UiLogItem>?Published;publicvoidEmit(LogEventlogEvent){varitem=newUiLogItem(logEvent.Timestamp.LocalDateTime,logEvent.Level,logEvent.RenderMessage());Published?.Invoke(item);}}

ViewModel 订阅后切换到 Dispatcher:

privatevoidOnUiLog(UiLogItemitem){Application.Current.Dispatcher.BeginInvoke(()=>{VisibleLogs.Insert(0,item);while(VisibleLogs.Count>200)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?ex=null){if(ex==null)Log.Error(message);elseLog.Error(ex,message);}}

日志模板尽量使用结构化参数:

Log.Information("设备连接成功,模式={Mode},地址={Address}",settings.Mode,settings.Address);

不要把密码、完整连接字符串、Cookie 或内部路径写进日志。异常对象本身可能包含路径和参数,生产环境也要评估日志的可见范围。


启动顺序很重要

日志初始化必须早于大部分服务初始化,否则启动阶段的异常没有去处:

protectedoverridevoidOnStartup(StartupEventArgse){varlogDirectory=Path.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);}

如果进程被强制终止,任何异步日志方案都不能保证最后一条一定落盘。所以关键动作不能只依赖日志,真正重要的数据还应该写入业务表。


级别不是越详细越好

常见级别可以这样使用:

  • Verbose:SQL、轮询细节,只在调试时开启;
  • 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 导出入口。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/7 2:43:15

源站连接超时诊断方法(进阶篇)最佳实践与踩坑记录

本文深入探讨源站连接超时诊断方法&#xff08;进阶篇&#xff09;&#xff0c;涵盖背景分析、原理剖析、实战步骤、配置示例、优化建议和避坑指南。很多团队在故障排查实战场景中都会遇到与源站连接超时诊断方法&#xff08;进阶篇&#xff09;相关的挑战。本文结合生产环境经…

作者头像 李华
网站建设 2026/10/7 2:43:13

AI赋能企业数据资产:从盘点、血缘到平台架构的落地拆解

简介&#xff1a;这份PPT方案面向金融、制造、零售等行业中负责数字化转型的决策者与数据平台规划人员&#xff0c;系统梳理了AI赋能企业数据资产管理与数据平台建设的完整路径。内容从建设背景与需求分析切入&#xff0c;覆盖总体架构设计、数据资产治理体系、AI能力平台建设、…

作者头像 李华
网站建设 2026/10/7 2:41:38

C++17 核心新特性总览:一张速查表 + 8 个上手示例

C17 不是「又多了一堆语法糖」的版本&#xff0c;它是让日常业务代码真正变短、变安全的一版&#xff1a;std::optional 替掉了 -1 这种哨兵值&#xff0c;if (auto it m.find(k); it ! m.end()) 把迭代器关进了作用域&#xff0c;std::filesystem 让路径操作不再写平台相关的…

作者头像 李华
网站建设 2026/10/7 2:41:18

USB Type-C 24脚与12脚引脚定义本质区别与工程判别法

1. 为什么24脚和12脚TYPE-C引脚定义会让人反复查资料&#xff1f;USB Type-C接口看似就一个椭圆形小孔&#xff0c;但背后藏着两套完全不同的物理结构体系——24-pin标准型和12-pin简化型。这不是版本迭代的“升级”&#xff0c;而是从设计源头就分道扬镳的两种实现路径&#x…

作者头像 李华
网站建设 2026/10/7 2:39:48

大促全链路压测中记忆系统内存泄漏事故全景剖析

大促全链路压测中记忆系统内存泄漏事故全景剖析在大促&#xff08;如双 11、618&#xff09;技术保障周期中&#xff0c;全链路军团级压测是检验系统高可用水位的终极试金石。在某头部电商集团的大促前夕演练中&#xff0c;数智导购与智能售后 Agent 集群迎来了 50,000 QPS 的全…

作者头像 李华
网站建设 2026/10/7 2:39:21

PHP + Vue 智慧城市管理系统01235----居民报修、管理员派单、员工处理:用工单链路连接城市服务与设备维护

摘要智慧城市系统如果只做公告和服务展示&#xff0c;很难真正参与城市管理。本项目把居民、员工和管理员三类角色放在同一条业务链中&#xff1a;居民可以查看服务、政策与设备信息并提交反馈或报修&#xff1b;管理员审核报修、维护设备并分配任务&#xff1b;员工负责处理维…

作者头像 李华