feat(日志): 任务步骤明细表 + 「日志 → 步骤明细」面板(按设备/任务/时间过滤、按运行归组、导出 CSV)
文本日志只能 grep,"这台设备这次运行为什么失败"翻起来很费劲。新增一张 **结构化**的步骤明细表,把"哪一步、什么类型、哪个选择器、结果、耗时"落库。 - core/models.py:新增 task_step_log(run_id/job/设备/step_path/selector/ result/detail/duration_ms),索引 run_id、created_at、(serial,created_at)、 (job_id,created_at); - core/step_log.py(新):**专用写线程 + 有界队列**批量落库——步骤执行是热路径, 任务线程只 put_nowait(实测 9000 次入队 31ms),队列满丢弃并计数,绝不阻塞; 另有保留期清理(默认 14 天,每日 04:13 + 每次启动); - tasks/generic/task.py:_exec_one 记一条(异常=error / handler 返回 False=miss / 未知类型=unknown / 概率未触发=skip);_exec_steps 维护路径栈得到 "2.1.3" 这样的嵌套位置;**单次运行封顶 2000 条**——forever 循环任务否则会写爆表; - 运行上下文 ctx(run_id/job_id/job_name/device_name)由 TaskManager 生成, 经 create_worker(serial, params, ctx=None) 传入 worker(扩展点向后兼容); - 接口:/api/step_logs(过滤+分页)、/runs(按运行归组)、/filters(下拉选项)、 /download(CSV,带 BOM); - 前端:日志页拆成「文件日志 / 步骤明细」子分栏 + static/admin/steplog.js。 红线:新表自动进备份覆盖清单(SUMMARY_TABLES 由元数据派生),已补 TABLE_LABELS 中文标签,导出实测 14 张表、coverage_missing 为空。 文档:DATA_MODEL §2.8/§1、API §10.2、ARCHITECTURE §1.1/§2.2/§3.1、 DEVELOPMENT §5.2 与红线表、DEPLOY §5.2、TASK_DEV §8.3、README。
This commit is contained in:
@@ -73,6 +73,12 @@ try:
|
||||
_notifier.init_app(app)
|
||||
except Exception as _e: # 通知出问题不影响启动
|
||||
_log.warning(f"通知模块初始化失败(不影响启动): {_e}")
|
||||
# 任务步骤明细:起一个专用写线程,任务线程只入队(见 core/step_log.py)
|
||||
try:
|
||||
from core import step_log as _step_log
|
||||
_step_log.init_app(app)
|
||||
except Exception as _e:
|
||||
_log.warning(f"步骤明细模块初始化失败(不影响启动): {_e}")
|
||||
# 消费「待生效的备份恢复」——必须在 init_db 之后(表已建好,且现在是事务替换而非换文件)
|
||||
_consume_pending_restore()
|
||||
# 库环境标签校验 + 启动横幅:每次启动都明确写出「现在连的是哪个库」,
|
||||
@@ -104,6 +110,24 @@ from web.auth import _csrf_protect, _csrf_token
|
||||
context.init(mgr, apk_mgr, device_pool)
|
||||
register_blueprints(app)
|
||||
|
||||
def _purge_step_log():
|
||||
"""清理超期的任务步骤明细(启动时 + 每日 04:13)。
|
||||
|
||||
app context 由本函数自建:APScheduler 的作业跑在调度线程里,没有请求上下文。
|
||||
"""
|
||||
try:
|
||||
from core.step_log import purge_old
|
||||
with app.app_context():
|
||||
purge_old()
|
||||
except Exception as e:
|
||||
_log.warning(f"步骤明细清理失败(不影响主服务): {e}")
|
||||
|
||||
|
||||
def _purge_step_log_async():
|
||||
"""启动时后台清理一次(不阻塞启动;服务若频繁重启,定时任务可能一直轮不到)。"""
|
||||
threading.Thread(target=_purge_step_log, daemon=True).start()
|
||||
|
||||
|
||||
# 经验库每日 AI 巡检(凌晨 03:47):评审标记疑似问题经验,删除只走人工确认。
|
||||
# 巡检线程在 run_experience_audit 内自建 app context,不依赖这里。
|
||||
try:
|
||||
@@ -112,6 +136,9 @@ try:
|
||||
from web.agent_api import run_experience_audit
|
||||
_audit_sched = BackgroundScheduler(timezone="Asia/Shanghai")
|
||||
_audit_sched.add_job(run_experience_audit, CronTrigger(hour=3, minute=47))
|
||||
# 步骤明细清理:超过保留期(core/step_log.KEEP_DAYS)的记录每天删一次。
|
||||
# 不清理的话这张表是唯一无界增长的表,会一路顶大备份包。
|
||||
_audit_sched.add_job(_purge_step_log, CronTrigger(hour=4, minute=13))
|
||||
_audit_sched.start()
|
||||
_log.info("经验库每日巡检已注册(03:47 Asia/Shanghai)")
|
||||
except Exception as e:
|
||||
@@ -294,6 +321,8 @@ if __name__ == "__main__":
|
||||
_ensure_uiauto_running()
|
||||
# 预连接设备池:adb server 重启后设备全掉线,后台并发重连加速恢复
|
||||
_preconnect_pool_devices()
|
||||
# 步骤明细清理:服务频繁重启时定时任务可能一直轮不到,启动也清一次
|
||||
_purge_step_log_async()
|
||||
_log.info(f"管理后台: http://localhost:{WEB_PORT}/ (admin/admin123)")
|
||||
# 启动通知:放在 __main__ 里是刻意的——以 WSGI 方式 import 本模块只有"半个启动"
|
||||
# (阶段 A~F 在 import 期就跑完了),不该发"服务已启动"。
|
||||
@@ -316,6 +345,11 @@ if __name__ == "__main__":
|
||||
_notifier.shutdown(timeout=3.0)
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
from core import step_log as _step_log
|
||||
_step_log.shutdown(timeout=3.0) # 排空队列后再退
|
||||
except Exception:
|
||||
pass
|
||||
mgr.shutdown()
|
||||
device_discovery.shutdown()
|
||||
_stop_uiauto()
|
||||
|
||||
Reference in New Issue
Block a user