"""规则加载与缓存。 - 从 RuleStore(MySQL)全量加载规则; - 写入 Cache(Redis)作为热缓存(hms:rule:cache:{rule_id}、hms:rule:version); - 后台线程按 reload_interval 轮询版本号,版本变化则热加载; - 提供 match():按 metric / scope(host_id、host_group、service) 匹配启用的规则。 """ from __future__ import annotations import json import threading import time from typing import Dict, List, Optional from .models import MetricRule, MetricSample, parse_duration from .storage import Cache, RuleStore RULE_CACHE_PREFIX = "hms:rule:cache:" RULE_VERSION_KEY = "hms:rule:version" class RuleLoader: def __init__(self, store: RuleStore, cache: Cache, reload_interval: str = "10s") -> None: self._store = store self._cache = cache self._reload_interval = parse_duration(reload_interval) or 10 self._rules: Dict[str, MetricRule] = {} self._version = 0 self._lock = threading.RLock() self._stop_event = threading.Event() self._thread: Optional[threading.Thread] = None # ------------------------------------------------------------------ # # 加载与热更新 # ------------------------------------------------------------------ # def load(self) -> int: """全量加载规则,返回当前版本号。""" rules = self._store.list_rules() new_map = {r.rule_id: r for r in rules} version = self._store.get_version() with self._lock: self._rules = new_map self._version = version for rule in rules: self._cache.set(RULE_CACHE_PREFIX + rule.rule_id, json.dumps(rule.to_dict()), ttl=600) self._cache.set(RULE_VERSION_KEY, str(version)) return version def start(self) -> None: """启动热加载后台线程。""" if self._thread and self._thread.is_alive(): return self._thread = threading.Thread(target=self._run, name="rule-hot-reload", daemon=True) self._thread.start() def stop(self) -> None: self._stop_event.set() if self._thread: self._thread.join(timeout=2) def _run(self) -> None: while not self._stop_event.is_set(): try: current = self._store.get_version() if current != self._version: self.load() except Exception: # pragma: no cover - 日志由上层处理 pass self._stop_event.wait(self._reload_interval) # ------------------------------------------------------------------ # # 查询与匹配 # ------------------------------------------------------------------ # def get_rules(self) -> List[MetricRule]: with self._lock: return list(self._rules.values()) def get_rule(self, rule_id: str) -> Optional[MetricRule]: with self._lock: return self._rules.get(rule_id) def match( self, sample: MetricSample, host_id: str, host_group: Optional[str] = None, service: Optional[str] = None, ) -> List[MetricRule]: """返回匹配该样本的启用规则。""" matched: List[MetricRule] = [] service = service or sample.labels.get("service") for rule in self.get_rules(): if not rule.enabled: continue if rule.metric != sample.name: continue if not rule.scope.matches(host_id, host_group, service): continue matched.append(rule) return matched