avatar

目录
web页面管理服务以及查看日志

一、前言

​ 最近老大让我负责了一个关于部署服务的功能,旨在方便同事可以发布自己负责的服务,同时可以让没有权限对服务器进行操作的同事不再频繁的发出吼叫:“哥,我接口没通,帮我看下日志!!”。大体先介绍一下背景,因为项目是分布式集群上的,光服务就二十多个,所以每次发布服务到生产环境就成了头号难题,在和老大讨论了好久之后终于决定要啃一下这个骨头。

​ 大概的一个流程是,进入web管理页面,选择命令,后台接受之后运行linux命令。

二、演示

这个是从eureka中获取的注册的服务列表,点击详情可以查看该服务部署在了哪几台机器上,如下

可以对单个服务器上的服务进行操作,例如可以启动服务,停止服务,重启服务,更新也就是更新target,也就相当于重新发布服务,强制停止,还可以查看日志,查看日志放在最下面。

好了接下了开搞!

三、在java中运行linux命令

​ 在接收到前端页面发来的请求时,比如现在我想启动我的服务器

业务逻辑

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
/**
* 执行启动命令
*
* @param cmdName 命令名称
* @param service 服务名称
* @return
*/
@Override
public String execCmdOfStart(String cmdName, String service) {
String s = "";
try {
//service为服务名
service = service.split("-")[1].toLowerCase();
logger.info("当前操作的服务名称为========>" + service);
//此时path是我从数据库中读取的在linux中存储的shell脚本的路径
String path = eurekaServiceMapper.getShellCmd("path");
//getJarName()方法是用来获取指定target目录下的jar包的名称,如此一来方便运行不同的jar包
String jarName = getJarName(path, service);
if (StringUtils.isEmpty(jarName) && StringUtils.isBlank(jarName)) {
return "服务启动jar包为空";
}
logger.info("执行服务jar包名称为:" + jarName);
path = path + "starter.sh";//shell脚本名称
logger.info("执行脚本的路径为=======>" + path);
String jvm = eurekaServiceMapper.getShellCmd("jvm");
logger.info("执行jar包内存设置为:" + jvm);

String[] command = new String[]{path};
String[] param = new String[]{jvm, service, jarName};//命令集合
/*解决参数中包含空格*/
command = (String[]) ArrayUtils.addAll(command, param);
logger.info("要调用启动服务shell脚本为" + Arrays.toString(command));

logger.info("解决脚本没有执行权限逻辑start");
//解决脚本没有执行权限
String chmod = "chmod 777 " + path;
Process process = Runtime.getRuntime().exec(chmod);
process.waitFor();

logger.info("解决脚本没有执行权限逻辑end");

ProcessStatus processStatus = execShell(command);
s = processStatus.output;

return s;
} catch (Exception e) {
logger.error("执行启动命令异常====>" + e);
return "执行启动命令异常====>" + e;
}
}

上方代码只是一下简单的业务中用到的,不必深究,下面代码才是真正处理命令的

通过ProcessBuilder进行调度

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
/**
* 执行shell命令(通过ProcessBuilder进行调度)
*
* @param command
* @return
*/
public ProcessStatus execShell(String[] command) {
logger.info("execShell======>" + Arrays.toString(command));
try {
ProcessBuilder builder = new ProcessBuilder(command);
builder.redirectErrorStream(true);//将getInputStream(),getErrorStream()两个流合并,自动清空流
Process process = builder.start();

Worker worker = new Worker(process);
worker.start();
ProcessStatus status = worker.getProcessStatus();

try {
worker.join(20000); //设置20s超时
if (status.exitCode == ProcessStatus.CODE_STARTED){
//未结束
worker.interrupt();//中断
throw new InterruptedException();
}else {
return status;
}
}catch (InterruptedException e){
worker.interrupt();
throw e;
}finally {
process.destroy();
}
} catch (Exception e) {
logger.error("执行shell命令异常====>" + e);
}
return null;
}

