Spaces:
Running
Running
| """ | |
| 错误快照模块 | |
| 保存请求错误的完整上下文,便于调试和问题排查。 | |
| 特性: | |
| - 分层目录结构 | |
| - 完整的请求/响应链路记录 | |
| - 自动清理旧快照 | |
| - 与主日志系统集成 | |
| """ | |
| import json | |
| import shutil | |
| from datetime import datetime, timedelta | |
| from typing import Any, cast | |
| from pathlib import Path | |
| class ErrorSnapshotManager: | |
| """错误快照管理器""" | |
| def __init__( | |
| self, | |
| base_dir: str = "errors", | |
| max_snapshots: int = 100, | |
| max_age_days: int = 7, | |
| compress_old: bool = True | |
| ): | |
| """ | |
| 初始化错误快照管理器 | |
| Args: | |
| base_dir: 快照存储基础目录 | |
| max_snapshots: 最大保留快照数量 | |
| max_age_days: 快照最大保留天数 | |
| compress_old: 是否压缩旧快照 | |
| """ | |
| self.base_dir = Path(base_dir) | |
| self.max_snapshots = max_snapshots | |
| self.max_age_days = max_age_days | |
| self.compress_old = compress_old | |
| # 确保目录存在 | |
| self.base_dir.mkdir(parents=True, exist_ok=True) | |
| def save_snapshot( | |
| self, | |
| downstream_payload: dict[str, Any], | |
| upstream_payload: dict[str, Any], | |
| upstream_response: str, | |
| error_type: str = "request_error", | |
| metadata: dict[str, Any] | None = None | |
| ) -> str | None: | |
| """ | |
| 保存错误快照 | |
| Args: | |
| downstream_payload: 接收到的原始请求 | |
| upstream_payload: 发送给上游的请求 | |
| upstream_response: 上游返回的响应 | |
| error_type: 错误类型 | |
| metadata: 额外元数据 | |
| Returns: | |
| 快照目录路径,失败返回 None | |
| """ | |
| try: | |
| # 创建时间戳目录 | |
| timestamp = datetime.now() | |
| date_dir = timestamp.strftime("%Y-%m-%d") | |
| time_str = timestamp.strftime("%H%M%S_%f") | |
| # 目录结构: errors/2024-01-21/http_400_143052_123456/ | |
| snapshot_name = f"{error_type}_{time_str}" | |
| snapshot_dir = self.base_dir / date_dir / snapshot_name | |
| snapshot_dir.mkdir(parents=True, exist_ok=True) | |
| # 1. 保存下游请求 | |
| self._save_json( | |
| snapshot_dir / "1_downstream_request.json", | |
| downstream_payload | |
| ) | |
| # 2. 保存上游请求 | |
| self._save_json( | |
| snapshot_dir / "2_upstream_request.json", | |
| upstream_payload | |
| ) | |
| # 3. 保存上游响应 | |
| resp_ext = self._save_response( | |
| snapshot_dir / "3_upstream_response", | |
| upstream_response | |
| ) | |
| # 4. 保存摘要 | |
| summary: dict[str, Any] = { | |
| "timestamp": timestamp.isoformat(), | |
| "error_type": error_type, | |
| "snapshot_id": snapshot_name, | |
| "files": [ | |
| "1_downstream_request.json", | |
| "2_upstream_request.json", | |
| f"3_upstream_response{resp_ext}" | |
| ], | |
| "metadata": metadata or {} | |
| } | |
| self._save_json(snapshot_dir / "summary.json", summary) | |
| # 清理旧快照 | |
| self._cleanup_old_snapshots() | |
| return str(snapshot_dir) | |
| except Exception: | |
| return None | |
| def _save_json(self, path: Path, data: Any): | |
| """保存 JSON 文件""" | |
| with open(path, 'w', encoding='utf-8') as f: | |
| if isinstance(data, (dict, list)): | |
| json.dump(data, f, ensure_ascii=False, indent=2) | |
| else: | |
| f.write(str(data)) | |
| def _save_response(self, path_base: Path, response: str) -> str: | |
| """保存响应内容,自动检测格式,返回文件后缀""" | |
| if self._is_json(response): | |
| try: | |
| data = json.loads(response) | |
| self._save_json(path_base.with_suffix('.json'), data) | |
| return ".json" | |
| except json.JSONDecodeError: | |
| pass | |
| # 保存为文本 | |
| with open(path_base.with_suffix('.txt'), 'w', encoding='utf-8') as f: | |
| f.write(str(response)) | |
| return ".txt" | |
| def _is_json(self, text: str) -> bool: | |
| """检查是否为 JSON 格式""" | |
| # Pylance: text 已经是 str,不需要 isinstance 检查 | |
| # 但为了运行时健壮性,如果 text 可能为 None 或其他类型,可以保留, | |
| # 但既然类型提示是 str,我们假设它就是 str。 | |
| # 如果调用者可能传 None,类型提示应该是 Optional[str] | |
| # 兼容性处理:如果 text 为空 | |
| if not text: | |
| return False | |
| # 安全转换,防止传入了非字符串 | |
| text_str = str(text).strip() | |
| return text_str.startswith('{') or text_str.startswith('[') | |
| def _cleanup_old_snapshots(self): | |
| """清理旧快照""" | |
| try: | |
| # 获取所有日期目录 | |
| date_dirs = sorted( | |
| [d for d in self.base_dir.iterdir() if d.is_dir()], | |
| key=lambda x: x.name, | |
| reverse=True | |
| ) | |
| # 按日期清理 | |
| cutoff_date = datetime.now() - timedelta(days=self.max_age_days) | |
| cutoff_str = cutoff_date.strftime("%Y-%m-%d") | |
| for date_dir in date_dirs: | |
| if date_dir.name < cutoff_str: | |
| # 删除过期目录 | |
| shutil.rmtree(date_dir) | |
| # 按数量清理 | |
| all_snapshots: list[dict[str, Any]] = [] | |
| for date_dir in self.base_dir.iterdir(): | |
| if date_dir.is_dir(): | |
| for snapshot_dir in date_dir.iterdir(): | |
| if snapshot_dir.is_dir(): | |
| summary_file = snapshot_dir / "summary.json" | |
| if summary_file.exists(): | |
| try: | |
| with open(summary_file, 'r') as f: | |
| summary = json.load(f) | |
| all_snapshots.append({ | |
| 'path': snapshot_dir, | |
| 'timestamp': str(summary.get('timestamp', '')) | |
| }) | |
| except Exception: | |
| pass | |
| # 按时间排序,删除超出数量限制的 | |
| # 显式指定 key 函数的返回类型 | |
| def sort_key(x: dict[str, Any]) -> str: | |
| return str(x['timestamp']) | |
| all_snapshots.sort(key=sort_key, reverse=True) | |
| for snapshot in all_snapshots[self.max_snapshots:]: | |
| snapshot_path = cast(Path, snapshot['path']) | |
| shutil.rmtree(snapshot_path) | |
| # 清理空的日期目录 | |
| for date_dir in self.base_dir.iterdir(): | |
| if date_dir.is_dir() and not any(date_dir.iterdir()): | |
| date_dir.rmdir() | |
| except Exception: | |
| pass | |
| def list_snapshots( | |
| self, | |
| error_type: str | None = None, | |
| limit: int = 20 | |
| ) -> list[dict[str, Any]]: | |
| """ | |
| 列出快照 | |
| Args: | |
| error_type: 过滤错误类型 | |
| limit: 返回数量限制 | |
| Returns: | |
| 快照摘要列表 | |
| """ | |
| snapshots: list[dict[str, Any]] = [] | |
| try: | |
| for date_dir in sorted(self.base_dir.iterdir(), reverse=True): | |
| if not date_dir.is_dir(): | |
| continue | |
| for snapshot_dir in sorted(date_dir.iterdir(), reverse=True): | |
| if not snapshot_dir.is_dir(): | |
| continue | |
| summary_file = snapshot_dir / "summary.json" | |
| if not summary_file.exists(): | |
| continue | |
| try: | |
| with open(summary_file, 'r') as f: | |
| summary = json.load(f) | |
| if error_type and summary.get('error_type') != error_type: | |
| continue | |
| summary['path'] = str(snapshot_dir) | |
| snapshots.append(summary) | |
| if len(snapshots) >= limit: | |
| return snapshots | |
| except Exception: | |
| continue | |
| except Exception: | |
| pass | |
| return snapshots | |
| def get_snapshot(self, snapshot_path: str) -> dict[str, Any] | None: | |
| """ | |
| 获取完整快照内容 | |
| Args: | |
| snapshot_path: 快照目录路径 | |
| Returns: | |
| 快照完整内容 | |
| """ | |
| try: | |
| snapshot_dir = Path(snapshot_path) | |
| if not snapshot_dir.exists(): | |
| return None | |
| result: dict[str, Any] = {} | |
| for file in snapshot_dir.iterdir(): | |
| if file.suffix == '.json': | |
| with open(file, 'r', encoding='utf-8') as f: | |
| result[file.stem] = json.load(f) | |
| elif file.suffix == '.txt': | |
| with open(file, 'r', encoding='utf-8') as f: | |
| result[file.stem] = f.read() | |
| return result | |
| except Exception: | |
| return None | |
| # ==================== 全局实例 ==================== | |
| _snapshot_manager: ErrorSnapshotManager | None = None | |
| def _get_manager() -> ErrorSnapshotManager: | |
| """获取或创建快照管理器""" | |
| global _snapshot_manager | |
| if _snapshot_manager is None: | |
| try: | |
| from src.core.config import load_config | |
| config = load_config() | |
| base_dir = config.get("error_dir", "errors") | |
| except Exception: | |
| base_dir = "errors" | |
| _snapshot_manager = ErrorSnapshotManager(base_dir=base_dir) | |
| return _snapshot_manager | |
| # ==================== 便捷函数 ==================== | |
| def save_error_snapshot( | |
| downstream_payload: dict[str, Any], | |
| upstream_payload: dict[str, Any], | |
| upstream_response: str, | |
| error_type: str = "request_error" | |
| ) -> str | None: | |
| """ | |
| 保存请求错误快照 | |
| 这是主要的对外接口,保持与原有 API 兼容。 | |
| Args: | |
| downstream_payload: 接收到的原始请求 (Gemini 格式) | |
| upstream_payload: 最终发送给 Google 的 Payload | |
| upstream_response: 上游返回的原始错误信息 | |
| error_type: 错误类别 (如 http_400, transform_error 等) | |
| Returns: | |
| 快照目录路径,失败返回 None | |
| """ | |
| manager = _get_manager() | |
| return manager.save_snapshot( | |
| downstream_payload=downstream_payload, | |
| upstream_payload=upstream_payload, | |
| upstream_response=upstream_response, | |
| error_type=error_type | |
| ) | |
| def list_error_snapshots( | |
| error_type: str | None = None, | |
| limit: int = 20 | |
| ) -> list[dict[str, Any]]: | |
| """ | |
| 列出错误快照 | |
| Args: | |
| error_type: 过滤错误类型 | |
| limit: 返回数量限制 | |
| Returns: | |
| 快照摘要列表 | |
| """ | |
| manager = _get_manager() | |
| return manager.list_snapshots(error_type=error_type, limit=limit) | |
| def get_error_snapshot(snapshot_path: str) -> dict[str, Any] | None: | |
| """ | |
| 获取完整错误快照 | |
| Args: | |
| snapshot_path: 快照目录路径 | |
| Returns: | |
| 快照完整内容 | |
| """ | |
| manager = _get_manager() | |
| return manager.get_snapshot(snapshot_path) | |