From f077188ebb63dba63fc0d1f2d52f8b3205182f5a Mon Sep 17 00:00:00 2001 From: Pipeline Agent Date: Sat, 15 Aug 2026 12:29:53 +0800 Subject: [PATCH] =?UTF-8?q?develop:=20=E9=87=8D=E5=81=9A=20fault-log-analy?= =?UTF-8?q?zer=20=E5=88=86=E6=9E=90=E6=A8=A1=E5=9D=97=EF=BC=88=E9=81=B5?= =?UTF-8?q?=E5=AE=88=E6=A8=A1=E5=9D=97=E5=BC=80=E5=8F=91=E8=A7=84=E8=8C=83?= =?UTF-8?q?=EF=BC=8C=E8=A1=A5=E9=BD=90=E6=A0=B9=E5=9B=A0=E5=88=86=E6=9E=90?= =?UTF-8?q?=20naive/aware=20=E6=97=B6=E9=97=B4=E5=BD=92=E4=B8=80=E5=8C=96?= =?UTF-8?q?=E4=B8=8E=E8=A7=84=E8=8C=83=E4=BA=A4=E4=BB=98=E7=89=A9=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 9 +++++---- src/fault_log_analyzer/root_cause.py | 18 +++++++++++++++--- tests/test_root_cause.py | 16 ++++++++++++++++ 3 files changed, 36 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 7a22b7e..a38458d 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ src/fault_log_analyzer/ # 安装(核心零依赖,可选依赖见 requirements.txt) pip install -e . -# 运行单元测试(131 个测试用例) +# 运行单元测试(133 个测试用例) python -m unittest discover -s tests -v # 启动服务(内存存储,便于本地验证) @@ -53,12 +53,13 @@ curl http://127.0.0.1:8080/api/v1/fault-logs ```bash python -m unittest discover -s tests -v -# Ran 131 tests ... OK +# Ran 133 tests ... OK ``` 测试覆盖:配置加载、数据模型、日志解析/模板化、故障过滤规则、MinHash 指纹去重、 -TF-IDF/DBSCAN 聚类、故障归类、根因分析、捕获管道、聚类/根因 worker、REST API、 -以及 Kafka/Elasticsearch/MySQL/Redis 可选后端适配器(未安装依赖时的回退行为与索引名)。 +TF-IDF/DBSCAN 聚类、故障归类、根因分析(含 naive/aware 时间归一化)、捕获管道、 +聚类/根因 worker、REST API,以及 Kafka/Elasticsearch/MySQL/Redis 可选后端适配器 +(未安装依赖时的回退行为与索引名)。 ## 环境变量(生产) diff --git a/src/fault_log_analyzer/root_cause.py b/src/fault_log_analyzer/root_cause.py index 77d27af..3ac4636 100644 --- a/src/fault_log_analyzer/root_cause.py +++ b/src/fault_log_analyzer/root_cause.py @@ -13,7 +13,7 @@ from __future__ import annotations import math import re -from datetime import timedelta +from datetime import datetime, timedelta, timezone from typing import Dict, List, Optional, Sequence from .models import Event, FaultLog, RootCause @@ -44,6 +44,18 @@ def _op_ok(operator: str, value: float, threshold: float) -> bool: 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: """根据故障日志与近期事件进行根因分析。""" @@ -57,14 +69,14 @@ class RootCauseAnalyzer: related_logs: Optional[Sequence[FaultLog]] = None, ) -> RootCause: related_logs = list(related_logs or []) - now = fault_log.occurred_at + now = _as_aware(fault_log.occurred_at) window = timedelta(minutes=self.window_minutes) host_events = [ e for e in events 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] = [] diff --git a/tests/test_root_cause.py b/tests/test_root_cause.py index fee6d87..e0669d1 100644 --- a/tests/test_root_cause.py +++ b/tests/test_root_cause.py @@ -67,6 +67,22 @@ class TestMetricEvents(unittest.TestCase): 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): def test_trace_association(self): log = _log("request failed", trace="tr-1")