如何实现Web应用、网站状态的监控?
 
- 关键词:网站监控,服务器监控,页面性能监控,用户体验监控
 - 本文通过代码分析、网站应用介绍网站状态监控的方式
 - 下文主要分为网站应用、技术实现两部分
 
 
一、网站应用
 
- 现在网络上已经存在一些Web网站监控的服务,虽然功能五花八门,但限制较大,需付费使用
 - 本文介绍的技术运行网站见下方地址,不会关闭,可以直接使用
 - 一个朴实无华且免费的WEB网站监控工具
 - 先看下效果
 
 
 
1. 打开网站
 
 
https://www.xujian.tech/monitor
 
2. 微信扫码登录
 
- 这里通过微信扫码取得小程序openid,利用openid标记用户,不涉及隐私
 
 - 扫码完成后会自动跳转到系统
 
 
3. 进入监控表
 
- 进入系统后,选中左侧菜单进入监控表页面
 
 
 
4. 添加监控器
 
- 监控器支持POST、GET两种请求方式
 - GET请求时,如有参数,请直接放置在地址中
 - POST请求时,如有参数,请在表单中填写JSON键值对对象
 - Header如果有需要,也可按JSON对象方式填写
 - 仅需如下三步,即可完成设置
 - 提交后,点击刷新即可在页面上看到监控器记录(此时还未执行)
 
 
 
5. 说明和操作
 
5.1 关于成功率
 
 
5.2 关于监控频率
 
- 每次执行完成计算下一次执行时间,默认30分钟一次(免费用户暂不支持自定义频率)
 - 计时器每5分钟执行一次,发现监控器执行时间小于当前时间的,就执行请求
 - 所以监控频率并非严格按照30分钟一次
 
 
5.2 相关操作
 
- 新增/编辑监控器后,可以点击“立即执行”进行一次请求,观察设置是否正确
 - 需要修改时,可点击“编辑”按钮对监控器内容进行修改
 - 点击运行记录,可查看近期运行的情况
 - 运行记录中,点击结果复制,可以复制运行的结果(当返回内容大于512b的时候,只存储前512个内容)
 - 需要邮件通知的用户,可点击右上角头像设置邮箱,在系统异常的时候,会通过邮件进行提示,邮件内容如下:
 
 
 
6. 功能拓展
 
- 如果有更多建议、合作,请在本文下方留言
 - 或按网站提示添加作者
 
 
二、技术实现
 
1. 技术栈
 
- 实现一个监控器需前端、后端、数据库、缓存等技术
 - 本站主要应用了以下技术:
 
 
| 序号 | 技术 | 所属端 | 
|---|
| 1 | VUE | 前端 | 
| 2 | Vue Element Admin | 前端 | 
| 3 | Java | 后端 | 
| 4 | MySQL 数据库 | 后端 | 
| 5 | Redis缓存 | 后端 | 
| 6 | Nginx | 运维 | 
| 7 | MyBaits-plus | 后端 | 
 
2. 核心代码
 
- 实现web应用监控的核心是定期按规则进行请求,并将结果记录,遇到错误时发送邮件提醒
 - 本文以在Spring Boot中实现为例,除Spring Boot基础依赖外,还需添加如下依赖
 
 
	
	<dependency>
	    <groupId>org.springframework.boot</groupId>
	    <artifactId>spring-boot-starter-mail</artifactId>
	</dependency>
	
	<dependency>
	    <groupId>cn.hutool</groupId>
	    <artifactId>hutool-all</artifactId>
	    <version>5.8.20</version>
	</dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>1.18.0</version>
    </dependency>
 
2.1 监控器实体
 
- 记录监控器基本属性、执行时间、统计结果等
 - 下方代码含实体和下次执行时间计算方法
 
 
@Data
@Builder
@TableName("m_monitor")
public class MMonitor {
    @JsonFormat(shape = JsonFormat.Shape.STRING)
    private Long id;
    
    private String name;
    
    private Date createdAt;
    
    private Date nextRunAt;
    
    private Date lastRunAt;
    