因为通过Process进行调度的时候回创建子进程,而创建的子进程是没有自己的终端或者控制器的,它的所有标准io都是通过三个流(getOutputStream()、getInputStream() 和 getErrorStream())来重定向到父进程的,父进程使用这些流来提供到子进程的输入和获得从子进程的输出。因为有些本机平台仅针对标准输入和输出流提供有限的缓冲区大小,如果读写子进程的输出流或输入流迅速出现失败,则可能导致子进程阻塞,甚至产生死锁。所以在处理标准输入流的时候同时也要处理标准错误流的。

解决死锁问题

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
/**
* 设置超时
*/
private static class Worker extends Thread{
private final Process PROCESS;
private ProcessStatus processStatus;

private Worker(Process process){
this.PROCESS = process;
this.processStatus = new ProcessStatus();
}

@Override
public void run() {
BufferedReader bufferedReader = null;
String line = "";
try {
bufferedReader = new BufferedReader(new InputStreamReader(PROCESS.getInputStream(), "UTF-8"));
StringBuffer sb = new StringBuffer();
while ((line = bufferedReader.readLine()) != null){
sb.append(line).append("<br>");
}
processStatus.output = sb.toString();
processStatus.exitCode = PROCESS.waitFor();
}catch (Exception e){
}
}

public ProcessStatus getProcessStatus(){
return this.processStatus;
}
}

public static class ProcessStatus{
public static final int CODE_STARTED =-257;
public volatile int exitCode;
public volatile String output;
}

当然还有另一种调度方式即Process pro = Runtime.getRuntime().exec(new String[]{"sh",***});,但这个已经是好久的了,而且我用的时候有时候运行命令会导致死锁,有点搞不清楚,所以我用了比较简洁的 ProcessBuilder进行调度。

四、实时查看日志

这里参考的文章:https://blog.csdn.net/xiao__gui/article/details/50041673

在Linux操作系统中,经常需要查看日志文件的实时输出内容,通常会使用tail -f或者tailf命令。查看实时日志可能会需要首先SSH连上Linux主机,步骤很麻烦不说,如果是生产环境的服务器,可能还会控制各种权限。基于Web的实时日志可以解决这个问题。

由于传统的HTTP协议是请求/响应模式,而实时日志需要不定时的持续的输出,由服务器主动推送给客户端浏览器。所以这里使用的是HTML5的WebSocket协议。

准备

首先引入Spring websocket支持

xml
1
2
3
4
5
 <!-- spring websocket-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>

实现

配置WebsocketConfig

java
1
2
3
4
5
6
7
8
9
10
11
/**
* create by zwx on 2019/12/4
*/
@Configuration
public class WebSocketConfig {

@Bean
public ServerEndpointExporter serverEndpointExporter(){
return new ServerEndpointExporter();
}
}

注解@ServerEndpoint,并需要指定一个路径,用于处理客户端WebSocket请求。

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
/**
* create by zwx on 2019/12/4
*/
@ServerEndpoint(value = "/log" , configurator = HttpSessionConfigurator.class)
@Component
public class LogWebSocketHandle implements BaseCommonInterFace {

private final Logger logger = getLogger();

private Process process;

private InputStream inputStream;

@Autowired
private EurekaServiceMapper eurekaServiceMapper;

/*解决@component下@Autowired无法取值的问题*/
private static LogWebSocketHandle logWebSocketHandle;

@PostConstruct
public void init(){
logWebSocketHandle = this;
logWebSocketHandle.eurekaServiceMapper = this.eurekaServiceMapper;
}

/**
* webSocket请求开启
* @param session
*/
@OnOpen
public void onOpen(Session session, EndpointConfig config)
{
// HttpSession httpSession = (HttpSession) config.getUserProperties().get(HttpSession.class.getName());

logger.info("====websocket连接建立=========");
}

/**
* webSocket接收到消息
*/
@OnMessage
public void onMessage(String message,Session session){
String tailLog = logWebSocketHandle.eurekaServiceMapper.getShellCmd("tailLog");
JSONObject object = JSON.parseObject(message);
String service = (String) object.get("service");
service = service.split("-")[1].toLowerCase();
tailLog=tailLog+"igs-"+service+"-log.log";
logger.info("======日志命令====>"+tailLog);

String[] cmdShell = new String[]{"/bin/sh","-c",tailLog};/*linux*/
// String[] cmdShell = new String[]{"cmd","/c",tailLog}; //windows
try {
//执行tail命令
process = Runtime.getRuntime().exec(cmdShell);
inputStream = process.getInputStream();

//开启新线程,防止InputStream阻塞处理WebSocket的线程
TailLogThread logThread = new TailLogThread(inputStream,session);
logThread.start();

}catch (Exception e){
e.printStackTrace();
}
}

/**
* websocket请求关闭
*/
@OnClose
public void onClose(){
logger.info("====websocket连接关闭=========");
try {
if (inputStream != null)
inputStream.close();
}catch (Exception e){
e.printStackTrace();
}
if (process != null){
process.destroy();
}
}
}

