feat: 接入企微智能机器人——异常报警/数据核验日报/设备健康日报
- flows/shared/wecom.py: WSClient 按需连接(连接→认证→发送→断开),全容错,超长分段 - flows/monitor/alert_failure.py: Prefect 事件触发(Failed/Crashed→run deployment),发群报警 - flows/monitor/daily_report.py: 每日 18:30 近7天 RIIN/SDS 核验(复用 verify)+商品完整性+OCR+失败回顾 - flows/monitor/device_check.py: 每日 09:00 内存/磁盘/GPU(80%阈值)+Docker 容器异常+服务探活 - db.py: 新增 get_goods_integrity/ping; compose worker 挂载 docker.sock+/:/host:ro+GPU - scripts/wecom_capture_chatid.py: 一次性捕获群 chat_id 工具
This commit is contained in:
@@ -0,0 +1,129 @@
|
||||
"""企微机器人异常报警:任意 flow run 失败/崩溃时由 Prefect 事件触发器自动调用。
|
||||
|
||||
对应 wecom-alert deployment(prefect.yaml 里的 triggers 声明):
|
||||
- match: prefect.resource.id = prefect.flow-run.*
|
||||
- expect: prefect.flow-run.Failed / Crashed
|
||||
- parameters.event = "{{ event }}"
|
||||
|
||||
flow 体全容错——本 flow 自身永不进入 Failed 状态,避免自我触发循环。
|
||||
event 可能以 dict 或 str(jinja 渲染结果)传入,均做防御性解析;
|
||||
解析出 flow run id 后再查 Prefect API 补全权威信息(名称/错误消息/时间)。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
import httpx
|
||||
from prefect import flow
|
||||
|
||||
from flows.shared import send_markdown
|
||||
|
||||
UI_BASE = os.environ.get("PREFECT_UI_URL", "http://192.168.124.92:4200")
|
||||
|
||||
|
||||
def _parse_event(event: Any) -> dict:
|
||||
if isinstance(event, dict):
|
||||
return event
|
||||
if isinstance(event, str) and event.strip():
|
||||
# jinja "{{ event }}" 可能渲染成 JSON 串或 Python repr,统一抠除关键字段
|
||||
rid = re.search(r"prefect\.flow-run\.[0-9a-fA-F-]{36}", event)
|
||||
ev = re.search(r"prefect\.flow-run\.(?:Failed|Crashed)", event)
|
||||
name = re.search(r"prefect\.resource\.name['\"]?\s*[:=]\s*['\"]([^'\"]+)", event)
|
||||
occurred = re.search(r"occurred['\"]?\s*[:=]\s*['\"]([^'\"]+)", event)
|
||||
return {
|
||||
"event": ev.group(0) if ev else "",
|
||||
"resource": {
|
||||
"prefect.resource.id": rid.group(0) if rid else "",
|
||||
"prefect.resource.name": name.group(1) if name else "",
|
||||
},
|
||||
"occurred": occurred.group(1) if occurred else "",
|
||||
}
|
||||
return {}
|
||||
|
||||
|
||||
def _flow_run_id(event: dict) -> str | None:
|
||||
rid = (event.get("resource") or {}).get("prefect.resource.id", "")
|
||||
m = re.match(r"^prefect\.flow-run\.(.+)$", rid)
|
||||
return m.group(1) if m else None
|
||||
|
||||
|
||||
def _fetch_run_detail(run_id: str) -> dict:
|
||||
"""查 Prefect API 补全 flow run 权威信息,失败返回空 dict。"""
|
||||
api_url = os.environ.get("PREFECT_API_URL", "http://localhost:4200/api")
|
||||
try:
|
||||
resp = httpx.get(f"{api_url}/flow_runs/{run_id}", timeout=10)
|
||||
resp.raise_for_status()
|
||||
return resp.json()
|
||||
except Exception as err: # noqa: BLE001
|
||||
print(f"[wecom-alert] 查询 flow run {run_id} 失败: {err}")
|
||||
return {}
|
||||
|
||||
|
||||
def _fmt_ts(value: Any) -> str:
|
||||
if not value:
|
||||
return "—"
|
||||
try:
|
||||
return datetime.fromisoformat(value.replace("Z", "+00:00")).astimezone().strftime("%m-%d %H:%M")
|
||||
except (ValueError, TypeError):
|
||||
return str(value)[:19]
|
||||
|
||||
|
||||
@flow(name="wecom-alert", log_prints=True)
|
||||
def wecom_alert_failure(event: dict | str | None = None) -> dict:
|
||||
"""Prefect 事件触发:flow run 失败/崩溃 → 发企微群报警。永不抛异常。"""
|
||||
result = {"sent": False, "flowName": None, "runId": None}
|
||||
try:
|
||||
ev = _parse_event(event)
|
||||
event_name = ev.get("event", "prefect.flow-run.?")
|
||||
resource_name = (ev.get("resource") or {}).get("prefect.resource.name", "")
|
||||
run_id = _flow_run_id(ev)
|
||||
|
||||
flow_name, run_name, state_msg, occurred = "", resource_name, "", ev.get("occurred", "")
|
||||
if run_id:
|
||||
detail = _fetch_run_detail(run_id)
|
||||
flow_name = detail.get("name", "") or resource_name
|
||||
run_name = detail.get("name", "")
|
||||
state = detail.get("state") or {}
|
||||
state_msg = (state.get("message") or "").strip()
|
||||
ended = detail.get("end_time") or detail.get("expected_start_time")
|
||||
occurred = occurred or ended
|
||||
|
||||
lines = [
|
||||
"🔴 **Prefect 流程失败报警**",
|
||||
f"- 事件: `{event_name}`",
|
||||
f"- Flow: {flow_name or resource_name or '未知'}",
|
||||
]
|
||||
if run_name and run_name != flow_name:
|
||||
lines.append(f"- Run: {run_name}")
|
||||
if state_msg:
|
||||
lines.append(f"- 错误: {state_msg[:200].replace(chr(10), ' ')}")
|
||||
lines.append(f"- 时间: {_fmt_ts(occurred)}")
|
||||
if run_id:
|
||||
lines.append(f"- 详情: [Prefect UI]({UI_BASE}/runs/flow-run/{run_id})")
|
||||
else:
|
||||
raw = json.dumps(ev, ensure_ascii=False)[:300] if ev else "(事件内容解析失败)"
|
||||
lines.append(f"- 原始事件: {raw}")
|
||||
|
||||
result["sent"] = send_markdown("\n".join(lines))
|
||||
result.update({"flowName": flow_name or None, "runId": run_id})
|
||||
print(f"[wecom-alert] 已发送失败报警: {flow_name or resource_name} ({event_name})")
|
||||
except Exception as err: # noqa: BLE001
|
||||
print(f"[wecom-alert] 报警流程异常(已吞掉,避免自我触发): {err}")
|
||||
return result
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# 本地手测:模拟一条 Failed 事件
|
||||
wecom_alert_failure({
|
||||
"event": "prefect.flow-run.Failed",
|
||||
"resource": {
|
||||
"prefect.resource.id": "prefect.flow-run.00000000-0000-0000-0000-000000000000",
|
||||
"prefect.resource.name": "riin-order-sync/manual",
|
||||
},
|
||||
"occurred": datetime.now().astimezone().isoformat(),
|
||||
})
|
||||
Reference in New Issue
Block a user