Files
myhloli 366fadb1c9 Promote TSV to an independent file suffix (ADR-0034)
- Route tsv through the shared CSV flash pipeline with an independent
  file_type and metadata.file_suffix; bump docvortex pin to 0.4.9
- Add ADR-0034 documenting the content-addressed cache identity decision,
  annotate ADR-0028, and update docs/next tier-group wording, telemetry
  and e2e specs
- Cover detection, sync/async analyze, API server jobs and doclib
  lifecycle for tsv in tests
2026-09-16 17:55:07 +08:00

900 lines
35 KiB
Markdown
Raw Permalink 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.
# Telemetry 设计
状态: Superseded
取代说明: 本文已被 `telemetry-minimum.md` 全面取代,仅保留历史设计参考;现状以 `telemetry-minimum.md` 与代码为准。
读者: 产品负责人、核心开发者、doclib server 开发者、数据分析与合规参与者
范围: 下一代 MinerU 的匿名使用统计、稳定性指标和粗粒度环境画像
非目标: 计费、审计、安全风控、精确故障追踪、上传文档内容或用户输入
## 1. 目标
Telemetry 的首要目标是产品使用统计,用于评估 MinerU 的使用规模和产品影响力。
P0 同时兼顾三类问题:
1. 产品使用统计: 有多少安装实例在使用、每天使用多少次、处理了多少文件和页面。
2. 质量与稳定性: 哪些主链路失败、失败发生在哪个阶段、错误码分布如何。
3. 性能与环境画像: 不采集细粒度设备指纹,只用粗粒度字段理解运行环境和处理规模。
Telemetry 数据只用于趋势分析和产品改进,不用于计费、安全判断或强一致审计。
## 2. 采集边界
只有 doclib server 上报 telemetry。
`mineru` CLI 依赖 doclib server,因此可以被间接统计。以下纯工具无 telemetry 能力:
- parser SDK,包括 `mineru.parser.parse()` 和 `parse_async()`
- `mineru-kit parse`
- `mineru-kit api-server`
- 其他不依赖 doclib server 的专家工具
这个边界避免 parser/tool 层在作为库或底层工具使用时包含 telemetry 相关配置、存储或网络行为。
## 3. 默认策略
首次启动 doclib server 时,必须要求用户完成 telemetry 选择。
默认行为:
- 首次选择界面不预选开启或关闭。
- 用户可以关闭。
- 用户选择结果记录为 `consent_state`。
- 用户关闭 telemetry 后,不再记录新的 telemetry metrics,也不再上报。
- 关闭时,DB 中未上报 telemetry 数据应删除,避免后续误发。
用户必须可以通过命令查看和修改状态:
```bash
mineru telemetry status
mineru telemetry enable
mineru telemetry disable
mineru telemetry preview
mineru telemetry flush
```
`preview` 应展示将要上报的 JSON,但不得展示任何 token 或服务端内部凭据。
## 4. 隐私边界
Telemetry 禁止采集:
- 文档内容
- 文件名
- 文件路径
- 原始 URL
- search query
- prompt
- sha256
- job_id
- parse_id
- traceback
- exception message
- hostname
- 用户名
- 邮箱
- 账号 ID
- CPU/GPU 具体型号
- GPU 显存、驱动版本、CUDA 版本
允许采集的数据必须来自白名单字段。不得把请求体、异常对象、配置对象、`kwargs` 或任意字典直接透传到 telemetry。
## 5. 统计口径
P0 使用匿名 `installation_id` 作为主统计口径。
- `installation_id` 在 doclib server 首次启用 telemetry 时生成。
- `installation_id` 存储在 doclib DB 的 telemetry 内部状态中。
- 不依赖账号体系。
- 用户删除配置或重装后,会被视为新的安装实例。
服务端基于 `installation_id` 计算:
- 安装量: 出现过的唯一 `installation_id`
- DAU: 当日有 telemetry 上报的唯一 `installation_id`
- WAU / MAU: 对应周期内有 telemetry 上报的唯一 `installation_id`
如果未来引入账号,账号只能作为可选维度,不替代 `installation_id`。
## 6. 上报模型
P0 采用本地聚合 metrics 上报,不采用每个事件即时上报。
运行时行为:
```text
doclib server runtime
-> record metric delta
-> update telemetry aggregate in doclib DB
-> periodic flush
-> POST telemetry endpoint
-> delete flushed rows from doclib DB after success
```
建议聚合窗口:
- 本地 telemetry 配置和 telemetry 聚合数据都存储在 doclib DB 中。
- 本地按 1 小时 period 聚合。
- 上报时可以一次提交多个未 flush 的小时级 period。
- 启动后尝试 flush 一次。
- 运行中每 24 小时尝试 flush 一次。
- 网络失败或服务端 5xx 时保留本地聚合结果,后续重试。
- 服务端 4xx 表示 payload 不合法,对应 batch 应丢弃或进入本地失败记录,不应无限重试。
- flush 成功后,已被服务端确认接收的 telemetry 聚合数据应从 doclib DB 中删除。
- 同一个 period 内 context 变化可以接受;P0 flush 时使用当前 context 上报该 period 的聚合 metrics。
主流程不得等待 telemetry 上报。telemetry 失败不得影响 parse、watch、search 或 doclib API。
## 7. Endpoint
建议使用独立 telemetry endpoint:
```http
POST https://telemetry.mineru.net/v1/metrics
Content-Type: application/json
```
不建议与 `mineru.net/api` 的 parse API 混在一起,因为 telemetry 的认证、限流、存储和合规策略不同。
响应示例:
```json
{
"object": "telemetry_metrics_result",
"accepted": 42,
"rejected": 0,
"server_time": "2026-06-10T12:00:00Z"
}
```
如果服务端支持部分拒绝,可以返回:
```json
{
"object": "telemetry_metrics_result",
"accepted": 40,
"rejected": 2,
"errors": [
{
"index": 5,
"code": "invalid_dimension",
"message": "dimension value is not allowed"
}
],
"server_time": "2026-06-10T12:00:00Z"
}
```
错误信息只用于解释 schema 问题,不应包含用户上报原文中的敏感字段。
## 8. Payload 结构
一次上报包含一个 batch。环境信息放在 batch-level `context` 中,不重复放入每个 metric。
```json
{
"batch_id": "tb_01HX...",
"schema_version": "1",
"installation_id": "inst_01HX...",
"period_start": "2026-06-10T14:00:00Z",
"period_end": "2026-06-10T15:00:00Z",
"context": {
"app": "mineru",
"app_version": "3.0.0",
"os": "macos",
"arch": "arm64",
"python_version": "3.11",
"install_channel": "pip",
"cpu_count_bucket": "9_16",
"gpu_vendor": "apple"
},
"metrics": [
{
"name": "parse.completed.count",
"value": 12,
"dimensions": {
"source": "cli",
"caller": "agent",
"tier": "basic(default)",
"status": "succeeded"
}
},
{
"name": "parse.processed_page_count",
"value": 1240,
"dimensions": {
"source": "cli",
"caller": "agent",
"tier": "basic(default)"
}
}
]
}
```
规则:
- `batch_id` 由本地 flush 任务生成,用于服务端短窗口幂等。
- `value` 必须是非负数。
- 字符串数据只能出现在 `context` 或 `dimensions`。
- `context` 描述安装实例、运行环境和版本。
- `metrics[].dimensions` 描述当前 metric 的业务分类。
- `metrics[].dimensions` 必须来自白名单,不能包含任意字符串。
- `context` 表示 flush 时 doclib server 的当前运行环境。同一 period 内 context 变化可以接受,不要求拆分 batch。
## 9. Context 字段
P0 context 字段:
| 字段 | 类型 | 枚举 / 格式 | 说明 |
|------|------|-------------|------|
| `app` | string | `mineru` | 固定应用名 |
| `app_version` | string | semver 或发布版本 | MinerU 版本 |
| `os` | string | `macos` / `windows` / `linux` / `other` / `unknown` | 操作系统 |
| `arch` | string | `x86_64` / `arm64` / `other` / `unknown` | CPU 架构 |
| `python_version` | string | `3.10` / `3.11` / `3.12` / `3.13` / `other` / `unknown` | Python 主次版本 |
| `install_channel` | string | `pip` / `uv` / `docker` / `source` / `unknown` | 安装来源 |
| `cpu_count_bucket` | string | `1_4` / `5_8` / `9_16` / `gt_16` / `unknown` | CPU 核心数分桶 |
| `gpu_vendor` | string | `nvidia` / `apple` / `amd` / `none` / `unknown` | GPU 或常见加速器厂商 |
`gpu_vendor` 规则:
- `nvidia`: 识别到 NVIDIA GPU。
- `apple`: 识别到 Apple GPU。
- `amd`: 识别到 AMD GPU。
- `none`: 明确没有发现 GPU。
- `unknown`: 无法检测、检测失败,或发现了 P0 未分类的其他加速器。
P0 不采集 CPU/GPU family、具体型号、显存、驱动和 CUDA 版本。
## 10. Metric 与 Dimension 规则
Metric 的 `name` 表示数值指标,`value` 表示计数或数值累加。
字符串型信息必须作为 dimension:
```json
{
"name": "parse.completed.count",
"value": 1,
"dimensions": {
"tier": "basic",
"status": "succeeded"
}
}
```
不允许:
```json
{
"name": "parse.tier",
"value": "basic"
}
```
Dimension 只允许低基数字段:
| 字段 | 枚举 / 说明 |
|------|-------------|
| `source` | `http_api` / `sdk` / `cli` / `mcp` / `web` / `app` / `watch` / `background` / `unknown` |
| `caller` | `agent` / `user` / `http_client` / `sdk` / `web` / `app` / `system` / `unknown` |
| `tier` | `default` / `flash` / `basic` / `standard` / `advanced` / `basic(default)` / `standard(default)` / `advanced(default)` / `unknown` |
| `status` | `succeeded` / `failed` / `partial` / `canceled` |
| `input_type` | `pdf` / `csv` / `tsv` / `rtf` / `docx` / `pptx` / `xlsx` / `odt` / `ods` / `odp` / `html` / `other` / `unknown` |
| `output_format` | `middle_json` / `markdown` / `structured_content` / `other` |
| `stage` | `enqueue` / `cache_lookup` / `parse_server_call` / `parsing` / `merge_pages` / `persist` / `index` / `unknown` |
| `error_code` | 稳定错误码枚举,不允许 exception message |
| `retryable` | `true` / `false` / `unknown` |
| `bucket` | 当前 metric 定义的分桶值 |
| `server` | `local(flash)` / `local(managed)` / `local(self-hosted)` / `remote(official)` / `remote(custom)` / `none` / `unknown` |
| `rule_type` | `exclude` / `parsing` / `unknown` |
| `remote` | `true` / `false` / `unknown` |
| `command` | 当前 metric 定义的命令枚举 |
| `wait_mode` | `sync` / `async` / `unknown` |
| `wait_bucket` | `0` / `1_10s` / `11_60s` / `gt_60s` / `unknown` |
| `page_range_mode` | `all` / `partial` / `default` / `unknown` |
| `config_area` | `watch` / `exclude` / `parsing_rules` / `parse_server` / `telemetry` / `unknown` |
| `cleanup_target` | `orphan_docs` / `deleted_files` / `temp` / `mixed` / `unknown` |
| `dry_run` | `true` / `false` / `unknown` |
`caller` 与 `source` 语义不同:
- `source` 表示请求从哪个调用通道进入 doclib,例如 HTTP API、SDK、CLI、MCP、Web、App、watch 或后台任务。
- `caller` 表示发起行为的主体,例如 Agent、用户、HTTP client、SDK、Web、App 或系统后台任务。
Agent 时代的核心产品目标要求 telemetry 能区分 `agent` 与 `user`。因此,CLI 通道必须尽量识别真实调用主体。
调用通道与主体规则:
| 使用方式 | `source` | `caller` |
|----------|----------|---------------|
| 直接调用 HTTP API,且未传 caller 信息 | `http_api` | `http_client` |
| Doclib SDK | `sdk` | `sdk` |
| `mineru` CLI,由 Agent 进程调用 | `cli` | `agent` |
| `mineru` CLI,由用户 shell 调用 | `cli` | `user` |
| `mineru` CLI,无法可靠判断父进程 | `cli` | `unknown` |
| MCP Server | `mcp` | `agent` |
| Web | `web` | `web` |
| 桌面 App | `app` | `app` |
| watch 自动发现 | `watch` | `system` |
| compaction / cleanup / health check 等后台任务 | `background` | `system` |
CLI 的 `caller` 可以通过父进程识别。若父进程是 Claude Code、Codex 等已知编程 Agent,则记为 `agent`;若父进程是普通 shell 或终端,则记为 `user`;无法可靠判断时记为 `unknown`。不得通过文件路径、命令文本、query、prompt、文档内容或具体会话内容推断。
`tier` 同时表达请求档位和实际解析档位:
- 用户未指定 tier,且尚未解析到实体 tier 时,使用 `default`。
- 用户显式指定实体 tier,或任务已经完成且没有必要保留默认来源时,使用 `flash`、`basic`、`standard` 或 `advanced`。
- 用户未指定 tier,且已经解析到实体 tier,同时仍需要保留来源信息时,使用 `basic(default)`、`standard(default)` 或 `advanced(default)`。
- 默认选择不会解析为 `flash`,因此不定义 `flash(default)`。
- parse-server 相关 metric 也使用同一个 `tier` 维度,不再单独使用 `parse_server_tier`。
`server` 表达解析执行位置和服务形态:
- `local(flash)`: 本地 flash 解析,不经过 parse-server。
- `local(managed)`: doclib 管理的本地 parse-server。
- `local(self-hosted)`: 用户自行启动的本地或自部署 parse-server。
- `remote(official)`: 官方 `mineru.net/api`。
- `remote(custom)`: 用户显式配置的其他远端兼容 parse-server。
- `none`: 当前行为没有解析服务,例如 cache hit、查询类操作或本地 parse-server disabled 配置状态。
- `unknown`: 无法可靠判断。
`parse_server.*` metric 不使用 `server=local(flash)`,因为 flash 不经过 parse-server。`parse.*` metric 可以使用 `server=local(flash)` 表示显式 flash 本地解析。
## 11. Metric 候选全集
Telemetry 记录业务行为,不记录所有 HTTP 请求。状态查询、健康检查、列表读取和普通配置读取默认不计入 telemetry。
本节列出第一版候选全集。后续实现前可以按 P0 范围裁剪,但裁剪后的实现必须仍能回答产品使用规模、核心链路稳定性和主要性能瓶颈。
`parse.*` 与 `parse_server.*` 的语义边界:
- `parse.*` 统计 doclib 解析业务生命周期,回答用户是否发起解析、缓存是否命中、任务是否完成、处理了多少文件和页面、最终状态如何。
- `parse_server.*` 统计 doclib 解析业务生命周期中调用 parse-server 的执行片段,回答是否实际使用 parse-server、使用 local 还是 remote、使用什么模式和 tier、该执行片段的请求数、处理量、失败率和耗时。
- 一次 parse 请求可以产生 `parse.*`,但不一定产生 `parse_server.*`。例如 cache hit、显式 `flash` 本地解析、复用已有完成批次时,通常不产生 parse-server 执行指标。
- 一次 parse 请求如果实际调用了 local 或 remote parse-server,则同时产生 `parse.*` 和 `parse_server.*`。两者不能互相替代。
- `parse.duration.bucket.count` 统计 doclib parse 业务生命周期耗时;`parse_server.duration.bucket.count` 只统计 doclib 调用 parse-server 的执行片段耗时。
产品使用统计:
| Metric | 说明 |
|--------|------|
| `server.started.count` | doclib server 启动次数 |
| `server.shutdown.count` | doclib server 正常关闭次数 |
| `server.recovered_stale_lock.count` | doclib server 启动恢复 stale lock 的次数 |
| `cli.command.count` | `mineru` CLI 子命令调用次数 |
| `server.command.count` | `mineru server` 生命周期命令调用次数 |
| `ingest.requested.count` | 入库请求次数 |
| `ingest.completed.count` | 入库成功次数 |
| `ingest.duplicate.count` | 入库时发现内容已存在的次数 |
| `parse.requested.count` | parse 请求次数 |
| `parse.completed.count` | parse 完成次数,按 `status` 区分成功、失败、部分成功和取消 |
| `parse.files.count` | parse 处理文件数 |
| `parse.processed_page_count` | parse 处理页数;数值统计,不是页面列表 |
| `parse.output.requested.count` | parse 请求输出格式次数 |
| `parse.output.to_stdout.count` | parse 输出到 STDOUT 的次数 |
| `parse.output.to_file.count` | parse 输出到文件的次数 |
| `parse.marker.disabled.count` | 用户通过 `--no-marker` 禁用 marker 的次数 |
| `parse.wait_mode.count` | parse 同步或异步等待模式使用次数 |
| `parse.page_range.requested.count` | parse 请求页码范围模式使用次数 |
| `parse.force.count` | 用户通过 `--force` 请求重新解析的次数 |
| `parse.created_batch.count` | 本次请求创建的新 parse batch 数 |
| `parse.active_reused.count` | 本次请求复用 pending/parsing batch 的次数 |
| `parse.waited_batch.count` | 本次请求等待的 parse batch 数 |
| `watch.files.discovered.count` | watch 发现文件数 |
| `watch.files.parsed.count` | watch 自动解析文件数 |
| `rules.matched.count` | rule 命中次数 |
| `rules.excluded.count` | exclude rule 排除文件次数 |
| `rules.parse_rule_applied.count` | parsing-rule 被应用次数 |
| `search.performed.count` | search 次数,不采集 query |
| `find.performed.count` | find 文件名搜索次数,不采集 query |
| `doclib.content.requested.count` | 请求读取或渲染文档内容的次数 |
| `doclib.content.rendered.count` | 内容读取时成功渲染输出格式的次数 |
| `doclib.content.truncated.count` | 内容输出因 token/page budget 被截断的次数 |
| `doclib.content.continuation_requested.count` | 用户或 Agent 请求 continuation 的次数 |
| `doclib.docs.added.count` | doclib 新增文档数 |
| `search.index.updated.count` | 搜索索引更新次数 |
| `search.index.source_tier.count` | 搜索索引来源 tier 计数 |
| `parse.cache.hit.count` | parse cache 命中次数 |
| `parse.cache.miss.count` | parse cache 未命中次数 |
| `remote.requested.count` | 用户显式请求 remote 的次数 |
| `remote.allowed.count` | remote 请求被隐私策略允许的次数 |
| `config.watch.added.count` | watch 目录添加次数 |
| `config.watch.removed.count` | watch 目录移除次数 |
| `config.rule.added.count` | exclude 或 parsing rule 添加次数 |
| `config.rule.removed.count` | exclude 或 parsing rule 移除次数 |
| `config.parse_server.changed.count` | parse-server 配置变更次数 |
| `parse_server.configured.count` | doclib server 启动或配置变化时记录当前 parse-server 配置 |
| `parse_server.local.started.count` | managed 本地 parse-server 被 doclib 拉起的次数 |
| `parse_server.local.restarted.count` | managed 本地 parse-server 被重启的次数 |
| `parse_server.requested.count` | doclib 调用 parse-server 的请求次数 |
| `parse_server.completed.count` | doclib 调用 parse-server 成功完成次数 |
| `parse_server.files.count` | parse-server 处理文件数 |
| `parse_server.processed_page_count` | parse-server 处理页数;数值统计,不是页面列表 |
| `parse.compaction.completed.count` | parse 批次 compaction 成功次数 |
| `cleanup.deleted_files.count` | cleanup 删除文件数 |
| `cleanup.orphan_records.count` | cleanup 清理孤儿记录数 |
质量与稳定性:
| Metric | 说明 |
|--------|------|
| `ingest.failed.count` | 入库失败次数 |
| `parse.failed.count` | parse 失败次数 |
| `parse.partial.count` | parse 部分成功次数 |
| `parse.retry.count` | parse 重试次数 |
| `parse.timeout.count` | parse 超时次数 |
| `remote.not_allowed.count` | remote 因隐私策略未显式允许而被拒绝的次数 |
| `remote.fallback_to_local.count` | remote 失败后 fallback 到 local 的次数 |
| `parse_server.unavailable.count` | doclib 调用 parse-server 不可用次数 |
| `parse_server.failed.count` | doclib 调用 parse-server 失败次数 |
| `parse_server.local.start_failed.count` | managed 本地 parse-server 启动失败次数 |
| `parse.fallback.count` | 发生 fallback 的次数 |
| `parse.invalidate.count` | 用户或系统触发 invalidate 的次数 |
| `watch.error.count` | watch 主链路错误次数 |
| `parse.wait.timeout.count` | parse 同步等待超时次数 |
| `search.index.failed.count` | 搜索索引更新失败次数 |
| `doclib.content.render.failed.count` | 内容读取时渲染输出格式失败次数 |
| `parse.compaction.failed.count` | parse 批次 compaction 失败次数 |
| `cleanup.failed.count` | cleanup 失败次数 |
性能与规模分布:
| Metric | 说明 |
|--------|------|
| `ingest.file_size.bucket.count` | 入库文件大小分布 |
| `parse.duration.bucket.count` | parse 耗时分布 |
| `parse.queue.wait.duration.bucket.count` | parse 请求排队等待耗时分布 |
| `parse.page_count.bucket.count` | 单文件页数分布 |
| `parse.file_size.bucket.count` | 文件大小分布 |
| `parse_server.duration.bucket.count` | doclib 调用 parse-server 的耗时分布 |
| `doclib.content.render.duration.bucket.count` | 内容读取时渲染输出格式耗时分布 |
| `search.result_count.bucket.count` | search 返回结果数量分布,不采集 query 或结果内容 |
| `find.result_count.bucket.count` | find 返回结果数量分布,不采集 query 或结果内容 |
| `cleanup.requested.count` | cleanup 用户请求次数 |
Parse-server 相关 metric 必须带以下维度,便于区分用户是否使用 parse-server、使用哪类解析服务、以及对应 tier:
```text
server: local(managed) | local(self-hosted) | remote(official) | remote(custom) | unknown
tier: basic | standard | advanced | basic(default) | standard(default) | advanced(default) | unknown
status: succeeded | failed | partial | canceled
```
Parse-server 配置口径:
- `parse_server.configured.count`: 记录 doclib server 启动或 parse-server 配置变化时的配置状态。
- 本地 disabled 时,使用 `server=none`、`tier=unknown`。
- 本地 managed 时,使用 `server=local(managed)`、`tier=<managed tier>`。
- 本地 self-hosted 时,使用 `server=local(self-hosted)`、`tier` 来自能力发现结果;发现失败时为 `unknown`。
- 官方远端使用 `server=remote(official)`。
- 用户显式配置的其他远端兼容 parse-server 使用 `server=remote(custom)`。
- 不记录 remote URL。
本地 parse-server 的解析数量和性能通过以下查询口径获得:
- 数量: `parse_server.requested.count`、`parse_server.completed.count`、`parse_server.files.count`、`parse_server.processed_page_count`,且 `server=local(managed)` 或 `local(self-hosted)`。
- 性能: `parse_server.duration.bucket.count`,且 `server=local(managed)` 或 `local(self-hosted)`。
- 模式: 由 `server=local(managed)` 或 `local(self-hosted)` 区分。
- tier: `tier=basic`、`standard`、`advanced`、`basic(default)`、`standard(default)` 或 `advanced(default)`。
Telemetry 自身:
| Metric | 说明 |
|--------|------|
| `telemetry.enabled.count` | 用户开启 telemetry 次数 |
| `telemetry.disabled.count` | 用户关闭 telemetry 次数 |
| `telemetry.flush.succeeded.count` | telemetry 上报成功次数 |
| `telemetry.flush.failed.count` | telemetry 上报失败次数 |
## 12. P0 裁剪原则
P0 不一定实现所有候选 metric。裁剪时必须保留能回答以下问题的指标:
1. 使用规模: WAI、DAU、parse 次数、文件数、页数。
2. Agent 入口: `caller=agent` 与 `caller=user` 的使用差异。
3. 调用通道: HTTP API、SDK、CLI、MCP、Web、App、watch 和后台任务的来源分布。
4. 解析服务: 是否使用 parse-server,使用 `local(managed)`、`local(self-hosted)`、`remote(official)` 还是 `remote(custom)`,以及具体解析 tier。
5. 主链路质量: parse、ingest、render、search index 的成功和失败。
6. 关键性能: parse 耗时、parse-server 耗时、queue wait、render 耗时的 bucket。
7. 隐私边界: remote 是否被请求、允许、拒绝,以及 remote 失败后是否 fallback 到 local。
可优先裁剪或延后实现的指标:
- cleanup 和 compaction 维护类指标。
- server shutdown 等生命周期补充指标。
- continuation / truncation 等尚未进入第一版输出能力的指标。
- 过细且暂时没有 dashboard 消费场景的 bucket 指标。
## 13. Bucket 定义
耗时分桶:
```text
lt_1s | 1_5s | 5_30s | 30_120s | 2_10m | gt_10m
```
页数分桶:
```text
1 | 2_5 | 6_20 | 21_100 | 101_500 | gt_500
```
文件大小分桶:
```text
lt_1mb | 1_10mb | 10_50mb | 50_200mb | gt_200mb
```
search 结果数分桶:
```text
0 | 1_5 | 6_20 | 21_100 | gt_100
```
find 结果数分桶:
```text
0 | 1_5 | 6_20 | 21_100 | gt_100
```
## 14. 候选 Metric 口径
### 14.1 Ingest
Ingest 指文件进入 doclib 后计算 SHA256、提取基础 metadata、写入 `files` / `docs` / 文件名索引的阶段。
推荐维度:
```text
source: http_api | sdk | cli | mcp | web | app | watch | background | unknown
caller: agent | user | http_client | sdk | web | app | system | unknown
input_type
status
error_code
```
`ingest.duplicate.count` 表示同一 `sha256` 已存在,只新增或更新文件路径实例,不代表失败。
### 14.2 Search Index
Search index 指文件名索引和内容全文索引更新,不记录索引文本、snippet 或命中文档。
推荐维度:
```text
source: http_api | sdk | cli | mcp | web | app | watch | background | unknown
caller: agent | user | http_client | sdk | web | app | system | unknown
tier
status
error_code
```
`search.index.source_tier.count` 用于统计当前索引文本来自哪个 tier。搜索结果可以来自 `flash`,但用户主动阅读时不能默认停留在 `flash`。
### 14.3 Render / Content
Render 指 `GET /docs/{sha256}/content` 或等价 SDK/CLI 读取动作中,从 Middle JSON 转换为 Markdown、structured_content、HTML 等派生格式。
推荐维度:
```text
source
caller
output_format
tier
status
error_code
bucket
```
`doclib.content.requested.count` 记录内容读取请求。`doclib.content.rendered.count` 记录实际完成渲染。请求 `middle_json` 时可以只记录 requested,不一定记录 rendered。
### 14.4 Queue / Batch
Queue 和 batch 指 doclib parse 请求在 DB 任务队列中的创建、复用和等待行为。
推荐维度:
```text
source
caller
tier
status
```
`parse.created_batch.count` 表示请求创建了新的 parse batch。`parse.active_reused.count` 表示请求复用了已经存在的 pending/parsing batch。`parse.waited_batch.count` 表示当前请求需要等待的 batch 数;一次请求可以等待多个 batch。
`parse.queue.wait.duration.bucket.count` 只统计从请求进入等待到相关 batch 完成或失败的等待耗时,不包含实际 parse-server 执行耗时。
### 14.5 Remote Boundary
Remote metrics 只记录用户是否显式允许远端和是否发生 local fallback,不记录 remote URL、API Key 或账号信息。
推荐维度:
```text
source
caller
tier
status
error_code
```
`remote.requested.count` 表示当前请求显式带有 remote 语义。`remote.allowed.count` 表示隐私策略允许该请求使用 remote。`remote.not_allowed.count` 表示系统知道 remote 可能解决问题,但用户没有显式允许。`remote.fallback_to_local.count` 表示 remote 失败后按隐私边界 fallback 到 local。
### 14.6 Rules
Rules metrics 只记录规则是否命中,不记录 rule name、pattern、路径或文件名。
推荐维度:
```text
rule_type: exclude | parsing | unknown
tier
remote: true | false | unknown
```
`rules.excluded.count` 表示 exclude rule 阻止文件进入后续链路。`rules.parse_rule_applied.count` 表示 parsing-rule 决定了 tier、page_range 或 remote 语义。
### 14.7 Server Lifecycle
Server lifecycle metrics 记录 doclib server 和 managed local parse-server 的生命周期。
推荐维度:
```text
status
error_code
server
tier
```
`server.recovered_stale_lock.count` 用于统计启动恢复能力。`parse_server.local.start_failed.count` 和 `parse_server.local.restarted.count` 用于观察 managed local parse-server 稳定性。
### 14.8 Compaction / Cleanup
Compaction 和 cleanup 属于维护链路,用于观察 JSON 批次增长、孤儿记录和文件清理效果。
推荐维度:
```text
tier
status
error_code
```
这些指标不记录 sha256、路径或文件名。
### 14.9 CLI 行为
CLI 行为 metrics 只记录用户或 Agent 使用了哪些 `mineru` 子命令和关键模式,不记录命令参数中的路径、query、pattern、URL、API Key 或输出内容。
推荐维度:
```text
source: cli
caller
command
status
```
`cli.command.count` 的 `command` 取值:
```text
parse | invalidate | search | find | show | cleanup | server | config | telemetry | unknown
```
`server.command.count` 的 `command` 取值:
```text
start | stop | restart | status | unknown
```
`GET /server/status` 不计入 `server.command.count`,避免健康检查和轮询污染。只有用户或 Agent 通过 CLI 主动执行 `mineru server status` 时才记录。
Parse CLI 行为推荐维度:
```text
source: cli
caller
tier
server
output_format
wait_mode
wait_bucket
page_range_mode
status
```
口径:
- `parse.output.requested.count`: 记录 `--format` 请求的输出格式。
- `parse.output.to_stdout.count`: 输出到 STDOUT。
- `parse.output.to_file.count`: 指定 `--output` 写入文件;不记录输出路径。
- `parse.marker.disabled.count`: 用户传入 `--no-marker`。
- `parse.wait_mode.count`: `--no-wait` 记为 `wait_mode=async`,否则记为 `sync`。
- `parse.wait.timeout.count`: 同步等待超时。
- `parse.page_range.requested.count`: 未传页码为 `default`,显式 `all` 为 `all`,其他页码范围为 `partial`;不记录具体页码字符串。
- `parse.force.count`: 用户传入 `--force`。
配置类 CLI 行为推荐维度:
```text
source: cli
caller
config_area
server
tier
remote
status
```
口径:
- `config.watch.added.count` / `config.watch.removed.count`: 不记录 watch path、label 或 removable 设备标识。
- `config.rule.added.count` / `config.rule.removed.count`: 不记录 rule name、pattern 或 page_range。
- `config.parse_server.changed.count`: 不记录 URL 或 API Key,只通过 `server` 区分 `local(managed)`、`local(self-hosted)`、`remote(official)`、`remote(custom)`、`none`、`unknown`。
Cleanup CLI 行为推荐维度:
```text
source: cli
caller
cleanup_target
dry_run
status
```
`cleanup_target` 取值:
```text
orphan_docs | deleted_files | temp | mixed | unknown
```
## 15. Doclib API 感知范围
Telemetry 不记录所有 doclib HTTP API 调用,只记录能够代表产品使用规模或核心行为的 API。
对用户行为类 API,调用方应尽量向 doclib 传递 `caller`。这使 telemetry 可以区分 Agent 调用 `mineru` 命令和用户直接调用 `mineru` 命令。不能可靠识别时使用 `unknown`,不得通过敏感内容推断。
直接调用 HTTP API 且未传 caller 信息时,`source=http_api`、`caller=http_client`。SDK、CLI、MCP、Web 和桌面 App 应由对应入口向 doclib 传递 `source` 与 `caller`。
应记录的 API:
| API | Metric | 说明 |
|-----|--------|------|
| `POST /parses` | `parse.requested.count` 等 parse 指标;使用 parse-server 时记录 `parse_server.*` 指标 | 记录请求、cache hit/miss、完成/失败、文件数、页数、tier、耗时、大小分布和 parse-server 使用情况 |
| `POST /invalidate` | `parse.invalidate.count` | 记录 invalidate 行为,不记录路径或 sha256 |
| `GET /search` | `search.performed.count`、`search.result_count.bucket.count` | 不记录 query、命中文档或返回内容 |
| `GET /find` | `find.performed.count`、`find.result_count.bucket.count` | 与 search 分开统计,不记录 query、命中文档或返回内容 |
| `GET /docs/{sha256}/content` | `doclib.content.requested.count` | 记录内容读取或渲染次数,不记录 sha256 或内容 |
不应记录为 telemetry 的 API:
| API | 原因 |
|-----|------|
| `GET /parses` | 状态查询和轮询噪音较大 |
| `GET /parses/{id}` | 状态查询,不代表核心产品使用量 |
| `GET /docs` | 列表浏览噪音较大 |
| `GET /docs/{sha256}` | 元数据读取,P0 不作为核心指标 |
| `GET /files/by-path` | 工具性查询 |
| `GET /config` / `POST /config` | 普通配置读取和修改不计入,telemetry 开关变化除外 |
| watch / rules 配置 API | 只记录后台 watch 实际发现和解析行为 |
| cleanup API | 管理维护行为 |
| `GET /server/status` | 健康检查和轮询会污染活跃度 |
| `POST /shutdown` | 管理行为 |
## 16. 本地存储
Telemetry 用户状态、内部状态和聚合数据都存储在 doclib DB 中,不使用启动前文件配置。
Telemetry endpoint 不支持用户配置,应由代码内置。开发和测试环境如需覆盖 endpoint,应使用测试构建或专门的开发开关,不进入用户配置体系。
建议状态结构可以继续用 KV 表表达:
```toml
[telemetry]
consent_state = "enabled"
installation_id = "inst_01HX..."
last_flush_at = "2026-06-10T12:00:00Z"
```
状态语义:
- `consent_state`: 用户 telemetry 状态,取值为 `unset` / `enabled` / `disabled`。
- `installation_id`: 内部匿名安装实例 ID,用户不可配置,也不需要默认展示完整值。
- `last_flush_at`: 内部运行状态,表示最近一次成功 flush 时间,不是用户配置。
`consent_state` 语义:
| 值 | 含义 |
|----|------|
| `unset` | 用户尚未完成首次选择,doclib server 应展示或触发首次选择流程 |
| `enabled` | 用户允许记录并上报 telemetry |
| `disabled` | 用户明确关闭 telemetry |
如果 `consent_state=disabled`,doclib server 不应写入新的 telemetry 聚合数据。
Telemetry 聚合数据应使用独立表保存,至少包含:
| 字段 | 说明 |
|------|------|
| `id` | 本地聚合记录 ID |
| `period_start` | 聚合窗口开始时间 |
| `period_end` | 聚合窗口结束时间 |
| `metric_name` | metric 名称 |
| `metric_value` | 非负数值 |
| `dimensions` | metric-level dimensions JSON |
| `dimensions_hash` | dimensions JSON 的稳定 hash,用于聚合 upsert |
| `created_at` | 创建时间 |
| `updated_at` | 最近更新时间 |
聚合唯一键:
```text
period_start + period_end + metric_name + dimensions_hash
```
同一唯一键重复写入时,累加 `metric_value`。聚合 key 不包含 context;flush 时读取当前 context 并组装 telemetry batch。服务端确认成功接收后,删除对应聚合记录。网络失败或 5xx 时保留记录以便后续重试。
Flush 并发与幂等采用 P0 简单方案:
1. doclib 同一时间只允许一个 flush 任务执行。
2. flush 开始时获取本地 flush lock;已有 lock 且未过期时,本次 flush 直接跳过。
3. flush 从 DB 读取一批未上报聚合记录,生成 `batch_id` 并 POST 到 telemetry endpoint。
4. 服务端确认成功接收后,本地删除这些聚合记录。
5. 如果 POST 成功但本地删除前 doclib 崩溃,后续可能重复上报同一批 metrics。
6. P0 接受少量重复。服务端应基于 `batch_id` 在短窗口内做幂等去重;超过窗口的重复数据由 cleaned metrics 层处理。
Flush lock 可以放在 telemetry 内部状态中,例如:
```toml
[telemetry]
flush_locked_at = "2026-06-10T12:00:00Z"
```
## 17. 服务端防污染
匿名 telemetry endpoint 无法完全避免伪造数据。服务端必须把本地 telemetry 视为半可信数据,只用于趋势分析。
服务端必须实现:
- schema version 校验
- metric name 白名单
- context key/value 白名单
- dimension key/value 白名单
- `value` 非负数校验
- batch 大小限制
- period 时间范围限制
- `installation_id` 限流
- IP 限流
- 异常数据 quarantine
- raw metrics 与 cleaned metrics 分层
Dashboard 默认展示 cleaned metrics。mineru.net API 服务端日志属于 trusted usage,本地 anonymous telemetry 属于 anonymous product analytics,两类数据必须分开标记和分析。
## 18. P0 实现边界
P0 需要完成:
- doclib server telemetry DB 配置读取与首次选择状态管理。
- 匿名 `installation_id` 生成并持久化到 doclib DB。
- telemetry metrics 聚合存储到 doclib DB。
- 小时级 period 聚合与 metric upsert。
- flush 成功后删除 DB 中已确认上报的 telemetry 聚合数据。
- `mineru` CLI、Doclib SDK、MCP、Web、桌面 App 和直接 HTTP API 到 doclib server 的 `source` / `caller` 透传。
- P0 metric 白名单与 dimension 白名单。
- 周期 flush 与失败重试。
- `mineru telemetry status|enable|disable|preview|flush`。
- 服务端 endpoint schema。
P0 不需要完成:
- 每事件实时上报。
- 详细硬件画像。
- 账号级归因。
- GPU 型号、显存、驱动和 CUDA 版本采集。
- exception message / traceback 上报。
- telemetry 结果查询 API。