540 lines
18 KiB
Markdown
540 lines
18 KiB
Markdown
# 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()` 钩子,在保留原有串口输出的同时,将日志复制到远程日志缓冲区。
|
||
|
||
```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_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
|
||
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. 用户对话期间暂停上传,保证实时语音不受干扰
|