Jiang commited on
Commit
b70643d
·
unverified ·
2 Parent(s): 81053f9 22871df

Merge pull request #2 from east-and-west-magic/save-log

Browse files
README.md CHANGED
@@ -10,3 +10,8 @@ short_description: log displayer for all end
10
  ---
11
 
12
  Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
10
  ---
11
 
12
  Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
13
+
14
+ Please set following environment variables:
15
+
16
+ - `hf_token`: hugginface token
17
+ - `SECRET_KEY`: key of parsing user token
logging_helper.py ADDED
@@ -0,0 +1,207 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ a module of logs saving and backuping
3
+ """
4
+
5
+ import os
6
+ import datasets as ds
7
+ from apscheduler.schedulers.background import BackgroundScheduler
8
+ from tqdm import tqdm
9
+ from utils import beijing, md5, json_to_str
10
+ from huggingface_hub import HfApi
11
+ import pandas as pd
12
+ import glob
13
+
14
+ hf = HfApi()
15
+ hf.token = os.environ.get("hf_token")
16
+
17
+
18
+ class LoggingHelper:
19
+
20
+ def __init__(
21
+ self,
22
+ repo_id: str,
23
+ local_dir: str = "data/logs",
24
+ synchronize_interval: int = 60,
25
+ ):
26
+ """
27
+ :param repo_id: the repo_id of the dataset in huggingface
28
+ :param local_dir: a directory in cwd to store downloaded files
29
+ :param synchronize_interval: the interval of synchronizing between local and huggingface
30
+
31
+ """
32
+ self.local_dir = local_dir
33
+ self.repo_id = repo_id
34
+ self.synchronize_interval = synchronize_interval
35
+ self.repo_type = "dataset"
36
+ self.scheduler = BackgroundScheduler()
37
+ self.buffer = dict[str, ds.Dataset]()
38
+ self.need_push = dict[str, bool]()
39
+ self.today = beijing().date()
40
+ ds.disable_progress_bar()
41
+ self.dataframe: pd.DataFrame
42
+ self.pull()
43
+ self.start_synchronize()
44
+
45
+ def addlog(self, log: dict):
46
+ """add one log"""
47
+ remotedir = self.remotedir()
48
+ filename = md5(json_to_str(log))[:2] + ".json"
49
+ remotepath = "/".join([remotedir, filename])
50
+ if remotepath in self.buffer:
51
+ self.buffer[remotepath] = self.buffer[remotepath].add_item(log) # type: ignore
52
+ else:
53
+ self.buffer[remotepath] = ds.Dataset.from_dict({})
54
+ self.buffer[remotepath] = self.buffer[remotepath].add_item(log) # type: ignore
55
+ self.need_push[remotepath] = True
56
+ print("[addlog] Added a log to buffer")
57
+
58
+ def remotedir(self):
59
+ now = beijing()
60
+ year = now.year.__str__()
61
+ month = now.month.__str__()
62
+ day = now.day.__str__()
63
+ return "/".join([year, month, day])
64
+
65
+ def pull(self):
66
+ try:
67
+ self.download()
68
+ remotedir = self.remotedir()
69
+ print(f"[pull] today dir: {remotedir}")
70
+ filenames = hf.list_repo_files(
71
+ repo_id=self.repo_id,
72
+ repo_type=self.repo_type,
73
+ )
74
+ files_to_load = [
75
+ filename
76
+ for filename in filenames
77
+ if filename not in self.buffer
78
+ and filename.startswith(remotedir)
79
+ and filename.endswith(".json")
80
+ ]
81
+ print(f"[pull] total {len(files_to_load)} to load")
82
+ for filename in tqdm(files_to_load):
83
+ print()
84
+ path = os.sep.join([self.local_dir, filename])
85
+ with open(path, "r") as f:
86
+ data = f.read()
87
+ if len(data) != 0:
88
+ self.buffer[filename] = ds.Dataset.from_json(path) # type: ignore
89
+ self.need_push[filename] = False
90
+ return True
91
+ except Exception as e:
92
+ print(f"[pull] {type(e)}: {e}")
93
+ return False
94
+
95
+ def push_yesterday(self) -> bool:
96
+ try:
97
+ year = self.today.year.__str__()
98
+ month = self.today.month.__str__()
99
+ day = self.today.day.__str__()
100
+ remotedir = "/".join([year, month, day])
101
+ files_to_push = []
102
+ for filename in self.buffer:
103
+ if not filename.startswith(remotedir):
104
+ continue
105
+ if not self.need_push[filename]:
106
+ del self.buffer[filename]
107
+ del self.need_push[filename]
108
+ files_to_push.append(filename)
109
+ if len(files_to_push) == 0:
110
+ return True
111
+ print("[push_yesterday] Writing datasets to json files")
112
+ for filename in files_to_push:
113
+ localpath = os.sep.join([self.local_dir, filename])
114
+ self.buffer[filename].to_json(localpath)
115
+ files_to_push.append(filename)
116
+ print(f"[push_yesterday] {filename} finished")
117
+ print("[push_yesterday] Done")
118
+ print("[push_yesterday] Pushing log files to remote")
119
+ if len(files_to_push):
120
+ localdir = os.sep.join([self.local_dir, remotedir])
121
+ res = hf.upload_folder(
122
+ repo_id=self.repo_id,
123
+ folder_path=localdir,
124
+ path_in_repo=remotedir,
125
+ repo_type=self.repo_type,
126
+ commit_message=f"Updated at {beijing()}",
127
+ )
128
+ print(f"[push_yesterday] Log files pushed to {res}")
129
+ print("[push_yesterday] Done")
130
+ return True
131
+ except Exception as e:
132
+ print(f"[push_yesterday] {type(e)}: {e}")
133
+ return False
134
+
135
+ def push(self):
136
+ try:
137
+ now = beijing().date()
138
+ if now != self.today: # new day comes
139
+ if not self.push_yesterday():
140
+ print("[push] Failed to upload yesterday's log files")
141
+ self.today = now
142
+ files_to_push = [
143
+ filename for filename in self.need_push if self.need_push[filename]
144
+ ]
145
+ if len(files_to_push) == 0:
146
+ return True
147
+ print("[push] Writing datasets to json files")
148
+ for filename in files_to_push:
149
+ localpath = os.path.join(self.local_dir, filename)
150
+ self.buffer[filename].to_json(localpath)
151
+ print(f"[push] {filename} finished")
152
+ print("[push] Done")
153
+ print("[push] Pushing log files to remote")
154
+ remotedir = self.remotedir()
155
+ localdir = "/".join([self.local_dir, remotedir])
156
+ res = hf.upload_folder(
157
+ repo_id=self.repo_id,
158
+ folder_path=localdir,
159
+ path_in_repo=remotedir,
160
+ repo_type=self.repo_type,
161
+ commit_message=f"Updated at {beijing()}",
162
+ )
163
+ for filename in files_to_push:
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}")
170
+ return False
171
+
172
+ def download(self):
173
+ print("[download] Starting downloading")
174
+ try:
175
+ res = hf.snapshot_download(
176
+ repo_id=self.repo_id,
177
+ repo_type="dataset",
178
+ local_dir=self.local_dir,
179
+ )
180
+ print(f"[download] Downloaded to {res}")
181
+ except Exception as e:
182
+ print(f"[download] {type(e)}: {e}")
183
+ print("[download] Done")
184
+
185
+ def start_synchronize(self):
186
+ self.scheduler.add_job(
187
+ self.push,
188
+ "interval",
189
+ seconds=self.synchronize_interval,
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 = []
198
+ for path in tqdm(filepathes):
199
+ path = str(path)
200
+ datasets.append(ds.Dataset.from_json(path))
201
+ df = pd.DataFrame()
202
+ if datasets:
203
+ dataset: ds.Dataset = ds.concatenate_datasets(datasets)
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")
main.py CHANGED
@@ -1,9 +1,9 @@
1
- from fastapi import FastAPI, Body
2
  from fastapi.middleware.cors import CORSMiddleware
3
- from panel.io.fastapi import add_applications
4
- import page
5
- from utils import beijing
6
- import logging
7
 
8
  app = FastAPI(
9
  title=f"Log Displayer",
@@ -19,25 +19,55 @@ app.add_middleware(
19
  allow_headers=["*"],
20
  )
21
  print("Done\n")
22
- add_applications(page.page, app=app, title="Log Displayer")
23
-
24
 
25
- @app.post("/app")
26
- async def log_app(message: str = Body(..., embed=True)):
27
- print(f"[APP] {message}")
28
- logger = logging.getLogger("APP")
29
- logger.debug(message)
30
- return True
31
 
32
 
33
- @app.post("/game")
34
- async def log_game(message: str = Body(..., embed=True)):
35
- print(f"[GAME] {message}")
36
- logger = logging.getLogger("Game")
37
- logger.debug(message)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
38
  return True
39
 
40
 
41
  @app.get("/healthcheck")
42
  async def health_check():
43
- return True
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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(
9
  title=f"Log Displayer",
 
19
  allow_headers=["*"],
20
  )
21
  print("Done\n")
 
 
22
 
23
+ LOGS_REPO_ID = "pgsoft/logs"
24
+ local_dir = "data/logs"
25
+ logger = LoggingHelper(
26
+ repo_id=LOGS_REPO_ID,
27
+ local_dir=local_dir,
28
+ )
29
 
30
 
31
+ @app.post("/{end}")
32
+ async def add_log(
33
+ end: str,
34
+ message: str = Body(..., embed=True),
35
+ token: str | None = Header(None),
36
+ source: str | None = Header("web"),
37
+ ):
38
+ print("Type: ", end)
39
+ print("From: ", source)
40
+ print("Token: ", token)
41
+ uid, username = parse_token(token)
42
+ timestamp = beijing().isoformat()
43
+ print("Timestamp: ", timestamp)
44
+ print("Message: ", message)
45
+ log = {
46
+ "type": end,
47
+ "source": source,
48
+ "uid": uid,
49
+ "username": username,
50
+ "token": token,
51
+ "content": message,
52
+ "timestamp": timestamp,
53
+ }
54
+ logger.addlog(log)
55
  return True
56
 
57
 
58
  @app.get("/healthcheck")
59
  async def health_check():
60
+ return True
61
+
62
+
63
+ templates = Jinja2Templates(directory="static")
64
+
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},
73
+ )
page.py DELETED
@@ -1,39 +0,0 @@
1
- import panel as pn
2
- import logging
3
- from pathlib import Path
4
-
5
-
6
- tab_console_names = ["APP", "Game"]
7
- pn.extension("terminal") # type: ignore
8
- tabs = []
9
- for name in tab_console_names:
10
- terminal = pn.widgets.Terminal(height=600, sizing_mode="stretch_width")
11
-
12
- logger = logging.getLogger(name)
13
- logger.setLevel(logging.DEBUG)
14
- stream_handler = logging.StreamHandler(terminal)
15
- stream_handler.terminator = " \n"
16
- formatter = logging.Formatter("%(asctime)s [%(levelname)s]: %(message)s")
17
- stream_handler.setFormatter(formatter)
18
- stream_handler.setLevel(logging.DEBUG)
19
- logger.addHandler(stream_handler)
20
-
21
- log_path = Path(f"data/logs/{name.lower()}.log")
22
- log_path.parent.mkdir(parents=True, exist_ok=True)
23
- file_handler = logging.FileHandler(log_path)
24
- file_handler.setFormatter(formatter)
25
- logger.addHandler(file_handler)
26
- downloader = pn.widgets.FileDownload(log_path)
27
- tabs.append(
28
- (
29
- name,
30
- pn.Column(
31
- pn.Row(terminal, align="center"),
32
- pn.Row(downloader, align="center"),
33
- ),
34
- )
35
- )
36
-
37
-
38
- def page():
39
- return pn.Column(pn.Tabs(*tabs))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
requirements.txt CHANGED
@@ -4,3 +4,7 @@ transformers
4
  numpy
5
  torch
6
  aiohttp
 
 
 
 
 
4
  numpy
5
  torch
6
  aiohttp
7
+ python-jose[cryptography]
8
+ apscheduler==3.11.0
9
+ huggingface-hub==0.34.4
10
+ datasets==4.0.0
scratch/test_dataset_to_dict.py ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import datasets as ds
2
+ import pandas as pd
3
+ import glob
4
+
5
+ dataset = ds.Dataset.from_dict({})
6
+ files = glob.glob("**/*.json", root_dir="data/logs", recursive=True)
7
+ for file in files:
8
+ path = "data/logs/" + file
9
+ temp = ds.Dataset.from_json(path)
10
+ dataset = ds.concatenate_datasets([dataset, temp]) # type: ignore
11
+ df = dataset.to_pandas()
12
+ assert isinstance(df, pd.DataFrame)
13
+ print(len(df))
14
+ res = df.to_dict(orient="records")
15
+ print(res)
scratch/test_glob.py ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ import glob
2
+
3
+
4
+ path = "data/logs"
5
+ files = glob.glob("**/*.json", root_dir=path, recursive=True)
6
+ print(files)
static/index.html ADDED
@@ -0,0 +1,566 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>日志管理</title>
7
+ <style>
8
+ * {
9
+ margin: 0;
10
+ padding: 0;
11
+ box-sizing: border-box;
12
+ }
13
+
14
+ body {
15
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
16
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
17
+ min-height: 100vh;
18
+ padding: 20px;
19
+ }
20
+
21
+ .container {
22
+ max-width: 1200px;
23
+ margin: 0 auto;
24
+ background: white;
25
+ border-radius: 15px;
26
+ box-shadow: 0 20px 40px rgba(0,0,0,0.1);
27
+ overflow: hidden;
28
+ }
29
+
30
+ .header {
31
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
32
+ color: white;
33
+ padding: 30px;
34
+ text-align: center;
35
+ }
36
+
37
+ .header h1 {
38
+ font-size: 2.5rem;
39
+ margin-bottom: 10px;
40
+ font-weight: 300;
41
+ }
42
+
43
+ .header p {
44
+ opacity: 0.9;
45
+ font-size: 1.1rem;
46
+ }
47
+
48
+ .controls {
49
+ padding: 30px;
50
+ background: #f8f9fa;
51
+ border-bottom: 1px solid #e9ecef;
52
+ }
53
+
54
+ .search-container {
55
+ display: flex;
56
+ gap: 15px;
57
+ align-items: center;
58
+ flex-wrap: wrap;
59
+ }
60
+
61
+ .search-input {
62
+ flex: 1;
63
+ min-width: 250px;
64
+ padding: 12px 20px;
65
+ border: 2px solid #e9ecef;
66
+ border-radius: 25px;
67
+ font-size: 16px;
68
+ transition: all 0.3s ease;
69
+ }
70
+
71
+ .search-input:focus {
72
+ outline: none;
73
+ border-color: #667eea;
74
+ box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
75
+ }
76
+
77
+ .btn {
78
+ padding: 12px 25px;
79
+ border: none;
80
+ border-radius: 25px;
81
+ cursor: pointer;
82
+ font-size: 16px;
83
+ font-weight: 500;
84
+ transition: all 0.3s ease;
85
+ text-decoration: none;
86
+ display: inline-block;
87
+ }
88
+
89
+ .btn-primary {
90
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
91
+ color: white;
92
+ }
93
+
94
+ .btn-primary:hover {
95
+ transform: translateY(-2px);
96
+ box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
97
+ }
98
+
99
+ .btn-secondary {
100
+ background: #6c757d;
101
+ color: white;
102
+ }
103
+
104
+ .btn-secondary:hover {
105
+ background: #5a6268;
106
+ transform: translateY(-2px);
107
+ }
108
+
109
+ .page-size-container {
110
+ display: flex;
111
+ align-items: center;
112
+ gap: 10px;
113
+ margin-top: 20px;
114
+ }
115
+
116
+ .page-size-container label {
117
+ font-weight: 500;
118
+ color: #495057;
119
+ }
120
+
121
+ .page-size-container select {
122
+ padding: 8px 15px;
123
+ border: 2px solid #e9ecef;
124
+ border-radius: 8px;
125
+ font-size: 14px;
126
+ background: white;
127
+ cursor: pointer;
128
+ transition: all 0.3s ease;
129
+ }
130
+
131
+ .page-size-container select:focus {
132
+ outline: none;
133
+ border-color: #667eea;
134
+ box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
135
+ }
136
+
137
+ .content {
138
+ padding: 30px;
139
+ }
140
+
141
+ .log-table {
142
+ width: 100%;
143
+ border-collapse: collapse;
144
+ margin-bottom: 30px;
145
+ background: white;
146
+ border-radius: 10px;
147
+ overflow: hidden;
148
+ box-shadow: 0 2px 10px rgba(0,0,0,0.1);
149
+ }
150
+
151
+ .log-table th {
152
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
153
+ color: white;
154
+ padding: 15px;
155
+ text-align: left;
156
+ font-weight: 500;
157
+ }
158
+
159
+ .log-table td {
160
+ padding: 15px;
161
+ border-bottom: 1px solid #e9ecef;
162
+ vertical-align: middle;
163
+ }
164
+
165
+ .log-table tr:hover {
166
+ background: #f8f9fa;
167
+ }
168
+
169
+ .log-table tr:last-child td {
170
+ border-bottom: none;
171
+ }
172
+
173
+ .username {
174
+ font-weight: 600;
175
+ color: #495057;
176
+ }
177
+
178
+ .token {
179
+ font-family: 'Courier New', monospace;
180
+ background: #f8f9fa;
181
+ padding: 8px 12px;
182
+ border-radius: 4px;
183
+ font-size: 0.85rem;
184
+ color: #6c757d;
185
+ word-break: break-all;
186
+ cursor: pointer;
187
+ transition: all 0.3s ease;
188
+ position: relative;
189
+ border: 1px solid #e9ecef;
190
+ }
191
+
192
+ .token:hover {
193
+ background: #e9ecef;
194
+ border-color: #667eea;
195
+ }
196
+
197
+ .token::after {
198
+ content: "点击复制";
199
+ position: absolute;
200
+ top: -30px;
201
+ left: 50%;
202
+ transform: translateX(-50%);
203
+ background: #333;
204
+ color: white;
205
+ padding: 4px 8px;
206
+ border-radius: 4px;
207
+ font-size: 12px;
208
+ opacity: 0;
209
+ pointer-events: none;
210
+ transition: opacity 0.3s ease;
211
+ white-space: nowrap;
212
+ }
213
+
214
+ .token:hover::after {
215
+ opacity: 1;
216
+ }
217
+
218
+ .copy-success {
219
+ background: #d4edda !important;
220
+ border-color: #28a745 !important;
221
+ color: #155724 !important;
222
+ }
223
+
224
+ .content {
225
+ word-wrap: break-word;
226
+ line-height: 1.4;
227
+ }
228
+
229
+
230
+
231
+ .timestamp {
232
+ color: #6c757d;
233
+ font-size: 0.9rem;
234
+ }
235
+
236
+ .pagination {
237
+ display: flex;
238
+ justify-content: center;
239
+ align-items: center;
240
+ gap: 20px;
241
+ margin-top: 30px;
242
+ flex-wrap: wrap;
243
+ }
244
+
245
+ .pagination .page-size-container {
246
+ margin: 0;
247
+ }
248
+
249
+ .pagination-controls {
250
+ display: flex;
251
+ align-items: center;
252
+ gap: 10px;
253
+ }
254
+
255
+ .pagination button {
256
+ padding: 8px 12px;
257
+ border: 1px solid #dee2e6;
258
+ background: white;
259
+ cursor: pointer;
260
+ border-radius: 5px;
261
+ transition: all 0.3s ease;
262
+ }
263
+
264
+ .pagination button:hover:not(:disabled) {
265
+ background: #667eea;
266
+ color: white;
267
+ border-color: #667eea;
268
+ }
269
+
270
+ .pagination button:disabled {
271
+ opacity: 0.5;
272
+ cursor: not-allowed;
273
+ }
274
+
275
+ .pagination .active {
276
+ background: #667eea;
277
+ color: white;
278
+ border-color: #667eea;
279
+ }
280
+
281
+ .page-info {
282
+ color: #6c757d;
283
+ margin: 0 15px;
284
+ }
285
+
286
+ .no-data {
287
+ text-align: center;
288
+ padding: 60px 20px;
289
+ color: #6c757d;
290
+ }
291
+
292
+ .no-data i {
293
+ font-size: 4rem;
294
+ margin-bottom: 20px;
295
+ opacity: 0.5;
296
+ }
297
+
298
+ @media (max-width: 768px) {
299
+ .container {
300
+ margin: 10px;
301
+ border-radius: 10px;
302
+ }
303
+
304
+ .header {
305
+ padding: 20px;
306
+ }
307
+
308
+ .header h1 {
309
+ font-size: 2rem;
310
+ }
311
+
312
+ .controls {
313
+ padding: 20px;
314
+ }
315
+
316
+ .search-container {
317
+ flex-direction: column;
318
+ align-items: stretch;
319
+ }
320
+
321
+ .search-input {
322
+ min-width: auto;
323
+ }
324
+
325
+ .stats {
326
+ justify-content: center;
327
+ }
328
+
329
+ .content {
330
+ padding: 20px;
331
+ }
332
+
333
+ .log-table {
334
+ font-size: 0.9rem;
335
+ }
336
+
337
+ .log-table th,
338
+ .log-table td {
339
+ padding: 10px 8px;
340
+ }
341
+
342
+ .error-content {
343
+ max-width: 200px;
344
+ }
345
+ }
346
+ </style>
347
+ </head>
348
+ <body>
349
+ <div class="container">
350
+ <div class="header">
351
+ <h1>日志管理系统</h1>
352
+ </div>
353
+
354
+ <div class="controls">
355
+ <div class="search-container">
356
+ <input type="text" id="searchInput" class="search-input" placeholder="搜索用户名...">
357
+ <button class="btn btn-primary" onclick="searchLogs()">搜索</button>
358
+ <button class="btn btn-secondary" onclick="refreshLogs()">刷新</button>
359
+ </div>
360
+
361
+
362
+ </div>
363
+
364
+ <div class="content">
365
+ <table class="log-table" id="logTable">
366
+ <thead>
367
+ <tr>
368
+ <th>Type</th>
369
+ <th>Source</th>
370
+ <th>UID</th>
371
+ <th>Username</th>
372
+ <th>Token</th>
373
+ <th>Content</th>
374
+ <th>Timestamp</th>
375
+ </tr>
376
+ </thead>
377
+ <tbody id="logTableBody">
378
+ </tbody>
379
+ </table>
380
+
381
+ <div class="no-data" id="noData" style="display: none;">
382
+ <div>📋</div>
383
+ <h3>暂无数据</h3>
384
+ <p>没有找到匹配的错误日志</p>
385
+ </div>
386
+
387
+ <div class="pagination" id="pagination">
388
+ <div class="page-size-container">
389
+ <label for="pageSize">每页显示:</label>
390
+ <select id="pageSize" onchange="changePageSize()">
391
+ <option value="20" selected>20条</option>
392
+ <option value="40">40条</option>
393
+ <option value="60">60条</option>
394
+ </select>
395
+ </div>
396
+ <div class="pagination-controls">
397
+ <button onclick="goToPage(1)" id="firstBtn">首页</button>
398
+ <button onclick="goToPage(currentPage - 1)" id="prevBtn">上一页</button>
399
+ <span class="page-info" id="pageInfo">第 1 页,共 1 页</span>
400
+ <button onclick="goToPage(currentPage + 1)" id="nextBtn">下一页</button>
401
+ <button onclick="goToPage(totalPages)" id="lastBtn">末页</button>
402
+ </div>
403
+ </div>
404
+ </div>
405
+ </div>
406
+
407
+ <script>
408
+ const data = {{ data | tojson}};
409
+ // 分页配置
410
+ let currentPage = 1;
411
+ let pageSize = 20;
412
+ let totalPages = 1;
413
+ let filteredLogs = [...data];
414
+ let searchTerm = '';
415
+
416
+ // 初始化页面
417
+ function initPage() {
418
+ renderLogs();
419
+ updatePagination();
420
+ }
421
+
422
+ // 改变每页显示数量
423
+ function changePageSize() {
424
+ const select = document.getElementById('pageSize');
425
+ pageSize = parseInt(select.value);
426
+ currentPage = 1; // 重置到第一页
427
+ renderLogs();
428
+ updatePagination();
429
+ }
430
+
431
+ // 渲染日志表格
432
+ function renderLogs() {
433
+ const tbody = document.getElementById('logTableBody');
434
+ const noData = document.getElementById('noData');
435
+ const table = document.getElementById('logTable');
436
+
437
+ if (filteredLogs.length === 0) {
438
+ table.style.display = 'none';
439
+ noData.style.display = 'block';
440
+ return;
441
+ }
442
+
443
+ table.style.display = 'table';
444
+ noData.style.display = 'none';
445
+
446
+ totalPages = Math.ceil(filteredLogs.length / pageSize);
447
+ const startIndex = (currentPage - 1) * pageSize;
448
+ const endIndex = startIndex + pageSize;
449
+ const currentLogs = filteredLogs.slice(startIndex, endIndex);
450
+
451
+ tbody.innerHTML = '';
452
+
453
+ currentLogs.forEach(log => {
454
+ const row = document.createElement('tr');
455
+ row.innerHTML = `
456
+ <td>
457
+ <div class="type">${log.type}</div>
458
+ </td>
459
+ <td>
460
+ <div class="uid">${log.source}</div>
461
+ </td>
462
+ <td>
463
+ <div class="uid">${log.uid}</div>
464
+ </td>
465
+ <td>
466
+ <div class="username">${log.username}</div>
467
+ </td>
468
+ <td>
469
+ <div class="token" onclick="copyToken('${log.token}')" title="点击复制">${log.token}</div>
470
+ </td>
471
+ <td>
472
+ <div class="content">${log.content}</div>
473
+ </td>
474
+ <td>
475
+ <div class="timestamp">${log.timestamp}</div>
476
+ </td>
477
+ `;
478
+ tbody.appendChild(row);
479
+ });
480
+ }
481
+
482
+ // 复制token到剪贴板
483
+ function copyToken(token) {
484
+ navigator.clipboard.writeText(token).then(function() {
485
+ // 显示复制成功的视觉反馈
486
+ const tokenElements = document.querySelectorAll('.token');
487
+ tokenElements.forEach(el => {
488
+ if (el.textContent === token) {
489
+ el.classList.add('copy-success');
490
+ setTimeout(() => {
491
+ el.classList.remove('copy-success');
492
+ }, 1000);
493
+ }
494
+ });
495
+ }).catch(function(err) {
496
+ console.error('复制失败: ', err);
497
+ // 降级方案:使用旧的复制方法
498
+ const textArea = document.createElement('textarea');
499
+ textArea.value = token;
500
+ document.body.appendChild(textArea);
501
+ textArea.select();
502
+ document.execCommand('copy');
503
+ document.body.removeChild(textArea);
504
+ });
505
+ }
506
+
507
+ // 更新分页控件
508
+ function updatePagination() {
509
+ const pageInfo = document.getElementById('pageInfo');
510
+ const firstBtn = document.getElementById('firstBtn');
511
+ const prevBtn = document.getElementById('prevBtn');
512
+ const nextBtn = document.getElementById('nextBtn');
513
+ const lastBtn = document.getElementById('lastBtn');
514
+
515
+ pageInfo.textContent = `第 ${currentPage} 页,共 ${totalPages} 页`;
516
+
517
+ firstBtn.disabled = currentPage === 1;
518
+ prevBtn.disabled = currentPage === 1;
519
+ nextBtn.disabled = currentPage === totalPages;
520
+ lastBtn.disabled = currentPage === totalPages;
521
+ }
522
+
523
+ // 跳转到指定页面
524
+ function goToPage(page) {
525
+ if (page < 1 || page > totalPages) return;
526
+ currentPage = page;
527
+ renderLogs();
528
+ updatePagination();
529
+ }
530
+
531
+ // 搜索日志
532
+ function searchLogs() {
533
+ searchTerm = document.getElementById('searchInput').value.trim();
534
+
535
+ if (searchTerm === '') {
536
+ filteredLogs = [...data];
537
+ } else {
538
+ filteredLogs = data.filter(log =>
539
+ log.username.toLowerCase().includes(searchTerm.toLowerCase())
540
+ );
541
+ }
542
+
543
+ currentPage = 1;
544
+ renderLogs();
545
+ updatePagination();
546
+ }
547
+
548
+ // 刷新日志
549
+ function refreshLogs() {
550
+ // 模拟刷新数据
551
+ console.log('刷新日志数据...');
552
+ initPage();
553
+ }
554
+
555
+ // 监听回车键搜索
556
+ document.getElementById('searchInput').addEventListener('keypress', function(e) {
557
+ if (e.key === 'Enter') {
558
+ searchLogs();
559
+ }
560
+ });
561
+
562
+ // 页面加载完成后初始化
563
+ document.addEventListener('DOMContentLoaded', initPage);
564
+ </script>
565
+ </body>
566
+ </html>
utils.py CHANGED
@@ -1,7 +1,63 @@
1
  from datetime import datetime
2
  from zoneinfo import ZoneInfo
 
 
 
 
 
 
 
 
 
3
 
4
 
5
  def beijing():
6
  """get beijing time"""
7
  return datetime.now(tz=ZoneInfo("Asia/Shanghai"))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  from datetime import datetime
2
  from zoneinfo import ZoneInfo
3
+ from typing import Any
4
+ from jose import jwt
5
+ import hashlib
6
+ import json
7
+ import uuid
8
+ import os
9
+
10
+
11
+ SECRET_KEY = os.environ.get("SECRET_KEY")
12
 
13
 
14
  def beijing():
15
  """get beijing time"""
16
  return datetime.now(tz=ZoneInfo("Asia/Shanghai"))
17
+
18
+
19
+ def decode_jwt(token: str) -> dict[str, Any]:
20
+ """get payload in the jwt token"""
21
+ assert SECRET_KEY, "Please set the environment variable SECRET_KEY"
22
+ return jwt.decode(token, SECRET_KEY, algorithms=["HS256"])
23
+
24
+
25
+ def parse_token(token: str | None) -> tuple[str, str]:
26
+ """parse user token to get uid and username
27
+
28
+ :param token (str): jwt token
29
+ :return (str, str): (uid, username)
30
+ """
31
+ if not token:
32
+ uid, username = "no_uid", "Anonymous"
33
+ else:
34
+ payload: dict = decode_jwt(token)
35
+ uid = payload.get("uid", "no_uid")
36
+ username = payload.get("username", "Anonymous")
37
+ print(f"UID: {uid}")
38
+ print(f"Username: {username}")
39
+ return uid, username
40
+
41
+
42
+ def md5(text: list[str | bytes] | str | bytes | None = None) -> str:
43
+ """generate the md5 hash code of the given text, if text is None,
44
+ return a random md5"""
45
+ code = hashlib.md5()
46
+ if text:
47
+ if isinstance(text, str):
48
+ text = text.encode("utf-8")
49
+ code.update(text)
50
+ elif isinstance(text, list):
51
+ for t in text:
52
+ if isinstance(t, str):
53
+ t = t.encode("utf-8")
54
+ code.update(t)
55
+ else:
56
+ code.update(text)
57
+ else:
58
+ code.update(uuid.uuid4().bytes)
59
+ return code.hexdigest()
60
+
61
+
62
+ def json_to_str(obj: dict | list) -> str:
63
+ return json.dumps(obj, separators=(",", ":"))