43376abae6
Co-authored-by: guke <guke@autohome.com.cn> Reviewed-on: #221
128 lines
4.9 KiB
Python
128 lines
4.9 KiB
Python
"""比价失败报警规则:一条记录 → 命中的 AlertHit(或 None)。
|
|
|
|
纯函数、不碰 DB(阈值/关键词由调用方从 config 传入),便于单测与调阈值。判定顺序保证四类互斥:
|
|
failed → fail_reason 空(且 information 非业务)=T1 / 含未识别=T6 / 含超时词=T2 / 其余业务不报;
|
|
cancelled 且耗时或步数超阈值=T5(深度放弃);success/running 不报。
|
|
口径依据见 docs/superpowers/specs/2026-08-04-compare-fail-alert-design.md 第 3 节。
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
from datetime import datetime
|
|
from typing import Any
|
|
|
|
ALERT_TYPE_LABELS: dict[str, str] = {
|
|
"T1": "系统技术失败",
|
|
"T6": "商品识别失败",
|
|
"T2": "启动/超时失败",
|
|
"T5": "深度放弃(cancelled)",
|
|
}
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class AlertHit:
|
|
trace_id: str
|
|
alert_type: str
|
|
reason: str
|
|
app_version: str | None
|
|
created_at: datetime | None
|
|
trace_url: str | None
|
|
user_id: int | None
|
|
total_ms: int | None = None
|
|
step_count: int | None = None
|
|
stuck_point: str | None = None
|
|
|
|
|
|
def make_hit(rec: Any, alert_type: str, reason: str) -> AlertHit:
|
|
return AlertHit(
|
|
trace_id=rec.trace_id,
|
|
alert_type=alert_type,
|
|
reason=reason,
|
|
app_version=getattr(rec, "app_version", None),
|
|
created_at=getattr(rec, "created_at", None),
|
|
trace_url=getattr(rec, "trace_url", None),
|
|
user_id=getattr(rec, "user_id", None),
|
|
total_ms=getattr(rec, "total_ms", None),
|
|
step_count=getattr(rec, "step_count", None),
|
|
)
|
|
|
|
|
|
def classify_cancelled_fallback(
|
|
rec: Any,
|
|
*,
|
|
cancelled_ms_threshold: int,
|
|
cancelled_step_threshold: int,
|
|
) -> AlertHit | None:
|
|
"""cancelled 保底判定(读不到 trace 时用):超耗时或步数阈值 → T5 深度放弃,否则 None。纯函数。"""
|
|
ms = rec.total_ms
|
|
step = rec.step_count
|
|
deep = (ms is not None and ms > cancelled_ms_threshold) or (
|
|
step is not None and step > cancelled_step_threshold
|
|
)
|
|
if deep:
|
|
return make_hit(rec, "T5", "深度放弃")
|
|
return None
|
|
|
|
|
|
def _target_technical_failure_reason(
|
|
rec: Any, biz_exclude_keywords: tuple[str, ...]
|
|
) -> str | None:
|
|
"""某目标平台「真技术失败」(pricebot 原始 status=='failed' 且 reason 非业务话术)的原因;无 → None。
|
|
|
|
记录级 fail_reason 是「展示口径」——一条比价里若某平台是干净业务结局(京东未找到菜),它会被派生
|
|
成 headline,盖住另一平台的真技术崩溃(淘宝'比价过程出错')。这里扫 raw_payload.platform_results
|
|
补判:任一目标平台 status='failed' 且 reason 不含业务词(pricebot 偶把打烊/不配送漏标成 failed,
|
|
用 biz_exclude 过滤掉这些业务误标)→ 真技术崩溃。缺 raw_payload/platform_results / 结构异常 → None。
|
|
"""
|
|
raw = getattr(rec, "raw_payload", None)
|
|
pr = raw.get("platform_results") if isinstance(raw, dict) else None
|
|
if not isinstance(pr, dict):
|
|
return None
|
|
for v in pr.values():
|
|
if not isinstance(v, dict) or v.get("is_source"):
|
|
continue
|
|
if v.get("status") != "failed":
|
|
continue
|
|
reason = (v.get("reason") or "").strip()
|
|
if not any(w in reason for w in biz_exclude_keywords):
|
|
return reason or "比价过程出错"
|
|
return None
|
|
|
|
|
|
def classify_record(
|
|
rec: Any,
|
|
*,
|
|
cancelled_ms_threshold: int,
|
|
cancelled_step_threshold: int,
|
|
timeout_keywords: tuple[str, ...],
|
|
unrecognized_keywords: tuple[str, ...],
|
|
biz_exclude_keywords: tuple[str, ...],
|
|
) -> AlertHit | None:
|
|
"""判定单条记录是否触发报警。rec 需有 status/fail_reason/information/total_ms/step_count/
|
|
trace_id/app_version 属性(ComparisonRecord 或等价对象)。"""
|
|
status = rec.status
|
|
if status == "failed":
|
|
fail_reason = rec.fail_reason
|
|
if fail_reason is None:
|
|
info = (rec.information or "").strip()
|
|
if info and any(w in info for w in biz_exclude_keywords):
|
|
return None
|
|
return make_hit(rec, "T1", f"技术失败·{info[:80] or '比价过程出错'}")
|
|
if any(w in fail_reason for w in unrecognized_keywords):
|
|
return make_hit(rec, "T6", f"识别失败·{fail_reason[:80]}")
|
|
if any(w in fail_reason for w in timeout_keywords):
|
|
return make_hit(rec, "T2", fail_reason[:80])
|
|
# fail_reason 是干净业务 headline,但可能盖住某目标平台的真技术失败(比价过程出错)→ 补判 T1。
|
|
tech = _target_technical_failure_reason(rec, biz_exclude_keywords)
|
|
if tech:
|
|
return make_hit(rec, "T1", f"技术失败·{tech[:80]}")
|
|
return None # 纯业务失败,不报
|
|
if status == "cancelled":
|
|
return classify_cancelled_fallback(
|
|
rec,
|
|
cancelled_ms_threshold=cancelled_ms_threshold,
|
|
cancelled_step_threshold=cancelled_step_threshold,
|
|
)
|
|
return None
|
|
|