diff --git a/gsuid_core/logger.py b/gsuid_core/logger.py
index 4f5bb58..8b74f9b 100644
--- a/gsuid_core/logger.py
+++ b/gsuid_core/logger.py
@@ -1,11 +1,14 @@
+import re
import sys
import asyncio
import logging
import datetime
+from pathlib import Path
from functools import wraps
-from typing import TYPE_CHECKING, List
+from typing import TYPE_CHECKING, Dict, List
import loguru
+import aiofiles
from uvicorn.config import LOGGING_CONFIG
from gsuid_core.config import core_config
@@ -23,7 +26,7 @@ if TYPE_CHECKING:
logger: 'Logger' = loguru.logger
logging.getLogger().handlers = []
-LOGGING_CONFIG["disable_existing_loggers"] = False
+LOGGING_CONFIG['disable_existing_loggers'] = False
# https://loguru.readthedocs.io/en/stable/overview.html#entirely-compatible-with-standard-logging
@@ -143,12 +146,12 @@ if 'stdout' in logger_list:
level=LEVEL,
diagnose=True,
backtrace=True,
- filter=lambda record: record["level"].no < 40,
+ filter=lambda record: record['level'].no < 40,
format=std_format_event,
)
if 'stderr' in logger_list:
- logger.add(sys.stderr, level="ERROR")
+ logger.add(sys.stderr, level='ERROR')
if 'file' in logger_list:
logger.add(
@@ -186,7 +189,60 @@ def handle_exceptions(async_function):
try:
return await async_function(*args, **kwargs)
except Exception as e:
- logger.exception("[错误发生] %s: %s", async_function.__name__, e)
+ logger.exception('[错误发生] %s: %s', async_function.__name__, e)
return None
return wrapper
+
+
+class HistoryLogData:
+ def __init__(self):
+ self.log_list: Dict[str, List[Dict]] = {}
+
+ async def get_parse_logs(self, log_file_path: Path):
+ if log_file_path.name in self.log_list:
+ return self.log_list[log_file_path.name]
+
+ log_entries: List[Dict] = []
+
+ log_entry_pattern = re.compile(
+ r'^(\d{2}-\d{2} \d{2}:\d{2}:\d{2}) \[(\w+)] ([^\|]+) \| (.*)'
+ )
+
+ async with aiofiles.open(log_file_path, 'r', encoding='utf-8') as file:
+ lines = await file.readlines()
+
+ current_entry = None
+
+ _id = 1
+ for line in lines:
+ line = line.strip()
+ match = log_entry_pattern.match(line)
+
+ if match:
+ if current_entry:
+ log_entries.append(current_entry)
+ current_entry = {
+ 'id': _id,
+ '时间': match.group(1),
+ '日志等级': match.group(2),
+ '模块': match.group(3).strip(),
+ '内容': match.group(4).strip(),
+ }
+ _id += 1
+ elif current_entry:
+ current_entry['内容'] += '\n' + line
+
+ if current_entry:
+ log_entries.append(current_entry)
+
+ self.log_list[log_file_path.name] = log_entries
+ return log_entries
+
+
+def get_all_log_path():
+ return [
+ file
+ for file in LOG_PATH.iterdir()
+ if file.is_file() and file.suffix == '.log'
+ ]
diff --git a/gsuid_core/web_app.py b/gsuid_core/web_app.py
index e69b316..1f9237f 100644
--- a/gsuid_core/web_app.py
+++ b/gsuid_core/web_app.py
@@ -1,7 +1,8 @@
import asyncio
from io import BytesIO
from pathlib import Path
-from typing import Dict, List
+from datetime import datetime
+from typing import Dict, List, Optional
from contextlib import asynccontextmanager
from PIL import Image
@@ -18,7 +19,6 @@ from gsuid_core.data_store import image_res
from gsuid_core.webconsole.mount_app import site
from gsuid_core.segment import Message, MessageSegment
from gsuid_core.config import CONFIG_DEFAULT, core_config
-from gsuid_core.logger import logger, read_log, clean_log
from gsuid_core.aps import start_scheduler, shutdown_scheduler
from gsuid_core.server import core_start_def, core_shutdown_def
from gsuid_core.utils.database.models import CoreUser, CoreGroup
@@ -27,6 +27,13 @@ from gsuid_core.utils.plugins_config.gs_config import (
all_config_list,
core_plugins_config,
)
+from gsuid_core.logger import (
+ LOG_PATH,
+ HistoryLogData,
+ logger,
+ read_log,
+ clean_log,
+)
from gsuid_core.utils.plugins_update._plugins import (
check_status,
check_plugins,
@@ -425,4 +432,38 @@ async def core_log():
return StreamingResponse(read_log(), media_type='text/plain')
+@app.get('/genshinuid/api/historyLogs')
+@site.auth.requires('root')
+async def get_history_logs(
+ request: Request,
+ date: Optional[str] = None,
+ page: int = 0,
+ perPage: int = 0,
+):
+ if date is None:
+ date = datetime.now().strftime('%Y-%m-%d')
+
+ if date.endswith('.log'):
+ date = date.removesuffix('.log')
+
+ history_log_data = HistoryLogData()
+ log_files = await history_log_data.get_parse_logs(LOG_PATH / f'{date}.log')
+ total = len(log_files)
+ if page != 0 and perPage != 0:
+ start = (page - 1) * perPage
+ end = start + perPage
+ log_file = log_files[start:end]
+ else:
+ log_file = log_files
+ return {
+ 'status': 0,
+ 'msg': 'ok',
+ 'data': {
+ 'count': total,
+ 'rows': log_file,
+ },
+ }
+
+
+site.mount_app(app)
site.mount_app(app)
diff --git a/gsuid_core/webconsole/create_history_log.py b/gsuid_core/webconsole/create_history_log.py
new file mode 100644
index 0000000..9e4ea3b
--- /dev/null
+++ b/gsuid_core/webconsole/create_history_log.py
@@ -0,0 +1,402 @@
+from gsuid_core.logger import get_all_log_path
+
+
+def get_history_logs_page():
+ all_log_path = get_all_log_path()
+ ACTION = {
+ "actionType": "dialog",
+ "dialog": {
+ "body": {
+ "id": "u:448f2ad2a090",
+ "type": "form",
+ "title": "查看数据",
+ "mode": "flex",
+ "labelAlign": "top",
+ "dsType": "api",
+ "feat": "View",
+ "body": [
+ {
+ "name": "id",
+ "label": "id",
+ "row": 0,
+ "type": "input-number",
+ },
+ {
+ "name": "时间",
+ "label": "时间",
+ "row": 1,
+ "type": "input-text",
+ },
+ {
+ "name": "日志等级",
+ "label": "日志等级",
+ "row": 2,
+ "type": "select",
+ },
+ {
+ "name": "模块",
+ "label": "模块",
+ "row": 3,
+ "type": "input-text",
+ },
+ {
+ "name": "内容",
+ "label": "内容",
+ "row": 4,
+ "type": "textarea",
+ },
+ ],
+ "static": True,
+ "actions": [
+ {
+ "type": "button",
+ "actionType": "cancel",
+ "label": "关闭",
+ }
+ ],
+ "onEvent": {
+ "submitSucc": {
+ "actions": [
+ {
+ "actionType": "search",
+ "groupType": "component",
+ "componentId": "u:b1f51d7a324a",
+ }
+ ]
+ }
+ },
+ },
+ "title": "查看数据",
+ "size": "md",
+ "actions": [
+ {
+ "type": "button",
+ "actionType": "cancel",
+ "label": "关闭",
+ }
+ ],
+ },
+ }
+ data = {
+ "type": "page",
+ "title": "历史日志过滤查询",
+ "id": "u:908f0ee5fa73",
+ "asideResizor": False,
+ "pullRefresh": {"disabled": True},
+ "body": [
+ {
+ "type": "select",
+ "label": "选择日期",
+ "name": "select",
+ "options": [
+ {"label": i.name, "value": i.name} for i in all_log_path
+ ],
+ "id": "u:30962bfb9f83",
+ "multiple": False,
+ "onEvent": {
+ "change": {
+ "weight": 0,
+ "actions": [
+ {
+ "componentId": "u:b1f51d7a324a",
+ "ignoreError": False,
+ "actionType": "reload",
+ "data": {"date": "${event.data.value}"},
+ "dataMergeMode": "merge",
+ }
+ ],
+ }
+ },
+ },
+ {
+ "type": "divider",
+ "id": "u:d1c9f48ea328",
+ "lineStyle": "solid",
+ "direction": "horizontal",
+ "rotate": 0,
+ },
+ {
+ "id": "u:b1f51d7a324a",
+ "type": "crud2",
+ "mode": "table2",
+ "dsType": "api",
+ "syncLocation": True,
+ "primaryField": "id",
+ "loadType": "pagination",
+ "api": {
+ "url": "/genshinuid/api/historyLogs",
+ "method": "get",
+ "requestAdaptor": "",
+ "adaptor": "",
+ "messages": {},
+ "dataType": "json",
+ },
+ "filter": {
+ "type": "form",
+ "title": "条件查询",
+ "mode": "inline",
+ "columnCount": 3,
+ "clearValueOnHidden": True,
+ "behavior": ["SimpleQuery"],
+ "body": [
+ {
+ "name": "id",
+ "label": "id",
+ "type": "input-number",
+ "size": "full",
+ "required": False,
+ "behavior": "SimpleQuery",
+ "id": "u:7d5dfd6fb4a5",
+ "keyboard": True,
+ "step": 1,
+ },
+ {
+ "name": "时间",
+ "label": "时间",
+ "type": "input-text",
+ "size": "full",
+ "required": False,
+ "behavior": "SimpleQuery",
+ "id": "u:946cd5942b15",
+ },
+ {
+ "name": "日志等级",
+ "label": "日志等级",
+ "type": "select",
+ "size": "full",
+ "required": False,
+ "behavior": "SimpleQuery",
+ "id": "u:d0bf26304939",
+ "multiple": False,
+ "options": [
+ {"label": "INFO", "value": "INFO"},
+ {"label": "SUCCESS", "value": "SUCCESS"},
+ {"label": "WARNING", "value": "WARNING"},
+ {"label": "ERROR", "value": "ERROR"},
+ {"label": "DEBUG", "value": "DEBUG"},
+ {"label": "TRACE", "value": "TRACE"},
+ ],
+ "selectFirst": False,
+ "removable": False,
+ "clearable": True,
+ "checkAll": False,
+ "joinValues": True,
+ },
+ {
+ "name": "模块",
+ "label": "模块",
+ "type": "input-text",
+ "size": "full",
+ "required": False,
+ "behavior": "SimpleQuery",
+ "id": "u:294ae6f80ff2",
+ },
+ {
+ "name": "内容",
+ "label": "内容",
+ "type": "textarea",
+ "size": "full",
+ "required": False,
+ "behavior": "SimpleQuery",
+ "id": "u:ee5fc2b996d7",
+ },
+ ],
+ "actions": [
+ {
+ "type": "reset",
+ "label": "重置",
+ "id": "u:22b8ade74682",
+ },
+ {
+ "type": "submit",
+ "label": "查询",
+ "level": "primary",
+ "id": "u:e853a21b2077",
+ },
+ ],
+ "id": "u:2a882fb870a0",
+ "feat": "Insert",
+ },
+ "headerToolbar": [
+ {
+ "type": "flex",
+ "direction": "row",
+ "justify": "flex-start",
+ "alignItems": "stretch",
+ "style": {"position": "static"},
+ "items": [
+ {
+ "type": "container",
+ "align": "left",
+ "behavior": [
+ "Insert",
+ "BulkEdit",
+ "BulkDelete",
+ ],
+ "body": [],
+ "wrapperBody": False,
+ "style": {
+ "flexGrow": 1,
+ "flex": "1 1 auto",
+ "position": "static",
+ "display": "flex",
+ "flexBasis": "auto",
+ "flexDirection": "row",
+ "flexWrap": "nowrap",
+ "alignItems": "stretch",
+ "justifyContent": "flex-start",
+ },
+ "id": "u:f7dee2ce6250",
+ },
+ {
+ "type": "container",
+ "align": "right",
+ "behavior": ["FuzzyQuery"],
+ "body": [],
+ "wrapperBody": False,
+ "style": {
+ "flexGrow": 1,
+ "flex": "1 1 auto",
+ "position": "static",
+ "display": "flex",
+ "flexBasis": "auto",
+ "flexDirection": "row",
+ "flexWrap": "nowrap",
+ "alignItems": "stretch",
+ "justifyContent": "flex-end",
+ },
+ "id": "u:8d2dc85fed4b",
+ },
+ ],
+ "id": "u:14f26e218177",
+ }
+ ],
+ "footerToolbar": [
+ {
+ "type": "flex",
+ "direction": "row",
+ "justify": "flex-start",
+ "alignItems": "stretch",
+ "style": {"position": "static"},
+ "items": [
+ {
+ "type": "container",
+ "align": "left",
+ "body": [],
+ "wrapperBody": False,
+ "style": {
+ "flexGrow": 1,
+ "flex": "1 1 auto",
+ "position": "static",
+ "display": "flex",
+ "flexBasis": "auto",
+ "flexDirection": "row",
+ "flexWrap": "nowrap",
+ "alignItems": "stretch",
+ "justifyContent": "flex-start",
+ },
+ "id": "u:ffc293c3af21",
+ },
+ {
+ "type": "container",
+ "align": "right",
+ "body": [
+ {
+ "type": "pagination",
+ "behavior": "Pagination",
+ "layout": [
+ "total",
+ "perPage",
+ "pager",
+ ],
+ "perPage": 10,
+ "perPageAvailable": [10, 20, 50, 100],
+ "align": "right",
+ "id": "u:515f5d3a63d3",
+ "size": "",
+ }
+ ],
+ "wrapperBody": False,
+ "style": {
+ "flexGrow": 1,
+ "flex": "1 1 auto",
+ "position": "static",
+ "display": "flex",
+ "flexBasis": "auto",
+ "flexDirection": "row",
+ "flexWrap": "nowrap",
+ "alignItems": "stretch",
+ "justifyContent": "flex-end",
+ },
+ "id": "u:6b708bcf8cf1",
+ },
+ ],
+ "id": "u:6cedb9797d50",
+ }
+ ],
+ "columns": [
+ {
+ "type": "tpl",
+ "title": "id",
+ "name": "id",
+ "id": "u:f360a70eb838",
+ },
+ {
+ "type": "tpl",
+ "title": "时间",
+ "name": "时间",
+ "id": "u:40ba8ecccd71",
+ },
+ {
+ "type": "mapping",
+ "title": "日志等级",
+ "name": "日志等级",
+ "id": "u:8af643bd0487",
+ "placeholder": "-",
+ "map": {
+ "*": "其他",
+ "ERROR": "错误", # noqa: E501
+ "WARNING": "警告", # noqa: E501
+ "SUCCESS": "成功", # noqa: E501
+ "DEBUG": "调试", # noqa: E501
+ "INFO": "正常", # noqa: E501
+ "TRACE": "追溯", # noqa: E501
+ },
+ },
+ {
+ "type": "tpl",
+ "title": "模块",
+ "name": "模块",
+ "id": "u:561fed6e37bf",
+ },
+ {
+ "type": "tpl",
+ "title": "内容",
+ "name": "内容",
+ "id": "u:306a49203d90",
+ },
+ {
+ "type": "operation",
+ "title": "操作",
+ "buttons": [
+ {
+ "type": "button",
+ "label": "查看",
+ "level": "link",
+ "behavior": "View",
+ "onEvent": {"click": {"actions": [ACTION]}},
+ "id": "u:2a40b63939db",
+ }
+ ],
+ "id": "u:b1aead938964",
+ },
+ ],
+ "editorSetting": {
+ "mock": {"enable": True, "maxDisplayRows": 5}
+ },
+ "loadDataOnce": True,
+ "showHeader": True,
+ },
+ ],
+ }
+ return data
diff --git a/gsuid_core/webconsole/mount_app.py b/gsuid_core/webconsole/mount_app.py
index 7a58a79..1a4258b 100644
--- a/gsuid_core/webconsole/mount_app.py
+++ b/gsuid_core/webconsole/mount_app.py
@@ -49,6 +49,7 @@ from gsuid_core.webconsole.create_task_panel import get_tasks_panel
from gsuid_core.webconsole.create_config_panel import get_config_page
from gsuid_core.utils.plugins_config.gs_config import core_plugins_config
from gsuid_core.webconsole.create_analysis_panel import get_analysis_page
+from gsuid_core.webconsole.create_history_log import get_history_logs_page
from gsuid_core.utils.database.models import GsBind, GsPush, GsUser, GsCache
from gsuid_core.webconsole.create_batch_push_panel import get_batch_push_panel
from gsuid_core.webconsole.create_core_config_panel import get_core_config_page
@@ -508,6 +509,21 @@ class LogsPage(GsAdminPage):
return Page.parse_obj(create_log_page())
+@site.register_admin
+class HistoryLogsPage(GsAdminPage):
+ page_schema = PageSchema(
+ label=('历史日志'),
+ icon='fa fa-columns',
+ url='/logs',
+ isDefaultPage=True,
+ sort=100,
+ ) # type: ignore
+
+ @handle_exceptions
+ async def get_page(self, request: Request) -> Page:
+ return Page.parse_obj(get_history_logs_page())
+
+
@site.register_admin
class PushPage(GsAdminPage):
page_schema = PageSchema(