feat: node stat

This commit is contained in:
bwcx_jzy 2022-01-22 16:11:58 +08:00
parent e78c655633
commit 04da92db0a
No known key found for this signature in database
GPG Key ID: 5E48E9372088B9E5
27 changed files with 797 additions and 139 deletions

View File

@ -4,9 +4,10 @@
### 新增功能
1. 【server】系统配置新增节点白名单、节点系统配置分发功能方便集群节点统一配置
2. 【server】构建新增快捷复制功能,方便快速创建类型一致的项目
3. 【server】系统配置新增配置菜单是否显示,用于非超级管理员页面菜单控制
1. 【server】新增系统配置-节点白名单、节点系统配置分发功能,方便集群节点统一配置
2. 【server】新增构建快捷复制功能,方便快速创建类型一致的项目
3. 【server】新增系统配置-配置菜单是否显示,用于非超级管理员页面菜单控制
4. 【server】新增节点统计功能快速了解当前所有节点状态
### 解决BUG、优化功能
@ -14,6 +15,8 @@
2. 【server】修复快速安装服务端 ping 检查超时时间 5ms to 5s
3. 项目文本文件支持在线实时阅读(感谢@)
4. 【server】控制台日志支持搜索高亮
5. 【server】跨工作空间更新节点授权将自动同步更新
6. 【server】取消节点监控周期字段采用全局统计
------

View File

