"""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