2026/10/13 16:46:48

工控软件日志系统设计:分级、轮转、检索,别再 printf 了

工控软件日志系统设计:分级、轮转、检索,别再 printf 了 工控软件日志系统设计分级、轮转、检索别再 printf 了引言很多上位机项目的日志是这样的Console.WriteLine(读到数据了)跑起来一屏滚出了事翻不到不出事占满盘。现场断电、通信抖动、批次混料事后能还原真相的只有日志——它不是开发期的调试输出是产线的黑匣子。这篇讲一套通用打法分级怎么定、格式怎么写、文件怎么转、坏了怎么查全部用 Python 标准库落地换 C# 思路一样。一、分级先定语义再定级别日志分级的价值不在级别名字在于每一级绑定一个动作。工控场景我建议这么定界DEBUG原始报文、轮询细节、中间变量。默认关闭排查通信问题时临时打开量极大INFO业务事实——开工、完工、换型、批次切换、班次汇总。这是事后追溯的主食WARNING程序自己处理了的异常——通信超时后重连成功、单次写库失败后重试成功。记录但不打扰人ERROR需要人介入的事——重连 N 次仍失败、数据库打不开、设备状态与预期不一致。最常见的错误是把所有异常都打成 ERROR。通信抖一下重连成功了也 ERROR一晚上几千条真正要命的报警被淹死。判断标准就一句话这条日志出来需不需要有人看到。不需要就是 WARNING 及以下。分级落地要支持全局阈值和模块阈值两层全局 INFO通信模块可以单独压到 WARNING正式量产后轮询报文没信息量调试时单独放开到 DEBUG。二、格式写结构化行别写散文2026-09-26 10:32 PLC连不上了怎么办啊——这种日志检索等于肉眼。结构化日志用 JSON 行JSONL每行一个对象固定字段加业务字段importlogging,logging.handlers,json,queue,time,atexitclassJsonFormatter(logging.Formatter):每条日志一行 JSON时间/级别/模块/消息 业务上下文(dev设备/ev事件)defformat(self,rec):returnjson.dumps({ts:time.strftime(%Y-%m-%d %H:%M:%S,time.localtime(rec.created)),lvl:rec.levelname,mod:rec.name,msg:rec.getMessage(),dev:getattr(rec,dev,),# 业务字段设备编号ev:getattr(rec,ev,),# 业务字段事件码},ensure_asciiFalse)defbuild_logger(pathlogs/app.log):qqueue.SimpleQueue()rootlogging.getLogger()root.setLevel(logging.INFO)root.addHandler(logging.handlers.QueueHandler(q))# 业务线程只往队列丢不碰磁盘fhlogging.handlers.RotatingFileHandler(path,maxBytes10*1024*1024,backupCount10,encodingutf-8)fh.setFormatter(JsonFormatter())listenerlogging.handlers.QueueListener(q,fh,respect_handler_levelTrue)listener.start()atexit.register(listener.stop)returnroot logbuild_logger()log.info(批次开工,extra{dev:PLC-01,ev:batch_start})log.warning(通信超时重连成功,extra{dev:PLC-01,ev:poll_timeout})两个设计点一是extra带结构化业务字段设备号、事件码检索时能按字段过滤不用正则猜二是QueueHandler 把写盘挪到后台线程——采集主循环里同步写文件磁盘一抖就拖慢轮询周期工控机上这是真事故源。三、轮转日志写满盘 二次事故用 RotatingFileHandler 按大小轮转上面maxBytesbackupCount10MB × 10 份封顶 100MB。三个量要按现场算日志速率 × 保留天数 ≤ 磁盘余量的十分之一。想多留历史压缩归档旧文件别加份数。工控机磁盘小日志把盘写满的后果是数据库也写不进去——一次通信抖动放大成整线停机。所以轮转上限不是可选项是系统设计的一部分。四、检索为十分钟内出结论设计有了 JSONL检索就是过滤。现场要的答案通常是三类某设备最近半小时的 WARNING、某个时间段全厂 ERROR、某事件码的出现次数。一个 30 行脚本全覆盖importjson,datetimeasdtdefquery(path,levelNone,devNone,minutes30,tail50):cutdt.datetime.now()-dt.timedelta(minutesminutes)out[]withopen(path,encodingutf-8)asf:forlineinf:try:rjson.loads(line)exceptjson.JSONDecodeError:continue# 半行坏记录(断电截断)直接跳过ifdt.datetime.strptime(r[ts],%Y-%m-%d %H:%M:%S)cut:continueiflevelandr[lvl]!level:continueifdevandr[dev]!dev:continueout.append(line.rstrip())returnout[-tail:]forlinquery(logs/app.log,levelWARNING,devPLC-01):print(l)注意json.JSONDecodeError那行断电最容易把最后一行日志截成半行检索代码必须容忍不然排查工具自己先崩。五、三个避坑时间戳用统一时钟。多台工控机时间不齐日志对不上时间轴。开机对时时间戳带毫秒更好日志里永远不写口令和密钥。配置里的连接串、授权码打印前先抹掉——日志文件会被原样发出去排障走到哪泄露到哪别在 ERROR 里只写失败两个字。上下文设备号、参数、当时状态一次带全现场没有第二次复现机会。要点回顾分级绑定动作ERROR要人介入自愈的异常降为 WARNINGJSONL 结构化格式 extra业务字段检索按字段过滤不靠正则QueueHandler 异步写盘轮转封顶日志永远不能写满盘检索脚本容忍半行坏记录为断电后十分钟出结论设计。这套东西在我参与的采集、测试、报表类项目里是同一份代码复制着用的。系列前作写过配置管理和报警系统设计日志体系是同一层基础设施——报警通知链的事后可查、远程排障的有据可依都建立在日志先记对的基础上。完整可运行的日志模块Python 版含按模块动态调级 压缩归档另附 C# NLog 等价配置放在作者付费资源里拿去就能挂进现有工程。