Files
lucky-cat/remote-logging-system-design.md
T

18 KiB
Raw Blame History

TQ_catN 远程日志系统设计方案

版本:v1.0(待审核) 日期:2026-06-27 范围:全设备日志收集、本地缓冲、网络上传


一、设计目标

目标 说明
可靠性 网络抖动或断开时,日志不丢失,具备本地持久化和重传能力
低开销 对实时语音交互(PTT/WS/audio)零干扰,CPU 占用 < 5%,内存占用 < 256KB
分层合规 严格遵循现有四层单向依赖架构(platform → domain → control_plane → app_composition)
可配置 支持按模块/级别动态开关,支持生产环境静默/调试环境全量
安全性 日志上传使用 HTTPS + device JWT 认证,敏感字段自动脱敏

二、架构设计

2.1 与现有分层架构的集成

app_composition
    └── 初始化 remote_log_runtime(在 boot sequence 中启动)

control_plane
    └── remote_log_policy.c         # 策略:上传触发条件、级别控制、模块过滤
    └── remote_log_config.h         # 模块/级别开关配置

domain
    └── remote_log_format.c         # 日志格式化:JSON 序列化、时间戳、device_id 注入
    └── remote_log_compress.c       # 可选:压缩(miniz/lz4)

platform
    └── remote_log_buffer.c         # 双缓冲:内存 ring buffer + 文件队列(LittleFS)
    └── remote_log_transport.c      # 网络传输:复用 platform_https_post_json
    └── remote_log_hook.c           # ESP-IDF log hook:esp_log_set_vprintf 重定向

依赖方向:

  • platform 层提供底层能力(buffer、transport、hook)
  • domain 层提供格式化/压缩(无状态转换)
  • control_plane 层提供策略(何时上传、上传什么)
  • app_composition 层负责初始化编排

禁止:control_plane / domain 直接调用 esp_log_set_vprintf 或 esp_http_client API,必须通过 platform facade。


2.2 数据流

[App Code: ESP_LOGI/ESP_LOGE]
    │
    ▼
[platform/remote_log_hook.c] ──→ 拦截 ESP-IDF 日志输出
    │                              (原始串口输出保留,不影响调试)
    ▼
[platform/remote_log_buffer.c]
    ├── 内存环形缓冲区(热路径,ISR/任务安全)
    │   └── 容量:64KB(PSRAM 优先,内部 RAM 回退)
    │
    └── 文件队列(冷路径,网络断开时持久化)
        └── 容量:512KB(LittleFS gifpack 分区空闲空间)
        └── 最大文件数:32 个,每个 16KB
        └── 命名:log_batch_001.jsonl ~ log_batch_032.jsonl
    │
    ▼(满足上传条件时)
[domain/remote_log_format.c]
    └── 封装为 JSON batch:
        {
          "device_id": "66c...",
          "batch_id": "uuid",
          "timestamp_ms": 1782552243000,
          "logs": [
            {"ts": 1782552243001, "level": "E", "tag": "cp_mm_session", "msg": "..."},
            ...
          ]
        }
    │
    ▼
[domain/remote_log_compress.c] ──→ 可选:miniz 压缩(压缩率 ~60%)
    │
    ▼
[platform/remote_log_transport.c]
    └── 复用 platform_https_post_json() → 服务端
        POST https://gz.api.talkingq.com/log/batch
        Content-Type: application/json
        Authorization: Bearer <device_token>

三、核心模块设计

3.1 日志拦截层(platform/remote_log_hook.c)

方案:使用 ESP-IDF 的 esp_log_set_vprintf() 钩子,在保留原有串口输出的同时,将日志复制到远程日志缓冲区。

// 接口设计
void platform_remote_log_hook_install(void);
void platform_remote_log_hook_uninstall(void);

// 内部实现
static int remote_log_vprintf(const char *fmt, va_list args)
{
    // 1. 调用原始 vprintf,保持串口输出
    int ret = s_original_vprintf(fmt, args);

    // 2. 格式化为结构化记录,写入 ring buffer
    //    使用非阻塞写入,buffer 满时丢弃最旧记录
    platform_remote_log_buffer_write(fmt, args);

    return ret;
}

关键约束:

  • remote_log_vprintf 可能在 ISR 上下文被调用(如 ESP_EARLY_LOG),因此 platform_remote_log_buffer_write 内部必须:
    • 使用 xRingbufferSendFromISR 或自旋锁保护的环形缓冲区
    • 禁止任何可能阻塞的操作(malloc、semaphore take 等)
  • 如果检测到 ISR 上下文,只做原子计数 + 标记,由后台任务在退出 ISR 后批量拷贝

日志级别过滤:在 hook 层不做级别过滤(避免重复判断),过滤由 control_plane 策略层通过 Kconfig 宏控制编译期开关。


