# 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` 请求体超过上限（默认 2MB;服务端查 `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 ≤2MB。
- `client_ts` 是端侧时间（客户端时钟不可信），`time` 由服务端补（可靠时间轴）。
- `user_id` 不靠 JWT:未登录态也要采日志;带上便于按用户排查。
