Compare commits
765 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f8b4458685 | ||
|
|
1e59b3211f | ||
|
|
87d356348b | ||
|
|
e78f3ac8ac | ||
|
|
ec03dec72d | ||
|
|
620b605786 | ||
|
|
20bb5e703c | ||
|
|
e3a10b327c | ||
|
|
2ae3a9a8a3 | ||
|
|
88605a7ea2 | ||
|
|
5d9b9b88b9 | ||
|
|
39aabdbadc | ||
|
|
563d76dedb | ||
|
|
27f87d4797 | ||
|
|
06a8a981c3 | ||
|
|
c0808a4146 | ||
|
|
db781a9342 | ||
|
|
e5868b9c29 | ||
|
|
65afe3b141 | ||
|
|
bba5e62911 | ||
|
|
220d193244 | ||
|
|
6e49ed4af0 | ||
|
|
91b9b5a808 | ||
|
|
7e7d8abc4a | ||
|
|
d63bb52c0f | ||
|
|
09c6c7350b | ||
|
|
e6acd16daf | ||
|
|
11869a8307 | ||
|
|
11ae1ae924 | ||
|
|
8ae9825bb4 | ||
|
|
7bfb5efaef | ||
|
|
f30044cd5c | ||
|
|
54ec080bbc | ||
|
|
3eef1ddc7d | ||
|
|
370024c7ed | ||
|
|
e03e46af4e | ||
|
|
fb6eab03a2 | ||
|
|
918ed5cb32 | ||
|
|
894416b4ca | ||
|
|
0fa7effc4b | ||
|
|
565bd08c43 | ||
|
|
77e9992fee | ||
|
|
ef1afeed6c | ||
|
|
8b21f40217 | ||
|
|
74bb9ea734 | ||
|
|
227d5182af | ||
|
|
7717967d42 | ||
|
|
702aa4948b | ||
|
|
df088dd78a | ||
|
|
9576bd875a | ||
|
|
7a0e1e252f | ||
|
|
470cd639c6 | ||
|
|
3f8ab2e4be | ||
|
|
ce8d28f8f4 | ||
|
|
0a4aadffac | ||
|
|
c84a8b34cc | ||
|
|
7da4068f48 | ||
|
|
e8fdb27625 | ||
|
|
59877d9f32 | ||
|
|
e757ebc58b | ||
|
|
9fe2e4e2c2 | ||
|
|
0d79c8cbef | ||
|
|
7e99bbb967 | ||
|
|
8bf3fb917a | ||
|
|
a16f1ae565 | ||
|
|
af5bdb9254 | ||
|
|
0d47c23a03 | ||
|
|
3f49bdaeff | ||
|
|
d128a5bf99 | ||
|
|
fbac1a9dad | ||
|
|
62b46007c5 | ||
|
|
acbea6c1ee | ||
|
|
205d34eae6 | ||
|
|
0017814ad4 | ||
|
|
58ca52faf8 | ||
|
|
3e91e15d1c | ||
|
|
87b393fb91 | ||
|
|
df5b0067ca | ||
|
|
f2b711b976 | ||
|
|
b9381ccf8b | ||
|
|
6d277396c3 | ||
|
|
b0c8337618 | ||
|
|
ec86f2289c | ||
|
|
0a38542a45 | ||
|
|
75f3db1f5c | ||
|
|
1685e181ae | ||
|
|
f72b35665f | ||
|
|
ed12c60826 | ||
|
|
5d45ea1003 | ||
|
|
5808774e06 | ||
|
|
5a7f9d1cf4 | ||
|
|
69d1893f4c | ||
|
|
f620f159a5 | ||
|
|
14799e69d7 | ||
|
|
6f34934b41 | ||
|
|
909712846d | ||
|
|
eb4fb60ee1 | ||
|
|
979d89f5dd | ||
|
|
e5ebdb9b1a | ||
|
|
b6ed9afd6d | ||
|
|
affaf373ea | ||
|
|
e93e168bdc | ||
|
|
7cd371f08f | ||
|
|
ea86716d06 | ||
|
|
debe75f51c | ||
|
|
3ac3124eed | ||
|
|
10590bde47 | ||
|
|
2b87b4d183 | ||
|
|
71ef3155c8 | ||
|
|
c3affb0c4f | ||
|
|
3c3805865b | ||
|
|
3d19fa6932 | ||
|
|
9dd191b27c | ||
|
|
5366d9be73 | ||
|
|
6ff71474a6 | ||
|
|
b71be42d90 | ||
|
|
21c92d7ef1 | ||
|
|
88a2659f1a | ||
|
|
445edcc6ac | ||
|
|
5748139aa4 | ||
|
|
424121c126 | ||
|
|
55facde841 | ||
|
|
ee5da826e9 | ||
|
|
b3e1119592 | ||
|
|
2efa46a11c | ||
|
|
64720d3c03 | ||
|
|
6f685c8e43 | ||
|
|
d91c1d4eee | ||
|
|
0f9e107e36 | ||
|
|
ce5082912b | ||
|
|
ad6bdd78d0 | ||
|
|
e29b2b8444 | ||
|
|
0d3e00e512 | ||
|
|
ac502785b6 | ||
|
|
1215f51043 | ||
|
|
3d890e89f1 | ||
|
|
75e84144c7 | ||
|
|
1d7c877b7b | ||
|
|
93c2db5546 | ||
|
|
578a37aa14 | ||
|
|
c90c1c4d54 | ||
|
|
d924f4b7ba | ||
|
|
f3c1c2e2ec | ||
|
|
f10c38b827 | ||
|
|
96dce63dbd | ||
|
|
b1f94f7f0e | ||
|
|
e08b74fcd6 | ||
|
|
33fd30ff61 | ||
|
|
a56b77db5b | ||
|
|
d8ffbf55a2 | ||
|
|
ea153e5f90 | ||
|
|
cf1a8bce6b | ||
|
|
08428464e9 | ||
|
|
e3adcbec6e | ||
|
|
3cb72ccc2a | ||
|
|
4e7f7f3302 | ||
|
|
f9a17cb5fe | ||
|
|
a9bb22b213 | ||
|
|
8f2d03fdc7 | ||
|
|
480e40b344 | ||
|
|
4e722c459b | ||
|
|
db8c4054e5 | ||
|
|
4507b111a9 | ||
|
|
2455a988e4 | ||
|
|
af77f449da | ||
|
|
c1997889f8 | ||
|
|
a6a2c5324a | ||
|
|
fb5614ab5c | ||
|
|
a1b494ac91 | ||
|
|
481ce692c7 | ||
|
|
107b637aef | ||
|
|
a6e66b1f6f | ||
|
|
26e85e642b | ||
|
|
42fe13995e | ||
|
|
5ea197f300 | ||
|
|
0028b2c6d1 | ||
|
|
a8acad7453 | ||
|
|
e855b202df | ||
|
|
3e783aa2a1 | ||
|
|
9bb60ab00f | ||
|
|
a19e7f8c5b | ||
|
|
de26d1ff23 | ||
|
|
d0f785defd | ||
|
|
46bd2c4d6d | ||
|
|
e86b7cc9a5 | ||
|
|
c3d02ee75a | ||
|
|
cde4664f0d | ||
|
|
21bd204e81 | ||
|
|
8b36044c93 | ||
|
|
baab622db6 | ||
|
|
cf3a041c2f | ||
|
|
865f09ecbb | ||
|
|
ba1b3b8ef2 | ||
|
|
b5b3c585b3 | ||
|
|
2968779f16 | ||
|
|
96b7de6736 | ||
|
|
4b850c2a59 | ||
|
|
4ef32df4fa | ||
|
|
6530bcedec | ||
|
|
a6587ded51 | ||
|
|
55e3bbd4cc | ||
|
|
f57982eddc | ||
|
|
5da71eb685 | ||
|
|
2016a2c899 | ||
|
|
d4b09896fa | ||
|
|
9c62b25ad6 | ||
|
|
4bdd10ab90 | ||
|
|
e13ce2ee98 | ||
|
|
a8509c112a | ||
|
|
a8d22e1223 | ||
|
|
f50cf60534 | ||
|
|
ead66155ef | ||
|
|
e7f1ceeb84 | ||
|
|
15475a9d1f | ||
|
|
d2ac954fe1 | ||
|
|
3d8a4bf023 | ||
|
|
7edf8be3bc | ||
|
|
6a519896db | ||
|
|
02a1a39796 | ||
|
|
4477a2e513 | ||
|
|
53852e35d8 | ||
|
|
b8a47c6589 | ||
|
|
9226b9917a | ||
|
|
5ae9892f5f | ||
|
|
86a7a72400 | ||
|
|
96aa3761fc | ||
|
|
1999bbfe82 | ||
|
|
97947c5fcf | ||
|
|
f6899cc289 | ||
|
|
527bee4b1e | ||
|
|
2e59b17108 | ||
|
|
e02e0508da | ||
|
|
ac92d471a6 | ||
|
|
f0eb1f3749 | ||
|
|
74a2297dcc | ||
|
|
e3995572bb | ||
|
|
2ef3fabcb8 | ||
|
|
a41c34705e | ||
|
|
f4989edd96 | ||
|
|
91f2af2d7a | ||
|
|
285bb2bbec | ||
|
|
4c13bae1cf | ||
|
|
132425eb46 | ||
|
|
c3ea279080 | ||
|
|
b38c54a25e | ||
|
|
b4ec350a94 | ||
|
|
789bee8792 | ||
|
|
7d73bb4f40 | ||
|
|
c60b5d4f00 | ||
|
|
015eb6faa7 | ||
|
|
624107deae | ||
|
|
746ee191e8 | ||
|
|
f5f27a5fbf | ||
|
|
0d7374ad2f | ||
|
|
ceb1376267 | ||
|
|
ad5059f2d3 | ||
|
|
e46b7d33a7 | ||
|
|
ede93469ea | ||
|
|
5f84b17ed6 | ||
|
|
3ea054a52c | ||
|
|
adbb821eac | ||
|
|
eb4bd92fac | ||
|
|
00b7c97d2a | ||
|
|
ea87f21e23 | ||
|
|
9797c928ef | ||
|
|
145337792d | ||
|
|
84f6b3014c | ||
|
|
56168d8565 | ||
|
|
109363de49 | ||
|
|
ccf04239e6 | ||
|
|
98edeac7b7 | ||
|
|
d79a915583 | ||
|
|
8f5902dfcf | ||
|
|
bce7d7ac60 | ||
|
|
919ee73153 | ||
|
|
f2cc4e0436 | ||
|
|
0b0bf94c96 | ||
|
|
672fcba223 | ||
|
|
5a77c86e97 | ||
|
|
8bdc45ba00 | ||
|
|
70737ea4ac | ||
|
|
dcadec65b6 | ||
|
|
8e3f9c1fbb | ||
|
|
ca11def2a5 | ||
|
|
e933e3150d | ||
|
|
fcd33fc409 | ||
|
|
c2a3911bb5 | ||
|
|
dbfa1421ac | ||
|
|
74a4c29729 | ||
|
|
44f4c4f9ba | ||
|
|
ce602827e5 | ||
|
|
dc7b63a793 | ||
|
|
a5265e2a56 | ||
|
|
060f17d1d8 | ||
|
|
aba94ef4d6 | ||
|
|
e0314ad8ca | ||
|
|
fc76ecde13 | ||
|
|
1bdc71d917 | ||
|
|
f41846d002 | ||
|
|
96707223db | ||
|
|
d7d83d6d93 | ||
|
|
1d05444b33 | ||
|
|
4e84c38b70 | ||
|
|
831b93a755 | ||
|
|
80f03177c4 | ||
|
|
80f966b80c | ||
|
|
355a63733d | ||
|
|
c883c15878 | ||
|
|
9469696e46 | ||
|
|
4e7026320a | ||
|
|
7d5ed49d23 | ||
|
|
5c321c7178 | ||
|
|
17eb86a689 | ||
|
|
68a117a25a | ||
|
|
7a2c46d951 | ||
|
|
b434be3d2d | ||
|
|
bd9e30c054 | ||
|
|
90c844576e | ||
|
|
ae897372bc | ||
|
|
ad2fc75676 | ||
|
|
c2dbc642e7 | ||
|
|
2e7b537b68 | ||
|
|
f847efe621 | ||
|
|
77bfa8181d | ||
|
|
e47385d34a | ||
|
|
71fa1c8baf | ||
|
|
bdba50432b | ||
|
|
e4e36383e2 | ||
|
|
bc03ab6688 | ||
|
|
46c310f62f | ||
|
|
b8369e2f3e | ||
|
|
dd1b789c15 | ||
|
|
c70c064752 | ||
|
|
ae89b4e818 | ||
|
|
dbe592597f | ||
|
|
178dd87e26 | ||
|
|
38bf5fc136 | ||
|
|
e1d7cbfc77 | ||
|
|
ced5f2e5e7 | ||
|
|
60266078ca | ||
|
|
5ce94e1dd3 | ||
|
|
ac47733044 | ||
|
|
ceade70d4e | ||
|
|
89ff7b2465 | ||
|
|
042570584f | ||
|
|
8262372d72 | ||
|
|
6e75129c77 | ||
|
|
cbeaa000ef | ||
|
|
72d127e187 | ||
|
|
70bd94b50b | ||
|
|
b1b67169f1 | ||
|
|
a8d74e15dd | ||
|
|
7339645a29 | ||
|
|
12d0a59074 | ||
|
|
923bb42cb9 | ||
|
|
a6a39a2591 | ||
|
|
5721804047 | ||
|
|
498b166e5f | ||
|
|
cc63f80193 | ||
|
|
2104330d4c | ||
|
|
681a800086 | ||
|
|
f0c331c724 | ||
|
|
b5ce35dfc8 | ||
|
|
543bd0ea0c | ||
|
|
cbaa2af280 | ||
|
|
c7826ab36e | ||
|
|
8ff7da7202 | ||
|
|
f40b1e7e9f | ||
|
|
9e17b51d45 | ||
|
|
0f97c34204 | ||
|
|
06cf4e0f70 | ||
|
|
9bb7905d26 | ||
|
|
4b40acd964 | ||
|
|
ce333f28d8 | ||
|
|
3cfb90b227 | ||
|
|
34fdc8881b | ||
|
|
5dc9ab5829 | ||
|
|
d44cc14c6b | ||
|
|
ee17516afd | ||
|
|
4701c108ff | ||
|
|
b9363d9726 | ||
|
|
afafeb379a | ||
|
|
718c352946 | ||
|
|
871528fedb | ||
|
|
52a3b2d77e | ||
|
|
5a36e241f4 | ||
|
|
ad388ecd78 | ||
|
|
6d77cc9b08 | ||
|
|
40073bbcb5 | ||
|
|
974d9c0eee | ||
|
|
46eee933b7 | ||
|
|
4ba1f62507 | ||
|
|
e11c09be82 | ||
|
|
d56bd7df19 | ||
|
|
52335bb48e | ||
|
|
7171ce767f | ||
|
|
177e345d8a | ||
|
|
d87414c57c | ||
|
|
bc79bdf68a | ||
|
|
36f4130cf1 | ||
|
|
2fe74069be | ||
|
|
7749b47d6a | ||
|
|
a6b86941a1 | ||
|
|
76bb135181 | ||
|
|
be17387682 | ||
|
|
b9c41ff051 | ||
|
|
16636a458f | ||
|
|
8a7f08ded3 | ||
|
|
6814cc6809 | ||
|
|
e6d4641bf0 | ||
|
|
193331d522 | ||
|
|
f30ed13155 | ||
|
|
8b0d340c18 | ||
|
|
eaf82fe411 | ||
|
|
a3adf24527 | ||
|
|
5efe377a26 | ||
|
|
27a1ae57e5 | ||
|
|
9baad51004 | ||
|
|
65bef771f6 | ||
|
|
8e1a87491a | ||
|
|
4ff647137a | ||
|
|
92070cbb67 | ||
|
|
acd56603b0 | ||
|
|
1d20a19c7d | ||
|
|
e1a715b0f5 | ||
|
|
496b6e4d3d | ||
|
|
d456af7499 | ||
|
|
80996d916b | ||
|
|
49e6a921df | ||
|
|
b5b701d590 | ||
|
|
ce80a0ce5e | ||
|
|
0a157f65bd | ||
|
|
f6f1e1821e | ||
|
|
c522630f72 | ||
|
|
e98a863f91 | ||
|
|
1d2708b147 | ||
|
|
51e7ba65ff | ||
|
|
f583b7bdcf | ||
|
|
9929f08968 | ||
|
|
60f734ecce | ||
|
|
325758317f | ||
|
|
b5cec7fdb9 | ||
|
|
e37152d74e | ||
|
|
b02d655dcf | ||
|
|
c82cc9cd11 | ||
|
|
7c3b6365f0 | ||
|
|
a8ad870bd0 | ||
|
|
7d58f57a52 | ||
|
|
d1f8915ed1 | ||
|
|
3d4349343d | ||
|
|
2851709745 | ||
|
|
b85e88e2db | ||
|
|
b42981c465 | ||
|
|
a2e0275f14 | ||
|
|
52eb9c99e2 | ||
|
|
c1fd93e8a0 | ||
|
|
0288078cfb | ||
|
|
64da5c2bf6 | ||
|
|
a581a93c9b | ||
|
|
896fa9bb7c | ||
|
|
2711d2ea55 | ||
|
|
ff15a752c1 | ||
|
|
45d082bbe2 | ||
|
|
732a0cd3e1 | ||
|
|
e2d9bf3b57 | ||
|
|
e06d01f0eb | ||
|
|
0c614f3e9d | ||
|
|
2f74d17297 | ||
|
|
4931da89f0 | ||
|
|
27faaec2b9 | ||
|
|
da402fbdfa | ||
|
|
4888e2c232 | ||
|
|
06642d97f5 | ||
|
|
62b4efb3e7 | ||
|
|
13368bed18 | ||
|
|
7f2f26b25f | ||
|
|
ed9ef7733b | ||
|
|
0afd14a14a | ||
|
|
77e19b3f87 | ||
|
|
e5b451a66a | ||
|
|
394a345ae0 | ||
|
|
90c542af12 | ||
|
|
03f5ad3060 | ||
|
|
49a18b8660 | ||
|
|
91d8873d86 | ||
|
|
9c66848c32 | ||
|
|
c0cbf0de2a | ||
|
|
7275ebf91a | ||
|
|
2f63dec2e3 | ||
|
|
d052c8c81e | ||
|
|
23a03d23aa | ||
|
|
866b6a842b | ||
|
|
2c41f25fb8 | ||
|
|
0afec0259b | ||
|
|
ed54b27b85 | ||
|
|
2fb5a6ca78 | ||
|
|
06eff5a72c | ||
|
|
5881c4ae48 | ||
|
|
e625436bca | ||
|
|
f06495c50a | ||
|
|
d666755159 | ||
|
|
f67427ae61 | ||
|
|
40fcf667b0 | ||
|
|
10bd8b1d86 | ||
|
|
852a895b70 | ||
|
|
ca6fc0265e | ||
|
|
f05cddd2fc | ||
|
|
841647643a | ||
|
|
9dd650f67f | ||
|
|
7e79fc6e3c | ||
|
|
624ad73705 | ||
|
|
98d244b288 | ||
|
|
c6d5927281 | ||
|
|
1b58d126c0 | ||
|
|
ba927d1c77 | ||
|
|
8e338632a3 | ||
|
|
91243ad5cd | ||
|
|
d44c585ca4 | ||
|
|
ee79ab46bb | ||
|
|
11308767a2 | ||
|
|
079ede79a3 | ||
|
|
f977aaee41 | ||
|
|
f56456a45c | ||
|
|
4da6e28802 | ||
|
|
f85480bb3c | ||
|
|
bfea7271d5 | ||
|
|
50dac0bd8f | ||
|
|
cb508e9678 | ||
|
|
b78fe28f0b | ||
|
|
26777abd02 | ||
|
|
fc67ca5cfa | ||
|
|
6e9f753057 | ||
|
|
ca3106f3bd | ||
|
|
b36fe59dd6 | ||
|
|
5a86354aaa | ||
|
|
e6a0c87c7e | ||
|
|
ce31e837eb | ||
|
|
03509025bc | ||
|
|
37faf1f426 | ||
|
|
083044c3e2 | ||
|
|
96e6f9ecb6 | ||
|
|
ad7e225193 | ||
|
|
ee2405b042 | ||
|
|
cf8c171f85 | ||
|
|
695cb617b2 | ||
|
|
9bee043ff2 | ||
|
|
b688960db0 | ||
|
|
b900560b83 | ||
|
|
b3c6334fbb | ||
|
|
4b660a7fc9 | ||
|
|
284fec8fcd | ||
|
|
52e19a0577 | ||
|
|
b72eed1f5e | ||
|
|
1fb3dbcbda | ||
|
|
fc534a1e7f | ||
|
|
d8c70903ec | ||
|
|
f3ac945d74 | ||
|
|
7fda5d52ae | ||
|
|
5a180c6659 | ||
|
|
09b0641ccb | ||
|
|
f43586c63c | ||
|
|
b585a550ba | ||
|
|
129b0d2b22 | ||
|
|
49ee952e9a | ||
|
|
c77ff2d293 | ||
|
|
9fa098d8e3 | ||
|
|
8b6c89423d | ||
|
|
e2f823fffc | ||
|
|
e5d4c7f4a7 | ||
|
|
e5ac9d8e57 | ||
|
|
802f05f73f | ||
|
|
5046efb94b | ||
|
|
840ac283ef | ||
|
|
a67518fc6d | ||
|
|
f39ee8dc95 | ||
|
|
69e655ba7f | ||
|
|
b78ab88a1c | ||
|
|
fd596945e7 | ||
|
|
3419ac1d36 | ||
|
|
1be4838ca0 | ||
|
|
e44137d46b | ||
|
|
b07010839c | ||
|
|
5edf695bc9 | ||
|
|
6d1d558c4f | ||
|
|
34b5414ba8 | ||
|
|
47d1612bf8 | ||
|
|
237885e0d2 | ||
|
|
24dce03aaa | ||
|
|
bf814320b0 | ||
|
|
c43bcdb5fb | ||
|
|
cbfc7b7c92 | ||
|
|
e73a82f7a5 | ||
|
|
3db1f2d550 | ||
|
|
cd966bf552 | ||
|
|
faa0eb6b52 | ||
|
|
4839d07f34 | ||
|
|
a69264e885 | ||
|
|
e0d2ba5608 | ||
|
|
558f77c259 | ||
|
|
2178335618 | ||
|
|
ebaa4e7256 | ||
|
|
9ed7ead84f | ||
|
|
06114c7bb2 | ||
|
|
1e84339df0 | ||
|
|
aa534c2582 | ||
|
|
27044b84d2 | ||
|
|
43a58bd618 | ||
|
|
da2e0e29a4 | ||
|
|
d1eb87c831 | ||
|
|
28b6456f3b | ||
|
|
cb3819d44e | ||
|
|
701973877f | ||
|
|
1a16dab9e1 | ||
|
|
bb87949d5c | ||
|
|
d0e7c0535e | ||
|
|
acfda6d8fd | ||
|
|
47ee3744f2 | ||
|
|
74b8af9891 | ||
|
|
6608705652 | ||
|
|
e3a91b186a | ||
|
|
95d44157fc | ||
|
|
1952ab99aa | ||
|
|
1ae7ca848c | ||
|
|
9ec0175e83 | ||
|
|
c560a338e8 | ||
|
|
4821adfd95 | ||
|
|
51641c0840 | ||
|
|
956cf83e7b | ||
|
|
88d42c3ac1 | ||
|
|
e706fb5686 | ||
|
|
d282a7593b | ||
|
|
a7e3cbd6ad | ||
|
|
3dbdf1632e | ||
|
|
d5825f13d3 | ||
|
|
6b6a4ca51d | ||
|
|
df8f967040 | ||
|
|
8d0fafc377 | ||
|
|
f64f626927 | ||
|
|
7f7cac20c1 | ||
|
|
b76db7c772 | ||
|
|
8124f202a4 | ||
|
|
a69f1baa13 | ||
|
|
013d626889 | ||
|
|
7fa15f7f86 | ||
|
|
7e88713ca3 | ||
|
|
6106d4069d | ||
|
|
2876137c92 | ||
|
|
43a7984cd8 | ||
|
|
8568003bb1 | ||
|
|
a3684fe3de | ||
|
|
2b266cb87e | ||
|
|
8991c8b589 | ||
|
|
8ad95f0db7 | ||
|
|
676ad70d9f | ||
|
|
53bb58ed2a | ||
|
|
bdfac4ff53 | ||
|
|
dcd881bb7a | ||
|
|
b8123b862a | ||
|
|
35a5eaeeb1 | ||
|
|
3408a05d12 | ||
|
|
0d48b89afe | ||
|
|
c64a134146 | ||
|
|
ec40affb59 | ||
|
|
cbcc622786 | ||
|
|
ea8f625b53 | ||
|
|
865a60f13e | ||
|
|
f744c1c6d9 | ||
|
|
dea8521ab9 | ||
|
|
a3e09a57c2 | ||
|
|
146a5b504c | ||
|
|
478854d36d | ||
|
|
5416e18007 | ||
|
|
0e2486df56 | ||
|
|
c0e58ade45 | ||
|
|
da97e58979 | ||
|
|
c37f285466 | ||
|
|
dfc719e012 | ||
|
|
a1e54fa2c9 | ||
|
|
47c6baf5ea | ||
|
|
3e9ffb6e33 | ||
|
|
ede9dd43e8 | ||
|
|
c055bc478c | ||
|
|
9761b7f3ef | ||
|
|
06b0982d6b | ||
|
|
cae174b11c | ||
|
|
32793adbd9 | ||
|
|
9866dd95c1 | ||
|
|
f6d33596ff | ||
|
|
0db0410237 | ||
|
|
78425561ce | ||
|
|
1ac12597fa | ||
|
|
bbd34fa15e | ||
|
|
1a7287c408 | ||
|
|
7fcbd3fa4b | ||
|
|
1c17fe70e0 | ||
|
|
e3c8304deb | ||
|
|
8df3c569c7 | ||
|
|
3d61a10367 | ||
|
|
c0a932a55f | ||
|
|
9882cda8b9 | ||
|
|
5a58c041c2 | ||
|
|
4f242980be | ||
|
|
1eaaf8ad51 | ||
|
|
2c6d86226f | ||
|
|
a5001b9c20 | ||
|
|
81c6720392 | ||
|
|
8679ba71dd | ||
|
|
873aac584e | ||
|
|
dd4038f0e5 | ||
|
|
986bed8261 | ||
|
|
9b557a88fc | ||
|
|
83a2a9f2f7 | ||
|
|
92b92d4d2c | ||
|
|
001750c239 | ||
|
|
4b0cefc4bd | ||
|
|
00fe5230e9 | ||
|
|
6058edb0d1 | ||
|
|
0a3a774202 | ||
|
|
0ff8fcac6a | ||
|
|
7bfb44113e | ||
|
|
cf5cbd1c70 | ||
|
|
4290b46e8c | ||
|
|
2748255c8b | ||
|
|
c45210a6f9 | ||
|
|
3e084be06b | ||
|
|
a19e7c7ce8 | ||
|
|
a71c9ad650 | ||
|
|
05a1396247 | ||
|
|
402c995d6d | ||
|
|
ec3a87bb46 | ||
|
|
c7c966d0e9 | ||
|
|
3dea9e02d0 | ||
|
|
9515e58e28 | ||
|
|
6ee66fb6b1 | ||
|
|
0e3de5a0cc | ||
|
|
a31407006c | ||
|
|
344490d89b | ||
|
|
463a5bf76e | ||
|
|
6d9f1d4227 | ||
|
|
58964d52a5 | ||
|
|
2b623ae302 | ||
|
|
d80d72efec | ||
|
|
396e233ac1 | ||
|
|
0e5ab52908 | ||
|
|
893af0a92c | ||
|
|
cc72f9428d | ||
|
|
5dc84bf210 | ||
|
|
ead59bdebf | ||
|
|
de810031bf | ||
|
|
dd536b475c | ||
|
|
03cd93bf1a | ||
|
|
50ec259750 | ||
|
|
3d17112a7e | ||
|
|
91b3c601bc | ||
|
|
a64155d91e | ||
|
|
8c0283381d | ||
|
|
2efe0acfc9 | ||
|
|
c4c77aa2dd | ||
|
|
526dd93b32 | ||
|
|
80b0b92d2f | ||
|
|
1b23224f9c | ||
|
|
8ed95e82c6 | ||
|
|
57b3320478 | ||
|
|
e564411a62 | ||
|
|
3f1e6da1d7 |
@@ -4,3 +4,4 @@ gocache-for-docker
|
||||
victoria-metrics-data
|
||||
vmstorage-data
|
||||
vmselect-cache
|
||||
.vscode
|
||||
|
||||
2
.github/dependabot.yml
vendored
@@ -21,6 +21,6 @@ updates:
|
||||
schedule:
|
||||
interval: "daily"
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/app/vmui"
|
||||
directory: "/app/vmui/packages/vmui"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
|
||||
2
.github/workflows/check-licenses.yml
vendored
@@ -14,7 +14,7 @@ jobs:
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@main
|
||||
with:
|
||||
go-version: 1.16
|
||||
go-version: 1.17
|
||||
id: go
|
||||
- name: Code checkout
|
||||
uses: actions/checkout@master
|
||||
|
||||
12
.github/workflows/main.yml
vendored
@@ -16,15 +16,15 @@ jobs:
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@main
|
||||
with:
|
||||
go-version: 1.16
|
||||
go-version: 1.17
|
||||
id: go
|
||||
- name: Dependencies
|
||||
run: |
|
||||
go get -u golang.org/x/lint/golint
|
||||
go get -u github.com/kisielk/errcheck
|
||||
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.29.0
|
||||
- name: Code checkout
|
||||
uses: actions/checkout@master
|
||||
- name: Dependencies
|
||||
run: |
|
||||
make install-golint
|
||||
make install-errcheck
|
||||
make install-golangci-lint
|
||||
- name: Build
|
||||
env:
|
||||
GO111MODULE: on
|
||||
|
||||
1
.gitignore
vendored
@@ -4,6 +4,7 @@
|
||||
*.pprof
|
||||
/bin
|
||||
.idea
|
||||
.vscode
|
||||
*.test
|
||||
*.swp
|
||||
/gocache-for-docker
|
||||
|
||||
2
LICENSE
@@ -175,7 +175,7 @@
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
Copyright 2019-2021 VictoriaMetrics, Inc.
|
||||
Copyright 2019-2022 VictoriaMetrics, Inc.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
||||
98
Makefile
@@ -24,6 +24,8 @@ all: \
|
||||
|
||||
include app/*/Makefile
|
||||
include deployment/*/Makefile
|
||||
include snap/local/Makefile
|
||||
|
||||
|
||||
clean:
|
||||
rm -rf bin/*
|
||||
@@ -84,9 +86,12 @@ vmutils-windows-amd64: \
|
||||
vmauth-windows-amd64 \
|
||||
vmctl-windows-amd64
|
||||
|
||||
release-snap:
|
||||
snapcraft
|
||||
snapcraft upload "victoriametrics_$(PKG_TAG)_multi.snap" --release beta,edge,candidate
|
||||
|
||||
publish-release:
|
||||
git checkout $(TAG) && $(MAKE) release publish && \
|
||||
git checkout $(TAG)-cluster && $(MAKE) release publish && \
|
||||
git checkout $(TAG)-enterprise && $(MAKE) release publish && \
|
||||
git checkout $(TAG)-enterprise-cluster && $(MAKE) release publish
|
||||
|
||||
release: \
|
||||
release-victoria-metrics \
|
||||
@@ -95,66 +100,82 @@ release: \
|
||||
release-victoria-metrics: \
|
||||
release-victoria-metrics-amd64 \
|
||||
release-victoria-metrics-arm \
|
||||
release-victoria-metrics-arm64
|
||||
release-victoria-metrics-arm64 \
|
||||
release-victoria-metrics-darwin-amd64 \
|
||||
release-victoria-metrics-darwin-arm64
|
||||
|
||||
release-victoria-metrics-amd64:
|
||||
GOARCH=amd64 $(MAKE) release-victoria-metrics-generic
|
||||
OSARCH=amd64 $(MAKE) release-victoria-metrics-generic
|
||||
|
||||
release-victoria-metrics-arm:
|
||||
GOARCH=arm $(MAKE) release-victoria-metrics-generic
|
||||
OSARCH=arm $(MAKE) release-victoria-metrics-generic
|
||||
|
||||
release-victoria-metrics-arm64:
|
||||
GOARCH=arm64 $(MAKE) release-victoria-metrics-generic
|
||||
OSARCH=arm64 $(MAKE) release-victoria-metrics-generic
|
||||
|
||||
release-victoria-metrics-generic: victoria-metrics-$(GOARCH)-prod
|
||||
release-victoria-metrics-darwin-amd64:
|
||||
OSARCH=darwin-amd64 $(MAKE) release-victoria-metrics-generic
|
||||
|
||||
release-victoria-metrics-darwin-arm64:
|
||||
OSARCH=darwin-arm64 $(MAKE) release-victoria-metrics-generic
|
||||
|
||||
release-victoria-metrics-generic: victoria-metrics-$(OSARCH)-prod
|
||||
cd bin && \
|
||||
tar --transform="flags=r;s|-$(GOARCH)||" -czf victoria-metrics-$(GOARCH)-$(PKG_TAG).tar.gz \
|
||||
victoria-metrics-$(GOARCH)-prod \
|
||||
&& sha256sum victoria-metrics-$(GOARCH)-$(PKG_TAG).tar.gz \
|
||||
victoria-metrics-$(GOARCH)-prod \
|
||||
| sed s/-$(GOARCH)-prod/-prod/ > victoria-metrics-$(GOARCH)-$(PKG_TAG)_checksums.txt
|
||||
tar --transform="flags=r;s|-$(OSARCH)||" -czf victoria-metrics-$(OSARCH)-$(PKG_TAG).tar.gz \
|
||||
victoria-metrics-$(OSARCH)-prod \
|
||||
&& sha256sum victoria-metrics-$(OSARCH)-$(PKG_TAG).tar.gz \
|
||||
victoria-metrics-$(OSARCH)-prod \
|
||||
| sed s/-$(OSARCH)-prod/-prod/ > victoria-metrics-$(OSARCH)-$(PKG_TAG)_checksums.txt
|
||||
|
||||
release-vmutils: \
|
||||
release-vmutils-amd64 \
|
||||
release-vmutils-arm64 \
|
||||
release-vmutils-arm \
|
||||
release-vmutils-darwin-amd64 \
|
||||
release-vmutils-darwin-arm64 \
|
||||
release-vmutils-windows-amd64
|
||||
|
||||
release-vmutils-amd64:
|
||||
GOARCH=amd64 $(MAKE) release-vmutils-generic
|
||||
OSARCH=amd64 $(MAKE) release-vmutils-generic
|
||||
|
||||
release-vmutils-arm64:
|
||||
GOARCH=arm64 $(MAKE) release-vmutils-generic
|
||||
OSARCH=arm64 $(MAKE) release-vmutils-generic
|
||||
|
||||
release-vmutils-arm:
|
||||
GOARCH=arm $(MAKE) release-vmutils-generic
|
||||
OSARCH=arm $(MAKE) release-vmutils-generic
|
||||
|
||||
release-vmutils-darwin-amd64:
|
||||
OSARCH=darwin-amd64 $(MAKE) release-vmutils-generic
|
||||
|
||||
release-vmutils-darwin-arm64:
|
||||
OSARCH=darwin-arm64 $(MAKE) release-vmutils-generic
|
||||
|
||||
release-vmutils-windows-amd64:
|
||||
GOARCH=amd64 $(MAKE) release-vmutils-windows-generic
|
||||
|
||||
release-vmutils-generic: \
|
||||
vmagent-$(GOARCH)-prod \
|
||||
vmalert-$(GOARCH)-prod \
|
||||
vmauth-$(GOARCH)-prod \
|
||||
vmbackup-$(GOARCH)-prod \
|
||||
vmrestore-$(GOARCH)-prod \
|
||||
vmctl-$(GOARCH)-prod
|
||||
vmagent-$(OSARCH)-prod \
|
||||
vmalert-$(OSARCH)-prod \
|
||||
vmauth-$(OSARCH)-prod \
|
||||
vmbackup-$(OSARCH)-prod \
|
||||
vmrestore-$(OSARCH)-prod \
|
||||
vmctl-$(OSARCH)-prod
|
||||
cd bin && \
|
||||
tar --transform="flags=r;s|-$(GOARCH)||" -czf vmutils-$(GOARCH)-$(PKG_TAG).tar.gz \
|
||||
vmagent-$(GOARCH)-prod \
|
||||
vmalert-$(GOARCH)-prod \
|
||||
vmauth-$(GOARCH)-prod \
|
||||
vmbackup-$(GOARCH)-prod \
|
||||
vmrestore-$(GOARCH)-prod \
|
||||
vmctl-$(GOARCH)-prod \
|
||||
&& sha256sum vmutils-$(GOARCH)-$(PKG_TAG).tar.gz \
|
||||
vmagent-$(GOARCH)-prod \
|
||||
vmalert-$(GOARCH)-prod \
|
||||
vmauth-$(GOARCH)-prod \
|
||||
vmbackup-$(GOARCH)-prod \
|
||||
vmrestore-$(GOARCH)-prod \
|
||||
vmctl-$(GOARCH)-prod \
|
||||
| sed s/-$(GOARCH)-prod/-prod/ > vmutils-$(GOARCH)-$(PKG_TAG)_checksums.txt
|
||||
tar --transform="flags=r;s|-$(OSARCH)||" -czf vmutils-$(OSARCH)-$(PKG_TAG).tar.gz \
|
||||
vmagent-$(OSARCH)-prod \
|
||||
vmalert-$(OSARCH)-prod \
|
||||
vmauth-$(OSARCH)-prod \
|
||||
vmbackup-$(OSARCH)-prod \
|
||||
vmrestore-$(OSARCH)-prod \
|
||||
vmctl-$(OSARCH)-prod \
|
||||
&& sha256sum vmutils-$(OSARCH)-$(PKG_TAG).tar.gz \
|
||||
vmagent-$(OSARCH)-prod \
|
||||
vmalert-$(OSARCH)-prod \
|
||||
vmauth-$(OSARCH)-prod \
|
||||
vmbackup-$(OSARCH)-prod \
|
||||
vmrestore-$(OSARCH)-prod \
|
||||
vmctl-$(OSARCH)-prod \
|
||||
| sed s/-$(OSARCH)-prod/-prod/ > vmutils-$(OSARCH)-$(PKG_TAG)_checksums.txt
|
||||
|
||||
release-vmutils-windows-generic: \
|
||||
vmagent-windows-$(GOARCH)-prod \
|
||||
@@ -174,6 +195,7 @@ release-vmutils-windows-generic: \
|
||||
vmctl-windows-$(GOARCH)-prod.exe \
|
||||
> vmutils-windows-$(GOARCH)-$(PKG_TAG)_checksums.txt
|
||||
|
||||
|
||||
pprof-cpu:
|
||||
go tool pprof -trim_path=github.com/VictoriaMetrics/VictoriaMetrics@ $(PPROF_FILE)
|
||||
|
||||
@@ -261,7 +283,7 @@ golangci-lint: install-golangci-lint
|
||||
golangci-lint run --exclude '(SA4003|SA1019|SA5011):' -D errcheck -D structcheck --timeout 2m
|
||||
|
||||
install-golangci-lint:
|
||||
which golangci-lint || curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(shell go env GOPATH)/bin v1.40.1
|
||||
which golangci-lint || curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(shell go env GOPATH)/bin v1.44.1
|
||||
|
||||
install-wwhrd:
|
||||
which wwhrd || GO111MODULE=off go get github.com/frapposelli/wwhrd
|
||||
|
||||
547
README.md
@@ -13,41 +13,13 @@
|
||||
VictoriaMetrics is a fast, cost-effective and scalable monitoring solution and time series database.
|
||||
|
||||
VictoriaMetrics is available in [binary releases](https://github.com/VictoriaMetrics/VictoriaMetrics/releases),
|
||||
in [Docker images](https://hub.docker.com/r/victoriametrics/victoria-metrics/), in [Snap packages](https://snapcraft.io/victoriametrics)
|
||||
and in [source code](https://github.com/VictoriaMetrics/VictoriaMetrics). Just download VictoriaMetrics follow [these instructions](#how-to-start-victoriametrics).
|
||||
[Docker images](https://hub.docker.com/r/victoriametrics/victoria-metrics/), [Snap packages](https://snapcraft.io/victoriametrics)
|
||||
and [source code](https://github.com/VictoriaMetrics/VictoriaMetrics). Just download VictoriaMetrics and follow [these instructions](#how-to-start-victoriametrics).
|
||||
Then read [Prometheus setup](#prometheus-setup) and [Grafana setup](#grafana-setup) docs.
|
||||
|
||||
Cluster version of VictoriaMetrics is available [here](https://docs.victoriametrics.com/Cluster-VictoriaMetrics.html).
|
||||
|
||||
[Contact us](mailto:info@victoriametrics.com) if you need enterprise support for VictoriaMetrics.
|
||||
See [features available in enterprise package](https://victoriametrics.com/enterprise.html).
|
||||
Enterprise binaries can be downloaded and evaluated for free from [the releases page](https://github.com/VictoriaMetrics/VictoriaMetrics/releases).
|
||||
|
||||
|
||||
## Case studies and talks
|
||||
|
||||
Case studies:
|
||||
|
||||
* [adidas](https://docs.victoriametrics.com/CaseStudies.html#adidas)
|
||||
* [Adsterra](https://docs.victoriametrics.com/CaseStudies.html#adsterra)
|
||||
* [ARNES](https://docs.victoriametrics.com/CaseStudies.html#arnes)
|
||||
* [Brandwatch](https://docs.victoriametrics.com/CaseStudies.html#brandwatch)
|
||||
* [CERN](https://docs.victoriametrics.com/CaseStudies.html#cern)
|
||||
* [COLOPL](https://docs.victoriametrics.com/CaseStudies.html#colopl)
|
||||
* [Dreamteam](https://docs.victoriametrics.com/CaseStudies.html#dreamteam)
|
||||
* [German Research Center for Artificial Intelligence](https://docs.victoriametrics.com/CaseStudies.html#german-research-center-for-artificial-intelligence)
|
||||
* [Grammarly](https://docs.victoriametrics.com/CaseStudies.html#grammarly)
|
||||
* [Groove X](https://docs.victoriametrics.com/CaseStudies.html#groove-x)
|
||||
* [Idealo.de](https://docs.victoriametrics.com/CaseStudies.html#idealode)
|
||||
* [MHI Vestas Offshore Wind](https://docs.victoriametrics.com/CaseStudies.html#mhi-vestas-offshore-wind)
|
||||
* [Sensedia](https://docs.victoriametrics.com/CaseStudies.html#sensedia)
|
||||
* [Synthesio](https://docs.victoriametrics.com/CaseStudies.html#synthesio)
|
||||
* [Wedos.com](https://docs.victoriametrics.com/CaseStudies.html#wedoscom)
|
||||
* [Wix.com](https://docs.victoriametrics.com/CaseStudies.html#wixcom)
|
||||
* [Zerodha](https://docs.victoriametrics.com/CaseStudies.html#zerodha)
|
||||
* [zhihu](https://docs.victoriametrics.com/CaseStudies.html#zhihu)
|
||||
|
||||
See also [articles and slides about VictoriaMetrics from our users](https://docs.victoriametrics.com/Articles.html#third-party-articles-and-slides-about-victoriametrics)
|
||||
[Contact us](mailto:info@victoriametrics.com) if you need enterprise support for VictoriaMetrics. See [features available in enterprise package](https://victoriametrics.com/products/enterprise/). Enterprise binaries can be downloaded and evaluated for free from [the releases page](https://github.com/VictoriaMetrics/VictoriaMetrics/releases).
|
||||
|
||||
|
||||
## Prominent features
|
||||
@@ -84,12 +56,43 @@ VictoriaMetrics has the following prominent features:
|
||||
* [Native binary format](#how-to-import-data-in-native-format).
|
||||
* It supports metrics' relabeling. See [these docs](#relabeling) for details.
|
||||
* It can deal with [high cardinality issues](https://docs.victoriametrics.com/FAQ.html#what-is-high-cardinality) and [high churn rate](https://docs.victoriametrics.com/FAQ.html#what-is-high-churn-rate) issues via [series limiter](#cardinality-limiter).
|
||||
* It ideally works with big amounts of time series data from APM, Kubernetes, IoT sensors, connected cars, industrial telemetry, financial data and various [Enterprise workloads](https://victoriametrics.com/enterprise.html).
|
||||
* It ideally works with big amounts of time series data from APM, Kubernetes, IoT sensors, connected cars, industrial telemetry, financial data and various [Enterprise workloads](https://victoriametrics.com/products/enterprise/).
|
||||
* It has open source [cluster version](https://github.com/VictoriaMetrics/VictoriaMetrics/tree/cluster).
|
||||
|
||||
See also [various Articles about VictoriaMetrics](https://docs.victoriametrics.com/Articles.html).
|
||||
|
||||
|
||||
## Case studies and talks
|
||||
|
||||
Case studies:
|
||||
|
||||
* [AbiosGaming](https://docs.victoriametrics.com/CaseStudies.html#abiosgaming)
|
||||
* [adidas](https://docs.victoriametrics.com/CaseStudies.html#adidas)
|
||||
* [Adsterra](https://docs.victoriametrics.com/CaseStudies.html#adsterra)
|
||||
* [ARNES](https://docs.victoriametrics.com/CaseStudies.html#arnes)
|
||||
* [Brandwatch](https://docs.victoriametrics.com/CaseStudies.html#brandwatch)
|
||||
* [CERN](https://docs.victoriametrics.com/CaseStudies.html#cern)
|
||||
* [COLOPL](https://docs.victoriametrics.com/CaseStudies.html#colopl)
|
||||
* [Dreamteam](https://docs.victoriametrics.com/CaseStudies.html#dreamteam)
|
||||
* [Fly.io](https://docs.victoriametrics.com/CaseStudies.html#flyio)
|
||||
* [German Research Center for Artificial Intelligence](https://docs.victoriametrics.com/CaseStudies.html#german-research-center-for-artificial-intelligence)
|
||||
* [Grammarly](https://docs.victoriametrics.com/CaseStudies.html#grammarly)
|
||||
* [Groove X](https://docs.victoriametrics.com/CaseStudies.html#groove-x)
|
||||
* [Idealo.de](https://docs.victoriametrics.com/CaseStudies.html#idealode)
|
||||
* [MHI Vestas Offshore Wind](https://docs.victoriametrics.com/CaseStudies.html#mhi-vestas-offshore-wind)
|
||||
* [Razorpay](https://docs.victoriametrics.com/CaseStudies.html#razorpay)
|
||||
* [Percona](https://docs.victoriametrics.com/CaseStudies.html#percona)
|
||||
* [Sensedia](https://docs.victoriametrics.com/CaseStudies.html#sensedia)
|
||||
* [Smarkets](https://docs.victoriametrics.com/CaseStudies.html#smarkets)
|
||||
* [Synthesio](https://docs.victoriametrics.com/CaseStudies.html#synthesio)
|
||||
* [Wedos.com](https://docs.victoriametrics.com/CaseStudies.html#wedoscom)
|
||||
* [Wix.com](https://docs.victoriametrics.com/CaseStudies.html#wixcom)
|
||||
* [Zerodha](https://docs.victoriametrics.com/CaseStudies.html#zerodha)
|
||||
* [zhihu](https://docs.victoriametrics.com/CaseStudies.html#zhihu)
|
||||
|
||||
See also [articles and slides about VictoriaMetrics from our users](https://docs.victoriametrics.com/Articles.html#third-party-articles-and-slides-about-victoriametrics)
|
||||
|
||||
|
||||
## Operation
|
||||
|
||||
## How to start VictoriaMetrics
|
||||
@@ -99,7 +102,7 @@ Just download [VictoriaMetrics executable](https://github.com/VictoriaMetrics/Vi
|
||||
The following command-line flags are used the most:
|
||||
|
||||
* `-storageDataPath` - VictoriaMetrics stores all the data in this directory. Default path is `victoria-metrics-data` in the current working directory.
|
||||
* `-retentionPeriod` - retention for stored data. Older data is automatically deleted. Default retention is 1 month. See [these docs](#retention) for more details.
|
||||
* `-retentionPeriod` - retention for stored data. Older data is automatically deleted. Default retention is 1 month. See [the Retention section](#retention) for more details.
|
||||
|
||||
Other flags have good enough default values, so set them only if you really need this. Pass `-help` to see [all the available flags with description and default values](#list-of-command-line-flags).
|
||||
|
||||
@@ -265,6 +268,54 @@ VictoriaMetrics also supports [importing data in Prometheus exposition format](#
|
||||
See also [vmagent](https://docs.victoriametrics.com/vmagent.html), which can be used as drop-in replacement for Prometheus.
|
||||
|
||||
|
||||
## How to send data from DataDog agent
|
||||
|
||||
VictoriaMetrics accepts data from [DataDog agent](https://docs.datadoghq.com/agent/) or [DogStatsD]() via ["submit metrics" API](https://docs.datadoghq.com/api/latest/metrics/#submit-metrics) at `/datadog/api/v1/series` path.
|
||||
|
||||
Run DataDog agent with `DD_DD_URL=http://victoriametrics-host:8428/datadog` environment variable in order to write data to VictoriaMetrics at `victoriametrics-host` host. Another option is to set `dd_url` param at [DataDog agent configuration file](https://docs.datadoghq.com/agent/guide/agent-configuration-files/) to `http://victoriametrics-host:8428/datadog`.
|
||||
|
||||
VictoriaMetrics doesn't check `DD_API_KEY` param, so it can be set to arbitrary value.
|
||||
|
||||
Example on how to send data to VictoriaMetrics via DataDog "submit metrics" API from command line:
|
||||
|
||||
```bash
|
||||
echo '
|
||||
{
|
||||
"series": [
|
||||
{
|
||||
"host": "test.example.com",
|
||||
"interval": 20,
|
||||
"metric": "system.load.1",
|
||||
"points": [[
|
||||
0,
|
||||
0.5
|
||||
]],
|
||||
"tags": [
|
||||
"environment:test"
|
||||
],
|
||||
"type": "rate"
|
||||
}
|
||||
]
|
||||
}
|
||||
' | curl -X POST --data-binary @- http://localhost:8428/datadog/api/v1/series
|
||||
```
|
||||
|
||||
The imported data can be read via [export API](https://docs.victoriametrics.com/#how-to-export-data-in-json-line-format):
|
||||
|
||||
```bash
|
||||
curl http://localhost:8428/api/v1/export -d 'match[]=system.load.1'
|
||||
```
|
||||
|
||||
This command should return the following output if everything is OK:
|
||||
|
||||
```
|
||||
{"metric":{"__name__":"system.load.1","environment":"test","host":"test.example.com"},"values":[0.5],"timestamps":[1632833641000]}
|
||||
```
|
||||
|
||||
Extra labels may be added to all the written time series by passing `extra_label=name=value` query args.
|
||||
For example, `/datadog/api/v1/series?extra_label=foo=bar` would add `{foo="bar"}` label to all the ingested metrics.
|
||||
|
||||
|
||||
## How to send data from InfluxDB-compatible agents such as [Telegraf](https://www.influxdata.com/time-series-platform/telegraf/)
|
||||
|
||||
Use `http://<victoriametric-addr>:8428` url instead of InfluxDB url in agents' configs.
|
||||
@@ -281,7 +332,7 @@ and stream plain InfluxDB line protocol data to the configured TCP and/or UDP ad
|
||||
VictoriaMetrics performs the following transformations to the ingested InfluxDB data:
|
||||
|
||||
* [`db` query arg](https://docs.influxdata.com/influxdb/v1.7/tools/api/#write-http-endpoint) is mapped into `db` label value
|
||||
unless `db` tag exists in the InfluxDB line.
|
||||
unless `db` tag exists in the InfluxDB line. The `db` label name can be overriden via `-influxDBLabel` command-line flag.
|
||||
* Field names are mapped to time series names prefixed with `{measurement}{separator}` value, where `{separator}` equals to `_` by default. It can be changed with `-influxMeasurementFieldSeparator` command-line flag. See also `-influxSkipSingleField` command-line flag. If `{measurement}` is empty or if `-influxSkipMeasurement` command-line flag is set, then time series names correspond to field names.
|
||||
* Field values are mapped to time series values.
|
||||
* Tags are mapped to Prometheus labels as-is.
|
||||
@@ -367,14 +418,20 @@ The `/api/v1/export` endpoint should return the following response:
|
||||
Data sent to VictoriaMetrics via `Graphite plaintext protocol` may be read via the following APIs:
|
||||
|
||||
* [Graphite API](#graphite-api-usage)
|
||||
* [Prometheus querying API](#prometheus-querying-api-usage). VictoriaMetrics supports `__graphite__` pseudo-label for selecting time series with Graphite-compatible filters in [MetricsQL](https://docs.victoriametrics.com/MetricsQL.html). For example, `{__graphite__="foo.*.bar"}` is equivalent to `{__name__=~"foo[.][^.]*[.]bar"}`, but it works faster and it is easier to use when migrating from Graphite to VictoriaMetrics.
|
||||
* [Prometheus querying API](#prometheus-querying-api-usage). See also [selecting Graphite metrics](#selecting-graphite-metrics).
|
||||
* [go-graphite/carbonapi](https://github.com/go-graphite/carbonapi/blob/main/cmd/carbonapi/carbonapi.example.victoriametrics.yaml)
|
||||
|
||||
## Selecting Graphite metrics
|
||||
|
||||
VictoriaMetrics supports `__graphite__` pseudo-label for selecting time series with Graphite-compatible filters in [MetricsQL](https://docs.victoriametrics.com/MetricsQL.html). For example, `{__graphite__="foo.*.bar"}` is equivalent to `{__name__=~"foo[.][^.]*[.]bar"}`, but it works faster and it is easier to use when migrating from Graphite to VictoriaMetrics. See [docs for Graphite paths and wildcards](https://graphite.readthedocs.io/en/latest/render_api.html#paths-and-wildcards). VictoriaMetrics also supports [label_graphite_group](https://docs.victoriametrics.com/MetricsQL.html#label_graphite_group) function for extracting the given groups from Graphite metric name.
|
||||
|
||||
The `__graphite__` pseudo-label supports e.g. alternate regexp filters such as `(value1|...|valueN)`. They are transparently converted to `{value1,...,valueN}` syntax [used in Graphite](https://graphite.readthedocs.io/en/latest/render_api.html#paths-and-wildcards). This allows using [multi-value template variables in Grafana](https://grafana.com/docs/grafana/latest/variables/formatting-multi-value-variables/) inside `__graphite__` pseudo-label. For example, Grafana expands `{__graphite__=~"foo.($bar).baz"}` into `{__graphite__=~"foo.(x|y).baz"}` if `$bar` template variable contains `x` and `y` values. In this case the query is automatically converted into `{__graphite__=~"foo.{x,y}.baz"}` before execution.
|
||||
|
||||
## How to send data from OpenTSDB-compatible agents
|
||||
|
||||
VictoriaMetrics supports [telnet put protocol](http://opentsdb.net/docs/build/html/api_telnet/put.html)
|
||||
and [HTTP /api/put requests](http://opentsdb.net/docs/build/html/api_http/put.html) for ingesting OpenTSDB data.
|
||||
The same protocol is used for [ingesting data in KairosDB](https://kairosdb.github.io/docs/build/html/PushingData.html).
|
||||
The same protocol is used for [ingesting data in KairosDB](https://kairosdb.github.io/docs/PushingData.html).
|
||||
|
||||
### Sending data via `telnet put` protocol
|
||||
|
||||
@@ -466,9 +523,10 @@ All the Prometheus querying API handlers can be prepended with `/prometheus` pre
|
||||
### Prometheus querying API enhancements
|
||||
|
||||
VictoriaMetrics accepts optional `extra_label=<label_name>=<label_value>` query arg, which can be used for enforcing additional label filters for queries. For example,
|
||||
`/api/v1/query_range?extra_label=user_id=123&query=<query>` would automatically add `{user_id="123"}` label filter to the given `<query>`. This functionality can be used
|
||||
for limiting the scope of time series visible to the given tenant. It is expected that the `extra_label` query arg is automatically set by auth proxy sitting
|
||||
in front of VictoriaMetrics. See [vmauth](https://docs.victoriametrics.com/vmauth.html) and [vmgateway](https://docs.victoriametrics.com/vmgateway.html) as examples of such proxies.
|
||||
`/api/v1/query_range?extra_label=user_id=123&extra_label=group_id=456&query=<query>` would automatically add `{user_id="123",group_id="456"}` label filters to the given `<query>`. This functionality can be used for limiting the scope of time series visible to the given tenant. It is expected that the `extra_label` query args are automatically set by auth proxy sitting in front of VictoriaMetrics. See [vmauth](https://docs.victoriametrics.com/vmauth.html) and [vmgateway](https://docs.victoriametrics.com/vmgateway.html) as examples of such proxies.
|
||||
|
||||
VictoriaMetrics accepts optional `extra_filters[]=series_selector` query arg, which can be used for enforcing arbitrary label filters for queries. For example,
|
||||
`/api/v1/query_range?extra_filters[]={env=~"prod|staging",user="xyz"}&query=<query>` would automatically add `{env=~"prod|staging",user="xyz"}` label filters to the given `<query>`. This functionality can be used for limiting the scope of time series visible to the given tenant. It is expected that the `extra_filters[]` query args are automatically set by auth proxy sitting in front of VictoriaMetrics. See [vmauth](https://docs.victoriametrics.com/vmauth.html) and [vmgateway](https://docs.victoriametrics.com/vmgateway.html) as examples of such proxies.
|
||||
|
||||
VictoriaMetrics accepts relative times in `time`, `start` and `end` query args additionally to unix timestamps and [RFC3339](https://www.ietf.org/rfc/rfc3339.txt).
|
||||
For example, the following query would return data for the last 30 minutes: `/api/v1/query_range?start=-30m&query=...`.
|
||||
@@ -479,7 +537,7 @@ By default, VictoriaMetrics returns time series for the last 5 minutes from `/ap
|
||||
|
||||
Additionally VictoriaMetrics provides the following handlers:
|
||||
|
||||
* `/vmui` - Basic Web UI
|
||||
* `/vmui` - Basic Web UI. See [these docs](#vmui).
|
||||
* `/api/v1/series/count` - returns the total number of time series in the database. Some notes:
|
||||
* the handler scans all the inverted index, so it can be slow if the database contains tens of millions of time series;
|
||||
* the handler may count [deleted time series](#how-to-delete-time-series) additionally to normal time series due to internal implementation restrictions;
|
||||
@@ -505,18 +563,16 @@ VictoriaMetrics supports the following Graphite APIs, which are needed for [Grap
|
||||
|
||||
All the Graphite handlers can be pre-pended with `/graphite` prefix. For example, both `/graphite/metrics/find` and `/metrics/find` should work.
|
||||
|
||||
VictoriaMetrics accepts optional `extra_label=<label_name>=<label_value>` query arg for all the Graphite APIs. This arg can be used for limiting the scope of time series
|
||||
visible to the given tenant. It is expected that the `extra_label` query arg is automatically set by auth proxy sitting in front of VictoriaMetrics.
|
||||
VictoriaMetrics accepts optional query args: `extra_label=<label_name>=<label_value>` and `extra_filters[]=series_selector` query args for all the Graphite APIs. These args can be used for limiting the scope of time series visible to the given tenant. It is expected that the `extra_label` query arg is automatically set by auth proxy sitting in front of VictoriaMetrics. See [vmauth](https://docs.victoriametrics.com/vmauth.html) and [vmgateway](https://docs.victoriametrics.com/vmgateway.html) as examples of such proxies.
|
||||
|
||||
[Contact us](mailto:sales@victoriametrics.com) if you need assistance with such a proxy.
|
||||
|
||||
VictoriaMetrics supports `__graphite__` pseudo-label for filtering time series with Graphite-compatible filters in [MetricsQL](https://docs.victoriametrics.com/MetricsQL.html).
|
||||
For example, `{__graphite__="foo.*.bar"}` is equivalent to `{__name__=~"foo[.][^.]*[.]bar"}`, but it works faster
|
||||
and it is easier to use when migrating from Graphite to VictoriaMetrics.
|
||||
VictoriaMetrics supports `__graphite__` pseudo-label for filtering time series with Graphite-compatible filters in [MetricsQL](https://docs.victoriametrics.com/MetricsQL.html). See [these docs](#selecting-graphite-metrics).
|
||||
|
||||
|
||||
### Graphite Render API usage
|
||||
|
||||
[VictoriaMetrics Enterprise](https://victoriametrics.com/enterprise.html) supports [Graphite Render API](https://graphite.readthedocs.io/en/stable/render_api.html) subset
|
||||
[VictoriaMetrics Enterprise](https://victoriametrics.com/products/enterprise/) supports [Graphite Render API](https://graphite.readthedocs.io/en/stable/render_api.html) subset
|
||||
at `/render` endpoint, which is used by [Graphite datasource in Grafana](https://grafana.com/docs/grafana/latest/datasources/graphite/).
|
||||
When configuring Graphite datasource in Grafana, the `Storage-Step` http request header must be set to a step between Graphite data points stored in VictoriaMetrics. For example, `Storage-Step: 10s` would mean 10 seconds distance between Graphite datapoints stored in VictoriaMetrics.
|
||||
Enterprise binaries can be downloaded and evaluated for free from [the releases page](https://github.com/VictoriaMetrics/VictoriaMetrics/releases).
|
||||
@@ -550,6 +606,25 @@ VictoriaMetrics supports the following handlers from [Graphite Tags API](https:/
|
||||
* [/tags/delSeries](https://graphite.readthedocs.io/en/stable/tags.html#removing-series-from-the-tagdb)
|
||||
|
||||
|
||||
## vmui
|
||||
|
||||
VictoriaMetrics provides UI for query troubleshooting and exploration. The UI is available at `http://victoriametrics:8428/vmui`.
|
||||
The UI allows exploring query results via graphs and tables. Graphs support scrolling and zooming:
|
||||
|
||||
* Drag the graph to the left / right in order to move the displayed time range into the past / future.
|
||||
* Hold `Ctrl` (or `Cmd` on MacOS) and scroll up / down in order to zoom in / out the graph.
|
||||
|
||||
Query history can be navigated by holding `Ctrl` (or `Cmd` on MacOS) and pressing `up` or `down` arrows on the keyboard while the cursor is located in the query input field.
|
||||
|
||||
When querying the [backfilled data](https://docs.victoriametrics.com/#backfilling), it may be useful disabling response cache by clicking `Enable cache` checkbox.
|
||||
|
||||
VMUI automatically adjusts the interval between datapoints on the graph depending on the horizontal resolution and on the selected time range. The step value can be customized by clickhing `Override step value` checkbox.
|
||||
|
||||
VMUI allows investigating correlations between two queries on the same graph. Just click `+Query` button, enter the second query in the newly appeared input field and press `Ctrl+Enter`. Results for both queries should be displayed simultaneously on the same graph. Every query has its own vertical scale, which is displayed on the left and the right side of the graph. Lines for the second query are dashed.
|
||||
|
||||
See the [example VMUI at VictoriaMetrics playground](https://play.victoriametrics.com/select/accounting/1/6a716b0f-38bc-4856-90ce-448fd713e3fe/prometheus/graph/?g0.expr=100%20*%20sum(rate(process_cpu_seconds_total))%20by%20(job)&g0.range_input=1d).
|
||||
|
||||
|
||||
## How to build from sources
|
||||
|
||||
We recommend using either [binary releases](https://github.com/VictoriaMetrics/VictoriaMetrics/releases) or
|
||||
@@ -559,7 +634,7 @@ to your needs or when testing bugfixes.
|
||||
|
||||
### Development build
|
||||
|
||||
1. [Install Go](https://golang.org/doc/install). The minimum supported version is Go 1.16.
|
||||
1. [Install Go](https://golang.org/doc/install). The minimum supported version is Go 1.17.
|
||||
2. Run `make victoria-metrics` from the root folder of [the repository](https://github.com/VictoriaMetrics/VictoriaMetrics).
|
||||
It builds `victoria-metrics` binary and puts it into the `bin` folder.
|
||||
|
||||
@@ -575,7 +650,7 @@ ARM build may run on Raspberry Pi or on [energy-efficient ARM servers](https://b
|
||||
|
||||
### Development ARM build
|
||||
|
||||
1. [Install Go](https://golang.org/doc/install). The minimum supported version is Go 1.16.
|
||||
1. [Install Go](https://golang.org/doc/install). The minimum supported version is Go 1.17.
|
||||
2. Run `make victoria-metrics-arm` or `make victoria-metrics-arm64` from the root folder of [the repository](https://github.com/VictoriaMetrics/VictoriaMetrics).
|
||||
It builds `victoria-metrics-arm` or `victoria-metrics-arm64` binary respectively and puts it into the `bin` folder.
|
||||
|
||||
@@ -589,7 +664,7 @@ ARM build may run on Raspberry Pi or on [energy-efficient ARM servers](https://b
|
||||
|
||||
`Pure Go` mode builds only Go code without [cgo](https://golang.org/cmd/cgo/) dependencies.
|
||||
|
||||
1. [Install Go](https://golang.org/doc/install). The minimum supported version is Go 1.16.
|
||||
1. [Install Go](https://golang.org/doc/install). The minimum supported version is Go 1.17.
|
||||
2. Run `make victoria-metrics-pure` from the root folder of [the repository](https://github.com/VictoriaMetrics/VictoriaMetrics).
|
||||
It builds `victoria-metrics-pure` binary and puts it into the `bin` folder.
|
||||
|
||||
@@ -669,7 +744,7 @@ The delete API is intended mainly for the following cases:
|
||||
* One-off deleting of accidentally written invalid (or undesired) time series.
|
||||
* One-off deleting of user data due to [GDPR](https://en.wikipedia.org/wiki/General_Data_Protection_Regulation).
|
||||
|
||||
It isn't recommended using delete API for the following cases, since it brings non-zero overhead:
|
||||
Using the delete API is not recommended in the following cases, since it brings a non-zero overhead:
|
||||
|
||||
* Regular cleanups for unneeded data. Just prevent writing unneeded data into VictoriaMetrics.
|
||||
This can be done with [relabeling](#relabeling).
|
||||
@@ -678,7 +753,7 @@ It isn't recommended using delete API for the following cases, since it brings n
|
||||
time series occupy disk space until the next merge operation, which can never occur when deleting too old data.
|
||||
[Forced merge](#forced-merge) may be used for freeing up disk space occupied by old data.
|
||||
|
||||
It is better using `-retentionPeriod` command-line flag for efficient pruning of old data.
|
||||
It's better to use the `-retentionPeriod` command-line flag for efficient pruning of old data.
|
||||
|
||||
|
||||
## Forced merge
|
||||
@@ -700,33 +775,10 @@ when new data is ingested into it.
|
||||
|
||||
VictoriaMetrics provides the following handlers for exporting data:
|
||||
|
||||
* `/api/v1/export/native` for exporting data in native binary format. This is the most efficient format for data export.
|
||||
See [these docs](#how-to-export-data-in-native-format) for details.
|
||||
* `/api/v1/export` for exporing data in JSON line format. See [these docs](#how-to-export-data-in-json-line-format) for details.
|
||||
* `/api/v1/export/csv` for exporting data in CSV. See [these docs](#how-to-export-csv-data) for details.
|
||||
|
||||
|
||||
### How to export data in native format
|
||||
|
||||
Send a request to `http://<victoriametrics-addr>:8428/api/v1/export/native?match[]=<timeseries_selector_for_export>`,
|
||||
where `<timeseries_selector_for_export>` may contain any [time series selector](https://prometheus.io/docs/prometheus/latest/querying/basics/#time-series-selectors)
|
||||
for metrics to export. Use `{__name__=~".*"}` selector for fetching all the time series.
|
||||
|
||||
On large databases you may experience problems with limit on unique timeseries (default value is 300000). In this case you need to adjust `-search.maxUniqueTimeseries` parameter:
|
||||
|
||||
```bash
|
||||
# count unique timeseries in database
|
||||
wget -O- -q 'http://your_victoriametrics_instance:8428/api/v1/series/count' | jq '.data[0]'
|
||||
|
||||
# relaunch victoriametrics with search.maxUniqueTimeseries more than value from previous command
|
||||
```
|
||||
|
||||
Optional `start` and `end` args may be added to the request in order to limit the time frame for the exported data. These args may contain either
|
||||
unix timestamp in seconds or [RFC3339](https://www.ietf.org/rfc/rfc3339.txt) values.
|
||||
|
||||
The exported data can be imported to VictoriaMetrics via [/api/v1/import/native](#how-to-import-data-in-native-format).
|
||||
The native export format may change in incompatible way between VictoriaMetrics releases, so the data exported from the release X
|
||||
can fail to be imported into VictoriaMetrics release Y.
|
||||
* `/api/v1/export/native` for exporting data in native binary format. This is the most efficient format for data export.
|
||||
See [these docs](#how-to-export-data-in-native-format) for details.
|
||||
|
||||
|
||||
### How to export data in JSON line format
|
||||
@@ -747,7 +799,7 @@ unix timestamp in seconds or [RFC3339](https://www.ietf.org/rfc/rfc3339.txt) val
|
||||
|
||||
Optional `max_rows_per_line` arg may be added to the request for limiting the maximum number of rows exported per each JSON line.
|
||||
Optional `reduce_mem_usage=1` arg may be added to the request for reducing memory usage when exporting big number of time series.
|
||||
In this case the output may contain multiple lines with distinct samples for the same time series.
|
||||
In this case the output may contain multiple lines with samples for the same time series.
|
||||
|
||||
Pass `Accept-Encoding: gzip` HTTP header in the request to `/api/v1/export` in order to reduce network bandwidth during exporing big amounts
|
||||
of time series data. This enables gzip compression for the exported data. Example for exporting gzipped data:
|
||||
@@ -760,6 +812,9 @@ The maximum duration for each request to `/api/v1/export` is limited by `-search
|
||||
|
||||
Exported data can be imported via POST'ing it to [/api/v1/import](#how-to-import-data-in-json-line-format).
|
||||
|
||||
The [deduplication](#deduplication) is applied to the data exported via `/api/v1/export` by default. The deduplication
|
||||
isn't applied if `reduce_mem_usage=1` query arg is passed to the request.
|
||||
|
||||
|
||||
### How to export CSV data
|
||||
|
||||
@@ -784,12 +839,40 @@ unix timestamp in seconds or [RFC3339](https://www.ietf.org/rfc/rfc3339.txt) val
|
||||
|
||||
The exported CSV data can be imported to VictoriaMetrics via [/api/v1/import/csv](#how-to-import-csv-data).
|
||||
|
||||
The [deduplication](#deduplication) is applied for the data exported in CSV by default. It is possible to export raw data without de-duplication by passing `reduce_mem_usage=1` query arg to `/api/v1/export/csv`.
|
||||
|
||||
|
||||
### How to export data in native format
|
||||
|
||||
Send a request to `http://<victoriametrics-addr>:8428/api/v1/export/native?match[]=<timeseries_selector_for_export>`,
|
||||
where `<timeseries_selector_for_export>` may contain any [time series selector](https://prometheus.io/docs/prometheus/latest/querying/basics/#time-series-selectors)
|
||||
for metrics to export. Use `{__name__=~".*"}` selector for fetching all the time series.
|
||||
|
||||
On large databases you may experience problems with limit on unique timeseries (default value is 300000). In this case you need to adjust `-search.maxUniqueTimeseries` parameter:
|
||||
|
||||
```bash
|
||||
# count unique timeseries in database
|
||||
wget -O- -q 'http://your_victoriametrics_instance:8428/api/v1/series/count' | jq '.data[0]'
|
||||
|
||||
# relaunch victoriametrics with search.maxUniqueTimeseries more than value from previous command
|
||||
```
|
||||
|
||||
Optional `start` and `end` args may be added to the request in order to limit the time frame for the exported data. These args may contain either
|
||||
unix timestamp in seconds or [RFC3339](https://www.ietf.org/rfc/rfc3339.txt) values.
|
||||
|
||||
The exported data can be imported to VictoriaMetrics via [/api/v1/import/native](#how-to-import-data-in-native-format).
|
||||
The native export format may change in incompatible way between VictoriaMetrics releases, so the data exported from the release X
|
||||
can fail to be imported into VictoriaMetrics release Y.
|
||||
|
||||
The [deduplication](#deduplication) isn't applied for the data exported in native format. It is expected that the de-duplication is performed during data import.
|
||||
|
||||
|
||||
## How to import time series data
|
||||
|
||||
Time series data can be imported via any supported ingestion protocol:
|
||||
Time series data can be imported into VictoriaMetrics via any supported ingestion protocol:
|
||||
|
||||
* [Prometheus remote_write API](https://prometheus.io/docs/prometheus/latest/configuration/configuration/#remote_write). See [these docs](#prometheus-setup) for details.
|
||||
* DataDog `submit metrics` API. See [these docs](#how-to-send-data-from-datadog-agent) for details.
|
||||
* InfluxDB line protocol. See [these docs](#how-to-send-data-from-influxdb-compatible-agents-such-as-telegraf) for details.
|
||||
* Graphite plaintext protocol. See [these docs](#how-to-send-data-from-graphite-compatible-agents-such-as-statsd) for details.
|
||||
* OpenTSDB telnet put protocol. See [these docs](#sending-data-via-telnet-put-protocol) for details.
|
||||
@@ -802,36 +885,6 @@ Time series data can be imported via any supported ingestion protocol:
|
||||
* `/api/v1/import/prometheus` for importing data in Prometheus exposition format. See [these docs](#how-to-import-data-in-prometheus-exposition-format) for details.
|
||||
|
||||
|
||||
### How to import data in native format
|
||||
|
||||
The specification of VictoriaMetrics' native format may yet change and is not formally documented yet. So currently we do not recommend that external clients attempt to pack their own metrics in native format file.
|
||||
|
||||
If you have a native format file obtained via [/api/v1/export/native](#how-to-export-data-in-native-format) however this is the most efficient protocol for importing data in.
|
||||
|
||||
```bash
|
||||
# Export the data from <source-victoriametrics>:
|
||||
curl http://source-victoriametrics:8428/api/v1/export/native -d 'match={__name__!=""}' > exported_data.bin
|
||||
|
||||
# Import the data to <destination-victoriametrics>:
|
||||
curl -X POST http://destination-victoriametrics:8428/api/v1/import/native -T exported_data.bin
|
||||
```
|
||||
|
||||
Pass `Content-Encoding: gzip` HTTP request header to `/api/v1/import/native` for importing gzipped data:
|
||||
|
||||
```bash
|
||||
# Export gzipped data from <source-victoriametrics>:
|
||||
curl -H 'Accept-Encoding: gzip' http://source-victoriametrics:8428/api/v1/export/native -d 'match={__name__!=""}' > exported_data.bin.gz
|
||||
|
||||
# Import gzipped data to <destination-victoriametrics>:
|
||||
curl -X POST -H 'Content-Encoding: gzip' http://destination-victoriametrics:8428/api/v1/import/native -T exported_data.bin.gz
|
||||
```
|
||||
|
||||
Extra labels may be added to all the imported time series by passing `extra_label=name=value` query args.
|
||||
For example, `/api/v1/import/native?extra_label=foo=bar` would add `"foo":"bar"` label to all the imported time series.
|
||||
|
||||
Note that it could be required to flush response cache after importing historical data. See [these docs](#backfilling) for detail.
|
||||
|
||||
|
||||
### How to import data in JSON line format
|
||||
|
||||
Example for importing data obtained via [/api/v1/export](#how-to-export-data-in-json-line-format):
|
||||
@@ -862,6 +915,26 @@ Note that it could be required to flush response cache after importing historica
|
||||
VictoriaMetrics parses input JSON lines one-by-one. It loads the whole JSON line in memory, then parses it and then saves the parsed samples into persistent storage. This means that VictoriaMetrics can occupy big amounts of RAM when importing too long JSON lines. The solution is to split too long JSON lines into smaller lines. It is OK if samples for a single time series are split among multiple JSON lines.
|
||||
|
||||
|
||||
### How to import data in native format
|
||||
|
||||
The specification of VictoriaMetrics' native format may yet change and is not formally documented yet. So currently we do not recommend that external clients attempt to pack their own metrics in native format file.
|
||||
|
||||
If you have a native format file obtained via [/api/v1/export/native](#how-to-export-data-in-native-format) however this is the most efficient protocol for importing data in.
|
||||
|
||||
```bash
|
||||
# Export the data from <source-victoriametrics>:
|
||||
curl http://source-victoriametrics:8428/api/v1/export/native -d 'match={__name__!=""}' > exported_data.bin
|
||||
|
||||
# Import the data to <destination-victoriametrics>:
|
||||
curl -X POST http://destination-victoriametrics:8428/api/v1/import/native -T exported_data.bin
|
||||
```
|
||||
|
||||
Extra labels may be added to all the imported time series by passing `extra_label=name=value` query args.
|
||||
For example, `/api/v1/import/native?extra_label=foo=bar` would add `"foo":"bar"` label to all the imported time series.
|
||||
|
||||
Note that it could be required to flush response cache after importing historical data. See [these docs](#backfilling) for detail.
|
||||
|
||||
|
||||
### How to import CSV data
|
||||
|
||||
Arbitrary CSV data can be imported via `/api/v1/import/csv`. The CSV data is imported according to the provided `format` query arg.
|
||||
@@ -937,6 +1010,13 @@ It should return something like the following:
|
||||
{"metric":{"__name__":"foo","bar":"baz"},"values":[123],"timestamps":[1594370496905]}
|
||||
```
|
||||
|
||||
Pass `Content-Encoding: gzip` HTTP request header to `/api/v1/import/prometheus` for importing gzipped data:
|
||||
|
||||
```bash
|
||||
# Import gzipped data to <destination-victoriametrics>:
|
||||
curl -X POST -H 'Content-Encoding: gzip' http://destination-victoriametrics:8428/api/v1/import/prometheus -T prometheus_data.gz
|
||||
```
|
||||
|
||||
Extra labels may be added to all the imported metrics by passing `extra_label=name=value` query args.
|
||||
For example, `/api/v1/import/prometheus?extra_label=foo=bar` would add `{foo="bar"}` label to all the imported metrics.
|
||||
|
||||
@@ -955,6 +1035,7 @@ VictoriaMetrics also may scrape Prometheus targets - see [these docs](#how-to-sc
|
||||
|
||||
VictoriaMetrics supports Prometheus-compatible relabeling for all the ingested metrics if `-relabelConfig` command-line flag points
|
||||
to a file containing a list of [relabel_config](https://prometheus.io/docs/prometheus/latest/configuration/configuration/#relabel_config) entries.
|
||||
The `-relabelConfig` also can point to http or https url. For example, `-relabelConfig=https://config-server/relabel_config.yml`.
|
||||
See [this article with relabeling tips and tricks](https://valyala.medium.com/how-to-use-relabeling-in-prometheus-and-victoriametrics-8b90fc22c4b2).
|
||||
|
||||
Example contents for `-relabelConfig` file:
|
||||
@@ -988,7 +1069,7 @@ with scrape intervals exceeding `5m`.
|
||||
|
||||
VictoriaMetrics uses lower amounts of CPU, RAM and storage space on production workloads compared to competing solutions (Prometheus, Thanos, Cortex, TimescaleDB, InfluxDB, QuestDB, M3DB) according to [our case studies](https://docs.victoriametrics.com/CaseStudies.html).
|
||||
|
||||
VictoriaMetrics capacity scales linearly with the available resources. The needed amounts of CPU and RAM highly depends on the workload - the number of [active time series](https://docs.victoriametrics.com/FAQ.html#what-is-active-time-series), series [churn rate](https://docs.victoriametrics.com/FAQ.html#what-is-high-churn-rate), query types, query qps, etc. It is recommended setting up a test VictoriaMetrics for your production workload and iteratively scaling CPU and RAM resources until it becomes stable according to [troubleshooting docs](#troubleshooting). A single-node VictoriaMetrics works perfectly with the following production workload according to [our case studies](https://docs.victoriametrics.com/CaseStudies.html):
|
||||
VictoriaMetrics capacity scales linearly with the available resources. The needed amounts of CPU and RAM highly depends on the workload - the number of [active time series](https://docs.victoriametrics.com/FAQ.html#what-is-an-active-time-series), series [churn rate](https://docs.victoriametrics.com/FAQ.html#what-is-high-churn-rate), query types, query qps, etc. It is recommended setting up a test VictoriaMetrics for your production workload and iteratively scaling CPU and RAM resources until it becomes stable according to [troubleshooting docs](#troubleshooting). A single-node VictoriaMetrics works perfectly with the following production workload according to [our case studies](https://docs.victoriametrics.com/CaseStudies.html):
|
||||
|
||||
* Ingestion rate: 1.5+ million samples per second
|
||||
* Active time series: 50+ million
|
||||
@@ -1004,7 +1085,7 @@ It is recommended leaving the following amounts of spare resources:
|
||||
|
||||
* 50% of free RAM for reducing the probability of OOM (out of memory) crashes and slowdowns during temporary spikes in workload.
|
||||
* 50% of spare CPU for reducing the probability of slowdowns during temporary spikes in workload.
|
||||
* At least 30% of free storage space at the directory pointed by `-storageDataPath` command-line flag.
|
||||
* At least 30% of free storage space at the directory pointed by `-storageDataPath` command-line flag. See also `-storage.minFreeDiskSpaceBytes` command-line flag description [here](#list-of-command-line-flags).
|
||||
|
||||
|
||||
## High availability
|
||||
@@ -1050,9 +1131,9 @@ with the enabled de-duplication. See [this section](#deduplication) for details.
|
||||
|
||||
## Deduplication
|
||||
|
||||
VictoriaMetrics de-duplicates data points if `-dedup.minScrapeInterval` command-line flag
|
||||
is set to positive duration. For example, `-dedup.minScrapeInterval=60s` would de-duplicate data points
|
||||
on the same time series if they fall within the same discrete 60s bucket. The earliest data point will be kept. In the case of equal timestamps, an arbitrary data point will be kept.
|
||||
VictoriaMetrics de-duplicates data points if `-dedup.minScrapeInterval` command-line flag is set to positive duration. For example, `-dedup.minScrapeInterval=60s` would de-duplicate data points on the same time series if they fall within the same discrete 60s bucket. The earliest data point will be kept. In the case of equal timestamps, an arbitrary data point will be kept. See [this comment](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/2112#issuecomment-1032587618) for more details on how downsampling works.
|
||||
|
||||
The `-dedup.minScrapeInterval=D` is equivalent to `-downsampling.period=0s:D` if [downsampling](#downsampling) is enabled. It is safe to use deduplication and downsampling simultaneously.
|
||||
|
||||
The recommended value for `-dedup.minScrapeInterval` must equal to `scrape_interval` config from Prometheus configs. It is recommended to have a single `scrape_interval` across all the scrape targets. See [this article](https://www.robustperception.io/keep-it-simple-scrape_interval-id) for details.
|
||||
|
||||
@@ -1061,24 +1142,63 @@ write data to the same VictoriaMetrics instance. These vmagent or Prometheus ins
|
||||
`external_labels` section in their configs, so they write data to the same time series.
|
||||
|
||||
|
||||
## Storage
|
||||
|
||||
VictoriaMetrics stores time series data in [MergeTree](https://en.wikipedia.org/wiki/Log-structured_merge-tree)-like
|
||||
data structures. On insert, VictoriaMetrics accumulates up to 1s of data and dumps it on disk to
|
||||
`<-storageDataPath>/data/small/YYYY_MM/` subdirectory forming a `part` with the following
|
||||
name pattern: `rowsCount_blocksCount_minTimestamp_maxTimestamp`. Each part consists of two "columns":
|
||||
values and timestamps. These are sorted and compressed raw time series values. Additionally, part contains
|
||||
index files for searching for specific series in the values and timestamps files.
|
||||
|
||||
`Parts` are periodically merged into the bigger parts. The resulting `part` is constructed
|
||||
under `<-storageDataPath>/data/{small,big}/YYYY_MM/tmp` subdirectory. When the resulting `part` is complete, it is atomically moved from the `tmp`
|
||||
to its own subdirectory, while the source parts are atomically removed. The end result is that the source
|
||||
parts are substituted by a single resulting bigger `part` in the `<-storageDataPath>/data/{small,big}/YYYY_MM/` directory.
|
||||
Information about merging process is available in [single-node VictoriaMetrics](https://grafana.com/dashboards/10229)
|
||||
and [clustered VictoriaMetrics](https://grafana.com/grafana/dashboards/11176) Grafana dashboards.
|
||||
See more details in [monitoring docs](#monitoring).
|
||||
|
||||
The `merge` process is usually named "compaction", because the resulting `part` size is usually smaller than
|
||||
the sum of the source `parts`. There are following benefits of doing the merge process:
|
||||
* it improves query performance, since lower number of `parts` are inspected with each query;
|
||||
* it reduces the number of data files, since each `part`contains fixed number of files;
|
||||
* better compression rate for the resulting part.
|
||||
|
||||
Newly added `parts` either appear in the storage or fail to appear.
|
||||
Storage never contains partially created parts. The same applies to merge process — `parts` are either fully
|
||||
merged into a new `part` or fail to merge. There are no partially merged `parts` in MergeTree.
|
||||
`Part` contents in MergeTree never change. Parts are immutable. They may be only deleted after the merge
|
||||
to a bigger `part` or when the `part` contents goes outside the configured `-retentionPeriod`.
|
||||
|
||||
See [this article](https://valyala.medium.com/how-victoriametrics-makes-instant-snapshots-for-multi-terabyte-time-series-data-e1f3fb0e0282) for more details.
|
||||
|
||||
See also [how to work with snapshots](#how-to-work-with-snapshots).
|
||||
|
||||
## Retention
|
||||
|
||||
Retention is configured with `-retentionPeriod` command-line flag. For instance, `-retentionPeriod=3` means
|
||||
that the data will be stored for 3 months and then deleted.
|
||||
Data is split in per-month subdirectories inside `<-storageDataPath>/data/small` and `<-storageDataPath>/data/big` folders.
|
||||
Directories for months outside the configured retention are deleted on the first day of new month.
|
||||
In order to keep data according to `-retentionPeriod` max disk space usage is going to be `-retentionPeriod` + 1 month.
|
||||
For example if `-retentionPeriod` is set to 1, data for January is deleted on March 1st.
|
||||
It is safe to extend `-retentionPeriod` on existing data. If `-retentionPeriod` is set to lower
|
||||
value than before then data outside the configured period will be eventually deleted.
|
||||
Retention is configured with the `-retentionPeriod` command-line flag, which takes a number followed by a time unit character - `h(ours)`, `d(ays)`, `w(eeks)`, `y(ears)`. If the time unit is not specified, a month is assumed. For instance, `-retentionPeriod=3` means that the data will be stored for 3 months and then deleted. The default retention period is one month.
|
||||
|
||||
VictoriaMetrics supports retention smaller than 1 month. For example, `-retentionPeriod=5d` would set data retention for 5 days.
|
||||
Older data is eventually deleted during [background merge](https://medium.com/@valyala/how-victoriametrics-makes-instant-snapshots-for-multi-terabyte-time-series-data-e1f3fb0e0282).
|
||||
Data is split in per-month partitions inside `<-storageDataPath>/data/{small,big}` folders.
|
||||
Data partitions outside the configured retention are deleted on the first day of the new month.
|
||||
Each partition consists of one or more data parts with the following name pattern `rowsCount_blocksCount_minTimestamp_maxTimestamp`.
|
||||
Data parts outside of the configured retention are eventually deleted during
|
||||
[background merge](https://medium.com/@valyala/how-victoriametrics-makes-instant-snapshots-for-multi-terabyte-time-series-data-e1f3fb0e0282).
|
||||
|
||||
The maximum disk space usage for a given `-retentionPeriod` is going to be (`-retentionPeriod` + 1) months.
|
||||
For example, if `-retentionPeriod` is set to 1, data for January is deleted on March 1st.
|
||||
|
||||
Please note, the time range covered by data part is not limited by retention period unit. Hence, data part may contain data
|
||||
for multiple days and will be deleted only when fully outside of the configured retention.
|
||||
|
||||
It is safe to extend `-retentionPeriod` on existing data. If `-retentionPeriod` is set to a lower
|
||||
value than before, then data outside the configured period will be eventually deleted.
|
||||
|
||||
VictoriaMetrics does not support indefinite retention, but you can specify an arbitrarily high duration, e.g. `-retentionPeriod=100y`.
|
||||
|
||||
## Multiple retentions
|
||||
|
||||
Just start multiple VictoriaMetrics instances with distinct values for the following flags:
|
||||
A single instance of VictoriaMetrics supports only a single retention, which can be configured via `-retentionPeriod` command-line flag. If you need multiple retentions, then you may start multiple VictoriaMetrics instances with distinct values for the following flags:
|
||||
|
||||
* `-retentionPeriod`
|
||||
* `-storageDataPath`, so the data for each retention period is saved in a separate directory
|
||||
@@ -1087,24 +1207,20 @@ Just start multiple VictoriaMetrics instances with distinct values for the follo
|
||||
Then set up [vmauth](https://docs.victoriametrics.com/vmauth.html) in front of VictoriaMetrics instances,
|
||||
so it could route requests from particular user to VictoriaMetrics with the desired retention.
|
||||
The same scheme could be implemented for multiple tenants in [VictoriaMetrics cluster](https://docs.victoriametrics.com/Cluster-VictoriaMetrics.html).
|
||||
See [these docs](https://docs.victoriametrics.com/guides/guide-vmcluster-multiple-retention-setup.html) for multi-retention setup details.
|
||||
|
||||
|
||||
## Downsampling
|
||||
|
||||
There is no downsampling support at the moment, but:
|
||||
[VictoriaMetrics Enterprise](https://victoriametrics.com/products/enterprise/) supports multi-level downsampling with `-downsampling.period` command-line flag. For example:
|
||||
|
||||
* VictoriaMetrics is optimized for querying big amounts of raw data. See benchmark results for heavy queries
|
||||
in [this article](https://medium.com/@valyala/measuring-vertical-scalability-for-time-series-databases-in-google-cloud-92550d78d8ae).
|
||||
* VictoriaMetrics has good compression for on-disk data. See [this article](https://medium.com/@valyala/victoriametrics-achieving-better-compression-for-time-series-data-than-gorilla-317bc1f95932)
|
||||
for details.
|
||||
* The downsampling doesn't improve query performance on a long time range if the time range contains big number of time series due to [high churn rate](https://docs.victoriametrics.com/FAQ.html#what-is-high-churn-rate). The query performance depends on the number of unique time series on the selected time range, while downsampling doesn't reduce the number of unique time series in the database - it can reduce only the number of samples per each time series.
|
||||
* `-downsampling.period=30d:5m` instructs VictoriaMetrics to [deduplicate](#deduplication) samples older than 30 days with 5 minutes interval.
|
||||
|
||||
These properties reduce the need of downsampling. We plan to implement downsampling in the future.
|
||||
See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/36) for details.
|
||||
* `-downsampling.period=30d:5m,180d:1h` instructs VictoriaMetrics to deduplicate samples older than 30 days with 5 minutes interval and to deduplicate samples older than 180 days with 1 hour interval.
|
||||
|
||||
It is possible to (ab)use [-dedup.minScrapeInterval](#deduplication) for basic downsampling.
|
||||
For instance, if interval between the ingested data points is 15s, then `-dedup.minScrapeInterval=5m` will leave
|
||||
only a single data point out of 20 initial data points per each 5m interval.
|
||||
Downsampling is applied independently per each time series. It can reduce disk space usage and improve query performance if it is applied to time series with big number of samples per each series. The downsampling doesn't improve query performance if the database contains big number of time series with small number of samples per each series (aka [high churn rate](https://docs.victoriametrics.com/FAQ.html#what-is-high-churn-rate)), since downsampling doesn't reduce the number of time series. So the majority of time is spent on searching for the matching time series. It is possible to use recording rules in [vmalert](https://docs.victoriametrics.com/vmalert.html) in order to reduce the number of time series. See [these docs](https://docs.victoriametrics.com/vmalert.html#downsampling-and-aggregation-via-vmalert).
|
||||
|
||||
The downsampling can be evaluated for free by downloading and using enterprise binaries from [the releases page](https://github.com/VictoriaMetrics/VictoriaMetrics/releases).
|
||||
|
||||
|
||||
## Multi-tenancy
|
||||
@@ -1146,6 +1262,8 @@ Consider setting the following command-line flags:
|
||||
* `-snapshotAuthKey` for protecting `/snapshot*` endpoints. See [how to work with snapshots](#how-to-work-with-snapshots).
|
||||
* `-forceMergeAuthKey` for protecting `/internal/force_merge` endpoint. See [force merge docs](#forced-merge).
|
||||
* `-search.resetCacheAuthKey` for protecting `/internal/resetRollupResultCache` endpoint. See [backfilling](#backfilling) for more details.
|
||||
* `-configAuthKey` for protecting `/config` endpoint, since it may contain sensitive information such as passwords.
|
||||
- `-pprofAuthKey` for protecting `/debug/pprof/*` endpoints, which can be used for [profiling](#profiling).
|
||||
|
||||
Explicitly set internal network interface for TCP and UDP ports for data ingestion with Graphite and OpenTSDB formats.
|
||||
For example, substitute `-graphiteListenAddr=:2003` with `-graphiteListenAddr=<internal_iface_ip>:2003`.
|
||||
@@ -1181,15 +1299,16 @@ or Prometheus by adding the corresponding scrape config to it.
|
||||
Alternatively they can be self-scraped by setting `-selfScrapeInterval` command-line flag to duration greater than 0.
|
||||
For example, `-selfScrapeInterval=10s` would enable self-scraping of `/metrics` page with 10 seconds interval.
|
||||
|
||||
There are officials Grafana dashboards for [single-node VictoriaMetrics](https://grafana.com/dashboards/10229) and [clustered VictoriaMetrics](https://grafana.com/grafana/dashboards/11176).
|
||||
There is also an [alternative dashboard for clustered VictoriaMetrics](https://grafana.com/grafana/dashboards/11831).
|
||||
There are officials Grafana dashboards for [single-node VictoriaMetrics](https://grafana.com/dashboards/10229) and [clustered VictoriaMetrics](https://grafana.com/grafana/dashboards/11176). There is also an [alternative dashboard for clustered VictoriaMetrics](https://grafana.com/grafana/dashboards/11831).
|
||||
|
||||
Graphs on these dashboard contain useful hints - hover the `i` icon at the top left corner of each graph in order to read it.
|
||||
|
||||
It is recommended setting up alerts in [vmalert](https://docs.victoriametrics.com/vmalert.html) or in Prometheus from [this config](https://github.com/VictoriaMetrics/VictoriaMetrics/blob/master/deployment/docker/alerts.yml).
|
||||
|
||||
The most interesting metrics are:
|
||||
|
||||
* `vm_cache_entries{type="storage/hour_metric_ids"}` - the number of time series with new data points during the last hour
|
||||
aka [active time series](https://docs.victoriametrics.com/FAQ.html#what-is-active-time-series).
|
||||
aka [active time series](https://docs.victoriametrics.com/FAQ.html#what-is-an-active-time-series).
|
||||
* `increase(vm_new_timeseries_created_total[1h])` - time series [churn rate](https://docs.victoriametrics.com/FAQ.html#what-is-high-churn-rate) during the previous hour.
|
||||
* `sum(vm_rows{type=~"storage/.*"})` - total number of `(timestamp, value)` data points in the database.
|
||||
* `sum(rate(vm_rows_inserted_total[5m]))` - ingestion rate, i.e. how many samples are inserted int the database per second.
|
||||
@@ -1197,10 +1316,10 @@ The most interesting metrics are:
|
||||
* `sum(vm_data_size_bytes)` - the total size of data on disk.
|
||||
* `increase(vm_slow_row_inserts_total[5m])` - the number of slow inserts during the last 5 minutes.
|
||||
If this number remains high during extended periods of time, then it is likely more RAM is needed for optimal handling
|
||||
of the current number of [active time series](https://docs.victoriametrics.com/FAQ.html#what-is-active-time-series).
|
||||
of the current number of [active time series](https://docs.victoriametrics.com/FAQ.html#what-is-an-active-time-series).
|
||||
* `increase(vm_slow_metric_name_loads_total[5m])` - the number of slow loads of metric names during the last 5 minutes.
|
||||
If this number remains high during extended periods of time, then it is likely more RAM is needed for optimal handling
|
||||
of the current number of [active time series](https://docs.victoriametrics.com/FAQ.html#what-is-active-time-series).
|
||||
of the current number of [active time series](https://docs.victoriametrics.com/FAQ.html#what-is-an-active-time-series).
|
||||
|
||||
VictoriaMetrics also exposes currently running queries with their execution times at `/api/v1/status/active_queries` page.
|
||||
|
||||
@@ -1220,7 +1339,7 @@ VictoriaMetrics returns TSDB stats at `/api/v1/status/tsdb` page in the way simi
|
||||
|
||||
By default VictoriaMetrics doesn't limit the number of stored time series. The limit can be enforced by setting the following command-line flags:
|
||||
|
||||
* `-storage.maxHourlySeries` - limits the number of time series that can be added during the last hour. Useful for limiting the number of [active time series](https://docs.victoriametrics.com/FAQ.html#what-is-active-time-series).
|
||||
* `-storage.maxHourlySeries` - limits the number of time series that can be added during the last hour. Useful for limiting the number of [active time series](https://docs.victoriametrics.com/FAQ.html#what-is-an-active-time-series).
|
||||
* `-storage.maxDailySeries` - limits the number of time series that can be added during the last day. Useful for limiting daily [churn rate](https://docs.victoriametrics.com/FAQ.html#what-is-high-churn-rate).
|
||||
|
||||
Both limits can be set simultaneously. If any of these limits is reached, then incoming samples for new time series are dropped. A sample of dropped series is put in the log with `WARNING` level.
|
||||
@@ -1264,7 +1383,7 @@ See also more advanced [cardinality limiter in vmagent](https://docs.victoriamet
|
||||
See [this article for technical details](https://valyala.medium.com/wal-usage-looks-broken-in-modern-time-series-databases-b62a627ab704).
|
||||
|
||||
* If VictoriaMetrics works slowly and eats more than a CPU core per 100K ingested data points per second,
|
||||
then it is likely you have too many [active time series](https://docs.victoriametrics.com/FAQ.html#what-is-active-time-series) for the current amount of RAM.
|
||||
then it is likely you have too many [active time series](https://docs.victoriametrics.com/FAQ.html#what-is-an-active-time-series) for the current amount of RAM.
|
||||
VictoriaMetrics [exposes](#monitoring) `vm_slow_*` metrics such as `vm_slow_row_inserts_total` and `vm_slow_metric_name_loads_total`, which could be used
|
||||
as an indicator of low amounts of RAM. It is recommended increasing the amount of RAM on the node with VictoriaMetrics in order to improve
|
||||
ingestion and query performance in this case.
|
||||
@@ -1299,8 +1418,7 @@ See also more advanced [cardinality limiter in vmagent](https://docs.victoriamet
|
||||
This prevents from ingesting metrics with too many labels. It is recommended [monitoring](#monitoring) `vm_metrics_with_dropped_labels_total`
|
||||
metric in order to determine whether `-maxLabelsPerTimeseries` must be adjusted for your workload.
|
||||
|
||||
* If you store Graphite metrics like `foo.bar.baz` in VictoriaMetrics, then use `{__graphite__="foo.*.baz"}` syntax for selecting such metrics.
|
||||
This expression is equivalent to `{__name__=~"foo[.][^.]*[.]baz"}`, but it works faster and it is easier to use when migrating from Graphite.
|
||||
* If you store Graphite metrics like `foo.bar.baz` in VictoriaMetrics, then `{__graphite__="foo.*.baz"}` filter can be used for selecting such metrics. See [these docs](#selecting-graphite-metrics) for details.
|
||||
|
||||
* VictoriaMetrics ignores `NaN` values during data ingestion.
|
||||
|
||||
@@ -1310,8 +1428,54 @@ See also more advanced [cardinality limiter in vmagent](https://docs.victoriamet
|
||||
VictoriaMetrics uses various internal caches. These caches are stored to `<-storageDataPath>/cache` directory during graceful shutdown (e.g. when VictoriaMetrics is stopped by sending `SIGINT` signal). The caches are read on the next VictoriaMetrics startup. Sometimes it is needed to remove such caches on the next startup. This can be performed by placing `reset_cache_on_startup` file inside the `<-storageDataPath>/cache` directory before the restart of VictoriaMetrics. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1447) for details.
|
||||
|
||||
|
||||
## Cache tuning
|
||||
|
||||
VictoriaMetrics uses various in-memory caches for faster data ingestion and query performance.
|
||||
The following metrics for each type of cache are exported at [`/metrics` page](#monitoring):
|
||||
- `vm_cache_size_bytes` - the actual cache size
|
||||
- `vm_cache_size_max_bytes` - cache size limit
|
||||
- `vm_cache_requests_total` - the number of requests to the cache
|
||||
- `vm_cache_misses_total` - the number of cache misses
|
||||
- `vm_cache_entries` - the number of entries in the cache
|
||||
|
||||
Both Grafana dashboards for [single-node VictoriaMetrics](https://grafana.com/dashboards/10229)
|
||||
and [clustered VictoriaMetrics](https://grafana.com/grafana/dashboards/11176)
|
||||
contain `Caches` section with cache metrics visualized. The panels show the current
|
||||
memory usage by each type of cache, and also a cache hit rate. If hit rate is close to 100%
|
||||
then cache efficiency is already very high and does not need any tuning.
|
||||
The panel `Cache usage %` in `Troubleshooting` section shows the percentage of used cache size
|
||||
from the allowed size by type. If the percentage is below 100%, then no further tuning needed.
|
||||
|
||||
Please note, default cache sizes were carefully adjusted accordingly to the most
|
||||
practical scenarios and workloads. Change the defaults only if you understand the implications.
|
||||
|
||||
To override the default values see command-line flags with `-storage.cacheSize` prefix.
|
||||
See the full description of flags [here](#list-of-command-line-flags).
|
||||
|
||||
|
||||
## Data migration
|
||||
|
||||
### From VictoriaMetrics
|
||||
|
||||
The simplest way to migrate data from one single-node (source) to another (destination), or from one vmstorage node
|
||||
to another do the following:
|
||||
1. Stop the VictoriaMetrics (source) with `kill -INT`;
|
||||
2. Copy (via [rsync](https://en.wikipedia.org/wiki/Rsync) or any other tool) the entire folder specified
|
||||
via `-storageDataPath` from the source node to the empty folder at the destination node.
|
||||
3. Once copy is done, stop the VictoriaMetrics (destination) with `kill -INT` and verify that
|
||||
its `-storageDataPath` points to the copied folder from p.2;
|
||||
4. Start the VictoriaMetrics (destination). The copied data should be now available.
|
||||
|
||||
Things to consider when copying data:
|
||||
1. Data formats between single-node and vmstorage node aren't compatible and can't be copied.
|
||||
2. Copying data folder means complete replacement of the previous data on destination VictoriaMetrics.
|
||||
|
||||
For more complex scenarios like single-to-cluster, cluster-to-single, re-sharding or migrating only a fraction
|
||||
of data - see [vmctl. Migrating data from VictoriaMetrics](https://docs.victoriametrics.com/vmctl.html#migrating-data-from-victoriametrics).
|
||||
|
||||
|
||||
### From other systems
|
||||
|
||||
Use [vmctl](https://docs.victoriametrics.com/vmctl.html) for data migration. It supports the following data migration types:
|
||||
|
||||
* From Prometheus to VictoriaMetrics
|
||||
@@ -1325,6 +1489,7 @@ See [vmctl docs](https://docs.victoriametrics.com/vmctl.html) for more details.
|
||||
## Backfilling
|
||||
|
||||
VictoriaMetrics accepts historical data in arbitrary order of time via [any supported ingestion method](#how-to-import-time-series-data).
|
||||
See [how to backfill data with recording rules in vmalert](https://docs.victoriametrics.com/vmalert.html#rules-backfilling).
|
||||
Make sure that configured `-retentionPeriod` covers timestamps for the backfilled data.
|
||||
|
||||
It is recommended disabling query cache with `-search.disableCache` command-line flag when writing
|
||||
@@ -1343,7 +1508,7 @@ cache when samples with timestamps older than `now - search.cacheTimestampOffset
|
||||
|
||||
VictoriaMetrics doesn't support updating already existing sample values to new ones. It stores all the ingested data points
|
||||
for the same time series with identical timestamps. While it is possible substituting old time series with new time series via
|
||||
[removal of old time series](#how-to-delete-timeseries) and then [writing new time series](#backfilling), this approach
|
||||
[removal of old time series](#how-to-delete-time-series) and then [writing new time series](#backfilling), this approach
|
||||
should be used only for one-off updates. It shouldn't be used for frequent updates because of non-zero overhead related to data removal.
|
||||
|
||||
|
||||
@@ -1365,22 +1530,42 @@ We also provide [vmbackupmanager](https://docs.victoriametrics.com/vmbackupmanag
|
||||
Enterprise binaries can be downloaded and evaluated for free from [the releases page](https://github.com/VictoriaMetrics/VictoriaMetrics/releases).
|
||||
|
||||
|
||||
## Benchmarks
|
||||
|
||||
Note, that vendors (including VictoriaMetrics) are often biased when doing such tests. E.g. they try highlighting
|
||||
the best parts of their product, while highlighting the worst parts of competing products.
|
||||
So we encourage users and all independent third parties to conduct their becnhmarks for various products
|
||||
they are evaluating in production and publish the results.
|
||||
|
||||
As a reference, please see [benchmarks](https://docs.victoriametrics.com/Articles.html#benchmarks) conducted by
|
||||
VictoriaMetrics team. Please also see the [helm chart](https://github.com/VictoriaMetrics/benchmark)
|
||||
for running ingestion benchmarks based on node_exporter metrics.
|
||||
|
||||
|
||||
## Profiling
|
||||
|
||||
VictoriaMetrics provides handlers for collecting the following [Go profiles](https://blog.golang.org/profiling-go-programs):
|
||||
|
||||
* Memory profile. It can be collected with the following command:
|
||||
* Memory profile. It can be collected with the following command (replace `0.0.0.0` with hostname if needed):
|
||||
|
||||
<div class="with-copy" markdown="1">
|
||||
|
||||
```bash
|
||||
curl -s http://<victoria-metrics-host>:8428/debug/pprof/heap > mem.pprof
|
||||
curl http://0.0.0.0:8428/debug/pprof/heap > mem.pprof
|
||||
```
|
||||
|
||||
* CPU profile. It can be collected with the following command:
|
||||
</div>
|
||||
|
||||
* CPU profile. It can be collected with the following command (replace `0.0.0.0` with hostname if needed):
|
||||
|
||||
<div class="with-copy" markdown="1">
|
||||
|
||||
```bash
|
||||
curl -s http://<victoria-metrics-host>:8428/debug/pprof/profile > cpu.pprof
|
||||
curl http://0.0.0.0:8428/debug/pprof/profile > cpu.pprof
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
The command for collecting CPU profile waits for 30 seconds before returning.
|
||||
|
||||
The collected profiles may be analyzed with [go tool pprof](https://github.com/google/pprof).
|
||||
@@ -1418,9 +1603,11 @@ Contact us with any questions regarding VictoriaMetrics at [info@victoriametrics
|
||||
Feel free asking any questions regarding VictoriaMetrics:
|
||||
|
||||
* [slack](https://slack.victoriametrics.com/)
|
||||
* [linkedin](https://www.linkedin.com/company/victoriametrics/)
|
||||
* [reddit](https://www.reddit.com/r/VictoriaMetrics/)
|
||||
* [telegram-en](https://t.me/VictoriaMetrics_en)
|
||||
* [telegram-ru](https://t.me/VictoriaMetrics_ru1)
|
||||
* [articles and talks about VictoriaMetrics in Russian](https://github.com/denisgolius/victoriametrics-ru-links)
|
||||
* [google groups](https://groups.google.com/forum/#!forum/victorametrics-users)
|
||||
|
||||
If you like VictoriaMetrics and want to contribute, then we need the following:
|
||||
@@ -1447,7 +1634,7 @@ Report bugs and propose new features [here](https://github.com/VictoriaMetrics/V
|
||||
|
||||
## VictoriaMetrics Logo
|
||||
|
||||
[Zip](VM_logo.zip) contains three folders with different image orientations (main color and inverted version).
|
||||
[Zip](https://github.com/VictoriaMetrics/VictoriaMetrics/blob/master/VM_logo.zip) contains three folders with different image orientations (main color and inverted version).
|
||||
|
||||
Files included in each folder:
|
||||
|
||||
@@ -1482,22 +1669,32 @@ Pass `-help` to VictoriaMetrics in order to see the list of supported command-li
|
||||
```
|
||||
-bigMergeConcurrency int
|
||||
The maximum number of CPU cores to use for big merges. Default value is used if set to 0
|
||||
-configAuthKey string
|
||||
Authorization key for accessing /config page. It must be passed via authKey query arg
|
||||
-csvTrimTimestamp duration
|
||||
Trim timestamps when importing csv data to this duration. Minimum practical duration is 1ms. Higher duration (i.e. 1s) may be used for reducing disk space usage for timestamp data (default 1ms)
|
||||
-datadog.maxInsertRequestSize size
|
||||
The maximum size in bytes of a single DataDog POST request to /api/v1/series
|
||||
Supports the following optional suffixes for size values: KB, MB, GB, KiB, MiB, GiB (default 67108864)
|
||||
-dedup.minScrapeInterval duration
|
||||
Leave only the first sample in every time series per each discrete interval equal to -dedup.minScrapeInterval > 0. See https://docs.victoriametrics.com/#deduplication for details
|
||||
Leave only the first sample in every time series per each discrete interval equal to -dedup.minScrapeInterval > 0. See https://docs.victoriametrics.com/#deduplication and https://docs.victoriametrics.com/#downsampling
|
||||
-deleteAuthKey string
|
||||
authKey for metrics' deletion via /api/v1/admin/tsdb/delete_series and /tags/delSeries
|
||||
-denyQueriesOutsideRetention
|
||||
Whether to deny queries outside of the configured -retentionPeriod. When set, then /api/v1/query_range would return '503 Service Unavailable' error for queries with 'from' value outside -retentionPeriod. This may be useful when multiple data sources with distinct retentions are hidden behind query-tee
|
||||
-downsampling.period array
|
||||
Comma-separated downsampling periods in the format 'offset:period'. For example, '30d:10m' instructs to leave a single sample per 10 minutes for samples older than 30 days. See https://docs.victoriametrics.com/#downsampling for details
|
||||
Supports an array of values separated by comma or specified via multiple flags.
|
||||
-dryRun
|
||||
Whether to check only -promscrape.config and then exit. Unknown config entries are allowed in -promscrape.config by default. This can be changed with -promscrape.config.strictParse
|
||||
Whether to check only -promscrape.config and then exit. Unknown config entries aren't allowed in -promscrape.config by default. This can be changed with -promscrape.config.strictParse=false command-line flag
|
||||
-enableTCP6
|
||||
Whether to enable IPv6 for listening and dialing. By default only IPv4 TCP and UDP is used
|
||||
-envflag.enable
|
||||
Whether to enable reading flags from environment variables additionally to command line. Command line flag values have priority over values from environment vars. Flags are read only from command line if this flag isn't set. See https://docs.victoriametrics.com/#environment-variables for more details
|
||||
-envflag.prefix string
|
||||
Prefix for environment variables if -envflag.enable is set
|
||||
-eula
|
||||
By specifying this flag, you confirm that you have an enterprise license and accept the EULA https://victoriametrics.com/assets/VM_EULA.pdf
|
||||
-finalMergeDelay duration
|
||||
The delay before starting final merge for per-month partition after no new data is ingested into it. Final merge may require additional disk IO and CPU resources. Final merge may increase query speed and reduce disk space usage in some cases. Zero value disables final merge
|
||||
-forceFlushAuthKey string
|
||||
@@ -1537,6 +1734,8 @@ Pass `-help` to VictoriaMetrics in order to see the list of supported command-li
|
||||
-influx.maxLineSize size
|
||||
The maximum size in bytes for a single InfluxDB line during parsing
|
||||
Supports the following optional suffixes for size values: KB, MB, GB, KiB, MiB, GiB (default 262144)
|
||||
-influxDBLabel string
|
||||
Default label for the DB name sent over '?db={db_name}' query parameter (default "db")
|
||||
-influxListenAddr string
|
||||
TCP and UDP address to listen for InfluxDB line protocol data. Usually :8189 must be set. Doesn't work if empty. This flag isn't needed when ingesting data over HTTP - just send it to http://<victoriametrics>:8428/write
|
||||
-influxMeasurementFieldSeparator string
|
||||
@@ -1570,15 +1769,17 @@ Pass `-help` to VictoriaMetrics in order to see the list of supported command-li
|
||||
-maxInsertRequestSize size
|
||||
The maximum size in bytes of a single Prometheus remote_write API request
|
||||
Supports the following optional suffixes for size values: KB, MB, GB, KiB, MiB, GiB (default 33554432)
|
||||
-maxLabelValueLen int
|
||||
The maximum length of label values in the accepted time series. Longer label values are truncated. In this case the vm_too_long_label_values_total metric at /metrics page is incremented (default 16384)
|
||||
-maxLabelsPerTimeseries int
|
||||
The maximum number of labels accepted per time series. Superfluous labels are dropped (default 30)
|
||||
The maximum number of labels accepted per time series. Superfluous labels are dropped. In this case the vm_metrics_with_dropped_labels_total metric at /metrics page is incremented (default 30)
|
||||
-memory.allowedBytes size
|
||||
Allowed size of system memory VictoriaMetrics caches may occupy. This option overrides -memory.allowedPercent if set to a non-zero value. Too low a value may increase the cache miss rate usually resulting in higher CPU and disk IO usage. Too high a value may evict too much data from OS page cache resulting in higher disk IO usage
|
||||
Supports the following optional suffixes for size values: KB, MB, GB, KiB, MiB, GiB (default 0)
|
||||
-memory.allowedPercent float
|
||||
Allowed percent of system memory VictoriaMetrics caches may occupy. See also -memory.allowedBytes. Too low a value may increase cache miss rate usually resulting in higher CPU and disk IO usage. Too high a value may evict too much data from OS page cache which will result in higher disk IO usage (default 60)
|
||||
-metricsAuthKey string
|
||||
Auth key for /metrics. It overrides httpAuth settings
|
||||
Auth key for /metrics. It must be passed via authKey query arg. It overrides httpAuth.* settings
|
||||
-opentsdbHTTPListenAddr string
|
||||
TCP address to listen for OpentTSDB HTTP put requests. Usually :4242 must be set. Doesn't work if empty
|
||||
-opentsdbListenAddr string
|
||||
@@ -1591,7 +1792,7 @@ Pass `-help` to VictoriaMetrics in order to see the list of supported command-li
|
||||
-opentsdbhttpTrimTimestamp duration
|
||||
Trim timestamps for OpenTSDB HTTP data to this duration. Minimum practical duration is 1ms. Higher duration (i.e. 1s) may be used for reducing disk space usage for timestamp data (default 1ms)
|
||||
-pprofAuthKey string
|
||||
Auth key for /debug/pprof. It overrides httpAuth settings
|
||||
Auth key for /debug/pprof. It must be passed via authKey query arg. It overrides httpAuth.* settings
|
||||
-precisionBits int
|
||||
The number of precision bits to store per each value. Lower precision bits improves data compression at the cost of precision loss (default 64)
|
||||
-promscrape.cluster.memberNum int
|
||||
@@ -1601,11 +1802,11 @@ Pass `-help` to VictoriaMetrics in order to see the list of supported command-li
|
||||
-promscrape.cluster.replicationFactor int
|
||||
The number of members in the cluster, which scrape the same targets. If the replication factor is greater than 2, then the deduplication must be enabled at remote storage side. See https://docs.victoriametrics.com/#deduplication (default 1)
|
||||
-promscrape.config string
|
||||
Optional path to Prometheus config file with 'scrape_configs' section containing targets to scrape. See https://docs.victoriametrics.com/#how-to-scrape-prometheus-exporters-such-as-node-exporter for details
|
||||
Optional path to Prometheus config file with 'scrape_configs' section containing targets to scrape. The path can point to local file and to http url. See https://docs.victoriametrics.com/#how-to-scrape-prometheus-exporters-such-as-node-exporter for details
|
||||
-promscrape.config.dryRun
|
||||
Checks -promscrape.config file for errors and unsupported fields and then exits. Returns non-zero exit code on parsing errors and emits these errors to stderr. See also -promscrape.config.strictParse command-line flag. Pass -loggerLevel=ERROR if you don't need to see info messages in the output.
|
||||
-promscrape.config.strictParse
|
||||
Whether to allow only supported fields in -promscrape.config . By default unsupported fields are silently skipped
|
||||
Whether to deny unsupported fields in -promscrape.config . Set to false in order to silently skip unsupported fields (default true)
|
||||
-promscrape.configCheckInterval duration
|
||||
Interval for checking for changes in '-promscrape.config' file. By default the checking is disabled. Send SIGHUP signal in order to force config check for changes
|
||||
-promscrape.consul.waitTime duration
|
||||
@@ -1635,7 +1836,7 @@ Pass `-help` to VictoriaMetrics in order to see the list of supported command-li
|
||||
-promscrape.eurekaSDCheckInterval duration
|
||||
Interval for checking for changes in eureka. This works only if eureka_sd_configs is configured in '-promscrape.config' file. See https://prometheus.io/docs/prometheus/latest/configuration/configuration/#eureka_sd_config for details (default 30s)
|
||||
-promscrape.fileSDCheckInterval duration
|
||||
Interval for checking for changes in 'file_sd_config'. See https://prometheus.io/docs/prometheus/latest/configuration/configuration/#file_sd_config for details (default 30s)
|
||||
Interval for checking for changes in 'file_sd_config'. See https://prometheus.io/docs/prometheus/latest/configuration/configuration/#file_sd_config for details (default 5m0s)
|
||||
-promscrape.gceSDCheckInterval duration
|
||||
Interval for checking for changes in gce. This works only if gce_sd_configs is configured in '-promscrape.config' file. See https://prometheus.io/docs/prometheus/latest/configuration/configuration/#gce_sd_config for details (default 1m0s)
|
||||
-promscrape.httpSDCheckInterval duration
|
||||
@@ -1646,11 +1847,17 @@ Pass `-help` to VictoriaMetrics in order to see the list of supported command-li
|
||||
Interval for checking for changes in Kubernetes API server. This works only if kubernetes_sd_configs is configured in '-promscrape.config' file. See https://prometheus.io/docs/prometheus/latest/configuration/configuration/#kubernetes_sd_config for details (default 30s)
|
||||
-promscrape.maxDroppedTargets int
|
||||
The maximum number of droppedTargets to show at /api/v1/targets page. Increase this value if your setup drops more scrape targets during relabeling and you need investigating labels for all the dropped targets. Note that the increased number of tracked dropped targets may result in increased memory usage (default 1000)
|
||||
-promscrape.maxResponseHeadersSize size
|
||||
The maximum size of http response headers from Prometheus scrape targets
|
||||
Supports the following optional suffixes for size values: KB, MB, GB, KiB, MiB, GiB (default 4096)
|
||||
-promscrape.maxScrapeSize size
|
||||
The maximum size of scrape response in bytes to process from Prometheus targets. Bigger responses are rejected
|
||||
Supports the following optional suffixes for size values: KB, MB, GB, KiB, MiB, GiB (default 16777216)
|
||||
-promscrape.minResponseSizeForStreamParse size
|
||||
The minimum target response size for automatic switching to stream parsing mode, which can reduce memory usage. See https://docs.victoriametrics.com/vmagent.html#stream-parsing-mode
|
||||
Supports the following optional suffixes for size values: KB, MB, GB, KiB, MiB, GiB (default 1000000)
|
||||
-promscrape.noStaleMarkers
|
||||
Whether to disable seding Prometheus stale markers for metrics when scrape target disappears. This option may reduce memory usage if stale markers aren't needed for your setup. See also https://docs.victoriametrics.com/vmagent.html#stream-parsing-mode
|
||||
Whether to disable sending Prometheus stale markers for metrics when scrape target disappears. This option may reduce memory usage if stale markers aren't needed for your setup. This option also disables populating the scrape_series_added metric. See https://prometheus.io/docs/concepts/jobs_instances/#automatically-generated-labels-and-time-series
|
||||
-promscrape.openstackSDCheckInterval duration
|
||||
Interval for checking for changes in openstack API server. This works only if openstack_sd_configs is configured in '-promscrape.config' file. See https://prometheus.io/docs/prometheus/latest/configuration/configuration/#openstack_sd_config for details (default 30s)
|
||||
-promscrape.seriesLimitPerTarget int
|
||||
@@ -1662,7 +1869,7 @@ Pass `-help` to VictoriaMetrics in order to see the list of supported command-li
|
||||
-promscrape.suppressScrapeErrors
|
||||
Whether to suppress scrape errors logging. The last error for each target is always available at '/targets' page even if scrape errors logging is suppressed
|
||||
-relabelConfig string
|
||||
Optional path to a file with relabeling rules, which are applied to all the ingested metrics. See https://docs.victoriametrics.com/#relabeling for details
|
||||
Optional path to a file with relabeling rules, which are applied to all the ingested metrics. The path can point either to local file or to http url. See https://docs.victoriametrics.com/#relabeling for details. The config is reloaded on SIGHUP signal
|
||||
-relabelDebug
|
||||
Whether to log metrics before and after relabeling with -relabelConfig. If the -relabelDebug is enabled, then the metrics aren't sent to storage. This is useful for debugging the relabeling configs
|
||||
-retentionPeriod value
|
||||
@@ -1674,6 +1881,10 @@ Pass `-help` to VictoriaMetrics in order to see the list of supported command-li
|
||||
Whether to disable automatic response cache reset if a sample with timestamp outside -search.cacheTimestampOffset is inserted into VictoriaMetrics
|
||||
-search.disableCache
|
||||
Whether to disable response caching. This may be useful during data backfilling
|
||||
-search.graphiteMaxPointsPerSeries int
|
||||
The maximum number of points per series Graphite render API can return (default 1000000)
|
||||
-search.graphiteStorageStep duration
|
||||
The interval between datapoints stored in the database. It is used at Graphite Render API handler for normalizing the interval between datapoints in case it isn't normalized. It can be overriden by sending 'storage_step' query arg to /render API or by sending the desired interval via 'Storage-Step' http header during querying /render API (default 10s)
|
||||
-search.latencyOffset duration
|
||||
The time when data points become visible in query results after the collection. Too small value can result in incomplete last points for query results (default 30s)
|
||||
-search.logSlowQueryDuration duration
|
||||
@@ -1735,18 +1946,30 @@ Pass `-help` to VictoriaMetrics in order to see the list of supported command-li
|
||||
authKey, which must be passed in query string to /snapshot* pages
|
||||
-sortLabels
|
||||
Whether to sort labels for incoming samples before writing them to storage. This may be needed for reducing memory usage at storage when the order of labels in incoming samples is random. For example, if m{k1="v1",k2="v2"} may be sent as m{k2="v2",k1="v1"}. Enabled sorting for labels can slow down ingestion performance a bit
|
||||
-storage.cacheSizeIndexDBDataBlocks size
|
||||
Overrides max size for indexdb/dataBlocks cache. See https://docs.victoriametrics.com/Single-server-VictoriaMetrics.html#cache-tuning
|
||||
Supports the following optional suffixes for size values: KB, MB, GB, KiB, MiB, GiB (default 0)
|
||||
-storage.cacheSizeIndexDBIndexBlocks size
|
||||
Overrides max size for indexdb/indexBlocks cache. See https://docs.victoriametrics.com/Single-server-VictoriaMetrics.html#cache-tuning
|
||||
Supports the following optional suffixes for size values: KB, MB, GB, KiB, MiB, GiB (default 0)
|
||||
-storage.cacheSizeStorageTSID size
|
||||
Overrides max size for storage/tsid cache. See https://docs.victoriametrics.com/Single-server-VictoriaMetrics.html#cache-tuning
|
||||
Supports the following optional suffixes for size values: KB, MB, GB, KiB, MiB, GiB (default 0)
|
||||
-storage.maxDailySeries int
|
||||
The maximum number of unique series can be added to the storage during the last 24 hours. Excess series are logged and dropped. This can be useful for limiting series churn rate. See also -storage.maxHourlySeries
|
||||
-storage.maxHourlySeries int
|
||||
The maximum number of unique series can be added to the storage during the last hour. Excess series are logged and dropped. This can be useful for limiting series cardinality. See also -storage.maxDailySeries
|
||||
-storage.minFreeDiskSpaceBytes size
|
||||
The minimum free disk space at -storageDataPath after which the storage stops accepting new data
|
||||
Supports the following optional suffixes for size values: KB, MB, GB, KiB, MiB, GiB (default 10000000)
|
||||
-storageDataPath string
|
||||
Path to storage data (default "victoria-metrics-data")
|
||||
-tls
|
||||
Whether to enable TLS (aka HTTPS) for incoming requests. -tlsCertFile and -tlsKeyFile must be set if -tls is set
|
||||
-tlsCertFile string
|
||||
Path to file with TLS certificate. Used only if -tls is set. Prefer ECDSA certs instead of RSA certs as RSA certs are slower
|
||||
Path to file with TLS certificate. Used only if -tls is set. Prefer ECDSA certs instead of RSA certs as RSA certs are slower. The provided certificate file is automatically re-read every second, so it can be dynamically updated
|
||||
-tlsKeyFile string
|
||||
Path to file with TLS key. Used only if -tls is set
|
||||
Path to file with TLS key. Used only if -tls is set. The provided key file is automatically re-read every second, so it can be dynamically updated
|
||||
-version
|
||||
Show VictoriaMetrics version
|
||||
```
|
||||
|
||||
@@ -27,6 +27,12 @@ victoria-metrics-ppc64le-prod:
|
||||
victoria-metrics-386-prod:
|
||||
APP_NAME=victoria-metrics $(MAKE) app-via-docker-386
|
||||
|
||||
victoria-metrics-darwin-amd64-prod:
|
||||
APP_NAME=victoria-metrics $(MAKE) app-via-docker-darwin-amd64
|
||||
|
||||
victoria-metrics-darwin-arm64-prod:
|
||||
APP_NAME=victoria-metrics $(MAKE) app-via-docker-darwin-arm64
|
||||
|
||||
package-victoria-metrics:
|
||||
APP_NAME=victoria-metrics $(MAKE) package-via-docker
|
||||
|
||||
|
||||
@@ -25,9 +25,9 @@ import (
|
||||
var (
|
||||
httpListenAddr = flag.String("httpListenAddr", ":8428", "TCP address to listen for http connections")
|
||||
minScrapeInterval = flag.Duration("dedup.minScrapeInterval", 0, "Leave only the first sample in every time series per each discrete interval "+
|
||||
"equal to -dedup.minScrapeInterval > 0. See https://docs.victoriametrics.com/#deduplication for details")
|
||||
"equal to -dedup.minScrapeInterval > 0. See https://docs.victoriametrics.com/#deduplication and https://docs.victoriametrics.com/#downsampling")
|
||||
dryRun = flag.Bool("dryRun", false, "Whether to check only -promscrape.config and then exit. "+
|
||||
"Unknown config entries are allowed in -promscrape.config by default. This can be changed with -promscrape.config.strictParse")
|
||||
"Unknown config entries aren't allowed in -promscrape.config by default. This can be changed with -promscrape.config.strictParse=false command-line flag")
|
||||
)
|
||||
|
||||
func main() {
|
||||
@@ -51,7 +51,7 @@ func main() {
|
||||
|
||||
logger.Infof("starting VictoriaMetrics at %q...", *httpListenAddr)
|
||||
startTime := time.Now()
|
||||
storage.SetMinScrapeIntervalForDeduplication(*minScrapeInterval)
|
||||
storage.SetDedupInterval(*minScrapeInterval)
|
||||
vmstorage.Init(promql.ResetRollupResultCacheIfNeeded)
|
||||
vmselect.Init()
|
||||
vminsert.Init()
|
||||
@@ -90,13 +90,15 @@ func requestHandler(w http.ResponseWriter, r *http.Request) bool {
|
||||
fmt.Fprintf(w, "See docs at <a href='https://docs.victoriametrics.com/'>https://docs.victoriametrics.com/</a></br>")
|
||||
fmt.Fprintf(w, "Useful endpoints:</br>")
|
||||
httpserver.WriteAPIHelp(w, [][2]string{
|
||||
{"/vmui", "Web UI"},
|
||||
{"/targets", "discovered targets list"},
|
||||
{"/api/v1/targets", "advanced information about discovered targets in JSON format"},
|
||||
{"/metrics", "available service metrics"},
|
||||
{"/api/v1/status/tsdb", "tsdb status page"},
|
||||
{"/api/v1/status/top_queries", "top queries"},
|
||||
{"/api/v1/status/active_queries", "active queries"},
|
||||
{"vmui", "Web UI"},
|
||||
{"targets", "discovered targets list"},
|
||||
{"api/v1/targets", "advanced information about discovered targets in JSON format"},
|
||||
{"config", "-promscrape.config contents"},
|
||||
{"metrics", "available service metrics"},
|
||||
{"flags", "command-line flags"},
|
||||
{"api/v1/status/tsdb", "tsdb status page"},
|
||||
{"api/v1/status/top_queries", "top queries"},
|
||||
{"api/v1/status/active_queries", "active queries"},
|
||||
})
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
["{TIME_S-110s}","3"],
|
||||
["{TIME_S-100s}","3"],
|
||||
["{TIME_S-90s}","3"],
|
||||
["{TIME_S-80s}","3"],
|
||||
["{TIME_S-60s}","2"],
|
||||
["{TIME_S-50s}","2"],
|
||||
["{TIME_S-40s}","2"],
|
||||
|
||||
@@ -27,6 +27,15 @@ vmagent-ppc64le-prod:
|
||||
vmagent-386-prod:
|
||||
APP_NAME=vmagent $(MAKE) app-via-docker-386
|
||||
|
||||
vmagent-darwin-amd64-prod:
|
||||
APP_NAME=vmagent $(MAKE) app-via-docker-darwin-amd64
|
||||
|
||||
vmagent-darwin-arm64-prod:
|
||||
APP_NAME=vmagent $(MAKE) app-via-docker-darwin-arm64
|
||||
|
||||
vmagent-windows-amd64-prod:
|
||||
APP_NAME=vmagent $(MAKE) app-via-docker-windows-amd64
|
||||
|
||||
package-vmagent:
|
||||
APP_NAME=vmagent $(MAKE) package-via-docker
|
||||
|
||||
@@ -81,6 +90,3 @@ vmagent-pure:
|
||||
|
||||
vmagent-windows-amd64:
|
||||
GOARCH=amd64 APP_NAME=vmagent $(MAKE) app-local-windows-with-goarch
|
||||
|
||||
vmagent-windows-amd64-prod:
|
||||
APP_NAME=vmagent $(MAKE) app-via-docker-windows-amd64
|
||||
|
||||
@@ -12,15 +12,17 @@ or any other Prometheus-compatible storage systems that support the `remote_writ
|
||||
While VictoriaMetrics provides an efficient solution to store and observe metrics, our users needed something fast
|
||||
and RAM friendly to scrape metrics from Prometheus-compatible exporters into VictoriaMetrics.
|
||||
Also, we found that our user's infrastructure are like snowflakes in that no two are alike. Therefore we decided to add more flexibility
|
||||
to `vmagent` such as the ability to push metrics instead of pulling them. We did our best and will continue to improve vmagent.
|
||||
to `vmagent` such as the ability to push metrics additionally to pulling them. We did our best and will continue to improve `vmagent`.
|
||||
|
||||
|
||||
## Features
|
||||
|
||||
* Can be used as a drop-in replacement for Prometheus for scraping targets such as [node_exporter](https://github.com/prometheus/node_exporter).
|
||||
See [Quick Start](#quick-start) for details.
|
||||
* Can be used as a drop-in replacement for Prometheus for scraping targets such as [node_exporter](https://github.com/prometheus/node_exporter). See [Quick Start](#quick-start) for details.
|
||||
* Can read data from Kafka. See [these docs](#reading-metrics-from-kafka).
|
||||
* Can write data to Kafka. See [these docs](#writing-metrics-to-kafka).
|
||||
* Can add, remove and modify labels (aka tags) via Prometheus relabeling. Can filter data before sending it to remote storage. See [these docs](#relabeling) for details.
|
||||
* Accepts data via all ingestion protocols supported by VictoriaMetrics:
|
||||
* DataDog "submit metrics" API. See [these docs](https://docs.victoriametrics.com/Single-server-VictoriaMetrics.html#how-to-send-data-from-datadog-agent).
|
||||
* InfluxDB line protocol via `http://<vmagent>:8429/write`. See [these docs](https://docs.victoriametrics.com/Single-server-VictoriaMetrics.html#how-to-send-data-from-influxdb-compatible-agents-such-as-telegraf).
|
||||
* Graphite plaintext protocol if `-graphiteListenAddr` command-line flag is set. See [these docs](https://docs.victoriametrics.com/Single-server-VictoriaMetrics.html#how-to-send-data-from-graphite-compatible-agents-such-as-statsd).
|
||||
* OpenTSDB telnet and http protocols if `-opentsdbListenAddr` command-line flag is set. See [these docs](https://docs.victoriametrics.com/Single-server-VictoriaMetrics.html#how-to-send-data-from-opentsdb-compatible-agents).
|
||||
@@ -44,7 +46,7 @@ to `vmagent` such as the ability to push metrics instead of pulling them. We did
|
||||
Please download `vmutils-*` archive from [releases page](https://github.com/VictoriaMetrics/VictoriaMetrics/releases), unpack it
|
||||
and configure the following flags to the `vmagent` binary in order to start scraping Prometheus targets:
|
||||
|
||||
* `-promscrape.config` with the path to Prometheus config file (usually located at `/etc/prometheus/prometheus.yml`)
|
||||
* `-promscrape.config` with the path to Prometheus config file (usually located at `/etc/prometheus/prometheus.yml`). The path can point either to local file or to http url. `vmagent` doesn't support some sections of Prometheus config file, so you may need either to delete these sections or to run `vmagent` with `-promscrape.config.strictParse=false` additional command-line flag, so `vmagent` will ignore unsupported sections. See [the list of unsupported sections](#unsupported-prometheus-config-sections).
|
||||
* `-remoteWrite.url` with the remote storage endpoint such as VictoriaMetrics, the `-remoteWrite.url` argument can be specified multiple times to replicate data concurrently to an arbitrary number of remote storage systems.
|
||||
|
||||
Example command line:
|
||||
@@ -212,15 +214,16 @@ The file pointed by `-promscrape.config` may contain `%{ENV_VAR}` placeholders w
|
||||
|
||||
## Loading scrape configs from multiple files
|
||||
|
||||
`vmagent` supports loading scrape configs from multiple files specified in the `scrape_config_files` section of `-promscrape.config` file. For example, the following `-promscrape.config` instructs `vmagent` loading scrape configs from all the `*.yml` files under `configs` directory plus a `single_scrape_config.yml` file:
|
||||
`vmagent` supports loading scrape configs from multiple files specified in the `scrape_config_files` section of `-promscrape.config` file. For example, the following `-promscrape.config` instructs `vmagent` loading scrape configs from all the `*.yml` files under `configs` directory, from `single_scrape_config.yml` local file and from `https://config-server/scrape_config.yml` url:
|
||||
|
||||
```yml
|
||||
scrape_config_files:
|
||||
- configs/*.yml
|
||||
- single_scrape_config.yml
|
||||
- https://config-server/scrape_config.yml
|
||||
```
|
||||
|
||||
Every referred file can contain arbitrary number of any [supported scrape configs](#how-to-collect-metrics-in-prometheus-format). There is no need in specifying top-level `scrape_configs` section in these files. For example:
|
||||
Every referred file can contain arbitrary number of [supported scrape configs](#how-to-collect-metrics-in-prometheus-format). There is no need in specifying top-level `scrape_configs` section in these files. For example:
|
||||
|
||||
```yml
|
||||
- job_name: foo
|
||||
@@ -234,6 +237,19 @@ Every referred file can contain arbitrary number of any [supported scrape config
|
||||
`vmagent` dynamically reloads these files on `SIGHUP` signal or on the request to `http://vmagent:8429/-/reload`.
|
||||
|
||||
|
||||
## Unsupported Prometheus config sections
|
||||
|
||||
`vmagent` doesn't support the following sections in Prometheus config file passed to `-promscrape.config` command-line flag:
|
||||
|
||||
* [remote_write](https://prometheus.io/docs/prometheus/latest/configuration/configuration/#remote_write). This section is substituted with various `-remoteWrite*` command-line flags. See [the full list of flags](#advanced-usage). The `remote_write` section isn't supported in order to reduce possible confusion when `vmagent` is used for accepting incoming metrics via push protocols such as InfluxDB, Graphite, OpenTSDB, DataDog, etc. In this case the `-promscrape.config` file isn't needed. See [these docs](#features) for details.
|
||||
* `remote_read`. This section isn't supported at all.
|
||||
* `rule_files` and `alerting`. These sections are supported by [vmalert](https://docs.victoriametrics.com/vmalert.html).
|
||||
|
||||
The list of supported service discovery types is available [here](#how-to-collect-metrics-in-prometheus-format).
|
||||
|
||||
Additionally `vmagent` doesn't support `refresh_interval` option at service discovery sections. This option is substituted with `-promscrape.*CheckInterval` command-line options, which are specific per each service discovery type. See [the full list of command-line flags for vmagent](#advanced-usage).
|
||||
|
||||
|
||||
## Adding labels to metrics
|
||||
|
||||
Labels can be added to metrics by the following mechanisms:
|
||||
@@ -248,7 +264,7 @@ Labels can be added to metrics by the following mechanisms:
|
||||
|
||||
## Relabeling
|
||||
|
||||
`vmagent` and VictoriaMetrics support Prometheus-compatible relabeling.
|
||||
VictoriaMetrics components (including `vmagent`) support Prometheus-compatible relabeling.
|
||||
They provide the following additional actions on top of actions from the [Prometheus relabeling](https://prometheus.io/docs/prometheus/latest/configuration/configuration/#relabel_config):
|
||||
|
||||
* `replace_all`: replaces all of the occurences of `regex` in the values of `source_labels` with the `replacement` and stores the results in the `target_label`.
|
||||
@@ -273,11 +289,26 @@ The `regex` value can be split into multiple lines for improved readability and
|
||||
- "foo_.+"
|
||||
```
|
||||
|
||||
VictoriaMetrics components support an optional `if` filter, which can be used for conditional relabeling. The `if` filter may contain arbitrary [time series selector](https://prometheus.io/docs/prometheus/latest/querying/basics/#time-series-selectors). For example, the following relabeling rule drops targets, which don't match `foo{bar="baz"}` series selector:
|
||||
|
||||
```yaml
|
||||
- action: keep
|
||||
if: 'foo{bar="baz"}'
|
||||
```
|
||||
|
||||
This is equivalent to less clear traditional relabeling rule:
|
||||
|
||||
```yaml
|
||||
- action: keep
|
||||
source_labels: [__name__, bar]
|
||||
regex: 'foo;baz'
|
||||
```
|
||||
|
||||
The relabeling can be defined in the following places:
|
||||
|
||||
* At the `scrape_config -> relabel_configs` section in `-promscrape.config` file. This relabeling is applied to target labels. This relabeling can be debugged by passing `relabel_debug: true` option to the corresponding `scrape_config` section. In this case `vmagent` logs target labels before and after the relabeling and then drops the logged target.
|
||||
* At the `scrape_config -> metric_relabel_configs` section in `-promscrape.config` file. This relabeling is applied to all the scraped metrics in the given `scrape_config`. This relabeling can be debugged by passing `metric_relabel_debug: true` option to the corresponding `scrape_config` section. In this case `vmagent` logs metrics before and after the relabeling and then drops the logged metrics.
|
||||
* At the `-remoteWrite.relabelConfig` file. This relabeling is aplied to all the collected metrics before sending them to remote storage. This relabeling can be debugged by passing `-remoteWrite.relabelDebug` command-line option to `vmagent`. In this case `vmagent` logs metrics before and after the relabeling and then drops all the logged metrics instead of sending them to remote storage.
|
||||
* At the `-remoteWrite.relabelConfig` file. This relabeling is applied to all the collected metrics before sending them to remote storage. This relabeling can be debugged by passing `-remoteWrite.relabelDebug` command-line option to `vmagent`. In this case `vmagent` logs metrics before and after the relabeling and then drops all the logged metrics instead of sending them to remote storage.
|
||||
* At the `-remoteWrite.urlRelabelConfig` files. This relabeling is applied to metrics before sending them to the corresponding `-remoteWrite.url`. This relabeling can be debugged by passing `-remoteWrite.urlRelabelDebug` command-line options to `vmagent`. In this case `vmagent` logs metrics before and after the relabeling and then drops all the logged metrics instead of sending them to the corresponding `-remoteWrite.url`.
|
||||
|
||||
You can read more about relabeling in the following articles:
|
||||
@@ -298,14 +329,15 @@ You can read more about relabeling in the following articles:
|
||||
* If the metric disappears from the list of scraped metrics, then stale marker is sent to this particular metric.
|
||||
* If the scrape target becomes temporarily unavailable, then stale markers are sent for all the metrics scraped from this target.
|
||||
* If the scrape target is removed from the list of targets, then stale markers are sent for all the metrics scraped from this target.
|
||||
* Stale markers are sent for all the scraped metrics on graceful shutdown of `vmagent`.
|
||||
|
||||
Prometheus staleness markers aren't sent to `-remoteWrite.url` in [stream parsing mode](#stream-parsing-mode) or if `-promscrape.noStaleMarkers` command-line is set.
|
||||
Prometheus staleness markers' tracking needs additional memory, since it must store the previous response body per each scrape target in order to compare it to the current response body. The memory usage may be reduced by passing `-promscrape.noStaleMarkers` command-line flag to `vmagent`. This disables staleness tracking. This also disables tracking the number of new time series per each scrape with the auto-generated `scrape_series_added` metric. See [these docs](https://prometheus.io/docs/concepts/jobs_instances/#automatically-generated-labels-and-time-series) for details.
|
||||
|
||||
|
||||
## Stream parsing mode
|
||||
|
||||
By default `vmagent` reads the full response from scrape target into memory, then parses it, applies [relabeling](#relabeling) and then pushes the resulting metrics to the configured `-remoteWrite.url`. This mode works good for the majority of cases when the scrape target exposes small number of metrics (e.g. less than 10 thousand). But this mode may take big amounts of memory when the scrape target exposes big number of metrics. In this case it is recommended enabling stream parsing mode. When this mode is enabled, then `vmagent` reads response from scrape target in chunks, then immediately processes every chunk and pushes the processed metrics to remote storage. This allows saving memory when scraping targets that expose millions of metrics. Stream parsing mode may be enabled in the following places:
|
||||
By default `vmagent` reads the full response body from scrape target into memory, then parses it, applies [relabeling](#relabeling) and then pushes the resulting metrics to the configured `-remoteWrite.url`. This mode works good for the majority of cases when the scrape target exposes small number of metrics (e.g. less than 10 thousand). But this mode may take big amounts of memory when the scrape target exposes big number of metrics. In this case it is recommended enabling stream parsing mode. When this mode is enabled, then `vmagent` reads response from scrape target in chunks, then immediately processes every chunk and pushes the processed metrics to remote storage. This allows saving memory when scraping targets that expose millions of metrics.
|
||||
|
||||
Stream parsing mode is automatically enabled for scrape targets returning response bodies with sizes bigger than the `-promscrape.minResponseSizeForStreamParse` command-line flag value. Additionally, the stream parsing mode can be explicitly enabled in the following places:
|
||||
|
||||
- Via `-promscrape.streamParse` command-line flag. In this case all the scrape targets defined in the file pointed by `-promscrape.config` are scraped in stream parsing mode.
|
||||
- Via `stream_parse: true` option at `scrape_configs` section. In this case all the scrape targets defined in this section are scraped in stream parsing mode.
|
||||
@@ -327,7 +359,7 @@ scrape_configs:
|
||||
'match[]': ['{__name__!=""}']
|
||||
```
|
||||
|
||||
Note that `sample_limit` option doesn't prevent from data push to remote storage if stream parsing is enabled because the parsed data is pushed to remote storage as soon as it is parsed.
|
||||
Note that `sample_limit` and `series_limit` options cannot be used in stream parsing mode because the parsed data is pushed to remote storage as soon as it is parsed.
|
||||
|
||||
|
||||
## Scraping big number of targets
|
||||
@@ -424,7 +456,7 @@ These limits are approximate, so `vmagent` can underflow/overflow the limit by a
|
||||
|
||||
`vmagent` exports various metrics in Prometheus exposition format at `http://vmagent-host:8429/metrics` page. We recommend setting up regular scraping of this page
|
||||
either through `vmagent` itself or by Prometheus so that the exported metrics may be analyzed later.
|
||||
Use official [Grafana dashboard](https://grafana.com/grafana/dashboards/12683) for `vmagent` state overview.
|
||||
Use official [Grafana dashboard](https://grafana.com/grafana/dashboards/12683) for `vmagent` state overview. Graphs on this dashboard contain useful hints - hover the `i` icon at the top left corner of each graph in order to read it.
|
||||
If you have suggestions for improvements or have found a bug - please open an issue on github or add a review to the dashboard.
|
||||
|
||||
`vmagent` also exports the status for various targets at the following handlers:
|
||||
@@ -447,10 +479,12 @@ It may be useful to perform `vmagent` rolling update without any scrape loss.
|
||||
as `vmagent` establishes at least a single TCP connection per target.
|
||||
|
||||
* If `vmagent` uses too big amounts of memory, then the following options can help:
|
||||
* Enabling stream parsing. See [these docs](#stream-parsing-mode).
|
||||
* Disabling staleness tracking with `-promscrape.noStaleMarkers` option. See [these docs](#prometheus-staleness-markers).
|
||||
* Enabling stream parsing mode if `vmagent` scrapes targets with millions of metrics per target. See [these docs](#stream-parsing-mode).
|
||||
* Reducing the number of output queues with `-remoteWrite.queues` command-line option.
|
||||
* Reducing the amounts of RAM vmagent can use for in-memory buffering with `-memory.allowedPercent` or `-memory.allowedBytes` command-line option. Another option is to reduce memory limits in Docker and/or Kuberntes if `vmagent` runs under these systems.
|
||||
* Reducing the number of CPU cores vmagent can use by passing `GOMAXPROCS=N` environment variable to `vmagent`, where `N` is the desired limit on CPU cores. Another option is to reduce CPU limits in Docker or Kubernetes if `vmagent` runs under these systems.
|
||||
* Passing `-promscrape.dropOriginalLabels` command-line option to `vmagent`, so it drops `"discoveredLabels"` and `"droppedTargets"` lists at `/api/v1/targets` page. This reduces memory usage when scraping big number of targets at the cost of reduced debuggability for improperly configured per-target relabeling.
|
||||
|
||||
* When `vmagent` scrapes many unreliable targets, it can flood the error log with scrape errors. These errors can be suppressed
|
||||
by passing `-promscrape.suppressScrapeErrors` command-line flag to `vmagent`. The most recent scrape error per each target can be observed at `http://vmagent-host:8429/targets`
|
||||
@@ -459,17 +493,9 @@ It may be useful to perform `vmagent` rolling update without any scrape loss.
|
||||
* The `/api/v1/targets` page could be useful for debugging relabeling process for scrape targets.
|
||||
This page contains original labels for targets dropped during relabeling (see "droppedTargets" section in the page output). By default the `-promscrape.maxDroppedTargets` targets are shown here. If your setup drops more targets during relabeling, then increase `-promscrape.maxDroppedTargets` command-line flag value to see all the dropped targets. Note that tracking each dropped target requires up to 10Kb of RAM. Therefore big values for `-promscrape.maxDroppedTargets` may result in increased memory usage if a big number of scrape targets are dropped during relabeling.
|
||||
|
||||
* If `vmagent` scrapes a big number of targets then the `-promscrape.dropOriginalLabels` command-line option may be passed to `vmagent` in order to reduce memory usage.
|
||||
This option drops `"discoveredLabels"` and `"droppedTargets"` lists at `/api/v1/targets` page, which may result in reduced debuggability for improperly configured per-target relabeling.
|
||||
* We recommend you increase `-remoteWrite.queues` if `vmagent_remotewrite_pending_data_bytes` metric exported at `http://vmagent-host:8429/metrics` page grows constantly. It is also recommended increasing `-remoteWrite.maxBlockSize` and `-remoteWrite.maxRowsPerBlock` command-line options in this case. This can improve data ingestion performance to the configured remote storage systems at the cost of higher memory usage.
|
||||
|
||||
* If `vmagent` scrapes targets with millions of metrics per target (for example, when scraping [federation endpoints](https://prometheus.io/docs/prometheus/latest/federation/)),
|
||||
we recommend enabling [stream parsing mode](#stream-parsing-mode) in order to reduce memory usage during scraping.
|
||||
|
||||
* We recommend you increase `-remoteWrite.queues` if `vmagent_remotewrite_pending_data_bytes` metric exported at `http://vmagent-host:8429/metrics` page grows constantly.
|
||||
|
||||
* If you see gaps in the data pushed by `vmagent` to remote storage when `-remoteWrite.maxDiskUsagePerURL` is set, try increasing `-remoteWrite.queues`.
|
||||
Such gaps may appear because `vmagent` cannot keep up with sending the collected data to remote storage. Therefore it starts dropping the buffered data
|
||||
if the on-disk buffer size exceeds `-remoteWrite.maxDiskUsagePerURL`.
|
||||
* If you see gaps in the data pushed by `vmagent` to remote storage when `-remoteWrite.maxDiskUsagePerURL` is set, try increasing `-remoteWrite.queues`. Such gaps may appear because `vmagent` cannot keep up with sending the collected data to remote storage. Therefore it starts dropping the buffered data if the on-disk buffer size exceeds `-remoteWrite.maxDiskUsagePerURL`.
|
||||
|
||||
* `vmagent` drops data blocks if remote storage replies with `400 Bad Request` and `409 Conflict` HTTP responses. The number of dropped blocks can be monitored via `vmagent_remotewrite_packets_dropped_total` metric exported at [/metrics page](#monitoring).
|
||||
|
||||
@@ -520,6 +546,108 @@ It may be useful to perform `vmagent` rolling update without any scrape loss.
|
||||
regex: true
|
||||
```
|
||||
|
||||
## Kafka integration
|
||||
|
||||
[Enterprise version](https://victoriametrics.com/products/enterprise/) of `vmagent` can read and write metrics from / to Kafka:
|
||||
|
||||
* [Reading metrics from Kafka](#reading-metrics-from-kafka)
|
||||
* [Writing metrics to Kafka](#writing-metrics-to-kafka)
|
||||
|
||||
The enterprise version of vmagent is available for evaluation at [releases](https://github.com/VictoriaMetrics/VictoriaMetrics/releases) page in `vmutils-*-enteprise.tar.gz` archives and in [docker images](https://hub.docker.com/r/victoriametrics/vmagent/tags) with tags containing `enterprise` suffix.
|
||||
|
||||
|
||||
### Reading metrics from Kafka
|
||||
|
||||
[Enterprise version](https://victoriametrics.com/products/enterprise/) of `vmagent` can read metrics in various formats from Kafka messages. These formats can be configured with `-kafka.consumer.topic.defaultFormat` or `-kafka.consumer.topic.format` command-line options. The following formats are supported:
|
||||
|
||||
* `promremotewrite` - [Prometheus remote_write](https://prometheus.io/docs/prometheus/latest/configuration/configuration/#remote_write). Messages in this format can be sent by vmagent - see [these docs](#writing-metrics-to-kafka).
|
||||
* `influx` - [InfluxDB line protocol format](https://docs.influxdata.com/influxdb/v1.7/write_protocols/line_protocol_tutorial/).
|
||||
* `prometheus` - [Prometheus text exposition format](https://github.com/prometheus/docs/blob/master/content/docs/instrumenting/exposition_formats.md#text-based-format) and [OpenMetrics format](https://github.com/OpenObservability/OpenMetrics/blob/master/specification/OpenMetrics.md).
|
||||
* `graphite` - [Graphite plaintext format](https://graphite.readthedocs.io/en/latest/feeding-carbon.html#the-plaintext-protocol).
|
||||
* `jsonline` - [JSON line format](https://docs.victoriametrics.com/#how-to-import-data-in-json-line-format).
|
||||
|
||||
Every Kafka message may contain multiple lines in `influx`, `prometheus`, `graphite` and `jsonline` format delimited by `\n`.
|
||||
|
||||
`vmagent` consumes messages from Kafka topics specified by `-kafka.consumer.topic` command-line flag. Multiple topics can be specified by passing multiple `-kafka.consumer.topic` command-line flags to `vmagent`.
|
||||
|
||||
`vmagent` consumes messages from Kafka brokers specified by `-kafka.consumer.topic.brokers` command-line flag. Multiple brokers can be specified per each `-kafka.consumer.topic` by passing a list of brokers delimited by `;`. For example, `-kafka.consumer.topic.brokers=host1:9092;host2:9092`.
|
||||
|
||||
The following command starts `vmagent`, which reads metrics in InfluxDB line protocol format from Kafka broker at `localhost:9092` from the topic `metrics-by-telegraf` and sends them to remote storage at `http://localhost:8428/api/v1/write`:
|
||||
|
||||
```bash
|
||||
./bin/vmagent -remoteWrite.url=http://localhost:8428/api/v1/write \
|
||||
-kafka.consumer.topic.brokers=localhost:9092 \
|
||||
-kafka.consumer.topic.format=influx \
|
||||
-kafka.consumer.topic=metrics-by-telegraf \
|
||||
-kafka.consumer.topic.groupID=some-id
|
||||
```
|
||||
|
||||
It is expected that [Telegraf](https://github.com/influxdata/telegraf) sends metrics to the `metrics-by-telegraf` topic with the following config:
|
||||
|
||||
```yaml
|
||||
[[outputs.kafka]]
|
||||
brokers = ["localhost:9092"]
|
||||
topic = "influx"
|
||||
data_format = "influx"
|
||||
```
|
||||
|
||||
|
||||
#### Command-line flags for Kafka consumer
|
||||
|
||||
These command-line flags are available only in [enterprise](https://victoriametrics.com/products/enterprise/) version of `vmagent`, which can be downloaded for evaluation from [releases](https://github.com/VictoriaMetrics/VictoriaMetrics/releases) page (see `vmutils-*-enteprise.tar.gz` archives) and from [docker images](https://hub.docker.com/r/victoriametrics/vmagent/tags) with tags containing `enterprise` suffix.
|
||||
|
||||
```
|
||||
-kafka.consumer.topic array
|
||||
Kafka topic names for data consumption.
|
||||
Supports an array of values separated by comma or specified via multiple flags.
|
||||
-kafka.consumer.topic.basicAuth.password array
|
||||
Optional basic auth password for -kafka.consumer.topic. Must be used in conjunction with any supported auth methods for kafka client, specified by flag -kafka.consumer.topic.options='security.protocol=SASL_SSL;sasl.mechanisms=PLAIN'
|
||||
Supports an array of values separated by comma or specified via multiple flags.
|
||||
-kafka.consumer.topic.basicAuth.username array
|
||||
Optional basic auth username for -kafka.consumer.topic. Must be used in conjunction with any supported auth methods for kafka client, specified by flag -kafka.consumer.topic.options='security.protocol=SASL_SSL;sasl.mechanisms=PLAIN'
|
||||
Supports an array of values separated by comma or specified via multiple flags.
|
||||
-kafka.consumer.topic.brokers array
|
||||
List of brokers to connect for given topic, e.g. -kafka.consumer.topic.broker=host-1:9092;host-2:9092
|
||||
Supports an array of values separated by comma or specified via multiple flags.
|
||||
-kafka.consumer.topic.defaultFormat string
|
||||
Expected data format in the topic if -kafka.consumer.topic.format is skipped. (default "promremotewrite")
|
||||
-kafka.consumer.topic.format array
|
||||
data format for corresponding kafka topic. Valid formats: influx, prometheus, promremotewrite, graphite, jsonline
|
||||
Supports an array of values separated by comma or specified via multiple flags.
|
||||
-kafka.consumer.topic.groupID array
|
||||
Defines group.id for topic
|
||||
Supports an array of values separated by comma or specified via multiple flags.
|
||||
-kafka.consumer.topic.isGzipped array
|
||||
Enables gzip setting for topic messages payload. Only prometheus, jsonline and influx formats accept gzipped messages.
|
||||
Supports array of values separated by comma or specified via multiple flags.
|
||||
-kafka.consumer.topic.options array
|
||||
Optional key=value;key1=value2 settings for topic consumer. See full configuration options at https://github.com/edenhill/librdkafka/blob/master/CONFIGURATION.md.
|
||||
Supports an array of values separated by comma or specified via multiple flags.
|
||||
```
|
||||
|
||||
### Writing metrics to Kafka
|
||||
|
||||
[Enterprise version](https://victoriametrics.com/products/enterprise/) of `vmagent` writes data to Kafka with `at-least-once` semantics if `-remoteWrite.url` contains e.g. Kafka url. For example, if `vmagent` is started with `-remoteWrite.url=kafka://localhost:9092/?topic=prom-rw`, then it would send Prometheus remote_write messages to Kafka bootstrap server at `localhost:9092` with the topic `prom-rw`. These messages can be read later from Kafka by another `vmagent` - see [these docs](#reading-metrics-from-kafka) for details.
|
||||
|
||||
Additional Kafka options can be passed as query params to `-remoteWrite.url`. For instance, `kafka://localhost:9092/?topic=prom-rw&client.id=my-favorite-id` sets `client.id` Kafka option to `my-favorite-id`. The full list of Kafka options is available [here](https://github.com/edenhill/librdkafka/blob/master/CONFIGURATION.md).
|
||||
|
||||
|
||||
#### Kafka broker authorization and authentication
|
||||
|
||||
Two types of auth are supported:
|
||||
|
||||
* sasl with username and password:
|
||||
|
||||
```bash
|
||||
./bin/vmagent -remoteWrite.url=kafka://localhost:9092/?topic=prom-rw&security.protocol=SASL_SSL&sasl.mechanisms=PLAIN -remoteWrite.basicAuth.username=user -remoteWrite.basicAuth.password=password
|
||||
```
|
||||
|
||||
* tls certificates:
|
||||
|
||||
```bash
|
||||
./bin/vmagent -remoteWrite.url=kafka://localhost:9092/?topic=prom-rw&security.protocol=SSL -remoteWrite.tlsCAFile=/opt/ca.pem -remoteWrite.tlsCertFile=/opt/cert.pem -remoteWrite.tlsKeyFile=/opt/key.pem
|
||||
```
|
||||
|
||||
|
||||
## How to build from sources
|
||||
|
||||
@@ -528,7 +656,7 @@ We recommend using [binary releases](https://github.com/VictoriaMetrics/Victoria
|
||||
|
||||
### Development build
|
||||
|
||||
1. [Install Go](https://golang.org/doc/install). The minimum supported version is Go 1.16.
|
||||
1. [Install Go](https://golang.org/doc/install). The minimum supported version is Go 1.17.
|
||||
2. Run `make vmagent` from the root folder of [the repository](https://github.com/VictoriaMetrics/VictoriaMetrics).
|
||||
It builds the `vmagent` binary and puts it into the `bin` folder.
|
||||
|
||||
@@ -557,7 +685,7 @@ ARM build may run on Raspberry Pi or on [energy-efficient ARM servers](https://b
|
||||
|
||||
### Development ARM build
|
||||
|
||||
1. [Install Go](https://golang.org/doc/install). The minimum supported version is Go 1.16.
|
||||
1. [Install Go](https://golang.org/doc/install). The minimum supported version is Go 1.17.
|
||||
2. Run `make vmagent-arm` or `make vmagent-arm64` from the root folder of [the repository](https://github.com/VictoriaMetrics/VictoriaMetrics)
|
||||
It builds `vmagent-arm` or `vmagent-arm64` binary respectively and puts it into the `bin` folder.
|
||||
|
||||
@@ -572,18 +700,26 @@ ARM build may run on Raspberry Pi or on [energy-efficient ARM servers](https://b
|
||||
|
||||
`vmagent` provides handlers for collecting the following [Go profiles](https://blog.golang.org/profiling-go-programs):
|
||||
|
||||
* Memory profile can be collected with the following command:
|
||||
* Memory profile can be collected with the following command (replace `0.0.0.0` with hostname if needed):
|
||||
|
||||
<div class="with-copy" markdown="1">
|
||||
|
||||
```bash
|
||||
curl -s http://<vmagent-host>:8429/debug/pprof/heap > mem.pprof
|
||||
curl http://0.0.0.0:8429/debug/pprof/heap > mem.pprof
|
||||
```
|
||||
|
||||
* CPU profile can be collected with the following command:
|
||||
</div>
|
||||
|
||||
* CPU profile can be collected with the following command (replace `0.0.0.0` with hostname if needed):
|
||||
|
||||
<div class="with-copy" markdown="1">
|
||||
|
||||
```bash
|
||||
curl -s http://<vmagent-host>:8429/debug/pprof/profile > cpu.pprof
|
||||
curl http://0.0.0.0:8429/debug/pprof/profile > cpu.pprof
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
The command for collecting CPU profile waits for 30 seconds before returning.
|
||||
|
||||
The collected profiles may be analyzed with [go tool pprof](https://github.com/google/pprof).
|
||||
@@ -600,16 +736,23 @@ vmagent collects metrics data via popular data ingestion protocols and routes th
|
||||
|
||||
See the docs at https://docs.victoriametrics.com/vmagent.html .
|
||||
|
||||
-configAuthKey string
|
||||
Authorization key for accessing /config page. It must be passed via authKey query arg
|
||||
-csvTrimTimestamp duration
|
||||
Trim timestamps when importing csv data to this duration. Minimum practical duration is 1ms. Higher duration (i.e. 1s) may be used for reducing disk space usage for timestamp data (default 1ms)
|
||||
-datadog.maxInsertRequestSize size
|
||||
The maximum size in bytes of a single DataDog POST request to /api/v1/series
|
||||
Supports the following optional suffixes for size values: KB, MB, GB, KiB, MiB, GiB (default 67108864)
|
||||
-dryRun
|
||||
Whether to check only config files without running vmagent. The following files are checked: -promscrape.config, -remoteWrite.relabelConfig, -remoteWrite.urlRelabelConfig . Unknown config entries are allowed in -promscrape.config by default. This can be changed with -promscrape.config.strictParse
|
||||
Whether to check only config files without running vmagent. The following files are checked: -promscrape.config, -remoteWrite.relabelConfig, -remoteWrite.urlRelabelConfig . Unknown config entries aren't allowed in -promscrape.config by default. This can be changed by passing -promscrape.config.strictParse=false command-line flag
|
||||
-enableTCP6
|
||||
Whether to enable IPv6 for listening and dialing. By default only IPv4 TCP and UDP is used
|
||||
-envflag.enable
|
||||
Whether to enable reading flags from environment variables additionally to command line. Command line flag values have priority over values from environment vars. Flags are read only from command line if this flag isn't set. See https://docs.victoriametrics.com/#environment-variables for more details
|
||||
-envflag.prefix string
|
||||
Prefix for environment variables if -envflag.enable is set
|
||||
-eula
|
||||
By specifying this flag, you confirm that you have an enterprise license and accept the EULA https://victoriametrics.com/assets/VM_EULA.pdf
|
||||
-fs.disableMmap
|
||||
Whether to use pread() instead of mmap() for reading data files. By default mmap() is used for 64-bit arches and pread() is used for 32-bit arches, since they cannot read data files bigger than 2^32 bytes in memory. mmap() is usually faster for reading small data chunks than pread()
|
||||
-graphiteListenAddr string
|
||||
@@ -643,6 +786,8 @@ See the docs at https://docs.victoriametrics.com/vmagent.html .
|
||||
-influx.maxLineSize size
|
||||
The maximum size in bytes for a single InfluxDB line during parsing
|
||||
Supports the following optional suffixes for size values: KB, MB, GB, KiB, MiB, GiB (default 262144)
|
||||
-influxDBLabel string
|
||||
Default label for the DB name sent over '?db={db_name}' query parameter (default "db")
|
||||
-influxListenAddr string
|
||||
TCP and UDP address to listen for InfluxDB line protocol data. Usually :8189 must be set. Doesn't work if empty. This flag isn't needed when ingesting data over HTTP - just send it to http://<vmagent>:8429/write
|
||||
-influxMeasurementFieldSeparator string
|
||||
@@ -655,6 +800,32 @@ See the docs at https://docs.victoriametrics.com/vmagent.html .
|
||||
Trim timestamps for InfluxDB line protocol data to this duration. Minimum practical duration is 1ms. Higher duration (i.e. 1s) may be used for reducing disk space usage for timestamp data (default 1ms)
|
||||
-insert.maxQueueDuration duration
|
||||
The maximum duration for waiting in the queue for insert requests due to -maxConcurrentInserts (default 1m0s)
|
||||
-kafka.consumer.topic array
|
||||
Kafka topic names for data consumption.
|
||||
Supports an array of values separated by comma or specified via multiple flags.
|
||||
-kafka.consumer.topic.basicAuth.password array
|
||||
Optional basic auth password for -kafka.consumer.topic. Must be used in conjunction with any supported auth methods for kafka client, specified by flag -kafka.consumer.topic.options='security.protocol=SASL_SSL;sasl.mechanisms=PLAIN'
|
||||
Supports an array of values separated by comma or specified via multiple flags.
|
||||
-kafka.consumer.topic.basicAuth.username array
|
||||
Optional basic auth username for -kafka.consumer.topic. Must be used in conjunction with any supported auth methods for kafka client, specified by flag -kafka.consumer.topic.options='security.protocol=SASL_SSL;sasl.mechanisms=PLAIN'
|
||||
Supports an array of values separated by comma or specified via multiple flags.
|
||||
-kafka.consumer.topic.brokers array
|
||||
List of brokers to connect for given topic, e.g. -kafka.consumer.topic.broker=host-1:9092;host-2:9092
|
||||
Supports an array of values separated by comma or specified via multiple flags.
|
||||
-kafka.consumer.topic.defaultFormat string
|
||||
Expected data format in the topic if -kafka.consumer.topic.format is skipped. (default "promremotewrite")
|
||||
-kafka.consumer.topic.format array
|
||||
data format for corresponding kafka topic. Valid formats: influx, prometheus, promremotewrite, graphite, jsonline
|
||||
Supports an array of values separated by comma or specified via multiple flags.
|
||||
-kafka.consumer.topic.groupID array
|
||||
Defines group.id for topic
|
||||
Supports an array of values separated by comma or specified via multiple flags.
|
||||
-kafka.consumer.topic.isGzipped array
|
||||
Enables gzip setting for topic messages payload. Only prometheus, jsonline and influx formats accept gzipped messages.
|
||||
Supports array of values separated by comma or specified via multiple flags.
|
||||
-kafka.consumer.topic.options array
|
||||
Optional key=value;key1=value2 settings for topic consumer. See full configuration options at https://github.com/edenhill/librdkafka/blob/master/CONFIGURATION.md.
|
||||
Supports an array of values separated by comma or specified via multiple flags.
|
||||
-loggerDisableTimestamps
|
||||
Whether to disable writing timestamps in logs
|
||||
-loggerErrorsPerSecondLimit int
|
||||
@@ -680,7 +851,7 @@ See the docs at https://docs.victoriametrics.com/vmagent.html .
|
||||
-memory.allowedPercent float
|
||||
Allowed percent of system memory VictoriaMetrics caches may occupy. See also -memory.allowedBytes. Too low a value may increase cache miss rate usually resulting in higher CPU and disk IO usage. Too high a value may evict too much data from OS page cache which will result in higher disk IO usage (default 60)
|
||||
-metricsAuthKey string
|
||||
Auth key for /metrics. It overrides httpAuth settings
|
||||
Auth key for /metrics. It must be passed via authKey query arg. It overrides httpAuth.* settings
|
||||
-opentsdbHTTPListenAddr string
|
||||
TCP address to listen for OpentTSDB HTTP put requests. Usually :4242 must be set. Doesn't work if empty
|
||||
-opentsdbListenAddr string
|
||||
@@ -693,7 +864,7 @@ See the docs at https://docs.victoriametrics.com/vmagent.html .
|
||||
-opentsdbhttpTrimTimestamp duration
|
||||
Trim timestamps for OpenTSDB HTTP data to this duration. Minimum practical duration is 1ms. Higher duration (i.e. 1s) may be used for reducing disk space usage for timestamp data (default 1ms)
|
||||
-pprofAuthKey string
|
||||
Auth key for /debug/pprof. It overrides httpAuth settings
|
||||
Auth key for /debug/pprof. It must be passed via authKey query arg. It overrides httpAuth.* settings
|
||||
-promscrape.cluster.memberNum int
|
||||
The number of number in the cluster of scrapers. It must be an unique value in the range 0 ... promscrape.cluster.membersCount-1 across scrapers in the cluster
|
||||
-promscrape.cluster.membersCount int
|
||||
@@ -701,11 +872,11 @@ See the docs at https://docs.victoriametrics.com/vmagent.html .
|
||||
-promscrape.cluster.replicationFactor int
|
||||
The number of members in the cluster, which scrape the same targets. If the replication factor is greater than 2, then the deduplication must be enabled at remote storage side. See https://docs.victoriametrics.com/#deduplication (default 1)
|
||||
-promscrape.config string
|
||||
Optional path to Prometheus config file with 'scrape_configs' section containing targets to scrape. See https://docs.victoriametrics.com/#how-to-scrape-prometheus-exporters-such-as-node-exporter for details
|
||||
Optional path to Prometheus config file with 'scrape_configs' section containing targets to scrape. The path can point to local file and to http url. See https://docs.victoriametrics.com/#how-to-scrape-prometheus-exporters-such-as-node-exporter for details
|
||||
-promscrape.config.dryRun
|
||||
Checks -promscrape.config file for errors and unsupported fields and then exits. Returns non-zero exit code on parsing errors and emits these errors to stderr. See also -promscrape.config.strictParse command-line flag. Pass -loggerLevel=ERROR if you don't need to see info messages in the output.
|
||||
-promscrape.config.strictParse
|
||||
Whether to allow only supported fields in -promscrape.config . By default unsupported fields are silently skipped
|
||||
Whether to deny unsupported fields in -promscrape.config . Set to false in order to silently skip unsupported fields (default true)
|
||||
-promscrape.configCheckInterval duration
|
||||
Interval for checking for changes in '-promscrape.config' file. By default the checking is disabled. Send SIGHUP signal in order to force config check for changes
|
||||
-promscrape.consul.waitTime duration
|
||||
@@ -735,7 +906,7 @@ See the docs at https://docs.victoriametrics.com/vmagent.html .
|
||||
-promscrape.eurekaSDCheckInterval duration
|
||||
Interval for checking for changes in eureka. This works only if eureka_sd_configs is configured in '-promscrape.config' file. See https://prometheus.io/docs/prometheus/latest/configuration/configuration/#eureka_sd_config for details (default 30s)
|
||||
-promscrape.fileSDCheckInterval duration
|
||||
Interval for checking for changes in 'file_sd_config'. See https://prometheus.io/docs/prometheus/latest/configuration/configuration/#file_sd_config for details (default 30s)
|
||||
Interval for checking for changes in 'file_sd_config'. See https://prometheus.io/docs/prometheus/latest/configuration/configuration/#file_sd_config for details (default 5m0s)
|
||||
-promscrape.gceSDCheckInterval duration
|
||||
Interval for checking for changes in gce. This works only if gce_sd_configs is configured in '-promscrape.config' file. See https://prometheus.io/docs/prometheus/latest/configuration/configuration/#gce_sd_config for details (default 1m0s)
|
||||
-promscrape.httpSDCheckInterval duration
|
||||
@@ -746,11 +917,17 @@ See the docs at https://docs.victoriametrics.com/vmagent.html .
|
||||
Interval for checking for changes in Kubernetes API server. This works only if kubernetes_sd_configs is configured in '-promscrape.config' file. See https://prometheus.io/docs/prometheus/latest/configuration/configuration/#kubernetes_sd_config for details (default 30s)
|
||||
-promscrape.maxDroppedTargets int
|
||||
The maximum number of droppedTargets to show at /api/v1/targets page. Increase this value if your setup drops more scrape targets during relabeling and you need investigating labels for all the dropped targets. Note that the increased number of tracked dropped targets may result in increased memory usage (default 1000)
|
||||
-promscrape.maxResponseHeadersSize size
|
||||
The maximum size of http response headers from Prometheus scrape targets
|
||||
Supports the following optional suffixes for size values: KB, MB, GB, KiB, MiB, GiB (default 4096)
|
||||
-promscrape.maxScrapeSize size
|
||||
The maximum size of scrape response in bytes to process from Prometheus targets. Bigger responses are rejected
|
||||
Supports the following optional suffixes for size values: KB, MB, GB, KiB, MiB, GiB (default 16777216)
|
||||
-promscrape.minResponseSizeForStreamParse size
|
||||
The minimum target response size for automatic switching to stream parsing mode, which can reduce memory usage. See https://docs.victoriametrics.com/vmagent.html#stream-parsing-mode
|
||||
Supports the following optional suffixes for size values: KB, MB, GB, KiB, MiB, GiB (default 1000000)
|
||||
-promscrape.noStaleMarkers
|
||||
Whether to disable sending Prometheus stale markers for metrics when scrape target disappears. This option may reduce memory usage if stale markers aren't needed for your setup. See also https://docs.victoriametrics.com/vmagent.html#stream-parsing-mode
|
||||
Whether to disable sending Prometheus stale markers for metrics when scrape target disappears. This option may reduce memory usage if stale markers aren't needed for your setup. This option also disables populating the scrape_series_added metric. See https://prometheus.io/docs/concepts/jobs_instances/#automatically-generated-labels-and-time-series
|
||||
-promscrape.openstackSDCheckInterval duration
|
||||
Interval for checking for changes in openstack API server. This works only if openstack_sd_configs is configured in '-promscrape.config' file. See https://prometheus.io/docs/prometheus/latest/configuration/configuration/#openstack_sd_config for details (default 30s)
|
||||
-promscrape.seriesLimitPerTarget int
|
||||
@@ -782,7 +959,7 @@ See the docs at https://docs.victoriametrics.com/vmagent.html .
|
||||
Optional label in the form 'name=value' to add to all the metrics before sending them to -remoteWrite.url. Pass multiple -remoteWrite.label flags in order to add multiple labels to metrics before sending them to remote storage
|
||||
Supports an array of values separated by comma or specified via multiple flags.
|
||||
-remoteWrite.maxBlockSize size
|
||||
The maximum size in bytes of unpacked request to send to remote storage. It shouldn't exceed -maxInsertRequestSize from VictoriaMetrics
|
||||
The maximum block size to send to remote storage. Bigger blocks may improve performance at the cost of the increased memory usage. See also -remoteWrite.maxRowsPerBlock
|
||||
Supports the following optional suffixes for size values: KB, MB, GB, KiB, MiB, GiB (default 8388608)
|
||||
-remoteWrite.maxDailySeries int
|
||||
The maximum number of unique series vmagent can send to remote storage systems during the last 24 hours. Excess series are logged and dropped. This can be useful for limiting series churn rate. See https://docs.victoriametrics.com/vmagent.html#cardinality-limiter
|
||||
@@ -791,6 +968,8 @@ See the docs at https://docs.victoriametrics.com/vmagent.html .
|
||||
Supports the following optional suffixes for size values: KB, MB, GB, KiB, MiB, GiB (default 0)
|
||||
-remoteWrite.maxHourlySeries int
|
||||
The maximum number of unique series vmagent can send to remote storage systems during the last hour. Excess series are logged and dropped. This can be useful for limiting series cardinality. See https://docs.victoriametrics.com/vmagent.html#cardinality-limiter
|
||||
-remoteWrite.maxRowsPerBlock int
|
||||
The maximum number of samples to send in each block to remote storage. Higher number may improve performance at the cost of the increased memory usage. See also -remoteWrite.maxBlockSize (default 10000)
|
||||
-remoteWrite.multitenantURL array
|
||||
Base path for multitenant remote storage URL to write data to. See https://docs.victoriametrics.com/vmagent.html#multitenancy for details. Example url: http://<vminsert>:8480 . Pass multiple -remoteWrite.multitenantURL flags in order to replicate data to multiple remote storage systems. See also -remoteWrite.url
|
||||
Supports an array of values separated by comma or specified via multiple flags.
|
||||
@@ -818,7 +997,7 @@ See the docs at https://docs.victoriametrics.com/vmagent.html .
|
||||
Optional rate limit in bytes per second for data sent to -remoteWrite.url. By default the rate limit is disabled. It can be useful for limiting load on remote storage when big amounts of buffered data is sent after temporary unavailability of the remote storage
|
||||
Supports array of values separated by comma or specified via multiple flags.
|
||||
-remoteWrite.relabelConfig string
|
||||
Optional path to file with relabel_config entries. These entries are applied to all the metrics before sending them to -remoteWrite.url. See https://docs.victoriametrics.com/vmagent.html#relabeling for details
|
||||
Optional path to file with relabel_config entries. The path can point either to local file or to http url. These entries are applied to all the metrics before sending them to -remoteWrite.url. See https://docs.victoriametrics.com/vmagent.html#relabeling for details
|
||||
-remoteWrite.relabelDebug
|
||||
Whether to log metrics before and after relabeling with -remoteWrite.relabelConfig. If the -remoteWrite.relabelDebug is enabled, then the metrics aren't sent to remote storage. This is useful for debugging the relabeling configs
|
||||
-remoteWrite.roundDigits array
|
||||
@@ -853,7 +1032,7 @@ See the docs at https://docs.victoriametrics.com/vmagent.html .
|
||||
Remote storage URL to write data to. It must support Prometheus remote_write API. It is recommended using VictoriaMetrics as remote storage. Example url: http://<victoriametrics-host>:8428/api/v1/write . Pass multiple -remoteWrite.url flags in order to replicate data to multiple remote storage systems. See also -remoteWrite.multitenantURL
|
||||
Supports an array of values separated by comma or specified via multiple flags.
|
||||
-remoteWrite.urlRelabelConfig array
|
||||
Optional path to relabel config for the corresponding -remoteWrite.url
|
||||
Optional path to relabel config for the corresponding -remoteWrite.url. The path can point either to local file or to http url
|
||||
Supports an array of values separated by comma or specified via multiple flags.
|
||||
-remoteWrite.urlRelabelDebug array
|
||||
Whether to log metrics before and after relabeling with -remoteWrite.urlRelabelConfig. If the -remoteWrite.urlRelabelDebug is enabled, then the metrics aren't sent to the corresponding -remoteWrite.url. This is useful for debugging the relabeling configs
|
||||
@@ -863,9 +1042,9 @@ See the docs at https://docs.victoriametrics.com/vmagent.html .
|
||||
-tls
|
||||
Whether to enable TLS (aka HTTPS) for incoming requests. -tlsCertFile and -tlsKeyFile must be set if -tls is set
|
||||
-tlsCertFile string
|
||||
Path to file with TLS certificate. Used only if -tls is set. Prefer ECDSA certs instead of RSA certs as RSA certs are slower
|
||||
Path to file with TLS certificate. Used only if -tls is set. Prefer ECDSA certs instead of RSA certs as RSA certs are slower. The provided certificate file is automatically re-read every second, so it can be dynamically updated
|
||||
-tlsKeyFile string
|
||||
Path to file with TLS key. Used only if -tls is set
|
||||
Path to file with TLS key. Used only if -tls is set. The provided key file is automatically re-read every second, so it can be dynamically updated
|
||||
-version
|
||||
Show VictoriaMetrics version
|
||||
```
|
||||
|
||||
99
app/vmagent/datadog/request_handler.go
Normal file
@@ -0,0 +1,99 @@
|
||||
package datadog
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmagent/common"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmagent/remotewrite"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/auth"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/prompbmarshal"
|
||||
parserCommon "github.com/VictoriaMetrics/VictoriaMetrics/lib/protoparser/common"
|
||||
parser "github.com/VictoriaMetrics/VictoriaMetrics/lib/protoparser/datadog"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/tenantmetrics"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/writeconcurrencylimiter"
|
||||
"github.com/VictoriaMetrics/metrics"
|
||||
)
|
||||
|
||||
var (
|
||||
rowsInserted = metrics.NewCounter(`vmagent_rows_inserted_total{type="datadog"}`)
|
||||
rowsTenantInserted = tenantmetrics.NewCounterMap(`vmagent_tenant_inserted_rows_total{type="datadog"}`)
|
||||
rowsPerInsert = metrics.NewHistogram(`vmagent_rows_per_insert{type="datadog"}`)
|
||||
)
|
||||
|
||||
// InsertHandlerForHTTP processes remote write for DataDog POST /api/v1/series request.
|
||||
//
|
||||
// See https://docs.datadoghq.com/api/latest/metrics/#submit-metrics
|
||||
func InsertHandlerForHTTP(at *auth.Token, req *http.Request) error {
|
||||
extraLabels, err := parserCommon.GetExtraLabels(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return writeconcurrencylimiter.Do(func() error {
|
||||
ce := req.Header.Get("Content-Encoding")
|
||||
return parser.ParseStream(req.Body, ce, func(series []parser.Series) error {
|
||||
return insertRows(at, series, extraLabels)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func insertRows(at *auth.Token, series []parser.Series, extraLabels []prompbmarshal.Label) error {
|
||||
ctx := common.GetPushCtx()
|
||||
defer common.PutPushCtx(ctx)
|
||||
|
||||
rowsTotal := 0
|
||||
tssDst := ctx.WriteRequest.Timeseries[:0]
|
||||
labels := ctx.Labels[:0]
|
||||
samples := ctx.Samples[:0]
|
||||
for i := range series {
|
||||
ss := &series[i]
|
||||
rowsTotal += len(ss.Points)
|
||||
labelsLen := len(labels)
|
||||
labels = append(labels, prompbmarshal.Label{
|
||||
Name: "__name__",
|
||||
Value: ss.Metric,
|
||||
})
|
||||
labels = append(labels, prompbmarshal.Label{
|
||||
Name: "host",
|
||||
Value: ss.Host,
|
||||
})
|
||||
for _, tag := range ss.Tags {
|
||||
n := strings.IndexByte(tag, ':')
|
||||
if n < 0 {
|
||||
return fmt.Errorf("cannot find ':' in tag %q", tag)
|
||||
}
|
||||
name := tag[:n]
|
||||
value := tag[n+1:]
|
||||
if name == "host" {
|
||||
name = "exported_host"
|
||||
}
|
||||
labels = append(labels, prompbmarshal.Label{
|
||||
Name: name,
|
||||
Value: value,
|
||||
})
|
||||
}
|
||||
labels = append(labels, extraLabels...)
|
||||
samplesLen := len(samples)
|
||||
for _, pt := range ss.Points {
|
||||
samples = append(samples, prompbmarshal.Sample{
|
||||
Timestamp: pt.Timestamp(),
|
||||
Value: pt.Value(),
|
||||
})
|
||||
}
|
||||
tssDst = append(tssDst, prompbmarshal.TimeSeries{
|
||||
Labels: labels[labelsLen:],
|
||||
Samples: samples[samplesLen:],
|
||||
})
|
||||
}
|
||||
ctx.WriteRequest.Timeseries = tssDst
|
||||
ctx.Labels = labels
|
||||
ctx.Samples = samples
|
||||
remotewrite.PushWithAuthToken(at, &ctx.WriteRequest)
|
||||
rowsInserted.Add(rowsTotal)
|
||||
if at != nil {
|
||||
rowsTenantInserted.Get(at).Add(rowsTotal)
|
||||
}
|
||||
rowsPerInsert.Update(float64(rowsTotal))
|
||||
return nil
|
||||
}
|
||||
@@ -24,6 +24,7 @@ var (
|
||||
measurementFieldSeparator = flag.String("influxMeasurementFieldSeparator", "_", "Separator for '{measurement}{separator}{field_name}' metric name when inserted via InfluxDB line protocol")
|
||||
skipSingleField = flag.Bool("influxSkipSingleField", false, "Uses '{measurement}' instead of '{measurement}{separator}{field_name}' for metic name if InfluxDB line contains only a single field")
|
||||
skipMeasurement = flag.Bool("influxSkipMeasurement", false, "Uses '{field_name}' as a metric name while ignoring '{measurement}' and '-influxMeasurementFieldSeparator'")
|
||||
dbLabel = flag.String("influxDBLabel", "db", "Default label for the DB name sent over '?db={db_name}' query parameter")
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -80,7 +81,7 @@ func insertRows(at *auth.Token, db string, rows []parser.Row, extraLabels []prom
|
||||
hasDBKey := false
|
||||
for j := range r.Tags {
|
||||
tag := &r.Tags[j]
|
||||
if tag.Key == "db" {
|
||||
if tag.Key == *dbLabel {
|
||||
hasDBKey = true
|
||||
}
|
||||
commonLabels = append(commonLabels, prompbmarshal.Label{
|
||||
@@ -90,7 +91,7 @@ func insertRows(at *auth.Token, db string, rows []parser.Row, extraLabels []prom
|
||||
}
|
||||
if len(db) > 0 && !hasDBKey {
|
||||
commonLabels = append(commonLabels, prompbmarshal.Label{
|
||||
Name: "db",
|
||||
Name: *dbLabel,
|
||||
Value: db,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmagent/csvimport"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmagent/datadog"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmagent/graphite"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmagent/influx"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmagent/native"
|
||||
@@ -49,9 +50,10 @@ var (
|
||||
"Telnet put messages and HTTP /api/put messages are simultaneously served on TCP port. "+
|
||||
"Usually :4242 must be set. Doesn't work if empty")
|
||||
opentsdbHTTPListenAddr = flag.String("opentsdbHTTPListenAddr", "", "TCP address to listen for OpentTSDB HTTP put requests. Usually :4242 must be set. Doesn't work if empty")
|
||||
configAuthKey = flag.String("configAuthKey", "", "Authorization key for accessing /config page. It must be passed via authKey query arg")
|
||||
dryRun = flag.Bool("dryRun", false, "Whether to check only config files without running vmagent. The following files are checked: "+
|
||||
"-promscrape.config, -remoteWrite.relabelConfig, -remoteWrite.urlRelabelConfig . "+
|
||||
"Unknown config entries are allowed in -promscrape.config by default. This can be changed with -promscrape.config.strictParse")
|
||||
"Unknown config entries aren't allowed in -promscrape.config by default. This can be changed by passing -promscrape.config.strictParse=false command-line flag")
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -156,10 +158,12 @@ func requestHandler(w http.ResponseWriter, r *http.Request) bool {
|
||||
fmt.Fprintf(w, "See docs at <a href='https://docs.victoriametrics.com/vmagent.html'>https://docs.victoriametrics.com/vmagent.html</a></br>")
|
||||
fmt.Fprintf(w, "Useful endpoints:</br>")
|
||||
httpserver.WriteAPIHelp(w, [][2]string{
|
||||
{"/targets", "discovered targets list"},
|
||||
{"/api/v1/targets", "advanced information about discovered targets in JSON format"},
|
||||
{"/metrics", "available service metrics"},
|
||||
{"/-/reload", "reload configuration"},
|
||||
{"targets", "discovered targets list"},
|
||||
{"api/v1/targets", "advanced information about discovered targets in JSON format"},
|
||||
{"config", "-promscrape.config contents"},
|
||||
{"metrics", "available service metrics"},
|
||||
{"flags", "command-line flags"},
|
||||
{"-/reload", "reload configuration"},
|
||||
})
|
||||
return true
|
||||
}
|
||||
@@ -224,13 +228,64 @@ func requestHandler(w http.ResponseWriter, r *http.Request) bool {
|
||||
influxQueryRequests.Inc()
|
||||
influxutils.WriteDatabaseNames(w)
|
||||
return true
|
||||
case "/datadog/api/v1/series":
|
||||
datadogWriteRequests.Inc()
|
||||
if err := datadog.InsertHandlerForHTTP(nil, r); err != nil {
|
||||
datadogWriteErrors.Inc()
|
||||
httpserver.Errorf(w, r, "%s", err)
|
||||
return true
|
||||
}
|
||||
// See https://docs.datadoghq.com/api/latest/metrics/#submit-metrics
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(202)
|
||||
fmt.Fprintf(w, `{"status":"ok"}`)
|
||||
return true
|
||||
case "/datadog/api/v1/validate":
|
||||
datadogValidateRequests.Inc()
|
||||
// See https://docs.datadoghq.com/api/latest/authentication/#validate-api-key
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
fmt.Fprintf(w, `{"valid":true}`)
|
||||
return true
|
||||
case "/datadog/api/v1/check_run":
|
||||
datadogCheckRunRequests.Inc()
|
||||
// See https://docs.datadoghq.com/api/latest/service-checks/#submit-a-service-check
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(202)
|
||||
fmt.Fprintf(w, `{"status":"ok"}`)
|
||||
return true
|
||||
case "/datadog/intake/":
|
||||
datadogIntakeRequests.Inc()
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
fmt.Fprintf(w, `{}`)
|
||||
return true
|
||||
case "/targets":
|
||||
promscrapeTargetsRequests.Inc()
|
||||
promscrape.WriteHumanReadableTargetsStatus(w, r)
|
||||
return true
|
||||
case "/target_response":
|
||||
promscrapeTargetResponseRequests.Inc()
|
||||
if err := promscrape.WriteTargetResponse(w, r); err != nil {
|
||||
promscrapeTargetResponseErrors.Inc()
|
||||
httpserver.Errorf(w, r, "%s", err)
|
||||
return true
|
||||
}
|
||||
return true
|
||||
case "/config":
|
||||
if *configAuthKey != "" && r.FormValue("authKey") != *configAuthKey {
|
||||
err := &httpserver.ErrorWithStatusCode{
|
||||
Err: fmt.Errorf("The provided authKey doesn't match -configAuthKey"),
|
||||
StatusCode: http.StatusUnauthorized,
|
||||
}
|
||||
httpserver.Errorf(w, r, "%s", err)
|
||||
return true
|
||||
}
|
||||
promscrapeConfigRequests.Inc()
|
||||
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||
promscrape.WriteConfigData(w)
|
||||
return true
|
||||
case "/api/v1/targets":
|
||||
promscrapeAPIV1TargetsRequests.Inc()
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
state := r.FormValue("state")
|
||||
promscrape.WriteAPIV1Targets(w, state)
|
||||
return true
|
||||
@@ -330,6 +385,35 @@ func processMultitenantRequest(w http.ResponseWriter, r *http.Request, path stri
|
||||
influxQueryRequests.Inc()
|
||||
influxutils.WriteDatabaseNames(w)
|
||||
return true
|
||||
case "datadog/api/v1/series":
|
||||
datadogWriteRequests.Inc()
|
||||
if err := datadog.InsertHandlerForHTTP(at, r); err != nil {
|
||||
datadogWriteErrors.Inc()
|
||||
httpserver.Errorf(w, r, "%s", err)
|
||||
return true
|
||||
}
|
||||
// See https://docs.datadoghq.com/api/latest/metrics/#submit-metrics
|
||||
w.WriteHeader(202)
|
||||
fmt.Fprintf(w, `{"status":"ok"}`)
|
||||
return true
|
||||
case "datadog/api/v1/validate":
|
||||
datadogValidateRequests.Inc()
|
||||
// See https://docs.datadoghq.com/api/latest/authentication/#validate-api-key
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
fmt.Fprintf(w, `{"valid":true}`)
|
||||
return true
|
||||
case "datadog/api/v1/check_run":
|
||||
datadogCheckRunRequests.Inc()
|
||||
// See https://docs.datadoghq.com/api/latest/service-checks/#submit-a-service-check
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(202)
|
||||
fmt.Fprintf(w, `{"status":"ok"}`)
|
||||
return true
|
||||
case "datadog/intake/":
|
||||
datadogIntakeRequests.Inc()
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
fmt.Fprintf(w, `{}`)
|
||||
return true
|
||||
default:
|
||||
httpserver.Errorf(w, r, "unsupported multitenant path suffix: %q", p.Suffix)
|
||||
return true
|
||||
@@ -352,14 +436,26 @@ var (
|
||||
nativeimportRequests = metrics.NewCounter(`vmagent_http_requests_total{path="/api/v1/import/native", protocol="nativeimport"}`)
|
||||
nativeimportErrors = metrics.NewCounter(`vmagent_http_request_errors_total{path="/api/v1/import/native", protocol="nativeimport"}`)
|
||||
|
||||
influxWriteRequests = metrics.NewCounter(`vmagent_http_requests_total{path="/write", protocol="influx"}`)
|
||||
influxWriteErrors = metrics.NewCounter(`vmagent_http_request_errors_total{path="/write", protocol="influx"}`)
|
||||
influxWriteRequests = metrics.NewCounter(`vmagent_http_requests_total{path="/influx/write", protocol="influx"}`)
|
||||
influxWriteErrors = metrics.NewCounter(`vmagent_http_request_errors_total{path="/influx/write", protocol="influx"}`)
|
||||
|
||||
influxQueryRequests = metrics.NewCounter(`vmagent_http_requests_total{path="/query", protocol="influx"}`)
|
||||
influxQueryRequests = metrics.NewCounter(`vmagent_http_requests_total{path="/influx/query", protocol="influx"}`)
|
||||
|
||||
datadogWriteRequests = metrics.NewCounter(`vmagent_http_requests_total{path="/datadog/api/v1/series", protocol="datadog"}`)
|
||||
datadogWriteErrors = metrics.NewCounter(`vmagent_http_request_errors_total{path="/datadog/api/v1/series", protocol="datadog"}`)
|
||||
|
||||
datadogValidateRequests = metrics.NewCounter(`vmagent_http_requests_total{path="/datadog/api/v1/validate", protocol="datadog"}`)
|
||||
datadogCheckRunRequests = metrics.NewCounter(`vmagent_http_requests_total{path="/datadog/api/v1/check_run", protocol="datadog"}`)
|
||||
datadogIntakeRequests = metrics.NewCounter(`vmagent_http_requests_total{path="/datadog/intake/", protocol="datadog"}`)
|
||||
|
||||
promscrapeTargetsRequests = metrics.NewCounter(`vmagent_http_requests_total{path="/targets"}`)
|
||||
promscrapeAPIV1TargetsRequests = metrics.NewCounter(`vmagent_http_requests_total{path="/api/v1/targets"}`)
|
||||
|
||||
promscrapeTargetResponseRequests = metrics.NewCounter(`vmagent_http_requests_total{path="/target_response"}`)
|
||||
promscrapeTargetResponseErrors = metrics.NewCounter(`vmagent_http_request_errors_total{path="/target_response"}`)
|
||||
|
||||
promscrapeConfigRequests = metrics.NewCounter(`vmagent_http_requests_total{path="/config"}`)
|
||||
|
||||
promscrapeConfigReloadRequests = metrics.NewCounter(`vmagent_http_requests_total{path="/-/reload"}`)
|
||||
)
|
||||
|
||||
|
||||
@@ -37,10 +37,10 @@ func InsertHandler(at *auth.Token, req *http.Request) error {
|
||||
}
|
||||
|
||||
// InsertHandlerForReader processes metrics from given reader
|
||||
func InsertHandlerForReader(r io.Reader) error {
|
||||
func InsertHandlerForReader(at *auth.Token, r io.Reader) error {
|
||||
return writeconcurrencylimiter.Do(func() error {
|
||||
return parser.ParseStream(r, func(tss []prompb.TimeSeries) error {
|
||||
return insertRows(nil, tss, nil)
|
||||
return insertRows(at, tss, nil)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -67,7 +67,8 @@ type client struct {
|
||||
fq *persistentqueue.FastQueue
|
||||
hc *http.Client
|
||||
|
||||
authCfg *promauth.Config
|
||||
sendBlock func(block []byte) bool
|
||||
authCfg *promauth.Config
|
||||
|
||||
rl rateLimiter
|
||||
|
||||
@@ -84,7 +85,7 @@ type client struct {
|
||||
stopCh chan struct{}
|
||||
}
|
||||
|
||||
func newClient(argIdx int, remoteWriteURL, sanitizedURL string, fq *persistentqueue.FastQueue, concurrency int) *client {
|
||||
func newHTTPClient(argIdx int, remoteWriteURL, sanitizedURL string, fq *persistentqueue.FastQueue, concurrency int) *client {
|
||||
authCfg, err := getAuthConfig(argIdx)
|
||||
if err != nil {
|
||||
logger.Panicf("FATAL: cannot initialize auth config: %s", err)
|
||||
@@ -104,11 +105,11 @@ func newClient(argIdx int, remoteWriteURL, sanitizedURL string, fq *persistentqu
|
||||
if !strings.Contains(pURL, "://") {
|
||||
logger.Fatalf("cannot parse -remoteWrite.proxyURL=%q: it must start with `http://`, `https://` or `socks5://`", pURL)
|
||||
}
|
||||
urlProxy, err := url.Parse(pURL)
|
||||
pu, err := url.Parse(pURL)
|
||||
if err != nil {
|
||||
logger.Fatalf("cannot parse -remoteWrite.proxyURL=%q: %s", pURL, err)
|
||||
}
|
||||
tr.Proxy = http.ProxyURL(urlProxy)
|
||||
tr.Proxy = http.ProxyURL(pu)
|
||||
}
|
||||
c := &client{
|
||||
sanitizedURL: sanitizedURL,
|
||||
@@ -121,6 +122,11 @@ func newClient(argIdx int, remoteWriteURL, sanitizedURL string, fq *persistentqu
|
||||
},
|
||||
stopCh: make(chan struct{}),
|
||||
}
|
||||
c.sendBlock = c.sendBlockHTTP
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *client) init(argIdx, concurrency int, sanitizedURL string) {
|
||||
if bytesPerSec := rateLimit.GetOptionalArgOrDefault(argIdx, 0); bytesPerSec > 0 {
|
||||
logger.Infof("applying %d bytes per second rate limit for -remoteWrite.url=%q", bytesPerSec, sanitizedURL)
|
||||
c.rl.perSecondLimit = int64(bytesPerSec)
|
||||
@@ -143,7 +149,6 @@ func newClient(argIdx int, remoteWriteURL, sanitizedURL string, fq *persistentqu
|
||||
}()
|
||||
}
|
||||
logger.Infof("initialized client for -remoteWrite.url=%q", c.sanitizedURL)
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *client) MustStop() {
|
||||
@@ -160,7 +165,7 @@ func getAuthConfig(argIdx int) (*promauth.Config, error) {
|
||||
if username != "" || password != "" || passwordFile != "" {
|
||||
basicAuthCfg = &promauth.BasicAuthConfig{
|
||||
Username: username,
|
||||
Password: password,
|
||||
Password: promauth.NewSecret(password),
|
||||
PasswordFile: passwordFile,
|
||||
}
|
||||
}
|
||||
@@ -174,7 +179,7 @@ func getAuthConfig(argIdx int) (*promauth.Config, error) {
|
||||
if clientSecretFile != "" || clientSecret != "" {
|
||||
oauth2Cfg = &promauth.OAuth2Config{
|
||||
ClientID: oauth2ClientID.GetOptionalArg(argIdx),
|
||||
ClientSecret: clientSecret,
|
||||
ClientSecret: promauth.NewSecret(clientSecret),
|
||||
ClientSecretFile: clientSecretFile,
|
||||
TokenURL: oauth2TokenURL.GetOptionalArg(argIdx),
|
||||
Scopes: strings.Split(oauth2Scopes.GetOptionalArg(argIdx), ";"),
|
||||
@@ -237,9 +242,9 @@ func (c *client) runWorker() {
|
||||
}
|
||||
}
|
||||
|
||||
// sendBlock returns false only if c.stopCh is closed.
|
||||
// sendBlockHTTP returns false only if c.stopCh is closed.
|
||||
// Otherwise it tries sending the block to remote storage indefinitely.
|
||||
func (c *client) sendBlock(block []byte) bool {
|
||||
func (c *client) sendBlockHTTP(block []byte) bool {
|
||||
c.rl.register(len(block), c.stopCh)
|
||||
retryDuration := time.Second
|
||||
retriesCount := 0
|
||||
@@ -249,7 +254,7 @@ func (c *client) sendBlock(block []byte) bool {
|
||||
again:
|
||||
req, err := http.NewRequest("POST", c.remoteWriteURL, bytes.NewBuffer(block))
|
||||
if err != nil {
|
||||
logger.Panicf("BUG: unexected error from http.NewRequest(%q): %s", c.sanitizedURL, err)
|
||||
logger.Panicf("BUG: unexpected error from http.NewRequest(%q): %s", c.sanitizedURL, err)
|
||||
}
|
||||
h := req.Header
|
||||
h.Set("User-Agent", "vmagent")
|
||||
@@ -290,6 +295,17 @@ again:
|
||||
}
|
||||
metrics.GetOrCreateCounter(fmt.Sprintf(`vmagent_remotewrite_requests_total{url=%q, status_code="%d"}`, c.sanitizedURL, statusCode)).Inc()
|
||||
if statusCode == 409 || statusCode == 400 {
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
_ = resp.Body.Close()
|
||||
l := logger.WithThrottler("remoteWriteRejected", 5*time.Second)
|
||||
if err != nil {
|
||||
l.Errorf("sending a block with size %d bytes to %q was rejected (skipping the block): status code %d; "+
|
||||
"failed to read response body: %s",
|
||||
len(block), c.sanitizedURL, statusCode, err)
|
||||
} else {
|
||||
l.Errorf("sending a block with size %d bytes to %q was rejected (skipping the block): status code %d; response body: %s",
|
||||
len(block), c.sanitizedURL, statusCode, string(body))
|
||||
}
|
||||
// Just drop block on 409 and 400 status codes like Prometheus does.
|
||||
// See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/873
|
||||
// and https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1149
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/decimal"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/fasttime"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/flagutil"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/persistentqueue"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/prompbmarshal"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promrelabel"
|
||||
@@ -20,16 +21,10 @@ import (
|
||||
var (
|
||||
flushInterval = flag.Duration("remoteWrite.flushInterval", time.Second, "Interval for flushing the data to remote storage. "+
|
||||
"This option takes effect only when less than 10K data points per second are pushed to -remoteWrite.url")
|
||||
maxUnpackedBlockSize = flagutil.NewBytes("remoteWrite.maxBlockSize", 8*1024*1024, "The maximum size in bytes of unpacked request to send to remote storage. "+
|
||||
"It shouldn't exceed -maxInsertRequestSize from VictoriaMetrics")
|
||||
maxUnpackedBlockSize = flagutil.NewBytes("remoteWrite.maxBlockSize", 8*1024*1024, "The maximum block size to send to remote storage. Bigger blocks may improve performance at the cost of the increased memory usage. See also -remoteWrite.maxRowsPerBlock")
|
||||
maxRowsPerBlock = flag.Int("remoteWrite.maxRowsPerBlock", 10000, "The maximum number of samples to send in each block to remote storage. Higher number may improve performance at the cost of the increased memory usage. See also -remoteWrite.maxBlockSize")
|
||||
)
|
||||
|
||||
// the maximum number of rows to send per each block.
|
||||
const maxRowsPerBlock = 10000
|
||||
|
||||
// the maximum number of labels to send per each block.
|
||||
const maxLabelsPerBlock = 10 * maxRowsPerBlock
|
||||
|
||||
type pendingSeries struct {
|
||||
mu sync.Mutex
|
||||
wr writeRequest
|
||||
@@ -153,10 +148,13 @@ func (wr *writeRequest) adjustSampleValues() {
|
||||
|
||||
func (wr *writeRequest) push(src []prompbmarshal.TimeSeries) {
|
||||
tssDst := wr.tss
|
||||
maxSamplesPerBlock := *maxRowsPerBlock
|
||||
// Allow up to 10x of labels per each block on average.
|
||||
maxLabelsPerBlock := 10 * maxSamplesPerBlock
|
||||
for i := range src {
|
||||
tssDst = append(tssDst, prompbmarshal.TimeSeries{})
|
||||
wr.copyTimeSeries(&tssDst[len(tssDst)-1], &src[i])
|
||||
if len(wr.samples) >= maxRowsPerBlock || len(wr.labels) >= maxLabelsPerBlock {
|
||||
if len(wr.samples) >= maxSamplesPerBlock || len(wr.labels) >= maxLabelsPerBlock {
|
||||
wr.tss = tssDst
|
||||
wr.flush()
|
||||
tssDst = wr.tss
|
||||
@@ -213,7 +211,22 @@ func pushWriteRequest(wr *prompbmarshal.WriteRequest, pushBlock func(block []byt
|
||||
writeRequestBufPool.Put(bb)
|
||||
}
|
||||
|
||||
// Too big block. Recursively split it into smaller parts.
|
||||
// Too big block. Recursively split it into smaller parts if possible.
|
||||
if len(wr.Timeseries) == 1 {
|
||||
// A single time series left. Recursively split its samples into smaller parts if possible.
|
||||
samples := wr.Timeseries[0].Samples
|
||||
if len(samples) == 1 {
|
||||
logger.Warnf("dropping a sample for metric with too long labels exceeding -remoteWrite.maxBlockSize=%d bytes", maxUnpackedBlockSize.N)
|
||||
return
|
||||
}
|
||||
n := len(samples) / 2
|
||||
wr.Timeseries[0].Samples = samples[:n]
|
||||
pushWriteRequest(wr, pushBlock)
|
||||
wr.Timeseries[0].Samples = samples[n:]
|
||||
pushWriteRequest(wr, pushBlock)
|
||||
wr.Timeseries[0].Samples = samples
|
||||
return
|
||||
}
|
||||
timeseries := wr.Timeseries
|
||||
n := len(timeseries) / 2
|
||||
wr.Timeseries = timeseries[:n]
|
||||
|
||||
@@ -15,12 +15,14 @@ import (
|
||||
var (
|
||||
unparsedLabelsGlobal = flagutil.NewArray("remoteWrite.label", "Optional label in the form 'name=value' to add to all the metrics before sending them to -remoteWrite.url. "+
|
||||
"Pass multiple -remoteWrite.label flags in order to add multiple labels to metrics before sending them to remote storage")
|
||||
relabelConfigPathGlobal = flag.String("remoteWrite.relabelConfig", "", "Optional path to file with relabel_config entries. These entries are applied to all the metrics "+
|
||||
relabelConfigPathGlobal = flag.String("remoteWrite.relabelConfig", "", "Optional path to file with relabel_config entries. "+
|
||||
"The path can point either to local file or to http url. These entries are applied to all the metrics "+
|
||||
"before sending them to -remoteWrite.url. See https://docs.victoriametrics.com/vmagent.html#relabeling for details")
|
||||
relabelDebugGlobal = flag.Bool("remoteWrite.relabelDebug", false, "Whether to log metrics before and after relabeling with -remoteWrite.relabelConfig. "+
|
||||
"If the -remoteWrite.relabelDebug is enabled, then the metrics aren't sent to remote storage. This is useful for debugging the relabeling configs")
|
||||
relabelConfigPaths = flagutil.NewArray("remoteWrite.urlRelabelConfig", "Optional path to relabel config for the corresponding -remoteWrite.url")
|
||||
relabelDebug = flagutil.NewArrayBool("remoteWrite.urlRelabelDebug", "Whether to log metrics before and after relabeling with -remoteWrite.urlRelabelConfig. "+
|
||||
relabelConfigPaths = flagutil.NewArray("remoteWrite.urlRelabelConfig", "Optional path to relabel config for the corresponding -remoteWrite.url. "+
|
||||
"The path can point either to local file or to http url")
|
||||
relabelDebug = flagutil.NewArrayBool("remoteWrite.urlRelabelDebug", "Whether to log metrics before and after relabeling with -remoteWrite.urlRelabelConfig. "+
|
||||
"If the -remoteWrite.urlRelabelDebug is enabled, then the metrics aren't sent to the corresponding -remoteWrite.url. "+
|
||||
"This is useful for debugging the relabeling configs")
|
||||
)
|
||||
|
||||
@@ -3,6 +3,7 @@ package remotewrite
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
@@ -170,28 +171,32 @@ func newRemoteWriteCtxs(at *auth.Token, urls []string) []*remoteWriteCtx {
|
||||
logger.Panicf("BUG: urls must be non-empty")
|
||||
}
|
||||
|
||||
maxInmemoryBlocks := memory.Allowed() / len(urls) / maxRowsPerBlock / 100
|
||||
if maxInmemoryBlocks > 400 {
|
||||
maxInmemoryBlocks := memory.Allowed() / len(urls) / *maxRowsPerBlock / 100
|
||||
if maxInmemoryBlocks / *queues > 100 {
|
||||
// There is no much sense in keeping higher number of blocks in memory,
|
||||
// since this means that the producer outperforms consumer and the queue
|
||||
// will continue growing. It is better storing the queue to file.
|
||||
maxInmemoryBlocks = 400
|
||||
maxInmemoryBlocks = 100 * *queues
|
||||
}
|
||||
if maxInmemoryBlocks < 2 {
|
||||
maxInmemoryBlocks = 2
|
||||
}
|
||||
rwctxs := make([]*remoteWriteCtx, len(urls))
|
||||
for i, remoteWriteURL := range urls {
|
||||
for i, remoteWriteURLRaw := range urls {
|
||||
remoteWriteURL, err := url.Parse(remoteWriteURLRaw)
|
||||
if err != nil {
|
||||
logger.Fatalf("invalid -remoteWrite.url=%q: %s", remoteWriteURL, err)
|
||||
}
|
||||
sanitizedURL := fmt.Sprintf("%d:secret-url", i+1)
|
||||
if at != nil {
|
||||
// Construct full remote_write url for the given tenant according to https://docs.victoriametrics.com/Cluster-VictoriaMetrics.html#url-format
|
||||
remoteWriteURL = fmt.Sprintf("%s/insert/%d:%d/prometheus/api/v1/write", remoteWriteURL, at.AccountID, at.ProjectID)
|
||||
remoteWriteURL.Path = fmt.Sprintf("%s/insert/%d:%d/prometheus/api/v1/write", remoteWriteURL.Path, at.AccountID, at.ProjectID)
|
||||
sanitizedURL = fmt.Sprintf("%s:%d:%d", sanitizedURL, at.AccountID, at.ProjectID)
|
||||
}
|
||||
if *showRemoteWriteURL {
|
||||
sanitizedURL = fmt.Sprintf("%d:%s", i+1, remoteWriteURL)
|
||||
}
|
||||
rwctxs[i] = newRemoteWriteCtx(i, remoteWriteURL, maxInmemoryBlocks, sanitizedURL)
|
||||
rwctxs[i] = newRemoteWriteCtx(i, at, remoteWriteURL, maxInmemoryBlocks, sanitizedURL)
|
||||
}
|
||||
return rwctxs
|
||||
}
|
||||
@@ -269,6 +274,9 @@ func PushWithAuthToken(at *auth.Token, wr *prompbmarshal.WriteRequest) {
|
||||
rctx = getRelabelCtx()
|
||||
}
|
||||
tss := wr.Timeseries
|
||||
maxSamplesPerBlock := *maxRowsPerBlock
|
||||
// Allow up to 10x of labels per each block on average.
|
||||
maxLabelsPerBlock := 10 * maxSamplesPerBlock
|
||||
for len(tss) > 0 {
|
||||
// Process big tss in smaller blocks in order to reduce the maximum memory usage
|
||||
samplesCount := 0
|
||||
@@ -278,7 +286,7 @@ func PushWithAuthToken(at *auth.Token, wr *prompbmarshal.WriteRequest) {
|
||||
samplesCount += len(tss[i].Samples)
|
||||
labelsCount += len(tss[i].Labels)
|
||||
i++
|
||||
if samplesCount >= maxRowsPerBlock || labelsCount >= maxLabelsPerBlock {
|
||||
if samplesCount >= maxSamplesPerBlock || labelsCount >= maxLabelsPerBlock {
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -296,11 +304,7 @@ func PushWithAuthToken(at *auth.Token, wr *prompbmarshal.WriteRequest) {
|
||||
}
|
||||
sortLabelsIfNeeded(tssBlock)
|
||||
tssBlock = limitSeriesCardinality(tssBlock)
|
||||
if len(tssBlock) > 0 {
|
||||
for _, rwctx := range rwctxs {
|
||||
rwctx.Push(tssBlock)
|
||||
}
|
||||
}
|
||||
pushBlockToRemoteStorages(rwctxs, tssBlock)
|
||||
if rctx != nil {
|
||||
rctx.reset()
|
||||
}
|
||||
@@ -310,6 +314,23 @@ func PushWithAuthToken(at *auth.Token, wr *prompbmarshal.WriteRequest) {
|
||||
}
|
||||
}
|
||||
|
||||
func pushBlockToRemoteStorages(rwctxs []*remoteWriteCtx, tssBlock []prompbmarshal.TimeSeries) {
|
||||
if len(tssBlock) == 0 {
|
||||
// Nothing to push
|
||||
return
|
||||
}
|
||||
// Push block to remote storages in parallel in order to reduce the time needed for sending the data to multiple remote storage systems.
|
||||
var wg sync.WaitGroup
|
||||
for _, rwctx := range rwctxs {
|
||||
wg.Add(1)
|
||||
go func(rwctx *remoteWriteCtx) {
|
||||
defer wg.Done()
|
||||
rwctx.Push(tssBlock)
|
||||
}(rwctx)
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
// sortLabelsIfNeeded sorts labels if -sortLabels command-line flag is set.
|
||||
func sortLabelsIfNeeded(tss []prompbmarshal.TimeSeries) {
|
||||
if !*sortLabels {
|
||||
@@ -369,6 +390,8 @@ var labelsHashBufPool bytesutil.ByteBufferPool
|
||||
func logSkippedSeries(labels []prompbmarshal.Label, flagName string, flagValue int) {
|
||||
select {
|
||||
case <-logSkippedSeriesTicker.C:
|
||||
// Do not use logger.WithThrottler() here, since this will increase CPU usage
|
||||
// because every call to logSkippedSeries will result to a call to labelsToString.
|
||||
logger.Warnf("skip series %s because %s=%d reached", labelsToString(labels), flagName, flagValue)
|
||||
default:
|
||||
}
|
||||
@@ -403,17 +426,29 @@ type remoteWriteCtx struct {
|
||||
relabelMetricsDropped *metrics.Counter
|
||||
}
|
||||
|
||||
func newRemoteWriteCtx(argIdx int, remoteWriteURL string, maxInmemoryBlocks int, sanitizedURL string) *remoteWriteCtx {
|
||||
h := xxhash.Sum64([]byte(remoteWriteURL))
|
||||
path := fmt.Sprintf("%s/persistent-queue/%d_%016X", *tmpDataPath, argIdx+1, h)
|
||||
fq := persistentqueue.MustOpenFastQueue(path, sanitizedURL, maxInmemoryBlocks, maxPendingBytesPerURL.N)
|
||||
_ = metrics.GetOrCreateGauge(fmt.Sprintf(`vmagent_remotewrite_pending_data_bytes{path=%q, url=%q}`, path, sanitizedURL), func() float64 {
|
||||
func newRemoteWriteCtx(argIdx int, at *auth.Token, remoteWriteURL *url.URL, maxInmemoryBlocks int, sanitizedURL string) *remoteWriteCtx {
|
||||
// strip query params, otherwise changing params resets pq
|
||||
pqURL := *remoteWriteURL
|
||||
pqURL.RawQuery = ""
|
||||
pqURL.Fragment = ""
|
||||
h := xxhash.Sum64([]byte(pqURL.String()))
|
||||
queuePath := fmt.Sprintf("%s/persistent-queue/%d_%016X", *tmpDataPath, argIdx+1, h)
|
||||
fq := persistentqueue.MustOpenFastQueue(queuePath, sanitizedURL, maxInmemoryBlocks, maxPendingBytesPerURL.N)
|
||||
_ = metrics.GetOrCreateGauge(fmt.Sprintf(`vmagent_remotewrite_pending_data_bytes{path=%q, url=%q}`, queuePath, sanitizedURL), func() float64 {
|
||||
return float64(fq.GetPendingBytes())
|
||||
})
|
||||
_ = metrics.GetOrCreateGauge(fmt.Sprintf(`vmagent_remotewrite_pending_inmemory_blocks{path=%q, url=%q}`, path, sanitizedURL), func() float64 {
|
||||
_ = metrics.GetOrCreateGauge(fmt.Sprintf(`vmagent_remotewrite_pending_inmemory_blocks{path=%q, url=%q}`, queuePath, sanitizedURL), func() float64 {
|
||||
return float64(fq.GetInmemoryQueueLen())
|
||||
})
|
||||
c := newClient(argIdx, remoteWriteURL, sanitizedURL, fq, *queues)
|
||||
var c *client
|
||||
switch remoteWriteURL.Scheme {
|
||||
case "http", "https":
|
||||
c = newHTTPClient(argIdx, remoteWriteURL.String(), sanitizedURL, fq, *queues)
|
||||
default:
|
||||
logger.Fatalf("unsupported scheme: %s for remoteWriteURL: %s, want `http`, `https`", remoteWriteURL.Scheme, sanitizedURL)
|
||||
}
|
||||
c.init(argIdx, *queues, sanitizedURL)
|
||||
|
||||
sf := significantFigures.GetOptionalArgOrDefault(argIdx, 0)
|
||||
rd := roundDigits.GetOptionalArgOrDefault(argIdx, 100)
|
||||
pssLen := *queues
|
||||
@@ -432,7 +467,7 @@ func newRemoteWriteCtx(argIdx int, remoteWriteURL string, maxInmemoryBlocks int,
|
||||
c: c,
|
||||
pss: pss,
|
||||
|
||||
relabelMetricsDropped: metrics.GetOrCreateCounter(fmt.Sprintf(`vmagent_remotewrite_relabel_metrics_dropped_total{path=%q, url=%q}`, path, sanitizedURL)),
|
||||
relabelMetricsDropped: metrics.GetOrCreateCounter(fmt.Sprintf(`vmagent_remotewrite_relabel_metrics_dropped_total{path=%q, url=%q}`, queuePath, sanitizedURL)),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -27,6 +27,15 @@ vmalert-ppc64le-prod:
|
||||
vmalert-386-prod:
|
||||
APP_NAME=vmalert $(MAKE) app-via-docker-386
|
||||
|
||||
vmalert-darwin-amd64-prod:
|
||||
APP_NAME=vmalert $(MAKE) app-via-docker-darwin-amd64
|
||||
|
||||
vmalert-darwin-arm64-prod:
|
||||
APP_NAME=vmalert $(MAKE) app-via-docker-darwin-arm64
|
||||
|
||||
vmalert-windows-amd64-prod:
|
||||
APP_NAME=vmalert $(MAKE) app-via-docker-windows-amd64
|
||||
|
||||
package-vmalert:
|
||||
APP_NAME=vmalert $(MAKE) package-via-docker
|
||||
|
||||
@@ -70,6 +79,13 @@ run-vmalert: vmalert
|
||||
-evaluationInterval=3s \
|
||||
-rule.configCheckInterval=10s
|
||||
|
||||
run-vmalert-sd: vmalert
|
||||
./bin/vmalert -rule=app/vmalert/config/testdata/rules2-good.rules \
|
||||
-datasource.url=http://localhost:8428 \
|
||||
-remoteWrite.url=http://localhost:8428 \
|
||||
-notifier.config=app/vmalert/notifier/testdata/consul.good.yaml \
|
||||
-configCheckInterval=10s
|
||||
|
||||
replay-vmalert: vmalert
|
||||
./bin/vmalert -rule=app/vmalert/config/testdata/rules-replay-good.rules \
|
||||
-datasource.url=http://localhost:8428 \
|
||||
@@ -102,6 +118,3 @@ vmalert-pure:
|
||||
|
||||
vmalert-windows-amd64:
|
||||
GOARCH=amd64 APP_NAME=vmalert $(MAKE) app-local-windows-with-goarch
|
||||
|
||||
vmalert-windows-amd64-prod:
|
||||
APP_NAME=vmalert $(MAKE) app-via-docker-windows-amd64
|
||||
|
||||
@@ -2,7 +2,11 @@
|
||||
|
||||
`vmalert` executes a list of the given [alerting](https://prometheus.io/docs/prometheus/latest/configuration/alerting_rules/)
|
||||
or [recording](https://prometheus.io/docs/prometheus/latest/configuration/recording_rules/)
|
||||
rules against configured address. It is heavily inspired by [Prometheus](https://prometheus.io/docs/alerting/latest/overview/)
|
||||
rules against configured `-datasource.url`. For sending alerting notifications
|
||||
vmalert relies on [Alertmanager](https://github.com/prometheus/alertmanager) configured via `-notifier.url` flag.
|
||||
Recording rules results are persisted via [remote write](https://prometheus.io/docs/prometheus/latest/storage/#remote-storage-integrations)
|
||||
protocol and require `-remoteWrite.url` to be configured.
|
||||
Vmalert is heavily inspired by [Prometheus](https://prometheus.io/docs/alerting/latest/overview/)
|
||||
implementation and aims to be compatible with its syntax.
|
||||
|
||||
## Features
|
||||
@@ -11,19 +15,19 @@ implementation and aims to be compatible with its syntax.
|
||||
support and expressions validation;
|
||||
* Prometheus [alerting rules definition format](https://prometheus.io/docs/prometheus/latest/configuration/alerting_rules/#defining-alerting-rules)
|
||||
support;
|
||||
* Integration with [Alertmanager](https://github.com/prometheus/alertmanager);
|
||||
* Integration with [Alertmanager](https://github.com/prometheus/alertmanager) starting from [Alertmanager v0.16.0-aplha](https://github.com/prometheus/alertmanager/releases/tag/v0.16.0-alpha.0);
|
||||
* Keeps the alerts [state on restarts](#alerts-state-on-restarts);
|
||||
* Graphite datasource can be used for alerting and recording rules. See [these docs](#graphite);
|
||||
* Recording and Alerting rules backfilling (aka `replay`). See [these docs](#rules-backfilling);
|
||||
* Lightweight without extra dependencies.
|
||||
|
||||
## Limitations
|
||||
* `vmalert` execute queries against remote datasource which has reliability risks because of network.
|
||||
It is recommended to configure alerts thresholds and rules expressions with understanding that network request
|
||||
may fail;
|
||||
* by default, rules execution is sequential within one group, but persisting of execution results to remote
|
||||
storage is asynchronous. Hence, user shouldn't rely on recording rules chaining when result of previous
|
||||
recording rule is reused in next one;
|
||||
* `vmalert` execute queries against remote datasource which has reliability risks because of the network.
|
||||
It is recommended to configure alerts thresholds and rules expressions with the understanding that network
|
||||
requests may fail;
|
||||
* by default, rules execution is sequential within one group, but persistence of execution results to remote
|
||||
storage is asynchronous. Hence, user shouldn't rely on chaining of recording rules when result of previous
|
||||
recording rule is reused in the next one;
|
||||
|
||||
## QuickStart
|
||||
|
||||
@@ -33,29 +37,37 @@ git clone https://github.com/VictoriaMetrics/VictoriaMetrics
|
||||
cd VictoriaMetrics
|
||||
make vmalert
|
||||
```
|
||||
The build binary will be placed to `VictoriaMetrics/bin` folder.
|
||||
The build binary will be placed in `VictoriaMetrics/bin` folder.
|
||||
|
||||
To start using `vmalert` you will need the following things:
|
||||
* list of rules - PromQL/MetricsQL expressions to execute;
|
||||
* datasource address - reachable VictoriaMetrics instance for rules execution;
|
||||
* notifier address - reachable [Alert Manager](https://github.com/prometheus/alertmanager) instance for processing,
|
||||
aggregating alerts and sending notifications.
|
||||
* datasource address - reachable MetricsQL endpoint to run queries against;
|
||||
* notifier address [optional] - reachable [Alert Manager](https://github.com/prometheus/alertmanager) instance for processing,
|
||||
aggregating alerts, and sending notifications. Please note, notifier address also supports Consul Service Discovery via
|
||||
[config file](https://github.com/VictoriaMetrics/VictoriaMetrics/blob/master/app/vmalert/notifier/config.go).
|
||||
* remote write address [optional] - [remote write](https://prometheus.io/docs/prometheus/latest/storage/#remote-storage-integrations)
|
||||
compatible storage address for storing recording rules results and alerts state in for of timeseries.
|
||||
compatible storage to persist rules and alerts state info;
|
||||
* remote read address [optional] - MetricsQL compatible datasource to restore alerts state from.
|
||||
|
||||
Then configure `vmalert` accordingly:
|
||||
```
|
||||
./bin/vmalert -rule=alert.rules \ # Path to the file with rules configuration. Supports wildcard
|
||||
-datasource.url=http://localhost:8428 \ # PromQL compatible datasource
|
||||
-notifier.url=http://localhost:9093 \ # AlertManager URL
|
||||
-notifier.url=http://localhost:9093 \ # AlertManager URL (required if alerting rules are used)
|
||||
-notifier.url=http://127.0.0.1:9093 \ # AlertManager replica URL
|
||||
-remoteWrite.url=http://localhost:8428 \ # Remote write compatible storage to persist rules
|
||||
-remoteWrite.url=http://localhost:8428 \ # Remote write compatible storage to persist rules and alerts state info (required if recording rules are used)
|
||||
-remoteRead.url=http://localhost:8428 \ # MetricsQL compatible datasource to restore alerts state from
|
||||
-external.label=cluster=east-1 \ # External label to be applied for each rule
|
||||
-external.label=replica=a # Multiple external labels may be set
|
||||
```
|
||||
|
||||
See the fill list of configuration flags in [configuration](#configuration) section.
|
||||
Note there's a separate `remoteRead.url` to allow writing results of
|
||||
alerting/recording rules into a different storage than the initial data that's
|
||||
queried. This allows using `vmalert` to aggregate data from a short-term,
|
||||
high-frequency, high-cardinality storage into a long-term storage with
|
||||
decreased cardinality and a bigger interval between samples.
|
||||
|
||||
See the full list of configuration flags in [configuration](#configuration) section.
|
||||
|
||||
If you run multiple `vmalert` services for the same datastore or AlertManager - do not forget
|
||||
to specify different `external.label` flags in order to define which `vmalert` generated rules or alerts.
|
||||
@@ -85,15 +97,27 @@ name: <string>
|
||||
[ concurrency: <integer> | default = 1 ]
|
||||
|
||||
# Optional type for expressions inside the rules. Supported values: "graphite" and "prometheus".
|
||||
# By default "prometheus" rule type is used.
|
||||
# By default "prometheus" type is used.
|
||||
[ type: <string> ]
|
||||
|
||||
# Optional list of label filters applied to every rule's
|
||||
# request withing a group. Is compatible only with VM datasource.
|
||||
# See more details at https://docs.victoriametrics.com#prometheus-querying-api-enhancements
|
||||
# Warning: DEPRECATED
|
||||
# Please use `params` instead:
|
||||
# params:
|
||||
# extra_label: ["job=nodeexporter", "env=prod"]
|
||||
extra_filter_labels:
|
||||
[ <labelname>: <labelvalue> ... ]
|
||||
|
||||
# Optional list of HTTP URL parameters
|
||||
# applied for all rules requests within a group
|
||||
# For example:
|
||||
# params:
|
||||
# nocache: ["1"] # disable caching for vmselect
|
||||
# denyPartialResponse: ["true"] # fail if one or more vmstorage nodes returned an error
|
||||
# extra_label: ["env=dev"] # apply additional label filter "env=dev" for all requests
|
||||
# see more details at https://docs.victoriametrics.com#prometheus-querying-api-enhancements
|
||||
params:
|
||||
[ <string>: [<string>, ...]]
|
||||
|
||||
# Optional list of labels added to every rule within a group.
|
||||
# It has priority over the external labels.
|
||||
# Labels are commonly used for adding environment
|
||||
@@ -113,14 +137,14 @@ expression and then act according to the Rule type.
|
||||
|
||||
There are two types of Rules:
|
||||
* [alerting](https://prometheus.io/docs/prometheus/latest/configuration/alerting_rules/) -
|
||||
Alerting rules allows to define alert conditions via `expr` field and to send notifications
|
||||
Alerting rules allow defining alert conditions via `expr` field and to send notifications to
|
||||
[Alertmanager](https://github.com/prometheus/alertmanager) if execution result is not empty.
|
||||
* [recording](https://prometheus.io/docs/prometheus/latest/configuration/recording_rules/) -
|
||||
Recording rules allows to define `expr` which result will be than backfilled to configured
|
||||
Recording rules allow defining `expr` which result will be then backfilled to configured
|
||||
`-remoteWrite.url`. Recording rules are used to precompute frequently needed or computationally
|
||||
expensive expressions and save their result as a new set of time series.
|
||||
|
||||
`vmalert` forbids to define duplicates - rules with the same combination of name, expression and labels
|
||||
`vmalert` forbids defining duplicates - rules with the same combination of name, expression, and labels
|
||||
within one group.
|
||||
|
||||
#### Alerting rules
|
||||
@@ -130,17 +154,13 @@ The syntax for alerting rule is the following:
|
||||
# The name of the alert. Must be a valid metric name.
|
||||
alert: <string>
|
||||
|
||||
# Optional type for the rule. Supported values: "graphite", "prometheus".
|
||||
# By default "prometheus" rule type is used.
|
||||
[ type: <string> ]
|
||||
|
||||
# The expression to evaluate. The expression language depends on the type value.
|
||||
# By default PromQL/MetricsQL expression is used. If type="graphite", then the expression
|
||||
# By default PromQL/MetricsQL expression is used. If group.type="graphite", then the expression
|
||||
# must contain valid Graphite expression.
|
||||
expr: <string>
|
||||
|
||||
# Alerts are considered firing once they have been returned for this long.
|
||||
# Alerts which have not yet fired for long enough are considered pending.
|
||||
# Alerts which have not yet been fired for long enough are considered pending.
|
||||
# If param is omitted or set to 0 then alerts will be immediately considered
|
||||
# as firing once they return.
|
||||
[ for: <duration> | default = 0s ]
|
||||
@@ -166,12 +186,8 @@ The syntax for recording rules is following:
|
||||
# The name of the time series to output to. Must be a valid metric name.
|
||||
record: <string>
|
||||
|
||||
# Optional type for the rule. Supported values: "graphite", "prometheus".
|
||||
# By default "prometheus" rule type is used.
|
||||
[ type: <string> ]
|
||||
|
||||
# The expression to evaluate. The expression language depends on the type value.
|
||||
# By default MetricsQL expression is used. If type="graphite", then the expression
|
||||
# By default MetricsQL expression is used. If group.type="graphite", then the expression
|
||||
# must contain valid Graphite expression.
|
||||
expr: <string>
|
||||
|
||||
@@ -189,19 +205,19 @@ For recording rules to work `-remoteWrite.url` must be specified.
|
||||
the process alerts state will be lost. To avoid this situation, `vmalert` should be configured via the following flags:
|
||||
* `-remoteWrite.url` - URL to VictoriaMetrics (Single) or vminsert (Cluster). `vmalert` will persist alerts state
|
||||
into the configured address in the form of time series named `ALERTS` and `ALERTS_FOR_STATE` via remote-write protocol.
|
||||
These are regular time series and may be queried from VM just as any other time series.
|
||||
The state stored to the configured address on every rule evaluation.
|
||||
These are regular time series and maybe queried from VM just as any other time series.
|
||||
The state is stored to the configured address on every rule evaluation.
|
||||
* `-remoteRead.url` - URL to VictoriaMetrics (Single) or vmselect (Cluster). `vmalert` will try to restore alerts state
|
||||
from configured address by querying time series with name `ALERTS_FOR_STATE`.
|
||||
|
||||
Both flags are required for the proper state restoring. Restore process may fail if time series are missing
|
||||
Both flags are required for proper state restoration. Restore process may fail if time series are missing
|
||||
in configured `-remoteRead.url`, weren't updated in the last `1h` (controlled by `-remoteRead.lookback`)
|
||||
or received state doesn't match current `vmalert` rules configuration.
|
||||
|
||||
|
||||
### Multitenancy
|
||||
|
||||
There are the following approaches for alerting and recording rules across
|
||||
There are the following approaches exist for alerting and recording rules across
|
||||
[multiple tenants](https://docs.victoriametrics.com/Cluster-VictoriaMetrics.html#multitenancy):
|
||||
|
||||
* To run a separate `vmalert` instance per each tenant.
|
||||
@@ -214,7 +230,7 @@ There are the following approaches for alerting and recording rules across
|
||||
rules to `AccountID=123`.
|
||||
|
||||
* To specify `tenant` parameter per each alerting and recording group if
|
||||
[enterprise version of vmalert](https://victoriametrics.com/enterprise.html) is used
|
||||
[enterprise version of vmalert](https://victoriametrics.com/products/enterprise/) is used
|
||||
with `-clusterMode` command-line flag. For example:
|
||||
|
||||
```yaml
|
||||
@@ -234,12 +250,121 @@ If `-clusterMode` is enabled, then `-datasource.url`, `-remoteRead.url` and `-re
|
||||
contain only the hostname without tenant id. For example: `-datasource.url=http://vmselect:8481`.
|
||||
`vmalert` automatically adds the specified tenant to urls per each recording rule in this case.
|
||||
|
||||
If `-clusterMode` is enabled and the `tenant` in a particular group is missing, then the tenant value
|
||||
is obtained from `-defaultTenant.prometheus` or `-defaultTenant.graphite` depending on the `type` of the group.
|
||||
|
||||
The enterprise version of vmalert is available in `vmutils-*-enterprise.tar.gz` files
|
||||
at [release page](https://github.com/VictoriaMetrics/VictoriaMetrics/releases) and in `*-enterprise`
|
||||
tags at [Docker Hub](https://hub.docker.com/r/victoriametrics/vmalert/tags).
|
||||
|
||||
### Topology examples
|
||||
|
||||
### WEB
|
||||
The following sections are showing how `vmalert` may be used and configured
|
||||
for different scenarios.
|
||||
|
||||
Please note, not all flags in examples are required:
|
||||
* `-remoteWrite.url` and `-remoteRead.url` are optional and are needed only if
|
||||
you have recording rules or want to store [alerts state](#alerts-state-on-restarts) on `vmalert` restarts;
|
||||
* `-notifier.url` is optional and is needed only if you have alerting rules.
|
||||
|
||||
#### Single-node VictoriaMetrics
|
||||
|
||||
The simplest configuration where one single-node VM server is used for
|
||||
rules execution, storing recording rules results and alerts state.
|
||||
|
||||
`vmalert` configuration flags:
|
||||
```
|
||||
./bin/vmalert -rule=rules.yml \ # Path to the file with rules configuration. Supports wildcard
|
||||
-datasource.url=http://victoriametrics:8428 \ # VM-single addr for executing rules expressions
|
||||
-remoteWrite.url=http://victoriametrics:8428 \ # VM-single addr to persist alerts state and recording rules results
|
||||
-remoteRead.url=http://victoriametrics:8428 \ # VM-single addr for restoring alerts state after restart
|
||||
-notifier.url=http://alertmanager:9093 # AlertManager addr to send alerts when they trigger
|
||||
```
|
||||
|
||||
<img alt="vmalert single" width="500" src="vmalert_single.png">
|
||||
|
||||
|
||||
#### Cluster VictoriaMetrics
|
||||
|
||||
In [cluster mode](https://docs.victoriametrics.com/Cluster-VictoriaMetrics.html)
|
||||
VictoriaMetrics has separate components for writing and reading path:
|
||||
`vminsert` and `vmselect` components respectively. `vmselect` is used for executing rules expressions
|
||||
and `vminsert` is used to persist recording rules results and alerts state.
|
||||
Cluster mode could have multiple `vminsert` and `vmselect` components.
|
||||
|
||||
`vmalert` configuration flags:
|
||||
```
|
||||
./bin/vmalert -rule=rules.yml \ # Path to the file with rules configuration. Supports wildcard
|
||||
-datasource.url=http://vmselect:8481/select/0/prometheus # vmselect addr for executing rules expressions
|
||||
-remoteWrite.url=http://vminsert:8480/insert/0/prometheus # vminsert addr to persist alerts state and recording rules results
|
||||
-remoteRead.url=http://vmselect:8481/select/0/prometheus # vmselect addr for restoring alerts state after restart
|
||||
-notifier.url=http://alertmanager:9093 # AlertManager addr to send alerts when they trigger
|
||||
```
|
||||
|
||||
<img alt="vmalert cluster" src="vmalert_cluster.png">
|
||||
|
||||
In case when you want to spread the load on these components - add balancers before them and configure
|
||||
`vmalert` with balancer's addresses. Please, see more about VM's cluster architecture
|
||||
[here](https://docs.victoriametrics.com/Cluster-VictoriaMetrics.html#architecture-overview).
|
||||
|
||||
#### HA vmalert
|
||||
|
||||
For HA user can run multiple identically configured `vmalert` instances.
|
||||
It means all of them will execute the same rules, write state and results to
|
||||
the same destinations, and send alert notifications to multiple configured
|
||||
Alertmanagers.
|
||||
|
||||
`vmalert` configuration flags:
|
||||
```
|
||||
./bin/vmalert -rule=rules.yml \ # Path to the file with rules configuration. Supports wildcard
|
||||
-datasource.url=http://victoriametrics:8428 \ # VM-single addr for executing rules expressions
|
||||
-remoteWrite.url=http://victoriametrics:8428 \ # VM-single addr to persist alerts state and recording rules results
|
||||
-remoteRead.url=http://victoriametrics:8428 \ # VM-single addr for restoring alerts state after restart
|
||||
-notifier.url=http://alertmanager1:9093 \ # Multiple AlertManager addresses to send alerts when they trigger
|
||||
-notifier.url=http://alertmanagerN:9093 # The same alert will be sent to all configured notifiers
|
||||
```
|
||||
|
||||
<img alt="vmalert ha" width="800px" src="vmalert_ha.png">
|
||||
|
||||
To avoid recording rules results and alerts state duplication in VictoriaMetrics server
|
||||
don't forget to configure [deduplication](https://docs.victoriametrics.com/Single-server-VictoriaMetrics.html#deduplication).
|
||||
|
||||
Alertmanager will automatically deduplicate alerts with identical labels, so ensure that
|
||||
all `vmalert`s are having the same config.
|
||||
|
||||
Don't forget to configure [cluster mode](https://prometheus.io/docs/alerting/latest/alertmanager/)
|
||||
for Alertmanagers for better reliability.
|
||||
|
||||
This example uses single-node VM server for the sake of simplicity.
|
||||
Check how to replace it with [cluster VictoriaMetrics](#cluster-victoriametrics) if needed.
|
||||
|
||||
|
||||
#### Downsampling and aggregation via vmalert
|
||||
|
||||
The following example shows how to build a topology where `vmalert` will process data from one cluster
|
||||
and write results into another. Such clusters may be called as "hot" (low retention,
|
||||
high-speed disks, used for operative monitoring) and "cold" (long term retention,
|
||||
slower/cheaper disks, low resolution data). With help of `vmalert`, user can setup
|
||||
recording rules to process raw data from "hot" cluster (by applying additional transformations
|
||||
or reducing resolution) and push results to "cold" cluster.
|
||||
|
||||
`vmalert` configuration flags:
|
||||
```
|
||||
./bin/vmalert -rule=downsampling-rules.yml \ # Path to the file with rules configuration. Supports wildcard
|
||||
-datasource.url=http://raw-cluster-vmselect:8481/select/0/prometheus # vmselect addr for executing recordi ng rules expressions
|
||||
-remoteWrite.url=http://aggregated-cluster-vminsert:8480/insert/0/prometheus # vminsert addr to persist recording rules results
|
||||
```
|
||||
|
||||
<img alt="vmalert multi cluster" src="vmalert_multicluster.png">
|
||||
|
||||
Please note, [replay](#rules-backfilling) feature may be used for transforming historical data.
|
||||
|
||||
Flags `-remoteRead.url` and `-notifier.url` are omitted since we assume only recording rules are used.
|
||||
|
||||
See also [downsampling docs](https://docs.victoriametrics.com/#downsampling).
|
||||
|
||||
|
||||
### Web
|
||||
|
||||
`vmalert` runs a web-server (`-httpListenAddr`) for serving metrics and alerts endpoints:
|
||||
* `http://<vmalert-addr>` - UI;
|
||||
@@ -257,12 +382,12 @@ vmalert sends requests to `<-datasource.url>/render?format=json` during evaluati
|
||||
if the corresponding group or rule contains `type: "graphite"` config option. It is expected that the `<-datasource.url>/render`
|
||||
implements [Graphite Render API](https://graphite.readthedocs.io/en/stable/render_api.html) for `format=json`.
|
||||
When using vmalert with both `graphite` and `prometheus` rules configured against cluster version of VM do not forget
|
||||
to set `-datasource.appendTypePrefix` flag to `true`, so vmalert can adjust URL prefix automatically based on query type.
|
||||
to set `-datasource.appendTypePrefix` flag to `true`, so vmalert can adjust URL prefix automatically based on the query type.
|
||||
|
||||
## Rules backfilling
|
||||
|
||||
vmalert supports alerting and recording rules backfilling (aka `replay`). In replay mode vmalert
|
||||
can read the same rules configuration as normally, evaluate them on the given time range and backfill
|
||||
can read the same rules configuration as normal, evaluate them on the given time range and backfill
|
||||
results via remote write to the configured storage. vmalert supports any PromQL/MetricsQL compatible
|
||||
data source for backfilling.
|
||||
|
||||
@@ -308,19 +433,19 @@ max range per request: 8h20m0s
|
||||
In `replay` mode all groups are executed sequentially one-by-one. Rules within the group are
|
||||
executed sequentially as well (`concurrency` setting is ignored). Vmalert sends rule's expression
|
||||
to [/query_range](https://prometheus.io/docs/prometheus/latest/querying/api/#range-queries) endpoint
|
||||
of the configured `-datasource.url`. Returned data then processed according to the rule type and
|
||||
backfilled to `-remoteWrite.url` via [Remote Write protocol](https://prometheus.io/docs/prometheus/latest/storage/#remote-storage-integrations).
|
||||
of the configured `-datasource.url`. Returned data is then processed according to the rule type and
|
||||
backfilled to `-remoteWrite.url` via [remote Write protocol](https://prometheus.io/docs/prometheus/latest/storage/#remote-storage-integrations).
|
||||
Vmalert respects `evaluationInterval` value set by flag or per-group during the replay.
|
||||
Vmalert automatically disables caching on VictoriaMetrics side by sending `nocache=1` param. It allows
|
||||
to prevent cache pollution and unwanted time range boundaries adjustment during backfilling.
|
||||
|
||||
#### Recording rules
|
||||
|
||||
Result of recording rules `replay` should match with results of normal rules evaluation.
|
||||
The result of recording rules `replay` should match with results of normal rules evaluation.
|
||||
|
||||
#### Alerting rules
|
||||
|
||||
Result of alerting rules `replay` is time series reflecting [alert's state](#alerts-state-on-restarts).
|
||||
The result of alerting rules `replay` is time series reflecting [alert's state](#alerts-state-on-restarts).
|
||||
To see if `replayed` alert has fired in the past use the following PromQL/MetricsQL expression:
|
||||
```
|
||||
ALERTS{alertname="your_alertname", alertstate="firing"}
|
||||
@@ -333,7 +458,7 @@ There are following non-required `replay` flags:
|
||||
|
||||
* `-replay.maxDatapointsPerQuery` - the max number of data points expected to receive in one request.
|
||||
In two words, it affects the max time range for every `/query_range` request. The higher the value,
|
||||
the less requests will be issued during `replay`.
|
||||
the fewer requests will be issued during `replay`.
|
||||
* `-replay.ruleRetryAttempts` - when datasource fails to respond vmalert will make this number of retries
|
||||
per rule before giving up.
|
||||
* `-replay.rulesDelay` - delay between sequential rules execution. Important in cases if there are chaining
|
||||
@@ -351,22 +476,28 @@ See full description for these flags in `./vmalert --help`.
|
||||
|
||||
## Monitoring
|
||||
|
||||
`vmalert` exports various metrics in Prometheus exposition format at `http://vmalert-host:8880/metrics` page.
|
||||
We recommend setting up regular scraping of this page either through `vmagent` or by Prometheus so that the exported
|
||||
`vmalert` exports various metrics in Prometheus exposition format at `http://vmalert-host:8880/metrics` page.
|
||||
We recommend setting up regular scraping of this page either through `vmagent` or by Prometheus so that the exported
|
||||
metrics may be analyzed later.
|
||||
|
||||
Use official [Grafana dashboard](https://grafana.com/grafana/dashboards/14950) for `vmalert` overview.
|
||||
If you have suggestions for improvements or have found a bug - please open an issue on github or add
|
||||
Use the official [Grafana dashboard](https://grafana.com/grafana/dashboards/14950) for `vmalert` overview. Graphs on this dashboard contain useful hints - hover the `i` icon at the top left corner of each graph in order to read it.
|
||||
If you have suggestions for improvements or have found a bug - please open an issue on github or add
|
||||
a review to the dashboard.
|
||||
|
||||
|
||||
## Configuration
|
||||
|
||||
### Flags
|
||||
|
||||
Pass `-help` to `vmalert` in order to see the full list of supported
|
||||
command-line flags with their descriptions.
|
||||
|
||||
The shortlist of configuration flags is the following:
|
||||
```
|
||||
-clusterMode
|
||||
If clusterMode is enabled, then vmalert automatically adds the tenant specified in config groups to -datasource.url, -remoteWrite.url and -remoteRead.url. See https://docs.victoriametrics.com/vmalert.html#multitenancy
|
||||
-configCheckInterval duration
|
||||
Interval for checking for changes in '-rule' or '-notifier.config' files. By default the checking is disabled. Send SIGHUP signal in order to force config check for changes.
|
||||
-datasource.appendTypePrefix
|
||||
Whether to add type prefix to -datasource.url based on the query type. Set to true if sending different query types to the vmselect URL.
|
||||
-datasource.basicAuth.password string
|
||||
@@ -383,8 +514,20 @@ The shortlist of configuration flags is the following:
|
||||
Lookback defines how far into the past to look when evaluating queries. For example, if the datasource.lookback=5m then param "time" with value now()-5m will be added to every query.
|
||||
-datasource.maxIdleConnections int
|
||||
Defines the number of idle (keep-alive connections) to each configured datasource. Consider setting this value equal to the value: groups_total * group.concurrency. Too low a value may result in a high number of sockets in TIME_WAIT state. (default 100)
|
||||
-datasource.oauth2.clientID string
|
||||
Optional OAuth2 clientID to use for -datasource.url.
|
||||
-datasource.oauth2.clientSecret string
|
||||
Optional OAuth2 clientSecret to use for -datasource.url.
|
||||
-datasource.oauth2.clientSecretFile string
|
||||
Optional OAuth2 clientSecretFile to use for -datasource.url.
|
||||
-datasource.oauth2.scopes string
|
||||
Optional OAuth2 scopes to use for -datasource.url. Scopes must be delimited by ';'
|
||||
-datasource.oauth2.tokenUrl string
|
||||
Optional OAuth2 tokenURL to use for -datasource.url.
|
||||
-datasource.queryStep duration
|
||||
queryStep defines how far a value can fallback to when evaluating queries. For example, if datasource.queryStep=15s then param "step" with value "15s" will be added to every query.If queryStep isn't specified, rule's evaluationInterval will be used instead.
|
||||
-datasource.queryTimeAlignment
|
||||
Whether to align "time" parameter with evaluation interval.Alignment supposed to produce deterministic results despite of number of vmalert replicas or time they were started. See more details here https://github.com/VictoriaMetrics/VictoriaMetrics/pull/1257 (default true)
|
||||
-datasource.roundDigits int
|
||||
Adds "round_digits" GET param to datasource requests. In VM "round_digits" limits the number of digits after the decimal point in response values.
|
||||
-datasource.tlsCAFile string
|
||||
@@ -399,8 +542,12 @@ The shortlist of configuration flags is the following:
|
||||
Optional TLS server name to use for connections to -datasource.url. By default, the server name from -datasource.url is used
|
||||
-datasource.url string
|
||||
VictoriaMetrics or vmselect url. Required parameter. E.g. http://127.0.0.1:8428
|
||||
-defaultTenant.graphite string
|
||||
Default tenant for Graphite alerting groups. See https://docs.victoriametrics.com/vmalert.html#multitenancy
|
||||
-defaultTenant.prometheus string
|
||||
Default tenant for Prometheus alerting groups. See https://docs.victoriametrics.com/vmalert.html#multitenancy
|
||||
-disableAlertgroupLabel
|
||||
Whether to disable adding group's name as label to generated alerts and time series.
|
||||
Whether to disable adding group's Name as label to generated alerts and time series.
|
||||
-dryRun -rule
|
||||
Whether to check only config files without running vmalert. The rules file are validated. The -rule flag must be specified.
|
||||
-enableTCP6
|
||||
@@ -409,13 +556,15 @@ The shortlist of configuration flags is the following:
|
||||
Whether to enable reading flags from environment variables additionally to command line. Command line flag values have priority over values from environment vars. Flags are read only from command line if this flag isn't set. See https://docs.victoriametrics.com/#environment-variables for more details
|
||||
-envflag.prefix string
|
||||
Prefix for environment variables if -envflag.enable is set
|
||||
-eula
|
||||
By specifying this flag, you confirm that you have an enterprise license and accept the EULA https://victoriametrics.com/assets/VM_EULA.pdf
|
||||
-evaluationInterval duration
|
||||
How often to evaluate the rules (default 1m0s)
|
||||
-external.alert.source string
|
||||
External Alert Source allows to override the Source link for alerts sent to AlertManager for cases where you want to build a custom link to Grafana, Prometheus or any other service.
|
||||
eg. 'explore?orgId=1&left=[\"now-1h\",\"now\",\"VictoriaMetrics\",{\"expr\": \"{{$expr|quotesEscape|crlfEscape|queryEscape}}\"},{\"mode\":\"Metrics\"},{\"ui\":[true,true,true,\"none\"]}]'.If empty '/api/v1/:groupID/alertID/status' is used
|
||||
-external.label array
|
||||
Optional label in the form 'name=value' to add to all generated recording rules and alerts. Pass multiple -label flags in order to add multiple label sets.
|
||||
Optional label in the form 'Name=value' to add to all generated recording rules and alerts. Pass multiple -label flags in order to add multiple label sets.
|
||||
Supports an array of values separated by comma or specified via multiple flags.
|
||||
-external.url string
|
||||
External URL is used as alert's source for sent alerts to the notifier
|
||||
@@ -459,13 +608,41 @@ The shortlist of configuration flags is the following:
|
||||
-memory.allowedPercent float
|
||||
Allowed percent of system memory VictoriaMetrics caches may occupy. See also -memory.allowedBytes. Too low a value may increase cache miss rate usually resulting in higher CPU and disk IO usage. Too high a value may evict too much data from OS page cache which will result in higher disk IO usage (default 60)
|
||||
-metricsAuthKey string
|
||||
Auth key for /metrics. It overrides httpAuth settings
|
||||
Auth key for /metrics. It must be passed via authKey query arg. It overrides httpAuth.* settings
|
||||
-notifier.basicAuth.password array
|
||||
Optional basic auth password for -notifier.url
|
||||
Supports an array of values separated by comma or specified via multiple flags.
|
||||
-notifier.basicAuth.passwordFile array
|
||||
Optional path to basic auth password file for -notifier.url
|
||||
Supports an array of values separated by comma or specified via multiple flags.
|
||||
-notifier.basicAuth.username array
|
||||
Optional basic auth username for -notifier.url
|
||||
Supports an array of values separated by comma or specified via multiple flags.
|
||||
-notifier.bearerToken array
|
||||
Optional bearer token for -notifier.url
|
||||
Supports an array of values separated by comma or specified via multiple flags.
|
||||
-notifier.bearerTokenFile array
|
||||
Optional path to bearer token file for -notifier.url
|
||||
Supports an array of values separated by comma or specified via multiple flags.
|
||||
-notifier.config string
|
||||
Path to configuration file for notifiers
|
||||
-notifier.oauth2.clientID array
|
||||
Optional OAuth2 clientID to use for -notifier.url. If multiple args are set, then they are applied independently for the corresponding -notifier.url
|
||||
Supports an array of values separated by comma or specified via multiple flags.
|
||||
-notifier.oauth2.clientSecret array
|
||||
Optional OAuth2 clientSecret to use for -notifier.url. If multiple args are set, then they are applied independently for the corresponding -notifier.url
|
||||
Supports an array of values separated by comma or specified via multiple flags.
|
||||
-notifier.oauth2.clientSecretFile array
|
||||
Optional OAuth2 clientSecretFile to use for -notifier.url. If multiple args are set, then they are applied independently for the corresponding -notifier.url
|
||||
Supports an array of values separated by comma or specified via multiple flags.
|
||||
-notifier.oauth2.scopes array
|
||||
Optional OAuth2 scopes to use for -notifier.url. Scopes must be delimited by ';'. If multiple args are set, then they are applied independently for the corresponding -notifier.url
|
||||
Supports an array of values separated by comma or specified via multiple flags.
|
||||
-notifier.oauth2.tokenUrl array
|
||||
Optional OAuth2 tokenURL to use for -notifier.url. If multiple args are set, then they are applied independently for the corresponding -notifier.url
|
||||
Supports an array of values separated by comma or specified via multiple flags.
|
||||
-notifier.suppressDuplicateTargetErrors
|
||||
Whether to suppress 'duplicate target' errors during discovery
|
||||
-notifier.tlsCAFile array
|
||||
Optional path to TLS CA file to use for verifying connections to -notifier.url. By default system CA is used
|
||||
Supports an array of values separated by comma or specified via multiple flags.
|
||||
@@ -482,10 +659,18 @@ The shortlist of configuration flags is the following:
|
||||
Optional TLS server name to use for connections to -notifier.url. By default the server name from -notifier.url is used
|
||||
Supports an array of values separated by comma or specified via multiple flags.
|
||||
-notifier.url array
|
||||
Prometheus alertmanager URL. Required parameter. e.g. http://127.0.0.1:9093
|
||||
Prometheus alertmanager URL, e.g. http://127.0.0.1:9093
|
||||
Supports an array of values separated by comma or specified via multiple flags.
|
||||
-pprofAuthKey string
|
||||
Auth key for /debug/pprof. It overrides httpAuth settings
|
||||
Auth key for /debug/pprof. It must be passed via authKey query arg. It overrides httpAuth.* settings
|
||||
-promscrape.consul.waitTime duration
|
||||
Wait time used by Consul service discovery. Default value is used if not set
|
||||
-promscrape.consulSDCheckInterval duration
|
||||
Interval for checking for changes in Consul. This works only if consul_sd_configs is configured in '-promscrape.config' file. See https://prometheus.io/docs/prometheus/latest/configuration/configuration/#consul_sd_config for details (default 30s)
|
||||
-promscrape.discovery.concurrency int
|
||||
The maximum number of concurrent requests to Prometheus autodiscovery API (Consul, Kubernetes, etc.) (default 100)
|
||||
-promscrape.discovery.concurrentWaitTime duration
|
||||
The maximum duration for waiting to perform API requests if more than -promscrape.discovery.concurrency requests are simultaneously performed (default 1m0s)
|
||||
-remoteRead.basicAuth.password string
|
||||
Optional basic auth password for -remoteRead.url
|
||||
-remoteRead.basicAuth.passwordFile string
|
||||
@@ -496,10 +681,22 @@ The shortlist of configuration flags is the following:
|
||||
Optional bearer auth token to use for -remoteRead.url.
|
||||
-remoteRead.bearerTokenFile string
|
||||
Optional path to bearer token file to use for -remoteRead.url.
|
||||
-remoteRead.disablePathAppend
|
||||
Whether to disable automatic appending of '/api/v1/query' path to the configured -remoteRead.url.
|
||||
-remoteRead.ignoreRestoreErrors
|
||||
Whether to ignore errors from remote storage when restoring alerts state on startup. (default true)
|
||||
-remoteRead.lookback duration
|
||||
Lookback defines how far to look into past for alerts timeseries. For example, if lookback=1h then range from now() to now()-1h will be scanned. (default 1h0m0s)
|
||||
-remoteRead.oauth2.clientID string
|
||||
Optional OAuth2 clientID to use for -remoteRead.url.
|
||||
-remoteRead.oauth2.clientSecret string
|
||||
Optional OAuth2 clientSecret to use for -remoteRead.url.
|
||||
-remoteRead.oauth2.clientSecretFile string
|
||||
Optional OAuth2 clientSecretFile to use for -remoteRead.url.
|
||||
-remoteRead.oauth2.scopes string
|
||||
Optional OAuth2 scopes to use for -remoteRead.url. Scopes must be delimited by ';'.
|
||||
-remoteRead.oauth2.tokenUrl string
|
||||
Optional OAuth2 tokenURL to use for -remoteRead.url.
|
||||
-remoteRead.tlsCAFile string
|
||||
Optional path to TLS CA file to use for verifying connections to -remoteRead.url. By default system CA is used
|
||||
-remoteRead.tlsCertFile string
|
||||
@@ -511,7 +708,7 @@ The shortlist of configuration flags is the following:
|
||||
-remoteRead.tlsServerName string
|
||||
Optional TLS server name to use for connections to -remoteRead.url. By default the server name from -remoteRead.url is used
|
||||
-remoteRead.url vmalert
|
||||
Optional URL to VictoriaMetrics or vmselect that will be used to restore alerts state. This configuration makes sense only if vmalert was configured with `remoteWrite.url` before and has been successfully persisted its state. E.g. http://127.0.0.1:8428
|
||||
Optional URL to VictoriaMetrics or vmselect that will be used to restore alerts state. This configuration makes sense only if vmalert was configured with `remoteWrite.url` before and has been successfully persisted its state. E.g. http://127.0.0.1:8428. See also -remoteRead.disablePathAppend
|
||||
-remoteWrite.basicAuth.password string
|
||||
Optional basic auth password for -remoteWrite.url
|
||||
-remoteWrite.basicAuth.passwordFile string
|
||||
@@ -532,6 +729,16 @@ The shortlist of configuration flags is the following:
|
||||
Defines defines max number of timeseries to be flushed at once (default 1000)
|
||||
-remoteWrite.maxQueueSize int
|
||||
Defines the max number of pending datapoints to remote write endpoint (default 100000)
|
||||
-remoteWrite.oauth2.clientID string
|
||||
Optional OAuth2 clientID to use for -remoteWrite.url.
|
||||
-remoteWrite.oauth2.clientSecret string
|
||||
Optional OAuth2 clientSecret to use for -remoteWrite.url.
|
||||
-remoteWrite.oauth2.clientSecretFile string
|
||||
Optional OAuth2 clientSecretFile to use for -remoteWrite.url.
|
||||
-remoteWrite.oauth2.scopes string
|
||||
Optional OAuth2 scopes to use for -notifier.url. Scopes must be delimited by ';'.
|
||||
-remoteWrite.oauth2.tokenUrl string
|
||||
Optional OAuth2 tokenURL to use for -notifier.url.
|
||||
-remoteWrite.tlsCAFile string
|
||||
Optional path to TLS CA file to use for verifying connections to -remoteWrite.url. By default system CA is used
|
||||
-remoteWrite.tlsCertFile string
|
||||
@@ -564,9 +771,11 @@ The shortlist of configuration flags is the following:
|
||||
Rule files may contain %{ENV_VAR} placeholders, which are substituted by the corresponding env vars.
|
||||
Supports an array of values separated by comma or specified via multiple flags.
|
||||
-rule.configCheckInterval duration
|
||||
Interval for checking for changes in '-rule' files. By default the checking is disabled. Send SIGHUP signal in order to force config check for changes
|
||||
Interval for checking for changes in '-rule' files. By default the checking is disabled. Send SIGHUP signal in order to force config check for changes. DEPRECATED - see '-configCheckInterval' instead
|
||||
-rule.maxResolveDuration duration
|
||||
Limits the maximum duration for automatic alert expiration, which is by default equal to 3 evaluation intervals of the parent group.
|
||||
-rule.resendDelay duration
|
||||
Minimum amount of time to wait before resending an alert to notifier
|
||||
-rule.validateExpressions
|
||||
Whether to validate rules expressions via MetricsQL engine (default true)
|
||||
-rule.validateTemplates
|
||||
@@ -574,19 +783,121 @@ The shortlist of configuration flags is the following:
|
||||
-tls
|
||||
Whether to enable TLS (aka HTTPS) for incoming requests. -tlsCertFile and -tlsKeyFile must be set if -tls is set
|
||||
-tlsCertFile string
|
||||
Path to file with TLS certificate. Used only if -tls is set. Prefer ECDSA certs instead of RSA certs as RSA certs are slower
|
||||
Path to file with TLS certificate. Used only if -tls is set. Prefer ECDSA certs instead of RSA certs as RSA certs are slower. The provided certificate file is automatically re-read every second, so it can be dynamically updated
|
||||
-tlsKeyFile string
|
||||
Path to file with TLS key. Used only if -tls is set
|
||||
Path to file with TLS key. Used only if -tls is set. The provided key file is automatically re-read every second, so it can be dynamically updated
|
||||
-version
|
||||
Show VictoriaMetrics version
|
||||
```
|
||||
|
||||
### Hot config reload
|
||||
`vmalert` supports "hot" config reload via the following methods:
|
||||
* send SIGHUP signal to `vmalert` process;
|
||||
* send GET request to `/-/reload` endpoint;
|
||||
* configure `-rule.configCheckInterval` flag for periodic reload
|
||||
* configure `-configCheckInterval` flag for periodic reload
|
||||
on config change.
|
||||
|
||||
### URL params
|
||||
|
||||
To set additional URL params for `datasource.url`, `remoteWrite.url` or `remoteRead.url`
|
||||
just add them in address: `-datasource.url=http://localhost:8428?nocache=1`.
|
||||
|
||||
To set additional URL params for specific [group of rules](#Groups) modify
|
||||
the `params` group:
|
||||
```yaml
|
||||
groups:
|
||||
- name: TestGroup
|
||||
params:
|
||||
denyPartialResponse: ["true"]
|
||||
extra_label: ["env=dev"]
|
||||
```
|
||||
Please note, `params` are used only for executing rules expressions (requests to `datasource.url`).
|
||||
If there would be a conflict between URL params set in `datasource.url` flag and params in group definition
|
||||
the latter will have higher priority.
|
||||
|
||||
### Notifier configuration file
|
||||
|
||||
Notifier also supports configuration via file specified with flag `notifier.config`:
|
||||
```
|
||||
./bin/vmalert -rule=app/vmalert/config/testdata/rules.good.rules \
|
||||
-datasource.url=http://localhost:8428 \
|
||||
-notifier.config=app/vmalert/notifier/testdata/consul.good.yaml
|
||||
```
|
||||
|
||||
The configuration file allows to configure static notifiers or discover notifiers via
|
||||
[Consul](https://prometheus.io/docs/prometheus/latest/configuration/configuration/#consul_sd_config).
|
||||
For example:
|
||||
```
|
||||
static_configs:
|
||||
- targets:
|
||||
- localhost:9093
|
||||
- localhost:9095
|
||||
|
||||
consul_sd_configs:
|
||||
- server: localhost:8500
|
||||
services:
|
||||
- alertmanager
|
||||
```
|
||||
|
||||
The list of configured or discovered Notifiers can be explored via [UI](#Web).
|
||||
|
||||
The configuration file [specification](https://github.com/VictoriaMetrics/VictoriaMetrics/blob/master/app/vmalert/notifier/config.go)
|
||||
is the following:
|
||||
```
|
||||
# Per-target Notifier timeout when pushing alerts.
|
||||
[ timeout: <duration> | default = 10s ]
|
||||
|
||||
# Prefix for the HTTP path alerts are pushed to.
|
||||
[ path_prefix: <path> | default = / ]
|
||||
|
||||
# Configures the protocol scheme used for requests.
|
||||
[ scheme: <scheme> | default = http ]
|
||||
|
||||
# Sets the `Authorization` header on every request with the
|
||||
# configured username and password.
|
||||
# password and password_file are mutually exclusive.
|
||||
basic_auth:
|
||||
[ username: <string> ]
|
||||
[ password: <secret> ]
|
||||
[ password_file: <string> ]
|
||||
|
||||
# Optional `Authorization` header configuration.
|
||||
authorization:
|
||||
# Sets the authentication type.
|
||||
[ type: <string> | default: Bearer ]
|
||||
# Sets the credentials. It is mutually exclusive with
|
||||
# `credentials_file`.
|
||||
[ credentials: <secret> ]
|
||||
# Sets the credentials to the credentials read from the configured file.
|
||||
# It is mutually exclusive with `credentials`.
|
||||
[ credentials_file: <filename> ]
|
||||
|
||||
# Configures the scrape request's TLS settings.
|
||||
# see https://prometheus.io/docs/prometheus/latest/configuration/configuration/#tls_config
|
||||
tls_config:
|
||||
[ <tls_config> ]
|
||||
|
||||
# List of labeled statically configured Notifiers.
|
||||
static_configs:
|
||||
targets:
|
||||
[ - '<host>' ]
|
||||
|
||||
# List of Consul service discovery configurations.
|
||||
# See https://prometheus.io/docs/prometheus/latest/configuration/configuration/#consul_sd_config
|
||||
consul_sd_configs:
|
||||
[ - <consul_sd_config> ... ]
|
||||
|
||||
# List of relabel configurations.
|
||||
# Supports the same relabeling features as the rest of VictoriaMetrics components.
|
||||
# See https://docs.victoriametrics.com/vmagent.html#relabeling
|
||||
relabel_configs:
|
||||
[ - <relabel_config> ... ]
|
||||
|
||||
```
|
||||
|
||||
The configuration file can be [hot-reloaded](#hot-config-reload).
|
||||
|
||||
|
||||
## Contributing
|
||||
|
||||
`vmalert` is mostly designed and built by VictoriaMetrics community.
|
||||
@@ -602,7 +913,7 @@ It is recommended using
|
||||
|
||||
### Development build
|
||||
|
||||
1. [Install Go](https://golang.org/doc/install). The minimum supported version is Go 1.16.
|
||||
1. [Install Go](https://golang.org/doc/install). The minimum supported version is Go 1.17.
|
||||
2. Run `make vmalert` from the root folder of [the repository](https://github.com/VictoriaMetrics/VictoriaMetrics).
|
||||
It builds `vmalert` binary and puts it into the `bin` folder.
|
||||
|
||||
@@ -619,7 +930,7 @@ ARM build may run on Raspberry Pi or on [energy-efficient ARM servers](https://b
|
||||
|
||||
### Development ARM build
|
||||
|
||||
1. [Install Go](https://golang.org/doc/install). The minimum supported version is Go 1.16.
|
||||
1. [Install Go](https://golang.org/doc/install). The minimum supported version is Go 1.17.
|
||||
2. Run `make vmalert-arm` or `make vmalert-arm64` from the root folder of [the repository](https://github.com/VictoriaMetrics/VictoriaMetrics).
|
||||
It builds `vmalert-arm` or `vmalert-arm64` binary respectively and puts it into the `bin` folder.
|
||||
|
||||
|
||||
@@ -12,9 +12,9 @@ import (
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/config"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/datasource"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/notifier"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/utils"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/prompbmarshal"
|
||||
"github.com/VictoriaMetrics/metrics"
|
||||
)
|
||||
|
||||
// AlertingRule is basic alert entity
|
||||
@@ -38,6 +38,8 @@ type AlertingRule struct {
|
||||
alerts map[uint64]*notifier.Alert
|
||||
// stores last moment of time Exec was called
|
||||
lastExecTime time.Time
|
||||
// stores the duration of the last Exec call
|
||||
lastExecDuration time.Duration
|
||||
// stores last error that happened in Exec func
|
||||
// resets on every successful Exec
|
||||
// may be used as Health state
|
||||
@@ -50,15 +52,15 @@ type AlertingRule struct {
|
||||
}
|
||||
|
||||
type alertingRuleMetrics struct {
|
||||
errors *gauge
|
||||
pending *gauge
|
||||
active *gauge
|
||||
samples *gauge
|
||||
errors *utils.Gauge
|
||||
pending *utils.Gauge
|
||||
active *utils.Gauge
|
||||
samples *utils.Gauge
|
||||
}
|
||||
|
||||
func newAlertingRule(qb datasource.QuerierBuilder, group *Group, cfg config.Rule) *AlertingRule {
|
||||
ar := &AlertingRule{
|
||||
Type: cfg.Type,
|
||||
Type: group.Type,
|
||||
RuleID: cfg.ID,
|
||||
Name: cfg.Alert,
|
||||
Expr: cfg.Expr,
|
||||
@@ -69,16 +71,16 @@ func newAlertingRule(qb datasource.QuerierBuilder, group *Group, cfg config.Rule
|
||||
GroupName: group.Name,
|
||||
EvalInterval: group.Interval,
|
||||
q: qb.BuildWithParams(datasource.QuerierParams{
|
||||
DataSourceType: &cfg.Type,
|
||||
DataSourceType: &group.Type,
|
||||
EvaluationInterval: group.Interval,
|
||||
ExtraLabels: group.ExtraFilterLabels,
|
||||
QueryParams: group.Params,
|
||||
}),
|
||||
alerts: make(map[uint64]*notifier.Alert),
|
||||
metrics: &alertingRuleMetrics{},
|
||||
}
|
||||
|
||||
labels := fmt.Sprintf(`alertname=%q, group=%q, id="%d"`, ar.Name, group.Name, ar.ID())
|
||||
ar.metrics.pending = getOrCreateGauge(fmt.Sprintf(`vmalert_alerts_pending{%s}`, labels),
|
||||
ar.metrics.pending = utils.GetOrCreateGauge(fmt.Sprintf(`vmalert_alerts_pending{%s}`, labels),
|
||||
func() float64 {
|
||||
ar.mu.RLock()
|
||||
defer ar.mu.RUnlock()
|
||||
@@ -90,7 +92,7 @@ func newAlertingRule(qb datasource.QuerierBuilder, group *Group, cfg config.Rule
|
||||
}
|
||||
return float64(num)
|
||||
})
|
||||
ar.metrics.active = getOrCreateGauge(fmt.Sprintf(`vmalert_alerts_firing{%s}`, labels),
|
||||
ar.metrics.active = utils.GetOrCreateGauge(fmt.Sprintf(`vmalert_alerts_firing{%s}`, labels),
|
||||
func() float64 {
|
||||
ar.mu.RLock()
|
||||
defer ar.mu.RUnlock()
|
||||
@@ -102,7 +104,7 @@ func newAlertingRule(qb datasource.QuerierBuilder, group *Group, cfg config.Rule
|
||||
}
|
||||
return float64(num)
|
||||
})
|
||||
ar.metrics.errors = getOrCreateGauge(fmt.Sprintf(`vmalert_alerting_rules_error{%s}`, labels),
|
||||
ar.metrics.errors = utils.GetOrCreateGauge(fmt.Sprintf(`vmalert_alerting_rules_error{%s}`, labels),
|
||||
func() float64 {
|
||||
ar.mu.RLock()
|
||||
defer ar.mu.RUnlock()
|
||||
@@ -111,7 +113,7 @@ func newAlertingRule(qb datasource.QuerierBuilder, group *Group, cfg config.Rule
|
||||
}
|
||||
return 1
|
||||
})
|
||||
ar.metrics.samples = getOrCreateGauge(fmt.Sprintf(`vmalert_alerting_rules_last_evaluation_samples{%s}`, labels),
|
||||
ar.metrics.samples = utils.GetOrCreateGauge(fmt.Sprintf(`vmalert_alerting_rules_last_evaluation_samples{%s}`, labels),
|
||||
func() float64 {
|
||||
ar.mu.RLock()
|
||||
defer ar.mu.RUnlock()
|
||||
@@ -122,10 +124,10 @@ func newAlertingRule(qb datasource.QuerierBuilder, group *Group, cfg config.Rule
|
||||
|
||||
// Close unregisters rule metrics
|
||||
func (ar *AlertingRule) Close() {
|
||||
metrics.UnregisterMetric(ar.metrics.active.name)
|
||||
metrics.UnregisterMetric(ar.metrics.pending.name)
|
||||
metrics.UnregisterMetric(ar.metrics.errors.name)
|
||||
metrics.UnregisterMetric(ar.metrics.samples.name)
|
||||
ar.metrics.active.Unregister()
|
||||
ar.metrics.pending.Unregister()
|
||||
ar.metrics.errors.Unregister()
|
||||
ar.metrics.samples.Unregister()
|
||||
}
|
||||
|
||||
// String implements Stringer interface
|
||||
@@ -153,6 +155,13 @@ func (ar *AlertingRule) ExecRange(ctx context.Context, start, end time.Time) ([]
|
||||
return nil, fmt.Errorf("`query` template isn't supported in replay mode")
|
||||
}
|
||||
for _, s := range series {
|
||||
// set additional labels to identify group and rule Name
|
||||
if ar.Name != "" {
|
||||
s.SetLabel(alertNameLabel, ar.Name)
|
||||
}
|
||||
if !*disableAlertGroupLabel && ar.GroupName != "" {
|
||||
s.SetLabel(alertGroupNameLabel, ar.GroupName)
|
||||
}
|
||||
// extra labels could contain templates, so we expand them first
|
||||
labels, err := expandLabels(s, qFn, ar)
|
||||
if err != nil {
|
||||
@@ -163,7 +172,6 @@ func (ar *AlertingRule) ExecRange(ctx context.Context, start, end time.Time) ([]
|
||||
// so the hash key will be consistent on restore
|
||||
s.SetLabel(k, v)
|
||||
}
|
||||
|
||||
a, err := ar.newAlert(s, time.Time{}, qFn) // initial alert
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create alert: %s", err)
|
||||
@@ -178,13 +186,11 @@ func (ar *AlertingRule) ExecRange(ctx context.Context, start, end time.Time) ([]
|
||||
|
||||
// if alert with For > 0
|
||||
prevT := time.Time{}
|
||||
//activeAt := time.Time{}
|
||||
for i := range s.Values {
|
||||
at := time.Unix(s.Timestamps[i], 0)
|
||||
if at.Sub(prevT) > ar.EvalInterval {
|
||||
// reset to Pending if there are gaps > EvalInterval between DPs
|
||||
a.State = notifier.StatePending
|
||||
//activeAt = at
|
||||
a.Start = at
|
||||
} else if at.Sub(a.Start) >= ar.For {
|
||||
a.State = notifier.StateFiring
|
||||
@@ -199,12 +205,14 @@ func (ar *AlertingRule) ExecRange(ctx context.Context, start, end time.Time) ([]
|
||||
// Exec executes AlertingRule expression via the given Querier.
|
||||
// Based on the Querier results AlertingRule maintains notifier.Alerts
|
||||
func (ar *AlertingRule) Exec(ctx context.Context) ([]prompbmarshal.TimeSeries, error) {
|
||||
start := time.Now()
|
||||
qMetrics, err := ar.q.Query(ctx, ar.Expr)
|
||||
ar.mu.Lock()
|
||||
defer ar.mu.Unlock()
|
||||
|
||||
ar.lastExecTime = start
|
||||
ar.lastExecDuration = time.Since(start)
|
||||
ar.lastExecError = err
|
||||
ar.lastExecTime = time.Now()
|
||||
ar.lastExecSamples = len(qMetrics)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to execute query %q: %w", ar.Expr, err)
|
||||
@@ -221,6 +229,13 @@ func (ar *AlertingRule) Exec(ctx context.Context) ([]prompbmarshal.TimeSeries, e
|
||||
updated := make(map[uint64]struct{})
|
||||
// update list of active alerts
|
||||
for _, m := range qMetrics {
|
||||
// set additional labels to identify group and rule name
|
||||
if ar.Name != "" {
|
||||
m.SetLabel(alertNameLabel, ar.Name)
|
||||
}
|
||||
if !*disableAlertGroupLabel && ar.GroupName != "" {
|
||||
m.SetLabel(alertGroupNameLabel, ar.GroupName)
|
||||
}
|
||||
// extra labels could contain templates, so we expand them first
|
||||
labels, err := expandLabels(m, qFn, ar)
|
||||
if err != nil {
|
||||
@@ -352,11 +367,6 @@ func (ar *AlertingRule) newAlert(m datasource.Metric, start time.Time, qFn notif
|
||||
Start: start,
|
||||
Expr: ar.Expr,
|
||||
}
|
||||
// label defined here to make override possible by
|
||||
// time series labels.
|
||||
if !*disableAlertGroupLabel && ar.GroupName != "" {
|
||||
a.Labels[alertGroupNameLabel] = ar.GroupName
|
||||
}
|
||||
for _, l := range m.Labels {
|
||||
// drop __name__ to be consistent with Prometheus alerting
|
||||
if l.Name == "__name__" {
|
||||
@@ -380,31 +390,48 @@ func (ar *AlertingRule) AlertAPI(id uint64) *APIAlert {
|
||||
return ar.newAlertAPI(*a)
|
||||
}
|
||||
|
||||
// RuleAPI returns Rule representation in form
|
||||
// of APIAlertingRule
|
||||
func (ar *AlertingRule) RuleAPI() APIAlertingRule {
|
||||
var lastErr string
|
||||
// ToAPI returns Rule representation in form
|
||||
// of APIRule
|
||||
func (ar *AlertingRule) ToAPI() APIRule {
|
||||
r := APIRule{
|
||||
Type: "alerting",
|
||||
DatasourceType: ar.Type.String(),
|
||||
Name: ar.Name,
|
||||
Query: ar.Expr,
|
||||
Duration: ar.For.Seconds(),
|
||||
Labels: ar.Labels,
|
||||
Annotations: ar.Annotations,
|
||||
LastEvaluation: ar.lastExecTime,
|
||||
EvaluationTime: ar.lastExecDuration.Seconds(),
|
||||
Health: "ok",
|
||||
State: "inactive",
|
||||
Alerts: ar.AlertsToAPI(),
|
||||
LastSamples: ar.lastExecSamples,
|
||||
|
||||
// encode as strings to avoid rounding in JSON
|
||||
ID: fmt.Sprintf("%d", ar.ID()),
|
||||
GroupID: fmt.Sprintf("%d", ar.GroupID),
|
||||
}
|
||||
if ar.lastExecError != nil {
|
||||
lastErr = ar.lastExecError.Error()
|
||||
r.LastError = ar.lastExecError.Error()
|
||||
r.Health = "err"
|
||||
}
|
||||
return APIAlertingRule{
|
||||
// encode as strings to avoid rounding
|
||||
ID: fmt.Sprintf("%d", ar.ID()),
|
||||
GroupID: fmt.Sprintf("%d", ar.GroupID),
|
||||
Type: ar.Type.String(),
|
||||
Name: ar.Name,
|
||||
Expression: ar.Expr,
|
||||
For: ar.For.String(),
|
||||
LastError: lastErr,
|
||||
LastSamples: ar.lastExecSamples,
|
||||
LastExec: ar.lastExecTime,
|
||||
Labels: ar.Labels,
|
||||
Annotations: ar.Annotations,
|
||||
// satisfy APIRule.State logic
|
||||
if len(r.Alerts) > 0 {
|
||||
r.State = notifier.StatePending.String()
|
||||
stateFiring := notifier.StateFiring.String()
|
||||
for _, a := range r.Alerts {
|
||||
if a.State == stateFiring {
|
||||
r.State = stateFiring
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// AlertsAPI generates list of APIAlert objects from existing alerts
|
||||
func (ar *AlertingRule) AlertsAPI() []*APIAlert {
|
||||
// AlertsToAPI generates list of APIAlert objects from existing alerts
|
||||
func (ar *AlertingRule) AlertsToAPI() []*APIAlert {
|
||||
var alerts []*APIAlert
|
||||
ar.mu.RLock()
|
||||
for _, a := range ar.alerts {
|
||||
@@ -415,7 +442,7 @@ func (ar *AlertingRule) AlertsAPI() []*APIAlert {
|
||||
}
|
||||
|
||||
func (ar *AlertingRule) newAlertAPI(a notifier.Alert) *APIAlert {
|
||||
return &APIAlert{
|
||||
aa := &APIAlert{
|
||||
// encode as strings to avoid rounding
|
||||
ID: fmt.Sprintf("%d", a.ID),
|
||||
GroupID: fmt.Sprintf("%d", a.GroupID),
|
||||
@@ -427,8 +454,13 @@ func (ar *AlertingRule) newAlertAPI(a notifier.Alert) *APIAlert {
|
||||
Annotations: a.Annotations,
|
||||
State: a.State.String(),
|
||||
ActiveAt: a.Start,
|
||||
Restored: a.Restored,
|
||||
Value: strconv.FormatFloat(a.Value, 'f', -1, 32),
|
||||
}
|
||||
if alertURLGeneratorFn != nil {
|
||||
aa.SourceLink = alertURLGeneratorFn(a)
|
||||
}
|
||||
return aa
|
||||
}
|
||||
|
||||
const (
|
||||
@@ -443,43 +475,42 @@ const (
|
||||
alertStateLabel = "alertstate"
|
||||
|
||||
// alertGroupNameLabel defines the label name attached for generated time series.
|
||||
// attaching this label may be disabled via `-disableAlertgroupLabel` flag.
|
||||
alertGroupNameLabel = "alertgroup"
|
||||
)
|
||||
|
||||
// alertToTimeSeries converts the given alert with the given timestamp to timeseries
|
||||
func (ar *AlertingRule) alertToTimeSeries(a *notifier.Alert, timestamp int64) []prompbmarshal.TimeSeries {
|
||||
var tss []prompbmarshal.TimeSeries
|
||||
tss = append(tss, alertToTimeSeries(ar.Name, a, timestamp))
|
||||
tss = append(tss, alertToTimeSeries(a, timestamp))
|
||||
if ar.For > 0 {
|
||||
tss = append(tss, alertForToTimeSeries(ar.Name, a, timestamp))
|
||||
tss = append(tss, alertForToTimeSeries(a, timestamp))
|
||||
}
|
||||
return tss
|
||||
}
|
||||
|
||||
func alertToTimeSeries(name string, a *notifier.Alert, timestamp int64) prompbmarshal.TimeSeries {
|
||||
func alertToTimeSeries(a *notifier.Alert, timestamp int64) prompbmarshal.TimeSeries {
|
||||
labels := make(map[string]string)
|
||||
for k, v := range a.Labels {
|
||||
labels[k] = v
|
||||
}
|
||||
labels["__name__"] = alertMetricName
|
||||
labels[alertNameLabel] = name
|
||||
labels[alertStateLabel] = a.State.String()
|
||||
return newTimeSeries([]float64{1}, []int64{timestamp}, labels)
|
||||
}
|
||||
|
||||
// alertForToTimeSeries returns a timeseries that represents
|
||||
// state of active alerts, where value is time when alert become active
|
||||
func alertForToTimeSeries(name string, a *notifier.Alert, timestamp int64) prompbmarshal.TimeSeries {
|
||||
func alertForToTimeSeries(a *notifier.Alert, timestamp int64) prompbmarshal.TimeSeries {
|
||||
labels := make(map[string]string)
|
||||
for k, v := range a.Labels {
|
||||
labels[k] = v
|
||||
}
|
||||
labels["__name__"] = alertForStateMetricName
|
||||
labels[alertNameLabel] = name
|
||||
return newTimeSeries([]float64{float64(a.Start.Unix())}, []int64{timestamp}, labels)
|
||||
}
|
||||
|
||||
// Restore restores the state of active alerts basing on previously written timeseries.
|
||||
// Restore restores the state of active alerts basing on previously written time series.
|
||||
// Restore restores only Start field. Field State will be always Pending and supposed
|
||||
// to be updated on next Exec, as well as Value field.
|
||||
// Only rules with For > 0 will be restored.
|
||||
@@ -507,25 +538,38 @@ func (ar *AlertingRule) Restore(ctx context.Context, q datasource.Querier, lookb
|
||||
}
|
||||
|
||||
for _, m := range qMetrics {
|
||||
labels := m.Labels
|
||||
m.Labels = make([]datasource.Label, 0)
|
||||
// drop all extra labels, so hash key will
|
||||
// be identical to time series received in Exec
|
||||
for _, l := range labels {
|
||||
if l.Name == alertNameLabel || l.Name == alertGroupNameLabel {
|
||||
continue
|
||||
}
|
||||
m.Labels = append(m.Labels, l)
|
||||
}
|
||||
|
||||
a, err := ar.newAlert(m, time.Unix(int64(m.Values[0]), 0), qFn)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create alert: %w", err)
|
||||
}
|
||||
a.ID = hash(m)
|
||||
a.State = notifier.StatePending
|
||||
a.Restored = true
|
||||
ar.alerts[a.ID] = a
|
||||
logger.Infof("alert %q (%d) restored to state at %v", a.Name, a.ID, a.Start)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// alertsToSend walks through the current alerts of AlertingRule
|
||||
// and returns only those which should be sent to notifier.
|
||||
// Isn't concurrent safe.
|
||||
func (ar *AlertingRule) alertsToSend(ts time.Time, resolveDuration, resendDelay time.Duration) []notifier.Alert {
|
||||
var alerts []notifier.Alert
|
||||
for _, a := range ar.alerts {
|
||||
switch a.State {
|
||||
case notifier.StateFiring:
|
||||
if time.Since(a.LastSent) < resendDelay {
|
||||
continue
|
||||
}
|
||||
a.End = ts.Add(resolveDuration)
|
||||
a.LastSent = ts
|
||||
alerts = append(alerts, *a)
|
||||
case notifier.StateInactive:
|
||||
a.End = ts
|
||||
a.LastSent = ts
|
||||
alerts = append(alerts, *a)
|
||||
}
|
||||
}
|
||||
return alerts
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -27,7 +28,6 @@ func TestAlertingRule_ToTimeSeries(t *testing.T) {
|
||||
newTimeSeries([]float64{1}, []int64{timestamp.UnixNano()}, map[string]string{
|
||||
"__name__": alertMetricName,
|
||||
alertStateLabel: notifier.StateFiring.String(),
|
||||
alertNameLabel: "instant",
|
||||
}),
|
||||
},
|
||||
},
|
||||
@@ -41,7 +41,6 @@ func TestAlertingRule_ToTimeSeries(t *testing.T) {
|
||||
newTimeSeries([]float64{1}, []int64{timestamp.UnixNano()}, map[string]string{
|
||||
"__name__": alertMetricName,
|
||||
alertStateLabel: notifier.StateFiring.String(),
|
||||
alertNameLabel: "instant extra labels",
|
||||
"job": "foo",
|
||||
"instance": "bar",
|
||||
}),
|
||||
@@ -57,7 +56,6 @@ func TestAlertingRule_ToTimeSeries(t *testing.T) {
|
||||
newTimeSeries([]float64{1}, []int64{timestamp.UnixNano()}, map[string]string{
|
||||
"__name__": alertMetricName,
|
||||
alertStateLabel: notifier.StateFiring.String(),
|
||||
alertNameLabel: "instant labels override",
|
||||
}),
|
||||
},
|
||||
},
|
||||
@@ -68,13 +66,11 @@ func TestAlertingRule_ToTimeSeries(t *testing.T) {
|
||||
newTimeSeries([]float64{1}, []int64{timestamp.UnixNano()}, map[string]string{
|
||||
"__name__": alertMetricName,
|
||||
alertStateLabel: notifier.StateFiring.String(),
|
||||
alertNameLabel: "for",
|
||||
}),
|
||||
newTimeSeries([]float64{float64(timestamp.Add(time.Second).Unix())},
|
||||
[]int64{timestamp.UnixNano()},
|
||||
map[string]string{
|
||||
"__name__": alertForStateMetricName,
|
||||
alertNameLabel: "for",
|
||||
"__name__": alertForStateMetricName,
|
||||
}),
|
||||
},
|
||||
},
|
||||
@@ -85,13 +81,11 @@ func TestAlertingRule_ToTimeSeries(t *testing.T) {
|
||||
newTimeSeries([]float64{1}, []int64{timestamp.UnixNano()}, map[string]string{
|
||||
"__name__": alertMetricName,
|
||||
alertStateLabel: notifier.StatePending.String(),
|
||||
alertNameLabel: "for pending",
|
||||
}),
|
||||
newTimeSeries([]float64{float64(timestamp.Add(time.Second).Unix())},
|
||||
[]int64{timestamp.UnixNano()},
|
||||
map[string]string{
|
||||
"__name__": alertForStateMetricName,
|
||||
alertNameLabel: "for pending",
|
||||
"__name__": alertForStateMetricName,
|
||||
}),
|
||||
},
|
||||
},
|
||||
@@ -109,23 +103,27 @@ func TestAlertingRule_ToTimeSeries(t *testing.T) {
|
||||
|
||||
func TestAlertingRule_Exec(t *testing.T) {
|
||||
const defaultStep = 5 * time.Millisecond
|
||||
type testAlert struct {
|
||||
labels []string
|
||||
alert *notifier.Alert
|
||||
}
|
||||
testCases := []struct {
|
||||
rule *AlertingRule
|
||||
steps [][]datasource.Metric
|
||||
expAlerts map[uint64]*notifier.Alert
|
||||
expAlerts []testAlert
|
||||
}{
|
||||
{
|
||||
newTestAlertingRule("empty", 0),
|
||||
[][]datasource.Metric{},
|
||||
map[uint64]*notifier.Alert{},
|
||||
nil,
|
||||
},
|
||||
{
|
||||
newTestAlertingRule("empty labels", 0),
|
||||
[][]datasource.Metric{
|
||||
{datasource.Metric{Values: []float64{1}, Timestamps: []int64{1}}},
|
||||
},
|
||||
map[uint64]*notifier.Alert{
|
||||
hash(datasource.Metric{}): {State: notifier.StateFiring},
|
||||
[]testAlert{
|
||||
{alert: ¬ifier.Alert{State: notifier.StateFiring}},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -133,8 +131,8 @@ func TestAlertingRule_Exec(t *testing.T) {
|
||||
[][]datasource.Metric{
|
||||
{metricWithLabels(t, "name", "foo")},
|
||||
},
|
||||
map[uint64]*notifier.Alert{
|
||||
hash(metricWithLabels(t, "name", "foo")): {State: notifier.StateFiring},
|
||||
[]testAlert{
|
||||
{labels: []string{"name", "foo"}, alert: ¬ifier.Alert{State: notifier.StateFiring}},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -143,8 +141,8 @@ func TestAlertingRule_Exec(t *testing.T) {
|
||||
{metricWithLabels(t, "name", "foo")},
|
||||
{},
|
||||
},
|
||||
map[uint64]*notifier.Alert{
|
||||
hash(metricWithLabels(t, "name", "foo")): {State: notifier.StateInactive},
|
||||
[]testAlert{
|
||||
{labels: []string{"name", "foo"}, alert: ¬ifier.Alert{State: notifier.StateInactive}},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -154,8 +152,8 @@ func TestAlertingRule_Exec(t *testing.T) {
|
||||
{},
|
||||
{metricWithLabels(t, "name", "foo")},
|
||||
},
|
||||
map[uint64]*notifier.Alert{
|
||||
hash(metricWithLabels(t, "name", "foo")): {State: notifier.StateFiring},
|
||||
[]testAlert{
|
||||
{labels: []string{"name", "foo"}, alert: ¬ifier.Alert{State: notifier.StateFiring}},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -166,8 +164,8 @@ func TestAlertingRule_Exec(t *testing.T) {
|
||||
{metricWithLabels(t, "name", "foo")},
|
||||
{},
|
||||
},
|
||||
map[uint64]*notifier.Alert{
|
||||
hash(metricWithLabels(t, "name", "foo")): {State: notifier.StateInactive},
|
||||
[]testAlert{
|
||||
{labels: []string{"name", "foo"}, alert: ¬ifier.Alert{State: notifier.StateInactive}},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -179,7 +177,7 @@ func TestAlertingRule_Exec(t *testing.T) {
|
||||
{},
|
||||
{},
|
||||
},
|
||||
map[uint64]*notifier.Alert{},
|
||||
nil,
|
||||
},
|
||||
{
|
||||
newTestAlertingRule("single-firing=>inactive=>firing=>inactive=>empty=>firing", 0),
|
||||
@@ -191,8 +189,8 @@ func TestAlertingRule_Exec(t *testing.T) {
|
||||
{},
|
||||
{metricWithLabels(t, "name", "foo")},
|
||||
},
|
||||
map[uint64]*notifier.Alert{
|
||||
hash(metricWithLabels(t, "name", "foo")): {State: notifier.StateFiring},
|
||||
[]testAlert{
|
||||
{labels: []string{"name", "foo"}, alert: ¬ifier.Alert{State: notifier.StateFiring}},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -204,10 +202,10 @@ func TestAlertingRule_Exec(t *testing.T) {
|
||||
metricWithLabels(t, "name", "foo2"),
|
||||
},
|
||||
},
|
||||
map[uint64]*notifier.Alert{
|
||||
hash(metricWithLabels(t, "name", "foo")): {State: notifier.StateFiring},
|
||||
hash(metricWithLabels(t, "name", "foo1")): {State: notifier.StateFiring},
|
||||
hash(metricWithLabels(t, "name", "foo2")): {State: notifier.StateFiring},
|
||||
[]testAlert{
|
||||
{labels: []string{"name", "foo"}, alert: ¬ifier.Alert{State: notifier.StateFiring}},
|
||||
{labels: []string{"name", "foo1"}, alert: ¬ifier.Alert{State: notifier.StateFiring}},
|
||||
{labels: []string{"name", "foo2"}, alert: ¬ifier.Alert{State: notifier.StateFiring}},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -220,9 +218,9 @@ func TestAlertingRule_Exec(t *testing.T) {
|
||||
// 1: fire first alert
|
||||
// 2: fire second alert, set first inactive
|
||||
// 3: fire third alert, set second inactive, delete first one
|
||||
map[uint64]*notifier.Alert{
|
||||
hash(metricWithLabels(t, "name", "foo1")): {State: notifier.StateInactive},
|
||||
hash(metricWithLabels(t, "name", "foo2")): {State: notifier.StateFiring},
|
||||
[]testAlert{
|
||||
{labels: []string{"name", "foo1"}, alert: ¬ifier.Alert{State: notifier.StateInactive}},
|
||||
{labels: []string{"name", "foo2"}, alert: ¬ifier.Alert{State: notifier.StateFiring}},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -230,8 +228,8 @@ func TestAlertingRule_Exec(t *testing.T) {
|
||||
[][]datasource.Metric{
|
||||
{metricWithLabels(t, "name", "foo")},
|
||||
},
|
||||
map[uint64]*notifier.Alert{
|
||||
hash(metricWithLabels(t, "name", "foo")): {State: notifier.StatePending},
|
||||
[]testAlert{
|
||||
{labels: []string{"name", "foo"}, alert: ¬ifier.Alert{State: notifier.StatePending}},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -240,8 +238,8 @@ func TestAlertingRule_Exec(t *testing.T) {
|
||||
{metricWithLabels(t, "name", "foo")},
|
||||
{metricWithLabels(t, "name", "foo")},
|
||||
},
|
||||
map[uint64]*notifier.Alert{
|
||||
hash(metricWithLabels(t, "name", "foo")): {State: notifier.StateFiring},
|
||||
[]testAlert{
|
||||
{labels: []string{"name", "foo"}, alert: ¬ifier.Alert{State: notifier.StateFiring}},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -252,7 +250,7 @@ func TestAlertingRule_Exec(t *testing.T) {
|
||||
// empty step to reset and delete pending alerts
|
||||
{},
|
||||
},
|
||||
map[uint64]*notifier.Alert{},
|
||||
nil,
|
||||
},
|
||||
{
|
||||
newTestAlertingRule("for-pending=>firing=>inactive", defaultStep),
|
||||
@@ -262,8 +260,8 @@ func TestAlertingRule_Exec(t *testing.T) {
|
||||
// empty step to reset pending alerts
|
||||
{},
|
||||
},
|
||||
map[uint64]*notifier.Alert{
|
||||
hash(metricWithLabels(t, "name", "foo")): {State: notifier.StateInactive},
|
||||
[]testAlert{
|
||||
{labels: []string{"name", "foo"}, alert: ¬ifier.Alert{State: notifier.StateInactive}},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -275,8 +273,8 @@ func TestAlertingRule_Exec(t *testing.T) {
|
||||
{},
|
||||
{metricWithLabels(t, "name", "foo")},
|
||||
},
|
||||
map[uint64]*notifier.Alert{
|
||||
hash(metricWithLabels(t, "name", "foo")): {State: notifier.StatePending},
|
||||
[]testAlert{
|
||||
{labels: []string{"name", "foo"}, alert: ¬ifier.Alert{State: notifier.StatePending}},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -289,8 +287,8 @@ func TestAlertingRule_Exec(t *testing.T) {
|
||||
{metricWithLabels(t, "name", "foo")},
|
||||
{metricWithLabels(t, "name", "foo")},
|
||||
},
|
||||
map[uint64]*notifier.Alert{
|
||||
hash(metricWithLabels(t, "name", "foo")): {State: notifier.StateFiring},
|
||||
[]testAlert{
|
||||
{labels: []string{"name", "foo"}, alert: ¬ifier.Alert{State: notifier.StateFiring}},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -312,7 +310,15 @@ func TestAlertingRule_Exec(t *testing.T) {
|
||||
if len(tc.rule.alerts) != len(tc.expAlerts) {
|
||||
t.Fatalf("expected %d alerts; got %d", len(tc.expAlerts), len(tc.rule.alerts))
|
||||
}
|
||||
for key, exp := range tc.expAlerts {
|
||||
expAlerts := make(map[uint64]*notifier.Alert)
|
||||
for _, ta := range tc.expAlerts {
|
||||
labels := ta.labels
|
||||
labels = append(labels, alertNameLabel)
|
||||
labels = append(labels, tc.rule.Name)
|
||||
h := hash(metricWithLabels(t, labels...))
|
||||
expAlerts[h] = ta.alert
|
||||
}
|
||||
for key, exp := range expAlerts {
|
||||
got, ok := tc.rule.alerts[key]
|
||||
if !ok {
|
||||
t.Fatalf("expected to have key %d", key)
|
||||
@@ -468,6 +474,11 @@ func TestAlertingRule_ExecRange(t *testing.T) {
|
||||
var j int
|
||||
for _, series := range tc.data {
|
||||
for _, timestamp := range series.Timestamps {
|
||||
a := tc.expAlerts[j]
|
||||
if a.Labels == nil {
|
||||
a.Labels = make(map[string]string)
|
||||
}
|
||||
a.Labels[alertNameLabel] = tc.rule.Name
|
||||
expTS = append(expTS, tc.rule.alertToTimeSeries(tc.expAlerts[j], timestamp)...)
|
||||
j++
|
||||
}
|
||||
@@ -496,7 +507,6 @@ func TestAlertingRule_Restore(t *testing.T) {
|
||||
[]datasource.Metric{
|
||||
metricWithValueAndLabels(t, float64(time.Now().Truncate(time.Hour).Unix()),
|
||||
"__name__", alertForStateMetricName,
|
||||
alertNameLabel, "",
|
||||
),
|
||||
},
|
||||
map[uint64]*notifier.Alert{
|
||||
@@ -509,7 +519,7 @@ func TestAlertingRule_Restore(t *testing.T) {
|
||||
[]datasource.Metric{
|
||||
metricWithValueAndLabels(t, float64(time.Now().Truncate(time.Hour).Unix()),
|
||||
"__name__", alertForStateMetricName,
|
||||
alertNameLabel, "",
|
||||
alertNameLabel, "metric labels",
|
||||
alertGroupNameLabel, "groupID",
|
||||
"foo", "bar",
|
||||
"namespace", "baz",
|
||||
@@ -517,6 +527,8 @@ func TestAlertingRule_Restore(t *testing.T) {
|
||||
},
|
||||
map[uint64]*notifier.Alert{
|
||||
hash(metricWithLabels(t,
|
||||
alertNameLabel, "metric labels",
|
||||
alertGroupNameLabel, "groupID",
|
||||
"foo", "bar",
|
||||
"namespace", "baz",
|
||||
)): {State: notifier.StatePending,
|
||||
@@ -528,7 +540,6 @@ func TestAlertingRule_Restore(t *testing.T) {
|
||||
[]datasource.Metric{
|
||||
metricWithValueAndLabels(t, float64(time.Now().Truncate(time.Hour).Unix()),
|
||||
"__name__", alertForStateMetricName,
|
||||
alertNameLabel, "",
|
||||
"foo", "bar",
|
||||
"namespace", "baz",
|
||||
// extra labels set by rule
|
||||
@@ -645,18 +656,20 @@ func TestAlertingRule_Template(t *testing.T) {
|
||||
metricWithValueAndLabels(t, 1, "instance", "bar"),
|
||||
},
|
||||
map[uint64]*notifier.Alert{
|
||||
hash(metricWithLabels(t, "region", "east", "instance", "foo")): {
|
||||
hash(metricWithLabels(t, alertNameLabel, "common", "region", "east", "instance", "foo")): {
|
||||
Annotations: map[string]string{},
|
||||
Labels: map[string]string{
|
||||
"region": "east",
|
||||
"instance": "foo",
|
||||
alertNameLabel: "common",
|
||||
"region": "east",
|
||||
"instance": "foo",
|
||||
},
|
||||
},
|
||||
hash(metricWithLabels(t, "region", "east", "instance", "bar")): {
|
||||
hash(metricWithLabels(t, alertNameLabel, "common", "region", "east", "instance", "bar")): {
|
||||
Annotations: map[string]string{},
|
||||
Labels: map[string]string{
|
||||
"region": "east",
|
||||
"instance": "bar",
|
||||
alertNameLabel: "common",
|
||||
"region": "east",
|
||||
"instance": "bar",
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -679,20 +692,22 @@ func TestAlertingRule_Template(t *testing.T) {
|
||||
metricWithValueAndLabels(t, 10, "instance", "bar"),
|
||||
},
|
||||
map[uint64]*notifier.Alert{
|
||||
hash(metricWithLabels(t, "region", "east", "instance", "foo")): {
|
||||
hash(metricWithLabels(t, alertNameLabel, "override label", "region", "east", "instance", "foo")): {
|
||||
Labels: map[string]string{
|
||||
"instance": "foo",
|
||||
"region": "east",
|
||||
alertNameLabel: "override label",
|
||||
"instance": "foo",
|
||||
"region": "east",
|
||||
},
|
||||
Annotations: map[string]string{
|
||||
"summary": `Too high connection number for "foo" for region east`,
|
||||
"description": `It is 2 connections for "foo"`,
|
||||
},
|
||||
},
|
||||
hash(metricWithLabels(t, "region", "east", "instance", "bar")): {
|
||||
hash(metricWithLabels(t, alertNameLabel, "override label", "region", "east", "instance", "bar")): {
|
||||
Labels: map[string]string{
|
||||
"instance": "bar",
|
||||
"region": "east",
|
||||
alertNameLabel: "override label",
|
||||
"instance": "bar",
|
||||
"region": "east",
|
||||
},
|
||||
Annotations: map[string]string{
|
||||
"summary": `Too high connection number for "bar" for region east`,
|
||||
@@ -701,6 +716,44 @@ func TestAlertingRule_Template(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
&AlertingRule{
|
||||
Name: "ExtraTemplating",
|
||||
GroupName: "Testing",
|
||||
Labels: map[string]string{
|
||||
"name": "alert_{{ $labels.alertname }}",
|
||||
"group": "group_{{ $labels.alertgroup }}",
|
||||
"instance": "{{ $labels.instance }}",
|
||||
},
|
||||
Annotations: map[string]string{
|
||||
"summary": `Alert "{{ $labels.alertname }}({{ $labels.alertgroup }})" for instance {{ $labels.instance }}`,
|
||||
"description": `Alert "{{ $labels.name }}({{ $labels.group }})" for instance {{ $labels.instance }}`,
|
||||
},
|
||||
alerts: make(map[uint64]*notifier.Alert),
|
||||
},
|
||||
[]datasource.Metric{
|
||||
metricWithValueAndLabels(t, 1, "instance", "foo"),
|
||||
},
|
||||
map[uint64]*notifier.Alert{
|
||||
hash(metricWithLabels(t, alertNameLabel, "ExtraTemplating",
|
||||
"name", "alert_ExtraTemplating",
|
||||
alertGroupNameLabel, "Testing",
|
||||
"group", "group_Testing",
|
||||
"instance", "foo")): {
|
||||
Labels: map[string]string{
|
||||
alertNameLabel: "ExtraTemplating",
|
||||
"name": "alert_ExtraTemplating",
|
||||
alertGroupNameLabel: "Testing",
|
||||
"group": "group_Testing",
|
||||
"instance": "foo",
|
||||
},
|
||||
Annotations: map[string]string{
|
||||
"summary": `Alert "ExtraTemplating(Testing)" for instance foo`,
|
||||
"description": `Alert "alert_ExtraTemplating(group_Testing)" for instance foo`,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
fakeGroup := Group{Name: "TestRule_Exec"}
|
||||
for _, tc := range testCases {
|
||||
@@ -729,6 +782,76 @@ func TestAlertingRule_Template(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestAlertsToSend(t *testing.T) {
|
||||
ts := time.Now()
|
||||
f := func(alerts, expAlerts []*notifier.Alert, resolveDuration, resendDelay time.Duration) {
|
||||
t.Helper()
|
||||
ar := &AlertingRule{alerts: make(map[uint64]*notifier.Alert)}
|
||||
for i, a := range alerts {
|
||||
ar.alerts[uint64(i)] = a
|
||||
}
|
||||
gotAlerts := ar.alertsToSend(ts, resolveDuration, resendDelay)
|
||||
if gotAlerts == nil && expAlerts == nil {
|
||||
return
|
||||
}
|
||||
if len(gotAlerts) != len(expAlerts) {
|
||||
t.Fatalf("expected to get %d alerts; got %d instead",
|
||||
len(expAlerts), len(gotAlerts))
|
||||
}
|
||||
sort.Slice(expAlerts, func(i, j int) bool {
|
||||
return expAlerts[i].Name < expAlerts[j].Name
|
||||
})
|
||||
sort.Slice(gotAlerts, func(i, j int) bool {
|
||||
return gotAlerts[i].Name < gotAlerts[j].Name
|
||||
})
|
||||
for i, exp := range expAlerts {
|
||||
got := gotAlerts[i]
|
||||
if got.LastSent != exp.LastSent {
|
||||
t.Fatalf("expected LastSent to be %v; got %v", exp.LastSent, got.LastSent)
|
||||
}
|
||||
if got.End != exp.End {
|
||||
t.Fatalf("expected End to be %v; got %v", exp.End, got.End)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
f( // send firing alert with custom resolve time
|
||||
[]*notifier.Alert{{State: notifier.StateFiring}},
|
||||
[]*notifier.Alert{{LastSent: ts, End: ts.Add(5 * time.Minute)}},
|
||||
5*time.Minute, time.Minute,
|
||||
)
|
||||
f( // resolve inactive alert at the current timestamp
|
||||
[]*notifier.Alert{{State: notifier.StateInactive}},
|
||||
[]*notifier.Alert{{LastSent: ts, End: ts}},
|
||||
time.Minute, time.Minute,
|
||||
)
|
||||
f( // mixed case of firing and resolved alerts. Names are added for deterministic sorting
|
||||
[]*notifier.Alert{{Name: "a", State: notifier.StateFiring}, {Name: "b", State: notifier.StateInactive}},
|
||||
[]*notifier.Alert{{Name: "a", LastSent: ts, End: ts.Add(5 * time.Minute)}, {Name: "b", LastSent: ts, End: ts}},
|
||||
5*time.Minute, time.Minute,
|
||||
)
|
||||
f( // mixed case of pending and resolved alerts. Names are added for deterministic sorting
|
||||
[]*notifier.Alert{{Name: "a", State: notifier.StatePending}, {Name: "b", State: notifier.StateInactive}},
|
||||
[]*notifier.Alert{{Name: "b", LastSent: ts, End: ts}},
|
||||
5*time.Minute, time.Minute,
|
||||
)
|
||||
f( // attempt to send alert that was already sent in the resendDelay interval
|
||||
[]*notifier.Alert{{State: notifier.StateFiring, LastSent: ts.Add(-time.Second)}},
|
||||
nil,
|
||||
time.Minute, time.Minute,
|
||||
)
|
||||
f( // attempt to send alert that was sent out of the resendDelay interval
|
||||
[]*notifier.Alert{{State: notifier.StateFiring, LastSent: ts.Add(-2 * time.Minute)}},
|
||||
[]*notifier.Alert{{LastSent: ts, End: ts.Add(time.Minute)}},
|
||||
time.Minute, time.Minute,
|
||||
)
|
||||
f( // alert must be sent even if resendDelay interval is 0
|
||||
[]*notifier.Alert{{State: notifier.StateFiring, LastSent: ts.Add(-time.Second)}},
|
||||
[]*notifier.Alert{{LastSent: ts, End: ts.Add(time.Minute)}},
|
||||
time.Minute, 0,
|
||||
)
|
||||
}
|
||||
|
||||
func newTestRuleWithLabels(name string, labels ...string) *AlertingRule {
|
||||
r := newTestAlertingRule(name, 0)
|
||||
r.Labels = make(map[string]string)
|
||||
|
||||
@@ -5,17 +5,19 @@ import (
|
||||
"fmt"
|
||||
"hash/fnv"
|
||||
"io/ioutil"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/datasource"
|
||||
"gopkg.in/yaml.v2"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/datasource"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/notifier"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/utils"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/envtemplate"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
|
||||
"gopkg.in/yaml.v2"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promutils"
|
||||
)
|
||||
|
||||
// Group contains list of Rules grouped into
|
||||
@@ -24,12 +26,13 @@ type Group struct {
|
||||
Type datasource.Type `yaml:"type,omitempty"`
|
||||
File string
|
||||
Name string `yaml:"name"`
|
||||
Interval utils.PromDuration `yaml:"interval"`
|
||||
Interval promutils.Duration `yaml:"interval"`
|
||||
Rules []Rule `yaml:"rules"`
|
||||
Concurrency int `yaml:"concurrency"`
|
||||
// ExtraFilterLabels is a list label filters applied to every rule
|
||||
// request withing a group. Is compatible only with VM datasources.
|
||||
// See https://docs.victoriametrics.com#prometheus-querying-api-enhancements
|
||||
// DEPRECATED: use Params field instead
|
||||
ExtraFilterLabels map[string]string `yaml:"extra_filter_labels"`
|
||||
// Labels is a set of label value pairs, that will be added to every rule.
|
||||
// It has priority over the external labels.
|
||||
@@ -37,6 +40,8 @@ type Group struct {
|
||||
// Checksum stores the hash of yaml definition for this group.
|
||||
// May be used to detect any changes like rules re-ordering etc.
|
||||
Checksum string
|
||||
// Optional HTTP URL parameters added to each rule request
|
||||
Params url.Values `yaml:"params"`
|
||||
|
||||
// Catches all undefined fields and must be empty after parsing.
|
||||
XXX map[string]interface{} `yaml:",inline"`
|
||||
@@ -56,12 +61,20 @@ func (g *Group) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
if g.Type.Get() == "" {
|
||||
g.Type.Set(datasource.NewPrometheusType())
|
||||
}
|
||||
// update rules with empty type.
|
||||
for i, r := range g.Rules {
|
||||
if r.Type.Get() == "" {
|
||||
r.Type.Set(g.Type)
|
||||
r.ID = HashRule(r)
|
||||
g.Rules[i] = r
|
||||
|
||||
// backward compatibility with deprecated `ExtraFilterLabels` param
|
||||
if len(g.ExtraFilterLabels) > 0 {
|
||||
if g.Params == nil {
|
||||
g.Params = url.Values{}
|
||||
}
|
||||
// Sort extraFilters for consistent order for query args across runs.
|
||||
extraFilters := make([]string, 0, len(g.ExtraFilterLabels))
|
||||
for k, v := range g.ExtraFilterLabels {
|
||||
extraFilters = append(extraFilters, fmt.Sprintf("%s=%s", k, v))
|
||||
}
|
||||
sort.Strings(extraFilters)
|
||||
for _, extraFilter := range extraFilters {
|
||||
g.Params.Add("extra_label", extraFilter)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,9 +89,6 @@ func (g *Group) Validate(validateAnnotations, validateExpressions bool) error {
|
||||
if g.Name == "" {
|
||||
return fmt.Errorf("group name must be set")
|
||||
}
|
||||
if len(g.Rules) == 0 {
|
||||
return fmt.Errorf("group %q can't contain no rules", g.Name)
|
||||
}
|
||||
|
||||
uniqueRules := map[uint64]struct{}{}
|
||||
for _, r := range g.Rules {
|
||||
@@ -97,9 +107,6 @@ func (g *Group) Validate(validateAnnotations, validateExpressions bool) error {
|
||||
// its needed only for tests.
|
||||
// because correct types must be inherited after unmarshalling.
|
||||
exprValidator := g.Type.ValidateExpr
|
||||
if r.Type.Get() != "" {
|
||||
exprValidator = r.Type.ValidateExpr
|
||||
}
|
||||
if err := exprValidator(r.Expr); err != nil {
|
||||
return fmt.Errorf("invalid expression for rule %q.%q: %w", g.Name, ruleName, err)
|
||||
}
|
||||
@@ -120,11 +127,10 @@ func (g *Group) Validate(validateAnnotations, validateExpressions bool) error {
|
||||
// recording rule or alerting rule.
|
||||
type Rule struct {
|
||||
ID uint64
|
||||
Type datasource.Type `yaml:"type,omitempty"`
|
||||
Record string `yaml:"record,omitempty"`
|
||||
Alert string `yaml:"alert,omitempty"`
|
||||
Expr string `yaml:"expr"`
|
||||
For utils.PromDuration `yaml:"for"`
|
||||
For promutils.Duration `yaml:"for"`
|
||||
Labels map[string]string `yaml:"labels,omitempty"`
|
||||
Annotations map[string]string `yaml:"annotations,omitempty"`
|
||||
|
||||
@@ -162,7 +168,6 @@ func HashRule(r Rule) uint64 {
|
||||
h.Write([]byte("alerting"))
|
||||
h.Write([]byte(r.Alert))
|
||||
}
|
||||
h.Write([]byte(r.Type.Get()))
|
||||
kv := sortMap(r.Labels)
|
||||
for _, i := range kv {
|
||||
h.Write([]byte(i.key))
|
||||
@@ -194,6 +199,7 @@ func Parse(pathPatterns []string, validateAnnotations, validateExpressions bool)
|
||||
fp = append(fp, matches...)
|
||||
}
|
||||
errGroup := new(utils.ErrGroup)
|
||||
var isExtraFilterLabelsUsed bool
|
||||
var groups []Group
|
||||
for _, file := range fp {
|
||||
uniqueGroups := map[string]struct{}{}
|
||||
@@ -213,6 +219,9 @@ func Parse(pathPatterns []string, validateAnnotations, validateExpressions bool)
|
||||
}
|
||||
uniqueGroups[g.Name] = struct{}{}
|
||||
g.File = file
|
||||
if len(g.ExtraFilterLabels) > 0 {
|
||||
isExtraFilterLabelsUsed = true
|
||||
}
|
||||
groups = append(groups, g)
|
||||
}
|
||||
}
|
||||
@@ -222,6 +231,9 @@ func Parse(pathPatterns []string, validateAnnotations, validateExpressions bool)
|
||||
if len(groups) < 1 {
|
||||
logger.Warnf("no groups found in %s", strings.Join(pathPatterns, ";"))
|
||||
}
|
||||
if isExtraFilterLabelsUsed {
|
||||
logger.Warnf("field `extra_filter_labels` is deprecated - use `params` instead")
|
||||
}
|
||||
return groups, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -7,10 +7,11 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gopkg.in/yaml.v2"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/datasource"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/notifier"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/utils"
|
||||
"gopkg.in/yaml.v2"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promutils"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
@@ -95,10 +96,6 @@ func TestGroup_Validate(t *testing.T) {
|
||||
group: &Group{},
|
||||
expErr: "group name must be set",
|
||||
},
|
||||
{
|
||||
group: &Group{Name: "test"},
|
||||
expErr: "contain no rules",
|
||||
},
|
||||
{
|
||||
group: &Group{Name: "test",
|
||||
Rules: []Rule{
|
||||
@@ -263,11 +260,10 @@ func TestGroup_Validate(t *testing.T) {
|
||||
Rules: []Rule{
|
||||
{
|
||||
Expr: "sumSeries(time('foo.bar',10))",
|
||||
For: utils.NewPromDuration(10 * time.Millisecond),
|
||||
For: promutils.NewDuration(10 * time.Millisecond),
|
||||
},
|
||||
{
|
||||
Expr: "sum(up == 0 ) by (host)",
|
||||
Type: datasource.NewPrometheusType(),
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -279,11 +275,10 @@ func TestGroup_Validate(t *testing.T) {
|
||||
Rules: []Rule{
|
||||
{
|
||||
Expr: "sum(up == 0 ) by (host)",
|
||||
For: utils.NewPromDuration(10 * time.Millisecond),
|
||||
For: promutils.NewDuration(10 * time.Millisecond),
|
||||
},
|
||||
{
|
||||
Expr: "sumSeries(time('foo.bar',10))",
|
||||
Type: datasource.NewPrometheusType(),
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -347,7 +342,7 @@ func TestHashRule(t *testing.T) {
|
||||
true,
|
||||
},
|
||||
{
|
||||
Rule{Alert: "alert", Expr: "up == 1", For: utils.NewPromDuration(time.Minute)},
|
||||
Rule{Alert: "alert", Expr: "up == 1", For: promutils.NewDuration(time.Minute)},
|
||||
Rule{Alert: "alert", Expr: "up == 1"},
|
||||
true,
|
||||
},
|
||||
@@ -478,6 +473,85 @@ concurrency: 16
|
||||
rules:
|
||||
- alert: ExampleAlertWithFor
|
||||
expr: sum by(job) (up == 1)
|
||||
`)
|
||||
})
|
||||
|
||||
t.Run("`params` change", func(t *testing.T) {
|
||||
f(t, `
|
||||
name: TestGroup
|
||||
params:
|
||||
nocache: ["1"]
|
||||
rules:
|
||||
- alert: foo
|
||||
expr: sum by(job) (up == 1)
|
||||
`, `
|
||||
name: TestGroup
|
||||
params:
|
||||
nocache: ["0"]
|
||||
rules:
|
||||
- alert: foo
|
||||
expr: sum by(job) (up == 1)
|
||||
`)
|
||||
})
|
||||
}
|
||||
|
||||
func TestGroupParams(t *testing.T) {
|
||||
f := func(t *testing.T, data string, expParams url.Values) {
|
||||
t.Helper()
|
||||
var g Group
|
||||
if err := yaml.Unmarshal([]byte(data), &g); err != nil {
|
||||
t.Fatalf("failed to unmarshal: %s", err)
|
||||
}
|
||||
got, exp := g.Params.Encode(), expParams.Encode()
|
||||
if got != exp {
|
||||
t.Fatalf("expected to have %q; got %q", exp, got)
|
||||
}
|
||||
}
|
||||
|
||||
t.Run("no params", func(t *testing.T) {
|
||||
f(t, `
|
||||
name: TestGroup
|
||||
rules:
|
||||
- alert: ExampleAlertAlwaysFiring
|
||||
expr: sum by(job) (up == 1)
|
||||
`, url.Values{})
|
||||
})
|
||||
|
||||
t.Run("params", func(t *testing.T) {
|
||||
f(t, `
|
||||
name: TestGroup
|
||||
params:
|
||||
nocache: ["1"]
|
||||
denyPartialResponse: ["true"]
|
||||
rules:
|
||||
- alert: ExampleAlertAlwaysFiring
|
||||
expr: sum by(job) (up == 1)
|
||||
`, url.Values{"nocache": {"1"}, "denyPartialResponse": {"true"}})
|
||||
})
|
||||
|
||||
t.Run("extra labels", func(t *testing.T) {
|
||||
f(t, `
|
||||
name: TestGroup
|
||||
extra_filter_labels:
|
||||
job: victoriametrics
|
||||
env: prod
|
||||
rules:
|
||||
- alert: ExampleAlertAlwaysFiring
|
||||
expr: sum by(job) (up == 1)
|
||||
`, url.Values{"extra_label": {"env=prod", "job=victoriametrics"}})
|
||||
})
|
||||
|
||||
t.Run("extra labels and params", func(t *testing.T) {
|
||||
f(t, `
|
||||
name: TestGroup
|
||||
extra_filter_labels:
|
||||
job: victoriametrics
|
||||
params:
|
||||
nocache: ["1"]
|
||||
extra_label: ["env=prod"]
|
||||
rules:
|
||||
- alert: ExampleAlertAlwaysFiring
|
||||
expr: sum by(job) (up == 1)
|
||||
`, url.Values{"nocache": {"1"}, "extra_label": {"env=prod", "job=victoriametrics"}})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
groups:
|
||||
- name: TestUpdateGroup
|
||||
interval: 2s
|
||||
concurrency: 2
|
||||
type: prometheus
|
||||
labels:
|
||||
cluster: main
|
||||
rules:
|
||||
- alert: up
|
||||
expr: up == 0
|
||||
for: 30s
|
||||
- alert: up graphite
|
||||
expr: filterSeries(time('host.1',20),'>','0')
|
||||
for: 30s
|
||||
type: graphite
|
||||
@@ -1,12 +0,0 @@
|
||||
groups:
|
||||
- name: TestUpdateGroup
|
||||
interval: 30s
|
||||
type: graphite
|
||||
rules:
|
||||
- alert: up
|
||||
expr: filterSeries(time('host.2',20),'>','0')
|
||||
for: 30s
|
||||
- alert: up graphite
|
||||
expr: filterSeries(time('host.1',20),'>','0')
|
||||
for: 30s
|
||||
type: graphite
|
||||
@@ -1,5 +1,8 @@
|
||||
groups:
|
||||
- name: groupGorSingleAlert
|
||||
params:
|
||||
nocache: ["1"]
|
||||
denyPartialResponse: ["true"]
|
||||
rules:
|
||||
- alert: VMRows
|
||||
for: 10s
|
||||
|
||||
@@ -2,8 +2,11 @@ groups:
|
||||
- name: TestGroup
|
||||
interval: 2s
|
||||
concurrency: 2
|
||||
extra_filter_labels:
|
||||
extra_filter_labels: # deprecated param, use `params` instead
|
||||
job: victoriametrics
|
||||
params:
|
||||
denyPartialResponse: ["true"]
|
||||
extra_label: ["env=dev"]
|
||||
rules:
|
||||
- alert: Conns
|
||||
expr: sum(vm_tcplistener_conns) by(instance) > 1
|
||||
@@ -22,6 +25,7 @@ groups:
|
||||
dynamic: '{{ $x := query "up" | first | value }}{{ if eq 1.0 $x }}one{{ else }}unknown{{ end }}'
|
||||
annotations:
|
||||
description: Job {{ $labels.job }} is up!
|
||||
external: cluster-{{ $externalLabels.cluster }}; replica-{{ $externalLabels.replica }}
|
||||
summary: All instances up {{ range query "up" }}
|
||||
{{ . | label "instance" }}
|
||||
{{ end }}
|
||||
|
||||
@@ -21,10 +21,3 @@ groups:
|
||||
annotations:
|
||||
summary: Too high connection number for {{$labels.instance}}
|
||||
description: "It is {{ $value }} connections for {{$labels.instance}}"
|
||||
- alert: HostDown
|
||||
type: graphite
|
||||
expr: filterSeries(sumSeries(host.receiver.interface.up),'last','=', 0)
|
||||
for: 3m
|
||||
annotations:
|
||||
summary: Too high connection number for {{$labels.instance}}
|
||||
description: "It is {{ $value }} connections for {{$labels.instance}}"
|
||||
|
||||
8
app/vmalert/config/testdata/rules4-good.rules
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
groups:
|
||||
- name: TestEmptyRules
|
||||
interval: 2s
|
||||
concurrency: 2
|
||||
rules:
|
||||
|
||||
- name: TestNoRules
|
||||
type: prometheus
|
||||
12
app/vmalert/config/testdata/rules_interval_good.rules
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
groups:
|
||||
- name: groupTest
|
||||
interval: 1s
|
||||
rules:
|
||||
- alert: VMRows
|
||||
for: 2s
|
||||
expr: sum(rate(vm_http_request_errors_total[2s])) > 0
|
||||
labels:
|
||||
label: bar
|
||||
host: "{{ $labels.instance }}"
|
||||
annotations:
|
||||
summary: "{{ $value }}"
|
||||
@@ -2,6 +2,7 @@ package datasource
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/url"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -20,8 +21,7 @@ type QuerierBuilder interface {
|
||||
type QuerierParams struct {
|
||||
DataSourceType *Type
|
||||
EvaluationInterval time.Duration
|
||||
// see https://docs.victoriametrics.com/#prometheus-querying-api-enhancements
|
||||
ExtraLabels map[string]string
|
||||
QueryParams url.Values
|
||||
}
|
||||
|
||||
// Metric is the basic entity which should be return by datasource
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/utils"
|
||||
@@ -12,12 +13,14 @@ import (
|
||||
var (
|
||||
addr = flag.String("datasource.url", "", "VictoriaMetrics or vmselect url. Required parameter. "+
|
||||
"E.g. http://127.0.0.1:8428")
|
||||
appendTypePrefix = flag.Bool("datasource.appendTypePrefix", false, "Whether to add type prefix to -datasource.url based on the query type. Set to true if sending different query types to the vmselect URL.")
|
||||
appendTypePrefix = flag.Bool("datasource.appendTypePrefix", false, "Whether to add type prefix to -datasource.url based on the query type. Set to true if sending different query types to the vmselect URL.")
|
||||
|
||||
basicAuthUsername = flag.String("datasource.basicAuth.username", "", "Optional basic auth username for -datasource.url")
|
||||
basicAuthPassword = flag.String("datasource.basicAuth.password", "", "Optional basic auth password for -datasource.url")
|
||||
basicAuthPasswordFile = flag.String("datasource.basicAuth.passwordFile", "", "Optional path to basic auth password to use for -datasource.url")
|
||||
bearerToken = flag.String("datasource.bearerToken", "", "Optional bearer auth token to use for -datasource.url.")
|
||||
bearerTokenFile = flag.String("datasource.bearerTokenFile", "", "Optional path to bearer token file to use for -datasource.url.")
|
||||
|
||||
bearerToken = flag.String("datasource.bearerToken", "", "Optional bearer auth token to use for -datasource.url.")
|
||||
bearerTokenFile = flag.String("datasource.bearerTokenFile", "", "Optional path to bearer token file to use for -datasource.url.")
|
||||
|
||||
tlsInsecureSkipVerify = flag.Bool("datasource.tlsInsecureSkipVerify", false, "Whether to skip tls verification when connecting to -datasource.url")
|
||||
tlsCertFile = flag.String("datasource.tlsCertFile", "", "Optional path to client-side TLS certificate file to use when connecting to -datasource.url")
|
||||
@@ -25,10 +28,18 @@ var (
|
||||
tlsCAFile = flag.String("datasource.tlsCAFile", "", `Optional path to TLS CA file to use for verifying connections to -datasource.url. By default, system CA is used`)
|
||||
tlsServerName = flag.String("datasource.tlsServerName", "", `Optional TLS server name to use for connections to -datasource.url. By default, the server name from -datasource.url is used`)
|
||||
|
||||
oauth2ClientID = flag.String("datasource.oauth2.clientID", "", "Optional OAuth2 clientID to use for -datasource.url. ")
|
||||
oauth2ClientSecret = flag.String("datasource.oauth2.clientSecret", "", "Optional OAuth2 clientSecret to use for -datasource.url.")
|
||||
oauth2ClientSecretFile = flag.String("datasource.oauth2.clientSecretFile", "", "Optional OAuth2 clientSecretFile to use for -datasource.url. ")
|
||||
oauth2TokenURL = flag.String("datasource.oauth2.tokenUrl", "", "Optional OAuth2 tokenURL to use for -datasource.url.")
|
||||
oauth2Scopes = flag.String("datasource.oauth2.scopes", "", "Optional OAuth2 scopes to use for -datasource.url. Scopes must be delimited by ';'")
|
||||
|
||||
lookBack = flag.Duration("datasource.lookback", 0, `Lookback defines how far into the past to look when evaluating queries. For example, if the datasource.lookback=5m then param "time" with value now()-5m will be added to every query.`)
|
||||
queryStep = flag.Duration("datasource.queryStep", 0, "queryStep defines how far a value can fallback to when evaluating queries. "+
|
||||
"For example, if datasource.queryStep=15s then param \"step\" with value \"15s\" will be added to every query."+
|
||||
"If queryStep isn't specified, rule's evaluationInterval will be used instead.")
|
||||
queryTimeAlignment = flag.Bool("datasource.queryTimeAlignment", true, `Whether to align "time" parameter with evaluation interval.`+
|
||||
"Alignment supposed to produce deterministic results despite of number of vmalert replicas or time they were started. See more details here https://github.com/VictoriaMetrics/VictoriaMetrics/pull/1257")
|
||||
maxIdleConnections = flag.Int("datasource.maxIdleConnections", 100, `Defines the number of idle (keep-alive connections) to each configured datasource. Consider setting this value equal to the value: groups_total * group.concurrency. Too low a value may result in a high number of sockets in TIME_WAIT state.`)
|
||||
roundDigits = flag.Int("datasource.roundDigits", 0, `Adds "round_digits" GET param to datasource requests. `+
|
||||
`In VM "round_digits" limits the number of digits after the decimal point in response values.`)
|
||||
@@ -40,9 +51,9 @@ type Param struct {
|
||||
}
|
||||
|
||||
// Init creates a Querier from provided flag values.
|
||||
// Provided extraParams will be added as GET params to
|
||||
// Provided extraParams will be added as GET params for
|
||||
// each request.
|
||||
func Init(extraParams []Param) (QuerierBuilder, error) {
|
||||
func Init(extraParams url.Values) (QuerierBuilder, error) {
|
||||
if *addr == "" {
|
||||
return nil, fmt.Errorf("datasource.url is empty")
|
||||
}
|
||||
@@ -52,15 +63,21 @@ func Init(extraParams []Param) (QuerierBuilder, error) {
|
||||
return nil, fmt.Errorf("failed to create transport: %w", err)
|
||||
}
|
||||
tr.MaxIdleConnsPerHost = *maxIdleConnections
|
||||
|
||||
if *roundDigits > 0 {
|
||||
extraParams = append(extraParams, Param{
|
||||
Key: "round_digits",
|
||||
Value: fmt.Sprintf("%d", *roundDigits),
|
||||
})
|
||||
if tr.MaxIdleConns != 0 && tr.MaxIdleConns < tr.MaxIdleConnsPerHost {
|
||||
tr.MaxIdleConns = tr.MaxIdleConnsPerHost
|
||||
}
|
||||
|
||||
authCfg, err := utils.AuthConfig(*basicAuthUsername, *basicAuthPassword, *basicAuthPasswordFile, *bearerToken, *bearerTokenFile)
|
||||
if extraParams == nil {
|
||||
extraParams = url.Values{}
|
||||
}
|
||||
if *roundDigits > 0 {
|
||||
extraParams.Set("round_digits", fmt.Sprintf("%d", *roundDigits))
|
||||
}
|
||||
|
||||
authCfg, err := utils.AuthConfig(
|
||||
utils.WithBasicAuth(*basicAuthUsername, *basicAuthPassword, *basicAuthPasswordFile),
|
||||
utils.WithBearer(*bearerToken, *bearerTokenFile),
|
||||
utils.WithOAuth(*oauth2ClientID, *oauth2ClientSecret, *oauth2ClientSecretFile, *oauth2TokenURL, *oauth2Scopes))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to configure auth: %w", err)
|
||||
}
|
||||
|
||||
@@ -7,9 +7,6 @@ import (
|
||||
"github.com/VictoriaMetrics/metricsql"
|
||||
)
|
||||
|
||||
const graphiteType = "graphite"
|
||||
const prometheusType = "prometheus"
|
||||
|
||||
// Type represents data source type
|
||||
type Type struct {
|
||||
name string
|
||||
@@ -17,12 +14,16 @@ type Type struct {
|
||||
|
||||
// NewPrometheusType returns prometheus datasource type
|
||||
func NewPrometheusType() Type {
|
||||
return Type{name: prometheusType}
|
||||
return Type{
|
||||
name: "prometheus",
|
||||
}
|
||||
}
|
||||
|
||||
// NewGraphiteType returns graphite datasource type
|
||||
func NewGraphiteType() Type {
|
||||
return Type{name: graphiteType}
|
||||
return Type{
|
||||
name: "graphite",
|
||||
}
|
||||
}
|
||||
|
||||
// NewRawType returns datasource type from raw string
|
||||
@@ -44,19 +45,19 @@ func (t *Type) Set(d Type) {
|
||||
// String implements String interface with default value.
|
||||
func (t Type) String() string {
|
||||
if t.name == "" {
|
||||
return prometheusType
|
||||
return "prometheus"
|
||||
}
|
||||
return t.name
|
||||
}
|
||||
|
||||
// ValidateExpr validates query expression with datasource ql.
|
||||
func (t *Type) ValidateExpr(expr string) error {
|
||||
switch t.name {
|
||||
case graphiteType:
|
||||
switch t.String() {
|
||||
case "graphite":
|
||||
if _, err := graphiteql.Parse(expr); err != nil {
|
||||
return fmt.Errorf("bad graphite expr: %q, err: %w", expr, err)
|
||||
}
|
||||
case "", prometheusType:
|
||||
case "prometheus":
|
||||
if _, err := metricsql.Parse(expr); err != nil {
|
||||
return fmt.Errorf("bad prometheus expr: %q, err: %w", expr, err)
|
||||
}
|
||||
@@ -72,12 +73,13 @@ func (t *Type) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
if err := unmarshal(&s); err != nil {
|
||||
return err
|
||||
}
|
||||
if s == "" {
|
||||
s = "prometheus"
|
||||
}
|
||||
switch s {
|
||||
case "":
|
||||
s = prometheusType
|
||||
case graphiteType, prometheusType:
|
||||
case "graphite", "prometheus":
|
||||
default:
|
||||
return fmt.Errorf("unknown datasource type=%q, want %q or %q", s, prometheusType, graphiteType)
|
||||
return fmt.Errorf("unknown datasource type=%q, want %q or %q", s, "prometheus", "graphite")
|
||||
}
|
||||
t.name = s
|
||||
return nil
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -22,20 +23,21 @@ type VMStorage struct {
|
||||
|
||||
dataSourceType Type
|
||||
evaluationInterval time.Duration
|
||||
extraLabels []string
|
||||
extraParams []Param
|
||||
extraParams url.Values
|
||||
disablePathAppend bool
|
||||
}
|
||||
|
||||
// Clone makes clone of VMStorage, shares http client.
|
||||
func (s *VMStorage) Clone() *VMStorage {
|
||||
return &VMStorage{
|
||||
c: s.c,
|
||||
authCfg: s.authCfg,
|
||||
datasourceURL: s.datasourceURL,
|
||||
lookBack: s.lookBack,
|
||||
queryStep: s.queryStep,
|
||||
appendTypePrefix: s.appendTypePrefix,
|
||||
dataSourceType: s.dataSourceType,
|
||||
c: s.c,
|
||||
authCfg: s.authCfg,
|
||||
datasourceURL: s.datasourceURL,
|
||||
lookBack: s.lookBack,
|
||||
queryStep: s.queryStep,
|
||||
appendTypePrefix: s.appendTypePrefix,
|
||||
dataSourceType: s.dataSourceType,
|
||||
disablePathAppend: s.disablePathAppend,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,9 +47,7 @@ func (s *VMStorage) ApplyParams(params QuerierParams) *VMStorage {
|
||||
s.dataSourceType = *params.DataSourceType
|
||||
}
|
||||
s.evaluationInterval = params.EvaluationInterval
|
||||
for k, v := range params.ExtraLabels {
|
||||
s.extraLabels = append(s.extraLabels, fmt.Sprintf("%s=%s", k, v))
|
||||
}
|
||||
s.extraParams = params.QueryParams
|
||||
return s
|
||||
}
|
||||
|
||||
@@ -57,15 +57,16 @@ func (s *VMStorage) BuildWithParams(params QuerierParams) Querier {
|
||||
}
|
||||
|
||||
// NewVMStorage is a constructor for VMStorage
|
||||
func NewVMStorage(baseURL string, authCfg *promauth.Config, lookBack time.Duration, queryStep time.Duration, appendTypePrefix bool, c *http.Client) *VMStorage {
|
||||
func NewVMStorage(baseURL string, authCfg *promauth.Config, lookBack time.Duration, queryStep time.Duration, appendTypePrefix bool, c *http.Client, disablePathAppend bool) *VMStorage {
|
||||
return &VMStorage{
|
||||
c: c,
|
||||
authCfg: authCfg,
|
||||
datasourceURL: strings.TrimSuffix(baseURL, "/"),
|
||||
appendTypePrefix: appendTypePrefix,
|
||||
lookBack: lookBack,
|
||||
queryStep: queryStep,
|
||||
dataSourceType: NewPrometheusType(),
|
||||
c: c,
|
||||
authCfg: authCfg,
|
||||
datasourceURL: strings.TrimSuffix(baseURL, "/"),
|
||||
appendTypePrefix: appendTypePrefix,
|
||||
lookBack: lookBack,
|
||||
queryStep: queryStep,
|
||||
dataSourceType: NewPrometheusType(),
|
||||
disablePathAppend: disablePathAppend,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,10 +78,10 @@ func (s *VMStorage) Query(ctx context.Context, query string) ([]Metric, error) {
|
||||
}
|
||||
|
||||
ts := time.Now()
|
||||
switch s.dataSourceType.name {
|
||||
case "", prometheusType:
|
||||
switch s.dataSourceType.String() {
|
||||
case "prometheus":
|
||||
s.setPrometheusInstantReqParams(req, query, ts)
|
||||
case graphiteType:
|
||||
case "graphite":
|
||||
s.setGraphiteReqParams(req, query, ts)
|
||||
default:
|
||||
return nil, fmt.Errorf("engine not found: %q", s.dataSourceType.name)
|
||||
@@ -95,7 +96,7 @@ func (s *VMStorage) Query(ctx context.Context, query string) ([]Metric, error) {
|
||||
}()
|
||||
|
||||
parseFn := parsePrometheusResponse
|
||||
if s.dataSourceType.name != prometheusType {
|
||||
if s.dataSourceType.name != "prometheus" {
|
||||
parseFn = parseGraphiteResponse
|
||||
}
|
||||
return parseFn(req, resp)
|
||||
@@ -105,7 +106,7 @@ func (s *VMStorage) Query(ctx context.Context, query string) ([]Metric, error) {
|
||||
// For Prometheus type see https://prometheus.io/docs/prometheus/latest/querying/api/#range-queries
|
||||
// Graphite type isn't supported.
|
||||
func (s *VMStorage) QueryRange(ctx context.Context, query string, start, end time.Time) ([]Metric, error) {
|
||||
if s.dataSourceType.name != prometheusType {
|
||||
if s.dataSourceType.name != "prometheus" {
|
||||
return nil, fmt.Errorf("%q is not supported for QueryRange", s.dataSourceType.name)
|
||||
}
|
||||
req, err := s.newRequestPOST()
|
||||
@@ -132,12 +133,12 @@ func (s *VMStorage) QueryRange(ctx context.Context, query string, start, end tim
|
||||
func (s *VMStorage) do(ctx context.Context, req *http.Request) (*http.Response, error) {
|
||||
resp, err := s.c.Do(req.WithContext(ctx))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting response from %s: %w", req.URL, err)
|
||||
return nil, fmt.Errorf("error getting response from %s: %w", req.URL.Redacted(), err)
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
body, _ := ioutil.ReadAll(resp.Body)
|
||||
_ = resp.Body.Close()
|
||||
return nil, fmt.Errorf("unexpected response code %d for %s. Response body %s", resp.StatusCode, req.URL, body)
|
||||
return nil, fmt.Errorf("unexpected response code %d for %s. Response body %s", resp.StatusCode, req.URL.Redacted(), body)
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
@@ -147,7 +148,7 @@ func (s *VMStorage) newRequestPOST() (*http.Request, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json; charset=utf-8")
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
if s.authCfg != nil {
|
||||
if auth := s.authCfg.GetAuthHeader(); auth != "" {
|
||||
req.Header.Set("Authorization", auth)
|
||||
|
||||
@@ -38,7 +38,7 @@ func (r graphiteResponse) metrics() []Metric {
|
||||
func parseGraphiteResponse(req *http.Request, resp *http.Response) ([]Metric, error) {
|
||||
r := &graphiteResponse{}
|
||||
if err := json.NewDecoder(resp.Body).Decode(r); err != nil {
|
||||
return nil, fmt.Errorf("error parsing graphite metrics for %s: %w", req.URL, err)
|
||||
return nil, fmt.Errorf("error parsing graphite metrics for %s: %w", req.URL.Redacted(), err)
|
||||
}
|
||||
return r.metrics(), nil
|
||||
}
|
||||
@@ -54,6 +54,14 @@ func (s *VMStorage) setGraphiteReqParams(r *http.Request, query string, timestam
|
||||
}
|
||||
r.URL.Path += graphitePath
|
||||
q := r.URL.Query()
|
||||
for k, vs := range s.extraParams {
|
||||
if q.Has(k) { // extraParams are prior to params in URL
|
||||
q.Del(k)
|
||||
}
|
||||
for _, v := range vs {
|
||||
q.Add(k, v)
|
||||
}
|
||||
}
|
||||
q.Set("format", "json")
|
||||
q.Set("target", query)
|
||||
from := "-5min"
|
||||
|
||||
@@ -82,10 +82,10 @@ const (
|
||||
func parsePrometheusResponse(req *http.Request, resp *http.Response) ([]Metric, error) {
|
||||
r := &promResponse{}
|
||||
if err := json.NewDecoder(resp.Body).Decode(r); err != nil {
|
||||
return nil, fmt.Errorf("error parsing prometheus metrics for %s: %w", req.URL, err)
|
||||
return nil, fmt.Errorf("error parsing prometheus metrics for %s: %w", req.URL.Redacted(), err)
|
||||
}
|
||||
if r.Status == statusError {
|
||||
return nil, fmt.Errorf("response error, query: %s, errorType: %s, error: %s", req.URL, r.ErrorType, r.Error)
|
||||
return nil, fmt.Errorf("response error, query: %s, errorType: %s, error: %s", req.URL.Redacted(), r.ErrorType, r.Error)
|
||||
}
|
||||
if r.Status != statusSuccess {
|
||||
return nil, fmt.Errorf("unknown status: %s, Expected success or error ", r.Status)
|
||||
@@ -118,12 +118,14 @@ func (s *VMStorage) setPrometheusInstantReqParams(r *http.Request, query string,
|
||||
if s.appendTypePrefix {
|
||||
r.URL.Path += prometheusPrefix
|
||||
}
|
||||
r.URL.Path += prometheusInstantPath
|
||||
if !s.disablePathAppend {
|
||||
r.URL.Path += prometheusInstantPath
|
||||
}
|
||||
q := r.URL.Query()
|
||||
if s.lookBack > 0 {
|
||||
timestamp = timestamp.Add(-s.lookBack)
|
||||
}
|
||||
if s.evaluationInterval > 0 {
|
||||
if *queryTimeAlignment && s.evaluationInterval > 0 {
|
||||
// see https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1232
|
||||
timestamp = timestamp.Truncate(s.evaluationInterval)
|
||||
}
|
||||
@@ -136,7 +138,9 @@ func (s *VMStorage) setPrometheusRangeReqParams(r *http.Request, query string, s
|
||||
if s.appendTypePrefix {
|
||||
r.URL.Path += prometheusPrefix
|
||||
}
|
||||
r.URL.Path += prometheusRangePath
|
||||
if !s.disablePathAppend {
|
||||
r.URL.Path += prometheusRangePath
|
||||
}
|
||||
q := r.URL.Query()
|
||||
q.Add("start", fmt.Sprintf("%d", start.Unix()))
|
||||
q.Add("end", fmt.Sprintf("%d", end.Unix()))
|
||||
@@ -146,20 +150,24 @@ func (s *VMStorage) setPrometheusRangeReqParams(r *http.Request, query string, s
|
||||
|
||||
func (s *VMStorage) setPrometheusReqParams(r *http.Request, query string) {
|
||||
q := r.URL.Query()
|
||||
for k, vs := range s.extraParams {
|
||||
if q.Has(k) { // extraParams are prior to params in URL
|
||||
q.Del(k)
|
||||
}
|
||||
for _, v := range vs {
|
||||
q.Add(k, v)
|
||||
}
|
||||
}
|
||||
q.Set("query", query)
|
||||
if s.evaluationInterval > 0 {
|
||||
// set step as evaluationInterval by default
|
||||
q.Set("step", s.evaluationInterval.String())
|
||||
if s.evaluationInterval > 0 { // set step as evaluationInterval by default
|
||||
// always convert to seconds to keep compatibility with older
|
||||
// Prometheus versions. See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1943
|
||||
q.Set("step", fmt.Sprintf("%ds", int(s.evaluationInterval.Seconds())))
|
||||
}
|
||||
if s.queryStep > 0 {
|
||||
// override step with user-specified value
|
||||
q.Set("step", s.queryStep.String())
|
||||
}
|
||||
for _, l := range s.extraLabels {
|
||||
q.Add("extra_label", l)
|
||||
}
|
||||
for _, p := range s.extraParams {
|
||||
q.Add(p.Key, p.Value)
|
||||
if s.queryStep > 0 { // override step with user-specified value
|
||||
// always convert to seconds to keep compatibility with older
|
||||
// Prometheus versions. See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1943
|
||||
q.Set("step", fmt.Sprintf("%ds", int(s.queryStep.Seconds())))
|
||||
}
|
||||
r.URL.RawQuery = q.Encode()
|
||||
}
|
||||
|
||||
@@ -5,12 +5,14 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/utils"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promauth"
|
||||
)
|
||||
|
||||
@@ -20,7 +22,7 @@ var (
|
||||
basicAuthPass = "bar"
|
||||
baCfg = &promauth.BasicAuthConfig{
|
||||
Username: basicAuthName,
|
||||
Password: basicAuthPass,
|
||||
Password: promauth.NewSecret(basicAuthPass),
|
||||
}
|
||||
query = "vm_rows"
|
||||
queryRender = "constantLine(10)"
|
||||
@@ -83,7 +85,7 @@ func TestVMInstantQuery(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected: %s", err)
|
||||
}
|
||||
s := NewVMStorage(srv.URL, authCfg, time.Minute, 0, false, srv.Client())
|
||||
s := NewVMStorage(srv.URL, authCfg, time.Minute, 0, false, srv.Client(), false)
|
||||
|
||||
p := NewPrometheusType()
|
||||
pq := s.BuildWithParams(QuerierParams{DataSourceType: &p, EvaluationInterval: 15 * time.Second})
|
||||
@@ -193,7 +195,7 @@ func TestVMRangeQuery(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected: %s", err)
|
||||
}
|
||||
s := NewVMStorage(srv.URL, authCfg, time.Minute, 0, false, srv.Client())
|
||||
s := NewVMStorage(srv.URL, authCfg, time.Minute, 0, false, srv.Client(), false)
|
||||
|
||||
p := NewPrometheusType()
|
||||
pq := s.BuildWithParams(QuerierParams{DataSourceType: &p, EvaluationInterval: 15 * time.Second})
|
||||
@@ -252,6 +254,17 @@ func TestRequestParams(t *testing.T) {
|
||||
checkEqualString(t, prometheusInstantPath, r.URL.Path)
|
||||
},
|
||||
},
|
||||
{
|
||||
"prometheus path with disablePathAppend",
|
||||
false,
|
||||
&VMStorage{
|
||||
dataSourceType: NewPrometheusType(),
|
||||
disablePathAppend: true,
|
||||
},
|
||||
func(t *testing.T, r *http.Request) {
|
||||
checkEqualString(t, "", r.URL.Path)
|
||||
},
|
||||
},
|
||||
{
|
||||
"prometheus prefix",
|
||||
false,
|
||||
@@ -263,6 +276,18 @@ func TestRequestParams(t *testing.T) {
|
||||
checkEqualString(t, prometheusPrefix+prometheusInstantPath, r.URL.Path)
|
||||
},
|
||||
},
|
||||
{
|
||||
"prometheus prefix with disablePathAppend",
|
||||
false,
|
||||
&VMStorage{
|
||||
dataSourceType: NewPrometheusType(),
|
||||
appendTypePrefix: true,
|
||||
disablePathAppend: true,
|
||||
},
|
||||
func(t *testing.T, r *http.Request) {
|
||||
checkEqualString(t, prometheusPrefix, r.URL.Path)
|
||||
},
|
||||
},
|
||||
{
|
||||
"prometheus range path",
|
||||
true,
|
||||
@@ -273,6 +298,17 @@ func TestRequestParams(t *testing.T) {
|
||||
checkEqualString(t, prometheusRangePath, r.URL.Path)
|
||||
},
|
||||
},
|
||||
{
|
||||
"prometheus range path with disablePathAppend",
|
||||
true,
|
||||
&VMStorage{
|
||||
dataSourceType: NewPrometheusType(),
|
||||
disablePathAppend: true,
|
||||
},
|
||||
func(t *testing.T, r *http.Request) {
|
||||
checkEqualString(t, "", r.URL.Path)
|
||||
},
|
||||
},
|
||||
{
|
||||
"prometheus range prefix",
|
||||
true,
|
||||
@@ -284,6 +320,18 @@ func TestRequestParams(t *testing.T) {
|
||||
checkEqualString(t, prometheusPrefix+prometheusRangePath, r.URL.Path)
|
||||
},
|
||||
},
|
||||
{
|
||||
"prometheus range prefix with disablePathAppend",
|
||||
true,
|
||||
&VMStorage{
|
||||
dataSourceType: NewPrometheusType(),
|
||||
appendTypePrefix: true,
|
||||
disablePathAppend: true,
|
||||
},
|
||||
func(t *testing.T, r *http.Request) {
|
||||
checkEqualString(t, prometheusPrefix, r.URL.Path)
|
||||
},
|
||||
},
|
||||
{
|
||||
"graphite path",
|
||||
false,
|
||||
@@ -389,15 +437,28 @@ func TestRequestParams(t *testing.T) {
|
||||
queryStep: time.Minute,
|
||||
},
|
||||
func(t *testing.T, r *http.Request) {
|
||||
exp := fmt.Sprintf("query=%s&step=%v&time=%d", query, time.Minute, timestamp.Unix())
|
||||
exp := fmt.Sprintf("query=%s&step=%ds&time=%d", query, int(time.Minute.Seconds()), timestamp.Unix())
|
||||
checkEqualString(t, exp, r.URL.RawQuery)
|
||||
},
|
||||
},
|
||||
{
|
||||
"round digits",
|
||||
"step to seconds",
|
||||
false,
|
||||
&VMStorage{
|
||||
extraParams: []Param{{"round_digits", "10"}},
|
||||
evaluationInterval: 3 * time.Hour,
|
||||
},
|
||||
func(t *testing.T, r *http.Request) {
|
||||
evalInterval := 3 * time.Hour
|
||||
tt := timestamp.Truncate(evalInterval)
|
||||
exp := fmt.Sprintf("query=%s&step=%ds&time=%d", query, int(evalInterval.Seconds()), tt.Unix())
|
||||
checkEqualString(t, exp, r.URL.RawQuery)
|
||||
},
|
||||
},
|
||||
{
|
||||
"prometheus extra params",
|
||||
false,
|
||||
&VMStorage{
|
||||
extraParams: url.Values{"round_digits": {"10"}},
|
||||
},
|
||||
func(t *testing.T, r *http.Request) {
|
||||
exp := fmt.Sprintf("query=%s&round_digits=10&time=%d", query, timestamp.Unix())
|
||||
@@ -405,45 +466,32 @@ func TestRequestParams(t *testing.T) {
|
||||
},
|
||||
},
|
||||
{
|
||||
"extra labels",
|
||||
false,
|
||||
&VMStorage{
|
||||
extraLabels: []string{
|
||||
"env=prod",
|
||||
"query=es=cape",
|
||||
},
|
||||
},
|
||||
func(t *testing.T, r *http.Request) {
|
||||
exp := fmt.Sprintf("extra_label=env%%3Dprod&extra_label=query%%3Des%%3Dcape&query=%s&time=%d", query, timestamp.Unix())
|
||||
checkEqualString(t, exp, r.URL.RawQuery)
|
||||
},
|
||||
},
|
||||
{
|
||||
"extra labels range",
|
||||
"prometheus extra params range",
|
||||
true,
|
||||
&VMStorage{
|
||||
extraLabels: []string{
|
||||
"env=prod",
|
||||
"query=es=cape",
|
||||
extraParams: url.Values{
|
||||
"nocache": {"1"},
|
||||
"max_lookback": {"1h"},
|
||||
},
|
||||
},
|
||||
func(t *testing.T, r *http.Request) {
|
||||
exp := fmt.Sprintf("end=%d&extra_label=env%%3Dprod&extra_label=query%%3Des%%3Dcape&query=%s&start=%d",
|
||||
exp := fmt.Sprintf("end=%d&max_lookback=1h&nocache=1&query=%s&start=%d",
|
||||
timestamp.Unix(), query, timestamp.Unix())
|
||||
checkEqualString(t, exp, r.URL.RawQuery)
|
||||
},
|
||||
},
|
||||
{
|
||||
"extra params",
|
||||
"graphite extra params",
|
||||
false,
|
||||
&VMStorage{
|
||||
extraParams: []Param{
|
||||
{Key: "nocache", Value: "1"},
|
||||
{Key: "max_lookback", Value: "1h"},
|
||||
dataSourceType: NewGraphiteType(),
|
||||
extraParams: url.Values{
|
||||
"nocache": {"1"},
|
||||
"max_lookback": {"1h"},
|
||||
},
|
||||
},
|
||||
func(t *testing.T, r *http.Request) {
|
||||
exp := fmt.Sprintf("max_lookback=1h&nocache=1&query=%s&time=%d", query, timestamp.Unix())
|
||||
exp := fmt.Sprintf("format=json&from=-5min&max_lookback=1h&nocache=1&target=%s&until=now", query)
|
||||
checkEqualString(t, exp, r.URL.RawQuery)
|
||||
},
|
||||
},
|
||||
@@ -455,14 +503,14 @@ func TestRequestParams(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %s", err)
|
||||
}
|
||||
switch tc.vm.dataSourceType.name {
|
||||
case "", prometheusType:
|
||||
switch tc.vm.dataSourceType.String() {
|
||||
case "prometheus":
|
||||
if tc.queryRange {
|
||||
tc.vm.setPrometheusRangeReqParams(req, query, timestamp, timestamp)
|
||||
} else {
|
||||
tc.vm.setPrometheusInstantReqParams(req, query, timestamp)
|
||||
}
|
||||
case graphiteType:
|
||||
case "graphite":
|
||||
tc.vm.setGraphiteReqParams(req, query, timestamp)
|
||||
}
|
||||
tc.checkFn(t, req)
|
||||
@@ -470,10 +518,63 @@ func TestRequestParams(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthConfig(t *testing.T) {
|
||||
var testCases = []struct {
|
||||
name string
|
||||
vmFn func() *VMStorage
|
||||
checkFn func(t *testing.T, r *http.Request)
|
||||
}{
|
||||
{
|
||||
name: "basic auth",
|
||||
vmFn: func() *VMStorage {
|
||||
cfg, err := utils.AuthConfig(utils.WithBasicAuth("foo", "bar", ""))
|
||||
if err != nil {
|
||||
t.Errorf("Error get auth config: %s", err)
|
||||
}
|
||||
return &VMStorage{authCfg: cfg}
|
||||
},
|
||||
checkFn: func(t *testing.T, r *http.Request) {
|
||||
u, p, _ := r.BasicAuth()
|
||||
checkEqualString(t, "foo", u)
|
||||
checkEqualString(t, "bar", p)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "bearer auth",
|
||||
vmFn: func() *VMStorage {
|
||||
cfg, err := utils.AuthConfig(utils.WithBearer("foo", ""))
|
||||
if err != nil {
|
||||
t.Errorf("Error get auth config: %s", err)
|
||||
}
|
||||
return &VMStorage{authCfg: cfg}
|
||||
},
|
||||
checkFn: func(t *testing.T, r *http.Request) {
|
||||
reqToken := r.Header.Get("Authorization")
|
||||
splitToken := strings.Split(reqToken, "Bearer ")
|
||||
if len(splitToken) != 2 {
|
||||
t.Errorf("expected two items got %d", len(splitToken))
|
||||
}
|
||||
token := splitToken[1]
|
||||
checkEqualString(t, "foo", token)
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range testCases {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
vm := tt.vmFn()
|
||||
req, err := vm.newRequestPOST()
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %s", err)
|
||||
}
|
||||
tt.checkFn(t, req)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func checkEqualString(t *testing.T, exp, got string) {
|
||||
t.Helper()
|
||||
if got != exp {
|
||||
t.Errorf("expected to get %q; got %q", exp, got)
|
||||
t.Errorf("expected to get: \n%q; \ngot: \n%q", exp, got)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"hash/fnv"
|
||||
"net/url"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -18,17 +19,18 @@ import (
|
||||
|
||||
// Group is an entity for grouping rules
|
||||
type Group struct {
|
||||
mu sync.RWMutex
|
||||
Name string
|
||||
File string
|
||||
Rules []Rule
|
||||
Type datasource.Type
|
||||
Interval time.Duration
|
||||
Concurrency int
|
||||
Checksum string
|
||||
mu sync.RWMutex
|
||||
Name string
|
||||
File string
|
||||
Rules []Rule
|
||||
Type datasource.Type
|
||||
Interval time.Duration
|
||||
Concurrency int
|
||||
Checksum string
|
||||
LastEvaluation time.Time
|
||||
|
||||
ExtraFilterLabels map[string]string
|
||||
Labels map[string]string
|
||||
Labels map[string]string
|
||||
Params url.Values
|
||||
|
||||
doneCh chan struct{}
|
||||
finishedCh chan struct{}
|
||||
@@ -40,15 +42,15 @@ type Group struct {
|
||||
}
|
||||
|
||||
type groupMetrics struct {
|
||||
iterationTotal *counter
|
||||
iterationDuration *summary
|
||||
iterationTotal *utils.Counter
|
||||
iterationDuration *utils.Summary
|
||||
}
|
||||
|
||||
func newGroupMetrics(name, file string) *groupMetrics {
|
||||
m := &groupMetrics{}
|
||||
labels := fmt.Sprintf(`group=%q, file=%q`, name, file)
|
||||
m.iterationTotal = getOrCreateCounter(fmt.Sprintf(`vmalert_iteration_total{%s}`, labels))
|
||||
m.iterationDuration = getOrCreateSummary(fmt.Sprintf(`vmalert_iteration_duration_seconds{%s}`, labels))
|
||||
m.iterationTotal = utils.GetOrCreateCounter(fmt.Sprintf(`vmalert_iteration_total{%s}`, labels))
|
||||
m.iterationDuration = utils.GetOrCreateSummary(fmt.Sprintf(`vmalert_iteration_duration_seconds{%s}`, labels))
|
||||
return m
|
||||
}
|
||||
|
||||
@@ -71,14 +73,14 @@ func mergeLabels(groupName, ruleName string, set1, set2 map[string]string) map[s
|
||||
|
||||
func newGroup(cfg config.Group, qb datasource.QuerierBuilder, defaultInterval time.Duration, labels map[string]string) *Group {
|
||||
g := &Group{
|
||||
Type: cfg.Type,
|
||||
Name: cfg.Name,
|
||||
File: cfg.File,
|
||||
Interval: cfg.Interval.Duration(),
|
||||
Concurrency: cfg.Concurrency,
|
||||
Checksum: cfg.Checksum,
|
||||
ExtraFilterLabels: cfg.ExtraFilterLabels,
|
||||
Labels: cfg.Labels,
|
||||
Type: cfg.Type,
|
||||
Name: cfg.Name,
|
||||
File: cfg.File,
|
||||
Interval: cfg.Interval.Duration(),
|
||||
Concurrency: cfg.Concurrency,
|
||||
Checksum: cfg.Checksum,
|
||||
Params: cfg.Params,
|
||||
Labels: cfg.Labels,
|
||||
|
||||
doneCh: make(chan struct{}),
|
||||
finishedCh: make(chan struct{}),
|
||||
@@ -121,8 +123,11 @@ func (g *Group) newRule(qb datasource.QuerierBuilder, rule config.Rule) Rule {
|
||||
}
|
||||
|
||||
// ID return unique group ID that consists of
|
||||
// rules file and group name
|
||||
// rules file and group Name
|
||||
func (g *Group) ID() uint64 {
|
||||
g.mu.RLock()
|
||||
defer g.mu.RUnlock()
|
||||
|
||||
hash := fnv.New64a()
|
||||
hash.Write([]byte(g.File))
|
||||
hash.Write([]byte("\xff"))
|
||||
@@ -195,7 +200,7 @@ func (g *Group) updateWith(newGroup *Group) error {
|
||||
// group.Start function
|
||||
g.Type = newGroup.Type
|
||||
g.Concurrency = newGroup.Concurrency
|
||||
g.ExtraFilterLabels = newGroup.ExtraFilterLabels
|
||||
g.Params = newGroup.Params
|
||||
g.Labels = newGroup.Labels
|
||||
g.Checksum = newGroup.Checksum
|
||||
g.Rules = newRules
|
||||
@@ -209,8 +214,8 @@ func (g *Group) close() {
|
||||
close(g.doneCh)
|
||||
<-g.finishedCh
|
||||
|
||||
metrics.UnregisterMetric(g.metrics.iterationDuration.name)
|
||||
metrics.UnregisterMetric(g.metrics.iterationTotal.name)
|
||||
g.metrics.iterationDuration.Unregister()
|
||||
g.metrics.iterationTotal.Unregister()
|
||||
for _, rule := range g.Rules {
|
||||
rule.Close()
|
||||
}
|
||||
@@ -218,12 +223,12 @@ func (g *Group) close() {
|
||||
|
||||
var skipRandSleepOnGroupStart bool
|
||||
|
||||
func (g *Group) start(ctx context.Context, nts []notifier.Notifier, rw *remotewrite.Client) {
|
||||
func (g *Group) start(ctx context.Context, nts func() []notifier.Notifier, rw *remotewrite.Client) {
|
||||
defer func() { close(g.finishedCh) }()
|
||||
|
||||
// Spread group rules evaluation over time in order to reduce load on VictoriaMetrics.
|
||||
if !skipRandSleepOnGroupStart {
|
||||
randSleep := uint64(float64(g.Interval) * (float64(uint32(g.ID())) / (1 << 32)))
|
||||
randSleep := uint64(float64(g.Interval) * (float64(g.ID()) / (1 << 64)))
|
||||
sleepOffset := uint64(time.Now().UnixNano()) % uint64(g.Interval)
|
||||
if randSleep < sleepOffset {
|
||||
randSleep += uint64(g.Interval)
|
||||
@@ -242,16 +247,7 @@ func (g *Group) start(ctx context.Context, nts []notifier.Notifier, rw *remotewr
|
||||
}
|
||||
|
||||
logger.Infof("group %q started; interval=%v; concurrency=%d", g.Name, g.Interval, g.Concurrency)
|
||||
e := &executor{rw: rw}
|
||||
for _, nt := range nts {
|
||||
ent := eNotifier{
|
||||
Notifier: nt,
|
||||
alertsSent: getOrCreateCounter(fmt.Sprintf("vmalert_alerts_sent_total{addr=%q}", nt.Addr())),
|
||||
alertsSendErrors: getOrCreateCounter(fmt.Sprintf("vmalert_alerts_send_errors_total{addr=%q}", nt.Addr())),
|
||||
}
|
||||
e.notifiers = append(e.notifiers, ent)
|
||||
}
|
||||
|
||||
e := &executor{rw: rw, notifiers: nts}
|
||||
t := time.NewTicker(g.Interval)
|
||||
defer t.Stop()
|
||||
for {
|
||||
@@ -280,41 +276,39 @@ func (g *Group) start(ctx context.Context, nts []notifier.Notifier, rw *remotewr
|
||||
case <-t.C:
|
||||
g.metrics.iterationTotal.Inc()
|
||||
iterationStart := time.Now()
|
||||
resolveDuration := getResolveDuration(g.Interval)
|
||||
errs := e.execConcurrently(ctx, g.Rules, g.Concurrency, resolveDuration)
|
||||
for err := range errs {
|
||||
if err != nil {
|
||||
logger.Errorf("group %q: %s", g.Name, err)
|
||||
if len(g.Rules) > 0 {
|
||||
errs := e.execConcurrently(ctx, g.Rules, g.Concurrency, getResolveDuration(g.Interval))
|
||||
for err := range errs {
|
||||
if err != nil {
|
||||
logger.Errorf("group %q: %s", g.Name, err)
|
||||
}
|
||||
}
|
||||
g.LastEvaluation = iterationStart
|
||||
}
|
||||
|
||||
g.metrics.iterationDuration.UpdateDuration(iterationStart)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// resolveDuration for alerts is equal to 3 interval evaluations
|
||||
// so in case if vmalert stops sending updates for some reason,
|
||||
// notifier could automatically resolve the alert.
|
||||
// getResolveDuration returns the duration after which firing alert
|
||||
// can be considered as resolved.
|
||||
func getResolveDuration(groupInterval time.Duration) time.Duration {
|
||||
resolveInterval := groupInterval * 3
|
||||
if *maxResolveDuration > 0 && (resolveInterval > *maxResolveDuration) {
|
||||
return *maxResolveDuration
|
||||
delta := *resendDelay
|
||||
if groupInterval > delta {
|
||||
delta = groupInterval
|
||||
}
|
||||
return resolveInterval
|
||||
resolveDuration := delta * 4
|
||||
if *maxResolveDuration > 0 && resolveDuration > *maxResolveDuration {
|
||||
resolveDuration = *maxResolveDuration
|
||||
}
|
||||
return resolveDuration
|
||||
}
|
||||
|
||||
type executor struct {
|
||||
notifiers []eNotifier
|
||||
notifiers func() []notifier.Notifier
|
||||
rw *remotewrite.Client
|
||||
}
|
||||
|
||||
type eNotifier struct {
|
||||
notifier.Notifier
|
||||
alertsSent *counter
|
||||
alertsSendErrors *counter
|
||||
}
|
||||
|
||||
func (e *executor) execConcurrently(ctx context.Context, rules []Rule, concurrency int, resolveDuration time.Duration) chan error {
|
||||
res := make(chan error, len(rules))
|
||||
if concurrency == 1 {
|
||||
@@ -351,11 +345,13 @@ var (
|
||||
execErrors = metrics.NewCounter(`vmalert_execution_errors_total`)
|
||||
|
||||
remoteWriteErrors = metrics.NewCounter(`vmalert_remotewrite_errors_total`)
|
||||
remoteWriteTotal = metrics.NewCounter(`vmalert_remotewrite_total`)
|
||||
)
|
||||
|
||||
func (e *executor) exec(ctx context.Context, rule Rule, resolveDuration time.Duration) error {
|
||||
execTotal.Inc()
|
||||
|
||||
now := time.Now()
|
||||
tss, err := rule.Exec(ctx)
|
||||
if err != nil {
|
||||
execErrors.Inc()
|
||||
@@ -364,6 +360,7 @@ func (e *executor) exec(ctx context.Context, rule Rule, resolveDuration time.Dur
|
||||
|
||||
if len(tss) > 0 && e.rw != nil {
|
||||
for _, ts := range tss {
|
||||
remoteWriteTotal.Inc()
|
||||
if err := e.rw.Push(ts); err != nil {
|
||||
remoteWriteErrors.Inc()
|
||||
return fmt.Errorf("rule %q: remote write failure: %w", rule, err)
|
||||
@@ -375,29 +372,16 @@ func (e *executor) exec(ctx context.Context, rule Rule, resolveDuration time.Dur
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
var alerts []notifier.Alert
|
||||
for _, a := range ar.alerts {
|
||||
switch a.State {
|
||||
case notifier.StateFiring:
|
||||
a.End = time.Now().Add(resolveDuration)
|
||||
alerts = append(alerts, *a)
|
||||
case notifier.StateInactive:
|
||||
// set End to execStart to notify
|
||||
// that it was just resolved
|
||||
a.End = time.Now()
|
||||
alerts = append(alerts, *a)
|
||||
}
|
||||
}
|
||||
|
||||
alerts := ar.alertsToSend(now, resolveDuration, *resendDelay)
|
||||
if len(alerts) < 1 {
|
||||
return nil
|
||||
}
|
||||
|
||||
errGr := new(utils.ErrGroup)
|
||||
for _, nt := range e.notifiers {
|
||||
nt.alertsSent.Add(len(alerts))
|
||||
for _, nt := range e.notifiers() {
|
||||
if err := nt.Send(ctx, alerts); err != nil {
|
||||
nt.alertsSendErrors.Inc()
|
||||
errGr.Add(fmt.Errorf("rule %q: failed to send alerts: %w", rule, err))
|
||||
errGr.Add(fmt.Errorf("rule %q: failed to send alerts to addr %q: %w", rule, nt.Addr(), err))
|
||||
}
|
||||
}
|
||||
return errGr.Err()
|
||||
|
||||
@@ -8,9 +8,8 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/config"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/datasource"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/notifier"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/utils"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promutils"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -35,7 +34,7 @@ func TestUpdateWith(t *testing.T) {
|
||||
[]config.Rule{{
|
||||
Alert: "foo",
|
||||
Expr: "up > 0",
|
||||
For: utils.NewPromDuration(time.Second),
|
||||
For: promutils.NewDuration(time.Second),
|
||||
Labels: map[string]string{
|
||||
"bar": "baz",
|
||||
},
|
||||
@@ -47,7 +46,7 @@ func TestUpdateWith(t *testing.T) {
|
||||
[]config.Rule{{
|
||||
Alert: "foo",
|
||||
Expr: "up > 10",
|
||||
For: utils.NewPromDuration(time.Second),
|
||||
For: promutils.NewDuration(time.Second),
|
||||
Labels: map[string]string{
|
||||
"baz": "bar",
|
||||
},
|
||||
@@ -108,17 +107,6 @@ func TestUpdateWith(t *testing.T) {
|
||||
{Record: "foo5"},
|
||||
},
|
||||
},
|
||||
{
|
||||
"update datasource type",
|
||||
[]config.Rule{
|
||||
{Alert: "foo1", Type: datasource.NewPrometheusType()},
|
||||
{Alert: "foo3", Type: datasource.NewGraphiteType()},
|
||||
},
|
||||
[]config.Rule{
|
||||
{Alert: "foo1", Type: datasource.NewGraphiteType()},
|
||||
{Alert: "foo10", Type: datasource.NewPrometheusType()},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
@@ -170,10 +158,11 @@ func TestGroupStart(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("failed to parse rules: %s", err)
|
||||
}
|
||||
const evalInterval = time.Millisecond
|
||||
|
||||
fs := &fakeQuerier{}
|
||||
fn := &fakeNotifier{}
|
||||
|
||||
const evalInterval = time.Millisecond
|
||||
g := newGroup(groups[0], fs, evalInterval, map[string]string{"cluster": "east-1"})
|
||||
g.Concurrency = 2
|
||||
|
||||
@@ -192,7 +181,14 @@ func TestGroupStart(t *testing.T) {
|
||||
// add rule labels - see config/testdata/rules1-good.rules
|
||||
alert1.Labels["label"] = "bar"
|
||||
alert1.Labels["host"] = inst1
|
||||
alert1.ID = hash(m1)
|
||||
// add service labels
|
||||
alert1.Labels[alertNameLabel] = alert1.Name
|
||||
alert1.Labels[alertGroupNameLabel] = g.Name
|
||||
var labels1 []string
|
||||
for k, v := range alert1.Labels {
|
||||
labels1 = append(labels1, k, v)
|
||||
}
|
||||
alert1.ID = hash(metricWithLabels(t, labels1...))
|
||||
|
||||
alert2, err := r.newAlert(m2, time.Now(), nil)
|
||||
if err != nil {
|
||||
@@ -204,13 +200,20 @@ func TestGroupStart(t *testing.T) {
|
||||
// add rule labels - see config/testdata/rules1-good.rules
|
||||
alert2.Labels["label"] = "bar"
|
||||
alert2.Labels["host"] = inst2
|
||||
alert2.ID = hash(m2)
|
||||
// add service labels
|
||||
alert2.Labels[alertNameLabel] = alert2.Name
|
||||
alert2.Labels[alertGroupNameLabel] = g.Name
|
||||
var labels2 []string
|
||||
for k, v := range alert2.Labels {
|
||||
labels2 = append(labels2, k, v)
|
||||
}
|
||||
alert2.ID = hash(metricWithLabels(t, labels2...))
|
||||
|
||||
finished := make(chan struct{})
|
||||
fs.add(m1)
|
||||
fs.add(m2)
|
||||
go func() {
|
||||
g.start(context.Background(), []notifier.Notifier{fn}, nil)
|
||||
g.start(context.Background(), func() []notifier.Notifier { return []notifier.Notifier{fn} }, nil)
|
||||
close(finished)
|
||||
}()
|
||||
|
||||
@@ -221,6 +224,12 @@ func TestGroupStart(t *testing.T) {
|
||||
expectedAlerts := []notifier.Alert{*alert1, *alert2}
|
||||
compareAlerts(t, expectedAlerts, gotAlerts)
|
||||
|
||||
gotAlertsNum := fn.getCounter()
|
||||
if gotAlertsNum < len(expectedAlerts)*2 {
|
||||
t.Fatalf("expected to receive at least %d alerts; got %d instead",
|
||||
len(expectedAlerts)*2, gotAlertsNum)
|
||||
}
|
||||
|
||||
// reset previous data
|
||||
fs.reset()
|
||||
// and set only one datapoint for response
|
||||
@@ -241,18 +250,29 @@ func TestResolveDuration(t *testing.T) {
|
||||
testCases := []struct {
|
||||
groupInterval time.Duration
|
||||
maxDuration time.Duration
|
||||
resendDelay time.Duration
|
||||
expected time.Duration
|
||||
}{
|
||||
{time.Minute, 0, 3 * time.Minute},
|
||||
{3 * time.Minute, 0, 9 * time.Minute},
|
||||
{time.Minute, 2 * time.Minute, 2 * time.Minute},
|
||||
{0, 0, 0},
|
||||
{time.Minute, 0, 0, 4 * time.Minute},
|
||||
{time.Minute, 0, 2 * time.Minute, 8 * time.Minute},
|
||||
{time.Minute, 4 * time.Minute, 4 * time.Minute, 4 * time.Minute},
|
||||
{2 * time.Minute, time.Minute, 2 * time.Minute, time.Minute},
|
||||
{time.Minute, 2 * time.Minute, 1 * time.Minute, 2 * time.Minute},
|
||||
{2 * time.Minute, 0, 1 * time.Minute, 8 * time.Minute},
|
||||
{0, 0, 0, 0},
|
||||
}
|
||||
|
||||
defaultResolveDuration := *maxResolveDuration
|
||||
defer func() { *maxResolveDuration = defaultResolveDuration }()
|
||||
defaultResendDelay := *resendDelay
|
||||
defer func() {
|
||||
*maxResolveDuration = defaultResolveDuration
|
||||
*resendDelay = defaultResendDelay
|
||||
}()
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(fmt.Sprintf("%v-%v-%v", tc.groupInterval, tc.expected, tc.maxDuration), func(t *testing.T) {
|
||||
*maxResolveDuration = tc.maxDuration
|
||||
*resendDelay = tc.resendDelay
|
||||
got := getResolveDuration(tc.groupInterval)
|
||||
if got != tc.expected {
|
||||
t.Errorf("expected to have %v; got %v", tc.expected, got)
|
||||
|
||||
@@ -61,16 +61,26 @@ func (fq *fakeQuerier) Query(_ context.Context, _ string) ([]datasource.Metric,
|
||||
type fakeNotifier struct {
|
||||
sync.Mutex
|
||||
alerts []notifier.Alert
|
||||
// records number of received alerts in total
|
||||
counter int
|
||||
}
|
||||
|
||||
func (*fakeNotifier) Close() {}
|
||||
func (*fakeNotifier) Addr() string { return "" }
|
||||
func (fn *fakeNotifier) Send(_ context.Context, alerts []notifier.Alert) error {
|
||||
fn.Lock()
|
||||
defer fn.Unlock()
|
||||
fn.counter += len(alerts)
|
||||
fn.alerts = alerts
|
||||
return nil
|
||||
}
|
||||
|
||||
func (fn *fakeNotifier) getCounter() int {
|
||||
fn.Lock()
|
||||
defer fn.Unlock()
|
||||
return fn.counter
|
||||
}
|
||||
|
||||
func (fn *fakeNotifier) getAlerts() []notifier.Alert {
|
||||
fn.Lock()
|
||||
defer fn.Unlock()
|
||||
@@ -205,7 +215,8 @@ func compareTimeSeries(t *testing.T, a, b []prompbmarshal.TimeSeries) error {
|
||||
}*/
|
||||
}
|
||||
if len(expTS.Labels) != len(gotTS.Labels) {
|
||||
return fmt.Errorf("expected number of labels %d; got %d", len(expTS.Labels), len(gotTS.Labels))
|
||||
return fmt.Errorf("expected number of labels %d (%v); got %d (%v)",
|
||||
len(expTS.Labels), expTS.Labels, len(gotTS.Labels), gotTS.Labels)
|
||||
}
|
||||
for i, exp := range expTS.Labels {
|
||||
got := gotTS.Labels[i]
|
||||
|
||||
@@ -35,7 +35,10 @@ absolute path to all .yaml files in root.
|
||||
Rule files may contain %{ENV_VAR} placeholders, which are substituted by the corresponding env vars.`)
|
||||
|
||||
rulesCheckInterval = flag.Duration("rule.configCheckInterval", 0, "Interval for checking for changes in '-rule' files. "+
|
||||
"By default the checking is disabled. Send SIGHUP signal in order to force config check for changes")
|
||||
"By default the checking is disabled. Send SIGHUP signal in order to force config check for changes. DEPRECATED - see '-configCheckInterval' instead")
|
||||
|
||||
configCheckInterval = flag.Duration("configCheckInterval", 0, "Interval for checking for changes in '-rule' or '-notifier.config' files. "+
|
||||
"By default the checking is disabled. Send SIGHUP signal in order to force config check for changes.")
|
||||
|
||||
httpListenAddr = flag.String("httpListenAddr", ":8880", "Address to listen for http connections")
|
||||
evaluationInterval = flag.Duration("evaluationInterval", time.Minute, "How often to evaluate the rules")
|
||||
@@ -44,21 +47,25 @@ Rule files may contain %{ENV_VAR} placeholders, which are substituted by the cor
|
||||
validateExpressions = flag.Bool("rule.validateExpressions", true, "Whether to validate rules expressions via MetricsQL engine")
|
||||
maxResolveDuration = flag.Duration("rule.maxResolveDuration", 0, "Limits the maximum duration for automatic alert expiration, "+
|
||||
"which is by default equal to 3 evaluation intervals of the parent group.")
|
||||
resendDelay = flag.Duration("rule.resendDelay", 0, "Minimum amount of time to wait before resending an alert to notifier")
|
||||
|
||||
externalURL = flag.String("external.url", "", "External URL is used as alert's source for sent alerts to the notifier")
|
||||
externalAlertSource = flag.String("external.alert.source", "", `External Alert Source allows to override the Source link for alerts sent to AlertManager for cases where you want to build a custom link to Grafana, Prometheus or any other service.
|
||||
eg. 'explore?orgId=1&left=[\"now-1h\",\"now\",\"VictoriaMetrics\",{\"expr\": \"{{$expr|quotesEscape|crlfEscape|queryEscape}}\"},{\"mode\":\"Metrics\"},{\"ui\":[true,true,true,\"none\"]}]'.If empty '/api/v1/:groupID/alertID/status' is used`)
|
||||
externalLabels = flagutil.NewArray("external.label", "Optional label in the form 'name=value' to add to all generated recording rules and alerts. "+
|
||||
externalLabels = flagutil.NewArray("external.label", "Optional label in the form 'Name=value' to add to all generated recording rules and alerts. "+
|
||||
"Pass multiple -label flags in order to add multiple label sets.")
|
||||
|
||||
remoteReadLookBack = flag.Duration("remoteRead.lookback", time.Hour, "Lookback defines how far to look into past for alerts timeseries."+
|
||||
" For example, if lookback=1h then range from now() to now()-1h will be scanned.")
|
||||
remoteReadIgnoreRestoreErrors = flag.Bool("remoteRead.ignoreRestoreErrors", true, "Whether to ignore errors from remote storage when restoring alerts state on startup.")
|
||||
|
||||
disableAlertGroupLabel = flag.Bool("disableAlertgroupLabel", false, "Whether to disable adding group's name as label to generated alerts and time series.")
|
||||
disableAlertGroupLabel = flag.Bool("disableAlertgroupLabel", false, "Whether to disable adding group's Name as label to generated alerts and time series.")
|
||||
|
||||
dryRun = flag.Bool("dryRun", false, "Whether to check only config files without running vmalert. The rules file are validated. The `-rule` flag must be specified.")
|
||||
)
|
||||
|
||||
var alertURLGeneratorFn notifier.AlertURLGenerator
|
||||
|
||||
func main() {
|
||||
// Write flags and help message to stdout, since it is easier to grep or pipe.
|
||||
flag.CommandLine.SetOutput(os.Stdout)
|
||||
@@ -79,14 +86,24 @@ func main() {
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
eu, err := getExternalURL(*externalURL, *httpListenAddr, httpserver.IsTLS())
|
||||
if err != nil {
|
||||
logger.Fatalf("failed to init `external.url`: %s", err)
|
||||
}
|
||||
notifier.InitTemplateFunc(eu)
|
||||
alertURLGeneratorFn, err = getAlertURLGenerator(eu, *externalAlertSource, *validateTemplates)
|
||||
if err != nil {
|
||||
logger.Fatalf("failed to init `external.alert.source`: %s", err)
|
||||
}
|
||||
|
||||
if *replayFrom != "" || *replayTo != "" {
|
||||
rw, err := remotewrite.Init(context.Background())
|
||||
if err != nil {
|
||||
logger.Fatalf("failed to init remoteWrite: %s", err)
|
||||
}
|
||||
eu, err := getExternalURL(*externalURL, *httpListenAddr, httpserver.IsTLS())
|
||||
if err != nil {
|
||||
logger.Fatalf("failed to init `external.url`: %s", err)
|
||||
if rw == nil {
|
||||
logger.Fatalf("remoteWrite.url can't be empty in replay mode")
|
||||
}
|
||||
notifier.InitTemplateFunc(eu)
|
||||
groupsCfg, err := config.Parse(*rulePath, *validateTemplates, *validateExpressions)
|
||||
@@ -95,8 +112,7 @@ func main() {
|
||||
}
|
||||
// prevent queries from caching and boundaries aligning
|
||||
// when querying VictoriaMetrics datasource.
|
||||
noCache := datasource.Param{Key: "nocache", Value: "1"}
|
||||
q, err := datasource.Init([]datasource.Param{noCache})
|
||||
q, err := datasource.Init(url.Values{"nocache": {"1"}})
|
||||
if err != nil {
|
||||
logger.Fatalf("failed to init datasource: %s", err)
|
||||
}
|
||||
@@ -118,11 +134,16 @@ func main() {
|
||||
logger.Fatalf("cannot parse configuration file: %s", err)
|
||||
}
|
||||
|
||||
// Register SIGHUP handler for config re-read just before manager.start call.
|
||||
// This guarantees that the config will be re-read if the signal arrives during manager.start call.
|
||||
// See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1240
|
||||
sighupCh := procutil.NewSighupChan()
|
||||
|
||||
if err := manager.start(ctx, groupsCfg); err != nil {
|
||||
logger.Fatalf("failed to start: %s", err)
|
||||
}
|
||||
|
||||
go configReload(ctx, manager, groupsCfg)
|
||||
go configReload(ctx, manager, groupsCfg, sighupCh)
|
||||
|
||||
rh := &requestHandler{m: manager}
|
||||
go httpserver.Serve(*httpListenAddr, rh.handler)
|
||||
@@ -148,25 +169,28 @@ func newManager(ctx context.Context) (*manager, error) {
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to init datasource: %w", err)
|
||||
}
|
||||
eu, err := getExternalURL(*externalURL, *httpListenAddr, httpserver.IsTLS())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to init `external.url`: %w", err)
|
||||
|
||||
labels := make(map[string]string, 0)
|
||||
for _, s := range *externalLabels {
|
||||
if len(s) == 0 {
|
||||
continue
|
||||
}
|
||||
n := strings.IndexByte(s, '=')
|
||||
if n < 0 {
|
||||
return nil, fmt.Errorf("missing '=' in `-label`. It must contain label in the form `Name=value`; got %q", s)
|
||||
}
|
||||
labels[s[:n]] = s[n+1:]
|
||||
}
|
||||
notifier.InitTemplateFunc(eu)
|
||||
aug, err := getAlertURLGenerator(eu, *externalAlertSource, *validateTemplates)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to init `external.alert.source`: %w", err)
|
||||
}
|
||||
nts, err := notifier.Init(aug)
|
||||
|
||||
nts, err := notifier.Init(alertURLGeneratorFn, labels, *externalURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to init notifier: %w", err)
|
||||
}
|
||||
|
||||
manager := &manager{
|
||||
groups: make(map[uint64]*Group),
|
||||
querierBuilder: q,
|
||||
notifiers: nts,
|
||||
labels: map[string]string{},
|
||||
labels: labels,
|
||||
}
|
||||
rw, err := remotewrite.Init(ctx)
|
||||
if err != nil {
|
||||
@@ -180,16 +204,6 @@ func newManager(ctx context.Context) (*manager, error) {
|
||||
}
|
||||
manager.rr = rr
|
||||
|
||||
for _, s := range *externalLabels {
|
||||
if len(s) == 0 {
|
||||
continue
|
||||
}
|
||||
n := strings.IndexByte(s, '=')
|
||||
if n < 0 {
|
||||
return nil, fmt.Errorf("missing '=' in `-label`. It must contain label in the form `name=value`; got %q", s)
|
||||
}
|
||||
manager.labels[s[:n]] = s[n+1:]
|
||||
}
|
||||
return manager, nil
|
||||
}
|
||||
|
||||
@@ -246,15 +260,15 @@ See the docs at https://docs.victoriametrics.com/vmalert.html .
|
||||
flagutil.Usage(s)
|
||||
}
|
||||
|
||||
func configReload(ctx context.Context, m *manager, groupsCfg []config.Group) {
|
||||
// Register SIGHUP handler for config re-read just before manager.start call.
|
||||
// This guarantees that the config will be re-read if the signal arrives during manager.start call.
|
||||
// See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1240
|
||||
sighupCh := procutil.NewSighupChan()
|
||||
|
||||
func configReload(ctx context.Context, m *manager, groupsCfg []config.Group, sighupCh <-chan os.Signal) {
|
||||
var configCheckCh <-chan time.Time
|
||||
if *rulesCheckInterval > 0 {
|
||||
ticker := time.NewTicker(*rulesCheckInterval)
|
||||
checkInterval := *configCheckInterval
|
||||
if checkInterval == 0 && *rulesCheckInterval > 0 {
|
||||
logger.Warnf("flag `rule.configCheckInterval` is deprecated - use `configCheckInterval` instead")
|
||||
checkInterval = *rulesCheckInterval
|
||||
}
|
||||
if checkInterval > 0 {
|
||||
ticker := time.NewTicker(checkInterval)
|
||||
configCheckCh = ticker.C
|
||||
defer ticker.Stop()
|
||||
}
|
||||
@@ -271,6 +285,12 @@ func configReload(ctx context.Context, m *manager, groupsCfg []config.Group) {
|
||||
configReloads.Inc()
|
||||
case <-configCheckCh:
|
||||
}
|
||||
if err := notifier.Reload(); err != nil {
|
||||
configReloadErrors.Inc()
|
||||
configSuccess.Set(0)
|
||||
logger.Errorf("failed to reload notifier config: %s", err)
|
||||
continue
|
||||
}
|
||||
newGroupsCfg, err := config.Parse(*rulePath, *validateTemplates, *validateExpressions)
|
||||
if err != nil {
|
||||
configReloadErrors.Inc()
|
||||
@@ -285,13 +305,13 @@ func configReload(ctx context.Context, m *manager, groupsCfg []config.Group) {
|
||||
// config didn't change - skip it
|
||||
continue
|
||||
}
|
||||
groupsCfg = newGroupsCfg
|
||||
if err := m.update(ctx, groupsCfg, false); err != nil {
|
||||
if err := m.update(ctx, newGroupsCfg, false); err != nil {
|
||||
configReloadErrors.Inc()
|
||||
configSuccess.Set(0)
|
||||
logger.Errorf("error while reloading rules: %s", err)
|
||||
continue
|
||||
}
|
||||
groupsCfg = newGroupsCfg
|
||||
configSuccess.Set(1)
|
||||
configTimestamp.Set(fasttime.UnixTimestamp())
|
||||
logger.Infof("Rules reloaded successfully from %q", *rulePath)
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/notifier"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/remotewrite"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/procutil"
|
||||
)
|
||||
|
||||
@@ -99,11 +100,14 @@ groups:
|
||||
querierBuilder: &fakeQuerier{},
|
||||
groups: make(map[uint64]*Group),
|
||||
labels: map[string]string{},
|
||||
notifiers: func() []notifier.Notifier { return []notifier.Notifier{&fakeNotifier{}} },
|
||||
rw: &remotewrite.Client{},
|
||||
}
|
||||
|
||||
syncCh := make(chan struct{})
|
||||
sighupCh := procutil.NewSighupChan()
|
||||
go func() {
|
||||
configReload(ctx, m, nil)
|
||||
configReload(ctx, m, nil, sighupCh)
|
||||
close(syncCh)
|
||||
}()
|
||||
|
||||
|
||||
@@ -3,6 +3,8 @@ package main
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"sort"
|
||||
"sync"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/config"
|
||||
@@ -15,7 +17,7 @@ import (
|
||||
// manager controls group states
|
||||
type manager struct {
|
||||
querierBuilder datasource.QuerierBuilder
|
||||
notifiers []notifier.Notifier
|
||||
notifiers func() []notifier.Notifier
|
||||
|
||||
rw *remotewrite.Client
|
||||
// remote read builder.
|
||||
@@ -85,12 +87,31 @@ func (m *manager) startGroup(ctx context.Context, group *Group, restore bool) er
|
||||
}
|
||||
|
||||
func (m *manager) update(ctx context.Context, groupsCfg []config.Group, restore bool) error {
|
||||
var rrPresent, arPresent bool
|
||||
groupsRegistry := make(map[uint64]*Group)
|
||||
for _, cfg := range groupsCfg {
|
||||
for _, r := range cfg.Rules {
|
||||
if rrPresent && arPresent {
|
||||
continue
|
||||
}
|
||||
if r.Record != "" {
|
||||
rrPresent = true
|
||||
}
|
||||
if r.Alert != "" {
|
||||
arPresent = true
|
||||
}
|
||||
}
|
||||
ng := newGroup(cfg, m.querierBuilder, *evaluationInterval, m.labels)
|
||||
groupsRegistry[ng.ID()] = ng
|
||||
}
|
||||
|
||||
if rrPresent && m.rw == nil {
|
||||
return fmt.Errorf("config contains recording rules but `-remoteWrite.url` isn't set")
|
||||
}
|
||||
if arPresent && m.notifiers == nil {
|
||||
return fmt.Errorf("config contains alerting rules but neither `-notifier.url` nor `-notifier.config` aren't set")
|
||||
}
|
||||
|
||||
type updateItem struct {
|
||||
old *Group
|
||||
new *Group
|
||||
@@ -142,21 +163,38 @@ func (g *Group) toAPI() APIGroup {
|
||||
// encode as string to avoid rounding
|
||||
ID: fmt.Sprintf("%d", g.ID()),
|
||||
|
||||
Name: g.Name,
|
||||
Type: g.Type.String(),
|
||||
File: g.File,
|
||||
Interval: g.Interval.String(),
|
||||
Concurrency: g.Concurrency,
|
||||
ExtraFilterLabels: g.ExtraFilterLabels,
|
||||
Labels: g.Labels,
|
||||
Name: g.Name,
|
||||
Type: g.Type.String(),
|
||||
File: g.File,
|
||||
Interval: g.Interval.Seconds(),
|
||||
LastEvaluation: g.LastEvaluation,
|
||||
Concurrency: g.Concurrency,
|
||||
Params: urlValuesToStrings(g.Params),
|
||||
Labels: g.Labels,
|
||||
}
|
||||
for _, r := range g.Rules {
|
||||
switch v := r.(type) {
|
||||
case *AlertingRule:
|
||||
ag.AlertingRules = append(ag.AlertingRules, v.RuleAPI())
|
||||
case *RecordingRule:
|
||||
ag.RecordingRules = append(ag.RecordingRules, v.RuleAPI())
|
||||
}
|
||||
ag.Rules = append(ag.Rules, r.ToAPI())
|
||||
}
|
||||
return ag
|
||||
}
|
||||
|
||||
func urlValuesToStrings(values url.Values) []string {
|
||||
if len(values) < 1 {
|
||||
return nil
|
||||
}
|
||||
|
||||
keys := make([]string, 0, len(values))
|
||||
for k := range values {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
var res []string
|
||||
for _, k := range keys {
|
||||
params := values[k]
|
||||
for _, v := range params {
|
||||
res = append(res, fmt.Sprintf("%s=%s", k, v))
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"math/rand"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -12,6 +13,7 @@ import (
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/config"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/datasource"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/notifier"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/remotewrite"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
@@ -38,7 +40,7 @@ func TestManagerUpdateConcurrent(t *testing.T) {
|
||||
m := &manager{
|
||||
groups: make(map[uint64]*Group),
|
||||
querierBuilder: &fakeQuerier{},
|
||||
notifiers: []notifier.Notifier{&fakeNotifier{}},
|
||||
notifiers: func() []notifier.Notifier { return []notifier.Notifier{&fakeNotifier{}} },
|
||||
}
|
||||
paths := []string{
|
||||
"config/testdata/dir/rules0-good.rules",
|
||||
@@ -113,18 +115,6 @@ func TestManagerUpdate(t *testing.T) {
|
||||
Name: "ExampleAlertAlwaysFiring",
|
||||
Expr: "sum by(job) (up == 1)",
|
||||
}
|
||||
ExampleAlertGraphite = &AlertingRule{
|
||||
Name: "up graphite",
|
||||
Expr: "filterSeries(time('host.1',20),'>','0')",
|
||||
Type: datasource.NewGraphiteType(),
|
||||
For: defaultEvalInterval,
|
||||
}
|
||||
ExampleAlertGraphite2 = &AlertingRule{
|
||||
Name: "up",
|
||||
Expr: "filterSeries(time('host.2',20),'>','0')",
|
||||
Type: datasource.NewGraphiteType(),
|
||||
For: defaultEvalInterval,
|
||||
}
|
||||
)
|
||||
|
||||
testCases := []struct {
|
||||
@@ -226,28 +216,15 @@ func TestManagerUpdate(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "update prometheus to graphite type",
|
||||
initPath: "config/testdata/dir/rules-update0-good.rules",
|
||||
updatePath: "config/testdata/dir/rules-update1-good.rules",
|
||||
want: []*Group{
|
||||
{
|
||||
File: "config/testdata/dir/rules-update1-good.rules",
|
||||
Interval: defaultEvalInterval,
|
||||
Type: datasource.NewGraphiteType(),
|
||||
Name: "TestUpdateGroup",
|
||||
Rules: []Rule{
|
||||
ExampleAlertGraphite2,
|
||||
ExampleAlertGraphite,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.TODO())
|
||||
m := &manager{groups: make(map[uint64]*Group), querierBuilder: &fakeQuerier{}}
|
||||
m := &manager{
|
||||
groups: make(map[uint64]*Group),
|
||||
querierBuilder: &fakeQuerier{},
|
||||
notifiers: func() []notifier.Notifier { return []notifier.Notifier{&fakeNotifier{}} },
|
||||
}
|
||||
|
||||
cfgInit := loadCfg(t, []string{tc.initPath}, true, true)
|
||||
if err := m.update(ctx, cfgInit, false); err != nil {
|
||||
@@ -276,6 +253,80 @@ func TestManagerUpdate(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestManagerUpdateNegative(t *testing.T) {
|
||||
testCases := []struct {
|
||||
notifiers []notifier.Notifier
|
||||
rw *remotewrite.Client
|
||||
cfg config.Group
|
||||
expErr string
|
||||
}{
|
||||
{
|
||||
nil,
|
||||
nil,
|
||||
config.Group{Name: "Recording rule only",
|
||||
Rules: []config.Rule{
|
||||
{Record: "record", Expr: "max(up)"},
|
||||
},
|
||||
},
|
||||
"contains recording rules",
|
||||
},
|
||||
{
|
||||
nil,
|
||||
nil,
|
||||
config.Group{Name: "Alerting rule only",
|
||||
Rules: []config.Rule{
|
||||
{Alert: "alert", Expr: "up > 0"},
|
||||
},
|
||||
},
|
||||
"contains alerting rules",
|
||||
},
|
||||
{
|
||||
[]notifier.Notifier{&fakeNotifier{}},
|
||||
nil,
|
||||
config.Group{Name: "Recording and alerting rules",
|
||||
Rules: []config.Rule{
|
||||
{Alert: "alert1", Expr: "up > 0"},
|
||||
{Alert: "alert2", Expr: "up > 0"},
|
||||
{Record: "record", Expr: "max(up)"},
|
||||
},
|
||||
},
|
||||
"contains recording rules",
|
||||
},
|
||||
{
|
||||
nil,
|
||||
&remotewrite.Client{},
|
||||
config.Group{Name: "Recording and alerting rules",
|
||||
Rules: []config.Rule{
|
||||
{Record: "record1", Expr: "max(up)"},
|
||||
{Record: "record2", Expr: "max(up)"},
|
||||
{Alert: "alert", Expr: "up > 0"},
|
||||
},
|
||||
},
|
||||
"contains alerting rules",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.cfg.Name, func(t *testing.T) {
|
||||
m := &manager{
|
||||
groups: make(map[uint64]*Group),
|
||||
querierBuilder: &fakeQuerier{},
|
||||
rw: tc.rw,
|
||||
}
|
||||
if tc.notifiers != nil {
|
||||
m.notifiers = func() []notifier.Notifier { return tc.notifiers }
|
||||
}
|
||||
err := m.update(context.Background(), []config.Group{tc.cfg}, false)
|
||||
if err == nil {
|
||||
t.Fatalf("expected to get error; got nil")
|
||||
}
|
||||
if !strings.Contains(err.Error(), tc.expErr) {
|
||||
t.Fatalf("expected err to contain %q; got %q", tc.expErr, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func loadCfg(t *testing.T, path []string, validateAnnotations, validateExpressions bool) []config.Group {
|
||||
t.Helper()
|
||||
cfg, err := config.Parse(path, validateAnnotations, validateExpressions)
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
package main
|
||||
|
||||
import "github.com/VictoriaMetrics/metrics"
|
||||
|
||||
type gauge struct {
|
||||
name string
|
||||
*metrics.Gauge
|
||||
}
|
||||
|
||||
func getOrCreateGauge(name string, f func() float64) *gauge {
|
||||
return &gauge{
|
||||
name: name,
|
||||
Gauge: metrics.GetOrCreateGauge(name, f),
|
||||
}
|
||||
}
|
||||
|
||||
type counter struct {
|
||||
name string
|
||||
*metrics.Counter
|
||||
}
|
||||
|
||||
func getOrCreateCounter(name string) *counter {
|
||||
return &counter{
|
||||
name: name,
|
||||
Counter: metrics.GetOrCreateCounter(name),
|
||||
}
|
||||
}
|
||||
|
||||
type summary struct {
|
||||
name string
|
||||
*metrics.Summary
|
||||
}
|
||||
|
||||
func getOrCreateSummary(name string) *summary {
|
||||
return &summary{
|
||||
name: name,
|
||||
Summary: metrics.GetOrCreateSummary(name),
|
||||
}
|
||||
}
|
||||
@@ -30,10 +30,14 @@ type Alert struct {
|
||||
Start time.Time
|
||||
// End defines the moment of time when Alert supposed to expire
|
||||
End time.Time
|
||||
// LastSent defines the moment when Alert was sent last time
|
||||
LastSent time.Time
|
||||
// Value stores the value returned from evaluating expression from Expr field
|
||||
Value float64
|
||||
// ID is the unique identifer for the Alert
|
||||
ID uint64
|
||||
// Restored is true if Alert was restored after restart
|
||||
Restored bool
|
||||
}
|
||||
|
||||
// AlertState type indicates the Alert state
|
||||
@@ -68,7 +72,13 @@ type AlertTplData struct {
|
||||
Expr string
|
||||
}
|
||||
|
||||
const tplHeader = `{{ $value := .Value }}{{ $labels := .Labels }}{{ $expr := .Expr }}`
|
||||
var tplHeaders = []string{
|
||||
"{{ $value := .Value }}",
|
||||
"{{ $labels := .Labels }}",
|
||||
"{{ $expr := .Expr }}",
|
||||
"{{ $externalLabels := .ExternalLabels }}",
|
||||
"{{ $externalURL := .ExternalURL }}",
|
||||
}
|
||||
|
||||
// ExecTemplate executes the Alert template for given
|
||||
// map of annotations.
|
||||
@@ -98,13 +108,15 @@ func templateAnnotations(annotations map[string]string, data AlertTplData, funcs
|
||||
var buf bytes.Buffer
|
||||
eg := new(utils.ErrGroup)
|
||||
r := make(map[string]string, len(annotations))
|
||||
tData := tplData{data, externalLabels, externalURL}
|
||||
header := strings.Join(tplHeaders, "")
|
||||
for key, text := range annotations {
|
||||
buf.Reset()
|
||||
builder.Reset()
|
||||
builder.Grow(len(tplHeader) + len(text))
|
||||
builder.WriteString(tplHeader)
|
||||
builder.Grow(len(header) + len(text))
|
||||
builder.WriteString(header)
|
||||
builder.WriteString(text)
|
||||
if err := templateAnnotation(&buf, builder.String(), data, funcs); err != nil {
|
||||
if err := templateAnnotation(&buf, builder.String(), tData, funcs); err != nil {
|
||||
r[key] = text
|
||||
eg.Add(fmt.Errorf("key %q, template %q: %w", key, text, err))
|
||||
continue
|
||||
@@ -114,7 +126,13 @@ func templateAnnotations(annotations map[string]string, data AlertTplData, funcs
|
||||
return r, eg.Err()
|
||||
}
|
||||
|
||||
func templateAnnotation(dst io.Writer, text string, data AlertTplData, funcs template.FuncMap) error {
|
||||
type tplData struct {
|
||||
AlertTplData
|
||||
ExternalLabels map[string]string
|
||||
ExternalURL string
|
||||
}
|
||||
|
||||
func templateAnnotation(dst io.Writer, text string, data tplData, funcs template.FuncMap) error {
|
||||
t := template.New("").Funcs(funcs).Option("missingkey=zero")
|
||||
tpl, err := t.Parse(text)
|
||||
if err != nil {
|
||||
|
||||
@@ -1,12 +1,24 @@
|
||||
package notifier
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/datasource"
|
||||
)
|
||||
|
||||
func TestAlert_ExecTemplate(t *testing.T) {
|
||||
extLabels := make(map[string]string, 0)
|
||||
const (
|
||||
extCluster = "prod"
|
||||
extDC = "east"
|
||||
extURL = "https://foo.bar"
|
||||
)
|
||||
extLabels["cluster"] = extCluster
|
||||
extLabels["dc"] = extDC
|
||||
_, err := Init(nil, extLabels, extURL)
|
||||
checkErr(t, err)
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
alert *Alert
|
||||
@@ -74,6 +86,26 @@ func TestAlert_ExecTemplate(t *testing.T) {
|
||||
"desc": "bar 1;garply 2;",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "external",
|
||||
alert: &Alert{
|
||||
Value: 1e4,
|
||||
Labels: map[string]string{
|
||||
"job": "staging",
|
||||
"instance": "localhost",
|
||||
},
|
||||
},
|
||||
annotations: map[string]string{
|
||||
"url": "{{ $externalURL }}",
|
||||
"summary": "Issues with {{$labels.instance}} (dc-{{$externalLabels.dc}}) for job {{$labels.job}}",
|
||||
"description": "It is {{ $value }} connections for {{$labels.instance}} (cluster-{{$externalLabels.cluster}})",
|
||||
},
|
||||
expTpl: map[string]string{
|
||||
"url": extURL,
|
||||
"summary": fmt.Sprintf("Issues with localhost (dc-%s) for job staging", extDC),
|
||||
"description": fmt.Sprintf("It is 10000 connections for localhost (cluster-%s)", extCluster),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
qFn := func(q string) ([]datasource.Metric, error) {
|
||||
|
||||
@@ -7,17 +7,41 @@ import (
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/utils"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promauth"
|
||||
)
|
||||
|
||||
// AlertManager represents integration provider with Prometheus alert manager
|
||||
// https://github.com/prometheus/alertmanager
|
||||
type AlertManager struct {
|
||||
addr string
|
||||
alertURL string
|
||||
basicAuthUser string
|
||||
basicAuthPass string
|
||||
argFunc AlertURLGenerator
|
||||
client *http.Client
|
||||
addr string
|
||||
argFunc AlertURLGenerator
|
||||
client *http.Client
|
||||
timeout time.Duration
|
||||
|
||||
authCfg *promauth.Config
|
||||
|
||||
metrics *metrics
|
||||
}
|
||||
|
||||
type metrics struct {
|
||||
alertsSent *utils.Counter
|
||||
alertsSendErrors *utils.Counter
|
||||
}
|
||||
|
||||
func newMetrics(addr string) *metrics {
|
||||
return &metrics{
|
||||
alertsSent: utils.GetOrCreateCounter(fmt.Sprintf("vmalert_alerts_sent_total{addr=%q}", addr)),
|
||||
alertsSendErrors: utils.GetOrCreateCounter(fmt.Sprintf("vmalert_alerts_send_errors_total{addr=%q}", addr)),
|
||||
}
|
||||
}
|
||||
|
||||
// Close is a destructor method for AlertManager
|
||||
func (am *AlertManager) Close() {
|
||||
am.metrics.alertsSent.Unregister()
|
||||
am.metrics.alertsSendErrors.Unregister()
|
||||
}
|
||||
|
||||
// Addr returns address where alerts are sent.
|
||||
@@ -25,17 +49,36 @@ func (am AlertManager) Addr() string { return am.addr }
|
||||
|
||||
// Send an alert or resolve message
|
||||
func (am *AlertManager) Send(ctx context.Context, alerts []Alert) error {
|
||||
am.metrics.alertsSent.Add(len(alerts))
|
||||
err := am.send(ctx, alerts)
|
||||
if err != nil {
|
||||
am.metrics.alertsSendErrors.Add(len(alerts))
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (am *AlertManager) send(ctx context.Context, alerts []Alert) error {
|
||||
b := &bytes.Buffer{}
|
||||
writeamRequest(b, alerts, am.argFunc)
|
||||
|
||||
req, err := http.NewRequest("POST", am.alertURL, b)
|
||||
req, err := http.NewRequest("POST", am.addr, b)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json; charset=utf-8")
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
if am.timeout > 0 {
|
||||
var cancel context.CancelFunc
|
||||
ctx, cancel = context.WithTimeout(ctx, am.timeout)
|
||||
defer cancel()
|
||||
}
|
||||
|
||||
req = req.WithContext(ctx)
|
||||
if am.basicAuthPass != "" {
|
||||
req.SetBasicAuth(am.basicAuthUser, am.basicAuthPass)
|
||||
|
||||
if am.authCfg != nil {
|
||||
if auth := am.authCfg.GetAuthHeader(); auth != "" {
|
||||
req.Header.Set("Authorization", auth)
|
||||
}
|
||||
}
|
||||
resp, err := am.client.Do(req)
|
||||
if err != nil {
|
||||
@@ -47,9 +90,9 @@ func (am *AlertManager) Send(ctx context.Context, alerts []Alert) error {
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read response from %q: %w", am.alertURL, err)
|
||||
return fmt.Errorf("failed to read response from %q: %w", am.addr, err)
|
||||
}
|
||||
return fmt.Errorf("invalid SC %d from %q; response body: %s", resp.StatusCode, am.alertURL, string(body))
|
||||
return fmt.Errorf("invalid SC %d from %q; response body: %s", resp.StatusCode, am.addr, string(body))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -60,14 +103,39 @@ type AlertURLGenerator func(Alert) string
|
||||
const alertManagerPath = "/api/v2/alerts"
|
||||
|
||||
// NewAlertManager is a constructor for AlertManager
|
||||
func NewAlertManager(alertManagerURL, user, pass string, fn AlertURLGenerator, c *http.Client) *AlertManager {
|
||||
url := strings.TrimSuffix(alertManagerURL, "/") + alertManagerPath
|
||||
return &AlertManager{
|
||||
addr: alertManagerURL,
|
||||
alertURL: url,
|
||||
argFunc: fn,
|
||||
client: c,
|
||||
basicAuthUser: user,
|
||||
basicAuthPass: pass,
|
||||
func NewAlertManager(alertManagerURL string, fn AlertURLGenerator, authCfg promauth.HTTPClientConfig, timeout time.Duration) (*AlertManager, error) {
|
||||
tls := &promauth.TLSConfig{}
|
||||
if authCfg.TLSConfig != nil {
|
||||
tls = authCfg.TLSConfig
|
||||
}
|
||||
tr, err := utils.Transport(alertManagerURL, tls.CertFile, tls.KeyFile, tls.CAFile, tls.ServerName, tls.InsecureSkipVerify)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create transport: %w", err)
|
||||
}
|
||||
|
||||
ba := new(promauth.BasicAuthConfig)
|
||||
oauth := new(promauth.OAuth2Config)
|
||||
if authCfg.BasicAuth != nil {
|
||||
ba = authCfg.BasicAuth
|
||||
}
|
||||
if authCfg.OAuth2 != nil {
|
||||
oauth = authCfg.OAuth2
|
||||
}
|
||||
|
||||
aCfg, err := utils.AuthConfig(
|
||||
utils.WithBasicAuth(ba.Username, ba.Password.String(), ba.PasswordFile),
|
||||
utils.WithBearer(authCfg.BearerToken.String(), authCfg.BearerTokenFile),
|
||||
utils.WithOAuth(oauth.ClientID, oauth.ClientSecretFile, oauth.ClientSecretFile, oauth.TokenURL, strings.Join(oauth.Scopes, ";")))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to configure auth: %w", err)
|
||||
}
|
||||
|
||||
return &AlertManager{
|
||||
addr: alertManagerURL,
|
||||
argFunc: fn,
|
||||
authCfg: aCfg,
|
||||
client: &http.Client{Transport: tr},
|
||||
timeout: timeout,
|
||||
metrics: newMetrics(alertManagerURL),
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -8,11 +8,16 @@ import (
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promauth"
|
||||
)
|
||||
|
||||
func TestAlertManager_Addr(t *testing.T) {
|
||||
const addr = "http://localhost"
|
||||
am := NewAlertManager(addr, "", "", nil, nil)
|
||||
am, err := NewAlertManager(addr, nil, promauth.HTTPClientConfig{}, 0)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %s", err)
|
||||
}
|
||||
if am.Addr() != addr {
|
||||
t.Errorf("expected to have %q; got %q", addr, am.Addr())
|
||||
}
|
||||
@@ -75,9 +80,19 @@ func TestAlertManager_Send(t *testing.T) {
|
||||
})
|
||||
srv := httptest.NewServer(mux)
|
||||
defer srv.Close()
|
||||
am := NewAlertManager(srv.URL, baUser, baPass, func(alert Alert) string {
|
||||
|
||||
aCfg := promauth.HTTPClientConfig{
|
||||
BasicAuth: &promauth.BasicAuthConfig{
|
||||
Username: baUser,
|
||||
Password: promauth.NewSecret(baPass),
|
||||
},
|
||||
}
|
||||
am, err := NewAlertManager(srv.URL+alertManagerPath, func(alert Alert) string {
|
||||
return strconv.FormatUint(alert.GroupID, 10) + "/" + strconv.FormatUint(alert.ID, 10)
|
||||
}, srv.Client())
|
||||
}, aCfg, 0)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %s", err)
|
||||
}
|
||||
if err := am.Send(context.Background(), []Alert{{}, {}}); err == nil {
|
||||
t.Error("expected connection error got nil")
|
||||
}
|
||||
|
||||
182
app/vmalert/notifier/config.go
Normal file
@@ -0,0 +1,182 @@
|
||||
package notifier
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"fmt"
|
||||
"gopkg.in/yaml.v2"
|
||||
"io/ioutil"
|
||||
"net/url"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promauth"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/prompbmarshal"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promrelabel"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promscrape/discovery/consul"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promutils"
|
||||
)
|
||||
|
||||
// Config contains list of supported configuration settings
|
||||
// for Notifier
|
||||
type Config struct {
|
||||
// Scheme defines the HTTP scheme for Notifier address
|
||||
Scheme string `yaml:"scheme,omitempty"`
|
||||
// PathPrefix is added to URL path before adding alertManagerPath value
|
||||
PathPrefix string `yaml:"path_prefix,omitempty"`
|
||||
|
||||
// ConsulSDConfigs contains list of settings for service discovery via Consul
|
||||
// see https://prometheus.io/docs/prometheus/latest/configuration/configuration/#consul_sd_config
|
||||
ConsulSDConfigs []consul.SDConfig `yaml:"consul_sd_configs,omitempty"`
|
||||
// StaticConfigs contains list of static targets
|
||||
StaticConfigs []StaticConfig `yaml:"static_configs,omitempty"`
|
||||
|
||||
// HTTPClientConfig contains HTTP configuration for Notifier clients
|
||||
HTTPClientConfig promauth.HTTPClientConfig `yaml:",inline"`
|
||||
// RelabelConfigs contains list of relabeling rules
|
||||
RelabelConfigs []promrelabel.RelabelConfig `yaml:"relabel_configs,omitempty"`
|
||||
|
||||
// The timeout used when sending alerts.
|
||||
Timeout promutils.Duration `yaml:"timeout,omitempty"`
|
||||
|
||||
// Checksum stores the hash of yaml definition for the config.
|
||||
// May be used to detect any changes to the config file.
|
||||
Checksum string
|
||||
|
||||
// Catches all undefined fields and must be empty after parsing.
|
||||
XXX map[string]interface{} `yaml:",inline"`
|
||||
|
||||
// This is set to the directory from where the config has been loaded.
|
||||
baseDir string
|
||||
|
||||
// stores already parsed RelabelConfigs object
|
||||
parsedRelabelConfigs *promrelabel.ParsedConfigs
|
||||
}
|
||||
|
||||
// StaticConfig contains list of static targets in the following form:
|
||||
// targets:
|
||||
// [ - '<host>' ]
|
||||
type StaticConfig struct {
|
||||
Targets []string `yaml:"targets"`
|
||||
}
|
||||
|
||||
// UnmarshalYAML implements the yaml.Unmarshaler interface.
|
||||
func (cfg *Config) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
type config Config
|
||||
if err := unmarshal((*config)(cfg)); err != nil {
|
||||
return err
|
||||
}
|
||||
if cfg.Scheme == "" {
|
||||
cfg.Scheme = "http"
|
||||
}
|
||||
if cfg.Timeout.Duration() == 0 {
|
||||
cfg.Timeout = promutils.NewDuration(time.Second * 10)
|
||||
}
|
||||
rCfg, err := promrelabel.ParseRelabelConfigs(cfg.RelabelConfigs, false)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse relabeling config: %w", err)
|
||||
}
|
||||
cfg.parsedRelabelConfigs = rCfg
|
||||
|
||||
b, err := yaml.Marshal(cfg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal configuration for checksum: %w", err)
|
||||
}
|
||||
h := md5.New()
|
||||
h.Write(b)
|
||||
cfg.Checksum = fmt.Sprintf("%x", h.Sum(nil))
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseConfig(path string) (*Config, error) {
|
||||
data, err := ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error reading config file: %w", err)
|
||||
}
|
||||
var cfg *Config
|
||||
err = yaml.Unmarshal(data, &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(cfg.XXX) > 0 {
|
||||
var keys []string
|
||||
for k := range cfg.XXX {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
return nil, fmt.Errorf("unknown fields in %s", strings.Join(keys, ", "))
|
||||
}
|
||||
absPath, err := filepath.Abs(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot obtain abs path for %q: %w", path, err)
|
||||
}
|
||||
cfg.baseDir = filepath.Dir(absPath)
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func parseLabels(target string, metaLabels map[string]string, cfg *Config) (string, []prompbmarshal.Label, error) {
|
||||
labels := mergeLabels(target, metaLabels, cfg)
|
||||
labels = cfg.parsedRelabelConfigs.Apply(labels, 0, false)
|
||||
labels = promrelabel.RemoveMetaLabels(labels[:0], labels)
|
||||
// Remove references to already deleted labels, so GC could clean strings for label name and label value past len(labels).
|
||||
// This should reduce memory usage when relabeling creates big number of temporary labels with long names and/or values.
|
||||
// See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/825 for details.
|
||||
labels = append([]prompbmarshal.Label{}, labels...)
|
||||
|
||||
if len(labels) == 0 {
|
||||
return "", nil, nil
|
||||
}
|
||||
schemeRelabeled := promrelabel.GetLabelValueByName(labels, "__scheme__")
|
||||
if len(schemeRelabeled) == 0 {
|
||||
schemeRelabeled = "http"
|
||||
}
|
||||
addressRelabeled := promrelabel.GetLabelValueByName(labels, "__address__")
|
||||
if len(addressRelabeled) == 0 {
|
||||
return "", nil, nil
|
||||
}
|
||||
if strings.Contains(addressRelabeled, "/") {
|
||||
return "", nil, nil
|
||||
}
|
||||
addressRelabeled = addMissingPort(schemeRelabeled, addressRelabeled)
|
||||
alertsPathRelabeled := promrelabel.GetLabelValueByName(labels, "__alerts_path__")
|
||||
if !strings.HasPrefix(alertsPathRelabeled, "/") {
|
||||
alertsPathRelabeled = "/" + alertsPathRelabeled
|
||||
}
|
||||
u := fmt.Sprintf("%s://%s%s", schemeRelabeled, addressRelabeled, alertsPathRelabeled)
|
||||
if _, err := url.Parse(u); err != nil {
|
||||
return "", nil, fmt.Errorf("invalid url %q for scheme=%q (%q), target=%q, metrics_path=%q (%q): %w",
|
||||
u, cfg.Scheme, schemeRelabeled, target, addressRelabeled, alertsPathRelabeled, err)
|
||||
}
|
||||
return u, labels, nil
|
||||
}
|
||||
|
||||
func addMissingPort(scheme, target string) string {
|
||||
if strings.Contains(target, ":") {
|
||||
return target
|
||||
}
|
||||
if scheme == "https" {
|
||||
target += ":443"
|
||||
} else {
|
||||
target += ":80"
|
||||
}
|
||||
return target
|
||||
}
|
||||
|
||||
func mergeLabels(target string, metaLabels map[string]string, cfg *Config) []prompbmarshal.Label {
|
||||
// See https://prometheus.io/docs/prometheus/latest/configuration/configuration/#relabel_config
|
||||
m := make(map[string]string)
|
||||
m["__address__"] = target
|
||||
m["__scheme__"] = cfg.Scheme
|
||||
m["__alerts_path__"] = path.Join("/", cfg.PathPrefix, alertManagerPath)
|
||||
for k, v := range metaLabels {
|
||||
m[k] = v
|
||||
}
|
||||
result := make([]prompbmarshal.Label, 0, len(m))
|
||||
for k, v := range m {
|
||||
result = append(result, prompbmarshal.Label{
|
||||
Name: k,
|
||||
Value: v,
|
||||
})
|
||||
}
|
||||
return result
|
||||
}
|
||||
31
app/vmalert/notifier/config_test.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package notifier
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestConfigParseGood(t *testing.T) {
|
||||
f := func(path string) {
|
||||
_, err := parseConfig(path)
|
||||
checkErr(t, err)
|
||||
}
|
||||
f("testdata/mixed.good.yaml")
|
||||
f("testdata/consul.good.yaml")
|
||||
f("testdata/static.good.yaml")
|
||||
}
|
||||
|
||||
func TestConfigParseBad(t *testing.T) {
|
||||
f := func(path, expErr string) {
|
||||
_, err := parseConfig(path)
|
||||
if err == nil {
|
||||
t.Fatalf("expected to get non-nil err for config %q", path)
|
||||
}
|
||||
if !strings.Contains(err.Error(), expErr) {
|
||||
t.Errorf("expected err to contain %q; got %q instead", expErr, err)
|
||||
}
|
||||
}
|
||||
|
||||
f("testdata/unknownFields.bad.yaml", "unknown field")
|
||||
f("non-existing-file", "error reading")
|
||||
}
|
||||
235
app/vmalert/notifier/config_watcher.go
Normal file
@@ -0,0 +1,235 @@
|
||||
package notifier
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promscrape/discovery/consul"
|
||||
)
|
||||
|
||||
// configWatcher supports dynamic reload of Notifier objects
|
||||
// from static configuration and service discovery.
|
||||
// Use newWatcher to create a new object.
|
||||
type configWatcher struct {
|
||||
cfg *Config
|
||||
genFn AlertURLGenerator
|
||||
wg sync.WaitGroup
|
||||
|
||||
reloadCh chan struct{}
|
||||
syncCh chan struct{}
|
||||
|
||||
targetsMu sync.RWMutex
|
||||
targets map[TargetType][]Target
|
||||
}
|
||||
|
||||
func newWatcher(path string, gen AlertURLGenerator) (*configWatcher, error) {
|
||||
cfg, err := parseConfig(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cw := &configWatcher{
|
||||
cfg: cfg,
|
||||
wg: sync.WaitGroup{},
|
||||
reloadCh: make(chan struct{}, 1),
|
||||
syncCh: make(chan struct{}),
|
||||
genFn: gen,
|
||||
targetsMu: sync.RWMutex{},
|
||||
targets: make(map[TargetType][]Target),
|
||||
}
|
||||
return cw, cw.start()
|
||||
}
|
||||
|
||||
func (cw *configWatcher) notifiers() []Notifier {
|
||||
cw.targetsMu.RLock()
|
||||
defer cw.targetsMu.RUnlock()
|
||||
|
||||
var notifiers []Notifier
|
||||
for _, ns := range cw.targets {
|
||||
for _, n := range ns {
|
||||
notifiers = append(notifiers, n.Notifier)
|
||||
}
|
||||
|
||||
}
|
||||
return notifiers
|
||||
}
|
||||
|
||||
func (cw *configWatcher) reload(path string) error {
|
||||
select {
|
||||
case cw.reloadCh <- struct{}{}:
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
|
||||
defer func() { <-cw.reloadCh }()
|
||||
|
||||
cfg, err := parseConfig(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if cfg.Checksum == cw.cfg.Checksum {
|
||||
return nil
|
||||
}
|
||||
|
||||
// stop existing discovery
|
||||
cw.mustStop()
|
||||
|
||||
// re-start cw with new config
|
||||
cw.syncCh = make(chan struct{})
|
||||
cw.cfg = cfg
|
||||
return cw.start()
|
||||
}
|
||||
|
||||
func (cw *configWatcher) add(typeK TargetType, interval time.Duration, labelsFn getLabels) error {
|
||||
targets, errors := targetsFromLabels(labelsFn, cw.cfg, cw.genFn)
|
||||
for _, err := range errors {
|
||||
return fmt.Errorf("failed to init notifier for %q: %s", typeK, err)
|
||||
}
|
||||
|
||||
cw.setTargets(typeK, targets)
|
||||
|
||||
cw.wg.Add(1)
|
||||
go func() {
|
||||
defer cw.wg.Done()
|
||||
|
||||
ticker := time.NewTicker(interval)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-cw.syncCh:
|
||||
return
|
||||
case <-ticker.C:
|
||||
}
|
||||
updateTargets, errors := targetsFromLabels(labelsFn, cw.cfg, cw.genFn)
|
||||
for _, err := range errors {
|
||||
logger.Errorf("failed to init notifier for %q: %s", typeK, err)
|
||||
}
|
||||
cw.setTargets(typeK, updateTargets)
|
||||
}
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
func targetsFromLabels(labelsFn getLabels, cfg *Config, genFn AlertURLGenerator) ([]Target, []error) {
|
||||
metaLabels, err := labelsFn()
|
||||
if err != nil {
|
||||
return nil, []error{fmt.Errorf("failed to get labels: %s", err)}
|
||||
}
|
||||
var targets []Target
|
||||
var errors []error
|
||||
duplicates := make(map[string]struct{})
|
||||
for _, labels := range metaLabels {
|
||||
target := labels["__address__"]
|
||||
u, processedLabels, err := parseLabels(target, labels, cfg)
|
||||
if err != nil {
|
||||
errors = append(errors, err)
|
||||
continue
|
||||
}
|
||||
if len(u) == 0 {
|
||||
continue
|
||||
}
|
||||
if _, ok := duplicates[u]; ok { // check for duplicates
|
||||
if !*suppressDuplicateTargetErrors {
|
||||
logger.Errorf("skipping duplicate target with identical address %q; "+
|
||||
"make sure service discovery and relabeling is set up properly; "+
|
||||
"original labels: %s; resulting labels: %s",
|
||||
u, labels, processedLabels)
|
||||
}
|
||||
continue
|
||||
}
|
||||
duplicates[u] = struct{}{}
|
||||
|
||||
am, err := NewAlertManager(u, genFn, cfg.HTTPClientConfig, cfg.Timeout.Duration())
|
||||
if err != nil {
|
||||
errors = append(errors, err)
|
||||
continue
|
||||
}
|
||||
targets = append(targets, Target{
|
||||
Notifier: am,
|
||||
Labels: processedLabels,
|
||||
})
|
||||
}
|
||||
return targets, errors
|
||||
}
|
||||
|
||||
type getLabels func() ([]map[string]string, error)
|
||||
|
||||
func (cw *configWatcher) start() error {
|
||||
if len(cw.cfg.StaticConfigs) > 0 {
|
||||
var targets []Target
|
||||
for _, cfg := range cw.cfg.StaticConfigs {
|
||||
for _, target := range cfg.Targets {
|
||||
address, labels, err := parseLabels(target, nil, cw.cfg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse labels for target %q: %s", target, err)
|
||||
}
|
||||
notifier, err := NewAlertManager(address, cw.genFn, cw.cfg.HTTPClientConfig, cw.cfg.Timeout.Duration())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to init alertmanager for addr %q: %s", address, err)
|
||||
}
|
||||
targets = append(targets, Target{
|
||||
Notifier: notifier,
|
||||
Labels: labels,
|
||||
})
|
||||
}
|
||||
}
|
||||
cw.setTargets(TargetStatic, targets)
|
||||
}
|
||||
|
||||
if len(cw.cfg.ConsulSDConfigs) > 0 {
|
||||
err := cw.add(TargetConsul, *consul.SDCheckInterval, func() ([]map[string]string, error) {
|
||||
var labels []map[string]string
|
||||
for i := range cw.cfg.ConsulSDConfigs {
|
||||
sdc := &cw.cfg.ConsulSDConfigs[i]
|
||||
targetLabels, err := sdc.GetLabels(cw.cfg.baseDir)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("got labels err: %s", err)
|
||||
}
|
||||
labels = append(labels, targetLabels...)
|
||||
}
|
||||
return labels, nil
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to start consulSD discovery: %s", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cw *configWatcher) mustStop() {
|
||||
close(cw.syncCh)
|
||||
cw.wg.Wait()
|
||||
|
||||
cw.targetsMu.Lock()
|
||||
for _, targets := range cw.targets {
|
||||
for _, t := range targets {
|
||||
t.Close()
|
||||
}
|
||||
}
|
||||
cw.targets = make(map[TargetType][]Target)
|
||||
cw.targetsMu.Unlock()
|
||||
|
||||
for i := range cw.cfg.ConsulSDConfigs {
|
||||
cw.cfg.ConsulSDConfigs[i].MustStop()
|
||||
}
|
||||
cw.cfg = nil
|
||||
}
|
||||
|
||||
func (cw *configWatcher) setTargets(key TargetType, targets []Target) {
|
||||
cw.targetsMu.Lock()
|
||||
newT := make(map[string]Target)
|
||||
for _, t := range targets {
|
||||
newT[t.Addr()] = t
|
||||
}
|
||||
oldT := cw.targets[key]
|
||||
|
||||
for _, ot := range oldT {
|
||||
if _, ok := newT[ot.Addr()]; !ok {
|
||||
ot.Notifier.Close()
|
||||
}
|
||||
}
|
||||
cw.targets[key] = targets
|
||||
cw.targetsMu.Unlock()
|
||||
}
|
||||
301
app/vmalert/notifier/config_watcher_test.go
Normal file
@@ -0,0 +1,301 @@
|
||||
package notifier
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"sync"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestConfigWatcherReload(t *testing.T) {
|
||||
f, err := ioutil.TempFile("", "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer func() { _ = os.Remove(f.Name()) }()
|
||||
|
||||
writeToFile(t, f.Name(), `
|
||||
static_configs:
|
||||
- targets:
|
||||
- localhost:9093
|
||||
- localhost:9094
|
||||
`)
|
||||
cw, err := newWatcher(f.Name(), nil)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to start config watcher: %s", err)
|
||||
}
|
||||
defer cw.mustStop()
|
||||
ns := cw.notifiers()
|
||||
if len(ns) != 2 {
|
||||
t.Fatalf("expected to have 2 notifiers; got %d %#v", len(ns), ns)
|
||||
}
|
||||
|
||||
f2, err := ioutil.TempFile("", "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer func() { _ = os.Remove(f2.Name()) }()
|
||||
|
||||
writeToFile(t, f2.Name(), `
|
||||
static_configs:
|
||||
- targets:
|
||||
- 127.0.0.1:9093
|
||||
`)
|
||||
checkErr(t, cw.reload(f2.Name()))
|
||||
|
||||
ns = cw.notifiers()
|
||||
if len(ns) != 1 {
|
||||
t.Fatalf("expected to have 1 notifier; got %d", len(ns))
|
||||
}
|
||||
expAddr := "http://127.0.0.1:9093/api/v2/alerts"
|
||||
if ns[0].Addr() != expAddr {
|
||||
t.Fatalf("expected to get %q; got %q instead", expAddr, ns[0].Addr())
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigWatcherStart(t *testing.T) {
|
||||
consulSDServer := newFakeConsulServer()
|
||||
defer consulSDServer.Close()
|
||||
|
||||
consulSDFile, err := ioutil.TempFile("", "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer func() { _ = os.Remove(consulSDFile.Name()) }()
|
||||
|
||||
writeToFile(t, consulSDFile.Name(), fmt.Sprintf(`
|
||||
scheme: https
|
||||
path_prefix: proxy
|
||||
consul_sd_configs:
|
||||
- server: %s
|
||||
services:
|
||||
- alertmanager
|
||||
`, consulSDServer.URL))
|
||||
|
||||
cw, err := newWatcher(consulSDFile.Name(), nil)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to start config watcher: %s", err)
|
||||
}
|
||||
defer cw.mustStop()
|
||||
|
||||
if len(cw.notifiers()) != 2 {
|
||||
t.Fatalf("expected to get 2 notifiers; got %d", len(cw.notifiers()))
|
||||
}
|
||||
|
||||
expAddr1 := fmt.Sprintf("https://%s/proxy/api/v2/alerts", fakeConsulService1)
|
||||
expAddr2 := fmt.Sprintf("https://%s/proxy/api/v2/alerts", fakeConsulService2)
|
||||
|
||||
n1, n2 := cw.notifiers()[0], cw.notifiers()[1]
|
||||
if n1.Addr() != expAddr1 {
|
||||
t.Fatalf("exp address %q; got %q", expAddr1, n1.Addr())
|
||||
}
|
||||
if n2.Addr() != expAddr2 {
|
||||
t.Fatalf("exp address %q; got %q", expAddr2, n2.Addr())
|
||||
}
|
||||
}
|
||||
|
||||
// TestConfigWatcherReloadConcurrent supposed to test concurrent
|
||||
// execution of configuration update.
|
||||
// Should be executed with -race flag
|
||||
func TestConfigWatcherReloadConcurrent(t *testing.T) {
|
||||
consulSDServer1 := newFakeConsulServer()
|
||||
defer consulSDServer1.Close()
|
||||
consulSDServer2 := newFakeConsulServer()
|
||||
defer consulSDServer2.Close()
|
||||
|
||||
consulSDFile, err := ioutil.TempFile("", "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer func() { _ = os.Remove(consulSDFile.Name()) }()
|
||||
|
||||
writeToFile(t, consulSDFile.Name(), fmt.Sprintf(`
|
||||
consul_sd_configs:
|
||||
- server: %s
|
||||
services:
|
||||
- alertmanager
|
||||
- server: %s
|
||||
services:
|
||||
- consul
|
||||
`, consulSDServer1.URL, consulSDServer2.URL))
|
||||
|
||||
staticAndConsulSDFile, err := ioutil.TempFile("", "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer func() { _ = os.Remove(staticAndConsulSDFile.Name()) }()
|
||||
|
||||
writeToFile(t, staticAndConsulSDFile.Name(), fmt.Sprintf(`
|
||||
static_configs:
|
||||
- targets:
|
||||
- localhost:9093
|
||||
- localhost:9095
|
||||
consul_sd_configs:
|
||||
- server: %s
|
||||
services:
|
||||
- alertmanager
|
||||
- server: %s
|
||||
services:
|
||||
- consul
|
||||
`, consulSDServer1.URL, consulSDServer2.URL))
|
||||
|
||||
paths := []string{
|
||||
staticAndConsulSDFile.Name(),
|
||||
consulSDFile.Name(),
|
||||
"testdata/static.good.yaml",
|
||||
"unknownFields.bad.yaml",
|
||||
}
|
||||
|
||||
cw, err := newWatcher(paths[0], nil)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to start config watcher: %s", err)
|
||||
}
|
||||
defer cw.mustStop()
|
||||
|
||||
const workers = 500
|
||||
const iterations = 10
|
||||
wg := sync.WaitGroup{}
|
||||
wg.Add(workers)
|
||||
for i := 0; i < workers; i++ {
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for i := 0; i < iterations; i++ {
|
||||
rnd := rand.Intn(len(paths))
|
||||
_ = cw.reload(paths[rnd]) // update can fail and this is expected
|
||||
_ = cw.notifiers()
|
||||
}
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func writeToFile(t *testing.T, file, b string) {
|
||||
t.Helper()
|
||||
checkErr(t, ioutil.WriteFile(file, []byte(b), 0644))
|
||||
}
|
||||
|
||||
func checkErr(t *testing.T, err error) {
|
||||
t.Helper()
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected err: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
fakeConsulService1 = "127.0.0.1:9093"
|
||||
fakeConsulService2 = "127.0.0.1:9095"
|
||||
)
|
||||
|
||||
func newFakeConsulServer() *httptest.Server {
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/v1/agent/self", func(rw http.ResponseWriter, _ *http.Request) {
|
||||
rw.Write([]byte(`{"Config": {"Datacenter": "dc1"}}`))
|
||||
})
|
||||
mux.HandleFunc("/v1/catalog/services", func(rw http.ResponseWriter, _ *http.Request) {
|
||||
rw.Header().Set("X-Consul-Index", "1")
|
||||
rw.Write([]byte(`{
|
||||
"alertmanager": [
|
||||
"alertmanager",
|
||||
"__scheme__=http"
|
||||
]
|
||||
}`))
|
||||
})
|
||||
mux.HandleFunc("/v1/health/service/alertmanager", func(rw http.ResponseWriter, _ *http.Request) {
|
||||
rw.Header().Set("X-Consul-Index", "1")
|
||||
rw.Write([]byte(`
|
||||
[
|
||||
{
|
||||
"Node": {
|
||||
"ID": "e8e3629a-3f50-9d6e-aaf8-f173b5b05c72",
|
||||
"Node": "machine",
|
||||
"Address": "127.0.0.1",
|
||||
"Datacenter": "dc1",
|
||||
"TaggedAddresses": {
|
||||
"lan": "127.0.0.1",
|
||||
"lan_ipv4": "127.0.0.1",
|
||||
"wan": "127.0.0.1",
|
||||
"wan_ipv4": "127.0.0.1"
|
||||
},
|
||||
"Meta": {
|
||||
"consul-network-segment": ""
|
||||
},
|
||||
"CreateIndex": 13,
|
||||
"ModifyIndex": 14
|
||||
},
|
||||
"Service": {
|
||||
"ID": "am1",
|
||||
"Service": "alertmanager",
|
||||
"Tags": [
|
||||
"alertmanager",
|
||||
"__scheme__=http"
|
||||
],
|
||||
"Address": "",
|
||||
"Meta": null,
|
||||
"Port": 9093,
|
||||
"Weights": {
|
||||
"Passing": 1,
|
||||
"Warning": 1
|
||||
},
|
||||
"EnableTagOverride": false,
|
||||
"Proxy": {
|
||||
"Mode": "",
|
||||
"MeshGateway": {},
|
||||
"Expose": {}
|
||||
},
|
||||
"Connect": {},
|
||||
"CreateIndex": 16,
|
||||
"ModifyIndex": 16
|
||||
}
|
||||
},
|
||||
{
|
||||
"Node": {
|
||||
"ID": "e8e3629a-3f50-9d6e-aaf8-f173b5b05c72",
|
||||
"Node": "machine",
|
||||
"Address": "127.0.0.1",
|
||||
"Datacenter": "dc1",
|
||||
"TaggedAddresses": {
|
||||
"lan": "127.0.0.1",
|
||||
"lan_ipv4": "127.0.0.1",
|
||||
"wan": "127.0.0.1",
|
||||
"wan_ipv4": "127.0.0.1"
|
||||
},
|
||||
"Meta": {
|
||||
"consul-network-segment": ""
|
||||
},
|
||||
"CreateIndex": 13,
|
||||
"ModifyIndex": 14
|
||||
},
|
||||
"Service": {
|
||||
"ID": "am2",
|
||||
"Service": "alertmanager",
|
||||
"Tags": [
|
||||
"alertmanager",
|
||||
"bad-node"
|
||||
],
|
||||
"Address": "",
|
||||
"Meta": null,
|
||||
"Port": 9095,
|
||||
"Weights": {
|
||||
"Passing": 1,
|
||||
"Warning": 1
|
||||
},
|
||||
"EnableTagOverride": false,
|
||||
"Proxy": {
|
||||
"Mode": "",
|
||||
"MeshGateway": {},
|
||||
"Expose": {}
|
||||
},
|
||||
"Connect": {},
|
||||
"CreateIndex": 15,
|
||||
"ModifyIndex": 15
|
||||
}
|
||||
}
|
||||
]`))
|
||||
})
|
||||
|
||||
return httptest.NewServer(mux)
|
||||
}
|
||||
@@ -1,17 +1,28 @@
|
||||
package notifier
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/utils"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/flagutil"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promauth"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/prompbmarshal"
|
||||
)
|
||||
|
||||
var (
|
||||
addrs = flagutil.NewArray("notifier.url", "Prometheus alertmanager URL. Required parameter. e.g. http://127.0.0.1:9093")
|
||||
basicAuthUsername = flagutil.NewArray("notifier.basicAuth.username", "Optional basic auth username for -notifier.url")
|
||||
basicAuthPassword = flagutil.NewArray("notifier.basicAuth.password", "Optional basic auth password for -notifier.url")
|
||||
configPath = flag.String("notifier.config", "", "Path to configuration file for notifiers")
|
||||
suppressDuplicateTargetErrors = flag.Bool("notifier.suppressDuplicateTargetErrors", false, "Whether to suppress 'duplicate target' errors during discovery")
|
||||
|
||||
addrs = flagutil.NewArray("notifier.url", "Prometheus alertmanager URL, e.g. http://127.0.0.1:9093")
|
||||
|
||||
basicAuthUsername = flagutil.NewArray("notifier.basicAuth.username", "Optional basic auth username for -notifier.url")
|
||||
basicAuthPassword = flagutil.NewArray("notifier.basicAuth.password", "Optional basic auth password for -notifier.url")
|
||||
basicAuthPasswordFile = flagutil.NewArray("notifier.basicAuth.passwordFile", "Optional path to basic auth password file for -notifier.url")
|
||||
|
||||
bearerToken = flagutil.NewArray("notifier.bearerToken", "Optional bearer token for -notifier.url")
|
||||
bearerTokenFile = flagutil.NewArray("notifier.bearerTokenFile", "Optional path to bearer token file for -notifier.url")
|
||||
|
||||
tlsInsecureSkipVerify = flagutil.NewArrayBool("notifier.tlsInsecureSkipVerify", "Whether to skip tls verification when connecting to -notifier.url")
|
||||
tlsCertFile = flagutil.NewArray("notifier.tlsCertFile", "Optional path to client-side TLS certificate file to use when connecting to -notifier.url")
|
||||
@@ -20,26 +31,158 @@ var (
|
||||
"By default system CA is used")
|
||||
tlsServerName = flagutil.NewArray("notifier.tlsServerName", "Optional TLS server name to use for connections to -notifier.url. "+
|
||||
"By default the server name from -notifier.url is used")
|
||||
|
||||
oauth2ClientID = flagutil.NewArray("notifier.oauth2.clientID", "Optional OAuth2 clientID to use for -notifier.url. "+
|
||||
"If multiple args are set, then they are applied independently for the corresponding -notifier.url")
|
||||
oauth2ClientSecret = flagutil.NewArray("notifier.oauth2.clientSecret", "Optional OAuth2 clientSecret to use for -notifier.url. "+
|
||||
"If multiple args are set, then they are applied independently for the corresponding -notifier.url")
|
||||
oauth2ClientSecretFile = flagutil.NewArray("notifier.oauth2.clientSecretFile", "Optional OAuth2 clientSecretFile to use for -notifier.url. "+
|
||||
"If multiple args are set, then they are applied independently for the corresponding -notifier.url")
|
||||
oauth2TokenURL = flagutil.NewArray("notifier.oauth2.tokenUrl", "Optional OAuth2 tokenURL to use for -notifier.url. "+
|
||||
"If multiple args are set, then they are applied independently for the corresponding -notifier.url")
|
||||
oauth2Scopes = flagutil.NewArray("notifier.oauth2.scopes", "Optional OAuth2 scopes to use for -notifier.url. Scopes must be delimited by ';'. "+
|
||||
"If multiple args are set, then they are applied independently for the corresponding -notifier.url")
|
||||
)
|
||||
|
||||
// Init creates a Notifier object based on provided flags.
|
||||
func Init(gen AlertURLGenerator) ([]Notifier, error) {
|
||||
if len(*addrs) == 0 {
|
||||
return nil, fmt.Errorf("at least one `-notifier.url` must be set")
|
||||
// cw holds a configWatcher for configPath configuration file
|
||||
// configWatcher provides a list of Notifier objects discovered
|
||||
// from static config or via service discovery.
|
||||
// cw is not nil only if configPath is provided.
|
||||
var cw *configWatcher
|
||||
|
||||
// Reload checks the changes in configPath configuration file
|
||||
// and applies changes if any.
|
||||
func Reload() error {
|
||||
if cw == nil {
|
||||
return nil
|
||||
}
|
||||
return cw.reload(*configPath)
|
||||
}
|
||||
|
||||
var staticNotifiersFn func() []Notifier
|
||||
|
||||
var (
|
||||
// externalLabels is a global variable for holding external labels configured via flags
|
||||
// It is supposed to be inited via Init function only.
|
||||
externalLabels map[string]string
|
||||
// externalURL is a global variable for holding external URL value configured via flag
|
||||
// It is supposed to be inited via Init function only.
|
||||
externalURL string
|
||||
)
|
||||
|
||||
// Init returns a function for retrieving actual list of Notifier objects.
|
||||
// Init works in two mods:
|
||||
// * configuration via flags (for backward compatibility). Is always static
|
||||
// and don't support live reloads.
|
||||
// * configuration via file. Supports live reloads and service discovery.
|
||||
// Init returns an error if both mods are used.
|
||||
func Init(gen AlertURLGenerator, extLabels map[string]string, extURL string) (func() []Notifier, error) {
|
||||
if externalLabels != nil || externalURL != "" {
|
||||
return nil, fmt.Errorf("BUG: notifier.Init was called multiple times")
|
||||
}
|
||||
|
||||
externalURL = extURL
|
||||
externalLabels = extLabels
|
||||
|
||||
if *configPath == "" && len(*addrs) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
if *configPath != "" && len(*addrs) > 0 {
|
||||
return nil, fmt.Errorf("only one of -notifier.config or -notifier.url flags must be specified")
|
||||
}
|
||||
|
||||
if len(*addrs) > 0 {
|
||||
notifiers, err := notifiersFromFlags(gen)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create notifier from flag values: %s", err)
|
||||
}
|
||||
staticNotifiersFn = func() []Notifier {
|
||||
return notifiers
|
||||
}
|
||||
return staticNotifiersFn, nil
|
||||
}
|
||||
|
||||
var err error
|
||||
cw, err = newWatcher(*configPath, gen)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to init config watcher: %s", err)
|
||||
}
|
||||
return cw.notifiers, nil
|
||||
}
|
||||
|
||||
func notifiersFromFlags(gen AlertURLGenerator) ([]Notifier, error) {
|
||||
var notifiers []Notifier
|
||||
for i, addr := range *addrs {
|
||||
cert, key := tlsCertFile.GetOptionalArg(i), tlsKeyFile.GetOptionalArg(i)
|
||||
ca, serverName := tlsCAFile.GetOptionalArg(i), tlsServerName.GetOptionalArg(i)
|
||||
tr, err := utils.Transport(addr, cert, key, ca, serverName, tlsInsecureSkipVerify.GetOptionalArg(i))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create transport: %w", err)
|
||||
authCfg := promauth.HTTPClientConfig{
|
||||
TLSConfig: &promauth.TLSConfig{
|
||||
CAFile: tlsCAFile.GetOptionalArg(i),
|
||||
CertFile: tlsCertFile.GetOptionalArg(i),
|
||||
KeyFile: tlsKeyFile.GetOptionalArg(i),
|
||||
ServerName: tlsServerName.GetOptionalArg(i),
|
||||
InsecureSkipVerify: tlsInsecureSkipVerify.GetOptionalArg(i),
|
||||
},
|
||||
BasicAuth: &promauth.BasicAuthConfig{
|
||||
Username: basicAuthUsername.GetOptionalArg(i),
|
||||
Password: promauth.NewSecret(basicAuthPassword.GetOptionalArg(i)),
|
||||
PasswordFile: basicAuthPasswordFile.GetOptionalArg(i),
|
||||
},
|
||||
BearerToken: promauth.NewSecret(bearerToken.GetOptionalArg(i)),
|
||||
BearerTokenFile: bearerTokenFile.GetOptionalArg(i),
|
||||
OAuth2: &promauth.OAuth2Config{
|
||||
ClientID: oauth2ClientID.GetOptionalArg(i),
|
||||
ClientSecret: promauth.NewSecret(oauth2ClientSecret.GetOptionalArg(i)),
|
||||
ClientSecretFile: oauth2ClientSecretFile.GetOptionalArg(i),
|
||||
Scopes: strings.Split(oauth2Scopes.GetOptionalArg(i), ";"),
|
||||
TokenURL: oauth2TokenURL.GetOptionalArg(i),
|
||||
},
|
||||
}
|
||||
|
||||
addr = strings.TrimSuffix(addr, "/")
|
||||
am, err := NewAlertManager(addr+alertManagerPath, gen, authCfg, time.Minute)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
user, pass := basicAuthUsername.GetOptionalArg(i), basicAuthPassword.GetOptionalArg(i)
|
||||
am := NewAlertManager(addr, user, pass, gen, &http.Client{Transport: tr})
|
||||
notifiers = append(notifiers, am)
|
||||
}
|
||||
|
||||
return notifiers, nil
|
||||
}
|
||||
|
||||
// Target represents a Notifier and optional
|
||||
// list of labels added during discovery.
|
||||
type Target struct {
|
||||
Notifier
|
||||
Labels []prompbmarshal.Label
|
||||
}
|
||||
|
||||
// TargetType defines how the Target was discovered
|
||||
type TargetType string
|
||||
|
||||
const (
|
||||
// TargetStatic is for targets configured statically
|
||||
TargetStatic TargetType = "static"
|
||||
// TargetConsul is for targets discovered via Consul
|
||||
TargetConsul TargetType = "consulSD"
|
||||
)
|
||||
|
||||
// GetTargets returns list of static or discovered targets
|
||||
// via notifier configuration.
|
||||
func GetTargets() map[TargetType][]Target {
|
||||
var targets = make(map[TargetType][]Target)
|
||||
|
||||
if staticNotifiersFn != nil {
|
||||
for _, ns := range staticNotifiersFn() {
|
||||
targets[TargetStatic] = append(targets[TargetStatic], Target{
|
||||
Notifier: ns,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if cw != nil {
|
||||
cw.targetsMu.RLock()
|
||||
for key, ns := range cw.targets {
|
||||
targets[key] = append(targets[key], ns...)
|
||||
}
|
||||
cw.targetsMu.RUnlock()
|
||||
}
|
||||
return targets
|
||||
}
|
||||
|
||||
@@ -10,4 +10,6 @@ type Notifier interface {
|
||||
Send(ctx context.Context, alerts []Alert) error
|
||||
// Addr returns address where alerts are sent.
|
||||
Addr() string
|
||||
// Close is a destructor for the Notifier
|
||||
Close()
|
||||
}
|
||||
|
||||
@@ -17,8 +17,10 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"net"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -26,6 +28,7 @@ import (
|
||||
textTpl "text/template"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/datasource"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promutils"
|
||||
)
|
||||
|
||||
// metric is private copy of datasource.Metric,
|
||||
@@ -61,6 +64,7 @@ var tmplFunc textTpl.FuncMap
|
||||
|
||||
// InitTemplateFunc initiates template helper functions
|
||||
func InitTemplateFunc(externalURL *url.URL) {
|
||||
// See https://prometheus.io/docs/prometheus/latest/configuration/template_reference/
|
||||
tmplFunc = textTpl.FuncMap{
|
||||
/* Strings */
|
||||
|
||||
@@ -91,6 +95,24 @@ func InitTemplateFunc(externalURL *url.URL) {
|
||||
// alias for https://golang.org/pkg/strings/#ToLower
|
||||
"toLower": strings.ToLower,
|
||||
|
||||
// stripPort splits string into host and port, then returns only host.
|
||||
"stripPort": func(hostPort string) string {
|
||||
host, _, err := net.SplitHostPort(hostPort)
|
||||
if err != nil {
|
||||
return hostPort
|
||||
}
|
||||
return host
|
||||
},
|
||||
|
||||
// parseDuration parses a duration string such as "1h" into the number of seconds it represents
|
||||
"parseDuration": func(s string) (float64, error) {
|
||||
d, err := promutils.ParseDuration(s)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return d.Seconds(), nil
|
||||
},
|
||||
|
||||
/* Numbers */
|
||||
|
||||
// humanize converts given number to a human readable format
|
||||
@@ -261,6 +283,14 @@ func InitTemplateFunc(externalURL *url.URL) {
|
||||
return m.Labels[label]
|
||||
},
|
||||
|
||||
// sortByLabel sorts the given metrics by provided label key
|
||||
"sortByLabel": func(label string, metrics []metric) []metric {
|
||||
sort.SliceStable(metrics, func(i, j int) bool {
|
||||
return metrics[i].Labels[label] < metrics[j].Labels[label]
|
||||
})
|
||||
return metrics
|
||||
},
|
||||
|
||||
// value returns the value of the given metric.
|
||||
// usually used alongside with `query` template function.
|
||||
"value": func(m metric) float64 {
|
||||
|
||||
13
app/vmalert/notifier/testdata/consul.good.yaml
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
consul_sd_configs:
|
||||
- server: localhost:8500
|
||||
scheme: http
|
||||
services:
|
||||
- alertmanager
|
||||
- server: localhost:8500
|
||||
services:
|
||||
- consul
|
||||
relabel_configs:
|
||||
- source_labels: [__meta_consul_tags]
|
||||
regex: .*,__scheme__=([^,]+),.*
|
||||
replacement: '${1}'
|
||||
target_label: __scheme__
|
||||
18
app/vmalert/notifier/testdata/mixed.good.yaml
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
static_configs:
|
||||
- targets:
|
||||
- localhost:9093
|
||||
- localhost:9095
|
||||
|
||||
consul_sd_configs:
|
||||
- server: localhost:8500
|
||||
scheme: http
|
||||
services:
|
||||
- alertmanager
|
||||
- server: localhost:8500
|
||||
services:
|
||||
- consul
|
||||
relabel_configs:
|
||||
- source_labels: [__meta_consul_tags]
|
||||
regex: .*,__scheme__=([^,]+),.*
|
||||
replacement: '${1}'
|
||||
target_label: __scheme__
|
||||
4
app/vmalert/notifier/testdata/static.good.yaml
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
static_configs:
|
||||
- targets:
|
||||
- localhost:9093
|
||||
- localhost:9095
|
||||
5
app/vmalert/notifier/testdata/unknownFields.bad.yaml
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
scheme: https
|
||||
unknown: field
|
||||
static_configs:
|
||||
- targets:
|
||||
- localhost:9093
|
||||
@@ -10,8 +10,8 @@ import (
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/config"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/datasource"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/utils"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/prompbmarshal"
|
||||
"github.com/VictoriaMetrics/metrics"
|
||||
)
|
||||
|
||||
// RecordingRule is a Rule that supposed
|
||||
@@ -31,6 +31,8 @@ type RecordingRule struct {
|
||||
mu sync.RWMutex
|
||||
// stores last moment of time Exec was called
|
||||
lastExecTime time.Time
|
||||
// stores the duration of the last Exec call
|
||||
lastExecDuration time.Duration
|
||||
// stores last error that happened in Exec func
|
||||
// resets on every successful Exec
|
||||
// may be used as Health state
|
||||
@@ -43,8 +45,8 @@ type RecordingRule struct {
|
||||
}
|
||||
|
||||
type recordingRuleMetrics struct {
|
||||
errors *gauge
|
||||
samples *gauge
|
||||
errors *utils.Gauge
|
||||
samples *utils.Gauge
|
||||
}
|
||||
|
||||
// String implements Stringer interface
|
||||
@@ -60,7 +62,7 @@ func (rr *RecordingRule) ID() uint64 {
|
||||
|
||||
func newRecordingRule(qb datasource.QuerierBuilder, group *Group, cfg config.Rule) *RecordingRule {
|
||||
rr := &RecordingRule{
|
||||
Type: cfg.Type,
|
||||
Type: group.Type,
|
||||
RuleID: cfg.ID,
|
||||
Name: cfg.Record,
|
||||
Expr: cfg.Expr,
|
||||
@@ -68,14 +70,14 @@ func newRecordingRule(qb datasource.QuerierBuilder, group *Group, cfg config.Rul
|
||||
GroupID: group.ID(),
|
||||
metrics: &recordingRuleMetrics{},
|
||||
q: qb.BuildWithParams(datasource.QuerierParams{
|
||||
DataSourceType: &cfg.Type,
|
||||
DataSourceType: &group.Type,
|
||||
EvaluationInterval: group.Interval,
|
||||
ExtraLabels: group.ExtraFilterLabels,
|
||||
QueryParams: group.Params,
|
||||
}),
|
||||
}
|
||||
|
||||
labels := fmt.Sprintf(`recording=%q, group=%q, id="%d"`, rr.Name, group.Name, rr.ID())
|
||||
rr.metrics.errors = getOrCreateGauge(fmt.Sprintf(`vmalert_recording_rules_error{%s}`, labels),
|
||||
rr.metrics.errors = utils.GetOrCreateGauge(fmt.Sprintf(`vmalert_recording_rules_error{%s}`, labels),
|
||||
func() float64 {
|
||||
rr.mu.RLock()
|
||||
defer rr.mu.RUnlock()
|
||||
@@ -84,7 +86,7 @@ func newRecordingRule(qb datasource.QuerierBuilder, group *Group, cfg config.Rul
|
||||
}
|
||||
return 1
|
||||
})
|
||||
rr.metrics.samples = getOrCreateGauge(fmt.Sprintf(`vmalert_recording_rules_last_evaluation_samples{%s}`, labels),
|
||||
rr.metrics.samples = utils.GetOrCreateGauge(fmt.Sprintf(`vmalert_recording_rules_last_evaluation_samples{%s}`, labels),
|
||||
func() float64 {
|
||||
rr.mu.RLock()
|
||||
defer rr.mu.RUnlock()
|
||||
@@ -95,8 +97,8 @@ func newRecordingRule(qb datasource.QuerierBuilder, group *Group, cfg config.Rul
|
||||
|
||||
// Close unregisters rule metrics
|
||||
func (rr *RecordingRule) Close() {
|
||||
metrics.UnregisterMetric(rr.metrics.errors.name)
|
||||
metrics.UnregisterMetric(rr.metrics.samples.name)
|
||||
rr.metrics.errors.Unregister()
|
||||
rr.metrics.samples.Unregister()
|
||||
}
|
||||
|
||||
// ExecRange executes recording rule on the given time range similarly to Exec.
|
||||
@@ -123,11 +125,13 @@ func (rr *RecordingRule) ExecRange(ctx context.Context, start, end time.Time) ([
|
||||
|
||||
// Exec executes RecordingRule expression via the given Querier.
|
||||
func (rr *RecordingRule) Exec(ctx context.Context) ([]prompbmarshal.TimeSeries, error) {
|
||||
start := time.Now()
|
||||
qMetrics, err := rr.q.Query(ctx, rr.Expr)
|
||||
rr.mu.Lock()
|
||||
defer rr.mu.Unlock()
|
||||
|
||||
rr.lastExecTime = time.Now()
|
||||
rr.lastExecTime = start
|
||||
rr.lastExecDuration = time.Since(start)
|
||||
rr.lastExecError = err
|
||||
rr.lastExecSamples = len(qMetrics)
|
||||
if err != nil {
|
||||
@@ -193,23 +197,27 @@ func (rr *RecordingRule) UpdateWith(r Rule) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// RuleAPI returns Rule representation in form
|
||||
// of APIRecordingRule
|
||||
func (rr *RecordingRule) RuleAPI() APIRecordingRule {
|
||||
var lastErr string
|
||||
if rr.lastExecError != nil {
|
||||
lastErr = rr.lastExecError.Error()
|
||||
}
|
||||
return APIRecordingRule{
|
||||
// ToAPI returns Rule's representation in form
|
||||
// of APIRule
|
||||
func (rr *RecordingRule) ToAPI() APIRule {
|
||||
r := APIRule{
|
||||
Type: "recording",
|
||||
DatasourceType: rr.Type.String(),
|
||||
Name: rr.Name,
|
||||
Query: rr.Expr,
|
||||
Labels: rr.Labels,
|
||||
LastEvaluation: rr.lastExecTime,
|
||||
EvaluationTime: rr.lastExecDuration.Seconds(),
|
||||
Health: "ok",
|
||||
LastSamples: rr.lastExecSamples,
|
||||
// encode as strings to avoid rounding
|
||||
ID: fmt.Sprintf("%d", rr.ID()),
|
||||
GroupID: fmt.Sprintf("%d", rr.GroupID),
|
||||
Name: rr.Name,
|
||||
Type: rr.Type.String(),
|
||||
Expression: rr.Expr,
|
||||
LastError: lastErr,
|
||||
LastSamples: rr.lastExecSamples,
|
||||
LastExec: rr.lastExecTime,
|
||||
Labels: rr.Labels,
|
||||
ID: fmt.Sprintf("%d", rr.ID()),
|
||||
GroupID: fmt.Sprintf("%d", rr.GroupID),
|
||||
}
|
||||
|
||||
if rr.lastExecError != nil {
|
||||
r.LastError = rr.lastExecError.Error()
|
||||
r.Health = "err"
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
@@ -12,12 +12,14 @@ import (
|
||||
var (
|
||||
addr = flag.String("remoteRead.url", "", "Optional URL to VictoriaMetrics or vmselect that will be used to restore alerts "+
|
||||
"state. This configuration makes sense only if `vmalert` was configured with `remoteWrite.url` before and has been successfully persisted its state. "+
|
||||
"E.g. http://127.0.0.1:8428")
|
||||
"E.g. http://127.0.0.1:8428. See also -remoteRead.disablePathAppend")
|
||||
|
||||
basicAuthUsername = flag.String("remoteRead.basicAuth.username", "", "Optional basic auth username for -remoteRead.url")
|
||||
basicAuthPassword = flag.String("remoteRead.basicAuth.password", "", "Optional basic auth password for -remoteRead.url")
|
||||
basicAuthPasswordFile = flag.String("remoteRead.basicAuth.passwordFile", "", "Optional path to basic auth password to use for -remoteRead.url")
|
||||
bearerToken = flag.String("remoteRead.bearerToken", "", "Optional bearer auth token to use for -remoteRead.url.")
|
||||
bearerTokenFile = flag.String("remoteRead.bearerTokenFile", "", "Optional path to bearer token file to use for -remoteRead.url.")
|
||||
|
||||
bearerToken = flag.String("remoteRead.bearerToken", "", "Optional bearer auth token to use for -remoteRead.url.")
|
||||
bearerTokenFile = flag.String("remoteRead.bearerTokenFile", "", "Optional path to bearer token file to use for -remoteRead.url.")
|
||||
|
||||
tlsInsecureSkipVerify = flag.Bool("remoteRead.tlsInsecureSkipVerify", false, "Whether to skip tls verification when connecting to -remoteRead.url")
|
||||
tlsCertFile = flag.String("remoteRead.tlsCertFile", "", "Optional path to client-side TLS certificate file to use when connecting to -remoteRead.url")
|
||||
@@ -26,6 +28,14 @@ var (
|
||||
"By default system CA is used")
|
||||
tlsServerName = flag.String("remoteRead.tlsServerName", "", "Optional TLS server name to use for connections to -remoteRead.url. "+
|
||||
"By default the server name from -remoteRead.url is used")
|
||||
|
||||
oauth2ClientID = flag.String("remoteRead.oauth2.clientID", "", "Optional OAuth2 clientID to use for -remoteRead.url.")
|
||||
oauth2ClientSecret = flag.String("remoteRead.oauth2.clientSecret", "", "Optional OAuth2 clientSecret to use for -remoteRead.url.")
|
||||
oauth2ClientSecretFile = flag.String("remoteRead.oauth2.clientSecretFile", "", "Optional OAuth2 clientSecretFile to use for -remoteRead.url.")
|
||||
oauth2TokenURL = flag.String("remoteRead.oauth2.tokenUrl", "", "Optional OAuth2 tokenURL to use for -remoteRead.url. ")
|
||||
oauth2Scopes = flag.String("remoteRead.oauth2.scopes", "", "Optional OAuth2 scopes to use for -remoteRead.url. Scopes must be delimited by ';'.")
|
||||
|
||||
disablePathAppend = flag.Bool("remoteRead.disablePathAppend", false, "Whether to disable automatic appending of '/api/v1/query' path to the configured -remoteRead.url.")
|
||||
)
|
||||
|
||||
// Init creates a Querier from provided flag values.
|
||||
@@ -38,10 +48,14 @@ func Init() (datasource.QuerierBuilder, error) {
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create transport: %w", err)
|
||||
}
|
||||
authCfg, err := utils.AuthConfig(*basicAuthUsername, *basicAuthPassword, *basicAuthPasswordFile, *bearerToken, *bearerTokenFile)
|
||||
|
||||
authCfg, err := utils.AuthConfig(
|
||||
utils.WithBasicAuth(*basicAuthUsername, *basicAuthPassword, *basicAuthPasswordFile),
|
||||
utils.WithBearer(*bearerToken, *bearerTokenFile),
|
||||
utils.WithOAuth(*oauth2ClientID, *oauth2ClientSecret, *oauth2ClientSecretFile, *oauth2TokenURL, *oauth2Scopes))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to configure auth: %w", err)
|
||||
}
|
||||
c := &http.Client{Transport: tr}
|
||||
return datasource.NewVMStorage(*addr, authCfg, 0, 0, false, c), nil
|
||||
return datasource.NewVMStorage(*addr, authCfg, 0, 0, false, c, *disablePathAppend), nil
|
||||
}
|
||||
|
||||
@@ -13,11 +13,13 @@ var (
|
||||
addr = flag.String("remoteWrite.url", "", "Optional URL to VictoriaMetrics or vminsert where to persist alerts state "+
|
||||
"and recording rules results in form of timeseries. For example, if -remoteWrite.url=http://127.0.0.1:8428 is specified, "+
|
||||
"then the alerts state will be written to http://127.0.0.1:8428/api/v1/write . See also -remoteWrite.disablePathAppend")
|
||||
|
||||
basicAuthUsername = flag.String("remoteWrite.basicAuth.username", "", "Optional basic auth username for -remoteWrite.url")
|
||||
basicAuthPassword = flag.String("remoteWrite.basicAuth.password", "", "Optional basic auth password for -remoteWrite.url")
|
||||
basicAuthPasswordFile = flag.String("remoteWrite.basicAuth.passwordFile", "", "Optional path to basic auth password to use for -remoteWrite.url")
|
||||
bearerToken = flag.String("remoteWrite.bearerToken", "", "Optional bearer auth token to use for -remoteWrite.url.")
|
||||
bearerTokenFile = flag.String("remoteWrite.bearerTokenFile", "", "Optional path to bearer token file to use for -remoteWrite.url.")
|
||||
|
||||
bearerToken = flag.String("remoteWrite.bearerToken", "", "Optional bearer auth token to use for -remoteWrite.url.")
|
||||
bearerTokenFile = flag.String("remoteWrite.bearerTokenFile", "", "Optional path to bearer token file to use for -remoteWrite.url.")
|
||||
|
||||
maxQueueSize = flag.Int("remoteWrite.maxQueueSize", 1e5, "Defines the max number of pending datapoints to remote write endpoint")
|
||||
maxBatchSize = flag.Int("remoteWrite.maxBatchSize", 1e3, "Defines defines max number of timeseries to be flushed at once")
|
||||
@@ -31,6 +33,13 @@ var (
|
||||
"By default system CA is used")
|
||||
tlsServerName = flag.String("remoteWrite.tlsServerName", "", "Optional TLS server name to use for connections to -remoteWrite.url. "+
|
||||
"By default the server name from -remoteWrite.url is used")
|
||||
|
||||
oauth2ClientID = flag.String("remoteWrite.oauth2.clientID", "", "Optional OAuth2 clientID to use for -remoteWrite.url.")
|
||||
oauth2ClientSecret = flag.String("remoteWrite.oauth2.clientSecret", "", "Optional OAuth2 clientSecret to use for -remoteWrite.url.")
|
||||
oauth2ClientSecretFile = flag.String("remoteWrite.oauth2.clientSecretFile", "", "Optional OAuth2 clientSecretFile to use for -remoteWrite.url.")
|
||||
oauth2TokenURL = flag.String("remoteWrite.oauth2.tokenUrl", "", "Optional OAuth2 tokenURL to use for -notifier.url.")
|
||||
oauth2Scopes = flag.String("remoteWrite.oauth2.scopes", "", "Optional OAuth2 scopes to use for -notifier.url. Scopes must be delimited by ';'.")
|
||||
|
||||
disablePathAppend = flag.Bool("remoteWrite.disablePathAppend", false, "Whether to disable automatic appending of '/api/v1/write' path to the configured -remoteWrite.url.")
|
||||
)
|
||||
|
||||
@@ -46,7 +55,10 @@ func Init(ctx context.Context) (*Client, error) {
|
||||
return nil, fmt.Errorf("failed to create transport: %w", err)
|
||||
}
|
||||
|
||||
authCfg, err := utils.AuthConfig(*basicAuthUsername, *basicAuthPassword, *basicAuthPasswordFile, *bearerToken, *bearerTokenFile)
|
||||
authCfg, err := utils.AuthConfig(
|
||||
utils.WithBasicAuth(*basicAuthUsername, *basicAuthPassword, *basicAuthPasswordFile),
|
||||
utils.WithBearer(*bearerToken, *bearerTokenFile),
|
||||
utils.WithOAuth(*oauth2ClientID, *oauth2ClientSecret, *oauth2ClientSecretFile, *oauth2TokenURL, *oauth2Scopes))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to configure auth: %w", err)
|
||||
}
|
||||
|
||||
@@ -246,13 +246,13 @@ func (c *Client) send(ctx context.Context, data []byte) error {
|
||||
resp, err := c.c.Do(req.WithContext(ctx))
|
||||
if err != nil {
|
||||
return fmt.Errorf("error while sending request to %s: %w; Data len %d(%d)",
|
||||
req.URL, err, len(data), r.Size())
|
||||
req.URL.Redacted(), err, len(data), r.Size())
|
||||
}
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
if resp.StatusCode != http.StatusNoContent && resp.StatusCode != http.StatusOK {
|
||||
body, _ := ioutil.ReadAll(resp.Body)
|
||||
return fmt.Errorf("unexpected response code %d for %s. Response body %q",
|
||||
resp.StatusCode, req.URL, body)
|
||||
resp.StatusCode, req.URL.Redacted(), body)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/config"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/datasource"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/utils"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promutils"
|
||||
)
|
||||
|
||||
type fakeReplayQuerier struct {
|
||||
@@ -83,7 +83,7 @@ func TestReplay(t *testing.T) {
|
||||
to: "2021-01-01T15:02:30.000Z",
|
||||
maxDP: 60,
|
||||
cfg: []config.Group{
|
||||
{Interval: utils.NewPromDuration(time.Minute), Rules: []config.Rule{{Record: "foo", Expr: "sum(up)"}}},
|
||||
{Interval: promutils.NewDuration(time.Minute), Rules: []config.Rule{{Record: "foo", Expr: "sum(up)"}}},
|
||||
},
|
||||
qb: &fakeReplayQuerier{
|
||||
registry: map[string]map[string]struct{}{
|
||||
|
||||
@@ -21,6 +21,8 @@ type Rule interface {
|
||||
// UpdateWith performs modification of current Rule
|
||||
// with fields of the given Rule.
|
||||
UpdateWith(Rule) error
|
||||
// ToAPI converts Rule into APIRule
|
||||
ToAPI() APIRule
|
||||
// Close performs the shutdown procedures for rule
|
||||
// such as metrics unregister
|
||||
Close()
|
||||
|
||||
@@ -1,18 +1,60 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promauth"
|
||||
)
|
||||
|
||||
// AuthConfigOptions options which helps build promauth.Config
|
||||
type AuthConfigOptions func(config *promauth.HTTPClientConfig)
|
||||
|
||||
// AuthConfig returns promauth.Config based on the given params
|
||||
func AuthConfig(baUser, baPass, baFile, bearerToken, bearerTokenFile string) (*promauth.Config, error) {
|
||||
var baCfg *promauth.BasicAuthConfig
|
||||
if baUser != "" || baPass != "" || baFile != "" {
|
||||
baCfg = &promauth.BasicAuthConfig{
|
||||
Username: baUser,
|
||||
Password: baPass,
|
||||
PasswordFile: baFile,
|
||||
func AuthConfig(filterOptions ...AuthConfigOptions) (*promauth.Config, error) {
|
||||
authCfg := &promauth.HTTPClientConfig{}
|
||||
for _, option := range filterOptions {
|
||||
option(authCfg)
|
||||
}
|
||||
|
||||
return authCfg.NewConfig(".")
|
||||
}
|
||||
|
||||
// WithBasicAuth returns AuthConfigOptions and initialized promauth.BasicAuthConfig based on given params
|
||||
func WithBasicAuth(username, password, passwordFile string) AuthConfigOptions {
|
||||
return func(config *promauth.HTTPClientConfig) {
|
||||
if username != "" || password != "" || passwordFile != "" {
|
||||
config.BasicAuth = &promauth.BasicAuthConfig{
|
||||
Username: username,
|
||||
Password: promauth.NewSecret(password),
|
||||
PasswordFile: passwordFile,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WithBearer returns AuthConfigOptions and set BearerToken or BearerTokenFile based on given params
|
||||
func WithBearer(token, tokenFile string) AuthConfigOptions {
|
||||
return func(config *promauth.HTTPClientConfig) {
|
||||
if token != "" {
|
||||
config.BearerToken = promauth.NewSecret(token)
|
||||
}
|
||||
if tokenFile != "" {
|
||||
config.BearerTokenFile = tokenFile
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WithOAuth returns AuthConfigOptions and set OAuth params based on given params
|
||||
func WithOAuth(clientID, clientSecret, clientSecretFile, tokenURL, scopes string) AuthConfigOptions {
|
||||
return func(config *promauth.HTTPClientConfig) {
|
||||
if clientSecretFile != "" || clientSecret != "" {
|
||||
config.OAuth2 = &promauth.OAuth2Config{
|
||||
ClientID: clientID,
|
||||
ClientSecret: promauth.NewSecret(clientSecret),
|
||||
ClientSecretFile: clientSecretFile,
|
||||
TokenURL: tokenURL,
|
||||
Scopes: strings.Split(scopes, ";"),
|
||||
}
|
||||
}
|
||||
}
|
||||
return promauth.NewConfig(".", nil, baCfg, bearerToken, bearerTokenFile, nil, nil)
|
||||
}
|
||||
|
||||
54
app/vmalert/utils/metrics.go
Normal file
@@ -0,0 +1,54 @@
|
||||
package utils
|
||||
|
||||
import "github.com/VictoriaMetrics/metrics"
|
||||
|
||||
type namedMetric struct {
|
||||
Name string
|
||||
}
|
||||
|
||||
// Unregister removes the metric by name from default registry
|
||||
func (nm namedMetric) Unregister() {
|
||||
metrics.UnregisterMetric(nm.Name)
|
||||
}
|
||||
|
||||
// Gauge is a metrics.Gauge with Name
|
||||
type Gauge struct {
|
||||
namedMetric
|
||||
*metrics.Gauge
|
||||
}
|
||||
|
||||
// GetOrCreateGauge creates a new Gauge with the given name
|
||||
func GetOrCreateGauge(name string, f func() float64) *Gauge {
|
||||
return &Gauge{
|
||||
namedMetric: namedMetric{Name: name},
|
||||
Gauge: metrics.GetOrCreateGauge(name, f),
|
||||
}
|
||||
}
|
||||
|
||||
// Counter is a metrics.Counter with Name
|
||||
type Counter struct {
|
||||
namedMetric
|
||||
*metrics.Counter
|
||||
}
|
||||
|
||||
// GetOrCreateCounter creates a new Counter with the given name
|
||||
func GetOrCreateCounter(name string) *Counter {
|
||||
return &Counter{
|
||||
namedMetric: namedMetric{Name: name},
|
||||
Counter: metrics.GetOrCreateCounter(name),
|
||||
}
|
||||
}
|
||||
|
||||
// Summary is a metrics.Summary with Name
|
||||
type Summary struct {
|
||||
namedMetric
|
||||
*metrics.Summary
|
||||
}
|
||||
|
||||
// GetOrCreateSummary creates a new Summary with the given name
|
||||
func GetOrCreateSummary(name string) *Summary {
|
||||
return &Summary{
|
||||
namedMetric: namedMetric{Name: name},
|
||||
Summary: metrics.GetOrCreateSummary(name),
|
||||
}
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/VictoriaMetrics/metricsql"
|
||||
)
|
||||
|
||||
// PromDuration is Prometheus duration.
|
||||
type PromDuration struct {
|
||||
milliseconds int64
|
||||
}
|
||||
|
||||
// NewPromDuration returns PromDuration for given d.
|
||||
func NewPromDuration(d time.Duration) PromDuration {
|
||||
return PromDuration{
|
||||
milliseconds: d.Milliseconds(),
|
||||
}
|
||||
}
|
||||
|
||||
// MarshalYAML implements yaml.Marshaler interface.
|
||||
func (pd PromDuration) MarshalYAML() (interface{}, error) {
|
||||
return pd.Duration().String(), nil
|
||||
}
|
||||
|
||||
// UnmarshalYAML implements yaml.Unmarshaler interface.
|
||||
func (pd *PromDuration) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
var s string
|
||||
if err := unmarshal(&s); err != nil {
|
||||
return err
|
||||
}
|
||||
ms, err := metricsql.DurationValue(s, 0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pd.milliseconds = ms
|
||||
return nil
|
||||
}
|
||||
|
||||
// Duration returns duration for pd.
|
||||
func (pd *PromDuration) Duration() time.Duration {
|
||||
return time.Duration(pd.milliseconds) * time.Millisecond
|
||||
}
|
||||
525
app/vmalert/vmalert_cluster.excalidraw
Normal file
@@ -0,0 +1,525 @@
|
||||
{
|
||||
"type": "excalidraw",
|
||||
"version": 2,
|
||||
"source": "https://excalidraw.com",
|
||||
"elements": [
|
||||
{
|
||||
"type": "rectangle",
|
||||
"version": 794,
|
||||
"versionNonce": 1855937036,
|
||||
"isDeleted": false,
|
||||
"id": "VgBUzo0blGR-Ijd2mQEEf",
|
||||
"fillStyle": "hachure",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"angle": 0,
|
||||
"x": 422.3502197265625,
|
||||
"y": 215.55953979492188,
|
||||
"strokeColor": "#000000",
|
||||
"backgroundColor": "transparent",
|
||||
"width": 123.7601318359375,
|
||||
"height": 72.13211059570312,
|
||||
"seed": 1194011660,
|
||||
"groupIds": [
|
||||
"iBaXgbpyifSwPplm_GO5b"
|
||||
],
|
||||
"strokeSharpness": "sharp",
|
||||
"boundElementIds": [
|
||||
"miEbzHxOPXe4PEYvXiJp5",
|
||||
"rcmiQfIWtfbTTlwxqr1sl",
|
||||
"P-dpWlSTtnsux-zr5oqgF",
|
||||
"oAToSPttH7aWoD_AqXGFX",
|
||||
"Bpy5by47XGKB4yS99ZkuA",
|
||||
"wRO0q9xKPHc8e8XPPsQWh",
|
||||
"sxEhnxlbT7ldlSsmHDUHp"
|
||||
],
|
||||
"updated": 1638348083348
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"version": 659,
|
||||
"versionNonce": 247957684,
|
||||
"isDeleted": false,
|
||||
"id": "e9TDm09y-GhPm84XWt0Jv",
|
||||
"fillStyle": "hachure",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"angle": 0,
|
||||
"x": 443.89678955078125,
|
||||
"y": 236.64378356933594,
|
||||
"strokeColor": "#000000",
|
||||
"backgroundColor": "transparent",
|
||||
"width": 82,
|
||||
"height": 24,
|
||||
"seed": 327273100,
|
||||
"groupIds": [
|
||||
"iBaXgbpyifSwPplm_GO5b"
|
||||
],
|
||||
"strokeSharpness": "sharp",
|
||||
"boundElementIds": [],
|
||||
"updated": 1638347948032,
|
||||
"fontSize": 20,
|
||||
"fontFamily": 3,
|
||||
"text": "vmalert",
|
||||
"baseline": 19,
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "middle"
|
||||
},
|
||||
{
|
||||
"type": "rectangle",
|
||||
"version": 1670,
|
||||
"versionNonce": 2021681972,
|
||||
"isDeleted": false,
|
||||
"id": "dd52BjHfPMPRji9Tws7U-",
|
||||
"fillStyle": "hachure",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"angle": 0,
|
||||
"x": 750.3317260742188,
|
||||
"y": 226.5509033203125,
|
||||
"strokeColor": "#000000",
|
||||
"backgroundColor": "transparent",
|
||||
"width": 171.99359130859375,
|
||||
"height": 44.74725341796875,
|
||||
"seed": 1779959692,
|
||||
"groupIds": [
|
||||
"2Lijjn3PwPQW_8KrcDmdu"
|
||||
],
|
||||
"strokeSharpness": "sharp",
|
||||
"boundElementIds": [
|
||||
"Bpy5by47XGKB4yS99ZkuA"
|
||||
],
|
||||
"updated": 1638348054411
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"version": 1311,
|
||||
"versionNonce": 1283453068,
|
||||
"isDeleted": false,
|
||||
"id": "9TEzv0sVCHAkc46ou0oNF",
|
||||
"fillStyle": "hachure",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"angle": 0,
|
||||
"x": 759.2862243652344,
|
||||
"y": 238.68240356445312,
|
||||
"strokeColor": "#000000",
|
||||
"backgroundColor": "transparent",
|
||||
"width": 152,
|
||||
"height": 24,
|
||||
"seed": 1617178804,
|
||||
"groupIds": [
|
||||
"2Lijjn3PwPQW_8KrcDmdu"
|
||||
],
|
||||
"strokeSharpness": "sharp",
|
||||
"boundElementIds": [],
|
||||
"updated": 1638348054411,
|
||||
"fontSize": 20,
|
||||
"fontFamily": 3,
|
||||
"text": "vminsert:8480",
|
||||
"baseline": 19,
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "top"
|
||||
},
|
||||
{
|
||||
"type": "rectangle",
|
||||
"version": 897,
|
||||
"versionNonce": 1983434892,
|
||||
"isDeleted": false,
|
||||
"id": "Sa4OBd1ZjD6itohm7Ll8z",
|
||||
"fillStyle": "hachure",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"angle": 0,
|
||||
"x": 61.744873046875,
|
||||
"y": 224.9600830078125,
|
||||
"strokeColor": "#000000",
|
||||
"backgroundColor": "transparent",
|
||||
"width": 171.99359130859375,
|
||||
"height": 44.74725341796875,
|
||||
"seed": 126267060,
|
||||
"groupIds": [
|
||||
"ek-pq3umtz1yN-J_-preq"
|
||||
],
|
||||
"strokeSharpness": "sharp",
|
||||
"boundElementIds": [
|
||||
"wRO0q9xKPHc8e8XPPsQWh"
|
||||
],
|
||||
"updated": 1638348077724
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"version": 719,
|
||||
"versionNonce": 457402292,
|
||||
"isDeleted": false,
|
||||
"id": "we766A079lfGYu2_aC4Pl",
|
||||
"fillStyle": "hachure",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"angle": 0,
|
||||
"x": 70.69937133789062,
|
||||
"y": 237.33523559570312,
|
||||
"strokeColor": "#000000",
|
||||
"backgroundColor": "transparent",
|
||||
"width": 152,
|
||||
"height": 24,
|
||||
"seed": 478660236,
|
||||
"groupIds": [
|
||||
"ek-pq3umtz1yN-J_-preq"
|
||||
],
|
||||
"strokeSharpness": "sharp",
|
||||
"boundElementIds": [],
|
||||
"updated": 1638348077725,
|
||||
"fontSize": 20,
|
||||
"fontFamily": 3,
|
||||
"text": "vmselect:8481",
|
||||
"baseline": 19,
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "top"
|
||||
},
|
||||
{
|
||||
"type": "rectangle",
|
||||
"version": 1098,
|
||||
"versionNonce": 1480603788,
|
||||
"isDeleted": false,
|
||||
"id": "8-XFSbd6Zw96EUSJbJXZv",
|
||||
"fillStyle": "hachure",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"angle": 0,
|
||||
"x": 371.7434387207031,
|
||||
"y": 398.50787353515625,
|
||||
"strokeColor": "#000000",
|
||||
"backgroundColor": "transparent",
|
||||
"width": 240.10644531249997,
|
||||
"height": 44.74725341796875,
|
||||
"seed": 99322124,
|
||||
"groupIds": [
|
||||
"6obQBPHIfExBKfejeLLVO"
|
||||
],
|
||||
"strokeSharpness": "sharp",
|
||||
"boundElementIds": [
|
||||
"sxEhnxlbT7ldlSsmHDUHp"
|
||||
],
|
||||
"updated": 1638348083348
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"version": 864,
|
||||
"versionNonce": 1115813900,
|
||||
"isDeleted": false,
|
||||
"id": "GUs816aggGqUSdoEsSmea",
|
||||
"fillStyle": "hachure",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"angle": 0,
|
||||
"x": 393.73809814453125,
|
||||
"y": 410.5976257324219,
|
||||
"strokeColor": "#000000",
|
||||
"backgroundColor": "transparent",
|
||||
"width": 199,
|
||||
"height": 24,
|
||||
"seed": 1194745268,
|
||||
"groupIds": [
|
||||
"6obQBPHIfExBKfejeLLVO"
|
||||
],
|
||||
"strokeSharpness": "sharp",
|
||||
"boundElementIds": [],
|
||||
"updated": 1638348009180,
|
||||
"fontSize": 20,
|
||||
"fontFamily": 3,
|
||||
"text": "alertmanager:9093",
|
||||
"baseline": 19,
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "top"
|
||||
},
|
||||
{
|
||||
"type": "arrow",
|
||||
"version": 2405,
|
||||
"versionNonce": 959767732,
|
||||
"isDeleted": false,
|
||||
"id": "Bpy5by47XGKB4yS99ZkuA",
|
||||
"fillStyle": "hachure",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"angle": 0,
|
||||
"x": 556.6860961914062,
|
||||
"y": 252.2582773408825,
|
||||
"strokeColor": "#000000",
|
||||
"backgroundColor": "transparent",
|
||||
"width": 184.9195556640625,
|
||||
"height": 1.6022679018915937,
|
||||
"seed": 357577356,
|
||||
"groupIds": [],
|
||||
"strokeSharpness": "round",
|
||||
"boundElementIds": [],
|
||||
"updated": 1638348054411,
|
||||
"startBinding": {
|
||||
"elementId": "VgBUzo0blGR-Ijd2mQEEf",
|
||||
"focus": 0.0344528515859526,
|
||||
"gap": 10.57574462890625
|
||||
},
|
||||
"endBinding": {
|
||||
"elementId": "dd52BjHfPMPRji9Tws7U-",
|
||||
"focus": -0.039393828258510157,
|
||||
"gap": 8.72607421875
|
||||
},
|
||||
"lastCommittedPoint": null,
|
||||
"startArrowhead": null,
|
||||
"endArrowhead": "arrow",
|
||||
"points": [
|
||||
[
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
184.9195556640625,
|
||||
-1.6022679018915937
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "arrow",
|
||||
"version": 1173,
|
||||
"versionNonce": 1248255756,
|
||||
"isDeleted": false,
|
||||
"id": "wRO0q9xKPHc8e8XPPsQWh",
|
||||
"fillStyle": "hachure",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"angle": 0,
|
||||
"x": 406.0439244722469,
|
||||
"y": 246.80533728741074,
|
||||
"strokeColor": "#000000",
|
||||
"backgroundColor": "transparent",
|
||||
"width": 157.86774649373126,
|
||||
"height": 0.1417938392881979,
|
||||
"seed": 656189364,
|
||||
"groupIds": [],
|
||||
"strokeSharpness": "round",
|
||||
"boundElementIds": [],
|
||||
"updated": 1638348077725,
|
||||
"startBinding": {
|
||||
"elementId": "VgBUzo0blGR-Ijd2mQEEf",
|
||||
"focus": 0.13736472619498497,
|
||||
"gap": 16.306295254315614
|
||||
},
|
||||
"endBinding": {
|
||||
"elementId": "Sa4OBd1ZjD6itohm7Ll8z",
|
||||
"focus": -0.013200835330936087,
|
||||
"gap": 14.437713623046875
|
||||
},
|
||||
"lastCommittedPoint": null,
|
||||
"startArrowhead": null,
|
||||
"endArrowhead": "arrow",
|
||||
"points": [
|
||||
[
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
-157.86774649373126,
|
||||
0.1417938392881979
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"version": 557,
|
||||
"versionNonce": 995289780,
|
||||
"isDeleted": false,
|
||||
"id": "RbVSa4PnOgAMtzoKb-DhW",
|
||||
"fillStyle": "hachure",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"angle": 0,
|
||||
"x": 552.4987182617188,
|
||||
"y": 212.27996826171875,
|
||||
"strokeColor": "#000000",
|
||||
"backgroundColor": "transparent",
|
||||
"width": 188,
|
||||
"height": 76,
|
||||
"seed": 1989838604,
|
||||
"groupIds": [],
|
||||
"strokeSharpness": "sharp",
|
||||
"boundElementIds": [],
|
||||
"updated": 1638348059525,
|
||||
"fontSize": 16,
|
||||
"fontFamily": 3,
|
||||
"text": "persist alerts state\n\n\nand recording rules",
|
||||
"baseline": 72,
|
||||
"textAlign": "left",
|
||||
"verticalAlign": "top"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"version": 803,
|
||||
"versionNonce": 1576507444,
|
||||
"isDeleted": false,
|
||||
"id": "ia2QzZNl_tuvfY3ymLjyJ",
|
||||
"fillStyle": "hachure",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"angle": 0,
|
||||
"x": 290.34130859375,
|
||||
"y": 210.56927490234375,
|
||||
"strokeColor": "#000000",
|
||||
"backgroundColor": "transparent",
|
||||
"width": 122,
|
||||
"height": 19,
|
||||
"seed": 157304972,
|
||||
"groupIds": [],
|
||||
"strokeSharpness": "sharp",
|
||||
"boundElementIds": [
|
||||
"wRO0q9xKPHc8e8XPPsQWh"
|
||||
],
|
||||
"updated": 1638347948032,
|
||||
"fontSize": 16,
|
||||
"fontFamily": 3,
|
||||
"text": "execute rules",
|
||||
"baseline": 15,
|
||||
"textAlign": "left",
|
||||
"verticalAlign": "top"
|
||||
},
|
||||
{
|
||||
"type": "arrow",
|
||||
"version": 1471,
|
||||
"versionNonce": 1361321140,
|
||||
"isDeleted": false,
|
||||
"id": "sxEhnxlbT7ldlSsmHDUHp",
|
||||
"fillStyle": "hachure",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"angle": 0,
|
||||
"x": 484.18669893674246,
|
||||
"y": 302.3424013553929,
|
||||
"strokeColor": "#000000",
|
||||
"backgroundColor": "transparent",
|
||||
"width": 1.0484739253853945,
|
||||
"height": 84.72775855671654,
|
||||
"seed": 1818348300,
|
||||
"groupIds": [],
|
||||
"strokeSharpness": "round",
|
||||
"boundElementIds": [],
|
||||
"updated": 1638348083348,
|
||||
"startBinding": {
|
||||
"elementId": "VgBUzo0blGR-Ijd2mQEEf",
|
||||
"focus": 0.010768924644894236,
|
||||
"gap": 14.650750964767894
|
||||
},
|
||||
"endBinding": {
|
||||
"elementId": "8-XFSbd6Zw96EUSJbJXZv",
|
||||
"focus": -0.051051952959743775,
|
||||
"gap": 11.437713623046818
|
||||
},
|
||||
"lastCommittedPoint": null,
|
||||
"startArrowhead": null,
|
||||
"endArrowhead": "arrow",
|
||||
"points": [
|
||||
[
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
1.0484739253853945,
|
||||
84.72775855671654
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"version": 576,
|
||||
"versionNonce": 2112088460,
|
||||
"isDeleted": false,
|
||||
"id": "E9Run6wCm2chQ6JHrmc_y",
|
||||
"fillStyle": "hachure",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"angle": 0,
|
||||
"x": 530.5612182617188,
|
||||
"y": 318.60687255859375,
|
||||
"strokeColor": "#000000",
|
||||
"backgroundColor": "transparent",
|
||||
"width": 122,
|
||||
"height": 38,
|
||||
"seed": 1836541708,
|
||||
"groupIds": [],
|
||||
"strokeSharpness": "sharp",
|
||||
"boundElementIds": [
|
||||
"sxEhnxlbT7ldlSsmHDUHp"
|
||||
],
|
||||
"updated": 1638348023735,
|
||||
"fontSize": 16,
|
||||
"fontFamily": 3,
|
||||
"text": "send alert \nnotifications",
|
||||
"baseline": 34,
|
||||
"textAlign": "left",
|
||||
"verticalAlign": "top"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"version": 480,
|
||||
"versionNonce": 1835119500,
|
||||
"isDeleted": false,
|
||||
"id": "ff5OkfgmkKLifS13_TFj3",
|
||||
"fillStyle": "hachure",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"angle": 0,
|
||||
"x": 291.37474060058594,
|
||||
"y": 261.4861297607422,
|
||||
"strokeColor": "#000000",
|
||||
"backgroundColor": "transparent",
|
||||
"width": 122,
|
||||
"height": 19,
|
||||
"seed": 264004620,
|
||||
"groupIds": [],
|
||||
"strokeSharpness": "sharp",
|
||||
"boundElementIds": [
|
||||
"wRO0q9xKPHc8e8XPPsQWh"
|
||||
],
|
||||
"updated": 1638347948032,
|
||||
"fontSize": 16,
|
||||
"fontFamily": 3,
|
||||
"text": "restore state",
|
||||
"baseline": 15,
|
||||
"textAlign": "left",
|
||||
"verticalAlign": "top"
|
||||
}
|
||||
],
|
||||
"appState": {
|
||||
"gridSize": null,
|
||||
"viewBackgroundColor": "#ffffff"
|
||||
},
|
||||
"files": {}
|
||||
}
|
||||
BIN
app/vmalert/vmalert_cluster.png
Normal file
|
After Width: | Height: | Size: 73 KiB |
911
app/vmalert/vmalert_ha.excalidraw
Normal file
@@ -0,0 +1,911 @@
|
||||
{
|
||||
"type": "excalidraw",
|
||||
"version": 2,
|
||||
"source": "https://excalidraw.com",
|
||||
"elements": [
|
||||
{
|
||||
"type": "rectangle",
|
||||
"version": 906,
|
||||
"versionNonce": 448716468,
|
||||
"isDeleted": false,
|
||||
"id": "VgBUzo0blGR-Ijd2mQEEf",
|
||||
"fillStyle": "hachure",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"angle": 0,
|
||||
"x": 302.2676696777344,
|
||||
"y": 275.59356689453125,
|
||||
"strokeColor": "#000000",
|
||||
"backgroundColor": "transparent",
|
||||
"width": 123.7601318359375,
|
||||
"height": 72.13211059570312,
|
||||
"seed": 1194011660,
|
||||
"groupIds": [
|
||||
"iBaXgbpyifSwPplm_GO5b"
|
||||
],
|
||||
"strokeSharpness": "sharp",
|
||||
"boundElementIds": [
|
||||
"miEbzHxOPXe4PEYvXiJp5",
|
||||
"rcmiQfIWtfbTTlwxqr1sl",
|
||||
"P-dpWlSTtnsux-zr5oqgF",
|
||||
"oAToSPttH7aWoD_AqXGFX",
|
||||
"Bpy5by47XGKB4yS99ZkuA",
|
||||
"wRO0q9xKPHc8e8XPPsQWh",
|
||||
"sxEhnxlbT7ldlSsmHDUHp",
|
||||
"m9_BptFOFxbV2sS_xJDu2",
|
||||
"fsGFp4NW4JlrCdF0HR3uA",
|
||||
"OTKoeHmKtqCxFArDbY-sP"
|
||||
],
|
||||
"updated": 1638355221749
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"version": 762,
|
||||
"versionNonce": 223660724,
|
||||
"isDeleted": false,
|
||||
"id": "e9TDm09y-GhPm84XWt0Jv",
|
||||
"fillStyle": "hachure",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"angle": 0,
|
||||
"x": 318.0538024902344,
|
||||
"y": 296.6778106689453,
|
||||
"strokeColor": "#000000",
|
||||
"backgroundColor": "transparent",
|
||||
"width": 94,
|
||||
"height": 24,
|
||||
"seed": 327273100,
|
||||
"groupIds": [
|
||||
"iBaXgbpyifSwPplm_GO5b"
|
||||
],
|
||||
"strokeSharpness": "sharp",
|
||||
"boundElementIds": [
|
||||
"m9_BptFOFxbV2sS_xJDu2"
|
||||
],
|
||||
"updated": 1638355102070,
|
||||
"fontSize": 20,
|
||||
"fontFamily": 3,
|
||||
"text": "vmalert1",
|
||||
"baseline": 19,
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "top"
|
||||
},
|
||||
{
|
||||
"type": "rectangle",
|
||||
"version": 2067,
|
||||
"versionNonce": 817347852,
|
||||
"isDeleted": false,
|
||||
"id": "dd52BjHfPMPRji9Tws7U-",
|
||||
"fillStyle": "hachure",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"angle": 0,
|
||||
"x": 329.6426086425781,
|
||||
"y": 90.3275146484375,
|
||||
"strokeColor": "#000000",
|
||||
"backgroundColor": "transparent",
|
||||
"width": 269.336669921875,
|
||||
"height": 44.74725341796875,
|
||||
"seed": 1779959692,
|
||||
"groupIds": [
|
||||
"2Lijjn3PwPQW_8KrcDmdu"
|
||||
],
|
||||
"strokeSharpness": "sharp",
|
||||
"boundElementIds": [
|
||||
"Bpy5by47XGKB4yS99ZkuA",
|
||||
"m9_BptFOFxbV2sS_xJDu2",
|
||||
"S_dOHQrhGmu8SFJzobJK7"
|
||||
],
|
||||
"updated": 1638355058507
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"version": 1717,
|
||||
"versionNonce": 2040797452,
|
||||
"isDeleted": false,
|
||||
"id": "9TEzv0sVCHAkc46ou0oNF",
|
||||
"fillStyle": "hachure",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"angle": 0,
|
||||
"x": 349.01177978515625,
|
||||
"y": 102.45901489257812,
|
||||
"strokeColor": "#000000",
|
||||
"backgroundColor": "transparent",
|
||||
"width": 234,
|
||||
"height": 24,
|
||||
"seed": 1617178804,
|
||||
"groupIds": [
|
||||
"2Lijjn3PwPQW_8KrcDmdu"
|
||||
],
|
||||
"strokeSharpness": "sharp",
|
||||
"boundElementIds": [
|
||||
"m9_BptFOFxbV2sS_xJDu2",
|
||||
"S_dOHQrhGmu8SFJzobJK7"
|
||||
],
|
||||
"updated": 1638355040938,
|
||||
"fontSize": 20,
|
||||
"fontFamily": 3,
|
||||
"text": "victoriametrics:8428",
|
||||
"baseline": 19,
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "top"
|
||||
},
|
||||
{
|
||||
"type": "rectangle",
|
||||
"version": 1553,
|
||||
"versionNonce": 39381044,
|
||||
"isDeleted": false,
|
||||
"id": "8-XFSbd6Zw96EUSJbJXZv",
|
||||
"fillStyle": "hachure",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"angle": 0,
|
||||
"x": 173.67257690429688,
|
||||
"y": 470.5100402832031,
|
||||
"strokeColor": "#000000",
|
||||
"backgroundColor": "transparent",
|
||||
"width": 240.10644531249997,
|
||||
"height": 44.74725341796875,
|
||||
"seed": 99322124,
|
||||
"groupIds": [
|
||||
"6obQBPHIfExBKfejeLLVO"
|
||||
],
|
||||
"strokeSharpness": "sharp",
|
||||
"boundElementIds": [
|
||||
"sxEhnxlbT7ldlSsmHDUHp",
|
||||
"OTKoeHmKtqCxFArDbY-sP",
|
||||
"XhPgLRBk-YhWAFcSQi9TJ",
|
||||
"D6fkQH1E_MFbCuL697ArO"
|
||||
],
|
||||
"updated": 1638355221749
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"version": 1309,
|
||||
"versionNonce": 1112872844,
|
||||
"isDeleted": false,
|
||||
"id": "GUs816aggGqUSdoEsSmea",
|
||||
"fillStyle": "hachure",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"angle": 0,
|
||||
"x": 189.667236328125,
|
||||
"y": 482.59979248046875,
|
||||
"strokeColor": "#000000",
|
||||
"backgroundColor": "transparent",
|
||||
"width": 211,
|
||||
"height": 24,
|
||||
"seed": 1194745268,
|
||||
"groupIds": [
|
||||
"6obQBPHIfExBKfejeLLVO"
|
||||
],
|
||||
"strokeSharpness": "sharp",
|
||||
"boundElementIds": [],
|
||||
"updated": 1638355040938,
|
||||
"fontSize": 20,
|
||||
"fontFamily": 3,
|
||||
"text": "alertmanager1:9093",
|
||||
"baseline": 19,
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "top"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"version": 835,
|
||||
"versionNonce": 2036483596,
|
||||
"isDeleted": false,
|
||||
"id": "RbVSa4PnOgAMtzoKb-DhW",
|
||||
"fillStyle": "hachure",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"angle": 0,
|
||||
"x": 525.83837890625,
|
||||
"y": 147.33470153808594,
|
||||
"strokeColor": "#000000",
|
||||
"backgroundColor": "transparent",
|
||||
"width": 188,
|
||||
"height": 95,
|
||||
"seed": 1989838604,
|
||||
"groupIds": [],
|
||||
"strokeSharpness": "sharp",
|
||||
"boundElementIds": [],
|
||||
"updated": 1638355040939,
|
||||
"fontSize": 16,
|
||||
"fontFamily": 3,
|
||||
"text": "execute rules,\npersist alerts \nand recording rules,\nrestore state\n",
|
||||
"baseline": 91,
|
||||
"textAlign": "left",
|
||||
"verticalAlign": "top"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"version": 777,
|
||||
"versionNonce": 990468492,
|
||||
"isDeleted": false,
|
||||
"id": "E9Run6wCm2chQ6JHrmc_y",
|
||||
"fillStyle": "hachure",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"angle": 0,
|
||||
"x": 617.0690307617188,
|
||||
"y": 376.9822692871094,
|
||||
"strokeColor": "#000000",
|
||||
"backgroundColor": "transparent",
|
||||
"width": 122,
|
||||
"height": 38,
|
||||
"seed": 1836541708,
|
||||
"groupIds": [],
|
||||
"strokeSharpness": "sharp",
|
||||
"boundElementIds": [
|
||||
"sxEhnxlbT7ldlSsmHDUHp"
|
||||
],
|
||||
"updated": 1638355227582,
|
||||
"fontSize": 16,
|
||||
"fontFamily": 3,
|
||||
"text": "send alert \nnotifications",
|
||||
"baseline": 34,
|
||||
"textAlign": "left",
|
||||
"verticalAlign": "top"
|
||||
},
|
||||
{
|
||||
"type": "rectangle",
|
||||
"version": 1112,
|
||||
"versionNonce": 999136652,
|
||||
"isDeleted": false,
|
||||
"id": "mIu-d0lmShCxzMLD5iA_p",
|
||||
"fillStyle": "hachure",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"angle": 0,
|
||||
"x": 492.29505920410156,
|
||||
"y": 272.3052215576172,
|
||||
"strokeColor": "#000000",
|
||||
"backgroundColor": "transparent",
|
||||
"width": 123.7601318359375,
|
||||
"height": 72.13211059570312,
|
||||
"seed": 756416780,
|
||||
"groupIds": [
|
||||
"jbBot-UNdMoWy2jPXC1u5"
|
||||
],
|
||||
"strokeSharpness": "sharp",
|
||||
"boundElementIds": [
|
||||
"miEbzHxOPXe4PEYvXiJp5",
|
||||
"rcmiQfIWtfbTTlwxqr1sl",
|
||||
"P-dpWlSTtnsux-zr5oqgF",
|
||||
"oAToSPttH7aWoD_AqXGFX",
|
||||
"Bpy5by47XGKB4yS99ZkuA",
|
||||
"wRO0q9xKPHc8e8XPPsQWh",
|
||||
"sxEhnxlbT7ldlSsmHDUHp",
|
||||
"S_dOHQrhGmu8SFJzobJK7",
|
||||
"XhPgLRBk-YhWAFcSQi9TJ",
|
||||
"Ar-hcDLlzVSoTPs2MaywO"
|
||||
],
|
||||
"updated": 1638355153683
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"version": 970,
|
||||
"versionNonce": 430760500,
|
||||
"isDeleted": false,
|
||||
"id": "ZqIR6SaLNDQl8s0zZbdnE",
|
||||
"fillStyle": "hachure",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"angle": 0,
|
||||
"x": 508.08119201660156,
|
||||
"y": 293.38946533203125,
|
||||
"strokeColor": "#000000",
|
||||
"backgroundColor": "transparent",
|
||||
"width": 94,
|
||||
"height": 24,
|
||||
"seed": 1477404084,
|
||||
"groupIds": [
|
||||
"jbBot-UNdMoWy2jPXC1u5"
|
||||
],
|
||||
"strokeSharpness": "sharp",
|
||||
"boundElementIds": [],
|
||||
"updated": 1638355105020,
|
||||
"fontSize": 20,
|
||||
"fontFamily": 3,
|
||||
"text": "vmalertN",
|
||||
"baseline": 19,
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "top"
|
||||
},
|
||||
{
|
||||
"id": "qZFezRGU4Chwxgvb2451t",
|
||||
"type": "line",
|
||||
"x": 449.93653869628906,
|
||||
"y": 306.30010986328125,
|
||||
"width": 19.48321533203125,
|
||||
"height": 0.3865966796875,
|
||||
"angle": 0,
|
||||
"strokeColor": "#000000",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "hachure",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "dotted",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"strokeSharpness": "round",
|
||||
"seed": 1833426828,
|
||||
"version": 69,
|
||||
"versionNonce": 871948300,
|
||||
"isDeleted": false,
|
||||
"boundElementIds": null,
|
||||
"updated": 1638355040939,
|
||||
"points": [
|
||||
[
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
19.48321533203125,
|
||||
0.3865966796875
|
||||
]
|
||||
],
|
||||
"lastCommittedPoint": null,
|
||||
"startBinding": null,
|
||||
"endBinding": null,
|
||||
"startArrowhead": null,
|
||||
"endArrowhead": null
|
||||
},
|
||||
{
|
||||
"type": "rectangle",
|
||||
"version": 1649,
|
||||
"versionNonce": 93981708,
|
||||
"isDeleted": false,
|
||||
"id": "gNHiZJKo0ap69ALDobtZ-",
|
||||
"fillStyle": "hachure",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"angle": 0,
|
||||
"x": 518.5596466064453,
|
||||
"y": 471.4539489746094,
|
||||
"strokeColor": "#000000",
|
||||
"backgroundColor": "transparent",
|
||||
"width": 240.10644531249997,
|
||||
"height": 44.74725341796875,
|
||||
"seed": 454422412,
|
||||
"groupIds": [
|
||||
"fEAIeQ0DxLnI_rPlKPZqW"
|
||||
],
|
||||
"strokeSharpness": "sharp",
|
||||
"boundElementIds": [
|
||||
"sxEhnxlbT7ldlSsmHDUHp",
|
||||
"fsGFp4NW4JlrCdF0HR3uA",
|
||||
"Ar-hcDLlzVSoTPs2MaywO",
|
||||
"D6fkQH1E_MFbCuL697ArO"
|
||||
],
|
||||
"updated": 1638355153683
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"version": 1412,
|
||||
"versionNonce": 75038348,
|
||||
"isDeleted": false,
|
||||
"id": "O-zgjZBvt4RI1PrkBNQnb",
|
||||
"fillStyle": "hachure",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"angle": 0,
|
||||
"x": 534.3148040771484,
|
||||
"y": 483.543701171875,
|
||||
"strokeColor": "#000000",
|
||||
"backgroundColor": "transparent",
|
||||
"width": 211,
|
||||
"height": 24,
|
||||
"seed": 1026268980,
|
||||
"groupIds": [
|
||||
"fEAIeQ0DxLnI_rPlKPZqW"
|
||||
],
|
||||
"strokeSharpness": "sharp",
|
||||
"boundElementIds": [],
|
||||
"updated": 1638355040939,
|
||||
"fontSize": 20,
|
||||
"fontFamily": 3,
|
||||
"text": "alertmanagerN:9093",
|
||||
"baseline": 19,
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "top"
|
||||
},
|
||||
{
|
||||
"id": "m9_BptFOFxbV2sS_xJDu2",
|
||||
"type": "arrow",
|
||||
"x": 378.57185562792466,
|
||||
"y": 262.1596984863281,
|
||||
"width": 79.96146539019026,
|
||||
"height": 111.53411865234375,
|
||||
"angle": 0,
|
||||
"strokeColor": "#000000",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "hachure",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"strokeSharpness": "round",
|
||||
"seed": 141224884,
|
||||
"version": 521,
|
||||
"versionNonce": 283254540,
|
||||
"isDeleted": false,
|
||||
"boundElementIds": null,
|
||||
"updated": 1638355102070,
|
||||
"points": [
|
||||
[
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
79.96146539019026,
|
||||
-111.53411865234375
|
||||
]
|
||||
],
|
||||
"lastCommittedPoint": null,
|
||||
"startBinding": {
|
||||
"elementId": "VgBUzo0blGR-Ijd2mQEEf",
|
||||
"focus": -0.23996018586441184,
|
||||
"gap": 13.433868408203125
|
||||
},
|
||||
"endBinding": {
|
||||
"elementId": "dd52BjHfPMPRji9Tws7U-",
|
||||
"focus": -0.14207100087207922,
|
||||
"gap": 15.550811767578125
|
||||
},
|
||||
"startArrowhead": null,
|
||||
"endArrowhead": "arrow"
|
||||
},
|
||||
{
|
||||
"id": "S_dOHQrhGmu8SFJzobJK7",
|
||||
"type": "arrow",
|
||||
"x": 558.1990515577129,
|
||||
"y": 263.4271240234375,
|
||||
"width": 88.18940317574186,
|
||||
"height": 114.15780639648438,
|
||||
"angle": 0,
|
||||
"strokeColor": "#000000",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "hachure",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"strokeSharpness": "round",
|
||||
"seed": 1004789812,
|
||||
"version": 314,
|
||||
"versionNonce": 1259171724,
|
||||
"isDeleted": false,
|
||||
"boundElementIds": null,
|
||||
"updated": 1638355105019,
|
||||
"points": [
|
||||
[
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
-88.18940317574186,
|
||||
-114.15780639648438
|
||||
]
|
||||
],
|
||||
"lastCommittedPoint": null,
|
||||
"startBinding": {
|
||||
"elementId": "mIu-d0lmShCxzMLD5iA_p",
|
||||
"focus": 0.4315094049079832,
|
||||
"gap": 8.878097534179688
|
||||
},
|
||||
"endBinding": {
|
||||
"elementId": "dd52BjHfPMPRji9Tws7U-",
|
||||
"focus": 0.14840834302306677,
|
||||
"gap": 14.194549560546875
|
||||
},
|
||||
"startArrowhead": null,
|
||||
"endArrowhead": "arrow"
|
||||
},
|
||||
{
|
||||
"id": "fsGFp4NW4JlrCdF0HR3uA",
|
||||
"type": "arrow",
|
||||
"x": 356.6613630516658,
|
||||
"y": 354.95416259765625,
|
||||
"width": 278.74351860741064,
|
||||
"height": 106.90020751953125,
|
||||
"angle": 0,
|
||||
"strokeColor": "#000000",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "hachure",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"strokeSharpness": "round",
|
||||
"seed": 712384692,
|
||||
"version": 334,
|
||||
"versionNonce": 266493324,
|
||||
"isDeleted": false,
|
||||
"boundElementIds": null,
|
||||
"updated": 1638355102070,
|
||||
"points": [
|
||||
[
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
278.74351860741064,
|
||||
106.90020751953125
|
||||
]
|
||||
],
|
||||
"lastCommittedPoint": null,
|
||||
"startBinding": {
|
||||
"elementId": "VgBUzo0blGR-Ijd2mQEEf",
|
||||
"focus": 0.7721210277890177,
|
||||
"gap": 7.228485107421875
|
||||
},
|
||||
"endBinding": {
|
||||
"elementId": "gNHiZJKo0ap69ALDobtZ-",
|
||||
"focus": 0.4493598001028791,
|
||||
"gap": 9.599578857421875
|
||||
},
|
||||
"startArrowhead": null,
|
||||
"endArrowhead": "arrow"
|
||||
},
|
||||
{
|
||||
"id": "OTKoeHmKtqCxFArDbY-sP",
|
||||
"type": "arrow",
|
||||
"x": 361.02563221226757,
|
||||
"y": 356.2252197265625,
|
||||
"width": 95.3594006000182,
|
||||
"height": 105.52130126953125,
|
||||
"angle": 0,
|
||||
"strokeColor": "#000000",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "hachure",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"strokeSharpness": "round",
|
||||
"seed": 1992252596,
|
||||
"version": 466,
|
||||
"versionNonce": 472553100,
|
||||
"isDeleted": false,
|
||||
"boundElementIds": null,
|
||||
"updated": 1638355221749,
|
||||
"points": [
|
||||
[
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
-95.3594006000182,
|
||||
105.52130126953125
|
||||
]
|
||||
],
|
||||
"lastCommittedPoint": null,
|
||||
"startBinding": {
|
||||
"elementId": "VgBUzo0blGR-Ijd2mQEEf",
|
||||
"focus": -0.39325294425055324,
|
||||
"gap": 8.499542236328125
|
||||
},
|
||||
"endBinding": {
|
||||
"elementId": "8-XFSbd6Zw96EUSJbJXZv",
|
||||
"focus": -0.400636314282374,
|
||||
"gap": 8.763519287109375
|
||||
},
|
||||
"startArrowhead": null,
|
||||
"endArrowhead": "arrow"
|
||||
},
|
||||
{
|
||||
"id": "XhPgLRBk-YhWAFcSQi9TJ",
|
||||
"type": "arrow",
|
||||
"x": 561.7189961327852,
|
||||
"y": 349.238037109375,
|
||||
"width": 266.271510370216,
|
||||
"height": 112.383544921875,
|
||||
"angle": 0,
|
||||
"strokeColor": "#000000",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "hachure",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"strokeSharpness": "round",
|
||||
"seed": 588869260,
|
||||
"version": 453,
|
||||
"versionNonce": 584723212,
|
||||
"isDeleted": false,
|
||||
"boundElementIds": null,
|
||||
"updated": 1638355150131,
|
||||
"points": [
|
||||
[
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
-266.271510370216,
|
||||
112.383544921875
|
||||
]
|
||||
],
|
||||
"lastCommittedPoint": null,
|
||||
"startBinding": {
|
||||
"elementId": "mIu-d0lmShCxzMLD5iA_p",
|
||||
"focus": -0.7084007026732048,
|
||||
"gap": 4.8007049560546875
|
||||
},
|
||||
"endBinding": {
|
||||
"elementId": "8-XFSbd6Zw96EUSJbJXZv",
|
||||
"focus": -0.4180430111580123,
|
||||
"gap": 8.888458251953125
|
||||
},
|
||||
"startArrowhead": null,
|
||||
"endArrowhead": "arrow"
|
||||
},
|
||||
{
|
||||
"id": "Ar-hcDLlzVSoTPs2MaywO",
|
||||
"type": "arrow",
|
||||
"x": 557.5999880535809,
|
||||
"y": 352.71795654296875,
|
||||
"width": 112.87813113656136,
|
||||
"height": 106.3255615234375,
|
||||
"angle": 0,
|
||||
"strokeColor": "#000000",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "hachure",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"strokeSharpness": "round",
|
||||
"seed": 1508782476,
|
||||
"version": 331,
|
||||
"versionNonce": 1137561908,
|
||||
"isDeleted": false,
|
||||
"boundElementIds": null,
|
||||
"updated": 1638355153683,
|
||||
"points": [
|
||||
[
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
112.87813113656136,
|
||||
106.3255615234375
|
||||
]
|
||||
],
|
||||
"lastCommittedPoint": null,
|
||||
"startBinding": {
|
||||
"elementId": "mIu-d0lmShCxzMLD5iA_p",
|
||||
"focus": 0.4358123206211808,
|
||||
"gap": 8.280624389648438
|
||||
},
|
||||
"endBinding": {
|
||||
"elementId": "gNHiZJKo0ap69ALDobtZ-",
|
||||
"focus": 0.4783744286829653,
|
||||
"gap": 12.410430908203125
|
||||
},
|
||||
"startArrowhead": null,
|
||||
"endArrowhead": "arrow"
|
||||
},
|
||||
{
|
||||
"id": "WqHnv9_g3SdkLZuy4WSHa",
|
||||
"type": "text",
|
||||
"x": 186.5891876220703,
|
||||
"y": 523.145263671875,
|
||||
"width": 272,
|
||||
"height": 19,
|
||||
"angle": 0,
|
||||
"strokeColor": "#000000",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "hachure",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"strokeSharpness": "sharp",
|
||||
"seed": 1310147468,
|
||||
"version": 281,
|
||||
"versionNonce": 949157044,
|
||||
"isDeleted": false,
|
||||
"boundElementIds": null,
|
||||
"updated": 1638355040939,
|
||||
"text": "Alertmanagers in cluster mode",
|
||||
"fontSize": 16,
|
||||
"fontFamily": 3,
|
||||
"textAlign": "left",
|
||||
"verticalAlign": "top",
|
||||
"baseline": 15
|
||||
},
|
||||
{
|
||||
"id": "yA0kgvdF71wZbHJ7Cg2p8",
|
||||
"type": "rectangle",
|
||||
"x": 159.41685485839844,
|
||||
"y": 440.7666015625,
|
||||
"width": 625.9590148925781,
|
||||
"height": 110.13494873046878,
|
||||
"angle": 0,
|
||||
"strokeColor": "#000000",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "hachure",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "dotted",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"strokeSharpness": "sharp",
|
||||
"seed": 1465986996,
|
||||
"version": 366,
|
||||
"versionNonce": 196040844,
|
||||
"isDeleted": false,
|
||||
"boundElementIds": null,
|
||||
"updated": 1638355040939
|
||||
},
|
||||
{
|
||||
"id": "D6fkQH1E_MFbCuL697ArO",
|
||||
"type": "arrow",
|
||||
"x": 422.4853057861328,
|
||||
"y": 494.8779132311229,
|
||||
"width": 90.5517578125,
|
||||
"height": 0.12427004064892344,
|
||||
"angle": 0,
|
||||
"strokeColor": "#000000",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "hachure",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"strokeSharpness": "round",
|
||||
"seed": 2025208204,
|
||||
"version": 316,
|
||||
"versionNonce": 2144102156,
|
||||
"isDeleted": false,
|
||||
"boundElementIds": null,
|
||||
"updated": 1638355040939,
|
||||
"points": [
|
||||
[
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
90.5517578125,
|
||||
0.12427004064892344
|
||||
]
|
||||
],
|
||||
"lastCommittedPoint": null,
|
||||
"startBinding": {
|
||||
"elementId": "8-XFSbd6Zw96EUSJbJXZv",
|
||||
"focus": 0.08064204025505255,
|
||||
"gap": 8.706283569335938
|
||||
},
|
||||
"endBinding": {
|
||||
"elementId": "gNHiZJKo0ap69ALDobtZ-",
|
||||
"focus": -0.05976220061952895,
|
||||
"gap": 5.5225830078125
|
||||
},
|
||||
"startArrowhead": "arrow",
|
||||
"endArrowhead": "arrow"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"version": 463,
|
||||
"versionNonce": 2118914484,
|
||||
"isDeleted": false,
|
||||
"id": "p4EDvSKPqZzfM_SReybeq",
|
||||
"fillStyle": "hachure",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"angle": 0,
|
||||
"x": 274.6086883544922,
|
||||
"y": 56.77886962890625,
|
||||
"strokeColor": "#000000",
|
||||
"backgroundColor": "transparent",
|
||||
"width": 394,
|
||||
"height": 19,
|
||||
"seed": 1214462348,
|
||||
"groupIds": [],
|
||||
"strokeSharpness": "sharp",
|
||||
"boundElementIds": [],
|
||||
"updated": 1638355074872,
|
||||
"fontSize": 16,
|
||||
"fontFamily": 3,
|
||||
"text": "VictoriaMetrics with deduplication enabled",
|
||||
"baseline": 15,
|
||||
"textAlign": "left",
|
||||
"verticalAlign": "top"
|
||||
},
|
||||
{
|
||||
"type": "rectangle",
|
||||
"version": 712,
|
||||
"versionNonce": 737833612,
|
||||
"isDeleted": false,
|
||||
"id": "YfNFeOucMo8BJk2P2JRex",
|
||||
"fillStyle": "hachure",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "dotted",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"angle": 0,
|
||||
"x": 229.77923583984375,
|
||||
"y": 227.84378051757812,
|
||||
"strokeColor": "#000000",
|
||||
"backgroundColor": "transparent",
|
||||
"width": 448.5542297363281,
|
||||
"height": 134.06329345703122,
|
||||
"seed": 932207756,
|
||||
"groupIds": [],
|
||||
"strokeSharpness": "sharp",
|
||||
"boundElementIds": [],
|
||||
"updated": 1638355133244
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"version": 427,
|
||||
"versionNonce": 1458527796,
|
||||
"isDeleted": false,
|
||||
"id": "oldM7Q_aJtpWd2jXQ0iKf",
|
||||
"fillStyle": "hachure",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"angle": 0,
|
||||
"x": 238.9442901611328,
|
||||
"y": 230.862548828125,
|
||||
"strokeColor": "#000000",
|
||||
"backgroundColor": "transparent",
|
||||
"width": 225,
|
||||
"height": 38,
|
||||
"seed": 2131899572,
|
||||
"groupIds": [],
|
||||
"strokeSharpness": "sharp",
|
||||
"boundElementIds": [],
|
||||
"updated": 1638355259358,
|
||||
"fontSize": 16,
|
||||
"fontFamily": 3,
|
||||
"text": "vmalerts with identical \nconfigurations",
|
||||
"baseline": 34,
|
||||
"textAlign": "left",
|
||||
"verticalAlign": "top"
|
||||
}
|
||||
],
|
||||
"appState": {
|
||||
"gridSize": null,
|
||||
"viewBackgroundColor": "#ffffff"
|
||||
},
|
||||
"files": {}
|
||||
}
|
||||
BIN
app/vmalert/vmalert_ha.png
Normal file
|
After Width: | Height: | Size: 151 KiB |
915
app/vmalert/vmalert_multicluster.excalidraw
Normal file
@@ -0,0 +1,915 @@
|
||||
{
|
||||
"type": "excalidraw",
|
||||
"version": 2,
|
||||
"source": "https://excalidraw.com",
|
||||
"elements": [
|
||||
{
|
||||
"type": "rectangle",
|
||||
"version": 791,
|
||||
"versionNonce": 48874036,
|
||||
"isDeleted": false,
|
||||
"id": "VgBUzo0blGR-Ijd2mQEEf",
|
||||
"fillStyle": "hachure",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"angle": 0,
|
||||
"x": 289.6802978515625,
|
||||
"y": 399.3895568847656,
|
||||
"strokeColor": "#000000",
|
||||
"backgroundColor": "transparent",
|
||||
"width": 123.7601318359375,
|
||||
"height": 72.13211059570312,
|
||||
"seed": 1194011660,
|
||||
"groupIds": [
|
||||
"iBaXgbpyifSwPplm_GO5b"
|
||||
],
|
||||
"strokeSharpness": "sharp",
|
||||
"boundElementIds": [
|
||||
"miEbzHxOPXe4PEYvXiJp5",
|
||||
"rcmiQfIWtfbTTlwxqr1sl",
|
||||
"P-dpWlSTtnsux-zr5oqgF",
|
||||
"oAToSPttH7aWoD_AqXGFX",
|
||||
"wRO0q9xKPHc8e8XPPsQWh",
|
||||
"sxEhnxlbT7ldlSsmHDUHp",
|
||||
"pD9DcILMxa6GaR1U5YyMO",
|
||||
"HPEwr85wL4IedW0AgdArp",
|
||||
"EyecK0YM9Cc8T6ju-nTOc"
|
||||
],
|
||||
"updated": 1638347812431
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"version": 658,
|
||||
"versionNonce": 1653816076,
|
||||
"isDeleted": false,
|
||||
"id": "e9TDm09y-GhPm84XWt0Jv",
|
||||
"fillStyle": "hachure",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"angle": 0,
|
||||
"x": 311.22686767578125,
|
||||
"y": 420.4738006591797,
|
||||
"strokeColor": "#000000",
|
||||
"backgroundColor": "transparent",
|
||||
"width": 82,
|
||||
"height": 24,
|
||||
"seed": 327273100,
|
||||
"groupIds": [
|
||||
"iBaXgbpyifSwPplm_GO5b"
|
||||
],
|
||||
"strokeSharpness": "sharp",
|
||||
"boundElementIds": [],
|
||||
"updated": 1638347796775,
|
||||
"fontSize": 20,
|
||||
"fontFamily": 3,
|
||||
"text": "vmalert",
|
||||
"baseline": 19,
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "middle"
|
||||
},
|
||||
{
|
||||
"type": "rectangle",
|
||||
"version": 802,
|
||||
"versionNonce": 995326644,
|
||||
"isDeleted": false,
|
||||
"id": "Sa4OBd1ZjD6itohm7Ll8z",
|
||||
"fillStyle": "hachure",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"angle": 0,
|
||||
"x": 603.05322265625,
|
||||
"y": 228.65371704101562,
|
||||
"strokeColor": "#000000",
|
||||
"backgroundColor": "transparent",
|
||||
"width": 171.99359130859375,
|
||||
"height": 44.74725341796875,
|
||||
"seed": 126267060,
|
||||
"groupIds": [
|
||||
"ek-pq3umtz1yN-J_-preq"
|
||||
],
|
||||
"strokeSharpness": "sharp",
|
||||
"boundElementIds": [
|
||||
"wRO0q9xKPHc8e8XPPsQWh",
|
||||
"he-SpFjCxEQEWpWny2kKP",
|
||||
"-pjrKo16rOsasM8viZPJ-",
|
||||
"MGdu6GDIPNBAaEUr0Gt-a"
|
||||
],
|
||||
"updated": 1638347737174
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"version": 624,
|
||||
"versionNonce": 707755700,
|
||||
"isDeleted": false,
|
||||
"id": "we766A079lfGYu2_aC4Pl",
|
||||
"fillStyle": "hachure",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"angle": 0,
|
||||
"x": 640.7635803222656,
|
||||
"y": 241.02886962890625,
|
||||
"strokeColor": "#000000",
|
||||
"backgroundColor": "transparent",
|
||||
"width": 94,
|
||||
"height": 24,
|
||||
"seed": 478660236,
|
||||
"groupIds": [
|
||||
"ek-pq3umtz1yN-J_-preq"
|
||||
],
|
||||
"strokeSharpness": "sharp",
|
||||
"boundElementIds": [],
|
||||
"updated": 1638347701075,
|
||||
"fontSize": 20,
|
||||
"fontFamily": 3,
|
||||
"text": "vmselect",
|
||||
"baseline": 19,
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "top"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"version": 802,
|
||||
"versionNonce": 1974403340,
|
||||
"isDeleted": false,
|
||||
"id": "ia2QzZNl_tuvfY3ymLjyJ",
|
||||
"fillStyle": "hachure",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"angle": 0,
|
||||
"x": 401.241943359375,
|
||||
"y": 342.4627990722656,
|
||||
"strokeColor": "#000000",
|
||||
"backgroundColor": "transparent",
|
||||
"width": 178,
|
||||
"height": 38,
|
||||
"seed": 157304972,
|
||||
"groupIds": [],
|
||||
"strokeSharpness": "sharp",
|
||||
"boundElementIds": [
|
||||
"wRO0q9xKPHc8e8XPPsQWh"
|
||||
],
|
||||
"updated": 1638347863144,
|
||||
"fontSize": 16,
|
||||
"fontFamily": 3,
|
||||
"text": "execute aggregating\nrecording rules",
|
||||
"baseline": 34,
|
||||
"textAlign": "left",
|
||||
"verticalAlign": "top"
|
||||
},
|
||||
{
|
||||
"id": "jrFZeFNsxlss7IsEZpA-J",
|
||||
"type": "rectangle",
|
||||
"x": 594.1509857177734,
|
||||
"y": 192.09194946289062,
|
||||
"width": 397.1286010742188,
|
||||
"height": 209.62742614746097,
|
||||
"angle": 0,
|
||||
"strokeColor": "#000000",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "hachure",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "dotted",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"strokeSharpness": "sharp",
|
||||
"seed": 48379060,
|
||||
"version": 665,
|
||||
"versionNonce": 617456652,
|
||||
"isDeleted": false,
|
||||
"boundElementIds": [
|
||||
"pD9DcILMxa6GaR1U5YyMO"
|
||||
],
|
||||
"updated": 1638347763716
|
||||
},
|
||||
{
|
||||
"id": "6ibhLp94HJFIdfP5HCEv8",
|
||||
"type": "text",
|
||||
"x": 609.7205657958984,
|
||||
"y": 199.81723022460938,
|
||||
"width": 225,
|
||||
"height": 19,
|
||||
"angle": 0,
|
||||
"strokeColor": "#000000",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "hachure",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "dotted",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"strokeSharpness": "sharp",
|
||||
"seed": 1053164852,
|
||||
"version": 256,
|
||||
"versionNonce": 538595124,
|
||||
"isDeleted": false,
|
||||
"boundElementIds": null,
|
||||
"updated": 1638347761092,
|
||||
"text": "VM cluster with raw data",
|
||||
"fontSize": 16,
|
||||
"fontFamily": 3,
|
||||
"textAlign": "left",
|
||||
"verticalAlign": "top",
|
||||
"baseline": 15
|
||||
},
|
||||
{
|
||||
"type": "rectangle",
|
||||
"version": 914,
|
||||
"versionNonce": 1576666164,
|
||||
"isDeleted": false,
|
||||
"id": "R5v-yZCVJ97BkJqr0Qb7H",
|
||||
"fillStyle": "hachure",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"angle": 0,
|
||||
"x": 807.5664215087891,
|
||||
"y": 287.8628387451172,
|
||||
"strokeColor": "#000000",
|
||||
"backgroundColor": "transparent",
|
||||
"width": 171.99359130859375,
|
||||
"height": 44.74725341796875,
|
||||
"seed": 1794212620,
|
||||
"groupIds": [
|
||||
"BJNOAY1MY3Evr9B3qQtHf"
|
||||
],
|
||||
"strokeSharpness": "sharp",
|
||||
"boundElementIds": [
|
||||
"wRO0q9xKPHc8e8XPPsQWh",
|
||||
"he-SpFjCxEQEWpWny2kKP",
|
||||
"-pjrKo16rOsasM8viZPJ-",
|
||||
"bZXA8PH9gYu-clotqJ4f7",
|
||||
"MGdu6GDIPNBAaEUr0Gt-a"
|
||||
],
|
||||
"updated": 1638347737174
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"version": 725,
|
||||
"versionNonce": 267455500,
|
||||
"isDeleted": false,
|
||||
"id": "pWRC_smX7TuOI8_8UrA4H",
|
||||
"fillStyle": "hachure",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"angle": 0,
|
||||
"x": 840.0209197998047,
|
||||
"y": 300.2379913330078,
|
||||
"strokeColor": "#000000",
|
||||
"backgroundColor": "transparent",
|
||||
"width": 105,
|
||||
"height": 24,
|
||||
"seed": 421856180,
|
||||
"groupIds": [
|
||||
"BJNOAY1MY3Evr9B3qQtHf"
|
||||
],
|
||||
"strokeSharpness": "sharp",
|
||||
"boundElementIds": [],
|
||||
"updated": 1638347701076,
|
||||
"fontSize": 20,
|
||||
"fontFamily": 3,
|
||||
"text": "vmstorage",
|
||||
"baseline": 19,
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "top"
|
||||
},
|
||||
{
|
||||
"type": "rectangle",
|
||||
"version": 843,
|
||||
"versionNonce": 1244193972,
|
||||
"isDeleted": false,
|
||||
"id": "EqROOfYulSPsZm7ovxfQN",
|
||||
"fillStyle": "hachure",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"angle": 0,
|
||||
"x": 604.9091339111328,
|
||||
"y": 345.2847442626953,
|
||||
"strokeColor": "#000000",
|
||||
"backgroundColor": "transparent",
|
||||
"width": 171.99359130859375,
|
||||
"height": 44.74725341796875,
|
||||
"seed": 2043521972,
|
||||
"groupIds": [
|
||||
"ls6uq-W9bbVBM_UxAuyba"
|
||||
],
|
||||
"strokeSharpness": "sharp",
|
||||
"boundElementIds": [
|
||||
"wRO0q9xKPHc8e8XPPsQWh",
|
||||
"bZXA8PH9gYu-clotqJ4f7"
|
||||
],
|
||||
"updated": 1638347718537
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"version": 676,
|
||||
"versionNonce": 143370892,
|
||||
"isDeleted": false,
|
||||
"id": "ddQH1nnmT7HbKW7Xmv4zx",
|
||||
"fillStyle": "hachure",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"angle": 0,
|
||||
"x": 642.6199798583984,
|
||||
"y": 357.90354919433594,
|
||||
"strokeColor": "#000000",
|
||||
"backgroundColor": "transparent",
|
||||
"width": 94,
|
||||
"height": 24,
|
||||
"seed": 335223180,
|
||||
"groupIds": [
|
||||
"ls6uq-W9bbVBM_UxAuyba"
|
||||
],
|
||||
"strokeSharpness": "sharp",
|
||||
"boundElementIds": [
|
||||
"bZXA8PH9gYu-clotqJ4f7"
|
||||
],
|
||||
"updated": 1638347701076,
|
||||
"fontSize": 20,
|
||||
"fontFamily": 3,
|
||||
"text": "vminsert",
|
||||
"baseline": 19,
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "top"
|
||||
},
|
||||
{
|
||||
"id": "bZXA8PH9gYu-clotqJ4f7",
|
||||
"type": "arrow",
|
||||
"x": 785.2667708463345,
|
||||
"y": 367.2342882272049,
|
||||
"width": 99.87819315552736,
|
||||
"height": 24.681162491468,
|
||||
"angle": 0,
|
||||
"strokeColor": "#000000",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "hachure",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"strokeSharpness": "round",
|
||||
"seed": 1153029388,
|
||||
"version": 420,
|
||||
"versionNonce": 1726025228,
|
||||
"isDeleted": false,
|
||||
"boundElementIds": null,
|
||||
"updated": 1638347704644,
|
||||
"points": [
|
||||
[
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
99.87819315552736,
|
||||
-24.681162491468
|
||||
]
|
||||
],
|
||||
"lastCommittedPoint": null,
|
||||
"startBinding": {
|
||||
"elementId": "EqROOfYulSPsZm7ovxfQN",
|
||||
"focus": 0.5247890891198189,
|
||||
"gap": 8.36404562660789
|
||||
},
|
||||
"endBinding": {
|
||||
"elementId": "R5v-yZCVJ97BkJqr0Qb7H",
|
||||
"focus": -0.6931056940383433,
|
||||
"gap": 9.943033572650961
|
||||
},
|
||||
"startArrowhead": null,
|
||||
"endArrowhead": "arrow"
|
||||
},
|
||||
{
|
||||
"type": "arrow",
|
||||
"version": 528,
|
||||
"versionNonce": 1908259468,
|
||||
"isDeleted": false,
|
||||
"id": "MGdu6GDIPNBAaEUr0Gt-a",
|
||||
"fillStyle": "hachure",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"angle": 0,
|
||||
"x": 781.4456641707259,
|
||||
"y": 248.777857603323,
|
||||
"strokeColor": "#000000",
|
||||
"backgroundColor": "transparent",
|
||||
"width": 104.0940537386266,
|
||||
"height": 27.60697400233846,
|
||||
"seed": 1695061516,
|
||||
"groupIds": [],
|
||||
"strokeSharpness": "round",
|
||||
"boundElementIds": [],
|
||||
"updated": 1638347737174,
|
||||
"startBinding": {
|
||||
"elementId": "Sa4OBd1ZjD6itohm7Ll8z",
|
||||
"focus": -0.5921495247360469,
|
||||
"gap": 6.398850205882127
|
||||
},
|
||||
"endBinding": {
|
||||
"elementId": "R5v-yZCVJ97BkJqr0Qb7H",
|
||||
"focus": 0.7021471697457312,
|
||||
"gap": 11.478007139455713
|
||||
},
|
||||
"lastCommittedPoint": null,
|
||||
"startArrowhead": null,
|
||||
"endArrowhead": "arrow",
|
||||
"points": [
|
||||
[
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
104.0940537386266,
|
||||
27.60697400233846
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "rectangle",
|
||||
"version": 883,
|
||||
"versionNonce": 845352076,
|
||||
"isDeleted": false,
|
||||
"id": "4pW8hvBu3bo1eMtFvg_gS",
|
||||
"fillStyle": "hachure",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"angle": 0,
|
||||
"x": 601.6520690917969,
|
||||
"y": 473.7677536010742,
|
||||
"strokeColor": "#000000",
|
||||
"backgroundColor": "transparent",
|
||||
"width": 171.99359130859375,
|
||||
"height": 44.74725341796875,
|
||||
"seed": 1678097076,
|
||||
"groupIds": [
|
||||
"3kSpFrIN3kg4jwjDKpNWw"
|
||||
],
|
||||
"strokeSharpness": "sharp",
|
||||
"boundElementIds": [
|
||||
"wRO0q9xKPHc8e8XPPsQWh",
|
||||
"he-SpFjCxEQEWpWny2kKP",
|
||||
"-pjrKo16rOsasM8viZPJ-",
|
||||
"5W90eDBjtfZvkSuQTG0Iw"
|
||||
],
|
||||
"updated": 1638347777173
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"version": 703,
|
||||
"versionNonce": 519371956,
|
||||
"isDeleted": false,
|
||||
"id": "w_i2hO06oLa0bWbyAfFzU",
|
||||
"fillStyle": "hachure",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"angle": 0,
|
||||
"x": 639.3624267578125,
|
||||
"y": 486.14290618896484,
|
||||
"strokeColor": "#000000",
|
||||
"backgroundColor": "transparent",
|
||||
"width": 94,
|
||||
"height": 24,
|
||||
"seed": 340753036,
|
||||
"groupIds": [
|
||||
"3kSpFrIN3kg4jwjDKpNWw"
|
||||
],
|
||||
"strokeSharpness": "sharp",
|
||||
"boundElementIds": [],
|
||||
"updated": 1638347776748,
|
||||
"fontSize": 20,
|
||||
"fontFamily": 3,
|
||||
"text": "vmselect",
|
||||
"baseline": 19,
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "top"
|
||||
},
|
||||
{
|
||||
"type": "rectangle",
|
||||
"version": 745,
|
||||
"versionNonce": 879581324,
|
||||
"isDeleted": false,
|
||||
"id": "U5U-67wL5fPwvzlxOAF5A",
|
||||
"fillStyle": "hachure",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "dotted",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"angle": 0,
|
||||
"x": 592.7498321533203,
|
||||
"y": 437.2059860229492,
|
||||
"strokeColor": "#000000",
|
||||
"backgroundColor": "transparent",
|
||||
"width": 397.1286010742188,
|
||||
"height": 209.62742614746097,
|
||||
"seed": 912540724,
|
||||
"groupIds": [],
|
||||
"strokeSharpness": "sharp",
|
||||
"boundElementIds": [
|
||||
"pD9DcILMxa6GaR1U5YyMO"
|
||||
],
|
||||
"updated": 1638347776748
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"version": 345,
|
||||
"versionNonce": 1628116148,
|
||||
"isDeleted": false,
|
||||
"id": "0IGVrvICMVeZp2RCaqTeP",
|
||||
"fillStyle": "hachure",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "dotted",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"angle": 0,
|
||||
"x": 608.3194122314453,
|
||||
"y": 444.93126678466797,
|
||||
"strokeColor": "#000000",
|
||||
"backgroundColor": "transparent",
|
||||
"width": 291,
|
||||
"height": 19,
|
||||
"seed": 68700428,
|
||||
"groupIds": [],
|
||||
"strokeSharpness": "sharp",
|
||||
"boundElementIds": [],
|
||||
"updated": 1638347784373,
|
||||
"fontSize": 16,
|
||||
"fontFamily": 3,
|
||||
"text": "VM cluster with aggregated data",
|
||||
"baseline": 15,
|
||||
"textAlign": "left",
|
||||
"verticalAlign": "top"
|
||||
},
|
||||
{
|
||||
"type": "rectangle",
|
||||
"version": 996,
|
||||
"versionNonce": 2112461580,
|
||||
"isDeleted": false,
|
||||
"id": "9QMf0HXPE1W4M9S-zMJVO",
|
||||
"fillStyle": "hachure",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"angle": 0,
|
||||
"x": 806.1652679443359,
|
||||
"y": 532.9768753051758,
|
||||
"strokeColor": "#000000",
|
||||
"backgroundColor": "transparent",
|
||||
"width": 171.99359130859375,
|
||||
"height": 44.74725341796875,
|
||||
"seed": 523607476,
|
||||
"groupIds": [
|
||||
"Eb2kWfz3ZWBu8cNul0h_c"
|
||||
],
|
||||
"strokeSharpness": "sharp",
|
||||
"boundElementIds": [
|
||||
"wRO0q9xKPHc8e8XPPsQWh",
|
||||
"he-SpFjCxEQEWpWny2kKP",
|
||||
"-pjrKo16rOsasM8viZPJ-",
|
||||
"9WBH34em4CdU2OwzqYIl5",
|
||||
"5W90eDBjtfZvkSuQTG0Iw"
|
||||
],
|
||||
"updated": 1638347777173
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"version": 804,
|
||||
"versionNonce": 1660832052,
|
||||
"isDeleted": false,
|
||||
"id": "WOrOD5vn6EtMUQRJomt3-",
|
||||
"fillStyle": "hachure",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"angle": 0,
|
||||
"x": 838.6197662353516,
|
||||
"y": 545.3520278930664,
|
||||
"strokeColor": "#000000",
|
||||
"backgroundColor": "transparent",
|
||||
"width": 105,
|
||||
"height": 24,
|
||||
"seed": 1321420684,
|
||||
"groupIds": [
|
||||
"Eb2kWfz3ZWBu8cNul0h_c"
|
||||
],
|
||||
"strokeSharpness": "sharp",
|
||||
"boundElementIds": [],
|
||||
"updated": 1638347776748,
|
||||
"fontSize": 20,
|
||||
"fontFamily": 3,
|
||||
"text": "vmstorage",
|
||||
"baseline": 19,
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "top"
|
||||
},
|
||||
{
|
||||
"type": "rectangle",
|
||||
"version": 925,
|
||||
"versionNonce": 2052807604,
|
||||
"isDeleted": false,
|
||||
"id": "4dtJZpXEUxSK3biwFG7vd",
|
||||
"fillStyle": "hachure",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"angle": 0,
|
||||
"x": 603.5079803466797,
|
||||
"y": 590.3987808227539,
|
||||
"strokeColor": "#000000",
|
||||
"backgroundColor": "transparent",
|
||||
"width": 171.99359130859375,
|
||||
"height": 44.74725341796875,
|
||||
"seed": 1738524468,
|
||||
"groupIds": [
|
||||
"punXEDFtHkSpcd9seAkZj"
|
||||
],
|
||||
"strokeSharpness": "sharp",
|
||||
"boundElementIds": [
|
||||
"wRO0q9xKPHc8e8XPPsQWh",
|
||||
"9WBH34em4CdU2OwzqYIl5",
|
||||
"EyecK0YM9Cc8T6ju-nTOc"
|
||||
],
|
||||
"updated": 1638347812431
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"version": 756,
|
||||
"versionNonce": 2040967820,
|
||||
"isDeleted": false,
|
||||
"id": "rAbyooo-X08-86qjoK0WR",
|
||||
"fillStyle": "hachure",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"angle": 0,
|
||||
"x": 641.2188262939453,
|
||||
"y": 603.0175857543945,
|
||||
"strokeColor": "#000000",
|
||||
"backgroundColor": "transparent",
|
||||
"width": 94,
|
||||
"height": 24,
|
||||
"seed": 948598284,
|
||||
"groupIds": [
|
||||
"punXEDFtHkSpcd9seAkZj"
|
||||
],
|
||||
"strokeSharpness": "sharp",
|
||||
"boundElementIds": [
|
||||
"9WBH34em4CdU2OwzqYIl5"
|
||||
],
|
||||
"updated": 1638347776748,
|
||||
"fontSize": 20,
|
||||
"fontFamily": 3,
|
||||
"text": "vminsert",
|
||||
"baseline": 19,
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "top"
|
||||
},
|
||||
{
|
||||
"type": "arrow",
|
||||
"version": 660,
|
||||
"versionNonce": 1560678196,
|
||||
"isDeleted": false,
|
||||
"id": "9WBH34em4CdU2OwzqYIl5",
|
||||
"fillStyle": "hachure",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"angle": 0,
|
||||
"x": 783.8656172818814,
|
||||
"y": 612.3483247872634,
|
||||
"strokeColor": "#000000",
|
||||
"backgroundColor": "transparent",
|
||||
"width": 99.87819315552736,
|
||||
"height": 24.681162491468,
|
||||
"seed": 1141424308,
|
||||
"groupIds": [],
|
||||
"strokeSharpness": "round",
|
||||
"boundElementIds": [],
|
||||
"updated": 1638347777173,
|
||||
"startBinding": {
|
||||
"elementId": "4dtJZpXEUxSK3biwFG7vd",
|
||||
"focus": 0.5247890891198189,
|
||||
"gap": 8.364045626608004
|
||||
},
|
||||
"endBinding": {
|
||||
"elementId": "9QMf0HXPE1W4M9S-zMJVO",
|
||||
"focus": -0.6931056940383404,
|
||||
"gap": 9.943033572650847
|
||||
},
|
||||
"lastCommittedPoint": null,
|
||||
"startArrowhead": null,
|
||||
"endArrowhead": "arrow",
|
||||
"points": [
|
||||
[
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
99.87819315552736,
|
||||
-24.681162491468
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "arrow",
|
||||
"version": 768,
|
||||
"versionNonce": 1653264948,
|
||||
"isDeleted": false,
|
||||
"id": "5W90eDBjtfZvkSuQTG0Iw",
|
||||
"fillStyle": "hachure",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"angle": 0,
|
||||
"x": 780.0445106062728,
|
||||
"y": 493.8918941633816,
|
||||
"strokeColor": "#000000",
|
||||
"backgroundColor": "transparent",
|
||||
"width": 104.0940537386266,
|
||||
"height": 27.60697400233846,
|
||||
"seed": 1852298380,
|
||||
"groupIds": [],
|
||||
"strokeSharpness": "round",
|
||||
"boundElementIds": [],
|
||||
"updated": 1638347777173,
|
||||
"startBinding": {
|
||||
"elementId": "4pW8hvBu3bo1eMtFvg_gS",
|
||||
"focus": -0.5921495247360469,
|
||||
"gap": 6.398850205882127
|
||||
},
|
||||
"endBinding": {
|
||||
"elementId": "9QMf0HXPE1W4M9S-zMJVO",
|
||||
"focus": 0.7021471697457312,
|
||||
"gap": 11.478007139455713
|
||||
},
|
||||
"lastCommittedPoint": null,
|
||||
"startArrowhead": null,
|
||||
"endArrowhead": "arrow",
|
||||
"points": [
|
||||
[
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
104.0940537386266,
|
||||
27.60697400233846
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "HPEwr85wL4IedW0AgdArp",
|
||||
"type": "arrow",
|
||||
"x": 423.70701599121094,
|
||||
"y": 428.34434509277344,
|
||||
"width": 179.54901123046875,
|
||||
"height": 182.9937744140625,
|
||||
"angle": 0,
|
||||
"strokeColor": "#000000",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "hachure",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"strokeSharpness": "round",
|
||||
"seed": 389863732,
|
||||
"version": 47,
|
||||
"versionNonce": 1364399028,
|
||||
"isDeleted": false,
|
||||
"boundElementIds": null,
|
||||
"updated": 1638347805500,
|
||||
"points": [
|
||||
[
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
179.54901123046875,
|
||||
-182.9937744140625
|
||||
]
|
||||
],
|
||||
"lastCommittedPoint": null,
|
||||
"startBinding": {
|
||||
"elementId": "VgBUzo0blGR-Ijd2mQEEf",
|
||||
"focus": 0.6700023593531782,
|
||||
"gap": 10.266586303710938
|
||||
},
|
||||
"endBinding": null,
|
||||
"startArrowhead": null,
|
||||
"endArrowhead": "arrow"
|
||||
},
|
||||
{
|
||||
"id": "EyecK0YM9Cc8T6ju-nTOc",
|
||||
"type": "arrow",
|
||||
"x": 424.7585906982422,
|
||||
"y": 441.12806701660156,
|
||||
"width": 174.29449462890625,
|
||||
"height": 170.56607055664062,
|
||||
"angle": 0,
|
||||
"strokeColor": "#000000",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "hachure",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"strokeSharpness": "round",
|
||||
"seed": 981082124,
|
||||
"version": 92,
|
||||
"versionNonce": 1261546252,
|
||||
"isDeleted": false,
|
||||
"boundElementIds": null,
|
||||
"updated": 1638347812431,
|
||||
"points": [
|
||||
[
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
174.29449462890625,
|
||||
170.56607055664062
|
||||
]
|
||||
],
|
||||
"lastCommittedPoint": null,
|
||||
"startBinding": {
|
||||
"elementId": "VgBUzo0blGR-Ijd2mQEEf",
|
||||
"focus": -0.6826568395144794,
|
||||
"gap": 11.318161010742188
|
||||
},
|
||||
"endBinding": {
|
||||
"elementId": "4dtJZpXEUxSK3biwFG7vd",
|
||||
"focus": -0.8207814548026305,
|
||||
"gap": 4.45489501953125
|
||||
},
|
||||
"startArrowhead": null,
|
||||
"endArrowhead": "arrow"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"version": 775,
|
||||
"versionNonce": 95530764,
|
||||
"isDeleted": false,
|
||||
"id": "o-yIJ_WZzVubhbVODvhcC",
|
||||
"fillStyle": "hachure",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"angle": 0,
|
||||
"x": 407.27012634277344,
|
||||
"y": 489.33091735839844,
|
||||
"strokeColor": "#000000",
|
||||
"backgroundColor": "transparent",
|
||||
"width": 141,
|
||||
"height": 19,
|
||||
"seed": 775116812,
|
||||
"groupIds": [],
|
||||
"strokeSharpness": "sharp",
|
||||
"boundElementIds": [
|
||||
"wRO0q9xKPHc8e8XPPsQWh"
|
||||
],
|
||||
"updated": 1638347824876,
|
||||
"fontSize": 16,
|
||||
"fontFamily": 3,
|
||||
"text": "persist results",
|
||||
"baseline": 15,
|
||||
"textAlign": "left",
|
||||
"verticalAlign": "top"
|
||||
}
|
||||
],
|
||||
"appState": {
|
||||
"gridSize": null,
|
||||
"viewBackgroundColor": "#ffffff"
|
||||
},
|
||||
"files": {}
|
||||
}
|
||||
BIN
app/vmalert/vmalert_multicluster.png
Normal file
|
After Width: | Height: | Size: 122 KiB |
354
app/vmalert/vmalert_single.excalidraw
Normal file
@@ -0,0 +1,354 @@
|
||||
{
|
||||
"type": "excalidraw",
|
||||
"version": 2,
|
||||
"source": "https://excalidraw.com",
|
||||
"elements": [
|
||||
{
|
||||
"type": "rectangle",
|
||||
"version": 795,
|
||||
"versionNonce": 1195460364,
|
||||
"isDeleted": false,
|
||||
"id": "VgBUzo0blGR-Ijd2mQEEf",
|
||||
"fillStyle": "hachure",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"angle": 0,
|
||||
"x": 422.3502197265625,
|
||||
"y": 215.55953979492188,
|
||||
"strokeColor": "#000000",
|
||||
"backgroundColor": "transparent",
|
||||
"width": 123.7601318359375,
|
||||
"height": 72.13211059570312,
|
||||
"seed": 1194011660,
|
||||
"groupIds": [
|
||||
"iBaXgbpyifSwPplm_GO5b"
|
||||
],
|
||||
"strokeSharpness": "sharp",
|
||||
"boundElementIds": [
|
||||
"miEbzHxOPXe4PEYvXiJp5",
|
||||
"rcmiQfIWtfbTTlwxqr1sl",
|
||||
"P-dpWlSTtnsux-zr5oqgF",
|
||||
"oAToSPttH7aWoD_AqXGFX",
|
||||
"Bpy5by47XGKB4yS99ZkuA",
|
||||
"wRO0q9xKPHc8e8XPPsQWh",
|
||||
"sxEhnxlbT7ldlSsmHDUHp"
|
||||
],
|
||||
"updated": 1638348116633
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"version": 660,
|
||||
"versionNonce": 1034125236,
|
||||
"isDeleted": false,
|
||||
"id": "e9TDm09y-GhPm84XWt0Jv",
|
||||
"fillStyle": "hachure",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"angle": 0,
|
||||
"x": 443.89678955078125,
|
||||
"y": 236.64378356933594,
|
||||
"strokeColor": "#000000",
|
||||
"backgroundColor": "transparent",
|
||||
"width": 82,
|
||||
"height": 24,
|
||||
"seed": 327273100,
|
||||
"groupIds": [
|
||||
"iBaXgbpyifSwPplm_GO5b"
|
||||
],
|
||||
"strokeSharpness": "sharp",
|
||||
"boundElementIds": [],
|
||||
"updated": 1638348116633,
|
||||
"fontSize": 20,
|
||||
"fontFamily": 3,
|
||||
"text": "vmalert",
|
||||
"baseline": 19,
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "middle"
|
||||
},
|
||||
{
|
||||
"type": "rectangle",
|
||||
"version": 1690,
|
||||
"versionNonce": 236788620,
|
||||
"isDeleted": false,
|
||||
"id": "dd52BjHfPMPRji9Tws7U-",
|
||||
"fillStyle": "hachure",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"angle": 0,
|
||||
"x": 344.9837951660156,
|
||||
"y": 64.64248657226562,
|
||||
"strokeColor": "#000000",
|
||||
"backgroundColor": "transparent",
|
||||
"width": 269.336669921875,
|
||||
"height": 44.74725341796875,
|
||||
"seed": 1779959692,
|
||||
"groupIds": [
|
||||
"2Lijjn3PwPQW_8KrcDmdu"
|
||||
],
|
||||
"strokeSharpness": "sharp",
|
||||
"boundElementIds": [
|
||||
"Bpy5by47XGKB4yS99ZkuA"
|
||||
],
|
||||
"updated": 1638348176044
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"version": 1331,
|
||||
"versionNonce": 945335476,
|
||||
"isDeleted": false,
|
||||
"id": "9TEzv0sVCHAkc46ou0oNF",
|
||||
"fillStyle": "hachure",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"angle": 0,
|
||||
"x": 364.35296630859375,
|
||||
"y": 76.77398681640625,
|
||||
"strokeColor": "#000000",
|
||||
"backgroundColor": "transparent",
|
||||
"width": 234,
|
||||
"height": 24,
|
||||
"seed": 1617178804,
|
||||
"groupIds": [
|
||||
"2Lijjn3PwPQW_8KrcDmdu"
|
||||
],
|
||||
"strokeSharpness": "sharp",
|
||||
"boundElementIds": [],
|
||||
"updated": 1638348176045,
|
||||
"fontSize": 20,
|
||||
"fontFamily": 3,
|
||||
"text": "victoriametrics:8428",
|
||||
"baseline": 19,
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "top"
|
||||
},
|
||||
{
|
||||
"type": "rectangle",
|
||||
"version": 1099,
|
||||
"versionNonce": 671872012,
|
||||
"isDeleted": false,
|
||||
"id": "8-XFSbd6Zw96EUSJbJXZv",
|
||||
"fillStyle": "hachure",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"angle": 0,
|
||||
"x": 360.6495666503906,
|
||||
"y": 382.2536315917969,
|
||||
"strokeColor": "#000000",
|
||||
"backgroundColor": "transparent",
|
||||
"width": 240.10644531249997,
|
||||
"height": 44.74725341796875,
|
||||
"seed": 99322124,
|
||||
"groupIds": [
|
||||
"6obQBPHIfExBKfejeLLVO"
|
||||
],
|
||||
"strokeSharpness": "sharp",
|
||||
"boundElementIds": [
|
||||
"sxEhnxlbT7ldlSsmHDUHp"
|
||||
],
|
||||
"updated": 1638348116633
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"version": 865,
|
||||
"versionNonce": 635839156,
|
||||
"isDeleted": false,
|
||||
"id": "GUs816aggGqUSdoEsSmea",
|
||||
"fillStyle": "hachure",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"angle": 0,
|
||||
"x": 382.64422607421875,
|
||||
"y": 394.3433837890625,
|
||||
"strokeColor": "#000000",
|
||||
"backgroundColor": "transparent",
|
||||
"width": 199,
|
||||
"height": 24,
|
||||
"seed": 1194745268,
|
||||
"groupIds": [
|
||||
"6obQBPHIfExBKfejeLLVO"
|
||||
],
|
||||
"strokeSharpness": "sharp",
|
||||
"boundElementIds": [],
|
||||
"updated": 1638348116633,
|
||||
"fontSize": 20,
|
||||
"fontFamily": 3,
|
||||
"text": "alertmanager:9093",
|
||||
"baseline": 19,
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "top"
|
||||
},
|
||||
{
|
||||
"type": "arrow",
|
||||
"version": 2444,
|
||||
"versionNonce": 361351692,
|
||||
"isDeleted": false,
|
||||
"id": "Bpy5by47XGKB4yS99ZkuA",
|
||||
"fillStyle": "hachure",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"angle": 0,
|
||||
"x": 486.0465703465111,
|
||||
"y": 204.98379516601562,
|
||||
"strokeColor": "#000000",
|
||||
"backgroundColor": "transparent",
|
||||
"width": 0.7962088410123442,
|
||||
"height": 86.86798095703125,
|
||||
"seed": 357577356,
|
||||
"groupIds": [],
|
||||
"strokeSharpness": "round",
|
||||
"boundElementIds": [],
|
||||
"updated": 1638348176045,
|
||||
"startBinding": {
|
||||
"elementId": "VgBUzo0blGR-Ijd2mQEEf",
|
||||
"gap": 10.57574462890625,
|
||||
"focus": 0.0344528515859526
|
||||
},
|
||||
"endBinding": {
|
||||
"elementId": "dd52BjHfPMPRji9Tws7U-",
|
||||
"gap": 8.72607421875,
|
||||
"focus": -0.039393828258510157
|
||||
},
|
||||
"lastCommittedPoint": null,
|
||||
"startArrowhead": null,
|
||||
"endArrowhead": "arrow",
|
||||
"points": [
|
||||
[
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
-0.7962088410123442,
|
||||
-86.86798095703125
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"version": 640,
|
||||
"versionNonce": 1892951180,
|
||||
"isDeleted": false,
|
||||
"id": "RbVSa4PnOgAMtzoKb-DhW",
|
||||
"fillStyle": "hachure",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"angle": 0,
|
||||
"x": 500.609619140625,
|
||||
"y": 134.11544799804688,
|
||||
"strokeColor": "#000000",
|
||||
"backgroundColor": "transparent",
|
||||
"width": 188,
|
||||
"height": 95,
|
||||
"seed": 1989838604,
|
||||
"groupIds": [],
|
||||
"strokeSharpness": "sharp",
|
||||
"boundElementIds": [],
|
||||
"updated": 1638348163424,
|
||||
"fontSize": 16,
|
||||
"fontFamily": 3,
|
||||
"text": "execute rules,\npersist alerts \nand recording rules,\nrestore state\n",
|
||||
"baseline": 91,
|
||||
"textAlign": "left",
|
||||
"verticalAlign": "top"
|
||||
},
|
||||
{
|
||||
"type": "arrow",
|
||||
"version": 1472,
|
||||
"versionNonce": 768565516,
|
||||
"isDeleted": false,
|
||||
"id": "sxEhnxlbT7ldlSsmHDUHp",
|
||||
"fillStyle": "hachure",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"angle": 0,
|
||||
"x": 486.5245361328125,
|
||||
"y": 300.5478957313172,
|
||||
"strokeColor": "#000000",
|
||||
"backgroundColor": "transparent",
|
||||
"width": 0.3521007656264601,
|
||||
"height": 70.26802223743277,
|
||||
"seed": 1818348300,
|
||||
"groupIds": [],
|
||||
"strokeSharpness": "round",
|
||||
"boundElementIds": [],
|
||||
"updated": 1638348116633,
|
||||
"startBinding": {
|
||||
"elementId": "E9Run6wCm2chQ6JHrmc_y",
|
||||
"focus": 1.1925203824459496,
|
||||
"gap": 11.77154541015625
|
||||
},
|
||||
"endBinding": {
|
||||
"elementId": "8-XFSbd6Zw96EUSJbJXZv",
|
||||
"focus": 0.0441077573536454,
|
||||
"gap": 11.437713623046875
|
||||
},
|
||||
"lastCommittedPoint": null,
|
||||
"startArrowhead": null,
|
||||
"endArrowhead": "arrow",
|
||||
"points": [
|
||||
[
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
-0.3521007656264601,
|
||||
70.26802223743277
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"version": 577,
|
||||
"versionNonce": 1646003636,
|
||||
"isDeleted": false,
|
||||
"id": "E9Run6wCm2chQ6JHrmc_y",
|
||||
"fillStyle": "hachure",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"angle": 0,
|
||||
"x": 498.29608154296875,
|
||||
"y": 298.6573791503906,
|
||||
"strokeColor": "#000000",
|
||||
"backgroundColor": "transparent",
|
||||
"width": 122,
|
||||
"height": 38,
|
||||
"seed": 1836541708,
|
||||
"groupIds": [],
|
||||
"strokeSharpness": "sharp",
|
||||
"boundElementIds": [
|
||||
"sxEhnxlbT7ldlSsmHDUHp"
|
||||
],
|
||||
"updated": 1638348116633,
|
||||
"fontSize": 16,
|
||||
"fontFamily": 3,
|
||||
"text": "send alert \nnotifications",
|
||||
"baseline": 34,
|
||||
"textAlign": "left",
|
||||
"verticalAlign": "top"
|
||||
}
|
||||
],
|
||||
"appState": {
|
||||
"gridSize": null,
|
||||
"viewBackgroundColor": "#ffffff"
|
||||
},
|
||||
"files": {}
|
||||
}
|
||||
BIN
app/vmalert/vmalert_single.png
Normal file
|
After Width: | Height: | Size: 62 KiB |
@@ -10,6 +10,7 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/notifier"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/tpl"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/httpserver"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
|
||||
@@ -24,17 +25,22 @@ var (
|
||||
|
||||
func initLinks() {
|
||||
pathPrefix := httpserver.GetPathPrefix()
|
||||
if pathPrefix == "" {
|
||||
pathPrefix = "/"
|
||||
}
|
||||
apiLinks = [][2]string{
|
||||
{path.Join(pathPrefix, "api/v1/groups"), "list all loaded groups and rules"},
|
||||
{path.Join(pathPrefix, "api/v1/rules"), "list all loaded groups and rules"},
|
||||
{path.Join(pathPrefix, "api/v1/alerts"), "list all active alerts"},
|
||||
{path.Join(pathPrefix, "api/v1/groupID/alertID/status"), "get alert status by ID"},
|
||||
{path.Join(pathPrefix, "flags"), "command-line flags"},
|
||||
{path.Join(pathPrefix, "metrics"), "list of application metrics"},
|
||||
{path.Join(pathPrefix, "-/reload"), "reload configuration"},
|
||||
}
|
||||
navItems = []tpl.NavItem{
|
||||
{Name: "vmalert", Url: path.Join(pathPrefix, "/")},
|
||||
{Name: "Groups", Url: path.Join(pathPrefix, "groups")},
|
||||
{Name: "Alerts", Url: path.Join(pathPrefix, "/alerts")},
|
||||
{Name: "Alerts", Url: path.Join(pathPrefix, "alerts")},
|
||||
{Name: "Notifiers", Url: path.Join(pathPrefix, "notifiers")},
|
||||
{Name: "Docs", Url: "https://docs.victoriametrics.com/vmalert.html"},
|
||||
}
|
||||
}
|
||||
@@ -48,6 +54,11 @@ func (rh *requestHandler) handler(w http.ResponseWriter, r *http.Request) bool {
|
||||
initLinks()
|
||||
})
|
||||
|
||||
pathPrefix := httpserver.GetPathPrefix()
|
||||
if pathPrefix == "" {
|
||||
pathPrefix = "/"
|
||||
}
|
||||
|
||||
switch r.URL.Path {
|
||||
case "/":
|
||||
if r.Method != "GET" {
|
||||
@@ -56,18 +67,21 @@ func (rh *requestHandler) handler(w http.ResponseWriter, r *http.Request) bool {
|
||||
WriteWelcome(w)
|
||||
return true
|
||||
case "/alerts":
|
||||
WriteListAlerts(w, rh.groupAlerts())
|
||||
WriteListAlerts(w, pathPrefix, rh.groupAlerts())
|
||||
return true
|
||||
case "/groups":
|
||||
WriteListGroups(w, rh.groups())
|
||||
return true
|
||||
case "/api/v1/groups":
|
||||
case "/notifiers":
|
||||
WriteListTargets(w, notifier.GetTargets())
|
||||
return true
|
||||
case "/api/v1/rules":
|
||||
data, err := rh.listGroups()
|
||||
if err != nil {
|
||||
httpserver.Errorf(w, r, "%s", err)
|
||||
return true
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write(data)
|
||||
return true
|
||||
case "/api/v1/alerts":
|
||||
@@ -76,7 +90,7 @@ func (rh *requestHandler) handler(w http.ResponseWriter, r *http.Request) bool {
|
||||
httpserver.Errorf(w, r, "%s", err)
|
||||
return true
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write(data)
|
||||
return true
|
||||
case "/-/reload":
|
||||
@@ -101,22 +115,22 @@ func (rh *requestHandler) handler(w http.ResponseWriter, r *http.Request) bool {
|
||||
httpserver.Errorf(w, r, "failed to marshal alert: %s", err)
|
||||
return true
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write(data)
|
||||
return true
|
||||
}
|
||||
|
||||
// <groupID>/<alertID>/status
|
||||
WriteAlert(w, alert)
|
||||
WriteAlert(w, pathPrefix, alert)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
type listGroupsResponse struct {
|
||||
Data struct {
|
||||
Status string `json:"status"`
|
||||
Data struct {
|
||||
Groups []APIGroup `json:"groups"`
|
||||
} `json:"data"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
func (rh *requestHandler) groups() []APIGroup {
|
||||
@@ -135,6 +149,7 @@ func (rh *requestHandler) groups() []APIGroup {
|
||||
|
||||
return groups
|
||||
}
|
||||
|
||||
func (rh *requestHandler) listGroups() ([]byte, error) {
|
||||
lr := listGroupsResponse{Status: "success"}
|
||||
lr.Data.Groups = rh.groups()
|
||||
@@ -149,10 +164,10 @@ func (rh *requestHandler) listGroups() ([]byte, error) {
|
||||
}
|
||||
|
||||
type listAlertsResponse struct {
|
||||
Data struct {
|
||||
Status string `json:"status"`
|
||||
Data struct {
|
||||
Alerts []*APIAlert `json:"alerts"`
|
||||
} `json:"data"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
func (rh *requestHandler) groupAlerts() []GroupAlerts {
|
||||
@@ -167,7 +182,7 @@ func (rh *requestHandler) groupAlerts() []GroupAlerts {
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
alerts = append(alerts, a.AlertsAPI()...)
|
||||
alerts = append(alerts, a.AlertsToAPI()...)
|
||||
}
|
||||
if len(alerts) > 0 {
|
||||
groupAlerts = append(groupAlerts, GroupAlerts{
|
||||
@@ -190,7 +205,7 @@ func (rh *requestHandler) listAlerts() ([]byte, error) {
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
lr.Data.Alerts = append(lr.Data.Alerts, a.AlertsAPI()...)
|
||||
lr.Data.Alerts = append(lr.Data.Alerts, a.AlertsToAPI()...)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,8 +3,10 @@
|
||||
{% import (
|
||||
"time"
|
||||
"sort"
|
||||
"path"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/tpl"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/notifier"
|
||||
) %}
|
||||
|
||||
|
||||
@@ -29,14 +31,7 @@
|
||||
rOk := make(map[string]int)
|
||||
rNotOk := make(map[string]int)
|
||||
for _, g := range groups {
|
||||
for _, r := range g.AlertingRules{
|
||||
if r.LastError != "" {
|
||||
rNotOk[g.Name]++
|
||||
} else {
|
||||
rOk[g.Name]++
|
||||
}
|
||||
}
|
||||
for _, r := range g.RecordingRules{
|
||||
for _, r := range g.Rules {
|
||||
if r.LastError != "" {
|
||||
rNotOk[g.Name]++
|
||||
} else {
|
||||
@@ -50,14 +45,14 @@
|
||||
{% for _, g := range groups %}
|
||||
<div class="group-heading{% if rNotOk[g.Name] > 0 %} alert-danger{% endif %}" data-bs-target="rules-{%s g.ID %}">
|
||||
<span class="anchor" id="group-{%s g.ID %}"></span>
|
||||
<a href="#group-{%s g.ID %}">{%s g.Name %}{% if g.Type != "prometheus" %} ({%s g.Type %}){% endif %} (every {%s g.Interval %})</a>
|
||||
{% if rNotOk[g.Name] > 0 %}<span class="badge bg-danger" title="Number of rules withs status Error">{%d rNotOk[g.Name] %}</span> {% endif %}
|
||||
<a href="#group-{%s g.ID %}">{%s g.Name %}{% if g.Type != "prometheus" %} ({%s g.Type %}){% endif %} (every {%f.0 g.Interval %}s)</a>
|
||||
{% if rNotOk[g.Name] > 0 %}<span class="badge bg-danger" title="Number of rules with status Error">{%d rNotOk[g.Name] %}</span> {% endif %}
|
||||
<span class="badge bg-success" title="Number of rules withs status Ok">{%d rOk[g.Name] %}</span>
|
||||
<p class="fs-6 fw-lighter">{%s g.File %}</p>
|
||||
{% if len(g.ExtraFilterLabels) > 0 %}
|
||||
<div class="fs-6 fw-lighter">Extra filter labels
|
||||
{% for k, v := range g.ExtraFilterLabels %}
|
||||
<span class="float-left badge bg-primary">{%s k %}={%s v %}</span>
|
||||
{% if len(g.Params) > 0 %}
|
||||
<div class="fs-6 fw-lighter">Extra params
|
||||
{% for _, param := range g.Params %}
|
||||
<span class="float-left badge bg-primary">{%s param %}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
@@ -73,34 +68,24 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for _, ar := range g.AlertingRules %}
|
||||
<tr{% if ar.LastError != "" %} class="alert-danger"{% endif %}>
|
||||
{% for _, r := range g.Rules %}
|
||||
<tr{% if r.LastError != "" %} class="alert-danger"{% endif %}>
|
||||
<td>
|
||||
<b>alert:</b> {%s ar.Name %} (for: {%v ar.For %})<br>
|
||||
<code><pre>{%s ar.Expression %}</pre></code><br>
|
||||
{% if len(ar.Labels) > 0 %} <b>Labels:</b>{% endif %}
|
||||
{% for k, v := range ar.Labels %}
|
||||
{% if r.Type == "alerting" %}
|
||||
<b>alert:</b> (for: {%v r.Duration %})
|
||||
{% else %}
|
||||
<b>record:</b> {%s r.Name %}
|
||||
{% endif %}
|
||||
<br>
|
||||
<code><pre>{%s r.Query %}</pre></code><br>
|
||||
{% if len(r.Labels) > 0 %} <b>Labels:</b>{% endif %}
|
||||
{% for k, v := range r.Labels %}
|
||||
<span class="ms-1 badge bg-primary">{%s k %}={%s v %}</span>
|
||||
{% endfor %}
|
||||
</td>
|
||||
<td><div class="error-cell">{%s ar.LastError %}</div></td>
|
||||
<td>{%d ar.LastSamples %}</td>
|
||||
<td>{%f.3 time.Since(ar.LastExec).Seconds() %}s ago</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% for _, rr := range g.RecordingRules %}
|
||||
<tr>
|
||||
<td>
|
||||
<b>record:</b> {%s rr.Name %}<br>
|
||||
<code><pre>{%s rr.Expression %}</pre></code>
|
||||
{% if len(rr.Labels) > 0 %} <b>Labels:</b>{% endif %}
|
||||
{% for k, v := range rr.Labels %}
|
||||
<span class="ms-1 badge bg-primary">{%s k %}={%s v %}</span>
|
||||
{% endfor %}
|
||||
</td>
|
||||
<td><div class="error-cell">{%s rr.LastError %}</div></td>
|
||||
<td>{%d rr.LastSamples %}</td>
|
||||
<td>{%f.3 time.Since(rr.LastExec).Seconds() %}s ago</td>
|
||||
<td><div class="error-cell">{%s r.LastError %}</div></td>
|
||||
<td>{%d r.LastSamples %}</td>
|
||||
<td>{%f.3 time.Since(r.LastEvaluation).Seconds() %}s ago</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
@@ -119,7 +104,7 @@
|
||||
{% endfunc %}
|
||||
|
||||
|
||||
{% func ListAlerts(groupAlerts []GroupAlerts) %}
|
||||
{% func ListAlerts(pathPrefix string, groupAlerts []GroupAlerts) %}
|
||||
{%= tpl.Header("Alerts", navItems) %}
|
||||
{% if len(groupAlerts) > 0 %}
|
||||
<a class="btn btn-primary" role="button" onclick="collapseAll()">Collapse All</a>
|
||||
@@ -155,7 +140,9 @@
|
||||
sort.Strings(labelKeys)
|
||||
%}
|
||||
<br>
|
||||
<b>alert:</b> {%s defaultAR.Name %} ({%d len(alertsByRule[ruleID]) %})<br>
|
||||
<b>alert:</b> {%s defaultAR.Name %} ({%d len(alertsByRule[ruleID]) %})
|
||||
| <span><a target="_blank" href="{%s defaultAR.SourceLink %}">Source</a></span>
|
||||
<br>
|
||||
<b>expr:</b><code><pre>{%s defaultAR.Expression %}</pre></code>
|
||||
<table class="table table-striped table-hover table-sm">
|
||||
<thead>
|
||||
@@ -175,11 +162,14 @@
|
||||
<span class="ms-1 badge bg-primary">{%s k %}={%s ar.Labels[k] %}</span>
|
||||
{% endfor %}
|
||||
</td>
|
||||
<td><span class="badge {% if ar.State=="firing" %}bg-danger{% else %} bg-warning text-dark{% endif %}">{%s ar.State %}</span></td>
|
||||
<td>{%s ar.ActiveAt.Format("2006-01-02T15:04:05Z07:00") %}</td>
|
||||
<td>{%= badgeState(ar.State) %}</td>
|
||||
<td>
|
||||
{%s ar.ActiveAt.Format("2006-01-02T15:04:05Z07:00") %}
|
||||
{% if ar.Restored %}{%= badgeRestored() %}{% endif %}
|
||||
</td>
|
||||
<td>{%s ar.Value %}</td>
|
||||
<td>
|
||||
<a href="/{%s g.ID %}/{%s ar.ID %}/status">Details</a>
|
||||
<a href="{%s path.Join(pathPrefix, g.ID, ar.ID, "status") %}">Details</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
@@ -200,7 +190,63 @@
|
||||
|
||||
{% endfunc %}
|
||||
|
||||
{% func Alert(alert *APIAlert) %}
|
||||
{% func ListTargets(targets map[notifier.TargetType][]notifier.Target) %}
|
||||
{%= tpl.Header("Notifiers", navItems) %}
|
||||
{% if len(targets) > 0 %}
|
||||
<a class="btn btn-primary" role="button" onclick="collapseAll()">Collapse All</a>
|
||||
<a class="btn btn-primary" role="button" onclick="expandAll()">Expand All</a>
|
||||
|
||||
{%code
|
||||
var keys []string
|
||||
for key := range targets {
|
||||
keys = append(keys, string(key))
|
||||
}
|
||||
sort.Strings(keys)
|
||||
%}
|
||||
|
||||
{% for i := range keys %}
|
||||
{%code typeK, ns := keys[i], targets[notifier.TargetType(keys[i])]
|
||||
count := len(ns)
|
||||
%}
|
||||
<div class="group-heading data-bs-target="rules-{%s typeK %}">
|
||||
<span class="anchor" id="notifiers-{%s typeK %}"></span>
|
||||
<a href="#notifiers-{%s typeK %}">{%s typeK %} ({%d count %})</a>
|
||||
</div>
|
||||
<div class="collapse show" id="notifiers-{%s typeK %}">
|
||||
<table class="table table-striped table-hover table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Labels</th>
|
||||
<th scope="col">Address</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for _, n := range ns %}
|
||||
<tr>
|
||||
<td>
|
||||
{% for _, l := range n.Labels %}
|
||||
<span class="ms-1 badge bg-primary">{%s l.Name %}={%s l.Value %}</span>
|
||||
{% endfor %}
|
||||
</td>
|
||||
<td>{%s n.Notifier.Addr() %}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
{% else %}
|
||||
<div>
|
||||
<p>No items...</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{%= tpl.Footer() %}
|
||||
|
||||
{% endfunc %}
|
||||
|
||||
{% func Alert(pathPrefix string, alert *APIAlert) %}
|
||||
{%= tpl.Header("", navItems) %}
|
||||
{%code
|
||||
var labelKeys []string
|
||||
@@ -267,10 +313,34 @@
|
||||
Group
|
||||
</div>
|
||||
<div class="col">
|
||||
<a target="_blank" href="/groups#group-{%s alert.GroupID %}">{%s alert.GroupID %}</a>
|
||||
<a target="_blank" href="{%s path.Join(pathPrefix,"groups") %}#group-{%s alert.GroupID %}">{%s alert.GroupID %}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container border-bottom p-2">
|
||||
<div class="row">
|
||||
<div class="col-2">
|
||||
Source link
|
||||
</div>
|
||||
<div class="col">
|
||||
<a target="_blank" href="{%s alert.SourceLink %}">Link</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{%= tpl.Footer() %}
|
||||
|
||||
{% endfunc %}
|
||||
|
||||
{% func badgeState(state string) %}
|
||||
{%code
|
||||
badgeClass := "bg-warning text-dark"
|
||||
if state == "firing" {
|
||||
badgeClass = "bg-danger"
|
||||
}
|
||||
%}
|
||||
<span class="badge {%s badgeClass %}">{%s state %}</span>
|
||||
{% endfunc %}
|
||||
|
||||
{% func badgeRestored() %}
|
||||
<span class="badge bg-warning text-dark" title="Alert state was restored after the service restart from remote storage">restored</span>
|
||||
{% endfunc %}
|
||||
@@ -54,9 +54,9 @@ func TestHandler(t *testing.T) {
|
||||
t.Errorf("expected 1 alert got %d", length)
|
||||
}
|
||||
})
|
||||
t.Run("/api/v1/groups", func(t *testing.T) {
|
||||
t.Run("/api/v1/rules", func(t *testing.T) {
|
||||
lr := listGroupsResponse{}
|
||||
getResp(ts.URL+"/api/v1/groups", &lr, 200)
|
||||
getResp(ts.URL+"/api/v1/rules", &lr, 200)
|
||||
if length := len(lr.Data.Groups); length != 1 {
|
||||
t.Errorf("expected 1 group got %d", length)
|
||||
}
|
||||
|
||||
@@ -4,61 +4,61 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// APIAlert represents an notifier.AlertingRule state
|
||||
// APIAlert represents a notifier.AlertingRule state
|
||||
// for WEB view
|
||||
// https://github.com/prometheus/compliance/blob/main/alert_generator/specification.md#get-apiv1rules
|
||||
type APIAlert struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
RuleID string `json:"rule_id"`
|
||||
GroupID string `json:"group_id"`
|
||||
Expression string `json:"expression"`
|
||||
State string `json:"state"`
|
||||
Name string `json:"name"`
|
||||
Value string `json:"value"`
|
||||
Labels map[string]string `json:"labels"`
|
||||
Labels map[string]string `json:"labels,omitempty"`
|
||||
Annotations map[string]string `json:"annotations"`
|
||||
ActiveAt time.Time `json:"activeAt"`
|
||||
|
||||
// Additional fields
|
||||
|
||||
// ID is an unique Alert's ID within a group
|
||||
ID string `json:"id"`
|
||||
// RuleID is an unique Rule's ID within a group
|
||||
RuleID string `json:"rule_id"`
|
||||
// GroupID is an unique Group's ID
|
||||
GroupID string `json:"group_id"`
|
||||
// Expression contains the PromQL/MetricsQL expression
|
||||
// for Rule's evaluation
|
||||
Expression string `json:"expression"`
|
||||
// SourceLink contains a link to a system which should show
|
||||
// why Alert was generated
|
||||
SourceLink string `json:"source"`
|
||||
// Restored shows whether Alert's state was restored on restart
|
||||
Restored bool `json:"restored"`
|
||||
}
|
||||
|
||||
// APIGroup represents Group for WEB view
|
||||
// https://github.com/prometheus/compliance/blob/main/alert_generator/specification.md#get-apiv1rules
|
||||
type APIGroup struct {
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
ID string `json:"id"`
|
||||
File string `json:"file"`
|
||||
Interval string `json:"interval"`
|
||||
Concurrency int `json:"concurrency"`
|
||||
ExtraFilterLabels map[string]string `json:"extra_filter_labels"`
|
||||
Labels map[string]string `json:"labels,omitempty"`
|
||||
AlertingRules []APIAlertingRule `json:"alerting_rules"`
|
||||
RecordingRules []APIRecordingRule `json:"recording_rules"`
|
||||
}
|
||||
// Name is the group name as present in the config
|
||||
Name string `json:"name"`
|
||||
// Rules contains both recording and alerting rules
|
||||
Rules []APIRule `json:"rules"`
|
||||
// Interval is the Group's evaluation interval in float seconds as present in the file.
|
||||
Interval float64 `json:"interval"`
|
||||
// LastEvaluation is the timestamp of the last time the Group was executed
|
||||
LastEvaluation time.Time `json:"lastEvaluation"`
|
||||
|
||||
// APIAlertingRule represents AlertingRule for WEB view
|
||||
type APIAlertingRule struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
GroupID string `json:"group_id"`
|
||||
Expression string `json:"expression"`
|
||||
For string `json:"for"`
|
||||
LastError string `json:"last_error"`
|
||||
LastSamples int `json:"last_samples"`
|
||||
LastExec time.Time `json:"last_exec"`
|
||||
Labels map[string]string `json:"labels"`
|
||||
Annotations map[string]string `json:"annotations"`
|
||||
}
|
||||
// Additional fields
|
||||
|
||||
// APIRecordingRule represents RecordingRule for WEB view
|
||||
type APIRecordingRule struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
GroupID string `json:"group_id"`
|
||||
Expression string `json:"expression"`
|
||||
LastError string `json:"last_error"`
|
||||
LastSamples int `json:"last_samples"`
|
||||
LastExec time.Time `json:"last_exec"`
|
||||
Labels map[string]string `json:"labels"`
|
||||
// Type shows the datasource type (prometheus or graphite) of the Group
|
||||
Type string `json:"type"`
|
||||
// ID is a unique Group ID
|
||||
ID string `json:"id"`
|
||||
// File contains a path to the file with Group's config
|
||||
File string `json:"file"`
|
||||
// Concurrency shows how many rules may be evaluated simultaneously
|
||||
Concurrency int `json:"concurrency"`
|
||||
// Params contains HTTP URL parameters added to each Rule's request
|
||||
Params []string `json:"params,omitempty"`
|
||||
// Labels is a set of label value pairs, that will be added to every rule.
|
||||
Labels map[string]string `json:"labels,omitempty"`
|
||||
}
|
||||
|
||||
// GroupAlerts represents a group of alerts for WEB view
|
||||
@@ -66,3 +66,43 @@ type GroupAlerts struct {
|
||||
Group APIGroup
|
||||
Alerts []*APIAlert
|
||||
}
|
||||
|
||||
// APIRule represents a Rule for WEB view
|
||||
// see https://github.com/prometheus/compliance/blob/main/alert_generator/specification.md#get-apiv1rules
|
||||
type APIRule struct {
|
||||
// State must be one of these under following scenarios
|
||||
// "pending": at least 1 alert in the rule in pending state and no other alert in firing state.
|
||||
// "firing": at least 1 alert in the rule in firing state.
|
||||
// "inactive": no alert in the rule in firing or pending state.
|
||||
State string `json:"state"`
|
||||
Name string `json:"name"`
|
||||
// Query represents Rule's `expression` field
|
||||
Query string `json:"query"`
|
||||
// Duration represents Rule's `for` field
|
||||
Duration float64 `json:"duration"`
|
||||
Labels map[string]string `json:"labels,omitempty"`
|
||||
Annotations map[string]string `json:"annotations,omitempty"`
|
||||
// LastError contains the error faced while executing the rule.
|
||||
LastError string `json:"lastError"`
|
||||
// EvaluationTime is the time taken to completely evaluate the rule in float seconds.
|
||||
EvaluationTime float64 `json:"evaluationTime"`
|
||||
// LastEvaluation is the timestamp of the last time the rule was executed
|
||||
LastEvaluation time.Time `json:"lastEvaluation"`
|
||||
// Alerts is the list of all the alerts in this rule that are currently pending or firing
|
||||
Alerts []*APIAlert `json:"alerts,omitempty"`
|
||||
// Health is the health of rule evaluation.
|
||||
// It MUST be one of "ok", "err", "unknown"
|
||||
Health string `json:"health"`
|
||||
// Type of the rule: recording or alerting
|
||||
Type string `json:"type"`
|
||||
|
||||
// Additional fields
|
||||
|
||||
// Type of the rule: recording or alerting
|
||||
DatasourceType string `json:"datasourceType"`
|
||||
LastSamples int `json:"lastSamples"`
|
||||
// ID is an unique Alert's ID within a group
|
||||
ID string `json:"id"`
|
||||
// GroupID is an unique Group's ID
|
||||
GroupID string `json:"group_id"`
|
||||
}
|
||||
|
||||
@@ -27,6 +27,15 @@ vmauth-ppc64le-prod:
|
||||
vmauth-386-prod:
|
||||
APP_NAME=vmauth $(MAKE) app-via-docker-386
|
||||
|
||||
vmauth-darwin-amd64-prod:
|
||||
APP_NAME=vmauth $(MAKE) app-via-docker-darwin-amd64
|
||||
|
||||
vmauth-darwin-arm64-prod:
|
||||
APP_NAME=vmauth $(MAKE) app-via-docker-darwin-arm64
|
||||
|
||||
vmauth-windows-amd64-prod:
|
||||
APP_NAME=vmauth $(MAKE) app-via-docker-windows-amd64
|
||||
|
||||
package-vmauth:
|
||||
APP_NAME=vmauth $(MAKE) package-via-docker
|
||||
|
||||
@@ -80,6 +89,3 @@ vmauth-pure:
|
||||
|
||||
vmauth-windows-amd64:
|
||||
GOARCH=amd64 APP_NAME=vmauth $(MAKE) app-local-windows-with-goarch
|
||||
|
||||
vmauth-windows-amd64-prod:
|
||||
APP_NAME=vmauth $(MAKE) app-via-docker-windows-amd64
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
# vmauth
|
||||
|
||||
`vmauth` is a simple auth proxy, router and [load balancer](#load-balancing) for [VictoriaMetrics](https://github.com/VictoriaMetrics/VictoriaMetrics).
|
||||
It reads auth credentials from `Authorization` http header ([Basic Auth](https://en.wikipedia.org/wiki/Basic_access_authentication) and `Bearer token` is supported),
|
||||
It reads auth credentials from `Authorization` http header ([Basic Auth](https://en.wikipedia.org/wiki/Basic_access_authentication), `Bearer token` and [InfluxDB authorization](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1897) is supported),
|
||||
matches them against configs pointed by [-auth.config](#auth-config) command-line flag and proxies incoming HTTP requests to the configured per-user `url_prefix` on successful match.
|
||||
|
||||
The `-auth.config` can point to either local file or to http url.
|
||||
|
||||
## Quick start
|
||||
|
||||
@@ -26,27 +26,34 @@ Pass `-help` to `vmauth` in order to see all the supported command-line flags wi
|
||||
Feel free [contacting us](mailto:info@victoriametrics.com) if you need customized auth proxy for VictoriaMetrics with the support of LDAP, SSO, RBAC, SAML,
|
||||
accounting and rate limiting such as [vmgateway](https://docs.victoriametrics.com/vmgateway.html).
|
||||
|
||||
|
||||
## Load balancing
|
||||
|
||||
Each `url_prefix` in the [-auth.config](#auth-config) may contain either a single url or a list of urls. In the latter case `vmauth` balances load among the configured urls in a round-robin manner. This feature is useful for balancing the load among multiple `vmselect` and/or `vminsert` nodes in [VictoriaMetrics cluster](https://docs.victoriametrics.com/Cluster-VictoriaMetrics.html).
|
||||
|
||||
|
||||
## Auth config
|
||||
|
||||
`-auth.config` is represented in the following simple `yml` format:
|
||||
|
||||
```yml
|
||||
|
||||
# Arbitrary number of usernames may be put here.
|
||||
# Usernames must be unique.
|
||||
# Username and bearer_token values must be unique.
|
||||
|
||||
users:
|
||||
# Requests with the 'Authorization: Bearer XXXX' header are proxied to http://localhost:8428 .
|
||||
# Requests with the 'Authorization: Bearer XXXX' and 'Authorization: Token XXXX'
|
||||
# header are proxied to http://localhost:8428 .
|
||||
# For example, http://vmauth:8427/api/v1/query is proxied to http://localhost:8428/api/v1/query
|
||||
# Requests with the Basic Auth username=XXXX are proxied to http://localhost:8428 as well.
|
||||
- bearer_token: "XXXX"
|
||||
url_prefix: "http://localhost:8428"
|
||||
|
||||
# Requests with the 'Authorization: Bearer YYY' header are proxied to http://localhost:8428 ,
|
||||
# The `X-Scope-OrgID: foobar` http header is added to every proxied request.
|
||||
# For example, http://vmauth:8427/api/v1/query is proxied to http://localhost:8428/api/v1/query
|
||||
- bearer_token: "YYY"
|
||||
url_prefix: "http://localhost:8428"
|
||||
headers:
|
||||
- "X-Scope-OrgID: foobar"
|
||||
|
||||
# The user for querying local single-node VictoriaMetrics.
|
||||
# All the requests to http://vmauth:8427 with the given Basic Auth (username:password)
|
||||
# will be proxied to http://localhost:8428 .
|
||||
@@ -89,7 +96,6 @@ users:
|
||||
- "http://vminsert1:8480/insert/42/prometheus"
|
||||
- "http://vminsert2:8480/insert/42/prometheus"
|
||||
|
||||
|
||||
# A single user for querying and inserting data:
|
||||
# - Requests to http://vmauth:8427/api/v1/query, http://vmauth:8427/api/v1/query_range
|
||||
# and http://vmauth:8427/api/v1/label/<label_name>/values are proxied to the following urls in a round-robin manner:
|
||||
@@ -97,7 +103,8 @@ users:
|
||||
# - http://vmselect2:8481/select/42/prometheus
|
||||
# For example, http://vmauth:8427/api/v1/query is proxied to http://vmselect1:8480/select/42/prometheus/api/v1/query
|
||||
# or to http://vmselect2:8480/select/42/prometheus/api/v1/query .
|
||||
# - Requests to http://vmauth:8427/api/v1/write are proxied to http://vminsert:8480/insert/42/prometheus/api/v1/write
|
||||
# - Requests to http://vmauth:8427/api/v1/write are proxied to http://vminsert:8480/insert/42/prometheus/api/v1/write .
|
||||
# The "X-Scope-OrgID: abc" http header is added to these requests.
|
||||
- username: "foobar"
|
||||
url_map:
|
||||
- src_paths:
|
||||
@@ -109,14 +116,17 @@ users:
|
||||
- "http://vmselect2:8481/select/42/prometheus"
|
||||
- src_paths: ["/api/v1/write"]
|
||||
url_prefix: "http://vminsert:8480/insert/42/prometheus"
|
||||
headers:
|
||||
- "X-Scope-OrgID: abc"
|
||||
```
|
||||
|
||||
The config may contain `%{ENV_VAR}` placeholders, which are substituted by the corresponding `ENV_VAR` environment variable values.
|
||||
This may be useful for passing secrets to the config.
|
||||
|
||||
|
||||
## Security
|
||||
|
||||
It is expected that all the backend services protected by `vmauth` are located in an isolated private network, so they can be accessed by external users only via `vmauth`.
|
||||
|
||||
Do not transfer Basic Auth headers in plaintext over untrusted networks. Enable https. This can be done by passing the following `-tls*` command-line flags to `vmauth`:
|
||||
|
||||
```
|
||||
@@ -132,21 +142,27 @@ Alternatively, [https termination proxy](https://en.wikipedia.org/wiki/TLS_termi
|
||||
|
||||
It is recommended protecting `/-/reload` endpoint with `-reloadAuthKey` command-line flag, so external users couldn't trigger config reload.
|
||||
|
||||
|
||||
## Monitoring
|
||||
|
||||
`vmauth` exports various metrics in Prometheus exposition format at `http://vmauth-host:8427/metrics` page. It is recommended setting up regular scraping of this page
|
||||
either via [vmagent](https://docs.victoriametrics.com/vmagent.html) or via Prometheus, so the exported metrics could be analyzed later.
|
||||
|
||||
`vmauth` exports `vmauth_user_requests_total` metric with `username` label. The `username` label value equals to `username` field value set in the `-auth.config` file. It is possible to override or hide the value in the label by specifying `name` field. For example, the following config will result in `vmauth_user_requests_total{username="foobar"}` instead of `vmauth_user_requests_total{username="secret_user"}`:
|
||||
|
||||
```yml
|
||||
users:
|
||||
- username: "secret_user"
|
||||
name: "foobar"
|
||||
# other config options here
|
||||
```
|
||||
|
||||
## How to build from sources
|
||||
|
||||
It is recommended using [binary releases](https://github.com/VictoriaMetrics/VictoriaMetrics/releases) - `vmauth` is located in `vmutils-*` archives there.
|
||||
|
||||
|
||||
### Development build
|
||||
|
||||
1. [Install Go](https://golang.org/doc/install). The minimum supported version is Go 1.16.
|
||||
1. [Install Go](https://golang.org/doc/install). The minimum supported version is Go 1.17.
|
||||
2. Run `make vmauth` from the root folder of [the repository](https://github.com/VictoriaMetrics/VictoriaMetrics).
|
||||
It builds `vmauth` binary and puts it into the `bin` folder.
|
||||
|
||||
@@ -169,28 +185,34 @@ by setting it via `<ROOT_IMAGE>` environment variable. For example, the followin
|
||||
ROOT_IMAGE=scratch make package-vmauth
|
||||
```
|
||||
|
||||
|
||||
## Profiling
|
||||
|
||||
`vmauth` provides handlers for collecting the following [Go profiles](https://blog.golang.org/profiling-go-programs):
|
||||
|
||||
* Memory profile. It can be collected with the following command:
|
||||
* Memory profile. It can be collected with the following command (replace `0.0.0.0` with hostname if needed):
|
||||
|
||||
<div class="with-copy" markdown="1">
|
||||
|
||||
```bash
|
||||
curl -s http://<vmauth-host>:8427/debug/pprof/heap > mem.pprof
|
||||
curl http://0.0.0.0:8427/debug/pprof/heap > mem.pprof
|
||||
```
|
||||
|
||||
* CPU profile. It can be collected with the following command:
|
||||
</div>
|
||||
|
||||
* CPU profile. It can be collected with the following command (replace `0.0.0.0` with hostname if needed):
|
||||
|
||||
<div class="with-copy" markdown="1">
|
||||
|
||||
```bash
|
||||
curl -s http://<vmauth-host>:8427/debug/pprof/profile > cpu.pprof
|
||||
curl http://0.0.0.0:8427/debug/pprof/profile > cpu.pprof
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
The command for collecting CPU profile waits for 30 seconds before returning.
|
||||
|
||||
The collected profiles may be analyzed with [go tool pprof](https://github.com/google/pprof).
|
||||
|
||||
|
||||
## Advanced usage
|
||||
|
||||
Pass `-help` command-line arg to `vmauth` in order to see all the configuration options:
|
||||
@@ -203,13 +225,15 @@ vmauth authenticates and authorizes incoming requests and proxies them to Victor
|
||||
See the docs at https://docs.victoriametrics.com/vmauth.html .
|
||||
|
||||
-auth.config string
|
||||
Path to auth config. See https://docs.victoriametrics.com/vmauth.html for details on the format of this auth config
|
||||
Path to auth config. It can point either to local file or to http url. See https://docs.victoriametrics.com/vmauth.html for details on the format of this auth config
|
||||
-enableTCP6
|
||||
Whether to enable IPv6 for listening and dialing. By default only IPv4 TCP and UDP is used
|
||||
-envflag.enable
|
||||
Whether to enable reading flags from environment variables additionally to command line. Command line flag values have priority over values from environment vars. Flags are read only from command line if this flag isn't set. See https://docs.victoriametrics.com/#environment-variables for more details
|
||||
-envflag.prefix string
|
||||
Prefix for environment variables if -envflag.enable is set
|
||||
-eula
|
||||
By specifying this flag, you confirm that you have an enterprise license and accept the EULA https://victoriametrics.com/assets/VM_EULA.pdf
|
||||
-fs.disableMmap
|
||||
Whether to use pread() instead of mmap() for reading data files. By default mmap() is used for 64-bit arches and pread() is used for 32-bit arches, since they cannot read data files bigger than 2^32 bytes in memory. mmap() is usually faster for reading small data chunks than pread()
|
||||
-http.connTimeout duration
|
||||
@@ -231,7 +255,7 @@ See the docs at https://docs.victoriametrics.com/vmauth.html .
|
||||
-httpListenAddr string
|
||||
TCP address to listen for http connections (default ":8427")
|
||||
-logInvalidAuthTokens
|
||||
Whether to log requests with invalid auth tokens. Such requests are always counted at vmagent_http_request_errors_total{reason="invalid_auth_token"} metric, which is exposed at /metrics page
|
||||
Whether to log requests with invalid auth tokens. Such requests are always counted at vmauth_http_request_errors_total{reason="invalid_auth_token"} metric, which is exposed at /metrics page
|
||||
-loggerDisableTimestamps
|
||||
Whether to disable writing timestamps in logs
|
||||
-loggerErrorsPerSecondLimit int
|
||||
@@ -254,17 +278,17 @@ See the docs at https://docs.victoriametrics.com/vmauth.html .
|
||||
-memory.allowedPercent float
|
||||
Allowed percent of system memory VictoriaMetrics caches may occupy. See also -memory.allowedBytes. Too low a value may increase cache miss rate usually resulting in higher CPU and disk IO usage. Too high a value may evict too much data from OS page cache which will result in higher disk IO usage (default 60)
|
||||
-metricsAuthKey string
|
||||
Auth key for /metrics. It overrides httpAuth settings
|
||||
Auth key for /metrics. It must be passed via authKey query arg. It overrides httpAuth.* settings
|
||||
-pprofAuthKey string
|
||||
Auth key for /debug/pprof. It overrides httpAuth settings
|
||||
Auth key for /debug/pprof. It must be passed via authKey query arg. It overrides httpAuth.* settings
|
||||
-reloadAuthKey string
|
||||
Auth key for /-/reload http endpoint. It must be passed as authKey=...
|
||||
-tls
|
||||
Whether to enable TLS (aka HTTPS) for incoming requests. -tlsCertFile and -tlsKeyFile must be set if -tls is set
|
||||
-tlsCertFile string
|
||||
Path to file with TLS certificate. Used only if -tls is set. Prefer ECDSA certs instead of RSA certs as RSA certs are slower
|
||||
Path to file with TLS certificate. Used only if -tls is set. Prefer ECDSA certs instead of RSA certs as RSA certs are slower. The provided certificate file is automatically re-read every second, so it can be dynamically updated
|
||||
-tlsKeyFile string
|
||||
Path to file with TLS key. Used only if -tls is set
|
||||
Path to file with TLS key. Used only if -tls is set. The provided key file is automatically re-read every second, so it can be dynamically updated
|
||||
-version
|
||||
Show VictoriaMetrics version
|
||||
```
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"encoding/base64"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/url"
|
||||
"os"
|
||||
"regexp"
|
||||
@@ -14,6 +13,7 @@ import (
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/envtemplate"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/fs"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/procutil"
|
||||
"github.com/VictoriaMetrics/metrics"
|
||||
@@ -21,30 +21,60 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
authConfigPath = flag.String("auth.config", "", "Path to auth config. See https://docs.victoriametrics.com/vmauth.html "+
|
||||
"for details on the format of this auth config")
|
||||
authConfigPath = flag.String("auth.config", "", "Path to auth config. It can point either to local file or to http url. "+
|
||||
"See https://docs.victoriametrics.com/vmauth.html for details on the format of this auth config")
|
||||
)
|
||||
|
||||
// AuthConfig represents auth config.
|
||||
type AuthConfig struct {
|
||||
Users []UserInfo `yaml:"users"`
|
||||
Users []UserInfo `yaml:"users,omitempty"`
|
||||
}
|
||||
|
||||
// UserInfo is user information read from authConfigPath
|
||||
type UserInfo struct {
|
||||
BearerToken string `yaml:"bearer_token"`
|
||||
Username string `yaml:"username"`
|
||||
Password string `yaml:"password"`
|
||||
URLPrefix *URLPrefix `yaml:"url_prefix"`
|
||||
URLMap []URLMap `yaml:"url_map"`
|
||||
Name string `yaml:"name,omitempty"`
|
||||
BearerToken string `yaml:"bearer_token,omitempty"`
|
||||
Username string `yaml:"username,omitempty"`
|
||||
Password string `yaml:"password,omitempty"`
|
||||
URLPrefix *URLPrefix `yaml:"url_prefix,omitempty"`
|
||||
URLMap []URLMap `yaml:"url_map,omitempty"`
|
||||
Headers []Header `yaml:"headers,omitempty"`
|
||||
|
||||
requests *metrics.Counter
|
||||
}
|
||||
|
||||
// Header is `Name: Value` http header, which must be added to the proxied request.
|
||||
type Header struct {
|
||||
Name string
|
||||
Value string
|
||||
}
|
||||
|
||||
// UnmarshalYAML unmarshals h from f.
|
||||
func (h *Header) UnmarshalYAML(f func(interface{}) error) error {
|
||||
var s string
|
||||
if err := f(&s); err != nil {
|
||||
return err
|
||||
}
|
||||
n := strings.IndexByte(s, ':')
|
||||
if n < 0 {
|
||||
return fmt.Errorf("missing speparator char ':' between Name and Value in the header %q; expected format - 'Name: Value'", s)
|
||||
}
|
||||
h.Name = strings.TrimSpace(s[:n])
|
||||
h.Value = strings.TrimSpace(s[n+1:])
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarshalYAML marshals h to yaml.
|
||||
func (h *Header) MarshalYAML() (interface{}, error) {
|
||||
s := fmt.Sprintf("%s: %s", h.Name, h.Value)
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// URLMap is a mapping from source paths to target urls.
|
||||
type URLMap struct {
|
||||
SrcPaths []*SrcPath `yaml:"src_paths"`
|
||||
URLPrefix *URLPrefix `yaml:"url_prefix"`
|
||||
SrcPaths []*SrcPath `yaml:"src_paths,omitempty"`
|
||||
URLPrefix *URLPrefix `yaml:"url_prefix,omitempty"`
|
||||
Headers []Header `yaml:"headers,omitempty"`
|
||||
}
|
||||
|
||||
// SrcPath represents an src path
|
||||
@@ -207,9 +237,9 @@ var authConfigWG sync.WaitGroup
|
||||
var stopCh chan struct{}
|
||||
|
||||
func readAuthConfig(path string) (map[string]*UserInfo, error) {
|
||||
data, err := ioutil.ReadFile(path)
|
||||
data, err := fs.ReadFileOrHTTP(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot read %q: %w", path, err)
|
||||
return nil, err
|
||||
}
|
||||
m, err := parseAuthConfig(data)
|
||||
if err != nil {
|
||||
@@ -246,9 +276,12 @@ func parseAuthConfig(data []byte) (map[string]*UserInfo, error) {
|
||||
if byUsername[ui.Username] {
|
||||
return nil, fmt.Errorf("duplicate username found; username: %q", ui.Username)
|
||||
}
|
||||
authToken := getAuthToken(ui.BearerToken, ui.Username, ui.Password)
|
||||
if byAuthToken[authToken] != nil {
|
||||
return nil, fmt.Errorf("duplicate auth token found for bearer_token=%q, username=%q: %q", authToken, ui.BearerToken, ui.Username)
|
||||
at1, at2 := getAuthTokens(ui.BearerToken, ui.Username, ui.Password)
|
||||
if byAuthToken[at1] != nil {
|
||||
return nil, fmt.Errorf("duplicate auth token found for bearer_token=%q, username=%q: %q", ui.BearerToken, ui.Username, at1)
|
||||
}
|
||||
if byAuthToken[at2] != nil {
|
||||
return nil, fmt.Errorf("duplicate auth token found for bearer_token=%q, username=%q: %q", ui.BearerToken, ui.Username, at2)
|
||||
}
|
||||
if ui.URLPrefix != nil {
|
||||
if err := ui.URLPrefix.sanitize(); err != nil {
|
||||
@@ -270,21 +303,41 @@ func parseAuthConfig(data []byte) (map[string]*UserInfo, error) {
|
||||
return nil, fmt.Errorf("missing `url_prefix`")
|
||||
}
|
||||
if ui.BearerToken != "" {
|
||||
name := "bearer_token"
|
||||
if ui.Name != "" {
|
||||
name = ui.Name
|
||||
}
|
||||
if ui.Password != "" {
|
||||
return nil, fmt.Errorf("password shouldn't be set for bearer_token %q", ui.BearerToken)
|
||||
}
|
||||
ui.requests = metrics.GetOrCreateCounter(`vmauth_user_requests_total{username="bearer_token"}`)
|
||||
ui.requests = metrics.GetOrCreateCounter(fmt.Sprintf(`vmauth_user_requests_total{username=%q}`, name))
|
||||
byBearerToken[ui.BearerToken] = true
|
||||
}
|
||||
if ui.Username != "" {
|
||||
ui.requests = metrics.GetOrCreateCounter(fmt.Sprintf(`vmauth_user_requests_total{username=%q}`, ui.Username))
|
||||
name := ui.Username
|
||||
if ui.Name != "" {
|
||||
name = ui.Name
|
||||
}
|
||||
ui.requests = metrics.GetOrCreateCounter(fmt.Sprintf(`vmauth_user_requests_total{username=%q}`, name))
|
||||
byUsername[ui.Username] = true
|
||||
}
|
||||
byAuthToken[authToken] = ui
|
||||
byAuthToken[at1] = ui
|
||||
byAuthToken[at2] = ui
|
||||
}
|
||||
return byAuthToken, nil
|
||||
}
|
||||
|
||||
func getAuthTokens(bearerToken, username, password string) (string, string) {
|
||||
if bearerToken != "" {
|
||||
// Accept the bearerToken as Basic Auth username with empty password
|
||||
at1 := getAuthToken(bearerToken, "", "")
|
||||
at2 := getAuthToken("", bearerToken, "")
|
||||
return at1, at2
|
||||
}
|
||||
at := getAuthToken("", username, password)
|
||||
return at, at
|
||||
}
|
||||
|
||||
func getAuthToken(bearerToken, username, password string) string {
|
||||
if bearerToken != "" {
|
||||
return "Bearer " + bearerToken
|
||||
|
||||
@@ -69,6 +69,14 @@ users:
|
||||
- [foo]
|
||||
`)
|
||||
|
||||
// Invalid headers
|
||||
f(`
|
||||
users:
|
||||
- username: foo
|
||||
url_prefix: http://foo.bar
|
||||
headers: foobar
|
||||
`)
|
||||
|
||||
// empty url_prefix
|
||||
f(`
|
||||
users:
|
||||
@@ -156,6 +164,27 @@ users:
|
||||
- src_paths: ['fo[obar']
|
||||
url_prefix: http://foobar
|
||||
`)
|
||||
|
||||
// Invalid headers in url_map (missing ':')
|
||||
f(`
|
||||
users:
|
||||
- username: a
|
||||
url_map:
|
||||
- src_paths: ['/foobar']
|
||||
url_prefix: http://foobar
|
||||
headers:
|
||||
- foobar
|
||||
`)
|
||||
// Invalid headers in url_map (dictionary instead of array)
|
||||
f(`
|
||||
users:
|
||||
- username: a
|
||||
url_map:
|
||||
- src_paths: ['/foobar']
|
||||
url_prefix: http://foobar
|
||||
headers:
|
||||
aaa: bbb
|
||||
`)
|
||||
}
|
||||
|
||||
func TestParseAuthConfigSuccess(t *testing.T) {
|
||||
@@ -231,6 +260,9 @@ users:
|
||||
url_prefix: http://vmselect/select/0/prometheus
|
||||
- src_paths: ["/api/v1/write"]
|
||||
url_prefix: ["http://vminsert1/insert/0/prometheus","http://vminsert2/insert/0/prometheus"]
|
||||
headers:
|
||||
- "foo: bar"
|
||||
- "xxx: y"
|
||||
`, map[string]*UserInfo{
|
||||
getAuthToken("foo", "", ""): {
|
||||
BearerToken: "foo",
|
||||
@@ -245,6 +277,42 @@ users:
|
||||
"http://vminsert1/insert/0/prometheus",
|
||||
"http://vminsert2/insert/0/prometheus",
|
||||
}),
|
||||
Headers: []Header{
|
||||
{
|
||||
Name: "foo",
|
||||
Value: "bar",
|
||||
},
|
||||
{
|
||||
Name: "xxx",
|
||||
Value: "y",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
getAuthToken("", "foo", ""): {
|
||||
BearerToken: "foo",
|
||||
URLMap: []URLMap{
|
||||
{
|
||||
SrcPaths: getSrcPaths([]string{"/api/v1/query", "/api/v1/query_range", "/api/v1/label/[^./]+/.+"}),
|
||||
URLPrefix: mustParseURL("http://vmselect/select/0/prometheus"),
|
||||
},
|
||||
{
|
||||
SrcPaths: getSrcPaths([]string{"/api/v1/write"}),
|
||||
URLPrefix: mustParseURLs([]string{
|
||||
"http://vminsert1/insert/0/prometheus",
|
||||
"http://vminsert2/insert/0/prometheus",
|
||||
}),
|
||||
Headers: []Header{
|
||||
{
|
||||
Name: "foo",
|
||||
Value: "bar",
|
||||
},
|
||||
{
|
||||
Name: "xxx",
|
||||
Value: "y",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,12 +1,22 @@
|
||||
# Arbitrary number of usernames may be put here.
|
||||
# Usernames must be unique.
|
||||
# Username and bearer_token values must be unique.
|
||||
|
||||
users:
|
||||
# Requests with the 'Authorization: Bearer XXXX' header are proxied to http://localhost:8428 .
|
||||
# Requests with the 'Authorization: Bearer XXXX' and 'Authorization: Token XXXX'
|
||||
# header are proxied to http://localhost:8428 .
|
||||
# For example, http://vmauth:8427/api/v1/query is proxied to http://localhost:8428/api/v1/query
|
||||
# Requests with the Basic Auth username=XXXX are proxied to http://localhost:8428 as well.
|
||||
- bearer_token: "XXXX"
|
||||
url_prefix: "http://localhost:8428"
|
||||
|
||||
# Requests with the 'Authorization: Bearer YYY' header are proxied to http://localhost:8428 ,
|
||||
# The `X-Scope-OrgID: foobar` http header is added to every proxied request.
|
||||
# For example, http://vmauth:8427/api/v1/query is proxied to http://localhost:8428/api/v1/query
|
||||
- bearer_token: "YYY"
|
||||
url_prefix: "http://localhost:8428"
|
||||
headers:
|
||||
- "X-Scope-OrgID: foobar"
|
||||
|
||||
# The user for querying local single-node VictoriaMetrics.
|
||||
# All the requests to http://vmauth:8427 with the given Basic Auth (username:password)
|
||||
# will be proxied to http://localhost:8428 .
|
||||
@@ -49,7 +59,6 @@ users:
|
||||
- "http://vminsert1:8480/insert/42/prometheus"
|
||||
- "http://vminsert2:8480/insert/42/prometheus"
|
||||
|
||||
|
||||
# A single user for querying and inserting data:
|
||||
# - Requests to http://vmauth:8427/api/v1/query, http://vmauth:8427/api/v1/query_range
|
||||
# and http://vmauth:8427/api/v1/label/<label_name>/values are proxied to the following urls in a round-robin manner:
|
||||
@@ -57,7 +66,8 @@ users:
|
||||
# - http://vmselect2:8481/select/42/prometheus
|
||||
# For example, http://vmauth:8427/api/v1/query is proxied to http://vmselect1:8480/select/42/prometheus/api/v1/query
|
||||
# or to http://vmselect2:8480/select/42/prometheus/api/v1/query .
|
||||
# - Requests to http://vmauth:8427/api/v1/write are proxied to http://vminsert:8480/insert/42/prometheus/api/v1/write
|
||||
# - Requests to http://vmauth:8427/api/v1/write are proxied to http://vminsert:8480/insert/42/prometheus/api/v1/write .
|
||||
# The "X-Scope-OrgID: abc" http header is added to these requests.
|
||||
- username: "foobar"
|
||||
url_map:
|
||||
- src_paths:
|
||||
@@ -69,3 +79,5 @@ users:
|
||||
- "http://vmselect2:8481/select/42/prometheus"
|
||||
- src_paths: ["/api/v1/write"]
|
||||
url_prefix: "http://vminsert:8480/insert/42/prometheus"
|
||||
headers:
|
||||
- "X-Scope-OrgID: abc"
|
||||
|
||||
@@ -7,6 +7,8 @@ import (
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/buildinfo"
|
||||
@@ -23,7 +25,7 @@ var (
|
||||
maxIdleConnsPerBackend = flag.Int("maxIdleConnsPerBackend", 100, "The maximum number of idle connections vmauth can open per each backend host")
|
||||
reloadAuthKey = flag.String("reloadAuthKey", "", "Auth key for /-/reload http endpoint. It must be passed as authKey=...")
|
||||
logInvalidAuthTokens = flag.Bool("logInvalidAuthTokens", false, "Whether to log requests with invalid auth tokens. "+
|
||||
`Such requests are always counted at vmagent_http_request_errors_total{reason="invalid_auth_token"} metric, which is exposed at /metrics page`)
|
||||
`Such requests are always counted at vmauth_http_request_errors_total{reason="invalid_auth_token"} metric, which is exposed at /metrics page`)
|
||||
)
|
||||
|
||||
func main() {
|
||||
@@ -71,6 +73,11 @@ func requestHandler(w http.ResponseWriter, r *http.Request) bool {
|
||||
http.Error(w, "missing `Authorization` request header", http.StatusUnauthorized)
|
||||
return true
|
||||
}
|
||||
if strings.HasPrefix(authToken, "Token ") {
|
||||
// Handle InfluxDB's proprietary token authentication scheme as a bearer token authentication
|
||||
// See https://docs.influxdata.com/influxdb/v2.0/api/
|
||||
authToken = strings.Replace(authToken, "Token", "Bearer", 1)
|
||||
}
|
||||
ac := authConfig.Load().(map[string]*UserInfo)
|
||||
ui := ac[authToken]
|
||||
if ui == nil {
|
||||
@@ -84,12 +91,15 @@ func requestHandler(w http.ResponseWriter, r *http.Request) bool {
|
||||
return true
|
||||
}
|
||||
ui.requests.Inc()
|
||||
targetURL, err := createTargetURL(ui, r.URL)
|
||||
targetURL, headers, err := createTargetURL(ui, r.URL)
|
||||
if err != nil {
|
||||
httpserver.Errorf(w, r, "cannot determine targetURL: %s", err)
|
||||
return true
|
||||
}
|
||||
r.Header.Set("vm-target-url", targetURL.String())
|
||||
for _, h := range headers {
|
||||
r.Header.Set(h.Name, h.Value)
|
||||
}
|
||||
proxyRequest(w, r)
|
||||
return true
|
||||
}
|
||||
@@ -105,35 +115,51 @@ func proxyRequest(w http.ResponseWriter, r *http.Request) {
|
||||
// Forward other panics to the caller.
|
||||
panic(err)
|
||||
}()
|
||||
reverseProxy.ServeHTTP(w, r)
|
||||
getReverseProxy().ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
var (
|
||||
configReloadRequests = metrics.NewCounter(`vmagent_http_requests_total{path="/-/reload"}`)
|
||||
invalidAuthTokenRequests = metrics.NewCounter(`vmagent_http_request_errors_total{reason="invalid_auth_token"}`)
|
||||
missingRouteRequests = metrics.NewCounter(`vmagent_http_request_errors_total{reason="missing_route"}`)
|
||||
configReloadRequests = metrics.NewCounter(`vmauth_http_requests_total{path="/-/reload"}`)
|
||||
invalidAuthTokenRequests = metrics.NewCounter(`vmauth_http_request_errors_total{reason="invalid_auth_token"}`)
|
||||
missingRouteRequests = metrics.NewCounter(`vmauth_http_request_errors_total{reason="missing_route"}`)
|
||||
)
|
||||
|
||||
var reverseProxy = &httputil.ReverseProxy{
|
||||
Director: func(r *http.Request) {
|
||||
targetURL := r.Header.Get("vm-target-url")
|
||||
target, err := url.Parse(targetURL)
|
||||
if err != nil {
|
||||
logger.Panicf("BUG: unexpected error when parsing targetURL=%q: %s", targetURL, err)
|
||||
}
|
||||
r.URL = target
|
||||
},
|
||||
Transport: func() *http.Transport {
|
||||
tr := http.DefaultTransport.(*http.Transport).Clone()
|
||||
// Automatic compression must be disabled in order to fix https://github.com/VictoriaMetrics/VictoriaMetrics/issues/535
|
||||
tr.DisableCompression = true
|
||||
// Disable HTTP/2.0, since VictoriaMetrics components don't support HTTP/2.0 (because there is no sense in this).
|
||||
tr.ForceAttemptHTTP2 = false
|
||||
tr.MaxIdleConnsPerHost = *maxIdleConnsPerBackend
|
||||
return tr
|
||||
}(),
|
||||
FlushInterval: time.Second,
|
||||
ErrorLog: logger.StdErrorLogger(),
|
||||
var (
|
||||
reverseProxy *httputil.ReverseProxy
|
||||
reverseProxyOnce sync.Once
|
||||
)
|
||||
|
||||
func getReverseProxy() *httputil.ReverseProxy {
|
||||
reverseProxyOnce.Do(initReverseProxy)
|
||||
return reverseProxy
|
||||
}
|
||||
|
||||
// initReverseProxy must be called after flag.Parse(), since it uses command-line flags.
|
||||
func initReverseProxy() {
|
||||
reverseProxy = &httputil.ReverseProxy{
|
||||
Director: func(r *http.Request) {
|
||||
targetURL := r.Header.Get("vm-target-url")
|
||||
target, err := url.Parse(targetURL)
|
||||
if err != nil {
|
||||
logger.Panicf("BUG: unexpected error when parsing targetURL=%q: %s", targetURL, err)
|
||||
}
|
||||
r.URL = target
|
||||
},
|
||||
Transport: func() *http.Transport {
|
||||
tr := http.DefaultTransport.(*http.Transport).Clone()
|
||||
// Automatic compression must be disabled in order to fix https://github.com/VictoriaMetrics/VictoriaMetrics/issues/535
|
||||
tr.DisableCompression = true
|
||||
// Disable HTTP/2.0, since VictoriaMetrics components don't support HTTP/2.0 (because there is no sense in this).
|
||||
tr.ForceAttemptHTTP2 = false
|
||||
tr.MaxIdleConnsPerHost = *maxIdleConnsPerBackend
|
||||
if tr.MaxIdleConns != 0 && tr.MaxIdleConns < tr.MaxIdleConnsPerHost {
|
||||
tr.MaxIdleConns = tr.MaxIdleConnsPerHost
|
||||
}
|
||||
return tr
|
||||
}(),
|
||||
FlushInterval: time.Second,
|
||||
ErrorLog: logger.StdErrorLogger(),
|
||||
}
|
||||
}
|
||||
|
||||
func usage() {
|
||||
|
||||
@@ -35,7 +35,7 @@ func mergeURLs(uiURL, requestURI *url.URL) *url.URL {
|
||||
return &targetURL
|
||||
}
|
||||
|
||||
func createTargetURL(ui *UserInfo, uOrig *url.URL) (*url.URL, error) {
|
||||
func createTargetURL(ui *UserInfo, uOrig *url.URL) (*url.URL, []Header, error) {
|
||||
u := *uOrig
|
||||
// Prevent from attacks with using `..` in r.URL.Path
|
||||
u.Path = path.Clean(u.Path)
|
||||
@@ -46,13 +46,13 @@ func createTargetURL(ui *UserInfo, uOrig *url.URL) (*url.URL, error) {
|
||||
for _, e := range ui.URLMap {
|
||||
for _, sp := range e.SrcPaths {
|
||||
if sp.match(u.Path) {
|
||||
return e.URLPrefix.mergeURLs(&u), nil
|
||||
return e.URLPrefix.mergeURLs(&u), e.Headers, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
if ui.URLPrefix != nil {
|
||||
return ui.URLPrefix.mergeURLs(&u), nil
|
||||
return ui.URLPrefix.mergeURLs(&u), ui.Headers, nil
|
||||
}
|
||||
missingRouteRequests.Inc()
|
||||
return nil, fmt.Errorf("missing route for %q", u.String())
|
||||
return nil, nil, fmt.Errorf("missing route for %q", u.String())
|
||||
}
|
||||
|
||||
@@ -1,47 +1,56 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCreateTargetURLSuccess(t *testing.T) {
|
||||
f := func(ui *UserInfo, requestURI, expectedTarget string) {
|
||||
f := func(ui *UserInfo, requestURI, expectedTarget, expectedHeaders string) {
|
||||
t.Helper()
|
||||
u, err := url.Parse(requestURI)
|
||||
if err != nil {
|
||||
t.Fatalf("cannot parse %q: %s", requestURI, err)
|
||||
}
|
||||
target, err := createTargetURL(ui, u)
|
||||
target, headers, err := createTargetURL(ui, u)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %s", err)
|
||||
}
|
||||
if target.String() != expectedTarget {
|
||||
t.Fatalf("unexpected target; got %q; want %q", target, expectedTarget)
|
||||
}
|
||||
headersStr := fmt.Sprintf("%q", headers)
|
||||
if headersStr != expectedHeaders {
|
||||
t.Fatalf("unexpected headers; got %s; want %s", headersStr, expectedHeaders)
|
||||
}
|
||||
}
|
||||
// Simple routing with `url_prefix`
|
||||
f(&UserInfo{
|
||||
URLPrefix: mustParseURL("http://foo.bar"),
|
||||
}, "", "http://foo.bar/.")
|
||||
}, "", "http://foo.bar/.", "[]")
|
||||
f(&UserInfo{
|
||||
URLPrefix: mustParseURL("http://foo.bar"),
|
||||
}, "/", "http://foo.bar")
|
||||
Headers: []Header{{
|
||||
Name: "bb",
|
||||
Value: "aaa",
|
||||
}},
|
||||
}, "/", "http://foo.bar", `[{"bb" "aaa"}]`)
|
||||
f(&UserInfo{
|
||||
URLPrefix: mustParseURL("http://foo.bar/federate"),
|
||||
}, "/", "http://foo.bar/federate")
|
||||
}, "/", "http://foo.bar/federate", "[]")
|
||||
f(&UserInfo{
|
||||
URLPrefix: mustParseURL("http://foo.bar"),
|
||||
}, "a/b?c=d", "http://foo.bar/a/b?c=d")
|
||||
}, "a/b?c=d", "http://foo.bar/a/b?c=d", "[]")
|
||||
f(&UserInfo{
|
||||
URLPrefix: mustParseURL("https://sss:3894/x/y"),
|
||||
}, "/z", "https://sss:3894/x/y/z")
|
||||
}, "/z", "https://sss:3894/x/y/z", "[]")
|
||||
f(&UserInfo{
|
||||
URLPrefix: mustParseURL("https://sss:3894/x/y"),
|
||||
}, "/../../aaa", "https://sss:3894/x/y/aaa")
|
||||
}, "/../../aaa", "https://sss:3894/x/y/aaa", "[]")
|
||||
f(&UserInfo{
|
||||
URLPrefix: mustParseURL("https://sss:3894/x/y"),
|
||||
}, "/./asd/../../aaa?a=d&s=s/../d", "https://sss:3894/x/y/aaa?a=d&s=s%2F..%2Fd")
|
||||
}, "/./asd/../../aaa?a=d&s=s/../d", "https://sss:3894/x/y/aaa?a=d&s=s%2F..%2Fd", "[]")
|
||||
|
||||
// Complex routing with `url_map`
|
||||
ui := &UserInfo{
|
||||
@@ -49,6 +58,16 @@ func TestCreateTargetURLSuccess(t *testing.T) {
|
||||
{
|
||||
SrcPaths: getSrcPaths([]string{"/api/v1/query"}),
|
||||
URLPrefix: mustParseURL("http://vmselect/0/prometheus"),
|
||||
Headers: []Header{
|
||||
{
|
||||
Name: "xx",
|
||||
Value: "aa",
|
||||
},
|
||||
{
|
||||
Name: "yy",
|
||||
Value: "asdf",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
SrcPaths: getSrcPaths([]string{"/api/v1/write"}),
|
||||
@@ -56,10 +75,14 @@ func TestCreateTargetURLSuccess(t *testing.T) {
|
||||
},
|
||||
},
|
||||
URLPrefix: mustParseURL("http://default-server"),
|
||||
Headers: []Header{{
|
||||
Name: "bb",
|
||||
Value: "aaa",
|
||||
}},
|
||||
}
|
||||
f(ui, "/api/v1/query?query=up", "http://vmselect/0/prometheus/api/v1/query?query=up")
|
||||
f(ui, "/api/v1/write", "http://vminsert/0/prometheus/api/v1/write")
|
||||
f(ui, "/api/v1/query_range", "http://default-server/api/v1/query_range")
|
||||
f(ui, "/api/v1/query?query=up", "http://vmselect/0/prometheus/api/v1/query?query=up", `[{"xx" "aa"} {"yy" "asdf"}]`)
|
||||
f(ui, "/api/v1/write", "http://vminsert/0/prometheus/api/v1/write", "[]")
|
||||
f(ui, "/api/v1/query_range", "http://default-server/api/v1/query_range", `[{"bb" "aaa"}]`)
|
||||
|
||||
// Complex routing regexp paths in `url_map`
|
||||
ui = &UserInfo{
|
||||
@@ -75,17 +98,17 @@ func TestCreateTargetURLSuccess(t *testing.T) {
|
||||
},
|
||||
URLPrefix: mustParseURL("http://default-server"),
|
||||
}
|
||||
f(ui, "/api/v1/query?query=up", "http://vmselect/0/prometheus/api/v1/query?query=up")
|
||||
f(ui, "/api/v1/query_range?query=up", "http://vmselect/0/prometheus/api/v1/query_range?query=up")
|
||||
f(ui, "/api/v1/label/foo/values", "http://vmselect/0/prometheus/api/v1/label/foo/values")
|
||||
f(ui, "/api/v1/write", "http://vminsert/0/prometheus/api/v1/write")
|
||||
f(ui, "/api/v1/foo/bar", "http://default-server/api/v1/foo/bar")
|
||||
f(ui, "/api/v1/query?query=up", "http://vmselect/0/prometheus/api/v1/query?query=up", "[]")
|
||||
f(ui, "/api/v1/query_range?query=up", "http://vmselect/0/prometheus/api/v1/query_range?query=up", "[]")
|
||||
f(ui, "/api/v1/label/foo/values", "http://vmselect/0/prometheus/api/v1/label/foo/values", "[]")
|
||||
f(ui, "/api/v1/write", "http://vminsert/0/prometheus/api/v1/write", "[]")
|
||||
f(ui, "/api/v1/foo/bar", "http://default-server/api/v1/foo/bar", "[]")
|
||||
f(&UserInfo{
|
||||
URLPrefix: mustParseURL("http://foo.bar?extra_label=team=dev"),
|
||||
}, "/api/v1/query", "http://foo.bar/api/v1/query?extra_label=team=dev")
|
||||
}, "/api/v1/query", "http://foo.bar/api/v1/query?extra_label=team=dev", "[]")
|
||||
f(&UserInfo{
|
||||
URLPrefix: mustParseURL("http://foo.bar?extra_label=team=mobile"),
|
||||
}, "/api/v1/query?extra_label=team=dev", "http://foo.bar/api/v1/query?extra_label=team%3Dmobile")
|
||||
}, "/api/v1/query?extra_label=team=dev", "http://foo.bar/api/v1/query?extra_label=team%3Dmobile", "[]")
|
||||
|
||||
}
|
||||
|
||||
@@ -96,13 +119,16 @@ func TestCreateTargetURLFailure(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("cannot parse %q: %s", requestURI, err)
|
||||
}
|
||||
target, err := createTargetURL(ui, u)
|
||||
target, headers, err := createTargetURL(ui, u)
|
||||
if err == nil {
|
||||
t.Fatalf("expecting non-nil error")
|
||||
}
|
||||
if target != nil {
|
||||
t.Fatalf("unexpected target=%q; want empty string", target)
|
||||
}
|
||||
if headers != nil {
|
||||
t.Fatalf("unexpected headers=%q; want empty string", headers)
|
||||
}
|
||||
}
|
||||
f(&UserInfo{}, "/foo/bar")
|
||||
f(&UserInfo{
|
||||
|
||||
@@ -27,6 +27,12 @@ vmbackup-ppc64le-prod:
|
||||
vmbackup-386-prod:
|
||||
APP_NAME=vmbackup $(MAKE) app-via-docker-386
|
||||
|
||||
vmbackup-darwin-amd64-prod:
|
||||
APP_NAME=vmbackup $(MAKE) app-via-docker-darwin-amd64
|
||||
|
||||
vmbackup-darwin-arm64-prod:
|
||||
APP_NAME=vmbackup $(MAKE) app-via-docker-darwin-arm64
|
||||
|
||||
package-vmbackup:
|
||||
APP_NAME=vmbackup $(MAKE) package-via-docker
|
||||
|
||||
|
||||
@@ -4,13 +4,13 @@
|
||||
|
||||
Supported storage systems for backups:
|
||||
|
||||
* [GCS](https://cloud.google.com/storage/). Example: `gcs://<bucket>/<path/to/backup>`
|
||||
* [GCS](https://cloud.google.com/storage/). Example: `gs://<bucket>/<path/to/backup>`
|
||||
* [S3](https://aws.amazon.com/s3/). Example: `s3://<bucket>/<path/to/backup>`
|
||||
* Any S3-compatible storage such as [MinIO](https://github.com/minio/minio), [Ceph](https://docs.ceph.com/docs/mimic/radosgw/s3/) or [Swift](https://www.swiftstack.com/docs/admin/middleware/s3_middleware.html). See [these docs](#advanced-usage) for details.
|
||||
* Any S3-compatible storage such as [MinIO](https://github.com/minio/minio), [Ceph](https://docs.ceph.com/en/pacific/radosgw/s3/) or [Swift](https://platform.swiftstack.com/docs/admin/middleware/s3_middleware.html). See [these docs](#advanced-usage) for details.
|
||||
* Local filesystem. Example: `fs://</absolute/path/to/backup>`
|
||||
|
||||
`vmbackup` supports incremental and full backups. Incremental backups created automatically if the destination path already contains data from the previous backup.
|
||||
Full backups can be sped up with `-origin` pointing to already existing backup on the same remote storage. In this case `vmbackup` makes server-side copy for the shared
|
||||
`vmbackup` supports incremental and full backups. Incremental backups are created automatically if the destination path already contains data from the previous backup.
|
||||
Full backups can be sped up with `-origin` pointing to an already existing backup on the same remote storage. In this case `vmbackup` makes server-side copy for the shared
|
||||
data between the existing backup and new backup. It saves time and costs on data transfer.
|
||||
|
||||
Backup process can be interrupted at any time. It is automatically resumed from the interruption point when restarting `vmbackup` with the same args.
|
||||
@@ -30,11 +30,11 @@ creation of hourly, daily, weekly and monthly backups.
|
||||
Regular backup can be performed with the following command:
|
||||
|
||||
```
|
||||
vmbackup -storageDataPath=</path/to/victoria-metrics-data> -snapshotName=<local-snapshot> -dst=gcs://<bucket>/<path/to/new/backup>
|
||||
vmbackup -storageDataPath=</path/to/victoria-metrics-data> -snapshotName=<local-snapshot> -dst=gs://<bucket>/<path/to/new/backup>
|
||||
```
|
||||
|
||||
* `</path/to/victoria-metrics-data>` - path to VictoriaMetrics data pointed by `-storageDataPath` command-line flag in single-node VictoriaMetrics or in cluster `vmstorage`.
|
||||
There is no need to stop VictoriaMetrics for creating backups, since they are performed from immutable [instant snapshots](https://docs.victoriametrics.com/Single-server-VictoriaMetrics.html#how-to-work-with-snapshots).
|
||||
There is no need to stop VictoriaMetrics for creating backups since they are performed from immutable [instant snapshots](https://docs.victoriametrics.com/Single-server-VictoriaMetrics.html#how-to-work-with-snapshots).
|
||||
* `<local-snapshot>` is the snapshot to back up. See [how to create instant snapshots](https://docs.victoriametrics.com/Single-server-VictoriaMetrics.html#how-to-work-with-snapshots). `vmbackup` can create the snapshot on itself if `-snapshot.createURL` command-line flag is set to an url for creating snapshots. In this case `-snapshotName` flag isn't needed.
|
||||
* `<bucket>` is an already existing name for [GCS bucket](https://cloud.google.com/storage/docs/creating-buckets).
|
||||
* `<path/to/new/backup>` is the destination path where new backup will be placed.
|
||||
@@ -46,7 +46,7 @@ If the destination GCS bucket already contains the previous backup at `-origin`
|
||||
with the following command:
|
||||
|
||||
```
|
||||
vmbackup -storageDataPath=</path/to/victoria-metrics-data> -snapshotName=<local-snapshot> -dst=gcs://<bucket>/<path/to/new/backup> -origin=gcs://<bucket>/<path/to/existing/backup>
|
||||
vmbackup -storageDataPath=</path/to/victoria-metrics-data> -snapshotName=<local-snapshot> -dst=gs://<bucket>/<path/to/new/backup> -origin=gs://<bucket>/<path/to/existing/backup>
|
||||
```
|
||||
|
||||
It saves time and network bandwidth costs by performing server-side copy for the shared data from the `-origin` to `-dst`.
|
||||
@@ -54,11 +54,11 @@ It saves time and network bandwidth costs by performing server-side copy for the
|
||||
|
||||
### Incremental backups
|
||||
|
||||
Incremental backups performed if `-dst` points to an already existing backup. In this case only new data uploaded to remote storage.
|
||||
Incremental backups are performed if `-dst` points to an already existing backup. In this case only new data is uploaded to remote storage.
|
||||
It saves time and network bandwidth costs when working with big backups:
|
||||
|
||||
```
|
||||
vmbackup -storageDataPath=</path/to/victoria-metrics-data> -snapshotName=<local-snapshot> -dst=gcs://<bucket>/<path/to/existing/backup>
|
||||
vmbackup -storageDataPath=</path/to/victoria-metrics-data> -snapshotName=<local-snapshot> -dst=gs://<bucket>/<path/to/existing/backup>
|
||||
```
|
||||
|
||||
|
||||
@@ -69,16 +69,16 @@ Smart backups mean storing full daily backups into `YYYYMMDD` folders and creati
|
||||
* Run the following command every hour:
|
||||
|
||||
```
|
||||
vmbackup -snapshotName=<latest-snapshot> -dst=gcs://<bucket>/latest
|
||||
vmbackup -snapshotName=<latest-snapshot> -dst=gs://<bucket>/latest
|
||||
```
|
||||
|
||||
Where `<latest-snapshot>` is the latest [snapshot](https://docs.victoriametrics.com/Single-server-VictoriaMetrics.html#how-to-work-with-snapshots).
|
||||
The command will upload only changed data to `gcs://<bucket>/latest`.
|
||||
The command will upload only changed data to `gs://<bucket>/latest`.
|
||||
|
||||
* Run the following command once a day:
|
||||
|
||||
```
|
||||
vmbackup -snapshotName=<daily-snapshot> -dst=gcs://<bucket>/<YYYYMMDD> -origin=gcs://<bucket>/latest
|
||||
vmbackup -snapshotName=<daily-snapshot> -dst=gs://<bucket>/<YYYYMMDD> -origin=gs://<bucket>/latest
|
||||
```
|
||||
|
||||
Where `<daily-snapshot>` is the snapshot for the last day `<YYYYMMDD>`.
|
||||
@@ -87,7 +87,7 @@ Where `<daily-snapshot>` is the snapshot for the last day `<YYYYMMDD>`.
|
||||
This apporach saves network bandwidth costs on hourly backups (since they are incremental) and allows recovering data from either the last hour (`latest` backup)
|
||||
or from any day (`YYYYMMDD` backups). Note that hourly backup shouldn't run when creating daily backup.
|
||||
|
||||
Do not forget removing old snapshots and backups when they are no longer needed for saving storage costs.
|
||||
Do not forget to remove old snapshots and backups when they are no longer needed in order to save storage costs.
|
||||
|
||||
See also [vmbackupmanager tool](https://docs.victoriametrics.com/vmbackupmanager.html) for automating smart backups.
|
||||
|
||||
@@ -97,19 +97,19 @@ See also [vmbackupmanager tool](https://docs.victoriametrics.com/vmbackupmanager
|
||||
The backup algorithm is the following:
|
||||
|
||||
1. Collect information about files in the `-snapshotName`, in the `-dst` and in the `-origin`.
|
||||
2. Determine files in `-dst`, which are missing in `-snapshotName`, and delete them. These are usually small files, which are already merged into bigger files in the snapshot.
|
||||
3. Determine files from `-snapshotName`, which are missing in `-dst`. These are usually small new files and bigger merged files.
|
||||
4. Determine files from step 3, which exist in the `-origin`, and perform server-side copy of these files from `-origin` to `-dst`.
|
||||
2. Determine which files in `-dst` are missing in `-snapshotName`, and delete them. These are usually small files, which are already merged into bigger files in the snapshot.
|
||||
3. Determine which files in `-snapshotName` are missing in `-dst`. These are usually small new files and bigger merged files.
|
||||
4. Determine which files from step 3 exist in the `-origin`, and perform server-side copy of these files from `-origin` to `-dst`.
|
||||
These are usually the biggest and the oldest files, which are shared between backups.
|
||||
5. Upload the remaining files from step 3 from `-snapshotName` to `-dst`.
|
||||
|
||||
The algorithm splits source files into 100 MB chunks in the backup. Each chunk stored as a separate file in the backup.
|
||||
The algorithm splits source files into 1 GiB chunks in the backup. Each chunk is stored as a separate file in the backup.
|
||||
Such splitting minimizes the amounts of data to re-transfer after temporary errors.
|
||||
|
||||
`vmbackup` relies on [instant snapshot](https://medium.com/@valyala/how-victoriametrics-makes-instant-snapshots-for-multi-terabyte-time-series-data-e1f3fb0e0282) properties:
|
||||
|
||||
- All the files in the snapshot are immutable.
|
||||
- Old files periodically merged into new files.
|
||||
- Old files are periodically merged into new files.
|
||||
- Smaller files have higher probability to be merged.
|
||||
- Consecutive snapshots share many identical files.
|
||||
|
||||
@@ -183,14 +183,34 @@ See [this article](https://medium.com/@valyala/speeding-up-backups-for-big-time-
|
||||
-customS3Endpoint string
|
||||
Custom S3 endpoint for use with S3-compatible storages (e.g. MinIO). S3 is used if not set
|
||||
-dst string
|
||||
Where to put the backup on the remote storage. Example: gcs://bucket/path/to/backup/dir, s3://bucket/path/to/backup/dir or fs:///path/to/local/backup/dir
|
||||
Where to put the backup on the remote storage. Example: gs://bucket/path/to/backup/dir, s3://bucket/path/to/backup/dir or fs:///path/to/local/backup/dir
|
||||
-dst can point to the previous backup. In this case incremental backup is performed, i.e. only changed data is uploaded
|
||||
-enableTCP6
|
||||
Whether to enable IPv6 for listening and dialing. By default only IPv4 TCP and UDP is used
|
||||
-envflag.enable
|
||||
Whether to enable reading flags from environment variables additionally to command line. Command line flag values have priority over values from environment vars. Flags are read only from command line if this flag isn't set. See https://docs.victoriametrics.com/#environment-variables for more details
|
||||
-envflag.prefix string
|
||||
Prefix for environment variables if -envflag.enable is set
|
||||
-fs.disableMmap
|
||||
Whether to use pread() instead of mmap() for reading data files. By default mmap() is used for 64-bit arches and pread() is used for 32-bit arches, since they cannot read data files bigger than 2^32 bytes in memory. mmap() is usually faster for reading small data chunks than pread()
|
||||
-http.connTimeout duration
|
||||
Incoming http connections are closed after the configured timeout. This may help to spread the incoming load among a cluster of services behind a load balancer. Please note that the real timeout may be bigger by up to 10% as a protection against the thundering herd problem (default 2m0s)
|
||||
-http.disableResponseCompression
|
||||
Disable compression of HTTP responses to save CPU resources. By default compression is enabled to save network bandwidth
|
||||
-http.idleConnTimeout duration
|
||||
Timeout for incoming idle http connections (default 1m0s)
|
||||
-http.maxGracefulShutdownDuration duration
|
||||
The maximum duration for a graceful shutdown of the HTTP server. A highly loaded server may require increased value for a graceful shutdown (default 7s)
|
||||
-http.pathPrefix string
|
||||
An optional prefix to add to all the paths handled by http server. For example, if '-http.pathPrefix=/foo/bar' is set, then all the http requests will be handled on '/foo/bar/*' paths. This may be useful for proxied requests. See https://www.robustperception.io/using-external-urls-and-proxies-with-prometheus
|
||||
-http.shutdownDelay duration
|
||||
Optional delay before http server shutdown. During this delay, the server returns non-OK responses from /health page, so load balancers can route new requests to other servers
|
||||
-httpAuth.password string
|
||||
Password for HTTP Basic Auth. The authentication is disabled if -httpAuth.username is empty
|
||||
-httpAuth.username string
|
||||
Username for HTTP Basic Auth. The authentication is disabled if empty. See also -httpAuth.password
|
||||
-httpListenAddr string
|
||||
TCP address for exporting metrics at /metrics page (default ":8420")
|
||||
-loggerDisableTimestamps
|
||||
Whether to disable writing timestamps in logs
|
||||
-loggerErrorsPerSecondLimit int
|
||||
@@ -213,8 +233,14 @@ See [this article](https://medium.com/@valyala/speeding-up-backups-for-big-time-
|
||||
Supports the following optional suffixes for size values: KB, MB, GB, KiB, MiB, GiB (default 0)
|
||||
-memory.allowedPercent float
|
||||
Allowed percent of system memory VictoriaMetrics caches may occupy. See also -memory.allowedBytes. Too low a value may increase cache miss rate usually resulting in higher CPU and disk IO usage. Too high a value may evict too much data from OS page cache which will result in higher disk IO usage (default 60)
|
||||
-metricsAuthKey string
|
||||
Auth key for /metrics. It must be passed via authKey query arg. It overrides httpAuth.* settings
|
||||
-origin string
|
||||
Optional origin directory on the remote storage with old backup for server-side copying when performing full backup. This speeds up full backups
|
||||
-pprofAuthKey string
|
||||
Auth key for /debug/pprof. It must be passed via authKey query arg. It overrides httpAuth.* settings
|
||||
-s3ForcePathStyle
|
||||
Prefixing endpoint with bucket name when set false, true by default. (default true)
|
||||
-snapshot.createURL string
|
||||
VictoriaMetrics create snapshot url. When this is given a snapshot will automatically be created during backup. Example: http://victoriametrics:8428/snapshot/create . There is no need in setting -snapshotName if -snapshot.createURL is set
|
||||
-snapshot.deleteURL string
|
||||
@@ -223,6 +249,12 @@ See [this article](https://medium.com/@valyala/speeding-up-backups-for-big-time-
|
||||
Name for the snapshot to backup. See https://docs.victoriametrics.com/Single-server-VictoriaMetrics.html#how-to-work-with-snapshots. There is no need in setting -snapshotName if -snapshot.createURL is set
|
||||
-storageDataPath string
|
||||
Path to VictoriaMetrics data. Must match -storageDataPath from VictoriaMetrics or vmstorage (default "victoria-metrics-data")
|
||||
-tls
|
||||
Whether to enable TLS (aka HTTPS) for incoming requests. -tlsCertFile and -tlsKeyFile must be set if -tls is set
|
||||
-tlsCertFile string
|
||||
Path to file with TLS certificate. Used only if -tls is set. Prefer ECDSA certs instead of RSA certs as RSA certs are slower
|
||||
-tlsKeyFile string
|
||||
Path to file with TLS key. Used only if -tls is set
|
||||
-version
|
||||
Show VictoriaMetrics version
|
||||
```
|
||||
@@ -235,7 +267,7 @@ It is recommended using [binary releases](https://github.com/VictoriaMetrics/Vic
|
||||
|
||||
### Development build
|
||||
|
||||
1. [Install Go](https://golang.org/doc/install). The minimum supported version is Go 1.16.
|
||||
1. [Install Go](https://golang.org/doc/install). The minimum supported version is Go 1.17.
|
||||
2. Run `make vmbackup` from the root folder of [the repository](https://github.com/VictoriaMetrics/VictoriaMetrics).
|
||||
It builds `vmbackup` binary and puts it into the `bin` folder.
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmbackup/snapshot"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/backup/actions"
|
||||
@@ -14,10 +15,12 @@ import (
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/buildinfo"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/envflag"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/flagutil"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/httpserver"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
|
||||
)
|
||||
|
||||
var (
|
||||
httpListenAddr = flag.String("httpListenAddr", ":8420", "TCP address for exporting metrics at /metrics page")
|
||||
storageDataPath = flag.String("storageDataPath", "victoria-metrics-data", "Path to VictoriaMetrics data. Must match -storageDataPath from VictoriaMetrics or vmstorage")
|
||||
snapshotName = flag.String("snapshotName", "", "Name for the snapshot to backup. See https://docs.victoriametrics.com/Single-server-VictoriaMetrics.html#how-to-work-with-snapshots. There is no need in setting -snapshotName if -snapshot.createURL is set")
|
||||
snapshotCreateURL = flag.String("snapshot.createURL", "", "VictoriaMetrics create snapshot url. When this is given a snapshot will automatically be created during backup. "+
|
||||
@@ -25,7 +28,7 @@ var (
|
||||
snapshotDeleteURL = flag.String("snapshot.deleteURL", "", "VictoriaMetrics delete snapshot url. Optional. Will be generated from -snapshot.createURL if not provided. "+
|
||||
"All created snapshots will be automatically deleted. Example: http://victoriametrics:8428/snapshot/delete")
|
||||
dst = flag.String("dst", "", "Where to put the backup on the remote storage. "+
|
||||
"Example: gcs://bucket/path/to/backup/dir, s3://bucket/path/to/backup/dir or fs:///path/to/local/backup/dir\n"+
|
||||
"Example: gs://bucket/path/to/backup/dir, s3://bucket/path/to/backup/dir or fs:///path/to/local/backup/dir\n"+
|
||||
"-dst can point to the previous backup. In this case incremental backup is performed, i.e. only changed data is uploaded")
|
||||
origin = flag.String("origin", "", "Optional origin directory on the remote storage with old backup for server-side copying when performing full backup. This speeds up full backups")
|
||||
concurrency = flag.Int("concurrency", 10, "The number of concurrent workers. Higher concurrency may reduce backup duration")
|
||||
@@ -70,6 +73,8 @@ func main() {
|
||||
}()
|
||||
}
|
||||
|
||||
go httpserver.Serve(*httpListenAddr, nil)
|
||||
|
||||
srcFS, err := newSrcFS()
|
||||
if err != nil {
|
||||
logger.Fatalf("%s", err)
|
||||
@@ -94,6 +99,13 @@ func main() {
|
||||
srcFS.MustStop()
|
||||
dstFS.MustStop()
|
||||
originFS.MustStop()
|
||||
|
||||
startTime := time.Now()
|
||||
logger.Infof("gracefully shutting down http server for metrics at %q", *httpListenAddr)
|
||||
if err := httpserver.Stop(*httpListenAddr); err != nil {
|
||||
logger.Fatalf("cannot stop http server for metrics: %s", err)
|
||||
}
|
||||
logger.Infof("successfully shut down http server for metrics in %.3f seconds", time.Since(startTime).Seconds())
|
||||
}
|
||||
|
||||
func usage() {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
## vmbackupmanager
|
||||
|
||||
***vmbackupmanager is a part of [enterprise package](https://victoriametrics.com/enterprise.html). It is available for download and evaluation at [releases page](https://github.com/VictoriaMetrics/VictoriaMetrics/releases)***
|
||||
***vmbackupmanager is a part of [enterprise package](https://victoriametrics.com/products/enterprise/). It is available for download and evaluation at [releases page](https://github.com/VictoriaMetrics/VictoriaMetrics/releases)***
|
||||
|
||||
The VictoriaMetrics backup manager automates regular backup procedures. It supports the following backup intervals: **hourly**, **daily**, **weekly** and **monthly**. Multiple backup intervals may be configured simultaneously. I.e. the backup manager creates hourly backups every hour, while it creates daily backups every day, etc. Backup manager must have read access to the storage data, so best practice is to install it on the same machine (or as a sidecar) where the storage node is installed.
|
||||
The backup service makes a backup every hour and puts it to the latest folder and then copies data to the folders which represent the backup intervals (hourly, daily, weekly and monthly)
|
||||
@@ -76,7 +76,7 @@ Backup manager launched with the following configuration:
|
||||
```console
|
||||
export NODE_IP=192.168.0.10
|
||||
export VMSTORAGE_ENDPOINT=http://127.0.0.1:8428
|
||||
./vmbackupmanager -dst=gcs://vmstorage-data/$NODE_IP -credsFilePath=credentials.json -storageDataPath=/vmstorage-data -snapshot.createURL=$VMSTORAGE_ENDPOINT/snapshot/create -eula
|
||||
./vmbackupmanager -dst=gs://vmstorage-data/$NODE_IP -credsFilePath=credentials.json -storageDataPath=/vmstorage-data -snapshot.createURL=$VMSTORAGE_ENDPOINT/snapshot/create -eula
|
||||
```
|
||||
|
||||
Expected logs in vmbackupmanager:
|
||||
@@ -98,11 +98,11 @@ The result on the GCS bucket
|
||||
|
||||
- The root folder
|
||||
|
||||

|
||||

|
||||
|
||||
- The latest folder
|
||||
|
||||

|
||||

|
||||
|
||||
## Backup Retention Policy
|
||||
|
||||
@@ -117,7 +117,7 @@ Backup retention policy is controlled by:
|
||||
|
||||
Let’s assume we have a backup manager collecting daily backups for the past 10 days.
|
||||
|
||||

|
||||

|
||||
|
||||
|
||||
We enable backup retention policy for backup manager by using following configuration:
|
||||
@@ -125,7 +125,7 @@ We enable backup retention policy for backup manager by using following configur
|
||||
```console
|
||||
export NODE_IP=192.168.0.10
|
||||
export VMSTORAGE_ENDPOINT=http://127.0.0.1:8428
|
||||
./vmbackupmanager -dst=gcs://vmstorage-data/$NODE_IP -credsFilePath=credentials.json -storageDataPath=/vmstorage-data -snapshot.createURL=$VMSTORAGE_ENDPOINT/snapshot/create
|
||||
./vmbackupmanager -dst=gs://vmstorage-data/$NODE_IP -credsFilePath=credentials.json -storageDataPath=/vmstorage-data -snapshot.createURL=$VMSTORAGE_ENDPOINT/snapshot/create
|
||||
-keepLastDaily=3 -eula
|
||||
```
|
||||
|
||||
@@ -143,4 +143,4 @@ info app/vmbackupmanager/retention.go:106 daily backups to delete [daily/2
|
||||
|
||||
The result on the GCS bucket. We see only 3 daily backups:
|
||||
|
||||

|
||||

|
||||
|
||||
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |