impl: 前端面板的简单监测数据查询以及监控规则管理

dev-lensfrex
lensfrex 7 months ago
parent 91e03bee52
commit 41221a4bca
Signed by: lensfrex
GPG Key ID: B1E395B3C6CA0356
  1. 8
      rition-center/api/src/main/java/rition/backend/api/v1/panel/AlertRuleController.java
  2. 14
      rition-center/common/src/main/java/rition/common/data/entity/RuleEntity.java
  3. 5
      rition-center/common/src/main/resources/mapper/MetricRecordMapper.xml
  4. 18
      rition-center/service/panel/src/main/java/rition/service/panel/AlertRuleService.java
  5. 19
      rition-center/service/panel/src/main/java/rition/service/panel/MetricService.java
  6. 10
      rition-panel/quasar.config.js
  7. 2
      rition-panel/src/boot/axios.js
  8. 4
      rition-panel/src/css/app.scss
  9. 2
      rition-panel/src/layouts/MainLayout.vue
  10. 2
      rition-panel/src/pages/AlertListPage.vue
  11. 2
      rition-panel/src/pages/ContractPage.vue
  12. 204
      rition-panel/src/pages/MetricPage.vue
  13. 54
      rition-panel/src/pages/RulePage.vue
  14. 4
      rition-probe/client/http.go
  15. 2
      rition-probe/main.go

