Pārlūkot izejas kodu

添加系统实时日志呈现功能

reghao 2 gadi atpakaļ
vecāks
revīzija
c86e4d0afa

+ 13 - 1
manager/src/main/java/cn/reghao/devops/manager/config/spring/AppLifecycle.java

@@ -3,10 +3,13 @@ package cn.reghao.devops.manager.config.spring;
 import cn.reghao.devops.common.machine.Machine;
 import cn.reghao.devops.manager.app.model.po.config.build.BuildDir;
 import cn.reghao.devops.manager.app.service.config.BuildDirService;
+import cn.reghao.devops.manager.log.Appenders;
+import cn.reghao.devops.manager.log.LoggerConfig;
 import cn.reghao.devops.manager.machine.service.MachineService;
 import cn.reghao.devops.manager.rbac.db.repository.RoleRepository;
 import cn.reghao.devops.manager.rbac.model.constant.RoleType;
 import cn.reghao.devops.manager.rbac.model.po.Role;
+import cn.reghao.devops.manager.ws.handler.LogHandler;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.DisposableBean;
 import org.springframework.boot.ApplicationArguments;
@@ -26,18 +29,21 @@ public class AppLifecycle implements ApplicationRunner, DisposableBean {
     private final BuildDirService buildDirService;
     private final MachineService machineService;
     private final RoleRepository roleRepository;
+    private final LogHandler logHandler;
 
     public AppLifecycle(BuildDirService buildDirService, MachineService machineService, 
-                        RoleRepository roleRepository) {
+                        RoleRepository roleRepository, LogHandler logHandler) {
         this.buildDirService = buildDirService;
         this.machineService = machineService;
         this.roleRepository = roleRepository;
+        this.logHandler = logHandler;
     }
 
     @Override
     public void run(ApplicationArguments args) {
         initBuildDir();
         initRoles();
+        initLogConfig();
         log.info("devops-manager 初始化完成");
     }
 
@@ -73,4 +79,10 @@ public class AppLifecycle implements ApplicationRunner, DisposableBean {
         
         return list;
     }
+
+    private void initLogConfig() {
+        String app = "devops-manager";
+        String host = "devops.reghao.cn";
+        LoggerConfig.initLogger(List.of(Appenders.pushAppender(app, host, logHandler)));
+    }
 }

+ 5 - 27
manager/src/main/java/cn/reghao/devops/manager/home/controller/page/AppEnvPageController.java

@@ -2,43 +2,21 @@ package cn.reghao.devops.manager.home.controller.page;
 
 import io.swagger.annotations.Api;
 import io.swagger.annotations.ApiOperation;
-import org.springframework.data.domain.Page;
 import org.springframework.stereotype.Controller;
-import org.springframework.ui.Model;
 import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.PathVariable;
 import org.springframework.web.bind.annotation.RequestMapping;
 
-import java.util.Collections;
-
 /**
  * @author reghao
  * @date 2022-05-12 10:51:13
  */
 @Api(tags = "应用环境页面")
 @Controller
