Prechádzať zdrojové kódy

devops-manager 添加应用运行日志的实时查看

reghao 2 rokov pred
rodič
commit
e985f66f7a
22 zmenil súbory, kde vykonal 639 pridanie a 15 odobranie
  1. 5 0
      manager/pom.xml
  2. 30 2
      manager/src/main/java/cn/reghao/devops/manager/app/controller/page/StatusPageController.java
  3. 19 3
      manager/src/main/java/cn/reghao/devops/manager/event/ws/WebSocketConfig.java
  4. 3 1
      manager/src/main/java/cn/reghao/devops/manager/event/ws/WebSocketInterceptor.java
  5. 53 0
      manager/src/main/java/cn/reghao/devops/manager/event/ws/WsConnection.java
  6. 1 0
      manager/src/main/java/cn/reghao/devops/manager/event/ws/WsSender.java
  7. 2 1
      manager/src/main/java/cn/reghao/devops/manager/event/ws/handler/AgentWebSocketHandler.java
  8. 108 0
      manager/src/main/java/cn/reghao/devops/manager/event/ws/handler/LogHandler.java
  9. 1 1
      manager/src/main/java/cn/reghao/devops/manager/event/ws/handler/SshWebSocketHandler.java
  10. 87 0
      manager/src/main/java/cn/reghao/devops/manager/log/db/mongo/AccessLogMongo.java
  11. 99 0
      manager/src/main/java/cn/reghao/devops/manager/log/db/mongo/AppLogMongo.java
  12. 12 0
      manager/src/main/java/cn/reghao/devops/manager/log/model/po/AccessLog.java
  13. 31 0
      manager/src/main/java/cn/reghao/devops/manager/log/model/po/AppLog.java
  14. 30 0
      manager/src/main/java/cn/reghao/devops/manager/log/model/vo/AppLogVO.java
  15. 41 0
      manager/src/main/java/cn/reghao/devops/manager/log/service/LogService.java
  16. 29 1
      manager/src/main/java/cn/reghao/devops/manager/monitor/controller/page/MachineMonitorPageController.java
  17. 4 0
      manager/src/main/java/cn/reghao/devops/manager/thymeleaf/attribute/SelectDictAttrProcessor.java
  18. 8 1
      manager/src/main/resources/application-dev.yml
  19. 4 1
      manager/src/main/resources/static/js/websocket.js
  20. 2 2
      manager/src/main/resources/templates/app/stat/index1.html
  21. 3 0
      manager/src/main/resources/templates/app/stat/index2.html
  22. 67 2
      manager/src/main/resources/templates/monitor/machine/index.html

+ 5 - 0
manager/pom.xml

@@ -92,6 +92,11 @@
             <version>3.3.1</version>
         </dependency>
 
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-data-mongodb</artifactId>
+        </dependency>
+
         <dependency>
             <groupId>org.springframework.boot</groupId>
             <artifactId>spring-boot-starter-security</artifactId>

+ 30 - 2
manager/src/main/java/cn/reghao/devops/manager/app/controller/page/StatusPageController.java

@@ -5,6 +5,8 @@ import cn.reghao.devops.manager.app.model.constant.EnvMap;
 import cn.reghao.devops.manager.app.model.vo.AppRunning;
 import cn.reghao.devops.manager.app.model.vo.AppRunningVO;
 import cn.reghao.devops.manager.app.service.bd.DeployStat;
+import cn.reghao.devops.manager.log.model.vo.AppLogVO;
+import cn.reghao.devops.manager.log.service.LogService;
 import cn.reghao.devops.manager.util.db.PageSort;
 import cn.reghao.jutil.jdk.db.PageList;
 import io.swagger.annotations.Api;
