Browse Source

完成机器监控的主要功能和前端页面

reghao 5 years ago
parent
commit
c66e463f5e
28 changed files with 696 additions and 114 deletions
  1. 2 0
      dmaster/src/main/java/cn/reghao/autodop/dmaster/machine/db/crud/MachineStatusCrudService.java
  2. 41 0
      dmaster/src/main/java/cn/reghao/autodop/dmaster/monitor/controller/JvmMonitorController.java
  3. 80 17
      dmaster/src/main/java/cn/reghao/autodop/dmaster/monitor/controller/MonitorController.java
  4. 49 9
      dmaster/src/main/java/cn/reghao/autodop/dmaster/monitor/controller/MonitorPageController.java
  5. 36 0
      dmaster/src/main/java/cn/reghao/autodop/dmaster/monitor/entity/AppMonitor.java
  6. 9 2
      dmaster/src/main/java/cn/reghao/autodop/dmaster/monitor/entity/MachineMonitor.java
  7. 17 4
      dmaster/src/main/java/cn/reghao/autodop/dmaster/monitor/entity/MonitorJob.java
  8. 12 0
      dmaster/src/main/java/cn/reghao/autodop/dmaster/monitor/repository/AppMonitorRepository.java
  9. 12 0
      dmaster/src/main/java/cn/reghao/autodop/dmaster/monitor/repository/MachineMonitorRepository.java
  10. 27 0
      dmaster/src/main/java/cn/reghao/autodop/dmaster/monitor/service/JvmMonitorService.java
  11. 39 20
      dmaster/src/main/java/cn/reghao/autodop/dmaster/monitor/service/MonitorScheduler.java
  12. 55 10
      dmaster/src/main/java/cn/reghao/autodop/dmaster/monitor/service/MonitorService.java
  13. 4 3
      dmaster/src/main/java/cn/reghao/autodop/dmaster/monitor/service/job/AppHealthCheckJob.java
  14. 19 4
      dmaster/src/main/java/cn/reghao/autodop/dmaster/monitor/service/job/MachineHeartbeatMonitorJob.java
  15. 22 0
      dmaster/src/main/java/cn/reghao/autodop/dmaster/notification/entity/DingAccount.java
  16. 1 0
      dmaster/src/main/java/cn/reghao/autodop/dmaster/notification/entity/NotifyAccount.java
  17. 12 0
      dmaster/src/main/java/cn/reghao/autodop/dmaster/notification/repository/DingAccountRepository.java
  18. 8 33
      dmaster/src/main/java/cn/reghao/autodop/dmaster/notification/service/NotifyService.java
  19. 0 1
      dmaster/src/main/java/cn/reghao/autodop/dmaster/notification/service/notifier/ding/DingMsg.java
  20. 38 1
      dmaster/src/main/java/cn/reghao/autodop/dmaster/notification/service/notifier/ding/DingNotify.java
  21. 1 1
      dmaster/src/main/resources/templates/app/bd/build.html
  22. 83 4
      dmaster/src/main/resources/templates/monitor/app.html
  23. 24 0
      dmaster/src/main/resources/templates/monitor/enablemonitor.html
  24. 13 5
      dmaster/src/main/resources/templates/monitor/machine.html
  25. 39 0
      dmaster/src/main/resources/templates/monitor/machinenotify.html
  26. 24 0
      dmaster/src/test/java/cn/reghao/autodop/dmaster/monitor/service/MonitorServiceTest.java
  27. 13 0
      dmaster/src/test/java/cn/reghao/autodop/dmaster/notification/service/notifier/ding/DingNotifyTest.java
  28. 16 0
      dmaster/src/test/java/cn/reghao/autodop/dmaster/sys/db/MongoQueryTest.java

+ 2 - 0
dmaster/src/main/java/cn/reghao/autodop/dmaster/machine/db/crud/MachineStatusCrudService.java

@@ -7,6 +7,8 @@ import org.springframework.cache.annotation.CacheConfig;
 import org.springframework.stereotype.Service;
 
 /**
+ * TODO 系统启动时将所有数据放入缓存,系统结束前将所有数据持久化到数据库
+ *
  * @author reghao
  * @date 2021-06-15 16:29:18
  */

+ 41 - 0
dmaster/src/main/java/cn/reghao/autodop/dmaster/monitor/controller/JvmMonitorController.java

@@ -0,0 +1,41 @@
+package cn.reghao.autodop.dmaster.monitor.controller;
+
+import cn.reghao.autodop.dmaster.monitor.service.JvmMonitorService;
+import cn.reghao.autodop.dmaster.utils.WebBody;
+import io.swagger.annotations.Api;
+import org.springframework.web.bind.annotation.*;
+
+/**
+ * @author reghao
+ * @date 2019-11-15 08:44:50
+ */
+@Api(tags = "JVM 监控接口")
+@RestController
+@RequestMapping("/api/monitor")
+public class JvmMonitorController {
+    private JvmMonitorService jvmMonitorService;
+
+    public JvmMonitorController(JvmMonitorService jvmMonitorService) {
+        this.jvmMonitorService = jvmMonitorService;
+    }
+
+    @GetMapping("/jvm/info")
+    public String jvmInfo() {
+        return WebBody.success(jvmMonitorService.jvmInfo());
+    }
+
+    @GetMapping("/jvm/stat")
+    public String jvmStat() {
+        return WebBody.success(jvmMonitorService.jvmStat());
+    }
+
+    @GetMapping("/linux/info")
+    public String osInfo() {
+        return WebBody.success(jvmMonitorService.jvmInfo());
+    }
+
+    @GetMapping("/linux/stat")
+    public String osStat() {
+        return WebBody.success();
+    }
+}

+ 80 - 17
dmaster/src/main/java/cn/reghao/autodop/dmaster/monitor/controller/MonitorController.java

@@ -1,41 +1,104 @@
 package cn.reghao.autodop.dmaster.monitor.controller;
 
-import cn.reghao.autodop.dmaster.monitor.service.MonitorService;
+import cn.reghao.autodop.dmaster.monitor.entity.MachineMonitor;
+import cn.reghao.autodop.dmaster.monitor.repository.AppMonitorRepository;
+import cn.reghao.autodop.dmaster.monitor.repository.MachineMonitorRepository;
+import cn.reghao.autodop.dmaster.monitor.service.MonitorScheduler;
+import cn.reghao.autodop.dmaster.notification.entity.NotifyGroup;
 import cn.reghao.autodop.dmaster.utils.WebBody;
 import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import lombok.extern.slf4j.Slf4j;
+import org.quartz.SchedulerException;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
 import org.springframework.web.bind.annotation.*;
 
+import java.util.List;
+
 /**
  * @author reghao
  * @date 2019-11-15 08:44:50
  */
