微服务场景下基于监控指标数据按照场景入口自动生成链路数据图实时展示前后端实现
1 背景
日常运维工作中,面对日益复杂的交易链路,需要时刻关注生产环境中各个服务组件运行情况,当时随着微服务化后,各个系统的调用关系日趋复杂,难以按照传统方式靠人工进行绘图,获取指标进行实时链路观察,也即是云原生可观测性的实现。
用到的技术主要是Django、VUE、Element、Antv G6等。
2 效果展示
3 后端实现逻辑
3.1 前提
已知后端服务间两两调用关系逻辑,及(A,B)(B,C) (B,D) (C, D) (D,B)等这样的有向图,如何从大量的有向访问关系数据中自动生成按照入口起始服务形成交易链路,并实时展示相关指标。
3.2 生成场景
def queryServerByClient(dbcon, cloudtype, tenant, namespace, client, tplTupleList):
# tplTupleList存放根据1个client查询所有相关的调用关系
sql = "select distinct(server) from tb_trace_info " + \
" where cloudType = '" + cloudtype + \
"' and env = 'prod'" + \
" and tenant = '" + tenant + \
"' and nameSpace = '" + namespace + \
"' and client = '" + client + "';"
if int(dbcon.select(sql)['code']) != 1000:
return {"code": 500,
"context": "查询数据库失败"
}
else:
if len(dbcon.select(sql)['context']) == 0:
return {"code": 600,
"context": "查询数据为空"
}
server_res = dbcon.select(sql)['context']
if len(server_res) == 0:
return tplTupleList
else:
for svc in server_res:
server = svc[0]
tpl = (client, server)
if tpl in tplTupleList:
break
else:
tplTupleList.append(tpl)
queryServerByClient(dbcon, cloudtype, tenant, namespace, server, tplTupleList)
4 前端实现逻辑
4.1 创建画布高宽并对中英文展示长度处理
const width = document.getElementById('mountNode').scrollWidth;
const height = (document.getElementById('mountNode').scrollHeight * 0.8) || 600;
const fittingString = (str, maxWidth, fontSize) => {
let currentWidth = 0;
let res = str;
const pattern = new RegExp('[\u4E00-\u9FA5]+'); // distinguish the Chinese charactors and letters
str.split('').forEach((letter, i) => {
if (currentWidth > maxWidth) return;
if (pattern.test(letter)) {
// Chinese charactors
currentWidth += fontSize;
} else {
// get the width of single letter according to the fontSize
currentWidth += G6.Util.getLetterWidth(letter, fontSize);
}
if (currentWidth > maxWidth) {
res = `${str.substr(0, i)}\n${str.substr(i)}`;
}
});
return res;
};
const ICON_MAP = {
pod: 'src/icons/pod.png',
service: 'src/icons/service.png',
trans: 'src/icons/trans.png',
alarm: 'src/icons/alarm.png',
useTime: 'src/icons/time.png',
successRate: 'src/icons/success.png',
};
4.2 自定义节点样式
// 自定义节点
G6.registerNode(
"service", //第一个参数自定义节点的名字
// 第二个参数是这个节点的图形分组
{
draw: function (cfg, group){
//根据数据动态改变颜色或者图片
let duration_avg = cfg.statistics.duration_avg>1 ?"#F3FAFF" :"#F3FAFF";
// let img2=cfg.statistics.duration_avg>1 ?require('../../../../../assets/shouye备份1.png') :require('../../../../../assets/shouye备份.png');
let alert_num = cfg.statistics.alert_num>20 ?"#F3FAFF" :"#F3FAFF";
// let img3=cfg.statistics.alert_num>20 ?require('../../../../../assets/a-fuwuqilan2.png') :require('../../../../../assets/fuwuqilan.png');
let svc_fontsize = 10
let metric_fontsize = 8
// 增加一个图像 最外边的虚线框
let keyShape=group.addShape('rect',{
// 代表矩形的一些属性
// 设置节点部署 相对定位,整个节点布局
attrs:{
x: 0,
y: 0,
width: 130,
height: 80,
stroke: "#1a66b2", //描边色
fill: "#d7dae0", //填充颜色
radius: 3,
lineWidth: 2,
lineDash: [2,2] //设置虚线的样式
},
name:"card-node-keyShape" //起个唯一名字便于识别
});
// 设置节点上方服务名显示布局
group.addShape("rect",{
attrs:{
x:5,
y:1,
width: 120,
height:25,
// stroke:"pink", //描边色
fill:"#3d8ad7",
radius:3,
},
name:"card-node-titleShape"
});
// 左上布局
group.addShape("rect",{ // 指标部分展示
attrs:{
x: 5,
y: 29,
width: 59,
height: 24,
// stroke:"pink", //描边色
fill: "#F3FAFF",
radius: 3,
},
name:"card-node-countShape"
});
// 右上布局
group.addShape("rect",{ // 指标部分展示
attrs:{
x: 66,
y: 29,
width: 59,
height: 24,
fill: duration_avg,
radius: 3,
},
name:"card-node-timeShape"
});
// 左下布局
group.addShape("rect",{ // 指标部分展示
attrs:{
x: 5,
y: 55,
width: 59,
height: 24,
fill: alert_num,
radius: 3,
},
name:"card-node-alarmShape"
});
// 右下布局
group.addShape("rect",{ // 指标部分展示
attrs:{
x: 66,
y: 55,
width: 59,
height: 24,
fill: "#F3FAFF",
radius: 3,
},
name:"card-node-successShape"
});
// 设置服务图标
group.addShape('image', {
attrs: {
x: 4,
y: 2,
height: 16,
width: 16,
cursor: 'pointer',
img: ICON_MAP[cfg.nodeType],
},
// must be assigned in G6 3.3 and later versions. it can be any string you want, but should be unique in a custom item type
name: 'node-icon',
});
// 设置服务名称位置
group.addShape('text',{
attrs:{
text: fittingString(cfg.name,80, 8), //fittingString('Break the line if it is too long', 80, globalFontSize),
x: 20,
y: 2,
fontSize: svc_fontsize,
fill: "block",
textBaseline: "top"
},
name: "card-node-title"
});
// 交易量布局
group.addShape("image",{
attrs:{
x: 5,
y: 33,
width: 16,
height: 16,
img: ICON_MAP['trans'],
},
name:"card-node-countIco"
});
group.addShape("text",{
attrs:{
text:cfg.statistics.trace_num,
x: 22,
y: 46,
fill: "#226DFF",
fontSize: metric_fontsize,
},
name:"card-node-count",
});
// 平均响应时长布局
group.addShape("image",{
attrs:{
x: 65,
y: 33,
width: 16,
height: 16,
img: ICON_MAP['useTime'],
},
name:"card-node-timeIco"
});
group.addShape("text",{
attrs:{
text:cfg.statistics.duration_avg*1000+"ms",
x: 82,
y: 46,
fill: "#226DFF",
fontSize: metric_fontsize,
},
name:"card-node-time",
});
// 告警数
group.addShape("image",{
attrs:{
x: 5,
y: 57,
width: 16,
height: 16,
img: ICON_MAP['alarm'],
},
name:"card-node-alarmIco"
});
group.addShape("text",{
attrs:{
text:cfg.statistics.alert_num,
x: 22,
y: 72,
fill: "#226DFF",
fontSize: metric_fontsize,
},
name:"card-node-alarm",
});
// 成功率模块
group.addShape("image",{
attrs:{
x: 65,
y: 57,
width: 16,
height: 16,
img: ICON_MAP['successRate'],
},
name:"card-node-successIco"
});
group.addShape("text",{
attrs:{
text: cfg.statistics.situation_num + "%",
x: 82,
y: 72,
fill: "#226DFF",
fontSize: metric_fontsize,
},
name:"card-node-success",
});
return keyShape;
},
},'rect'); //第三个参数,是如果没有设置样式,会默认继承rect的样式
4.3 对节点或画布双击、单击事件
function clearAllStats() {
graph.setAutoPaint(false);
graph.getNodes().forEach(function (node) {
graph.clearItemStates(node);
});
graph.getEdges().forEach(function (edge) {
graph.clearItemStates(edge);
});
graph.paint();
graph.setAutoPaint(true);
};
// graph.on('node:mouseleave', clearAllStats);
// 单击画布
graph.on('canvas:click', clearAllStats);
// 双击节点
graph.on('node:dblclick', function (e) {
const item = e.item;
graph.setAutoPaint(false);
graph.getNodes().forEach(function (node) {
graph.clearItemStates(node);
graph.setItemState(node, 'dark', true);
});
graph.setItemState(item, 'dark', false);
graph.setItemState(item, 'highlight', true);
graph.getEdges().forEach(function (edge) {
if (edge.getSource() === item) {
graph.setItemState(edge.getTarget(), 'dark', false);
graph.setItemState(edge.getTarget(), 'highlight', true);
graph.setItemState(edge, 'highlight', true);
edge.toFront();
} else if (edge.getTarget() === item) {
graph.setItemState(edge.getSource(), 'dark', false);
graph.setItemState(edge.getSource(), 'highlight', true);
graph.setItemState(edge, 'highlight', true);
edge.toFront();
} else {
graph.setItemState(edge, 'highlight', false);
}
});
graph.paint();
graph.setAutoPaint(true);
});
// 鼠标进入节点 click dblclick mouseenter
graph.on('node:click', (e) => {
const nodeItem = e.item // 获取鼠标进入的节点元素对象
console.log('单击节点: ', nodeItem._cfg)
});
graph.on('edge:click', (e) => {
// console.log('单击边:', e)
const nodeItem = e.item // 获取鼠标进入的节点元素对象
console.log('单击边: ', nodeItem._cfg)
});
graph.on('canvas:dblclick', (e) => {
// console.log('双击画布: ', e)
console.log('双击画布: ', e.currentTarget.cfg.nodes)
for( var k=0; k < e.currentTarget.cfg.nodes.length; k++) {
var x = e.currentTarget.cfg.nodes[k]._cfg.bboxCache.centerX
var y = e.currentTarget.cfg.nodes[k]._cfg.bboxCache.centerY
console.log(e.currentTarget.cfg.nodes[k]._cfg.id, {"x": x,"y": y})
}
});
参考文件:
[1]: https://g6.antv.antgroup.com/
[2]: https://www.iconfont.cn/search/index?searchType=icon&q=time&page=1&fromCollection=-1