feat(observability): 抓取去掉首页预热,交易补齐链路埋点
两个问题一起处理,都与「出站请求与可观测性」有关。 ## 抓取:正常路径不再多打一次首页 site_session 原先每条通道每 30 分钟打一次 www.rakuten.co.jp/ 做预热,而且预热 返回非 2xx 时 warmed_at 不置位——那种情况下每个请求前都会再打一次首页。 Akamai 的 cookie 随任意页面响应下发,目标页自己就会带回来,专门先打一次首页除了 多一个出站请求(以及多一次被风控计数的机会)之外没有额外收益:首个请求无论打哪个 URL 都是冷的 ~11s,之后都复用 cookie。 改为 cookie 由目标页响应建立(_note_cookies)、超 TTL 主动清空 (_drop_expired_cookies)。首页只保留在失败修复路径上(_rewarm_on_home):目标页 已经吃了挑战页时,拿首页换一套干净 cookie 比继续撞同一个 URL 更安全。happy path 的出站请求数 2 → 1。 _note_cookies 刻意不在每次响应时刷新时刻:TTL 要从「这套 cookie 第一次出现」算起, 每次都刷新会让一套 cookie 被无限续命,反而绕过了 session_ttl_seconds 的本意。 profile_status() 的 warmed 字段名保留(上游健康检查看板在用),语义改为「当前有 可复用的 Akamai cookie」,不再代表「已专门预热过首页」。 ## 交易:此前没有任何有意义的链路数据 根因是 trading 的实际工作两类自动埋点都覆盖不到:站点交互走 Playwright(不经 httpx),worker 主循环是后台 asyncio 任务(没有 HTTP 入口,因此没有根 span)。 于是发给网关的每次 httpx 调用各自成为孤立 trace——观测后台上只剩一堆请求记录。 新增手工埋点: - order.task:一笔下单的根 span,一个 task_id 一条 trace,带 order.route (execute / recovery / already_finished)与终态 order.terminal_status - order.step.*:清车 → 加购 → 校验 → 确认 → 提交 → 付款,每步一个子 span, 带 order.evidence_ref,可从 span 直接定位落盘证据 - site.*:12 个 Playwright 交互方法(用 traced 装饰器而非 with 块——这些方法的 函数体本就很长,再加一层缩进不利于阅读) - account_query:只读查询单的根 span,带 query.outcome 空转的长轮询(30 秒一次、绝大多数返回空)用 suppressed() 屏蔽:量大且没有信息量, 把观测后台刷满的正是它们。领到任务后的网关调用都在任务根 span 底下,不受影响。 闸门 / 风控拦截会被 _execute_with_renewal 吞掉转 needs_human,异常冒不到根 span, 被拦下的单在 trace 里跟成功下单一模一样。加 _execute_recording_errors 一层统一 记录,比每个 except 分支各写一遍省事,也不会漏掉后续新增的分支。 _report_safe 写 span 属性前判断 is_recording():付款后监控是 create_task 起的, asyncio 在创建时就把 context 复制了进去,等它真正跑起来根 span 早已结束—— get_current_span() 拿到的仍是那个已结束的 span(不是 INVALID_SPAN),写属性会打 "Setting attribute on ended span"。当前监控路径不传 terminal_status 走不到那里, 这道判断是防以后。 ## 顺带修掉:instrument_app 从未生效 instrument_app 用 _provider is None 做前置判断,但三个服务都在模块导入时执行 app = create_app(),而 setup_telemetry 要等 lifespan 才跑——那时 _provider 还是 None,照着判断直接 return。**FastAPI 从来没被打桩过,三个服务一条 server span 都没有。** 实测确认两件事:导入期打桩能出 span,lifespan 内打桩出不来(instrument_app 是加 中间件,应用开始服务后加进去不生效);provider 后设也不影响 ProxyTracer 委托到 真实 provider。所以只能在导入期装,判断条件改为 otel_enabled。 app/gateway/main.py 此前完全没接 telemetry,worker 出站请求带过来的 traceparent 没人接上,一条下单链路在网关这里断掉,只看得到 worker 侧那半截。补上 setup_telemetry(service_name="rakuten-gateway") 与 instrument_app / shutdown。 ## 验证 新增 8 个用例:首页零请求、cookie 复用与过期清空、失败后用首页换 cookie、一任务 一 trace 的父子结构、闸门失败标 ERROR、空转不埋点,以及 instrument_app 调用顺序 的回归测试。全量 526 passed。 Playwright 那些 site.* 埋点只做了静态验证(测试用桩替换站点方法),没有跑真实 浏览器下单确认 span 真的落地。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -93,8 +93,8 @@ PC UA 在搜索页、详情页、店铺页上都能拿到完整模板。因此
|
||||
|
||||
- FastAPI 提供 HTTP API
|
||||
- Bearer Token 接口鉴权(`Authorization: Bearer <token>`)
|
||||
- Akamai cookie 自动预热与复用,过期自动重新预热(仅乐天链路)
|
||||
- 失败逐级升级:重新预热 → Playwright 兜底取 cookie → 结构化报错
|
||||
- Akamai cookie 随目标页响应自动建立与复用,过期自动清空重建(仅乐天链路;正常路径不额外访问首页)
|
||||
- 失败逐级升级:首页换一套 cookie → Playwright 兜底取 cookie → 结构化报错
|
||||
- 搜索结果自动剔除混入的 CPC 广告位(乐天)
|
||||
- 识别站点深翻页「静默回绕到第 1 页」的行为,避免上游重复入库(乐天)
|
||||
- 商品详情按落地域名分派解析,覆盖楽天ブックス / Rakuten Fashion / ビックカメラ 等官方旗舰子站
|
||||
@@ -660,6 +660,30 @@ trading 加购时的字段选择策略:多规格挑第一个非售罄的 varia
|
||||
(账号只读查询通道)**可以重试**——查询只读,重发没有副作用
|
||||
(见 [docs/order-gateway.md §11](docs/order-gateway.md#11-账号只读查询通道))。
|
||||
|
||||
## 链路追踪
|
||||
|
||||
可选,默认关闭。`RAKUTEN_OTEL_ENABLED=true` + `RAKUTEN_OTEL_ENDPOINT` 指向 OTLP/HTTP
|
||||
端点后,三个服务分别以 `rakuten-scraping` / `rakuten-trading` / `rakuten-gateway` 上报。
|
||||
|
||||
FastAPI 与 httpx 走自动埋点,但那只覆盖「收到 HTTP 请求」与「发出 httpx 请求」两类
|
||||
边界。交易侧的实际工作两者都不是——站点交互走 Playwright,worker 主循环是后台任务,
|
||||
所以这一侧手工埋点:
|
||||
|
||||
- `order.task`:一笔下单任务的**根 span**(一个 `task_id` 一条 trace),带
|
||||
`order.route`(`execute` / `recovery` / `already_finished`)与终态
|
||||
`order.terminal_status`;被闸门或风控拦下时置 ERROR 并带 `error.code`
|
||||
- `order.step.*`:清车 → 加购 → 校验 → 确认页 → 提交 → 付款,每步一个子 span,
|
||||
带 `order.evidence_ref`(可直接定位落盘证据)
|
||||
- `site.*`:Playwright 站点交互(`site.add_to_cart`、`site.enter_checkout`、
|
||||
`site.submit_order`、`site.pay` 等)
|
||||
- `account_query`:一张只读查询单的根 span,带 `query.outcome`
|
||||
|
||||
worker 空转的长轮询(每 30 秒问一次网关有没有活干)刻意不埋点——它们没有信息量,
|
||||
量却极大,会把观测后台刷满。
|
||||
|
||||
> 解析失败时页面 HTML 会作为 span event 上报(`RAKUTEN_OTEL_SNAPSHOT_MAX_BYTES`
|
||||
> 控制上限,默认 2MB),用于事后复现「抓到的内容为什么解析不出预期字段」。
|
||||
|
||||
## 常用环境变量
|
||||
|
||||
完整列表见 [.env.example](.env.example)。配置项前缀统一为 `RAKUTEN_`,三个服务共用同一份
|
||||
@@ -675,6 +699,7 @@ trading 加购时的字段选择策略:多规格挑第一个非售罄的 varia
|
||||
- 抓取:`RAKUTEN_MAX_SITE_CONCURRENCY`(默认 `8`,两站各自独立计数)、`RAKUTEN_HTTP_MAX_ATTEMPTS`(默认 `3`)、`RAKUTEN_SESSION_TTL_SECONDS`(默认 `1800`,仅乐天)
|
||||
- 浏览器兜底(仅乐天):`RAKUTEN_BROWSER_FALLBACK_ENABLED`、`RAKUTEN_BROWSER_HEADLESS`、`RAKUTEN_BROWSER_CHANNEL`
|
||||
- 代理(需日本 IP 时):`RAKUTEN_PROXY_SERVER`、`RAKUTEN_PROXY_USERNAME`、`RAKUTEN_PROXY_PASSWORD`、`RAKUTEN_PROXY_BYPASS`
|
||||
- 链路追踪(可选,默认关闭;三个服务共用):`RAKUTEN_OTEL_ENABLED`、`RAKUTEN_OTEL_ENDPOINT`、`RAKUTEN_OTEL_HEADERS`、`RAKUTEN_OTEL_SNAPSHOT_MAX_BYTES`
|
||||
|
||||
> 从中国大陆直连实测可用、无需代理;`RAKUTEN_PROXY_SERVER` 留空即可。设置后,所有面向
|
||||
> 外部站点的 HTTPX 与 Playwright 流量都会经代理;本机和 Docker 服务间地址由
|
||||
|
||||
Reference in New Issue
Block a user