From 68ad064f8080d6f73524d82ed5bafae7ee688d35 Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Thu, 28 May 2026 23:36:39 +0800 Subject: [PATCH] Add log retention settings and clear logs --- README.md | 5 ++- app.py | 35 ++++++++++++++++--- config.example.json | 1 + static/index.html | 11 +++--- tests/test_log_retention.py | 67 +++++++++++++++++++++++++++++++++++++ 5 files changed, 108 insertions(+), 11 deletions(-) create mode 100644 tests/test_log_retention.py diff --git a/README.md b/README.md index b17c933..02430d8 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ - 检测间隔、超时、失败/恢复阈值可配置 - 通知渠道:Bark、PushPlus、Autman、Webhook - SQLite 保存检测记录、事件和通知日志 +- 可配置单节点最多保留的监测记录数,并可一键清空日志 - Docker / Docker Compose 部署 ## 快速启动 @@ -43,7 +44,8 @@ python3 app.py --host 0.0.0.0 --port 8787 --config config.json "interval_seconds": 300, "timeout_seconds": 8, "fail_threshold": 3, - "recover_threshold": 1 + "recover_threshold": 1, + "max_checks_per_proxy": 500 } ``` @@ -96,6 +98,7 @@ Autman 通道适配 `/m/push1` 消息推送插件,发送 JSON: ```text GET /api/status PUT /api/settings +DELETE /api/logs POST /api/check/ POST /api/notify/test GET /api/proxies diff --git a/app.py b/app.py index 9b1efb8..fd44f23 100644 --- a/app.py +++ b/app.py @@ -59,6 +59,7 @@ def default_config() -> dict[str, Any]: "target_port": 443, "fail_threshold": 3, "recover_threshold": 1, + "max_checks_per_proxy": 500, "db_path": str(DEFAULT_DB), "proxies": [], "notifications": {"enabled": True, "timeout_seconds": 10, "channels": []}, @@ -74,6 +75,7 @@ def merge_defaults(cfg: dict[str, Any]) -> dict[str, Any]: base["target_host"] = "www.baidu.com" base["target_port"] = int(base.get("target_port") or 443) base["interval_seconds"] = int(base.get("interval_seconds") or 300) + base["max_checks_per_proxy"] = max(1, int(base.get("max_checks_per_proxy") or 500)) base.setdefault("proxies", []) for proxy in base["proxies"]: if proxy.get("target_host") in (None, "", "1.1.1.1", "8.8.8.8", "google.com"): @@ -180,12 +182,22 @@ class Store: ) self.conn.commit() - async def add_check(self, proxy_id: str, ok: bool, latency_ms: int | None, error: str | None, exit_ip: str | None) -> None: + async def add_check(self, proxy_id: str, ok: bool, latency_ms: int | None, error: str | None, exit_ip: str | None, max_per_proxy: int | None = None) -> None: async with self.lock: self.conn.execute( "insert into checks(proxy_id,ts,ok,latency_ms,error,exit_ip) values(?,?,?,?,?,?)", (proxy_id, now_ts(), 1 if ok else 0, latency_ms, error, exit_ip), ) + if max_per_proxy: + self.conn.execute( + """ + delete from checks + where proxy_id=? and id not in ( + select id from checks where proxy_id=? order by ts desc, id desc limit ? + ) + """, + (proxy_id, proxy_id, max(1, int(max_per_proxy))), + ) self.conn.commit() async def add_event(self, proxy_id: str, old_status: str | None, new_status: str, message: str) -> None: @@ -205,7 +217,7 @@ class Store: self.conn.commit() def recent_checks(self, proxy_id: str, limit: int = 50) -> list[dict[str, Any]]: - rows = self.conn.execute("select * from checks where proxy_id=? order by ts desc limit ?", (proxy_id, limit)).fetchall() + rows = self.conn.execute("select * from checks where proxy_id=? order by ts desc, id desc limit ?", (proxy_id, limit)).fetchall() return [dict(r) for r in rows] def recent_events(self, limit: int = 50) -> list[dict[str, Any]]: @@ -216,6 +228,15 @@ class Store: rows = self.conn.execute("select * from notification_logs order by ts desc limit ?", (limit,)).fetchall() return [dict(r) for r in rows] + async def clear_logs(self) -> dict[str, int]: + async with self.lock: + removed = {} + for table in ("checks", "events", "notification_logs"): + cur = self.conn.execute(f"delete from {table}") + removed[table] = cur.rowcount if cur.rowcount is not None else 0 + self.conn.commit() + return removed + @dataclasses.dataclass class RuntimeState: @@ -538,6 +559,8 @@ class App: out["fail_threshold"] = max(1, int(data.get("fail_threshold") or 3)) if "recover_threshold" in data: out["recover_threshold"] = max(1, int(data.get("recover_threshold") or 1)) + if "max_checks_per_proxy" in data: + out["max_checks_per_proxy"] = max(1, int(data.get("max_checks_per_proxy") or 500)) return out async def update_settings(self, data: dict[str, Any]) -> dict[str, Any]: @@ -554,7 +577,7 @@ class App: await self.save_config() for p in list(self.proxies): await self.restart_proxy_task(p["id"]) - return {k: self.config.get(k) for k in ["interval_seconds", "timeout_seconds", "target_host", "target_port", "fail_threshold", "recover_threshold"]} + return {k: self.config.get(k) for k in ["interval_seconds", "timeout_seconds", "target_host", "target_port", "fail_threshold", "recover_threshold", "max_checks_per_proxy"]} async def upsert_proxy(self, data: dict[str, Any], proxy_id: str | None = None) -> dict[str, Any]: existing = self.get_proxy(proxy_id) if proxy_id else None @@ -600,7 +623,7 @@ class App: proxy_id = proxy["id"] st = self.state_for(proxy_id) ok, latency, err, exit_ip = await check_proxy(proxy) - await self.store.add_check(proxy_id, ok, latency, err, exit_ip) + await self.store.add_check(proxy_id, ok, latency, err, exit_ip, int(self.config.get("max_checks_per_proxy") or 500)) st.last_ts = now_ts(); st.last_ok = ok; st.latency_ms = latency; st.error = err; st.exit_ip = exit_ip fail_threshold = int(proxy.get("fail_threshold", self.config.get("fail_threshold", 3))) recover_threshold = int(proxy.get("recover_threshold", self.config.get("recover_threshold", 1))) @@ -650,7 +673,7 @@ class App: items.append({"proxy": public_proxy(p), "state": dataclasses.asdict(st), "recent": self.store.recent_checks(sid, 20)}) return { "ok": True, "now": now_ts(), "now_text": iso(), - "settings": {k: self.config.get(k) for k in ["interval_seconds", "timeout_seconds", "target_host", "target_port", "fail_threshold", "recover_threshold"]}, + "settings": {k: self.config.get(k) for k in ["interval_seconds", "timeout_seconds", "target_host", "target_port", "fail_threshold", "recover_threshold", "max_checks_per_proxy"]}, "proxies": items, "notifications": self.config.get("notifications", {}), "events": self.store.recent_events(50), @@ -696,6 +719,8 @@ async def handle_http(reader: asyncio.StreamReader, writer: asyncio.StreamWriter status, ctype, resp = json_response(app.status_payload()) elif method == "PUT" and p == "/api/settings": status, ctype, resp = json_response({"ok": True, "settings": await app.update_settings(data)}) + elif method == "DELETE" and p == "/api/logs": + status, ctype, resp = json_response({"ok": True, "removed": await app.store.clear_logs()}) elif method == "POST" and p == "/api/proxies": status, ctype, resp = json_response({"ok": True, "proxy": await app.upsert_proxy(data)}, 201) elif method == "PUT" and p.startswith("/api/proxies/"): diff --git a/config.example.json b/config.example.json index bdfef25..acea1cc 100644 --- a/config.example.json +++ b/config.example.json @@ -5,6 +5,7 @@ "target_port": 443, "fail_threshold": 3, "recover_threshold": 1, + "max_checks_per_proxy": 500, "db_path": "/app/data/monitor.db", "proxies": [ { diff --git a/static/index.html b/static/index.html index 24d5725..833b0b5 100644 --- a/static/index.html +++ b/static/index.html @@ -9,23 +9,24 @@

运行概览

加载中...
节点
0
正常
0
故障
0
通知
0

快速操作

管理节点和通知渠道。

SOCKS5 节点

通知渠道

检测设置

配置全局检测目标、间隔、超时和状态阈值。

默认检测目标
-
默认间隔
-
默认超时
-
阈值
-

事件

时间节点状态消息

通知记录

时间渠道结果标题
+

运行概览

加载中...
节点
0
正常
0
故障
0
通知
0
单节点记录
-

快速操作

管理节点和通知渠道。

SOCKS5 节点

通知渠道

检测设置

配置全局检测目标、间隔、超时和状态阈值。

默认检测目标
-
默认间隔
-
默认超时
-
阈值
-
单节点记录上限
-

事件

时间节点状态消息

通知记录

时间渠道结果标题