3.2 双缓冲层(platform/remote_log_buffer.c)

3.2.1 内存环形缓冲区(热路径)

#define REMOTE_LOG_RING_BUFFER_SIZE  (64 * 1024)   // 64KB
#define REMOTE_LOG_RING_BLOCK_SIZE   256            // 每条日志最大 256 字节(超长截断)

static uint8_t s_ring_buffer[REMOTE_LOG_RING_BUFFER_SIZE];  // PSRAM 优先分配
static volatile size_t s_ring_write_idx;
static volatile size_t s_ring_read_idx;

设计:

  • 使用简单的无锁环形缓冲区(单生产者:hook ISR/任务;单消费者:后台 flush 任务)
  • 写入:原子递增 write_idx,memcpy 数据
  • 读取:后台任务定期(100ms)将数据从 ring buffer 搬到文件队列
  • 满策略:丢弃最旧记录(覆盖 read_idx 指向的数据),避免阻塞生产者

3.2.2 文件队列(冷路径 / 持久化)

#define REMOTE_LOG_FILE_MAX_BATCHES   32
#define REMOTE_LOG_FILE_BATCH_SIZE    (16 * 1024)  // 16KB / 文件

设计:

  • 使用 LittleFS 的 gifpack 分区空闲空间(当前分区 8MB,OTA 后仍有富余)
  • 文件名:/littlefs/log/batch_001.jsonl ~ /littlefs/log/batch_032.jsonl
  • 写入格式:JSON Lines(每行一条 JSON,便于追加和断点续传)
  • 循环覆盖:当 32 个文件都写满后,覆盖最早的文件

掉电保护:

  • 每个 batch 文件写完后调用 fsync,确保数据落盘
  • 启动时扫描 /littlefs/log/ 目录,恢复未上传的 batch

3.3 格式化层(domain/remote_log_format.c)

输入:原始 ESP-IDF 日志字符串(如 I (1934379) cp_mm_session: Transport unavailable)

输出:结构化 JSON batch

typedef struct {
    int64_t timestamp_ms;       // SNTP 同步后的毫秒时间戳
    char level;                 // 'V'/'D'/'I'/'W'/'E'
    char tag[32];               // 模块 TAG
    char message[192];          // 日志内容(超长截断)
} remote_log_entry_t;

批处理策略:

  • 每批最多 50 条日志 或 每批最多 8KB 数据(先到为准)
  • 超时机制:即使不满 50 条,最多等待 5 秒后强制上传
  • 压缩:使用 miniz(zlib 兼容,纯 C,~8KB 代码)压缩 JSON batch,减少流量 50-70%

脱敏规则:

  • 自动匹配并替换:password=xxx、Bearer xxx、token=xxx
  • 使用正则或简单字符串扫描:s/([Pp]assword|[Tt]oken|[Bb]earer)[=:]\S+/\1=***REDACTED***/g

3.4 传输层(platform/remote_log_transport.c)

复用现有能力:

  • HTTP 客户端:复用 platform_https_post_json()(已有 TLS、DNS、超时处理)
  • 认证:复用 device JWT token(已有 control_plane_multimodal_auth_service)

上传策略:

typedef struct {
    const char *endpoint_url;           // 如 "https://gz.api.talkingq.com/log/batch"
    uint32_t batch_max_count;           // 50
    uint32_t batch_max_size_bytes;      // 8192
    uint32_t flush_interval_ms;         // 5000
    uint32_t upload_timeout_ms;         // 10000
    uint32_t max_retry_count;           // 3
    uint32_t retry_backoff_ms;          // 2000
} remote_log_transport_config_t;

上传流程:

  1. 检查 Wi-Fi 是否连接(control_plane_is_sta_connected())
  2. 检查 device token 是否有效(复用 auth service)
  3. 读取一批日志(优先内存 buffer,其次文件队列)
  4. 封装 JSON batch + device_id + batch_id
  5. 调用 platform_https_post_json() 上传
  6. 上传成功:删除已上传的文件 batch / 释放内存 buffer
  7. 上传失败(非 401):按指数退避重试(2s / 4s / 8s),失败后回写文件队列
  8. 上传失败(401):触发 device token 刷新,刷新成功后重试

网络降级:

  • WS 连接活跃时(用户正在对话):暂停日志上传,避免干扰实时音频/WS 流量
  • Wi-Fi 断开时:完全停止上传,仅写入文件队列
  • 内存 buffer 满且文件队列满时:丢弃最旧记录(采样策略:优先保留 Error/Warning)

3.5 策略层(control_plane/remote_log_policy.c)

职责:控制"上传什么、何时上传、上传多少"

typedef enum {
    REMOTE_LOG_LEVEL_NONE = 0,
    REMOTE_LOG_LEVEL_ERROR = 1,     // 仅 Error
    REMOTE_LOG_LEVEL_WARN = 2,      // Error + Warning
    REMOTE_LOG_LEVEL_INFO = 3,      // Error + Warning + Info
    REMOTE_LOG_LEVEL_DEBUG = 4,     // 全部
} remote_log_level_t;

