diff --git a/docs/api/README.md b/docs/api/README.md index 4ade4e6..26e911e 100644 --- a/docs/api/README.md +++ b/docs/api/README.md @@ -106,9 +106,10 @@ | 38 | `POST /api/v1/feedback` | Bearer | [详情](./other/feedback.md) | | 38a | `GET /api/v1/feedback/config` | Bearer | [详情](./other/feedback-config.md)(反馈页「加群二维码」卡配置:开关+二维码图+三行文案) | | 38b | `GET /api/v1/feedback/records` | Bearer | [详情](./other/feedback-records.md)(我的反馈历史,pending/adopted/rejected) | -| **埋点 & 订单上报**(前缀分散;全部 Bearer 除 analytics/events 不强制登录) ||| +| **埋点 & 订单上报 / 客户端日志**(前缀分散;全部 Bearer 除 analytics/events、applog/batch 不强制登录) ||| | E1 | `POST /api/v1/analytics/events` | 无 | [详情](./other/analytics-events.md)(批量上报埋点事件,不强制登录,每批最多200条) | | E2 | `POST /api/v1/order/report` | Bearer | [详情](./other/order-report.md)(上报归因订单,比价后5分钟内点链接+支付金额与比价价相差≤1元) | +| E3 | `POST /api/v1/applog/batch` | 无 | [详情](./other/applog-batch.md)(批量上报客户端运行日志,逐条落独立文件供 Logtail 采进 SLS,不强制登录,每批≤500条) | | **首页门面数据 / 客户端配置**(前缀 `/api/v1/platform`;全平台展示数字 + 运营开关,**全部不鉴权**,登录前可读) ||| | 39 | `GET /api/v1/platform/stats` | 无 | [详情](./platform/platform-stats.md) | | 40 | `GET /api/v1/platform/savings-feed` | 无 | [详情](./savings/platform-savings-feed.md) | diff --git a/docs/api/other/applog-batch.md b/docs/api/other/applog-batch.md new file mode 100644 index 0000000..8a0e729 --- /dev/null +++ b/docs/api/other/applog-batch.md @@ -0,0 +1,87 @@ +# POST /api/v1/applog/batch — 批量上报客户端运行日志 + +> 所属:客户端日志组(前缀 `/api/v1/applog`) | 鉴权:无(不强制登录,`user_id` 可选带上) | [← 返回 API 索引](../README.md) + +批量接收客户端 App 运行日志(自动化步骤 / 网络 / 崩溃 / 调试等),**逐条**封装成单行 JSON 写入独立滚动文件 `logs/app-client.log`(与服务日志 `app-server.log` 分开),由阿里云 Logtail 采进**独立 SLS logstore**,**不落库**。服务端补 `client_ip`(X-Forwarded-For)与 `time`(接收时间,SLS 主时间)。`trace_id` 提到输出行顶层,便于在 SLS 里跨「客户端 / 服务端」两个 logstore 按 trace 拼出端到端链路。**fire-and-forget**:写失败也返回 2xx,不 500。 + +## 入参 + +批级公共字段发一次;`logs` 里每条只带日志本身。**每条只有 `client_ts/level/trace_id/tag/msg` 会提到输出顶层,其余自定义字段一律并入输出的 `data`**(防 SLS 索引列爆炸)。 + +| 字段 | 类型 | 必填 | 说明 | +|---|---|---|---| +| `device_id` | string | ✅(≤64) | 设备 ID(限流 / 分组键) | +| `user_id` | int \| null | ❌ | 登录用户 ID(未登录可空) | +| `app_ver` | string \| null | ❌(≤32) | App 版本 | +| `platform` | string \| null | ❌(≤16) | 平台(`android` / `ios` / `harmony`) | +| `sent_at` | int \| null | ❌ | 批次发送时间(epoch ms) | +| `logs` | list[object] | ✅(1-500 条) | 日志数组(每条为对象,内部字段**不强类型**) | +| `logs[].client_ts` | int | ❌ | 端侧日志时间(epoch ms) | +| `logs[].level` | string | ❌ | 级别(服务端归一化大写,截断 ≤16) | +| `logs[].trace_id` | string \| null | ❌ | 关联服务端比价链路的 trace(有服务端交互的日志带上,截断 ≤256) | +| `logs[].tag` | string \| null | ❌ | 模块 / 分类(截断 ≤128) | +| `logs[].msg` | string | ❌ | 消息主体(超 8KB **字节**截断,加 `msg_truncated` 标记) | +| `logs[].*` | any | ❌ | 其余任意自定义字段 → 一律并入输出的 `data`(客户端自带的 `data` 对象会被合并进来) | + +Mock 入参: +```json +{ + "device_id": "android_abc123def456", + "user_id": 42, + "app_ver": "0.1.5", + "platform": "android", + "sent_at": 1719993700000, + "logs": [ + { + "client_ts": 1719993600000, + "level": "info", + "tag": "automation", + "msg": "compare flow start", + "trace_id": "t_ab12cd34" + }, + { + "client_ts": 1719993615000, + "level": "error", + "tag": "network", + "msg": "timeout calling /price/step", + "trace_id": "t_ab12cd34", + "http_status": 504, + "retry": 2 + } + ] +} +``` +> 上例第二条的 `http_status` / `retry` 不在白名单 → 会被并入落盘行的 `data`:`{"http_status":504,"retry":2}`。 + +## 出参 + +响应 `200`:`AppLogIngestOut` + +| 字段 | 类型 | 说明 | +|---|---|---| +| `ok` | bool | 固定 `true` | +| `received` | int | 成功写入文件的条数 | +| `dropped` | int | 跳过的条数(服务端处理 / 序列化失败;正常为 `0`) | + +Mock 出参: +```json +{ + "ok": true, + "received": 2, + "dropped": 0 +} +``` + +## 错误码 +- `413` 请求体超过上限(默认 1MB;服务端查 `Content-Length`,在 body 校验前拦截。缺该头时由 nginx `client_max_body_size` 兜底) +- `422` `logs` 为空或超过 500 条 / `device_id` 缺失 / 字段类型不符 +- `429` 触发限流(同 IP 每分钟 > 120 次) + +## 说明 +- **落盘 → SLS**:逐条写独立文件 `logs/app-client.log`(单行 JSON,`propagate=False` 不污染 `app-server.log`),由 Logtail JSON 模式采进**独立 logstore**;不进数据库。落盘行除白名单字段外,服务端另补 `time`(接收时间)、`source="client"`、`service`、`client_ip` 及批级 `device_id/user_id/app_ver/platform/sent_at`。 +- **`trace_id` 跨层检索**:字段名与服务端日志一致。客户端应给**有服务端交互**的日志带上当初 API 返回的 `trace_id`(如 `/api/v1/price/step` 等透传族返回的 trace),即可在 SLS 里 `trace_id: "xxx"` 一查拼出「客户端视角 + 服务端比价链路」;纯客户端日志不带即可。 +- **白名单 + `data` 兜底**:只有 `client_ts/level/trace_id/tag/msg` 上顶层,其余键并入 `data`,把 SLS 索引列钉死在固定集合,防客户端任意 key 撑爆索引。索引字段另有长度上限(`level`≤16 / `trace_id`≤256 / `tag`≤128 / `msg`≤8KB)。 +- **fire-and-forget**:写文件失败也返回 2xx(避免客户端重试风暴);网络重试可能在 SLS 造成重复条目,可接受。 +- **量级建议**:客户端做等级过滤 / 采样,攒到一定量再批量上报;单批 ≤500 条、body ≤1MB。 +- `client_ts` 是端侧时间(客户端时钟不可信),`time` 由服务端补(可靠时间轴)。 +- `user_id` 不靠 JWT:未登录态也要采日志;带上便于按用户排查。