Spaces:
Running
Running
新增 CLAUDE.md 文档并优化日志管理的缓存机制
Browse files- CLAUDE.md +189 -0
- logging_helper.py +30 -3
CLAUDE.md
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# CLAUDE.md
|
| 2 |
+
|
| 3 |
+
此文件为 Claude Code (claude.ai/code) 在此存储库中工作时提供指导。
|
| 4 |
+
|
| 5 |
+
## 项目概述
|
| 6 |
+
|
| 7 |
+
**LogDisplayer** 是一个基于 FastAPI 的日志聚合和显示系统,可以从多个端点/源收集日志,将其存储在本地,并同步到 Hugging Face 数据集。它提供了一个 Web UI,用于查看和管理带有 JWT 令牌用户认证的日志。
|
| 8 |
+
|
| 9 |
+
**技术栈:**
|
| 10 |
+
- 后端:FastAPI + Uvicorn(Python 3.10+)
|
| 11 |
+
- 数据存储:Hugging Face Datasets、Pandas
|
| 12 |
+
- 云同步:Hugging Face Hub API
|
| 13 |
+
- 后台任务:APScheduler
|
| 14 |
+
- 前端:Jinja2 模板(HTML/CSS/JavaScript)
|
| 15 |
+
- 部署:Docker
|
| 16 |
+
|
| 17 |
+
## 开发设置与命令
|
| 18 |
+
|
| 19 |
+
### 前置要求
|
| 20 |
+
- Python 3.10+
|
| 21 |
+
- pip 包管理器
|
| 22 |
+
- 环境变量:`hf_token`(Hugging Face 令牌)、`SECRET_KEY`(用于 JWT 解析)
|
| 23 |
+
|
| 24 |
+
### 安装依赖
|
| 25 |
+
```bash
|
| 26 |
+
pip install -r requirements.txt
|
| 27 |
+
```
|
| 28 |
+
|
| 29 |
+
### 运行应用程序
|
| 30 |
+
```bash
|
| 31 |
+
# 标准开发运行
|
| 32 |
+
uvicorn main:app --host 0.0.0.0 --port 7860
|
| 33 |
+
|
| 34 |
+
# 带自动重载的开发运行
|
| 35 |
+
uvicorn main:app --reload --host 0.0.0.0 --port 7860
|
| 36 |
+
```
|
| 37 |
+
|
| 38 |
+
应用将在 `http://localhost:7860` 可用
|
| 39 |
+
|
| 40 |
+
### Docker 开发
|
| 41 |
+
```bash
|
| 42 |
+
# 构建 Docker 镜像
|
| 43 |
+
docker build -t log-displayer .
|
| 44 |
+
|
| 45 |
+
# 运行 Docker 容器
|
| 46 |
+
docker run -p 7860:7860 \
|
| 47 |
+
-e hf_token="your_hf_token" \
|
| 48 |
+
-e SECRET_KEY="your_secret_key" \
|
| 49 |
+
log-displayer
|
| 50 |
+
```
|
| 51 |
+
|
| 52 |
+
### 测试
|
| 53 |
+
当前没有配置正式的测试框架。手动测试脚本位于 `scratch/`:
|
| 54 |
+
- `scratch/test_dataset_to_dict.py` - 测试数据集转换
|
| 55 |
+
- `scratch/test_glob.py` - 测试文件搜索
|
| 56 |
+
|
| 57 |
+
运行手动测试:
|
| 58 |
+
```bash
|
| 59 |
+
python scratch/test_dataset_to_dict.py
|
| 60 |
+
python scratch/test_glob.py
|
| 61 |
+
```
|
| 62 |
+
|
| 63 |
+
## 架构概览
|
| 64 |
+
|
| 65 |
+
### 核心组件
|
| 66 |
+
|
| 67 |
+
**1. main.py(FastAPI 应用)**
|
| 68 |
+
- 初始化 FastAPI 应用,配置 CORS 中间件
|
| 69 |
+
- 定义 3 个主要端点:
|
| 70 |
+
- `POST /{end}` - 接受日志,包含消息体、可选的令牌头和源头
|
| 71 |
+
- `GET /healthcheck` - 健康检查端点
|
| 72 |
+
- `GET /` 或 `GET ""` - 使用所有日志渲染 HTML 模板
|
| 73 |
+
- 实例化和管理 `LoggingHelper` 实例
|
| 74 |
+
|
| 75 |
+
**2. logging_helper.py(日志管理引擎)**
|
| 76 |
+
- `LoggingHelper` 类处理所有日志持久化和同步
|
| 77 |
+
- **关键方法:**
|
| 78 |
+
- `addlog(log)` - 将日志添加到内存缓冲区
|
| 79 |
+
- `pull()` - 从 Hugging Face 下载今天的日志
|
| 80 |
+
- `push()` - 将缓冲的日志上传到 Hugging Face 数据集(标记缓存需要刷新)
|
| 81 |
+
- `push_yesterday()` - 归档昨天的日志
|
| 82 |
+
- `refresh()` - **[优化]** 返回所有日志作为排序的字典列表,使用 DataFrame 缓存机制避免重复加载
|
| 83 |
+
- `_load_all_logs()` - **[新增]** 从磁盘加载所有日志文件并合并成 DataFrame
|
| 84 |
+
- **后台同步:** 使用 APScheduler 定期推送日志(默认:60 秒间隔)
|
| 85 |
+
- **文件组织:** 日志在 HF 中组织为 `{year}/{month}/{day}/*.json`
|
| 86 |
+
- **缓冲策略:** 内存中的 Hugging Face 数据集字典,按文件路径和需要推送状态跟踪
|
| 87 |
+
- **缓存策略:** DataFrame 缓存 + 智能失效。只在 push() 完成或首次加载时重新读取磁盘文件
|
| 88 |
+
|
| 89 |
+
**3. utils.py(辅助函数)**
|
| 90 |
+
- `beijing()` - 返回 Asia/Shanghai 时区的当前时间
|
| 91 |
+
- `parse_token(token)` - 解码 JWT 令牌以提取 uid 和用户名
|
| 92 |
+
- `decode_jwt(token)` - 使用 SECRET_KEY 解码 JWT
|
| 93 |
+
- `md5(text)` - 生成 MD5 哈希(用于日志文件名)
|
| 94 |
+
- `json_to_str(obj)` - 将 JSON 转换为紧凑字符串格式
|
| 95 |
+
|
| 96 |
+
**4. static/index.html(前端模板)**
|
| 97 |
+
- 带有中文 UI 的 Jinja2 模板
|
| 98 |
+
- 显示带有排序和过滤的日志表格
|
| 99 |
+
- 显示列:类型、来源、用户、时间戳、内容
|
| 100 |
+
|
| 101 |
+
### 数据流
|
| 102 |
+
|
| 103 |
+
```
|
| 104 |
+
日志 POST 请求
|
| 105 |
+
→ main.py add_log()
|
| 106 |
+
→ parse_token() 获取用户信息
|
| 107 |
+
→ logging_helper.addlog()(添加到缓冲区)
|
| 108 |
+
→ APScheduler 每 60 秒触发 push()
|
| 109 |
+
→ logging_helper.push()(保存到本地 JSON,上传到 HF)
|
| 110 |
+
→ 设置 cache_needs_refresh = True
|
| 111 |
+
|
| 112 |
+
日志显示请求(带缓存优化)
|
| 113 |
+
→ GET / 或 GET ""
|
| 114 |
+
→ logging_helper.refresh()
|
| 115 |
+
→ 调用 push()(如无新日志,快速返回)
|
| 116 |
+
→ 检查缓存:
|
| 117 |
+
- 如果 cache_needs_refresh == True 或缓存为空 → _load_all_logs()(从磁盘加载)
|
| 118 |
+
- 否则 → 直接返回缓存的 DataFrame
|
| 119 |
+
→ 返回排序的字典列表
|
| 120 |
+
→ Jinja2 渲染 HTML 模板
|
| 121 |
+
```
|
| 122 |
+
|
| 123 |
+
### 环境变量
|
| 124 |
+
|
| 125 |
+
必需:
|
| 126 |
+
- `hf_token` - Hugging Face API 令牌,用于认证
|
| 127 |
+
- `SECRET_KEY` - 用于 JWT 解码的密钥(用于解析用户令牌)
|
| 128 |
+
|
| 129 |
+
### 关键设计模式
|
| 130 |
+
|
| 131 |
+
1. **两级缓冲:** 内存缓冲 + 磁盘存储。日志在 Python 对象中缓冲,定期写入 JSON,然后推送到 Hugging Face。
|
| 132 |
+
2. **基于日期的组织:** 日志自动组织到年/月/日目录中,便于归档数据管理。
|
| 133 |
+
3. **后台同步:** APScheduler 确保定期推送日志,而不会阻止主请求处理程序。
|
| 134 |
+
4. **无状态端点:** 每个请求都是独立的;用户信息在每次调用时从 JWT 令牌中提取。
|
| 135 |
+
5. **DataFrame 缓存(性能优化):** `refresh()` 方法缓存合并后的 DataFrame。只有在 `push()` 完成后才重新加载磁盘文件,避免每次刷新都重复读取和解析所有 JSON 文件。
|
| 136 |
+
|
| 137 |
+
## 重要文件与职责
|
| 138 |
+
|
| 139 |
+
| 文件 | 行数 | 用途 |
|
| 140 |
+
|------|------|------|
|
| 141 |
+
| [main.py](main.py) | 74 | FastAPI 应用初始化、端点定义 |
|
| 142 |
+
| [logging_helper.py](logging_helper.py) | 235 | 核心日志持久化、缓冲、HF 同步和缓存机制 |
|
| 143 |
+
| [utils.py](utils.py) | 64 | 时区、JWT 解析、哈希工具函数 |
|
| 144 |
+
| [static/index.html](static/index.html) | ~400 | Jinja2 Web UI 模板 |
|
| 145 |
+
| [requirements.txt](requirements.txt) | 10 | Python 依赖 |
|
| 146 |
+
| [Dockerfile](Dockerfile) | - | Docker 镜像定义 |
|
| 147 |
+
| [data/logs/](data/logs/) | - | 本地日志文件存储 |
|
| 148 |
+
|
| 149 |
+
## 性能优化说明
|
| 150 |
+
|
| 151 |
+
### 首页刷新优化(v1.1)
|
| 152 |
+
|
| 153 |
+
**问题:** 之前每次刷新首页都需要从磁盘重新加载所有 JSON 日志文件,在日志数量较多时会导致加载时间过长。
|
| 154 |
+
|
| 155 |
+
**解决方案:** 实现了 DataFrame 缓存机制。
|
| 156 |
+
|
| 157 |
+
**具体改进:**
|
| 158 |
+
|
| 159 |
+
1. **DataFrame 内存缓存** - 在 LoggingHelper 中添加 `cached_df` 变量存储合并后的 DataFrame
|
| 160 |
+
2. **智能缓存失效** - 只有在调用 `push()` 方法写入新日志到磁盘后,才设置 `cache_needs_refresh = True` 标记
|
| 161 |
+
3. **增量加载** - 新增 `_load_all_logs()` 私有方法,只在必要时(首次加载或 push 完成后)从磁盘重新加载数据
|
| 162 |
+
|
| 163 |
+
**性能改进:**
|
| 164 |
+
- **首次刷新:** 需要加载所有 JSON 文件(不可避免)
|
| 165 |
+
- **后续刷新(无新日志):** 直接返回缓存,避免磁盘 I/O,响应时间从秒级降低到毫秒级
|
| 166 |
+
- **后续刷新(有新日志):** push() 完成后重新加载,但由于 push() 已经处理完新日志,只需一次加载即可
|
| 167 |
+
|
| 168 |
+
**相关代码变更:**
|
| 169 |
+
- [logging_helper.py:43-45](logging_helper.py#L43-L45) - 添加缓存变量初始化
|
| 170 |
+
- [logging_helper.py:172](logging_helper.py#L172) - push() 方法中标记缓存失效
|
| 171 |
+
- [logging_helper.py:199-216](logging_helper.py#L199-L216) - 新增 _load_all_logs() 方法
|
| 172 |
+
- [logging_helper.py:218-234](logging_helper.py#L218-L234) - 优化后的 refresh() 方法
|
| 173 |
+
|
| 174 |
+
## 常见开发任务
|
| 175 |
+
|
| 176 |
+
### 添加新的日志类型
|
| 177 |
+
1. POST 到 `/{end}`,其中 `{end}` 是日志类型(例如 `/web`、`/mobile`、`/api`)
|
| 178 |
+
2. LoggingHelper 自动在缓冲区中创建新条目,按日期组织
|
| 179 |
+
|
| 180 |
+
### 调试日志
|
| 181 |
+
- 查看 uvicorn 控制台输出,了解 add_log() 和 push() 中的打印语句
|
| 182 |
+
- 查看 `data/logs/{year}/{month}/{day}/` 中的本地 JSON 文件以获取存储的日志
|
| 183 |
+
- 检查 `data/logs/` 中下载的 HF 数据集
|
| 184 |
+
|
| 185 |
+
### 修改同步间隔
|
| 186 |
+
在 `logging_helper.py` 初始化(main.py 第 25-28 行)中调整 `synchronize_interval` 参数(以秒为单位)
|
| 187 |
+
|
| 188 |
+
### 扩展 JWT 有效负载
|
| 189 |
+
修改 utils.py 中的 `parse_token()` 以从 JWT 有效负载中提取其他字段,然后更新 main.py 中 add_log() 中的日志架构
|
logging_helper.py
CHANGED
|
@@ -39,6 +39,10 @@ class LoggingHelper:
|
|
| 39 |
self.today = beijing().date()
|
| 40 |
ds.disable_progress_bar()
|
| 41 |
self.dataframe: pd.DataFrame
|
|
|
|
|
|
|
|
|
|
|
|
|
| 42 |
self.pull()
|
| 43 |
self.start_synchronize()
|
| 44 |
|
|
@@ -164,6 +168,8 @@ class LoggingHelper:
|
|
| 164 |
self.need_push[filename] = False
|
| 165 |
print(f"[push] Log files pushed to {res}")
|
| 166 |
print("[push] Done")
|
|
|
|
|
|
|
| 167 |
return True
|
| 168 |
except Exception as e:
|
| 169 |
print(f"[push] {type(e)}: {e}")
|
|
@@ -190,8 +196,9 @@ class LoggingHelper:
|
|
| 190 |
)
|
| 191 |
self.scheduler.start()
|
| 192 |
|
| 193 |
-
def
|
| 194 |
-
|
|
|
|
| 195 |
files = glob.glob("**/*.json", root_dir=self.local_dir, recursive=True)
|
| 196 |
filepathes = [os.sep.join([self.local_dir, file]) for file in files]
|
| 197 |
datasets = []
|
|
@@ -204,4 +211,24 @@ class LoggingHelper:
|
|
| 204 |
df = dataset.to_pandas()
|
| 205 |
assert isinstance(df, pd.DataFrame)
|
| 206 |
df = df.sort_values(by="timestamp", ascending=False)
|
| 207 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 39 |
self.today = beijing().date()
|
| 40 |
ds.disable_progress_bar()
|
| 41 |
self.dataframe: pd.DataFrame
|
| 42 |
+
# 缓存相关变量
|
| 43 |
+
self.cached_df: pd.DataFrame | None = None
|
| 44 |
+
self.loaded_files: set[str] = set()
|
| 45 |
+
self.cache_needs_refresh = False
|
| 46 |
self.pull()
|
| 47 |
self.start_synchronize()
|
| 48 |
|
|
|
|
| 168 |
self.need_push[filename] = False
|
| 169 |
print(f"[push] Log files pushed to {res}")
|
| 170 |
print("[push] Done")
|
| 171 |
+
# 标记缓存需要刷新
|
| 172 |
+
self.cache_needs_refresh = True
|
| 173 |
return True
|
| 174 |
except Exception as e:
|
| 175 |
print(f"[push] {type(e)}: {e}")
|
|
|
|
| 196 |
)
|
| 197 |
self.scheduler.start()
|
| 198 |
|
| 199 |
+
def _load_all_logs(self) -> pd.DataFrame:
|
| 200 |
+
"""加载所有日志文件并返回合并后的DataFrame"""
|
| 201 |
+
print("[_load_all_logs] Starting to load all logs")
|
| 202 |
files = glob.glob("**/*.json", root_dir=self.local_dir, recursive=True)
|
| 203 |
filepathes = [os.sep.join([self.local_dir, file]) for file in files]
|
| 204 |
datasets = []
|
|
|
|
| 211 |
df = dataset.to_pandas()
|
| 212 |
assert isinstance(df, pd.DataFrame)
|
| 213 |
df = df.sort_values(by="timestamp", ascending=False)
|
| 214 |
+
print(f"[_load_all_logs] Loaded {len(df)} logs")
|
| 215 |
+
self.loaded_files = set(files)
|
| 216 |
+
return df
|
| 217 |
+
|
| 218 |
+
def refresh(self) -> list[dict]:
|
| 219 |
+
"""获取刷新后的日志列表,使用缓存机制加速"""
|
| 220 |
+
self.push()
|
| 221 |
+
|
| 222 |
+
# 如果缓存需要刷新或者缓存为空,重新加载所有日志
|
| 223 |
+
if self.cache_needs_refresh or self.cached_df is None:
|
| 224 |
+
print("[refresh] Cache miss, reloading all logs")
|
| 225 |
+
self.cached_df = self._load_all_logs()
|
| 226 |
+
self.cache_needs_refresh = False
|
| 227 |
+
else:
|
| 228 |
+
print("[refresh] Using cached data")
|
| 229 |
+
|
| 230 |
+
# 返回缓存的DataFrame
|
| 231 |
+
if self.cached_df is None or self.cached_df.empty:
|
| 232 |
+
return []
|
| 233 |
+
|
| 234 |
+
return self.cached_df.to_dict(orient="records")
|