// 模块级别开关(与现有 TAG 对齐)
typedef struct {
    bool cp_mm_session;             // control_plane multimodal session
    bool cp_mm_runtime;             // control_plane multimodal runtime
    bool cp_workers;                // control_plane runtime workers
    bool cp_connectivity;           // control_plane connectivity
    bool transport_ws;              // platform transport ws
    bool audio_pipeline;            // platform audio pipeline
    bool direction;                 // domain direction
    bool fortune;                   // domain fortune
    // ... 其他模块
} remote_log_module_mask_t;

策略规则:

  • 生产环境默认:LEVEL_ERROR,仅关键模块开启
  • 调试环境默认:LEVEL_DEBUG,全部模块开启
  • 动态调整:通过 LAN HTTP 配置页面(复用 domain_user_config_server)下发策略变更

四、Kconfig 配置项

建议在 main/Kconfig.projbuild 中新增菜单:

menu "Remote Logging"

    config TQ_REMOTE_LOG_ENABLED
        bool "Enable remote logging"
        default n
        help
            Enable log collection and remote upload. Increases flash
            usage by ~24KB and RAM usage by ~80KB.

    config TQ_REMOTE_LOG_LEVEL
        int "Default log level for remote upload"
        default 1
        range 0 4
        help
            0=NONE, 1=ERROR, 2=WARN, 3=INFO, 4=DEBUG

    config TQ_REMOTE_LOG_RING_BUFFER_SIZE
        int "Memory ring buffer size (KB)"
        default 64
        range 8 256

    config TQ_REMOTE_LOG_FILE_BATCHES
        int "Max file batches on LittleFS"
        default 32
        range 8 64

    config TQ_REMOTE_LOG_BATCH_MAX_COUNT
        int "Max log entries per batch"
        default 50
        range 10 200

    config TQ_REMOTE_LOG_FLUSH_INTERVAL_MS
        int "Flush interval (ms)"
        default 5000
        range 1000 30000

    config TQ_REMOTE_LOG_UPLOAD_TIMEOUT_MS
        int "Upload HTTP timeout (ms)"
        default 10000
        range 3000 30000

    config TQ_REMOTE_LOG_COMPRESS
        bool "Enable miniz compression"
        default y
        help
            Compress log batches before upload. Reduces data usage
            by ~60% at the cost of ~8KB flash and ~2KB RAM.

    config TQ_REMOTE_LOG_ENDPOINT
        string "Log upload endpoint URL"
        default "https://gz.api.talkingq.com/log/batch"
        help
            HTTPS endpoint for log batch upload.

endmenu

五、内存和性能预算

5.1 Flash 占用

组件 代码+数据 说明
remote_log_hook.c ~4KB 钩子 + ring buffer 管理
remote_log_buffer.c ~6KB 双缓冲 + LittleFS 文件操作
remote_log_transport.c ~4KB HTTP 上传 + 重试逻辑
remote_log_format.c ~6KB JSON 序列化 + 脱敏
remote_log_compress.c ~8KB miniz 压缩库
remote_log_policy.c ~2KB 策略判断 + 模块过滤
合计 ~30KB 可配置关闭压缩节省 8KB

5.2 RAM 占用(运行时)

组件 内存 位置
Ring buffer 64KB PSRAM(配置为 PSRAM 优先)
Upload working buffer 8KB PSRAM
HTTP client(复用现有) ~4KB 内部 RAM(复用已有实例)
格式化/压缩临时区 ~4KB PSRAM
合计 ~80KB 其中内部 RAM < 10KB

5.3 CPU 占用

场景 CPU 占用
日志写入(hook) < 1%(纯内存拷贝)
定时 flush(100ms) < 2%(文件写入)
上传(每 5s 一次) < 5%(JSON 格式化 + HTTP POST)
用户对话期间(WS 活跃) 0%(暂停上传)

六、启动时序

在 app_composition_default_runtime.c 的 boot sequence 中,在 control_plane_init() 之后、wifi_bootstrap 之前插入:

// 步骤 7.5:初始化远程日志系统
esp_err_t log_err = control_plane_init_remote_logging();
if (log_err != ESP_OK) {
    ESP_LOGW(TAG, "Remote logging init failed: %s", esp_err_to_name(log_err));
    // 非致命错误,继续启动
}

理由:

  • 在 control_plane_init() 之后:确保 device auth 已加载(用于上传认证)
  • 在 wifi_bootstrap 之前:确保 Wi-Fi 连接建立后日志系统已就绪,可以立即开始收集

七、服务端接口契约

7.1 上传接口

