develop: 重做 fault-log-analyzer 分析模块(遵守模块开发规范,补齐根因分析 naive/aware 时间归一化与规范交付物)

This commit is contained in:
Pipeline Agent 2026-08-15 12:29:53 +08:00
parent 0ef53fe493
commit f077188ebb
3 changed files with 36 additions and 7 deletions

View File

@ -38,7 +38,7 @@ src/fault_log_analyzer/
# 安装(核心零依赖,可选依赖见 requirements.txt) # 安装(核心零依赖,可选依赖见 requirements.txt)
pip install -e . pip install -e .
# 运行单元测试(131 个测试用例) # 运行单元测试(133 个测试用例)
python -m unittest discover -s tests -v python -m unittest discover -s tests -v
# 启动服务(内存存储,便于本地验证) # 启动服务(内存存储,便于本地验证)
@ -53,12 +53,13 @@ curl http://127.0.0.1:8080/api/v1/fault-logs
```bash ```bash
python -m unittest discover -s tests -v python -m unittest discover -s tests -v
# Ran 131 tests ... OK # Ran 133 tests ... OK
``` ```
测试覆盖:配置加载、数据模型、日志解析/模板化、故障过滤规则、MinHash 指纹去重、 测试覆盖:配置加载、数据模型、日志解析/模板化、故障过滤规则、MinHash 指纹去重、
TF-IDF/DBSCAN 聚类、故障归类、根因分析、捕获管道、聚类/根因 worker、REST API、 TF-IDF/DBSCAN 聚类、故障归类、根因分析(含 naive/aware 时间归一化)、捕获管道、
以及 Kafka/Elasticsearch/MySQL/Redis 可选后端适配器(未安装依赖时的回退行为与索引名)。 聚类/根因 worker、REST API,以及 Kafka/Elasticsearch/MySQL/Redis 可选后端适配器
(未安装依赖时的回退行为与索引名)。
## 环境变量(生产) ## 环境变量(生产)

View File

@ -13,7 +13,7 @@ from __future__ import annotations
import math import math
import re import re
from datetime import timedelta from datetime import datetime, timedelta, timezone
from typing import Dict, List, Optional, Sequence from typing import Dict, List, Optional, Sequence
from .models import Event, FaultLog, RootCause from .models import Event, FaultLog, RootCause
@ -44,6 +44,18 @@ def _op_ok(operator: str, value: float, threshold: float) -> bool:
return False return False
def _as_aware(dt: datetime) -> datetime:
"""将 naive datetime 统一转为 UTC aware,避免跨模块时间比较报错。
上游模块(如 threshold-event-detector)可能产出不带时区的 datetime,
而捕获管道内 fault_log.occurred_at 已被规范化为带时区时间;这里做一次
防御性归一,保证根因分析的时间窗口计算稳健。
"""
if dt.tzinfo is None:
return dt.replace(tzinfo=timezone.utc)
return dt
class RootCauseAnalyzer: class RootCauseAnalyzer:
"""根据故障日志与近期事件进行根因分析。""" """根据故障日志与近期事件进行根因分析。"""
@ -57,14 +69,14 @@ class RootCauseAnalyzer:
related_logs: Optional[Sequence[FaultLog]] = None, related_logs: Optional[Sequence[FaultLog]] = None,
) -> RootCause: ) -> RootCause:
related_logs = list(related_logs or []) related_logs = list(related_logs or [])
now = fault_log.occurred_at now = _as_aware(fault_log.occurred_at)
window = timedelta(minutes=self.window_minutes) window = timedelta(minutes=self.window_minutes)
host_events = [ host_events = [
e e
for e in events for e in events
if e.host_id == fault_log.host_id if e.host_id == fault_log.host_id
and abs((e.fired_at - now).total_seconds()) <= window.total_seconds() and abs((_as_aware(e.fired_at) - now).total_seconds()) <= window.total_seconds()
] ]
evidence: List[Dict] = [] evidence: List[Dict] = []

View File

@ -67,6 +67,22 @@ class TestMetricEvents(unittest.TestCase):
self.assertEqual(rc.cause_type, "memory_exhausted") self.assertEqual(rc.cause_type, "memory_exhausted")
class TestNaiveDatetimeRobustness(unittest.TestCase):
"""跨模块时间比较防御性归一:上游可能传入 naive datetime。"""
def test_naive_fault_log_and_naive_event(self):
naive = datetime(2025, 1, 1, 10, 18, 0)
log = _log("no space left on device", at=naive)
event = _event(value=97.2, at=naive)
rc = RootCauseAnalyzer().analyze(log, [event])
self.assertEqual(rc.cause_type, "disk_full")
def test_naive_event_with_aware_log(self):
naive_event = _event(value=97.2, at=datetime(2025, 1, 1, 10, 18, 0))
rc = RootCauseAnalyzer().analyze(_log("some error"), [naive_event])
self.assertEqual(rc.cause_type, "disk_full")
class TestTraceAssociation(unittest.TestCase): class TestTraceAssociation(unittest.TestCase):
def test_trace_association(self): def test_trace_association(self):
log = _log("request failed", trace="tr-1") log = _log("request failed", trace="tr-1")