18 KiB
18 KiB
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;
上传流程:
- 检查 Wi-Fi 是否连接(
control_plane_is_sta_connected()) - 检查 device token 是否有效(复用 auth service)
- 读取一批日志(优先内存 buffer,其次文件队列)
- 封装 JSON batch + device_id + batch_id
- 调用
platform_https_post_json()上传 - 上传成功:删除已上传的文件 batch / 释放内存 buffer
- 上传失败(非 401):按指数退避重试(2s / 4s / 8s),失败后回写文件队列
- 上传失败(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(最小可用)
platform/remote_log_hook.c— 拦截 ESP_LOG 到 ring bufferplatform/remote_log_buffer.c— 内存缓冲 + 文件持久化platform/remote_log_transport.c— 复用 HTTPS 上传control_plane/remote_log_policy.c— 级别过滤 + 模块开关- Kconfig 配置项 + boot sequence 初始化
Phase 2(增强)
domain/remote_log_format.c— JSON 格式化 + 脱敏domain/remote_log_compress.c— miniz 压缩- LAN HTTP 配置页面集成(动态调整级别)
- 诊断接口(
/api/diag/log/status)
Phase 3(优化)
- 服务端实时告警(Error 级别日志触发服务端告警)
- 日志采样(高频 Debug 日志按概率采样)
- 日志关联(与 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 层:负责启动编排
关键设计决策:
- 复用
platform_https_post_json(),不引入新 HTTP 客户端 - 复用 device JWT,不引入新认证体系
- 使用
esp_log_set_vprintf()拦截,不修改现有 ESP_LOGx 调用 - 使用 LittleFS 持久化,不引入新文件系统
- 用户对话期间暂停上传,保证实时语音不受干扰