From 3083d9f6272254faef9cb749efcb8a00d796a006 Mon Sep 17 00:00:00 2001 From: left666 <2868322078@qq.com> Date: Thu, 25 Sep 2025 10:03:16 +0800 Subject: [PATCH 1/3] =?UTF-8?q?=E2=9C=A8=20feat(=E6=97=A5=E5=BF=97?= =?UTF-8?q?=E8=BF=BD=E8=B8=AA):=20=E5=A2=9E=E5=BC=BA=E8=AF=B7=E6=B1=82?= =?UTF-8?q?=E8=BF=BD=E8=B8=AA=E5=8A=9F=E8=83=BD=EF=BC=8C=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E8=AF=B7=E6=B1=82=E6=97=A5=E5=BF=97=E8=AE=B0=E5=BD=95=E5=92=8C?= =?UTF-8?q?=E7=8A=B6=E6=80=81=E7=A0=81=E7=9D=80=E8=89=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🔧 refactor(日志工具): 重构日志配置,分离请求日志和普通日志处理 🔧 chore(应用配置): 禁用默认访问日志以使用自定义日志系统 --- ruoyi-fastapi-backend/app.py | 1 + .../middlewares/trace_middleware/span.py | 18 ++- ruoyi-fastapi-backend/utils/log_util.py | 110 ++++++++++++++---- 3 files changed, 104 insertions(+), 25 deletions(-) diff --git a/ruoyi-fastapi-backend/app.py b/ruoyi-fastapi-backend/app.py index 1ee76955..f55a73cd 100644 --- a/ruoyi-fastapi-backend/app.py +++ b/ruoyi-fastapi-backend/app.py @@ -9,4 +9,5 @@ port=AppConfig.app_port, root_path=AppConfig.app_root_path, reload=AppConfig.app_reload, + access_log=False ) diff --git a/ruoyi-fastapi-backend/middlewares/trace_middleware/span.py b/ruoyi-fastapi-backend/middlewares/trace_middleware/span.py index 1e38eab1..49e13015 100644 --- a/ruoyi-fastapi-backend/middlewares/trace_middleware/span.py +++ b/ruoyi-fastapi-backend/middlewares/trace_middleware/span.py @@ -3,11 +3,14 @@ @author: peng @file: span.py @time: 2025/1/17 16:57 +@modifier: left666 +@modify_time: 2025/9/25 9:58 """ - +import time from contextlib import asynccontextmanager from starlette.types import Scope, Message from .ctx import TraceCtx +from utils.log_util import logger class Span: @@ -18,12 +21,18 @@ class Span: def __init__(self, scope: Scope): self.scope = scope + self.client_ip = scope.get('client')[0] + self.start_time = time.time() + self.status_code = None async def request_before(self): """ request_before: 处理header信息等, 如记录请求体信息 """ TraceCtx.set_id() + # 安全获取客户端真实IP + if x_forwarded_for := [v.decode() for k, v in self.scope.get('headers', []) if k.lower() == b'x-forwarded-for']: + self.client_ip = x_forwarded_for[0].split(',')[0].strip() async def request_after(self, message: Message): """ @@ -44,6 +53,13 @@ async def response(self, message: Message): """ if message['type'] == 'http.response.start': message['headers'].append((b'request-id', TraceCtx.get_id().encode())) + self.status_code = message['status'] # 存储状态码 + elif message['type'] == 'http.response.body': + if not message.get('more_body', False): # 是最后一部分响应体时 + # 计算请求处理时间 + duration = round((time.time() - self.start_time) * 1000) + with logger.contextualize(status_code=self.status_code, duration=duration): + logger.log("request", f"{self.client_ip} {self.scope.get('method')} {self.scope.get('path')}") return message diff --git a/ruoyi-fastapi-backend/utils/log_util.py b/ruoyi-fastapi-backend/utils/log_util.py index f953f551..b92081c7 100644 --- a/ruoyi-fastapi-backend/utils/log_util.py +++ b/ruoyi-fastapi-backend/utils/log_util.py @@ -1,16 +1,27 @@ +""" +@modifier: left666 +@modify_time: 2025/9/25 9:00 +""" import os import sys import time from loguru import logger as _logger -from typing import Dict -from middlewares.trace_middleware import TraceCtx class LoggerInitializer: + + format_str = ( + '{time:YYYY-MM-DD HH:mm:ss.S} | ' + '{trace_id} | ' + '{level: <8} | ' + '{name}:{function}:{line} - ' + '{message}' + ) # 自定义日志格式 + def __init__(self): self.log_path = os.path.join(os.getcwd(), 'logs') self.__ensure_log_directory_exists() - self.log_path_error = os.path.join(self.log_path, f'{time.strftime("%Y-%m-%d")}_error.log') + # self.log_path_error = os.path.join(self.log_path, f'{time.strftime("%Y-%m-%d")}_error.log') def __ensure_log_directory_exists(self): """ @@ -20,36 +31,87 @@ def __ensure_log_directory_exists(self): os.mkdir(self.log_path) @staticmethod - def __filter(log: Dict): + def __sift_out_common(log) -> bool: """ - 自定义日志过滤器,添加trace_id + 筛选出非request日志,并添加trace_id """ - log['trace_id'] = TraceCtx.get_id() - return log + if log["level"].name != "request": + from middlewares.trace_middleware import TraceCtx + log['trace_id'] = TraceCtx.get_id() + return True + return False + + @staticmethod + def __status_code_color(status_code: int) -> str: + """根据状态码返回对应的颜色标签""" + if status_code < 200: + return "blue" # 1xx 信息响应 + elif 200 <= status_code < 300: + return "green" # 2xx 成功 + elif 300 <= status_code < 400: + return "cyan" # 3xx 重定向 + elif 400 <= status_code < 500: + return "yellow" # 4xx 客户端错误 + else: + return "red" # 5xx 服务器错误 + + def __format_request_log(self, log) -> str: + """请求记录格式处理器""" + status_code = log["extra"].get("status_code", 0) + duration = log["extra"].get("duration", 0) + if status_code == 0: + return self.format_str + color = self.__status_code_color(status_code) + return ( + "{time:YYYY-MM-DD HH:mm:ss.S} |- " + "{message}" + f"<{color}> {status_code}" + f" -| {duration}ms\n" + ) def init_log(self): """ 初始化日志配置 """ - # 自定义日志格式 - format_str = ( - '{time:YYYY-MM-DD HH:mm:ss.SSS} | ' - '{trace_id} | ' - '{level: <8} | ' - '{name}:{function}:{line} - ' - '{message}' + _logger.level(name="request", no=23, color="") # 自定义等级(介于info与success之间) + _logger.remove() # 移除后重新添加sys.stderr, 目的: 控制台输出与文件日志内容和结构一致 + + # 添加请求记录控制台输出 + _logger.add( + sys.stderr, + filter=lambda log: log["level"].name == "request", + format=self.__format_request_log, + colorize=True, # 终端着色 + enqueue=True # 启用异步安全的日志记录 + ) + # 添加请求记录文件 + _logger.add( + os.path.join(self.log_path, f'request.log'), + rotation="50 MB", # 文件最大50MB + retention=1, # 只保留一个文件 + delay=True, # 延迟到第一条记录消息时再创建文件 + filter=lambda log: log["level"].name == "request", + format=self.__format_request_log, + encoding="utf-8", + enqueue=True + ) + # 添加普通日志控制台输出 + _logger.add( + sys.stderr, + filter=self.__sift_out_common, + format=self.format_str, + colorize=True, + enqueue=True ) - _logger.remove() - # 移除后重新添加sys.stderr, 目的: 控制台输出与文件日志内容和结构一致 - _logger.add(sys.stderr, filter=self.__filter, format=format_str, enqueue=True) + # 添加普通日志文件 _logger.add( - self.log_path_error, - filter=self.__filter, - format=format_str, - rotation='50MB', - encoding='utf-8', - enqueue=True, - compression='zip', + os.path.join(self.log_path, f'{time.strftime("%Y-%m-%d")}_app.log'), + rotation="00:00", # 每天午夜创建新文件 + retention=30, # 保留30个文件 + delay=True, + filter=self.__sift_out_common, + format=self.format_str, + encoding="utf-8" ) return _logger From bcad0ce59b453bff640c9ef834319915b27ca719 Mon Sep 17 00:00:00 2001 From: left666 <2868322078@qq.com> Date: Thu, 9 Oct 2025 10:37:01 +0800 Subject: [PATCH 2/3] =?UTF-8?q?=E2=9C=A8=20perf(trace/logger):=20=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E6=80=A7=E8=83=BD=E8=AE=A1=E6=97=B6=E7=B2=BE=E5=BA=A6?= =?UTF-8?q?=E5=92=8C=E6=97=A5=E5=BF=97=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../middlewares/trace_middleware/span.py | 4 ++-- ruoyi-fastapi-backend/utils/log_util.py | 14 +++++++------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/ruoyi-fastapi-backend/middlewares/trace_middleware/span.py b/ruoyi-fastapi-backend/middlewares/trace_middleware/span.py index 49e13015..d4118a3e 100644 --- a/ruoyi-fastapi-backend/middlewares/trace_middleware/span.py +++ b/ruoyi-fastapi-backend/middlewares/trace_middleware/span.py @@ -22,7 +22,7 @@ class Span: def __init__(self, scope: Scope): self.scope = scope self.client_ip = scope.get('client')[0] - self.start_time = time.time() + self.start_time = time.perf_counter() self.status_code = None async def request_before(self): @@ -57,7 +57,7 @@ async def response(self, message: Message): elif message['type'] == 'http.response.body': if not message.get('more_body', False): # 是最后一部分响应体时 # 计算请求处理时间 - duration = round((time.time() - self.start_time) * 1000) + duration = round((time.perf_counter() - self.start_time) * 1000) with logger.contextualize(status_code=self.status_code, duration=duration): logger.log("request", f"{self.client_ip} {self.scope.get('method')} {self.scope.get('path')}") return message diff --git a/ruoyi-fastapi-backend/utils/log_util.py b/ruoyi-fastapi-backend/utils/log_util.py index b92081c7..e0dd19ea 100644 --- a/ruoyi-fastapi-backend/utils/log_util.py +++ b/ruoyi-fastapi-backend/utils/log_util.py @@ -1,6 +1,6 @@ """ @modifier: left666 -@modify_time: 2025/9/25 9:00 +@modify_time: 2025/10/9 9:00 """ import os import sys @@ -21,7 +21,6 @@ class LoggerInitializer: def __init__(self): self.log_path = os.path.join(os.getcwd(), 'logs') self.__ensure_log_directory_exists() - # self.log_path_error = os.path.join(self.log_path, f'{time.strftime("%Y-%m-%d")}_error.log') def __ensure_log_directory_exists(self): """ @@ -88,12 +87,12 @@ def init_log(self): _logger.add( os.path.join(self.log_path, f'request.log'), rotation="50 MB", # 文件最大50MB - retention=1, # 只保留一个文件 delay=True, # 延迟到第一条记录消息时再创建文件 filter=lambda log: log["level"].name == "request", format=self.__format_request_log, encoding="utf-8", - enqueue=True + enqueue=True, + compression="zip" ) # 添加普通日志控制台输出 _logger.add( @@ -106,12 +105,13 @@ def init_log(self): # 添加普通日志文件 _logger.add( os.path.join(self.log_path, f'{time.strftime("%Y-%m-%d")}_app.log'), - rotation="00:00", # 每天午夜创建新文件 - retention=30, # 保留30个文件 + rotation="50 MB", delay=True, filter=self.__sift_out_common, format=self.format_str, - encoding="utf-8" + encoding="utf-8", + enqueue=True, + compression="zip" ) return _logger From b555c9187ea492e418fd4b6408bf1706cfdaa97e Mon Sep 17 00:00:00 2001 From: left666 <2868322078@qq.com> Date: Tue, 14 Oct 2025 09:52:50 +0800 Subject: [PATCH 3/3] =?UTF-8?q?=E2=9C=A8=20feat(=E5=9C=A8=E7=BA=BF?= =?UTF-8?q?=E7=94=A8=E6=88=B7):=20=E6=B7=BB=E5=8A=A0=E6=89=B9=E9=87=8F?= =?UTF-8?q?=E5=BC=BA=E9=80=80=E5=8A=9F=E8=83=BD=E5=92=8C=E5=88=97=E8=A1=A8?= =?UTF-8?q?=E5=88=B7=E6=96=B0=20=F0=9F=90=9B=20fix(=E6=97=B6=E9=97=B4?= =?UTF-8?q?=E5=B7=A5=E5=85=B7):=20=E4=BF=AE=E5=A4=8D=E6=97=B6=E9=97=B4?= =?UTF-8?q?=E8=A7=A3=E6=9E=90=E6=A0=BC=E5=BC=8F=E9=97=AE=E9=A2=98=20?= =?UTF-8?q?=F0=9F=94=A7=20refactor(=E6=97=A5=E5=BF=97=E6=B3=A8=E8=A7=A3):?= =?UTF-8?q?=20=E4=BC=98=E5=8C=96IP=E8=8E=B7=E5=8F=96=E9=80=BB=E8=BE=91?= =?UTF-8?q?=EF=BC=8C=E6=9B=BF=E6=8D=A2=E5=B7=B2=E5=A4=B1=E6=95=88=E7=9A=84?= =?UTF-8?q?=E5=BD=92=E5=B1=9E=E6=9F=A5=E8=AF=A2API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../module_admin/annotation/log_annotation.py | 4 +- ruoyi-fastapi-frontend/src/utils/ruoyi.js | 9 ++-- .../src/views/monitor/online/index.vue | 53 +++++++++++++++---- 3 files changed, 49 insertions(+), 17 deletions(-) diff --git a/ruoyi-fastapi-backend/module_admin/annotation/log_annotation.py b/ruoyi-fastapi-backend/module_admin/annotation/log_annotation.py index f7e938cf..0621a561 100644 --- a/ruoyi-fastapi-backend/module_admin/annotation/log_annotation.py +++ b/ruoyi-fastapi-backend/module_admin/annotation/log_annotation.py @@ -72,7 +72,7 @@ async def wrapper(*args, **kwargs): # 获取请求的url oper_url = request.url.path # 获取请求的ip及ip归属区域 - oper_ip = request.headers.get('X-Forwarded-For') + oper_ip = request.headers.get('X-Forwarded-For') or request.headers.get('X-Real-IP') or request.client.host oper_location = '内网IP' if AppConfig.app_ip_location_query: oper_location = await get_ip_location(oper_ip) @@ -217,7 +217,7 @@ async def get_ip_location(oper_ip: str): if oper_ip != '127.0.0.1' and oper_ip != 'localhost': oper_location = '未知' async with httpx.AsyncClient() as client: - ip_result = await client.get(f'https://qifu-api.baidubce.com/ip/geo/v1/district?ip={oper_ip}') + ip_result = await client.get(f'https://ip9.com.cn/get?ip={oper_ip}') if ip_result.status_code == 200: prov = ip_result.json().get('data', {}).get('prov') city = ip_result.json().get('data', {}).get('city') diff --git a/ruoyi-fastapi-frontend/src/utils/ruoyi.js b/ruoyi-fastapi-frontend/src/utils/ruoyi.js index 21037428..6a15e8c2 100644 --- a/ruoyi-fastapi-frontend/src/utils/ruoyi.js +++ b/ruoyi-fastapi-frontend/src/utils/ruoyi.js @@ -18,10 +18,11 @@ export function parseTime(time, pattern) { if ((typeof time === 'string') && (/^[0-9]+$/.test(time))) { time = parseInt(time) } else if (typeof time === 'string') { - time = time.replace(new RegExp(/-/gm), '/').replace('T', ' ').replace(new RegExp(/\.[\d]{3}/gm), ''); - } - if ((typeof time === 'number') && (time.toString().length === 10)) { - time = time * 1000 + time = time.trim().replace(/-/g, '/').replace('T', ' ') + if (time.indexOf('+') > -1) { + time = time.substring(0, time.indexOf('+')) + } + time = time.replace(/Z$/, '').replace(/\.\d+/, '') } date = new Date(time) } diff --git a/ruoyi-fastapi-frontend/src/views/monitor/online/index.vue b/ruoyi-fastapi-frontend/src/views/monitor/online/index.vue index eb17ebc3..f2e947d0 100644 --- a/ruoyi-fastapi-frontend/src/views/monitor/online/index.vue +++ b/ruoyi-fastapi-frontend/src/views/monitor/online/index.vue @@ -1,6 +1,6 @@ - + @@ -67,6 +87,10 @@ const loading = ref(true); const total = ref(0); const pageNum = ref(1); const pageSize = ref(10); +const showSearch = ref(true); +const ids = ref([]); +const single = ref(true); +const multiple = ref(true); const queryParams = ref({ ipaddr: undefined, @@ -92,13 +116,20 @@ function resetQuery() { proxy.resetForm("queryRef"); handleQuery(); } +/** 多选框选中数据 */ +function handleSelectionChange(selection) { + ids.value = selection.map(item => item.tokenId); + single.value = selection.length != 1; + multiple.value = !selection.length; +} /** 强退按钮操作 */ function handleForceLogout(row) { - proxy.$modal.confirm('是否确认强退名称为"' + row.userName + '"的用户?').then(function () { - return forceLogout(row.tokenId); + const tokenIds = row.tokenId || ids.value; + proxy.$modal.confirm('是否确认强退名称为"' + row.userName + '"的用户?').then(function () { + return forceLogout(tokenIds); }).then(() => { getList(); - proxy.$modal.msgSuccess("删除成功"); + proxy.$modal.msgSuccess("强退成功"); }).catch(() => {}); }