feat: Add node load submission and display functionality

- Implemented node load status submission in UniProxyController with dynamic cache expiration based on server push interval.
- Added log filtering capability in the admin panel for better log management and analysis.
This commit is contained in:
xboard
2025-05-24 12:31:18 +08:00
parent 61300fbcc3
commit a3700ad685
13 changed files with 270 additions and 75 deletions

View File

@@ -211,4 +211,48 @@ class UniProxyController extends Controller
$this->userOnlineService->updateAliveData($data, $node->type, $node->id);
return response()->json(['data' => true]);
}
// 提交节点负载状态
public function status(Request $request): JsonResponse
{
$node = $request->input('node_info');
$data = $request->validate([
'cpu' => 'required|numeric|min:0|max:100',
'mem.total' => 'required|integer|min:0',
'mem.used' => 'required|integer|min:0',
'swap.total' => 'required|integer|min:0',
'swap.used' => 'required|integer|min:0',
'disk.total' => 'required|integer|min:0',
'disk.used' => 'required|integer|min:0',
]);
$nodeType = $node->type;
$nodeId = $node->id;
$statusData = [
'cpu' => (float) $data['cpu'],
'mem' => [
'total' => (int) $data['mem']['total'],
'used' => (int) $data['mem']['used'],
],
'swap' => [
'total' => (int) $data['swap']['total'],
'used' => (int) $data['swap']['used'],
],
'disk' => [
'total' => (int) $data['disk']['total'],
'used' => (int) $data['disk']['used'],
],
'updated_at' => now()->timestamp,
];
$cacheTime = max(300, (int) admin_setting('server_push_interval', 60) * 3);
cache([
CacheKey::get('SERVER_' . strtoupper($nodeType) . '_LOAD_STATUS', $nodeId) => $statusData,
CacheKey::get('SERVER_' . strtoupper($nodeType) . '_LAST_LOAD_AT', $nodeId) => now()->timestamp,
], $cacheTime);
return response()->json(['data' => true, "code" => 0, "message" => "success"]);
}
}

View File

@@ -128,11 +128,26 @@ class SystemController extends Controller
{
$current = $request->input('current') ? $request->input('current') : 1;
$pageSize = $request->input('page_size') >= 10 ? $request->input('page_size') : 10;
$level = $request->input('level');
$keyword = $request->input('keyword');
$builder = LogModel::orderBy('created_at', 'DESC')
->setFilterAllowKeys('level');
->when($level, function ($query) use ($level) {
return $query->where('level', strtoupper($level));
})
->when($keyword, function ($query) use ($keyword) {
return $query->where(function ($q) use ($keyword) {
$q->where('data', 'like', '%' . $keyword . '%')
->orWhere('context', 'like', '%' . $keyword . '%')
->orWhere('title', 'like', '%' . $keyword . '%')
->orWhere('uri', 'like', '%' . $keyword . '%');
});
});
$total = $builder->count();
$res = $builder->forPage($current, $pageSize)
->get();
return response([
'data' => $res,
'total' => $total

View File

@@ -23,6 +23,7 @@ class ServerRoute
$route->post('push', [UniProxyController::class, 'push']);
$route->post('alive', [UniProxyController::class, 'alive']);
$route->get('alivelist', [UniProxyController::class, 'alivelist']);
$route->post('status', [UniProxyController::class, 'status']);
});
$router->group([
'prefix' => 'ShadowsocksTidalab',

View File

@@ -47,6 +47,7 @@ use Illuminate\Database\Eloquent\Casts\Attribute;
* @property int|null $u 上行流量
* @property int|null $d 下行流量
* @property int|null $total 总流量
* @property-read array|null $load_status 负载状态包含CPU、内存、交换区、磁盘信息
*/
class Server extends Model
{
@@ -432,4 +433,18 @@ class Server extends Model
}
);
}
/**
* 负载状态访问器
*/
protected function loadStatus(): Attribute
{
return Attribute::make(
get: function () {
$type = strtoupper($this->type);
$serverId = $this->parent_id ?: $this->id;
return Cache::get(CacheKey::get("SERVER_{$type}_LOAD_STATUS", $serverId));
}
);
}
}

View File

@@ -25,7 +25,8 @@ class ServerService
'online',
'is_online',
'available_status',
'cache_key'
'cache_key',
'load_status'
]);
}

View File

