| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198 |
- #!/usr/bin/env python3
- # -*- coding: utf-8 -*-
- """旧体系接口连续 N 天零调用巡检(统一维度体系 P2-T3 退役证据判定)。
- 对应设计文档 2026-11-03-unified-dimension-system-design.md §8.3 退役触发条件
- 「旧接口连续 14 天零调用 + 双写一致性 0 差异」。本脚本只判定「零调用」这一半证据。
- 用法(Git Bash / 任意 Python3 环境):
- python3 legacy_dimension_zero_call_check.py --days 14 \
- --host 127.0.0.1 --port 3306 --db cfc_test --user root --password xxx
- 或通过环境变量传入连接参数(无需命令行硬编码生产连接串):
- DB_HOST / DB_PORT / DB_NAME / DB_USER / DB_PASSWORD
- 判定:
- PASS - 近 N 天每天都有 __heartbeat__ 心跳 且 业务路径调用总数=0
- → 连续 N 天零调用,可退役(输出零调用观察窗口起止日期)
- FAIL - 有心跳但业务调用>0 → 未满足退役条件,列出剩余调用路径与日期
- WARN - 无心跳(或心跳未覆盖整个观察窗口)→ 采集器未运行 / flag 未开启,
- 检查 DIM_REGISTRY_LEGACY_CALL_LOG_ENABLED 是否置 true
- 依赖:pymysql(pip install pymysql)。采集表 legacy_dimension_call_log 由后端
- DatabaseInitializer 迁移454 创建,__heartbeat__ 心跳行证明采集器当天存活,
- 防止「表空 = 假零调用」误判。
- """
- import argparse
- import datetime
- import os
- import sys
- HEARTBEAT = "__heartbeat__"
- def parse_args():
- env = os.environ
- p = argparse.ArgumentParser(
- description="旧体系接口连续 N 天零调用巡检(P2-T3 退役证据判定)")
- p.add_argument("--days", type=int, default=14,
- help="观察窗口天数(默认 14)")
- p.add_argument("--host", default=env.get("DB_HOST", "127.0.0.1"),
- help="数据库主机(默认 env DB_HOST / 127.0.0.1)")
- p.add_argument("--port", type=int, default=int(env.get("DB_PORT", "3306")),
- help="数据库端口(默认 env DB_PORT / 3306)")
- p.add_argument("--db", default=env.get("DB_NAME", "cfc"),
- help="数据库名(默认 env DB_NAME / cfc)")
- p.add_argument("--user", default=env.get("DB_USER", "root"),
- help="数据库用户(默认 env DB_USER / root)")
- p.add_argument("--password", default=env.get("DB_PASSWORD", ""),
- help="数据库密码(默认 env DB_PASSWORD / 空)")
- return p.parse_args()
- def connect(params):
- try:
- import pymysql
- except ImportError:
- sys.exit("缺少 pymysql 依赖,请先执行:pip install pymysql")
- conn = pymysql.connect(
- host=params.host, port=params.port, user=params.user,
- password=params.password, database=params.db, charset="utf8mb4",
- cursorclass=pymysql.cursors.DictCursor, autocommit=True)
- return conn
- def load_rows(conn, days):
- """返回近 N 天 (period, api_path, call_count) 列表(period 为 ISO 字符串)。"""
- with conn.cursor() as cur:
- cur.execute(
- "SELECT api_path, period, call_count FROM legacy_dimension_call_log "
- "WHERE period >= DATE_SUB(CURDATE(), INTERVAL %s DAY) "
- "ORDER BY period ASC, api_path ASC",
- (days,))
- rows = cur.fetchall()
- out = []
- for r in rows:
- out.append({
- "period": r["period"].isoformat() if hasattr(r["period"], "isoformat") else str(r["period"]),
- "api_path": r["api_path"],
- "call_count": int(r["call_count"] or 0),
- })
- return out
- def window_dates(days, today=None):
- """观察窗口日期(升序,含今天),返回 ISO 字符串列表,长度=days。"""
- if today is None:
- today = datetime.date.today()
- return [(today - datetime.timedelta(days=days - 1 - i)).isoformat()
- for i in range(days)]
- def judge(rows, days, today=None):
- """纯函数判定(无 DB),返回 (verdict, detail dict)。
- detail:
- verdict PASS / FAIL / WARN
- window_dates [iso...]
- heartbeat_dates {iso: True}
- business_by_date {iso: {path: count}}
- business_total int
- missing_heartbeat [iso...] 窗口内无心跳的日期
- zero_start / zero_end 连续零调用起止(PASS 时 = 窗口起止)
- remaining [(path, iso, count)] FAIL 时剩余调用
- """
- dates = window_dates(days, today)
- heartbeat_dates = {r["period"] for r in rows if r["api_path"] == HEARTBEAT}
- business_by_date = {}
- business_total = 0
- remaining = []
- for r in rows:
- if r["api_path"] == HEARTBEAT:
- continue
- business_by_date.setdefault(r["period"], {})
- business_by_date[r["period"]][r["api_path"]] = (
- business_by_date[r["period"]].get(r["api_path"], 0) + r["call_count"])
- business_total += r["call_count"]
- remaining.append((r["api_path"], r["period"], r["call_count"]))
- missing_heartbeat = [d for d in dates if d not in heartbeat_dates]
- detail = {
- "verdict": None,
- "window_dates": dates,
- "heartbeat_dates": heartbeat_dates,
- "business_by_date": business_by_date,
- "business_total": business_total,
- "missing_heartbeat": missing_heartbeat,
- "zero_start": dates[0] if dates else None,
- "zero_end": dates[-1] if dates else None,
- "remaining": remaining,
- }
- if missing_heartbeat:
- # 心跳未覆盖整个窗口 → 无法证明「采集器全程活着」,不轻下 PASS
- detail["verdict"] = "WARN"
- elif business_total == 0:
- detail["verdict"] = "PASS"
- else:
- detail["verdict"] = "FAIL"
- return detail["verdict"], detail
- def render_matrix(detail):
- """输出 日期×接口路径 调用矩阵(表格文本)。"""
- lines = []
- lines.append("日期 | 心跳 | 业务调用路径(当日次数)")
- lines.append("-" * 72)
- for d in detail["window_dates"][::-1]: # 最新在上
- hb = "有" if d in detail["heartbeat_dates"] else "无"
- biz = detail["business_by_date"].get(d, {})
- if biz:
- parts = ", ".join("%s×%d" % (p, c) for p, c in sorted(biz.items()))
- else:
- parts = "-"
- lines.append("%s | %s | %s" % (d, hb, parts))
- return "\n".join(lines)
- def render_verdict(verdict, detail):
- if verdict == "PASS":
- return (
- "判定:PASS —— 连续 %d 天零调用,可退役\n"
- "零调用观察窗口:%s 至 %s(窗口内每天有心跳,业务调用总数=0)"
- % (len(detail["window_dates"]), detail["zero_start"], detail["zero_end"]))
- if verdict == "FAIL":
- rem = "\n".join(" %s %s x%d" % (p, d, c)
- for p, d, c in detail["remaining"])
- return (
- "判定:FAIL —— 仍有业务调用,未满足退役条件\n"
- "剩余调用路径与日期:\n%s" % rem)
- return (
- "判定:WARN —— 采集证据不足,不能判定可退役\n"
- "缺失心跳日期(%d 天):%s\n"
- "检查项:\n"
- " 1) 采集开关 dimension.registry.legacyCallLog.enabled "
- "(环境变量 DIM_REGISTRY_LEGACY_CALL_LOG_ENABLED)是否置 true;\n"
- " 2) 采集服务 LegacyDimensionCallLogService 每小时心跳是否正常写入;\n"
- " 3) 若表完全为空:采集器未运行 / 采集开关未开启(默认 false 零开销)。"
- % (len(detail["missing_heartbeat"]), ", ".join(detail["missing_heartbeat"])))
- def main():
- args = parse_args()
- conn = connect(args)
- try:
- rows = load_rows(conn, args.days)
- finally:
- conn.close()
- verdict, detail = judge(rows, args.days)
- print("=== 旧体系接口调用矩阵(近 %d 天)===" % args.days)
- print(render_matrix(detail))
- print()
- print(render_verdict(verdict, detail))
- return 0
- if __name__ == "__main__":
- sys.exit(main())
|