Beracles commited on
Commit
88be5d0
·
1 Parent(s): 83705b6

优化日志加载功能,支持日期范围过滤并增强前端日期选择器

Browse files
Files changed (3) hide show
  1. logging_helper.py +109 -20
  2. main.py +22 -3
  3. static/index.html +242 -9
logging_helper.py CHANGED
@@ -196,39 +196,128 @@ class LoggingHelper:
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 = []
205
  for path in tqdm(filepathes):
206
  path = str(path)
207
- datasets.append(ds.Dataset.from_json(path))
 
 
 
 
 
 
208
  df = pd.DataFrame()
209
  if datasets:
210
  dataset: ds.Dataset = ds.concatenate_datasets(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")
 
 
 
 
 
 
 
 
196
  )
197
  self.scheduler.start()
198
 
199
+ def _load_all_logs(self, from_date=None, to_date=None) -> pd.DataFrame:
200
+ """
201
+ 加载日志文件并返回合并后的DataFrame
202
+
203
+ 使用直接路径构造方式高效地检索特定日期范围内的文件
204
+
205
+ :param from_date: 开始日期(格式:YYYY-MM-DD或datetime.date),默认为None
206
+ :param to_date: 结束日期(格式:YYYY-MM-DD或datetime.date),默认为None
207
+ """
208
+ import datetime
209
+
210
+ print("[_load_all_logs] Starting to load logs")
211
+ print(f"[_load_all_logs] Date range: {from_date} to {to_date}")
212
+
213
+ filepathes = []
214
+
215
+ # 确定日期范围
216
+ if from_date is None and to_date is None:
217
+ # 如果没有指定范围,扫描所有目录
218
+ files = glob.glob("**/*.json", root_dir=self.local_dir, recursive=True)
219
+ filepathes = [os.path.join(self.local_dir, file) for file in files]
220
+ else:
221
+ # 将日期参数转换为 datetime.date 对象
222
+ start_date = from_date
223
+ end_date = to_date
224
+
225
+ if isinstance(start_date, str):
226
+ start_date = datetime.datetime.strptime(start_date, "%Y-%m-%d").date()
227
+ if isinstance(end_date, str):
228
+ end_date = datetime.datetime.strptime(end_date, "%Y-%m-%d").date()
229
+
230
+ # 如果只指定了一个日期,设置默认值
231
+ if start_date is None:
232
+ start_date = end_date
233
+ if end_date is None:
234
+ end_date = start_date
235
+
236
+ # 确保日期不为 None 的类型检查
237
+ if start_date is not None and end_date is not None:
238
+ # 直接构造日期范围内的目录路径,避免 glob 遍历
239
+ current_date = start_date
240
+ date_dirs = []
241
+ while current_date <= end_date:
242
+ year = str(current_date.year)
243
+ month = str(current_date.month)
244
+ day = str(current_date.day)
245
+ date_dir = os.path.join(self.local_dir, year, month, day)
246
+ date_dirs.append((date_dir, year, month, day))
247
+ current_date += datetime.timedelta(days=1)
248
+
249
+ print(
250
+ f"[_load_all_logs] Constructed {len(date_dirs)} date directories"
251
+ )
252
+
253
+ # 从指定日期目录中查找 JSON 文件
254
+ for date_dir, year, month, day in date_dirs:
255
+ if os.path.isdir(date_dir):
256
+ json_files = glob.glob("*.json", root_dir=date_dir)
257
+ for json_file in json_files:
258
+ filepathes.append(os.path.join(date_dir, json_file))
259
+
260
+ print(f"[_load_all_logs] Found {len(filepathes)} files in date range")
261
+
262
+ # 加载所有日志文件
263
  datasets = []
264
  for path in tqdm(filepathes):
265
  path = str(path)
266
+ try:
267
+ datasets.append(ds.Dataset.from_json(path))
268
+ except Exception as e:
269
+ print(f"[_load_all_logs] Error loading {path}: {e}")
270
+ continue
271
+
272
+ # 合并数据集并排序
273
  df = pd.DataFrame()
274
  if datasets:
275
  dataset: ds.Dataset = ds.concatenate_datasets(datasets)
276
  df = dataset.to_pandas()
277
  assert isinstance(df, pd.DataFrame)