POST /log/batch
Content-Type: application/json
Authorization: Bearer <device_jwt_token>

{
  "device_id": "66c7f8a2e4b0d1e2f3a4b5c6",
  "batch_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "timestamp_ms": 1782552243000,
  "firmware_version": "1.2.3",
  "logs": [
    {
      "ts": 1782552243001,
      "level": "E",
      "tag": "transport_ws",
      "msg": "Sec-WebSocket-Accept not found"
    },
    {
      "ts": 1782552243010,
      "level": "W",
      "tag": "cp_mm_session",
      "msg": "Transport unavailable: reason=ws_error"
    }
  ]
}

7.2 服务端响应

HTTP/1.1 200 OK
Content-Type: application/json

{
  "code": 200,
  "message": "OK",
  "data": {
    "accepted": 2,
    "dropped": 0
  }
}

7.3 错误码

HTTP 状态码 含义 客户端行为
200 成功 删除已上传的 batch
400 格式错误 丢弃当前 batch(避免无限重试)
401 认证失败 刷新 device JWT 后重试
413 Payload 过大 减小 batch size 后重试
429 频率限制 指数退避后重试
500+ 服务端错误 指数退避后重试

八、安全与隐私

8.1 认证

  • 复用现有 device JWT 认证体系,不引入新密钥
  • token 过期时自动刷新(复用 control_plane_multimodal_auth_service)

8.2 传输

  • 强制 HTTPS,复用现有 TLS 证书校验(esp-x509-crt-bundle)
  • 禁止 HTTP 明文传输

8.3 脱敏

  • 自动脱敏:password、token、Bearer、secret、api_key 等前缀后的值
  • 用户配置:支持通过 LAN HTTP 配置页面自定义脱敏关键词

8.4 数据保留

  • 本地:最多保留 32 个 batch 文件(~512KB),循环覆盖
  • 服务端:建议保留 30 天,过期自动清理

九、故障排查与调试

9.1 本地诊断命令

通过现有的 factory CLI 或 LAN HTTP 配置页面,增加以下诊断接口:

GET /api/diag/log/status
→ {
    "enabled": true,
    "level": 2,
    "ring_buffer_used": 32768,
    "ring_buffer_total": 65536,
    "file_batches_pending": 5,
    "last_upload_status": "success",
    "last_upload_time": "2026-06-27T10:30:00Z"
  }

9.2 本地日志导出

GET /api/diag/log/download?batch=latest
→ 返回最近一个 batch 文件的内容(纯文本)

十、实施建议

Phase 1(最小可用)

  1. platform/remote_log_hook.c — 拦截 ESP_LOG 到 ring buffer
  2. platform/remote_log_buffer.c — 内存缓冲 + 文件持久化
  3. platform/remote_log_transport.c — 复用 HTTPS 上传
  4. control_plane/remote_log_policy.c — 级别过滤 + 模块开关
  5. Kconfig 配置项 + boot sequence 初始化

Phase 2(增强)

  1. domain/remote_log_format.c — JSON 格式化 + 脱敏
  2. domain/remote_log_compress.c — miniz 压缩
  3. LAN HTTP 配置页面集成(动态调整级别)
  4. 诊断接口(/api/diag/log/status)

Phase 3(优化)

  1. 服务端实时告警(Error 级别日志触发服务端告警)
  2. 日志采样(高频 Debug 日志按概率采样)
  3. 日志关联(与 device session_id 关联,便于追踪单设备全链路)

十一、风险评估

风险 缓解措施
日志写入拖慢 ISR hook 层使用纯内存拷贝 + 无锁 ring buffer,禁止 malloc
文件写入磨损 Flash LittleFS 磨损均衡,日志 batch 文件大小控制在 16KB
上传流量过大 压缩(-60%)+ 级别过滤(仅 ERROR/WARN)+ 批量上传(5s 间隔)
上传干扰 WS 音频 WS 活跃时暂停上传,仅写入本地 buffer
敏感信息泄露 自动脱敏 + HTTPS + 服务端 ACL
内存不足 PSRAM 优先分配,内部 RAM 占用 < 10KB

十二、总结

本方案基于现有四层架构,以 "零侵入、低开销、高可靠" 为核心原则:

  • platform 层:提供日志拦截、双缓冲、网络传输的底层能力
  • domain 层:提供 JSON 格式化、压缩、脱敏的业务处理
  • control_plane 层:提供级别控制、模块过滤、上传策略的策略决策
  • app_composition 层:负责启动编排

关键设计决策:

  1. 复用 platform_https_post_json(),不引入新 HTTP 客户端
  2. 复用 device JWT,不引入新认证体系
  3. 使用 esp_log_set_vprintf() 拦截,不修改现有 ESP_LOGx 调用
  4. 使用 LittleFS 持久化,不引入新文件系统
  5. 用户对话期间暂停上传,保证实时语音不受干扰