@@ -32,9 +34,11 @@ import java.util.*;
 @RequestMapping("/app/stat")
 public class StatusPageController {
     private final DeployStat deployStat;
+    private final LogService logService;
 
-    public StatusPageController(DeployStat deployStat) {
+    public StatusPageController(DeployStat deployStat, LogService logService) {
         this.deployStat = deployStat;
+        this.logService = logService;
     }
 
     @ApiOperation(value = "应用运行状态列表页面")
@@ -81,10 +85,34 @@ public class StatusPageController {
     }
 
     @ApiOperation(value = "应用运行状态详情页面")
-    @GetMapping(value = "/{appId}")
+    @GetMapping(value = "/detail/{appId}")
     public String statusPage2(@PathVariable(value = "appId") String appId, Model model) {
         List<AppRunning> list = deployStat.getAppRunning(appId);
         model.addAttribute("list", list);
         return "/app/stat/index2";
     }
+
+    @ApiOperation(value = "应用运行日志页面")
+    @GetMapping(value = "/log")
+    public String appLogPage(@RequestParam(value = "app", required = false) String app,
+                            @RequestParam(value = "host", required = false) String host,
+                            @RequestParam(value = "level", required = false) String level,
+                            Model model) {
+        if (app == null) {
+            app = "admin-service";
+        }
+
+        if (host == null) {
+            host = "172.16.90.200";
+        }
+
+        if (level == null) {
+            level = "info";
+        }
+
+        List<AppLogVO> list = logService.getAppLogs(app, host, level);
+        model.addAttribute("level", level);
+        model.addAttribute("list", list);
+        return "/monitor/machine/index";
+    }
 }

+ 19 - 3
manager/src/main/java/cn/reghao/devops/manager/event/ws/WebSocketConfig.java

@@ -13,19 +13,35 @@ import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry
 @EnableWebSocket
 @Configuration
 public class WebSocketConfig implements WebSocketConfigurer {
+    private final WebSocketInterceptor webSocketInterceptor;
     private final WebSocketHandler sshWebSocketHandler;
     private final WebSocketHandler agentWebSocketHandler;
+    private final WebSocketHandler logHandler;
 
-    public WebSocketConfig(WebSocketHandler sshWebSocketHandler, WebSocketHandler agentWebSocketHandler) {
+    public WebSocketConfig(WebSocketInterceptor webSocketInterceptor, WebSocketHandler sshWebSocketHandler, 
+                           WebSocketHandler agentWebSocketHandler, WebSocketHandler logHandler) {
+        this.webSocketInterceptor = webSocketInterceptor;
         this.sshWebSocketHandler = sshWebSocketHandler;
         this.agentWebSocketHandler = agentWebSocketHandler;
+        this.logHandler = logHandler;
     }
 
     @Override
     public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
         registry.addHandler(sshWebSocketHandler, "/ws/ssh")
-                .addHandler(agentWebSocketHandler, "/ws/agent")
-                .addInterceptors(new WebsocketInterceptor())
+                .addInterceptors(webSocketInterceptor)
+                .setAllowedOrigins("*");
+        
+        registry.addHandler(agentWebSocketHandler, "/ws/agent")
+                .addInterceptors(webSocketInterceptor)
+                .setAllowedOrigins("*");
+
+        registry.addHandler(logHandler, "/ws/log/push")
+                .addInterceptors(webSocketInterceptor)
+                .setAllowedOrigins("*");
+
+        registry.addHandler(logHandler, "/ws/log/pull")
+                .addInterceptors(webSocketInterceptor)
                 .setAllowedOrigins("*");
     }
 }

+ 3 - 1
manager/src/main/java/cn/reghao/devops/manager/event/ws/WebsocketInterceptor.java → manager/src/main/java/cn/reghao/devops/manager/event/ws/WebSocketInterceptor.java

@@ -5,6 +5,7 @@ import org.springframework.http.server.ServerHttpRequest;
 import org.springframework.http.server.ServerHttpResponse;
 import org.springframework.http.server.ServletServerHttpRequest;
 import org.springframework.lang.Nullable;
+import org.springframework.stereotype.Component;
 import org.springframework.web.socket.WebSocketHandler;
 import org.springframework.web.socket.server.HandshakeInterceptor;
 
@@ -14,7 +15,8 @@ import java.util.Map;
  * @author reghao
  * @date 2021-07-07 13:15:26
  */
-public class WebsocketInterceptor implements HandshakeInterceptor {
+@Component
+public class WebSocketInterceptor implements HandshakeInterceptor {
     @Override
     public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response,
                                    WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception {

+ 53 - 0
manager/src/main/java/cn/reghao/devops/manager/event/ws/WsConnection.java

@@ -0,0 +1,53 @@
+package cn.reghao.devops.manager.event.ws;
+
+import cn.reghao.jutil.jdk.serializer.JsonConverter;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+import org.springframework.web.socket.TextMessage;
+import org.springframework.web.socket.WebSocketSession;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * @author reghao
+ * @date 2022-09-01 10:44:06
+ */
+@Slf4j
+@Component
+public class WsConnection {
+    private final Map<String, WebSocketSession> userSessions = new HashMap<>();
+    private final Map<String, String> sessionUsers = new HashMap<>();
+
+    public void addUserSession(String userId, WebSocketSession webSocketSession) {
+        String sessionId = webSocketSession.getId();
+        userSessions.put(userId, webSocketSession);
+        sessionUsers.put(sessionId, userId);
+    }
+
+    public void removeUserSession(WebSocketSession webSocketSession) {
+        String sessionId = webSocketSession.getId();
+        String userId = sessionUsers.get(sessionId);
+        userSessions.remove(userId);
+        sessionUsers.remove(userId);
+    }
+
+    public String getUserId(String sessionId) {
+        return sessionUsers.get(sessionId);
+    }
+
+    /*public void sendTextMsg(PushMsg pushMsg) throws IOException {
+        String userId = pushMsg.getReceiverId();
+        EventMsg eventMsg = pushMsg.getEventMsg();
+
+        WebSocketSession webSocketSession = userSessions.get(userId);
+        if (webSocketSession == null) {
+            log.error("{} 不在线", userId);
+            return;
+        }
+
+        TextMessage textMessage = new TextMessage(JsonConverter.objectToJson(eventMsg));
+        webSocketSession.sendMessage(textMessage);
+    }*/
+}

+ 1 - 0
manager/src/main/java/cn/reghao/devops/manager/event/ws/WsSender.java

@@ -17,6 +17,7 @@ import java.util.Map;
  */
 @Slf4j
 @Component
+@Deprecated
 public class WsSender implements MessageSender {
     private final static Map<String, WebSocketSession> sessionMap = new HashMap<>();
 

+ 2 - 1
manager/src/main/java/cn/reghao/devops/manager/event/ws/AgentWebSocketHandler.java → manager/src/main/java/cn/reghao/devops/manager/event/ws/handler/AgentWebSocketHandler.java

@@ -1,5 +1,6 @@
-package cn.reghao.devops.manager.event.ws;
+package cn.reghao.devops.manager.event.ws.handler;
 
+import cn.reghao.devops.manager.event.ws.WsSender;
 import cn.reghao.devops.manager.machine.service.MachineService;
 import cn.reghao.jutil.jdk.serializer.JdkSerializer;
 import cn.reghao.devops.manager.machine.util.Keys;

+ 108 - 0
manager/src/main/java/cn/reghao/devops/manager/event/ws/handler/LogHandler.java

@@ -0,0 +1,108 @@
+package cn.reghao.devops.manager.event.ws.handler;
+
+import cn.reghao.devops.manager.event.ws.WsConnection;
+import cn.reghao.devops.manager.log.service.LogService;
+import cn.reghao.jutil.jdk.result.AppLog;
+import cn.reghao.jutil.jdk.serializer.JdkSerializer;
+import cn.reghao.jutil.jdk.serializer.JsonConverter;
+import cn.reghao.jutil.tool.jwt.Jwt;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+import org.springframework.web.socket.*;
+
+import java.io.IOException;
+
+/**
+ * @author reghao
+ * @date 2022-03-11 16:45:52
+ */
+@Slf4j
+@Component
+public class LogHandler implements WebSocketHandler {
+    private final WsConnection wsConnection;
+    private LogService logService;
+    private WebSocketSession pullSession;
+
+    public LogHandler(WsConnection wsConnection, LogService logService) {
+        this.wsConnection = wsConnection;
+        this.logService = logService;
+    }
+
+    @Override
+    public void afterConnectionEstablished(WebSocketSession webSocketSession) throws IOException {
+        String path = webSocketSession.getUri().getPath();
+        if (path.equals("/ws/log/pull")) {
+            this.pullSession = webSocketSession;
+        }
+
+        String query = webSocketSession.getUri().getQuery();
+        /*String jwtToken = query.replace("token=", "");
+        String userId = getUserId(jwtToken);
+        if (userId == null) {
+            webSocketSession.close(CloseStatus.NO_STATUS_CODE);
+            return;
+        }*/
+        String userId = "10001";
+        wsConnection.addUserSession(userId, webSocketSession);
+        log.info("WebSocket 建立连接");
+    }
+
+    private String getUserId(String jwtToken) {
+        if (jwtToken == null) {
+            return null;
+        }
+
+        return Jwt.parse(jwtToken, "").getUserId();
+    }
+
+    @Override
+    public void handleMessage(WebSocketSession webSocketSession, WebSocketMessage<?> webSocketMessage)
+            throws IOException {
+        try {
+            String sessionId = webSocketSession.getId();
+            if (webSocketMessage instanceof TextMessage) {
+                String payload = (String) webSocketMessage.getPayload();
+                AppLog appLog = JsonConverter.jsonToObject(payload, AppLog.class);
+                wsConnection.getUserId("");
+            } else if (webSocketMessage instanceof BinaryMessage) {
+                log.info("接收到 WebSocket 二进制消息");
+                BinaryMessage binaryMessage = (BinaryMessage) webSocketMessage;
+                Object object = JdkSerializer.deserialize(binaryMessage.getPayload().array());
+                if (object instanceof AppLog) {
+                    AppLog appLog = (AppLog) object;
+                    logService.saveAppLog(appLog);
+                    if (pullSession != null) {
+                        String jsonData = JsonConverter.objectToJson(appLog);
+                        WebSocketMessage<String> message1 = new TextMessage(jsonData);
+                        pullSession.sendMessage(message1);
+                    }
+                }
+            } else if (webSocketMessage instanceof PingMessage) {
+                log.info("接收到 WebSocket PingMessage");
+            } else if (webSocketMessage instanceof PongMessage) {
+                log.info("接收到 WebSocket PongMessage");
+            } else {
+                log.error("接收到未知类型的 WebSocket 消息");
+            }
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+    }
+
+    @Override
+    public void handleTransportError(WebSocketSession webSocketSession, Throwable throwable) {
+        log.error("WebSocket 数据传输错误");
+        wsConnection.removeUserSession(webSocketSession);
+    }
+
+    @Override
+    public void afterConnectionClosed(WebSocketSession webSocketSession, CloseStatus closeStatus) {
+        log.info("WebSocket 断开连接");
+        wsConnection.removeUserSession(webSocketSession);
+    }
+
+    @Override
+    public boolean supportsPartialMessages() {
+        return false;
+    }
+}

+ 1 - 1
manager/src/main/java/cn/reghao/devops/manager/event/ws/SshWebSocketHandler.java → manager/src/main/java/cn/reghao/devops/manager/event/ws/handler/SshWebSocketHandler.java

@@ -1,4 +1,4 @@
-package cn.reghao.devops.manager.event.ws;
+package cn.reghao.devops.manager.event.ws.handler;
 
 import cn.reghao.devops.manager.machine.util.Keys;
 import cn.reghao.devops.manager.machine.util.WebSsh;

+ 87 - 0
manager/src/main/java/cn/reghao/devops/manager/log/db/mongo/AccessLogMongo.java

@@ -0,0 +1,87 @@
+package cn.reghao.devops.manager.log.db.mongo;
+
+import cn.reghao.devops.manager.log.model.po.AccessLog;
+import cn.reghao.jutil.jdk.db.BaseCrud;
+import cn.reghao.jutil.jdk.db.BaseQuery;
+import com.mongodb.MongoBulkWriteException;
+import com.mongodb.client.model.InsertManyOptions;
+import com.mongodb.client.result.InsertManyResult;
+import lombok.extern.slf4j.Slf4j;
+import org.bson.Document;
+import org.springframework.data.domain.Sort;
+import org.springframework.data.mongodb.core.MongoTemplate;
+import org.springframework.data.mongodb.core.convert.MongoConverter;
+import org.springframework.data.mongodb.core.query.Criteria;
+import org.springframework.data.mongodb.core.query.Query;
+import org.springframework.stereotype.Repository;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * @author reghao
+ * @date 2023-10-23 11:23:02
+ */
+@Slf4j
+@Repository
+public class AccessLogMongo implements BaseCrud<AccessLog>, BaseQuery<AccessLog> {
+    private final String colName = "AccessLog";
+    private final MongoTemplate mongoTemplate;
+    private final MongoConverter mongoConverter;
+    private final int pageSize = 1000;
+
+    public AccessLogMongo(MongoTemplate mongoTemplate, MongoConverter mongoConverter) {
+        this.mongoTemplate = mongoTemplate;
+        this.mongoConverter = mongoConverter;
+    }
+
+    @Override
+    public AccessLog save(AccessLog accessLog) {
+        Document doc = new Document();
+        mongoConverter.write(accessLog, doc);
+        mongoTemplate.getCollection(colName).insertOne(doc);
+        return null;
+    }
+
+    @Override
+    public void saveAll(List<AccessLog> list) {
+        List<Document> documents = list.stream()
+                .map(t -> {
+                    Document doc = new Document();
+                    mongoConverter.write(t, doc);
+                    return doc;
+                })
+                .collect(Collectors.toList());
+
+        InsertManyOptions options = new InsertManyOptions();
+        // 忽略 insert 失败的文档
+        options.ordered(false);
+        try {
+            InsertManyResult result = mongoTemplate.getCollection(colName).insertMany(documents, options);
+        }  catch (MongoBulkWriteException ignore) {
+        }
+    }
+
+    @Override
+    public void update(AccessLog accessLog) {
+    }
+
+    @Override
+    public void delete(AccessLog accessLog) {
+    }
+
+    public List<AccessLog> findByPage(long page) {
+        Query query = new Query();
+        query.skip((page - 1) * pageSize).limit(pageSize);
+        query.with(Sort.by(new Sort.Order(Sort.Direction.DESC, "createTime")));
+        return mongoTemplate.find(query, AccessLog.class, colName);
+    }
+
+    public List<AccessLog> findAllByPage(long page, int size, List<Criteria> criteriaList) {
+        Query query = new Query();
+        criteriaList.forEach(query::addCriteria);
+        query.skip((page - 1) * size).limit(size);
+        query.with(Sort.by(new Sort.Order(Sort.Direction.DESC, "createTime")));
+        return mongoTemplate.find(query, AccessLog.class, colName);
+    }
+}

+ 99 - 0
manager/src/main/java/cn/reghao/devops/manager/log/db/mongo/AppLogMongo.java

@@ -0,0 +1,99 @@
+package cn.reghao.devops.manager.log.db.mongo;
+
+import cn.reghao.devops.manager.log.model.po.AppLog;
+import cn.reghao.jutil.jdk.db.BaseCrud;
+import cn.reghao.jutil.jdk.db.BaseQuery;
+import com.mongodb.MongoBulkWriteException;
+import com.mongodb.client.model.InsertManyOptions;
+import com.mongodb.client.result.InsertManyResult;
+import lombok.extern.slf4j.Slf4j;
+import org.bson.Document;
+import org.springframework.data.domain.Sort;
+import org.springframework.data.mongodb.core.MongoTemplate;
+import org.springframework.data.mongodb.core.convert.MongoConverter;
+import org.springframework.data.mongodb.core.query.Criteria;
+import org.springframework.data.mongodb.core.query.Query;
+import org.springframework.stereotype.Repository;
+
+import java.util.List;
+import java.util.Locale;
+import java.util.stream.Collectors;
+
+/**
+ * @author reghao
+ * @date 2023-10-23 11:31:02
+ */
+@Slf4j
+@Repository
+public class AppLogMongo implements BaseCrud<AppLog>, BaseQuery<AppLog> {
+    private final String colName = "AppLog";
+    private final MongoTemplate mongoTemplate;
+    private final MongoConverter mongoConverter;
+    private final int pageSize = 1000;
+
+    public AppLogMongo(MongoTemplate mongoTemplate, MongoConverter mongoConverter) {
+        this.mongoTemplate = mongoTemplate;
+        this.mongoConverter = mongoConverter;
+    }
+
+    @Override
+    public AppLog save(AppLog appLog) {
+        Document doc = new Document();
+        mongoConverter.write(appLog, doc);
+        mongoTemplate.getCollection(colName).insertOne(doc);
+        return null;
+    }
+
+    @Override
+    public void saveAll(List<AppLog> list) {
+        List<Document> documents = list.stream()
+                .map(t -> {
+                    Document doc = new Document();
+                    mongoConverter.write(t, doc);
+                    return doc;
+                })
+                .collect(Collectors.toList());
+
+        InsertManyOptions options = new InsertManyOptions();
+        // 忽略 insert 失败的文档
+        options.ordered(false);
+        try {
+            InsertManyResult result = mongoTemplate.getCollection(colName).insertMany(documents, options);
+        }  catch (MongoBulkWriteException ignore) {
+        }
+    }
+
+    @Override
+    public void update(AppLog appLog) {
+    }
+
+    @Override
+    public void delete(AppLog appLog) {
+    }
+
+    public List<AppLog> findByPage(long page) {
+        Query query = new Query();
+        query.skip((page - 1) * pageSize).limit(pageSize);
+        query.with(Sort.by(new Sort.Order(Sort.Direction.DESC, "timestamp")));
+        return mongoTemplate.find(query, AppLog.class, colName);
+    }
+
+    public List<AppLog> findByPage(String app, String host, String level) {
+        Query query = new Query();
+        query.addCriteria(Criteria.where("app").is(app));
+        query.addCriteria(Criteria.where("host").is(host));
+        query.addCriteria(Criteria.where("level").is(level.toUpperCase(Locale.ROOT)));
+
+        query.skip(0).limit(pageSize);
+        query.with(Sort.by(new Sort.Order(Sort.Direction.DESC, "timestamp")));
+        return mongoTemplate.find(query, AppLog.class, colName);
+    }
+
+    public List<AppLog> findAllByPage(long page, int size, List<Criteria> criteriaList) {
+        Query query = new Query();
+        criteriaList.forEach(query::addCriteria);
+        query.skip((page - 1) * size).limit(size);
+        query.with(Sort.by(new Sort.Order(Sort.Direction.DESC, "createTime")));
+        return mongoTemplate.find(query, AppLog.class, colName);
+    }
+}

+ 12 - 0
manager/src/main/java/cn/reghao/devops/manager/log/model/po/AccessLog.java

@@ -0,0 +1,12 @@
+package cn.reghao.devops.manager.log.model.po;
+
+import cn.reghao.jutil.jdk.db.BaseObject;
+import lombok.NoArgsConstructor;
+
+/**
+ * @author reghao
+ * @date 2023-10-23 11:23:02
+ */
+@NoArgsConstructor
+public class AccessLog extends BaseObject<String> {
+}

+ 31 - 0
manager/src/main/java/cn/reghao/devops/manager/log/model/po/AppLog.java

@@ -0,0 +1,31 @@
+package cn.reghao.devops.manager.log.model.po;
+
+import cn.reghao.jutil.jdk.db.BaseObject;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+/**
+ * @author reghao
+ * @date 2023-10-23 11:26:38
+ */
+@NoArgsConstructor
+@Getter
+public class AppLog extends BaseObject<String> {
+    private String app;
+    private String host;
+    private long timestamp;
+    private String level;
+    private String thread;
+    private String logger;
+    private String message;
+
+    public AppLog(cn.reghao.jutil.jdk.result.AppLog appLog) {
+        this.app = appLog.getApp();
+        this.host = appLog.getHost();
+        this.timestamp = appLog.getTimestamp();
+        this.level = appLog.getLevel();
+        this.thread = appLog.getThread();
+        this.logger = appLog.getLogger();
+        this.message = appLog.getMessage();
+    }
+}

+ 30 - 0
manager/src/main/java/cn/reghao/devops/manager/log/model/vo/AppLogVO.java

@@ -0,0 +1,30 @@
+package cn.reghao.devops.manager.log.model.vo;
+
+import cn.reghao.devops.manager.log.model.po.AppLog;
+import cn.reghao.jutil.jdk.converter.DateTimeConverter;
+import lombok.Getter;
+
+/**
+ * @author reghao
+ * @date 2023-10-23 16:18:26
+ */
+@Getter
+public class AppLogVO {
+    private String app;
+    private String host;
+    private String timestamp;
+    private String level;
+    private String thread;
+    private String logger;
+    private String message;
+
+    public AppLogVO(AppLog appLog) {
+        this.app = appLog.getApp();
+        this.host = appLog.getHost();
+        this.timestamp = DateTimeConverter.format(appLog.getTimestamp());
+        this.level = appLog.getLevel();
+        this.thread = appLog.getThread();
+        this.logger = appLog.getLogger();
+        this.message = appLog.getMessage();
+    }
+}

+ 41 - 0
manager/src/main/java/cn/reghao/devops/manager/log/service/LogService.java

@@ -0,0 +1,41 @@
+package cn.reghao.devops.manager.log.service;
+
+import cn.reghao.devops.manager.log.db.mongo.AccessLogMongo;
+import cn.reghao.devops.manager.log.db.mongo.AppLogMongo;
+import cn.reghao.devops.manager.log.model.po.AccessLog;
+import cn.reghao.devops.manager.log.model.po.AppLog;
+import cn.reghao.devops.manager.log.model.vo.AppLogVO;
+import org.springframework.stereotype.Service;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * @author reghao
+ * @date 2023-10-23 14:36:52
+ */
+@Service
+public class LogService {
+    private final AppLogMongo appLogMongo;
+    private final AccessLogMongo accessLogMongo;
+
+    private LogService(AppLogMongo appLogMongo, AccessLogMongo accessLogMongo) {
+        this.appLogMongo = appLogMongo;
+        this.accessLogMongo = accessLogMongo;
+    }
+
+    public void saveAppLog(cn.reghao.jutil.jdk.result.AppLog appLog) {
+        appLogMongo.save(new AppLog(appLog));
+    }
+
+    public List<AppLogVO> getAppLogs(String app, String host, String level) {
+        List<AppLog> list = appLogMongo.findByPage(app, host ,level);
+        Collections.reverse(list);
+        return list.stream().map(AppLogVO::new).collect(Collectors.toList());
+    }
+
+    public void saveAccessLog(AccessLog accessLog) {
+        accessLogMongo.save(accessLog);
+    }
+}

+ 29 - 1
manager/src/main/java/cn/reghao/devops/manager/monitor/controller/page/MachineMonitorPageController.java

@@ -1,5 +1,7 @@
 package cn.reghao.devops.manager.monitor.controller.page;
 
+import cn.reghao.devops.manager.log.model.vo.AppLogVO;
+import cn.reghao.devops.manager.log.service.LogService;
 import io.swagger.annotations.Api;
 import io.swagger.annotations.ApiOperation;
 import org.springframework.stereotype.Controller;
@@ -9,6 +11,8 @@ import org.springframework.web.bind.annotation.PathVariable;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RequestParam;
 
+import java.util.List;
+
 /**
  * @author reghao
  * @date 2022-09-23 15:26:26
@@ -17,9 +21,33 @@ import org.springframework.web.bind.annotation.RequestParam;
 @Controller
 @RequestMapping("/monitor/machine")
 public class MachineMonitorPageController {
+    private final LogService logService;
+
+    public MachineMonitorPageController(LogService logService) {
+        this.logService = logService;
+    }
+
     @ApiOperation(value = "机器监控页面")
     @GetMapping
-    public String indexPage(@RequestParam(value = "env", required = false) String env, Model model) {
+    public String indexPage(@RequestParam(value = "app", required = false) String app,
+                            @RequestParam(value = "host", required = false) String host,
+                            @RequestParam(value = "level", required = false) String level,
+                            Model model) {
+        if (app == null) {
+            app = "admin-service";
+        }
+
+        if (host == null) {
+            host = "172.16.90.200";
+        }
+
+        if (level == null) {
+            level = "info";
+        }
+
+        List<AppLogVO> list = logService.getAppLogs(app, host, level);
+        model.addAttribute("level", level);
+        model.addAttribute("list", list);
         return "/monitor/machine/index";
     }
 

+ 4 - 0
manager/src/main/java/cn/reghao/devops/manager/thymeleaf/attribute/SelectDictAttrProcessor.java

@@ -37,6 +37,10 @@ public class SelectDictAttrProcessor extends AbstractAttributeTagProcessor {
             localCache.get("ENVIRONMENT").put(appEnv.getEnvId(), appEnv.getEnvId());
         });
 
+        localCache.put("LOGLEVEL", new HashMap<>());
+        localCache.get("LOGLEVEL").put("info", "info");
+        localCache.get("LOGLEVEL").put("error", "error");
+
         localCache.put("APP_TYPE", new HashMap<>());
         Arrays.asList(AppType.values()).forEach(appType -> {
             localCache.get("APP_TYPE").put(appType.name(), appType.name());

+ 8 - 1
manager/src/main/resources/application-dev.yml

@@ -4,4 +4,11 @@ spring:
   datasource:
     url: jdbc:mysql://localhost:3306/reghao_devops_rdb?useSSL=false&useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2b8
     username: dev
-    password: Dev@123456
+    password: Dev@123456
+  data:
+    mongodb:
+      host: localhost
+      database: "reghao_log_rdb"
+      authentication-database: admin
+      username: dev
+      password: Dev@123456

+ 4 - 1
manager/src/main/resources/static/js/websocket.js

@@ -20,7 +20,10 @@ WebSocketClient.prototype._generateEndpoint = function () {
             prefix = 'ws://' + hostname + ':' + port;
         }
     }
-    return  prefix + '/ws/ssh';
+
+    var url = 'wss://devops.reghao.cn/ws/log/pull?token=12345678'
+    url = 'wss://devops.reghao.cn/ws/ssh?token=12345678'
+    return  url;
 };
 
 WebSocketClient.prototype.connect = function (options) {

+ 2 - 2
manager/src/main/resources/templates/app/stat/index1.html

@@ -60,8 +60,8 @@
                     <td th:text="${item.bindPorts}">监听端口</td>
                     <td th:text="${item.totalDeployed}">已部署数量</td>
                     <td>
-                        <a class="open-popup" th:attr="data-title=${item.appName} + ' - 运行状态', data-url=@{'/app/stat/'+${item.appId}}"
-                           data-size="1200,500" href="#">查看</a>
+                        <a class="open-popup" th:attr="data-title=${item.appName} + ' - 运行状态',
+                        data-url=@{'/app/stat/detail/'+${item.appId}}" data-size="1200,500" href="#">查看</a>
                     </td>
                 </tr>
                 </tbody>

+ 3 - 0
manager/src/main/resources/templates/app/stat/index2.html

@@ -32,6 +32,9 @@
                         <a class="ajax-post" th:href="@{'/api/app/status/stop/'+${item.appId}+'/'+${item.machineId}}">停止</a>
                         <a class="ajax-post" th:href="@{'/api/app/status/start/'+${item.appId}+'/'+${item.machineId}}">启动</a>
                         <a class="ajax-get" th:href="@{'/api/app/status/'+${item.appId}+'/'+${item.machineId}}">当前状态</a>
+                        <a class="open-popup" th:attr="data-title=${item.appName} + ' - 运行日志',
+                            data-url=@{'/app/stat/log?app='+${item.appId}+'&host='+${item.machineIpv4}}"
+                            data-size="960,480" href="#">运行日志</a>
                     </td>
                 </tr>
                 </tbody>

+ 67 - 2
manager/src/main/resources/templates/monitor/machine/index.html

@@ -6,20 +6,85 @@
 <body class="timo-layout-page">
 <div class="layui-card">
     <div class="layui-card-header timo-card-header">
-        <span><i class="fa fa-bars"></i> 机器监控列表</span>
+        <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="layui-row timo-card-screen put-row">
+                <div class="pull-left layui-form-pane">
+                    <div class="layui-inline">
+                        <label class="layui-form-label">日志级别</label>
+                        <div class="layui-input-block timo-search-status">
+                            <select id="getPageByEnv" class="timo-search-select" name="level" onchange="getPageByEnv()"
+                                    mo:dict="LOGLEVEL" mo-selected="${level}"></select>
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </div>
+        <div class="timo-table-wrap">
+            <table class="layui-table timo-table">
+                <thead>
+                <tr>
+                    <th class="sortable" data-field="app">app</th>
+                    <th class="sortable" data-field="host">host</th>
+                    <th class="sortable" data-field="timestamp">timestamp</th>
+                    <th class="sortable" data-field="level">level</th>
+                    <th class="sortable" data-field="thread">thread</th>
+                    <th class="sortable" data-field="logger">logger</th>
+                    <th class="sortable" data-field="message">message</th>
+                </tr>
+                </thead>
+                <tbody>
+                <tr th:each="item:${list}">
+                    <td th:text="${item.app}">app</td>
+                    <td th:text="${item.host}">host</td>
+                    <td th:text="${item.timestamp}">timestamp</td>
+                    <td th:text="${item.level}">level</td>
+                    <td th:text="${item.thread}">thread</td>
+                    <td th:text="${item.logger}">logger</td>
+                    <td th:text="${item.message}">message</td>
+                </tr>
+                </tbody>
+            </table>
+        </div>
+    </div>
 </div>
 
 <script th:replace="/common/template :: script"></script>
 <script type="text/javascript" th:src="@{/js/plugins/jquery-2.2.4.min.js}"></script>
+<script type="text/javascript" th:src="@{/js/websocket.js}"></script>
 <script type="text/javascript">
     function getPageByEnv() {
         var selectedOption = $("#getPageByEnv option:selected")
         var param = selectedOption.text()
-        url = '?env=' + param
+        url = '?level=' + param
         window.location.href = window.location.pathname + url;
     }
+
+    /*var client = new WebSocketClient();
+    //执行连接操作
+    client.connect({
+        onError: function (error) {
+            // 连接失败回调
+            console.log('WebSocket 连接错误...')
+        },
+        onConnect: function () {
+            // 连接成功回调
+            console.log('WebSocket 连接成功...')
+        },
+        onClose: function () {
+            // 连接关闭回调
+            console.log('WebSocket 连接关闭...')
+        },
+        onData: function (data) {
+            // 收到数据时回调
+            console.log('接收到 WebSocket 的数据...')
+            const jsonData = JSON.parse(data)
+            console.log(jsonData)
+        }
+    });*/
 </script>
 </body>
 </html>