Beracles commited on
Commit
83705b6
·
1 Parent(s): 082ede4

新增 CLAUDE.md 文档并优化日志管理的缓存机制

Browse files
Files changed (2) hide show
  1. CLAUDE.md +189 -0
  2. 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 refresh(self) -> list[dict]:
194
- self.push()
 
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
- return df.to_dict(orient="records")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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")