278
  df = df.sort_values(by="timestamp", ascending=False)
279
+
280
  print(f"[_load_all_logs] Loaded {len(df)} logs")
281
+ self.loaded_files = set([os.path.relpath(p, self.local_dir) for p in filepathes])
282
  return df
283
 
284
+ def refresh(self, from_date=None, to_date=None) -> list[dict]:
285
+ """
286
+ 获取刷新后的日志列表,支持日期范围过滤
287
+
288
+ :param from_date: 开始日期(格式:YYYY-MM-DD或datetime.date),默认为None
289
+ :param to_date: 结束日期(格式:YYYY-MM-DD或datetime.date),默认为None
290
+ :return: 日志字典列表
291
+ """
292
+ import datetime
293
+
294
  self.push()
295
 
296
+ # 将字符串日期转换为 datetime.date 对象
297
+ if isinstance(from_date, str):
298
+ from_date = datetime.datetime.strptime(from_date, "%Y-%m-%d").date()
299
+ if isinstance(to_date, str):
300
+ to_date = datetime.datetime.strptime(to_date, "%Y-%m-%d").date()
301
+
302
+ # 如果没有指定日期范围,使用缓存机制
303
+ if from_date is None and to_date is None:
304
+ # 如果缓存需要刷新或者缓存为空,重新加载所有日志
305
+ if self.cache_needs_refresh or self.cached_df is None:
306
+ print("[refresh] Cache miss, reloading all logs")
307
+ self.cached_df = self._load_all_logs()
308
+ self.cache_needs_refresh = False
309
+ else:
310
+ print("[refresh] Using cached data")
311
 
312
+ # 返回缓存的DataFrame
313
+ if self.cached_df is None or self.cached_df.empty:
314
+ return []
315
 
316
+ return self.cached_df.to_dict(orient="records")
317
+ else:
318
+ # 如果指定了日期范围,直接加载不使用缓存
319
+ print("[refresh] Date range specified, loading without cache")
320
+ df = self._load_all_logs(from_date=from_date, to_date=to_date)
321
+ if df is None or df.empty:
322
+ return []
323
+ return df.to_dict(orient="records")
main.py CHANGED
@@ -1,8 +1,9 @@
1
- from fastapi import FastAPI, Body, Header, Request
2
  from fastapi.middleware.cors import CORSMiddleware
3
  from utils import beijing, parse_token
4
  from logging_helper import LoggingHelper
5
  from fastapi.templating import Jinja2Templates
 
6
 
7
 
