基于zabbix数据开发一个监控页面
1、项目背景
由于zabbix监控信息量大、监控项多且杂乱、且警告信息展示不够直观醒目,在此条件下,我们决定以zabbix监控数据为基础,做一套重要系统的单独监控页,要展示全面且更加直观;因此,该项目初代的设计理念就是,尽量在一页中用表格展示出所有相关设备,在设备有警告时,表格背景颜色会变成红色;此外,如果有重要设备警告,页面中会有急促的红色呼吸灯提示,以实用性为主导。
2、使用技术
该项目在Windows环境下使用java语言开发;数据库使用MySQL;采用MVC设计模式,使用Mybatis实现模型层;使用html、css、js、jQuery、ajax等前端技术实现视图层;使用springboot微服务框架实现控制器层;集成开发环境使用IDEA。
3、数据来源
该项目获取监控数据使用了两种方式,一种是通过zabbix的数据库;另一种是通过zabbix API。
4、业务实现
4.1、项目1——zabbix数据库
4.1.1、前端页面实现
第一版的监控页面以实用性为主导,也没有专业的UI设计师,因此并没有考虑太多关于美观的方面;而且,由于没有前车之鉴,只能通过自己实践、踩坑。因此,第一版监控页面上线时,设备名称等信息都是根据已有的文档把数据写死在前端代码里面,下面展示一些代码片段:
<tr>
<td class="bk" rowspan="2">
<input type="text" disabled="disabled" class="louyu" size="10" value=" 二号楼"/>
</td>
<td class="bk" id="i110" height="22px" width="157px">
<input type="text" disabled="disabled" class="jhj" size="16" value="交换机-28"/>
</td>
</tr>
这时候第一个难点问题就出现了;由于数据库中存的名称和页面上要展示的名称是有区别的,而且还要涉及到编码转换问题,因此,如何把前端页面展示的设备与数据库中存储的数据进行一对一绑定成了问题。起初我想到的是IP,因为设备的IP是固定的;但是html中的id选择器不支持IP的格式。后来我在看数据库的表结构时得到了启发,其中hosts表中的hostid和interface表中的interfaceid都是唯一值,就算id选择器不支持纯数字的格式,我只要在前面拼接一个字母,组成新的字符串,就能实现一对一匹配了。下面是代码片段:
//将后台传来的list存入数组
parr = [[${pcList}]];
var a;
//遍历数组拼接字符串
for(var i=0;i<parr.length;i++){
a = 'i'+parr[i];
//通过id绑定
var obj = document.getElementById(a);
if(obj==null){
continue;
}
//实现有警告的设备背景色变红
obj.style["background-color"] = "rgba(255,0,51,0.2)";
4.1.2、SQL语句实现
由于zabbix数据库相对复杂且了解甚少;因此略过了熟悉数据库的环节,直接百度。在了解到查询警告数据相关的表后,开始研究对表结构并进行简单查询;在此之后,开始尝试查询想要的数据,也是因为没有前车之鉴,只能通过不断尝试、踩坑(暂且按下不表,详见踩坑指南),最终查出了想要的数据;下面是SQL语句(优化过后的,ChatGPT是个好东西):
SELECT i.interfaceid
FROM interface i
JOIN hosts h ON i.hostid = h.hostid
JOIN items m ON h.hostid = m.hostid
JOIN functions f ON m.itemid = f.itemid
JOIN triggers t ON f.triggerid = t.triggerid
JOIN events e1 ON t.triggerid = e1.objectid AND e1.value=1 AND e1.name LIKE '%ping%'
LEFT JOIN events e2 ON t.triggerid = e2.objectid AND e2.value=0 AND e2.name LIKE '%ping%'
AND e1.clock > IFNULL(e2.clock,0)
WHERE (i.ip LIKE '10.1.29%' OR i.ip LIKE '10.2.6%' OR i.ip LIKE '10.3.50%'
OR i.ip LIKE '10.3.51%' OR i.ip LIKE '10.3.100%' OR i.ip LIKE '10.3.54%'
OR i.ip LIKE '10.3.55%' OR i.ip LIKE '172.16.26%' OR i.ip LIKE '10.100.2%'
OR i.ip LIKE '10.13.11%' OR i.ip LIKE '10.3.2%' OR i.ip LIKE '10.3.53%'
OR i.ip LIKE '172.16.28%' OR i.ip LIKE '10.2.0%' OR i.ip LIKE '10.3.0%'
OR i.ip LIKE '10.3.1%')
GROUP BY i.interfaceid;
4.1.3、java代码实现
在前端页面和SQL语句基本确定后,解题思路也就确定了,剩下的java代码也就容易实现了。首先创建一个springboot项目实现简单的增删改查…(咳,只有查)。建项目、实体类、mapper、service、controller;先测试跳转,再测试上传数据,OK,跑通了!
该文章主要讲思路以及踩过的坑,代码方面不做过多赘述。(还有ajax实现异步功能,踩坑指南会说。)
package com.cyxf.pojo;
import lombok.Data;
@Data
public class CyxfPC {
private String interfaceid;
private String hostid;
private String itemid;
private String triggerid;
private String objectid;
private String ip;
private String description;
@Override
public String toString() {
return interfaceid;
}
}
package com.cyxf.mapper;
import com.cyxf.pojo.CyxfPC;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
@Mapper
public interface CyxfPCMapper {
List<CyxfPC> selectAllEvents();
}
package com.cyxf.service;
import com.cyxf.pojo.CyxfPC;
import java.util.List;
public interface CyxfPCService {
List<CyxfPC> selectAllEvents();
}
//*******************************************这是一条分割线
package com.cyxf.service;
import com.cyxf.mapper.CyxfPCMapper;
import com.cyxf.pojo.CyxfPC;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class CyxfPCServiceImpl implements CyxfPCService{
@Autowired
CyxfPCMapper pcMapper;
@Override
public List<CyxfPC> selectAllEvents() {
List<CyxfPC> pcList = pcMapper.selectAllEvents();
return pcList;
}
}
package com.cyxf.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import com.cyxf.pojo.CyxfPC;
import com.cyxf.pojo.CyxfURL;
import com.cyxf.service.CyxfPCServiceImpl;
import com.cyxf.service.CyxfURLSerciveImpl;
import java.util.List;
@Controller
public class TestController {
@Autowired
CyxfPCServiceImpl pcService;
@Autowired
CyxfURLSerciveImpl urlService;
@RequestMapping("/cyxf")
public String test(Model model){
List<CyxfPC> pcList = pcService.selectAllEvents();
List<CyxfURL> urlList = urlService.selectAllCode();
model.addAttribute("pcList",pcList);
model.addAttribute("urlList",urlList);
return "cyxf.html";
}
}
4.2、 项目2——zabbix API
4.2.1、设计目的
由于第一版监控页面只考虑了实用性,对于美观方面实在欠缺;又因为该页面不止为监控使用,还要大屏展示,因此请了专业的UI来设计样式,我这个毫无美感的人也退出了历史舞台…
4.2.2、实现逻辑
这次终于有了前车之鉴,回头望去,踩过的坑已是山路十八弯;实现方法大同小异,除了前端代码的变化之外,最大的不同就是数据获取方式了,通过脚本采集到zabbix API的数据后,经过格式调整,存储在txt文件中,我只需要读取txt文件并通过subString方法截取字符串,得到想要的部分传到前端,即可实现功能。
package com.cyxf.service;
import org.springframework.stereotype.Service;
import java.io.*;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
@Service
public class GetData {
// 创建一个File对象,表示要读取的文件
File file = new File("E:/bash/rsync/data_process.txt");
// 创建一个BufferedReader对象,用于读取指定文件的内容
private BufferedReader reader;
// 定义一个方法,用于读取文件并去重
public List<String> getData(){
// 创建两个List对象,分别用于存储读取到的数据和去重后的结果
List<String> list = new ArrayList<>();
List<String> newList = new ArrayList<>();
try {
// 创建BufferedReader对象,读取指定文件的内容
reader = new BufferedReader(new FileReader(file));
String str = "";
char secondLastChar = 'a';
// 逐行读取文件内容
while((str=reader.readLine())!=null){
//这里判断警告的等级以及警告内容
if(str.trim().length()>2&&str.length()>64){
secondLastChar = str.charAt(str.length() - 2);
if (secondLastChar == '4' ||secondLastChar == '5' ||
str.substring(60,64).equals("ping")){
// 将该行的第18到23个字符子字符串添加到list中
list.add(str.substring(18,23));
}
}
}
// lambda表达式 使用Stream API对list进行去重操作
newList = list.stream().distinct().collect(Collectors.toList());
/**
* 也可以使用HashSet集合,它可以自动去重。首先将list转换成HashSet,再将其转换成ArrayList即可得到去重后的结果。
* List<String> newList = new ArrayList<>(new HashSet<>(list));
* */
} catch (FileNotFoundException e) {
e.printStackTrace();
}catch (IOException e){
e.printStackTrace();
}
// 返回去重后的结果
return newList;
}
}
/************************文件中内容***************************
"hosts":"hostid":"12231" "description":"Unavailable by ICMP ping" "priority":"3"
"hosts":"hostid":"10482" "description":"Interface XGigabitEthernet2/2/0/14(): Link down" "priority":"3"
"hosts":"hostid":"13242" "description":"Interface GigabitEthernet0/0/3(): Ethernet has changed to lower speed than it was before" "priority":"1"
*/
package com.cyxf.controller;
import com.cyxf.service.GetData;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import java.util.List;
@Controller
public class TestController {
// 自动注入GetData对象
@Autowired
GetData getData;
// 处理访问/cyxf请求的方法
@RequestMapping("/cyxf")
public String toCyxf(Model model){
// 调用GetData对象的getData方法,获取去重后的结果
List<String> flist = getData.getData();
// 将去重后的结果存储到Model对象中,以便在视图中使用
model.addAttribute("flist",flist);
// 返回cyxf.html视图
return "cyxf.html";
}
}
package com.cyxf.controller;
import com.cyxf.service.GetData;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@RestController
public class DataController {
// 自动注入GetData对象
@Autowired
GetData getData;
// 处理访问/data请求的方法
@GetMapping("/data")
public String getData(){
// 创建一个Map对象,用于存储数据
Map<Object,Object> map = new HashMap<>();
// 调用GetData对象的getData方法,获取去重后的结果
List<String> list = getData.getData();
// 将去重后的结果存储到Map对象中,以便转换成JSON格式的数据
map.put("js",list);
// 使用Jackson库将Map对象转换成JSON格式的数据
String jsonData = null;
try {
jsonData = new ObjectMapper().writeValueAsString(map);
} catch (JsonProcessingException e) {
e.printStackTrace();
}
// 返回JSON格式的数据
return jsonData;
}
}
5、踩坑指南
5.1、坑1——数据库
在使用数据库获取数据时,events表中有两个比较关键的字段,value 和 clock;value只有两个值,1代表有警告,0代表警告已消除;clock是时间戳;一开始我没考虑时间戳;当时的想法是,既然只有0和1两个值,那比较数量不就行了,1多则代表有警告,相等则代表无警告(这里指单个设备,就是根据某个IP所对应的id进行查询);一开始运行没问题,数据获取也很准确。但好景不长,万万没想到tnnd数据库自动清理缓存了,数据清理导致0和1数量失调,整个项目一下子垮掉了…
后来我把时间戳这一字段考虑进去了,既然比较不了数量,那就比时间吧;最新时间value为1,那必然有警告,最新时间value为0,则设备正常;嗯,有品,可行。
5.2、坑2——ajax异步通信
最开始的时候,我设置了页面每分钟自动刷新,这样既方便又准确。但是大屏展示,不仅要美观,刷新偶尔闪白屏,严重影响了观感,也是没品。因此我尝试了用ajax实现异步传输数据;但是问题又来了,虽然能实现数据同步,但是网页是有缓存的,有时候警告消除了,但是样式没有变回来…在经历了多次测试以及查阅代码后,我发现了退而求其次的办法,既然无法清理缓存,那我干脆全部初始化,样式不是回不去吗,我直接一行代码给它重置初始样式,大概看一下吧:
var arr = flist;
setInterval(function() {
$.ajax({
url: "/data", // 后台Controller的请求路径
type: "GET", // 请求方式
dataType: "json", // 返回数据类型
success: function (data) { // 请求成功后的回调函数
// 解析返回的JSON数据
arr = data.js;
}
});
getwarning();
},10000)
//每次获取数据都初始化样式
$('.yic').css("visibility","hidden");
$('.jhj').css("background-color","#3982c5");
document.getElementById('abry').src = "img/ry.png";
document.getElementById('abqb').src = "img/qb.png";
document.getElementById('absk').src = "img/sk.png";
document.getElementById('cry').src = "img/ry.png";
document.getElementById('csk').src = "img/sk.png";
document.getElementById('cqb').src = "img/qb.png";
//初始化ul为空
document.getElementById('xxzs').innerHTML = "";
6、一些图
6.1、项目结构
这是第一版的,有数据库。
这是第二版,通过zabbix API获取数据
6.2 监控页
页面由云应用系统、地图以及终端设备三个模块组成,其中终端设备展示部分实现在框里自动无缝滚动,鼠标悬停可以停止自动滚动,有交换机警告项时,对应的框会变红,且地图红色呼吸灯闪烁,如果是云设备有警告,则图标变红,地图呼吸灯闪烁。
这里仅展示有UI设计的版本,因为我设计的,太没品了。
涉密项目,打码了,见谅
这是有警告的时候,红色图标,闪烁呼吸灯以及警告信息提示
7、总结
经过一路摸爬滚打、试错踩坑,回头看去,其实也不是多难的项目,但原创属实不易,如果有缘人能刷到这个帖子,我也想说一句:
有品!