Files
ts-anomaly-td/ts_anomaly_td/cli.py
T

317 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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()