238 lines
9.0 KiB
Python
238 lines
9.0 KiB
Python
"""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")
|
||
|
||
return parser
|
||
|
||
|
||
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,
|
||
}
|
||
|
||
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()
|