网页控制台新增历史日志模块, 可以按照日志等级等条件过滤或查询

This commit is contained in:
KimigaiiWuyi 2024-11-22 05:36:09 +08:00
parent 3f6b926c33
commit 90ba2fd54c
4 changed files with 522 additions and 7 deletions

View File

@ -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'
]

View File

@ -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)

View File

@ -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": {
"*": "<span class='label label-default'>其他</span>",
"ERROR": "<span class='label label-danger'>错误</span>", # noqa: E501
"WARNING": "<span class='label label-label label-warning'>警告</span>", # noqa: E501
"SUCCESS": "<span class='label label-success'>成功</span>", # noqa: E501
"DEBUG": "<span class=\"label\" style=\"background-color: rgb(58, 118, 251); color: white;\">调试</span>", # noqa: E501
"INFO": "<span class=\"label\" style=\"background-color: rgb(140, 140, 140); color: rgb(255, 255, 255);\">正常</span>", # noqa: E501
"TRACE": "<span class=\"label\" style=\"background-color: rgb(235, 62, 247); color: white;\">追溯</span>", # 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

View File

@ -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(