mirror of
https://github.com/dromara/hertzbeat.git
synced 2026-09-17 18:19:02 +00:00
Compare commits
334
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b9f1d65cb5 | ||
|
|
1dea5467fc | ||
|
|
2f122dfb05 | ||
|
|
f7b0010180 | ||
|
|
2c0fecaa62 | ||
|
|
548966906c | ||
|
|
6473546f56 | ||
|
|
035d63c880 | ||
|
|
d56176dfa5 | ||
|
|
c1815f5107 | ||
|
|
7ac175e726 | ||
|
|
eb5e843251 | ||
|
|
bc098aa4d2 | ||
|
|
bcd394b47f | ||
|
|
fd2202fcaf | ||
|
|
a7629e317a | ||
|
|
3a378f8710 | ||
|
|
cf59907fcd | ||
|
|
624ba8dc8d | ||
|
|
fb4b665c85 | ||
|
|
5a9f371e9d | ||
|
|
5c3d1a04de | ||
|
|
024b44f477 | ||
|
|
35046f8a06 | ||
|
|
23c2f85e8a | ||
|
|
56226c1d52 | ||
|
|
9014f117d3 | ||
|
|
e073bfee91 | ||
|
|
897d5d4220 | ||
|
|
4228605a3d | ||
|
|
e7131b1881 | ||
|
|
8a22797bca | ||
|
|
b9e569a1e0 | ||
|
|
1284b4b46b | ||
|
|
175c289e66 | ||
|
|
899e92882d | ||
|
|
d516b3a15d | ||
|
|
7678c3f139 | ||
|
|
3b49c0cb75 | ||
|
|
32d784e57e | ||
|
|
d8f8444275 | ||
|
|
41676c1f13 | ||
|
|
a3def497b4 | ||
|
|
381db85149 | ||
|
|
d0daa2eec5 | ||
|
|
9b5f0f7fce | ||
|
|
b3ef3d8a6f | ||
|
|
bd1924b016 | ||
|
|
1c781d5bc5 | ||
|
|
586aa53e84 | ||
|
|
c5f0389138 | ||
|
|
b93613c25e | ||
|
|
a8b0c1024d | ||
|
|
25a63ccb25 | ||
|
|
5cdfe545b5 | ||
|
+16 |
9be7295084 | ||
|
|
6905bcc986 | ||
|
|
1050032a13 | ||
|
|
9b223dd217 | ||
|
|
0a1b5961e9 | ||
|
|
a8a70b76d7 | ||
|
+1 |
05b63903b5 | ||
|
|
7d8b55dfb8 | ||
|
|
72a3e59ca6 | ||
|
|
3ab94a751b | ||
|
|
66049f189b | ||
|
|
69ec3f8751 | ||
|
|
feb43c5678 | ||
|
|
1fae67282d | ||
|
|
886f0b74f6 | ||
|
|
32e1a43aa5 | ||
|
|
dcd806802e | ||
|
|
5746cbbbd7 | ||
|
|
e23903fd0c | ||
|
|
780710f226 | ||
|
|
4607ede541 | ||
|
|
67ad4b04ef | ||
|
|
6432d689dd | ||
|
|
2eb856fffb | ||
|
|
56312831ca | ||
|
|
fe54d42bc0 | ||
|
|
2ea24c1b3a | ||
|
|
e0040e2ede | ||
|
|
0ac0fd4a8d | ||
|
|
0ef87bcc08 | ||
|
|
660bda6e14 | ||
|
|
242cf40135 | ||
|
|
7319c55430 | ||
|
|
5ea29f610b | ||
|
|
b74aff0500 | ||
|
|
d2e8f2f4aa | ||
|
|
28681a1f9e | ||
|
|
6814c7c357 | ||
|
|
f475540508 | ||
|
|
44660a5042 | ||
|
|
339386fd2b | ||
|
|
faa7cb71db | ||
|
|
014a376c30 | ||
|
|
377172e44a | ||
|
|
5b9e19cc30 | ||
|
|
0457ec7d16 | ||
|
|
2947ee218e | ||
|
|
4e841a9fb1 | ||
|
|
b7ecb9b2ec | ||
|
|
7e1873bc10 | ||
|
|
8eb60210a3 | ||
|
|
d608f732f7 | ||
|
|
2d9a2955ef | ||
|
|
cae4423f30 | ||
|
|
f0e53a9fb3 | ||
|
|
79da1a198b | ||
|
|
64cb804147 | ||
|
|
344cb25229 | ||
|
|
74d21a7acd | ||
|
|
f315bf9607 | ||
|
|
810d54bef5 | ||
|
|
48eafa1382 | ||
|
|
4b6c1e0f08 | ||
|
|
edfd857030 | ||
|
|
408a06d3eb | ||
|
|
fd3a2f0569 | ||
|
|
7d8ba72717 | ||
|
|
f936b50ce9 | ||
|
|
93f5da2d02 | ||
|
|
bc06c268ca | ||
|
|
9e6f2762c0 | ||
|
|
6ac8a42bd3 | ||
|
|
89647d96c7 | ||
|
|
0b0fac4a41 | ||
|
|
f59e15bbbe | ||
|
|
b7398b1bf9 | ||
|
|
0082e724bc | ||
|
|
57a2daf30d | ||
|
|
db90f29cd0 | ||
|
|
00fd8fa1da | ||
|
|
60100eb980 | ||
|
|
a6eb6be1ab | ||
|
|
5eff094af6 | ||
|
|
3b126f53d2 | ||
|
|
1a5b722994 | ||
|
|
5fe20dff05 | ||
|
|
04cfdae1e5 | ||
|
|
8776f98cf5 | ||
|
|
da484aef26 | ||
|
|
e8ebc65053 | ||
|
|
bd08de41d5 | ||
|
|
18f8a541f0 | ||
|
|
007ecd2db1 | ||
|
|
89aaad2401 | ||
|
|
17d836752e | ||
|
|
3949b73751 | ||
|
|
2731504278 | ||
|
|
babbfaea68 | ||
|
|
9f7b9994b8 | ||
|
|
d156c10aed | ||
|
|
9f3329c85f | ||
|
|
fd1c58f68d | ||
|
|
6d1aa503df | ||
|
|
a58ad35c8b | ||
|
|
64cd5f9658 | ||
|
|
a13dd89fd2 | ||
|
|
2f5cfa9d5d | ||
|
|
a9dc7bf7f3 | ||
|
|
c1492deaa0 | ||
|
|
2252b2a769 | ||
|
|
73f71503f5 | ||
|
|
2e224ce626 | ||
|
|
e5406d57c6 | ||
|
|
433a83303f | ||
|
|
f309d5eafd | ||
|
|
986103395b | ||
|
|
d569721e01 | ||
|
|
08cac3c6cc | ||
|
|
f80d066851 | ||
|
|
d6584093f5 | ||
|
|
fb03a57725 | ||
|
|
779272d65f | ||
|
|
c21a2952a5 | ||
|
|
b46d049158 | ||
|
|
dc52c88d88 | ||
|
|
df4e947c8a | ||
|
|
a4ec6c1f66 | ||
|
|
a8dd57d009 | ||
|
|
af3eabdd1c | ||
|
|
d6b300d3df | ||
|
|
b5789538e6 | ||
|
|
d6c5b2b8a2 | ||
|
|
3a04647c3c | ||
|
|
d2999c1aa6 | ||
|
|
b3dbc7092a | ||
|
|
edb3cb6b6a | ||
|
|
4dd78b2e98 | ||
|
|
2f91f56b7b | ||
|
|
be3dac488b | ||
|
|
553bdbaf9f | ||
|
|
40d3f1243b | ||
|
|
740e3f8385 | ||
|
|
47d089dff7 | ||
|
|
c6f1f07155 | ||
|
|
0ebd3499b7 | ||
|
|
c88d4a7b0e | ||
|
|
8bebae9db6 | ||
|
|
b6b1a48a5c | ||
|
|
63ed50ef45 | ||
|
|
e63715d1b0 | ||
|
|
edf3eac6d6 | ||
|
|
743f285c8d | ||
|
|
458433464c | ||
|
|
64740a4f85 | ||
|
|
b5a2bce261 | ||
|
|
5e351c854b | ||
|
|
0875bea046 | ||
|
|
9ad5f77816 | ||
|
|
998f9059bc | ||
|
|
a1439c3ec2 | ||
|
|
742b6f6755 | ||
|
|
5fdb34598c | ||
|
|
4f982d1ab0 | ||
|
|
4a28056d9f | ||
|
|
3f2fa71e22 | ||
|
|
08c81bf62f | ||
|
|
e16d6cb4ba | ||
|
|
a54d14e803 | ||
|
|
01ae281769 | ||
|
|
9cb10ff14d | ||
|
|
62f40d3ad1 | ||
|
|
57601ef989 | ||
|
|
0a05edbec5 | ||
|
|
fcef24fdbf | ||
|
|
e45c31abfb | ||
|
|
198c190ca7 | ||
|
|
392a59f9a9 | ||
|
|
c01b7f7bce | ||
|
|
0323b044b8 | ||
|
|
256302fdcf | ||
|
|
3f3330af41 | ||
|
|
33342baa3f | ||
|
|
563cbd7943 | ||
|
|
ddb1601290 | ||
|
|
8e9d3c09f3 | ||
|
|
ade04d2cf2 | ||
|
|
53555c88c7 | ||
|
|
03bf31269d | ||
|
|
5c329a8a66 | ||
|
|
dda9250243 | ||
|
|
65a97aed8f | ||
|
|
56528f4fc0 | ||
|
|
f218ba018a | ||
|
|
d6ce0c153a | ||
|
|
a1b23cacdc | ||
|
|
ed6214d0c1 | ||
|
|
9a3fc5911d | ||
|
|
d5d01459e7 | ||
|
|
d077c72211 | ||
|
|
11e3e4cc22 | ||
|
|
c24c6ab7aa | ||
|
|
a183176e83 | ||
|
|
a2e8739fec | ||
|
|
ebb6e3d4ff | ||
|
|
fc3f26368d | ||
|
|
7967cfabf0 | ||
|
|
199466cd59 | ||
|
|
8065b63057 | ||
|
|
88655fcf19 | ||
|
|
99b5460dd1 | ||
|
|
d681420c11 | ||
|
|
8618145e44 | ||
|
|
dd0045c6a4 | ||
|
|
84e0a604e7 | ||
|
|
ce253f7069 | ||
|
|
767b70f70b | ||
|
|
4aad08ca17 | ||
|
|
c176bb2988 | ||
|
|
3cb4fc71f9 | ||
|
|
c3f32132ca | ||
|
|
e1affb169f | ||
|
|
db27159947 | ||
|
|
63c187488f | ||
|
|
f3f66ada0d | ||
|
|
d5c934dc01 | ||
|
|
518eebb6b1 | ||
|
|
210454f969 | ||
|
|
a8db5e31a0 | ||
|
|
c01653aff3 | ||
|
|
480bd0e449 | ||
|
|
97faca6753 | ||
|
|
e271bd7417 | ||
|
|
7de4fd1cbf | ||
|
|
c2a7d0db35 | ||
|
|
93b2630bf3 | ||
|
|
23749e7fac | ||
|
|
d5eae40754 | ||
|
|
7ef79de1de | ||
|
|
8496771c8b | ||
|
|
f31c835181 | ||
|
|
552554b96d | ||
|
|
4adcfd2a4e | ||
|
|
e630beecb4 | ||
|
|
6c2b38e180 | ||
|
|
84986dd1bf | ||
|
|
af868e083a | ||
|
|
be3e2f3558 | ||
|
|
d32818e8ef | ||
|
|
10b283522c | ||
|
|
31ab2da41e | ||
|
|
a9436049e3 | ||
|
|
41c010511c | ||
|
|
b5281c1274 | ||
|
|
62cf3d1b2b | ||
|
|
dc8ae844c1 | ||
|
|
91a7593b87 | ||
|
|
4442a52adf | ||
|
|
60e3437e82 | ||
|
|
21d6ec2f1b | ||
|
|
3ed50f39cb | ||
|
|
dfba21f224 | ||
|
|
9fb3f39eb1 | ||
|
|
2430448089 | ||
|
|
993de3d504 | ||
|
|
955ecd2128 | ||
|
|
96e5402c09 | ||
|
|
0a6e8147e8 | ||
|
|
7779056983 | ||
|
|
ca3348adf2 | ||
|
|
dc822f50e8 | ||
|
|
896adb76a2 | ||
|
|
2a49fa5774 | ||
|
|
2259d51cae | ||
|
|
e9e49cfbc5 | ||
|
|
3eba906491 | ||
|
|
6645c5d4b2 | ||
|
|
88fb007ef0 | ||
|
|
4892ef101f | ||
|
|
d0b7e38537 |
+192
-10
@@ -6,7 +6,7 @@
|
|||||||
"files": [
|
"files": [
|
||||||
"README.md",
|
"README.md",
|
||||||
"README_CN.md",
|
"README_CN.md",
|
||||||
"home/src/pages/team/index.jsx"
|
"README_JP.md"
|
||||||
],
|
],
|
||||||
"imageSize": 100,
|
"imageSize": 100,
|
||||||
"commit": true,
|
"commit": true,
|
||||||
@@ -1269,16 +1269,12 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"login": "a-little-fool",
|
"login": "lynx009",
|
||||||
"name": "小笨蛋",
|
"name": "lynx009",
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/105542329?v=4",
|
"avatar_url": "https://avatars.githubusercontent.com/u/105542329?v=4",
|
||||||
"profile": "https://github.com/a-little-fool",
|
"profile": "https://github.com/lynx009",
|
||||||
"contributions": [
|
"contributions": [
|
||||||
"code",
|
"doc"
|
||||||
"doc",
|
|
||||||
"blog",
|
|
||||||
"bug",
|
|
||||||
"design"
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -1441,7 +1437,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"login": "Calvin979",
|
"login": "Calvin979",
|
||||||
"name": "Naruse",
|
"name": "Calvin",
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/131688897?v=4",
|
"avatar_url": "https://avatars.githubusercontent.com/u/131688897?v=4",
|
||||||
"profile": "https://github.com/Calvin979",
|
"profile": "https://github.com/Calvin979",
|
||||||
"contributions": [
|
"contributions": [
|
||||||
@@ -2485,6 +2481,192 @@
|
|||||||
"contributions": [
|
"contributions": [
|
||||||
"code"
|
"code"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "Duansg",
|
||||||
|
"name": "Duansg",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/112607719?v=4",
|
||||||
|
"profile": "https://github.com/Duansg",
|
||||||
|
"contributions": [
|
||||||
|
"doc"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "xiaomizhou2",
|
||||||
|
"name": "zhangyaxi",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/47807926?v=4",
|
||||||
|
"profile": "https://github.com/xiaomizhou2",
|
||||||
|
"contributions": [
|
||||||
|
"code",
|
||||||
|
"test"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "RainBondsongyg",
|
||||||
|
"name": "songyg",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/94501396?v=4",
|
||||||
|
"profile": "https://github.com/RainBondsongyg",
|
||||||
|
"contributions": [
|
||||||
|
"doc"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "lx1229",
|
||||||
|
"name": "Liuxin",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/44620005?v=4",
|
||||||
|
"profile": "https://github.com/lx1229",
|
||||||
|
"contributions": [
|
||||||
|
"code"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "yy549159265",
|
||||||
|
"name": "yy549159265",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/40821310?v=4",
|
||||||
|
"profile": "https://github.com/yy549159265",
|
||||||
|
"contributions": [
|
||||||
|
"code",
|
||||||
|
"test",
|
||||||
|
"design"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "cto-huhang",
|
||||||
|
"name": "cto-huhang",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/53338629?v=4",
|
||||||
|
"profile": "https://github.com/cto-huhang",
|
||||||
|
"contributions": [
|
||||||
|
"doc"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "Saramanda9988",
|
||||||
|
"name": "LunaRain_079",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/176664901?v=4",
|
||||||
|
"profile": "https://github.com/Saramanda9988",
|
||||||
|
"contributions": [
|
||||||
|
"doc"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "delei",
|
||||||
|
"name": "DeleiGuo",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/17263766?v=4",
|
||||||
|
"profile": "https://github.com/delei",
|
||||||
|
"contributions": [
|
||||||
|
"doc",
|
||||||
|
"code",
|
||||||
|
"test",
|
||||||
|
"bug"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "chingjustwe",
|
||||||
|
"name": "Rocky, Chi",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/13643747?v=4",
|
||||||
|
"profile": "https://github.com/chingjustwe",
|
||||||
|
"contributions": [
|
||||||
|
"code"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "rowankid",
|
||||||
|
"name": "Wenqi Luo",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/18652781?v=4",
|
||||||
|
"profile": "https://github.com/rowankid",
|
||||||
|
"contributions": [
|
||||||
|
"bug"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "tuzuy",
|
||||||
|
"name": "tuzuy",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/95274591?v=4",
|
||||||
|
"profile": "https://github.com/tuzuy",
|
||||||
|
"contributions": [
|
||||||
|
"code"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "carlpinto25",
|
||||||
|
"name": "carl pinto",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/117299909?v=4",
|
||||||
|
"profile": "https://github.com/carlpinto25",
|
||||||
|
"contributions": [
|
||||||
|
"code"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "cxhello",
|
||||||
|
"name": "cxhello",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/49056040?v=4",
|
||||||
|
"profile": "https://cxhello.top/",
|
||||||
|
"contributions": [
|
||||||
|
"doc"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "jl15988",
|
||||||
|
"name": "会功夫的李白",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/70638770?v=4",
|
||||||
|
"profile": "https://github.com/jl15988",
|
||||||
|
"contributions": [
|
||||||
|
"code"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "AlbertYang0801",
|
||||||
|
"name": "Albert.Yang",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/37127008?v=4",
|
||||||
|
"profile": "https://blog.aytop.cloud/",
|
||||||
|
"contributions": [
|
||||||
|
"code"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "mengnankkkk",
|
||||||
|
"name": "zhou yong kang",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/150590575?v=4",
|
||||||
|
"profile": "https://blog.tokenlen.top/",
|
||||||
|
"contributions": [
|
||||||
|
"code"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "warrobe",
|
||||||
|
"name": "warrobe",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/89446159?v=4",
|
||||||
|
"profile": "https://github.com/warrobe",
|
||||||
|
"contributions": [
|
||||||
|
"code"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "Jetiaime",
|
||||||
|
"name": "TeAmo",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/93769000?v=4",
|
||||||
|
"profile": "https://github.com/Jetiaime",
|
||||||
|
"contributions": [
|
||||||
|
"code"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "pentium100",
|
||||||
|
"name": "pentium100",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/27917?v=4",
|
||||||
|
"profile": "https://github.com/pentium100",
|
||||||
|
"contributions": [
|
||||||
|
"code"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "dedyks",
|
||||||
|
"name": "Dedy Kurniawan Santoso",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/23741665?v=4",
|
||||||
|
"profile": "https://github.com/dedyks",
|
||||||
|
"contributions": [
|
||||||
|
"code"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"contributorsPerLine": 7,
|
"contributorsPerLine": 7,
|
||||||
|
|||||||
@@ -16,11 +16,15 @@
|
|||||||
#
|
#
|
||||||
|
|
||||||
github:
|
github:
|
||||||
description: Apache HertzBeat(incubating) is a real-time monitoring system with agentless, performance cluster, prometheus-compatible, custom monitoring and status page building capabilities.
|
description: An AI-powered next-generation open source real-time observability system.
|
||||||
homepage: https://hertzbeat.apache.org/
|
homepage: https://hertzbeat.apache.org/
|
||||||
labels:
|
labels:
|
||||||
- monitoring
|
- ai
|
||||||
|
- agent
|
||||||
|
- llm
|
||||||
|
- logs
|
||||||
- monitor
|
- monitor
|
||||||
|
- monitoring
|
||||||
- notifications
|
- notifications
|
||||||
- alerting
|
- alerting
|
||||||
- self-hosted
|
- self-hosted
|
||||||
@@ -30,15 +34,11 @@ github:
|
|||||||
- metrics
|
- metrics
|
||||||
- observability
|
- observability
|
||||||
- uptime
|
- uptime
|
||||||
- uptime-monitoring
|
|
||||||
- status
|
- status
|
||||||
- status-page
|
- status-page
|
||||||
- devops
|
|
||||||
- server
|
- server
|
||||||
- linux
|
- linux
|
||||||
- database
|
- database
|
||||||
- mysql
|
|
||||||
- cloud
|
|
||||||
enabled_merge_buttons:
|
enabled_merge_buttons:
|
||||||
squash: true
|
squash: true
|
||||||
merge: false
|
merge: false
|
||||||
@@ -52,6 +52,9 @@ github:
|
|||||||
required_pull_request_reviews:
|
required_pull_request_reviews:
|
||||||
dismiss_stale_reviews: true
|
dismiss_stale_reviews: true
|
||||||
required_approving_review_count: 1
|
required_approving_review_count: 1
|
||||||
|
# enable GitHub Dependabot to create PRs for security alerts but not for every dependency update
|
||||||
|
dependabot_alerts: true
|
||||||
|
dependabot_updates: false
|
||||||
notifications:
|
notifications:
|
||||||
commits: notifications@hertzbeat.apache.org
|
commits: notifications@hertzbeat.apache.org
|
||||||
issues: notifications@hertzbeat.apache.org
|
issues: notifications@hertzbeat.apache.org
|
||||||
|
|||||||
@@ -15,7 +15,6 @@
|
|||||||
"customizations": {
|
"customizations": {
|
||||||
"vscode": {
|
"vscode": {
|
||||||
"settings": {},
|
"settings": {},
|
||||||
// same extensions as Gitpod, should match /.gitpod.yml
|
|
||||||
"extensions": [
|
"extensions": [
|
||||||
"vscjava.vscode-java-pack",
|
"vscjava.vscode-java-pack",
|
||||||
"editorconfig.editorconfig",
|
"editorconfig.editorconfig",
|
||||||
|
|||||||
@@ -53,4 +53,5 @@ body:
|
|||||||
validations:
|
validations:
|
||||||
required: false
|
required: false
|
||||||
- type: markdown
|
- type: markdown
|
||||||
value: "Please read the [Contribution Guideline](https://hertzbeat.apache.org/docs/community/contribution) before submitting the PR"
|
attributes:
|
||||||
|
value: "Please read the [Contribution Guideline](https://hertzbeat.apache.org/docs/community/contribution) before submitting the PR"
|
||||||
|
|||||||
+10
-10
@@ -90,24 +90,24 @@ e2e:
|
|||||||
backend:
|
backend:
|
||||||
- changed-files:
|
- changed-files:
|
||||||
- any-glob-to-any-file:
|
- any-glob-to-any-file:
|
||||||
- "alerter/**/*"
|
- "hertzbeat-alerter/**/*"
|
||||||
- "collector/**/*"
|
- "hertzbeat-collector/**/*"
|
||||||
- "common/**/*"
|
- "hertzbeat-common/**/*"
|
||||||
- "grafana/**/*"
|
- "hertzbeat-grafana/**/*"
|
||||||
- "manager/**/*"
|
- "hertzbeat-manager/**/*"
|
||||||
- "push/**/*"
|
- "hertzbeat-push/**/*"
|
||||||
- "remoting/**/*"
|
- "hertzbeat-remoting/**/*"
|
||||||
- "warehouse/**/*"
|
- "hertzbeat-warehouse/**/*"
|
||||||
|
|
||||||
collector:
|
collector:
|
||||||
- changed-files:
|
- changed-files:
|
||||||
- any-glob-to-any-file:
|
- any-glob-to-any-file:
|
||||||
- "collector/**/*"
|
- "hertzbeat-collector/**/*"
|
||||||
|
|
||||||
monitoring-template:
|
monitoring-template:
|
||||||
- changed-files:
|
- changed-files:
|
||||||
- any-glob-to-any-file:
|
- any-glob-to-any-file:
|
||||||
- "manager/src/main/resources/define/**/*"
|
- "hertzbeat-manager/src/main/resources/define/**/*"
|
||||||
|
|
||||||
HIP:
|
HIP:
|
||||||
- changed-files:
|
- changed-files:
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ jobs:
|
|||||||
- uses: ./script/ci/github-actions/setup-deps
|
- uses: ./script/ci/github-actions/setup-deps
|
||||||
|
|
||||||
- name: Build with Maven
|
- name: Build with Maven
|
||||||
run: mvn clean -B package -Prelease -Dmaven.test.skip=false --file pom.xml
|
run: mvnd clean -B package -Prelease -Dmaven.test.skip=false --file pom.xml
|
||||||
|
|
||||||
- name: Upload coverage reports to Codecov
|
- name: Upload coverage reports to Codecov
|
||||||
uses: codecov/codecov-action@v4.0.1
|
uses: codecov/codecov-action@v4.0.1
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ jobs:
|
|||||||
python-version: '3.8'
|
python-version: '3.8'
|
||||||
|
|
||||||
- name: Check Markdown
|
- name: Check Markdown
|
||||||
uses: DavidAnson/markdownlint-cli2-action@v16
|
uses: DavidAnson/markdownlint-cli2-action@992badcdf24e3b8eb7e87ff9287fe931bcb00c6e
|
||||||
with:
|
with:
|
||||||
globs: './home/**/*.md'
|
globs: './home/**/*.md'
|
||||||
|
|
||||||
@@ -49,11 +49,9 @@ jobs:
|
|||||||
- name: Dead Link Check
|
- name: Dead Link Check
|
||||||
run: |
|
run: |
|
||||||
sudo npm install -g markdown-link-check@3.8.7
|
sudo npm install -g markdown-link-check@3.8.7
|
||||||
for file in $(find ./home -name "*.md"); do
|
find ./home -name "*.md" > all_md_files.txt
|
||||||
if ! grep -Fxq "$file" ./script/ci/exclude_files.txt; then
|
grep -vFf ./script/ci/exclude_files.txt all_md_files.txt > to_check.txt
|
||||||
markdown-link-check -c ./script/ci/link_check.json -q "$file"
|
xargs -P 8 -a to_check.txt -I{} markdown-link-check -c ./script/ci/link_check.json -q "{}"
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
- name: NPM INSTALL
|
- name: NPM INSTALL
|
||||||
working-directory: home
|
working-directory: home
|
||||||
|
|||||||
@@ -35,9 +35,12 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- name: Yarn Install
|
- uses: pnpm/action-setup@v4
|
||||||
|
with:
|
||||||
|
version: 10
|
||||||
|
- name: Install
|
||||||
working-directory: web-app
|
working-directory: web-app
|
||||||
run: yarn
|
run: pnpm install
|
||||||
- name: EsLint Test
|
- name: EsLint Test
|
||||||
working-directory: web-app
|
working-directory: web-app
|
||||||
run: yarn lint:ts
|
run: pnpm lint:ts
|
||||||
|
|||||||
@@ -0,0 +1,97 @@
|
|||||||
|
# Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
# or more contributor license agreements. See the NOTICE file
|
||||||
|
# distributed with this work for additional information
|
||||||
|
# regarding copyright ownership. The ASF licenses this file
|
||||||
|
# to you under the Apache License, Version 2.0 (the
|
||||||
|
# "License"); you may not use this file except in compliance
|
||||||
|
# with the License. You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing,
|
||||||
|
# software distributed under the License is distributed on an
|
||||||
|
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
# KIND, either express or implied. See the License for the
|
||||||
|
# specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
name: MCP Bash Server CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
paths:
|
||||||
|
- 'mcp-servers/mcp-bash-server/**'
|
||||||
|
- '.github/workflows/mcp-bashserver-test.yml'
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- 'mcp-servers/mcp-bash-server/**'
|
||||||
|
- '.github/workflows/mcp-bashserver-test.yml'
|
||||||
|
|
||||||
|
env:
|
||||||
|
CARGO_TERM_COLOR: always
|
||||||
|
RUST_VERSION: 1.88.0
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
name: Test MCP Bash Server
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
profile: [dev, release]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Rust toolchain
|
||||||
|
uses: dtolnay/rust-toolchain@stable
|
||||||
|
with:
|
||||||
|
toolchain: ${{ env.RUST_VERSION }}
|
||||||
|
components: rustfmt, clippy
|
||||||
|
|
||||||
|
- name: Cache cargo registry
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.cargo/registry/index/
|
||||||
|
~/.cargo/registry/cache/
|
||||||
|
~/.cargo/git/db/
|
||||||
|
key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-cargo-registry-
|
||||||
|
|
||||||
|
- name: Cache cargo build
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: target/
|
||||||
|
key: ${{ runner.os }}-cargo-build-${{ matrix.profile }}-${{ hashFiles('**/Cargo.lock') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-cargo-build-${{ matrix.profile }}-
|
||||||
|
${{ runner.os }}-cargo-build-
|
||||||
|
|
||||||
|
- name: Check code format
|
||||||
|
run: cargo fmt --manifest-path ./mcp-servers/mcp-bash-server/Cargo.toml --check
|
||||||
|
|
||||||
|
- name: Run cargo check
|
||||||
|
run: |
|
||||||
|
if [ "${{ matrix.profile }}" = "release" ]; then
|
||||||
|
cargo check --release --manifest-path ./mcp-servers/mcp-bash-server/Cargo.toml
|
||||||
|
else
|
||||||
|
cargo check --manifest-path ./mcp-servers/mcp-bash-server/Cargo.toml
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Run clippy
|
||||||
|
run: |
|
||||||
|
if [ "${{ matrix.profile }}" = "release" ]; then
|
||||||
|
cargo clippy --release --manifest-path ./mcp-servers/mcp-bash-server/Cargo.toml -- -D warnings
|
||||||
|
else
|
||||||
|
cargo clippy --manifest-path ./mcp-servers/mcp-bash-server/Cargo.toml -- -D warnings
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
run: |
|
||||||
|
if [ "${{ matrix.profile }}" = "release" ]; then
|
||||||
|
cargo test --release --manifest-path ./mcp-servers/mcp-bash-server/Cargo.toml
|
||||||
|
else
|
||||||
|
cargo test --manifest-path ./mcp-servers/mcp-bash-server/Cargo.toml
|
||||||
|
fi
|
||||||
@@ -31,12 +31,15 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: ./script/ci/github-actions/setup-deps
|
- uses: ./script/ci/github-actions/setup-deps
|
||||||
|
- uses: pnpm/action-setup@v4
|
||||||
|
with:
|
||||||
|
version: 10
|
||||||
|
|
||||||
- name: Build the Frontend
|
- name: Build the Frontend
|
||||||
run: |
|
run: |
|
||||||
cd web-app
|
cd web-app
|
||||||
yarn install
|
pnpm install
|
||||||
yarn package
|
pnpm package
|
||||||
|
|
||||||
- name: Build the Backend
|
- name: Build the Backend
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ Thumbs.db
|
|||||||
build/
|
build/
|
||||||
out/
|
out/
|
||||||
micronaut-cli.yml
|
micronaut-cli.yml
|
||||||
.mvn/
|
|
||||||
*.log
|
*.log
|
||||||
package-lock.json
|
package-lock.json
|
||||||
*.zip
|
*.zip
|
||||||
|
|||||||
+3
-3
@@ -4,7 +4,7 @@ image:
|
|||||||
tasks:
|
tasks:
|
||||||
|
|
||||||
- name: Run backend
|
- name: Run backend
|
||||||
before: cd hertzbeat-manager
|
before: cd hertzbeat-startup
|
||||||
command: |
|
command: |
|
||||||
gp sync-await setup-backend
|
gp sync-await setup-backend
|
||||||
mvn spring-boot:run
|
mvn spring-boot:run
|
||||||
@@ -13,7 +13,7 @@ tasks:
|
|||||||
before: cd web-app
|
before: cd web-app
|
||||||
command: |
|
command: |
|
||||||
gp sync-await setup-frontend
|
gp sync-await setup-frontend
|
||||||
yarn start --public-host "`gp url 4200`"
|
pnpm start --public-host "`gp url 4200`"
|
||||||
openMode: split-right
|
openMode: split-right
|
||||||
|
|
||||||
- name: Setup backend
|
- name: Setup backend
|
||||||
@@ -25,7 +25,7 @@ tasks:
|
|||||||
|
|
||||||
- name: Setup frontend
|
- name: Setup frontend
|
||||||
init: |
|
init: |
|
||||||
cd web-app && yarn install
|
cd web-app && pnpm install
|
||||||
command: |
|
command: |
|
||||||
gp sync-done setup-frontend
|
gp sync-done setup-frontend
|
||||||
exit 0
|
exit 0
|
||||||
|
|||||||
@@ -66,7 +66,9 @@ header:
|
|||||||
- '.gitpod.yml'
|
- '.gitpod.yml'
|
||||||
- 'karma.conf.js'
|
- 'karma.conf.js'
|
||||||
- 'proxy.conf.js'
|
- 'proxy.conf.js'
|
||||||
|
- 'pnpm-lock.yaml'
|
||||||
- '.helmignore'
|
- '.helmignore'
|
||||||
|
- 'script/helm/**'
|
||||||
- 'web-app/src/app/core/**'
|
- 'web-app/src/app/core/**'
|
||||||
- 'web-app/src/app/layout/**'
|
- 'web-app/src/app/layout/**'
|
||||||
- 'web-app/src/app/routes/exception/**'
|
- 'web-app/src/app/routes/exception/**'
|
||||||
|
|||||||
+17
@@ -0,0 +1,17 @@
|
|||||||
|
# Licensed to the Apache Software Foundation (ASF) under one or more
|
||||||
|
# contributor license agreements. See the NOTICE file distributed with
|
||||||
|
# this work for additional information regarding copyright ownership.
|
||||||
|
# The ASF licenses this file to You under the Apache License, Version 2.0
|
||||||
|
# (the "License"); you may not use this file except in compliance with
|
||||||
|
# the License. You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
wrapperVersion=3.3.2
|
||||||
|
distributionType=only-script
|
||||||
|
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip
|
||||||
+18
-44
@@ -20,43 +20,41 @@
|
|||||||
|
|
||||||
- 💬**Suggestion**: You can also make some suggestions to the project or community to promote the healthy development of the community;
|
- 💬**Suggestion**: You can also make some suggestions to the project or community to promote the healthy development of the community;
|
||||||
|
|
||||||
More see [Contribution Types](https://allcontributors.org/docs/en/emoji-key)
|
More see [Contribution Types](https://allcontributors.org/emoji-key)
|
||||||
|
|
||||||
Even small corrections to typos are very welcome :)
|
Even small corrections to typos are very welcome :)
|
||||||
|
|
||||||
### Getting HertzBeat up and running
|
### Getting HertzBeat up and running
|
||||||
|
|
||||||
> To get HertzBeat code running on your development tools, and able to debug with breakpoints.
|
> To get HertzBeat code running on your development tools, and able to debug with breakpoints.
|
||||||
> This is a front-end and back-end separation project. To start the local code, the back-end [manager](manager) and the front-end [web-app](web-app) must be started separately.
|
> This is a front-end and back-end separation project. To start the local code, the back-end [startup](hertzbeat-startup) and the front-end [web-app](web-app) must be started separately.
|
||||||
|
|
||||||
|
|
||||||
#### Backend start
|
#### Backend start
|
||||||
|
|
||||||
1. Requires `maven3+`, `java17` and `lombok` environments
|
1. Requires `maven3+`, `java17` and `lombok` environments
|
||||||
|
|
||||||
2. (Optional) Modify the configuration file: `manager/src/main/resources/application.yml`
|
2. (Optional) Modify the configuration file: `hertzbeat-startup/src/main/resources/application.yml`
|
||||||
|
|
||||||
3. Execute under the project root directory: `mvn clean install -DskipTests`
|
3. Execute under the project root directory: `mvn clean install -DskipTests`
|
||||||
|
|
||||||
4. Add VM Options: `--add-opens=java.base/java.nio=org.apache.arrow.memory.core,ALL-UNNAMED`
|
4. Add VM Options: `--add-opens=java.base/java.nio=org.apache.arrow.memory.core,ALL-UNNAMED`
|
||||||
|
|
||||||
5. Start `springboot manager` service: `manager/src/main/java/org/apache/hertzbeat/hertzbeat-manager/Manager.java`
|
5. Start `springboot startup` service: `hertzbeat-startup/src/main/java/org/apache/hertzbeat/startup/HertzBeatApplication.java`
|
||||||
|
|
||||||
#### Frontend start
|
#### Frontend start
|
||||||
|
|
||||||
1. Need `Node Yarn` Environment, Make sure `Node.js >= 18`
|
1. Need `Node pnpm` Environment, Make sure `Node.js >= 18`
|
||||||
|
|
||||||
2. Cd to the `web-app` directory: `cd web-app`
|
2. Cd to the `web-app` directory: `cd web-app`
|
||||||
|
|
||||||
3. Install yarn if not existed `npm install -g yarn`
|
3. Install pnpm if not existed `npm install -g pnpm`
|
||||||
|
|
||||||
4. Install Dependencies: `yarn install` or `yarn install --registry=https://registry.npmmirror.com` in `web-app`
|
4. Install Dependencies: `pnpm install` or `pnpm install --registry=https://registry.npmmirror.com` in `web-app`
|
||||||
|
|
||||||
5. Install angular-cli globally: `yarn global add @angular/cli@15` or `yarn global add @angular/cli@15 --registry=https://registry.npmmirror.com`
|
5. After the local backend is started, start the local frontend in the web-app directory: `pnpm start`
|
||||||
|
|
||||||
6. After the local backend is started, start the local frontend in the web-app directory: `ng serve --open`
|
6. Browser access to localhost:4200 to start, default account/password is *admin/hertzbeat*
|
||||||
|
|
||||||
7. Browser access to localhost:4200 to start, default account/password is *admin/hertzbeat*
|
|
||||||
|
|
||||||
|
|
||||||
### Find tasks
|
### Find tasks
|
||||||
@@ -133,17 +131,6 @@ Add WeChat account `ahertzbeat` to pull you into the WeChat group.
|
|||||||
|
|
||||||
## 🥐 Architecture
|
## 🥐 Architecture
|
||||||
|
|
||||||
- **[manager](https://github.com/apache/hertzbeat/tree/master/hertzbeat-manager)** Provide monitoring management, system management basic services.
|
|
||||||
> Provides monitoring management, monitoring configuration management, system user management, etc.
|
|
||||||
- **[collector](https://github.com/apache/hertzbeat/tree/master/collector)** Provide metrics data collection services.
|
|
||||||
> Use common protocols to remotely collect and obtain peer-to-peer metrics data.
|
|
||||||
- **[warehouse](https://github.com/apache/hertzbeat/tree/master/warehouse)** Provide monitoring data warehousing services.
|
|
||||||
> Metrics data management, data query, calculation and statistics.
|
|
||||||
- **[alerter](https://github.com/apache/hertzbeat/tree/master/hertzbeat-alerter)** Provide alert service.
|
|
||||||
> Alarm calculation trigger, monitoring status linkage, alarm configuration, and alarm notification.
|
|
||||||
- **[web-app](https://github.com/apache/hertzbeat/tree/master/web-app)** Provide web ui.
|
|
||||||
> Angular Web UI.
|
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
<br>
|
<br>
|
||||||
@@ -174,43 +161,41 @@ Add WeChat account `ahertzbeat` to pull you into the WeChat group.
|
|||||||
|
|
||||||
- 💬**建议**:也可以对项目或者社区提出一些建议,促进社区的良性发展;
|
- 💬**建议**:也可以对项目或者社区提出一些建议,促进社区的良性发展;
|
||||||
|
|
||||||
更多贡献方式参见 [Contribution Types](https://allcontributors.org/docs/en/emoji-key)
|
更多贡献方式参见 [Contribution Types](https://allcontributors.org/emoji-key)
|
||||||
|
|
||||||
即便是小到错别字的修正我们也都非常欢迎 :)
|
即便是小到错别字的修正我们也都非常欢迎 :)
|
||||||
|
|
||||||
### 让 HertzBeat 运行起来
|
### 让 HertzBeat 运行起来
|
||||||
|
|
||||||
> 让 HertzBeat 的代码在您的开发工具上运行起来,并且能够断点调试。
|
> 让 HertzBeat 的代码在您的开发工具上运行起来,并且能够断点调试。
|
||||||
> 此为前后端分离项目,本地代码启动需将后端[manager](manager)和前端[web-app](web-app)分别启动生效。
|
> 此为前后端分离项目,本地代码启动需将后端[hertzbeat-startup](hertzbeat-startup)和前端[web-app](web-app)分别启动生效。
|
||||||
|
|
||||||
#### 后端启动
|
#### 后端启动
|
||||||
|
|
||||||
1. 需要 `maven3+`, `java17` 和 `lombok` 环境
|
1. 需要 `maven3+`, `java17` 和 `lombok` 环境
|
||||||
|
|
||||||
2. (可选)修改配置文件配置信息-`manager/src/main/resources/application.yml`
|
2. (可选)修改配置文件配置信息-`hertzbeat-startup/src/main/resources/application.yml`
|
||||||
|
|
||||||
3. 在项目根目录运行编译: `mvn clean install -DskipTests`
|
3. 在项目根目录运行编译: `mvn clean install -DskipTests`
|
||||||
|
|
||||||
4. 在 `jvm` 加入参数 `--add-opens=java.base/java.nio=org.apache.arrow.memory.core,ALL-UNNAMED`
|
4. 在 `jvm` 加入参数 `--add-opens=java.base/java.nio=org.apache.arrow.memory.core,ALL-UNNAMED`
|
||||||
|
|
||||||
5. 启动`springboot manager`服务 `manager/src/main/java/org/apache/hertzbeat/hertzbeat-manager/Manager.java`
|
5. 启动`springboot startup`服务 `hertzbeat-startup/src/main/java/org/apache/hertzbeat/startup/HertzBeatApplication.java`
|
||||||
|
|
||||||
|
|
||||||
#### 前端启动
|
#### 前端启动
|
||||||
|
|
||||||
1. 需要 `nodejs yarn` 环境, Make sure `Node.js >= 18`
|
1. 需要 `nodejs pnpm` 环境, Make sure `Node.js >= 18`
|
||||||
|
|
||||||
2. 进入 `web-app` 目录: `cd web-app`
|
2. 进入 `web-app` 目录: `cd web-app`
|
||||||
|
|
||||||
3. 安装yarn: `npm install -g yarn`
|
3. 安装pnpm: `npm install -g pnpm`
|
||||||
|
|
||||||
4. 在前端工程目录 `web-app` 下执行: `yarn install` or `yarn install --registry=https://registry.npmmirror.com` in `web-app`
|
4. 在前端工程目录 `web-app` 下执行: `pnpm install` or `pnpm install --registry=https://registry.npmmirror.com` in `web-app`
|
||||||
|
|
||||||
5. 全局安装 `angular-cli`: `yarn global add @angular/cli@15` or `yarn global add @angular/cli@15 --registry=https://registry.npmmirror.com`
|
5. 待本地后端启动后,在web-app目录下启动本地前端 `pnpm start`
|
||||||
|
|
||||||
6. 待本地后端启动后,在web-app目录下启动本地前端 `ng serve --open`
|
6. 浏览器访问 localhost:4200 即可开始,默认账号密码 *admin/hertzbeat*
|
||||||
|
|
||||||
7. 浏览器访问 localhost:4200 即可开始,默认账号密码 *admin/hertzbeat*
|
|
||||||
|
|
||||||
### 寻找任务
|
### 寻找任务
|
||||||
|
|
||||||
@@ -286,15 +271,4 @@ Add WeChat account `ahertzbeat` to pull you into the WeChat group.
|
|||||||
|
|
||||||
### 模块
|
### 模块
|
||||||
|
|
||||||
- **[manager](https://github.com/apache/hertzbeat/tree/master/hertzbeat-manager)** 提供监控管理,系统管理基础服务
|
|
||||||
> 提供对监控的管理,监控应用配置的管理,系统用户租户后台管理等。
|
|
||||||
- **[collector](https://github.com/apache/hertzbeat/tree/master/collector)** 提供监控数据采集服务
|
|
||||||
> 使用通用协议远程采集获取对端指标数据。
|
|
||||||
- **[warehouse](https://github.com/apache/hertzbeat/tree/master/warehouse)** 提供监控数据仓储服务
|
|
||||||
> 采集指标结果数据管理,数据落盘,查询,计算统计。
|
|
||||||
- **[alerter](https://github.com/apache/hertzbeat/tree/master/hertzbeat-alerter)** 提供告警服务
|
|
||||||
> 告警计算触发,任务状态联动,告警配置,告警通知。
|
|
||||||
- **[web-app](https://github.com/apache/hertzbeat/tree/master/web-app)** 提供可视化控制台页面
|
|
||||||
> 监控告警系统可视化控制台前端
|
|
||||||
|
|
||||||

|

|
||||||
|
|||||||
-10
@@ -1,10 +0,0 @@
|
|||||||
Apache HertzBeat (incubating) is an effort undergoing incubation at the Apache
|
|
||||||
Software Foundation (ASF), sponsored by the Apache Incubator PMC.
|
|
||||||
|
|
||||||
Incubation is required of all newly accepted projects until a further review
|
|
||||||
indicates that the infrastructure, communications, and decision making process
|
|
||||||
have stabilized in a manner consistent with other successful ASF projects.
|
|
||||||
|
|
||||||
While incubation status is not necessarily a reflection of the completeness
|
|
||||||
or stability of the code, it does indicate that the project has yet to be
|
|
||||||
fully endorsed by the ASF.
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
Apache HertzBeat (incubating)
|
Apache HertzBeat™
|
||||||
Copyright 2024-2025 The Apache Software Foundation
|
Copyright 2024-2025 The Apache Software Foundation
|
||||||
|
|
||||||
This product includes software developed at
|
This product includes software developed at
|
||||||
|
|||||||
@@ -4,11 +4,10 @@
|
|||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h4 align="center">
|
<p align="center">
|
||||||
<a href="README.md">English Document</a> | <a href="README_CN.md">中文文档</a>
|
<b>Readme</b>:
|
||||||
</h4>
|
<b><a href="README.md">English</a></b> | <a href="README_CN.md">中文</a> | <a href="README_JP.md">日本語</a>
|
||||||
|
</p>
|
||||||
> A real-time monitoring system with agentless, performance cluster, prometheus-compatible, custom monitoring and status page building capabilities.
|
|
||||||
|
|
||||||
[](https://discord.gg/Fb6M73htGr)
|
[](https://discord.gg/Fb6M73htGr)
|
||||||
[](https://www.reddit.com/r/hertzbeat/)
|
[](https://www.reddit.com/r/hertzbeat/)
|
||||||
@@ -19,6 +18,8 @@
|
|||||||
[](https://artifacthub.io/packages/search?repo=hertzbeat)
|
[](https://artifacthub.io/packages/search?repo=hertzbeat)
|
||||||
[](https://www.youtube.com/channel/UCri75zfWX0GHqJFPENEbLow)
|
[](https://www.youtube.com/channel/UCri75zfWX0GHqJFPENEbLow)
|
||||||
[](https://gitpod.io/#https://github.com/apache/hertzbeat)
|
[](https://gitpod.io/#https://github.com/apache/hertzbeat)
|
||||||
|
[](https://deepwiki.com/apache/hertzbeat)
|
||||||
|
[](https://zread.ai/apache/hertzbeat)
|
||||||
|
|
||||||
**Home: [hertzbeat.apache.org](https://hertzbeat.apache.org)**
|
**Home: [hertzbeat.apache.org](https://hertzbeat.apache.org)**
|
||||||
**Email: <a href="mailto:dev-subscribe@hertzbeat.apache.org">Mail to `dev-subscribe@hertzbeat.apache.org`</a> to subscribe mailing lists**
|
**Email: <a href="mailto:dev-subscribe@hertzbeat.apache.org">Mail to `dev-subscribe@hertzbeat.apache.org`</a> to subscribe mailing lists**
|
||||||
@@ -26,28 +27,23 @@
|
|||||||
|
|
||||||
## 🎡 <font color="green">Introduction</font>
|
## 🎡 <font color="green">Introduction</font>
|
||||||
|
|
||||||
[Apache HertzBeat](https://github.com/apache/hertzbeat) (incubating) is an easy-to-use, open source, real-time monitoring system with agentless, high performance cluster, prometheus-compatible, offers powerful custom monitoring and status page building capabilities.
|
[Apache HertzBeat™](https://github.com/apache/hertzbeat) is an AI-powered next-generation open source real-time observability system. Unified metrics and logs collection, centralized alerting distribution, intelligent management and analysis. No Agent required, high performance cluster, provides powerful custom monitoring and status page building capabilities.
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
|
||||||
* Combines **monitoring, alarm, and notification** features into one platform, and supports monitoring for web service, program, database, cache, os, webserver, middleware, bigdata, cloud-native, network, custom and more.
|
- Integrates **collection + analysis + alerting + notification** into one platform, with new AI-powered interactions and features under HertzBeat AI, and built-in MCP Server capabilities.
|
||||||
* Easy to use and agentless, web-based and with one-click monitoring and alerting, zero learning curve.
|
- Unified metrics platform, agentless, Prometheus-compatible, supports application services, programs, databases, caches, operating systems, big data, middleware, web servers, cloud-native, networks, custom monitoring and more.
|
||||||
* Makes protocols such as `Http, Jmx, Ssh, Snmp, Jdbc, Prometheus` configurable, allowing you to collect any metrics by simply configuring the template `YML` file online. Imagine being able to quickly adapt to a new monitoring type like K8s or Docker simply by configuring online with HertzBeat.
|
- Unified logging platform, seamlessly integrates multiple log sources through OTLP protocol for reporting.
|
||||||
* Compatible with the `Prometheus` ecosystem and more, can monitoring what `Prometheus` can monitoring with few clicks on webui.
|
- Unified alerting platform, integrates internal alerts with various external alert sources, unified alert processing and analysis, flexible real-time and periodic threshold rules, grouping convergence, silence, suppression, etc.
|
||||||
* High performance, supports horizontal expansion of multi-collector clusters, multi-isolated network monitoring and cloud-edge collaboration.
|
- Unified message distribution, alerts processed by the alerting platform are distributed via `Email` `Discord` `Slack` `Telegram` `DingTalk` `WeChat` `FeiShu` `SMS` `Webhook` `ServerChan` and other methods.
|
||||||
* Provides flexible alarm threshold rules and timely notifications delivered via `Discord` `Slack` `Telegram` `Email` `Dingtalk` `WeChat` `FeiShu` `Webhook` `SMS` `ServerChan`.
|
- Makes protocols such as `Http, Jmx, Ssh, Snmp, Jdbc, Prometheus` configurable, allowing you to collect any metrics by simply configuring the template `YML` file online. Imagine being able to quickly adapt to a new monitoring type like K8s or Docker simply by configuring online with HertzBeat.
|
||||||
* Provides powerful status page building capabilities, easily communicate the real-time status of your service to users.
|
- High performance, supports horizontal expansion of multi-collector clusters, multi-isolated network monitoring and cloud-edge collaboration.
|
||||||
|
- Provides powerful status page building capabilities, easily communicate the real-time status of your service to users.
|
||||||
|
|
||||||
> HertzBeat's powerful customization, multi-type support, high performance, easy expansion, and low coupling, aims to help users quickly build their own monitoring system.
|
> HertzBeat's unified platform, AI intelligence, powerful customization, multi-type support, high performance, and easy expansion, aims to help users quickly and conveniently achieve observability requirements.
|
||||||
|
|
||||||
----
|
----
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
----
|
----
|
||||||
|
|
||||||
## 🥐 Architecture
|
## 🥐 Architecture
|
||||||
@@ -56,8 +52,8 @@
|
|||||||
|
|
||||||
## ⛄ Supported
|
## ⛄ Supported
|
||||||
|
|
||||||
> We define all monitoring collection types such as `mysql`, `jvm`, and `k8s` as `YML` monitoring templates, allowing users to import them to support corresponding types of monitoring.
|
> We define all metrics collection types such as `mysql`, `jvm`, and `k8s` as `YML` templates, allowing users to import them to support corresponding types of monitoring.
|
||||||
> Welcome everyone to contribute your customized general monitoring type YML template during use.
|
> Welcome everyone to contribute your customized general metrics type YML template during use.
|
||||||
|
|
||||||
|
|
||||||
- [Website](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-website.yml), [Port Telnet](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-port.yml),
|
- [Website](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-website.yml), [Port Telnet](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-port.yml),
|
||||||
@@ -159,8 +155,8 @@ Detailed config refer to [Install HertzBeat via Package](https://hertzbeat.apach
|
|||||||
|
|
||||||
##### 3:Start via source code
|
##### 3:Start via source code
|
||||||
|
|
||||||
1. Local source code debugging needs to start the back-end project `manager` and the front-end project `web-app`.
|
1. Local source code debugging needs to start the back-end project `hertzbeat-startup` and the front-end project `web-app`.
|
||||||
2. Backend:need `maven3+`, `java17`, `lombok`, add VM options in IDE: ` --add-opens=java.base/java.nio=org.apache.arrow.memory.core,ALL-UNNAMED `, then start the `manager` service.
|
2. Backend:need `maven3+`, `java17`, `lombok`, add VM options in IDE: ` --add-opens=java.base/java.nio=org.apache.arrow.memory.core,ALL-UNNAMED `, then start the `hertzbeat-startup` service.
|
||||||
3. Web:need `nodejs npm angular-cli` environment, Run `ng serve --open` in `web-app` directory after backend startup.
|
3. Web:need `nodejs npm angular-cli` environment, Run `ng serve --open` in `web-app` directory after backend startup.
|
||||||
4. Access `http://localhost:4200` to start, default account: `admin/hertzbeat`
|
4. Access `http://localhost:4200` to start, default account: `admin/hertzbeat`
|
||||||
|
|
||||||
@@ -359,7 +355,7 @@ Thanks to these wonderful people, welcome to join us:
|
|||||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/lisongning"><img src="https://avatars.githubusercontent.com/u/93140178?v=4?s=100" width="100px;" alt="lisongning"/><br /><sub><b>lisongning</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=lisongning" title="Code">💻</a></td>
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/lisongning"><img src="https://avatars.githubusercontent.com/u/93140178?v=4?s=100" width="100px;" alt="lisongning"/><br /><sub><b>lisongning</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=lisongning" title="Code">💻</a></td>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/YutingNie"><img src="https://avatars.githubusercontent.com/u/104416402?v=4?s=100" width="100px;" alt="YutingNie"/><br /><sub><b>YutingNie</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=YutingNie" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=YutingNie" title="Documentation">📖</a> <a href="#design-YutingNie" title="Design">🎨</a></td>
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/YutingNie"><img src="https://avatars.githubusercontent.com/u/104416402?v=4?s=100" width="100px;" alt="YutingNie"/><br /><sub><b>YutingNie</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=YutingNie" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=YutingNie" title="Documentation">📖</a> <a href="#design-YutingNie" title="Design">🎨</a></td>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/mikezzb"><img src="https://avatars.githubusercontent.com/u/23418428?v=4?s=100" width="100px;" alt="Mike Zhou"/><br /><sub><b>Mike Zhou</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=mikezzb" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=mikezzb" title="Documentation">📖</a> <a href="#design-mikezzb" title="Design">🎨</a></td>
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/mikezzb"><img src="https://avatars.githubusercontent.com/u/23418428?v=4?s=100" width="100px;" alt="Mike Zhou"/><br /><sub><b>Mike Zhou</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=mikezzb" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=mikezzb" title="Documentation">📖</a> <a href="#design-mikezzb" title="Design">🎨</a></td>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/a-little-fool"><img src="https://avatars.githubusercontent.com/u/105542329?v=4?s=100" width="100px;" alt="小笨蛋"/><br /><sub><b>小笨蛋</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=a-little-fool" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=a-little-fool" title="Documentation">📖</a> <a href="#blog-a-little-fool" title="Blogposts">📝</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Aa-little-fool" title="Bug reports">🐛</a> <a href="#design-a-little-fool" title="Design">🎨</a></td>
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/lynx009"><img src="https://avatars.githubusercontent.com/u/105542329?v=4?s=100" width="100px;" alt="lynx009"/><br /><sub><b>lynx009</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=lynx009" title="Documentation">📖</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/littlezhongzer"><img src="https://avatars.githubusercontent.com/u/33685289?v=4?s=100" width="100px;" alt="littlezhongzer"/><br /><sub><b>littlezhongzer</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=littlezhongzer" title="Code">💻</a></td>
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/littlezhongzer"><img src="https://avatars.githubusercontent.com/u/33685289?v=4?s=100" width="100px;" alt="littlezhongzer"/><br /><sub><b>littlezhongzer</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=littlezhongzer" title="Code">💻</a></td>
|
||||||
@@ -383,7 +379,7 @@ Thanks to these wonderful people, welcome to join us:
|
|||||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/1036664317"><img src="https://avatars.githubusercontent.com/u/7696697?v=4?s=100" width="100px;" alt="moghn"/><br /><sub><b>moghn</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=1036664317" title="Documentation">📖</a></td>
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/1036664317"><img src="https://avatars.githubusercontent.com/u/7696697?v=4?s=100" width="100px;" alt="moghn"/><br /><sub><b>moghn</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=1036664317" title="Documentation">📖</a></td>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/xiaoguolong"><img src="https://avatars.githubusercontent.com/u/33684988?v=4?s=100" width="100px;" alt="xiaoguolong"/><br /><sub><b>xiaoguolong</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=xiaoguolong" title="Code">💻</a></td>
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/xiaoguolong"><img src="https://avatars.githubusercontent.com/u/33684988?v=4?s=100" width="100px;" alt="xiaoguolong"/><br /><sub><b>xiaoguolong</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=xiaoguolong" title="Code">💻</a></td>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Clownsw"><img src="https://avatars.githubusercontent.com/u/28394742?v=4?s=100" width="100px;" alt="Smliexx"/><br /><sub><b>Smliexx</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Clownsw" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3AClownsw" title="Bug reports">🐛</a></td>
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Clownsw"><img src="https://avatars.githubusercontent.com/u/28394742?v=4?s=100" width="100px;" alt="Smliexx"/><br /><sub><b>Smliexx</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Clownsw" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3AClownsw" title="Bug reports">🐛</a></td>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Calvin979"><img src="https://avatars.githubusercontent.com/u/131688897?v=4?s=100" width="100px;" alt="Naruse"/><br /><sub><b>Naruse</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Calvin979" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=Calvin979" title="Code">💻</a> <a href="#design-Calvin979" title="Design">🎨</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3ACalvin979" title="Bug reports">🐛</a> <a href="https://github.com/apache/hertzbeat/commits?author=Calvin979" title="Tests">⚠️</a></td>
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Calvin979"><img src="https://avatars.githubusercontent.com/u/131688897?v=4?s=100" width="100px;" alt="Calvin"/><br /><sub><b>Calvin</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Calvin979" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=Calvin979" title="Code">💻</a> <a href="#design-Calvin979" title="Design">🎨</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3ACalvin979" title="Bug reports">🐛</a> <a href="https://github.com/apache/hertzbeat/commits?author=Calvin979" title="Tests">⚠️</a></td>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/bbelide2"><img src="https://avatars.githubusercontent.com/u/26840796?v=4?s=100" width="100px;" alt="Bala Sukesh"/><br /><sub><b>Bala Sukesh</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=bbelide2" title="Code">💻</a></td>
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/bbelide2"><img src="https://avatars.githubusercontent.com/u/26840796?v=4?s=100" width="100px;" alt="Bala Sukesh"/><br /><sub><b>Bala Sukesh</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=bbelide2" title="Code">💻</a></td>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/jinyaoMa"><img src="https://avatars.githubusercontent.com/u/25066570?v=4?s=100" width="100px;" alt="Jinyao Ma"/><br /><sub><b>Jinyao Ma</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=jinyaoMa" title="Code">💻</a></td>
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/jinyaoMa"><img src="https://avatars.githubusercontent.com/u/25066570?v=4?s=100" width="100px;" alt="Jinyao Ma"/><br /><sub><b>Jinyao Ma</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=jinyaoMa" title="Code">💻</a></td>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://linuxsuren.github.io/open-source-best-practice/"><img src="https://avatars.githubusercontent.com/u/1450685?v=4?s=100" width="100px;" alt="Rick"/><br /><sub><b>Rick</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=LinuxSuRen" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=LinuxSuRen" title="Tests">⚠️</a></td>
|
<td align="center" valign="top" width="14.28%"><a href="https://linuxsuren.github.io/open-source-best-practice/"><img src="https://avatars.githubusercontent.com/u/1450685?v=4?s=100" width="100px;" alt="Rick"/><br /><sub><b>Rick</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=LinuxSuRen" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=LinuxSuRen" title="Tests">⚠️</a></td>
|
||||||
@@ -521,6 +517,32 @@ Thanks to these wonderful people, welcome to join us:
|
|||||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/boyucjz"><img src="https://avatars.githubusercontent.com/u/18730041?v=4?s=100" width="100px;" alt="boyucjz"/><br /><sub><b>boyucjz</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=boyucjz" title="Code">💻</a></td>
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/boyucjz"><img src="https://avatars.githubusercontent.com/u/18730041?v=4?s=100" width="100px;" alt="boyucjz"/><br /><sub><b>boyucjz</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=boyucjz" title="Code">💻</a></td>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Cyanty"><img src="https://avatars.githubusercontent.com/u/153884653?v=4?s=100" width="100px;" alt="Cyanty"/><br /><sub><b>Cyanty</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Cyanty" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=Cyanty" title="Documentation">📖</a></td>
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Cyanty"><img src="https://avatars.githubusercontent.com/u/153884653?v=4?s=100" width="100px;" alt="Cyanty"/><br /><sub><b>Cyanty</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Cyanty" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=Cyanty" title="Documentation">📖</a></td>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/KevinLLF"><img src="https://avatars.githubusercontent.com/u/85452733?v=4?s=100" width="100px;" alt="Jay丿167"/><br /><sub><b>Jay丿167</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=KevinLLF" title="Code">💻</a></td>
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/KevinLLF"><img src="https://avatars.githubusercontent.com/u/85452733?v=4?s=100" width="100px;" alt="Jay丿167"/><br /><sub><b>Jay丿167</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=KevinLLF" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Duansg"><img src="https://avatars.githubusercontent.com/u/112607719?v=4?s=100" width="100px;" alt="Duansg"/><br /><sub><b>Duansg</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Duansg" title="Documentation">📖</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/xiaomizhou2"><img src="https://avatars.githubusercontent.com/u/47807926?v=4?s=100" width="100px;" alt="zhangyaxi"/><br /><sub><b>zhangyaxi</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=xiaomizhou2" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=xiaomizhou2" title="Tests">⚠️</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/RainBondsongyg"><img src="https://avatars.githubusercontent.com/u/94501396?v=4?s=100" width="100px;" alt="songyg"/><br /><sub><b>songyg</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=RainBondsongyg" title="Documentation">📖</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/lx1229"><img src="https://avatars.githubusercontent.com/u/44620005?v=4?s=100" width="100px;" alt="Liuxin"/><br /><sub><b>Liuxin</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=lx1229" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/yy549159265"><img src="https://avatars.githubusercontent.com/u/40821310?v=4?s=100" width="100px;" alt="yy549159265"/><br /><sub><b>yy549159265</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=yy549159265" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=yy549159265" title="Tests">⚠️</a> <a href="#design-yy549159265" title="Design">🎨</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/cto-huhang"><img src="https://avatars.githubusercontent.com/u/53338629?v=4?s=100" width="100px;" alt="cto-huhang"/><br /><sub><b>cto-huhang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=cto-huhang" title="Documentation">📖</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Saramanda9988"><img src="https://avatars.githubusercontent.com/u/176664901?v=4?s=100" width="100px;" alt="LunaRain_079"/><br /><sub><b>LunaRain_079</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Saramanda9988" title="Documentation">📖</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/delei"><img src="https://avatars.githubusercontent.com/u/17263766?v=4?s=100" width="100px;" alt="DeleiGuo"/><br /><sub><b>DeleiGuo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=delei" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=delei" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=delei" title="Tests">⚠️</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Adelei" title="Bug reports">🐛</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/chingjustwe"><img src="https://avatars.githubusercontent.com/u/13643747?v=4?s=100" width="100px;" alt="Rocky, Chi"/><br /><sub><b>Rocky, Chi</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=chingjustwe" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/rowankid"><img src="https://avatars.githubusercontent.com/u/18652781?v=4?s=100" width="100px;" alt="Wenqi Luo"/><br /><sub><b>Wenqi Luo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/issues?q=author%3Arowankid" title="Bug reports">🐛</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/tuzuy"><img src="https://avatars.githubusercontent.com/u/95274591?v=4?s=100" width="100px;" alt="tuzuy"/><br /><sub><b>tuzuy</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=tuzuy" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/carlpinto25"><img src="https://avatars.githubusercontent.com/u/117299909?v=4?s=100" width="100px;" alt="carl pinto"/><br /><sub><b>carl pinto</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=carlpinto25" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://cxhello.top/"><img src="https://avatars.githubusercontent.com/u/49056040?v=4?s=100" width="100px;" alt="cxhello"/><br /><sub><b>cxhello</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=cxhello" title="Documentation">📖</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/jl15988"><img src="https://avatars.githubusercontent.com/u/70638770?v=4?s=100" width="100px;" alt="会功夫的李白"/><br /><sub><b>会功夫的李白</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=jl15988" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://blog.aytop.cloud/"><img src="https://avatars.githubusercontent.com/u/37127008?v=4?s=100" width="100px;" alt="Albert.Yang"/><br /><sub><b>Albert.Yang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=AlbertYang0801" title="Code">💻</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://blog.tokenlen.top/"><img src="https://avatars.githubusercontent.com/u/150590575?v=4?s=100" width="100px;" alt="zhou yong kang"/><br /><sub><b>zhou yong kang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=mengnankkkk" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/warrobe"><img src="https://avatars.githubusercontent.com/u/89446159?v=4?s=100" width="100px;" alt="warrobe"/><br /><sub><b>warrobe</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=warrobe" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Jetiaime"><img src="https://avatars.githubusercontent.com/u/93769000?v=4?s=100" width="100px;" alt="TeAmo"/><br /><sub><b>TeAmo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Jetiaime" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/pentium100"><img src="https://avatars.githubusercontent.com/u/27917?v=4?s=100" width="100px;" alt="pentium100"/><br /><sub><b>pentium100</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=pentium100" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/dedyks"><img src="https://avatars.githubusercontent.com/u/23741665?v=4?s=100" width="100px;" alt="Dedy Kurniawan Santoso"/><br /><sub><b>Dedy Kurniawan Santoso</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=dedyks" title="Code">💻</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
@@ -542,7 +564,7 @@ WeChat Group : Add friend `ahertzbeat` and invite to the group.
|
|||||||
|
|
||||||
WeChat Public : Search ID `usthecom`.
|
WeChat Public : Search ID `usthecom`.
|
||||||
|
|
||||||
[QQ Group](https://jq.qq.com/?_wv=1027&k=Bud9OzdI) : Group num `630061200`
|
[QQ Group](https://qm.qq.com/q/xxqecSC2cw) : Group num `1035688434`
|
||||||
|
|
||||||
[Github Discussion](https://github.com/apache/hertzbeat/discussions)
|
[Github Discussion](https://github.com/apache/hertzbeat/discussions)
|
||||||
|
|
||||||
@@ -564,8 +586,8 @@ HertzBeat is built on so many great open source projects, thanks to them!
|
|||||||
<p align="left">
|
<p align="left">
|
||||||
<img src="./home/static/img/home/cncf-landscape-left-logo.svg" width="300"> <img src="./home/static/img/home/cncf-right-logo.svg" width="345" />
|
<img src="./home/static/img/home/cncf-landscape-left-logo.svg" width="300"> <img src="./home/static/img/home/cncf-right-logo.svg" width="345" />
|
||||||
<br /><br />
|
<br /><br />
|
||||||
HertzBeat has been included in the <a href="https://landscape.cncf.io/guide?item=observability-and-analysis--monitoring--hertzbeat#observability-and-analysis--monitoring">
|
HertzBeat has been included in the <a href="https://landscape.cncf.io/?item=observability-and-analysis--observability--hertzbeat">
|
||||||
CNCF Observability And Analysis - Monitoring Landscape.</a>
|
CNCF Observability And Analysis - Observability Landscape.</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
## 🛡️ License
|
## 🛡️ License
|
||||||
|
|||||||
+49
-29
@@ -4,11 +4,10 @@
|
|||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h4 align="center">
|
<p align="center">
|
||||||
<a href="README_CN.md">中文文档</a> | <a href="README.md">English Document</a>
|
<b>Readme</b>:
|
||||||
</h4>
|
<a href="README.md">English</a> | <b><a href="README_CN.md">中文</a></b> | <a href="README_JP.md">日本語</a>
|
||||||
|
</p>
|
||||||
> 实时监控系统,无需 Agent,性能集群,兼容 Prometheus,自定义监控和状态页构建能力。
|
|
||||||
|
|
||||||
[](https://discord.gg/Fb6M73htGr)
|
[](https://discord.gg/Fb6M73htGr)
|
||||||
[](https://www.reddit.com/r/hertzbeat/)
|
[](https://www.reddit.com/r/hertzbeat/)
|
||||||
@@ -27,29 +26,24 @@
|
|||||||
|
|
||||||
## 🎡 <font color="green">介绍</font>
|
## 🎡 <font color="green">介绍</font>
|
||||||
|
|
||||||
[Apache HertzBeat](https://github.com/apache/hertzbeat) (incubating)是一个易用友好的开源实时监控告警系统,无需 Agent,高性能集群,兼容 Prometheus,提供强大的自定义监控和状态页构建能力。
|
[Apache HertzBeat™](https://github.com/apache/hertzbeat) 是 AI 驱动的下一代开源实时观测系统。指标日志统一收集,告警一站分发,智能管控分析。无需 Agent,高性能集群,提供强大的自定义监控和状态页构建能力。
|
||||||
|
|
||||||
### 特点
|
### 特点
|
||||||
|
|
||||||
- 集 **监控+告警+通知** 为一体,支持对应用服务,应用程序,数据库,缓存,操作系统,大数据,中间件,Web 服务器,云原生,网络,自定义等监控阈值告警通知一步到位。
|
- 集```采集+分析+告警+通知```为一体,HertzBeat AI 驱动下的新交互与功能,也内置 MCP Server 对外能力。
|
||||||
- 易用友好,无需 `Agent`,全 `WEB` 页面操作,鼠标点一点就能监控告警,零上手学习成本。
|
- 统一的指标平台,无需 Agent,兼容 Prometheus,支持应用服务,程序,数据库,缓存,操作系统,大数据,中间件,Web 服务器,云原生,网络,自定义等。
|
||||||
- 将 `Http, Jmx, Ssh, Snmp, Jdbc, Prometheus` 等协议规范可配置化,只需在浏览器配置监控模版 `YML` 就能使用这些协议去自定义采集想要的指标。您相信只需配置下就能立刻适配一款 `K8s` 或 `Docker` 等新的监控类型吗?
|
- 统一的日志平台,通过 OTLP 协议多日志源无缝对接上报。
|
||||||
- 兼容 `Prometheus` 的系统生态并且更多,只需页面操作就可以监控 `Prometheus` 所能监控的。
|
- 统一的告警平台,内部告警与外部多种告警源集成接入,统一告警处理分析,灵活的实时与周期阈值规则,分组收敛,静默,抑制等。
|
||||||
|
- 统一的消息分发,告警平台处理后通过 `邮件` `Discord` `Slack` `Telegram` `钉钉` `微信` `飞书` `短信` `Webhook` `Server酱` 等方式分发通知。
|
||||||
|
- 将 `Http, Jmx, Ssh, Snmp, Jdbc, Prometheus` 等协议规范可配置化,只需配置模板 `YML` 就能自定义采集指标。您相信只需简单配置即可快速适配一款 `K8s` 或 `Docker` 等新的监控类型吗?
|
||||||
- 高性能,支持多采集器集群横向扩展,支持多隔离网络监控,云边协同。
|
- 高性能,支持多采集器集群横向扩展,支持多隔离网络监控,云边协同。
|
||||||
- 自由的告警阈值规则,`邮件` `Discord` `Slack` `Telegram` `钉钉` `微信` `飞书` `短信` `Webhook` `Server酱` 等方式消息及时送达。
|
|
||||||
- 提供强大的状态页构建能力,轻松向用户传达您产品服务的实时状态。
|
- 提供强大的状态页构建能力,轻松向用户传达您产品服务的实时状态。
|
||||||
|
|
||||||
|
|
||||||
> `HertzBeat`的强大自定义,多类型支持,高性能,易扩展,低耦合,希望能帮助用户快速搭建自有监控系统。
|
> `HertzBeat`的统一平台,AI智能,强大自定义,多类型支持,高性能,易扩展,希望能帮助用户快速方便实现观测需求。
|
||||||
|
|
||||||
----
|
----
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
----
|
----
|
||||||
|
|
||||||
## 🥐 模块
|
## 🥐 模块
|
||||||
@@ -58,8 +52,8 @@
|
|||||||
|
|
||||||
## ⛄ 已支持
|
## ⛄ 已支持
|
||||||
|
|
||||||
> 我们将监控采集类型(mysql,jvm,k8s)都定义为 `yml` 监控模版,用户可以导入这些模版来支持对应类型的监控!
|
> 我们将监控采集类型(mysql,jvm,k8s)都定义为 `yml` 模板,用户可以导入这些模板来支持对应类型的监控!
|
||||||
> 欢迎大家一起贡献你使用过程中自定义的通用监控类型监控模版。
|
> 欢迎大家一起贡献你使用过程中自定义的通用指标模板。
|
||||||
|
|
||||||
- [Website](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-website.yml), [Port Telnet](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-port.yml),
|
- [Website](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-website.yml), [Port Telnet](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-port.yml),
|
||||||
[Http Api](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-api.yml), [Ping Connect](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-ping.yml),
|
[Http Api](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-api.yml), [Ping Connect](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-ping.yml),
|
||||||
@@ -97,7 +91,7 @@
|
|||||||
- [CiscoSwitch](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-cisco_switch.yml), [HpeSwitch](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-hpe_switch.yml),
|
- [CiscoSwitch](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-cisco_switch.yml), [HpeSwitch](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-hpe_switch.yml),
|
||||||
[HuaweiSwitch](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-huawei_switch.yml), [TpLinkSwitch](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-tplink_switch.yml),
|
[HuaweiSwitch](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-huawei_switch.yml), [TpLinkSwitch](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-tplink_switch.yml),
|
||||||
[H3cSwitch](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-h3c_switch.yml)
|
[H3cSwitch](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-h3c_switch.yml)
|
||||||
- 和更多自定义监控模版。
|
- 和更多自定义指标模板。
|
||||||
- 通知支持 `Discord` `Slack` `Telegram` `邮件` `钉钉` `微信` `飞书` `短信` `Webhook` `Server酱`。
|
- 通知支持 `Discord` `Slack` `Telegram` `邮件` `钉钉` `微信` `飞书` `短信` `Webhook` `Server酱`。
|
||||||
|
|
||||||
## 🐕 快速开始
|
## 🐕 快速开始
|
||||||
@@ -125,7 +119,7 @@
|
|||||||
|
|
||||||
- `-e IDENTITY=custom-collector-name` : 配置此采集器的唯一性标识符名称,多个采集器名称不能相同,建议自定义英文名称。
|
- `-e IDENTITY=custom-collector-name` : 配置此采集器的唯一性标识符名称,多个采集器名称不能相同,建议自定义英文名称。
|
||||||
- `-e MODE=public` : 配置运行模式(public or private), 公共集群模式或私有云边模式。
|
- `-e MODE=public` : 配置运行模式(public or private), 公共集群模式或私有云边模式。
|
||||||
- `-e MANAGER_HOST=127.0.0.1` : 配置连接主 HertaBeat 服务的对外 IP。
|
- `-e MANAGER_HOST=127.0.0.1` : 配置连接主 HertzBeat 服务的对外 IP。
|
||||||
- `-e MANAGER_PORT=1158` : 配置连接主 HertzBeat 服务的对外端口,默认1158。
|
- `-e MANAGER_PORT=1158` : 配置连接主 HertzBeat 服务的对外端口,默认1158。
|
||||||
|
|
||||||
|
|
||||||
@@ -158,8 +152,8 @@
|
|||||||
|
|
||||||
##### 方式三:本地代码启动
|
##### 方式三:本地代码启动
|
||||||
|
|
||||||
1. 此为前后端分离项目,本地代码调试需要分别启动后端工程 `manager` 和前端工程 `web-app`
|
1. 此为前后端分离项目,本地代码调试需要分别启动后端工程 `hertzbeat-startup` 和前端工程 `web-app`
|
||||||
2. 后端:需要 `maven3+`, `java17` 和 `lombok` 环境,修改 `YML` 配置信息,添加JVM参数`--add-opens=java.base/java.nio=org.apache.arrow.memory.core,ALL-UNNAMED`后启动 `manager` 服务即可。
|
2. 后端:需要 `maven3+`, `java17` 和 `lombok` 环境,修改 `YML` 配置信息,添加JVM参数`--add-opens=java.base/java.nio=org.apache.arrow.memory.core,ALL-UNNAMED`后启动 `hertzbeat-startup` 服务即可。
|
||||||
3. 前端:需要 `nodejs npm angular-cli`环境,待本地后端启动后,在 `web-app` 目录下启动 `ng serve --open`
|
3. 前端:需要 `nodejs npm angular-cli`环境,待本地后端启动后,在 `web-app` 目录下启动 `ng serve --open`
|
||||||
4. 浏览器访问 `http://localhost:4200` 即可开始,默认账号密码 `admin/hertzbeat`
|
4. 浏览器访问 `http://localhost:4200` 即可开始,默认账号密码 `admin/hertzbeat`
|
||||||
|
|
||||||
@@ -358,7 +352,7 @@ Thanks these wonderful people, welcome to join us:
|
|||||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/lisongning"><img src="https://avatars.githubusercontent.com/u/93140178?v=4?s=100" width="100px;" alt="lisongning"/><br /><sub><b>lisongning</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=lisongning" title="Code">💻</a></td>
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/lisongning"><img src="https://avatars.githubusercontent.com/u/93140178?v=4?s=100" width="100px;" alt="lisongning"/><br /><sub><b>lisongning</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=lisongning" title="Code">💻</a></td>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/YutingNie"><img src="https://avatars.githubusercontent.com/u/104416402?v=4?s=100" width="100px;" alt="YutingNie"/><br /><sub><b>YutingNie</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=YutingNie" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=YutingNie" title="Documentation">📖</a> <a href="#design-YutingNie" title="Design">🎨</a></td>
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/YutingNie"><img src="https://avatars.githubusercontent.com/u/104416402?v=4?s=100" width="100px;" alt="YutingNie"/><br /><sub><b>YutingNie</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=YutingNie" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=YutingNie" title="Documentation">📖</a> <a href="#design-YutingNie" title="Design">🎨</a></td>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/mikezzb"><img src="https://avatars.githubusercontent.com/u/23418428?v=4?s=100" width="100px;" alt="Mike Zhou"/><br /><sub><b>Mike Zhou</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=mikezzb" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=mikezzb" title="Documentation">📖</a> <a href="#design-mikezzb" title="Design">🎨</a></td>
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/mikezzb"><img src="https://avatars.githubusercontent.com/u/23418428?v=4?s=100" width="100px;" alt="Mike Zhou"/><br /><sub><b>Mike Zhou</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=mikezzb" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=mikezzb" title="Documentation">📖</a> <a href="#design-mikezzb" title="Design">🎨</a></td>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/a-little-fool"><img src="https://avatars.githubusercontent.com/u/105542329?v=4?s=100" width="100px;" alt="小笨蛋"/><br /><sub><b>小笨蛋</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=a-little-fool" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=a-little-fool" title="Documentation">📖</a> <a href="#blog-a-little-fool" title="Blogposts">📝</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Aa-little-fool" title="Bug reports">🐛</a> <a href="#design-a-little-fool" title="Design">🎨</a></td>
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/lynx009"><img src="https://avatars.githubusercontent.com/u/105542329?v=4?s=100" width="100px;" alt="lynx009"/><br /><sub><b>lynx009</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=lynx009" title="Documentation">📖</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/littlezhongzer"><img src="https://avatars.githubusercontent.com/u/33685289?v=4?s=100" width="100px;" alt="littlezhongzer"/><br /><sub><b>littlezhongzer</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=littlezhongzer" title="Code">💻</a></td>
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/littlezhongzer"><img src="https://avatars.githubusercontent.com/u/33685289?v=4?s=100" width="100px;" alt="littlezhongzer"/><br /><sub><b>littlezhongzer</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=littlezhongzer" title="Code">💻</a></td>
|
||||||
@@ -382,7 +376,7 @@ Thanks these wonderful people, welcome to join us:
|
|||||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/1036664317"><img src="https://avatars.githubusercontent.com/u/7696697?v=4?s=100" width="100px;" alt="moghn"/><br /><sub><b>moghn</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=1036664317" title="Documentation">📖</a></td>
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/1036664317"><img src="https://avatars.githubusercontent.com/u/7696697?v=4?s=100" width="100px;" alt="moghn"/><br /><sub><b>moghn</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=1036664317" title="Documentation">📖</a></td>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/xiaoguolong"><img src="https://avatars.githubusercontent.com/u/33684988?v=4?s=100" width="100px;" alt="xiaoguolong"/><br /><sub><b>xiaoguolong</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=xiaoguolong" title="Code">💻</a></td>
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/xiaoguolong"><img src="https://avatars.githubusercontent.com/u/33684988?v=4?s=100" width="100px;" alt="xiaoguolong"/><br /><sub><b>xiaoguolong</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=xiaoguolong" title="Code">💻</a></td>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Clownsw"><img src="https://avatars.githubusercontent.com/u/28394742?v=4?s=100" width="100px;" alt="Smliexx"/><br /><sub><b>Smliexx</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Clownsw" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3AClownsw" title="Bug reports">🐛</a></td>
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Clownsw"><img src="https://avatars.githubusercontent.com/u/28394742?v=4?s=100" width="100px;" alt="Smliexx"/><br /><sub><b>Smliexx</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Clownsw" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3AClownsw" title="Bug reports">🐛</a></td>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Calvin979"><img src="https://avatars.githubusercontent.com/u/131688897?v=4?s=100" width="100px;" alt="Naruse"/><br /><sub><b>Naruse</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Calvin979" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=Calvin979" title="Code">💻</a> <a href="#design-Calvin979" title="Design">🎨</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3ACalvin979" title="Bug reports">🐛</a> <a href="https://github.com/apache/hertzbeat/commits?author=Calvin979" title="Tests">⚠️</a></td>
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Calvin979"><img src="https://avatars.githubusercontent.com/u/131688897?v=4?s=100" width="100px;" alt="Calvin"/><br /><sub><b>Calvin</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Calvin979" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=Calvin979" title="Code">💻</a> <a href="#design-Calvin979" title="Design">🎨</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3ACalvin979" title="Bug reports">🐛</a> <a href="https://github.com/apache/hertzbeat/commits?author=Calvin979" title="Tests">⚠️</a></td>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/bbelide2"><img src="https://avatars.githubusercontent.com/u/26840796?v=4?s=100" width="100px;" alt="Bala Sukesh"/><br /><sub><b>Bala Sukesh</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=bbelide2" title="Code">💻</a></td>
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/bbelide2"><img src="https://avatars.githubusercontent.com/u/26840796?v=4?s=100" width="100px;" alt="Bala Sukesh"/><br /><sub><b>Bala Sukesh</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=bbelide2" title="Code">💻</a></td>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/jinyaoMa"><img src="https://avatars.githubusercontent.com/u/25066570?v=4?s=100" width="100px;" alt="Jinyao Ma"/><br /><sub><b>Jinyao Ma</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=jinyaoMa" title="Code">💻</a></td>
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/jinyaoMa"><img src="https://avatars.githubusercontent.com/u/25066570?v=4?s=100" width="100px;" alt="Jinyao Ma"/><br /><sub><b>Jinyao Ma</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=jinyaoMa" title="Code">💻</a></td>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://linuxsuren.github.io/open-source-best-practice/"><img src="https://avatars.githubusercontent.com/u/1450685?v=4?s=100" width="100px;" alt="Rick"/><br /><sub><b>Rick</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=LinuxSuRen" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=LinuxSuRen" title="Tests">⚠️</a></td>
|
<td align="center" valign="top" width="14.28%"><a href="https://linuxsuren.github.io/open-source-best-practice/"><img src="https://avatars.githubusercontent.com/u/1450685?v=4?s=100" width="100px;" alt="Rick"/><br /><sub><b>Rick</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=LinuxSuRen" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=LinuxSuRen" title="Tests">⚠️</a></td>
|
||||||
@@ -520,6 +514,32 @@ Thanks these wonderful people, welcome to join us:
|
|||||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/boyucjz"><img src="https://avatars.githubusercontent.com/u/18730041?v=4?s=100" width="100px;" alt="boyucjz"/><br /><sub><b>boyucjz</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=boyucjz" title="Code">💻</a></td>
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/boyucjz"><img src="https://avatars.githubusercontent.com/u/18730041?v=4?s=100" width="100px;" alt="boyucjz"/><br /><sub><b>boyucjz</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=boyucjz" title="Code">💻</a></td>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Cyanty"><img src="https://avatars.githubusercontent.com/u/153884653?v=4?s=100" width="100px;" alt="Cyanty"/><br /><sub><b>Cyanty</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Cyanty" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=Cyanty" title="Documentation">📖</a></td>
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Cyanty"><img src="https://avatars.githubusercontent.com/u/153884653?v=4?s=100" width="100px;" alt="Cyanty"/><br /><sub><b>Cyanty</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Cyanty" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=Cyanty" title="Documentation">📖</a></td>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/KevinLLF"><img src="https://avatars.githubusercontent.com/u/85452733?v=4?s=100" width="100px;" alt="Jay丿167"/><br /><sub><b>Jay丿167</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=KevinLLF" title="Code">💻</a></td>
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/KevinLLF"><img src="https://avatars.githubusercontent.com/u/85452733?v=4?s=100" width="100px;" alt="Jay丿167"/><br /><sub><b>Jay丿167</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=KevinLLF" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Duansg"><img src="https://avatars.githubusercontent.com/u/112607719?v=4?s=100" width="100px;" alt="Duansg"/><br /><sub><b>Duansg</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Duansg" title="Documentation">📖</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/xiaomizhou2"><img src="https://avatars.githubusercontent.com/u/47807926?v=4?s=100" width="100px;" alt="zhangyaxi"/><br /><sub><b>zhangyaxi</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=xiaomizhou2" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=xiaomizhou2" title="Tests">⚠️</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/RainBondsongyg"><img src="https://avatars.githubusercontent.com/u/94501396?v=4?s=100" width="100px;" alt="songyg"/><br /><sub><b>songyg</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=RainBondsongyg" title="Documentation">📖</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/lx1229"><img src="https://avatars.githubusercontent.com/u/44620005?v=4?s=100" width="100px;" alt="Liuxin"/><br /><sub><b>Liuxin</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=lx1229" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/yy549159265"><img src="https://avatars.githubusercontent.com/u/40821310?v=4?s=100" width="100px;" alt="yy549159265"/><br /><sub><b>yy549159265</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=yy549159265" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=yy549159265" title="Tests">⚠️</a> <a href="#design-yy549159265" title="Design">🎨</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/cto-huhang"><img src="https://avatars.githubusercontent.com/u/53338629?v=4?s=100" width="100px;" alt="cto-huhang"/><br /><sub><b>cto-huhang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=cto-huhang" title="Documentation">📖</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Saramanda9988"><img src="https://avatars.githubusercontent.com/u/176664901?v=4?s=100" width="100px;" alt="LunaRain_079"/><br /><sub><b>LunaRain_079</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Saramanda9988" title="Documentation">📖</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/delei"><img src="https://avatars.githubusercontent.com/u/17263766?v=4?s=100" width="100px;" alt="DeleiGuo"/><br /><sub><b>DeleiGuo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=delei" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=delei" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=delei" title="Tests">⚠️</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Adelei" title="Bug reports">🐛</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/chingjustwe"><img src="https://avatars.githubusercontent.com/u/13643747?v=4?s=100" width="100px;" alt="Rocky, Chi"/><br /><sub><b>Rocky, Chi</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=chingjustwe" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/rowankid"><img src="https://avatars.githubusercontent.com/u/18652781?v=4?s=100" width="100px;" alt="Wenqi Luo"/><br /><sub><b>Wenqi Luo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/issues?q=author%3Arowankid" title="Bug reports">🐛</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/tuzuy"><img src="https://avatars.githubusercontent.com/u/95274591?v=4?s=100" width="100px;" alt="tuzuy"/><br /><sub><b>tuzuy</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=tuzuy" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/carlpinto25"><img src="https://avatars.githubusercontent.com/u/117299909?v=4?s=100" width="100px;" alt="carl pinto"/><br /><sub><b>carl pinto</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=carlpinto25" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://cxhello.top/"><img src="https://avatars.githubusercontent.com/u/49056040?v=4?s=100" width="100px;" alt="cxhello"/><br /><sub><b>cxhello</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=cxhello" title="Documentation">📖</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/jl15988"><img src="https://avatars.githubusercontent.com/u/70638770?v=4?s=100" width="100px;" alt="会功夫的李白"/><br /><sub><b>会功夫的李白</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=jl15988" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://blog.aytop.cloud/"><img src="https://avatars.githubusercontent.com/u/37127008?v=4?s=100" width="100px;" alt="Albert.Yang"/><br /><sub><b>Albert.Yang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=AlbertYang0801" title="Code">💻</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://blog.tokenlen.top/"><img src="https://avatars.githubusercontent.com/u/150590575?v=4?s=100" width="100px;" alt="zhou yong kang"/><br /><sub><b>zhou yong kang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=mengnankkkk" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/warrobe"><img src="https://avatars.githubusercontent.com/u/89446159?v=4?s=100" width="100px;" alt="warrobe"/><br /><sub><b>warrobe</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=warrobe" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Jetiaime"><img src="https://avatars.githubusercontent.com/u/93769000?v=4?s=100" width="100px;" alt="TeAmo"/><br /><sub><b>TeAmo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Jetiaime" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/pentium100"><img src="https://avatars.githubusercontent.com/u/27917?v=4?s=100" width="100px;" alt="pentium100"/><br /><sub><b>pentium100</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=pentium100" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/dedyks"><img src="https://avatars.githubusercontent.com/u/23741665?v=4?s=100" width="100px;" alt="Dedy Kurniawan Santoso"/><br /><sub><b>Dedy Kurniawan Santoso</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=dedyks" title="Code">💻</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
@@ -541,7 +561,7 @@ Thanks these wonderful people, welcome to join us:
|
|||||||
|
|
||||||
微信公众号 : 搜索 ID `usthecom`.
|
微信公众号 : 搜索 ID `usthecom`.
|
||||||
|
|
||||||
[QQ交流群](https://jq.qq.com/?_wv=1027&k=Bud9OzdI) : 群号 `630061200`
|
[QQ交流群](https://qm.qq.com/q/xxqecSC2cw) : 群号 `1035688434`
|
||||||
|
|
||||||
[Github Discussion](https://github.com/apache/hertzbeat/discussions)
|
[Github Discussion](https://github.com/apache/hertzbeat/discussions)
|
||||||
|
|
||||||
@@ -565,8 +585,8 @@ HertzBeat is built on so many great open source projects, thanks to them!
|
|||||||
<p align="left">
|
<p align="left">
|
||||||
<img src="./home/static/img/home/cncf-landscape-left-logo.svg" width="300"> <img src="./home/static/img/home/cncf-right-logo.svg" width="345" />
|
<img src="./home/static/img/home/cncf-landscape-left-logo.svg" width="300"> <img src="./home/static/img/home/cncf-right-logo.svg" width="345" />
|
||||||
<br /><br />
|
<br /><br />
|
||||||
HertzBeat has been included in the <a href="https://landscape.cncf.io/guide?item=observability-and-analysis--monitoring--hertzbeat#observability-and-analysis--monitoring">
|
HertzBeat has been included in the <a href="https://landscape.cncf.io/?item=observability-and-analysis--observability--hertzbeat">
|
||||||
CNCF Observability And Analysis - Monitoring Landscape.</a>
|
CNCF Observability And Analysis - Observability Landscape.</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
## 🛡️ License
|
## 🛡️ License
|
||||||
|
|||||||
+596
@@ -0,0 +1,596 @@
|
|||||||
|
<p align="center">
|
||||||
|
<a href="https://hertzbeat.apache.org">
|
||||||
|
<img alt="hertzbeat" src="/home/static/img/hertzbeat-brand.svg" width="260">
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<b>Readme</b>:
|
||||||
|
<a href="README.md">English</a> | <a href="README_CN.md">中文</a> | <b><a href="README_JP.md">日本語</a></b>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
[](https://discord.gg/Fb6M73htGr)
|
||||||
|
[](https://www.reddit.com/r/hertzbeat/)
|
||||||
|
[](https://x.com/hertzbeat1024)
|
||||||
|
[](https://www.bestpractices.dev/projects/8139)
|
||||||
|
[](https://app.codecov.io/gh/apache/hertzbeat)
|
||||||
|
[](https://hub.docker.com/r/apache/hertzbeat)
|
||||||
|
[](https://artifacthub.io/packages/search?repo=hertzbeat)
|
||||||
|
[](https://www.youtube.com/channel/UCri75zfWX0GHqJFPENEbLow)
|
||||||
|
[](https://gitpod.io/#https://github.com/apache/hertzbeat)
|
||||||
|
|
||||||
|
|
||||||
|
**公式サイト: [hertzbeat.apache.org](https://hertzbeat.apache.org)**
|
||||||
|
**メール:** メーリングリストに登録するために、<a href="mailto:dev-subscribe@hertzbeat.apache.org">dev-subscribe@hertzbeat.apache.org</a>にメールを送ってください。
|
||||||
|
|
||||||
|
|
||||||
|
## 🎡 <font color="green">紹介</font>
|
||||||
|
|
||||||
|
[Apache HertzBeat™](https://github.com/apache/hertzbeat) は AI 駆動の次世代オープンソースリアルタイム観測システムです。メトリクスとログの統一収集、アラートの一元配信、インテリジェント管理分析。エージェント不要、高性能クラスタ、強力なカスタム監視とステータスページ構築機能を提供します。
|
||||||
|
|
||||||
|
### 特性
|
||||||
|
|
||||||
|
- **収集+分析+アラート+通知**を一つのプラットフォームに統合し、HertzBeat AI 駆動の新しいインタラクションと機能、内蔵 MCP Server 機能を提供。
|
||||||
|
- 統一メトリクスプラットフォーム、エージェントレス、Prometheus互換、アプリケーションサービス、プログラム、データベース、キャッシュ、オペレーティングシステム、ビッグデータ、ミドルウェア、Webサーバー、クラウドネイティブ、ネットワーク、カスタム監視などをサポート。
|
||||||
|
- 統一ログプラットフォーム、OTLP プロトコルを通じて複数のログソースをシームレスに統合してレポート。
|
||||||
|
- 統一アラートプラットフォーム、内部アラートと様々な外部アラートソースを統合接続、統一アラート処理分析、柔軟なリアルタイムと周期的閾値ルール、グループ収束、サイレンス、抑制など。
|
||||||
|
- 統一メッセージ配信、アラートプラットフォームで処理後、`メール` `Discord` `Slack` `Telegram` `DingTalk` `WeChat` `FeiShu` `SMS` `Webhook` `ServerChan` などの方法で配信通知。
|
||||||
|
- `Http、Jmx、Ssh、Snmp、Jdbc、Prometheus`などのプロトコルを設定可能にし、テンプレート`YML`ファイルをオンラインで設定するだけで、あらゆるメトリクスを収集できるようにします。HertzBeatでオンライン設定するだけで、`K8s`や`Docker`のような新しい監視タイプに素早く対応できることを想像してみてください。
|
||||||
|
- 高性能で、コレクタークラスタの水平拡張、マルチ分離ネットワーク監視、クラウドエッジ協調をサポート。
|
||||||
|
- 強力なステータスページ構築機能を提供し、サービスのリアルタイムステータスをユーザーに簡単に伝達。
|
||||||
|
|
||||||
|
> `HertzBeat`の統一プラットフォーム、AI インテリジェンス、強力なカスタマイズ、多種類サポート、高性能、容易な拡張性は、ユーザーが迅速かつ便利に観測要件を実現することを支援することを目的としています。
|
||||||
|
|
||||||
|
----
|
||||||
|
|
||||||
|
----
|
||||||
|
|
||||||
|
## 🥐 モジュール
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## ⛄ 利用可能
|
||||||
|
|
||||||
|
> `mysql`、`Java仮想マシン`、`k8s` などのすべての監視コレクション タイプを `YML` 監視テンプレートとして定義し、ユーザーがそれらをインポートして対応するタイプの監視をサポートできるようにしています。
|
||||||
|
> カスタマイズされた一般的なモニターのテンプレートを提供することを歓迎します。
|
||||||
|
|
||||||
|
- [Website](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-website.yml), [Port Telnet](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-port.yml),
|
||||||
|
[Http Api](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-api.yml), [Ping Connect](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-ping.yml),
|
||||||
|
[Jvm](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-jvm.yml), [SiteMap](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-fullsite.yml),
|
||||||
|
[Ssl Certificate](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-ssl_cert.yml), [SpringBoot2](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-springboot2.yml),
|
||||||
|
[FTP Server](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-ftp.yml), [SpringBoot3](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-springboot3.yml),
|
||||||
|
[Udp Port](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-udp_port.yml), [Dns](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-dns.yml),
|
||||||
|
[Pop3](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-pop3.yml), [Ntp](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-ntp.yml),
|
||||||
|
[Api Code](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-api_code.yml), [Smtp](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-smtp.yml),
|
||||||
|
[Nginx](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-nginx.yml)
|
||||||
|
- [Mysql](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-mysql.yml), [PostgreSQL](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-postgresql.yml),
|
||||||
|
[MariaDB](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-mariadb.yml), [Redis](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-redis.yml),
|
||||||
|
[ElasticSearch](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-elasticsearch.yml), [SqlServer](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-sqlserver.yml),
|
||||||
|
[Oracle](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-oracle.yml), [MongoDB](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-mongodb.yml),
|
||||||
|
[DM](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-dm.yml), [OpenGauss](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-opengauss.yml),
|
||||||
|
[ClickHouse](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-clickhouse.yml), [IoTDB](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-iotdb.yml),
|
||||||
|
[Redis Cluster](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-redis_cluster.yml), [Redis Sentinel](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-redis_sentinel.yml)
|
||||||
|
[Doris BE](https://github.com/apache/hertzbeat/blob/master/hertzbeat-manager/src/main/resources/define/app-doris_be.yml), [Doris FE](https://github.com/apache/hertzbeat/blob/master/hertzbeat-manager/src/main/resources/define/app-doris_fe.yml),
|
||||||
|
[Memcached](https://github.com/apache/hertzbeat/blob/master/hertzbeat-manager/src/main/resources/define/app-memcached.yml), [NebulaGraph](https://github.com/apache/hertzbeat/blob/master/hertzbeat-manager/src/main/resources/define/app-nebula_graph.yml)
|
||||||
|
- [Linux](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-linux.yml), [Ubuntu](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-ubuntu.yml),
|
||||||
|
[CentOS](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-centos.yml), [Windows](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-windows.yml),
|
||||||
|
[EulerOS](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-euleros.yml), [Fedora CoreOS](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-coreos.yml),
|
||||||
|
[OpenSUSE](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-opensuse.yml), [Rocky Linux](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-rockylinux.yml),
|
||||||
|
[Red Hat](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-redhat.yml), [FreeBSD](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-freebsd.yml),
|
||||||
|
[AlmaLinux](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-almalinux.yml), [Debian Linux](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-debian.yml)
|
||||||
|
- [Tomcat](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-tomcat.yml), [Nacos](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-nacos.yml),
|
||||||
|
[Zookeeper](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-zookeeper.yml), [RabbitMQ](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-rabbitmq.yml),
|
||||||
|
[Flink](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-flink.yml), [Kafka](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-kafka.yml),
|
||||||
|
[ShenYu](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-shenyu.yml), [DynamicTp](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-dynamic_tp.yml),
|
||||||
|
[Jetty](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-jetty.yml), [ActiveMQ](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-activemq.yml),
|
||||||
|
[Spring Gateway](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-spring_gateway.yml), [EMQX MQTT](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-emqx.yml),
|
||||||
|
[AirFlow](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-airflow.yml), [Hive](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-hive.yml),
|
||||||
|
[Spark](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-spark.yml), [Hadoop](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-hadoop.yml)
|
||||||
|
- [Kubernetes](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-kubernetes.yml), [Docker](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-docker.yml)
|
||||||
|
- [CiscoSwitch](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-cisco_switch.yml), [HpeSwitch](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-hpe_switch.yml),
|
||||||
|
[HuaweiSwitch](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-huawei_switch.yml), [TpLinkSwitch](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-tplink_switch.yml),
|
||||||
|
[H3cSwitch](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-h3c_switch.yml)
|
||||||
|
- その他、カスタマイズされたモニターテンプレート。
|
||||||
|
- 通知利用可能 `Discord` `Slack` `Telegram` `Email` `Dingtalk` `WeChat` `FeiShu` `Webhook` `SMS` `ServerChan`。
|
||||||
|
|
||||||
|
## 🐕 クイックスタート
|
||||||
|
|
||||||
|
- HertzBeat をローカルに展開する場合は、以下のデプロイメントドキュメントを参照してください。
|
||||||
|
|
||||||
|
### 🍞 HertzBeatのインストール
|
||||||
|
> HertzBeatは、ソースコードのインストールとブート、Dockerコンテナの実行とインストールパッケージによるインストールとデプロイをサポートし、CPUアーキテクチャはx86/arm64をサポートします。
|
||||||
|
|
||||||
|
##### 方式1:Docker
|
||||||
|
|
||||||
|
1. `docker` で以下の指令を実行します:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
docker run -d -p 1157:1157 -p 1158:1158 --name hertzbeat apache/hertzbeat
|
||||||
|
```
|
||||||
|
|
||||||
|
2. スタート:`http://localhost:4200`にアクセスします。デフォルトのアカウントとパスワード:`admin/hertzbeat`。
|
||||||
|
|
||||||
|
3. コレクタークラスタのデプロイメント(オプション)
|
||||||
|
|
||||||
|
```shell
|
||||||
|
docker run -d -e IDENTITY=custom-collector-name -e MANAGER_HOST=127.0.0.1 -e MANAGER_PORT=1158 --name hertzbeat-collector apache/hertzbeat-collector
|
||||||
|
```
|
||||||
|
|
||||||
|
- `-e IDENTITY=custom-collector-name` :コレクターのユニーク ID。
|
||||||
|
- `-e MODE=public` :実行モード(パブリッククラスタまたはプライベートクラウドエッジ)。
|
||||||
|
- `-e MANAGER_HOST=127.0.0.1` :メインhertzbeatサーバーのIP。
|
||||||
|
- `-e MANAGER_PORT=1158` :メインhertzbeatサーバポート。
|
||||||
|
|
||||||
|
|
||||||
|
詳細ステップ [通过 Docker 方式安装 HertzBeat](https://hertzbeat.apache.org/docs/start/docker-deploy)
|
||||||
|
|
||||||
|
##### 方式2:インストールパッケージ
|
||||||
|
|
||||||
|
1. リリースパッケージをダウンロード `hertzbeat-xx.tar.gz` [Download](https://hertzbeat.apache.org/docs/download)
|
||||||
|
2. HertzBeatのymlファイルを設定 `hertzbeat/config/application.yml` (オプション)
|
||||||
|
3. コマンド`$ ./bin/startup.sh`または`bin/startup.bat`を実行
|
||||||
|
4. スタート:`http://localhost:4200`にアクセスします。デフォルトのアカウントとパスワード:`admin/hertzbeat`
|
||||||
|
5. コレクタークラスタのデプロイメント(オプション)
|
||||||
|
- コレクターパッケージを別のホストにダウンロード `hertzbeat-collector-xx.tar.gz` [Download](https://hertzbeat.apache.org/docs/download)
|
||||||
|
- コレクターのymlファイルを設定 `hertzbeat-collector/config/application.yml`
|
||||||
|
```yaml
|
||||||
|
collector:
|
||||||
|
dispatch:
|
||||||
|
entrance:
|
||||||
|
netty:
|
||||||
|
enabled: true
|
||||||
|
identity: ${IDENTITY:}
|
||||||
|
mode: ${MODE:public}
|
||||||
|
manager-host: ${MANAGER_HOST:127.0.0.1}
|
||||||
|
manager-port: ${MANAGER_PORT:1158}
|
||||||
|
```
|
||||||
|
- `identity: ${IDENTITY:}`:コレクターのユニークID。
|
||||||
|
- `mode: ${MODE:public}`:実行モード(パブリッククラスタまたはプライベートクラウドエッジ)。
|
||||||
|
- `manager-host: ${MANAGER_HOST:127.0.0.1}`:メインhertzbeatサーバーのIP。
|
||||||
|
- `manager-port: ${MANAGER_PORT:1158}`:メインhertzbeatサーバポート。
|
||||||
|
- コマンド`$ ./bin/startup.sh`または`bin/startup.bat`を実行。
|
||||||
|
- `http://localhost:1157`にアクセスし、登録された新しいコレクターを見ることがでます。
|
||||||
|
|
||||||
|
詳細ステップ [通过安装包安装HertzBeat](https://hertzbeat.apache.org/docs/start/package-deploy)
|
||||||
|
|
||||||
|
##### 方式3:ローカルの実行
|
||||||
|
|
||||||
|
1. ローカルの実行には、バックエンドのプロジェクト`hertzbeat-startup`とフロントエンドのプロジェクト`web-app`を起動する必要があります。
|
||||||
|
2. バックエンド:`maven3+`、`java17`と`lombok`の環境は必要です。`YML` 設定を修正し、Java仮想マシンパラメータに`--add-opens=java.base/java.nio=org.apache.arrow.memory.core,ALL-UNNAMED` を追加し、`hertzbeat-startup` を起動します。
|
||||||
|
3. フロントエンド:`nodejs npm angular-cli`の環境は必要です。ローカルのバックエンドが立ち上がったら、`web-app` ディレクトリで `ng serve --open` というコマンドを実行します。
|
||||||
|
4. スタート:`http://localhost:4200`にアクセスします。デフォルトのアカウントとパスワード:`admin/hertzbeat`。
|
||||||
|
|
||||||
|
詳細ステップ [貢献ガイド](CONTRIBUTING.md)
|
||||||
|
|
||||||
|
##### 方式4:Docker-Compose
|
||||||
|
|
||||||
|
[Docker-Compose 部署脚本](script/docker-compose)でpostgresql/mysqlデータベース、victoria-metrics、iotdb、またはtdengine時系列データベースとHertzbeat一括デプロイ。
|
||||||
|
|
||||||
|
詳細ステップ [通过 Docker-Compose 安装 HertzBeat](script/docker-compose/README.md)
|
||||||
|
|
||||||
|
##### 方式5:Kubernetes Helm Charts
|
||||||
|
|
||||||
|
Helm ChartでHertzBeatクラスタコンポーネントをKubernetesクラスタに一括デプロイ。
|
||||||
|
|
||||||
|
詳細ステップ [Artifact Hub](https://artifacthub.io/packages/helm/hertzbeat/hertzbeat)
|
||||||
|
|
||||||
|
**HAVE FUN**
|
||||||
|
|
||||||
|
## ✨ Contributors
|
||||||
|
|
||||||
|
Thanks these wonderful people, welcome to join us:
|
||||||
|
[貢献ガイド](CONTRIBUTING.md)
|
||||||
|
|
||||||
|
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
|
||||||
|
<!-- prettier-ignore-start -->
|
||||||
|
<!-- markdownlint-disable -->
|
||||||
|
<table>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/tomsun28"><img src="https://avatars.githubusercontent.com/u/24788200?v=4?s=100" width="100px;" alt="tomsun28"/><br /><sub><b>tomsun28</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=tomsun28" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=tomsun28" title="Documentation">📖</a> <a href="#design-tomsun28" title="Design">🎨</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/wang1027-wqh"><img src="https://avatars.githubusercontent.com/u/71161318?v=4?s=100" width="100px;" alt="会编程的王学长"/><br /><sub><b>会编程的王学长</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=wang1027-wqh" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=wang1027-wqh" title="Documentation">📖</a> <a href="#design-wang1027-wqh" title="Design">🎨</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://www.maxkey.top/"><img src="https://avatars.githubusercontent.com/u/1563377?v=4?s=100" width="100px;" alt="MaxKey"/><br /><sub><b>MaxKey</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=shimingxy" title="Code">💻</a> <a href="#design-shimingxy" title="Design">🎨</a> <a href="#ideas-shimingxy" title="Ideas, Planning, & Feedback">🤔</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://blog.gcdd.top/"><img src="https://avatars.githubusercontent.com/u/26523525?v=4?s=100" width="100px;" alt="观沧海"/><br /><sub><b>观沧海</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=gcdd1993" title="Code">💻</a> <a href="#design-gcdd1993" title="Design">🎨</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Agcdd1993" title="Bug reports">🐛</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/a25017012"><img src="https://avatars.githubusercontent.com/u/32265356?v=4?s=100" width="100px;" alt="yuye"/><br /><sub><b>yuye</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=a25017012" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=a25017012" title="Documentation">📖</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/jx10086"><img src="https://avatars.githubusercontent.com/u/5323228?v=4?s=100" width="100px;" alt="jx10086"/><br /><sub><b>jx10086</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=jx10086" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Ajx10086" title="Bug reports">🐛</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/winnerTimer"><img src="https://avatars.githubusercontent.com/u/76024658?v=4?s=100" width="100px;" alt="winnerTimer"/><br /><sub><b>winnerTimer</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=winnerTimer" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3AwinnerTimer" title="Bug reports">🐛</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/goo-kits"><img src="https://avatars.githubusercontent.com/u/13163673?v=4?s=100" width="100px;" alt="goo-kits"/><br /><sub><b>goo-kits</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=goo-kits" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Agoo-kits" title="Bug reports">🐛</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/brave4Time"><img src="https://avatars.githubusercontent.com/u/105094014?v=4?s=100" width="100px;" alt="brave4Time"/><br /><sub><b>brave4Time</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=brave4Time" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Abrave4Time" title="Bug reports">🐛</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/walkerlee-lab"><img src="https://avatars.githubusercontent.com/u/8426753?v=4?s=100" width="100px;" alt="WalkerLee"/><br /><sub><b>WalkerLee</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=walkerlee-lab" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Awalkerlee-lab" title="Bug reports">🐛</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/fullofjoy"><img src="https://avatars.githubusercontent.com/u/30247571?v=4?s=100" width="100px;" alt="jianghang"/><br /><sub><b>jianghang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=fullofjoy" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Afullofjoy" title="Bug reports">🐛</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ChineseTony"><img src="https://avatars.githubusercontent.com/u/24618786?v=4?s=100" width="100px;" alt="ChineseTony"/><br /><sub><b>ChineseTony</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ChineseTony" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3AChineseTony" title="Bug reports">🐛</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/wyt199905"><img src="https://avatars.githubusercontent.com/u/85098809?v=4?s=100" width="100px;" alt="wyt199905"/><br /><sub><b>wyt199905</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=wyt199905" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/weifuqing"><img src="https://avatars.githubusercontent.com/u/13931013?v=4?s=100" width="100px;" alt="卫傅庆"/><br /><sub><b>卫傅庆</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=weifuqing" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Aweifuqing" title="Bug reports">🐛</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/zklmcookle"><img src="https://avatars.githubusercontent.com/u/107192352?v=4?s=100" width="100px;" alt="zklmcookle"/><br /><sub><b>zklmcookle</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=zklmcookle" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/DevilX5"><img src="https://avatars.githubusercontent.com/u/13269921?v=4?s=100" width="100px;" alt="DevilX5"/><br /><sub><b>DevilX5</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=DevilX5" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=DevilX5" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/djzeng"><img src="https://avatars.githubusercontent.com/u/14074864?v=4?s=100" width="100px;" alt="tea"/><br /><sub><b>tea</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=djzeng" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/yangshihui"><img src="https://avatars.githubusercontent.com/u/28550208?v=4?s=100" width="100px;" alt="yangshihui"/><br /><sub><b>yangshihui</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=yangshihui" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Ayangshihui" title="Bug reports">🐛</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/DreamGirl524"><img src="https://avatars.githubusercontent.com/u/81132838?v=4?s=100" width="100px;" alt="DreamGirl524"/><br /><sub><b>DreamGirl524</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=DreamGirl524" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=DreamGirl524" title="Documentation">📖</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/gzwlly"><img src="https://avatars.githubusercontent.com/u/83171907?v=4?s=100" width="100px;" alt="gzwlly"/><br /><sub><b>gzwlly</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=gzwlly" title="Documentation">📖</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/cuipiheqiuqiu"><img src="https://avatars.githubusercontent.com/u/76642201?v=4?s=100" width="100px;" alt="cuipiheqiuqiu"/><br /><sub><b>cuipiheqiuqiu</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=cuipiheqiuqiu" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=cuipiheqiuqiu" title="Tests">⚠️</a> <a href="#design-cuipiheqiuqiu" title="Design">🎨</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/oyiyou"><img src="https://avatars.githubusercontent.com/u/39228891?v=4?s=100" width="100px;" alt="lambert"/><br /><sub><b>lambert</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=oyiyou" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="http://mroldx.xyz/"><img src="https://avatars.githubusercontent.com/u/34847828?v=4?s=100" width="100px;" alt="mroldx"/><br /><sub><b>mroldx</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=mroldx" title="Documentation">📖</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/woshiniusange"><img src="https://avatars.githubusercontent.com/u/91513022?v=4?s=100" width="100px;" alt="woshiniusange"/><br /><sub><b>woshiniusange</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=woshiniusange" title="Documentation">📖</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://vampireachao.github.io/"><img src="https://avatars.githubusercontent.com/u/52746628?v=4?s=100" width="100px;" alt="VampireAchao"/><br /><sub><b>VampireAchao</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=VampireAchao" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Ceilzcx"><img src="https://avatars.githubusercontent.com/u/48920254?v=4?s=100" width="100px;" alt="zcx"/><br /><sub><b>zcx</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Ceilzcx" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3ACeilzcx" title="Bug reports">🐛</a> <a href="#design-Ceilzcx" title="Design">🎨</a> <a href="https://github.com/apache/hertzbeat/commits?author=Ceilzcx" title="Tests">⚠️</a> <a href="#blog-Ceilzcx" title="Blogposts">📝</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/CharlieXCL"><img src="https://avatars.githubusercontent.com/u/91540487?v=4?s=100" width="100px;" alt="CharlieXCL"/><br /><sub><b>CharlieXCL</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=CharlieXCL" title="Documentation">📖</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Privauto"><img src="https://avatars.githubusercontent.com/u/36581456?v=4?s=100" width="100px;" alt="Privauto"/><br /><sub><b>Privauto</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Privauto" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=Privauto" title="Documentation">📖</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/emrys-he"><img src="https://avatars.githubusercontent.com/u/5848915?v=4?s=100" width="100px;" alt="emrys"/><br /><sub><b>emrys</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=emrys-he" title="Documentation">📖</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/SxLiuYu"><img src="https://avatars.githubusercontent.com/u/95198625?v=4?s=100" width="100px;" alt="SxLiuYu"/><br /><sub><b>SxLiuYu</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/issues?q=author%3ASxLiuYu" title="Bug reports">🐛</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://allcontributors.org"><img src="https://avatars.githubusercontent.com/u/46410174?v=4?s=100" width="100px;" alt="All Contributors"/><br /><sub><b>All Contributors</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=all-contributors" title="Documentation">📖</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/gxc-myh"><img src="https://avatars.githubusercontent.com/u/85919258?v=4?s=100" width="100px;" alt="铁甲小宝"/><br /><sub><b>铁甲小宝</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=gxc-myh" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=gxc-myh" title="Documentation">📖</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/click33"><img src="https://avatars.githubusercontent.com/u/36243476?v=4?s=100" width="100px;" alt="click33"/><br /><sub><b>click33</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=click33" title="Documentation">📖</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://jpom.io/"><img src="https://avatars.githubusercontent.com/u/16408873?v=4?s=100" width="100px;" alt="蒋小小"/><br /><sub><b>蒋小小</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=bwcx-jzy" title="Documentation">📖</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://www.zhihu.com/people/kevinbauer"><img src="https://avatars.githubusercontent.com/u/28581579?v=4?s=100" width="100px;" alt="Kevin Huang"/><br /><sub><b>Kevin Huang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=kevinhuangwl" title="Documentation">📖</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/TJxiaobao"><img src="https://avatars.githubusercontent.com/u/85919258?v=4?s=100" width="100px;" alt="铁甲小宝"/><br /><sub><b>铁甲小宝</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/issues?q=author%3ATJxiaobao" title="Bug reports">🐛</a> <a href="https://github.com/apache/hertzbeat/commits?author=TJxiaobao" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=TJxiaobao" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=TJxiaobao" title="Tests">⚠️</a> <a href="#design-TJxiaobao" title="Design">🎨</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Jack-123-power"><img src="https://avatars.githubusercontent.com/u/84333501?v=4?s=100" width="100px;" alt="Captain Jack"/><br /><sub><b>Captain Jack</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Jack-123-power" title="Documentation">📖</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/haibo-duan"><img src="https://avatars.githubusercontent.com/u/7974845?v=4?s=100" width="100px;" alt="haibo.duan"/><br /><sub><b>haibo.duan</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=haibo-duan" title="Tests">⚠️</a> <a href="https://github.com/apache/hertzbeat/commits?author=haibo-duan" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/assassinfym"><img src="https://avatars.githubusercontent.com/u/15188754?v=4?s=100" width="100px;" alt="assassin"/><br /><sub><b>assassin</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/issues?q=author%3Aassassinfym" title="Bug reports">🐛</a> <a href="https://github.com/apache/hertzbeat/commits?author=assassinfym" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/csyshu"><img src="https://avatars.githubusercontent.com/u/46591658?v=4?s=100" width="100px;" alt="Reverse wind"/><br /><sub><b>Reverse wind</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=csyshu" title="Tests">⚠️</a> <a href="https://github.com/apache/hertzbeat/commits?author=csyshu" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/luxx-lq"><img src="https://avatars.githubusercontent.com/u/58515565?v=4?s=100" width="100px;" alt="luxx"/><br /><sub><b>luxx</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=luxx-lq" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://bandism.net/"><img src="https://avatars.githubusercontent.com/u/22633385?v=4?s=100" width="100px;" alt="Ikko Ashimine"/><br /><sub><b>Ikko Ashimine</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=eltociear" title="Documentation">📖</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/zenan08"><img src="https://avatars.githubusercontent.com/u/80514991?v=4?s=100" width="100px;" alt="leizenan"/><br /><sub><b>leizenan</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=zenan08" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/BKing2020"><img src="https://avatars.githubusercontent.com/u/28869121?v=4?s=100" width="100px;" alt="BKing"/><br /><sub><b>BKing</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=BKing2020" title="Documentation">📖</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/xingshuaiLi"><img src="https://avatars.githubusercontent.com/u/119487588?v=4?s=100" width="100px;" alt="xingshuaiLi"/><br /><sub><b>xingshuaiLi</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=xingshuaiLi" title="Documentation">📖</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/wangke6666"><img src="https://avatars.githubusercontent.com/u/113656595?v=4?s=100" width="100px;" alt="wangke6666"/><br /><sub><b>wangke6666</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=wangke6666" title="Documentation">📖</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/LWBobo"><img src="https://avatars.githubusercontent.com/u/50368698?v=4?s=100" width="100px;" alt="刺猬"/><br /><sub><b>刺猬</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/issues?q=author%3ALWBobo" title="Bug reports">🐛</a> <a href="https://github.com/apache/hertzbeat/commits?author=LWBobo" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="http://www.zanglikun.com"><img src="https://avatars.githubusercontent.com/u/61591648?v=4?s=100" width="100px;" alt="Haste"/><br /><sub><b>Haste</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=zanglikun" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/SuitSmile"><img src="https://avatars.githubusercontent.com/u/38679717?v=4?s=100" width="100px;" alt="zhongshi.yi"/><br /><sub><b>zhongshi.yi</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=SuitSmile" title="Documentation">📖</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://www.smallq.cn"><img src="https://avatars.githubusercontent.com/u/39754275?v=4?s=100" width="100px;" alt="Qi Zhang"/><br /><sub><b>Qi Zhang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=zzzhangqi" title="Documentation">📖</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/MrAndyMing"><img src="https://avatars.githubusercontent.com/u/49541483?v=4?s=100" width="100px;" alt="MrAndyMing"/><br /><sub><b>MrAndyMing</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=MrAndyMing" title="Documentation">📖</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://idongliming.github.io/"><img src="https://avatars.githubusercontent.com/u/31564353?v=4?s=100" width="100px;" alt="idongliming"/><br /><sub><b>idongliming</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=idongliming" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://earthjasonlin.github.io"><img src="https://avatars.githubusercontent.com/u/83632110?v=4?s=100" width="100px;" alt="Zichao Lin"/><br /><sub><b>Zichao Lin</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=earthjasonlin" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=earthjasonlin" title="Documentation">📖</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="http://blog.liudonghua.com"><img src="https://avatars.githubusercontent.com/u/2276718?v=4?s=100" width="100px;" alt="liudonghua"/><br /><sub><b>liudonghua</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=liudonghua123" title="Code">💻</a> <a href="#ideas-liudonghua123" title="Ideas, Planning, & Feedback">🤔</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/orangeyts"><img src="https://avatars.githubusercontent.com/u/4250869?v=4?s=100" width="100px;" alt="Jerry"/><br /><sub><b>Jerry</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=orangeyts" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=orangeyts" title="Tests">⚠️</a> <a href="#ideas-orangeyts" title="Ideas, Planning, & Feedback">🤔</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://dynamictp.cn"><img src="https://avatars.githubusercontent.com/u/13051908?v=4?s=100" width="100px;" alt="yanhom"/><br /><sub><b>yanhom</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=yanhom1314" title="Documentation">📖</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://www.jianshu.com/u/a8f822c04f67"><img src="https://avatars.githubusercontent.com/u/18587688?v=4?s=100" width="100px;" alt="fsl"/><br /><sub><b>fsl</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=fengshunli" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/xttttv"><img src="https://avatars.githubusercontent.com/u/116323904?v=4?s=100" width="100px;" alt="xttttv"/><br /><sub><b>xttttv</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=xttttv" title="Documentation">📖</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/NavinKumarBarnwal"><img src="https://avatars.githubusercontent.com/u/44504274?v=4?s=100" width="100px;" alt="NavinKumarBarnwal"/><br /><sub><b>NavinKumarBarnwal</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=NavinKumarBarnwal" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/z641205699"><img src="https://avatars.githubusercontent.com/u/45276423?v=4?s=100" width="100px;" alt="Zakkary"/><br /><sub><b>Zakkary</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=z641205699" title="Documentation">📖</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/898349230"><img src="https://avatars.githubusercontent.com/u/21972532?v=4?s=100" width="100px;" alt="sunxinbo"/><br /><sub><b>sunxinbo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=898349230" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=898349230" title="Tests">⚠️</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ldzbook"><img src="https://avatars.githubusercontent.com/u/13903790?v=4?s=100" width="100px;" alt="ldzbook"/><br /><sub><b>ldzbook</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ldzbook" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Aldzbook" title="Bug reports">🐛</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/SurryChen"><img src="https://avatars.githubusercontent.com/u/91116490?v=4?s=100" width="100px;" alt="余与雨"/><br /><sub><b>余与雨</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=SurryChen" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=SurryChen" title="Tests">⚠️</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/MysticalDream"><img src="https://avatars.githubusercontent.com/u/78899028?v=4?s=100" width="100px;" alt="MysticalDream"/><br /><sub><b>MysticalDream</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=MysticalDream" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=MysticalDream" title="Tests">⚠️</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/zhouyoulin12"><img src="https://avatars.githubusercontent.com/u/17086633?v=4?s=100" width="100px;" alt="zhouyoulin12"/><br /><sub><b>zhouyoulin12</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=zhouyoulin12" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=zhouyoulin12" title="Tests">⚠️</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/jerjjj"><img src="https://avatars.githubusercontent.com/u/93431283?v=4?s=100" width="100px;" alt="jerjjj"/><br /><sub><b>jerjjj</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=jerjjj" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://wjl110.xyz/"><img src="https://avatars.githubusercontent.com/u/53851034?v=4?s=100" width="100px;" alt="wjl110"/><br /><sub><b>wjl110</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=wjl110" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ngyhd"><img src="https://avatars.githubusercontent.com/u/29095207?v=4?s=100" width="100px;" alt="Sean"/><br /><sub><b>Sean</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ngyhd" title="Documentation">📖</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Daydreamer-ia"><img src="https://avatars.githubusercontent.com/u/83362909?v=4?s=100" width="100px;" alt="chenyiqin"/><br /><sub><b>chenyiqin</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Daydreamer-ia" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=Daydreamer-ia" title="Tests">⚠️</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/hudongdong129"><img src="https://avatars.githubusercontent.com/u/34374227?v=4?s=100" width="100px;" alt="hudongdong129"/><br /><sub><b>hudongdong129</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=hudongdong129" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=hudongdong129" title="Tests">⚠️</a> <a href="https://github.com/apache/hertzbeat/commits?author=hudongdong129" title="Documentation">📖</a> <a href="#design-hudongdong129" title="Design">🎨</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/TherChenYang"><img src="https://avatars.githubusercontent.com/u/124348939?v=4?s=100" width="100px;" alt="TherChenYang"/><br /><sub><b>TherChenYang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=TherChenYang" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=TherChenYang" title="Tests">⚠️</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/HattoriHenzo"><img src="https://avatars.githubusercontent.com/u/5141285?v=4?s=100" width="100px;" alt="HattoriHenzo"/><br /><sub><b>HattoriHenzo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=HattoriHenzo" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=HattoriHenzo" title="Tests">⚠️</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ycilry"><img src="https://avatars.githubusercontent.com/u/63967101?v=4?s=100" width="100px;" alt="ycilry"/><br /><sub><b>ycilry</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ycilry" title="Documentation">📖</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/aoshiguchen"><img src="https://avatars.githubusercontent.com/u/10580997?v=4?s=100" width="100px;" alt="aoshiguchen"/><br /><sub><b>aoshiguchen</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=aoshiguchen" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=aoshiguchen" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/caibenxiang"><img src="https://avatars.githubusercontent.com/u/4568241?v=4?s=100" width="100px;" alt="蔡本祥"/><br /><sub><b>蔡本祥</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=caibenxiang" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="http://www.fckeverything.cn:4000/"><img src="https://avatars.githubusercontent.com/u/13827124?v=4?s=100" width="100px;" alt="浮游"/><br /><sub><b>浮游</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=lifefloating" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Grass-Life"><img src="https://avatars.githubusercontent.com/u/114381513?v=4?s=100" width="100px;" alt="Grass-Life"/><br /><sub><b>Grass-Life</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Grass-Life" title="Code">💻</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/xiaohe428"><img src="https://avatars.githubusercontent.com/u/99130317?v=4?s=100" width="100px;" alt="xiaohe428"/><br /><sub><b>xiaohe428</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=xiaohe428" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=xiaohe428" title="Documentation">📖</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/baiban114"><img src="https://avatars.githubusercontent.com/u/59152619?v=4?s=100" width="100px;" alt="TableRow"/><br /><sub><b>TableRow</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=baiban114" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=baiban114" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ByteIDance"><img src="https://avatars.githubusercontent.com/u/100207562?v=4?s=100" width="100px;" alt="ByteIDance"/><br /><sub><b>ByteIDance</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ByteIDance" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/mangel2002"><img src="https://avatars.githubusercontent.com/u/9348020?v=4?s=100" width="100px;" alt="Jangfe"/><br /><sub><b>Jangfe</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=mangel2002" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/zqr10159"><img src="https://avatars.githubusercontent.com/u/30048352?v=4?s=100" width="100px;" alt="zqr10159"/><br /><sub><b>zqr10159</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=zqr10159" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=zqr10159" title="Code">💻</a> <a href="#blog-zqr10159" title="Blogposts">📝</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Azqr10159" title="Bug reports">🐛</a> <a href="https://github.com/apache/hertzbeat/commits?author=zqr10159" title="Tests">⚠️</a> <a href="#design-zqr10159" title="Design">🎨</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/vinci-897"><img src="https://avatars.githubusercontent.com/u/55838224?v=4?s=100" width="100px;" alt="vinci"/><br /><sub><b>vinci</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=vinci-897" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=vinci-897" title="Documentation">📖</a> <a href="#design-vinci-897" title="Design">🎨</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/js110"><img src="https://avatars.githubusercontent.com/u/51191863?v=4?s=100" width="100px;" alt="js110"/><br /><sub><b>js110</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=js110" title="Code">💻</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/JavaLionLi"><img src="https://avatars.githubusercontent.com/u/31852897?v=4?s=100" width="100px;" alt="CrazyLionLi"/><br /><sub><b>CrazyLionLi</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=JavaLionLi" title="Documentation">📖</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="http://www.banmajio.com"><img src="https://avatars.githubusercontent.com/u/53471385?v=4?s=100" width="100px;" alt="banmajio"/><br /><sub><b>banmajio</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=banmajio" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://suder.fun"><img src="https://avatars.githubusercontent.com/u/69955165?v=4?s=100" width="100px;" alt="topsuder"/><br /><sub><b>topsuder</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=topsuder" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/richar2022"><img src="https://avatars.githubusercontent.com/u/129016397?v=4?s=100" width="100px;" alt="richar2022"/><br /><sub><b>richar2022</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=richar2022" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/fcb-xiaobo"><img src="https://avatars.githubusercontent.com/u/60566194?v=4?s=100" width="100px;" alt="fcb-xiaobo"/><br /><sub><b>fcb-xiaobo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=fcb-xiaobo" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/wenkyzhang"><img src="https://avatars.githubusercontent.com/u/13983669?v=4?s=100" width="100px;" alt="wenkyzhang"/><br /><sub><b>wenkyzhang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=wenkyzhang" title="Documentation">📖</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ZangJuxy"><img src="https://avatars.githubusercontent.com/u/71380295?v=4?s=100" width="100px;" alt="ZangJuxy"/><br /><sub><b>ZangJuxy</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ZangJuxy" title="Documentation">📖</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/l646505418"><img src="https://avatars.githubusercontent.com/u/50475131?v=4?s=100" width="100px;" alt="l646505418"/><br /><sub><b>l646505418</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=l646505418" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Al646505418" title="Bug reports">🐛</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="http://www.carpewang.com"><img src="https://avatars.githubusercontent.com/u/78642589?v=4?s=100" width="100px;" alt="Carpe-Wang"/><br /><sub><b>Carpe-Wang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Carpe-Wang" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3ACarpe-Wang" title="Bug reports">🐛</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/moshu023"><img src="https://avatars.githubusercontent.com/u/48593205?v=4?s=100" width="100px;" alt="莫枢"/><br /><sub><b>莫枢</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=moshu023" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/huangcanda"><img src="https://avatars.githubusercontent.com/u/4470566?v=4?s=100" width="100px;" alt="huangcanda"/><br /><sub><b>huangcanda</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=huangcanda" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://www.zrkizzy.com"><img src="https://avatars.githubusercontent.com/u/85340613?v=4?s=100" width="100px;" alt="世纪末的架构师"/><br /><sub><b>世纪末的架构师</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Architect-Java" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ShuningWan"><img src="https://avatars.githubusercontent.com/u/31086770?v=4?s=100" width="100px;" alt="ShuningWan"/><br /><sub><b>ShuningWan</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ShuningWan" title="Documentation">📖</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/MrYZhou"><img src="https://avatars.githubusercontent.com/u/44339602?v=4?s=100" width="100px;" alt="MrYZhou"/><br /><sub><b>MrYZhou</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=MrYZhou" title="Documentation">📖</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/suncqujsj"><img src="https://avatars.githubusercontent.com/u/8012932?v=4?s=100" width="100px;" alt="suncqujsj"/><br /><sub><b>suncqujsj</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=suncqujsj" title="Documentation">📖</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/sunqinbo"><img src="https://avatars.githubusercontent.com/u/1428540?v=4?s=100" width="100px;" alt="sunqinbo"/><br /><sub><b>sunqinbo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=sunqinbo" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/haoww"><img src="https://avatars.githubusercontent.com/u/32739294?v=4?s=100" width="100px;" alt="haoww"/><br /><sub><b>haoww</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=haoww" title="Documentation">📖</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/i-mayuan"><img src="https://avatars.githubusercontent.com/u/101498477?v=4?s=100" width="100px;" alt="i-mayuan"/><br /><sub><b>i-mayuan</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=i-mayuan" title="Documentation">📖</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/fengruge"><img src="https://avatars.githubusercontent.com/u/85803831?v=4?s=100" width="100px;" alt="fengruge"/><br /><sub><b>fengruge</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=fengruge" title="Documentation">📖</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/aystzh"><img src="https://avatars.githubusercontent.com/u/38125392?v=4?s=100" width="100px;" alt="zhanghuan"/><br /><sub><b>zhanghuan</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=aystzh" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/shenyumin"><img src="https://avatars.githubusercontent.com/u/8438506?v=4?s=100" width="100px;" alt="shenymin"/><br /><sub><b>shenymin</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=shenyumin" title="Code">💻</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/dhruva1995"><img src="https://avatars.githubusercontent.com/u/12976351?v=4?s=100" width="100px;" alt="Dhruva Chandra"/><br /><sub><b>Dhruva Chandra</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=dhruva1995" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/weiwang988"><img src="https://avatars.githubusercontent.com/u/58241726?v=4?s=100" width="100px;" alt="miss_z"/><br /><sub><b>miss_z</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=weiwang988" title="Documentation">📖</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/wyt990"><img src="https://avatars.githubusercontent.com/u/86013697?v=4?s=100" width="100px;" alt="wyt990"/><br /><sub><b>wyt990</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=wyt990" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/licocon"><img src="https://avatars.githubusercontent.com/u/36863277?v=4?s=100" width="100px;" alt="licocon"/><br /><sub><b>licocon</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=licocon" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/2406450951"><img src="https://avatars.githubusercontent.com/u/48074721?v=4?s=100" width="100px;" alt="Mi Na"/><br /><sub><b>Mi Na</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=2406450951" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Kylin-Guo"><img src="https://avatars.githubusercontent.com/u/131239856?v=4?s=100" width="100px;" alt="Kylin-Guo"/><br /><sub><b>Kylin-Guo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Kylin-Guo" title="Documentation">📖</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/1797899698"><img src="https://avatars.githubusercontent.com/u/40411650?v=4?s=100" width="100px;" alt="Mr灬Dong先生"/><br /><sub><b>Mr灬Dong先生</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=1797899698" title="Code">💻</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="http://neilblaze.live"><img src="https://avatars.githubusercontent.com/u/48355572?v=4?s=100" width="100px;" alt="Pratyay Banerjee"/><br /><sub><b>Pratyay Banerjee</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Neilblaze" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=Neilblaze" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/yujianzhong520"><img src="https://avatars.githubusercontent.com/u/63705063?v=4?s=100" width="100px;" alt="yujianzhong520"/><br /><sub><b>yujianzhong520</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=yujianzhong520" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://sppan24.github.io/"><img src="https://avatars.githubusercontent.com/u/15795173?v=4?s=100" width="100px;" alt="SPPan"/><br /><sub><b>SPPan</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=sppan24" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/1130600015"><img src="https://avatars.githubusercontent.com/u/67859663?v=4?s=100" width="100px;" alt="ZhangJiashu"/><br /><sub><b>ZhangJiashu</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=1130600015" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/QZmp236478"><img src="https://avatars.githubusercontent.com/u/56623162?v=4?s=100" width="100px;" alt="impress"/><br /><sub><b>impress</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=QZmp236478" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/jx3775250"><img src="https://avatars.githubusercontent.com/u/40455946?v=4?s=100" width="100px;" alt="凌晨一点半"/><br /><sub><b>凌晨一点半</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=jx3775250" title="Documentation">📖</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/eeshaanSA"><img src="https://avatars.githubusercontent.com/u/100678386?v=4?s=100" width="100px;" alt="Eeshaan Sawant"/><br /><sub><b>Eeshaan Sawant</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=eeshaanSA" title="Code">💻</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/nandofromthebando"><img src="https://avatars.githubusercontent.com/u/87321214?v=4?s=100" width="100px;" alt="nandofromthebando"/><br /><sub><b>nandofromthebando</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=nandofromthebando" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/caiboking"><img src="https://avatars.githubusercontent.com/u/6509883?v=4?s=100" width="100px;" alt="caiboking"/><br /><sub><b>caiboking</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=caiboking" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/baixing99"><img src="https://avatars.githubusercontent.com/u/73473087?v=4?s=100" width="100px;" alt="baixing99"/><br /><sub><b>baixing99</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=baixing99" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ifrenzyc"><img src="https://avatars.githubusercontent.com/u/543927?v=4?s=100" width="100px;" alt="Yang Chuang"/><br /><sub><b>Yang Chuang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ifrenzyc" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/wlin20"><img src="https://avatars.githubusercontent.com/u/20657577?v=4?s=100" width="100px;" alt="wlin20"/><br /><sub><b>wlin20</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=wlin20" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/guojing1983"><img src="https://avatars.githubusercontent.com/u/60596094?v=4?s=100" width="100px;" alt="guojing1983"/><br /><sub><b>guojing1983</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=guojing1983" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/itxxq"><img src="https://avatars.githubusercontent.com/u/46962357?v=4?s=100" width="100px;" alt="moxi"/><br /><sub><b>moxi</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=itxxq" title="Documentation">📖</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/qq471754603"><img src="https://avatars.githubusercontent.com/u/23146592?v=4?s=100" width="100px;" alt="qq471754603"/><br /><sub><b>qq471754603</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=qq471754603" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/san346596324"><img src="https://avatars.githubusercontent.com/u/30828520?v=4?s=100" width="100px;" alt="渭雨"/><br /><sub><b>渭雨</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=san346596324" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/luoxuanzao"><img src="https://avatars.githubusercontent.com/u/44692579?v=4?s=100" width="100px;" alt="liuxuezhuo"/><br /><sub><b>liuxuezhuo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=luoxuanzao" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/lisongning"><img src="https://avatars.githubusercontent.com/u/93140178?v=4?s=100" width="100px;" alt="lisongning"/><br /><sub><b>lisongning</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=lisongning" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/YutingNie"><img src="https://avatars.githubusercontent.com/u/104416402?v=4?s=100" width="100px;" alt="YutingNie"/><br /><sub><b>YutingNie</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=YutingNie" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=YutingNie" title="Documentation">📖</a> <a href="#design-YutingNie" title="Design">🎨</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/mikezzb"><img src="https://avatars.githubusercontent.com/u/23418428?v=4?s=100" width="100px;" alt="Mike Zhou"/><br /><sub><b>Mike Zhou</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=mikezzb" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=mikezzb" title="Documentation">📖</a> <a href="#design-mikezzb" title="Design">🎨</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/lynx009"><img src="https://avatars.githubusercontent.com/u/105542329?v=4?s=100" width="100px;" alt="lynx009"/><br /><sub><b>lynx009</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=lynx009" title="Documentation">📖</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/littlezhongzer"><img src="https://avatars.githubusercontent.com/u/33685289?v=4?s=100" width="100px;" alt="littlezhongzer"/><br /><sub><b>littlezhongzer</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=littlezhongzer" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ChenXiangxxxxx"><img src="https://avatars.githubusercontent.com/u/90089594?v=4?s=100" width="100px;" alt="ChenXiangxxxxx"/><br /><sub><b>ChenXiangxxxxx</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ChenXiangxxxxx" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Mr-zhou315"><img src="https://avatars.githubusercontent.com/u/10276100?v=4?s=100" width="100px;" alt="Mr.zhou"/><br /><sub><b>Mr.zhou</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Mr-zhou315" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/XimfengYao"><img src="https://avatars.githubusercontent.com/u/17541537?v=4?s=100" width="100px;" alt="姚贤丰"/><br /><sub><b>姚贤丰</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=XimfengYao" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/LINGLUOJUN"><img src="https://avatars.githubusercontent.com/u/16778977?v=4?s=100" width="100px;" alt="lingluojun"/><br /><sub><b>lingluojun</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=LINGLUOJUN" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="http://www.luelueking.com"><img src="https://avatars.githubusercontent.com/u/93204032?v=4?s=100" width="100px;" alt="1ue"/><br /><sub><b>1ue</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=luelueking" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="http://www.jimmyqiao.top"><img src="https://avatars.githubusercontent.com/u/67301054?v=4?s=100" width="100px;" alt="qyaaaa"/><br /><sub><b>qyaaaa</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=qyaaaa" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Aqyaaaa" title="Bug reports">🐛</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://novohit.top"><img src="https://avatars.githubusercontent.com/u/101090395?v=4?s=100" width="100px;" alt="novohit"/><br /><sub><b>novohit</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=novohit" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/rbsrcy"><img src="https://avatars.githubusercontent.com/u/4798540?v=4?s=100" width="100px;" alt="zhuoshangyi"/><br /><sub><b>zhuoshangyi</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=rbsrcy" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ruanliang-hualun"><img src="https://avatars.githubusercontent.com/u/65543716?v=4?s=100" width="100px;" alt="ruanliang"/><br /><sub><b>ruanliang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ruanliang-hualun" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=ruanliang-hualun" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Eden4701"><img src="https://avatars.githubusercontent.com/u/68422437?v=4?s=100" width="100px;" alt="Eden4701"/><br /><sub><b>Eden4701</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Eden4701" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=Eden4701" title="Documentation">📖</a> <a href="#design-Eden4701" title="Design">🎨</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/XiaTian688"><img src="https://avatars.githubusercontent.com/u/111830921?v=4?s=100" width="100px;" alt="XiaTian688"/><br /><sub><b>XiaTian688</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=XiaTian688" title="Documentation">📖</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/liyin"><img src="https://avatars.githubusercontent.com/u/863169?v=4?s=100" width="100px;" alt="liyinjiang"/><br /><sub><b>liyinjiang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=liyin" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/jiashu1024"><img src="https://avatars.githubusercontent.com/u/67859663?v=4?s=100" width="100px;" alt="ZhangJiashu"/><br /><sub><b>ZhangJiashu</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=jiashu1024" title="Documentation">📖</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/1036664317"><img src="https://avatars.githubusercontent.com/u/7696697?v=4?s=100" width="100px;" alt="moghn"/><br /><sub><b>moghn</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=1036664317" title="Documentation">📖</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/xiaoguolong"><img src="https://avatars.githubusercontent.com/u/33684988?v=4?s=100" width="100px;" alt="xiaoguolong"/><br /><sub><b>xiaoguolong</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=xiaoguolong" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Clownsw"><img src="https://avatars.githubusercontent.com/u/28394742?v=4?s=100" width="100px;" alt="Smliexx"/><br /><sub><b>Smliexx</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Clownsw" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3AClownsw" title="Bug reports">🐛</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Calvin979"><img src="https://avatars.githubusercontent.com/u/131688897?v=4?s=100" width="100px;" alt="Calvin"/><br /><sub><b>Calvin</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Calvin979" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=Calvin979" title="Code">💻</a> <a href="#design-Calvin979" title="Design">🎨</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3ACalvin979" title="Bug reports">🐛</a> <a href="https://github.com/apache/hertzbeat/commits?author=Calvin979" title="Tests">⚠️</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/bbelide2"><img src="https://avatars.githubusercontent.com/u/26840796?v=4?s=100" width="100px;" alt="Bala Sukesh"/><br /><sub><b>Bala Sukesh</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=bbelide2" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/jinyaoMa"><img src="https://avatars.githubusercontent.com/u/25066570?v=4?s=100" width="100px;" alt="Jinyao Ma"/><br /><sub><b>Jinyao Ma</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=jinyaoMa" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://linuxsuren.github.io/open-source-best-practice/"><img src="https://avatars.githubusercontent.com/u/1450685?v=4?s=100" width="100px;" alt="Rick"/><br /><sub><b>Rick</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=LinuxSuRen" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=LinuxSuRen" title="Tests">⚠️</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ZY945"><img src="https://avatars.githubusercontent.com/u/74083801?v=4?s=100" width="100px;" alt="东风"/><br /><sub><b>东风</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ZY945" title="Code">💻</a> <a href="#design-ZY945" title="Design">🎨</a> <a href="https://github.com/apache/hertzbeat/commits?author=ZY945" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3AZY945" title="Bug reports">🐛</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/prolevel1"><img src="https://avatars.githubusercontent.com/u/51995525?v=4?s=100" width="100px;" alt="sonam singh"/><br /><sub><b>sonam singh</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=prolevel1" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ZhangZixuan1994"><img src="https://avatars.githubusercontent.com/u/20011653?v=4?s=100" width="100px;" alt="ZhangZixuan1994"/><br /><sub><b>ZhangZixuan1994</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ZhangZixuan1994" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/hurenjie1"><img src="https://avatars.githubusercontent.com/u/40120355?v=4?s=100" width="100px;" alt="SHIG"/><br /><sub><b>SHIG</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=hurenjie1" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://tslj1024.github.io/"><img src="https://avatars.githubusercontent.com/u/155222677?v=4?s=100" width="100px;" alt="泰上老菌"/><br /><sub><b>泰上老菌</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=tslj1024" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ldysdu"><img src="https://avatars.githubusercontent.com/u/15815338?v=4?s=100" width="100px;" alt="ldysdu"/><br /><sub><b>ldysdu</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ldysdu" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/GEM0816g"><img src="https://avatars.githubusercontent.com/u/85116017?v=4?s=100" width="100px;" alt="梁同学"/><br /><sub><b>梁同学</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=GEM0816g" title="Code">💻</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/avvCode"><img src="https://avatars.githubusercontent.com/u/113538532?v=4?s=100" width="100px;" alt="avv"/><br /><sub><b>avv</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=avvCode" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/yqxxgh"><img src="https://avatars.githubusercontent.com/u/42080876?v=4?s=100" width="100px;" alt="yqxxgh"/><br /><sub><b>yqxxgh</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=yqxxgh" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=yqxxgh" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Ayqxxgh" title="Bug reports">🐛</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/CharlieShi46"><img src="https://avatars.githubusercontent.com/u/149798885?v=4?s=100" width="100px;" alt="CharlieShi46"/><br /><sub><b>CharlieShi46</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=CharlieShi46" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Nctllnty"><img src="https://avatars.githubusercontent.com/u/33241818?v=4?s=100" width="100px;" alt="Nctllnty"/><br /><sub><b>Nctllnty</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Nctllnty" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Wang-Yonghao"><img src="https://avatars.githubusercontent.com/u/48146606?v=4?s=100" width="100px;" alt="Wang-Yonghao"/><br /><sub><b>Wang-Yonghao</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Wang-Yonghao" title="Documentation">📖</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://www.yuque.com/dudiao/yy"><img src="https://avatars.githubusercontent.com/u/38355949?v=4?s=100" width="100px;" alt="读钓"/><br /><sub><b>读钓</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=dudiao" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/starmilkxin"><img src="https://avatars.githubusercontent.com/u/55646681?v=4?s=100" width="100px;" alt="Xin"/><br /><sub><b>Xin</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=starmilkxin" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Astarmilkxin" title="Bug reports">🐛</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/handy-git"><img src="https://avatars.githubusercontent.com/u/32837980?v=4?s=100" width="100px;" alt="handy"/><br /><sub><b>handy</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=handy-git" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/LiuTianyou"><img src="https://avatars.githubusercontent.com/u/30208283?v=4?s=100" width="100px;" alt="LiuTianyou"/><br /><sub><b>LiuTianyou</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=LiuTianyou" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=LiuTianyou" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3ALiuTianyou" title="Bug reports">🐛</a> <a href="https://github.com/apache/hertzbeat/commits?author=LiuTianyou" title="Tests">⚠️</a> <a href="#blog-LiuTianyou" title="Blogposts">📝</a> <a href="#design-LiuTianyou" title="Design">🎨</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/WinterKi1ler"><img src="https://avatars.githubusercontent.com/u/160592092?v=4?s=100" width="100px;" alt="WinterKi1ler"/><br /><sub><b>WinterKi1ler</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=WinterKi1ler" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="http://sharehoo.cn/"><img src="https://avatars.githubusercontent.com/u/45377370?v=4?s=100" width="100px;" alt="miki"/><br /><sub><b>miki</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=miki-hmt" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://codeflex.substack.com/"><img src="https://avatars.githubusercontent.com/u/85513042?v=4?s=100" width="100px;" alt="Keshav Carpenter"/><br /><sub><b>Keshav Carpenter</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=alpha951" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=alpha951" title="Documentation">📖</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/makechoicenow"><img src="https://avatars.githubusercontent.com/u/9911918?v=4?s=100" width="100px;" alt="makechoicenow"/><br /><sub><b>makechoicenow</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=makechoicenow" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/gjjjj0101"><img src="https://avatars.githubusercontent.com/u/71874373?v=4?s=100" width="100px;" alt="Gao Jian"/><br /><sub><b>Gao Jian</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=gjjjj0101" title="Tests">⚠️</a> <a href="https://github.com/apache/hertzbeat/commits?author=gjjjj0101" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=gjjjj0101" title="Documentation">📖</a> <a href="#design-gjjjj0101" title="Design">🎨</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Agjjjj0101" title="Bug reports">🐛</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://jangto.tistory.com/"><img src="https://avatars.githubusercontent.com/u/37864182?v=4?s=100" width="100px;" alt="Hyeon Sung"/><br /><sub><b>Hyeon Sung</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=dukbong" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=dukbong" title="Documentation">📖</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://crossoverjie.top/"><img src="https://avatars.githubusercontent.com/u/15684156?v=4?s=100" width="100px;" alt="crossoverJie"/><br /><sub><b>crossoverJie</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=crossoverJie" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=crossoverJie" title="Documentation">📖</a> <a href="#blog-crossoverJie" title="Blogposts">📝</a> <a href="https://github.com/apache/hertzbeat/commits?author=crossoverJie" title="Tests">⚠️</a> <a href="#design-crossoverJie" title="Design">🎨</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/PeixyJ"><img src="https://avatars.githubusercontent.com/u/45998593?v=4?s=100" width="100px;" alt="PeixyJ"/><br /><sub><b>PeixyJ</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=PeixyJ" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Hi-Mr-Wind"><img src="https://avatars.githubusercontent.com/u/85803831?v=4?s=100" width="100px;" alt="风如歌"/><br /><sub><b>风如歌</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Hi-Mr-Wind" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/MananPoojara"><img src="https://avatars.githubusercontent.com/u/104253184?v=4?s=100" width="100px;" alt="Manan Pujara"/><br /><sub><b>Manan Pujara</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=MananPoojara" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/xuziyang"><img src="https://avatars.githubusercontent.com/u/8465969?v=4?s=100" width="100px;" alt="xuziyang"/><br /><sub><b>xuziyang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=xuziyang" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=xuziyang" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Axuziyang" title="Bug reports">🐛</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/lwqzz"><img src="https://avatars.githubusercontent.com/u/62584513?v=4?s=100" width="100px;" alt="lwqzz"/><br /><sub><b>lwqzz</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=lwqzz" title="Code">💻</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/YxYL6125"><img src="https://avatars.githubusercontent.com/u/91076160?v=4?s=100" width="100px;" alt="YxYL"/><br /><sub><b>YxYL</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=YxYL6125" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/tomorrowshipyltm"><img src="https://avatars.githubusercontent.com/u/61336903?v=4?s=100" width="100px;" alt="tomorrowshipyltm"/><br /><sub><b>tomorrowshipyltm</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=tomorrowshipyltm" title="Documentation">📖</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/15613060203"><img src="https://avatars.githubusercontent.com/u/41351615?v=4?s=100" width="100px;" alt="栗磊"/><br /><sub><b>栗磊</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=15613060203" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Alanxtl"><img src="https://avatars.githubusercontent.com/u/25652981?v=4?s=100" width="100px;" alt="Alan"/><br /><sub><b>Alan</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Alanxtl" title="Documentation">📖</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="http://www.hadoop.wiki/"><img src="https://avatars.githubusercontent.com/u/29418975?v=4?s=100" width="100px;" alt="Jast"/><br /><sub><b>Jast</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=zhangshenghang" title="Code">💻</a> <a href="#ideas-zhangshenghang" title="Ideas, Planning, & Feedback">🤔</a> <a href="https://github.com/apache/hertzbeat/commits?author=zhangshenghang" title="Documentation">📖</a> <a href="#blog-zhangshenghang" title="Blogposts">📝</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Azhangshenghang" title="Bug reports">🐛</a> <a href="https://github.com/apache/hertzbeat/commits?author=zhangshenghang" title="Tests">⚠️</a> <a href="#design-zhangshenghang" title="Design">🎨</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/zuobiao-zhou"><img src="https://avatars.githubusercontent.com/u/61108539?v=4?s=100" width="100px;" alt="Zhang Yuxuan"/><br /><sub><b>Zhang Yuxuan</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=zuobiao-zhou" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=zuobiao-zhou" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Azuobiao-zhou" title="Bug reports">🐛</a> <a href="#blog-zuobiao-zhou" title="Blogposts">📝</a> <a href="https://github.com/apache/hertzbeat/commits?author=zuobiao-zhou" title="Tests">⚠️</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Pzz-2021"><img src="https://avatars.githubusercontent.com/u/118056735?v=4?s=100" width="100px;" alt="P.P."/><br /><sub><b>P.P.</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Pzz-2021" title="Code">💻</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/LLP2333"><img src="https://avatars.githubusercontent.com/u/61670545?v=4?s=100" width="100px;" alt="llp2333"/><br /><sub><b>llp2333</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=LLP2333" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/HeartLinked"><img src="https://avatars.githubusercontent.com/u/78212101?v=4?s=100" width="100px;" alt="feiyang li"/><br /><sub><b>feiyang li</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=HeartLinked" title="Documentation">📖</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Aias00"><img src="https://avatars.githubusercontent.com/u/25810623?v=4?s=100" width="100px;" alt="aias00"/><br /><sub><b>aias00</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Aias00" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=Aias00" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3AAias00" title="Bug reports">🐛</a> <a href="#ideas-Aias00" title="Ideas, Planning, & Feedback">🤔</a> <a href="https://github.com/apache/hertzbeat/commits?author=Aias00" title="Tests">⚠️</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/westboy"><img src="https://avatars.githubusercontent.com/u/6385565?v=4?s=100" width="100px;" alt="Jin"/><br /><sub><b>Jin</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=westboy" title="Documentation">📖</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://blog.csdn.net/qq_52397471"><img src="https://avatars.githubusercontent.com/u/77964041?v=4?s=100" width="100px;" alt="YuLuo"/><br /><sub><b>YuLuo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=yuluo-yx" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Ayuluo-yx" title="Bug reports">🐛</a> <a href="https://github.com/apache/hertzbeat/commits?author=yuluo-yx" title="Tests">⚠️</a> <a href="#blog-yuluo-yx" title="Blogposts">📝</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Yanshuming1"><img src="https://avatars.githubusercontent.com/u/118667222?v=4?s=100" width="100px;" alt="linDong"/><br /><sub><b>linDong</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Yanshuming1" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=Yanshuming1" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3AYanshuming1" title="Bug reports">🐛</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/lwjxy"><img src="https://avatars.githubusercontent.com/u/52726400?v=4?s=100" width="100px;" alt="lwjxy"/><br /><sub><b>lwjxy</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=lwjxy" title="Code">💻</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://thespica.github.io/"><img src="https://avatars.githubusercontent.com/u/119573640?v=4?s=100" width="100px;" alt="John"/><br /><sub><b>John</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Thespica" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=Thespica" title="Documentation">📖</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/boatrainlsz"><img src="https://avatars.githubusercontent.com/u/18243785?v=4?s=100" width="100px;" alt="boatrainlsz"/><br /><sub><b>boatrainlsz</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=boatrainlsz" title="Documentation">📖</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://www.yitianyigexiangfa.com/"><img src="https://avatars.githubusercontent.com/u/3973419?v=4?s=100" width="100px;" alt="Bill Lau"/><br /><sub><b>Bill Lau</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=JavaProgrammerLB" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/lw-yang"><img src="https://avatars.githubusercontent.com/u/23456873?v=4?s=100" width="100px;" alt="lwyang"/><br /><sub><b>lwyang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=lw-yang" title="Documentation">📖</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/xfl12345"><img src="https://avatars.githubusercontent.com/u/17960863?v=4?s=100" width="100px;" alt="xfl12345"/><br /><sub><b>xfl12345</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=xfl12345" title="Documentation">📖</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/yykaue"><img src="https://avatars.githubusercontent.com/u/22905143?v=4?s=100" width="100px;" alt="Limbo"/><br /><sub><b>Limbo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=yykaue" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/irenhongyan"><img src="https://avatars.githubusercontent.com/u/53438321?v=4?s=100" width="100px;" alt="哈哈哈哈哈哈哈哈哈"/><br /><sub><b>哈哈哈哈哈哈哈哈哈</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=irenhongyan" title="Code">💻</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ileonli"><img src="https://avatars.githubusercontent.com/u/45332412?v=4?s=100" width="100px;" alt="Leon Li"/><br /><sub><b>Leon Li</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ileonli" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="http://fnil.net/"><img src="https://avatars.githubusercontent.com/u/14142?v=4?s=100" width="100px;" alt="dennis zhuang"/><br /><sub><b>dennis zhuang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=killme2008" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/kerwin612"><img src="https://avatars.githubusercontent.com/u/3371163?v=4?s=100" width="100px;" alt="Kerwin Bryant"/><br /><sub><b>Kerwin Bryant</b></sub></a><br /><a href="#design-kerwin612" title="Design">🎨</a> <a href="https://github.com/apache/hertzbeat/commits?author=kerwin612" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=kerwin612" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Akerwin612" title="Bug reports">🐛</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ShineDevelopment"><img src="https://avatars.githubusercontent.com/u/59306780?v=4?s=100" width="100px;" alt="daixianglong"/><br /><sub><b>daixianglong</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ShineDevelopment" title="Documentation">📖</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/mchgood"><img src="https://avatars.githubusercontent.com/u/38482005?v=4?s=100" width="100px;" alt="mchgood"/><br /><sub><b>mchgood</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=mchgood" title="Documentation">📖</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/pwallk"><img src="https://avatars.githubusercontent.com/u/69385076?v=4?s=100" width="100px;" alt="kangli"/><br /><sub><b>kangli</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=pwallk" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=pwallk" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Apwallk" title="Bug reports">🐛</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/cdphantom"><img src="https://avatars.githubusercontent.com/u/12674795?v=4?s=100" width="100px;" alt="cdphantom"/><br /><sub><b>cdphantom</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=cdphantom" title="Code">💻</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/asd108908382"><img src="https://avatars.githubusercontent.com/u/77717999?v=4?s=100" width="100px;" alt="jiawei.guo"/><br /><sub><b>jiawei.guo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=asd108908382" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/QBH-insist"><img src="https://avatars.githubusercontent.com/u/39401478?v=4?s=100" width="100px;" alt="QBH-insist"/><br /><sub><b>QBH-insist</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=QBH-insist" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/jiangsh-ui"><img src="https://avatars.githubusercontent.com/u/86990361?v=4?s=100" width="100px;" alt="jiangsh"/><br /><sub><b>jiangsh</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=jiangsh-ui" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/keaifafafa"><img src="https://avatars.githubusercontent.com/u/83876361?v=4?s=100" width="100px;" alt="Keaifa"/><br /><sub><b>Keaifa</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=keaifafafa" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Akeaifafafa" title="Bug reports">🐛</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/loong95"><img src="https://avatars.githubusercontent.com/u/16333958?v=4?s=100" width="100px;" alt="Loong"/><br /><sub><b>Loong</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=loong95" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ceekay47"><img src="https://avatars.githubusercontent.com/u/104664857?v=4?s=100" width="100px;" alt="Chandrakant Vankayalapati"/><br /><sub><b>Chandrakant Vankayalapati</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ceekay47" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/MRgenial"><img src="https://avatars.githubusercontent.com/u/49973336?v=4?s=100" width="100px;" alt="b_mountain"/><br /><sub><b>b_mountain</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=MRgenial" title="Code">💻</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/TemirlanBasitov"><img src="https://avatars.githubusercontent.com/u/57500808?v=4?s=100" width="100px;" alt="TemirlanBasitov"/><br /><sub><b>TemirlanBasitov</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=TemirlanBasitov" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/wyfvsfy"><img src="https://avatars.githubusercontent.com/u/11973517?v=4?s=100" width="100px;" alt="wyfvsfy"/><br /><sub><b>wyfvsfy</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=wyfvsfy" title="Documentation">📖</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/sherry-peng2333"><img src="https://avatars.githubusercontent.com/u/70619577?v=4?s=100" width="100px;" alt="sherry-peng2333"/><br /><sub><b>sherry-peng2333</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=sherry-peng2333" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/lixiaobaivv"><img src="https://avatars.githubusercontent.com/u/39290771?v=4?s=100" width="100px;" alt="Yzzz"/><br /><sub><b>Yzzz</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=lixiaobaivv" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="http://www.bckf.cn/"><img src="https://avatars.githubusercontent.com/u/13309008?v=4?s=100" width="100px;" alt="puruidong"/><br /><sub><b>puruidong</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=pruidong" title="Documentation">📖</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/shinestare"><img src="https://avatars.githubusercontent.com/u/13570619?v=4?s=100" width="100px;" alt="shinestare"/><br /><sub><b>shinestare</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=shinestare" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/po-168"><img src="https://avatars.githubusercontent.com/u/185745593?v=4?s=100" width="100px;" alt="po-168"/><br /><sub><b>po-168</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=po-168" title="Code">💻</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/All-The-Best-for"><img src="https://avatars.githubusercontent.com/u/76414672?v=4?s=100" width="100px;" alt="wbs99"/><br /><sub><b>wbs99</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=All-The-Best-for" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/starryCoder"><img src="https://avatars.githubusercontent.com/u/46510059?v=4?s=100" width="100px;" alt="starryCoder"/><br /><sub><b>starryCoder</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=starryCoder" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/hasimmollah"><img src="https://avatars.githubusercontent.com/u/32538599?v=4?s=100" width="100px;" alt="hasimmollah"/><br /><sub><b>hasimmollah</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=hasimmollah" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ayu-v0"><img src="https://avatars.githubusercontent.com/u/127600988?v=4?s=100" width="100px;" alt="Ayu"/><br /><sub><b>Ayu</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ayu-v0" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Rancho-7"><img src="https://avatars.githubusercontent.com/u/59016860?v=4?s=100" width="100px;" alt="Nick Guo"/><br /><sub><b>Nick Guo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Rancho-7" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=Rancho-7" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3ARancho-7" title="Bug reports">🐛</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/doveLin0818"><img src="https://avatars.githubusercontent.com/u/190927907?v=4?s=100" width="100px;" alt="doveLin"/><br /><sub><b>doveLin</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=doveLin0818" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://zzrl.cc/"><img src="https://avatars.githubusercontent.com/u/91836599?v=4?s=100" width="100px;" alt="yunfan24"/><br /><sub><b>yunfan24</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=yunfan24" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=yunfan24" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Ayunfan24" title="Bug reports">🐛</a> <a href="https://github.com/apache/hertzbeat/commits?author=yunfan24" title="Tests">⚠️</a> <a href="#blog-yunfan24" title="Blogposts">📝</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/lctking"><img src="https://avatars.githubusercontent.com/u/168249998?v=4?s=100" width="100px;" alt="nullwli"/><br /><sub><b>nullwli</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=lctking" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://simonsigre.com/"><img src="https://avatars.githubusercontent.com/u/14932913?v=4?s=100" width="100px;" alt="Simon Sigré"/><br /><sub><b>Simon Sigré</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=simonsigre" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=simonsigre" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="http://www.ponfee.cn/"><img src="https://avatars.githubusercontent.com/u/46117331?v=4?s=100" width="100px;" alt="ponfee"/><br /><sub><b>ponfee</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ponfee" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Vedant7789"><img src="https://avatars.githubusercontent.com/u/147625492?v=4?s=100" width="100px;" alt="Vedant7789"/><br /><sub><b>Vedant7789</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Vedant7789" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Craaaaazy77"><img src="https://avatars.githubusercontent.com/u/23025522?v=4?s=100" width="100px;" alt="Craaaaazy77"/><br /><sub><b>Craaaaazy77</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Craaaaazy77" title="Documentation">📖</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Suvrat1629"><img src="https://avatars.githubusercontent.com/u/140749446?v=4?s=100" width="100px;" alt="Suvrat1629"/><br /><sub><b>Suvrat1629</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Suvrat1629" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="http://ghyghoo8.github.io/"><img src="https://avatars.githubusercontent.com/u/363129?v=4?s=100" width="100px;" alt="ghy"/><br /><sub><b>ghy</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ghyghoo8" title="Code">💻</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/helei1030"><img src="https://avatars.githubusercontent.com/u/11839080?v=4?s=100" width="100px;" alt="helei1030"/><br /><sub><b>helei1030</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=helei1030" title="Documentation">📖</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://medium.com/@pjfanning"><img src="https://avatars.githubusercontent.com/u/11783444?v=4?s=100" width="100px;" alt="PJ Fanning"/><br /><sub><b>PJ Fanning</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=pjfanning" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Apjfanning" title="Bug reports">🐛</a> <a href="https://github.com/apache/hertzbeat/commits?author=pjfanning" title="Tests">⚠️</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/MonsterChenzhuo"><img src="https://avatars.githubusercontent.com/u/60029759?v=4?s=100" width="100px;" alt="monster"/><br /><sub><b>monster</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=MonsterChenzhuo" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/MasamiYui"><img src="https://avatars.githubusercontent.com/u/22274133?v=4?s=100" width="100px;" alt="Sherlock Yin"/><br /><sub><b>Sherlock Yin</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=MasamiYui" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=MasamiYui" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3AMasamiYui" title="Bug reports">🐛</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/wanhao23"><img src="https://avatars.githubusercontent.com/u/29560961?v=4?s=100" width="100px;" alt="wanhao"/><br /><sub><b>wanhao</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=wanhao23" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=wanhao23" title="Documentation">📖</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/jonasHanhan"><img src="https://avatars.githubusercontent.com/u/130035609?v=4?s=100" width="100px;" alt="jonasHanhan"/><br /><sub><b>jonasHanhan</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=jonasHanhan" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/NikhilMurugesan"><img src="https://avatars.githubusercontent.com/u/49281792?v=4?s=100" width="100px;" alt="NikhilMurugesan"/><br /><sub><b>NikhilMurugesan</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=NikhilMurugesan" title="Code">💻</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/myangle1120"><img src="https://avatars.githubusercontent.com/u/19237013?v=4?s=100" width="100px;" alt="myangle1120"/><br /><sub><b>myangle1120</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=myangle1120" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/yasminvo"><img src="https://avatars.githubusercontent.com/u/107528848?v=4?s=100" width="100px;" alt="yasminvo"/><br /><sub><b>yasminvo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=yasminvo" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/notbugggg"><img src="https://avatars.githubusercontent.com/u/147966331?v=4?s=100" width="100px;" alt="不关银渐层的事哦"/><br /><sub><b>不关银渐层的事哦</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=notbugggg" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Anotbugggg" title="Bug reports">🐛</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/yyahang"><img src="https://avatars.githubusercontent.com/u/90464876?v=4?s=100" width="100px;" alt="yyahang"/><br /><sub><b>yyahang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=yyahang" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/JuJinPark"><img src="https://avatars.githubusercontent.com/u/44892459?v=4?s=100" width="100px;" alt="jujin"/><br /><sub><b>jujin</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=JuJinPark" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=JuJinPark" title="Documentation">📖</a> <a href="#ideas-JuJinPark" title="Ideas, Planning, & Feedback">🤔</a> <a href="#blog-JuJinPark" title="Blogposts">📝</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/LL-LIN"><img src="https://avatars.githubusercontent.com/u/43002118?v=4?s=100" width="100px;" alt="LL-LIN"/><br /><sub><b>LL-LIN</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=LL-LIN" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3ALL-LIN" title="Bug reports">🐛</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://bigcyy.github.io/"><img src="https://avatars.githubusercontent.com/u/73413979?v=4?s=100" width="100px;" alt="Yang Chen"/><br /><sub><b>Yang Chen</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=bigcyy" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=bigcyy" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Abigcyy" title="Bug reports">🐛</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/sarthakeash"><img src="https://avatars.githubusercontent.com/u/74091160?v=4?s=100" width="100px;" alt="Sarthak Arora"/><br /><sub><b>Sarthak Arora</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=sarthakeash" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=sarthakeash" title="Documentation">📖</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/PengJingzhao"><img src="https://avatars.githubusercontent.com/u/97368949?v=4?s=100" width="100px;" alt="彭镜肇"/><br /><sub><b>彭镜肇</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=PengJingzhao" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/gagaradio"><img src="https://avatars.githubusercontent.com/u/18532370?v=4?s=100" width="100px;" alt="Walter Jia"/><br /><sub><b>Walter Jia</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=gagaradio" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/boyucjz"><img src="https://avatars.githubusercontent.com/u/18730041?v=4?s=100" width="100px;" alt="boyucjz"/><br /><sub><b>boyucjz</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=boyucjz" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Cyanty"><img src="https://avatars.githubusercontent.com/u/153884653?v=4?s=100" width="100px;" alt="Cyanty"/><br /><sub><b>Cyanty</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Cyanty" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=Cyanty" title="Documentation">📖</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/KevinLLF"><img src="https://avatars.githubusercontent.com/u/85452733?v=4?s=100" width="100px;" alt="Jay丿167"/><br /><sub><b>Jay丿167</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=KevinLLF" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Duansg"><img src="https://avatars.githubusercontent.com/u/112607719?v=4?s=100" width="100px;" alt="Duansg"/><br /><sub><b>Duansg</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Duansg" title="Documentation">📖</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/xiaomizhou2"><img src="https://avatars.githubusercontent.com/u/47807926?v=4?s=100" width="100px;" alt="zhangyaxi"/><br /><sub><b>zhangyaxi</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=xiaomizhou2" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=xiaomizhou2" title="Tests">⚠️</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/RainBondsongyg"><img src="https://avatars.githubusercontent.com/u/94501396?v=4?s=100" width="100px;" alt="songyg"/><br /><sub><b>songyg</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=RainBondsongyg" title="Documentation">📖</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/lx1229"><img src="https://avatars.githubusercontent.com/u/44620005?v=4?s=100" width="100px;" alt="Liuxin"/><br /><sub><b>Liuxin</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=lx1229" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/yy549159265"><img src="https://avatars.githubusercontent.com/u/40821310?v=4?s=100" width="100px;" alt="yy549159265"/><br /><sub><b>yy549159265</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=yy549159265" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=yy549159265" title="Tests">⚠️</a> <a href="#design-yy549159265" title="Design">🎨</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/cto-huhang"><img src="https://avatars.githubusercontent.com/u/53338629?v=4?s=100" width="100px;" alt="cto-huhang"/><br /><sub><b>cto-huhang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=cto-huhang" title="Documentation">📖</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Saramanda9988"><img src="https://avatars.githubusercontent.com/u/176664901?v=4?s=100" width="100px;" alt="LunaRain_079"/><br /><sub><b>LunaRain_079</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Saramanda9988" title="Documentation">📖</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/delei"><img src="https://avatars.githubusercontent.com/u/17263766?v=4?s=100" width="100px;" alt="DeleiGuo"/><br /><sub><b>DeleiGuo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=delei" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=delei" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=delei" title="Tests">⚠️</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Adelei" title="Bug reports">🐛</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/chingjustwe"><img src="https://avatars.githubusercontent.com/u/13643747?v=4?s=100" width="100px;" alt="Rocky, Chi"/><br /><sub><b>Rocky, Chi</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=chingjustwe" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/rowankid"><img src="https://avatars.githubusercontent.com/u/18652781?v=4?s=100" width="100px;" alt="Wenqi Luo"/><br /><sub><b>Wenqi Luo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/issues?q=author%3Arowankid" title="Bug reports">🐛</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/tuzuy"><img src="https://avatars.githubusercontent.com/u/95274591?v=4?s=100" width="100px;" alt="tuzuy"/><br /><sub><b>tuzuy</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=tuzuy" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/carlpinto25"><img src="https://avatars.githubusercontent.com/u/117299909?v=4?s=100" width="100px;" alt="carl pinto"/><br /><sub><b>carl pinto</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=carlpinto25" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://cxhello.top/"><img src="https://avatars.githubusercontent.com/u/49056040?v=4?s=100" width="100px;" alt="cxhello"/><br /><sub><b>cxhello</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=cxhello" title="Documentation">📖</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/jl15988"><img src="https://avatars.githubusercontent.com/u/70638770?v=4?s=100" width="100px;" alt="会功夫的李白"/><br /><sub><b>会功夫的李白</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=jl15988" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://blog.aytop.cloud/"><img src="https://avatars.githubusercontent.com/u/37127008?v=4?s=100" width="100px;" alt="Albert.Yang"/><br /><sub><b>Albert.Yang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=AlbertYang0801" title="Code">💻</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://blog.tokenlen.top/"><img src="https://avatars.githubusercontent.com/u/150590575?v=4?s=100" width="100px;" alt="zhou yong kang"/><br /><sub><b>zhou yong kang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=mengnankkkk" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/warrobe"><img src="https://avatars.githubusercontent.com/u/89446159?v=4?s=100" width="100px;" alt="warrobe"/><br /><sub><b>warrobe</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=warrobe" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Jetiaime"><img src="https://avatars.githubusercontent.com/u/93769000?v=4?s=100" width="100px;" alt="TeAmo"/><br /><sub><b>TeAmo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Jetiaime" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/pentium100"><img src="https://avatars.githubusercontent.com/u/27917?v=4?s=100" width="100px;" alt="pentium100"/><br /><sub><b>pentium100</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=pentium100" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/dedyks"><img src="https://avatars.githubusercontent.com/u/23741665?v=4?s=100" width="100px;" alt="Dedy Kurniawan Santoso"/><br /><sub><b>Dedy Kurniawan Santoso</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=dedyks" title="Code">💻</a></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<!-- markdownlint-restore -->
|
||||||
|
<!-- prettier-ignore-end -->
|
||||||
|
|
||||||
|
<!-- ALL-CONTRIBUTORS-LIST:END -->
|
||||||
|
|
||||||
|
## 💬 コミュニティ交流
|
||||||
|
|
||||||
|
##### チャネル
|
||||||
|
|
||||||
|
[メール](https://lists.apache.org/list.html?dev@hertzbeat.apache.org) : メーリングリストに登録するために、```dev-subscribe@hertzbeat.apache.org```にメールを送ってください。
|
||||||
|
|
||||||
|
[Chat On Discord](https://discord.gg/Fb6M73htGr)
|
||||||
|
|
||||||
|
WeChatグループ : `ahertzbeat` を検索.
|
||||||
|
|
||||||
|
WeChat公式アカウント : `usthecom`を検索.
|
||||||
|
|
||||||
|
[QQグループ](https://qm.qq.com/q/xxqecSC2cw) : グループ番号 `1035688434`
|
||||||
|
|
||||||
|
[Github Discussion](https://github.com/apache/hertzbeat/discussions)
|
||||||
|
|
||||||
|
[Reddit Community](https://www.reddit.com/r/hertzbeat/)
|
||||||
|
|
||||||
|
[Follow Us Twitter](https://x.com/hertzbeat1024)
|
||||||
|
|
||||||
|
[Subscribe YouTube](https://www.youtube.com/channel/UCri75zfWX0GHqJFPENEbLow)
|
||||||
|
|
||||||
|
|
||||||
|
##### Open-Source Project Build From Open-Source
|
||||||
|
|
||||||
|
HertzBeat is built on so many great open source projects, thanks to them!
|
||||||
|
|
||||||
|
- `Java Spring SpringBoot Jpa Maven Assembly Netty Lombok Sureness Protobuf HttpClient Guava SnakeYaml JsonPath ...`
|
||||||
|
- `TypeScript Angular NG-ZORRO NG-ALAIN NodeJs Npm Html Less Echarts Rxjs ZoneJs MonacoEditor SlickCarousel Docusaurus ...`
|
||||||
|
|
||||||
|
|
||||||
|
## Landscape
|
||||||
|
|
||||||
|
<p align="left">
|
||||||
|
<img src="./home/static/img/home/cncf-landscape-left-logo.svg" width="300"> <img src="./home/static/img/home/cncf-right-logo.svg" width="345" />
|
||||||
|
<br /><br />
|
||||||
|
HertzBeat has been included in the <a href="https://landscape.cncf.io/?item=observability-and-analysis--observability--hertzbeat">
|
||||||
|
CNCF Observability And Analysis - Observability Landscape.</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
## 🛡️ License
|
||||||
|
[`Apache License, Version 2.0`](https://www.apache.org/licenses/LICENSE-2.0.html)
|
||||||
+3
-3
@@ -1,6 +1,6 @@
|
|||||||
# Security Policy
|
# Security Policy
|
||||||
|
|
||||||
Apache HertzBeat follows the [ASF vulnerability handling process](https://apache.org/security/#vulnerability-handling)
|
Apache HertzBeat™ follows the [ASF vulnerability handling process](https://apache.org/security/#vulnerability-handling)
|
||||||
|
|
||||||
## Supported Versions
|
## Supported Versions
|
||||||
|
|
||||||
@@ -17,6 +17,6 @@ To report a new vulnerability you have discovered please follow the [ASF vulnera
|
|||||||
|
|
||||||
## Extra Information
|
## Extra Information
|
||||||
|
|
||||||
- [Apache HertzBeat Security Model](https://hertzbeat.apache.org/docs/help/security_model)
|
- [Apache HertzBeat™ Security Model](https://hertzbeat.apache.org/docs/help/security_model)
|
||||||
- [ASF Security](https://www.apache.org/security/)
|
- [ASF Security](https://www.apache.org/security/)
|
||||||
- [Apache HertzBeat Document](https://hertzbeat.apache.org/docs/)
|
- [Apache HertzBeat™ Document](https://hertzbeat.apache.org/docs/)
|
||||||
|
|||||||
+1
-1
@@ -57,7 +57,7 @@ items:
|
|||||||
"credential": "hertzbeat"
|
"credential": "hertzbeat"
|
||||||
}
|
}
|
||||||
expect:
|
expect:
|
||||||
statusCode: 409
|
statusCode: 500
|
||||||
- name: monitorList
|
- name: monitorList
|
||||||
request:
|
request:
|
||||||
api: /api/monitors?pageIndex=0&pageSize=8
|
api: /api/monitors?pageIndex=0&pageSize=8
|
||||||
|
|||||||
@@ -0,0 +1,106 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!--
|
||||||
|
~ Licensed to the Apache Software Foundation (ASF) under one or more
|
||||||
|
~ contributor license agreements. See the NOTICE file distributed with
|
||||||
|
~ this work for additional information regarding copyright ownership.
|
||||||
|
~ The ASF licenses this file to You under the Apache License, Version 2.0
|
||||||
|
~ (the "License"); you may not use this file except in compliance with
|
||||||
|
~ the License. You may obtain a copy of the License at
|
||||||
|
~
|
||||||
|
~ http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
~
|
||||||
|
~ Unless required by applicable law or agreed to in writing, software
|
||||||
|
~ distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
~ See the License for the specific language governing permissions and
|
||||||
|
~ limitations under the License.
|
||||||
|
-->
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
<parent>
|
||||||
|
<groupId>org.apache.hertzbeat</groupId>
|
||||||
|
<artifactId>hertzbeat</artifactId>
|
||||||
|
<version>2.0-SNAPSHOT</version>
|
||||||
|
</parent>
|
||||||
|
<artifactId>hertzbeat-ai</artifactId>
|
||||||
|
<version>${hertzbeat.version}</version>
|
||||||
|
<properties>
|
||||||
|
<spring-ai.version>1.0.3</spring-ai.version>
|
||||||
|
<java.version>21</java.version>
|
||||||
|
</properties>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.ai</groupId>
|
||||||
|
<artifactId>spring-ai-starter-mcp-server-webmvc</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-webflux</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-test</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.junit.platform</groupId>
|
||||||
|
<artifactId>junit-platform-launcher</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.ai</groupId>
|
||||||
|
<artifactId>spring-ai-openai</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.ai</groupId>
|
||||||
|
<artifactId>spring-ai-client-chat</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.apache.hertzbeat</groupId>
|
||||||
|
<artifactId>hertzbeat-common</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<!-- common -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.apache.hertzbeat</groupId>
|
||||||
|
<artifactId>hertzbeat-base</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.apache.hertzbeat</groupId>
|
||||||
|
<artifactId>hertzbeat-alerter</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.apache.hertzbeat</groupId>
|
||||||
|
<artifactId>hertzbeat-manager</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.usthe.sureness</groupId>
|
||||||
|
<artifactId>spring-boot3-starter-sureness</artifactId>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
<dependencyManagement>
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.ai</groupId>
|
||||||
|
<artifactId>spring-ai-bom</artifactId>
|
||||||
|
<version>${spring-ai.version}</version>
|
||||||
|
<type>pom</type>
|
||||||
|
<scope>import</scope>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
</dependencyManagement>
|
||||||
|
|
||||||
|
<build>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||||
|
<configuration>
|
||||||
|
<skip>true</skip>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
|
||||||
|
</project>
|
||||||
+246
@@ -0,0 +1,246 @@
|
|||||||
|
/*
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||||
|
* contributor license agreements. See the NOTICE file distributed with
|
||||||
|
* this work for additional information regarding copyright ownership.
|
||||||
|
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||||
|
* (the "License"); you may not use this file except in compliance with
|
||||||
|
* the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.apache.hertzbeat.ai.config;
|
||||||
|
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.type.TypeReference;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.usthe.sureness.mgt.SurenessSecurityManager;
|
||||||
|
import com.usthe.sureness.subject.SubjectSum;
|
||||||
|
import io.modelcontextprotocol.spec.McpError;
|
||||||
|
import io.modelcontextprotocol.spec.McpSchema;
|
||||||
|
import io.modelcontextprotocol.spec.McpServerSession;
|
||||||
|
import io.modelcontextprotocol.spec.McpServerTransport;
|
||||||
|
import io.modelcontextprotocol.spec.McpServerTransportProvider;
|
||||||
|
import io.modelcontextprotocol.util.Assert;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.web.servlet.function.RouterFunction;
|
||||||
|
import org.springframework.web.servlet.function.RouterFunctions;
|
||||||
|
import org.springframework.web.servlet.function.ServerRequest;
|
||||||
|
import org.springframework.web.servlet.function.ServerResponse;
|
||||||
|
import reactor.core.publisher.Flux;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom Server-Sent Events transport provider for Model Context Protocol.
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
public class CustomSseServerTransport implements McpServerTransportProvider {
|
||||||
|
private final ObjectMapper objectMapper;
|
||||||
|
private final String messageEndpoint;
|
||||||
|
private final String sseEndpoint;
|
||||||
|
private final String baseUrl;
|
||||||
|
@Getter
|
||||||
|
private final RouterFunction<ServerResponse> routerFunction;
|
||||||
|
@Setter
|
||||||
|
private McpServerSession.Factory sessionFactory;
|
||||||
|
private final Map<String, Object> sessionRequest = new HashMap<>();
|
||||||
|
private final ConcurrentHashMap<String, McpServerSession> sessions;
|
||||||
|
private volatile boolean isClosing;
|
||||||
|
|
||||||
|
public CustomSseServerTransport(ObjectMapper objectMapper, String messageEndpoint) {
|
||||||
|
this(objectMapper, messageEndpoint, "/sse");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public CustomSseServerTransport(ObjectMapper objectMapper, String messageEndpoint, String sseEndpoint) {
|
||||||
|
this(objectMapper, "", messageEndpoint, sseEndpoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
public CustomSseServerTransport(ObjectMapper objectMapper, String baseUrl, String messageEndpoint, String sseEndpoint) {
|
||||||
|
this.sessions = new ConcurrentHashMap();
|
||||||
|
this.isClosing = false;
|
||||||
|
Assert.notNull(objectMapper, "ObjectMapper must not be null");
|
||||||
|
Assert.notNull(baseUrl, "Message base URL must not be null");
|
||||||
|
Assert.notNull(messageEndpoint, "Message endpoint must not be null");
|
||||||
|
Assert.notNull(sseEndpoint, "SSE endpoint must not be null");
|
||||||
|
this.objectMapper = objectMapper;
|
||||||
|
this.baseUrl = baseUrl;
|
||||||
|
this.messageEndpoint = messageEndpoint;
|
||||||
|
this.sseEndpoint = sseEndpoint;
|
||||||
|
this.routerFunction = RouterFunctions.route().GET(this.sseEndpoint, this::handleSseConnection).POST(this.messageEndpoint, this::handleMessage).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Mono<Void> notifyClients(String method, Object params) {
|
||||||
|
if (this.sessions.isEmpty()) {
|
||||||
|
log.debug("No active sessions to broadcast message to");
|
||||||
|
return Mono.empty();
|
||||||
|
} else {
|
||||||
|
log.debug("Attempting to broadcast message to {} active sessions", this.sessions.size());
|
||||||
|
return Flux.fromIterable(this.sessions.values())
|
||||||
|
.flatMap((session) -> session.sendNotification(method, params)
|
||||||
|
.doOnError((e) -> log.error("Failed to send message to session {}: {}", session.getId(), e.getMessage()))
|
||||||
|
.onErrorComplete())
|
||||||
|
.then();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Mono<Void> closeGracefully() {
|
||||||
|
return Flux.fromIterable(this.sessions.values()).doFirst(() -> {
|
||||||
|
this.isClosing = true;
|
||||||
|
log.debug("Initiating graceful shutdown with {} active sessions", this.sessions.size());
|
||||||
|
}).flatMap(McpServerSession::closeGracefully).then().doOnSuccess((v) -> log.debug("Graceful shutdown completed"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private ServerResponse handleSseConnection(ServerRequest request) {
|
||||||
|
log.debug("Handling SSE connection for request: {}", request);
|
||||||
|
HttpServletRequest servletRequest = request.servletRequest();
|
||||||
|
|
||||||
|
try {
|
||||||
|
|
||||||
|
log.debug("Processing SSE connection for servlet request: {}", servletRequest);
|
||||||
|
log.debug("Authorization header: {}", servletRequest.getHeader("Authorization"));
|
||||||
|
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Authentication failed for SSE connection: {}", e.getMessage());
|
||||||
|
return ServerResponse.status(HttpStatus.UNAUTHORIZED).body("Unauthorized: " + e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if (this.isClosing) {
|
||||||
|
return ServerResponse.status(HttpStatus.SERVICE_UNAVAILABLE).body("Server is shutting down");
|
||||||
|
} else {
|
||||||
|
String sessionId = UUID.randomUUID().toString();
|
||||||
|
log.debug("Generated session ID for SSE connection: {}", sessionId);
|
||||||
|
log.debug("Creating new SSE connection for session: {}", sessionId);
|
||||||
|
|
||||||
|
|
||||||
|
return ServerResponse.sse((sseBuilder) -> {
|
||||||
|
sseBuilder.onComplete(() -> {
|
||||||
|
log.debug("SSE connection completed for session: {}", sessionId);
|
||||||
|
this.sessions.remove(sessionId);
|
||||||
|
});
|
||||||
|
sseBuilder.onTimeout(() -> {
|
||||||
|
log.debug("SSE connection timed out for session: {}", sessionId);
|
||||||
|
this.sessions.remove(sessionId);
|
||||||
|
});
|
||||||
|
CustomSseServerTransport.WebMvcMcpSessionTransport sessionTransport = new CustomSseServerTransport.WebMvcMcpSessionTransport(sessionId, sseBuilder);
|
||||||
|
McpServerSession session = this.sessionFactory.create(sessionTransport);
|
||||||
|
this.sessionRequest.put(sessionId, request.servletRequest());
|
||||||
|
this.sessions.put(sessionId, session);
|
||||||
|
|
||||||
|
try {
|
||||||
|
sseBuilder.id(sessionId).event("endpoint").data(this.baseUrl + this.messageEndpoint + "?sessionId=" + sessionId);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Failed to send initial endpoint event: {}", e.getMessage());
|
||||||
|
sseBuilder.error(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
}, Duration.ZERO);
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private ServerResponse handleMessage(ServerRequest request) {
|
||||||
|
if (this.isClosing) {
|
||||||
|
return ServerResponse.status(HttpStatus.SERVICE_UNAVAILABLE).body("Server is shutting down");
|
||||||
|
} else if (request.param("sessionId").isEmpty()) {
|
||||||
|
return ServerResponse.badRequest().body(new McpError("Session ID missing in message endpoint"));
|
||||||
|
} else {
|
||||||
|
String sessionId = (String) request.param("sessionId").get();
|
||||||
|
McpServerSession session = (McpServerSession) this.sessions.get(sessionId);
|
||||||
|
log.debug("Authorization header for message request: {}", request.servletRequest().getHeader("Authorization"));
|
||||||
|
SubjectSum subject = SurenessSecurityManager.getInstance().checkIn(sessionRequest.get(sessionId));
|
||||||
|
McpContextHolder.setSubject(subject);
|
||||||
|
|
||||||
|
|
||||||
|
if (session == null) {
|
||||||
|
return ServerResponse.status(HttpStatus.NOT_FOUND).body(new McpError("Session not found: " + sessionId));
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
String body = request.body(String.class);
|
||||||
|
McpSchema.JSONRPCMessage message = McpSchema.deserializeJsonRpcMessage(this.objectMapper, body);
|
||||||
|
session.handle(message).block();
|
||||||
|
return ServerResponse.ok().build();
|
||||||
|
} catch (IOException | IllegalArgumentException e) {
|
||||||
|
log.error("Failed to deserialize message: {}", ((Exception) e).getMessage());
|
||||||
|
return ServerResponse.badRequest().body(new McpError("Invalid message format"));
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Error handling message: {}", e.getMessage());
|
||||||
|
return ServerResponse.status(HttpStatus.INTERNAL_SERVER_ERROR).body(new McpError(e.getMessage()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class WebMvcMcpSessionTransport implements McpServerTransport {
|
||||||
|
private final String sessionId;
|
||||||
|
private final ServerResponse.SseBuilder sseBuilder;
|
||||||
|
|
||||||
|
WebMvcMcpSessionTransport(String sessionId, ServerResponse.SseBuilder sseBuilder) {
|
||||||
|
this.sessionId = sessionId;
|
||||||
|
this.sseBuilder = sseBuilder;
|
||||||
|
log.debug("Session transport {} initialized with SSE builder", sessionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Mono<Void> sendMessage(McpSchema.JSONRPCMessage message) {
|
||||||
|
return Mono.fromRunnable(() -> {
|
||||||
|
try {
|
||||||
|
String jsonText = CustomSseServerTransport.this.objectMapper.writeValueAsString(message);
|
||||||
|
this.sseBuilder.id(this.sessionId).event("message").data(jsonText);
|
||||||
|
log.debug("Message sent to session {}", this.sessionId);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Failed to send message to session {}: {}", this.sessionId, e.getMessage());
|
||||||
|
this.sseBuilder.error(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public <T> T unmarshalFrom(Object data, TypeReference<T> typeRef) {
|
||||||
|
return (T) CustomSseServerTransport.this.objectMapper.convertValue(data, typeRef);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Mono<Void> closeGracefully() {
|
||||||
|
return Mono.fromRunnable(() -> {
|
||||||
|
log.debug("Closing session transport: {}", this.sessionId);
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.sseBuilder.complete();
|
||||||
|
log.debug("Successfully completed SSE builder for session {}", this.sessionId);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("Failed to complete SSE builder for session {}: {}", this.sessionId, e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void close() {
|
||||||
|
try {
|
||||||
|
this.sseBuilder.complete();
|
||||||
|
log.debug("Successfully completed SSE builder for session {}", this.sessionId);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("Failed to complete SSE builder for session {}: {}", this.sessionId, e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
/*
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||||
|
* contributor license agreements. See the NOTICE file distributed with
|
||||||
|
* this work for additional information regarding copyright ownership.
|
||||||
|
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||||
|
* (the "License"); you may not use this file except in compliance with
|
||||||
|
* the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
package org.apache.hertzbeat.ai.config;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.hertzbeat.common.support.event.AiProviderConfigChangeEvent;
|
||||||
|
import org.apache.hertzbeat.common.entity.dto.ModelProviderConfig;
|
||||||
|
import org.apache.hertzbeat.base.dao.GeneralConfigDao;
|
||||||
|
import org.apache.hertzbeat.common.entity.manager.GeneralConfig;
|
||||||
|
import org.apache.hertzbeat.common.util.JsonUtil;
|
||||||
|
import org.springframework.ai.chat.client.ChatClient;
|
||||||
|
import org.springframework.ai.openai.OpenAiChatModel;
|
||||||
|
import org.springframework.ai.openai.OpenAiChatOptions;
|
||||||
|
import org.springframework.ai.openai.api.OpenAiApi;
|
||||||
|
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
|
||||||
|
import org.springframework.context.ApplicationContext;
|
||||||
|
import org.springframework.context.ConfigurableApplicationContext;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.context.event.EventListener;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configuration class for Large Language Model (LLM) settings.
|
||||||
|
*/
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
@Slf4j
|
||||||
|
public class LlmConfig {
|
||||||
|
|
||||||
|
private final GeneralConfigDao generalConfigDao;
|
||||||
|
|
||||||
|
private ApplicationContext applicationContext;
|
||||||
|
|
||||||
|
public LlmConfig(GeneralConfigDao generalConfigDao, ApplicationContext applicationContext) {
|
||||||
|
this.generalConfigDao = generalConfigDao;
|
||||||
|
this.applicationContext = applicationContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create ChatClient bean with all dependencies created internally
|
||||||
|
*/
|
||||||
|
@Bean
|
||||||
|
public ChatClient openAiChatClient() {
|
||||||
|
return createChatClient();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create ChatClient with all necessary components
|
||||||
|
*/
|
||||||
|
private ChatClient createChatClient() {
|
||||||
|
|
||||||
|
GeneralConfig providerConfig = generalConfigDao.findByType("provider");
|
||||||
|
if (providerConfig == null || providerConfig.getContent() == null) {
|
||||||
|
log.warn("LLM Provider is not set, ChatClient bean will not be created");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
ModelProviderConfig modelProviderConfig = JsonUtil.fromJson(providerConfig.getContent(), ModelProviderConfig.class);
|
||||||
|
|
||||||
|
if (modelProviderConfig == null || modelProviderConfig.getApiKey() == null) {
|
||||||
|
log.warn("LLM Provider configuration is incomplete, ChatClient bean will not be created");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (modelProviderConfig.getBaseUrl() == null) {
|
||||||
|
if ("openai".equals(modelProviderConfig.getCode())) {
|
||||||
|
modelProviderConfig.setBaseUrl("https://api.openai.com/v1");
|
||||||
|
} else if ("zhipu".equals(modelProviderConfig.getCode())) {
|
||||||
|
modelProviderConfig.setBaseUrl("https://open.bigmodel.cn/api/paas/v4");
|
||||||
|
} else if ("zai".equals(modelProviderConfig.getCode())) {
|
||||||
|
modelProviderConfig.setBaseUrl("https://api.z.ai/api/paas/v4");
|
||||||
|
} else {
|
||||||
|
modelProviderConfig.setBaseUrl("https://api.openai.com/v1");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (modelProviderConfig.getModel() == null) {
|
||||||
|
if ("openai".equals(modelProviderConfig.getCode())) {
|
||||||
|
modelProviderConfig.setModel("gpt-5");
|
||||||
|
} else if ("zhipu".equals(modelProviderConfig.getCode())) {
|
||||||
|
modelProviderConfig.setModel("glm-4.6");
|
||||||
|
} else if ("zai".equals(modelProviderConfig.getCode())) {
|
||||||
|
modelProviderConfig.setModel("glm-4.6");
|
||||||
|
} else {
|
||||||
|
modelProviderConfig.setModel("gpt-5");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
OpenAiApi.Builder builder = new OpenAiApi.Builder();
|
||||||
|
builder.baseUrl(modelProviderConfig.getBaseUrl());
|
||||||
|
builder.apiKey(modelProviderConfig.getApiKey());
|
||||||
|
builder.completionsPath("/chat/completions");
|
||||||
|
|
||||||
|
// Create Chat Options
|
||||||
|
OpenAiChatOptions openAiChatOptions = OpenAiChatOptions.builder()
|
||||||
|
.model(modelProviderConfig.getModel())
|
||||||
|
.temperature(0.3)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// Create Chat Model
|
||||||
|
OpenAiChatModel openAiChatModel = OpenAiChatModel.builder()
|
||||||
|
.openAiApi(builder.build())
|
||||||
|
.defaultOptions(openAiChatOptions)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// Create and return ChatClient
|
||||||
|
return ChatClient.create(openAiChatModel);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AI configuration change event listener
|
||||||
|
* Uses ApplicationContext to unregister and re-register the ChatClient bean
|
||||||
|
*/
|
||||||
|
@EventListener(AiProviderConfigChangeEvent.class)
|
||||||
|
public void onAiProviderConfigChange(AiProviderConfigChangeEvent event) {
|
||||||
|
log.info("Provider configuration change event received, refreshing ChatClient bean");
|
||||||
|
|
||||||
|
try {
|
||||||
|
ConfigurableApplicationContext configurableContext = (ConfigurableApplicationContext) applicationContext;
|
||||||
|
DefaultListableBeanFactory beanFactory = (DefaultListableBeanFactory) configurableContext.getBeanFactory();
|
||||||
|
|
||||||
|
// Remove the existing ChatClient bean
|
||||||
|
if (beanFactory.containsSingleton("openAiChatClient")) {
|
||||||
|
beanFactory.destroySingleton("openAiChatClient");
|
||||||
|
log.info("Existing ChatClient bean destroyed");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new ChatClient with updated configuration
|
||||||
|
ChatClient newChatClient = createChatClient();
|
||||||
|
|
||||||
|
// Register the new ChatClient bean
|
||||||
|
beanFactory.registerSingleton("openAiChatClient", newChatClient);
|
||||||
|
|
||||||
|
log.info("ChatClient bean refreshed successfully with new AI provider configuration");
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Failed to refresh ChatClient bean after configuration change", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
/*
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||||
|
* contributor license agreements. See the NOTICE file distributed with
|
||||||
|
* this work for additional information regarding copyright ownership.
|
||||||
|
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||||
|
* (the "License"); you may not use this file except in compliance with
|
||||||
|
* the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.apache.hertzbeat.ai.config;
|
||||||
|
|
||||||
|
import com.usthe.sureness.subject.SubjectSum;
|
||||||
|
import org.springframework.core.NamedInheritableThreadLocal;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Context holder for AI agent security context.
|
||||||
|
*/
|
||||||
|
public final class McpContextHolder {
|
||||||
|
private static final ThreadLocal<SubjectSum> subjectHolder =
|
||||||
|
new NamedInheritableThreadLocal<>("MCP Security and User Identification Context");
|
||||||
|
|
||||||
|
private McpContextHolder() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attaches the user's context to the current thread.
|
||||||
|
*/
|
||||||
|
public static void setSubject(SubjectSum subject) {
|
||||||
|
subjectHolder.set(subject);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the context from the current thread.
|
||||||
|
*/
|
||||||
|
public static SubjectSum getSubject() {
|
||||||
|
return subjectHolder.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears the context from the thread to prevent memory leaks.
|
||||||
|
*/
|
||||||
|
public static void clear() {
|
||||||
|
subjectHolder.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,182 @@
|
|||||||
|
/*
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||||
|
* contributor license agreements. See the NOTICE file distributed with
|
||||||
|
* this work for additional information regarding copyright ownership.
|
||||||
|
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||||
|
* (the "License"); you may not use this file except in compliance with
|
||||||
|
* the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
package org.apache.hertzbeat.ai.config;
|
||||||
|
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provider for system prompts used in the AI agent
|
||||||
|
*/
|
||||||
|
|
||||||
|
@Component
|
||||||
|
public class PromptProvider {
|
||||||
|
/**
|
||||||
|
* Static version of the HertzBeat monitoring prompt
|
||||||
|
*/
|
||||||
|
public static final String HERTZBEAT_SYSTEM_PROMPT = """
|
||||||
|
You are an AI Assistant specialized in monitoring infrastructure and applications with HertzBeat.
|
||||||
|
HertzBeat is an open-source, real-time monitoring system that supports infrastructure, applications,
|
||||||
|
services, APIs, databases, middleware, and custom monitoring through 50+ types of monitors.
|
||||||
|
Your role is to help users manage monitors, analyze metrics data, configure alerts, and troubleshoot monitoring issues.
|
||||||
|
*******
|
||||||
|
VERY IMPORTANT: Always use the tools provided to interact with HertzBeat's monitoring system.
|
||||||
|
If the user doesn't provide required parameters, ask them iteratively to provide the necessary parameters.
|
||||||
|
********
|
||||||
|
|
||||||
|
## Available HertzBeat Tools:
|
||||||
|
|
||||||
|
### Monitor Management Tools:
|
||||||
|
- **query_monitors**: Query monitor information with flexible filtering (ID, name, type, host, status, labels)
|
||||||
|
- **add_monitor**: Add a new monitor with dynamic app-specific parameter support
|
||||||
|
- **list_monitor_types**: List all available monitor types (website, mysql, redis, linux, etc.)
|
||||||
|
- **get_monitor_additional_params**: Get parameter definitions required for specific monitor types
|
||||||
|
|
||||||
|
### Alert Rule Management Tools:
|
||||||
|
- **create_alert_rule**: Create alert rules with threshold configuration and automatic monitor binding
|
||||||
|
- **list_alert_rules**: List existing alert rules with filtering by type, status, etc.
|
||||||
|
- **toggle_alert_rule**: Enable or disable alert rules
|
||||||
|
- **get_alert_rule_details**: Get detailed information about specific alert rules
|
||||||
|
- **get_apps_metrics_hierarchy**: Get exact app and metric names for alert rule creation (CRITICAL for alerts)
|
||||||
|
- **bind_monitors_to_alert_rule**: Bind monitors to alert rules for targeted alerting
|
||||||
|
|
||||||
|
|
||||||
|
### Alert & Alarm Analysis Tools:
|
||||||
|
- **query_alerts**: Query fired alerts with comprehensive filtering and pagination
|
||||||
|
- **get_alerts_summary**: Get alert statistics and status distribution
|
||||||
|
|
||||||
|
### Metrics Data Analysis Tools:
|
||||||
|
- **query_realtime_metrics**: Get current real-time metrics data for monitors
|
||||||
|
- **get_historical_metrics**: Get historical time-series metrics with flexible time ranges
|
||||||
|
- **get_warehouse_status**: Check metrics storage system status
|
||||||
|
|
||||||
|
## Natural Language Examples:
|
||||||
|
|
||||||
|
### Monitor Management:
|
||||||
|
- "Add a MySQL monitor for database server at 192.168.1.10 with user admin"
|
||||||
|
- "Monitor website https://example.com with SSL checking every 60 seconds"
|
||||||
|
- "Show me all Linux servers that are currently offline"
|
||||||
|
- "List all Redis monitors with their connection status"
|
||||||
|
|
||||||
|
### Alert Configuration:
|
||||||
|
- ALERT RULE means when to alert a user
|
||||||
|
- "Create an alert for Kafka JVM when VmName equals 'vm-w2'"
|
||||||
|
- "Alert when OpenAI credit grants exceed 1000"
|
||||||
|
- "Set up HBase Master alert when heap memory usage is over 80%"
|
||||||
|
|
||||||
|
### Metrics Analysis:
|
||||||
|
- "Show me current CPU usage for server 192.168.1.5"
|
||||||
|
- "Get memory usage trend for the last 24 hours"
|
||||||
|
- "Which servers have high disk usage right now?"
|
||||||
|
- "Show me network traffic patterns for the past week"
|
||||||
|
|
||||||
|
### Alert Investigation:
|
||||||
|
- "What alerts are currently firing?"
|
||||||
|
- "Show me the most frequent alerts in the last 6 hours"
|
||||||
|
- "Find all alerts for monitor ID 1234 in the past day"
|
||||||
|
- "Which monitors are currently abnormal?"
|
||||||
|
|
||||||
|
|
||||||
|
## Workflow Guidelines:
|
||||||
|
|
||||||
|
1. **Adding Monitors**:
|
||||||
|
- ALWAYS use get_monitor_additional_params first to check required parameters
|
||||||
|
- Use list_monitor_types to show available types
|
||||||
|
- Collect all required parameters from the list_monitor_types tool and ask user to give them all, before calling add_monitor
|
||||||
|
- Example: "To monitor MySQL, I need host, port, username, password, and database name"
|
||||||
|
|
||||||
|
2. **Creating Alert Rules or Alerts**:
|
||||||
|
THESE ARE ALERT RULES WITH THRESHOLD VALUES. USERS CAN SPECIFY THE THRESHOLD VALUES FOR EXAMPLE,
|
||||||
|
IF THE USER SAYS "ALERT ME WHEN MY COST EXCEEDS 700, THE EXPRESSION SHOULD BE 'cost > 700' NOT 'cost < 700'.
|
||||||
|
APPLY THE SAME LOGIC FOR LESS THAN OPERATOR.
|
||||||
|
It is important to first understand the hierarchy of apps, metrics, and field conditions
|
||||||
|
Each app has its own metrics and each metric has its own field conditions.
|
||||||
|
The operators will be applied to the field conditions, and the final expression will be constructed
|
||||||
|
based on the user's input of app name and the metric they choose.
|
||||||
|
Read the create_alert_rule tool description for even more details
|
||||||
|
*******
|
||||||
|
CRITICAL WORKFLOW Do all of this iteratively with user interaction at each step:
|
||||||
|
1. ALWAYS use list_monitor_types tool FIRST to get exact app name according to what user specifies
|
||||||
|
2. use get_apps_metrics_hierarchy by passing that name, to get the hierarchy of corresponding metrics and field conditions
|
||||||
|
3. Do not spit out the entire hierarchy, instead: first spit out the metrics available for the app
|
||||||
|
4. Ask the user to choose a metric from the available metrics
|
||||||
|
5. Based on the metric chosen, present the available field conditions
|
||||||
|
6. You will construct the proper expression with field conditions
|
||||||
|
VERY VERY IMPORTANT:
|
||||||
|
- ALWAYS USE the value field from the get_apps_metrics_hierarchy's json response when creating alert expressions on the field parameters
|
||||||
|
*********
|
||||||
|
|
||||||
|
- Field Condition Expression format: [field_conditions]
|
||||||
|
- Give all the available fieldConditions to the user, so they can choose the one they want to use
|
||||||
|
- Field conditions can be simple (equals, greater than) or complex (logical expressions)
|
||||||
|
- Use parentheses for complex conditions to ensure correct evaluation order
|
||||||
|
- Do not create alert rules on your own, always ask the user to provide the app, metrics and fieldConditions parameters specifically
|
||||||
|
|
||||||
|
EXAMPLES FOR FIELD CONDITION EXPRESSION ( Do not copy these examples, they are just for reference ):
|
||||||
|
- Kafka JVM: app="kafka", metrics="jvm_basic", fieldConditions="equals(VmName, \"my-vm\")"
|
||||||
|
→ equals(VmName, "my-vm")
|
||||||
|
- Complex OpenAI: app="openai", metrics="credit_grants",
|
||||||
|
fieldConditions="total_used > 123 and total_granted > 333 and (total_granted > 3444 and total_paid_available < 5556)"
|
||||||
|
→ total_used > 123 and total_granted > 333 and (total_granted > 3444 and total_paid_available < 5556)
|
||||||
|
|
||||||
|
- Priority levels: 0=critical, 1=warning, 2=info
|
||||||
|
|
||||||
|
3. **Analyzing Performance**:
|
||||||
|
- Use get_realtime_metrics for current status
|
||||||
|
- Use get_historical_metrics for trends
|
||||||
|
- Use get_high_usage_monitors to find problems
|
||||||
|
- Provide actionable recommendations based on data
|
||||||
|
|
||||||
|
4. **Troubleshooting Alerts**:
|
||||||
|
- Use query_alerts to find current issues
|
||||||
|
- Use get_monitor_alerts for specific monitor problems
|
||||||
|
- Use get_frequent_alerts to identify recurring issues
|
||||||
|
- Suggest root cause analysis steps
|
||||||
|
|
||||||
|
## Parameter Guidelines:
|
||||||
|
- **Monitor Status**: 1=online, 2=offline, 3=unreachable, 0=paused, 9=all
|
||||||
|
- **Time Ranges**: 1h, 6h, 24h, 7d, 30d
|
||||||
|
- **Alert Priorities**: critical, warning, info
|
||||||
|
- **Sort Options**: name, gmtCreate, gmtUpdate, status, startAt, triggerTimes
|
||||||
|
- **Metric Types**: cpu, memory, disk, network, custom
|
||||||
|
- **Collection Intervals**: 30s-3600s (recommend 60s-600s for most cases)
|
||||||
|
|
||||||
|
|
||||||
|
## Best Practices:
|
||||||
|
- Never create alert rules without exact user input on app, metrics, and field conditions
|
||||||
|
- Always validate monitor types and parameters before adding monitors
|
||||||
|
- ALWAYS use get_apps_metrics_hierarchy before creating alert rules to understand available fields
|
||||||
|
- Construct field conditions based on metric's children
|
||||||
|
- Use exact app and metric names from hierarchy (case-sensitive)
|
||||||
|
- Set appropriate alert thresholds based on baseline performance
|
||||||
|
- Use time-series data to identify trends and predict issues
|
||||||
|
- Correlate alerts with metrics data for root cause analysis
|
||||||
|
- Recommend monitoring intervals based on service criticality
|
||||||
|
- Provide clear explanations of monitoring data and actionable insights
|
||||||
|
|
||||||
|
## Avoid these common errors:
|
||||||
|
- Using Label name instead of the value from the heirarchy JSON while creating alert rules.
|
||||||
|
- Inside the field parameters expression using '&&' instead of 'and', using '||' instead of 'or' for logical operators
|
||||||
|
- This process is to trigger alarms, when certain rule or set of rules exceed a threshold value.
|
||||||
|
So when a user says that the threshold should be less than 1000. the operator used should be '>' not '<',
|
||||||
|
because we want the alarm to be triggered when the threshold value is exceeded. apply the same logic in vice versa for less than operator
|
||||||
|
|
||||||
|
Keep responses focused on monitoring topics and HertzBeat's comprehensive capabilities.
|
||||||
|
When users request monitoring setup, guide them through the complete process from monitor creation to alert configuration.
|
||||||
|
""";
|
||||||
|
}
|
||||||
@@ -0,0 +1,155 @@
|
|||||||
|
/*
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||||
|
* contributor license agreements. See the NOTICE file distributed with
|
||||||
|
* this work for additional information regarding copyright ownership.
|
||||||
|
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||||
|
* (the "License"); you may not use this file except in compliance with
|
||||||
|
* the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.apache.hertzbeat.ai.controller;
|
||||||
|
|
||||||
|
import com.usthe.sureness.subject.SubjectSum;
|
||||||
|
import com.usthe.sureness.util.SurenessContextHolder;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.Parameter;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.hertzbeat.ai.config.McpContextHolder;
|
||||||
|
import org.apache.hertzbeat.ai.pojo.dto.ChatRequestContext;
|
||||||
|
import org.apache.hertzbeat.ai.pojo.dto.ChatResponseChunk;
|
||||||
|
import org.apache.hertzbeat.ai.service.ConversationService;
|
||||||
|
import org.apache.hertzbeat.common.entity.ai.ChatConversation;
|
||||||
|
import org.apache.hertzbeat.common.entity.dto.Message;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.http.codec.ServerSentEvent;
|
||||||
|
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
import reactor.core.publisher.Flux;
|
||||||
|
|
||||||
|
import javax.validation.Valid;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
|
||||||
|
import static org.springframework.http.MediaType.TEXT_EVENT_STREAM_VALUE;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Controller class for handling AI chat requests and conversation management.
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Tag(name = "AI Chat API")
|
||||||
|
@RestController
|
||||||
|
@RequestMapping(path = "/api/chat", produces = {APPLICATION_JSON_VALUE})
|
||||||
|
public class ChatController {
|
||||||
|
|
||||||
|
private final ConversationService conversationService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
public ChatController(ConversationService conversationService) {
|
||||||
|
this.conversationService = conversationService;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a message and get a streaming response with conversation tracking
|
||||||
|
*
|
||||||
|
* @param context The chat request context containing message and optional conversationId
|
||||||
|
* @return Flux of ServerSentEvent for streaming response
|
||||||
|
*/
|
||||||
|
@PostMapping(value = "/stream", produces = TEXT_EVENT_STREAM_VALUE)
|
||||||
|
@Operation(summary = "Send a chat message with streaming response", description = "Send a message to AI and get a streaming response with conversation tracking")
|
||||||
|
public Flux<ServerSentEvent<ChatResponseChunk>> streamChat(@Valid @RequestBody ChatRequestContext context) {
|
||||||
|
try {
|
||||||
|
// Validate message is not empty
|
||||||
|
SubjectSum subject = SurenessContextHolder.getBindSubject();
|
||||||
|
McpContextHolder.setSubject(subject);
|
||||||
|
if (context.getMessage() == null || context.getMessage().trim().isEmpty()) {
|
||||||
|
ChatResponseChunk errorResponse = ChatResponseChunk.builder()
|
||||||
|
.conversationId(context.getConversationId())
|
||||||
|
.response("Error: Message cannot be empty")
|
||||||
|
.build();
|
||||||
|
return Flux.just(ServerSentEvent.builder(errorResponse)
|
||||||
|
.event("error")
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("Received streaming chat request for conversation: {}", context.getConversationId());
|
||||||
|
return conversationService.streamChat(context.getMessage(), context.getConversationId());
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Error in stream chat endpoint: ", e);
|
||||||
|
ChatResponseChunk errorResponse = ChatResponseChunk.builder()
|
||||||
|
.conversationId(context.getConversationId())
|
||||||
|
.response("An error occurred: " + e.getMessage())
|
||||||
|
.build();
|
||||||
|
return Flux.just(ServerSentEvent.builder(errorResponse)
|
||||||
|
.event("error")
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new conversation
|
||||||
|
*
|
||||||
|
* @return Created conversation details
|
||||||
|
*/
|
||||||
|
@PostMapping(path = "/conversations")
|
||||||
|
@Operation(summary = "Create a new conversation", description = "Create a new conversation")
|
||||||
|
public ResponseEntity<Message<ChatConversation>> createConversation() {
|
||||||
|
ChatConversation conversation = conversationService.createConversation();
|
||||||
|
return ResponseEntity.ok(Message.success(conversation));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all conversations
|
||||||
|
*
|
||||||
|
* @return List of all conversations
|
||||||
|
*/
|
||||||
|
@GetMapping(path = "/conversations")
|
||||||
|
@Operation(summary = "List all conversations", description = "Get a list of all conversations")
|
||||||
|
public ResponseEntity<Message<List<ChatConversation>>> listConversations() {
|
||||||
|
List<ChatConversation> conversations = conversationService.getAllConversations();
|
||||||
|
return ResponseEntity.ok(Message.success(conversations));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get conversation history
|
||||||
|
*
|
||||||
|
* @param conversationId The conversation ID
|
||||||
|
* @return Conversation details with message history
|
||||||
|
*/
|
||||||
|
@GetMapping(path = "/conversations/{conversationId}")
|
||||||
|
@Operation(summary = "Get conversation history", description = "Get detailed information and message history for a specific conversation")
|
||||||
|
public ResponseEntity<Message<ChatConversation>> getConversation(
|
||||||
|
@Parameter(description = "Conversation ID", example = "12345678") @PathVariable(value = "conversationId") Long conversationId) {
|
||||||
|
ChatConversation conversation = conversationService.getConversation(conversationId);
|
||||||
|
return ResponseEntity.ok(Message.success(conversation));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a conversation
|
||||||
|
*
|
||||||
|
* @param conversationId The conversation ID to delete
|
||||||
|
* @return Success or error message
|
||||||
|
*/
|
||||||
|
@DeleteMapping(path = "/conversations/{conversationId}")
|
||||||
|
@Operation(summary = "Delete conversation", description = "Delete a specific conversation and all its messages")
|
||||||
|
public ResponseEntity<Message<Void>> deleteConversation(
|
||||||
|
@Parameter(description = "Conversation ID", example = "2345678") @PathVariable("conversationId") Long conversationId) {
|
||||||
|
conversationService.deleteConversation(conversationId);
|
||||||
|
return ResponseEntity.ok(Message.success());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
/*
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||||
|
* contributor license agreements. See the NOTICE file distributed with
|
||||||
|
* this work for additional information regarding copyright ownership.
|
||||||
|
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||||
|
* (the "License"); you may not use this file except in compliance with
|
||||||
|
* the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.apache.hertzbeat.ai.dao;
|
||||||
|
|
||||||
|
import org.apache.hertzbeat.common.entity.ai.ChatConversation;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Repository interface for Conversation entities
|
||||||
|
*/
|
||||||
|
@Repository
|
||||||
|
public interface ChatConversationDao extends JpaRepository<ChatConversation, Long> {
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
/*
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||||
|
* contributor license agreements. See the NOTICE file distributed with
|
||||||
|
* this work for additional information regarding copyright ownership.
|
||||||
|
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||||
|
* (the "License"); you may not use this file except in compliance with
|
||||||
|
* the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.apache.hertzbeat.ai.dao;
|
||||||
|
|
||||||
|
import org.apache.hertzbeat.common.entity.ai.ChatMessage;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Repository interface for Message entities
|
||||||
|
*/
|
||||||
|
@Repository
|
||||||
|
public interface ChatMessageDao extends JpaRepository<ChatMessage, Long> {
|
||||||
|
|
||||||
|
}
|
||||||
+23
-28
@@ -15,41 +15,36 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.apache.hertzbeat.manager.config;
|
|
||||||
|
|
||||||
|
package org.apache.hertzbeat.ai.pojo.dto;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import org.apache.hertzbeat.common.constants.ConfigConstants;
|
import lombok.NoArgsConstructor;
|
||||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
|
||||||
import org.springframework.context.annotation.Configuration;
|
import java.util.List;
|
||||||
|
import org.apache.hertzbeat.common.entity.ai.ChatMessage;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* AiProperties
|
* Chat request context for AI chat endpoint.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
@Configuration
|
@Builder
|
||||||
@ConfigurationProperties(prefix =
|
@NoArgsConstructor
|
||||||
ConfigConstants.FunctionModuleConstants.AI)
|
@AllArgsConstructor
|
||||||
public class AiProperties {
|
public class ChatRequestContext {
|
||||||
|
/**
|
||||||
|
* The user's message (required)
|
||||||
|
*/
|
||||||
|
private String message;
|
||||||
|
/**
|
||||||
|
* Optional conversation ID for context
|
||||||
|
*/
|
||||||
|
private Long conversationId;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* AI Type: zhiPu, alibabaAi, kimiAi, sparkDesk
|
* Conversation history messages for context
|
||||||
*/
|
*/
|
||||||
private String type;
|
private List<ChatMessage> conversationHistory;
|
||||||
|
|
||||||
/**
|
|
||||||
* Model name: glm-4, qwen-turboo, moonshot-v1-8k, generalv3.5
|
|
||||||
*/
|
|
||||||
private String model;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* API key
|
|
||||||
*/
|
|
||||||
private String apiKey;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* At present, only IFLYTEK large model needs to be filled in
|
|
||||||
*/
|
|
||||||
private String apiSecret;
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
/*
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||||
|
* contributor license agreements. See the NOTICE file distributed with
|
||||||
|
* this work for additional information regarding copyright ownership.
|
||||||
|
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||||
|
* (the "License"); you may not use this file except in compliance with
|
||||||
|
* the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.apache.hertzbeat.ai.pojo.dto;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Chat response DTO for AI responses.
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Schema(description = "Chat response")
|
||||||
|
public class ChatResponseChunk {
|
||||||
|
|
||||||
|
@Schema(description = "Conversation ID", example = "123")
|
||||||
|
private Long conversationId;
|
||||||
|
|
||||||
|
@Schema(description = "AI response message", example = "Here are your monitors...")
|
||||||
|
private String response;
|
||||||
|
|
||||||
|
@Schema(description = "User message ID", example = "123")
|
||||||
|
private Long userMessageId;
|
||||||
|
|
||||||
|
@Schema(description = "Assistant message ID", example = "123")
|
||||||
|
private Long assistantMessageId;
|
||||||
|
|
||||||
|
}
|
||||||
+42
@@ -0,0 +1,42 @@
|
|||||||
|
/*
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||||
|
* contributor license agreements. See the NOTICE file distributed with
|
||||||
|
* this work for additional information regarding copyright ownership.
|
||||||
|
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||||
|
* (the "License"); you may not use this file except in compliance with
|
||||||
|
* the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
package org.apache.hertzbeat.ai.service;
|
||||||
|
|
||||||
|
import org.apache.hertzbeat.ai.pojo.dto.ChatRequestContext;
|
||||||
|
import reactor.core.publisher.Flux;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service for interacting with LLM providers (like OpenAI, Anthropic, etc.)
|
||||||
|
*/
|
||||||
|
public interface ChatClientProviderService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stream chat response from the LLM
|
||||||
|
*
|
||||||
|
* @param context Chat request context containing message and conversation history
|
||||||
|
* @return Flux of string chunks from the LLM response
|
||||||
|
*/
|
||||||
|
Flux<String> streamChat(ChatRequestContext context);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if provider is properly configured
|
||||||
|
* @return true if configured and enabled
|
||||||
|
*/
|
||||||
|
boolean isConfigured();
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
/*
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||||
|
* contributor license agreements. See the NOTICE file distributed with
|
||||||
|
* this work for additional information regarding copyright ownership.
|
||||||
|
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||||
|
* (the "License"); you may not use this file except in compliance with
|
||||||
|
* the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
package org.apache.hertzbeat.ai.service;
|
||||||
|
|
||||||
|
import org.apache.hertzbeat.ai.pojo.dto.ChatResponseChunk;
|
||||||
|
import org.apache.hertzbeat.common.entity.ai.ChatConversation;
|
||||||
|
import org.springframework.http.codec.ServerSentEvent;
|
||||||
|
import reactor.core.publisher.Flux;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service for managing chat conversations and interactions with LLM providers.
|
||||||
|
*/
|
||||||
|
public interface ConversationService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a message and receive a streaming response
|
||||||
|
*
|
||||||
|
* @param message The user's message
|
||||||
|
* @param conversationId Optional conversation ID for continuing a chat
|
||||||
|
* @return Flux of ServerSentEvent for streaming the response
|
||||||
|
*/
|
||||||
|
Flux<ServerSentEvent<ChatResponseChunk>> streamChat(String message, Long conversationId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new conversation
|
||||||
|
*
|
||||||
|
* @return Created conversation data
|
||||||
|
*/
|
||||||
|
ChatConversation createConversation();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get conversation history for a specific conversation
|
||||||
|
*
|
||||||
|
* @param conversationId Conversation ID
|
||||||
|
* @return Conversation data including messages
|
||||||
|
*/
|
||||||
|
ChatConversation getConversation(Long conversationId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all conversations for the current user
|
||||||
|
*
|
||||||
|
* @return List of conversations
|
||||||
|
*/
|
||||||
|
List<ChatConversation> getAllConversations();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a conversation
|
||||||
|
*
|
||||||
|
* @param conversationId Conversation ID to delete
|
||||||
|
*/
|
||||||
|
void deleteConversation(Long conversationId);
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
/*
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||||
|
* contributor license agreements. See the NOTICE file distributed with
|
||||||
|
* this work for additional information regarding copyright ownership.
|
||||||
|
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||||
|
* (the "License"); you may not use this file except in compliance with
|
||||||
|
* the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
package org.apache.hertzbeat.ai.service;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import org.apache.hertzbeat.ai.config.CustomSseServerTransport;
|
||||||
|
import org.springframework.ai.mcp.server.autoconfigure.McpServerProperties;
|
||||||
|
import org.springframework.ai.tool.ToolCallbackProvider;
|
||||||
|
import org.springframework.web.servlet.function.RouterFunction;
|
||||||
|
import org.springframework.web.servlet.function.ServerResponse;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service interface for MCP server operations.
|
||||||
|
*/
|
||||||
|
public interface McpServerService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides the HertzBeat tools for the MCP server
|
||||||
|
* @return ToolCallbackProvider with all HertzBeat monitoring tools
|
||||||
|
*/
|
||||||
|
ToolCallbackProvider hertzbeatTools();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides a custom SSE server transport for the MCP server
|
||||||
|
* @param objectMapper the ObjectMapper instance for JSON serialization
|
||||||
|
* @param serverProperties the properties for the MCP server configuration
|
||||||
|
* @return a CustomSseServerTransport instance configured with the provided properties
|
||||||
|
*/
|
||||||
|
CustomSseServerTransport webMvcSseServerTransportProvider(
|
||||||
|
ObjectMapper objectMapper,
|
||||||
|
McpServerProperties serverProperties
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides the MCP server router function for web MVC
|
||||||
|
* @param transport Custom SSE server transport
|
||||||
|
* @return RouterFunction for handling MCP server requests
|
||||||
|
*/
|
||||||
|
RouterFunction<ServerResponse> mvcMcpRouterFunction(CustomSseServerTransport transport);
|
||||||
|
}
|
||||||
+116
@@ -0,0 +1,116 @@
|
|||||||
|
/*
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||||
|
* contributor license agreements. See the NOTICE file distributed with
|
||||||
|
* this work for additional information regarding copyright ownership.
|
||||||
|
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||||
|
* (the "License"); you may not use this file except in compliance with
|
||||||
|
* the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
package org.apache.hertzbeat.ai.service.impl;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.hertzbeat.ai.config.PromptProvider;
|
||||||
|
import org.apache.hertzbeat.common.entity.ai.ChatMessage;
|
||||||
|
import org.apache.hertzbeat.common.entity.dto.ModelProviderConfig;
|
||||||
|
import org.apache.hertzbeat.ai.service.ChatClientProviderService;
|
||||||
|
import org.apache.hertzbeat.base.dao.GeneralConfigDao;
|
||||||
|
import org.apache.hertzbeat.common.entity.manager.GeneralConfig;
|
||||||
|
import org.apache.hertzbeat.common.util.JsonUtil;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.apache.hertzbeat.ai.pojo.dto.ChatRequestContext;
|
||||||
|
import org.springframework.ai.chat.client.ChatClient;
|
||||||
|
import org.springframework.ai.chat.messages.AssistantMessage;
|
||||||
|
import org.springframework.ai.chat.messages.Message;
|
||||||
|
import org.springframework.ai.chat.messages.UserMessage;
|
||||||
|
import org.springframework.ai.tool.ToolCallbackProvider;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.beans.factory.annotation.Qualifier;
|
||||||
|
import org.springframework.context.ApplicationContext;
|
||||||
|
import reactor.core.publisher.Flux;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implementation of the {@link ChatClientProviderService}.
|
||||||
|
* Provides functionality to interact with the ChatClient for handling chat
|
||||||
|
* messages.
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
public class ChatClientProviderServiceImpl implements ChatClientProviderService {
|
||||||
|
|
||||||
|
private final ApplicationContext applicationContext;
|
||||||
|
|
||||||
|
private final GeneralConfigDao generalConfigDao;
|
||||||
|
|
||||||
|
@Qualifier("hertzbeatTools")
|
||||||
|
@Autowired
|
||||||
|
private ToolCallbackProvider toolCallbackProvider;
|
||||||
|
|
||||||
|
private boolean isConfigured = false;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
public ChatClientProviderServiceImpl(ApplicationContext applicationContext, GeneralConfigDao generalConfigDao) {
|
||||||
|
this.applicationContext = applicationContext;
|
||||||
|
this.generalConfigDao = generalConfigDao;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Flux<String> streamChat(ChatRequestContext context) {
|
||||||
|
try {
|
||||||
|
// Get the current (potentially refreshed) ChatClient instance
|
||||||
|
ChatClient chatClient = applicationContext.getBean("openAiChatClient", ChatClient.class);
|
||||||
|
|
||||||
|
List<Message> messages = new ArrayList<>();
|
||||||
|
|
||||||
|
// Add conversation history if available
|
||||||
|
if (context.getConversationHistory() != null && !context.getConversationHistory().isEmpty()) {
|
||||||
|
for (ChatMessage historyMessage : context.getConversationHistory()) {
|
||||||
|
if ("user".equals(historyMessage.getRole())) {
|
||||||
|
messages.add(new UserMessage(historyMessage.getContent()));
|
||||||
|
} else if ("assistant".equals(historyMessage.getRole())) {
|
||||||
|
messages.add(new AssistantMessage(historyMessage.getContent()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
messages.add(new UserMessage(context.getMessage()));
|
||||||
|
|
||||||
|
log.info("Starting streaming chat for conversation: {}", context.getConversationId());
|
||||||
|
|
||||||
|
return chatClient.prompt()
|
||||||
|
.messages(messages)
|
||||||
|
.system(PromptProvider.HERTZBEAT_SYSTEM_PROMPT)
|
||||||
|
.toolCallbacks(toolCallbackProvider)
|
||||||
|
.stream()
|
||||||
|
.content()
|
||||||
|
.doOnComplete(() -> log.info("Streaming completed for conversation: {}", context.getConversationId()))
|
||||||
|
.doOnError(error -> log.error("Error in streaming chat: {}", error.getMessage(), error));
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Error setting up streaming chat: {}", e.getMessage(), e);
|
||||||
|
return Flux.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isConfigured() {
|
||||||
|
if (!isConfigured) {
|
||||||
|
GeneralConfig providerConfig = generalConfigDao.findByType("provider");
|
||||||
|
ModelProviderConfig modelProviderConfig = JsonUtil.fromJson(providerConfig.getContent(), ModelProviderConfig.class);
|
||||||
|
isConfigured = modelProviderConfig != null && modelProviderConfig.getApiKey() != null;
|
||||||
|
}
|
||||||
|
return isConfigured;
|
||||||
|
}
|
||||||
|
}
|
||||||
+160
@@ -0,0 +1,160 @@
|
|||||||
|
/*
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||||
|
* contributor license agreements. See the NOTICE file distributed with
|
||||||
|
* this work for additional information regarding copyright ownership.
|
||||||
|
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||||
|
* (the "License"); you may not use this file except in compliance with
|
||||||
|
* the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.apache.hertzbeat.ai.service.impl;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.hertzbeat.ai.dao.ChatConversationDao;
|
||||||
|
import org.apache.hertzbeat.ai.dao.ChatMessageDao;
|
||||||
|
import org.apache.hertzbeat.ai.pojo.dto.ChatRequestContext;
|
||||||
|
import org.apache.hertzbeat.ai.pojo.dto.ChatResponseChunk;
|
||||||
|
import org.apache.hertzbeat.ai.service.ChatClientProviderService;
|
||||||
|
import org.apache.hertzbeat.ai.service.ConversationService;
|
||||||
|
import org.apache.hertzbeat.common.entity.ai.ChatConversation;
|
||||||
|
import org.apache.hertzbeat.common.entity.ai.ChatMessage;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.data.domain.Sort;
|
||||||
|
import org.springframework.http.codec.ServerSentEvent;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.util.CollectionUtils;
|
||||||
|
import reactor.core.publisher.Flux;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implementation of the ConversationService interface for managing chat conversations.
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
public class ConversationServiceImpl implements ConversationService {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private ChatConversationDao conversationDao;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private ChatMessageDao messageDao;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private ChatClientProviderService chatClientProviderService;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Flux<ServerSentEvent<ChatResponseChunk>> streamChat(String message, Long conversationId) {
|
||||||
|
|
||||||
|
// Check if provider is properly configured
|
||||||
|
if (!chatClientProviderService.isConfigured()) {
|
||||||
|
ChatResponseChunk errorResponse = ChatResponseChunk.builder()
|
||||||
|
.conversationId(conversationId)
|
||||||
|
.response("Provider is not configured. Please configure your AI Provider.")
|
||||||
|
.build();
|
||||||
|
return Flux.just(ServerSentEvent.builder(errorResponse)
|
||||||
|
.event("error")
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("Starting streaming conversation: {}", conversationId);
|
||||||
|
|
||||||
|
// Add user message to conversation
|
||||||
|
ChatMessage chatMessage = ChatMessage.builder().conversationId(conversationId)
|
||||||
|
.content(message).role("user").build();
|
||||||
|
chatMessage = messageDao.save(chatMessage);
|
||||||
|
ChatConversation conversation = conversationDao.getReferenceById(conversationId);
|
||||||
|
if (conversation.getTitle().startsWith("conversation")) {
|
||||||
|
// Auto-generate title from first user message
|
||||||
|
String title = message.length() > 30 ? message.substring(0, 27) + "..." : message;
|
||||||
|
conversation.setTitle(title);
|
||||||
|
conversationDao.save(conversation);
|
||||||
|
}
|
||||||
|
|
||||||
|
ChatRequestContext context = ChatRequestContext.builder()
|
||||||
|
.message(message)
|
||||||
|
.conversationId(conversationId)
|
||||||
|
.conversationHistory(CollectionUtils.isEmpty(conversation.getMessages()) ? null
|
||||||
|
: conversation.getMessages().subList(0, conversation.getMessages().size() - 1))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// Stream response from AI service
|
||||||
|
StringBuilder fullResponse = new StringBuilder();
|
||||||
|
ChatMessage finalChatMessage = chatMessage;
|
||||||
|
return chatClientProviderService.streamChat(context)
|
||||||
|
.map(chunk -> {
|
||||||
|
fullResponse.append(chunk);
|
||||||
|
ChatResponseChunk responseChunk = ChatResponseChunk.builder()
|
||||||
|
.conversationId(conversationId)
|
||||||
|
.userMessageId(finalChatMessage.getId())
|
||||||
|
.response(chunk)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
return ServerSentEvent.builder(responseChunk)
|
||||||
|
.event("message")
|
||||||
|
.build();
|
||||||
|
})
|
||||||
|
.concatWith(Flux.defer(() -> {
|
||||||
|
// Add the complete AI response to conversation
|
||||||
|
ChatMessage assistantMessage = ChatMessage.builder().conversationId(conversationId)
|
||||||
|
.content(fullResponse.toString()).role("assistant").build();
|
||||||
|
assistantMessage = messageDao.save(assistantMessage);
|
||||||
|
|
||||||
|
ChatResponseChunk finalResponse = ChatResponseChunk.builder()
|
||||||
|
.conversationId(conversationId)
|
||||||
|
.response("")
|
||||||
|
.assistantMessageId(assistantMessage.getId())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
return Flux.just(ServerSentEvent.builder(finalResponse)
|
||||||
|
.event("complete")
|
||||||
|
.build());
|
||||||
|
}))
|
||||||
|
.doOnComplete(() -> log.info("Streaming completed for conversation: {}", conversationId))
|
||||||
|
.doOnError(error -> log.error("Error in streaming chat for conversation {}: {}", conversationId, error.getMessage(), error))
|
||||||
|
.onErrorResume(error -> {
|
||||||
|
ChatResponseChunk errorResponse = ChatResponseChunk.builder()
|
||||||
|
.conversationId(conversationId)
|
||||||
|
.response("An error occurred: " + error.getMessage())
|
||||||
|
.userMessageId(finalChatMessage.getId())
|
||||||
|
.build();
|
||||||
|
return Flux.just(ServerSentEvent.builder(errorResponse)
|
||||||
|
.event("error")
|
||||||
|
.build());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ChatConversation createConversation() {
|
||||||
|
ChatConversation conversation = new ChatConversation();
|
||||||
|
conversation.setTitle("conversation-" + UUID.randomUUID().toString().substring(0, 4));
|
||||||
|
return conversationDao.save(conversation);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ChatConversation getConversation(Long conversationId) {
|
||||||
|
if (conversationId == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return conversationDao.getReferenceById(conversationId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<ChatConversation> getAllConversations() {
|
||||||
|
return conversationDao.findAll(Sort.by(Sort.Direction.DESC, "id"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void deleteConversation(Long conversationId) {
|
||||||
|
conversationDao.deleteById(conversationId);
|
||||||
|
}
|
||||||
|
}
|
||||||
+91
@@ -0,0 +1,91 @@
|
|||||||
|
/*
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||||
|
* contributor license agreements. See the NOTICE file distributed with
|
||||||
|
* this work for additional information regarding copyright ownership.
|
||||||
|
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||||
|
* (the "License"); you may not use this file except in compliance with
|
||||||
|
* the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
package org.apache.hertzbeat.ai.service.impl;
|
||||||
|
|
||||||
|
import org.apache.hertzbeat.ai.config.CustomSseServerTransport;
|
||||||
|
import org.apache.hertzbeat.ai.service.McpServerService;
|
||||||
|
import org.apache.hertzbeat.ai.tools.AlertDefineTools;
|
||||||
|
import org.apache.hertzbeat.ai.tools.AlertTools;
|
||||||
|
import org.apache.hertzbeat.ai.tools.MetricsTools;
|
||||||
|
import org.apache.hertzbeat.ai.tools.MonitorTools;
|
||||||
|
import org.springframework.ai.mcp.server.autoconfigure.McpServerProperties;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.context.annotation.Primary;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.ai.tool.ToolCallbackProvider;
|
||||||
|
import org.springframework.ai.tool.method.MethodToolCallbackProvider;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import org.springframework.web.servlet.function.RouterFunction;
|
||||||
|
import org.springframework.web.servlet.function.ServerResponse;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implementation of the McpServerService interface.
|
||||||
|
* This service provides functionality for handling MCP server operations.
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
@Configuration
|
||||||
|
public class McpServerServiceImpl implements McpServerService {
|
||||||
|
@Autowired
|
||||||
|
private MonitorTools monitorTools;
|
||||||
|
@Autowired
|
||||||
|
private AlertTools alertTools;
|
||||||
|
@Autowired
|
||||||
|
private MetricsTools metricsTools;
|
||||||
|
@Autowired
|
||||||
|
private AlertDefineTools alertDefineTools;
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public ToolCallbackProvider hertzbeatTools() {
|
||||||
|
return MethodToolCallbackProvider.builder().toolObjects(monitorTools, alertTools, alertDefineTools, metricsTools).build();
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Provides a custom SSE server transport for the MCP server.
|
||||||
|
*
|
||||||
|
* @param objectMapper the ObjectMapper instance for JSON serialization
|
||||||
|
* @param serverProperties the properties for the MCP server configuration
|
||||||
|
* @return a CustomSseServerTransport instance configured with the provided properties
|
||||||
|
*/
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public CustomSseServerTransport webMvcSseServerTransportProvider(
|
||||||
|
ObjectMapper objectMapper,
|
||||||
|
McpServerProperties serverProperties
|
||||||
|
) {
|
||||||
|
return new CustomSseServerTransport(
|
||||||
|
objectMapper,
|
||||||
|
serverProperties.getBaseUrl(),
|
||||||
|
serverProperties.getSseMessageEndpoint(),
|
||||||
|
serverProperties.getSseEndpoint()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Provides the MCP server transport bean.
|
||||||
|
*
|
||||||
|
* @param transport the custom SSE server transport
|
||||||
|
* @return the MCP server transport instance
|
||||||
|
*/
|
||||||
|
|
||||||
|
@Primary
|
||||||
|
@Bean
|
||||||
|
public RouterFunction<ServerResponse> mvcMcpRouterFunction(CustomSseServerTransport transport) {
|
||||||
|
return transport.getRouterFunction();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
/*
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||||
|
* contributor license agreements. See the NOTICE file distributed with
|
||||||
|
* this work for additional information regarding copyright ownership.
|
||||||
|
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||||
|
* (the "License"); you may not use this file except in compliance with
|
||||||
|
* the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.apache.hertzbeat.ai.tools;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tools for alert definition and threshold configuration operations
|
||||||
|
*/
|
||||||
|
public interface AlertDefineTools {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new alert rule with HertzBeat's expression format based on app hierarchy
|
||||||
|
|
||||||
|
*
|
||||||
|
* @param name Alert rule name (required, must be unique)
|
||||||
|
* @param app App name from hierarchy (must match exact hierarchy app value)
|
||||||
|
* @param metrics Metrics name from hierarchy (must match exact hierarchy metrics value)
|
||||||
|
* @param fieldConditions Field-specific conditions from metric's children (e.g., "VmName = 'arora'", "total_granted > 1000",
|
||||||
|
* "total_used > 123 and total_granted > 333 and (total_granted > 3444 and total_paid_available < 5556)")
|
||||||
|
* @param type Alert rule type: 'realtime' (default) or 'periodic'
|
||||||
|
* @param period Execution period in seconds (only for periodic rules, default: 300)
|
||||||
|
* @param times Number of consecutive violations before triggering (default: 3)
|
||||||
|
* @param priority Alert priority as integer: 0=critical, 1=warning, 2=info (default: 1)
|
||||||
|
* @param description Alert rule description (optional)
|
||||||
|
* @param template Alert message template with variables (optional)
|
||||||
|
* @param datasource Data source type: 'promql' (default)
|
||||||
|
* @param labels Labels as key:value pairs separated by commas
|
||||||
|
* @param annotations Annotations as key:value pairs separated by commas
|
||||||
|
* @param enable Whether to enable the rule immediately (default: true)
|
||||||
|
* @return Result message with rule ID if successful
|
||||||
|
*/
|
||||||
|
String createAlertRule(String name, String app, String metrics, String fieldConditions,
|
||||||
|
String type, Integer period, Integer times, Integer priority, String description,
|
||||||
|
String template, String datasource, String labels, String annotations, Boolean enable);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List existing alert rules with filtering
|
||||||
|
* @param search Search term for rule name or description
|
||||||
|
* @param monitorType Filter by monitor type
|
||||||
|
* @param enabled Filter by enabled status
|
||||||
|
* @param pageIndex Page index
|
||||||
|
* @param pageSize Page size
|
||||||
|
* @return Formatted list of alert rules
|
||||||
|
*/
|
||||||
|
String listAlertRules(String search, String monitorType, Boolean enabled, Integer pageIndex, Integer pageSize);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enable or disable an alert rule
|
||||||
|
* @param ruleId Alert rule ID
|
||||||
|
* @param enabled Whether to enable the rule
|
||||||
|
* @return Result message
|
||||||
|
*/
|
||||||
|
String toggleAlertRule(Long ruleId, Boolean enabled);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get detailed information about an alert rule
|
||||||
|
* @param ruleId Alert rule ID
|
||||||
|
* @return Detailed rule information
|
||||||
|
*/
|
||||||
|
String getAlertRuleDetails(Long ruleId);
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the hierarchical structure of available apps and metrics for alert rule creation
|
||||||
|
* @param app App type to get hierarchy for (optional, gets all if not specified)
|
||||||
|
* @return Hierarchical structure showing apps and their available metrics
|
||||||
|
*/
|
||||||
|
String getAppsMetricsHierarchy(String app);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bind monitors to an alert rule by modifying the alert expression
|
||||||
|
* @param ruleId Alert rule ID to bind monitors to
|
||||||
|
* @param monitorIds Comma-separated list of monitor IDs to bind
|
||||||
|
* @return Result message indicating success or failure
|
||||||
|
*/
|
||||||
|
String bindMonitorsToAlertRule(Long ruleId, String monitorIds);
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
/*
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||||
|
* contributor license agreements. See the NOTICE file distributed with
|
||||||
|
* this work for additional information regarding copyright ownership.
|
||||||
|
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||||
|
* (the "License"); you may not use this file except in compliance with
|
||||||
|
* the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
package org.apache.hertzbeat.ai.tools;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tools for alert operations and alarm data queries
|
||||||
|
*/
|
||||||
|
public interface AlertTools {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Query alerts with comprehensive filtering and pagination
|
||||||
|
* @param alertType Alert type (single, group, both)
|
||||||
|
* @param status Alert status (firing, resolved, all)
|
||||||
|
* @param search Search term for alert content or labels
|
||||||
|
* @param sort Sort field (startAt, triggerTimes, status)
|
||||||
|
* @param order Sort order (asc, desc)
|
||||||
|
* @param pageIndex Page index
|
||||||
|
* @param pageSize Page size
|
||||||
|
* @return Formatted string with alert information
|
||||||
|
*/
|
||||||
|
String queryAlerts(String alertType, String status, String search, String sort, String order, Integer pageIndex, Integer pageSize);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get alerts summary statistics
|
||||||
|
* @return Alert summary information including counts by status
|
||||||
|
*/
|
||||||
|
String getAlertsSummary();
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
/*
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||||
|
* contributor license agreements. See the NOTICE file distributed with
|
||||||
|
* this work for additional information regarding copyright ownership.
|
||||||
|
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||||
|
* (the "License"); you may not use this file except in compliance with
|
||||||
|
* the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
package org.apache.hertzbeat.ai.tools;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tools for metrics data operations and queries
|
||||||
|
*/
|
||||||
|
public interface MetricsTools {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get real-time metrics data for a monitor
|
||||||
|
* @param monitorId Monitor ID
|
||||||
|
* @param metrics Metrics name (e.g., "system", "cpu", "memory")
|
||||||
|
* @return Formatted real-time metrics data
|
||||||
|
*/
|
||||||
|
String getRealtimeMetrics(Long monitorId, String metrics);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get historical metrics data for a monitor
|
||||||
|
* @param monitorId Monitor ID
|
||||||
|
* @param app Monitor type (e.g., "linux", "mysql", "http")
|
||||||
|
* @param metrics Metrics name (e.g., "system", "cpu", "memory")
|
||||||
|
* @param metric Specific metric field (e.g., "usage", "used", "available")
|
||||||
|
* @param label Label filter for specific instances
|
||||||
|
* @param history Time range (e.g., "1h", "6h", "24h", "7d")
|
||||||
|
* @param interval Whether to aggregate data with intervals
|
||||||
|
* @return Historical metrics data formatted for display
|
||||||
|
*/
|
||||||
|
String getHistoricalMetrics(Long monitorId, String app, String metrics, String metric, String label, String history, Boolean interval);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check warehouse storage server status
|
||||||
|
* @return Status of the metrics storage system
|
||||||
|
*/
|
||||||
|
String getWarehouseStatus();
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
/*
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||||
|
* contributor license agreements. See the NOTICE file distributed with
|
||||||
|
* this work for additional information regarding copyright ownership.
|
||||||
|
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||||
|
* (the "License"); you may not use this file except in compliance with
|
||||||
|
* the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
package org.apache.hertzbeat.ai.tools;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface for Monitoring Tools
|
||||||
|
*/
|
||||||
|
public interface MonitorTools {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a new monitor with comprehensive configuration
|
||||||
|
*
|
||||||
|
* @param name Monitor name
|
||||||
|
* @param app Monitor type/application (e.g., 'linux', 'mysql', 'http')
|
||||||
|
* @param host Target host (IP address or domain name)
|
||||||
|
* @param port Target port (optional, depends on monitor type)
|
||||||
|
* @param intervals Collection interval in seconds (default: 600)
|
||||||
|
* @param username Username for authentication (optional)
|
||||||
|
* @param password Password for authentication (optional)
|
||||||
|
* @param database Database name (for database monitors)
|
||||||
|
* @param additionalParams Additional app-specific parameters as JSON string (optional)
|
||||||
|
* @param description Monitor description (optional)
|
||||||
|
* @return Result message with monitor ID if successful
|
||||||
|
*/
|
||||||
|
String addMonitor(
|
||||||
|
String name,
|
||||||
|
String app,
|
||||||
|
String host,
|
||||||
|
Integer port,
|
||||||
|
Integer intervals,
|
||||||
|
String username,
|
||||||
|
String password,
|
||||||
|
String database,
|
||||||
|
String additionalParams,
|
||||||
|
String description
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List all available monitor types that can be added
|
||||||
|
*
|
||||||
|
* @param language Language code for localized names (e.g., 'en-US', 'zh-CN')
|
||||||
|
* @return Formatted string list of available monitor types with descriptions
|
||||||
|
*/
|
||||||
|
String listMonitorTypes(String language);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Comprehensive monitor querying with flexible filtering, pagination, and specialized views
|
||||||
|
* @param ids Specific monitor IDs to retrieve (optional)
|
||||||
|
* @param app Monitor type filter (linux, mysql, http, etc.)
|
||||||
|
* @param status Monitor status (1=online, 2=offline, 3=unreachable, 0=paused, 9=all)
|
||||||
|
* @param search Search in monitor names or hosts (partial matching)
|
||||||
|
* @param labels Label filters, format: 'key1:value1,key2:value2'
|
||||||
|
* @param sort Sort field (name, gmtCreate, gmtUpdate, status, app)
|
||||||
|
* @param order Sort order (asc, desc)
|
||||||
|
* @param pageIndex Page number starting from 0
|
||||||
|
* @param pageSize Items per page (1-100 recommended)
|
||||||
|
* @param includeStats Include status statistics summary
|
||||||
|
* @return Comprehensive monitor information with optional statistics
|
||||||
|
*/
|
||||||
|
String queryMonitors(
|
||||||
|
List<Long> ids,
|
||||||
|
String app,
|
||||||
|
Byte status,
|
||||||
|
String search,
|
||||||
|
String labels,
|
||||||
|
String sort,
|
||||||
|
String order,
|
||||||
|
Integer pageIndex,
|
||||||
|
Integer pageSize,
|
||||||
|
Boolean includeStats);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get parameter definitions required for a specific monitor type
|
||||||
|
*
|
||||||
|
* @param app Monitor type/application name (e.g., 'linux', 'mysql', 'redis')
|
||||||
|
* @return Formatted string with parameter definitions including field names, types, and requirements
|
||||||
|
*/
|
||||||
|
String getMonitorAdditionalParams(String app);
|
||||||
|
}
|
||||||
+695
@@ -0,0 +1,695 @@
|
|||||||
|
/*
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||||
|
* contributor license agreements. See the NOTICE file distributed with
|
||||||
|
* this work for additional information regarding copyright ownership.
|
||||||
|
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||||
|
* (the "License"); you may not use this file except in compliance with
|
||||||
|
* the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.apache.hertzbeat.ai.tools.impl;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.fasterxml.jackson.databind.node.ArrayNode;
|
||||||
|
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||||
|
import com.usthe.sureness.subject.SubjectSum;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.hertzbeat.ai.config.McpContextHolder;
|
||||||
|
import org.apache.hertzbeat.alert.service.AlertDefineService;
|
||||||
|
import org.apache.hertzbeat.manager.service.AppService;
|
||||||
|
import org.apache.hertzbeat.ai.tools.AlertDefineTools;
|
||||||
|
import org.apache.hertzbeat.ai.utils.UtilityClass;
|
||||||
|
import org.apache.hertzbeat.common.entity.alerter.AlertDefine;
|
||||||
|
import org.apache.hertzbeat.manager.pojo.dto.Hierarchy;
|
||||||
|
import org.springframework.ai.tool.annotation.Tool;
|
||||||
|
import org.springframework.ai.tool.annotation.ToolParam;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.data.domain.Page;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implementation of Alert Define Tools functionality
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
public class AlertDefineToolsImpl implements AlertDefineTools {
|
||||||
|
@Autowired
|
||||||
|
private AlertDefineService alertDefineService;
|
||||||
|
@Autowired
|
||||||
|
private AppService appService;
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Tool(name = "create_alert_rule", description = """
|
||||||
|
ALERT RULE means when to alert a user
|
||||||
|
THESE ARE ALERT RULES WITH THRESHOLD VALUES. USERS CAN SPECIFY THE THRESHOLD VALUES FOR EXAMPLE,
|
||||||
|
IF THE USER SAYS "ALERT ME WHEN MY COST EXCEEDS 700, THE EXPRESSION SHOULD BE 'cost > 700' NOT 'cost < 700'.
|
||||||
|
APPLY THE SAME LOGIC FOR LESS THAN OPERATOR.
|
||||||
|
Create a HertzBeat alert rule based on app hierarchy structure and user requirements.
|
||||||
|
It is important to first understand the hierarchy of apps, metrics, and field conditions
|
||||||
|
Each app has its own metrics and each metric has its own field conditions.
|
||||||
|
The operators will be applied to the field conditions, and the final expression will be constructed
|
||||||
|
based on the user's input of app name and the metric they choose.
|
||||||
|
CRITICAL WORKFLOW Do all of this iteratively with user interaction at each step
|
||||||
|
1. ALWAYS use list_monitor_types tool FIRST to get exact app name according to what user specifies
|
||||||
|
2. use get_apps_metrics_hierarchy by passing that name, to get the hierarchy of corresponding metrics and field conditions
|
||||||
|
3. Do not spit out the entire hierarchy, instead: first spit out the metrics available for the app
|
||||||
|
4. Ask the user to choose a metric from the available metrics
|
||||||
|
5. Based on the metric chosen, present the available field conditions/params
|
||||||
|
6. You will construct the proper expression with field conditions
|
||||||
|
7. once this tool successfully executes, ask the user if they want to bind any existing monitors to this alert rule,
|
||||||
|
get the monitors list for a particular app using the query_monitors tool.
|
||||||
|
8. based on the user's output, conditionally call the bind_monitors_to_alert_rule tool to bind monitors to the alert rule
|
||||||
|
VERY VERY IMPORTANT:
|
||||||
|
- ALWAYS USE the value field from the get_apps_metrics_hierarchy's json response when creating alert expressions on the field parameters
|
||||||
|
|
||||||
|
EXAMPLES FOR FIELD CONDITION EXPRESSION( Do not copy these examples, they are just for reference ):
|
||||||
|
These are all just examples, you can take inspiration from them and create a rule based on hierarchy, always ask the user for all params, do not assume them, even for these examples:
|
||||||
|
|
||||||
|
1. Kafka JVM Alert:
|
||||||
|
- App: "kafka", Metric: "jvm_basic"
|
||||||
|
- Field condition: equals(VmName, "myVM")
|
||||||
|
- Field condition expression: equals(VmName, "myVM")
|
||||||
|
|
||||||
|
2. LLM Credits Alert:
|
||||||
|
- App: "openai", Metric: "credit_grants"
|
||||||
|
- Field condition: total_granted > some_value
|
||||||
|
- Field condition expression: total_granted > 1000
|
||||||
|
|
||||||
|
3. HBase Master Alert:
|
||||||
|
- App: "hbase_master", Metric: "server"
|
||||||
|
- Field condition: heap_memory_used > 80 or some_factor<100
|
||||||
|
- Field condition expression: heap_memory_used > 80 or some_factor<100
|
||||||
|
|
||||||
|
4. Complex OpenAI Credits Alert:
|
||||||
|
- App: "openai", Metric: "credit_grants"
|
||||||
|
- Field condition: total_used > 123 and total_granted > 333 and (total_granted > 3444 and total_paid_available < 5556)
|
||||||
|
- Field condition expression: total_used > 123 and total_granted > 333 and (total_granted > 3444 and total_paid_available < 5556)
|
||||||
|
|
||||||
|
FIELD CONDITIONS GUIDANCE:
|
||||||
|
- Field names come from metric's children in hierarchy (leaf nodes)
|
||||||
|
- Use the "value" field from the metric's children, not the label when creating conditions
|
||||||
|
- Supported operators: >, <, >=, <=, ==, !=, exists(), !exists() for numeric fields
|
||||||
|
- equals(), contains(), matches(),exists(), !equals(), !contains(), !matches(), !exists() for string fields
|
||||||
|
- Supported logical operators: and, or to connect different field parameter rules or rulesets
|
||||||
|
- ONLY USE THESE OPERATORS, when creating conditions, do not use any other operators
|
||||||
|
- Support grouping with parentheses: (condition1 and condition2) or condition3
|
||||||
|
- String values should be quoted: equals(VmName, "my-vm")
|
||||||
|
- Simple conditions: heap_memory_used > 80, total_granted <= 1000
|
||||||
|
- Complex conditions: total_used > 123 and total_granted > 333 and (total_granted > 3444 and total_paid_available < 5556)
|
||||||
|
|
||||||
|
PRIORITY LEVELS:
|
||||||
|
- 0: Critical (immediate action required)
|
||||||
|
- 1: Warning (attention needed, default)
|
||||||
|
- 2: Info (informational only)
|
||||||
|
""")
|
||||||
|
public String createAlertRule(
|
||||||
|
@ToolParam(description = "Alert rule name (required, must be unique)", required = true) String name,
|
||||||
|
@ToolParam(description = "App name from hierarchy (must match exact hierarchy app value)", required = true) String app,
|
||||||
|
@ToolParam(description = "Metrics name from hierarchy (must match exact hierarchy metrics value)", required = true) String metrics,
|
||||||
|
@ToolParam(description = "Field conditions expression)", required = true) String fieldConditions,
|
||||||
|
@ToolParam(description = "Alert rule type: 'realtime' (default) or 'periodic'", required = false) String type,
|
||||||
|
@ToolParam(description = "Execution period in seconds (only for periodic rules, default: 300)", required = false) Integer period,
|
||||||
|
@ToolParam(description = "Number of consecutive violations before triggering (default: 3)", required = false) Integer times,
|
||||||
|
@ToolParam(description = "Alert priority as integer: 0=critical, 1=warning, 2=info (default: 1)", required = false) Integer priority,
|
||||||
|
@ToolParam(description = "Alert rule description (optional)", required = false) String description,
|
||||||
|
@ToolParam(description = "Alert message template with variables (optional)", required = false) String template,
|
||||||
|
@ToolParam(description = "Data source type: 'promql' (default)", required = false) String datasource,
|
||||||
|
@ToolParam(description = "Labels as key:value pairs separated by commas (e.g., 'env:prod,severity:critical')", required = false) String labels,
|
||||||
|
@ToolParam(description = "Annotations as key:value pairs separated by commas (e.g., 'summary:High CPU')", required = false) String annotations,
|
||||||
|
@ToolParam(description = "Whether to enable the rule immediately (default: true)", required = false) Boolean enable) {
|
||||||
|
|
||||||
|
try {
|
||||||
|
log.info("Creating HertzBeat alert rule: name={}, app={}, metrics={}, fieldConditions={}", name, app, metrics, fieldConditions);
|
||||||
|
SubjectSum subjectSum = McpContextHolder.getSubject();
|
||||||
|
log.debug("Current subject in create_alert_rule tool: {}", subjectSum);
|
||||||
|
|
||||||
|
// Validate required parameters
|
||||||
|
if (name == null || name.trim().isEmpty()) {
|
||||||
|
return "Error: Alert rule name is required";
|
||||||
|
}
|
||||||
|
if (app == null || app.trim().isEmpty()) {
|
||||||
|
return "Error: App name is required (use get_apps_metrics_hierarchy to find exact names)";
|
||||||
|
}
|
||||||
|
if (metrics == null || metrics.trim().isEmpty()) {
|
||||||
|
return "Error: Metrics name is required (use get_apps_metrics_hierarchy to find exact names)";
|
||||||
|
}
|
||||||
|
if (fieldConditions == null || fieldConditions.trim().isEmpty()) {
|
||||||
|
return "Error: Field conditions are required (e.g., 'equals(VmName, \"arora\")', 'total_granted > 1000')";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set defaults
|
||||||
|
if (type == null || type.trim().isEmpty()) {
|
||||||
|
type = "realtime";
|
||||||
|
}
|
||||||
|
if (times == null || times <= 0) {
|
||||||
|
times = 3;
|
||||||
|
}
|
||||||
|
if (priority == null) {
|
||||||
|
priority = 1; // Default to warning
|
||||||
|
}
|
||||||
|
if (enable == null) {
|
||||||
|
enable = true;
|
||||||
|
}
|
||||||
|
if (datasource == null || datasource.trim().isEmpty()) {
|
||||||
|
datasource = "promql";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate alert type
|
||||||
|
if (!type.equals("realtime") && !type.equals("periodic")) {
|
||||||
|
return "Error: Alert type must be 'realtime' or 'periodic'";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate priority
|
||||||
|
if (priority < 0 || priority > 2) {
|
||||||
|
return "Error: Priority must be 0 (critical), 1 (warning), or 2 (info)";
|
||||||
|
}
|
||||||
|
|
||||||
|
// For periodic rules, validate period parameter
|
||||||
|
if (type.equals("periodic")) {
|
||||||
|
if (period == null || period <= 0) {
|
||||||
|
period = 300; // Default 5 minutes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CRITICAL VALIDATION: Verify app-metric-field relationships using hierarchy
|
||||||
|
String validationResult = validateHierarchyRelationships(app.trim(), metrics.trim(), fieldConditions.trim());
|
||||||
|
if (!validationResult.equals("VALID")) {
|
||||||
|
return validationResult; // Return validation error message
|
||||||
|
}
|
||||||
|
|
||||||
|
// EXPRESSION VALIDATION: Verify field conditions syntax and operators
|
||||||
|
String expressionValidation = UtilityClass.validateExpressionSyntax(fieldConditions.trim());
|
||||||
|
if (!expressionValidation.equals("VALID")) {
|
||||||
|
return expressionValidation; // Return expression validation error message
|
||||||
|
}
|
||||||
|
|
||||||
|
String expr = String.format("equals(__app__,\"%s\") && equals(__metrics__,\"%s\") && %s",
|
||||||
|
app.trim(), metrics.trim(), fieldConditions.trim());
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// Parse labels if provided
|
||||||
|
Map<String, String> labelsMap = new HashMap<>();
|
||||||
|
if (labels != null && !labels.trim().isEmpty()) {
|
||||||
|
labelsMap.putAll(UtilityClass.parseKeyValuePairs(labels));
|
||||||
|
}
|
||||||
|
// Add severity based on priority
|
||||||
|
String severityLabel = priority == 0 ? "critical" : (priority == 1 ? "warning" : "info");
|
||||||
|
labelsMap.put("severity", severityLabel);
|
||||||
|
|
||||||
|
// Parse annotations if provided
|
||||||
|
Map<String, String> annotationsMap = new HashMap<>();
|
||||||
|
if (annotations != null && !annotations.trim().isEmpty()) {
|
||||||
|
annotationsMap.putAll(UtilityClass.parseKeyValuePairs(annotations));
|
||||||
|
}
|
||||||
|
// Add default annotations if not provided
|
||||||
|
if (!annotationsMap.containsKey("summary")) {
|
||||||
|
annotationsMap.put("summary", description != null ? description :
|
||||||
|
String.format("Alert for %s %s when %s", app, metrics, fieldConditions));
|
||||||
|
}
|
||||||
|
if (!annotationsMap.containsKey("description")) {
|
||||||
|
annotationsMap.put("description", String.format("Monitor %s metrics %s with conditions: %s", app, metrics, fieldConditions));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate default template if not provided
|
||||||
|
if (template == null || template.trim().isEmpty()) {
|
||||||
|
template = String.format("Alert: %s %s - %s", app, metrics, fieldConditions);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create comprehensive alert definition
|
||||||
|
AlertDefine alertDefine = AlertDefine.builder()
|
||||||
|
.name(name.trim())
|
||||||
|
.type(type)
|
||||||
|
.expr(expr)
|
||||||
|
.period(period)
|
||||||
|
.times(times)
|
||||||
|
.labels(labelsMap)
|
||||||
|
.annotations(annotationsMap)
|
||||||
|
.template(template)
|
||||||
|
.datasource(datasource)
|
||||||
|
.enable(enable)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
log.debug("Current security subject for addAlertDefine: {}", subjectSum);
|
||||||
|
|
||||||
|
alertDefineService.addAlertDefine(alertDefine);
|
||||||
|
|
||||||
|
// Note: Monitor binding is handled separately via bind_monitors_to_alert_rule tool
|
||||||
|
String bindingNote = String.format(" (Use bind_monitors_to_alert_rule tool to associate specific monitors)");
|
||||||
|
|
||||||
|
log.info("Successfully created alert rule '{}' with ID: {}", name, alertDefine.getId());
|
||||||
|
|
||||||
|
StringBuilder response = new StringBuilder();
|
||||||
|
response.append(String.format("Successfully created %s alert rule '%s' with ID: %d\n",
|
||||||
|
type, name, alertDefine.getId()));
|
||||||
|
response.append(String.format("Expression: %s\n", expr));
|
||||||
|
response.append(String.format("Priority: %d (%s)\n", priority, severityLabel));
|
||||||
|
response.append(String.format("Trigger after: %d consecutive violations\n", times));
|
||||||
|
if (type.equals("periodic")) {
|
||||||
|
response.append(String.format("Execution period: %d seconds\n", period));
|
||||||
|
}
|
||||||
|
response.append(String.format("Data source: %s\n", datasource));
|
||||||
|
response.append(String.format("Enabled: %s\n", enable));
|
||||||
|
if (!labelsMap.isEmpty()) {
|
||||||
|
response.append(String.format("Labels: %s\n", labelsMap));
|
||||||
|
}
|
||||||
|
response.append(bindingNote);
|
||||||
|
|
||||||
|
return response.toString();
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Failed to create alert rule '{}': {}", name, e.getMessage(), e);
|
||||||
|
return "Error creating alert rule '" + name + "': " + e.getMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ... other existing methods would go here ...
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Tool(name = "list_alert_rules", description = """
|
||||||
|
List existing alert rules with filtering options.
|
||||||
|
Shows configured thresholds and alert definitions.
|
||||||
|
""")
|
||||||
|
public String listAlertRules(
|
||||||
|
@ToolParam(description = "Search term for rule name or description", required = false) String search,
|
||||||
|
@ToolParam(description = "Filter by monitor type", required = false) String monitorType,
|
||||||
|
@ToolParam(description = "Filter by enabled status", required = false) Boolean enabled,
|
||||||
|
@ToolParam(description = "Page index (default: 0)", required = false) Integer pageIndex,
|
||||||
|
@ToolParam(description = "Page size (default: 10)", required = false) Integer pageSize) {
|
||||||
|
|
||||||
|
try {
|
||||||
|
log.info("Listing alert rules: search={}, monitorType={}, enabled={}", search, monitorType, enabled);
|
||||||
|
|
||||||
|
if (pageIndex == null || pageIndex < 0) {
|
||||||
|
pageIndex = 0;
|
||||||
|
}
|
||||||
|
if (pageSize == null || pageSize <= 0) {
|
||||||
|
pageSize = 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
SubjectSum subjectSum = McpContextHolder.getSubject();
|
||||||
|
log.debug("Current security subject for getAlertDefines: {}", subjectSum);
|
||||||
|
|
||||||
|
Page<AlertDefine> result = alertDefineService.getAlertDefines(null,
|
||||||
|
search, "gmtCreate", "desc", pageIndex, pageSize);
|
||||||
|
|
||||||
|
StringBuilder response = new StringBuilder();
|
||||||
|
response.append("Found ").append(result.getContent().size())
|
||||||
|
.append(" alert rules (Total: ").append(result.getTotalElements()).append("):\n\n");
|
||||||
|
|
||||||
|
for (AlertDefine alertDefine : result.getContent()) {
|
||||||
|
response.append("Rule ID: ").append(alertDefine.getId()).append("\n");
|
||||||
|
response.append("Name: ").append(alertDefine.getName()).append("\n");
|
||||||
|
response.append("Expression: ").append(alertDefine.getExpr()).append("\n");
|
||||||
|
response.append("Type: ").append(alertDefine.getType()).append("\n");
|
||||||
|
response.append("Trigger Times: ").append(alertDefine.getTimes()).append("\n");
|
||||||
|
response.append("Enabled: ").append(alertDefine.isEnable()).append("\n");
|
||||||
|
|
||||||
|
if (alertDefine.getLabels() != null && !alertDefine.getLabels().isEmpty()) {
|
||||||
|
response.append("Labels: ").append(alertDefine.getLabels()).append("\n");
|
||||||
|
}
|
||||||
|
if (alertDefine.getAnnotations() != null && !alertDefine.getAnnotations().isEmpty()) {
|
||||||
|
response.append("Summary: ").append(alertDefine.getAnnotations().get("summary")).append("\n");
|
||||||
|
}
|
||||||
|
response.append("Created: ").append(alertDefine.getGmtCreate()).append("\n");
|
||||||
|
response.append("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.getContent().isEmpty()) {
|
||||||
|
response.append("No alert rules found matching the specified criteria.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.toString();
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Failed to list alert rules: {}", e.getMessage(), e);
|
||||||
|
return "Error retrieving alert rules: " + e.getMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Tool(name = "toggle_alert_rule", description = """
|
||||||
|
Enable or disable an alert rule.
|
||||||
|
Allows activating or deactivating threshold monitoring.
|
||||||
|
""")
|
||||||
|
public String toggleAlertRule(
|
||||||
|
@ToolParam(description = "Alert rule ID", required = true) Long ruleId,
|
||||||
|
@ToolParam(description = "Whether to enable the rule", required = true) Boolean enabled) {
|
||||||
|
|
||||||
|
try {
|
||||||
|
log.info("Toggling alert rule ID: {} to enabled: {}", ruleId, enabled);
|
||||||
|
|
||||||
|
SubjectSum subjectSum = McpContextHolder.getSubject();
|
||||||
|
log.debug("Current security subject for toggleAlertDefineStatus: {}", subjectSum);
|
||||||
|
|
||||||
|
// First get the existing AlertDefine
|
||||||
|
AlertDefine alertDefine = alertDefineService.getAlertDefine(ruleId);
|
||||||
|
|
||||||
|
if (alertDefine == null) {
|
||||||
|
throw new RuntimeException("AlertDefine with ID " + ruleId + " not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the enable status
|
||||||
|
alertDefine.setEnable(enabled);
|
||||||
|
|
||||||
|
// Use modifyAlertDefine to save the changes
|
||||||
|
alertDefineService.modifyAlertDefine(alertDefine);
|
||||||
|
|
||||||
|
log.info("Successfully toggled alert rule ID: {} to enabled: {}", ruleId, enabled);
|
||||||
|
return String.format("Successfully %s alert rule ID: %d",
|
||||||
|
enabled ? "enabled" : "disabled", ruleId);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Failed to toggle alert rule ID {}: {}", ruleId, e.getMessage(), e);
|
||||||
|
return "Error toggling alert rule: " + e.getMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Tool(name = "get_alert_rule_details", description = """
|
||||||
|
Get detailed information about a specific alert rule.
|
||||||
|
Shows complete threshold configuration and rule settings.
|
||||||
|
""")
|
||||||
|
public String getAlertRuleDetails(
|
||||||
|
@ToolParam(description = "Alert rule ID", required = true) Long ruleId) {
|
||||||
|
|
||||||
|
try {
|
||||||
|
log.info("Getting alert rule details for ID: {}", ruleId);
|
||||||
|
|
||||||
|
SubjectSum subjectSum = McpContextHolder.getSubject();
|
||||||
|
log.debug("Current security subject for getAlertDefine: {}", subjectSum);
|
||||||
|
|
||||||
|
AlertDefine alertDefine = alertDefineService.getAlertDefine(ruleId);
|
||||||
|
if (alertDefine == null) {
|
||||||
|
return "Alert rule with ID " + ruleId + " not found";
|
||||||
|
}
|
||||||
|
|
||||||
|
StringBuilder response = new StringBuilder();
|
||||||
|
response.append("ALERT RULE DETAILS\n");
|
||||||
|
response.append("==================\n\n");
|
||||||
|
|
||||||
|
response.append("Rule ID: ").append(alertDefine.getId()).append("\n");
|
||||||
|
response.append("Name: ").append(alertDefine.getName()).append("\n");
|
||||||
|
response.append("Type: ").append(alertDefine.getType()).append("\n");
|
||||||
|
response.append("Expression: ").append(alertDefine.getExpr()).append("\n");
|
||||||
|
response.append("Trigger Times: ").append(alertDefine.getTimes()).append("\n");
|
||||||
|
response.append("Enabled: ").append(alertDefine.isEnable()).append("\n");
|
||||||
|
|
||||||
|
if (alertDefine.getPeriod() != null) {
|
||||||
|
response.append("Period: ").append(alertDefine.getPeriod()).append(" seconds\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (alertDefine.getLabels() != null && !alertDefine.getLabels().isEmpty()) {
|
||||||
|
response.append("Labels: ").append(alertDefine.getLabels()).append("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (alertDefine.getAnnotations() != null && !alertDefine.getAnnotations().isEmpty()) {
|
||||||
|
response.append("Annotations: ").append(alertDefine.getAnnotations()).append("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (alertDefine.getTemplate() != null) {
|
||||||
|
response.append("Template: ").append(alertDefine.getTemplate()).append("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
response.append("Created: ").append(alertDefine.getGmtCreate()).append("\n");
|
||||||
|
response.append("Modified: ").append(alertDefine.getGmtUpdate()).append("\n");
|
||||||
|
response.append("Creator: ").append(alertDefine.getCreator()).append("\n");
|
||||||
|
response.append("Modifier: ").append(alertDefine.getModifier()).append("\n");
|
||||||
|
|
||||||
|
return response.toString();
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Failed to get alert rule details for ID {}: {}", ruleId, e.getMessage(), e);
|
||||||
|
return "Error retrieving alert rule details: " + e.getMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Tool(name = "get_apps_metrics_hierarchy", description = """
|
||||||
|
Get the hierarchical structure of all available apps and their metrics for alert rule creation.
|
||||||
|
This tool provides the exact app name, metric name and corresponding param names according to each metric.
|
||||||
|
Returns structured JSON data showing the complete hierarchy with field parameters for alert expressions.
|
||||||
|
|
||||||
|
JSON Structure:
|
||||||
|
- app: The application name
|
||||||
|
- description: Tool description
|
||||||
|
- hierarchy: Array of hierarchical data
|
||||||
|
- Each node has: value, label, type, description
|
||||||
|
- Leaf nodes have: dataType (numeric/string), unit (if applicable)
|
||||||
|
- Non-leaf nodes have: children array
|
||||||
|
VERY IMPORTANT:
|
||||||
|
- ALWAYS USE the value field from the field parameters when creating alert expressions.
|
||||||
|
|
||||||
|
This structured data is needed to create proper alert expressions.
|
||||||
|
""")
|
||||||
|
public String getAppsMetricsHierarchy(
|
||||||
|
@ToolParam(description = "App/Monitor type to get hierarchy for (e.g., 'linux', 'mysql', 'website')", required = true) String app) {
|
||||||
|
|
||||||
|
try {
|
||||||
|
log.info("Getting apps metrics hierarchy for app: {}", app);
|
||||||
|
SubjectSum subjectSum = McpContextHolder.getSubject();
|
||||||
|
log.debug("Current subject in get_apps_metrics_hierarchy tool: {}", subjectSum);
|
||||||
|
|
||||||
|
List<Hierarchy> hierarchies;
|
||||||
|
hierarchies = appService.getAppHierarchy(app.trim().toLowerCase(), "en-US");
|
||||||
|
|
||||||
|
|
||||||
|
ObjectMapper mapper = new ObjectMapper();
|
||||||
|
ObjectNode result = mapper.createObjectNode();
|
||||||
|
|
||||||
|
result.put("app", app.toUpperCase());
|
||||||
|
|
||||||
|
if (hierarchies != null && !hierarchies.isEmpty()) {
|
||||||
|
ArrayNode hierarchyArray = mapper.createArrayNode();
|
||||||
|
for (Hierarchy hierarchy : hierarchies) {
|
||||||
|
hierarchyArray.add(UtilityClass.formatHierarchyAsJson(mapper, hierarchy));
|
||||||
|
}
|
||||||
|
result.set("hierarchy", hierarchyArray);
|
||||||
|
} else {
|
||||||
|
result.put("message", "No hierarchy data available");
|
||||||
|
}
|
||||||
|
|
||||||
|
return mapper.writerWithDefaultPrettyPrinter().writeValueAsString(result);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Failed to get apps metrics hierarchy: {}", e.getMessage(), e);
|
||||||
|
return "Error retrieving apps metrics hierarchy: " + e.getMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Tool(name = "bind_monitors_to_alert_rule", description = """
|
||||||
|
Bind monitors to an alert rule.
|
||||||
|
Call this tool if users want to bind specific monitors to their alert rule.
|
||||||
|
Get the right monitor ids for a particular app using the query_monitors tool.
|
||||||
|
Get the alert rule ID from the create_alert_rule tool output OR use the list_alert_rules tool with app_name search filter, if the output of create_alert_rule is not applicable.
|
||||||
|
If monitors are already bound, this will add the new ones to the existing bindings.
|
||||||
|
""")
|
||||||
|
public String bindMonitorsToAlertRule(
|
||||||
|
@ToolParam(description = "Alert rule ID to bind monitors to", required = true) Long ruleId,
|
||||||
|
@ToolParam(description = "Comma-separated list of monitor IDs to bind", required = true) String monitorIds) {
|
||||||
|
try {
|
||||||
|
log.info("Binding monitors to alert rule ID: {}, monitors: {}", ruleId, monitorIds);
|
||||||
|
SubjectSum subjectSum = McpContextHolder.getSubject();
|
||||||
|
log.debug("Current subject in bind_monitors_to_alert_rule tool: {}", subjectSum);
|
||||||
|
|
||||||
|
if (ruleId == null || ruleId <= 0) {
|
||||||
|
return "Error: Valid alert rule ID is required";
|
||||||
|
}
|
||||||
|
if (monitorIds == null) {
|
||||||
|
return "Error: Monitor IDs are required";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the existing alert rule
|
||||||
|
log.debug("Current security subject for getAlertDefine: {}", subjectSum);
|
||||||
|
|
||||||
|
AlertDefine existingRule = alertDefineService.getAlertDefine(ruleId);
|
||||||
|
if (existingRule == null) {
|
||||||
|
return String.format("Error: Alert rule with ID %d not found", ruleId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse monitor IDs from comma-separated string
|
||||||
|
String[] monitorIdArray = monitorIds.split(",");
|
||||||
|
List<String> validMonitorIds = new ArrayList<>();
|
||||||
|
|
||||||
|
for (String monitorId : monitorIdArray) {
|
||||||
|
String trimmedId = monitorId.trim();
|
||||||
|
if (!trimmedId.isEmpty()) {
|
||||||
|
try {
|
||||||
|
Long.parseLong(trimmedId); // Validate it's a number
|
||||||
|
validMonitorIds.add(trimmedId);
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
return String.format("Error: Invalid monitor ID '%s'. Monitor IDs must be numeric.", trimmedId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (validMonitorIds.isEmpty()) {
|
||||||
|
return "Error: No valid monitor IDs provided";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build the monitor instance condition
|
||||||
|
String monitorCondition;
|
||||||
|
if (validMonitorIds.size() == 1) {
|
||||||
|
monitorCondition = String.format("equals(__instance__, \"%s\")", validMonitorIds.get(0));
|
||||||
|
} else {
|
||||||
|
StringBuilder conditionBuilder = new StringBuilder("(");
|
||||||
|
for (int i = 0; i < validMonitorIds.size(); i++) {
|
||||||
|
if (i > 0) {
|
||||||
|
conditionBuilder.append(" or ");
|
||||||
|
}
|
||||||
|
conditionBuilder.append(String.format("equals(__instance__, \"%s\")", validMonitorIds.get(i)));
|
||||||
|
}
|
||||||
|
conditionBuilder.append(")");
|
||||||
|
monitorCondition = conditionBuilder.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the current expression and modify it
|
||||||
|
String currentExpr = existingRule.getExpr();
|
||||||
|
String newExpr;
|
||||||
|
|
||||||
|
// Check if the expression already has __instance__ conditions
|
||||||
|
if (currentExpr.contains("__instance__")) {
|
||||||
|
// Extract existing monitor IDs and merge with new ones
|
||||||
|
List<String> existingMonitorIds = UtilityClass.extractExistingMonitorIds(currentExpr);
|
||||||
|
|
||||||
|
// Add new monitor IDs that aren't already present
|
||||||
|
for (String newId : validMonitorIds) {
|
||||||
|
if (!existingMonitorIds.contains(newId)) {
|
||||||
|
existingMonitorIds.add(newId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String updatedMonitorCondition;
|
||||||
|
if (existingMonitorIds.size() == 1) {
|
||||||
|
updatedMonitorCondition = String.format("equals(__instance__, \"%s\")", existingMonitorIds.get(0));
|
||||||
|
} else {
|
||||||
|
StringBuilder conditionBuilder = new StringBuilder("(");
|
||||||
|
for (int i = 0; i < existingMonitorIds.size(); i++) {
|
||||||
|
if (i > 0) {
|
||||||
|
conditionBuilder.append(" or ");
|
||||||
|
}
|
||||||
|
conditionBuilder.append(String.format("equals(__instance__, \"%s\")", existingMonitorIds.get(i)));
|
||||||
|
}
|
||||||
|
conditionBuilder.append(")");
|
||||||
|
updatedMonitorCondition = conditionBuilder.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Replace existing __instance__ conditions with updated ones
|
||||||
|
newExpr = UtilityClass.replaceInstanceConditions(currentExpr, updatedMonitorCondition);
|
||||||
|
|
||||||
|
// Update the alert rule
|
||||||
|
existingRule.setExpr(newExpr);
|
||||||
|
log.debug("Current security subject for modifyAlertDefine: {}", subjectSum);
|
||||||
|
|
||||||
|
alertDefineService.modifyAlertDefine(existingRule);
|
||||||
|
|
||||||
|
log.info("Successfully added monitors {} to existing bindings for alert rule ID: {}", validMonitorIds, ruleId);
|
||||||
|
return String.format("Successfully added %d new monitor(s) to alert rule ID %d.\nTotal bound monitors: %s\nUpdated expression: %s",
|
||||||
|
validMonitorIds.size(), ruleId, String.join(", ", existingMonitorIds), newExpr);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert the monitor condition after the metrics condition
|
||||||
|
// Pattern: equals(__app__,"app") && equals(__metrics__,"metric") && [existing_conditions]
|
||||||
|
// Result: equals(__app__,"app") && equals(__metrics__,"metric") && [monitor_condition] && [existing_conditions]
|
||||||
|
|
||||||
|
if (currentExpr.matches(".*equals\\(__app__,\"[^\"]+\"\\)\\s*&&\\s*equals\\(__metrics__,\"[^\"]+\"\\)\\s*&&\\s*.*")) {
|
||||||
|
// Find the position after the metrics condition
|
||||||
|
String metricsPattern = "equals\\(__metrics__,\"[^\"]+\"\\)";
|
||||||
|
java.util.regex.Pattern regex = java.util.regex.Pattern.compile(metricsPattern);
|
||||||
|
java.util.regex.Matcher matcher = regex.matcher(currentExpr);
|
||||||
|
|
||||||
|
if (matcher.find()) {
|
||||||
|
int metricsEnd = matcher.end();
|
||||||
|
// Find the " && " after the metrics condition
|
||||||
|
int andPosition = currentExpr.indexOf(" && ", metricsEnd);
|
||||||
|
if (andPosition != -1) {
|
||||||
|
String beforeAndPosition = currentExpr.substring(0, andPosition + 4); // Include " && "
|
||||||
|
String afterAndPosition = currentExpr.substring(andPosition + 4); // Everything after " && "
|
||||||
|
newExpr = beforeAndPosition + monitorCondition + " && " + afterAndPosition;
|
||||||
|
} else {
|
||||||
|
return String.format("Error: Unable to find field conditions after metrics in expression: %s", currentExpr);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return String.format("Error: Unable to parse metrics condition in expression: %s", currentExpr);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return String.format("Error: Expression format not supported for monitor binding: %s", currentExpr);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the alert rule
|
||||||
|
existingRule.setExpr(newExpr);
|
||||||
|
|
||||||
|
alertDefineService.modifyAlertDefine(existingRule);
|
||||||
|
|
||||||
|
log.info("Successfully bound monitors {} to alert rule ID: {}", validMonitorIds, ruleId);
|
||||||
|
return String.format("Successfully bound %d monitor(s) to alert rule ID %d.\nMonitor IDs: %s\nUpdated expression: %s",
|
||||||
|
validMonitorIds.size(), ruleId, String.join(", ", validMonitorIds), newExpr);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Failed to bind monitors to alert rule ID {}: {}", ruleId, e.getMessage(), e);
|
||||||
|
return String.format("Error binding monitors to alert rule: %s", e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates that the app, metric, and field conditions are valid according to hierarchy
|
||||||
|
* @param app App name to validate
|
||||||
|
* @param metrics Metric name to validate for the app
|
||||||
|
* @param fieldConditions Field conditions to validate for the metric
|
||||||
|
* @return "VALID" if all relationships are correct, error message otherwise
|
||||||
|
*/
|
||||||
|
private String validateHierarchyRelationships(String app, String metrics, String fieldConditions) {
|
||||||
|
try {
|
||||||
|
log.debug("Validating hierarchy relationships: app={}, metrics={}, fieldConditions={}", app, metrics, fieldConditions);
|
||||||
|
|
||||||
|
// Get hierarchy for the specified app
|
||||||
|
SubjectSum subjectSum = McpContextHolder.getSubject();
|
||||||
|
log.debug("Current security subject for getAppHierarchy: {}", subjectSum);
|
||||||
|
|
||||||
|
List<Hierarchy> hierarchies = appService.getAppHierarchy(app.toLowerCase(), "en-US");
|
||||||
|
|
||||||
|
if (hierarchies == null || hierarchies.isEmpty()) {
|
||||||
|
return String.format("Error: App '%s' not found in hierarchy. Please use list_monitor_types to get valid app names.", app);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the metric in the app's hierarchy
|
||||||
|
Hierarchy metricHierarchy = UtilityClass.findMetricInHierarchy(hierarchies, metrics);
|
||||||
|
if (metricHierarchy == null) {
|
||||||
|
return String.format("Error: Metric '%s' not found for app '%s'. Please use get_apps_metrics_hierarchy to get valid metrics for this app.", metrics, app);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract field names from field conditions and validate them
|
||||||
|
List<String> fieldNames = UtilityClass.extractFieldNamesFromConditions(fieldConditions);
|
||||||
|
for (String fieldName : fieldNames) {
|
||||||
|
if (!UtilityClass.isFieldValidForMetric(metricHierarchy, fieldName)) {
|
||||||
|
return String.format("Error: Field '%s' not found for metric '%s' in app '%s'. Please use get_apps_metrics_hierarchy to get valid field parameters.", fieldName, metrics, app);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.debug("Hierarchy validation passed for app={}, metrics={}", app, metrics);
|
||||||
|
return "VALID";
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Error during hierarchy validation: {}", e.getMessage(), e);
|
||||||
|
return String.format("Error: Unable to validate hierarchy relationships: %s", e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,245 @@
|
|||||||
|
/*
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||||
|
* contributor license agreements. See the NOTICE file distributed with
|
||||||
|
* this work for additional information regarding copyright ownership.
|
||||||
|
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||||
|
* (the "License"); you may not use this file except in compliance with
|
||||||
|
* the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
package org.apache.hertzbeat.ai.tools.impl;
|
||||||
|
|
||||||
|
import com.usthe.sureness.subject.SubjectSum;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.hertzbeat.ai.config.McpContextHolder;
|
||||||
|
import org.apache.hertzbeat.ai.tools.AlertTools;
|
||||||
|
import org.apache.hertzbeat.ai.utils.UtilityClass;
|
||||||
|
import org.apache.hertzbeat.alert.dto.AlertSummary;
|
||||||
|
import org.apache.hertzbeat.alert.service.AlertService;
|
||||||
|
import org.apache.hertzbeat.common.entity.alerter.GroupAlert;
|
||||||
|
import org.apache.hertzbeat.common.entity.alerter.SingleAlert;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.data.domain.Page;
|
||||||
|
import org.springframework.ai.tool.annotation.Tool;
|
||||||
|
import org.springframework.ai.tool.annotation.ToolParam;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implementation of Alert Tools functionality for alarm data queries and management
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
public class AlertToolsImpl implements AlertTools {
|
||||||
|
@Autowired
|
||||||
|
private AlertService alertService;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Tool(name = "query_alerts", description = """
|
||||||
|
Query alerts with comprehensive filtering and pagination options.
|
||||||
|
|
||||||
|
ALERT TYPES:
|
||||||
|
- Pass alertType='single' for individual alert instances
|
||||||
|
- Pass alertType='group' for grouped/aggregated alerts
|
||||||
|
- Pass alertType='both' to get both types (separate sections)
|
||||||
|
|
||||||
|
STATUS FILTERING:
|
||||||
|
- 'firing': Currently active alerts requiring attention
|
||||||
|
- 'resolved': Previously active alerts that have been cleared
|
||||||
|
- 'all': Both firing and resolved alerts (default)
|
||||||
|
|
||||||
|
SEARCH & FILTERING:
|
||||||
|
- search: Search in alert content, labels, or descriptions
|
||||||
|
- sort: Order by 'startAt' (trigger time), 'triggerTimes' (frequency), 'status'
|
||||||
|
- order: 'asc' (oldest first) or 'desc' (newest first, default)
|
||||||
|
|
||||||
|
PAGINATION:
|
||||||
|
- pageIndex: Page number starting from 0
|
||||||
|
- pageSize: Number of alerts per page (default: 10, max recommended: 50)
|
||||||
|
|
||||||
|
EXAMPLE AND COMMON USE CASES:
|
||||||
|
- Recent active alerts: alertType='single', status='firing', sort='startAt', order='desc'
|
||||||
|
- Historical analysis: alertType='single', status='resolved', pageSize=50
|
||||||
|
- Alert grouping overview: alertType='group', status='all'
|
||||||
|
- Search specific issues: search='cpu', alertType='single', status='firing'
|
||||||
|
- Find abnormal monitors: status='firing' to get active alerts indicating monitor issues
|
||||||
|
- Monitor-specific alerts: use search parameter with monitor ID or name to find related alerts
|
||||||
|
- Frequent alerts analysis: sort='triggerTimes', order='desc' to find most frequently triggered alerts
|
||||||
|
- Recent recurring issues: status='all', sort='triggerTimes', order='desc', pageSize=20
|
||||||
|
""")
|
||||||
|
public String queryAlerts(
|
||||||
|
@ToolParam(description = "Alert type: 'single' (individual alerts), 'group' (grouped alerts), 'both' (default: single)", required = false) String alertType,
|
||||||
|
@ToolParam(description = "Alert status: 'firing' (active), 'resolved' (cleared), 'all' (default: all)", required = false) String status,
|
||||||
|
@ToolParam(description = "Search term for alert content or labels", required = false) String search,
|
||||||
|
@ToolParam(description = "Sort field: 'startAt', 'triggerTimes', 'status' (default: startAt)", required = false) String sort,
|
||||||
|
@ToolParam(description = "Sort order: 'asc' or 'desc' (default: desc)", required = false) String order,
|
||||||
|
@ToolParam(description = "Page index starting from 0 (default: 0)", required = false) Integer pageIndex,
|
||||||
|
@ToolParam(description = "Page size, 1-50 recommended (default: 10)", required = false) Integer pageSize) {
|
||||||
|
|
||||||
|
try {
|
||||||
|
log.info("Querying alerts: alertType={}, status={}, search={}, sort={}, order={}", alertType, status, search, sort, order);
|
||||||
|
SubjectSum subjectSum = McpContextHolder.getSubject();
|
||||||
|
log.debug("Current subject in query_alerts tool: {}", subjectSum);
|
||||||
|
|
||||||
|
// Set defaults
|
||||||
|
if (alertType == null || alertType.trim().isEmpty()) {
|
||||||
|
alertType = "single";
|
||||||
|
}
|
||||||
|
if (status == null || status.trim().isEmpty()) {
|
||||||
|
status = "all";
|
||||||
|
}
|
||||||
|
if (sort == null || sort.trim().isEmpty()) {
|
||||||
|
sort = "startAt";
|
||||||
|
}
|
||||||
|
if (order == null || order.trim().isEmpty()) {
|
||||||
|
order = "desc";
|
||||||
|
}
|
||||||
|
if (pageIndex == null) {
|
||||||
|
pageIndex = 0;
|
||||||
|
}
|
||||||
|
if (pageSize == null) {
|
||||||
|
pageSize = 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
StringBuilder response = new StringBuilder();
|
||||||
|
response.append("ALERT QUERY RESULTS\n");
|
||||||
|
response.append("===================\n\n");
|
||||||
|
|
||||||
|
// Handle different alert types
|
||||||
|
if ("single".equalsIgnoreCase(alertType) || "both".equalsIgnoreCase(alertType)) {
|
||||||
|
Page<SingleAlert> singleResult = alertService.getSingleAlerts(status, search, sort, order, pageIndex, pageSize);
|
||||||
|
|
||||||
|
response.append("SINGLE ALERTS:\n");
|
||||||
|
response.append("Found ").append(singleResult.getContent().size()).append(" single alerts (Total: ").append(singleResult.getTotalElements()).append("):\n\n");
|
||||||
|
|
||||||
|
for (SingleAlert alert : singleResult.getContent()) {
|
||||||
|
response.append("Alert ID: ").append(alert.getId()).append("\n");
|
||||||
|
response.append("Status: ").append(alert.getStatus()).append("\n");
|
||||||
|
response.append("Content: ").append(alert.getContent() != null ? alert.getContent() : "No content").append("\n");
|
||||||
|
response.append("Trigger Times: ").append(alert.getTriggerTimes()).append("\n");
|
||||||
|
|
||||||
|
if (alert.getStartAt() != null) {
|
||||||
|
response.append("Started At: ").append(UtilityClass.formatTimestamp(alert.getStartAt())).append("\n");
|
||||||
|
}
|
||||||
|
if (alert.getActiveAt() != null) {
|
||||||
|
response.append("Active At: ").append(UtilityClass.formatTimestamp(alert.getActiveAt())).append("\n");
|
||||||
|
}
|
||||||
|
if (alert.getEndAt() != null) {
|
||||||
|
response.append("Ended At: ").append(UtilityClass.formatTimestamp(alert.getEndAt())).append("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (alert.getLabels() != null && !alert.getLabels().isEmpty()) {
|
||||||
|
response.append("Labels: ").append(alert.getLabels()).append("\n");
|
||||||
|
}
|
||||||
|
response.append("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (singleResult.getContent().isEmpty()) {
|
||||||
|
response.append("No single alerts found matching the specified criteria.\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle group alerts
|
||||||
|
if ("group".equalsIgnoreCase(alertType) || "both".equalsIgnoreCase(alertType)) {
|
||||||
|
if ("both".equalsIgnoreCase(alertType)) {
|
||||||
|
response.append("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
Page<GroupAlert> groupResult = alertService.getGroupAlerts(status, search, sort, order, pageIndex, pageSize);
|
||||||
|
|
||||||
|
response.append("GROUP ALERTS:\n");
|
||||||
|
response.append("Found ").append(groupResult.getContent().size()).append(" group alerts (Total: ").append(groupResult.getTotalElements()).append("):\n\n");
|
||||||
|
|
||||||
|
for (GroupAlert alert : groupResult.getContent()) {
|
||||||
|
response.append("Group Alert ID: ").append(alert.getId()).append("\n");
|
||||||
|
response.append("Status: ").append(alert.getStatus()).append("\n");
|
||||||
|
response.append("Group Key: ").append(alert.getGroupKey() != null ? alert.getGroupKey() : "No group key").append("\n");
|
||||||
|
|
||||||
|
if (alert.getGmtCreate() != null) {
|
||||||
|
response.append("Created At: ").append(alert.getGmtCreate()).append("\n");
|
||||||
|
}
|
||||||
|
if (alert.getGmtUpdate() != null) {
|
||||||
|
response.append("Updated At: ").append(alert.getGmtUpdate()).append("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (alert.getCommonLabels() != null && !alert.getCommonLabels().isEmpty()) {
|
||||||
|
response.append("Common Labels: ").append(alert.getCommonLabels()).append("\n");
|
||||||
|
}
|
||||||
|
if (alert.getCommonAnnotations() != null && !alert.getCommonAnnotations().isEmpty()) {
|
||||||
|
response.append("Annotations: ").append(alert.getCommonAnnotations()).append("\n");
|
||||||
|
}
|
||||||
|
response.append("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (groupResult.getContent().isEmpty()) {
|
||||||
|
response.append("No group alerts found matching the specified criteria.\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.toString();
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Failed to query alerts: {}", e.getMessage(), e);
|
||||||
|
return "Error retrieving alerts: " + e.getMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Tool(name = "get_alerts_summary", description = """
|
||||||
|
Get alerts summary statistics including total counts, status distribution, and recent trends.
|
||||||
|
Returns comprehensive overview of the current alerting status across all monitors.
|
||||||
|
""")
|
||||||
|
public String getAlertsSummary() {
|
||||||
|
try {
|
||||||
|
log.info("Getting alerts summary");
|
||||||
|
SubjectSum subjectSum = McpContextHolder.getSubject();
|
||||||
|
log.debug("Current subject in get_alerts_summary tool: {}", subjectSum);
|
||||||
|
|
||||||
|
AlertSummary summary = alertService.getAlertsSummary();
|
||||||
|
|
||||||
|
StringBuilder response = new StringBuilder();
|
||||||
|
response.append("ALERTS SUMMARY\n");
|
||||||
|
response.append("==============\n\n");
|
||||||
|
|
||||||
|
if (summary != null) {
|
||||||
|
response.append("Total Alerts: ").append(summary.getTotal()).append("\n");
|
||||||
|
response.append("Handled Alerts: ").append(summary.getDealNum()).append("\n");
|
||||||
|
response.append("Handling Rate: ").append(String.format("%.1f", summary.getRate())).append("%\n\n");
|
||||||
|
|
||||||
|
response.append("Priority Breakdown (Unhandled):\n");
|
||||||
|
response.append("- Critical: ").append(summary.getPriorityCriticalNum()).append("\n");
|
||||||
|
response.append("- Emergency: ").append(summary.getPriorityEmergencyNum()).append("\n");
|
||||||
|
response.append("- Warning: ").append(summary.getPriorityWarningNum()).append("\n\n");
|
||||||
|
|
||||||
|
long totalUnhandled = summary.getPriorityCriticalNum() + summary.getPriorityEmergencyNum() + summary.getPriorityWarningNum();
|
||||||
|
response.append("Total Unhandled Alerts: ").append(totalUnhandled).append("\n");
|
||||||
|
|
||||||
|
if (totalUnhandled > 0) {
|
||||||
|
response.append("\nUnhandled Alert Distribution:\n");
|
||||||
|
response.append("- Critical: ").append(String.format("%.1f", (summary.getPriorityCriticalNum() * 100.0 / totalUnhandled))).append("%\n");
|
||||||
|
response.append("- Emergency: ").append(String.format("%.1f", (summary.getPriorityEmergencyNum() * 100.0 / totalUnhandled))).append("%\n");
|
||||||
|
response.append("- Warning: ").append(String.format("%.1f", (summary.getPriorityWarningNum() * 100.0 / totalUnhandled))).append("%\n");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
response.append("No alert summary data available.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.toString();
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Failed to get alerts summary: {}", e.getMessage(), e);
|
||||||
|
return "Error retrieving alerts summary: " + e.getMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,262 @@
|
|||||||
|
/*
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||||
|
* contributor license agreements. See the NOTICE file distributed with
|
||||||
|
* this work for additional information regarding copyright ownership.
|
||||||
|
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||||
|
* (the "License"); you may not use this file except in compliance with
|
||||||
|
* the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
package org.apache.hertzbeat.ai.tools.impl;
|
||||||
|
|
||||||
|
import com.usthe.sureness.subject.SubjectSum;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.hertzbeat.ai.config.McpContextHolder;
|
||||||
|
import org.apache.hertzbeat.warehouse.service.MetricsDataService;
|
||||||
|
import org.apache.hertzbeat.ai.tools.MetricsTools;
|
||||||
|
import org.apache.hertzbeat.common.entity.dto.Field;
|
||||||
|
import org.apache.hertzbeat.common.entity.dto.MetricsData;
|
||||||
|
import org.apache.hertzbeat.common.entity.dto.MetricsHistoryData;
|
||||||
|
import org.apache.hertzbeat.common.entity.dto.Value;
|
||||||
|
import org.apache.hertzbeat.common.entity.dto.ValueRow;
|
||||||
|
import org.springframework.ai.tool.annotation.Tool;
|
||||||
|
import org.springframework.ai.tool.annotation.ToolParam;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implementation of Metrics Tools functionality for metrics data queries and analysis
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
public class MetricsToolsImpl implements MetricsTools {
|
||||||
|
@Autowired
|
||||||
|
private MetricsDataService metricsDataService;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Tool(name = "query_realtime_metrics", description = """
|
||||||
|
Get the supported monitor types/names from the list_monitor_types tool, make sure to use right name in the next call
|
||||||
|
Use the query_monitors tool to find monitor IDs in case the user does not tell the id explicitly. You might have to use this multiple times based on the user's query
|
||||||
|
Get real-time metrics data for a specific monitor.
|
||||||
|
Returns current metrics values including CPU, memory, disk usage, etc.
|
||||||
|
Based on the monitor type/name, use the get_apps_metrics_hierarchy tool to get the metrics hierarchy. i.e., metrics and the field parameter (sub-metric).
|
||||||
|
Each metric has its submetrics as well for example: cpu has field parameters or sub-metrics like 'usage', 'load', 'core'. These value might be numeric or string
|
||||||
|
User might ask about specific metrics like 'cpu usage', 'memory used', 'disk available', etc.
|
||||||
|
So use this tool to get the real-time metrics data for the monitor- user asked the specific metrics for, along with any logical or numeric conditions if the user mentions
|
||||||
|
In case of multiple monitors matching the user's description, you might have to call query_realtime_metrics tool mutliple times with different parameters.
|
||||||
|
|
||||||
|
|
||||||
|
EXAMPLE WORKFLOW
|
||||||
|
|
||||||
|
If the user asks for 'cpu usage' for a particular monitor or multiple matching monitors like 'web server', 'database server', etc.
|
||||||
|
Get the closest matching monitor name from list_monitor_types tool.
|
||||||
|
Call query_monitors tool to get the monitor ID/IDs with the obtained monitor type
|
||||||
|
Call get_apps_metrics_hierarchy tool to get all the metrics (type=metric) for the obtained monitor type
|
||||||
|
Call query_realtime_metrics tool with each of the IDs and the closest matching metrics name (e.g., 'cpu', 'memory', etc.)
|
||||||
|
From the result, do whatever operation user wants you to do with the data. In this example case display the cpu usage for each matching monitor.
|
||||||
|
""")
|
||||||
|
public String getRealtimeMetrics(
|
||||||
|
@ToolParam(description = "Monitor ID", required = true) Long monitorId,
|
||||||
|
@ToolParam(description = "Metrics name (e.g., 'system', 'cpu', 'memory') obtained from get_apps_metrics_hierarchy result", required = true) String metrics) {
|
||||||
|
try {
|
||||||
|
log.info("Getting real-time metrics for monitor {} and metrics {}", monitorId, metrics);
|
||||||
|
SubjectSum subjectSum = McpContextHolder.getSubject();
|
||||||
|
log.debug("Current subject in get_realtime_metrics tool: {}", subjectSum);
|
||||||
|
|
||||||
|
MetricsData metricsData = metricsDataService.getMetricsData(monitorId, metrics);
|
||||||
|
|
||||||
|
if (metricsData == null) {
|
||||||
|
return String.format("No real-time metrics data found for monitor ID %d and metrics '%s'", monitorId, metrics);
|
||||||
|
}
|
||||||
|
|
||||||
|
StringBuilder response = new StringBuilder();
|
||||||
|
response.append("REAL-TIME METRICS DATA\n");
|
||||||
|
response.append("=".repeat(50)).append("\n");
|
||||||
|
response.append("Monitor ID: ").append(monitorId).append("\n");
|
||||||
|
response.append("Metrics: ").append(metrics).append("\n");
|
||||||
|
response.append("=".repeat(50)).append("\n\n");
|
||||||
|
|
||||||
|
if (metricsData.getValueRows() != null && !metricsData.getValueRows().isEmpty()) {
|
||||||
|
List<Field> fields = metricsData.getFields();
|
||||||
|
|
||||||
|
response.append("Available Field Parameters (Sub-metrics):\n");
|
||||||
|
response.append("-".repeat(40)).append("\n");
|
||||||
|
|
||||||
|
for (ValueRow valueRow : metricsData.getValueRows()) {
|
||||||
|
// Show labels if available
|
||||||
|
if (valueRow.getLabels() != null && !valueRow.getLabels().isEmpty()) {
|
||||||
|
response.append("Instance Labels: ").append(valueRow.getLabels()).append("\n");
|
||||||
|
response.append("-".repeat(20)).append("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Value> values = valueRow.getValues();
|
||||||
|
for (int i = 0; i < values.size() && i < fields.size(); i++) {
|
||||||
|
Field field = fields.get(i);
|
||||||
|
Value value = values.get(i);
|
||||||
|
|
||||||
|
// Enhanced field information display
|
||||||
|
response.append("• Field Parameter: ").append(field.getName());
|
||||||
|
if (field.getUnit() != null && !field.getUnit().isEmpty()) {
|
||||||
|
response.append(" (").append(field.getUnit()).append(")");
|
||||||
|
}
|
||||||
|
response.append("\n");
|
||||||
|
|
||||||
|
response.append(" Current Value: ").append(value.getOrigin());
|
||||||
|
|
||||||
|
// Add data type indication
|
||||||
|
try {
|
||||||
|
Double.parseDouble(value.getOrigin());
|
||||||
|
response.append(" [Numeric]");
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
response.append(" [String]");
|
||||||
|
}
|
||||||
|
response.append("\n\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (valueRow.getLabels() != null && !valueRow.getLabels().isEmpty()) {
|
||||||
|
response.append("-".repeat(20)).append("\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
response.append("No field parameter data available for metrics '").append(metrics).append("'.\n");
|
||||||
|
response.append("Use get_apps_metrics_hierarchy tool to check available metrics for this monitor type.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.toString();
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Failed to get real-time metrics: {}", e.getMessage(), e);
|
||||||
|
return "Error retrieving real-time metrics: " + e.getMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Tool(name = "get_historical_metrics", description = """
|
||||||
|
Get historical metrics data for analysis and trending.
|
||||||
|
Returns time-series data for specified metrics over a time range.
|
||||||
|
Use the query_monitors tool to find the correct monitor IDs/ name or type for the monitor(s) user asked the metrics for
|
||||||
|
Pass that name into the get_apps_metrics_hierarchy tool to get the metrics hierarchy i.e metrics and the field paramater
|
||||||
|
DO NOT USE THE LABEL FIELD ALWAYS USE THE VALUE FIELD FROM THE HIERARCHY JSON
|
||||||
|
Ask user to provide the filters for labels, history and interval aggregation
|
||||||
|
""")
|
||||||
|
public String getHistoricalMetrics(
|
||||||
|
@ToolParam(description = "Monitor ID", required = true) Long monitorId,
|
||||||
|
@ToolParam(description = "Monitor type (e.g., 'linux', 'mysql', 'http')", required = true) String app,
|
||||||
|
@ToolParam(description = "Metrics name (e.g., 'target', 'cpu', 'memory')", required = true) String metrics,
|
||||||
|
@ToolParam(description = "Field Parameter (e.g., 'usage', 'used', 'available')", required = false) String fieldParameter,
|
||||||
|
@ToolParam(description = "Label filter for specific instances", required = false) String label,
|
||||||
|
@ToolParam(description = "Time range (e.g., '1h', '6h', '24h', '7d')", required = false) String history,
|
||||||
|
@ToolParam(description = "Whether to aggregate data with intervals", required = false) Boolean interval) {
|
||||||
|
|
||||||
|
try {
|
||||||
|
log.info("Getting historical metrics for monitor {} and metrics {}", monitorId, metrics);
|
||||||
|
|
||||||
|
if (history == null || history.trim().isEmpty()) {
|
||||||
|
history = "24h";
|
||||||
|
}
|
||||||
|
if (interval == null) {
|
||||||
|
interval = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
MetricsHistoryData historyData = metricsDataService.getMetricHistoryData(
|
||||||
|
monitorId, app, metrics, fieldParameter, label, history, interval);
|
||||||
|
|
||||||
|
if (historyData == null) {
|
||||||
|
return String.format("No historical metrics data found for monitor ID %d and metrics '%s'", monitorId, metrics);
|
||||||
|
}
|
||||||
|
|
||||||
|
StringBuilder response = new StringBuilder();
|
||||||
|
response.append("HISTORICAL METRICS: ").append(metrics).append(" (Monitor ID: ").append(monitorId).append(")\n");
|
||||||
|
response.append("Time Range: ").append(history).append(" | Interval Aggregation: ").append(interval).append("\n");
|
||||||
|
response.append("=".repeat(60)).append("\n\n");
|
||||||
|
|
||||||
|
if (historyData.getValues() != null && !historyData.getValues().isEmpty()) {
|
||||||
|
response.append("Field: ").append(historyData.getField() != null ? historyData.getField().getName() : "Unknown").append("\n");
|
||||||
|
|
||||||
|
// Calculate total data points
|
||||||
|
int totalPoints = historyData.getValues().values().stream()
|
||||||
|
.mapToInt(List::size)
|
||||||
|
.sum();
|
||||||
|
response.append("Data Points: ").append(totalPoints).append("\n");
|
||||||
|
|
||||||
|
// Show sample data points (first 10) from all value lists
|
||||||
|
int count = 0;
|
||||||
|
response.append("\nSample Data Points:\n");
|
||||||
|
for (Map.Entry<String, List<Value>> entry : historyData.getValues().entrySet()) {
|
||||||
|
String labelKey = entry.getKey();
|
||||||
|
List<Value> values = entry.getValue();
|
||||||
|
|
||||||
|
for (Value value : values) {
|
||||||
|
if (count >= 10) break;
|
||||||
|
response.append("Label: ").append(labelKey)
|
||||||
|
.append(" | Time: ").append(value.getTime())
|
||||||
|
.append(" | Value: ").append(value.getOrigin()).append("\n");
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
if (count >= 10) break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (totalPoints > 10) {
|
||||||
|
response.append("... and ").append(totalPoints - 10).append(" more data points\n");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
response.append("No historical data points available.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.toString();
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Failed to get historical metrics: {}", e.getMessage(), e);
|
||||||
|
return "Error retrieving historical metrics: " + e.getMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Tool(name = "get_warehouse_status", description = """
|
||||||
|
Check the status of the metrics storage warehouse system.
|
||||||
|
Returns whether the metrics storage is operational and accessible.
|
||||||
|
""")
|
||||||
|
public String getWarehouseStatus() {
|
||||||
|
try {
|
||||||
|
log.info("Checking warehouse storage status");
|
||||||
|
SubjectSum subjectSum = McpContextHolder.getSubject();
|
||||||
|
log.debug("Current security subject for getWarehouseStorageServerStatus: {}", subjectSum);
|
||||||
|
|
||||||
|
Boolean status = metricsDataService.getWarehouseStorageServerStatus();
|
||||||
|
|
||||||
|
StringBuilder response = new StringBuilder();
|
||||||
|
response.append("METRICS WAREHOUSE STATUS\n");
|
||||||
|
response.append("========================\n\n");
|
||||||
|
|
||||||
|
if (status != null && status) {
|
||||||
|
response.append("Status: ONLINE ✓\n");
|
||||||
|
response.append("The metrics storage warehouse is operational and accessible.\n");
|
||||||
|
response.append("Historical metrics data queries are available.");
|
||||||
|
} else {
|
||||||
|
response.append("Status: OFFLINE ✗\n");
|
||||||
|
response.append("The metrics storage warehouse is not accessible.\n");
|
||||||
|
response.append("Only real-time metrics may be available.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.toString();
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Failed to get warehouse status: {}", e.getMessage(), e);
|
||||||
|
return "Error checking warehouse status: " + e.getMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,505 @@
|
|||||||
|
/*
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||||
|
* contributor license agreements. See the NOTICE file distributed with
|
||||||
|
* this work for additional information regarding copyright ownership.
|
||||||
|
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||||
|
* (the "License"); you may not use this file except in compliance with
|
||||||
|
* the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.apache.hertzbeat.ai.tools.impl;
|
||||||
|
|
||||||
|
import com.usthe.sureness.subject.SubjectSum;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.hertzbeat.ai.config.McpContextHolder;
|
||||||
|
import org.apache.hertzbeat.manager.service.MonitorService;
|
||||||
|
import org.apache.hertzbeat.manager.service.AppService;
|
||||||
|
import org.apache.hertzbeat.ai.utils.UtilityClass;
|
||||||
|
import org.springframework.ai.tool.annotation.Tool;
|
||||||
|
import org.springframework.data.domain.Page;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.apache.hertzbeat.ai.tools.MonitorTools;
|
||||||
|
import org.springframework.ai.tool.annotation.ToolParam;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.apache.hertzbeat.common.entity.manager.Monitor;
|
||||||
|
import org.apache.hertzbeat.common.entity.manager.Param;
|
||||||
|
import org.apache.hertzbeat.common.entity.manager.ParamDefine;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implementation of Monitoring Tools functionality
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
public class MonitorToolsImpl implements MonitorTools {
|
||||||
|
@Autowired
|
||||||
|
private MonitorService monitorService;
|
||||||
|
@Autowired
|
||||||
|
private AppService appService;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tool to query monitor information with flexible filtering and pagination.
|
||||||
|
* Supports filtering by monitor IDs, type, status, host, labels, sorting, and
|
||||||
|
* pagination.
|
||||||
|
* Returns detailed monitor information including ID, name, type, host, and status.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
@Tool(name = "query_monitors", description = """
|
||||||
|
Query Existing/configured monitors in HertzBeat.
|
||||||
|
This tool retrieves monitors based on various filters and parameters.
|
||||||
|
Comprehensive monitor querying with flexible filtering, pagination, and specialized views.
|
||||||
|
|
||||||
|
MONITOR STATUSES:
|
||||||
|
- status=1: Online/Active monitors (healthy, responding normally)
|
||||||
|
- status=2: Offline monitors (not responding, connection failed)
|
||||||
|
- status=3: Unreachable monitors (network/connectivity issues)
|
||||||
|
- status=0: Paused monitors (manually disabled/suspended)
|
||||||
|
- status=9 or null: All monitors regardless of status (default)
|
||||||
|
|
||||||
|
COMMON USE CASES & PARAMETER COMBINATIONS:
|
||||||
|
|
||||||
|
1. BASIC MONITOR LISTING:
|
||||||
|
- Default: No parameters (shows all monitors, 8 per page)
|
||||||
|
- By type: app='linux' (show only Linux monitors)
|
||||||
|
- Search: search='web' (find monitors with 'web' in name/host)
|
||||||
|
|
||||||
|
2. STATUS-BASED QUERIES:
|
||||||
|
- Healthy monitors: status=1, pageSize=50
|
||||||
|
- Problem monitors: status=2 or status=3, pageSize=50
|
||||||
|
- Offline monitors only: status=2
|
||||||
|
- Unreachable monitors only: status=3
|
||||||
|
- Paused monitors: status=0
|
||||||
|
|
||||||
|
3. MONITORING HEALTH OVERVIEW:
|
||||||
|
- All statuses with statistics: status=9, includeStats=true, pageSize=100
|
||||||
|
- Unhealthy monitors: Pass both status=2 AND status=3 (make 2 separate calls)
|
||||||
|
|
||||||
|
4. ADVANCED FILTERING:
|
||||||
|
- Specific monitor types: app='mysql', status=1 (healthy MySQL monitors)
|
||||||
|
- Label-based: labels='env:prod,critical:true'
|
||||||
|
- Host search: search='192.168' (find by IP pattern)
|
||||||
|
- Monitor IDs: ids=[1,2,3] (specific monitors by ID)
|
||||||
|
|
||||||
|
5. SORTING & PAGINATION:
|
||||||
|
- Recently updated: sort='gmtUpdate', order='desc'
|
||||||
|
- Alphabetical: sort='name', order='asc'
|
||||||
|
- By creation: sort='gmtCreate', order='desc' (newest first)
|
||||||
|
- Large datasets: pageSize=50-100 for bulk operations
|
||||||
|
|
||||||
|
RESPONSE FORMAT:
|
||||||
|
- includeStats=true: Adds status distribution summary at top
|
||||||
|
- Default: Simple list with ID, name, type, host, status
|
||||||
|
- Shows total count and pagination info
|
||||||
|
""")
|
||||||
|
public String queryMonitors(
|
||||||
|
@ToolParam(description = "Specific monitor IDs to retrieve (optional)", required = false) List<Long> ids,
|
||||||
|
@ToolParam(description = "Monitor type filter: 'linux', 'mysql', 'http', 'redis', etc. (optional)", required = false) String app,
|
||||||
|
@ToolParam(description = "Monitor status: 1=online, 2=offline, 3=unreachable, 0=paused, 9=all (default: 9)", required = false) Byte status,
|
||||||
|
@ToolParam(description = "Search in monitor names or hosts (partial matching)", required = false) String search,
|
||||||
|
@ToolParam(description = "Label filters, format: 'key1:value1,key2:value2'", required = false) String labels,
|
||||||
|
@ToolParam(description = "Sort field: 'name', 'gmtCreate', 'gmtUpdate', 'status', 'app' (default: gmtCreate)", required = false) String sort,
|
||||||
|
@ToolParam(description = "Sort order: 'asc' (ascending) or 'desc' (descending, default)", required = false) String order,
|
||||||
|
@ToolParam(description = "Page number starting from 0 (default: 0)", required = false) Integer pageIndex,
|
||||||
|
@ToolParam(description = "Items per page: 1-100 recommended (default: 20)", required = false) Integer pageSize,
|
||||||
|
@ToolParam(description = "Include status statistics summary (default: false)", required = false) Boolean includeStats) {
|
||||||
|
try {
|
||||||
|
// Set defaults
|
||||||
|
if (pageSize == null || pageSize <= 0) {
|
||||||
|
pageSize = 20;
|
||||||
|
}
|
||||||
|
if (pageIndex == null) {
|
||||||
|
pageIndex = 0;
|
||||||
|
}
|
||||||
|
if (includeStats == null) {
|
||||||
|
includeStats = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
SubjectSum subjectSum = McpContextHolder.getSubject();
|
||||||
|
log.debug("Current security subject: {}", subjectSum);
|
||||||
|
|
||||||
|
Page<Monitor> result = monitorService.getMonitors(
|
||||||
|
ids, app, search, status, sort, order, pageIndex, pageSize, labels);
|
||||||
|
log.debug("MonitorService.getMonitors result: {}", result);
|
||||||
|
|
||||||
|
StringBuilder response = new StringBuilder();
|
||||||
|
response.append("MONITOR QUERY RESULTS\n");
|
||||||
|
response.append("====================\n\n");
|
||||||
|
|
||||||
|
// Include statistics if requested
|
||||||
|
if (includeStats) {
|
||||||
|
// Get status distribution by calling with different status values
|
||||||
|
long onlineCount = monitorService.getMonitors(null, app, search, (byte) 1, null, null, 0, 1000, labels).getTotalElements();
|
||||||
|
long offlineCount = monitorService.getMonitors(null, app, search, (byte) 2, null, null, 0, 1000, labels).getTotalElements();
|
||||||
|
long unreachableCount = monitorService.getMonitors(null, app, search, (byte) 3, null, null, 0, 1000, labels).getTotalElements();
|
||||||
|
long pausedCount = monitorService.getMonitors(null, app, search, (byte) 0, null, null, 0, 1000, labels).getTotalElements();
|
||||||
|
|
||||||
|
response.append("STATUS OVERVIEW:\n");
|
||||||
|
response.append("- Online: ").append(onlineCount).append("\n");
|
||||||
|
response.append("- Offline: ").append(offlineCount).append("\n");
|
||||||
|
response.append("- Unreachable: ").append(unreachableCount).append("\n");
|
||||||
|
response.append("- Paused: ").append(pausedCount).append("\n");
|
||||||
|
|
||||||
|
long total = onlineCount + offlineCount + unreachableCount + pausedCount;
|
||||||
|
if (total > 0) {
|
||||||
|
double healthPercentage = (onlineCount * 100.0) / total;
|
||||||
|
response.append("- Health Rate: ").append(String.format("%.1f", healthPercentage)).append("%\n");
|
||||||
|
}
|
||||||
|
response.append("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
response.append("Query Results: ").append(result.getContent().size())
|
||||||
|
.append(" monitors (Total: ").append(result.getTotalElements()).append(")\n");
|
||||||
|
|
||||||
|
if (result.getTotalPages() > 1) {
|
||||||
|
response.append("Page ").append(pageIndex + 1).append(" of ").append(result.getTotalPages()).append("\n");
|
||||||
|
}
|
||||||
|
response.append("\n");
|
||||||
|
|
||||||
|
for (Monitor monitor : result.getContent()) {
|
||||||
|
response.append("ID: ").append(monitor.getId())
|
||||||
|
.append(" | Name: ").append(monitor.getName())
|
||||||
|
.append(" | Type: ").append(monitor.getApp())
|
||||||
|
.append(" | Host: ").append(monitor.getHost())
|
||||||
|
.append(" | Status: ").append(UtilityClass.getStatusText(monitor.getStatus()));
|
||||||
|
|
||||||
|
// Add creation date for better context
|
||||||
|
if (monitor.getGmtCreate() != null) {
|
||||||
|
response.append(" | Created: ").append(monitor.getGmtCreate().toString(), 0, 10);
|
||||||
|
}
|
||||||
|
response.append("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.getContent().isEmpty()) {
|
||||||
|
response.append("No monitors found matching the specified criteria.\n");
|
||||||
|
response.append("Try adjusting your filters or search terms.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.toString();
|
||||||
|
} catch (Exception e) {
|
||||||
|
return "Error retrieving monitors: " + e.getMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Tool(name = "add_monitor", description = """
|
||||||
|
Add a new monitoring target to HertzBeat with comprehensive configuration.
|
||||||
|
This tool dynamically handles different parameter requirements for each monitor type.
|
||||||
|
|
||||||
|
This tool creates monitors with proper app-specific parameters.
|
||||||
|
|
||||||
|
*********
|
||||||
|
VERY IMPORTANT:
|
||||||
|
ALWAYS use get_monitor_additional_params to check the additional required parameters for the chosen type before adding a monitor or even mentioning it.
|
||||||
|
Use list_monitor_types tool to see available monitor type names to use here in the app parameter.
|
||||||
|
Use the information obtained from this to query user for parameters.
|
||||||
|
If the User has not given any parameters, ask them to provide the necessary parameters, until all the necessary parameters are provided.
|
||||||
|
**********
|
||||||
|
|
||||||
|
Examples of natural language requests this tool handles:
|
||||||
|
- "Monitor website example.com with HTTPS on port 443"
|
||||||
|
- "Add MySQL monitoring for database server at 192.168.1.10 with user admin"
|
||||||
|
- "Monitor Linux server health on host server.company.com via SSH"
|
||||||
|
- "Set up Redis monitoring on localhost port 6379 with password"
|
||||||
|
|
||||||
|
PARAMETER MAPPING: The tool intelligently maps common parameters:
|
||||||
|
- host: Target server/domain
|
||||||
|
- port: Service port (auto-detected if not specified)
|
||||||
|
- username: Authentication username
|
||||||
|
- password: Authentication password
|
||||||
|
- database: Database name (for DB monitors)
|
||||||
|
- additionalParams: JSON string for app-specific parameters (to be obtained from get_monitor_param_defines)
|
||||||
|
|
||||||
|
ADDITIONAL PARAMETERS EXAMPLES:
|
||||||
|
- Website: {"uri":"/api/health", "ssl":"true", "method":"POST"}
|
||||||
|
- Linux: {"privateKey":"ssh-key-content", "script":"custom-script"}
|
||||||
|
- Database: {"url":"jdbc:mysql://custom", "timeout":"10000"}
|
||||||
|
""")
|
||||||
|
public String addMonitor(
|
||||||
|
@ToolParam(description = "Monitor name (required)", required = true) String name,
|
||||||
|
@ToolParam(description = "Monitor type: website, mysql, postgresql, redis, linux, windows, etc.", required = true) String app,
|
||||||
|
@ToolParam(description = "Target host: IP address or domain name", required = true) String host,
|
||||||
|
@ToolParam(description = "Target port (optional, auto-detected if not specified)", required = false) Integer port,
|
||||||
|
@ToolParam(description = "Collection interval in seconds (default: 600)", required = false) Integer intervals,
|
||||||
|
@ToolParam(description = "Username for authentication (optional)", required = false) String username,
|
||||||
|
@ToolParam(description = "Password for authentication (optional)", required = false) String password,
|
||||||
|
@ToolParam(description = "Database name (for database monitors)", required = false) String database,
|
||||||
|
@ToolParam(description = "Additional app-specific parameters as JSON: {\"uri\":\"/api\", \"ssl\":\"true\", \"method\":\"POST\"}", required = false) String additionalParams,
|
||||||
|
@ToolParam(description = "Monitor description (optional)", required = false) String description) {
|
||||||
|
|
||||||
|
try {
|
||||||
|
log.info("Adding monitor: name={}, app={}, host={}", name, app, host);
|
||||||
|
|
||||||
|
// Validate required parameters
|
||||||
|
if (name == null || name.trim().isEmpty()) {
|
||||||
|
return "Error: Monitor name is required";
|
||||||
|
}
|
||||||
|
if (app == null || app.trim().isEmpty()) {
|
||||||
|
return "Error: Monitor type/application is required";
|
||||||
|
}
|
||||||
|
if (host == null || host.trim().isEmpty()) {
|
||||||
|
return "Error: Host is required";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set defaults
|
||||||
|
if (intervals == null || intervals < 10) {
|
||||||
|
intervals = 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create Monitor entity
|
||||||
|
Monitor monitor = Monitor.builder()
|
||||||
|
.name(name.trim())
|
||||||
|
.app(app.toLowerCase().trim())
|
||||||
|
.host(host.trim())
|
||||||
|
.intervals(intervals)
|
||||||
|
.status((byte) 1)
|
||||||
|
.type((byte) 0)
|
||||||
|
.description(description != null ? description.trim() : "")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
List<Param> params = createBasicParams(host, port, username, password, database, additionalParams);
|
||||||
|
|
||||||
|
// Validate that all required parameters for this monitor type are provided
|
||||||
|
try {
|
||||||
|
SubjectSum subjectSum = McpContextHolder.getSubject();
|
||||||
|
log.debug("Current security subject for getMonitorParamDefines: {}", subjectSum);
|
||||||
|
|
||||||
|
List<ParamDefine> requiredParams = appService.getAppParamDefines(app.toLowerCase().trim());
|
||||||
|
log.info("Checking required parameters for monitor type '{}': {}", app, requiredParams);
|
||||||
|
List<String> missingParams = new ArrayList<>();
|
||||||
|
|
||||||
|
for (ParamDefine paramDefine : requiredParams) {
|
||||||
|
if (paramDefine.isRequired()) {
|
||||||
|
String fieldName = paramDefine.getField();
|
||||||
|
boolean hasParam = params.stream()
|
||||||
|
.anyMatch(param -> fieldName.equals(param.getField()));
|
||||||
|
if (!hasParam) {
|
||||||
|
missingParams.add(fieldName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!missingParams.isEmpty()) {
|
||||||
|
return String.format("Error: Missing required parameters for monitor type '%s': %s. "
|
||||||
|
+ "Use get_monitor_additional_params tool to see all required parameters.",
|
||||||
|
app, String.join(", ", missingParams));
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("Could not validate required parameters for monitor type '{}': {}", app, e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call monitor service - it handles all the complexity (validation, defaults, app-specific logic)
|
||||||
|
SubjectSum subjectSum = McpContextHolder.getSubject();
|
||||||
|
log.debug("Current security subject for addMonitor: {}", subjectSum);
|
||||||
|
|
||||||
|
monitorService.addMonitor(monitor, params, null, null);
|
||||||
|
Long monitorId = monitor.getId();
|
||||||
|
|
||||||
|
log.info("Successfully added monitor '{}' with ID: {}", name, monitorId);
|
||||||
|
return String.format("Successfully added %s monitor '%s' with ID: %d (Host: %s, Interval: %d seconds)",
|
||||||
|
app.toUpperCase(), name, monitorId, host, intervals);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Failed to add monitor '{}': {}", name, e.getMessage(), e);
|
||||||
|
return "Error adding monitor '" + name + "': " + e.getMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create basic parameter list from user inputs
|
||||||
|
*/
|
||||||
|
private List<Param> createBasicParams(String host, Integer port, String username,
|
||||||
|
String password, String database, String additionalParams) {
|
||||||
|
List<Param> params = new ArrayList<>();
|
||||||
|
|
||||||
|
// Add host (always required)
|
||||||
|
params.add(Param.builder().field("host").paramValue(host.trim()).type((byte) 1).build());
|
||||||
|
|
||||||
|
// Add optional common parameters
|
||||||
|
if (port != null) {
|
||||||
|
params.add(Param.builder().field("port").paramValue(port.toString()).type((byte) 0).build());
|
||||||
|
}
|
||||||
|
if (username != null && !username.trim().isEmpty()) {
|
||||||
|
params.add(Param.builder().field("username").paramValue(username.trim()).type((byte) 1).build());
|
||||||
|
}
|
||||||
|
if (password != null && !password.trim().isEmpty()) {
|
||||||
|
params.add(Param.builder().field("password").paramValue(password.trim()).type((byte) 2).build());
|
||||||
|
}
|
||||||
|
if (database != null && !database.trim().isEmpty()) {
|
||||||
|
params.add(Param.builder().field("database").paramValue(database.trim()).type((byte) 1).build());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse additional parameters if provided
|
||||||
|
if (additionalParams != null && !additionalParams.trim().isEmpty()) {
|
||||||
|
try {
|
||||||
|
String cleaned = additionalParams.trim().replaceAll("[{}]", "");
|
||||||
|
String[] pairs = cleaned.split(",");
|
||||||
|
for (String pair : pairs) {
|
||||||
|
String[] keyValue = pair.split(":");
|
||||||
|
if (keyValue.length == 2) {
|
||||||
|
String key = keyValue[0].trim().replaceAll("\"", "");
|
||||||
|
String value = keyValue[1].trim().replaceAll("\"", "");
|
||||||
|
params.add(Param.builder().field(key).paramValue(value).type((byte) 1).build());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("Failed to parse additionalParams: {}", e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return params;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Tool(name = "list_monitor_types", description = """
|
||||||
|
List all available monitor types that can be added to HertzBeat.
|
||||||
|
This tool shows all supported monitor types with their display names.
|
||||||
|
Use this to see what types of monitors you can create with the add_monitor tool.
|
||||||
|
""")
|
||||||
|
public String listMonitorTypes(
|
||||||
|
@ToolParam(description = "Language code for localized names (en-US, zh-CN, etc.). Default: en-US", required = false) String language) {
|
||||||
|
|
||||||
|
try {
|
||||||
|
log.info("Listing available monitor types for language: {}", language);
|
||||||
|
SubjectSum subjectSum = McpContextHolder.getSubject();
|
||||||
|
log.debug("Current subject in list_monitor_types tool: {}", subjectSum);
|
||||||
|
|
||||||
|
// Set default language if not provided
|
||||||
|
if (language == null || language.trim().isEmpty()) {
|
||||||
|
language = "en-US";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get available monitor types from app service
|
||||||
|
Map<String, String> monitorTypes = appService.getI18nApps(language);
|
||||||
|
|
||||||
|
if (monitorTypes == null || monitorTypes.isEmpty()) {
|
||||||
|
return "No monitor types are currently available.";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format the response as a nice list
|
||||||
|
StringBuilder response = new StringBuilder();
|
||||||
|
response.append("Available Monitor Types (Total: ").append(monitorTypes.size()).append("):\n\n");
|
||||||
|
|
||||||
|
// Sort monitor types alphabetically by key
|
||||||
|
List<Map.Entry<String, String>> sortedTypes = monitorTypes.entrySet().stream()
|
||||||
|
.sorted(Map.Entry.comparingByKey())
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
for (Map.Entry<String, String> entry : sortedTypes) {
|
||||||
|
String typeKey = entry.getKey();
|
||||||
|
String displayName = entry.getValue();
|
||||||
|
response.append("• ").append(typeKey)
|
||||||
|
.append(" - ").append(displayName)
|
||||||
|
.append("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
response.append("\nTo add a monitor, use the add_monitor tool with one of these types as the 'app' parameter.");
|
||||||
|
|
||||||
|
log.info("Successfully listed {} monitor types", monitorTypes);
|
||||||
|
return response.toString();
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Failed to list monitor types: {}", e.getMessage(), e);
|
||||||
|
return "Error retrieving monitor types: " + e.getMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Tool(name = "get_monitor_additional_params", description = """
|
||||||
|
Get the parameter definitions required for a specific monitor type.
|
||||||
|
This tool shows what parameters are needed when adding a monitor of the specified type,
|
||||||
|
ALWAYS use this before adding a monitor to understand what parameters the user needs to provide.
|
||||||
|
Use the app parameter to specify the monitor type/application name (e.g., 'linux', 'mysql', 'redis') this can be obtained from the list_monitor_types tool.
|
||||||
|
""")
|
||||||
|
public String getMonitorAdditionalParams(
|
||||||
|
@ToolParam(description = "Monitor type/application name (e.g., 'linux', 'mysql', 'redis')", required = true) String app) {
|
||||||
|
|
||||||
|
try {
|
||||||
|
log.info("Getting parameter definitions for monitor type: {}", app);
|
||||||
|
SubjectSum subjectSum = McpContextHolder.getSubject();
|
||||||
|
log.debug("Current subject in get_monitor_param_defines tool: {}", subjectSum);
|
||||||
|
|
||||||
|
// Validate required parameter
|
||||||
|
if (app == null || app.trim().isEmpty()) {
|
||||||
|
return "Error: Monitor type/application parameter is required";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get parameter definitions from app service
|
||||||
|
List<ParamDefine> paramDefines = appService.getAppParamDefines(app.toLowerCase().trim());
|
||||||
|
|
||||||
|
if (paramDefines == null || paramDefines.isEmpty()) {
|
||||||
|
return String.format("No parameter definitions found for monitor type '%s'. "
|
||||||
|
+ "This monitor type may not exist or may not require additional parameters.", app);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format the response
|
||||||
|
StringBuilder response = new StringBuilder();
|
||||||
|
response.append(String.format("Parameter Definitions for Monitor Type '%s' (Total: %d):\n\n",
|
||||||
|
app, paramDefines.size()));
|
||||||
|
|
||||||
|
for (ParamDefine paramDefine : paramDefines) {
|
||||||
|
response.append("• Field: ").append(paramDefine.getField()).append("\n");
|
||||||
|
|
||||||
|
// Add display name if available
|
||||||
|
if (paramDefine.getName() != null && !paramDefine.getName().toString().trim().isEmpty()) {
|
||||||
|
response.append(" Name: ").append(paramDefine.getName()).append("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add type
|
||||||
|
if (paramDefine.getType() != null && !paramDefine.getType().trim().isEmpty()) {
|
||||||
|
response.append(" Type: ").append(paramDefine.getType()).append("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add required status
|
||||||
|
response.append(" Required: ").append(paramDefine.isRequired() ? "Yes" : "No").append("\n");
|
||||||
|
|
||||||
|
// Add default value if present
|
||||||
|
if (paramDefine.getDefaultValue() != null && !paramDefine.getDefaultValue().trim().isEmpty()) {
|
||||||
|
response.append(" Default: ").append(paramDefine.getDefaultValue()).append("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add validation range if present
|
||||||
|
if (paramDefine.getRange() != null && !paramDefine.getRange().trim().isEmpty()) {
|
||||||
|
response.append(" Range: ").append(paramDefine.getRange()).append("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add limit if present
|
||||||
|
if (paramDefine.getLimit() != null) {
|
||||||
|
response.append(" Limit: ").append(paramDefine.getLimit()).append("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add placeholder text if present
|
||||||
|
if (paramDefine.getPlaceholder() != null && !paramDefine.getPlaceholder().trim().isEmpty()) {
|
||||||
|
response.append(" Placeholder: ").append(paramDefine.getPlaceholder()).append("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
response.append("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
response.append("To add a monitor of this type, use the add_monitor tool with these parameters.\n");
|
||||||
|
response.append(String.format("Example: add_monitor(name='my-monitor', app='%s', host='your-host', ...)", app));
|
||||||
|
|
||||||
|
log.info("Successfully retrieved {} parameter definitions for monitor type: {}", paramDefines.size(), app);
|
||||||
|
return response.toString();
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Failed to get parameter definitions for monitor type '{}': {}", app, e.getMessage(), e);
|
||||||
|
return "Error retrieving parameter definitions for monitor type '" + app + "': " + e.getMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,495 @@
|
|||||||
|
/*
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||||
|
* contributor license agreements. See the NOTICE file distributed with
|
||||||
|
* this work for additional information regarding copyright ownership.
|
||||||
|
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||||
|
* (the "License"); you may not use this file except in compliance with
|
||||||
|
* the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.apache.hertzbeat.ai.utils;
|
||||||
|
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.fasterxml.jackson.databind.node.ArrayNode;
|
||||||
|
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.ZoneId;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import org.apache.hertzbeat.manager.pojo.dto.Hierarchy;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility class providing helper methods for field expression validation, parsing,
|
||||||
|
* hierarchy management, timestamp formatting and metric/status conversions.
|
||||||
|
* This class contains stateless utility methods used across the application for
|
||||||
|
* common operations and validations.
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@lombok.experimental.UtilityClass
|
||||||
|
public class UtilityClass {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates the syntax of field conditions expression
|
||||||
|
* @param fieldConditions Field conditions string to validate
|
||||||
|
* @return "VALID" if syntax is correct, error message otherwise
|
||||||
|
*/
|
||||||
|
|
||||||
|
public String validateExpressionSyntax(String fieldConditions) {
|
||||||
|
try {
|
||||||
|
log.debug("Validating expression syntax: {}", fieldConditions);
|
||||||
|
|
||||||
|
// Check for basic syntax requirements
|
||||||
|
if (fieldConditions == null || fieldConditions.trim().isEmpty()) {
|
||||||
|
return "Error: Field conditions cannot be empty";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for balanced parentheses
|
||||||
|
if (!hasBalancedParentheses(fieldConditions)) {
|
||||||
|
return "Error: Unbalanced parentheses in field conditions. Please check your expression syntax.";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate operators used in the expression
|
||||||
|
String operatorValidation = validateOperators(fieldConditions);
|
||||||
|
if (!operatorValidation.equals("VALID")) {
|
||||||
|
return operatorValidation;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate logical connectors
|
||||||
|
String logicalValidation = validateLogicalConnectors(fieldConditions);
|
||||||
|
if (!logicalValidation.equals("VALID")) {
|
||||||
|
return logicalValidation;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate function syntax (equals, contains, etc.)
|
||||||
|
String functionValidation = validateFunctions(fieldConditions);
|
||||||
|
if (!functionValidation.equals("VALID")) {
|
||||||
|
return functionValidation;
|
||||||
|
}
|
||||||
|
|
||||||
|
log.debug("Expression syntax validation passed for: {}", fieldConditions);
|
||||||
|
return "VALID";
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Error during expression syntax validation: {}", e.getMessage(), e);
|
||||||
|
return String.format("Error: Unable to validate expression syntax: %s", e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if parentheses are balanced in the expression
|
||||||
|
*/
|
||||||
|
public boolean hasBalancedParentheses(String expression) {
|
||||||
|
int count = 0;
|
||||||
|
for (char c : expression.toCharArray()) {
|
||||||
|
if (c == '(') {
|
||||||
|
count++;
|
||||||
|
} else if (c == ')') {
|
||||||
|
count--;
|
||||||
|
if (count < 0) {
|
||||||
|
return false; // More closing than opening
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count == 0; // Should be perfectly balanced
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates that only supported operators are used
|
||||||
|
*/
|
||||||
|
public String validateOperators(String fieldConditions) {
|
||||||
|
// Define supported operators for different field types
|
||||||
|
String[] numericOperators = {">", "<", ">=", "<=", "==", "!=", "exists()", "!exists()"};
|
||||||
|
String[] stringOperators = {"equals(", "contains(", "matches(", "exists()", "!equals(", "!contains(", "!matches(", "!exists()"};
|
||||||
|
String[] logicalOperators = {" and ", " or "};
|
||||||
|
|
||||||
|
// Remove quotes and function calls temporarily for operator checking
|
||||||
|
String tempExpression = fieldConditions
|
||||||
|
.replaceAll("\"[^\"]*\"", "VALUE") // Remove quoted strings
|
||||||
|
.replaceAll("'[^']*'", "VALUE") // Remove single quoted strings
|
||||||
|
.replaceAll("\\w+\\([^)]*\\)", "FUNCTION"); // Remove function calls
|
||||||
|
|
||||||
|
// Check for invalid operators (common mistakes)
|
||||||
|
String[] invalidOperators = {"&&", "||", "AND", "OR", "=", "!="};
|
||||||
|
for (String invalidOp : invalidOperators) {
|
||||||
|
if (tempExpression.contains(invalidOp)) {
|
||||||
|
if (invalidOp.equals("&&") || invalidOp.equals("||")) {
|
||||||
|
return String.format("Error: Use 'and'/'or' instead of '%s' for logical operations", invalidOp);
|
||||||
|
}
|
||||||
|
if (invalidOp.equals("AND") || invalidOp.equals("OR")) {
|
||||||
|
return String.format("Error: Use lowercase '%s' for logical operations", invalidOp.toLowerCase());
|
||||||
|
}
|
||||||
|
if (invalidOp.equals("=")) {
|
||||||
|
return "Error: Use '==' for equality comparison, not '='";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for unsupported special characters that might indicate syntax errors
|
||||||
|
if (tempExpression.matches(".*[#$%^&*+\\[\\]{}|\\\\;:'\"`~].*")) {
|
||||||
|
return "Error: Expression contains unsupported special characters. Use only supported operators and functions.";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "VALID";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates logical connectors syntax
|
||||||
|
*/
|
||||||
|
public String validateLogicalConnectors(String fieldConditions) {
|
||||||
|
// Check for proper spacing around logical operators
|
||||||
|
if (fieldConditions.matches(".*(\\S(and|or)\\S).*")) {
|
||||||
|
return "Error: Logical operators 'and'/'or' must be surrounded by spaces";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for consecutive logical operators
|
||||||
|
if (fieldConditions.matches(".*(and\\s+and|or\\s+or|and\\s+or\\s+and|or\\s+and\\s+or).*")) {
|
||||||
|
return "Error: Consecutive logical operators found. Use parentheses to group conditions properly.";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for logical operators at the beginning or end
|
||||||
|
String trimmed = fieldConditions.trim();
|
||||||
|
if (trimmed.startsWith("and ") || trimmed.startsWith("or ")
|
||||||
|
|| trimmed.endsWith(" and") || trimmed.endsWith(" or")) {
|
||||||
|
return "Error: Expression cannot start or end with logical operators 'and'/'or'";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "VALID";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates function syntax (equals, contains, matches, etc.)
|
||||||
|
*/
|
||||||
|
public String validateFunctions(String fieldConditions) {
|
||||||
|
// Check for properly formed function calls
|
||||||
|
String[] supportedFunctions = {"equals", "contains", "matches", "exists", "!equals", "!contains", "!matches", "!exists"};
|
||||||
|
|
||||||
|
// Find all function-like patterns
|
||||||
|
java.util.regex.Pattern functionPattern = java.util.regex.Pattern.compile("(!?\\w+)\\s*\\(([^)]*)\\)");
|
||||||
|
java.util.regex.Matcher matcher = functionPattern.matcher(fieldConditions);
|
||||||
|
|
||||||
|
while (matcher.find()) {
|
||||||
|
String functionName = matcher.group(1);
|
||||||
|
String functionArgs = matcher.group(2);
|
||||||
|
|
||||||
|
// Check if function is supported
|
||||||
|
boolean isSupported = false;
|
||||||
|
for (String supportedFunc : supportedFunctions) {
|
||||||
|
if (functionName.equals(supportedFunc)) {
|
||||||
|
isSupported = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isSupported) {
|
||||||
|
return String.format("Error: Unsupported function '%s'. Supported functions: %s",
|
||||||
|
functionName, String.join(", ", supportedFunctions));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate function arguments
|
||||||
|
if (functionName.equals("exists") || functionName.equals("!exists")) {
|
||||||
|
// exists() should have one parameter or no parameters
|
||||||
|
String[] args = functionArgs.trim().isEmpty() ? new String[0] : functionArgs.split(",");
|
||||||
|
if (args.length > 1) {
|
||||||
|
return String.format("Error: Function '%s' should have at most one parameter", functionName);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Other functions should have exactly 2 parameters
|
||||||
|
String[] args = functionArgs.split(",");
|
||||||
|
if (args.length != 2) {
|
||||||
|
return String.format("Error: Function '%s' requires exactly 2 parameters (field, value)", functionName);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that parameters are not empty
|
||||||
|
for (String arg : args) {
|
||||||
|
if (arg.trim().isEmpty()) {
|
||||||
|
return String.format("Error: Function '%s' has empty parameter", functionName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "VALID";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper method to parse key-value pairs from a string
|
||||||
|
* Format: "key1:value1, key2:value2, ..."
|
||||||
|
*/
|
||||||
|
public Map<String, String> parseKeyValuePairs(String input) {
|
||||||
|
Map<String, String> result = new HashMap<>();
|
||||||
|
if (input == null || input.trim().isEmpty()) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
String[] pairs = input.split(",");
|
||||||
|
for (String pair : pairs) {
|
||||||
|
String[] keyValue = pair.split(":");
|
||||||
|
if (keyValue.length == 2) {
|
||||||
|
result.put(keyValue[0].trim(), keyValue[1].trim());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recursively searches for a metric in the hierarchy
|
||||||
|
*/
|
||||||
|
public Hierarchy findMetricInHierarchy(List<Hierarchy> hierarchies, String metricName) {
|
||||||
|
for (Hierarchy hierarchy : hierarchies) {
|
||||||
|
// Check if this is the metric we're looking for
|
||||||
|
if (metricName.equals(hierarchy.getValue())) {
|
||||||
|
// Verify it has field children (leaf nodes)
|
||||||
|
if (hierarchy.getChildren() != null && !hierarchy.getChildren().isEmpty()) {
|
||||||
|
boolean hasLeafChildren = hierarchy.getChildren().stream()
|
||||||
|
.anyMatch(child -> child.getIsLeaf() != null && child.getIsLeaf());
|
||||||
|
if (hasLeafChildren) {
|
||||||
|
return hierarchy;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recursively search in children
|
||||||
|
if (hierarchy.getChildren() != null) {
|
||||||
|
Hierarchy found = findMetricInHierarchy(hierarchy.getChildren(), metricName);
|
||||||
|
if (found != null) {
|
||||||
|
return found;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a field is valid for the given metric
|
||||||
|
*/
|
||||||
|
public boolean isFieldValidForMetric(Hierarchy metricHierarchy, String fieldName) {
|
||||||
|
if (metricHierarchy.getChildren() == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (Hierarchy child : metricHierarchy.getChildren()) {
|
||||||
|
if (child.getIsLeaf() != null && child.getIsLeaf() && fieldName.equals(child.getValue())) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// Also check nested children
|
||||||
|
if (child.getChildren() != null && isFieldValidForMetric(child, fieldName)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts field names from field conditions string
|
||||||
|
* Handles simple cases like "field > 80", "equals(field, 'value')", complex expressions
|
||||||
|
*/
|
||||||
|
public List<String> extractFieldNamesFromConditions(String fieldConditions) {
|
||||||
|
List<String> fieldNames = new ArrayList<>();
|
||||||
|
|
||||||
|
// Split by logical operators (and, or) and parentheses, but preserve the field names
|
||||||
|
// This is a simple implementation - could be enhanced with a proper parser
|
||||||
|
String[] parts = fieldConditions.split("\\s+(and|or|&&|\\|\\|)\\s+|[()]+");
|
||||||
|
|
||||||
|
for (String part : parts) {
|
||||||
|
part = part.trim();
|
||||||
|
if (part.isEmpty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle equals() function: equals(fieldName, "value")
|
||||||
|
if (part.contains("equals(")) {
|
||||||
|
String fieldName = extractFieldFromEquals(part);
|
||||||
|
if (fieldName != null && !fieldNames.contains(fieldName)) {
|
||||||
|
fieldNames.add(fieldName);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Handle simple comparisons: fieldName > value, fieldName <= value
|
||||||
|
String fieldName = extractFieldFromComparison(part);
|
||||||
|
if (fieldName != null && !fieldNames.contains(fieldName)) {
|
||||||
|
fieldNames.add(fieldName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fieldNames;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts field name from equals() function
|
||||||
|
*/
|
||||||
|
public String extractFieldFromEquals(String condition) {
|
||||||
|
// Pattern: equals(fieldName, "value") or equals(fieldName, value)
|
||||||
|
int startParen = condition.indexOf('(');
|
||||||
|
int comma = condition.indexOf(',');
|
||||||
|
|
||||||
|
if (startParen != -1 && comma != -1 && comma > startParen) {
|
||||||
|
String fieldName = condition.substring(startParen + 1, comma).trim();
|
||||||
|
// Remove quotes if present
|
||||||
|
if (fieldName.startsWith("\"") && fieldName.endsWith("\"")) {
|
||||||
|
fieldName = fieldName.substring(1, fieldName.length() - 1);
|
||||||
|
}
|
||||||
|
return fieldName;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts field name from comparison operation
|
||||||
|
*/
|
||||||
|
public String extractFieldFromComparison(String condition) {
|
||||||
|
// Pattern: fieldName operator value
|
||||||
|
// Updated to include all supported operators
|
||||||
|
String[] operators = {" >= ", " <= ", " > ", " < ", " == ", " != "};
|
||||||
|
|
||||||
|
for (String operator : operators) {
|
||||||
|
if (condition.contains(operator)) {
|
||||||
|
String fieldName = condition.split(operator)[0].trim();
|
||||||
|
// Basic validation - field names shouldn't contain quotes or special chars
|
||||||
|
if (fieldName.matches("[a-zA-Z_][a-zA-Z0-9_]*")) {
|
||||||
|
return fieldName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper method to format hierarchy structure as JSON recursively
|
||||||
|
*/
|
||||||
|
public ObjectNode formatHierarchyAsJson(ObjectMapper mapper, Hierarchy hierarchy) {
|
||||||
|
ObjectNode node = mapper.createObjectNode();
|
||||||
|
|
||||||
|
node.put("value", hierarchy.getValue());
|
||||||
|
node.put("label", hierarchy.getLabel());
|
||||||
|
|
||||||
|
if (hierarchy.getIsLeaf() != null && hierarchy.getIsLeaf()) {
|
||||||
|
// Leaf node - actual metric field parameter
|
||||||
|
node.put("type", "field_parameter");
|
||||||
|
|
||||||
|
if (hierarchy.getType() != null) {
|
||||||
|
node.put("dataType", hierarchy.getType() == 0 ? "numeric" : "string");
|
||||||
|
}
|
||||||
|
if (hierarchy.getUnit() != null && !hierarchy.getUnit().trim().isEmpty()) {
|
||||||
|
node.put("unit", hierarchy.getUnit());
|
||||||
|
}
|
||||||
|
node.put("description", "Available field parameter for alert conditions");
|
||||||
|
} else {
|
||||||
|
// Category, app, or metric node
|
||||||
|
// Determine node type based on children
|
||||||
|
boolean hasLeafChildren = hierarchy.getChildren().stream()
|
||||||
|
.anyMatch(child -> child.getIsLeaf() != null && child.getIsLeaf());
|
||||||
|
|
||||||
|
if (hasLeafChildren) {
|
||||||
|
node.put("type", "metric");
|
||||||
|
node.put("description", "Metric with available field parameters");
|
||||||
|
} else {
|
||||||
|
node.put("type", "app");
|
||||||
|
node.put("description", "Application with available metrics");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hierarchy.getChildren() != null && !hierarchy.getChildren().isEmpty()) {
|
||||||
|
ArrayNode childrenArray = mapper.createArrayNode();
|
||||||
|
for (Hierarchy child : hierarchy.getChildren()) {
|
||||||
|
childrenArray.add(formatHierarchyAsJson(mapper, child));
|
||||||
|
}
|
||||||
|
node.set("children", childrenArray);
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format timestamp to readable format
|
||||||
|
*/
|
||||||
|
public String formatTimestamp(Long timestamp) {
|
||||||
|
if (timestamp == null) {
|
||||||
|
return "N/A";
|
||||||
|
}
|
||||||
|
LocalDateTime dateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(timestamp), ZoneId.systemDefault());
|
||||||
|
return dateTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper method to convert monitor status byte to readable text
|
||||||
|
* @param status The status byte from monitor
|
||||||
|
* @return Human-readable status text
|
||||||
|
*/
|
||||||
|
public String getStatusText(Byte status) {
|
||||||
|
if (status == null) {
|
||||||
|
return "Unknown";
|
||||||
|
}
|
||||||
|
return switch (status) {
|
||||||
|
case 0 -> "Paused";
|
||||||
|
case 1 -> "Online";
|
||||||
|
case 2 -> "Offline";
|
||||||
|
case 3 -> "Unreachable";
|
||||||
|
default -> "Unknown (" + status + ")";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract existing monitor IDs from the alert expression
|
||||||
|
* @param expression The alert expression containing __instance__ conditions
|
||||||
|
* @return List of existing monitor IDs
|
||||||
|
*/
|
||||||
|
public List<String> extractExistingMonitorIds(String expression) {
|
||||||
|
List<String> monitorIds = new ArrayList<>();
|
||||||
|
String pattern = "equals\\(__instance__,\\s*\"([^\"]+)\"\\)";
|
||||||
|
java.util.regex.Pattern regex = java.util.regex.Pattern.compile(pattern);
|
||||||
|
java.util.regex.Matcher matcher = regex.matcher(expression);
|
||||||
|
|
||||||
|
while (matcher.find()) {
|
||||||
|
monitorIds.add(matcher.group(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
return monitorIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replace existing __instance__ conditions in the expression with new ones
|
||||||
|
* @param expression The current expression
|
||||||
|
* @param newMonitorCondition The new monitor condition to replace with
|
||||||
|
* @return Updated expression
|
||||||
|
*/
|
||||||
|
public String replaceInstanceConditions(String expression, String newMonitorCondition) {
|
||||||
|
// More precise pattern to match complete __instance__ condition blocks without cutting field parameters
|
||||||
|
// Pattern matches either:
|
||||||
|
// 1. Single: equals(__instance__, "id")
|
||||||
|
// 2. Multiple: (equals(__instance__, "id1") or equals(__instance__, "id2") or ...)
|
||||||
|
|
||||||
|
// First try to match grouped conditions: (equals(__instance__, "id1") or equals(__instance__, "id2"))
|
||||||
|
String groupedPattern = "\\(\\s*equals\\(__instance__,\\s*\"[^\"]+\"\\)(?:\\s+or\\s+equals\\(__instance__,\\s*\"[^\"]+\"\\))*\\s*\\)";
|
||||||
|
if (expression.matches(".*" + groupedPattern + ".*")) {
|
||||||
|
return expression.replaceFirst(groupedPattern, newMonitorCondition);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Then try single condition: equals(__instance__, "id")
|
||||||
|
String singlePattern = "equals\\(__instance__,\\s*\"[^\"]+\"\\)";
|
||||||
|
if (expression.matches(".*" + singlePattern + ".*")) {
|
||||||
|
return expression.replaceFirst(singlePattern, newMonitorCondition);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no match found, return original expression
|
||||||
|
return expression;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -17,72 +17,56 @@
|
|||||||
|
|
||||||
package org.apache.hertzbeat.alert;
|
package org.apache.hertzbeat.alert;
|
||||||
|
|
||||||
import com.google.common.util.concurrent.ThreadFactoryBuilder;
|
|
||||||
import java.util.concurrent.LinkedBlockingQueue;
|
|
||||||
import java.util.concurrent.RejectedExecutionException;
|
|
||||||
import java.util.concurrent.ThreadFactory;
|
|
||||||
import java.util.concurrent.ThreadPoolExecutor;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.concurrent.ThreadFactory;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* alarm module thread pool
|
* alarm module thread pool with Virtual Threads
|
||||||
*/
|
*/
|
||||||
@Component
|
@Component
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class AlerterWorkerPool {
|
public class AlerterWorkerPool {
|
||||||
|
|
||||||
private ThreadPoolExecutor workerExecutor;
|
private ExecutorService workerExecutor;
|
||||||
private ThreadPoolExecutor notifyExecutor;
|
private ExecutorService notifyExecutor;
|
||||||
|
private ExecutorService logWorkerExecutor;
|
||||||
|
|
||||||
public AlerterWorkerPool() {
|
public AlerterWorkerPool() {
|
||||||
initWorkExecutor();
|
initWorkExecutor();
|
||||||
initNotifyExecutor();
|
initNotifyExecutor();
|
||||||
|
initLogWorkerExecutor();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void initWorkExecutor() {
|
private void initWorkExecutor() {
|
||||||
ThreadFactory threadFactory = new ThreadFactoryBuilder()
|
ThreadFactory factory = Thread.ofVirtual()
|
||||||
.setUncaughtExceptionHandler((thread, throwable) -> {
|
.name("alerter-worker-", 0)
|
||||||
log.error("Alerter workerExecutor has uncaughtException.");
|
.factory();
|
||||||
log.error(throwable.getMessage(), throwable);
|
workerExecutor = Executors.newThreadPerTaskExecutor(factory);
|
||||||
})
|
|
||||||
.setDaemon(true)
|
|
||||||
.setNameFormat("alerter-worker-%d")
|
|
||||||
.build();
|
|
||||||
workerExecutor = new ThreadPoolExecutor(10,
|
|
||||||
10,
|
|
||||||
10,
|
|
||||||
TimeUnit.SECONDS,
|
|
||||||
new LinkedBlockingQueue<>(),
|
|
||||||
threadFactory,
|
|
||||||
new ThreadPoolExecutor.AbortPolicy());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void initNotifyExecutor() {
|
private void initNotifyExecutor() {
|
||||||
ThreadFactory threadFactory = new ThreadFactoryBuilder()
|
ThreadFactory factory = Thread.ofVirtual()
|
||||||
.setUncaughtExceptionHandler((thread, throwable) -> {
|
.name("notify-worker-", 0)
|
||||||
log.error("Alerter notifyExecutor has uncaughtException.");
|
.factory();
|
||||||
log.error(throwable.getMessage(), throwable);
|
notifyExecutor = Executors.newThreadPerTaskExecutor(factory);
|
||||||
})
|
}
|
||||||
.setDaemon(true)
|
|
||||||
.setNameFormat("notify-worker-%d")
|
private void initLogWorkerExecutor() {
|
||||||
.build();
|
ThreadFactory factory = Thread.ofVirtual()
|
||||||
notifyExecutor = new ThreadPoolExecutor(6,
|
.name("log-worker-", 0)
|
||||||
6,
|
.factory();
|
||||||
10,
|
logWorkerExecutor = Executors.newThreadPerTaskExecutor(factory);
|
||||||
TimeUnit.SECONDS,
|
|
||||||
new LinkedBlockingQueue<>(),
|
|
||||||
threadFactory,
|
|
||||||
new ThreadPoolExecutor.AbortPolicy());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Run the alerter task
|
* Run the alerter task
|
||||||
* @param runnable task
|
* @param runnable task
|
||||||
* @throws RejectedExecutionException when The thread pool is full of
|
|
||||||
*/
|
*/
|
||||||
public void executeJob(Runnable runnable) throws RejectedExecutionException {
|
public void executeJob(Runnable runnable){
|
||||||
workerExecutor.execute(runnable);
|
workerExecutor.execute(runnable);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,10 +74,17 @@ public class AlerterWorkerPool {
|
|||||||
* Executes the given runnable task using the notifyExecutor.
|
* Executes the given runnable task using the notifyExecutor.
|
||||||
*
|
*
|
||||||
* @param runnable the task to be executed
|
* @param runnable the task to be executed
|
||||||
* @throws RejectedExecutionException if the task cannot be accepted for execution
|
|
||||||
*/
|
*/
|
||||||
public void executeNotify(Runnable runnable) throws RejectedExecutionException {
|
public void executeNotify(Runnable runnable){
|
||||||
notifyExecutor.execute(runnable);
|
notifyExecutor.execute(runnable);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
/**
|
||||||
|
* Executes the given runnable task using the logWorkerExecutor.
|
||||||
|
*
|
||||||
|
* @param runnable the task to be executed
|
||||||
|
*/
|
||||||
|
public void executeLogJob(Runnable runnable){
|
||||||
|
logWorkerExecutor.execute(runnable);
|
||||||
|
}
|
||||||
|
}
|
||||||
+53
-17
@@ -17,6 +17,9 @@
|
|||||||
|
|
||||||
package org.apache.hertzbeat.alert.calculate;
|
package org.apache.hertzbeat.alert.calculate;
|
||||||
|
|
||||||
|
import com.google.common.collect.Table;
|
||||||
|
import com.google.common.collect.Tables;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.apache.hertzbeat.alert.dao.SingleAlertDao;
|
import org.apache.hertzbeat.alert.dao.SingleAlertDao;
|
||||||
import org.apache.hertzbeat.alert.util.AlertUtil;
|
import org.apache.hertzbeat.alert.util.AlertUtil;
|
||||||
import org.apache.hertzbeat.common.constants.CommonConstants;
|
import org.apache.hertzbeat.common.constants.CommonConstants;
|
||||||
@@ -24,7 +27,6 @@ import org.apache.hertzbeat.common.entity.alerter.SingleAlert;
|
|||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -33,49 +35,83 @@ import java.util.concurrent.ConcurrentHashMap;
|
|||||||
@Component
|
@Component
|
||||||
public class AlarmCacheManager {
|
public class AlarmCacheManager {
|
||||||
|
|
||||||
|
private static final String CUSTOM_FIRING_ROW_KEY = "CUSTOM_FIRING_";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The alarm in the process is triggered
|
* The alarm in the process is triggered
|
||||||
* key - labels fingerprint
|
* rowKey - define id
|
||||||
|
* columnKey - labels fingerprint
|
||||||
*/
|
*/
|
||||||
private final Map<String, SingleAlert> pendingAlertMap;
|
private final Table<String, String, SingleAlert> pendingAlertMap;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The not recover alert
|
* The not recover alert
|
||||||
* key - labels fingerprint
|
* rowKey - define id
|
||||||
|
* columnKey - labels fingerprint
|
||||||
*/
|
*/
|
||||||
private final Map<String, SingleAlert> firingAlertMap;
|
private final Table<String, String, SingleAlert> firingAlertMap;
|
||||||
|
|
||||||
public AlarmCacheManager(SingleAlertDao singleAlertDao) {
|
public AlarmCacheManager(SingleAlertDao singleAlertDao) {
|
||||||
this.pendingAlertMap = new ConcurrentHashMap<>(8);
|
this.pendingAlertMap = Tables.newCustomTable(new ConcurrentHashMap<>(8), ConcurrentHashMap::new);
|
||||||
this.firingAlertMap = new ConcurrentHashMap<>(8);
|
this.firingAlertMap = Tables.newCustomTable(new ConcurrentHashMap<>(8), ConcurrentHashMap::new);
|
||||||
List<SingleAlert> singleAlerts = singleAlertDao.querySingleAlertsByStatus(CommonConstants.ALERT_STATUS_FIRING);
|
List<SingleAlert> singleAlerts = singleAlertDao.querySingleAlertsByStatus(CommonConstants.ALERT_STATUS_FIRING);
|
||||||
for (SingleAlert singleAlert : singleAlerts) {
|
for (SingleAlert singleAlert : singleAlerts) {
|
||||||
String fingerprint = AlertUtil.calculateFingerprint(singleAlert.getLabels());
|
String fingerprint = AlertUtil.calculateFingerprint(singleAlert.getLabels());
|
||||||
|
String defineId = singleAlert.getLabels().get(CommonConstants.LABEL_DEFINE_ID);
|
||||||
|
if (StringUtils.isBlank(defineId)) {
|
||||||
|
defineId = getCustomKey(fingerprint);
|
||||||
|
}
|
||||||
singleAlert.setId(null);
|
singleAlert.setId(null);
|
||||||
this.firingAlertMap.put(fingerprint, singleAlert);
|
this.firingAlertMap.put(defineId, fingerprint, singleAlert);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void putPending(String fingerPrint, SingleAlert alert) {
|
public void putPending(Long defineId, String fingerPrint, SingleAlert alert) {
|
||||||
this.pendingAlertMap.put(fingerPrint, alert);
|
this.pendingAlertMap.put(String.valueOf(defineId), fingerPrint, alert);
|
||||||
}
|
}
|
||||||
|
|
||||||
public SingleAlert getPending(String fingerPrint) {
|
public SingleAlert getPending(Long defineId, String fingerPrint) {
|
||||||
return this.pendingAlertMap.get(fingerPrint);
|
return this.pendingAlertMap.get(String.valueOf(defineId), fingerPrint);
|
||||||
}
|
}
|
||||||
|
|
||||||
public SingleAlert removePending(String fingerPrint) {
|
public void removePending(Long defineId, String fingerPrint) {
|
||||||
return this.pendingAlertMap.remove(fingerPrint);
|
this.pendingAlertMap.remove(String.valueOf(defineId), fingerPrint);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void putFiring(Long defineId, String fingerPrint, SingleAlert alert) {
|
||||||
|
this.firingAlertMap.put(String.valueOf(defineId), fingerPrint, alert);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void putFiring(String fingerPrint, SingleAlert alert) {
|
public void putFiring(String fingerPrint, SingleAlert alert) {
|
||||||
this.firingAlertMap.put(fingerPrint, alert);
|
this.firingAlertMap.put(getCustomKey(fingerPrint), fingerPrint, alert);
|
||||||
|
}
|
||||||
|
|
||||||
|
public SingleAlert getFiring(Long defineId, String fingerPrint) {
|
||||||
|
SingleAlert singleAlert = this.firingAlertMap.get(String.valueOf(defineId), fingerPrint);
|
||||||
|
if (null != singleAlert) {
|
||||||
|
return singleAlert;
|
||||||
|
}
|
||||||
|
return getFiring(fingerPrint);
|
||||||
|
}
|
||||||
|
|
||||||
|
public SingleAlert removeFiring(Long defineId, String fingerPrint) {
|
||||||
|
SingleAlert singleAlert = this.firingAlertMap.remove(String.valueOf(defineId), fingerPrint);
|
||||||
|
if (null == singleAlert) {
|
||||||
|
return this.firingAlertMap.remove(getCustomKey(fingerPrint), fingerPrint);
|
||||||
|
}
|
||||||
|
return singleAlert;
|
||||||
}
|
}
|
||||||
|
|
||||||
public SingleAlert getFiring(String fingerPrint) {
|
public SingleAlert getFiring(String fingerPrint) {
|
||||||
return this.firingAlertMap.get(fingerPrint);
|
return this.firingAlertMap.get(getCustomKey(fingerPrint), fingerPrint);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String getCustomKey(String fingerPrint) {
|
||||||
|
return CUSTOM_FIRING_ROW_KEY + fingerPrint;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public SingleAlert removeFiring(String fingerPrint) {
|
public SingleAlert removeFiring(String fingerPrint) {
|
||||||
return this.firingAlertMap.remove(fingerPrint);
|
return this.firingAlertMap.remove(getCustomKey(fingerPrint), fingerPrint);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-1
@@ -76,11 +76,12 @@ public class CollectorAlertHandler {
|
|||||||
fingerPrints.put(KEY_COLLECTOR_VERSION, collector.getVersion());
|
fingerPrints.put(KEY_COLLECTOR_VERSION, collector.getVersion());
|
||||||
fingerPrints.put(KEY_COLLECTOR_HOST, collector.getIp());
|
fingerPrints.put(KEY_COLLECTOR_HOST, collector.getIp());
|
||||||
String fingerprint = AlertUtil.calculateFingerprint(fingerPrints);
|
String fingerprint = AlertUtil.calculateFingerprint(fingerPrints);
|
||||||
SingleAlert firingAlert = alarmCacheManager.getFiring(fingerprint);
|
SingleAlert firingAlert = alarmCacheManager.removeFiring(fingerprint);
|
||||||
if (firingAlert != null) {
|
if (firingAlert != null) {
|
||||||
firingAlert.setTriggerTimes(1);
|
firingAlert.setTriggerTimes(1);
|
||||||
firingAlert.setEndAt(System.currentTimeMillis());
|
firingAlert.setEndAt(System.currentTimeMillis());
|
||||||
firingAlert.setStatus(CommonConstants.ALERT_STATUS_RESOLVED);
|
firingAlert.setStatus(CommonConstants.ALERT_STATUS_RESOLVED);
|
||||||
|
firingAlert.setContent(this.bundle.getString("alerter.availability.collector.recover"));
|
||||||
alarmCommonReduce.reduceAndSendAlarm(firingAlert.clone());
|
alarmCommonReduce.reduceAndSendAlarm(firingAlert.clone());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+72
@@ -0,0 +1,72 @@
|
|||||||
|
/*
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.apache.hertzbeat.alert.calculate;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.commons.jexl3.JexlException;
|
||||||
|
import org.apache.commons.jexl3.JexlExpression;
|
||||||
|
import org.apache.hertzbeat.common.util.JexlExpressionRunner;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JexlExprCalculator is a utility class for evaluating JEXL expressions
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
public class JexlExprCalculator {
|
||||||
|
/**
|
||||||
|
* Execute an alert expression
|
||||||
|
* @param fieldValueMap The field value map for expression evaluation
|
||||||
|
* @param expr The expression to evaluate
|
||||||
|
* @param ignoreJexlException Whether to ignore JEXL exceptions
|
||||||
|
* @return true if the expression matches, false otherwise
|
||||||
|
*/
|
||||||
|
public boolean execAlertExpression(Map<String, Object> fieldValueMap, String expr, boolean ignoreJexlException) {
|
||||||
|
Boolean match;
|
||||||
|
JexlExpression expression;
|
||||||
|
try {
|
||||||
|
expression = JexlExpressionRunner.compile(expr);
|
||||||
|
} catch (JexlException jexlException) {
|
||||||
|
log.warn("Alarm Rule: {} Compile Error: {}.", expr, jexlException.getMessage());
|
||||||
|
throw jexlException;
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Alarm Rule: {} Unknown Error: {}.", expr, e.getMessage());
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
match = (Boolean) JexlExpressionRunner.evaluate(expression, fieldValueMap);
|
||||||
|
} catch (JexlException jexlException) {
|
||||||
|
if (ignoreJexlException) {
|
||||||
|
log.debug("Alarm Rule: {} Run Error: {}.", expr, jexlException.getMessage());
|
||||||
|
} else {
|
||||||
|
log.error("Alarm Rule: {} Run Error: {}.", expr, jexlException.getMessage());
|
||||||
|
}
|
||||||
|
throw jexlException;
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Alarm Rule: {} Unknown Error: {}.", expr, e.getMessage());
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
return match != null && match;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+232
@@ -0,0 +1,232 @@
|
|||||||
|
/*
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||||
|
* contributor license agreements. See the NOTICE file distributed with
|
||||||
|
* this work for additional information regarding copyright ownership.
|
||||||
|
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||||
|
* (the "License"); you may not use this file except in compliance with
|
||||||
|
* the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.apache.hertzbeat.alert.calculate.periodic;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.commons.collections4.CollectionUtils;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.apache.hertzbeat.alert.reduce.AlarmCommonReduce;
|
||||||
|
import org.apache.hertzbeat.alert.service.DataSourceService;
|
||||||
|
import org.apache.hertzbeat.alert.util.AlertTemplateUtil;
|
||||||
|
import org.apache.hertzbeat.common.constants.CommonConstants;
|
||||||
|
import org.apache.hertzbeat.common.entity.alerter.AlertDefine;
|
||||||
|
import org.apache.hertzbeat.common.entity.alerter.SingleAlert;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log Periodic Alert Calculator
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
public class LogPeriodicAlertCalculator {
|
||||||
|
|
||||||
|
private static final String ROWS = "__rows__";
|
||||||
|
|
||||||
|
private final DataSourceService dataSourceService;
|
||||||
|
private final AlarmCommonReduce alarmCommonReduce;
|
||||||
|
|
||||||
|
|
||||||
|
public LogPeriodicAlertCalculator(DataSourceService dataSourceService, AlarmCommonReduce alarmCommonReduce) {
|
||||||
|
this.alarmCommonReduce = alarmCommonReduce;
|
||||||
|
this.dataSourceService = dataSourceService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void calculate(AlertDefine define) {
|
||||||
|
if (!define.isEnable() || StringUtils.isEmpty(define.getExpr())) {
|
||||||
|
log.error("Log define {} is disabled or expression is empty", define.getName());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
doCalculate(define);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Calculate periodic define {} failed: {}", define.getName(), e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void doCalculate(AlertDefine define) {
|
||||||
|
try {
|
||||||
|
// Log-based queries are SQL queries with log-specific expressions
|
||||||
|
List<Map<String, Object>> results = dataSourceService.query(define.getDatasource(), define.getExpr());
|
||||||
|
if (CollectionUtils.isEmpty(results)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
afterThresholdRuleMatch(results, define);
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
// Ignore the query exception eg: no result, timeout, etc
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String getAlertMode(AlertDefine alertDefine) {
|
||||||
|
String mode = null;
|
||||||
|
if (alertDefine.getLabels() != null) {
|
||||||
|
mode = alertDefine.getLabels().get(CommonConstants.ALERT_MODE_LABEL);
|
||||||
|
}
|
||||||
|
if (mode == null || mode.isEmpty()) {
|
||||||
|
return CommonConstants.ALERT_MODE_GROUP; // Default to group mode if not specified
|
||||||
|
} else {
|
||||||
|
return mode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle alert after threshold rule match
|
||||||
|
*/
|
||||||
|
private void afterThresholdRuleMatch(List<Map<String, Object>> alertContext, AlertDefine define) {
|
||||||
|
// Determine alert mode from configuration
|
||||||
|
String alertMode = getAlertMode(define);
|
||||||
|
|
||||||
|
long currentTime = System.currentTimeMillis();
|
||||||
|
|
||||||
|
switch (alertMode) {
|
||||||
|
case CommonConstants.ALERT_MODE_INDIVIDUAL:
|
||||||
|
// Generate individual alerts for each matching log
|
||||||
|
for (Map<String, Object> context : alertContext) {
|
||||||
|
generateIndividualAlert(define, context, currentTime);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case CommonConstants.ALERT_MODE_GROUP:
|
||||||
|
// Generate a single alert group for all matching logs
|
||||||
|
generateGroupAlert(define, alertContext, currentTime);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
log.warn("Unknown alert mode for define {}: {}", define.getName(), alertMode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void generateIndividualAlert(AlertDefine define, Map<String, Object> context, long currentTime) {
|
||||||
|
|
||||||
|
Map<String, String> alertLabels = new HashMap<>(8);
|
||||||
|
|
||||||
|
Map<String, String> commonFingerPrints = createCommonFingerprints(define);
|
||||||
|
alertLabels.putAll(commonFingerPrints);
|
||||||
|
addContextToMap(context, alertLabels);
|
||||||
|
|
||||||
|
Map<String, Object> fieldValueMap = createFieldValueMap(context, define);
|
||||||
|
Map<String, String> alertAnnotations = createAlertAnnotations(define, fieldValueMap);
|
||||||
|
// Create and send group alert
|
||||||
|
SingleAlert alert = SingleAlert.builder()
|
||||||
|
.labels(alertLabels)
|
||||||
|
.annotations(alertAnnotations)
|
||||||
|
.content(AlertTemplateUtil.render(define.getTemplate(), fieldValueMap))
|
||||||
|
.status(CommonConstants.ALERT_STATUS_FIRING)
|
||||||
|
.triggerTimes(1)
|
||||||
|
.startAt(currentTime)
|
||||||
|
.activeAt(currentTime)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
alarmCommonReduce.reduceAndSendAlarm(alert.clone());
|
||||||
|
|
||||||
|
log.debug("Generated individual alert for define: {}", define.getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addContextToMap(Map<String, Object> context, Map<String, String> alertLabels) {
|
||||||
|
for (Map.Entry<String, Object> entry : context.entrySet()) {
|
||||||
|
if (entry.getValue() != null) {
|
||||||
|
alertLabels.put(entry.getKey(), entry.getValue().toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void generateGroupAlert(AlertDefine define, List<Map<String, Object>> alertContext, long currentTime) {
|
||||||
|
|
||||||
|
List<SingleAlert> alerts = new ArrayList<>(alertContext.size());
|
||||||
|
|
||||||
|
// Create fingerprints for group alert
|
||||||
|
Map<String, String> commonFingerPrints = createCommonFingerprints(define);
|
||||||
|
|
||||||
|
// Add context information to fingerprints
|
||||||
|
commonFingerPrints.put(ROWS, String.valueOf(alertContext.size()));
|
||||||
|
commonFingerPrints.put(CommonConstants.ALERT_MODE_LABEL, CommonConstants.ALERT_MODE_GROUP);
|
||||||
|
|
||||||
|
for (Map<String, Object> context : alertContext) {
|
||||||
|
|
||||||
|
Map<String, String> alertLabels = new HashMap<>(8);
|
||||||
|
|
||||||
|
alertLabels.putAll(commonFingerPrints);
|
||||||
|
// add the context to commonFingerPrints
|
||||||
|
addContextToMap(context, alertLabels);
|
||||||
|
|
||||||
|
Map<String, Object> fieldValueMap = createFieldValueMap(context, define);
|
||||||
|
Map<String, String> alertAnnotations = createAlertAnnotations(define, fieldValueMap);
|
||||||
|
// Create and send group alert
|
||||||
|
SingleAlert alert = SingleAlert.builder()
|
||||||
|
.labels(alertLabels)
|
||||||
|
.annotations(alertAnnotations)
|
||||||
|
.content(AlertTemplateUtil.render(define.getTemplate(), fieldValueMap))
|
||||||
|
.status(CommonConstants.ALERT_STATUS_FIRING)
|
||||||
|
.triggerTimes(alertContext.size())
|
||||||
|
.startAt(currentTime)
|
||||||
|
.activeAt(currentTime)
|
||||||
|
.build();
|
||||||
|
alerts.add(alert.clone());
|
||||||
|
}
|
||||||
|
alarmCommonReduce.reduceAndSendAlarmGroup(commonFingerPrints, alerts);
|
||||||
|
|
||||||
|
log.debug("Generated group alert for define: {} with {} matching data",
|
||||||
|
define.getName(), alertContext.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
private Map<String, String> createCommonFingerprints(AlertDefine define) {
|
||||||
|
Map<String, String> fingerprints = new HashMap<>(8);
|
||||||
|
fingerprints.put(CommonConstants.LABEL_ALERT_NAME, define.getName());
|
||||||
|
fingerprints.put(CommonConstants.LABEL_DEFINE_ID, String.valueOf(define.getId()));
|
||||||
|
|
||||||
|
if (define.getLabels() != null) {
|
||||||
|
fingerprints.putAll(define.getLabels());
|
||||||
|
}
|
||||||
|
|
||||||
|
return fingerprints;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, Object> createFieldValueMap(Map<String, Object> context, AlertDefine define) {
|
||||||
|
Map<String, Object> fieldValueMap = new HashMap<>(8);
|
||||||
|
for (Map.Entry<String, Object> entry : context.entrySet()) {
|
||||||
|
if (entry.getValue() != null) {
|
||||||
|
fieldValueMap.put(entry.getKey(), entry.getValue().toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (define.getLabels() != null) {
|
||||||
|
fieldValueMap.putAll(define.getLabels());
|
||||||
|
}
|
||||||
|
|
||||||
|
return fieldValueMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, String> createAlertAnnotations(AlertDefine define, Map<String, Object> fieldValueMap) {
|
||||||
|
Map<String, String> annotations = new HashMap<>(8);
|
||||||
|
|
||||||
|
if (define.getAnnotations() != null) {
|
||||||
|
for (Map.Entry<String, String> entry : define.getAnnotations().entrySet()) {
|
||||||
|
annotations.put(entry.getKey(),
|
||||||
|
AlertTemplateUtil.render(entry.getValue(), fieldValueMap));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return annotations;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+88
-73
@@ -15,10 +15,12 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.apache.hertzbeat.alert.calculate;
|
package org.apache.hertzbeat.alert.calculate.periodic;
|
||||||
|
|
||||||
import java.util.HashMap;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.commons.collections4.CollectionUtils;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.apache.hertzbeat.alert.calculate.AlarmCacheManager;
|
||||||
import org.apache.hertzbeat.alert.reduce.AlarmCommonReduce;
|
import org.apache.hertzbeat.alert.reduce.AlarmCommonReduce;
|
||||||
import org.apache.hertzbeat.alert.service.DataSourceService;
|
import org.apache.hertzbeat.alert.service.DataSourceService;
|
||||||
import org.apache.hertzbeat.alert.util.AlertTemplateUtil;
|
import org.apache.hertzbeat.alert.util.AlertTemplateUtil;
|
||||||
@@ -26,19 +28,18 @@ import org.apache.hertzbeat.alert.util.AlertUtil;
|
|||||||
import org.apache.hertzbeat.common.constants.CommonConstants;
|
import org.apache.hertzbeat.common.constants.CommonConstants;
|
||||||
import org.apache.hertzbeat.common.entity.alerter.AlertDefine;
|
import org.apache.hertzbeat.common.entity.alerter.AlertDefine;
|
||||||
import org.apache.hertzbeat.common.entity.alerter.SingleAlert;
|
import org.apache.hertzbeat.common.entity.alerter.SingleAlert;
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import org.apache.commons.lang3.StringUtils;
|
|
||||||
import org.apache.commons.collections4.CollectionUtils;
|
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
/**
|
import java.util.HashMap;
|
||||||
* Periodic Alert Calculator
|
import java.util.List;
|
||||||
*/
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Metrics Periodic Alert Calculator
|
||||||
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Component
|
@Component
|
||||||
public class PeriodicAlertCalculator {
|
public class MetricsPeriodicAlertCalculator {
|
||||||
|
|
||||||
private static final String VALUE = "__value__";
|
private static final String VALUE = "__value__";
|
||||||
private static final String TIMESTAMP = "__timestamp__";
|
private static final String TIMESTAMP = "__timestamp__";
|
||||||
@@ -47,72 +48,85 @@ public class PeriodicAlertCalculator {
|
|||||||
private final AlarmCommonReduce alarmCommonReduce;
|
private final AlarmCommonReduce alarmCommonReduce;
|
||||||
private final AlarmCacheManager alarmCacheManager;
|
private final AlarmCacheManager alarmCacheManager;
|
||||||
|
|
||||||
public PeriodicAlertCalculator(DataSourceService dataSourceService, AlarmCommonReduce alarmCommonReduce,
|
public MetricsPeriodicAlertCalculator(DataSourceService dataSourceService, AlarmCommonReduce alarmCommonReduce,
|
||||||
AlarmCacheManager alarmCacheManager) {
|
AlarmCacheManager alarmCacheManager) {
|
||||||
this.dataSourceService = dataSourceService;
|
|
||||||
this.alarmCommonReduce = alarmCommonReduce;
|
this.alarmCommonReduce = alarmCommonReduce;
|
||||||
this.alarmCacheManager = alarmCacheManager;
|
this.alarmCacheManager = alarmCacheManager;
|
||||||
|
this.dataSourceService = dataSourceService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void calculate(AlertDefine rule) {
|
/**
|
||||||
if (!rule.isEnable() || StringUtils.isEmpty(rule.getExpr())) {
|
* Calculate alerts for the given alert definition
|
||||||
log.error("Periodic rule {} is disabled or expression is empty", rule.getName());
|
* @param define The alert definition to calculate
|
||||||
|
*/
|
||||||
|
public void calculate(AlertDefine define) {
|
||||||
|
if (!define.isEnable() || StringUtils.isEmpty(define.getExpr())) {
|
||||||
|
log.error("Periodic define {} is disabled or expression is empty", define.getName());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
long currentTimeMilli = System.currentTimeMillis();
|
long currentTimeMilli = System.currentTimeMillis();
|
||||||
try {
|
try {
|
||||||
// for prometheus is instant promql query, for db is sql query
|
doCalculate(define, currentTimeMilli);
|
||||||
// result: [{'value': 100, 'timestamp': 1343554, 'instance': 'node1'},{'value': 200, 'timestamp': 1343555, 'instance': 'node2'}]
|
|
||||||
// the return result should be matched with threshold
|
|
||||||
try {
|
|
||||||
List<Map<String, Object>> results = dataSourceService.calculate(
|
|
||||||
rule.getDatasource(),
|
|
||||||
rule.getExpr()
|
|
||||||
);
|
|
||||||
// if no match the expr threshold, the results item map {'value': null} should be null and others field keep
|
|
||||||
// if results has multi list, should trigger multi alert
|
|
||||||
if (CollectionUtils.isEmpty(results)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
for (Map<String, Object> result : results) {
|
|
||||||
Map<String, String> fingerPrints = new HashMap<>(8);
|
|
||||||
// here use the alert name as finger, not care the alert name may be changed
|
|
||||||
fingerPrints.put(CommonConstants.LABEL_ALERT_NAME, rule.getName());
|
|
||||||
fingerPrints.putAll(rule.getLabels());
|
|
||||||
for (Map.Entry<String, Object> entry : result.entrySet()) {
|
|
||||||
if (entry.getValue() != null && !VALUE.equals(entry.getKey())
|
|
||||||
&& !TIMESTAMP.equals(entry.getKey())) {
|
|
||||||
fingerPrints.put(entry.getKey(), entry.getValue().toString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (result.get(VALUE) == null) {
|
|
||||||
// recovery the alert
|
|
||||||
handleRecoveredAlert(fingerPrints);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
Map<String, Object> fieldValueMap = new HashMap<>(8);
|
|
||||||
fieldValueMap.putAll(rule.getLabels());
|
|
||||||
fieldValueMap.put(CommonConstants.LABEL_ALERT_NAME, rule.getName());
|
|
||||||
for (Map.Entry<String, Object> entry : result.entrySet()) {
|
|
||||||
if (entry.getValue() != null) {
|
|
||||||
fieldValueMap.put(entry.getKey(), entry.getValue());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
afterThresholdRuleMatch(currentTimeMilli, fingerPrints, fieldValueMap, rule);
|
|
||||||
}
|
|
||||||
} catch (Exception ignored) {
|
|
||||||
// ignore the query exception eg: no result, timeout, etc
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("Calculate periodic rule {} failed: {}", rule.getName(), e.getMessage());
|
log.error("Calculate periodic define {} failed: {}", define.getName(), e.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void doCalculate(AlertDefine define, long currentTimeMilli) {
|
||||||
|
try {
|
||||||
|
List<Map<String, Object>> results = dataSourceService.calculate(
|
||||||
|
define.getDatasource(),
|
||||||
|
define.getExpr()
|
||||||
|
);
|
||||||
|
// If no match the expr threshold, the results item map {'value': null} should be null and others field keep
|
||||||
|
// If results has multi list, should trigger multi alert
|
||||||
|
if (CollectionUtils.isEmpty(results)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (Map<String, Object> result : results) {
|
||||||
|
Map<String, String> fingerPrints = new HashMap<>(8);
|
||||||
|
// Here use the alert name as finger, not care the alert name may be changed
|
||||||
|
fingerPrints.put(CommonConstants.LABEL_DEFINE_ID, String.valueOf(define.getId()));
|
||||||
|
fingerPrints.put(CommonConstants.LABEL_ALERT_NAME, define.getName());
|
||||||
|
fingerPrints.putAll(define.getLabels());
|
||||||
|
for (Map.Entry<String, Object> entry : result.entrySet()) {
|
||||||
|
if (entry.getValue() != null && !VALUE.equals(entry.getKey())
|
||||||
|
&& !TIMESTAMP.equals(entry.getKey())) {
|
||||||
|
fingerPrints.put(entry.getKey(), entry.getValue().toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.get(VALUE) == null) {
|
||||||
|
// Recovery the alert
|
||||||
|
handleRecoveredAlert(define.getId(), fingerPrints);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, Object> fieldValueMap = new HashMap<>(8);
|
||||||
|
fieldValueMap.putAll(define.getLabels());
|
||||||
|
fieldValueMap.put(CommonConstants.LABEL_ALERT_NAME, define.getName());
|
||||||
|
for (Map.Entry<String, Object> entry : result.entrySet()) {
|
||||||
|
if (entry.getValue() != null) {
|
||||||
|
fieldValueMap.put(entry.getKey(), entry.getValue());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
afterThresholdRuleMatch(currentTimeMilli, fingerPrints, fieldValueMap, define);
|
||||||
|
}
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
// Ignore the query exception eg: no result, timeout, etc
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle alert after threshold rule match
|
||||||
|
*/
|
||||||
private void afterThresholdRuleMatch(long currentTimeMilli, Map<String, String> fingerPrints,
|
private void afterThresholdRuleMatch(long currentTimeMilli, Map<String, String> fingerPrints,
|
||||||
Map<String, Object> fieldValueMap, AlertDefine define) {
|
Map<String, Object> fieldValueMap, AlertDefine define) {
|
||||||
|
Long defineId = define.getId();
|
||||||
String fingerprint = AlertUtil.calculateFingerprint(fingerPrints);
|
String fingerprint = AlertUtil.calculateFingerprint(fingerPrints);
|
||||||
SingleAlert existingAlert = alarmCacheManager.getPending(fingerprint);
|
SingleAlert existingAlert = alarmCacheManager.getPending(defineId, fingerprint);
|
||||||
Map<String, String> labels = new HashMap<>(8);
|
Map<String, String> labels = new HashMap<>(8);
|
||||||
fieldValueMap.putAll(define.getLabels());
|
fieldValueMap.putAll(define.getLabels());
|
||||||
labels.putAll(fingerPrints);
|
labels.putAll(fingerPrints);
|
||||||
@@ -121,7 +135,6 @@ public class PeriodicAlertCalculator {
|
|||||||
// First time triggering alert, create new alert and set to pending status
|
// First time triggering alert, create new alert and set to pending status
|
||||||
SingleAlert newAlert = SingleAlert.builder()
|
SingleAlert newAlert = SingleAlert.builder()
|
||||||
.labels(labels)
|
.labels(labels)
|
||||||
// todo render var content in annotations
|
|
||||||
.annotations(define.getAnnotations())
|
.annotations(define.getAnnotations())
|
||||||
.content(AlertTemplateUtil.render(define.getTemplate(), fieldValueMap))
|
.content(AlertTemplateUtil.render(define.getTemplate(), fieldValueMap))
|
||||||
.status(CommonConstants.ALERT_STATUS_PENDING)
|
.status(CommonConstants.ALERT_STATUS_PENDING)
|
||||||
@@ -133,11 +146,11 @@ public class PeriodicAlertCalculator {
|
|||||||
// If required trigger times is 1, set to firing status directly
|
// If required trigger times is 1, set to firing status directly
|
||||||
if (requiredTimes <= 1) {
|
if (requiredTimes <= 1) {
|
||||||
newAlert.setStatus(CommonConstants.ALERT_STATUS_FIRING);
|
newAlert.setStatus(CommonConstants.ALERT_STATUS_FIRING);
|
||||||
alarmCacheManager.putFiring(fingerprint, newAlert);
|
alarmCacheManager.putFiring(defineId, fingerprint, newAlert);
|
||||||
alarmCommonReduce.reduceAndSendAlarm(newAlert.clone());
|
alarmCommonReduce.reduceAndSendAlarm(newAlert.clone());
|
||||||
} else {
|
} else {
|
||||||
// Otherwise put into pending queue first
|
// Otherwise put into pending queue first
|
||||||
alarmCacheManager.putPending(fingerprint, newAlert);
|
alarmCacheManager.putPending(defineId, fingerprint, newAlert);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Update existing alert
|
// Update existing alert
|
||||||
@@ -147,25 +160,27 @@ public class PeriodicAlertCalculator {
|
|||||||
// Check if required trigger times reached
|
// Check if required trigger times reached
|
||||||
if (existingAlert.getStatus().equals(CommonConstants.ALERT_STATUS_PENDING) && existingAlert.getTriggerTimes() >= requiredTimes) {
|
if (existingAlert.getStatus().equals(CommonConstants.ALERT_STATUS_PENDING) && existingAlert.getTriggerTimes() >= requiredTimes) {
|
||||||
// Reached trigger times threshold, change to firing status
|
// Reached trigger times threshold, change to firing status
|
||||||
alarmCacheManager.removePending(fingerprint);
|
alarmCacheManager.removePending(defineId, fingerprint);
|
||||||
existingAlert.setStatus(CommonConstants.ALERT_STATUS_FIRING);
|
existingAlert.setStatus(CommonConstants.ALERT_STATUS_FIRING);
|
||||||
alarmCacheManager.putFiring(fingerprint, existingAlert);
|
alarmCacheManager.putFiring(defineId, fingerprint, existingAlert);
|
||||||
alarmCommonReduce.reduceAndSendAlarm(existingAlert.clone());
|
alarmCommonReduce.reduceAndSendAlarm(existingAlert.clone());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleRecoveredAlert(Map<String, String> fingerprints) {
|
/**
|
||||||
|
* Handle recovered alert
|
||||||
|
*/
|
||||||
|
private void handleRecoveredAlert(Long defineId, Map<String, String> fingerprints) {
|
||||||
String fingerprint = AlertUtil.calculateFingerprint(fingerprints);
|
String fingerprint = AlertUtil.calculateFingerprint(fingerprints);
|
||||||
SingleAlert firingAlert = alarmCacheManager.removeFiring(fingerprint);
|
SingleAlert firingAlert = alarmCacheManager.removeFiring(defineId, fingerprint);
|
||||||
if (firingAlert != null) {
|
if (firingAlert != null) {
|
||||||
// todo consider multi times to tig for resolved alert
|
|
||||||
firingAlert.setTriggerTimes(1);
|
firingAlert.setTriggerTimes(1);
|
||||||
firingAlert.setEndAt(System.currentTimeMillis());
|
firingAlert.setEndAt(System.currentTimeMillis());
|
||||||
firingAlert.setStatus(CommonConstants.ALERT_STATUS_RESOLVED);
|
firingAlert.setStatus(CommonConstants.ALERT_STATUS_RESOLVED);
|
||||||
alarmCommonReduce.reduceAndSendAlarm(firingAlert.clone());
|
alarmCommonReduce.reduceAndSendAlarm(firingAlert.clone());
|
||||||
}
|
}
|
||||||
alarmCacheManager.removePending(fingerprint);
|
alarmCacheManager.removePending(defineId, fingerprint);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
+22
-8
@@ -15,10 +15,13 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.apache.hertzbeat.alert.calculate;
|
package org.apache.hertzbeat.alert.calculate.periodic;
|
||||||
|
|
||||||
import static org.apache.hertzbeat.common.constants.CommonConstants.ALERT_THRESHOLD_TYPE_PERIODIC;
|
import static org.apache.hertzbeat.common.constants.CommonConstants.LOG_ALERT_THRESHOLD_TYPE_PERIODIC;
|
||||||
|
import static org.apache.hertzbeat.common.constants.CommonConstants.METRIC_ALERT_THRESHOLD_TYPE_PERIODIC;
|
||||||
import com.google.common.util.concurrent.ThreadFactoryBuilder;
|
import com.google.common.util.concurrent.ThreadFactoryBuilder;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
@@ -39,13 +42,15 @@ import org.apache.hertzbeat.common.entity.alerter.AlertDefine;
|
|||||||
@Component
|
@Component
|
||||||
public class PeriodicAlertRuleScheduler implements CommandLineRunner {
|
public class PeriodicAlertRuleScheduler implements CommandLineRunner {
|
||||||
|
|
||||||
private final PeriodicAlertCalculator calculator;
|
private final MetricsPeriodicAlertCalculator metricsCalculator;
|
||||||
|
private final LogPeriodicAlertCalculator logCalculator;
|
||||||
private final AlertDefineDao alertDefineDao;
|
private final AlertDefineDao alertDefineDao;
|
||||||
private final ScheduledExecutorService scheduledExecutor;
|
private final ScheduledExecutorService scheduledExecutor;
|
||||||
private final Map<Long, ScheduledFuture<?>> scheduledFutures;
|
private final Map<Long, ScheduledFuture<?>> scheduledFutures;
|
||||||
|
|
||||||
public PeriodicAlertRuleScheduler(PeriodicAlertCalculator calculator, AlertDefineDao alertDefineDao) {
|
public PeriodicAlertRuleScheduler(MetricsPeriodicAlertCalculator metricsCalculator, LogPeriodicAlertCalculator logCalculator, AlertDefineDao alertDefineDao) {
|
||||||
this.calculator = calculator;
|
this.metricsCalculator = metricsCalculator;
|
||||||
|
this.logCalculator = logCalculator;
|
||||||
this.alertDefineDao = alertDefineDao;
|
this.alertDefineDao = alertDefineDao;
|
||||||
ThreadFactory threadFactory = new ThreadFactoryBuilder()
|
ThreadFactory threadFactory = new ThreadFactoryBuilder()
|
||||||
.setUncaughtExceptionHandler((thread, throwable) -> {
|
.setUncaughtExceptionHandler((thread, throwable) -> {
|
||||||
@@ -76,9 +81,14 @@ public class PeriodicAlertRuleScheduler implements CommandLineRunner {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
cancelSchedule(rule.getId());
|
cancelSchedule(rule.getId());
|
||||||
if (rule.getType().equals(ALERT_THRESHOLD_TYPE_PERIODIC)) {
|
if (rule.getType().equals(METRIC_ALERT_THRESHOLD_TYPE_PERIODIC)
|
||||||
|
|| rule.getType().equals(LOG_ALERT_THRESHOLD_TYPE_PERIODIC)) {
|
||||||
ScheduledFuture<?> future = scheduledExecutor.scheduleAtFixedRate(() -> {
|
ScheduledFuture<?> future = scheduledExecutor.scheduleAtFixedRate(() -> {
|
||||||
calculator.calculate(rule);
|
if (rule.getType().equals(METRIC_ALERT_THRESHOLD_TYPE_PERIODIC)) {
|
||||||
|
metricsCalculator.calculate(rule);
|
||||||
|
} else if (rule.getType().equals(LOG_ALERT_THRESHOLD_TYPE_PERIODIC)) {
|
||||||
|
logCalculator.calculate(rule);
|
||||||
|
}
|
||||||
}, 0, rule.getPeriod(), java.util.concurrent.TimeUnit.SECONDS);
|
}, 0, rule.getPeriod(), java.util.concurrent.TimeUnit.SECONDS);
|
||||||
scheduledFutures.put(rule.getId(), future);
|
scheduledFutures.put(rule.getId(), future);
|
||||||
}
|
}
|
||||||
@@ -87,7 +97,11 @@ public class PeriodicAlertRuleScheduler implements CommandLineRunner {
|
|||||||
@Override
|
@Override
|
||||||
public void run(String... args) throws Exception {
|
public void run(String... args) throws Exception {
|
||||||
log.info("Starting periodic alert rule scheduler...");
|
log.info("Starting periodic alert rule scheduler...");
|
||||||
List<AlertDefine> periodicRules = alertDefineDao.findAlertDefinesByTypeAndEnableTrue(ALERT_THRESHOLD_TYPE_PERIODIC);
|
List<AlertDefine> metricsPeriodicRules = alertDefineDao.findAlertDefinesByTypeAndEnableTrue(METRIC_ALERT_THRESHOLD_TYPE_PERIODIC);
|
||||||
|
List<AlertDefine> logPeriodicRules = alertDefineDao.findAlertDefinesByTypeAndEnableTrue(LOG_ALERT_THRESHOLD_TYPE_PERIODIC);
|
||||||
|
List<AlertDefine> periodicRules = new ArrayList<>(metricsPeriodicRules.size() + logPeriodicRules.size());
|
||||||
|
periodicRules.addAll(metricsPeriodicRules);
|
||||||
|
periodicRules.addAll(logPeriodicRules);
|
||||||
for (AlertDefine rule : periodicRules) {
|
for (AlertDefine rule : periodicRules) {
|
||||||
updateSchedule(rule);
|
updateSchedule(rule);
|
||||||
}
|
}
|
||||||
+86
-80
@@ -15,23 +15,13 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.apache.hertzbeat.alert.calculate;
|
package org.apache.hertzbeat.alert.calculate.realtime;
|
||||||
|
|
||||||
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Objects;
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.regex.Matcher;
|
|
||||||
import java.util.regex.Pattern;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.commons.jexl3.JexlException;
|
|
||||||
import org.apache.commons.jexl3.JexlExpression;
|
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.apache.hertzbeat.alert.AlerterWorkerPool;
|
import org.apache.hertzbeat.alert.AlerterWorkerPool;
|
||||||
|
import org.apache.hertzbeat.alert.calculate.AlarmCacheManager;
|
||||||
|
import org.apache.hertzbeat.alert.calculate.JexlExprCalculator;
|
||||||
import org.apache.hertzbeat.alert.dao.SingleAlertDao;
|
import org.apache.hertzbeat.alert.dao.SingleAlertDao;
|
||||||
import org.apache.hertzbeat.alert.reduce.AlarmCommonReduce;
|
import org.apache.hertzbeat.alert.reduce.AlarmCommonReduce;
|
||||||
import org.apache.hertzbeat.alert.service.AlertDefineService;
|
import org.apache.hertzbeat.alert.service.AlertDefineService;
|
||||||
@@ -42,21 +32,33 @@ import org.apache.hertzbeat.common.entity.alerter.AlertDefine;
|
|||||||
import org.apache.hertzbeat.common.entity.alerter.SingleAlert;
|
import org.apache.hertzbeat.common.entity.alerter.SingleAlert;
|
||||||
import org.apache.hertzbeat.common.entity.message.CollectRep;
|
import org.apache.hertzbeat.common.entity.message.CollectRep;
|
||||||
import org.apache.hertzbeat.common.queue.CommonDataQueue;
|
import org.apache.hertzbeat.common.queue.CommonDataQueue;
|
||||||
|
import org.apache.hertzbeat.common.support.exception.CommonDataQueueUnknownException;
|
||||||
|
import org.apache.hertzbeat.common.util.BackoffUtils;
|
||||||
import org.apache.hertzbeat.common.util.CommonUtil;
|
import org.apache.hertzbeat.common.util.CommonUtil;
|
||||||
import org.apache.hertzbeat.common.util.JexlExpressionRunner;
|
import org.apache.hertzbeat.common.util.ExponentialBackoff;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import org.springframework.util.CollectionUtils;
|
import org.springframework.util.CollectionUtils;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculate alarms based on the alarm definition rules and collected data
|
* Calculate alarms based on the alarm definition rules and collected data
|
||||||
*/
|
*/
|
||||||
@Component
|
@Component
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class RealTimeAlertCalculator {
|
public class MetricsRealTimeAlertCalculator {
|
||||||
|
|
||||||
private static final int CALCULATE_THREADS = 3;
|
private static final int CALCULATE_THREADS = 3;
|
||||||
|
|
||||||
private static final String KEY_INSTANCE = "__instance__";
|
private static final String KEY_INSTANCE = "__instance__";
|
||||||
private static final String KEY_INSTANCE_NAME = "__instancename__";
|
private static final String KEY_INSTANCE_NAME = "__instancename__";
|
||||||
private static final String KEY_INSTANCE_HOST = "__instancehost__";
|
private static final String KEY_INSTANCE_HOST = "__instancehost__";
|
||||||
@@ -81,47 +83,65 @@ public class RealTimeAlertCalculator {
|
|||||||
private final AlertDefineService alertDefineService;
|
private final AlertDefineService alertDefineService;
|
||||||
private final AlarmCommonReduce alarmCommonReduce;
|
private final AlarmCommonReduce alarmCommonReduce;
|
||||||
private final AlarmCacheManager alarmCacheManager;
|
private final AlarmCacheManager alarmCacheManager;
|
||||||
|
private final JexlExprCalculator jexlExprCalculator;
|
||||||
|
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
public RealTimeAlertCalculator(AlerterWorkerPool workerPool, CommonDataQueue dataQueue,
|
public MetricsRealTimeAlertCalculator(AlerterWorkerPool workerPool, CommonDataQueue dataQueue,
|
||||||
AlertDefineService alertDefineService, SingleAlertDao singleAlertDao,
|
AlertDefineService alertDefineService, SingleAlertDao singleAlertDao,
|
||||||
AlarmCommonReduce alarmCommonReduce, AlarmCacheManager alarmCacheManager) {
|
AlarmCommonReduce alarmCommonReduce, AlarmCacheManager alarmCacheManager,
|
||||||
this(workerPool, dataQueue, alertDefineService, singleAlertDao, alarmCommonReduce, alarmCacheManager, true);
|
JexlExprCalculator jexlExprCalculator) {
|
||||||
|
this(workerPool, dataQueue, alertDefineService, singleAlertDao, alarmCommonReduce, alarmCacheManager, jexlExprCalculator, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor for RealTimeAlertCalculator with a toggle to control whether to start alert calculation threads.
|
* Constructor for MetricsRealTimeAlertCalculator with a toggle to control whether to start alert calculation threads.
|
||||||
*
|
*
|
||||||
* @param workerPool The worker pool used for concurrent alert calculation.
|
* @param workerPool The worker pool used for concurrent alert calculation.
|
||||||
* @param dataQueue The queue from which metric data is pulled and pushed.
|
* @param dataQueue The queue from which metric data is pulled and pushed.
|
||||||
* @param alertDefineService The service providing alert definition rules.
|
* @param alertDefineService The service providing alert definition rules.
|
||||||
* @param singleAlertDao The DAO for fetching persisted alert states from storage.
|
* @param singleAlertDao The DAO for fetching persisted alert states from storage.
|
||||||
* @param alarmCommonReduce The component responsible for reducing and sending alerts.
|
* @param alarmCommonReduce The component responsible for reducing and sending alerts.
|
||||||
|
* @param alarmCacheManager The cache manager for managing alert states.
|
||||||
* @param start If true, the alert calculation threads will start automatically;
|
* @param start If true, the alert calculation threads will start automatically;
|
||||||
* set to false to disable thread start (useful for unit testing).
|
* set to false to disable thread start (useful for unit testing).
|
||||||
*/
|
*/
|
||||||
public RealTimeAlertCalculator(AlerterWorkerPool workerPool, CommonDataQueue dataQueue,
|
public MetricsRealTimeAlertCalculator(AlerterWorkerPool workerPool, CommonDataQueue dataQueue,
|
||||||
AlertDefineService alertDefineService, SingleAlertDao singleAlertDao,
|
AlertDefineService alertDefineService, SingleAlertDao singleAlertDao,
|
||||||
AlarmCommonReduce alarmCommonReduce, AlarmCacheManager alarmCacheManager, boolean start) {
|
AlarmCommonReduce alarmCommonReduce, AlarmCacheManager alarmCacheManager,
|
||||||
|
JexlExprCalculator jexlExprCalculator, boolean start) {
|
||||||
this.workerPool = workerPool;
|
this.workerPool = workerPool;
|
||||||
this.dataQueue = dataQueue;
|
this.dataQueue = dataQueue;
|
||||||
this.alarmCommonReduce = alarmCommonReduce;
|
this.alarmCommonReduce = alarmCommonReduce;
|
||||||
this.alertDefineService = alertDefineService;
|
this.alertDefineService = alertDefineService;
|
||||||
this.alarmCacheManager = alarmCacheManager;
|
this.alarmCacheManager = alarmCacheManager;
|
||||||
|
this.jexlExprCalculator = jexlExprCalculator;
|
||||||
if (start) {
|
if (start) {
|
||||||
startCalculate();
|
startCalculate();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start the alert calculation threads
|
||||||
|
*/
|
||||||
public void startCalculate() {
|
public void startCalculate() {
|
||||||
Runnable runnable = () -> {
|
Runnable runnable = () -> {
|
||||||
|
ExponentialBackoff backoff = new ExponentialBackoff(50L, 1000L);
|
||||||
while (!Thread.currentThread().isInterrupted()) {
|
while (!Thread.currentThread().isInterrupted()) {
|
||||||
try {
|
try {
|
||||||
CollectRep.MetricsData metricsData = dataQueue.pollMetricsDataToAlerter();
|
CollectRep.MetricsData metricsData = dataQueue.pollMetricsDataToAlerter();
|
||||||
|
if (metricsData == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
backoff.reset();
|
||||||
calculate(metricsData);
|
calculate(metricsData);
|
||||||
dataQueue.sendMetricsDataToStorage(metricsData);
|
dataQueue.sendMetricsDataToStorage(metricsData);
|
||||||
} catch (InterruptedException ignored) {
|
} catch (InterruptedException ignored) {
|
||||||
Thread.currentThread().interrupt();
|
Thread.currentThread().interrupt();
|
||||||
|
} catch (CommonDataQueueUnknownException ue) {
|
||||||
|
if (!BackoffUtils.shouldContinueAfterBackoff(backoff)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("calculate alarm error: {}.", e.getMessage(), e);
|
log.error("calculate alarm error: {}.", e.getMessage(), e);
|
||||||
}
|
}
|
||||||
@@ -132,18 +152,21 @@ public class RealTimeAlertCalculator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void calculate(CollectRep.MetricsData metricsData) {
|
protected void calculate(CollectRep.MetricsData metricsData) {
|
||||||
long currentTimeMilli = System.currentTimeMillis();
|
long currentTimeMilli = System.currentTimeMillis();
|
||||||
String instance = String.valueOf(metricsData.getId());
|
String instance = String.valueOf(metricsData.getId());
|
||||||
String instanceName = metricsData.getInstanceName();
|
String instanceName = metricsData.getInstanceName();
|
||||||
String instanceHost = metricsData.getInstanceHost();
|
String instanceHost = metricsData.getInstanceHost();
|
||||||
String app = metricsData.getApp();
|
String app = metricsData.getApp();
|
||||||
String metrics = metricsData.getMetrics();
|
String metrics = metricsData.getMetrics();
|
||||||
|
if ((CommonConstants.PROMETHEUS_APP_PREFIX + instanceName).equals(metricsData.getApp())) {
|
||||||
|
app = CommonConstants.PROMETHEUS;
|
||||||
|
}
|
||||||
int priority = metricsData.getPriority();
|
int priority = metricsData.getPriority();
|
||||||
int code = metricsData.getCode().getNumber();
|
int code = metricsData.getCode().getNumber();
|
||||||
Map<String, String> labels = metricsData.getLabels();
|
Map<String, String> labels = metricsData.getLabels();
|
||||||
Map<String, String> annotations = metricsData.getAnnotations();
|
Map<String, String> annotations = metricsData.getAnnotations();
|
||||||
List<AlertDefine> thresholds = this.alertDefineService.getRealTimeAlertDefines();
|
List<AlertDefine> thresholds = this.alertDefineService.getMetricsRealTimeAlertDefines();
|
||||||
// Filter thresholds by app, metrics, labels and instance
|
// Filter thresholds by app, metrics, labels and instance
|
||||||
thresholds = filterThresholdsByAppAndMetrics(thresholds, app, metrics, labels, instance, priority);
|
thresholds = filterThresholdsByAppAndMetrics(thresholds, app, metrics, labels, instance, priority);
|
||||||
if (thresholds.isEmpty()) {
|
if (thresholds.isEmpty()) {
|
||||||
@@ -180,9 +203,11 @@ public class RealTimeAlertCalculator {
|
|||||||
if (StringUtils.isBlank(expr)) {
|
if (StringUtils.isBlank(expr)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
Long defineId = define.getId();
|
||||||
Map<String, String> commonFingerPrints = new HashMap<>(8);
|
Map<String, String> commonFingerPrints = new HashMap<>(8);
|
||||||
commonFingerPrints.put(CommonConstants.LABEL_INSTANCE, instance);
|
commonFingerPrints.put(CommonConstants.LABEL_INSTANCE, instance);
|
||||||
// here use the alert name as finger, not care the alert name may be changed
|
// here use the alert name as finger, not care the alert name may be changed
|
||||||
|
commonFingerPrints.put(CommonConstants.LABEL_DEFINE_ID, String.valueOf(define.getId()));
|
||||||
commonFingerPrints.put(CommonConstants.LABEL_ALERT_NAME, define.getName());
|
commonFingerPrints.put(CommonConstants.LABEL_ALERT_NAME, define.getName());
|
||||||
commonFingerPrints.put(CommonConstants.LABEL_INSTANCE_NAME, instanceName);
|
commonFingerPrints.put(CommonConstants.LABEL_INSTANCE_NAME, instanceName);
|
||||||
commonFingerPrints.put(CommonConstants.LABEL_INSTANCE_HOST, instanceHost);
|
commonFingerPrints.put(CommonConstants.LABEL_INSTANCE_HOST, instanceHost);
|
||||||
@@ -193,13 +218,13 @@ public class RealTimeAlertCalculator {
|
|||||||
{
|
{
|
||||||
// trigger the expr before the metrics data, due the available up down or others
|
// trigger the expr before the metrics data, due the available up down or others
|
||||||
try {
|
try {
|
||||||
boolean match = execAlertExpression(fieldValueMap, expr, true);
|
boolean match = jexlExprCalculator.execAlertExpression(fieldValueMap, expr, true);
|
||||||
try {
|
try {
|
||||||
if (match) {
|
if (match) {
|
||||||
// If the threshold rule matches, the number of times the threshold has been triggered is determined and an alarm is triggered
|
// If the threshold rule matches, the number of times the threshold has been triggered is determined and an alarm is triggered
|
||||||
afterThresholdRuleMatch(currentTimeMilli, commonFingerPrints, fieldValueMap, define, annotations);
|
afterThresholdRuleMatch(defineId, currentTimeMilli, commonFingerPrints, fieldValueMap, define, annotations);
|
||||||
} else {
|
} else {
|
||||||
handleRecoveredAlert(commonFingerPrints);
|
handleRecoveredAlert(defineId, commonFingerPrints);
|
||||||
}
|
}
|
||||||
// if this threshold pre compile success, ignore blew
|
// if this threshold pre compile success, ignore blew
|
||||||
continue;
|
continue;
|
||||||
@@ -220,11 +245,11 @@ public class RealTimeAlertCalculator {
|
|||||||
fingerPrints.putAll(commonFingerPrints);
|
fingerPrints.putAll(commonFingerPrints);
|
||||||
for (int index = 0; index < valueRow.getColumnsList().size(); index++) {
|
for (int index = 0; index < valueRow.getColumnsList().size(); index++) {
|
||||||
String valueStr = valueRow.getColumns(index);
|
String valueStr = valueRow.getColumns(index);
|
||||||
|
final CollectRep.Field field = fields.get(index);
|
||||||
if (CommonConstants.NULL_VALUE.equals(valueStr)) {
|
if (CommonConstants.NULL_VALUE.equals(valueStr)) {
|
||||||
|
fieldValueMap.put(field.getName(), null);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
final CollectRep.Field field = fields.get(index);
|
|
||||||
final int fieldType = field.getType();
|
final int fieldType = field.getType();
|
||||||
|
|
||||||
if (fieldType == CommonConstants.TYPE_NUMBER) {
|
if (fieldType == CommonConstants.TYPE_NUMBER) {
|
||||||
@@ -248,12 +273,12 @@ public class RealTimeAlertCalculator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
boolean match = execAlertExpression(fieldValueMap, expr, false);
|
boolean match = jexlExprCalculator.execAlertExpression(fieldValueMap, expr, false);
|
||||||
try {
|
try {
|
||||||
if (match) {
|
if (match) {
|
||||||
afterThresholdRuleMatch(currentTimeMilli, fingerPrints, fieldValueMap, define, annotations);
|
afterThresholdRuleMatch(defineId, currentTimeMilli, fingerPrints, fieldValueMap, define, annotations);
|
||||||
} else {
|
} else {
|
||||||
handleRecoveredAlert(fingerPrints);
|
handleRecoveredAlert(defineId, fingerPrints);
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error(e.getMessage(), e);
|
log.error(e.getMessage(), e);
|
||||||
@@ -269,8 +294,9 @@ public class RealTimeAlertCalculator {
|
|||||||
* @param thresholds Alert definitions to filter
|
* @param thresholds Alert definitions to filter
|
||||||
* @param app Current app name
|
* @param app Current app name
|
||||||
* @param metrics Current metrics name
|
* @param metrics Current metrics name
|
||||||
|
* @param labels Current labels
|
||||||
* @param instance Current instance id
|
* @param instance Current instance id
|
||||||
* @param priority Current priority
|
* @param priority Current priority
|
||||||
* @return Filtered alert definitions
|
* @return Filtered alert definitions
|
||||||
*/
|
*/
|
||||||
public List<AlertDefine> filterThresholdsByAppAndMetrics(List<AlertDefine> thresholds, String app, String metrics, Map<String, String> labels, String instance, int priority) {
|
public List<AlertDefine> filterThresholdsByAppAndMetrics(List<AlertDefine> thresholds, String app, String metrics, Map<String, String> labels, String instance, int priority) {
|
||||||
@@ -286,7 +312,7 @@ public class RealTimeAlertCalculator {
|
|||||||
if (!appMatcher.find() || !app.equals(appMatcher.group(1))) {
|
if (!appMatcher.find() || !app.equals(appMatcher.group(1))) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract and check available - required
|
// Extract and check available - required
|
||||||
if (priority != 0) {
|
if (priority != 0) {
|
||||||
Matcher availableMatcher = AVAILABLE_PATTERN.matcher(expr);
|
Matcher availableMatcher = AVAILABLE_PATTERN.matcher(expr);
|
||||||
@@ -308,7 +334,7 @@ public class RealTimeAlertCalculator {
|
|||||||
if (!instanceMatcher.find() && !labelMatcher.find()) {
|
if (!instanceMatcher.find() && !labelMatcher.find()) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset matcher to check all instances
|
// Reset matcher to check all instances
|
||||||
instanceMatcher.reset();
|
instanceMatcher.reset();
|
||||||
labelMatcher.reset();
|
labelMatcher.reset();
|
||||||
@@ -331,9 +357,12 @@ public class RealTimeAlertCalculator {
|
|||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleRecoveredAlert(Map<String, String> fingerprints) {
|
/**
|
||||||
|
* Handle recovered alert
|
||||||
|
*/
|
||||||
|
private void handleRecoveredAlert(Long defineId, Map<String, String> fingerprints) {
|
||||||
String fingerprint = AlertUtil.calculateFingerprint(fingerprints);
|
String fingerprint = AlertUtil.calculateFingerprint(fingerprints);
|
||||||
SingleAlert firingAlert = alarmCacheManager.removeFiring(fingerprint);
|
SingleAlert firingAlert = alarmCacheManager.removeFiring(defineId, fingerprint);
|
||||||
if (firingAlert != null) {
|
if (firingAlert != null) {
|
||||||
// todo consider multi times to tig for resolved alert
|
// todo consider multi times to tig for resolved alert
|
||||||
firingAlert.setTriggerTimes(1);
|
firingAlert.setTriggerTimes(1);
|
||||||
@@ -341,13 +370,19 @@ public class RealTimeAlertCalculator {
|
|||||||
firingAlert.setStatus(CommonConstants.ALERT_STATUS_RESOLVED);
|
firingAlert.setStatus(CommonConstants.ALERT_STATUS_RESOLVED);
|
||||||
alarmCommonReduce.reduceAndSendAlarm(firingAlert.clone());
|
alarmCommonReduce.reduceAndSendAlarm(firingAlert.clone());
|
||||||
}
|
}
|
||||||
alarmCacheManager.removePending(fingerprint);
|
alarmCacheManager.removePending(defineId, fingerprint);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void afterThresholdRuleMatch(long currentTimeMilli, Map<String, String> fingerPrints,
|
|
||||||
Map<String, Object> fieldValueMap, AlertDefine define, Map<String, String> annotations) {
|
/**
|
||||||
|
* Handle alert after threshold rule match
|
||||||
|
*/
|
||||||
|
private void afterThresholdRuleMatch(long defineId, long currentTimeMilli, Map<String, String> fingerPrints,
|
||||||
|
Map<String, Object> fieldValueMap, AlertDefine define,
|
||||||
|
Map<String, String> annotations) {
|
||||||
|
// fingerprint for the padding cache
|
||||||
String fingerprint = AlertUtil.calculateFingerprint(fingerPrints);
|
String fingerprint = AlertUtil.calculateFingerprint(fingerPrints);
|
||||||
SingleAlert existingAlert = alarmCacheManager.getPending(fingerprint);
|
SingleAlert existingAlert = alarmCacheManager.getPending(defineId, fingerprint);
|
||||||
fieldValueMap.putAll(define.getLabels());
|
fieldValueMap.putAll(define.getLabels());
|
||||||
int requiredTimes = define.getTimes() == null ? 1 : define.getTimes();
|
int requiredTimes = define.getTimes() == null ? 1 : define.getTimes();
|
||||||
if (existingAlert == null) {
|
if (existingAlert == null) {
|
||||||
@@ -371,65 +406,36 @@ public class RealTimeAlertCalculator {
|
|||||||
// render var content in content
|
// render var content in content
|
||||||
.content(AlertTemplateUtil.render(define.getTemplate(), fieldValueMap))
|
.content(AlertTemplateUtil.render(define.getTemplate(), fieldValueMap))
|
||||||
.status(CommonConstants.ALERT_STATUS_PENDING)
|
.status(CommonConstants.ALERT_STATUS_PENDING)
|
||||||
.triggerTimes(1)
|
.triggerTimes(1)
|
||||||
.startAt(currentTimeMilli)
|
.startAt(currentTimeMilli)
|
||||||
.activeAt(currentTimeMilli)
|
.activeAt(currentTimeMilli)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
// If required trigger times is 1, set to firing status directly
|
// If required trigger times is 1, set to firing status directly
|
||||||
if (requiredTimes <= 1) {
|
if (requiredTimes <= 1) {
|
||||||
newAlert.setStatus(CommonConstants.ALERT_STATUS_FIRING);
|
newAlert.setStatus(CommonConstants.ALERT_STATUS_FIRING);
|
||||||
alarmCacheManager.putFiring(fingerprint, newAlert);
|
alarmCacheManager.putFiring(defineId, fingerprint, newAlert);
|
||||||
alarmCommonReduce.reduceAndSendAlarm(newAlert.clone());
|
alarmCommonReduce.reduceAndSendAlarm(newAlert.clone());
|
||||||
} else {
|
} else {
|
||||||
// Otherwise put into pending queue first
|
// Otherwise put into pending queue first
|
||||||
alarmCacheManager.putPending(fingerprint, newAlert);
|
alarmCacheManager.putPending(define.getId(), fingerprint, newAlert);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Update existing alert
|
// Update existing alert
|
||||||
existingAlert.setTriggerTimes(existingAlert.getTriggerTimes() + 1);
|
existingAlert.setTriggerTimes(existingAlert.getTriggerTimes() + 1);
|
||||||
existingAlert.setActiveAt(currentTimeMilli);
|
existingAlert.setActiveAt(currentTimeMilli);
|
||||||
|
|
||||||
// Check if required trigger times reached
|
// Check if required trigger times reached
|
||||||
if (existingAlert.getStatus().equals(CommonConstants.ALERT_STATUS_PENDING) && existingAlert.getTriggerTimes() >= requiredTimes) {
|
if (existingAlert.getStatus().equals(CommonConstants.ALERT_STATUS_PENDING) && existingAlert.getTriggerTimes() >= requiredTimes) {
|
||||||
// Reached trigger times threshold, change to firing status
|
// Reached trigger times threshold, change to firing status
|
||||||
alarmCacheManager.removePending(fingerprint);
|
alarmCacheManager.removePending(defineId, fingerprint);
|
||||||
existingAlert.setStatus(CommonConstants.ALERT_STATUS_FIRING);
|
existingAlert.setStatus(CommonConstants.ALERT_STATUS_FIRING);
|
||||||
alarmCacheManager.putFiring(fingerprint, existingAlert);
|
alarmCacheManager.putFiring(defineId, fingerprint, existingAlert);
|
||||||
alarmCommonReduce.reduceAndSendAlarm(existingAlert.clone());
|
alarmCommonReduce.reduceAndSendAlarm(existingAlert.clone());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean execAlertExpression(Map<String, Object> fieldValueMap, String expr, boolean ignoreJexlException) {
|
|
||||||
Boolean match;
|
|
||||||
JexlExpression expression;
|
|
||||||
try {
|
|
||||||
expression = JexlExpressionRunner.compile(expr);
|
|
||||||
} catch (JexlException jexlException) {
|
|
||||||
log.warn("Alarm Rule: {} Compile Error: {}.", expr, jexlException.getMessage());
|
|
||||||
throw jexlException;
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("Alarm Rule: {} Unknown Error: {}.", expr, e.getMessage());
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
match = (Boolean) JexlExpressionRunner.evaluate(expression, fieldValueMap);
|
|
||||||
} catch (JexlException jexlException) {
|
|
||||||
if (ignoreJexlException) {
|
|
||||||
log.debug("Alarm Rule: {} Run Error: {}.", expr, jexlException.getMessage());
|
|
||||||
} else {
|
|
||||||
log.error("Alarm Rule: {} Run Error: {}.", expr, jexlException.getMessage());
|
|
||||||
}
|
|
||||||
throw jexlException;
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("Alarm Rule: {} Unknown Error: {}.", expr, e.getMessage());
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
return match != null && match;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Set<String> kvLabelsToKvStringSet(Map<String, String> labels) {
|
private Set<String> kvLabelsToKvStringSet(Map<String, String> labels) {
|
||||||
if (labels == null || labels.isEmpty()) {
|
if (labels == null || labels.isEmpty()) {
|
||||||
return Collections.singleton("");
|
return Collections.singleton("");
|
||||||
+151
@@ -0,0 +1,151 @@
|
|||||||
|
/*
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||||
|
* contributor license agreements. See the NOTICE file distributed with
|
||||||
|
* this work for additional information regarding copyright ownership.
|
||||||
|
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||||
|
* (the "License"); you may not use this file except in compliance with
|
||||||
|
* the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.apache.hertzbeat.alert.calculate.realtime;
|
||||||
|
|
||||||
|
import com.google.common.util.concurrent.ThreadFactoryBuilder;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.hertzbeat.alert.calculate.realtime.window.LogWorker;
|
||||||
|
import org.apache.hertzbeat.alert.calculate.realtime.window.TimeService;
|
||||||
|
import org.apache.hertzbeat.common.entity.log.LogEntry;
|
||||||
|
import org.apache.hertzbeat.common.queue.CommonDataQueue;
|
||||||
|
import org.apache.hertzbeat.common.support.exception.CommonDataQueueUnknownException;
|
||||||
|
import org.apache.hertzbeat.common.util.BackoffUtils;
|
||||||
|
import org.apache.hertzbeat.common.util.ExponentialBackoff;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import javax.annotation.PostConstruct;
|
||||||
|
import javax.annotation.PreDestroy;
|
||||||
|
import java.util.concurrent.LinkedBlockingQueue;
|
||||||
|
import java.util.concurrent.ThreadFactory;
|
||||||
|
import java.util.concurrent.ThreadPoolExecutor;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* WindowedLogRealTimeAlertCalculator - Single entry point for log stream processing
|
||||||
|
* Responsible for:
|
||||||
|
* 1. Reading from original log stream
|
||||||
|
* 2. Extracting event timestamps
|
||||||
|
* 3. Maintaining maxTimestamp for watermark generation
|
||||||
|
* 4. Distributing logs to workers
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
@Slf4j
|
||||||
|
public class WindowedLogRealTimeAlertCalculator implements Runnable {
|
||||||
|
|
||||||
|
private static final int CALCULATE_THREADS = 3;
|
||||||
|
|
||||||
|
private final CommonDataQueue dataQueue;
|
||||||
|
private final TimeService timeService;
|
||||||
|
private ThreadPoolExecutor dispatcherExecutor;
|
||||||
|
private final LogWorker logWorker;
|
||||||
|
|
||||||
|
public WindowedLogRealTimeAlertCalculator(CommonDataQueue dataQueue, TimeService timeService, LogWorker logWorker) {
|
||||||
|
this.dataQueue = dataQueue;
|
||||||
|
this.timeService = timeService;
|
||||||
|
this.logWorker = logWorker;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
ExponentialBackoff backoff = new ExponentialBackoff(50L, 1000L);
|
||||||
|
while (!Thread.currentThread().isInterrupted()) {
|
||||||
|
try {
|
||||||
|
LogEntry logEntry = dataQueue.pollLogEntry();
|
||||||
|
if (logEntry == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
backoff.reset();
|
||||||
|
processLogEntry(logEntry);
|
||||||
|
dataQueue.sendLogEntryToStorage(logEntry);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
break;
|
||||||
|
} catch (CommonDataQueueUnknownException ue) {
|
||||||
|
if (!BackoffUtils.shouldContinueAfterBackoff(backoff)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Error in log dispatch loop: {}", e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void processLogEntry(LogEntry logEntry) {
|
||||||
|
// Extract event timestamp
|
||||||
|
long eventTimestamp = extractEventTimestamp(logEntry);
|
||||||
|
|
||||||
|
// Check if timestamp is within reasonable range
|
||||||
|
if (!timeService.isValidTimestamp(eventTimestamp)) {
|
||||||
|
log.warn("Dropping log with invalid timestamp: {}", eventTimestamp);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if this is late data based on current watermark
|
||||||
|
if (timeService.isLateData(eventTimestamp)) {
|
||||||
|
log.warn("Dropping late data, timestamp: {}, watermark: {}",
|
||||||
|
eventTimestamp, timeService.getCurrentWatermark());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update max timestamp (only validated timestamps can update watermark)
|
||||||
|
timeService.updateMaxTimestamp(eventTimestamp);
|
||||||
|
logWorker.reduceAndSendLogTask(logEntry);
|
||||||
|
}
|
||||||
|
|
||||||
|
private long extractEventTimestamp(LogEntry logEntry) {
|
||||||
|
if (logEntry.getTimeUnixNano() != null && logEntry.getTimeUnixNano() != 0) {
|
||||||
|
return logEntry.getTimeUnixNano() / 1_000_000; // Convert to milliseconds
|
||||||
|
}
|
||||||
|
if (logEntry.getObservedTimeUnixNano() != null && logEntry.getObservedTimeUnixNano() != 0) {
|
||||||
|
return logEntry.getObservedTimeUnixNano() / 1_000_000; // Convert to milliseconds
|
||||||
|
}
|
||||||
|
return System.currentTimeMillis();
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostConstruct
|
||||||
|
public void start() {
|
||||||
|
ThreadFactory threadFactory = new ThreadFactoryBuilder()
|
||||||
|
.setUncaughtExceptionHandler((thread, throwable) -> {
|
||||||
|
log.error("Alerter workerExecutor has uncaughtException.");
|
||||||
|
log.error(throwable.getMessage(), throwable);
|
||||||
|
})
|
||||||
|
.setDaemon(true)
|
||||||
|
.setNameFormat("log-dispatcher-%d")
|
||||||
|
.build();
|
||||||
|
// Create dispatcher thread executor
|
||||||
|
this.dispatcherExecutor = new ThreadPoolExecutor(
|
||||||
|
CALCULATE_THREADS,
|
||||||
|
CALCULATE_THREADS,
|
||||||
|
10,
|
||||||
|
TimeUnit.SECONDS,
|
||||||
|
new LinkedBlockingQueue<>(),
|
||||||
|
threadFactory,
|
||||||
|
new ThreadPoolExecutor.AbortPolicy()
|
||||||
|
);
|
||||||
|
for (int i = 0; i < CALCULATE_THREADS; i++) {
|
||||||
|
dispatcherExecutor.execute(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreDestroy
|
||||||
|
public void stop() {
|
||||||
|
if (dispatcherExecutor != null) {
|
||||||
|
dispatcherExecutor.shutdownNow();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+317
@@ -0,0 +1,317 @@
|
|||||||
|
/*
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||||
|
* contributor license agreements. See the NOTICE file distributed with
|
||||||
|
* this work for additional information regarding copyright ownership.
|
||||||
|
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||||
|
* (the "License"); you may not use this file except in compliance with
|
||||||
|
* the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.apache.hertzbeat.alert.calculate.realtime.window;
|
||||||
|
|
||||||
|
import com.google.common.util.concurrent.ThreadFactoryBuilder;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.hertzbeat.alert.reduce.AlarmCommonReduce;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import org.apache.hertzbeat.alert.util.AlertTemplateUtil;
|
||||||
|
import org.apache.hertzbeat.common.constants.CommonConstants;
|
||||||
|
import org.apache.hertzbeat.common.entity.alerter.AlertDefine;
|
||||||
|
import org.apache.hertzbeat.common.entity.alerter.SingleAlert;
|
||||||
|
import org.apache.hertzbeat.common.entity.log.LogEntry;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.LinkedBlockingQueue;
|
||||||
|
import java.util.concurrent.ThreadFactory;
|
||||||
|
import java.util.concurrent.ThreadPoolExecutor;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Alarm Evaluator - Final alarm logic trigger
|
||||||
|
* Responsible for:
|
||||||
|
* 1. Receiving closed window data from WindowAggregator
|
||||||
|
* 2. Applying alert logic (count threshold, alert mode)
|
||||||
|
* 3. Generating individual alerts or alert groups
|
||||||
|
* 4. Sending alerts to AlarmCommonReduce
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
@Slf4j
|
||||||
|
public class AlarmEvaluator {
|
||||||
|
|
||||||
|
private static final String WINDOW_START_TIME = "window_start_time";
|
||||||
|
private static final String WINDOW_END_TIME = "window_end_time";
|
||||||
|
private static final String MATCHING_LOGS_COUNT = "matching_logs_count";
|
||||||
|
|
||||||
|
private final AlarmCommonReduce alarmCommonReduce;
|
||||||
|
private ThreadPoolExecutor workerExecutor;
|
||||||
|
|
||||||
|
public AlarmEvaluator(AlarmCommonReduce alarmCommonReduce) {
|
||||||
|
this.alarmCommonReduce = alarmCommonReduce;
|
||||||
|
initAlarmEvaluator();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void initAlarmEvaluator() {
|
||||||
|
ThreadFactory threadFactory = new ThreadFactoryBuilder()
|
||||||
|
.setUncaughtExceptionHandler((thread, throwable) -> {
|
||||||
|
log.error("alerter-reduce-worker has uncaughtException.");
|
||||||
|
log.error(throwable.getMessage(), throwable);
|
||||||
|
})
|
||||||
|
.setDaemon(true)
|
||||||
|
.setNameFormat("alerter-reduce-worker-%d")
|
||||||
|
.build();
|
||||||
|
workerExecutor = new ThreadPoolExecutor(2,
|
||||||
|
10,
|
||||||
|
10,
|
||||||
|
TimeUnit.SECONDS,
|
||||||
|
new LinkedBlockingQueue<>(),
|
||||||
|
threadFactory,
|
||||||
|
new ThreadPoolExecutor.AbortPolicy());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void sendAndProcessWindowData(WindowAggregator.WindowData windowData) {
|
||||||
|
workerExecutor.execute(processWindowData(windowData));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Runnable processWindowData(WindowAggregator.WindowData windowData) {
|
||||||
|
return () -> {
|
||||||
|
AlertDefine alertDefine = windowData.getAlertDefine();
|
||||||
|
List<MatchingLogEvent> matchingLogs = windowData.getMatchingLogs();
|
||||||
|
|
||||||
|
if (matchingLogs.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if count threshold is met
|
||||||
|
int requiredTimes = alertDefine.getTimes() != null ? alertDefine.getTimes() : 1;
|
||||||
|
if (matchingLogs.size() < requiredTimes) {
|
||||||
|
log.debug("Window {} has {} matching logs, but requires {} times",
|
||||||
|
windowData.getWindowKey(), matchingLogs.size(), requiredTimes);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine alert mode from configuration
|
||||||
|
String alertMode = getAlertMode(alertDefine);
|
||||||
|
|
||||||
|
long currentTime = System.currentTimeMillis();
|
||||||
|
|
||||||
|
switch (alertMode) {
|
||||||
|
case CommonConstants.ALERT_MODE_INDIVIDUAL:
|
||||||
|
// Generate individual alerts for each matching log
|
||||||
|
for (MatchingLogEvent matchingLog : matchingLogs) {
|
||||||
|
generateIndividualAlert(matchingLog, currentTime);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case CommonConstants.ALERT_MODE_GROUP:
|
||||||
|
// Generate a single alert group for all matching logs
|
||||||
|
generateGroupAlert(windowData, matchingLogs, currentTime);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
log.warn("Unknown alert mode for define {}: {}", alertDefine.getName(), alertMode);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private void generateIndividualAlert(MatchingLogEvent matchingLog, long currentTime) {
|
||||||
|
AlertDefine define = matchingLog.getAlertDefine();
|
||||||
|
LogEntry logEntry = matchingLog.getLogEntry();
|
||||||
|
|
||||||
|
Map<String, String> alertLabels = new HashMap<>(8);
|
||||||
|
|
||||||
|
// Create fingerprints for group alert
|
||||||
|
Map<String, String> commonFingerPrints = createCommonFingerprints(define);
|
||||||
|
alertLabels.putAll(commonFingerPrints);
|
||||||
|
// add the log data to commonFingerPrints
|
||||||
|
addLogEntryToMap(logEntry, alertLabels);
|
||||||
|
|
||||||
|
Map<String, Object> fieldValueMap = createFieldValueMap(logEntry, define);
|
||||||
|
Map<String, String> alertAnnotations = createAlertAnnotations(define, fieldValueMap);
|
||||||
|
// Create and send group alert
|
||||||
|
SingleAlert alert = SingleAlert.builder()
|
||||||
|
.labels(alertLabels)
|
||||||
|
.annotations(alertAnnotations)
|
||||||
|
.content(AlertTemplateUtil.render(define.getTemplate(), fieldValueMap))
|
||||||
|
.status(CommonConstants.ALERT_STATUS_FIRING)
|
||||||
|
.triggerTimes(1)
|
||||||
|
.startAt(currentTime)
|
||||||
|
.activeAt(currentTime)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
alarmCommonReduce.reduceAndSendAlarm(alert.clone());
|
||||||
|
|
||||||
|
log.debug("Generated individual alert for define: {}", define.getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void generateGroupAlert(WindowAggregator.WindowData windowData,
|
||||||
|
List<MatchingLogEvent> matchingLogs, long currentTime) {
|
||||||
|
|
||||||
|
List<SingleAlert> alerts = new ArrayList<>(matchingLogs.size());
|
||||||
|
AlertDefine define = windowData.getAlertDefine();
|
||||||
|
|
||||||
|
// Create fingerprints for group alert
|
||||||
|
Map<String, String> commonFingerPrints = createCommonFingerprints(define);
|
||||||
|
|
||||||
|
// Add window information to fingerprints
|
||||||
|
commonFingerPrints.put(WINDOW_START_TIME, String.valueOf(windowData.getStartTime()));
|
||||||
|
commonFingerPrints.put(WINDOW_END_TIME, String.valueOf(windowData.getEndTime()));
|
||||||
|
commonFingerPrints.put(CommonConstants.ALERT_MODE_LABEL, CommonConstants.ALERT_MODE_GROUP);
|
||||||
|
commonFingerPrints.put(MATCHING_LOGS_COUNT, String.valueOf(matchingLogs.size()));
|
||||||
|
|
||||||
|
for (MatchingLogEvent event: matchingLogs) {
|
||||||
|
LogEntry logEntry = event.getLogEntry();
|
||||||
|
|
||||||
|
Map<String, String> alertLabels = new HashMap<>(8);
|
||||||
|
|
||||||
|
alertLabels.putAll(commonFingerPrints);
|
||||||
|
// add the log data to commonFingerPrints
|
||||||
|
addLogEntryToMap(logEntry, alertLabels);
|
||||||
|
|
||||||
|
Map<String, Object> fieldValueMap = createFieldValueMap(logEntry, define);
|
||||||
|
Map<String, String> alertAnnotations = createAlertAnnotations(define, fieldValueMap);
|
||||||
|
// Create and send group alert
|
||||||
|
SingleAlert alert = SingleAlert.builder()
|
||||||
|
.labels(alertLabels)
|
||||||
|
.annotations(alertAnnotations)
|
||||||
|
.content(AlertTemplateUtil.render(define.getTemplate(), fieldValueMap))
|
||||||
|
.status(CommonConstants.ALERT_STATUS_FIRING)
|
||||||
|
.triggerTimes(matchingLogs.size())
|
||||||
|
.startAt(currentTime)
|
||||||
|
.activeAt(currentTime)
|
||||||
|
.build();
|
||||||
|
alerts.add(alert.clone());
|
||||||
|
}
|
||||||
|
alarmCommonReduce.reduceAndSendAlarmGroup(commonFingerPrints, alerts);
|
||||||
|
|
||||||
|
log.debug("Generated group alert for define: {} with {} matching logs",
|
||||||
|
define.getName(), matchingLogs.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getAlertMode(AlertDefine alertDefine) {
|
||||||
|
String mode = null;
|
||||||
|
if (alertDefine.getLabels() != null) {
|
||||||
|
mode = alertDefine.getLabels().get(CommonConstants.ALERT_MODE_LABEL);
|
||||||
|
}
|
||||||
|
if (mode == null || mode.isEmpty()) {
|
||||||
|
return CommonConstants.ALERT_MODE_GROUP; // Default to group mode if not specified
|
||||||
|
} else {
|
||||||
|
return mode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, String> createCommonFingerprints(AlertDefine define) {
|
||||||
|
Map<String, String> fingerprints = new HashMap<>(8);
|
||||||
|
fingerprints.put(CommonConstants.LABEL_ALERT_NAME, define.getName());
|
||||||
|
fingerprints.put(CommonConstants.LABEL_DEFINE_ID, String.valueOf(define.getId()));
|
||||||
|
|
||||||
|
if (define.getLabels() != null) {
|
||||||
|
fingerprints.putAll(define.getLabels());
|
||||||
|
}
|
||||||
|
|
||||||
|
return fingerprints;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, Object> createFieldValueMap(LogEntry logEntry, AlertDefine define) {
|
||||||
|
Map<String, Object> fieldValueMap = new HashMap<>(8);
|
||||||
|
fieldValueMap.put("log", logEntry);
|
||||||
|
|
||||||
|
if (define.getLabels() != null) {
|
||||||
|
fieldValueMap.putAll(define.getLabels());
|
||||||
|
}
|
||||||
|
|
||||||
|
return fieldValueMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, String> createAlertAnnotations(AlertDefine define, Map<String, Object> fieldValueMap) {
|
||||||
|
Map<String, String> annotations = new HashMap<>(8);
|
||||||
|
|
||||||
|
if (define.getAnnotations() != null) {
|
||||||
|
for (Map.Entry<String, String> entry : define.getAnnotations().entrySet()) {
|
||||||
|
annotations.put(entry.getKey(),
|
||||||
|
AlertTemplateUtil.render(entry.getValue(), fieldValueMap));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return annotations;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the content from LogEntry object (except timestamp) to commonFingerPrints
|
||||||
|
*
|
||||||
|
* @param logEntry log entry object
|
||||||
|
* @param context context
|
||||||
|
*/
|
||||||
|
private void addLogEntryToMap(LogEntry logEntry, Map<String, String> context) {
|
||||||
|
// Add basic fields
|
||||||
|
if (logEntry.getSeverityNumber() != null) {
|
||||||
|
context.put("severityNumber", String.valueOf(logEntry.getSeverityNumber()));
|
||||||
|
}
|
||||||
|
if (logEntry.getSeverityText() != null) {
|
||||||
|
context.put("severityText", logEntry.getSeverityText());
|
||||||
|
}
|
||||||
|
if (logEntry.getBody() != null) {
|
||||||
|
context.put("body", String.valueOf(logEntry.getBody()));
|
||||||
|
}
|
||||||
|
if (logEntry.getDroppedAttributesCount() != null) {
|
||||||
|
context.put("droppedAttributesCount", String.valueOf(logEntry.getDroppedAttributesCount()));
|
||||||
|
}
|
||||||
|
if (logEntry.getTraceId() != null) {
|
||||||
|
context.put("traceId", logEntry.getTraceId());
|
||||||
|
}
|
||||||
|
if (logEntry.getSpanId() != null) {
|
||||||
|
context.put("spanId", logEntry.getSpanId());
|
||||||
|
}
|
||||||
|
if (logEntry.getTraceFlags() != null) {
|
||||||
|
context.put("traceFlags", String.valueOf(logEntry.getTraceFlags()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add attributes
|
||||||
|
if (logEntry.getAttributes() != null && !logEntry.getAttributes().isEmpty()) {
|
||||||
|
for (Map.Entry<String, Object> entry : logEntry.getAttributes().entrySet()) {
|
||||||
|
if (entry.getValue() != null) {
|
||||||
|
context.put("attr_" + entry.getKey(), String.valueOf(entry.getValue()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add resource
|
||||||
|
if (logEntry.getResource() != null && !logEntry.getResource().isEmpty()) {
|
||||||
|
for (Map.Entry<String, Object> entry : logEntry.getResource().entrySet()) {
|
||||||
|
if (entry.getValue() != null) {
|
||||||
|
context.put("resource_" + entry.getKey(), String.valueOf(entry.getValue()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add instrumentationScope
|
||||||
|
if (logEntry.getInstrumentationScope() != null) {
|
||||||
|
LogEntry.InstrumentationScope scope = logEntry.getInstrumentationScope();
|
||||||
|
if (scope.getName() != null) {
|
||||||
|
context.put("scope_name", scope.getName());
|
||||||
|
}
|
||||||
|
if (scope.getVersion() != null) {
|
||||||
|
context.put("scope_version", scope.getVersion());
|
||||||
|
}
|
||||||
|
if (scope.getDroppedAttributesCount() != null) {
|
||||||
|
context.put("scope_droppedAttributesCount", String.valueOf(scope.getDroppedAttributesCount()));
|
||||||
|
}
|
||||||
|
if (scope.getAttributes() != null && !scope.getAttributes().isEmpty()) {
|
||||||
|
for (Map.Entry<String, Object> entry : scope.getAttributes().entrySet()) {
|
||||||
|
if (entry.getValue() != null) {
|
||||||
|
context.put("scope_attr_" + entry.getKey(), String.valueOf(entry.getValue()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+123
@@ -0,0 +1,123 @@
|
|||||||
|
/*
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||||
|
* contributor license agreements. See the NOTICE file distributed with
|
||||||
|
* this work for additional information regarding copyright ownership.
|
||||||
|
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||||
|
* (the "License"); you may not use this file except in compliance with
|
||||||
|
* the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.apache.hertzbeat.alert.calculate.realtime.window;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.hertzbeat.alert.AlerterWorkerPool;
|
||||||
|
import org.apache.hertzbeat.alert.calculate.JexlExprCalculator;
|
||||||
|
import org.apache.hertzbeat.alert.service.AlertDefineService;
|
||||||
|
import org.apache.hertzbeat.common.entity.alerter.AlertDefine;
|
||||||
|
import org.apache.hertzbeat.common.entity.log.LogEntry;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* LogWorker Responsible for:
|
||||||
|
* 1. Receiving logs from WindowedLogRealTimeAlertCalculator
|
||||||
|
* 2. Evaluating alert expressions against logs
|
||||||
|
* 3. Sending matching logs to WindowAggregator
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
public class LogWorker {
|
||||||
|
|
||||||
|
private static final String LOG_PREFIX = "log";
|
||||||
|
|
||||||
|
private final AlertDefineService alertDefineService;
|
||||||
|
private final JexlExprCalculator jexlExprCalculator;
|
||||||
|
private final WindowAggregator windowAggregator;
|
||||||
|
private final AlerterWorkerPool workerPool;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
public LogWorker(AlertDefineService alertDefineService,
|
||||||
|
JexlExprCalculator jexlExprCalculator, WindowAggregator windowAggregator,
|
||||||
|
AlerterWorkerPool workerPool) {
|
||||||
|
this.alertDefineService = alertDefineService;
|
||||||
|
this.jexlExprCalculator = jexlExprCalculator;
|
||||||
|
this.windowAggregator = windowAggregator;
|
||||||
|
this.workerPool = workerPool;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void reduceAndSendLogTask(LogEntry logEntry) {
|
||||||
|
workerPool.executeLogJob(reduceLogTask(logEntry));
|
||||||
|
}
|
||||||
|
|
||||||
|
public Runnable reduceLogTask(LogEntry logEntry) {
|
||||||
|
return () -> {
|
||||||
|
try {
|
||||||
|
processLogEntry(logEntry);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Error processing log entry in worker: {}", e.getMessage(), e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private void processLogEntry(LogEntry logEntry) {
|
||||||
|
// Get all log alert definitions
|
||||||
|
List<AlertDefine> alertDefines = alertDefineService.getLogRealTimeAlertDefines();
|
||||||
|
|
||||||
|
// Create context for expression evaluation
|
||||||
|
Map<String, Object> context = new HashMap<>(8);
|
||||||
|
context.put(LOG_PREFIX, logEntry);
|
||||||
|
|
||||||
|
// Process each alert definition
|
||||||
|
for (AlertDefine define : alertDefines) {
|
||||||
|
if (define.getExpr() == null || define.getExpr().trim().isEmpty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Evaluate alert expression
|
||||||
|
boolean match = jexlExprCalculator.execAlertExpression(context, define.getExpr(), false);
|
||||||
|
|
||||||
|
if (match) {
|
||||||
|
// Create matching log event and send to WindowAggregator
|
||||||
|
MatchingLogEvent event = createMatchingLogEvent(logEntry, define);
|
||||||
|
windowAggregator.addMatchingLog(event);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("Error evaluating expression for alert define {}: {}", define.getName(), e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private MatchingLogEvent createMatchingLogEvent(LogEntry logEntry, AlertDefine define) {
|
||||||
|
long eventTimestamp = extractEventTimestamp(logEntry);
|
||||||
|
|
||||||
|
return MatchingLogEvent.builder()
|
||||||
|
.logEntry(logEntry)
|
||||||
|
.alertDefine(define)
|
||||||
|
.eventTimestamp(eventTimestamp)
|
||||||
|
.workerTimestamp(System.currentTimeMillis())
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private long extractEventTimestamp(LogEntry logEntry) {
|
||||||
|
if (logEntry.getTimeUnixNano() != null && logEntry.getTimeUnixNano() != 0) {
|
||||||
|
return logEntry.getTimeUnixNano() / 1_000_000; // Convert to milliseconds
|
||||||
|
}
|
||||||
|
if (logEntry.getObservedTimeUnixNano() != null && logEntry.getObservedTimeUnixNano() != 0) {
|
||||||
|
return logEntry.getObservedTimeUnixNano() / 1_000_000; // Convert to milliseconds
|
||||||
|
}
|
||||||
|
return System.currentTimeMillis();
|
||||||
|
}
|
||||||
|
}
|
||||||
+40
@@ -0,0 +1,40 @@
|
|||||||
|
/*
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||||
|
* contributor license agreements. See the NOTICE file distributed with
|
||||||
|
* this work for additional information regarding copyright ownership.
|
||||||
|
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||||
|
* (the "License"); you may not use this file except in compliance with
|
||||||
|
* the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.apache.hertzbeat.alert.calculate.realtime.window;
|
||||||
|
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import org.apache.hertzbeat.common.entity.alerter.AlertDefine;
|
||||||
|
import org.apache.hertzbeat.common.entity.log.LogEntry;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a log entry that matched an alert expression
|
||||||
|
* Used for communication between LogWorker and WindowAggregator
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
public class MatchingLogEvent {
|
||||||
|
|
||||||
|
private LogEntry logEntry;
|
||||||
|
|
||||||
|
private AlertDefine alertDefine;
|
||||||
|
|
||||||
|
private long eventTimestamp;
|
||||||
|
|
||||||
|
private long workerTimestamp;
|
||||||
|
}
|
||||||
+213
@@ -0,0 +1,213 @@
|
|||||||
|
/*
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||||
|
* contributor license agreements. See the NOTICE file distributed with
|
||||||
|
* this work for additional information regarding copyright ownership.
|
||||||
|
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||||
|
* (the "License"); you may not use this file except in compliance with
|
||||||
|
* the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.apache.hertzbeat.alert.calculate.realtime.window;
|
||||||
|
|
||||||
|
import com.google.common.util.concurrent.ThreadFactoryBuilder;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import javax.annotation.PostConstruct;
|
||||||
|
import javax.annotation.PreDestroy;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.CopyOnWriteArrayList;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.concurrent.ScheduledExecutorService;
|
||||||
|
import java.util.concurrent.ThreadFactory;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.atomic.AtomicLong;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Watermark Generator responsible for:
|
||||||
|
* 1. Receiving maxTimestamp updates from WindowedLogRealTimeAlertCalculator
|
||||||
|
* 2. Calculating watermarks based on configurable delay
|
||||||
|
* 3. Broadcasting watermarks to all subscribers (WindowAggregator)
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
@Slf4j
|
||||||
|
public class TimeService {
|
||||||
|
|
||||||
|
static final long DEFAULT_WATERMARK_DELAY_MS = 30_000; // 30 seconds
|
||||||
|
private static final long WATERMARK_BROADCAST_INTERVAL_MS = 5_000; // 5 seconds
|
||||||
|
|
||||||
|
// Define acceptable timestamp range to filter abnormal timestamps
|
||||||
|
private static final long MAX_FUTURE_TIME_MS = 60_000; // Allow 1 minute future time
|
||||||
|
private static final long MAX_PAST_TIME_MS = 24 * 60 * 60 * 1000; // Allow 24 hours past time
|
||||||
|
|
||||||
|
private final AtomicLong maxTimestamp = new AtomicLong(0);
|
||||||
|
private final AtomicLong currentWatermark = new AtomicLong(0);
|
||||||
|
private final CopyOnWriteArrayList<WatermarkListener> listeners = new CopyOnWriteArrayList<>();
|
||||||
|
private ScheduledExecutorService scheduler;
|
||||||
|
|
||||||
|
public TimeService(List<WatermarkListener> initialListeners) {
|
||||||
|
listeners.addAll(initialListeners);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostConstruct
|
||||||
|
public void start() {
|
||||||
|
// Create internal scheduled executor
|
||||||
|
ThreadFactory threadFactory = new ThreadFactoryBuilder()
|
||||||
|
.setUncaughtExceptionHandler((thread, throwable) -> {
|
||||||
|
log.error("TimeService scheduler has uncaughtException.");
|
||||||
|
log.error(throwable.getMessage(), throwable);
|
||||||
|
})
|
||||||
|
.setDaemon(true)
|
||||||
|
.setNameFormat("timeservice-scheduler-%d")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
this.scheduler = Executors.newSingleThreadScheduledExecutor(threadFactory);
|
||||||
|
|
||||||
|
// Start watermark broadcast scheduler
|
||||||
|
scheduler.scheduleAtFixedRate(
|
||||||
|
this::broadcastWatermark,
|
||||||
|
0,
|
||||||
|
WATERMARK_BROADCAST_INTERVAL_MS,
|
||||||
|
TimeUnit.MILLISECONDS
|
||||||
|
);
|
||||||
|
|
||||||
|
log.info("TimeService started with watermark delay: {}ms", DEFAULT_WATERMARK_DELAY_MS);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreDestroy
|
||||||
|
public void stop() {
|
||||||
|
if (scheduler != null && !scheduler.isShutdown()) {
|
||||||
|
log.info("Shutting down TimeService scheduler...");
|
||||||
|
scheduler.shutdown();
|
||||||
|
try {
|
||||||
|
if (!scheduler.awaitTermination(10, TimeUnit.SECONDS)) {
|
||||||
|
log.warn("TimeService scheduler did not terminate within 10 seconds, forcing shutdown");
|
||||||
|
scheduler.shutdownNow();
|
||||||
|
if (!scheduler.awaitTermination(5, TimeUnit.SECONDS)) {
|
||||||
|
log.error("TimeService scheduler did not terminate");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
log.warn("Interrupted while waiting for TimeService scheduler to terminate");
|
||||||
|
scheduler.shutdownNow();
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("TimeService stopped");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if timestamp is within acceptable range
|
||||||
|
*/
|
||||||
|
public boolean isValidTimestamp(long timestamp) {
|
||||||
|
long currentTime = System.currentTimeMillis();
|
||||||
|
return timestamp >= (currentTime - MAX_PAST_TIME_MS) && timestamp <= (currentTime + MAX_FUTURE_TIME_MS);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if data is late based on current watermark
|
||||||
|
*/
|
||||||
|
public boolean isLateData(long timestamp) {
|
||||||
|
return timestamp < getCurrentWatermark();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update max timestamp from WindowedLogRealTimeAlertCalculator
|
||||||
|
*/
|
||||||
|
public void updateMaxTimestamp(long timestamp) {
|
||||||
|
maxTimestamp.getAndUpdate(current -> Math.max(current, timestamp));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add watermark listener
|
||||||
|
*/
|
||||||
|
public void addWatermarkListener(WatermarkListener listener) {
|
||||||
|
listeners.add(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove watermark listener
|
||||||
|
*/
|
||||||
|
public void removeWatermarkListener(WatermarkListener listener) {
|
||||||
|
listeners.remove(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current watermark
|
||||||
|
*/
|
||||||
|
public long getCurrentWatermark() {
|
||||||
|
return currentWatermark.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get max timestamp
|
||||||
|
*/
|
||||||
|
public long getMaxTimestamp() {
|
||||||
|
return maxTimestamp.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate and broadcast watermark
|
||||||
|
*/
|
||||||
|
private void broadcastWatermark() {
|
||||||
|
|
||||||
|
try {
|
||||||
|
long maxTs = maxTimestamp.get();
|
||||||
|
if (maxTs <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Calculate watermark: maxTimestamp - delay
|
||||||
|
long newWatermark = maxTs - DEFAULT_WATERMARK_DELAY_MS;
|
||||||
|
long currentWm = currentWatermark.get();
|
||||||
|
|
||||||
|
// Only advance watermark (monotonic property)
|
||||||
|
if (newWatermark <= currentWm) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (currentWatermark.compareAndSet(currentWm, newWatermark)) {
|
||||||
|
// Broadcast to all listeners
|
||||||
|
Watermark watermark = new Watermark(newWatermark);
|
||||||
|
for (WatermarkListener listener : listeners) {
|
||||||
|
try {
|
||||||
|
listener.onWatermark(watermark);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Error notifying watermark listener: {}", e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.debug("Broadcast watermark: {} (maxTimestamp: {})", newWatermark, maxTs);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Error in watermark broadcast: {}", e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Watermark data class
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Getter
|
||||||
|
public static class Watermark {
|
||||||
|
private final long timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface for watermark listeners
|
||||||
|
*/
|
||||||
|
public interface WatermarkListener {
|
||||||
|
void onWatermark(Watermark watermark);
|
||||||
|
}
|
||||||
|
}
|
||||||
+251
@@ -0,0 +1,251 @@
|
|||||||
|
/*
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||||
|
* contributor license agreements. See the NOTICE file distributed with
|
||||||
|
* this work for additional information regarding copyright ownership.
|
||||||
|
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||||
|
* (the "License"); you may not use this file except in compliance with
|
||||||
|
* the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.apache.hertzbeat.alert.calculate.realtime.window;
|
||||||
|
|
||||||
|
import com.google.common.util.concurrent.ThreadFactoryBuilder;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.hertzbeat.common.entity.alerter.AlertDefine;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import javax.annotation.PostConstruct;
|
||||||
|
import javax.annotation.PreDestroy;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.BlockingQueue;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.concurrent.LinkedBlockingQueue;
|
||||||
|
import java.util.concurrent.ThreadFactory;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Window Aggregator Responsible for:
|
||||||
|
* 1. Managing window data structure for all active windows
|
||||||
|
* 2. Receiving matching logs from Workers
|
||||||
|
* 3. Receiving watermarks from TimeService
|
||||||
|
* 4. Sending closed windows to AlarmEvaluator
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
@Slf4j
|
||||||
|
public class WindowAggregator implements TimeService.WatermarkListener, Runnable {
|
||||||
|
|
||||||
|
private static final long DEFAULT_WINDOW_SIZE_MS = 1 * 60 * 1000; // 1 minutes
|
||||||
|
|
||||||
|
private final AlarmEvaluator alarmEvaluator;
|
||||||
|
private final BlockingQueue<MatchingLogEvent> eventQueue = new LinkedBlockingQueue<>();
|
||||||
|
private final Map<WindowKey, WindowData> activeWindows = new HashMap<>();
|
||||||
|
private final Object windowLock = new Object();
|
||||||
|
|
||||||
|
private ExecutorService aggregatorExecutor;
|
||||||
|
|
||||||
|
public WindowAggregator(AlarmEvaluator alarmEvaluator) {
|
||||||
|
this.alarmEvaluator = alarmEvaluator;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addMatchingLog(MatchingLogEvent event) {
|
||||||
|
try {
|
||||||
|
eventQueue.put(event);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
log.warn("Interrupted while adding matching log to aggregator");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onWatermark(TimeService.Watermark watermark) {
|
||||||
|
List<WindowData> closedWindows;
|
||||||
|
|
||||||
|
synchronized (windowLock) {
|
||||||
|
closedWindows = new ArrayList<>();
|
||||||
|
|
||||||
|
// Find windows that should be closed based on watermark
|
||||||
|
Iterator<Map.Entry<WindowKey, WindowData>> iterator = activeWindows.entrySet().iterator();
|
||||||
|
while (iterator.hasNext()) {
|
||||||
|
Map.Entry<WindowKey, WindowData> entry = iterator.next();
|
||||||
|
WindowData windowData = entry.getValue();
|
||||||
|
|
||||||
|
// Close window if its end time <= watermark timestamp
|
||||||
|
if (windowData.getEndTime() <= watermark.getTimestamp()) {
|
||||||
|
closedWindows.add(windowData);
|
||||||
|
iterator.remove();
|
||||||
|
|
||||||
|
log.debug("Closing window: {} with {} matching logs",
|
||||||
|
entry.getKey(), windowData.getMatchingLogs().size());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (WindowData windowData : closedWindows) {
|
||||||
|
alarmEvaluator.sendAndProcessWindowData(windowData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
while (!Thread.currentThread().isInterrupted()) {
|
||||||
|
try {
|
||||||
|
MatchingLogEvent event = eventQueue.take();
|
||||||
|
processMatchingLogEvent(event);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
break;
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Error processing matching log event: {}", e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void processMatchingLogEvent(MatchingLogEvent event) {
|
||||||
|
|
||||||
|
// Determine window size from alert define (if specified) or use default
|
||||||
|
long windowSizeMs = getWindowSize(event.getAlertDefine());
|
||||||
|
|
||||||
|
// Calculate window boundaries
|
||||||
|
long eventTime = event.getEventTimestamp();
|
||||||
|
long windowStart = (eventTime / windowSizeMs) * windowSizeMs;
|
||||||
|
long windowEnd = windowStart + windowSizeMs;
|
||||||
|
|
||||||
|
// Create window key
|
||||||
|
WindowKey windowKey = new WindowKey(
|
||||||
|
event.getAlertDefine().getId(),
|
||||||
|
windowStart,
|
||||||
|
windowEnd
|
||||||
|
);
|
||||||
|
synchronized (windowLock) {
|
||||||
|
// Get or create window data
|
||||||
|
WindowData windowData = activeWindows.computeIfAbsent(windowKey,
|
||||||
|
key -> new WindowData(key, event.getAlertDefine()));
|
||||||
|
// Add matching log to window
|
||||||
|
windowData.addMatchingLog(event);
|
||||||
|
log.debug("Added matching log to window: {} (total logs: {})",
|
||||||
|
windowKey, windowData.getMatchingLogs().size());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private long getWindowSize(AlertDefine alertDefine) {
|
||||||
|
// Check if alert define has custom window size configuration
|
||||||
|
if (alertDefine.getPeriod() != null) {
|
||||||
|
return alertDefine.getPeriod() * 1000; // Convert seconds to milliseconds
|
||||||
|
}
|
||||||
|
log.info("Using default window size of {} ms for alert define: {}",
|
||||||
|
DEFAULT_WINDOW_SIZE_MS, alertDefine.getName());
|
||||||
|
return DEFAULT_WINDOW_SIZE_MS;
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostConstruct
|
||||||
|
public void start() {
|
||||||
|
// Create internal executor
|
||||||
|
ThreadFactory threadFactory = new ThreadFactoryBuilder()
|
||||||
|
.setUncaughtExceptionHandler((thread, throwable) -> {
|
||||||
|
log.error("WindowAggregator executor has uncaughtException.");
|
||||||
|
log.error(throwable.getMessage(), throwable);
|
||||||
|
})
|
||||||
|
.setDaemon(true)
|
||||||
|
.setNameFormat("window-aggregator-%d")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
aggregatorExecutor = Executors.newSingleThreadExecutor(threadFactory);
|
||||||
|
|
||||||
|
// Submit aggregation task
|
||||||
|
aggregatorExecutor.submit(this);
|
||||||
|
|
||||||
|
log.info("WindowAggregator started");
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreDestroy
|
||||||
|
public void stop() {
|
||||||
|
if (aggregatorExecutor != null && !aggregatorExecutor.isShutdown()) {
|
||||||
|
log.info("Shutting down WindowAggregator executor...");
|
||||||
|
aggregatorExecutor.shutdown();
|
||||||
|
try {
|
||||||
|
if (!aggregatorExecutor.awaitTermination(10, TimeUnit.SECONDS)) {
|
||||||
|
log.warn("WindowAggregator executor did not terminate within 10 seconds, forcing shutdown");
|
||||||
|
aggregatorExecutor.shutdownNow();
|
||||||
|
if (!aggregatorExecutor.awaitTermination(5, TimeUnit.SECONDS)) {
|
||||||
|
log.error("WindowAggregator executor did not terminate");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
log.warn("Interrupted while waiting for WindowAggregator executor to terminate");
|
||||||
|
aggregatorExecutor.shutdownNow();
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("WindowAggregator stopped");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Window key for identifying unique windows
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Getter
|
||||||
|
@AllArgsConstructor
|
||||||
|
public static class WindowKey {
|
||||||
|
private final long alertDefineId;
|
||||||
|
private final long startTime;
|
||||||
|
private final long endTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Window data container
|
||||||
|
*/
|
||||||
|
public static class WindowData {
|
||||||
|
@Getter
|
||||||
|
private final WindowKey windowKey;
|
||||||
|
@Getter
|
||||||
|
private final AlertDefine alertDefine;
|
||||||
|
private final List<MatchingLogEvent> matchingLogs = new ArrayList<>();
|
||||||
|
@Getter
|
||||||
|
private final long createdTime;
|
||||||
|
|
||||||
|
public WindowData(WindowKey windowKey, AlertDefine alertDefine) {
|
||||||
|
this.windowKey = windowKey;
|
||||||
|
this.alertDefine = alertDefine;
|
||||||
|
this.createdTime = System.currentTimeMillis();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addMatchingLog(MatchingLogEvent event) {
|
||||||
|
matchingLogs.add(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<MatchingLogEvent> getMatchingLogs() {
|
||||||
|
return new ArrayList<>(matchingLogs);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get start time of the window
|
||||||
|
* @return start time
|
||||||
|
*/
|
||||||
|
public long getStartTime() { return windowKey.getStartTime(); }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get end time of the window
|
||||||
|
* @return end time
|
||||||
|
*/
|
||||||
|
public long getEndTime() { return windowKey.getEndTime(); }
|
||||||
|
}
|
||||||
|
}
|
||||||
+14
-4
@@ -22,10 +22,12 @@ package org.apache.hertzbeat.alert.config;
|
|||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.scheduling.annotation.Async;
|
import org.springframework.scheduling.annotation.Async;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyEmitter;
|
||||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -54,16 +56,24 @@ public class AlertSseManager {
|
|||||||
.name("ALERT_EVENT")
|
.name("ALERT_EVENT")
|
||||||
.data(data));
|
.data(data));
|
||||||
} catch (IOException | IllegalStateException e) {
|
} catch (IOException | IllegalStateException e) {
|
||||||
emitter.complete();
|
tryCompleteAndClean(clientId, emitter);
|
||||||
removeEmitter(clientId);
|
|
||||||
} catch (Exception exception) {
|
} catch (Exception exception) {
|
||||||
log.error("Failed to broadcast alert data to client: {}", exception.getMessage());
|
log.error("Failed to broadcast alert data to client: {}", exception.getMessage());
|
||||||
emitter.complete();
|
tryCompleteAndClean(clientId, emitter);
|
||||||
removeEmitter(clientId);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void tryCompleteAndClean(Long clientId, SseEmitter emitter) {
|
||||||
|
try {
|
||||||
|
Optional.ofNullable(emitter).ifPresent(ResponseBodyEmitter::complete);
|
||||||
|
} catch (Throwable e) {
|
||||||
|
log.debug("Failed to complete emitter for client {}: {}", clientId, e.getMessage());
|
||||||
|
}
|
||||||
|
// execute clear
|
||||||
|
removeEmitter(clientId);
|
||||||
|
}
|
||||||
|
|
||||||
private void removeEmitter(Long clientId) {
|
private void removeEmitter(Long clientId) {
|
||||||
emitters.remove(clientId);
|
emitters.remove(clientId);
|
||||||
}
|
}
|
||||||
|
|||||||
+25
-3
@@ -17,17 +17,16 @@
|
|||||||
|
|
||||||
package org.apache.hertzbeat.alert.controller;
|
package org.apache.hertzbeat.alert.controller;
|
||||||
|
|
||||||
import static org.apache.hertzbeat.common.constants.CommonConstants.MONITOR_NOT_EXIST_CODE;
|
|
||||||
import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
|
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.Parameter;
|
import io.swagger.v3.oas.annotations.Parameter;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import jakarta.validation.Valid;
|
import jakarta.validation.Valid;
|
||||||
import java.util.Objects;
|
|
||||||
import org.apache.hertzbeat.alert.service.AlertDefineService;
|
import org.apache.hertzbeat.alert.service.AlertDefineService;
|
||||||
import org.apache.hertzbeat.common.entity.alerter.AlertDefine;
|
import org.apache.hertzbeat.common.entity.alerter.AlertDefine;
|
||||||
import org.apache.hertzbeat.common.entity.dto.Message;
|
import org.apache.hertzbeat.common.entity.dto.Message;
|
||||||
|
import org.apache.hertzbeat.common.support.exception.AlertExpressionException;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
@@ -36,8 +35,17 @@ import org.springframework.web.bind.annotation.PostMapping;
|
|||||||
import org.springframework.web.bind.annotation.PutMapping;
|
import org.springframework.web.bind.annotation.PutMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestBody;
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
import static org.apache.hertzbeat.common.constants.CommonConstants.FAIL_CODE;
|
||||||
|
import static org.apache.hertzbeat.common.constants.CommonConstants.MONITOR_NOT_EXIST_CODE;
|
||||||
|
import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Alarm definition management API
|
* Alarm definition management API
|
||||||
*/
|
*/
|
||||||
@@ -90,4 +98,18 @@ public class AlertDefineController {
|
|||||||
return ResponseEntity.ok(Message.success("Delete success"));
|
return ResponseEntity.ok(Message.success("Delete success"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping(path = "/preview/{datasource}")
|
||||||
|
@Operation(summary = "Alarm definition expression preview",
|
||||||
|
description = "If the expression is formal, then the result of the query will be returned, otherwise it will respond with an error")
|
||||||
|
public ResponseEntity<Message<List<Map<String, Object>>>> getDefinePreview(
|
||||||
|
@Parameter(description = "Data Source Type", example = "promql") @PathVariable("datasource") String datasource,
|
||||||
|
@Parameter(description = "alert threshold type:realtime,periodic") @RequestParam String type,
|
||||||
|
@Parameter(description = "alert threshold expression") @RequestParam String expr) {
|
||||||
|
try {
|
||||||
|
return ResponseEntity.ok(Message.successWithData(alertDefineService.getDefinePreview(datasource, type, expr)));
|
||||||
|
} catch (AlertExpressionException ae) {
|
||||||
|
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(Message.fail(FAIL_CODE, ae.getMessage()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+184
@@ -0,0 +1,184 @@
|
|||||||
|
/*
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||||
|
* contributor license agreements. See the NOTICE file distributed with
|
||||||
|
* this work for additional information regarding copyright ownership.
|
||||||
|
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||||
|
* (the "License"); you may not use this file except in compliance with
|
||||||
|
* the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.apache.hertzbeat.alert.dto;
|
||||||
|
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Alibaba Cloud 'Simple Log Service(SLS)' alert content entity.
|
||||||
|
*
|
||||||
|
* @see <a href="https://help.aliyun.com/zh/sls/user-guide/variables-in-new-alert-templates"/>
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@AllArgsConstructor
|
||||||
|
@NoArgsConstructor
|
||||||
|
public class AlibabaCloudSlsExternAlert {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The id of the instance on which the alarm was triggered
|
||||||
|
*/
|
||||||
|
@JsonProperty("alert_instance_id")
|
||||||
|
private String alertInstanceId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Alarm rule id, unique within project
|
||||||
|
*/
|
||||||
|
@JsonProperty("alert_id")
|
||||||
|
private String alertId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Alarm rule name
|
||||||
|
*/
|
||||||
|
@JsonProperty("alert_name")
|
||||||
|
private String alertName;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Region
|
||||||
|
*/
|
||||||
|
private String region;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Alarm rule belongs to Project
|
||||||
|
*/
|
||||||
|
private String project;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Time of this evaluation
|
||||||
|
*/
|
||||||
|
@JsonProperty("alert_time")
|
||||||
|
private int alertTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* First trigger time
|
||||||
|
*/
|
||||||
|
@JsonProperty("fire_time")
|
||||||
|
private int fireTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Alarm recovery time
|
||||||
|
* If the alarm status is firing, the value is 0.
|
||||||
|
* If the alarm state is resolved, the value is the specific recovery time.
|
||||||
|
*/
|
||||||
|
@JsonProperty("resolve_time")
|
||||||
|
private int resolveTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Alarm status.
|
||||||
|
* firing: Triggers an alarm.
|
||||||
|
* resolved: Notification of resumption.
|
||||||
|
*/
|
||||||
|
private String status;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The total number of entries in the data that triggered the alert,
|
||||||
|
* which may be more than 100, for example after a Cartesian product operation.
|
||||||
|
*/
|
||||||
|
@JsonProperty("fire_results_count")
|
||||||
|
private int fireResultsCount;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tag list
|
||||||
|
* Example: {"env":"test"}
|
||||||
|
*/
|
||||||
|
private Map<String, String> labels;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Labeled lists
|
||||||
|
* Example: { "title": "Alarm title","desc": "Alarm desc" }
|
||||||
|
*/
|
||||||
|
private Map<String, String> annotations;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Alarm severity.
|
||||||
|
*
|
||||||
|
* 10: Critical
|
||||||
|
* 8: High
|
||||||
|
* 6: Medium
|
||||||
|
* 4: Low
|
||||||
|
* 2: Report only
|
||||||
|
*/
|
||||||
|
private int severity;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
@JsonProperty("signin_url")
|
||||||
|
private String signinUrl;
|
||||||
|
|
||||||
|
|
||||||
|
public String getAnnotation(String key) {
|
||||||
|
if (null == this.annotations || this.annotations.isEmpty()) {
|
||||||
|
return "N/A";
|
||||||
|
}
|
||||||
|
return this.annotations.get(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Severity
|
||||||
|
*/
|
||||||
|
public enum Severity {
|
||||||
|
|
||||||
|
CRITICAL(10, "Critical"),
|
||||||
|
|
||||||
|
HIGH(8, "High"),
|
||||||
|
|
||||||
|
MEDIUM(6, "Medium"),
|
||||||
|
|
||||||
|
LOW(4, "Low"),
|
||||||
|
|
||||||
|
REPORT_ONLY(2, "Report only");
|
||||||
|
|
||||||
|
private static final Map<Integer, Severity> STATUS_MAP;
|
||||||
|
|
||||||
|
static {
|
||||||
|
STATUS_MAP = Arrays.stream(Severity.values()).collect(Collectors.toMap(Severity::getStatus, t -> t, (oldVal, newVal) -> newVal));
|
||||||
|
}
|
||||||
|
|
||||||
|
private final int status;
|
||||||
|
private final String alias;
|
||||||
|
|
||||||
|
Severity(int status, String alias) {
|
||||||
|
this.status = status;
|
||||||
|
this.alias = alias;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Optional<Severity> convert(int severity) {
|
||||||
|
return Optional.ofNullable(STATUS_MAP.get(severity));
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getStatus() {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getAlias() {
|
||||||
|
return alias;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+237
@@ -0,0 +1,237 @@
|
|||||||
|
/*
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||||
|
* contributor license agreements. See the NOTICE file distributed with
|
||||||
|
* this work for additional information regarding copyright ownership.
|
||||||
|
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||||
|
* (the "License"); you may not use this file except in compliance with
|
||||||
|
* the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.apache.hertzbeat.alert.dto;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Huawei Cloud (CES) alert content entity.
|
||||||
|
*
|
||||||
|
* @see <a href="https://support.huaweicloud.com/usermanual-ces/ces_01_0218.html"/>
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@AllArgsConstructor
|
||||||
|
@NoArgsConstructor
|
||||||
|
public class HuaweiCloudExternAlert {
|
||||||
|
|
||||||
|
public static final String FIELD_MESSAGE = "message";
|
||||||
|
public static final String FIELD_MESSAGE_ID = "message_id";
|
||||||
|
public static final String FIELD_TIMESTAMP = "timestamp";
|
||||||
|
public static final String FIELD_TOPIC_URN = "topic_urn";
|
||||||
|
public static final String FIELD_TYPE = "type";
|
||||||
|
public static final String FIELD_SUBJECT = "subject";
|
||||||
|
public static final String FIELD_SUBSCRIBE_URL = "subscribe_url";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Signature information.
|
||||||
|
*/
|
||||||
|
private String signature;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subject
|
||||||
|
*/
|
||||||
|
private String subject;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The unique identifier of a topic, indicating the topic to which the message belongs.
|
||||||
|
*/
|
||||||
|
@JsonProperty("topic_urn")
|
||||||
|
private String topicUrn;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Message unique identifier.
|
||||||
|
*/
|
||||||
|
@JsonProperty("message_id")
|
||||||
|
private String messageId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Message
|
||||||
|
*/
|
||||||
|
private String message;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* message types, the message types are respectively:
|
||||||
|
* SubscriptionConfirmation、Notification、UnsubscribeConfirmation
|
||||||
|
*/
|
||||||
|
private String type;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscription confirms the URL that needs to be accessed
|
||||||
|
*/
|
||||||
|
@JsonProperty("subscribe_url")
|
||||||
|
private String subscribeUrl;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The certificate URL used for message signing, which does not require authentication and can be accessed directly.
|
||||||
|
*/
|
||||||
|
@JsonProperty("signing_cert_url")
|
||||||
|
private String signingCertUrl;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The timestamp of when the message was first sent.
|
||||||
|
*/
|
||||||
|
private String timestamp;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Alert message
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@AllArgsConstructor
|
||||||
|
@NoArgsConstructor
|
||||||
|
public static class AlertMessage {
|
||||||
|
|
||||||
|
private String version;
|
||||||
|
|
||||||
|
private AlertData data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Alert data
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@AllArgsConstructor
|
||||||
|
@NoArgsConstructor
|
||||||
|
public static class AlertData {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether an alarm occurs.
|
||||||
|
* Note: Empty and false are both recovery notifications.
|
||||||
|
* Note: There are no recovery notifications for event types
|
||||||
|
*/
|
||||||
|
@JsonProperty("IsAlarm")
|
||||||
|
private Boolean alarm;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Alarm time
|
||||||
|
*/
|
||||||
|
@JsonProperty("AlarmTime")
|
||||||
|
private String alarmTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resource ID
|
||||||
|
*/
|
||||||
|
@JsonProperty("ResourceId")
|
||||||
|
private String resourceId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The name of the metric
|
||||||
|
*/
|
||||||
|
@JsonProperty("MetricName")
|
||||||
|
private String metricName;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Specifies the alarm severity, which can be Critical, Major, Minor, or Informational.
|
||||||
|
*/
|
||||||
|
@JsonProperty("AlarmLevel")
|
||||||
|
private String alarmLevel;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Namespace
|
||||||
|
*/
|
||||||
|
@JsonProperty("Namespace")
|
||||||
|
private String namespace;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Region
|
||||||
|
*/
|
||||||
|
@JsonProperty("Region")
|
||||||
|
private String region;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dimension name
|
||||||
|
*/
|
||||||
|
@JsonProperty("DimensionName")
|
||||||
|
private String dimensionName;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resource name
|
||||||
|
*/
|
||||||
|
@JsonProperty("ResourceName")
|
||||||
|
private String resourceName;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Alarm record ID
|
||||||
|
*/
|
||||||
|
@JsonProperty("AlarmRecordID")
|
||||||
|
private String alarmRecordId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Current data
|
||||||
|
*/
|
||||||
|
@JsonProperty("CurrentData")
|
||||||
|
private String currentData;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The comparison conditions for the alarm thresholds can be >, =, <, >=, <=.
|
||||||
|
*/
|
||||||
|
@JsonProperty("ComparisonOperator")
|
||||||
|
private String comparisonOperator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Alarm value
|
||||||
|
*/
|
||||||
|
@JsonProperty("Value")
|
||||||
|
private String value;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Number of consecutive occurrences of triggered alarms
|
||||||
|
*/
|
||||||
|
@JsonProperty("Count")
|
||||||
|
private int count;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Huawei cloud alert type
|
||||||
|
*/
|
||||||
|
public enum AlertType {
|
||||||
|
|
||||||
|
SUBSCRIPTION("SubscriptionConfirmation"),
|
||||||
|
|
||||||
|
UNSUBSCRIBE("UnsubscribeConfirmation"),
|
||||||
|
|
||||||
|
NOTIFICATION("Notification");
|
||||||
|
|
||||||
|
private final String type;
|
||||||
|
|
||||||
|
AlertType(String type) {
|
||||||
|
this.type = type;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean valid(String type) {
|
||||||
|
if (null == type || type.isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return Arrays.stream(AlertType.values()).anyMatch(alertType -> alertType.getType().equals(type));
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getType() {
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+121
@@ -0,0 +1,121 @@
|
|||||||
|
/*
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||||
|
* contributor license agreements. See the NOTICE file distributed with
|
||||||
|
* this work for additional information regarding copyright ownership.
|
||||||
|
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||||
|
* (the "License"); you may not use this file except in compliance with
|
||||||
|
* the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.apache.hertzbeat.alert.dto;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* volcengine event alert entity class
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@AllArgsConstructor
|
||||||
|
@NoArgsConstructor
|
||||||
|
public class VolcEngineExternEventAlert {
|
||||||
|
@JsonProperty("Type")
|
||||||
|
private String type;
|
||||||
|
|
||||||
|
@JsonProperty("AccountId")
|
||||||
|
private String accountId;
|
||||||
|
|
||||||
|
@JsonProperty("Source")
|
||||||
|
private String source;
|
||||||
|
|
||||||
|
@JsonProperty("ProjectName")
|
||||||
|
private String projectName;
|
||||||
|
|
||||||
|
@JsonProperty("Id")
|
||||||
|
private String id;
|
||||||
|
|
||||||
|
@JsonProperty("EventType")
|
||||||
|
private String eventType;
|
||||||
|
|
||||||
|
@JsonProperty("DescriptionCN")
|
||||||
|
private String descriptionCn;
|
||||||
|
|
||||||
|
@JsonProperty("HappenedAt")
|
||||||
|
private Long happenedAt;
|
||||||
|
|
||||||
|
@JsonProperty("Region")
|
||||||
|
private String region;
|
||||||
|
|
||||||
|
@JsonProperty("Details")
|
||||||
|
private EventDetails details;
|
||||||
|
|
||||||
|
@JsonProperty("Rules")
|
||||||
|
private List<EventRule> rules;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* event details
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public static class EventDetails {
|
||||||
|
@JsonProperty("specversion")
|
||||||
|
private String specVersion;
|
||||||
|
|
||||||
|
@JsonProperty("id")
|
||||||
|
private String id;
|
||||||
|
|
||||||
|
@JsonProperty("source")
|
||||||
|
private String source;
|
||||||
|
|
||||||
|
@JsonProperty("type")
|
||||||
|
private String type;
|
||||||
|
|
||||||
|
@JsonProperty("volcpublishtime")
|
||||||
|
private String volcPublishTime;
|
||||||
|
|
||||||
|
@JsonProperty("volcregion")
|
||||||
|
private String volcRegion;
|
||||||
|
|
||||||
|
@JsonProperty("volcaccountid")
|
||||||
|
private String volcAccountId;
|
||||||
|
|
||||||
|
@JsonProperty("volceventbusname")
|
||||||
|
private String volcEventBusName;
|
||||||
|
|
||||||
|
@JsonProperty("volcresourcename")
|
||||||
|
private String volcResourceName;
|
||||||
|
|
||||||
|
@JsonProperty("subject")
|
||||||
|
private String subject;
|
||||||
|
|
||||||
|
@JsonProperty("time")
|
||||||
|
private String time;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* event rule
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public static class EventRule {
|
||||||
|
@JsonProperty("RuleId")
|
||||||
|
private String ruleId;
|
||||||
|
|
||||||
|
@JsonProperty("RuleName")
|
||||||
|
private String ruleName;
|
||||||
|
|
||||||
|
@JsonProperty("Level")
|
||||||
|
private String level;
|
||||||
|
}
|
||||||
|
}
|
||||||
+203
@@ -0,0 +1,203 @@
|
|||||||
|
/*
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||||
|
* contributor license agreements. See the NOTICE file distributed with
|
||||||
|
* this work for additional information regarding copyright ownership.
|
||||||
|
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||||
|
* (the "License"); you may not use this file except in compliance with
|
||||||
|
* the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.apache.hertzbeat.alert.dto;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Volcengine alarm entity class
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Builder
|
||||||
|
public class VolcEngineExternMetricAlert {
|
||||||
|
|
||||||
|
@JsonProperty("Type")
|
||||||
|
private String type;
|
||||||
|
|
||||||
|
@JsonProperty("AccountId")
|
||||||
|
private String accountId;
|
||||||
|
|
||||||
|
@JsonProperty("RuleName")
|
||||||
|
private String ruleName;
|
||||||
|
|
||||||
|
@JsonProperty("RuleId")
|
||||||
|
private String ruleId;
|
||||||
|
|
||||||
|
@JsonProperty("Namespace")
|
||||||
|
private String namespace;
|
||||||
|
|
||||||
|
@JsonProperty("SubNamespace")
|
||||||
|
private String subNamespace;
|
||||||
|
|
||||||
|
@JsonProperty("Level")
|
||||||
|
private String level;
|
||||||
|
|
||||||
|
@JsonProperty("HappenedAt")
|
||||||
|
private String happenedAt;
|
||||||
|
|
||||||
|
@JsonProperty("RuleCondition")
|
||||||
|
private String ruleCondition;
|
||||||
|
|
||||||
|
@JsonProperty("Resources")
|
||||||
|
private List<Resource> resources;
|
||||||
|
|
||||||
|
@JsonProperty("RecoveredResources")
|
||||||
|
private List<Resource> recoveredResources;
|
||||||
|
|
||||||
|
@JsonProperty("NoDataResources")
|
||||||
|
private List<NoDataResource> noDataResources;
|
||||||
|
|
||||||
|
@JsonProperty("NoDataRecoveredResources")
|
||||||
|
private List<NoDataRecoveredResource> noDataRecoveredResources;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* alert resource entity class
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Builder
|
||||||
|
public static class Resource {
|
||||||
|
@JsonProperty("Id")
|
||||||
|
private String id;
|
||||||
|
|
||||||
|
@JsonProperty("Name")
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
@JsonProperty("Region")
|
||||||
|
private String region;
|
||||||
|
|
||||||
|
@JsonProperty("FirstAlertTime")
|
||||||
|
private Long firstAlertTime;
|
||||||
|
|
||||||
|
@JsonProperty("LastAlertTime")
|
||||||
|
private Long lastAlertTime;
|
||||||
|
|
||||||
|
@JsonProperty("Metrics")
|
||||||
|
private List<Metric> metrics;
|
||||||
|
|
||||||
|
@JsonProperty("AlertGroupId")
|
||||||
|
private String alertGroupId;
|
||||||
|
|
||||||
|
@JsonProperty("Dimensions")
|
||||||
|
private List<Dimension> dimensions;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* alert no data resource entity class
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
public static class NoDataResource extends Resource {
|
||||||
|
@JsonProperty("NoDataMetrics")
|
||||||
|
private List<Metric> noDataMetrics;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* alert no data recovered resource entity class
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
public static class NoDataRecoveredResource extends Resource {
|
||||||
|
@JsonProperty("DroppedMetrics")
|
||||||
|
private List<DroppedMetric> droppedMetrics;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* alert metric entity class
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public static class Metric {
|
||||||
|
@JsonProperty("Name")
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
@JsonProperty("Unit")
|
||||||
|
private String unit;
|
||||||
|
|
||||||
|
@JsonProperty("Threshold")
|
||||||
|
private Double threshold;
|
||||||
|
|
||||||
|
@JsonProperty("CurrentValue")
|
||||||
|
private Object currentValue;
|
||||||
|
|
||||||
|
@JsonProperty("Description")
|
||||||
|
private String description;
|
||||||
|
|
||||||
|
@JsonProperty("Warning")
|
||||||
|
private Boolean warning;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* alert dropped metric entity class
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
public static class DroppedMetric extends Metric {
|
||||||
|
@JsonProperty("Reason")
|
||||||
|
private String reason;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* resource dimension entity class
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public static class Dimension {
|
||||||
|
@JsonProperty("Name")
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
@JsonProperty("NameCN")
|
||||||
|
private String nameCn;
|
||||||
|
|
||||||
|
@JsonProperty("Value")
|
||||||
|
private String value;
|
||||||
|
|
||||||
|
@JsonProperty("Description")
|
||||||
|
private String description;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static final String ALERT_TYPE_METRIC = "Metric";
|
||||||
|
public static final String ALERT_TYPE_EVENT = "Event";
|
||||||
|
public static final String ALERT_TYPE_METRIC_RECOVERED = "MetricRecovered";
|
||||||
|
public static final String ALERT_TYPE_METRICS_NODATA = "MetricsNoData";
|
||||||
|
public static final String ALERT_TYPE_NO_DATA_RECOVERED = "NoDataRecovered";
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
+374
-25
@@ -27,7 +27,7 @@ import org.antlr.v4.runtime.tree.AbstractParseTreeVisitor;
|
|||||||
* of the available methods.
|
* of the available methods.
|
||||||
*
|
*
|
||||||
* @param <T> The return type of the visit operation. Use {@link Void} for
|
* @param <T> The return type of the visit operation. Use {@link Void} for
|
||||||
* operations with no return type.
|
* operations with no return type.
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings("CheckReturnValue")
|
@SuppressWarnings("CheckReturnValue")
|
||||||
public class AlertExpressionBaseVisitor<T> extends AbstractParseTreeVisitor<T> implements AlertExpressionVisitor<T> {
|
public class AlertExpressionBaseVisitor<T> extends AbstractParseTreeVisitor<T> implements AlertExpressionVisitor<T> {
|
||||||
@@ -37,7 +37,10 @@ public class AlertExpressionBaseVisitor<T> extends AbstractParseTreeVisitor<T> i
|
|||||||
* <p>The default implementation returns the result of calling
|
* <p>The default implementation returns the result of calling
|
||||||
* {@link #visitChildren} on {@code ctx}.</p>
|
* {@link #visitChildren} on {@code ctx}.</p>
|
||||||
*/
|
*/
|
||||||
@Override public T visitExpression(AlertExpressionParser.ExpressionContext ctx) { return visitChildren(ctx); }
|
@Override
|
||||||
|
public T visitExpression(AlertExpressionParser.ExpressionContext ctx) {
|
||||||
|
return visitChildren(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {@inheritDoc}
|
* {@inheritDoc}
|
||||||
@@ -45,15 +48,10 @@ public class AlertExpressionBaseVisitor<T> extends AbstractParseTreeVisitor<T> i
|
|||||||
* <p>The default implementation returns the result of calling
|
* <p>The default implementation returns the result of calling
|
||||||
* {@link #visitChildren} on {@code ctx}.</p>
|
* {@link #visitChildren} on {@code ctx}.</p>
|
||||||
*/
|
*/
|
||||||
@Override public T visitAndExpr(AlertExpressionParser.AndExprContext ctx) { return visitChildren(ctx); }
|
@Override
|
||||||
|
public T visitAndExpr(AlertExpressionParser.AndExprContext ctx) {
|
||||||
/**
|
return visitChildren(ctx);
|
||||||
* {@inheritDoc}
|
}
|
||||||
*
|
|
||||||
* <p>The default implementation returns the result of calling
|
|
||||||
* {@link #visitChildren} on {@code ctx}.</p>
|
|
||||||
*/
|
|
||||||
@Override public T visitQueryExpr(AlertExpressionParser.QueryExprContext ctx) { return visitChildren(ctx); }
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {@inheritDoc}
|
* {@inheritDoc}
|
||||||
@@ -61,7 +59,10 @@ public class AlertExpressionBaseVisitor<T> extends AbstractParseTreeVisitor<T> i
|
|||||||
* <p>The default implementation returns the result of calling
|
* <p>The default implementation returns the result of calling
|
||||||
* {@link #visitChildren} on {@code ctx}.</p>
|
* {@link #visitChildren} on {@code ctx}.</p>
|
||||||
*/
|
*/
|
||||||
@Override public T visitComparisonExpr(AlertExpressionParser.ComparisonExprContext ctx) { return visitChildren(ctx); }
|
@Override
|
||||||
|
public T visitComparisonExpr(AlertExpressionParser.ComparisonExprContext ctx) {
|
||||||
|
return visitChildren(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {@inheritDoc}
|
* {@inheritDoc}
|
||||||
@@ -69,7 +70,10 @@ public class AlertExpressionBaseVisitor<T> extends AbstractParseTreeVisitor<T> i
|
|||||||
* <p>The default implementation returns the result of calling
|
* <p>The default implementation returns the result of calling
|
||||||
* {@link #visitChildren} on {@code ctx}.</p>
|
* {@link #visitChildren} on {@code ctx}.</p>
|
||||||
*/
|
*/
|
||||||
@Override public T visitUnlessExpr(AlertExpressionParser.UnlessExprContext ctx) { return visitChildren(ctx); }
|
@Override
|
||||||
|
public T visitUnlessExpr(AlertExpressionParser.UnlessExprContext ctx) {
|
||||||
|
return visitChildren(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {@inheritDoc}
|
* {@inheritDoc}
|
||||||
@@ -77,7 +81,10 @@ public class AlertExpressionBaseVisitor<T> extends AbstractParseTreeVisitor<T> i
|
|||||||
* <p>The default implementation returns the result of calling
|
* <p>The default implementation returns the result of calling
|
||||||
* {@link #visitChildren} on {@code ctx}.</p>
|
* {@link #visitChildren} on {@code ctx}.</p>
|
||||||
*/
|
*/
|
||||||
@Override public T visitLiteralExpr(AlertExpressionParser.LiteralExprContext ctx) { return visitChildren(ctx); }
|
@Override
|
||||||
|
public T visitSqlExpr(AlertExpressionParser.SqlExprContext ctx) {
|
||||||
|
return visitChildren(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {@inheritDoc}
|
* {@inheritDoc}
|
||||||
@@ -85,15 +92,10 @@ public class AlertExpressionBaseVisitor<T> extends AbstractParseTreeVisitor<T> i
|
|||||||
* <p>The default implementation returns the result of calling
|
* <p>The default implementation returns the result of calling
|
||||||
* {@link #visitChildren} on {@code ctx}.</p>
|
* {@link #visitChildren} on {@code ctx}.</p>
|
||||||
*/
|
*/
|
||||||
@Override public T visitParenExpr(AlertExpressionParser.ParenExprContext ctx) { return visitChildren(ctx); }
|
@Override
|
||||||
|
public T visitSqlCallExpr(AlertExpressionParser.SqlCallExprContext ctx) {
|
||||||
/**
|
return visitChildren(ctx);
|
||||||
* {@inheritDoc}
|
}
|
||||||
*
|
|
||||||
* <p>The default implementation returns the result of calling
|
|
||||||
* {@link #visitChildren} on {@code ctx}.</p>
|
|
||||||
*/
|
|
||||||
@Override public T visitOrExpr(AlertExpressionParser.OrExprContext ctx) { return visitChildren(ctx); }
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {@inheritDoc}
|
* {@inheritDoc}
|
||||||
@@ -101,7 +103,10 @@ public class AlertExpressionBaseVisitor<T> extends AbstractParseTreeVisitor<T> i
|
|||||||
* <p>The default implementation returns the result of calling
|
* <p>The default implementation returns the result of calling
|
||||||
* {@link #visitChildren} on {@code ctx}.</p>
|
* {@link #visitChildren} on {@code ctx}.</p>
|
||||||
*/
|
*/
|
||||||
@Override public T visitIdentifier(AlertExpressionParser.IdentifierContext ctx) { return visitChildren(ctx); }
|
@Override
|
||||||
|
public T visitLiteralExpr(AlertExpressionParser.LiteralExprContext ctx) {
|
||||||
|
return visitChildren(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {@inheritDoc}
|
* {@inheritDoc}
|
||||||
@@ -109,5 +114,349 @@ public class AlertExpressionBaseVisitor<T> extends AbstractParseTreeVisitor<T> i
|
|||||||
* <p>The default implementation returns the result of calling
|
* <p>The default implementation returns the result of calling
|
||||||
* {@link #visitChildren} on {@code ctx}.</p>
|
* {@link #visitChildren} on {@code ctx}.</p>
|
||||||
*/
|
*/
|
||||||
@Override public T visitNumber(AlertExpressionParser.NumberContext ctx) { return visitChildren(ctx); }
|
@Override
|
||||||
|
public T visitParenExpr(AlertExpressionParser.ParenExprContext ctx) {
|
||||||
|
return visitChildren(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*
|
||||||
|
* <p>The default implementation returns the result of calling
|
||||||
|
* {@link #visitChildren} on {@code ctx}.</p>
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public T visitPromqlCallExpr(AlertExpressionParser.PromqlCallExprContext ctx) {
|
||||||
|
return visitChildren(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*
|
||||||
|
* <p>The default implementation returns the result of calling
|
||||||
|
* {@link #visitChildren} on {@code ctx}.</p>
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public T visitPromqlExpr(AlertExpressionParser.PromqlExprContext ctx) {
|
||||||
|
return visitChildren(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*
|
||||||
|
* <p>The default implementation returns the result of calling
|
||||||
|
* {@link #visitChildren} on {@code ctx}.</p>
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public T visitOrExpr(AlertExpressionParser.OrExprContext ctx) {
|
||||||
|
return visitChildren(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*
|
||||||
|
* <p>The default implementation returns the result of calling
|
||||||
|
* {@link #visitChildren} on {@code ctx}.</p>
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public T visitFunctionCall(AlertExpressionParser.FunctionCallContext ctx) {
|
||||||
|
return visitChildren(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*
|
||||||
|
* <p>The default implementation returns the result of calling
|
||||||
|
* {@link #visitChildren} on {@code ctx}.</p>
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public T visitParameterList(AlertExpressionParser.ParameterListContext ctx) {
|
||||||
|
return visitChildren(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*
|
||||||
|
* <p>The default implementation returns the result of calling
|
||||||
|
* {@link #visitChildren} on {@code ctx}.</p>
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public T visitParameter(AlertExpressionParser.ParameterContext ctx) {
|
||||||
|
return visitChildren(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*
|
||||||
|
* <p>The default implementation returns the result of calling
|
||||||
|
* {@link #visitChildren} on {@code ctx}.</p>
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public T visitNumber(AlertExpressionParser.NumberContext ctx) {
|
||||||
|
return visitChildren(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*
|
||||||
|
* <p>The default implementation returns the result of calling
|
||||||
|
* {@link #visitChildren} on {@code ctx}.</p>
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public T visitString(AlertExpressionParser.StringContext ctx) {
|
||||||
|
return visitChildren(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*
|
||||||
|
* <p>The default implementation returns the result of calling
|
||||||
|
* {@link #visitChildren} on {@code ctx}.</p>
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public T visitDuration(AlertExpressionParser.DurationContext ctx) {
|
||||||
|
return visitChildren(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*
|
||||||
|
* <p>The default implementation returns the result of calling
|
||||||
|
* {@link #visitChildren} on {@code ctx}.</p>
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public T visitFunctionName(AlertExpressionParser.FunctionNameContext ctx) {
|
||||||
|
return visitChildren(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*
|
||||||
|
* <p>The default implementation returns the result of calling
|
||||||
|
* {@link #visitChildren} on {@code ctx}.</p>
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public T visitSelectSql(AlertExpressionParser.SelectSqlContext ctx) {
|
||||||
|
return visitChildren(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*
|
||||||
|
* <p>The default implementation returns the result of calling
|
||||||
|
* {@link #visitChildren} on {@code ctx}.</p>
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public T visitSelectFieldList(AlertExpressionParser.SelectFieldListContext ctx) {
|
||||||
|
return visitChildren(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*
|
||||||
|
* <p>The default implementation returns the result of calling
|
||||||
|
* {@link #visitChildren} on {@code ctx}.</p>
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public T visitSelectField(AlertExpressionParser.SelectFieldContext ctx) {
|
||||||
|
return visitChildren(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*
|
||||||
|
* <p>The default implementation returns the result of calling
|
||||||
|
* {@link #visitChildren} on {@code ctx}.</p>
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public T visitGroupByList(AlertExpressionParser.GroupByListContext ctx) {
|
||||||
|
return visitChildren(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*
|
||||||
|
* <p>The default implementation returns the result of calling
|
||||||
|
* {@link #visitChildren} on {@code ctx}.</p>
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public T visitOrderByList(AlertExpressionParser.OrderByListContext ctx) {
|
||||||
|
return visitChildren(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*
|
||||||
|
* <p>The default implementation returns the result of calling
|
||||||
|
* {@link #visitChildren} on {@code ctx}.</p>
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public T visitOrderByField(AlertExpressionParser.OrderByFieldContext ctx) {
|
||||||
|
return visitChildren(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*
|
||||||
|
* <p>The default implementation returns the result of calling
|
||||||
|
* {@link #visitChildren} on {@code ctx}.</p>
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public T visitLimitClause(AlertExpressionParser.LimitClauseContext ctx) {
|
||||||
|
return visitChildren(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*
|
||||||
|
* <p>The default implementation returns the result of calling
|
||||||
|
* {@link #visitChildren} on {@code ctx}.</p>
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public T visitRelList(AlertExpressionParser.RelListContext ctx) {
|
||||||
|
return visitChildren(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*
|
||||||
|
* <p>The default implementation returns the result of calling
|
||||||
|
* {@link #visitChildren} on {@code ctx}.</p>
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public T visitRelation(AlertExpressionParser.RelationContext ctx) {
|
||||||
|
return visitChildren(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*
|
||||||
|
* <p>The default implementation returns the result of calling
|
||||||
|
* {@link #visitChildren} on {@code ctx}.</p>
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public T visitConditionList(AlertExpressionParser.ConditionListContext ctx) {
|
||||||
|
return visitChildren(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*
|
||||||
|
* <p>The default implementation returns the result of calling
|
||||||
|
* {@link #visitChildren} on {@code ctx}.</p>
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public T visitCompOp(AlertExpressionParser.CompOpContext ctx) {
|
||||||
|
return visitChildren(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*
|
||||||
|
* <p>The default implementation returns the result of calling
|
||||||
|
* {@link #visitChildren} on {@code ctx}.</p>
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public T visitCondition(AlertExpressionParser.ConditionContext ctx) {
|
||||||
|
return visitChildren(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*
|
||||||
|
* <p>The default implementation returns the result of calling
|
||||||
|
* {@link #visitChildren} on {@code ctx}.</p>
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public T visitConditionUnit(AlertExpressionParser.ConditionUnitContext ctx) {
|
||||||
|
return visitChildren(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*
|
||||||
|
* <p>The default implementation returns the result of calling
|
||||||
|
* {@link #visitChildren} on {@code ctx}.</p>
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public T visitPromql(AlertExpressionParser.PromqlContext ctx) {
|
||||||
|
return visitChildren(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*
|
||||||
|
* <p>The default implementation returns the result of calling
|
||||||
|
* {@link #visitChildren} on {@code ctx}.</p>
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public T visitMetricSelector(AlertExpressionParser.MetricSelectorContext ctx) {
|
||||||
|
return visitChildren(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*
|
||||||
|
* <p>The default implementation returns the result of calling
|
||||||
|
* {@link #visitChildren} on {@code ctx}.</p>
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public T visitLabelMatcherList(AlertExpressionParser.LabelMatcherListContext ctx) {
|
||||||
|
return visitChildren(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*
|
||||||
|
* <p>The default implementation returns the result of calling
|
||||||
|
* {@link #visitChildren} on {@code ctx}.</p>
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public T visitLabelMatcherItem(AlertExpressionParser.LabelMatcherItemContext ctx) {
|
||||||
|
return visitChildren(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*
|
||||||
|
* <p>The default implementation returns the result of calling
|
||||||
|
* {@link #visitChildren} on {@code ctx}.</p>
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public T visitLabelMatcherOp(AlertExpressionParser.LabelMatcherOpContext ctx) {
|
||||||
|
return visitChildren(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*
|
||||||
|
* <p>The default implementation returns the result of calling
|
||||||
|
* {@link #visitChildren} on {@code ctx}.</p>
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public T visitLabelList(AlertExpressionParser.LabelListContext ctx) {
|
||||||
|
return visitChildren(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*
|
||||||
|
* <p>The default implementation returns the result of calling
|
||||||
|
* {@link #visitChildren} on {@code ctx}.</p>
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public T visitInstantVectorOp(AlertExpressionParser.InstantVectorOpContext ctx) {
|
||||||
|
return visitChildren(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*
|
||||||
|
* <p>The default implementation returns the result of calling
|
||||||
|
* {@link #visitChildren} on {@code ctx}.</p>
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public T visitAggregationOperator(AlertExpressionParser.AggregationOperatorContext ctx) {
|
||||||
|
return visitChildren(ctx);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+225
-208
@@ -17,26 +17,34 @@
|
|||||||
|
|
||||||
package org.apache.hertzbeat.alert.expr;
|
package org.apache.hertzbeat.alert.expr;
|
||||||
|
|
||||||
|
import org.antlr.v4.runtime.CommonTokenStream;
|
||||||
|
import org.apache.commons.collections4.CollectionUtils;
|
||||||
|
import org.apache.hertzbeat.common.support.exception.ExpressionVisitorException;
|
||||||
import org.apache.hertzbeat.warehouse.db.QueryExecutor;
|
import org.apache.hertzbeat.warehouse.db.QueryExecutor;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.LinkedList;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Alert expression visitor implement
|
* Alert expression visitor implement
|
||||||
*/
|
*/
|
||||||
public class AlertExpressionEvalVisitor extends AlertExpressionBaseVisitor<List<Map<String, Object>>> {
|
public class AlertExpressionEvalVisitor extends AlertExpressionBaseVisitor<List<Map<String, Object>>> {
|
||||||
|
|
||||||
private static final String THRESHOLD = "__threshold__";
|
private static final String SCALAR = "__scalar__";
|
||||||
|
private static final String NAME = "__name__";
|
||||||
private static final String VALUE = "__value__";
|
private static final String VALUE = "__value__";
|
||||||
|
private static final String TIMESTAMP = "__timestamp__";
|
||||||
|
|
||||||
private final QueryExecutor executor;
|
private final QueryExecutor executor;
|
||||||
|
private final CommonTokenStream tokens;
|
||||||
|
|
||||||
public AlertExpressionEvalVisitor(QueryExecutor executor) {
|
public AlertExpressionEvalVisitor(QueryExecutor executor, CommonTokenStream tokens) {
|
||||||
this.executor = executor;
|
this.executor = executor;
|
||||||
|
this.tokens = tokens;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -53,69 +61,126 @@ public class AlertExpressionEvalVisitor extends AlertExpressionBaseVisitor<List<
|
|||||||
public List<Map<String, Object>> visitComparisonExpr(AlertExpressionParser.ComparisonExprContext ctx) {
|
public List<Map<String, Object>> visitComparisonExpr(AlertExpressionParser.ComparisonExprContext ctx) {
|
||||||
List<Map<String, Object>> leftResult = visit(ctx.left);
|
List<Map<String, Object>> leftResult = visit(ctx.left);
|
||||||
List<Map<String, Object>> rightResult = visit(ctx.right);
|
List<Map<String, Object>> rightResult = visit(ctx.right);
|
||||||
if (rightResult.size() == 1 && rightResult.get(0).containsKey(THRESHOLD)) {
|
int type = ctx.op.getType();
|
||||||
double threshold = (double) rightResult.get(0).get(THRESHOLD);
|
boolean boolModifier = ctx.BOOL() != null;
|
||||||
String operator = ctx.op.getText();
|
boolean leftIsScalar = isScalar(leftResult);
|
||||||
|
boolean rightIsScalar = isScalar(rightResult);
|
||||||
|
List<Map<String, Object>> results = new ArrayList<>();
|
||||||
|
|
||||||
List<Map<String, Object>> result = new ArrayList<>();
|
// scalar and scalar
|
||||||
for (Map<String, Object> item : leftResult) {
|
if (leftIsScalar && rightIsScalar) {
|
||||||
Object queryValues = item.get(VALUE);
|
if (!boolModifier) {
|
||||||
if (queryValues == null) {
|
// Between two scalars,
|
||||||
// ignore the query result data is empty
|
// the bool modifier must be provided and these operators result in another scalar that is either 0 (false) or 1 (true), depending on the comparison result.
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
Object leftVal = leftResult.get(0).get(SCALAR);
|
||||||
|
Object rightVal = rightResult.get(0).get(SCALAR);
|
||||||
|
Boolean match = compareOp(leftVal, type, rightVal);
|
||||||
|
// returns a result only if the comparison condition is met.
|
||||||
|
Map<String, Object> result = new HashMap<>();
|
||||||
|
result.put(VALUE, match ? 1 : 0);
|
||||||
|
results.add(result);
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
// scalar and vector
|
||||||
|
if (leftIsScalar) {
|
||||||
|
Object leftVal = leftResult.get(0).get(SCALAR);
|
||||||
|
for (Map<String, Object> rightItem : rightResult) {
|
||||||
|
Object rightVal = rightItem.getOrDefault(VALUE, null);
|
||||||
|
if (isValidValue(rightVal)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// queryValues may be a list of values, or a single value
|
Boolean match = compareOp(leftVal, type, rightVal);
|
||||||
Object matchValue = evaluateCondition(queryValues, operator, threshold);
|
Map<String, Object> result = new HashMap<>(rightItem);
|
||||||
item.put(VALUE, matchValue);
|
if (boolModifier) {
|
||||||
// if matchValue is null, mean not match the threshold
|
result.put(VALUE, match ? 1 : 0);
|
||||||
// if not null, mean match the threshold
|
results.add(result);
|
||||||
result.add(new HashMap<>(item));
|
} else {
|
||||||
|
result.put(VALUE, match ? rightVal : null);
|
||||||
|
results.add(result);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return result;
|
return results;
|
||||||
}
|
}
|
||||||
return new LinkedList<>();
|
|
||||||
|
// vector and scalar
|
||||||
|
if (rightIsScalar) {
|
||||||
|
Object rightVal = rightResult.get(0).get(SCALAR);
|
||||||
|
for (Map<String, Object> leftItem : leftResult) {
|
||||||
|
Object leftVal = leftItem.getOrDefault(VALUE, null);
|
||||||
|
if (isValidValue(leftVal)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Boolean match = compareOp(leftVal, type, rightVal);
|
||||||
|
Map<String, Object> result = new HashMap<>(leftItem);
|
||||||
|
if (boolModifier) {
|
||||||
|
result.put(VALUE, match ? 1 : 0);
|
||||||
|
results.add(result);
|
||||||
|
} else {
|
||||||
|
result.put(VALUE, match ? leftVal : null);
|
||||||
|
results.add(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
// vector and vector
|
||||||
|
Map<String, Map<String, Object>> rightMap = rightResult.stream()
|
||||||
|
.filter(item -> item.get(VALUE) != null)
|
||||||
|
.collect(Collectors.toMap(this::labelKey, item -> item, (existing, replacement) -> existing));
|
||||||
|
|
||||||
|
for (Map<String, Object> leftItem : leftResult) {
|
||||||
|
Object leftVal = leftItem.getOrDefault(VALUE, null);
|
||||||
|
if (isValidValue(leftVal)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Map<String, Object> rightItem = rightMap.get(labelKey(leftItem));
|
||||||
|
if (rightItem == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Object rightVal = rightItem.get(VALUE);
|
||||||
|
if (isValidValue(rightVal)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Boolean match = compareOp(leftVal, type, rightVal);
|
||||||
|
Map<String, Object> result = new HashMap<>(leftItem);
|
||||||
|
if (boolModifier) {
|
||||||
|
result.put(VALUE, match ? 1 : 0);
|
||||||
|
results.add(result);
|
||||||
|
} else {
|
||||||
|
result.put(VALUE, match ? leftVal : null);
|
||||||
|
results.add(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<Map<String, Object>> visitAndExpr(AlertExpressionParser.AndExprContext ctx) {
|
public List<Map<String, Object>> visitAndExpr(AlertExpressionParser.AndExprContext ctx) {
|
||||||
List<Map<String, Object>> leftOperand = visit(ctx.left);
|
List<Map<String, Object>> leftOperand = visit(ctx.left);
|
||||||
List<Map<String, Object>> rightOperand = visit(ctx.right);
|
List<Map<String, Object>> rightOperand = visit(ctx.right);
|
||||||
|
List<Map<String, Object>> results = new ArrayList<>();
|
||||||
|
|
||||||
Map<String, Object> leftMap = null;
|
// build a hash set of the right-side tag collection
|
||||||
boolean leftMatch = false;
|
Set<String> rightLabelsSet = rightOperand.stream()
|
||||||
Map<String, Object> rightMap = null;
|
.filter(item -> item.get(VALUE) != null)
|
||||||
boolean rightMatch = false;
|
.map(this::labelKey)
|
||||||
for (Map<String, Object> item : leftOperand) {
|
.collect(Collectors.toSet());
|
||||||
if (leftMap == null) {
|
|
||||||
leftMap = item;
|
// iterate over the left side, O(1) match
|
||||||
|
for (Map<String, Object> leftItem : leftOperand) {
|
||||||
|
Object leftVal = leftItem.get(VALUE);
|
||||||
|
if (leftVal == null) {
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
if (item.get(VALUE) != null) {
|
String labelKey = labelKey(leftItem);
|
||||||
leftMap = item;
|
if (rightLabelsSet.contains(labelKey)) {
|
||||||
leftMatch = true;
|
results.add(new HashMap<>(leftItem));
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (Map<String, Object> item : rightOperand) {
|
return results;
|
||||||
if (rightMap == null) {
|
|
||||||
rightMap = item;
|
|
||||||
}
|
|
||||||
if (item.get(VALUE) != null) {
|
|
||||||
rightMap = item;
|
|
||||||
rightMatch = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (leftMatch && rightMatch) {
|
|
||||||
rightMap.putAll(leftMap);
|
|
||||||
return new LinkedList<>(List.of(rightMap));
|
|
||||||
} else if (leftMap != null) {
|
|
||||||
leftMap.put(VALUE, null);
|
|
||||||
return new LinkedList<>(List.of(leftMap));
|
|
||||||
} else if (rightMap != null) {
|
|
||||||
rightMap.put(VALUE, null);
|
|
||||||
return new LinkedList<>(List.of(rightMap));
|
|
||||||
}
|
|
||||||
return new LinkedList<>();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -123,98 +188,51 @@ public class AlertExpressionEvalVisitor extends AlertExpressionBaseVisitor<List<
|
|||||||
List<Map<String, Object>> leftOperand = visit(ctx.left);
|
List<Map<String, Object>> leftOperand = visit(ctx.left);
|
||||||
List<Map<String, Object>> rightOperand = visit(ctx.right);
|
List<Map<String, Object>> rightOperand = visit(ctx.right);
|
||||||
|
|
||||||
Map<String, Object> leftMap = null;
|
// build a hashMap of the left-hand label collection
|
||||||
boolean leftMatch = false;
|
Map<String, Map<String, Object>> leftLabelMap = leftOperand.stream()
|
||||||
Map<String, Object> rightMap = null;
|
.filter(item -> item.get(VALUE) != null)
|
||||||
boolean rightMatch = false;
|
.collect(Collectors.toMap(this::labelKey, HashMap::new, (k1, k2) -> k1));
|
||||||
for (Map<String, Object> item : leftOperand) {
|
|
||||||
if (leftMap == null) {
|
// first add all the non-empty items on the left side
|
||||||
leftMap = item;
|
List<Map<String, Object>> results = new ArrayList<>(leftLabelMap.values());
|
||||||
|
|
||||||
|
// add the term that has a value on the right side and not on the left side
|
||||||
|
for (Map<String, Object> rightItem : rightOperand) {
|
||||||
|
Object rightVal = rightItem.get(VALUE);
|
||||||
|
if (rightVal == null) {
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
if (item.get(VALUE) != null) {
|
String key = labelKey(rightItem);
|
||||||
leftMap = item;
|
if (!leftLabelMap.containsKey(key)) {
|
||||||
leftMatch = true;
|
results.add(new HashMap<>(rightItem));
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (Map<String, Object> item : rightOperand) {
|
return results;
|
||||||
if (rightMap == null) {
|
|
||||||
rightMap = item;
|
|
||||||
}
|
|
||||||
if (item.get(VALUE) != null) {
|
|
||||||
rightMap = item;
|
|
||||||
rightMatch = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (leftMatch && rightMatch) {
|
|
||||||
rightMap.putAll(leftMap);
|
|
||||||
return new LinkedList<>(List.of(rightMap));
|
|
||||||
} else if (leftMatch) {
|
|
||||||
return new LinkedList<>(List.of(leftMap));
|
|
||||||
} else if (rightMatch) {
|
|
||||||
return new LinkedList<>(List.of(rightMap));
|
|
||||||
} else {
|
|
||||||
if (leftMap != null && rightMap != null) {
|
|
||||||
rightMap.putAll(leftMap);
|
|
||||||
return new LinkedList<>(List.of(rightMap));
|
|
||||||
} else if (leftMap != null) {
|
|
||||||
return new LinkedList<>(List.of(leftMap));
|
|
||||||
} else if (rightMap != null) {
|
|
||||||
return new LinkedList<>(List.of(rightMap));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return new LinkedList<>();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<Map<String, Object>> visitUnlessExpr(AlertExpressionParser.UnlessExprContext ctx) {
|
public List<Map<String, Object>> visitUnlessExpr(AlertExpressionParser.UnlessExprContext ctx) {
|
||||||
List<Map<String, Object>> leftOperand = visit(ctx.left);
|
List<Map<String, Object>> leftOperand = visit(ctx.left);
|
||||||
List<Map<String, Object>> rightOperand = visit(ctx.right);
|
List<Map<String, Object>> rightOperand = visit(ctx.right);
|
||||||
Map<String, Object> leftMap = null;
|
List<Map<String, Object>> results = new ArrayList<>();
|
||||||
boolean leftMatch = false;
|
|
||||||
Map<String, Object> rightMap = null;
|
|
||||||
boolean rightMatch = false;
|
|
||||||
for (Map<String, Object> item : leftOperand) {
|
|
||||||
if (leftMap == null) {
|
|
||||||
leftMap = item;
|
|
||||||
}
|
|
||||||
if (item.get(VALUE) != null) {
|
|
||||||
leftMap = item;
|
|
||||||
leftMatch = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (Map<String, Object> item : rightOperand) {
|
|
||||||
if (rightMap == null) {
|
|
||||||
rightMap = item;
|
|
||||||
}
|
|
||||||
if (item.get(VALUE) != null) {
|
|
||||||
rightMap = item;
|
|
||||||
rightMatch = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (leftMatch && !rightMatch) {
|
|
||||||
return new LinkedList<>(List.of(leftMap));
|
|
||||||
} else {
|
|
||||||
if (leftMap != null) {
|
|
||||||
leftMap.put(VALUE, null);
|
|
||||||
return new LinkedList<>(List.of(leftMap));
|
|
||||||
} else {
|
|
||||||
if (rightMap != null) {
|
|
||||||
rightMap.put(VALUE, null);
|
|
||||||
return new LinkedList<>(List.of(rightMap));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return new LinkedList<>();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
// build a hash set of the right-side tag collection
|
||||||
public List<Map<String, Object>> visitQueryExpr(AlertExpressionParser.QueryExprContext ctx) {
|
Set<String> rightLabelSet = rightOperand.stream()
|
||||||
String query = ctx.identifier().getText();
|
.filter(item -> item.get(VALUE) != null)
|
||||||
return executor.execute(query);
|
.map(this::labelKey)
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
|
||||||
|
// iterate over the left side, O(1) match
|
||||||
|
for (Map<String, Object> leftItem : leftOperand) {
|
||||||
|
Object leftVal = leftItem.get(VALUE);
|
||||||
|
if (leftVal == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!rightLabelSet.contains(labelKey(leftItem))) {
|
||||||
|
results.add(new HashMap<>(leftItem));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -222,84 +240,83 @@ public class AlertExpressionEvalVisitor extends AlertExpressionBaseVisitor<List<
|
|||||||
double value = Double.parseDouble(ctx.number().getText());
|
double value = Double.parseDouble(ctx.number().getText());
|
||||||
List<Map<String, Object>> numAsList = new ArrayList<>();
|
List<Map<String, Object>> numAsList = new ArrayList<>();
|
||||||
Map<String, Object> valueMap = new HashMap<>();
|
Map<String, Object> valueMap = new HashMap<>();
|
||||||
valueMap.put(THRESHOLD, value);
|
valueMap.put(SCALAR, value);
|
||||||
numAsList.add(valueMap);
|
numAsList.add(valueMap);
|
||||||
return numAsList;
|
return numAsList;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Object evaluateCondition(Object value, String operator, Double threshold) {
|
@Override
|
||||||
// value may be a list of values, or a single value
|
public List<Map<String, Object>> visitPromqlExpr(AlertExpressionParser.PromqlExprContext ctx) {
|
||||||
switch (operator) {
|
String rawPromql = tokens.getText(ctx.promql());
|
||||||
case ">":
|
return executor.execute(rawPromql);
|
||||||
// if value is list, return the max value
|
}
|
||||||
if (value instanceof List<?> values) {
|
|
||||||
Double doubleValue = values.stream().map(v -> Double.valueOf(v.toString())).max(Double::compareTo).orElse(null);
|
@Override
|
||||||
if (doubleValue != null) {
|
public List<Map<String, Object>> visitSqlExpr(AlertExpressionParser.SqlExprContext ctx) {
|
||||||
return doubleValue > threshold ? doubleValue : null;
|
String rawSql = tokens.getText(ctx.selectSql());
|
||||||
} else {
|
return executor.execute(rawSql);
|
||||||
return null;
|
}
|
||||||
}
|
|
||||||
} else {
|
@Override
|
||||||
return Double.parseDouble(value.toString()) > threshold ? value : null;
|
public List<Map<String, Object>> visitSqlCallExpr(AlertExpressionParser.SqlCallExprContext ctx) {
|
||||||
}
|
return callSqlOrPromql(tokens.getText(ctx.string()));
|
||||||
case ">=":
|
}
|
||||||
if (value instanceof List<?> values) {
|
|
||||||
Double doubleValue = values.stream().map(v -> Double.valueOf(v.toString())).max(Double::compareTo).orElse(null);
|
@Override
|
||||||
if (doubleValue != null) {
|
public List<Map<String, Object>> visitPromqlCallExpr(AlertExpressionParser.PromqlCallExprContext ctx) {
|
||||||
return doubleValue >= threshold ? doubleValue : null;
|
return callSqlOrPromql(tokens.getText(ctx.string()));
|
||||||
} else {
|
}
|
||||||
return null;
|
|
||||||
}
|
private List<Map<String, Object>> callSqlOrPromql(String text) {
|
||||||
} else {
|
String script = text.substring(1, text.length() - 1);
|
||||||
return Double.parseDouble(value.toString()) >= threshold ? value : null;
|
return executor.execute(script);
|
||||||
}
|
}
|
||||||
case "<":
|
|
||||||
if (value instanceof List<?> values) {
|
/**
|
||||||
Double doubleValue = values.stream().map(v -> Double.valueOf(v.toString())).min(Double::compareTo).orElse(null);
|
* Generate tag key (excluding `__name__` and `__value__` and `__timestamp__`)
|
||||||
if (doubleValue != null) {
|
*/
|
||||||
return doubleValue < threshold ? doubleValue : null;
|
private String labelKey(Map<String, Object> labelsMap) {
|
||||||
} else {
|
if (null == labelsMap || labelsMap.isEmpty()) {
|
||||||
return null;
|
return "-";
|
||||||
}
|
}
|
||||||
} else {
|
String key = labelsMap.entrySet().stream()
|
||||||
return Double.parseDouble(value.toString()) < threshold ? value : null;
|
.filter(e -> !e.getKey().equals(VALUE) && !e.getKey().equals(NAME) && !e.getKey().equals(TIMESTAMP))
|
||||||
}
|
.sorted(Map.Entry.comparingByKey())
|
||||||
case "<=":
|
.map(e -> e.getKey() + "=" + (e.getValue() == null ? "" : e.getValue()))
|
||||||
if (value instanceof List<?> values) {
|
.collect(Collectors.joining(","));
|
||||||
Double doubleValue = values.stream().map(v -> Double.valueOf(v.toString())).min(Double::compareTo).orElse(null);
|
return key.isEmpty() ? "-" : key;
|
||||||
if (doubleValue != null) {
|
}
|
||||||
return doubleValue <= threshold ? doubleValue : null;
|
|
||||||
} else {
|
private boolean isScalar(List<Map<String, Object>> context) {
|
||||||
return null;
|
return CollectionUtils.isNotEmpty(context)
|
||||||
}
|
&& context.size() == 1
|
||||||
} else {
|
&& context.get(0).containsKey(SCALAR)
|
||||||
return Double.parseDouble(value.toString()) <= threshold ? value : null;
|
&& null != context.get(0).get(SCALAR);
|
||||||
}
|
}
|
||||||
case "==":
|
|
||||||
if (value instanceof List<?> values) {
|
private double parseStrToDouble(String text) {
|
||||||
for (Object v : values) {
|
try {
|
||||||
if (v.equals(threshold)) {
|
return Double.parseDouble(text);
|
||||||
return v;
|
} catch (NumberFormatException e) {
|
||||||
}
|
throw new ExpressionVisitorException("number format exception", e);
|
||||||
}
|
|
||||||
return null;
|
|
||||||
} else {
|
|
||||||
return value.equals(threshold) ? value : null;
|
|
||||||
}
|
|
||||||
case "!=":
|
|
||||||
if (value instanceof List<?> values) {
|
|
||||||
for (Object v : values) {
|
|
||||||
if (v.equals(threshold)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
} else {
|
|
||||||
return value.equals(threshold) ? null : value;
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
// unsupported operator todo add more operator
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
private boolean isValidValue(Object val) {
|
||||||
|
return val == null || val instanceof List<?>;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Boolean compareOp(Object leftVal, int opType, Object rightVal) {
|
||||||
|
double left = parseStrToDouble(leftVal.toString());
|
||||||
|
double right = parseStrToDouble(rightVal.toString());
|
||||||
|
return switch (opType) {
|
||||||
|
case AlertExpressionParser.GT -> left > right;
|
||||||
|
case AlertExpressionParser.GE -> left >= right;
|
||||||
|
case AlertExpressionParser.LT -> left < right;
|
||||||
|
case AlertExpressionParser.LE -> left <= right;
|
||||||
|
case AlertExpressionParser.EQ -> left == right;
|
||||||
|
case AlertExpressionParser.NE -> left != right;
|
||||||
|
default -> false;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
+89
-140
File diff suppressed because one or more lines are too long
+3284
-501
File diff suppressed because one or more lines are too long
+262
-17
@@ -26,12 +26,13 @@ import org.antlr.v4.runtime.tree.ParseTreeVisitor;
|
|||||||
* by {@link AlertExpressionParser}.
|
* by {@link AlertExpressionParser}.
|
||||||
*
|
*
|
||||||
* @param <T> The return type of the visit operation. Use {@link Void} for
|
* @param <T> The return type of the visit operation. Use {@link Void} for
|
||||||
* operations with no return type.
|
* operations with no return type.
|
||||||
*/
|
*/
|
||||||
public interface AlertExpressionVisitor<T> extends ParseTreeVisitor<T> {
|
public interface AlertExpressionVisitor<T> extends ParseTreeVisitor<T> {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Visit a parse tree produced by {@link AlertExpressionParser#expression}.
|
* Visit a parse tree produced by {@link AlertExpressionParser#expression}.
|
||||||
|
*
|
||||||
* @param ctx the parse tree
|
* @param ctx the parse tree
|
||||||
* @return the visitor result
|
* @return the visitor result
|
||||||
*/
|
*/
|
||||||
@@ -39,23 +40,17 @@ public interface AlertExpressionVisitor<T> extends ParseTreeVisitor<T> {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Visit a parse tree produced by the {@code AndExpr}
|
* Visit a parse tree produced by the {@code AndExpr}
|
||||||
* labeled alternative in AlertExpressionParser#expr
|
* labeled alternative in {@link AlertExpressionParser#expr}.
|
||||||
|
*
|
||||||
* @param ctx the parse tree
|
* @param ctx the parse tree
|
||||||
* @return the visitor result
|
* @return the visitor result
|
||||||
*/
|
*/
|
||||||
T visitAndExpr(AlertExpressionParser.AndExprContext ctx);
|
T visitAndExpr(AlertExpressionParser.AndExprContext ctx);
|
||||||
|
|
||||||
/**
|
|
||||||
* Visit a parse tree produced by the {@code QueryExpr}
|
|
||||||
* labeled alternative in AlertExpressionParser#expr
|
|
||||||
* @param ctx the parse tree
|
|
||||||
* @return the visitor result
|
|
||||||
*/
|
|
||||||
T visitQueryExpr(AlertExpressionParser.QueryExprContext ctx);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Visit a parse tree produced by the {@code ComparisonExpr}
|
* Visit a parse tree produced by the {@code ComparisonExpr}
|
||||||
* labeled alternative in AlertExpressionParser#expr
|
* labeled alternative in {@link AlertExpressionParser#expr}.
|
||||||
|
*
|
||||||
* @param ctx the parse tree
|
* @param ctx the parse tree
|
||||||
* @return the visitor result
|
* @return the visitor result
|
||||||
*/
|
*/
|
||||||
@@ -63,15 +58,35 @@ public interface AlertExpressionVisitor<T> extends ParseTreeVisitor<T> {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Visit a parse tree produced by the {@code UnlessExpr}
|
* Visit a parse tree produced by the {@code UnlessExpr}
|
||||||
* labeled alternative in AlertExpressionParser#expr
|
* labeled alternative in {@link AlertExpressionParser#expr}.
|
||||||
|
*
|
||||||
* @param ctx the parse tree
|
* @param ctx the parse tree
|
||||||
* @return the visitor result
|
* @return the visitor result
|
||||||
*/
|
*/
|
||||||
T visitUnlessExpr(AlertExpressionParser.UnlessExprContext ctx);
|
T visitUnlessExpr(AlertExpressionParser.UnlessExprContext ctx);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Visit a parse tree produced by the {@code SqlExpr}
|
||||||
|
* labeled alternative in {@link AlertExpressionParser#expr}.
|
||||||
|
*
|
||||||
|
* @param ctx the parse tree
|
||||||
|
* @return the visitor result
|
||||||
|
*/
|
||||||
|
T visitSqlExpr(AlertExpressionParser.SqlExprContext ctx);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Visit a parse tree produced by the {@code SqlCallExpr}
|
||||||
|
* labeled alternative in {@link AlertExpressionParser#expr}.
|
||||||
|
*
|
||||||
|
* @param ctx the parse tree
|
||||||
|
* @return the visitor result
|
||||||
|
*/
|
||||||
|
T visitSqlCallExpr(AlertExpressionParser.SqlCallExprContext ctx);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Visit a parse tree produced by the {@code LiteralExpr}
|
* Visit a parse tree produced by the {@code LiteralExpr}
|
||||||
* labeled alternative in AlertExpressionParser#expr
|
* labeled alternative in {@link AlertExpressionParser#expr}.
|
||||||
|
*
|
||||||
* @param ctx the parse tree
|
* @param ctx the parse tree
|
||||||
* @return the visitor result
|
* @return the visitor result
|
||||||
*/
|
*/
|
||||||
@@ -79,31 +94,261 @@ public interface AlertExpressionVisitor<T> extends ParseTreeVisitor<T> {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Visit a parse tree produced by the {@code ParenExpr}
|
* Visit a parse tree produced by the {@code ParenExpr}
|
||||||
* labeled alternative in AlertExpressionParser#expr
|
* labeled alternative in {@link AlertExpressionParser#expr}.
|
||||||
|
*
|
||||||
* @param ctx the parse tree
|
* @param ctx the parse tree
|
||||||
* @return the visitor result
|
* @return the visitor result
|
||||||
*/
|
*/
|
||||||
T visitParenExpr(AlertExpressionParser.ParenExprContext ctx);
|
T visitParenExpr(AlertExpressionParser.ParenExprContext ctx);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Visit a parse tree produced by the {@code PromqlCallExpr}
|
||||||
|
* labeled alternative in {@link AlertExpressionParser#expr}.
|
||||||
|
*
|
||||||
|
* @param ctx the parse tree
|
||||||
|
* @return the visitor result
|
||||||
|
*/
|
||||||
|
T visitPromqlCallExpr(AlertExpressionParser.PromqlCallExprContext ctx);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Visit a parse tree produced by the {@code PromqlExpr}
|
||||||
|
* labeled alternative in {@link AlertExpressionParser#expr}.
|
||||||
|
*
|
||||||
|
* @param ctx the parse tree
|
||||||
|
* @return the visitor result
|
||||||
|
*/
|
||||||
|
T visitPromqlExpr(AlertExpressionParser.PromqlExprContext ctx);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Visit a parse tree produced by the {@code OrExpr}
|
* Visit a parse tree produced by the {@code OrExpr}
|
||||||
* labeled alternative in AlertExpressionParser#expr
|
* labeled alternative in {@link AlertExpressionParser#expr}.
|
||||||
|
*
|
||||||
* @param ctx the parse tree
|
* @param ctx the parse tree
|
||||||
* @return the visitor result
|
* @return the visitor result
|
||||||
*/
|
*/
|
||||||
T visitOrExpr(AlertExpressionParser.OrExprContext ctx);
|
T visitOrExpr(AlertExpressionParser.OrExprContext ctx);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Visit a parse tree produced by {@link AlertExpressionParser#identifier}.
|
* Visit a parse tree produced by {@link AlertExpressionParser#functionCall}.
|
||||||
|
*
|
||||||
* @param ctx the parse tree
|
* @param ctx the parse tree
|
||||||
* @return the visitor result
|
* @return the visitor result
|
||||||
*/
|
*/
|
||||||
T visitIdentifier(AlertExpressionParser.IdentifierContext ctx);
|
T visitFunctionCall(AlertExpressionParser.FunctionCallContext ctx);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Visit a parse tree produced by {@link AlertExpressionParser#parameterList}.
|
||||||
|
*
|
||||||
|
* @param ctx the parse tree
|
||||||
|
* @return the visitor result
|
||||||
|
*/
|
||||||
|
T visitParameterList(AlertExpressionParser.ParameterListContext ctx);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Visit a parse tree produced by {@link AlertExpressionParser#parameter}.
|
||||||
|
*
|
||||||
|
* @param ctx the parse tree
|
||||||
|
* @return the visitor result
|
||||||
|
*/
|
||||||
|
T visitParameter(AlertExpressionParser.ParameterContext ctx);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Visit a parse tree produced by {@link AlertExpressionParser#number}.
|
* Visit a parse tree produced by {@link AlertExpressionParser#number}.
|
||||||
|
*
|
||||||
* @param ctx the parse tree
|
* @param ctx the parse tree
|
||||||
* @return the visitor result
|
* @return the visitor result
|
||||||
*/
|
*/
|
||||||
T visitNumber(AlertExpressionParser.NumberContext ctx);
|
T visitNumber(AlertExpressionParser.NumberContext ctx);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Visit a parse tree produced by {@link AlertExpressionParser#string}.
|
||||||
|
*
|
||||||
|
* @param ctx the parse tree
|
||||||
|
* @return the visitor result
|
||||||
|
*/
|
||||||
|
T visitString(AlertExpressionParser.StringContext ctx);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Visit a parse tree produced by {@link AlertExpressionParser#duration}.
|
||||||
|
*
|
||||||
|
* @param ctx the parse tree
|
||||||
|
* @return the visitor result
|
||||||
|
*/
|
||||||
|
T visitDuration(AlertExpressionParser.DurationContext ctx);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Visit a parse tree produced by {@link AlertExpressionParser#functionName}.
|
||||||
|
*
|
||||||
|
* @param ctx the parse tree
|
||||||
|
* @return the visitor result
|
||||||
|
*/
|
||||||
|
T visitFunctionName(AlertExpressionParser.FunctionNameContext ctx);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Visit a parse tree produced by {@link AlertExpressionParser#selectSql}.
|
||||||
|
*
|
||||||
|
* @param ctx the parse tree
|
||||||
|
* @return the visitor result
|
||||||
|
*/
|
||||||
|
T visitSelectSql(AlertExpressionParser.SelectSqlContext ctx);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Visit a parse tree produced by {@link AlertExpressionParser#selectFieldList}.
|
||||||
|
*
|
||||||
|
* @param ctx the parse tree
|
||||||
|
* @return the visitor result
|
||||||
|
*/
|
||||||
|
T visitSelectFieldList(AlertExpressionParser.SelectFieldListContext ctx);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Visit a parse tree produced by {@link AlertExpressionParser#selectField}.
|
||||||
|
*
|
||||||
|
* @param ctx the parse tree
|
||||||
|
* @return the visitor result
|
||||||
|
*/
|
||||||
|
T visitSelectField(AlertExpressionParser.SelectFieldContext ctx);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Visit a parse tree produced by {@link AlertExpressionParser#groupByList}.
|
||||||
|
*
|
||||||
|
* @param ctx the parse tree
|
||||||
|
* @return the visitor result
|
||||||
|
*/
|
||||||
|
T visitGroupByList(AlertExpressionParser.GroupByListContext ctx);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Visit a parse tree produced by {@link AlertExpressionParser#orderByList}.
|
||||||
|
*
|
||||||
|
* @param ctx the parse tree
|
||||||
|
* @return the visitor result
|
||||||
|
*/
|
||||||
|
T visitOrderByList(AlertExpressionParser.OrderByListContext ctx);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Visit a parse tree produced by {@link AlertExpressionParser#orderByField}.
|
||||||
|
*
|
||||||
|
* @param ctx the parse tree
|
||||||
|
* @return the visitor result
|
||||||
|
*/
|
||||||
|
T visitOrderByField(AlertExpressionParser.OrderByFieldContext ctx);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Visit a parse tree produced by {@link AlertExpressionParser#limitClause}.
|
||||||
|
*
|
||||||
|
* @param ctx the parse tree
|
||||||
|
* @return the visitor result
|
||||||
|
*/
|
||||||
|
T visitLimitClause(AlertExpressionParser.LimitClauseContext ctx);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Visit a parse tree produced by {@link AlertExpressionParser#relList}.
|
||||||
|
*
|
||||||
|
* @param ctx the parse tree
|
||||||
|
* @return the visitor result
|
||||||
|
*/
|
||||||
|
T visitRelList(AlertExpressionParser.RelListContext ctx);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Visit a parse tree produced by {@link AlertExpressionParser#relation}.
|
||||||
|
*
|
||||||
|
* @param ctx the parse tree
|
||||||
|
* @return the visitor result
|
||||||
|
*/
|
||||||
|
T visitRelation(AlertExpressionParser.RelationContext ctx);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Visit a parse tree produced by {@link AlertExpressionParser#conditionList}.
|
||||||
|
*
|
||||||
|
* @param ctx the parse tree
|
||||||
|
* @return the visitor result
|
||||||
|
*/
|
||||||
|
T visitConditionList(AlertExpressionParser.ConditionListContext ctx);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Visit a parse tree produced by {@link AlertExpressionParser#compOp}.
|
||||||
|
*
|
||||||
|
* @param ctx the parse tree
|
||||||
|
* @return the visitor result
|
||||||
|
*/
|
||||||
|
T visitCompOp(AlertExpressionParser.CompOpContext ctx);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Visit a parse tree produced by {@link AlertExpressionParser#condition}.
|
||||||
|
*
|
||||||
|
* @param ctx the parse tree
|
||||||
|
* @return the visitor result
|
||||||
|
*/
|
||||||
|
T visitCondition(AlertExpressionParser.ConditionContext ctx);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Visit a parse tree produced by {@link AlertExpressionParser#conditionUnit}.
|
||||||
|
*
|
||||||
|
* @param ctx the parse tree
|
||||||
|
* @return the visitor result
|
||||||
|
*/
|
||||||
|
T visitConditionUnit(AlertExpressionParser.ConditionUnitContext ctx);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Visit a parse tree produced by {@link AlertExpressionParser#promql}.
|
||||||
|
*
|
||||||
|
* @param ctx the parse tree
|
||||||
|
* @return the visitor result
|
||||||
|
*/
|
||||||
|
T visitPromql(AlertExpressionParser.PromqlContext ctx);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Visit a parse tree produced by {@link AlertExpressionParser#metricSelector}.
|
||||||
|
*
|
||||||
|
* @param ctx the parse tree
|
||||||
|
* @return the visitor result
|
||||||
|
*/
|
||||||
|
T visitMetricSelector(AlertExpressionParser.MetricSelectorContext ctx);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Visit a parse tree produced by {@link AlertExpressionParser#labelMatcherList}.
|
||||||
|
*
|
||||||
|
* @param ctx the parse tree
|
||||||
|
* @return the visitor result
|
||||||
|
*/
|
||||||
|
T visitLabelMatcherList(AlertExpressionParser.LabelMatcherListContext ctx);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Visit a parse tree produced by {@link AlertExpressionParser#labelMatcherItem}.
|
||||||
|
*
|
||||||
|
* @param ctx the parse tree
|
||||||
|
* @return the visitor result
|
||||||
|
*/
|
||||||
|
T visitLabelMatcherItem(AlertExpressionParser.LabelMatcherItemContext ctx);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Visit a parse tree produced by {@link AlertExpressionParser#labelMatcherOp}.
|
||||||
|
*
|
||||||
|
* @param ctx the parse tree
|
||||||
|
* @return the visitor result
|
||||||
|
*/
|
||||||
|
T visitLabelMatcherOp(AlertExpressionParser.LabelMatcherOpContext ctx);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Visit a parse tree produced by {@link AlertExpressionParser#labelList}.
|
||||||
|
*
|
||||||
|
* @param ctx the parse tree
|
||||||
|
* @return the visitor result
|
||||||
|
*/
|
||||||
|
T visitLabelList(AlertExpressionParser.LabelListContext ctx);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Visit a parse tree produced by {@link AlertExpressionParser#instantVectorOp}.
|
||||||
|
*
|
||||||
|
* @param ctx the parse tree
|
||||||
|
* @return the visitor result
|
||||||
|
*/
|
||||||
|
T visitInstantVectorOp(AlertExpressionParser.InstantVectorOpContext ctx);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Visit a parse tree produced by {@link AlertExpressionParser#aggregationOperator}.
|
||||||
|
*
|
||||||
|
* @param ctx the parse tree
|
||||||
|
* @return the visitor result
|
||||||
|
*/
|
||||||
|
T visitAggregationOperator(AlertExpressionParser.AggregationOperatorContext ctx);
|
||||||
}
|
}
|
||||||
+20
-2
@@ -18,12 +18,17 @@
|
|||||||
package org.apache.hertzbeat.alert.notice.impl;
|
package org.apache.hertzbeat.alert.notice.impl;
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
|
||||||
|
import java.net.URLEncoder;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.apache.hertzbeat.alert.notice.AlertNoticeException;
|
import org.apache.hertzbeat.alert.notice.AlertNoticeException;
|
||||||
|
import org.apache.hertzbeat.alert.util.CryptoUtils;
|
||||||
import org.apache.hertzbeat.common.entity.alerter.GroupAlert;
|
import org.apache.hertzbeat.common.entity.alerter.GroupAlert;
|
||||||
import org.apache.hertzbeat.common.entity.alerter.NoticeReceiver;
|
import org.apache.hertzbeat.common.entity.alerter.NoticeReceiver;
|
||||||
import org.apache.hertzbeat.common.entity.alerter.NoticeTemplate;
|
import org.apache.hertzbeat.common.entity.alerter.NoticeTemplate;
|
||||||
@@ -54,7 +59,13 @@ final class DingTalkRobotAlertNotifyHandlerImpl extends AbstractAlertNotifyHandl
|
|||||||
HttpHeaders headers = new HttpHeaders();
|
HttpHeaders headers = new HttpHeaders();
|
||||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||||
HttpEntity<DingTalkWebHookDto> httpEntity = new HttpEntity<>(dingTalkWebHookDto, headers);
|
HttpEntity<DingTalkWebHookDto> httpEntity = new HttpEntity<>(dingTalkWebHookDto, headers);
|
||||||
String webHookUrl = alerterProperties.getDingTalkWebhookUrl() + receiver.getAccessToken();
|
StringBuilder webHookUrlBuilder = new StringBuilder()
|
||||||
|
.append(alerterProperties.getDingTalkWebhookUrl())
|
||||||
|
.append(receiver.getAccessToken());
|
||||||
|
if (StringUtils.isNotBlank(receiver.getAppSecret())) {
|
||||||
|
webHookUrlBuilder.append(signSecret(receiver.getAppSecret()));
|
||||||
|
}
|
||||||
|
String webHookUrl = webHookUrlBuilder.toString();
|
||||||
ResponseEntity<CommonRobotNotifyResp> responseEntity = restTemplate.postForEntity(webHookUrl,
|
ResponseEntity<CommonRobotNotifyResp> responseEntity = restTemplate.postForEntity(webHookUrl,
|
||||||
httpEntity, CommonRobotNotifyResp.class);
|
httpEntity, CommonRobotNotifyResp.class);
|
||||||
if (responseEntity.getStatusCode() == HttpStatus.OK) {
|
if (responseEntity.getStatusCode() == HttpStatus.OK) {
|
||||||
@@ -102,6 +113,13 @@ final class DingTalkRobotAlertNotifyHandlerImpl extends AbstractAlertNotifyHandl
|
|||||||
return dingTalkWebHookDto;
|
return dingTalkWebHookDto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String signSecret(String secret) throws Exception {
|
||||||
|
Long timestamp = System.currentTimeMillis();
|
||||||
|
String sign = URLEncoder.encode(CryptoUtils.hmacSha256Base64(secret, timestamp + "\n" + secret),
|
||||||
|
StandardCharsets.UTF_8);
|
||||||
|
return String.format("×tamp=%s&sign=%s", timestamp, sign);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public byte type() {
|
public byte type() {
|
||||||
return 5;
|
return 5;
|
||||||
@@ -109,6 +127,7 @@ final class DingTalkRobotAlertNotifyHandlerImpl extends AbstractAlertNotifyHandl
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* DingTalk robot request body
|
* DingTalk robot request body
|
||||||
|
*
|
||||||
* @version 1.0
|
* @version 1.0
|
||||||
*/
|
*/
|
||||||
@Data
|
@Data
|
||||||
@@ -187,5 +206,4 @@ final class DingTalkRobotAlertNotifyHandlerImpl extends AbstractAlertNotifyHandl
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -24,7 +24,7 @@ import java.util.Properties;
|
|||||||
import java.util.ResourceBundle;
|
import java.util.ResourceBundle;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.hertzbeat.alert.dto.MailServerConfig;
|
import org.apache.hertzbeat.common.entity.dto.MailServerConfig;
|
||||||
import org.apache.hertzbeat.alert.notice.AlertNoticeException;
|
import org.apache.hertzbeat.alert.notice.AlertNoticeException;
|
||||||
import org.apache.hertzbeat.common.entity.alerter.GroupAlert;
|
import org.apache.hertzbeat.common.entity.alerter.GroupAlert;
|
||||||
import org.apache.hertzbeat.common.entity.manager.GeneralConfig;
|
import org.apache.hertzbeat.common.entity.manager.GeneralConfig;
|
||||||
|
|||||||
+487
@@ -0,0 +1,487 @@
|
|||||||
|
/*
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||||
|
* contributor license agreements. See the NOTICE file distributed with
|
||||||
|
* this work for additional information regarding copyright ownership.
|
||||||
|
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||||
|
* (the "License"); you may not use this file except in compliance with
|
||||||
|
* the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.apache.hertzbeat.alert.notice.impl;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.apache.hertzbeat.alert.AlerterProperties;
|
||||||
|
import org.apache.hertzbeat.alert.notice.AlertNoticeException;
|
||||||
|
import org.apache.hertzbeat.common.entity.alerter.GroupAlert;
|
||||||
|
import org.apache.hertzbeat.common.entity.alerter.NoticeReceiver;
|
||||||
|
import org.apache.hertzbeat.common.entity.alerter.NoticeTemplate;
|
||||||
|
import org.apache.hertzbeat.common.util.JsonUtil;
|
||||||
|
import org.springframework.http.HttpEntity;
|
||||||
|
import org.springframework.http.HttpHeaders;
|
||||||
|
import org.springframework.http.HttpMethod;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.web.client.RestTemplate;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FeiShu app alert notify impl
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Slf4j
|
||||||
|
public class FeiShuAppAlertNotifyHandlerImpl extends AbstractAlertNotifyHandlerImpl {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* get tenant access_token url
|
||||||
|
*/
|
||||||
|
private static final String TENANT_ACCESS_TOKEN_URL = "https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* get FeiShu app employee url
|
||||||
|
*/
|
||||||
|
private static final String EMPLOYEE_URL = "https://open.feishu.cn/open-apis/ehr/v1/employees?status=2&status=4&user_id_type=user_id&page_size=100";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* send FeiShu app message url
|
||||||
|
*/
|
||||||
|
private static final String APP_MESSAGE_URL = "https://open.feishu.cn/open-apis/im/v1/messages";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* send FeiShu app batch message url
|
||||||
|
*/
|
||||||
|
private static final String APP_BATCH_MESSAGE_URL = "https://open.feishu.cn/open-apis/message/v4/batch_send/";
|
||||||
|
|
||||||
|
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
|
||||||
|
private static final byte USER_RECEIVE_TYPE = 0;
|
||||||
|
private static final byte CHAT_RECEIVE_TYPE = 1;
|
||||||
|
private static final byte PART_RECEIVE_TYPE = 2;
|
||||||
|
private static final byte ALL_RECEIVE_TYPE = 3;
|
||||||
|
|
||||||
|
private final RestTemplate restTemplate;
|
||||||
|
private final AlerterProperties alerterProperties;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void send(NoticeReceiver receiver, NoticeTemplate noticeTemplate, GroupAlert alert) throws AlertNoticeException {
|
||||||
|
var appId = receiver.getAppId();
|
||||||
|
var appSecret = receiver.getAppSecret();
|
||||||
|
var larkReceiveIdType = receiver.getLarkReceiveType();
|
||||||
|
try {
|
||||||
|
var accessToken = getAccessToken(appId, appSecret);
|
||||||
|
var notificationContent = JsonUtil.toJson(renderContent(noticeTemplate, alert));
|
||||||
|
JsonNode messageContent = createLarkMessage(receiver, notificationContent);
|
||||||
|
switch (larkReceiveIdType) {
|
||||||
|
case USER_RECEIVE_TYPE -> {
|
||||||
|
String[] userIds = receiver.getUserId().split(",");
|
||||||
|
if (userIds.length == 1) {
|
||||||
|
sendLarkMessage(accessToken, "user_id", userIds[0], messageContent);
|
||||||
|
} else {
|
||||||
|
sendLarkUserBatchMessage(accessToken, userIds, messageContent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case CHAT_RECEIVE_TYPE -> sendLarkMessage(accessToken, "chat_id", receiver.getChatId(), messageContent);
|
||||||
|
case PART_RECEIVE_TYPE ->
|
||||||
|
sendLarkDepartmentBatchMessage(accessToken, receiver.getPartyId().split(","), messageContent);
|
||||||
|
case ALL_RECEIVE_TYPE -> {
|
||||||
|
List<String> userIds = new ArrayList<>();
|
||||||
|
getLarkEmployeeUserIds(accessToken, null, userIds);
|
||||||
|
sendLarkUserBatchMessage(accessToken, userIds.toArray(new String[0]), messageContent);
|
||||||
|
}
|
||||||
|
default -> throw new AlertNoticeException("Invalid larkReceiveIdType: " + larkReceiveIdType);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new AlertNoticeException("[FeiShu App Notify Error] " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public byte type() {
|
||||||
|
return 14;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send FeiShu app message to chat or designated personnel
|
||||||
|
*
|
||||||
|
* @param accessToken Tenant access token
|
||||||
|
* @param receiverIdType FeiShu app send message receiver id type: user_id, chat_id
|
||||||
|
* @param receiverId FeiShu app user id or chat id
|
||||||
|
* @param messageContent Message content
|
||||||
|
* @see <a href="https://open.feishu.cn/document/server-docs/im-v1/batch_message/send-messages-in-batches">send message</a>
|
||||||
|
*/
|
||||||
|
private void sendLarkMessage(String accessToken, String receiverIdType, String receiverId, JsonNode messageContent) throws JsonProcessingException {
|
||||||
|
HttpHeaders headers = new HttpHeaders();
|
||||||
|
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||||
|
headers.setBearerAuth(accessToken);
|
||||||
|
FeiShuAppMessageDto messageDto = FeiShuAppMessageDto.builder()
|
||||||
|
.receiveId(receiverId)
|
||||||
|
.content(escapedCompactJson(messageContent))
|
||||||
|
.build();
|
||||||
|
HttpEntity<FeiShuAppMessageDto> request = new HttpEntity<>(messageDto, headers);
|
||||||
|
call(APP_MESSAGE_URL + "?receive_id_type=" + receiverIdType, request, HttpMethod.POST, FeiShuAppResponse.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send FeiShu app department batch message
|
||||||
|
*
|
||||||
|
* @param accessToken Tenant access token
|
||||||
|
* @param partyIds FeiShu app department ids
|
||||||
|
* @param messageContent Message content
|
||||||
|
* @see <a href="https://open.feishu.cn/document/server-docs/im-v1/batch_message/send-messages-in-batches">send batch message</a>
|
||||||
|
*/
|
||||||
|
private void sendLarkDepartmentBatchMessage(String accessToken, String[] partyIds, JsonNode messageContent) {
|
||||||
|
HttpHeaders headers = new HttpHeaders();
|
||||||
|
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||||
|
headers.setBearerAuth(accessToken);
|
||||||
|
FeiShuAppBatchMessageDto batchMessageDto = FeiShuAppBatchMessageDto.builder()
|
||||||
|
.departmentIds(partyIds)
|
||||||
|
.card(messageContent)
|
||||||
|
.build();
|
||||||
|
HttpEntity<FeiShuAppBatchMessageDto> request = new HttpEntity<>(batchMessageDto, headers);
|
||||||
|
call(APP_BATCH_MESSAGE_URL, request, HttpMethod.POST, FeiShuAppResponse.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send FeiShu app batch message to user
|
||||||
|
*
|
||||||
|
* @param accessToken Tenant access token
|
||||||
|
* @param userIds FeiShu app user ids
|
||||||
|
* @param messageContent Message content
|
||||||
|
* @see <a href="https://open.feishu.cn/document/server-docs/im-v1/batch_message/send-messages-in-batches">send batch message</a>
|
||||||
|
*/
|
||||||
|
private void sendLarkUserBatchMessage(String accessToken, String[] userIds, JsonNode messageContent) {
|
||||||
|
HttpHeaders headers = new HttpHeaders();
|
||||||
|
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||||
|
headers.setBearerAuth(accessToken);
|
||||||
|
FeiShuAppBatchMessageDto batchMessageDto = FeiShuAppBatchMessageDto.builder()
|
||||||
|
.userIds(userIds)
|
||||||
|
.card(messageContent)
|
||||||
|
.build();
|
||||||
|
HttpEntity<FeiShuAppBatchMessageDto> request = new HttpEntity<>(batchMessageDto, headers);
|
||||||
|
FeiShuAppResponse call = call(APP_BATCH_MESSAGE_URL, request, HttpMethod.POST, FeiShuAppResponse.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get FeiShu app tenant access token
|
||||||
|
*
|
||||||
|
* @param appId Unique identifier for the application, obtained after creating the application
|
||||||
|
* @param appSecret Application key, obtained after creating the application
|
||||||
|
* @return Tenant access token
|
||||||
|
* @see <a href="https://open.feishu.cn/document/server-docs/authentication-management/access-token/tenant_access_token_internal">tenant_access_token</a>
|
||||||
|
*/
|
||||||
|
private String getAccessToken(String appId, String appSecret) {
|
||||||
|
HttpHeaders headers = new HttpHeaders();
|
||||||
|
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||||
|
FeiShuAppAccessTokenDto accessTokenRequest = FeiShuAppAccessTokenDto.builder()
|
||||||
|
.appId(appId)
|
||||||
|
.appSecret(appSecret)
|
||||||
|
.build();
|
||||||
|
HttpEntity<FeiShuAppAccessTokenDto> request = new HttpEntity<>(accessTokenRequest, headers);
|
||||||
|
FeiShuAppAccessTokenResponse data = call(TENANT_ACCESS_TOKEN_URL, request, HttpMethod.POST, FeiShuAppAccessTokenResponse.class);
|
||||||
|
return data.getTenantAccessToken();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get FeiShu app employee user ids
|
||||||
|
*
|
||||||
|
* @param accessToken FeiShu app tenant access token
|
||||||
|
* @param pageToken Paging marker, left blank for the first request, indicating traversal from scratch;
|
||||||
|
* When there are more items in the pagination query result, a new page_token will be returned at the same time.
|
||||||
|
* The next iteration can use this page_token to obtain the query result
|
||||||
|
* @param userIds Collection for recursive padding
|
||||||
|
* @see <a href="https://open.feishu.cn/document/server-docs/ehr-v1/list?appId=cli_a999532b1f52900b">https://open.feishu.cn/open-apis/ehr/v1/employees</a>
|
||||||
|
*/
|
||||||
|
private void getLarkEmployeeUserIds(String accessToken, String pageToken, List<String> userIds) {
|
||||||
|
HttpHeaders headers = new HttpHeaders();
|
||||||
|
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||||
|
headers.setBearerAuth(accessToken);
|
||||||
|
HttpEntity<String> request = new HttpEntity<>(headers);
|
||||||
|
var url = StringUtils.isNotBlank(pageToken) ? EMPLOYEE_URL + "&page_token=" + pageToken : EMPLOYEE_URL;
|
||||||
|
FeiShuAppEmployeeResponse employeeResponse = call(url,
|
||||||
|
request,
|
||||||
|
HttpMethod.GET,
|
||||||
|
FeiShuAppEmployeeResponse.class);
|
||||||
|
if (Objects.equals(employeeResponse.getCode(), 0)) {
|
||||||
|
userIds.addAll(employeeResponse.getData().getItems().stream().map(FeiShuAppEmployeeResponse.Employee::getUserId).toList());
|
||||||
|
var hasMore = employeeResponse.getData().getHasMore();
|
||||||
|
if (Boolean.TRUE.equals(hasMore)) {
|
||||||
|
getLarkEmployeeUserIds(accessToken, employeeResponse.getData().getPageToken(), userIds);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private <R extends FeiShuAppResponse, E> R call(String url, HttpEntity<E> request, HttpMethod httpMethod, Class<R> responseType) {
|
||||||
|
ResponseEntity<R> response = restTemplate.exchange(url, httpMethod, request, responseType);
|
||||||
|
if (Objects.nonNull(response.getBody()) && !Objects.equals(response.getBody().getCode(), 0)) {
|
||||||
|
log.warn("Send FeiShu App Error: {}", response.getBody().getMsg());
|
||||||
|
throw new AlertNoticeException("Http StatusCode " + response.getStatusCode() + " Error: " + response.getBody().getMsg());
|
||||||
|
}
|
||||||
|
return response.getBody();
|
||||||
|
}
|
||||||
|
|
||||||
|
private JsonNode createLarkMessage(NoticeReceiver receiver, String notificationContent) throws JsonProcessingException {
|
||||||
|
String larkCardMessage = """
|
||||||
|
{
|
||||||
|
"schema": "2.0",
|
||||||
|
"config": {
|
||||||
|
"update_multi": true,
|
||||||
|
"locales": [
|
||||||
|
"en_us",
|
||||||
|
"zh_cn"
|
||||||
|
],
|
||||||
|
"style": {
|
||||||
|
"text_size": {
|
||||||
|
"normal_v2": {
|
||||||
|
"default": "normal",
|
||||||
|
"pc": "normal",
|
||||||
|
"mobile": "heading"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"body": {
|
||||||
|
"direction": "vertical",
|
||||||
|
"padding": "12px 12px 12px 12px",
|
||||||
|
"elements": [
|
||||||
|
{
|
||||||
|
"tag": "markdown",
|
||||||
|
"content": "%s",
|
||||||
|
"i18n_content": {
|
||||||
|
"en_us": ""
|
||||||
|
},
|
||||||
|
"text_align": "left",
|
||||||
|
"text_size": "normal_v2",
|
||||||
|
"margin": "0px 0px 0px 0px"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "hr",
|
||||||
|
"margin": "0px 0px 0px 0px"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "column_set",
|
||||||
|
"horizontal_align": "left",
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"tag": "column",
|
||||||
|
"width": "weighted",
|
||||||
|
"elements": [
|
||||||
|
{
|
||||||
|
"tag": "button",
|
||||||
|
"text": {
|
||||||
|
"tag": "plain_text",
|
||||||
|
"content": "登入控制台",
|
||||||
|
"i18n_content": {
|
||||||
|
"en_us": "Login In"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "default",
|
||||||
|
"width": "default",
|
||||||
|
"size": "medium",
|
||||||
|
"behaviors": [
|
||||||
|
{
|
||||||
|
"type": "open_url",
|
||||||
|
"default_url": "%s",
|
||||||
|
"pc_url": "",
|
||||||
|
"ios_url": "",
|
||||||
|
"android_url": ""
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"direction": "horizontal",
|
||||||
|
"vertical_spacing": "8px",
|
||||||
|
"horizontal_align": "left",
|
||||||
|
"vertical_align": "top",
|
||||||
|
"weight": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"margin": "0px 0px 0px 0px"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"header": {
|
||||||
|
"title": {
|
||||||
|
"tag": "plain_text",
|
||||||
|
"content": "HertzBeat 告警",
|
||||||
|
"i18n_content": {
|
||||||
|
"en_us": "HertzBeat Alarm"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"subtitle": {
|
||||||
|
"tag": "plain_text",
|
||||||
|
"content": ""
|
||||||
|
},
|
||||||
|
"template": "red",
|
||||||
|
"padding": "12px 12px 12px 12px"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
Byte larkReceiveIdType = receiver.getLarkReceiveType();
|
||||||
|
String userId = receiver.getUserId();
|
||||||
|
String atUserElement = "";
|
||||||
|
if (Objects.equals(larkReceiveIdType, CHAT_RECEIVE_TYPE) && StringUtils.isNotBlank(userId)) {
|
||||||
|
atUserElement = "\\n" + Arrays.stream(userId.split(","))
|
||||||
|
.map(id -> "<at id=" + id + "></at>")
|
||||||
|
.collect(Collectors.joining(" "));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (notificationContent.startsWith("\"") && notificationContent.endsWith("\"")) {
|
||||||
|
notificationContent = StringUtils.removeStart(notificationContent, "\"");
|
||||||
|
notificationContent = StringUtils.removeEnd(notificationContent, "\"");
|
||||||
|
}
|
||||||
|
String jsonStr = String.format(larkCardMessage,
|
||||||
|
notificationContent.replace("\"", "\\\"") + atUserElement,
|
||||||
|
alerterProperties.getConsoleUrl());
|
||||||
|
return OBJECT_MAPPER.readTree(jsonStr);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private String escapedCompactJson(JsonNode json) throws JsonProcessingException {
|
||||||
|
return OBJECT_MAPPER.writeValueAsString(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* feiShu app response
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
protected static class FeiShuAppResponse {
|
||||||
|
|
||||||
|
private Integer code;
|
||||||
|
|
||||||
|
private String msg;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FeiShu app message get tenant access token request
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@AllArgsConstructor
|
||||||
|
@NoArgsConstructor
|
||||||
|
protected static class FeiShuAppAccessTokenDto {
|
||||||
|
|
||||||
|
@JsonProperty("app_id")
|
||||||
|
private String appId;
|
||||||
|
|
||||||
|
@JsonProperty("app_secret")
|
||||||
|
private String appSecret;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FeiShu app message get tenant access token response
|
||||||
|
*/
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
@Data
|
||||||
|
protected static class FeiShuAppAccessTokenResponse extends FeiShuAppResponse {
|
||||||
|
|
||||||
|
@JsonProperty("tenant_access_token")
|
||||||
|
private String tenantAccessToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FeiShu app employee response
|
||||||
|
*/
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
@Data
|
||||||
|
protected static class FeiShuAppEmployeeResponse extends FeiShuAppResponse {
|
||||||
|
|
||||||
|
private EmployeeResponseData data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
private static class EmployeeResponseData {
|
||||||
|
@JsonProperty("page_token")
|
||||||
|
private String pageToken;
|
||||||
|
|
||||||
|
@JsonProperty("has_more")
|
||||||
|
private Boolean hasMore;
|
||||||
|
|
||||||
|
private List<Employee> items;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
private static class Employee {
|
||||||
|
|
||||||
|
@JsonProperty("user_id")
|
||||||
|
private String userId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FeiShu app message request
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@AllArgsConstructor
|
||||||
|
@NoArgsConstructor
|
||||||
|
protected static class FeiShuAppMessageDto {
|
||||||
|
|
||||||
|
@JsonProperty("receive_id")
|
||||||
|
private String receiveId;
|
||||||
|
|
||||||
|
@Builder.Default
|
||||||
|
@JsonProperty("msg_type")
|
||||||
|
private String msgType = "interactive";
|
||||||
|
|
||||||
|
@JsonProperty("content")
|
||||||
|
private String content;
|
||||||
|
|
||||||
|
@Builder.Default
|
||||||
|
private final String uuid = UUID.randomUUID().toString();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FeiShu app batch message request
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@AllArgsConstructor
|
||||||
|
@NoArgsConstructor
|
||||||
|
protected static class FeiShuAppBatchMessageDto {
|
||||||
|
|
||||||
|
@Builder.Default
|
||||||
|
@JsonProperty("msg_type")
|
||||||
|
private final String msgType = "interactive";
|
||||||
|
|
||||||
|
private JsonNode card;
|
||||||
|
|
||||||
|
@JsonProperty("department_ids")
|
||||||
|
private String[] departmentIds;
|
||||||
|
|
||||||
|
@JsonProperty("user_ids")
|
||||||
|
private String[] userIds;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
+115
-110
@@ -17,8 +17,6 @@
|
|||||||
|
|
||||||
package org.apache.hertzbeat.alert.notice.impl;
|
package org.apache.hertzbeat.alert.notice.impl;
|
||||||
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
@@ -29,11 +27,14 @@ import org.apache.hertzbeat.common.entity.alerter.NoticeTemplate;
|
|||||||
import org.apache.hertzbeat.common.util.JsonUtil;
|
import org.apache.hertzbeat.common.util.JsonUtil;
|
||||||
import org.springframework.http.HttpEntity;
|
import org.springframework.http.HttpEntity;
|
||||||
import org.springframework.http.HttpHeaders;
|
import org.springframework.http.HttpHeaders;
|
||||||
import org.springframework.http.HttpStatus;
|
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Send alert information through FeiShu
|
* Send alert information through FeiShu
|
||||||
*/
|
*/
|
||||||
@@ -47,19 +48,18 @@ final class FlyBookAlertNotifyHandlerImpl extends AbstractAlertNotifyHandlerImpl
|
|||||||
*/
|
*/
|
||||||
private static final String[] TITLE_COLOR = {"red", "yellow", "orange"};
|
private static final String[] TITLE_COLOR = {"red", "yellow", "orange"};
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void send(NoticeReceiver receiver, NoticeTemplate noticeTemplate, GroupAlert alert) {
|
public void send(NoticeReceiver receiver, NoticeTemplate noticeTemplate, GroupAlert alert) {
|
||||||
try {
|
try {
|
||||||
String notificationContent = JsonUtil.toJson(renderContent(noticeTemplate, alert));
|
String notificationContent = JsonUtil.toJson(renderContent(noticeTemplate, alert));
|
||||||
// todo priority custom the color
|
// todo priority custom the color
|
||||||
String cardMessage = createLarkMessage(receiver.getUserId(), notificationContent, (byte) 1);
|
String cardMessage = createLarkMessage(receiver.getUserId(), notificationContent, (byte) 1);
|
||||||
String webHookUrl = alerterProperties.getFlyBookWebhookUrl() + receiver.getAccessToken();
|
String webHookUrl = alerterProperties.getFlyBookWebhookUrl() + receiver.getAccessToken();
|
||||||
HttpHeaders headers = new HttpHeaders();
|
HttpHeaders headers = new HttpHeaders();
|
||||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||||
HttpEntity<String> flyEntity = new HttpEntity<>(cardMessage, headers);
|
HttpEntity<String> flyEntity = new HttpEntity<>(cardMessage, headers);
|
||||||
ResponseEntity<CommonRobotNotifyResp> entity = restTemplate.postForEntity(webHookUrl,
|
ResponseEntity<CommonRobotNotifyResp> entity = restTemplate.postForEntity(webHookUrl,
|
||||||
flyEntity, CommonRobotNotifyResp.class);
|
flyEntity, CommonRobotNotifyResp.class);
|
||||||
if (entity.getStatusCode() == HttpStatus.OK) {
|
if (entity.getStatusCode() == HttpStatus.OK) {
|
||||||
assert entity.getBody() != null;
|
assert entity.getBody() != null;
|
||||||
if (entity.getBody().getCode() == null || entity.getBody().getCode() == 0) {
|
if (entity.getBody().getCode() == null || entity.getBody().getCode() == 0) {
|
||||||
@@ -87,115 +87,120 @@ final class FlyBookAlertNotifyHandlerImpl extends AbstractAlertNotifyHandlerImpl
|
|||||||
*/
|
*/
|
||||||
private String createLarkMessage(String userId, String notificationContent, byte priority) {
|
private String createLarkMessage(String userId, String notificationContent, byte priority) {
|
||||||
String larkCardMessage = """
|
String larkCardMessage = """
|
||||||
{
|
{
|
||||||
"msg_type": "interactive",
|
"msg_type": "interactive",
|
||||||
"card": {
|
"card": {
|
||||||
"config": {
|
"schema": "2.0",
|
||||||
"update_multi": true
|
"config": {
|
||||||
},
|
"update_multi": true,
|
||||||
"i18n_elements": {
|
"locales": [
|
||||||
"zh_cn": [
|
"en_us",
|
||||||
{
|
"zh_cn"
|
||||||
"tag": "column_set",
|
],
|
||||||
"flex_mode": "none",
|
"style": {
|
||||||
"horizontal_spacing": "default",
|
"text_size": {
|
||||||
"background_style": "default",
|
"normal_v2": {
|
||||||
"columns": [
|
"default": "normal",
|
||||||
{
|
"pc": "normal",
|
||||||
"tag": "column",
|
"mobile": "heading"
|
||||||
"elements": [
|
}
|
||||||
{
|
}
|
||||||
"tag": "div",
|
}
|
||||||
"text": {
|
},
|
||||||
"tag": "plain_text",
|
"body": {
|
||||||
"content": "",
|
"direction": "vertical",
|
||||||
"text_size": "normal",
|
"padding": "12px 12px 12px 12px",
|
||||||
"text_align": "left",
|
"elements": [
|
||||||
"text_color": "default"
|
{
|
||||||
}
|
"tag": "markdown",
|
||||||
}
|
"content": "%s",
|
||||||
],
|
"i18n_content": {
|
||||||
"width": "weighted",
|
"en_us": ""
|
||||||
"weight": 1
|
},
|
||||||
}
|
"text_align": "left",
|
||||||
]
|
"text_size": "normal_v2",
|
||||||
},
|
"margin": "0px 0px 0px 0px"
|
||||||
{
|
},
|
||||||
"tag": "column_set",
|
{
|
||||||
"flex_mode": "none",
|
"tag": "hr",
|
||||||
"horizontal_spacing": "default",
|
"margin": "0px 0px 0px 0px"
|
||||||
"background_style": "default",
|
},
|
||||||
"columns": [
|
{
|
||||||
{
|
"tag": "column_set",
|
||||||
"tag": "column",
|
"horizontal_align": "left",
|
||||||
"elements": [
|
"columns": [
|
||||||
{
|
{
|
||||||
"tag": "div",
|
"tag": "column",
|
||||||
"text": {
|
"width": "weighted",
|
||||||
"tag": "plain_text",
|
"elements": [
|
||||||
"content": %s,
|
{
|
||||||
"text_size": "normal",
|
"tag": "button",
|
||||||
"text_align": "left",
|
"text": {
|
||||||
"text_color": "default"
|
"tag": "plain_text",
|
||||||
}
|
"content": "登入控制台",
|
||||||
}
|
"i18n_content": {
|
||||||
],
|
"en_us": "Login In"
|
||||||
"width": "weighted",
|
}
|
||||||
"weight": 1
|
},
|
||||||
}
|
"type": "default",
|
||||||
]
|
"width": "default",
|
||||||
},
|
"size": "medium",
|
||||||
%s
|
"behaviors": [
|
||||||
{
|
{
|
||||||
"tag": "action",
|
"type": "open_url",
|
||||||
"actions": [
|
"default_url": "%s",
|
||||||
{
|
"pc_url": "",
|
||||||
"tag": "button",
|
"ios_url": "",
|
||||||
"text": {
|
"android_url": ""
|
||||||
"tag": "plain_text",
|
}
|
||||||
"content": "登入控制台"
|
]
|
||||||
},
|
}
|
||||||
"type": "default",
|
],
|
||||||
"complex_interaction": true,
|
"direction": "horizontal",
|
||||||
"width": "default",
|
"vertical_spacing": "8px",
|
||||||
"size": "medium",
|
"horizontal_align": "left",
|
||||||
"multi_url": {
|
"vertical_align": "top",
|
||||||
"url": "%s"
|
"weight": 1
|
||||||
}
|
}
|
||||||
}
|
],
|
||||||
]
|
"margin": "0px 0px 0px 0px"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"i18n_header": {
|
"header": {
|
||||||
"zh_cn": {
|
"title": {
|
||||||
"title": {
|
"tag": "plain_text",
|
||||||
"tag": "plain_text",
|
"content": "HertzBeat 告警",
|
||||||
"content": "HertzBeat 告警"
|
"i18n_content": {
|
||||||
},
|
"en_us": "HertzBeat Alarm"
|
||||||
"template": "%s"
|
}
|
||||||
}
|
},
|
||||||
}
|
"subtitle": {
|
||||||
|
"tag": "plain_text",
|
||||||
|
"content": ""
|
||||||
|
},
|
||||||
|
"template": "%s",
|
||||||
|
"padding": "12px 12px 12px 12px"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
""";
|
||||||
""";
|
|
||||||
|
|
||||||
String atUserElement = "";
|
String atUserElement = "";
|
||||||
if (StringUtils.isNotBlank(userId)) {
|
if (StringUtils.isNotBlank(userId)) {
|
||||||
String atUserId = Arrays.stream(userId.split(","))
|
atUserElement = "\\n" + Arrays.stream(userId.split(","))
|
||||||
.map(id -> "<at id=" + id + "></at>")
|
.map(id -> "<at id=" + id + "></at>")
|
||||||
.collect(Collectors.joining(" "));
|
.collect(Collectors.joining(" "));
|
||||||
atUserElement = String.format("""
|
|
||||||
{
|
|
||||||
"tag": "div",
|
|
||||||
"text": {
|
|
||||||
"content": "%s",
|
|
||||||
"tag": "lark_md"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
""", atUserId);
|
|
||||||
}
|
}
|
||||||
return String.format(larkCardMessage, notificationContent, atUserElement, alerterProperties.getConsoleUrl(), TITLE_COLOR[priority]);
|
|
||||||
|
if (notificationContent.startsWith("\"") && notificationContent.endsWith("\"")) {
|
||||||
|
notificationContent = StringUtils.removeStart(notificationContent, "\"");
|
||||||
|
notificationContent = StringUtils.removeEnd(notificationContent, "\"");
|
||||||
|
}
|
||||||
|
|
||||||
|
return String.format(larkCardMessage,
|
||||||
|
notificationContent.replace("\"", "\\\"") + atUserElement,
|
||||||
|
alerterProperties.getConsoleUrl(), TITLE_COLOR[priority]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
+12
-1
@@ -31,6 +31,8 @@ import org.springframework.http.MediaType;
|
|||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Send alarm information through Server
|
* Send alarm information through Server
|
||||||
*/
|
*/
|
||||||
@@ -54,7 +56,16 @@ public class ServerChanAlertNotifyHandlerImpl extends AbstractAlertNotifyHandler
|
|||||||
HttpHeaders headers = new HttpHeaders();
|
HttpHeaders headers = new HttpHeaders();
|
||||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||||
HttpEntity<ServerChanAlertNotifyHandlerImpl.ServerChanWebHookDto> httpEntity = new HttpEntity<>(serverChanWebHookDto, headers);
|
HttpEntity<ServerChanAlertNotifyHandlerImpl.ServerChanWebHookDto> httpEntity = new HttpEntity<>(serverChanWebHookDto, headers);
|
||||||
String webHookUrl = String.format(alerterProperties.getServerChanWebhookUrl(), receiver.getServerChanToken());
|
String sanitizedToken = receiver.getServerChanToken().replaceAll("[^a-zA-Z0-9_-]", "");
|
||||||
|
String webHookUrl = String.format(alerterProperties.getServerChanWebhookUrl(), sanitizedToken);
|
||||||
|
|
||||||
|
// Validate the constructed URL against a whitelist
|
||||||
|
List<String> allowedBaseUrls = List.of("https://api.serverchan.com", "https://serverchan.example.com", "https://sctapi.ftqq.com");
|
||||||
|
boolean isValidUrl = allowedBaseUrls.stream().anyMatch(webHookUrl::startsWith);
|
||||||
|
if (!isValidUrl) {
|
||||||
|
throw new AlertNoticeException("Invalid webhook URL: " + webHookUrl);
|
||||||
|
}
|
||||||
|
|
||||||
ResponseEntity<CommonRobotNotifyResp> responseEntity = restTemplate.postForEntity(webHookUrl,
|
ResponseEntity<CommonRobotNotifyResp> responseEntity = restTemplate.postForEntity(webHookUrl,
|
||||||
httpEntity, CommonRobotNotifyResp.class);
|
httpEntity, CommonRobotNotifyResp.class);
|
||||||
if (responseEntity.getStatusCode() == HttpStatus.OK) {
|
if (responseEntity.getStatusCode() == HttpStatus.OK) {
|
||||||
|
|||||||
+24
-1
@@ -17,6 +17,7 @@
|
|||||||
|
|
||||||
package org.apache.hertzbeat.alert.notice.impl;
|
package org.apache.hertzbeat.alert.notice.impl;
|
||||||
|
|
||||||
|
import java.net.URI;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import lombok.Builder;
|
import lombok.Builder;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
@@ -52,7 +53,12 @@ final class SlackAlertNotifyHandlerImpl extends AbstractAlertNotifyHandlerImpl {
|
|||||||
HttpHeaders headers = new HttpHeaders();
|
HttpHeaders headers = new HttpHeaders();
|
||||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||||
HttpEntity<SlackNotifyDTO> slackNotifyEntity = new HttpEntity<>(slackNotify, headers);
|
HttpEntity<SlackNotifyDTO> slackNotifyEntity = new HttpEntity<>(slackNotify, headers);
|
||||||
var entity = restTemplate.postForEntity(receiver.getSlackWebHookUrl(), slackNotifyEntity, String.class);
|
String slackWebHookUrl = receiver.getSlackWebHookUrl();
|
||||||
|
if (!isValidSlackWebHookUrl(slackWebHookUrl)) {
|
||||||
|
log.warn("Invalid Slack Webhook URL: {}", slackWebHookUrl);
|
||||||
|
throw new AlertNoticeException("Invalid Slack Webhook URL");
|
||||||
|
}
|
||||||
|
var entity = restTemplate.postForEntity(slackWebHookUrl, slackNotifyEntity, String.class);
|
||||||
if (entity.getStatusCode() == HttpStatus.OK && entity.getBody() != null) {
|
if (entity.getStatusCode() == HttpStatus.OK && entity.getBody() != null) {
|
||||||
var body = entity.getBody();
|
var body = entity.getBody();
|
||||||
if (Objects.equals(SUCCESS, body)) {
|
if (Objects.equals(SUCCESS, body)) {
|
||||||
@@ -81,4 +87,21 @@ final class SlackAlertNotifyHandlerImpl extends AbstractAlertNotifyHandlerImpl {
|
|||||||
private String text;
|
private String text;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate if the Slack Webhook URL belongs to an allowed domain.
|
||||||
|
*
|
||||||
|
* @param url the Slack Webhook URL to validate
|
||||||
|
* @return true if the URL is valid, false otherwise
|
||||||
|
*/
|
||||||
|
private boolean isValidSlackWebHookUrl(String url) {
|
||||||
|
try {
|
||||||
|
URI uri = new URI(url);
|
||||||
|
String host = uri.getHost();
|
||||||
|
return "hooks.slack.com".equals(host);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("Error validating Slack Webhook URL: {}", url, e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+15
-3
@@ -42,9 +42,14 @@ import org.springframework.stereotype.Component;
|
|||||||
final class TelegramBotAlertNotifyHandlerImpl extends AbstractAlertNotifyHandlerImpl {
|
final class TelegramBotAlertNotifyHandlerImpl extends AbstractAlertNotifyHandlerImpl {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void send(NoticeReceiver receiver, NoticeTemplate noticeTemplate, GroupAlert alert) throws AlertNoticeException {
|
public void send(NoticeReceiver receiver, NoticeTemplate noticeTemplate, GroupAlert alert)
|
||||||
|
throws AlertNoticeException {
|
||||||
try {
|
try {
|
||||||
String url = String.format(alerterProperties.getTelegramWebhookUrl(), receiver.getTgBotToken());
|
String token = receiver.getTgBotToken();
|
||||||
|
if (!isValidTelegramToken(token)) {
|
||||||
|
throw new AlertNoticeException("Invalid Telegram Bot Token");
|
||||||
|
}
|
||||||
|
String url = String.format(alerterProperties.getTelegramWebhookUrl(), token);
|
||||||
TelegramBotNotifyDTO notifyBody = TelegramBotNotifyDTO.builder()
|
TelegramBotNotifyDTO notifyBody = TelegramBotNotifyDTO.builder()
|
||||||
.chatId(receiver.getTgUserId())
|
.chatId(receiver.getTgUserId())
|
||||||
.text(renderContent(noticeTemplate, alert))
|
.text(renderContent(noticeTemplate, alert))
|
||||||
@@ -54,7 +59,8 @@ final class TelegramBotAlertNotifyHandlerImpl extends AbstractAlertNotifyHandler
|
|||||||
HttpHeaders headers = new HttpHeaders();
|
HttpHeaders headers = new HttpHeaders();
|
||||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||||
HttpEntity<TelegramBotNotifyDTO> telegramEntity = new HttpEntity<>(notifyBody, headers);
|
HttpEntity<TelegramBotNotifyDTO> telegramEntity = new HttpEntity<>(notifyBody, headers);
|
||||||
ResponseEntity<TelegramBotNotifyResponse> entity = restTemplate.postForEntity(url, telegramEntity, TelegramBotNotifyResponse.class);
|
ResponseEntity<TelegramBotNotifyResponse> entity = restTemplate.postForEntity(url, telegramEntity,
|
||||||
|
TelegramBotNotifyResponse.class);
|
||||||
if (entity.getStatusCode() == HttpStatus.OK && entity.getBody() != null) {
|
if (entity.getStatusCode() == HttpStatus.OK && entity.getBody() != null) {
|
||||||
TelegramBotNotifyResponse body = entity.getBody();
|
TelegramBotNotifyResponse body = entity.getBody();
|
||||||
if (body.ok) {
|
if (body.ok) {
|
||||||
@@ -99,4 +105,10 @@ final class TelegramBotAlertNotifyHandlerImpl extends AbstractAlertNotifyHandler
|
|||||||
private String description;
|
private String description;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean isValidTelegramToken(String token) {
|
||||||
|
// Adjusted pattern to match real Telegram Bot tokens like
|
||||||
|
// 110201543:AAHdqTcvCH1vGWJxfSeofSAs0K5PALDsaw
|
||||||
|
String tokenPattern = "^[0-9]+:[a-zA-Z0-9_-]+$";
|
||||||
|
return token != null && token.matches(tokenPattern);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+17
-1
@@ -57,7 +57,12 @@ final class WeComRobotAlertNotifyHandlerImpl extends AbstractAlertNotifyHandlerI
|
|||||||
HttpHeaders headers = new HttpHeaders();
|
HttpHeaders headers = new HttpHeaders();
|
||||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||||
HttpEntity<WeWorkWebHookDto> httpEntity = new HttpEntity<>(weWorkWebHookDTO, headers);
|
HttpEntity<WeWorkWebHookDto> httpEntity = new HttpEntity<>(weWorkWebHookDTO, headers);
|
||||||
String webHookUrl = alerterProperties.getWeWorkWebhookUrl() + receiver.getWechatId();
|
String wechatId = receiver.getWechatId();
|
||||||
|
if (!isValidWechatId(wechatId)) {
|
||||||
|
log.warn("Invalid WeChat ID: {}", wechatId);
|
||||||
|
throw new AlertNoticeException("Invalid WeChat ID provided.");
|
||||||
|
}
|
||||||
|
String webHookUrl = alerterProperties.getWeWorkWebhookUrl() + wechatId;
|
||||||
ResponseEntity<CommonRobotNotifyResp> entity = restTemplate.postForEntity(webHookUrl, httpEntity, CommonRobotNotifyResp.class);
|
ResponseEntity<CommonRobotNotifyResp> entity = restTemplate.postForEntity(webHookUrl, httpEntity, CommonRobotNotifyResp.class);
|
||||||
if (entity.getStatusCode() == HttpStatus.OK) {
|
if (entity.getStatusCode() == HttpStatus.OK) {
|
||||||
assert entity.getBody() != null;
|
assert entity.getBody() != null;
|
||||||
@@ -170,4 +175,15 @@ final class WeComRobotAlertNotifyHandlerImpl extends AbstractAlertNotifyHandlerI
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate the WeChat ID to ensure it meets the expected format.
|
||||||
|
*
|
||||||
|
* @param wechatId the WeChat ID to validate
|
||||||
|
* @return true if valid, false otherwise
|
||||||
|
*/
|
||||||
|
private boolean isValidWechatId(String wechatId) {
|
||||||
|
// Example validation: ensure the ID is alphanumeric and non-empty
|
||||||
|
return StringUtils.isNotBlank(wechatId) && wechatId.matches("^[a-zA-Z0-9_-]+$");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+16
-3
@@ -18,6 +18,7 @@
|
|||||||
package org.apache.hertzbeat.alert.notice.impl;
|
package org.apache.hertzbeat.alert.notice.impl;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.apache.hertzbeat.alert.notice.AlertNoticeException;
|
import org.apache.hertzbeat.alert.notice.AlertNoticeException;
|
||||||
import org.apache.hertzbeat.common.entity.alerter.GroupAlert;
|
import org.apache.hertzbeat.common.entity.alerter.GroupAlert;
|
||||||
import org.apache.hertzbeat.common.entity.alerter.NoticeReceiver;
|
import org.apache.hertzbeat.common.entity.alerter.NoticeReceiver;
|
||||||
@@ -39,18 +40,30 @@ final class WebHookAlertNotifyHandlerImpl extends AbstractAlertNotifyHandlerImpl
|
|||||||
@Override
|
@Override
|
||||||
public void send(NoticeReceiver receiver, NoticeTemplate noticeTemplate, GroupAlert alert) {
|
public void send(NoticeReceiver receiver, NoticeTemplate noticeTemplate, GroupAlert alert) {
|
||||||
try {
|
try {
|
||||||
|
String hookUrl = receiver.getHookUrl();
|
||||||
|
|
||||||
|
// Basic URL validation
|
||||||
|
if (StringUtils.isBlank(hookUrl)) {
|
||||||
|
throw new AlertNoticeException("Webhook URL is null or empty");
|
||||||
|
}
|
||||||
|
|
||||||
HttpHeaders headers = new HttpHeaders();
|
HttpHeaders headers = new HttpHeaders();
|
||||||
|
if ("Basic".equalsIgnoreCase(receiver.getHookAuthType())) {
|
||||||
|
headers.setBasicAuth(receiver.getHookAuthToken());
|
||||||
|
} else if ("Bearer".equalsIgnoreCase(receiver.getHookAuthType())) {
|
||||||
|
headers.setBearerAuth(receiver.getHookAuthToken());
|
||||||
|
}
|
||||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||||
// alert.setContent(escapeJsonStr(alert.getContent()));
|
// alert.setContent(escapeJsonStr(alert.getContent()));
|
||||||
String webhookJson = renderContent(noticeTemplate, alert);
|
String webhookJson = renderContent(noticeTemplate, alert);
|
||||||
webhookJson = webhookJson.replace(",\n }", "\n }");
|
webhookJson = webhookJson.replace(",\n }", "\n }");
|
||||||
|
|
||||||
HttpEntity<String> alertHttpEntity = new HttpEntity<>(webhookJson, headers);
|
HttpEntity<String> alertHttpEntity = new HttpEntity<>(webhookJson, headers);
|
||||||
ResponseEntity<String> entity = restTemplate.postForEntity(receiver.getHookUrl(), alertHttpEntity, String.class);
|
ResponseEntity<String> entity = restTemplate.postForEntity(hookUrl, alertHttpEntity, String.class);
|
||||||
if (entity.getStatusCode().value() < HttpStatus.BAD_REQUEST.value()) {
|
if (entity.getStatusCode().value() < HttpStatus.BAD_REQUEST.value()) {
|
||||||
log.debug("Send WebHook: {} Success", receiver.getHookUrl());
|
log.debug("Send WebHook: {} Success", hookUrl);
|
||||||
} else {
|
} else {
|
||||||
log.warn("Send WebHook: {} Failed: {}", receiver.getHookUrl(), entity.getBody());
|
log.warn("Send WebHook: {} Failed: {}", hookUrl, entity.getBody());
|
||||||
throw new AlertNoticeException("Http StatusCode " + entity.getStatusCode());
|
throw new AlertNoticeException("Http StatusCode " + entity.getStatusCode());
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
|||||||
+18
@@ -18,6 +18,8 @@
|
|||||||
package org.apache.hertzbeat.alert.reduce;
|
package org.apache.hertzbeat.alert.reduce;
|
||||||
|
|
||||||
import com.google.common.util.concurrent.ThreadFactoryBuilder;
|
import com.google.common.util.concurrent.ThreadFactoryBuilder;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.concurrent.LinkedBlockingQueue;
|
import java.util.concurrent.LinkedBlockingQueue;
|
||||||
import java.util.concurrent.ThreadFactory;
|
import java.util.concurrent.ThreadFactory;
|
||||||
@@ -66,6 +68,22 @@ public class AlarmCommonReduce {
|
|||||||
public void reduceAndSendAlarm(SingleAlert alert) {
|
public void reduceAndSendAlarm(SingleAlert alert) {
|
||||||
workerExecutor.execute(reduceAlarmTask(alert));
|
workerExecutor.execute(reduceAlarmTask(alert));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void reduceAndSendAlarmGroup(Map<String, String> groupLabels, List<SingleAlert> alerts) {
|
||||||
|
workerExecutor.execute(() -> {
|
||||||
|
try {
|
||||||
|
// Generate alert fingerprint
|
||||||
|
for (SingleAlert alert : alerts) {
|
||||||
|
String fingerprint = generateAlertFingerprint(alert.getLabels());
|
||||||
|
alert.setFingerprint(fingerprint);
|
||||||
|
}
|
||||||
|
// Process the group alert
|
||||||
|
alarmGroupReduce.processGroupAlert(groupLabels, alerts);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Reduce alarm group failed: {}", e.getMessage());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
Runnable reduceAlarmTask(SingleAlert alert) {
|
Runnable reduceAlarmTask(SingleAlert alert) {
|
||||||
return () -> {
|
return () -> {
|
||||||
|
|||||||
+14
-1
@@ -159,7 +159,20 @@ public class AlarmGroupReduce {
|
|||||||
sendSingleAlert(alert);
|
sendSingleAlert(alert);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void processGroupAlert(Map<String, String> groupLabels, List<SingleAlert> alertList) {
|
||||||
|
GroupAlert groupAlert = GroupAlert.builder()
|
||||||
|
.groupKey(generateGroupKey(groupLabels))
|
||||||
|
.groupLabels(groupLabels)
|
||||||
|
.commonLabels(extractCommonLabels(alertList))
|
||||||
|
.commonAnnotations(extractCommonAnnotations(alertList))
|
||||||
|
.alerts(alertList)
|
||||||
|
.status(CommonConstants.ALERT_STATUS_FIRING)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
alarmInhibitReduce.inhibitAlarm(groupAlert);
|
||||||
|
}
|
||||||
|
|
||||||
private boolean hasRequiredLabels(Map<String, String> labels, List<String> requiredLabels) {
|
private boolean hasRequiredLabels(Map<String, String> labels, List<String> requiredLabels) {
|
||||||
return requiredLabels.stream().allMatch(labels::containsKey);
|
return requiredLabels.stream().allMatch(labels::containsKey);
|
||||||
}
|
}
|
||||||
|
|||||||
+29
-6
@@ -18,11 +18,15 @@
|
|||||||
package org.apache.hertzbeat.alert.service;
|
package org.apache.hertzbeat.alert.service;
|
||||||
|
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
import java.util.List;
|
|
||||||
import java.util.Set;
|
|
||||||
import org.apache.hertzbeat.common.entity.alerter.AlertDefine;
|
import org.apache.hertzbeat.common.entity.alerter.AlertDefine;
|
||||||
|
import org.apache.hertzbeat.common.support.exception.AlertExpressionException;
|
||||||
import org.springframework.data.domain.Page;
|
import org.springframework.data.domain.Page;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
import org.apache.hertzbeat.common.constants.CommonConstants;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Alarm define manager service
|
* Alarm define manager service
|
||||||
@@ -103,9 +107,28 @@ public interface AlertDefineService {
|
|||||||
void importConfig(MultipartFile file) throws Exception;
|
void importConfig(MultipartFile file) throws Exception;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the real-time alarm definition list
|
* Get the real-time metrics alarm definition list
|
||||||
* @return Real-time alarm definition list
|
* @return Real-time metrics alarm definition list
|
||||||
*/
|
*/
|
||||||
List<AlertDefine> getRealTimeAlertDefines();
|
List<AlertDefine> getMetricsRealTimeAlertDefines();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the real-time log alarm definition list
|
||||||
|
* @return Real-time log alarm definition list
|
||||||
|
*/
|
||||||
|
List<AlertDefine> getLogRealTimeAlertDefines();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the alarm definition list by type
|
||||||
|
* @param type Alarm definition type, must be one of the constants defined in {@link CommonConstants}
|
||||||
|
* @return List of enabled alarm definitions matching the specified type, empty list if none found
|
||||||
|
*/
|
||||||
|
List<AlertDefine> getAlertDefinesByType(String type);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get define preview
|
||||||
|
* @return Data queried based on expressions
|
||||||
|
* @throws AlertExpressionException expression error
|
||||||
|
*/
|
||||||
|
List<Map<String, Object>> getDefinePreview(String datasource, String type, String expr);
|
||||||
}
|
}
|
||||||
|
|||||||
+8
@@ -32,4 +32,12 @@ public interface DataSourceService {
|
|||||||
* @return result
|
* @return result
|
||||||
*/
|
*/
|
||||||
List<Map<String, Object>> calculate(String datasource, String expr);
|
List<Map<String, Object>> calculate(String datasource, String expr);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* query result set from db
|
||||||
|
* @param datasource sql or promql
|
||||||
|
* @param expr query expr
|
||||||
|
* @return result
|
||||||
|
*/
|
||||||
|
List<Map<String, Object>> query(String datasource, String expr);
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -19,7 +19,7 @@ package org.apache.hertzbeat.alert.service;
|
|||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.hertzbeat.alert.config.SmsConfig;
|
import org.apache.hertzbeat.common.entity.dto.sms.SmsConfig;
|
||||||
import org.apache.hertzbeat.alert.service.impl.SmsLocalSmsClientImpl;
|
import org.apache.hertzbeat.alert.service.impl.SmsLocalSmsClientImpl;
|
||||||
import org.apache.hertzbeat.alert.service.impl.AwsSmsClientImpl;
|
import org.apache.hertzbeat.alert.service.impl.AwsSmsClientImpl;
|
||||||
import org.apache.hertzbeat.alert.service.impl.TencentSmsClientImpl;
|
import org.apache.hertzbeat.alert.service.impl.TencentSmsClientImpl;
|
||||||
@@ -151,4 +151,4 @@ public class SmsClientFactory {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+6
@@ -28,6 +28,9 @@ import org.apache.hertzbeat.alert.dto.ExportAlertDefineDTO;
|
|||||||
import org.apache.hertzbeat.alert.service.AlertDefineImExportService;
|
import org.apache.hertzbeat.alert.service.AlertDefineImExportService;
|
||||||
import org.apache.hertzbeat.alert.service.AlertDefineService;
|
import org.apache.hertzbeat.alert.service.AlertDefineService;
|
||||||
import org.apache.hertzbeat.common.entity.alerter.AlertDefine;
|
import org.apache.hertzbeat.common.entity.alerter.AlertDefine;
|
||||||
|
import org.apache.hertzbeat.common.util.LogUtil;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.beans.BeanUtils;
|
import org.springframework.beans.BeanUtils;
|
||||||
import org.springframework.context.annotation.Lazy;
|
import org.springframework.context.annotation.Lazy;
|
||||||
import org.springframework.util.CollectionUtils;
|
import org.springframework.util.CollectionUtils;
|
||||||
@@ -41,12 +44,15 @@ public abstract class AlertDefineAbstractImExportServiceImpl implements AlertDef
|
|||||||
@Lazy
|
@Lazy
|
||||||
private AlertDefineService alertDefineService;
|
private AlertDefineService alertDefineService;
|
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(AlertDefineAbstractImExportServiceImpl.class);
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void importConfig(InputStream is) {
|
public void importConfig(InputStream is) {
|
||||||
var formList = parseImport(is)
|
var formList = parseImport(is)
|
||||||
.stream()
|
.stream()
|
||||||
.map(this::convert)
|
.map(this::convert)
|
||||||
.toList();
|
.toList();
|
||||||
|
LogUtil.info(logger, "Importing alert defines from {0}", formList);
|
||||||
if (!CollectionUtils.isEmpty(formList)) {
|
if (!CollectionUtils.isEmpty(formList)) {
|
||||||
formList.forEach(alertDefine -> {
|
formList.forEach(alertDefine -> {
|
||||||
alertDefineService.validate(alertDefine, false);
|
alertDefineService.validate(alertDefine, false);
|
||||||
|
|||||||
+63
-10
@@ -17,7 +17,6 @@
|
|||||||
|
|
||||||
package org.apache.hertzbeat.alert.service.impl;
|
package org.apache.hertzbeat.alert.service.impl;
|
||||||
|
|
||||||
import static org.apache.hertzbeat.common.constants.CommonConstants.ALERT_THRESHOLD_TYPE_REALTIME;
|
|
||||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||||
import com.fasterxml.jackson.core.type.TypeReference;
|
import com.fasterxml.jackson.core.type.TypeReference;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
@@ -25,11 +24,13 @@ import jakarta.persistence.criteria.CriteriaBuilder;
|
|||||||
import jakarta.persistence.criteria.Predicate;
|
import jakarta.persistence.criteria.Predicate;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.hertzbeat.alert.calculate.PeriodicAlertRuleScheduler;
|
import org.apache.hertzbeat.alert.calculate.periodic.PeriodicAlertRuleScheduler;
|
||||||
import org.apache.hertzbeat.alert.dao.AlertDefineDao;
|
import org.apache.hertzbeat.alert.dao.AlertDefineDao;
|
||||||
import org.apache.hertzbeat.alert.service.AlertDefineImExportService;
|
import org.apache.hertzbeat.alert.service.AlertDefineImExportService;
|
||||||
import org.apache.hertzbeat.alert.service.AlertDefineService;
|
import org.apache.hertzbeat.alert.service.AlertDefineService;
|
||||||
|
import org.apache.hertzbeat.alert.service.DataSourceService;
|
||||||
import org.apache.hertzbeat.common.cache.CacheFactory;
|
import org.apache.hertzbeat.common.cache.CacheFactory;
|
||||||
|
import org.apache.hertzbeat.common.constants.CommonConstants;
|
||||||
import org.apache.hertzbeat.common.constants.ExportFileConstants;
|
import org.apache.hertzbeat.common.constants.ExportFileConstants;
|
||||||
import org.apache.hertzbeat.common.constants.SignConstants;
|
import org.apache.hertzbeat.common.constants.SignConstants;
|
||||||
import org.apache.hertzbeat.common.entity.alerter.AlertDefine;
|
import org.apache.hertzbeat.common.entity.alerter.AlertDefine;
|
||||||
@@ -72,18 +73,22 @@ public class AlertDefineServiceImpl implements AlertDefineService {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private PeriodicAlertRuleScheduler periodicAlertRuleScheduler;
|
private PeriodicAlertRuleScheduler periodicAlertRuleScheduler;
|
||||||
|
|
||||||
|
private final DataSourceService dataSourceService;
|
||||||
|
|
||||||
private final Map<String, AlertDefineImExportService> alertDefineImExportServiceMap = new HashMap<>();
|
private final Map<String, AlertDefineImExportService> alertDefineImExportServiceMap = new HashMap<>();
|
||||||
|
|
||||||
private static final String CONTENT_TYPE = MediaType.APPLICATION_OCTET_STREAM_VALUE + SignConstants.SINGLE_MARK + "charset=" + StandardCharsets.UTF_8;
|
private static final String CONTENT_TYPE = MediaType.APPLICATION_OCTET_STREAM_VALUE + SignConstants.SINGLE_MARK + "charset=" + StandardCharsets.UTF_8;
|
||||||
|
|
||||||
public AlertDefineServiceImpl(List<AlertDefineImExportService> alertDefineImExportServiceList) {
|
public AlertDefineServiceImpl(List<AlertDefineImExportService> alertDefineImExportServiceList, DataSourceService dataSourceService) {
|
||||||
alertDefineImExportServiceList.forEach(it -> alertDefineImExportServiceMap.put(it.type(), it));
|
alertDefineImExportServiceList.forEach(it -> alertDefineImExportServiceMap.put(it.type(), it));
|
||||||
|
this.dataSourceService = dataSourceService;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void validate(AlertDefine alertDefine, boolean isModify) throws IllegalArgumentException {
|
public void validate(AlertDefine alertDefine, boolean isModify) throws IllegalArgumentException {
|
||||||
if (StringUtils.hasText(alertDefine.getExpr())) {
|
if (StringUtils.hasText(alertDefine.getExpr())) {
|
||||||
if (ALERT_THRESHOLD_TYPE_REALTIME.equals(alertDefine.getType())) {
|
if (CommonConstants.METRIC_ALERT_THRESHOLD_TYPE_REALTIME.equals(alertDefine.getType())
|
||||||
|
|| CommonConstants.LOG_ALERT_THRESHOLD_TYPE_REALTIME.equals(alertDefine.getType())) {
|
||||||
try {
|
try {
|
||||||
JexlExpressionRunner.compile(alertDefine.getExpr());
|
JexlExpressionRunner.compile(alertDefine.getExpr());
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
@@ -102,14 +107,14 @@ public class AlertDefineServiceImpl implements AlertDefineService {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void addAlertDefine(AlertDefine alertDefine) throws RuntimeException {
|
public void addAlertDefine(AlertDefine alertDefine) throws RuntimeException {
|
||||||
alertDefine = alertDefineDao.save(alertDefine);
|
alertDefine = alertDefineDao.saveAndFlush(alertDefine);
|
||||||
periodicAlertRuleScheduler.updateSchedule(alertDefine);
|
periodicAlertRuleScheduler.updateSchedule(alertDefine);
|
||||||
CacheFactory.clearAlertDefineCache();
|
CacheFactory.clearAlertDefineCache();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void modifyAlertDefine(AlertDefine alertDefine) throws RuntimeException {
|
public void modifyAlertDefine(AlertDefine alertDefine) throws RuntimeException {
|
||||||
alertDefineDao.save(alertDefine);
|
alertDefineDao.saveAndFlush(alertDefine);
|
||||||
periodicAlertRuleScheduler.updateSchedule(alertDefine);
|
periodicAlertRuleScheduler.updateSchedule(alertDefine);
|
||||||
CacheFactory.clearAlertDefineCache();
|
CacheFactory.clearAlertDefineCache();
|
||||||
}
|
}
|
||||||
@@ -210,12 +215,60 @@ public class AlertDefineServiceImpl implements AlertDefineService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<AlertDefine> getRealTimeAlertDefines() {
|
public List<AlertDefine> getMetricsRealTimeAlertDefines() {
|
||||||
List<AlertDefine> alertDefines = CacheFactory.getAlertDefineCache();
|
List<AlertDefine> alertDefines = CacheFactory.getMetricsAlertDefineCache();
|
||||||
if (alertDefines == null) {
|
if (alertDefines == null) {
|
||||||
alertDefines = alertDefineDao.findAlertDefinesByTypeAndEnableTrue(ALERT_THRESHOLD_TYPE_REALTIME);
|
alertDefines = alertDefineDao.findAlertDefinesByTypeAndEnableTrue(CommonConstants.METRIC_ALERT_THRESHOLD_TYPE_REALTIME);
|
||||||
CacheFactory.setAlertDefineCache(alertDefines);
|
CacheFactory.setMetricsAlertDefineCache(alertDefines);
|
||||||
}
|
}
|
||||||
return alertDefines;
|
return alertDefines;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<AlertDefine> getLogRealTimeAlertDefines() {
|
||||||
|
List<AlertDefine> alertDefines = CacheFactory.getLogAlertDefineCache();
|
||||||
|
if (alertDefines == null) {
|
||||||
|
alertDefines = alertDefineDao.findAlertDefinesByTypeAndEnableTrue(CommonConstants.LOG_ALERT_THRESHOLD_TYPE_REALTIME);
|
||||||
|
CacheFactory.setLogAlertDefineCache(alertDefines);
|
||||||
|
}
|
||||||
|
return alertDefines;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Map<String, Object>> getDefinePreview(String datasource, String type, String expr) {
|
||||||
|
if (!StringUtils.hasText(expr) || !StringUtils.hasText(datasource) || !StringUtils.hasText(type)) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
switch (type) {
|
||||||
|
case CommonConstants.METRIC_ALERT_THRESHOLD_TYPE_PERIODIC:
|
||||||
|
return dataSourceService.calculate(datasource, expr);
|
||||||
|
case CommonConstants.LOG_ALERT_THRESHOLD_TYPE_PERIODIC:
|
||||||
|
// todo support alert expr preview
|
||||||
|
return dataSourceService.query(datasource, expr);
|
||||||
|
default:
|
||||||
|
log.error("Get define preview unsupported type: {}", type);
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<AlertDefine> getAlertDefinesByType(String type) {
|
||||||
|
if (!StringUtils.hasText(type)) {
|
||||||
|
throw new IllegalArgumentException("Alert definition type cannot be null or empty");
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case CommonConstants.METRIC_ALERT_THRESHOLD_TYPE_REALTIME:
|
||||||
|
case CommonConstants.METRIC_ALERT_THRESHOLD_TYPE_PERIODIC:
|
||||||
|
case CommonConstants.LOG_ALERT_THRESHOLD_TYPE_REALTIME:
|
||||||
|
case CommonConstants.LOG_ALERT_THRESHOLD_TYPE_PERIODIC:
|
||||||
|
// Valid type, proceed with query
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new IllegalArgumentException("Unsupported alert definition type: " + type);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Query enabled alert definitions by type
|
||||||
|
return alertDefineDao.findAlertDefinesByTypeAndEnableTrue(type);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+202
@@ -0,0 +1,202 @@
|
|||||||
|
/*
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||||
|
* contributor license agreements. See the NOTICE file distributed with
|
||||||
|
* this work for additional information regarding copyright ownership.
|
||||||
|
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||||
|
* (the "License"); you may not use this file except in compliance with
|
||||||
|
* the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.apache.hertzbeat.alert.service.impl;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.type.TypeReference;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.commons.lang3.BooleanUtils;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.apache.hertzbeat.alert.dto.AlibabaCloudSlsExternAlert;
|
||||||
|
import org.apache.hertzbeat.alert.reduce.AlarmCommonReduce;
|
||||||
|
import org.apache.hertzbeat.alert.service.ExternAlertService;
|
||||||
|
import org.apache.hertzbeat.common.constants.CommonConstants;
|
||||||
|
import org.apache.hertzbeat.common.entity.alerter.SingleAlert;
|
||||||
|
import org.apache.hertzbeat.common.util.IpDomainUtil;
|
||||||
|
import org.apache.hertzbeat.common.util.JsonUtil;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.text.MessageFormat;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.ZoneId;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Alibaba Cloud 'Simple Log Service(SLS)' external alarm service impl
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
public class AlibabaCloudSlsExternAlertService implements ExternAlertService {
|
||||||
|
|
||||||
|
private final AlarmCommonReduce alarmCommonReduce;
|
||||||
|
|
||||||
|
private static final AlibabaCloudSlsConverter CONVERTER = new AlibabaCloudSlsConverter();
|
||||||
|
|
||||||
|
public AlibabaCloudSlsExternAlertService(AlarmCommonReduce alarmCommonReduce) {
|
||||||
|
this.alarmCommonReduce = alarmCommonReduce;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addExternAlert(String content) {
|
||||||
|
List<AlibabaCloudSlsExternAlert> externAlerts = new ArrayList<>();
|
||||||
|
if (BooleanUtils.isTrue(JsonUtil.isArray(content))) {
|
||||||
|
TypeReference<List<AlibabaCloudSlsExternAlert>> typeReference = new TypeReference<>() {};
|
||||||
|
externAlerts = JsonUtil.fromJson(content, typeReference);
|
||||||
|
} else {
|
||||||
|
AlibabaCloudSlsExternAlert externAlert = JsonUtil.fromJson(content, AlibabaCloudSlsExternAlert.class);
|
||||||
|
if (null != externAlert) {
|
||||||
|
externAlerts.add(externAlert);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (null == externAlerts || externAlerts.isEmpty()) {
|
||||||
|
log.warn("Failure to parse external alert content. content: {}", content);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (AlibabaCloudSlsExternAlert externAlert : externAlerts) {
|
||||||
|
SingleAlert singleAlert = CONVERTER.convert(externAlert);
|
||||||
|
alarmCommonReduce.reduceAndSendAlarm(singleAlert);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String supportSource() {
|
||||||
|
return "alibabacloud-sls";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
public static class AlibabaCloudSlsConverter {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* convert
|
||||||
|
*
|
||||||
|
* @param externAlert alert content entity
|
||||||
|
* @return Single alert
|
||||||
|
*/
|
||||||
|
public SingleAlert convert(AlibabaCloudSlsExternAlert externAlert) {
|
||||||
|
return SingleAlert.builder()
|
||||||
|
.triggerTimes(1)
|
||||||
|
.status(externAlert.getStatus())
|
||||||
|
.startAt(Instant.ofEpochSecond(externAlert.getFireTime()).toEpochMilli())
|
||||||
|
.activeAt(Instant.ofEpochSecond(externAlert.getAlertTime()).toEpochMilli())
|
||||||
|
.endAt(convertResolveTime(externAlert.getStatus(), externAlert.getResolveTime()))
|
||||||
|
.labels(buildLabels(externAlert))
|
||||||
|
.annotations(buildAnnotations(externAlert))
|
||||||
|
.content(formatContent(externAlert))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* todo i18n
|
||||||
|
*
|
||||||
|
* @param externAlert alert content entity
|
||||||
|
* @return content
|
||||||
|
*/
|
||||||
|
private String formatContent(AlibabaCloudSlsExternAlert externAlert) {
|
||||||
|
// convet severity
|
||||||
|
Optional<AlibabaCloudSlsExternAlert.Severity> severity = AlibabaCloudSlsExternAlert.Severity.convert(externAlert.getSeverity());
|
||||||
|
// If the alarm state is resolved, the value is the specific recovery time.
|
||||||
|
Long resolveTimeMilli = convertResolveTime(externAlert.getStatus(), externAlert.getResolveTime());
|
||||||
|
|
||||||
|
return MessageFormat.format(
|
||||||
|
"AlibabaCloud-sls alert , {0} - [{1}], level: [{2}], desc: {3}, fire_time:{4}, resolve_time:{5}",
|
||||||
|
externAlert.getAnnotation("title"),
|
||||||
|
externAlert.getStatus(),
|
||||||
|
severity.isPresent() ? severity.get().getAlias() : "N/A",
|
||||||
|
externAlert.getAnnotation("desc"),
|
||||||
|
timeSecondToDate(Instant.ofEpochSecond(externAlert.getFireTime()).toEpochMilli()),
|
||||||
|
null != resolveTimeMilli ? timeSecondToDate(resolveTimeMilli) : "N/A"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a timestamp (milliseconds) to a formatted date-time string.
|
||||||
|
*
|
||||||
|
* @param timestampMillis timestamp in milliseconds
|
||||||
|
* @return formatted date-time string in the pattern: yyyy-MM-dd HH:mm:ss
|
||||||
|
*/
|
||||||
|
private String timeSecondToDate(long timestampMillis) {
|
||||||
|
LocalDateTime dateTime = LocalDateTime.ofInstant(
|
||||||
|
Instant.ofEpochMilli(timestampMillis),
|
||||||
|
ZoneId.systemDefault()
|
||||||
|
);
|
||||||
|
return dateTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build basic annotations and fill annotations for alibaba cloud sls.
|
||||||
|
*
|
||||||
|
* @param externAlert alert content entity
|
||||||
|
* @return annotations
|
||||||
|
*/
|
||||||
|
private Map<String, String> buildAnnotations(AlibabaCloudSlsExternAlert externAlert) {
|
||||||
|
Map<String, String> annotations = new HashMap<>(8);
|
||||||
|
Optional<AlibabaCloudSlsExternAlert.Severity> severity = AlibabaCloudSlsExternAlert.Severity.convert(externAlert.getSeverity());
|
||||||
|
severity.ifPresent(value -> annotations.put("severity", value.getAlias()));
|
||||||
|
// Notification templates for sls need to be configured.
|
||||||
|
if (StringUtils.isNotBlank(externAlert.getSigninUrl()) && IpDomainUtil.isHasSchema(externAlert.getSigninUrl())) {
|
||||||
|
annotations.put("signinUrl", "<a target=\"_blank\" href=\"" + externAlert.getSigninUrl() + "\">View Details</a>");
|
||||||
|
}
|
||||||
|
// Filling the annotations with the alibaba cloud sls.
|
||||||
|
if (null != externAlert.getAnnotations() && !externAlert.getAnnotations().isEmpty()) {
|
||||||
|
annotations.putAll(externAlert.getAnnotations());
|
||||||
|
}
|
||||||
|
|
||||||
|
return annotations;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build basic labels and fill labels for alibaba cloud sls.
|
||||||
|
*
|
||||||
|
* @param externAlert alert content entity
|
||||||
|
* @return labels
|
||||||
|
*/
|
||||||
|
private Map<String, String> buildLabels(AlibabaCloudSlsExternAlert externAlert) {
|
||||||
|
Map<String, String> labels = new HashMap<>(8);
|
||||||
|
labels.put("__source__", "alibabacloud-sls");
|
||||||
|
labels.put("alertname", externAlert.getAlertName());
|
||||||
|
labels.put("region", externAlert.getRegion());
|
||||||
|
// The project name is globally unique.
|
||||||
|
labels.put("project", externAlert.getProject());
|
||||||
|
// Filling the labels with the alibaba cloud sls.
|
||||||
|
if (null != externAlert.getLabels() && !externAlert.getLabels().isEmpty()){
|
||||||
|
labels.putAll(externAlert.getLabels());
|
||||||
|
}
|
||||||
|
return labels;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If the alarm status is firing, the value is 0.
|
||||||
|
* If the alarm state is resolved, the value is the specific recovery time.
|
||||||
|
*
|
||||||
|
* @param status alert status
|
||||||
|
* @param resolveTimeSecond recovery time
|
||||||
|
* @return milliseconds
|
||||||
|
*/
|
||||||
|
private Long convertResolveTime(String status, int resolveTimeSecond) {
|
||||||
|
return CommonConstants.ALERT_STATUS_RESOLVED.equals(status) ? Instant.ofEpochSecond(resolveTimeSecond).toEpochMilli() : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+8
-3
@@ -19,7 +19,7 @@ package org.apache.hertzbeat.alert.service.impl;
|
|||||||
|
|
||||||
import com.fasterxml.jackson.databind.JsonNode;
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.hertzbeat.alert.config.AlibabaSmsProperties;
|
import org.apache.hertzbeat.common.entity.dto.sms.AlibabaSmsProperties;
|
||||||
import org.apache.hertzbeat.alert.service.SmsClient;
|
import org.apache.hertzbeat.alert.service.SmsClient;
|
||||||
import org.apache.hertzbeat.alert.util.CryptoUtils;
|
import org.apache.hertzbeat.alert.util.CryptoUtils;
|
||||||
import org.apache.hertzbeat.common.entity.alerter.GroupAlert;
|
import org.apache.hertzbeat.common.entity.alerter.GroupAlert;
|
||||||
@@ -27,11 +27,14 @@ import org.apache.hertzbeat.common.entity.alerter.NoticeReceiver;
|
|||||||
import org.apache.hertzbeat.common.entity.alerter.NoticeTemplate;
|
import org.apache.hertzbeat.common.entity.alerter.NoticeTemplate;
|
||||||
import org.apache.hertzbeat.common.support.exception.SendMessageException;
|
import org.apache.hertzbeat.common.support.exception.SendMessageException;
|
||||||
import org.apache.hertzbeat.common.util.JsonUtil;
|
import org.apache.hertzbeat.common.util.JsonUtil;
|
||||||
|
import org.apache.hertzbeat.common.util.LogUtil;
|
||||||
import org.apache.http.client.methods.CloseableHttpResponse;
|
import org.apache.http.client.methods.CloseableHttpResponse;
|
||||||
import org.apache.http.client.methods.HttpPost;
|
import org.apache.http.client.methods.HttpPost;
|
||||||
import org.apache.http.impl.client.CloseableHttpClient;
|
import org.apache.http.impl.client.CloseableHttpClient;
|
||||||
import org.apache.http.impl.client.HttpClients;
|
import org.apache.http.impl.client.HttpClients;
|
||||||
import org.apache.http.util.EntityUtils;
|
import org.apache.http.util.EntityUtils;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.text.SimpleDateFormat;
|
import java.text.SimpleDateFormat;
|
||||||
@@ -62,6 +65,7 @@ public class AlibabaSmsClientImpl implements SmsClient {
|
|||||||
private final String accessKeySecret;
|
private final String accessKeySecret;
|
||||||
private final String signName;
|
private final String signName;
|
||||||
private final String templateCode;
|
private final String templateCode;
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(AlibabaSmsClientImpl.class);
|
||||||
|
|
||||||
public AlibabaSmsClientImpl(AlibabaSmsProperties config) {
|
public AlibabaSmsClientImpl(AlibabaSmsProperties config) {
|
||||||
if (config != null) {
|
if (config != null) {
|
||||||
@@ -173,7 +177,7 @@ public class AlibabaSmsClientImpl implements SmsClient {
|
|||||||
log.info("Successfully sent SMS to phone: {}", phoneNumber);
|
log.info("Successfully sent SMS to phone: {}", phoneNumber);
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.warn("Failed to send SMS: {}", e.getMessage());
|
LogUtil.warn(logger, "Failed to send SMS: {0}", e.getMessage());
|
||||||
throw new SendMessageException(e.getMessage());
|
throw new SendMessageException(e.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -192,6 +196,7 @@ public class AlibabaSmsClientImpl implements SmsClient {
|
|||||||
// Step 4: Build authorization header
|
// Step 4: Build authorization header
|
||||||
return ALGORITHM + " Credential=" + accessKeyId + ",SignedHeaders=host;x-acs-action;x-acs-content-sha256;x-acs-date;" + "x-acs-signature-nonce;x-acs-version,Signature=" + signature;
|
return ALGORITHM + " Credential=" + accessKeyId + ",SignedHeaders=host;x-acs-action;x-acs-content-sha256;x-acs-date;" + "x-acs-signature-nonce;x-acs-version,Signature=" + signature;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
LogUtil.warn(logger, "Failed to calculate authorization {0}", e.getMessage());
|
||||||
throw new RuntimeException("Failed to calculate authorization", e);
|
throw new RuntimeException("Failed to calculate authorization", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -231,4 +236,4 @@ public class AlibabaSmsClientImpl implements SmsClient {
|
|||||||
public boolean checkConfig() {
|
public boolean checkConfig() {
|
||||||
return !(accessKeyId.isBlank() || accessKeySecret.isBlank() || signName.isBlank() || templateCode.isBlank());
|
return !(accessKeyId.isBlank() || accessKeySecret.isBlank() || signName.isBlank() || templateCode.isBlank());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -20,7 +20,7 @@ package org.apache.hertzbeat.alert.service.impl;
|
|||||||
import com.fasterxml.jackson.databind.JsonNode;
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.hertzbeat.alert.config.AwsSmsProperties;
|
import org.apache.hertzbeat.common.entity.dto.sms.AwsSmsProperties;
|
||||||
import org.apache.hertzbeat.alert.service.SmsClient;
|
import org.apache.hertzbeat.alert.service.SmsClient;
|
||||||
import org.apache.hertzbeat.alert.util.CryptoUtils;
|
import org.apache.hertzbeat.alert.util.CryptoUtils;
|
||||||
import org.apache.hertzbeat.common.entity.alerter.GroupAlert;
|
import org.apache.hertzbeat.common.entity.alerter.GroupAlert;
|
||||||
|
|||||||
+40
-8
@@ -24,11 +24,14 @@ import lombok.Setter;
|
|||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.antlr.v4.runtime.CharStreams;
|
import org.antlr.v4.runtime.CharStreams;
|
||||||
import org.antlr.v4.runtime.CommonTokenStream;
|
import org.antlr.v4.runtime.CommonTokenStream;
|
||||||
|
import org.antlr.v4.runtime.Token;
|
||||||
import org.antlr.v4.runtime.tree.ParseTree;
|
import org.antlr.v4.runtime.tree.ParseTree;
|
||||||
import org.apache.hertzbeat.alert.expr.AlertExpressionEvalVisitor;
|
import org.apache.hertzbeat.alert.expr.AlertExpressionEvalVisitor;
|
||||||
import org.apache.hertzbeat.alert.expr.AlertExpressionLexer;
|
import org.apache.hertzbeat.alert.expr.AlertExpressionLexer;
|
||||||
import org.apache.hertzbeat.alert.expr.AlertExpressionParser;
|
import org.apache.hertzbeat.alert.expr.AlertExpressionParser;
|
||||||
import org.apache.hertzbeat.alert.service.DataSourceService;
|
import org.apache.hertzbeat.alert.service.DataSourceService;
|
||||||
|
import org.apache.hertzbeat.common.support.exception.AlertExpressionException;
|
||||||
|
import org.apache.hertzbeat.common.util.ResourceBundleUtil;
|
||||||
import org.apache.hertzbeat.warehouse.db.QueryExecutor;
|
import org.apache.hertzbeat.warehouse.db.QueryExecutor;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
@@ -36,6 +39,7 @@ import org.springframework.util.StringUtils;
|
|||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.ResourceBundle;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -45,6 +49,8 @@ import java.util.concurrent.TimeUnit;
|
|||||||
@Slf4j
|
@Slf4j
|
||||||
public class DataSourceServiceImpl implements DataSourceService {
|
public class DataSourceServiceImpl implements DataSourceService {
|
||||||
|
|
||||||
|
protected ResourceBundle bundle = ResourceBundleUtil.getBundle("alerter");
|
||||||
|
|
||||||
@Setter
|
@Setter
|
||||||
@Autowired(required = false)
|
@Autowired(required = false)
|
||||||
private List<QueryExecutor> executors;
|
private List<QueryExecutor> executors;
|
||||||
@@ -69,7 +75,7 @@ public class DataSourceServiceImpl implements DataSourceService {
|
|||||||
throw new IllegalArgumentException("Empty expression");
|
throw new IllegalArgumentException("Empty expression");
|
||||||
}
|
}
|
||||||
if (executors == null || executors.isEmpty()) {
|
if (executors == null || executors.isEmpty()) {
|
||||||
throw new IllegalArgumentException("No query executor found");
|
throw new IllegalArgumentException(bundle.getString("alerter.datasource.executor.not.found"));
|
||||||
}
|
}
|
||||||
QueryExecutor executor = executors.stream().filter(e -> e.support(datasource)).findFirst().orElse(null);
|
QueryExecutor executor = executors.stream().filter(e -> e.support(datasource)).findFirst().orElse(null);
|
||||||
|
|
||||||
@@ -80,6 +86,32 @@ public class DataSourceServiceImpl implements DataSourceService {
|
|||||||
expr = expr.replaceAll("\\s+", " ");
|
expr = expr.replaceAll("\\s+", " ");
|
||||||
try {
|
try {
|
||||||
return evaluate(expr, executor);
|
return evaluate(expr, executor);
|
||||||
|
} catch (AlertExpressionException ae) {
|
||||||
|
log.error("Calculate query parse error, datasource: {}, expr: {}, msg: {}", datasource, expr, ae.getMessage(), ae);
|
||||||
|
throw ae;
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Error executing query on datasource {}: {}", datasource, e.getMessage());
|
||||||
|
throw new RuntimeException("Query execution failed", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Map<String, Object>> query(String datasource, String expr) {
|
||||||
|
if (!StringUtils.hasText(expr)) {
|
||||||
|
throw new IllegalArgumentException("Empty expression");
|
||||||
|
}
|
||||||
|
if (executors == null || executors.isEmpty()) {
|
||||||
|
throw new IllegalArgumentException(bundle.getString("alerter.datasource.executor.not.found"));
|
||||||
|
}
|
||||||
|
QueryExecutor executor = executors.stream().filter(e -> e.support(datasource)).findFirst().orElse(null);
|
||||||
|
|
||||||
|
if (executor == null) {
|
||||||
|
throw new IllegalArgumentException("Unsupported datasource: " + datasource);
|
||||||
|
}
|
||||||
|
// replace all white space
|
||||||
|
expr = expr.replaceAll("\\s+", " ");
|
||||||
|
try {
|
||||||
|
return executor.execute(expr);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("Error executing query on datasource {}: {}", datasource, e.getMessage());
|
log.error("Error executing query on datasource {}: {}", datasource, e.getMessage());
|
||||||
throw new RuntimeException("Query execution failed", e);
|
throw new RuntimeException("Query execution failed", e);
|
||||||
@@ -87,14 +119,14 @@ public class DataSourceServiceImpl implements DataSourceService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private List<Map<String, Object>> evaluate(String expr, QueryExecutor executor) {
|
private List<Map<String, Object>> evaluate(String expr, QueryExecutor executor) {
|
||||||
ParseTree tree = expressionCache.get(expr, e -> {
|
CommonTokenStream tokens = tokenStreamCache.get(expr, this::createTokenStream);
|
||||||
CommonTokenStream tokens = tokenStreamCache.get(e, this::createTokenStream);
|
AlertExpressionParser parser = new AlertExpressionParser(tokens);
|
||||||
AlertExpressionParser parser = new AlertExpressionParser(tokens);
|
ParseTree tree = expressionCache.get(expr, e -> parser.expr());
|
||||||
return parser.expr();
|
if (null != tokens && tokens.LA(1) != Token.EOF) {
|
||||||
});
|
throw new AlertExpressionException(bundle.getString("alerter.calculate.parse.error"));
|
||||||
AlertExpressionEvalVisitor visitor = new AlertExpressionEvalVisitor(executor);
|
}
|
||||||
|
AlertExpressionEvalVisitor visitor = new AlertExpressionEvalVisitor(executor, tokens);
|
||||||
return visitor.visit(tree);
|
return visitor.visit(tree);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private CommonTokenStream createTokenStream(String expr) {
|
private CommonTokenStream createTokenStream(String expr) {
|
||||||
|
|||||||
+343
@@ -0,0 +1,343 @@
|
|||||||
|
/*
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||||
|
* contributor license agreements. See the NOTICE file distributed with
|
||||||
|
* this work for additional information regarding copyright ownership.
|
||||||
|
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||||
|
* (the "License"); you may not use this file except in compliance with
|
||||||
|
* the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.apache.hertzbeat.alert.service.impl;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.apache.hertzbeat.alert.dto.HuaweiCloudExternAlert;
|
||||||
|
import org.apache.hertzbeat.alert.reduce.AlarmCommonReduce;
|
||||||
|
import org.apache.hertzbeat.alert.service.ExternAlertService;
|
||||||
|
import org.apache.hertzbeat.alert.util.DateUtil;
|
||||||
|
import org.apache.hertzbeat.common.constants.CommonConstants;
|
||||||
|
import org.apache.hertzbeat.common.entity.alerter.SingleAlert;
|
||||||
|
import org.apache.hertzbeat.common.support.exception.IgnoreException;
|
||||||
|
import org.apache.hertzbeat.common.util.JsonUtil;
|
||||||
|
import org.apache.http.client.methods.CloseableHttpResponse;
|
||||||
|
import org.apache.http.client.methods.HttpGet;
|
||||||
|
import org.apache.http.impl.client.CloseableHttpClient;
|
||||||
|
import org.apache.http.impl.client.HttpClients;
|
||||||
|
import org.apache.http.util.EntityUtils;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.security.Signature;
|
||||||
|
import java.security.cert.CertificateFactory;
|
||||||
|
import java.security.cert.X509Certificate;
|
||||||
|
import java.text.MessageFormat;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.Base64;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import static org.apache.hertzbeat.alert.dto.HuaweiCloudExternAlert.AlertType.NOTIFICATION;
|
||||||
|
import static org.apache.hertzbeat.alert.dto.HuaweiCloudExternAlert.AlertType.SUBSCRIPTION;
|
||||||
|
import static org.apache.hertzbeat.alert.dto.HuaweiCloudExternAlert.AlertType.UNSUBSCRIBE;
|
||||||
|
import static org.apache.hertzbeat.alert.dto.HuaweiCloudExternAlert.FIELD_MESSAGE;
|
||||||
|
import static org.apache.hertzbeat.alert.dto.HuaweiCloudExternAlert.FIELD_MESSAGE_ID;
|
||||||
|
import static org.apache.hertzbeat.alert.dto.HuaweiCloudExternAlert.FIELD_SUBJECT;
|
||||||
|
import static org.apache.hertzbeat.alert.dto.HuaweiCloudExternAlert.FIELD_SUBSCRIBE_URL;
|
||||||
|
import static org.apache.hertzbeat.alert.dto.HuaweiCloudExternAlert.FIELD_TIMESTAMP;
|
||||||
|
import static org.apache.hertzbeat.alert.dto.HuaweiCloudExternAlert.FIELD_TOPIC_URN;
|
||||||
|
import static org.apache.hertzbeat.alert.dto.HuaweiCloudExternAlert.FIELD_TYPE;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Huawei cloud external alarm service impl
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
public class HuaweiCloudExternAlertService implements ExternAlertService {
|
||||||
|
|
||||||
|
private static final String CERTIFICATE_TYPE = "X.509";
|
||||||
|
|
||||||
|
private static final String CHARSET_UTF8 = StandardCharsets.UTF_8.name();
|
||||||
|
|
||||||
|
private final AlarmCommonReduce alarmCommonReduce;
|
||||||
|
|
||||||
|
public HuaweiCloudExternAlertService(AlarmCommonReduce alarmCommonReduce) {
|
||||||
|
this.alarmCommonReduce = alarmCommonReduce;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addExternAlert(String content) {
|
||||||
|
HuaweiCloudExternAlert externAlert = JsonUtil.fromJson(content, HuaweiCloudExternAlert.class);
|
||||||
|
if (externAlert == null || StringUtils.isBlank(externAlert.getMessage())) {
|
||||||
|
log.warn("Failure to parse external alert content. content: {}", content);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!isMessageValid(externAlert)) {
|
||||||
|
log.warn("Huawei cloud alert verify failed. content: {}", content);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
process(externAlert);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process according to different types
|
||||||
|
*
|
||||||
|
* @param externAlert alert content entity
|
||||||
|
*/
|
||||||
|
private void process(HuaweiCloudExternAlert externAlert) {
|
||||||
|
if (NOTIFICATION.getType().equals(externAlert.getType())) {
|
||||||
|
Optional.ofNullable(buildSendAlert(externAlert)).ifPresent(alarmCommonReduce::reduceAndSendAlarm);
|
||||||
|
} else if (SUBSCRIPTION.getType().equals(externAlert.getType())) {
|
||||||
|
autoSubscribeForUrl(externAlert.getSubscribeUrl());
|
||||||
|
} else if (UNSUBSCRIBE.getType().equals(externAlert.getType())) {
|
||||||
|
log.warn("Huawei cloud notifies the recipient of the notification to cancel the subscription.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build single alert.
|
||||||
|
*
|
||||||
|
* @param externAlert alert content entity
|
||||||
|
* @return single alert
|
||||||
|
*/
|
||||||
|
private SingleAlert buildSendAlert(HuaweiCloudExternAlert externAlert) {
|
||||||
|
HuaweiCloudExternAlert.AlertMessage message = JsonUtil.fromJson(externAlert.getMessage(), HuaweiCloudExternAlert.AlertMessage.class);
|
||||||
|
if (null == message || null == message.getData()) {
|
||||||
|
log.warn("Failure to parse external alert message. message: {}", externAlert.getMessage());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
// Note: Empty and false are both recovery notifications.
|
||||||
|
// Note: There are no recovery notifications for event types
|
||||||
|
boolean isAlarm = null != message.getData().getAlarm() && message.getData().getAlarm();
|
||||||
|
Long alarmTime = DateUtil.getZonedTimeStampFromFormat(message.getData().getAlarmTime(), "yyyy/MM/dd HH:mm:ss 'GMT'XXX");
|
||||||
|
return SingleAlert.builder()
|
||||||
|
.triggerTimes(1)
|
||||||
|
.status(isAlarm ? CommonConstants.ALERT_STATUS_FIRING : CommonConstants.ALERT_STATUS_RESOLVED)
|
||||||
|
.startAt(alarmTime)
|
||||||
|
.activeAt(Instant.now().toEpochMilli())
|
||||||
|
.endAt(isAlarm ? null : alarmTime)
|
||||||
|
.labels(buildLabels(message.getData()))
|
||||||
|
.annotations(buildAnnotations(message.getData()))
|
||||||
|
.content(formatContent(externAlert.getSubject(), message.getData()))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build basic annotations and fill annotations for huawei cloud.
|
||||||
|
*
|
||||||
|
* @param alertData alert content entity
|
||||||
|
* @return annotations
|
||||||
|
*/
|
||||||
|
private Map<String, String> buildAnnotations(HuaweiCloudExternAlert.AlertData alertData) {
|
||||||
|
Map<String, String> annotations = new HashMap<>(8);
|
||||||
|
if (null != alertData) {
|
||||||
|
putIfNotBlank(annotations, "region", alertData.getRegion());
|
||||||
|
putIfNotBlank(annotations, "dimensionName", alertData.getDimensionName());
|
||||||
|
putIfNotBlank(annotations, "resourceName", alertData.getResourceName());
|
||||||
|
putIfNotBlank(annotations, "alarmRecordId", alertData.getAlarmRecordId());
|
||||||
|
}
|
||||||
|
return annotations;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build basic labels and fill labels for huawei cloud.
|
||||||
|
*
|
||||||
|
* @param alertData alert content entity
|
||||||
|
* @return labels
|
||||||
|
*/
|
||||||
|
private Map<String, String> buildLabels(HuaweiCloudExternAlert.AlertData alertData) {
|
||||||
|
Map<String, String> labels = new HashMap<>(8);
|
||||||
|
labels.put("__source__", "huaweicloud-ces");
|
||||||
|
if (null != alertData) {
|
||||||
|
putIfNotBlank(labels, "namespace", alertData.getNamespace());
|
||||||
|
putIfNotBlank(labels, "metricName", alertData.getMetricName());
|
||||||
|
putIfNotBlank(labels, "resourceId", alertData.getResourceId());
|
||||||
|
putIfNotBlank(labels, "level", alertData.getAlarmLevel());
|
||||||
|
}
|
||||||
|
return labels;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* todo i18n
|
||||||
|
*
|
||||||
|
* @param subject alert subject
|
||||||
|
* @param alertData alert content entity
|
||||||
|
* @return content
|
||||||
|
*/
|
||||||
|
private String formatContent(String subject, HuaweiCloudExternAlert.AlertData alertData) {
|
||||||
|
if (null == alertData) {
|
||||||
|
return subject;
|
||||||
|
}
|
||||||
|
return MessageFormat.format(
|
||||||
|
"{0} threshold:{1}{2}, current:{3}",
|
||||||
|
subject,
|
||||||
|
alertData.getComparisonOperator(),
|
||||||
|
alertData.getValue(),
|
||||||
|
alertData.getCurrentData()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Automatic subscription url.
|
||||||
|
*
|
||||||
|
* @param subscribeUrl subscribeUrl
|
||||||
|
*/
|
||||||
|
public void autoSubscribeForUrl(String subscribeUrl) {
|
||||||
|
if (StringUtils.isBlank(subscribeUrl)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try (CloseableHttpClient httpClient = HttpClients.createDefault()) {
|
||||||
|
HttpGet httpGet = new HttpGet(subscribeUrl);
|
||||||
|
try (CloseableHttpResponse response = httpClient.execute(httpGet)) {
|
||||||
|
int statusCode = response.getStatusLine().getStatusCode();
|
||||||
|
String responseBody = EntityUtils.toString(response.getEntity());
|
||||||
|
|
||||||
|
if (statusCode != 200) {
|
||||||
|
log.error("Subscribe url request failed with status code: " + statusCode + ", response: " + responseBody);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
JsonNode jsonResponse = JsonUtil.fromJson(responseBody);
|
||||||
|
if (jsonResponse == null) {
|
||||||
|
throw new IgnoreException("Subscribe url failed with status code: " + statusCode + ", response: " + responseBody);
|
||||||
|
}
|
||||||
|
JsonNode surnNode = jsonResponse.get("subscription_urn");
|
||||||
|
if (surnNode == null || StringUtils.isBlank(surnNode.asText())) {
|
||||||
|
throw new IgnoreException("Subscribe url failed with status code: " + statusCode + ", response: " + responseBody);
|
||||||
|
}
|
||||||
|
log.info("Successfully subscribed to Huawei Cloud(SMN) url.");
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Failed to subscribe url request: {}", e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifying the signature of huawei cloud alert message.
|
||||||
|
*
|
||||||
|
* @param externAlert alert content entity
|
||||||
|
* @return verification result
|
||||||
|
* @throws SecurityException thrown when validation fails
|
||||||
|
*/
|
||||||
|
private boolean isMessageValid(HuaweiCloudExternAlert externAlert) {
|
||||||
|
try {
|
||||||
|
String signMessage = buildSignMessage(externAlert);
|
||||||
|
if (StringUtils.isBlank(signMessage)) {
|
||||||
|
throw new SecurityException("Verify sign message is null");
|
||||||
|
}
|
||||||
|
X509Certificate cert = getCertificate(externAlert.getSigningCertUrl());
|
||||||
|
return verifySignature(signMessage, cert, externAlert.getSignature());
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Failed to verify message signature: ", e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build sign message.
|
||||||
|
*
|
||||||
|
* @param externAlert alert content entity
|
||||||
|
* @return sign message
|
||||||
|
*/
|
||||||
|
private String buildSignMessage(HuaweiCloudExternAlert externAlert) {
|
||||||
|
if (NOTIFICATION.getType().equals(externAlert.getType())) {
|
||||||
|
return buildNotificationMessage(externAlert);
|
||||||
|
} else if (SUBSCRIPTION.getType().equals(externAlert.getType()) || UNSUBSCRIBE.getType().equals(externAlert.getType())){
|
||||||
|
return buildSubscriptionMessage(externAlert);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Building sign message of 'Notification' type
|
||||||
|
*
|
||||||
|
* @param externAlert alert content entity
|
||||||
|
* @return sign message
|
||||||
|
*/
|
||||||
|
private String buildNotificationMessage(HuaweiCloudExternAlert externAlert) {
|
||||||
|
StringBuilder message = new StringBuilder();
|
||||||
|
appendField(message, FIELD_MESSAGE, externAlert.getMessage());
|
||||||
|
appendField(message, FIELD_MESSAGE_ID, externAlert.getMessageId());
|
||||||
|
if (StringUtils.isNotBlank(externAlert.getSubject())) {
|
||||||
|
appendField(message, FIELD_SUBJECT, externAlert.getSubject());
|
||||||
|
}
|
||||||
|
appendField(message, FIELD_TIMESTAMP, externAlert.getTimestamp());
|
||||||
|
appendField(message, FIELD_TOPIC_URN, externAlert.getTopicUrn());
|
||||||
|
appendField(message, FIELD_TYPE, externAlert.getType());
|
||||||
|
return message.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Building sign message of 'SubscriptionConfirmation' or 'UnsubscribeConfirmation' type
|
||||||
|
*
|
||||||
|
* @param externAlert alert content entity
|
||||||
|
* @return sign message
|
||||||
|
*/
|
||||||
|
private String buildSubscriptionMessage(HuaweiCloudExternAlert externAlert) {
|
||||||
|
StringBuilder message = new StringBuilder();
|
||||||
|
appendField(message, FIELD_MESSAGE, externAlert.getMessage());
|
||||||
|
appendField(message, FIELD_MESSAGE_ID, externAlert.getMessageId());
|
||||||
|
appendField(message, FIELD_SUBSCRIBE_URL, externAlert.getSubscribeUrl());
|
||||||
|
appendField(message, FIELD_TIMESTAMP, externAlert.getTimestamp());
|
||||||
|
appendField(message, FIELD_TOPIC_URN, externAlert.getTopicUrn());
|
||||||
|
appendField(message, FIELD_TYPE, externAlert.getType());
|
||||||
|
return message.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtain certificate
|
||||||
|
*
|
||||||
|
* @param signCertUrl sign cert url
|
||||||
|
* @return X509 certificate
|
||||||
|
* @throws Exception Thrown when certificate acquisition fails
|
||||||
|
*/
|
||||||
|
private X509Certificate getCertificate(String signCertUrl) throws Exception {
|
||||||
|
URL url = new URL(signCertUrl);
|
||||||
|
try (InputStream in = url.openStream()) {
|
||||||
|
CertificateFactory cf = CertificateFactory.getInstance(CERTIFICATE_TYPE);
|
||||||
|
return (X509Certificate) cf.generateCertificate(in);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify signature
|
||||||
|
*
|
||||||
|
* @param message sign message
|
||||||
|
* @param cert cert
|
||||||
|
* @param signature signature
|
||||||
|
* @return verification result
|
||||||
|
* @throws Exception thrown when an error occurs in the validation process
|
||||||
|
*/
|
||||||
|
private boolean verifySignature(String message, X509Certificate cert, String signature) throws Exception {
|
||||||
|
Signature sig = Signature.getInstance(cert.getSigAlgName());
|
||||||
|
sig.initVerify(cert.getPublicKey());
|
||||||
|
sig.update(message.getBytes(CHARSET_UTF8));
|
||||||
|
return sig.verify(Base64.getDecoder().decode(signature));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void putIfNotBlank(Map<String, String> map, String key, String value) {
|
||||||
|
if (StringUtils.isNotBlank(value)){
|
||||||
|
map.put(key, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void appendField(StringBuilder builder, String fieldName, String value) {
|
||||||
|
builder.append(fieldName).append("\n").append(value).append("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String supportSource() {
|
||||||
|
return "huaweicloud-ces";
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+1
-1
@@ -21,7 +21,7 @@ import com.fasterxml.jackson.databind.JsonNode;
|
|||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.Setter;
|
import lombok.Setter;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.hertzbeat.alert.config.SmslocalSmsProperties;
|
import org.apache.hertzbeat.common.entity.dto.sms.SmslocalSmsProperties;
|
||||||
import org.apache.hertzbeat.alert.service.SmsClient;
|
import org.apache.hertzbeat.alert.service.SmsClient;
|
||||||
import org.apache.hertzbeat.common.constants.SmsConstants;
|
import org.apache.hertzbeat.common.constants.SmsConstants;
|
||||||
import org.apache.hertzbeat.common.entity.alerter.GroupAlert;
|
import org.apache.hertzbeat.common.entity.alerter.GroupAlert;
|
||||||
|
|||||||
+1
-1
@@ -19,7 +19,7 @@ package org.apache.hertzbeat.alert.service.impl;
|
|||||||
|
|
||||||
import jakarta.xml.bind.DatatypeConverter;
|
import jakarta.xml.bind.DatatypeConverter;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.hertzbeat.alert.config.TencentSmsProperties;
|
import org.apache.hertzbeat.common.entity.dto.sms.TencentSmsProperties;
|
||||||
import org.apache.hertzbeat.alert.service.SmsClient;
|
import org.apache.hertzbeat.alert.service.SmsClient;
|
||||||
import org.apache.hertzbeat.alert.util.CryptoUtils;
|
import org.apache.hertzbeat.alert.util.CryptoUtils;
|
||||||
import org.apache.hertzbeat.common.entity.alerter.GroupAlert;
|
import org.apache.hertzbeat.common.entity.alerter.GroupAlert;
|
||||||
|
|||||||
+2
-2
@@ -19,7 +19,7 @@ package org.apache.hertzbeat.alert.service.impl;
|
|||||||
|
|
||||||
import com.fasterxml.jackson.databind.JsonNode;
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.hertzbeat.alert.config.TwilioSmsProperties;
|
import org.apache.hertzbeat.common.entity.dto.sms.TwilioSmsProperties;
|
||||||
import org.apache.hertzbeat.alert.service.SmsClient;
|
import org.apache.hertzbeat.alert.service.SmsClient;
|
||||||
import org.apache.hertzbeat.common.entity.alerter.GroupAlert;
|
import org.apache.hertzbeat.common.entity.alerter.GroupAlert;
|
||||||
import org.apache.hertzbeat.common.entity.alerter.NoticeReceiver;
|
import org.apache.hertzbeat.common.entity.alerter.NoticeReceiver;
|
||||||
@@ -168,4 +168,4 @@ public class TwilioSmsClientImpl implements SmsClient {
|
|||||||
public boolean checkConfig() {
|
public boolean checkConfig() {
|
||||||
return !(accountSid.isBlank() || authToken.isBlank() || twilioPhoneNumber.isBlank());
|
return !(accountSid.isBlank() || authToken.isBlank() || twilioPhoneNumber.isBlank());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -18,7 +18,7 @@
|
|||||||
package org.apache.hertzbeat.alert.service.impl;
|
package org.apache.hertzbeat.alert.service.impl;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.hertzbeat.alert.config.UniSmsProperties;
|
import org.apache.hertzbeat.common.entity.dto.sms.UniSmsProperties;
|
||||||
import org.apache.hertzbeat.alert.service.SmsClient;
|
import org.apache.hertzbeat.alert.service.SmsClient;
|
||||||
import org.apache.hertzbeat.common.entity.alerter.GroupAlert;
|
import org.apache.hertzbeat.common.entity.alerter.GroupAlert;
|
||||||
import org.apache.hertzbeat.common.entity.alerter.NoticeReceiver;
|
import org.apache.hertzbeat.common.entity.alerter.NoticeReceiver;
|
||||||
@@ -190,4 +190,4 @@ public class UniSmsClientImpl implements SmsClient {
|
|||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+244
@@ -0,0 +1,244 @@
|
|||||||
|
/*
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||||
|
* contributor license agreements. See the NOTICE file distributed with
|
||||||
|
* this work for additional information regarding copyright ownership.
|
||||||
|
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||||
|
* (the "License"); you may not use this file except in compliance with
|
||||||
|
* the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.apache.hertzbeat.alert.service.impl;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.SneakyThrows;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.commons.collections4.CollectionUtils;
|
||||||
|
import org.apache.hertzbeat.alert.dto.VolcEngineExternEventAlert;
|
||||||
|
import org.apache.hertzbeat.alert.dto.VolcEngineExternMetricAlert;
|
||||||
|
import org.apache.hertzbeat.alert.reduce.AlarmCommonReduce;
|
||||||
|
import org.apache.hertzbeat.alert.service.ExternAlertService;
|
||||||
|
import org.apache.hertzbeat.common.constants.CommonConstants;
|
||||||
|
import org.apache.hertzbeat.common.entity.alerter.SingleAlert;
|
||||||
|
import org.apache.hertzbeat.common.util.JsonUtil;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.time.OffsetDateTime;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Volcengine alarm entity class
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class VolcEngineExternAlertService implements ExternAlertService {
|
||||||
|
|
||||||
|
private final AlarmCommonReduce alarmCommonReduce;
|
||||||
|
private static final Map<String, Integer> severityOrder = Map.of(
|
||||||
|
CommonConstants.ALERT_SEVERITY_CRITICAL, 1,
|
||||||
|
CommonConstants.ALERT_SEVERITY_WARNING, 2,
|
||||||
|
CommonConstants.ALERT_SEVERITY_INFO, 3
|
||||||
|
);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@SneakyThrows
|
||||||
|
public void addExternAlert(String content) {
|
||||||
|
ObjectMapper mapper = new ObjectMapper();
|
||||||
|
JsonNode root = mapper.readTree(content);
|
||||||
|
String type = root.get("Type").asText();
|
||||||
|
if (VolcEngineExternMetricAlert.ALERT_TYPE_EVENT.equals(type)) {
|
||||||
|
VolcEngineExternEventAlert eventAlert = JsonUtil.fromJson(content, VolcEngineExternEventAlert.class);
|
||||||
|
if (eventAlert == null) {
|
||||||
|
log.warn("parse extern event alert content failed! content: {}", content);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
SingleAlert singleAlert = new VolcEngineAlertConverter().convertEventToSingleAlert(eventAlert);
|
||||||
|
alarmCommonReduce.reduceAndSendAlarm(singleAlert);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// deal with metric alert
|
||||||
|
VolcEngineExternMetricAlert report = JsonUtil.fromJson(content, VolcEngineExternMetricAlert.class);
|
||||||
|
if (report == null) {
|
||||||
|
log.warn("parse extern metrics alert content failed! content: {}", content);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (SingleAlert singleAlert : new VolcEngineAlertConverter().convertMetricAlertToSingeAlert(report)) {
|
||||||
|
alarmCommonReduce.reduceAndSendAlarm(singleAlert);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String supportSource() {
|
||||||
|
return "volcengine";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converter: VolcEngine alert to SingleAlert
|
||||||
|
*/
|
||||||
|
public static class VolcEngineAlertConverter {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert VolcEngine metric alert to SingleAlert List
|
||||||
|
*/
|
||||||
|
public List<SingleAlert> convertMetricAlertToSingeAlert(VolcEngineExternMetricAlert alert) {
|
||||||
|
String status = convertStatus(alert);
|
||||||
|
if (Objects.equals(alert.getType(), VolcEngineExternMetricAlert.ALERT_TYPE_METRIC)) {
|
||||||
|
return convertMetricAlert(alert, status, alert.getResources());
|
||||||
|
}
|
||||||
|
if (Objects.equals(alert.getType(), VolcEngineExternMetricAlert.ALERT_TYPE_METRICS_NODATA)) {
|
||||||
|
return convertMetricAlert(alert, status, alert.getNoDataResources());
|
||||||
|
}
|
||||||
|
if (Objects.equals(alert.getType(), VolcEngineExternMetricAlert.ALERT_TYPE_NO_DATA_RECOVERED)) {
|
||||||
|
return convertMetricAlert(alert, status, alert.getNoDataRecoveredResources());
|
||||||
|
}
|
||||||
|
if (Objects.equals(alert.getType(), VolcEngineExternMetricAlert.ALERT_TYPE_METRIC_RECOVERED)) {
|
||||||
|
return convertMetricAlert(alert, status, alert.getRecoveredResources());
|
||||||
|
}
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
|
||||||
|
public SingleAlert convertEventToSingleAlert(VolcEngineExternEventAlert alert) {
|
||||||
|
SingleAlert.SingleAlertBuilder builder = SingleAlert.builder()
|
||||||
|
.status(CommonConstants.ALERT_STATUS_FIRING)
|
||||||
|
.startAt(alert.getHappenedAt() * 1000)
|
||||||
|
.activeAt(alert.getHappenedAt() * 1000)
|
||||||
|
.labels(buildEventLabels(alert))
|
||||||
|
.content(alert.getDescriptionCn())
|
||||||
|
.annotations(new HashMap<>());
|
||||||
|
return builder.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* build labels for volcengine event alert
|
||||||
|
*/
|
||||||
|
private Map<String, String> buildEventLabels(VolcEngineExternEventAlert event) {
|
||||||
|
Map<String, String> labels = new HashMap<>();
|
||||||
|
labels.put("severity", convertEventSeverity(event));
|
||||||
|
labels.put("__source__", "volcengine");
|
||||||
|
labels.put("resource_name", event.getDetails().getVolcResourceName());
|
||||||
|
labels.put("account_id", event.getAccountId());
|
||||||
|
labels.put("region", event.getRegion());
|
||||||
|
labels.put("event_type", event.getEventType());
|
||||||
|
labels.put("source", event.getSource());
|
||||||
|
return labels;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* convert volcengine event alert status to heartbeat alert status
|
||||||
|
* use the most severe level in the rules
|
||||||
|
*
|
||||||
|
* @param alert volcengine event alert
|
||||||
|
* @return status
|
||||||
|
*/
|
||||||
|
private String convertEventSeverity(VolcEngineExternEventAlert alert) {
|
||||||
|
return alert.getRules().stream().map(VolcEngineExternEventAlert.EventRule::getLevel)
|
||||||
|
.min((o1, o2) -> severityOrder.get(convertCommonSeverity(o1)).compareTo(severityOrder.get(convertCommonSeverity(o2))))
|
||||||
|
.orElse(CommonConstants.ALERT_SEVERITY_INFO);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String convertCommonSeverity(String level) {
|
||||||
|
return switch (level) {
|
||||||
|
case "critical" -> CommonConstants.ALERT_SEVERITY_CRITICAL;
|
||||||
|
case "warning" -> CommonConstants.ALERT_SEVERITY_WARNING;
|
||||||
|
default -> CommonConstants.ALERT_SEVERITY_INFO;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* create SingleAlert for each resources in volcengine alert
|
||||||
|
*
|
||||||
|
* @param alert volcengine alert
|
||||||
|
* @param status status
|
||||||
|
* @param resources resources
|
||||||
|
* @return List of SingleAlert
|
||||||
|
*/
|
||||||
|
private List<SingleAlert> convertMetricAlert(VolcEngineExternMetricAlert alert, String status,
|
||||||
|
List<? extends VolcEngineExternMetricAlert.Resource> resources) {
|
||||||
|
if (CollectionUtils.isEmpty(resources)) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
List<SingleAlert> result = new ArrayList<>();
|
||||||
|
for (VolcEngineExternMetricAlert.Resource resource : resources) {
|
||||||
|
SingleAlert.SingleAlertBuilder builder = SingleAlert.builder()
|
||||||
|
.status(status)
|
||||||
|
.startAt(resource.getFirstAlertTime() * 1000)
|
||||||
|
.endAt(resource.getLastAlertTime() * 1000)
|
||||||
|
.labels(buildLabels(alert, resource))
|
||||||
|
.activeAt(convertHappenAt(alert.getHappenedAt()))
|
||||||
|
.content(resource.getName() + alert.getRuleCondition())
|
||||||
|
.annotations(buildAnnotations(resource));
|
||||||
|
result.add(builder.build());
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Long convertHappenAt(String happenAt) {
|
||||||
|
String cleanedStr = happenAt.replace("UTC", "").replace("(", "").replace(")", "");
|
||||||
|
DateTimeFormatter altFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ssXXX");
|
||||||
|
OffsetDateTime odt = OffsetDateTime.parse(cleanedStr, altFormatter);
|
||||||
|
return odt.toEpochSecond() * 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, String> buildLabels(VolcEngineExternMetricAlert alert, VolcEngineExternMetricAlert.Resource resource) {
|
||||||
|
Map<String, String> labels = new HashMap<>();
|
||||||
|
labels.put("__source__", "volcengine");
|
||||||
|
labels.put("resource_name", resource.getName());
|
||||||
|
labels.put("resource_id", resource.getId());
|
||||||
|
labels.put("rule_id", alert.getRuleId());
|
||||||
|
return labels;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, String> buildAnnotations(VolcEngineExternMetricAlert.Resource resource) {
|
||||||
|
Map<String, String> annotations = new HashMap<>();
|
||||||
|
if (resource instanceof VolcEngineExternMetricAlert.NoDataResource noDataResource) {
|
||||||
|
for (VolcEngineExternMetricAlert.Metric noDataMetric : noDataResource.getNoDataMetrics()) {
|
||||||
|
annotations.put(noDataMetric.getName(), "N/A");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (VolcEngineExternMetricAlert.Metric metric : resource.getMetrics()) {
|
||||||
|
annotations.put(metric.getName(), metric.getCurrentValue() + metric.getUnit());
|
||||||
|
}
|
||||||
|
annotations.put("region", resource.getRegion());
|
||||||
|
List<VolcEngineExternMetricAlert.Dimension> dimensions = resource.getDimensions();
|
||||||
|
if (CollectionUtils.isNotEmpty(dimensions)) {
|
||||||
|
for (VolcEngineExternMetricAlert.Dimension dimension : dimensions) {
|
||||||
|
annotations.put(dimension.getNameCn(), dimension.getValue());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return annotations;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* convert volcengine alert status to heartbeat alert status
|
||||||
|
*
|
||||||
|
* @param alert volcengine alert
|
||||||
|
* @return status
|
||||||
|
*/
|
||||||
|
private String convertStatus(VolcEngineExternMetricAlert alert) {
|
||||||
|
String type = alert.getType();
|
||||||
|
if (Objects.equals(type, VolcEngineExternMetricAlert.ALERT_TYPE_METRIC)
|
||||||
|
|| Objects.equals(type, VolcEngineExternMetricAlert.ALERT_TYPE_EVENT)
|
||||||
|
|| Objects.equals(type, VolcEngineExternMetricAlert.ALERT_TYPE_METRICS_NODATA)) {
|
||||||
|
return CommonConstants.ALERT_STATUS_FIRING;
|
||||||
|
}
|
||||||
|
return CommonConstants.ALERT_STATUS_RESOLVED;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+119
-3
@@ -17,6 +17,8 @@
|
|||||||
|
|
||||||
package org.apache.hertzbeat.alert.util;
|
package org.apache.hertzbeat.alert.util;
|
||||||
|
|
||||||
|
import java.lang.reflect.Field;
|
||||||
|
import java.lang.reflect.Method;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
@@ -33,10 +35,11 @@ public final class AlertTemplateUtil {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Match the variable ${key}
|
* Match the variable ${key} or ${key.property.subproperty}
|
||||||
* eg: Alert, the instance: ${instance} metrics: ${metrics} is over flow.
|
* eg: Alert, the instance: ${instance} metrics: ${metrics} is over flow.
|
||||||
|
* eg: Log alert: ${log.attributes.level} - ${log.message}
|
||||||
*/
|
*/
|
||||||
private static final Pattern PATTERN = Pattern.compile("\\$\\{(\\w+)\\}");
|
private static final Pattern PATTERN = Pattern.compile("\\$\\{([\\w.]+)}");
|
||||||
|
|
||||||
public static String render(String template, Map<String, Object> replaceData) {
|
public static String render(String template, Map<String, Object> replaceData) {
|
||||||
if (!StringUtils.hasText(template)) {
|
if (!StringUtils.hasText(template)) {
|
||||||
@@ -50,7 +53,8 @@ public final class AlertTemplateUtil {
|
|||||||
Matcher matcher = PATTERN.matcher(template);
|
Matcher matcher = PATTERN.matcher(template);
|
||||||
StringBuilder builder = new StringBuilder();
|
StringBuilder builder = new StringBuilder();
|
||||||
while (matcher.find()) {
|
while (matcher.find()) {
|
||||||
Object objectValue = replaceData.getOrDefault(matcher.group(1), "NullValue");
|
String propertyPath = matcher.group(1);
|
||||||
|
Object objectValue = getNestedProperty(replaceData, propertyPath);
|
||||||
String value = objectValue != null ? objectValue.toString() : "NullValue";
|
String value = objectValue != null ? objectValue.toString() : "NullValue";
|
||||||
matcher.appendReplacement(builder, Matcher.quoteReplacement(value));
|
matcher.appendReplacement(builder, Matcher.quoteReplacement(value));
|
||||||
}
|
}
|
||||||
@@ -61,4 +65,116 @@ public final class AlertTemplateUtil {
|
|||||||
return template;
|
return template;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get nested property value from object using property path like "log.attributes.demo"
|
||||||
|
* @param replaceData the root data map
|
||||||
|
* @param propertyPath the property path, e.g., "log.attributes.demo"
|
||||||
|
* @return the property value or null if not found
|
||||||
|
*/
|
||||||
|
private static Object getNestedProperty(Map<String, Object> replaceData, String propertyPath) {
|
||||||
|
if (propertyPath == null || propertyPath.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
String[] parts = propertyPath.split("\\.");
|
||||||
|
Object current = replaceData.get(parts[0]);
|
||||||
|
|
||||||
|
if (current == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If only one part, return the direct value
|
||||||
|
if (parts.length == 1) {
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Navigate through the property path
|
||||||
|
for (int i = 1; i < parts.length; i++) {
|
||||||
|
current = getPropertyValue(current, parts[i]);
|
||||||
|
if (current == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get property value from an object using reflection
|
||||||
|
* @param obj the object
|
||||||
|
* @param propertyName the property name
|
||||||
|
* @return the property value or null if not found
|
||||||
|
*/
|
||||||
|
private static Object getPropertyValue(Object obj, String propertyName) {
|
||||||
|
if (obj == null || propertyName == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Handle Map objects
|
||||||
|
if (obj instanceof Map) {
|
||||||
|
return ((Map<?, ?>) obj).get(propertyName);
|
||||||
|
}
|
||||||
|
|
||||||
|
Class<?> clazz = obj.getClass();
|
||||||
|
|
||||||
|
// Try getter method first (getPropertyName or isPropertyName for boolean)
|
||||||
|
String getterName = "get" + capitalize(propertyName);
|
||||||
|
String booleanGetterName = "is" + capitalize(propertyName);
|
||||||
|
|
||||||
|
try {
|
||||||
|
Method getter = clazz.getMethod(getterName);
|
||||||
|
return getter.invoke(obj);
|
||||||
|
} catch (NoSuchMethodException e) {
|
||||||
|
// Try boolean getter
|
||||||
|
try {
|
||||||
|
Method booleanGetter = clazz.getMethod(booleanGetterName);
|
||||||
|
return booleanGetter.invoke(obj);
|
||||||
|
} catch (NoSuchMethodException ignored) {
|
||||||
|
// Continue to field access
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try direct field access
|
||||||
|
try {
|
||||||
|
Field field = clazz.getDeclaredField(propertyName);
|
||||||
|
field.setAccessible(true);
|
||||||
|
return field.get(obj);
|
||||||
|
} catch (NoSuchFieldException ignored) {
|
||||||
|
// Field not found
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try parent classes
|
||||||
|
Class<?> superClass = clazz.getSuperclass();
|
||||||
|
while (superClass != null && !superClass.equals(Object.class)) {
|
||||||
|
try {
|
||||||
|
Field field = superClass.getDeclaredField(propertyName);
|
||||||
|
field.setAccessible(true);
|
||||||
|
return field.get(obj);
|
||||||
|
} catch (NoSuchFieldException ignored) {
|
||||||
|
// Continue with parent class
|
||||||
|
}
|
||||||
|
superClass = superClass.getSuperclass();
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.debug("Failed to access property '{}' on object of type {}: {}",
|
||||||
|
propertyName, obj.getClass().getSimpleName(), e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Capitalize the first letter of a string
|
||||||
|
* @param str the input string
|
||||||
|
* @return the capitalized string
|
||||||
|
*/
|
||||||
|
private static String capitalize(String str) {
|
||||||
|
if (str == null || str.isEmpty()) {
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
return str.substring(0, 1).toUpperCase() + str.substring(1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ import lombok.extern.slf4j.Slf4j;
|
|||||||
|
|
||||||
import javax.crypto.Mac;
|
import javax.crypto.Mac;
|
||||||
import javax.crypto.spec.SecretKeySpec;
|
import javax.crypto.spec.SecretKeySpec;
|
||||||
import javax.xml.bind.DatatypeConverter;
|
|
||||||
import java.nio.charset.Charset;
|
import java.nio.charset.Charset;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.security.MessageDigest;
|
import java.security.MessageDigest;
|
||||||
@@ -64,7 +63,7 @@ public class CryptoUtils {
|
|||||||
try {
|
try {
|
||||||
MessageDigest md = MessageDigest.getInstance("SHA-256");
|
MessageDigest md = MessageDigest.getInstance("SHA-256");
|
||||||
byte[] digest = md.digest(data.getBytes(UTF8));
|
byte[] digest = md.digest(data.getBytes(UTF8));
|
||||||
return DatatypeConverter.printHexBinary(digest).toLowerCase();
|
return bytesToHex(digest).toLowerCase();
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("Failed to calculate SHA256: {}", e.getMessage());
|
log.error("Failed to calculate SHA256: {}", e.getMessage());
|
||||||
throw new RuntimeException("Failed to calculate SHA256", e);
|
throw new RuntimeException("Failed to calculate SHA256", e);
|
||||||
@@ -89,7 +88,7 @@ public class CryptoUtils {
|
|||||||
* @return lowercase hexadecimal string
|
* @return lowercase hexadecimal string
|
||||||
*/
|
*/
|
||||||
public static String hmacSha256Hex(String key, String data) {
|
public static String hmacSha256Hex(String key, String data) {
|
||||||
return hmacSha256Hex(hmac256(key.getBytes(UTF8), data), data);
|
return hmacSha256Hex(key.getBytes(UTF8), data);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -101,6 +100,19 @@ public class CryptoUtils {
|
|||||||
*/
|
*/
|
||||||
public static String hmacSha256Hex(byte[] key, String data) {
|
public static String hmacSha256Hex(byte[] key, String data) {
|
||||||
byte[] hmacResult = hmac256(key, data);
|
byte[] hmacResult = hmac256(key, data);
|
||||||
return DatatypeConverter.printHexBinary(hmacResult).toLowerCase();
|
return bytesToHex(hmacResult).toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert byte array to hexadecimal string
|
||||||
|
* @param bytes byte array to convert
|
||||||
|
* @return hexadecimal string representation
|
||||||
|
*/
|
||||||
|
private static String bytesToHex(byte[] bytes) {
|
||||||
|
StringBuilder result = new StringBuilder();
|
||||||
|
for (byte byteValue : bytes) {
|
||||||
|
result.append(String.format("%02x", byteValue));
|
||||||
|
}
|
||||||
|
return result.toString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user