239 lines
7.3 KiB
Python
239 lines
7.3 KiB
Python
"""领域模型:指标样本、阈值规则、作用范围、事件与告警。
|
||
|
||
字段命名与 docs/01-design 的 architecture.md / database-design.md / api-design.md 对齐;
|
||
对外 JSON 使用 camelCase,内部 Python 使用 snake_case。
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
from dataclasses import dataclass, field
|
||
from typing import Any, Dict, List, Optional
|
||
|
||
|
||
def parse_duration(value: Optional[str]) -> int:
|
||
"""将 '60s' / '5m' / '1h' / '1h30m' 等时长解析为秒。
|
||
|
||
纯数字按秒处理;空值返回 0。
|
||
"""
|
||
if value is None or value == "":
|
||
return 0
|
||
text = str(value).strip().lower()
|
||
if text.isdigit():
|
||
return int(text)
|
||
|
||
total = 0.0
|
||
num = ""
|
||
for ch in text:
|
||
if ch.isdigit() or ch == ".":
|
||
num += ch
|
||
elif ch in "smhd":
|
||
if not num:
|
||
raise ValueError(f"invalid duration: {value!r}")
|
||
n = float(num)
|
||
if ch == "s":
|
||
total += n
|
||
elif ch == "m":
|
||
total += n * 60
|
||
elif ch == "h":
|
||
total += n * 3600
|
||
elif ch == "d":
|
||
total += n * 86400
|
||
num = ""
|
||
else:
|
||
raise ValueError(f"invalid duration: {value!r}")
|
||
if num:
|
||
total += float(num)
|
||
return int(total)
|
||
|
||
|
||
@dataclass
|
||
class MetricSample:
|
||
"""规范化后的单条指标样本(对应 metrics.samples 消息)。"""
|
||
|
||
name: str
|
||
value: float
|
||
timestamp: float
|
||
labels: Dict[str, str] = field(default_factory=dict)
|
||
|
||
@classmethod
|
||
def from_dict(cls, data: Dict[str, Any]) -> "MetricSample":
|
||
return cls(
|
||
name=str(data["name"]),
|
||
value=float(data["value"]),
|
||
timestamp=float(data.get("timestamp", 0)),
|
||
labels=dict(data.get("labels") or {}),
|
||
)
|
||
|
||
def to_dict(self) -> Dict[str, Any]:
|
||
return {
|
||
"name": self.name,
|
||
"value": self.value,
|
||
"timestamp": self.timestamp,
|
||
"labels": self.labels,
|
||
}
|
||
|
||
|
||
@dataclass
|
||
class RuleScope:
|
||
"""规则作用范围(对应 rule_scope 表)。"""
|
||
|
||
scope_type: str = "all" # all | host_ids | host_group
|
||
host_ids: List[str] = field(default_factory=list)
|
||
host_group: str = ""
|
||
service: str = ""
|
||
|
||
def matches(self, host_id: str, host_group: Optional[str], service: Optional[str]) -> bool:
|
||
if self.scope_type == "all":
|
||
return True
|
||
if self.scope_type == "host_ids":
|
||
return host_id in self.host_ids
|
||
if self.scope_type == "host_group":
|
||
if self.host_group and host_group and self.host_group == host_group:
|
||
return True
|
||
return False
|
||
return False
|
||
|
||
def to_dict(self) -> Dict[str, Any]:
|
||
return {
|
||
"scope_type": self.scope_type,
|
||
"host_ids": list(self.host_ids),
|
||
"host_group": self.host_group,
|
||
"service": self.service,
|
||
}
|
||
|
||
@classmethod
|
||
def from_dict(cls, data: Optional[Dict[str, Any]]) -> "RuleScope":
|
||
if not data:
|
||
return cls()
|
||
return cls(
|
||
scope_type=str(data.get("scope_type") or "all"),
|
||
host_ids=[str(x) for x in (data.get("host_ids") or [])],
|
||
host_group=str(data.get("host_group") or ""),
|
||
service=str(data.get("service") or ""),
|
||
)
|
||
|
||
|
||
@dataclass
|
||
class MetricRule:
|
||
"""阈值规则(对应 metric_rule 表)。"""
|
||
|
||
rule_id: str
|
||
name: str
|
||
metric: str
|
||
aggregation: str = "avg" # avg | min | max | sum | last
|
||
operator: str = "gt" # gt | gte | lt | lte | eq | neq | between
|
||
threshold: float = 0.0
|
||
threshold2: Optional[float] = None
|
||
for_duration: str = "60s"
|
||
severity: str = "warning"
|
||
scope: RuleScope = field(default_factory=RuleScope)
|
||
labels: Dict[str, str] = field(default_factory=dict)
|
||
notify_channels: List[str] = field(default_factory=list)
|
||
enabled: bool = True
|
||
version: int = 0
|
||
|
||
def to_dict(self) -> Dict[str, Any]:
|
||
return {
|
||
"rule_id": self.rule_id,
|
||
"name": self.name,
|
||
"metric": self.metric,
|
||
"aggregation": self.aggregation,
|
||
"operator": self.operator,
|
||
"threshold": self.threshold,
|
||
"threshold2": self.threshold2,
|
||
"for_duration": self.for_duration,
|
||
"severity": self.severity,
|
||
"scope": self.scope.to_dict(),
|
||
"labels": dict(self.labels),
|
||
"notify_channels": list(self.notify_channels),
|
||
"enabled": self.enabled,
|
||
"version": self.version,
|
||
}
|
||
|
||
@classmethod
|
||
def from_dict(cls, data: Dict[str, Any]) -> "MetricRule":
|
||
return cls(
|
||
rule_id=str(data["rule_id"]),
|
||
name=str(data.get("name") or ""),
|
||
metric=str(data.get("metric") or ""),
|
||
aggregation=str(data.get("aggregation") or "avg"),
|
||
operator=str(data.get("operator") or "gt"),
|
||
threshold=float(data.get("threshold") or 0.0),
|
||
threshold2=float(data["threshold2"]) if data.get("threshold2") is not None else None,
|
||
for_duration=str(data.get("for_duration") or "60s"),
|
||
severity=str(data.get("severity") or "warning"),
|
||
scope=RuleScope.from_dict(data.get("scope")),
|
||
labels={str(k): str(v) for k, v in (data.get("labels") or {}).items()},
|
||
notify_channels=[str(x) for x in (data.get("notify_channels") or [])],
|
||
enabled=bool(data.get("enabled", True)),
|
||
version=int(data.get("version") or 0),
|
||
)
|
||
|
||
|
||
@dataclass
|
||
class Event:
|
||
"""检测事件(对应 event 表)。"""
|
||
|
||
event_id: str
|
||
host_id: str
|
||
rule_id: str
|
||
metric: str
|
||
agg_value: float
|
||
threshold: float
|
||
operator: str
|
||
status: str # firing | resolved
|
||
severity: str
|
||
fired_at: float
|
||
labels: Dict[str, str] = field(default_factory=dict)
|
||
|
||
def to_dict(self) -> Dict[str, Any]:
|
||
return {
|
||
"event_id": self.event_id,
|
||
"host_id": self.host_id,
|
||
"rule_id": self.rule_id,
|
||
"metric": self.metric,
|
||
"agg_value": self.agg_value,
|
||
"threshold": self.threshold,
|
||
"operator": self.operator,
|
||
"status": self.status,
|
||
"severity": self.severity,
|
||
"fired_at": self.fired_at,
|
||
"labels": self.labels,
|
||
}
|
||
|
||
|
||
@dataclass
|
||
class Alert:
|
||
"""收敛后告警(对应 alert 表)。"""
|
||
|
||
alert_id: str
|
||
dedup_key: str
|
||
aggregate_key: str
|
||
severity: str
|
||
status: str # firing | resolved
|
||
title: str = ""
|
||
detail: Dict[str, Any] = field(default_factory=dict)
|
||
count: int = 1
|
||
first_at: float = 0.0
|
||
last_at: float = 0.0
|
||
ack_status: str = "open"
|
||
ack_by: Optional[str] = None
|
||
ack_at: Optional[float] = None
|
||
|
||
def to_dict(self) -> Dict[str, Any]:
|
||
return {
|
||
"alert_id": self.alert_id,
|
||
"dedup_key": self.dedup_key,
|
||
"aggregate_key": self.aggregate_key,
|
||
"severity": self.severity,
|
||
"status": self.status,
|
||
"title": self.title,
|
||
"detail": dict(self.detail),
|
||
"count": self.count,
|
||
"first_at": self.first_at,
|
||
"last_at": self.last_at,
|
||
"ack_status": self.ack_status,
|
||
"ack_by": self.ack_by,
|
||
"ack_at": self.ack_at,
|
||
}
|