@ -22,6 +22,7 @@
*/
package io.jpom.common.commander.impl;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.text.CharPool;
@ -207,12 +208,13 @@ public class LinuxSystemCommander extends AbstractSystemCommander {
* @param info cpu信息
* @return cpu信息
*/
private static String getLinuxCpu(String info) {
if (StrUtil.isEmpty(info)) {
public static String getLinuxCpu(String info) {
List<String> strings = StrUtil.splitTrim(info, StrUtil.COLON);
String last = CollUtil.getLast(strings);
List<String> list = StrUtil.splitTrim(last, StrUtil.COMMA);
if (CollUtil.isEmpty(list)) {
return null;
}
int i = info.indexOf(CharPool.COLON);
String[] split = info.substring(i + 1).split(StrUtil.COMMA);
// 1.3% us 用户空间占用CPU的百分比
// 1.0% sy 内核空间占用CPU的百分比
// 0.0% ni 改变过优先级的进程占用CPU的百分比
@ -220,17 +222,12 @@ public class LinuxSystemCommander extends AbstractSystemCommander {
// 0.0% wa IO等待占用CPU的百分比
// 0.3% hi 硬中断Hardware IRQ占用CPU的百分比
// 0.0% si 软中断Software Interrupts占用CPU的百分比
for (String str : split) {
str = str.trim();
String value = str.substring(0, str.length() - 2).trim();
String tag = str.substring(str.length() - 2);
if ("id".equalsIgnoreCase(tag)) {
value = value.replace("%", "");
double val = Convert.toDouble(value, 0.0);
return String.format("%.2f", 100.00 - val);
}
String value = list.stream().filter(s -> StrUtil.endWithIgnoreCase(s, "id")).map(s -> StrUtil.removeSuffixIgnoreCase(s, "id")).findAny().orElse(null);
Double val = Convert.toDouble(value);
if (val == null) {
return null;
}
return "0";
return String.format("%.2f", 100.00 - val);
}
@Override

View File

@ -136,6 +136,8 @@ public class AutoRegSeverNode {
* @param url 服务端url
*/
public static void autoPushToServer(String url) {
url = StrUtil.removeSuffix(url, CharPool.SINGLE_QUOTE + "");
url = StrUtil.removePrefix(url, CharPool.SINGLE_QUOTE + "");
UrlBuilder urlBuilder = UrlBuilder.ofHttp(url);
//
LinkedHashSet<InetAddress> localAddressList = NetUtil.localAddressList(address -> {

View File

@ -23,6 +23,7 @@
import cn.hutool.core.lang.RegexPool;
import cn.hutool.core.util.NumberUtil;
import cn.hutool.core.util.ReUtil;
import io.jpom.common.commander.impl.LinuxSystemCommander;
import org.junit.Test;
/**
@ -47,4 +48,10 @@ public class TestStr {
System.out.println(String.format("%.2f", (float)1 / (float)2 * 100));
System.out.println(NumberUtil.div(1,2));
}
@Test
public void testParse(){
String linuxCpu = LinuxSystemCommander.getLinuxCpu("%Cpu(s): 0.0 us, 0.0 sy, 0.0 ni,100.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st\n");
System.out.println(linuxCpu);
}
}

View File

@ -34,6 +34,7 @@ public enum ClassFeature {
*/
NULL(""),
NODE("节点管理"),
NODE_STAT("节点统计"),
UPGRADE_NODE_LIST("节点升级"),
SEARCH_PROJECT("搜索项目"),
SSH("SSH管理"),

View File

@ -127,7 +127,7 @@ public class PermissionInterceptor extends BaseJpomInterceptor {
String workspaceId = ServletUtil.getHeader(request, Const.WORKSPACEID_REQ_HEADER, CharsetUtil.CHARSET_UTF_8);
boolean exists = userBindWorkspaceService.exists(userModel.getId(), workspaceId + StrUtil.DASHED + method.name());
if (!exists) {
this.errorMsg(response, "您没有对应功能管理权限:" + method.getName());
this.errorMsg(response, "您没有对应功能" + feature.cls().getName() + "管理权限:" + method.getName());
return false;
}
}

View File

@ -0,0 +1,53 @@
package io.jpom.controller.node;
import cn.hutool.core.collection.CollStreamUtil;
import cn.hutool.db.Entity;
import cn.jiangzeyin.common.JsonMessage;
import io.jpom.common.BaseServerController;
import io.jpom.model.PageResultDto;
import io.jpom.model.stat.NodeStatModel;
import io.jpom.plugin.ClassFeature;
import io.jpom.plugin.Feature;
import io.jpom.plugin.MethodFeature;
import io.jpom.service.stat.NodeStatService;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
import java.util.Map;
/**
* @author bwcx_jzy
* @since 2022/1/22
*/
@RestController
@RequestMapping(value = "/node/stat")
@Feature(cls = ClassFeature.NODE_STAT)
public class NodeStatController extends BaseServerController {
private final NodeStatService nodeStatService;
public NodeStatController(NodeStatService nodeStatService) {
this.nodeStatService = nodeStatService;
}
@PostMapping(value = "list_data.json", produces = MediaType.APPLICATION_JSON_VALUE)
@Feature(method = MethodFeature.LIST)
public String listJson() {
PageResultDto<NodeStatModel> nodeModelPageResultDto = nodeStatService.listPage(getRequest());
return JsonMessage.getString(200, "", nodeModelPageResultDto);
}
@GetMapping(value = "status_stat.json", produces = MediaType.APPLICATION_JSON_VALUE)
@Feature(method = MethodFeature.LIST)
public String statusStat() {
String workspaceId = nodeStatService.getCheckUserWorkspace(getRequest());
String sql = "select `status`,count(1) as cunt from " + nodeStatService.getTableName() + " where workspaceId=? group by `status`";
List<Entity> list = nodeStatService.query(sql, workspaceId);
Map<String, Integer> map = CollStreamUtil.toMap(list, entity -> entity.getStr("status"), entity -> entity.getInt("cunt"));
return JsonMessage.getString(200, "", map);
}
}

View File

@ -16,7 +16,6 @@ import cn.jiangzeyin.controller.multipart.MultipartFileBuilder;
import com.alibaba.fastjson.JSONObject;
import io.jpom.common.BaseServerController;
import io.jpom.common.Type;
import io.jpom.model.Cycle;
import io.jpom.model.data.NodeModel;
import io.jpom.model.data.SshModel;
import io.jpom.model.system.AgentAutoUser;
@ -199,7 +198,7 @@ public class SshInstallAgentController extends BaseServerController {
Assert.hasText(nodeModel.getName(), "输入节点名称");
Assert.hasText(nodeModel.getUrl(), "请输入节点地址");
nodeModel.setCycle(Cycle.one.getCode());
//nodeModel.setCycle(Cycle.one.getCode());
//
//nodeModel.setProtocol(StrUtil.emptyToDefault(nodeModel.getProtocol(), "http"));
//

View File

@ -69,6 +69,7 @@ public class NodeModel extends BaseGroupModel {
*
* @see io.jpom.model.Cycle
*/
@Deprecated
private Integer cycle;
public String getName() {
@ -79,6 +80,7 @@ public class NodeModel extends BaseGroupModel {
this.name = name;
}
@Deprecated
public Integer getCycle() {
return cycle;
}
@ -87,6 +89,7 @@ public class NodeModel extends BaseGroupModel {
* @param cycle 监控频率
* @see io.jpom.model.Cycle
*/
@Deprecated
public void setCycle(Integer cycle) {
this.cycle = cycle;
}

View File

@ -0,0 +1,196 @@
/*
* The MIT License (MIT)
*
* Copyright (c) 2019 Code Technology Studio
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package io.jpom.model.stat;
import io.jpom.model.BaseWorkspaceModel;
import io.jpom.model.log.SystemMonitorLog;
import io.jpom.service.h2db.TableName;
/**
* @author bwcx_jzy
* @see SystemMonitorLog
* @since 2022/1/22
*/
@TableName(value = "NODE_STAT", name = "节点统计")
public class NodeStatModel extends BaseWorkspaceModel {
/**
* 占用cpu
*/
private Double occupyCpu;
/**
* 占用内存 总共
*/
private Double occupyMemory;
/**
* 占用内存 (使用) @author jzy
*/
private Double occupyMemoryUsed;
/**
* 占用磁盘
*/
private Double occupyDisk;
/**
* 网络耗时
*/
private Integer networkTime;
/**
* 运行时间
*/
private String upTimeStr;
/**
* 系统名称
*/
private String osName;
/**
* jpom 版本
*/
private String jpomVersion;
/**
* 状态{1无法连接0 正常, 2 授权信息错误, 3 状态码错误}
*/
private Integer status;
/**
* 开启状态如果关闭状态就暂停使用节点
*/
private Integer openStatus;
/**
* 错误消息
*/
private String failureMsg;
/**
* 节点地址
*/
private String url;
/**
* 节点名称
*/
private String name;
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getFailureMsg() {
return failureMsg;
}
public void setFailureMsg(String failureMsg) {
this.failureMsg = failureMsg;
}
public Double getOccupyCpu() {
return occupyCpu;
}
public void setOccupyCpu(Double occupyCpu) {
this.occupyCpu = occupyCpu;
}
public Double getOccupyMemory() {
return occupyMemory;
}
public void setOccupyMemory(Double occupyMemory) {
this.occupyMemory = occupyMemory;
}
public Double getOccupyMemoryUsed() {
return occupyMemoryUsed;
}
public void setOccupyMemoryUsed(Double occupyMemoryUsed) {
this.occupyMemoryUsed = occupyMemoryUsed;
}
public Double getOccupyDisk() {
return occupyDisk;
}
public void setOccupyDisk(Double occupyDisk) {
this.occupyDisk = occupyDisk;
}
public Integer getNetworkTime() {
return networkTime;
}
public void setNetworkTime(Integer networkTime) {
this.networkTime = networkTime;
}
public String getUpTimeStr() {
return upTimeStr;
}
public void setUpTimeStr(String upTimeStr) {
this.upTimeStr = upTimeStr;
}
public String getOsName() {
return osName;
}
public void setOsName(String osName) {
this.osName = osName;
}
public String getJpomVersion() {
return jpomVersion;
}
public void setJpomVersion(String jpomVersion) {
this.jpomVersion = jpomVersion;
}
public Integer getStatus() {
return status;
}
public void setStatus(Integer status) {
this.status = status;
}
public Integer getOpenStatus() {
return openStatus;
}
public void setOpenStatus(Integer openStatus) {
this.openStatus = openStatus;
}
}

View File

@ -22,7 +22,11 @@
*/
package io.jpom.monitor;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.date.SystemClock;
import cn.hutool.core.thread.ThreadUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.cron.CronUtil;
import cn.hutool.cron.pattern.CronPattern;
import cn.hutool.cron.task.Task;
@ -30,16 +34,22 @@ import cn.jiangzeyin.common.DefaultSystemLog;
import cn.jiangzeyin.common.JsonMessage;
import cn.jiangzeyin.common.spring.SpringUtil;
import com.alibaba.fastjson.JSONObject;
import io.jpom.common.BaseServerController;
import io.jpom.common.forward.NodeForward;
import io.jpom.common.forward.NodeUrl;
import io.jpom.cron.CronUtils;
import io.jpom.model.Cycle;
import io.jpom.model.data.NodeModel;
import io.jpom.model.data.UserModel;
import io.jpom.model.log.SystemMonitorLog;
import io.jpom.model.stat.NodeStatModel;
import io.jpom.service.dblog.DbSystemMonitorLogService;
import io.jpom.service.node.NodeService;
import io.jpom.cron.CronUtils;
import io.jpom.service.stat.NodeStatService;
import io.jpom.system.AuthorizeException;
import java.util.List;
import java.util.stream.Collectors;
/**
* 节点监控
@ -52,6 +62,8 @@ public class NodeMonitor implements Task {
private static final String CRON_ID = "node_monitor";
private static DbSystemMonitorLogService dbSystemMonitorLogService;
private static NodeStatService nodeStatService;
private static NodeService nodeService;
/**
* 开启调度
@ -62,7 +74,18 @@ public class NodeMonitor implements Task {
CronPattern cronPattern = Cycle.seconds30.getCronPattern();
CronUtils.upsert(CRON_ID, cronPattern.toString(), new NodeMonitor());
}
dbSystemMonitorLogService = SpringUtil.getBean(DbSystemMonitorLogService.class);
}
private void init() {
if (dbSystemMonitorLogService == null) {
dbSystemMonitorLogService = SpringUtil.getBean(DbSystemMonitorLogService.class);
}
if (nodeStatService == null) {
nodeStatService = SpringUtil.getBean(NodeStatService.class);
}
if (nodeService == null) {
nodeService = SpringUtil.getBean(NodeService.class);
}
}
public static void stop() {
@ -71,61 +94,153 @@ public class NodeMonitor implements Task {
@Override
public void execute() {
long time = System.currentTimeMillis();
this.init();
NodeService nodeService = SpringUtil.getBean(NodeService.class);
//
List<NodeModel> nodeModels = nodeService.listByCycle(Cycle.seconds30);
//
if (Cycle.one.getCronPattern().match(time, CronUtil.getScheduler().isMatchSecond())) {
nodeModels.addAll(nodeService.listByCycle(Cycle.one));
}
//
if (Cycle.five.getCronPattern().match(time, CronUtil.getScheduler().isMatchSecond())) {
nodeModels.addAll(nodeService.listByCycle(Cycle.five));
}
//
if (Cycle.ten.getCronPattern().match(time, CronUtil.getScheduler().isMatchSecond())) {
nodeModels.addAll(nodeService.listByCycle(Cycle.ten));
}
//
if (Cycle.thirty.getCronPattern().match(time, CronUtil.getScheduler().isMatchSecond())) {
nodeModels.addAll(nodeService.listByCycle(Cycle.thirty));
}
List<NodeModel> nodeModels = nodeService.listDeDuplicationByUrl();
//
this.checkList(nodeModels);
}
private void checkList(List<NodeModel> nodeModels) {
if (nodeModels == null || nodeModels.isEmpty()) {
return;
}
nodeModels.forEach(nodeModel -> ThreadUtil.execute(() -> {
try {
getNodeInfo(nodeModel);
} catch (Exception e) {
DefaultSystemLog.getLog().error("获取节点监控信息失败:{}", e.getMessage());
}
}));
private List<NodeModel> getListByUrl(String url) {
NodeModel nodeModel = new NodeModel();
nodeModel.setUrl(url);
return nodeService.listByBean(nodeModel);
}
private void getNodeInfo(NodeModel nodeModel) {
JsonMessage<JSONObject> message = NodeForward.request(nodeModel, null, NodeUrl.GetDirectTop);
JSONObject jsonObject = message.getData();
if (jsonObject == null) {
private void checkList(List<NodeModel> nodeModels) {
if (CollUtil.isEmpty(nodeModels)) {
return;
}
double disk = jsonObject.getDoubleValue("disk");
if (disk <= 0) {
return;
nodeModels.forEach(nodeModel -> {
//
nodeModel.setName(nodeModel.getUrl());
List<NodeModel> modelList = this.getListByUrl(nodeModel.getUrl());
boolean match = modelList.stream().allMatch(NodeModel::isOpenStatus);
if (!match) {
// 节点都关闭
return;
}
nodeModel.setOpenStatus(1);
nodeModel.setTimeOut(5);
//
ThreadUtil.execute(() -> {
try {
BaseServerController.resetInfo(UserModel.EMPTY);
JSONObject nodeTopInfo = this.getNodeTopInfo(nodeModel);
//
long timeMillis = SystemClock.now();
JsonMessage<Object> jsonMessage = NodeForward.requestBySys(nodeModel, NodeUrl.Status, "nodeId", nodeModel.getId());
int networkTime = (int) (System.currentTimeMillis() - timeMillis);
JSONObject jsonObject;
if (jsonMessage.getCode() == 200) {
jsonObject = jsonMessage.getData(JSONObject.class);
jsonObject.put("networkTime", networkTime);
this.save(modelList, nodeTopInfo, jsonObject);
} else {
// 状态码错
jsonObject = new JSONObject();
jsonObject.put("status", 3);
jsonObject.put("failureMsg", jsonMessage.toString());
jsonObject.put("networkTime", networkTime);
this.save(modelList, nodeTopInfo, jsonObject);
}
} catch (AuthorizeException agentException) {
this.save(modelList, 2, agentException.getMessage());
} catch (Exception e) {
this.save(modelList, 1, e.getMessage());
DefaultSystemLog.getLog().error("获取节点监控信息失败", e);
} finally {
BaseServerController.removeEmpty();
}
});
});
}
private void saveSystemMonitor(List<NodeModel> modelList, JSONObject systemMonitor) {
if (systemMonitor != null) {
List<SystemMonitorLog> monitorLogs = modelList.stream().map(nodeModel -> {
SystemMonitorLog log = new SystemMonitorLog();
log.setOccupyMemory(systemMonitor.getDouble("memory"));
log.setOccupyMemoryUsed(systemMonitor.getDouble("memoryUsed"));
log.setOccupyDisk(systemMonitor.getDouble("disk"));
log.setOccupyCpu(systemMonitor.getDouble("cpu"));
log.setMonitorTime(systemMonitor.getLongValue("time"));
log.setNodeId(nodeModel.getId());
return log;
}).collect(Collectors.toList());
//
dbSystemMonitorLogService.insert(monitorLogs);
}
}
/**
* 更新状态 和错误信息
*
* @param modelList 节点
* @param satus 状态
* @param msg 错误消息
*/
private void save(List<NodeModel> modelList, int satus, String msg) {
for (NodeModel nodeModel : modelList) {
NodeStatModel nodeStatModel = this.create(nodeModel);
nodeStatModel.setFailureMsg(StrUtil.maxLength(msg, 240));
nodeStatModel.setStatus(satus);
nodeStatService.upsert(nodeStatModel);
}
}
/**
* 报错结果
*
* @param modelList 节点
* @param systemMonitor 系统监控
* @param statusData 状态数据
*/
private void save(List<NodeModel> modelList, JSONObject systemMonitor, JSONObject statusData) {
this.saveSystemMonitor(modelList, systemMonitor);
//
SystemMonitorLog log = new SystemMonitorLog();
log.setOccupyMemory(jsonObject.getDoubleValue("memory"));
log.setOccupyMemoryUsed(jsonObject.getDoubleValue("memoryUsed"));
log.setOccupyDisk(disk);
log.setOccupyCpu(jsonObject.getDoubleValue("cpu"));
log.setMonitorTime(jsonObject.getLongValue("time"));
log.setNodeId(nodeModel.getId());
dbSystemMonitorLogService.insert(log);
for (NodeModel nodeModel : modelList) {
NodeStatModel nodeStatModel = this.create(nodeModel);
if (systemMonitor != null) {
nodeStatModel.setOccupyMemory(ObjectUtil.defaultIfNull(systemMonitor.getDouble("memory"), -1D));
nodeStatModel.setOccupyMemoryUsed(ObjectUtil.defaultIfNull(systemMonitor.getDouble("memoryUsed"), -1D));
nodeStatModel.setOccupyDisk(ObjectUtil.defaultIfNull(systemMonitor.getDouble("disk"), -1D));
nodeStatModel.setOccupyCpu(ObjectUtil.defaultIfNull(systemMonitor.getDouble("cpu"), -1D));
}
//
nodeStatModel.setNetworkTime(statusData.getIntValue("networkTime"));
nodeStatModel.setJpomVersion(statusData.getString("jpomVersion"));
nodeStatModel.setOsName(statusData.getString("osName"));
nodeStatModel.setUpTimeStr(statusData.getString("runTime"));
nodeStatModel.setFailureMsg(StrUtil.emptyToDefault(statusData.getString("failureMsg"), StrUtil.EMPTY));
//
Integer statusInteger = statusData.getInteger("status");
if (statusInteger != null) {
nodeStatModel.setStatus(statusInteger);
} else {
nodeStatModel.setStatus(0);
}
nodeStatModel.setOpenStatus(nodeStatModel.getOpenStatus());
nodeStatService.upsert(nodeStatModel);
}
}
private NodeStatModel create(NodeModel model) {
NodeStatModel nodeStatModel = new NodeStatModel();
nodeStatModel.setId(model.getId());
nodeStatModel.setWorkspaceId(model.getWorkspaceId());
nodeStatModel.setName(model.getName());
nodeStatModel.setUrl(model.getUrl());
return nodeStatModel;
}
/**
* 获取节点监控信息
*
* @param reqNode 真实节点
*/
private JSONObject getNodeTopInfo(NodeModel reqNode) {
JsonMessage<JSONObject> message = NodeForward.request(reqNode, null, NodeUrl.GetDirectTop);
return message.getData();
}
}

View File

@ -285,7 +285,7 @@ public abstract class BaseDbCommonService<T> {
* @param <R> 乏型
* @return data
*/
private <R> R entityToBean(Entity entity, Class<R> rClass) {
protected <R> R entityToBean(Entity entity, Class<R> rClass) {
if (entity == null) {
return null;
}

View File

@ -5,6 +5,7 @@ import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.db.Entity;
import cn.hutool.extra.servlet.ServletUtil;
import cn.jiangzeyin.common.DefaultSystemLog;
import cn.jiangzeyin.common.JsonMessage;
import cn.jiangzeyin.common.spring.SpringUtil;
import io.jpom.common.BaseServerController;
@ -12,13 +13,11 @@ import io.jpom.common.Const;
import io.jpom.common.JpomManifest;
import io.jpom.common.forward.NodeForward;
import io.jpom.common.forward.NodeUrl;
import io.jpom.cron.ICron;
import io.jpom.model.Cycle;
import io.jpom.model.data.NodeModel;
import io.jpom.model.data.SshModel;
import io.jpom.model.data.UserModel;
import io.jpom.model.data.WorkspaceModel;
import io.jpom.monitor.NodeMonitor;
import io.jpom.service.h2db.BaseGroupService;
import io.jpom.service.node.ssh.SshService;
import io.jpom.service.system.WorkspaceService;
@ -26,17 +25,17 @@ import org.springframework.stereotype.Service;
import org.springframework.util.Assert;
import javax.servlet.http.HttpServletRequest;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
/**
* @author bwcx_jzy
* @since 2021/12/4
*/
@Service
public class NodeService extends BaseGroupService<NodeModel> implements ICron {
public class NodeService extends BaseGroupService<NodeModel> {
private final SshService sshService;
private final WorkspaceService workspaceService;
@ -203,10 +202,7 @@ public class NodeService extends BaseGroupService<NodeModel> implements ICron {
public void insert(NodeModel nodeModel) {
this.fillNodeInfo(nodeModel);
super.insert(nodeModel);
Integer cycle = nodeModel.getCycle();
if (nodeModel.isOpenStatus() && cycle != null && cycle != Cycle.none.getCode()) {
NodeMonitor.start();
}
this.updateDuplicateNode(nodeModel);
}
@Override
@ -214,10 +210,7 @@ public class NodeService extends BaseGroupService<NodeModel> implements ICron {
nodeModel.setWorkspaceId(Const.WORKSPACE_DEFAULT_ID);
this.fillNodeInfo(nodeModel);
super.insertNotFill(nodeModel);
Integer cycle = nodeModel.getCycle();
if (nodeModel.isOpenStatus() && cycle != null && cycle != Cycle.none.getCode()) {
NodeMonitor.start();
}
this.updateDuplicateNode(nodeModel);
}
/**
@ -241,50 +234,48 @@ public class NodeService extends BaseGroupService<NodeModel> implements ICron {
return super.del(where);
}
@Override
public int update(NodeModel nodeModel) {
int update = super.update(nodeModel);
this.startCron();
return update;
}
@Override
public int updateById(NodeModel info) {
int updateById = super.updateById(info);
this.startCron();
if (updateById > 0) {
this.updateDuplicateNode(info);
}
return updateById;
}
@Override
public int startCron() {
// 关闭监听
Entity entity = Entity.create();
entity.set("openStatus", 1);
entity.set("cycle", StrUtil.format(" <> {}", Cycle.none.getCode()));
long count = super.count(entity);
if (count <= 0) {
NodeMonitor.stop();
} else {
NodeMonitor.start();
/**
* 更新相同节点对 授权信息
*
* @param info 节点信息
*/
private void updateDuplicateNode(NodeModel info) {
if (StrUtil.hasEmpty(info.getUrl(), info.getLoginName(), info.getLoginPwd())) {
return;
}
NodeModel update = new NodeModel();
update.setLoginName(info.getLoginName());
update.setLoginPwd(info.getLoginPwd());
//
NodeModel where = new NodeModel();
where.setUrl(info.getUrl());
int updateCount = super.update(super.dataBeanToEntity(update), super.dataBeanToEntity(where));
if (updateCount > 1) {
DefaultSystemLog.getLog().debug("update duplicate node {} {}", info.getUrl(), updateCount);
}
return (int) count;
}
/**
* 根据周期获取list
* 根据 url 去重
*
* @param cycle 周期
* @return list
*/
public List<NodeModel> listByCycle(Cycle cycle) {
NodeModel nodeModel = new NodeModel();
nodeModel.setCycle(cycle.getCode());
nodeModel.setOpenStatus(1);
List<NodeModel> list = this.listByBean(nodeModel);
if (list == null) {
return new ArrayList<>();
public List<NodeModel> listDeDuplicationByUrl() {
String sql = "select url,max(loginName) as loginName,max(loginPwd) as loginPwd,max(protocol) as protocol from " + super.getTableName() + " group by url";
List<Entity> query = this.query(sql);
if (query != null) {
return query.stream().map((entity -> this.entityToBean(entity, this.tClass))).collect(Collectors.toList());
}
return list;
return null;
}
public List<NodeModel> getNodeBySshId(String sshId) {

View File

@ -0,0 +1,35 @@
/*
* The MIT License (MIT)
*
* Copyright (c) 2019 Code Technology Studio
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package io.jpom.service.stat;
import io.jpom.model.stat.NodeStatModel;
import io.jpom.service.h2db.BaseWorkspaceService;
import org.springframework.stereotype.Service;
/**
* @author bwcx_jzy
* @since 2022/1/22
*/
@Service
public class NodeStatService extends BaseWorkspaceService<NodeStatModel> {
}

View File

@ -37,6 +37,7 @@ import io.jpom.common.forward.NodeUrl;
import io.jpom.cron.CronUtils;
import io.jpom.cron.ICron;
import io.jpom.model.data.NodeModel;
import io.jpom.monitor.NodeMonitor;
import io.jpom.service.IStatusRecover;
import io.jpom.service.dblog.BackupInfoService;
import io.jpom.service.node.NodeService;
@ -141,6 +142,8 @@ public class CheckMonitor {
DefaultSystemLog.getLog().debug("{} Recover bad data {}", name, count);
}
});
// 节点监控
NodeMonitor.start();
//
RemoteVersion.loadRemoteInfo();
});

View File

@ -8,6 +8,10 @@
"title": "节点列表",
"id": "nodeList"
},
{
"title": "节点统计",
"id": "nodeStat"
},
{
"title": "项目列表",
"id": "projectSearch"
@ -129,7 +133,6 @@
{
"title": "系统管理",
"icon_v3": "setting",
"role": "sys",
"id": "setting",
"childs": [
{

View File

@ -61,7 +61,6 @@
"id": "systemConfig",
"title": "系统管理",
"icon_v3": "setting",
"role": "sys",
"childs": [
{
"id": "whitelistDirectory",

View File

@ -330,4 +330,31 @@ CREATE TABLE IF NOT EXISTS PUBLIC.SERVER_SCRIPT_EXECUTE_LOG
comment on table SERVER_SCRIPT_EXECUTE_LOG is '脚本模版执行记录';
-- 节点统计
CREATE TABLE IF NOT EXISTS PUBLIC.NODE_STAT
(
id VARCHAR(50) not null comment 'id',
createTimeMillis BIGINT COMMENT '数据创建时间',
modifyTimeMillis BIGINT COMMENT '数据修改时间',
modifyUser VARCHAR(50) comment '修改人',
strike int DEFAULT 0 comment '逻辑删除{1删除0 未删除(默认)}',
workspaceId varchar(50) not null comment '所属工作空间',
occupyMemoryUsed DOUBLE comment '占用cpu',
occupyCpu DOUBLE comment '占用cpu',
occupyMemory DOUBLE comment '占用内存',
occupyDisk DOUBLE comment '占用磁盘',
networkTime int DEFAULT 0 comment '网络耗时',
upTimeStr varchar(50) comment '运行时长',
osName varchar(100) comment '所属工作空间',
jpomVersion varchar(50) comment 'jpom 版本',
status int DEFAULT 0 comment '状态{1无法连接0 正常, 2 授权信息错误}',
openStatus int DEFAULT 0 comment '启用状态{1启用0 未启用)}',
failureMsg VARCHAR(255) comment '错误消息',
url VARCHAR(255) comment '节点地址',
name VARCHAR(255) comment '节点名称',
CONSTRAINT NODE_STAT_PK PRIMARY KEY (id)
);
comment on table NODE_STAT is '节点统计';

View File

@ -0,0 +1,39 @@
import axios from "./config";
// node 列表
export function getStatist(params) {
return axios({
url: "/node/stat/list_data.json",
method: "post",
params: params,
headers: {
loading: "no",
},
});
}
// node 列表
export function statusStat() {
return axios({
url: "/node/stat/status_stat.json",
method: "get",
headers: {
loading: "no",
},
});
}
export const status = {
1: "无法连接",
0: "正常",
2: "授权信息错误",
3: "状态码错误",
};
// export const nodeMonitorCycle = {
// "-30": "30 秒",
// 1: "1 分钟",
// 5: "5 分钟",
// 10: "10 分钟",
// 30: "30 分钟",
// };

View File

@ -281,12 +281,3 @@ export function downloadRemote() {
data: {},
});
}
export const nodeMonitorCycle = {
0: "不开启",
"-30": "30 秒",
1: "1 分钟",
5: "5 分钟",
10: "10 分钟",
30: "30 分钟",
};

View File

@ -2,16 +2,36 @@
<div class="code-mirror-div">
<div class="tool-bar" ref="toolBar" v-if="showTool">
<slot name="tool_before" />
皮肤
<a-select v-model="cmOptions.theme" @select="handleSelectTheme" show-search option-filter-prop="children" :filter-option="filterOption" placeholder="请选择" style="width: 150px">
<a-select-option v-for="item in cmThemeOptions" :key="item">{{ item }}</a-select-option>
</a-select>
<div style="margin-left: 30px">
语言
<a-select v-model="cmOptions.mode" @select="handleSelectMode" show-search option-filter-prop="children" :filter-option="filterOption" placeholder="请选择" style="width: 150px">
<a-select-option v-for="item in cmEditorModeOptions" :key="item">{{ item }}</a-select-option>
</a-select>
</div>
<a-space>
<div>
皮肤
<a-select v-model="cmOptions.theme" @select="handleSelectTheme" show-search option-filter-prop="children" :filter-option="filterOption" placeholder="请选择" style="width: 150px">
<a-select-option v-for="item in cmThemeOptions" :key="item">{{ item }}</a-select-option>
</a-select>
</div>
<div>
语言
<a-select v-model="cmOptions.mode" @select="handleSelectMode" show-search option-filter-prop="children" :filter-option="filterOption" placeholder="请选择" style="width: 150px">
<a-select-option v-for="item in cmEditorModeOptions" :key="item">{{ item }}</a-select-option>
</a-select>
</div>
<a-tooltip>
<template slot="title">
<ul>
<li>Ctrl-F / Cmd-F Start searching</li>
<li>Ctrl-G / Cmd-G Find next</li>
<li>Shift-Ctrl-G / Shift-Cmd-G Find previous</li>
<li>Shift-Ctrl-F / Cmd-Option-F Replace</li>
<li>Shift-Ctrl-R / Shift-Cmd-Option-F Replace all</li>
<li>Alt-F Persistent search (dialog doesn't autoclose, enter to find next, Shift-Enter to find previous)</li>
<li>Alt-G Jump to line</li>
</ul>
</template>
<a-icon type="question-circle" theme="filled" />
</a-tooltip>
</a-space>
<slot name="tool_after" />
</div>
<div :style="{ height: codeMirrorHeight }">

View File

@ -43,7 +43,7 @@ import {
Select,
// Slider,
Spin,
// Statistic,
Statistic,
Steps,
Switch,
Table,
@ -112,7 +112,7 @@ const components = [
Select,
// Slider,
Spin,
// Statistic,
Statistic,
Steps,
Switch,
Table,

View File

@ -124,11 +124,11 @@
<a-select-option v-for="ssh in sshList" :key="ssh.id" :disabled="ssh.disabled">{{ ssh.name }}</a-select-option>
</a-select>
</a-form-model-item>
<a-form-model-item label="监控周期" prop="cycle">
<!-- <a-form-model-item label="监控周期" prop="cycle">
<a-select v-model="temp.cycle" defaultValue="0" placeholder="监控周期">
<a-select-option v-for="(name, key) in nodeMonitorCycle" :key="parseInt(key)">{{ name }}</a-select-option>
</a-select>
</a-form-model-item>
</a-form-model-item> -->
<a-form-model-item label="节点状态" prop="openStatus">
<a-switch
@ -326,7 +326,7 @@
</template>
<script>
import { mapGetters } from "vuex";
import { getNodeList, getNodeStatus, editNode, deleteNode, syncProject, unLockWorkspace, nodeMonitorCycle, getNodeGroupAll, fastInstall, pullFastInstallResult, confirmFastInstall } from "@/api/node";
import { getNodeList, getNodeStatus, editNode, deleteNode, syncProject, unLockWorkspace, getNodeGroupAll, fastInstall, pullFastInstallResult, confirmFastInstall } from "@/api/node";
import { getSshListAll } from "@/api/ssh";
import { syncScript } from "@/api/node-other";
import NodeLayout from "./node-layout";
@ -348,7 +348,7 @@ export default {
loading: false,
childLoading: false,
listQuery: Object.assign({}, PAGE_DEFAULT_LIST_QUERY),
nodeMonitorCycle: nodeMonitorCycle,
// nodeMonitorCycle: nodeMonitorCycle,
sshList: [],
list: [],
groupList: [],
@ -374,7 +374,7 @@ export default {
{ title: "节点协议", dataIndex: "protocol", sorter: true, key: "protocol", width: 100, ellipsis: true, scopedSlots: { customRender: "protocol" } },
{ title: "节点地址", dataIndex: "url", sorter: true, key: "url", ellipsis: true, scopedSlots: { customRender: "url" } },
{ title: "账号", dataIndex: "loginName", sorter: true, key: "loginName", ellipsis: true, scopedSlots: { customRender: "loginName" } },
{ title: "监控周期", dataIndex: "cycle", sorter: true, key: "cycle", ellipsis: true, scopedSlots: { customRender: "cycle" } },
// { title: "", dataIndex: "cycle", sorter: true, key: "cycle", ellipsis: true, scopedSlots: { customRender: "cycle" } },
{ title: "超时时间", dataIndex: "timeOut", sorter: true, key: "timeOut", width: 100, ellipsis: true },
{
title: "修改时间",

View File

@ -0,0 +1,168 @@
<template>
<div class="full-content">
<a-space direction="vertical">
<a-card>
<!-- <template slot="title">
<a-row>
<a-col :span="2">状态概况</a-col>
</a-row>
</template> -->
<a-row>
<a-col :span="4">
<a-statistic title="节点总数" :value="nodeCount"> </a-statistic>
</a-col>
<a-col :span="4" v-for="(desc, key) in statusMap" :key="key">
<a-statistic :title="desc" :value="statusStatMap[key]"> </a-statistic>
</a-col>
<a-col :span="2"> <a-statistic-countdown format="s 秒" title="刷新倒计时" :value="deadline" @finish="onFinish" /> </a-col>
</a-row>
</a-card>
<div ref="filter" class="filter">
<a-space>
<a-input v-model="listQuery['%name%']" placeholder="节点名称" />
<a-input v-model="listQuery['%url%']" placeholder="节点地址" />
<a-select v-model="listQuery.status" allowClear placeholder="请选择状态" class="search-input-item">
<a-select-option v-for="(desc, key) in statusMap" :key="key">{{ desc }}</a-select-option>
</a-select>
<a-tooltip title="按住 Ctr 或者 Alt 键点击按钮快速回到第一页">
<a-button :loading="loading" type="primary" @click="loadData">搜索</a-button>
</a-tooltip>
</a-space>
</div>
<!-- 表格 :scroll="{ x: 1070, y: tableHeight -60 }" scroll expandedRowRender 不兼容没法同时使用不然会多出一行数据-->
<a-table :columns="columns" :data-source="list" bordered rowKey="id" :pagination="(this, pagination)" @change="changePage">
<a-tooltip slot="tooltip" slot-scope="text" placement="topLeft" :title="text">
<span>{{ text }}</span>
</a-tooltip>
<template slot="status" slot-scope="text, record">
<a-tooltip v-if="text !== 0" placement="topLeft" :title="record.failureMsg">
<span>{{ statusMap[text] }}</span>
</a-tooltip>
<span v-else>{{ statusMap[text] }}</span>
</template>
<template slot="progress" slot-scope="text">
<a-tooltip placement="topLeft" :title="`${text}%`">
<a-progress
:percent="text"
:stroke-color="{
from: '#87d068',
to: '#108ee9',
}"
size="small"
status="active"
:showInfo="false"
/>
</a-tooltip>
</template>
</a-table>
</a-space>
</div>
</template>
<script>
import { getStatist, status, statusStat } from "@/api/node-stat";
import { parseTime } from "@/utils/time";
import { PAGE_DEFAULT_LIMIT, PAGE_DEFAULT_SIZW_OPTIONS, PAGE_DEFAULT_SHOW_TOTAL, PAGE_DEFAULT_LIST_QUERY } from "@/utils/const";
export default {
components: {},
data() {
return {
loading: false,
statusMap: status,
listQuery: Object.assign({}, PAGE_DEFAULT_LIST_QUERY),
list: [],
statusStatMap: {},
nodeCount: 0,
// nodeMonitorCycle: nodeMonitorCycle,
deadline: 0,
temp: {},
columns: [
{ title: "节点名称", dataIndex: "name", sorter: true, key: "name", ellipsis: true, scopedSlots: { customRender: "tooltip" } },
{ title: "节点地址", dataIndex: "url", sorter: true, key: "url", ellipsis: true, scopedSlots: { customRender: "tooltip" } },
{ title: "cpu", dataIndex: "occupyCpu", sorter: true, key: "occupyCpu", ellipsis: true, scopedSlots: { customRender: "progress" } },
{ title: "disk", dataIndex: "occupyDisk", sorter: true, key: "occupyDisk", ellipsis: true, scopedSlots: { customRender: "progress" } },
{ title: "memory", dataIndex: "occupyMemory", sorter: true, key: "occupyMemory", ellipsis: true, scopedSlots: { customRender: "progress" } },
{ title: "memoryUsed", dataIndex: "occupyMemoryUsed", sorter: true, key: "occupyMemoryUsed", ellipsis: true, scopedSlots: { customRender: "progress" } },
{ title: "运行时间", dataIndex: "upTimeStr", sorter: true, key: "upTimeStr", ellipsis: true, scopedSlots: { customRender: "tooltip" } },
{ title: "状态", dataIndex: "status", sorter: true, key: "status", ellipsis: true, scopedSlots: { customRender: "status" } },
{
title: "更新时间",
dataIndex: "modifyTimeMillis",
ellipsis: true,
sorter: true,
customRender: (text) => {
return parseTime(text);
},
width: 170,
},
],
};
},
computed: {
pagination() {
return {
total: this.listQuery.total || 0,
current: this.listQuery.page || 1,
pageSize: this.listQuery.limit || PAGE_DEFAULT_LIMIT,
pageSizeOptions: PAGE_DEFAULT_SIZW_OPTIONS,
showSizeChanger: true,
showTotal: (total) => {
return PAGE_DEFAULT_SHOW_TOTAL(total, this.listQuery);
},
};
},
},
watch: {},
created() {
this.loadData();
},
destroyed() {
if (this.pullFastInstallResultTime) {
clearInterval(this.pullFastInstallResultTime);
}
},
methods: {
//
loadData(pointerEvent) {
this.list = [];
this.listQuery.page = pointerEvent?.altKey || pointerEvent?.ctrlKey ? 1 : this.listQuery.page;
this.loading = true;
getStatist(this.listQuery).then((res) => {
if (res.code === 200) {
this.list = res.data.result;
this.listQuery.total = res.data.total;
}
this.loading = false;
});
statusStat().then((res) => {
if (res.data) {
this.statusStatMap = res.data;
let nodeCount2 = 0;
// console.log(this.statusStatMap);
Object.values(this.statusStatMap).forEach((element) => {
nodeCount2 += element;
});
this.nodeCount = nodeCount2;
this.deadline = Date.now() + 30 * 1000;
}
});
},
//
changePage(pagination, filters, sorter) {
this.listQuery.page = pagination.current;
this.listQuery.limit = pagination.pageSize;
if (sorter) {
this.listQuery.order = sorter.order;
this.listQuery.order_field = sorter.field;
}
this.loadData();
},
onFinish() {
this.loadData();
},
},
};
</script>
<style scoped></style>

View File

@ -183,7 +183,7 @@
<a-icon type="menu" />
菜单配置
</span>
<a-alert :message="`菜单配置只对非超级管理员生效`" style="margin-top: 10px; margin-bottom: 20px" banner />
<a-alert :message="`菜单配置只对非超级管理员生效,当前配置对当前工作空间生效,其他工作空间请切换后配置`" style="margin-top: 10px; margin-bottom: 20px" banner />
<a-form-model ref="editWhiteForm" :model="menusConfigData">
<a-row type="flex" justify="center">
<a-col :span="12">

View File

@ -21,6 +21,11 @@ const children = [
name: "node-list",
component: () => import("../pages/node/list"),
},
{
path: "/node/stat",
name: "node-stat",
component: () => import("../pages/node/stat"),
},
{
path: "/node/search",
name: "node-search",

View File

@ -5,6 +5,7 @@
*/
const routeMenuMap = {
nodeList: "/node/list",
nodeStat: "/node/stat",
sshList: "/ssh",
commandList: "/ssh/command",
commandLogList: "/ssh/command-log",