    private Integer timerType;
    
    private Integer isDeleted;
    
    private Integer timerLength;
    
    private Integer status;
    
    private String openid;
    
    private String toUrl;
    
    private String toMethod;
    
    private String toParams;
    
    private String toHeaders;
    
    private String toResult;
    
    private Integer toResultCode;
    
    private Integer toBodyType;
    
    private Integer runStatus;
    
    private String runResult;
    
    private Integer countSucceed;
    
    private Integer countAll;
    
    private static final int MIN_MINUTE_LENGTH = 30;
    
    public void calNextRunAt(){
        if(this.lastRunAt == null){
            this.lastRunAt = new Date();
        }
        timerType = timerType == null ? 1 : timerType;
        if(this.timerLength == null || this.timerLength < 1){
            this.timerLength = 30;
        }
        int addMinute = 0;
        switch (timerType){
            case 1:
                addMinute = timerLength;
                break;
            case 2:
                addMinute = 60 * timerLength;
                break;
            case 3:
                addMinute = 60 * 24 * timerLength;
                break;
        }
        addMinute = Math.max(addMinute,MIN_MINUTE_LENGTH);
        this.nextRunAt = new Date(System.currentTimeMillis() + 1000L * 60 * addMinute);
    }
}
 
2.2 计时器
 
- 利用Spring Boot的Scheduled定时器实现
 
 
@Component
@Slf4j
public class MonitorTimerTask {
    @Resource
    MMonitorMapper monitorMapper;
    @Autowired
    MonitorService monitorService;
    @Scheduled(cron="0 0/5 * * * *")
    public void exec(){
        List<MMonitor> monitorList = monitorMapper.selectList(
                new LambdaQueryWrapper<MMonitor>()
                        .isNotNull(MMonitor::getNextRunAt)
                        .lt(MMonitor::getNextRunAt,DateUtil.formatDateTime(new Date()))
                        .eq(MMonitor::getStatus,1)
                        .eq(MMonitor::getIsDeleted,0)
                );
        log.info(String.format("符合执行条件的监控器有%d个", monitorList.size()));
        for (MMonitor mMonitor : monitorList) {
            monitorService.run(mMonitor);
        }
    }
}
 
- 定时器不生效?记得在SpringBootApplication上添加注解:@EnableScheduling
 
 
2.3 按规则进行请求
 