+@Slf4j
 @Api(tags = "监控接口")
 @RestController
 @RequestMapping("/api/monitor")
 public class MonitorController {
-    private MonitorService monitorService;
+    private MonitorScheduler monitorScheduler;
+    private MachineMonitorRepository machineMonitorRepository;
+    private AppMonitorRepository appMonitorRepository;
 
-    public MonitorController(MonitorService monitorService) {
-        this.monitorService = monitorService;
+    public MonitorController(MonitorScheduler monitorScheduler,
+                             MachineMonitorRepository machineMonitorRepository,
+                             AppMonitorRepository appMonitorRepository) {
+        this.monitorScheduler = monitorScheduler;
+        this.machineMonitorRepository = machineMonitorRepository;
+        this.appMonitorRepository = appMonitorRepository;
     }
 
-    @GetMapping("/jvm/info")
-    public String jvmInfo() {
-        return WebBody.success(monitorService.jvmInfo());
-    }
+    @ApiOperation(value = "设置机器状态监控任务")
+    @PostMapping(value = "/machine/job/{id}", produces = MediaType.APPLICATION_JSON_VALUE)
+    public ResponseEntity<String> machineStatusMonitor(@PathVariable("id") MachineMonitor machineMonitor,
+                                                 @RequestParam("cronExp") String cronExp) throws SchedulerException {
+        String jobId = machineMonitor.getJobId();
+        if (jobId == null) {
+            jobId = machineMonitor.getMachineId() + "-heartbeat";
+            machineMonitor.setJobId(jobId);
+        }
+        // TODO 验证 cronExp 是否有效
+        machineMonitor.setCronExp(cronExp);
+        machineMonitor.setEnable(true);
 
-    @GetMapping("/jvm/stat")
-    public String jvmStat() {
-        return WebBody.success(monitorService.jvmStat());
+        // 这两个操作应该在一个事务中
+        monitorScheduler.addMachineStatusMonitorJob(machineMonitor);
+        machineMonitorRepository.save(machineMonitor);
+
+        return ResponseEntity.ok().body(WebBody.success());
     }
 
-    @GetMapping("/linux/info")
-    public String osInfo() {
-        return WebBody.success(monitorService.jvmInfo());
+    @ApiOperation(value = "是否开始监控")
+    @PostMapping(value = "/machine/{status}/{id}", produces = MediaType.APPLICATION_JSON_VALUE)
+    public ResponseEntity<String> disableMachineMonitor(@PathVariable("status") String status,
+                                                        @PathVariable("id") MachineMonitor machineMonitor)
+            throws SchedulerException {
+        Boolean enable = machineMonitor.getEnable();
+        if ("enable".equals(status)) {
+            if (enable) {
+                return ResponseEntity.ok().body(WebBody.successMsg("当前正在监控中"));
+            }
+
+            String jobId = machineMonitor.getJobId();
+            if (jobId == null) {
+                return ResponseEntity.ok().body(WebBody.failWithMsg("请先设置监控任务"));
+            }
+
+            // TODO 应该在一个事务中
+            machineMonitor.setEnable(true);
+            monitorScheduler.resume(machineMonitor.getJobId());
+            machineMonitorRepository.save(machineMonitor);
+
+            return ResponseEntity.ok().body(WebBody.successMsg("监控已开启"));
+        } else {
+            if (!enable) {
+                return ResponseEntity.ok().body(WebBody.successMsg("当前没有监控"));
+            }
+
+            // TODO 应该在一个事务中
+            machineMonitor.setEnable(false);
+            monitorScheduler.pause(machineMonitor.getJobId());
+            machineMonitorRepository.save(machineMonitor);
+
+            return ResponseEntity.ok().body(WebBody.successMsg("监控已停止"));
+        }
     }
 
-    @GetMapping("/linux/stat")
-    public String osStat() {
-        return WebBody.success();
+    @ApiOperation(value = "设置机器状态监控通知")
+    @PostMapping(value = "/machine/notify", produces = MediaType.APPLICATION_JSON_VALUE)
+    public ResponseEntity<String> machineMonitorNotify(@RequestParam("id") MachineMonitor machineMonitor,
+                                                       @RequestParam("groupId") List<NotifyGroup> notifyGroups) {
+        machineMonitor.setNotifyGroups(notifyGroups);
+        machineMonitorRepository.save(machineMonitor);
+        return ResponseEntity.ok().body(WebBody.success());
     }
 }

+ 49 - 9
dmaster/src/main/java/cn/reghao/autodop/dmaster/monitor/controller/MonitorPageController.java

@@ -1,8 +1,11 @@
 package cn.reghao.autodop.dmaster.monitor.controller;
 
-import cn.reghao.autodop.dmaster.machine.db.crud.MachineInfoCrudService;
-import cn.reghao.autodop.dmaster.machine.entity.MachineInfo;
-import cn.reghao.autodop.dmaster.monitor.vo.MachineMonitor;
+import cn.reghao.autodop.dmaster.monitor.entity.AppMonitor;
+import cn.reghao.autodop.dmaster.monitor.entity.MachineMonitor;
+import cn.reghao.autodop.dmaster.monitor.repository.AppMonitorRepository;
+import cn.reghao.autodop.dmaster.monitor.repository.MachineMonitorRepository;
+import cn.reghao.autodop.dmaster.notification.entity.NotifyGroup;
+import cn.reghao.autodop.dmaster.notification.repository.NotifyGroupRepository;
 import cn.reghao.autodop.dmaster.utils.db.PageList;
 import cn.reghao.autodop.dmaster.utils.db.PageSort;
 import io.swagger.annotations.Api;
@@ -13,8 +16,13 @@ import org.springframework.data.domain.PageRequest;
 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.HashSet;
+import java.util.List;
+import java.util.Set;
+
 /**
  * @author reghao
  * @date 2019-08-30 18:49:15
@@ -24,28 +32,60 @@ import org.springframework.web.bind.annotation.RequestMapping;
 @Controller
 @RequestMapping("/monitor")
 public class MonitorPageController {
-    private MachineInfoCrudService infoCrudService;
+    private MachineMonitorRepository machineMonitorRepository;
+    private AppMonitorRepository appMonitorRepository;
+    private NotifyGroupRepository receiverRepository;
 
-    public MonitorPageController(MachineInfoCrudService infoCrudService) {
-        this.infoCrudService = infoCrudService;
+    public MonitorPageController(MachineMonitorRepository machineMonitorRepository,
+                                 AppMonitorRepository appMonitorRepository,
+                                 NotifyGroupRepository receiverRepository) {
+        this.machineMonitorRepository = machineMonitorRepository;
+        this.appMonitorRepository = appMonitorRepository;
+        this.receiverRepository = receiverRepository;
     }
 
     @ApiOperation(value = "机器监控页面")
     @GetMapping("/machine")
     public String machineMonitorPage(Model model) {
         PageRequest pageRequest = PageSort.pageRequest();
-        Page<MachineInfo> page = infoCrudService.selectByPage(pageRequest);
-        Page<MachineMonitor> machineMonitors = page.map(MachineMonitor::new);
-        PageList<MachineMonitor> pageList = PageList.pageList(machineMonitors);
+        Page<MachineMonitor> page = machineMonitorRepository.findAll(pageRequest);
+        PageList<MachineMonitor> pageList = PageList.pageList(page);
 
         model.addAttribute("page", page);
         model.addAttribute("list", pageList.getList());
         return "/monitor/machine";
     }
 
+    @ApiOperation(value = "机器监控通知页面")
+    @GetMapping("/machine/notify/{id}")
+    public String monitorNotifyPage(@PathVariable("id") MachineMonitor machineMonitor, Model model) {
+        Set<NotifyGroup> currentSet = new HashSet<>(machineMonitor.getNotifyGroups());
+        List<NotifyGroup> list = receiverRepository.findAll();
+
+        model.addAttribute("id", machineMonitor.getId());
+        model.addAttribute("currentSet", currentSet);
+        model.addAttribute("list", list);
+        return "/monitor/machinenotify";
+    }
+
+    @ApiOperation(value = "机器监控任务页面")
+    @GetMapping("/machine/job/{id}")
+    public String monitorAddPage(@PathVariable("id") MachineMonitor machineMonitor, Model model) {
+        model.addAttribute("id", machineMonitor.getId());
+        model.addAttribute("jobId", machineMonitor.getJobId());
+        model.addAttribute("cronExp", machineMonitor.getCronExp());
+        return "/monitor/enablemonitor";
+    }
+
     @ApiOperation(value = "应用监控页面")
     @GetMapping("/app")
     public String appMonitorPage(Model model) {
+        PageRequest pageRequest = PageSort.pageRequest();
+        Page<AppMonitor> page = appMonitorRepository.findAll(pageRequest);
+        PageList<AppMonitor> pageList = PageList.pageList(page);
+
+        model.addAttribute("page", page);
+        model.addAttribute("list", pageList.getList());
         return "/monitor/app";
     }
 

+ 36 - 0
dmaster/src/main/java/cn/reghao/autodop/dmaster/monitor/entity/AppMonitor.java

@@ -0,0 +1,36 @@
+package cn.reghao.autodop.dmaster.monitor.entity;
+
+import cn.reghao.autodop.dmaster.app.entity.AppRunning;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.NoArgsConstructor;
+
+import javax.persistence.Entity;
+
+/**
+ * @author reghao
+ * @date 2021-06-23 15:35:17
+ */
+@NoArgsConstructor
+@EqualsAndHashCode(callSuper = false)
+@Data
+@Entity
+public class AppMonitor extends MonitorJob {
+    private String appId;
+    private String appName;
+    private String machineId;
+    private String machineIpv4;
+
+    public AppMonitor(AppRunning appRunning) {
+        this.appId = appRunning.getAppId();
+        this.appName = appRunning.getAppName();
+        this.machineId = appRunning.getMachineId();
+        this.machineIpv4 = appRunning.getMachineIpv4();
+    }
+
+    public AppMonitor update(AppRunning appRunning) {
+        this.appName = appRunning.getAppName();
+        this.machineIpv4 = appRunning.getMachineIpv4();
+        return this;
+    }
+}

+ 9 - 2
dmaster/src/main/java/cn/reghao/autodop/dmaster/monitor/vo/MachineMonitor.java → dmaster/src/main/java/cn/reghao/autodop/dmaster/monitor/entity/MachineMonitor.java

@@ -1,14 +1,21 @@
-package cn.reghao.autodop.dmaster.monitor.vo;
+package cn.reghao.autodop.dmaster.monitor.entity;
 
 import cn.reghao.autodop.dmaster.machine.entity.MachineInfo;
 import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.NoArgsConstructor;
+
+import javax.persistence.Entity;
 
 /**
  * @author reghao
  * @date 2021-06-23 15:35:17
  */
+@NoArgsConstructor
+@EqualsAndHashCode(callSuper = false)
 @Data
-public class MachineMonitor {
+@Entity
+public class MachineMonitor extends MonitorJob {
     private String machineId;
     private String machineIpv4;
 

+ 17 - 4
dmaster/src/main/java/cn/reghao/autodop/dmaster/monitor/entity/MonitorJob.java

@@ -1,11 +1,15 @@
 package cn.reghao.autodop.dmaster.monitor.entity;
 
 import cn.reghao.autodop.dmaster.common.orm.BaseEntity;
+import cn.reghao.autodop.dmaster.notification.entity.NotifyGroup;
 import lombok.Data;
 import lombok.EqualsAndHashCode;
+import org.hibernate.annotations.LazyCollection;
+import org.hibernate.annotations.LazyCollectionOption;
 
-import javax.persistence.MappedSuperclass;
+import javax.persistence.*;
 import javax.validation.constraints.NotBlank;
+import java.util.List;
 
 /**
  * @author reghao
@@ -15,8 +19,17 @@ import javax.validation.constraints.NotBlank;
 @EqualsAndHashCode(callSuper = false)
 @Data
 public class MonitorJob extends BaseEntity<Integer> {
-    @NotBlank(message = "任务 ID 不能为空白字符串")
+    //@NotBlank(message = "任务 ID 不能为空白字符串")
     private String jobId;
-    @NotBlank(message = "CRON 表达式不能为空白字符串")
-    private String cron;
+    //@NotBlank(message = "CRON 表达式不能为空白字符串")
+    private String cronExp;
+    private Boolean enable;
+    @ManyToMany(cascade = CascadeType.REFRESH)
+    @JoinColumn(name = "notify_group_id", foreignKey = @ForeignKey(value = ConstraintMode.NO_CONSTRAINT))
+    @LazyCollection(LazyCollectionOption.FALSE)
+    private List<NotifyGroup> notifyGroups;
+
+    public MonitorJob() {
+        this.enable = false;
+    }
 }

+ 12 - 0
dmaster/src/main/java/cn/reghao/autodop/dmaster/monitor/repository/AppMonitorRepository.java

@@ -0,0 +1,12 @@
+package cn.reghao.autodop.dmaster.monitor.repository;
+
+import cn.reghao.autodop.dmaster.monitor.entity.AppMonitor;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+/**
+ * @author reghao
+ * @date 2021-05-24 15:20:24
+ */
+public interface AppMonitorRepository extends JpaRepository<AppMonitor, Integer> {
+    AppMonitor findByAppIdAndMachineId(String appId, String machineId);
+}

+ 12 - 0
dmaster/src/main/java/cn/reghao/autodop/dmaster/monitor/repository/MachineMonitorRepository.java

@@ -0,0 +1,12 @@
+package cn.reghao.autodop.dmaster.monitor.repository;
+
+import cn.reghao.autodop.dmaster.monitor.entity.MachineMonitor;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+/**
+ * @author reghao
+ * @date 2021-05-24 15:20:24
+ */
+public interface MachineMonitorRepository extends JpaRepository<MachineMonitor, Integer> {
+    MachineMonitor findByMachineId(String machineId);
+}

+ 27 - 0
dmaster/src/main/java/cn/reghao/autodop/dmaster/monitor/service/JvmMonitorService.java

@@ -0,0 +1,27 @@
+package cn.reghao.autodop.dmaster.monitor.service;
+
+import cn.reghao.autodop.common.jvm.JVM;
+import cn.reghao.autodop.common.jvm.pojo.JVMInfo;
+import cn.reghao.autodop.common.jvm.pojo.JVMStat;
+import org.springframework.stereotype.Service;
+
+/**
+ * @author reghao
+ * @date 2020-10-22 17:51:56
+ */
+@Service
+public class JvmMonitorService {
+    private JVM jvm;
+
+    public JvmMonitorService() {
+        this.jvm = new JVM();
+    }
+
+    public JVMInfo jvmInfo() {
+        return jvm.info();
+    }
+
+    public JVMStat jvmStat() {
+        return jvm.stat();
+    }
+}

+ 39 - 20
dmaster/src/main/java/cn/reghao/autodop/dmaster/monitor/service/MonitorScheduler.java

@@ -4,8 +4,10 @@ import cn.reghao.autodop.common.http.DefaultWebRequest;
 import cn.reghao.autodop.common.http.WebRequest;
 import cn.reghao.autodop.dmaster.machine.db.crud.MachineStatusCrudService;
 import cn.reghao.autodop.dmaster.app.repository.AppRunningRepository;
-import cn.reghao.autodop.dmaster.monitor.service.job.AppHealthCheckJob;
-import cn.reghao.autodop.dmaster.monitor.service.job.MachineStatusMonitorJob;
+import cn.reghao.autodop.dmaster.monitor.entity.MachineMonitor;
+import cn.reghao.autodop.dmaster.monitor.repository.MachineMonitorRepository;
+import cn.reghao.autodop.dmaster.monitor.service.job.MachineHeartbeatMonitorJob;
+import cn.reghao.autodop.dmaster.notification.entity.NotifyGroup;
 import cn.reghao.autodop.dmaster.notification.service.NotifyService;
 import lombok.extern.slf4j.Slf4j;
 import org.quartz.*;
@@ -13,6 +15,7 @@ import org.quartz.impl.StdSchedulerFactory;
 import org.springframework.stereotype.Service;
 
 import javax.annotation.PostConstruct;
+import java.util.List;
 
 /**
  * 监控使用的定时任务调度器
@@ -28,46 +31,54 @@ public class MonitorScheduler {
     private AppRunningRepository runningRepository;
     private MachineStatusCrudService statusCrudService;
     private WebRequest webRequest;
+    private MachineMonitorRepository machineMonitorRepository;
 
-    public MonitorScheduler(NotifyService notifyService, AppRunningRepository runningRepository,
-                            MachineStatusCrudService statusCrudService) throws SchedulerException {
+    public MonitorScheduler(NotifyService notifyService,
+                            AppRunningRepository runningRepository,
+                            MachineStatusCrudService statusCrudService,
+                            MachineMonitorRepository machineMonitorRepository) throws SchedulerException {
         this.scheduler = StdSchedulerFactory.getDefaultScheduler();
         this.notifyService = notifyService;
         this.runningRepository = runningRepository;
         this.statusCrudService = statusCrudService;
         this.webRequest = new DefaultWebRequest();
+        this.machineMonitorRepository = machineMonitorRepository;
     }
 
+    /**
+     * @param
+     * @return
+     * @date 2021-06-24 上午10:22
+     */
     @PostConstruct
     public void startScheduler() throws SchedulerException {
-        scheduler.start();
+        for (MachineMonitor machineMonitor : machineMonitorRepository.findAll()) {
+            addMachineStatusMonitorJob(machineMonitor);
+        }
+        // TODO 系统启动时启用所有存在的任务
+        //scheduler.start();
     }
 
-    public void pause() throws SchedulerException {
+    public void pauseScheduler() throws SchedulerException {
         if (!scheduler.isShutdown()) {
             scheduler.pauseAll();
         }
     }
 
-    public void addAppHealthCheckJob(String jobId, String cronExp) throws SchedulerException {
-        JobDataMap jobDataMap = new JobDataMap();
-        jobDataMap.put("notifyService", notifyService);
-        jobDataMap.put("appId", "");
-        jobDataMap.put("machineId", "");
-        jobDataMap.put("webRequest", webRequest);
-        jobDataMap.put("runningRepository", runningRepository);
-        add(AppHealthCheckJob.class, jobId, cronExp, jobDataMap);
-    }
+    public void addMachineStatusMonitorJob(MachineMonitor machineMonitor) throws SchedulerException {
+        String jobId = machineMonitor.getJobId();
+        String cronExp = machineMonitor.getCronExp();
+        List<NotifyGroup> notifyGroups = machineMonitor.getNotifyGroups();
 
-    public void addMachineStatusMonitorJob(String jobId, String cronExp) throws SchedulerException {
         JobDataMap jobDataMap = new JobDataMap();
         jobDataMap.put("notifyService", notifyService);
-        jobDataMap.put("machineId", "");
+        jobDataMap.put("notifyGroups", notifyGroups);
+        jobDataMap.put("machineId", machineMonitor.getMachineId());
         jobDataMap.put("statusCrudService", statusCrudService);
-        add(MachineStatusMonitorJob.class, jobId, cronExp, jobDataMap);
+        addAndStart(MachineHeartbeatMonitorJob.class, jobId, cronExp, jobDataMap);
     }
 
-    private void add(Class<? extends Job> clazz, String jobId, String cronExp, JobDataMap jobDataMap)
+    private void addAndStart(Class<? extends Job> clazz, String jobId, String cronExp, JobDataMap jobDataMap)
             throws SchedulerException {
         JobDetail jobDetail = JobBuilder.newJob(clazz)
                 .withIdentity(jobId)
@@ -86,6 +97,14 @@ public class MonitorScheduler {
         }
     }
 
-    public void remove() {
+    public void pause(String jobId) throws SchedulerException {
+        scheduler.pauseJob(JobKey.jobKey(jobId));
+    }
+
+    public void resume(String jobId) throws SchedulerException {
+        scheduler.resumeJob(JobKey.jobKey(jobId));
+    }
+
+    public void remove(String jobId) {
     }
 }

+ 55 - 10
dmaster/src/main/java/cn/reghao/autodop/dmaster/monitor/service/MonitorService.java

@@ -1,27 +1,72 @@
 package cn.reghao.autodop.dmaster.monitor.service;
 
-import cn.reghao.autodop.common.jvm.JVM;
-import cn.reghao.autodop.common.jvm.pojo.JVMInfo;
-import cn.reghao.autodop.common.jvm.pojo.JVMStat;
+import cn.reghao.autodop.dmaster.app.entity.AppRunning;
+import cn.reghao.autodop.dmaster.app.repository.AppRunningRepository;
+import cn.reghao.autodop.dmaster.machine.entity.MachineInfo;
+import cn.reghao.autodop.dmaster.machine.repository.MachineInfoRepository;
+import cn.reghao.autodop.dmaster.monitor.entity.AppMonitor;
+import cn.reghao.autodop.dmaster.monitor.entity.MachineMonitor;
+import cn.reghao.autodop.dmaster.monitor.repository.AppMonitorRepository;
+import cn.reghao.autodop.dmaster.monitor.repository.MachineMonitorRepository;
 import org.springframework.stereotype.Service;
 
+import java.util.List;
+import java.util.stream.Collectors;
+
 /**
  * @author reghao
  * @date 2020-10-22 17:51:56
  */
 @Service
 public class MonitorService {
-    private JVM jvm;
+    private MachineInfoRepository infoRepository;
+    private MachineMonitorRepository machineMonitorRepository;
+    private AppRunningRepository runningRepository;
+    private AppMonitorRepository appMonitorRepository;
+
+    public MonitorService(MachineInfoRepository infoRepository, MachineMonitorRepository machineMonitorRepository,
+                          AppRunningRepository runningRepository, AppMonitorRepository appMonitorRepository) {
+        this.infoRepository = infoRepository;
+        this.machineMonitorRepository = machineMonitorRepository;
+        this.runningRepository = runningRepository;
+        this.appMonitorRepository = appMonitorRepository;
+    }
 
-    public MonitorService() {
-        this.jvm = new JVM();
+    public void refresh() {
+        refreshMachines();
+        refreshApps();
     }
 
-    public JVMInfo jvmInfo() {
-        return jvm.info();
+    private void refreshMachines() {
+        List<MachineInfo> machineInfos = infoRepository.findAll();
+        List<MachineMonitor> machineMonitors = machineInfos.stream()
+                .map(machineInfo -> {
+                    String machineId = machineInfo.getMachineId();
+                    MachineMonitor machineMonitor = machineMonitorRepository.findByMachineId(machineId);
+                    if (machineMonitor == null) {
+                        machineMonitor = new MachineMonitor(machineInfo);
+                    }
+                    return machineMonitor;
+                })
+                .collect(Collectors.toList());
+        machineMonitorRepository.saveAll(machineMonitors);
     }
 
-    public JVMStat jvmStat() {
-        return jvm.stat();
+    private void refreshApps() {
+        List<AppRunning> appRunnings = runningRepository.findAll();
+        List<AppMonitor> appMonitors = appRunnings.stream()
+                .map(appRunning -> {
+                    String appId = appRunning.getAppId();
+                    String machineId = appRunning.getMachineId();
+                    AppMonitor appMonitor = appMonitorRepository.findByAppIdAndMachineId(appId, machineId);
+                    if (appMonitor == null) {
+                        appMonitor = new AppMonitor(appRunning);
+                    } else {
+                        appMonitor.update(appRunning);
+                    }
+                    return appMonitor;
+                })
+                .collect(Collectors.toList());
+        appMonitorRepository.saveAll(appMonitors);
     }
 }

+ 4 - 3
dmaster/src/main/java/cn/reghao/autodop/dmaster/monitor/service/job/AppHealthCheckJob.java

@@ -12,6 +12,7 @@ import org.quartz.JobDetail;
 import org.quartz.JobExecutionContext;
 
 import java.time.LocalDateTime;
+import java.util.List;
 
 /**
  * 应用健康检查任务
@@ -28,13 +29,13 @@ public class AppHealthCheckJob implements Job {
         JobDataMap jobDataMap = jobDetail.getJobDataMap();
 
         NotifyService notifyService = (NotifyService) jobDataMap.get("notifyService");
-        NotifyGroup notifyGroup = (NotifyGroup) jobDataMap.get("notifyGroup");
+        List<NotifyGroup> notifyGroups = (List<NotifyGroup>) jobDataMap.get("notifyGroups");
 
         WebRequest webRequest = (WebRequest) jobDataMap.get("webRequest");
         AppRunningRepository runningRepository = (AppRunningRepository) jobDataMap.get("runningRepository");
-
         String appId = jobDataMap.getString("appId");
         String machineId = jobDataMap.getString("machineId");
+
         AppRunning appRunning = runningRepository.findByAppIdAndMachineId(appId, machineId);
         String machineIpv4 = appRunning.getMachineIpv4();
         String healthCheck = appRunning.getHealthCheck();
@@ -52,7 +53,7 @@ public class AppHealthCheckJob implements Job {
             int statusCode = webRequest.head(url);
             if (statusCode != 200) {
                 String msg = String.format("%s 机器上的 %s 应用健康检查失败", machineIpv4, appId);
-                notifyService.notify(notifyGroup, msg);
+                notifyGroups.forEach(notifyGroup -> notifyService.notify(notifyGroup, msg));
             } else {
                 appRunning.setLastCheck(LocalDateTime.now());
                 runningRepository.save(appRunning);

+ 19 - 4
dmaster/src/main/java/cn/reghao/autodop/dmaster/monitor/service/job/MachineStatusMonitorJob.java → dmaster/src/main/java/cn/reghao/autodop/dmaster/monitor/service/job/MachineHeartbeatMonitorJob.java

@@ -3,34 +3,49 @@ package cn.reghao.autodop.dmaster.monitor.service.job;
 import cn.reghao.autodop.common.utils.DateTimeConverter;
 import cn.reghao.autodop.dmaster.machine.db.crud.MachineStatusCrudService;
 import cn.reghao.autodop.dmaster.machine.entity.MachineStatus;
+import cn.reghao.autodop.dmaster.notification.entity.NotifyGroup;
+import cn.reghao.autodop.dmaster.notification.service.NotifyService;
+import cn.reghao.autodop.dmaster.notification.service.notifier.ding.DingMsg;
 import lombok.extern.slf4j.Slf4j;
 import org.quartz.Job;
 import org.quartz.JobDataMap;
 import org.quartz.JobDetail;
 import org.quartz.JobExecutionContext;
 
+import java.time.LocalDateTime;
+import java.util.List;
+
 /**
- * 机器状态监控任务
+ * 机器心跳监控任务
  *
  * @author reghao
  * @date 2021-06-22 19:04:10
  */
 @Slf4j
-public class MachineStatusMonitorJob implements Job {
+public class MachineHeartbeatMonitorJob implements Job {
     @Override
     public void execute(JobExecutionContext context) {
         JobDetail jobDetail = context.getJobDetail();
         JobDataMap jobDataMap = jobDetail.getJobDataMap();
 
+        NotifyService notifyService = (NotifyService) jobDataMap.get("notifyService");
+        List<NotifyGroup> notifyGroups = (List<NotifyGroup>) jobDataMap.get("notifyGroups");
         MachineStatusCrudService statusCrudService = (MachineStatusCrudService) jobDataMap.get("statusCrudService");
-
         String machineId = jobDataMap.getString("machineId");
+
         MachineStatus machineStatus = statusCrudService.selectByUniqueKey(machineId);
         long lastCheck = DateTimeConverter.msTimestamp(machineStatus.getLastCheck());
         long now = System.currentTimeMillis();
+        machineStatus.setLastCheck(LocalDateTime.now());
+        statusCrudService.insertOrUpdate(machineStatus);
+
         long result = now - lastCheck;
         if (result > 10_000) {
-            log.info("距离上次接收到心跳消息已过去 {}s", result/1000);
+            // TODO 检测 IP 是否能 ping 通
+            // TODO 检测 MQTT 服务器是否正常连接
+            String msg = String.format("距离上次接收到 %s 的心跳消息已过去 %ss", machineStatus.getMachineIpv4(), result/1000);
+            DingMsg dingMsg = new DingMsg("监控报警",msg);
+            notifyGroups.forEach(notifyGroup -> notifyService.notify(notifyGroup, dingMsg));
         }
     }
 }

+ 22 - 0
dmaster/src/main/java/cn/reghao/autodop/dmaster/notification/entity/DingAccount.java

@@ -0,0 +1,22 @@
+package cn.reghao.autodop.dmaster.notification.entity;
+
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.NoArgsConstructor;
+
+import javax.persistence.Entity;
+
+/**
+ * 钉钉机器人
+ *
+ * @author reghao
+ * @date 2021-06-23 10:29:18
+ */
+@NoArgsConstructor
+@EqualsAndHashCode(callSuper = false)
+@Data
+@Entity
+public class DingAccount extends NotifyAccount {
+    // 若没有启用签名,password 字段默认为 none
+    private String personal;
+}

+ 1 - 0
dmaster/src/main/java/cn/reghao/autodop/dmaster/notification/entity/NotifyAccount.java

@@ -21,6 +21,7 @@ public class NotifyAccount extends BaseEntity<Integer> {
     private String username;
     @NotBlank(message = "用户密码不能为空白字符串")
     private String password;
+    @Deprecated
     private Boolean isDefault;
 
     public NotifyAccount() {

+ 12 - 0
dmaster/src/main/java/cn/reghao/autodop/dmaster/notification/repository/DingAccountRepository.java

@@ -0,0 +1,12 @@
+package cn.reghao.autodop.dmaster.notification.repository;
+
+import cn.reghao.autodop.dmaster.notification.entity.DingAccount;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+/**
+ * @author reghao
+ * @date 2021-05-24 15:20:24
+ */
+public interface DingAccountRepository extends JpaRepository<DingAccount, Integer> {
+    DingAccount findDingAccountByNotifyAccountId(String notifyAccountId);
+}

+ 8 - 33
dmaster/src/main/java/cn/reghao/autodop/dmaster/notification/service/NotifyService.java

@@ -2,11 +2,9 @@ package cn.reghao.autodop.dmaster.notification.service;
 
 import cn.reghao.autodop.common.http.DefaultWebRequest;
 import cn.reghao.autodop.common.http.WebRequest;
-import cn.reghao.autodop.dmaster.notification.entity.EmailAccount;
-import cn.reghao.autodop.dmaster.notification.entity.NotifyType;
-import cn.reghao.autodop.dmaster.notification.entity.NotifyGroup;
+import cn.reghao.autodop.dmaster.notification.entity.*;
 import cn.reghao.autodop.dmaster.common.thread.ThreadPoolWrapper;
-import cn.reghao.autodop.dmaster.notification.entity.SmsAccount;
+import cn.reghao.autodop.dmaster.notification.repository.DingAccountRepository;
 import cn.reghao.autodop.dmaster.notification.repository.EmailAccountRepository;
 import cn.reghao.autodop.dmaster.notification.repository.NotifyGroupRepository;
 import cn.reghao.autodop.dmaster.notification.repository.SmsAccountRepository;
@@ -38,14 +36,17 @@ public class NotifyService {
     private Map<NotifyGroup, Notify> notifierMap = new HashMap<>();
     private WebRequest webRequest;
     private NotifyGroupRepository groupRepository;
+    private DingAccountRepository dingRepository;
     private EmailAccountRepository emailRepository;
     private SmsAccountRepository smsRepository;
 
     public NotifyService(NotifyGroupRepository groupRepository,
+                         DingAccountRepository dingRepository,
                          EmailAccountRepository emailRepository,
                          SmsAccountRepository smsRepository) {
         this.webRequest = new DefaultWebRequest();
         this.groupRepository = groupRepository;
+        this.dingRepository = dingRepository;
         this.emailRepository = emailRepository;
         this.smsRepository = smsRepository;
     }
@@ -74,7 +75,8 @@ public class NotifyService {
         String notifyAccountId = notifyGroup.getNotifyAccountId();
         switch (NotifyType.valueOf(notifyType)) {
             case ding:
-                notifierMap.put(notifyGroup, new DingNotify(webRequest));
+                DingAccount dingAccount = dingRepository.findDingAccountByNotifyAccountId(notifyAccountId);
+                notifierMap.put(notifyGroup, new DingNotify(webRequest, dingAccount));
                 break;
             case email:
                 EmailAccount emailAccount = emailRepository.findEmailAccountByNotifyAccountId(notifyAccountId);
@@ -109,6 +111,7 @@ public class NotifyService {
                 }
                 notifyGroup.getReceivers()
                         .forEach(receiver -> threadPool.execute(new NotifyTask(notify, receiver, msg)));
+                break;
             case email:
                 if (!(msg instanceof EmailMsg)) {
                     log.error("{} 消息格式不正确, 不是 EmailMsg...", msg);
@@ -128,34 +131,6 @@ public class NotifyService {
         }
     }
 
-    /*public void notify(NotifyGroup notifyGroup, String msg) {
-        Notify notify = notifierMap.get(notifyGroup);
-        if (notify == null) {
-            log.error("类型为 {}, 账户为 {} 的通知发送器不存在...",
-                    notifyGroup.getNotifyType(), notifyGroup.getNotifyAccountId());
-            return;
-        }
-
-        String notifyType = notifyGroup.getNotifyType();
-        switch (NotifyType.valueOf(notifyType)) {
-            case ding:
-                DingMsg dingMsg = new DingMsg(msg);
-                notifyGroup.getReceivers()
-                        .forEach(receiver -> threadPool.execute(new NotifyTask(notify, receiver, dingMsg)));
-            case email:
-                EmailMsg emailMsg = new EmailMsg(notifyGroup.getGroupId(), msg);
-                notifyGroup.getReceivers()
-                        .forEach(receiver -> threadPool.execute(new NotifyTask(notify, receiver, emailMsg)));
-                break;
-            case sms:
-                notifyGroup.getReceivers()
-                        .forEach(receiver -> threadPool.execute(new NotifyTask(notify, receiver, msg)));
-                break;
-            default:
-                log.error("通知类型不存在...");
-        }
-    }*/
-
     /**
      * TODO 添加通知日志
      *

+ 0 - 1
dmaster/src/main/java/cn/reghao/autodop/dmaster/notification/service/notifier/ding/DingMsg.java

@@ -28,7 +28,6 @@ public class DingMsg {
     public DingMsg(Object content) {
         this.text = new DingMsg.Text(content);
         this.msgtype = MsgType.text.name();
-        this.msgtype = MsgType.markdown.name();
         this.at = new DingMsg.At(true);
     }
 

+ 38 - 1
dmaster/src/main/java/cn/reghao/autodop/dmaster/notification/service/notifier/ding/DingNotify.java

@@ -3,8 +3,17 @@ package cn.reghao.autodop.dmaster.notification.service.notifier.ding;
 import cn.reghao.autodop.common.http.WebRequest;
 import cn.reghao.autodop.common.http.WebResponse;
 import cn.reghao.autodop.common.utils.serializer.JsonConverter;
+import cn.reghao.autodop.dmaster.notification.entity.DingAccount;
 import cn.reghao.autodop.dmaster.notification.service.notifier.Notify;
 
+import javax.crypto.Mac;
+import javax.crypto.spec.SecretKeySpec;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.util.Base64;
+
 /**
  * 钉钉通知
  *
@@ -13,16 +22,44 @@ import cn.reghao.autodop.dmaster.notification.service.notifier.Notify;
  */
 public class DingNotify implements Notify<DingMsg> {
     private WebRequest webRequest;
+    private DingAccount dingAccount;
 
-    public DingNotify(WebRequest webRequest) {
+    public DingNotify(WebRequest webRequest, DingAccount dingAccount) {
         this.webRequest = webRequest;
+        this.dingAccount = dingAccount;
     }
 
     @Override
     public void send(String receiver, DingMsg msg) throws Exception {
+        if (!"none".equals(dingAccount.getPassword())) {
+            receiver = getReceiver(receiver, dingAccount.getPassword());
+        }
+
         WebResponse webResponse = webRequest.postJson(receiver, JsonConverter.objectToJson(msg));
         if (webResponse.getStatusCode() != 200) {
             throw new Exception("通知发送失败, " + webResponse.getBody());
         }
     }
+
+    private String getReceiver(String receiver, String secret) throws InvalidKeyException, NoSuchAlgorithmException {
+        long timestamp = System.currentTimeMillis();
+        //String secret = "SEC4d7e0c126147b3679c4d15e47dc26311ca053bd47b21f4025832a6e246a93ec4";
+        String sign = calcSign(timestamp, secret);
+        return receiver + String.format("&timestamp=%s&sign=%s", timestamp, sign);
+    }
+
+    /**
+     * 钉钉机器人计算签名
+     *
+     * @param
+     * @return
+     * @date 2021-06-24 下午3:47
+     */
+    private String calcSign(long timestamp, String secret) throws NoSuchAlgorithmException, InvalidKeyException {
+        String stringToSign = timestamp + System.lineSeparator() + secret;
+        Mac mac = Mac.getInstance("HmacSHA256");
+        mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
+        byte[] signData = mac.doFinal(stringToSign.getBytes(StandardCharsets.UTF_8));
+        return URLEncoder.encode(new String(Base64.getEncoder().encode(signData)), StandardCharsets.UTF_8);
+    }
 }

+ 1 - 1
dmaster/src/main/resources/templates/app/bd/build.html

@@ -82,7 +82,7 @@
                     <td>
                         <a class="ajax-post" th:href="@{'/api/app/bd/update?appId='+${item.appId}}">更新</a>
                         <a class="ajax-post" th:href="@{'/api/app/bd/build?appId='+${item.appId}}">构建</a>
-                        <a class="open-popup" data-title="应用部署" th:attr="data-url=@{'/app/deploy/' + ${item.appId}}"
+                        <a class="open-popup" data-title="应用部署" th:attr="data-url=@{'/app/deploy/'+${item.appId}}"
                            data-size="1000,500" href="#">部署</a>
                     </td>
                 </tr>

+ 83 - 4
dmaster/src/main/resources/templates/monitor/app.html

@@ -1,11 +1,90 @@
 <!DOCTYPE html>
-<html xmlns:th="http://www.thymeleaf.org">
+<html xmlns:th="http://www.thymeleaf.org"
+      xmlns:mo="https://gitee.com/aun/Timo">
 <head th:replace="/common/template :: header(~{::title},~{::link},~{::style})"></head>
 
-<body>
-    <div class="timo-detail-page">
-        <div class="timo-detail-title">应用监控</div>
+<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>
+        <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="env" onchange="getPageByEnv()"
+                                    mo:dict="ENVIRONMENT" mo-selected="${env}"></select>
+                        </div>
+                    </div>
+                    <div class="layui-inline timo-search-box">
+                        <label class="layui-form-label">应用名</label>
+                        <div class="layui-input-block">
+                            <input type="text" name="appName" th:value="${param.appName}" placeholder="请输入应用名"
+                                   autocomplete="off" class="layui-input">
+                        </div>
+                    </div>
+                    <div class="layui-inline">
+                        <button class="layui-btn timo-search-btn">
+                            <i class="fa fa-search"></i>
+                        </button>
+                    </div>
+                </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="appName">应用名</th>
+                    <th class="sortable" data-field="machineIpv4">机器地址</th>
+                    <th class="sortable" data-field="enable">正在监控?</th>
+                    <th class="sortable" data-field="jobId">任务 ID</th>
+                    <th class="sortable" data-field="cronExp">CRON 表达式</th>
+                    <th>操作</th>
+                </tr>
+                </thead>
+                <tbody>
+                <tr th:each="item:${list}">
+                    <td><label class="timo-checkbox"><input type="checkbox" th:value="${item.appId}">
+                        <i class="layui-icon layui-icon-ok"></i></label></td>
+                    <td th:text="${item.appName}">应用名</td>
+                    <td th:text="${item.machineIpv4}">机器地址</td>
+                    <td th:text="${item.enable}">正在监控?</td>
+                    <td th:text="${item.jobId}">任务 ID</td>
+                    <td th:text="${item.cronExp}">CRON 表达式</td>
+                    <td>
+                        <a class="open-popup" data-title="添加/修改监控任务" th:attr="data-url=@{'/monitor/machine/job/'+${item.id}}"
+                           data-size="600,400" href="#">设置监控任务</a>
+                        <a class="ajax-post" th:href="@{'/api/monitor/app/enable/'+${item.id}}">开启监控</a>
+                        <a class="ajax-post" th:href="@{'/api/monitor/app/disable/'+${item.id}}">停止监控</a>
+                        <a class="open-popup" data-title="设置通知组" th:attr="data-url=@{'/monitor/app/notify/'+${item.id}}"
+                           data-size="640,480" href="#">设置通知</a>
+                    </td>
+                </tr>
+                </tbody>
+            </table>
+        </div>
+        <div th:replace="/common/fragment :: page"></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">
+    function getPageByEnv() {
+        var selectedOption = $("#getPageByEnv option:selected")
+        var param = selectedOption.text()
+        url = '?env=' + param
+        window.location.href = window.location.pathname + url;
+    }
+</script>
 </body>
 </html>

+ 24 - 0
dmaster/src/main/resources/templates/monitor/enablemonitor.html

@@ -0,0 +1,24 @@
+<!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/monitor/machine/job/'+${id}}">
+        <!--<form th:action="@{/api/monitor/machine/job}">-->
+            <input type="hidden" name="id" th:value="${id}"/>
+            <div class="layui-form-item">
+                <label class="layui-form-label required">CRON 表达式</label>
+                <div class="layui-input-inline">
+                    <input class="layui-input" type="text" name="cronExp" placeholder="请输入 CRON 表达式" required th:value="${cronExp}">
+                </div>
+            </div>
+            <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>
+</body>
+</html>

+ 13 - 5
dmaster/src/main/resources/templates/monitor/machine.html

@@ -23,7 +23,7 @@
                     <div class="layui-inline timo-search-box">
                         <label class="layui-form-label">机器地址</label>
                         <div class="layui-input-block">
-                            <input type="text" name="machineIpv4" th:value="${param.machineIpv4}" placeholder="请输入机器地址"
+                            <input type="text" name="appName" th:value="${param.machineIpv4}" placeholder="请输入机器地址"
                                    autocomplete="off" class="layui-input">
                         </div>
                     </div>
@@ -43,8 +43,10 @@
                         <label class="timo-checkbox"><input type="checkbox">
                             <i class="layui-icon layui-icon-ok"></i></label>
                     </th>
-                    <th class="sortable" data-field="machineId">机器 ID</th>
                     <th class="sortable" data-field="machineIpv4">机器地址</th>
+                    <th class="sortable" data-field="enable">正在监控?</th>
+                    <th class="sortable" data-field="jobId">任务 ID</th>
+                    <th class="sortable" data-field="cronExp">CRON 表达式</th>
                     <th>操作</th>
                 </tr>
                 </thead>
@@ -52,11 +54,17 @@
                 <tr th:each="item:${list}">
                     <td><label class="timo-checkbox"><input type="checkbox" th:value="${item.machineId}">
                         <i class="layui-icon layui-icon-ok"></i></label></td>
-                    <td th:text="${item.machineId}">机器 ID</td>
                     <td th:text="${item.machineIpv4}">机器地址</td>
+                    <td th:text="${item.enable}">正在监控?</td>
+                    <td th:text="${item.jobId}">任务 ID</td>
+                    <td th:text="${item.cronExp}">CRON 表达式</td>
                     <td>
-                        <a class="open-popup" data-title="监控任务设置" th:attr="data-url=@{'/machine/host/status/'+${item.machineId}}"
-                           data-size="1200,600" href="#">启用监控</a>
+                        <a class="open-popup" data-title="添加/修改监控任务" th:attr="data-url=@{'/monitor/machine/job/'+${item.id}}"
+                           data-size="600,400" href="#">设置监控任务</a>
+                        <a class="ajax-post" th:href="@{'/api/monitor/machine/enable/'+${item.id}}">开启监控</a>
+                        <a class="ajax-post" th:href="@{'/api/monitor/machine/disable/'+${item.id}}">停止监控</a>
+                        <a class="open-popup" data-title="设置通知组" th:attr="data-url=@{'/monitor/machine/notify/'+${item.id}}"
+                           data-size="640,480" href="#">设置通知</a>
                     </td>
                 </tr>
                 </tbody>

+ 39 - 0
dmaster/src/main/resources/templates/monitor/machinenotify.html

@@ -0,0 +1,39 @@
+<!DOCTYPE html>
+<html xmlns:th="http://www.thymeleaf.org">
+<head th:replace="/common/template :: header(~{::title},~{::link},~{::style})">
+    <style>
+        .layui-input-block{
+            margin-left: 20px;
+            margin-right: 20px;
+            margin-bottom: 70px;
+        }
+        .timo-compile .timo-finally{
+            position: fixed;
+            bottom: 0;
+            left: 0;
+            right: 0;
+            padding-bottom: 14px;
+            margin-bottom: 0;
+            background-color: #ffffff;
+        }
+    </style>
+</head>
+<body>
+<div class="layui-form timo-compile">
+    <form th:action="@{/api/monitor/machine/notify/}">
+        <input type="hidden" name="id" th:value="${id}"/>
+        <div class="layui-form-item">
+            <div class="layui-input-block">
+                <input th:each="item:${list}" type="checkbox" name="groupId" th:title="${item.groupId}"
+                       th:value="${item.id}" th:checked="${#sets.contains(currentSet, item)}" lay-skin="primary">
+            </div>
+        </div>
+        <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>
+</body>
+</html>

+ 24 - 0
dmaster/src/test/java/cn/reghao/autodop/dmaster/monitor/service/MonitorServiceTest.java

@@ -0,0 +1,24 @@
+package cn.reghao.autodop.dmaster.monitor.service;
+
+import cn.reghao.autodop.dmaster.DmasterApplication;
+import org.junit.jupiter.api.Test;
+import org.junit.runner.RunWith;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.context.ActiveProfiles;
+import org.springframework.test.context.junit4.SpringRunner;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+@ActiveProfiles("dev")
+@SpringBootTest(classes = DmasterApplication.class)
+@RunWith(SpringRunner.class)
+class MonitorServiceTest {
+    @Autowired
+    private MonitorService monitorService;
+
+    @Test
+    void refresh() {
+        monitorService.refresh();
+    }
+}

+ 13 - 0
dmaster/src/test/java/cn/reghao/autodop/dmaster/notification/service/notifier/ding/DingNotifyTest.java

@@ -0,0 +1,13 @@
+package cn.reghao.autodop.dmaster.notification.service.notifier.ding;
+
+import cn.reghao.autodop.common.http.DefaultWebRequest;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class DingNotifyTest {
+
+    @Test
+    void send() throws Exception {
+    }
+}

+ 16 - 0
dmaster/src/test/java/cn/reghao/autodop/dmaster/sys/db/MongoQueryTest.java

@@ -7,6 +7,8 @@ import cn.reghao.autodop.dmaster.app.entity.AppRunning;
 import cn.reghao.autodop.dmaster.app.repository.AppRunningRepository;
 import cn.reghao.autodop.dmaster.app.repository.config.AppOrchestrationRepository;
 import cn.reghao.autodop.dmaster.app.repository.log.BuildLogRepository;
+import cn.reghao.autodop.dmaster.notification.entity.DingAccount;
+import cn.reghao.autodop.dmaster.notification.repository.DingAccountRepository;
 import com.mongodb.client.FindIterable;
 import lombok.extern.slf4j.Slf4j;
 import org.bson.Document;
@@ -37,6 +39,20 @@ class MongoQueryTest {
     @Autowired
     private AppOrchestrationRepository appRepository;
     private WebRequest webRequest = new DefaultWebRequest();
+    @Autowired
+    private DingAccountRepository dingAccountRepository;
+
+    @Test
+    void ding() {
+        String username = "ding-build";
+        String password = "test";
+
+        DingAccount dingAccount = new DingAccount();
+        dingAccount.setNotifyAccountId(username);
+        dingAccount.setUsername(username);
+        dingAccount.setPassword(password);
+        dingAccountRepository.save(dingAccount);
+    }
 
     @Test
     void aggregate() {