102 lines
3.6 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""规则加载与缓存。
- 从 RuleStoreMySQL全量加载规则
- 写入 CacheRedis作为热缓存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