# 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 ``` --- ## 三、核心模块设计 ### 3.1 日志拦截层(platform/remote_log_hook.c) **方案**:使用 ESP-IDF 的 `esp_log_set_vprintf()` 钩子,在保留原有串口输出的同时,将日志复制到远程日志缓冲区。 ```c // 接口设计 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 内存环形缓冲区(热路径) ```c #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 文件队列(冷路径 / 持久化) ```c #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 ```c 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`) **上传策略**: ```c 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) **职责**:控制"上传什么、何时上传、上传多少" ```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` 中新增菜单: ```kconfig 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` 之前插入: ```c // 步骤 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 上传接口 ```http POST /log/batch Content-Type: application/json Authorization: Bearer { "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 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(增强) 6. `domain/remote_log_format.c` — JSON 格式化 + 脱敏 7. `domain/remote_log_compress.c` — miniz 压缩 8. LAN HTTP 配置页面集成(动态调整级别) 9. 诊断接口(`/api/diag/log/status`) ### Phase 3(优化) 10. 服务端实时告警(Error 级别日志触发服务端告警) 11. 日志采样(高频 Debug 日志按概率采样) 12. 日志关联(与 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. 用户对话期间暂停上传,保证实时语音不受干扰