Files
ewoooc/services/data_freshness_probe.py
OoO e12e6a8f96
Some checks failed
CD Pipeline / deploy (push) Has been cancelled
feat(telegram): ADR-019 Phase 6 - daily data freshness probe + cron 09:05
ADR-019 Phase 6:每日 09:05 OpenClawBot scheduler 主動巡檢 realtime_sales_monthly
最新資料日期,落後超過閾值時透過 EventRouter 發 Telegram 警告。

新增 services/data_freshness_probe.py:
- gap == 0:靜默不發(資料齊全)
- gap == 1:info(昨日資料齊,正常)
- gap == 2~3:warning
- gap >= 4:alert(P2,ETL 大概率出問題)
- latest_date 取不到:alert(DB 連線異常)

routes/openclaw_bot_routes.py 加 cron job openclaw_data_freshness_probe
(hour=9 minute=5,避開 09:00 的其他既有 job)。

從『用戶月初按按鈕踩坑才發現資料缺口』的被動模式,轉成 agent 主動巡檢通知。
配合 Phase 1(PPT freshness gate)+ Phase 2(agent tool)+ Phase 3(cmd 路徑
agent dispatch)+ Phase 4(對話 state),Telegram Bot 互動層的『rigid default
+ 靜默空白』反模式根除。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 13:07:30 +08:00

129 lines
4.4 KiB
Python
Raw 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.
"""ADR-019 Phase 6每日資料新鮮度主動巡檢。
每天 09:00 由 OpenClawBot scheduler 觸發,檢查 realtime_sales_monthly 最新資料
日期,若落後超過閾值就透過 EventRouter 主動發 Telegram 警告,避免用戶月初/
ETL 卡住才靠按按鈕踩坑發現。
EventRouter severity 對應:
- gap == 0今日資料已進靜默不發
- gap == 1昨日資料齊全info不發或只 log
- gap == 2~3warning可能 ETL 跑慢
- gap >= 4alertP2ETL 大概率出問題
"""
from __future__ import annotations
from datetime import datetime, date
import logging
from services.event_router import dispatch_sync
logger = logging.getLogger("DataFreshnessProbe")
_DEFAULT_WARN_GAP_DAYS = 2
_DEFAULT_ALERT_GAP_DAYS = 4
def _query_latest_date():
"""獨立 import 避免循環依賴。回傳 'YYYY-MM-DD' 字串或 None。"""
try:
from routes.openclaw_bot_routes import latest_date
return latest_date()
except Exception as e:
logger.warning(f"[DataFreshnessProbe] 取得 latest_date 失敗:{e}")
return None
def _parse_date(s: str) -> date | None:
if not s:
return None
try:
return datetime.strptime(s.replace('/', '-'), '%Y-%m-%d').date()
except (ValueError, AttributeError):
return None
def run_data_freshness_probe(
warn_gap_days: int = _DEFAULT_WARN_GAP_DAYS,
alert_gap_days: int = _DEFAULT_ALERT_GAP_DAYS,
) -> dict:
"""主動巡檢一次,缺資料時透過 EventRouter 發通知。回傳 probe 結果摘要。"""
today = date.today()
latest_str = _query_latest_date()
latest_dt = _parse_date(latest_str)
result = {
'today': today.isoformat(),
'latest_date': latest_str,
'gap_days': None,
'severity': 'unknown',
'notified': False,
}
if latest_dt is None:
logger.error("[DataFreshnessProbe] 無法取得 latest_date — 視為嚴重異常")
result['severity'] = 'alert'
try:
dispatch_sync({
'source': 'OpenClawBot.DataFreshnessProbe',
'event_type': 'data_freshness_unknown',
'severity': 'alert',
'title': '⚠️ 資料新鮮度巡檢失敗',
'status': '無法取得 latest_date',
'impact': 'P2 — 業績資料源不可達,所有報表 / NL 查詢可能失準',
'summary': '請檢查 realtime_sales_monthly 表與 DB 連線。',
})
result['notified'] = True
except Exception as e:
logger.error(f"[DataFreshnessProbe] EventRouter dispatch 失敗:{e}")
return result
gap = (today - latest_dt).days
result['gap_days'] = gap
if gap <= 0:
result['severity'] = 'success'
logger.info(f"[DataFreshnessProbe] 資料齊全latest={latest_dt}, today={today}")
return result
if gap == 1:
result['severity'] = 'info'
logger.info(f"[DataFreshnessProbe] 落後 1 天正常作息ETL 通常隔天匯入)")
return result
if gap < warn_gap_days:
result['severity'] = 'info'
return result
severity = 'alert' if gap >= alert_gap_days else 'warning'
result['severity'] = severity
try:
dispatch_sync({
'source': 'OpenClawBot.DataFreshnessProbe',
'event_type': 'data_freshness_lag',
'severity': severity,
'title': f'📊 業績資料落後 {gap}',
'status': f'最新資料:{latest_str}(今日:{today.isoformat()}',
'impact': (
f'P2 — 月報/日報 PPT 與 NL 業績查詢可能空白或失準,'
f'請檢查 realtime_sales_monthly ETL 任務run_momo_task / run_auto_import_task'
if severity == 'alert' else
f'P3 — 資料落後 {gap} 天,請確認 ETL 是否正常排程'
),
'summary': f'gap={gap}d, latest={latest_str}',
'payload': {
'gap_days': gap,
'latest_date': latest_str,
'probe_at': datetime.now().isoformat(timespec='seconds'),
},
})
result['notified'] = True
logger.warning(
f"[DataFreshnessProbe] {severity}: gap={gap}d, latest={latest_str} → notified"
)
except Exception as e:
logger.error(f"[DataFreshnessProbe] EventRouter dispatch 失敗:{e}")
return result