如何实现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详聊
- 欢迎点赞、收藏、评论