#!/usr/bin/env python3 # -*- coding: utf-8 -*- """阈值判定变更回归(project-20260911-threshold-fix ⑥) 对比三条链路的判定结果,产出「改前改后差异样本」: A. 旧实现:alert_generator 顺序 绿→黄→红 + 正则 (>=|<=|>|<|=|!=)\\s*([\\d.]+)(不支持负数/区间) B. 新实现 red→green→yellow(统一 threshold_parser) C. 若按任务字面「red→yellow→green」严重度顺序(用于说明为何 green 必须先于 yellow) 输出:逐 (KPI, 期间最新值) 的 A/B 差异清单 + 预警集合差异 + 真实 alert_generator 事务内预演(不落库) """ import os import re import sys import json from collections import Counter BASE = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) sys.path.insert(0, BASE) envf = os.path.join(BASE, ".env") for line in open(envf, encoding="utf-8"): line = line.strip() if line and not line.startswith("#") and "=" in line: k, v = line.split("=", 1) os.environ.setdefault(k.strip(), v.strip().strip('"').strip("'")) from app.database import get_session_local from app.models import KPIDefinition, KPIValue from app.utils.threshold_parser import calc_level_from_exprs OLD_RE = re.compile(r"(>=|<=|>|<|=|!=)\s*([\d.]+)") def old_level(actual, kpi) -> str: """旧实现:绿→黄→红,命中即止;不支持负数/区间。""" if actual is None: return "info" for level, field in (("green", "threshold_green"), ("yellow", "threshold_yellow"), ("red", "threshold_red")): expr = getattr(kpi, field, None) if not expr: continue m = OLD_RE.match(str(expr).strip()) if not m: continue op, val = m.group(1), float(m.group(2)) hit = {">=": actual >= val, "<=": actual <= val, ">": actual > val, "<": actual < val, "=": actual == val, "!=": actual != val}[op] if hit: return level return "info" def severity_order_level(actual, kpi) -> str: """按任务字面顺序 red→yellow→green(严重度优先)——用于对比说明。""" for level, field in (("red", "threshold_red"), ("yellow", "threshold_yellow"), ("green", "threshold_green")): expr = getattr(kpi, field, None) if not expr: continue from app.utils.threshold_parser import eval_threshold if eval_threshold(actual, expr): return level return "info" def main(): db = get_session_local()() kpis = db.query(KPIDefinition).filter(KPIDefinition.status == "active").all() samples = [] diffs_ab = [] diffs_bc = [] count_old = Counter() count_new = Counter() for kpi in kpis: vals = db.query(KPIValue).filter(KPIValue.kpi_id == kpi.id).all() # 每期间取最新一条(对齐 alert_generator 取数方式) latest_by_period = {} for v in vals: if v.actual_value is None: continue key = v.period cur = latest_by_period.get(key) if cur is None or (v.calculated_at and cur.calculated_at and v.calculated_at > cur.calculated_at): latest_by_period[key] = v for period, v in sorted(latest_by_period.items()): a = old_level(v.actual_value, kpi) b = calc_level_from_exprs(v.actual_value, kpi.threshold_green, kpi.threshold_yellow, kpi.threshold_red) c = severity_order_level(v.actual_value, kpi) samples.append({ "kpi_id": kpi.id, "kpi_code": kpi.kpi_code, "period": period, "actual": v.actual_value, "old": a, "new": b, "severity_order": c, "G": kpi.threshold_green, "Y": kpi.threshold_yellow, "R": kpi.threshold_red, }) count_old[a] += 1 count_new[b] += 1 if a != b: diffs_ab.append(samples[-1]) if c != b: diffs_bc.append(samples[-1]) print(f"参与回归的 (KPI, 期间) 判定点: {len(samples)}") print("旧实现级别分布 :", dict(count_old)) print("新实现级别分布 :", dict(count_new)) print(f"\n=== A(旧) vs B(新) 差异 {len(diffs_ab)} 条 ===") for d in diffs_ab: print(f" kpi={d['kpi_code']}(id={d['kpi_id']}) {d['period']} actual={d['actual']} " f"old={d['old']} -> new={d['new']} G={d['G']!r} Y={d['Y']!r} R={d['R']!r}") print(f"\n=== B(new=red>green>yellow) vs C(red>yellow>green) 差异 {len(diffs_bc)} 条 ===") for d in diffs_bc[:20]: print(f" kpi={d['kpi_code']}(id={d['kpi_id']}) {d['period']} actual={d['actual']} " f"B={d['new']} vs C={d['severity_order']} G={d['G']!r} Y={d['Y']!r} R={d['R']!r}") if len(diffs_bc) > 20: print(f" ... 其余 {len(diffs_bc) - 20} 条") # 预警集合差异(yellow/red 才落库) old_alerts = [(d['kpi_id'], d['period'], d['old']) for d in samples if d['old'] in ("yellow", "red")] new_alerts = [(d['kpi_id'], d['period'], d['new']) for d in samples if d['new'] in ("yellow", "red")] print(f"\n=== 预警集合:旧 {len(old_alerts)} 条 → 新 {len(new_alerts)} 条 ===") so, sn = set(old_alerts), set(new_alerts) for x in sorted(so - sn): print(" 仅旧有(解除):", x) for x in sorted(sn - so): print(" 仅新有(新增):", x) # 真实 alert_generator 事务内预演(阻断 commit,事后 rollback,零落库) import importlib.util # noqa: E402 _spec = importlib.util.spec_from_file_location( "cma_alert_generator", os.path.join(BASE, "scripts", "alert_generator.py")) ag = importlib.util.module_from_spec(_spec) _spec.loader.exec_module(ag) from datetime import datetime period = datetime.now().strftime("%Y-%m") real_commit = db.commit db.commit = lambda: None # 阻断落库 try: new_count = ag.run_alert_check(db, period) finally: db.commit = real_commit db.rollback() print(f"\n=== alert_generator 事务内预演(period={period},已 rollback 未落库)新增预警={new_count} ===") out = os.path.join(BASE, "scripts", "backups", "threshold_regression_20260911.json") os.makedirs(os.path.dirname(out), exist_ok=True) with open(out, "w", encoding="utf-8") as fh: json.dump({"samples": len(samples), "count_old": dict(count_old), "count_new": dict(count_new), "diff_ab": diffs_ab, "diff_bc": diffs_bc, "alert_only_old": sorted(so - sn), "alert_only_new": sorted(sn - so), "alert_gen_dry_run_new": new_count}, fh, ensure_ascii=False, indent=2) print("报告 JSON:", out) db.close() if __name__ == "__main__": main()