Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions ruoyi-fastapi-backend/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@
port=AppConfig.app_port,
root_path=AppConfig.app_root_path,
reload=AppConfig.app_reload,
access_log=False
)
18 changes: 17 additions & 1 deletion ruoyi-fastapi-backend/middlewares/trace_middleware/span.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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.perf_counter()
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):
"""
Expand All @@ -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.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


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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')
Expand Down
108 changes: 85 additions & 23 deletions ruoyi-fastapi-backend/utils/log_util.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,26 @@
"""
@modifier: left666
@modify_time: 2025/10/9 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 = (
'<green>{time:YYYY-MM-DD HH:mm:ss.S}</green> | '
'<cyan>{trace_id}</cyan> | '
'<level>{level: <8}</level> | '
'<cyan>{name}</cyan>:<cyan>{function}</cyan>:<cyan>{line}</cyan> - '
'<level>{message}</level>'
) # 自定义日志格式

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):
"""
Expand All @@ -20,36 +30,88 @@ 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 (
"<green>{time:YYYY-MM-DD HH:mm:ss.S}</green> |- "
"<level>{message}</level>"
f"<{color}> {status_code}</{color}>"
f" -| <level>{duration}ms</level>\n"
)

def init_log(self):
"""
初始化日志配置
"""
# 自定义日志格式
format_str = (
'<green>{time:YYYY-MM-DD HH:mm:ss.SSS}</green> | '
'<cyan>{trace_id}</cyan> | '
'<level>{level: <8}</level> | '
'<cyan>{name}</cyan>:<cyan>{function}</cyan>:<cyan>{line}</cyan> - '
'<level>{message}</level>'
_logger.level(name="request", no=23, color="<magenta>") # 自定义等级(介于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
delay=True, # 延迟到第一条记录消息时再创建文件
filter=lambda log: log["level"].name == "request",
format=self.__format_request_log,
encoding="utf-8",
enqueue=True,
compression="zip"
)
# 添加普通日志控制台输出
_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',
os.path.join(self.log_path, f'{time.strftime("%Y-%m-%d")}_app.log'),
rotation="50 MB",
delay=True,
filter=self.__sift_out_common,
format=self.format_str,
encoding="utf-8",
enqueue=True,
compression='zip',
compression="zip"
)

return _logger
Expand Down
9 changes: 5 additions & 4 deletions ruoyi-fastapi-frontend/src/utils/ruoyi.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
53 changes: 42 additions & 11 deletions ruoyi-fastapi-frontend/src/views/monitor/online/index.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<template>
<div class="app-container">
<el-form :model="queryParams" ref="queryRef" :inline="true">
<el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch">
<el-form-item label="登录地址" prop="ipaddr">
<el-input
v-model="queryParams.ipaddr"
Expand All @@ -24,16 +24,34 @@
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>

<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button
type="danger"
plain
icon="Remove"
:disabled="multiple"
@click="handleForceLogout"
v-hasPermi="['monitor:online:batchLogout']"
>强退</el-button>
</el-col>

<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
</el-row>

<el-table
v-loading="loading"
:data="onlineList.slice((pageNum - 1) * pageSize, pageNum * pageSize)"
style="width: 100%;"
@selection-change="handleSelectionChange"
>
<el-table-column label="序号" width="50" type="index" align="center">
<template #default="scope">
<span>{{ (pageNum - 1) * pageSize + scope.$index + 1 }}</span>
</template>
</el-table-column>
<!-- <el-table-column label="序号" width="50" type="index" align="center">-->
<!-- <template #default="scope">-->
<!-- <span>{{ (pageNum - 1) * pageSize + scope.$index + 1 }}</span>-->
<!-- </template>-->
<!-- </el-table-column>-->
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="会话编号" align="center" prop="tokenId" :show-overflow-tooltip="true" />
<el-table-column label="登录名称" align="center" prop="userName" :show-overflow-tooltip="true" />
<el-table-column label="所属部门" align="center" prop="deptName" :show-overflow-tooltip="true" />
Expand All @@ -46,9 +64,11 @@
<span>{{ parseTime(scope.row.loginTime) }}</span>
</template>
</el-table-column>
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<el-table-column label="操作" width="100" align="center" class-name="small-padding fixed-width">
<template #default="scope">
<el-button link type="primary" icon="Delete" @click="handleForceLogout(scope.row)" v-hasPermi="['monitor:online:forceLogout']">强退</el-button>
<el-tooltip content="强退" placement="top">
<el-button link type="primary" icon="Remove" @click="handleForceLogout(scope.row)" v-hasPermi="['monitor:online:forceLogout']"></el-button>
</el-tooltip>
</template>
</el-table-column>
</el-table>
Expand All @@ -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,
Expand All @@ -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(() => {});
}

Expand Down