mirror of
https://github.com/ccfos/nightingale.git
synced 2026-03-03 14:38:55 +00:00
Compare commits
624 Commits
refactor_h
...
motify-mig
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7463b7c61b | ||
|
|
728fa5306b | ||
|
|
57f8bd3612 | ||
|
|
8ab96e2cea | ||
|
|
0a2e23c285 | ||
|
|
5c1d4077e2 | ||
|
|
2a46d9f98e | ||
|
|
771a8d121b | ||
|
|
1a22211a5d | ||
|
|
0a0049c6fb | ||
|
|
1b56ebe62e | ||
|
|
a5e92b95b0 | ||
|
|
8e9d06d43e | ||
|
|
ab289de785 | ||
|
|
8667b7743a | ||
|
|
45b9436f69 | ||
|
|
3d03bcf329 | ||
|
|
1851601889 | ||
|
|
fa9745decf | ||
|
|
6f007deeaa | ||
|
|
8fad705065 | ||
|
|
675076779e | ||
|
|
b9e78eee22 | ||
|
|
2219584abb | ||
|
|
ebe31fd6bc | ||
|
|
95ca69e170 | ||
|
|
ef1b5d8d16 | ||
|
|
5b375cf037 | ||
|
|
108b729cae | ||
|
|
a385972fa9 | ||
|
|
98a0a9d94c | ||
|
|
c79eec648d | ||
|
|
603eadd1f2 | ||
|
|
61a2f552be | ||
|
|
e3453328a7 | ||
|
|
4424a6b89c | ||
|
|
9fdb2f0753 | ||
|
|
3d358e367f | ||
|
|
5264874628 | ||
|
|
e0a3ff248c | ||
|
|
1fecf78ede | ||
|
|
839b45904b | ||
|
|
cd0f43f808 | ||
|
|
8047f3deee | ||
|
|
f209ed5bee | ||
|
|
8c61d8c14d | ||
|
|
f7372b1c3b | ||
|
|
a39ced86aa | ||
|
|
f365b7db2a | ||
|
|
7eaec13b6c | ||
|
|
2e824a165e | ||
|
|
f2909b6029 | ||
|
|
a543a5ad09 | ||
|
|
2ee34bf1f9 | ||
|
|
4623622dd0 | ||
|
|
4f259137e5 | ||
|
|
75f1e8a80b | ||
|
|
3648d8dc45 | ||
|
|
8c90d7ab33 | ||
|
|
c6ac3fb959 | ||
|
|
ce854b3166 | ||
|
|
a2be5230fa | ||
|
|
21276a77b6 | ||
|
|
cffd012ec6 | ||
|
|
a9ebdad1cd | ||
|
|
785c577728 | ||
|
|
0e2a66570e | ||
|
|
76583a6227 | ||
|
|
48e0e1a9f8 | ||
|
|
17bb7fa468 | ||
|
|
fc2638680a | ||
|
|
e01a899ae1 | ||
|
|
07c1ef6bd4 | ||
|
|
bfa7059098 | ||
|
|
096a2d3675 | ||
|
|
2232733922 | ||
|
|
b15f638688 | ||
|
|
4f818e3642 | ||
|
|
638c62da2f | ||
|
|
e1a9c995c2 | ||
|
|
1898675075 | ||
|
|
ce7f0272d8 | ||
|
|
93159f07fd | ||
|
|
7d410baa2d | ||
|
|
20b30c3e2c | ||
|
|
8805bf6598 | ||
|
|
fe6a64dae8 | ||
|
|
2c564a2c58 | ||
|
|
ae3c13224d | ||
|
|
9a4015f13f | ||
|
|
274ca09551 | ||
|
|
3d9b4fc14e | ||
|
|
07436a5e0d | ||
|
|
f7b2f1acb9 | ||
|
|
4f4287030a | ||
|
|
e25e712c48 | ||
|
|
66951d7e77 | ||
|
|
f5ff27cd18 | ||
|
|
9e3f6e6285 | ||
|
|
48e3df2cb4 | ||
|
|
ac5d69dba4 | ||
|
|
597351c424 | ||
|
|
1f6b2e341a | ||
|
|
035752ace2 | ||
|
|
60a1437207 | ||
|
|
e31414bc8c | ||
|
|
785a294845 | ||
|
|
98933eee34 | ||
|
|
20905810d7 | ||
|
|
c1bde83639 | ||
|
|
782a0e9616 | ||
|
|
6a3720bc8b | ||
|
|
de252359d6 | ||
|
|
deb313ca3d | ||
|
|
d119de56be | ||
|
|
f05417fa23 | ||
|
|
9ab2eb591f | ||
|
|
3f476d770f | ||
|
|
ced6759686 | ||
|
|
eba3014c59 | ||
|
|
3aeb4e16e9 | ||
|
|
3b62722251 | ||
|
|
fb1cc4868e | ||
|
|
4a0dcf0dbf | ||
|
|
4f913f146e | ||
|
|
533560f432 | ||
|
|
cf7b479a1b | ||
|
|
2e4c29a0de | ||
|
|
6f0ceb94c6 | ||
|
|
800d7ba04b | ||
|
|
fb6a6d2b93 | ||
|
|
cf2b19ae90 | ||
|
|
fb1cc93613 | ||
|
|
c2bba796c2 | ||
|
|
a02bf83842 | ||
|
|
cd9f129e2d | ||
|
|
e85c80bdcf | ||
|
|
7e83e0c482 | ||
|
|
92ac3125f3 | ||
|
|
a61feca369 | ||
|
|
8b0b811919 | ||
|
|
8742526c7f | ||
|
|
ee757cfd92 | ||
|
|
b12cfea379 | ||
|
|
45365e3e03 | ||
|
|
1b676eefd2 | ||
|
|
0092dc44fd | ||
|
|
4941b376f3 | ||
|
|
e46813cd17 | ||
|
|
58ebd224c2 | ||
|
|
95ece6e16f | ||
|
|
b82cbd06fa | ||
|
|
16210892da | ||
|
|
a452d63a56 | ||
|
|
51c7abedd3 | ||
|
|
6d0a2420a8 | ||
|
|
9cf687b73d | ||
|
|
49c9e41df5 | ||
|
|
2ec2e64213 | ||
|
|
867a61c8dc | ||
|
|
12263d1453 | ||
|
|
c0cacb2e64 | ||
|
|
0637b343b1 | ||
|
|
2473e144ef | ||
|
|
00a37d6de7 | ||
|
|
50c664e6bf | ||
|
|
22b7d20455 | ||
|
|
141262e5a5 | ||
|
|
4717abfa77 | ||
|
|
1bf1a01c32 | ||
|
|
05b714de38 | ||
|
|
11377d4e5f | ||
|
|
46ea46fdfe | ||
|
|
d4f0483238 | ||
|
|
a79610f5ea | ||
|
|
d9fb71b9a0 | ||
|
|
37057fa0cf | ||
|
|
b234128a45 | ||
|
|
67a2d57966 | ||
|
|
3a1516877e | ||
|
|
53f31d175f | ||
|
|
25323e9ce2 | ||
|
|
3136596add | ||
|
|
e7200b0b23 | ||
|
|
dfb19c1dde | ||
|
|
2363b35263 | ||
|
|
99367aaf88 | ||
|
|
ad17ef328f | ||
|
|
5f149f6a38 | ||
|
|
73ed57301b | ||
|
|
138b929db4 | ||
|
|
4585e94cd1 | ||
|
|
69ad6344f5 | ||
|
|
a55665bd14 | ||
|
|
b5e2053b0c | ||
|
|
94265eab9f | ||
|
|
eb79d473b0 | ||
|
|
c4e0a9962f | ||
|
|
ee613616ca | ||
|
|
6bbf00c371 | ||
|
|
f9f45d315d | ||
|
|
84f215b7f1 | ||
|
|
016220bb2a | ||
|
|
ba1eb73ace | ||
|
|
b304091fb3 | ||
|
|
840eaea667 | ||
|
|
956cc9fd68 | ||
|
|
e78e212f83 | ||
|
|
cdc2d4c039 | ||
|
|
cd4b0c4f94 | ||
|
|
53ada6cc40 | ||
|
|
2e6cb0f21d | ||
|
|
4287591a6b | ||
|
|
2fe0c21e36 | ||
|
|
bfa043aeba | ||
|
|
f4336ca5e9 | ||
|
|
8125cb7090 | ||
|
|
0ae1e7fbc4 | ||
|
|
88f8111a56 | ||
|
|
dbfaa519ba | ||
|
|
402e803146 | ||
|
|
5eae14a3c9 | ||
|
|
e0bfc45f5a | ||
|
|
7d8fb7aab7 | ||
|
|
846ef00aed | ||
|
|
f2f730e88c | ||
|
|
311a9405e4 | ||
|
|
6c53981883 | ||
|
|
f23f960368 | ||
|
|
f593c6d310 | ||
|
|
3fb5ea96bc | ||
|
|
30c697a3df | ||
|
|
1d50d05329 | ||
|
|
840221d9ec | ||
|
|
e52a76921f | ||
|
|
80fdb37129 | ||
|
|
bbef4aa8d9 | ||
|
|
35eba3b1e1 | ||
|
|
28a1230d26 | ||
|
|
86dd6a9608 | ||
|
|
f7a40b7324 | ||
|
|
e2e8eb837d | ||
|
|
020f7ae07e | ||
|
|
8311667930 | ||
|
|
741ab94150 | ||
|
|
5d6ca183be | ||
|
|
0f937ad6d0 | ||
|
|
ab38f220f7 | ||
|
|
a8c0b3bfd5 | ||
|
|
5d1629bf0b | ||
|
|
da7fa40c70 | ||
|
|
f1f0ee193f | ||
|
|
deccccead0 | ||
|
|
47b4464ad8 | ||
|
|
3cf4a2edc1 | ||
|
|
350f3a66dd | ||
|
|
f8edcabb05 | ||
|
|
00cafc613d | ||
|
|
8c614dc8a1 | ||
|
|
216c9d8852 | ||
|
|
741e3eb89b | ||
|
|
bc06684694 | ||
|
|
2539cb9c1a | ||
|
|
32dd3d5098 | ||
|
|
b6cf382b86 | ||
|
|
03d19a797c | ||
|
|
98cbc14039 | ||
|
|
248bb50b3e | ||
|
|
01f1dcf93e | ||
|
|
fdac82b8dc | ||
|
|
0f926cb218 | ||
|
|
de35b61b52 | ||
|
|
341aa3f070 | ||
|
|
f47254e72d | ||
|
|
0b75d4d2ed | ||
|
|
d204aa0cd4 | ||
|
|
4f6584a41d | ||
|
|
8f8f24ccfe | ||
|
|
0f2257b8bb | ||
|
|
8bd99f13c1 | ||
|
|
f8deb89592 | ||
|
|
701407581b | ||
|
|
ba2ee05bc0 | ||
|
|
c6e649129e | ||
|
|
329249ea99 | ||
|
|
65d8a30396 | ||
|
|
e29a45c4a3 | ||
|
|
438078cdc5 | ||
|
|
ae07ba7523 | ||
|
|
f201b12dd8 | ||
|
|
ee5322f406 | ||
|
|
60a2e0c963 | ||
|
|
2b55ed9b46 | ||
|
|
68eb7cb57e | ||
|
|
6387b601b1 | ||
|
|
af58fa8802 | ||
|
|
80daea5744 | ||
|
|
bf9a471484 | ||
|
|
195ed9761c | ||
|
|
fdc0123681 | ||
|
|
6fd75ae552 | ||
|
|
10c462a477 | ||
|
|
694c43292a | ||
|
|
cfa78dc9e2 | ||
|
|
cc80f5b685 | ||
|
|
58f4a11669 | ||
|
|
4f57624a67 | ||
|
|
9558520dcd | ||
|
|
8ded3623a4 | ||
|
|
12fcca2faf | ||
|
|
9dc20fc674 | ||
|
|
16430550d1 | ||
|
|
b34b66785d | ||
|
|
2cf38b6027 | ||
|
|
e1b4edaa68 | ||
|
|
97f3f70d57 | ||
|
|
cee0ce6620 | ||
|
|
89b659695f | ||
|
|
d52848ab1b | ||
|
|
314a8d71ef | ||
|
|
bfa85cd8f1 | ||
|
|
2254cb1f87 | ||
|
|
17cbfb8453 | ||
|
|
6b89b7b4a5 | ||
|
|
4fe5828d8d | ||
|
|
98422d696e | ||
|
|
e3103faeae | ||
|
|
0b23ddffb2 | ||
|
|
37fa12e214 | ||
|
|
328f8ac125 | ||
|
|
744749d22b | ||
|
|
a56fd039b4 | ||
|
|
e16867b72a | ||
|
|
ff6756447b | ||
|
|
546980a906 | ||
|
|
f93e2ad4b6 | ||
|
|
68732d6b31 | ||
|
|
c3b8146e7f | ||
|
|
271b7ca8a5 | ||
|
|
2fa87cc428 | ||
|
|
ffa7c4ee79 | ||
|
|
86c4374238 | ||
|
|
4ee8f1b9ad | ||
|
|
47dcd2b054 | ||
|
|
d099e6b85c | ||
|
|
320401e8f3 | ||
|
|
6d75244f8f | ||
|
|
05ac5d51b5 | ||
|
|
2b7c9a9673 | ||
|
|
f76bfdf6b3 | ||
|
|
7ebb70b896 | ||
|
|
960ed6bf70 | ||
|
|
b4fd4f1087 | ||
|
|
109d6db1fc | ||
|
|
07e2d9ed10 | ||
|
|
c5cd6c0337 | ||
|
|
fe1d566326 | ||
|
|
cedc918a09 | ||
|
|
1e6c0865dd | ||
|
|
7649986b55 | ||
|
|
86a82b409a | ||
|
|
f6ad9bdf82 | ||
|
|
a647526084 | ||
|
|
44ed90e181 | ||
|
|
3e7273701d | ||
|
|
d77ed30940 | ||
|
|
5ae80e67a3 | ||
|
|
184389be33 | ||
|
|
c1f022001f | ||
|
|
616d56d515 | ||
|
|
10a0b5099e | ||
|
|
0815605298 | ||
|
|
2df3216b32 | ||
|
|
74491c666d | ||
|
|
29a2eb6f2f | ||
|
|
baf56746ce | ||
|
|
5867c5af8f | ||
|
|
4a358f5cff | ||
|
|
13f2b008fd | ||
|
|
84400cd657 | ||
|
|
f2a3a6933e | ||
|
|
0a4d1cad4c | ||
|
|
08f472f9ee | ||
|
|
7f73945c8d | ||
|
|
56a7860b5a | ||
|
|
25dab86b8e | ||
|
|
35b90ca162 | ||
|
|
5babee6de3 | ||
|
|
7567d440a9 | ||
|
|
2ecd799dab | ||
|
|
5b3561f983 | ||
|
|
cce3711c02 | ||
|
|
9cdbda0828 | ||
|
|
9c4775fd38 | ||
|
|
212e0aa4c3 | ||
|
|
05300ec0e9 | ||
|
|
67fb49e54e | ||
|
|
7164b696b1 | ||
|
|
8728167733 | ||
|
|
6e80a63b68 | ||
|
|
9e43a22ec3 | ||
|
|
49d8ed4a6f | ||
|
|
c7b537e6c7 | ||
|
|
f1cdd2fa46 | ||
|
|
3d5ad02274 | ||
|
|
1cb9f4becf | ||
|
|
0d0dafbe49 | ||
|
|
048d1df2d1 | ||
|
|
4fb4154e30 | ||
|
|
0be69bbccd | ||
|
|
7015a40256 | ||
|
|
03cca642e9 | ||
|
|
579fd3780b | ||
|
|
a85d91c10e | ||
|
|
af31c496a1 | ||
|
|
f9efbaa954 | ||
|
|
d541ec7f20 | ||
|
|
1d847e2c6f | ||
|
|
2fedf4f075 | ||
|
|
e9a02c4c80 | ||
|
|
8beaccdded | ||
|
|
af6003da6d | ||
|
|
76ac2cd013 | ||
|
|
859876e3f8 | ||
|
|
7d49e7fb34 | ||
|
|
6c42ae9077 | ||
|
|
15dcc60407 | ||
|
|
5b811b7003 | ||
|
|
55d670fe3c | ||
|
|
ac3a5e52c7 | ||
|
|
2abe00e251 | ||
|
|
1bd3c29e39 | ||
|
|
1a8087bda7 | ||
|
|
72b4c2b1ec | ||
|
|
38e6820d7b | ||
|
|
765b3a57fe | ||
|
|
1c4a32f8fa | ||
|
|
3f258fcebf | ||
|
|
140f2cbfa8 | ||
|
|
6aacd77492 | ||
|
|
ef3f46f8b7 | ||
|
|
0cdd25d2cf | ||
|
|
5d02ce0636 | ||
|
|
0cd1228ba7 | ||
|
|
0595401d14 | ||
|
|
d724f8cc8e | ||
|
|
a3f5d458d7 | ||
|
|
76bfb130b0 | ||
|
|
184bb78e3b | ||
|
|
6a41af2cb2 | ||
|
|
faa149cc87 | ||
|
|
24592fe480 | ||
|
|
4be53082e0 | ||
|
|
ae8c9c668c | ||
|
|
b0c15af04f | ||
|
|
c05b710aff | ||
|
|
4299c48aef | ||
|
|
ae0523dec0 | ||
|
|
e18a6bda7b | ||
|
|
e64be95f1c | ||
|
|
a1aa0150f8 | ||
|
|
32f9cb5996 | ||
|
|
3b7e692b01 | ||
|
|
6491eba1da | ||
|
|
bb7ea7e809 | ||
|
|
169930e3b8 | ||
|
|
8e14047f36 | ||
|
|
fd29a96f7b | ||
|
|
820c12f230 | ||
|
|
ff3550e7b3 | ||
|
|
b65e43351d | ||
|
|
3fb74b632b | ||
|
|
253e54344d | ||
|
|
f1ee7d24a6 | ||
|
|
475673b3e7 | ||
|
|
dd49afef01 | ||
|
|
d0c842fe87 | ||
|
|
b873bd161e | ||
|
|
60b76b9ccc | ||
|
|
ef39ee2f66 | ||
|
|
6c83c2ef9b | ||
|
|
9495ec67ab | ||
|
|
bb5680f6c4 | ||
|
|
acbe49f518 | ||
|
|
9dd55938c2 | ||
|
|
5433e6e27e | ||
|
|
2dd6eb5f0f | ||
|
|
1731713dbb | ||
|
|
327ddb7bad | ||
|
|
9e4adc1fa2 | ||
|
|
bce7fdb470 | ||
|
|
b79422962c | ||
|
|
e5989ae5c2 | ||
|
|
64feafa3a6 | ||
|
|
52e4fa4d0d | ||
|
|
6462c02861 | ||
|
|
c657182659 | ||
|
|
04d93eff34 | ||
|
|
40d60aeb4a | ||
|
|
ac875fa1b9 | ||
|
|
b7c3e8a4f5 | ||
|
|
2524e15947 | ||
|
|
995c579403 | ||
|
|
848b7ac1ae | ||
|
|
9476b5ba7c | ||
|
|
7b58696bdc | ||
|
|
6159178d99 | ||
|
|
99e5e0c117 | ||
|
|
be1a3c1d8b | ||
|
|
f6378b055c | ||
|
|
2574bb19cd | ||
|
|
aa9d43cc69 | ||
|
|
d7f18ebec1 | ||
|
|
b40f6976bb | ||
|
|
cd1db57b7c | ||
|
|
5a6ca42c75 | ||
|
|
80874a743c | ||
|
|
6cc612564f | ||
|
|
909bbb5e66 | ||
|
|
ff3ea7de58 | ||
|
|
dd316e6ce1 | ||
|
|
ba893e77cd | ||
|
|
21904f1e39 | ||
|
|
b5d5ecbab2 | ||
|
|
ee612908ac | ||
|
|
2ee04dffac | ||
|
|
be25adf990 | ||
|
|
ab72b6e1ba | ||
|
|
a4718e7a45 | ||
|
|
f948d50d8b | ||
|
|
cb797d5913 | ||
|
|
8941c192de | ||
|
|
5b726c1e61 | ||
|
|
03871a0bf0 | ||
|
|
e002e9cb8f | ||
|
|
d414831c79 | ||
|
|
89807ada94 | ||
|
|
351a31b079 | ||
|
|
af0127c905 | ||
|
|
95612e7140 | ||
|
|
a338b5233c | ||
|
|
ad26225f63 | ||
|
|
16db570f18 | ||
|
|
97c68360a1 | ||
|
|
00192b9d0f | ||
|
|
e745253d08 | ||
|
|
76905c55d5 | ||
|
|
d4bce5456b | ||
|
|
58136d30e6 | ||
|
|
563fb0330a | ||
|
|
c2ab3b4240 | ||
|
|
f5dde6e4d6 | ||
|
|
a9779703dd | ||
|
|
9f4a9e77ae | ||
|
|
df37071c3d | ||
|
|
fa164ac5d2 | ||
|
|
f5de4c3f22 | ||
|
|
dd9099af0a | ||
|
|
5bdb63a818 | ||
|
|
8a4c709e87 | ||
|
|
75f6e07c40 | ||
|
|
de9b11a049 | ||
|
|
067b3f91a7 | ||
|
|
5d215a89b6 | ||
|
|
63679c15dd | ||
|
|
38229a43dc | ||
|
|
1d1ae238d4 | ||
|
|
c2d300c0f1 | ||
|
|
bcb89017a0 | ||
|
|
e04a3eed5f | ||
|
|
e77cf40938 | ||
|
|
cb66b19d70 | ||
|
|
9edf05c19a | ||
|
|
6a6b4a2283 | ||
|
|
0473bb3925 | ||
|
|
4afc3a60a4 | ||
|
|
e9c9a3ac58 | ||
|
|
98260e239e | ||
|
|
f751b2034d | ||
|
|
9ce22a33f0 | ||
|
|
3da64ca0fe | ||
|
|
9a883dc02c | ||
|
|
5ab6fe7e56 | ||
|
|
c730eaa860 | ||
|
|
5ba2d6bc8e | ||
|
|
64feee79ff | ||
|
|
c490ab09ad | ||
|
|
61762e894c | ||
|
|
ac4ff33dff | ||
|
|
72abeea51f | ||
|
|
6ec2b42669 | ||
|
|
a93e967d30 | ||
|
|
b5984b7871 | ||
|
|
70ccbbc929 | ||
|
|
79d4fc508c | ||
|
|
794f0f874f | ||
|
|
aff53e8be3 | ||
|
|
2de6847323 | ||
|
|
eed037a3a1 | ||
|
|
4099c467bb | ||
|
|
6b51adbc9a | ||
|
|
307be1dda2 | ||
|
|
7da6145ec6 | ||
|
|
0e4298a592 | ||
|
|
037fab74eb | ||
|
|
fb849928c9 | ||
|
|
7833aae0a1 | ||
|
|
6edd71b1f0 | ||
|
|
2f2f310a40 | ||
|
|
14bfdaa2ee | ||
|
|
ffd0a69e43 | ||
|
|
5b79d0ef46 | ||
|
|
8f2a885a7d | ||
|
|
31f6300c16 | ||
|
|
54710c22f0 | ||
|
|
352aa2b6b1 | ||
|
|
624e5b5e62 | ||
|
|
65e3b5c8f1 | ||
|
|
750732f203 | ||
|
|
9957711643 | ||
|
|
8f4fb0d28b | ||
|
|
5d63f23cfc | ||
|
|
c0fb8d22db | ||
|
|
1732b297b1 |
67
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
67
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -1,67 +0,0 @@
|
||||
name: Bug Report
|
||||
description: Report a bug encountered while running Nightingale
|
||||
labels: ["kind/bug"]
|
||||
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for taking time to fill out this bug report!
|
||||
The more detailed the form is filled in, the easier the problem will be solved.
|
||||
- type: textarea
|
||||
id: config
|
||||
attributes:
|
||||
label: Relevant server.conf | webapi.conf
|
||||
description: Place config in the toml code section. This will be automatically formatted into toml, so no need for backticks.
|
||||
render: toml
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: logs
|
||||
attributes:
|
||||
label: Relevant logs
|
||||
description: categraf | telegraf | server | webapi | prometheus | chrome request/response ...
|
||||
render: text
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: system-info
|
||||
attributes:
|
||||
label: System info
|
||||
description: Include nightingale version, operating system, and other relevant details
|
||||
placeholder: ex. n9e 5.9.2, n9e-fe 5.5.0, categraf 0.1.0, Ubuntu 20.04, Docker 20.10.8
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: reproduce
|
||||
attributes:
|
||||
label: Steps to reproduce
|
||||
description: Describe the steps to reproduce the bug.
|
||||
value: |
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
...
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: expected-behavior
|
||||
attributes:
|
||||
label: Expected behavior
|
||||
description: Describe what you expected to happen when you performed the above steps.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: actual-behavior
|
||||
attributes:
|
||||
label: Actual behavior
|
||||
description: Describe what actually happened when you performed the above steps.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: additional-info
|
||||
attributes:
|
||||
label: Additional info
|
||||
description: Include gist of relevant config, logs, etc.
|
||||
validations:
|
||||
required: false
|
||||
33
.github/ISSUE_TEMPLATE/question.yml
vendored
Normal file
33
.github/ISSUE_TEMPLATE/question.yml
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
name: Bug Report & Usage Question
|
||||
description: Reporting a bug or asking a question about how to use Nightingale
|
||||
labels: []
|
||||
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
The more detailed the form is filled in, the easier the problem will be solved.
|
||||
提供的信息越详细,问题解决的可能性就越大。另外, 提问之前请先搜索历史 issue (包括 close 的), 以免重复提问。
|
||||
- type: textarea
|
||||
id: question
|
||||
attributes:
|
||||
label: Question and Steps to reproduce
|
||||
description: Describe your question and steps to reproduce the bug. 描述问题以及复现步骤
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: logs
|
||||
attributes:
|
||||
label: Relevant logs and configurations
|
||||
description: Relevant logs and configurations. 报错日志([查看方法](https://flashcat.cloud/docs/content/flashcat-monitor/nightingale-v6/faq/how-to-check-logs/))以及各个相关组件的配置信息
|
||||
render: text
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: system-info
|
||||
attributes:
|
||||
label: Version
|
||||
description: Include nightingale version, operating system, and other relevant details. 请告知夜莺的版本、操作系统的版本、CPU架构等信息
|
||||
validations:
|
||||
required: true
|
||||
|
||||
3
.github/workflows/n9e.yml
vendored
3
.github/workflows/n9e.yml
vendored
@@ -26,7 +26,8 @@ jobs:
|
||||
- name: Run GoReleaser
|
||||
uses: goreleaser/goreleaser-action@v3
|
||||
with:
|
||||
version: latest
|
||||
distribution: goreleaser
|
||||
version: '~> v1'
|
||||
args: release --rm-dist
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
13
.gitignore
vendored
13
.gitignore
vendored
@@ -30,7 +30,11 @@ _test
|
||||
/dist
|
||||
/etc/*.local.yml
|
||||
/etc/*.local.conf
|
||||
/etc/rsa/*
|
||||
/etc/plugins/*.local.yml
|
||||
/etc/script/rules.yaml
|
||||
/etc/script/alert-rules.json
|
||||
/etc/script/record-rules.json
|
||||
/data*
|
||||
/tarball
|
||||
/run
|
||||
@@ -40,9 +44,14 @@ _test
|
||||
/n9e
|
||||
/docker/pub
|
||||
/docker/n9e
|
||||
/docker/mysqldata
|
||||
/docker/experience_pg_vm/pgdata
|
||||
/docker/compose-bridge/mysqldata
|
||||
/docker/compose-host-network/mysqldata
|
||||
/docker/compose-host-network-metric-log/mysqldata
|
||||
/docker/compose-host-network-metric-log/n9e-logs
|
||||
/docker/compose-postgres/pgdata
|
||||
/etc.local*
|
||||
/front/statik/statik.go
|
||||
/docker/compose-bridge/etc-nightingale/rsa/
|
||||
|
||||
.alerts
|
||||
.idea
|
||||
|
||||
@@ -15,7 +15,8 @@ builds:
|
||||
- id: build
|
||||
hooks:
|
||||
pre:
|
||||
- ./fe.sh
|
||||
- cmd: sh -x ./fe.sh
|
||||
output: true
|
||||
main: ./cmd/center/
|
||||
binary: n9e
|
||||
env:
|
||||
@@ -41,22 +42,9 @@ builds:
|
||||
ldflags:
|
||||
- -s -w
|
||||
- -X github.com/ccfos/nightingale/v6/pkg/version.Version={{ .Tag }}-{{.Commit}}
|
||||
- id: build-alert
|
||||
main: ./cmd/alert/
|
||||
binary: n9e-alert
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
goos:
|
||||
- linux
|
||||
goarch:
|
||||
- amd64
|
||||
- arm64
|
||||
ldflags:
|
||||
- -s -w
|
||||
- -X github.com/ccfos/nightingale/v6/pkg/version.Version={{ .Tag }}-{{.Commit}}
|
||||
- id: build-pushgw
|
||||
main: ./cmd/pushgw/
|
||||
binary: n9e-pushgw
|
||||
- id: build-edge
|
||||
main: ./cmd/edge/
|
||||
binary: n9e-edge
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
goos:
|
||||
@@ -73,8 +61,7 @@ archives:
|
||||
builds:
|
||||
- build
|
||||
- build-cli
|
||||
- build-alert
|
||||
- build-pushgw
|
||||
- build-edge
|
||||
format: tar.gz
|
||||
format_overrides:
|
||||
- goos: windows
|
||||
@@ -84,7 +71,6 @@ archives:
|
||||
files:
|
||||
- docker/*
|
||||
- etc/*
|
||||
- pub/*
|
||||
- integrations/*
|
||||
- cli/*
|
||||
- n9e.sql
|
||||
@@ -104,7 +90,6 @@ dockers:
|
||||
- build
|
||||
dockerfile: docker/Dockerfile.goreleaser
|
||||
extra_files:
|
||||
- pub
|
||||
- etc
|
||||
- integrations
|
||||
use: buildx
|
||||
@@ -118,7 +103,6 @@ dockers:
|
||||
- build
|
||||
dockerfile: docker/Dockerfile.goreleaser.arm64
|
||||
extra_files:
|
||||
- pub
|
||||
- etc
|
||||
- integrations
|
||||
use: buildx
|
||||
|
||||
13
Makefile
13
Makefile
@@ -1,4 +1,4 @@
|
||||
.PHONY: prebuild start build
|
||||
.PHONY: prebuild build
|
||||
|
||||
ROOT:=$(shell pwd -P)
|
||||
GIT_COMMIT:=$(shell git --work-tree ${ROOT} rev-parse 'HEAD^{commit}')
|
||||
@@ -6,16 +6,19 @@ _GIT_VERSION:=$(shell git --work-tree ${ROOT} describe --tags --abbrev=14 "${GIT
|
||||
TAG=$(shell echo "${_GIT_VERSION}" | awk -F"-" '{print $$1}')
|
||||
RELEASE_VERSION:="$(TAG)-$(GIT_COMMIT)"
|
||||
|
||||
all: prebuild build
|
||||
|
||||
prebuild:
|
||||
echo "begin download and embed the front-end file..."
|
||||
sh fe.sh
|
||||
echo "front-end file download and embedding completed."
|
||||
|
||||
all: build
|
||||
|
||||
build:
|
||||
go build -ldflags "-w -s -X github.com/ccfos/nightingale/v6/pkg/version.Version=$(RELEASE_VERSION)" -o n9e ./cmd/center/main.go
|
||||
|
||||
build-edge:
|
||||
go build -ldflags "-w -s -X github.com/ccfos/nightingale/v6/pkg/version.Version=$(RELEASE_VERSION)" -o n9e-edge ./cmd/edge/
|
||||
|
||||
build-alert:
|
||||
go build -ldflags "-w -s -X github.com/ccfos/nightingale/v6/pkg/version.Version=$(RELEASE_VERSION)" -o n9e-alert ./cmd/alert/main.go
|
||||
|
||||
@@ -28,10 +31,10 @@ build-cli:
|
||||
run:
|
||||
nohup ./n9e > n9e.log 2>&1 &
|
||||
|
||||
run_alert:
|
||||
run-alert:
|
||||
nohup ./n9e-alert > n9e-alert.log 2>&1 &
|
||||
|
||||
run_pushgw:
|
||||
run-pushgw:
|
||||
nohup ./n9e-pushgw > n9e-pushgw.log 2>&1 &
|
||||
|
||||
release:
|
||||
|
||||
120
README.md
120
README.md
@@ -1,68 +1,110 @@
|
||||
<p align="center">
|
||||
<a href="https://github.com/ccfos/nightingale">
|
||||
<img src="doc/img/nightingale_logo_h.png" alt="nightingale - cloud native monitoring" width="240" /></a>
|
||||
<img src="doc/img/Nightingale_L_V.png" alt="nightingale - cloud native monitoring" width="100" /></a>
|
||||
</p>
|
||||
<p align="center">
|
||||
<b>开源告警管理专家 一体化的可观测平台</b>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img alt="GitHub latest release" src="https://img.shields.io/github/v/release/ccfos/nightingale"/>
|
||||
<a href="https://n9e.github.io">
|
||||
<a href="https://flashcat.cloud/docs/">
|
||||
<img alt="Docs" src="https://img.shields.io/badge/docs-get%20started-brightgreen"/></a>
|
||||
<a href="https://hub.docker.com/u/flashcatcloud">
|
||||
<img alt="Docker pulls" src="https://img.shields.io/docker/pulls/flashcatcloud/nightingale"/></a>
|
||||
<img alt="GitHub Repo stars" src="https://img.shields.io/github/stars/ccfos/nightingale">
|
||||
<img alt="GitHub Repo issues" src="https://img.shields.io/github/issues/ccfos/nightingale">
|
||||
<img alt="GitHub Repo issues closed" src="https://img.shields.io/github/issues-closed/ccfos/nightingale">
|
||||
<img alt="GitHub forks" src="https://img.shields.io/github/forks/ccfos/nightingale">
|
||||
<a href="https://github.com/ccfos/nightingale/graphs/contributors">
|
||||
<img alt="GitHub contributors" src="https://img.shields.io/github/contributors-anon/ccfos/nightingale"/></a>
|
||||
<img alt="GitHub Repo stars" src="https://img.shields.io/github/stars/ccfos/nightingale">
|
||||
<img alt="GitHub forks" src="https://img.shields.io/github/forks/ccfos/nightingale">
|
||||
<br/><img alt="GitHub Repo issues" src="https://img.shields.io/github/issues/ccfos/nightingale">
|
||||
<img alt="GitHub Repo issues closed" src="https://img.shields.io/github/issues-closed/ccfos/nightingale">
|
||||
<img alt="GitHub latest release" src="https://img.shields.io/github/v/release/ccfos/nightingale"/>
|
||||
<img alt="License" src="https://img.shields.io/badge/license-Apache--2.0-blue"/>
|
||||
<a href="https://n9e-talk.slack.com/">
|
||||
<img alt="GitHub contributors" src="https://img.shields.io/badge/join%20slack-%23n9e-brightgreen.svg"/></a>
|
||||
<img alt="License" src="https://img.shields.io/badge/license-Apache--2.0-blue"/>
|
||||
</p>
|
||||
<p align="center">
|
||||
告警管理专家,一体化开源观测平台!
|
||||
</p>
|
||||
|
||||
|
||||
|
||||
[English](./README_en.md) | [中文](./README.md)
|
||||
|
||||
## 资料
|
||||
## 夜莺 Nightingale 是什么
|
||||
|
||||
- 文档:[https://flashcat.cloud/docs/](https://flashcat.cloud/docs/)
|
||||
- 论坛提问:[https://answer.flashcat.cloud/](https://answer.flashcat.cloud/)
|
||||
- 报Bug:[https://github.com/ccfos/nightingale/issues](https://github.com/ccfos/nightingale/issues/new?assignees=&labels=kind%2Fbug&projects=&template=bug_report.yml)
|
||||
- 商业版本:[企业版](https://mp.weixin.qq.com/s/FOwnnGPkRao2ZDV574EHrw) | [专业版](https://mp.weixin.qq.com/s/uM2a8QUDJEYwdBpjkbQDxA) 感兴趣请 [联系我们交流试用](https://flashcat.cloud/contact/)
|
||||
夜莺监控是一款开源云原生观测分析工具,采用 All-in-One 的设计理念,集数据采集、可视化、监控告警、数据分析于一体,与云原生生态紧密集成,提供开箱即用的企业级监控分析和告警能力。夜莺于 2020 年 3 月 20 日,在 github 上发布 v1 版本,已累计迭代 100 多个版本。
|
||||
|
||||
## 功能和特点
|
||||
|
||||
- **统一接入各种时序库**:支持对接 Prometheus、VictoriaMetrics、Thanos、Mimir、M3DB 等多种时序库,实现统一告警管理
|
||||
- **专业告警能力**:内置支持多种告警规则,可以扩展支持所有通知媒介,支持告警屏蔽、告警抑制、告警自愈、告警事件管理
|
||||
- **无缝搭配 [FlashDuty](https://flashcat.cloud/product/flashcat-duty/)**:实现告警聚合收敛、认领、升级、排班、IM集成,确保告警处理不遗漏,减少打扰,更好协同
|
||||
- **支持所有常见采集器**:支持 categraf、telegraf、grafana-agent、datadog-agent、给类 exporter 作为采集器,没有什么数据是不能监控的
|
||||
- **统一的观测平台**:从 v6 版本开始,支持接入 ElasticSearch、Jaeger 数据源,逐步实现日志、链路、指标的一体化观测
|
||||
|
||||
## 产品示意图
|
||||
|
||||
https://user-images.githubusercontent.com/792850/216888712-2565fcea-9df5-47bd-a49e-d60af9bd76e8.mp4
|
||||
夜莺最初由滴滴开发和开源,并于 2022 年 5 月 11 日,捐赠予中国计算机学会开源发展委员会(CCF ODC),为 CCF ODC 成立后接受捐赠的第一个开源项目。夜莺的核心研发团队,也是 Open-Falcon 项目原核心研发人员,从 2014 年(Open-Falcon 是 2014 年开源)算起来,也有 10 年了,只为把监控这个事情做好。
|
||||
|
||||
|
||||
## 加入交流群
|
||||
## 快速开始
|
||||
- 👉[文档中心](https://flashcat.cloud/docs/) | [下载中心](https://flashcat.cloud/download/nightingale/)
|
||||
- ❤️[报告 Bug](https://github.com/ccfos/nightingale/issues/new?assignees=&labels=&projects=&template=question.yml)
|
||||
- ℹ️为了提供更快速的访问体验,上述文档和下载站点托管于 [FlashcatCloud](https://flashcat.cloud)
|
||||
|
||||
欢迎加入 QQ 交流群,群号:479290895,也可以扫下方二维码加入微信交流群:
|
||||
## 功能特点
|
||||
|
||||
<img src="doc/img/wecom.png" width="240">
|
||||
- 对接多种时序库:支持对接 Prometheus、VictoriaMetrics、Thanos、Mimir、M3DB、TDengine 等多种时序库,实现统一告警管理。
|
||||
- 专业告警能力:内置支持多种告警规则,可以扩展支持常见通知媒介,支持告警屏蔽/抑制/订阅/自愈、告警事件管理。
|
||||
- 高性能可视化引擎:支持多种图表样式,内置众多 Dashboard 模版,也可导入 Grafana 模版,开箱即用,开源协议商业友好。
|
||||
- 支持常见采集器:支持 [Categraf](https://flashcat.cloud/product/categraf)、Telegraf、Grafana-agent、Datadog-agent、各种 Exporter 作为采集器,没有什么数据是不能监控的。
|
||||
- 👀无缝搭配 [Flashduty](https://flashcat.cloud/product/flashcat-duty/):实现告警聚合收敛、认领、升级、排班、IM集成,确保告警处理不遗漏,减少打扰,高效协同。
|
||||
|
||||
## Stargazers over time
|
||||
[](https://starchart.cc/ccfos/nightingale)
|
||||
|
||||
## Contributors
|
||||
## 截图演示
|
||||
|
||||
即时查询,类似 Prometheus 内置的查询分析页面,做 ad-hoc 查询,夜莺做了一些 UI 优化,同时提供了一些内置 promql 指标,让不太了解 promql 的用户也可以快速查询。
|
||||
|
||||

|
||||
|
||||
当然,也可以直接通过指标视图查看,有了指标视图,即时查询基本可以不用了,或者只有高端玩家使用即时查询,普通用户直接通过指标视图查询即可。
|
||||
|
||||

|
||||
|
||||
夜莺内置了常用仪表盘,可以直接导入使用。也可以导入 Grafana 仪表盘,不过只能兼容 Grafana 基本图表,如果已经习惯了 Grafana 建议继续使用 Grafana 看图,把夜莺作为一个告警引擎使用。
|
||||
|
||||

|
||||
|
||||
除了内置的仪表盘,也内置了很多告警规则,开箱即用。
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
## 产品架构
|
||||
|
||||
社区使用夜莺最多的场景就是使用夜莺做告警引擎,对接多套时序库,统一告警规则管理。绘图仍然使用 Grafana 居多。作为一个告警引擎,夜莺的产品架构如下:
|
||||
|
||||

|
||||
|
||||
对于个别边缘机房,如果和中心夜莺服务端网络链路不好,希望提升告警可用性,我们也提供边缘机房告警引擎下沉部署模式,这个模式下,即便网络割裂,告警功能也不受影响。
|
||||
|
||||

|
||||
|
||||
## 近期计划
|
||||
|
||||
- [ ] 仪表盘:支持内嵌 Grafana
|
||||
- [ ] 告警规则:通知时支持配置过滤标签,避免告警事件中一堆不重要的标签
|
||||
- [x] 告警规则:支持配置恢复时的 Promql,告警恢复通知也可以带上恢复时的值了
|
||||
- [ ] 机器管理:自定义标签拆分管理,agent 自动上报的标签和用户在页面自定义的标签分开管理,对于 agent 自动上报的标签,以 agent 为准,直接覆盖服务端 DB 中的数据
|
||||
- [ ] 机器管理:机器支持角色字段,即无头标签,用于描述混部场景
|
||||
- [ ] 机器管理:把业务组的 busigroup 标签迁移到机器的属性里,让机器支持挂到多个业务组
|
||||
- [ ] 告警规则:增加 Host Metrics 类别,支持按照业务组、角色、标签等筛选机器,规则 promql 支持变量,支持在机器颗粒度配置变量值
|
||||
- [ ] 告警通知:重构整个通知逻辑,引入事件处理的 pipeline,支持对告警事件做自定义处理和灵活分派
|
||||
|
||||
## 交流渠道
|
||||
- 报告Bug,优先推荐提交[夜莺GitHub Issue](https://github.com/ccfos/nightingale/issues/new?assignees=&labels=kind%2Fbug&projects=&template=bug_report.yml)
|
||||
- 推荐完整浏览[夜莺文档站点](https://flashcat.cloud/docs/content/flashcat-monitor/nightingale-v7/introduction/),了解更多信息
|
||||
- 推荐搜索关注夜莺公众号,第一时间获取社区动态:`夜莺监控Nightingale`
|
||||
- 日常答疑、技术分享、用户之间的交流,统一使用知识星球,大伙可以免费加入交流,[入口在这里](https://download.flashcat.cloud/ulric/20240319095409.png)
|
||||
|
||||
## 广受关注
|
||||
[](https://star-history.com/#ccfos/nightingale&Date)
|
||||
|
||||
|
||||
## 社区共建
|
||||
- ❇️请阅读浏览[夜莺开源项目和社区治理架构草案](./doc/community-governance.md),真诚欢迎每一位用户、开发者、公司以及组织,使用夜莺监控、积极反馈 Bug、提交功能需求、分享最佳实践,共建专业、活跃的夜莺开源社区。
|
||||
- 夜莺贡献者❤️
|
||||
<a href="https://github.com/ccfos/nightingale/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=ccfos/nightingale" />
|
||||
</a>
|
||||
|
||||
## License
|
||||
[Apache License V2.0](https://github.com/didi/nightingale/blob/main/LICENSE)
|
||||
|
||||
## 社区管理
|
||||
|
||||
[夜莺开源项目和社区治理架构(草案)](./doc/community-governance.md)
|
||||
|
||||
- [Apache License V2.0](https://github.com/didi/nightingale/blob/main/LICENSE)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<p align="center">
|
||||
<a href="https://github.com/ccfos/nightingale">
|
||||
<img src="doc/img/nightingale_logo_h.png" alt="nightingale - cloud native monitoring" width="240" /></a>
|
||||
<img src="doc/img/Nightingale_L_V.png" alt="nightingale - cloud native monitoring" width="240" /></a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
@@ -25,7 +25,7 @@
|
||||
We recommend upgrading your <b>Prometheus + AlertManager + Grafana</b> combination to Nightingale!
|
||||
</p>
|
||||
|
||||
[English](./README.md) | [中文](./README_ZH.md)
|
||||
[English](./README_en.md) | [中文](./README.md)
|
||||
|
||||
|
||||
## Highlighted Features
|
||||
@@ -62,7 +62,7 @@
|
||||
|
||||
## Getting Started
|
||||
|
||||
[English Doc](https://n9e.github.io/) | [中文文档](http://n9e.flashcat.cloud/)
|
||||
[https://n9e.github.io/](https://n9e.github.io/)
|
||||
|
||||
## Screenshots
|
||||
|
||||
|
||||
@@ -2,11 +2,10 @@ package aconf
|
||||
|
||||
import (
|
||||
"path"
|
||||
|
||||
"github.com/toolkits/pkg/runner"
|
||||
)
|
||||
|
||||
type Alert struct {
|
||||
Disable bool
|
||||
EngineDelay int64
|
||||
Heartbeat HeartbeatConfig
|
||||
Alerting Alerting
|
||||
@@ -47,16 +46,9 @@ type RedisPub struct {
|
||||
ChannelKey string
|
||||
}
|
||||
|
||||
type Ibex struct {
|
||||
Address string
|
||||
BasicAuthUser string
|
||||
BasicAuthPass string
|
||||
Timeout int64
|
||||
}
|
||||
|
||||
func (a *Alert) PreCheck() {
|
||||
func (a *Alert) PreCheck(configDir string) {
|
||||
if a.Alerting.TemplatesDir == "" {
|
||||
a.Alerting.TemplatesDir = path.Join(runner.Cwd, "etc", "template")
|
||||
a.Alerting.TemplatesDir = path.Join(configDir, "template")
|
||||
}
|
||||
|
||||
if a.Alerting.NotifyConcurrency == 0 {
|
||||
@@ -70,4 +62,8 @@ func (a *Alert) PreCheck() {
|
||||
if a.Heartbeat.EngineName == "" {
|
||||
a.Heartbeat.EngineName = "default"
|
||||
}
|
||||
|
||||
if a.EngineDelay == 0 {
|
||||
a.EngineDelay = 30
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"github.com/ccfos/nightingale/v6/alert/router"
|
||||
"github.com/ccfos/nightingale/v6/alert/sender"
|
||||
"github.com/ccfos/nightingale/v6/conf"
|
||||
"github.com/ccfos/nightingale/v6/dumper"
|
||||
"github.com/ccfos/nightingale/v6/memsto"
|
||||
"github.com/ccfos/nightingale/v6/models"
|
||||
"github.com/ccfos/nightingale/v6/pkg/ctx"
|
||||
@@ -23,6 +24,10 @@ import (
|
||||
"github.com/ccfos/nightingale/v6/prom"
|
||||
"github.com/ccfos/nightingale/v6/pushgw/pconf"
|
||||
"github.com/ccfos/nightingale/v6/pushgw/writer"
|
||||
"github.com/ccfos/nightingale/v6/storage"
|
||||
"github.com/ccfos/nightingale/v6/tdengine"
|
||||
|
||||
"github.com/flashcatcloud/ibex/src/cmd/ibex"
|
||||
)
|
||||
|
||||
func Initialize(configDir string, cryptoKey string) (func(), error) {
|
||||
@@ -38,25 +43,42 @@ func Initialize(configDir string, cryptoKey string) (func(), error) {
|
||||
|
||||
ctx := ctx.NewContext(context.Background(), nil, false, config.CenterApi)
|
||||
|
||||
var redis storage.Redis
|
||||
redis, err = storage.NewRedis(config.Redis)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
syncStats := memsto.NewSyncStats()
|
||||
alertStats := astats.NewSyncStats()
|
||||
|
||||
targetCache := memsto.NewTargetCache(ctx, syncStats, nil)
|
||||
configCache := memsto.NewConfigCache(ctx, syncStats, nil, "")
|
||||
targetCache := memsto.NewTargetCache(ctx, syncStats, redis)
|
||||
busiGroupCache := memsto.NewBusiGroupCache(ctx, syncStats)
|
||||
alertMuteCache := memsto.NewAlertMuteCache(ctx, syncStats)
|
||||
alertRuleCache := memsto.NewAlertRuleCache(ctx, syncStats)
|
||||
notifyConfigCache := memsto.NewNotifyConfigCache(ctx)
|
||||
notifyConfigCache := memsto.NewNotifyConfigCache(ctx, configCache)
|
||||
dsCache := memsto.NewDatasourceCache(ctx, syncStats)
|
||||
userCache := memsto.NewUserCache(ctx, syncStats)
|
||||
userGroupCache := memsto.NewUserGroupCache(ctx, syncStats)
|
||||
taskTplsCache := memsto.NewTaskTplCache(ctx)
|
||||
|
||||
promClients := prom.NewPromClient(ctx, config.Alert.Heartbeat)
|
||||
promClients := prom.NewPromClient(ctx)
|
||||
tdengineClients := tdengine.NewTdengineClient(ctx, config.Alert.Heartbeat)
|
||||
|
||||
externalProcessors := process.NewExternalProcessors()
|
||||
|
||||
Start(config.Alert, config.Pushgw, syncStats, alertStats, externalProcessors, targetCache, busiGroupCache, alertMuteCache, alertRuleCache, notifyConfigCache, dsCache, ctx, promClients)
|
||||
Start(config.Alert, config.Pushgw, syncStats, alertStats, externalProcessors, targetCache, busiGroupCache, alertMuteCache, alertRuleCache, notifyConfigCache, taskTplsCache, dsCache, ctx, promClients, tdengineClients, userCache, userGroupCache)
|
||||
|
||||
r := httpx.GinEngine(config.Global.RunMode, config.HTTP)
|
||||
rt := router.New(config.HTTP, config.Alert, alertMuteCache, targetCache, busiGroupCache, alertStats, ctx, externalProcessors)
|
||||
|
||||
if config.Ibex.Enable {
|
||||
ibex.ServerStart(false, nil, redis, config.HTTP.APIForService.BasicAuth, config.Alert.Heartbeat, &config.CenterApi, r, nil, config.Ibex, config.HTTP.Port)
|
||||
}
|
||||
|
||||
rt.Config(r)
|
||||
dumper.ConfigRouter(r)
|
||||
|
||||
httpClean := httpx.Init(config.HTTP, r)
|
||||
|
||||
@@ -67,27 +89,28 @@ func Initialize(configDir string, cryptoKey string) (func(), error) {
|
||||
}
|
||||
|
||||
func Start(alertc aconf.Alert, pushgwc pconf.Pushgw, syncStats *memsto.Stats, alertStats *astats.Stats, externalProcessors *process.ExternalProcessorsType, targetCache *memsto.TargetCacheType, busiGroupCache *memsto.BusiGroupCacheType,
|
||||
alertMuteCache *memsto.AlertMuteCacheType, alertRuleCache *memsto.AlertRuleCacheType, notifyConfigCache *memsto.NotifyConfigCacheType, datasourceCache *memsto.DatasourceCacheType, ctx *ctx.Context, promClients *prom.PromClientMap) {
|
||||
userCache := memsto.NewUserCache(ctx, syncStats)
|
||||
userGroupCache := memsto.NewUserGroupCache(ctx, syncStats)
|
||||
alertMuteCache *memsto.AlertMuteCacheType, alertRuleCache *memsto.AlertRuleCacheType, notifyConfigCache *memsto.NotifyConfigCacheType, taskTplsCache *memsto.TaskTplCache, datasourceCache *memsto.DatasourceCacheType, ctx *ctx.Context,
|
||||
promClients *prom.PromClientMap, tdendgineClients *tdengine.TdengineClientMap, userCache *memsto.UserCacheType, userGroupCache *memsto.UserGroupCacheType) {
|
||||
alertSubscribeCache := memsto.NewAlertSubscribeCache(ctx, syncStats)
|
||||
recordingRuleCache := memsto.NewRecordingRuleCache(ctx, syncStats)
|
||||
targetsOfAlertRulesCache := memsto.NewTargetOfAlertRuleCache(ctx, alertc.Heartbeat.EngineName, syncStats)
|
||||
|
||||
go models.InitNotifyConfig(ctx, alertc.Alerting.TemplatesDir)
|
||||
|
||||
naming := naming.NewNaming(ctx, alertc.Heartbeat)
|
||||
naming := naming.NewNaming(ctx, alertc.Heartbeat, alertStats)
|
||||
|
||||
writers := writer.NewWriters(pushgwc)
|
||||
record.NewScheduler(alertc, recordingRuleCache, promClients, writers, alertStats)
|
||||
|
||||
eval.NewScheduler(alertc, externalProcessors, alertRuleCache, targetCache, busiGroupCache, alertMuteCache, datasourceCache, promClients, naming, ctx, alertStats)
|
||||
eval.NewScheduler(alertc, externalProcessors, alertRuleCache, targetCache, targetsOfAlertRulesCache,
|
||||
busiGroupCache, alertMuteCache, datasourceCache, promClients, tdendgineClients, naming, ctx, alertStats)
|
||||
|
||||
dp := dispatch.NewDispatch(alertRuleCache, userCache, userGroupCache, alertSubscribeCache, targetCache, notifyConfigCache, alertc.Alerting, ctx)
|
||||
consumer := dispatch.NewConsumer(alertc.Alerting, ctx, dp)
|
||||
dp := dispatch.NewDispatch(alertRuleCache, userCache, userGroupCache, alertSubscribeCache, targetCache, notifyConfigCache, taskTplsCache, alertc.Alerting, ctx, alertStats)
|
||||
consumer := dispatch.NewConsumer(alertc.Alerting, ctx, dp, promClients)
|
||||
|
||||
go dp.ReloadTpls()
|
||||
go consumer.LoopConsume()
|
||||
|
||||
go queue.ReportQueueSize(alertStats)
|
||||
go sender.StartEmailSender(notifyConfigCache.GetSMTP()) // todo
|
||||
go sender.InitEmailSender(notifyConfigCache)
|
||||
}
|
||||
|
||||
@@ -10,22 +10,77 @@ const (
|
||||
)
|
||||
|
||||
type Stats struct {
|
||||
CounterSampleTotal *prometheus.CounterVec
|
||||
CounterAlertsTotal *prometheus.CounterVec
|
||||
GaugeAlertQueueSize prometheus.Gauge
|
||||
GaugeSampleQueueSize *prometheus.GaugeVec
|
||||
RequestDuration *prometheus.HistogramVec
|
||||
ForwardDuration *prometheus.HistogramVec
|
||||
AlertNotifyTotal *prometheus.CounterVec
|
||||
AlertNotifyErrorTotal *prometheus.CounterVec
|
||||
CounterAlertsTotal *prometheus.CounterVec
|
||||
GaugeAlertQueueSize prometheus.Gauge
|
||||
CounterRuleEval *prometheus.CounterVec
|
||||
CounterQueryDataErrorTotal *prometheus.CounterVec
|
||||
CounterQueryDataTotal *prometheus.CounterVec
|
||||
CounterRecordEval *prometheus.CounterVec
|
||||
CounterRecordEvalErrorTotal *prometheus.CounterVec
|
||||
CounterMuteTotal *prometheus.CounterVec
|
||||
CounterRuleEvalErrorTotal *prometheus.CounterVec
|
||||
CounterHeartbeatErrorTotal *prometheus.CounterVec
|
||||
CounterSubEventTotal *prometheus.CounterVec
|
||||
}
|
||||
|
||||
func NewSyncStats() *Stats {
|
||||
// 从各个接收接口接收到的监控数据总量
|
||||
CounterSampleTotal := prometheus.NewCounterVec(prometheus.CounterOpts{
|
||||
CounterRuleEval := prometheus.NewCounterVec(prometheus.CounterOpts{
|
||||
Namespace: namespace,
|
||||
Subsystem: subsystem,
|
||||
Name: "samples_received_total",
|
||||
Help: "Total number samples received.",
|
||||
}, []string{"cluster", "channel"})
|
||||
Name: "rule_eval_total",
|
||||
Help: "Number of rule eval.",
|
||||
}, []string{})
|
||||
|
||||
CounterRuleEvalErrorTotal := prometheus.NewCounterVec(prometheus.CounterOpts{
|
||||
Namespace: namespace,
|
||||
Subsystem: subsystem,
|
||||
Name: "rule_eval_error_total",
|
||||
Help: "Number of rule eval error.",
|
||||
}, []string{"datasource", "stage"})
|
||||
|
||||
CounterQueryDataErrorTotal := prometheus.NewCounterVec(prometheus.CounterOpts{
|
||||
Namespace: namespace,
|
||||
Subsystem: subsystem,
|
||||
Name: "query_data_error_total",
|
||||
Help: "Number of rule eval query data error.",
|
||||
}, []string{"datasource"})
|
||||
|
||||
CounterQueryDataTotal := prometheus.NewCounterVec(prometheus.CounterOpts{
|
||||
Namespace: namespace,
|
||||
Subsystem: subsystem,
|
||||
Name: "query_data_total",
|
||||
Help: "Number of rule eval query data.",
|
||||
}, []string{"datasource"})
|
||||
|
||||
CounterRecordEval := prometheus.NewCounterVec(prometheus.CounterOpts{
|
||||
Namespace: namespace,
|
||||
Subsystem: subsystem,
|
||||
Name: "record_eval_total",
|
||||
Help: "Number of record eval.",
|
||||
}, []string{"datasource"})
|
||||
|
||||
CounterRecordEvalErrorTotal := prometheus.NewCounterVec(prometheus.CounterOpts{
|
||||
Namespace: namespace,
|
||||
Subsystem: subsystem,
|
||||
Name: "record_eval_error_total",
|
||||
Help: "Number of record eval error.",
|
||||
}, []string{"datasource"})
|
||||
|
||||
AlertNotifyTotal := prometheus.NewCounterVec(prometheus.CounterOpts{
|
||||
Namespace: namespace,
|
||||
Subsystem: subsystem,
|
||||
Name: "alert_notify_total",
|
||||
Help: "Number of send msg.",
|
||||
}, []string{"channel"})
|
||||
|
||||
AlertNotifyErrorTotal := prometheus.NewCounterVec(prometheus.CounterOpts{
|
||||
Namespace: namespace,
|
||||
Subsystem: subsystem,
|
||||
Name: "alert_notify_error_total",
|
||||
Help: "Number of send msg.",
|
||||
}, []string{"channel"})
|
||||
|
||||
// 产生的告警总量
|
||||
CounterAlertsTotal := prometheus.NewCounterVec(prometheus.CounterOpts{
|
||||
@@ -33,7 +88,7 @@ func NewSyncStats() *Stats {
|
||||
Subsystem: subsystem,
|
||||
Name: "alerts_total",
|
||||
Help: "Total number alert events.",
|
||||
}, []string{"cluster"})
|
||||
}, []string{"cluster", "type", "busi_group"})
|
||||
|
||||
// 内存中的告警事件队列的长度
|
||||
GaugeAlertQueueSize := prometheus.NewGauge(prometheus.GaugeOpts{
|
||||
@@ -43,51 +98,56 @@ func NewSyncStats() *Stats {
|
||||
Help: "The size of alert queue.",
|
||||
})
|
||||
|
||||
// 数据转发队列,各个队列的长度
|
||||
GaugeSampleQueueSize := prometheus.NewGaugeVec(prometheus.GaugeOpts{
|
||||
CounterMuteTotal := prometheus.NewCounterVec(prometheus.CounterOpts{
|
||||
Namespace: namespace,
|
||||
Subsystem: subsystem,
|
||||
Name: "sample_queue_size",
|
||||
Help: "The size of sample queue.",
|
||||
}, []string{"cluster", "channel_number"})
|
||||
Name: "mute_total",
|
||||
Help: "Number of mute.",
|
||||
}, []string{"group"})
|
||||
|
||||
// 一些重要的请求,比如接收数据的请求,应该统计一下延迟情况
|
||||
RequestDuration := prometheus.NewHistogramVec(
|
||||
prometheus.HistogramOpts{
|
||||
Namespace: namespace,
|
||||
Subsystem: subsystem,
|
||||
Buckets: []float64{.01, .1, 1},
|
||||
Name: "http_request_duration_seconds",
|
||||
Help: "HTTP request latencies in seconds.",
|
||||
}, []string{"code", "path", "method"},
|
||||
)
|
||||
CounterSubEventTotal := prometheus.NewCounterVec(prometheus.CounterOpts{
|
||||
Namespace: namespace,
|
||||
Subsystem: subsystem,
|
||||
Name: "sub_event_total",
|
||||
Help: "Number of sub event.",
|
||||
}, []string{"group"})
|
||||
|
||||
// 发往后端TSDB,延迟如何
|
||||
ForwardDuration := prometheus.NewHistogramVec(
|
||||
prometheus.HistogramOpts{
|
||||
Namespace: namespace,
|
||||
Subsystem: subsystem,
|
||||
Buckets: []float64{.1, 1, 10},
|
||||
Name: "forward_duration_seconds",
|
||||
Help: "Forward samples to TSDB. latencies in seconds.",
|
||||
}, []string{"cluster", "channel_number"},
|
||||
)
|
||||
CounterHeartbeatErrorTotal := prometheus.NewCounterVec(prometheus.CounterOpts{
|
||||
Namespace: namespace,
|
||||
Subsystem: subsystem,
|
||||
Name: "heartbeat_error_count",
|
||||
Help: "Number of heartbeat error.",
|
||||
}, []string{})
|
||||
|
||||
prometheus.MustRegister(
|
||||
CounterSampleTotal,
|
||||
CounterAlertsTotal,
|
||||
GaugeAlertQueueSize,
|
||||
GaugeSampleQueueSize,
|
||||
RequestDuration,
|
||||
ForwardDuration,
|
||||
AlertNotifyTotal,
|
||||
AlertNotifyErrorTotal,
|
||||
CounterRuleEval,
|
||||
CounterQueryDataTotal,
|
||||
CounterQueryDataErrorTotal,
|
||||
CounterRecordEval,
|
||||
CounterRecordEvalErrorTotal,
|
||||
CounterMuteTotal,
|
||||
CounterRuleEvalErrorTotal,
|
||||
CounterHeartbeatErrorTotal,
|
||||
CounterSubEventTotal,
|
||||
)
|
||||
|
||||
return &Stats{
|
||||
CounterSampleTotal: CounterSampleTotal,
|
||||
CounterAlertsTotal: CounterAlertsTotal,
|
||||
GaugeAlertQueueSize: GaugeAlertQueueSize,
|
||||
GaugeSampleQueueSize: GaugeSampleQueueSize,
|
||||
RequestDuration: RequestDuration,
|
||||
ForwardDuration: ForwardDuration,
|
||||
CounterAlertsTotal: CounterAlertsTotal,
|
||||
GaugeAlertQueueSize: GaugeAlertQueueSize,
|
||||
AlertNotifyTotal: AlertNotifyTotal,
|
||||
AlertNotifyErrorTotal: AlertNotifyErrorTotal,
|
||||
CounterRuleEval: CounterRuleEval,
|
||||
CounterQueryDataTotal: CounterQueryDataTotal,
|
||||
CounterQueryDataErrorTotal: CounterQueryDataErrorTotal,
|
||||
CounterRecordEval: CounterRecordEval,
|
||||
CounterRecordEvalErrorTotal: CounterRecordEvalErrorTotal,
|
||||
CounterMuteTotal: CounterMuteTotal,
|
||||
CounterRuleEvalErrorTotal: CounterRuleEvalErrorTotal,
|
||||
CounterHeartbeatErrorTotal: CounterHeartbeatErrorTotal,
|
||||
CounterSubEventTotal: CounterSubEventTotal,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,8 @@ type AnomalyPoint struct {
|
||||
Value float64 `json:"value"`
|
||||
Severity int `json:"severity"`
|
||||
Triggered bool `json:"triggered"`
|
||||
Query string `json:"query"`
|
||||
Values string `json:"values"`
|
||||
}
|
||||
|
||||
func NewAnomalyPoint(key string, labels map[string]string, ts int64, value float64, severity int) AnomalyPoint {
|
||||
|
||||
@@ -22,6 +22,14 @@ func MatchTags(eventTagsMap map[string]string, itags []models.TagFilter) bool {
|
||||
}
|
||||
return true
|
||||
}
|
||||
func MatchGroupsName(groupName string, groupFilter []models.TagFilter) bool {
|
||||
for _, filter := range groupFilter {
|
||||
if !matchTag(groupName, filter) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func matchTag(value string, filter models.TagFilter) bool {
|
||||
switch filter.Func {
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
package dispatch
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/ccfos/nightingale/v6/alert/aconf"
|
||||
"github.com/ccfos/nightingale/v6/alert/common"
|
||||
"github.com/ccfos/nightingale/v6/alert/queue"
|
||||
"github.com/ccfos/nightingale/v6/models"
|
||||
"github.com/ccfos/nightingale/v6/pkg/ctx"
|
||||
"github.com/ccfos/nightingale/v6/pkg/poster"
|
||||
promsdk "github.com/ccfos/nightingale/v6/pkg/prom"
|
||||
"github.com/ccfos/nightingale/v6/prom"
|
||||
|
||||
"github.com/toolkits/pkg/concurrent/semaphore"
|
||||
"github.com/toolkits/pkg/logger"
|
||||
@@ -18,15 +23,17 @@ type Consumer struct {
|
||||
alerting aconf.Alerting
|
||||
ctx *ctx.Context
|
||||
|
||||
dispatch *Dispatch
|
||||
dispatch *Dispatch
|
||||
promClients *prom.PromClientMap
|
||||
}
|
||||
|
||||
// 创建一个 Consumer 实例
|
||||
func NewConsumer(alerting aconf.Alerting, ctx *ctx.Context, dispatch *Dispatch) *Consumer {
|
||||
func NewConsumer(alerting aconf.Alerting, ctx *ctx.Context, dispatch *Dispatch, promClients *prom.PromClientMap) *Consumer {
|
||||
return &Consumer{
|
||||
alerting: alerting,
|
||||
ctx: ctx,
|
||||
dispatch: dispatch,
|
||||
alerting: alerting,
|
||||
ctx: ctx,
|
||||
dispatch: dispatch,
|
||||
promClients: promClients,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,16 +68,29 @@ func (e *Consumer) consume(events []interface{}, sema *semaphore.Semaphore) {
|
||||
func (e *Consumer) consumeOne(event *models.AlertCurEvent) {
|
||||
LogEvent(event, "consume")
|
||||
|
||||
eventType := "alert"
|
||||
if event.IsRecovered {
|
||||
eventType = "recovery"
|
||||
}
|
||||
|
||||
e.dispatch.Astats.CounterAlertsTotal.WithLabelValues(event.Cluster, eventType, event.GroupName).Inc()
|
||||
|
||||
if err := event.ParseRule("rule_name"); err != nil {
|
||||
logger.Warningf("ruleid:%d failed to parse rule name: %v", event.RuleId, err)
|
||||
event.RuleName = fmt.Sprintf("failed to parse rule name: %v", err)
|
||||
}
|
||||
|
||||
if err := event.ParseRule("rule_note"); err != nil {
|
||||
event.RuleNote = fmt.Sprintf("failed to parse rule note: %v", err)
|
||||
if err := event.ParseRule("annotations"); err != nil {
|
||||
logger.Warningf("ruleid:%d failed to parse annotations: %v", event.RuleId, err)
|
||||
event.Annotations = fmt.Sprintf("failed to parse annotations: %v", err)
|
||||
event.AnnotationsJSON["error"] = event.Annotations
|
||||
}
|
||||
|
||||
if err := event.ParseRule("annotations"); err != nil {
|
||||
event.Annotations = fmt.Sprintf("failed to parse rule note: %v", err)
|
||||
e.queryRecoveryVal(event)
|
||||
|
||||
if err := event.ParseRule("rule_note"); err != nil {
|
||||
logger.Warningf("ruleid:%d failed to parse rule note: %v", event.RuleId, err)
|
||||
event.RuleNote = fmt.Sprintf("failed to parse rule note: %v", err)
|
||||
}
|
||||
|
||||
e.persist(event)
|
||||
@@ -83,11 +103,17 @@ func (e *Consumer) consumeOne(event *models.AlertCurEvent) {
|
||||
}
|
||||
|
||||
func (e *Consumer) persist(event *models.AlertCurEvent) {
|
||||
if event.Status != 0 {
|
||||
return
|
||||
}
|
||||
|
||||
if !e.ctx.IsCenter {
|
||||
event.DB2FE()
|
||||
err := poster.PostByUrls(e.ctx, "/v1/n9e/event-persist", event)
|
||||
var err error
|
||||
event.Id, err = poster.PostByUrlsWithResp[int64](e.ctx, "/v1/n9e/event-persist", event)
|
||||
if err != nil {
|
||||
logger.Errorf("event%+v persist err:%v", event, err)
|
||||
logger.Errorf("event:%+v persist err:%v", event, err)
|
||||
e.dispatch.Astats.CounterRuleEvalErrorTotal.WithLabelValues(fmt.Sprintf("%v", event.DatasourceId), "persist_event").Inc()
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -95,5 +121,71 @@ func (e *Consumer) persist(event *models.AlertCurEvent) {
|
||||
err := models.EventPersist(e.ctx, event)
|
||||
if err != nil {
|
||||
logger.Errorf("event%+v persist err:%v", event, err)
|
||||
e.dispatch.Astats.CounterRuleEvalErrorTotal.WithLabelValues(fmt.Sprintf("%v", event.DatasourceId), "persist_event").Inc()
|
||||
}
|
||||
}
|
||||
|
||||
func (e *Consumer) queryRecoveryVal(event *models.AlertCurEvent) {
|
||||
if !event.IsRecovered {
|
||||
return
|
||||
}
|
||||
|
||||
// If the event is a recovery event, execute the recovery_promql query
|
||||
promql, ok := event.AnnotationsJSON["recovery_promql"]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
promql = strings.TrimSpace(promql)
|
||||
if promql == "" {
|
||||
logger.Warningf("rule_eval:%s promql is blank", getKey(event))
|
||||
return
|
||||
}
|
||||
|
||||
if e.promClients.IsNil(event.DatasourceId) {
|
||||
logger.Warningf("rule_eval:%s error reader client is nil", getKey(event))
|
||||
return
|
||||
}
|
||||
|
||||
readerClient := e.promClients.GetCli(event.DatasourceId)
|
||||
|
||||
var warnings promsdk.Warnings
|
||||
value, warnings, err := readerClient.Query(e.ctx.Ctx, promql, time.Now())
|
||||
if err != nil {
|
||||
logger.Errorf("rule_eval:%s promql:%s, error:%v", getKey(event), promql, err)
|
||||
event.AnnotationsJSON["recovery_promql_error"] = fmt.Sprintf("promql:%s error:%v", promql, err)
|
||||
|
||||
b, err := json.Marshal(event.AnnotationsJSON)
|
||||
if err != nil {
|
||||
event.AnnotationsJSON = make(map[string]string)
|
||||
event.AnnotationsJSON["error"] = fmt.Sprintf("failed to parse annotations: %v", err)
|
||||
} else {
|
||||
event.Annotations = string(b)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if len(warnings) > 0 {
|
||||
logger.Errorf("rule_eval:%s promql:%s, warnings:%v", getKey(event), promql, warnings)
|
||||
}
|
||||
|
||||
anomalyPoints := common.ConvertAnomalyPoints(value)
|
||||
if len(anomalyPoints) == 0 {
|
||||
logger.Warningf("rule_eval:%s promql:%s, result is empty", getKey(event), promql)
|
||||
event.AnnotationsJSON["recovery_promql_error"] = fmt.Sprintf("promql:%s error:%s", promql, "result is empty")
|
||||
} else {
|
||||
event.AnnotationsJSON["recovery_value"] = fmt.Sprintf("%v", anomalyPoints[0].Value)
|
||||
}
|
||||
|
||||
b, err := json.Marshal(event.AnnotationsJSON)
|
||||
if err != nil {
|
||||
event.AnnotationsJSON = make(map[string]string)
|
||||
event.AnnotationsJSON["error"] = fmt.Sprintf("failed to parse annotations: %v", err)
|
||||
} else {
|
||||
event.Annotations = string(b)
|
||||
}
|
||||
}
|
||||
|
||||
func getKey(event *models.AlertCurEvent) string {
|
||||
return common.RuleKey(event.DatasourceId, event.RuleId)
|
||||
}
|
||||
|
||||
@@ -4,11 +4,14 @@ import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"html/template"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/ccfos/nightingale/v6/alert/aconf"
|
||||
"github.com/ccfos/nightingale/v6/alert/astats"
|
||||
"github.com/ccfos/nightingale/v6/alert/common"
|
||||
"github.com/ccfos/nightingale/v6/alert/sender"
|
||||
"github.com/ccfos/nightingale/v6/memsto"
|
||||
@@ -25,14 +28,18 @@ type Dispatch struct {
|
||||
alertSubscribeCache *memsto.AlertSubscribeCacheType
|
||||
targetCache *memsto.TargetCacheType
|
||||
notifyConfigCache *memsto.NotifyConfigCacheType
|
||||
taskTplsCache *memsto.TaskTplCache
|
||||
|
||||
alerting aconf.Alerting
|
||||
|
||||
senders map[string]sender.Sender
|
||||
tpls map[string]*template.Template
|
||||
ExtraSenders map[string]sender.Sender
|
||||
Senders map[string]sender.Sender
|
||||
CallBacks map[string]sender.CallBacker
|
||||
tpls map[string]*template.Template
|
||||
ExtraSenders map[string]sender.Sender
|
||||
BeforeSenderHook func(*models.AlertCurEvent) bool
|
||||
|
||||
ctx *ctx.Context
|
||||
ctx *ctx.Context
|
||||
Astats *astats.Stats
|
||||
|
||||
RwLock sync.RWMutex
|
||||
}
|
||||
@@ -40,7 +47,7 @@ type Dispatch struct {
|
||||
// 创建一个 Notify 实例
|
||||
func NewDispatch(alertRuleCache *memsto.AlertRuleCacheType, userCache *memsto.UserCacheType, userGroupCache *memsto.UserGroupCacheType,
|
||||
alertSubscribeCache *memsto.AlertSubscribeCacheType, targetCache *memsto.TargetCacheType, notifyConfigCache *memsto.NotifyConfigCacheType,
|
||||
alerting aconf.Alerting, ctx *ctx.Context) *Dispatch {
|
||||
taskTplsCache *memsto.TaskTplCache, alerting aconf.Alerting, ctx *ctx.Context, astats *astats.Stats) *Dispatch {
|
||||
notify := &Dispatch{
|
||||
alertRuleCache: alertRuleCache,
|
||||
userCache: userCache,
|
||||
@@ -48,14 +55,17 @@ func NewDispatch(alertRuleCache *memsto.AlertRuleCacheType, userCache *memsto.Us
|
||||
alertSubscribeCache: alertSubscribeCache,
|
||||
targetCache: targetCache,
|
||||
notifyConfigCache: notifyConfigCache,
|
||||
taskTplsCache: taskTplsCache,
|
||||
|
||||
alerting: alerting,
|
||||
|
||||
senders: make(map[string]sender.Sender),
|
||||
tpls: make(map[string]*template.Template),
|
||||
ExtraSenders: make(map[string]sender.Sender),
|
||||
Senders: make(map[string]sender.Sender),
|
||||
tpls: make(map[string]*template.Template),
|
||||
ExtraSenders: make(map[string]sender.Sender),
|
||||
BeforeSenderHook: func(*models.AlertCurEvent) bool { return true },
|
||||
|
||||
ctx: ctx,
|
||||
ctx: ctx,
|
||||
Astats: astats,
|
||||
}
|
||||
return notify
|
||||
}
|
||||
@@ -63,7 +73,7 @@ func NewDispatch(alertRuleCache *memsto.AlertRuleCacheType, userCache *memsto.Us
|
||||
func (e *Dispatch) ReloadTpls() error {
|
||||
err := e.relaodTpls()
|
||||
if err != nil {
|
||||
logger.Error("failed to reload tpls: %v", err)
|
||||
logger.Errorf("failed to reload tpls: %v", err)
|
||||
}
|
||||
|
||||
duration := time.Duration(9000) * time.Millisecond
|
||||
@@ -83,23 +93,36 @@ func (e *Dispatch) relaodTpls() error {
|
||||
smtp := e.notifyConfigCache.GetSMTP()
|
||||
|
||||
senders := map[string]sender.Sender{
|
||||
models.Email: sender.NewSender(models.Email, tmpTpls, smtp),
|
||||
models.Dingtalk: sender.NewSender(models.Dingtalk, tmpTpls, smtp),
|
||||
models.Wecom: sender.NewSender(models.Wecom, tmpTpls, smtp),
|
||||
models.Feishu: sender.NewSender(models.Feishu, tmpTpls, smtp),
|
||||
models.Mm: sender.NewSender(models.Mm, tmpTpls, smtp),
|
||||
models.Telegram: sender.NewSender(models.Telegram, tmpTpls, smtp),
|
||||
models.Email: sender.NewSender(models.Email, tmpTpls, smtp),
|
||||
models.Dingtalk: sender.NewSender(models.Dingtalk, tmpTpls),
|
||||
models.Wecom: sender.NewSender(models.Wecom, tmpTpls),
|
||||
models.Feishu: sender.NewSender(models.Feishu, tmpTpls),
|
||||
models.Mm: sender.NewSender(models.Mm, tmpTpls),
|
||||
models.Telegram: sender.NewSender(models.Telegram, tmpTpls),
|
||||
models.FeishuCard: sender.NewSender(models.FeishuCard, tmpTpls),
|
||||
}
|
||||
|
||||
// domain -> Callback()
|
||||
callbacks := map[string]sender.CallBacker{
|
||||
models.DingtalkDomain: sender.NewCallBacker(models.DingtalkDomain, e.targetCache, e.userCache, e.taskTplsCache, tmpTpls),
|
||||
models.WecomDomain: sender.NewCallBacker(models.WecomDomain, e.targetCache, e.userCache, e.taskTplsCache, tmpTpls),
|
||||
models.FeishuDomain: sender.NewCallBacker(models.FeishuDomain, e.targetCache, e.userCache, e.taskTplsCache, tmpTpls),
|
||||
models.TelegramDomain: sender.NewCallBacker(models.TelegramDomain, e.targetCache, e.userCache, e.taskTplsCache, tmpTpls),
|
||||
models.FeishuCardDomain: sender.NewCallBacker(models.FeishuCardDomain, e.targetCache, e.userCache, e.taskTplsCache, tmpTpls),
|
||||
models.IbexDomain: sender.NewCallBacker(models.IbexDomain, e.targetCache, e.userCache, e.taskTplsCache, tmpTpls),
|
||||
models.DefaultDomain: sender.NewCallBacker(models.DefaultDomain, e.targetCache, e.userCache, e.taskTplsCache, tmpTpls),
|
||||
}
|
||||
|
||||
e.RwLock.RLock()
|
||||
for channel, sender := range e.ExtraSenders {
|
||||
senders[channel] = sender
|
||||
for channelName, extraSender := range e.ExtraSenders {
|
||||
senders[channelName] = extraSender
|
||||
}
|
||||
e.RwLock.RUnlock()
|
||||
|
||||
e.RwLock.Lock()
|
||||
e.tpls = tmpTpls
|
||||
e.senders = senders
|
||||
e.Senders = senders
|
||||
e.CallBacks = callbacks
|
||||
e.RwLock.Unlock()
|
||||
return nil
|
||||
}
|
||||
@@ -140,7 +163,7 @@ func (e *Dispatch) HandleEventNotify(event *models.AlertCurEvent, isSubscribe bo
|
||||
}
|
||||
|
||||
// 处理事件发送,这里用一个goroutine处理一个event的所有发送事件
|
||||
go e.Send(rule, event, notifyTarget, isSubscribe)
|
||||
go e.Send(rule, event, notifyTarget)
|
||||
|
||||
// 如果是不是订阅规则出现的event, 则需要处理订阅规则的event
|
||||
if !isSubscribe {
|
||||
@@ -167,45 +190,122 @@ func (e *Dispatch) handleSubs(event *models.AlertCurEvent) {
|
||||
|
||||
// handleSub 处理订阅规则的event,注意这里event要使用值传递,因为后面会修改event的状态
|
||||
func (e *Dispatch) handleSub(sub *models.AlertSubscribe, event models.AlertCurEvent) {
|
||||
if sub.IsDisabled() || !sub.MatchCluster(event.DatasourceId) {
|
||||
if sub.IsDisabled() {
|
||||
return
|
||||
}
|
||||
|
||||
if !sub.MatchCluster(event.DatasourceId) {
|
||||
return
|
||||
}
|
||||
|
||||
if !sub.MatchProd(event.RuleProd) {
|
||||
return
|
||||
}
|
||||
|
||||
if !common.MatchTags(event.TagsMap, sub.ITags) {
|
||||
return
|
||||
}
|
||||
// event BusiGroups filter
|
||||
if !common.MatchGroupsName(event.GroupName, sub.IBusiGroups) {
|
||||
return
|
||||
}
|
||||
if sub.ForDuration > (event.TriggerTime - event.FirstTriggerTime) {
|
||||
return
|
||||
}
|
||||
|
||||
if len(sub.SeveritiesJson) != 0 {
|
||||
match := false
|
||||
for _, s := range sub.SeveritiesJson {
|
||||
if s == event.Severity || s == 0 {
|
||||
match = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !match {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
e.Astats.CounterSubEventTotal.WithLabelValues(event.GroupName).Inc()
|
||||
sub.ModifyEvent(&event)
|
||||
event.SubRuleId = sub.Id
|
||||
|
||||
LogEvent(&event, "subscribe")
|
||||
e.HandleEventNotify(&event, true)
|
||||
}
|
||||
|
||||
func (e *Dispatch) Send(rule *models.AlertRule, event *models.AlertCurEvent, notifyTarget *NotifyTarget, isSubscribe bool) {
|
||||
for channel, uids := range notifyTarget.ToChannelUserMap() {
|
||||
ctx := sender.BuildMessageContext(rule, event, uids, e.userCache)
|
||||
e.RwLock.RLock()
|
||||
s := e.senders[channel]
|
||||
e.RwLock.RUnlock()
|
||||
if s == nil {
|
||||
logger.Debugf("no sender for channel: %s", channel)
|
||||
continue
|
||||
func (e *Dispatch) Send(rule *models.AlertRule, event *models.AlertCurEvent, notifyTarget *NotifyTarget) {
|
||||
needSend := e.BeforeSenderHook(event)
|
||||
if needSend {
|
||||
for channel, uids := range notifyTarget.ToChannelUserMap() {
|
||||
msgCtx := sender.BuildMessageContext(rule, []*models.AlertCurEvent{event}, uids, e.userCache, e.Astats)
|
||||
e.RwLock.RLock()
|
||||
s := e.Senders[channel]
|
||||
e.RwLock.RUnlock()
|
||||
if s == nil {
|
||||
logger.Debugf("no sender for channel: %s", channel)
|
||||
continue
|
||||
}
|
||||
|
||||
var event *models.AlertCurEvent
|
||||
if len(msgCtx.Events) > 0 {
|
||||
event = msgCtx.Events[0]
|
||||
}
|
||||
|
||||
logger.Debugf("send to channel:%s event:%+v users:%+v", channel, event, msgCtx.Users)
|
||||
s.Send(msgCtx)
|
||||
}
|
||||
logger.Debugf("send event: %s, channel: %s", event.Hash, channel)
|
||||
for i := 0; i < len(ctx.Users); i++ {
|
||||
logger.Debug("send event to user: ", ctx.Users[i])
|
||||
}
|
||||
s.Send(ctx)
|
||||
}
|
||||
|
||||
// handle event callbacks
|
||||
sender.SendCallbacks(e.ctx, notifyTarget.ToCallbackList(), event, e.targetCache, e.userCache, e.notifyConfigCache.GetIbex())
|
||||
e.SendCallbacks(rule, notifyTarget, event)
|
||||
|
||||
// handle global webhooks
|
||||
sender.SendWebhooks(notifyTarget.ToWebhookList(), event)
|
||||
sender.SendWebhooks(notifyTarget.ToWebhookList(), event, e.Astats)
|
||||
|
||||
// handle plugin call
|
||||
go sender.MayPluginNotify(e.genNoticeBytes(event), e.notifyConfigCache.GetNotifyScript())
|
||||
go sender.MayPluginNotify(e.genNoticeBytes(event), e.notifyConfigCache.GetNotifyScript(), e.Astats)
|
||||
}
|
||||
|
||||
func (e *Dispatch) SendCallbacks(rule *models.AlertRule, notifyTarget *NotifyTarget, event *models.AlertCurEvent) {
|
||||
|
||||
uids := notifyTarget.ToUidList()
|
||||
urls := notifyTarget.ToCallbackList()
|
||||
for _, urlStr := range urls {
|
||||
if len(urlStr) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
cbCtx := sender.BuildCallBackContext(e.ctx, urlStr, rule, []*models.AlertCurEvent{event}, uids, e.userCache, e.Astats)
|
||||
|
||||
if strings.HasPrefix(urlStr, "${ibex}") {
|
||||
e.CallBacks[models.IbexDomain].CallBack(cbCtx)
|
||||
continue
|
||||
}
|
||||
|
||||
if !(strings.HasPrefix(urlStr, "http://") || strings.HasPrefix(urlStr, "https://")) {
|
||||
cbCtx.CallBackURL = "http://" + urlStr
|
||||
}
|
||||
|
||||
parsedURL, err := url.Parse(urlStr)
|
||||
if err != nil {
|
||||
logger.Errorf("SendCallbacks: failed to url.Parse(urlStr=%s): %v", urlStr, err)
|
||||
continue
|
||||
}
|
||||
|
||||
// process feishu card
|
||||
if parsedURL.Host == models.FeishuDomain && parsedURL.Query().Get("card") == "1" {
|
||||
e.CallBacks[models.FeishuCardDomain].CallBack(cbCtx)
|
||||
continue
|
||||
}
|
||||
|
||||
callBacker, ok := e.CallBacks[parsedURL.Host]
|
||||
if ok {
|
||||
callBacker.CallBack(cbCtx)
|
||||
} else {
|
||||
e.CallBacks[models.DefaultDomain].CallBack(cbCtx)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type Notice struct {
|
||||
|
||||
@@ -18,11 +18,12 @@ func LogEvent(event *models.AlertCurEvent, location string, err ...error) {
|
||||
}
|
||||
|
||||
logger.Infof(
|
||||
"event(%s %s) %s: rule_id=%d cluster:%s %v%s@%d %s",
|
||||
"event(%s %s) %s: rule_id=%d sub_id:%d cluster:%s %v%s@%d %s",
|
||||
event.Hash,
|
||||
status,
|
||||
location,
|
||||
event.RuleId,
|
||||
event.SubRuleId,
|
||||
event.Cluster,
|
||||
event.TagsJSON,
|
||||
event.TriggerValue,
|
||||
|
||||
@@ -84,6 +84,14 @@ func (s *NotifyTarget) ToWebhookList() []*models.Webhook {
|
||||
return webhooks
|
||||
}
|
||||
|
||||
func (s *NotifyTarget) ToUidList() []int64 {
|
||||
uids := make([]int64, len(s.userMap))
|
||||
for uid, _ := range s.userMap {
|
||||
uids = append(uids, uid)
|
||||
}
|
||||
return uids
|
||||
}
|
||||
|
||||
// Dispatch 抽象由告警事件到信息接收者的路由策略
|
||||
// rule: 告警规则
|
||||
// event: 告警事件
|
||||
|
||||
@@ -3,6 +3,7 @@ package eval
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/ccfos/nightingale/v6/alert/aconf"
|
||||
@@ -12,6 +13,8 @@ import (
|
||||
"github.com/ccfos/nightingale/v6/memsto"
|
||||
"github.com/ccfos/nightingale/v6/pkg/ctx"
|
||||
"github.com/ccfos/nightingale/v6/prom"
|
||||
"github.com/ccfos/nightingale/v6/tdengine"
|
||||
|
||||
"github.com/toolkits/pkg/logger"
|
||||
)
|
||||
|
||||
@@ -23,13 +26,15 @@ type Scheduler struct {
|
||||
|
||||
aconf aconf.Alert
|
||||
|
||||
alertRuleCache *memsto.AlertRuleCacheType
|
||||
targetCache *memsto.TargetCacheType
|
||||
busiGroupCache *memsto.BusiGroupCacheType
|
||||
alertMuteCache *memsto.AlertMuteCacheType
|
||||
datasourceCache *memsto.DatasourceCacheType
|
||||
alertRuleCache *memsto.AlertRuleCacheType
|
||||
targetCache *memsto.TargetCacheType
|
||||
targetsOfAlertRuleCache *memsto.TargetsOfAlertRuleCacheType
|
||||
busiGroupCache *memsto.BusiGroupCacheType
|
||||
alertMuteCache *memsto.AlertMuteCacheType
|
||||
datasourceCache *memsto.DatasourceCacheType
|
||||
|
||||
promClients *prom.PromClientMap
|
||||
promClients *prom.PromClientMap
|
||||
tdengineClients *tdengine.TdengineClientMap
|
||||
|
||||
naming *naming.Naming
|
||||
|
||||
@@ -37,23 +42,26 @@ type Scheduler struct {
|
||||
stats *astats.Stats
|
||||
}
|
||||
|
||||
func NewScheduler(aconf aconf.Alert, externalProcessors *process.ExternalProcessorsType, arc *memsto.AlertRuleCacheType, targetCache *memsto.TargetCacheType,
|
||||
busiGroupCache *memsto.BusiGroupCacheType, alertMuteCache *memsto.AlertMuteCacheType, datasourceCache *memsto.DatasourceCacheType, promClients *prom.PromClientMap, naming *naming.Naming,
|
||||
ctx *ctx.Context, stats *astats.Stats) *Scheduler {
|
||||
func NewScheduler(aconf aconf.Alert, externalProcessors *process.ExternalProcessorsType, arc *memsto.AlertRuleCacheType,
|
||||
targetCache *memsto.TargetCacheType, toarc *memsto.TargetsOfAlertRuleCacheType,
|
||||
busiGroupCache *memsto.BusiGroupCacheType, alertMuteCache *memsto.AlertMuteCacheType, datasourceCache *memsto.DatasourceCacheType,
|
||||
promClients *prom.PromClientMap, tdengineClients *tdengine.TdengineClientMap, naming *naming.Naming, ctx *ctx.Context, stats *astats.Stats) *Scheduler {
|
||||
scheduler := &Scheduler{
|
||||
aconf: aconf,
|
||||
alertRules: make(map[string]*AlertRuleWorker),
|
||||
|
||||
ExternalProcessors: externalProcessors,
|
||||
|
||||
alertRuleCache: arc,
|
||||
targetCache: targetCache,
|
||||
busiGroupCache: busiGroupCache,
|
||||
alertMuteCache: alertMuteCache,
|
||||
datasourceCache: datasourceCache,
|
||||
alertRuleCache: arc,
|
||||
targetCache: targetCache,
|
||||
targetsOfAlertRuleCache: toarc,
|
||||
busiGroupCache: busiGroupCache,
|
||||
alertMuteCache: alertMuteCache,
|
||||
datasourceCache: datasourceCache,
|
||||
|
||||
promClients: promClients,
|
||||
naming: naming,
|
||||
promClients: promClients,
|
||||
tdengineClients: tdengineClients,
|
||||
naming: naming,
|
||||
|
||||
ctx: ctx,
|
||||
stats: stats,
|
||||
@@ -85,10 +93,13 @@ func (s *Scheduler) syncAlertRules() {
|
||||
if rule == nil {
|
||||
continue
|
||||
}
|
||||
if rule.IsPrometheusRule() {
|
||||
|
||||
ruleType := rule.GetRuleType()
|
||||
if rule.IsPrometheusRule() || rule.IsLokiRule() || rule.IsTdengineRule() {
|
||||
datasourceIds := s.promClients.Hit(rule.DatasourceIdsJson)
|
||||
datasourceIds = append(datasourceIds, s.tdengineClients.Hit(rule.DatasourceIdsJson)...)
|
||||
for _, dsId := range datasourceIds {
|
||||
if !naming.DatasourceHashRing.IsHit(dsId, fmt.Sprintf("%d", rule.Id), s.aconf.Heartbeat.Endpoint) {
|
||||
if !naming.DatasourceHashRing.IsHit(strconv.FormatInt(dsId, 10), fmt.Sprintf("%d", rule.Id), s.aconf.Heartbeat.Endpoint) {
|
||||
continue
|
||||
}
|
||||
ds := s.datasourceCache.GetById(dsId)
|
||||
@@ -97,22 +108,27 @@ func (s *Scheduler) syncAlertRules() {
|
||||
continue
|
||||
}
|
||||
|
||||
if ds.PluginType != ruleType {
|
||||
logger.Debugf("datasource %d category is %s not %s", dsId, ds.PluginType, ruleType)
|
||||
continue
|
||||
}
|
||||
|
||||
if ds.Status != "enabled" {
|
||||
logger.Debugf("datasource %d status is %s", dsId, ds.Status)
|
||||
continue
|
||||
}
|
||||
processor := process.NewProcessor(rule, dsId, s.alertRuleCache, s.targetCache, s.busiGroupCache, s.alertMuteCache, s.datasourceCache, s.promClients, s.ctx, s.stats)
|
||||
processor := process.NewProcessor(s.aconf.Heartbeat.EngineName, rule, dsId, s.alertRuleCache, s.targetCache, s.targetsOfAlertRuleCache, s.busiGroupCache, s.alertMuteCache, s.datasourceCache, s.ctx, s.stats)
|
||||
|
||||
alertRule := NewAlertRuleWorker(rule, dsId, processor, s.promClients, s.ctx)
|
||||
alertRule := NewAlertRuleWorker(rule, dsId, processor, s.promClients, s.tdengineClients, s.ctx)
|
||||
alertRuleWorkers[alertRule.Hash()] = alertRule
|
||||
}
|
||||
} else if rule.IsHostRule() && s.ctx.IsCenter {
|
||||
} else if rule.IsHostRule() {
|
||||
// all host rule will be processed by center instance
|
||||
if !naming.DatasourceHashRing.IsHit(naming.HostDatasource, fmt.Sprintf("%d", rule.Id), s.aconf.Heartbeat.Endpoint) {
|
||||
if !naming.DatasourceHashRing.IsHit(s.aconf.Heartbeat.EngineName, strconv.FormatInt(rule.Id, 10), s.aconf.Heartbeat.Endpoint) {
|
||||
continue
|
||||
}
|
||||
processor := process.NewProcessor(rule, 0, s.alertRuleCache, s.targetCache, s.busiGroupCache, s.alertMuteCache, s.datasourceCache, s.promClients, s.ctx, s.stats)
|
||||
alertRule := NewAlertRuleWorker(rule, 0, processor, s.promClients, s.ctx)
|
||||
processor := process.NewProcessor(s.aconf.Heartbeat.EngineName, rule, 0, s.alertRuleCache, s.targetCache, s.targetsOfAlertRuleCache, s.busiGroupCache, s.alertMuteCache, s.datasourceCache, s.ctx, s.stats)
|
||||
alertRule := NewAlertRuleWorker(rule, 0, processor, s.promClients, s.tdengineClients, s.ctx)
|
||||
alertRuleWorkers[alertRule.Hash()] = alertRule
|
||||
} else {
|
||||
// 如果 rule 不是通过 prometheus engine 来告警的,则创建为 externalRule
|
||||
@@ -128,7 +144,7 @@ func (s *Scheduler) syncAlertRules() {
|
||||
logger.Debugf("datasource %d status is %s", dsId, ds.Status)
|
||||
continue
|
||||
}
|
||||
processor := process.NewProcessor(rule, dsId, s.alertRuleCache, s.targetCache, s.busiGroupCache, s.alertMuteCache, s.datasourceCache, s.promClients, s.ctx, s.stats)
|
||||
processor := process.NewProcessor(s.aconf.Heartbeat.EngineName, rule, dsId, s.alertRuleCache, s.targetCache, s.targetsOfAlertRuleCache, s.busiGroupCache, s.alertMuteCache, s.datasourceCache, s.ctx, s.stats)
|
||||
externalRuleWorkers[processor.Key()] = processor
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -11,8 +13,11 @@ import (
|
||||
"github.com/ccfos/nightingale/v6/alert/process"
|
||||
"github.com/ccfos/nightingale/v6/models"
|
||||
"github.com/ccfos/nightingale/v6/pkg/ctx"
|
||||
"github.com/ccfos/nightingale/v6/pkg/hash"
|
||||
"github.com/ccfos/nightingale/v6/pkg/parser"
|
||||
promsdk "github.com/ccfos/nightingale/v6/pkg/prom"
|
||||
"github.com/ccfos/nightingale/v6/prom"
|
||||
"github.com/ccfos/nightingale/v6/tdengine"
|
||||
|
||||
"github.com/toolkits/pkg/logger"
|
||||
"github.com/toolkits/pkg/str"
|
||||
@@ -28,19 +33,29 @@ type AlertRuleWorker struct {
|
||||
|
||||
processor *process.Processor
|
||||
|
||||
promClients *prom.PromClientMap
|
||||
ctx *ctx.Context
|
||||
promClients *prom.PromClientMap
|
||||
tdengineClients *tdengine.TdengineClientMap
|
||||
ctx *ctx.Context
|
||||
}
|
||||
|
||||
func NewAlertRuleWorker(rule *models.AlertRule, datasourceId int64, processor *process.Processor, promClients *prom.PromClientMap, ctx *ctx.Context) *AlertRuleWorker {
|
||||
const (
|
||||
GET_RULE_CONFIG = "get_rule_config"
|
||||
GET_PROCESSOR = "get_processor"
|
||||
CHECK_QUERY = "check_query_config"
|
||||
GET_CLIENT = "get_client"
|
||||
QUERY_DATA = "query_data"
|
||||
)
|
||||
|
||||
func NewAlertRuleWorker(rule *models.AlertRule, datasourceId int64, processor *process.Processor, promClients *prom.PromClientMap, tdengineClients *tdengine.TdengineClientMap, ctx *ctx.Context) *AlertRuleWorker {
|
||||
arw := &AlertRuleWorker{
|
||||
datasourceId: datasourceId,
|
||||
quit: make(chan struct{}),
|
||||
rule: rule,
|
||||
processor: processor,
|
||||
|
||||
promClients: promClients,
|
||||
ctx: ctx,
|
||||
promClients: promClients,
|
||||
tdengineClients: tdengineClients,
|
||||
ctx: ctx,
|
||||
}
|
||||
|
||||
return arw
|
||||
@@ -69,14 +84,16 @@ func (arw *AlertRuleWorker) Start() {
|
||||
if interval <= 0 {
|
||||
interval = 10
|
||||
}
|
||||
|
||||
ticker := time.NewTicker(time.Duration(interval) * time.Second)
|
||||
go func() {
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-arw.quit:
|
||||
return
|
||||
default:
|
||||
case <-ticker.C:
|
||||
arw.Eval()
|
||||
time.Sleep(time.Duration(interval) * time.Second)
|
||||
}
|
||||
}
|
||||
}()
|
||||
@@ -85,17 +102,23 @@ func (arw *AlertRuleWorker) Start() {
|
||||
func (arw *AlertRuleWorker) Eval() {
|
||||
cachedRule := arw.rule
|
||||
if cachedRule == nil {
|
||||
//logger.Errorf("rule_eval:%s rule not found", arw.Key())
|
||||
// logger.Errorf("rule_eval:%s rule not found", arw.Key())
|
||||
return
|
||||
}
|
||||
arw.processor.Stats.CounterRuleEval.WithLabelValues().Inc()
|
||||
|
||||
typ := cachedRule.GetRuleType()
|
||||
var lst []common.AnomalyPoint
|
||||
var anomalyPoints []common.AnomalyPoint
|
||||
var recoverPoints []common.AnomalyPoint
|
||||
switch typ {
|
||||
case models.PROMETHEUS:
|
||||
lst = arw.GetPromAnomalyPoint(cachedRule.RuleConfig)
|
||||
anomalyPoints = arw.GetPromAnomalyPoint(cachedRule.RuleConfig)
|
||||
case models.HOST:
|
||||
lst = arw.GetHostAnomalyPoint(cachedRule.RuleConfig)
|
||||
anomalyPoints = arw.GetHostAnomalyPoint(cachedRule.RuleConfig)
|
||||
case models.TDENGINE:
|
||||
anomalyPoints, recoverPoints = arw.GetTdengineAnomalyPoint(cachedRule, arw.processor.DatasourceId())
|
||||
case models.LOKI:
|
||||
anomalyPoints = arw.GetPromAnomalyPoint(cachedRule.RuleConfig)
|
||||
default:
|
||||
return
|
||||
}
|
||||
@@ -105,7 +128,40 @@ func (arw *AlertRuleWorker) Eval() {
|
||||
return
|
||||
}
|
||||
|
||||
arw.processor.Handle(lst, "inner", arw.inhibit)
|
||||
if arw.inhibit {
|
||||
pointsMap := make(map[string]common.AnomalyPoint)
|
||||
for _, point := range recoverPoints {
|
||||
// 对于恢复的事件,合并处理
|
||||
tagHash := process.TagHash(point)
|
||||
|
||||
p, exists := pointsMap[tagHash]
|
||||
if !exists {
|
||||
pointsMap[tagHash] = point
|
||||
continue
|
||||
}
|
||||
|
||||
if p.Severity > point.Severity {
|
||||
hash := process.Hash(cachedRule.Id, arw.processor.DatasourceId(), p)
|
||||
arw.processor.DeleteProcessEvent(hash)
|
||||
|
||||
pointsMap[tagHash] = point
|
||||
}
|
||||
}
|
||||
|
||||
now := time.Now().Unix()
|
||||
for _, point := range pointsMap {
|
||||
str := fmt.Sprintf("%v", point.Value)
|
||||
arw.processor.RecoverSingle(process.Hash(cachedRule.Id, arw.processor.DatasourceId(), point), now, &str)
|
||||
}
|
||||
} else {
|
||||
now := time.Now().Unix()
|
||||
for _, point := range recoverPoints {
|
||||
str := fmt.Sprintf("%v", point.Value)
|
||||
arw.processor.RecoverSingle(process.Hash(cachedRule.Id, arw.processor.DatasourceId(), point), now, &str)
|
||||
}
|
||||
}
|
||||
|
||||
arw.processor.Handle(anomalyPoints, "inner", arw.inhibit)
|
||||
}
|
||||
|
||||
func (arw *AlertRuleWorker) Stop() {
|
||||
@@ -120,11 +176,13 @@ func (arw *AlertRuleWorker) GetPromAnomalyPoint(ruleConfig string) []common.Anom
|
||||
var rule *models.PromRuleConfig
|
||||
if err := json.Unmarshal([]byte(ruleConfig), &rule); err != nil {
|
||||
logger.Errorf("rule_eval:%s rule_config:%s, error:%v", arw.Key(), ruleConfig, err)
|
||||
arw.processor.Stats.CounterRuleEvalErrorTotal.WithLabelValues(fmt.Sprintf("%v", arw.processor.DatasourceId()), GET_RULE_CONFIG).Inc()
|
||||
return lst
|
||||
}
|
||||
|
||||
if rule == nil {
|
||||
logger.Errorf("rule_eval:%s rule_config:%s, error:rule is nil", arw.Key(), ruleConfig)
|
||||
arw.processor.Stats.CounterRuleEvalErrorTotal.WithLabelValues(fmt.Sprintf("%v", arw.processor.DatasourceId()), GET_RULE_CONFIG).Inc()
|
||||
return lst
|
||||
}
|
||||
|
||||
@@ -136,39 +194,101 @@ func (arw *AlertRuleWorker) GetPromAnomalyPoint(ruleConfig string) []common.Anom
|
||||
|
||||
promql := strings.TrimSpace(query.PromQl)
|
||||
if promql == "" {
|
||||
logger.Errorf("rule_eval:%s promql is blank", arw.Key())
|
||||
logger.Warningf("rule_eval:%s promql is blank", arw.Key())
|
||||
arw.processor.Stats.CounterRuleEvalErrorTotal.WithLabelValues(fmt.Sprintf("%v", arw.processor.DatasourceId()), CHECK_QUERY).Inc()
|
||||
continue
|
||||
}
|
||||
|
||||
if arw.promClients.IsNil(arw.datasourceId) {
|
||||
logger.Errorf("rule_eval:%s error reader client is nil", arw.Key())
|
||||
logger.Warningf("rule_eval:%s error reader client is nil", arw.Key())
|
||||
arw.processor.Stats.CounterRuleEvalErrorTotal.WithLabelValues(fmt.Sprintf("%v", arw.processor.DatasourceId()), GET_CLIENT).Inc()
|
||||
continue
|
||||
}
|
||||
|
||||
readerClient := arw.promClients.GetCli(arw.datasourceId)
|
||||
|
||||
var warnings promsdk.Warnings
|
||||
arw.processor.Stats.CounterQueryDataTotal.WithLabelValues(fmt.Sprintf("%d", arw.datasourceId)).Inc()
|
||||
value, warnings, err := readerClient.Query(context.Background(), promql, time.Now())
|
||||
if err != nil {
|
||||
logger.Errorf("rule_eval:%s promql:%s, error:%v", arw.Key(), promql, err)
|
||||
arw.processor.Stats.CounterQueryDataErrorTotal.WithLabelValues(fmt.Sprintf("%d", arw.datasourceId)).Inc()
|
||||
arw.processor.Stats.CounterRuleEvalErrorTotal.WithLabelValues(fmt.Sprintf("%v", arw.processor.DatasourceId()), QUERY_DATA).Inc()
|
||||
continue
|
||||
}
|
||||
|
||||
if len(warnings) > 0 {
|
||||
logger.Errorf("rule_eval:%s promql:%s, warnings:%v", arw.Key(), promql, warnings)
|
||||
continue
|
||||
arw.processor.Stats.CounterQueryDataErrorTotal.WithLabelValues(fmt.Sprintf("%d", arw.datasourceId)).Inc()
|
||||
arw.processor.Stats.CounterRuleEvalErrorTotal.WithLabelValues(fmt.Sprintf("%v", arw.processor.DatasourceId()), QUERY_DATA).Inc()
|
||||
}
|
||||
|
||||
logger.Debugf("rule_eval:%s query:%+v, value:%v", arw.Key(), query, value)
|
||||
points := common.ConvertAnomalyPoints(value)
|
||||
for i := 0; i < len(points); i++ {
|
||||
points[i].Severity = query.Severity
|
||||
points[i].Query = promql
|
||||
}
|
||||
lst = append(lst, points...)
|
||||
}
|
||||
return lst
|
||||
}
|
||||
|
||||
func (arw *AlertRuleWorker) GetTdengineAnomalyPoint(rule *models.AlertRule, dsId int64) ([]common.AnomalyPoint, []common.AnomalyPoint) {
|
||||
// 获取查询和规则判断条件
|
||||
points := []common.AnomalyPoint{}
|
||||
recoverPoints := []common.AnomalyPoint{}
|
||||
ruleConfig := strings.TrimSpace(rule.RuleConfig)
|
||||
if ruleConfig == "" {
|
||||
logger.Warningf("rule_eval:%d promql is blank", rule.Id)
|
||||
arw.processor.Stats.CounterRuleEvalErrorTotal.WithLabelValues(fmt.Sprintf("%v", arw.processor.DatasourceId()), GET_RULE_CONFIG).Inc()
|
||||
return points, recoverPoints
|
||||
}
|
||||
|
||||
var ruleQuery models.RuleQuery
|
||||
err := json.Unmarshal([]byte(ruleConfig), &ruleQuery)
|
||||
if err != nil {
|
||||
logger.Warningf("rule_eval:%d promql parse error:%s", rule.Id, err.Error())
|
||||
arw.processor.Stats.CounterRuleEvalErrorTotal.WithLabelValues(fmt.Sprintf("%v", arw.processor.DatasourceId())).Inc()
|
||||
arw.processor.Stats.CounterRuleEvalErrorTotal.WithLabelValues(fmt.Sprintf("%v", arw.processor.DatasourceId()), GET_RULE_CONFIG).Inc()
|
||||
return points, recoverPoints
|
||||
}
|
||||
|
||||
arw.inhibit = ruleQuery.Inhibit
|
||||
if len(ruleQuery.Queries) > 0 {
|
||||
seriesStore := make(map[uint64]models.DataResp)
|
||||
seriesTagIndex := make(map[uint64][]uint64)
|
||||
|
||||
for _, query := range ruleQuery.Queries {
|
||||
arw.processor.Stats.CounterQueryDataTotal.WithLabelValues(fmt.Sprintf("%d", arw.datasourceId)).Inc()
|
||||
cli := arw.tdengineClients.GetCli(dsId)
|
||||
if cli == nil {
|
||||
logger.Warningf("rule_eval:%d tdengine client is nil", rule.Id)
|
||||
arw.processor.Stats.CounterQueryDataErrorTotal.WithLabelValues(fmt.Sprintf("%d", arw.datasourceId)).Inc()
|
||||
arw.processor.Stats.CounterRuleEvalErrorTotal.WithLabelValues(fmt.Sprintf("%v", arw.processor.DatasourceId()), GET_CLIENT).Inc()
|
||||
continue
|
||||
}
|
||||
|
||||
series, err := cli.Query(query)
|
||||
arw.processor.Stats.CounterQueryDataTotal.WithLabelValues(fmt.Sprintf("%d", arw.datasourceId)).Inc()
|
||||
if err != nil {
|
||||
logger.Warningf("rule_eval rid:%d query data error: %v", rule.Id, err)
|
||||
arw.processor.Stats.CounterQueryDataErrorTotal.WithLabelValues(fmt.Sprintf("%d", arw.datasourceId)).Inc()
|
||||
arw.processor.Stats.CounterRuleEvalErrorTotal.WithLabelValues(fmt.Sprintf("%v", arw.processor.DatasourceId()), QUERY_DATA).Inc()
|
||||
continue
|
||||
}
|
||||
|
||||
// 此条日志很重要,是告警判断的现场值
|
||||
logger.Debugf("rule_eval rid:%d req:%+v resp:%+v", rule.Id, query, series)
|
||||
MakeSeriesMap(series, seriesTagIndex, seriesStore)
|
||||
}
|
||||
|
||||
points, recoverPoints = GetAnomalyPoint(rule.Id, ruleQuery, seriesTagIndex, seriesStore)
|
||||
}
|
||||
|
||||
return points, recoverPoints
|
||||
}
|
||||
|
||||
func (arw *AlertRuleWorker) GetHostAnomalyPoint(ruleConfig string) []common.AnomalyPoint {
|
||||
var lst []common.AnomalyPoint
|
||||
var severity int
|
||||
@@ -176,11 +296,13 @@ func (arw *AlertRuleWorker) GetHostAnomalyPoint(ruleConfig string) []common.Anom
|
||||
var rule *models.HostRuleConfig
|
||||
if err := json.Unmarshal([]byte(ruleConfig), &rule); err != nil {
|
||||
logger.Errorf("rule_eval:%s rule_config:%s, error:%v", arw.Key(), ruleConfig, err)
|
||||
arw.processor.Stats.CounterRuleEvalErrorTotal.WithLabelValues(fmt.Sprintf("%v", arw.processor.DatasourceId()), GET_RULE_CONFIG).Inc()
|
||||
return lst
|
||||
}
|
||||
|
||||
if rule == nil {
|
||||
logger.Errorf("rule_eval:%s rule_config:%s, error:rule is nil", arw.Key(), ruleConfig)
|
||||
arw.processor.Stats.CounterRuleEvalErrorTotal.WithLabelValues(fmt.Sprintf("%v", arw.processor.DatasourceId()), GET_RULE_CONFIG).Inc()
|
||||
return lst
|
||||
}
|
||||
|
||||
@@ -191,15 +313,42 @@ func (arw *AlertRuleWorker) GetHostAnomalyPoint(ruleConfig string) []common.Anom
|
||||
arw.severity = trigger.Severity
|
||||
}
|
||||
|
||||
query := models.GetHostsQuery(rule.Queries)
|
||||
switch trigger.Type {
|
||||
case "target_miss":
|
||||
t := now - int64(trigger.Duration)
|
||||
targets, err := models.MissTargetGetsByFilter(arw.ctx, query, t)
|
||||
if err != nil {
|
||||
logger.Errorf("rule_eval:%s query:%v, error:%v", arw.Key(), query, err)
|
||||
|
||||
var idents, engineIdents, missEngineIdents []string
|
||||
var exists bool
|
||||
if arw.ctx.IsCenter {
|
||||
// 如果是中心节点, 将不再上报数据的主机 engineName 为空的机器,也加入到 targets 中
|
||||
missEngineIdents, exists = arw.processor.TargetsOfAlertRuleCache.Get("", arw.rule.Id)
|
||||
if !exists {
|
||||
logger.Debugf("rule_eval:%s targets not found engineName:%s", arw.Key(), arw.processor.EngineName)
|
||||
arw.processor.Stats.CounterRuleEvalErrorTotal.WithLabelValues(fmt.Sprintf("%v", arw.processor.DatasourceId()), QUERY_DATA).Inc()
|
||||
}
|
||||
}
|
||||
idents = append(idents, missEngineIdents...)
|
||||
|
||||
engineIdents, exists = arw.processor.TargetsOfAlertRuleCache.Get(arw.processor.EngineName, arw.rule.Id)
|
||||
if !exists {
|
||||
logger.Warningf("rule_eval:%s targets not found engineName:%s", arw.Key(), arw.processor.EngineName)
|
||||
arw.processor.Stats.CounterRuleEvalErrorTotal.WithLabelValues(fmt.Sprintf("%v", arw.processor.DatasourceId()), QUERY_DATA).Inc()
|
||||
}
|
||||
idents = append(idents, engineIdents...)
|
||||
|
||||
if len(idents) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
var missTargets []string
|
||||
targetUpdateTimeMap := arw.processor.TargetCache.GetHostUpdateTime(idents)
|
||||
for ident, updateTime := range targetUpdateTimeMap {
|
||||
if updateTime < t {
|
||||
missTargets = append(missTargets, ident)
|
||||
}
|
||||
}
|
||||
logger.Debugf("rule_eval:%s missTargets:%v", arw.Key(), missTargets)
|
||||
targets := arw.processor.TargetCache.Gets(missTargets)
|
||||
for _, target := range targets {
|
||||
m := make(map[string]string)
|
||||
target.FillTagsMap()
|
||||
@@ -216,20 +365,43 @@ func (arw *AlertRuleWorker) GetHostAnomalyPoint(ruleConfig string) []common.Anom
|
||||
lst = append(lst, common.NewAnomalyPoint(trigger.Type, m, now, float64(now-target.UpdateAt), trigger.Severity))
|
||||
}
|
||||
case "offset":
|
||||
targets, err := models.TargetGetsByFilter(arw.ctx, query, 0, 0)
|
||||
if err != nil {
|
||||
logger.Errorf("rule_eval:%s query:%v, error:%v", arw.Key(), query, err)
|
||||
idents, exists := arw.processor.TargetsOfAlertRuleCache.Get(arw.processor.EngineName, arw.rule.Id)
|
||||
if !exists {
|
||||
logger.Warningf("rule_eval:%s targets not found", arw.Key())
|
||||
arw.processor.Stats.CounterRuleEvalErrorTotal.WithLabelValues(fmt.Sprintf("%v", arw.processor.DatasourceId()), QUERY_DATA).Inc()
|
||||
continue
|
||||
}
|
||||
var targetMap = make(map[string]*models.Target)
|
||||
|
||||
targets := arw.processor.TargetCache.Gets(idents)
|
||||
targetMap := make(map[string]*models.Target)
|
||||
for _, target := range targets {
|
||||
targetMap[target.Ident] = target
|
||||
}
|
||||
|
||||
hostOffsetMap := arw.processor.TargetCache.GetOffsetHost(targets, now, int64(trigger.Duration))
|
||||
for host, offset := range hostOffsetMap {
|
||||
offsetIdents := make(map[string]int64)
|
||||
targetsMeta := arw.processor.TargetCache.GetHostMetas(targets)
|
||||
for ident, meta := range targetsMeta {
|
||||
if meta.CpuNum <= 0 {
|
||||
// means this target is not collect by categraf, do not check offset
|
||||
continue
|
||||
}
|
||||
if target, exists := targetMap[ident]; exists {
|
||||
if now-target.UpdateAt > 120 {
|
||||
// means this target is not a active host, do not check offset
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
offset := meta.Offset
|
||||
if math.Abs(float64(offset)) > float64(trigger.Duration) {
|
||||
offsetIdents[ident] = offset
|
||||
}
|
||||
}
|
||||
|
||||
logger.Debugf("rule_eval:%s offsetIdents:%v", arw.Key(), offsetIdents)
|
||||
for host, offset := range offsetIdents {
|
||||
m := make(map[string]string)
|
||||
target, exists := targetMap[host]
|
||||
target, exists := arw.processor.TargetCache.Get(host)
|
||||
if exists {
|
||||
target.FillTagsMap()
|
||||
for k, v := range target.TagsMap {
|
||||
@@ -247,18 +419,22 @@ func (arw *AlertRuleWorker) GetHostAnomalyPoint(ruleConfig string) []common.Anom
|
||||
}
|
||||
case "pct_target_miss":
|
||||
t := now - int64(trigger.Duration)
|
||||
count, err := models.MissTargetCountByFilter(arw.ctx, query, t)
|
||||
if err != nil {
|
||||
logger.Errorf("rule_eval:%s query:%v, error:%v", arw.Key(), query, err)
|
||||
idents, exists := arw.processor.TargetsOfAlertRuleCache.Get(arw.processor.EngineName, arw.rule.Id)
|
||||
if !exists {
|
||||
logger.Warningf("rule_eval:%s targets not found", arw.Key())
|
||||
arw.processor.Stats.CounterRuleEvalErrorTotal.WithLabelValues(fmt.Sprintf("%v", arw.processor.DatasourceId()), QUERY_DATA).Inc()
|
||||
continue
|
||||
}
|
||||
|
||||
total, err := models.TargetCountByFilter(arw.ctx, query)
|
||||
if err != nil {
|
||||
logger.Errorf("rule_eval:%s query:%v, error:%v", arw.Key(), query, err)
|
||||
continue
|
||||
var missTargets []string
|
||||
targetUpdateTimeMap := arw.processor.TargetCache.GetHostUpdateTime(idents)
|
||||
for ident, updateTime := range targetUpdateTimeMap {
|
||||
if updateTime < t {
|
||||
missTargets = append(missTargets, ident)
|
||||
}
|
||||
}
|
||||
pct := float64(count) / float64(total) * 100
|
||||
logger.Debugf("rule_eval:%s missTargets:%v", arw.Key(), missTargets)
|
||||
pct := float64(len(missTargets)) / float64(len(idents)) * 100
|
||||
if pct >= float64(trigger.Percent) {
|
||||
lst = append(lst, common.NewAnomalyPoint(trigger.Type, nil, now, pct, trigger.Severity))
|
||||
}
|
||||
@@ -266,3 +442,92 @@ func (arw *AlertRuleWorker) GetHostAnomalyPoint(ruleConfig string) []common.Anom
|
||||
}
|
||||
return lst
|
||||
}
|
||||
|
||||
func GetAnomalyPoint(ruleId int64, ruleQuery models.RuleQuery, seriesTagIndex map[uint64][]uint64, seriesStore map[uint64]models.DataResp) ([]common.AnomalyPoint, []common.AnomalyPoint) {
|
||||
points := []common.AnomalyPoint{}
|
||||
recoverPoints := []common.AnomalyPoint{}
|
||||
|
||||
for _, trigger := range ruleQuery.Triggers {
|
||||
for _, seriesHash := range seriesTagIndex {
|
||||
sort.Slice(seriesHash, func(i, j int) bool {
|
||||
return seriesHash[i] < seriesHash[j]
|
||||
})
|
||||
|
||||
m := make(map[string]float64)
|
||||
var ts int64
|
||||
var sample models.DataResp
|
||||
var value float64
|
||||
for _, serieHash := range seriesHash {
|
||||
series, exists := seriesStore[serieHash]
|
||||
if !exists {
|
||||
logger.Warningf("rule_eval rid:%d series:%+v not found", ruleId, series)
|
||||
continue
|
||||
}
|
||||
t, v, exists := series.Last()
|
||||
if !exists {
|
||||
logger.Warningf("rule_eval rid:%d series:%+v value not found", ruleId, series)
|
||||
continue
|
||||
}
|
||||
|
||||
if !strings.Contains(trigger.Exp, "$"+series.Ref) {
|
||||
// 表达式中不包含该变量
|
||||
continue
|
||||
}
|
||||
|
||||
m["$"+series.Ref] = v
|
||||
m["$"+series.Ref+"."+series.MetricName()] = v
|
||||
ts = int64(t)
|
||||
sample = series
|
||||
value = v
|
||||
}
|
||||
isTriggered := parser.Calc(trigger.Exp, m)
|
||||
// 此条日志很重要,是告警判断的现场值
|
||||
logger.Infof("rule_eval rid:%d trigger:%+v exp:%s res:%v m:%v", ruleId, trigger, trigger.Exp, isTriggered, m)
|
||||
|
||||
var values string
|
||||
for k, v := range m {
|
||||
if !strings.Contains(k, ".") {
|
||||
continue
|
||||
}
|
||||
values += fmt.Sprintf("%s:%v ", k, v)
|
||||
}
|
||||
|
||||
point := common.AnomalyPoint{
|
||||
Key: sample.MetricName(),
|
||||
Labels: sample.Metric,
|
||||
Timestamp: int64(ts),
|
||||
Value: value,
|
||||
Values: values,
|
||||
Severity: trigger.Severity,
|
||||
Triggered: isTriggered,
|
||||
Query: fmt.Sprintf("query:%+v trigger:%+v", ruleQuery.Queries, trigger),
|
||||
}
|
||||
|
||||
if sample.Query != "" {
|
||||
point.Query = sample.Query
|
||||
}
|
||||
|
||||
if isTriggered {
|
||||
points = append(points, point)
|
||||
} else {
|
||||
recoverPoints = append(recoverPoints, point)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return points, recoverPoints
|
||||
}
|
||||
|
||||
func MakeSeriesMap(series []models.DataResp, seriesTagIndex map[uint64][]uint64, seriesStore map[uint64]models.DataResp) {
|
||||
for i := 0; i < len(series); i++ {
|
||||
serieHash := hash.GetHash(series[i].Metric, series[i].Ref)
|
||||
tagHash := hash.GetTagHash(series[i].Metric)
|
||||
seriesStore[serieHash] = series[i]
|
||||
|
||||
// 将曲线按照相同的 tag 分组
|
||||
if _, exists := seriesTagIndex[tagHash]; !exists {
|
||||
seriesTagIndex[tagHash] = make([]uint64, 0)
|
||||
}
|
||||
seriesTagIndex[tagHash] = append(seriesTagIndex[tagHash], serieHash)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,32 +12,33 @@ import (
|
||||
"github.com/toolkits/pkg/logger"
|
||||
)
|
||||
|
||||
func IsMuted(rule *models.AlertRule, event *models.AlertCurEvent, targetCache *memsto.TargetCacheType, alertMuteCache *memsto.AlertMuteCacheType) bool {
|
||||
if TimeNonEffectiveMuteStrategy(rule, event) {
|
||||
return true
|
||||
func IsMuted(rule *models.AlertRule, event *models.AlertCurEvent, targetCache *memsto.TargetCacheType, alertMuteCache *memsto.AlertMuteCacheType) (bool, string) {
|
||||
if rule.Disabled == 1 {
|
||||
return true, "rule disabled"
|
||||
}
|
||||
|
||||
if TimeSpanMuteStrategy(rule, event) {
|
||||
return true, "rule is not effective for period of time"
|
||||
}
|
||||
|
||||
if IdentNotExistsMuteStrategy(rule, event, targetCache) {
|
||||
return true
|
||||
return true, "ident not exists mute"
|
||||
}
|
||||
|
||||
if BgNotMatchMuteStrategy(rule, event, targetCache) {
|
||||
return true
|
||||
return true, "bg not match mute"
|
||||
}
|
||||
|
||||
if EventMuteStrategy(event, alertMuteCache) {
|
||||
return true
|
||||
return true, "match mute rule"
|
||||
}
|
||||
|
||||
return false
|
||||
return false, ""
|
||||
}
|
||||
|
||||
// TimeNonEffectiveMuteStrategy 根据规则配置的告警时间过滤,如果产生的告警不在规则配置的告警时间内,则不告警
|
||||
func TimeNonEffectiveMuteStrategy(rule *models.AlertRule, event *models.AlertCurEvent) bool {
|
||||
if rule.Disabled == 1 {
|
||||
return true
|
||||
}
|
||||
|
||||
// TimeSpanMuteStrategy 根据规则配置的告警生效时间段过滤,如果产生的告警不在规则配置的告警生效时间段内,则不告警,即被mute
|
||||
// 时间范围,左闭右开,默认范围:00:00-24:00
|
||||
func TimeSpanMuteStrategy(rule *models.AlertRule, event *models.AlertCurEvent) bool {
|
||||
tm := time.Unix(event.TriggerTime, 0)
|
||||
triggerTime := tm.Format("15:04")
|
||||
triggerWeek := strconv.Itoa(int(tm.Weekday()))
|
||||
@@ -52,18 +53,33 @@ func TimeNonEffectiveMuteStrategy(rule *models.AlertRule, event *models.AlertCur
|
||||
if !strings.Contains(enableDaysOfWeek[i], triggerWeek) {
|
||||
continue
|
||||
}
|
||||
if enableStime[i] <= enableEtime[i] {
|
||||
if triggerTime < enableStime[i] || triggerTime > enableEtime[i] {
|
||||
continue
|
||||
|
||||
if enableStime[i] < enableEtime[i] {
|
||||
if enableEtime[i] == "23:59" {
|
||||
// 02:00-23:59,这种情况做个特殊处理,相当于左闭右闭区间了
|
||||
if triggerTime < enableStime[i] {
|
||||
// mute, 即没生效
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
// 02:00-04:00 或者 02:00-24:00
|
||||
if triggerTime < enableStime[i] || triggerTime >= enableEtime[i] {
|
||||
// mute, 即没生效
|
||||
continue
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if triggerTime < enableStime[i] && triggerTime > enableEtime[i] {
|
||||
} else if enableStime[i] > enableEtime[i] {
|
||||
// 21:00-09:00
|
||||
if triggerTime < enableStime[i] && triggerTime >= enableEtime[i] {
|
||||
// mute, 即没生效
|
||||
continue
|
||||
}
|
||||
}
|
||||
// 到这里说明当前时刻在告警规则的某组生效时间范围内,直接返回 false
|
||||
|
||||
// 到这里说明当前时刻在告警规则的某组生效时间范围内,即没有 mute,直接返回 false
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -131,7 +147,7 @@ func matchMute(event *models.AlertCurEvent, mute *models.AlertMute, clock ...int
|
||||
}
|
||||
|
||||
// 如果不是全局的,判断 匹配的 datasource id
|
||||
if !(len(mute.DatasourceIdsJson) != 0 && mute.DatasourceIdsJson[0] == 0) && event.DatasourceId != 0 {
|
||||
if len(mute.DatasourceIdsJson) != 0 && mute.DatasourceIdsJson[0] != 0 && event.DatasourceId != 0 {
|
||||
idm := make(map[int64]struct{}, len(mute.DatasourceIdsJson))
|
||||
for i := 0; i < len(mute.DatasourceIdsJson); i++ {
|
||||
idm[mute.DatasourceIdsJson[i]] = struct{}{}
|
||||
@@ -156,13 +172,16 @@ func matchMute(event *models.AlertCurEvent, mute *models.AlertMute, clock ...int
|
||||
|
||||
for i := 0; i < len(mute.PeriodicMutesJson); i++ {
|
||||
if strings.Contains(mute.PeriodicMutesJson[i].EnableDaysOfWeek, triggerWeek) {
|
||||
if mute.PeriodicMutesJson[i].EnableStime <= mute.PeriodicMutesJson[i].EnableEtime {
|
||||
if mute.PeriodicMutesJson[i].EnableStime == mute.PeriodicMutesJson[i].EnableEtime || (mute.PeriodicMutesJson[i].EnableStime == "00:00" && mute.PeriodicMutesJson[i].EnableEtime == "23:59") {
|
||||
matchTime = true
|
||||
break
|
||||
} else if mute.PeriodicMutesJson[i].EnableStime < mute.PeriodicMutesJson[i].EnableEtime {
|
||||
if triggerTime >= mute.PeriodicMutesJson[i].EnableStime && triggerTime < mute.PeriodicMutesJson[i].EnableEtime {
|
||||
matchTime = true
|
||||
break
|
||||
}
|
||||
} else {
|
||||
if triggerTime < mute.PeriodicMutesJson[i].EnableStime || triggerTime >= mute.PeriodicMutesJson[i].EnableEtime {
|
||||
if triggerTime >= mute.PeriodicMutesJson[i].EnableStime || triggerTime < mute.PeriodicMutesJson[i].EnableEtime {
|
||||
matchTime = true
|
||||
break
|
||||
}
|
||||
@@ -174,5 +193,25 @@ func matchMute(event *models.AlertCurEvent, mute *models.AlertMute, clock ...int
|
||||
return false
|
||||
}
|
||||
|
||||
var matchSeverity bool
|
||||
if len(mute.SeveritiesJson) > 0 {
|
||||
for _, s := range mute.SeveritiesJson {
|
||||
if event.Severity == s || s == 0 {
|
||||
matchSeverity = true
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
matchSeverity = true
|
||||
}
|
||||
|
||||
if !matchSeverity {
|
||||
return false
|
||||
}
|
||||
|
||||
if mute.ITags == nil || len(mute.ITags) == 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
return common.MatchTags(event.TagsMap, mute.ITags)
|
||||
}
|
||||
|
||||
@@ -12,12 +12,12 @@ const NodeReplicas = 500
|
||||
|
||||
type DatasourceHashRingType struct {
|
||||
sync.RWMutex
|
||||
Rings map[int64]*consistent.Consistent
|
||||
Rings map[string]*consistent.Consistent
|
||||
}
|
||||
|
||||
// for alert_rule sharding
|
||||
var HostDatasource int64 = 100000
|
||||
var DatasourceHashRing = DatasourceHashRingType{Rings: make(map[int64]*consistent.Consistent)}
|
||||
var HostDatasource int64 = 99999999
|
||||
var DatasourceHashRing = DatasourceHashRingType{Rings: make(map[string]*consistent.Consistent)}
|
||||
|
||||
func NewConsistentHashRing(replicas int32, nodes []string) *consistent.Consistent {
|
||||
ret := consistent.New()
|
||||
@@ -28,7 +28,7 @@ func NewConsistentHashRing(replicas int32, nodes []string) *consistent.Consisten
|
||||
return ret
|
||||
}
|
||||
|
||||
func RebuildConsistentHashRing(datasourceId int64, nodes []string) {
|
||||
func RebuildConsistentHashRing(datasourceId string, nodes []string) {
|
||||
r := consistent.New()
|
||||
r.NumberOfReplicas = NodeReplicas
|
||||
for i := 0; i < len(nodes); i++ {
|
||||
@@ -36,10 +36,10 @@ func RebuildConsistentHashRing(datasourceId int64, nodes []string) {
|
||||
}
|
||||
|
||||
DatasourceHashRing.Set(datasourceId, r)
|
||||
logger.Infof("hash ring %d rebuild %+v", datasourceId, r.Members())
|
||||
logger.Infof("hash ring %s rebuild %+v", datasourceId, r.Members())
|
||||
}
|
||||
|
||||
func (chr *DatasourceHashRingType) GetNode(datasourceId int64, pk string) (string, error) {
|
||||
func (chr *DatasourceHashRingType) GetNode(datasourceId string, pk string) (string, error) {
|
||||
chr.Lock()
|
||||
defer chr.Unlock()
|
||||
_, exists := chr.Rings[datasourceId]
|
||||
@@ -50,21 +50,36 @@ func (chr *DatasourceHashRingType) GetNode(datasourceId int64, pk string) (strin
|
||||
return chr.Rings[datasourceId].Get(pk)
|
||||
}
|
||||
|
||||
func (chr *DatasourceHashRingType) IsHit(datasourceId int64, pk string, currentNode string) bool {
|
||||
func (chr *DatasourceHashRingType) IsHit(datasourceId string, pk string, currentNode string) bool {
|
||||
node, err := chr.GetNode(datasourceId, pk)
|
||||
if err != nil {
|
||||
if errors.Is(err, consistent.ErrEmptyCircle) {
|
||||
logger.Debugf("rule id:%s is not work, datasource id:%d is not assigned to active alert engine", pk, datasourceId)
|
||||
} else {
|
||||
logger.Debugf("rule id:%s is not work, datasource id:%d failed to get node from hashring:%v", pk, datasourceId, err)
|
||||
if !errors.Is(err, consistent.ErrEmptyCircle) {
|
||||
logger.Errorf("rule id:%s is not work, datasource id:%s failed to get node from hashring:%v", pk, datasourceId, err)
|
||||
}
|
||||
return false
|
||||
}
|
||||
return node == currentNode
|
||||
}
|
||||
|
||||
func (chr *DatasourceHashRingType) Set(datasourceId int64, r *consistent.Consistent) {
|
||||
func (chr *DatasourceHashRingType) Set(datasourceId string, r *consistent.Consistent) {
|
||||
chr.Lock()
|
||||
defer chr.Unlock()
|
||||
chr.Rings[datasourceId] = r
|
||||
}
|
||||
|
||||
func (chr *DatasourceHashRingType) Del(datasourceId string) {
|
||||
chr.Lock()
|
||||
defer chr.Unlock()
|
||||
delete(chr.Rings, datasourceId)
|
||||
}
|
||||
|
||||
func (chr *DatasourceHashRingType) Clear(engineName string) {
|
||||
chr.Lock()
|
||||
defer chr.Unlock()
|
||||
for id := range chr.Rings {
|
||||
if id == engineName {
|
||||
continue
|
||||
}
|
||||
delete(chr.Rings, id)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/ccfos/nightingale/v6/alert/aconf"
|
||||
"github.com/ccfos/nightingale/v6/alert/astats"
|
||||
"github.com/ccfos/nightingale/v6/models"
|
||||
"github.com/ccfos/nightingale/v6/pkg/ctx"
|
||||
"github.com/ccfos/nightingale/v6/pkg/poster"
|
||||
@@ -17,12 +18,14 @@ import (
|
||||
type Naming struct {
|
||||
ctx *ctx.Context
|
||||
heartbeatConfig aconf.HeartbeatConfig
|
||||
astats *astats.Stats
|
||||
}
|
||||
|
||||
func NewNaming(ctx *ctx.Context, heartbeat aconf.HeartbeatConfig) *Naming {
|
||||
func NewNaming(ctx *ctx.Context, heartbeat aconf.HeartbeatConfig, alertStats *astats.Stats) *Naming {
|
||||
naming := &Naming{
|
||||
ctx: ctx,
|
||||
heartbeatConfig: heartbeat,
|
||||
astats: alertStats,
|
||||
}
|
||||
naming.Heartbeats()
|
||||
return naming
|
||||
@@ -30,9 +33,11 @@ func NewNaming(ctx *ctx.Context, heartbeat aconf.HeartbeatConfig) *Naming {
|
||||
|
||||
// local servers
|
||||
var localss map[int64]string
|
||||
var localHostServers map[string]string
|
||||
|
||||
func (n *Naming) Heartbeats() error {
|
||||
localss = make(map[int64]string)
|
||||
localHostServers = make(map[string]string)
|
||||
if err := n.heartbeat(); err != nil {
|
||||
fmt.Println("failed to heartbeat:", err)
|
||||
return err
|
||||
@@ -86,20 +91,32 @@ func (n *Naming) heartbeat() error {
|
||||
err := models.AlertingEngineHeartbeatWithCluster(n.ctx, n.heartbeatConfig.Endpoint, n.heartbeatConfig.EngineName, 0)
|
||||
if err != nil {
|
||||
logger.Warningf("heartbeat with cluster %s err:%v", "", err)
|
||||
n.astats.CounterHeartbeatErrorTotal.WithLabelValues().Inc()
|
||||
}
|
||||
} else {
|
||||
for i := 0; i < len(datasourceIds); i++ {
|
||||
err := models.AlertingEngineHeartbeatWithCluster(n.ctx, n.heartbeatConfig.Endpoint, n.heartbeatConfig.EngineName, datasourceIds[i])
|
||||
if err != nil {
|
||||
logger.Warningf("heartbeat with cluster %d err:%v", datasourceIds[i], err)
|
||||
n.astats.CounterHeartbeatErrorTotal.WithLabelValues().Inc()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(datasourceIds) == 0 {
|
||||
DatasourceHashRing.Clear(n.heartbeatConfig.EngineName)
|
||||
for dsId := range localss {
|
||||
delete(localss, dsId)
|
||||
}
|
||||
}
|
||||
|
||||
newDatasource := make(map[int64]struct{})
|
||||
for i := 0; i < len(datasourceIds); i++ {
|
||||
newDatasource[datasourceIds[i]] = struct{}{}
|
||||
servers, err := n.ActiveServers(datasourceIds[i])
|
||||
if err != nil {
|
||||
logger.Warningf("hearbeat %d get active server err:%v", datasourceIds[i], err)
|
||||
n.astats.CounterHeartbeatErrorTotal.WithLabelValues().Inc()
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -111,36 +128,42 @@ func (n *Naming) heartbeat() error {
|
||||
continue
|
||||
}
|
||||
|
||||
RebuildConsistentHashRing(datasourceIds[i], servers)
|
||||
RebuildConsistentHashRing(fmt.Sprintf("%d", datasourceIds[i]), servers)
|
||||
localss[datasourceIds[i]] = newss
|
||||
}
|
||||
|
||||
if n.ctx.IsCenter {
|
||||
// 如果是中心节点,还需要处理 host 类型的告警规则,host 类型告警规则,和数据源无关,想复用下数据源的 hash ring,想用一个虚假的数据源 id 来处理
|
||||
// if is center node, we need to handle host type alerting rules, host type alerting rules are not related to datasource, we want to reuse the hash ring of datasource, we want to use a fake datasource id to handle it
|
||||
err := models.AlertingEngineHeartbeatWithCluster(n.ctx, n.heartbeatConfig.Endpoint, n.heartbeatConfig.EngineName, HostDatasource)
|
||||
if err != nil {
|
||||
logger.Warningf("heartbeat with cluster %s err:%v", "", err)
|
||||
for dsId := range localss {
|
||||
if _, exists := newDatasource[dsId]; !exists {
|
||||
delete(localss, dsId)
|
||||
DatasourceHashRing.Del(fmt.Sprintf("%d", dsId))
|
||||
}
|
||||
|
||||
servers, err := n.ActiveServers(HostDatasource)
|
||||
if err != nil {
|
||||
logger.Warningf("hearbeat %d get active server err:%v", HostDatasource, err)
|
||||
return nil
|
||||
}
|
||||
|
||||
sort.Strings(servers)
|
||||
newss := strings.Join(servers, " ")
|
||||
|
||||
oldss, exists := localss[HostDatasource]
|
||||
if exists && oldss == newss {
|
||||
return nil
|
||||
}
|
||||
|
||||
RebuildConsistentHashRing(HostDatasource, servers)
|
||||
localss[HostDatasource] = newss
|
||||
}
|
||||
|
||||
// host 告警使用的是 hash ring
|
||||
err = models.AlertingEngineHeartbeatWithCluster(n.ctx, n.heartbeatConfig.Endpoint, n.heartbeatConfig.EngineName, HostDatasource)
|
||||
if err != nil {
|
||||
logger.Warningf("heartbeat with cluster %s err:%v", "", err)
|
||||
n.astats.CounterHeartbeatErrorTotal.WithLabelValues().Inc()
|
||||
}
|
||||
|
||||
servers, err := n.ActiveServersByEngineName()
|
||||
if err != nil {
|
||||
logger.Warningf("hearbeat %d get active server err:%v", HostDatasource, err)
|
||||
n.astats.CounterHeartbeatErrorTotal.WithLabelValues().Inc()
|
||||
return nil
|
||||
}
|
||||
|
||||
sort.Strings(servers)
|
||||
newss := strings.Join(servers, " ")
|
||||
|
||||
oldss, exists := localHostServers[n.heartbeatConfig.EngineName]
|
||||
if exists && oldss == newss {
|
||||
return nil
|
||||
}
|
||||
|
||||
RebuildConsistentHashRing(n.heartbeatConfig.EngineName, servers)
|
||||
localHostServers[n.heartbeatConfig.EngineName] = newss
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -157,3 +180,13 @@ func (n *Naming) ActiveServers(datasourceId int64) ([]string, error) {
|
||||
// 30秒内有心跳,就认为是活的
|
||||
return models.AlertingEngineGetsInstances(n.ctx, "datasource_id = ? and clock > ?", datasourceId, time.Now().Unix()-30)
|
||||
}
|
||||
|
||||
func (n *Naming) ActiveServersByEngineName() ([]string, error) {
|
||||
if !n.ctx.IsCenter {
|
||||
lst, err := poster.GetByUrls[[]string](n.ctx, "/v1/n9e/servers-active?engine_name="+n.heartbeatConfig.EngineName)
|
||||
return lst, err
|
||||
}
|
||||
|
||||
// 30秒内有心跳,就认为是活的
|
||||
return models.AlertingEngineGetsInstances(n.ctx, "engine_cluster = ? and clock > ?", n.heartbeatConfig.EngineName, time.Now().Unix()-30)
|
||||
}
|
||||
|
||||
28
alert/naming/leader.go
Normal file
28
alert/naming/leader.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package naming
|
||||
|
||||
import (
|
||||
"sort"
|
||||
|
||||
"github.com/toolkits/pkg/logger"
|
||||
)
|
||||
|
||||
func (n *Naming) IamLeader() bool {
|
||||
if !n.ctx.IsCenter {
|
||||
return false
|
||||
}
|
||||
|
||||
servers, err := n.ActiveServersByEngineName()
|
||||
if err != nil {
|
||||
logger.Errorf("failed to get active servers: %v", err)
|
||||
return false
|
||||
}
|
||||
|
||||
if len(servers) == 0 {
|
||||
logger.Errorf("active servers empty")
|
||||
return false
|
||||
}
|
||||
|
||||
sort.Strings(servers)
|
||||
|
||||
return n.heartbeatConfig.Endpoint == servers[0]
|
||||
}
|
||||
@@ -18,11 +18,12 @@ import (
|
||||
"github.com/ccfos/nightingale/v6/models"
|
||||
"github.com/ccfos/nightingale/v6/pkg/ctx"
|
||||
"github.com/ccfos/nightingale/v6/pkg/tplx"
|
||||
"github.com/ccfos/nightingale/v6/prom"
|
||||
"github.com/toolkits/pkg/logger"
|
||||
"github.com/toolkits/pkg/str"
|
||||
)
|
||||
|
||||
type EventMuteHookFunc func(event *models.AlertCurEvent) bool
|
||||
|
||||
type ExternalProcessorsType struct {
|
||||
ExternalLock sync.RWMutex
|
||||
Processors map[string]*Processor
|
||||
@@ -43,8 +44,11 @@ func (e *ExternalProcessorsType) GetExternalAlertRule(datasourceId, id int64) (*
|
||||
return processor, has
|
||||
}
|
||||
|
||||
type HandleEventFunc func(event *models.AlertCurEvent)
|
||||
|
||||
type Processor struct {
|
||||
datasourceId int64
|
||||
EngineName string
|
||||
|
||||
rule *models.AlertRule
|
||||
fires *AlertCurEventMap
|
||||
@@ -57,15 +61,19 @@ type Processor struct {
|
||||
targetNote string
|
||||
groupName string
|
||||
|
||||
atertRuleCache *memsto.AlertRuleCacheType
|
||||
TargetCache *memsto.TargetCacheType
|
||||
BusiGroupCache *memsto.BusiGroupCacheType
|
||||
alertMuteCache *memsto.AlertMuteCacheType
|
||||
datasourceCache *memsto.DatasourceCacheType
|
||||
alertRuleCache *memsto.AlertRuleCacheType
|
||||
TargetCache *memsto.TargetCacheType
|
||||
TargetsOfAlertRuleCache *memsto.TargetsOfAlertRuleCacheType
|
||||
BusiGroupCache *memsto.BusiGroupCacheType
|
||||
alertMuteCache *memsto.AlertMuteCacheType
|
||||
datasourceCache *memsto.DatasourceCacheType
|
||||
|
||||
promClients *prom.PromClientMap
|
||||
ctx *ctx.Context
|
||||
stats *astats.Stats
|
||||
ctx *ctx.Context
|
||||
Stats *astats.Stats
|
||||
|
||||
HandleFireEventHook HandleEventFunc
|
||||
HandleRecoverEventHook HandleEventFunc
|
||||
EventMuteHook EventMuteHookFunc
|
||||
}
|
||||
|
||||
func (p *Processor) Key() string {
|
||||
@@ -85,23 +93,29 @@ func (p *Processor) Hash() string {
|
||||
))
|
||||
}
|
||||
|
||||
func NewProcessor(rule *models.AlertRule, datasourceId int64, atertRuleCache *memsto.AlertRuleCacheType, targetCache *memsto.TargetCacheType,
|
||||
busiGroupCache *memsto.BusiGroupCacheType, alertMuteCache *memsto.AlertMuteCacheType, datasourceCache *memsto.DatasourceCacheType, promClients *prom.PromClientMap, ctx *ctx.Context,
|
||||
func NewProcessor(engineName string, rule *models.AlertRule, datasourceId int64, alertRuleCache *memsto.AlertRuleCacheType,
|
||||
targetCache *memsto.TargetCacheType, targetsOfAlertRuleCache *memsto.TargetsOfAlertRuleCacheType,
|
||||
busiGroupCache *memsto.BusiGroupCacheType, alertMuteCache *memsto.AlertMuteCacheType, datasourceCache *memsto.DatasourceCacheType, ctx *ctx.Context,
|
||||
stats *astats.Stats) *Processor {
|
||||
|
||||
p := &Processor{
|
||||
EngineName: engineName,
|
||||
datasourceId: datasourceId,
|
||||
rule: rule,
|
||||
|
||||
TargetCache: targetCache,
|
||||
BusiGroupCache: busiGroupCache,
|
||||
alertMuteCache: alertMuteCache,
|
||||
atertRuleCache: atertRuleCache,
|
||||
datasourceCache: datasourceCache,
|
||||
TargetCache: targetCache,
|
||||
TargetsOfAlertRuleCache: targetsOfAlertRuleCache,
|
||||
BusiGroupCache: busiGroupCache,
|
||||
alertMuteCache: alertMuteCache,
|
||||
alertRuleCache: alertRuleCache,
|
||||
datasourceCache: datasourceCache,
|
||||
|
||||
promClients: promClients,
|
||||
ctx: ctx,
|
||||
stats: stats,
|
||||
ctx: ctx,
|
||||
Stats: stats,
|
||||
|
||||
HandleFireEventHook: func(event *models.AlertCurEvent) {},
|
||||
HandleRecoverEventHook: func(event *models.AlertCurEvent) {},
|
||||
EventMuteHook: func(event *models.AlertCurEvent) bool { return false },
|
||||
}
|
||||
|
||||
p.mayHandleGroup()
|
||||
@@ -113,11 +127,13 @@ func (p *Processor) Handle(anomalyPoints []common.AnomalyPoint, from string, inh
|
||||
// 这些信息的修改是不会引起worker restart的,但是确实会影响告警处理逻辑
|
||||
// 所以,这里直接从memsto.AlertRuleCache中获取并覆盖
|
||||
p.inhibit = inhibit
|
||||
cachedRule := p.atertRuleCache.Get(p.rule.Id)
|
||||
cachedRule := p.alertRuleCache.Get(p.rule.Id)
|
||||
if cachedRule == nil {
|
||||
logger.Errorf("rule not found %+v", anomalyPoints)
|
||||
p.Stats.CounterRuleEvalErrorTotal.WithLabelValues(fmt.Sprintf("%v", p.DatasourceId()), "handle_event").Inc()
|
||||
return
|
||||
}
|
||||
|
||||
p.rule = cachedRule
|
||||
now := time.Now().Unix()
|
||||
alertingKeys := map[string]struct{}{}
|
||||
@@ -129,10 +145,19 @@ func (p *Processor) Handle(anomalyPoints []common.AnomalyPoint, from string, inh
|
||||
// 如果 event 被 mute 了,本质也是 fire 的状态,这里无论如何都添加到 alertingKeys 中,防止 fire 的事件自动恢复了
|
||||
hash := event.Hash
|
||||
alertingKeys[hash] = struct{}{}
|
||||
if mute.IsMuted(cachedRule, event, p.TargetCache, p.alertMuteCache) {
|
||||
logger.Debugf("rule_eval:%s event:%v is muted", p.Key(), event)
|
||||
isMuted, detail := mute.IsMuted(cachedRule, event, p.TargetCache, p.alertMuteCache)
|
||||
if isMuted {
|
||||
p.Stats.CounterMuteTotal.WithLabelValues(event.GroupName).Inc()
|
||||
logger.Debugf("rule_eval:%s event:%v is muted, detail:%s", p.Key(), event, detail)
|
||||
continue
|
||||
}
|
||||
|
||||
if p.EventMuteHook(event) {
|
||||
p.Stats.CounterMuteTotal.WithLabelValues(event.GroupName).Inc()
|
||||
logger.Debugf("rule_eval:%s event:%v is muted by hook", p.Key(), event)
|
||||
continue
|
||||
}
|
||||
|
||||
tagHash := TagHash(anomalyPoint)
|
||||
eventsMap[tagHash] = append(eventsMap[tagHash], event)
|
||||
}
|
||||
@@ -141,7 +166,7 @@ func (p *Processor) Handle(anomalyPoints []common.AnomalyPoint, from string, inh
|
||||
p.handleEvent(events)
|
||||
}
|
||||
|
||||
p.HandleRecover(alertingKeys, now)
|
||||
p.HandleRecover(alertingKeys, now, inhibit)
|
||||
}
|
||||
|
||||
func (p *Processor) BuildEvent(anomalyPoint common.AnomalyPoint, from string, now int64) *models.AlertCurEvent {
|
||||
@@ -155,6 +180,12 @@ func (p *Processor) BuildEvent(anomalyPoint common.AnomalyPoint, from string, no
|
||||
}
|
||||
|
||||
event := p.rule.GenerateNewEvent(p.ctx)
|
||||
|
||||
bg := p.BusiGroupCache.GetByBusiGroupId(p.rule.GroupId)
|
||||
if bg != nil {
|
||||
event.GroupName = bg.Name
|
||||
}
|
||||
|
||||
event.TriggerTime = anomalyPoint.Timestamp
|
||||
event.TagsMap = p.tagsMap
|
||||
event.DatasourceId = p.datasourceId
|
||||
@@ -163,8 +194,8 @@ func (p *Processor) BuildEvent(anomalyPoint common.AnomalyPoint, from string, no
|
||||
event.TargetIdent = p.target
|
||||
event.TargetNote = p.targetNote
|
||||
event.TriggerValue = anomalyPoint.ReadableValue()
|
||||
event.TriggerValues = anomalyPoint.Values
|
||||
event.TagsJSON = p.tagsArr
|
||||
event.GroupName = p.groupName
|
||||
event.Tags = strings.Join(p.tagsArr, ",,")
|
||||
event.IsRecovered = false
|
||||
event.Callbacks = p.rule.Callbacks
|
||||
@@ -174,6 +205,13 @@ func (p *Processor) BuildEvent(anomalyPoint common.AnomalyPoint, from string, no
|
||||
event.RuleConfig = p.rule.RuleConfig
|
||||
event.RuleConfigJson = p.rule.RuleConfigJson
|
||||
event.Severity = anomalyPoint.Severity
|
||||
event.ExtraConfig = p.rule.ExtraConfigJSON
|
||||
event.PromQl = anomalyPoint.Query
|
||||
|
||||
if event.TriggerValues != "" && strings.Count(event.TriggerValues, "$") > 1 {
|
||||
// TriggerValues 有多个变量,将多个变量都放到 TriggerValue 中
|
||||
event.TriggerValue = event.TriggerValues
|
||||
}
|
||||
|
||||
if from == "inner" {
|
||||
event.LastEvalTime = now
|
||||
@@ -183,7 +221,7 @@ func (p *Processor) BuildEvent(anomalyPoint common.AnomalyPoint, from string, no
|
||||
return event
|
||||
}
|
||||
|
||||
func (p *Processor) HandleRecover(alertingKeys map[string]struct{}, now int64) {
|
||||
func (p *Processor) HandleRecover(alertingKeys map[string]struct{}, now int64, inhibit bool) {
|
||||
for _, hash := range p.pendings.Keys() {
|
||||
if _, has := alertingKeys[hash]; has {
|
||||
continue
|
||||
@@ -191,19 +229,63 @@ func (p *Processor) HandleRecover(alertingKeys map[string]struct{}, now int64) {
|
||||
p.pendings.Delete(hash)
|
||||
}
|
||||
|
||||
hashArr := make([]string, 0, len(alertingKeys))
|
||||
for hash := range p.fires.GetAll() {
|
||||
if _, has := alertingKeys[hash]; has {
|
||||
continue
|
||||
}
|
||||
p.RecoverSingle(hash, now, nil)
|
||||
|
||||
hashArr = append(hashArr, hash)
|
||||
}
|
||||
p.HandleRecoverEvent(hashArr, now, inhibit)
|
||||
|
||||
}
|
||||
|
||||
func (p *Processor) RecoverSingle(hash string, now int64, value *string) {
|
||||
func (p *Processor) HandleRecoverEvent(hashArr []string, now int64, inhibit bool) {
|
||||
cachedRule := p.rule
|
||||
if cachedRule == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if !inhibit {
|
||||
for _, hash := range hashArr {
|
||||
p.RecoverSingle(hash, now, nil)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
eventMap := make(map[string]models.AlertCurEvent)
|
||||
for _, hash := range hashArr {
|
||||
event, has := p.fires.Get(hash)
|
||||
if !has {
|
||||
continue
|
||||
}
|
||||
|
||||
e, exists := eventMap[event.Tags]
|
||||
if !exists {
|
||||
eventMap[event.Tags] = *event
|
||||
continue
|
||||
}
|
||||
|
||||
if e.Severity > event.Severity {
|
||||
// hash 对应的恢复事件的被抑制了,把之前的事件删除
|
||||
p.fires.Delete(e.Hash)
|
||||
p.pendings.Delete(e.Hash)
|
||||
eventMap[event.Tags] = *event
|
||||
}
|
||||
}
|
||||
|
||||
for _, event := range eventMap {
|
||||
p.RecoverSingle(event.Hash, now, nil)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Processor) RecoverSingle(hash string, now int64, value *string, values ...string) {
|
||||
cachedRule := p.rule
|
||||
if cachedRule == nil {
|
||||
return
|
||||
}
|
||||
|
||||
event, has := p.fires.Get(hash)
|
||||
if !has {
|
||||
return
|
||||
@@ -215,6 +297,9 @@ func (p *Processor) RecoverSingle(hash string, now int64, value *string) {
|
||||
}
|
||||
if value != nil {
|
||||
event.TriggerValue = *value
|
||||
if len(values) > 0 {
|
||||
event.TriggerValues = values[0]
|
||||
}
|
||||
}
|
||||
|
||||
// 没查到触发阈值的vector,姑且就认为这个vector的值恢复了
|
||||
@@ -227,6 +312,8 @@ func (p *Processor) RecoverSingle(hash string, now int64, value *string) {
|
||||
cachedRule.UpdateEvent(event)
|
||||
event.IsRecovered = true
|
||||
event.LastEvalTime = now
|
||||
|
||||
p.HandleRecoverEventHook(event)
|
||||
p.pushEventToQueue(event)
|
||||
}
|
||||
|
||||
@@ -284,9 +371,12 @@ func (p *Processor) fireEvent(event *models.AlertCurEvent) {
|
||||
if cachedRule == nil {
|
||||
return
|
||||
}
|
||||
|
||||
logger.Debugf("rule_eval:%s event:%+v fire", p.Key(), event)
|
||||
if fired, has := p.fires.Get(event.Hash); has {
|
||||
p.fires.UpdateLastEvalTime(event.Hash, event.LastEvalTime)
|
||||
event.FirstTriggerTime = fired.FirstTriggerTime
|
||||
p.HandleFireEventHook(event)
|
||||
|
||||
if cachedRule.NotifyRepeatStep == 0 {
|
||||
logger.Debugf("rule_eval:%s event:%+v repeat is zero nothing to do", p.Key(), event)
|
||||
@@ -296,11 +386,10 @@ func (p *Processor) fireEvent(event *models.AlertCurEvent) {
|
||||
}
|
||||
|
||||
// 之前发送过告警了,这次是否要继续发送,要看是否过了通道静默时间
|
||||
if event.LastEvalTime > fired.LastSentTime+int64(cachedRule.NotifyRepeatStep)*60 {
|
||||
if event.LastEvalTime >= fired.LastSentTime+int64(cachedRule.NotifyRepeatStep)*60 {
|
||||
if cachedRule.NotifyMaxNumber == 0 {
|
||||
// 最大可以发送次数如果是0,表示不想限制最大发送次数,一直发即可
|
||||
event.NotifyCurNumber = fired.NotifyCurNumber + 1
|
||||
event.FirstTriggerTime = fired.FirstTriggerTime
|
||||
p.pushEventToQueue(event)
|
||||
} else {
|
||||
// 有最大发送次数的限制,就要看已经发了几次了,是否达到了最大发送次数
|
||||
@@ -309,7 +398,6 @@ func (p *Processor) fireEvent(event *models.AlertCurEvent) {
|
||||
return
|
||||
} else {
|
||||
event.NotifyCurNumber = fired.NotifyCurNumber + 1
|
||||
event.FirstTriggerTime = fired.FirstTriggerTime
|
||||
p.pushEventToQueue(event)
|
||||
}
|
||||
}
|
||||
@@ -317,6 +405,7 @@ func (p *Processor) fireEvent(event *models.AlertCurEvent) {
|
||||
} else {
|
||||
event.NotifyCurNumber = 1
|
||||
event.FirstTriggerTime = event.TriggerTime
|
||||
p.HandleFireEventHook(event)
|
||||
p.pushEventToQueue(event)
|
||||
}
|
||||
}
|
||||
@@ -327,10 +416,10 @@ func (p *Processor) pushEventToQueue(e *models.AlertCurEvent) {
|
||||
p.fires.Set(e.Hash, e)
|
||||
}
|
||||
|
||||
p.stats.CounterAlertsTotal.WithLabelValues(fmt.Sprintf("%d", e.DatasourceId)).Inc()
|
||||
dispatch.LogEvent(e, "push_queue")
|
||||
if !queue.EventQueue.PushFront(e) {
|
||||
logger.Warningf("event_push_queue: queue is full, event:%+v", e)
|
||||
p.Stats.CounterRuleEvalErrorTotal.WithLabelValues(fmt.Sprintf("%v", p.DatasourceId()), "push_event_queue").Inc()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -340,12 +429,21 @@ func (p *Processor) RecoverAlertCurEventFromDb() {
|
||||
curEvents, err := models.AlertCurEventGetByRuleIdAndDsId(p.ctx, p.rule.Id, p.datasourceId)
|
||||
if err != nil {
|
||||
logger.Errorf("recover event from db for rule:%s failed, err:%s", p.Key(), err)
|
||||
p.Stats.CounterRuleEvalErrorTotal.WithLabelValues(fmt.Sprintf("%v", p.DatasourceId()), "get_recover_event").Inc()
|
||||
p.fires = NewAlertCurEventMap(nil)
|
||||
return
|
||||
}
|
||||
|
||||
fireMap := make(map[string]*models.AlertCurEvent)
|
||||
for _, event := range curEvents {
|
||||
if event.Cate == models.HOST {
|
||||
target, exists := p.TargetCache.Get(event.TargetIdent)
|
||||
if exists && target.EngineName != p.EngineName && !(p.ctx.IsCenter && target.EngineName == "") {
|
||||
// 如果是 host rule,且 target 的 engineName 不是当前的 engineName 或者是中心机房 target EngineName 为空,就跳过
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
event.DB2Mem()
|
||||
fireMap[event.Hash] = event
|
||||
}
|
||||
@@ -405,7 +503,13 @@ func (p *Processor) mayHandleIdent() {
|
||||
if target, exists := p.TargetCache.Get(ident); exists {
|
||||
p.target = target.Ident
|
||||
p.targetNote = target.Note
|
||||
} else {
|
||||
p.target = ident
|
||||
p.targetNote = ""
|
||||
}
|
||||
} else {
|
||||
p.target = ""
|
||||
p.targetNote = ""
|
||||
}
|
||||
}
|
||||
|
||||
@@ -417,6 +521,11 @@ func (p *Processor) mayHandleGroup() {
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Processor) DeleteProcessEvent(hash string) {
|
||||
p.fires.Delete(hash)
|
||||
p.pendings.Delete(hash)
|
||||
}
|
||||
|
||||
func labelMapToArr(m map[string]string) []string {
|
||||
numLabels := len(m)
|
||||
|
||||
@@ -432,7 +541,7 @@ func labelMapToArr(m map[string]string) []string {
|
||||
}
|
||||
|
||||
func Hash(ruleId, datasourceId int64, vector common.AnomalyPoint) string {
|
||||
return str.MD5(fmt.Sprintf("%d_%s_%d_%d", ruleId, vector.Labels.String(), datasourceId, vector.Severity))
|
||||
return str.MD5(fmt.Sprintf("%d_%s_%d_%d_%s", ruleId, vector.Labels.String(), datasourceId, vector.Severity, vector.Query))
|
||||
}
|
||||
|
||||
func TagHash(vector common.AnomalyPoint) string {
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/ccfos/nightingale/v6/alert/astats"
|
||||
"github.com/ccfos/nightingale/v6/models"
|
||||
"github.com/ccfos/nightingale/v6/prom"
|
||||
"github.com/ccfos/nightingale/v6/pushgw/writer"
|
||||
@@ -18,18 +19,18 @@ type RecordRuleContext struct {
|
||||
datasourceId int64
|
||||
quit chan struct{}
|
||||
|
||||
rule *models.RecordingRule
|
||||
// writers *writer.WritersType
|
||||
rule *models.RecordingRule
|
||||
promClients *prom.PromClientMap
|
||||
stats *astats.Stats
|
||||
}
|
||||
|
||||
func NewRecordRuleContext(rule *models.RecordingRule, datasourceId int64, promClients *prom.PromClientMap, writers *writer.WritersType) *RecordRuleContext {
|
||||
func NewRecordRuleContext(rule *models.RecordingRule, datasourceId int64, promClients *prom.PromClientMap, writers *writer.WritersType, stats *astats.Stats) *RecordRuleContext {
|
||||
return &RecordRuleContext{
|
||||
datasourceId: datasourceId,
|
||||
quit: make(chan struct{}),
|
||||
rule: rule,
|
||||
promClients: promClients,
|
||||
//writers: writers,
|
||||
stats: stats,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,20 +55,23 @@ func (rrc *RecordRuleContext) Start() {
|
||||
if interval <= 0 {
|
||||
interval = 10
|
||||
}
|
||||
|
||||
ticker := time.NewTicker(time.Duration(interval) * time.Second)
|
||||
go func() {
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-rrc.quit:
|
||||
return
|
||||
default:
|
||||
case <-ticker.C:
|
||||
rrc.Eval()
|
||||
time.Sleep(time.Duration(interval) * time.Second)
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (rrc *RecordRuleContext) Eval() {
|
||||
rrc.stats.CounterRecordEval.WithLabelValues(fmt.Sprintf("%d", rrc.datasourceId)).Inc()
|
||||
promql := strings.TrimSpace(rrc.rule.PromQl)
|
||||
if promql == "" {
|
||||
logger.Errorf("eval:%s promql is blank", rrc.Key())
|
||||
@@ -76,23 +80,30 @@ func (rrc *RecordRuleContext) Eval() {
|
||||
|
||||
if rrc.promClients.IsNil(rrc.datasourceId) {
|
||||
logger.Errorf("eval:%s reader client is nil", rrc.Key())
|
||||
rrc.stats.CounterRecordEvalErrorTotal.WithLabelValues(fmt.Sprintf("%d", rrc.datasourceId)).Inc()
|
||||
return
|
||||
}
|
||||
|
||||
value, warnings, err := rrc.promClients.GetCli(rrc.datasourceId).Query(context.Background(), promql, time.Now())
|
||||
if err != nil {
|
||||
logger.Errorf("eval:%s promql:%s, error:%v", rrc.Key(), promql, err)
|
||||
rrc.stats.CounterRecordEvalErrorTotal.WithLabelValues(fmt.Sprintf("%d", rrc.datasourceId)).Inc()
|
||||
return
|
||||
}
|
||||
|
||||
if len(warnings) > 0 {
|
||||
logger.Errorf("eval:%s promql:%s, warnings:%v", rrc.Key(), promql, warnings)
|
||||
rrc.stats.CounterRecordEvalErrorTotal.WithLabelValues(fmt.Sprintf("%d", rrc.datasourceId)).Inc()
|
||||
return
|
||||
}
|
||||
|
||||
ts := ConvertToTimeSeries(value, rrc.rule)
|
||||
if len(ts) != 0 {
|
||||
rrc.promClients.GetWriterCli(rrc.datasourceId).Write(ts)
|
||||
err := rrc.promClients.GetWriterCli(rrc.datasourceId).Write(ts)
|
||||
if err != nil {
|
||||
logger.Errorf("eval:%s promql:%s, error:%v", rrc.Key(), promql, err)
|
||||
rrc.stats.CounterRecordEvalErrorTotal.WithLabelValues(fmt.Sprintf("%d", rrc.datasourceId)).Inc()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ const (
|
||||
LabelName = "__name__"
|
||||
)
|
||||
|
||||
func ConvertToTimeSeries(value model.Value, rule *models.RecordingRule) (lst []*prompb.TimeSeries) {
|
||||
func ConvertToTimeSeries(value model.Value, rule *models.RecordingRule) (lst []prompb.TimeSeries) {
|
||||
switch value.Type() {
|
||||
case model.ValVector:
|
||||
items, ok := value.(model.Vector)
|
||||
@@ -31,7 +31,7 @@ func ConvertToTimeSeries(value model.Value, rule *models.RecordingRule) (lst []*
|
||||
s.Timestamp = time.Unix(item.Timestamp.Unix(), 0).UnixNano() / 1e6
|
||||
s.Value = float64(item.Value)
|
||||
l := labelsToLabelsProto(item.Metric, rule)
|
||||
lst = append(lst, &prompb.TimeSeries{
|
||||
lst = append(lst, prompb.TimeSeries{
|
||||
Labels: l,
|
||||
Samples: []prompb.Sample{s},
|
||||
})
|
||||
@@ -63,7 +63,7 @@ func ConvertToTimeSeries(value model.Value, rule *models.RecordingRule) (lst []*
|
||||
Value: float64(v.Value),
|
||||
})
|
||||
}
|
||||
lst = append(lst, &prompb.TimeSeries{
|
||||
lst = append(lst, prompb.TimeSeries{
|
||||
Labels: l,
|
||||
Samples: slst,
|
||||
})
|
||||
@@ -78,7 +78,7 @@ func ConvertToTimeSeries(value model.Value, rule *models.RecordingRule) (lst []*
|
||||
return
|
||||
}
|
||||
|
||||
lst = append(lst, &prompb.TimeSeries{
|
||||
lst = append(lst, prompb.TimeSeries{
|
||||
Labels: nil,
|
||||
Samples: []prompb.Sample{{Value: float64(item.Value), Timestamp: time.Unix(item.Timestamp.Unix(), 0).UnixNano() / 1e6}},
|
||||
})
|
||||
@@ -89,9 +89,9 @@ func ConvertToTimeSeries(value model.Value, rule *models.RecordingRule) (lst []*
|
||||
return
|
||||
}
|
||||
|
||||
func labelsToLabelsProto(labels model.Metric, rule *models.RecordingRule) (result []*prompb.Label) {
|
||||
func labelsToLabelsProto(labels model.Metric, rule *models.RecordingRule) (result []prompb.Label) {
|
||||
//name
|
||||
nameLs := &prompb.Label{
|
||||
nameLs := prompb.Label{
|
||||
Name: LabelName,
|
||||
Value: rule.Name,
|
||||
}
|
||||
@@ -101,7 +101,7 @@ func labelsToLabelsProto(labels model.Metric, rule *models.RecordingRule) (resul
|
||||
continue
|
||||
}
|
||||
if model.LabelNameRE.MatchString(string(k)) {
|
||||
result = append(result, &prompb.Label{
|
||||
result = append(result, prompb.Label{
|
||||
Name: string(k),
|
||||
Value: string(v),
|
||||
})
|
||||
@@ -111,7 +111,7 @@ func labelsToLabelsProto(labels model.Metric, rule *models.RecordingRule) (resul
|
||||
for _, v := range rule.AppendTagsJSON {
|
||||
index := strings.Index(v, "=")
|
||||
if model.LabelNameRE.MatchString(v[:index]) {
|
||||
result = append(result, &prompb.Label{
|
||||
result = append(result, prompb.Label{
|
||||
Name: v[:index],
|
||||
Value: v[index+1:],
|
||||
})
|
||||
|
||||
@@ -3,6 +3,7 @@ package record
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/ccfos/nightingale/v6/alert/aconf"
|
||||
@@ -68,11 +69,11 @@ func (s *Scheduler) syncRecordRules() {
|
||||
|
||||
datasourceIds := s.promClients.Hit(rule.DatasourceIdsJson)
|
||||
for _, dsId := range datasourceIds {
|
||||
if !naming.DatasourceHashRing.IsHit(dsId, fmt.Sprintf("%d", rule.Id), s.aconf.Heartbeat.Endpoint) {
|
||||
if !naming.DatasourceHashRing.IsHit(strconv.FormatInt(dsId, 10), fmt.Sprintf("%d", rule.Id), s.aconf.Heartbeat.Endpoint) {
|
||||
continue
|
||||
}
|
||||
|
||||
recordRule := NewRecordRuleContext(rule, dsId, s.promClients, s.writers)
|
||||
recordRule := NewRecordRuleContext(rule, dsId, s.promClients, s.writers, s.stats)
|
||||
recordRules[recordRule.Hash()] = recordRule
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,13 +39,13 @@ func New(httpConfig httpx.Config, alert aconf.Alert, amc *memsto.AlertMuteCacheT
|
||||
}
|
||||
|
||||
func (rt *Router) Config(r *gin.Engine) {
|
||||
if !rt.HTTP.Alert.Enable {
|
||||
if !rt.HTTP.APIForService.Enable {
|
||||
return
|
||||
}
|
||||
|
||||
service := r.Group("/v1/n9e")
|
||||
if len(rt.HTTP.Alert.BasicAuth) > 0 {
|
||||
service.Use(gin.BasicAuth(rt.HTTP.Alert.BasicAuth))
|
||||
if len(rt.HTTP.APIForService.BasicAuth) > 0 {
|
||||
service.Use(gin.BasicAuth(rt.HTTP.APIForService.BasicAuth))
|
||||
}
|
||||
service.POST("/event", rt.pushEventToQueue)
|
||||
service.POST("/event-persist", rt.eventPersist)
|
||||
|
||||
@@ -2,6 +2,7 @@ package router
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -72,8 +73,6 @@ func (rt *Router) pushEventToQueue(c *gin.Context) {
|
||||
event.NotifyChannels = strings.Join(event.NotifyChannelsJSON, " ")
|
||||
event.NotifyGroups = strings.Join(event.NotifyGroupsJSON, " ")
|
||||
|
||||
rt.AlertStats.CounterAlertsTotal.WithLabelValues(event.Cluster).Inc()
|
||||
|
||||
dispatch.LogEvent(event, "http_push_queue")
|
||||
if !queue.EventQueue.PushFront(event) {
|
||||
msg := fmt.Sprintf("event:%+v push_queue err: queue is full", event)
|
||||
@@ -87,7 +86,8 @@ func (rt *Router) eventPersist(c *gin.Context) {
|
||||
var event *models.AlertCurEvent
|
||||
ginx.BindJSON(c, &event)
|
||||
event.FE2DB()
|
||||
ginx.NewRender(c).Message(models.EventPersist(rt.Ctx, event))
|
||||
err := models.EventPersist(rt.Ctx, event)
|
||||
ginx.NewRender(c).Data(event.Id, err)
|
||||
}
|
||||
|
||||
type eventForm struct {
|
||||
@@ -103,7 +103,7 @@ func (rt *Router) makeEvent(c *gin.Context) {
|
||||
ginx.BindJSON(c, &events)
|
||||
//now := time.Now().Unix()
|
||||
for i := 0; i < len(events); i++ {
|
||||
node, err := naming.DatasourceHashRing.GetNode(events[i].DatasourceId, fmt.Sprintf("%d", events[i].RuleId))
|
||||
node, err := naming.DatasourceHashRing.GetNode(strconv.FormatInt(events[i].DatasourceId, 10), fmt.Sprintf("%d", events[i].RuleId))
|
||||
if err != nil {
|
||||
logger.Warningf("event:%+v get node err:%v", events[i], err)
|
||||
ginx.Bomb(200, "event node not exists")
|
||||
|
||||
@@ -1,191 +1,138 @@
|
||||
package sender
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"html/template"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/ccfos/nightingale/v6/alert/aconf"
|
||||
"github.com/ccfos/nightingale/v6/alert/astats"
|
||||
"github.com/ccfos/nightingale/v6/memsto"
|
||||
"github.com/ccfos/nightingale/v6/models"
|
||||
"github.com/ccfos/nightingale/v6/pkg/ctx"
|
||||
"github.com/ccfos/nightingale/v6/pkg/ibex"
|
||||
"github.com/ccfos/nightingale/v6/pkg/poster"
|
||||
|
||||
"github.com/toolkits/pkg/logger"
|
||||
)
|
||||
|
||||
func SendCallbacks(ctx *ctx.Context, urls []string, event *models.AlertCurEvent, targetCache *memsto.TargetCacheType, userCache *memsto.UserCacheType, ibexConf aconf.Ibex) {
|
||||
for _, url := range urls {
|
||||
if url == "" {
|
||||
continue
|
||||
}
|
||||
type (
|
||||
// CallBacker 进行回调的接口
|
||||
CallBacker interface {
|
||||
CallBack(ctx CallBackContext)
|
||||
}
|
||||
|
||||
if strings.HasPrefix(url, "${ibex}") {
|
||||
if !event.IsRecovered {
|
||||
handleIbex(ctx, url, event, targetCache, userCache, ibexConf)
|
||||
}
|
||||
continue
|
||||
}
|
||||
// CallBackContext 回调时所需的上下文
|
||||
CallBackContext struct {
|
||||
Ctx *ctx.Context
|
||||
CallBackURL string
|
||||
Users []*models.User
|
||||
Rule *models.AlertRule
|
||||
Events []*models.AlertCurEvent
|
||||
Stats *astats.Stats
|
||||
}
|
||||
|
||||
if !(strings.HasPrefix(url, "http://") || strings.HasPrefix(url, "https://")) {
|
||||
url = "http://" + url
|
||||
}
|
||||
DefaultCallBacker struct{}
|
||||
)
|
||||
|
||||
resp, code, err := poster.PostJSON(url, 5*time.Second, event, 3)
|
||||
if err != nil {
|
||||
logger.Errorf("event_callback_fail(rule_id=%d url=%s), resp: %s, err: %v, code: %d", event.RuleId, url, string(resp), err, code)
|
||||
} else {
|
||||
logger.Infof("event_callback_succ(rule_id=%d url=%s), resp: %s, code: %d", event.RuleId, url, string(resp), code)
|
||||
}
|
||||
func BuildCallBackContext(ctx *ctx.Context, callBackURL string, rule *models.AlertRule, events []*models.AlertCurEvent,
|
||||
uids []int64, userCache *memsto.UserCacheType, stats *astats.Stats) CallBackContext {
|
||||
users := userCache.GetByUserIds(uids)
|
||||
|
||||
newCallBackUrl, _ := events[0].ParseURL(callBackURL)
|
||||
return CallBackContext{
|
||||
Ctx: ctx,
|
||||
CallBackURL: newCallBackUrl,
|
||||
Rule: rule,
|
||||
Events: events,
|
||||
Users: users,
|
||||
Stats: stats,
|
||||
}
|
||||
}
|
||||
|
||||
type TaskForm struct {
|
||||
Title string `json:"title"`
|
||||
Account string `json:"account"`
|
||||
Batch int `json:"batch"`
|
||||
Tolerance int `json:"tolerance"`
|
||||
Timeout int `json:"timeout"`
|
||||
Pause string `json:"pause"`
|
||||
Script string `json:"script"`
|
||||
Args string `json:"args"`
|
||||
Action string `json:"action"`
|
||||
Creator string `json:"creator"`
|
||||
Hosts []string `json:"hosts"`
|
||||
func ExtractAtsParams(rawURL string) []string {
|
||||
ans := make([]string, 0, 1)
|
||||
parsedURL, err := url.Parse(rawURL)
|
||||
if err != nil {
|
||||
logger.Errorf("ExtractAtsParams(url=%s), err: %v", rawURL, err)
|
||||
return ans
|
||||
}
|
||||
|
||||
queryParams := parsedURL.Query()
|
||||
atParam := queryParams.Get("ats")
|
||||
if atParam == "" {
|
||||
return ans
|
||||
}
|
||||
|
||||
// Split the atParam by comma and return the result as a slice
|
||||
return strings.Split(atParam, ",")
|
||||
}
|
||||
|
||||
func NewCallBacker(
|
||||
key string,
|
||||
targetCache *memsto.TargetCacheType,
|
||||
userCache *memsto.UserCacheType,
|
||||
taskTplCache *memsto.TaskTplCache,
|
||||
tpls map[string]*template.Template,
|
||||
) CallBacker {
|
||||
|
||||
switch key {
|
||||
case models.IbexDomain: // Distribute to Ibex
|
||||
return &IbexCallBacker{
|
||||
targetCache: targetCache,
|
||||
userCache: userCache,
|
||||
taskTplCache: taskTplCache,
|
||||
}
|
||||
case models.DefaultDomain: // default callback
|
||||
return &DefaultCallBacker{}
|
||||
case models.DingtalkDomain:
|
||||
return &DingtalkSender{tpl: tpls[models.Dingtalk]}
|
||||
case models.WecomDomain:
|
||||
return &WecomSender{tpl: tpls[models.Wecom]}
|
||||
case models.FeishuDomain:
|
||||
return &FeishuSender{tpl: tpls[models.Feishu]}
|
||||
case models.FeishuCardDomain:
|
||||
return &FeishuCardSender{tpl: tpls[models.FeishuCard]}
|
||||
//case models.Mm:
|
||||
// return &MmSender{tpl: tpls[models.Mm]}
|
||||
case models.TelegramDomain:
|
||||
return &TelegramSender{tpl: tpls[models.Telegram]}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *DefaultCallBacker) CallBack(ctx CallBackContext) {
|
||||
if len(ctx.CallBackURL) == 0 || len(ctx.Events) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
event := ctx.Events[0]
|
||||
|
||||
ctx.Stats.AlertNotifyTotal.WithLabelValues("rule_callback").Inc()
|
||||
resp, code, err := poster.PostJSON(ctx.CallBackURL, 5*time.Second, event, 3)
|
||||
if err != nil {
|
||||
logger.Errorf("event_callback_fail(rule_id=%d url=%s), resp: %s, err: %v, code: %d",
|
||||
event.RuleId, ctx.CallBackURL, string(resp), err, code)
|
||||
ctx.Stats.AlertNotifyErrorTotal.WithLabelValues("rule_callback").Inc()
|
||||
} else {
|
||||
logger.Infof("event_callback_succ(rule_id=%d url=%s), resp: %s, code: %d",
|
||||
event.RuleId, ctx.CallBackURL, string(resp), code)
|
||||
}
|
||||
}
|
||||
|
||||
func doSend(url string, body interface{}, channel string, stats *astats.Stats) {
|
||||
stats.AlertNotifyTotal.WithLabelValues(channel).Inc()
|
||||
|
||||
res, code, err := poster.PostJSON(url, time.Second*5, body, 3)
|
||||
if err != nil {
|
||||
logger.Errorf("%s_sender: result=fail url=%s code=%d error=%v req:%v response=%s", channel, url, code, err, body, string(res))
|
||||
stats.AlertNotifyErrorTotal.WithLabelValues(channel).Inc()
|
||||
} else {
|
||||
logger.Infof("%s_sender: result=succ url=%s code=%d req:%v response=%s", channel, url, code, body, string(res))
|
||||
}
|
||||
}
|
||||
|
||||
type TaskCreateReply struct {
|
||||
Err string `json:"err"`
|
||||
Dat int64 `json:"dat"` // task.id
|
||||
}
|
||||
|
||||
func handleIbex(ctx *ctx.Context, url string, event *models.AlertCurEvent, targetCache *memsto.TargetCacheType, userCache *memsto.UserCacheType, ibexConf aconf.Ibex) {
|
||||
arr := strings.Split(url, "/")
|
||||
|
||||
var idstr string
|
||||
var host string
|
||||
|
||||
if len(arr) > 1 {
|
||||
idstr = arr[1]
|
||||
}
|
||||
|
||||
if len(arr) > 2 {
|
||||
host = arr[2]
|
||||
}
|
||||
|
||||
id, err := strconv.ParseInt(idstr, 10, 64)
|
||||
if err != nil {
|
||||
logger.Errorf("event_callback_ibex: failed to parse url: %s", url)
|
||||
return
|
||||
}
|
||||
|
||||
if host == "" {
|
||||
// 用户在callback url中没有传入host,就从event中解析
|
||||
host = event.TargetIdent
|
||||
}
|
||||
|
||||
if host == "" {
|
||||
logger.Error("event_callback_ibex: failed to get host")
|
||||
return
|
||||
}
|
||||
|
||||
tpl, err := models.TaskTplGet(ctx, "id = ?", id)
|
||||
if err != nil {
|
||||
logger.Errorf("event_callback_ibex: failed to get tpl: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if tpl == nil {
|
||||
logger.Errorf("event_callback_ibex: no such tpl(%d)", id)
|
||||
return
|
||||
}
|
||||
|
||||
// check perm
|
||||
// tpl.GroupId - host - account 三元组校验权限
|
||||
can, err := canDoIbex(ctx, tpl.UpdateBy, tpl, host, targetCache, userCache)
|
||||
if err != nil {
|
||||
logger.Errorf("event_callback_ibex: check perm fail: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if !can {
|
||||
logger.Errorf("event_callback_ibex: user(%s) no permission", tpl.UpdateBy)
|
||||
return
|
||||
}
|
||||
|
||||
// call ibex
|
||||
in := TaskForm{
|
||||
Title: tpl.Title + " FH: " + host,
|
||||
Account: tpl.Account,
|
||||
Batch: tpl.Batch,
|
||||
Tolerance: tpl.Tolerance,
|
||||
Timeout: tpl.Timeout,
|
||||
Pause: tpl.Pause,
|
||||
Script: tpl.Script,
|
||||
Args: tpl.Args,
|
||||
Action: "start",
|
||||
Creator: tpl.UpdateBy,
|
||||
Hosts: []string{host},
|
||||
}
|
||||
|
||||
var res TaskCreateReply
|
||||
err = ibex.New(
|
||||
ibexConf.Address,
|
||||
ibexConf.BasicAuthUser,
|
||||
ibexConf.BasicAuthPass,
|
||||
ibexConf.Timeout,
|
||||
).
|
||||
Path("/ibex/v1/tasks").
|
||||
In(in).
|
||||
Out(&res).
|
||||
POST()
|
||||
|
||||
if err != nil {
|
||||
logger.Errorf("event_callback_ibex: call ibex fail: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if res.Err != "" {
|
||||
logger.Errorf("event_callback_ibex: call ibex response error: %v", res.Err)
|
||||
return
|
||||
}
|
||||
|
||||
// write db
|
||||
record := models.TaskRecord{
|
||||
Id: res.Dat,
|
||||
EventId: event.Id,
|
||||
GroupId: tpl.GroupId,
|
||||
IbexAddress: ibexConf.Address,
|
||||
IbexAuthUser: ibexConf.BasicAuthUser,
|
||||
IbexAuthPass: ibexConf.BasicAuthPass,
|
||||
Title: in.Title,
|
||||
Account: in.Account,
|
||||
Batch: in.Batch,
|
||||
Tolerance: in.Tolerance,
|
||||
Timeout: in.Timeout,
|
||||
Pause: in.Pause,
|
||||
Script: in.Script,
|
||||
Args: in.Args,
|
||||
CreateAt: time.Now().Unix(),
|
||||
CreateBy: in.Creator,
|
||||
}
|
||||
|
||||
if err = record.Add(ctx); err != nil {
|
||||
logger.Errorf("event_callback_ibex: persist task_record fail: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func canDoIbex(ctx *ctx.Context, username string, tpl *models.TaskTpl, host string, targetCache *memsto.TargetCacheType, userCache *memsto.UserCacheType) (bool, error) {
|
||||
user := userCache.GetByUsername(username)
|
||||
if user != nil && user.IsAdmin() {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
target, has := targetCache.Get(host)
|
||||
if !has {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return target.GroupId == tpl.GroupId, nil
|
||||
}
|
||||
|
||||
@@ -1,14 +1,9 @@
|
||||
package sender
|
||||
|
||||
import (
|
||||
"github.com/ccfos/nightingale/v6/models"
|
||||
"html/template"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/ccfos/nightingale/v6/models"
|
||||
"github.com/ccfos/nightingale/v6/pkg/poster"
|
||||
|
||||
"github.com/toolkits/pkg/logger"
|
||||
)
|
||||
|
||||
type dingtalkMarkdown struct {
|
||||
@@ -27,12 +22,16 @@ type dingtalk struct {
|
||||
At dingtalkAt `json:"at"`
|
||||
}
|
||||
|
||||
var (
|
||||
_ CallBacker = (*DingtalkSender)(nil)
|
||||
)
|
||||
|
||||
type DingtalkSender struct {
|
||||
tpl *template.Template
|
||||
}
|
||||
|
||||
func (ds *DingtalkSender) Send(ctx MessageContext) {
|
||||
if len(ctx.Users) == 0 || ctx.Rule == nil || ctx.Event == nil {
|
||||
if len(ctx.Users) == 0 || len(ctx.Events) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -40,7 +39,7 @@ func (ds *DingtalkSender) Send(ctx MessageContext) {
|
||||
if len(urls) == 0 {
|
||||
return
|
||||
}
|
||||
message := BuildTplMessage(ds.tpl, ctx.Event)
|
||||
message := BuildTplMessage(models.Dingtalk, ds.tpl, ctx.Events)
|
||||
|
||||
for _, url := range urls {
|
||||
var body dingtalk
|
||||
@@ -49,7 +48,7 @@ func (ds *DingtalkSender) Send(ctx MessageContext) {
|
||||
body = dingtalk{
|
||||
Msgtype: "markdown",
|
||||
Markdown: dingtalkMarkdown{
|
||||
Title: ctx.Event.RuleName,
|
||||
Title: ctx.Events[0].RuleName,
|
||||
Text: message,
|
||||
},
|
||||
}
|
||||
@@ -57,7 +56,7 @@ func (ds *DingtalkSender) Send(ctx MessageContext) {
|
||||
body = dingtalk{
|
||||
Msgtype: "markdown",
|
||||
Markdown: dingtalkMarkdown{
|
||||
Title: ctx.Event.RuleName,
|
||||
Title: ctx.Events[0].RuleName,
|
||||
Text: message + "\n" + strings.Join(ats, " "),
|
||||
},
|
||||
At: dingtalkAt{
|
||||
@@ -66,10 +65,42 @@ func (ds *DingtalkSender) Send(ctx MessageContext) {
|
||||
},
|
||||
}
|
||||
}
|
||||
ds.doSend(url, body)
|
||||
|
||||
doSend(url, body, models.Dingtalk, ctx.Stats)
|
||||
}
|
||||
}
|
||||
|
||||
func (ds *DingtalkSender) CallBack(ctx CallBackContext) {
|
||||
if len(ctx.Events) == 0 || len(ctx.CallBackURL) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
body := dingtalk{
|
||||
Msgtype: "markdown",
|
||||
Markdown: dingtalkMarkdown{
|
||||
Title: ctx.Events[0].RuleName,
|
||||
},
|
||||
}
|
||||
|
||||
ats := ExtractAtsParams(ctx.CallBackURL)
|
||||
message := BuildTplMessage(models.Dingtalk, ds.tpl, ctx.Events)
|
||||
|
||||
if len(ats) > 0 {
|
||||
body.Markdown.Text = message + "\n@" + strings.Join(ats, "@")
|
||||
body.At = dingtalkAt{
|
||||
AtMobiles: ats,
|
||||
IsAtAll: false,
|
||||
}
|
||||
} else {
|
||||
// NoAt in url
|
||||
body.Markdown.Text = message
|
||||
}
|
||||
|
||||
doSend(ctx.CallBackURL, body, models.Dingtalk, ctx.Stats)
|
||||
|
||||
ctx.Stats.AlertNotifyTotal.WithLabelValues("rule_callback").Inc()
|
||||
}
|
||||
|
||||
// extract urls and ats from Users
|
||||
func (ds *DingtalkSender) extract(users []*models.User) ([]string, []string) {
|
||||
urls := make([]string, 0, len(users))
|
||||
@@ -81,7 +112,7 @@ func (ds *DingtalkSender) extract(users []*models.User) ([]string, []string) {
|
||||
}
|
||||
if token, has := user.ExtractToken(models.Dingtalk); has {
|
||||
url := token
|
||||
if !strings.HasPrefix(token, "https://") {
|
||||
if !strings.HasPrefix(token, "https://") && !strings.HasPrefix(token, "http://") {
|
||||
url = "https://oapi.dingtalk.com/robot/send?access_token=" + token
|
||||
}
|
||||
urls = append(urls, url)
|
||||
@@ -89,12 +120,3 @@ func (ds *DingtalkSender) extract(users []*models.User) ([]string, []string) {
|
||||
}
|
||||
return urls, ats
|
||||
}
|
||||
|
||||
func (ds *DingtalkSender) doSend(url string, body dingtalk) {
|
||||
res, code, err := poster.PostJSON(url, time.Second*5, body, 3)
|
||||
if err != nil {
|
||||
logger.Errorf("dingtalk_sender: result=fail url=%s code=%d error=%v response=%s", url, code, err, string(res))
|
||||
} else {
|
||||
logger.Infof("dingtalk_sender: result=succ url=%s code=%d response=%s", url, code, string(res))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,10 +2,12 @@ package sender
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"html/template"
|
||||
"time"
|
||||
|
||||
"github.com/ccfos/nightingale/v6/alert/aconf"
|
||||
"github.com/ccfos/nightingale/v6/memsto"
|
||||
"github.com/ccfos/nightingale/v6/models"
|
||||
|
||||
"github.com/toolkits/pkg/logger"
|
||||
@@ -22,19 +24,21 @@ type EmailSender struct {
|
||||
}
|
||||
|
||||
func (es *EmailSender) Send(ctx MessageContext) {
|
||||
if len(ctx.Users) == 0 || ctx.Rule == nil || ctx.Event == nil {
|
||||
if len(ctx.Users) == 0 || len(ctx.Events) == 0 {
|
||||
return
|
||||
}
|
||||
tos := extract(ctx.Users)
|
||||
var subject string
|
||||
|
||||
if es.subjectTpl != nil {
|
||||
subject = BuildTplMessage(es.subjectTpl, ctx.Event)
|
||||
subject = BuildTplMessage(models.Email, es.subjectTpl, []*models.AlertCurEvent{ctx.Events[0]})
|
||||
} else {
|
||||
subject = ctx.Event.RuleName
|
||||
subject = ctx.Events[0].RuleName
|
||||
}
|
||||
content := BuildTplMessage(es.contentTpl, ctx.Event)
|
||||
content := BuildTplMessage(models.Email, es.contentTpl, ctx.Events)
|
||||
es.WriteEmail(subject, content, tos)
|
||||
|
||||
ctx.Stats.AlertNotifyTotal.WithLabelValues(models.Email).Add(float64(len(tos)))
|
||||
}
|
||||
|
||||
func extract(users []*models.User) []string {
|
||||
@@ -47,7 +51,7 @@ func extract(users []*models.User) []string {
|
||||
return tos
|
||||
}
|
||||
|
||||
func (es *EmailSender) SendEmail(subject, content string, tos []string, stmp aconf.SMTPConfig) {
|
||||
func SendEmail(subject, content string, tos []string, stmp aconf.SMTPConfig) error {
|
||||
conf := stmp
|
||||
|
||||
d := gomail.NewDialer(conf.Host, conf.Port, conf.User, conf.Pass)
|
||||
@@ -64,8 +68,9 @@ func (es *EmailSender) SendEmail(subject, content string, tos []string, stmp aco
|
||||
|
||||
err := d.DialAndSend(m)
|
||||
if err != nil {
|
||||
logger.Errorf("email_sender: failed to send: %v", err)
|
||||
return errors.New("email_sender: failed to send: " + err.Error())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (es *EmailSender) WriteEmail(subject, content string, tos []string) {
|
||||
@@ -81,12 +86,18 @@ func (es *EmailSender) WriteEmail(subject, content string, tos []string) {
|
||||
|
||||
func dialSmtp(d *gomail.Dialer) gomail.SendCloser {
|
||||
for {
|
||||
if s, err := d.Dial(); err != nil {
|
||||
logger.Errorf("email_sender: failed to dial smtp: %s", err)
|
||||
select {
|
||||
case <-mailQuit:
|
||||
// Note that Sendcloser is not obtained below,
|
||||
// and the outgoing signal (with configuration changes) exits the current dial
|
||||
return nil
|
||||
default:
|
||||
if s, err := d.Dial(); err != nil {
|
||||
logger.Errorf("email_sender: failed to dial smtp: %s", err)
|
||||
} else {
|
||||
return s
|
||||
}
|
||||
time.Sleep(time.Second)
|
||||
continue
|
||||
} else {
|
||||
return s
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -94,21 +105,40 @@ func dialSmtp(d *gomail.Dialer) gomail.SendCloser {
|
||||
var mailQuit = make(chan struct{})
|
||||
|
||||
func RestartEmailSender(smtp aconf.SMTPConfig) {
|
||||
close(mailQuit)
|
||||
mailQuit = make(chan struct{})
|
||||
StartEmailSender(smtp)
|
||||
// Notify internal start exit
|
||||
mailQuit <- struct{}{}
|
||||
startEmailSender(smtp)
|
||||
}
|
||||
|
||||
func StartEmailSender(smtp aconf.SMTPConfig) {
|
||||
var smtpConfig aconf.SMTPConfig
|
||||
|
||||
func InitEmailSender(ncc *memsto.NotifyConfigCacheType) {
|
||||
mailch = make(chan *gomail.Message, 100000)
|
||||
go updateSmtp(ncc)
|
||||
smtpConfig = ncc.GetSMTP()
|
||||
startEmailSender(smtpConfig)
|
||||
}
|
||||
|
||||
func updateSmtp(ncc *memsto.NotifyConfigCacheType) {
|
||||
for {
|
||||
time.Sleep(1 * time.Minute)
|
||||
smtp := ncc.GetSMTP()
|
||||
if smtpConfig.Host != smtp.Host || smtpConfig.Batch != smtp.Batch || smtpConfig.From != smtp.From ||
|
||||
smtpConfig.Pass != smtp.Pass || smtpConfig.User != smtp.User || smtpConfig.Port != smtp.Port ||
|
||||
smtpConfig.InsecureSkipVerify != smtp.InsecureSkipVerify { //diff
|
||||
smtpConfig = smtp
|
||||
RestartEmailSender(smtp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func startEmailSender(smtp aconf.SMTPConfig) {
|
||||
conf := smtp
|
||||
|
||||
if conf.Host == "" || conf.Port == 0 {
|
||||
logger.Warning("SMTP configurations invalid")
|
||||
return
|
||||
}
|
||||
logger.Infof("start email sender... %+v", conf)
|
||||
logger.Infof("start email sender... conf.Host:%+v,conf.Port:%+v", conf.Host, conf.Port)
|
||||
|
||||
d := gomail.NewDialer(conf.Host, conf.Port, conf.User, conf.Pass)
|
||||
if conf.InsecureSkipVerify {
|
||||
@@ -129,6 +159,12 @@ func StartEmailSender(smtp aconf.SMTPConfig) {
|
||||
|
||||
if !open {
|
||||
s = dialSmtp(d)
|
||||
if s == nil {
|
||||
// Indicates that the dialing failed and exited the current goroutine directly,
|
||||
// but put the Message back in the mailch
|
||||
mailch <- m
|
||||
return
|
||||
}
|
||||
open = true
|
||||
}
|
||||
if err := gomail.Send(s, m); err != nil {
|
||||
@@ -140,6 +176,12 @@ func StartEmailSender(smtp aconf.SMTPConfig) {
|
||||
}
|
||||
|
||||
s = dialSmtp(d)
|
||||
if s == nil {
|
||||
// Indicates that the dialing failed and exited the current goroutine directly,
|
||||
// but put the Message back in the mailch
|
||||
mailch <- m
|
||||
return
|
||||
}
|
||||
open = true
|
||||
|
||||
if err := gomail.Send(s, m); err != nil {
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
package sender
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"html/template"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/ccfos/nightingale/v6/models"
|
||||
"github.com/ccfos/nightingale/v6/pkg/poster"
|
||||
|
||||
"github.com/toolkits/pkg/logger"
|
||||
)
|
||||
|
||||
type feishuContent struct {
|
||||
@@ -26,16 +23,47 @@ type feishu struct {
|
||||
At feishuAt `json:"at"`
|
||||
}
|
||||
|
||||
var (
|
||||
_ CallBacker = (*FeishuSender)(nil)
|
||||
)
|
||||
|
||||
type FeishuSender struct {
|
||||
tpl *template.Template
|
||||
}
|
||||
|
||||
func (fs *FeishuSender) CallBack(ctx CallBackContext) {
|
||||
if len(ctx.Events) == 0 || len(ctx.CallBackURL) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
ats := ExtractAtsParams(ctx.CallBackURL)
|
||||
message := BuildTplMessage(models.Feishu, fs.tpl, ctx.Events)
|
||||
|
||||
if len(ats) > 0 {
|
||||
atTags := ""
|
||||
for _, at := range ats {
|
||||
atTags += fmt.Sprintf("<at user_id=\"%s\"></at> ", at)
|
||||
}
|
||||
message = atTags + message
|
||||
}
|
||||
|
||||
body := feishu{
|
||||
Msgtype: "text",
|
||||
Content: feishuContent{
|
||||
Text: message,
|
||||
},
|
||||
}
|
||||
|
||||
doSend(ctx.CallBackURL, body, models.Feishu, ctx.Stats)
|
||||
ctx.Stats.AlertNotifyTotal.WithLabelValues("rule_callback").Inc()
|
||||
}
|
||||
|
||||
func (fs *FeishuSender) Send(ctx MessageContext) {
|
||||
if len(ctx.Users) == 0 || ctx.Rule == nil || ctx.Event == nil {
|
||||
if len(ctx.Users) == 0 || len(ctx.Events) == 0 {
|
||||
return
|
||||
}
|
||||
urls, ats := fs.extract(ctx.Users)
|
||||
message := BuildTplMessage(fs.tpl, ctx.Event)
|
||||
message := BuildTplMessage(models.Feishu, fs.tpl, ctx.Events)
|
||||
for _, url := range urls {
|
||||
body := feishu{
|
||||
Msgtype: "text",
|
||||
@@ -49,7 +77,7 @@ func (fs *FeishuSender) Send(ctx MessageContext) {
|
||||
IsAtAll: false,
|
||||
}
|
||||
}
|
||||
fs.doSend(url, body)
|
||||
doSend(url, body, models.Feishu, ctx.Stats)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,7 +91,7 @@ func (fs *FeishuSender) extract(users []*models.User) ([]string, []string) {
|
||||
}
|
||||
if token, has := user.ExtractToken(models.Feishu); has {
|
||||
url := token
|
||||
if !strings.HasPrefix(token, "https://") {
|
||||
if !strings.HasPrefix(token, "https://") && !strings.HasPrefix(token, "http://") {
|
||||
url = "https://open.feishu.cn/open-apis/bot/v2/hook/" + token
|
||||
}
|
||||
urls = append(urls, url)
|
||||
@@ -71,12 +99,3 @@ func (fs *FeishuSender) extract(users []*models.User) ([]string, []string) {
|
||||
}
|
||||
return urls, ats
|
||||
}
|
||||
|
||||
func (fs *FeishuSender) doSend(url string, body feishu) {
|
||||
res, code, err := poster.PostJSON(url, time.Second*5, body, 3)
|
||||
if err != nil {
|
||||
logger.Errorf("feishu_sender: result=fail url=%s code=%d error=%v response=%s", url, code, err, string(res))
|
||||
} else {
|
||||
logger.Infof("feishu_sender: result=succ url=%s code=%d response=%s", url, code, string(res))
|
||||
}
|
||||
}
|
||||
|
||||
177
alert/sender/feishucard.go
Normal file
177
alert/sender/feishucard.go
Normal file
@@ -0,0 +1,177 @@
|
||||
package sender
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"html/template"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/ccfos/nightingale/v6/models"
|
||||
)
|
||||
|
||||
type Conf struct {
|
||||
WideScreenMode bool `json:"wide_screen_mode"`
|
||||
EnableForward bool `json:"enable_forward"`
|
||||
}
|
||||
|
||||
type Te struct {
|
||||
Content string `json:"content"`
|
||||
Tag string `json:"tag"`
|
||||
}
|
||||
|
||||
type Element struct {
|
||||
Tag string `json:"tag"`
|
||||
Text Te `json:"text"`
|
||||
Content string `json:"content"`
|
||||
Elements []Element `json:"elements"`
|
||||
}
|
||||
|
||||
type Titles struct {
|
||||
Content string `json:"content"`
|
||||
Tag string `json:"tag"`
|
||||
}
|
||||
|
||||
type Headers struct {
|
||||
Title Titles `json:"title"`
|
||||
Template string `json:"template"`
|
||||
}
|
||||
|
||||
type Cards struct {
|
||||
Config Conf `json:"config"`
|
||||
Elements []Element `json:"elements"`
|
||||
Header Headers `json:"header"`
|
||||
}
|
||||
|
||||
type feishuCard struct {
|
||||
feishu
|
||||
Card Cards `json:"card"`
|
||||
}
|
||||
|
||||
type FeishuCardSender struct {
|
||||
tpl *template.Template
|
||||
}
|
||||
|
||||
const (
|
||||
Recovered = "recovered"
|
||||
Triggered = "triggered"
|
||||
)
|
||||
|
||||
var (
|
||||
body = feishuCard{
|
||||
feishu: feishu{Msgtype: "interactive"},
|
||||
Card: Cards{
|
||||
Config: Conf{
|
||||
WideScreenMode: true,
|
||||
EnableForward: true,
|
||||
},
|
||||
Header: Headers{
|
||||
Title: Titles{
|
||||
Tag: "plain_text",
|
||||
},
|
||||
},
|
||||
Elements: []Element{
|
||||
{
|
||||
Tag: "div",
|
||||
Text: Te{
|
||||
Tag: "lark_md",
|
||||
},
|
||||
},
|
||||
{
|
||||
Tag: "hr",
|
||||
},
|
||||
{
|
||||
Tag: "note",
|
||||
Elements: []Element{
|
||||
{
|
||||
Tag: "lark_md",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
func (fs *FeishuCardSender) CallBack(ctx CallBackContext) {
|
||||
if len(ctx.Events) == 0 || len(ctx.CallBackURL) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
ats := ExtractAtsParams(ctx.CallBackURL)
|
||||
message := BuildTplMessage(models.FeishuCard, fs.tpl, ctx.Events)
|
||||
|
||||
if len(ats) > 0 {
|
||||
atTags := ""
|
||||
for _, at := range ats {
|
||||
if strings.Contains(at, "@") {
|
||||
atTags += fmt.Sprintf("<at email=\"%s\" ></at>", at)
|
||||
} else {
|
||||
atTags += fmt.Sprintf("<at id=\"%s\" ></at>", at)
|
||||
}
|
||||
}
|
||||
message = atTags + message
|
||||
}
|
||||
|
||||
color := "red"
|
||||
lowerUnicode := strings.ToLower(message)
|
||||
if strings.Count(lowerUnicode, Recovered) > 0 && strings.Count(lowerUnicode, Triggered) > 0 {
|
||||
color = "orange"
|
||||
} else if strings.Count(lowerUnicode, Recovered) > 0 {
|
||||
color = "green"
|
||||
}
|
||||
|
||||
SendTitle := fmt.Sprintf("🔔 %s", ctx.Events[0].RuleName)
|
||||
body.Card.Header.Title.Content = SendTitle
|
||||
body.Card.Header.Template = color
|
||||
body.Card.Elements[0].Text.Content = message
|
||||
body.Card.Elements[2].Elements[0].Content = SendTitle
|
||||
|
||||
// This is to be compatible with the feishucard interface, if with query string parameters, the request will fail
|
||||
// Remove query parameters from the URL,
|
||||
parsedURL, err := url.Parse(ctx.CallBackURL)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
parsedURL.RawQuery = ""
|
||||
|
||||
doSend(parsedURL.String(), body, models.FeishuCard, ctx.Stats)
|
||||
}
|
||||
|
||||
func (fs *FeishuCardSender) Send(ctx MessageContext) {
|
||||
if len(ctx.Users) == 0 || len(ctx.Events) == 0 {
|
||||
return
|
||||
}
|
||||
urls, _ := fs.extract(ctx.Users)
|
||||
message := BuildTplMessage(models.FeishuCard, fs.tpl, ctx.Events)
|
||||
color := "red"
|
||||
lowerUnicode := strings.ToLower(message)
|
||||
if strings.Count(lowerUnicode, Recovered) > 0 && strings.Count(lowerUnicode, Triggered) > 0 {
|
||||
color = "orange"
|
||||
} else if strings.Count(lowerUnicode, Recovered) > 0 {
|
||||
color = "green"
|
||||
}
|
||||
|
||||
SendTitle := fmt.Sprintf("🔔 %s", ctx.Events[0].RuleName)
|
||||
body.Card.Header.Title.Content = SendTitle
|
||||
body.Card.Header.Template = color
|
||||
body.Card.Elements[0].Text.Content = message
|
||||
body.Card.Elements[2].Elements[0].Content = SendTitle
|
||||
for _, url := range urls {
|
||||
doSend(url, body, models.FeishuCard, ctx.Stats)
|
||||
}
|
||||
}
|
||||
|
||||
func (fs *FeishuCardSender) extract(users []*models.User) ([]string, []string) {
|
||||
urls := make([]string, 0, len(users))
|
||||
ats := make([]string, 0)
|
||||
for i := range users {
|
||||
if token, has := users[i].ExtractToken(models.FeishuCard); has {
|
||||
url := token
|
||||
if !strings.HasPrefix(token, "https://") && !strings.HasPrefix(token, "http://") {
|
||||
url = "https://open.feishu.cn/open-apis/bot/v2/hook/" + strings.TrimSpace(token)
|
||||
}
|
||||
urls = append(urls, url)
|
||||
}
|
||||
}
|
||||
return urls, ats
|
||||
}
|
||||
265
alert/sender/ibex.go
Normal file
265
alert/sender/ibex.go
Normal file
@@ -0,0 +1,265 @@
|
||||
// @Author: Ciusyan 6/5/24
|
||||
|
||||
package sender
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/ccfos/nightingale/v6/memsto"
|
||||
"github.com/ccfos/nightingale/v6/models"
|
||||
"github.com/ccfos/nightingale/v6/pkg/ctx"
|
||||
imodels "github.com/flashcatcloud/ibex/src/models"
|
||||
"github.com/flashcatcloud/ibex/src/storage"
|
||||
|
||||
"github.com/toolkits/pkg/logger"
|
||||
)
|
||||
|
||||
var (
|
||||
_ CallBacker = (*IbexCallBacker)(nil)
|
||||
)
|
||||
|
||||
type IbexCallBacker struct {
|
||||
targetCache *memsto.TargetCacheType
|
||||
userCache *memsto.UserCacheType
|
||||
taskTplCache *memsto.TaskTplCache
|
||||
}
|
||||
|
||||
func (c *IbexCallBacker) CallBack(ctx CallBackContext) {
|
||||
if len(ctx.CallBackURL) == 0 || len(ctx.Events) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
event := ctx.Events[0]
|
||||
|
||||
if event.IsRecovered {
|
||||
return
|
||||
}
|
||||
|
||||
c.handleIbex(ctx.Ctx, ctx.CallBackURL, event)
|
||||
}
|
||||
|
||||
func (c *IbexCallBacker) handleIbex(ctx *ctx.Context, url string, event *models.AlertCurEvent) {
|
||||
if imodels.DB() == nil && ctx.IsCenter {
|
||||
logger.Warning("event_callback_ibex: db is nil")
|
||||
return
|
||||
}
|
||||
|
||||
arr := strings.Split(url, "/")
|
||||
|
||||
var idstr string
|
||||
var host string
|
||||
|
||||
if len(arr) > 1 {
|
||||
idstr = arr[1]
|
||||
}
|
||||
|
||||
if len(arr) > 2 {
|
||||
host = arr[2]
|
||||
}
|
||||
|
||||
id, err := strconv.ParseInt(idstr, 10, 64)
|
||||
if err != nil {
|
||||
logger.Errorf("event_callback_ibex: failed to parse url: %s", url)
|
||||
return
|
||||
}
|
||||
|
||||
if host == "" {
|
||||
// 用户在callback url中没有传入host,就从event中解析
|
||||
host = event.TargetIdent
|
||||
}
|
||||
|
||||
if host == "" {
|
||||
logger.Error("event_callback_ibex: failed to get host")
|
||||
return
|
||||
}
|
||||
|
||||
tpl := c.taskTplCache.Get(id)
|
||||
if tpl == nil {
|
||||
logger.Errorf("event_callback_ibex: no such tpl(%d)", id)
|
||||
return
|
||||
}
|
||||
|
||||
// check perm
|
||||
// tpl.GroupId - host - account 三元组校验权限
|
||||
can, err := canDoIbex(tpl.UpdateBy, tpl, host, c.targetCache, c.userCache)
|
||||
if err != nil {
|
||||
logger.Errorf("event_callback_ibex: check perm fail: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if !can {
|
||||
logger.Errorf("event_callback_ibex: user(%s) no permission", tpl.UpdateBy)
|
||||
return
|
||||
}
|
||||
|
||||
tagsMap := make(map[string]string)
|
||||
for i := 0; i < len(event.TagsJSON); i++ {
|
||||
pair := strings.TrimSpace(event.TagsJSON[i])
|
||||
if pair == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
arr := strings.Split(pair, "=")
|
||||
if len(arr) != 2 {
|
||||
continue
|
||||
}
|
||||
|
||||
tagsMap[arr[0]] = arr[1]
|
||||
}
|
||||
// 附加告警级别 告警触发值标签
|
||||
tagsMap["alert_severity"] = strconv.Itoa(event.Severity)
|
||||
tagsMap["alert_trigger_value"] = event.TriggerValue
|
||||
|
||||
tags, err := json.Marshal(tagsMap)
|
||||
if err != nil {
|
||||
logger.Errorf("event_callback_ibex: failed to marshal tags to json: %v", tagsMap)
|
||||
return
|
||||
}
|
||||
|
||||
// call ibex
|
||||
in := models.TaskForm{
|
||||
Title: tpl.Title + " FH: " + host,
|
||||
Account: tpl.Account,
|
||||
Batch: tpl.Batch,
|
||||
Tolerance: tpl.Tolerance,
|
||||
Timeout: tpl.Timeout,
|
||||
Pause: tpl.Pause,
|
||||
Script: tpl.Script,
|
||||
Args: tpl.Args,
|
||||
Stdin: string(tags),
|
||||
Action: "start",
|
||||
Creator: tpl.UpdateBy,
|
||||
Hosts: []string{host},
|
||||
AlertTriggered: true,
|
||||
}
|
||||
|
||||
id, err = TaskAdd(in, tpl.UpdateBy, ctx.IsCenter)
|
||||
if err != nil {
|
||||
logger.Errorf("event_callback_ibex: call ibex fail: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// write db
|
||||
record := models.TaskRecord{
|
||||
Id: id,
|
||||
EventId: event.Id,
|
||||
GroupId: tpl.GroupId,
|
||||
Title: in.Title,
|
||||
Account: in.Account,
|
||||
Batch: in.Batch,
|
||||
Tolerance: in.Tolerance,
|
||||
Timeout: in.Timeout,
|
||||
Pause: in.Pause,
|
||||
Script: in.Script,
|
||||
Args: in.Args,
|
||||
CreateAt: time.Now().Unix(),
|
||||
CreateBy: in.Creator,
|
||||
}
|
||||
|
||||
if err = record.Add(ctx); err != nil {
|
||||
logger.Errorf("event_callback_ibex: persist task_record fail: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func canDoIbex(username string, tpl *models.TaskTpl, host string, targetCache *memsto.TargetCacheType, userCache *memsto.UserCacheType) (bool, error) {
|
||||
user := userCache.GetByUsername(username)
|
||||
if user != nil && user.IsAdmin() {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
target, has := targetCache.Get(host)
|
||||
if !has {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return target.GroupId == tpl.GroupId, nil
|
||||
}
|
||||
|
||||
func TaskAdd(f models.TaskForm, authUser string, isCenter bool) (int64, error) {
|
||||
hosts := cleanHosts(f.Hosts)
|
||||
if len(hosts) == 0 {
|
||||
return 0, fmt.Errorf("arg(hosts) empty")
|
||||
}
|
||||
|
||||
taskMeta := &imodels.TaskMeta{
|
||||
Title: f.Title,
|
||||
Account: f.Account,
|
||||
Batch: f.Batch,
|
||||
Tolerance: f.Tolerance,
|
||||
Timeout: f.Timeout,
|
||||
Pause: f.Pause,
|
||||
Script: f.Script,
|
||||
Args: f.Args,
|
||||
Stdin: f.Stdin,
|
||||
Creator: f.Creator,
|
||||
}
|
||||
|
||||
err := taskMeta.CleanFields()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
taskMeta.HandleFH(hosts[0])
|
||||
|
||||
// 任务类型分为"告警规则触发"和"n9e center用户下发"两种;
|
||||
// 边缘机房"告警规则触发"的任务不需要规划,并且它可能是失联的,无法使用db资源,所以放入redis缓存中,直接下发给agentd执行
|
||||
if !isCenter && f.AlertTriggered {
|
||||
if err := taskMeta.Create(); err != nil {
|
||||
// 当网络不连通时,生成唯一的id,防止边缘机房中不同任务的id相同;
|
||||
// 方法是,redis自增id去防止同一个机房的不同n9e edge生成的id相同;
|
||||
// 但没法防止不同边缘机房生成同样的id,所以,生成id的数据不会上报存入数据库,只用于闭环执行。
|
||||
taskMeta.Id, err = storage.IdGet()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
|
||||
taskHost := imodels.TaskHost{
|
||||
Id: taskMeta.Id,
|
||||
Host: hosts[0],
|
||||
Status: "running",
|
||||
}
|
||||
if err = taskHost.Create(); err != nil {
|
||||
logger.Warningf("task_add_fail: authUser=%s title=%s err=%s", authUser, taskMeta.Title, err.Error())
|
||||
}
|
||||
|
||||
// 缓存任务元信息和待下发的任务
|
||||
err = taskMeta.Cache(hosts[0])
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
} else {
|
||||
// 如果是中心机房,还是保持之前的逻辑
|
||||
err = taskMeta.Save(hosts, f.Action)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
|
||||
logger.Infof("task_add_succ: authUser=%s title=%s", authUser, taskMeta.Title)
|
||||
return taskMeta.Id, nil
|
||||
}
|
||||
|
||||
func cleanHosts(formHosts []string) []string {
|
||||
cnt := len(formHosts)
|
||||
arr := make([]string, 0, cnt)
|
||||
for i := 0; i < cnt; i++ {
|
||||
item := strings.TrimSpace(formHosts[i])
|
||||
if item == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.HasPrefix(item, "#") {
|
||||
continue
|
||||
}
|
||||
|
||||
arr = append(arr, item)
|
||||
}
|
||||
|
||||
return arr
|
||||
}
|
||||
@@ -4,10 +4,9 @@ import (
|
||||
"html/template"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/ccfos/nightingale/v6/alert/astats"
|
||||
"github.com/ccfos/nightingale/v6/models"
|
||||
"github.com/ccfos/nightingale/v6/pkg/poster"
|
||||
|
||||
"github.com/toolkits/pkg/logger"
|
||||
)
|
||||
@@ -15,6 +14,7 @@ import (
|
||||
type MatterMostMessage struct {
|
||||
Text string
|
||||
Tokens []string
|
||||
Stats *astats.Stats
|
||||
}
|
||||
|
||||
type mm struct {
|
||||
@@ -28,7 +28,7 @@ type MmSender struct {
|
||||
}
|
||||
|
||||
func (ms *MmSender) Send(ctx MessageContext) {
|
||||
if len(ctx.Users) == 0 || ctx.Rule == nil || ctx.Event == nil {
|
||||
if len(ctx.Users) == 0 || len(ctx.Events) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -36,14 +36,30 @@ func (ms *MmSender) Send(ctx MessageContext) {
|
||||
if len(urls) == 0 {
|
||||
return
|
||||
}
|
||||
message := BuildTplMessage(ms.tpl, ctx.Event)
|
||||
message := BuildTplMessage(models.Mm, ms.tpl, ctx.Events)
|
||||
|
||||
SendMM(MatterMostMessage{
|
||||
Text: message,
|
||||
Tokens: urls,
|
||||
Stats: ctx.Stats,
|
||||
})
|
||||
}
|
||||
|
||||
func (ms *MmSender) CallBack(ctx CallBackContext) {
|
||||
if len(ctx.Events) == 0 || len(ctx.CallBackURL) == 0 {
|
||||
return
|
||||
}
|
||||
message := BuildTplMessage(models.Mm, ms.tpl, ctx.Events)
|
||||
|
||||
SendMM(MatterMostMessage{
|
||||
Text: message,
|
||||
Tokens: []string{ctx.CallBackURL},
|
||||
Stats: ctx.Stats,
|
||||
})
|
||||
|
||||
ctx.Stats.AlertNotifyTotal.WithLabelValues("rule_callback").Inc()
|
||||
}
|
||||
|
||||
func (ms *MmSender) extract(users []*models.User) []string {
|
||||
tokens := make([]string, 0, len(users))
|
||||
for _, user := range users {
|
||||
@@ -87,13 +103,7 @@ func SendMM(message MatterMostMessage) {
|
||||
Username: username,
|
||||
Text: txt + message.Text,
|
||||
}
|
||||
|
||||
res, code, err := poster.PostJSON(ur, time.Second*5, body, 3)
|
||||
if err != nil {
|
||||
logger.Errorf("mm_sender: result=fail url=%s code=%d error=%v response=%s", ur, code, err, string(res))
|
||||
} else {
|
||||
logger.Infof("mm_sender: result=succ url=%s code=%d response=%s", ur, code, string(res))
|
||||
}
|
||||
doSend(ur, body, models.Mm, message.Stats)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"os/exec"
|
||||
"time"
|
||||
|
||||
"github.com/ccfos/nightingale/v6/alert/astats"
|
||||
"github.com/ccfos/nightingale/v6/models"
|
||||
|
||||
"github.com/toolkits/pkg/file"
|
||||
@@ -13,20 +14,22 @@ import (
|
||||
"github.com/toolkits/pkg/sys"
|
||||
)
|
||||
|
||||
func MayPluginNotify(noticeBytes []byte, notifyScript models.NotifyScript) {
|
||||
func MayPluginNotify(noticeBytes []byte, notifyScript models.NotifyScript, stats *astats.Stats) {
|
||||
if len(noticeBytes) == 0 {
|
||||
return
|
||||
}
|
||||
alertingCallScript(noticeBytes, notifyScript)
|
||||
alertingCallScript(noticeBytes, notifyScript, stats)
|
||||
}
|
||||
|
||||
func alertingCallScript(stdinBytes []byte, notifyScript models.NotifyScript) {
|
||||
func alertingCallScript(stdinBytes []byte, notifyScript models.NotifyScript, stats *astats.Stats) {
|
||||
// not enable or no notify.py? do nothing
|
||||
config := notifyScript
|
||||
if !config.Enable || config.Content == "" {
|
||||
return
|
||||
}
|
||||
|
||||
channel := "script"
|
||||
stats.AlertNotifyTotal.WithLabelValues(channel).Inc()
|
||||
fpath := ".notify_scriptt"
|
||||
if config.Type == 1 {
|
||||
fpath = config.Content
|
||||
@@ -36,6 +39,7 @@ func alertingCallScript(stdinBytes []byte, notifyScript models.NotifyScript) {
|
||||
oldContent, err := file.ToString(fpath)
|
||||
if err != nil {
|
||||
logger.Errorf("event_script_notify_fail: read script file err: %v", err)
|
||||
stats.AlertNotifyErrorTotal.WithLabelValues(channel).Inc()
|
||||
return
|
||||
}
|
||||
|
||||
@@ -48,12 +52,14 @@ func alertingCallScript(stdinBytes []byte, notifyScript models.NotifyScript) {
|
||||
_, err := file.WriteString(fpath, config.Content)
|
||||
if err != nil {
|
||||
logger.Errorf("event_script_notify_fail: write script file err: %v", err)
|
||||
stats.AlertNotifyErrorTotal.WithLabelValues(channel).Inc()
|
||||
return
|
||||
}
|
||||
|
||||
err = os.Chmod(fpath, 0777)
|
||||
if err != nil {
|
||||
logger.Errorf("event_script_notify_fail: chmod script file err: %v", err)
|
||||
stats.AlertNotifyErrorTotal.WithLabelValues(channel).Inc()
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -83,13 +89,14 @@ func alertingCallScript(stdinBytes []byte, notifyScript models.NotifyScript) {
|
||||
|
||||
if err != nil {
|
||||
logger.Errorf("event_script_notify_fail: kill process %s occur error %v", fpath, err)
|
||||
stats.AlertNotifyErrorTotal.WithLabelValues(channel).Inc()
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
logger.Errorf("event_script_notify_fail: exec script %s occur error: %v, output: %s", fpath, err, buf.String())
|
||||
stats.AlertNotifyErrorTotal.WithLabelValues(channel).Inc()
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"html/template"
|
||||
|
||||
"github.com/ccfos/nightingale/v6/alert/aconf"
|
||||
"github.com/ccfos/nightingale/v6/alert/astats"
|
||||
"github.com/ccfos/nightingale/v6/memsto"
|
||||
"github.com/ccfos/nightingale/v6/models"
|
||||
)
|
||||
@@ -17,13 +18,14 @@ type (
|
||||
|
||||
// MessageContext 一个event所生成的告警通知的上下文
|
||||
MessageContext struct {
|
||||
Users []*models.User
|
||||
Rule *models.AlertRule
|
||||
Event *models.AlertCurEvent
|
||||
Users []*models.User
|
||||
Rule *models.AlertRule
|
||||
Events []*models.AlertCurEvent
|
||||
Stats *astats.Stats
|
||||
}
|
||||
)
|
||||
|
||||
func NewSender(key string, tpls map[string]*template.Template, smtp aconf.SMTPConfig) Sender {
|
||||
func NewSender(key string, tpls map[string]*template.Template, smtp ...aconf.SMTPConfig) Sender {
|
||||
switch key {
|
||||
case models.Dingtalk:
|
||||
return &DingtalkSender{tpl: tpls[models.Dingtalk]}
|
||||
@@ -31,8 +33,10 @@ func NewSender(key string, tpls map[string]*template.Template, smtp aconf.SMTPCo
|
||||
return &WecomSender{tpl: tpls[models.Wecom]}
|
||||
case models.Feishu:
|
||||
return &FeishuSender{tpl: tpls[models.Feishu]}
|
||||
case models.FeishuCard:
|
||||
return &FeishuCardSender{tpl: tpls[models.FeishuCard]}
|
||||
case models.Email:
|
||||
return &EmailSender{subjectTpl: tpls["mailsubject"], contentTpl: tpls[models.Email], smtp: smtp}
|
||||
return &EmailSender{subjectTpl: tpls[models.EmailSubject], contentTpl: tpls[models.Email], smtp: smtp[0]}
|
||||
case models.Mm:
|
||||
return &MmSender{tpl: tpls[models.Mm]}
|
||||
case models.Telegram:
|
||||
@@ -41,23 +45,33 @@ func NewSender(key string, tpls map[string]*template.Template, smtp aconf.SMTPCo
|
||||
return nil
|
||||
}
|
||||
|
||||
func BuildMessageContext(rule *models.AlertRule, event *models.AlertCurEvent, uids []int64, userCache *memsto.UserCacheType) MessageContext {
|
||||
func BuildMessageContext(rule *models.AlertRule, events []*models.AlertCurEvent, uids []int64, userCache *memsto.UserCacheType, stats *astats.Stats) MessageContext {
|
||||
users := userCache.GetByUserIds(uids)
|
||||
return MessageContext{
|
||||
Rule: rule,
|
||||
Event: event,
|
||||
Users: users,
|
||||
Rule: rule,
|
||||
Events: events,
|
||||
Users: users,
|
||||
Stats: stats,
|
||||
}
|
||||
}
|
||||
|
||||
func BuildTplMessage(tpl *template.Template, event *models.AlertCurEvent) string {
|
||||
type BuildTplMessageFunc func(channel string, tpl *template.Template, events []*models.AlertCurEvent) string
|
||||
|
||||
var BuildTplMessage BuildTplMessageFunc = buildTplMessage
|
||||
|
||||
func buildTplMessage(channel string, tpl *template.Template, events []*models.AlertCurEvent) string {
|
||||
if tpl == nil {
|
||||
return "tpl for current sender not found, please check configuration"
|
||||
}
|
||||
|
||||
var body bytes.Buffer
|
||||
if err := tpl.Execute(&body, event); err != nil {
|
||||
return err.Error()
|
||||
var content string
|
||||
for _, event := range events {
|
||||
var body bytes.Buffer
|
||||
if err := tpl.Execute(&body, event); err != nil {
|
||||
return err.Error()
|
||||
}
|
||||
content += body.String() + "\n\n"
|
||||
}
|
||||
return body.String()
|
||||
|
||||
return content
|
||||
}
|
||||
|
||||
@@ -3,10 +3,9 @@ package sender
|
||||
import (
|
||||
"html/template"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/ccfos/nightingale/v6/alert/astats"
|
||||
"github.com/ccfos/nightingale/v6/models"
|
||||
"github.com/ccfos/nightingale/v6/pkg/poster"
|
||||
|
||||
"github.com/toolkits/pkg/logger"
|
||||
)
|
||||
@@ -14,6 +13,7 @@ import (
|
||||
type TelegramMessage struct {
|
||||
Text string
|
||||
Tokens []string
|
||||
Stats *astats.Stats
|
||||
}
|
||||
|
||||
type telegram struct {
|
||||
@@ -21,20 +21,40 @@ type telegram struct {
|
||||
Text string `json:"text"`
|
||||
}
|
||||
|
||||
var (
|
||||
_ CallBacker = (*TelegramSender)(nil)
|
||||
)
|
||||
|
||||
type TelegramSender struct {
|
||||
tpl *template.Template
|
||||
}
|
||||
|
||||
func (ts *TelegramSender) CallBack(ctx CallBackContext) {
|
||||
if len(ctx.Events) == 0 || len(ctx.CallBackURL) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
message := BuildTplMessage(models.Telegram, ts.tpl, ctx.Events)
|
||||
SendTelegram(TelegramMessage{
|
||||
Text: message,
|
||||
Tokens: []string{ctx.CallBackURL},
|
||||
Stats: ctx.Stats,
|
||||
})
|
||||
|
||||
ctx.Stats.AlertNotifyTotal.WithLabelValues("rule_callback").Inc()
|
||||
}
|
||||
|
||||
func (ts *TelegramSender) Send(ctx MessageContext) {
|
||||
if len(ctx.Users) == 0 || ctx.Rule == nil || ctx.Event == nil {
|
||||
if len(ctx.Users) == 0 || len(ctx.Events) == 0 {
|
||||
return
|
||||
}
|
||||
tokens := ts.extract(ctx.Users)
|
||||
message := BuildTplMessage(ts.tpl, ctx.Event)
|
||||
message := BuildTplMessage(models.Telegram, ts.tpl, ctx.Events)
|
||||
|
||||
SendTelegram(TelegramMessage{
|
||||
Text: message,
|
||||
Tokens: tokens,
|
||||
Stats: ctx.Stats,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -55,7 +75,7 @@ func SendTelegram(message TelegramMessage) {
|
||||
continue
|
||||
}
|
||||
var url string
|
||||
if strings.HasPrefix(message.Tokens[i], "https://") {
|
||||
if strings.HasPrefix(message.Tokens[i], "https://") || strings.HasPrefix(message.Tokens[i], "http://") {
|
||||
url = message.Tokens[i]
|
||||
} else {
|
||||
array := strings.Split(message.Tokens[i], "/")
|
||||
@@ -72,11 +92,6 @@ func SendTelegram(message TelegramMessage) {
|
||||
Text: message.Text,
|
||||
}
|
||||
|
||||
res, code, err := poster.PostJSON(url, time.Second*5, body, 3)
|
||||
if err != nil {
|
||||
logger.Errorf("telegram_sender: result=fail url=%s code=%d error=%v response=%s", url, code, err, string(res))
|
||||
} else {
|
||||
logger.Infof("telegram_sender: result=succ url=%s code=%d response=%s", url, code, string(res))
|
||||
}
|
||||
doSend(url, body, models.Telegram, message.Stats)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,16 +3,17 @@ package sender
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/ccfos/nightingale/v6/alert/astats"
|
||||
"github.com/ccfos/nightingale/v6/models"
|
||||
|
||||
"github.com/toolkits/pkg/logger"
|
||||
)
|
||||
|
||||
func SendWebhooks(webhooks []*models.Webhook, event *models.AlertCurEvent) {
|
||||
func SendWebhooks(webhooks []*models.Webhook, event *models.AlertCurEvent, stats *astats.Stats) {
|
||||
for _, conf := range webhooks {
|
||||
if conf.Url == "" || !conf.Enable {
|
||||
continue
|
||||
@@ -37,7 +38,7 @@ func SendWebhooks(webhooks []*models.Webhook, event *models.AlertCurEvent) {
|
||||
|
||||
if len(conf.Headers) > 0 && len(conf.Headers)%2 == 0 {
|
||||
for i := 0; i < len(conf.Headers); i += 2 {
|
||||
if conf.Headers[i] == "host" {
|
||||
if conf.Headers[i] == "host" || conf.Headers[i] == "Host" {
|
||||
req.Host = conf.Headers[i+1]
|
||||
continue
|
||||
}
|
||||
@@ -50,9 +51,11 @@ func SendWebhooks(webhooks []*models.Webhook, event *models.AlertCurEvent) {
|
||||
Timeout: time.Duration(conf.Timeout) * time.Second,
|
||||
}
|
||||
|
||||
stats.AlertNotifyTotal.WithLabelValues("webhook").Inc()
|
||||
var resp *http.Response
|
||||
resp, err = client.Do(req)
|
||||
if err != nil {
|
||||
stats.AlertNotifyErrorTotal.WithLabelValues("webhook").Inc()
|
||||
logger.Errorf("event_webhook_fail, ruleId: [%d], eventId: [%d], url: [%s], error: [%s]", event.RuleId, event.Id, conf.Url, err)
|
||||
continue
|
||||
}
|
||||
@@ -60,9 +63,9 @@ func SendWebhooks(webhooks []*models.Webhook, event *models.AlertCurEvent) {
|
||||
var body []byte
|
||||
if resp.Body != nil {
|
||||
defer resp.Body.Close()
|
||||
body, _ = ioutil.ReadAll(resp.Body)
|
||||
body, _ = io.ReadAll(resp.Body)
|
||||
}
|
||||
|
||||
logger.Debugf("event_webhook_succ, url: %s, response code: %d, body: %s", conf.Url, resp.StatusCode, string(body))
|
||||
logger.Debugf("event_webhook_succ, url: %s, response code: %d, body: %s event:%+v", conf.Url, resp.StatusCode, string(body), event)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,12 +3,8 @@ package sender
|
||||
import (
|
||||
"html/template"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/ccfos/nightingale/v6/models"
|
||||
"github.com/ccfos/nightingale/v6/pkg/poster"
|
||||
|
||||
"github.com/toolkits/pkg/logger"
|
||||
)
|
||||
|
||||
type wecomMarkdown struct {
|
||||
@@ -20,16 +16,37 @@ type wecom struct {
|
||||
Markdown wecomMarkdown `json:"markdown"`
|
||||
}
|
||||
|
||||
var (
|
||||
_ CallBacker = (*WecomSender)(nil)
|
||||
)
|
||||
|
||||
type WecomSender struct {
|
||||
tpl *template.Template
|
||||
}
|
||||
|
||||
func (ws *WecomSender) CallBack(ctx CallBackContext) {
|
||||
if len(ctx.Events) == 0 || len(ctx.CallBackURL) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
message := BuildTplMessage(models.Wecom, ws.tpl, ctx.Events)
|
||||
body := wecom{
|
||||
Msgtype: "markdown",
|
||||
Markdown: wecomMarkdown{
|
||||
Content: message,
|
||||
},
|
||||
}
|
||||
|
||||
doSend(ctx.CallBackURL, body, models.Wecom, ctx.Stats)
|
||||
ctx.Stats.AlertNotifyTotal.WithLabelValues("rule_callback").Inc()
|
||||
}
|
||||
|
||||
func (ws *WecomSender) Send(ctx MessageContext) {
|
||||
if len(ctx.Users) == 0 || ctx.Rule == nil || ctx.Event == nil {
|
||||
if len(ctx.Users) == 0 || len(ctx.Events) == 0 {
|
||||
return
|
||||
}
|
||||
urls := ws.extract(ctx.Users)
|
||||
message := BuildTplMessage(ws.tpl, ctx.Event)
|
||||
message := BuildTplMessage(models.Wecom, ws.tpl, ctx.Events)
|
||||
for _, url := range urls {
|
||||
body := wecom{
|
||||
Msgtype: "markdown",
|
||||
@@ -37,7 +54,7 @@ func (ws *WecomSender) Send(ctx MessageContext) {
|
||||
Content: message,
|
||||
},
|
||||
}
|
||||
ws.doSend(url, body)
|
||||
doSend(url, body, models.Wecom, ctx.Stats)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,7 +63,7 @@ func (ws *WecomSender) extract(users []*models.User) []string {
|
||||
for _, user := range users {
|
||||
if token, has := user.ExtractToken(models.Wecom); has {
|
||||
url := token
|
||||
if !strings.HasPrefix(token, "https://") {
|
||||
if !strings.HasPrefix(token, "https://") && !strings.HasPrefix(token, "http://") {
|
||||
url = "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=" + token
|
||||
}
|
||||
urls = append(urls, url)
|
||||
@@ -54,12 +71,3 @@ func (ws *WecomSender) extract(users []*models.User) []string {
|
||||
}
|
||||
return urls
|
||||
}
|
||||
|
||||
func (ws *WecomSender) doSend(url string, body wecom) {
|
||||
res, code, err := poster.PostJSON(url, time.Second*5, body, 3)
|
||||
if err != nil {
|
||||
logger.Errorf("wecom_sender: result=fail url=%s code=%d error=%v response=%s", url, code, err, string(res))
|
||||
} else {
|
||||
logger.Infof("wecom_sender: result=succ url=%s code=%d response=%s", url, code, string(res))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package cconf
|
||||
|
||||
import "time"
|
||||
|
||||
type Center struct {
|
||||
Plugins []Plugin
|
||||
MetricsYamlFile string
|
||||
@@ -8,6 +10,9 @@ type Center struct {
|
||||
I18NHeaderKey string
|
||||
MetricDesc MetricDescType
|
||||
AnonymousAccess AnonymousAccess
|
||||
UseFileAssets bool
|
||||
FlashDuty FlashDuty
|
||||
EventHistoryGroupView bool
|
||||
}
|
||||
|
||||
type Plugin struct {
|
||||
@@ -17,6 +22,12 @@ type Plugin struct {
|
||||
TypeName string `json:"plugin_type_name"`
|
||||
}
|
||||
|
||||
type FlashDuty struct {
|
||||
Api string
|
||||
Headers map[string]string
|
||||
Timeout time.Duration
|
||||
}
|
||||
|
||||
type AnonymousAccess struct {
|
||||
PromQuerier bool
|
||||
AlertDetail bool
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
package cconf
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path"
|
||||
|
||||
"github.com/toolkits/pkg/file"
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
var Operations = Operation{}
|
||||
@@ -26,6 +28,14 @@ func LoadOpsYaml(configDir string, opsYamlFile string) error {
|
||||
if !file.IsExist(fp) {
|
||||
return nil
|
||||
}
|
||||
|
||||
hash, _ := file.MD5(fp)
|
||||
if hash == "2f91a9ed265cf2024e266dc1d538ee77" {
|
||||
// ops.yaml 是老的默认文件,删除
|
||||
file.Remove(fp)
|
||||
return nil
|
||||
}
|
||||
|
||||
return file.ReadYaml(fp, &Operations)
|
||||
}
|
||||
|
||||
@@ -36,3 +46,160 @@ func GetAllOps(ops []Ops) []string {
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func MergeOperationConf() error {
|
||||
opsBuiltIn := Operation{}
|
||||
err := yaml.Unmarshal([]byte(builtInOps), &opsBuiltIn)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot parse builtInOps: %s", err.Error())
|
||||
}
|
||||
configOpsMap := make(map[string]struct{})
|
||||
for _, op := range Operations.Ops {
|
||||
configOpsMap[op.Name] = struct{}{}
|
||||
}
|
||||
//If the opBu.Name is not a constant in the target (Operations.Ops), add Ops from the built-in options
|
||||
for _, opBu := range opsBuiltIn.Ops {
|
||||
if _, has := configOpsMap[opBu.Name]; !has {
|
||||
Operations.Ops = append(Operations.Ops, opBu)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
const (
|
||||
builtInOps = `
|
||||
ops:
|
||||
- name: dashboards
|
||||
cname: 仪表盘
|
||||
ops:
|
||||
- "/dashboards"
|
||||
- "/dashboards/add"
|
||||
- "/dashboards/put"
|
||||
- "/dashboards/del"
|
||||
- "/embedded-dashboards/put"
|
||||
- "/embedded-dashboards"
|
||||
|
||||
- name: alert
|
||||
cname: 告警规则
|
||||
ops:
|
||||
- "/alert-rules"
|
||||
- "/alert-rules/add"
|
||||
- "/alert-rules/put"
|
||||
- "/alert-rules/del"
|
||||
|
||||
- name: alert-mutes
|
||||
cname: 告警静默管理
|
||||
ops:
|
||||
- "/alert-mutes"
|
||||
- "/alert-mutes/add"
|
||||
- "/alert-mutes/put"
|
||||
- "/alert-mutes/del"
|
||||
|
||||
- name: alert-subscribes
|
||||
cname: 告警订阅管理
|
||||
ops:
|
||||
- "/alert-subscribes"
|
||||
- "/alert-subscribes/add"
|
||||
- "/alert-subscribes/put"
|
||||
- "/alert-subscribes/del"
|
||||
|
||||
- name: alert-events
|
||||
cname: 告警事件管理
|
||||
ops:
|
||||
- "/alert-cur-events"
|
||||
- "/alert-cur-events/del"
|
||||
- "/alert-his-events"
|
||||
|
||||
- name: recording-rules
|
||||
cname: 记录规则管理
|
||||
ops:
|
||||
- "/recording-rules"
|
||||
- "/recording-rules/add"
|
||||
- "/recording-rules/put"
|
||||
- "/recording-rules/del"
|
||||
|
||||
- name: metric
|
||||
cname: 时序指标
|
||||
ops:
|
||||
- "/metric/explorer"
|
||||
- "/object/explorer"
|
||||
|
||||
- name: log
|
||||
cname: 日志分析
|
||||
ops:
|
||||
- "/log/explorer"
|
||||
- "/log/index-patterns"
|
||||
|
||||
- name: targets
|
||||
cname: 基础设施
|
||||
ops:
|
||||
- "/targets"
|
||||
- "/targets/add"
|
||||
- "/targets/put"
|
||||
- "/targets/del"
|
||||
- "/targets/bind"
|
||||
|
||||
- name: job
|
||||
cname: 任务管理
|
||||
ops:
|
||||
- "/job-tpls"
|
||||
- "/job-tpls/add"
|
||||
- "/job-tpls/put"
|
||||
- "/job-tpls/del"
|
||||
- "/job-tasks"
|
||||
- "/job-tasks/add"
|
||||
- "/job-tasks/put"
|
||||
- "/ibex-settings"
|
||||
|
||||
- name: user
|
||||
cname: 用户管理
|
||||
ops:
|
||||
- "/users"
|
||||
- "/user-groups"
|
||||
- "/user-groups/add"
|
||||
- "/user-groups/put"
|
||||
- "/user-groups/del"
|
||||
|
||||
- name: permissions
|
||||
cname: 权限管理
|
||||
ops:
|
||||
- "/permissions"
|
||||
|
||||
- name: busi-groups
|
||||
cname: 业务分组管理
|
||||
ops:
|
||||
- "/busi-groups"
|
||||
- "/busi-groups/add"
|
||||
- "/busi-groups/put"
|
||||
- "/busi-groups/del"
|
||||
|
||||
- name: builtin-metrics
|
||||
cname: 指标视图
|
||||
ops:
|
||||
- "/metrics-built-in"
|
||||
- "/builtin-metrics/add"
|
||||
- "/builtin-metrics/put"
|
||||
- "/builtin-metrics/del"
|
||||
|
||||
- name: built-in-components
|
||||
cname: 模版中心
|
||||
ops:
|
||||
- "/built-in-components"
|
||||
- "/built-in-components/add"
|
||||
- "/built-in-components/put"
|
||||
- "/built-in-components/del"
|
||||
|
||||
- name: system
|
||||
cname: 系统信息
|
||||
ops:
|
||||
- "/help/variable-configs"
|
||||
- "/help/version"
|
||||
- "/help/servers"
|
||||
- "/help/source"
|
||||
- "/help/sso"
|
||||
- "/help/notification-tpls"
|
||||
- "/help/notification-settings"
|
||||
- "/help/migrate"
|
||||
- "/site-settings"
|
||||
`
|
||||
)
|
||||
|
||||
@@ -15,8 +15,14 @@ var Plugins = []Plugin{
|
||||
},
|
||||
{
|
||||
Id: 3,
|
||||
Category: "logging",
|
||||
Type: "jaeger",
|
||||
TypeName: "Jaeger",
|
||||
Category: "loki",
|
||||
Type: "loki",
|
||||
TypeName: "Loki",
|
||||
},
|
||||
{
|
||||
Id: 4,
|
||||
Category: "timeseries",
|
||||
Type: "tdengine",
|
||||
TypeName: "TDengine",
|
||||
},
|
||||
}
|
||||
|
||||
105
center/cconf/rsa/rsa_conf.go
Normal file
105
center/cconf/rsa/rsa_conf.go
Normal file
@@ -0,0 +1,105 @@
|
||||
package rsa
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/ccfos/nightingale/v6/models"
|
||||
"github.com/ccfos/nightingale/v6/pkg/ctx"
|
||||
"github.com/ccfos/nightingale/v6/pkg/httpx"
|
||||
"github.com/ccfos/nightingale/v6/pkg/secu"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/toolkits/pkg/file"
|
||||
"github.com/toolkits/pkg/logger"
|
||||
)
|
||||
|
||||
func InitRSAConfig(ctx *ctx.Context, rsaConfig *httpx.RSAConfig) error {
|
||||
|
||||
// 1.Load RSA keys from Database
|
||||
rsaPassWord, err := models.ConfigsGet(ctx, models.RSA_PASSWORD)
|
||||
if err != nil {
|
||||
return errors.WithMessagef(err, "cannot query config(%s)", models.RSA_PASSWORD)
|
||||
}
|
||||
privateKeyVal, err := models.ConfigsGet(ctx, models.RSA_PRIVATE_KEY)
|
||||
if err != nil {
|
||||
return errors.WithMessagef(err, "cannot query config(%s)", models.RSA_PRIVATE_KEY)
|
||||
}
|
||||
publicKeyVal, err := models.ConfigsGet(ctx, models.RSA_PUBLIC_KEY)
|
||||
if err != nil {
|
||||
return errors.WithMessagef(err, "cannot query config(%s)", models.RSA_PUBLIC_KEY)
|
||||
}
|
||||
if rsaPassWord != "" && privateKeyVal != "" && publicKeyVal != "" {
|
||||
rsaConfig.RSAPassWord = rsaPassWord
|
||||
rsaConfig.RSAPrivateKey = []byte(privateKeyVal)
|
||||
rsaConfig.RSAPublicKey = []byte(publicKeyVal)
|
||||
return nil
|
||||
}
|
||||
|
||||
// 2.Read RSA configuration from file if exists
|
||||
if file.IsExist(rsaConfig.RSAPrivateKeyPath) && file.IsExist(rsaConfig.RSAPublicKeyPath) {
|
||||
//password already read from config
|
||||
rsaConfig.RSAPrivateKey, rsaConfig.RSAPublicKey, err = readConfigFile(rsaConfig)
|
||||
if err != nil {
|
||||
return errors.WithMessage(err, "failed to read rsa config from file")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
// 3.Generate RSA keys if not exist
|
||||
rsaConfig.RSAPassWord, rsaConfig.RSAPrivateKey, rsaConfig.RSAPublicKey, err = initRSAKeyPairs(ctx, rsaConfig.RSAPassWord)
|
||||
if err != nil {
|
||||
return errors.WithMessage(err, "failed to generate rsa key pair")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func initRSAKeyPairs(ctx *ctx.Context, rsaPassWord string) (password string, privateByte, publicByte []byte, err error) {
|
||||
|
||||
// Generate RSA keys
|
||||
|
||||
// Generate RSA password
|
||||
if rsaPassWord != "" {
|
||||
logger.Debug("Using existing RSA password")
|
||||
password = rsaPassWord
|
||||
err = models.ConfigsSet(ctx, models.RSA_PASSWORD, password)
|
||||
if err != nil {
|
||||
err = errors.WithMessagef(err, "failed to set config(%s)", models.RSA_PASSWORD)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
password, err = models.InitRSAPassWord(ctx)
|
||||
if err != nil {
|
||||
err = errors.WithMessage(err, "failed to generate rsa password")
|
||||
return
|
||||
}
|
||||
}
|
||||
privateByte, publicByte, err = secu.GenerateRsaKeyPair(password)
|
||||
if err != nil {
|
||||
err = errors.WithMessage(err, "failed to generate rsa key pair")
|
||||
return
|
||||
}
|
||||
// Save generated RSA keys
|
||||
err = models.ConfigsSet(ctx, models.RSA_PRIVATE_KEY, string(privateByte))
|
||||
if err != nil {
|
||||
err = errors.WithMessagef(err, "failed to set config(%s)", models.RSA_PRIVATE_KEY)
|
||||
return
|
||||
}
|
||||
err = models.ConfigsSet(ctx, models.RSA_PUBLIC_KEY, string(publicByte))
|
||||
if err != nil {
|
||||
err = errors.WithMessagef(err, "failed to set config(%s)", models.RSA_PUBLIC_KEY)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func readConfigFile(rsaConfig *httpx.RSAConfig) (privateBuf, publicBuf []byte, err error) {
|
||||
publicBuf, err = os.ReadFile(rsaConfig.RSAPublicKeyPath)
|
||||
if err != nil {
|
||||
err = errors.WithMessagef(err, "could not read RSAPublicKeyPath %q", rsaConfig.RSAPublicKeyPath)
|
||||
return
|
||||
}
|
||||
privateBuf, err = os.ReadFile(rsaConfig.RSAPrivateKeyPath)
|
||||
if err != nil {
|
||||
err = errors.WithMessagef(err, "could not read RSAPrivateKeyPath %q", rsaConfig.RSAPrivateKeyPath)
|
||||
}
|
||||
return
|
||||
}
|
||||
15
center/cconf/sql_tpl.go
Normal file
15
center/cconf/sql_tpl.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package cconf
|
||||
|
||||
var TDengineSQLTpl = map[string]string{
|
||||
"load5": "SELECT _wstart as ts, last(load5) FROM $database.system WHERE host = '$server' and _ts >= $from and _ts <= $to interval($interval) fill(null)",
|
||||
"process_total": "SELECT _wstart as ts, last(total) FROM $database.processes WHERE host = '$server' and _ts >= $from and _ts <= $to interval($interval) fill(null)",
|
||||
"thread_total": "SELECT _wstart as ts, last(total) FROM $database.threads WHERE host = '$server' and _ts >= $from and _ts <= $to interval($interval) fill(null)",
|
||||
"cpu_idle": "SELECT _wstart as ts, last(usage_idle) * -1 + 100 FROM $database.cpu WHERE (host = '$server' and cpu = 'cpu-total') and _ts >= $from and _ts <= $to interval($interval) fill(null)",
|
||||
"mem_used_percent": "SELECT _wstart as ts, last(used_percent) FROM $database.mem WHERE (host = '$server') and _ts >= $from and _ts <= $to interval($interval) fill(null)",
|
||||
"disk_used_percent": "SELECT _wstart as ts, last(used_percent) FROM $database.disk WHERE (host = '$server' and path = '/') and _ts >= $from and _ts <= $to interval($interval) fill(null)",
|
||||
"cpu_context_switches": "select ts, derivative(context_switches, 1s, 0) as context FROM (SELECT _wstart as ts, avg(context_switches) as context_switches FROM $database.kernel WHERE host = '$server' and _ts >= $from and _ts <= $to interval($interval) )",
|
||||
"tcp": "SELECT _wstart as ts, avg(tcp_close) as CLOSED, avg(tcp_close_wait) as CLOSE_WAIT, avg(tcp_closing) as CLOSING, avg(tcp_established) as ESTABLISHED, avg(tcp_fin_wait1) as FIN_WAIT1, avg(tcp_fin_wait2) as FIN_WAIT2, avg(tcp_last_ack) as LAST_ACK, avg(tcp_syn_recv) as SYN_RECV, avg(tcp_syn_sent) as SYN_SENT, avg(tcp_time_wait) as TIME_WAIT FROM $database.netstat WHERE host = '$server' and _ts >= $from and _ts <= $to interval($interval)",
|
||||
"net_bytes_recv": "SELECT _wstart as ts, derivative(bytes_recv,1s, 1) as bytes_in FROM $database.net WHERE host = '$server' and interface = '$netif' and _ts >= $from and _ts <= $to group by tbname",
|
||||
"net_bytes_sent": "SELECT _wstart as ts, derivative(bytes_sent,1s, 1) as bytes_out FROM $database.net WHERE host = '$server' and interface = '$netif' and _ts >= $from and _ts <= $to group by tbname",
|
||||
"disk_total": "SELECT _wstart as ts, avg(total) AS total, avg(used) as used FROM $database.disk WHERE path = '$mountpoint' and _ts >= $from and _ts <= $to interval($interval) group by host",
|
||||
}
|
||||
@@ -7,24 +7,33 @@ import (
|
||||
"github.com/ccfos/nightingale/v6/alert"
|
||||
"github.com/ccfos/nightingale/v6/alert/astats"
|
||||
"github.com/ccfos/nightingale/v6/alert/process"
|
||||
alertrt "github.com/ccfos/nightingale/v6/alert/router"
|
||||
"github.com/ccfos/nightingale/v6/center/cconf"
|
||||
"github.com/ccfos/nightingale/v6/center/cconf/rsa"
|
||||
"github.com/ccfos/nightingale/v6/center/cstats"
|
||||
"github.com/ccfos/nightingale/v6/center/integration"
|
||||
"github.com/ccfos/nightingale/v6/center/metas"
|
||||
centerrt "github.com/ccfos/nightingale/v6/center/router"
|
||||
"github.com/ccfos/nightingale/v6/center/sso"
|
||||
"github.com/ccfos/nightingale/v6/conf"
|
||||
"github.com/ccfos/nightingale/v6/dumper"
|
||||
"github.com/ccfos/nightingale/v6/memsto"
|
||||
"github.com/ccfos/nightingale/v6/models"
|
||||
"github.com/ccfos/nightingale/v6/models/migrate"
|
||||
"github.com/ccfos/nightingale/v6/pkg/ctx"
|
||||
"github.com/ccfos/nightingale/v6/pkg/flashduty"
|
||||
"github.com/ccfos/nightingale/v6/pkg/httpx"
|
||||
"github.com/ccfos/nightingale/v6/pkg/i18nx"
|
||||
"github.com/ccfos/nightingale/v6/pkg/logx"
|
||||
"github.com/ccfos/nightingale/v6/pkg/version"
|
||||
"github.com/ccfos/nightingale/v6/prom"
|
||||
"github.com/ccfos/nightingale/v6/pushgw/idents"
|
||||
pushgwrt "github.com/ccfos/nightingale/v6/pushgw/router"
|
||||
"github.com/ccfos/nightingale/v6/pushgw/writer"
|
||||
"github.com/ccfos/nightingale/v6/storage"
|
||||
"github.com/ccfos/nightingale/v6/tdengine"
|
||||
|
||||
alertrt "github.com/ccfos/nightingale/v6/alert/router"
|
||||
centerrt "github.com/ccfos/nightingale/v6/center/router"
|
||||
pushgwrt "github.com/ccfos/nightingale/v6/pushgw/router"
|
||||
"github.com/flashcatcloud/ibex/src/cmd/ibex"
|
||||
)
|
||||
|
||||
func Initialize(configDir string, cryptoKey string) (func(), error) {
|
||||
@@ -36,56 +45,83 @@ func Initialize(configDir string, cryptoKey string) (func(), error) {
|
||||
cconf.LoadMetricsYaml(configDir, config.Center.MetricsYamlFile)
|
||||
cconf.LoadOpsYaml(configDir, config.Center.OpsYamlFile)
|
||||
|
||||
cconf.MergeOperationConf()
|
||||
|
||||
logxClean, err := logx.Init(config.Log)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
i18nx.Init()
|
||||
i18nx.Init(configDir)
|
||||
cstats.Init()
|
||||
flashduty.Init(config.Center.FlashDuty)
|
||||
|
||||
db, err := storage.New(config.DB)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ctx := ctx.NewContext(context.Background(), db, true)
|
||||
migrate.Migrate(db)
|
||||
models.InitRoot(ctx)
|
||||
|
||||
redis, err := storage.NewRedis(config.Redis)
|
||||
config.HTTP.JWTAuth.SigningKey = models.InitJWTSigningKey(ctx)
|
||||
|
||||
err = rsa.InitRSAConfig(ctx, &config.HTTP.RSA)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
integration.Init(ctx, config.Center.BuiltinIntegrationsDir)
|
||||
var redis storage.Redis
|
||||
redis, err = storage.NewRedis(config.Redis)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
metas := metas.New(redis)
|
||||
idents := idents.New(ctx)
|
||||
idents := idents.New(ctx, redis)
|
||||
|
||||
syncStats := memsto.NewSyncStats()
|
||||
alertStats := astats.NewSyncStats()
|
||||
|
||||
sso := sso.Init(config.Center, ctx)
|
||||
|
||||
configCache := memsto.NewConfigCache(ctx, syncStats, config.HTTP.RSA.RSAPrivateKey, config.HTTP.RSA.RSAPassWord)
|
||||
busiGroupCache := memsto.NewBusiGroupCache(ctx, syncStats)
|
||||
targetCache := memsto.NewTargetCache(ctx, syncStats, redis)
|
||||
dsCache := memsto.NewDatasourceCache(ctx, syncStats)
|
||||
alertMuteCache := memsto.NewAlertMuteCache(ctx, syncStats)
|
||||
alertRuleCache := memsto.NewAlertRuleCache(ctx, syncStats)
|
||||
notifyConfigCache := memsto.NewNotifyConfigCache(ctx)
|
||||
notifyConfigCache := memsto.NewNotifyConfigCache(ctx, configCache)
|
||||
userCache := memsto.NewUserCache(ctx, syncStats)
|
||||
userGroupCache := memsto.NewUserGroupCache(ctx, syncStats)
|
||||
taskTplCache := memsto.NewTaskTplCache(ctx)
|
||||
|
||||
promClients := prom.NewPromClient(ctx, config.Alert.Heartbeat)
|
||||
sso := sso.Init(config.Center, ctx, configCache)
|
||||
promClients := prom.NewPromClient(ctx)
|
||||
tdengineClients := tdengine.NewTdengineClient(ctx, config.Alert.Heartbeat)
|
||||
|
||||
externalProcessors := process.NewExternalProcessors()
|
||||
alert.Start(config.Alert, config.Pushgw, syncStats, alertStats, externalProcessors, targetCache, busiGroupCache, alertMuteCache, alertRuleCache, notifyConfigCache, dsCache, ctx, promClients)
|
||||
alert.Start(config.Alert, config.Pushgw, syncStats, alertStats, externalProcessors, targetCache, busiGroupCache, alertMuteCache, alertRuleCache, notifyConfigCache, taskTplCache, dsCache, ctx, promClients, tdengineClients, userCache, userGroupCache)
|
||||
|
||||
writers := writer.NewWriters(config.Pushgw)
|
||||
|
||||
go version.GetGithubVersion()
|
||||
|
||||
alertrtRouter := alertrt.New(config.HTTP, config.Alert, alertMuteCache, targetCache, busiGroupCache, alertStats, ctx, externalProcessors)
|
||||
centerRouter := centerrt.New(config.HTTP, config.Center, cconf.Operations, dsCache, notifyConfigCache, promClients, redis, sso, ctx, metas, targetCache)
|
||||
pushgwRouter := pushgwrt.New(config.HTTP, config.Pushgw, targetCache, busiGroupCache, idents, writers, ctx)
|
||||
centerRouter := centerrt.New(config.HTTP, config.Center, config.Alert, cconf.Operations, dsCache, notifyConfigCache, promClients, tdengineClients,
|
||||
redis, sso, ctx, metas, idents, targetCache, userCache, userGroupCache)
|
||||
pushgwRouter := pushgwrt.New(config.HTTP, config.Pushgw, config.Alert, targetCache, busiGroupCache, idents, metas, writers, ctx)
|
||||
|
||||
r := httpx.GinEngine(config.Global.RunMode, config.HTTP)
|
||||
|
||||
centerRouter.Config(r)
|
||||
alertrtRouter.Config(r)
|
||||
pushgwRouter.Config(r)
|
||||
dumper.ConfigRouter(r)
|
||||
|
||||
if config.Ibex.Enable {
|
||||
migrate.MigrateIbexTables(db)
|
||||
ibex.ServerStart(true, db, redis, config.HTTP.APIForService.BasicAuth, config.Alert.Heartbeat, &config.CenterApi, r, centerRouter, config.Ibex, config.HTTP.Port)
|
||||
}
|
||||
|
||||
httpClean := httpx.Init(config.HTTP, r)
|
||||
|
||||
|
||||
365
center/integration/init.go
Normal file
365
center/integration/init.go
Normal file
@@ -0,0 +1,365 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"path"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/ccfos/nightingale/v6/models"
|
||||
"github.com/ccfos/nightingale/v6/pkg/ctx"
|
||||
"github.com/toolkits/pkg/file"
|
||||
"github.com/toolkits/pkg/logger"
|
||||
"github.com/toolkits/pkg/runner"
|
||||
)
|
||||
|
||||
const SYSTEM = "system"
|
||||
|
||||
func Init(ctx *ctx.Context, builtinIntegrationsDir string) {
|
||||
fp := builtinIntegrationsDir
|
||||
if fp == "" {
|
||||
fp = path.Join(runner.Cwd, "integrations")
|
||||
}
|
||||
|
||||
// var fileList []string
|
||||
dirList, err := file.DirsUnder(fp)
|
||||
if err != nil {
|
||||
logger.Warning("read builtin component dir fail ", err)
|
||||
return
|
||||
}
|
||||
|
||||
for _, dir := range dirList {
|
||||
// components icon
|
||||
componentDir := fp + "/" + dir
|
||||
component := models.BuiltinComponent{
|
||||
Ident: dir,
|
||||
}
|
||||
|
||||
// get logo name
|
||||
// /api/n9e/integrations/icon/AliYun/aliyun.png
|
||||
files, err := file.FilesUnder(componentDir + "/icon")
|
||||
if err == nil && len(files) > 0 {
|
||||
component.Logo = "/api/n9e/integrations/icon/" + component.Ident + "/" + files[0]
|
||||
} else if err != nil {
|
||||
logger.Warningf("read builtin component icon dir fail %s %v", component.Ident, err)
|
||||
}
|
||||
|
||||
// get description
|
||||
files, err = file.FilesUnder(componentDir + "/markdown")
|
||||
if err == nil && len(files) > 0 {
|
||||
var readmeFile string
|
||||
for _, file := range files {
|
||||
if strings.HasSuffix(strings.ToLower(file), "md") {
|
||||
readmeFile = componentDir + "/markdown/" + file
|
||||
break
|
||||
}
|
||||
}
|
||||
if readmeFile != "" {
|
||||
component.Readme, _ = file.ReadString(readmeFile)
|
||||
}
|
||||
} else if err != nil {
|
||||
logger.Warningf("read builtin component markdown dir fail %s %v", component.Ident, err)
|
||||
}
|
||||
|
||||
exists, _ := models.BuiltinComponentExists(ctx, &component)
|
||||
if !exists {
|
||||
err = component.Add(ctx, SYSTEM)
|
||||
if err != nil {
|
||||
logger.Warning("add builtin component fail ", component, err)
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
old, err := models.BuiltinComponentGet(ctx, "ident = ?", component.Ident)
|
||||
if err != nil {
|
||||
logger.Warning("get builtin component fail ", component, err)
|
||||
continue
|
||||
}
|
||||
|
||||
if old == nil {
|
||||
logger.Warning("get builtin component nil ", component)
|
||||
continue
|
||||
}
|
||||
|
||||
if old.UpdatedBy == SYSTEM {
|
||||
now := time.Now().Unix()
|
||||
old.CreatedAt = now
|
||||
old.UpdatedAt = now
|
||||
old.Readme = component.Readme
|
||||
old.UpdatedBy = SYSTEM
|
||||
|
||||
err = models.DB(ctx).Model(old).Select("*").Updates(old).Error
|
||||
if err != nil {
|
||||
logger.Warning("update builtin component fail ", old, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// delete uuid is emtpy
|
||||
err = models.DB(ctx).Exec("delete from builtin_payloads where uuid = 0 and type != 'collect' and (updated_by = 'system' or updated_by = '')").Error
|
||||
if err != nil {
|
||||
logger.Warning("delete builtin payloads fail ", err)
|
||||
}
|
||||
|
||||
// delete builtin metrics uuid is emtpy
|
||||
err = models.DB(ctx).Exec("delete from builtin_metrics where uuid = 0 and (updated_by = 'system' or updated_by = '')").Error
|
||||
if err != nil {
|
||||
logger.Warning("delete builtin metrics fail ", err)
|
||||
}
|
||||
|
||||
// alerts
|
||||
files, err = file.FilesUnder(componentDir + "/alerts")
|
||||
if err == nil && len(files) > 0 {
|
||||
for _, f := range files {
|
||||
fp := componentDir + "/alerts/" + f
|
||||
bs, err := file.ReadBytes(fp)
|
||||
if err != nil {
|
||||
logger.Warning("read builtin component alerts file fail ", f, err)
|
||||
continue
|
||||
}
|
||||
|
||||
alerts := []models.AlertRule{}
|
||||
err = json.Unmarshal(bs, &alerts)
|
||||
if err != nil {
|
||||
logger.Warning("parse builtin component alerts file fail ", f, err)
|
||||
continue
|
||||
}
|
||||
|
||||
newAlerts := []models.AlertRule{}
|
||||
writeAlertFileFlag := false
|
||||
for _, alert := range alerts {
|
||||
if alert.UUID == 0 {
|
||||
writeAlertFileFlag = true
|
||||
alert.UUID = time.Now().UnixNano()
|
||||
}
|
||||
|
||||
newAlerts = append(newAlerts, alert)
|
||||
content, err := json.Marshal(alert)
|
||||
if err != nil {
|
||||
logger.Warning("marshal builtin alert fail ", alert, err)
|
||||
continue
|
||||
}
|
||||
|
||||
cate := strings.Replace(f, ".json", "", -1)
|
||||
builtinAlert := models.BuiltinPayload{
|
||||
Component: component.Ident,
|
||||
Type: "alert",
|
||||
Cate: cate,
|
||||
Name: alert.Name,
|
||||
Tags: alert.AppendTags,
|
||||
Content: string(content),
|
||||
UUID: alert.UUID,
|
||||
}
|
||||
|
||||
old, err := models.BuiltinPayloadGet(ctx, "uuid = ?", alert.UUID)
|
||||
if err != nil {
|
||||
logger.Warning("get builtin alert fail ", builtinAlert, err)
|
||||
continue
|
||||
}
|
||||
|
||||
if old == nil {
|
||||
err := builtinAlert.Add(ctx, SYSTEM)
|
||||
if err != nil {
|
||||
logger.Warning("add builtin alert fail ", builtinAlert, err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if old.UpdatedBy == SYSTEM {
|
||||
old.Content = string(content)
|
||||
old.Name = alert.Name
|
||||
old.Tags = alert.AppendTags
|
||||
err = models.DB(ctx).Model(old).Select("*").Updates(old).Error
|
||||
if err != nil {
|
||||
logger.Warningf("update builtin alert:%+v fail %v", builtinAlert, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if writeAlertFileFlag {
|
||||
bs, err = json.MarshalIndent(newAlerts, "", " ")
|
||||
if err != nil {
|
||||
logger.Warning("marshal builtin alerts fail ", newAlerts, err)
|
||||
continue
|
||||
}
|
||||
|
||||
_, err = file.WriteBytes(fp, bs)
|
||||
if err != nil {
|
||||
logger.Warning("write builtin alerts file fail ", f, err)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// dashboards
|
||||
files, err = file.FilesUnder(componentDir + "/dashboards")
|
||||
if err == nil && len(files) > 0 {
|
||||
for _, f := range files {
|
||||
fp := componentDir + "/dashboards/" + f
|
||||
bs, err := file.ReadBytes(fp)
|
||||
if err != nil {
|
||||
logger.Warning("read builtin component dashboards file fail ", f, err)
|
||||
continue
|
||||
}
|
||||
|
||||
dashboard := BuiltinBoard{}
|
||||
err = json.Unmarshal(bs, &dashboard)
|
||||
if err != nil {
|
||||
logger.Warning("parse builtin component dashboards file fail ", f, err)
|
||||
continue
|
||||
}
|
||||
|
||||
if dashboard.UUID == 0 {
|
||||
dashboard.UUID = time.Now().UnixNano()
|
||||
// 补全文件中的 uuid
|
||||
bs, err = json.MarshalIndent(dashboard, "", " ")
|
||||
if err != nil {
|
||||
logger.Warning("marshal builtin dashboard fail ", dashboard, err)
|
||||
continue
|
||||
}
|
||||
|
||||
_, err = file.WriteBytes(fp, bs)
|
||||
if err != nil {
|
||||
logger.Warning("write builtin dashboard file fail ", f, err)
|
||||
}
|
||||
}
|
||||
|
||||
content, err := json.Marshal(dashboard)
|
||||
if err != nil {
|
||||
logger.Warning("marshal builtin dashboard fail ", dashboard, err)
|
||||
continue
|
||||
}
|
||||
|
||||
builtinDashboard := models.BuiltinPayload{
|
||||
Component: component.Ident,
|
||||
Type: "dashboard",
|
||||
Cate: "",
|
||||
Name: dashboard.Name,
|
||||
Tags: dashboard.Tags,
|
||||
Content: string(content),
|
||||
UUID: dashboard.UUID,
|
||||
}
|
||||
|
||||
old, err := models.BuiltinPayloadGet(ctx, "uuid = ?", dashboard.UUID)
|
||||
if err != nil {
|
||||
logger.Warning("get builtin alert fail ", builtinDashboard, err)
|
||||
continue
|
||||
}
|
||||
|
||||
if old == nil {
|
||||
err := builtinDashboard.Add(ctx, SYSTEM)
|
||||
if err != nil {
|
||||
logger.Warning("add builtin alert fail ", builtinDashboard, err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if old.UpdatedBy == SYSTEM {
|
||||
old.Content = string(content)
|
||||
old.Name = dashboard.Name
|
||||
old.Tags = dashboard.Tags
|
||||
err = models.DB(ctx).Model(old).Select("*").Updates(old).Error
|
||||
if err != nil {
|
||||
logger.Warningf("update builtin alert:%+v fail %v", builtinDashboard, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if err != nil {
|
||||
logger.Warningf("read builtin component dash dir fail %s %v", component.Ident, err)
|
||||
}
|
||||
|
||||
// metrics
|
||||
files, err = file.FilesUnder(componentDir + "/metrics")
|
||||
if err == nil && len(files) > 0 {
|
||||
for _, f := range files {
|
||||
fp := componentDir + "/metrics/" + f
|
||||
bs, err := file.ReadBytes(fp)
|
||||
if err != nil {
|
||||
logger.Warning("read builtin component metrics file fail", f, err)
|
||||
continue
|
||||
}
|
||||
|
||||
metrics := []models.BuiltinMetric{}
|
||||
newMetrics := []models.BuiltinMetric{}
|
||||
err = json.Unmarshal(bs, &metrics)
|
||||
if err != nil {
|
||||
logger.Warning("parse builtin component metrics file fail", f, err)
|
||||
continue
|
||||
}
|
||||
|
||||
writeMetricFileFlag := false
|
||||
for _, metric := range metrics {
|
||||
if metric.UUID == 0 {
|
||||
writeMetricFileFlag = true
|
||||
metric.UUID = time.Now().UnixNano()
|
||||
}
|
||||
newMetrics = append(newMetrics, metric)
|
||||
|
||||
old, err := models.BuiltinMetricGet(ctx, "uuid = ?", metric.UUID)
|
||||
if err != nil {
|
||||
logger.Warning("get builtin metrics fail ", metric, err)
|
||||
continue
|
||||
}
|
||||
|
||||
if old == nil {
|
||||
err := metric.Add(ctx, SYSTEM)
|
||||
if err != nil {
|
||||
logger.Warning("add builtin metrics fail ", metric, err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if old.UpdatedBy == SYSTEM {
|
||||
old.Collector = metric.Collector
|
||||
old.Typ = metric.Typ
|
||||
old.Name = metric.Name
|
||||
old.Unit = metric.Unit
|
||||
old.Note = metric.Note
|
||||
old.Lang = metric.Lang
|
||||
old.Expression = metric.Expression
|
||||
|
||||
err = models.DB(ctx).Model(old).Select("*").Updates(old).Error
|
||||
if err != nil {
|
||||
logger.Warningf("update builtin metric:%+v fail %v", metric, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if writeMetricFileFlag {
|
||||
bs, err = json.MarshalIndent(newMetrics, "", " ")
|
||||
if err != nil {
|
||||
logger.Warning("marshal builtin metrics fail ", newMetrics, err)
|
||||
continue
|
||||
}
|
||||
|
||||
_, err = file.WriteBytes(fp, bs)
|
||||
if err != nil {
|
||||
logger.Warning("write builtin metrics file fail ", f, err)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
} else if err != nil {
|
||||
logger.Warningf("read builtin component metrics dir fail %s %v", component.Ident, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type BuiltinBoard struct {
|
||||
Id int64 `json:"id" gorm:"primaryKey"`
|
||||
GroupId int64 `json:"group_id"`
|
||||
Name string `json:"name"`
|
||||
Ident string `json:"ident"`
|
||||
Tags string `json:"tags"`
|
||||
CreateAt int64 `json:"create_at"`
|
||||
CreateBy string `json:"create_by"`
|
||||
UpdateAt int64 `json:"update_at"`
|
||||
UpdateBy string `json:"update_by"`
|
||||
Configs interface{} `json:"configs" gorm:"-"`
|
||||
Public int `json:"public"` // 0: false, 1: true
|
||||
PublicCate int `json:"public_cate"` // 0: anonymous, 1: login, 2: busi
|
||||
Bgids []int64 `json:"bgids" gorm:"-"`
|
||||
BuiltIn int `json:"built_in"` // 0: false, 1: true
|
||||
Hide int `json:"hide"` // 0: false, 1: true
|
||||
UUID int64 `json:"uuid"`
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package metas
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -90,15 +91,41 @@ func (s *Set) updateMeta(items map[string]models.HostMeta) {
|
||||
}
|
||||
|
||||
func (s *Set) updateTargets(m map[string]models.HostMeta) error {
|
||||
if s.redis == nil {
|
||||
logger.Warningf("redis is nil")
|
||||
return nil
|
||||
}
|
||||
|
||||
count := int64(len(m))
|
||||
if count == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
newMap := make(map[string]interface{}, count)
|
||||
extendMap := make(map[string]interface{})
|
||||
for ident, meta := range m {
|
||||
if meta.ExtendInfo != nil {
|
||||
extendMeta := meta.ExtendInfo
|
||||
meta.ExtendInfo = make(map[string]interface{})
|
||||
extendMetaStr, err := json.Marshal(extendMeta)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
extendMap[models.WrapExtendIdent(ident)] = extendMetaStr
|
||||
}
|
||||
newMap[models.WrapIdent(ident)] = meta
|
||||
}
|
||||
err := storage.MSet(context.Background(), s.redis, newMap)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(extendMap) > 0 {
|
||||
err = storage.MSet(context.Background(), s.redis, extendMap)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -3,9 +3,12 @@ package router
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"path"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/ccfos/nightingale/v6/alert/aconf"
|
||||
"github.com/ccfos/nightingale/v6/center/cconf"
|
||||
"github.com/ccfos/nightingale/v6/center/cstats"
|
||||
"github.com/ccfos/nightingale/v6/center/metas"
|
||||
@@ -15,41 +18,55 @@ import (
|
||||
"github.com/ccfos/nightingale/v6/pkg/aop"
|
||||
"github.com/ccfos/nightingale/v6/pkg/ctx"
|
||||
"github.com/ccfos/nightingale/v6/pkg/httpx"
|
||||
"github.com/ccfos/nightingale/v6/pkg/version"
|
||||
"github.com/ccfos/nightingale/v6/prom"
|
||||
"github.com/ccfos/nightingale/v6/pushgw/idents"
|
||||
"github.com/ccfos/nightingale/v6/storage"
|
||||
"github.com/ccfos/nightingale/v6/tdengine"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/rakyll/statik/fs"
|
||||
"github.com/toolkits/pkg/ginx"
|
||||
"github.com/toolkits/pkg/logger"
|
||||
"github.com/toolkits/pkg/runner"
|
||||
)
|
||||
|
||||
type Router struct {
|
||||
HTTP httpx.Config
|
||||
Center cconf.Center
|
||||
Alert aconf.Alert
|
||||
Operations cconf.Operation
|
||||
DatasourceCache *memsto.DatasourceCacheType
|
||||
NotifyConfigCache *memsto.NotifyConfigCacheType
|
||||
PromClients *prom.PromClientMap
|
||||
TdendgineClients *tdengine.TdengineClientMap
|
||||
Redis storage.Redis
|
||||
MetaSet *metas.Set
|
||||
IdentSet *idents.Set
|
||||
TargetCache *memsto.TargetCacheType
|
||||
Sso *sso.SsoClient
|
||||
UserCache *memsto.UserCacheType
|
||||
UserGroupCache *memsto.UserGroupCacheType
|
||||
Ctx *ctx.Context
|
||||
}
|
||||
|
||||
func New(httpConfig httpx.Config, center cconf.Center, operations cconf.Operation, ds *memsto.DatasourceCacheType, ncc *memsto.NotifyConfigCacheType,
|
||||
pc *prom.PromClientMap, redis storage.Redis, sso *sso.SsoClient, ctx *ctx.Context, metaSet *metas.Set, tc *memsto.TargetCacheType) *Router {
|
||||
func New(httpConfig httpx.Config, center cconf.Center, alert aconf.Alert, operations cconf.Operation, ds *memsto.DatasourceCacheType, ncc *memsto.NotifyConfigCacheType, pc *prom.PromClientMap, tdendgineClients *tdengine.TdengineClientMap, redis storage.Redis, sso *sso.SsoClient, ctx *ctx.Context, metaSet *metas.Set, idents *idents.Set, tc *memsto.TargetCacheType, uc *memsto.UserCacheType, ugc *memsto.UserGroupCacheType) *Router {
|
||||
return &Router{
|
||||
HTTP: httpConfig,
|
||||
Center: center,
|
||||
Alert: alert,
|
||||
Operations: operations,
|
||||
DatasourceCache: ds,
|
||||
NotifyConfigCache: ncc,
|
||||
PromClients: pc,
|
||||
TdendgineClients: tdendgineClients,
|
||||
Redis: redis,
|
||||
MetaSet: metaSet,
|
||||
IdentSet: idents,
|
||||
TargetCache: tc,
|
||||
Sso: sso,
|
||||
UserCache: uc,
|
||||
UserGroupCache: ugc,
|
||||
Ctx: ctx,
|
||||
}
|
||||
}
|
||||
@@ -75,14 +92,14 @@ func languageDetector(i18NHeaderKey string) gin.HandlerFunc {
|
||||
lang := c.GetHeader(headerKey)
|
||||
if lang != "" {
|
||||
if strings.HasPrefix(lang, "zh") {
|
||||
c.Request.Header.Set("X-Language", "zh")
|
||||
c.Request.Header.Set("X-Language", "zh_CN")
|
||||
} else if strings.HasPrefix(lang, "en") {
|
||||
c.Request.Header.Set("X-Language", "en")
|
||||
} else {
|
||||
c.Request.Header.Set("X-Language", lang)
|
||||
}
|
||||
} else {
|
||||
c.Request.Header.Set("X-Language", "en")
|
||||
c.Request.Header.Set("X-Language", "zh_CN")
|
||||
}
|
||||
}
|
||||
c.Next()
|
||||
@@ -95,10 +112,32 @@ func (rt *Router) configNoRoute(r *gin.Engine, fs *http.FileSystem) {
|
||||
suffix := arr[len(arr)-1]
|
||||
|
||||
switch suffix {
|
||||
case "png", "jpeg", "jpg", "svg", "ico", "gif", "css", "js", "html", "htm", "gz", "zip", "map":
|
||||
c.FileFromFS(c.Request.URL.Path, *fs)
|
||||
case "png", "jpeg", "jpg", "svg", "ico", "gif", "css", "js", "html", "htm", "gz", "zip", "map", "ttf":
|
||||
if !rt.Center.UseFileAssets {
|
||||
c.FileFromFS(c.Request.URL.Path, *fs)
|
||||
} else {
|
||||
cwdarr := []string{"/"}
|
||||
if runtime.GOOS == "windows" {
|
||||
cwdarr[0] = ""
|
||||
}
|
||||
cwdarr = append(cwdarr, strings.Split(runner.Cwd, "/")...)
|
||||
cwdarr = append(cwdarr, "pub")
|
||||
cwdarr = append(cwdarr, strings.Split(c.Request.URL.Path, "/")...)
|
||||
c.File(path.Join(cwdarr...))
|
||||
}
|
||||
default:
|
||||
c.FileFromFS("/", *fs)
|
||||
if !rt.Center.UseFileAssets {
|
||||
c.FileFromFS("/", *fs)
|
||||
} else {
|
||||
cwdarr := []string{"/"}
|
||||
if runtime.GOOS == "windows" {
|
||||
cwdarr[0] = ""
|
||||
}
|
||||
cwdarr = append(cwdarr, strings.Split(runner.Cwd, "/")...)
|
||||
cwdarr = append(cwdarr, "pub")
|
||||
cwdarr = append(cwdarr, "index.html")
|
||||
c.File(path.Join(cwdarr...))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -113,7 +152,10 @@ func (rt *Router) Config(r *gin.Engine) {
|
||||
if err != nil {
|
||||
logger.Errorf("cannot create statik fs: %v", err)
|
||||
}
|
||||
r.StaticFS("/pub", statikFS)
|
||||
|
||||
if !rt.Center.UseFileAssets {
|
||||
r.StaticFS("/pub", statikFS)
|
||||
}
|
||||
|
||||
pagesPrefix := "/api/n9e"
|
||||
pages := r.Group(pagesPrefix)
|
||||
@@ -124,18 +166,38 @@ func (rt *Router) Config(r *gin.Engine) {
|
||||
pages.POST("/query-range-batch", rt.promBatchQueryRange)
|
||||
pages.POST("/query-instant-batch", rt.promBatchQueryInstant)
|
||||
pages.GET("/datasource/brief", rt.datasourceBriefs)
|
||||
|
||||
pages.POST("/ds-query", rt.QueryData)
|
||||
pages.POST("/logs-query", rt.QueryLog)
|
||||
|
||||
pages.POST("/tdengine-databases", rt.tdengineDatabases)
|
||||
pages.POST("/tdengine-tables", rt.tdengineTables)
|
||||
pages.POST("/tdengine-columns", rt.tdengineColumns)
|
||||
|
||||
} else {
|
||||
pages.Any("/proxy/:id/*url", rt.auth(), rt.dsProxy)
|
||||
pages.POST("/query-range-batch", rt.auth(), rt.promBatchQueryRange)
|
||||
pages.POST("/query-instant-batch", rt.auth(), rt.promBatchQueryInstant)
|
||||
pages.GET("/datasource/brief", rt.auth(), rt.datasourceBriefs)
|
||||
pages.GET("/datasource/brief", rt.auth(), rt.user(), rt.datasourceBriefs)
|
||||
|
||||
pages.POST("/ds-query", rt.auth(), rt.QueryData)
|
||||
pages.POST("/logs-query", rt.auth(), rt.QueryLog)
|
||||
|
||||
pages.POST("/tdengine-databases", rt.auth(), rt.tdengineDatabases)
|
||||
pages.POST("/tdengine-tables", rt.auth(), rt.tdengineTables)
|
||||
pages.POST("/tdengine-columns", rt.auth(), rt.tdengineColumns)
|
||||
}
|
||||
|
||||
pages.GET("/sql-template", rt.QuerySqlTemplate)
|
||||
pages.POST("/auth/login", rt.jwtMock(), rt.loginPost)
|
||||
pages.POST("/auth/logout", rt.jwtMock(), rt.logoutPost)
|
||||
pages.POST("/auth/logout", rt.jwtMock(), rt.auth(), rt.user(), rt.logoutPost)
|
||||
pages.POST("/auth/refresh", rt.jwtMock(), rt.refreshPost)
|
||||
pages.POST("/auth/captcha", rt.jwtMock(), rt.generateCaptcha)
|
||||
pages.POST("/auth/captcha-verify", rt.jwtMock(), rt.captchaVerify)
|
||||
pages.GET("/auth/ifshowcaptcha", rt.ifShowCaptcha)
|
||||
|
||||
pages.GET("/auth/sso-config", rt.ssoConfigNameGet)
|
||||
pages.GET("/auth/rsa-config", rt.rsaConfigGet)
|
||||
pages.GET("/auth/redirect", rt.loginRedirect)
|
||||
pages.GET("/auth/redirect/cas", rt.loginRedirectCas)
|
||||
pages.GET("/auth/redirect/oauth", rt.loginRedirectOAuth)
|
||||
@@ -167,6 +229,20 @@ func (rt *Router) Config(r *gin.Engine) {
|
||||
pages.POST("/metric-views", rt.auth(), rt.user(), rt.metricViewAdd)
|
||||
pages.PUT("/metric-views", rt.auth(), rt.user(), rt.metricViewPut)
|
||||
|
||||
pages.GET("/builtin-metric-filters", rt.auth(), rt.user(), rt.metricFilterGets)
|
||||
pages.DELETE("/builtin-metric-filters", rt.auth(), rt.user(), rt.metricFilterDel)
|
||||
pages.POST("/builtin-metric-filters", rt.auth(), rt.user(), rt.metricFilterAdd)
|
||||
pages.PUT("/builtin-metric-filters", rt.auth(), rt.user(), rt.metricFilterPut)
|
||||
pages.POST("/builtin-metric-promql", rt.auth(), rt.user(), rt.getMetricPromql)
|
||||
|
||||
pages.POST("/builtin-metrics", rt.auth(), rt.user(), rt.perm("/builtin-metrics/add"), rt.builtinMetricsAdd)
|
||||
pages.PUT("/builtin-metrics", rt.auth(), rt.user(), rt.perm("/builtin-metrics/put"), rt.builtinMetricsPut)
|
||||
pages.DELETE("/builtin-metrics", rt.auth(), rt.user(), rt.perm("/builtin-metrics/del"), rt.builtinMetricsDel)
|
||||
pages.GET("/builtin-metrics", rt.auth(), rt.user(), rt.builtinMetricsGets)
|
||||
pages.GET("/builtin-metrics/types", rt.auth(), rt.user(), rt.builtinMetricsTypes)
|
||||
pages.GET("/builtin-metrics/types/default", rt.auth(), rt.user(), rt.builtinMetricsDefaultTypes)
|
||||
pages.GET("/builtin-metrics/collectors", rt.auth(), rt.user(), rt.builtinMetricsCollectors)
|
||||
|
||||
pages.GET("/user-groups", rt.auth(), rt.user(), rt.userGroupGets)
|
||||
pages.POST("/user-groups", rt.auth(), rt.user(), rt.perm("/user-groups/add"), rt.userGroupAdd)
|
||||
pages.GET("/user-group/:id", rt.auth(), rt.user(), rt.userGroupGet)
|
||||
@@ -186,6 +262,7 @@ func (rt *Router) Config(r *gin.Engine) {
|
||||
pages.GET("/busi-group/:id/perm/:perm", rt.auth(), rt.user(), rt.checkBusiGroupPerm)
|
||||
|
||||
pages.GET("/targets", rt.auth(), rt.user(), rt.targetGets)
|
||||
pages.GET("/target/extra-meta", rt.auth(), rt.user(), rt.targetExtendInfoByIdent)
|
||||
pages.POST("/target/list", rt.auth(), rt.user(), rt.targetGetsByHostFilter)
|
||||
pages.DELETE("/targets", rt.auth(), rt.user(), rt.perm("/targets/del"), rt.targetDel)
|
||||
pages.GET("/targets/tags", rt.auth(), rt.user(), rt.targetGetTags)
|
||||
@@ -197,16 +274,21 @@ func (rt *Router) Config(r *gin.Engine) {
|
||||
pages.POST("/builtin-cate-favorite", rt.auth(), rt.user(), rt.builtinCateFavoriteAdd)
|
||||
pages.DELETE("/builtin-cate-favorite/:name", rt.auth(), rt.user(), rt.builtinCateFavoriteDel)
|
||||
|
||||
pages.GET("/builtin-boards", rt.builtinBoardGets)
|
||||
pages.GET("/builtin-board/:name", rt.builtinBoardGet)
|
||||
pages.GET("/dashboards/builtin/list", rt.builtinBoardGets)
|
||||
pages.GET("/builtin-boards-cates", rt.auth(), rt.user(), rt.builtinBoardCateGets)
|
||||
pages.POST("/builtin-boards-detail", rt.auth(), rt.user(), rt.builtinBoardDetailGets)
|
||||
pages.GET("/integrations/icon/:cate/:name", rt.builtinIcon)
|
||||
|
||||
// pages.GET("/builtin-boards", rt.builtinBoardGets)
|
||||
// pages.GET("/builtin-board/:name", rt.builtinBoardGet)
|
||||
// pages.GET("/dashboards/builtin/list", rt.builtinBoardGets)
|
||||
// pages.GET("/builtin-boards-cates", rt.auth(), rt.user(), rt.builtinBoardCateGets)
|
||||
// pages.POST("/builtin-boards-detail", rt.auth(), rt.user(), rt.builtinBoardDetailGets)
|
||||
// pages.GET("/integrations/makedown/:cate", rt.builtinMarkdown)
|
||||
|
||||
pages.GET("/busi-groups/public-boards", rt.auth(), rt.user(), rt.perm("/dashboards"), rt.publicBoardGets)
|
||||
pages.GET("/busi-groups/boards", rt.auth(), rt.user(), rt.perm("/dashboards"), rt.boardGetsByGids)
|
||||
pages.GET("/busi-group/:id/boards", rt.auth(), rt.user(), rt.perm("/dashboards"), rt.bgro(), rt.boardGets)
|
||||
pages.POST("/busi-group/:id/boards", rt.auth(), rt.user(), rt.perm("/dashboards/add"), rt.bgrw(), rt.boardAdd)
|
||||
pages.POST("/busi-group/:id/board/:bid/clone", rt.auth(), rt.user(), rt.perm("/dashboards/add"), rt.bgrw(), rt.boardClone)
|
||||
pages.POST("/busi-groups/boards/clones", rt.auth(), rt.user(), rt.perm("/dashboards/add"), rt.boardBatchClone)
|
||||
|
||||
pages.GET("/board/:bid", rt.boardGet)
|
||||
pages.GET("/board/:bid/pure", rt.boardPureGet)
|
||||
@@ -218,9 +300,11 @@ func (rt *Router) Config(r *gin.Engine) {
|
||||
pages.GET("/share-charts", rt.chartShareGets)
|
||||
pages.POST("/share-charts", rt.auth(), rt.chartShareAdd)
|
||||
|
||||
pages.GET("/alert-rules/builtin/alerts-cates", rt.auth(), rt.user(), rt.builtinAlertCateGets)
|
||||
pages.GET("/alert-rules/builtin/list", rt.auth(), rt.user(), rt.builtinAlertRules)
|
||||
// pages.GET("/alert-rules/builtin/alerts-cates", rt.auth(), rt.user(), rt.builtinAlertCateGets)
|
||||
// pages.GET("/alert-rules/builtin/list", rt.auth(), rt.user(), rt.builtinAlertRules)
|
||||
pages.GET("/alert-rules/callbacks", rt.auth(), rt.user(), rt.alertRuleCallbacks)
|
||||
|
||||
pages.GET("/busi-groups/alert-rules", rt.auth(), rt.user(), rt.perm("/alert-rules"), rt.alertRuleGetsByGids)
|
||||
pages.GET("/busi-group/:id/alert-rules", rt.auth(), rt.user(), rt.perm("/alert-rules"), rt.alertRuleGets)
|
||||
pages.POST("/busi-group/:id/alert-rules", rt.auth(), rt.user(), rt.perm("/alert-rules/add"), rt.bgrw(), rt.alertRuleAddByFE)
|
||||
pages.POST("/busi-group/:id/alert-rules/import", rt.auth(), rt.user(), rt.perm("/alert-rules/add"), rt.bgrw(), rt.alertRuleAddByImport)
|
||||
@@ -228,7 +312,9 @@ func (rt *Router) Config(r *gin.Engine) {
|
||||
pages.PUT("/busi-group/:id/alert-rules/fields", rt.auth(), rt.user(), rt.perm("/alert-rules/put"), rt.bgrw(), rt.alertRulePutFields)
|
||||
pages.PUT("/busi-group/:id/alert-rule/:arid", rt.auth(), rt.user(), rt.perm("/alert-rules/put"), rt.alertRulePutByFE)
|
||||
pages.GET("/alert-rule/:arid", rt.auth(), rt.user(), rt.perm("/alert-rules"), rt.alertRuleGet)
|
||||
pages.PUT("/busi-group/alert-rule/validate", rt.auth(), rt.user(), rt.perm("/alert-rules/put"), rt.alertRuleValidation)
|
||||
|
||||
pages.GET("/busi-groups/recording-rules", rt.auth(), rt.user(), rt.perm("/recording-rules"), rt.recordingRuleGetsByGids)
|
||||
pages.GET("/busi-group/:id/recording-rules", rt.auth(), rt.user(), rt.perm("/recording-rules"), rt.recordingRuleGets)
|
||||
pages.POST("/busi-group/:id/recording-rules", rt.auth(), rt.user(), rt.perm("/recording-rules/add"), rt.bgrw(), rt.recordingRuleAddByFE)
|
||||
pages.DELETE("/busi-group/:id/recording-rules", rt.auth(), rt.user(), rt.perm("/recording-rules/del"), rt.bgrw(), rt.recordingRuleDel)
|
||||
@@ -236,12 +322,16 @@ func (rt *Router) Config(r *gin.Engine) {
|
||||
pages.GET("/recording-rule/:rrid", rt.auth(), rt.user(), rt.perm("/recording-rules"), rt.recordingRuleGet)
|
||||
pages.PUT("/busi-group/:id/recording-rules/fields", rt.auth(), rt.user(), rt.perm("/recording-rules/put"), rt.recordingRulePutFields)
|
||||
|
||||
pages.GET("/busi-groups/alert-mutes", rt.auth(), rt.user(), rt.perm("/alert-mutes"), rt.alertMuteGetsByGids)
|
||||
pages.GET("/busi-group/:id/alert-mutes", rt.auth(), rt.user(), rt.perm("/alert-mutes"), rt.bgro(), rt.alertMuteGetsByBG)
|
||||
pages.POST("/busi-group/:id/alert-mutes/preview", rt.auth(), rt.user(), rt.perm("/alert-mutes/add"), rt.bgrw(), rt.alertMutePreview)
|
||||
pages.POST("/busi-group/:id/alert-mutes", rt.auth(), rt.user(), rt.perm("/alert-mutes/add"), rt.bgrw(), rt.alertMuteAdd)
|
||||
pages.DELETE("/busi-group/:id/alert-mutes", rt.auth(), rt.user(), rt.perm("/alert-mutes/del"), rt.bgrw(), rt.alertMuteDel)
|
||||
pages.PUT("/busi-group/:id/alert-mute/:amid", rt.auth(), rt.user(), rt.perm("/alert-mutes/put"), rt.alertMutePutByFE)
|
||||
pages.GET("/busi-group/:id/alert-mute/:amid", rt.auth(), rt.user(), rt.perm("/alert-mutes"), rt.alertMuteGet)
|
||||
pages.PUT("/busi-group/:id/alert-mutes/fields", rt.auth(), rt.user(), rt.perm("/alert-mutes/put"), rt.bgrw(), rt.alertMutePutFields)
|
||||
|
||||
pages.GET("/busi-groups/alert-subscribes", rt.auth(), rt.user(), rt.perm("/alert-subscribes"), rt.alertSubscribeGetsByGids)
|
||||
pages.GET("/busi-group/:id/alert-subscribes", rt.auth(), rt.user(), rt.perm("/alert-subscribes"), rt.bgro(), rt.alertSubscribeGets)
|
||||
pages.GET("/alert-subscribe/:sid", rt.auth(), rt.user(), rt.perm("/alert-subscribes"), rt.alertSubscribeGet)
|
||||
pages.POST("/busi-group/:id/alert-subscribes", rt.auth(), rt.user(), rt.perm("/alert-subscribes/add"), rt.bgrw(), rt.alertSubscribeAdd)
|
||||
@@ -252,22 +342,24 @@ func (rt *Router) Config(r *gin.Engine) {
|
||||
pages.GET("/alert-cur-event/:eid", rt.alertCurEventGet)
|
||||
pages.GET("/alert-his-event/:eid", rt.alertHisEventGet)
|
||||
} else {
|
||||
pages.GET("/alert-cur-event/:eid", rt.auth(), rt.alertCurEventGet)
|
||||
pages.GET("/alert-his-event/:eid", rt.auth(), rt.alertHisEventGet)
|
||||
pages.GET("/alert-cur-event/:eid", rt.auth(), rt.user(), rt.alertCurEventGet)
|
||||
pages.GET("/alert-his-event/:eid", rt.auth(), rt.user(), rt.alertHisEventGet)
|
||||
}
|
||||
|
||||
// card logic
|
||||
pages.GET("/alert-cur-events/list", rt.auth(), rt.alertCurEventsList)
|
||||
pages.GET("/alert-cur-events/card", rt.auth(), rt.alertCurEventsCard)
|
||||
pages.GET("/alert-cur-events/list", rt.auth(), rt.user(), rt.alertCurEventsList)
|
||||
pages.GET("/alert-cur-events/card", rt.auth(), rt.user(), rt.alertCurEventsCard)
|
||||
pages.POST("/alert-cur-events/card/details", rt.auth(), rt.alertCurEventsCardDetails)
|
||||
pages.GET("/alert-his-events/list", rt.auth(), rt.alertHisEventsList)
|
||||
pages.GET("/alert-his-events/list", rt.auth(), rt.user(), rt.alertHisEventsList)
|
||||
pages.DELETE("/alert-cur-events", rt.auth(), rt.user(), rt.perm("/alert-cur-events/del"), rt.alertCurEventDel)
|
||||
pages.GET("/alert-cur-events/stats", rt.auth(), rt.alertCurEventsStatistics)
|
||||
|
||||
pages.GET("/alert-aggr-views", rt.auth(), rt.alertAggrViewGets)
|
||||
pages.DELETE("/alert-aggr-views", rt.auth(), rt.user(), rt.alertAggrViewDel)
|
||||
pages.POST("/alert-aggr-views", rt.auth(), rt.user(), rt.alertAggrViewAdd)
|
||||
pages.PUT("/alert-aggr-views", rt.auth(), rt.user(), rt.alertAggrViewPut)
|
||||
|
||||
pages.GET("/busi-groups/task-tpls", rt.auth(), rt.user(), rt.perm("/job-tpls"), rt.taskTplGetsByGids)
|
||||
pages.GET("/busi-group/:id/task-tpls", rt.auth(), rt.user(), rt.perm("/job-tpls"), rt.bgro(), rt.taskTplGets)
|
||||
pages.POST("/busi-group/:id/task-tpls", rt.auth(), rt.user(), rt.perm("/job-tpls/add"), rt.bgrw(), rt.taskTplAdd)
|
||||
pages.DELETE("/busi-group/:id/task-tpl/:tid", rt.auth(), rt.user(), rt.perm("/job-tpls/del"), rt.bgrw(), rt.taskTplDel)
|
||||
@@ -276,15 +368,14 @@ func (rt *Router) Config(r *gin.Engine) {
|
||||
pages.GET("/busi-group/:id/task-tpl/:tid", rt.auth(), rt.user(), rt.perm("/job-tpls"), rt.bgro(), rt.taskTplGet)
|
||||
pages.PUT("/busi-group/:id/task-tpl/:tid", rt.auth(), rt.user(), rt.perm("/job-tpls/put"), rt.bgrw(), rt.taskTplPut)
|
||||
|
||||
pages.GET("/busi-groups/tasks", rt.auth(), rt.user(), rt.perm("/job-tasks"), rt.taskGetsByGids)
|
||||
pages.GET("/busi-group/:id/tasks", rt.auth(), rt.user(), rt.perm("/job-tasks"), rt.bgro(), rt.taskGets)
|
||||
pages.POST("/busi-group/:id/tasks", rt.auth(), rt.user(), rt.perm("/job-tasks/add"), rt.bgrw(), rt.taskAdd)
|
||||
pages.GET("/busi-group/:id/task/*url", rt.auth(), rt.user(), rt.perm("/job-tasks"), rt.taskProxy)
|
||||
pages.PUT("/busi-group/:id/task/*url", rt.auth(), rt.user(), rt.perm("/job-tasks/put"), rt.bgrw(), rt.taskProxy)
|
||||
|
||||
pages.GET("/servers", rt.auth(), rt.admin(), rt.serversGet)
|
||||
pages.GET("/server-clusters", rt.auth(), rt.admin(), rt.serverClustersGet)
|
||||
pages.GET("/servers", rt.auth(), rt.user(), rt.perm("/help/servers"), rt.serversGet)
|
||||
pages.GET("/server-clusters", rt.auth(), rt.user(), rt.perm("/help/servers"), rt.serverClustersGet)
|
||||
|
||||
pages.POST("/datasource/list", rt.auth(), rt.datasourceList)
|
||||
pages.POST("/datasource/list", rt.auth(), rt.user(), rt.datasourceList)
|
||||
pages.POST("/datasource/plugin/list", rt.auth(), rt.pluginList)
|
||||
pages.POST("/datasource/upsert", rt.auth(), rt.admin(), rt.datasourceUpsert)
|
||||
pages.POST("/datasource/desc", rt.auth(), rt.admin(), rt.datasourceGet)
|
||||
@@ -300,50 +391,105 @@ func (rt *Router) Config(r *gin.Engine) {
|
||||
pages.PUT("/role/:id/ops", rt.auth(), rt.admin(), rt.roleBindOperation)
|
||||
pages.GET("/operation", rt.operations)
|
||||
|
||||
pages.GET("/notify-tpls", rt.auth(), rt.admin(), rt.notifyTplGets)
|
||||
pages.PUT("/notify-tpl/content", rt.auth(), rt.admin(), rt.notifyTplUpdateContent)
|
||||
pages.PUT("/notify-tpl", rt.auth(), rt.admin(), rt.notifyTplUpdate)
|
||||
pages.POST("/notify-tpl/preview", rt.auth(), rt.admin(), rt.notifyTplPreview)
|
||||
pages.GET("/notify-tpls", rt.auth(), rt.user(), rt.notifyTplGets)
|
||||
pages.PUT("/notify-tpl/content", rt.auth(), rt.user(), rt.notifyTplUpdateContent)
|
||||
pages.PUT("/notify-tpl", rt.auth(), rt.user(), rt.notifyTplUpdate)
|
||||
pages.POST("/notify-tpl", rt.auth(), rt.user(), rt.notifyTplAdd)
|
||||
pages.DELETE("/notify-tpl/:id", rt.auth(), rt.user(), rt.notifyTplDel)
|
||||
pages.POST("/notify-tpl/preview", rt.auth(), rt.user(), rt.notifyTplPreview)
|
||||
|
||||
pages.GET("/sso-configs", rt.auth(), rt.admin(), rt.ssoConfigGets)
|
||||
pages.PUT("/sso-config", rt.auth(), rt.admin(), rt.ssoConfigUpdate)
|
||||
|
||||
pages.GET("/webhooks", rt.auth(), rt.admin(), rt.webhookGets)
|
||||
pages.GET("/webhooks", rt.auth(), rt.user(), rt.webhookGets)
|
||||
pages.PUT("/webhooks", rt.auth(), rt.admin(), rt.webhookPuts)
|
||||
|
||||
pages.GET("/notify-script", rt.auth(), rt.admin(), rt.notifyScriptGet)
|
||||
pages.GET("/notify-script", rt.auth(), rt.user(), rt.perm("/help/notification-settings"), rt.notifyScriptGet)
|
||||
pages.PUT("/notify-script", rt.auth(), rt.admin(), rt.notifyScriptPut)
|
||||
|
||||
pages.GET("/notify-channel", rt.auth(), rt.admin(), rt.notifyChannelGets)
|
||||
pages.GET("/notify-channel", rt.auth(), rt.user(), rt.perm("/help/notification-settings"), rt.notifyChannelGets)
|
||||
pages.PUT("/notify-channel", rt.auth(), rt.admin(), rt.notifyChannelPuts)
|
||||
|
||||
pages.GET("/notify-contact", rt.auth(), rt.admin(), rt.notifyContactGets)
|
||||
pages.GET("/notify-contact", rt.auth(), rt.user(), rt.perm("/help/notification-settings"), rt.notifyContactGets)
|
||||
pages.PUT("/notify-contact", rt.auth(), rt.admin(), rt.notifyContactPuts)
|
||||
|
||||
pages.GET("/notify-config", rt.auth(), rt.admin(), rt.notifyConfigGet)
|
||||
pages.GET("/notify-config", rt.auth(), rt.user(), rt.perm("/help/notification-settings"), rt.notifyConfigGet)
|
||||
pages.PUT("/notify-config", rt.auth(), rt.admin(), rt.notifyConfigPut)
|
||||
pages.PUT("/smtp-config-test", rt.auth(), rt.admin(), rt.attemptSendEmail)
|
||||
|
||||
pages.GET("/es-index-pattern", rt.auth(), rt.esIndexPatternGet)
|
||||
pages.GET("/es-index-pattern-list", rt.auth(), rt.esIndexPatternGetList)
|
||||
pages.POST("/es-index-pattern", rt.auth(), rt.admin(), rt.esIndexPatternAdd)
|
||||
pages.PUT("/es-index-pattern", rt.auth(), rt.admin(), rt.esIndexPatternPut)
|
||||
pages.DELETE("/es-index-pattern", rt.auth(), rt.admin(), rt.esIndexPatternDel)
|
||||
|
||||
pages.GET("/user-variable-configs", rt.auth(), rt.user(), rt.perm("/help/variable-configs"), rt.userVariableConfigGets)
|
||||
pages.POST("/user-variable-config", rt.auth(), rt.user(), rt.perm("/help/variable-configs"), rt.userVariableConfigAdd)
|
||||
pages.PUT("/user-variable-config/:id", rt.auth(), rt.user(), rt.perm("/help/variable-configs"), rt.userVariableConfigPut)
|
||||
pages.DELETE("/user-variable-config/:id", rt.auth(), rt.user(), rt.perm("/help/variable-configs"), rt.userVariableConfigDel)
|
||||
|
||||
pages.GET("/config", rt.auth(), rt.admin(), rt.configGetByKey)
|
||||
pages.PUT("/config", rt.auth(), rt.admin(), rt.configPutByKey)
|
||||
pages.GET("/site-info", rt.siteInfo)
|
||||
|
||||
// for admin api
|
||||
pages.GET("/user/busi-groups", rt.auth(), rt.admin(), rt.userBusiGroupsGets)
|
||||
|
||||
pages.GET("/builtin-components", rt.auth(), rt.user(), rt.builtinComponentsGets)
|
||||
pages.POST("/builtin-components", rt.auth(), rt.user(), rt.perm("/built-in-components/add"), rt.builtinComponentsAdd)
|
||||
pages.PUT("/builtin-components", rt.auth(), rt.user(), rt.perm("/built-in-components/put"), rt.builtinComponentsPut)
|
||||
pages.DELETE("/builtin-components", rt.auth(), rt.user(), rt.perm("/built-in-components/del"), rt.builtinComponentsDel)
|
||||
|
||||
pages.GET("/builtin-payloads", rt.auth(), rt.user(), rt.builtinPayloadsGets)
|
||||
pages.GET("/builtin-payloads/cates", rt.auth(), rt.user(), rt.builtinPayloadcatesGet)
|
||||
pages.POST("/builtin-payloads", rt.auth(), rt.user(), rt.perm("/built-in-components/add"), rt.builtinPayloadsAdd)
|
||||
pages.GET("/builtin-payload/:id", rt.auth(), rt.user(), rt.perm("/built-in-components"), rt.builtinPayloadGet)
|
||||
pages.PUT("/builtin-payloads", rt.auth(), rt.user(), rt.perm("/built-in-components/put"), rt.builtinPayloadsPut)
|
||||
pages.DELETE("/builtin-payloads", rt.auth(), rt.user(), rt.perm("/built-in-components/del"), rt.builtinPayloadsDel)
|
||||
}
|
||||
|
||||
if rt.HTTP.Service.Enable {
|
||||
r.GET("/api/n9e/versions", func(c *gin.Context) {
|
||||
v := version.Version
|
||||
lastIndex := strings.LastIndex(version.Version, "-")
|
||||
if lastIndex != -1 {
|
||||
v = version.Version[:lastIndex]
|
||||
}
|
||||
|
||||
gv := version.GithubVersion.Load()
|
||||
if gv != nil {
|
||||
ginx.NewRender(c).Data(gin.H{"version": v, "github_verison": gv.(string)}, nil)
|
||||
} else {
|
||||
ginx.NewRender(c).Data(gin.H{"version": v, "github_verison": ""}, nil)
|
||||
}
|
||||
})
|
||||
|
||||
if rt.HTTP.APIForService.Enable {
|
||||
service := r.Group("/v1/n9e")
|
||||
if len(rt.HTTP.Service.BasicAuth) > 0 {
|
||||
service.Use(gin.BasicAuth(rt.HTTP.Service.BasicAuth))
|
||||
if len(rt.HTTP.APIForService.BasicAuth) > 0 {
|
||||
service.Use(gin.BasicAuth(rt.HTTP.APIForService.BasicAuth))
|
||||
}
|
||||
{
|
||||
service.Any("/prometheus/*url", rt.dsProxy)
|
||||
service.POST("/users", rt.userAddPost)
|
||||
service.PUT("/user/:id", rt.userProfilePutByService)
|
||||
service.DELETE("/user/:id", rt.userDel)
|
||||
service.GET("/users", rt.userFindAll)
|
||||
|
||||
service.GET("/user-groups", rt.userGroupGetsByService)
|
||||
service.GET("/user-group-members", rt.userGroupMemberGetsByService)
|
||||
|
||||
service.GET("/targets", rt.targetGetsByService)
|
||||
service.GET("/target/extra-meta", rt.targetExtendInfoByIdent)
|
||||
service.POST("/target/list", rt.targetGetsByHostFilter)
|
||||
service.DELETE("/targets", rt.targetDelByService)
|
||||
service.GET("/targets/tags", rt.targetGetTags)
|
||||
service.POST("/targets/tags", rt.targetBindTagsByService)
|
||||
service.DELETE("/targets/tags", rt.targetUnbindTagsByService)
|
||||
service.PUT("/targets/note", rt.targetUpdateNoteByService)
|
||||
service.PUT("/targets/bgid", rt.targetUpdateBgidByService)
|
||||
|
||||
service.POST("/alert-rules", rt.alertRuleAddByService)
|
||||
service.POST("/alert-rule-add", rt.alertRuleAddOneByService)
|
||||
service.DELETE("/alert-rules", rt.alertRuleDelByService)
|
||||
service.PUT("/alert-rule/:arid", rt.alertRulePutByService)
|
||||
service.GET("/alert-rule/:arid", rt.alertRuleGet)
|
||||
@@ -369,6 +515,10 @@ func (rt *Router) Config(r *gin.Engine) {
|
||||
service.GET("/alert-his-events", rt.alertHisEventsList)
|
||||
service.GET("/alert-his-event/:eid", rt.alertHisEventGet)
|
||||
|
||||
service.GET("/task-tpl/:tid", rt.taskTplGetByService)
|
||||
service.GET("/task-tpls", rt.taskTplGetsByService)
|
||||
service.GET("/task-tpl/statistics", rt.taskTplStatistics)
|
||||
|
||||
service.GET("/config/:id", rt.configGet)
|
||||
service.GET("/configs", rt.configsGet)
|
||||
service.GET("/config", rt.configGetByKey)
|
||||
@@ -384,14 +534,19 @@ func (rt *Router) Config(r *gin.Engine) {
|
||||
service.GET("/notify-tpls", rt.notifyTplGets)
|
||||
|
||||
service.POST("/task-record-add", rt.taskRecordAdd)
|
||||
|
||||
service.GET("/user-variable/decrypt", rt.userVariableGetDecryptByService)
|
||||
|
||||
service.GET("/targets-of-alert-rule", rt.targetsOfAlertRule)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
if rt.HTTP.Heartbeat.Enable {
|
||||
if rt.HTTP.APIForAgent.Enable {
|
||||
heartbeat := r.Group("/v1/n9e")
|
||||
{
|
||||
if len(rt.HTTP.Heartbeat.BasicAuth) > 0 {
|
||||
heartbeat.Use(gin.BasicAuth(rt.HTTP.Heartbeat.BasicAuth))
|
||||
if len(rt.HTTP.APIForAgent.BasicAuth) > 0 {
|
||||
heartbeat.Use(gin.BasicAuth(rt.HTTP.APIForAgent.BasicAuth))
|
||||
}
|
||||
heartbeat.POST("/heartbeat", rt.heartbeat)
|
||||
}
|
||||
|
||||
@@ -69,6 +69,11 @@ func (rt *Router) alertAggrViewPut(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
ginx.NewRender(c).Message(view.Update(rt.Ctx, f.Name, f.Rule, f.Cate, me.Id))
|
||||
view.Name = f.Name
|
||||
view.Rule = f.Rule
|
||||
view.Cate = f.Cate
|
||||
if view.CreateBy == 0 {
|
||||
view.CreateBy = me.Id
|
||||
}
|
||||
ginx.NewRender(c).Message(view.Update(rt.Ctx))
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"net/http"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/ccfos/nightingale/v6/models"
|
||||
|
||||
@@ -42,7 +43,6 @@ func (rt *Router) alertCurEventsCard(c *gin.Context) {
|
||||
stime, etime := getTimeRange(c)
|
||||
severity := ginx.QueryInt(c, "severity", -1)
|
||||
query := ginx.QueryStr(c, "query", "")
|
||||
busiGroupId := ginx.QueryInt64(c, "bgid", 0)
|
||||
dsIds := queryDatasourceIds(c)
|
||||
rules := parseAggrRules(c)
|
||||
|
||||
@@ -61,8 +61,11 @@ func (rt *Router) alertCurEventsCard(c *gin.Context) {
|
||||
cates = strings.Split(cate, ",")
|
||||
}
|
||||
|
||||
bgids, err := GetBusinessGroupIds(c, rt.Ctx, rt.Center.EventHistoryGroupView)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
// 最多获取50000个,获取太多也没啥意义
|
||||
list, err := models.AlertCurEventGets(rt.Ctx, prods, busiGroupId, stime, etime, severity, dsIds, cates, query, 50000, 0)
|
||||
list, err := models.AlertCurEventGets(rt.Ctx, prods, bgids, stime, etime, severity, dsIds, cates, query, 50000, 0)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
cardmap := make(map[string]*AlertCard)
|
||||
@@ -141,7 +144,6 @@ func (rt *Router) alertCurEventsList(c *gin.Context) {
|
||||
severity := ginx.QueryInt(c, "severity", -1)
|
||||
query := ginx.QueryStr(c, "query", "")
|
||||
limit := ginx.QueryInt(c, "limit", 20)
|
||||
busiGroupId := ginx.QueryInt64(c, "bgid", 0)
|
||||
dsIds := queryDatasourceIds(c)
|
||||
|
||||
prod := ginx.QueryStr(c, "prods", "")
|
||||
@@ -160,10 +162,13 @@ func (rt *Router) alertCurEventsList(c *gin.Context) {
|
||||
cates = strings.Split(cate, ",")
|
||||
}
|
||||
|
||||
total, err := models.AlertCurEventTotal(rt.Ctx, prods, busiGroupId, stime, etime, severity, dsIds, cates, query)
|
||||
bgids, err := GetBusinessGroupIds(c, rt.Ctx, rt.Center.EventHistoryGroupView)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
list, err := models.AlertCurEventGets(rt.Ctx, prods, busiGroupId, stime, etime, severity, dsIds, cates, query, limit, ginx.Offset(c, limit))
|
||||
total, err := models.AlertCurEventTotal(rt.Ctx, prods, bgids, stime, etime, severity, dsIds, cates, query)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
list, err := models.AlertCurEventGets(rt.Ctx, prods, bgids, stime, etime, severity, dsIds, cates, query, limit, ginx.Offset(c, limit))
|
||||
ginx.Dangerous(err)
|
||||
|
||||
cache := make(map[int64]*models.UserGroup)
|
||||
@@ -182,10 +187,19 @@ func (rt *Router) alertCurEventDel(c *gin.Context) {
|
||||
ginx.BindJSON(c, &f)
|
||||
f.Verify()
|
||||
|
||||
rt.checkCurEventBusiGroupRWPermission(c, f.Ids)
|
||||
|
||||
ginx.NewRender(c).Message(models.AlertCurEventDel(rt.Ctx, f.Ids))
|
||||
}
|
||||
|
||||
func (rt *Router) checkCurEventBusiGroupRWPermission(c *gin.Context, ids []int64) {
|
||||
set := make(map[int64]struct{})
|
||||
|
||||
for i := 0; i < len(f.Ids); i++ {
|
||||
event, err := models.AlertCurEventGetById(rt.Ctx, f.Ids[i])
|
||||
// event group id is 0, ignore perm check
|
||||
set[0] = struct{}{}
|
||||
|
||||
for i := 0; i < len(ids); i++ {
|
||||
event, err := models.AlertCurEventGetById(rt.Ctx, ids[i])
|
||||
ginx.Dangerous(err)
|
||||
|
||||
if _, has := set[event.GroupId]; !has {
|
||||
@@ -193,8 +207,6 @@ func (rt *Router) alertCurEventDel(c *gin.Context) {
|
||||
set[event.GroupId] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
ginx.NewRender(c).Message(models.AlertCurEventDel(rt.Ctx, f.Ids))
|
||||
}
|
||||
|
||||
func (rt *Router) alertCurEventGet(c *gin.Context) {
|
||||
@@ -206,5 +218,14 @@ func (rt *Router) alertCurEventGet(c *gin.Context) {
|
||||
ginx.Bomb(404, "No such active event")
|
||||
}
|
||||
|
||||
if !rt.Center.AnonymousAccess.AlertDetail && rt.Center.EventHistoryGroupView {
|
||||
rt.bgroCheck(c, event.GroupId)
|
||||
}
|
||||
|
||||
ginx.NewRender(c).Data(event, nil)
|
||||
}
|
||||
|
||||
func (rt *Router) alertCurEventsStatistics(c *gin.Context) {
|
||||
|
||||
ginx.NewRender(c).Data(models.AlertCurEventStatistics(rt.Ctx, time.Now()), nil)
|
||||
}
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/ccfos/nightingale/v6/models"
|
||||
"github.com/ccfos/nightingale/v6/pkg/ctx"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/toolkits/pkg/ginx"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
func getTimeRange(c *gin.Context) (stime, etime int64) {
|
||||
@@ -33,7 +36,6 @@ func (rt *Router) alertHisEventsList(c *gin.Context) {
|
||||
recovered := ginx.QueryInt(c, "is_recovered", -1)
|
||||
query := ginx.QueryStr(c, "query", "")
|
||||
limit := ginx.QueryInt(c, "limit", 20)
|
||||
busiGroupId := ginx.QueryInt64(c, "bgid", 0)
|
||||
dsIds := queryDatasourceIds(c)
|
||||
|
||||
prod := ginx.QueryStr(c, "prods", "")
|
||||
@@ -52,10 +54,13 @@ func (rt *Router) alertHisEventsList(c *gin.Context) {
|
||||
cates = strings.Split(cate, ",")
|
||||
}
|
||||
|
||||
total, err := models.AlertHisEventTotal(rt.Ctx, prods, busiGroupId, stime, etime, severity, recovered, dsIds, cates, query)
|
||||
bgids, err := GetBusinessGroupIds(c, rt.Ctx, rt.Center.EventHistoryGroupView)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
list, err := models.AlertHisEventGets(rt.Ctx, prods, busiGroupId, stime, etime, severity, recovered, dsIds, cates, query, limit, ginx.Offset(c, limit))
|
||||
total, err := models.AlertHisEventTotal(rt.Ctx, prods, bgids, stime, etime, severity, recovered, dsIds, cates, query)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
list, err := models.AlertHisEventGets(rt.Ctx, prods, bgids, stime, etime, severity, recovered, dsIds, cates, query, limit, ginx.Offset(c, limit))
|
||||
ginx.Dangerous(err)
|
||||
|
||||
cache := make(map[int64]*models.UserGroup)
|
||||
@@ -78,5 +83,50 @@ func (rt *Router) alertHisEventGet(c *gin.Context) {
|
||||
ginx.Bomb(404, "No such alert event")
|
||||
}
|
||||
|
||||
if !rt.Center.AnonymousAccess.AlertDetail && rt.Center.EventHistoryGroupView {
|
||||
rt.bgroCheck(c, event.GroupId)
|
||||
}
|
||||
|
||||
ginx.NewRender(c).Data(event, err)
|
||||
}
|
||||
|
||||
func GetBusinessGroupIds(c *gin.Context, ctx *ctx.Context, eventHistoryGroupView bool) ([]int64, error) {
|
||||
bgid := ginx.QueryInt64(c, "bgid", 0)
|
||||
var bgids []int64
|
||||
|
||||
if !eventHistoryGroupView || strings.HasPrefix(c.Request.URL.Path, "/v1") {
|
||||
if bgid > 0 {
|
||||
return []int64{bgid}, nil
|
||||
}
|
||||
return bgids, nil
|
||||
}
|
||||
|
||||
user := c.MustGet("user").(*models.User)
|
||||
if user.IsAdmin() {
|
||||
if bgid > 0 {
|
||||
return []int64{bgid}, nil
|
||||
}
|
||||
return bgids, nil
|
||||
}
|
||||
|
||||
bussGroupIds, err := models.MyBusiGroupIds(ctx, user.Id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(bussGroupIds) == 0 {
|
||||
// 如果没查到用户属于任何业务组,需要返回一个0,否则会导致查询到全部告警历史
|
||||
return []int64{0}, nil
|
||||
}
|
||||
|
||||
if bgid > 0 && !slices.Contains(bussGroupIds, bgid) {
|
||||
return nil, fmt.Errorf("business group ID not allowed")
|
||||
}
|
||||
|
||||
if bgid > 0 {
|
||||
// Pass filter parameters, priority to use
|
||||
return []int64{bgid}, nil
|
||||
}
|
||||
|
||||
return bussGroupIds, nil
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package router
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -10,6 +11,7 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/toolkits/pkg/ginx"
|
||||
"github.com/toolkits/pkg/i18n"
|
||||
"github.com/toolkits/pkg/str"
|
||||
)
|
||||
|
||||
// Return all, front-end search and paging
|
||||
@@ -26,6 +28,37 @@ func (rt *Router) alertRuleGets(c *gin.Context) {
|
||||
ginx.NewRender(c).Data(ars, err)
|
||||
}
|
||||
|
||||
func (rt *Router) alertRuleGetsByGids(c *gin.Context) {
|
||||
gids := str.IdsInt64(ginx.QueryStr(c, "gids", ""), ",")
|
||||
if len(gids) > 0 {
|
||||
for _, gid := range gids {
|
||||
rt.bgroCheck(c, gid)
|
||||
}
|
||||
} else {
|
||||
me := c.MustGet("user").(*models.User)
|
||||
if !me.IsAdmin() {
|
||||
var err error
|
||||
gids, err = models.MyBusiGroupIds(rt.Ctx, me.Id)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
if len(gids) == 0 {
|
||||
ginx.NewRender(c).Data([]int{}, nil)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ars, err := models.AlertRuleGetsByBGIds(rt.Ctx, gids)
|
||||
if err == nil {
|
||||
cache := make(map[int64]*models.UserGroup)
|
||||
for i := 0; i < len(ars); i++ {
|
||||
ars[i].FillNotifyGroups(rt.Ctx, cache)
|
||||
ars[i].FillSeverities()
|
||||
}
|
||||
}
|
||||
ginx.NewRender(c).Data(ars, err)
|
||||
}
|
||||
|
||||
func (rt *Router) alertRulesGetByService(c *gin.Context) {
|
||||
prods := []string{}
|
||||
prodStr := ginx.QueryStr(c, "prods", "")
|
||||
@@ -100,6 +133,17 @@ func (rt *Router) alertRuleAddByService(c *gin.Context) {
|
||||
ginx.NewRender(c).Data(reterr, nil)
|
||||
}
|
||||
|
||||
func (rt *Router) alertRuleAddOneByService(c *gin.Context) {
|
||||
var f models.AlertRule
|
||||
ginx.BindJSON(c, &f)
|
||||
|
||||
err := f.FE2DB()
|
||||
ginx.Dangerous(err)
|
||||
|
||||
err = f.Add(rt.Ctx)
|
||||
ginx.NewRender(c).Data(f.Id, err)
|
||||
}
|
||||
|
||||
func (rt *Router) alertRuleAddForService(lst []models.AlertRule, username string) map[string]string {
|
||||
count := len(lst)
|
||||
// alert rule name -> error string
|
||||
@@ -271,3 +315,76 @@ func (rt *Router) alertRuleGet(c *gin.Context) {
|
||||
|
||||
ginx.NewRender(c).Data(ar, err)
|
||||
}
|
||||
|
||||
// pre validation before save rule
|
||||
func (rt *Router) alertRuleValidation(c *gin.Context) {
|
||||
var f models.AlertRule //new
|
||||
ginx.BindJSON(c, &f)
|
||||
|
||||
if len(f.NotifyChannelsJSON) > 0 && len(f.NotifyGroupsJSON) > 0 { //Validation NotifyChannels
|
||||
ngids := make([]int64, 0, len(f.NotifyChannelsJSON))
|
||||
for i := range f.NotifyGroupsJSON {
|
||||
id, _ := strconv.ParseInt(f.NotifyGroupsJSON[i], 10, 64)
|
||||
ngids = append(ngids, id)
|
||||
}
|
||||
userGroups := rt.UserGroupCache.GetByUserGroupIds(ngids)
|
||||
uids := make([]int64, 0)
|
||||
for i := range userGroups {
|
||||
uids = append(uids, userGroups[i].UserIds...)
|
||||
}
|
||||
users := rt.UserCache.GetByUserIds(uids)
|
||||
//If any users have a certain notify channel's token, it will be okay. Otherwise, this notify channel is absent of tokens.
|
||||
ancs := make([]string, 0, len(f.NotifyChannelsJSON)) //absent Notify Channels
|
||||
for i := range f.NotifyChannelsJSON {
|
||||
flag := true
|
||||
//ignore non-default channels
|
||||
switch f.NotifyChannelsJSON[i] {
|
||||
case models.Dingtalk, models.Wecom, models.Feishu, models.Mm,
|
||||
models.Telegram, models.Email, models.FeishuCard:
|
||||
// do nothing
|
||||
default:
|
||||
continue
|
||||
}
|
||||
//default channels
|
||||
for ui := range users {
|
||||
if _, b := users[ui].ExtractToken(f.NotifyChannelsJSON[i]); b {
|
||||
flag = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if flag {
|
||||
ancs = append(ancs, f.NotifyChannelsJSON[i])
|
||||
}
|
||||
}
|
||||
|
||||
if len(ancs) > 0 {
|
||||
ginx.NewRender(c).Message("All users are missing notify channel configurations. Please check for missing tokens (each channel should be configured with at least one user). %s", ancs)
|
||||
return
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
ginx.NewRender(c).Message("")
|
||||
}
|
||||
|
||||
func (rt *Router) alertRuleCallbacks(c *gin.Context) {
|
||||
user := c.MustGet("user").(*models.User)
|
||||
bussGroupIds, err := models.MyBusiGroupIds(rt.Ctx, user.Id)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
ars, err := models.AlertRuleGetsByBGIds(rt.Ctx, bussGroupIds)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
var callbacks []string
|
||||
callbackFilter := make(map[string]struct{})
|
||||
for i := range ars {
|
||||
for _, callback := range ars[i].CallbacksJSON {
|
||||
if _, ok := callbackFilter[callback]; !ok {
|
||||
callbackFilter[callback] = struct{}{}
|
||||
callbacks = append(callbacks, callback)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ginx.NewRender(c).Data(callbacks, nil)
|
||||
}
|
||||
|
||||
@@ -8,27 +8,61 @@ import (
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/toolkits/pkg/ginx"
|
||||
"github.com/toolkits/pkg/str"
|
||||
)
|
||||
|
||||
// Return all, front-end search and paging
|
||||
func (rt *Router) alertSubscribeGets(c *gin.Context) {
|
||||
bgid := ginx.UrlParamInt64(c, "id")
|
||||
lst, err := models.AlertSubscribeGets(rt.Ctx, bgid)
|
||||
if err == nil {
|
||||
ugcache := make(map[int64]*models.UserGroup)
|
||||
for i := 0; i < len(lst); i++ {
|
||||
ginx.Dangerous(lst[i].FillUserGroups(rt.Ctx, ugcache))
|
||||
}
|
||||
ginx.Dangerous(err)
|
||||
|
||||
rulecache := make(map[int64]string)
|
||||
for i := 0; i < len(lst); i++ {
|
||||
ginx.Dangerous(lst[i].FillRuleName(rt.Ctx, rulecache))
|
||||
}
|
||||
ugcache := make(map[int64]*models.UserGroup)
|
||||
rulecache := make(map[int64]string)
|
||||
|
||||
for i := 0; i < len(lst); i++ {
|
||||
ginx.Dangerous(lst[i].FillDatasourceIds(rt.Ctx))
|
||||
for i := 0; i < len(lst); i++ {
|
||||
ginx.Dangerous(lst[i].FillUserGroups(rt.Ctx, ugcache))
|
||||
ginx.Dangerous(lst[i].FillRuleNames(rt.Ctx, rulecache))
|
||||
ginx.Dangerous(lst[i].FillDatasourceIds(rt.Ctx))
|
||||
ginx.Dangerous(lst[i].DB2FE())
|
||||
}
|
||||
|
||||
ginx.NewRender(c).Data(lst, err)
|
||||
}
|
||||
|
||||
func (rt *Router) alertSubscribeGetsByGids(c *gin.Context) {
|
||||
gids := str.IdsInt64(ginx.QueryStr(c, "gids", ""), ",")
|
||||
if len(gids) > 0 {
|
||||
for _, gid := range gids {
|
||||
rt.bgroCheck(c, gid)
|
||||
}
|
||||
} else {
|
||||
me := c.MustGet("user").(*models.User)
|
||||
if !me.IsAdmin() {
|
||||
var err error
|
||||
gids, err = models.MyBusiGroupIds(rt.Ctx, me.Id)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
if len(gids) == 0 {
|
||||
ginx.NewRender(c).Data([]int{}, nil)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
lst, err := models.AlertSubscribeGetsByBGIds(rt.Ctx, gids)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
ugcache := make(map[int64]*models.UserGroup)
|
||||
rulecache := make(map[int64]string)
|
||||
|
||||
for i := 0; i < len(lst); i++ {
|
||||
ginx.Dangerous(lst[i].FillUserGroups(rt.Ctx, ugcache))
|
||||
ginx.Dangerous(lst[i].FillRuleNames(rt.Ctx, rulecache))
|
||||
ginx.Dangerous(lst[i].FillDatasourceIds(rt.Ctx))
|
||||
ginx.Dangerous(lst[i].DB2FE())
|
||||
}
|
||||
|
||||
ginx.NewRender(c).Data(lst, err)
|
||||
}
|
||||
|
||||
@@ -47,7 +81,7 @@ func (rt *Router) alertSubscribeGet(c *gin.Context) {
|
||||
ginx.Dangerous(sub.FillUserGroups(rt.Ctx, ugcache))
|
||||
|
||||
rulecache := make(map[int64]string)
|
||||
ginx.Dangerous(sub.FillRuleName(rt.Ctx, rulecache))
|
||||
ginx.Dangerous(sub.FillRuleNames(rt.Ctx, rulecache))
|
||||
ginx.Dangerous(sub.FillDatasourceIds(rt.Ctx))
|
||||
ginx.Dangerous(sub.DB2FE())
|
||||
|
||||
@@ -79,12 +113,19 @@ func (rt *Router) alertSubscribePut(c *gin.Context) {
|
||||
for i := 0; i < len(fs); i++ {
|
||||
fs[i].UpdateBy = username
|
||||
fs[i].UpdateAt = timestamp
|
||||
//After adding the function of batch subscription alert rules, rule_ids is used instead of rule_id.
|
||||
//When the subscription rules are updated, set rule_id=0 to prevent the wrong subscription caused by the old rule_id.
|
||||
fs[i].RuleId = 0
|
||||
ginx.Dangerous(fs[i].Update(
|
||||
rt.Ctx,
|
||||
"name",
|
||||
"disabled",
|
||||
"prod",
|
||||
"cate",
|
||||
"datasource_ids",
|
||||
"cluster",
|
||||
"rule_id",
|
||||
"rule_ids",
|
||||
"tags",
|
||||
"redefine_severity",
|
||||
"new_severity",
|
||||
@@ -96,7 +137,10 @@ func (rt *Router) alertSubscribePut(c *gin.Context) {
|
||||
"webhooks",
|
||||
"for_duration",
|
||||
"redefine_webhooks",
|
||||
"datasource_ids",
|
||||
"severities",
|
||||
"extra_config",
|
||||
"busi_groups",
|
||||
"note",
|
||||
))
|
||||
}
|
||||
|
||||
|
||||
@@ -1,22 +1,26 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/ccfos/nightingale/v6/models"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
"github.com/toolkits/pkg/ginx"
|
||||
"github.com/toolkits/pkg/i18n"
|
||||
"github.com/toolkits/pkg/str"
|
||||
)
|
||||
|
||||
type boardForm struct {
|
||||
Name string `json:"name"`
|
||||
Ident string `json:"ident"`
|
||||
Tags string `json:"tags"`
|
||||
Configs string `json:"configs"`
|
||||
Public int `json:"public"`
|
||||
Name string `json:"name"`
|
||||
Ident string `json:"ident"`
|
||||
Tags string `json:"tags"`
|
||||
Configs string `json:"configs"`
|
||||
Public int `json:"public"`
|
||||
PublicCate int `json:"public_cate"`
|
||||
Bgids []int64 `json:"bgids"`
|
||||
}
|
||||
|
||||
func (rt *Router) boardAdd(c *gin.Context) {
|
||||
@@ -65,6 +69,28 @@ func (rt *Router) boardGet(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
if board.PublicCate == models.PublicLogin {
|
||||
rt.auth()(c)
|
||||
} else if board.PublicCate == models.PublicBusi {
|
||||
rt.auth()(c)
|
||||
rt.user()(c)
|
||||
|
||||
me := c.MustGet("user").(*models.User)
|
||||
if !me.IsAdmin() {
|
||||
bgids, err := models.MyBusiGroupIds(rt.Ctx, me.Id)
|
||||
ginx.Dangerous(err)
|
||||
if len(bgids) == 0 {
|
||||
ginx.Bomb(http.StatusForbidden, "forbidden")
|
||||
}
|
||||
|
||||
ok, err := models.BoardBusigroupCheck(rt.Ctx, board.Id, bgids)
|
||||
ginx.Dangerous(err)
|
||||
if !ok {
|
||||
ginx.Bomb(http.StatusForbidden, "forbidden")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ginx.NewRender(c).Data(board, nil)
|
||||
}
|
||||
|
||||
@@ -192,10 +218,20 @@ func (rt *Router) boardPutPublic(c *gin.Context) {
|
||||
}
|
||||
|
||||
bo.Public = f.Public
|
||||
bo.PublicCate = f.PublicCate
|
||||
|
||||
if bo.PublicCate == models.PublicBusi {
|
||||
err := models.BoardBusigroupUpdate(rt.Ctx, bo.Id, f.Bgids)
|
||||
ginx.Dangerous(err)
|
||||
} else {
|
||||
err := models.BoardBusigroupDelByBoardId(rt.Ctx, bo.Id)
|
||||
ginx.Dangerous(err)
|
||||
}
|
||||
|
||||
bo.UpdateBy = me.Username
|
||||
bo.UpdateAt = time.Now().Unix()
|
||||
|
||||
err := bo.Update(rt.Ctx, "public", "update_by", "update_at")
|
||||
err := bo.Update(rt.Ctx, "public", "public_cate", "update_by", "update_at")
|
||||
ginx.NewRender(c).Data(bo, err)
|
||||
}
|
||||
|
||||
@@ -207,21 +243,64 @@ func (rt *Router) boardGets(c *gin.Context) {
|
||||
ginx.NewRender(c).Data(boards, err)
|
||||
}
|
||||
|
||||
func (rt *Router) publicBoardGets(c *gin.Context) {
|
||||
me := c.MustGet("user").(*models.User)
|
||||
|
||||
bgids, err := models.MyBusiGroupIds(rt.Ctx, me.Id)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
boardIds, err := models.BoardIdsByBusiGroupIds(rt.Ctx, bgids)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
boards, err := models.BoardGets(rt.Ctx, "", "public=1 and (public_cate in (?) or id in (?))", []int64{0, 1}, boardIds)
|
||||
ginx.NewRender(c).Data(boards, err)
|
||||
}
|
||||
|
||||
func (rt *Router) boardGetsByGids(c *gin.Context) {
|
||||
gids := str.IdsInt64(ginx.QueryStr(c, "gids", ""), ",")
|
||||
query := ginx.QueryStr(c, "query", "")
|
||||
|
||||
if len(gids) > 0 {
|
||||
for _, gid := range gids {
|
||||
rt.bgroCheck(c, gid)
|
||||
}
|
||||
} else {
|
||||
me := c.MustGet("user").(*models.User)
|
||||
if !me.IsAdmin() {
|
||||
var err error
|
||||
gids, err = models.MyBusiGroupIds(rt.Ctx, me.Id)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
if len(gids) == 0 {
|
||||
ginx.NewRender(c).Data([]int{}, nil)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
boardBusigroups, err := models.BoardBusigroupGets(rt.Ctx)
|
||||
ginx.Dangerous(err)
|
||||
m := make(map[int64][]int64)
|
||||
for _, boardBusigroup := range boardBusigroups {
|
||||
m[boardBusigroup.BoardId] = append(m[boardBusigroup.BoardId], boardBusigroup.BusiGroupId)
|
||||
}
|
||||
|
||||
boards, err := models.BoardGetsByBGIds(rt.Ctx, gids, query)
|
||||
ginx.Dangerous(err)
|
||||
for i := 0; i < len(boards); i++ {
|
||||
if ids, ok := m[boards[i].Id]; ok {
|
||||
boards[i].Bgids = ids
|
||||
}
|
||||
}
|
||||
|
||||
ginx.NewRender(c).Data(boards, err)
|
||||
}
|
||||
|
||||
func (rt *Router) boardClone(c *gin.Context) {
|
||||
me := c.MustGet("user").(*models.User)
|
||||
bo := rt.Board(ginx.UrlParamInt64(c, "bid"))
|
||||
|
||||
newBoard := &models.Board{
|
||||
Name: bo.Name + " Copy",
|
||||
Tags: bo.Tags,
|
||||
GroupId: bo.GroupId,
|
||||
CreateBy: me.Username,
|
||||
UpdateBy: me.Username,
|
||||
}
|
||||
|
||||
if bo.Ident != "" {
|
||||
newBoard.Ident = uuid.NewString()
|
||||
}
|
||||
newBoard := bo.Clone(me.Username, bo.GroupId, " Cloned")
|
||||
|
||||
ginx.Dangerous(newBoard.Add(rt.Ctx))
|
||||
|
||||
@@ -235,3 +314,39 @@ func (rt *Router) boardClone(c *gin.Context) {
|
||||
|
||||
ginx.NewRender(c).Message(nil)
|
||||
}
|
||||
|
||||
type boardsForm struct {
|
||||
BoardIds []int64 `json:"board_ids"`
|
||||
Bgids []int64 `json:"bgids"`
|
||||
}
|
||||
|
||||
func (rt *Router) boardBatchClone(c *gin.Context) {
|
||||
me := c.MustGet("user").(*models.User)
|
||||
var f boardsForm
|
||||
ginx.BindJSON(c, &f)
|
||||
|
||||
for _, bgid := range f.Bgids {
|
||||
rt.bgrwCheck(c, bgid)
|
||||
}
|
||||
|
||||
reterr := make(map[string]string, len(f.BoardIds))
|
||||
lang := c.GetHeader("X-Language")
|
||||
|
||||
for _, bgid := range f.Bgids {
|
||||
for _, bid := range f.BoardIds {
|
||||
bo := rt.Board(bid)
|
||||
newBoard := bo.Clone(me.Username, bgid, "")
|
||||
payload, err := models.BoardPayloadGet(rt.Ctx, bo.Id)
|
||||
if err != nil {
|
||||
reterr[fmt.Sprintf("%s-%d", newBoard.Name, bgid)] = i18n.Sprintf(lang, err.Error())
|
||||
continue
|
||||
}
|
||||
|
||||
if err = newBoard.AtomicAdd(rt.Ctx, payload); err != nil {
|
||||
reterr[fmt.Sprintf("%s-%d", newBoard.Name, bgid)] = i18n.Sprintf(lang, err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ginx.NewRender(c).Data(reterr, nil)
|
||||
}
|
||||
|
||||
@@ -78,7 +78,7 @@ func (rt *Router) builtinBoardCateGets(c *gin.Context) {
|
||||
}
|
||||
|
||||
me := c.MustGet("user").(*models.User)
|
||||
buildinFavoritesMap, err := models.BuiltinCateGetByUserId(rt.Ctx, me.Id)
|
||||
builtinFavoritesMap, err := models.BuiltinCateGetByUserId(rt.Ctx, me.Id)
|
||||
if err != nil {
|
||||
logger.Warningf("get builtin favorites fail: %v", err)
|
||||
}
|
||||
@@ -91,6 +91,9 @@ func (rt *Router) builtinBoardCateGets(c *gin.Context) {
|
||||
boardCate.Name = dir
|
||||
files, err := file.FilesUnder(fp + "/" + dir + "/dashboards")
|
||||
ginx.Dangerous(err)
|
||||
if len(files) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
var boards []Payload
|
||||
for _, f := range files {
|
||||
@@ -114,7 +117,7 @@ func (rt *Router) builtinBoardCateGets(c *gin.Context) {
|
||||
}
|
||||
boardCate.Boards = boards
|
||||
|
||||
if _, ok := buildinFavoritesMap[dir]; ok {
|
||||
if _, ok := builtinFavoritesMap[dir]; ok {
|
||||
boardCate.Favorite = true
|
||||
}
|
||||
|
||||
@@ -170,7 +173,7 @@ func (rt *Router) builtinAlertCateGets(c *gin.Context) {
|
||||
}
|
||||
|
||||
me := c.MustGet("user").(*models.User)
|
||||
buildinFavoritesMap, err := models.BuiltinCateGetByUserId(rt.Ctx, me.Id)
|
||||
builtinFavoritesMap, err := models.BuiltinCateGetByUserId(rt.Ctx, me.Id)
|
||||
if err != nil {
|
||||
logger.Warningf("get builtin favorites fail: %v", err)
|
||||
}
|
||||
@@ -207,7 +210,7 @@ func (rt *Router) builtinAlertCateGets(c *gin.Context) {
|
||||
alertCate.IconUrl = fmt.Sprintf("/api/n9e/integrations/icon/%s/%s", dir, iconFiles[0])
|
||||
}
|
||||
|
||||
if _, ok := buildinFavoritesMap[dir]; ok {
|
||||
if _, ok := builtinFavoritesMap[dir]; ok {
|
||||
alertCate.Favorite = true
|
||||
}
|
||||
|
||||
@@ -230,7 +233,7 @@ func (rt *Router) builtinAlertRules(c *gin.Context) {
|
||||
}
|
||||
|
||||
me := c.MustGet("user").(*models.User)
|
||||
buildinFavoritesMap, err := models.BuiltinCateGetByUserId(rt.Ctx, me.Id)
|
||||
builtinFavoritesMap, err := models.BuiltinCateGetByUserId(rt.Ctx, me.Id)
|
||||
if err != nil {
|
||||
logger.Warningf("get builtin favorites fail: %v", err)
|
||||
}
|
||||
@@ -243,6 +246,9 @@ func (rt *Router) builtinAlertRules(c *gin.Context) {
|
||||
alertCate.Name = dir
|
||||
files, err := file.FilesUnder(fp + "/" + dir + "/alerts")
|
||||
ginx.Dangerous(err)
|
||||
if len(files) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
alertRules := make(map[string][]models.AlertRule)
|
||||
for _, f := range files {
|
||||
@@ -268,7 +274,7 @@ func (rt *Router) builtinAlertRules(c *gin.Context) {
|
||||
alertCate.IconUrl = fmt.Sprintf("/api/n9e/integrations/icon/%s/%s", dir, iconFiles[0])
|
||||
}
|
||||
|
||||
if _, ok := buildinFavoritesMap[dir]; ok {
|
||||
if _, ok := builtinFavoritesMap[dir]; ok {
|
||||
alertCate.Favorite = true
|
||||
}
|
||||
|
||||
@@ -309,3 +315,26 @@ func (rt *Router) builtinIcon(c *gin.Context) {
|
||||
iconPath := fp + "/" + cate + "/icon/" + ginx.UrlParamStr(c, "name")
|
||||
c.File(path.Join(iconPath))
|
||||
}
|
||||
|
||||
func (rt *Router) builtinMarkdown(c *gin.Context) {
|
||||
fp := rt.Center.BuiltinIntegrationsDir
|
||||
if fp == "" {
|
||||
fp = path.Join(runner.Cwd, "integrations")
|
||||
}
|
||||
cate := ginx.UrlParamStr(c, "cate")
|
||||
|
||||
var markdown []byte
|
||||
markdownDir := fp + "/" + cate + "/markdown"
|
||||
markdownFiles, err := file.FilesUnder(markdownDir)
|
||||
if err != nil {
|
||||
logger.Warningf("get markdown fail: %v", err)
|
||||
} else if len(markdownFiles) > 0 {
|
||||
f := markdownFiles[0]
|
||||
fn := markdownDir + "/" + f
|
||||
markdown, err = file.ReadBytes(fn)
|
||||
if err != nil {
|
||||
logger.Warningf("get collect fail: %v", err)
|
||||
}
|
||||
}
|
||||
ginx.NewRender(c).Data(string(markdown), nil)
|
||||
}
|
||||
|
||||
66
center/router/router_builtin_componet.go
Normal file
66
center/router/router_builtin_componet.go
Normal file
@@ -0,0 +1,66 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/ccfos/nightingale/v6/models"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/toolkits/pkg/ginx"
|
||||
)
|
||||
|
||||
func (rt *Router) builtinComponentsAdd(c *gin.Context) {
|
||||
var lst []models.BuiltinComponent
|
||||
ginx.BindJSON(c, &lst)
|
||||
|
||||
username := Username(c)
|
||||
|
||||
count := len(lst)
|
||||
if count == 0 {
|
||||
ginx.Bomb(http.StatusBadRequest, "input json is empty")
|
||||
}
|
||||
|
||||
reterr := make(map[string]string)
|
||||
for i := 0; i < count; i++ {
|
||||
if err := lst[i].Add(rt.Ctx, username); err != nil {
|
||||
reterr[lst[i].Ident] = err.Error()
|
||||
}
|
||||
}
|
||||
|
||||
ginx.NewRender(c).Data(reterr, nil)
|
||||
}
|
||||
|
||||
func (rt *Router) builtinComponentsGets(c *gin.Context) {
|
||||
query := ginx.QueryStr(c, "query", "")
|
||||
|
||||
bc, err := models.BuiltinComponentGets(rt.Ctx, query)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
ginx.NewRender(c).Data(bc, nil)
|
||||
}
|
||||
|
||||
func (rt *Router) builtinComponentsPut(c *gin.Context) {
|
||||
var req models.BuiltinComponent
|
||||
ginx.BindJSON(c, &req)
|
||||
|
||||
bc, err := models.BuiltinComponentGet(rt.Ctx, "id = ?", req.ID)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
if bc == nil {
|
||||
ginx.NewRender(c, http.StatusNotFound).Message("No such builtin component")
|
||||
return
|
||||
}
|
||||
|
||||
username := Username(c)
|
||||
req.UpdatedBy = username
|
||||
|
||||
ginx.NewRender(c).Message(bc.Update(rt.Ctx, req))
|
||||
}
|
||||
|
||||
func (rt *Router) builtinComponentsDel(c *gin.Context) {
|
||||
var req idsForm
|
||||
ginx.BindJSON(c, &req)
|
||||
|
||||
req.Verify()
|
||||
|
||||
ginx.NewRender(c).Message(models.BuiltinComponentDels(rt.Ctx, req.Ids))
|
||||
}
|
||||
120
center/router/router_builtin_metric_filter.go
Normal file
120
center/router/router_builtin_metric_filter.go
Normal file
@@ -0,0 +1,120 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"github.com/ccfos/nightingale/v6/models"
|
||||
"github.com/ccfos/nightingale/v6/pkg/prom"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/toolkits/pkg/ginx"
|
||||
)
|
||||
|
||||
func (rt *Router) metricFilterGets(c *gin.Context) {
|
||||
lst, err := models.MetricFilterGets(rt.Ctx, "")
|
||||
ginx.Dangerous(err)
|
||||
me := c.MustGet("user").(*models.User)
|
||||
|
||||
gids, err := models.MyGroupIds(rt.Ctx, me.Id)
|
||||
ginx.Dangerous(err)
|
||||
arr := make([]models.MetricFilter, 0)
|
||||
|
||||
for _, f := range lst {
|
||||
if me.Username == f.CreateBy {
|
||||
arr = append(arr, f)
|
||||
continue
|
||||
}
|
||||
|
||||
if HasPerm(gids, f.GroupsPerm, false) {
|
||||
arr = append(arr, f)
|
||||
}
|
||||
}
|
||||
|
||||
ginx.NewRender(c).Data(arr, err)
|
||||
}
|
||||
|
||||
func (rt *Router) metricFilterAdd(c *gin.Context) {
|
||||
var f models.MetricFilter
|
||||
ginx.BindJSON(c, &f)
|
||||
me := c.MustGet("user").(*models.User)
|
||||
|
||||
f.CreateBy = me.Username
|
||||
f.UpdateBy = me.Username
|
||||
ginx.Dangerous(f.Add(rt.Ctx))
|
||||
ginx.NewRender(c).Data(f, nil)
|
||||
}
|
||||
|
||||
func (rt *Router) metricFilterDel(c *gin.Context) {
|
||||
var f idsForm
|
||||
ginx.BindJSON(c, &f)
|
||||
f.Verify()
|
||||
|
||||
me := c.MustGet("user").(*models.User)
|
||||
|
||||
for _, id := range f.Ids {
|
||||
old, err := models.MetricFilterGet(rt.Ctx, id)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
if me.Username != old.CreateBy {
|
||||
gids, err := models.MyGroupIds(rt.Ctx, me.Id)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
if !HasPerm(gids, old.GroupsPerm, true) {
|
||||
ginx.NewRender(c).Message("no permission")
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ginx.NewRender(c).Message(models.MetricFilterDel(rt.Ctx, f.Ids))
|
||||
}
|
||||
|
||||
func (rt *Router) metricFilterPut(c *gin.Context) {
|
||||
var f models.MetricFilter
|
||||
ginx.BindJSON(c, &f)
|
||||
|
||||
me := c.MustGet("user").(*models.User)
|
||||
old, err := models.MetricFilterGet(rt.Ctx, f.ID)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
if me.Username != old.CreateBy {
|
||||
gids, err := models.MyGroupIds(rt.Ctx, me.Id)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
if !HasPerm(gids, old.GroupsPerm, true) {
|
||||
ginx.NewRender(c).Message("no permission")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
f.UpdateBy = me.Username
|
||||
ginx.NewRender(c).Message(f.Update(rt.Ctx))
|
||||
}
|
||||
|
||||
type metricPromqlReq struct {
|
||||
LabelFilter string `json:"label_filter"`
|
||||
Promql string `json:"promql"`
|
||||
}
|
||||
|
||||
func (rt *Router) getMetricPromql(c *gin.Context) {
|
||||
var req metricPromqlReq
|
||||
ginx.BindJSON(c, &req)
|
||||
|
||||
promql := prom.AddLabelToPromQL(req.LabelFilter, req.Promql)
|
||||
ginx.NewRender(c).Data(promql, nil)
|
||||
}
|
||||
|
||||
func HasPerm(gids []int64, gps []models.GroupPerm, checkWrite bool) bool {
|
||||
gmap := make(map[int64]struct{})
|
||||
for _, gp := range gps {
|
||||
if checkWrite && !gp.Write {
|
||||
continue
|
||||
}
|
||||
gmap[gp.Gid] = struct{}{}
|
||||
}
|
||||
|
||||
for _, gid := range gids {
|
||||
if _, ok := gmap[gid]; ok {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
116
center/router/router_builtin_metrics.go
Normal file
116
center/router/router_builtin_metrics.go
Normal file
@@ -0,0 +1,116 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/ccfos/nightingale/v6/models"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/toolkits/pkg/ginx"
|
||||
"github.com/toolkits/pkg/i18n"
|
||||
)
|
||||
|
||||
// single or import
|
||||
func (rt *Router) builtinMetricsAdd(c *gin.Context) {
|
||||
var lst []models.BuiltinMetric
|
||||
ginx.BindJSON(c, &lst)
|
||||
username := Username(c)
|
||||
count := len(lst)
|
||||
if count == 0 {
|
||||
ginx.Bomb(http.StatusBadRequest, "input json is empty")
|
||||
}
|
||||
|
||||
lang := c.GetHeader("X-Language")
|
||||
if lang == "" {
|
||||
lang = "zh_CN"
|
||||
}
|
||||
|
||||
reterr := make(map[string]string)
|
||||
for i := 0; i < count; i++ {
|
||||
lst[i].Lang = lang
|
||||
lst[i].UUID = time.Now().UnixNano()
|
||||
if err := lst[i].Add(rt.Ctx, username); err != nil {
|
||||
reterr[lst[i].Name] = i18n.Sprintf(c.GetHeader("X-Language"), err.Error())
|
||||
}
|
||||
}
|
||||
ginx.NewRender(c).Data(reterr, nil)
|
||||
}
|
||||
|
||||
func (rt *Router) builtinMetricsGets(c *gin.Context) {
|
||||
collector := ginx.QueryStr(c, "collector", "")
|
||||
typ := ginx.QueryStr(c, "typ", "")
|
||||
query := ginx.QueryStr(c, "query", "")
|
||||
limit := ginx.QueryInt(c, "limit", 20)
|
||||
lang := c.GetHeader("X-Language")
|
||||
unit := ginx.QueryStr(c, "unit", "")
|
||||
if lang == "" {
|
||||
lang = "zh_CN"
|
||||
}
|
||||
|
||||
bm, err := models.BuiltinMetricGets(rt.Ctx, lang, collector, typ, query, unit, limit, ginx.Offset(c, limit))
|
||||
ginx.Dangerous(err)
|
||||
|
||||
total, err := models.BuiltinMetricCount(rt.Ctx, lang, collector, typ, query, unit)
|
||||
ginx.Dangerous(err)
|
||||
ginx.NewRender(c).Data(gin.H{
|
||||
"list": bm,
|
||||
"total": total,
|
||||
}, nil)
|
||||
}
|
||||
|
||||
func (rt *Router) builtinMetricsPut(c *gin.Context) {
|
||||
var req models.BuiltinMetric
|
||||
ginx.BindJSON(c, &req)
|
||||
|
||||
bm, err := models.BuiltinMetricGet(rt.Ctx, "id = ?", req.ID)
|
||||
ginx.Dangerous(err)
|
||||
if bm == nil {
|
||||
ginx.NewRender(c, http.StatusNotFound).Message("No such builtin metric")
|
||||
return
|
||||
}
|
||||
username := Username(c)
|
||||
|
||||
req.UpdatedBy = username
|
||||
ginx.NewRender(c).Message(bm.Update(rt.Ctx, req))
|
||||
}
|
||||
|
||||
func (rt *Router) builtinMetricsDel(c *gin.Context) {
|
||||
var req idsForm
|
||||
ginx.BindJSON(c, &req)
|
||||
req.Verify()
|
||||
|
||||
ginx.NewRender(c).Message(models.BuiltinMetricDels(rt.Ctx, req.Ids))
|
||||
}
|
||||
|
||||
func (rt *Router) builtinMetricsDefaultTypes(c *gin.Context) {
|
||||
lst := []string{
|
||||
"Linux",
|
||||
"cAdvisor",
|
||||
"Ping",
|
||||
"MySQL",
|
||||
"Redis",
|
||||
"Kafka",
|
||||
"Elasticsearch",
|
||||
"PostgreSQL",
|
||||
"MongoDB",
|
||||
"Memcached",
|
||||
}
|
||||
ginx.NewRender(c).Data(lst, nil)
|
||||
}
|
||||
|
||||
func (rt *Router) builtinMetricsTypes(c *gin.Context) {
|
||||
collector := ginx.QueryStr(c, "collector", "")
|
||||
query := ginx.QueryStr(c, "query", "")
|
||||
lang := c.GetHeader("X-Language")
|
||||
|
||||
ginx.NewRender(c).Data(models.BuiltinMetricTypes(rt.Ctx, lang, collector, query))
|
||||
}
|
||||
|
||||
func (rt *Router) builtinMetricsCollectors(c *gin.Context) {
|
||||
typ := ginx.QueryStr(c, "typ", "")
|
||||
query := ginx.QueryStr(c, "query", "")
|
||||
lang := c.GetHeader("X-Language")
|
||||
|
||||
ginx.NewRender(c).Data(models.BuiltinMetricCollectors(rt.Ctx, lang, typ, query))
|
||||
}
|
||||
247
center/router/router_builtin_payload.go
Normal file
247
center/router/router_builtin_payload.go
Normal file
@@ -0,0 +1,247 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/ccfos/nightingale/v6/models"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/toolkits/pkg/ginx"
|
||||
"github.com/toolkits/pkg/i18n"
|
||||
)
|
||||
|
||||
type Board struct {
|
||||
Name string `json:"name"`
|
||||
Tags string `json:"tags"`
|
||||
Configs interface{} `json:"configs"`
|
||||
UUID int64 `json:"uuid"`
|
||||
}
|
||||
|
||||
func (rt *Router) builtinPayloadsAdd(c *gin.Context) {
|
||||
var lst []models.BuiltinPayload
|
||||
ginx.BindJSON(c, &lst)
|
||||
|
||||
username := Username(c)
|
||||
|
||||
count := len(lst)
|
||||
if count == 0 {
|
||||
ginx.Bomb(http.StatusBadRequest, "input json is empty")
|
||||
}
|
||||
|
||||
reterr := make(map[string]string)
|
||||
for i := 0; i < count; i++ {
|
||||
if lst[i].Type == "alert" {
|
||||
if strings.HasPrefix(strings.TrimSpace(lst[i].Content), "[") {
|
||||
// 处理多个告警规则模板的情况
|
||||
alertRules := []models.AlertRule{}
|
||||
if err := json.Unmarshal([]byte(lst[i].Content), &alertRules); err != nil {
|
||||
reterr[lst[i].Name] = err.Error()
|
||||
}
|
||||
|
||||
for _, rule := range alertRules {
|
||||
if rule.UUID == 0 {
|
||||
rule.UUID = time.Now().UnixNano()
|
||||
}
|
||||
|
||||
contentBytes, err := json.Marshal(rule)
|
||||
if err != nil {
|
||||
reterr[rule.Name] = err.Error()
|
||||
continue
|
||||
}
|
||||
|
||||
bp := models.BuiltinPayload{
|
||||
Type: lst[i].Type,
|
||||
Component: lst[i].Component,
|
||||
Cate: lst[i].Cate,
|
||||
Name: rule.Name,
|
||||
Tags: rule.AppendTags,
|
||||
UUID: rule.UUID,
|
||||
Content: string(contentBytes),
|
||||
CreatedBy: username,
|
||||
UpdatedBy: username,
|
||||
}
|
||||
|
||||
if err := bp.Add(rt.Ctx, username); err != nil {
|
||||
reterr[bp.Name] = i18n.Sprintf(c.GetHeader("X-Language"), err.Error())
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
alertRule := models.AlertRule{}
|
||||
if err := json.Unmarshal([]byte(lst[i].Content), &alertRule); err != nil {
|
||||
reterr[lst[i].Name] = err.Error()
|
||||
continue
|
||||
}
|
||||
|
||||
if alertRule.UUID == 0 {
|
||||
alertRule.UUID = time.Now().UnixNano()
|
||||
}
|
||||
|
||||
bp := models.BuiltinPayload{
|
||||
Type: lst[i].Type,
|
||||
Component: lst[i].Component,
|
||||
Cate: lst[i].Cate,
|
||||
Name: alertRule.Name,
|
||||
Tags: alertRule.AppendTags,
|
||||
UUID: alertRule.UUID,
|
||||
Content: lst[i].Content,
|
||||
CreatedBy: username,
|
||||
UpdatedBy: username,
|
||||
}
|
||||
|
||||
if err := bp.Add(rt.Ctx, username); err != nil {
|
||||
reterr[bp.Name] = i18n.Sprintf(c.GetHeader("X-Language"), err.Error())
|
||||
}
|
||||
} else if lst[i].Type == "dashboard" {
|
||||
if strings.HasPrefix(strings.TrimSpace(lst[i].Content), "[") {
|
||||
// 处理多个告警规则模板的情况
|
||||
dashboards := []Board{}
|
||||
if err := json.Unmarshal([]byte(lst[i].Content), &dashboards); err != nil {
|
||||
reterr[lst[i].Name] = err.Error()
|
||||
}
|
||||
|
||||
for _, dashboard := range dashboards {
|
||||
if dashboard.UUID == 0 {
|
||||
dashboard.UUID = time.Now().UnixNano()
|
||||
}
|
||||
|
||||
contentBytes, err := json.Marshal(dashboard)
|
||||
if err != nil {
|
||||
reterr[dashboard.Name] = err.Error()
|
||||
continue
|
||||
}
|
||||
|
||||
bp := models.BuiltinPayload{
|
||||
Type: lst[i].Type,
|
||||
Component: lst[i].Component,
|
||||
Cate: lst[i].Cate,
|
||||
Name: dashboard.Name,
|
||||
Tags: dashboard.Tags,
|
||||
UUID: dashboard.UUID,
|
||||
Content: string(contentBytes),
|
||||
CreatedBy: username,
|
||||
UpdatedBy: username,
|
||||
}
|
||||
|
||||
if err := bp.Add(rt.Ctx, username); err != nil {
|
||||
reterr[bp.Name] = i18n.Sprintf(c.GetHeader("X-Language"), err.Error())
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
dashboard := Board{}
|
||||
if err := json.Unmarshal([]byte(lst[i].Content), &dashboard); err != nil {
|
||||
reterr[lst[i].Name] = i18n.Sprintf(c.GetHeader("X-Language"), err.Error())
|
||||
continue
|
||||
}
|
||||
|
||||
if dashboard.UUID == 0 {
|
||||
dashboard.UUID = time.Now().UnixNano()
|
||||
}
|
||||
|
||||
bp := models.BuiltinPayload{
|
||||
Type: lst[i].Type,
|
||||
Component: lst[i].Component,
|
||||
Cate: lst[i].Cate,
|
||||
Name: dashboard.Name,
|
||||
Tags: dashboard.Tags,
|
||||
UUID: dashboard.UUID,
|
||||
Content: lst[i].Content,
|
||||
CreatedBy: username,
|
||||
UpdatedBy: username,
|
||||
}
|
||||
|
||||
if err := bp.Add(rt.Ctx, username); err != nil {
|
||||
reterr[bp.Name] = i18n.Sprintf(c.GetHeader("X-Language"), err.Error())
|
||||
}
|
||||
} else {
|
||||
if err := lst[i].Add(rt.Ctx, username); err != nil {
|
||||
reterr[lst[i].Name] = i18n.Sprintf(c.GetHeader("X-Language"), err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
ginx.NewRender(c).Data(reterr, nil)
|
||||
}
|
||||
|
||||
func (rt *Router) builtinPayloadsGets(c *gin.Context) {
|
||||
typ := ginx.QueryStr(c, "type", "")
|
||||
component := ginx.QueryStr(c, "component", "")
|
||||
cate := ginx.QueryStr(c, "cate", "")
|
||||
query := ginx.QueryStr(c, "query", "")
|
||||
|
||||
lst, err := models.BuiltinPayloadGets(rt.Ctx, typ, component, cate, query)
|
||||
ginx.NewRender(c).Data(lst, err)
|
||||
}
|
||||
|
||||
func (rt *Router) builtinPayloadcatesGet(c *gin.Context) {
|
||||
typ := ginx.QueryStr(c, "type", "")
|
||||
component := ginx.QueryStr(c, "component", "")
|
||||
|
||||
cates, err := models.BuiltinPayloadCates(rt.Ctx, typ, component)
|
||||
ginx.NewRender(c).Data(cates, err)
|
||||
}
|
||||
|
||||
func (rt *Router) builtinPayloadGet(c *gin.Context) {
|
||||
id := ginx.UrlParamInt64(c, "id")
|
||||
|
||||
bp, err := models.BuiltinPayloadGet(rt.Ctx, "id = ?", id)
|
||||
if err != nil {
|
||||
ginx.Bomb(http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
if bp == nil {
|
||||
ginx.Bomb(http.StatusNotFound, "builtin payload not found")
|
||||
}
|
||||
|
||||
ginx.NewRender(c).Data(bp, nil)
|
||||
}
|
||||
|
||||
func (rt *Router) builtinPayloadsPut(c *gin.Context) {
|
||||
var req models.BuiltinPayload
|
||||
ginx.BindJSON(c, &req)
|
||||
|
||||
bp, err := models.BuiltinPayloadGet(rt.Ctx, "id = ?", req.ID)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
if bp == nil {
|
||||
ginx.NewRender(c, http.StatusNotFound).Message("No such builtin payload")
|
||||
return
|
||||
}
|
||||
|
||||
if req.Type == "alert" {
|
||||
alertRule := models.AlertRule{}
|
||||
if err := json.Unmarshal([]byte(req.Content), &alertRule); err != nil {
|
||||
ginx.Bomb(http.StatusBadRequest, err.Error())
|
||||
}
|
||||
|
||||
req.Name = alertRule.Name
|
||||
req.Tags = alertRule.AppendTags
|
||||
} else if req.Type == "dashboard" {
|
||||
dashboard := Board{}
|
||||
if err := json.Unmarshal([]byte(req.Content), &dashboard); err != nil {
|
||||
ginx.Bomb(http.StatusBadRequest, err.Error())
|
||||
}
|
||||
|
||||
req.Name = dashboard.Name
|
||||
req.Tags = dashboard.Tags
|
||||
}
|
||||
|
||||
username := Username(c)
|
||||
req.UpdatedBy = username
|
||||
|
||||
ginx.NewRender(c).Message(bp.Update(rt.Ctx, req))
|
||||
}
|
||||
|
||||
func (rt *Router) builtinPayloadsDel(c *gin.Context) {
|
||||
var req idsForm
|
||||
ginx.BindJSON(c, &req)
|
||||
|
||||
req.Verify()
|
||||
|
||||
ginx.NewRender(c).Message(models.BuiltinPayloadDels(rt.Ctx, req.Ids))
|
||||
}
|
||||
114
center/router/router_captcha.go
Normal file
114
center/router/router_captcha.go
Normal file
@@ -0,0 +1,114 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/ccfos/nightingale/v6/storage"
|
||||
"github.com/gin-gonic/gin"
|
||||
captcha "github.com/mojocn/base64Captcha"
|
||||
"github.com/toolkits/pkg/ginx"
|
||||
"github.com/toolkits/pkg/logger"
|
||||
)
|
||||
|
||||
type CaptchaRedisStore struct {
|
||||
redis storage.Redis
|
||||
}
|
||||
|
||||
func (s *CaptchaRedisStore) Set(id string, value string) error {
|
||||
ctx := context.Background()
|
||||
err := s.redis.Set(ctx, id, value, time.Duration(300*time.Second)).Err()
|
||||
if err != nil {
|
||||
logger.Errorf("captcha id set to redis error : %s", err.Error())
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *CaptchaRedisStore) Get(id string, clear bool) string {
|
||||
ctx := context.Background()
|
||||
val, err := s.redis.Get(ctx, id).Result()
|
||||
if err != nil {
|
||||
logger.Errorf("captcha id get from redis error : %s", err.Error())
|
||||
return ""
|
||||
}
|
||||
|
||||
if clear {
|
||||
s.redis.Del(ctx, id)
|
||||
}
|
||||
|
||||
return val
|
||||
}
|
||||
|
||||
func (s *CaptchaRedisStore) Verify(id, answer string, clear bool) bool {
|
||||
|
||||
old := s.Get(id, clear)
|
||||
return old == answer
|
||||
}
|
||||
|
||||
func (rt *Router) newCaptchaRedisStore() *CaptchaRedisStore {
|
||||
if captchaStore == nil {
|
||||
captchaStore = &CaptchaRedisStore{redis: rt.Redis}
|
||||
}
|
||||
return captchaStore
|
||||
}
|
||||
|
||||
var captchaStore *CaptchaRedisStore
|
||||
|
||||
type CaptchaReqBody struct {
|
||||
Id string
|
||||
VerifyValue string
|
||||
}
|
||||
|
||||
// 生成图形验证码
|
||||
func (rt *Router) generateCaptcha(c *gin.Context) {
|
||||
var driver = captcha.NewDriverMath(60, 200, 0, captcha.OptionShowHollowLine, nil, nil, []string{"wqy-microhei.ttc"})
|
||||
cc := captcha.NewCaptcha(driver, rt.newCaptchaRedisStore())
|
||||
//data:image/png;base64
|
||||
id, b64s, _, err := cc.Generate()
|
||||
|
||||
if err != nil {
|
||||
ginx.NewRender(c).Message(err)
|
||||
return
|
||||
}
|
||||
|
||||
ginx.NewRender(c).Data(gin.H{
|
||||
"imgdata": b64s,
|
||||
"captchaid": id,
|
||||
}, nil)
|
||||
}
|
||||
|
||||
// 验证
|
||||
func (rt *Router) captchaVerify(c *gin.Context) {
|
||||
|
||||
var param CaptchaReqBody
|
||||
ginx.BindJSON(c, ¶m)
|
||||
|
||||
//verify the captcha
|
||||
if captchaStore.Verify(param.Id, param.VerifyValue, true) {
|
||||
ginx.NewRender(c).Message("")
|
||||
return
|
||||
}
|
||||
ginx.NewRender(c).Message("incorrect verification code")
|
||||
}
|
||||
|
||||
// 验证码开关
|
||||
func (rt *Router) ifShowCaptcha(c *gin.Context) {
|
||||
|
||||
if rt.HTTP.ShowCaptcha.Enable {
|
||||
ginx.NewRender(c).Data(gin.H{
|
||||
"show": true,
|
||||
}, nil)
|
||||
return
|
||||
}
|
||||
|
||||
ginx.NewRender(c).Data(gin.H{
|
||||
"show": false,
|
||||
}, nil)
|
||||
}
|
||||
|
||||
// 验证
|
||||
func CaptchaVerify(id string, value string) bool {
|
||||
//verify the captcha
|
||||
return captchaStore.Verify(id, value, true)
|
||||
}
|
||||
@@ -62,3 +62,8 @@ func (rt *Router) contactKeysGets(c *gin.Context) {
|
||||
|
||||
ginx.NewRender(c).Data(labelAndKeys, nil)
|
||||
}
|
||||
|
||||
func (rt *Router) siteInfo(c *gin.Context) {
|
||||
config, err := models.ConfigsGet(rt.Ctx, "site_info")
|
||||
ginx.NewRender(c).Data(config, err)
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package router
|
||||
|
||||
import (
|
||||
"github.com/ccfos/nightingale/v6/models"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/toolkits/pkg/ginx"
|
||||
@@ -25,28 +26,49 @@ func (rt *Router) configGetByKey(c *gin.Context) {
|
||||
ginx.NewRender(c).Data(config, err)
|
||||
}
|
||||
|
||||
func (rt *Router) configPutByKey(c *gin.Context) {
|
||||
var f models.Configs
|
||||
ginx.BindJSON(c, &f)
|
||||
username := c.MustGet("username").(string)
|
||||
ginx.NewRender(c).Message(models.ConfigsSetWithUname(rt.Ctx, f.Ckey, f.Cval, username))
|
||||
}
|
||||
|
||||
func (rt *Router) configsDel(c *gin.Context) {
|
||||
var f idsForm
|
||||
ginx.BindJSON(c, &f)
|
||||
ginx.NewRender(c).Message(models.ConfigsDel(rt.Ctx, f.Ids))
|
||||
}
|
||||
|
||||
func (rt *Router) configsPut(c *gin.Context) {
|
||||
func (rt *Router) configsPut(c *gin.Context) { //for APIForService
|
||||
var arr []models.Configs
|
||||
ginx.BindJSON(c, &arr)
|
||||
|
||||
username := c.GetString("user")
|
||||
if username == "" {
|
||||
username = "default"
|
||||
}
|
||||
now := time.Now().Unix()
|
||||
for i := 0; i < len(arr); i++ {
|
||||
arr[i].UpdateBy = username
|
||||
arr[i].UpdateAt = now
|
||||
ginx.Dangerous(arr[i].Update(rt.Ctx))
|
||||
}
|
||||
|
||||
ginx.NewRender(c).Message(nil)
|
||||
}
|
||||
|
||||
func (rt *Router) configsPost(c *gin.Context) {
|
||||
func (rt *Router) configsPost(c *gin.Context) { //for APIForService
|
||||
var arr []models.Configs
|
||||
ginx.BindJSON(c, &arr)
|
||||
|
||||
username := c.GetString("user")
|
||||
if username == "" {
|
||||
username = "default"
|
||||
}
|
||||
now := time.Now().Unix()
|
||||
for i := 0; i < len(arr); i++ {
|
||||
arr[i].CreateBy = username
|
||||
arr[i].UpdateBy = username
|
||||
arr[i].CreateAt = now
|
||||
arr[i].UpdateAt = now
|
||||
ginx.Dangerous(arr[i].Add(rt.Ctx))
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ package router
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
@@ -25,6 +26,11 @@ type listReq struct {
|
||||
}
|
||||
|
||||
func (rt *Router) datasourceList(c *gin.Context) {
|
||||
if rt.DatasourceCache.DatasourceCheckHook(c) {
|
||||
Render(c, []int{}, nil)
|
||||
return
|
||||
}
|
||||
|
||||
var req listReq
|
||||
ginx.BindJSON(c, &req)
|
||||
|
||||
@@ -32,8 +38,10 @@ func (rt *Router) datasourceList(c *gin.Context) {
|
||||
category := req.Category
|
||||
name := req.Name
|
||||
|
||||
user := c.MustGet("user").(*models.User)
|
||||
|
||||
list, err := models.GetDatasourcesGetsBy(rt.Ctx, typ, category, name, "")
|
||||
Render(c, list, err)
|
||||
Render(c, rt.DatasourceCache.DatasourceFilter(list, user), err)
|
||||
}
|
||||
|
||||
func (rt *Router) datasourceGetsByService(c *gin.Context) {
|
||||
@@ -42,29 +50,40 @@ func (rt *Router) datasourceGetsByService(c *gin.Context) {
|
||||
ginx.NewRender(c).Data(lst, err)
|
||||
}
|
||||
|
||||
type datasourceBrief struct {
|
||||
Id int64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
PluginType string `json:"plugin_type"`
|
||||
}
|
||||
|
||||
func (rt *Router) datasourceBriefs(c *gin.Context) {
|
||||
var dss []datasourceBrief
|
||||
var dss []*models.Datasource
|
||||
list, err := models.GetDatasourcesGetsBy(rt.Ctx, "", "", "", "")
|
||||
ginx.Dangerous(err)
|
||||
|
||||
for i := range list {
|
||||
dss = append(dss, datasourceBrief{
|
||||
Id: list[i].Id,
|
||||
Name: list[i].Name,
|
||||
PluginType: list[i].PluginType,
|
||||
})
|
||||
for _, item := range list {
|
||||
item.AuthJson.BasicAuthPassword = ""
|
||||
if item.PluginType != models.PROMETHEUS {
|
||||
item.SettingsJson = nil
|
||||
} else {
|
||||
for k, v := range item.SettingsJson {
|
||||
if strings.HasPrefix(k, "prometheus.") {
|
||||
item.SettingsJson[strings.TrimPrefix(k, "prometheus.")] = v
|
||||
delete(item.SettingsJson, k)
|
||||
}
|
||||
}
|
||||
}
|
||||
dss = append(dss, item)
|
||||
}
|
||||
|
||||
if !rt.Center.AnonymousAccess.PromQuerier {
|
||||
user := c.MustGet("user").(*models.User)
|
||||
dss = rt.DatasourceCache.DatasourceFilter(dss, user)
|
||||
}
|
||||
|
||||
ginx.NewRender(c).Data(dss, err)
|
||||
}
|
||||
|
||||
func (rt *Router) datasourceUpsert(c *gin.Context) {
|
||||
if rt.DatasourceCache.DatasourceCheckHook(c) {
|
||||
Render(c, []int{}, nil)
|
||||
return
|
||||
}
|
||||
|
||||
var req models.Datasource
|
||||
ginx.BindJSON(c, &req)
|
||||
username := Username(c)
|
||||
@@ -94,7 +113,7 @@ func (rt *Router) datasourceUpsert(c *gin.Context) {
|
||||
}
|
||||
err = req.Add(rt.Ctx)
|
||||
} else {
|
||||
err = req.Update(rt.Ctx, "name", "description", "cluster_name", "settings", "http", "auth", "updated_by", "updated_at")
|
||||
err = req.Update(rt.Ctx, "name", "description", "cluster_name", "settings", "http", "auth", "updated_by", "updated_at", "is_default")
|
||||
}
|
||||
|
||||
Render(c, nil, err)
|
||||
@@ -105,6 +124,10 @@ func DatasourceCheck(ds models.Datasource) error {
|
||||
return fmt.Errorf("url is empty")
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(ds.HTTPJson.Url, "http") {
|
||||
return fmt.Errorf("url must start with http or https")
|
||||
}
|
||||
|
||||
client := &http.Client{
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{
|
||||
@@ -123,16 +146,33 @@ func DatasourceCheck(ds models.Datasource) error {
|
||||
if ds.PluginType == models.PROMETHEUS {
|
||||
subPath := "/api/v1/query"
|
||||
query := url.Values{}
|
||||
if strings.Contains(fullURL, "loki") {
|
||||
if ds.HTTPJson.IsLoki() {
|
||||
subPath = "/api/v1/labels"
|
||||
query.Add("start", "1")
|
||||
query.Add("end", "2")
|
||||
} else {
|
||||
query.Add("query", "1+1")
|
||||
}
|
||||
fullURL = fmt.Sprintf("%s%s?%s", ds.HTTPJson.Url, subPath, query.Encode())
|
||||
|
||||
req, err = http.NewRequest("POST", fullURL, nil)
|
||||
req, err = http.NewRequest("GET", fullURL, nil)
|
||||
if err != nil {
|
||||
logger.Errorf("Error creating request: %v", err)
|
||||
return fmt.Errorf("request url:%s failed", fullURL)
|
||||
}
|
||||
} else if ds.PluginType == models.TDENGINE {
|
||||
fullURL = fmt.Sprintf("%s/rest/sql", ds.HTTPJson.Url)
|
||||
req, err = http.NewRequest("POST", fullURL, strings.NewReader("show databases"))
|
||||
if err != nil {
|
||||
logger.Errorf("Error creating request: %v", err)
|
||||
return fmt.Errorf("request url:%s failed", fullURL)
|
||||
}
|
||||
}
|
||||
|
||||
if ds.PluginType == models.LOKI {
|
||||
subPath := "/api/v1/labels"
|
||||
|
||||
fullURL = fmt.Sprintf("%s%s", ds.HTTPJson.Url, subPath)
|
||||
|
||||
req, err = http.NewRequest("GET", fullURL, nil)
|
||||
if err != nil {
|
||||
logger.Errorf("Error creating request: %v", err)
|
||||
return fmt.Errorf("request url:%s failed", fullURL)
|
||||
@@ -156,13 +196,19 @@ func DatasourceCheck(ds models.Datasource) error {
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
logger.Errorf("Error making request: %v\n", resp.StatusCode)
|
||||
return fmt.Errorf("request url:%s failed code:%d", fullURL, resp.StatusCode)
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
return fmt.Errorf("request url:%s failed code:%d body:%s", fullURL, resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (rt *Router) datasourceGet(c *gin.Context) {
|
||||
if rt.DatasourceCache.DatasourceCheckHook(c) {
|
||||
Render(c, []int{}, nil)
|
||||
return
|
||||
}
|
||||
|
||||
var req models.Datasource
|
||||
ginx.BindJSON(c, &req)
|
||||
err := req.Get(rt.Ctx)
|
||||
@@ -170,6 +216,11 @@ func (rt *Router) datasourceGet(c *gin.Context) {
|
||||
}
|
||||
|
||||
func (rt *Router) datasourceUpdataStatus(c *gin.Context) {
|
||||
if rt.DatasourceCache.DatasourceCheckHook(c) {
|
||||
Render(c, []int{}, nil)
|
||||
return
|
||||
}
|
||||
|
||||
var req models.Datasource
|
||||
ginx.BindJSON(c, &req)
|
||||
username := Username(c)
|
||||
@@ -179,6 +230,11 @@ func (rt *Router) datasourceUpdataStatus(c *gin.Context) {
|
||||
}
|
||||
|
||||
func (rt *Router) datasourceDel(c *gin.Context) {
|
||||
if rt.DatasourceCache.DatasourceCheckHook(c) {
|
||||
Render(c, []int{}, nil)
|
||||
return
|
||||
}
|
||||
|
||||
var ids []int64
|
||||
ginx.BindJSON(c, &ids)
|
||||
err := models.DatasourceDel(rt.Ctx, ids)
|
||||
@@ -191,8 +247,3 @@ func (rt *Router) getDatasourceIds(c *gin.Context) {
|
||||
|
||||
ginx.NewRender(c).Data(datasourceIds, err)
|
||||
}
|
||||
|
||||
func Username(c *gin.Context) string {
|
||||
|
||||
return c.MustGet("username").(string)
|
||||
}
|
||||
|
||||
81
center/router/router_es_index_pattern.go
Normal file
81
center/router/router_es_index_pattern.go
Normal file
@@ -0,0 +1,81 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/ccfos/nightingale/v6/models"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/toolkits/pkg/ginx"
|
||||
)
|
||||
|
||||
// 创建 ES Index Pattern
|
||||
func (rt *Router) esIndexPatternAdd(c *gin.Context) {
|
||||
var f models.EsIndexPattern
|
||||
ginx.BindJSON(c, &f)
|
||||
|
||||
username := c.MustGet("username").(string)
|
||||
now := time.Now().Unix()
|
||||
f.CreateAt = now
|
||||
f.CreateBy = username
|
||||
f.UpdateAt = now
|
||||
f.UpdateBy = username
|
||||
|
||||
err := f.Add(rt.Ctx)
|
||||
ginx.NewRender(c).Message(err)
|
||||
}
|
||||
|
||||
// 更新 ES Index Pattern
|
||||
func (rt *Router) esIndexPatternPut(c *gin.Context) {
|
||||
var f models.EsIndexPattern
|
||||
ginx.BindJSON(c, &f)
|
||||
|
||||
id := ginx.QueryInt64(c, "id")
|
||||
|
||||
esIndexPattern, err := models.EsIndexPatternGetById(rt.Ctx, id)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
if esIndexPattern == nil {
|
||||
ginx.NewRender(c, http.StatusNotFound).Message("No such EsIndexPattern")
|
||||
return
|
||||
}
|
||||
|
||||
f.UpdateBy = c.MustGet("username").(string)
|
||||
|
||||
ginx.NewRender(c).Message(esIndexPattern.Update(rt.Ctx, f))
|
||||
}
|
||||
|
||||
// 删除 ES Index Pattern
|
||||
func (rt *Router) esIndexPatternDel(c *gin.Context) {
|
||||
var f idsForm
|
||||
ginx.BindJSON(c, &f)
|
||||
|
||||
if len(f.Ids) == 0 {
|
||||
ginx.Bomb(http.StatusBadRequest, "ids empty")
|
||||
}
|
||||
|
||||
ginx.NewRender(c).Message(models.EsIndexPatternDel(rt.Ctx, f.Ids))
|
||||
}
|
||||
|
||||
// ES Index Pattern列表
|
||||
func (rt *Router) esIndexPatternGetList(c *gin.Context) {
|
||||
datasourceId := ginx.QueryInt64(c, "datasource_id", 0)
|
||||
|
||||
var lst []*models.EsIndexPattern
|
||||
var err error
|
||||
if datasourceId != 0 {
|
||||
lst, err = models.EsIndexPatternGets(rt.Ctx, "datasource_id = ?", datasourceId)
|
||||
} else {
|
||||
lst, err = models.EsIndexPatternGets(rt.Ctx, "")
|
||||
}
|
||||
|
||||
ginx.NewRender(c).Data(lst, err)
|
||||
}
|
||||
|
||||
// ES Index Pattern 单个数据
|
||||
func (rt *Router) esIndexPatternGet(c *gin.Context) {
|
||||
id := ginx.QueryInt64(c, "id")
|
||||
|
||||
item, err := models.EsIndexPatternGet(rt.Ctx, "id=?", id)
|
||||
ginx.NewRender(c).Data(item, err)
|
||||
}
|
||||
@@ -1,17 +1,14 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/ccfos/nightingale/v6/alert/aconf"
|
||||
"github.com/ccfos/nightingale/v6/models"
|
||||
"github.com/ccfos/nightingale/v6/pkg/ctx"
|
||||
"github.com/ccfos/nightingale/v6/pkg/ibex"
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/toolkits/pkg/ginx"
|
||||
)
|
||||
|
||||
@@ -44,6 +41,10 @@ func (rt *Router) statistic(c *gin.Context) {
|
||||
statistics, err = models.DatasourceStatistics(rt.Ctx)
|
||||
ginx.NewRender(c).Data(statistics, err)
|
||||
return
|
||||
case "user_variable":
|
||||
statistics, err = models.ConfigsUserVariableStatistics(rt.Ctx)
|
||||
ginx.NewRender(c).Data(statistics, err)
|
||||
return
|
||||
default:
|
||||
ginx.Bomb(http.StatusBadRequest, "invalid name")
|
||||
}
|
||||
@@ -65,7 +66,8 @@ func queryDatasourceIds(c *gin.Context) []int64 {
|
||||
}
|
||||
|
||||
type idsForm struct {
|
||||
Ids []int64 `json:"ids"`
|
||||
Ids []int64 `json:"ids"`
|
||||
IsSyncToFlashDuty bool `json:"is_sync_to_flashduty"`
|
||||
}
|
||||
|
||||
func (f idsForm) Verify() {
|
||||
@@ -130,27 +132,11 @@ type TaskCreateReply struct {
|
||||
Dat int64 `json:"dat"` // task.id
|
||||
}
|
||||
|
||||
// return task.id, error
|
||||
func TaskCreate(v interface{}, ibexc aconf.Ibex) (int64, error) {
|
||||
var res TaskCreateReply
|
||||
err := ibex.New(
|
||||
ibexc.Address,
|
||||
ibexc.BasicAuthUser,
|
||||
ibexc.BasicAuthPass,
|
||||
ibexc.Timeout,
|
||||
).
|
||||
Path("/ibex/v1/tasks").
|
||||
In(v).
|
||||
Out(&res).
|
||||
POST()
|
||||
|
||||
if err != nil {
|
||||
return 0, err
|
||||
func Username(c *gin.Context) string {
|
||||
username := c.GetString(gin.AuthUserKey)
|
||||
if username == "" {
|
||||
user := c.MustGet("user").(*models.User)
|
||||
username = user.Username
|
||||
}
|
||||
|
||||
if res.Err != "" {
|
||||
return 0, fmt.Errorf("response.err: %v", res.Err)
|
||||
}
|
||||
|
||||
return res.Dat, nil
|
||||
return username
|
||||
}
|
||||
|
||||
@@ -4,12 +4,15 @@ import (
|
||||
"compress/gzip"
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/ccfos/nightingale/v6/models"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/toolkits/pkg/ginx"
|
||||
"github.com/toolkits/pkg/logger"
|
||||
)
|
||||
|
||||
func (rt *Router) heartbeat(c *gin.Context) {
|
||||
@@ -35,17 +38,60 @@ func (rt *Router) heartbeat(c *gin.Context) {
|
||||
err = json.Unmarshal(bs, &req)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
req.Offset = (time.Now().UnixMilli() - req.UnixTime)
|
||||
req.RemoteAddr = c.ClientIP()
|
||||
// maybe from pushgw
|
||||
if req.Offset == 0 {
|
||||
req.Offset = (time.Now().UnixMilli() - req.UnixTime)
|
||||
}
|
||||
|
||||
if req.RemoteAddr == "" {
|
||||
req.RemoteAddr = c.ClientIP()
|
||||
}
|
||||
|
||||
if req.EngineName == "" {
|
||||
req.EngineName = rt.Alert.Heartbeat.EngineName
|
||||
}
|
||||
|
||||
rt.MetaSet.Set(req.Hostname, req)
|
||||
var items = make(map[string]struct{})
|
||||
items[req.Hostname] = struct{}{}
|
||||
rt.IdentSet.MSet(items)
|
||||
|
||||
gid := ginx.QueryInt64(c, "gid", 0)
|
||||
if target, has := rt.TargetCache.Get(req.Hostname); has && target != nil {
|
||||
gid := ginx.QueryInt64(c, "gid", 0)
|
||||
hostIp := strings.TrimSpace(req.HostIp)
|
||||
|
||||
if gid != 0 {
|
||||
target, has := rt.TargetCache.Get(req.Hostname)
|
||||
if has && target.GroupId != gid {
|
||||
err = models.TargetUpdateBgid(rt.Ctx, []string{req.Hostname}, gid, false)
|
||||
filed := make(map[string]interface{})
|
||||
if gid != 0 && gid != target.GroupId {
|
||||
filed["group_id"] = gid
|
||||
}
|
||||
|
||||
if hostIp != "" && hostIp != target.HostIp {
|
||||
filed["host_ip"] = hostIp
|
||||
}
|
||||
|
||||
if len(req.GlobalLabels) > 0 {
|
||||
lst := []string{}
|
||||
for k, v := range req.GlobalLabels {
|
||||
lst = append(lst, k+"="+v)
|
||||
}
|
||||
sort.Strings(lst)
|
||||
labels := strings.Join(lst, " ")
|
||||
if target.Tags != labels {
|
||||
filed["tags"] = labels
|
||||
}
|
||||
}
|
||||
|
||||
if req.EngineName != "" && req.EngineName != target.EngineName {
|
||||
filed["engine_name"] = req.EngineName
|
||||
}
|
||||
|
||||
if len(filed) > 0 {
|
||||
err := target.UpdateFieldsMap(rt.Ctx, filed)
|
||||
if err != nil {
|
||||
logger.Errorf("update target fields failed, err: %v", err)
|
||||
}
|
||||
}
|
||||
logger.Debugf("heartbeat field:%+v target: %v", filed, *target)
|
||||
}
|
||||
|
||||
ginx.NewRender(c).Message(err)
|
||||
|
||||
@@ -1,50 +1,75 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/ccfos/nightingale/v6/models"
|
||||
"github.com/ccfos/nightingale/v6/pkg/cas"
|
||||
"github.com/ccfos/nightingale/v6/pkg/ldapx"
|
||||
"github.com/ccfos/nightingale/v6/pkg/oauth2x"
|
||||
"github.com/ccfos/nightingale/v6/pkg/oidcx"
|
||||
"github.com/pelletier/go-toml/v2"
|
||||
"github.com/ccfos/nightingale/v6/pkg/secu"
|
||||
|
||||
"github.com/dgrijalva/jwt-go"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/pelletier/go-toml/v2"
|
||||
"github.com/toolkits/pkg/ginx"
|
||||
"github.com/toolkits/pkg/logger"
|
||||
)
|
||||
|
||||
type loginForm struct {
|
||||
Username string `json:"username" binding:"required"`
|
||||
Password string `json:"password" binding:"required"`
|
||||
Username string `json:"username" binding:"required"`
|
||||
Password string `json:"password" binding:"required"`
|
||||
Captchaid string `json:"captchaid"`
|
||||
Verifyvalue string `json:"verifyvalue"`
|
||||
}
|
||||
|
||||
func (rt *Router) loginPost(c *gin.Context) {
|
||||
var f loginForm
|
||||
ginx.BindJSON(c, &f)
|
||||
logger.Infof("username:%s login from:%s", f.Username, c.ClientIP())
|
||||
|
||||
user, err := models.PassLogin(rt.Ctx, f.Username, f.Password)
|
||||
if err != nil {
|
||||
// pass validate fail, try ldap
|
||||
if rt.Sso.LDAP.Enable {
|
||||
roles := strings.Join(rt.Sso.LDAP.DefaultRoles, " ")
|
||||
user, err = models.LdapLogin(rt.Ctx, f.Username, f.Password, roles, rt.Sso.LDAP)
|
||||
if err != nil {
|
||||
logger.Debugf("ldap login failed: %v username: %s", err, f.Username)
|
||||
ginx.NewRender(c).Message(err)
|
||||
return
|
||||
}
|
||||
user.RolesLst = strings.Fields(user.Roles)
|
||||
} else {
|
||||
if rt.HTTP.ShowCaptcha.Enable {
|
||||
if !CaptchaVerify(f.Captchaid, f.Verifyvalue) {
|
||||
ginx.NewRender(c).Message("incorrect verification code")
|
||||
return
|
||||
}
|
||||
}
|
||||
authPassWord := f.Password
|
||||
// need decode
|
||||
if rt.HTTP.RSA.OpenRSA {
|
||||
decPassWord, err := secu.Decrypt(f.Password, rt.HTTP.RSA.RSAPrivateKey, rt.HTTP.RSA.RSAPassWord)
|
||||
if err != nil {
|
||||
logger.Errorf("RSA Decrypt failed: %v username: %s", err, f.Username)
|
||||
ginx.NewRender(c).Message(err)
|
||||
return
|
||||
}
|
||||
authPassWord = decPassWord
|
||||
}
|
||||
|
||||
var user *models.User
|
||||
var err error
|
||||
lc := rt.Sso.LDAP.Copy()
|
||||
if lc.Enable {
|
||||
user, err = ldapx.LdapLogin(rt.Ctx, f.Username, authPassWord, lc.DefaultRoles, lc)
|
||||
if err != nil {
|
||||
logger.Debugf("ldap login failed: %v username: %s", err, f.Username)
|
||||
var errLoginInN9e error
|
||||
// to use n9e as the minimum guarantee for login
|
||||
if user, errLoginInN9e = models.PassLogin(rt.Ctx, f.Username, authPassWord); errLoginInN9e != nil {
|
||||
ginx.NewRender(c).Message("ldap login failed: %v; n9e login failed: %v", err, errLoginInN9e)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
user.RolesLst = strings.Fields(user.Roles)
|
||||
}
|
||||
} else {
|
||||
user, err = models.PassLogin(rt.Ctx, f.Username, authPassWord)
|
||||
ginx.Dangerous(err)
|
||||
}
|
||||
|
||||
if user == nil {
|
||||
@@ -67,6 +92,7 @@ func (rt *Router) loginPost(c *gin.Context) {
|
||||
}
|
||||
|
||||
func (rt *Router) logoutPost(c *gin.Context) {
|
||||
logger.Infof("username:%s logout from:%s", c.GetString("username"), c.ClientIP())
|
||||
metadata, err := rt.extractTokenMetadata(c.Request)
|
||||
if err != nil {
|
||||
ginx.NewRender(c, http.StatusBadRequest).Message("failed to parse jwt token")
|
||||
@@ -79,7 +105,18 @@ func (rt *Router) logoutPost(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
ginx.NewRender(c).Message("")
|
||||
var logoutAddr string
|
||||
user := c.MustGet("user").(*models.User)
|
||||
switch user.Belong {
|
||||
case "oidc":
|
||||
logoutAddr = rt.Sso.OIDC.GetSsoLogoutAddr()
|
||||
case "cas":
|
||||
logoutAddr = rt.Sso.CAS.GetSsoLogoutAddr()
|
||||
case "oauth2":
|
||||
logoutAddr = rt.Sso.OAuth2.GetSsoLogoutAddr()
|
||||
}
|
||||
|
||||
ginx.NewRender(c).Data(logoutAddr, nil)
|
||||
}
|
||||
|
||||
type refreshForm struct {
|
||||
@@ -207,7 +244,7 @@ func (rt *Router) loginCallback(c *gin.Context) {
|
||||
|
||||
ret, err := rt.Sso.OIDC.Callback(rt.Redis, c.Request.Context(), code, state)
|
||||
if err != nil {
|
||||
logger.Debugf("sso.callback() get ret %+v error %v", ret, err)
|
||||
logger.Errorf("sso_callback fail. code:%s, state:%s, get ret: %+v. error: %v", code, state, ret, err)
|
||||
ginx.NewRender(c).Data(CallbackOutput{}, err)
|
||||
return
|
||||
}
|
||||
@@ -217,41 +254,23 @@ func (rt *Router) loginCallback(c *gin.Context) {
|
||||
|
||||
if user != nil {
|
||||
if rt.Sso.OIDC.CoverAttributes {
|
||||
if ret.Nickname != "" {
|
||||
user.Nickname = ret.Nickname
|
||||
}
|
||||
|
||||
if ret.Email != "" {
|
||||
user.Email = ret.Email
|
||||
}
|
||||
|
||||
if ret.Phone != "" {
|
||||
user.Phone = ret.Phone
|
||||
}
|
||||
|
||||
user.UpdateAt = time.Now().Unix()
|
||||
user.Update(rt.Ctx, "email", "nickname", "phone", "update_at")
|
||||
updatedFields := user.UpdateSsoFields("oidc", ret.Nickname, ret.Phone, ret.Email)
|
||||
ginx.Dangerous(user.Update(rt.Ctx, "update_at", updatedFields...))
|
||||
}
|
||||
} else {
|
||||
now := time.Now().Unix()
|
||||
user = &models.User{
|
||||
Username: ret.Username,
|
||||
Password: "******",
|
||||
Nickname: ret.Nickname,
|
||||
Phone: ret.Phone,
|
||||
Email: ret.Email,
|
||||
Portrait: "",
|
||||
Roles: strings.Join(rt.Sso.OIDC.DefaultRoles, " "),
|
||||
RolesLst: rt.Sso.OIDC.DefaultRoles,
|
||||
Contacts: []byte("{}"),
|
||||
CreateAt: now,
|
||||
UpdateAt: now,
|
||||
CreateBy: "oidc",
|
||||
UpdateBy: "oidc",
|
||||
}
|
||||
|
||||
user = new(models.User)
|
||||
user.FullSsoFields("oidc", ret.Username, ret.Nickname, ret.Phone, ret.Email, rt.Sso.OIDC.DefaultRoles)
|
||||
// create user from oidc
|
||||
ginx.Dangerous(user.Add(rt.Ctx))
|
||||
|
||||
if len(rt.Sso.OIDC.DefaultTeams) > 0 {
|
||||
for _, gid := range rt.Sso.OIDC.DefaultTeams {
|
||||
err = models.UserGroupMemberAdd(rt.Ctx, gid, user.Id)
|
||||
if err != nil {
|
||||
logger.Errorf("user:%v UserGroupMemberAdd: %s", user, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// set user login state
|
||||
@@ -327,38 +346,12 @@ func (rt *Router) loginCallbackCas(c *gin.Context) {
|
||||
ginx.Dangerous(err)
|
||||
if user != nil {
|
||||
if rt.Sso.CAS.CoverAttributes {
|
||||
if ret.Nickname != "" {
|
||||
user.Nickname = ret.Nickname
|
||||
}
|
||||
|
||||
if ret.Email != "" {
|
||||
user.Email = ret.Email
|
||||
}
|
||||
|
||||
if ret.Phone != "" {
|
||||
user.Phone = ret.Phone
|
||||
}
|
||||
|
||||
user.UpdateAt = time.Now().Unix()
|
||||
ginx.Dangerous(user.Update(rt.Ctx, "email", "nickname", "phone", "update_at"))
|
||||
updatedFields := user.UpdateSsoFields("cas", ret.Nickname, ret.Phone, ret.Email)
|
||||
ginx.Dangerous(user.Update(rt.Ctx, "update_at", updatedFields...))
|
||||
}
|
||||
} else {
|
||||
now := time.Now().Unix()
|
||||
user = &models.User{
|
||||
Username: ret.Username,
|
||||
Password: "******",
|
||||
Nickname: ret.Nickname,
|
||||
Portrait: "",
|
||||
Roles: strings.Join(rt.Sso.CAS.DefaultRoles, " "),
|
||||
RolesLst: rt.Sso.CAS.DefaultRoles,
|
||||
Contacts: []byte("{}"),
|
||||
Phone: ret.Phone,
|
||||
Email: ret.Email,
|
||||
CreateAt: now,
|
||||
UpdateAt: now,
|
||||
CreateBy: "CAS",
|
||||
UpdateBy: "CAS",
|
||||
}
|
||||
user = new(models.User)
|
||||
user.FullSsoFields("cas", ret.Username, ret.Nickname, ret.Phone, ret.Email, rt.Sso.CAS.DefaultRoles)
|
||||
// create user from cas
|
||||
ginx.Dangerous(user.Add(rt.Ctx))
|
||||
}
|
||||
@@ -429,39 +422,12 @@ func (rt *Router) loginCallbackOAuth(c *gin.Context) {
|
||||
|
||||
if user != nil {
|
||||
if rt.Sso.OAuth2.CoverAttributes {
|
||||
if ret.Nickname != "" {
|
||||
user.Nickname = ret.Nickname
|
||||
}
|
||||
|
||||
if ret.Email != "" {
|
||||
user.Email = ret.Email
|
||||
}
|
||||
|
||||
if ret.Phone != "" {
|
||||
user.Phone = ret.Phone
|
||||
}
|
||||
|
||||
user.UpdateAt = time.Now().Unix()
|
||||
user.Update(rt.Ctx, "email", "nickname", "phone", "update_at")
|
||||
updatedFields := user.UpdateSsoFields("oauth2", ret.Nickname, ret.Phone, ret.Email)
|
||||
ginx.Dangerous(user.Update(rt.Ctx, "update_at", updatedFields...))
|
||||
}
|
||||
} else {
|
||||
now := time.Now().Unix()
|
||||
user = &models.User{
|
||||
Username: ret.Username,
|
||||
Password: "******",
|
||||
Nickname: ret.Nickname,
|
||||
Phone: ret.Phone,
|
||||
Email: ret.Email,
|
||||
Portrait: "",
|
||||
Roles: strings.Join(rt.Sso.OAuth2.DefaultRoles, " "),
|
||||
RolesLst: rt.Sso.OAuth2.DefaultRoles,
|
||||
Contacts: []byte("{}"),
|
||||
CreateAt: now,
|
||||
UpdateAt: now,
|
||||
CreateBy: "oauth2",
|
||||
UpdateBy: "oauth2",
|
||||
}
|
||||
|
||||
user = new(models.User)
|
||||
user.FullSsoFields("oauth2", ret.Username, ret.Nickname, ret.Phone, ret.Email, rt.Sso.OAuth2.DefaultRoles)
|
||||
// create user from oidc
|
||||
ginx.Dangerous(user.Add(rt.Ctx))
|
||||
}
|
||||
@@ -492,10 +458,23 @@ type SsoConfigOutput struct {
|
||||
}
|
||||
|
||||
func (rt *Router) ssoConfigNameGet(c *gin.Context) {
|
||||
var oidcDisplayName, casDisplayName, oauthDisplayName string
|
||||
if rt.Sso.OIDC != nil {
|
||||
oidcDisplayName = rt.Sso.OIDC.GetDisplayName()
|
||||
}
|
||||
|
||||
if rt.Sso.CAS != nil {
|
||||
casDisplayName = rt.Sso.CAS.GetDisplayName()
|
||||
}
|
||||
|
||||
if rt.Sso.OAuth2 != nil {
|
||||
oauthDisplayName = rt.Sso.OAuth2.GetDisplayName()
|
||||
}
|
||||
|
||||
ginx.NewRender(c).Data(SsoConfigOutput{
|
||||
OidcDisplayName: rt.Sso.OIDC.GetDisplayName(),
|
||||
CasDisplayName: rt.Sso.CAS.GetDisplayName(),
|
||||
OauthDisplayName: rt.Sso.OAuth2.GetDisplayName(),
|
||||
OidcDisplayName: oidcDisplayName,
|
||||
CasDisplayName: casDisplayName,
|
||||
OauthDisplayName: oauthDisplayName,
|
||||
}, nil)
|
||||
}
|
||||
|
||||
@@ -520,8 +499,7 @@ func (rt *Router) ssoConfigUpdate(c *gin.Context) {
|
||||
var config oidcx.Config
|
||||
err := toml.Unmarshal([]byte(f.Content), &config)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
err = rt.Sso.OIDC.Reload(config)
|
||||
rt.Sso.OIDC, err = oidcx.New(config)
|
||||
ginx.Dangerous(err)
|
||||
case "CAS":
|
||||
var config cas.Config
|
||||
@@ -537,3 +515,19 @@ func (rt *Router) ssoConfigUpdate(c *gin.Context) {
|
||||
|
||||
ginx.NewRender(c).Message(nil)
|
||||
}
|
||||
|
||||
type RSAConfigOutput struct {
|
||||
OpenRSA bool
|
||||
RSAPublicKey string
|
||||
}
|
||||
|
||||
func (rt *Router) rsaConfigGet(c *gin.Context) {
|
||||
publicKey := ""
|
||||
if len(rt.HTTP.RSA.RSAPublicKey) > 0 {
|
||||
publicKey = base64.StdEncoding.EncodeToString(rt.HTTP.RSA.RSAPublicKey)
|
||||
}
|
||||
ginx.NewRender(c).Data(RSAConfigOutput{
|
||||
OpenRSA: rt.HTTP.RSA.OpenRSA,
|
||||
RSAPublicKey: publicKey,
|
||||
}, nil)
|
||||
}
|
||||
|
||||
@@ -5,10 +5,12 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/ccfos/nightingale/v6/alert/common"
|
||||
"github.com/ccfos/nightingale/v6/models"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/toolkits/pkg/ginx"
|
||||
"github.com/toolkits/pkg/str"
|
||||
)
|
||||
|
||||
// Return all, front-end search and paging
|
||||
@@ -19,6 +21,31 @@ func (rt *Router) alertMuteGetsByBG(c *gin.Context) {
|
||||
ginx.NewRender(c).Data(lst, err)
|
||||
}
|
||||
|
||||
func (rt *Router) alertMuteGetsByGids(c *gin.Context) {
|
||||
gids := str.IdsInt64(ginx.QueryStr(c, "gids", ""), ",")
|
||||
if len(gids) > 0 {
|
||||
for _, gid := range gids {
|
||||
rt.bgroCheck(c, gid)
|
||||
}
|
||||
} else {
|
||||
me := c.MustGet("user").(*models.User)
|
||||
if !me.IsAdmin() {
|
||||
var err error
|
||||
gids, err = models.MyBusiGroupIds(rt.Ctx, me.Id)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
if len(gids) == 0 {
|
||||
ginx.NewRender(c).Data([]int{}, nil)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
lst, err := models.AlertMuteGetsByBGIds(rt.Ctx, gids)
|
||||
|
||||
ginx.NewRender(c).Data(lst, err)
|
||||
}
|
||||
|
||||
func (rt *Router) alertMuteGets(c *gin.Context) {
|
||||
prods := strings.Fields(ginx.QueryStr(c, "prods", ""))
|
||||
bgid := ginx.QueryInt64(c, "bgid", -1)
|
||||
@@ -29,16 +56,41 @@ func (rt *Router) alertMuteGets(c *gin.Context) {
|
||||
}
|
||||
|
||||
func (rt *Router) alertMuteAdd(c *gin.Context) {
|
||||
|
||||
var f models.AlertMute
|
||||
ginx.BindJSON(c, &f)
|
||||
|
||||
username := c.MustGet("username").(string)
|
||||
f.CreateBy = username
|
||||
f.GroupId = ginx.UrlParamInt64(c, "id")
|
||||
|
||||
ginx.NewRender(c).Message(f.Add(rt.Ctx))
|
||||
}
|
||||
|
||||
// Preview events (alert_cur_event) that match the mute strategy based on the following criteria:
|
||||
// business group ID (group_id, group_id), product (prod, rule_prod),
|
||||
// alert event severity (severities, severity), and event tags (tags, tags).
|
||||
// For products of type not 'host', also consider the category (cate, cate) and datasource ID (datasource_ids, datasource_id).
|
||||
func (rt *Router) alertMutePreview(c *gin.Context) {
|
||||
//Generally the match of events would be less.
|
||||
|
||||
var f models.AlertMute
|
||||
ginx.BindJSON(c, &f)
|
||||
f.GroupId = ginx.UrlParamInt64(c, "id")
|
||||
ginx.Dangerous(f.Verify()) //verify and parse tags json to ITags
|
||||
events, err := models.AlertCurEventGetsFromAlertMute(rt.Ctx, &f)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
matchEvents := make([]*models.AlertCurEvent, 0, len(events))
|
||||
for i := 0; i < len(events); i++ {
|
||||
events[i].DB2Mem()
|
||||
if common.MatchTags(events[i].TagsMap, f.ITags) {
|
||||
matchEvents = append(matchEvents, events[i])
|
||||
}
|
||||
}
|
||||
ginx.NewRender(c).Data(matchEvents, err)
|
||||
|
||||
}
|
||||
|
||||
func (rt *Router) alertMuteAddByService(c *gin.Context) {
|
||||
var f models.AlertMute
|
||||
ginx.BindJSON(c, &f)
|
||||
@@ -54,6 +106,14 @@ func (rt *Router) alertMuteDel(c *gin.Context) {
|
||||
ginx.NewRender(c).Message(models.AlertMuteDel(rt.Ctx, f.Ids))
|
||||
}
|
||||
|
||||
// alertMuteGet returns the alert mute by ID
|
||||
func (rt *Router) alertMuteGet(c *gin.Context) {
|
||||
amid := ginx.UrlParamInt64(c, "amid")
|
||||
am, err := models.AlertMuteGetById(rt.Ctx, amid)
|
||||
am.DB2FE()
|
||||
ginx.NewRender(c).Data(am, err)
|
||||
}
|
||||
|
||||
func (rt *Router) alertMutePutByFE(c *gin.Context) {
|
||||
var f models.AlertMute
|
||||
ginx.BindJSON(c, &f)
|
||||
|
||||
@@ -92,6 +92,10 @@ func (rt *Router) jwtAuth() gin.HandlerFunc {
|
||||
}
|
||||
}
|
||||
|
||||
func (rt *Router) Auth() gin.HandlerFunc {
|
||||
return rt.auth()
|
||||
}
|
||||
|
||||
func (rt *Router) auth() gin.HandlerFunc {
|
||||
if rt.HTTP.ProxyAuth.Enable {
|
||||
return rt.proxyAuth()
|
||||
@@ -120,6 +124,10 @@ func (rt *Router) jwtMock() gin.HandlerFunc {
|
||||
}
|
||||
}
|
||||
|
||||
func (rt *Router) User() gin.HandlerFunc {
|
||||
return rt.user()
|
||||
}
|
||||
|
||||
func (rt *Router) user() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
userid := c.MustGet("userid").(int64)
|
||||
@@ -135,6 +143,8 @@ func (rt *Router) user() gin.HandlerFunc {
|
||||
|
||||
c.Set("user", user)
|
||||
c.Set("isadmin", user.IsAdmin())
|
||||
// Update user.LastActiveTime
|
||||
rt.UserCache.SetLastActiveTime(user.Id, time.Now().Unix())
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
@@ -174,6 +184,10 @@ func (rt *Router) bgro() gin.HandlerFunc {
|
||||
}
|
||||
|
||||
// bgrw 逐步要被干掉,不安全
|
||||
func (rt *Router) Bgrw() gin.HandlerFunc {
|
||||
return rt.bgrw()
|
||||
}
|
||||
|
||||
func (rt *Router) bgrw() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
me := c.MustGet("user").(*models.User)
|
||||
@@ -233,6 +247,10 @@ func (rt *Router) bgroCheck(c *gin.Context, bgid int64) {
|
||||
c.Set("busi_group", bg)
|
||||
}
|
||||
|
||||
func (rt *Router) Perm(operation string) gin.HandlerFunc {
|
||||
return rt.perm(operation)
|
||||
}
|
||||
|
||||
func (rt *Router) perm(operation string) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
me := c.MustGet("user").(*models.User)
|
||||
|
||||
@@ -2,15 +2,19 @@ package router
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/ccfos/nightingale/v6/alert/aconf"
|
||||
"github.com/ccfos/nightingale/v6/alert/sender"
|
||||
"github.com/ccfos/nightingale/v6/memsto"
|
||||
"github.com/ccfos/nightingale/v6/models"
|
||||
"github.com/pelletier/go-toml/v2"
|
||||
"github.com/ccfos/nightingale/v6/pkg/tplx"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/pelletier/go-toml/v2"
|
||||
"github.com/toolkits/pkg/ginx"
|
||||
"github.com/toolkits/pkg/str"
|
||||
)
|
||||
|
||||
func (rt *Router) webhookGets(c *gin.Context) {
|
||||
@@ -41,8 +45,8 @@ func (rt *Router) webhookPuts(c *gin.Context) {
|
||||
|
||||
data, err := json.Marshal(webhooks)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
ginx.NewRender(c).Message(models.ConfigsSet(rt.Ctx, models.WEBHOOKKEY, string(data)))
|
||||
username := c.MustGet("username").(string)
|
||||
ginx.NewRender(c).Message(models.ConfigsSetWithUname(rt.Ctx, models.WEBHOOKKEY, string(data), username))
|
||||
}
|
||||
|
||||
func (rt *Router) notifyScriptGet(c *gin.Context) {
|
||||
@@ -65,8 +69,8 @@ func (rt *Router) notifyScriptPut(c *gin.Context) {
|
||||
|
||||
data, err := json.Marshal(notifyScript)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
ginx.NewRender(c).Message(models.ConfigsSet(rt.Ctx, models.NOTIFYSCRIPT, string(data)))
|
||||
username := c.MustGet("username").(string)
|
||||
ginx.NewRender(c).Message(models.ConfigsSetWithUname(rt.Ctx, models.NOTIFYSCRIPT, string(data), username))
|
||||
}
|
||||
|
||||
func (rt *Router) notifyChannelGets(c *gin.Context) {
|
||||
@@ -101,8 +105,8 @@ func (rt *Router) notifyChannelPuts(c *gin.Context) {
|
||||
|
||||
data, err := json.Marshal(notifyChannels)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
ginx.NewRender(c).Message(models.ConfigsSet(rt.Ctx, models.NOTIFYCHANNEL, string(data)))
|
||||
username := c.MustGet("username").(string)
|
||||
ginx.NewRender(c).Message(models.ConfigsSetWithUname(rt.Ctx, models.NOTIFYCHANNEL, string(data), username))
|
||||
}
|
||||
|
||||
func (rt *Router) notifyContactGets(c *gin.Context) {
|
||||
@@ -137,8 +141,8 @@ func (rt *Router) notifyContactPuts(c *gin.Context) {
|
||||
|
||||
data, err := json.Marshal(notifyContacts)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
ginx.NewRender(c).Message(models.ConfigsSet(rt.Ctx, models.NOTIFYCONTACT, string(data)))
|
||||
username := c.MustGet("username").(string)
|
||||
ginx.NewRender(c).Message(models.ConfigsSetWithUname(rt.Ctx, models.NOTIFYCONTACT, string(data), username))
|
||||
}
|
||||
|
||||
func (rt *Router) notifyConfigGet(c *gin.Context) {
|
||||
@@ -158,36 +162,65 @@ func (rt *Router) notifyConfigGet(c *gin.Context) {
|
||||
func (rt *Router) notifyConfigPut(c *gin.Context) {
|
||||
var f models.Configs
|
||||
ginx.BindJSON(c, &f)
|
||||
userVariableMap := rt.NotifyConfigCache.ConfigCache.Get()
|
||||
text := tplx.ReplaceTemplateUseText(f.Ckey, f.Cval, userVariableMap)
|
||||
switch f.Ckey {
|
||||
case models.SMTP:
|
||||
var smtp aconf.SMTPConfig
|
||||
err := toml.Unmarshal([]byte(f.Cval), &smtp)
|
||||
ginx.Dangerous(err)
|
||||
case models.IBEX:
|
||||
var ibex aconf.Ibex
|
||||
err := toml.Unmarshal([]byte(f.Cval), &ibex)
|
||||
err := toml.Unmarshal([]byte(text), &smtp)
|
||||
ginx.Dangerous(err)
|
||||
default:
|
||||
ginx.Bomb(200, "key %s can not modify", f.Ckey)
|
||||
}
|
||||
|
||||
err := models.ConfigsSet(rt.Ctx, f.Ckey, f.Cval)
|
||||
if err != nil {
|
||||
ginx.Bomb(200, err.Error())
|
||||
}
|
||||
|
||||
username := c.MustGet("username").(string)
|
||||
//insert or update build-in config
|
||||
ginx.Dangerous(models.ConfigsSetWithUname(rt.Ctx, f.Ckey, f.Cval, username))
|
||||
if f.Ckey == models.SMTP {
|
||||
// 重置邮件发送器
|
||||
var smtp aconf.SMTPConfig
|
||||
err := toml.Unmarshal([]byte(f.Cval), &smtp)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
if smtp.Host == "" || smtp.Port == 0 {
|
||||
ginx.Bomb(200, "smtp host or port can not be empty")
|
||||
}
|
||||
|
||||
smtp, errSmtp := SmtpValidate(text)
|
||||
ginx.Dangerous(errSmtp)
|
||||
go sender.RestartEmailSender(smtp)
|
||||
}
|
||||
|
||||
ginx.NewRender(c).Message(nil)
|
||||
}
|
||||
func SmtpValidate(text string) (aconf.SMTPConfig, error) {
|
||||
var smtp aconf.SMTPConfig
|
||||
var err error
|
||||
|
||||
err = toml.Unmarshal([]byte(text), &smtp)
|
||||
if err != nil {
|
||||
return smtp, err
|
||||
}
|
||||
if smtp.Host == "" || smtp.Port == 0 {
|
||||
return smtp, fmt.Errorf("smtp host or port can not be empty")
|
||||
}
|
||||
return smtp, err
|
||||
}
|
||||
|
||||
type form struct {
|
||||
models.Configs
|
||||
Email string `json:"email"`
|
||||
}
|
||||
|
||||
// After configuring the aconf.SMTPConfig, users can choose to perform a test. In this test, the function attempts to send an email
|
||||
func (rt *Router) attemptSendEmail(c *gin.Context) {
|
||||
var f form
|
||||
ginx.BindJSON(c, &f)
|
||||
|
||||
if f.Email = strings.TrimSpace(f.Email); f.Email == "" || !str.IsMail(f.Email) {
|
||||
ginx.Bomb(200, "email(%s) invalid", f.Email)
|
||||
}
|
||||
|
||||
if f.Ckey != models.SMTP {
|
||||
ginx.Bomb(200, "config(%v) invalid", f)
|
||||
}
|
||||
userVariableMap := rt.NotifyConfigCache.ConfigCache.Get()
|
||||
text := tplx.ReplaceTemplateUseText(f.Ckey, f.Cval, userVariableMap)
|
||||
smtp, err := SmtpValidate(text)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
ginx.NewRender(c).Message(sender.SendEmail("Email test", "email content", []string{f.Email}, smtp))
|
||||
|
||||
}
|
||||
|
||||
@@ -6,45 +6,98 @@ import (
|
||||
"fmt"
|
||||
"html/template"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/ccfos/nightingale/v6/center/cconf"
|
||||
"github.com/ccfos/nightingale/v6/models"
|
||||
"github.com/ccfos/nightingale/v6/pkg/tplx"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/toolkits/pkg/ginx"
|
||||
"github.com/toolkits/pkg/str"
|
||||
)
|
||||
|
||||
func (rt *Router) notifyTplGets(c *gin.Context) {
|
||||
m := make(map[string]struct{})
|
||||
for _, channel := range models.DefaultChannels {
|
||||
m[channel] = struct{}{}
|
||||
}
|
||||
m[models.EmailSubject] = struct{}{}
|
||||
|
||||
lst, err := models.NotifyTplGets(rt.Ctx)
|
||||
for i := 0; i < len(lst); i++ {
|
||||
if _, exists := m[lst[i].Channel]; exists {
|
||||
lst[i].BuiltIn = true
|
||||
}
|
||||
}
|
||||
|
||||
ginx.NewRender(c).Data(lst, err)
|
||||
}
|
||||
|
||||
func (rt *Router) notifyTplUpdateContent(c *gin.Context) {
|
||||
user := c.MustGet("user").(*models.User)
|
||||
|
||||
var f models.NotifyTpl
|
||||
ginx.BindJSON(c, &f)
|
||||
ginx.Dangerous(templateValidate(f))
|
||||
|
||||
if err := templateValidate(f); err != nil {
|
||||
ginx.NewRender(c).Message(err.Error())
|
||||
return
|
||||
notifyTpl, err := models.NotifyTplGet(rt.Ctx, f.Id)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
if notifyTpl.CreateBy != user.Username && !user.IsAdmin() {
|
||||
ginx.Bomb(403, "no permission")
|
||||
}
|
||||
|
||||
f.UpdateAt = time.Now().Unix()
|
||||
f.UpdateBy = user.Username
|
||||
|
||||
ginx.NewRender(c).Message(f.UpdateContent(rt.Ctx))
|
||||
}
|
||||
|
||||
func (rt *Router) notifyTplUpdate(c *gin.Context) {
|
||||
var f models.NotifyTpl
|
||||
ginx.BindJSON(c, &f)
|
||||
ginx.Dangerous(templateValidate(f))
|
||||
user := c.MustGet("user").(*models.User)
|
||||
|
||||
if err := templateValidate(f); err != nil {
|
||||
ginx.NewRender(c).Message(err.Error())
|
||||
return
|
||||
notifyTpl, err := models.NotifyTplGet(rt.Ctx, f.Id)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
if notifyTpl.CreateBy != user.Username && !user.IsAdmin() {
|
||||
ginx.Bomb(403, "no permission")
|
||||
}
|
||||
|
||||
ginx.NewRender(c).Message(f.Update(rt.Ctx))
|
||||
// get the count of the same channel and name but different id
|
||||
count, err := models.Count(models.DB(rt.Ctx).Model(&models.NotifyTpl{}).Where("channel = ? or name = ? and id <> ?", f.Channel, f.Name, f.Id))
|
||||
ginx.Dangerous(err)
|
||||
if count != 0 {
|
||||
ginx.Bomb(200, "Refuse to create duplicate channel or name")
|
||||
}
|
||||
|
||||
notifyTpl.UpdateAt = time.Now().Unix()
|
||||
notifyTpl.UpdateBy = user.Username
|
||||
notifyTpl.Name = f.Name
|
||||
|
||||
ginx.NewRender(c).Message(notifyTpl.Update(rt.Ctx))
|
||||
}
|
||||
|
||||
func templateValidate(f models.NotifyTpl) error {
|
||||
if len(f.Channel) > 32 {
|
||||
return fmt.Errorf("channel length should not exceed 32")
|
||||
}
|
||||
|
||||
if str.Dangerous(f.Channel) {
|
||||
return fmt.Errorf("channel should not contain dangerous characters")
|
||||
}
|
||||
|
||||
if len(f.Name) > 255 {
|
||||
return fmt.Errorf("name length should not exceed 255")
|
||||
}
|
||||
|
||||
if str.Dangerous(f.Name) {
|
||||
return fmt.Errorf("name should not contain dangerous characters")
|
||||
}
|
||||
|
||||
if f.Content == "" {
|
||||
return nil
|
||||
}
|
||||
@@ -65,10 +118,7 @@ func templateValidate(f models.NotifyTpl) error {
|
||||
func (rt *Router) notifyTplPreview(c *gin.Context) {
|
||||
var event models.AlertCurEvent
|
||||
err := json.Unmarshal([]byte(cconf.EVENT_EXAMPLE), &event)
|
||||
if err != nil {
|
||||
ginx.NewRender(c).Message(err.Error())
|
||||
return
|
||||
}
|
||||
ginx.Dangerous(err)
|
||||
|
||||
var f models.NotifyTpl
|
||||
ginx.BindJSON(c, &f)
|
||||
@@ -106,3 +156,34 @@ func (rt *Router) notifyTplPreview(c *gin.Context) {
|
||||
|
||||
ginx.NewRender(c).Data(ret, nil)
|
||||
}
|
||||
|
||||
// add new notify template
|
||||
func (rt *Router) notifyTplAdd(c *gin.Context) {
|
||||
var f models.NotifyTpl
|
||||
ginx.BindJSON(c, &f)
|
||||
f.Channel = strings.TrimSpace(f.Channel)
|
||||
ginx.Dangerous(templateValidate(f))
|
||||
|
||||
count, err := models.Count(models.DB(rt.Ctx).Model(&models.NotifyTpl{}).Where("channel = ? or name = ?", f.Channel, f.Name))
|
||||
ginx.Dangerous(err)
|
||||
if count != 0 {
|
||||
ginx.Bomb(200, "Refuse to create duplicate channel(unique)")
|
||||
}
|
||||
ginx.NewRender(c).Message(f.Create(rt.Ctx))
|
||||
}
|
||||
|
||||
// delete notify template, not allowed to delete the system defaults(models.DefaultChannels)
|
||||
func (rt *Router) notifyTplDel(c *gin.Context) {
|
||||
f := new(models.NotifyTpl)
|
||||
id := ginx.UrlParamInt64(c, "id")
|
||||
user := c.MustGet("user").(*models.User)
|
||||
|
||||
notifyTpl, err := models.NotifyTplGet(rt.Ctx, id)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
if notifyTpl.CreateBy != user.Username && !user.IsAdmin() {
|
||||
ginx.Bomb(403, "no permission")
|
||||
}
|
||||
|
||||
ginx.NewRender(c).Message(f.NotifyTplDelete(rt.Ctx, id))
|
||||
}
|
||||
|
||||
@@ -3,39 +3,51 @@ package router
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
pkgprom "github.com/ccfos/nightingale/v6/pkg/prom"
|
||||
"github.com/ccfos/nightingale/v6/prom"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/prometheus/common/model"
|
||||
"github.com/toolkits/pkg/ginx"
|
||||
"github.com/toolkits/pkg/logger"
|
||||
)
|
||||
|
||||
type queryFormItem struct {
|
||||
type QueryFormItem struct {
|
||||
Start int64 `json:"start" binding:"required"`
|
||||
End int64 `json:"end" binding:"required"`
|
||||
Step int64 `json:"step" binding:"required"`
|
||||
Query string `json:"query" binding:"required"`
|
||||
}
|
||||
|
||||
type batchQueryForm struct {
|
||||
type BatchQueryForm struct {
|
||||
DatasourceId int64 `json:"datasource_id" binding:"required"`
|
||||
Queries []queryFormItem `json:"queries" binding:"required"`
|
||||
Queries []QueryFormItem `json:"queries" binding:"required"`
|
||||
}
|
||||
|
||||
func (rt *Router) promBatchQueryRange(c *gin.Context) {
|
||||
var f batchQueryForm
|
||||
var f BatchQueryForm
|
||||
ginx.Dangerous(c.BindJSON(&f))
|
||||
|
||||
cli := rt.PromClients.GetCli(f.DatasourceId)
|
||||
lst, err := PromBatchQueryRange(rt.PromClients, f)
|
||||
ginx.NewRender(c).Data(lst, err)
|
||||
}
|
||||
|
||||
func PromBatchQueryRange(pc *prom.PromClientMap, f BatchQueryForm) ([]model.Value, error) {
|
||||
var lst []model.Value
|
||||
|
||||
cli := pc.GetCli(f.DatasourceId)
|
||||
if cli == nil {
|
||||
return lst, fmt.Errorf("no such datasource id: %d", f.DatasourceId)
|
||||
}
|
||||
|
||||
for _, item := range f.Queries {
|
||||
r := pkgprom.Range{
|
||||
Start: time.Unix(item.Start, 0),
|
||||
@@ -44,15 +56,16 @@ func (rt *Router) promBatchQueryRange(c *gin.Context) {
|
||||
}
|
||||
|
||||
resp, _, err := cli.QueryRange(context.Background(), item.Query, r)
|
||||
ginx.Dangerous(err)
|
||||
if err != nil {
|
||||
return lst, err
|
||||
}
|
||||
|
||||
lst = append(lst, resp)
|
||||
}
|
||||
|
||||
ginx.NewRender(c).Data(lst, nil)
|
||||
return lst, nil
|
||||
}
|
||||
|
||||
type batchInstantForm struct {
|
||||
type BatchInstantForm struct {
|
||||
DatasourceId int64 `json:"datasource_id" binding:"required"`
|
||||
Queries []InstantFormItem `json:"queries" binding:"required"`
|
||||
}
|
||||
@@ -63,21 +76,31 @@ type InstantFormItem struct {
|
||||
}
|
||||
|
||||
func (rt *Router) promBatchQueryInstant(c *gin.Context) {
|
||||
var f batchInstantForm
|
||||
var f BatchInstantForm
|
||||
ginx.Dangerous(c.BindJSON(&f))
|
||||
|
||||
cli := rt.PromClients.GetCli(f.DatasourceId)
|
||||
lst, err := PromBatchQueryInstant(rt.PromClients, f)
|
||||
ginx.NewRender(c).Data(lst, err)
|
||||
}
|
||||
|
||||
func PromBatchQueryInstant(pc *prom.PromClientMap, f BatchInstantForm) ([]model.Value, error) {
|
||||
var lst []model.Value
|
||||
|
||||
cli := pc.GetCli(f.DatasourceId)
|
||||
if cli == nil {
|
||||
logger.Warningf("no such datasource id: %d", f.DatasourceId)
|
||||
return lst, fmt.Errorf("no such datasource id: %d", f.DatasourceId)
|
||||
}
|
||||
|
||||
for _, item := range f.Queries {
|
||||
resp, _, err := cli.Query(context.Background(), item.Query, time.Unix(item.Time, 0))
|
||||
ginx.Dangerous(err)
|
||||
if err != nil {
|
||||
return lst, err
|
||||
}
|
||||
|
||||
lst = append(lst, resp)
|
||||
}
|
||||
|
||||
ginx.NewRender(c).Data(lst, nil)
|
||||
return lst, nil
|
||||
}
|
||||
|
||||
func (rt *Router) dsProxy(c *gin.Context) {
|
||||
@@ -139,21 +162,77 @@ func (rt *Router) dsProxy(c *gin.Context) {
|
||||
http.Error(w, err.Error(), http.StatusBadGateway)
|
||||
}
|
||||
|
||||
transport := &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: ds.HTTPJson.TLS.SkipTlsVerify},
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
DialContext: (&net.Dialer{
|
||||
Timeout: time.Duration(ds.HTTPJson.DialTimeout) * time.Millisecond,
|
||||
}).DialContext,
|
||||
ResponseHeaderTimeout: time.Duration(ds.HTTPJson.Timeout) * time.Millisecond,
|
||||
MaxIdleConnsPerHost: ds.HTTPJson.MaxIdleConnsPerHost,
|
||||
transport, has := transportGet(dsId, ds.UpdatedAt)
|
||||
if !has {
|
||||
transport = &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: ds.HTTPJson.TLS.SkipTlsVerify},
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
DialContext: (&net.Dialer{
|
||||
Timeout: time.Duration(ds.HTTPJson.DialTimeout) * time.Millisecond,
|
||||
}).DialContext,
|
||||
ResponseHeaderTimeout: time.Duration(ds.HTTPJson.Timeout) * time.Millisecond,
|
||||
MaxIdleConnsPerHost: ds.HTTPJson.MaxIdleConnsPerHost,
|
||||
}
|
||||
transportPut(dsId, ds.UpdatedAt, transport)
|
||||
}
|
||||
|
||||
modifyResponse := func(r *http.Response) error {
|
||||
if r.StatusCode == http.StatusUnauthorized {
|
||||
logger.Warningf("proxy path:%s unauthorized access ", c.Request.URL.Path)
|
||||
return fmt.Errorf("unauthorized access")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
proxy := &httputil.ReverseProxy{
|
||||
Director: director,
|
||||
Transport: transport,
|
||||
ErrorHandler: errFunc,
|
||||
Director: director,
|
||||
Transport: transport,
|
||||
ErrorHandler: errFunc,
|
||||
ModifyResponse: modifyResponse,
|
||||
}
|
||||
|
||||
proxy.ServeHTTP(c.Writer, c.Request)
|
||||
|
||||
}
|
||||
|
||||
var (
|
||||
transports = map[int64]http.RoundTripper{}
|
||||
updatedAts = map[int64]int64{}
|
||||
transportsLock = &sync.Mutex{}
|
||||
)
|
||||
|
||||
func transportGet(dsid, newUpdatedAt int64) (http.RoundTripper, bool) {
|
||||
transportsLock.Lock()
|
||||
defer transportsLock.Unlock()
|
||||
|
||||
tran, has := transports[dsid]
|
||||
if !has {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
oldUpdateAt, has := updatedAts[dsid]
|
||||
if !has {
|
||||
oldtran := tran.(*http.Transport)
|
||||
oldtran.CloseIdleConnections()
|
||||
delete(transports, dsid)
|
||||
return nil, false
|
||||
}
|
||||
|
||||
if oldUpdateAt != newUpdatedAt {
|
||||
oldtran := tran.(*http.Transport)
|
||||
oldtran.CloseIdleConnections()
|
||||
delete(transports, dsid)
|
||||
delete(updatedAts, dsid)
|
||||
return nil, false
|
||||
}
|
||||
|
||||
return tran, has
|
||||
}
|
||||
|
||||
func transportPut(dsid, updatedat int64, tran http.RoundTripper) {
|
||||
transportsLock.Lock()
|
||||
transports[dsid] = tran
|
||||
updatedAts[dsid] = updatedat
|
||||
transportsLock.Unlock()
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/toolkits/pkg/ginx"
|
||||
"github.com/toolkits/pkg/str"
|
||||
)
|
||||
|
||||
func (rt *Router) recordingRuleGets(c *gin.Context) {
|
||||
@@ -19,6 +20,30 @@ func (rt *Router) recordingRuleGets(c *gin.Context) {
|
||||
ginx.NewRender(c).Data(ars, err)
|
||||
}
|
||||
|
||||
func (rt *Router) recordingRuleGetsByGids(c *gin.Context) {
|
||||
gids := str.IdsInt64(ginx.QueryStr(c, "gids", ""), ",")
|
||||
if len(gids) > 0 {
|
||||
for _, gid := range gids {
|
||||
rt.bgroCheck(c, gid)
|
||||
}
|
||||
} else {
|
||||
me := c.MustGet("user").(*models.User)
|
||||
if !me.IsAdmin() {
|
||||
var err error
|
||||
gids, err = models.MyBusiGroupIds(rt.Ctx, me.Id)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
if len(gids) == 0 {
|
||||
ginx.NewRender(c).Data([]int{}, nil)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ars, err := models.RecordingRuleGetsByBGIds(rt.Ctx, gids)
|
||||
ginx.NewRender(c).Data(ars, err)
|
||||
}
|
||||
|
||||
func (rt *Router) recordingRuleGetsByService(c *gin.Context) {
|
||||
ars, err := models.RecordingRuleEnabledGets(rt.Ctx)
|
||||
ginx.NewRender(c).Data(ars, err)
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/ccfos/nightingale/v6/center/cconf"
|
||||
"github.com/ccfos/nightingale/v6/models"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -17,6 +18,15 @@ func (rt *Router) rolesGets(c *gin.Context) {
|
||||
|
||||
func (rt *Router) permsGets(c *gin.Context) {
|
||||
user := c.MustGet("user").(*models.User)
|
||||
if user.IsAdmin() {
|
||||
var lst []string
|
||||
for _, ops := range cconf.Operations.Ops {
|
||||
lst = append(lst, ops.Ops...)
|
||||
}
|
||||
ginx.NewRender(c).Data(lst, nil)
|
||||
return
|
||||
}
|
||||
|
||||
lst, err := models.OperationsOfRole(rt.Ctx, strings.Fields(user.Roles))
|
||||
ginx.NewRender(c).Data(lst, err)
|
||||
}
|
||||
|
||||
@@ -3,9 +3,11 @@ package router
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/ccfos/nightingale/v6/center/cconf"
|
||||
"github.com/ccfos/nightingale/v6/models"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/toolkits/pkg/ginx"
|
||||
"github.com/toolkits/pkg/i18n"
|
||||
)
|
||||
|
||||
func (rt *Router) operationOfRole(c *gin.Context) {
|
||||
@@ -16,6 +18,15 @@ func (rt *Router) operationOfRole(c *gin.Context) {
|
||||
ginx.Bomb(http.StatusOK, "role not found")
|
||||
}
|
||||
|
||||
if role.Name == "Admin" {
|
||||
var lst []string
|
||||
for _, ops := range cconf.Operations.Ops {
|
||||
lst = append(lst, ops.Ops...)
|
||||
}
|
||||
ginx.NewRender(c).Data(lst, nil)
|
||||
return
|
||||
}
|
||||
|
||||
ops, err := models.OperationsOfRole(rt.Ctx, []string{role.Name})
|
||||
ginx.NewRender(c).Data(ops, err)
|
||||
}
|
||||
@@ -39,5 +50,11 @@ func (rt *Router) roleBindOperation(c *gin.Context) {
|
||||
}
|
||||
|
||||
func (rt *Router) operations(c *gin.Context) {
|
||||
ginx.NewRender(c).Data(rt.Operations.Ops, nil)
|
||||
var ops []cconf.Ops
|
||||
for _, v := range rt.Operations.Ops {
|
||||
v.Cname = i18n.Sprintf(c.GetHeader("X-Language"), v.Cname)
|
||||
ops = append(ops, v)
|
||||
}
|
||||
|
||||
ginx.NewRender(c).Data(ops, nil)
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package router
|
||||
|
||||
import (
|
||||
"github.com/ccfos/nightingale/v6/models"
|
||||
"github.com/ccfos/nightingale/v6/pkg/flashduty"
|
||||
"github.com/ccfos/nightingale/v6/pkg/ormx"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -29,6 +30,11 @@ func (rt *Router) selfProfilePut(c *gin.Context) {
|
||||
ginx.BindJSON(c, &f)
|
||||
|
||||
user := c.MustGet("user").(*models.User)
|
||||
oldInfo := models.User{
|
||||
Username: user.Username,
|
||||
Phone: user.Phone,
|
||||
Email: user.Email,
|
||||
}
|
||||
user.Nickname = f.Nickname
|
||||
user.Phone = f.Phone
|
||||
user.Email = f.Email
|
||||
@@ -36,6 +42,10 @@ func (rt *Router) selfProfilePut(c *gin.Context) {
|
||||
user.Contacts = f.Contacts
|
||||
user.UpdateBy = user.Username
|
||||
|
||||
if flashduty.NeedSyncUser(rt.Ctx) {
|
||||
flashduty.UpdateUser(rt.Ctx, oldInfo, f.Email, f.Phone)
|
||||
}
|
||||
|
||||
ginx.NewRender(c).Message(user.UpdateAllFields(rt.Ctx))
|
||||
}
|
||||
|
||||
|
||||
@@ -27,8 +27,18 @@ func (rt *Router) serverHeartbeat(c *gin.Context) {
|
||||
}
|
||||
|
||||
func (rt *Router) serversActive(c *gin.Context) {
|
||||
datasourceId := ginx.QueryInt64(c, "dsid")
|
||||
datasourceId := ginx.QueryInt64(c, "dsid", 0)
|
||||
engineName := ginx.QueryStr(c, "engine_name", "")
|
||||
if engineName != "" {
|
||||
servers, err := models.AlertingEngineGetsInstances(rt.Ctx, "engine_cluster = ? and clock > ?", engineName, time.Now().Unix()-30)
|
||||
ginx.NewRender(c).Data(servers, err)
|
||||
return
|
||||
}
|
||||
|
||||
if datasourceId == 0 {
|
||||
ginx.NewRender(c).Message("dsid is required")
|
||||
return
|
||||
}
|
||||
servers, err := models.AlertingEngineGetsInstances(rt.Ctx, "datasource_id = ? and clock > ?", datasourceId, time.Now().Unix()-30)
|
||||
ginx.NewRender(c).Data(servers, err)
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"github.com/prometheus/common/model"
|
||||
"github.com/toolkits/pkg/ginx"
|
||||
"github.com/toolkits/pkg/logger"
|
||||
"github.com/toolkits/pkg/str"
|
||||
)
|
||||
|
||||
type TargetQuery struct {
|
||||
@@ -42,15 +43,30 @@ func (rt *Router) targetGetsByHostFilter(c *gin.Context) {
|
||||
}
|
||||
|
||||
func (rt *Router) targetGets(c *gin.Context) {
|
||||
bgid := ginx.QueryInt64(c, "bgid", -1)
|
||||
bgids := str.IdsInt64(ginx.QueryStr(c, "gids", ""), ",")
|
||||
query := ginx.QueryStr(c, "query", "")
|
||||
limit := ginx.QueryInt(c, "limit", 30)
|
||||
downtime := ginx.QueryInt64(c, "downtime", 0)
|
||||
dsIds := queryDatasourceIds(c)
|
||||
|
||||
total, err := models.TargetTotal(rt.Ctx, bgid, dsIds, query)
|
||||
var err error
|
||||
if len(bgids) == 0 {
|
||||
user := c.MustGet("user").(*models.User)
|
||||
if !user.IsAdmin() {
|
||||
// 如果是非 admin 用户,全部对象的情况,找到用户有权限的业务组
|
||||
var err error
|
||||
bgids, err = models.MyBusiGroupIds(rt.Ctx, user.Id)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
// 将未分配业务组的对象也加入到列表中
|
||||
bgids = append(bgids, 0)
|
||||
}
|
||||
}
|
||||
|
||||
total, err := models.TargetTotal(rt.Ctx, bgids, dsIds, query, downtime)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
list, err := models.TargetGets(rt.Ctx, bgid, dsIds, query, limit, ginx.Offset(c, limit))
|
||||
list, err := models.TargetGets(rt.Ctx, bgids, dsIds, query, downtime, limit, ginx.Offset(c, limit))
|
||||
ginx.Dangerous(err)
|
||||
|
||||
if err == nil {
|
||||
@@ -61,6 +77,12 @@ func (rt *Router) targetGets(c *gin.Context) {
|
||||
for i := 0; i < len(list); i++ {
|
||||
ginx.Dangerous(list[i].FillGroup(rt.Ctx, cache))
|
||||
keys = append(keys, models.WrapIdent(list[i].Ident))
|
||||
|
||||
if now.Unix()-list[i].UpdateAt < 60 {
|
||||
list[i].TargetUp = 2
|
||||
} else if now.Unix()-list[i].UpdateAt < 180 {
|
||||
list[i].TargetUp = 1
|
||||
}
|
||||
}
|
||||
|
||||
if len(keys) > 0 {
|
||||
@@ -80,10 +102,6 @@ func (rt *Router) targetGets(c *gin.Context) {
|
||||
}
|
||||
|
||||
for i := 0; i < len(list); i++ {
|
||||
if now.Unix()-list[i].UpdateAt < 120 {
|
||||
list[i].TargetUp = 1
|
||||
}
|
||||
|
||||
if meta, ok := metaMap[list[i].Ident]; ok {
|
||||
list[i].FillMeta(meta)
|
||||
} else {
|
||||
@@ -101,6 +119,27 @@ func (rt *Router) targetGets(c *gin.Context) {
|
||||
}, nil)
|
||||
}
|
||||
|
||||
func (rt *Router) targetExtendInfoByIdent(c *gin.Context) {
|
||||
ident := ginx.QueryStr(c, "ident", "")
|
||||
key := models.WrapExtendIdent(ident)
|
||||
vals := storage.MGet(context.Background(), rt.Redis, []string{key})
|
||||
if len(vals) > 0 {
|
||||
extInfo := string(vals[0])
|
||||
if extInfo == "null" {
|
||||
extInfo = ""
|
||||
}
|
||||
ginx.NewRender(c).Data(gin.H{
|
||||
"extend_info": extInfo,
|
||||
"ident": ident,
|
||||
}, nil)
|
||||
return
|
||||
}
|
||||
ginx.NewRender(c).Data(gin.H{
|
||||
"extend_info": "",
|
||||
"ident": ident,
|
||||
}, nil)
|
||||
}
|
||||
|
||||
func (rt *Router) targetGetsByService(c *gin.Context) {
|
||||
lst, err := models.TargetGetsAll(rt.Ctx)
|
||||
ginx.NewRender(c).Data(lst, err)
|
||||
@@ -114,173 +153,242 @@ func (rt *Router) targetGetTags(c *gin.Context) {
|
||||
}
|
||||
|
||||
type targetTagsForm struct {
|
||||
Idents []string `json:"idents" binding:"required"`
|
||||
Tags []string `json:"tags" binding:"required"`
|
||||
Idents []string `json:"idents" binding:"required_without=HostIps"`
|
||||
HostIps []string `json:"host_ips" binding:"required_without=Idents"`
|
||||
Tags []string `json:"tags" binding:"required"`
|
||||
}
|
||||
|
||||
func (rt *Router) targetBindTagsByFE(c *gin.Context) {
|
||||
var f targetTagsForm
|
||||
var err error
|
||||
var failedResults = make(map[string]string)
|
||||
ginx.BindJSON(c, &f)
|
||||
|
||||
if len(f.Idents) == 0 {
|
||||
ginx.Bomb(http.StatusBadRequest, "idents empty")
|
||||
if len(f.Idents) == 0 && len(f.HostIps) == 0 {
|
||||
ginx.Bomb(http.StatusBadRequest, "idents or host_ips must be provided")
|
||||
}
|
||||
// Acquire idents by idents and hostIps
|
||||
failedResults, f.Idents, err = models.TargetsGetIdentsByIdentsAndHostIps(rt.Ctx, f.Idents, f.HostIps)
|
||||
if err != nil {
|
||||
ginx.Bomb(http.StatusBadRequest, err.Error())
|
||||
}
|
||||
|
||||
rt.checkTargetPerm(c, f.Idents)
|
||||
|
||||
ginx.NewRender(c).Message(rt.targetBindTags(f))
|
||||
ginx.NewRender(c).Data(rt.targetBindTags(f, failedResults))
|
||||
}
|
||||
|
||||
func (rt *Router) targetBindTagsByService(c *gin.Context) {
|
||||
var f targetTagsForm
|
||||
var err error
|
||||
var failedResults = make(map[string]string)
|
||||
ginx.BindJSON(c, &f)
|
||||
|
||||
if len(f.Idents) == 0 {
|
||||
ginx.Bomb(http.StatusBadRequest, "idents empty")
|
||||
if len(f.Idents) == 0 && len(f.HostIps) == 0 {
|
||||
ginx.Bomb(http.StatusBadRequest, "idents or host_ips must be provided")
|
||||
}
|
||||
// Acquire idents by idents and hostIps
|
||||
failedResults, f.Idents, err = models.TargetsGetIdentsByIdentsAndHostIps(rt.Ctx, f.Idents, f.HostIps)
|
||||
if err != nil {
|
||||
ginx.Bomb(http.StatusBadRequest, err.Error())
|
||||
}
|
||||
|
||||
ginx.NewRender(c).Message(rt.targetBindTags(f))
|
||||
ginx.NewRender(c).Data(rt.targetBindTags(f, failedResults))
|
||||
}
|
||||
|
||||
func (rt *Router) targetBindTags(f targetTagsForm) error {
|
||||
for i := 0; i < len(f.Tags); i++ {
|
||||
arr := strings.Split(f.Tags[i], "=")
|
||||
func (rt *Router) targetBindTags(f targetTagsForm, failedIdents map[string]string) (map[string]string, error) {
|
||||
// 1. Check tags
|
||||
if err := rt.validateTags(f.Tags); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 2. Acquire targets by idents
|
||||
targets, err := models.TargetsGetByIdents(rt.Ctx, f.Idents)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 3. Add tags to targets
|
||||
for _, target := range targets {
|
||||
if err = rt.addTagsToTarget(target, f.Tags); err != nil {
|
||||
failedIdents[target.Ident] = err.Error()
|
||||
}
|
||||
}
|
||||
|
||||
return failedIdents, nil
|
||||
}
|
||||
|
||||
func (rt *Router) validateTags(tags []string) error {
|
||||
for _, tag := range tags {
|
||||
arr := strings.Split(tag, "=")
|
||||
if len(arr) != 2 {
|
||||
return fmt.Errorf("invalid tag(%s)", f.Tags[i])
|
||||
return fmt.Errorf("invalid tag format: %s (expected format: key=value)", tag)
|
||||
}
|
||||
|
||||
if strings.TrimSpace(arr[0]) == "" || strings.TrimSpace(arr[1]) == "" {
|
||||
return fmt.Errorf("invalid tag(%s)", f.Tags[i])
|
||||
key, value := strings.TrimSpace(arr[0]), strings.TrimSpace(arr[1])
|
||||
if key == "" {
|
||||
return fmt.Errorf("invalid tag: key is empty in tag %s", tag)
|
||||
}
|
||||
if value == "" {
|
||||
return fmt.Errorf("invalid tag: value is empty in tag %s", tag)
|
||||
}
|
||||
|
||||
if strings.IndexByte(arr[0], '.') != -1 {
|
||||
return fmt.Errorf("invalid tagkey(%s): cannot contains . ", arr[0])
|
||||
if strings.Contains(key, ".") {
|
||||
return fmt.Errorf("invalid tag key: %s (key cannot contain '.')", key)
|
||||
}
|
||||
|
||||
if strings.IndexByte(arr[0], '-') != -1 {
|
||||
return fmt.Errorf("invalid tagkey(%s): cannot contains -", arr[0])
|
||||
if strings.Contains(key, "-") {
|
||||
return fmt.Errorf("invalid tag key: %s (key cannot contain '-')", key)
|
||||
}
|
||||
|
||||
if !model.LabelNameRE.MatchString(arr[0]) {
|
||||
return fmt.Errorf("invalid tagkey(%s)", arr[0])
|
||||
if !model.LabelNameRE.MatchString(key) {
|
||||
return fmt.Errorf("invalid tag key: %s "+
|
||||
"(key must start with a letter or underscore, followed by letters, digits, or underscores)", key)
|
||||
}
|
||||
}
|
||||
|
||||
for i := 0; i < len(f.Idents); i++ {
|
||||
target, err := models.TargetGetByIdent(rt.Ctx, f.Idents[i])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if target == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// 不能有同key的标签,否则附到时序数据上会产生覆盖,让人困惑
|
||||
for j := 0; j < len(f.Tags); j++ {
|
||||
tagkey := strings.Split(f.Tags[j], "=")[0]
|
||||
tagkeyPrefix := tagkey + "="
|
||||
if strings.HasPrefix(target.Tags, tagkeyPrefix) {
|
||||
return fmt.Errorf("duplicate tagkey(%s)", tagkey)
|
||||
}
|
||||
}
|
||||
|
||||
err = target.AddTags(rt.Ctx, f.Tags)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (rt *Router) addTagsToTarget(target *models.Target, tags []string) error {
|
||||
for _, tag := range tags {
|
||||
tagKey := strings.Split(tag, "=")[0]
|
||||
if strings.Contains(target.Tags, tagKey+"=") {
|
||||
return fmt.Errorf("duplicate tagkey(%s)", tagKey)
|
||||
}
|
||||
}
|
||||
|
||||
return target.AddTags(rt.Ctx, tags)
|
||||
}
|
||||
|
||||
func (rt *Router) targetUnbindTagsByFE(c *gin.Context) {
|
||||
var f targetTagsForm
|
||||
var err error
|
||||
var failedResults = make(map[string]string)
|
||||
ginx.BindJSON(c, &f)
|
||||
|
||||
if len(f.Idents) == 0 {
|
||||
ginx.Bomb(http.StatusBadRequest, "idents empty")
|
||||
if len(f.Idents) == 0 && len(f.HostIps) == 0 {
|
||||
ginx.Bomb(http.StatusBadRequest, "idents or host_ips must be provided")
|
||||
}
|
||||
// Acquire idents by idents and hostIps
|
||||
failedResults, f.Idents, err = models.TargetsGetIdentsByIdentsAndHostIps(rt.Ctx, f.Idents, f.HostIps)
|
||||
if err != nil {
|
||||
ginx.Bomb(http.StatusBadRequest, err.Error())
|
||||
}
|
||||
|
||||
rt.checkTargetPerm(c, f.Idents)
|
||||
|
||||
ginx.NewRender(c).Message(rt.targetUnbindTags(f))
|
||||
ginx.NewRender(c).Data(rt.targetUnbindTags(f, failedResults))
|
||||
}
|
||||
|
||||
func (rt *Router) targetUnbindTagsByService(c *gin.Context) {
|
||||
var f targetTagsForm
|
||||
var err error
|
||||
var failedResults = make(map[string]string)
|
||||
ginx.BindJSON(c, &f)
|
||||
|
||||
if len(f.Idents) == 0 {
|
||||
ginx.Bomb(http.StatusBadRequest, "idents empty")
|
||||
if len(f.Idents) == 0 && len(f.HostIps) == 0 {
|
||||
ginx.Bomb(http.StatusBadRequest, "idents or host_ips must be provided")
|
||||
}
|
||||
// Acquire idents by idents and hostIps
|
||||
failedResults, f.Idents, err = models.TargetsGetIdentsByIdentsAndHostIps(rt.Ctx, f.Idents, f.HostIps)
|
||||
if err != nil {
|
||||
ginx.Bomb(http.StatusBadRequest, err.Error())
|
||||
}
|
||||
|
||||
ginx.NewRender(c).Message(rt.targetUnbindTags(f))
|
||||
ginx.NewRender(c).Data(rt.targetUnbindTags(f, failedResults))
|
||||
}
|
||||
|
||||
func (rt *Router) targetUnbindTags(f targetTagsForm) error {
|
||||
for i := 0; i < len(f.Idents); i++ {
|
||||
target, err := models.TargetGetByIdent(rt.Ctx, f.Idents[i])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if target == nil {
|
||||
continue
|
||||
}
|
||||
func (rt *Router) targetUnbindTags(f targetTagsForm, failedIdents map[string]string) (map[string]string, error) {
|
||||
// 1. Acquire targets by idents
|
||||
targets, err := models.TargetsGetByIdents(rt.Ctx, f.Idents)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 2. Remove tags from targets
|
||||
for _, target := range targets {
|
||||
err = target.DelTags(rt.Ctx, f.Tags)
|
||||
if err != nil {
|
||||
return err
|
||||
failedIdents[target.Ident] = err.Error()
|
||||
continue
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
||||
return failedIdents, nil
|
||||
}
|
||||
|
||||
type targetNoteForm struct {
|
||||
Idents []string `json:"idents" binding:"required"`
|
||||
Note string `json:"note"`
|
||||
Idents []string `json:"idents" binding:"required_without=HostIps"`
|
||||
HostIps []string `json:"host_ips" binding:"required_without=Idents"`
|
||||
Note string `json:"note"`
|
||||
}
|
||||
|
||||
func (rt *Router) targetUpdateNote(c *gin.Context) {
|
||||
var f targetNoteForm
|
||||
var err error
|
||||
var failedResults = make(map[string]string)
|
||||
ginx.BindJSON(c, &f)
|
||||
|
||||
if len(f.Idents) == 0 {
|
||||
ginx.Bomb(http.StatusBadRequest, "idents empty")
|
||||
if len(f.Idents) == 0 && len(f.HostIps) == 0 {
|
||||
ginx.Bomb(http.StatusBadRequest, "idents or host_ips must be provided")
|
||||
}
|
||||
|
||||
// Acquire idents by idents and hostIps
|
||||
failedResults, f.Idents, err = models.TargetsGetIdentsByIdentsAndHostIps(rt.Ctx, f.Idents, f.HostIps)
|
||||
if err != nil {
|
||||
ginx.Bomb(http.StatusBadRequest, err.Error())
|
||||
}
|
||||
|
||||
rt.checkTargetPerm(c, f.Idents)
|
||||
|
||||
ginx.NewRender(c).Message(models.TargetUpdateNote(rt.Ctx, f.Idents, f.Note))
|
||||
ginx.NewRender(c).Data(failedResults, models.TargetUpdateNote(rt.Ctx, f.Idents, f.Note))
|
||||
}
|
||||
|
||||
func (rt *Router) targetUpdateNoteByService(c *gin.Context) {
|
||||
var f targetNoteForm
|
||||
var err error
|
||||
var failedResults = make(map[string]string)
|
||||
ginx.BindJSON(c, &f)
|
||||
|
||||
if len(f.Idents) == 0 {
|
||||
ginx.Bomb(http.StatusBadRequest, "idents empty")
|
||||
if len(f.Idents) == 0 && len(f.HostIps) == 0 {
|
||||
ginx.Bomb(http.StatusBadRequest, "idents or host_ips must be provided")
|
||||
}
|
||||
|
||||
ginx.NewRender(c).Message(models.TargetUpdateNote(rt.Ctx, f.Idents, f.Note))
|
||||
// Acquire idents by idents and hostIps
|
||||
failedResults, f.Idents, err = models.TargetsGetIdentsByIdentsAndHostIps(rt.Ctx, f.Idents, f.HostIps)
|
||||
if err != nil {
|
||||
ginx.Bomb(http.StatusBadRequest, err.Error())
|
||||
}
|
||||
|
||||
ginx.NewRender(c).Data(failedResults, models.TargetUpdateNote(rt.Ctx, f.Idents, f.Note))
|
||||
}
|
||||
|
||||
type targetBgidForm struct {
|
||||
Idents []string `json:"idents" binding:"required"`
|
||||
Bgid int64 `json:"bgid"`
|
||||
Idents []string `json:"idents" binding:"required_without=HostIps"`
|
||||
HostIps []string `json:"host_ips" binding:"required_without=Idents"`
|
||||
Bgid int64 `json:"bgid"`
|
||||
}
|
||||
|
||||
func (rt *Router) targetUpdateBgid(c *gin.Context) {
|
||||
var f targetBgidForm
|
||||
var err error
|
||||
var failedResults = make(map[string]string)
|
||||
ginx.BindJSON(c, &f)
|
||||
|
||||
if len(f.Idents) == 0 {
|
||||
ginx.Bomb(http.StatusBadRequest, "idents empty")
|
||||
if len(f.Idents) == 0 && len(f.HostIps) == 0 {
|
||||
ginx.Bomb(http.StatusBadRequest, "idents or host_ips must be provided")
|
||||
}
|
||||
|
||||
// Acquire idents by idents and hostIps
|
||||
failedResults, f.Idents, err = models.TargetsGetIdentsByIdentsAndHostIps(rt.Ctx, f.Idents, f.HostIps)
|
||||
if err != nil {
|
||||
ginx.Bomb(http.StatusBadRequest, err.Error())
|
||||
}
|
||||
|
||||
user := c.MustGet("user").(*models.User)
|
||||
if user.IsAdmin() {
|
||||
ginx.NewRender(c).Message(models.TargetUpdateBgid(rt.Ctx, f.Idents, f.Bgid, false))
|
||||
ginx.NewRender(c).Data(failedResults, models.TargetUpdateBgid(rt.Ctx, f.Idents, f.Bgid, false))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -293,7 +401,11 @@ func (rt *Router) targetUpdateBgid(c *gin.Context) {
|
||||
|
||||
// 机器里边存在未归组的,登录用户就需要是admin
|
||||
if len(orphans) > 0 && !user.IsAdmin() {
|
||||
ginx.Bomb(http.StatusForbidden, "No permission. Only admin can assign BG")
|
||||
can, err := user.CheckPerm(rt.Ctx, "/targets/bind")
|
||||
ginx.Dangerous(err)
|
||||
if !can {
|
||||
ginx.Bomb(http.StatusForbidden, "No permission. Only admin can assign BG")
|
||||
}
|
||||
}
|
||||
|
||||
reBelongs, err := models.IdentsFilter(rt.Ctx, f.Idents, "group_id > ?", 0)
|
||||
@@ -318,24 +430,69 @@ func (rt *Router) targetUpdateBgid(c *gin.Context) {
|
||||
ginx.Bomb(http.StatusBadRequest, "invalid bgid")
|
||||
}
|
||||
|
||||
ginx.NewRender(c).Message(models.TargetUpdateBgid(rt.Ctx, f.Idents, f.Bgid, false))
|
||||
ginx.NewRender(c).Data(failedResults, models.TargetUpdateBgid(rt.Ctx, f.Idents, f.Bgid, false))
|
||||
}
|
||||
|
||||
func (rt *Router) targetUpdateBgidByService(c *gin.Context) {
|
||||
var f targetBgidForm
|
||||
var err error
|
||||
var failedResults = make(map[string]string)
|
||||
ginx.BindJSON(c, &f)
|
||||
|
||||
if len(f.Idents) == 0 && len(f.HostIps) == 0 {
|
||||
ginx.Bomb(http.StatusBadRequest, "idents or host_ips must be provided")
|
||||
}
|
||||
|
||||
// Acquire idents by idents and hostIps
|
||||
failedResults, f.Idents, err = models.TargetsGetIdentsByIdentsAndHostIps(rt.Ctx, f.Idents, f.HostIps)
|
||||
if err != nil {
|
||||
ginx.Bomb(http.StatusBadRequest, err.Error())
|
||||
}
|
||||
|
||||
ginx.NewRender(c).Data(failedResults, models.TargetUpdateBgid(rt.Ctx, f.Idents, f.Bgid, false))
|
||||
}
|
||||
|
||||
type identsForm struct {
|
||||
Idents []string `json:"idents" binding:"required"`
|
||||
Idents []string `json:"idents" binding:"required_without=HostIps"`
|
||||
HostIps []string `json:"host_ips" binding:"required_without=Idents"`
|
||||
}
|
||||
|
||||
func (rt *Router) targetDel(c *gin.Context) {
|
||||
var f identsForm
|
||||
var err error
|
||||
var failedResults = make(map[string]string)
|
||||
ginx.BindJSON(c, &f)
|
||||
|
||||
if len(f.Idents) == 0 {
|
||||
ginx.Bomb(http.StatusBadRequest, "idents empty")
|
||||
if len(f.Idents) == 0 && len(f.HostIps) == 0 {
|
||||
ginx.Bomb(http.StatusBadRequest, "idents or host_ips must be provided")
|
||||
}
|
||||
|
||||
rt.checkTargetPerm(c, f.Idents)
|
||||
// Acquire idents by idents and hostIps
|
||||
failedResults, f.Idents, err = models.TargetsGetIdentsByIdentsAndHostIps(rt.Ctx, f.Idents, f.HostIps)
|
||||
if err != nil {
|
||||
ginx.Bomb(http.StatusBadRequest, err.Error())
|
||||
}
|
||||
|
||||
ginx.NewRender(c).Message(models.TargetDel(rt.Ctx, f.Idents))
|
||||
ginx.NewRender(c).Data(failedResults, models.TargetDel(rt.Ctx, f.Idents))
|
||||
}
|
||||
|
||||
func (rt *Router) targetDelByService(c *gin.Context) {
|
||||
var f identsForm
|
||||
var err error
|
||||
var failedResults = make(map[string]string)
|
||||
ginx.BindJSON(c, &f)
|
||||
|
||||
if len(f.Idents) == 0 && len(f.HostIps) == 0 {
|
||||
ginx.Bomb(http.StatusBadRequest, "idents or host_ips must be provided")
|
||||
}
|
||||
|
||||
// Acquire idents by idents and hostIps
|
||||
failedResults, f.Idents, err = models.TargetsGetIdentsByIdentsAndHostIps(rt.Ctx, f.Idents, f.HostIps)
|
||||
if err != nil {
|
||||
ginx.Bomb(http.StatusBadRequest, err.Error())
|
||||
}
|
||||
|
||||
ginx.NewRender(c).Data(failedResults, models.TargetDel(rt.Ctx, f.Idents))
|
||||
}
|
||||
|
||||
func (rt *Router) checkTargetPerm(c *gin.Context, idents []string) {
|
||||
@@ -347,3 +504,21 @@ func (rt *Router) checkTargetPerm(c *gin.Context, idents []string) {
|
||||
ginx.Bomb(http.StatusForbidden, "No permission to operate the targets: %s", strings.Join(nopri, ", "))
|
||||
}
|
||||
}
|
||||
|
||||
func (rt *Router) targetsOfAlertRule(c *gin.Context) {
|
||||
engineName := ginx.QueryStr(c, "engine_name", "")
|
||||
m, err := models.GetTargetsOfHostAlertRule(rt.Ctx, engineName)
|
||||
ret := make(map[string]map[int64][]string)
|
||||
for en, v := range m {
|
||||
if en != engineName {
|
||||
continue
|
||||
}
|
||||
|
||||
ret[en] = make(map[int64][]string)
|
||||
for rid, idents := range v {
|
||||
ret[en][rid] = idents
|
||||
}
|
||||
}
|
||||
|
||||
ginx.NewRender(c).Data(ret, err)
|
||||
}
|
||||
|
||||
@@ -1,13 +1,9 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/ccfos/nightingale/v6/alert/sender"
|
||||
"github.com/ccfos/nightingale/v6/models"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -30,10 +26,55 @@ func (rt *Router) taskGets(c *gin.Context) {
|
||||
|
||||
beginTime := time.Now().Unix() - days*24*3600
|
||||
|
||||
total, err := models.TaskRecordTotal(rt.Ctx, bgid, beginTime, creator, query)
|
||||
total, err := models.TaskRecordTotal(rt.Ctx, []int64{bgid}, beginTime, creator, query)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
list, err := models.TaskRecordGets(rt.Ctx, bgid, beginTime, creator, query, limit, ginx.Offset(c, limit))
|
||||
list, err := models.TaskRecordGets(rt.Ctx, []int64{bgid}, beginTime, creator, query, limit, ginx.Offset(c, limit))
|
||||
ginx.Dangerous(err)
|
||||
|
||||
ginx.NewRender(c).Data(gin.H{
|
||||
"total": total,
|
||||
"list": list,
|
||||
}, nil)
|
||||
}
|
||||
|
||||
func (rt *Router) taskGetsByGids(c *gin.Context) {
|
||||
gids := str.IdsInt64(ginx.QueryStr(c, "gids", ""), ",")
|
||||
if len(gids) > 0 {
|
||||
for _, gid := range gids {
|
||||
rt.bgroCheck(c, gid)
|
||||
}
|
||||
} else {
|
||||
me := c.MustGet("user").(*models.User)
|
||||
if !me.IsAdmin() {
|
||||
var err error
|
||||
gids, err = models.MyBusiGroupIds(rt.Ctx, me.Id)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
if len(gids) == 0 {
|
||||
ginx.NewRender(c).Data([]int{}, nil)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mine := ginx.QueryBool(c, "mine", false)
|
||||
days := ginx.QueryInt64(c, "days", 7)
|
||||
limit := ginx.QueryInt(c, "limit", 20)
|
||||
query := ginx.QueryStr(c, "query", "")
|
||||
user := c.MustGet("user").(*models.User)
|
||||
|
||||
creator := ""
|
||||
if mine {
|
||||
creator = user.Username
|
||||
}
|
||||
|
||||
beginTime := time.Now().Unix() - days*24*3600
|
||||
|
||||
total, err := models.TaskRecordTotal(rt.Ctx, gids, beginTime, creator, query)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
list, err := models.TaskRecordGets(rt.Ctx, gids, beginTime, creator, query, limit, ginx.Offset(c, limit))
|
||||
ginx.Dangerous(err)
|
||||
|
||||
ginx.NewRender(c).Data(gin.H{
|
||||
@@ -56,70 +97,6 @@ type taskForm struct {
|
||||
Hosts []string `json:"hosts" binding:"required"`
|
||||
}
|
||||
|
||||
func (f *taskForm) Verify() error {
|
||||
if f.Batch < 0 {
|
||||
return fmt.Errorf("arg(batch) should be nonnegative")
|
||||
}
|
||||
|
||||
if f.Tolerance < 0 {
|
||||
return fmt.Errorf("arg(tolerance) should be nonnegative")
|
||||
}
|
||||
|
||||
if f.Timeout < 0 {
|
||||
return fmt.Errorf("arg(timeout) should be nonnegative")
|
||||
}
|
||||
|
||||
if f.Timeout > 3600*24 {
|
||||
return fmt.Errorf("arg(timeout) longer than one day")
|
||||
}
|
||||
|
||||
if f.Timeout == 0 {
|
||||
f.Timeout = 30
|
||||
}
|
||||
|
||||
f.Pause = strings.Replace(f.Pause, ",", ",", -1)
|
||||
f.Pause = strings.Replace(f.Pause, " ", "", -1)
|
||||
f.Args = strings.Replace(f.Args, ",", ",", -1)
|
||||
|
||||
if f.Title == "" {
|
||||
return fmt.Errorf("arg(title) is required")
|
||||
}
|
||||
|
||||
if str.Dangerous(f.Title) {
|
||||
return fmt.Errorf("arg(title) is dangerous")
|
||||
}
|
||||
|
||||
if f.Script == "" {
|
||||
return fmt.Errorf("arg(script) is required")
|
||||
}
|
||||
|
||||
if str.Dangerous(f.Args) {
|
||||
return fmt.Errorf("arg(args) is dangerous")
|
||||
}
|
||||
|
||||
if str.Dangerous(f.Pause) {
|
||||
return fmt.Errorf("arg(pause) is dangerous")
|
||||
}
|
||||
|
||||
if len(f.Hosts) == 0 {
|
||||
return fmt.Errorf("arg(hosts) empty")
|
||||
}
|
||||
|
||||
if f.Action != "start" && f.Action != "pause" {
|
||||
return fmt.Errorf("arg(action) invalid")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *taskForm) HandleFH(fh string) {
|
||||
i := strings.Index(f.Title, " FH: ")
|
||||
if i > 0 {
|
||||
f.Title = f.Title[:i]
|
||||
}
|
||||
f.Title = f.Title + " FH: " + fh
|
||||
}
|
||||
|
||||
func (rt *Router) taskRecordAdd(c *gin.Context) {
|
||||
var f *models.TaskRecord
|
||||
ginx.BindJSON(c, &f)
|
||||
@@ -127,7 +104,7 @@ func (rt *Router) taskRecordAdd(c *gin.Context) {
|
||||
}
|
||||
|
||||
func (rt *Router) taskAdd(c *gin.Context) {
|
||||
var f taskForm
|
||||
var f models.TaskForm
|
||||
ginx.BindJSON(c, &f)
|
||||
|
||||
bgid := ginx.UrlParamInt64(c, "id")
|
||||
@@ -143,7 +120,7 @@ func (rt *Router) taskAdd(c *gin.Context) {
|
||||
rt.checkTargetPerm(c, f.Hosts)
|
||||
|
||||
// call ibex
|
||||
taskId, err := TaskCreate(f, rt.NotifyConfigCache.GetIbex())
|
||||
taskId, err := sender.TaskAdd(f, user.Username, rt.Ctx.IsCenter)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
if taskId <= 0 {
|
||||
@@ -152,65 +129,20 @@ func (rt *Router) taskAdd(c *gin.Context) {
|
||||
|
||||
// write db
|
||||
record := models.TaskRecord{
|
||||
Id: taskId,
|
||||
GroupId: bgid,
|
||||
IbexAddress: rt.NotifyConfigCache.GetIbex().Address,
|
||||
IbexAuthUser: rt.NotifyConfigCache.GetIbex().BasicAuthUser,
|
||||
IbexAuthPass: rt.NotifyConfigCache.GetIbex().BasicAuthPass,
|
||||
Title: f.Title,
|
||||
Account: f.Account,
|
||||
Batch: f.Batch,
|
||||
Tolerance: f.Tolerance,
|
||||
Timeout: f.Timeout,
|
||||
Pause: f.Pause,
|
||||
Script: f.Script,
|
||||
Args: f.Args,
|
||||
CreateAt: time.Now().Unix(),
|
||||
CreateBy: f.Creator,
|
||||
Id: taskId,
|
||||
GroupId: bgid,
|
||||
Title: f.Title,
|
||||
Account: f.Account,
|
||||
Batch: f.Batch,
|
||||
Tolerance: f.Tolerance,
|
||||
Timeout: f.Timeout,
|
||||
Pause: f.Pause,
|
||||
Script: f.Script,
|
||||
Args: f.Args,
|
||||
CreateAt: time.Now().Unix(),
|
||||
CreateBy: f.Creator,
|
||||
}
|
||||
|
||||
err = record.Add(rt.Ctx)
|
||||
ginx.NewRender(c).Data(taskId, err)
|
||||
}
|
||||
|
||||
func (rt *Router) taskProxy(c *gin.Context) {
|
||||
target, err := url.Parse(rt.NotifyConfigCache.GetIbex().Address)
|
||||
if err != nil {
|
||||
ginx.NewRender(c).Message("invalid ibex address: %s", rt.NotifyConfigCache.GetIbex().Address)
|
||||
return
|
||||
}
|
||||
|
||||
director := func(req *http.Request) {
|
||||
req.URL.Scheme = target.Scheme
|
||||
req.URL.Host = target.Host
|
||||
|
||||
// fe request e.g. /api/n9e/busi-group/:id/task/*url
|
||||
index := strings.Index(req.URL.Path, "/task/")
|
||||
if index == -1 {
|
||||
panic("url path invalid")
|
||||
}
|
||||
|
||||
req.URL.Path = "/ibex/v1" + req.URL.Path[index:]
|
||||
|
||||
if target.RawQuery == "" || req.URL.RawQuery == "" {
|
||||
req.URL.RawQuery = target.RawQuery + req.URL.RawQuery
|
||||
} else {
|
||||
req.URL.RawQuery = target.RawQuery + "&" + req.URL.RawQuery
|
||||
}
|
||||
|
||||
if rt.NotifyConfigCache.GetIbex().BasicAuthUser != "" {
|
||||
req.SetBasicAuth(rt.NotifyConfigCache.GetIbex().BasicAuthUser, rt.NotifyConfigCache.GetIbex().BasicAuthPass)
|
||||
}
|
||||
}
|
||||
|
||||
errFunc := func(w http.ResponseWriter, r *http.Request, err error) {
|
||||
ginx.NewRender(c, http.StatusBadGateway).Message(err)
|
||||
}
|
||||
|
||||
proxy := &httputil.ReverseProxy{
|
||||
Director: director,
|
||||
ErrorHandler: errFunc,
|
||||
}
|
||||
|
||||
proxy.ServeHTTP(c.Writer, c.Request)
|
||||
}
|
||||
|
||||
@@ -18,10 +18,45 @@ func (rt *Router) taskTplGets(c *gin.Context) {
|
||||
limit := ginx.QueryInt(c, "limit", 20)
|
||||
groupId := ginx.UrlParamInt64(c, "id")
|
||||
|
||||
total, err := models.TaskTplTotal(rt.Ctx, groupId, query)
|
||||
total, err := models.TaskTplTotal(rt.Ctx, []int64{groupId}, query)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
list, err := models.TaskTplGets(rt.Ctx, groupId, query, limit, ginx.Offset(c, limit))
|
||||
list, err := models.TaskTplGets(rt.Ctx, []int64{groupId}, query, limit, ginx.Offset(c, limit))
|
||||
ginx.Dangerous(err)
|
||||
|
||||
ginx.NewRender(c).Data(gin.H{
|
||||
"total": total,
|
||||
"list": list,
|
||||
}, nil)
|
||||
}
|
||||
|
||||
func (rt *Router) taskTplGetsByGids(c *gin.Context) {
|
||||
query := ginx.QueryStr(c, "query", "")
|
||||
limit := ginx.QueryInt(c, "limit", 20)
|
||||
|
||||
gids := str.IdsInt64(ginx.QueryStr(c, "gids", ""), ",")
|
||||
if len(gids) > 0 {
|
||||
for _, gid := range gids {
|
||||
rt.bgroCheck(c, gid)
|
||||
}
|
||||
} else {
|
||||
me := c.MustGet("user").(*models.User)
|
||||
if !me.IsAdmin() {
|
||||
var err error
|
||||
gids, err = models.MyBusiGroupIds(rt.Ctx, me.Id)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
if len(gids) == 0 {
|
||||
ginx.NewRender(c).Data([]int{}, nil)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
total, err := models.TaskTplTotal(rt.Ctx, gids, query)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
list, err := models.TaskTplGets(rt.Ctx, gids, query, limit, ginx.Offset(c, limit))
|
||||
ginx.Dangerous(err)
|
||||
|
||||
ginx.NewRender(c).Data(gin.H{
|
||||
@@ -48,6 +83,27 @@ func (rt *Router) taskTplGet(c *gin.Context) {
|
||||
}, err)
|
||||
}
|
||||
|
||||
func (rt *Router) taskTplGetByService(c *gin.Context) {
|
||||
tid := ginx.UrlParamInt64(c, "tid")
|
||||
|
||||
tpl, err := models.TaskTplGetById(rt.Ctx, tid)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
if tpl == nil {
|
||||
ginx.Bomb(404, "no such task template")
|
||||
}
|
||||
|
||||
ginx.NewRender(c).Data(tpl, err)
|
||||
}
|
||||
|
||||
func (rt *Router) taskTplGetsByService(c *gin.Context) {
|
||||
ginx.NewRender(c).Data(models.TaskTplGetAll(rt.Ctx))
|
||||
}
|
||||
|
||||
func (rt *Router) taskTplStatistics(c *gin.Context) {
|
||||
ginx.NewRender(c).Data(models.TaskTplStatistics(rt.Ctx))
|
||||
}
|
||||
|
||||
type taskTplForm struct {
|
||||
Title string `json:"title" binding:"required"`
|
||||
Batch int `json:"batch"`
|
||||
|
||||
117
center/router/router_tdengine.go
Normal file
117
center/router/router_tdengine.go
Normal file
@@ -0,0 +1,117 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/ccfos/nightingale/v6/center/cconf"
|
||||
"github.com/ccfos/nightingale/v6/models"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/toolkits/pkg/ginx"
|
||||
"github.com/toolkits/pkg/logger"
|
||||
)
|
||||
|
||||
type databasesQueryForm struct {
|
||||
Cate string `json:"cate" form:"cate"`
|
||||
DatasourceId int64 `json:"datasource_id" form:"datasource_id"`
|
||||
}
|
||||
|
||||
func (rt *Router) tdengineDatabases(c *gin.Context) {
|
||||
var f databasesQueryForm
|
||||
ginx.BindJSON(c, &f)
|
||||
|
||||
tdClient := rt.TdendgineClients.GetCli(f.DatasourceId)
|
||||
if tdClient == nil {
|
||||
ginx.NewRender(c, http.StatusNotFound).Message("No such datasource")
|
||||
return
|
||||
}
|
||||
|
||||
databases, err := tdClient.GetDatabases()
|
||||
ginx.NewRender(c).Data(databases, err)
|
||||
}
|
||||
|
||||
type tablesQueryForm struct {
|
||||
Cate string `json:"cate"`
|
||||
DatasourceId int64 `json:"datasource_id" `
|
||||
Database string `json:"db"`
|
||||
IsStable bool `json:"is_stable"`
|
||||
}
|
||||
|
||||
// get tdengine tables
|
||||
func (rt *Router) tdengineTables(c *gin.Context) {
|
||||
var f tablesQueryForm
|
||||
ginx.BindJSON(c, &f)
|
||||
|
||||
tdClient := rt.TdendgineClients.GetCli(f.DatasourceId)
|
||||
if tdClient == nil {
|
||||
ginx.NewRender(c, http.StatusNotFound).Message("No such datasource")
|
||||
return
|
||||
}
|
||||
|
||||
tables, err := tdClient.GetTables(f.Database, f.IsStable)
|
||||
ginx.NewRender(c).Data(tables, err)
|
||||
}
|
||||
|
||||
type columnsQueryForm struct {
|
||||
Cate string `json:"cate"`
|
||||
DatasourceId int64 `json:"datasource_id" `
|
||||
Database string `json:"db"`
|
||||
Table string `json:"table"`
|
||||
}
|
||||
|
||||
// get tdengine columns
|
||||
func (rt *Router) tdengineColumns(c *gin.Context) {
|
||||
var f columnsQueryForm
|
||||
ginx.BindJSON(c, &f)
|
||||
|
||||
tdClient := rt.TdendgineClients.GetCli(f.DatasourceId)
|
||||
if tdClient == nil {
|
||||
ginx.NewRender(c, http.StatusNotFound).Message("No such datasource")
|
||||
return
|
||||
}
|
||||
|
||||
columns, err := tdClient.GetColumns(f.Database, f.Table)
|
||||
ginx.NewRender(c).Data(columns, err)
|
||||
}
|
||||
|
||||
func (rt *Router) QueryData(c *gin.Context) {
|
||||
var f models.QueryParam
|
||||
ginx.BindJSON(c, &f)
|
||||
|
||||
var resp []models.DataResp
|
||||
var err error
|
||||
tdClient := rt.TdendgineClients.GetCli(f.DatasourceId)
|
||||
for _, q := range f.Querys {
|
||||
datas, err := tdClient.Query(q)
|
||||
ginx.Dangerous(err)
|
||||
resp = append(resp, datas...)
|
||||
}
|
||||
|
||||
ginx.NewRender(c).Data(resp, err)
|
||||
}
|
||||
|
||||
func (rt *Router) QueryLog(c *gin.Context) {
|
||||
var f models.QueryParam
|
||||
ginx.BindJSON(c, &f)
|
||||
|
||||
tdClient := rt.TdendgineClients.GetCli(f.DatasourceId)
|
||||
if len(f.Querys) == 0 {
|
||||
ginx.Bomb(200, "querys is empty")
|
||||
return
|
||||
}
|
||||
|
||||
data, err := tdClient.QueryLog(f.Querys[0])
|
||||
logger.Debugf("tdengine query:%s result: %+v", f.Querys[0], data)
|
||||
ginx.NewRender(c).Data(data, err)
|
||||
}
|
||||
|
||||
// query sql template
|
||||
func (rt *Router) QuerySqlTemplate(c *gin.Context) {
|
||||
cate := ginx.QueryStr(c, "cate")
|
||||
m := make(map[string]string)
|
||||
switch cate {
|
||||
case models.TDENGINE:
|
||||
m = cconf.TDengineSQLTpl
|
||||
}
|
||||
ginx.NewRender(c).Data(m, nil)
|
||||
}
|
||||
@@ -5,25 +5,52 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/ccfos/nightingale/v6/models"
|
||||
"github.com/ccfos/nightingale/v6/pkg/flashduty"
|
||||
"github.com/ccfos/nightingale/v6/pkg/ormx"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/toolkits/pkg/ginx"
|
||||
)
|
||||
|
||||
func (rt *Router) userBusiGroupsGets(c *gin.Context) {
|
||||
userid := ginx.QueryInt64(c, "userid", 0)
|
||||
username := ginx.QueryStr(c, "username", "")
|
||||
|
||||
if userid == 0 && username == "" {
|
||||
ginx.Bomb(http.StatusBadRequest, "userid or username required")
|
||||
}
|
||||
|
||||
var user *models.User
|
||||
var err error
|
||||
if userid > 0 {
|
||||
user, err = models.UserGetById(rt.Ctx, userid)
|
||||
} else {
|
||||
user, err = models.UserGetByUsername(rt.Ctx, username)
|
||||
}
|
||||
|
||||
ginx.Dangerous(err)
|
||||
|
||||
groups, err := user.BusiGroups(rt.Ctx, 10000, "")
|
||||
ginx.NewRender(c).Data(groups, err)
|
||||
}
|
||||
|
||||
func (rt *Router) userFindAll(c *gin.Context) {
|
||||
list, err := models.UserGetAll(rt.Ctx)
|
||||
ginx.NewRender(c).Data(list, err)
|
||||
}
|
||||
|
||||
func (rt *Router) userGets(c *gin.Context) {
|
||||
stime, etime := getTimeRange(c)
|
||||
limit := ginx.QueryInt(c, "limit", 20)
|
||||
query := ginx.QueryStr(c, "query", "")
|
||||
order := ginx.QueryStr(c, "order", "username")
|
||||
desc := ginx.QueryBool(c, "desc", false)
|
||||
|
||||
total, err := models.UserTotal(rt.Ctx, query)
|
||||
rt.UserCache.UpdateUsersLastActiveTime()
|
||||
total, err := models.UserTotal(rt.Ctx, query, stime, etime)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
list, err := models.UserGets(rt.Ctx, query, limit, ginx.Offset(c, limit))
|
||||
list, err := models.UserGets(rt.Ctx, query, limit, ginx.Offset(c, limit), stime, etime, order, desc)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
user := c.MustGet("user").(*models.User)
|
||||
@@ -57,7 +84,7 @@ func (rt *Router) userAddPost(c *gin.Context) {
|
||||
ginx.Bomb(http.StatusBadRequest, "roles empty")
|
||||
}
|
||||
|
||||
user := c.MustGet("user").(*models.User)
|
||||
username := Username(c)
|
||||
|
||||
u := models.User{
|
||||
Username: f.Username,
|
||||
@@ -68,10 +95,11 @@ func (rt *Router) userAddPost(c *gin.Context) {
|
||||
Portrait: f.Portrait,
|
||||
Roles: strings.Join(f.Roles, " "),
|
||||
Contacts: f.Contacts,
|
||||
CreateBy: user.Username,
|
||||
UpdateBy: user.Username,
|
||||
CreateBy: username,
|
||||
UpdateBy: username,
|
||||
}
|
||||
|
||||
ginx.Dangerous(u.Verify())
|
||||
ginx.NewRender(c).Message(u.Add(rt.Ctx))
|
||||
}
|
||||
|
||||
@@ -88,6 +116,30 @@ type userProfileForm struct {
|
||||
Contacts ormx.JSONObj `json:"contacts"`
|
||||
}
|
||||
|
||||
func (rt *Router) userProfilePutByService(c *gin.Context) {
|
||||
var f models.User
|
||||
ginx.BindJSON(c, &f)
|
||||
|
||||
if len(f.RolesLst) == 0 {
|
||||
ginx.Bomb(http.StatusBadRequest, "roles empty")
|
||||
}
|
||||
|
||||
password, err := models.CryptoPass(rt.Ctx, f.Password)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
target := User(rt.Ctx, ginx.UrlParamInt64(c, "id"))
|
||||
target.Nickname = f.Nickname
|
||||
target.Password = password
|
||||
target.Phone = f.Phone
|
||||
target.Email = f.Email
|
||||
target.Portrait = f.Portrait
|
||||
target.Roles = strings.Join(f.RolesLst, " ")
|
||||
target.Contacts = f.Contacts
|
||||
target.UpdateBy = Username(c)
|
||||
|
||||
ginx.NewRender(c).Message(target.UpdateAllFields(rt.Ctx))
|
||||
}
|
||||
|
||||
func (rt *Router) userProfilePut(c *gin.Context) {
|
||||
var f userProfileForm
|
||||
ginx.BindJSON(c, &f)
|
||||
@@ -97,6 +149,11 @@ func (rt *Router) userProfilePut(c *gin.Context) {
|
||||
}
|
||||
|
||||
target := User(rt.Ctx, ginx.UrlParamInt64(c, "id"))
|
||||
oldInfo := models.User{
|
||||
Username: target.Username,
|
||||
Phone: target.Phone,
|
||||
Email: target.Email,
|
||||
}
|
||||
target.Nickname = f.Nickname
|
||||
target.Phone = f.Phone
|
||||
target.Email = f.Email
|
||||
@@ -104,6 +161,10 @@ func (rt *Router) userProfilePut(c *gin.Context) {
|
||||
target.Contacts = f.Contacts
|
||||
target.UpdateBy = c.MustGet("username").(string)
|
||||
|
||||
if flashduty.NeedSyncUser(rt.Ctx) {
|
||||
flashduty.UpdateUser(rt.Ctx, oldInfo, f.Email, f.Phone)
|
||||
}
|
||||
|
||||
ginx.NewRender(c).Message(target.UpdateAllFields(rt.Ctx))
|
||||
}
|
||||
|
||||
|
||||
@@ -5,10 +5,12 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/ccfos/nightingale/v6/models"
|
||||
"github.com/ccfos/nightingale/v6/pkg/flashduty"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/toolkits/pkg/ginx"
|
||||
"github.com/toolkits/pkg/logger"
|
||||
"github.com/toolkits/pkg/str"
|
||||
)
|
||||
|
||||
func (rt *Router) checkBusiGroupPerm(c *gin.Context) {
|
||||
@@ -30,8 +32,36 @@ func (rt *Router) userGroupGets(c *gin.Context) {
|
||||
}
|
||||
|
||||
func (rt *Router) userGroupGetsByService(c *gin.Context) {
|
||||
lst, err := models.UserGroupGetAll(rt.Ctx)
|
||||
ginx.NewRender(c).Data(lst, err)
|
||||
ids := str.IdsInt64(ginx.QueryStr(c, "ids", ""))
|
||||
|
||||
if len(ids) == 0 {
|
||||
lst, err := models.UserGroupGetAll(rt.Ctx)
|
||||
ginx.Dangerous(err)
|
||||
for i := 0; i < len(lst); i++ {
|
||||
ids, err := models.MemberIds(rt.Ctx, lst[i].Id)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
lst[i].Users, err = models.UserGetsByIds(rt.Ctx, ids)
|
||||
ginx.Dangerous(err)
|
||||
}
|
||||
ginx.NewRender(c).Data(lst, err)
|
||||
return
|
||||
}
|
||||
|
||||
lst := make([]models.UserGroup, 0)
|
||||
for _, id := range ids {
|
||||
ug := UserGroup(rt.Ctx, id)
|
||||
|
||||
ids, err := models.MemberIds(rt.Ctx, ug.Id)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
ug.Users, err = models.UserGetsByIds(rt.Ctx, ids)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
lst = append(lst, *ug)
|
||||
}
|
||||
|
||||
ginx.NewRender(c).Data(lst, nil)
|
||||
}
|
||||
|
||||
// user group member get by service
|
||||
@@ -41,8 +71,9 @@ func (rt *Router) userGroupMemberGetsByService(c *gin.Context) {
|
||||
}
|
||||
|
||||
type userGroupForm struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
Note string `json:"note"`
|
||||
Name string `json:"name" binding:"required"`
|
||||
Note string `json:"note"`
|
||||
IsSyncToFlashDuty bool `json:"is_sync_to_flashduty"`
|
||||
}
|
||||
|
||||
func (rt *Router) userGroupAdd(c *gin.Context) {
|
||||
@@ -59,12 +90,19 @@ func (rt *Router) userGroupAdd(c *gin.Context) {
|
||||
}
|
||||
|
||||
err := ug.Add(rt.Ctx)
|
||||
if err == nil {
|
||||
// Even failure is not a big deal
|
||||
models.UserGroupMemberAdd(rt.Ctx, ug.Id, me.Id)
|
||||
}
|
||||
ginx.Dangerous(err)
|
||||
|
||||
// Even failure is not a big deal
|
||||
models.UserGroupMemberAdd(rt.Ctx, ug.Id, me.Id)
|
||||
|
||||
if f.IsSyncToFlashDuty || flashduty.NeedSyncTeam(rt.Ctx) {
|
||||
ugs, err := flashduty.NewUserGroupSyncer(rt.Ctx, &ug)
|
||||
ginx.Dangerous(err)
|
||||
err = ugs.SyncUGAdd()
|
||||
ginx.Dangerous(err)
|
||||
}
|
||||
ginx.NewRender(c).Data(ug.Id, err)
|
||||
|
||||
}
|
||||
|
||||
func (rt *Router) userGroupPut(c *gin.Context) {
|
||||
@@ -73,6 +111,7 @@ func (rt *Router) userGroupPut(c *gin.Context) {
|
||||
|
||||
me := c.MustGet("user").(*models.User)
|
||||
ug := c.MustGet("user_group").(*models.UserGroup)
|
||||
oldUGName := ug.Name
|
||||
|
||||
if ug.Name != f.Name {
|
||||
// name changed, check duplication
|
||||
@@ -88,8 +127,14 @@ func (rt *Router) userGroupPut(c *gin.Context) {
|
||||
ug.Note = f.Note
|
||||
ug.UpdateBy = me.Username
|
||||
ug.UpdateAt = time.Now().Unix()
|
||||
|
||||
if f.IsSyncToFlashDuty || flashduty.NeedSyncTeam(rt.Ctx) {
|
||||
ugs, err := flashduty.NewUserGroupSyncer(rt.Ctx, ug)
|
||||
ginx.Dangerous(err)
|
||||
err = ugs.SyncUGPut(oldUGName)
|
||||
ginx.Dangerous(err)
|
||||
}
|
||||
ginx.NewRender(c).Message(ug.Update(rt.Ctx, "Name", "Note", "UpdateAt", "UpdateBy"))
|
||||
|
||||
}
|
||||
|
||||
// Return all members, front-end search and paging
|
||||
@@ -109,8 +154,16 @@ func (rt *Router) userGroupGet(c *gin.Context) {
|
||||
}
|
||||
|
||||
func (rt *Router) userGroupDel(c *gin.Context) {
|
||||
isSyncToFlashDuty := ginx.QueryBool(c, "is_sync_to_flashduty", false)
|
||||
ug := c.MustGet("user_group").(*models.UserGroup)
|
||||
if isSyncToFlashDuty || flashduty.NeedSyncTeam(rt.Ctx) {
|
||||
ugs, err := flashduty.NewUserGroupSyncer(rt.Ctx, ug)
|
||||
ginx.Dangerous(err)
|
||||
err = ugs.SyncUGDel(ug.Name)
|
||||
ginx.Dangerous(err)
|
||||
}
|
||||
ginx.NewRender(c).Message(ug.Del(rt.Ctx))
|
||||
|
||||
}
|
||||
|
||||
func (rt *Router) userGroupMemberAdd(c *gin.Context) {
|
||||
@@ -122,13 +175,21 @@ func (rt *Router) userGroupMemberAdd(c *gin.Context) {
|
||||
ug := c.MustGet("user_group").(*models.UserGroup)
|
||||
|
||||
err := ug.AddMembers(rt.Ctx, f.Ids)
|
||||
ginx.Dangerous(err)
|
||||
if err == nil {
|
||||
ug.UpdateAt = time.Now().Unix()
|
||||
ug.UpdateBy = me.Username
|
||||
ug.Update(rt.Ctx, "UpdateAt", "UpdateBy")
|
||||
}
|
||||
|
||||
if f.IsSyncToFlashDuty || flashduty.NeedSyncTeam(rt.Ctx) {
|
||||
ugs, err := flashduty.NewUserGroupSyncer(rt.Ctx, ug)
|
||||
ginx.Dangerous(err)
|
||||
err = ugs.SyncMembersAdd()
|
||||
ginx.Dangerous(err)
|
||||
}
|
||||
ginx.NewRender(c).Message(err)
|
||||
|
||||
}
|
||||
|
||||
func (rt *Router) userGroupMemberDel(c *gin.Context) {
|
||||
@@ -145,6 +206,12 @@ func (rt *Router) userGroupMemberDel(c *gin.Context) {
|
||||
ug.UpdateBy = me.Username
|
||||
ug.Update(rt.Ctx, "UpdateAt", "UpdateBy")
|
||||
}
|
||||
if f.IsSyncToFlashDuty || flashduty.NeedSyncTeam(rt.Ctx) {
|
||||
ugs, err := flashduty.NewUserGroupSyncer(rt.Ctx, ug)
|
||||
ginx.Dangerous(err)
|
||||
err = ugs.SyncMembersDel()
|
||||
ginx.Dangerous(err)
|
||||
}
|
||||
|
||||
ginx.NewRender(c).Message(err)
|
||||
}
|
||||
|
||||
70
center/router/router_user_variable_config.go
Normal file
70
center/router/router_user_variable_config.go
Normal file
@@ -0,0 +1,70 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/ccfos/nightingale/v6/models"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/toolkits/pkg/ginx"
|
||||
)
|
||||
|
||||
func (rt *Router) userVariableConfigGets(context *gin.Context) {
|
||||
userVariables, err := models.ConfigsGetUserVariable(rt.Ctx)
|
||||
ginx.NewRender(context).Data(userVariables, err)
|
||||
}
|
||||
func (rt *Router) userVariableConfigAdd(context *gin.Context) {
|
||||
var f models.Configs
|
||||
ginx.BindJSON(context, &f)
|
||||
f.Ckey = strings.TrimSpace(f.Ckey)
|
||||
//insert external config. needs to make sure not plaintext for an encrypted type config
|
||||
username := context.MustGet("username").(string)
|
||||
now := time.Now().Unix()
|
||||
f.CreateBy = username
|
||||
f.UpdateBy = username
|
||||
f.CreateAt = now
|
||||
f.UpdateAt = now
|
||||
ginx.NewRender(context).Message(models.ConfigsUserVariableInsert(rt.Ctx, f))
|
||||
|
||||
}
|
||||
|
||||
func (rt *Router) userVariableConfigPut(context *gin.Context) {
|
||||
var f models.Configs
|
||||
ginx.BindJSON(context, &f)
|
||||
f.Id = ginx.UrlParamInt64(context, "id")
|
||||
f.Ckey = strings.TrimSpace(f.Ckey)
|
||||
f.UpdateBy = context.MustGet("username").(string)
|
||||
f.UpdateAt = time.Now().Unix()
|
||||
|
||||
user := context.MustGet("user").(*models.User)
|
||||
if !user.IsAdmin() && f.CreateBy != user.Username {
|
||||
// only admin or creator can update
|
||||
ginx.Bomb(403, "no permission")
|
||||
}
|
||||
|
||||
ginx.NewRender(context).Message(models.ConfigsUserVariableUpdate(rt.Ctx, f))
|
||||
}
|
||||
|
||||
func (rt *Router) userVariableConfigDel(context *gin.Context) {
|
||||
id := ginx.UrlParamInt64(context, "id")
|
||||
configs, err := models.ConfigGet(rt.Ctx, id)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
user := context.MustGet("user").(*models.User)
|
||||
if !user.IsAdmin() && configs.CreateBy != user.Username {
|
||||
// only admin or creator can delete
|
||||
ginx.Bomb(403, "no permission")
|
||||
}
|
||||
|
||||
if configs != nil && configs.External == models.ConfigExternal {
|
||||
ginx.NewRender(context).Message(models.ConfigsDel(rt.Ctx, []int64{id}))
|
||||
} else {
|
||||
ginx.NewRender(context).Message(nil)
|
||||
}
|
||||
}
|
||||
|
||||
func (rt *Router) userVariableGetDecryptByService(context *gin.Context) {
|
||||
decryptMap, decryptErr := models.ConfigUserVariableGetDecryptMap(rt.Ctx, rt.HTTP.RSA.RSAPrivateKey, rt.HTTP.RSA.RSAPassWord)
|
||||
ginx.NewRender(context).Data(decryptMap, decryptErr)
|
||||
}
|
||||
@@ -2,24 +2,30 @@ package sso
|
||||
|
||||
import (
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/BurntSushi/toml"
|
||||
"github.com/ccfos/nightingale/v6/center/cconf"
|
||||
"github.com/ccfos/nightingale/v6/memsto"
|
||||
"github.com/ccfos/nightingale/v6/models"
|
||||
"github.com/ccfos/nightingale/v6/pkg/cas"
|
||||
"github.com/ccfos/nightingale/v6/pkg/ctx"
|
||||
"github.com/ccfos/nightingale/v6/pkg/ldapx"
|
||||
"github.com/ccfos/nightingale/v6/pkg/oauth2x"
|
||||
"github.com/ccfos/nightingale/v6/pkg/oidcx"
|
||||
"github.com/ccfos/nightingale/v6/pkg/tplx"
|
||||
|
||||
"github.com/BurntSushi/toml"
|
||||
"github.com/toolkits/pkg/logger"
|
||||
)
|
||||
|
||||
type SsoClient struct {
|
||||
OIDC *oidcx.SsoClient
|
||||
LDAP *ldapx.SsoClient
|
||||
CAS *cas.SsoClient
|
||||
OAuth2 *oauth2x.SsoClient
|
||||
OIDC *oidcx.SsoClient
|
||||
LDAP *ldapx.SsoClient
|
||||
CAS *cas.SsoClient
|
||||
OAuth2 *oauth2x.SsoClient
|
||||
LastUpdateTime int64
|
||||
configCache *memsto.ConfigCache
|
||||
configLastUpdateTime int64
|
||||
}
|
||||
|
||||
const LDAP = `
|
||||
@@ -29,13 +35,21 @@ Port = 389
|
||||
BaseDn = 'dc=example,dc=org'
|
||||
BindUser = 'cn=manager,dc=example,dc=org'
|
||||
BindPass = '*******'
|
||||
SyncAddUsers = false
|
||||
SyncDelUsers = false
|
||||
# unit: s
|
||||
SyncInterval = 86400
|
||||
# openldap format e.g. (&(uid=%s))
|
||||
# AD format e.g. (&(sAMAccountName=%s))
|
||||
AuthFilter = '(&(uid=%s))'
|
||||
UserFilter = '(&(uid=*))'
|
||||
CoverAttributes = true
|
||||
TLS = false
|
||||
StartTLS = true
|
||||
DefaultRoles = ['Standard']
|
||||
|
||||
[Attributes]
|
||||
Username = 'uid'
|
||||
Nickname = 'cn'
|
||||
Phone = 'mobile'
|
||||
Email = 'mail'
|
||||
@@ -44,8 +58,9 @@ Email = 'mail'
|
||||
const OAuth2 = `
|
||||
Enable = false
|
||||
DisplayName = 'OAuth2登录'
|
||||
RedirectURL = 'http://127.0.0.1:18000/callback/oauth'
|
||||
RedirectURL = 'http://n9e.com/callback/oauth'
|
||||
SsoAddr = 'https://sso.example.com/oauth2/authorize'
|
||||
SsoLogoutAddr = 'https://sso.example.com/oauth2/authorize/session/end'
|
||||
TokenAddr = 'https://sso.example.com/oauth2/token'
|
||||
UserInfoAddr = 'https://api.example.com/api/v1/user/info'
|
||||
TranTokenMethod = 'header'
|
||||
@@ -58,7 +73,7 @@ UserinfoPrefix = 'data'
|
||||
Scopes = ['profile', 'email', 'phone']
|
||||
|
||||
[Attributes]
|
||||
Username = 'username'
|
||||
Username = 'sub'
|
||||
Nickname = 'nickname'
|
||||
Phone = 'phone_number'
|
||||
Email = 'email'
|
||||
@@ -66,34 +81,41 @@ Email = 'email'
|
||||
|
||||
const CAS = `
|
||||
Enable = false
|
||||
SsoAddr = 'https://cas.example.com/cas/'
|
||||
RedirectURL = 'http://127.0.0.1:18000/callback/cas'
|
||||
DisplayName = 'CAS登录'
|
||||
CoverAttributes = false
|
||||
RedirectURL = 'http://n9e.com/callback/cas'
|
||||
SsoAddr = 'https://cas.example.com/cas/'
|
||||
SsoLogoutAddr = 'https://cas.example.com/cas/session/end'
|
||||
# LoginPath = ''
|
||||
CoverAttributes = true
|
||||
DefaultRoles = ['Standard']
|
||||
|
||||
[Attributes]
|
||||
Username = 'sub'
|
||||
Nickname = 'nickname'
|
||||
Phone = 'phone_number'
|
||||
Email = 'email'
|
||||
`
|
||||
|
||||
const OIDC = `
|
||||
Enable = false
|
||||
DisplayName = 'OIDC登录'
|
||||
RedirectURL = 'http://n9e.com/callback'
|
||||
SsoAddr = 'http://sso.example.org'
|
||||
SsoLogoutAddr = 'http://sso.example.org/session/end'
|
||||
ClientId = ''
|
||||
ClientSecret = ''
|
||||
CoverAttributes = true
|
||||
DefaultRoles = ['Standard']
|
||||
Scopes = ['openid', 'profile', 'email', 'phone']
|
||||
|
||||
[Attributes]
|
||||
Username = 'sub'
|
||||
Nickname = 'nickname'
|
||||
Phone = 'phone_number'
|
||||
Email = 'email'
|
||||
`
|
||||
|
||||
func Init(center cconf.Center, ctx *ctx.Context) *SsoClient {
|
||||
func Init(center cconf.Center, ctx *ctx.Context, configCache *memsto.ConfigCache) *SsoClient {
|
||||
ssoClient := new(SsoClient)
|
||||
m := make(map[string]string)
|
||||
m["LDAP"] = LDAP
|
||||
@@ -122,6 +144,11 @@ func Init(center cconf.Center, ctx *ctx.Context) *SsoClient {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
}
|
||||
if configCache == nil {
|
||||
logger.Error("configCache is nil, sso initialization failed")
|
||||
}
|
||||
ssoClient.configCache = configCache
|
||||
userVariableMap := configCache.Get()
|
||||
|
||||
configs, err := models.SsoConfigGets(ctx)
|
||||
if err != nil {
|
||||
@@ -129,6 +156,7 @@ func Init(center cconf.Center, ctx *ctx.Context) *SsoClient {
|
||||
}
|
||||
|
||||
for _, cfg := range configs {
|
||||
cfg.Content = tplx.ReplaceTemplateUseText(cfg.Name, cfg.Content, userVariableMap)
|
||||
switch cfg.Name {
|
||||
case "LDAP":
|
||||
var config ldapx.Config
|
||||
@@ -143,6 +171,7 @@ func Init(center cconf.Center, ctx *ctx.Context) *SsoClient {
|
||||
if err != nil {
|
||||
log.Fatalln("init oidc failed:", err)
|
||||
}
|
||||
logger.Info("init oidc..")
|
||||
oidcClient, err := oidcx.New(config)
|
||||
if err != nil {
|
||||
logger.Error("init oidc failed:", err)
|
||||
@@ -165,5 +194,84 @@ func Init(center cconf.Center, ctx *ctx.Context) *SsoClient {
|
||||
ssoClient.OAuth2 = oauth2x.New(config)
|
||||
}
|
||||
}
|
||||
|
||||
go ssoClient.SyncSsoUsers(ctx)
|
||||
go ssoClient.Reload(ctx)
|
||||
return ssoClient
|
||||
}
|
||||
|
||||
// 定期更新sso配置
|
||||
func (s *SsoClient) reload(ctx *ctx.Context) error {
|
||||
lastUpdateTime, err := models.SsoConfigLastUpdateTime(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
lastCacheUpdateTime := s.configCache.GetLastUpdateTime()
|
||||
if lastUpdateTime == s.LastUpdateTime && lastCacheUpdateTime == s.configLastUpdateTime {
|
||||
return nil
|
||||
}
|
||||
|
||||
configs, err := models.SsoConfigGets(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
userVariableMap := s.configCache.Get()
|
||||
for _, cfg := range configs {
|
||||
cfg.Content = tplx.ReplaceTemplateUseText(cfg.Name, cfg.Content, userVariableMap)
|
||||
switch cfg.Name {
|
||||
case "LDAP":
|
||||
var config ldapx.Config
|
||||
err := toml.Unmarshal([]byte(cfg.Content), &config)
|
||||
if err != nil {
|
||||
logger.Warning("reload ldap failed", err)
|
||||
continue
|
||||
}
|
||||
s.LDAP.Reload(config)
|
||||
case "OIDC":
|
||||
var config oidcx.Config
|
||||
err := toml.Unmarshal([]byte(cfg.Content), &config)
|
||||
if err != nil {
|
||||
logger.Warning("reload oidc failed:", err)
|
||||
continue
|
||||
}
|
||||
|
||||
logger.Info("reload oidc..")
|
||||
err = s.OIDC.Reload(config)
|
||||
if err != nil {
|
||||
logger.Error("reload oidc failed:", err)
|
||||
continue
|
||||
}
|
||||
case "CAS":
|
||||
var config cas.Config
|
||||
err := toml.Unmarshal([]byte(cfg.Content), &config)
|
||||
if err != nil {
|
||||
logger.Warning("reload cas failed:", err)
|
||||
continue
|
||||
}
|
||||
s.CAS.Reload(config)
|
||||
case "OAuth2":
|
||||
var config oauth2x.Config
|
||||
err := toml.Unmarshal([]byte(cfg.Content), &config)
|
||||
if err != nil {
|
||||
logger.Warning("reload oauth2 failed:", err)
|
||||
continue
|
||||
}
|
||||
s.OAuth2.Reload(config)
|
||||
}
|
||||
}
|
||||
|
||||
s.LastUpdateTime = lastUpdateTime
|
||||
s.configLastUpdateTime = lastCacheUpdateTime
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *SsoClient) Reload(ctx *ctx.Context) {
|
||||
duration := time.Duration(9000) * time.Millisecond
|
||||
for {
|
||||
time.Sleep(duration)
|
||||
if err := s.reload(ctx); err != nil {
|
||||
logger.Warning("reload sso client err:", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
37
center/sso/sync.go
Normal file
37
center/sso/sync.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package sso
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/ccfos/nightingale/v6/pkg/ctx"
|
||||
"github.com/toolkits/pkg/logger"
|
||||
)
|
||||
|
||||
func (s *SsoClient) SyncSsoUsers(ctx *ctx.Context) {
|
||||
if err := s.LDAP.SyncAddAndDelUsers(ctx); err != nil {
|
||||
fmt.Println("failed to sync the addition and deletion of ldap users:", err)
|
||||
}
|
||||
|
||||
if err := s.LDAP.SyncDelUsers(ctx); err != nil {
|
||||
fmt.Println("failed to sync deletion of ldap users:", err)
|
||||
}
|
||||
|
||||
go s.loopSyncSsoUsers(ctx)
|
||||
}
|
||||
|
||||
func (s *SsoClient) loopSyncSsoUsers(ctx *ctx.Context) {
|
||||
for {
|
||||
select {
|
||||
case <-s.LDAP.Ticker.C:
|
||||
lc := s.LDAP.Copy()
|
||||
|
||||
if err := lc.SyncAddAndDelUsers(ctx); err != nil {
|
||||
logger.Warningf("failed to sync the addition and deletion of ldap users: %v", err)
|
||||
}
|
||||
|
||||
if err := lc.SyncDelUsers(ctx); err != nil {
|
||||
logger.Warningf("failed to sync deletion of ldap users: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,9 +18,9 @@ func Upgrade(configFile string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx := ctx.NewContext(context.Background(), db, false)
|
||||
ctx := ctx.NewContext(context.Background(), db, true)
|
||||
for _, cluster := range config.Clusters {
|
||||
count, err := models.GetDatasourcesCountBy(ctx, "", "", cluster.Name)
|
||||
count, err := models.GetDatasourcesCountByName(ctx, cluster.Name)
|
||||
if err != nil {
|
||||
logger.Errorf("get datasource %s count error: %v", cluster.Name, err)
|
||||
continue
|
||||
|
||||
@@ -8,6 +8,13 @@ insert into `role_operation`(role_name, operation) values('Standard', '/trace/ex
|
||||
insert into `role_operation`(role_name, operation) values('Standard', '/alert-rules-built-in');
|
||||
insert into `role_operation`(role_name, operation) values('Standard', '/dashboards-built-in');
|
||||
insert into `role_operation`(role_name, operation) values('Standard', '/trace/dependencies');
|
||||
insert into `role_operation`(role_name, operation) values('Standard', '/help/servers');
|
||||
insert into `role_operation`(role_name, operation) values('Standard', '/help/migrate');
|
||||
|
||||
insert into `role_operation`(role_name, operation) values('Admin', '/help/source');
|
||||
insert into `role_operation`(role_name, operation) values('Admin', '/help/sso');
|
||||
insert into `role_operation`(role_name, operation) values('Admin', '/help/notification-tpls');
|
||||
insert into `role_operation`(role_name, operation) values('Admin', '/help/notification-settings');
|
||||
|
||||
alter table `board` add built_in tinyint(1) not null default 0 comment '0:false 1:true';
|
||||
alter table `board` add hide tinyint(1) not null default 0 comment '0:false 1:true';
|
||||
|
||||
@@ -17,7 +17,7 @@ import (
|
||||
|
||||
var (
|
||||
showVersion = flag.Bool("version", false, "Show version.")
|
||||
configDir = flag.String("configs", osx.GetEnv("N9E_CONFIGS", "etc"), "Specify configuration directory.(env:N9E_CONFIGS)")
|
||||
configDir = flag.String("configs", osx.GetEnv("N9E_ALERT_CONFIGS", "etc"), "Specify configuration directory.(env:N9E_ALERT_CONFIGS)")
|
||||
cryptoKey = flag.String("crypto-key", "", "Specify the secret key for configuration file field encryption.")
|
||||
)
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"github.com/ccfos/nightingale/v6/pkg/osx"
|
||||
"github.com/ccfos/nightingale/v6/pkg/version"
|
||||
|
||||
"github.com/toolkits/pkg/net/tcpx"
|
||||
"github.com/toolkits/pkg/runner"
|
||||
)
|
||||
|
||||
@@ -31,6 +32,8 @@ func main() {
|
||||
|
||||
printEnv()
|
||||
|
||||
tcpx.WaitHosts()
|
||||
|
||||
cleanFunc, err := center.Initialize(*configDir, *cryptoKey)
|
||||
if err != nil {
|
||||
log.Fatalln("failed to initialize:", err)
|
||||
|
||||
96
cmd/edge/edge.go
Normal file
96
cmd/edge/edge.go
Normal file
@@ -0,0 +1,96 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/ccfos/nightingale/v6/alert"
|
||||
"github.com/ccfos/nightingale/v6/alert/astats"
|
||||
"github.com/ccfos/nightingale/v6/alert/process"
|
||||
alertrt "github.com/ccfos/nightingale/v6/alert/router"
|
||||
"github.com/ccfos/nightingale/v6/center/metas"
|
||||
"github.com/ccfos/nightingale/v6/conf"
|
||||
"github.com/ccfos/nightingale/v6/dumper"
|
||||
"github.com/ccfos/nightingale/v6/memsto"
|
||||
"github.com/ccfos/nightingale/v6/pkg/ctx"
|
||||
"github.com/ccfos/nightingale/v6/pkg/httpx"
|
||||
"github.com/ccfos/nightingale/v6/pkg/logx"
|
||||
"github.com/ccfos/nightingale/v6/prom"
|
||||
"github.com/ccfos/nightingale/v6/pushgw/idents"
|
||||
pushgwrt "github.com/ccfos/nightingale/v6/pushgw/router"
|
||||
"github.com/ccfos/nightingale/v6/pushgw/writer"
|
||||
"github.com/ccfos/nightingale/v6/storage"
|
||||
"github.com/ccfos/nightingale/v6/tdengine"
|
||||
|
||||
"github.com/flashcatcloud/ibex/src/cmd/ibex"
|
||||
)
|
||||
|
||||
func Initialize(configDir string, cryptoKey string) (func(), error) {
|
||||
config, err := conf.InitConfig(configDir, cryptoKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to init config: %v", err)
|
||||
}
|
||||
|
||||
logxClean, err := logx.Init(config.Log)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
//check CenterApi is default value
|
||||
if len(config.CenterApi.Addrs) < 1 {
|
||||
return nil, errors.New("failed to init config: the CenterApi configuration is missing")
|
||||
}
|
||||
ctx := ctx.NewContext(context.Background(), nil, false, config.CenterApi)
|
||||
|
||||
var redis storage.Redis
|
||||
redis, err = storage.NewRedis(config.Redis)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
syncStats := memsto.NewSyncStats()
|
||||
|
||||
targetCache := memsto.NewTargetCache(ctx, syncStats, redis)
|
||||
busiGroupCache := memsto.NewBusiGroupCache(ctx, syncStats)
|
||||
idents := idents.New(ctx, redis)
|
||||
metas := metas.New(redis)
|
||||
writers := writer.NewWriters(config.Pushgw)
|
||||
pushgwRouter := pushgwrt.New(config.HTTP, config.Pushgw, config.Alert, targetCache, busiGroupCache, idents, metas, writers, ctx)
|
||||
r := httpx.GinEngine(config.Global.RunMode, config.HTTP)
|
||||
pushgwRouter.Config(r)
|
||||
|
||||
if !config.Alert.Disable {
|
||||
configCache := memsto.NewConfigCache(ctx, syncStats, nil, "")
|
||||
alertStats := astats.NewSyncStats()
|
||||
dsCache := memsto.NewDatasourceCache(ctx, syncStats)
|
||||
alertMuteCache := memsto.NewAlertMuteCache(ctx, syncStats)
|
||||
alertRuleCache := memsto.NewAlertRuleCache(ctx, syncStats)
|
||||
notifyConfigCache := memsto.NewNotifyConfigCache(ctx, configCache)
|
||||
userCache := memsto.NewUserCache(ctx, syncStats)
|
||||
userGroupCache := memsto.NewUserGroupCache(ctx, syncStats)
|
||||
taskTplsCache := memsto.NewTaskTplCache(ctx)
|
||||
|
||||
promClients := prom.NewPromClient(ctx)
|
||||
tdengineClients := tdengine.NewTdengineClient(ctx, config.Alert.Heartbeat)
|
||||
externalProcessors := process.NewExternalProcessors()
|
||||
|
||||
alert.Start(config.Alert, config.Pushgw, syncStats, alertStats, externalProcessors, targetCache, busiGroupCache, alertMuteCache,
|
||||
alertRuleCache, notifyConfigCache, taskTplsCache, dsCache, ctx, promClients, tdengineClients, userCache, userGroupCache)
|
||||
|
||||
alertrtRouter := alertrt.New(config.HTTP, config.Alert, alertMuteCache, targetCache, busiGroupCache, alertStats, ctx, externalProcessors)
|
||||
|
||||
alertrtRouter.Config(r)
|
||||
|
||||
if config.Ibex.Enable {
|
||||
ibex.ServerStart(false, nil, redis, config.HTTP.APIForService.BasicAuth, config.Alert.Heartbeat, &config.CenterApi, r, nil, config.Ibex, config.HTTP.Port)
|
||||
}
|
||||
}
|
||||
|
||||
dumper.ConfigRouter(r)
|
||||
httpClean := httpx.Init(config.HTTP, r)
|
||||
|
||||
return func() {
|
||||
logxClean()
|
||||
httpClean()
|
||||
}, nil
|
||||
}
|
||||
68
cmd/edge/main.go
Normal file
68
cmd/edge/main.go
Normal file
@@ -0,0 +1,68 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
"github.com/ccfos/nightingale/v6/pkg/osx"
|
||||
"github.com/ccfos/nightingale/v6/pkg/version"
|
||||
|
||||
"github.com/toolkits/pkg/runner"
|
||||
)
|
||||
|
||||
var (
|
||||
showVersion = flag.Bool("version", false, "Show version.")
|
||||
configDir = flag.String("configs", osx.GetEnv("N9E_EDGE_CONFIGS", "etc"), "Specify configuration directory.(env:N9E_EDGE_CONFIGS)")
|
||||
cryptoKey = flag.String("crypto-key", "", "Specify the secret key for configuration file field encryption.")
|
||||
)
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
|
||||
if *showVersion {
|
||||
fmt.Println(version.Version)
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
printEnv()
|
||||
|
||||
cleanFunc, err := Initialize(*configDir, *cryptoKey)
|
||||
if err != nil {
|
||||
log.Fatalln("failed to initialize:", err)
|
||||
}
|
||||
|
||||
code := 1
|
||||
sc := make(chan os.Signal, 1)
|
||||
signal.Notify(sc, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)
|
||||
|
||||
EXIT:
|
||||
for {
|
||||
sig := <-sc
|
||||
fmt.Println("received signal:", sig.String())
|
||||
switch sig {
|
||||
case syscall.SIGQUIT, syscall.SIGTERM, syscall.SIGINT:
|
||||
code = 0
|
||||
break EXIT
|
||||
case syscall.SIGHUP:
|
||||
// reload configuration?
|
||||
default:
|
||||
break EXIT
|
||||
}
|
||||
}
|
||||
|
||||
cleanFunc()
|
||||
fmt.Println("process exited")
|
||||
os.Exit(code)
|
||||
}
|
||||
|
||||
func printEnv() {
|
||||
runner.Init()
|
||||
fmt.Println("runner.cwd:", runner.Cwd)
|
||||
fmt.Println("runner.hostname:", runner.Hostname)
|
||||
fmt.Println("runner.fd_limits:", runner.FdLimits())
|
||||
fmt.Println("runner.vm_limits:", runner.VMLimits())
|
||||
}
|
||||
@@ -17,7 +17,7 @@ import (
|
||||
|
||||
var (
|
||||
showVersion = flag.Bool("version", false, "Show version.")
|
||||
configDir = flag.String("configs", osx.GetEnv("N9E_CONFIGS", "etc"), "Specify configuration directory.(env:N9E_CONFIGS)")
|
||||
configDir = flag.String("configs", osx.GetEnv("N9E_PUSHGW_CONFIGS", "etc"), "Specify configuration directory.(env:N9E_PUSHGW_CONFIGS)")
|
||||
cryptoKey = flag.String("crypto-key", "", "Specify the secret key for configuration file field encryption.")
|
||||
)
|
||||
|
||||
|
||||
22
conf/conf.go
22
conf/conf.go
@@ -14,8 +14,6 @@ import (
|
||||
"github.com/ccfos/nightingale/v6/pkg/ormx"
|
||||
"github.com/ccfos/nightingale/v6/pushgw/pconf"
|
||||
"github.com/ccfos/nightingale/v6/storage"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type ConfigType struct {
|
||||
@@ -29,17 +27,31 @@ type ConfigType struct {
|
||||
Pushgw pconf.Pushgw
|
||||
Alert aconf.Alert
|
||||
Center cconf.Center
|
||||
Ibex Ibex
|
||||
}
|
||||
|
||||
type CenterApi struct {
|
||||
Addrs []string
|
||||
BasicAuth gin.Accounts
|
||||
Addrs []string
|
||||
BasicAuthUser string
|
||||
BasicAuthPass string
|
||||
Timeout int64
|
||||
}
|
||||
|
||||
type GlobalConfig struct {
|
||||
RunMode string
|
||||
}
|
||||
|
||||
type Ibex struct {
|
||||
Enable bool
|
||||
RPCListen string
|
||||
Output Output
|
||||
}
|
||||
|
||||
type Output struct {
|
||||
ComeFrom string
|
||||
AgtdPort int
|
||||
}
|
||||
|
||||
func InitConfig(configDir, cryptoKey string) (*ConfigType, error) {
|
||||
var config = new(ConfigType)
|
||||
|
||||
@@ -48,7 +60,7 @@ func InitConfig(configDir, cryptoKey string) (*ConfigType, error) {
|
||||
}
|
||||
|
||||
config.Pushgw.PreCheck()
|
||||
config.Alert.PreCheck()
|
||||
config.Alert.PreCheck(configDir)
|
||||
config.Center.PreCheck()
|
||||
|
||||
err := decryptConfig(config, cryptoKey)
|
||||
|
||||
@@ -14,39 +14,22 @@ func decryptConfig(config *ConfigType, cryptoKey string) error {
|
||||
|
||||
config.DB.DSN = decryptDsn
|
||||
|
||||
for k := range config.HTTP.Alert.BasicAuth {
|
||||
decryptPwd, err := secu.DealWithDecrypt(config.HTTP.Alert.BasicAuth[k], cryptoKey)
|
||||
for k := range config.HTTP.APIForService.BasicAuth {
|
||||
decryptPwd, err := secu.DealWithDecrypt(config.HTTP.APIForService.BasicAuth[k], cryptoKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to decrypt http basic auth password: %s", err)
|
||||
}
|
||||
|
||||
config.HTTP.Alert.BasicAuth[k] = decryptPwd
|
||||
config.HTTP.APIForService.BasicAuth[k] = decryptPwd
|
||||
}
|
||||
|
||||
for k := range config.HTTP.Pushgw.BasicAuth {
|
||||
decryptPwd, err := secu.DealWithDecrypt(config.HTTP.Pushgw.BasicAuth[k], cryptoKey)
|
||||
for k := range config.HTTP.APIForAgent.BasicAuth {
|
||||
decryptPwd, err := secu.DealWithDecrypt(config.HTTP.APIForAgent.BasicAuth[k], cryptoKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to decrypt http basic auth password: %s", err)
|
||||
}
|
||||
|
||||
config.HTTP.Pushgw.BasicAuth[k] = decryptPwd
|
||||
}
|
||||
|
||||
for k := range config.HTTP.Heartbeat.BasicAuth {
|
||||
decryptPwd, err := secu.DealWithDecrypt(config.HTTP.Heartbeat.BasicAuth[k], cryptoKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to decrypt http basic auth password: %s", err)
|
||||
}
|
||||
|
||||
config.HTTP.Heartbeat.BasicAuth[k] = decryptPwd
|
||||
}
|
||||
|
||||
for k := range config.HTTP.Service.BasicAuth {
|
||||
decryptPwd, err := secu.DealWithDecrypt(config.HTTP.Service.BasicAuth[k], cryptoKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to decrypt http basic auth password: %s", err)
|
||||
}
|
||||
config.HTTP.Service.BasicAuth[k] = decryptPwd
|
||||
config.HTTP.APIForAgent.BasicAuth[k] = decryptPwd
|
||||
}
|
||||
|
||||
for i, v := range config.Pushgw.Writers {
|
||||
|
||||
@@ -77,4 +77,3 @@ Committer 记录并公示于 **[COMMITTERS](https://github.com/ccfos/nightingale
|
||||
2. 提问之前请先搜索 [Github Issues](https://github.com/ccfos/nightingale/issues "Github Issue");
|
||||
3. 我们优先推荐通过提交 [Github Issue](https://github.com/ccfos/nightingale/issues "Github Issue") 来提问,如果[有问题点击这里](https://github.com/ccfos/nightingale/issues/new?assignees=&labels=kind%2Fbug&template=bug_report.yml "有问题点击这里") | [有需求建议点击这里](https://github.com/ccfos/nightingale/issues/new?assignees=&labels=kind%2Ffeature&template=enhancement.md "有需求建议点击这里");
|
||||
|
||||
最后,我们推荐你加入微信群,针对相关开放式问题,相互交流咨询 (请先加好友:[UlricGO](https://www.gitlink.org.cn/UlricQin/gist/tree/master/self.jpeg "UlricGO") 备注:夜莺加群+姓名+公司,交流群里会有开发者团队和专业、热心的群友回答问题)。
|
||||
|
||||
BIN
doc/img/Nightingale_L_V.png
Normal file
BIN
doc/img/Nightingale_L_V.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 230 KiB |
BIN
doc/img/n9e-arch-latest.png
Normal file
BIN
doc/img/n9e-arch-latest.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 215 KiB |
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user