mirror of
https://github.com/ccfos/nightingale.git
synced 2026-03-02 22:19:10 +00:00
Compare commits
1747 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
f1a5c2065c | ||
|
|
6b9ceda9c1 | ||
|
|
7390d42e62 | ||
|
|
a35f879dc0 | ||
|
|
3fd4ea4853 | ||
|
|
20f0a9d16d | ||
|
|
5d4151983a | ||
|
|
83b5f12474 | ||
|
|
8c7bfb4f4a | ||
|
|
4ccf887920 | ||
|
|
546d9cb2cc | ||
|
|
391b42a399 | ||
|
|
a916a0fc6b | ||
|
|
da9f5fbb12 | ||
|
|
ad3cf58bf3 | ||
|
|
a77dc15e36 | ||
|
|
9ad51aeeff | ||
|
|
2c7f030ea5 | ||
|
|
039be7fc6c | ||
|
|
9bff2509a8 | ||
|
|
35b3cbb697 | ||
|
|
d81275b9c8 | ||
|
|
e29dd58823 | ||
|
|
b64aa03ccf | ||
|
|
3893cb00a5 | ||
|
|
4b6985c8af | ||
|
|
7cc9470823 | ||
|
|
b97dfce0ad | ||
|
|
357d3dff78 | ||
|
|
d0604f0c97 | ||
|
|
8fafa0075b | ||
|
|
caa23fbba1 | ||
|
|
4b9fea3cb2 | ||
|
|
f61a04f43f | ||
|
|
ef3588ff46 | ||
|
|
3e3210bb81 | ||
|
|
da7ef5a92e | ||
|
|
82b91164fe | ||
|
|
033d45309f | ||
|
|
60e9fb21f1 | ||
|
|
508006ad01 | ||
|
|
97d7b0574a | ||
|
|
c44aebd404 | ||
|
|
2afa921a5d | ||
|
|
313c820f1f | ||
|
|
02f0b4579b | ||
|
|
36eb308ef6 | ||
|
|
cd2db571cf | ||
|
|
a0cf12b171 | ||
|
|
8358ab4b81 | ||
|
|
0fc6cb8ef2 | ||
|
|
e1ab013c45 | ||
|
|
d984ad8bf4 | ||
|
|
86fe3c7c43 | ||
|
|
0f4478318e | ||
|
|
c0d0eb0e69 | ||
|
|
b62762b2e6 | ||
|
|
810ca0e469 | ||
|
|
33e3b224b9 | ||
|
|
24d7b2b1bf | ||
|
|
1d5ff1b28d | ||
|
|
ed5c8c5758 | ||
|
|
01f7860900 | ||
|
|
a6bb03c8ba | ||
|
|
e9150b2ae0 | ||
|
|
30d1ebd808 | ||
|
|
2f69d92055 | ||
|
|
deeb40b4a0 | ||
|
|
37f68fd52b | ||
|
|
73828e50b5 | ||
|
|
7e73850117 | ||
|
|
3a075e7681 | ||
|
|
4ec5612d78 | ||
|
|
817ed0ab1b | ||
|
|
63aa615761 | ||
|
|
2a36902760 | ||
|
|
bca9331182 | ||
|
|
199a23e385 | ||
|
|
c733f16cc7 | ||
|
|
81585649aa | ||
|
|
2c4422d657 | ||
|
|
aaf66cb386 | ||
|
|
cfed4d8318 | ||
|
|
606cd538ec | ||
|
|
bafb3b2546 | ||
|
|
9a0224697f | ||
|
|
23156552db | ||
|
|
36bca795fa | ||
|
|
b5503ae93e | ||
|
|
3c102e47ed | ||
|
|
60bf8139b1 | ||
|
|
fc0d077c9f | ||
|
|
3a610f7ea0 | ||
|
|
f8990ee85e | ||
|
|
88040bf277 | ||
|
|
1e15dc1f30 | ||
|
|
9880b466db | ||
|
|
b7780ebbdb | ||
|
|
1fa524b710 | ||
|
|
aa2c0cffce | ||
|
|
ed1c89fb7e | ||
|
|
988327dead | ||
|
|
5db168224e | ||
|
|
7622eba87f | ||
|
|
1cb58fedf7 | ||
|
|
7dcaec0a7b | ||
|
|
4f315cb6d5 | ||
|
|
9a2d898214 | ||
|
|
530561c038 | ||
|
|
fc68d2d598 | ||
|
|
1b40c38a7a | ||
|
|
d39d4cb91d | ||
|
|
e415538ffd | ||
|
|
05c767a803 | ||
|
|
923cff1c19 | ||
|
|
ef18d2a95f | ||
|
|
3abc4d0bfd | ||
|
|
a3ec69fe4a | ||
|
|
403466f872 | ||
|
|
81abd2f02a | ||
|
|
263c77cbbf | ||
|
|
ef42a78e59 | ||
|
|
4c7746b3b4 | ||
|
|
b142a5726e | ||
|
|
cc68b75489 | ||
|
|
1ce79e29d5 | ||
|
|
ee167ce0ba | ||
|
|
544cd02ef1 | ||
|
|
34ad6bc220 | ||
|
|
c7c694e70b | ||
|
|
dc26bb78d8 | ||
|
|
a0c635b830 | ||
|
|
0e95c29b7d | ||
|
|
cab9fed700 | ||
|
|
4ad47fb8f4 | ||
|
|
50345cb823 | ||
|
|
95bb67e66d | ||
|
|
90fbd9f16a | ||
|
|
5c8411eba1 | ||
|
|
03edb84d09 | ||
|
|
958a8c3ed1 | ||
|
|
a2a0b41909 | ||
|
|
64e1085766 | ||
|
|
5c97986908 | ||
|
|
66e291e3c3 | ||
|
|
365fcd5dd7 | ||
|
|
63690ba084 | ||
|
|
bc6616ce7c | ||
|
|
b96ff22a21 | ||
|
|
bfec911e9c | ||
|
|
76a94db7c1 | ||
|
|
eef67c956f | ||
|
|
2a405c85e0 | ||
|
|
a2bdeb4f0e | ||
|
|
5a880f002e | ||
|
|
e4733e9a04 | ||
|
|
a9595aea18 | ||
|
|
101390b4ae | ||
|
|
39e80ea786 | ||
|
|
f118cadaea | ||
|
|
bad49d2773 | ||
|
|
a897ae6db8 | ||
|
|
aac135c498 | ||
|
|
e7621ae200 | ||
|
|
c3702cde43 | ||
|
|
578ce375b5 | ||
|
|
a00be34e8e | ||
|
|
02d02463f7 | ||
|
|
96a1d4e903 | ||
|
|
e2b57396e3 | ||
|
|
381654dec5 | ||
|
|
82ac0fa625 | ||
|
|
e4d65808bf | ||
|
|
34965d818b | ||
|
|
d4eadef378 | ||
|
|
300405dc50 | ||
|
|
49bb5e1ee3 | ||
|
|
45659ee98f | ||
|
|
82b98967d8 | ||
|
|
6336d6de66 | ||
|
|
e8fd80b6d5 | ||
|
|
dca4e4c83b | ||
|
|
6514891b3a | ||
|
|
3383ca12fa | ||
|
|
86b5c9668b | ||
|
|
540ef0244d | ||
|
|
0b25f77e61 | ||
|
|
2206e8d2c1 | ||
|
|
644df733d3 | ||
|
|
2f9d7843d8 | ||
|
|
9d1486b058 | ||
|
|
d52b675516 | ||
|
|
cae75d3930 | ||
|
|
8f6d256300 | ||
|
|
e74d6a3ee5 | ||
|
|
7ecc9a4614 | ||
|
|
0c7f97c826 | ||
|
|
b47d4f5385 | ||
|
|
91a38ffc5f | ||
|
|
4e4c0f5d82 | ||
|
|
88d0b277ca | ||
|
|
e3b0ed1fca | ||
|
|
a29b5b90d2 | ||
|
|
992d5cdebd | ||
|
|
848900a2bf | ||
|
|
814af8085d | ||
|
|
4715c8e073 | ||
|
|
d442e37051 | ||
|
|
e2226f3f34 | ||
|
|
7d8a4af2ec | ||
|
|
5208138a40 | ||
|
|
fce91ffedb | ||
|
|
2310b3d1e5 | ||
|
|
462e9dd696 | ||
|
|
4f6a0bf56b | ||
|
|
bc708b4e11 | ||
|
|
2b1244616a | ||
|
|
274da279f5 | ||
|
|
2b7ab746f5 | ||
|
|
10427f5a47 | ||
|
|
c366a641a4 | ||
|
|
e044954798 | ||
|
|
b51b93c846 | ||
|
|
717941a9bc | ||
|
|
1180f1fcfd | ||
|
|
b94b494f6d | ||
|
|
480dde89af | ||
|
|
6c587ea4ef | ||
|
|
82a6786457 | ||
|
|
70d41f0c77 | ||
|
|
21a0e755b2 | ||
|
|
1aed12d93d | ||
|
|
f07964c9c9 | ||
|
|
5156ec13b1 | ||
|
|
550a12a3f7 | ||
|
|
1426ccce53 | ||
|
|
ef1fe403ba | ||
|
|
eb9ad34748 | ||
|
|
615d909e5d | ||
|
|
a51eaabe85 | ||
|
|
fba99a1001 | ||
|
|
e910c1fb22 | ||
|
|
21cad3e56c | ||
|
|
69ca0e87e9 | ||
|
|
178de1fe73 | ||
|
|
87899cbedb | ||
|
|
d4257d11f2 | ||
|
|
00b9c31f29 | ||
|
|
1c8c6b92a9 | ||
|
|
9c9fe800e4 | ||
|
|
9aeeaa191e | ||
|
|
e69112958b | ||
|
|
6d8317927e | ||
|
|
072f1bd51f | ||
|
|
25dbc62ff4 | ||
|
|
b233067789 | ||
|
|
d531178c9b | ||
|
|
174df1495c | ||
|
|
ffe423148d | ||
|
|
926559c9a7 | ||
|
|
136642f126 | ||
|
|
a054828fcc | ||
|
|
e46e946689 | ||
|
|
cf083c543b | ||
|
|
2e1508fdd3 | ||
|
|
954543a5b2 | ||
|
|
71a402c33c | ||
|
|
e30a5a316f | ||
|
|
0c9b7de391 | ||
|
|
063b6f63df | ||
|
|
44b780093a | ||
|
|
780ad19dd9 | ||
|
|
c6d133772a | ||
|
|
c5bb8a4a13 | ||
|
|
06c1664577 | ||
|
|
96a4c1ebfa | ||
|
|
b0c05368f7 | ||
|
|
eebf2cff49 | ||
|
|
30d021bc19 | ||
|
|
b4ea395fe3 | ||
|
|
9f4d1a1ea7 | ||
|
|
ed06da90d9 | ||
|
|
9461b549d2 | ||
|
|
3b1b595461 | ||
|
|
4257de69fd | ||
|
|
ddc86f20ee | ||
|
|
bf27162a9b | ||
|
|
f8ac0a9b4a | ||
|
|
7a190b152c | ||
|
|
99fbdae121 | ||
|
|
aa26ddfb48 | ||
|
|
ba5aba9cdf | ||
|
|
3400803672 | ||
|
|
f11377b289 | ||
|
|
1165312532 | ||
|
|
8a145d5ba2 | ||
|
|
352415662a | ||
|
|
65d8f80637 | ||
|
|
b3700c7251 | ||
|
|
106a8e490a | ||
|
|
5332f797a6 | ||
|
|
aff0dbfea1 | ||
|
|
da5dd683d6 | ||
|
|
15892d6e57 | ||
|
|
fbff60eefb | ||
|
|
62867ddbf2 | ||
|
|
5d4acb6cc3 | ||
|
|
b893483d26 | ||
|
|
4130a5df02 | ||
|
|
445d03e096 | ||
|
|
577c402a5b | ||
|
|
40bbbfd475 | ||
|
|
0d05ad85f2 | ||
|
|
e70622d18c | ||
|
|
562f98ddaf | ||
|
|
ee07969c8a | ||
|
|
5b0e24cd40 | ||
|
|
78b2e54910 | ||
|
|
2e64c83632 | ||
|
|
537d5d2386 | ||
|
|
86899b8c48 | ||
|
|
fcc45ebf2a | ||
|
|
95727e9c00 | ||
|
|
3a3ad5d9d9 | ||
|
|
7209da192f | ||
|
|
98f3508424 | ||
|
|
c33900ee1b | ||
|
|
a2490104b9 | ||
|
|
1a25c3804e | ||
|
|
23eb766c14 | ||
|
|
a7bad003f5 | ||
|
|
e4e48cfda0 | ||
|
|
d0ce4c25e5 | ||
|
|
01aea821b9 | ||
|
|
bdc1c1c60b | ||
|
|
09f37b8076 | ||
|
|
fc4c4b96bf | ||
|
|
5c60c2c85e | ||
|
|
1e9bd900e9 | ||
|
|
1ca000af2c | ||
|
|
81fade557b | ||
|
|
b82f646636 | ||
|
|
26a3d2dafa | ||
|
|
5e931ebe8e | ||
|
|
8c45479c02 | ||
|
|
940313bd4e | ||
|
|
5057cd0ae6 | ||
|
|
a4be2c73ac | ||
|
|
a38e50d6b8 | ||
|
|
89f66dd5d1 | ||
|
|
3963470603 | ||
|
|
640b6e6825 | ||
|
|
e7d2c45f9d | ||
|
|
80ee54898a | ||
|
|
fe68cebbf9 | ||
|
|
c1fec215a9 | ||
|
|
388228a631 | ||
|
|
b4ddd03691 | ||
|
|
b92e4abf86 | ||
|
|
a1c458b764 | ||
|
|
acb4b8e33e | ||
|
|
54eab51e54 | ||
|
|
be89fde030 | ||
|
|
37711ea6b2 | ||
|
|
3b5c8d8357 | ||
|
|
635369e3fd | ||
|
|
6c2c945bd9 | ||
|
|
48d24c79d6 | ||
|
|
c6a1761a7b | ||
|
|
23d7e5a7de | ||
|
|
b1b2c7d6b0 | ||
|
|
f34c3c6a2c | ||
|
|
454dc7f983 | ||
|
|
c1e92b56b9 | ||
|
|
fd93fd7182 | ||
|
|
1a446f0749 | ||
|
|
f18ed76593 | ||
|
|
9b3a9f29d9 | ||
|
|
49965fd5d5 | ||
|
|
a248e054fa | ||
|
|
bbb35d36be | ||
|
|
fd3e51cbb1 | ||
|
|
bd0480216c | ||
|
|
2c963258cf | ||
|
|
b4f267fb01 | ||
|
|
ea46401db2 | ||
|
|
58e777eb00 | ||
|
|
04a9161f75 | ||
|
|
1ed8f38833 | ||
|
|
bb17751a81 | ||
|
|
a8dcb1fe83 | ||
|
|
1ea30e03a4 | ||
|
|
ba0eafa065 | ||
|
|
c78c8d07f2 | ||
|
|
8fe9e57c03 | ||
|
|
64646d2ace | ||
|
|
e747e73145 | ||
|
|
896f85efdf | ||
|
|
77e4499a32 | ||
|
|
7c351e09e5 | ||
|
|
14ad3b1b0a | ||
|
|
184867d07c | ||
|
|
3476b95b35 | ||
|
|
76e105c93a | ||
|
|
39705787c9 | ||
|
|
293680a9cd | ||
|
|
05005357fb | ||
|
|
ba7ff133e6 | ||
|
|
0bd7ba9549 | ||
|
|
17c7361620 | ||
|
|
c45cbd02cc | ||
|
|
04cb501ab4 | ||
|
|
ba6f089c78 | ||
|
|
ab0cb6fc47 | ||
|
|
2847a315b1 | ||
|
|
65439df7fb | ||
|
|
b6436b09ce | ||
|
|
92354d5765 | ||
|
|
05651ad744 | ||
|
|
b7ff82d722 | ||
|
|
a285966560 | ||
|
|
538880b0e0 | ||
|
|
299270f74e | ||
|
|
9c69362650 | ||
|
|
d508aef7e5 | ||
|
|
616674b643 | ||
|
|
94847d9059 | ||
|
|
cbd416495c | ||
|
|
cc32194fb6 | ||
|
|
f5e2b43526 | ||
|
|
5bc8f0b9b1 | ||
|
|
7359a69223 | ||
|
|
04d64d09d7 | ||
|
|
43343182e4 | ||
|
|
072ab98fcf | ||
|
|
35ef6b9265 | ||
|
|
eaa53f2533 | ||
|
|
de322c4daf | ||
|
|
936c751a93 | ||
|
|
796a7014a1 | ||
|
|
f4368302ea | ||
|
|
01e611a9f9 | ||
|
|
315e0ef903 | ||
|
|
98d5dfff8e | ||
|
|
6b4705608b | ||
|
|
5907817cba | ||
|
|
aa97ac54d1 | ||
|
|
8fe548aba9 | ||
|
|
18a9288b75 | ||
|
|
fe82886f09 | ||
|
|
32e6993eea | ||
|
|
56b61909a3 | ||
|
|
2ef541cdd7 | ||
|
|
6b1d283cda | ||
|
|
c8e5566c81 | ||
|
|
7f3d9df089 | ||
|
|
99aa4dbca8 | ||
|
|
c193b8abd4 | ||
|
|
4efdc4f169 | ||
|
|
1304a4630b | ||
|
|
2cc3f939a7 | ||
|
|
d0260e564c | ||
|
|
2a24179423 | ||
|
|
34082b44f1 | ||
|
|
bfe340d24d | ||
|
|
a9288e376d | ||
|
|
cb9a03d010 | ||
|
|
e62366b755 | ||
|
|
2a2a96d9fc | ||
|
|
64a671ae13 | ||
|
|
45945876d8 | ||
|
|
90dacd0085 | ||
|
|
540ef68dc8 | ||
|
|
54cc981956 | ||
|
|
2e8ea354d7 | ||
|
|
217f52294e | ||
|
|
f9b2675077 | ||
|
|
95fd2d99b2 | ||
|
|
dba2b23e9e | ||
|
|
acbc199143 | ||
|
|
2449c8715e | ||
|
|
c62593c0eb | ||
|
|
00cbc9342f | ||
|
|
df5a3a37f2 | ||
|
|
5ec14c588b | ||
|
|
d78a3a638a | ||
|
|
19d2cbfa27 | ||
|
|
f9af916352 | ||
|
|
90db12b513 | ||
|
|
7d326ef306 | ||
|
|
d0b005fb14 | ||
|
|
118060cf77 | ||
|
|
d2ef68daac | ||
|
|
8393b93c53 | ||
|
|
63adcc2cd9 | ||
|
|
60c842c704 | ||
|
|
f6fd6aed7f | ||
|
|
cb92368e5b | ||
|
|
8cd97db362 | ||
|
|
94e1359895 | ||
|
|
1bcc5b77ec | ||
|
|
ae622e0c08 | ||
|
|
c951f7d822 | ||
|
|
6a366acc74 | ||
|
|
a5f7d5e9cf | ||
|
|
ea2249c30c | ||
|
|
a8c60c9f2b | ||
|
|
0581e02cf3 | ||
|
|
efec811b91 | ||
|
|
f85209c817 | ||
|
|
7fda5a9a4b | ||
|
|
ab689fc0db | ||
|
|
cdcdbe8f70 | ||
|
|
46ca8a409a | ||
|
|
e5c1641b6b | ||
|
|
3e475e7e08 | ||
|
|
3899144f8f | ||
|
|
b8cb9e7734 | ||
|
|
c62b9edf87 | ||
|
|
0e5aea40e8 | ||
|
|
1dbfcd3dc8 | ||
|
|
a4ef5fca46 | ||
|
|
7cf309345f | ||
|
|
495632a064 | ||
|
|
f6591e80ea | ||
|
|
ab5e8c366e | ||
|
|
ce35e23a0f | ||
|
|
ece263ea45 | ||
|
|
f777318cc7 | ||
|
|
c29f3ecdeb | ||
|
|
8f8740ad94 | ||
|
|
9acabba761 | ||
|
|
c3adcc877a | ||
|
|
7f92e921b4 | ||
|
|
e22a4394f7 | ||
|
|
070e5051c6 | ||
|
|
c040dffb5f | ||
|
|
c2f2a7d5e2 | ||
|
|
fd29d18312 | ||
|
|
2f724075b2 | ||
|
|
06224e4b20 | ||
|
|
f81888cd8a | ||
|
|
6a7b543ad6 | ||
|
|
6ba93527ba | ||
|
|
d6d2639e3a | ||
|
|
ecc51001c3 | ||
|
|
e2232bfa12 | ||
|
|
2bea8b7c84 | ||
|
|
dd5ae29f82 | ||
|
|
cb741a5521 | ||
|
|
9d434a36d6 | ||
|
|
e89760f374 | ||
|
|
02dd70480d | ||
|
|
c8e59cdd0c | ||
|
|
882952de3e | ||
|
|
279bec6eaa | ||
|
|
614ed283c0 | ||
|
|
06672d5ff9 | ||
|
|
78b8cfd365 | ||
|
|
e0f0e08852 | ||
|
|
e00f102703 | ||
|
|
3921627fa2 | ||
|
|
7a1a65c31b | ||
|
|
5e763f1a8b | ||
|
|
808fa5839a | ||
|
|
a0c5f94017 | ||
|
|
9ba1c2c32d | ||
|
|
5333fb8eab | ||
|
|
ee4a918fc7 | ||
|
|
1dbfe3417b | ||
|
|
c829732af0 | ||
|
|
1b313a3202 | ||
|
|
5732c4403b | ||
|
|
6033a0a743 | ||
|
|
e8cfe46381 | ||
|
|
e94f807d52 | ||
|
|
c15490e756 | ||
|
|
b25c523528 | ||
|
|
6d27da8ad8 | ||
|
|
d0e6788724 | ||
|
|
1633308000 | ||
|
|
08141e36cb | ||
|
|
b5cfdb1ef6 | ||
|
|
3a97a67c7e | ||
|
|
8d6101ec5a | ||
|
|
e73da37bc0 | ||
|
|
3d587a5762 | ||
|
|
42a6be95e8 | ||
|
|
ee8c367933 | ||
|
|
a20e19922e | ||
|
|
d6d588c5aa | ||
|
|
1ba0f5ab74 | ||
|
|
b838cb1c6f | ||
|
|
7eb665e401 | ||
|
|
cb3e371094 | ||
|
|
ea30f38b9b | ||
|
|
8187334ef6 | ||
|
|
ac24e8b028 | ||
|
|
30ba544f35 | ||
|
|
14b1bc3710 | ||
|
|
e8c0d6b987 | ||
|
|
d0efb206d9 | ||
|
|
8abb04afde | ||
|
|
f7318cfc5a | ||
|
|
067727165a | ||
|
|
544c93c7cf | ||
|
|
66bc023e51 | ||
|
|
c5ea2d0d24 | ||
|
|
9e8d9b44b1 | ||
|
|
db15eaab04 | ||
|
|
9d016212c8 | ||
|
|
a4158c476e | ||
|
|
0f1148e096 | ||
|
|
5d17f006f0 | ||
|
|
16d303a6fb | ||
|
|
70e5ac4898 | ||
|
|
a914de63c6 | ||
|
|
dec518369b | ||
|
|
926a4e642a | ||
|
|
3236883cce | ||
|
|
be1c3b17d6 | ||
|
|
a67356639b | ||
|
|
7b3cb2eb00 | ||
|
|
8459ffb690 | ||
|
|
b260a20646 | ||
|
|
db29adff5d | ||
|
|
d3576440d4 | ||
|
|
c557e383b6 | ||
|
|
768a1e37e9 | ||
|
|
46e2fc6ab6 | ||
|
|
dacf004797 | ||
|
|
44ed81218a | ||
|
|
d802abc86c | ||
|
|
4c22284ca7 | ||
|
|
929c970b42 | ||
|
|
496c8d8356 | ||
|
|
e707f1a23d | ||
|
|
e7145018ef | ||
|
|
18164fdb16 | ||
|
|
3b9e40c5d4 | ||
|
|
6d20b8ef72 | ||
|
|
8bdd35975e | ||
|
|
9ccdd6c3e7 | ||
|
|
30365a2256 | ||
|
|
cdd4100a30 | ||
|
|
2cd9f50357 | ||
|
|
106345ff49 | ||
|
|
7c8c961aef | ||
|
|
e1bd7f0267 | ||
|
|
025c5809be | ||
|
|
d45fdd50e7 | ||
|
|
f4388d36de | ||
|
|
4a62339c69 | ||
|
|
5a9b8d6bd0 | ||
|
|
8ce71de693 | ||
|
|
6d9846f1f5 | ||
|
|
c9be9b0538 | ||
|
|
65f7214e67 | ||
|
|
302cebbbec | ||
|
|
46c60a32fd | ||
|
|
7ec6d84c7d | ||
|
|
0bbdb03ace | ||
|
|
149d074206 | ||
|
|
0b491826ee | ||
|
|
e6d4f2540c | ||
|
|
fcc75710cb | ||
|
|
de65c5a6cf | ||
|
|
fde52167b3 | ||
|
|
1ffdf3d283 | ||
|
|
94a49c17f7 | ||
|
|
e515039ad4 | ||
|
|
93f88296da | ||
|
|
1f4e8e752e | ||
|
|
fed9b9a19d | ||
|
|
fbcc71340d | ||
|
|
c6356df81f | ||
|
|
085bd39684 | ||
|
|
b73bef8a0c | ||
|
|
9c662de129 | ||
|
|
caa37b087c | ||
|
|
b63c853889 | ||
|
|
2ff79c7780 | ||
|
|
403cb5a6ad | ||
|
|
b43f196d86 | ||
|
|
483b353494 | ||
|
|
cddc99981d | ||
|
|
01f1f50880 | ||
|
|
8664c3df37 | ||
|
|
f009c43878 | ||
|
|
f8482601a8 | ||
|
|
8c4ab88888 | ||
|
|
37421dd56a | ||
|
|
5c2581a90a | ||
|
|
6a3a630759 | ||
|
|
fff5110e9a | ||
|
|
d31fe9cb71 | ||
|
|
bd762172d4 | ||
|
|
b32a7b3a9e | ||
|
|
3ccc09674e | ||
|
|
c10f10010a | ||
|
|
9beef8f36a | ||
|
|
8408220870 | ||
|
|
2e63993b7f | ||
|
|
b482c7a076 | ||
|
|
733abd5568 | ||
|
|
dd1147f534 | ||
|
|
19c90d356c | ||
|
|
c042e39d54 | ||
|
|
598ae07fc2 | ||
|
|
e5d7612af9 | ||
|
|
f3924dab5b | ||
|
|
ac6f49e63d | ||
|
|
7f4cb3888f | ||
|
|
120c2fe52a | ||
|
|
b9c674d662 | ||
|
|
dcee4677ed | ||
|
|
d590f6d5c1 | ||
|
|
850a370f9d | ||
|
|
40e7ede5e3 | ||
|
|
9a2257dd1e | ||
|
|
7b4eddc967 | ||
|
|
843e37b99d | ||
|
|
19981ce649 | ||
|
|
2740af3571 | ||
|
|
b693e80d75 | ||
|
|
e9ce679649 | ||
|
|
a56d6b568b | ||
|
|
904d09d91c | ||
|
|
3700f7a10b | ||
|
|
d57415d23d | ||
|
|
86649d8314 | ||
|
|
06eca94492 | ||
|
|
ef6f6f95c0 | ||
|
|
991a3e2ab5 | ||
|
|
08c6659804 | ||
|
|
74e4724e66 | ||
|
|
1ea8694769 | ||
|
|
218140066b | ||
|
|
837cfab1bd | ||
|
|
3428b11ea8 | ||
|
|
f661a6bd37 | ||
|
|
c3c1aa5aff | ||
|
|
7bcb6acb03 | ||
|
|
5b22d65dba | ||
|
|
8570c2d287 | ||
|
|
acc797666d | ||
|
|
b62a42bed8 | ||
|
|
b452be880b | ||
|
|
49176ae240 | ||
|
|
8eb4a39e7d | ||
|
|
0f65a1f5dd | ||
|
|
a71edc4040 | ||
|
|
23b6cf1a68 | ||
|
|
3babc6c50a | ||
|
|
a4ef00fe3e | ||
|
|
0f3bbf6368 | ||
|
|
95ebc44f05 | ||
|
|
64945637e0 | ||
|
|
0baf977bc9 | ||
|
|
caa33c29e9 | ||
|
|
d5050338f3 | ||
|
|
7f0877bf28 | ||
|
|
d4c4257517 | ||
|
|
61f76afa0d | ||
|
|
fe86cb4b74 | ||
|
|
5634f48725 | ||
|
|
964d50b4e7 | ||
|
|
d2cb48a2ef | ||
|
|
53411dc5d9 | ||
|
|
cab6089a37 | ||
|
|
32fea64f3e | ||
|
|
bf4e0ca7c0 | ||
|
|
39bd02f741 | ||
|
|
930b1181ee | ||
|
|
1aac8c1e25 | ||
|
|
3e8b110809 | ||
|
|
e0c1bebb13 | ||
|
|
7ccb2aaa9c | ||
|
|
aa2e5f15ee | ||
|
|
ed5e93f373 | ||
|
|
48247ea7fe | ||
|
|
12a5f335bd | ||
|
|
5e19eadd61 | ||
|
|
0e88f0074c | ||
|
|
2bfc67686d | ||
|
|
6c2c8f9900 | ||
|
|
766bf9e401 | ||
|
|
4f8fedbaa0 | ||
|
|
b108c9f11a | ||
|
|
cc380c85b9 | ||
|
|
62165ce01d | ||
|
|
c8b05649f5 | ||
|
|
94fb62fcca | ||
|
|
bef8e8e548 | ||
|
|
88063cd30e | ||
|
|
2185fbff65 | ||
|
|
a94a602d4f | ||
|
|
6ed13bdccb | ||
|
|
ff79ad1338 | ||
|
|
f6703e11c4 | ||
|
|
2e1936dcce | ||
|
|
698ac2758f | ||
|
|
8cef8e5c9e | ||
|
|
c5c53466fb | ||
|
|
acd2e9398b | ||
|
|
df97166f07 | ||
|
|
022fef2b9e | ||
|
|
5f05e8fcaf | ||
|
|
7e353eb0e8 | ||
|
|
499389d2c3 | ||
|
|
7274f606dc | ||
|
|
da52f125f3 | ||
|
|
b418dec3ab | ||
|
|
79401183ca | ||
|
|
270d3b7e5b | ||
|
|
4e3f9914f1 | ||
|
|
dd8e1f2d71 | ||
|
|
f63f019e87 | ||
|
|
11e7c41908 | ||
|
|
57c2fd9b73 | ||
|
|
dc9fe38735 | ||
|
|
622d4ac165 | ||
|
|
3090e13be7 | ||
|
|
f96a36aa43 | ||
|
|
6ad24419ab | ||
|
|
04319a6b41 | ||
|
|
952f6b139d | ||
|
|
f58cb923d4 | ||
|
|
d43067bad4 | ||
|
|
c17ade64e1 | ||
|
|
4ddbba1400 | ||
|
|
536e2a3b7c | ||
|
|
fe97e158ef | ||
|
|
6e3ad3dd6b | ||
|
|
7a2b07eebd | ||
|
|
70235eeeee | ||
|
|
f0f5af4fb0 | ||
|
|
0254a4ec34 | ||
|
|
6d24b07573 | ||
|
|
0d19ec267f | ||
|
|
c63987d726 | ||
|
|
086dcad81f | ||
|
|
eaacf04c68 | ||
|
|
ee859df057 | ||
|
|
d809c6ffa9 | ||
|
|
0cc4d85b37 | ||
|
|
7c2f49146d | ||
|
|
19c2fb6f82 | ||
|
|
882a97566b | ||
|
|
b1d67af206 | ||
|
|
ca1daaaea3 | ||
|
|
b24b1d17e6 | ||
|
|
86b2dcd248 | ||
|
|
bd84c433cd | ||
|
|
238a611cbb | ||
|
|
8b951a306d | ||
|
|
d685fa2a30 | ||
|
|
ae0b036ae4 | ||
|
|
351dee6a12 | ||
|
|
8b748a7840 | ||
|
|
afb01b0258 | ||
|
|
d92ca5f2a9 | ||
|
|
f21909adb8 | ||
|
|
2bf4a25dd1 | ||
|
|
2096e6ea8d | ||
|
|
25cad60ef8 | ||
|
|
8e5b72c833 | ||
|
|
90d9510d23 | ||
|
|
58093eedf0 | ||
|
|
754ad160dc | ||
|
|
12e3b3a490 | ||
|
|
eca12a6587 | ||
|
|
5106b73699 | ||
|
|
98fe1e0121 | ||
|
|
de99077b32 | ||
|
|
e288a3d3a9 | ||
|
|
0588d39911 | ||
|
|
55ec76a23d | ||
|
|
a2eab9e5ab | ||
|
|
f651af970f | ||
|
|
1eecb324d0 | ||
|
|
60a964ae55 | ||
|
|
a7cf8f9ec9 | ||
|
|
0b4e3b9656 | ||
|
|
ca8a8701b4 | ||
|
|
4ca83fcc1a | ||
|
|
509e1ef00a | ||
|
|
42fc0527cb | ||
|
|
8b508fc514 | ||
|
|
2f060cfb43 | ||
|
|
4eb79fb017 | ||
|
|
c38d595cb8 | ||
|
|
e29407486d | ||
|
|
4d3ca94e4c | ||
|
|
e27ec8136e | ||
|
|
9383976918 | ||
|
|
8764270f47 | ||
|
|
2ef85d9aae | ||
|
|
5064e5b0b1 | ||
|
|
72244b1983 | ||
|
|
e14d3eac4d | ||
|
|
62cbe4a833 | ||
|
|
6040e79d50 | ||
|
|
ffe3dd6bca | ||
|
|
853053f56d | ||
|
|
dde431b422 | ||
|
|
e6cf77f34b | ||
|
|
834a5e83ee | ||
|
|
f2173bff26 | ||
|
|
c4b1da66d4 | ||
|
|
9c9f2973f8 | ||
|
|
0c35f32c5c | ||
|
|
f2e3e3dbf1 | ||
|
|
5045098c91 | ||
|
|
20e34bfe15 | ||
|
|
0f63feed22 | ||
|
|
ec6f3098bb | ||
|
|
53f08eae30 | ||
|
|
cade83f075 | ||
|
|
af93088d2f | ||
|
|
e396ad4f67 | ||
|
|
979a77eafa | ||
|
|
c0b42cf29a | ||
|
|
ae4f20bca1 | ||
|
|
b947a466a9 | ||
|
|
4063998ddb | ||
|
|
266397bac3 | ||
|
|
f023b99fa9 | ||
|
|
bb148f9bea | ||
|
|
7bdcbf2e95 | ||
|
|
070d1947e8 | ||
|
|
a4c244cb61 | ||
|
|
e089271f78 | ||
|
|
24887cce83 | ||
|
|
0f7a81ff11 | ||
|
|
2d15445482 | ||
|
|
3236d7dfd1 | ||
|
|
8a06cac5f1 | ||
|
|
60d5c6b55e | ||
|
|
77c6e0dbff | ||
|
|
3e22aabe28 | ||
|
|
bedea9eb05 | ||
|
|
0d8e5ec77c | ||
|
|
de840af331 | ||
|
|
255b2b2320 | ||
|
|
034b7a642e | ||
|
|
407f9ca6ad | ||
|
|
bf1d8b1be4 | ||
|
|
562f3ea937 | ||
|
|
0a29fb89c4 | ||
|
|
27daddcb72 | ||
|
|
c7b00ee8c6 | ||
|
|
1d7c7fd8af | ||
|
|
6b06e78b61 | ||
|
|
9ec1882032 | ||
|
|
18fc86d68a | ||
|
|
a628d5bb59 | ||
|
|
df1e1cd334 | ||
|
|
d6c6eaa064 | ||
|
|
b4bdb08dc1 | ||
|
|
ae9c21e293 | ||
|
|
b65c8f696b | ||
|
|
88e6e4bf56 | ||
|
|
8f4597045d | ||
|
|
a7f12ad871 | ||
|
|
4e791d50d4 | ||
|
|
9758e55b72 | ||
|
|
473239cc9a | ||
|
|
477cac6ca9 | ||
|
|
258e9738f7 | ||
|
|
39de0892f1 | ||
|
|
36ec4e09fd | ||
|
|
aa4e6b7f36 | ||
|
|
6440645c5a | ||
|
|
4585519943 | ||
|
|
1f16bc9a7b | ||
|
|
4b9cbf9aee | ||
|
|
e0cc7dbffa | ||
|
|
fd9d78061b | ||
|
|
b03d57f40a | ||
|
|
4e6e70c14d | ||
|
|
2ef9a77325 | ||
|
|
18b9fb3ee2 | ||
|
|
02f2554cc1 | ||
|
|
07961c9f21 | ||
|
|
f770b3cf14 | ||
|
|
62dd006d50 | ||
|
|
9ff845d375 | ||
|
|
58860dca48 | ||
|
|
f2e397f533 | ||
|
|
5afff12848 | ||
|
|
37abf19f0d | ||
|
|
bbbd7faeb1 | ||
|
|
c1382dc0aa | ||
|
|
a73f2654df | ||
|
|
abc9a6ffbf | ||
|
|
87e32a159b | ||
|
|
22f0aee55d | ||
|
|
01420ff1d8 | ||
|
|
c4b5d13348 | ||
|
|
9cf2d47eef | ||
|
|
a9d6d6f820 | ||
|
|
f70d303942 | ||
|
|
967c3aa591 | ||
|
|
3a47fb2c79 | ||
|
|
1112186d1c | ||
|
|
f40332f197 | ||
|
|
a11813f4b2 | ||
|
|
13d396a388 | ||
|
|
3d3458d577 | ||
|
|
e142785a9d | ||
|
|
ddac3a9871 | ||
|
|
bdb15aa0bb | ||
|
|
6693b131d8 | ||
|
|
41efc66d25 | ||
|
|
a5197b4ced | ||
|
|
d49d40768c | ||
|
|
c71264ab30 | ||
|
|
8f1fd17f5c | ||
|
|
7179bb79a0 | ||
|
|
bb64a2f1ec | ||
|
|
3f0dfd63d4 | ||
|
|
46f7ec7af9 | ||
|
|
999c1b4239 | ||
|
|
f6b2535cdb | ||
|
|
5f1c868006 | ||
|
|
59366e4d3a | ||
|
|
bea0532872 | ||
|
|
e684c583fb | ||
|
|
eed2f073a0 | ||
|
|
31a03aa331 | ||
|
|
71984c72b5 | ||
|
|
72573e32cb | ||
|
|
50f4cc10c4 | ||
|
|
c2f98583e1 | ||
|
|
1ff6d0a2dc | ||
|
|
92ac8b09c0 | ||
|
|
384e993ca1 | ||
|
|
c1241fdfbc | ||
|
|
be9d6ac660 | ||
|
|
30b469ddbd | ||
|
|
22ee99f222 | ||
|
|
f4675f0a34 | ||
|
|
111c6fc1bf | ||
|
|
0cd2761021 | ||
|
|
0a7c8988c6 | ||
|
|
7947533182 | ||
|
|
184c39d311 | ||
|
|
d89eaec596 | ||
|
|
40ce0d75ed | ||
|
|
61bd28db31 | ||
|
|
b1426945d4 | ||
|
|
dec9097ce7 | ||
|
|
7bb93e8351 | ||
|
|
7a84223d5b | ||
|
|
398628870c | ||
|
|
3e426537c7 | ||
|
|
bf1bd3ef5a | ||
|
|
b85b1e44ef | ||
|
|
ff194c0382 | ||
|
|
078f7cfc90 | ||
|
|
bd72a773f4 | ||
|
|
9637fb4bf3 | ||
|
|
22dc5c909c | ||
|
|
cd4336d438 | ||
|
|
fb619e0fa9 | ||
|
|
08dbc3c035 | ||
|
|
3fba4390c5 | ||
|
|
500585a0a0 | ||
|
|
acaa88f1a9 | ||
|
|
005dc47868 | ||
|
|
9c1c894e29 | ||
|
|
b055bc73c5 | ||
|
|
322cbf27dc | ||
|
|
417a13c1be | ||
|
|
05819497e4 | ||
|
|
66c93f472a | ||
|
|
023b23a0ef | ||
|
|
900896c045 | ||
|
|
db97453c54 | ||
|
|
3fdd61edfc | ||
|
|
e839c6bd6b | ||
|
|
2d9bc50401 | ||
|
|
c48d8b93dd | ||
|
|
e2e96a04d1 | ||
|
|
c724896ecd | ||
|
|
914aaa0a96 | ||
|
|
b5becda6fc | ||
|
|
37868777e7 | ||
|
|
3663ed0235 | ||
|
|
7fa84af66a | ||
|
|
55718a09e0 | ||
|
|
3754e0cbe3 | ||
|
|
3df2536bb6 | ||
|
|
9e07f1924c | ||
|
|
fbf4544849 | ||
|
|
2d4e6bb8da | ||
|
|
211dfc62e4 | ||
|
|
679c5892a4 | ||
|
|
1dac755787 | ||
|
|
1f4e0f5e73 | ||
|
|
b616894f2e | ||
|
|
7a910709f9 | ||
|
|
d61f8dac2e | ||
|
|
6d1fecc408 | ||
|
|
366d44959e | ||
|
|
d37c8f5387 | ||
|
|
06c5ca412a | ||
|
|
afa95f79cd | ||
|
|
8fe3457e0a | ||
|
|
7bfd60be86 | ||
|
|
b7284ada94 | ||
|
|
66e2dc73f9 | ||
|
|
d254e5670b | ||
|
|
9c945b33fb | ||
|
|
c53a66d20e | ||
|
|
335b113327 | ||
|
|
122590265d | ||
|
|
aab2f8b090 | ||
|
|
f0a4c130f6 | ||
|
|
029f0a09ba | ||
|
|
8fe3d2b0b3 | ||
|
|
25c31fcb2e | ||
|
|
2a74809294 | ||
|
|
09154e40aa | ||
|
|
2e9b236406 | ||
|
|
1f88b72dba | ||
|
|
0c133afbaf | ||
|
|
2f87121e27 | ||
|
|
b72e2d3fe0 | ||
|
|
4119414079 | ||
|
|
955fe6795d | ||
|
|
fbbb59971c | ||
|
|
0a6df20b7d | ||
|
|
d640d86160 | ||
|
|
6a70bed30f | ||
|
|
91503cfd25 | ||
|
|
56feba9b45 | ||
|
|
5eec7c317c | ||
|
|
04c650528f | ||
|
|
e8ba0fb0bb | ||
|
|
22cb24da09 | ||
|
|
8204641656 | ||
|
|
c5ba127b9e | ||
|
|
d2be562619 | ||
|
|
a4c8638448 | ||
|
|
51cf58fcdf | ||
|
|
b00b7817f2 | ||
|
|
6b1e432f6d | ||
|
|
f590194fba | ||
|
|
72ec59bdac | ||
|
|
a07df519ab | ||
|
|
7bf3049f4a | ||
|
|
a88315ee74 | ||
|
|
c182c70b8d | ||
|
|
fab8568633 | ||
|
|
74545012ed | ||
|
|
903a1654b6 | ||
|
|
7161c1ac4e | ||
|
|
a9cf307cbf | ||
|
|
f9cfcaeabe | ||
|
|
b9aacf28e5 | ||
|
|
54512491b7 | ||
|
|
a3fee54f7a | ||
|
|
e5f05aa724 | ||
|
|
312c2d1574 | ||
|
|
7289636d35 | ||
|
|
733da1ea94 | ||
|
|
a1b4344943 | ||
|
|
3c26beb48c | ||
|
|
c0049326b6 | ||
|
|
543d345aea | ||
|
|
fb1354898c | ||
|
|
e0263edc54 | ||
|
|
b3a7d7c9a8 | ||
|
|
a8008a9418 | ||
|
|
e82f560c44 | ||
|
|
3589c7de69 | ||
|
|
72cf2c7578 | ||
|
|
eddd77d2d9 | ||
|
|
5d61468de6 | ||
|
|
e85debddfc | ||
|
|
d45ea02562 | ||
|
|
bad43090ff | ||
|
|
c2867d9638 | ||
|
|
6dbbbac344 | ||
|
|
e903f609a5 | ||
|
|
9dd1f1f90b | ||
|
|
4b22390faf | ||
|
|
9dbfef3df8 | ||
|
|
28c794b2da | ||
|
|
ee96ce5046 | ||
|
|
28d311e759 | ||
|
|
d355393074 | ||
|
|
7b220da936 | ||
|
|
a1130b0e7c | ||
|
|
8bba55e441 | ||
|
|
d6d2e32b2e | ||
|
|
37c8317410 | ||
|
|
19337b230c | ||
|
|
d0ec6d8244 | ||
|
|
a232e7dfcd | ||
|
|
9b0a8dbb07 | ||
|
|
2e7a2a07ac | ||
|
|
36e119770a | ||
|
|
ac2efd2baf | ||
|
|
5bd9c5fefc | ||
|
|
68bccb4d3f | ||
|
|
284b2c0db3 | ||
|
|
83a63da6c4 | ||
|
|
dc7c0885a7 | ||
|
|
82a42f3649 | ||
|
|
30b600fe36 | ||
|
|
eedfc99064 | ||
|
|
80c316201d | ||
|
|
a8f7f6a04e | ||
|
|
94eb306692 | ||
|
|
e673c5340c | ||
|
|
3c7c836b64 | ||
|
|
7068faaa92 | ||
|
|
d063bc0e78 | ||
|
|
c6442ed68a | ||
|
|
ebb95a8292 | ||
|
|
7a185b5054 | ||
|
|
0bd9b5b0d1 | ||
|
|
74e85cdadc | ||
|
|
82dadb31b5 | ||
|
|
b65e56b9fd | ||
|
|
0b696202e7 | ||
|
|
5d0c6c0c6e | ||
|
|
99d2d7a2ae | ||
|
|
0e8b626528 | ||
|
|
d80ce1d8c5 | ||
|
|
86c0520076 | ||
|
|
966870200b | ||
|
|
a3004a5140 | ||
|
|
dc97556807 | ||
|
|
79d8aeee11 | ||
|
|
d5430256c7 | ||
|
|
6691801721 | ||
|
|
46c1c972ab | ||
|
|
4339a653ce | ||
|
|
299122f965 | ||
|
|
a6b160caed | ||
|
|
a5672bc1b3 | ||
|
|
6e5dff6454 | ||
|
|
38e060f704 | ||
|
|
227652ec8f | ||
|
|
fa263eb68d | ||
|
|
d3992b81ef | ||
|
|
c430657738 | ||
|
|
d78301567b | ||
|
|
bddd93cd80 | ||
|
|
a659820b07 | ||
|
|
d32f9ef763 | ||
|
|
920dd9a947 | ||
|
|
3f352a393b | ||
|
|
86df27587e | ||
|
|
7b1ccd956b | ||
|
|
e928faf4f9 | ||
|
|
70e2cefd98 | ||
|
|
49e8dbe5f6 | ||
|
|
250dd0c92d | ||
|
|
d4adafbcb7 | ||
|
|
ffc98f31c9 | ||
|
|
1a71de851a | ||
|
|
dd67efe0f6 | ||
|
|
033383eea4 | ||
|
|
ee873a4ae2 | ||
|
|
5667a6ee09 | ||
|
|
d26a4a35ab | ||
|
|
d7acc88c05 | ||
|
|
1ab6dfceb1 | ||
|
|
a90c746626 | ||
|
|
fb1b5802ab | ||
|
|
69ceeff9b8 | ||
|
|
b2baef0643 | ||
|
|
2d1a2fd187 | ||
|
|
6d02d8876a | ||
|
|
712d0051d9 | ||
|
|
e64a892275 | ||
|
|
2e8a8966d7 | ||
|
|
f9b3db4058 | ||
|
|
633f224be6 | ||
|
|
79501b46fe | ||
|
|
df55398100 | ||
|
|
2bcb20d710 | ||
|
|
79ae96f15d | ||
|
|
ac6d269d90 | ||
|
|
a4026e8c25 | ||
|
|
b448dad860 | ||
|
|
17762d9daa | ||
|
|
205201668c | ||
|
|
522cfca0af | ||
|
|
9bef8ddee3 | ||
|
|
7999c1fbe5 | ||
|
|
bec893d662 | ||
|
|
6f999f6a87 | ||
|
|
10fef82225 | ||
|
|
ebbcfa3157 | ||
|
|
313144bebf | ||
|
|
c6b5a5b400 | ||
|
|
1fdcbd848c | ||
|
|
e63e741ad6 | ||
|
|
282aede691 | ||
|
|
e5b95921cf | ||
|
|
8c6726800f | ||
|
|
6987b3b4d4 | ||
|
|
28a2196143 | ||
|
|
5b9a03a261 | ||
|
|
228ffcfbc9 | ||
|
|
cc3b3575b6 | ||
|
|
ecacc71596 | ||
|
|
b2ad4b6995 | ||
|
|
86929d8f69 | ||
|
|
39fa7e3e17 | ||
|
|
70bc909565 | ||
|
|
9ebe967e28 | ||
|
|
93c35fd0ec | ||
|
|
2e80e82fc4 | ||
|
|
2d19a1e86a | ||
|
|
91700ab93e | ||
|
|
ecc736be8b | ||
|
|
8feb2287cc | ||
|
|
e8d907156c | ||
|
|
744980f119 | ||
|
|
7f4dd8859e | ||
|
|
f3962266b4 | ||
|
|
b867c985ed | ||
|
|
0baa06cee0 | ||
|
|
e84d7f8741 | ||
|
|
8c5e9534b2 | ||
|
|
8f5a9c9349 | ||
|
|
e11d1dd5d9 | ||
|
|
df7210f9ad | ||
|
|
8eead759d9 | ||
|
|
c6b9fd181f | ||
|
|
c4ad9f1e88 | ||
|
|
8455994118 | ||
|
|
2495405511 | ||
|
|
68f6312953 | ||
|
|
e87b8dc368 | ||
|
|
6083484d19 | ||
|
|
3bdd0a7265 | ||
|
|
9d509efe35 | ||
|
|
97acc7d1d0 | ||
|
|
3534aa7e69 | ||
|
|
700370f70f | ||
|
|
95c96b3894 | ||
|
|
fdce55cf3b | ||
|
|
9d118e0ef3 | ||
|
|
18ef32a34c | ||
|
|
cfd81a91fc | ||
|
|
87ebbcd7ec | ||
|
|
4b2ebf4761 | ||
|
|
1482cfcf32 | ||
|
|
472ed62c12 | ||
|
|
04822e9d8f | ||
|
|
fe81c6cad7 | ||
|
|
6e6771ff3b | ||
|
|
e5fc1bef44 | ||
|
|
68b213b737 | ||
|
|
ceb86a2d5f | ||
|
|
6bd28ba82e | ||
|
|
dbc0c0ad40 | ||
|
|
6baebfad11 | ||
|
|
536b154aaa | ||
|
|
db75a5fb74 | ||
|
|
be1e161f31 | ||
|
|
7864114d7c | ||
|
|
405114a893 | ||
|
|
a90adf1212 | ||
|
|
6c5fb4cd35 | ||
|
|
a443710669 | ||
|
|
9e881bc9a5 | ||
|
|
4197cfba98 | ||
|
|
4ec1086fc9 | ||
|
|
3d11f2cacc | ||
|
|
b4ce2e8167 | ||
|
|
5527ed0a86 | ||
|
|
107ad572f8 | ||
|
|
f16315328b | ||
|
|
ae705e1b40 | ||
|
|
2d9287805e | ||
|
|
ed35ddc388 | ||
|
|
8379624581 | ||
|
|
86b31575eb | ||
|
|
654e4278fa | ||
|
|
f9c6c0465a | ||
|
|
9cb3bd564b | ||
|
|
bc884175be | ||
|
|
fb6e789909 | ||
|
|
944982898e | ||
|
|
a8357dffb9 | ||
|
|
50f5c6a98d | ||
|
|
f5d050f3f2 | ||
|
|
2a79303241 | ||
|
|
a72fa5b8dd | ||
|
|
66421ae557 | ||
|
|
4b2f6a2c27 | ||
|
|
9b99fe61e0 | ||
|
|
e0584066a9 | ||
|
|
d70e60d4a5 | ||
|
|
7abec2ccb8 | ||
|
|
16a39410b8 | ||
|
|
b63c4e510c | ||
|
|
520dda70c0 | ||
|
|
b6169ac706 | ||
|
|
080d921791 | ||
|
|
a283329e4d | ||
|
|
5bb48df01d | ||
|
|
b3e961a3c6 | ||
|
|
420b61ab52 | ||
|
|
dbd81eed2b | ||
|
|
d0a00236ba | ||
|
|
01aa9352aa | ||
|
|
5c258520a2 | ||
|
|
c232260e46 | ||
|
|
bce825ff32 | ||
|
|
3c1ed52bb9 | ||
|
|
982fc6aaa2 | ||
|
|
19890460b9 | ||
|
|
a46824c8ab | ||
|
|
bbfa03c894 | ||
|
|
56d1f7b6eb | ||
|
|
1d2e183839 | ||
|
|
d741f24e8c | ||
|
|
22489f2dec | ||
|
|
163c116871 | ||
|
|
7d5d791376 | ||
|
|
4b3f11418e | ||
|
|
0b4d1639c6 | ||
|
|
018d19857d | ||
|
|
4233c36fd0 | ||
|
|
8375caaaba | ||
|
|
12ae7bbf56 | ||
|
|
8555ad5118 | ||
|
|
eeffa02f59 | ||
|
|
7003e3a03b | ||
|
|
fc023fd833 | ||
|
|
b45a968a9a | ||
|
|
8dac520a79 | ||
|
|
6237673725 | ||
|
|
0f9ec99c1d | ||
|
|
433da35d34 | ||
|
|
1a675ed40e | ||
|
|
a1c47b7ca3 | ||
|
|
6c8e7b024f | ||
|
|
76d7b8c3b8 | ||
|
|
2b1387620b | ||
|
|
2d0fa2d26f | ||
|
|
3eca4b3dac | ||
|
|
436ae6c610 | ||
|
|
70c00f1424 | ||
|
|
61fc79ff47 | ||
|
|
7dfefedf77 | ||
|
|
48ce07eaa1 | ||
|
|
61d0f87f5b | ||
|
|
2ee7668382 | ||
|
|
eaf1d1be6d | ||
|
|
36a24add6e | ||
|
|
e66c14b086 | ||
|
|
6b646e3510 | ||
|
|
a727a7f377 | ||
|
|
a366f14434 | ||
|
|
834669bf36 | ||
|
|
3c1c43ae9c | ||
|
|
0d2860dd8e | ||
|
|
ea25842f9d | ||
|
|
4b21874251 | ||
|
|
36e000b988 | ||
|
|
0d5ca9306e | ||
|
|
fb070a9843 | ||
|
|
f8426529fe | ||
|
|
79b3698b64 | ||
|
|
e74db54485 | ||
|
|
5f14e76b95 | ||
|
|
2c45f9585d |
@@ -1 +0,0 @@
|
||||
web
|
||||
18
.github/ISSUE_TEMPLATE/bug_report.md
vendored
18
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -1,18 +0,0 @@
|
||||
---
|
||||
name: Bug Report
|
||||
about: Report a bug encountered while operating Nightingale
|
||||
labels: kind/bug
|
||||
---
|
||||
|
||||
**What happened**:
|
||||
|
||||
**What you expected to happen**:
|
||||
|
||||
**How to reproduce it (as minimally and precisely as possible)**:
|
||||
|
||||
**Anything else we need to know?**:
|
||||
|
||||
**Environment**:
|
||||
- OS (e.g: `cat /etc/os-release`):
|
||||
- Logs:
|
||||
- Others:
|
||||
67
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
67
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
@@ -0,0 +1,67 @@
|
||||
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
|
||||
6
.github/ISSUE_TEMPLATE/config.yml
vendored
6
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1,5 +1,5 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Nightingale community
|
||||
url: https://n9e.didiyun.com/community/
|
||||
about: List of communication channels for the Nightingale community.
|
||||
- name: Nightingale docs
|
||||
url: https://n9e.github.io/
|
||||
about: You may want to read through the document before asking questions.
|
||||
44
.github/workflows/n9e.yml
vendored
44
.github/workflows/n9e.yml
vendored
@@ -1,26 +1,32 @@
|
||||
name: Go
|
||||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
tags:
|
||||
- 'v*'
|
||||
env:
|
||||
GO_VERSION: 1.18
|
||||
|
||||
jobs:
|
||||
|
||||
build:
|
||||
name: Build
|
||||
goreleaser:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
- name: Set up Go 1.13
|
||||
uses: actions/setup-go@v1
|
||||
with:
|
||||
go-version: 1.13
|
||||
id: go
|
||||
|
||||
- name: Check out code into the Go module directory
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Build
|
||||
run: ./control build
|
||||
- name: Checkout Source Code
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Setup Go Environment
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
- uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: Run GoReleaser
|
||||
uses: goreleaser/goreleaser-action@v3
|
||||
with:
|
||||
version: latest
|
||||
args: release --rm-dist
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
34
.gitignore
vendored
34
.gitignore
vendored
@@ -29,25 +29,33 @@ _test
|
||||
/build
|
||||
/dist
|
||||
/etc/*.local.yml
|
||||
/etc/log/log.test.json
|
||||
/etc/*.local.conf
|
||||
/etc/plugins/*.local.yml
|
||||
/etc/script/rules.yaml
|
||||
/etc/script/alert-rules.json
|
||||
/etc/script/record-rules.json
|
||||
/data*
|
||||
/tarball
|
||||
/run
|
||||
/vendor
|
||||
/tmp
|
||||
/pub
|
||||
/n9e
|
||||
/docker/pub
|
||||
/docker/n9e
|
||||
/docker/mysqldata
|
||||
/docker/experience_pg_vm/pgdata
|
||||
/etc.local*
|
||||
/front/statik/statik.go
|
||||
|
||||
.alerts
|
||||
.idea
|
||||
.index
|
||||
.vscode
|
||||
.DS_Store
|
||||
.cache-loader
|
||||
.payload
|
||||
queries.active
|
||||
|
||||
/n9e-*
|
||||
|
||||
/src/modules/index/index
|
||||
/src/modules/collector/collector
|
||||
/src/modules/transfer/transfer
|
||||
/src/modules/tsdb/tsdb
|
||||
/src/modules/monapi/monapi
|
||||
|
||||
/web/node_modules
|
||||
/web/.cache-loader
|
||||
/web/yarn.lock
|
||||
|
||||
|
||||
n9e.sql
|
||||
|
||||
122
.goreleaser.yaml
Normal file
122
.goreleaser.yaml
Normal file
@@ -0,0 +1,122 @@
|
||||
before:
|
||||
hooks:
|
||||
# You may remove this if you don't use go modules.
|
||||
- go mod tidy
|
||||
- go install github.com/rakyll/statik
|
||||
|
||||
snapshot:
|
||||
name_template: '{{ .Tag }}'
|
||||
checksum:
|
||||
name_template: 'checksums.txt'
|
||||
changelog:
|
||||
skip: true
|
||||
|
||||
builds:
|
||||
- id: build
|
||||
hooks:
|
||||
pre:
|
||||
- cmd: sh -x ./fe.sh
|
||||
output: true
|
||||
main: ./cmd/center/
|
||||
binary: n9e
|
||||
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-cli
|
||||
main: ./cmd/cli/
|
||||
binary: n9e-cli
|
||||
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-edge
|
||||
main: ./cmd/edge/
|
||||
binary: n9e-edge
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
goos:
|
||||
- linux
|
||||
goarch:
|
||||
- amd64
|
||||
- arm64
|
||||
ldflags:
|
||||
- -s -w
|
||||
- -X github.com/ccfos/nightingale/v6/pkg/version.Version={{ .Tag }}-{{.Commit}}
|
||||
|
||||
archives:
|
||||
- id: n9e
|
||||
builds:
|
||||
- build
|
||||
- build-cli
|
||||
- build-edge
|
||||
format: tar.gz
|
||||
format_overrides:
|
||||
- goos: windows
|
||||
format: zip
|
||||
name_template: "n9e-v{{ .Version }}-{{ .Os }}-{{ .Arch }}"
|
||||
wrap_in_directory: false
|
||||
files:
|
||||
- docker/*
|
||||
- etc/*
|
||||
- integrations/*
|
||||
- cli/*
|
||||
- n9e.sql
|
||||
|
||||
release:
|
||||
github:
|
||||
owner: ccfos
|
||||
name: nightingale
|
||||
name_template: "v{{ .Version }}"
|
||||
|
||||
dockers:
|
||||
- image_templates:
|
||||
- flashcatcloud/nightingale:{{ .Version }}-amd64
|
||||
goos: linux
|
||||
goarch: amd64
|
||||
ids:
|
||||
- build
|
||||
dockerfile: docker/Dockerfile.goreleaser
|
||||
extra_files:
|
||||
- etc
|
||||
- integrations
|
||||
use: buildx
|
||||
build_flag_templates:
|
||||
- "--platform=linux/amd64"
|
||||
- image_templates:
|
||||
- flashcatcloud/nightingale:{{ .Version }}-arm64v8
|
||||
goos: linux
|
||||
goarch: arm64
|
||||
ids:
|
||||
- build
|
||||
dockerfile: docker/Dockerfile.goreleaser.arm64
|
||||
extra_files:
|
||||
- etc
|
||||
- integrations
|
||||
use: buildx
|
||||
build_flag_templates:
|
||||
- "--platform=linux/arm64/v8"
|
||||
|
||||
docker_manifests:
|
||||
|
||||
- name_template: flashcatcloud/nightingale:{{ .Version }}
|
||||
image_templates:
|
||||
- flashcatcloud/nightingale:{{ .Version }}-amd64
|
||||
- flashcatcloud/nightingale:{{ .Version }}-arm64v8
|
||||
|
||||
- name_template: flashcatcloud/nightingale:latest
|
||||
image_templates:
|
||||
- flashcatcloud/nightingale:{{ .Version }}-amd64
|
||||
- flashcatcloud/nightingale:{{ .Version }}-arm64v8
|
||||
11
Dockerfile
11
Dockerfile
@@ -1,11 +0,0 @@
|
||||
FROM golang:1.13
|
||||
|
||||
LABEL maintainer="llitfkitfk@gmail.com,chenjiandongx@qq.com"
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN apt-get update && apt-get install net-tools -y
|
||||
|
||||
COPY . .
|
||||
RUN ./control build docker
|
||||
RUN mv /app/bin/* /usr/local/bin
|
||||
2
LICENSE
2
LICENSE
@@ -430,4 +430,4 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
||||
See the License for the specific language governing permissions and
|
||||
|
||||
limitations under the License.
|
||||
limitations under the License.
|
||||
41
Makefile
Normal file
41
Makefile
Normal file
@@ -0,0 +1,41 @@
|
||||
.PHONY: prebuild build
|
||||
|
||||
ROOT:=$(shell pwd -P)
|
||||
GIT_COMMIT:=$(shell git --work-tree ${ROOT} rev-parse 'HEAD^{commit}')
|
||||
_GIT_VERSION:=$(shell git --work-tree ${ROOT} describe --tags --abbrev=14 "${GIT_COMMIT}^{commit}" 2>/dev/null)
|
||||
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."
|
||||
|
||||
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
|
||||
|
||||
build-pushgw:
|
||||
go build -ldflags "-w -s -X github.com/ccfos/nightingale/v6/pkg/version.Version=$(RELEASE_VERSION)" -o n9e-pushgw ./cmd/pushgw/main.go
|
||||
|
||||
build-cli:
|
||||
go build -ldflags "-w -s -X github.com/ccfos/nightingale/v6/pkg/version.Version=$(RELEASE_VERSION)" -o n9e-cli ./cmd/cli/main.go
|
||||
|
||||
run:
|
||||
nohup ./n9e > n9e.log 2>&1 &
|
||||
|
||||
run-alert:
|
||||
nohup ./n9e-alert > n9e-alert.log 2>&1 &
|
||||
|
||||
run-pushgw:
|
||||
nohup ./n9e-pushgw > n9e-pushgw.log 2>&1 &
|
||||
|
||||
release:
|
||||
goreleaser --skip-validate --skip-publish --snapshot
|
||||
140
README.md
140
README.md
@@ -1,54 +1,104 @@
|
||||
<img src="https://s3-gz01.didistatic.com/n9e-pub/image/n9e-logo-bg-white.png" width="200" alt="Nightingale"/>
|
||||
<br>
|
||||
<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>
|
||||
</p>
|
||||
|
||||
[中文简介](README_ZH.md)
|
||||
<p align="center">
|
||||
<img alt="GitHub latest release" src="https://img.shields.io/github/v/release/ccfos/nightingale"/>
|
||||
<a href="https://n9e.github.io">
|
||||
<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>
|
||||
<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">
|
||||
An open-source cloud-native monitoring system that is <b>all-in-one</b> <br/>
|
||||
<b>Out-of-the-box</b>, it integrates data collection, visualization, and monitoring alert <br/>
|
||||
We recommend upgrading your <b>Prometheus + AlertManager + Grafana</b> combination to Nightingale!
|
||||
</p>
|
||||
|
||||
Nightingale is a fork of Open-Falcon, and all the core modules have been greatly optimized. It integrates the best practices of DiDi. You can think of it as the next generation of Open-Falcon, and use directly in production environment.
|
||||
|
||||
## Documentation
|
||||
|
||||
Nightingale user manual: [https://n9e.didiyun.com/](https://n9e.didiyun.com/)
|
||||
|
||||
## Compile
|
||||
|
||||
```bash
|
||||
mkdir -p $GOPATH/src/github.com/didi
|
||||
cd $GOPATH/src/github.com/didi
|
||||
git clone https://github.com/didi/nightingale.git
|
||||
cd nightingale
|
||||
./control build
|
||||
```
|
||||
|
||||
## Quickstart with Docker
|
||||
|
||||
We has offered a Docker demo for the users who want to give it a try. Before you get started, make sure you have installed **Docker** & **docker-compose** and there are some details you should know.
|
||||
|
||||
* We highly recommend users prepare a new VM environment to use it.
|
||||
* All the core components will be installed on your OS according to the `docker-compose.yaml`.
|
||||
* Nightingale will use the following ports, `80`, `5800`, `5810`, `5811`, `5820`, `5821`, `5830`, `5831`, `5840`, `5841`, `6379`, `2058`, `3306`.
|
||||
|
||||
Okay. Run it! Once the docker finish its jobs, visits http://your-env-ip in your broswer. Default username and password is `root:root`.
|
||||
```bash
|
||||
$ docker-compose up -d
|
||||
```
|
||||
|
||||

|
||||
|
||||
## Upgrading
|
||||
If upgrade `version<1.4.0` to `v1.4.0`, follow the operating instructions in [v1.4.0](https://github.com/didi/nightingale/releases/tag/V1.4.0) release
|
||||
[English](./README.md) | [中文](./README_zh.md)
|
||||
|
||||
|
||||
## Team
|
||||
## Highlighted Features
|
||||
|
||||
[ulricqin](https://github.com/ulricqin) [710leo](https://github.com/710leo) [jsers](https://github.com/jsers) [hujter](https://github.com/hujter) [n4mine](https://github.com/n4mine) [heli567](https://github.com/heli567)
|
||||
- **Out-of-the-box**
|
||||
- Supports multiple deployment methods such as **Docker, Helm Chart, and cloud services**, integrates data collection, monitoring, and alerting into one system, and comes with various monitoring dashboards, quick views, and alert rule templates. **It greatly reduces the construction cost, learning cost, and usage cost of cloud-native monitoring systems**.
|
||||
- **Professional Alerting**
|
||||
- Provides visual alert configuration and management, supports various alert rules, offers the ability to configure silence and subscription rules, supports multiple alert delivery channels, and has features such as alert self-healing and event management.
|
||||
- **Cloud-Native**
|
||||
- Quickly builds an enterprise-level cloud-native monitoring system through a turnkey approach, supports multiple collectors such as [Categraf](https://github.com/flashcatcloud/categraf), Telegraf, and Grafana-agent, supports multiple data sources such as Prometheus, VictoriaMetrics, M3DB, ElasticSearch, and Jaeger, and is compatible with importing Grafana dashboards. **It seamlessly integrates with the cloud-native ecosystem**.
|
||||
- **High Performance and High Availability**
|
||||
- Due to the multi-data-source management engine of Nightingale and its excellent architecture design, and utilizing a high-performance time-series database, it can handle data collection, storage, and alert analysis scenarios with billions of time-series data, saving a lot of costs.
|
||||
- Nightingale components can be horizontally scaled with no single point of failure. It has been deployed in thousands of enterprises and tested in harsh production practices. Many leading Internet companies have used Nightingale for cluster machines with hundreds of nodes, processing billions of time-series data.
|
||||
- **Flexible Extension and Centralized Management**
|
||||
- Nightingale can be deployed on a 1-core 1G cloud host, deployed in a cluster of hundreds of machines, or run in Kubernetes. Time-series databases, alert engines, and other components can also be decentralized to various data centers and regions, balancing edge deployment with centralized management. **It solves the problem of data fragmentation and lack of unified views**.
|
||||
|
||||
## Community
|
||||
|
||||
Nightingale is developed in open. Here we set up an organization, [github.com/n9e](https://github.com/n9e), which is used to communicate and contribute. We sincerely hope more developers can use their creativity to make lots of related projects for the Nightingale ecosystem.
|
||||
#### If you are using Prometheus and have one or more of the following requirement scenarios, it is recommended that you upgrade to Nightingale:
|
||||
|
||||
- Multiple systems such as Prometheus, Alertmanager, Grafana, etc. are fragmented and lack a unified view and cannot be used out of the box;
|
||||
- The way to manage Prometheus and Alertmanager by modifying configuration files has a big learning curve and is difficult to collaborate;
|
||||
- Too much data to scale-up your Prometheus cluster;
|
||||
- Multiple Prometheus clusters running in production environments, which faced high management and usage costs;
|
||||
|
||||
#### If you are using Zabbix and have the following scenarios, it is recommended that you upgrade to Nightingale:
|
||||
|
||||
- Monitoring too much data and wanting a better scalable solution;
|
||||
- A high learning curve and a desire for better efficiency of collaborative use in a multi-person, multi-team model;
|
||||
- Microservice and cloud-native architectures with variable monitoring data lifecycles and high monitoring data dimension bases, which are not easily adaptable to the Zabbix data model;
|
||||
|
||||
|
||||
#### If you are using [open-falcon](https://github.com/open-falcon/falcon-plus), we recommend you to upgrade to Nightingale:
|
||||
- For more information about open-falcon and Nightingale, please refer to read [Ten features and trends of cloud-native monitoring](https://mp.weixin.qq.com/s?__biz=MzkzNjI5OTM5Nw==&mid=2247483738&idx=1&sn=e8bdbb974a2cd003c1abcc2b5405dd18&chksm=c2a19fb0f5d616a63185cd79277a79a6b80118ef2185890d0683d2bb20451bd9303c78d083c5#rd)。
|
||||
|
||||
## Getting Started
|
||||
|
||||
[https://n9e.github.io/](https://n9e.github.io/)
|
||||
|
||||
## Screenshots
|
||||
|
||||
https://user-images.githubusercontent.com/792850/216888712-2565fcea-9df5-47bd-a49e-d60af9bd76e8.mp4
|
||||
|
||||
## Architecture
|
||||
|
||||
<img src="doc/img/arch-product.png" width="600">
|
||||
|
||||
Nightingale monitoring can receive monitoring data reported by various collectors (such as [Categraf](https://github.com/flashcatcloud/categraf) , telegraf, grafana-agent, Prometheus, etc.) and write them to various popular time-series databases (such as Prometheus, M3DB, VictoriaMetrics, Thanos, TDEngine, etc.). It provides configuration capabilities for alert rules, silence rules, and subscription rules, as well as the ability to view monitoring data. It also provides automatic alarm self-healing mechanisms (such as automatically calling back to a webhook address or executing a script after an alarm is triggered), and the ability to store and manage historical alarm events and view them in groups.
|
||||
|
||||
If the performance of a standalone time-series database (such as Prometheus) has bottlenecks or poor disaster recovery, we recommend using [VictoriaMetrics](https://github.com/VictoriaMetrics/VictoriaMetrics). The VictoriaMetrics architecture is relatively simple, has excellent performance, and is easy to deploy and maintain. The architecture diagram is as shown above. For more detailed documentation on VictoriaMetrics, please refer to its [official website](https://victoriametrics.com/).
|
||||
|
||||
**We welcome you to participate in the Nightingale open-source project and community in various ways, including but not limited to**:
|
||||
- Adding and improving documentation => [n9e.github.io](https://n9e.github.io/)
|
||||
- Sharing your best practices and experience in using Nightingale monitoring => [Article sharing]((https://n9e.github.io/docs/prologue/share/))
|
||||
- Submitting product suggestions => [github issue](https://github.com/ccfos/nightingale/issues/new?assignees=&labels=kind%2Ffeature&template=enhancement.md)
|
||||
- Submitting code to make Nightingale monitoring faster, more stable, and easier to use => [github pull request](https://github.com/didi/nightingale/pulls)
|
||||
|
||||
|
||||
**Respecting, recognizing, and recording the work of every contributor** is the first guiding principle of the Nightingale open-source community. We advocate effective questioning, which not only respects the developer's time but also contributes to the accumulation of knowledge in the entire community
|
||||
- Before asking a question, please first refer to the [FAQ](https://www.gitlink.org.cn/ccfos/nightingale/wiki/faq)
|
||||
- We use [GitHub Discussions](https://github.com/ccfos/nightingale/discussions) as the communication forum. You can search and ask questions here.
|
||||
- We also recommend that you join ours [Slack channel](https://n9e-talk.slack.com/) to exchange experiences with other Nightingale users.
|
||||
|
||||
|
||||
## Who is using Nightingale
|
||||
You can register your usage and share your experience by posting on **[Who is Using Nightingale](https://github.com/ccfos/nightingale/issues/897)**.
|
||||
|
||||
## Stargazers over time
|
||||
[](https://starchart.cc/ccfos/nightingale)
|
||||
|
||||
## Contributors
|
||||
<a href="https://github.com/ccfos/nightingale/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=ccfos/nightingale" />
|
||||
</a>
|
||||
|
||||
## License
|
||||
|
||||
<img alt="Apache-2.0 license" src="https://s3-gz01.didistatic.com/n9e-pub/image/apache.jpeg" width="128">
|
||||
|
||||
Nightingale is available under the Apache-2.0 license. See the [LICENSE](LICENSE) file for more info.
|
||||
|
||||
[Apache License V2.0](https://github.com/didi/nightingale/blob/main/LICENSE)
|
||||
52
README_ZH.md
52
README_ZH.md
@@ -1,52 +0,0 @@
|
||||
<img src="https://s3-gz01.didistatic.com/n9e-pub/image/n9e-logo-bg-white.png" width="200" alt="Nightingale"/>
|
||||
<br>
|
||||
|
||||
[English Introduction](README.md)
|
||||
|
||||
Nightingale 是一套衍生自 Open-Falcon 的互联网监控解决方案,融入了部分滴滴生产环境的最佳实践,灵活易用,稳定可靠,是一个生产环境直接可用的版本 :-)
|
||||
|
||||
## 文档
|
||||
|
||||
使用手册请参考:[夜莺使用手册](https://n9e.didiyun.com/)
|
||||
|
||||
## 编译
|
||||
|
||||
```bash
|
||||
mkdir -p $GOPATH/src/github.com/didi
|
||||
cd $GOPATH/src/github.com/didi
|
||||
git clone https://github.com/didi/nightingale.git
|
||||
cd nightingale
|
||||
./control build
|
||||
```
|
||||
|
||||
## 快速开始
|
||||
|
||||
使用 docker 和 docker-compose 环境可以快速部署一整套 nightingale 系统,涵盖了所有的核心组件。
|
||||
|
||||
* 强烈建议使用一个新的虚拟环境来部署和测试这个系统。
|
||||
* 系统组件占用了以下端口,`80`, `5800`, `5810`, `5811`, `5820`, `5821`, `5830`, `5831`, `5840`, `5841`, `6379`, `2058`, `3306`,部署前请确保这些端口没有被使用。
|
||||
|
||||
|
||||
使用 docker-compose 一键构建部署,完成以后可以使用浏览器打开 http://your-env-ip。 默认的登录账号密码均为 `root`。
|
||||
```bash
|
||||
$ docker-compose up -d
|
||||
```
|
||||
|
||||

|
||||
|
||||
## 版本升级
|
||||
如果需要从 `v1.4.0` 之前的版本升级到 `v1.4.0` , 按照 [v1.4.0](https://github.com/didi/nightingale/releases/tag/V1.4.0) release 说明操作即可
|
||||
|
||||
## 团队
|
||||
|
||||
[ulricqin](https://github.com/ulricqin) [710leo](https://github.com/710leo) [jsers](https://github.com/jsers) [hujter](https://github.com/hujter) [n4mine](https://github.com/n4mine) [heli567](https://github.com/heli567)
|
||||
|
||||
## 社区
|
||||
|
||||
[github.com/n9e](https://github.com/n9e) 是为夜莺所创建的 Organization,用于收集和开发夜莺周边项目。
|
||||
|
||||
## License
|
||||
|
||||
<img alt="Apache-2.0 license" src="https://s3-gz01.didistatic.com/n9e-pub/image/apache.jpeg" width="128">
|
||||
|
||||
Nightingale 基于 Apache-2.0 许可证进行分发和使用,更多信息参见 [LICENSE](LICENSE)。
|
||||
74
README_zh.md
Normal file
74
README_zh.md
Normal file
@@ -0,0 +1,74 @@
|
||||
<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>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<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>
|
||||
<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">
|
||||
<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 forks" src="https://img.shields.io/github/forks/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>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
告警管理专家,一体化的开源可观测平台
|
||||
</p>
|
||||
|
||||
[English](./README.md) | [中文](./README_zh.md)
|
||||
|
||||
夜莺Nightingale是中国计算机学会托管的开源云原生可观测工具,最早由滴滴于 2020 年孵化并开源,并于 2022 年正式捐赠予中国计算机学会。夜莺采用 All-in-One 的设计理念,集数据采集、可视化、监控告警、数据分析于一体,与云原生生态紧密集成,融入了顶级互联网公司可观测性最佳实践,沉淀了众多社区专家经验,开箱即用。
|
||||
|
||||
## 资料
|
||||
|
||||
- 文档:[flashcat.cloud/docs](https://flashcat.cloud/docs/)
|
||||
- 提问:[answer.flashcat.cloud](https://answer.flashcat.cloud/)
|
||||
- 报Bug:[github.com/ccfos/nightingale/issues](https://github.com/ccfos/nightingale/issues/new?assignees=&labels=kind%2Fbug&projects=&template=bug_report.yml)
|
||||
|
||||
|
||||
## 功能和特点
|
||||
|
||||
- 统一接入各种时序库:支持对接 Prometheus、VictoriaMetrics、Thanos、Mimir、M3DB 等多种时序库,实现统一告警管理
|
||||
- 专业告警能力:内置支持多种告警规则,可以扩展支持所有通知媒介,支持告警屏蔽、告警抑制、告警自愈、告警事件管理
|
||||
- 高性能可视化引擎:支持多种图表样式,内置众多Dashboard模版,也可导入Grafana模版,开箱即用,开源协议商业友好
|
||||
- 无缝搭配 [Flashduty](https://flashcat.cloud/product/flashcat-duty/):实现告警聚合收敛、认领、升级、排班、IM集成,确保告警处理不遗漏,减少打扰,更好协同
|
||||
- 支持所有常见采集器:支持 [Categraf](https://flashcat.cloud/product/categraf)、telegraf、grafana-agent、datadog-agent、各种 exporter 作为采集器,没有什么数据是不能监控的
|
||||
- 一体化观测平台:从 v6 版本开始,支持接入 ElasticSearch、Jaeger 数据源,实现日志、链路、指标多维度的统一可观测
|
||||
|
||||
|
||||
## 产品演示
|
||||
|
||||

|
||||
|
||||
## 部署架构
|
||||
|
||||

|
||||
|
||||
## 加入交流群
|
||||
|
||||
欢迎加入 QQ 交流群,群号:479290895,QQ 群适合群友互助,夜莺研发人员通常不在群里。如果要报 bug 请到[这里](https://github.com/ccfos/nightingale/issues/new?assignees=&labels=kind%2Fbug&projects=&template=bug_report.yml),提问到[这里](https://answer.flashcat.cloud/)。
|
||||
|
||||
## Stargazers over time
|
||||
|
||||
[](https://star-history.com/#ccfos/nightingale&Date)
|
||||
|
||||
|
||||
## Contributors
|
||||
<a href="https://github.com/ccfos/nightingale/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=ccfos/nightingale" />
|
||||
</a>
|
||||
|
||||
## 社区治理
|
||||
[夜莺开源项目和社区治理架构(草案)](./doc/community-governance.md)
|
||||
|
||||
## License
|
||||
[Apache License V2.0](https://github.com/didi/nightingale/blob/main/LICENSE)
|
||||
76
alert/aconf/conf.go
Normal file
76
alert/aconf/conf.go
Normal file
@@ -0,0 +1,76 @@
|
||||
package aconf
|
||||
|
||||
import (
|
||||
"path"
|
||||
)
|
||||
|
||||
type Alert struct {
|
||||
Disable bool
|
||||
EngineDelay int64
|
||||
Heartbeat HeartbeatConfig
|
||||
Alerting Alerting
|
||||
}
|
||||
|
||||
type SMTPConfig struct {
|
||||
Host string
|
||||
Port int
|
||||
User string
|
||||
Pass string
|
||||
From string
|
||||
InsecureSkipVerify bool
|
||||
Batch int
|
||||
}
|
||||
|
||||
type HeartbeatConfig struct {
|
||||
IP string
|
||||
Interval int64
|
||||
Endpoint string
|
||||
EngineName string
|
||||
}
|
||||
|
||||
type Alerting struct {
|
||||
Timeout int64
|
||||
TemplatesDir string
|
||||
NotifyConcurrency int
|
||||
}
|
||||
|
||||
type CallPlugin struct {
|
||||
Enable bool
|
||||
PluginPath string
|
||||
Caller string
|
||||
}
|
||||
|
||||
type RedisPub struct {
|
||||
Enable bool
|
||||
ChannelPrefix string
|
||||
ChannelKey string
|
||||
}
|
||||
|
||||
type Ibex struct {
|
||||
Address string
|
||||
BasicAuthUser string
|
||||
BasicAuthPass string
|
||||
Timeout int64
|
||||
}
|
||||
|
||||
func (a *Alert) PreCheck(configDir string) {
|
||||
if a.Alerting.TemplatesDir == "" {
|
||||
a.Alerting.TemplatesDir = path.Join(configDir, "template")
|
||||
}
|
||||
|
||||
if a.Alerting.NotifyConcurrency == 0 {
|
||||
a.Alerting.NotifyConcurrency = 10
|
||||
}
|
||||
|
||||
if a.Heartbeat.Interval == 0 {
|
||||
a.Heartbeat.Interval = 1000
|
||||
}
|
||||
|
||||
if a.Heartbeat.EngineName == "" {
|
||||
a.Heartbeat.EngineName = "default"
|
||||
}
|
||||
|
||||
if a.EngineDelay == 0 {
|
||||
a.EngineDelay = 30
|
||||
}
|
||||
}
|
||||
98
alert/alert.go
Normal file
98
alert/alert.go
Normal file
@@ -0,0 +1,98 @@
|
||||
package alert
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/ccfos/nightingale/v6/alert/aconf"
|
||||
"github.com/ccfos/nightingale/v6/alert/astats"
|
||||
"github.com/ccfos/nightingale/v6/alert/dispatch"
|
||||
"github.com/ccfos/nightingale/v6/alert/eval"
|
||||
"github.com/ccfos/nightingale/v6/alert/naming"
|
||||
"github.com/ccfos/nightingale/v6/alert/process"
|
||||
"github.com/ccfos/nightingale/v6/alert/queue"
|
||||
"github.com/ccfos/nightingale/v6/alert/record"
|
||||
"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"
|
||||
"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/pconf"
|
||||
"github.com/ccfos/nightingale/v6/pushgw/writer"
|
||||
"github.com/ccfos/nightingale/v6/tdengine"
|
||||
)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
ctx := ctx.NewContext(context.Background(), nil, false, config.CenterApi)
|
||||
|
||||
syncStats := memsto.NewSyncStats()
|
||||
alertStats := astats.NewSyncStats()
|
||||
|
||||
targetCache := memsto.NewTargetCache(ctx, syncStats, nil)
|
||||
busiGroupCache := memsto.NewBusiGroupCache(ctx, syncStats)
|
||||
alertMuteCache := memsto.NewAlertMuteCache(ctx, syncStats)
|
||||
alertRuleCache := memsto.NewAlertRuleCache(ctx, syncStats)
|
||||
notifyConfigCache := memsto.NewNotifyConfigCache(ctx)
|
||||
dsCache := memsto.NewDatasourceCache(ctx, syncStats)
|
||||
userCache := memsto.NewUserCache(ctx, syncStats)
|
||||
userGroupCache := memsto.NewUserGroupCache(ctx, syncStats)
|
||||
|
||||
promClients := prom.NewPromClient(ctx, config.Alert.Heartbeat)
|
||||
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, tdengineClients, userCache, userGroupCache)
|
||||
|
||||
r := httpx.GinEngine(config.Global.RunMode, config.HTTP)
|
||||
rt := router.New(config.HTTP, config.Alert, alertMuteCache, targetCache, busiGroupCache, alertStats, ctx, externalProcessors)
|
||||
rt.Config(r)
|
||||
dumper.ConfigRouter(r)
|
||||
|
||||
httpClean := httpx.Init(config.HTTP, r)
|
||||
|
||||
return func() {
|
||||
logxClean()
|
||||
httpClean()
|
||||
}, nil
|
||||
}
|
||||
|
||||
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, tdendgineClients *tdengine.TdengineClientMap, userCache *memsto.UserCacheType, userGroupCache *memsto.UserGroupCacheType) {
|
||||
alertSubscribeCache := memsto.NewAlertSubscribeCache(ctx, syncStats)
|
||||
recordingRuleCache := memsto.NewRecordingRuleCache(ctx, syncStats)
|
||||
|
||||
go models.InitNotifyConfig(ctx, alertc.Alerting.TemplatesDir)
|
||||
|
||||
naming := naming.NewNaming(ctx, alertc.Heartbeat)
|
||||
|
||||
writers := writer.NewWriters(pushgwc)
|
||||
record.NewScheduler(alertc, recordingRuleCache, promClients, writers, alertStats)
|
||||
|
||||
eval.NewScheduler(alertc, externalProcessors, alertRuleCache, targetCache, busiGroupCache, alertMuteCache, datasourceCache, promClients, tdendgineClients, naming, ctx, alertStats)
|
||||
|
||||
dp := dispatch.NewDispatch(alertRuleCache, userCache, userGroupCache, alertSubscribeCache, targetCache, notifyConfigCache, alertc.Alerting, ctx, alertStats)
|
||||
consumer := dispatch.NewConsumer(alertc.Alerting, ctx, dp)
|
||||
|
||||
go dp.ReloadTpls()
|
||||
go consumer.LoopConsume()
|
||||
|
||||
go queue.ReportQueueSize(alertStats)
|
||||
go sender.InitEmailSender(notifyConfigCache.GetSMTP())
|
||||
}
|
||||
113
alert/astats/stats.go
Normal file
113
alert/astats/stats.go
Normal file
@@ -0,0 +1,113 @@
|
||||
package astats
|
||||
|
||||
import (
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
const (
|
||||
namespace = "n9e"
|
||||
subsystem = "alert"
|
||||
)
|
||||
|
||||
type Stats struct {
|
||||
AlertNotifyTotal *prometheus.CounterVec
|
||||
AlertNotifyErrorTotal *prometheus.CounterVec
|
||||
CounterAlertsTotal *prometheus.CounterVec
|
||||
GaugeAlertQueueSize prometheus.Gauge
|
||||
CounterRuleEval *prometheus.CounterVec
|
||||
CounterQueryDataErrorTotal *prometheus.CounterVec
|
||||
CounterRecordEval *prometheus.CounterVec
|
||||
CounterRecordEvalErrorTotal *prometheus.CounterVec
|
||||
CounterMuteTotal *prometheus.CounterVec
|
||||
}
|
||||
|
||||
func NewSyncStats() *Stats {
|
||||
CounterRuleEval := prometheus.NewCounterVec(prometheus.CounterOpts{
|
||||
Namespace: namespace,
|
||||
Subsystem: subsystem,
|
||||
Name: "rule_eval_total",
|
||||
Help: "Number of rule eval.",
|
||||
}, []string{})
|
||||
|
||||
CounterRecordEval := prometheus.NewCounterVec(prometheus.CounterOpts{
|
||||
Namespace: namespace,
|
||||
Subsystem: subsystem,
|
||||
Name: "record_eval_total",
|
||||
Help: "Number of record eval.",
|
||||
}, []string{})
|
||||
|
||||
CounterRecordEvalErrorTotal := prometheus.NewCounterVec(prometheus.CounterOpts{
|
||||
Namespace: namespace,
|
||||
Subsystem: subsystem,
|
||||
Name: "record_eval_error_total",
|
||||
Help: "Number of record eval error.",
|
||||
}, []string{})
|
||||
|
||||
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{
|
||||
Namespace: namespace,
|
||||
Subsystem: subsystem,
|
||||
Name: "alerts_total",
|
||||
Help: "Total number alert events.",
|
||||
}, []string{"cluster", "type", "busi_group"})
|
||||
|
||||
// 内存中的告警事件队列的长度
|
||||
GaugeAlertQueueSize := prometheus.NewGauge(prometheus.GaugeOpts{
|
||||
Namespace: namespace,
|
||||
Subsystem: subsystem,
|
||||
Name: "alert_queue_size",
|
||||
Help: "The size of alert queue.",
|
||||
})
|
||||
|
||||
CounterQueryDataErrorTotal := prometheus.NewCounterVec(prometheus.CounterOpts{
|
||||
Namespace: namespace,
|
||||
Subsystem: subsystem,
|
||||
Name: "query_data_error_total",
|
||||
Help: "Number of query data error.",
|
||||
}, []string{"datasource"})
|
||||
|
||||
CounterMuteTotal := prometheus.NewCounterVec(prometheus.CounterOpts{
|
||||
Namespace: namespace,
|
||||
Subsystem: subsystem,
|
||||
Name: "mute_total",
|
||||
Help: "Number of mute.",
|
||||
}, []string{"group"})
|
||||
|
||||
prometheus.MustRegister(
|
||||
CounterAlertsTotal,
|
||||
GaugeAlertQueueSize,
|
||||
AlertNotifyTotal,
|
||||
AlertNotifyErrorTotal,
|
||||
CounterRuleEval,
|
||||
CounterQueryDataErrorTotal,
|
||||
CounterRecordEval,
|
||||
CounterRecordEvalErrorTotal,
|
||||
CounterMuteTotal,
|
||||
)
|
||||
|
||||
return &Stats{
|
||||
CounterAlertsTotal: CounterAlertsTotal,
|
||||
GaugeAlertQueueSize: GaugeAlertQueueSize,
|
||||
AlertNotifyTotal: AlertNotifyTotal,
|
||||
AlertNotifyErrorTotal: AlertNotifyErrorTotal,
|
||||
CounterRuleEval: CounterRuleEval,
|
||||
CounterQueryDataErrorTotal: CounterQueryDataErrorTotal,
|
||||
CounterRecordEval: CounterRecordEval,
|
||||
CounterRecordEvalErrorTotal: CounterRecordEvalErrorTotal,
|
||||
CounterMuteTotal: CounterMuteTotal,
|
||||
}
|
||||
}
|
||||
111
alert/common/conv.go
Normal file
111
alert/common/conv.go
Normal file
@@ -0,0 +1,111 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"strings"
|
||||
|
||||
"github.com/prometheus/common/model"
|
||||
)
|
||||
|
||||
type AnomalyPoint struct {
|
||||
Key string `json:"key"`
|
||||
Labels model.Metric `json:"labels"`
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
Value float64 `json:"value"`
|
||||
Severity int `json:"severity"`
|
||||
Triggered bool `json:"triggered"`
|
||||
Query string `json:"query"`
|
||||
}
|
||||
|
||||
func NewAnomalyPoint(key string, labels map[string]string, ts int64, value float64, severity int) AnomalyPoint {
|
||||
anomalyPointLabels := make(model.Metric)
|
||||
for k, v := range labels {
|
||||
anomalyPointLabels[model.LabelName(k)] = model.LabelValue(v)
|
||||
}
|
||||
anomalyPointLabels[model.MetricNameLabel] = model.LabelValue(key)
|
||||
return AnomalyPoint{
|
||||
Key: key,
|
||||
Labels: anomalyPointLabels,
|
||||
Timestamp: ts,
|
||||
Value: value,
|
||||
Severity: severity,
|
||||
}
|
||||
}
|
||||
|
||||
func (v *AnomalyPoint) ReadableValue() string {
|
||||
ret := fmt.Sprintf("%.5f", v.Value)
|
||||
ret = strings.TrimRight(ret, "0")
|
||||
return strings.TrimRight(ret, ".")
|
||||
}
|
||||
|
||||
func ConvertAnomalyPoints(value model.Value) (lst []AnomalyPoint) {
|
||||
if value == nil {
|
||||
return
|
||||
}
|
||||
|
||||
switch value.Type() {
|
||||
case model.ValVector:
|
||||
items, ok := value.(model.Vector)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
for _, item := range items {
|
||||
if math.IsNaN(float64(item.Value)) {
|
||||
continue
|
||||
}
|
||||
|
||||
lst = append(lst, AnomalyPoint{
|
||||
Key: item.Metric.String(),
|
||||
Timestamp: item.Timestamp.Unix(),
|
||||
Value: float64(item.Value),
|
||||
Labels: item.Metric,
|
||||
})
|
||||
}
|
||||
case model.ValMatrix:
|
||||
items, ok := value.(model.Matrix)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
for _, item := range items {
|
||||
if len(item.Values) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
last := item.Values[len(item.Values)-1]
|
||||
|
||||
if math.IsNaN(float64(last.Value)) {
|
||||
continue
|
||||
}
|
||||
|
||||
lst = append(lst, AnomalyPoint{
|
||||
Key: item.Metric.String(),
|
||||
Labels: item.Metric,
|
||||
Timestamp: last.Timestamp.Unix(),
|
||||
Value: float64(last.Value),
|
||||
})
|
||||
}
|
||||
case model.ValScalar:
|
||||
item, ok := value.(*model.Scalar)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
if math.IsNaN(float64(item.Value)) {
|
||||
return
|
||||
}
|
||||
|
||||
lst = append(lst, AnomalyPoint{
|
||||
Key: "{}",
|
||||
Timestamp: item.Timestamp.Unix(),
|
||||
Value: float64(item.Value),
|
||||
Labels: model.Metric{},
|
||||
})
|
||||
default:
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
53
alert/common/key.go
Normal file
53
alert/common/key.go
Normal file
@@ -0,0 +1,53 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/ccfos/nightingale/v6/models"
|
||||
)
|
||||
|
||||
func RuleKey(datasourceId, id int64) string {
|
||||
return fmt.Sprintf("alert-%d-%d", datasourceId, id)
|
||||
}
|
||||
|
||||
func MatchTags(eventTagsMap map[string]string, itags []models.TagFilter) bool {
|
||||
for _, filter := range itags {
|
||||
value, has := eventTagsMap[filter.Key]
|
||||
if !has {
|
||||
return false
|
||||
}
|
||||
if !matchTag(value, filter) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
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 {
|
||||
case "==":
|
||||
return filter.Value == value
|
||||
case "!=":
|
||||
return filter.Value != value
|
||||
case "in":
|
||||
_, has := filter.Vset[value]
|
||||
return has
|
||||
case "not in":
|
||||
_, has := filter.Vset[value]
|
||||
return !has
|
||||
case "=~":
|
||||
return filter.Regexp.MatchString(value)
|
||||
case "!~":
|
||||
return !filter.Regexp.MatchString(value)
|
||||
}
|
||||
// unexpect func
|
||||
return false
|
||||
}
|
||||
110
alert/dispatch/consume.go
Normal file
110
alert/dispatch/consume.go
Normal file
@@ -0,0 +1,110 @@
|
||||
package dispatch
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/ccfos/nightingale/v6/alert/aconf"
|
||||
"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"
|
||||
|
||||
"github.com/toolkits/pkg/concurrent/semaphore"
|
||||
"github.com/toolkits/pkg/logger"
|
||||
)
|
||||
|
||||
type Consumer struct {
|
||||
alerting aconf.Alerting
|
||||
ctx *ctx.Context
|
||||
|
||||
dispatch *Dispatch
|
||||
}
|
||||
|
||||
// 创建一个 Consumer 实例
|
||||
func NewConsumer(alerting aconf.Alerting, ctx *ctx.Context, dispatch *Dispatch) *Consumer {
|
||||
return &Consumer{
|
||||
alerting: alerting,
|
||||
ctx: ctx,
|
||||
dispatch: dispatch,
|
||||
}
|
||||
}
|
||||
|
||||
func (e *Consumer) LoopConsume() {
|
||||
sema := semaphore.NewSemaphore(e.alerting.NotifyConcurrency)
|
||||
duration := time.Duration(100) * time.Millisecond
|
||||
for {
|
||||
events := queue.EventQueue.PopBackBy(100)
|
||||
if len(events) == 0 {
|
||||
time.Sleep(duration)
|
||||
continue
|
||||
}
|
||||
e.consume(events, sema)
|
||||
}
|
||||
}
|
||||
|
||||
func (e *Consumer) consume(events []interface{}, sema *semaphore.Semaphore) {
|
||||
for i := range events {
|
||||
if events[i] == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
event := events[i].(*models.AlertCurEvent)
|
||||
sema.Acquire()
|
||||
go func(event *models.AlertCurEvent) {
|
||||
defer sema.Release()
|
||||
e.consumeOne(event)
|
||||
}(event)
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
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 {
|
||||
event.Annotations = fmt.Sprintf("failed to parse rule note: %v", err)
|
||||
}
|
||||
|
||||
e.persist(event)
|
||||
|
||||
if event.IsRecovered && event.NotifyRecovered == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
e.dispatch.HandleEventNotify(event, false)
|
||||
}
|
||||
|
||||
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)
|
||||
if err != nil {
|
||||
logger.Errorf("event%+v persist err:%v", event, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
err := models.EventPersist(e.ctx, event)
|
||||
if err != nil {
|
||||
logger.Errorf("event%+v persist err:%v", event, err)
|
||||
}
|
||||
}
|
||||
305
alert/dispatch/dispatch.go
Normal file
305
alert/dispatch/dispatch.go
Normal file
@@ -0,0 +1,305 @@
|
||||
package dispatch
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"html/template"
|
||||
"strconv"
|
||||
"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"
|
||||
"github.com/ccfos/nightingale/v6/models"
|
||||
"github.com/ccfos/nightingale/v6/pkg/ctx"
|
||||
|
||||
"github.com/toolkits/pkg/logger"
|
||||
)
|
||||
|
||||
type Dispatch struct {
|
||||
alertRuleCache *memsto.AlertRuleCacheType
|
||||
userCache *memsto.UserCacheType
|
||||
userGroupCache *memsto.UserGroupCacheType
|
||||
alertSubscribeCache *memsto.AlertSubscribeCacheType
|
||||
targetCache *memsto.TargetCacheType
|
||||
notifyConfigCache *memsto.NotifyConfigCacheType
|
||||
|
||||
alerting aconf.Alerting
|
||||
|
||||
Senders map[string]sender.Sender
|
||||
tpls map[string]*template.Template
|
||||
ExtraSenders map[string]sender.Sender
|
||||
BeforeSenderHook func(*models.AlertCurEvent) bool
|
||||
|
||||
ctx *ctx.Context
|
||||
astats *astats.Stats
|
||||
|
||||
RwLock sync.RWMutex
|
||||
}
|
||||
|
||||
// 创建一个 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, astats *astats.Stats) *Dispatch {
|
||||
notify := &Dispatch{
|
||||
alertRuleCache: alertRuleCache,
|
||||
userCache: userCache,
|
||||
userGroupCache: userGroupCache,
|
||||
alertSubscribeCache: alertSubscribeCache,
|
||||
targetCache: targetCache,
|
||||
notifyConfigCache: notifyConfigCache,
|
||||
|
||||
alerting: alerting,
|
||||
|
||||
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,
|
||||
astats: astats,
|
||||
}
|
||||
return notify
|
||||
}
|
||||
|
||||
func (e *Dispatch) ReloadTpls() error {
|
||||
err := e.relaodTpls()
|
||||
if err != nil {
|
||||
logger.Errorf("failed to reload tpls: %v", err)
|
||||
}
|
||||
|
||||
duration := time.Duration(9000) * time.Millisecond
|
||||
for {
|
||||
time.Sleep(duration)
|
||||
if err := e.relaodTpls(); err != nil {
|
||||
logger.Warning("failed to reload tpls:", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (e *Dispatch) relaodTpls() error {
|
||||
tmpTpls, err := models.ListTpls(e.ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
smtp := e.notifyConfigCache.GetSMTP()
|
||||
|
||||
senders := map[string]sender.Sender{
|
||||
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),
|
||||
}
|
||||
|
||||
e.RwLock.RLock()
|
||||
for channelName, extraSender := range e.ExtraSenders {
|
||||
senders[channelName] = extraSender
|
||||
}
|
||||
e.RwLock.RUnlock()
|
||||
|
||||
e.RwLock.Lock()
|
||||
e.tpls = tmpTpls
|
||||
e.Senders = senders
|
||||
e.RwLock.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
// HandleEventNotify 处理event事件的主逻辑
|
||||
// event: 告警/恢复事件
|
||||
// isSubscribe: 告警事件是否由subscribe的配置产生
|
||||
func (e *Dispatch) HandleEventNotify(event *models.AlertCurEvent, isSubscribe bool) {
|
||||
rule := e.alertRuleCache.Get(event.RuleId)
|
||||
if rule == nil {
|
||||
return
|
||||
}
|
||||
fillUsers(event, e.userCache, e.userGroupCache)
|
||||
|
||||
var (
|
||||
// 处理事件到 notifyTarget 关系,处理的notifyTarget用OrMerge进行合并
|
||||
handlers []NotifyTargetDispatch
|
||||
|
||||
// 额外去掉一些订阅,处理的notifyTarget用AndMerge进行合并, 如设置 channel=false,合并后不通过这个channel发送
|
||||
// 如果实现了相关 Dispatch,可以添加到interceptors中
|
||||
interceptorHandlers []NotifyTargetDispatch
|
||||
)
|
||||
if isSubscribe {
|
||||
handlers = []NotifyTargetDispatch{NotifyGroupDispatch, EventCallbacksDispatch}
|
||||
} else {
|
||||
handlers = []NotifyTargetDispatch{NotifyGroupDispatch, GlobalWebhookDispatch, EventCallbacksDispatch}
|
||||
}
|
||||
|
||||
notifyTarget := NewNotifyTarget()
|
||||
// 处理订阅关系使用OrMerge
|
||||
for _, handler := range handlers {
|
||||
notifyTarget.OrMerge(handler(rule, event, notifyTarget, e))
|
||||
}
|
||||
|
||||
// 处理移除订阅关系的逻辑,比如员工离职,临时静默某个通道的策略等
|
||||
for _, handler := range interceptorHandlers {
|
||||
notifyTarget.AndMerge(handler(rule, event, notifyTarget, e))
|
||||
}
|
||||
|
||||
// 处理事件发送,这里用一个goroutine处理一个event的所有发送事件
|
||||
go e.Send(rule, event, notifyTarget)
|
||||
|
||||
// 如果是不是订阅规则出现的event, 则需要处理订阅规则的event
|
||||
if !isSubscribe {
|
||||
e.handleSubs(event)
|
||||
}
|
||||
}
|
||||
|
||||
func (e *Dispatch) handleSubs(event *models.AlertCurEvent) {
|
||||
// handle alert subscribes
|
||||
subscribes := make([]*models.AlertSubscribe, 0)
|
||||
// rule specific subscribes
|
||||
if subs, has := e.alertSubscribeCache.Get(event.RuleId); has {
|
||||
subscribes = append(subscribes, subs...)
|
||||
}
|
||||
// global subscribes
|
||||
if subs, has := e.alertSubscribeCache.Get(0); has {
|
||||
subscribes = append(subscribes, subs...)
|
||||
}
|
||||
|
||||
for _, sub := range subscribes {
|
||||
e.handleSub(sub, *event)
|
||||
}
|
||||
}
|
||||
|
||||
// handleSub 处理订阅规则的event,注意这里event要使用值传递,因为后面会修改event的状态
|
||||
func (e *Dispatch) handleSub(sub *models.AlertSubscribe, event models.AlertCurEvent) {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
sub.ModifyEvent(&event)
|
||||
LogEvent(&event, "subscribe")
|
||||
|
||||
event.SubRuleId = sub.Id
|
||||
e.HandleEventNotify(&event, true)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
s.Send(msgCtx)
|
||||
}
|
||||
}
|
||||
|
||||
// handle event callbacks
|
||||
sender.SendCallbacks(e.ctx, notifyTarget.ToCallbackList(), event, e.targetCache, e.userCache, e.notifyConfigCache.GetIbex(), e.astats)
|
||||
|
||||
// handle global webhooks
|
||||
sender.SendWebhooks(notifyTarget.ToWebhookList(), event, e.astats)
|
||||
|
||||
// handle plugin call
|
||||
go sender.MayPluginNotify(e.genNoticeBytes(event), e.notifyConfigCache.GetNotifyScript(), e.astats)
|
||||
}
|
||||
|
||||
type Notice struct {
|
||||
Event *models.AlertCurEvent `json:"event"`
|
||||
Tpls map[string]string `json:"tpls"`
|
||||
}
|
||||
|
||||
func (e *Dispatch) genNoticeBytes(event *models.AlertCurEvent) []byte {
|
||||
// build notice body with templates
|
||||
ntpls := make(map[string]string)
|
||||
|
||||
e.RwLock.RLock()
|
||||
defer e.RwLock.RUnlock()
|
||||
for filename, tpl := range e.tpls {
|
||||
var body bytes.Buffer
|
||||
if err := tpl.Execute(&body, event); err != nil {
|
||||
ntpls[filename] = err.Error()
|
||||
} else {
|
||||
ntpls[filename] = body.String()
|
||||
}
|
||||
}
|
||||
|
||||
notice := Notice{Event: event, Tpls: ntpls}
|
||||
stdinBytes, err := json.Marshal(notice)
|
||||
if err != nil {
|
||||
logger.Errorf("event_notify: failed to marshal notice: %v", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
return stdinBytes
|
||||
}
|
||||
|
||||
// for alerting
|
||||
func fillUsers(ce *models.AlertCurEvent, uc *memsto.UserCacheType, ugc *memsto.UserGroupCacheType) {
|
||||
gids := make([]int64, 0, len(ce.NotifyGroupsJSON))
|
||||
for i := 0; i < len(ce.NotifyGroupsJSON); i++ {
|
||||
gid, err := strconv.ParseInt(ce.NotifyGroupsJSON[i], 10, 64)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
gids = append(gids, gid)
|
||||
}
|
||||
|
||||
ce.NotifyGroupsObj = ugc.GetByUserGroupIds(gids)
|
||||
|
||||
uids := make(map[int64]struct{})
|
||||
for i := 0; i < len(ce.NotifyGroupsObj); i++ {
|
||||
ug := ce.NotifyGroupsObj[i]
|
||||
for j := 0; j < len(ug.UserIds); j++ {
|
||||
uids[ug.UserIds[j]] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
ce.NotifyUsersObj = uc.GetByUserIds(mapKeys(uids))
|
||||
}
|
||||
|
||||
func mapKeys(m map[int64]struct{}) []int64 {
|
||||
lst := make([]int64, 0, len(m))
|
||||
for k := range m {
|
||||
lst = append(lst, k)
|
||||
}
|
||||
return lst
|
||||
}
|
||||
32
alert/dispatch/log.go
Normal file
32
alert/dispatch/log.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package dispatch
|
||||
|
||||
import (
|
||||
"github.com/ccfos/nightingale/v6/models"
|
||||
|
||||
"github.com/toolkits/pkg/logger"
|
||||
)
|
||||
|
||||
func LogEvent(event *models.AlertCurEvent, location string, err ...error) {
|
||||
status := "triggered"
|
||||
if event.IsRecovered {
|
||||
status = "recovered"
|
||||
}
|
||||
|
||||
message := ""
|
||||
if len(err) > 0 && err[0] != nil {
|
||||
message = "error_message: " + err[0].Error()
|
||||
}
|
||||
|
||||
logger.Infof(
|
||||
"event(%s %s) %s: rule_id=%d cluster:%s %v%s@%d %s",
|
||||
event.Hash,
|
||||
status,
|
||||
location,
|
||||
event.RuleId,
|
||||
event.Cluster,
|
||||
event.TagsJSON,
|
||||
event.TriggerValue,
|
||||
event.TriggerTime,
|
||||
message,
|
||||
)
|
||||
}
|
||||
33
alert/dispatch/notify_channel.go
Normal file
33
alert/dispatch/notify_channel.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package dispatch
|
||||
|
||||
// NotifyChannels channelKey -> bool
|
||||
type NotifyChannels map[string]bool
|
||||
|
||||
func NewNotifyChannels(channels []string) NotifyChannels {
|
||||
nc := make(NotifyChannels)
|
||||
for _, ch := range channels {
|
||||
nc[ch] = true
|
||||
}
|
||||
return nc
|
||||
}
|
||||
|
||||
func (nc NotifyChannels) OrMerge(other NotifyChannels) {
|
||||
nc.merge(other, func(a, b bool) bool { return a || b })
|
||||
}
|
||||
|
||||
func (nc NotifyChannels) AndMerge(other NotifyChannels) {
|
||||
nc.merge(other, func(a, b bool) bool { return a && b })
|
||||
}
|
||||
|
||||
func (nc NotifyChannels) merge(other NotifyChannels, f func(bool, bool) bool) {
|
||||
if other == nil {
|
||||
return
|
||||
}
|
||||
for k, v := range other {
|
||||
if curV, has := nc[k]; has {
|
||||
nc[k] = f(curV, v)
|
||||
} else {
|
||||
nc[k] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
134
alert/dispatch/notify_target.go
Normal file
134
alert/dispatch/notify_target.go
Normal file
@@ -0,0 +1,134 @@
|
||||
package dispatch
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/ccfos/nightingale/v6/models"
|
||||
)
|
||||
|
||||
// NotifyTarget 维护所有需要发送的目标 用户-通道/回调/钩子信息,用map维护的数据结构具有去重功能
|
||||
type NotifyTarget struct {
|
||||
userMap map[int64]NotifyChannels
|
||||
webhooks map[string]*models.Webhook
|
||||
callbacks map[string]struct{}
|
||||
}
|
||||
|
||||
func NewNotifyTarget() *NotifyTarget {
|
||||
return &NotifyTarget{
|
||||
userMap: make(map[int64]NotifyChannels),
|
||||
webhooks: make(map[string]*models.Webhook),
|
||||
callbacks: make(map[string]struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
// OrMerge 将 channelMap 按照 or 的方式合并,方便实现多种组合的策略,比如根据某个 tag 进行路由等
|
||||
func (s *NotifyTarget) OrMerge(other *NotifyTarget) {
|
||||
s.merge(other, NotifyChannels.OrMerge)
|
||||
}
|
||||
|
||||
// AndMerge 将 channelMap 中的 bool 值按照 and 的逻辑进行合并,可以单独将人/通道维度的通知移除
|
||||
// 常用的场景有:
|
||||
// 1. 人员离职了不需要发送告警了
|
||||
// 2. 某个告警通道进行维护,暂时不需要发送告警了
|
||||
// 3. 业务值班的重定向逻辑,将高等级的告警额外发送给应急人员等
|
||||
// 可以结合业务需求自己实现router
|
||||
func (s *NotifyTarget) AndMerge(other *NotifyTarget) {
|
||||
s.merge(other, NotifyChannels.AndMerge)
|
||||
}
|
||||
|
||||
func (s *NotifyTarget) merge(other *NotifyTarget, f func(NotifyChannels, NotifyChannels)) {
|
||||
if other == nil {
|
||||
return
|
||||
}
|
||||
for k, v := range other.userMap {
|
||||
if curV, has := s.userMap[k]; has {
|
||||
f(curV, v)
|
||||
} else {
|
||||
s.userMap[k] = v
|
||||
}
|
||||
}
|
||||
for k, v := range other.webhooks {
|
||||
s.webhooks[k] = v
|
||||
}
|
||||
for k, v := range other.callbacks {
|
||||
s.callbacks[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
// ToChannelUserMap userMap(map[uid][channel]bool) 转换为 map[channel][]uid 的结构
|
||||
func (s *NotifyTarget) ToChannelUserMap() map[string][]int64 {
|
||||
m := make(map[string][]int64)
|
||||
for uid, nc := range s.userMap {
|
||||
for ch, send := range nc {
|
||||
if send {
|
||||
m[ch] = append(m[ch], uid)
|
||||
}
|
||||
}
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func (s *NotifyTarget) ToCallbackList() []string {
|
||||
callbacks := make([]string, 0, len(s.callbacks))
|
||||
for cb := range s.callbacks {
|
||||
callbacks = append(callbacks, cb)
|
||||
}
|
||||
return callbacks
|
||||
}
|
||||
|
||||
func (s *NotifyTarget) ToWebhookList() []*models.Webhook {
|
||||
webhooks := make([]*models.Webhook, 0, len(s.webhooks))
|
||||
for _, wh := range s.webhooks {
|
||||
webhooks = append(webhooks, wh)
|
||||
}
|
||||
return webhooks
|
||||
}
|
||||
|
||||
// Dispatch 抽象由告警事件到信息接收者的路由策略
|
||||
// rule: 告警规则
|
||||
// event: 告警事件
|
||||
// prev: 前一次路由结果, Dispatch 的实现可以直接修改 prev, 也可以返回一个新的 NotifyTarget 用于 AndMerge/OrMerge
|
||||
type NotifyTargetDispatch func(rule *models.AlertRule, event *models.AlertCurEvent, prev *NotifyTarget, dispatch *Dispatch) *NotifyTarget
|
||||
|
||||
// GroupDispatch 处理告警规则的组订阅关系
|
||||
func NotifyGroupDispatch(rule *models.AlertRule, event *models.AlertCurEvent, prev *NotifyTarget, dispatch *Dispatch) *NotifyTarget {
|
||||
groupIds := make([]int64, 0, len(event.NotifyGroupsJSON))
|
||||
for _, groupId := range event.NotifyGroupsJSON {
|
||||
gid, err := strconv.ParseInt(groupId, 10, 64)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
groupIds = append(groupIds, gid)
|
||||
}
|
||||
|
||||
groups := dispatch.userGroupCache.GetByUserGroupIds(groupIds)
|
||||
NotifyTarget := NewNotifyTarget()
|
||||
for _, group := range groups {
|
||||
for _, userId := range group.UserIds {
|
||||
NotifyTarget.userMap[userId] = NewNotifyChannels(event.NotifyChannelsJSON)
|
||||
}
|
||||
}
|
||||
return NotifyTarget
|
||||
}
|
||||
|
||||
func GlobalWebhookDispatch(rule *models.AlertRule, event *models.AlertCurEvent, prev *NotifyTarget, dispatch *Dispatch) *NotifyTarget {
|
||||
webhooks := dispatch.notifyConfigCache.GetWebhooks()
|
||||
NotifyTarget := NewNotifyTarget()
|
||||
for _, webhook := range webhooks {
|
||||
if !webhook.Enable {
|
||||
continue
|
||||
}
|
||||
NotifyTarget.webhooks[webhook.Url] = webhook
|
||||
}
|
||||
return NotifyTarget
|
||||
}
|
||||
|
||||
func EventCallbacksDispatch(rule *models.AlertRule, event *models.AlertCurEvent, prev *NotifyTarget, dispatch *Dispatch) *NotifyTarget {
|
||||
for _, c := range event.CallbacksJSON {
|
||||
if c == "" {
|
||||
continue
|
||||
}
|
||||
prev.callbacks[c] = struct{}{}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
178
alert/eval/alert_rule.go
Normal file
178
alert/eval/alert_rule.go
Normal file
@@ -0,0 +1,178 @@
|
||||
package eval
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/ccfos/nightingale/v6/alert/aconf"
|
||||
"github.com/ccfos/nightingale/v6/alert/astats"
|
||||
"github.com/ccfos/nightingale/v6/alert/naming"
|
||||
"github.com/ccfos/nightingale/v6/alert/process"
|
||||
"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"
|
||||
)
|
||||
|
||||
type Scheduler struct {
|
||||
// key: hash
|
||||
alertRules map[string]*AlertRuleWorker
|
||||
|
||||
ExternalProcessors *process.ExternalProcessorsType
|
||||
|
||||
aconf aconf.Alert
|
||||
|
||||
alertRuleCache *memsto.AlertRuleCacheType
|
||||
targetCache *memsto.TargetCacheType
|
||||
busiGroupCache *memsto.BusiGroupCacheType
|
||||
alertMuteCache *memsto.AlertMuteCacheType
|
||||
datasourceCache *memsto.DatasourceCacheType
|
||||
|
||||
promClients *prom.PromClientMap
|
||||
tdengineClients *tdengine.TdengineClientMap
|
||||
|
||||
naming *naming.Naming
|
||||
|
||||
ctx *ctx.Context
|
||||
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, 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,
|
||||
|
||||
promClients: promClients,
|
||||
tdengineClients: tdengineClients,
|
||||
naming: naming,
|
||||
|
||||
ctx: ctx,
|
||||
stats: stats,
|
||||
}
|
||||
|
||||
go scheduler.LoopSyncRules(context.Background())
|
||||
return scheduler
|
||||
}
|
||||
|
||||
func (s *Scheduler) LoopSyncRules(ctx context.Context) {
|
||||
time.Sleep(time.Duration(s.aconf.EngineDelay) * time.Second)
|
||||
duration := 9000 * time.Millisecond
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-time.After(duration):
|
||||
s.syncAlertRules()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Scheduler) syncAlertRules() {
|
||||
ids := s.alertRuleCache.GetRuleIds()
|
||||
alertRuleWorkers := make(map[string]*AlertRuleWorker)
|
||||
externalRuleWorkers := make(map[string]*process.Processor)
|
||||
for _, id := range ids {
|
||||
rule := s.alertRuleCache.Get(id)
|
||||
if rule == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
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) {
|
||||
continue
|
||||
}
|
||||
ds := s.datasourceCache.GetById(dsId)
|
||||
if ds == nil {
|
||||
logger.Debugf("datasource %d not found", dsId)
|
||||
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.ctx, s.stats)
|
||||
|
||||
alertRule := NewAlertRuleWorker(rule, dsId, processor, s.promClients, s.tdengineClients, s.ctx)
|
||||
alertRuleWorkers[alertRule.Hash()] = alertRule
|
||||
}
|
||||
} else if rule.IsHostRule() && s.ctx.IsCenter {
|
||||
// all host rule will be processed by center instance
|
||||
if !naming.DatasourceHashRing.IsHit(naming.HostDatasource, fmt.Sprintf("%d", rule.Id), s.aconf.Heartbeat.Endpoint) {
|
||||
continue
|
||||
}
|
||||
processor := process.NewProcessor(rule, 0, s.alertRuleCache, s.targetCache, 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
|
||||
// if rule is not processed by prometheus engine, create it as externalRule
|
||||
for _, dsId := range rule.DatasourceIdsJson {
|
||||
ds := s.datasourceCache.GetById(dsId)
|
||||
if ds == nil {
|
||||
logger.Debugf("datasource %d not found", dsId)
|
||||
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.ctx, s.stats)
|
||||
externalRuleWorkers[processor.Key()] = processor
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for hash, rule := range alertRuleWorkers {
|
||||
if _, has := s.alertRules[hash]; !has {
|
||||
rule.Prepare()
|
||||
rule.Start()
|
||||
s.alertRules[hash] = rule
|
||||
}
|
||||
}
|
||||
|
||||
for hash, rule := range s.alertRules {
|
||||
if _, has := alertRuleWorkers[hash]; !has {
|
||||
rule.Stop()
|
||||
delete(s.alertRules, hash)
|
||||
}
|
||||
}
|
||||
|
||||
s.ExternalProcessors.ExternalLock.Lock()
|
||||
for key, processor := range externalRuleWorkers {
|
||||
if curProcessor, has := s.ExternalProcessors.Processors[key]; has {
|
||||
// rule存在,且hash一致,认为没有变更,这里可以根据需求单独实现一个关联数据更多的hash函数
|
||||
if processor.Hash() == curProcessor.Hash() {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// 现有规则中没有rule以及有rule但hash不一致的场景,需要触发rule的update
|
||||
processor.RecoverAlertCurEventFromDb()
|
||||
s.ExternalProcessors.Processors[key] = processor
|
||||
}
|
||||
|
||||
for key := range s.ExternalProcessors.Processors {
|
||||
if _, has := externalRuleWorkers[key]; !has {
|
||||
delete(s.ExternalProcessors.Processors, key)
|
||||
}
|
||||
}
|
||||
s.ExternalProcessors.ExternalLock.Unlock()
|
||||
}
|
||||
396
alert/eval/eval.go
Normal file
396
alert/eval/eval.go
Normal file
@@ -0,0 +1,396 @@
|
||||
package eval
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/ccfos/nightingale/v6/alert/common"
|
||||
"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"
|
||||
)
|
||||
|
||||
type AlertRuleWorker struct {
|
||||
datasourceId int64
|
||||
quit chan struct{}
|
||||
inhibit bool
|
||||
severity int
|
||||
|
||||
rule *models.AlertRule
|
||||
|
||||
processor *process.Processor
|
||||
|
||||
promClients *prom.PromClientMap
|
||||
tdengineClients *tdengine.TdengineClientMap
|
||||
ctx *ctx.Context
|
||||
}
|
||||
|
||||
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,
|
||||
tdengineClients: tdengineClients,
|
||||
ctx: ctx,
|
||||
}
|
||||
|
||||
return arw
|
||||
}
|
||||
|
||||
func (arw *AlertRuleWorker) Key() string {
|
||||
return common.RuleKey(arw.datasourceId, arw.rule.Id)
|
||||
}
|
||||
|
||||
func (arw *AlertRuleWorker) Hash() string {
|
||||
return str.MD5(fmt.Sprintf("%d_%d_%s_%d",
|
||||
arw.rule.Id,
|
||||
arw.rule.PromEvalInterval,
|
||||
arw.rule.RuleConfig,
|
||||
arw.datasourceId,
|
||||
))
|
||||
}
|
||||
|
||||
func (arw *AlertRuleWorker) Prepare() {
|
||||
arw.processor.RecoverAlertCurEventFromDb()
|
||||
}
|
||||
|
||||
func (arw *AlertRuleWorker) Start() {
|
||||
logger.Infof("eval:%s started", arw.Key())
|
||||
interval := arw.rule.PromEvalInterval
|
||||
if interval <= 0 {
|
||||
interval = 10
|
||||
}
|
||||
|
||||
ticker := time.NewTicker(time.Duration(interval) * time.Second)
|
||||
go func() {
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-arw.quit:
|
||||
return
|
||||
case <-ticker.C:
|
||||
arw.Eval()
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (arw *AlertRuleWorker) Eval() {
|
||||
cachedRule := arw.rule
|
||||
if cachedRule == nil {
|
||||
// logger.Errorf("rule_eval:%s rule not found", arw.Key())
|
||||
return
|
||||
}
|
||||
arw.processor.Stats.CounterRuleEval.WithLabelValues().Inc()
|
||||
|
||||
typ := cachedRule.GetRuleType()
|
||||
var anomalyPoints []common.AnomalyPoint
|
||||
var recoverPoints []common.AnomalyPoint
|
||||
switch typ {
|
||||
case models.PROMETHEUS:
|
||||
anomalyPoints = arw.GetPromAnomalyPoint(cachedRule.RuleConfig)
|
||||
case models.HOST:
|
||||
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
|
||||
}
|
||||
|
||||
if arw.processor == nil {
|
||||
logger.Warningf("rule_eval:%s processor is nil", arw.Key())
|
||||
return
|
||||
}
|
||||
|
||||
arw.processor.Handle(anomalyPoints, "inner", arw.inhibit)
|
||||
for _, point := range recoverPoints {
|
||||
str := fmt.Sprintf("%v", point.Value)
|
||||
arw.processor.RecoverSingle(process.Hash(cachedRule.Id, arw.processor.DatasourceId(), point), point.Timestamp, &str)
|
||||
}
|
||||
}
|
||||
|
||||
func (arw *AlertRuleWorker) Stop() {
|
||||
logger.Infof("rule_eval %s stopped", arw.Key())
|
||||
close(arw.quit)
|
||||
}
|
||||
|
||||
func (arw *AlertRuleWorker) GetPromAnomalyPoint(ruleConfig string) []common.AnomalyPoint {
|
||||
var lst []common.AnomalyPoint
|
||||
var severity int
|
||||
|
||||
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)
|
||||
return lst
|
||||
}
|
||||
|
||||
if rule == nil {
|
||||
logger.Errorf("rule_eval:%s rule_config:%s, error:rule is nil", arw.Key(), ruleConfig)
|
||||
return lst
|
||||
}
|
||||
|
||||
arw.inhibit = rule.Inhibit
|
||||
for _, query := range rule.Queries {
|
||||
if query.Severity < severity {
|
||||
arw.severity = query.Severity
|
||||
}
|
||||
|
||||
promql := strings.TrimSpace(query.PromQl)
|
||||
if promql == "" {
|
||||
logger.Errorf("rule_eval:%s promql is blank", arw.Key())
|
||||
continue
|
||||
}
|
||||
|
||||
if arw.promClients.IsNil(arw.datasourceId) {
|
||||
logger.Errorf("rule_eval:%s error reader client is nil", arw.Key())
|
||||
continue
|
||||
}
|
||||
|
||||
readerClient := arw.promClients.GetCli(arw.datasourceId)
|
||||
|
||||
var warnings promsdk.Warnings
|
||||
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()
|
||||
continue
|
||||
}
|
||||
|
||||
if len(warnings) > 0 {
|
||||
logger.Errorf("rule_eval:%s promql:%s, warnings:%v", arw.Key(), promql, warnings)
|
||||
arw.processor.Stats.CounterQueryDataErrorTotal.WithLabelValues(fmt.Sprintf("%d", arw.datasourceId)).Inc()
|
||||
continue
|
||||
}
|
||||
|
||||
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)
|
||||
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())
|
||||
return points, recoverPoints
|
||||
}
|
||||
|
||||
if len(ruleQuery.Queries) > 0 {
|
||||
seriesStore := make(map[uint64]*models.DataResp)
|
||||
seriesTagIndex := make(map[uint64][]uint64)
|
||||
|
||||
for _, query := range ruleQuery.Queries {
|
||||
cli := arw.tdengineClients.GetCli(dsId)
|
||||
if cli == nil {
|
||||
logger.Warningf("rule_eval:%d tdengine client is nil", rule.Id)
|
||||
continue
|
||||
}
|
||||
|
||||
series, err := cli.Query(query)
|
||||
if err != nil {
|
||||
logger.Warningf("rule_eval rid:%d query data error: %v", rule.Id, err)
|
||||
continue
|
||||
}
|
||||
|
||||
// 此条日志很重要,是告警判断的现场值
|
||||
logger.Debugf("rule_eval rid:%d req:%+v resp:%+v", rule.Id, query, series)
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
// 判断
|
||||
for _, trigger := range ruleQuery.Triggers {
|
||||
for _, seriesHash := range seriesTagIndex {
|
||||
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", rule.Id, series)
|
||||
continue
|
||||
}
|
||||
t, v, exists := series.Last()
|
||||
if !exists {
|
||||
logger.Warningf("rule_eval rid:%d series:%+v value not found", rule.Id, 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.Debugf("rule_eval rid:%d trigger:%+v exp:%s res:%v m:%v", rule.Id, trigger, trigger.Exp, isTriggered, m)
|
||||
|
||||
point := common.AnomalyPoint{
|
||||
Key: sample.MetricName(),
|
||||
Labels: sample.Metric,
|
||||
Timestamp: int64(ts),
|
||||
Value: value,
|
||||
Severity: trigger.Severity,
|
||||
Triggered: isTriggered,
|
||||
}
|
||||
|
||||
if isTriggered {
|
||||
points = append(points, point)
|
||||
} else {
|
||||
recoverPoints = append(recoverPoints, point)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return points, recoverPoints
|
||||
}
|
||||
|
||||
func (arw *AlertRuleWorker) GetHostAnomalyPoint(ruleConfig string) []common.AnomalyPoint {
|
||||
var lst []common.AnomalyPoint
|
||||
var severity int
|
||||
|
||||
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)
|
||||
return lst
|
||||
}
|
||||
|
||||
if rule == nil {
|
||||
logger.Errorf("rule_eval:%s rule_config:%s, error:rule is nil", arw.Key(), ruleConfig)
|
||||
return lst
|
||||
}
|
||||
|
||||
arw.inhibit = rule.Inhibit
|
||||
now := time.Now().Unix()
|
||||
for _, trigger := range rule.Triggers {
|
||||
if trigger.Severity < severity {
|
||||
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)
|
||||
arw.processor.Stats.CounterQueryDataErrorTotal.WithLabelValues(fmt.Sprintf("%d", arw.datasourceId)).Inc()
|
||||
continue
|
||||
}
|
||||
for _, target := range targets {
|
||||
m := make(map[string]string)
|
||||
target.FillTagsMap()
|
||||
for k, v := range target.TagsMap {
|
||||
m[k] = v
|
||||
}
|
||||
m["ident"] = target.Ident
|
||||
|
||||
bg := arw.processor.BusiGroupCache.GetByBusiGroupId(target.GroupId)
|
||||
if bg != nil && bg.LabelEnable == 1 {
|
||||
m["busigroup"] = bg.LabelValue
|
||||
}
|
||||
|
||||
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)
|
||||
arw.processor.Stats.CounterQueryDataErrorTotal.WithLabelValues(fmt.Sprintf("%d", arw.datasourceId)).Inc()
|
||||
continue
|
||||
}
|
||||
var 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 {
|
||||
m := make(map[string]string)
|
||||
target, exists := targetMap[host]
|
||||
if exists {
|
||||
target.FillTagsMap()
|
||||
for k, v := range target.TagsMap {
|
||||
m[k] = v
|
||||
}
|
||||
}
|
||||
m["ident"] = host
|
||||
|
||||
bg := arw.processor.BusiGroupCache.GetByBusiGroupId(target.GroupId)
|
||||
if bg != nil && bg.LabelEnable == 1 {
|
||||
m["busigroup"] = bg.LabelValue
|
||||
}
|
||||
|
||||
lst = append(lst, common.NewAnomalyPoint(trigger.Type, m, now, float64(offset), trigger.Severity))
|
||||
}
|
||||
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)
|
||||
arw.processor.Stats.CounterQueryDataErrorTotal.WithLabelValues(fmt.Sprintf("%d", arw.datasourceId)).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)
|
||||
arw.processor.Stats.CounterQueryDataErrorTotal.WithLabelValues(fmt.Sprintf("%d", arw.datasourceId)).Inc()
|
||||
continue
|
||||
}
|
||||
pct := float64(count) / float64(total) * 100
|
||||
if pct >= float64(trigger.Percent) {
|
||||
lst = append(lst, common.NewAnomalyPoint(trigger.Type, nil, now, pct, trigger.Severity))
|
||||
}
|
||||
}
|
||||
}
|
||||
return lst
|
||||
}
|
||||
213
alert/mute/mute.go
Normal file
213
alert/mute/mute.go
Normal file
@@ -0,0 +1,213 @@
|
||||
package mute
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/ccfos/nightingale/v6/alert/common"
|
||||
"github.com/ccfos/nightingale/v6/memsto"
|
||||
"github.com/ccfos/nightingale/v6/models"
|
||||
|
||||
"github.com/toolkits/pkg/logger"
|
||||
)
|
||||
|
||||
func IsMuted(rule *models.AlertRule, event *models.AlertCurEvent, targetCache *memsto.TargetCacheType, alertMuteCache *memsto.AlertMuteCacheType) bool {
|
||||
if rule.Disabled == 1 {
|
||||
return true
|
||||
}
|
||||
|
||||
if TimeSpanMuteStrategy(rule, event) {
|
||||
return true
|
||||
}
|
||||
|
||||
if IdentNotExistsMuteStrategy(rule, event, targetCache) {
|
||||
return true
|
||||
}
|
||||
|
||||
if BgNotMatchMuteStrategy(rule, event, targetCache) {
|
||||
return true
|
||||
}
|
||||
|
||||
if EventMuteStrategy(event, alertMuteCache) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// 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()))
|
||||
|
||||
enableStime := strings.Fields(rule.EnableStime)
|
||||
enableEtime := strings.Fields(rule.EnableEtime)
|
||||
enableDaysOfWeek := strings.Split(rule.EnableDaysOfWeek, ";")
|
||||
length := len(enableDaysOfWeek)
|
||||
// enableStime,enableEtime,enableDaysOfWeek三者长度肯定相同,这里循环一个即可
|
||||
for i := 0; i < length; i++ {
|
||||
enableDaysOfWeek[i] = strings.Replace(enableDaysOfWeek[i], "7", "0", 1)
|
||||
if !strings.Contains(enableDaysOfWeek[i], triggerWeek) {
|
||||
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 enableStime[i] > enableEtime[i] {
|
||||
// 21:00-09:00
|
||||
if triggerTime < enableStime[i] && triggerTime >= enableEtime[i] {
|
||||
// mute, 即没生效
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// 到这里说明当前时刻在告警规则的某组生效时间范围内,即没有 mute,直接返回 false
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// IdentNotExistsMuteStrategy 根据ident是否存在过滤,如果ident不存在,则target_up的告警直接过滤掉
|
||||
func IdentNotExistsMuteStrategy(rule *models.AlertRule, event *models.AlertCurEvent, targetCache *memsto.TargetCacheType) bool {
|
||||
ident, has := event.TagsMap["ident"]
|
||||
if !has {
|
||||
return false
|
||||
}
|
||||
_, exists := targetCache.Get(ident)
|
||||
// 如果是target_up的告警,且ident已经不存在了,直接过滤掉
|
||||
// 这里的判断有点太粗暴了,但是目前没有更好的办法
|
||||
if !exists && strings.Contains(rule.PromQl, "target_up") {
|
||||
logger.Debugf("[%s] mute: rule_eval:%d cluster:%s ident:%s", "IdentNotExistsMuteStrategy", rule.Id, event.Cluster, ident)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// BgNotMatchMuteStrategy 当规则开启只在bg内部告警时,对于非bg内部的机器过滤
|
||||
func BgNotMatchMuteStrategy(rule *models.AlertRule, event *models.AlertCurEvent, targetCache *memsto.TargetCacheType) bool {
|
||||
// 没有开启BG内部告警,直接不过滤
|
||||
if rule.EnableInBG == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
ident, has := event.TagsMap["ident"]
|
||||
if !has {
|
||||
return false
|
||||
}
|
||||
|
||||
target, exists := targetCache.Get(ident)
|
||||
// 对于包含ident的告警事件,check一下ident所属bg和rule所属bg是否相同
|
||||
// 如果告警规则选择了只在本BG生效,那其他BG的机器就不能因此规则产生告警
|
||||
if exists && target.GroupId != rule.GroupId {
|
||||
logger.Debugf("[%s] mute: rule_eval:%d cluster:%s", "BgNotMatchMuteStrategy", rule.Id, event.Cluster)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func EventMuteStrategy(event *models.AlertCurEvent, alertMuteCache *memsto.AlertMuteCacheType) bool {
|
||||
mutes, has := alertMuteCache.Gets(event.GroupId)
|
||||
if !has || len(mutes) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
for i := 0; i < len(mutes); i++ {
|
||||
if matchMute(event, mutes[i]) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// matchMute 如果传入了clock这个可选参数,就表示使用这个clock表示的时间,否则就从event的字段中取TriggerTime
|
||||
func matchMute(event *models.AlertCurEvent, mute *models.AlertMute, clock ...int64) bool {
|
||||
if mute.Disabled == 1 {
|
||||
return false
|
||||
}
|
||||
ts := event.TriggerTime
|
||||
if len(clock) > 0 {
|
||||
ts = clock[0]
|
||||
}
|
||||
|
||||
// 如果不是全局的,判断 匹配的 datasource id
|
||||
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{}{}
|
||||
}
|
||||
|
||||
// 判断 event.datasourceId 是否包含在 idm 中
|
||||
if _, has := idm[event.DatasourceId]; !has {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
var matchTime bool
|
||||
if mute.MuteTimeType == models.TimeRange {
|
||||
if ts < mute.Btime || ts > mute.Etime {
|
||||
return false
|
||||
}
|
||||
matchTime = true
|
||||
} else if mute.MuteTimeType == models.Periodic {
|
||||
tm := time.Unix(event.TriggerTime, 0)
|
||||
triggerTime := tm.Format("15:04")
|
||||
triggerWeek := strconv.Itoa(int(tm.Weekday()))
|
||||
|
||||
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 {
|
||||
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 {
|
||||
matchTime = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if !matchTime {
|
||||
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
|
||||
}
|
||||
|
||||
return common.MatchTags(event.TagsMap, mute.ITags)
|
||||
}
|
||||
79
alert/naming/hashring.go
Normal file
79
alert/naming/hashring.go
Normal file
@@ -0,0 +1,79 @@
|
||||
package naming
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sync"
|
||||
|
||||
"github.com/toolkits/pkg/consistent"
|
||||
"github.com/toolkits/pkg/logger"
|
||||
)
|
||||
|
||||
const NodeReplicas = 500
|
||||
|
||||
type DatasourceHashRingType struct {
|
||||
sync.RWMutex
|
||||
Rings map[int64]*consistent.Consistent
|
||||
}
|
||||
|
||||
// for alert_rule sharding
|
||||
var HostDatasource int64 = 99999999
|
||||
var DatasourceHashRing = DatasourceHashRingType{Rings: make(map[int64]*consistent.Consistent)}
|
||||
|
||||
func NewConsistentHashRing(replicas int32, nodes []string) *consistent.Consistent {
|
||||
ret := consistent.New()
|
||||
ret.NumberOfReplicas = int(replicas)
|
||||
for i := 0; i < len(nodes); i++ {
|
||||
ret.Add(nodes[i])
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func RebuildConsistentHashRing(datasourceId int64, nodes []string) {
|
||||
r := consistent.New()
|
||||
r.NumberOfReplicas = NodeReplicas
|
||||
for i := 0; i < len(nodes); i++ {
|
||||
r.Add(nodes[i])
|
||||
}
|
||||
|
||||
DatasourceHashRing.Set(datasourceId, r)
|
||||
logger.Infof("hash ring %d rebuild %+v", datasourceId, r.Members())
|
||||
}
|
||||
|
||||
func (chr *DatasourceHashRingType) GetNode(datasourceId int64, pk string) (string, error) {
|
||||
chr.Lock()
|
||||
defer chr.Unlock()
|
||||
_, exists := chr.Rings[datasourceId]
|
||||
if !exists {
|
||||
chr.Rings[datasourceId] = NewConsistentHashRing(int32(NodeReplicas), []string{})
|
||||
}
|
||||
|
||||
return chr.Rings[datasourceId].Get(pk)
|
||||
}
|
||||
|
||||
func (chr *DatasourceHashRingType) IsHit(datasourceId int64, 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 failed to get node from hashring:%v", pk, datasourceId, err)
|
||||
}
|
||||
return false
|
||||
}
|
||||
return node == currentNode
|
||||
}
|
||||
|
||||
func (chr *DatasourceHashRingType) Set(datasourceId int64, r *consistent.Consistent) {
|
||||
chr.Lock()
|
||||
defer chr.Unlock()
|
||||
chr.Rings[datasourceId] = r
|
||||
}
|
||||
|
||||
func (chr *DatasourceHashRingType) Clear() {
|
||||
chr.Lock()
|
||||
defer chr.Unlock()
|
||||
for id := range chr.Rings {
|
||||
if id == HostDatasource {
|
||||
continue
|
||||
}
|
||||
delete(chr.Rings, id)
|
||||
}
|
||||
}
|
||||
179
alert/naming/heartbeat.go
Normal file
179
alert/naming/heartbeat.go
Normal file
@@ -0,0 +1,179 @@
|
||||
package naming
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"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/poster"
|
||||
|
||||
"github.com/toolkits/pkg/logger"
|
||||
)
|
||||
|
||||
type Naming struct {
|
||||
ctx *ctx.Context
|
||||
heartbeatConfig aconf.HeartbeatConfig
|
||||
}
|
||||
|
||||
func NewNaming(ctx *ctx.Context, heartbeat aconf.HeartbeatConfig) *Naming {
|
||||
naming := &Naming{
|
||||
ctx: ctx,
|
||||
heartbeatConfig: heartbeat,
|
||||
}
|
||||
naming.Heartbeats()
|
||||
return naming
|
||||
}
|
||||
|
||||
// local servers
|
||||
var localss map[int64]string
|
||||
|
||||
func (n *Naming) Heartbeats() error {
|
||||
localss = make(map[int64]string)
|
||||
if err := n.heartbeat(); err != nil {
|
||||
fmt.Println("failed to heartbeat:", err)
|
||||
return err
|
||||
}
|
||||
|
||||
go n.loopHeartbeat()
|
||||
go n.loopDeleteInactiveInstances()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *Naming) loopDeleteInactiveInstances() {
|
||||
if !n.ctx.IsCenter {
|
||||
return
|
||||
}
|
||||
|
||||
interval := time.Duration(10) * time.Minute
|
||||
for {
|
||||
time.Sleep(interval)
|
||||
n.DeleteInactiveInstances()
|
||||
}
|
||||
}
|
||||
|
||||
func (n *Naming) DeleteInactiveInstances() {
|
||||
err := models.DB(n.ctx).Where("clock < ?", time.Now().Unix()-600).Delete(new(models.AlertingEngines)).Error
|
||||
if err != nil {
|
||||
logger.Errorf("delete inactive instances err:%v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (n *Naming) loopHeartbeat() {
|
||||
interval := time.Duration(n.heartbeatConfig.Interval) * time.Millisecond
|
||||
for {
|
||||
time.Sleep(interval)
|
||||
if err := n.heartbeat(); err != nil {
|
||||
logger.Warning(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (n *Naming) heartbeat() error {
|
||||
var datasourceIds []int64
|
||||
var err error
|
||||
|
||||
// 在页面上维护实例和集群的对应关系
|
||||
datasourceIds, err = models.GetDatasourceIdsByEngineName(n.ctx, n.heartbeatConfig.EngineName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(datasourceIds) == 0 {
|
||||
err := models.AlertingEngineHeartbeatWithCluster(n.ctx, n.heartbeatConfig.Endpoint, n.heartbeatConfig.EngineName, 0)
|
||||
if err != nil {
|
||||
logger.Warningf("heartbeat with cluster %s err:%v", "", err)
|
||||
}
|
||||
} 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(datasourceIds) == 0 {
|
||||
DatasourceHashRing.Clear()
|
||||
for dsId := range localss {
|
||||
if dsId == HostDatasource {
|
||||
continue
|
||||
}
|
||||
delete(localss, dsId)
|
||||
}
|
||||
}
|
||||
|
||||
for i := 0; i < len(datasourceIds); i++ {
|
||||
servers, err := n.ActiveServers(datasourceIds[i])
|
||||
if err != nil {
|
||||
logger.Warningf("hearbeat %d get active server err:%v", datasourceIds[i], err)
|
||||
continue
|
||||
}
|
||||
|
||||
sort.Strings(servers)
|
||||
newss := strings.Join(servers, " ")
|
||||
|
||||
oldss, exists := localss[datasourceIds[i]]
|
||||
if exists && oldss == newss {
|
||||
continue
|
||||
}
|
||||
|
||||
RebuildConsistentHashRing(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)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *Naming) ActiveServers(datasourceId int64) ([]string, error) {
|
||||
if datasourceId == -1 {
|
||||
return nil, fmt.Errorf("cluster is empty")
|
||||
}
|
||||
|
||||
if !n.ctx.IsCenter {
|
||||
lst, err := poster.GetByUrls[[]string](n.ctx, "/v1/n9e/servers-active?dsid="+fmt.Sprintf("%d", datasourceId))
|
||||
return lst, err
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
74
alert/process/alert_cur_event.go
Normal file
74
alert/process/alert_cur_event.go
Normal file
@@ -0,0 +1,74 @@
|
||||
package process
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/ccfos/nightingale/v6/models"
|
||||
)
|
||||
|
||||
type AlertCurEventMap struct {
|
||||
sync.RWMutex
|
||||
Data map[string]*models.AlertCurEvent
|
||||
}
|
||||
|
||||
func NewAlertCurEventMap(data map[string]*models.AlertCurEvent) *AlertCurEventMap {
|
||||
if data == nil {
|
||||
return &AlertCurEventMap{
|
||||
Data: make(map[string]*models.AlertCurEvent),
|
||||
}
|
||||
}
|
||||
return &AlertCurEventMap{
|
||||
Data: data,
|
||||
}
|
||||
}
|
||||
|
||||
func (a *AlertCurEventMap) SetAll(data map[string]*models.AlertCurEvent) {
|
||||
a.Lock()
|
||||
defer a.Unlock()
|
||||
a.Data = data
|
||||
}
|
||||
|
||||
func (a *AlertCurEventMap) Set(key string, value *models.AlertCurEvent) {
|
||||
a.Lock()
|
||||
defer a.Unlock()
|
||||
a.Data[key] = value
|
||||
}
|
||||
|
||||
func (a *AlertCurEventMap) Get(key string) (*models.AlertCurEvent, bool) {
|
||||
a.RLock()
|
||||
defer a.RUnlock()
|
||||
event, exists := a.Data[key]
|
||||
return event, exists
|
||||
}
|
||||
|
||||
func (a *AlertCurEventMap) UpdateLastEvalTime(key string, lastEvalTime int64) {
|
||||
a.Lock()
|
||||
defer a.Unlock()
|
||||
event, exists := a.Data[key]
|
||||
if !exists {
|
||||
return
|
||||
}
|
||||
event.LastEvalTime = lastEvalTime
|
||||
}
|
||||
|
||||
func (a *AlertCurEventMap) Delete(key string) {
|
||||
a.Lock()
|
||||
defer a.Unlock()
|
||||
delete(a.Data, key)
|
||||
}
|
||||
|
||||
func (a *AlertCurEventMap) Keys() []string {
|
||||
a.RLock()
|
||||
defer a.RUnlock()
|
||||
keys := make([]string, 0, len(a.Data))
|
||||
for k := range a.Data {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
return keys
|
||||
}
|
||||
|
||||
func (a *AlertCurEventMap) GetAll() map[string]*models.AlertCurEvent {
|
||||
a.RLock()
|
||||
defer a.RUnlock()
|
||||
return a.Data
|
||||
}
|
||||
468
alert/process/process.go
Normal file
468
alert/process/process.go
Normal file
@@ -0,0 +1,468 @@
|
||||
package process
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/ccfos/nightingale/v6/alert/astats"
|
||||
"github.com/ccfos/nightingale/v6/alert/common"
|
||||
"github.com/ccfos/nightingale/v6/alert/dispatch"
|
||||
"github.com/ccfos/nightingale/v6/alert/mute"
|
||||
"github.com/ccfos/nightingale/v6/alert/queue"
|
||||
"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/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
|
||||
}
|
||||
|
||||
var ExternalProcessors ExternalProcessorsType
|
||||
|
||||
func NewExternalProcessors() *ExternalProcessorsType {
|
||||
return &ExternalProcessorsType{
|
||||
Processors: make(map[string]*Processor),
|
||||
}
|
||||
}
|
||||
|
||||
func (e *ExternalProcessorsType) GetExternalAlertRule(datasourceId, id int64) (*Processor, bool) {
|
||||
e.ExternalLock.RLock()
|
||||
defer e.ExternalLock.RUnlock()
|
||||
processor, has := e.Processors[common.RuleKey(datasourceId, id)]
|
||||
return processor, has
|
||||
}
|
||||
|
||||
type HandleEventFunc func(event *models.AlertCurEvent)
|
||||
|
||||
type Processor struct {
|
||||
datasourceId int64
|
||||
|
||||
rule *models.AlertRule
|
||||
fires *AlertCurEventMap
|
||||
pendings *AlertCurEventMap
|
||||
inhibit bool
|
||||
|
||||
tagsMap map[string]string
|
||||
tagsArr []string
|
||||
target string
|
||||
targetNote string
|
||||
groupName string
|
||||
|
||||
atertRuleCache *memsto.AlertRuleCacheType
|
||||
TargetCache *memsto.TargetCacheType
|
||||
BusiGroupCache *memsto.BusiGroupCacheType
|
||||
alertMuteCache *memsto.AlertMuteCacheType
|
||||
datasourceCache *memsto.DatasourceCacheType
|
||||
|
||||
promClients *prom.PromClientMap
|
||||
ctx *ctx.Context
|
||||
Stats *astats.Stats
|
||||
|
||||
HandleFireEventHook HandleEventFunc
|
||||
HandleRecoverEventHook HandleEventFunc
|
||||
EventMuteHook EventMuteHookFunc
|
||||
}
|
||||
|
||||
func (p *Processor) Key() string {
|
||||
return common.RuleKey(p.datasourceId, p.rule.Id)
|
||||
}
|
||||
|
||||
func (p *Processor) DatasourceId() int64 {
|
||||
return p.datasourceId
|
||||
}
|
||||
|
||||
func (p *Processor) Hash() string {
|
||||
return str.MD5(fmt.Sprintf("%d_%d_%s_%d",
|
||||
p.rule.Id,
|
||||
p.rule.PromEvalInterval,
|
||||
p.rule.RuleConfig,
|
||||
p.datasourceId,
|
||||
))
|
||||
}
|
||||
|
||||
func NewProcessor(rule *models.AlertRule, datasourceId int64, atertRuleCache *memsto.AlertRuleCacheType, targetCache *memsto.TargetCacheType,
|
||||
busiGroupCache *memsto.BusiGroupCacheType, alertMuteCache *memsto.AlertMuteCacheType, datasourceCache *memsto.DatasourceCacheType, ctx *ctx.Context,
|
||||
stats *astats.Stats) *Processor {
|
||||
|
||||
p := &Processor{
|
||||
datasourceId: datasourceId,
|
||||
rule: rule,
|
||||
|
||||
TargetCache: targetCache,
|
||||
BusiGroupCache: busiGroupCache,
|
||||
alertMuteCache: alertMuteCache,
|
||||
atertRuleCache: atertRuleCache,
|
||||
datasourceCache: datasourceCache,
|
||||
|
||||
ctx: ctx,
|
||||
Stats: stats,
|
||||
|
||||
HandleFireEventHook: func(event *models.AlertCurEvent) {},
|
||||
HandleRecoverEventHook: func(event *models.AlertCurEvent) {},
|
||||
EventMuteHook: func(event *models.AlertCurEvent) bool { return false },
|
||||
}
|
||||
|
||||
p.mayHandleGroup()
|
||||
return p
|
||||
}
|
||||
|
||||
func (p *Processor) Handle(anomalyPoints []common.AnomalyPoint, from string, inhibit bool) {
|
||||
// 有可能rule的一些配置已经发生变化,比如告警接收人、callbacks等
|
||||
// 这些信息的修改是不会引起worker restart的,但是确实会影响告警处理逻辑
|
||||
// 所以,这里直接从memsto.AlertRuleCache中获取并覆盖
|
||||
p.inhibit = inhibit
|
||||
cachedRule := p.atertRuleCache.Get(p.rule.Id)
|
||||
if cachedRule == nil {
|
||||
logger.Errorf("rule not found %+v", anomalyPoints)
|
||||
return
|
||||
}
|
||||
p.rule = cachedRule
|
||||
now := time.Now().Unix()
|
||||
alertingKeys := map[string]struct{}{}
|
||||
|
||||
// 根据 event 的 tag 将 events 分组,处理告警抑制的情况
|
||||
eventsMap := make(map[string][]*models.AlertCurEvent)
|
||||
for _, anomalyPoint := range anomalyPoints {
|
||||
event := p.BuildEvent(anomalyPoint, from, now)
|
||||
// 如果 event 被 mute 了,本质也是 fire 的状态,这里无论如何都添加到 alertingKeys 中,防止 fire 的事件自动恢复了
|
||||
hash := event.Hash
|
||||
alertingKeys[hash] = struct{}{}
|
||||
if mute.IsMuted(cachedRule, event, p.TargetCache, p.alertMuteCache) {
|
||||
p.Stats.CounterMuteTotal.WithLabelValues(event.GroupName).Inc()
|
||||
logger.Debugf("rule_eval:%s event:%v is muted", p.Key(), event)
|
||||
continue
|
||||
}
|
||||
|
||||
if p.EventMuteHook(event) {
|
||||
continue
|
||||
}
|
||||
|
||||
tagHash := TagHash(anomalyPoint)
|
||||
eventsMap[tagHash] = append(eventsMap[tagHash], event)
|
||||
}
|
||||
|
||||
for _, events := range eventsMap {
|
||||
p.handleEvent(events)
|
||||
}
|
||||
|
||||
p.HandleRecover(alertingKeys, now)
|
||||
}
|
||||
|
||||
func (p *Processor) BuildEvent(anomalyPoint common.AnomalyPoint, from string, now int64) *models.AlertCurEvent {
|
||||
p.fillTags(anomalyPoint)
|
||||
p.mayHandleIdent()
|
||||
hash := Hash(p.rule.Id, p.datasourceId, anomalyPoint)
|
||||
ds := p.datasourceCache.GetById(p.datasourceId)
|
||||
var dsName string
|
||||
if ds != nil {
|
||||
dsName = ds.Name
|
||||
}
|
||||
|
||||
event := p.rule.GenerateNewEvent(p.ctx)
|
||||
event.TriggerTime = anomalyPoint.Timestamp
|
||||
event.TagsMap = p.tagsMap
|
||||
event.DatasourceId = p.datasourceId
|
||||
event.Cluster = dsName
|
||||
event.Hash = hash
|
||||
event.TargetIdent = p.target
|
||||
event.TargetNote = p.targetNote
|
||||
event.TriggerValue = anomalyPoint.ReadableValue()
|
||||
event.TagsJSON = p.tagsArr
|
||||
event.GroupName = p.groupName
|
||||
event.Tags = strings.Join(p.tagsArr, ",,")
|
||||
event.IsRecovered = false
|
||||
event.Callbacks = p.rule.Callbacks
|
||||
event.CallbacksJSON = p.rule.CallbacksJSON
|
||||
event.Annotations = p.rule.Annotations
|
||||
event.AnnotationsJSON = make(map[string]string)
|
||||
event.RuleConfig = p.rule.RuleConfig
|
||||
event.RuleConfigJson = p.rule.RuleConfigJson
|
||||
event.Severity = anomalyPoint.Severity
|
||||
event.ExtraConfig = p.rule.ExtraConfigJSON
|
||||
event.PromQl = anomalyPoint.Query
|
||||
|
||||
if from == "inner" {
|
||||
event.LastEvalTime = now
|
||||
} else {
|
||||
event.LastEvalTime = event.TriggerTime
|
||||
}
|
||||
return event
|
||||
}
|
||||
|
||||
func (p *Processor) HandleRecover(alertingKeys map[string]struct{}, now int64) {
|
||||
for _, hash := range p.pendings.Keys() {
|
||||
if _, has := alertingKeys[hash]; has {
|
||||
continue
|
||||
}
|
||||
p.pendings.Delete(hash)
|
||||
}
|
||||
|
||||
for hash := range p.fires.GetAll() {
|
||||
if _, has := alertingKeys[hash]; has {
|
||||
continue
|
||||
}
|
||||
p.RecoverSingle(hash, now, nil)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Processor) RecoverSingle(hash string, now int64, value *string) {
|
||||
cachedRule := p.rule
|
||||
if cachedRule == nil {
|
||||
return
|
||||
}
|
||||
event, has := p.fires.Get(hash)
|
||||
if !has {
|
||||
return
|
||||
}
|
||||
// 如果配置了留观时长,就不能立马恢复了
|
||||
if cachedRule.RecoverDuration > 0 && now-event.LastEvalTime < cachedRule.RecoverDuration {
|
||||
logger.Debugf("rule_eval:%s event:%v not recover", p.Key(), event)
|
||||
return
|
||||
}
|
||||
if value != nil {
|
||||
event.TriggerValue = *value
|
||||
}
|
||||
|
||||
// 没查到触发阈值的vector,姑且就认为这个vector的值恢复了
|
||||
// 我确实无法分辨,是prom中有值但是未满足阈值所以没返回,还是prom中确实丢了一些点导致没有数据可以返回,尴尬
|
||||
p.fires.Delete(hash)
|
||||
p.pendings.Delete(hash)
|
||||
|
||||
// 可能是因为调整了promql才恢复的,所以事件里边要体现最新的promql,否则用户会比较困惑
|
||||
// 当然,其实rule的各个字段都可能发生变化了,都更新一下吧
|
||||
cachedRule.UpdateEvent(event)
|
||||
event.IsRecovered = true
|
||||
event.LastEvalTime = now
|
||||
|
||||
p.HandleRecoverEventHook(event)
|
||||
p.pushEventToQueue(event)
|
||||
}
|
||||
|
||||
func (p *Processor) handleEvent(events []*models.AlertCurEvent) {
|
||||
var fireEvents []*models.AlertCurEvent
|
||||
// severity 初始为 4, 一定为遇到比自己优先级高的事件
|
||||
severity := 4
|
||||
for _, event := range events {
|
||||
if event == nil {
|
||||
continue
|
||||
}
|
||||
if p.rule.PromForDuration == 0 {
|
||||
fireEvents = append(fireEvents, event)
|
||||
if severity > event.Severity {
|
||||
severity = event.Severity
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
var preTriggerTime int64
|
||||
preEvent, has := p.pendings.Get(event.Hash)
|
||||
if has {
|
||||
p.pendings.UpdateLastEvalTime(event.Hash, event.LastEvalTime)
|
||||
preTriggerTime = preEvent.TriggerTime
|
||||
} else {
|
||||
p.pendings.Set(event.Hash, event)
|
||||
preTriggerTime = event.TriggerTime
|
||||
}
|
||||
|
||||
if event.LastEvalTime-preTriggerTime+int64(event.PromEvalInterval) >= int64(p.rule.PromForDuration) {
|
||||
fireEvents = append(fireEvents, event)
|
||||
if severity > event.Severity {
|
||||
severity = event.Severity
|
||||
}
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
p.inhibitEvent(fireEvents, severity)
|
||||
}
|
||||
|
||||
func (p *Processor) inhibitEvent(events []*models.AlertCurEvent, highSeverity int) {
|
||||
for _, event := range events {
|
||||
if p.inhibit && event.Severity > highSeverity {
|
||||
logger.Debugf("rule_eval:%s event:%+v inhibit highSeverity:%d", p.Key(), event, highSeverity)
|
||||
continue
|
||||
}
|
||||
p.fireEvent(event)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Processor) fireEvent(event *models.AlertCurEvent) {
|
||||
// As p.rule maybe outdated, use rule from cache
|
||||
cachedRule := p.rule
|
||||
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)
|
||||
// 说明不想重复通知,那就直接返回了,nothing to do
|
||||
// do not need to send alert again
|
||||
return
|
||||
}
|
||||
|
||||
// 之前发送过告警了,这次是否要继续发送,要看是否过了通道静默时间
|
||||
if event.LastEvalTime >= fired.LastSentTime+int64(cachedRule.NotifyRepeatStep)*60 {
|
||||
if cachedRule.NotifyMaxNumber == 0 {
|
||||
// 最大可以发送次数如果是0,表示不想限制最大发送次数,一直发即可
|
||||
event.NotifyCurNumber = fired.NotifyCurNumber + 1
|
||||
p.pushEventToQueue(event)
|
||||
} else {
|
||||
// 有最大发送次数的限制,就要看已经发了几次了,是否达到了最大发送次数
|
||||
if fired.NotifyCurNumber >= cachedRule.NotifyMaxNumber {
|
||||
logger.Debugf("rule_eval:%s event:%+v reach max number", p.Key(), event)
|
||||
return
|
||||
} else {
|
||||
event.NotifyCurNumber = fired.NotifyCurNumber + 1
|
||||
p.pushEventToQueue(event)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
event.NotifyCurNumber = 1
|
||||
event.FirstTriggerTime = event.TriggerTime
|
||||
p.HandleFireEventHook(event)
|
||||
p.pushEventToQueue(event)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Processor) pushEventToQueue(e *models.AlertCurEvent) {
|
||||
if !e.IsRecovered {
|
||||
e.LastSentTime = e.LastEvalTime
|
||||
p.fires.Set(e.Hash, e)
|
||||
}
|
||||
|
||||
dispatch.LogEvent(e, "push_queue")
|
||||
if !queue.EventQueue.PushFront(e) {
|
||||
logger.Warningf("event_push_queue: queue is full, event:%+v", e)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Processor) RecoverAlertCurEventFromDb() {
|
||||
p.pendings = NewAlertCurEventMap(nil)
|
||||
|
||||
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.fires = NewAlertCurEventMap(nil)
|
||||
return
|
||||
}
|
||||
|
||||
fireMap := make(map[string]*models.AlertCurEvent)
|
||||
for _, event := range curEvents {
|
||||
event.DB2Mem()
|
||||
fireMap[event.Hash] = event
|
||||
}
|
||||
|
||||
p.fires = NewAlertCurEventMap(fireMap)
|
||||
}
|
||||
|
||||
func (p *Processor) fillTags(anomalyPoint common.AnomalyPoint) {
|
||||
// handle series tags
|
||||
tagsMap := make(map[string]string)
|
||||
for label, value := range anomalyPoint.Labels {
|
||||
tagsMap[string(label)] = string(value)
|
||||
}
|
||||
|
||||
var e = &models.AlertCurEvent{
|
||||
TagsMap: tagsMap,
|
||||
}
|
||||
|
||||
// handle rule tags
|
||||
for _, tag := range p.rule.AppendTagsJSON {
|
||||
arr := strings.SplitN(tag, "=", 2)
|
||||
|
||||
var defs = []string{
|
||||
"{{$labels := .TagsMap}}",
|
||||
"{{$value := .TriggerValue}}",
|
||||
}
|
||||
tagValue := arr[1]
|
||||
text := strings.Join(append(defs, tagValue), "")
|
||||
t, err := template.New(fmt.Sprint(p.rule.Id)).Funcs(template.FuncMap(tplx.TemplateFuncMap)).Parse(text)
|
||||
if err != nil {
|
||||
tagValue = fmt.Sprintf("parse tag value failed, err:%s", err)
|
||||
tagsMap[arr[0]] = tagValue
|
||||
continue
|
||||
}
|
||||
|
||||
var body bytes.Buffer
|
||||
err = t.Execute(&body, e)
|
||||
if err != nil {
|
||||
tagValue = fmt.Sprintf("parse tag value failed, err:%s", err)
|
||||
tagsMap[arr[0]] = tagValue
|
||||
continue
|
||||
}
|
||||
|
||||
tagsMap[arr[0]] = body.String()
|
||||
}
|
||||
|
||||
tagsMap["rulename"] = p.rule.Name
|
||||
p.tagsMap = tagsMap
|
||||
|
||||
// handle tagsArr
|
||||
p.tagsArr = labelMapToArr(tagsMap)
|
||||
}
|
||||
|
||||
func (p *Processor) mayHandleIdent() {
|
||||
// handle ident
|
||||
if ident, has := p.tagsMap["ident"]; has {
|
||||
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 = ""
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Processor) mayHandleGroup() {
|
||||
// handle bg
|
||||
bg := p.BusiGroupCache.GetByBusiGroupId(p.rule.GroupId)
|
||||
if bg != nil {
|
||||
p.groupName = bg.Name
|
||||
}
|
||||
}
|
||||
|
||||
func labelMapToArr(m map[string]string) []string {
|
||||
numLabels := len(m)
|
||||
|
||||
labelStrings := make([]string, 0, numLabels)
|
||||
for label, value := range m {
|
||||
labelStrings = append(labelStrings, fmt.Sprintf("%s=%s", label, value))
|
||||
}
|
||||
|
||||
if numLabels > 1 {
|
||||
sort.Strings(labelStrings)
|
||||
}
|
||||
return labelStrings
|
||||
}
|
||||
|
||||
func Hash(ruleId, datasourceId int64, vector common.AnomalyPoint) string {
|
||||
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 {
|
||||
return str.MD5(vector.Labels.String())
|
||||
}
|
||||
18
alert/queue/queue.go
Normal file
18
alert/queue/queue.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package queue
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/ccfos/nightingale/v6/alert/astats"
|
||||
"github.com/toolkits/pkg/container/list"
|
||||
)
|
||||
|
||||
var EventQueue = list.NewSafeListLimited(10000000)
|
||||
|
||||
func ReportQueueSize(stats *astats.Stats) {
|
||||
for {
|
||||
time.Sleep(time.Second)
|
||||
|
||||
stats.GaugeAlertQueueSize.Set(float64(EventQueue.Len()))
|
||||
}
|
||||
}
|
||||
109
alert/record/prom_rule.go
Normal file
109
alert/record/prom_rule.go
Normal file
@@ -0,0 +1,109 @@
|
||||
package record
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"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"
|
||||
|
||||
"github.com/toolkits/pkg/logger"
|
||||
"github.com/toolkits/pkg/str"
|
||||
)
|
||||
|
||||
type RecordRuleContext struct {
|
||||
datasourceId int64
|
||||
quit chan struct{}
|
||||
|
||||
rule *models.RecordingRule
|
||||
promClients *prom.PromClientMap
|
||||
stats *astats.Stats
|
||||
}
|
||||
|
||||
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,
|
||||
stats: stats,
|
||||
}
|
||||
}
|
||||
|
||||
func (rrc *RecordRuleContext) Key() string {
|
||||
return fmt.Sprintf("record-%d-%d", rrc.datasourceId, rrc.rule.Id)
|
||||
}
|
||||
|
||||
func (rrc *RecordRuleContext) Hash() string {
|
||||
return str.MD5(fmt.Sprintf("%d_%d_%s_%d",
|
||||
rrc.rule.Id,
|
||||
rrc.rule.PromEvalInterval,
|
||||
rrc.rule.PromQl,
|
||||
rrc.datasourceId,
|
||||
))
|
||||
}
|
||||
|
||||
func (rrc *RecordRuleContext) Prepare() {}
|
||||
|
||||
func (rrc *RecordRuleContext) Start() {
|
||||
logger.Infof("eval:%s started", rrc.Key())
|
||||
interval := rrc.rule.PromEvalInterval
|
||||
if interval <= 0 {
|
||||
interval = 10
|
||||
}
|
||||
|
||||
ticker := time.NewTicker(time.Duration(interval) * time.Second)
|
||||
go func() {
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-rrc.quit:
|
||||
return
|
||||
case <-ticker.C:
|
||||
rrc.Eval()
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (rrc *RecordRuleContext) Eval() {
|
||||
rrc.stats.CounterRecordEval.WithLabelValues().Inc()
|
||||
promql := strings.TrimSpace(rrc.rule.PromQl)
|
||||
if promql == "" {
|
||||
logger.Errorf("eval:%s promql is blank", rrc.Key())
|
||||
return
|
||||
}
|
||||
|
||||
if rrc.promClients.IsNil(rrc.datasourceId) {
|
||||
logger.Errorf("eval:%s reader client is nil", rrc.Key())
|
||||
rrc.stats.CounterRecordEvalErrorTotal.WithLabelValues().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().Inc()
|
||||
return
|
||||
}
|
||||
|
||||
if len(warnings) > 0 {
|
||||
logger.Errorf("eval:%s promql:%s, warnings:%v", rrc.Key(), promql, warnings)
|
||||
rrc.stats.CounterRecordEvalErrorTotal.WithLabelValues().Inc()
|
||||
return
|
||||
}
|
||||
|
||||
ts := ConvertToTimeSeries(value, rrc.rule)
|
||||
if len(ts) != 0 {
|
||||
rrc.promClients.GetWriterCli(rrc.datasourceId).Write(ts)
|
||||
}
|
||||
}
|
||||
|
||||
func (rrc *RecordRuleContext) Stop() {
|
||||
logger.Infof("%s stopped", rrc.Key())
|
||||
close(rrc.quit)
|
||||
}
|
||||
122
alert/record/sample.go
Normal file
122
alert/record/sample.go
Normal file
@@ -0,0 +1,122 @@
|
||||
package record
|
||||
|
||||
import (
|
||||
"math"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/ccfos/nightingale/v6/models"
|
||||
|
||||
"github.com/prometheus/common/model"
|
||||
"github.com/prometheus/prometheus/prompb"
|
||||
)
|
||||
|
||||
const (
|
||||
LabelName = "__name__"
|
||||
)
|
||||
|
||||
func ConvertToTimeSeries(value model.Value, rule *models.RecordingRule) (lst []*prompb.TimeSeries) {
|
||||
switch value.Type() {
|
||||
case model.ValVector:
|
||||
items, ok := value.(model.Vector)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
for _, item := range items {
|
||||
if math.IsNaN(float64(item.Value)) {
|
||||
continue
|
||||
}
|
||||
s := prompb.Sample{}
|
||||
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{
|
||||
Labels: l,
|
||||
Samples: []prompb.Sample{s},
|
||||
})
|
||||
}
|
||||
case model.ValMatrix:
|
||||
items, ok := value.(model.Matrix)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
for _, item := range items {
|
||||
if len(item.Values) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
last := item.Values[len(item.Values)-1]
|
||||
|
||||
if math.IsNaN(float64(last.Value)) {
|
||||
continue
|
||||
}
|
||||
l := labelsToLabelsProto(item.Metric, rule)
|
||||
var slst []prompb.Sample
|
||||
for _, v := range item.Values {
|
||||
if math.IsNaN(float64(v.Value)) {
|
||||
continue
|
||||
}
|
||||
slst = append(slst, prompb.Sample{
|
||||
Timestamp: time.Unix(v.Timestamp.Unix(), 0).UnixNano() / 1e6,
|
||||
Value: float64(v.Value),
|
||||
})
|
||||
}
|
||||
lst = append(lst, &prompb.TimeSeries{
|
||||
Labels: l,
|
||||
Samples: slst,
|
||||
})
|
||||
}
|
||||
case model.ValScalar:
|
||||
item, ok := value.(*model.Scalar)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
if math.IsNaN(float64(item.Value)) {
|
||||
return
|
||||
}
|
||||
|
||||
lst = append(lst, &prompb.TimeSeries{
|
||||
Labels: nil,
|
||||
Samples: []prompb.Sample{{Value: float64(item.Value), Timestamp: time.Unix(item.Timestamp.Unix(), 0).UnixNano() / 1e6}},
|
||||
})
|
||||
default:
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func labelsToLabelsProto(labels model.Metric, rule *models.RecordingRule) (result []*prompb.Label) {
|
||||
//name
|
||||
nameLs := &prompb.Label{
|
||||
Name: LabelName,
|
||||
Value: rule.Name,
|
||||
}
|
||||
result = append(result, nameLs)
|
||||
for k, v := range labels {
|
||||
if k == LabelName {
|
||||
continue
|
||||
}
|
||||
if model.LabelNameRE.MatchString(string(k)) {
|
||||
result = append(result, &prompb.Label{
|
||||
Name: string(k),
|
||||
Value: string(v),
|
||||
})
|
||||
}
|
||||
}
|
||||
if len(rule.AppendTagsJSON) != 0 {
|
||||
for _, v := range rule.AppendTagsJSON {
|
||||
index := strings.Index(v, "=")
|
||||
if model.LabelNameRE.MatchString(v[:index]) {
|
||||
result = append(result, &prompb.Label{
|
||||
Name: v[:index],
|
||||
Value: v[index+1:],
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
94
alert/record/scheduler.go
Normal file
94
alert/record/scheduler.go
Normal file
@@ -0,0 +1,94 @@
|
||||
package record
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/ccfos/nightingale/v6/alert/aconf"
|
||||
"github.com/ccfos/nightingale/v6/alert/astats"
|
||||
"github.com/ccfos/nightingale/v6/alert/naming"
|
||||
"github.com/ccfos/nightingale/v6/memsto"
|
||||
"github.com/ccfos/nightingale/v6/prom"
|
||||
"github.com/ccfos/nightingale/v6/pushgw/writer"
|
||||
)
|
||||
|
||||
type Scheduler struct {
|
||||
// key: hash
|
||||
recordRules map[string]*RecordRuleContext
|
||||
|
||||
aconf aconf.Alert
|
||||
|
||||
recordingRuleCache *memsto.RecordingRuleCacheType
|
||||
|
||||
promClients *prom.PromClientMap
|
||||
writers *writer.WritersType
|
||||
|
||||
stats *astats.Stats
|
||||
}
|
||||
|
||||
func NewScheduler(aconf aconf.Alert, rrc *memsto.RecordingRuleCacheType, promClients *prom.PromClientMap, writers *writer.WritersType, stats *astats.Stats) *Scheduler {
|
||||
scheduler := &Scheduler{
|
||||
aconf: aconf,
|
||||
recordRules: make(map[string]*RecordRuleContext),
|
||||
|
||||
recordingRuleCache: rrc,
|
||||
|
||||
promClients: promClients,
|
||||
writers: writers,
|
||||
|
||||
stats: stats,
|
||||
}
|
||||
|
||||
go scheduler.LoopSyncRules(context.Background())
|
||||
return scheduler
|
||||
}
|
||||
|
||||
func (s *Scheduler) LoopSyncRules(ctx context.Context) {
|
||||
time.Sleep(time.Duration(s.aconf.EngineDelay) * time.Second)
|
||||
duration := 9000 * time.Millisecond
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-time.After(duration):
|
||||
s.syncRecordRules()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Scheduler) syncRecordRules() {
|
||||
ids := s.recordingRuleCache.GetRuleIds()
|
||||
recordRules := make(map[string]*RecordRuleContext)
|
||||
for _, id := range ids {
|
||||
rule := s.recordingRuleCache.Get(id)
|
||||
if rule == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
datasourceIds := s.promClients.Hit(rule.DatasourceIdsJson)
|
||||
for _, dsId := range datasourceIds {
|
||||
if !naming.DatasourceHashRing.IsHit(dsId, fmt.Sprintf("%d", rule.Id), s.aconf.Heartbeat.Endpoint) {
|
||||
continue
|
||||
}
|
||||
|
||||
recordRule := NewRecordRuleContext(rule, dsId, s.promClients, s.writers, s.stats)
|
||||
recordRules[recordRule.Hash()] = recordRule
|
||||
}
|
||||
}
|
||||
|
||||
for hash, rule := range recordRules {
|
||||
if _, has := s.recordRules[hash]; !has {
|
||||
rule.Prepare()
|
||||
rule.Start()
|
||||
s.recordRules[hash] = rule
|
||||
}
|
||||
}
|
||||
|
||||
for hash, rule := range s.recordRules {
|
||||
if _, has := recordRules[hash]; !has {
|
||||
rule.Stop()
|
||||
delete(s.recordRules, hash)
|
||||
}
|
||||
}
|
||||
}
|
||||
79
alert/router/router.go
Normal file
79
alert/router/router.go
Normal file
@@ -0,0 +1,79 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/ccfos/nightingale/v6/alert/aconf"
|
||||
"github.com/ccfos/nightingale/v6/alert/astats"
|
||||
"github.com/ccfos/nightingale/v6/alert/process"
|
||||
"github.com/ccfos/nightingale/v6/memsto"
|
||||
"github.com/ccfos/nightingale/v6/pkg/ctx"
|
||||
"github.com/ccfos/nightingale/v6/pkg/httpx"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type Router struct {
|
||||
HTTP httpx.Config
|
||||
Alert aconf.Alert
|
||||
AlertMuteCache *memsto.AlertMuteCacheType
|
||||
TargetCache *memsto.TargetCacheType
|
||||
BusiGroupCache *memsto.BusiGroupCacheType
|
||||
AlertStats *astats.Stats
|
||||
Ctx *ctx.Context
|
||||
ExternalProcessors *process.ExternalProcessorsType
|
||||
}
|
||||
|
||||
func New(httpConfig httpx.Config, alert aconf.Alert, amc *memsto.AlertMuteCacheType, tc *memsto.TargetCacheType, bgc *memsto.BusiGroupCacheType,
|
||||
astats *astats.Stats, ctx *ctx.Context, externalProcessors *process.ExternalProcessorsType) *Router {
|
||||
return &Router{
|
||||
HTTP: httpConfig,
|
||||
Alert: alert,
|
||||
AlertMuteCache: amc,
|
||||
TargetCache: tc,
|
||||
BusiGroupCache: bgc,
|
||||
AlertStats: astats,
|
||||
Ctx: ctx,
|
||||
ExternalProcessors: externalProcessors,
|
||||
}
|
||||
}
|
||||
|
||||
func (rt *Router) Config(r *gin.Engine) {
|
||||
if !rt.HTTP.APIForService.Enable {
|
||||
return
|
||||
}
|
||||
|
||||
service := r.Group("/v1/n9e")
|
||||
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)
|
||||
service.POST("/make-event", rt.makeEvent)
|
||||
}
|
||||
|
||||
func Render(c *gin.Context, data, msg interface{}) {
|
||||
if msg == nil {
|
||||
if data == nil {
|
||||
data = struct{}{}
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"data": data, "error": ""})
|
||||
} else {
|
||||
c.JSON(http.StatusOK, gin.H{"error": gin.H{"message": msg}})
|
||||
}
|
||||
}
|
||||
|
||||
func Dangerous(c *gin.Context, v interface{}, code ...int) {
|
||||
if v == nil {
|
||||
return
|
||||
}
|
||||
|
||||
switch t := v.(type) {
|
||||
case string:
|
||||
if t != "" {
|
||||
c.JSON(http.StatusOK, gin.H{"error": gin.H{"message": v}})
|
||||
}
|
||||
case error:
|
||||
c.JSON(http.StatusOK, gin.H{"error": gin.H{"message": t.Error()}})
|
||||
}
|
||||
}
|
||||
146
alert/router/router_event.go
Normal file
146
alert/router/router_event.go
Normal file
@@ -0,0 +1,146 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/ccfos/nightingale/v6/alert/common"
|
||||
"github.com/ccfos/nightingale/v6/alert/dispatch"
|
||||
"github.com/ccfos/nightingale/v6/alert/mute"
|
||||
"github.com/ccfos/nightingale/v6/alert/naming"
|
||||
"github.com/ccfos/nightingale/v6/alert/process"
|
||||
"github.com/ccfos/nightingale/v6/alert/queue"
|
||||
"github.com/ccfos/nightingale/v6/models"
|
||||
"github.com/ccfos/nightingale/v6/pkg/poster"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/toolkits/pkg/ginx"
|
||||
"github.com/toolkits/pkg/logger"
|
||||
)
|
||||
|
||||
func (rt *Router) pushEventToQueue(c *gin.Context) {
|
||||
var event *models.AlertCurEvent
|
||||
ginx.BindJSON(c, &event)
|
||||
if event.RuleId == 0 {
|
||||
ginx.Bomb(200, "event is illegal")
|
||||
}
|
||||
|
||||
event.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
|
||||
}
|
||||
|
||||
event.TagsMap[arr[0]] = arr[1]
|
||||
}
|
||||
|
||||
if mute.EventMuteStrategy(event, rt.AlertMuteCache) {
|
||||
logger.Infof("event_muted: rule_id=%d %s", event.RuleId, event.Hash)
|
||||
ginx.NewRender(c).Message(nil)
|
||||
return
|
||||
}
|
||||
|
||||
if err := event.ParseRule("rule_name"); err != nil {
|
||||
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 {
|
||||
event.RuleNote = fmt.Sprintf("failed to parse rule note: %v", err)
|
||||
}
|
||||
|
||||
// 如果 rule_note 中有 ; 前缀,则使用 rule_note 替换 tags 中的内容
|
||||
if strings.HasPrefix(event.RuleNote, ";") {
|
||||
event.RuleNote = strings.TrimPrefix(event.RuleNote, ";")
|
||||
event.Tags = strings.ReplaceAll(event.RuleNote, " ", ",,")
|
||||
event.TagsJSON = strings.Split(event.Tags, ",,")
|
||||
} else {
|
||||
event.Tags = strings.Join(event.TagsJSON, ",,")
|
||||
}
|
||||
|
||||
event.Callbacks = strings.Join(event.CallbacksJSON, " ")
|
||||
event.NotifyChannels = strings.Join(event.NotifyChannelsJSON, " ")
|
||||
event.NotifyGroups = strings.Join(event.NotifyGroupsJSON, " ")
|
||||
|
||||
dispatch.LogEvent(event, "http_push_queue")
|
||||
if !queue.EventQueue.PushFront(event) {
|
||||
msg := fmt.Sprintf("event:%+v push_queue err: queue is full", event)
|
||||
ginx.Bomb(200, msg)
|
||||
logger.Warningf(msg)
|
||||
}
|
||||
ginx.NewRender(c).Message(nil)
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
|
||||
type eventForm struct {
|
||||
Alert bool `json:"alert"`
|
||||
AnomalyPoints []common.AnomalyPoint `json:"vectors"`
|
||||
RuleId int64 `json:"rule_id"`
|
||||
DatasourceId int64 `json:"datasource_id"`
|
||||
Inhibit bool `json:"inhibit"`
|
||||
}
|
||||
|
||||
func (rt *Router) makeEvent(c *gin.Context) {
|
||||
var events []*eventForm
|
||||
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))
|
||||
if err != nil {
|
||||
logger.Warningf("event:%+v get node err:%v", events[i], err)
|
||||
ginx.Bomb(200, "event node not exists")
|
||||
}
|
||||
|
||||
if node != rt.Alert.Heartbeat.Endpoint {
|
||||
err := forwardEvent(events[i], node)
|
||||
if err != nil {
|
||||
logger.Warningf("event:%+v forward err:%v", events[i], err)
|
||||
ginx.Bomb(200, "event forward error")
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
ruleWorker, exists := rt.ExternalProcessors.GetExternalAlertRule(events[i].DatasourceId, events[i].RuleId)
|
||||
logger.Debugf("handle event:%+v exists:%v", events[i], exists)
|
||||
if !exists {
|
||||
ginx.Bomb(200, "rule not exists")
|
||||
}
|
||||
|
||||
if events[i].Alert {
|
||||
go ruleWorker.Handle(events[i].AnomalyPoints, "http", events[i].Inhibit)
|
||||
} else {
|
||||
for _, vector := range events[i].AnomalyPoints {
|
||||
readableString := vector.ReadableValue()
|
||||
go ruleWorker.RecoverSingle(process.Hash(events[i].RuleId, events[i].DatasourceId, vector), vector.Timestamp, &readableString)
|
||||
}
|
||||
}
|
||||
}
|
||||
ginx.NewRender(c).Message(nil)
|
||||
}
|
||||
|
||||
// event 不归本实例处理,转发给对应的实例
|
||||
func forwardEvent(event *eventForm, instance string) error {
|
||||
ur := fmt.Sprintf("http://%s/v1/n9e/make-event", instance)
|
||||
res, code, err := poster.PostJSON(ur, time.Second*5, []*eventForm{event}, 3)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
logger.Infof("forward event: result=succ url=%s code=%d event:%v response=%s", ur, code, event, string(res))
|
||||
return nil
|
||||
}
|
||||
222
alert/sender/callback.go
Normal file
222
alert/sender/callback.go
Normal file
@@ -0,0 +1,222 @@
|
||||
package sender
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strconv"
|
||||
"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, stats *astats.Stats) {
|
||||
for _, url := range urls {
|
||||
if url == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.HasPrefix(url, "${ibex}") {
|
||||
if !event.IsRecovered {
|
||||
handleIbex(ctx, url, event, targetCache, userCache, ibexConf)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if !(strings.HasPrefix(url, "http://") || strings.HasPrefix(url, "https://")) {
|
||||
url = "http://" + url
|
||||
}
|
||||
|
||||
stats.AlertNotifyTotal.WithLabelValues("rule_callback").Inc()
|
||||
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)
|
||||
stats.AlertNotifyErrorTotal.WithLabelValues("rule_callback").Inc()
|
||||
} else {
|
||||
logger.Infof("event_callback_succ(rule_id=%d url=%s), resp: %s, code: %d", event.RuleId, url, string(resp), code)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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"`
|
||||
Stdin string `json:"stdin"`
|
||||
Action string `json:"action"`
|
||||
Creator string `json:"creator"`
|
||||
Hosts []string `json:"hosts"`
|
||||
}
|
||||
|
||||
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.TaskTplGetById(ctx, 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
|
||||
}
|
||||
|
||||
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 := 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},
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
105
alert/sender/dingtalk.go
Normal file
105
alert/sender/dingtalk.go
Normal file
@@ -0,0 +1,105 @@
|
||||
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"
|
||||
)
|
||||
|
||||
type dingtalkMarkdown struct {
|
||||
Title string `json:"title"`
|
||||
Text string `json:"text"`
|
||||
}
|
||||
|
||||
type dingtalkAt struct {
|
||||
AtMobiles []string `json:"atMobiles"`
|
||||
IsAtAll bool `json:"isAtAll"`
|
||||
}
|
||||
|
||||
type dingtalk struct {
|
||||
Msgtype string `json:"msgtype"`
|
||||
Markdown dingtalkMarkdown `json:"markdown"`
|
||||
At dingtalkAt `json:"at"`
|
||||
}
|
||||
|
||||
type DingtalkSender struct {
|
||||
tpl *template.Template
|
||||
}
|
||||
|
||||
func (ds *DingtalkSender) Send(ctx MessageContext) {
|
||||
if len(ctx.Users) == 0 || len(ctx.Events) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
urls, ats := ds.extract(ctx.Users)
|
||||
if len(urls) == 0 {
|
||||
return
|
||||
}
|
||||
message := BuildTplMessage(ds.tpl, ctx.Events)
|
||||
|
||||
for _, url := range urls {
|
||||
var body dingtalk
|
||||
// NoAt in url
|
||||
if strings.Contains(url, "noat=1") {
|
||||
body = dingtalk{
|
||||
Msgtype: "markdown",
|
||||
Markdown: dingtalkMarkdown{
|
||||
Title: ctx.Events[0].RuleName,
|
||||
Text: message,
|
||||
},
|
||||
}
|
||||
} else {
|
||||
body = dingtalk{
|
||||
Msgtype: "markdown",
|
||||
Markdown: dingtalkMarkdown{
|
||||
Title: ctx.Events[0].RuleName,
|
||||
Text: message + "\n" + strings.Join(ats, " "),
|
||||
},
|
||||
At: dingtalkAt{
|
||||
AtMobiles: ats,
|
||||
IsAtAll: false,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
doSend(url, body, models.Dingtalk, ctx.Stats)
|
||||
}
|
||||
}
|
||||
|
||||
// extract urls and ats from Users
|
||||
func (ds *DingtalkSender) extract(users []*models.User) ([]string, []string) {
|
||||
urls := make([]string, 0, len(users))
|
||||
ats := make([]string, 0, len(users))
|
||||
|
||||
for _, user := range users {
|
||||
if user.Phone != "" {
|
||||
ats = append(ats, "@"+user.Phone)
|
||||
}
|
||||
if token, has := user.ExtractToken(models.Dingtalk); has {
|
||||
url := token
|
||||
if !strings.HasPrefix(token, "https://") && !strings.HasPrefix(token, "http://") {
|
||||
url = "https://oapi.dingtalk.com/robot/send?access_token=" + token
|
||||
}
|
||||
urls = append(urls, url)
|
||||
}
|
||||
}
|
||||
return urls, ats
|
||||
}
|
||||
|
||||
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 response=%s", channel, url, code, err, string(res))
|
||||
stats.AlertNotifyErrorTotal.WithLabelValues(channel).Inc()
|
||||
} else {
|
||||
logger.Infof("%s_sender: result=succ url=%s code=%d response=%s", channel, url, code, string(res))
|
||||
}
|
||||
}
|
||||
179
alert/sender/email.go
Normal file
179
alert/sender/email.go
Normal file
@@ -0,0 +1,179 @@
|
||||
package sender
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"html/template"
|
||||
"time"
|
||||
|
||||
"github.com/ccfos/nightingale/v6/alert/aconf"
|
||||
"github.com/ccfos/nightingale/v6/models"
|
||||
|
||||
"github.com/toolkits/pkg/logger"
|
||||
|
||||
"gopkg.in/gomail.v2"
|
||||
)
|
||||
|
||||
var mailch chan *gomail.Message
|
||||
|
||||
type EmailSender struct {
|
||||
subjectTpl *template.Template
|
||||
contentTpl *template.Template
|
||||
smtp aconf.SMTPConfig
|
||||
}
|
||||
|
||||
func (es *EmailSender) Send(ctx MessageContext) {
|
||||
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, []*models.AlertCurEvent{ctx.Events[0]})
|
||||
} else {
|
||||
subject = ctx.Events[0].RuleName
|
||||
}
|
||||
content := BuildTplMessage(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 {
|
||||
tos := make([]string, 0, len(users))
|
||||
for _, u := range users {
|
||||
if u.Email != "" {
|
||||
tos = append(tos, u.Email)
|
||||
}
|
||||
}
|
||||
return tos
|
||||
}
|
||||
|
||||
func SendEmail(subject, content string, tos []string, stmp aconf.SMTPConfig) error {
|
||||
conf := stmp
|
||||
|
||||
d := gomail.NewDialer(conf.Host, conf.Port, conf.User, conf.Pass)
|
||||
if conf.InsecureSkipVerify {
|
||||
d.TLSConfig = &tls.Config{InsecureSkipVerify: true}
|
||||
}
|
||||
|
||||
m := gomail.NewMessage()
|
||||
|
||||
m.SetHeader("From", stmp.From)
|
||||
m.SetHeader("To", tos...)
|
||||
m.SetHeader("Subject", subject)
|
||||
m.SetBody("text/html", content)
|
||||
|
||||
err := d.DialAndSend(m)
|
||||
if err != nil {
|
||||
return errors.New("email_sender: failed to send: " + err.Error())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (es *EmailSender) WriteEmail(subject, content string, tos []string) {
|
||||
m := gomail.NewMessage()
|
||||
|
||||
m.SetHeader("From", es.smtp.From)
|
||||
m.SetHeader("To", tos...)
|
||||
m.SetHeader("Subject", subject)
|
||||
m.SetBody("text/html", content)
|
||||
|
||||
mailch <- m
|
||||
}
|
||||
|
||||
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)
|
||||
time.Sleep(time.Second)
|
||||
continue
|
||||
} else {
|
||||
return s
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var mailQuit = make(chan struct{})
|
||||
|
||||
func RestartEmailSender(smtp aconf.SMTPConfig) {
|
||||
close(mailQuit)
|
||||
mailQuit = make(chan struct{})
|
||||
startEmailSender(smtp)
|
||||
}
|
||||
|
||||
func InitEmailSender(smtp aconf.SMTPConfig) {
|
||||
mailch = make(chan *gomail.Message, 100000)
|
||||
startEmailSender(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)
|
||||
|
||||
d := gomail.NewDialer(conf.Host, conf.Port, conf.User, conf.Pass)
|
||||
if conf.InsecureSkipVerify {
|
||||
d.TLSConfig = &tls.Config{InsecureSkipVerify: true}
|
||||
}
|
||||
|
||||
var s gomail.SendCloser
|
||||
var open bool
|
||||
var size int
|
||||
for {
|
||||
select {
|
||||
case <-mailQuit:
|
||||
return
|
||||
case m, ok := <-mailch:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
if !open {
|
||||
s = dialSmtp(d)
|
||||
open = true
|
||||
}
|
||||
if err := gomail.Send(s, m); err != nil {
|
||||
logger.Errorf("email_sender: failed to send: %s", err)
|
||||
|
||||
// close and retry
|
||||
if err := s.Close(); err != nil {
|
||||
logger.Warningf("email_sender: failed to close smtp connection: %s", err)
|
||||
}
|
||||
|
||||
s = dialSmtp(d)
|
||||
open = true
|
||||
|
||||
if err := gomail.Send(s, m); err != nil {
|
||||
logger.Errorf("email_sender: failed to retry send: %s", err)
|
||||
}
|
||||
} else {
|
||||
logger.Infof("email_sender: result=succ subject=%v to=%v", m.GetHeader("Subject"), m.GetHeader("To"))
|
||||
}
|
||||
|
||||
size++
|
||||
|
||||
if size >= conf.Batch {
|
||||
if err := s.Close(); err != nil {
|
||||
logger.Warningf("email_sender: failed to close smtp connection: %s", err)
|
||||
}
|
||||
open = false
|
||||
size = 0
|
||||
}
|
||||
|
||||
// Close the connection to the SMTP server if no email was sent in
|
||||
// the last 30 seconds.
|
||||
case <-time.After(30 * time.Second):
|
||||
if open {
|
||||
if err := s.Close(); err != nil {
|
||||
logger.Warningf("email_sender: failed to close smtp connection: %s", err)
|
||||
}
|
||||
open = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
69
alert/sender/feishu.go
Normal file
69
alert/sender/feishu.go
Normal file
@@ -0,0 +1,69 @@
|
||||
package sender
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"strings"
|
||||
|
||||
"github.com/ccfos/nightingale/v6/models"
|
||||
)
|
||||
|
||||
type feishuContent struct {
|
||||
Text string `json:"text"`
|
||||
}
|
||||
|
||||
type feishuAt struct {
|
||||
AtMobiles []string `json:"atMobiles"`
|
||||
IsAtAll bool `json:"isAtAll"`
|
||||
}
|
||||
|
||||
type feishu struct {
|
||||
Msgtype string `json:"msg_type"`
|
||||
Content feishuContent `json:"content"`
|
||||
At feishuAt `json:"at"`
|
||||
}
|
||||
|
||||
type FeishuSender struct {
|
||||
tpl *template.Template
|
||||
}
|
||||
|
||||
func (fs *FeishuSender) Send(ctx MessageContext) {
|
||||
if len(ctx.Users) == 0 || len(ctx.Events) == 0 {
|
||||
return
|
||||
}
|
||||
urls, ats := fs.extract(ctx.Users)
|
||||
message := BuildTplMessage(fs.tpl, ctx.Events)
|
||||
for _, url := range urls {
|
||||
body := feishu{
|
||||
Msgtype: "text",
|
||||
Content: feishuContent{
|
||||
Text: message,
|
||||
},
|
||||
}
|
||||
if !strings.Contains(url, "noat=1") {
|
||||
body.At = feishuAt{
|
||||
AtMobiles: ats,
|
||||
IsAtAll: false,
|
||||
}
|
||||
}
|
||||
doSend(url, body, models.Feishu, ctx.Stats)
|
||||
}
|
||||
}
|
||||
|
||||
func (fs *FeishuSender) extract(users []*models.User) ([]string, []string) {
|
||||
urls := make([]string, 0, len(users))
|
||||
ats := make([]string, 0, len(users))
|
||||
|
||||
for _, user := range users {
|
||||
if user.Phone != "" {
|
||||
ats = append(ats, user.Phone)
|
||||
}
|
||||
if token, has := user.ExtractToken(models.Feishu); has {
|
||||
url := token
|
||||
if !strings.HasPrefix(token, "https://") && !strings.HasPrefix(token, "http://") {
|
||||
url = "https://open.feishu.cn/open-apis/bot/v2/hook/" + token
|
||||
}
|
||||
urls = append(urls, url)
|
||||
}
|
||||
}
|
||||
return urls, ats
|
||||
}
|
||||
131
alert/sender/feishucard.go
Normal file
131
alert/sender/feishucard.go
Normal file
@@ -0,0 +1,131 @@
|
||||
package sender
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"html/template"
|
||||
"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) Send(ctx MessageContext) {
|
||||
if len(ctx.Users) == 0 || len(ctx.Events) == 0 {
|
||||
return
|
||||
}
|
||||
urls, _ := fs.extract(ctx.Users)
|
||||
message := BuildTplMessage(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
|
||||
}
|
||||
102
alert/sender/mm.go
Normal file
102
alert/sender/mm.go
Normal file
@@ -0,0 +1,102 @@
|
||||
package sender
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/ccfos/nightingale/v6/alert/astats"
|
||||
"github.com/ccfos/nightingale/v6/models"
|
||||
|
||||
"github.com/toolkits/pkg/logger"
|
||||
)
|
||||
|
||||
type MatterMostMessage struct {
|
||||
Text string
|
||||
Tokens []string
|
||||
Stats *astats.Stats
|
||||
}
|
||||
|
||||
type mm struct {
|
||||
Channel string `json:"channel"`
|
||||
Username string `json:"username"`
|
||||
Text string `json:"text"`
|
||||
}
|
||||
|
||||
type MmSender struct {
|
||||
tpl *template.Template
|
||||
}
|
||||
|
||||
func (ms *MmSender) Send(ctx MessageContext) {
|
||||
if len(ctx.Users) == 0 || len(ctx.Events) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
urls := ms.extract(ctx.Users)
|
||||
if len(urls) == 0 {
|
||||
return
|
||||
}
|
||||
message := BuildTplMessage(ms.tpl, ctx.Events)
|
||||
|
||||
SendMM(MatterMostMessage{
|
||||
Text: message,
|
||||
Tokens: urls,
|
||||
Stats: ctx.Stats,
|
||||
})
|
||||
}
|
||||
|
||||
func (ms *MmSender) extract(users []*models.User) []string {
|
||||
tokens := make([]string, 0, len(users))
|
||||
for _, user := range users {
|
||||
if token, has := user.ExtractToken(models.Mm); has {
|
||||
tokens = append(tokens, token)
|
||||
}
|
||||
}
|
||||
return tokens
|
||||
}
|
||||
|
||||
func SendMM(message MatterMostMessage) {
|
||||
for i := 0; i < len(message.Tokens); i++ {
|
||||
u, err := url.Parse(message.Tokens[i])
|
||||
if err != nil {
|
||||
logger.Errorf("mm_sender: failed to parse error=%v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
v, err := url.ParseQuery(u.RawQuery)
|
||||
if err != nil {
|
||||
logger.Errorf("mm_sender: failed to parse query error=%v", err)
|
||||
}
|
||||
|
||||
channels := v["channel"] // do not get
|
||||
txt := ""
|
||||
atuser := v["atuser"]
|
||||
if len(atuser) != 0 {
|
||||
txt = strings.Join(MapStrToStr(atuser, func(u string) string {
|
||||
return "@" + u
|
||||
}), ",") + "\n"
|
||||
}
|
||||
username := v.Get("username")
|
||||
if err != nil {
|
||||
logger.Errorf("mm_sender: failed to parse error=%v", err)
|
||||
}
|
||||
// simple concatenating
|
||||
ur := u.Scheme + "://" + u.Host + u.Path
|
||||
for _, channel := range channels {
|
||||
body := mm{
|
||||
Channel: channel,
|
||||
Username: username,
|
||||
Text: txt + message.Text,
|
||||
}
|
||||
doSend(ur, body, models.Mm, message.Stats)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func MapStrToStr(arr []string, fn func(s string) string) []string {
|
||||
var newArray = []string{}
|
||||
for _, it := range arr {
|
||||
newArray = append(newArray, fn(it))
|
||||
}
|
||||
return newArray
|
||||
}
|
||||
104
alert/sender/plugin.go
Normal file
104
alert/sender/plugin.go
Normal file
@@ -0,0 +1,104 @@
|
||||
package sender
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"os/exec"
|
||||
"time"
|
||||
|
||||
"github.com/ccfos/nightingale/v6/alert/astats"
|
||||
"github.com/ccfos/nightingale/v6/models"
|
||||
|
||||
"github.com/toolkits/pkg/file"
|
||||
"github.com/toolkits/pkg/logger"
|
||||
"github.com/toolkits/pkg/sys"
|
||||
)
|
||||
|
||||
func MayPluginNotify(noticeBytes []byte, notifyScript models.NotifyScript, stats *astats.Stats) {
|
||||
if len(noticeBytes) == 0 {
|
||||
return
|
||||
}
|
||||
alertingCallScript(noticeBytes, notifyScript, stats)
|
||||
}
|
||||
|
||||
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
|
||||
} else {
|
||||
rewrite := true
|
||||
if file.IsExist(fpath) {
|
||||
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
|
||||
}
|
||||
|
||||
if oldContent == config.Content {
|
||||
rewrite = false
|
||||
}
|
||||
}
|
||||
|
||||
if rewrite {
|
||||
_, 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
|
||||
}
|
||||
}
|
||||
fpath = "./" + fpath
|
||||
}
|
||||
|
||||
cmd := exec.Command(fpath)
|
||||
cmd.Stdin = bytes.NewReader(stdinBytes)
|
||||
|
||||
// combine stdout and stderr
|
||||
var buf bytes.Buffer
|
||||
cmd.Stdout = &buf
|
||||
cmd.Stderr = &buf
|
||||
|
||||
err := startCmd(cmd)
|
||||
if err != nil {
|
||||
logger.Errorf("event_script_notify_fail: run cmd err: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
err, isTimeout := sys.WrapTimeout(cmd, time.Duration(config.Timeout)*time.Second)
|
||||
|
||||
if isTimeout {
|
||||
if err == nil {
|
||||
logger.Errorf("event_script_notify_fail: timeout and killed process %s", fpath)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
logger.Infof("event_script_notify_ok: exec %s output: %s", fpath, buf.String())
|
||||
}
|
||||
14
alert/sender/plugin_cmd_unix.go
Normal file
14
alert/sender/plugin_cmd_unix.go
Normal file
@@ -0,0 +1,14 @@
|
||||
//go:build !windows
|
||||
// +build !windows
|
||||
|
||||
package sender
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
func startCmd(c *exec.Cmd) error {
|
||||
c.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
|
||||
return c.Start()
|
||||
}
|
||||
7
alert/sender/plugin_cmd_windows.go
Normal file
7
alert/sender/plugin_cmd_windows.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package sender
|
||||
|
||||
import "os/exec"
|
||||
|
||||
func startCmd(c *exec.Cmd) error {
|
||||
return c.Start()
|
||||
}
|
||||
77
alert/sender/sender.go
Normal file
77
alert/sender/sender.go
Normal file
@@ -0,0 +1,77 @@
|
||||
package sender
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"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"
|
||||
)
|
||||
|
||||
type (
|
||||
// Sender 发送消息通知的接口
|
||||
Sender interface {
|
||||
Send(ctx MessageContext)
|
||||
}
|
||||
|
||||
// MessageContext 一个event所生成的告警通知的上下文
|
||||
MessageContext struct {
|
||||
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 {
|
||||
switch key {
|
||||
case models.Dingtalk:
|
||||
return &DingtalkSender{tpl: tpls[models.Dingtalk]}
|
||||
case models.Wecom:
|
||||
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[models.EmailSubject], contentTpl: tpls[models.Email], smtp: smtp[0]}
|
||||
case models.Mm:
|
||||
return &MmSender{tpl: tpls[models.Mm]}
|
||||
case models.Telegram:
|
||||
return &TelegramSender{tpl: tpls[models.Telegram]}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
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,
|
||||
Events: events,
|
||||
Users: users,
|
||||
Stats: stats,
|
||||
}
|
||||
}
|
||||
|
||||
type BuildTplMessageFunc func(tpl *template.Template, events []*models.AlertCurEvent) string
|
||||
|
||||
var BuildTplMessage BuildTplMessageFunc = buildTplMessage
|
||||
|
||||
func buildTplMessage(tpl *template.Template, events []*models.AlertCurEvent) string {
|
||||
if tpl == nil {
|
||||
return "tpl for current sender not found, please check configuration"
|
||||
}
|
||||
|
||||
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 content
|
||||
}
|
||||
78
alert/sender/telegram.go
Normal file
78
alert/sender/telegram.go
Normal file
@@ -0,0 +1,78 @@
|
||||
package sender
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"strings"
|
||||
|
||||
"github.com/ccfos/nightingale/v6/alert/astats"
|
||||
"github.com/ccfos/nightingale/v6/models"
|
||||
|
||||
"github.com/toolkits/pkg/logger"
|
||||
)
|
||||
|
||||
type TelegramMessage struct {
|
||||
Text string
|
||||
Tokens []string
|
||||
Stats *astats.Stats
|
||||
}
|
||||
|
||||
type telegram struct {
|
||||
ParseMode string `json:"parse_mode"`
|
||||
Text string `json:"text"`
|
||||
}
|
||||
|
||||
type TelegramSender struct {
|
||||
tpl *template.Template
|
||||
}
|
||||
|
||||
func (ts *TelegramSender) Send(ctx MessageContext) {
|
||||
if len(ctx.Users) == 0 || len(ctx.Events) == 0 {
|
||||
return
|
||||
}
|
||||
tokens := ts.extract(ctx.Users)
|
||||
message := BuildTplMessage(ts.tpl, ctx.Events)
|
||||
|
||||
SendTelegram(TelegramMessage{
|
||||
Text: message,
|
||||
Tokens: tokens,
|
||||
Stats: ctx.Stats,
|
||||
})
|
||||
}
|
||||
|
||||
func (ts *TelegramSender) extract(users []*models.User) []string {
|
||||
tokens := make([]string, 0, len(users))
|
||||
for _, user := range users {
|
||||
if token, has := user.ExtractToken(models.Telegram); has {
|
||||
tokens = append(tokens, token)
|
||||
}
|
||||
}
|
||||
return tokens
|
||||
}
|
||||
|
||||
func SendTelegram(message TelegramMessage) {
|
||||
for i := 0; i < len(message.Tokens); i++ {
|
||||
if !strings.Contains(message.Tokens[i], "/") && !strings.HasPrefix(message.Tokens[i], "https://") {
|
||||
logger.Errorf("telegram_sender: result=fail invalid token=%s", message.Tokens[i])
|
||||
continue
|
||||
}
|
||||
var url string
|
||||
if strings.HasPrefix(message.Tokens[i], "https://") || strings.HasPrefix(message.Tokens[i], "http://") {
|
||||
url = message.Tokens[i]
|
||||
} else {
|
||||
array := strings.Split(message.Tokens[i], "/")
|
||||
if len(array) != 2 {
|
||||
logger.Errorf("telegram_sender: result=fail invalid token=%s", message.Tokens[i])
|
||||
continue
|
||||
}
|
||||
botToken := array[0]
|
||||
chatId := array[1]
|
||||
url = "https://api.telegram.org/bot" + botToken + "/sendMessage?chat_id=" + chatId
|
||||
}
|
||||
body := telegram{
|
||||
ParseMode: "markdown",
|
||||
Text: message.Text,
|
||||
}
|
||||
|
||||
doSend(url, body, models.Telegram, message.Stats)
|
||||
}
|
||||
}
|
||||
71
alert/sender/webhook.go
Normal file
71
alert/sender/webhook.go
Normal file
@@ -0,0 +1,71 @@
|
||||
package sender
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"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, stats *astats.Stats) {
|
||||
for _, conf := range webhooks {
|
||||
if conf.Url == "" || !conf.Enable {
|
||||
continue
|
||||
}
|
||||
bs, err := json.Marshal(event)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
bf := bytes.NewBuffer(bs)
|
||||
|
||||
req, err := http.NewRequest("POST", conf.Url, bf)
|
||||
if err != nil {
|
||||
logger.Warning("alertingWebhook failed to new request", err)
|
||||
continue
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
if conf.BasicAuthUser != "" && conf.BasicAuthPass != "" {
|
||||
req.SetBasicAuth(conf.BasicAuthUser, conf.BasicAuthPass)
|
||||
}
|
||||
|
||||
if len(conf.Headers) > 0 && len(conf.Headers)%2 == 0 {
|
||||
for i := 0; i < len(conf.Headers); i += 2 {
|
||||
if conf.Headers[i] == "host" {
|
||||
req.Host = conf.Headers[i+1]
|
||||
continue
|
||||
}
|
||||
req.Header.Set(conf.Headers[i], conf.Headers[i+1])
|
||||
}
|
||||
}
|
||||
|
||||
// todo add skip verify
|
||||
client := http.Client{
|
||||
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
|
||||
}
|
||||
|
||||
var body []byte
|
||||
if resp.Body != nil {
|
||||
defer resp.Body.Close()
|
||||
body, _ = io.ReadAll(resp.Body)
|
||||
}
|
||||
|
||||
logger.Debugf("event_webhook_succ, url: %s, response code: %d, body: %s", conf.Url, resp.StatusCode, string(body))
|
||||
}
|
||||
}
|
||||
52
alert/sender/wecom.go
Normal file
52
alert/sender/wecom.go
Normal file
@@ -0,0 +1,52 @@
|
||||
package sender
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"strings"
|
||||
|
||||
"github.com/ccfos/nightingale/v6/models"
|
||||
)
|
||||
|
||||
type wecomMarkdown struct {
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
type wecom struct {
|
||||
Msgtype string `json:"msgtype"`
|
||||
Markdown wecomMarkdown `json:"markdown"`
|
||||
}
|
||||
|
||||
type WecomSender struct {
|
||||
tpl *template.Template
|
||||
}
|
||||
|
||||
func (ws *WecomSender) Send(ctx MessageContext) {
|
||||
if len(ctx.Users) == 0 || len(ctx.Events) == 0 {
|
||||
return
|
||||
}
|
||||
urls := ws.extract(ctx.Users)
|
||||
message := BuildTplMessage(ws.tpl, ctx.Events)
|
||||
for _, url := range urls {
|
||||
body := wecom{
|
||||
Msgtype: "markdown",
|
||||
Markdown: wecomMarkdown{
|
||||
Content: message,
|
||||
},
|
||||
}
|
||||
doSend(url, body, models.Wecom, ctx.Stats)
|
||||
}
|
||||
}
|
||||
|
||||
func (ws *WecomSender) extract(users []*models.User) []string {
|
||||
urls := make([]string, 0, len(users))
|
||||
for _, user := range users {
|
||||
if token, has := user.ExtractToken(models.Wecom); has {
|
||||
url := token
|
||||
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)
|
||||
}
|
||||
}
|
||||
return urls
|
||||
}
|
||||
30
center/cconf/conf.go
Normal file
30
center/cconf/conf.go
Normal file
@@ -0,0 +1,30 @@
|
||||
package cconf
|
||||
|
||||
type Center struct {
|
||||
Plugins []Plugin
|
||||
MetricsYamlFile string
|
||||
OpsYamlFile string
|
||||
BuiltinIntegrationsDir string
|
||||
I18NHeaderKey string
|
||||
MetricDesc MetricDescType
|
||||
AnonymousAccess AnonymousAccess
|
||||
UseFileAssets bool
|
||||
}
|
||||
|
||||
type Plugin struct {
|
||||
Id int64 `json:"id"`
|
||||
Category string `json:"category"`
|
||||
Type string `json:"plugin_type"`
|
||||
TypeName string `json:"plugin_type_name"`
|
||||
}
|
||||
|
||||
type AnonymousAccess struct {
|
||||
PromQuerier bool
|
||||
AlertDetail bool
|
||||
}
|
||||
|
||||
func (c *Center) PreCheck() {
|
||||
if len(c.Plugins) == 0 {
|
||||
c.Plugins = Plugins
|
||||
}
|
||||
}
|
||||
60
center/cconf/event_example.go
Normal file
60
center/cconf/event_example.go
Normal file
@@ -0,0 +1,60 @@
|
||||
package cconf
|
||||
|
||||
const EVENT_EXAMPLE = `
|
||||
{
|
||||
"id": 1000000,
|
||||
"cate": "prometheus",
|
||||
"datasource_id": 1,
|
||||
"group_id": 1,
|
||||
"group_name": "Default Busi Group",
|
||||
"hash": "2cb966f9ba1cdc7af94c3796e855955a",
|
||||
"rule_id": 23,
|
||||
"rule_name": "测试告警",
|
||||
"rule_note": "测试告警",
|
||||
"rule_prod": "metric",
|
||||
"rule_config": {
|
||||
"queries": [
|
||||
{
|
||||
"key": "all_hosts",
|
||||
"op": "==",
|
||||
"values": []
|
||||
}
|
||||
],
|
||||
"triggers": [
|
||||
{
|
||||
"duration": 3,
|
||||
"percent": 10,
|
||||
"severity": 3,
|
||||
"type": "pct_target_miss"
|
||||
}
|
||||
]
|
||||
},
|
||||
"prom_for_duration": 60,
|
||||
"prom_eval_interval": 30,
|
||||
"callbacks": ["https://n9e.github.io"],
|
||||
"notify_recovered": 1,
|
||||
"notify_channels": ["dingtalk"],
|
||||
"notify_groups": [],
|
||||
"notify_groups_obj": null,
|
||||
"target_ident": "host01",
|
||||
"target_note": "机器备注",
|
||||
"trigger_time": 1677229517,
|
||||
"trigger_value": "2273533952",
|
||||
"tags": [
|
||||
"__name__=disk_free",
|
||||
"dc=qcloud-dev",
|
||||
"device=vda1",
|
||||
"fstype=ext4",
|
||||
"ident=tt-fc-dev00.nj"
|
||||
],
|
||||
"is_recovered": false,
|
||||
"notify_users_obj": null,
|
||||
"last_eval_time": 1677229517,
|
||||
"last_sent_time": 1677229517,
|
||||
"notify_cur_number": 1,
|
||||
"first_trigger_time": 1677229517,
|
||||
"annotations": {
|
||||
"summary": "测试告警"
|
||||
}
|
||||
}
|
||||
`
|
||||
44
center/cconf/metric.go
Normal file
44
center/cconf/metric.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package cconf
|
||||
|
||||
import (
|
||||
"path"
|
||||
|
||||
"github.com/toolkits/pkg/file"
|
||||
)
|
||||
|
||||
// metricDesc , As load map happens before read map, there is no necessary to use concurrent map for metric desc store
|
||||
type MetricDescType struct {
|
||||
CommonDesc map[string]string `yaml:",inline" json:"common"`
|
||||
Zh map[string]string `yaml:"zh" json:"zh"`
|
||||
En map[string]string `yaml:"en" json:"en"`
|
||||
}
|
||||
|
||||
var MetricDesc MetricDescType
|
||||
|
||||
// GetMetricDesc , if metric is not registered, empty string will be returned
|
||||
func GetMetricDesc(lang, metric string) string {
|
||||
var m map[string]string
|
||||
if lang == "zh" {
|
||||
m = MetricDesc.Zh
|
||||
} else {
|
||||
m = MetricDesc.En
|
||||
}
|
||||
if m != nil {
|
||||
if desc, has := m[metric]; has {
|
||||
return desc
|
||||
}
|
||||
}
|
||||
|
||||
return MetricDesc.CommonDesc[metric]
|
||||
}
|
||||
|
||||
func LoadMetricsYaml(configDir, metricsYamlFile string) error {
|
||||
fp := metricsYamlFile
|
||||
if fp == "" {
|
||||
fp = path.Join(configDir, "metrics.yaml")
|
||||
}
|
||||
if !file.IsExist(fp) {
|
||||
return nil
|
||||
}
|
||||
return file.ReadYaml(fp, &MetricDesc)
|
||||
}
|
||||
38
center/cconf/ops.go
Normal file
38
center/cconf/ops.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package cconf
|
||||
|
||||
import (
|
||||
"path"
|
||||
|
||||
"github.com/toolkits/pkg/file"
|
||||
)
|
||||
|
||||
var Operations = Operation{}
|
||||
|
||||
type Operation struct {
|
||||
Ops []Ops `yaml:"ops"`
|
||||
}
|
||||
|
||||
type Ops struct {
|
||||
Name string `yaml:"name" json:"name"`
|
||||
Cname string `yaml:"cname" json:"cname"`
|
||||
Ops []string `yaml:"ops" json:"ops"`
|
||||
}
|
||||
|
||||
func LoadOpsYaml(configDir string, opsYamlFile string) error {
|
||||
fp := opsYamlFile
|
||||
if fp == "" {
|
||||
fp = path.Join(configDir, "ops.yaml")
|
||||
}
|
||||
if !file.IsExist(fp) {
|
||||
return nil
|
||||
}
|
||||
return file.ReadYaml(fp, &Operations)
|
||||
}
|
||||
|
||||
func GetAllOps(ops []Ops) []string {
|
||||
var ret []string
|
||||
for _, op := range ops {
|
||||
ret = append(ret, op.Ops...)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
28
center/cconf/plugin.go
Normal file
28
center/cconf/plugin.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package cconf
|
||||
|
||||
var Plugins = []Plugin{
|
||||
{
|
||||
Id: 1,
|
||||
Category: "timeseries",
|
||||
Type: "prometheus",
|
||||
TypeName: "Prometheus Like",
|
||||
},
|
||||
{
|
||||
Id: 2,
|
||||
Category: "logging",
|
||||
Type: "elasticsearch",
|
||||
TypeName: "Elasticsearch",
|
||||
},
|
||||
{
|
||||
Id: 3,
|
||||
Category: "loki",
|
||||
Type: "loki",
|
||||
TypeName: "Loki",
|
||||
},
|
||||
{
|
||||
Id: 4,
|
||||
Category: "timeseries",
|
||||
Type: "tdengine",
|
||||
TypeName: "TDengine",
|
||||
},
|
||||
}
|
||||
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",
|
||||
}
|
||||
111
center/center.go
Normal file
111
center/center.go
Normal file
@@ -0,0 +1,111 @@
|
||||
package center
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/ccfos/nightingale/v6/alert"
|
||||
"github.com/ccfos/nightingale/v6/alert/astats"
|
||||
"github.com/ccfos/nightingale/v6/alert/process"
|
||||
"github.com/ccfos/nightingale/v6/center/cconf"
|
||||
"github.com/ccfos/nightingale/v6/center/cstats"
|
||||
"github.com/ccfos/nightingale/v6/center/metas"
|
||||
"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/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"
|
||||
"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"
|
||||
)
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
cconf.LoadMetricsYaml(configDir, config.Center.MetricsYamlFile)
|
||||
cconf.LoadOpsYaml(configDir, config.Center.OpsYamlFile)
|
||||
|
||||
logxClean, err := logx.Init(config.Log)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
i18nx.Init(configDir)
|
||||
cstats.Init()
|
||||
|
||||
db, err := storage.New(config.DB)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ctx := ctx.NewContext(context.Background(), db, true)
|
||||
models.InitRoot(ctx)
|
||||
migrate.Migrate(db)
|
||||
|
||||
redis, err := storage.NewRedis(config.Redis)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
metas := metas.New(redis)
|
||||
idents := idents.New(ctx)
|
||||
|
||||
syncStats := memsto.NewSyncStats()
|
||||
alertStats := astats.NewSyncStats()
|
||||
|
||||
sso := sso.Init(config.Center, ctx)
|
||||
|
||||
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)
|
||||
userCache := memsto.NewUserCache(ctx, syncStats)
|
||||
userGroupCache := memsto.NewUserGroupCache(ctx, syncStats)
|
||||
|
||||
promClients := prom.NewPromClient(ctx, config.Alert.Heartbeat)
|
||||
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, tdengineClients, userCache, userGroupCache)
|
||||
|
||||
writers := writer.NewWriters(config.Pushgw)
|
||||
|
||||
httpx.InitRSAConfig(&config.HTTP.RSA)
|
||||
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, tdengineClients,
|
||||
redis, sso, ctx, metas, idents, targetCache, userCache, userGroupCache)
|
||||
pushgwRouter := pushgwrt.New(config.HTTP, config.Pushgw, targetCache, busiGroupCache, idents, writers, ctx)
|
||||
|
||||
r := httpx.GinEngine(config.Global.RunMode, config.HTTP)
|
||||
|
||||
centerRouter.Config(r)
|
||||
alertrtRouter.Config(r)
|
||||
pushgwRouter.Config(r)
|
||||
dumper.ConfigRouter(r)
|
||||
|
||||
httpClean := httpx.Init(config.HTTP, r)
|
||||
|
||||
return func() {
|
||||
logxClean()
|
||||
httpClean()
|
||||
}, nil
|
||||
}
|
||||
53
center/cstats/stats.go
Normal file
53
center/cstats/stats.go
Normal file
@@ -0,0 +1,53 @@
|
||||
package cstats
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
const Service = "n9e-center"
|
||||
|
||||
var (
|
||||
labels = []string{"service", "code", "path", "method"}
|
||||
|
||||
uptime = prometheus.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Name: "uptime",
|
||||
Help: "HTTP service uptime.",
|
||||
}, []string{"service"},
|
||||
)
|
||||
|
||||
RequestCounter = prometheus.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Name: "http_request_count_total",
|
||||
Help: "Total number of HTTP requests made.",
|
||||
}, labels,
|
||||
)
|
||||
|
||||
RequestDuration = prometheus.NewHistogramVec(
|
||||
prometheus.HistogramOpts{
|
||||
Buckets: []float64{.01, .1, 1, 10},
|
||||
Name: "http_request_duration_seconds",
|
||||
Help: "HTTP request latencies in seconds.",
|
||||
}, labels,
|
||||
)
|
||||
)
|
||||
|
||||
func Init() {
|
||||
// Register the summary and the histogram with Prometheus's default registry.
|
||||
prometheus.MustRegister(
|
||||
uptime,
|
||||
RequestCounter,
|
||||
RequestDuration,
|
||||
)
|
||||
|
||||
go recordUptime()
|
||||
}
|
||||
|
||||
// recordUptime increases service uptime per second.
|
||||
func recordUptime() {
|
||||
for range time.Tick(time.Second) {
|
||||
uptime.WithLabelValues(Service).Inc()
|
||||
}
|
||||
}
|
||||
104
center/metas/metas.go
Normal file
104
center/metas/metas.go
Normal file
@@ -0,0 +1,104 @@
|
||||
package metas
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/ccfos/nightingale/v6/models"
|
||||
"github.com/ccfos/nightingale/v6/storage"
|
||||
|
||||
"github.com/toolkits/pkg/logger"
|
||||
)
|
||||
|
||||
type Set struct {
|
||||
sync.RWMutex
|
||||
items map[string]models.HostMeta
|
||||
redis storage.Redis
|
||||
}
|
||||
|
||||
func New(redis storage.Redis) *Set {
|
||||
set := &Set{
|
||||
items: make(map[string]models.HostMeta),
|
||||
redis: redis,
|
||||
}
|
||||
|
||||
set.Init()
|
||||
return set
|
||||
}
|
||||
|
||||
func (s *Set) Init() {
|
||||
go s.LoopPersist()
|
||||
}
|
||||
|
||||
func (s *Set) MSet(items map[string]models.HostMeta) {
|
||||
s.Lock()
|
||||
defer s.Unlock()
|
||||
for ident, meta := range items {
|
||||
s.items[ident] = meta
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Set) Set(ident string, meta models.HostMeta) {
|
||||
s.Lock()
|
||||
defer s.Unlock()
|
||||
s.items[ident] = meta
|
||||
}
|
||||
|
||||
func (s *Set) LoopPersist() {
|
||||
for {
|
||||
time.Sleep(time.Second)
|
||||
s.persist()
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Set) persist() {
|
||||
var items map[string]models.HostMeta
|
||||
|
||||
s.Lock()
|
||||
if len(s.items) == 0 {
|
||||
s.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
items = s.items
|
||||
s.items = make(map[string]models.HostMeta)
|
||||
s.Unlock()
|
||||
|
||||
s.updateMeta(items)
|
||||
}
|
||||
|
||||
func (s *Set) updateMeta(items map[string]models.HostMeta) {
|
||||
m := make(map[string]models.HostMeta, 100)
|
||||
num := 0
|
||||
|
||||
for _, meta := range items {
|
||||
m[meta.Hostname] = meta
|
||||
num++
|
||||
if num == 100 {
|
||||
if err := s.updateTargets(m); err != nil {
|
||||
logger.Errorf("failed to update targets: %v", err)
|
||||
}
|
||||
m = make(map[string]models.HostMeta, 100)
|
||||
num = 0
|
||||
}
|
||||
}
|
||||
|
||||
if err := s.updateTargets(m); err != nil {
|
||||
logger.Errorf("failed to update targets: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Set) updateTargets(m map[string]models.HostMeta) error {
|
||||
count := int64(len(m))
|
||||
if count == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
newMap := make(map[string]interface{}, count)
|
||||
for ident, meta := range m {
|
||||
newMap[models.WrapIdent(ident)] = meta
|
||||
}
|
||||
err := storage.MSet(context.Background(), s.redis, newMap)
|
||||
return err
|
||||
}
|
||||
521
center/router/router.go
Normal file
521
center/router/router.go
Normal file
@@ -0,0 +1,521 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"path"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/ccfos/nightingale/v6/center/cconf"
|
||||
"github.com/ccfos/nightingale/v6/center/cstats"
|
||||
"github.com/ccfos/nightingale/v6/center/metas"
|
||||
"github.com/ccfos/nightingale/v6/center/sso"
|
||||
_ "github.com/ccfos/nightingale/v6/front/statik"
|
||||
"github.com/ccfos/nightingale/v6/memsto"
|
||||
"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
|
||||
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
|
||||
|
||||
DatasourceCheckHook func(*gin.Context) bool
|
||||
}
|
||||
|
||||
func New(httpConfig httpx.Config, center cconf.Center, 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,
|
||||
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,
|
||||
|
||||
DatasourceCheckHook: func(ctx *gin.Context) bool { return false },
|
||||
}
|
||||
}
|
||||
|
||||
func stat() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
start := time.Now()
|
||||
c.Next()
|
||||
|
||||
code := fmt.Sprintf("%d", c.Writer.Status())
|
||||
method := c.Request.Method
|
||||
labels := []string{cstats.Service, code, c.FullPath(), method}
|
||||
|
||||
cstats.RequestCounter.WithLabelValues(labels...).Inc()
|
||||
cstats.RequestDuration.WithLabelValues(labels...).Observe(float64(time.Since(start).Seconds()))
|
||||
}
|
||||
}
|
||||
|
||||
func languageDetector(i18NHeaderKey string) gin.HandlerFunc {
|
||||
headerKey := i18NHeaderKey
|
||||
return func(c *gin.Context) {
|
||||
if headerKey != "" {
|
||||
lang := c.GetHeader(headerKey)
|
||||
if lang != "" {
|
||||
if strings.HasPrefix(lang, "zh") {
|
||||
c.Request.Header.Set("X-Language", "zh")
|
||||
} 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.Next()
|
||||
}
|
||||
}
|
||||
|
||||
func (rt *Router) configNoRoute(r *gin.Engine, fs *http.FileSystem) {
|
||||
r.NoRoute(func(c *gin.Context) {
|
||||
arr := strings.Split(c.Request.URL.Path, ".")
|
||||
suffix := arr[len(arr)-1]
|
||||
|
||||
switch suffix {
|
||||
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:
|
||||
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...))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (rt *Router) Config(r *gin.Engine) {
|
||||
|
||||
r.Use(stat())
|
||||
r.Use(languageDetector(rt.Center.I18NHeaderKey))
|
||||
r.Use(aop.Recovery())
|
||||
|
||||
statikFS, err := fs.New()
|
||||
if err != nil {
|
||||
logger.Errorf("cannot create statik fs: %v", err)
|
||||
}
|
||||
|
||||
if !rt.Center.UseFileAssets {
|
||||
r.StaticFS("/pub", statikFS)
|
||||
}
|
||||
|
||||
pagesPrefix := "/api/n9e"
|
||||
pages := r.Group(pagesPrefix)
|
||||
{
|
||||
|
||||
if rt.Center.AnonymousAccess.PromQuerier {
|
||||
pages.Any("/proxy/:id/*url", rt.dsProxy)
|
||||
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)
|
||||
|
||||
pages.GET("/sql-template", rt.QuerySqlTemplate)
|
||||
} 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.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.POST("/auth/login", rt.jwtMock(), rt.loginPost)
|
||||
pages.POST("/auth/logout", rt.jwtMock(), rt.auth(), 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)
|
||||
pages.GET("/auth/callback", rt.loginCallback)
|
||||
pages.GET("/auth/callback/cas", rt.loginCallbackCas)
|
||||
pages.GET("/auth/callback/oauth", rt.loginCallbackOAuth)
|
||||
pages.GET("/auth/perms", rt.allPerms)
|
||||
|
||||
pages.GET("/metrics/desc", rt.metricsDescGetFile)
|
||||
pages.POST("/metrics/desc", rt.metricsDescGetMap)
|
||||
|
||||
pages.GET("/notify-channels", rt.notifyChannelsGets)
|
||||
pages.GET("/contact-keys", rt.contactKeysGets)
|
||||
|
||||
pages.GET("/self/perms", rt.auth(), rt.user(), rt.permsGets)
|
||||
pages.GET("/self/profile", rt.auth(), rt.user(), rt.selfProfileGet)
|
||||
pages.PUT("/self/profile", rt.auth(), rt.user(), rt.selfProfilePut)
|
||||
pages.PUT("/self/password", rt.auth(), rt.user(), rt.selfPasswordPut)
|
||||
|
||||
pages.GET("/users", rt.auth(), rt.user(), rt.perm("/users"), rt.userGets)
|
||||
pages.POST("/users", rt.auth(), rt.admin(), rt.userAddPost)
|
||||
pages.GET("/user/:id/profile", rt.auth(), rt.userProfileGet)
|
||||
pages.PUT("/user/:id/profile", rt.auth(), rt.admin(), rt.userProfilePut)
|
||||
pages.PUT("/user/:id/password", rt.auth(), rt.admin(), rt.userPasswordPut)
|
||||
pages.DELETE("/user/:id", rt.auth(), rt.admin(), rt.userDel)
|
||||
|
||||
pages.GET("/metric-views", rt.auth(), rt.metricViewGets)
|
||||
pages.DELETE("/metric-views", rt.auth(), rt.user(), rt.metricViewDel)
|
||||
pages.POST("/metric-views", rt.auth(), rt.user(), rt.metricViewAdd)
|
||||
pages.PUT("/metric-views", rt.auth(), rt.user(), rt.metricViewPut)
|
||||
|
||||
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)
|
||||
pages.PUT("/user-group/:id", rt.auth(), rt.user(), rt.perm("/user-groups/put"), rt.userGroupWrite(), rt.userGroupPut)
|
||||
pages.DELETE("/user-group/:id", rt.auth(), rt.user(), rt.perm("/user-groups/del"), rt.userGroupWrite(), rt.userGroupDel)
|
||||
pages.POST("/user-group/:id/members", rt.auth(), rt.user(), rt.perm("/user-groups/put"), rt.userGroupWrite(), rt.userGroupMemberAdd)
|
||||
pages.DELETE("/user-group/:id/members", rt.auth(), rt.user(), rt.perm("/user-groups/put"), rt.userGroupWrite(), rt.userGroupMemberDel)
|
||||
|
||||
pages.GET("/busi-groups", rt.auth(), rt.user(), rt.busiGroupGets)
|
||||
pages.POST("/busi-groups", rt.auth(), rt.user(), rt.perm("/busi-groups/add"), rt.busiGroupAdd)
|
||||
pages.GET("/busi-groups/alertings", rt.auth(), rt.busiGroupAlertingsGets)
|
||||
pages.GET("/busi-group/:id", rt.auth(), rt.user(), rt.bgro(), rt.busiGroupGet)
|
||||
pages.PUT("/busi-group/:id", rt.auth(), rt.user(), rt.perm("/busi-groups/put"), rt.bgrw(), rt.busiGroupPut)
|
||||
pages.POST("/busi-group/:id/members", rt.auth(), rt.user(), rt.perm("/busi-groups/put"), rt.bgrw(), rt.busiGroupMemberAdd)
|
||||
pages.DELETE("/busi-group/:id/members", rt.auth(), rt.user(), rt.perm("/busi-groups/put"), rt.bgrw(), rt.busiGroupMemberDel)
|
||||
pages.DELETE("/busi-group/:id", rt.auth(), rt.user(), rt.perm("/busi-groups/del"), rt.bgrw(), rt.busiGroupDel)
|
||||
pages.GET("/busi-group/:id/perm/:perm", rt.auth(), rt.user(), rt.checkBusiGroupPerm)
|
||||
|
||||
pages.GET("/targets", rt.auth(), rt.user(), rt.targetGets)
|
||||
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)
|
||||
pages.POST("/targets/tags", rt.auth(), rt.user(), rt.perm("/targets/put"), rt.targetBindTagsByFE)
|
||||
pages.DELETE("/targets/tags", rt.auth(), rt.user(), rt.perm("/targets/put"), rt.targetUnbindTagsByFE)
|
||||
pages.PUT("/targets/note", rt.auth(), rt.user(), rt.perm("/targets/put"), rt.targetUpdateNote)
|
||||
pages.PUT("/targets/bgid", rt.auth(), rt.user(), rt.perm("/targets/put"), rt.targetUpdateBgid)
|
||||
|
||||
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("/integrations/makedown/:cate", rt.builtinMarkdown)
|
||||
|
||||
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.GET("/board/:bid", rt.boardGet)
|
||||
pages.GET("/board/:bid/pure", rt.boardPureGet)
|
||||
pages.PUT("/board/:bid", rt.auth(), rt.user(), rt.perm("/dashboards/put"), rt.boardPut)
|
||||
pages.PUT("/board/:bid/configs", rt.auth(), rt.user(), rt.perm("/dashboards/put"), rt.boardPutConfigs)
|
||||
pages.PUT("/board/:bid/public", rt.auth(), rt.user(), rt.perm("/dashboards/put"), rt.boardPutPublic)
|
||||
pages.DELETE("/boards", rt.auth(), rt.user(), rt.perm("/dashboards/del"), rt.boardDel)
|
||||
|
||||
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("/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)
|
||||
pages.DELETE("/busi-group/:id/alert-rules", rt.auth(), rt.user(), rt.perm("/alert-rules/del"), rt.bgrw(), rt.alertRuleDel)
|
||||
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-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)
|
||||
pages.PUT("/busi-group/:id/recording-rule/:rrid", rt.auth(), rt.user(), rt.perm("/recording-rules/put"), rt.bgrw(), rt.recordingRulePutByFE)
|
||||
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-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.PUT("/busi-group/:id/alert-mutes/fields", rt.auth(), rt.user(), rt.perm("/alert-mutes/put"), rt.bgrw(), rt.alertMutePutFields)
|
||||
|
||||
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)
|
||||
pages.PUT("/busi-group/:id/alert-subscribes", rt.auth(), rt.user(), rt.perm("/alert-subscribes/put"), rt.bgrw(), rt.alertSubscribePut)
|
||||
pages.DELETE("/busi-group/:id/alert-subscribes", rt.auth(), rt.user(), rt.perm("/alert-subscribes/del"), rt.bgrw(), rt.alertSubscribeDel)
|
||||
|
||||
if rt.Center.AnonymousAccess.AlertDetail {
|
||||
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)
|
||||
}
|
||||
|
||||
// card logic
|
||||
pages.GET("/alert-cur-events/list", rt.auth(), rt.alertCurEventsList)
|
||||
pages.GET("/alert-cur-events/card", rt.auth(), rt.alertCurEventsCard)
|
||||
pages.POST("/alert-cur-events/card/details", rt.auth(), rt.alertCurEventsCardDetails)
|
||||
pages.GET("/alert-his-events/list", rt.auth(), 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-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)
|
||||
pages.POST("/busi-group/:id/task-tpls/tags", rt.auth(), rt.user(), rt.perm("/job-tpls/put"), rt.bgrw(), rt.taskTplBindTags)
|
||||
pages.DELETE("/busi-group/:id/task-tpls/tags", rt.auth(), rt.user(), rt.perm("/job-tpls/put"), rt.bgrw(), rt.taskTplUnbindTags)
|
||||
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-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.POST("/datasource/list", rt.auth(), 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)
|
||||
pages.POST("/datasource/status/update", rt.auth(), rt.admin(), rt.datasourceUpdataStatus)
|
||||
pages.DELETE("/datasource/", rt.auth(), rt.admin(), rt.datasourceDel)
|
||||
|
||||
pages.GET("/roles", rt.auth(), rt.admin(), rt.roleGets)
|
||||
pages.POST("/roles", rt.auth(), rt.admin(), rt.roleAdd)
|
||||
pages.PUT("/roles", rt.auth(), rt.admin(), rt.rolePut)
|
||||
pages.DELETE("/role/:id", rt.auth(), rt.admin(), rt.roleDel)
|
||||
|
||||
pages.GET("/role/:id/ops", rt.auth(), rt.admin(), rt.operationOfRole)
|
||||
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", rt.auth(), rt.admin(), rt.notifyTplAdd)
|
||||
pages.DELETE("/notify-tpl/:id", rt.auth(), rt.admin(), rt.notifyTplDel)
|
||||
pages.POST("/notify-tpl/preview", rt.auth(), rt.admin(), 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.PUT("/webhooks", rt.auth(), rt.admin(), rt.webhookPuts)
|
||||
|
||||
pages.GET("/notify-script", rt.auth(), rt.admin(), rt.notifyScriptGet)
|
||||
pages.PUT("/notify-script", rt.auth(), rt.admin(), rt.notifyScriptPut)
|
||||
|
||||
pages.GET("/notify-channel", rt.auth(), rt.admin(), rt.notifyChannelGets)
|
||||
pages.PUT("/notify-channel", rt.auth(), rt.admin(), rt.notifyChannelPuts)
|
||||
|
||||
pages.GET("/notify-contact", rt.auth(), rt.admin(), rt.notifyContactGets)
|
||||
pages.PUT("/notify-contact", rt.auth(), rt.admin(), rt.notifyContactPuts)
|
||||
|
||||
pages.GET("/notify-config", rt.auth(), rt.admin(), 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("/config", rt.auth(), rt.admin(), rt.configGetByKey)
|
||||
pages.PUT("/config", rt.auth(), rt.admin(), rt.configPutByKey)
|
||||
}
|
||||
|
||||
r.GET("/api/n9e/versions", func(c *gin.Context) {
|
||||
v := version.Version
|
||||
lastIndex := strings.LastIndex(version.Version, "-")
|
||||
if lastIndex != -1 {
|
||||
v = version.Version[:lastIndex]
|
||||
}
|
||||
|
||||
ginx.NewRender(c).Data(gin.H{"version": v, "github_verison": version.GithubVersion.Load().(string)}, nil)
|
||||
})
|
||||
|
||||
if rt.HTTP.APIForService.Enable {
|
||||
service := r.Group("/v1/n9e")
|
||||
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.GET("/users", rt.userFindAll)
|
||||
|
||||
service.GET("/user-groups", rt.userGroupGetsByService)
|
||||
service.GET("/user-group-members", rt.userGroupMemberGetsByService)
|
||||
|
||||
service.GET("/targets", rt.targetGetsByService)
|
||||
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.POST("/alert-rules", rt.alertRuleAddByService)
|
||||
service.DELETE("/alert-rules", rt.alertRuleDelByService)
|
||||
service.PUT("/alert-rule/:arid", rt.alertRulePutByService)
|
||||
service.GET("/alert-rule/:arid", rt.alertRuleGet)
|
||||
service.GET("/alert-rules", rt.alertRulesGetByService)
|
||||
|
||||
service.GET("/alert-subscribes", rt.alertSubscribeGetsByService)
|
||||
|
||||
service.GET("/busi-groups", rt.busiGroupGetsByService)
|
||||
|
||||
service.GET("/datasources", rt.datasourceGetsByService)
|
||||
service.GET("/datasource-ids", rt.getDatasourceIds)
|
||||
service.POST("/server-heartbeat", rt.serverHeartbeat)
|
||||
service.GET("/servers-active", rt.serversActive)
|
||||
|
||||
service.GET("/recording-rules", rt.recordingRuleGetsByService)
|
||||
|
||||
service.GET("/alert-mutes", rt.alertMuteGets)
|
||||
service.POST("/alert-mutes", rt.alertMuteAddByService)
|
||||
service.DELETE("/alert-mutes", rt.alertMuteDel)
|
||||
|
||||
service.GET("/alert-cur-events", rt.alertCurEventsList)
|
||||
service.GET("/alert-cur-events-get-by-rid", rt.alertCurEventsGetByRid)
|
||||
service.GET("/alert-his-events", rt.alertHisEventsList)
|
||||
service.GET("/alert-his-event/:eid", rt.alertHisEventGet)
|
||||
|
||||
service.GET("/task-tpl/:tid", rt.taskTplGetByService)
|
||||
|
||||
service.GET("/config/:id", rt.configGet)
|
||||
service.GET("/configs", rt.configsGet)
|
||||
service.GET("/config", rt.configGetByKey)
|
||||
service.PUT("/configs", rt.configsPut)
|
||||
service.POST("/configs", rt.configsPost)
|
||||
service.DELETE("/configs", rt.configsDel)
|
||||
|
||||
service.POST("/conf-prop/encrypt", rt.confPropEncrypt)
|
||||
service.POST("/conf-prop/decrypt", rt.confPropDecrypt)
|
||||
|
||||
service.GET("/statistic", rt.statistic)
|
||||
|
||||
service.GET("/notify-tpls", rt.notifyTplGets)
|
||||
|
||||
service.POST("/task-record-add", rt.taskRecordAdd)
|
||||
}
|
||||
}
|
||||
|
||||
if rt.HTTP.APIForAgent.Enable {
|
||||
heartbeat := r.Group("/v1/n9e")
|
||||
{
|
||||
if len(rt.HTTP.APIForAgent.BasicAuth) > 0 {
|
||||
heartbeat.Use(gin.BasicAuth(rt.HTTP.APIForAgent.BasicAuth))
|
||||
}
|
||||
heartbeat.POST("/heartbeat", rt.heartbeat)
|
||||
}
|
||||
}
|
||||
|
||||
rt.configNoRoute(r, &statikFS)
|
||||
|
||||
}
|
||||
|
||||
func Render(c *gin.Context, data, msg interface{}) {
|
||||
if msg == nil {
|
||||
if data == nil {
|
||||
data = struct{}{}
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"data": data, "error": ""})
|
||||
} else {
|
||||
c.JSON(http.StatusOK, gin.H{"error": gin.H{"message": msg}})
|
||||
}
|
||||
}
|
||||
|
||||
func Dangerous(c *gin.Context, v interface{}, code ...int) {
|
||||
if v == nil {
|
||||
return
|
||||
}
|
||||
|
||||
switch t := v.(type) {
|
||||
case string:
|
||||
if t != "" {
|
||||
c.JSON(http.StatusOK, gin.H{"error": v})
|
||||
}
|
||||
case error:
|
||||
c.JSON(http.StatusOK, gin.H{"error": t.Error()})
|
||||
}
|
||||
}
|
||||
79
center/router/router_alert_aggr_view.go
Normal file
79
center/router/router_alert_aggr_view.go
Normal file
@@ -0,0 +1,79 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/ccfos/nightingale/v6/models"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/toolkits/pkg/ginx"
|
||||
)
|
||||
|
||||
// no param
|
||||
func (rt *Router) alertAggrViewGets(c *gin.Context) {
|
||||
lst, err := models.AlertAggrViewGets(rt.Ctx, c.MustGet("userid"))
|
||||
ginx.NewRender(c).Data(lst, err)
|
||||
}
|
||||
|
||||
// body: name, rule, cate
|
||||
func (rt *Router) alertAggrViewAdd(c *gin.Context) {
|
||||
var f models.AlertAggrView
|
||||
ginx.BindJSON(c, &f)
|
||||
|
||||
me := c.MustGet("user").(*models.User)
|
||||
if !me.IsAdmin() {
|
||||
// 管理员可以选择当前这个视图是公开呢,还是私有,普通用户的话就只能是私有的
|
||||
f.Cate = 1
|
||||
}
|
||||
|
||||
f.Id = 0
|
||||
f.CreateBy = me.Id
|
||||
ginx.Dangerous(f.Add(rt.Ctx))
|
||||
|
||||
ginx.NewRender(c).Data(f, nil)
|
||||
}
|
||||
|
||||
// body: ids
|
||||
func (rt *Router) alertAggrViewDel(c *gin.Context) {
|
||||
var f idsForm
|
||||
ginx.BindJSON(c, &f)
|
||||
f.Verify()
|
||||
|
||||
me := c.MustGet("user").(*models.User)
|
||||
if me.IsAdmin() {
|
||||
ginx.NewRender(c).Message(models.AlertAggrViewDel(rt.Ctx, f.Ids))
|
||||
} else {
|
||||
ginx.NewRender(c).Message(models.AlertAggrViewDel(rt.Ctx, f.Ids, me.Id))
|
||||
}
|
||||
}
|
||||
|
||||
// body: id, name, rule, cate
|
||||
func (rt *Router) alertAggrViewPut(c *gin.Context) {
|
||||
var f models.AlertAggrView
|
||||
ginx.BindJSON(c, &f)
|
||||
|
||||
view, err := models.AlertAggrViewGet(rt.Ctx, "id = ?", f.Id)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
if view == nil {
|
||||
ginx.NewRender(c).Message("no such item(id: %d)", f.Id)
|
||||
return
|
||||
}
|
||||
|
||||
me := c.MustGet("user").(*models.User)
|
||||
if !me.IsAdmin() {
|
||||
f.Cate = 1
|
||||
|
||||
if view.CreateBy != me.Id {
|
||||
ginx.NewRender(c, http.StatusForbidden).Message("forbidden")
|
||||
return
|
||||
}
|
||||
}
|
||||
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))
|
||||
}
|
||||
223
center/router/router_alert_cur_event.go
Normal file
223
center/router/router_alert_cur_event.go
Normal file
@@ -0,0 +1,223 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/ccfos/nightingale/v6/models"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/toolkits/pkg/ginx"
|
||||
)
|
||||
|
||||
func parseAggrRules(c *gin.Context) []*models.AggrRule {
|
||||
aggrRules := strings.Split(ginx.QueryStr(c, "rule", ""), "::") // e.g. field:group_name::field:severity::tagkey:ident
|
||||
|
||||
if len(aggrRules) == 0 {
|
||||
ginx.Bomb(http.StatusBadRequest, "rule empty")
|
||||
}
|
||||
|
||||
rules := make([]*models.AggrRule, len(aggrRules))
|
||||
for i := 0; i < len(aggrRules); i++ {
|
||||
pair := strings.Split(aggrRules[i], ":")
|
||||
if len(pair) != 2 {
|
||||
ginx.Bomb(http.StatusBadRequest, "rule invalid")
|
||||
}
|
||||
|
||||
if !(pair[0] == "field" || pair[0] == "tagkey") {
|
||||
ginx.Bomb(http.StatusBadRequest, "rule invalid")
|
||||
}
|
||||
|
||||
rules[i] = &models.AggrRule{
|
||||
Type: pair[0],
|
||||
Value: pair[1],
|
||||
}
|
||||
}
|
||||
|
||||
return rules
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
prod := ginx.QueryStr(c, "prods", "")
|
||||
if prod == "" {
|
||||
prod = ginx.QueryStr(c, "rule_prods", "")
|
||||
}
|
||||
prods := []string{}
|
||||
if prod != "" {
|
||||
prods = strings.Split(prod, ",")
|
||||
}
|
||||
|
||||
cate := ginx.QueryStr(c, "cate", "$all")
|
||||
cates := []string{}
|
||||
if cate != "$all" {
|
||||
cates = strings.Split(cate, ",")
|
||||
}
|
||||
|
||||
// 最多获取50000个,获取太多也没啥意义
|
||||
list, err := models.AlertCurEventGets(rt.Ctx, prods, busiGroupId, stime, etime, severity, dsIds, cates, query, 50000, 0)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
cardmap := make(map[string]*AlertCard)
|
||||
for _, event := range list {
|
||||
title := event.GenCardTitle(rules)
|
||||
if _, has := cardmap[title]; has {
|
||||
cardmap[title].Total++
|
||||
cardmap[title].EventIds = append(cardmap[title].EventIds, event.Id)
|
||||
if event.Severity < cardmap[title].Severity {
|
||||
cardmap[title].Severity = event.Severity
|
||||
}
|
||||
} else {
|
||||
cardmap[title] = &AlertCard{
|
||||
Total: 1,
|
||||
EventIds: []int64{event.Id},
|
||||
Title: title,
|
||||
Severity: event.Severity,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
titles := make([]string, 0, len(cardmap))
|
||||
for title := range cardmap {
|
||||
titles = append(titles, title)
|
||||
}
|
||||
|
||||
sort.Strings(titles)
|
||||
|
||||
cards := make([]*AlertCard, len(titles))
|
||||
for i := 0; i < len(titles); i++ {
|
||||
cards[i] = cardmap[titles[i]]
|
||||
}
|
||||
|
||||
sort.SliceStable(cards, func(i, j int) bool {
|
||||
if cards[i].Severity != cards[j].Severity {
|
||||
return cards[i].Severity < cards[j].Severity
|
||||
}
|
||||
return cards[i].Total > cards[j].Total
|
||||
})
|
||||
|
||||
ginx.NewRender(c).Data(cards, nil)
|
||||
}
|
||||
|
||||
type AlertCard struct {
|
||||
Title string `json:"title"`
|
||||
Total int `json:"total"`
|
||||
EventIds []int64 `json:"event_ids"`
|
||||
Severity int `json:"severity"`
|
||||
}
|
||||
|
||||
func (rt *Router) alertCurEventsCardDetails(c *gin.Context) {
|
||||
var f idsForm
|
||||
ginx.BindJSON(c, &f)
|
||||
|
||||
list, err := models.AlertCurEventGetByIds(rt.Ctx, f.Ids)
|
||||
if err == nil {
|
||||
cache := make(map[int64]*models.UserGroup)
|
||||
for i := 0; i < len(list); i++ {
|
||||
list[i].FillNotifyGroups(rt.Ctx, cache)
|
||||
}
|
||||
}
|
||||
|
||||
ginx.NewRender(c).Data(list, err)
|
||||
}
|
||||
|
||||
// alertCurEventsGetByRid
|
||||
func (rt *Router) alertCurEventsGetByRid(c *gin.Context) {
|
||||
rid := ginx.QueryInt64(c, "rid")
|
||||
dsId := ginx.QueryInt64(c, "dsid")
|
||||
ginx.NewRender(c).Data(models.AlertCurEventGetByRuleIdAndDsId(rt.Ctx, rid, dsId))
|
||||
}
|
||||
|
||||
// 列表方式,拉取活跃告警
|
||||
func (rt *Router) alertCurEventsList(c *gin.Context) {
|
||||
stime, etime := getTimeRange(c)
|
||||
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", "")
|
||||
if prod == "" {
|
||||
prod = ginx.QueryStr(c, "rule_prods", "")
|
||||
}
|
||||
|
||||
prods := []string{}
|
||||
if prod != "" {
|
||||
prods = strings.Split(prod, ",")
|
||||
}
|
||||
|
||||
cate := ginx.QueryStr(c, "cate", "$all")
|
||||
cates := []string{}
|
||||
if cate != "$all" {
|
||||
cates = strings.Split(cate, ",")
|
||||
}
|
||||
|
||||
total, err := models.AlertCurEventTotal(rt.Ctx, prods, busiGroupId, stime, etime, severity, dsIds, cates, query)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
list, err := models.AlertCurEventGets(rt.Ctx, prods, busiGroupId, stime, etime, severity, dsIds, cates, query, limit, ginx.Offset(c, limit))
|
||||
ginx.Dangerous(err)
|
||||
|
||||
cache := make(map[int64]*models.UserGroup)
|
||||
for i := 0; i < len(list); i++ {
|
||||
list[i].FillNotifyGroups(rt.Ctx, cache)
|
||||
}
|
||||
|
||||
ginx.NewRender(c).Data(gin.H{
|
||||
"list": list,
|
||||
"total": total,
|
||||
}, nil)
|
||||
}
|
||||
|
||||
func (rt *Router) alertCurEventDel(c *gin.Context) {
|
||||
var f idsForm
|
||||
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{})
|
||||
|
||||
// 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 {
|
||||
rt.bgrwCheck(c, event.GroupId)
|
||||
set[event.GroupId] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (rt *Router) alertCurEventGet(c *gin.Context) {
|
||||
eid := ginx.UrlParamInt64(c, "eid")
|
||||
event, err := models.AlertCurEventGetById(rt.Ctx, eid)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
if event == nil {
|
||||
ginx.Bomb(404, "No such active event")
|
||||
}
|
||||
|
||||
ginx.NewRender(c).Data(event, nil)
|
||||
}
|
||||
|
||||
func (rt *Router) alertCurEventsStatistics(c *gin.Context) {
|
||||
|
||||
ginx.NewRender(c).Data(models.AlertCurEventStatistics(rt.Ctx, time.Now()), nil)
|
||||
}
|
||||
82
center/router/router_alert_his_event.go
Normal file
82
center/router/router_alert_his_event.go
Normal file
@@ -0,0 +1,82 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/ccfos/nightingale/v6/models"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/toolkits/pkg/ginx"
|
||||
)
|
||||
|
||||
func getTimeRange(c *gin.Context) (stime, etime int64) {
|
||||
stime = ginx.QueryInt64(c, "stime", 0)
|
||||
etime = ginx.QueryInt64(c, "etime", 0)
|
||||
hours := ginx.QueryInt64(c, "hours", 0)
|
||||
now := time.Now().Unix()
|
||||
if hours != 0 {
|
||||
stime = now - 3600*hours
|
||||
etime = now + 3600*24
|
||||
}
|
||||
|
||||
if stime != 0 && etime == 0 {
|
||||
etime = now + 3600*24
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (rt *Router) alertHisEventsList(c *gin.Context) {
|
||||
stime, etime := getTimeRange(c)
|
||||
|
||||
severity := ginx.QueryInt(c, "severity", -1)
|
||||
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", "")
|
||||
if prod == "" {
|
||||
prod = ginx.QueryStr(c, "rule_prods", "")
|
||||
}
|
||||
|
||||
prods := []string{}
|
||||
if prod != "" {
|
||||
prods = strings.Split(prod, ",")
|
||||
}
|
||||
|
||||
cate := ginx.QueryStr(c, "cate", "$all")
|
||||
cates := []string{}
|
||||
if cate != "$all" {
|
||||
cates = strings.Split(cate, ",")
|
||||
}
|
||||
|
||||
total, err := models.AlertHisEventTotal(rt.Ctx, prods, busiGroupId, stime, etime, severity, recovered, dsIds, cates, query)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
list, err := models.AlertHisEventGets(rt.Ctx, prods, busiGroupId, stime, etime, severity, recovered, dsIds, cates, query, limit, ginx.Offset(c, limit))
|
||||
ginx.Dangerous(err)
|
||||
|
||||
cache := make(map[int64]*models.UserGroup)
|
||||
for i := 0; i < len(list); i++ {
|
||||
list[i].FillNotifyGroups(rt.Ctx, cache)
|
||||
}
|
||||
|
||||
ginx.NewRender(c).Data(gin.H{
|
||||
"list": list,
|
||||
"total": total,
|
||||
}, nil)
|
||||
}
|
||||
|
||||
func (rt *Router) alertHisEventGet(c *gin.Context) {
|
||||
eid := ginx.UrlParamInt64(c, "eid")
|
||||
event, err := models.AlertHisEventGetById(rt.Ctx, eid)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
if event == nil {
|
||||
ginx.Bomb(404, "No such alert event")
|
||||
}
|
||||
|
||||
ginx.NewRender(c).Data(event, err)
|
||||
}
|
||||
325
center/router/router_alert_rule.go
Normal file
325
center/router/router_alert_rule.go
Normal file
@@ -0,0 +1,325 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/ccfos/nightingale/v6/models"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/toolkits/pkg/ginx"
|
||||
"github.com/toolkits/pkg/i18n"
|
||||
)
|
||||
|
||||
// Return all, front-end search and paging
|
||||
func (rt *Router) alertRuleGets(c *gin.Context) {
|
||||
busiGroupId := ginx.UrlParamInt64(c, "id")
|
||||
ars, err := models.AlertRuleGets(rt.Ctx, busiGroupId)
|
||||
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", "")
|
||||
if prodStr != "" {
|
||||
prods = strings.Split(ginx.QueryStr(c, "prods", ""), ",")
|
||||
}
|
||||
|
||||
query := ginx.QueryStr(c, "query", "")
|
||||
algorithm := ginx.QueryStr(c, "algorithm", "")
|
||||
cluster := ginx.QueryStr(c, "cluster", "")
|
||||
cate := ginx.QueryStr(c, "cate", "$all")
|
||||
cates := []string{}
|
||||
if cate != "$all" {
|
||||
cates = strings.Split(cate, ",")
|
||||
}
|
||||
|
||||
disabled := ginx.QueryInt(c, "disabled", -1)
|
||||
ars, err := models.AlertRulesGetsBy(rt.Ctx, prods, query, algorithm, cluster, cates, disabled)
|
||||
if err == nil {
|
||||
cache := make(map[int64]*models.UserGroup)
|
||||
for i := 0; i < len(ars); i++ {
|
||||
ars[i].FillNotifyGroups(rt.Ctx, cache)
|
||||
}
|
||||
}
|
||||
ginx.NewRender(c).Data(ars, err)
|
||||
}
|
||||
|
||||
// single or import
|
||||
func (rt *Router) alertRuleAddByFE(c *gin.Context) {
|
||||
username := c.MustGet("username").(string)
|
||||
|
||||
var lst []models.AlertRule
|
||||
ginx.BindJSON(c, &lst)
|
||||
|
||||
count := len(lst)
|
||||
if count == 0 {
|
||||
ginx.Bomb(http.StatusBadRequest, "input json is empty")
|
||||
}
|
||||
|
||||
bgid := ginx.UrlParamInt64(c, "id")
|
||||
reterr := rt.alertRuleAdd(lst, username, bgid, c.GetHeader("X-Language"))
|
||||
|
||||
ginx.NewRender(c).Data(reterr, nil)
|
||||
}
|
||||
|
||||
func (rt *Router) alertRuleAddByImport(c *gin.Context) {
|
||||
username := c.MustGet("username").(string)
|
||||
|
||||
var lst []models.AlertRule
|
||||
ginx.BindJSON(c, &lst)
|
||||
|
||||
count := len(lst)
|
||||
if count == 0 {
|
||||
ginx.Bomb(http.StatusBadRequest, "input json is empty")
|
||||
}
|
||||
|
||||
bgid := ginx.UrlParamInt64(c, "id")
|
||||
reterr := rt.alertRuleAdd(lst, username, bgid, c.GetHeader("X-Language"))
|
||||
|
||||
ginx.NewRender(c).Data(reterr, nil)
|
||||
}
|
||||
|
||||
func (rt *Router) alertRuleAddByService(c *gin.Context) {
|
||||
var lst []models.AlertRule
|
||||
ginx.BindJSON(c, &lst)
|
||||
|
||||
count := len(lst)
|
||||
if count == 0 {
|
||||
ginx.Bomb(http.StatusBadRequest, "input json is empty")
|
||||
}
|
||||
reterr := rt.alertRuleAddForService(lst, "")
|
||||
ginx.NewRender(c).Data(reterr, nil)
|
||||
}
|
||||
|
||||
func (rt *Router) alertRuleAddForService(lst []models.AlertRule, username string) map[string]string {
|
||||
count := len(lst)
|
||||
// alert rule name -> error string
|
||||
reterr := make(map[string]string)
|
||||
for i := 0; i < count; i++ {
|
||||
lst[i].Id = 0
|
||||
if username != "" {
|
||||
lst[i].CreateBy = username
|
||||
lst[i].UpdateBy = username
|
||||
}
|
||||
|
||||
if err := lst[i].FE2DB(); err != nil {
|
||||
reterr[lst[i].Name] = err.Error()
|
||||
continue
|
||||
}
|
||||
|
||||
if err := lst[i].Add(rt.Ctx); err != nil {
|
||||
reterr[lst[i].Name] = err.Error()
|
||||
} else {
|
||||
reterr[lst[i].Name] = ""
|
||||
}
|
||||
}
|
||||
return reterr
|
||||
}
|
||||
|
||||
func (rt *Router) alertRuleAdd(lst []models.AlertRule, username string, bgid int64, lang string) map[string]string {
|
||||
count := len(lst)
|
||||
// alert rule name -> error string
|
||||
reterr := make(map[string]string)
|
||||
for i := 0; i < count; i++ {
|
||||
lst[i].Id = 0
|
||||
lst[i].GroupId = bgid
|
||||
if username != "" {
|
||||
lst[i].CreateBy = username
|
||||
lst[i].UpdateBy = username
|
||||
}
|
||||
|
||||
if err := lst[i].FE2DB(); err != nil {
|
||||
reterr[lst[i].Name] = i18n.Sprintf(lang, err.Error())
|
||||
continue
|
||||
}
|
||||
|
||||
if err := lst[i].Add(rt.Ctx); err != nil {
|
||||
reterr[lst[i].Name] = i18n.Sprintf(lang, err.Error())
|
||||
} else {
|
||||
reterr[lst[i].Name] = ""
|
||||
}
|
||||
}
|
||||
return reterr
|
||||
}
|
||||
|
||||
func (rt *Router) alertRuleDel(c *gin.Context) {
|
||||
var f idsForm
|
||||
ginx.BindJSON(c, &f)
|
||||
f.Verify()
|
||||
|
||||
// param(busiGroupId) for protect
|
||||
ginx.NewRender(c).Message(models.AlertRuleDels(rt.Ctx, f.Ids, ginx.UrlParamInt64(c, "id")))
|
||||
}
|
||||
|
||||
func (rt *Router) alertRuleDelByService(c *gin.Context) {
|
||||
var f idsForm
|
||||
ginx.BindJSON(c, &f)
|
||||
f.Verify()
|
||||
ginx.NewRender(c).Message(models.AlertRuleDels(rt.Ctx, f.Ids))
|
||||
}
|
||||
|
||||
func (rt *Router) alertRulePutByFE(c *gin.Context) {
|
||||
var f models.AlertRule
|
||||
ginx.BindJSON(c, &f)
|
||||
|
||||
arid := ginx.UrlParamInt64(c, "arid")
|
||||
ar, err := models.AlertRuleGetById(rt.Ctx, arid)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
if ar == nil {
|
||||
ginx.NewRender(c, http.StatusNotFound).Message("No such AlertRule")
|
||||
return
|
||||
}
|
||||
|
||||
rt.bgrwCheck(c, ar.GroupId)
|
||||
|
||||
f.UpdateBy = c.MustGet("username").(string)
|
||||
ginx.NewRender(c).Message(ar.Update(rt.Ctx, f))
|
||||
}
|
||||
|
||||
func (rt *Router) alertRulePutByService(c *gin.Context) {
|
||||
var f models.AlertRule
|
||||
ginx.BindJSON(c, &f)
|
||||
|
||||
arid := ginx.UrlParamInt64(c, "arid")
|
||||
ar, err := models.AlertRuleGetById(rt.Ctx, arid)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
if ar == nil {
|
||||
ginx.NewRender(c, http.StatusNotFound).Message("No such AlertRule")
|
||||
return
|
||||
}
|
||||
ginx.NewRender(c).Message(ar.Update(rt.Ctx, f))
|
||||
}
|
||||
|
||||
type alertRuleFieldForm struct {
|
||||
Ids []int64 `json:"ids"`
|
||||
Fields map[string]interface{} `json:"fields"`
|
||||
Action string `json:"action"`
|
||||
}
|
||||
|
||||
// update one field: cluster note severity disabled prom_eval_interval prom_for_duration notify_channels notify_groups notify_recovered notify_repeat_step callbacks runbook_url append_tags
|
||||
func (rt *Router) alertRulePutFields(c *gin.Context) {
|
||||
var f alertRuleFieldForm
|
||||
ginx.BindJSON(c, &f)
|
||||
|
||||
if len(f.Fields) == 0 {
|
||||
ginx.Bomb(http.StatusBadRequest, "fields empty")
|
||||
}
|
||||
|
||||
f.Fields["update_by"] = c.MustGet("username").(string)
|
||||
f.Fields["update_at"] = time.Now().Unix()
|
||||
|
||||
for i := 0; i < len(f.Ids); i++ {
|
||||
ar, err := models.AlertRuleGetById(rt.Ctx, f.Ids[i])
|
||||
ginx.Dangerous(err)
|
||||
|
||||
if ar == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if f.Action == "callback_add" {
|
||||
// 增加一个 callback 地址
|
||||
if callbacks, has := f.Fields["callbacks"]; has {
|
||||
callback := callbacks.(string)
|
||||
if !strings.Contains(ar.Callbacks, callback) {
|
||||
ginx.Dangerous(ar.UpdateFieldsMap(rt.Ctx, map[string]interface{}{"callbacks": ar.Callbacks + " " + callback}))
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if f.Action == "callback_del" {
|
||||
// 删除一个 callback 地址
|
||||
if callbacks, has := f.Fields["callbacks"]; has {
|
||||
callback := callbacks.(string)
|
||||
ginx.Dangerous(ar.UpdateFieldsMap(rt.Ctx, map[string]interface{}{"callbacks": strings.ReplaceAll(ar.Callbacks, callback, "")}))
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
for k, v := range f.Fields {
|
||||
ginx.Dangerous(ar.UpdateColumn(rt.Ctx, k, v))
|
||||
}
|
||||
}
|
||||
|
||||
ginx.NewRender(c).Message(nil)
|
||||
}
|
||||
|
||||
func (rt *Router) alertRuleGet(c *gin.Context) {
|
||||
arid := ginx.UrlParamInt64(c, "arid")
|
||||
|
||||
ar, err := models.AlertRuleGetById(rt.Ctx, arid)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
if ar == nil {
|
||||
ginx.NewRender(c, http.StatusNotFound).Message("No such AlertRule")
|
||||
return
|
||||
}
|
||||
|
||||
err = ar.FillNotifyGroups(rt.Ctx, make(map[int64]*models.UserGroup))
|
||||
ginx.Dangerous(err)
|
||||
|
||||
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("")
|
||||
}
|
||||
119
center/router/router_alert_subscribe.go
Normal file
119
center/router/router_alert_subscribe.go
Normal file
@@ -0,0 +1,119 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/ccfos/nightingale/v6/models"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/toolkits/pkg/ginx"
|
||||
)
|
||||
|
||||
// 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)
|
||||
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].FillRuleName(rt.Ctx, rulecache))
|
||||
ginx.Dangerous(lst[i].FillDatasourceIds(rt.Ctx))
|
||||
ginx.Dangerous(lst[i].DB2FE())
|
||||
}
|
||||
|
||||
ginx.NewRender(c).Data(lst, err)
|
||||
}
|
||||
|
||||
func (rt *Router) alertSubscribeGet(c *gin.Context) {
|
||||
subid := ginx.UrlParamInt64(c, "sid")
|
||||
|
||||
sub, err := models.AlertSubscribeGet(rt.Ctx, "id=?", subid)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
if sub == nil {
|
||||
ginx.NewRender(c, 404).Message("No such alert subscribe")
|
||||
return
|
||||
}
|
||||
|
||||
ugcache := make(map[int64]*models.UserGroup)
|
||||
ginx.Dangerous(sub.FillUserGroups(rt.Ctx, ugcache))
|
||||
|
||||
rulecache := make(map[int64]string)
|
||||
ginx.Dangerous(sub.FillRuleName(rt.Ctx, rulecache))
|
||||
ginx.Dangerous(sub.FillDatasourceIds(rt.Ctx))
|
||||
ginx.Dangerous(sub.DB2FE())
|
||||
|
||||
ginx.NewRender(c).Data(sub, nil)
|
||||
}
|
||||
|
||||
func (rt *Router) alertSubscribeAdd(c *gin.Context) {
|
||||
var f models.AlertSubscribe
|
||||
ginx.BindJSON(c, &f)
|
||||
|
||||
username := c.MustGet("username").(string)
|
||||
f.CreateBy = username
|
||||
f.UpdateBy = username
|
||||
f.GroupId = ginx.UrlParamInt64(c, "id")
|
||||
|
||||
if f.GroupId <= 0 {
|
||||
ginx.Bomb(http.StatusBadRequest, "group_id invalid")
|
||||
}
|
||||
|
||||
ginx.NewRender(c).Message(f.Add(rt.Ctx))
|
||||
}
|
||||
|
||||
func (rt *Router) alertSubscribePut(c *gin.Context) {
|
||||
var fs []models.AlertSubscribe
|
||||
ginx.BindJSON(c, &fs)
|
||||
|
||||
timestamp := time.Now().Unix()
|
||||
username := c.MustGet("username").(string)
|
||||
for i := 0; i < len(fs); i++ {
|
||||
fs[i].UpdateBy = username
|
||||
fs[i].UpdateAt = timestamp
|
||||
ginx.Dangerous(fs[i].Update(
|
||||
rt.Ctx,
|
||||
"name",
|
||||
"disabled",
|
||||
"prod",
|
||||
"cate",
|
||||
"datasource_ids",
|
||||
"cluster",
|
||||
"rule_id",
|
||||
"tags",
|
||||
"redefine_severity",
|
||||
"new_severity",
|
||||
"redefine_channels",
|
||||
"new_channels",
|
||||
"user_group_ids",
|
||||
"update_at",
|
||||
"update_by",
|
||||
"webhooks",
|
||||
"for_duration",
|
||||
"redefine_webhooks",
|
||||
"severities",
|
||||
"extra_config",
|
||||
"busi_groups",
|
||||
))
|
||||
}
|
||||
|
||||
ginx.NewRender(c).Message(nil)
|
||||
}
|
||||
|
||||
func (rt *Router) alertSubscribeDel(c *gin.Context) {
|
||||
var f idsForm
|
||||
ginx.BindJSON(c, &f)
|
||||
f.Verify()
|
||||
|
||||
ginx.NewRender(c).Message(models.AlertSubscribeDel(rt.Ctx, f.Ids))
|
||||
}
|
||||
|
||||
func (rt *Router) alertSubscribeGetsByService(c *gin.Context) {
|
||||
lst, err := models.AlertSubscribeGetsByService(rt.Ctx)
|
||||
ginx.NewRender(c).Data(lst, err)
|
||||
}
|
||||
237
center/router/router_board.go
Normal file
237
center/router/router_board.go
Normal file
@@ -0,0 +1,237 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/ccfos/nightingale/v6/models"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
"github.com/toolkits/pkg/ginx"
|
||||
)
|
||||
|
||||
type boardForm struct {
|
||||
Name string `json:"name"`
|
||||
Ident string `json:"ident"`
|
||||
Tags string `json:"tags"`
|
||||
Configs string `json:"configs"`
|
||||
Public int `json:"public"`
|
||||
}
|
||||
|
||||
func (rt *Router) boardAdd(c *gin.Context) {
|
||||
var f boardForm
|
||||
ginx.BindJSON(c, &f)
|
||||
|
||||
me := c.MustGet("user").(*models.User)
|
||||
|
||||
board := &models.Board{
|
||||
GroupId: ginx.UrlParamInt64(c, "id"),
|
||||
Name: f.Name,
|
||||
Ident: f.Ident,
|
||||
Tags: f.Tags,
|
||||
Configs: f.Configs,
|
||||
CreateBy: me.Username,
|
||||
UpdateBy: me.Username,
|
||||
}
|
||||
|
||||
err := board.Add(rt.Ctx)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
if f.Configs != "" {
|
||||
ginx.Dangerous(models.BoardPayloadSave(rt.Ctx, board.Id, f.Configs))
|
||||
}
|
||||
|
||||
ginx.NewRender(c).Data(board, nil)
|
||||
}
|
||||
|
||||
func (rt *Router) boardGet(c *gin.Context) {
|
||||
bid := ginx.UrlParamStr(c, "bid")
|
||||
board, err := models.BoardGet(rt.Ctx, "id = ? or ident = ?", bid, bid)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
if board == nil {
|
||||
ginx.Bomb(http.StatusNotFound, "No such dashboard")
|
||||
}
|
||||
|
||||
if board.Public == 0 {
|
||||
rt.auth()(c)
|
||||
rt.user()(c)
|
||||
|
||||
me := c.MustGet("user").(*models.User)
|
||||
if !me.IsAdmin() {
|
||||
// check permission
|
||||
rt.bgroCheck(c, board.GroupId)
|
||||
}
|
||||
}
|
||||
|
||||
ginx.NewRender(c).Data(board, nil)
|
||||
}
|
||||
|
||||
func (rt *Router) boardPureGet(c *gin.Context) {
|
||||
board, err := models.BoardGetByID(rt.Ctx, ginx.UrlParamInt64(c, "bid"))
|
||||
ginx.Dangerous(err)
|
||||
|
||||
if board == nil {
|
||||
ginx.Bomb(http.StatusNotFound, "No such dashboard")
|
||||
}
|
||||
|
||||
ginx.NewRender(c).Data(board, nil)
|
||||
}
|
||||
|
||||
// bgrwCheck
|
||||
func (rt *Router) boardDel(c *gin.Context) {
|
||||
var f idsForm
|
||||
ginx.BindJSON(c, &f)
|
||||
f.Verify()
|
||||
|
||||
for i := 0; i < len(f.Ids); i++ {
|
||||
bid := f.Ids[i]
|
||||
|
||||
board, err := models.BoardGet(rt.Ctx, "id = ?", bid)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
if board == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
me := c.MustGet("user").(*models.User)
|
||||
if !me.IsAdmin() {
|
||||
// check permission
|
||||
rt.bgrwCheck(c, board.GroupId)
|
||||
}
|
||||
|
||||
ginx.Dangerous(board.Del(rt.Ctx))
|
||||
}
|
||||
|
||||
ginx.NewRender(c).Message(nil)
|
||||
}
|
||||
|
||||
func (rt *Router) Board(id int64) *models.Board {
|
||||
obj, err := models.BoardGet(rt.Ctx, "id=?", id)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
if obj == nil {
|
||||
ginx.Bomb(http.StatusNotFound, "No such dashboard")
|
||||
}
|
||||
|
||||
return obj
|
||||
}
|
||||
|
||||
// bgrwCheck
|
||||
func (rt *Router) boardPut(c *gin.Context) {
|
||||
var f boardForm
|
||||
ginx.BindJSON(c, &f)
|
||||
|
||||
me := c.MustGet("user").(*models.User)
|
||||
bo := rt.Board(ginx.UrlParamInt64(c, "bid"))
|
||||
|
||||
if !me.IsAdmin() {
|
||||
// check permission
|
||||
rt.bgrwCheck(c, bo.GroupId)
|
||||
}
|
||||
|
||||
can, err := bo.CanRenameIdent(rt.Ctx, f.Ident)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
if !can {
|
||||
ginx.Bomb(http.StatusOK, "Ident duplicate")
|
||||
}
|
||||
|
||||
bo.Name = f.Name
|
||||
bo.Ident = f.Ident
|
||||
bo.Tags = f.Tags
|
||||
bo.UpdateBy = me.Username
|
||||
bo.UpdateAt = time.Now().Unix()
|
||||
|
||||
err = bo.Update(rt.Ctx, "name", "ident", "tags", "update_by", "update_at")
|
||||
ginx.NewRender(c).Data(bo, err)
|
||||
}
|
||||
|
||||
// bgrwCheck
|
||||
func (rt *Router) boardPutConfigs(c *gin.Context) {
|
||||
var f boardForm
|
||||
ginx.BindJSON(c, &f)
|
||||
|
||||
me := c.MustGet("user").(*models.User)
|
||||
|
||||
bid := ginx.UrlParamStr(c, "bid")
|
||||
bo, err := models.BoardGet(rt.Ctx, "id = ? or ident = ?", bid, bid)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
if bo == nil {
|
||||
ginx.Bomb(http.StatusNotFound, "No such dashboard")
|
||||
}
|
||||
|
||||
// check permission
|
||||
if !me.IsAdmin() {
|
||||
rt.bgrwCheck(c, bo.GroupId)
|
||||
}
|
||||
|
||||
bo.UpdateBy = me.Username
|
||||
bo.UpdateAt = time.Now().Unix()
|
||||
ginx.Dangerous(bo.Update(rt.Ctx, "update_by", "update_at"))
|
||||
|
||||
bo.Configs = f.Configs
|
||||
ginx.Dangerous(models.BoardPayloadSave(rt.Ctx, bo.Id, f.Configs))
|
||||
|
||||
ginx.NewRender(c).Data(bo, nil)
|
||||
}
|
||||
|
||||
// bgrwCheck
|
||||
func (rt *Router) boardPutPublic(c *gin.Context) {
|
||||
var f boardForm
|
||||
ginx.BindJSON(c, &f)
|
||||
|
||||
me := c.MustGet("user").(*models.User)
|
||||
bo := rt.Board(ginx.UrlParamInt64(c, "bid"))
|
||||
|
||||
// check permission
|
||||
if !me.IsAdmin() {
|
||||
rt.bgrwCheck(c, bo.GroupId)
|
||||
}
|
||||
|
||||
bo.Public = f.Public
|
||||
bo.UpdateBy = me.Username
|
||||
bo.UpdateAt = time.Now().Unix()
|
||||
|
||||
err := bo.Update(rt.Ctx, "public", "update_by", "update_at")
|
||||
ginx.NewRender(c).Data(bo, err)
|
||||
}
|
||||
|
||||
func (rt *Router) boardGets(c *gin.Context) {
|
||||
bgid := ginx.UrlParamInt64(c, "id")
|
||||
query := ginx.QueryStr(c, "query", "")
|
||||
|
||||
boards, err := models.BoardGetsByGroupId(rt.Ctx, bgid, query)
|
||||
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()
|
||||
}
|
||||
|
||||
ginx.Dangerous(newBoard.Add(rt.Ctx))
|
||||
|
||||
// clone payload
|
||||
payload, err := models.BoardPayloadGet(rt.Ctx, bo.Id)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
if payload != "" {
|
||||
ginx.Dangerous(models.BoardPayloadSave(rt.Ctx, newBoard.Id, payload))
|
||||
}
|
||||
|
||||
ginx.NewRender(c).Message(nil)
|
||||
}
|
||||
340
center/router/router_builtin.go
Normal file
340
center/router/router_builtin.go
Normal file
@@ -0,0 +1,340 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/ccfos/nightingale/v6/models"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/toolkits/pkg/file"
|
||||
"github.com/toolkits/pkg/ginx"
|
||||
"github.com/toolkits/pkg/logger"
|
||||
"github.com/toolkits/pkg/runner"
|
||||
)
|
||||
|
||||
// 创建 builtin_cate
|
||||
func (rt *Router) builtinCateFavoriteAdd(c *gin.Context) {
|
||||
var f models.BuiltinCate
|
||||
ginx.BindJSON(c, &f)
|
||||
|
||||
if f.Name == "" {
|
||||
ginx.Bomb(http.StatusBadRequest, "name is empty")
|
||||
}
|
||||
|
||||
me := c.MustGet("user").(*models.User)
|
||||
f.UserId = me.Id
|
||||
|
||||
ginx.NewRender(c).Message(f.Create(rt.Ctx))
|
||||
}
|
||||
|
||||
// 删除 builtin_cate
|
||||
func (rt *Router) builtinCateFavoriteDel(c *gin.Context) {
|
||||
name := ginx.UrlParamStr(c, "name")
|
||||
me := c.MustGet("user").(*models.User)
|
||||
|
||||
ginx.NewRender(c).Message(models.BuiltinCateDelete(rt.Ctx, name, me.Id))
|
||||
}
|
||||
|
||||
type Payload struct {
|
||||
Cate string `json:"cate"`
|
||||
Fname string `json:"fname"`
|
||||
Name string `json:"name"`
|
||||
Configs interface{} `json:"configs"`
|
||||
Tags string `json:"tags"`
|
||||
}
|
||||
|
||||
type BoardCate struct {
|
||||
Name string `json:"name"`
|
||||
IconUrl string `json:"icon_url"`
|
||||
Boards []Payload `json:"boards"`
|
||||
Favorite bool `json:"favorite"`
|
||||
}
|
||||
|
||||
func (rt *Router) builtinBoardDetailGets(c *gin.Context) {
|
||||
var payload Payload
|
||||
ginx.BindJSON(c, &payload)
|
||||
|
||||
fp := rt.Center.BuiltinIntegrationsDir
|
||||
if fp == "" {
|
||||
fp = path.Join(runner.Cwd, "integrations")
|
||||
}
|
||||
|
||||
fn := fp + "/" + payload.Cate + "/dashboards/" + payload.Fname
|
||||
content, err := file.ReadBytes(fn)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
err = json.Unmarshal(content, &payload)
|
||||
ginx.NewRender(c).Data(payload, err)
|
||||
}
|
||||
|
||||
func (rt *Router) builtinBoardCateGets(c *gin.Context) {
|
||||
fp := rt.Center.BuiltinIntegrationsDir
|
||||
if fp == "" {
|
||||
fp = path.Join(runner.Cwd, "integrations")
|
||||
}
|
||||
|
||||
me := c.MustGet("user").(*models.User)
|
||||
buildinFavoritesMap, err := models.BuiltinCateGetByUserId(rt.Ctx, me.Id)
|
||||
if err != nil {
|
||||
logger.Warningf("get builtin favorites fail: %v", err)
|
||||
}
|
||||
|
||||
var boardCates []BoardCate
|
||||
dirList, err := file.DirsUnder(fp)
|
||||
ginx.Dangerous(err)
|
||||
for _, dir := range dirList {
|
||||
var boardCate BoardCate
|
||||
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 {
|
||||
fn := fp + "/" + dir + "/dashboards/" + f
|
||||
content, err := file.ReadBytes(fn)
|
||||
if err != nil {
|
||||
logger.Warningf("add board fail: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
var payload Payload
|
||||
err = json.Unmarshal(content, &payload)
|
||||
if err != nil {
|
||||
logger.Warningf("add board:%s fail: %v", fn, err)
|
||||
continue
|
||||
}
|
||||
payload.Cate = dir
|
||||
payload.Fname = f
|
||||
payload.Configs = ""
|
||||
boards = append(boards, payload)
|
||||
}
|
||||
boardCate.Boards = boards
|
||||
|
||||
if _, ok := buildinFavoritesMap[dir]; ok {
|
||||
boardCate.Favorite = true
|
||||
}
|
||||
|
||||
iconFiles, _ := file.FilesUnder(fp + "/" + dir + "/icon")
|
||||
if len(iconFiles) > 0 {
|
||||
boardCate.IconUrl = fmt.Sprintf("/api/n9e/integrations/icon/%s/%s", dir, iconFiles[0])
|
||||
}
|
||||
|
||||
boardCates = append(boardCates, boardCate)
|
||||
}
|
||||
ginx.NewRender(c).Data(boardCates, nil)
|
||||
}
|
||||
|
||||
func (rt *Router) builtinBoardGets(c *gin.Context) {
|
||||
fp := rt.Center.BuiltinIntegrationsDir
|
||||
if fp == "" {
|
||||
fp = path.Join(runner.Cwd, "integrations")
|
||||
}
|
||||
|
||||
var fileList []string
|
||||
dirList, err := file.DirsUnder(fp)
|
||||
ginx.Dangerous(err)
|
||||
for _, dir := range dirList {
|
||||
files, err := file.FilesUnder(fp + "/" + dir + "/dashboards")
|
||||
ginx.Dangerous(err)
|
||||
fileList = append(fileList, files...)
|
||||
}
|
||||
|
||||
names := make([]string, 0, len(fileList))
|
||||
for _, f := range fileList {
|
||||
if !strings.HasSuffix(f, ".json") {
|
||||
continue
|
||||
}
|
||||
|
||||
name := strings.TrimSuffix(f, ".json")
|
||||
names = append(names, name)
|
||||
}
|
||||
|
||||
ginx.NewRender(c).Data(names, nil)
|
||||
}
|
||||
|
||||
type AlertCate struct {
|
||||
Name string `json:"name"`
|
||||
IconUrl string `json:"icon_url"`
|
||||
AlertRules []models.AlertRule `json:"alert_rules"`
|
||||
Favorite bool `json:"favorite"`
|
||||
}
|
||||
|
||||
func (rt *Router) builtinAlertCateGets(c *gin.Context) {
|
||||
fp := rt.Center.BuiltinIntegrationsDir
|
||||
if fp == "" {
|
||||
fp = path.Join(runner.Cwd, "integrations")
|
||||
}
|
||||
|
||||
me := c.MustGet("user").(*models.User)
|
||||
buildinFavoritesMap, err := models.BuiltinCateGetByUserId(rt.Ctx, me.Id)
|
||||
if err != nil {
|
||||
logger.Warningf("get builtin favorites fail: %v", err)
|
||||
}
|
||||
|
||||
var alertCates []AlertCate
|
||||
dirList, err := file.DirsUnder(fp)
|
||||
ginx.Dangerous(err)
|
||||
for _, dir := range dirList {
|
||||
var alertCate AlertCate
|
||||
alertCate.Name = dir
|
||||
files, err := file.FilesUnder(fp + "/" + dir + "/alerts")
|
||||
ginx.Dangerous(err)
|
||||
|
||||
var alertRules []models.AlertRule
|
||||
for _, f := range files {
|
||||
fn := fp + "/" + dir + "/alerts/" + f
|
||||
content, err := file.ReadBytes(fn)
|
||||
if err != nil {
|
||||
logger.Warningf("add board fail: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
var ars []models.AlertRule
|
||||
err = json.Unmarshal(content, &ars)
|
||||
if err != nil {
|
||||
logger.Warningf("add board:%s fail: %v", fn, err)
|
||||
continue
|
||||
}
|
||||
alertRules = append(alertRules, ars...)
|
||||
}
|
||||
alertCate.AlertRules = alertRules
|
||||
iconFiles, _ := file.FilesUnder(fp + "/" + dir + "/icon")
|
||||
if len(iconFiles) > 0 {
|
||||
alertCate.IconUrl = fmt.Sprintf("/api/n9e/integrations/icon/%s/%s", dir, iconFiles[0])
|
||||
}
|
||||
|
||||
if _, ok := buildinFavoritesMap[dir]; ok {
|
||||
alertCate.Favorite = true
|
||||
}
|
||||
|
||||
alertCates = append(alertCates, alertCate)
|
||||
}
|
||||
ginx.NewRender(c).Data(alertCates, nil)
|
||||
}
|
||||
|
||||
type builtinAlertRulesList struct {
|
||||
Name string `json:"name"`
|
||||
IconUrl string `json:"icon_url"`
|
||||
AlertRules map[string][]models.AlertRule `json:"alert_rules"`
|
||||
Favorite bool `json:"favorite"`
|
||||
}
|
||||
|
||||
func (rt *Router) builtinAlertRules(c *gin.Context) {
|
||||
fp := rt.Center.BuiltinIntegrationsDir
|
||||
if fp == "" {
|
||||
fp = path.Join(runner.Cwd, "integrations")
|
||||
}
|
||||
|
||||
me := c.MustGet("user").(*models.User)
|
||||
buildinFavoritesMap, err := models.BuiltinCateGetByUserId(rt.Ctx, me.Id)
|
||||
if err != nil {
|
||||
logger.Warningf("get builtin favorites fail: %v", err)
|
||||
}
|
||||
|
||||
var alertCates []builtinAlertRulesList
|
||||
dirList, err := file.DirsUnder(fp)
|
||||
ginx.Dangerous(err)
|
||||
for _, dir := range dirList {
|
||||
var alertCate builtinAlertRulesList
|
||||
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 {
|
||||
fn := fp + "/" + dir + "/alerts/" + f
|
||||
content, err := file.ReadBytes(fn)
|
||||
if err != nil {
|
||||
logger.Warningf("add board fail: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
var ars []models.AlertRule
|
||||
err = json.Unmarshal(content, &ars)
|
||||
if err != nil {
|
||||
logger.Warningf("add board:%s fail: %v", fn, err)
|
||||
continue
|
||||
}
|
||||
alertRules[strings.TrimSuffix(f, ".json")] = ars
|
||||
}
|
||||
|
||||
alertCate.AlertRules = alertRules
|
||||
iconFiles, _ := file.FilesUnder(fp + "/" + dir + "/icon")
|
||||
if len(iconFiles) > 0 {
|
||||
alertCate.IconUrl = fmt.Sprintf("/api/n9e/integrations/icon/%s/%s", dir, iconFiles[0])
|
||||
}
|
||||
|
||||
if _, ok := buildinFavoritesMap[dir]; ok {
|
||||
alertCate.Favorite = true
|
||||
}
|
||||
|
||||
alertCates = append(alertCates, alertCate)
|
||||
}
|
||||
ginx.NewRender(c).Data(alertCates, nil)
|
||||
}
|
||||
|
||||
// read the json file content
|
||||
func (rt *Router) builtinBoardGet(c *gin.Context) {
|
||||
name := ginx.UrlParamStr(c, "name")
|
||||
dirpath := rt.Center.BuiltinIntegrationsDir
|
||||
if dirpath == "" {
|
||||
dirpath = path.Join(runner.Cwd, "integrations")
|
||||
}
|
||||
|
||||
dirList, err := file.DirsUnder(dirpath)
|
||||
ginx.Dangerous(err)
|
||||
for _, dir := range dirList {
|
||||
jsonFile := dirpath + "/" + dir + "/dashboards/" + name + ".json"
|
||||
if file.IsExist(jsonFile) {
|
||||
body, err := file.ReadString(jsonFile)
|
||||
ginx.NewRender(c).Data(body, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
ginx.Bomb(http.StatusBadRequest, "%s not found", name)
|
||||
}
|
||||
|
||||
func (rt *Router) builtinIcon(c *gin.Context) {
|
||||
fp := rt.Center.BuiltinIntegrationsDir
|
||||
if fp == "" {
|
||||
fp = path.Join(runner.Cwd, "integrations")
|
||||
}
|
||||
|
||||
cate := ginx.UrlParamStr(c, "cate")
|
||||
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)
|
||||
}
|
||||
142
center/router/router_busi_group.go
Normal file
142
center/router/router_busi_group.go
Normal file
@@ -0,0 +1,142 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/ccfos/nightingale/v6/models"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/toolkits/pkg/ginx"
|
||||
"github.com/toolkits/pkg/logger"
|
||||
"github.com/toolkits/pkg/str"
|
||||
)
|
||||
|
||||
type busiGroupForm struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
LabelEnable int `json:"label_enable"`
|
||||
LabelValue string `json:"label_value"`
|
||||
Members []models.BusiGroupMember `json:"members"`
|
||||
}
|
||||
|
||||
func (rt *Router) busiGroupAdd(c *gin.Context) {
|
||||
var f busiGroupForm
|
||||
ginx.BindJSON(c, &f)
|
||||
|
||||
if len(f.Members) == 0 {
|
||||
ginx.Bomb(http.StatusBadRequest, "members empty")
|
||||
}
|
||||
|
||||
rwhas := false
|
||||
for i := 0; i < len(f.Members); i++ {
|
||||
if f.Members[i].PermFlag == "rw" {
|
||||
rwhas = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !rwhas {
|
||||
ginx.Bomb(http.StatusBadRequest, "At least one team have rw permission")
|
||||
}
|
||||
|
||||
username := c.MustGet("username").(string)
|
||||
ginx.Dangerous(models.BusiGroupAdd(rt.Ctx, f.Name, f.LabelEnable, f.LabelValue, f.Members, username))
|
||||
|
||||
// 如果创建成功,拿着name去查,应该可以查到
|
||||
newbg, err := models.BusiGroupGet(rt.Ctx, "name=?", f.Name)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
if newbg == nil {
|
||||
ginx.NewRender(c).Message("Failed to create BusiGroup(%s)", f.Name)
|
||||
return
|
||||
}
|
||||
|
||||
ginx.NewRender(c).Data(newbg.Id, nil)
|
||||
}
|
||||
|
||||
func (rt *Router) busiGroupPut(c *gin.Context) {
|
||||
var f busiGroupForm
|
||||
ginx.BindJSON(c, &f)
|
||||
|
||||
username := c.MustGet("username").(string)
|
||||
targetbg := c.MustGet("busi_group").(*models.BusiGroup)
|
||||
ginx.NewRender(c).Message(targetbg.Update(rt.Ctx, f.Name, f.LabelEnable, f.LabelValue, username))
|
||||
}
|
||||
|
||||
func (rt *Router) busiGroupMemberAdd(c *gin.Context) {
|
||||
var members []models.BusiGroupMember
|
||||
ginx.BindJSON(c, &members)
|
||||
|
||||
username := c.MustGet("username").(string)
|
||||
targetbg := c.MustGet("busi_group").(*models.BusiGroup)
|
||||
|
||||
for i := 0; i < len(members); i++ {
|
||||
if members[i].BusiGroupId != targetbg.Id {
|
||||
ginx.Bomb(http.StatusBadRequest, "business group id invalid")
|
||||
}
|
||||
}
|
||||
|
||||
ginx.NewRender(c).Message(targetbg.AddMembers(rt.Ctx, members, username))
|
||||
}
|
||||
|
||||
func (rt *Router) busiGroupMemberDel(c *gin.Context) {
|
||||
var members []models.BusiGroupMember
|
||||
ginx.BindJSON(c, &members)
|
||||
|
||||
username := c.MustGet("username").(string)
|
||||
targetbg := c.MustGet("busi_group").(*models.BusiGroup)
|
||||
|
||||
for i := 0; i < len(members); i++ {
|
||||
if members[i].BusiGroupId != targetbg.Id {
|
||||
ginx.Bomb(http.StatusBadRequest, "business group id invalid")
|
||||
}
|
||||
}
|
||||
|
||||
ginx.NewRender(c).Message(targetbg.DelMembers(rt.Ctx, members, username))
|
||||
}
|
||||
|
||||
func (rt *Router) busiGroupDel(c *gin.Context) {
|
||||
username := c.MustGet("username").(string)
|
||||
targetbg := c.MustGet("busi_group").(*models.BusiGroup)
|
||||
|
||||
err := targetbg.Del(rt.Ctx)
|
||||
if err != nil {
|
||||
logger.Infof("busi_group_delete fail: operator=%s, group_name=%s error=%v", username, targetbg.Name, err)
|
||||
} else {
|
||||
logger.Infof("busi_group_delete succ: operator=%s, group_name=%s", username, targetbg.Name)
|
||||
}
|
||||
|
||||
ginx.NewRender(c).Message(err)
|
||||
}
|
||||
|
||||
// 我是超管、或者我是业务组成员
|
||||
func (rt *Router) busiGroupGets(c *gin.Context) {
|
||||
limit := ginx.QueryInt(c, "limit", defaultLimit)
|
||||
query := ginx.QueryStr(c, "query", "")
|
||||
all := ginx.QueryBool(c, "all", false)
|
||||
|
||||
me := c.MustGet("user").(*models.User)
|
||||
lst, err := me.BusiGroups(rt.Ctx, limit, query, all)
|
||||
if len(lst) == 0 {
|
||||
lst = []models.BusiGroup{}
|
||||
}
|
||||
|
||||
ginx.NewRender(c).Data(lst, err)
|
||||
}
|
||||
|
||||
func (rt *Router) busiGroupGetsByService(c *gin.Context) {
|
||||
lst, err := models.BusiGroupGetAll(rt.Ctx)
|
||||
ginx.NewRender(c).Data(lst, err)
|
||||
}
|
||||
|
||||
// 这个接口只有在活跃告警页面才调用,获取各个BG的活跃告警数量
|
||||
func (rt *Router) busiGroupAlertingsGets(c *gin.Context) {
|
||||
ids := ginx.QueryStr(c, "ids", "")
|
||||
ret, err := models.AlertNumbers(rt.Ctx, str.IdsInt64(ids))
|
||||
ginx.NewRender(c).Data(ret, err)
|
||||
}
|
||||
|
||||
func (rt *Router) busiGroupGet(c *gin.Context) {
|
||||
bg := BusiGroup(rt.Ctx, ginx.UrlParamInt64(c, "id"))
|
||||
ginx.Dangerous(bg.FillUserGroups(rt.Ctx))
|
||||
ginx.NewRender(c).Data(bg, nil)
|
||||
}
|
||||
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)
|
||||
}
|
||||
45
center/router/router_chart_share.go
Normal file
45
center/router/router_chart_share.go
Normal file
@@ -0,0 +1,45 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/ccfos/nightingale/v6/models"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/toolkits/pkg/ginx"
|
||||
"github.com/toolkits/pkg/str"
|
||||
)
|
||||
|
||||
func (rt *Router) chartShareGets(c *gin.Context) {
|
||||
ids := ginx.QueryStr(c, "ids", "")
|
||||
lst, err := models.ChartShareGetsByIds(rt.Ctx, str.IdsInt64(ids, ","))
|
||||
ginx.NewRender(c).Data(lst, err)
|
||||
}
|
||||
|
||||
type chartShareForm struct {
|
||||
DatasourceId int64 `json:"datasource_id"`
|
||||
Configs string `json:"configs"`
|
||||
}
|
||||
|
||||
func (rt *Router) chartShareAdd(c *gin.Context) {
|
||||
username := c.MustGet("username").(string)
|
||||
|
||||
var forms []chartShareForm
|
||||
ginx.BindJSON(c, &forms)
|
||||
|
||||
ids := []int64{}
|
||||
now := time.Now().Unix()
|
||||
|
||||
for _, f := range forms {
|
||||
chart := models.ChartShare{
|
||||
DatasourceId: f.DatasourceId,
|
||||
Configs: f.Configs,
|
||||
CreateBy: username,
|
||||
CreateAt: now,
|
||||
}
|
||||
ginx.Dangerous(chart.Add(rt.Ctx))
|
||||
ids = append(ids, chart.Id)
|
||||
}
|
||||
|
||||
ginx.NewRender(c).Data(ids, nil)
|
||||
}
|
||||
64
center/router/router_config.go
Normal file
64
center/router/router_config.go
Normal file
@@ -0,0 +1,64 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/ccfos/nightingale/v6/models"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/toolkits/pkg/ginx"
|
||||
)
|
||||
|
||||
func (rt *Router) notifyChannelsGets(c *gin.Context) {
|
||||
var labelAndKeys []models.LabelAndKey
|
||||
cval, err := models.ConfigsGet(rt.Ctx, models.NOTIFYCHANNEL)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
if cval == "" {
|
||||
ginx.NewRender(c).Data(labelAndKeys, nil)
|
||||
return
|
||||
}
|
||||
|
||||
var notifyChannels []models.NotifyChannel
|
||||
err = json.Unmarshal([]byte(cval), ¬ifyChannels)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
for _, v := range notifyChannels {
|
||||
if v.Hide {
|
||||
continue
|
||||
}
|
||||
var labelAndKey models.LabelAndKey
|
||||
labelAndKey.Label = v.Name
|
||||
labelAndKey.Key = v.Ident
|
||||
labelAndKeys = append(labelAndKeys, labelAndKey)
|
||||
}
|
||||
|
||||
ginx.NewRender(c).Data(labelAndKeys, nil)
|
||||
}
|
||||
|
||||
func (rt *Router) contactKeysGets(c *gin.Context) {
|
||||
var labelAndKeys []models.LabelAndKey
|
||||
cval, err := models.ConfigsGet(rt.Ctx, models.NOTIFYCONTACT)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
if cval == "" {
|
||||
ginx.NewRender(c).Data(labelAndKeys, nil)
|
||||
return
|
||||
}
|
||||
|
||||
var notifyContacts []models.NotifyContact
|
||||
err = json.Unmarshal([]byte(cval), ¬ifyContacts)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
for _, v := range notifyContacts {
|
||||
if v.Hide {
|
||||
continue
|
||||
}
|
||||
var labelAndKey models.LabelAndKey
|
||||
labelAndKey.Label = v.Name
|
||||
labelAndKey.Key = v.Ident
|
||||
labelAndKeys = append(labelAndKeys, labelAndKey)
|
||||
}
|
||||
|
||||
ginx.NewRender(c).Data(labelAndKeys, nil)
|
||||
}
|
||||
60
center/router/router_configs.go
Normal file
60
center/router/router_configs.go
Normal file
@@ -0,0 +1,60 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"github.com/ccfos/nightingale/v6/models"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/toolkits/pkg/ginx"
|
||||
)
|
||||
|
||||
func (rt *Router) configsGet(c *gin.Context) {
|
||||
prefix := ginx.QueryStr(c, "prefix", "")
|
||||
limit := ginx.QueryInt(c, "limit", 10)
|
||||
configs, err := models.ConfigsGets(rt.Ctx, prefix, limit, ginx.Offset(c, limit))
|
||||
ginx.NewRender(c).Data(configs, err)
|
||||
}
|
||||
|
||||
func (rt *Router) configGet(c *gin.Context) {
|
||||
id := ginx.UrlParamInt64(c, "id")
|
||||
configs, err := models.ConfigGet(rt.Ctx, id)
|
||||
ginx.NewRender(c).Data(configs, err)
|
||||
}
|
||||
|
||||
func (rt *Router) configGetByKey(c *gin.Context) {
|
||||
config, err := models.ConfigsGet(rt.Ctx, ginx.QueryStr(c, "key"))
|
||||
ginx.NewRender(c).Data(config, err)
|
||||
}
|
||||
|
||||
func (rt *Router) configPutByKey(c *gin.Context) {
|
||||
var f models.Configs
|
||||
ginx.BindJSON(c, &f)
|
||||
ginx.NewRender(c).Message(models.ConfigsSet(rt.Ctx, f.Ckey, f.Cval))
|
||||
}
|
||||
|
||||
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) {
|
||||
var arr []models.Configs
|
||||
ginx.BindJSON(c, &arr)
|
||||
|
||||
for i := 0; i < len(arr); i++ {
|
||||
ginx.Dangerous(arr[i].Update(rt.Ctx))
|
||||
}
|
||||
|
||||
ginx.NewRender(c).Message(nil)
|
||||
}
|
||||
|
||||
func (rt *Router) configsPost(c *gin.Context) {
|
||||
var arr []models.Configs
|
||||
ginx.BindJSON(c, &arr)
|
||||
|
||||
for i := 0; i < len(arr); i++ {
|
||||
ginx.Dangerous(arr[i].Add(rt.Ctx))
|
||||
}
|
||||
|
||||
ginx.NewRender(c).Message(nil)
|
||||
}
|
||||
63
center/router/router_crypto.go
Normal file
63
center/router/router_crypto.go
Normal file
@@ -0,0 +1,63 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"github.com/ccfos/nightingale/v6/pkg/secu"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/toolkits/pkg/ginx"
|
||||
)
|
||||
|
||||
type confPropCrypto struct {
|
||||
Data string `json:"data" binding:"required"`
|
||||
Key string `json:"key" binding:"required"`
|
||||
}
|
||||
|
||||
func (rt *Router) confPropEncrypt(c *gin.Context) {
|
||||
var f confPropCrypto
|
||||
ginx.BindJSON(c, &f)
|
||||
|
||||
k := len(f.Key)
|
||||
switch k {
|
||||
default:
|
||||
c.String(400, "The key length should be 16, 24 or 32")
|
||||
return
|
||||
case 16, 24, 32:
|
||||
break
|
||||
}
|
||||
|
||||
s, err := secu.DealWithEncrypt(f.Data, f.Key)
|
||||
if err != nil {
|
||||
c.String(500, err.Error())
|
||||
}
|
||||
|
||||
c.JSON(200, gin.H{
|
||||
"src": f.Data,
|
||||
"key": f.Key,
|
||||
"encrypt": s,
|
||||
})
|
||||
}
|
||||
|
||||
func (rt *Router) confPropDecrypt(c *gin.Context) {
|
||||
var f confPropCrypto
|
||||
ginx.BindJSON(c, &f)
|
||||
|
||||
k := len(f.Key)
|
||||
switch k {
|
||||
default:
|
||||
c.String(400, "The key length should be 16, 24 or 32")
|
||||
return
|
||||
case 16, 24, 32:
|
||||
break
|
||||
}
|
||||
|
||||
s, err := secu.DealWithDecrypt(f.Data, f.Key)
|
||||
if err != nil {
|
||||
c.String(500, err.Error())
|
||||
}
|
||||
|
||||
c.JSON(200, gin.H{
|
||||
"src": f.Data,
|
||||
"key": f.Key,
|
||||
"decrypt": s,
|
||||
})
|
||||
}
|
||||
19
center/router/router_dashboard.go
Normal file
19
center/router/router_dashboard.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package router
|
||||
|
||||
type ChartPure struct {
|
||||
Configs string `json:"configs"`
|
||||
Weight int `json:"weight"`
|
||||
}
|
||||
|
||||
type ChartGroupPure struct {
|
||||
Name string `json:"name"`
|
||||
Weight int `json:"weight"`
|
||||
Charts []ChartPure `json:"charts"`
|
||||
}
|
||||
|
||||
type DashboardPure struct {
|
||||
Name string `json:"name"`
|
||||
Tags string `json:"tags"`
|
||||
Configs string `json:"configs"`
|
||||
ChartGroups []ChartGroupPure `json:"chart_groups"`
|
||||
}
|
||||
246
center/router/router_datasource.go
Normal file
246
center/router/router_datasource.go
Normal file
@@ -0,0 +1,246 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/ccfos/nightingale/v6/models"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/toolkits/pkg/ginx"
|
||||
"github.com/toolkits/pkg/logger"
|
||||
)
|
||||
|
||||
func (rt *Router) pluginList(c *gin.Context) {
|
||||
Render(c, rt.Center.Plugins, nil)
|
||||
}
|
||||
|
||||
type listReq struct {
|
||||
Name string `json:"name"`
|
||||
Type string `json:"plugin_type"`
|
||||
Category string `json:"category"`
|
||||
}
|
||||
|
||||
func (rt *Router) datasourceList(c *gin.Context) {
|
||||
if rt.DatasourceCheckHook(c) {
|
||||
Render(c, []int{}, nil)
|
||||
return
|
||||
}
|
||||
|
||||
var req listReq
|
||||
ginx.BindJSON(c, &req)
|
||||
|
||||
typ := req.Type
|
||||
category := req.Category
|
||||
name := req.Name
|
||||
|
||||
list, err := models.GetDatasourcesGetsBy(rt.Ctx, typ, category, name, "")
|
||||
Render(c, list, err)
|
||||
}
|
||||
|
||||
func (rt *Router) datasourceGetsByService(c *gin.Context) {
|
||||
typ := ginx.QueryStr(c, "typ", "")
|
||||
lst, err := models.GetDatasourcesGetsBy(rt.Ctx, typ, "", "", "")
|
||||
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
|
||||
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,
|
||||
})
|
||||
}
|
||||
|
||||
ginx.NewRender(c).Data(dss, err)
|
||||
}
|
||||
|
||||
func (rt *Router) datasourceUpsert(c *gin.Context) {
|
||||
if rt.DatasourceCheckHook(c) {
|
||||
Render(c, []int{}, nil)
|
||||
return
|
||||
}
|
||||
|
||||
var req models.Datasource
|
||||
ginx.BindJSON(c, &req)
|
||||
username := Username(c)
|
||||
req.UpdatedBy = username
|
||||
|
||||
var err error
|
||||
var count int64
|
||||
|
||||
err = DatasourceCheck(req)
|
||||
if err != nil {
|
||||
Dangerous(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
if req.Id == 0 {
|
||||
req.CreatedBy = username
|
||||
req.Status = "enabled"
|
||||
count, err = models.GetDatasourcesCountBy(rt.Ctx, "", "", req.Name)
|
||||
if err != nil {
|
||||
Render(c, nil, err)
|
||||
return
|
||||
}
|
||||
|
||||
if count > 0 {
|
||||
Render(c, nil, "name already exists")
|
||||
return
|
||||
}
|
||||
err = req.Add(rt.Ctx)
|
||||
} else {
|
||||
err = req.Update(rt.Ctx, "name", "description", "cluster_name", "settings", "http", "auth", "updated_by", "updated_at")
|
||||
}
|
||||
|
||||
Render(c, nil, err)
|
||||
}
|
||||
|
||||
func DatasourceCheck(ds models.Datasource) error {
|
||||
if ds.HTTPJson.Url == "" {
|
||||
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{
|
||||
InsecureSkipVerify: ds.HTTPJson.TLS.SkipTlsVerify,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
fullURL := ds.HTTPJson.Url
|
||||
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)
|
||||
}
|
||||
|
||||
if ds.PluginType == models.PROMETHEUS {
|
||||
subPath := "/api/v1/query"
|
||||
query := url.Values{}
|
||||
if ds.HTTPJson.IsLoki() {
|
||||
subPath = "/api/v1/labels"
|
||||
} else {
|
||||
query.Add("query", "1+1")
|
||||
}
|
||||
fullURL = fmt.Sprintf("%s%s?%s", ds.HTTPJson.Url, subPath, query.Encode())
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
if ds.AuthJson.BasicAuthUser != "" {
|
||||
req.SetBasicAuth(ds.AuthJson.BasicAuthUser, ds.AuthJson.BasicAuthPassword)
|
||||
}
|
||||
|
||||
for k, v := range ds.HTTPJson.Headers {
|
||||
req.Header.Set(k, v)
|
||||
}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
logger.Errorf("Error making request: %v\n", err)
|
||||
return fmt.Errorf("request url:%s failed", fullURL)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
logger.Errorf("Error making request: %v\n", 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.DatasourceCheckHook(c) {
|
||||
Render(c, []int{}, nil)
|
||||
return
|
||||
}
|
||||
|
||||
var req models.Datasource
|
||||
ginx.BindJSON(c, &req)
|
||||
err := req.Get(rt.Ctx)
|
||||
Render(c, req, err)
|
||||
}
|
||||
|
||||
func (rt *Router) datasourceUpdataStatus(c *gin.Context) {
|
||||
if rt.DatasourceCheckHook(c) {
|
||||
Render(c, []int{}, nil)
|
||||
return
|
||||
}
|
||||
|
||||
var req models.Datasource
|
||||
ginx.BindJSON(c, &req)
|
||||
username := Username(c)
|
||||
req.UpdatedBy = username
|
||||
err := req.Update(rt.Ctx, "status", "updated_by", "updated_at")
|
||||
Render(c, req, err)
|
||||
}
|
||||
|
||||
func (rt *Router) datasourceDel(c *gin.Context) {
|
||||
if rt.DatasourceCheckHook(c) {
|
||||
Render(c, []int{}, nil)
|
||||
return
|
||||
}
|
||||
|
||||
var ids []int64
|
||||
ginx.BindJSON(c, &ids)
|
||||
err := models.DatasourceDel(rt.Ctx, ids)
|
||||
Render(c, nil, err)
|
||||
}
|
||||
|
||||
func (rt *Router) getDatasourceIds(c *gin.Context) {
|
||||
name := ginx.QueryStr(c, "name")
|
||||
datasourceIds, err := models.GetDatasourceIdsByEngineName(rt.Ctx, name)
|
||||
|
||||
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)
|
||||
}
|
||||
156
center/router/router_funcs.go
Normal file
156
center/router/router_funcs.go
Normal file
@@ -0,0 +1,156 @@
|
||||
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/toolkits/pkg/ginx"
|
||||
)
|
||||
|
||||
const defaultLimit = 300
|
||||
|
||||
func (rt *Router) statistic(c *gin.Context) {
|
||||
name := ginx.QueryStr(c, "name")
|
||||
var model interface{}
|
||||
var err error
|
||||
var statistics *models.Statistics
|
||||
switch name {
|
||||
case "alert_mute":
|
||||
model = models.AlertMute{}
|
||||
case "alert_rule":
|
||||
model = models.AlertRule{}
|
||||
case "alert_subscribe":
|
||||
model = models.AlertSubscribe{}
|
||||
case "busi_group":
|
||||
model = models.BusiGroup{}
|
||||
case "recording_rule":
|
||||
model = models.RecordingRule{}
|
||||
case "target":
|
||||
model = models.Target{}
|
||||
case "user":
|
||||
model = models.User{}
|
||||
case "user_group":
|
||||
model = models.UserGroup{}
|
||||
case "datasource":
|
||||
// datasource update_at is different from others
|
||||
statistics, err = models.DatasourceStatistics(rt.Ctx)
|
||||
ginx.NewRender(c).Data(statistics, err)
|
||||
return
|
||||
default:
|
||||
ginx.Bomb(http.StatusBadRequest, "invalid name")
|
||||
}
|
||||
|
||||
statistics, err = models.StatisticsGet(rt.Ctx, model)
|
||||
ginx.NewRender(c).Data(statistics, err)
|
||||
}
|
||||
|
||||
func queryDatasourceIds(c *gin.Context) []int64 {
|
||||
datasourceIds := ginx.QueryStr(c, "datasource_ids", "")
|
||||
datasourceIds = strings.ReplaceAll(datasourceIds, ",", " ")
|
||||
idsStr := strings.Fields(datasourceIds)
|
||||
ids := make([]int64, len(idsStr))
|
||||
for i, idStr := range idsStr {
|
||||
id, _ := strconv.ParseInt(idStr, 10, 64)
|
||||
ids[i] = id
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
type idsForm struct {
|
||||
Ids []int64 `json:"ids"`
|
||||
}
|
||||
|
||||
func (f idsForm) Verify() {
|
||||
if len(f.Ids) == 0 {
|
||||
ginx.Bomb(http.StatusBadRequest, "ids empty")
|
||||
}
|
||||
}
|
||||
|
||||
func User(ctx *ctx.Context, id int64) *models.User {
|
||||
obj, err := models.UserGetById(ctx, id)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
if obj == nil {
|
||||
ginx.Bomb(http.StatusNotFound, "No such user")
|
||||
}
|
||||
|
||||
return obj
|
||||
}
|
||||
|
||||
func UserGroup(ctx *ctx.Context, id int64) *models.UserGroup {
|
||||
obj, err := models.UserGroupGetById(ctx, id)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
if obj == nil {
|
||||
ginx.Bomb(http.StatusNotFound, "No such UserGroup")
|
||||
}
|
||||
|
||||
return obj
|
||||
}
|
||||
|
||||
func BusiGroup(ctx *ctx.Context, id int64) *models.BusiGroup {
|
||||
obj, err := models.BusiGroupGetById(ctx, id)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
if obj == nil {
|
||||
ginx.Bomb(http.StatusNotFound, "No such BusiGroup")
|
||||
}
|
||||
|
||||
return obj
|
||||
}
|
||||
|
||||
func Dashboard(ctx *ctx.Context, id int64) *models.Dashboard {
|
||||
obj, err := models.DashboardGet(ctx, "id=?", id)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
if obj == nil {
|
||||
ginx.Bomb(http.StatusNotFound, "No such dashboard")
|
||||
}
|
||||
|
||||
return obj
|
||||
}
|
||||
|
||||
type DoneIdsReply struct {
|
||||
Err string `json:"err"`
|
||||
Dat struct {
|
||||
List []int64 `json:"list"`
|
||||
} `json:"dat"`
|
||||
}
|
||||
|
||||
type TaskCreateReply struct {
|
||||
Err string `json:"err"`
|
||||
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
|
||||
}
|
||||
|
||||
if res.Err != "" {
|
||||
return 0, fmt.Errorf("response.err: %v", res.Err)
|
||||
}
|
||||
|
||||
return res.Dat, nil
|
||||
}
|
||||
68
center/router/router_heartbeat.go
Normal file
68
center/router/router_heartbeat.go
Normal file
@@ -0,0 +1,68 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"compress/gzip"
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"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) {
|
||||
var bs []byte
|
||||
var err error
|
||||
var r *gzip.Reader
|
||||
var req models.HostMeta
|
||||
if c.GetHeader("Content-Encoding") == "gzip" {
|
||||
r, err = gzip.NewReader(c.Request.Body)
|
||||
if err != nil {
|
||||
c.String(400, err.Error())
|
||||
return
|
||||
}
|
||||
defer r.Close()
|
||||
bs, err = ioutil.ReadAll(r)
|
||||
ginx.Dangerous(err)
|
||||
} else {
|
||||
defer c.Request.Body.Close()
|
||||
bs, err = ioutil.ReadAll(c.Request.Body)
|
||||
ginx.Dangerous(err)
|
||||
}
|
||||
|
||||
err = json.Unmarshal(bs, &req)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
// maybe from pushgw
|
||||
if req.Offset == 0 {
|
||||
req.Offset = (time.Now().UnixMilli() - req.UnixTime)
|
||||
}
|
||||
|
||||
if req.RemoteAddr == "" {
|
||||
req.RemoteAddr = c.ClientIP()
|
||||
}
|
||||
|
||||
rt.MetaSet.Set(req.Hostname, req)
|
||||
var items = make(map[string]struct{})
|
||||
items[req.Hostname] = struct{}{}
|
||||
rt.IdentSet.MSet(items)
|
||||
|
||||
if target, has := rt.TargetCache.Get(req.Hostname); has && target != nil {
|
||||
var defGid int64 = -1
|
||||
gid := ginx.QueryInt64(c, "gid", defGid)
|
||||
hostIpStr := strings.TrimSpace(req.HostIp)
|
||||
if gid == defGid { //set gid value from cache
|
||||
gid = target.GroupId
|
||||
}
|
||||
logger.Debugf("heartbeat gid: %v, host_ip: '%v', target: %v", gid, hostIpStr, *target)
|
||||
if gid != target.GroupId || hostIpStr != target.HostIp { // if either gid or host_ip has a new value
|
||||
err = models.TargetUpdateHostIpAndBgid(rt.Ctx, req.Hostname, hostIpStr, gid)
|
||||
}
|
||||
}
|
||||
|
||||
ginx.NewRender(c).Message(err)
|
||||
}
|
||||
590
center/router/router_login.go
Normal file
590
center/router/router_login.go
Normal file
@@ -0,0 +1,590 @@
|
||||
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/ccfos/nightingale/v6/pkg/secu"
|
||||
"github.com/pelletier/go-toml/v2"
|
||||
|
||||
"github.com/dgrijalva/jwt-go"
|
||||
"github.com/gin-gonic/gin"
|
||||
"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"`
|
||||
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())
|
||||
|
||||
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
|
||||
}
|
||||
user, err := models.PassLogin(rt.Ctx, f.Username, authPassWord)
|
||||
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, authPassWord, 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 {
|
||||
ginx.NewRender(c).Message(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if user == nil {
|
||||
// Theoretically impossible
|
||||
ginx.NewRender(c).Message("Username or password invalid")
|
||||
return
|
||||
}
|
||||
|
||||
userIdentity := fmt.Sprintf("%d-%s", user.Id, user.Username)
|
||||
|
||||
ts, err := rt.createTokens(rt.HTTP.JWTAuth.SigningKey, userIdentity)
|
||||
ginx.Dangerous(err)
|
||||
ginx.Dangerous(rt.createAuth(c.Request.Context(), userIdentity, ts))
|
||||
|
||||
ginx.NewRender(c).Data(gin.H{
|
||||
"user": user,
|
||||
"access_token": ts.AccessToken,
|
||||
"refresh_token": ts.RefreshToken,
|
||||
}, nil)
|
||||
}
|
||||
|
||||
func (rt *Router) logoutPost(c *gin.Context) {
|
||||
logger.Infof("username:%s login 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")
|
||||
return
|
||||
}
|
||||
|
||||
delErr := rt.deleteTokens(c.Request.Context(), metadata)
|
||||
if delErr != nil {
|
||||
ginx.NewRender(c).Message(http.StatusText(http.StatusInternalServerError))
|
||||
return
|
||||
}
|
||||
|
||||
ginx.NewRender(c).Message("")
|
||||
}
|
||||
|
||||
type refreshForm struct {
|
||||
RefreshToken string `json:"refresh_token" binding:"required"`
|
||||
}
|
||||
|
||||
func (rt *Router) refreshPost(c *gin.Context) {
|
||||
var f refreshForm
|
||||
ginx.BindJSON(c, &f)
|
||||
|
||||
// verify the token
|
||||
token, err := jwt.Parse(f.RefreshToken, func(token *jwt.Token) (interface{}, error) {
|
||||
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
return nil, fmt.Errorf("unexpected jwt signing method: %v", token.Header["alg"])
|
||||
}
|
||||
return []byte(rt.HTTP.JWTAuth.SigningKey), nil
|
||||
})
|
||||
|
||||
// if there is an error, the token must have expired
|
||||
if err != nil {
|
||||
// redirect to login page
|
||||
ginx.NewRender(c, http.StatusUnauthorized).Message("refresh token expired")
|
||||
return
|
||||
}
|
||||
|
||||
// Since token is valid, get the uuid:
|
||||
claims, ok := token.Claims.(jwt.MapClaims) //the token claims should conform to MapClaims
|
||||
if ok && token.Valid {
|
||||
refreshUuid, ok := claims["refresh_uuid"].(string) //convert the interface to string
|
||||
if !ok {
|
||||
// Theoretically impossible
|
||||
ginx.NewRender(c, http.StatusUnauthorized).Message("failed to parse refresh_uuid from jwt")
|
||||
return
|
||||
}
|
||||
|
||||
userIdentity, ok := claims["user_identity"].(string)
|
||||
if !ok {
|
||||
// Theoretically impossible
|
||||
ginx.NewRender(c, http.StatusUnauthorized).Message("failed to parse user_identity from jwt")
|
||||
return
|
||||
}
|
||||
|
||||
userid, err := strconv.ParseInt(strings.Split(userIdentity, "-")[0], 10, 64)
|
||||
if err != nil {
|
||||
ginx.NewRender(c, http.StatusUnauthorized).Message("failed to parse user_identity from jwt")
|
||||
return
|
||||
}
|
||||
|
||||
u, err := models.UserGetById(rt.Ctx, userid)
|
||||
if err != nil {
|
||||
ginx.NewRender(c, http.StatusInternalServerError).Message("failed to query user by id")
|
||||
return
|
||||
}
|
||||
|
||||
if u == nil {
|
||||
// user already deleted
|
||||
ginx.NewRender(c, http.StatusUnauthorized).Message("user already deleted")
|
||||
return
|
||||
}
|
||||
|
||||
// Delete the previous Refresh Token
|
||||
err = rt.deleteAuth(c.Request.Context(), refreshUuid)
|
||||
if err != nil {
|
||||
ginx.NewRender(c, http.StatusUnauthorized).Message(http.StatusText(http.StatusInternalServerError))
|
||||
return
|
||||
}
|
||||
|
||||
// Delete previous Access Token
|
||||
rt.deleteAuth(c.Request.Context(), strings.Split(refreshUuid, "++")[0])
|
||||
|
||||
// Create new pairs of refresh and access tokens
|
||||
ts, err := rt.createTokens(rt.HTTP.JWTAuth.SigningKey, userIdentity)
|
||||
ginx.Dangerous(err)
|
||||
ginx.Dangerous(rt.createAuth(c.Request.Context(), userIdentity, ts))
|
||||
|
||||
ginx.NewRender(c).Data(gin.H{
|
||||
"access_token": ts.AccessToken,
|
||||
"refresh_token": ts.RefreshToken,
|
||||
}, nil)
|
||||
} else {
|
||||
// redirect to login page
|
||||
ginx.NewRender(c, http.StatusUnauthorized).Message("refresh token expired")
|
||||
}
|
||||
}
|
||||
|
||||
func (rt *Router) loginRedirect(c *gin.Context) {
|
||||
redirect := ginx.QueryStr(c, "redirect", "/")
|
||||
|
||||
v, exists := c.Get("userid")
|
||||
if exists {
|
||||
userid := v.(int64)
|
||||
user, err := models.UserGetById(rt.Ctx, userid)
|
||||
ginx.Dangerous(err)
|
||||
if user == nil {
|
||||
ginx.Bomb(200, "user not found")
|
||||
}
|
||||
|
||||
if user.Username != "" { // already login
|
||||
ginx.NewRender(c).Data(redirect, nil)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if !rt.Sso.OIDC.Enable {
|
||||
ginx.NewRender(c).Data("", nil)
|
||||
return
|
||||
}
|
||||
|
||||
redirect, err := rt.Sso.OIDC.Authorize(rt.Redis, redirect)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
ginx.NewRender(c).Data(redirect, err)
|
||||
}
|
||||
|
||||
type CallbackOutput struct {
|
||||
Redirect string `json:"redirect"`
|
||||
User *models.User `json:"user"`
|
||||
AccessToken string `json:"access_token"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
}
|
||||
|
||||
func (rt *Router) loginCallback(c *gin.Context) {
|
||||
code := ginx.QueryStr(c, "code", "")
|
||||
state := ginx.QueryStr(c, "state", "")
|
||||
|
||||
ret, err := rt.Sso.OIDC.Callback(rt.Redis, c.Request.Context(), code, state)
|
||||
if err != nil {
|
||||
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
|
||||
}
|
||||
|
||||
user, err := models.UserGet(rt.Ctx, "username=?", ret.Username)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
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")
|
||||
}
|
||||
} 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",
|
||||
}
|
||||
|
||||
// create user from oidc
|
||||
ginx.Dangerous(user.Add(rt.Ctx))
|
||||
}
|
||||
|
||||
// set user login state
|
||||
userIdentity := fmt.Sprintf("%d-%s", user.Id, user.Username)
|
||||
ts, err := rt.createTokens(rt.HTTP.JWTAuth.SigningKey, userIdentity)
|
||||
ginx.Dangerous(err)
|
||||
ginx.Dangerous(rt.createAuth(c.Request.Context(), userIdentity, ts))
|
||||
|
||||
redirect := "/"
|
||||
if ret.Redirect != "/login" {
|
||||
redirect = ret.Redirect
|
||||
}
|
||||
|
||||
ginx.NewRender(c).Data(CallbackOutput{
|
||||
Redirect: redirect,
|
||||
User: user,
|
||||
AccessToken: ts.AccessToken,
|
||||
RefreshToken: ts.RefreshToken,
|
||||
}, nil)
|
||||
}
|
||||
|
||||
type RedirectOutput struct {
|
||||
Redirect string `json:"redirect"`
|
||||
State string `json:"state"`
|
||||
}
|
||||
|
||||
func (rt *Router) loginRedirectCas(c *gin.Context) {
|
||||
redirect := ginx.QueryStr(c, "redirect", "/")
|
||||
|
||||
v, exists := c.Get("userid")
|
||||
if exists {
|
||||
userid := v.(int64)
|
||||
user, err := models.UserGetById(rt.Ctx, userid)
|
||||
ginx.Dangerous(err)
|
||||
if user == nil {
|
||||
ginx.Bomb(200, "user not found")
|
||||
}
|
||||
|
||||
if user.Username != "" { // already login
|
||||
ginx.NewRender(c).Data(redirect, nil)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if !rt.Sso.CAS.Enable {
|
||||
logger.Error("cas is not enable")
|
||||
ginx.NewRender(c).Data("", nil)
|
||||
return
|
||||
}
|
||||
|
||||
redirect, state, err := rt.Sso.CAS.Authorize(rt.Redis, redirect)
|
||||
|
||||
ginx.Dangerous(err)
|
||||
ginx.NewRender(c).Data(RedirectOutput{
|
||||
Redirect: redirect,
|
||||
State: state,
|
||||
}, err)
|
||||
}
|
||||
|
||||
func (rt *Router) loginCallbackCas(c *gin.Context) {
|
||||
ticket := ginx.QueryStr(c, "ticket", "")
|
||||
state := ginx.QueryStr(c, "state", "")
|
||||
ret, err := rt.Sso.CAS.ValidateServiceTicket(c.Request.Context(), ticket, state, rt.Redis)
|
||||
if err != nil {
|
||||
logger.Errorf("ValidateServiceTicket: %s", err)
|
||||
ginx.NewRender(c).Data("", err)
|
||||
return
|
||||
}
|
||||
user, err := models.UserGet(rt.Ctx, "username=?", ret.Username)
|
||||
if err != nil {
|
||||
logger.Errorf("UserGet: %s", err)
|
||||
}
|
||||
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"))
|
||||
}
|
||||
} 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",
|
||||
}
|
||||
// create user from cas
|
||||
ginx.Dangerous(user.Add(rt.Ctx))
|
||||
}
|
||||
|
||||
// set user login state
|
||||
userIdentity := fmt.Sprintf("%d-%s", user.Id, user.Username)
|
||||
ts, err := rt.createTokens(rt.HTTP.JWTAuth.SigningKey, userIdentity)
|
||||
if err != nil {
|
||||
logger.Errorf("createTokens: %s", err)
|
||||
}
|
||||
ginx.Dangerous(err)
|
||||
ginx.Dangerous(rt.createAuth(c.Request.Context(), userIdentity, ts))
|
||||
|
||||
redirect := "/"
|
||||
if ret.Redirect != "/login" {
|
||||
redirect = ret.Redirect
|
||||
}
|
||||
ginx.NewRender(c).Data(CallbackOutput{
|
||||
Redirect: redirect,
|
||||
User: user,
|
||||
AccessToken: ts.AccessToken,
|
||||
RefreshToken: ts.RefreshToken,
|
||||
}, nil)
|
||||
}
|
||||
|
||||
func (rt *Router) loginRedirectOAuth(c *gin.Context) {
|
||||
redirect := ginx.QueryStr(c, "redirect", "/")
|
||||
|
||||
v, exists := c.Get("userid")
|
||||
if exists {
|
||||
userid := v.(int64)
|
||||
user, err := models.UserGetById(rt.Ctx, userid)
|
||||
ginx.Dangerous(err)
|
||||
if user == nil {
|
||||
ginx.Bomb(200, "user not found")
|
||||
}
|
||||
|
||||
if user.Username != "" { // already login
|
||||
ginx.NewRender(c).Data(redirect, nil)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if !rt.Sso.OAuth2.Enable {
|
||||
ginx.NewRender(c).Data("", nil)
|
||||
return
|
||||
}
|
||||
|
||||
redirect, err := rt.Sso.OAuth2.Authorize(rt.Redis, redirect)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
ginx.NewRender(c).Data(redirect, err)
|
||||
}
|
||||
|
||||
func (rt *Router) loginCallbackOAuth(c *gin.Context) {
|
||||
code := ginx.QueryStr(c, "code", "")
|
||||
state := ginx.QueryStr(c, "state", "")
|
||||
|
||||
ret, err := rt.Sso.OAuth2.Callback(rt.Redis, c.Request.Context(), code, state)
|
||||
if err != nil {
|
||||
logger.Debugf("sso.callback() get ret %+v error %v", ret, err)
|
||||
ginx.NewRender(c).Data(CallbackOutput{}, err)
|
||||
return
|
||||
}
|
||||
|
||||
user, err := models.UserGet(rt.Ctx, "username=?", ret.Username)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
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")
|
||||
}
|
||||
} 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",
|
||||
}
|
||||
|
||||
// create user from oidc
|
||||
ginx.Dangerous(user.Add(rt.Ctx))
|
||||
}
|
||||
|
||||
// set user login state
|
||||
userIdentity := fmt.Sprintf("%d-%s", user.Id, user.Username)
|
||||
ts, err := rt.createTokens(rt.HTTP.JWTAuth.SigningKey, userIdentity)
|
||||
ginx.Dangerous(err)
|
||||
ginx.Dangerous(rt.createAuth(c.Request.Context(), userIdentity, ts))
|
||||
|
||||
redirect := "/"
|
||||
if ret.Redirect != "/login" {
|
||||
redirect = ret.Redirect
|
||||
}
|
||||
|
||||
ginx.NewRender(c).Data(CallbackOutput{
|
||||
Redirect: redirect,
|
||||
User: user,
|
||||
AccessToken: ts.AccessToken,
|
||||
RefreshToken: ts.RefreshToken,
|
||||
}, nil)
|
||||
}
|
||||
|
||||
type SsoConfigOutput struct {
|
||||
OidcDisplayName string `json:"oidcDisplayName"`
|
||||
CasDisplayName string `json:"casDisplayName"`
|
||||
OauthDisplayName string `json:"oauthDisplayName"`
|
||||
}
|
||||
|
||||
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: oidcDisplayName,
|
||||
CasDisplayName: casDisplayName,
|
||||
OauthDisplayName: oauthDisplayName,
|
||||
}, nil)
|
||||
}
|
||||
|
||||
func (rt *Router) ssoConfigGets(c *gin.Context) {
|
||||
ginx.NewRender(c).Data(models.SsoConfigGets(rt.Ctx))
|
||||
}
|
||||
|
||||
func (rt *Router) ssoConfigUpdate(c *gin.Context) {
|
||||
var f models.SsoConfig
|
||||
ginx.BindJSON(c, &f)
|
||||
|
||||
err := f.Update(rt.Ctx)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
switch f.Name {
|
||||
case "LDAP":
|
||||
var config ldapx.Config
|
||||
err := toml.Unmarshal([]byte(f.Content), &config)
|
||||
ginx.Dangerous(err)
|
||||
rt.Sso.LDAP.Reload(config)
|
||||
case "OIDC":
|
||||
var config oidcx.Config
|
||||
err := toml.Unmarshal([]byte(f.Content), &config)
|
||||
ginx.Dangerous(err)
|
||||
rt.Sso.OIDC, err = oidcx.New(config)
|
||||
ginx.Dangerous(err)
|
||||
case "CAS":
|
||||
var config cas.Config
|
||||
err := toml.Unmarshal([]byte(f.Content), &config)
|
||||
ginx.Dangerous(err)
|
||||
rt.Sso.CAS.Reload(config)
|
||||
case "OAuth2":
|
||||
var config oauth2x.Config
|
||||
err := toml.Unmarshal([]byte(f.Content), &config)
|
||||
ginx.Dangerous(err)
|
||||
rt.Sso.OAuth2.Reload(config)
|
||||
}
|
||||
|
||||
ginx.NewRender(c).Message(nil)
|
||||
}
|
||||
|
||||
type RSAConfigOutput struct {
|
||||
OpenRSA bool
|
||||
RSAPublicKey string
|
||||
}
|
||||
|
||||
func (rt *Router) rsaConfigGet(c *gin.Context) {
|
||||
publicKey := ""
|
||||
if rt.HTTP.RSA.OpenRSA {
|
||||
publicKey = base64.StdEncoding.EncodeToString(rt.HTTP.RSA.RSAPublicKey)
|
||||
}
|
||||
ginx.NewRender(c).Data(RSAConfigOutput{
|
||||
OpenRSA: rt.HTTP.RSA.OpenRSA,
|
||||
RSAPublicKey: publicKey,
|
||||
}, nil)
|
||||
}
|
||||
97
center/router/router_metric_desc.go
Normal file
97
center/router/router_metric_desc.go
Normal file
@@ -0,0 +1,97 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"github.com/ccfos/nightingale/v6/center/cconf"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/toolkits/pkg/ginx"
|
||||
)
|
||||
|
||||
func (rt *Router) metricsDescGetFile(c *gin.Context) {
|
||||
c.JSON(200, rt.Center.MetricDesc)
|
||||
}
|
||||
|
||||
// 前端传过来一个metric数组,后端去查询有没有对应的释义,返回map
|
||||
func (rt *Router) metricsDescGetMap(c *gin.Context) {
|
||||
var arr []string
|
||||
ginx.BindJSON(c, &arr)
|
||||
|
||||
ret := make(map[string]string)
|
||||
for _, key := range arr {
|
||||
ret[key] = cconf.GetMetricDesc(c.GetHeader("X-Language"), key)
|
||||
}
|
||||
|
||||
ginx.NewRender(c).Data(ret, nil)
|
||||
}
|
||||
|
||||
// 页面功能暂时先不要了,直接通过配置文件来维护
|
||||
// func metricDescriptionGets(c *gin.Context) {
|
||||
// limit := ginx.QueryInt(c, "limit", 20)
|
||||
// query := ginx.QueryStr(c, "query", "")
|
||||
|
||||
// total, err := models.MetricDescriptionTotal(query)
|
||||
// ginx.Dangerous(err)
|
||||
|
||||
// list, err := models.MetricDescriptionGets(query, limit, ginx.Offset(c, limit))
|
||||
// ginx.Dangerous(err)
|
||||
|
||||
// ginx.NewRender(c).Data(gin.H{
|
||||
// "list": list,
|
||||
// "total": total,
|
||||
// }, nil)
|
||||
// }
|
||||
|
||||
// type metricDescriptionAddForm struct {
|
||||
// Data string `json:"data"`
|
||||
// }
|
||||
|
||||
// func metricDescriptionAdd(c *gin.Context) {
|
||||
// var f metricDescriptionAddForm
|
||||
// ginx.BindJSON(c, &f)
|
||||
|
||||
// var metricDescriptions []models.MetricDescription
|
||||
|
||||
// lines := strings.Split(f.Data, "\n")
|
||||
// for _, md := range lines {
|
||||
// arr := strings.SplitN(md, ":", 2)
|
||||
// if len(arr) != 2 {
|
||||
// ginx.Bomb(200, "metric description %s is illegal", md)
|
||||
// }
|
||||
// m := models.MetricDescription{
|
||||
// Metric: arr[0],
|
||||
// Description: arr[1],
|
||||
// }
|
||||
// metricDescriptions = append(metricDescriptions, m)
|
||||
// }
|
||||
|
||||
// if len(metricDescriptions) == 0 {
|
||||
// ginx.Bomb(http.StatusBadRequest, "Decoded metric description empty")
|
||||
// }
|
||||
|
||||
// ginx.NewRender(c).Message(models.MetricDescriptionUpdate(metricDescriptions))
|
||||
// }
|
||||
|
||||
// func metricDescriptionDel(c *gin.Context) {
|
||||
// var f idsForm
|
||||
// ginx.BindJSON(c, &f)
|
||||
// f.Verify()
|
||||
// ginx.NewRender(c).Message(models.MetricDescriptionDel(f.Ids))
|
||||
// }
|
||||
|
||||
// type metricDescriptionForm struct {
|
||||
// Description string `json:"description"`
|
||||
// }
|
||||
|
||||
// func metricDescriptionPut(c *gin.Context) {
|
||||
// var f metricDescriptionForm
|
||||
// ginx.BindJSON(c, &f)
|
||||
|
||||
// md, err := models.MetricDescriptionGet("id=?", ginx.UrlParamInt64(c, "id"))
|
||||
// ginx.Dangerous(err)
|
||||
|
||||
// if md == nil {
|
||||
// ginx.Bomb(200, "No such metric description")
|
||||
// }
|
||||
|
||||
// ginx.NewRender(c).Message(md.Update(f.Description, time.Now().Unix()))
|
||||
// }
|
||||
76
center/router/router_metric_view.go
Normal file
76
center/router/router_metric_view.go
Normal file
@@ -0,0 +1,76 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/ccfos/nightingale/v6/models"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/toolkits/pkg/ginx"
|
||||
)
|
||||
|
||||
// no param
|
||||
func (rt *Router) metricViewGets(c *gin.Context) {
|
||||
lst, err := models.MetricViewGets(rt.Ctx, c.MustGet("userid"))
|
||||
ginx.NewRender(c).Data(lst, err)
|
||||
}
|
||||
|
||||
// body: name, configs, cate
|
||||
func (rt *Router) metricViewAdd(c *gin.Context) {
|
||||
var f models.MetricView
|
||||
ginx.BindJSON(c, &f)
|
||||
|
||||
me := c.MustGet("user").(*models.User)
|
||||
if !me.IsAdmin() {
|
||||
// 管理员可以选择当前这个视图是公开呢,还是私有,普通用户的话就只能是私有的
|
||||
f.Cate = 1
|
||||
}
|
||||
|
||||
f.Id = 0
|
||||
f.CreateBy = me.Id
|
||||
|
||||
ginx.Dangerous(f.Add(rt.Ctx))
|
||||
|
||||
ginx.NewRender(c).Data(f, nil)
|
||||
}
|
||||
|
||||
// body: ids
|
||||
func (rt *Router) metricViewDel(c *gin.Context) {
|
||||
var f idsForm
|
||||
ginx.BindJSON(c, &f)
|
||||
f.Verify()
|
||||
|
||||
me := c.MustGet("user").(*models.User)
|
||||
if me.IsAdmin() {
|
||||
ginx.NewRender(c).Message(models.MetricViewDel(rt.Ctx, f.Ids))
|
||||
} else {
|
||||
ginx.NewRender(c).Message(models.MetricViewDel(rt.Ctx, f.Ids, me.Id))
|
||||
}
|
||||
}
|
||||
|
||||
// body: id, name, configs, cate
|
||||
func (rt *Router) metricViewPut(c *gin.Context) {
|
||||
var f models.MetricView
|
||||
ginx.BindJSON(c, &f)
|
||||
|
||||
view, err := models.MetricViewGet(rt.Ctx, "id = ?", f.Id)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
if view == nil {
|
||||
ginx.NewRender(c).Message("no such item(id: %d)", f.Id)
|
||||
return
|
||||
}
|
||||
|
||||
me := c.MustGet("user").(*models.User)
|
||||
if !me.IsAdmin() {
|
||||
f.Cate = 1
|
||||
|
||||
// 如果是普通用户,只能修改自己的
|
||||
if view.CreateBy != me.Id {
|
||||
ginx.NewRender(c, http.StatusForbidden).Message("forbidden")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
ginx.NewRender(c).Message(view.Update(rt.Ctx, f.Name, f.Configs, f.Cate, me.Id))
|
||||
}
|
||||
131
center/router/router_mute.go
Normal file
131
center/router/router_mute.go
Normal file
@@ -0,0 +1,131 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"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"
|
||||
)
|
||||
|
||||
// Return all, front-end search and paging
|
||||
func (rt *Router) alertMuteGetsByBG(c *gin.Context) {
|
||||
bgid := ginx.UrlParamInt64(c, "id")
|
||||
lst, err := models.AlertMuteGetsByBG(rt.Ctx, bgid)
|
||||
|
||||
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)
|
||||
query := ginx.QueryStr(c, "query", "")
|
||||
lst, err := models.AlertMuteGets(rt.Ctx, prods, bgid, query)
|
||||
|
||||
ginx.NewRender(c).Data(lst, err)
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
ginx.NewRender(c).Message(f.Add(rt.Ctx))
|
||||
}
|
||||
|
||||
func (rt *Router) alertMuteDel(c *gin.Context) {
|
||||
var f idsForm
|
||||
ginx.BindJSON(c, &f)
|
||||
f.Verify()
|
||||
|
||||
ginx.NewRender(c).Message(models.AlertMuteDel(rt.Ctx, f.Ids))
|
||||
}
|
||||
|
||||
func (rt *Router) alertMutePutByFE(c *gin.Context) {
|
||||
var f models.AlertMute
|
||||
ginx.BindJSON(c, &f)
|
||||
|
||||
amid := ginx.UrlParamInt64(c, "amid")
|
||||
am, err := models.AlertMuteGetById(rt.Ctx, amid)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
if am == nil {
|
||||
ginx.NewRender(c, http.StatusNotFound).Message("No such AlertMute")
|
||||
return
|
||||
}
|
||||
|
||||
rt.bgrwCheck(c, am.GroupId)
|
||||
|
||||
f.UpdateBy = c.MustGet("username").(string)
|
||||
ginx.NewRender(c).Message(am.Update(rt.Ctx, f))
|
||||
}
|
||||
|
||||
type alertMuteFieldForm struct {
|
||||
Ids []int64 `json:"ids"`
|
||||
Fields map[string]interface{} `json:"fields"`
|
||||
}
|
||||
|
||||
func (rt *Router) alertMutePutFields(c *gin.Context) {
|
||||
var f alertMuteFieldForm
|
||||
ginx.BindJSON(c, &f)
|
||||
|
||||
if len(f.Fields) == 0 {
|
||||
ginx.Bomb(http.StatusBadRequest, "fields empty")
|
||||
}
|
||||
|
||||
f.Fields["update_by"] = c.MustGet("username").(string)
|
||||
f.Fields["update_at"] = time.Now().Unix()
|
||||
|
||||
for i := 0; i < len(f.Ids); i++ {
|
||||
am, err := models.AlertMuteGetById(rt.Ctx, f.Ids[i])
|
||||
ginx.Dangerous(err)
|
||||
|
||||
if am == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
am.FE2DB()
|
||||
ginx.Dangerous(am.UpdateFieldsMap(rt.Ctx, f.Fields))
|
||||
}
|
||||
|
||||
ginx.NewRender(c).Message(nil)
|
||||
}
|
||||
422
center/router/router_mw.go
Normal file
422
center/router/router_mw.go
Normal file
@@ -0,0 +1,422 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/ccfos/nightingale/v6/models"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/golang-jwt/jwt"
|
||||
"github.com/google/uuid"
|
||||
"github.com/toolkits/pkg/ginx"
|
||||
)
|
||||
|
||||
type AccessDetails struct {
|
||||
AccessUuid string
|
||||
UserIdentity string
|
||||
}
|
||||
|
||||
func (rt *Router) handleProxyUser(c *gin.Context) *models.User {
|
||||
headerUserNameKey := rt.HTTP.ProxyAuth.HeaderUserNameKey
|
||||
username := c.GetHeader(headerUserNameKey)
|
||||
if username == "" {
|
||||
ginx.Bomb(http.StatusUnauthorized, "unauthorized")
|
||||
}
|
||||
|
||||
user, err := models.UserGetByUsername(rt.Ctx, username)
|
||||
if err != nil {
|
||||
ginx.Bomb(http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
if user == nil {
|
||||
now := time.Now().Unix()
|
||||
user = &models.User{
|
||||
Username: username,
|
||||
Nickname: username,
|
||||
Roles: strings.Join(rt.HTTP.ProxyAuth.DefaultRoles, " "),
|
||||
CreateAt: now,
|
||||
UpdateAt: now,
|
||||
CreateBy: "system",
|
||||
UpdateBy: "system",
|
||||
}
|
||||
err = user.Add(rt.Ctx)
|
||||
if err != nil {
|
||||
ginx.Bomb(http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
}
|
||||
return user
|
||||
}
|
||||
|
||||
func (rt *Router) proxyAuth() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
user := rt.handleProxyUser(c)
|
||||
c.Set("userid", user.Id)
|
||||
c.Set("username", user.Username)
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
func (rt *Router) jwtAuth() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
metadata, err := rt.extractTokenMetadata(c.Request)
|
||||
if err != nil {
|
||||
ginx.Bomb(http.StatusUnauthorized, "unauthorized")
|
||||
}
|
||||
|
||||
userIdentity, err := rt.fetchAuth(c.Request.Context(), metadata.AccessUuid)
|
||||
if err != nil {
|
||||
ginx.Bomb(http.StatusUnauthorized, "unauthorized")
|
||||
}
|
||||
|
||||
// ${userid}-${username}
|
||||
arr := strings.SplitN(userIdentity, "-", 2)
|
||||
if len(arr) != 2 {
|
||||
ginx.Bomb(http.StatusUnauthorized, "unauthorized")
|
||||
}
|
||||
|
||||
userid, err := strconv.ParseInt(arr[0], 10, 64)
|
||||
if err != nil {
|
||||
ginx.Bomb(http.StatusUnauthorized, "unauthorized")
|
||||
}
|
||||
|
||||
c.Set("userid", userid)
|
||||
c.Set("username", arr[1])
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
func (rt *Router) auth() gin.HandlerFunc {
|
||||
if rt.HTTP.ProxyAuth.Enable {
|
||||
return rt.proxyAuth()
|
||||
} else {
|
||||
return rt.jwtAuth()
|
||||
}
|
||||
}
|
||||
|
||||
// if proxy auth is enabled, mock jwt login/logout/refresh request
|
||||
func (rt *Router) jwtMock() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
if !rt.HTTP.ProxyAuth.Enable {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
if strings.Contains(c.FullPath(), "logout") {
|
||||
ginx.Bomb(http.StatusBadRequest, "logout is not supported when proxy auth is enabled")
|
||||
}
|
||||
user := rt.handleProxyUser(c)
|
||||
ginx.NewRender(c).Data(gin.H{
|
||||
"user": user,
|
||||
"access_token": "",
|
||||
"refresh_token": "",
|
||||
}, nil)
|
||||
c.Abort()
|
||||
}
|
||||
}
|
||||
|
||||
func (rt *Router) user() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
userid := c.MustGet("userid").(int64)
|
||||
|
||||
user, err := models.UserGetById(rt.Ctx, userid)
|
||||
if err != nil {
|
||||
ginx.Bomb(http.StatusUnauthorized, "unauthorized")
|
||||
}
|
||||
|
||||
if user == nil {
|
||||
ginx.Bomb(http.StatusUnauthorized, "unauthorized")
|
||||
}
|
||||
|
||||
c.Set("user", user)
|
||||
c.Set("isadmin", user.IsAdmin())
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
func (rt *Router) userGroupWrite() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
me := c.MustGet("user").(*models.User)
|
||||
ug := UserGroup(rt.Ctx, ginx.UrlParamInt64(c, "id"))
|
||||
|
||||
can, err := me.CanModifyUserGroup(rt.Ctx, ug)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
if !can {
|
||||
ginx.Bomb(http.StatusForbidden, "forbidden")
|
||||
}
|
||||
|
||||
c.Set("user_group", ug)
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
func (rt *Router) bgro() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
me := c.MustGet("user").(*models.User)
|
||||
bg := BusiGroup(rt.Ctx, ginx.UrlParamInt64(c, "id"))
|
||||
|
||||
can, err := me.CanDoBusiGroup(rt.Ctx, bg)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
if !can {
|
||||
ginx.Bomb(http.StatusForbidden, "forbidden")
|
||||
}
|
||||
|
||||
c.Set("busi_group", bg)
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// bgrw 逐步要被干掉,不安全
|
||||
func (rt *Router) bgrw() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
me := c.MustGet("user").(*models.User)
|
||||
bg := BusiGroup(rt.Ctx, ginx.UrlParamInt64(c, "id"))
|
||||
|
||||
can, err := me.CanDoBusiGroup(rt.Ctx, bg, "rw")
|
||||
ginx.Dangerous(err)
|
||||
|
||||
if !can {
|
||||
ginx.Bomb(http.StatusForbidden, "forbidden")
|
||||
}
|
||||
|
||||
c.Set("busi_group", bg)
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// bgrwCheck 要逐渐替换掉bgrw方法,更安全
|
||||
func (rt *Router) bgrwCheck(c *gin.Context, bgid int64) {
|
||||
me := c.MustGet("user").(*models.User)
|
||||
bg := BusiGroup(rt.Ctx, bgid)
|
||||
|
||||
can, err := me.CanDoBusiGroup(rt.Ctx, bg, "rw")
|
||||
ginx.Dangerous(err)
|
||||
|
||||
if !can {
|
||||
ginx.Bomb(http.StatusForbidden, "forbidden")
|
||||
}
|
||||
|
||||
c.Set("busi_group", bg)
|
||||
}
|
||||
|
||||
func (rt *Router) bgrwChecks(c *gin.Context, bgids []int64) {
|
||||
set := make(map[int64]struct{})
|
||||
|
||||
for i := 0; i < len(bgids); i++ {
|
||||
if _, has := set[bgids[i]]; has {
|
||||
continue
|
||||
}
|
||||
|
||||
rt.bgrwCheck(c, bgids[i])
|
||||
set[bgids[i]] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
func (rt *Router) bgroCheck(c *gin.Context, bgid int64) {
|
||||
me := c.MustGet("user").(*models.User)
|
||||
bg := BusiGroup(rt.Ctx, bgid)
|
||||
|
||||
can, err := me.CanDoBusiGroup(rt.Ctx, bg)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
if !can {
|
||||
ginx.Bomb(http.StatusForbidden, "forbidden")
|
||||
}
|
||||
|
||||
c.Set("busi_group", bg)
|
||||
}
|
||||
|
||||
func (rt *Router) perm(operation string) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
me := c.MustGet("user").(*models.User)
|
||||
|
||||
can, err := me.CheckPerm(rt.Ctx, operation)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
if !can {
|
||||
ginx.Bomb(http.StatusForbidden, "forbidden")
|
||||
}
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
func (rt *Router) admin() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
userid := c.MustGet("userid").(int64)
|
||||
|
||||
user, err := models.UserGetById(rt.Ctx, userid)
|
||||
if err != nil {
|
||||
ginx.Bomb(http.StatusUnauthorized, "unauthorized")
|
||||
}
|
||||
|
||||
if user == nil {
|
||||
ginx.Bomb(http.StatusUnauthorized, "unauthorized")
|
||||
}
|
||||
|
||||
roles := strings.Fields(user.Roles)
|
||||
found := false
|
||||
for i := 0; i < len(roles); i++ {
|
||||
if roles[i] == models.AdminRole {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
ginx.Bomb(http.StatusForbidden, "forbidden")
|
||||
}
|
||||
|
||||
c.Set("user", user)
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
func (rt *Router) extractTokenMetadata(r *http.Request) (*AccessDetails, error) {
|
||||
token, err := rt.verifyToken(rt.HTTP.JWTAuth.SigningKey, rt.extractToken(r))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
claims, ok := token.Claims.(jwt.MapClaims)
|
||||
if ok && token.Valid {
|
||||
accessUuid, ok := claims["access_uuid"].(string)
|
||||
if !ok {
|
||||
return nil, errors.New("failed to parse access_uuid from jwt")
|
||||
}
|
||||
|
||||
return &AccessDetails{
|
||||
AccessUuid: accessUuid,
|
||||
UserIdentity: claims["user_identity"].(string),
|
||||
}, nil
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
func (rt *Router) extractToken(r *http.Request) string {
|
||||
tok := r.Header.Get("Authorization")
|
||||
|
||||
if len(tok) > 6 && strings.ToUpper(tok[0:7]) == "BEARER " {
|
||||
return tok[7:]
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func (rt *Router) createAuth(ctx context.Context, userIdentity string, td *TokenDetails) error {
|
||||
at := time.Unix(td.AtExpires, 0)
|
||||
rte := time.Unix(td.RtExpires, 0)
|
||||
now := time.Now()
|
||||
|
||||
errAccess := rt.Redis.Set(ctx, rt.wrapJwtKey(td.AccessUuid), userIdentity, at.Sub(now)).Err()
|
||||
if errAccess != nil {
|
||||
return errAccess
|
||||
}
|
||||
|
||||
errRefresh := rt.Redis.Set(ctx, rt.wrapJwtKey(td.RefreshUuid), userIdentity, rte.Sub(now)).Err()
|
||||
if errRefresh != nil {
|
||||
return errRefresh
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (rt *Router) fetchAuth(ctx context.Context, givenUuid string) (string, error) {
|
||||
return rt.Redis.Get(ctx, rt.wrapJwtKey(givenUuid)).Result()
|
||||
}
|
||||
|
||||
func (rt *Router) deleteAuth(ctx context.Context, givenUuid string) error {
|
||||
return rt.Redis.Del(ctx, rt.wrapJwtKey(givenUuid)).Err()
|
||||
}
|
||||
|
||||
func (rt *Router) deleteTokens(ctx context.Context, authD *AccessDetails) error {
|
||||
// get the refresh uuid
|
||||
refreshUuid := authD.AccessUuid + "++" + authD.UserIdentity
|
||||
|
||||
// delete access token
|
||||
err := rt.Redis.Del(ctx, rt.wrapJwtKey(authD.AccessUuid)).Err()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// delete refresh token
|
||||
err = rt.Redis.Del(ctx, rt.wrapJwtKey(refreshUuid)).Err()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (rt *Router) wrapJwtKey(key string) string {
|
||||
return rt.HTTP.JWTAuth.RedisKeyPrefix + key
|
||||
}
|
||||
|
||||
type TokenDetails struct {
|
||||
AccessToken string
|
||||
RefreshToken string
|
||||
AccessUuid string
|
||||
RefreshUuid string
|
||||
AtExpires int64
|
||||
RtExpires int64
|
||||
}
|
||||
|
||||
func (rt *Router) createTokens(signingKey, userIdentity string) (*TokenDetails, error) {
|
||||
td := &TokenDetails{}
|
||||
td.AtExpires = time.Now().Add(time.Minute * time.Duration(rt.HTTP.JWTAuth.AccessExpired)).Unix()
|
||||
td.AccessUuid = uuid.NewString()
|
||||
|
||||
td.RtExpires = time.Now().Add(time.Minute * time.Duration(rt.HTTP.JWTAuth.RefreshExpired)).Unix()
|
||||
td.RefreshUuid = td.AccessUuid + "++" + userIdentity
|
||||
|
||||
var err error
|
||||
// Creating Access Token
|
||||
atClaims := jwt.MapClaims{}
|
||||
atClaims["authorized"] = true
|
||||
atClaims["access_uuid"] = td.AccessUuid
|
||||
atClaims["user_identity"] = userIdentity
|
||||
atClaims["exp"] = td.AtExpires
|
||||
at := jwt.NewWithClaims(jwt.SigningMethodHS256, atClaims)
|
||||
td.AccessToken, err = at.SignedString([]byte(signingKey))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Creating Refresh Token
|
||||
rtClaims := jwt.MapClaims{}
|
||||
rtClaims["refresh_uuid"] = td.RefreshUuid
|
||||
rtClaims["user_identity"] = userIdentity
|
||||
rtClaims["exp"] = td.RtExpires
|
||||
jrt := jwt.NewWithClaims(jwt.SigningMethodHS256, rtClaims)
|
||||
td.RefreshToken, err = jrt.SignedString([]byte(signingKey))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return td, nil
|
||||
}
|
||||
|
||||
func (rt *Router) verifyToken(signingKey, tokenString string) (*jwt.Token, error) {
|
||||
if tokenString == "" {
|
||||
return nil, fmt.Errorf("bearer token not found")
|
||||
}
|
||||
|
||||
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
|
||||
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
return nil, fmt.Errorf("unexpected jwt signing method: %v", token.Header["alg"])
|
||||
}
|
||||
return []byte(signingKey), nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return token, nil
|
||||
}
|
||||
221
center/router/router_notify_config.go
Normal file
221
center/router/router_notify_config.go
Normal file
@@ -0,0 +1,221 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"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/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) {
|
||||
var webhooks []models.Webhook
|
||||
cval, err := models.ConfigsGet(rt.Ctx, models.WEBHOOKKEY)
|
||||
ginx.Dangerous(err)
|
||||
if cval == "" {
|
||||
ginx.NewRender(c).Data(webhooks, nil)
|
||||
return
|
||||
}
|
||||
|
||||
err = json.Unmarshal([]byte(cval), &webhooks)
|
||||
ginx.NewRender(c).Data(webhooks, err)
|
||||
}
|
||||
|
||||
func (rt *Router) webhookPuts(c *gin.Context) {
|
||||
var webhooks []models.Webhook
|
||||
ginx.BindJSON(c, &webhooks)
|
||||
for i := 0; i < len(webhooks); i++ {
|
||||
webhooks[i].Headers = []string{}
|
||||
if len(webhooks[i].HeaderMap) > 0 {
|
||||
for k, v := range webhooks[i].HeaderMap {
|
||||
webhooks[i].Headers = append(webhooks[i].Headers, k)
|
||||
webhooks[i].Headers = append(webhooks[i].Headers, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data, err := json.Marshal(webhooks)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
ginx.NewRender(c).Message(models.ConfigsSet(rt.Ctx, models.WEBHOOKKEY, string(data)))
|
||||
}
|
||||
|
||||
func (rt *Router) notifyScriptGet(c *gin.Context) {
|
||||
var notifyScript models.NotifyScript
|
||||
cval, err := models.ConfigsGet(rt.Ctx, models.NOTIFYSCRIPT)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
if cval == "" {
|
||||
ginx.NewRender(c).Data(notifyScript, nil)
|
||||
return
|
||||
}
|
||||
|
||||
err = json.Unmarshal([]byte(cval), ¬ifyScript)
|
||||
ginx.NewRender(c).Data(notifyScript, err)
|
||||
}
|
||||
|
||||
func (rt *Router) notifyScriptPut(c *gin.Context) {
|
||||
var notifyScript models.NotifyScript
|
||||
ginx.BindJSON(c, ¬ifyScript)
|
||||
|
||||
data, err := json.Marshal(notifyScript)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
ginx.NewRender(c).Message(models.ConfigsSet(rt.Ctx, models.NOTIFYSCRIPT, string(data)))
|
||||
}
|
||||
|
||||
func (rt *Router) notifyChannelGets(c *gin.Context) {
|
||||
var notifyChannels []models.NotifyChannel
|
||||
cval, err := models.ConfigsGet(rt.Ctx, models.NOTIFYCHANNEL)
|
||||
ginx.Dangerous(err)
|
||||
if cval == "" {
|
||||
ginx.NewRender(c).Data(notifyChannels, nil)
|
||||
return
|
||||
}
|
||||
|
||||
err = json.Unmarshal([]byte(cval), ¬ifyChannels)
|
||||
ginx.NewRender(c).Data(notifyChannels, err)
|
||||
}
|
||||
|
||||
func (rt *Router) notifyChannelPuts(c *gin.Context) {
|
||||
var notifyChannels []models.NotifyChannel
|
||||
ginx.BindJSON(c, ¬ifyChannels)
|
||||
|
||||
channels := []string{models.Dingtalk, models.Wecom, models.Feishu, models.Mm, models.Telegram, models.Email}
|
||||
|
||||
m := make(map[string]struct{})
|
||||
for _, v := range notifyChannels {
|
||||
m[v.Ident] = struct{}{}
|
||||
}
|
||||
|
||||
for _, v := range channels {
|
||||
if _, ok := m[v]; !ok {
|
||||
ginx.Bomb(200, "channel %s ident can not modify", v)
|
||||
}
|
||||
}
|
||||
|
||||
data, err := json.Marshal(notifyChannels)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
ginx.NewRender(c).Message(models.ConfigsSet(rt.Ctx, models.NOTIFYCHANNEL, string(data)))
|
||||
}
|
||||
|
||||
func (rt *Router) notifyContactGets(c *gin.Context) {
|
||||
var notifyContacts []models.NotifyContact
|
||||
cval, err := models.ConfigsGet(rt.Ctx, models.NOTIFYCONTACT)
|
||||
ginx.Dangerous(err)
|
||||
if cval == "" {
|
||||
ginx.NewRender(c).Data(notifyContacts, nil)
|
||||
return
|
||||
}
|
||||
|
||||
err = json.Unmarshal([]byte(cval), ¬ifyContacts)
|
||||
ginx.NewRender(c).Data(notifyContacts, err)
|
||||
}
|
||||
|
||||
func (rt *Router) notifyContactPuts(c *gin.Context) {
|
||||
var notifyContacts []models.NotifyContact
|
||||
ginx.BindJSON(c, ¬ifyContacts)
|
||||
|
||||
keys := []string{models.DingtalkKey, models.WecomKey, models.FeishuKey, models.MmKey, models.TelegramKey}
|
||||
|
||||
m := make(map[string]struct{})
|
||||
for _, v := range notifyContacts {
|
||||
m[v.Ident] = struct{}{}
|
||||
}
|
||||
|
||||
for _, v := range keys {
|
||||
if _, ok := m[v]; !ok {
|
||||
ginx.Bomb(200, "contact %s ident can not modify", v)
|
||||
}
|
||||
}
|
||||
|
||||
data, err := json.Marshal(notifyContacts)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
ginx.NewRender(c).Message(models.ConfigsSet(rt.Ctx, models.NOTIFYCONTACT, string(data)))
|
||||
}
|
||||
|
||||
func (rt *Router) notifyConfigGet(c *gin.Context) {
|
||||
key := ginx.QueryStr(c, "ckey")
|
||||
cval, err := models.ConfigsGet(rt.Ctx, key)
|
||||
if cval == "" {
|
||||
switch key {
|
||||
case models.IBEX:
|
||||
cval = memsto.DefaultIbex
|
||||
case models.SMTP:
|
||||
cval = memsto.DefaultSMTP
|
||||
}
|
||||
}
|
||||
ginx.NewRender(c).Data(cval, err)
|
||||
}
|
||||
|
||||
func (rt *Router) notifyConfigPut(c *gin.Context) {
|
||||
var f models.Configs
|
||||
ginx.BindJSON(c, &f)
|
||||
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)
|
||||
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())
|
||||
}
|
||||
|
||||
if f.Ckey == models.SMTP {
|
||||
// 重置邮件发送器
|
||||
smtp := smtpValidate(f.Cval)
|
||||
|
||||
go sender.RestartEmailSender(smtp)
|
||||
}
|
||||
|
||||
ginx.NewRender(c).Message(nil)
|
||||
}
|
||||
|
||||
func smtpValidate(smtpStr string) aconf.SMTPConfig {
|
||||
var smtp aconf.SMTPConfig
|
||||
ginx.Dangerous(toml.Unmarshal([]byte(smtpStr), &smtp))
|
||||
|
||||
if smtp.Host == "" || smtp.Port == 0 {
|
||||
ginx.Bomb(200, "smtp host or port can not be empty")
|
||||
}
|
||||
return smtp
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
smtp := smtpValidate(f.Cval)
|
||||
ginx.NewRender(c).Message(sender.SendEmail("Email test", "email content", []string{f.Email}, smtp))
|
||||
|
||||
}
|
||||
148
center/router/router_notify_tpl.go
Normal file
148
center/router/router_notify_tpl.go
Normal file
@@ -0,0 +1,148 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"strings"
|
||||
|
||||
"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) {
|
||||
var f models.NotifyTpl
|
||||
ginx.BindJSON(c, &f)
|
||||
ginx.Dangerous(templateValidate(f))
|
||||
|
||||
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))
|
||||
|
||||
ginx.NewRender(c).Message(f.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
|
||||
}
|
||||
|
||||
var defs = []string{
|
||||
"{{$labels := .TagsMap}}",
|
||||
"{{$value := .TriggerValue}}",
|
||||
}
|
||||
text := strings.Join(append(defs, f.Content), "")
|
||||
|
||||
if _, err := template.New(f.Channel).Funcs(tplx.TemplateFuncMap).Parse(text); err != nil {
|
||||
return fmt.Errorf("notify template verify illegal:%s", err.Error())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (rt *Router) notifyTplPreview(c *gin.Context) {
|
||||
var event models.AlertCurEvent
|
||||
err := json.Unmarshal([]byte(cconf.EVENT_EXAMPLE), &event)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
var f models.NotifyTpl
|
||||
ginx.BindJSON(c, &f)
|
||||
|
||||
var defs = []string{
|
||||
"{{$labels := .TagsMap}}",
|
||||
"{{$value := .TriggerValue}}",
|
||||
}
|
||||
text := strings.Join(append(defs, f.Content), "")
|
||||
tpl, err := template.New(f.Channel).Funcs(tplx.TemplateFuncMap).Parse(text)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
event.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
|
||||
}
|
||||
|
||||
event.TagsMap[arr[0]] = arr[1]
|
||||
}
|
||||
|
||||
var body bytes.Buffer
|
||||
var ret string
|
||||
if err := tpl.Execute(&body, event); err != nil {
|
||||
ret = err.Error()
|
||||
} else {
|
||||
ret = body.String()
|
||||
}
|
||||
|
||||
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.NotifyTplCountByChannel(rt.Ctx, f.Channel)
|
||||
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")
|
||||
ginx.NewRender(c).Message(f.NotifyTplDelete(rt.Ctx, id))
|
||||
}
|
||||
224
center/router/router_proxy.go
Normal file
224
center/router/router_proxy.go
Normal file
@@ -0,0 +1,224 @@
|
||||
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/gin-gonic/gin"
|
||||
"github.com/prometheus/common/model"
|
||||
"github.com/toolkits/pkg/ginx"
|
||||
"github.com/toolkits/pkg/logger"
|
||||
)
|
||||
|
||||
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 {
|
||||
DatasourceId int64 `json:"datasource_id" binding:"required"`
|
||||
Queries []queryFormItem `json:"queries" binding:"required"`
|
||||
}
|
||||
|
||||
func (rt *Router) promBatchQueryRange(c *gin.Context) {
|
||||
var f batchQueryForm
|
||||
ginx.Dangerous(c.BindJSON(&f))
|
||||
var lst []model.Value
|
||||
|
||||
cli := rt.PromClients.GetCli(f.DatasourceId)
|
||||
if cli == nil {
|
||||
logger.Warningf("no such datasource id: %d", f.DatasourceId)
|
||||
ginx.NewRender(c).Data(lst, nil)
|
||||
return
|
||||
}
|
||||
|
||||
for _, item := range f.Queries {
|
||||
r := pkgprom.Range{
|
||||
Start: time.Unix(item.Start, 0),
|
||||
End: time.Unix(item.End, 0),
|
||||
Step: time.Duration(item.Step) * time.Second,
|
||||
}
|
||||
|
||||
resp, _, err := cli.QueryRange(context.Background(), item.Query, r)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
lst = append(lst, resp)
|
||||
}
|
||||
|
||||
ginx.NewRender(c).Data(lst, nil)
|
||||
}
|
||||
|
||||
type batchInstantForm struct {
|
||||
DatasourceId int64 `json:"datasource_id" binding:"required"`
|
||||
Queries []InstantFormItem `json:"queries" binding:"required"`
|
||||
}
|
||||
|
||||
type InstantFormItem struct {
|
||||
Time int64 `json:"time" binding:"required"`
|
||||
Query string `json:"query" binding:"required"`
|
||||
}
|
||||
|
||||
func (rt *Router) promBatchQueryInstant(c *gin.Context) {
|
||||
var f batchInstantForm
|
||||
ginx.Dangerous(c.BindJSON(&f))
|
||||
|
||||
var lst []model.Value
|
||||
|
||||
cli := rt.PromClients.GetCli(f.DatasourceId)
|
||||
if cli == nil {
|
||||
logger.Warningf("no such datasource id: %d", f.DatasourceId)
|
||||
ginx.NewRender(c).Data(lst, nil)
|
||||
return
|
||||
}
|
||||
|
||||
for _, item := range f.Queries {
|
||||
resp, _, err := cli.Query(context.Background(), item.Query, time.Unix(item.Time, 0))
|
||||
ginx.Dangerous(err)
|
||||
|
||||
lst = append(lst, resp)
|
||||
}
|
||||
|
||||
ginx.NewRender(c).Data(lst, nil)
|
||||
}
|
||||
|
||||
func (rt *Router) dsProxy(c *gin.Context) {
|
||||
dsId := ginx.UrlParamInt64(c, "id")
|
||||
ds := rt.DatasourceCache.GetById(dsId)
|
||||
|
||||
if ds == nil {
|
||||
c.String(http.StatusBadRequest, "no such datasource")
|
||||
return
|
||||
}
|
||||
|
||||
target, err := url.Parse(ds.HTTPJson.Url)
|
||||
if err != nil {
|
||||
c.String(http.StatusInternalServerError, "invalid url: %s", ds.HTTPJson.Url)
|
||||
return
|
||||
}
|
||||
|
||||
director := func(req *http.Request) {
|
||||
req.URL.Scheme = target.Scheme
|
||||
req.URL.Host = target.Host
|
||||
req.Host = target.Host
|
||||
|
||||
req.Header.Set("Host", target.Host)
|
||||
|
||||
// fe request e.g. /api/n9e/proxy/:id/*
|
||||
arr := strings.Split(req.URL.Path, "/")
|
||||
if len(arr) < 6 {
|
||||
c.String(http.StatusBadRequest, "invalid url path")
|
||||
return
|
||||
}
|
||||
|
||||
req.URL.Path = strings.TrimRight(target.Path, "/") + "/" + strings.Join(arr[5:], "/")
|
||||
if target.RawQuery == "" || req.URL.RawQuery == "" {
|
||||
req.URL.RawQuery = target.RawQuery + req.URL.RawQuery
|
||||
} else {
|
||||
req.URL.RawQuery = target.RawQuery + "&" + req.URL.RawQuery
|
||||
}
|
||||
|
||||
if _, ok := req.Header["User-Agent"]; !ok {
|
||||
req.Header.Set("User-Agent", "")
|
||||
}
|
||||
|
||||
if ds.AuthJson.BasicAuthUser != "" {
|
||||
req.SetBasicAuth(ds.AuthJson.BasicAuthUser, ds.AuthJson.BasicAuthPassword)
|
||||
}
|
||||
|
||||
headerCount := len(ds.HTTPJson.Headers)
|
||||
if headerCount > 0 {
|
||||
for key, value := range ds.HTTPJson.Headers {
|
||||
req.Header.Set(key, value)
|
||||
if key == "Host" {
|
||||
req.Host = value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
errFunc := func(w http.ResponseWriter, r *http.Request, err error) {
|
||||
http.Error(w, err.Error(), http.StatusBadGateway)
|
||||
}
|
||||
|
||||
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 {
|
||||
return fmt.Errorf("unauthorized access")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
proxy := &httputil.ReverseProxy{
|
||||
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()
|
||||
}
|
||||
146
center/router/router_recording_rule.go
Normal file
146
center/router/router_recording_rule.go
Normal file
@@ -0,0 +1,146 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/ccfos/nightingale/v6/models"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/toolkits/pkg/ginx"
|
||||
)
|
||||
|
||||
func (rt *Router) recordingRuleGets(c *gin.Context) {
|
||||
busiGroupId := ginx.UrlParamInt64(c, "id")
|
||||
ars, err := models.RecordingRuleGets(rt.Ctx, busiGroupId)
|
||||
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)
|
||||
}
|
||||
|
||||
func (rt *Router) recordingRuleGet(c *gin.Context) {
|
||||
rrid := ginx.UrlParamInt64(c, "rrid")
|
||||
|
||||
ar, err := models.RecordingRuleGetById(rt.Ctx, rrid)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
if ar == nil {
|
||||
ginx.NewRender(c, http.StatusNotFound).Message("No such recording rule")
|
||||
return
|
||||
}
|
||||
|
||||
ginx.NewRender(c).Data(ar, err)
|
||||
}
|
||||
|
||||
func (rt *Router) recordingRuleAddByFE(c *gin.Context) {
|
||||
username := c.MustGet("username").(string)
|
||||
|
||||
var lst []models.RecordingRule
|
||||
ginx.BindJSON(c, &lst)
|
||||
|
||||
count := len(lst)
|
||||
if count == 0 {
|
||||
ginx.Bomb(http.StatusBadRequest, "input json is empty")
|
||||
}
|
||||
|
||||
bgid := ginx.UrlParamInt64(c, "id")
|
||||
reterr := make(map[string]string)
|
||||
for i := 0; i < count; i++ {
|
||||
lst[i].Id = 0
|
||||
lst[i].GroupId = bgid
|
||||
lst[i].CreateBy = username
|
||||
lst[i].UpdateBy = username
|
||||
lst[i].FE2DB()
|
||||
|
||||
if err := lst[i].Add(rt.Ctx); err != nil {
|
||||
reterr[lst[i].Name] = err.Error()
|
||||
} else {
|
||||
reterr[lst[i].Name] = ""
|
||||
}
|
||||
}
|
||||
ginx.NewRender(c).Data(reterr, nil)
|
||||
}
|
||||
|
||||
func (rt *Router) recordingRulePutByFE(c *gin.Context) {
|
||||
var f models.RecordingRule
|
||||
ginx.BindJSON(c, &f)
|
||||
|
||||
rrid := ginx.UrlParamInt64(c, "rrid")
|
||||
ar, err := models.RecordingRuleGetById(rt.Ctx, rrid)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
if ar == nil {
|
||||
ginx.NewRender(c, http.StatusNotFound).Message("No such recording rule")
|
||||
return
|
||||
}
|
||||
|
||||
rt.bgrwCheck(c, ar.GroupId)
|
||||
|
||||
f.UpdateBy = c.MustGet("username").(string)
|
||||
ginx.NewRender(c).Message(ar.Update(rt.Ctx, f))
|
||||
|
||||
}
|
||||
|
||||
func (rt *Router) recordingRuleDel(c *gin.Context) {
|
||||
var f idsForm
|
||||
ginx.BindJSON(c, &f)
|
||||
f.Verify()
|
||||
|
||||
ginx.NewRender(c).Message(models.RecordingRuleDels(rt.Ctx, f.Ids, ginx.UrlParamInt64(c, "id")))
|
||||
|
||||
}
|
||||
|
||||
type recordRuleFieldForm struct {
|
||||
Ids []int64 `json:"ids"`
|
||||
Fields map[string]interface{} `json:"fields"`
|
||||
}
|
||||
|
||||
func (rt *Router) recordingRulePutFields(c *gin.Context) {
|
||||
var f recordRuleFieldForm
|
||||
ginx.BindJSON(c, &f)
|
||||
|
||||
if len(f.Fields) == 0 {
|
||||
ginx.Bomb(http.StatusBadRequest, "fields empty")
|
||||
}
|
||||
|
||||
f.Fields["update_by"] = c.MustGet("username").(string)
|
||||
f.Fields["update_at"] = time.Now().Unix()
|
||||
|
||||
if _, ok := f.Fields["datasource_ids"]; ok {
|
||||
// datasource_ids = "1 2 3"
|
||||
idsStr := strings.Fields(f.Fields["datasource_ids"].(string))
|
||||
ids := make([]int64, 0)
|
||||
for _, idStr := range idsStr {
|
||||
id, err := strconv.ParseInt(idStr, 10, 64)
|
||||
if err != nil {
|
||||
ginx.Bomb(http.StatusBadRequest, "datasource_ids error")
|
||||
}
|
||||
ids = append(ids, id)
|
||||
}
|
||||
|
||||
bs, err := json.Marshal(ids)
|
||||
if err != nil {
|
||||
ginx.Bomb(http.StatusBadRequest, "datasource_ids error")
|
||||
}
|
||||
f.Fields["datasource_ids"] = string(bs)
|
||||
}
|
||||
|
||||
for i := 0; i < len(f.Ids); i++ {
|
||||
ar, err := models.RecordingRuleGetById(rt.Ctx, f.Ids[i])
|
||||
ginx.Dangerous(err)
|
||||
|
||||
if ar == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
ginx.Dangerous(ar.UpdateFieldsMap(rt.Ctx, f.Fields))
|
||||
}
|
||||
|
||||
ginx.NewRender(c).Message(nil)
|
||||
}
|
||||
100
center/router/router_role.go
Normal file
100
center/router/router_role.go
Normal file
@@ -0,0 +1,100 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/ccfos/nightingale/v6/models"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/toolkits/pkg/ginx"
|
||||
)
|
||||
|
||||
func (rt *Router) rolesGets(c *gin.Context) {
|
||||
lst, err := models.RoleGetsAll(rt.Ctx)
|
||||
ginx.NewRender(c).Data(lst, err)
|
||||
}
|
||||
|
||||
func (rt *Router) permsGets(c *gin.Context) {
|
||||
user := c.MustGet("user").(*models.User)
|
||||
lst, err := models.OperationsOfRole(rt.Ctx, strings.Fields(user.Roles))
|
||||
ginx.NewRender(c).Data(lst, err)
|
||||
}
|
||||
|
||||
// 创建角色
|
||||
func (rt *Router) roleAdd(c *gin.Context) {
|
||||
var f models.Role
|
||||
ginx.BindJSON(c, &f)
|
||||
|
||||
err := f.Add(rt.Ctx)
|
||||
ginx.NewRender(c).Message(err)
|
||||
}
|
||||
|
||||
// 更新角色
|
||||
func (rt *Router) rolePut(c *gin.Context) {
|
||||
var f models.Role
|
||||
ginx.BindJSON(c, &f)
|
||||
oldRule, err := models.RoleGet(rt.Ctx, "id=?", f.Id)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
if oldRule == nil {
|
||||
ginx.Bomb(http.StatusOK, "role not found")
|
||||
}
|
||||
|
||||
if oldRule.Name == "Admin" {
|
||||
ginx.Bomb(http.StatusOK, "admin role can not be modified")
|
||||
}
|
||||
|
||||
if oldRule.Name != f.Name {
|
||||
// name changed, check duplication
|
||||
num, err := models.RoleCount(rt.Ctx, "name=? and id<>?", f.Name, oldRule.Id)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
if num > 0 {
|
||||
ginx.Bomb(http.StatusOK, "role name already exists")
|
||||
}
|
||||
}
|
||||
|
||||
oldRule.Name = f.Name
|
||||
oldRule.Note = f.Note
|
||||
|
||||
ginx.NewRender(c).Message(oldRule.Update(rt.Ctx, "name", "note"))
|
||||
}
|
||||
|
||||
func (rt *Router) roleDel(c *gin.Context) {
|
||||
id := ginx.UrlParamInt64(c, "id")
|
||||
target, err := models.RoleGet(rt.Ctx, "id=?", id)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
if target.Name == "Admin" {
|
||||
ginx.Bomb(http.StatusOK, "admin role can not be modified")
|
||||
}
|
||||
|
||||
if target == nil {
|
||||
ginx.NewRender(c).Message(nil)
|
||||
return
|
||||
}
|
||||
|
||||
ginx.NewRender(c).Message(target.Del(rt.Ctx))
|
||||
}
|
||||
|
||||
// 角色列表
|
||||
func (rt *Router) roleGets(c *gin.Context) {
|
||||
lst, err := models.RoleGetsAll(rt.Ctx)
|
||||
ginx.NewRender(c).Data(lst, err)
|
||||
}
|
||||
|
||||
func (rt *Router) allPerms(c *gin.Context) {
|
||||
roles, err := models.RoleGetsAll(rt.Ctx)
|
||||
ginx.Dangerous(err)
|
||||
m := make(map[string][]string)
|
||||
for _, r := range roles {
|
||||
lst, err := models.OperationsOfRole(rt.Ctx, strings.Fields(r.Name))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
m[r.Name] = lst
|
||||
}
|
||||
|
||||
ginx.NewRender(c).Data(m, err)
|
||||
}
|
||||
43
center/router/router_role_operation.go
Normal file
43
center/router/router_role_operation.go
Normal file
@@ -0,0 +1,43 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/ccfos/nightingale/v6/models"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/toolkits/pkg/ginx"
|
||||
)
|
||||
|
||||
func (rt *Router) operationOfRole(c *gin.Context) {
|
||||
id := ginx.UrlParamInt64(c, "id")
|
||||
role, err := models.RoleGet(rt.Ctx, "id=?", id)
|
||||
ginx.Dangerous(err)
|
||||
if role == nil {
|
||||
ginx.Bomb(http.StatusOK, "role not found")
|
||||
}
|
||||
|
||||
ops, err := models.OperationsOfRole(rt.Ctx, []string{role.Name})
|
||||
ginx.NewRender(c).Data(ops, err)
|
||||
}
|
||||
|
||||
func (rt *Router) roleBindOperation(c *gin.Context) {
|
||||
id := ginx.UrlParamInt64(c, "id")
|
||||
role, err := models.RoleGet(rt.Ctx, "id=?", id)
|
||||
ginx.Dangerous(err)
|
||||
if role == nil {
|
||||
ginx.Bomb(http.StatusOK, "role not found")
|
||||
}
|
||||
|
||||
if role.Name == "Admin" {
|
||||
ginx.Bomb(http.StatusOK, "admin role can not be modified")
|
||||
}
|
||||
|
||||
var ops []string
|
||||
ginx.BindJSON(c, &ops)
|
||||
|
||||
ginx.NewRender(c).Message(models.RoleOperationBind(rt.Ctx, role.Name, ops))
|
||||
}
|
||||
|
||||
func (rt *Router) operations(c *gin.Context) {
|
||||
ginx.NewRender(c).Data(rt.Operations.Ops, nil)
|
||||
}
|
||||
52
center/router/router_self.go
Normal file
52
center/router/router_self.go
Normal file
@@ -0,0 +1,52 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"github.com/ccfos/nightingale/v6/models"
|
||||
"github.com/ccfos/nightingale/v6/pkg/ormx"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/toolkits/pkg/ginx"
|
||||
)
|
||||
|
||||
func (rt *Router) selfProfileGet(c *gin.Context) {
|
||||
user := c.MustGet("user").(*models.User)
|
||||
if user.IsAdmin() {
|
||||
user.Admin = true
|
||||
}
|
||||
ginx.NewRender(c).Data(user, nil)
|
||||
}
|
||||
|
||||
type selfProfileForm struct {
|
||||
Nickname string `json:"nickname"`
|
||||
Phone string `json:"phone"`
|
||||
Email string `json:"email"`
|
||||
Portrait string `json:"portrait"`
|
||||
Contacts ormx.JSONObj `json:"contacts"`
|
||||
}
|
||||
|
||||
func (rt *Router) selfProfilePut(c *gin.Context) {
|
||||
var f selfProfileForm
|
||||
ginx.BindJSON(c, &f)
|
||||
|
||||
user := c.MustGet("user").(*models.User)
|
||||
user.Nickname = f.Nickname
|
||||
user.Phone = f.Phone
|
||||
user.Email = f.Email
|
||||
user.Portrait = f.Portrait
|
||||
user.Contacts = f.Contacts
|
||||
user.UpdateBy = user.Username
|
||||
|
||||
ginx.NewRender(c).Message(user.UpdateAllFields(rt.Ctx))
|
||||
}
|
||||
|
||||
type selfPasswordForm struct {
|
||||
OldPass string `json:"oldpass" binding:"required"`
|
||||
NewPass string `json:"newpass" binding:"required"`
|
||||
}
|
||||
|
||||
func (rt *Router) selfPasswordPut(c *gin.Context) {
|
||||
var f selfPasswordForm
|
||||
ginx.BindJSON(c, &f)
|
||||
user := c.MustGet("user").(*models.User)
|
||||
ginx.NewRender(c).Message(user.ChangePassword(rt.Ctx, f.OldPass, f.NewPass))
|
||||
}
|
||||
40
center/router/router_server.go
Normal file
40
center/router/router_server.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/ccfos/nightingale/v6/models"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/toolkits/pkg/ginx"
|
||||
)
|
||||
|
||||
func (rt *Router) serversGet(c *gin.Context) {
|
||||
list, err := models.AlertingEngineGets(rt.Ctx, "")
|
||||
ginx.NewRender(c).Data(list, err)
|
||||
}
|
||||
|
||||
func (rt *Router) serverClustersGet(c *gin.Context) {
|
||||
list, err := models.AlertingEngineGetsClusters(rt.Ctx, "")
|
||||
ginx.NewRender(c).Data(list, err)
|
||||
}
|
||||
|
||||
func (rt *Router) serverHeartbeat(c *gin.Context) {
|
||||
var req models.HeartbeatInfo
|
||||
ginx.BindJSON(c, &req)
|
||||
err := models.AlertingEngineHeartbeatWithCluster(rt.Ctx, req.Instance, req.EngineCluster, req.DatasourceId)
|
||||
ginx.NewRender(c).Message(err)
|
||||
}
|
||||
|
||||
func (rt *Router) serversActive(c *gin.Context) {
|
||||
datasourceId := ginx.QueryInt64(c, "dsid")
|
||||
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
|
||||
}
|
||||
|
||||
servers, err := models.AlertingEngineGetsInstances(rt.Ctx, "datasource_id = ? and clock > ?", datasourceId, time.Now().Unix()-30)
|
||||
ginx.NewRender(c).Data(servers, err)
|
||||
}
|
||||
371
center/router/router_target.go
Normal file
371
center/router/router_target.go
Normal file
@@ -0,0 +1,371 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/ccfos/nightingale/v6/models"
|
||||
"github.com/ccfos/nightingale/v6/storage"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/prometheus/common/model"
|
||||
"github.com/toolkits/pkg/ginx"
|
||||
"github.com/toolkits/pkg/logger"
|
||||
)
|
||||
|
||||
type TargetQuery struct {
|
||||
Filters []models.HostQuery `json:"queries"`
|
||||
P int `json:"p"`
|
||||
Limit int `json:"limit"`
|
||||
}
|
||||
|
||||
func (rt *Router) targetGetsByHostFilter(c *gin.Context) {
|
||||
var f TargetQuery
|
||||
ginx.BindJSON(c, &f)
|
||||
|
||||
query := models.GetHostsQuery(f.Filters)
|
||||
|
||||
hosts, err := models.TargetGetsByFilter(rt.Ctx, query, f.Limit, (f.P-1)*f.Limit)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
total, err := models.TargetCountByFilter(rt.Ctx, query)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
ginx.NewRender(c).Data(gin.H{
|
||||
"list": hosts,
|
||||
"total": total,
|
||||
}, nil)
|
||||
}
|
||||
|
||||
func (rt *Router) targetGets(c *gin.Context) {
|
||||
bgid := ginx.QueryInt64(c, "bgid", -1)
|
||||
query := ginx.QueryStr(c, "query", "")
|
||||
limit := ginx.QueryInt(c, "limit", 30)
|
||||
downtime := ginx.QueryInt64(c, "downtime", 0)
|
||||
dsIds := queryDatasourceIds(c)
|
||||
|
||||
var bgids []int64
|
||||
var err error
|
||||
if bgid == -1 {
|
||||
user := c.MustGet("user").(*models.User)
|
||||
if !user.IsAdmin() {
|
||||
// 如果是非 admin 用户,全部对象的情况,找到用户有权限的业务组
|
||||
userGroupIds, err := models.MyGroupIds(rt.Ctx, user.Id)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
bgids, err = models.BusiGroupIds(rt.Ctx, userGroupIds)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
// 将未分配业务组的对象也加入到列表中
|
||||
bgids = append(bgids, 0)
|
||||
}
|
||||
} else {
|
||||
bgids = append(bgids, bgid)
|
||||
}
|
||||
|
||||
total, err := models.TargetTotal(rt.Ctx, bgids, dsIds, query, downtime)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
list, err := models.TargetGets(rt.Ctx, bgids, dsIds, query, downtime, limit, ginx.Offset(c, limit))
|
||||
ginx.Dangerous(err)
|
||||
|
||||
if err == nil {
|
||||
now := time.Now()
|
||||
cache := make(map[int64]*models.BusiGroup)
|
||||
|
||||
var keys []string
|
||||
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 {
|
||||
metaMap := make(map[string]*models.HostMeta)
|
||||
vals := storage.MGet(context.Background(), rt.Redis, keys)
|
||||
for _, value := range vals {
|
||||
var meta models.HostMeta
|
||||
if value == nil {
|
||||
continue
|
||||
}
|
||||
err := json.Unmarshal(value, &meta)
|
||||
if err != nil {
|
||||
logger.Warningf("unmarshal %v host meta failed: %v", value, err)
|
||||
continue
|
||||
}
|
||||
metaMap[meta.Hostname] = &meta
|
||||
}
|
||||
|
||||
for i := 0; i < len(list); i++ {
|
||||
if meta, ok := metaMap[list[i].Ident]; ok {
|
||||
list[i].FillMeta(meta)
|
||||
} else {
|
||||
// 未上报过元数据的主机,cpuNum默认为-1, 用于前端展示 unknown
|
||||
list[i].CpuNum = -1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
ginx.NewRender(c).Data(gin.H{
|
||||
"list": list,
|
||||
"total": total,
|
||||
}, nil)
|
||||
}
|
||||
|
||||
func (rt *Router) targetGetsByService(c *gin.Context) {
|
||||
lst, err := models.TargetGetsAll(rt.Ctx)
|
||||
ginx.NewRender(c).Data(lst, err)
|
||||
}
|
||||
|
||||
func (rt *Router) targetGetTags(c *gin.Context) {
|
||||
idents := ginx.QueryStr(c, "idents", "")
|
||||
idents = strings.ReplaceAll(idents, ",", " ")
|
||||
lst, err := models.TargetGetTags(rt.Ctx, strings.Fields(idents))
|
||||
ginx.NewRender(c).Data(lst, err)
|
||||
}
|
||||
|
||||
type targetTagsForm struct {
|
||||
Idents []string `json:"idents" binding:"required"`
|
||||
Tags []string `json:"tags" binding:"required"`
|
||||
}
|
||||
|
||||
func (rt *Router) targetBindTagsByFE(c *gin.Context) {
|
||||
var f targetTagsForm
|
||||
ginx.BindJSON(c, &f)
|
||||
|
||||
if len(f.Idents) == 0 {
|
||||
ginx.Bomb(http.StatusBadRequest, "idents empty")
|
||||
}
|
||||
|
||||
rt.checkTargetPerm(c, f.Idents)
|
||||
|
||||
ginx.NewRender(c).Message(rt.targetBindTags(f))
|
||||
}
|
||||
|
||||
func (rt *Router) targetBindTagsByService(c *gin.Context) {
|
||||
var f targetTagsForm
|
||||
ginx.BindJSON(c, &f)
|
||||
|
||||
if len(f.Idents) == 0 {
|
||||
ginx.Bomb(http.StatusBadRequest, "idents empty")
|
||||
}
|
||||
|
||||
ginx.NewRender(c).Message(rt.targetBindTags(f))
|
||||
}
|
||||
|
||||
func (rt *Router) targetBindTags(f targetTagsForm) error {
|
||||
for i := 0; i < len(f.Tags); i++ {
|
||||
arr := strings.Split(f.Tags[i], "=")
|
||||
if len(arr) != 2 {
|
||||
return fmt.Errorf("invalid tag(%s)", f.Tags[i])
|
||||
}
|
||||
|
||||
if strings.TrimSpace(arr[0]) == "" || strings.TrimSpace(arr[1]) == "" {
|
||||
return fmt.Errorf("invalid tag(%s)", f.Tags[i])
|
||||
}
|
||||
|
||||
if strings.IndexByte(arr[0], '.') != -1 {
|
||||
return fmt.Errorf("invalid tagkey(%s): cannot contains . ", arr[0])
|
||||
}
|
||||
|
||||
if strings.IndexByte(arr[0], '-') != -1 {
|
||||
return fmt.Errorf("invalid tagkey(%s): cannot contains -", arr[0])
|
||||
}
|
||||
|
||||
if !model.LabelNameRE.MatchString(arr[0]) {
|
||||
return fmt.Errorf("invalid tagkey(%s)", arr[0])
|
||||
}
|
||||
}
|
||||
|
||||
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) targetUnbindTagsByFE(c *gin.Context) {
|
||||
var f targetTagsForm
|
||||
ginx.BindJSON(c, &f)
|
||||
|
||||
if len(f.Idents) == 0 {
|
||||
ginx.Bomb(http.StatusBadRequest, "idents empty")
|
||||
}
|
||||
|
||||
rt.checkTargetPerm(c, f.Idents)
|
||||
|
||||
ginx.NewRender(c).Message(rt.targetUnbindTags(f))
|
||||
}
|
||||
|
||||
func (rt *Router) targetUnbindTagsByService(c *gin.Context) {
|
||||
var f targetTagsForm
|
||||
ginx.BindJSON(c, &f)
|
||||
|
||||
if len(f.Idents) == 0 {
|
||||
ginx.Bomb(http.StatusBadRequest, "idents empty")
|
||||
}
|
||||
|
||||
ginx.NewRender(c).Message(rt.targetUnbindTags(f))
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
err = target.DelTags(rt.Ctx, f.Tags)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type targetNoteForm struct {
|
||||
Idents []string `json:"idents" binding:"required"`
|
||||
Note string `json:"note"`
|
||||
}
|
||||
|
||||
func (rt *Router) targetUpdateNote(c *gin.Context) {
|
||||
var f targetNoteForm
|
||||
ginx.BindJSON(c, &f)
|
||||
|
||||
if len(f.Idents) == 0 {
|
||||
ginx.Bomb(http.StatusBadRequest, "idents empty")
|
||||
}
|
||||
|
||||
rt.checkTargetPerm(c, f.Idents)
|
||||
|
||||
ginx.NewRender(c).Message(models.TargetUpdateNote(rt.Ctx, f.Idents, f.Note))
|
||||
}
|
||||
|
||||
func (rt *Router) targetUpdateNoteByService(c *gin.Context) {
|
||||
var f targetNoteForm
|
||||
ginx.BindJSON(c, &f)
|
||||
|
||||
if len(f.Idents) == 0 {
|
||||
ginx.Bomb(http.StatusBadRequest, "idents empty")
|
||||
}
|
||||
|
||||
ginx.NewRender(c).Message(models.TargetUpdateNote(rt.Ctx, f.Idents, f.Note))
|
||||
}
|
||||
|
||||
type targetBgidForm struct {
|
||||
Idents []string `json:"idents" binding:"required"`
|
||||
Bgid int64 `json:"bgid"`
|
||||
}
|
||||
|
||||
func (rt *Router) targetUpdateBgid(c *gin.Context) {
|
||||
var f targetBgidForm
|
||||
ginx.BindJSON(c, &f)
|
||||
|
||||
if len(f.Idents) == 0 {
|
||||
ginx.Bomb(http.StatusBadRequest, "idents empty")
|
||||
}
|
||||
|
||||
user := c.MustGet("user").(*models.User)
|
||||
if user.IsAdmin() {
|
||||
ginx.NewRender(c).Message(models.TargetUpdateBgid(rt.Ctx, f.Idents, f.Bgid, false))
|
||||
return
|
||||
}
|
||||
|
||||
if f.Bgid > 0 {
|
||||
// 把要操作的机器分成两部分,一部分是bgid为0,需要管理员分配,另一部分bgid>0,说明是业务组内部想调整
|
||||
// 比如原来分配给didiyun的机器,didiyun的管理员想把部分机器调整到didiyun-ceph下
|
||||
// 对于调整的这种情况,当前登录用户要对这批机器有操作权限,同时还要对目标BG有操作权限
|
||||
orphans, err := models.IdentsFilter(rt.Ctx, f.Idents, "group_id = ?", 0)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
// 机器里边存在未归组的,登录用户就需要是admin
|
||||
if len(orphans) > 0 && !user.IsAdmin() {
|
||||
ginx.Bomb(http.StatusForbidden, "No permission. Only admin can assign BG")
|
||||
}
|
||||
|
||||
reBelongs, err := models.IdentsFilter(rt.Ctx, f.Idents, "group_id > ?", 0)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
if len(reBelongs) > 0 {
|
||||
// 对于这些要重新分配的机器,操作者要对这些机器本身有权限,同时要对目标bgid有权限
|
||||
rt.checkTargetPerm(c, f.Idents)
|
||||
|
||||
bg := BusiGroup(rt.Ctx, f.Bgid)
|
||||
can, err := user.CanDoBusiGroup(rt.Ctx, bg, "rw")
|
||||
ginx.Dangerous(err)
|
||||
|
||||
if !can {
|
||||
ginx.Bomb(http.StatusForbidden, "No permission. You are not admin of BG(%s)", bg.Name)
|
||||
}
|
||||
}
|
||||
} else if f.Bgid == 0 {
|
||||
// 退还机器
|
||||
rt.checkTargetPerm(c, f.Idents)
|
||||
} else {
|
||||
ginx.Bomb(http.StatusBadRequest, "invalid bgid")
|
||||
}
|
||||
|
||||
ginx.NewRender(c).Message(models.TargetUpdateBgid(rt.Ctx, f.Idents, f.Bgid, false))
|
||||
}
|
||||
|
||||
type identsForm struct {
|
||||
Idents []string `json:"idents" binding:"required"`
|
||||
}
|
||||
|
||||
func (rt *Router) targetDel(c *gin.Context) {
|
||||
var f identsForm
|
||||
ginx.BindJSON(c, &f)
|
||||
|
||||
if len(f.Idents) == 0 {
|
||||
ginx.Bomb(http.StatusBadRequest, "idents empty")
|
||||
}
|
||||
|
||||
rt.checkTargetPerm(c, f.Idents)
|
||||
|
||||
ginx.NewRender(c).Message(models.TargetDel(rt.Ctx, f.Idents))
|
||||
}
|
||||
|
||||
func (rt *Router) checkTargetPerm(c *gin.Context, idents []string) {
|
||||
user := c.MustGet("user").(*models.User)
|
||||
nopri, err := user.NopriIdents(rt.Ctx, idents)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
if len(nopri) > 0 {
|
||||
ginx.Bomb(http.StatusForbidden, "No permission to operate the targets: %s", strings.Join(nopri, ", "))
|
||||
}
|
||||
}
|
||||
216
center/router/router_task.go
Normal file
216
center/router/router_task.go
Normal file
@@ -0,0 +1,216 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/ccfos/nightingale/v6/models"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/toolkits/pkg/ginx"
|
||||
"github.com/toolkits/pkg/str"
|
||||
)
|
||||
|
||||
func (rt *Router) taskGets(c *gin.Context) {
|
||||
bgid := ginx.UrlParamInt64(c, "id")
|
||||
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, bgid, beginTime, creator, query)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
list, err := models.TaskRecordGets(rt.Ctx, bgid, beginTime, creator, query, limit, ginx.Offset(c, limit))
|
||||
ginx.Dangerous(err)
|
||||
|
||||
ginx.NewRender(c).Data(gin.H{
|
||||
"total": total,
|
||||
"list": list,
|
||||
}, nil)
|
||||
}
|
||||
|
||||
type taskForm struct {
|
||||
Title string `json:"title" binding:"required"`
|
||||
Account string `json:"account" binding:"required"`
|
||||
Batch int `json:"batch"`
|
||||
Tolerance int `json:"tolerance"`
|
||||
Timeout int `json:"timeout"`
|
||||
Pause string `json:"pause"`
|
||||
Script string `json:"script" binding:"required"`
|
||||
Args string `json:"args"`
|
||||
Action string `json:"action" binding:"required"`
|
||||
Creator string `json:"creator"`
|
||||
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)
|
||||
ginx.NewRender(c).Message(f.Add(rt.Ctx))
|
||||
}
|
||||
|
||||
func (rt *Router) taskAdd(c *gin.Context) {
|
||||
var f taskForm
|
||||
ginx.BindJSON(c, &f)
|
||||
|
||||
bgid := ginx.UrlParamInt64(c, "id")
|
||||
user := c.MustGet("user").(*models.User)
|
||||
f.Creator = user.Username
|
||||
|
||||
err := f.Verify()
|
||||
ginx.Dangerous(err)
|
||||
|
||||
f.HandleFH(f.Hosts[0])
|
||||
|
||||
// check permission
|
||||
rt.checkTargetPerm(c, f.Hosts)
|
||||
|
||||
// call ibex
|
||||
taskId, err := TaskCreate(f, rt.NotifyConfigCache.GetIbex())
|
||||
ginx.Dangerous(err)
|
||||
|
||||
if taskId <= 0 {
|
||||
ginx.Dangerous("created task.id is zero")
|
||||
}
|
||||
|
||||
// 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,
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
227
center/router/router_task_tpl.go
Normal file
227
center/router/router_task_tpl.go
Normal file
@@ -0,0 +1,227 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/ccfos/nightingale/v6/models"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/toolkits/pkg/ginx"
|
||||
"github.com/toolkits/pkg/str"
|
||||
)
|
||||
|
||||
func (rt *Router) taskTplGets(c *gin.Context) {
|
||||
query := ginx.QueryStr(c, "query", "")
|
||||
limit := ginx.QueryInt(c, "limit", 20)
|
||||
groupId := ginx.UrlParamInt64(c, "id")
|
||||
|
||||
total, err := models.TaskTplTotal(rt.Ctx, groupId, query)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
list, err := models.TaskTplGets(rt.Ctx, groupId, query, limit, ginx.Offset(c, limit))
|
||||
ginx.Dangerous(err)
|
||||
|
||||
ginx.NewRender(c).Data(gin.H{
|
||||
"total": total,
|
||||
"list": list,
|
||||
}, nil)
|
||||
}
|
||||
|
||||
func (rt *Router) taskTplGet(c *gin.Context) {
|
||||
tid := ginx.UrlParamInt64(c, "tid")
|
||||
|
||||
tpl, err := models.TaskTplGet(rt.Ctx, "id = ?", tid)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
if tpl == nil {
|
||||
ginx.Bomb(404, "no such task template")
|
||||
}
|
||||
|
||||
hosts, err := tpl.Hosts(rt.Ctx)
|
||||
|
||||
ginx.NewRender(c).Data(gin.H{
|
||||
"tpl": tpl,
|
||||
"hosts": hosts,
|
||||
}, 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)
|
||||
}
|
||||
|
||||
type taskTplForm struct {
|
||||
Title string `json:"title" binding:"required"`
|
||||
Batch int `json:"batch"`
|
||||
Tolerance int `json:"tolerance"`
|
||||
Timeout int `json:"timeout"`
|
||||
Pause string `json:"pause"`
|
||||
Script string `json:"script"`
|
||||
Args string `json:"args"`
|
||||
Tags []string `json:"tags"`
|
||||
Account string `json:"account"`
|
||||
Hosts []string `json:"hosts"`
|
||||
}
|
||||
|
||||
func (rt *Router) taskTplAdd(c *gin.Context) {
|
||||
var f taskTplForm
|
||||
ginx.BindJSON(c, &f)
|
||||
|
||||
user := c.MustGet("user").(*models.User)
|
||||
now := time.Now().Unix()
|
||||
|
||||
sort.Strings(f.Tags)
|
||||
|
||||
tpl := &models.TaskTpl{
|
||||
GroupId: ginx.UrlParamInt64(c, "id"),
|
||||
Title: f.Title,
|
||||
Batch: f.Batch,
|
||||
Tolerance: f.Tolerance,
|
||||
Timeout: f.Timeout,
|
||||
Pause: f.Pause,
|
||||
Script: f.Script,
|
||||
Args: f.Args,
|
||||
Tags: strings.Join(f.Tags, " ") + " ",
|
||||
Account: f.Account,
|
||||
CreateBy: user.Username,
|
||||
UpdateBy: user.Username,
|
||||
CreateAt: now,
|
||||
UpdateAt: now,
|
||||
}
|
||||
|
||||
ginx.NewRender(c).Message(tpl.Save(rt.Ctx, f.Hosts))
|
||||
}
|
||||
|
||||
func (rt *Router) taskTplPut(c *gin.Context) {
|
||||
tid := ginx.UrlParamInt64(c, "tid")
|
||||
|
||||
tpl, err := models.TaskTplGet(rt.Ctx, "id = ?", tid)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
if tpl == nil {
|
||||
ginx.NewRender(c).Message("no such task template")
|
||||
return
|
||||
}
|
||||
|
||||
user := c.MustGet("user").(*models.User)
|
||||
|
||||
var f taskTplForm
|
||||
ginx.BindJSON(c, &f)
|
||||
|
||||
sort.Strings(f.Tags)
|
||||
|
||||
tpl.Title = f.Title
|
||||
tpl.Batch = f.Batch
|
||||
tpl.Tolerance = f.Tolerance
|
||||
tpl.Timeout = f.Timeout
|
||||
tpl.Pause = f.Pause
|
||||
tpl.Script = f.Script
|
||||
tpl.Args = f.Args
|
||||
tpl.Tags = strings.Join(f.Tags, " ") + " "
|
||||
tpl.Account = f.Account
|
||||
tpl.UpdateBy = user.Username
|
||||
tpl.UpdateAt = time.Now().Unix()
|
||||
|
||||
ginx.NewRender(c).Message(tpl.Update(rt.Ctx, f.Hosts))
|
||||
}
|
||||
|
||||
func (rt *Router) taskTplDel(c *gin.Context) {
|
||||
tid := ginx.UrlParamInt64(c, "tid")
|
||||
|
||||
tpl, err := models.TaskTplGet(rt.Ctx, "id = ?", tid)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
if tpl == nil {
|
||||
ginx.NewRender(c).Message(nil)
|
||||
return
|
||||
}
|
||||
|
||||
ginx.NewRender(c).Message(tpl.Del(rt.Ctx))
|
||||
}
|
||||
|
||||
type tplTagsForm struct {
|
||||
Ids []int64 `json:"ids" binding:"required"`
|
||||
Tags []string `json:"tags" binding:"required"`
|
||||
}
|
||||
|
||||
func (f *tplTagsForm) Verify() {
|
||||
if len(f.Ids) == 0 {
|
||||
ginx.Bomb(http.StatusBadRequest, "arg(ids) empty")
|
||||
}
|
||||
|
||||
if len(f.Tags) == 0 {
|
||||
ginx.Bomb(http.StatusBadRequest, "arg(tags) empty")
|
||||
}
|
||||
|
||||
newTags := make([]string, 0, len(f.Tags))
|
||||
for i := 0; i < len(f.Tags); i++ {
|
||||
tag := strings.TrimSpace(f.Tags[i])
|
||||
if tag == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
if str.Dangerous(tag) {
|
||||
ginx.Bomb(http.StatusBadRequest, "arg(tags) invalid")
|
||||
}
|
||||
|
||||
newTags = append(newTags, tag)
|
||||
}
|
||||
|
||||
f.Tags = newTags
|
||||
if len(f.Tags) == 0 {
|
||||
ginx.Bomb(http.StatusBadRequest, "arg(tags) empty")
|
||||
}
|
||||
}
|
||||
|
||||
func (rt *Router) taskTplBindTags(c *gin.Context) {
|
||||
var f tplTagsForm
|
||||
ginx.BindJSON(c, &f)
|
||||
f.Verify()
|
||||
|
||||
username := c.MustGet("username").(string)
|
||||
|
||||
for i := 0; i < len(f.Ids); i++ {
|
||||
tpl, err := models.TaskTplGet(rt.Ctx, "id = ?", f.Ids[i])
|
||||
ginx.Dangerous(err)
|
||||
|
||||
if tpl == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
ginx.Dangerous(tpl.AddTags(rt.Ctx, f.Tags, username))
|
||||
}
|
||||
|
||||
ginx.NewRender(c).Message(nil)
|
||||
}
|
||||
|
||||
func (rt *Router) taskTplUnbindTags(c *gin.Context) {
|
||||
var f tplTagsForm
|
||||
ginx.BindJSON(c, &f)
|
||||
f.Verify()
|
||||
|
||||
username := c.MustGet("username").(string)
|
||||
|
||||
for i := 0; i < len(f.Ids); i++ {
|
||||
tpl, err := models.TaskTplGet(rt.Ctx, "id = ?", f.Ids[i])
|
||||
ginx.Dangerous(err)
|
||||
|
||||
if tpl == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
ginx.Dangerous(tpl.DelTags(rt.Ctx, f.Tags, username))
|
||||
}
|
||||
|
||||
ginx.NewRender(c).Message(nil)
|
||||
}
|
||||
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)
|
||||
}
|
||||
137
center/router/router_user.go
Normal file
137
center/router/router_user.go
Normal file
@@ -0,0 +1,137 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/ccfos/nightingale/v6/models"
|
||||
"github.com/ccfos/nightingale/v6/pkg/ormx"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/toolkits/pkg/ginx"
|
||||
)
|
||||
|
||||
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) {
|
||||
limit := ginx.QueryInt(c, "limit", 20)
|
||||
query := ginx.QueryStr(c, "query", "")
|
||||
|
||||
total, err := models.UserTotal(rt.Ctx, query)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
list, err := models.UserGets(rt.Ctx, query, limit, ginx.Offset(c, limit))
|
||||
ginx.Dangerous(err)
|
||||
|
||||
user := c.MustGet("user").(*models.User)
|
||||
|
||||
ginx.NewRender(c).Data(gin.H{
|
||||
"list": list,
|
||||
"total": total,
|
||||
"admin": user.IsAdmin(),
|
||||
}, nil)
|
||||
}
|
||||
|
||||
type userAddForm struct {
|
||||
Username string `json:"username" binding:"required"`
|
||||
Password string `json:"password" binding:"required"`
|
||||
Nickname string `json:"nickname"`
|
||||
Phone string `json:"phone"`
|
||||
Email string `json:"email"`
|
||||
Portrait string `json:"portrait"`
|
||||
Roles []string `json:"roles" binding:"required"`
|
||||
Contacts ormx.JSONObj `json:"contacts"`
|
||||
}
|
||||
|
||||
func (rt *Router) userAddPost(c *gin.Context) {
|
||||
var f userAddForm
|
||||
ginx.BindJSON(c, &f)
|
||||
|
||||
password, err := models.CryptoPass(rt.Ctx, f.Password)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
if len(f.Roles) == 0 {
|
||||
ginx.Bomb(http.StatusBadRequest, "roles empty")
|
||||
}
|
||||
|
||||
user := c.MustGet("user").(*models.User)
|
||||
|
||||
u := models.User{
|
||||
Username: f.Username,
|
||||
Password: password,
|
||||
Nickname: f.Nickname,
|
||||
Phone: f.Phone,
|
||||
Email: f.Email,
|
||||
Portrait: f.Portrait,
|
||||
Roles: strings.Join(f.Roles, " "),
|
||||
Contacts: f.Contacts,
|
||||
CreateBy: user.Username,
|
||||
UpdateBy: user.Username,
|
||||
}
|
||||
|
||||
ginx.NewRender(c).Message(u.Add(rt.Ctx))
|
||||
}
|
||||
|
||||
func (rt *Router) userProfileGet(c *gin.Context) {
|
||||
user := User(rt.Ctx, ginx.UrlParamInt64(c, "id"))
|
||||
ginx.NewRender(c).Data(user, nil)
|
||||
}
|
||||
|
||||
type userProfileForm struct {
|
||||
Nickname string `json:"nickname"`
|
||||
Phone string `json:"phone"`
|
||||
Email string `json:"email"`
|
||||
Roles []string `json:"roles"`
|
||||
Contacts ormx.JSONObj `json:"contacts"`
|
||||
}
|
||||
|
||||
func (rt *Router) userProfilePut(c *gin.Context) {
|
||||
var f userProfileForm
|
||||
ginx.BindJSON(c, &f)
|
||||
|
||||
if len(f.Roles) == 0 {
|
||||
ginx.Bomb(http.StatusBadRequest, "roles empty")
|
||||
}
|
||||
|
||||
target := User(rt.Ctx, ginx.UrlParamInt64(c, "id"))
|
||||
target.Nickname = f.Nickname
|
||||
target.Phone = f.Phone
|
||||
target.Email = f.Email
|
||||
target.Roles = strings.Join(f.Roles, " ")
|
||||
target.Contacts = f.Contacts
|
||||
target.UpdateBy = c.MustGet("username").(string)
|
||||
|
||||
ginx.NewRender(c).Message(target.UpdateAllFields(rt.Ctx))
|
||||
}
|
||||
|
||||
type userPasswordForm struct {
|
||||
Password string `json:"password" binding:"required"`
|
||||
}
|
||||
|
||||
func (rt *Router) userPasswordPut(c *gin.Context) {
|
||||
var f userPasswordForm
|
||||
ginx.BindJSON(c, &f)
|
||||
|
||||
target := User(rt.Ctx, ginx.UrlParamInt64(c, "id"))
|
||||
|
||||
cryptoPass, err := models.CryptoPass(rt.Ctx, f.Password)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
ginx.NewRender(c).Message(target.UpdatePassword(rt.Ctx, cryptoPass, c.MustGet("username").(string)))
|
||||
}
|
||||
|
||||
func (rt *Router) userDel(c *gin.Context) {
|
||||
id := ginx.UrlParamInt64(c, "id")
|
||||
target, err := models.UserGetById(rt.Ctx, id)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
if target == nil {
|
||||
ginx.NewRender(c).Message(nil)
|
||||
return
|
||||
}
|
||||
|
||||
ginx.NewRender(c).Message(target.Del(rt.Ctx))
|
||||
}
|
||||
150
center/router/router_user_group.go
Normal file
150
center/router/router_user_group.go
Normal file
@@ -0,0 +1,150 @@
|
||||
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/logger"
|
||||
)
|
||||
|
||||
func (rt *Router) checkBusiGroupPerm(c *gin.Context) {
|
||||
me := c.MustGet("user").(*models.User)
|
||||
bg := BusiGroup(rt.Ctx, ginx.UrlParamInt64(c, "id"))
|
||||
|
||||
can, err := me.CanDoBusiGroup(rt.Ctx, bg, ginx.UrlParamStr(c, "perm"))
|
||||
ginx.NewRender(c).Data(can, err)
|
||||
}
|
||||
|
||||
func (rt *Router) userGroupGets(c *gin.Context) {
|
||||
limit := ginx.QueryInt(c, "limit", 1500)
|
||||
query := ginx.QueryStr(c, "query", "")
|
||||
|
||||
me := c.MustGet("user").(*models.User)
|
||||
lst, err := me.UserGroups(rt.Ctx, limit, query)
|
||||
|
||||
ginx.NewRender(c).Data(lst, err)
|
||||
}
|
||||
|
||||
func (rt *Router) userGroupGetsByService(c *gin.Context) {
|
||||
lst, err := models.UserGroupGetAll(rt.Ctx)
|
||||
ginx.NewRender(c).Data(lst, err)
|
||||
}
|
||||
|
||||
// user group member get by service
|
||||
func (rt *Router) userGroupMemberGetsByService(c *gin.Context) {
|
||||
members, err := models.UserGroupMemberGetAll(rt.Ctx)
|
||||
ginx.NewRender(c).Data(members, err)
|
||||
}
|
||||
|
||||
type userGroupForm struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
Note string `json:"note"`
|
||||
}
|
||||
|
||||
func (rt *Router) userGroupAdd(c *gin.Context) {
|
||||
var f userGroupForm
|
||||
ginx.BindJSON(c, &f)
|
||||
|
||||
me := c.MustGet("user").(*models.User)
|
||||
|
||||
ug := models.UserGroup{
|
||||
Name: f.Name,
|
||||
Note: f.Note,
|
||||
CreateBy: me.Username,
|
||||
UpdateBy: me.Username,
|
||||
}
|
||||
|
||||
err := ug.Add(rt.Ctx)
|
||||
if err == nil {
|
||||
// Even failure is not a big deal
|
||||
models.UserGroupMemberAdd(rt.Ctx, ug.Id, me.Id)
|
||||
}
|
||||
|
||||
ginx.NewRender(c).Data(ug.Id, err)
|
||||
}
|
||||
|
||||
func (rt *Router) userGroupPut(c *gin.Context) {
|
||||
var f userGroupForm
|
||||
ginx.BindJSON(c, &f)
|
||||
|
||||
me := c.MustGet("user").(*models.User)
|
||||
ug := c.MustGet("user_group").(*models.UserGroup)
|
||||
|
||||
if ug.Name != f.Name {
|
||||
// name changed, check duplication
|
||||
num, err := models.UserGroupCount(rt.Ctx, "name=? and id<>?", f.Name, ug.Id)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
if num > 0 {
|
||||
ginx.Bomb(http.StatusOK, "UserGroup already exists")
|
||||
}
|
||||
}
|
||||
|
||||
ug.Name = f.Name
|
||||
ug.Note = f.Note
|
||||
ug.UpdateBy = me.Username
|
||||
ug.UpdateAt = time.Now().Unix()
|
||||
|
||||
ginx.NewRender(c).Message(ug.Update(rt.Ctx, "Name", "Note", "UpdateAt", "UpdateBy"))
|
||||
}
|
||||
|
||||
// Return all members, front-end search and paging
|
||||
func (rt *Router) userGroupGet(c *gin.Context) {
|
||||
ug := UserGroup(rt.Ctx, ginx.UrlParamInt64(c, "id"))
|
||||
|
||||
ids, err := models.MemberIds(rt.Ctx, ug.Id)
|
||||
ginx.Dangerous(err)
|
||||
|
||||
logger.Info("userGroupGet", ids)
|
||||
users, err := models.UserGetsByIds(rt.Ctx, ids)
|
||||
|
||||
ginx.NewRender(c).Data(gin.H{
|
||||
"users": users,
|
||||
"user_group": ug,
|
||||
}, err)
|
||||
}
|
||||
|
||||
func (rt *Router) userGroupDel(c *gin.Context) {
|
||||
ug := c.MustGet("user_group").(*models.UserGroup)
|
||||
ginx.NewRender(c).Message(ug.Del(rt.Ctx))
|
||||
}
|
||||
|
||||
func (rt *Router) userGroupMemberAdd(c *gin.Context) {
|
||||
var f idsForm
|
||||
ginx.BindJSON(c, &f)
|
||||
f.Verify()
|
||||
|
||||
me := c.MustGet("user").(*models.User)
|
||||
ug := c.MustGet("user_group").(*models.UserGroup)
|
||||
|
||||
err := ug.AddMembers(rt.Ctx, f.Ids)
|
||||
if err == nil {
|
||||
ug.UpdateAt = time.Now().Unix()
|
||||
ug.UpdateBy = me.Username
|
||||
ug.Update(rt.Ctx, "UpdateAt", "UpdateBy")
|
||||
}
|
||||
|
||||
ginx.NewRender(c).Message(err)
|
||||
}
|
||||
|
||||
func (rt *Router) userGroupMemberDel(c *gin.Context) {
|
||||
var f idsForm
|
||||
ginx.BindJSON(c, &f)
|
||||
f.Verify()
|
||||
|
||||
me := c.MustGet("user").(*models.User)
|
||||
ug := c.MustGet("user_group").(*models.UserGroup)
|
||||
|
||||
err := ug.DelMembers(rt.Ctx, f.Ids)
|
||||
if err == nil {
|
||||
ug.UpdateAt = time.Now().Unix()
|
||||
ug.UpdateBy = me.Username
|
||||
ug.Update(rt.Ctx, "UpdateAt", "UpdateBy")
|
||||
}
|
||||
|
||||
ginx.NewRender(c).Message(err)
|
||||
}
|
||||
171
center/sso/init.go
Normal file
171
center/sso/init.go
Normal file
@@ -0,0 +1,171 @@
|
||||
package sso
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"github.com/BurntSushi/toml"
|
||||
"github.com/ccfos/nightingale/v6/center/cconf"
|
||||
"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/toolkits/pkg/logger"
|
||||
)
|
||||
|
||||
type SsoClient struct {
|
||||
OIDC *oidcx.SsoClient
|
||||
LDAP *ldapx.SsoClient
|
||||
CAS *cas.SsoClient
|
||||
OAuth2 *oauth2x.SsoClient
|
||||
}
|
||||
|
||||
const LDAP = `
|
||||
Enable = false
|
||||
Host = 'ldap.example.org'
|
||||
Port = 389
|
||||
BaseDn = 'dc=example,dc=org'
|
||||
BindUser = 'cn=manager,dc=example,dc=org'
|
||||
BindPass = '*******'
|
||||
# openldap format e.g. (&(uid=%s))
|
||||
# AD format e.g. (&(sAMAccountName=%s))
|
||||
AuthFilter = '(&(uid=%s))'
|
||||
CoverAttributes = true
|
||||
TLS = false
|
||||
StartTLS = true
|
||||
DefaultRoles = ['Standard']
|
||||
|
||||
[Attributes]
|
||||
Nickname = 'cn'
|
||||
Phone = 'mobile'
|
||||
Email = 'mail'
|
||||
`
|
||||
|
||||
const OAuth2 = `
|
||||
Enable = false
|
||||
DisplayName = 'OAuth2登录'
|
||||
RedirectURL = 'http://127.0.0.1:18000/callback/oauth'
|
||||
SsoAddr = 'https://sso.example.com/oauth2/authorize'
|
||||
TokenAddr = 'https://sso.example.com/oauth2/token'
|
||||
UserInfoAddr = 'https://api.example.com/api/v1/user/info'
|
||||
TranTokenMethod = 'header'
|
||||
ClientId = ''
|
||||
ClientSecret = ''
|
||||
CoverAttributes = true
|
||||
DefaultRoles = ['Standard']
|
||||
UserinfoIsArray = false
|
||||
UserinfoPrefix = 'data'
|
||||
Scopes = ['profile', 'email', 'phone']
|
||||
|
||||
[Attributes]
|
||||
Username = 'username'
|
||||
Nickname = 'nickname'
|
||||
Phone = 'phone_number'
|
||||
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
|
||||
DefaultRoles = ['Standard']
|
||||
|
||||
[Attributes]
|
||||
Nickname = 'nickname'
|
||||
Phone = 'phone_number'
|
||||
Email = 'email'
|
||||
`
|
||||
const OIDC = `
|
||||
Enable = false
|
||||
DisplayName = 'OIDC登录'
|
||||
RedirectURL = 'http://n9e.com/callback'
|
||||
SsoAddr = 'http://sso.example.org'
|
||||
ClientId = ''
|
||||
ClientSecret = ''
|
||||
CoverAttributes = true
|
||||
DefaultRoles = ['Standard']
|
||||
|
||||
[Attributes]
|
||||
Nickname = 'nickname'
|
||||
Phone = 'phone_number'
|
||||
Email = 'email'
|
||||
`
|
||||
|
||||
func Init(center cconf.Center, ctx *ctx.Context) *SsoClient {
|
||||
ssoClient := new(SsoClient)
|
||||
m := make(map[string]string)
|
||||
m["LDAP"] = LDAP
|
||||
m["CAS"] = CAS
|
||||
m["OIDC"] = OIDC
|
||||
m["OAuth2"] = OAuth2
|
||||
|
||||
for name, config := range m {
|
||||
count, err := models.SsoConfigCountByName(ctx, name)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
continue
|
||||
}
|
||||
|
||||
if count > 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
ssoConfig := models.SsoConfig{
|
||||
Name: name,
|
||||
Content: config,
|
||||
}
|
||||
|
||||
err = ssoConfig.Create(ctx)
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
}
|
||||
|
||||
configs, err := models.SsoConfigGets(ctx)
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
|
||||
for _, cfg := range configs {
|
||||
switch cfg.Name {
|
||||
case "LDAP":
|
||||
var config ldapx.Config
|
||||
err := toml.Unmarshal([]byte(cfg.Content), &config)
|
||||
if err != nil {
|
||||
log.Fatalln("init ldap failed", err)
|
||||
}
|
||||
ssoClient.LDAP = ldapx.New(config)
|
||||
case "OIDC":
|
||||
var config oidcx.Config
|
||||
err := toml.Unmarshal([]byte(cfg.Content), &config)
|
||||
if err != nil {
|
||||
log.Fatalln("init oidc failed:", err)
|
||||
}
|
||||
oidcClient, err := oidcx.New(config)
|
||||
if err != nil {
|
||||
logger.Error("init oidc failed:", err)
|
||||
} else {
|
||||
ssoClient.OIDC = oidcClient
|
||||
}
|
||||
case "CAS":
|
||||
var config cas.Config
|
||||
err := toml.Unmarshal([]byte(cfg.Content), &config)
|
||||
if err != nil {
|
||||
log.Fatalln("init cas failed:", err)
|
||||
}
|
||||
ssoClient.CAS = cas.New(config)
|
||||
case "OAuth2":
|
||||
var config oauth2x.Config
|
||||
err := toml.Unmarshal([]byte(cfg.Content), &config)
|
||||
if err != nil {
|
||||
log.Fatalln("init oauth2 failed:", err)
|
||||
}
|
||||
ssoClient.OAuth2 = oauth2x.New(config)
|
||||
}
|
||||
}
|
||||
return ssoClient
|
||||
}
|
||||
9
cli/cli.go
Normal file
9
cli/cli.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"github.com/ccfos/nightingale/v6/cli/upgrade"
|
||||
)
|
||||
|
||||
func Upgrade(configFile string) error {
|
||||
return upgrade.Upgrade(configFile)
|
||||
}
|
||||
63
cli/upgrade/config.go
Normal file
63
cli/upgrade/config.go
Normal file
@@ -0,0 +1,63 @@
|
||||
package upgrade
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"path"
|
||||
|
||||
"github.com/ccfos/nightingale/v6/pkg/cfg"
|
||||
"github.com/ccfos/nightingale/v6/pkg/ormx"
|
||||
"github.com/ccfos/nightingale/v6/pkg/tlsx"
|
||||
"github.com/koding/multiconfig"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
DB ormx.DBConfig
|
||||
Clusters []ClusterOptions
|
||||
}
|
||||
|
||||
type ClusterOptions struct {
|
||||
Name string
|
||||
Prom string
|
||||
|
||||
BasicAuthUser string
|
||||
BasicAuthPass string
|
||||
|
||||
Headers []string
|
||||
|
||||
Timeout int64
|
||||
DialTimeout int64
|
||||
|
||||
UseTLS bool
|
||||
tlsx.ClientConfig
|
||||
|
||||
MaxIdleConnsPerHost int
|
||||
}
|
||||
|
||||
func Parse(fpath string, configPtr interface{}) error {
|
||||
var (
|
||||
tBuf []byte
|
||||
)
|
||||
loaders := []multiconfig.Loader{
|
||||
&multiconfig.TagLoader{},
|
||||
&multiconfig.EnvironmentLoader{},
|
||||
}
|
||||
s := cfg.NewFileScanner()
|
||||
|
||||
s.Read(path.Join(fpath))
|
||||
tBuf = append(tBuf, s.Data()...)
|
||||
tBuf = append(tBuf, []byte("\n")...)
|
||||
|
||||
if s.Err() != nil {
|
||||
return s.Err()
|
||||
}
|
||||
|
||||
if len(tBuf) != 0 {
|
||||
loaders = append(loaders, &multiconfig.TOMLLoader{Reader: bytes.NewReader(tBuf)})
|
||||
}
|
||||
|
||||
m := multiconfig.DefaultLoader{
|
||||
Loader: multiconfig.MultiLoader(loaders...),
|
||||
Validator: multiconfig.MultiValidator(&multiconfig.RequiredValidator{}),
|
||||
}
|
||||
return m.Load(configPtr)
|
||||
}
|
||||
21
cli/upgrade/readme.md
Normal file
21
cli/upgrade/readme.md
Normal file
@@ -0,0 +1,21 @@
|
||||
# v5 升级 v6 手册
|
||||
0. 操作之前,记得备注下数据库!
|
||||
|
||||
1. 需要先将你正在使用的夜莺数据源表结构更新到和 v5.15.0 一致,[release](https://github.com/ccfos/nightingale/releases) 页面有每个版本表结构的更新说明,可以根据你正在使用的版本,按照说明,逐个执行的更新表结构的语句
|
||||
|
||||
2. 解压 n9e 安装包,导入 upgrade.sql 到 n9e_v5 数据库
|
||||
```
|
||||
mysql -h 127.0.0.1 -u root -p1234 < cli/upgrade/upgrade.sql
|
||||
```
|
||||
|
||||
3. 执行 n9e-cli 完成数据库表结构升级, webapi.conf 为 v5 版本 n9e-webapi 正在使用的配置文件
|
||||
```
|
||||
./n9e-cli --upgrade --config webapi.conf
|
||||
```
|
||||
|
||||
4. 修改 n9e 配置文件中的数据库为 n9e_v5,启动 n9e 进程
|
||||
```
|
||||
nohup ./n9e &> n9e.log &
|
||||
```
|
||||
|
||||
5. n9e 监听的端口为 17000,需要将之前的 web 端口和数据上报的端口,都调整为 17000
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user