8
  app = FastAPI(
@@ -65,8 +66,26 @@ templates = Jinja2Templates(directory="static")
65
 
66
  @app.get("")
67
  @app.get("/")
68
- async def root(request: Request):
69
- data = logger.refresh()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
70
  return templates.TemplateResponse(
71
  "index.html",
72
  {"request": request, "data": data},
 
1
+ from fastapi import FastAPI, Body, Header, Request, Query
2
  from fastapi.middleware.cors import CORSMiddleware
3
  from utils import beijing, parse_token
4
  from logging_helper import LoggingHelper
5
  from fastapi.templating import Jinja2Templates
6
+ import datetime
7
 
8
 
9
  app = FastAPI(
 
66
 
67
  @app.get("")
68
  @app.get("/")
69
+ async def root(
70
+ request: Request,
71
+ from_date: str | None = Query(None),
72
+ to_date: str | None = Query(None),
73
+ ):
74
+ """
75
+ 首页端点,支持日期范围查询
76
+
77
+ 查询参数:
78
+ - from_date: 开始日期(格式:YYYY-MM-DD),不指定时默认加载最近7天
79
+ - to_date: 结束日期(格式:YYYY-MM-DD),不指定时默认为今天
80
+ """
81
+ # 如果没有指定日期范围,默认加载最近7天的日志
82
+ if from_date is None and to_date is None:
83
+ today = beijing().date()
84
+ from_date = str(today - datetime.timedelta(days=6)) # 最近7天(包括今天)
85
+ to_date = str(today)
86
+ print(f"[root] No date range specified, using last 7 days: {from_date} to {to_date}")
87
+
88
+ data = logger.refresh(from_date=from_date, to_date=to_date)
89
  return templates.TemplateResponse(
90
  "index.html",
91
  {"request": request, "data": data},
static/index.html CHANGED
@@ -134,6 +134,71 @@
134
  box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
135
  }
136
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
137
  .main {
138
  padding: 30px;
139
  }
@@ -228,8 +293,6 @@
228
  line-height: 1.4;
229
  }
230
 
231
-
232
-
233
  .timestamp {
234
  color: #6c757d;
235
  font-size: 0.9rem;
@@ -324,6 +387,19 @@
324
  min-width: auto;
325
  }
326
 
 
 
 
 
 
 
 
 
 
 
 
 
 
327
  .stats {
328
  justify-content: center;
329
  }
@@ -360,7 +436,24 @@
360
  <button class="btn btn-secondary" onclick="refreshLogs()">刷新</button>
361
  </div>
362
 
 
 
 
 
 
 
 
 
 
 
363
 
 
 
 
 
 
 
 
364
  </div>
365
 
366
  <div class="main">
@@ -383,7 +476,7 @@
383
  <div class="no-data" id="noData" style="display: none;">
384
  <div>📋</div>
385
  <h3>暂无数据</h3>
386
- <p>没有找到匹配的错误日志</p>
387
  </div>
388
 
389
  <div class="pagination" id="pagination">
@@ -414,13 +507,124 @@
414
  let totalPages = 1;
415
  let filteredLogs = [...data];
416
  let searchTerm = '';
 
417
 
418
  // 初始化页面
419
  function initPage() {
 
 
 
 
 
 
 
 
 
 
 
 
420
  renderLogs();
421
  updatePagination();
422
  }
423
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
424
  // 改变每页显示数量
425
  function changePageSize() {
426
  const select = document.getElementById('pageSize');
@@ -533,11 +737,11 @@
533
  // 搜索日志
534
  function searchLogs() {
535
  searchTerm = document.getElementById('searchInput').value.trim();
536
-
537
  if (searchTerm === '') {
538
  filteredLogs = [...data];
539
  } else {
540
- filteredLogs = data.filter(log =>
541
  log.username.toLowerCase().includes(searchTerm.toLowerCase())
542
  );
543
  }
@@ -549,9 +753,25 @@
549
 
550
  // 刷新日志
551
  function refreshLogs() {
552
- // 模拟刷新数据
553
- console.log('刷新日志数据...');
554
- initPage();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
555
  }
556
 
557
  // 监听回车键搜索
@@ -561,8 +781,21 @@
561
  }
562
  });
563
 
 
 
 
 
 
 
 
 
 
 
 
 
 
564
  // 页面加载完成后初始化
565
  document.addEventListener('DOMContentLoaded', initPage);
566
  </script>
567
  </body>
568
- </html>
 
134
  box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
135
  }
136
 
137
+ .date-filter-container {
138
+ display: flex;
139
+ align-items: center;
140
+ gap: 15px;
141
+ margin-top: 20px;
142
+ flex-wrap: wrap;
143
+ }
144
+
145
+ .date-input-group {
146
+ display: flex;
147
+ align-items: center;
148
+ gap: 8px;
149
+ }
150
+
151
+ .date-input-group label {
152
+ font-weight: 500;
153
+ color: #495057;
154
+ min-width: 60px;
155
+ }
156
+
157
+ .date-input-group input[type="date"] {
158
+ padding: 10px 15px;
159
+ border: 2px solid #e9ecef;
160
+ border-radius: 8px;
161
+ font-size: 14px;
162
+ background: white;
163
+ cursor: pointer;
164
+ transition: all 0.3s ease;
165
+ }
166
+
167
+ .date-input-group input[type="date"]:focus {
168
+ outline: none;
169
+ border-color: #667eea;
170
+ box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
171
+ }
172
+
173
+ .date-shortcuts {
174
+ display: flex;
175
+ gap: 8px;
176
+ flex-wrap: wrap;
177
+ }
178
+
179
+ .date-shortcut-btn {
180
+ padding: 8px 15px;
181
+ border: 1px solid #e9ecef;
182
+ background: white;
183
+ border-radius: 8px;
184
+ cursor: pointer;
185
+ font-size: 13px;
186
+ color: #495057;
187
+ transition: all 0.3s ease;
188
+ }
189
+
190
+ .date-shortcut-btn:hover {
191
+ background: #667eea;
192
+ color: white;
193
+ border-color: #667eea;
194
+ }
195
+
196
+ .date-shortcut-btn.active {
197
+ background: #667eea;
198
+ color: white;
199
+ border-color: #667eea;
200
+ }
201
+
202
  .main {
203
  padding: 30px;
204
  }
 