-@RequestMapping("/sys/env")
+@RequestMapping("/sys")
 public class AppEnvPageController {
-    @ApiOperation(value = "应用环境页面")
-    @GetMapping("")
-    public String sysEnvPage(Model model) {
-        model.addAttribute("page", Page.empty());
-        model.addAttribute("list", Collections.emptyList());
-        return "/sys/env/index";
-    }
-
-    @ApiOperation(value = "应用环境添加/编辑页面")
-    @GetMapping("/add")
-    public String addAppEnvPage(Model model) {
-        model.addAttribute("page", Page.empty());
-        model.addAttribute("list", Collections.emptyList());
-        return "/sys/env/add";
-    }
-
-    @ApiOperation(value = "应用环境添加/编辑页面")
-    @GetMapping("/edit/{id}")
-    public String editAppEnvPage(@PathVariable("id") int id, Model model) {
-        model.addAttribute("appEnv", null);
-        return "/sys/env/add";
+    @ApiOperation(value = "系统实时日志页面")
+    @GetMapping("/log")
+    public String logPage() {
+        return "/sys/syslog";
     }
 }

+ 15 - 1
common/src/main/java/cn/reghao/devops/common/log/Appenders.java → manager/src/main/java/cn/reghao/devops/manager/log/Appenders.java

@@ -1,4 +1,4 @@
-package cn.reghao.devops.common.log;
+package cn.reghao.devops.manager.log;
 
 import ch.qos.logback.classic.LoggerContext;
 import ch.qos.logback.classic.encoder.PatternLayoutEncoder;
@@ -6,6 +6,8 @@ import ch.qos.logback.classic.spi.ILoggingEvent;
 import ch.qos.logback.core.Appender;
 import ch.qos.logback.core.ConsoleAppender;
 import ch.qos.logback.core.FileAppender;
+import cn.reghao.devops.manager.ws.PushAppender;
+import cn.reghao.devops.manager.ws.handler.LogHandler;
 import org.slf4j.LoggerFactory;
 
 /**
@@ -28,6 +30,18 @@ public class Appenders {
         return null;
     }
 
+    public static Appender<ILoggingEvent> pushAppender(String app, String host, LogHandler logHandler) {
+        PatternLayoutEncoder layoutEncoder = new PatternLayoutEncoder();
+        layoutEncoder.setPattern("%date %level [%thread] %logger{10} [%file:%line] %msg%n");
+        layoutEncoder.setContext(loggerContext);
+        layoutEncoder.start();
+
+        PushAppender pushAppender = new PushAppender(app, host, logHandler);
+        pushAppender.setContext(loggerContext);
+        pushAppender.start();
+        return pushAppender;
+    }
+
     public static Appender<ILoggingEvent> fileAppender() {
         PatternLayoutEncoder layoutEncoder = new PatternLayoutEncoder();
         layoutEncoder.setPattern("%date %level [%thread] %logger{10} [%file:%line] %msg%n");

+ 1 - 1
common/src/main/java/cn/reghao/devops/common/log/LoggerConfig.java → manager/src/main/java/cn/reghao/devops/manager/log/LoggerConfig.java

@@ -1,4 +1,4 @@
-package cn.reghao.devops.common.log;
+package cn.reghao.devops.manager.log;
 
 import ch.qos.logback.classic.Level;
 import ch.qos.logback.classic.Logger;

+ 1 - 1
common/src/main/java/cn/reghao/devops/common/log/WsAppender.java → manager/src/main/java/cn/reghao/devops/manager/log/WsAppender.java

@@ -1,4 +1,4 @@
-package cn.reghao.devops.common.log;
+package cn.reghao.devops.manager.log;
 
 import ch.qos.logback.classic.spi.ILoggingEvent;
 import ch.qos.logback.core.UnsynchronizedAppenderBase;

+ 52 - 0
manager/src/main/java/cn/reghao/devops/manager/ws/PushAppender.java

@@ -0,0 +1,52 @@
+package cn.reghao.devops.manager.ws;
+
+import ch.qos.logback.classic.spi.ILoggingEvent;
+import ch.qos.logback.core.UnsynchronizedAppenderBase;
+import cn.reghao.devops.common.ws.WsClient;
+import cn.reghao.devops.manager.ws.handler.LogHandler;
+import cn.reghao.jutil.jdk.converter.DateTimeConverter;
+import cn.reghao.jutil.jdk.result.AppLog;
+
+/**
+ * @author reghao
+ * @date 2023-06-03 19:37:21
+ */
+public class PushAppender extends UnsynchronizedAppenderBase<ILoggingEvent> {
+    private final String app;
+    private final String host;
+    private final LogHandler logHandler;
+
+    public PushAppender(String app, String host, LogHandler logHandler) {
+        this.app = app;
+        this.host = host;
+        this.logHandler = logHandler;
+        setName("pushAppender");
+    }
+
+    @Override
+    public void start() {
+        super.start();
+    }
+
+    @Override
+    public void stop() {
+        super.stop();
+    }
+
+    @Override
+    protected void append(ILoggingEvent event) {
+        AppLog appLog = getAppLog(event);
+        logHandler.pushAppLog(appLog);
+    }
+
+    private AppLog getAppLog(ILoggingEvent event) {
+        long timestamp = event.getTimeStamp();
+        String level = event.getLevel().toString();
+        String thread = event.getThreadName();
+        String logger = event.getLoggerName();
+        String message = event.getFormattedMessage();
+        AppLog appLog = new AppLog(app, host, timestamp, level, thread, logger, message);
+        appLog.setDateTimeStr(DateTimeConverter.format(timestamp));
+        return appLog;
+    }
+}

+ 33 - 23
manager/src/main/java/cn/reghao/devops/manager/ws/handler/LogHandler.java

@@ -30,7 +30,7 @@ public class LogHandler implements WebSocketHandler {
     private final Map<String, WebSocketSession> pullSessions = new ConcurrentHashMap<>();
 
     public LogHandler() {
-        threadPool.submit(new PushTask());
+        //threadPool.submit(new PushTask());
     }
 
     @Override
@@ -55,6 +55,21 @@ public class LogHandler implements WebSocketHandler {
         log.info("WebSocket 建立连接");
     }
 
+    public void pushAppLog(AppLog appLog) {
+        String app = appLog.getApp();
+        String host = appLog.getHost();
+        WebSocketSession pullSession = getPullSession(app, host);
+        if (pullSession != null) {
+            String jsonData = JsonConverter.objectToJson(appLog);
+            WebSocketMessage<String> message1 = new TextMessage(jsonData);
+            try {
+                pullSession.sendMessage(message1);
+            } catch (IOException e) {
+                e.printStackTrace();
+            }
+        }
+    }
+
     private Map<String, String> parseParams(String query) {
         String[] params = query.split("&");
         Map<String, String> map = new HashMap<>();
@@ -66,7 +81,7 @@ public class LogHandler implements WebSocketHandler {
         return map;
     }
 
-    Map<Long, Integer> map = new TreeMap<>();
+    Map<Long, Integer> ngxLogMap = new TreeMap<>();
     @Override
     public void handleMessage(WebSocketSession webSocketSession, WebSocketMessage<?> webSocketMessage)
             throws IOException {
@@ -80,19 +95,7 @@ public class LogHandler implements WebSocketHandler {
                 if (object instanceof AppLog) {
                     AppLog appLog = (AppLog) object;
                     String dateTimeStr = DateTimeConverter.format(appLog.getTimestamp());
-
-                    String app = appLog.getApp();
-                    String host = appLog.getHost();
-                    WebSocketSession pullSession = getPullSession(app, host);
-                    if (pullSession != null) {
-                        String jsonData = JsonConverter.objectToJson(appLog);
-                        WebSocketMessage<String> message1 = new TextMessage(jsonData);
-                        try {
-                            pullSession.sendMessage(message1);
-                        } catch (IOException e) {
-                            e.printStackTrace();
-                        }
-                    }
+                    pushAppLog(appLog);
                 } else if (object instanceof NginxLog) {
                     NginxLog nginxLog = (NginxLog) object;
                     String date = nginxLog.getTimeIso8601();
@@ -101,12 +104,12 @@ public class LogHandler implements WebSocketHandler {
                     LocalDateTime localDateTime = LocalDateTime.parse(date, formatter);
                     Long timestamp = localDateTime.toEpochSecond(ZoneOffset.of("+8"));
                     Long key = timestamp;
-                    Integer count = map.get(key);
+                    Integer count = ngxLogMap.get(key);
                     if (count == null) {
-                        map.put(key, 1);
+                        ngxLogMap.put(key, 1);
                     } else {
-                        int count1 = map.get(key) + 1;
-                        map.put(key, count1);
+                        int count1 = ngxLogMap.get(key) + 1;
+                        ngxLogMap.put(key, count1);
                     }
                 }
             } else if (webSocketMessage instanceof PingMessage) {
@@ -167,12 +170,19 @@ public class LogHandler implements WebSocketHandler {
         return false;
     }
 
+    /**
+     * NginxLog 在前端 echarts 中的可视化
+     *
+     * @param
+     * @return
+     * @date 2023-12-01 17:41:07
+     */
     class PushTask implements Runnable {
         @Override
         public void run() {
             while (!Thread.interrupted()) {
                 try {
-                    if (map.size() < 3) {
+                    if (ngxLogMap.size() < 3) {
                         Thread.sleep(10_000);
                         continue;
                     }
@@ -183,15 +193,15 @@ public class LogHandler implements WebSocketHandler {
                     List<String> xList = new ArrayList<>();
                     List<Integer> yList = new ArrayList<>();
                     Set<Long> keys = new HashSet<>();
-                    for (Long key : map.keySet()) {
+                    for (Long key : ngxLogMap.keySet()) {
                         if (key < baseKey) {
                             xList.add(DateTimeConverter.format(key*1000).split(" ")[1]);
-                            yList.add(map.get(key));
+                            yList.add(ngxLogMap.get(key));
                             keys.add(key);
                         }
                     }
 
-                    keys.forEach(map::remove);
+                    keys.forEach(ngxLogMap::remove);
                     keys.clear();
                     List results = new ArrayList();
                     results.add(xList.toArray());

+ 3 - 3
manager/src/main/resources/application-dev.yml

@@ -1,8 +1,8 @@
 spring:
   datasource:
-    url: jdbc:mysql://192.168.0.110:3306/reghao_devops_tdb?useSSL=false&useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2b8
-    username: test
-    password: Test@123456
+    url: jdbc:mysql://localhost:3306/reghao_devops_rdb?useSSL=false&useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2b8
+    username: dev
+    password: Dev@123456
 webhook:
   name: ding-tnb
   url: https://oapi.dingtalk.com/robot/send?access_token=2ede844511f6a12a0429a25585222ef1f0eb99094421ea4b3155f17fda0f4662

+ 0 - 51
manager/src/main/resources/templates/sys/env/add.html

@@ -1,51 +0,0 @@
-<!DOCTYPE html>
-<html xmlns:th="http://www.thymeleaf.org">
-<head th:replace="/common/template :: header(~{::title},~{::link},~{::style})"></head>
-
-<body>
-<div class="layui-form timo-compile">
-    <form th:action="@{/api/sys/env}">
-        <input type="hidden" name="id" th:if="${appEnv}" th:value="${appEnv?.id}"/>
-        <table class="layui-table timo-detail-table">
-            <tbody>
-            <tr>
-                <th>
-                    <label class="layui-form-label required">环境 ID</label>
-                </th>
-                <td>
-                    <div class="layui-form-item" th:if="${appEnv} == null">
-                        <div class="layui-input-inline">
-                            <input class="layui-input" type="text" name="envId" placeholder="请输入环境 ID" required>
-                        </div>
-                    </div>
-                    <div class="layui-form-item" th:if="${appEnv} != null">
-                        <div class="layui-input-inline">
-                            <input class="layui-input" type="text" name="envId" readonly="true" th:value="${appEnv.envId}">
-                        </div>
-                    </div>
-                </td>
-            </tr>
-            <tr>
-                <th>
-                    <label class="layui-form-label required">环境名</label>
-                </th>
-                <td >
-                    <div class="layui-form-item">
-                        <div class="layui-input-inline">
-                            <input class="layui-input" type="text" name="envName" placeholder="请输入应用名" required th:value="${appEnv?.envName}">
-                        </div>
-                    </div>
-                </td>
-            </tr>
-            </tbody>
-        </table>
-        <div class="layui-form-item timo-finally">
-            <button class="layui-btn ajax-submit"><i class="fa fa-check-circle"></i> 保存</button>
-            <button class="layui-btn btn-secondary close-popup"><i class="fa fa-times-circle"></i> 关闭</button>
-        </div>
-    </form>
-</div>
-<script th:replace="/common/template :: script"></script>
-<script type="text/javascript" th:src="@{/js/plugins/jquery-2.2.4.min.js}"></script>
-</body>
-</html>

+ 0 - 117
manager/src/main/resources/templates/sys/env/index.html

@@ -1,117 +0,0 @@
-<!DOCTYPE html>
-<html xmlns:th="http://www.thymeleaf.org">
-<head th:replace="/common/template :: header(~{::title},~{::link},~{::style})">
-    <link rel="stylesheet" th:href="@{/lib/zTree_v3/css/zTreeStyle/zTreeStyle.css}" type="text/css">
-</head>
-
-<body class="timo-layout-page">
-<div class="layui-row layui-col-space20">
-    <div class="layui-col-md6">
-        <div class="layui-card">
-            <div class="layui-card-header timo-card-header">
-                <span><i class="fa fa-bars"></i> 应用环境列表</span>
-                <i class="layui-icon layui-icon-refresh refresh-btn"></i>
-            </div>
-            <div class="layui-card-body">
-                <div class="layui-row timo-card-screen put-row">
-                    <div class="pull-right screen-btn-group">
-                        <div class="btn-group-right">
-                            <button class="layui-btn open-popup" data-title="添加环境" th:attr="data-url=@{/sys/env/add}"
-                                    data-size="640,480">
-                                <i class="fa fa-plus"></i> 添加
-                            </button>
-                        </div>
-                    </div>
-                </div>
-                <div class="timo-table-wrap">
-                    <table class="layui-table timo-table">
-                        <thead>
-                        <tr>
-                            <th class="timo-table-checkbox">
-                                <label class="timo-checkbox"><input type="checkbox">
-                                    <i class="layui-icon layui-icon-ok"></i></label>
-                            </th>
-                            <th class="sortable" data-field="envId">环境 ID</th>
-                            <th class="sortable" data-field="envName">环境名</th>
-                            <th class="sortable" data-field="defaultEnv">是否默认</th>
-                            <th>操作</th>
-                        </tr>
-                        </thead>
-                        <tbody>
-                        <tr th:each="item:${list}">
-                            <td><label class="timo-checkbox"><input type="checkbox" th:value="${item.id}">
-                                <i class="layui-icon layui-icon-ok"></i></label></td>
-                            <td th:text="${item.envId}">环境</td>
-                            <td th:text="${item.envName}">环境名</td>
-                            <td th:text="${item.defaultEnv}">是否默认</td>
-                            <td>
-                                <a class="ajax-post" th:href="@{'/api/sys/env/setdefault/'+${item.id}}">设为默认</a>
-                                <a class="open-popup" data-title="编辑" th:attr="data-url=@{'/sys/env/edit/'+${item.id}}"
-                                   data-size="640,480" href="#">编辑</a>
-                                <a class="ajax-delete" th:attr="data-msg='确定要删除 '+ ${item.envId}"
-                                   th:href="@{'/api/sys/env/' + ${item.id}}">删除</a>
-                            </td>
-                        </tr>
-                        </tbody>
-                    </table>
-                </div>
-            </div>
-        </div>
-    </div>
-    <div class="layui-col-md6">
-        <div class="layui-card">
-            <div class="layui-card-header timo-card-header">
-                <span><i class="fa fa-bars"></i> 应用环境列表</span>
-                <i class="layui-icon layui-icon-refresh refresh-btn"></i>
-            </div>
-            <div class="layui-card-body">
-                <div class="layui-row timo-card-screen put-row">
-                    <div class="pull-right screen-btn-group">
-                        <div class="btn-group-right">
-                            <button class="layui-btn open-popup" data-title="添加环境" th:attr="data-url=@{/sys/env/add}"
-                                    data-size="640,480">
-                                <i class="fa fa-plus"></i> 添加
-                            </button>
-                        </div>
-                    </div>
-                </div>
-                <div class="timo-table-wrap">
-                    <table class="layui-table timo-table">
-                        <thead>
-                        <tr>
-                            <th class="timo-table-checkbox">
-                                <label class="timo-checkbox"><input type="checkbox">
-                                    <i class="layui-icon layui-icon-ok"></i></label>
-                            </th>
-                            <th class="sortable" data-field="envId">环境 ID</th>
-                            <th class="sortable" data-field="envName">环境名</th>
-                            <th class="sortable" data-field="defaultEnv">是否默认</th>
-                            <th>操作</th>
-                        </tr>
-                        </thead>
-                        <tbody>
-                        <tr th:each="item:${list}">
-                            <td><label class="timo-checkbox"><input type="checkbox" th:value="${item.id}">
-                                <i class="layui-icon layui-icon-ok"></i></label></td>
-                            <td th:text="${item.envId}">环境</td>
-                            <td th:text="${item.envName}">环境名</td>
-                            <td th:text="${item.defaultEnv}">是否默认</td>
-                            <td>
-                                <a class="ajax-post" th:href="@{'/api/sys/env/setdefault/'+${item.id}}">设为默认</a>
-                                <a class="open-popup" data-title="编辑" th:attr="data-url=@{'/sys/env/edit/'+${item.id}}"
-                                   data-size="640,480" href="#">编辑</a>
-                                <a class="ajax-delete" th:attr="data-msg='确定要删除 '+ ${item.envId}"
-                                   th:href="@{'/api/sys/env/' + ${item.id}}">删除</a>
-                            </td>
-                        </tr>
-                        </tbody>
-                    </table>
-                </div>
-            </div>
-        </div>
-    </div>
-</div>
-
-<script th:replace="/common/template :: script"></script>
-</body>
-</html>

+ 123 - 0
manager/src/main/resources/templates/sys/syslog.html

@@ -0,0 +1,123 @@
+<!DOCTYPE html>
+<html xmlns:th="http://www.thymeleaf.org">
+<head th:replace="/common/template :: header(~{::title},~{::link},~{::style})">
+    <link rel="stylesheet" th:href="@{/lib/zTree_v3/css/zTreeStyle/zTreeStyle.css}" type="text/css">
+    <link rel="stylesheet" th:href="@{/css/xterm.css}"/>
+</head>
+
+<body class="timo-layout-page">
+<div class="layui-card">
+    <div class="layui-card-header">
+        <span><i class="fa fa-table"></i> 系统实时日志</span>
+        <span id="ws-status" style="background: red">离线</span>
+    </div>
+    <div class="layui-card-body" style="background: black">
+        <ul id="applog" class="flow-default layui-timeline" style="height: 500px;overflow: auto">
+            <!--<li id="li2">
+                <div class="layui-text">
+                    <p>
+                        1. 更新:重命名菜单类型为:目录、菜单、按钮<2. 更新:重写Shiro“记住我”系列化数据,减少cookie体积3. 新增:获取用户角色列表方法4. 修复:获取部门数据时延迟加载超时问题 5. 修复:将jq版本改为2.2.4,解决layui弹出窗口最大化问题 6. 新增:项目配置项,可直接通过yml文件配置Shiro和XSS防护忽略规则 7. 新增:ResultExceptionError和ResultExceptionSuccess异常类 8. 修复:若干页面显示问题,优化加载时提示<br>
+                    </p>
+                </div>
+            </li>-->
+        </ul>
+    </div>
+</div>
+</body>
+<script th:replace="/common/template :: script"></script>
+<script type="text/javascript" th:src="@{/js/plugins/jquery-3.4.1.min.js}"></script>
+<script type="text/javascript" th:inline="javascript">
+    function add(text) {
+        var p = document.createElement("p");
+        p.innerHTML = text + '<br>'
+
+        var div = document.createElement("div");
+        div.className = 'layui-text'
+        div.appendChild(p)
+
+        var li = document.createElement("li");
+        li.appendChild(div)
+
+        var ul = document.getElementById("applog");
+        //ul.prepend(li);
+        ul.appendChild(li);
+    }
+
+    function setOnline() {
+        var span = document.getElementById("ws-status")
+        span.style = "background: green"
+        span.innerHTML = '在线'
+    }
+
+    function setOffline() {
+        var span = document.getElementById("ws-status")
+        span.style = "background: red"
+        span.innerHTML = '离线'
+    }
+
+    var connected = false
+    var ws
+    function initWebSocket() {
+        if ("WebSocket" in window) {
+            var token = '0123456789'
+            var app = 'devops-manager'
+            var host = 'devops.reghao.cn'
+            var params = 'token=' + token + '&app=' + app + '&host=' + host;
+            var url = "wss://devops.reghao.cn/ws/log/pull?" + params
+            ws = new WebSocket(url);
+            ws.onopen = function() {
+                connected = true
+                setOnline()
+                // Web Socket 已连接上,使用 send() 方法发送数据
+                console.log("websocket connected...");
+            };
+            ws.onclose = function() {
+                connected = false
+                setOffline()
+                console.log("websocket connection closed...");
+                reconnect()
+            };
+            ws.onerror = function () {
+                connected = false
+                setOffline()
+                console.log("websocket connection error...");
+                reconnect()
+            };
+            ws.onmessage = function (evt) {
+                var payload = JSON.parse(evt.data)
+                var app = '<span style="color: yellowgreen">' +  payload.app + '</span>'
+                var concat = '<span style="color: red">' +  '@' + '</span>'
+                var host = '<span style="color: yellowgreen">' +  payload.host + ' ' + '</span>'
+                var timestamp = payload.dateTimeStr
+                var thread = payload.thread
+                var level
+                if (payload.level === 'INFO') {
+                    level = '<span style="color: green">' +  payload.level + ' ' + '</span>'
+                } else if (payload.level === 'ERROR') {
+                    level = '<span style="color: red">' +  payload.level + ' ' + '</span>'
+                } else {
+                    level = '<span style="color: yellow">' +  payload.level + ' ' + '</span>'
+                }
+
+                var logger = '<span style="color: wheat">' +  payload.logger + ' ' + '</span>'
+                var message = '<span style="color: white">' +  payload.message + ' ' + '</span>'
+
+                var text = app + concat +  host + ' ' + timestamp + ' ' + thread + ' ' + level + ' ' + logger + ' ' + message + '<br>'
+                add(text)
+            }
+        } else {
+            // 浏览器不支持 WebSocket
+            alert("您的浏览器不支持 WebSocket!");
+        }
+    }
+    function reconnect() {
+        if (connected) return
+
+        setTimeout(function () {
+            console.log('websocket reconnecting...')
+            initWebSocket()
+        }, 5000)
+    }
+    initWebSocket()
+</script>
+</html>