@ -27,11 +27,11 @@ public class AlertRuleController {
for (RuleEntity ruleEntity : ruleEntityList) {
RuleResponse response = new RuleResponse();
response.setId(ruleEntity.getId());
response.setExpression(ruleEntity.getExpression());
response.setCondition(ruleEntity.getCondition());
response.setExpression(ruleEntity.getExpr());
response.setCondition(ruleEntity.getCond());
response.setThreshold(ruleEntity.getThreshold());
response.setTrigger(ruleEntity.getTrigger());
response.setDescription(ruleEntity.getDescription());
response.setTrigger(ruleEntity.getTrig());
response.setDescription(ruleEntity.getComment());
response.setCreateTime(ruleEntity.getCreateTime());
response.setUpdateTime(ruleEntity.getUpdateTime());

@ -23,14 +23,14 @@ public class RuleEntity {
private Long id;
/**
* 需要计算的指标项或者表达式
* 需要计算的指标项或者表达式(expression)
*/
private String expression;
private String expr;
/**
* 触发条件
* 触发条件(condition)
*/
private Integer condition;
private Integer cond;
/**
* 阈值
@ -38,14 +38,14 @@ public class RuleEntity {
private Double threshold;
/**
* 触发方法实时计算或定时计算
* 触发方法实时计算或定时计算(trigger)
*/
private Integer trigger;
private Integer trig;
/**
* 规则描述
*/
private String description;
private String comment;
/**
* create_time

@ -67,7 +67,10 @@
<foreach collection="metricItems" item="metricItem">
avg(JSON_EXTRACT(metric_data, "$.${metricItem}")) as ${metricItem},
</foreach>
DATE_FORMAT(time,'%Y-%m-%d %H:%i:00') as t
DATE_FORMAT(
concat( date( TimeStart ), ' ', HOUR ( TimeStart ), ':', floor( MINUTE ( TimeStart ) / #{minutes} ) * ${minutes} ),
'%Y-%m-%d %H:%i'
) as t
FROM record WHERE `time` BETWEEN #{startTime} and #{endTime} and `instance_id` = #{instanceId}
GROUP BY t ORDER BY t
) AS tab;

@ -29,11 +29,11 @@ public class AlertRuleService {
public void addAlertRule(AlertRuleAddDto alertRuleAddDto) {
RuleEntity ruleEntity = new RuleEntity();
ruleEntity.setId(YitIdHelper.nextId());
ruleEntity.setExpression(alertRuleAddDto.getExpression());
ruleEntity.setCondition(alertRuleAddDto.getCondition());
ruleEntity.setExpr(alertRuleAddDto.getExpression());
ruleEntity.setCond(alertRuleAddDto.getCondition());
ruleEntity.setThreshold(alertRuleAddDto.getThreshold());
ruleEntity.setTrigger(alertRuleAddDto.getTrigger());
ruleEntity.setDescription(alertRuleAddDto.getDescription());
ruleEntity.setTrig(alertRuleAddDto.getTrigger());
ruleEntity.setComment(alertRuleAddDto.getDescription());
var now = Instant.now();
ruleEntity.setCreateTime(now);
@ -44,15 +44,15 @@ public class AlertRuleService {
AlertRuleDto alertRuleDto = new AlertRuleDto();
alertRuleDto.setId(ruleEntity.getId());
alertRuleDto.setExpression(ruleEntity.getExpression());
alertRuleDto.setCondition(ruleEntity.getCondition());
alertRuleDto.setExpression(ruleEntity.getExpr());
alertRuleDto.setCondition(ruleEntity.getCond());
alertRuleDto.setThreshold(ruleEntity.getThreshold());
alertRuleDto.setTrigger(ruleEntity.getTrigger());
alertRuleDto.setDescription(ruleEntity.getDescription());
alertRuleDto.setTrigger(ruleEntity.getTrig());
alertRuleDto.setDescription(ruleEntity.getComment());
alertRuleDto.setCreateTime(now);
// 规则缓存到redis
redisTemplate.opsForHash().put(Constants.RedisKeys.RULE_CACHE, alertRuleDto.getId(), alertRuleDto);
redisTemplate.opsForHash().put(Constants.RedisKeys.RULE_CACHE, alertRuleDto.getId().toString(), alertRuleDto);
}
public List<RuleEntity> getRule() {

@ -1,6 +1,5 @@
package rition.service.panel;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import rition.common.data.dao.mapper.MetricRecordMapper;
@ -35,17 +34,17 @@ public class MetricService {
public List<MetricRecordEntity> getMetricDataRange(String instanceId,
List<String> metricItems,
Instant start, Instant end) {
// 时间跨度小于1小时:不按时间粒度获取
// 时间跨度大于1小时:时间粒度为1分钟
// 时间跨度超过一天:时间粒度为小时
// 时间跨度小于6小时:时间粒度为1分钟
// 时间跨度大于6小时:时间粒度为5分钟
// 时间跨度大于12小时:时间粒度为10分钟
// 时间跨度超过15天:时间粒度为小时
var diff = end.minusMillis(start.toEpochMilli()).toEpochMilli();
if (diff < HOUR) {
var query = new LambdaQueryWrapper<MetricRecordEntity>()
.eq(MetricRecordEntity::getInstanceId, instanceId)
.between(MetricRecordEntity::getTime, start, end);
return metricRecordMapper.selectList(query);
} else if (diff < DAY) {
if (diff < HOUR * 6) {
return metricRecordMapper.getMetricDataGroupByMinute(instanceId, metricItems, start, end);
} else if (diff < HOUR * 12) {
return metricRecordMapper.getMetricDataGroupBySomeMinute(5, instanceId, metricItems, start, end);
} else if (diff < DAY * 15) {
return metricRecordMapper.getMetricDataGroupBySomeMinute(10, instanceId, metricItems, start, end);
} else {
return metricRecordMapper.getMetricDataGroupByHour(instanceId, metricItems, start, end);
}

@ -82,7 +82,15 @@ module.exports = configure(function (/* ctx */) {
// Full list of options: https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#devServer
devServer: {
// https: true
open: false // opens browser window automatically
open: false, // opens browser window automatically
proxy: {
// with options
'/api': {
target: 'http://127.0.0.1:8000',
changeOrigin: true,
// rewrite: (path) => path.replace(/^\/api/, '/api')
}
}
},
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#framework

@ -7,7 +7,7 @@ import axios from 'axios'
// good idea to move this instance creation inside of the
// "export default () => {}" function below (which runs individually
// for each client)
const api = axios.create({ baseURL: 'https://api.example.com' })
const api = axios.create({ baseURL: window.location.origin + "/api" })
export default boot(({ app }) => {
// for use inside Vue files (Options API) through this.$axios and this.$api

@ -2,3 +2,7 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
html, body, #q-app {
height: 100%;
}

@ -24,7 +24,7 @@
</q-list>
</q-drawer>
<q-page-container>
<q-page-container class="h-full">
<router-view/>
</q-page-container>
</q-layout>

@ -6,7 +6,7 @@ import {ref} from "vue";
<template>
<div class="flex flex-col gap-2 p-2">
<el-table border style="width: 100%" height="250">
<el-table border style="width: 100%;height: 100%">
<el-table-column fixed prop="id" label="ID"/>
<el-table-column prop="instanceId" label="实例ID"/>
<el-table-column prop="rule" label="触发规则"/>

@ -20,7 +20,7 @@ const form = reactive({
<q-btn unelevated color="primary" @click="addDialogPanelOpen = !addDialogPanelOpen">添加</q-btn>
</div>
<el-table border style="width: 100%" height="250">
<el-table border style="width: 100%;height: 100%">
<el-table-column fixed prop="id" label="ID"/>
<el-table-column prop="instanceId" label="实例ID"/>
<el-table-column prop="contract" label="联系信息"/>

@ -1,5 +1,5 @@
<script setup>
import {ref, provide, reactive} from "vue";
import {ref, provide, reactive, computed} from "vue";
import VChart, {THEME_KEY} from "vue-echarts";
@ -7,7 +7,7 @@ import {use} from 'echarts/core'
import {LineChart} from 'echarts/charts'
import {GridComponent, LegendComponent, TitleComponent, TooltipComponent} from 'echarts/components'
import {CanvasRenderer} from 'echarts/renderers'
import zhCn from 'element-plus/dist/locale/zh-cn.mjs';
import {api} from "boot/axios";
use([
GridComponent,
@ -20,46 +20,146 @@ use([
// provide(THEME_KEY, "dark");
const option = reactive({
grid: {
left: "1%",
right: "2%",
top: "20%",
bottom: "1%",
containLabel: true
},
title: {
text: "CPU使用率",
left: "center",
textStyle: {
fontSize: 16
}
},
tooltip: {
trigger: 'axis'
},
xAxis: {},
yAxis: {},
series: [
{
data: [
[0, 20],
[10, 10],
[20, 64],
[40, 12],
[50, 15],
],
type: 'line',
smooth: true
}
]
});
const options = ref([])
const timeRange = ref([
new Date(Number(new Date()) - 86400000),
new Date()
new Date('2024-04-17 12:00:00'),
new Date('2024-04-17 16:00:00'),
])
const availableMetricItems = [
"up",
"node_load5",
"node_sockstat_TCP_tw",
"node_cpu_seconds_total",
"node_memory_Cached_bytes",
"node_memory_Buffers_bytes",
"node_memory_MemFree_bytes",
"node_disk_read_bytes_total",
"node_filesystem_free_bytes",
"node_filesystem_size_bytes",
"node_memory_MemTotal_bytes",
"node_netstat_Tcp_CurrEstab",
"node_filesystem_avail_bytes",
"node_disk_written_bytes_total",
"node_disk_reads_completed_total",
"node_network_receive_drop_total",
"node_disk_writes_completed_total",
"node_network_receive_bytes_total",
"node_network_transmit_drop_total",
"node_network_transmit_bytes_total",
"node_network_receive_packets_total",
"node_network_transmit_packets_total",
]
const metricText = {
"up": "服务器离线/上线",
"node_load5": "5分钟负载",
"node_sockstat_TCP_tw": "TIME-WAIT连接",
"node_cpu_seconds_total": "CPU使用时间",
"node_memory_Cached_bytes": "缓存内存",
"node_memory_Buffers_bytes": "缓冲内存",
"node_memory_MemFree_bytes": "空闲内存",
"node_disk_read_bytes_total": "磁盘读总字节",
"node_disk_written_bytes_total": "磁盘写总字节",
"node_filesystem_free_bytes": "文件系统空闲空间",
"node_filesystem_size_bytes": "文件系统总大小",
"node_memory_MemTotal_bytes": "总内存",
"node_netstat_Tcp_CurrEstab": "已连接TCP数",
"node_filesystem_avail_bytes": "文件系统可用空间",
"node_disk_reads_completed_total": "磁盘IO读次数",
"node_disk_writes_completed_total": "磁盘IO写次数",
"node_network_receive_drop_total": "接收丢包数",
"node_network_transmit_drop_total": "发送丢包数",
"node_network_receive_bytes_total": "接收总字节",
"node_network_transmit_bytes_total": "发送总字节",
"node_network_receive_packets_total": "总接收包数",
"node_network_transmit_packets_total": "总发送包数"
}
/**
*
* @type {import('vue').Ref<string[]>}
*/
const selectedMetric = ref([
"node_cpu_seconds_total",
"node_memory_MemFree_bytes",
"node_filesystem_free_bytes",
"node_disk_read_bytes_total",
"node_disk_written_bytes_total",
"node_network_receive_bytes_total",
"node_network_transmit_bytes_total",
"node_network_receive_packets_total",
"node_network_transmit_packets_total",
])
const instanceId = ref('7273a1ea-0089-4674-b606-b1b8d809d866')
/**
* @typedef {{
* "instanceId": string,
* "start": number,
* "end": number,
* "metricItems": string[]
* }} MetricRequest
*/
function getMetricData() {
api.post('/panel/metrics/list', {
instanceId: instanceId.value,
start: Number(timeRange.value[0]),
end: Number(timeRange.value[1]),
metricItems: selectedMetric.value
})
.then(r => {
const response = r.data
const opts = []
const metricOptionsMap = new Map
for (const selectedMetricElement of selectedMetric.value) {
metricOptionsMap.set(selectedMetricElement, {
grid: {left: "1%", right: "2%", top: "20%", bottom: "1%", containLabel: true},
title: {text: metricText[selectedMetricElement], left: "center", textStyle: {fontSize: 16}},
tooltip: {trigger: 'axis'},
xAxis: {
type: 'time'
},
yAxis: {
type: 'value', scale: true,
axisLabel: {
color: '#444343',
formatter: function (value, index) {
if (value >= 1e9) {
return (value / 1e9).toFixed(2) + 'G';
} else if (value >= 1e6) {
return (value / 1e6).toFixed(2) + 'M';
} else if (value >= 1e3) {
return (value / 1e3).toFixed(2) + 'K';
} else {
return value;
}
},
},
},
series: [{data: [], type: 'line', smooth: true}]
})
}
for (const dataItem of response.data) {
for (const metricItem in dataItem.metricData) {
const value = dataItem.metricData[metricItem]
metricOptionsMap.get(metricItem).series[0].data.push([dataItem.time / 1000, value])
}
}
for (const metricOptionsMapElement of metricOptionsMap.values()) {
opts.push(metricOptionsMapElement)
}
options.value = opts
})
}
getMetricData()
</script>
<template>
@ -70,23 +170,27 @@ const timeRange = ref([
<el-date-picker v-model="timeRange" type="datetimerange"/>
</div>
<q-separator vertical></q-separator>
<q-select filled dense options-dense
v-model="selectedMetric"
multiple
:options="availableMetricItems"
:option-label="opt => metricText[opt]"
label="想要查看的指标数据"
style="width: 35rem"
/>
<q-separator vertical></q-separator>
<q-input class="flex-1" v-model="instanceId" label="实例ID"/>
<q-separator vertical></q-separator>
<q-btn unelevated color="primary">刷新</q-btn>
<q-btn unelevated color="primary" @click="getMetricData">刷新</q-btn>
</div>
<div class="flex flex-row gap-10">
<v-chart :autoresize=true class="chart" :option="option"/>
<v-chart :autoresize=true class="chart" :option="option"/>
<v-chart :autoresize=true class="chart" :option="option"/>
<v-chart :autoresize=true class="chart" :option="option"/>
<v-chart :autoresize=true class="chart" :option="option"/>
<v-chart :autoresize=true class="chart" :option="option"/>
<v-chart :autoresize=true class="chart" :option="option"/>
<v-chart :autoresize=true class="chart" :option="option"/>
<v-chart :autoresize=true class="chart" :option="option"/>
<v-chart :autoresize=true class="chart" :option="option"/>
<v-chart :autoresize=true class="chart" :option="option"/>
<v-chart v-for="option in options" :key="option" :autoresize=true class="chart" :option="option"/>
</div>
</div>
</template>

@ -1,12 +1,14 @@
<script setup>
import {reactive, ref} from "vue";
import {api} from "boot/axios";
const addDialogPanelOpen = ref(false)
/**
*
* @type {{condition: number, expression: string, description: string, threshold: string, trigger: number}}
*/
const form = reactive({
const ruleForm = reactive({
expression: "",
condition: undefined,
threshold: "",
@ -14,32 +16,60 @@ const form = reactive({
description: ""
})
const rules = ref([])
function getRules() {
api.get('/panel/rules/list')
.then(r => {
const response = r.data
rules.value = response.data
})
}
getRules()
function addRule() {
api.post('/panel/rules/add', ruleForm)
.then(() => getRules())
}
function deleteRule(id) {
api.post('/panel/rules/delete', undefined, {
params: {
id: id
}
})
.then(() => getRules())
}
</script>
<template>
<div class="flex flex-col gap-2 p-2">
<div class="h-full flex flex-col gap-2 p-2">
<div class="flex flex-row gap-4">
<q-btn unelevated color="primary" @click="addDialogPanelOpen = !addDialogPanelOpen">添加</q-btn>
</div>
<el-table border style="width: 100%" height="250">
<el-table class="h-full" :data="rules" border style="width: 100%;height: 100%">
<el-table-column fixed prop="id" label="ID"/>
<el-table-column prop="instanceId" label="实例ID"/>
<el-table-column prop="expression" label="计算值"/>
<el-table-column prop="condition" label="条件"/>
<el-table-column prop="threshold" label="阈值"/>
<el-table-column prop="trigger" label="触发类型"/>
<el-table-column prop="description" label="备注"/>
<el-table-column prop="addTime" label="添加时间"/>
<el-table-column prop="createTime" label="添加时间"/>
<el-table-column label="操作">
<template #default="scope">
<el-button size="small" type="danger" @click="deleteRule(scope.row.id)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-dialog v-model="addDialogPanelOpen" title="添加联系信息" width="500">
<el-form :model="form">
<el-form :model="ruleForm">
<el-form-item label="值" >
<el-input v-model="form.expression" autocomplete="off" />
<el-input v-model="ruleForm.expression" autocomplete="off" />
</el-form-item>
<el-form-item label="条件">
<el-select v-model="form.condition" placeholder="选择判断条件">
<el-select v-model="ruleForm.condition" placeholder="选择判断条件">
<el-option label="<" value="0" />
<el-option label="<=" value="1" />
<el-option label="=" value="2" />
@ -48,23 +78,23 @@ const form = reactive({
</el-select>
</el-form-item>
<el-form-item label="阈值" >
<el-input v-model="form.threshold" autocomplete="off" />
<el-input v-model="ruleForm.threshold" autocomplete="off" />
</el-form-item>
<el-form-item label="触发类型">
<el-select v-model="form.trigger" placeholder="选择触发类型">
<el-select v-model="ruleForm.trigger" placeholder="选择触发类型">
<el-option label="立即计算" value="0" />
<el-option label="定时计算" value="1" />
</el-select>
</el-form-item>
<el-form-item label="备注">
<el-input v-model="form.description" type="textarea"></el-input>
<el-input v-model="ruleForm.description" type="textarea"></el-input>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<q-btn unelevated @click="addDialogPanelOpen = false">取消</q-btn>
<q-btn class="ml-2" unelevated color="primary" @click="addDialogPanelOpen = false">确认</q-btn>
<q-btn class="ml-2" unelevated color="primary" @click="addDialogPanelOpen = false;addRule()">确认</q-btn>
</div>
</template>
</el-dialog>

@ -40,7 +40,7 @@ func (c *Client) Report(dataItem []DataItem) (*ReportResponse, error) {
Metric: item.Metric,
Value: item.Value,
Tags: tags,
Timestamp: time.Now().UnixMilli(),
Timestamp: time.Now().Unix(),
})
}
@ -49,8 +49,6 @@ func (c *Client) Report(dataItem []DataItem) (*ReportResponse, error) {
return nil, err
}
fmt.Println(string(reportJsonBytes))
// send report data
resp, err := http.Post(c.config.CenterServer, "application/json", bytes.NewBuffer(reportJsonBytes))
if err != nil {

@ -14,7 +14,7 @@ func main() {
diskDevice := flag.String("disk_dev", "", "监控的硬盘设备")
mount := flag.String("mount", "/", "监控的分区挂载位置")
netInterface := flag.String("net_dev", "", "监控的网卡设备")
interval := flag.Int("interval", 30, "上报间隔,单位为秒")
interval := flag.Int("interval", 5, "上报间隔,单位为秒")
flag.Parse()
hostname, _ := os.Hostname()

Loading…
Cancel
Save