Some checks failed
CD Pipeline / deploy (push) Has been cancelled
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>
129 lines
4.4 KiB
Python
129 lines
4.4 KiB
Python
"""ADR-019 Phase 6:每日資料新鮮度主動巡檢。
|
||
|
||
每天 09:00 由 OpenClawBot scheduler 觸發,檢查 realtime_sales_monthly 最新資料
|
||
日期,若落後超過閾值就透過 EventRouter 主動發 Telegram 警告,避免用戶月初/
|
||
ETL 卡住才靠按按鈕踩坑發現。
|
||
|
||
EventRouter severity 對應:
|
||
- gap == 0:今日資料已進,靜默不發
|
||
- gap == 1:昨日資料齊全,info(不發或只 log)
|
||
- gap == 2~3:warning,可能 ETL 跑慢
|
||
- gap >= 4:alert(P2),ETL 大概率出問題
|
||
"""
|
||
|
||
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
|