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

540 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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. 用户对话期间暂停上传,保证实时语音不受干扰