客户打电话说"昨晚三点设备停了一会儿",你打开程序想找原因——什么都没有。这是没有日志系统的上位机最常见的窘境。日志不是调试期用完就扔的东西,而是交付后唯一的现场证据链。但日志也不是越多越好:全量 DEBUG 一天能写几个 G,把磁盘撑爆反而误事。我们把日志设计拆成三件事:分类、分级、滚动写入,这篇讲透。

先分类:三种日志写三个文件,别混在一起

运行日志 run.log

程序自身行为:启动/退出、模块加载、异常堆栈、重连过程。排"程序为什么崩"看这个。

操作日志 op.log

人的行为:谁在几点登录、改了哪个参数、下发了什么配方、确认了哪条报警。排"谁动了设备"看这个。

通信日志 comm.log

设备交互:收发报文十六进制、超时、重连、校验失败。排"设备为什么没应答"看这个。

混在一个文件里的后果是:查一次通信问题要在几十万行运行日志里翻,最后没人愿意查。分开之后每个文件职责单一,排查时直奔目标文件。通信日志量大,默认只记"异常帧 + 收发计数",需要时通过界面开关临时打开全量报文记录——这个开关我们做在诊断页面上,现场远程指导时让客户打开,比跑一趟现场快得多。

再分级:五级够用,关键是定清楚"什么进哪级"

级别记录内容现场默认
Debug变量快照、流程分支走向关闭
Info启动/停止、登录登出、配方切换、批次开始结束开启
Warn重连成功、队列水位高、单次超时后恢复、磁盘空间低于阈值开启
Error功能失败但不致命:写库失败重试、单设备通信中断开启
Fatal程序级故障:未捕获异常、数据库彻底连不上、主流程崩溃开启

▲ 判断口诀:Info 记"发生了什么正常大事",Warn 记"出了问题但自己恢复了",Error 记"出了问题需要人看一眼",Fatal 记"程序干不下去了"。级别定错最典型的是把重连成功记成 Error——重连成功是好事,记 Warn 就够。

写盘不能阻塞采集:异步队列是标配

日志写入是磁盘 IO,机械盘上单次写入几十毫秒很常见。如果在采集线程里同步写日志,一次磁盘抖动就能拖慢采集周期。我们的做法和采集数据入库一样:日志进内存队列,独立线程批量落盘

public sealed class FileLogger
{
    private readonly BlockingCollection<string> _q = new(10000);
    private readonly string _dir;

    public void Log(LogLevel lv, string category, string msg)
    {
        if (lv < MinLevel) return;
        var line = $"{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff} [{lv}] [{category}] {msg}";
        if (!_q.TryAdd(line)) Interlocked.Increment(ref _dropped);
        // 队列满直接丢并计数,日志永远不能反过来拖死业务线程
    }

    private void WriterLoop()   // 独立线程
    {
        var buf = new List<string>(256);
        foreach (var line in _q.GetConsumingEnumerable())
        {
            buf.Add(line);
            if (buf.Count >= 256 || _q.Count == 0)
            {
                AppendBatch(buf);   // 按类别路由到 run/op/comm 文件
                buf.Clear();
            }
        }
    }
}

格式上固定为 时间(毫秒) [级别] [分类] 内容 四段,毫秒精度必须有——两条报警之间差 200ms 还是差 2 秒,判断结论完全不同。

滚动与清理:别让日志把工控机磁盘吃光

工控机通常只有 128G 固态,日志不管控就是定时炸弹。我们的固定策略:

  • 按天分文件:run_20260912.log,跨天自动切换,单文件天然不会太大;
  • 单文件超 50MB 追加序号:防止某天报警风暴把单文件撑到打不开;
  • 保留 30 天,启动时清理:程序启动时扫一遍日志目录,删掉超期的——比定时器清理可靠,因为程序可能中途没运行;
  • 清理前压缩:文本日志压缩比极高,7 天以上的先 zip 再留 90 天,追溯期更长;
  • 磁盘水位监控:剩余空间低于 10% 记 Warn 并上屏提示,这是运维层面的最后一道保险。
一个必须处理的边界:日志写入本身失败怎么办。磁盘满、目录权限不对时,写日志抛异常——如果这个异常没被吞掉,会反过来把业务线程带崩。我们的规则:日志模块内部所有 IO 全部 try-catch,失败只累加计数器并降低重试频率,日志系统永远不允许向上抛异常

收尾:日志的价值在"查得到"

最后说两个让日志真正发挥作用的实践。一是关键事件带上下文:记"配方下发失败"没用,要记"配方#17 下发到 2#炉 失败,错误码 0x06,重试 2/3"——排查时不用再问任何人。二是程序里内置日志查看页,按日期、级别、关键字过滤,客户现场出问题先自己翻一遍,电话里就能定位一半。

如果你的上位机现在还没有日志,最小起步方案:先把 Info/Error 两级、按天滚动写进 run.log,一周之内你就会庆幸——第一次客户说"昨晚出过问题"时,你终于有东西可查了。