293
  line-height: 1.4;
294
  }
295
 
 
 
296
  .timestamp {
297
  color: #6c757d;
298
  font-size: 0.9rem;
 
387
  min-width: auto;
388
  }
389
 
390
+ .date-filter-container {
391
+ flex-direction: column;
392
+ align-items: stretch;
393
+ }
394
+
395
+ .date-input-group {
396
+ flex-direction: column;
397
+ }
398
+
399
+ .date-input-group label {
400
+ min-width: auto;
401
+ }
402
+
403
  .stats {
404
  justify-content: center;
405
  }
 
436
  <button class="btn btn-secondary" onclick="refreshLogs()">刷新</button>
437
  </div>
438
 
439
+ <div class="date-filter-container">
440
+ <div class="date-input-group">
441
+ <label for="fromDate">开始日期:</label>
442
+ <input type="date" id="fromDate">
443
+ </div>
444
+ <div class="date-input-group">
445
+ <label for="toDate">结束日期:</label>
446
+ <input type="date" id="toDate">
447
+ </div>
448
+ <button class="btn btn-primary" onclick="filterByDate()">过滤</button>
449
 
450
+ <div class="date-shortcuts">
451
+ <button class="date-shortcut-btn" onclick="setDateRange('today')">今天</button>
452
+ <button class="date-shortcut-btn" onclick="setDateRange('week')">最近7天</button>
453
+ <button class="date-shortcut-btn" onclick="setDateRange('month')">最近30天</button>
454
+ <button class="date-shortcut-btn" onclick="setDateRange('all')">全部</button>
455
+ </div>
456
+ </div>
457
  </div>
458
 
459
  <div class="main">
 
476
  <div class="no-data" id="noData" style="display: none;">
477
  <div>📋</div>
478
  <h3>暂无数据</h3>
479
+ <p>没有找到匹配的日志</p>
480
  </div>
481
 
482
  <div class="pagination" id="pagination">
 
507
  let totalPages = 1;
508
  let filteredLogs = [...data];
509
  let searchTerm = '';
510
+ let currentDateFilter = null; // 记录当前的日期过滤范围
511
 
512
  // 初始化页面
513
  function initPage() {
514
+ // 从URL参数中读取日期范围
515
+ const urlParams = new URLSearchParams(window.location.search);
516
+ const fromDate = urlParams.get('from_date');
517
+ const toDate = urlParams.get('to_date');
518
+
519
+ if (fromDate) {
520
+ document.getElementById('fromDate').value = fromDate;
521
+ }
522
+ if (toDate) {
523
+ document.getElementById('toDate').value = toDate;
524
+ }
525
+
526
  renderLogs();
527
  updatePagination();
528
  }
529
 
