Files
agentic-soc-platform/Lib/monitor.py
T
2026-05-19 16:34:22 +08:00

220 lines
9.0 KiB
Python

# -*- coding: utf-8 -*-
import importlib
import threading
import time
import uuid
from typing import Callable, Dict, Optional
from apscheduler.schedulers.background import BackgroundScheduler
from Lib.baseplaybook import BasePlaybook
from Lib.log import logger
from Lib.moduleengine import ModuleEngine
from Lib.playbookloader import PlaybookLoader
from Lib.threadmodulemanager import thread_module_manager
from Lib.xcache import Xcache
from PLUGINS.Redis.redis_stream_api import RedisStreamAPI
from PLUGINS.SIRP.analysis import run_case_analysis
from PLUGINS.SIRP.sirpapi import Playbook, Case
from PLUGINS.SIRP.sirpextramodel import PlaybookJobStatus, PlaybookModel
class MainMonitor(object):
@staticmethod
def on_playbook_task_finished(thread_id: str, task_obj: BasePlaybook, result: object,
exception: Optional[Exception], context: Optional[Dict[str, str]] = None):
context = context or {}
playbook_row_id = context.get("playbook_row_id")
if not isinstance(playbook_row_id, str) or playbook_row_id == "":
logger.error(f"[Thread {thread_id}] Missing playbook_row_id in callback context.")
return
try:
playbook_current = Playbook.get(playbook_row_id, lazy_load=True)
except Exception as e:
logger.error(f"[Thread {thread_id}] Failed to load playbook for fallback status update.")
logger.exception(e)
return
# Business code has already written a terminal state; do not overwrite.
if playbook_current.job_status != PlaybookJobStatus.RUNNING:
logger.info(
f"[Thread {thread_id}] Skip fallback status update, current status: {playbook_current.job_status}, "
f"row_id: {playbook_row_id}"
)
return
fallback_status = PlaybookJobStatus.SUCCESS if exception is None else PlaybookJobStatus.FAILED
if exception is None:
remark = f"source=thread_fallback; thread_id={thread_id}; message=Task finished without explicit business status update."
else:
remark = (
f"source=thread_fallback; thread_id={thread_id}; "
f"exception={type(exception).__name__}; message={exception}"
)
model_tmp = PlaybookModel(row_id=playbook_row_id)
model_tmp.job_status = fallback_status
model_tmp.remark = remark
Playbook.update(model_tmp)
logger.info(
f"[Thread {thread_id}] Applied fallback status update: {fallback_status}, row_id: {playbook_row_id}"
)
MainScheduler: BackgroundScheduler
def __init__(self):
self._background_threads: Dict[str, list] = {}
self.engine = ModuleEngine()
self.redis_stream_api = RedisStreamAPI()
self.MainScheduler = BackgroundScheduler(timezone='Asia/Shanghai')
@staticmethod
def run_task_in_loop(task_func: Callable, task_name: str, retry_interval: int = 5, exec_interval: int = None):
"""
Run a task function in an infinite loop with error handling
Args:
task_func: The function to run
task_name: Name of the task for logging
retry_interval: Seconds to wait between retries on error
exec_interval: Seconds to wait between executions (defaults to retry_interval if None)
"""
# If exec_interval is not specified, use retry_interval
if exec_interval is None:
exec_interval = retry_interval
while True:
try:
task_func()
# Wait for the specified execution interval before running again
time.sleep(exec_interval)
except Exception as e:
logger.error(f"Error in {task_name}")
logger.exception(e)
time.sleep(retry_interval)
def start_background_task(self, task_func: Callable, task_name: str, retry_interval: int = 5,
exec_interval: int = None, thread_count: int = 1):
"""
Start a background task in one or more separate threads
Args:
task_func: The function to run
task_name: Name of the task for logging
retry_interval: Seconds to wait between retries on error
exec_interval: Seconds to wait between executions (defaults to retry_interval if None)
thread_count: Number of threads to spawn for this task
"""
if task_name not in self._background_threads:
self._background_threads[task_name] = []
for i in range(thread_count):
suffix = f"#{i}" if thread_count > 1 else ""
thread_name = f"{task_name}{suffix}"
thread = threading.Thread(
target=self.run_task_in_loop,
args=(task_func, thread_name, retry_interval, exec_interval),
daemon=True,
name=thread_name,
)
self._background_threads[task_name].append(thread)
thread.start()
logger.info(f"Started background task: {thread_name}")
def start(self):
logger.info("Starting background services...")
logger.info("Load PlaybookLoader module config")
PlaybookLoader.load_all_playbook_config()
delay_time = 3
# Start background tasks
self.start_background_task(self.subscribe_pending_playbook, "subscribe_pending_playbook", delay_time)
self.start_background_task(self.subscribe_case_analysis_scheduler, "subscribe_case_analysis_scheduler", delay_time)
self.start_background_task(self.subscribe_case_analysis_queue, "subscribe_case_analysis_queue", delay_time, thread_count=3)
# engine
self.engine.start()
logger.info("Background services started.")
@staticmethod
def subscribe_pending_playbook():
models = Playbook.list_pending_playbooks()
for model in models:
module_config = Xcache.get_module_config_by_name(model.name)
model_tmp = PlaybookModel(row_id=model.row_id)
if module_config is None:
PlaybookLoader.load_all_playbook_config() # try again
module_config = Xcache.get_module_config_by_name(model.name)
if module_config is None:
logger.error(f"PlaybookLoader module config not found: {model.name}")
model_tmp.job_status = PlaybookJobStatus.FAILED
model_tmp.remark = f"PlaybookLoader module config not found: {model.name}"
Playbook.update(model_tmp)
continue
load_path = module_config.get("load_path")
try:
class_intent = importlib.import_module(load_path)
playbook_intent: BasePlaybook = class_intent.Playbook()
playbook_intent._playbook_model = model
except Exception as E:
logger.exception(E)
model_tmp.job_status = PlaybookJobStatus.FAILED
model_tmp.remark = str(E)
Playbook.update(model_tmp)
continue
job_id = str(uuid.uuid1())
model_tmp.job_status = PlaybookJobStatus.RUNNING
model_tmp.job_id = job_id
Playbook.update(model_tmp)
try:
thread_module_manager.start_task(
playbook_intent,
thread_id=job_id,
on_finished=MainMonitor.on_playbook_task_finished,
callback_context={
"playbook_row_id": model.row_id,
},
)
except Exception as e:
model_tmp.job_status = PlaybookJobStatus.FAILED
model_tmp.remark = f"Failed to create playbook job. exception={type(e).__name__}; message={e}"
Playbook.update(model_tmp)
continue
logger.info(f"Create playbook job success: {job_id}")
@staticmethod
def subscribe_case_analysis_scheduler():
# Periodically move due cases into the analysis queue.
# 周期性扫描到点案件,并将其送入分析队列。
promoted_row_ids = Case.promote_due_analysis_cases()
if promoted_row_ids:
logger.info(f"Queued {len(promoted_row_ids)} case(s) for scheduled analysis.")
def subscribe_case_analysis_queue(self):
# Consume one queue message and hand it to the analysis runner.
# 每次消费一条队列消息,并交给分析执行器处理。
message = self.redis_stream_api.read_stream_message_with_id(
stream_name=Case.ANALYSIS_STREAM_NAME,
consumer_name="case-analysis-worker",
)
queue_message_id = message.get("message_id")
payload = message.get("data", {})
case_row_id = payload.get("case_row_id")
trigger = payload.get("trigger", "unknown")
if not case_row_id:
logger.error("Case analysis queue message missing case_row_id.")
return
run_case_analysis(case_row_id, trigger, queue_message_id=queue_message_id)