第三章 日志系统搭建:syslog与自定义日志等级、日志轮转策略、远程日志收集方案

做嵌入式WebRTC开发,最怕什么?设备在现场跑着,突然通话卡顿、断连、甚至死机。你手里没有屏幕,没有键盘,唯一能抓住的就是日志。日志系统搭得好不好,直接决定了你排查问题的效率。这一章,我就把我在多个嵌入式WebRTC项目里积累的日志搭建经验,掰开了讲给你听。

3.1 为什么syslog是嵌入式首选?

很多刚入行的朋友喜欢自己写日志函数,往文件里fprintf。说实话,我早期也这么干过。但后来发现,自己写的日志系统在嵌入式环境里问题太多:没有等级控制、没有轮转、多线程写日志还会乱掉。

syslog是Linux系统原生的日志服务,它有几个天然优势:

  • 标准化:几乎所有嵌入式Linux发行版都支持
  • 等级分明:从emerg到debug,8个等级够你用
  • 远程能力:原生支持UDP发送到远程日志服务器
  • 性能好:异步写入,不会阻塞你的WebRTC音视频线程

我个人习惯,只要板子上有Linux内核,第一件事就是把syslog配好。你想想看,一个通话质量分析,需要毫秒级的时间戳和准确的日志等级,自己写的日志函数能做到吗?

3.2 自定义日志等级:别被8个等级框死

syslog自带的8个等级(emerg、alert、crit、err、warning、notice、info、debug)在通用场景下够用。但做WebRTC通话,你会发现有些场景很尴尬。

举个例子:ICE连接状态变化。这算info还是debug?说它是info吧,ICE状态切换太频繁,日志会刷屏;说它是debug吧,出问题时你又想快速看到它。

我的做法是:在syslog等级基础上,叠加一个模块标签和子等级。