530
+ // 获取今天的日期(YYYY-MM-DD格式)
531
+ function getTodayDate() {
532
+ const today = new Date();
533
+ return today.toISOString().split('T')[0];
534
+ }
535
+
536
+ // 获取指定天数前的日期
537
+ function getDateBefore(days) {
538
+ const date = new Date();
539
+ date.setDate(date.getDate() - days);
540
+ return date.toISOString().split('T')[0];
541
+ }
542
+
543
+ // 设置日期范围快捷选项
544
+ function setDateRange(range) {
545
+ const today = getTodayDate();
546
+
547
+ switch(range) {
548
+ case 'today':
549
+ document.getElementById('fromDate').value = today;
550
+ document.getElementById('toDate').value = today;
551
+ break;
552
+ case 'week':
553
+ document.getElementById('fromDate').value = getDateBefore(6);
554
+ document.getElementById('toDate').value = today;
555
+ break;
556
+ case 'month':
557
+ document.getElementById('fromDate').value = getDateBefore(29);
558
+ document.getElementById('toDate').value = today;
559
+ break;
560
+ case 'all':
561
+ document.getElementById('fromDate').value = '';
562
+ document.getElementById('toDate').value = '';
563
+ break;
564
+ }
565
+
566
+ // 更新快捷按钮的active状态
567
+ updateShortcutButtons(range);
568
+ }
569
+
570
+ // 更新快捷按钮的样式
571
+ function updateShortcutButtons(active) {
572
+ const buttons = document.querySelectorAll('.date-shortcut-btn');
573
+ buttons.forEach((btn, index) => {
574
+ const ranges = ['today', 'week', 'month', 'all'];
575
+ btn.classList.remove('active');
576
+ if (ranges[index] === active) {
577
+ btn.classList.add('active');
578
+ }
579
+ });
580
+ }
581
+
582
+ // 按日期过滤
583
+ function filterByDate() {
584
+ const fromDate = document.getElementById('fromDate').value;
585
+ const toDate = document.getElementById('toDate').value;
586
+
587
+ if (!fromDate && !toDate) {
588
+ // 如果两个都为空,加载所有数据
589
+ currentDateFilter = null;
590
+ refreshFromServer(null, null);
591
+ return;
592
+ }
593
+
594
+ if (fromDate && toDate) {
595
+ if (fromDate > toDate) {
596
+ alert('开始日期不能晚于结束日期');
597
+ return;
598
+ }
599
+ }
600
+
601
+ // 记录当前的日期过滤
602
+ currentDateFilter = { from_date: fromDate, to_date: toDate };
603
+
604
+ // 刷新并带上日期参数
605
+ refreshFromServer(fromDate, toDate);
606
+ }
607
+
608
+ // 从服务器刷新数据,支持日期参数
609
+ function refreshFromServer(fromDate, toDate) {
610
+ let url = window.location.pathname;
611
+ const params = new URLSearchParams();
612
+
613
+ if (fromDate) {
614
+ params.append('from_date', fromDate);
615
+ }
616
+ if (toDate) {
617
+ params.append('to_date', toDate);
618
+ }
619
+
620
+ if (params.toString()) {
621
+ url += '?' + params.toString();
622
+ }
623
+
624
+ // 重新加载页面
625
+ window.location.href = url;
626
+ }
627
+
628
  // 改变每页显示数量
629
  function changePageSize() {
630
  const select = document.getElementById('pageSize');
 
737
  // 搜索日志
738
  function searchLogs() {
739
  searchTerm = document.getElementById('searchInput').value.trim();
740
+
741
  if (searchTerm === '') {
742
  filteredLogs = [...data];
743
  } else {
744
+ filteredLogs = data.filter(log =>
745
  log.username.toLowerCase().includes(searchTerm.toLowerCase())
746
  );
747
  }
 
753
 
754
  // 刷新日志
755
  function refreshLogs() {
756
+ // 如果有日期过滤,使用带日期参数的刷新
757
+ if (currentDateFilter) {
758
+ refreshFromServer(currentDateFilter.from_date, currentDateFilter.to_date);
759
+ } else {
760
+ // 获取当前的URL参数
761
+ const urlParams = new URLSearchParams(window.location.search);
762
+ const fromDate = urlParams.get('from_date');
763
+ const toDate = urlParams.get('to_date');
764
+
765
+ if (fromDate || toDate) {
766
+ refreshFromServer(fromDate, toDate);
767
+ } else {
768
+ // 模拟刷新数据
769
+ console.log('刷新日志数据...');
770
+ currentPage = 1;
771
+ renderLogs();
772
+ updatePagination();
773
+ }
774
+ }
775
  }
776
 
777
  // 监听回车键搜索
 
781
  }
782
  });
783
 
784
+ // 监听日期输入框的回车键
785
+ document.getElementById('fromDate').addEventListener('keypress', function(e) {
786
+ if (e.key === 'Enter') {
787
+ filterByDate();
788
+ }
789
+ });
790
+
791
+ document.getElementById('toDate').addEventListener('keypress', function(e) {
792
+ if (e.key === 'Enter') {
793
+ filterByDate();
794
+ }
795
+ });
796
+
797
  // 页面加载完成后初始化
798
  document.addEventListener('DOMContentLoaded', initPage);
799
  </script>
800
  </body>
801
+ </html>