日志系统:出了问题靠它救命
这是「从零搭建工业控制系统」系列第28篇。前面所有功能模块都依赖日志,这篇终于讲日志系统本身——log4net分级别输出、自定义UI Appender、性能优化、调试技巧。
没有日志的深夜
有一次凌晨两点现场报故障,说程序闪退了。我问有没有日志,对方说"有啊,但是Logs文件夹是空的"。
远程连上去一看,log4net配置文件没被复制到输出目录。程序里到处调_logger.Info(),但所有日志都写到了一个不存在的路径。
从那以后我做了三件事:配置文件设为AlwaysCopy、启动时检查Logs目录存在、加了一个UI日志面板作为兜底——就算文件日志挂了,界面上至少还能看到。
log4net配置:分级别分文件
项目用log4net按级别分文件输出:ERROR、WARN、INFO、DEBUG各一个文件。
<log4net><!-- 错误日志 --><appendername="errorFileAppender"type="log4net.Appender.RollingFileAppender"><filtertype="log4net.Filter.LevelMatchFilter"><levelToMatchvalue="ERROR"/></filter><filtertype="log4net.Filter.DenyAllFilter"/><filevalue="Logs\err.log"/><rollingStylevalue="Composite"/><datePatternvalue="yyyyMMdd"/><maxSizeRollBackupsvalue="30"/><maximumFileSizevalue="50MB"/><layouttype="log4net.Layout.PatternLayout"><conversionPatternvalue="%date{yyyy-MM-dd HH:mm:ss.fff} [%thread] %-5level %logger{1} - %message%newline"/></layout></appender></log4net>日志格式:2024-12-20 14:30:25.123 [1] ERROR ValveExecutor - 阀门TV状态变化超时
每个级别都有独立的文件和滚动策略:
| 级别 | 文件 | 最大文件数 | 单文件上限 |
|---|---|---|---|
| ERROR | err.log | 30个 | 50MB |
| WARN | warn.log | 15个 | 30MB |
| INFO | info.log | 10个 | 100MB |
| DEBUG | debug.log | 3个 | 50MB |
| PERF | perf.log | 7个 | 20MB |
ERROR保留最多(30个),因为排查问题最需要历史错误记录。DEBUG只保留3个,调试日志量大但价值低。
缓冲Appender:性能优化
工业系统每秒可能产生上百条日志,直接写文件会拖慢主线程。用BufferingForwardingAppender批量写入:
<appendername="bufferedErrorAppender"type="log4net.Appender.BufferingForwardingAppender"><bufferSizevalue="5"/><lossyvalue="false"/><fixvalue="Partial"/><evaluatortype="log4net.Core.LevelEvaluator"><thresholdvalue="ERROR"/></evaluator><appender-refref="errorFileAppender"/></appender>bufferSize=5:攒够5条一起写lossy=false:不丢日志fix=Partial:只序列化必要字段,减少开销evaluator threshold=ERROR:遇到ERROR立即刷新缓冲区
ERROR级别缓冲5条就刷,DEBUG缓冲50条加5秒超时刷新。错误日志要快,调试日志可以慢。
MinimalLock:多进程不抢锁
<lockingModeltype="log4net.Appender.MinimalLockDeleteEmpty"/>默认的ExclusiveLock会锁住文件,其他进程读不了。MinimalLock写完就释放锁,日志查看工具可以实时读取。DeleteEmpty额外处理空文件——滚动后如果文件为空就删掉,不占空间。
UI日志Appender:界面实时看日志
这是项目里最有用的自定义组件。UILogAppender继承AppenderSkeleton,把日志转发到界面的LogView面板:
publicclassUILogAppender:AppenderSkeleton{privatestaticbool_isInitialized=false;[ThreadStatic]privatestaticbool_isAppending;publicstaticvoidSetInitialized(boolinitialized){_isInitialized=initialized;}protectedoverridevoidAppend(LoggingEventloggingEvent){if(!_isInitialized||_isAppending)return;try{_isAppending=true;stringmessage=RenderLoggingEvent(loggingEvent);Application.Current?.Dispatcher?.BeginInvoke(newAction(()=>{LogViewModel.Current?.AddLogFromAppender(message);}));}finally{_isAppending=false;}}}几个关键设计:
1._isInitialized延迟激活:LogViewModel构造完成后才设为true。之前没这个标志,程序启动阶段LogViewModel还没创建,日志转发报NullRef。
2._isAppending防循环:[ThreadStatic]标志位防止LogViewModel添加日志时触发新的日志事件形成死循环。
3.Dispatcher.BeginInvoke:日志可能在后台线程产生,用Dispatcher切到UI线程更新界面。
log4net配置中注册UI Appender
<appendername="uiLogAppender"type="MyApp.Logging.UILogAppender, MyApp"><thresholdvalue="INFO"/></appender>UI只显示INFO及以上级别。DEBUG日志太频繁,刷屏影响操作员看关键信息。
配置里还可以通过ShowUILog配置项控制是否启用:
publicboolShowUILog{get;set;}=true;多环境配置
项目维护了多套log4net配置:
Configs/Log4netConfig/ ├── log4net.config # 默认 ├── log4net-development.config # 开发环境:DEBUG全开 ├── log4net-production.config # 生产环境:INFO以上 └── log4net-optimized.config # 性能优化:WARN以上开发环境开DEBUG看详细信息,生产环境只留INFO减少IO压力。切换时改App.config的引用路径就行。
调试技巧:按关键词搜日志
排查问题时不是从头看日志,是搜关键词:
# 搜所有ERRORSelect-String-Path"Logs\err*.log"-Pattern"ValveExecutor"# 搜特定时间段的阀门操作Select-String-Path"Logs\info*.log"-Pattern"2024-12-20 14:3.*Valve"# 搜跨线程异常Select-String-Path"Logs\err*.log"-Pattern"InvalidOperationException"调用方法名显示
配置里有个开关控制是否在UI日志里显示调用方法名和行号:
publicboolShowCallerInfoInUILog{get;set;}=false;默认关闭,因为获取调用栈有性能开销。排查疑难bug时临时打开,能看到每条日志是哪个方法输出的。
日志踩坑清单
| 坑 | 现象 | 解决 |
|---|---|---|
| 配置文件没复制 | Logs文件夹空 | Build Action设为AlwaysCopy |
| 启动时NullRef | LogViewModel还没创建 | _isInitialized延迟激活 |
| 日志死循环 | 界面卡死 | [ThreadStatic]防循环 |
| 文件被锁 | 查看工具读不了 | MinimalLockDeleteEmpty |
| DEBUG刷屏 | 操作员看不到关键信息 | UI只显示INFO以上 |
本篇小结
| 知识点 | 关键做法 |
|---|---|
| 分级别输出 | 每个级别独立Appender+文件 |
| 缓冲写入 | BufferingForwardingAppender批量 |
| UI日志 | 自定义UILogAppender转发到界面 |
| 防循环 | [ThreadStatic]标志位 |
| 多环境 | dev/prod/optimized三套配置 |
| 文件锁 | MinimalLockDeleteEmpty |
| 日志搜索 | Select-String按关键词搜 |
日志系统的核心:出了问题能查到,不出问题不拖慢系统。
下期预告
第29篇:项目打包与部署
最后一篇,说打包——ClickOnce、混淆、配置文件打包、现场部署流程。