"""CLI argparse 入口:inject / detect-batch / forecast-anomaly / e2e。""" import argparse import logging import sys from ts_anomaly_td.connector import TDConnection from ts_anomaly_td.schema import setup_schema from ts_anomaly_td.io_csv import read_csv from ts_anomaly_td.detection import detect_all_algos, ALL_ALGOS from ts_anomaly_td.forecast import forecast_anomaly logger = logging.getLogger(__name__) def cmd_inject(args): """inject 子命令:读 CSV → 建表 → 批量插入。""" logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s") ts, vals, labels = read_csv(args.csv, value_col=args.value_col) rows = list(zip(ts, vals, labels)) logger.info("read %d rows from %s", len(rows), args.csv) conn = TDConnection(args.url) try: db = args.db or "ts_anomaly" setup_schema(conn, args.stable, db) conn.batch_insert(args.stable, rows) logger.info("inject complete: %d rows → %s.s_%s", len(rows), db, args.stable) finally: conn.close() def cmd_detect_batch(args): """detect-batch 子命令:多算法 ANOMALY_WINDOW。""" logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s") # 确定算法列表(--algo 优先于 --algos) if args.algo: algos = [args.algo] elif args.algos: algos = args.algos.split(",") else: algos = ALL_ALGOS conn = TDConnection(args.url) try: db = args.db or "ts_anomaly" conn.execute_no_result(f"USE {db}") results = detect_all_algos(conn, args.stable, algos=algos) if getattr(args, 'json_output', False): import json if len(algos) == 1: algo_name = algos[0] r = results[algo_name] output = { "algo": algo_name, "stable": args.stable, "windows": r["windows"], "window_count": len(r["windows"]), "error": r["error"], } else: output = [] for algo_name, r in results.items(): output.append({ "algo": algo_name, "stable": args.stable, "windows": r["windows"], "window_count": len(r["windows"]), "error": r["error"], }) print(json.dumps(output, ensure_ascii=False)) else: for algo, r in results.items(): if r["error"]: print(f" {algo}: ERROR - {r['error']}") else: print(f" {algo}: {len(r['windows'])} windows") finally: conn.close() def cmd_forecast_anomaly(args): """forecast-anomaly 子命令:FORECAST + 区间外点。""" logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s") conn = TDConnection(args.url) try: db = args.db or "ts_anomaly" conn.execute_no_result(f"USE {db}") points = forecast_anomaly( conn, args.stable, algo=args.algo, rows=args.rows, conf=args.conf, ) anomalies = [p for p in points if p["is_anomaly"]] print(f"FORECAST: {len(points)} points, {len(anomalies)} anomalies") for p in anomalies: print(f" ts={p['ts']} value={p['value']} _flow={p['_flow']} _fhigh={p['_fhigh']}") finally: conn.close() def cmd_e2e(args): """e2e 子命令:编排完整流程。""" from ts_anomaly_td.e2e import run_e2e return run_e2e(args) def cmd_visualize(args): """visualize 子命令:渲染双栏对比图。""" logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s") from ts_anomaly_td.io_csv import read_csv from ts_anomaly_td.visualize import render_contrast ts, vals, labels = read_csv(args.gt_csv) conn = TDConnection(args.url) try: db = args.db or "ts_anomaly" conn.execute_no_result(f"USE {db}") if args.algos: from ts_anomaly_td.detection import detect_all_algos results = detect_all_algos(conn, args.stable, algos=args.algos.split(",")) else: from ts_anomaly_td.detection import detect_all_algos results = detect_all_algos(conn, args.stable) render_contrast(ts, vals, labels, results, args.output, title=args.stable) logger.info("visualize: %s", args.output) finally: conn.close() def cmd_watch(args): """watch 子命令:TMQ 准实时监控。""" logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s") from ts_anomaly_td.tmq_watch import WatchRunner conn = TDConnection(args.url) try: db = args.db or "ts_anomaly" conn.execute_no_result(f"USE {db}") runner = WatchRunner(conn, args.stable, window_sec=args.window_sec) runner.run() finally: conn.close() def build_parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser( prog="ts-anomaly-td", description="TDengine 时序异常分析 CLI", ) sub = parser.add_subparsers(dest="command", required=True) # inject p_inj = sub.add_parser("inject", help="注入 CSV 数据到 TDengine") p_inj.add_argument("--csv", required=True, help="CSV 文件路径") p_inj.add_argument("--stable", required=True, help="supertable 名称") p_inj.add_argument("--db", default="ts_anomaly", help="数据库名 (default: ts_anomaly)") p_inj.add_argument("--value-col", default="value", help="数值列名 (default: value)") p_inj.add_argument("--url", default="ws://root:taosdata@localhost:6041", help="TDengine WebSocket URL") # detect-batch p_det = sub.add_parser("detect-batch", help="批量 ANOMALY_WINDOW 6 算法") p_det.add_argument("--stable", required=True, help="supertable 名称") p_det.add_argument("--db", default="ts_anomaly", help="数据库名") p_det.add_argument("--algos", default=None, help="逗号分隔算法列表 (默认全 6 个)") p_det.add_argument("--algo", default=None, help="单算法模式(优先于 --algos)") p_det.add_argument("--json", action="store_true", dest="json_output", help="输出 JSON 到 stdout") p_det.add_argument("--url", default="ws://root:taosdata@localhost:6041", help="TDengine WebSocket URL") # forecast-anomaly p_fc = sub.add_parser("forecast-anomaly", help="FORECAST 预测式异常检测") p_fc.add_argument("--stable", required=True, help="supertable 名称") p_fc.add_argument("--db", default="ts_anomaly", help="数据库名") p_fc.add_argument("--algo", default="holtwinters", help="预测算法 (default: holtwinters)") p_fc.add_argument("--rows", type=int, default=10, help="预测行数 (default: 10)") p_fc.add_argument("--conf", type=int, default=95, help="置信度 (default: 95)") p_fc.add_argument("--url", default="ws://root:taosdata@localhost:6041", help="TDengine WebSocket URL") # e2e p_e2e = sub.add_parser("e2e", help="端到端编排") p_e2e.add_argument("--data-dir", default="data", help="CSV 数据目录 (default: data)") p_e2e.add_argument("--output-dir", default="render", help="PNG 输出目录 (default: render)") p_e2e.add_argument("--log-dir", default="logs", help="JSON 日志目录 (default: logs)") p_e2e.add_argument("--url", default="ws://root:taosdata@localhost:6041", help="TDengine WebSocket URL") # visualize p_viz = sub.add_parser("visualize", help="渲染 GT 对比可视化图") p_viz.add_argument("--gt-csv", required=True, help="GT CSV 文件路径") p_viz.add_argument("--stable", required=True, help="supertable 名称") p_viz.add_argument("--db", default="ts_anomaly", help="数据库名") p_viz.add_argument("--algos", default=None, help="逗号分隔算法列表 (默认全 6 个)") p_viz.add_argument("--output", required=True, help="PNG 输出路径") p_viz.add_argument("--url", default="ws://root:taosdata@localhost:6041", help="TDengine WebSocket URL") # watch (Iter-2) p_watch = sub.add_parser("watch", help="TMQ 准实时异常监控") p_watch.add_argument("--stable", required=True, help="supertable 名称") p_watch.add_argument("--db", default="ts_anomaly", help="数据库名") p_watch.add_argument("--window-sec", type=int, default=30, help="检测间隔秒数 (default: 30)") p_watch.add_argument("--url", default="ws://root:taosdata@localhost:6041", help="TDengine WebSocket URL") # render-algo p_ra = sub.add_parser("render-algo", help="单算法 GT+检测渲染 PNG") p_ra.add_argument("--gt-csv", required=True, help="GT CSV 文件路径") p_ra.add_argument("--det-json", required=True, help="detect-batch --json 输出的 JSON 文件") p_ra.add_argument("--algo", required=True, help="算法名称") p_ra.add_argument("--output", required=True, help="PNG 输出路径") p_ra.add_argument("--value-col", default="value", help="数值列名 (default: value)") p_ra.add_argument("--title", default="", help="图表标题") # montage p_mt = sub.add_parser("montage", help="多 PNG 拼图") p_mt.add_argument("--inputs", nargs="+", required=True, help="输入 PNG 文件列表") p_mt.add_argument("--output", required=True, help="拼图输出路径") p_mt.add_argument("--cols", type=int, default=2, help="列数 (default: 2)") p_mt.add_argument("--title", default="", help="总标题") # render-column p_rc = sub.add_parser("render-column", help="GT 顶部 + 各算法纵向对比") p_rc.add_argument("--gt-csv", required=True, help="GT CSV 文件路径") p_rc.add_argument("--stable", required=True, help="supertable 名称") p_rc.add_argument("--db", default="ts_anomaly", help="数据库名") p_rc.add_argument("--value-col", default="value", help="数值列名 (default: value)") p_rc.add_argument("--output", required=True, help="PNG 输出路径") p_rc.add_argument("--url", default="ws://root:taosdata@localhost:6041", help="TDengine WebSocket URL") return parser def cmd_render_algo(args): """render-algo 子命令:单算法 GT+检测渲染 PNG。""" logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s") import json from ts_anomaly_td.io_csv import read_csv from ts_anomaly_td.visualize import render_algo ts, vals, labels = read_csv(args.gt_csv, value_col=args.value_col) with open(args.det_json) as f: det_data = json.load(f) # det_json 格式: {"algo": "ksigma", "windows": [[start, end], ...], "error": null} windows = det_data.get("windows", []) title = args.title or f"{Path(args.gt_csv).stem} / {args.algo}" render_algo(ts, vals, labels, windows, args.algo, args.output, title=title) def cmd_montage(args): """montage 子命令:多 PNG 拼图。""" logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s") from ts_anomaly_td.visualize import montage_pngs montage_pngs(args.inputs, args.output, cols=args.cols, title=args.title) logger.info("montage complete: %s", args.output) def cmd_render_column(args): """render-column 子命令:GT 顶部 + 各算法纵向对比。""" logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s") from ts_anomaly_td.io_csv import read_csv from ts_anomaly_td.visualize import render_column ts, vals, labels = read_csv(args.gt_csv, value_col=args.value_col) conn = TDConnection(args.url) try: db = args.db or "ts_anomaly" conn.execute_no_result(f"USE {db}") results = detect_all_algos(conn, args.stable) finally: conn.close() title = f"{args.stable} — 算法对比" render_column(ts, vals, labels, results, args.output, title=title) def main(): parser = build_parser() args = parser.parse_args() dispatch = { "inject": cmd_inject, "detect-batch": cmd_detect_batch, "forecast-anomaly": cmd_forecast_anomaly, "e2e": cmd_e2e, "visualize": cmd_visualize, "watch": cmd_watch, "render-algo": cmd_render_algo, "montage": cmd_montage, "render-column": cmd_render_column, } fn = dispatch.get(args.command) if fn is None: parser.print_help() sys.exit(1) sys.exit(fn(args) or 0) if __name__ == "__main__": main()