102 lines
3.6 KiB
Python
102 lines
3.6 KiB
Python
"""规则加载与缓存。
|
||
|
||
- 从 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
|