这里可能会出一个bug,即component下@Autowired无法取值的问题,已经解决了,就在上方代码中

由于针对每个WebSocket连接都会创建一个新的LogWebSocketHandle实例,所以可以不用像Servlet一样考虑线程安全问题。由于tail -f命令的输入流会阻塞当前线程,所以一定要创建一个新的线程来读取tail -f命令的返回结果:

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
/**
* 创建一个新的线程来读取返回的日志
*/
public class TailLogThread extends Thread{

private BufferedReader bufferedReader;
private Session session;

public TailLogThread(InputStream in, Session session){
this.bufferedReader = new BufferedReader(new InputStreamReader(in));
this.session = session;
}

@Override
public void run() {
String line;
try {
// StringBuffer sb = new StringBuffer();
// int count = 0;
// int lineNum = 1;
while ((line = bufferedReader.readLine()) != null){
/*对日志进行限流*/
// if (count == 100){
// session.getBasicRemote().sendText(sb.toString());
Thread.sleep(40);
session.getBasicRemote().sendText(line+"<br/>");
// count = 0;
// }else {
// sb.append(line).append("<br>");
// count++;
// lineNum++;
// }
}
}catch (Exception e){
e.printStackTrace();
}
}
}

web前端页面

Web前端需要通过WebSocket连接到服务端,实时接收最新的日志内容并展示到页面上。

Javascript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
var ws = new WebSocket('ws://'+addr+'/log');//初始化websocket对象

//连接成功的回调
ws.onopen = function (ev) {
var message = {
"addr":addr,
"service":service
};
ws.send(JSON.stringify(message));
$("#log-container div").append("连接成功!");
console.log("连接成功!")
};
//连接错误的回调
ws.onerror = function (ev) {
$("#log-container div").append("连接发生错误!");
console.log("连接发生错误!")
};
//连接关闭的回调
ws.onclose = function (ev) {
$("#log-container div").append("连接关闭!");
console.log("连接关闭!")
};

//接受到消息的回调
ws.onmessage = function (ev) {
// 接收服务端的实时日志并添加到HTML页面中(error显示红色)
if (ev.data.search("ERROR") != -1) {
$("#log-container div").append(ev.data).css("color", "#AA0000");
} else {
$("#log-container div").append(ev.data).css("color", "#aaa");
}
// 滚动条滚动到最低部
var scrollHeight = $("#modalBody").prop("scrollHeight");
$("#modalBody").animate({scrollTop:scrollHeight},1);//4秒落下
};
html
html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!-- 日志.模态框 -->
<div class="modal fade bs-example-modal-lg" id="logModal" tabindex="-1" role="dialog" aria-hidden="true">
<div class="modal-dialog modal-lg" style="position: absolute;top:0;bottom:0;left: 0;right:0;width: 80%;height: 80%" role="document">
<div class="modal-content" style="position: absolute;top: 0;bottom: 0;width: 100%">
<div class="modal-header">
<button type="button" class="close" style="margin-right: 15px" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<h4 style="font-size: 25px">实时日志</h4>
</div>
<div class="modal-body" style="overflow-y: scroll;overflow-x:hidden;position: absolute;top: 80px;bottom: 20px;width: 100%" id="modalBody">
<div id="log-container" style="height: auto; background: #333; padding: 50px 10px 10px 10px;">
<div>
</div>
</div>
</div>

</div>
</div>
</div>
文章作者: 十四礼
文章链接: https://lemcoo.top/2020/01/202001041631-agent/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 观礼塘

评论