- 即按监控器的toXX字段配置的内容填充请求参数,进行请求!
 - 本段不多说,直接上代码
 
 
    
    @Override
    public void run(MMonitor monitor) {
        long timestamp = System.currentTimeMillis();
        if(monitor.getIsDeleted() != null && monitor.getIsDeleted() == 1){
            return;
        }
        Date date = new Date();
        boolean isSucceed = false;
        String resultMsg = "成功";
        String requestResult = "";
        int code = 0;
        try{
            HttpResponse httpResponse = null;
            if(monitor.getToMethod() != null && monitor.getToMethod().equalsIgnoreCase("GET")){
                HttpRequest httpRequest =  HttpRequest.get(monitor.getToUrl());
                setHeaders(httpRequest,monitor);
                httpResponse = httpRequest.execute(false);
            }else{
                HttpRequest httpRequest =  HttpRequest.post(monitor.getToUrl());
                setHeaders(httpRequest,monitor);
                setPostParams(httpRequest,monitor);
                httpResponse = httpRequest.execute(false);
            }
            code = httpResponse.getStatus();
            requestResult = httpResponse.body();
            if(monitor.getToResultCode() == 200){
                isSucceed = code == 200;
                if(!isSucceed){
                    throw new Exception("返回结果HTTP CODE不为200");
                }
            }else {
                isSucceed = requestResult.contains(monitor.getToResult());
                if(!isSucceed){
                    throw new Exception("返回结果缺少包含内容");
                }
            }
        }catch (Exception e){
            isSucceed = false;
            resultMsg = e.getMessage();
        }
        if(isSucceed){
            monitor.setCountSucceed(monitor.getCountSucceed() + 1);
        }
        monitor.setCountAll(monitor.getCountAll() + 1);
        monitor.setRunStatus(isSucceed ? 1 : 0);
        monitor.setRunResult(resultMsg);
        monitor.calNextRunAt();
        monitor.setLastRunAt(date);
        monitorMapper.updateById(monitor);
        timestamp = System.currentTimeMillis() - timestamp;
        log.info(monitor.getName() + String.format("检查完毕,耗时%dms.", timestamp));
        if(requestResult != null && requestResult.length() > 512){
            requestResult = requestResult.substring(0,511) + "...";
        }
        MRunRecord runRecord = MRunRecord.builder()
                .monitorId(monitor.getId())
                .runCode(code)
                .runResult(requestResult)
                .runAt(date)
                .timeSpent(timestamp)
                .openid(monitor.getOpenid())
                .runStatus(isSucceed ? 1: 0)
                .build();
        mRunRecordMapper.insert(runRecord);
        sendEmail(runRecord,monitor);
    }
    
    
    private void sendEmail(MRunRecord runRecord,MMonitor monitor){
        if(runRecord.getRunStatus() != null && runRecord.getRunStatus() == 1){
            return;
        }
        new Thread(() -> {
            MUser user = userMapper.selectOne(new LambdaQueryWrapper<MUser>().eq(MUser::getOpenid,runRecord.getOpenid()).orderByDesc(MUser::getId).last(" LIMIT 1"));
            if(user == null || StrUtil.isBlank(user.getEmail()) || user.getEmail().length() < 5 || !user.getEmail().contains("@")){
                return;
            }
            String subject = "【亚特技术Web监控】【监控异常】" + monitor.getName();
            String text =
                    "----------------详情登录网站查看----------------\n" +
                    "-------------------请求内容-------------------\n" +
                    "URL:" + monitor.getToUrl() + "\n" +
                    "Method:" + monitor.getToMethod() + "\n" +
                    "-------------------返回内容-------------------\n" +
                    "HttpCode:" + runRecord.getRunCode() + "\n" +
                    "Result:" + runRecord.getRunResult() + "\n";
            eMailUtils.sendTextMailMessage(user.getEmail(), subject, text);
        }).start();
    }
    
    private void setPostParams(HttpRequest httpRequest,MMonitor monitor){
        if(monitor.getToBodyType() != null && monitor.getToBodyType() == 1){
            
            httpRequest.contentType("application/x-www-form-urlencoded;charset=GBK");
            try{
                if(!JSONUtil.isTypeJSONObject(monitor.getToParams())){
                    return;
                }
                JSONObject joParams = new JSONObject(monitor.getToParams());
                Map<String, Object> paramsMap = new HashMap<>();
                for (String key : joParams.keySet()) {
                    paramsMap.put(key,joParams.getStr(key));
                }
                httpRequest.form(paramsMap);
            }catch (Exception e){
            }
        }else if(monitor.getToBodyType() != null && monitor.getToBodyType() == 0){
            httpRequest.contentType("application/json");
            httpRequest.body(monitor.getToParams());
        }
    }
    
    private void setHeaders(HttpRequest httpRequest,MMonitor monitor){
        try{
            if(!JSONUtil.isTypeJSONObject(monitor.getToHeaders())){
                return;
            }
            JSONObject joHeader = new JSONObject(monitor.getToHeaders());
            Map<String,String> headerMap = new HashMap<>();
            for (String key : joHeader.keySet()) {
                headerMap.put(key,joHeader.getStr(key));
            }
            httpRequest.addHeaders(headerMap);
        }catch (Exception e){
        }
    }
 
三、结尾说明
 
- 第一部分说的网站已经可用了,欢迎试用、欢迎长期使用、欢迎联系合作、欢迎定制功能
 - 第二部分给出了核心内容,但这部分实际上不是实现整个网站最耗时的:前端开发工作也是费力不讨好的
 - 本人同时还提供Java开发一对一教学,有需要的添加微信:xujian_cq详聊
 - 欢迎点赞、收藏、评论