// 自定义日志宏
#define LOG_WEBRTC(level, module, fmt, ...) \
    do { \
        if (level <= g_log_level) { \
            syslog(level, "[%s] " fmt, module, ##__VA_ARGS__); \
        } \
    } while(0)

// 使用示例
LOG_WEBRTC(LOG_INFO, "ICE", "新连接建立, 候选者类型: %s", cand_type);
LOG_WEBRTC(LOG_DEBUG, "RTP", "序列号: %u, 时间戳: %u", seq, ts);

这样,你可以通过g_log_level全局变量,动态控制日志的详细程度。我在项目中遇到过,现场调试时把等级调到DEBUG,问题复现后立刻调回INFO,既拿到了关键信息,又不会把存储空间撑爆。

小技巧:把模块标签统一用大写字母,比如ICE、RTP、SDP、STUN。这样在日志文件里grep的时候,一眼就能定位到目标模块。

3.3 日志轮转策略:别让日志撑爆Flash

嵌入式设备的存储空间通常很紧张。一块128MB的Flash,系统占一半,应用占一部分,留给日志的空间可能就10MB。如果不做轮转,一个DEBUG级别的日志跑一天就能把空间吃光。

syslogd自带的轮转机制,说实话,在嵌入式环境里不太够用。我推荐用logrotate工具,配合自定义配置。

这是我的典型配置:

/var/log/webrtc.log {
    size 5M          # 每个日志文件最大5MB
    rotate 3         # 保留3个历史文件
    compress         # 压缩历史文件
    delaycompress    # 延迟压缩,方便查看最新历史
    missingok        # 文件不存在不报错
    notifempty       # 空文件不轮转
    postrotate
        # 通知应用重新打开日志文件
        kill -HUP `cat /var/run/syslogd.pid`
    endscript
}

为什么用size而不是daily?因为嵌入式设备的日志产生速度不稳定。通话高峰期,一小时可能产生10MB日志;空闲时段,一天可能只有几百KB。按大小轮转更合理。

轮转策略 适用场景 优点 缺点
按大小(size) 日志产生速度波动大 存储空间可控 轮转频率不确定
按时间(daily) 日志产生速度稳定 时间规律,便于归档 高峰期可能撑爆空间
按数量(rotate) 存储空间极小 历史文件数量固定 可能丢失重要历史

警告:我曾经在一个项目里,因为日志轮转配置不当,导致syslogd进程把Flash写坏了。原因是轮转时同时压缩和删除,I/O压力太大。后来我加了delaycompress和missingok,问题才解决。

3.4 远程日志收集方案:现场出问题,你在办公室就能看

嵌入式设备部署在客户现场,你不可能每次都跑过去插串口线。远程日志收集是必备能力。我常用的方案有三种:

方案一:syslog远程发送(最简单)

修改syslog配置文件,把日志同时发到远程服务器:

# /etc/syslog.conf
# 本地记录
*.* /var/log/webrtc.log
# 远程发送
*.* @192.168.1.100:514

远程服务器上启动一个syslogd监听即可:

syslogd -R 0.0.0.0:514

这个方案的好处是零代码改动,但缺点也很明显:UDP传输可能丢包,而且没有加密。

方案二:TCP长连接+自定义协议(推荐)

我在项目中用得最多的是这个方案。在设备端开一个线程,维护到日志服务器的TCP长连接,把日志按自定义格式发送。

// 伪代码示例
void *log_sender_thread(void *arg) {
    int sock = tcp_connect("logserver.company.com", 9000);
    while (1) {
        char log_buf[1024];
        // 从本地日志队列取一条日志
        log_queue_pop(log_buf, sizeof(log_buf));
        // 发送到远程服务器
        send(sock, log_buf, strlen(log_buf), 0);
        // 心跳保活
        if (need_heartbeat()) {
            send(sock, "PING", 4, 0);
        }
    }
}

TCP方案可靠,但要注意:网络断开时要自动重连,而且不能阻塞主业务线程。我习惯用环形缓冲区做日志队列,生产者(业务线程)和消费者(发送线程)解耦。

方案三:日志文件上传(适合弱网环境)

如果网络不稳定,TCP长连接经常断,那就用这个方案:日志先写本地,轮转后把历史文件通过HTTP/FTP上传到服务器。

我做过一个项目,设备在海上平台,网络时断时续。TCP长连接方案根本跑不起来。后来改成:本地保留3个日志文件,轮转时触发上传,上传失败就等下次轮转再试。效果很好。

核心要点:远程日志收集,可靠性比实时性重要。宁可晚到几分钟,也不能丢日志。因为你要靠这些日志还原现场,少一条关键日志,可能就找不到根因。

3.5 整体架构:一张图看懂

下面这张图,是我在多个项目里沉淀下来的日志系统架构。你看完应该能直接照着搭。

嵌入式WebRTC日志系统架构图 嵌入式WebRTC日志系统架构 WebRTC应用 系统服务 第三方库 统一日志接口 (syslog + 自定义宏) 日志处理 (等级过滤 + 模块分类 + 格式化) 本地文件 远程TCP 环形缓冲区 logrotate轮转 远程日志服务器 日志队列消费 日志产生源 统一入口 处理与过滤 分发策略 持久化与传输

这个架构的核心思想是分层解耦。应用层只管调用日志接口,不用关心日志最终去了哪里。处理层负责等级过滤和模块分类。分发层根据配置,决定日志是写本地、发远程、还是先缓存。底层负责轮转、上传、重连等脏活累活。

我建议你从最简单的方案一开始搭,跑通了再加远程和轮转。别一上来就想搞个大而全的系统,容易把自己绕进去。

3.6 避坑指南:我踩过的几个坑

日志系统看起来简单,但实际落地时坑不少。我挑几个典型的说说:

  • 日志打太多,影响WebRTC实时性:我曾经在ICE连接阶段打了大量DEBUG日志,结果导致网络线程被日志I/O阻塞,连接建立时间从500ms变成了3秒。解决办法:把日志写入放到独立线程,用异步队列。
  • 远程日志丢数据:UDP发送日志,网络抖动时丢包严重。后来我改成TCP+本地缓存,发送失败就暂存,下次重发。
  • 时间戳不准:嵌入式设备如果没有NTP同步,系统时间可能差几个小时。我习惯在日志里同时打上系统时间和上电以来的毫秒数(uptime),这样即使系统时间不准,也能分析出事件发生的相对顺序。
  • 日志文件权限问题:WebRTC进程如果以非root用户运行,可能没有权限写日志文件。记得在syslog配置里指定正确的用户和组。

重要提醒:千万不要在生产环境把日志等级开到DEBUG。我见过一个团队,设备上线后忘了改日志等级,结果DEBUG日志每小时产生200MB数据,两天后Flash写满,设备直接死机。教训深刻。