@@ -40,7 +40,7 @@ class UpdateService
list($date, $hash) = explode(':', trim($result->output()));
Cache::forever(self::CACHE_VERSION_DATE, $date);
Cache::forever(self::CACHE_VERSION, substr($hash, 0, 7));
Log::info('Version cache updated: ' . $date . '-' . substr($hash, 0, 7));
// Log::info('Version cache updated: ' . $date . '-' . substr($hash, 0, 7));
return;
}
} catch (\Exception $e) {

View File

@@ -4,53 +4,10 @@ namespace App\Utils;
class CacheKey
{
const KEYS = [
// 核心缓存键定义
const CORE_KEYS = [
'EMAIL_VERIFY_CODE' => '邮箱验证码',
'LAST_SEND_EMAIL_VERIFY_TIMESTAMP' => '最后一次发送邮箱验证码时间',
'SERVER_VMESS_ONLINE_USER' => '节点在线用户',
'MULTI_SERVER_VMESS_ONLINE_USER' => '节点多服务器在线用户',
'SERVER_VMESS_LAST_CHECK_AT' => '节点最后检查时间',
'SERVER_VMESS_LAST_PUSH_AT' => '节点最后推送时间',
'SERVER_TROJAN_ONLINE_USER' => 'trojan节点在线用户',
'MULTI_SERVER_TROJAN_ONLINE_USER' => 'trojan节点多服务器在线用户',
'SERVER_TROJAN_LAST_CHECK_AT' => 'trojan节点最后检查时间',
'SERVER_TROJAN_LAST_PUSH_AT' => 'trojan节点最后推送时间',
'SERVER_SHADOWSOCKS_ONLINE_USER' => 'ss节点在线用户',
'MULTI_SERVER_SHADOWSOCKS_ONLINE_USER' => 'ss节点多服务器在线用户',
'SERVER_SHADOWSOCKS_LAST_CHECK_AT' => 'ss节点最后检查时间',
'SERVER_SHADOWSOCKS_LAST_PUSH_AT' => 'ss节点最后推送时间',
'SERVER_HYSTERIA_ONLINE_USER' => 'hysteria节点在线用户',
'MULTI_SERVER_HYSTERIA_ONLINE_USER' => 'hysteria节点多服务器在线用户',
'SERVER_HYSTERIA_LAST_CHECK_AT' => 'hysteria节点最后检查时间',
'SERVER_HYSTERIA_LAST_PUSH_AT' => 'hysteria节点最后推送时间',
'SERVER_VLESS_ONLINE_USER' => 'vless节点在线用户',
'MULTI_SERVER_VLESS_ONLINE_USER' => 'vless节点多服务器在线用户',
'SERVER_VLESS_LAST_CHECK_AT' => 'vless节点最后检查时间',
'SERVER_VLESS_LAST_PUSH_AT' => 'vless节点最后推送时间',
'SERVER_TUIC_ONLINE_USER' => 'TUIC节点在线用户',
'MULTI_SERVER_TUIC_ONLINE_USER' => 'TUIC节点多服务器在线用户',
'SERVER_TUIC_LAST_CHECK_AT' => 'TUIC节点最后检查时间',
'SERVER_TUIC_LAST_PUSH_AT' => 'TUIC节点最后推送时间',
'SERVER_ANYTLS_ONLINE_USER' => 'ANYTLS节点在线用户',
'MULTI_SERVER_ANYTLS_ONLINE_USER' => 'ANYTLS节点多服务器在线用户',
'SERVER_ANYTLS_LAST_CHECK_AT' => 'ANYTLS节点最后检查时间',
'SERVER_ANYTLS_LAST_PUSH_AT' => 'ANYTLS节点最后推送时间',
'SERVER_SOCKS_ONLINE_USER' => 'socks节点在线用户',
'MULTI_SERVER_SOCKS_ONLINE_USER' => 'socks节点多服务器在线用户',
'SERVER_SOCKS_LAST_CHECK_AT' => 'socks节点最后检查时间',
'SERVER_SOCKS_LAST_PUSH_AT' => 'socks节点最后推送时间',
'SERVER_NAIVE_ONLINE_USER' => 'naive节点在线用户',
'MULTI_SERVER_NAIVE_ONLINE_USER' => 'naive节点多服务器在线用户',
'SERVER_NAIVE_LAST_CHECK_AT' => 'naive节点最后检查时间',
'SERVER_NAIVE_LAST_PUSH_AT' => 'naive节点最后推送时间',
'SERVER_HTTP_ONLINE_USER' => 'http节点在线用户',
'MULTI_SERVER_HTTP_ONLINE_USER' => 'http节点多服务器在线用户',
'SERVER_HTTP_LAST_CHECK_AT' => 'http节点最后检查时间',
'SERVER_HTTP_LAST_PUSH_AT' => 'http节点最后推送时间',
'SERVER_MIERU_ONLINE_USER' => 'mieru节点在线用户',
'MULTI_SERVER_MIERU_ONLINE_USER' => 'mieru节点多服务器在线用户',
'SERVER_MIERU_LAST_CHECK_AT' => 'mieru节点最后检查时间',
'SERVER_MIERU_LAST_PUSH_AT' => 'mieru节点最后推送时间',
'TEMP_TOKEN' => '临时令牌',
'LAST_SEND_EMAIL_REMIND_TRAFFIC' => '最后发送流量邮件提醒',
'SCHEDULE_LAST_CHECK_AT' => '计划任务最后检查时间',
@@ -61,11 +18,50 @@ class CacheKey
'FORGET_REQUEST_LIMIT' => '找回密码次数限制'
];
public static function get(string $key, $uniqueValue)
// 允许的缓存键模式(支持通配符)
const ALLOWED_PATTERNS = [
'SERVER_*_ONLINE_USER', // 节点在线用户
'MULTI_SERVER_*_ONLINE_USER', // 多服务器在线用户
'SERVER_*_LAST_CHECK_AT', // 节点最后检查时间
'SERVER_*_LAST_PUSH_AT', // 节点最后推送时间
'SERVER_*_LOAD_STATUS', // 节点负载状态
'SERVER_*_LAST_LOAD_AT', // 节点最后负载提交时间
];
/**
* 生成缓存键
*/
public static function get(string $key, $uniqueValue = null): string
{
if (!in_array($key, array_keys(self::KEYS))) {
abort(500, 'key is not in cache key list');
// 检查是否为核心键
if (array_key_exists($key, self::CORE_KEYS)) {
return $uniqueValue ? $key . '_' . $uniqueValue : $key;
}
return $key . '_' . $uniqueValue;
// 检查是否匹配允许的模式
if (self::matchesPattern($key)) {
return $uniqueValue ? $key . '_' . $uniqueValue : $key;
}
// 开发环境下记录警告,生产环境允许通过
if (app()->environment('local', 'development')) {
logger()->warning("Unknown cache key used: {$key}");
}
return $uniqueValue ? $key . '_' . $uniqueValue : $key;
}
/**
* 检查键名是否匹配允许的模式
*/
private static function matchesPattern(string $key): bool
{
foreach (self::ALLOWED_PATTERNS as $pattern) {
$regex = '/^' . str_replace('*', '[A-Z_]+', $pattern) . '$/';
if (preg_match($regex, $key)) {
return true;
}
}
return false;
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -866,6 +866,7 @@ window.XBOARD_TRANSLATIONS['en-US'] = {
"save": "Save",
"cancel": "Cancel",
"confirm": "Confirm",
"close": "Close",
"delete": {
"success": "Deleted successfully",
"failed": "Failed to delete"
@@ -1061,14 +1062,36 @@ window.XBOARD_TRANSLATIONS['en-US'] = {
"level": "Level",
"time": "Time",
"message": "Message",
"logTitle": "Title",
"method": "Method",
"action": "Action",
"context": "Context",
"search": "Search logs...",
"noLogs": "No logs available",
"noInfoLogs": "No info logs available",
"noWarningLogs": "No warning logs available",
"noErrorLogs": "No error logs available",
"noSearchResults": "No matching logs found",
"detailTitle": "Log Details",
"viewDetail": "View Details",
"totalLogs": "Total logs: {{count}}"
"host": "Host",
"ip": "IP Address",
"uri": "URI",
"requestData": "Request Data",
"exception": "Exception",
"totalLogs": "Total logs: {{count}}",
"tabs": {
"all": "All",
"info": "Info",
"warning": "Warning",
"error": "Error"
},
"filter": {
"searchAndLevel": "Filter results: {{count}} logs containing \\\"{{keyword}}\\\" with level \\\"{{level}}\\\"",
"searchOnly": "Search results: {{count}} logs containing \\\"{{keyword}}\\\"",
"levelOnly": "Filter results: {{count}} logs with level \\\"{{level}}\\\"",
"reset": "Reset Filters"
}
},
"common": {
"refresh": "Refresh",
@@ -1547,6 +1570,17 @@ window.XBOARD_TRANSLATIONS['en-US'] = {
"tooltip": "Groups that can subscribe to this node",
"empty": "--"
},
"loadStatus": {
"title": "Load Status",
"tooltip": "Server resource usage",
"noData": "No Data",
"details": "System Load Details",
"cpu": "CPU Usage",
"memory": "Memory Usage",
"swap": "Swap Usage",
"disk": "Disk Usage",
"lastUpdate": "Last Updated"
},
"type": "Type",
"actions": "Actions",
"copyAddress": "Copy Connection Address",

View File

@@ -864,6 +864,7 @@ window.XBOARD_TRANSLATIONS['ko-KR'] = {
"save": "저장",
"cancel": "취소",
"confirm": "확인",
"close": "닫기",
"delete": {
"success": "삭제되었습니다",
"failed": "삭제에 실패했습니다"
@@ -1070,6 +1071,49 @@ window.XBOARD_TRANSLATIONS['ko-KR'] = {
"action": "작업"
}
},
"systemLog": {
"title": "시스템 로그",
"description": "시스템 운영 로그 조회",
"viewAll": "모두 보기",
"level": "레벨",
"time": "시간",
"message": "메시지",
"logTitle": "제목",
"method": "요청 방법",
"action": "작업",
"context": "컨텍스트",
"search": "로그 검색...",
"noLogs": "로그 없음",
"noInfoLogs": "정보 로그 없음",
"noWarningLogs": "경고 로그 없음",
"noErrorLogs": "오류 로그 없음",
"noSearchResults": "일치하는 로그가 없습니다",
"detailTitle": "로그 세부 정보",
"viewDetail": "세부 정보 보기",
"host": "호스트",
"ip": "IP 주소",
"uri": "URI",
"requestData": "요청 데이터",
"exception": "예외",
"totalLogs": "총 로그 수: {{count}}",
"tabs": {
"all": "전체",
"info": "정보",
"warning": "경고",
"error": "오류"
},
"filter": {
"searchAndLevel": "필터 결과: \\\"{{keyword}}\\\"를 포함하고 레벨이 \\\"{{level}}\\\"인 로그 {{count}}개",
"searchOnly": "검색 결과: \\\"{{keyword}}\\\"를 포함하는 로그 {{count}}개",
"levelOnly": "필터 결과: 레벨이 \\\"{{level}}\\\"인 로그 {{count}}개",
"reset": "필터 초기화"
}
},
"common": {
"refresh": "새로고침",
"close": "닫기",
"pagination": "{{current}}/{{total}} 페이지, 총 {{count}}개 항목"
},
"search": {
"placeholder": "메뉴 및 기능 검색...",
"title": "메뉴 네비게이션",
@@ -1542,6 +1586,17 @@ window.XBOARD_TRANSLATIONS['ko-KR'] = {
"tooltip": "이 노드를 구독할 수 있는 그룹",
"empty": "--"
},
"loadStatus": {
"title": "부하 상태",
"tooltip": "서버 리소스 사용량",
"noData": "데이터 없음",
"details": "시스템 부하 세부정보",
"cpu": "CPU 사용률",
"memory": "메모리 사용량",
"swap": "스왑 사용량",
"disk": "디스크 사용량",
"lastUpdate": "마지막 업데이트"
},
"type": "유형",
"actions": "작업",
"copyAddress": "연결 주소 복사",

View File

@@ -871,6 +871,7 @@ window.XBOARD_TRANSLATIONS['zh-CN'] = {
"save": "保存",
"cancel": "取消",
"confirm": "确认",
"close": "关闭",
"delete": {
"success": "删除成功",
"failed": "删除失败"
@@ -1059,14 +1060,36 @@ window.XBOARD_TRANSLATIONS['zh-CN'] = {
"level": "级别",
"time": "时间",
"message": "消息",
"logTitle": "标题",
"method": "请求方法",
"action": "操作",
"context": "上下文",
"search": "搜索日志内容...",
"noLogs": "暂无日志记录",
"noInfoLogs": "暂无信息日志记录",
"noWarningLogs": "暂无警告日志记录",
"noErrorLogs": "暂无错误日志记录",
"noSearchResults": "没有匹配的日志记录",
"detailTitle": "日志详情",
"viewDetail": "查看详情",
"totalLogs": "总日志数:{{count}}"
"host": "主机",
"ip": "IP地址",
"uri": "URI",
"requestData": "请求数据",
"exception": "异常信息",
"totalLogs": "总日志数:{{count}}",
"tabs": {
"all": "全部",
"info": "信息",
"warning": "警告",
"error": "错误"
},
"filter": {
"searchAndLevel": "筛选结果: 包含\"{{keyword}}\"且级别为\"{{level}}\"的日志共 {{count}} 条",
"searchOnly": "搜索结果: 包含\"{{keyword}}\"的日志共 {{count}} 条",
"levelOnly": "筛选结果: 级别为\"{{level}}\"的日志共 {{count}} 条",
"reset": "重置筛选"
}
},
"common": {
"refresh": "刷新",
@@ -1514,6 +1537,17 @@ window.XBOARD_TRANSLATIONS['zh-CN'] = {
"tooltip": "可订阅到该节点的权限组",
"empty": "--"
},
"loadStatus": {
"title": "负载状态",
"tooltip": "服务器资源使用情况",
"noData": "暂无数据",
"details": "系统负载详情",
"cpu": "CPU 使用率",
"memory": "内存使用",
"swap": "交换区",
"disk": "磁盘使用",
"lastUpdate": "最后更新"
},
"type": "类型",
"actions": "操作",
"copyAddress": "复制连接地址",