mirror of
https://github.com/prometheus/statsd_exporter.git
synced 2024-06-06 07:19:23 +00:00
Compare commits
886 commits
Author | SHA1 | Date | |
---|---|---|---|
58769c7b4d | |||
9c4dfce4e0 | |||
29c77f407c | |||
233d74d0f7 | |||
3f985fa9ac | |||
479379a908 | |||
6e02dfcaae | |||
2c4ffa9620 | |||
ac0ef06e65 | |||
337849188c | |||
4b21c8e662 | |||
91ccdb962a | |||
d93cb36b75 | |||
6483ce0ffe | |||
d5473a0f96 | |||
b9ee6639ce | |||
80810d614e | |||
cae614397a | |||
e729f64ef3 | |||
adeacdd760 | |||
a19a729111 | |||
4c268bcdf7 | |||
8ec3225483 | |||
7d28d2145f | |||
48b0038897 | |||
abc3a1f95c | |||
4a0e88e27b | |||
7188ed4292 | |||
c48fa1bfa7 | |||
5d3e63295a | |||
8fdc626bfc | |||
8adea73c00 | |||
a853c2b0b2 | |||
2c7fd1edd4 | |||
1e89c26ad6 | |||
413f482751 | |||
f9d238dbdd | |||
ef104085db | |||
0750ae0346 | |||
51cc8e4659 | |||
a9b63f9e5f | |||
736b8b676b | |||
5b8dfc866d | |||
3c4f1af3ff | |||
4392beadc3 | |||
22381d60c5 | |||
56fe4f51cd | |||
3855b3e8c9 | |||
3ea12e444f | |||
fb1a02cf4a | |||
8373cb382e | |||
3d8cac0b30 | |||
2337d38124 | |||
12d14bb1d8 | |||
04ed8c69a9 | |||
c246633aec | |||
8e326ef3f3 | |||
b98a84c3f6 | |||
5a947bd1a3 | |||
ee8b05b646 | |||
3dcff5fed5 | |||
aaaf26c074 | |||
b0c6d983e1 | |||
ccb3eb6277 | |||
03255bf218 | |||
871e2d8df1 | |||
5123a1caa6 | |||
3f8b98f180 | |||
886c18f947 | |||
c76a152291 | |||
69bc83fc47 | |||
b1094b9061 | |||
8ffcdb3d5e | |||
029cd6bca5 | |||
e91ce201d5 | |||
ddf7e0d31d | |||
80e119a781 | |||
191b07124c | |||
f73cc98fd7 | |||
7f5125abdd | |||
61f201130e | |||
2813a69578 | |||
2a8dab9248 | |||
5daeef52d8 | |||
c87a7ac5eb | |||
d747c05579 | |||
08c4bccb64 | |||
a0f4bcff5c | |||
fdc8b5f852 | |||
c3752cf30f | |||
a5b0ed182c | |||
972cb24750 | |||
24e2fc9980 | |||
aa341da7c4 | |||
fb5d639dbf | |||
14d18d8e70 | |||
bbba726e66 | |||
f0e9a8e2f8 | |||
0bdeac2162 | |||
9a980af22f | |||
052676c42d | |||
21a1763baf | |||
832c5a5316 | |||
e22a86f5f9 | |||
d73608eb14 | |||
9303da56a3 | |||
2caee689db | |||
07cf2fa0b1 | |||
f6b2a21fbd | |||
c2447be23b | |||
b8ace4ae2c | |||
fea420de84 | |||
9c6e245521 | |||
f6ab38f75e | |||
559fc9c1af | |||
507b0a20dd | |||
f77011fd34 | |||
fbfa209c87 | |||
fc1d834d59 | |||
81884eb3d8 | |||
f231009802 | |||
dbdf4d9349 | |||
c74db12eaf | |||
fd4ea19c21 | |||
1bcb9d1a97 | |||
a92aa4556e | |||
00d82daaf2 | |||
65e7877ab3 | |||
aecad1a2fa | |||
5d28e6d898 | |||
edaa33860e | |||
89e7668d8d | |||
da06fabcb6 | |||
83cec219c8 | |||
31b05ef232 | |||
c41fa101f5 | |||
7afa060ce4 | |||
30c3e31574 | |||
e85098da3f | |||
d0585ec62b | |||
af84364004 | |||
1e801bc499 | |||
b43a60e9c8 | |||
bc43c5606d | |||
c2505cf91e | |||
8f351a5577 | |||
b1068d058a | |||
3b9ef1fef5 | |||
9d4eeda5b1 | |||
c2c1af12ab | |||
9115f0fa39 | |||
6ef0b3b4e8 | |||
1a148215de | |||
f935a9c869 | |||
3a63a4b86c | |||
2ab624a917 | |||
848212e028 | |||
d90c8ff92d | |||
6e341dd805 | |||
c753dfaf76 | |||
0d22f85f04 | |||
b8504edbe4 | |||
c714dcdf2e | |||
dcddbc4234 | |||
d46243aa66 | |||
4d07e28b7c | |||
ada7248ccb | |||
0de518d3d5 | |||
3427e36d50 | |||
99a94fa8b4 | |||
4b6983811e | |||
e416ec14d3 | |||
f8fc001bba | |||
542ee7cfa9 | |||
0cb34c771d | |||
549a96a54c | |||
fbf7837387 | |||
d015cda365 | |||
2d339b0853 | |||
60742e1bd6 | |||
78dcd3b7b2 | |||
4ad11fcafa | |||
2f5add464e | |||
4a086bc25b | |||
55e536bc15 | |||
161f982ae2 | |||
5654505569 | |||
1a1dd2d8a3 | |||
b8462d09ec | |||
c481b95155 | |||
b1222831b6 | |||
db4a4f2fbe | |||
83180ae37f | |||
5c6c8a28d0 | |||
a32a9da527 | |||
7e2fe6c2e6 | |||
7ba3f6a841 | |||
6a5a089597 | |||
6564725760 | |||
bfdf4ddee0 | |||
aadb43ed02 | |||
258c8c0b2c | |||
fae515f739 | |||
339c8efb33 | |||
16d4f317a5 | |||
6c0283942c | |||
d0d948ee05 | |||
69b17ee390 | |||
697eb33e2c | |||
b60989291b | |||
f699c4ae94 | |||
b6fc5ded9f | |||
2ab2c442cf | |||
95d34445ed | |||
83513ab186 | |||
5c8622abc1 | |||
4946fc865c | |||
d4e89cd38c | |||
f0c6a23b8c | |||
f7894137b8 | |||
9200507034 | |||
884a8c0a21 | |||
ae26c506f5 | |||
f39c9c3645 | |||
f5c6a55c0d | |||
9b31bb8533 | |||
e86e67a7cf | |||
0b84893d4a | |||
b04eba273a | |||
6c43ebbc79 | |||
37da8dd118 | |||
6329577a6b | |||
d57c8b11f3 | |||
a9c883a298 | |||
f9fea18a92 | |||
e84990974b | |||
42158f5cca | |||
1f53a743c7 | |||
eea2c0f734 | |||
828d50cea5 | |||
c4f435e140 | |||
efdf11343c | |||
e395798230 | |||
4c8028e865 | |||
c3a818ef62 | |||
f4fe58dcf2 | |||
abb7ec0afb | |||
ef6627b9f0 | |||
5ea682c01f | |||
04bc13d73c | |||
2852aa2a5b | |||
278a862099 | |||
5f419f96dd | |||
2d1face4e0 | |||
7e3a7a5068 | |||
c5113b732c | |||
1a86938882 | |||
cd27e25711 | |||
393b4ca495 | |||
ea25c7a114 | |||
2a6bbd7827 | |||
a8f8067315 | |||
c552b8b84b | |||
ce538cbae2 | |||
b8d1fde4e4 | |||
2b5239a67f | |||
a8c09aaa97 | |||
5793e05795 | |||
5f7d52f795 | |||
84e68332f3 | |||
12281a972e | |||
8b496dfab5 | |||
da69d725d0 | |||
e462a256b1 | |||
ad8d919409 | |||
0151d93b46 | |||
8e28d7acca | |||
ad0c3a62e1 | |||
094f3cd9e5 | |||
4962fd01ae | |||
b3aafad8f0 | |||
b1045e33ac | |||
f1ca904c98 | |||
a197834f64 | |||
8b306c8c76 | |||
0c4a66e6a1 | |||
aa529c8884 | |||
940e653ea6 | |||
32c612d5e8 | |||
de9a51c863 | |||
0099df7f71 | |||
fbcadbf71b | |||
eeeedce405 | |||
8115b37bd9 | |||
799943cb47 | |||
ee3b81b864 | |||
f8bba00868 | |||
66a63a8c1f | |||
58aed41ff2 | |||
06411336c7 | |||
6a9749cd42 | |||
64dd103e3f | |||
bade06f775 | |||
4171ba0c9b | |||
eef7922de8 | |||
175e9a6728 | |||
cafe6c75b8 | |||
7e8ab9bad7 | |||
64c79eea8b | |||
5a7d7fe3d0 | |||
709c0da81b | |||
8b2b4c1a2b | |||
8772c03c0f | |||
b5deeda251 | |||
dcd95d01df | |||
dc1f856da1 | |||
7c792fba66 | |||
45b616fe8c | |||
ede356f65e | |||
4acb05aa89 | |||
420dc651d8 | |||
cba4a0783a | |||
f5e400962a | |||
3ed211ec7a | |||
e7b26d48bc | |||
07c754c759 | |||
a94056e060 | |||
f0c7abebb1 | |||
da85f9d207 | |||
477d566101 | |||
4c0e26bfa1 | |||
2c4cda7fb3 | |||
424d4781ca | |||
d93907009c | |||
b19217a19b | |||
96410eb4f5 | |||
bac6cbe8c5 | |||
a73707d102 | |||
e0a39974e2 | |||
c162b349b7 | |||
1293a24b59 | |||
afa40f4ada | |||
6942b5a4f3 | |||
db25b1d658 | |||
3f3ab23359 | |||
da4a2950a7 | |||
2bea72dd37 | |||
9763353a54 | |||
baa976dc6e | |||
e43831c078 | |||
8c97daddee | |||
b162bd047a | |||
cc709f242c | |||
36de8c9e12 | |||
7cbd9525d2 | |||
a991a4ba57 | |||
fb382a2fea | |||
15eece3cf8 | |||
9faa4898de | |||
5b863848dd | |||
4f7abe5226 | |||
f1de97dbdd | |||
ab73924e53 | |||
ed37775e02 | |||
b64e07c7d9 | |||
7ba3550f8f | |||
aa591e3e8e | |||
b4ce2913fa | |||
32f1677f81 | |||
38414f106a | |||
4a64979563 | |||
1444824911 | |||
bdd4a76348 | |||
77277a5150 | |||
32e9e58e32 | |||
d95a53553e | |||
374d202daa | |||
01b19722d7 | |||
063112b138 | |||
5c0b206f6e | |||
2fc2dcdff6 | |||
b3520aabd4 | |||
c8b8ddc952 | |||
3b3ff3c473 | |||
44ae8f557c | |||
bb2fedc556 | |||
7f7d5171c3 | |||
0201b4f71f | |||
bd5e04d6e3 | |||
3908609918 | |||
3630398b17 | |||
588fde9fc1 | |||
7b78f2c1cd | |||
fbceeef47a | |||
d804941a18 | |||
cc10478f61 | |||
4eac64eb59 | |||
a75e588cf0 | |||
2898eb8c0c | |||
eb3997b287 | |||
bf8af64f5e | |||
f6384291a6 | |||
682bc92b45 | |||
1f3adf69c6 | |||
c75b6091b8 | |||
f0a46c4ec9 | |||
828e12c345 | |||
5ec58a32c2 | |||
4b3b9ba207 | |||
390e862252 | |||
44d4daf599 | |||
16b6f95c96 | |||
a455a8ad64 | |||
77e8e78a88 | |||
3207ad13ea | |||
6079c91345 | |||
b00162470f | |||
d55b42eabb | |||
cb516fa69a | |||
37e54d0f47 | |||
5179715a82 | |||
2ad8989cd2 | |||
60fbaf5e27 | |||
9778828863 | |||
90e247b091 | |||
7b027d00a6 | |||
7c8c2b571f | |||
6cef4dadac | |||
b4e9e95cf2 | |||
dae5d782a6 | |||
80b77513a6 | |||
c9c23a4f9e | |||
e60f77df30 | |||
5aae305b35 | |||
d7f22028aa | |||
3a88bd3ddb | |||
fa2159f8e8 | |||
b234e1dd4e | |||
20006621cc | |||
512c53c703 | |||
8a309f2e6e | |||
4f82807b38 | |||
7e6d394af0 | |||
53f732d480 | |||
0d72309324 | |||
6f6d036307 | |||
a4a9f26e16 | |||
ac3e901f19 | |||
9d0ea80917 | |||
4b69da2d03 | |||
2e3d3ab962 | |||
e60a0b6d00 | |||
d888f25cb6 | |||
a1c201d821 | |||
eeaefea1e2 | |||
600423ad61 | |||
4f60ae856b | |||
8ebffcfb93 | |||
18a3c26447 | |||
1aa5127349 | |||
84657e85ec | |||
5f4f780e16 | |||
c710b851c7 | |||
7d2a901b6c | |||
c1db8ba02d | |||
df740e7778 | |||
a4b92689bc | |||
0e000fe833 | |||
e01507a57c | |||
22619bb8a9 | |||
5537c504cc | |||
84889bd5ea | |||
2933dd8ad0 | |||
a35d17c160 | |||
1865ea1be3 | |||
5b44b372d1 | |||
1bddef857d | |||
d64c674394 | |||
9d822d4196 | |||
91cc64da62 | |||
b6e4b2a82e | |||
54359d3b06 | |||
028531e953 | |||
a1f8bae0a3 | |||
8479e3d7a3 | |||
8fa4613e74 | |||
4d518de467 | |||
74a561fbed | |||
95fb214d16 | |||
085643f486 | |||
d49afad05c | |||
b0a54e1d65 | |||
d308796e97 | |||
074fc349b0 | |||
353b38c80b | |||
49296e321e | |||
b555158e53 | |||
e60f2f147b | |||
2d44e85fdc | |||
6635ea71e8 | |||
802aa8a9e7 | |||
ccd86002cf | |||
d56b864f83 | |||
878609a8e8 | |||
f7c7db0b8d | |||
bdfa130554 | |||
f6e6e6d122 | |||
f72f25f8ce | |||
d59f306cd9 | |||
193e4c9be4 | |||
64a51731f7 | |||
23ef5daeac | |||
eb51631f33 | |||
611dd8c60a | |||
3494db2a67 | |||
f2d3b9eb79 | |||
ef5212ce06 | |||
511836b7ff | |||
bb448be4f4 | |||
bb88165d52 | |||
a276bacac9 | |||
4653781f9b | |||
70c227522f | |||
c3dc7b4b96 | |||
6f7cb49280 | |||
38d62c39a6 | |||
4f9843ad0d | |||
5cea0077e4 | |||
8f7676eaa9 | |||
7035e5e075 | |||
5832aa9bcf | |||
091bf99641 | |||
2e5c38204a | |||
95e376a40b | |||
1d21cdcc1e | |||
59056c097a | |||
8551a65827 | |||
0135b40c08 | |||
c7e76967c8 | |||
50d5932124 | |||
f6f1d7f071 | |||
a241eb0b69 | |||
680eb0e826 | |||
f6c0dd965b | |||
7c30120dbc | |||
7a107f899f | |||
d74922e0fd | |||
83cd28067a | |||
bfa30a94b2 | |||
f12451ee5b | |||
09c2603f4b | |||
56a091a693 | |||
7d6244987a | |||
b333ecaacc | |||
3abf21e053 | |||
26e9d482db | |||
471b28dc21 | |||
c9004f8f3f | |||
4e64da2a41 | |||
27d9273107 | |||
cf4290bf7e | |||
df81d15e84 | |||
d143398343 | |||
732be39b4b | |||
4cf6f74dc3 | |||
98da6f7057 | |||
a294491e0b | |||
f502171bdc | |||
8cc291f419 | |||
228a969b1a | |||
698bcdf8c3 | |||
cce95f6980 | |||
a42de85289 | |||
04b7b71a2a | |||
3dcad090b3 | |||
24e288a3a4 | |||
0eadae3ca7 | |||
7514e37e5b | |||
9f1c6b81a5 | |||
f08bf14965 | |||
bee73cbb9e | |||
d7eb1edeed | |||
f1c0052ce7 | |||
cddeb87405 | |||
04bba78d61 | |||
a4faae262b | |||
e3cdd85a09 | |||
a441eac07a | |||
052beaa3ac | |||
9cd711ed3e | |||
4d0cb1992d | |||
e82cf2444b | |||
35d1a99592 | |||
54cf241044 | |||
7631485b0b | |||
abb2ab04fb | |||
6fd7690961 | |||
f1de314e14 | |||
0bd90aee34 | |||
9963806a62 | |||
ee653990e6 | |||
e3d6050616 | |||
47b5ef9be0 | |||
383ee9bd3b | |||
024f9dc0aa | |||
ece8a26a79 | |||
485b28a9ff | |||
88a12bc9b9 | |||
c3eb636525 | |||
0766e70e43 | |||
38df95f440 | |||
f0feef950b | |||
526a81b445 | |||
45402964a4 | |||
92957ce080 | |||
468c70df7d | |||
ece9385f22 | |||
9ed6d59151 | |||
cfcf3c9ba5 | |||
7f70979120 | |||
e6bdf13407 | |||
d371436f01 | |||
3955c6ef1b | |||
178d130388 | |||
43cef6ce6a | |||
05bca84294 | |||
1cbc5a9b27 | |||
2fc89536f4 | |||
2025b47cb1 | |||
71df5a3198 | |||
035a309552 | |||
51e735c878 | |||
c9e5e94ed2 | |||
c477c7703f | |||
1c71191aaf | |||
1036439f69 | |||
680afe25f1 | |||
88af8dcd24 | |||
eeab36e63f | |||
7830fea9ae | |||
d5b22a2993 | |||
5ff2356e36 | |||
671c01aa7f | |||
a566da3f87 | |||
c73a7b2c9a | |||
2d35b097ae | |||
8f56cc811d | |||
65070524c3 | |||
d655643a58 | |||
ad36bba13d | |||
4d0c8ab2c2 | |||
f13069832d | |||
d712534c13 | |||
15e0f4c963 | |||
141e366e22 | |||
cd3a546179 | |||
7364c6fe44 | |||
699c11ca11 | |||
331d2a56d0 | |||
e550f061f6 | |||
d1b2dd47a8 | |||
57495db281 | |||
e1a3a5fc32 | |||
b4e29c5f18 | |||
b638b9d808 | |||
ef5d2c8a79 | |||
a133af8ffb | |||
a856251d79 | |||
b3bf4d1f8b | |||
e019c01fb5 | |||
d0ad532fa1 | |||
275559585e | |||
b27cfd9c36 | |||
8a4f87d19e | |||
71ec03e5b3 | |||
c8cf42ca4e | |||
040760ec22 | |||
397fd90e94 | |||
a51c9be791 | |||
9b44d79582 | |||
ab844a3f63 | |||
3b846b33a8 | |||
555cd98958 | |||
877630c2b7 | |||
00e2c3ff26 | |||
4e53440316 | |||
e5734e34e9 | |||
97f71db21b | |||
9fc976d906 | |||
761e64df10 | |||
4d9ce8c70a | |||
c10e80c44b | |||
699fa13c8c | |||
fcf11f02e5 | |||
5262b2904c | |||
6d709d52c1 | |||
a0681a0cd2 | |||
a8dcc589e5 | |||
f387766cc2 | |||
668e31e5f1 | |||
5e1df60d22 | |||
e634997791 | |||
a751c0c091 | |||
9ebab25dfa | |||
c2742aa299 | |||
dad04e9c8b | |||
bfe23298aa | |||
825b734b3e | |||
26846b86b3 | |||
3ba10e2cb5 | |||
9480025e61 | |||
90b6066252 | |||
95d60eed01 | |||
0ad6f7e7cb | |||
3c74b10043 | |||
1870cbb30c | |||
82f75d3b9a | |||
3a6b6ce5d5 | |||
e1693708d4 | |||
88a7eef426 | |||
fcc398a032 | |||
84f1ef33d4 | |||
d9f305b6f2 | |||
40a13e604f | |||
124c5b88d0 | |||
419e27d284 | |||
8dbd5e7770 | |||
e32ce67fd9 | |||
b7657e8399 | |||
8d1a9e0330 | |||
a144b1f9f7 | |||
0ffa3f9865 | |||
c4fb3768ac | |||
778ca7f991 | |||
09e16e7aa9 | |||
8971b30a76 | |||
79fa36fe39 | |||
d7357a2173 | |||
b2423c56a5 | |||
568f744f2e | |||
b6c7e863d3 | |||
4a9cde8cef | |||
27f07a6516 | |||
16865d75b7 | |||
7e86bcaa92 | |||
d1f7ca4239 | |||
745e53700f | |||
fbbd6452c6 | |||
76f5f422b4 | |||
c11d1e9cf2 | |||
3fd85c92fc | |||
17524907f7 | |||
9f409ad982 | |||
8608d9db40 | |||
9180cd3afd | |||
f058f3aa33 | |||
515ae0ee89 | |||
27ee4050c4 | |||
d96145e47f | |||
25c9946cf3 | |||
6738c909a5 | |||
a794d58413 | |||
3523ac00b8 | |||
1fa2dff843 | |||
50a5c3e5f7 | |||
a4c9927b89 | |||
d9db83b9be | |||
143a588871 | |||
520e51e6ff | |||
e58bf8e1ca | |||
0ceac67bc6 | |||
0ac908603a | |||
6c04ec1d89 | |||
45d22f2add | |||
e24888356f | |||
cdf79ba2f9 | |||
d4d0b4a6a7 | |||
78de4b11db | |||
ff308119cf | |||
ea77554dc4 | |||
92702b80fd | |||
4f847ac004 | |||
ab2a88c06f | |||
9accf494a9 | |||
ec3cc120e2 | |||
bd0f2139af | |||
0478c40ab0 | |||
892b725faf | |||
2dfca9be7c | |||
f717018146 | |||
4b83af49b7 | |||
19d90f1c21 | |||
b14d0d6611 | |||
072bc00f64 | |||
3f4f12eb1f | |||
115cb0e157 | |||
fe0b1f55de | |||
c7a2ae9b14 | |||
22270c168d | |||
533a6fcb18 | |||
65d2513fde | |||
7e11f326ae | |||
d2ca0a8c0b | |||
e493a9c766 | |||
06444ed5c0 | |||
42e41f1a7e | |||
b2082eda2b | |||
e37bee9bf2 | |||
aeab2905ad | |||
a31feb5365 | |||
1194e00dec | |||
bbdbb9a050 | |||
9bca336876 | |||
9c1cde63be | |||
c339cc66ac | |||
07543ac557 | |||
93243f61a1 | |||
c9510f7f23 | |||
2643f1550f | |||
b467a537d5 | |||
e8997f7cd9 | |||
74975b8411 | |||
8b40f781ef | |||
bbd5227a1c | |||
c1791d7ecb | |||
d9aa6e2867 | |||
22520f4c7b | |||
663b6a1e69 | |||
20becbc27b | |||
5018c7415a | |||
b5dfc9c9ce | |||
89e5e36b51 | |||
c15ffa2aa2 | |||
4b0d773a6e | |||
3039d12e3a | |||
641dc37b5d | |||
1488f0f902 | |||
3048412df6 | |||
df926ae85a | |||
03335d91cf | |||
9cd4b974e4 | |||
3efcef6229 | |||
7fc8727306 | |||
446b7e6d44 | |||
0285d7b4a7 | |||
109b5ad053 | |||
b1f3e6d7be | |||
6ac8203d40 | |||
5a9d2f996a | |||
c1f2d12554 | |||
24eab4e35a | |||
022c10fe0c | |||
8bf751d4e3 | |||
35828e29e9 | |||
237499eced | |||
1fbc392800 | |||
bb4e42068f | |||
0c8b644ee1 | |||
56f65c4870 | |||
00f56b3809 | |||
e00d9e84aa | |||
cc3025a1f9 | |||
dcb67944be | |||
7a76b03aa7 | |||
246008307f | |||
1710048a70 | |||
03fde8f3ba | |||
4d22a721eb | |||
61f4c6e926 | |||
cdeb0c9743 | |||
e26a5e4728 | |||
1f1c0f7030 | |||
a78a6d2635 | |||
dd0b68f2d8 | |||
dacfb3f1be | |||
8e43fc2654 | |||
ba9c37eb20 | |||
3d0ccb9ed5 | |||
0b792e0be6 | |||
8f36baf045 | |||
f031e30721 | |||
60dbb43e8a | |||
c9baaf5ba1 | |||
121026ae3d | |||
494cc38bf8 | |||
ab7c27ee77 |
48
.circleci/config.yml
Normal file
48
.circleci/config.yml
Normal file
|
@ -0,0 +1,48 @@
|
|||
version: 2.1
|
||||
orbs:
|
||||
prometheus: prometheus/prometheus@0.17.1
|
||||
executors:
|
||||
# Whenever the Go version is updated here, .promu.yml should also be updated.
|
||||
golang:
|
||||
docker:
|
||||
- image: cimg/go:1.21
|
||||
jobs:
|
||||
test:
|
||||
executor: golang
|
||||
steps:
|
||||
- prometheus/setup_environment
|
||||
- run: make
|
||||
- run: git diff --exit-code
|
||||
- prometheus/store_artifact:
|
||||
file: statsd_exporter
|
||||
workflows:
|
||||
version: 2
|
||||
statsd_exporter:
|
||||
jobs:
|
||||
- test:
|
||||
filters:
|
||||
tags:
|
||||
only: /.*/
|
||||
- prometheus/build:
|
||||
name: build
|
||||
filters:
|
||||
tags:
|
||||
only: /.*/
|
||||
- prometheus/publish_master:
|
||||
context: org-context
|
||||
requires:
|
||||
- test
|
||||
- build
|
||||
filters:
|
||||
branches:
|
||||
only: master
|
||||
- prometheus/publish_release:
|
||||
context: org-context
|
||||
requires:
|
||||
- test
|
||||
- build
|
||||
filters:
|
||||
tags:
|
||||
only: /^v[0-9]+(\.[0-9]+){2}(-.+|[^-.]*)$/
|
||||
branches:
|
||||
ignore: /.*/
|
6
.dockerignore
Normal file
6
.dockerignore
Normal file
|
@ -0,0 +1,6 @@
|
|||
.build/
|
||||
.tarballs/
|
||||
|
||||
!.build/linux-amd64
|
||||
!.build/linux-armv7
|
||||
!.build/linux-arm64
|
4
.gitattributes
vendored
Normal file
4
.gitattributes
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
# Managing line ending conversions
|
||||
# See http://git-scm.com/docs/gitattributes#_end-of-line_conversion
|
||||
* text=auto
|
||||
* eol=lf
|
6
.github/dependabot.yml
vendored
Normal file
6
.github/dependabot.yml
vendored
Normal file
|
@ -0,0 +1,6 @@
|
|||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "gomod"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "monthly"
|
52
.github/workflows/container_description.yml
vendored
Normal file
52
.github/workflows/container_description.yml
vendored
Normal file
|
@ -0,0 +1,52 @@
|
|||
---
|
||||
name: Push README to Docker Hub
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- "README.md"
|
||||
- ".github/workflows/container_description.yml"
|
||||
branches: [ main, master ]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
PushDockerHubReadme:
|
||||
runs-on: ubuntu-latest
|
||||
name: Push README to Docker Hub
|
||||
if: github.repository_owner == 'prometheus' || github.repository_owner == 'prometheus-community' # Don't run this workflow on forks.
|
||||
steps:
|
||||
- name: git checkout
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
- name: Set docker hub repo name
|
||||
run: echo "DOCKER_REPO_NAME=$(make docker-repo-name)" >> $GITHUB_ENV
|
||||
- name: Push README to Dockerhub
|
||||
uses: christian-korneck/update-container-description-action@d36005551adeaba9698d8d67a296bd16fa91f8e8 # v1
|
||||
env:
|
||||
DOCKER_USER: ${{ secrets.DOCKER_HUB_LOGIN }}
|
||||
DOCKER_PASS: ${{ secrets.DOCKER_HUB_PASSWORD }}
|
||||
with:
|
||||
destination_container_repo: ${{ env.DOCKER_REPO_NAME }}
|
||||
provider: dockerhub
|
||||
short_description: ${{ env.DOCKER_REPO_NAME }}
|
||||
readme_file: 'README.md'
|
||||
|
||||
PushQuayIoReadme:
|
||||
runs-on: ubuntu-latest
|
||||
name: Push README to quay.io
|
||||
if: github.repository_owner == 'prometheus' || github.repository_owner == 'prometheus-community' # Don't run this workflow on forks.
|
||||
steps:
|
||||
- name: git checkout
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
- name: Set quay.io org name
|
||||
run: echo "DOCKER_REPO=$(echo quay.io/${GITHUB_REPOSITORY_OWNER} | tr -d '-')" >> $GITHUB_ENV
|
||||
- name: Set quay.io repo name
|
||||
run: echo "DOCKER_REPO_NAME=$(make docker-repo-name)" >> $GITHUB_ENV
|
||||
- name: Push README to quay.io
|
||||
uses: christian-korneck/update-container-description-action@d36005551adeaba9698d8d67a296bd16fa91f8e8 # v1
|
||||
env:
|
||||
DOCKER_APIKEY: ${{ secrets.QUAY_IO_API_TOKEN }}
|
||||
with:
|
||||
destination_container_repo: ${{ env.DOCKER_REPO_NAME }}
|
||||
provider: quay
|
||||
readme_file: 'README.md'
|
38
.github/workflows/golangci-lint.yml
vendored
Normal file
38
.github/workflows/golangci-lint.yml
vendored
Normal file
|
@ -0,0 +1,38 @@
|
|||
---
|
||||
# This action is synced from https://github.com/prometheus/prometheus
|
||||
name: golangci-lint
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- "go.sum"
|
||||
- "go.mod"
|
||||
- "**.go"
|
||||
- "scripts/errcheck_excludes.txt"
|
||||
- ".github/workflows/golangci-lint.yml"
|
||||
- ".golangci.yml"
|
||||
pull_request:
|
||||
|
||||
permissions: # added using https://github.com/step-security/secure-repo
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
golangci:
|
||||
permissions:
|
||||
contents: read # for actions/checkout to fetch code
|
||||
pull-requests: read # for golangci/golangci-lint-action to fetch pull requests
|
||||
name: lint
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
- name: install Go
|
||||
uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 # v5.0.0
|
||||
with:
|
||||
go-version: 1.22.x
|
||||
- name: Install snmp_exporter/generator dependencies
|
||||
run: sudo apt-get update && sudo apt-get -y install libsnmp-dev
|
||||
if: github.repository == 'prometheus/snmp_exporter'
|
||||
- name: Lint
|
||||
uses: golangci/golangci-lint-action@3cfe3a4abbb849e10058ce4af15d205b6da42804 # v4.0.0
|
||||
with:
|
||||
version: v1.56.2
|
10
.gitignore
vendored
10
.gitignore
vendored
|
@ -1,4 +1,8 @@
|
|||
.build/
|
||||
dependencies-stamp
|
||||
*.tar.gz
|
||||
statsd_exporter
|
||||
/statsd_exporter
|
||||
/.build
|
||||
/.deps
|
||||
/.release
|
||||
/.tarballs
|
||||
*~
|
||||
/vendor
|
||||
|
|
6
.gitpod.Dockerfile
vendored
Normal file
6
.gitpod.Dockerfile
vendored
Normal file
|
@ -0,0 +1,6 @@
|
|||
FROM gitpod/workspace-full
|
||||
|
||||
RUN sudo apt-get update && \
|
||||
sudo apt-get install -y netcat-traditional socat && \
|
||||
sudo apt-get clean && \
|
||||
sudo rm -rf /var/lib/apt/lists/*
|
13
.gitpod.yml
Normal file
13
.gitpod.yml
Normal file
|
@ -0,0 +1,13 @@
|
|||
tasks:
|
||||
- init: go get
|
||||
command: go build . && ./statsd_exporter
|
||||
- command: printf 'Try:\n\n\techo "test.gauge:42|g" | socat - TCP:127.0.0.1:9125\n\n'
|
||||
|
||||
ports:
|
||||
- port: 9102
|
||||
onOpen: open-preview
|
||||
- port: 9125
|
||||
onOpen: ignore
|
||||
|
||||
image:
|
||||
file: .gitpod.Dockerfile
|
27
.golangci.yml
Normal file
27
.golangci.yml
Normal file
|
@ -0,0 +1,27 @@
|
|||
run:
|
||||
issues-exit-code: 1
|
||||
tests: true
|
||||
linters:
|
||||
disable:
|
||||
- errcheck
|
||||
enable:
|
||||
- deadcode
|
||||
- goimports
|
||||
- gosimple
|
||||
- govet
|
||||
- ineffassign
|
||||
- nakedret
|
||||
- staticcheck
|
||||
- structcheck
|
||||
- unused
|
||||
- varcheck
|
||||
- whitespace
|
||||
linters-settings:
|
||||
govet:
|
||||
check-shadowing: true
|
||||
issues:
|
||||
exclude-use-default: false
|
||||
exclude:
|
||||
- 'shadow: declaration of "err" shadows declaration at line (\d+)'
|
||||
max-issues-per-linter: 0
|
||||
max-same-issues: 0
|
18
.promu.yml
Normal file
18
.promu.yml
Normal file
|
@ -0,0 +1,18 @@
|
|||
go:
|
||||
# Whenever the Go version is updated here, .circle/config.yml should also
|
||||
# be updated.
|
||||
version: 1.21
|
||||
repository:
|
||||
path: github.com/prometheus/statsd_exporter
|
||||
build:
|
||||
ldflags: |
|
||||
-X github.com/prometheus/common/version.Version={{.Version}}
|
||||
-X github.com/prometheus/common/version.Revision={{.Revision}}
|
||||
-X github.com/prometheus/common/version.Branch={{.Branch}}
|
||||
-X github.com/prometheus/common/version.BuildUser={{user}}@{{host}}
|
||||
-X github.com/prometheus/common/version.BuildDate={{date "20060102-15:04:05"}}
|
||||
|
||||
tarball:
|
||||
files:
|
||||
- LICENSE
|
||||
- NOTICE
|
25
.yamllint
Normal file
25
.yamllint
Normal file
|
@ -0,0 +1,25 @@
|
|||
---
|
||||
extends: default
|
||||
ignore: |
|
||||
ui/react-app/node_modules
|
||||
|
||||
rules:
|
||||
braces:
|
||||
max-spaces-inside: 1
|
||||
level: error
|
||||
brackets:
|
||||
max-spaces-inside: 1
|
||||
level: error
|
||||
commas: disable
|
||||
comments: disable
|
||||
comments-indentation: disable
|
||||
document-start: disable
|
||||
indentation:
|
||||
spaces: consistent
|
||||
indent-sequences: consistent
|
||||
key-duplicates:
|
||||
ignore: |
|
||||
config/testdata/section_key_dup.bad.yml
|
||||
line-length: disable
|
||||
truthy:
|
||||
check-keys: false
|
13
AUTHORS.md
13
AUTHORS.md
|
@ -1,13 +0,0 @@
|
|||
The Prometheus project was started by Matt T. Proud (emeritus) and
|
||||
Julius Volz in 2012.
|
||||
|
||||
Maintainers of this repository:
|
||||
|
||||
* Julius Volz <julius.volz@gmail.com>
|
||||
|
||||
The following individuals have contributed code to this repository
|
||||
(listed in alphabetical order):
|
||||
|
||||
* Björn Rabenstein <beorn@soundcloud.com>
|
||||
* Julius Volz <julius.volz@gmail.com>
|
||||
* Matt T. Proud <matt.proud@gmail.com>
|
462
CHANGELOG.md
462
CHANGELOG.md
|
@ -1,13 +1,463 @@
|
|||
## 0.26.1 / 2024-03-22
|
||||
|
||||
* [SECURITY] Update dependencies, including `google.golang.org/protobuf` for CVE-2024-24786
|
||||
|
||||
This is a maintenance release.
|
||||
|
||||
## 0.26.0 / 2023-12-06
|
||||
|
||||
* [CHANGE] Update dependencies: prometheus/common, client model, and Go 1.21
|
||||
* [FEATURE] Add option to honor original labels from event tags over labels specified in mapping configuration ([#521](https://github.com/prometheus/statsd_exporter/pull/521))
|
||||
|
||||
Thank you @rabenhorst for the `honor_labels` contribution!
|
||||
|
||||
## 0.25.0 / 2023-10-23
|
||||
|
||||
* [CHANGE] Update `client_golang` ([#508](https://github.com/prometheus/statsd_exporter/pull/508), [#513](https://github.com/prometheus/statsd_exporter/pull/513))
|
||||
* [ENHANCEMENT] Process UDP packets asynchronously ([#511](https://github.com/prometheus/statsd_exporter/pull/511))
|
||||
* [BUGFIX] Debug-log incoming lines in cleartext ([#510](https://github.com/prometheus/statsd_exporter/pull/510))
|
||||
* [SECURITY] Update `golang.org/x/net` ([#516](https://github.com/prometheus/statsd_exporter/pull/516))
|
||||
|
||||
This release is less likely to drop UDP packets under very high traffic.
|
||||
Additionally, when it does, it now attempts to record that this happened in the metric `statsd_exporter_udp_packet_drops_total`, where previously this could only be detected from operating system metrics.
|
||||
If you are already monitoring for OS-level UDP packet drops, you _must_ also monitor this metric.
|
||||
The exporter will pull packets from the UDP socket queue much more quickly and queue them internally before processing.
|
||||
Existing monitoring for packet drops will no longer be sufficient to detect dropped events, but attribution to the exporter is easier with this new metric.
|
||||
|
||||
Many thanks to @sumeshpremraj and @kullanici0606 for their contributions, and @pedro-stanaka for helping with the async UDP processing!
|
||||
|
||||
## 0.24.0 / 2023-06-02
|
||||
|
||||
* [FEATURE] Improve the landing page experience ([#504](https://github.com/prometheus/statsd_exporter/pull/504))
|
||||
* [FEATURE] Support scaling parameter in mapping ([#499](https://github.com/prometheus/statsd_exporter/pull/499))
|
||||
|
||||
## 0.23.3 / 2023-06-02
|
||||
|
||||
* [SECURITY] Maintenance release, updating dependencies
|
||||
* [ENHANCEMENT][library] Allow instantiating configuration without going through YAML ([#491](https://github.com/prometheus/statsd_exporter/pull/491))
|
||||
|
||||
Version 0.23.2 was mistagged and thus skipped.
|
||||
|
||||
## 0.23.1 / 2023-03-08
|
||||
|
||||
* [SECURITY] Update all dependencies ([#489](https://github.com/prometheus/statsd_exporter/pull/489))
|
||||
|
||||
## 0.23.0 / 2022-12-07
|
||||
|
||||
* [CHANGE] Print help and version to standard out ([#469](https://github.com/prometheus/statsd_exporter/pull/469))
|
||||
* [FEATURE] Support experimental native histograms ([#474](https://github.com/prometheus/statsd_exporter/pull/474))
|
||||
|
||||
## 0.22.8 / 2022-09-13
|
||||
|
||||
* [BUGFIX] Prevent poisoning with gauge/distribution naming collision ([#461](https://github.com/prometheus/statsd_exporter/pull/461))
|
||||
* [CHANGE] Update `client_golang` dependency ([#463](https://github.com/prometheus/statsd_exporter/pull/463))
|
||||
|
||||
## 0.22.7 / 2022-07-08
|
||||
|
||||
* [CHANGE] Build with Go 1.18 ([#450](https://github.com/prometheus/statsd_exporter/pull/450))
|
||||
|
||||
## 0.22.6 / 2022-07-08
|
||||
|
||||
* [CHANGE] Update dependencies ([#449](https://github.com/prometheus/statsd_exporter/pull/449))
|
||||
|
||||
This is another housekeeping release.
|
||||
|
||||
## 0.22.5 / 2022-05-06
|
||||
|
||||
* [ENHANCEMENT] Add metric for total lines relayed ([#434](https://github.com/prometheus/statsd_exporter/pull/434))
|
||||
|
||||
This release is built with Go 1.17.9, to address security issues in Go.
|
||||
|
||||
## 0.22.4 / 2021-11-26
|
||||
|
||||
* [BUGFIX] Make Docker image compatible with the runAsNonRoot setting in Kubernetes pods ([#409](https://github.com/prometheus/statsd_exporter/pull/409))
|
||||
* [BUGFIX] Library: fix support for custom Registerers with histograms and summaries ([#410](https://github.com/prometheus/statsd_exporter/pull/410))
|
||||
|
||||
## 0.22.3 / 2021-10-26
|
||||
|
||||
* [BUGFIX] Accept metrics with multiple dashes even if not mapped ([#402](https://github.com/prometheus/statsd_exporter/pull/402))
|
||||
|
||||
## 0.22.2 / 2021-09-10
|
||||
|
||||
* [ENHANCEMENT] Add metrics to relay ([#393](https://github.com/prometheus/statsd_exporter/pull/393))
|
||||
|
||||
## 0.22.1 / 2021-09-01
|
||||
|
||||
* [ENHANCEMENT] Accept incoming metrics with multiple dashes (with mapping) ([#381](https://github.com/prometheus/statsd_exporter//pull/381))
|
||||
* [ENHANCEMENT] Allow forwarding messages to statsd for easier transition ([#388](https://github.com/prometheus/statsd_exporter/pull/388))
|
||||
* [BUGFIX] Actually expose pprof endpoints ([#386](https://github.com/prometheus/statsd_exporter/pull/386))
|
||||
* [BUGFIX] Fix performance regression on metric ingestion ([#390](https://github.com/prometheus/statsd_exporter/pull/390))
|
||||
|
||||
## 0.21.0 / 2021-06-10
|
||||
|
||||
* [ENHANCEMENT] Update dependencies & switch to go-kit/log ([#379](https://github.com/prometheus/statsd_exporter/pull/379))
|
||||
|
||||
This release changes the log format to be more structured, in line with other Prometheus projects.
|
||||
|
||||
## 0.20.3 / 2021-06-04
|
||||
|
||||
* [ENHANCEMENT] Use extracted go-kit/log to reduce transitive dependencies ([#378](https://github.com/prometheus/statsd_exporter/pull/378))
|
||||
|
||||
Once again there is no functional change.
|
||||
For library users, the dependency tree shrinks considerably.
|
||||
See [prometheus/common#255](https://github.com/prometheus/common/issues/255) for more details.
|
||||
|
||||
## 0.20.2 / 2021-05-03
|
||||
|
||||
* [BUGFIX] Remove copyleft licensed dependency ([#375](https://github.com/prometheus/statsd_exporter/pull/375))
|
||||
|
||||
There is no functional change for exporter users.
|
||||
Removing this dependency reduces uncertainty for anyone reusing the mapping code.
|
||||
|
||||
## 0.20.1 / 2021-03-26
|
||||
|
||||
* [CHANGE] [library] Split mapper caches out from mapper ([#363](https://github.com/prometheus/statsd_exporter/pull/363))
|
||||
* [BUGFIX] Accept metric segments that start with numbers ([#365](https://github.com/prometheus/statsd_exporter/pull/365))
|
||||
|
||||
## 0.20.0 / 2021-02-05
|
||||
|
||||
* [ENHANCEMENT] Support full defaults for summaries and histograms ([#361](https://github.com/prometheus/statsd_exporter/pull/361))
|
||||
|
||||
This completes support for `summary_options` and `histogram_options`.
|
||||
Change the legacy configuration attributes throughout the mapping configuration as follows:
|
||||
|
||||
* `quantiles: …` to `summary_options: { quantiles: … }`
|
||||
* `buckets: …` to `histogram_options: { buckets: … }`
|
||||
* `timer_type` to `observer_type`.
|
||||
|
||||
Support for the deprecated attributes will be removed in a future release.
|
||||
|
||||
## 0.19.1 / 2021-01-29
|
||||
|
||||
* [BUGFIX] Don't return empty responses to lifecycle api requests ([#360](https://github.com/prometheus/statsd_exporter/pull/360))
|
||||
|
||||
## 0.19.0 / 2021-01-22
|
||||
|
||||
* [CHANGE] [library] Require explicit Registerer ([#347](https://github.com/prometheus/statsd_exporter/pull/347))
|
||||
* [ENHANCEMENT] Add /-/healthy and /-/ready endpoints ([#339](https://github.com/prometheus/statsd_exporter/pull/339))
|
||||
* [BUGFIX] Do not open network ports when only checking config ([#357](https://github.com/prometheus/statsd_exporter/pull/357))
|
||||
|
||||
## 0.18.0 / 2020-08-21
|
||||
|
||||
* [ENHANCEMENT] Allow turning off tagging extensions ([#325](https://github.com/prometheus/statsd_exporter/pull/325))
|
||||
* [ENHANCEMENT] Add a lifecycle API for configuration reloads and restarts ([#329](https://github.com/prometheus/statsd_exporter/pull/329))
|
||||
|
||||
This release changes the interface for the [`github.com/prometheus/statsd_exporter/pkg/line` library package](https://pkg.go.dev/github.com/prometheus/statsd_exporter@v0.18.0/pkg/line?tab=doc) to support the new configurability.
|
||||
|
||||
## 0.17.0 / 2020-06-26
|
||||
|
||||
* [CHANGE] Support non-timer distributions without unit conversion ([#314](https://github.com/prometheus/statsd_exporter/pull/314))
|
||||
* [ENHANCEMENT] Offline configuration check ([#312](https://github.com/prometheus/statsd_exporter/pull/312))
|
||||
* [ENHANCEMENT] Support the SignalFX tagging extension ([#315](https://github.com/prometheus/statsd_exporter/pull/315))
|
||||
* [BUGFIX] Allow matching single-letter metric name components ([#309](https://github.com/prometheus/statsd_exporter/pull/309))
|
||||
|
||||
Distribution and histogram events (type `d`, `h`) are now treated as distinct from timer events (type `ms`).
|
||||
Their values are observed as they are, while timer events are converted from milliseconds to seconds.
|
||||
|
||||
To reflect this generalization, the `observer_type` mapping option replaces `timer_type`.
|
||||
Similary, change `match_metric_type: timer` to `match_metric_type: observer`.
|
||||
The old name remains available for compatibility.
|
||||
|
||||
For users of the mapper library, the `ObserverEvent` replaces `TimerEvent`.
|
||||
For timer metrics, it is emitted by the mapper already converted to seconds.
|
||||
|
||||
## 0.16.0 / 2020-05-29
|
||||
|
||||
* [CHANGE] Break out much of the exporter into reusable packages ([#298](https://github.com/prometheus/statsd_exporter/pull/298))
|
||||
* [ENHANCEMENT] Log ingested lines at debug level ([#305](https://github.com/prometheus/statsd_exporter/pull/305))
|
||||
|
||||
This release mainly consists of an internal reorganization of the exporter.
|
||||
This should not have any impact on users of the binary, if it does, please file
|
||||
an issue.
|
||||
|
||||
For users of the existing library packages, nothing changes.
|
||||
|
||||
There are now multiple new packages available, exposing functionality that had
|
||||
been locked away in the main package. Consider the interfaces of these
|
||||
libraries preliminary; we will change them as we gain experience in how they
|
||||
are used.
|
||||
|
||||
## 0.15.0 / 2020-03-05
|
||||
|
||||
* [ENHANCEMENT] Allow setting granularity for summary metrics ([#290](https://github.com/prometheus/statsd_exporter/pull/290))
|
||||
* [ENHANCEMENT] Support a random-replacement cache invalidation strategy ([#281](https://github.com/prometheus/statsd_exporter/pull/281)
|
||||
|
||||
To facilitate the expanded settings for summaries, the configuration format changes from
|
||||
|
||||
```yaml
|
||||
mappings:
|
||||
- match: …
|
||||
timer_type: summary
|
||||
quantiles:
|
||||
- quantile: 0.99
|
||||
error: 0.001
|
||||
- quantile: 0.95
|
||||
error: 0.01
|
||||
…
|
||||
```
|
||||
|
||||
to
|
||||
|
||||
```yaml
|
||||
mappings:
|
||||
- match: …
|
||||
timer_type: summary
|
||||
summary_options:
|
||||
quantiles:
|
||||
- quantile: 0.99
|
||||
error: 0.001
|
||||
- quantile: 0.95
|
||||
error: 0.01
|
||||
…
|
||||
max_summary_age: 30s
|
||||
summary_age_buckets: 3
|
||||
stream_buffer_size: 1000
|
||||
…
|
||||
```
|
||||
|
||||
For consistency, the format for histogram buckets also changes from
|
||||
|
||||
```yaml
|
||||
mappings:
|
||||
- match: …
|
||||
timer_type: histogram
|
||||
buckets: [ 0.01, 0.025, 0.05, 0.1 ]
|
||||
```
|
||||
|
||||
to
|
||||
|
||||
```yaml
|
||||
mappings:
|
||||
- match: …
|
||||
timer_type: histogram
|
||||
histogram_options:
|
||||
buckets: [ 0.01, 0.025, 0.05, 0.1 ]
|
||||
```
|
||||
|
||||
Transitionally, the old format will still work but is *deprecated*. The new
|
||||
settings are optional.
|
||||
|
||||
For users of the [mapper](https://pkg.go.dev/github.com/prometheus/statsd_exporter/pkg/mapper?tab=doc)
|
||||
as a library, this is a breaking change. To adjust your code, replace
|
||||
`mapping.Buckets` with `mapping.HistogramOptions.Buckets` and
|
||||
`mapping.Quantiles` with `mapping.SummaryOptions.Quantiles`.
|
||||
|
||||
## 0.14.1 / 2020-01-13
|
||||
|
||||
* [BUGFIX] Mapper cache poisoning when name is variable ([#286](https://github.com/prometheus/statsd_exporter/pull/286))
|
||||
* [BUGFIX] nil pointer dereference in UDP listener ([#287](https://github.com/prometheus/statsd_exporter/pull/287))
|
||||
|
||||
Thank you to everyone who reported these, and @bakins for the mapper cache fix!
|
||||
|
||||
## 0.14.0 / 2020-01-10
|
||||
|
||||
* [CHANGE] Switch logging to go-kit ([#283](https://github.com/prometheus/statsd_exporter/pull/283))
|
||||
* [CHANGE] Rename existing metric for mapping cache size ([#284](https://github.com/prometheus/statsd_exporter/pull/284))
|
||||
* [ENHANCEMENT] Add metrics for mapping cache hits ([#280](https://github.com/prometheus/statsd_exporter/pull/280))
|
||||
|
||||
Logs are more structured now. The `fatal` log level no longer exists; use `--log.level=error` instead. The valid log formats are `logfmt` and `json`.
|
||||
|
||||
The metric `statsd_exporter_cache_length` is now called `statsd_metric_mapper_cache_length`.
|
||||
|
||||
## 0.13.0 / 2019-12-06
|
||||
|
||||
* [ENHANCEMENT] Support sampling factors for all statsd metric types ([#264](https://github.com/prometheus/statsd_exporter/issues/250))
|
||||
* [ENHANCEMENT] Support Librato and InfluxDB labeling formats ([#267](https://github.com/prometheus/statsd_exporter/pull/267))
|
||||
|
||||
## 0.12.2 / 2019-07-25
|
||||
|
||||
* [BUGFIX] Fix Unix socket handler ([#252](https://github.com/prometheus/statsd_exporter/pull/252))
|
||||
* [BUGFIX] Fix panic under high load ([#253](https://github.com/prometheus/statsd_exporter/pull/253))
|
||||
|
||||
Thank you to everyone who reported and helped debug these issues!
|
||||
|
||||
## 0.12.1 / 2019-07-08
|
||||
|
||||
* [BUGFIX] Renew TTL when a metric receives updates ([#246](https://github.com/prometheus/statsd_exporter/pull/246))
|
||||
* [CHANGE] Reload on SIGHUP instead of watching the file ([#243](https://github.com/prometheus/statsd_exporter/pull/243))
|
||||
|
||||
## 0.11.2 / 2019-06-14
|
||||
|
||||
* [BUGFIX] Fix TCP handler ([#235](https://github.com/prometheus/statsd_exporter/pull/235))
|
||||
|
||||
## 0.11.1 / 2019-06-14
|
||||
|
||||
* [ENHANCEMENT] Batch event processing for improved ingestion performance ([#227](https://github.com/prometheus/statsd_exporter/pull/227))
|
||||
* [ENHANCEMENT] Switch Prometheus client to promhttp, freeing the standard HTTP metrics ([#233](https://github.com/prometheus/statsd_exporter/pull/233))
|
||||
|
||||
With #233, the exporter no longer exports metrics about its own HTTP status. These were not helpful since you could not get them when scraping fails. This allows mapping to metric names like `http_requests_total` that are useful as application metrics.
|
||||
|
||||
## 0.10.6 / 2019-06-07
|
||||
|
||||
* [BUGFIX] Fix mapping collision for metrics with different types, but the same name ([#229](https://github.com/prometheus/statsd_exporter/pull/229))
|
||||
|
||||
## 0.10.5 / 2019-05-27
|
||||
|
||||
* [BUGFIX] Fix "Error: inconsistent label cardinality: expected 0 label values but got N in prometheus.Labels" ([#224](https://github.com/prometheus/statsd_exporter/pull/224))
|
||||
|
||||
## 0.10.4 / 2019-05-20
|
||||
|
||||
* [BUGFIX] Revert #218 due to a race condition ([#221](https://github.com/prometheus/statsd_exporter/pull/221))
|
||||
|
||||
## 0.10.3 / 2019-05-17
|
||||
|
||||
* [ENHANCEMENT] Reduce allocations when escaping metric names ([#217](https://github.com/prometheus/statsd_exporter/pull/217))
|
||||
* [ENHANCEMENT] Reduce allocations when handling packets ([#218](https://github.com/prometheus/statsd_exporter/pull/218))
|
||||
* [ENHANCEMENT] Optimize label sorting ([#219](https://github.com/prometheus/statsd_exporter/pull/219))
|
||||
|
||||
This release is entirely powered by @claytono. Kudos!
|
||||
|
||||
## 0.10.2 / 2019-05-17
|
||||
|
||||
* [CHANGE] Do not run as root in the Docker container by default ([#202](https://github.com/prometheus/statsd_exporter/pull/202))
|
||||
* [FEATURE] Add metric for count of events by action ([#193](https://github.com/prometheus/statsd_exporter/pull/193))
|
||||
* [FEATURE] Add metric for count of distinct metric names ([#200](https://github.com/prometheus/statsd_exporter/pull/200))
|
||||
* [FEATURE] Add UNIX socket listener support ([#199](https://github.com/prometheus/statsd_exporter/pull/199))
|
||||
* [FEATURE] Accept Datadog [distributions](https://docs.datadoghq.com/graphing/metrics/distributions/) ([#211](https://github.com/prometheus/statsd_exporter/pull/211))
|
||||
* [ENHANCEMENT] Add a health check to the Docker container ([#182](https://github.com/prometheus/statsd_exporter/pull/182))
|
||||
* [ENHANCEMENT] Allow inconsistent label sets ([#194](https://github.com/prometheus/statsd_exporter/pull/194))
|
||||
* [ENHANCEMENT] Speed up sanitization of metric names ([#197](https://github.com/prometheus/statsd_exporter/pull/197))
|
||||
* [ENHANCEMENT] Enable pprof endpoints ([#205](https://github.com/prometheus/statsd_exporter/pull/205))
|
||||
* [ENHANCEMENT] DogStatsD tag parsing is faster ([#210](https://github.com/prometheus/statsd_exporter/pull/210))
|
||||
* [ENHANCEMENT] Cache mapped metrics ([#198](https://github.com/prometheus/statsd_exporter/pull/198))
|
||||
* [BUGFIX] Fix panic if a mapping resulted in an empty name ([#192](https://github.com/prometheus/statsd_exporter/pull/192))
|
||||
* [BUGFIX] Ensure that there are always default quantiles if using summaries ([#212](https://github.com/prometheus/statsd_exporter/pull/212))
|
||||
* [BUGFIX] Prevent ingesting conflicting metric types that would make scraping fail ([#213](https://github.com/prometheus/statsd_exporter/pull/213))
|
||||
|
||||
With #192, the count of events rejected because of negative counter increments has moved into the `statsd_exporter_events_error_total` metric, instead of being lumped in with the different kinds of successful events.
|
||||
|
||||
## 0.9.0 / 2019-03-11
|
||||
|
||||
* [ENHANCEMENT] Update the Prometheus client library to 0.9.2 ([#171](https://github.com/prometheus/statsd_exporter/pull/171))
|
||||
* [FEATURE] Metrics can now be expired with a per-mapping TTL ([#164](https://github.com/prometheus/statsd_exporter/pull/164))
|
||||
* [CHANGE] Timers that mapped to a summary are scaled to seconds, just like histograms ([#178](https://github.com/prometheus/statsd_exporter/pull/178))
|
||||
|
||||
If you are using summaries, all your quantiles and `_total` will change by a factor of 1000.
|
||||
Adjust your queries and dashboards, or consider switching to histograms altogether.
|
||||
|
||||
## 0.8.1 / 2018-12-05
|
||||
|
||||
* [BUGFIX] Expose the counter for unmapped matches ([#161](https://github.com/prometheus/statsd_exporter/pull/161))
|
||||
* [BUGFIX] Unsuccessful backtracking does not clobber captures ([#169](https://github.com/prometheus/statsd_exporter/pull/169), fixes [#168](https://github.com/prometheus/statsd_exporter/issues/168))
|
||||
|
||||
## 0.8.0 / 2018-10-12
|
||||
|
||||
* [ENHANCEMENT] Speed up glob matching ([#157](https://github.com/prometheus/statsd_exporter/pull/157))
|
||||
|
||||
This release replaces the implementation of the glob matching mechanism,
|
||||
speeding it up significantly. In certain sub-optimal configurations, a warning
|
||||
is logged.
|
||||
|
||||
This major enhancement was contributed by [Wangchong Zhou](https://github.com/fffonion).
|
||||
|
||||
## 0.7.0 / 2018-08-22
|
||||
|
||||
This is a breaking release, but the migration is easy: command line flags now
|
||||
require two dashes (`--help` instead of `-help`). The previous flag library
|
||||
already accepts this form, so if necessary you can migrate the flags first
|
||||
before upgrading.
|
||||
|
||||
The deprecated `--statsd.listen-address` flag has been removed, use
|
||||
`--statsd.listen-udp` instead.
|
||||
|
||||
* [CHANGE] Switch to Kingpin for flags, fixes setting log level ([#141](https://github.com/prometheus/statsd_exporter/pull/141))
|
||||
* [ENHANCEMENT] Allow matching on specific metric types ([#136](https://github.com/prometheus/statsd_exporter/pulls/136))
|
||||
* [ENHANCEMENT] Summary quantiles can be configured ([#135](https://github.com/prometheus/statsd_exporter/pulls/135))
|
||||
* [BUGFIX] Fix panic if an invalid regular expression is supplied ([#126](https://github.com/prometheus/statsd_exporter/pulls/126))
|
||||
|
||||
## 0.6.0 / 2018-01-17
|
||||
|
||||
* [ENHANCEMENT] Add a drop action ([#115](https://github.com/prometheus/statsd_exporter/pulls/115))
|
||||
* [ENHANCEMENT] Allow templating metric names ([#117](https://github.com/prometheus/statsd_exporter/pulls/117))
|
||||
|
||||
## 0.5.0 / 2017-11-16
|
||||
|
||||
NOTE: This release breaks backward compatibility. `statsd_exporter` now uses
|
||||
a YAML configuration file. You must convert your mappings configuration to
|
||||
the new format when you upgrade. For example, the configuration
|
||||
|
||||
```
|
||||
test.dispatcher.*.*.*
|
||||
name="dispatcher_events_total"
|
||||
processor="$1"
|
||||
action="$2"
|
||||
outcome="$3"
|
||||
job="test_dispatcher"
|
||||
|
||||
*.signup.*.*
|
||||
name="signup_events_total"
|
||||
provider="$2"
|
||||
outcome="$3"
|
||||
job="${1}_server"
|
||||
```
|
||||
|
||||
now has the format
|
||||
|
||||
```yaml
|
||||
mappings:
|
||||
- match: test.dispatcher.*.*.*
|
||||
help: "The total number of events handled by the dispatcher."
|
||||
name: "dispatcher_events_total"
|
||||
labels:
|
||||
processor: "$1"
|
||||
action: "$2"
|
||||
outcome: "$3"
|
||||
job: "test_dispatcher"
|
||||
- match: *.signup.*.*
|
||||
name: "signup_events_total"
|
||||
help: "The total number of signup events."
|
||||
labels:
|
||||
provider: "$2"
|
||||
outcome: "$3"
|
||||
job: "${1}_server"
|
||||
```
|
||||
|
||||
The help field is optional.
|
||||
|
||||
There is a [tool](https://github.com/bakins/statsd-exporter-convert) available to help with this conversion.
|
||||
|
||||
* [CHANGE] Replace the overloaded "packets" metric ([#106](https://github.com/prometheus/statsd_exporter/pulls/106))
|
||||
* [CHANGE] Removed `-statsd.add-suffix` option flag [#99](https://github.com/prometheus/statsd_exporter/pulls/99). Users should remove
|
||||
this flag when upgrading. Metrics will no longer automatically include the
|
||||
suffixes `_timer` or `counter`. You may need to adjust any graphs that used
|
||||
metrics with these suffixes.
|
||||
* [CHANGE] Reduce log levels [#92](https://github.com/prometheus/statsd_exporter/pulls/92). Many log events have been changed from error
|
||||
to debug log level.
|
||||
* [CHANGE] Use YAML for configuration file [#66](https://github.com/prometheus/statsd_exporter/pulls/66). See note above about file format
|
||||
conversion.
|
||||
* [ENHANCEMENT] Allow help text to be customized [#87](https://github.com/prometheus/statsd_exporter/pulls/87)
|
||||
* [ENHANCEMENT] Add support for regex mappers [#85](https://github.com/prometheus/statsd_exporter/pulls/85)
|
||||
* [ENHANCEMENT] Add TCP listener support [#71](https://github.com/prometheus/statsd_exporter/pulls/71)
|
||||
* [ENHANCEMENT] Allow histograms for timer metrics [#66](https://github.com/prometheus/statsd_exporter/pulls/66)
|
||||
* [ENHANCEMENT] Added support for sampling factor on timing events [#28](https://github.com/prometheus/statsd_exporter/pulls/28)
|
||||
* [BUGFIX] Conflicting label sets no longer crash the exporter and will be
|
||||
ignored. Restart to clear the remembered label set. [#72](https://github.com/prometheus/statsd_exporter/pulls/72)
|
||||
|
||||
## 0.4.0 / 2017-05-12
|
||||
|
||||
* [ENHANCEMENT] Improve mapping configuration parser [#61](https://github.com/prometheus/statsd_exporter/pulls/61)
|
||||
* [ENHANCEMENT] Add increment/decrement support to Gauges [#65](https://github.com/prometheus/statsd_exporter/pulls/65)
|
||||
* [BUGFIX] Tolerate more forms of broken lines from StatsD [#48](https://github.com/prometheus/statsd_exporter/pulls/48)
|
||||
* [BUGFIX] Skip metrics with invalid utf8 [#50](https://github.com/prometheus/statsd_exporter/pulls/50)
|
||||
* [BUGFIX] ListenAndServe now fails on exit [#58](https://github.com/prometheus/statsd_exporter/pulls/58)
|
||||
|
||||
## 0.3.0 / 2016-05-05
|
||||
|
||||
* [CHANGE] Drop `_count` suffix for `loaded_mappings` metric ([#41](https://github.com/prometheus/statsd_exporter/pulls/41))
|
||||
* [ENHANCEMENT] Use common's log and version packages, and add -version flag ([#44](https://github.com/prometheus/statsd_exporter/pulls/44))
|
||||
* [ENHANCEMENT] Add flag to disable metric type suffixes ([#37](https://github.com/prometheus/statsd_exporter/pulls/37))
|
||||
* [BUGFIX] Increase receivable UDP datagram size to 65535 bytes ([#36](https://github.com/prometheus/statsd_exporter/pulls/36))
|
||||
* [BUGFIX] Warn, not panic when negative number counter is submitted ([#33](https://github.com/prometheus/statsd_exporter/pulls/33))
|
||||
|
||||
## 0.2.0 / 2016-03-19
|
||||
|
||||
NOTE: This release renames `statsd_bridge` to `statsd_exporter`
|
||||
|
||||
* [CHANGE] New Dockerfile using alpine-golang-make-onbuild base image (#17)
|
||||
* [IMPROVEMENT] Allow configuration of UDP read buffer (#22)
|
||||
* [BUGFIX] allow metrics with dashes when mapping (#24)
|
||||
* [IMPROVEMENT] add root endpoint with redirect (#25)
|
||||
* [CHANGE] rename bridge to exporter (#26)
|
||||
|
||||
* [CHANGE] New Dockerfile using alpine-golang-make-onbuild base image ([#17](https://github.com/prometheus/statsd_exporter/pulls/17))
|
||||
* [ENHANCEMENT] Allow configuration of UDP read buffer ([#22](https://github.com/prometheus/statsd_exporter/pulls/22))
|
||||
* [BUGFIX] allow metrics with dashes when mapping ([#24](https://github.com/prometheus/statsd_exporter/pulls/24))
|
||||
* [ENHANCEMENT] add root endpoint with redirect ([#25](https://github.com/prometheus/statsd_exporter/pulls/25))
|
||||
* [CHANGE] rename bridge to exporter ([#26](https://github.com/prometheus/statsd_exporter/pulls/26))
|
||||
|
||||
## 0.1.0 / 2015-04-17
|
||||
|
||||
|
|
3
CODE_OF_CONDUCT.md
Normal file
3
CODE_OF_CONDUCT.md
Normal file
|
@ -0,0 +1,3 @@
|
|||
# Prometheus Community Code of Conduct
|
||||
|
||||
Prometheus follows the [CNCF Code of Conduct](https://github.com/cncf/foundation/blob/main/code-of-conduct.md).
|
|
@ -2,9 +2,9 @@
|
|||
|
||||
Prometheus uses GitHub to manage reviews of pull requests.
|
||||
|
||||
* If you have a trivial fix or improvement, go ahead and create a pull
|
||||
request, addressing (with `@...`) one or more of the maintainers
|
||||
(see [AUTHORS.md](AUTHORS.md)) in the description of the pull request.
|
||||
* If you have a trivial fix or improvement, go ahead and create a pull request,
|
||||
addressing (with `@...`) the maintainer of this repository (see
|
||||
[MAINTAINERS.md](MAINTAINERS.md)) in the description of the pull request.
|
||||
|
||||
* If you plan to do something more involved, first discuss your ideas
|
||||
on our [mailing list](https://groups.google.com/forum/?fromgroups#!forum/prometheus-developers).
|
||||
|
@ -16,3 +16,5 @@ Prometheus uses GitHub to manage reviews of pull requests.
|
|||
and the _Formatting and style_ section of Peter Bourgon's [Go: Best
|
||||
Practices for Production
|
||||
Environments](http://peter.bourgon.org/go-in-production/#formatting-and-style).
|
||||
|
||||
* [![Gitpod ready-to-code](https://img.shields.io/badge/Gitpod-ready--to--code-blue?logo=gitpod)](https://gitpod.io/#https://github.com/prometheus/statsd_exporter)
|
||||
|
|
15
Dockerfile
15
Dockerfile
|
@ -1,4 +1,13 @@
|
|||
FROM sdurrheimer/alpine-golang-make-onbuild
|
||||
MAINTAINER The Prometheus Authors <prometheus-developers@googlegroups.com>
|
||||
ARG ARCH="amd64"
|
||||
ARG OS="linux"
|
||||
FROM quay.io/prometheus/busybox-${OS}-${ARCH}:latest
|
||||
LABEL maintainer="The Prometheus Authors <prometheus-developers@googlegroups.com>"
|
||||
|
||||
EXPOSE 9102 9125/udp
|
||||
ARG ARCH="amd64"
|
||||
ARG OS="linux"
|
||||
COPY .build/${OS}-${ARCH}/statsd_exporter /bin/statsd_exporter
|
||||
|
||||
USER 65534
|
||||
EXPOSE 9102 9125 9125/udp
|
||||
HEALTHCHECK CMD wget --spider -S "http://localhost:9102/metrics" -T 60 2>&1 || exit 1
|
||||
ENTRYPOINT [ "/bin/statsd_exporter" ]
|
||||
|
|
1
MAINTAINERS.md
Normal file
1
MAINTAINERS.md
Normal file
|
@ -0,0 +1 @@
|
|||
* Matthias Rampke <matthias@prometheus.io>
|
17
Makefile
17
Makefile
|
@ -11,7 +11,18 @@
|
|||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
VERSION := 0.2.0
|
||||
TARGET := statsd_exporter
|
||||
# Needs to be defined before including Makefile.common to auto-generate targets
|
||||
DOCKER_ARCHS ?= amd64 armv7 arm64
|
||||
|
||||
include Makefile.COMMON
|
||||
include Makefile.common
|
||||
|
||||
STATICCHECK_IGNORE =
|
||||
|
||||
DOCKER_IMAGE_NAME ?= statsd-exporter
|
||||
|
||||
.PHONY: bench
|
||||
bench:
|
||||
@echo ">> running all benchmarks"
|
||||
$(GO) test -bench . -race $(pkgs)
|
||||
|
||||
all: bench
|
||||
|
|
131
Makefile.COMMON
131
Makefile.COMMON
|
@ -1,131 +0,0 @@
|
|||
# Copyright 2015 The Prometheus Authors
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
# THE AUTHORITATIVE VERSION OF THIS MAKEFILE LIVES IN:
|
||||
#
|
||||
# https://github.com/prometheus/utils
|
||||
#
|
||||
# PLEASE MAKE ANY CHANGES THERE AND PROPAGATE THEM TO ALL PROMETHEUS
|
||||
# REPOSITORIES THAT ARE USING THIS MAKEFILE.
|
||||
#
|
||||
# This file provides common Makefile infrastructure for several Prometheus
|
||||
# components. This includes make tasks for downloading Go, setting up a
|
||||
# self-contained build environment, fetching Go dependencies, building
|
||||
# binaries, running tests, and doing release management. This file is intended
|
||||
# to be included from a project's Makefile, which needs to define the following
|
||||
# variables, at a minimum:
|
||||
#
|
||||
# * VERSION - The current version of the project in question.
|
||||
# * TARGET - The desired name of the built binary.
|
||||
#
|
||||
# Many of the variables defined below are defined conditionally (using '?'),
|
||||
# which allows the project's main Makefile to override any of these settings, if
|
||||
# needed. See also:
|
||||
#
|
||||
# https://www.gnu.org/software/make/manual/html_node/Flavors.html#Flavors.
|
||||
#
|
||||
# The including Makefile may define any number of extra targets that are
|
||||
# specific to that project.
|
||||
|
||||
VERSION ?= $(error VERSION not set in including Makefile)
|
||||
TARGET ?= $(error TARGET not set in including Makefile)
|
||||
|
||||
SRC ?= $(shell find . -type f -name "*.go" ! -path "./.build/*")
|
||||
GOOS ?= $(shell uname | tr A-Z a-z)
|
||||
GOARCH ?= $(subst x86_64,amd64,$(patsubst i%86,386,$(shell uname -m)))
|
||||
|
||||
GO_VERSION ?= 1.5.3
|
||||
GOPATH ?= $(CURDIR)/.build/gopath
|
||||
ROOTPKG ?= github.com/prometheus/$(TARGET)
|
||||
SELFLINK ?= $(GOPATH)/src/$(ROOTPKG)
|
||||
|
||||
# Check for the correct version of go in the path. If we find it, use it.
|
||||
# Otherwise, prepare to build go locally.
|
||||
ifeq ($(shell command -v "go" >/dev/null && go version | sed -e 's/^[^0-9.]*\([0-9.]*\).*/\1/'), $(GO_VERSION))
|
||||
GOCC ?= $(shell command -v "go")
|
||||
GOFMT ?= $(shell command -v "gofmt")
|
||||
GO ?= GOPATH=$(GOPATH) $(GOCC)
|
||||
else
|
||||
GOURL ?= https://golang.org/dl
|
||||
GOPKG ?= go$(GO_VERSION).$(GOOS)-$(GOARCH).tar.gz
|
||||
GOROOT ?= $(CURDIR)/.build/go$(GO_VERSION)
|
||||
GOCC ?= $(GOROOT)/bin/go
|
||||
GOFMT ?= $(GOROOT)/bin/gofmt
|
||||
GO ?= GOPATH=$(GOPATH) GOROOT=$(GOROOT) $(GOCC)
|
||||
endif
|
||||
|
||||
# Use vendored dependencies if available. Otherwise try to download them.
|
||||
ifneq (,$(wildcard vendor))
|
||||
DEPENDENCIES := $(shell find vendor/ -type f -iname '*.go')
|
||||
GO := GO15VENDOREXPERIMENT=1 $(GO)
|
||||
else
|
||||
DEPENDENCIES := dependencies-stamp
|
||||
endif
|
||||
|
||||
# Never honor GOBIN, should it be set at all.
|
||||
unexport GOBIN
|
||||
|
||||
SUFFIX ?= $(GOOS)-$(GOARCH)
|
||||
BINARY ?= $(TARGET)
|
||||
ARCHIVE ?= $(TARGET)-$(VERSION).$(SUFFIX).tar.gz
|
||||
|
||||
default: $(BINARY)
|
||||
|
||||
$(BINARY): $(GOCC) $(SRC) $(DEPENDENCIES) Makefile Makefile.COMMON | $(SELFLINK)
|
||||
cd $(SELFLINK) && $(GO) build $(GOFLAGS) -o $@
|
||||
|
||||
.PHONY: archive
|
||||
archive: $(ARCHIVE)
|
||||
|
||||
$(ARCHIVE): $(BINARY)
|
||||
tar -czf $@ $<
|
||||
|
||||
.PHONY: tag
|
||||
tag:
|
||||
git tag $(VERSION)
|
||||
git push --tags
|
||||
|
||||
.PHONY: test
|
||||
test: $(GOCC) $(DEPENDENCIES) | $(SELFLINK)
|
||||
cd $(SELFLINK) && $(GO) test $$($(GO) list ./... | grep -v /vendor/)
|
||||
|
||||
.PHONY: format
|
||||
format: $(GOCC)
|
||||
find . -iname '*.go' | egrep -v "^\./\.build|./generated|\./vendor|\.(l|y)\.go" | xargs -n1 $(GOFMT) -w -s=true
|
||||
|
||||
.PHONY: clean
|
||||
clean:
|
||||
rm -rf $(BINARY) $(ARCHIVE) .build *-stamp
|
||||
|
||||
|
||||
|
||||
$(GOCC):
|
||||
@echo Go version $(GO_VERSION) required but not found in PATH.
|
||||
@echo About to download and install go$(GO_VERSION) to $(GOROOT)
|
||||
@echo Abort now if you want to manually install it system-wide instead.
|
||||
@echo
|
||||
@sleep 5
|
||||
mkdir -p .build
|
||||
# The archive contains a single directory called 'go/'.
|
||||
curl -L $(GOURL)/$(GOPKG) | tar -C .build -xzf -
|
||||
rm -rf $(GOROOT)
|
||||
mv .build/go $(GOROOT)
|
||||
|
||||
$(SELFLINK):
|
||||
mkdir -p $(dir $@)
|
||||
ln -s $(CURDIR) $@
|
||||
|
||||
# Download dependencies if project doesn't vendor them.
|
||||
dependencies-stamp: $(GOCC) $(SRC) | $(SELFLINK)
|
||||
$(GO) get -d
|
||||
touch $@
|
277
Makefile.common
Normal file
277
Makefile.common
Normal file
|
@ -0,0 +1,277 @@
|
|||
# Copyright 2018 The Prometheus Authors
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
|
||||
# A common Makefile that includes rules to be reused in different prometheus projects.
|
||||
# !!! Open PRs only against the prometheus/prometheus/Makefile.common repository!
|
||||
|
||||
# Example usage :
|
||||
# Create the main Makefile in the root project directory.
|
||||
# include Makefile.common
|
||||
# customTarget:
|
||||
# @echo ">> Running customTarget"
|
||||
#
|
||||
|
||||
# Ensure GOBIN is not set during build so that promu is installed to the correct path
|
||||
unexport GOBIN
|
||||
|
||||
GO ?= go
|
||||
GOFMT ?= $(GO)fmt
|
||||
FIRST_GOPATH := $(firstword $(subst :, ,$(shell $(GO) env GOPATH)))
|
||||
GOOPTS ?=
|
||||
GOHOSTOS ?= $(shell $(GO) env GOHOSTOS)
|
||||
GOHOSTARCH ?= $(shell $(GO) env GOHOSTARCH)
|
||||
|
||||
GO_VERSION ?= $(shell $(GO) version)
|
||||
GO_VERSION_NUMBER ?= $(word 3, $(GO_VERSION))
|
||||
PRE_GO_111 ?= $(shell echo $(GO_VERSION_NUMBER) | grep -E 'go1\.(10|[0-9])\.')
|
||||
|
||||
PROMU := $(FIRST_GOPATH)/bin/promu
|
||||
pkgs = ./...
|
||||
|
||||
ifeq (arm, $(GOHOSTARCH))
|
||||
GOHOSTARM ?= $(shell GOARM= $(GO) env GOARM)
|
||||
GO_BUILD_PLATFORM ?= $(GOHOSTOS)-$(GOHOSTARCH)v$(GOHOSTARM)
|
||||
else
|
||||
GO_BUILD_PLATFORM ?= $(GOHOSTOS)-$(GOHOSTARCH)
|
||||
endif
|
||||
|
||||
GOTEST := $(GO) test
|
||||
GOTEST_DIR :=
|
||||
ifneq ($(CIRCLE_JOB),)
|
||||
ifneq ($(shell command -v gotestsum 2> /dev/null),)
|
||||
GOTEST_DIR := test-results
|
||||
GOTEST := gotestsum --junitfile $(GOTEST_DIR)/unit-tests.xml --
|
||||
endif
|
||||
endif
|
||||
|
||||
PROMU_VERSION ?= 0.15.0
|
||||
PROMU_URL := https://github.com/prometheus/promu/releases/download/v$(PROMU_VERSION)/promu-$(PROMU_VERSION).$(GO_BUILD_PLATFORM).tar.gz
|
||||
|
||||
SKIP_GOLANGCI_LINT :=
|
||||
GOLANGCI_LINT :=
|
||||
GOLANGCI_LINT_OPTS ?=
|
||||
GOLANGCI_LINT_VERSION ?= v1.56.2
|
||||
# golangci-lint only supports linux, darwin and windows platforms on i386/amd64/arm64.
|
||||
# windows isn't included here because of the path separator being different.
|
||||
ifeq ($(GOHOSTOS),$(filter $(GOHOSTOS),linux darwin))
|
||||
ifeq ($(GOHOSTARCH),$(filter $(GOHOSTARCH),amd64 i386 arm64))
|
||||
# If we're in CI and there is an Actions file, that means the linter
|
||||
# is being run in Actions, so we don't need to run it here.
|
||||
ifneq (,$(SKIP_GOLANGCI_LINT))
|
||||
GOLANGCI_LINT :=
|
||||
else ifeq (,$(CIRCLE_JOB))
|
||||
GOLANGCI_LINT := $(FIRST_GOPATH)/bin/golangci-lint
|
||||
else ifeq (,$(wildcard .github/workflows/golangci-lint.yml))
|
||||
GOLANGCI_LINT := $(FIRST_GOPATH)/bin/golangci-lint
|
||||
endif
|
||||
endif
|
||||
endif
|
||||
|
||||
PREFIX ?= $(shell pwd)
|
||||
BIN_DIR ?= $(shell pwd)
|
||||
DOCKER_IMAGE_TAG ?= $(subst /,-,$(shell git rev-parse --abbrev-ref HEAD))
|
||||
DOCKERFILE_PATH ?= ./Dockerfile
|
||||
DOCKERBUILD_CONTEXT ?= ./
|
||||
DOCKER_REPO ?= prom
|
||||
|
||||
DOCKER_ARCHS ?= amd64
|
||||
|
||||
BUILD_DOCKER_ARCHS = $(addprefix common-docker-,$(DOCKER_ARCHS))
|
||||
PUBLISH_DOCKER_ARCHS = $(addprefix common-docker-publish-,$(DOCKER_ARCHS))
|
||||
TAG_DOCKER_ARCHS = $(addprefix common-docker-tag-latest-,$(DOCKER_ARCHS))
|
||||
|
||||
SANITIZED_DOCKER_IMAGE_TAG := $(subst +,-,$(DOCKER_IMAGE_TAG))
|
||||
|
||||
ifeq ($(GOHOSTARCH),amd64)
|
||||
ifeq ($(GOHOSTOS),$(filter $(GOHOSTOS),linux freebsd darwin windows))
|
||||
# Only supported on amd64
|
||||
test-flags := -race
|
||||
endif
|
||||
endif
|
||||
|
||||
# This rule is used to forward a target like "build" to "common-build". This
|
||||
# allows a new "build" target to be defined in a Makefile which includes this
|
||||
# one and override "common-build" without override warnings.
|
||||
%: common-% ;
|
||||
|
||||
.PHONY: common-all
|
||||
common-all: precheck style check_license lint yamllint unused build test
|
||||
|
||||
.PHONY: common-style
|
||||
common-style:
|
||||
@echo ">> checking code style"
|
||||
@fmtRes=$$($(GOFMT) -d $$(find . -path ./vendor -prune -o -name '*.go' -print)); \
|
||||
if [ -n "$${fmtRes}" ]; then \
|
||||
echo "gofmt checking failed!"; echo "$${fmtRes}"; echo; \
|
||||
echo "Please ensure you are using $$($(GO) version) for formatting code."; \
|
||||
exit 1; \
|
||||
fi
|
||||
|
||||
.PHONY: common-check_license
|
||||
common-check_license:
|
||||
@echo ">> checking license header"
|
||||
@licRes=$$(for file in $$(find . -type f -iname '*.go' ! -path './vendor/*') ; do \
|
||||
awk 'NR<=3' $$file | grep -Eq "(Copyright|generated|GENERATED)" || echo $$file; \
|
||||
done); \
|
||||
if [ -n "$${licRes}" ]; then \
|
||||
echo "license header checking failed:"; echo "$${licRes}"; \
|
||||
exit 1; \
|
||||
fi
|
||||
|
||||
.PHONY: common-deps
|
||||
common-deps:
|
||||
@echo ">> getting dependencies"
|
||||
$(GO) mod download
|
||||
|
||||
.PHONY: update-go-deps
|
||||
update-go-deps:
|
||||
@echo ">> updating Go dependencies"
|
||||
@for m in $$($(GO) list -mod=readonly -m -f '{{ if and (not .Indirect) (not .Main)}}{{.Path}}{{end}}' all); do \
|
||||
$(GO) get -d $$m; \
|
||||
done
|
||||
$(GO) mod tidy
|
||||
|
||||
.PHONY: common-test-short
|
||||
common-test-short: $(GOTEST_DIR)
|
||||
@echo ">> running short tests"
|
||||
$(GOTEST) -short $(GOOPTS) $(pkgs)
|
||||
|
||||
.PHONY: common-test
|
||||
common-test: $(GOTEST_DIR)
|
||||
@echo ">> running all tests"
|
||||
$(GOTEST) $(test-flags) $(GOOPTS) $(pkgs)
|
||||
|
||||
$(GOTEST_DIR):
|
||||
@mkdir -p $@
|
||||
|
||||
.PHONY: common-format
|
||||
common-format:
|
||||
@echo ">> formatting code"
|
||||
$(GO) fmt $(pkgs)
|
||||
|
||||
.PHONY: common-vet
|
||||
common-vet:
|
||||
@echo ">> vetting code"
|
||||
$(GO) vet $(GOOPTS) $(pkgs)
|
||||
|
||||
.PHONY: common-lint
|
||||
common-lint: $(GOLANGCI_LINT)
|
||||
ifdef GOLANGCI_LINT
|
||||
@echo ">> running golangci-lint"
|
||||
$(GOLANGCI_LINT) run $(GOLANGCI_LINT_OPTS) $(pkgs)
|
||||
endif
|
||||
|
||||
.PHONY: common-lint-fix
|
||||
common-lint-fix: $(GOLANGCI_LINT)
|
||||
ifdef GOLANGCI_LINT
|
||||
@echo ">> running golangci-lint fix"
|
||||
$(GOLANGCI_LINT) run --fix $(GOLANGCI_LINT_OPTS) $(pkgs)
|
||||
endif
|
||||
|
||||
.PHONY: common-yamllint
|
||||
common-yamllint:
|
||||
@echo ">> running yamllint on all YAML files in the repository"
|
||||
ifeq (, $(shell command -v yamllint 2> /dev/null))
|
||||
@echo "yamllint not installed so skipping"
|
||||
else
|
||||
yamllint .
|
||||
endif
|
||||
|
||||
# For backward-compatibility.
|
||||
.PHONY: common-staticcheck
|
||||
common-staticcheck: lint
|
||||
|
||||
.PHONY: common-unused
|
||||
common-unused:
|
||||
@echo ">> running check for unused/missing packages in go.mod"
|
||||
$(GO) mod tidy
|
||||
@git diff --exit-code -- go.sum go.mod
|
||||
|
||||
.PHONY: common-build
|
||||
common-build: promu
|
||||
@echo ">> building binaries"
|
||||
$(PROMU) build --prefix $(PREFIX) $(PROMU_BINARIES)
|
||||
|
||||
.PHONY: common-tarball
|
||||
common-tarball: promu
|
||||
@echo ">> building release tarball"
|
||||
$(PROMU) tarball --prefix $(PREFIX) $(BIN_DIR)
|
||||
|
||||
.PHONY: common-docker-repo-name
|
||||
common-docker-repo-name:
|
||||
@echo "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)"
|
||||
|
||||
.PHONY: common-docker $(BUILD_DOCKER_ARCHS)
|
||||
common-docker: $(BUILD_DOCKER_ARCHS)
|
||||
$(BUILD_DOCKER_ARCHS): common-docker-%:
|
||||
docker build -t "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(SANITIZED_DOCKER_IMAGE_TAG)" \
|
||||
-f $(DOCKERFILE_PATH) \
|
||||
--build-arg ARCH="$*" \
|
||||
--build-arg OS="linux" \
|
||||
$(DOCKERBUILD_CONTEXT)
|
||||
|
||||
.PHONY: common-docker-publish $(PUBLISH_DOCKER_ARCHS)
|
||||
common-docker-publish: $(PUBLISH_DOCKER_ARCHS)
|
||||
$(PUBLISH_DOCKER_ARCHS): common-docker-publish-%:
|
||||
docker push "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(SANITIZED_DOCKER_IMAGE_TAG)"
|
||||
|
||||
DOCKER_MAJOR_VERSION_TAG = $(firstword $(subst ., ,$(shell cat VERSION)))
|
||||
.PHONY: common-docker-tag-latest $(TAG_DOCKER_ARCHS)
|
||||
common-docker-tag-latest: $(TAG_DOCKER_ARCHS)
|
||||
$(TAG_DOCKER_ARCHS): common-docker-tag-latest-%:
|
||||
docker tag "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(SANITIZED_DOCKER_IMAGE_TAG)" "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:latest"
|
||||
docker tag "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(SANITIZED_DOCKER_IMAGE_TAG)" "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:v$(DOCKER_MAJOR_VERSION_TAG)"
|
||||
|
||||
.PHONY: common-docker-manifest
|
||||
common-docker-manifest:
|
||||
DOCKER_CLI_EXPERIMENTAL=enabled docker manifest create -a "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME):$(SANITIZED_DOCKER_IMAGE_TAG)" $(foreach ARCH,$(DOCKER_ARCHS),$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$(ARCH):$(SANITIZED_DOCKER_IMAGE_TAG))
|
||||
DOCKER_CLI_EXPERIMENTAL=enabled docker manifest push "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME):$(SANITIZED_DOCKER_IMAGE_TAG)"
|
||||
|
||||
.PHONY: promu
|
||||
promu: $(PROMU)
|
||||
|
||||
$(PROMU):
|
||||
$(eval PROMU_TMP := $(shell mktemp -d))
|
||||
curl -s -L $(PROMU_URL) | tar -xvzf - -C $(PROMU_TMP)
|
||||
mkdir -p $(FIRST_GOPATH)/bin
|
||||
cp $(PROMU_TMP)/promu-$(PROMU_VERSION).$(GO_BUILD_PLATFORM)/promu $(FIRST_GOPATH)/bin/promu
|
||||
rm -r $(PROMU_TMP)
|
||||
|
||||
.PHONY: proto
|
||||
proto:
|
||||
@echo ">> generating code from proto files"
|
||||
@./scripts/genproto.sh
|
||||
|
||||
ifdef GOLANGCI_LINT
|
||||
$(GOLANGCI_LINT):
|
||||
mkdir -p $(FIRST_GOPATH)/bin
|
||||
curl -sfL https://raw.githubusercontent.com/golangci/golangci-lint/$(GOLANGCI_LINT_VERSION)/install.sh \
|
||||
| sed -e '/install -d/d' \
|
||||
| sh -s -- -b $(FIRST_GOPATH)/bin $(GOLANGCI_LINT_VERSION)
|
||||
endif
|
||||
|
||||
.PHONY: precheck
|
||||
precheck::
|
||||
|
||||
define PRECHECK_COMMAND_template =
|
||||
precheck:: $(1)_precheck
|
||||
|
||||
PRECHECK_COMMAND_$(1) ?= $(1) $$(strip $$(PRECHECK_OPTIONS_$(1)))
|
||||
.PHONY: $(1)_precheck
|
||||
$(1)_precheck:
|
||||
@if ! $$(PRECHECK_COMMAND_$(1)) 1>/dev/null 2>&1; then \
|
||||
echo "Execution of '$$(PRECHECK_COMMAND_$(1))' command failed. Is $(1) installed?"; \
|
||||
exit 1; \
|
||||
fi
|
||||
endef
|
578
README.md
578
README.md
|
@ -1,40 +1,123 @@
|
|||
statsd_exporter
|
||||
=============
|
||||
# statsd exporter [![Build Status](https://travis-ci.org/prometheus/statsd_exporter.svg)][travis]
|
||||
|
||||
[![CircleCI](https://circleci.com/gh/prometheus/statsd_exporter/tree/master.svg?style=shield)][circleci]
|
||||
[![Docker Repository on Quay](https://quay.io/repository/prometheus/statsd-exporter/status)][quay]
|
||||
[![Docker Pulls](https://img.shields.io/docker/pulls/prom/statsd-exporter.svg)][hub]
|
||||
|
||||
`statsd_exporter` receives StatsD-style metrics and exports them as Prometheus metrics.
|
||||
|
||||
## Overview
|
||||
|
||||
### With StatsD
|
||||
The StatsD exporter is a drop-in replacement for StatsD.
|
||||
This exporter translates StatsD metrics to Prometheus metrics via configured mapping rules.
|
||||
|
||||
To pipe metrics from an existing StatsD environment into Prometheus, configure
|
||||
StatsD's repeater backend to repeat all received packets to a `statsd_exporter`
|
||||
process. This exporter translates StatsD metrics to Prometheus metrics via
|
||||
configured mapping rules.
|
||||
We recommend using the exporter only as an intermediate solution, and switching to [native Prometheus instrumentation](http://prometheus.io/docs/instrumenting/clientlibs/) in the long term.
|
||||
While it is common to run centralized StatsD servers, the exporter works best as a [sidecar](https://docs.microsoft.com/en-us/azure/architecture/patterns/sidecar).
|
||||
|
||||
+----------+ +-------------------+ +--------------+
|
||||
| StatsD |---(UDP repeater)--->| statsd_exporter |<---(scrape /metrics)---| Prometheus |
|
||||
+----------+ +-------------------+ +--------------+
|
||||
### Transitioning from an existing StatsD setup
|
||||
|
||||
### Without StatsD
|
||||
The relay feature allows for a gradual transition.
|
||||
|
||||
Since the StatsD exporter uses the same UDP protocol as StatsD itself, you can
|
||||
also configure your applications to send StatsD metrics directly to the exporter.
|
||||
In that case, you don't need to run a StatsD server anymore.
|
||||
Introduce the exporter by adding it as a sidecar alongside the application instances.
|
||||
In Kubernetes, this means adding it to the [pod](https://kubernetes.io/docs/concepts/workloads/pods/).
|
||||
Use the `--statsd.relay.address` to forward metrics to your existing StatsD UDP endpoint.
|
||||
Relaying forwards statsd events unmodified, preserving the original metric name and tags in any format.
|
||||
|
||||
We recommend this only as an intermediate solution and recommend switching to
|
||||
[native Prometheus instrumentation](http://prometheus.io/docs/instrumenting/clientlibs/)
|
||||
in the long term.
|
||||
+-------------+ +----------+ +------------+
|
||||
| Application +--->| Exporter +----------------->| StatsD |
|
||||
+-------------+ +----------+ +------------+
|
||||
^
|
||||
| +------------+
|
||||
+----------------------+ Prometheus |
|
||||
+------------+
|
||||
|
||||
### Relaying from StatsD
|
||||
|
||||
To pipe metrics from an existing StatsD environment into Prometheus, configure StatsD's repeater backend to repeat all received metrics to a `statsd_exporter` process.
|
||||
|
||||
+----------+ +-------------------+ +--------------+
|
||||
| StatsD |---(UDP/TCP repeater)--->| statsd_exporter |<---(scrape /metrics)---| Prometheus |
|
||||
+----------+ +-------------------+ +--------------+
|
||||
|
||||
This allows trying out the exporter with minimal effort, but does not provide the per-instance metrics of the sidecar pattern.
|
||||
|
||||
### Tagging Extensions
|
||||
|
||||
The exporter supports Librato, InfluxDB, DogStatsD, and SignalFX-style tags,
|
||||
which will be converted into Prometheus labels.
|
||||
|
||||
For Librato-style tags, they must be appended to the metric name with a
|
||||
delimiting `#`, as so:
|
||||
|
||||
```
|
||||
metric.name#tagName=val,tag2Name=val2:0|c
|
||||
```
|
||||
|
||||
See the [statsd-librato-backend README](https://github.com/librato/statsd-librato-backend#tags)
|
||||
for a more complete description.
|
||||
|
||||
For InfluxDB-style tags, they must be appended to the metric name with a
|
||||
delimiting comma, as so:
|
||||
|
||||
```
|
||||
metric.name,tagName=val,tag2Name=val2:0|c
|
||||
```
|
||||
|
||||
See [this InfluxDB blog post](https://www.influxdata.com/blog/getting-started-with-sending-statsd-metrics-to-telegraf-influxdb/#introducing-influx-statsd)
|
||||
for a larger overview.
|
||||
|
||||
|
||||
For DogStatsD-style tags, they're appended as a `|#` delimited section at the
|
||||
end of the metric, as so:
|
||||
|
||||
```
|
||||
metric.name:0|c|#tagName:val,tag2Name:val2
|
||||
```
|
||||
|
||||
See [Tags](https://docs.datadoghq.com/developers/dogstatsd/data_types/#tagging)
|
||||
in the DogStatsD documentation for the concept description and
|
||||
[Datagram Format](https://docs.datadoghq.com/developers/dogstatsd/datagram_shell/).
|
||||
If you encounter problems, note that this tagging style is incompatible with
|
||||
the original `statsd` implementation.
|
||||
|
||||
For [SignalFX dimension](https://github.com/signalfx/signalfx-agent/blob/main/docs/monitors/collectd-statsd.md#adding-dimensions-to-statsd-metrics), add the tags to the metric name in square brackets, as so:
|
||||
|
||||
```
|
||||
metric.name[tagName=val,tag2Name=val2]:0|c
|
||||
```
|
||||
|
||||
Be aware: If you mix tag styles (e.g., Librato/InfluxDB with DogStatsD), the exporter will consider this an error and the behavior is undefined.
|
||||
Also, tags without values (`#some_tag`) are not supported and will be ignored.
|
||||
|
||||
The exporter parses all tagging formats by default, but individual tagging formats can be disabled with command line flags:
|
||||
```
|
||||
--no-statsd.parse-dogstatsd-tags
|
||||
--no-statsd.parse-influxdb-tags
|
||||
--no-statsd.parse-librato-tags
|
||||
--no-statsd.parse-signalfx-tags
|
||||
```
|
||||
|
||||
By default, labels explicitly specified in configuration take precedence over labels from tags.
|
||||
To set the label from the statsd event tag, use [`honor_labels`](#honor-labels).
|
||||
|
||||
## Building and Running
|
||||
|
||||
$ go build
|
||||
$ ./statsd_exporter --help
|
||||
Usage of ./statsd_exporter:
|
||||
-statsd.listen-address=":9125": The UDP address on which to receive statsd metric lines.
|
||||
-statsd.mapping-config="": Metric mapping configuration file name.
|
||||
-web.listen-address=":9102": The address on which to expose the web interface and generated Prometheus metrics.
|
||||
-web.telemetry-path="/metrics": Path under which to expose metrics.
|
||||
NOTE: Version 0.7.0 switched to the [kingpin](https://github.com/alecthomas/kingpin) flags library. With this change, flag behaviour is POSIX-ish:
|
||||
|
||||
* long flags start with two dashes (`--version`)
|
||||
* boolean long flags are disabled by prefixing with no (`--flag-name` is true, `--no-flag-name` is false)
|
||||
* multiple short flags can be combined (but there currently is only one)
|
||||
* flag processing stops at the first `--`
|
||||
* see `--help` for a full list of flags
|
||||
|
||||
## Lifecycle API
|
||||
|
||||
The `statsd_exporter` has an optional lifecycle API (disabled by default) that can be used to reload or quit the exporter
|
||||
by sending a `PUT` or `POST` request to the `/-/reload` or `/-/quit` endpoints.
|
||||
|
||||
## Relay
|
||||
|
||||
The `statsd_exporter` has an optional mode that will buffer and relay incoming statsd lines to a remote server. This is useful to "tee" the data when migrating to using the exporter. The relay will flush the buffer at least once per second to avoid delaying delivery of metrics.
|
||||
|
||||
## Tests
|
||||
|
||||
|
@ -43,8 +126,10 @@ in the long term.
|
|||
## Metric Mapping and Configuration
|
||||
|
||||
The `statsd_exporter` can be configured to translate specific dot-separated StatsD
|
||||
metrics into labeled Prometheus metrics via a simple mapping language. A
|
||||
mapping definition starts with a line matching the StatsD metric in question,
|
||||
metrics into labeled Prometheus metrics via a simple mapping language. The config
|
||||
file is reloaded on SIGHUP.
|
||||
|
||||
A mapping definition starts with a line matching the StatsD metric in question,
|
||||
with `*`s acting as wildcards for each dot-separated metric component. The
|
||||
lines following the matching expression must contain one `label="value"` pair
|
||||
each, and at least define the metric name (label name `name`). The Prometheus
|
||||
|
@ -54,57 +139,436 @@ starting at 1. Multiple matching definitions are separated by one or more empty
|
|||
lines. The first mapping rule that matches a StatsD metric wins.
|
||||
|
||||
Metrics that don't match any mapping in the configuration file are translated
|
||||
into Prometheus metrics without any labels and with certain characters escaped
|
||||
(`_` -> `__`; `-` -> `__`; `.` -> `_`).
|
||||
into Prometheus metrics without any labels and with any non-alphanumeric
|
||||
characters, including periods, translated into underscores.
|
||||
|
||||
In general, the different metric types are translated as follows, with certain
|
||||
suffixes appended to the Prometheus metric names:
|
||||
In general, the different metric types are translated as follows:
|
||||
|
||||
StatsD gauge -> Prometheus gauge (suffix `_gauge`)
|
||||
StatsD gauge -> Prometheus gauge
|
||||
|
||||
StatsD counter -> Prometheus counter (suffix `_counter`)
|
||||
StatsD counter -> Prometheus counter
|
||||
|
||||
StatsD timer -> Prometheus summary (suffix `_timer`) <-- indicates timer quantiles
|
||||
-> Prometheus counter (suffix `_timer_total`) <-- indicates total time spent
|
||||
-> Prometheus counter (suffix `_timer_count`) <-- indicates total number of timer events
|
||||
StatsD timer, histogram, distribution -> Prometheus summary or histogram
|
||||
|
||||
### Glob matching
|
||||
|
||||
The default (and fastest) `glob` mapping style uses `*` to denote parts of the statsd metric name that may vary.
|
||||
These varying parts can then be referenced in the construction of the Prometheus metric name and labels.
|
||||
|
||||
An example mapping configuration:
|
||||
|
||||
test.dispatcher.*.*.*
|
||||
name="dispatcher_events"
|
||||
processor="$1"
|
||||
action="$2"
|
||||
outcome="$3"
|
||||
job="test_dispatcher"
|
||||
|
||||
*.signup.*.*
|
||||
name="signup_events"
|
||||
provider="$2"
|
||||
outcome="$3"
|
||||
job="${1}_server"
|
||||
```yaml
|
||||
mappings:
|
||||
- match: "test.dispatcher.*.*.*"
|
||||
name: "dispatcher_events_total"
|
||||
labels:
|
||||
processor: "$1"
|
||||
action: "$2"
|
||||
outcome: "$3"
|
||||
job: "test_dispatcher"
|
||||
- match: "*.signup.*.*"
|
||||
name: "signup_events_total"
|
||||
labels:
|
||||
provider: "$2"
|
||||
outcome: "$3"
|
||||
job: "${1}_server"
|
||||
```
|
||||
|
||||
This would transform these example StatsD metrics into Prometheus metrics as
|
||||
follows:
|
||||
|
||||
test.dispatcher.FooProcessor.send.success (counter)
|
||||
=> dispatcher_events_counter{processor="FooProcessor", action="send", outcome="success", job="test_dispatcher"}
|
||||
test.dispatcher.FooProcessor.send.success
|
||||
=> dispatcher_events_total{processor="FooProcessor", action="send", outcome="success", job="test_dispatcher"}
|
||||
|
||||
foo_product.signup.facebook.failure (counter)
|
||||
=> signup_events_counter{provider="facebook", outcome="failure", job="foo_product_server"}
|
||||
foo_product.signup.facebook.failure
|
||||
=> signup_events_total{provider="facebook", outcome="failure", job="foo_product_server"}
|
||||
|
||||
test.web-server.foo.bar (gauge)
|
||||
=> test_web__server_foo_bar_gauge{}
|
||||
test.web-server.foo.bar
|
||||
=> test_web_server_foo_bar{}
|
||||
|
||||
Each mapping in the configuration file must define a `name` for the metric. The
|
||||
metric's name can contain `$n`-style references to be replaced by the n-th
|
||||
wildcard match in the matching line. That allows for dynamic rewrites, such as:
|
||||
|
||||
```yaml
|
||||
mappings:
|
||||
- match: "test.*.*.counter"
|
||||
name: "${2}_total"
|
||||
labels:
|
||||
provider: "$1"
|
||||
```
|
||||
|
||||
Glob matching offers the best performance for common mappings.
|
||||
|
||||
#### Ordering glob rules
|
||||
|
||||
List more specific matches before wildcards, from left to right:
|
||||
|
||||
a.b.c
|
||||
a.b.*
|
||||
a.*.d
|
||||
a.*.*
|
||||
|
||||
This avoids unexpected shadowing of later rules, and performance impact from backtracking.
|
||||
|
||||
Alternatively, you can disable mapping ordering altogether.
|
||||
With unordered mapping, at each hierarchy level the most specific match wins.
|
||||
This has the same effect as using the recommended ordering.
|
||||
|
||||
### Regular expression matching
|
||||
|
||||
The `regex` mapping style uses regular expressions to match the full statsd metric name.
|
||||
Use it if the glob mapping is not flexible enough to pull structured data from the available statsd metric names.
|
||||
|
||||
Regular expression matching is significantly slower than glob mapping as all mappings must be tested in order.
|
||||
Because of this, **regex mappings are only executed after all glob mappings**.
|
||||
In other words, glob mappings take preference over regex matches, irrespective of the order in which they are specified.
|
||||
Regular expression matches are always evaluated in order, and the first match wins.
|
||||
|
||||
The metric name can also contain references to regex matches. The mapping above
|
||||
could be written as:
|
||||
|
||||
```yaml
|
||||
mappings:
|
||||
- match: "test\\.(\\w+)\\.(\\w+)\\.counter"
|
||||
match_type: regex
|
||||
name: "${2}_total"
|
||||
labels:
|
||||
provider: "$1"
|
||||
- match: "(.*)\\.(.*)--(.*)\\.status\.(.*)\\.count"
|
||||
match_type: regex
|
||||
name: "request_total"
|
||||
labels:
|
||||
hostname: "$1"
|
||||
exec: "$2"
|
||||
protocol: "$3"
|
||||
code: "$4"
|
||||
```
|
||||
|
||||
Be aware about yaml escape rules as a mapping like the following one will not work.
|
||||
```yaml
|
||||
mappings:
|
||||
- match: "test\\.(\w+)\\.(\w+)\\.counter"
|
||||
match_type: regex
|
||||
name: "${2}_total"
|
||||
labels:
|
||||
provider: "$1"
|
||||
```
|
||||
|
||||
#### Special match groups
|
||||
|
||||
When using regex, the match group `0` is the full match and can be used to attach labels to the metric.
|
||||
Example:
|
||||
|
||||
```yaml
|
||||
mappings:
|
||||
- match: ".+"
|
||||
match_type: regex
|
||||
name: "$0"
|
||||
labels:
|
||||
statsd_metric_name: "$0"
|
||||
```
|
||||
|
||||
If a metric `my.statsd_counter` is received, the metric name will **still** be mapped to `my_statsd_counter` (Prometheus compatible name).
|
||||
But the metric will also have the label `statsd_metric_name` with the value `my.statsd_counter` (unchanged value).
|
||||
|
||||
Note: If you use the `match` like the example (i.e. `.+`), be aware that it will be a "catch-all" block. So it should come at the very end of the mapping list.
|
||||
|
||||
The same is not achievable with glob matching, for more details check [this issue](https://github.com/prometheus/statsd_exporter/issues/444).
|
||||
|
||||
### Naming, labels, and help
|
||||
|
||||
Please note that metrics with the same name must also have the same set of
|
||||
label names.
|
||||
|
||||
If the default metric help text is insufficient for your needs you may use the YAML
|
||||
configuration to specify a custom help text for each mapping:
|
||||
|
||||
```yaml
|
||||
mappings:
|
||||
- match: "http.request.*"
|
||||
help: "Total number of http requests"
|
||||
name: "http_requests_total"
|
||||
labels:
|
||||
code: "$1"
|
||||
```
|
||||
|
||||
### Honor labels
|
||||
|
||||
By default, labels specified in the mapping configuration take precedence over tags in the statsd event.
|
||||
|
||||
To set the label value to the original tag value, if present, specify `honor_labels: true` in the mapping configuration.
|
||||
In this case, the label specified in the mapping acts as a default.
|
||||
|
||||
### StatsD timers and distributions
|
||||
|
||||
By default, statsd timers and distributions (collectively "observers") are
|
||||
represented as a Prometheus summary with quantiles. You may optionally
|
||||
configure the [quantiles and acceptable
|
||||
error](https://prometheus.io/docs/practices/histograms/#quantiles), as well
|
||||
as adjusting how the summary metric is aggregated:
|
||||
|
||||
```yaml
|
||||
mappings:
|
||||
- match: "test.timing.*.*.*"
|
||||
observer_type: summary
|
||||
name: "my_timer"
|
||||
labels:
|
||||
provider: "$2"
|
||||
outcome: "$3"
|
||||
job: "${1}_server"
|
||||
summary_options:
|
||||
quantiles:
|
||||
- quantile: 0.99
|
||||
error: 0.001
|
||||
- quantile: 0.95
|
||||
error: 0.01
|
||||
- quantile: 0.9
|
||||
error: 0.05
|
||||
- quantile: 0.5
|
||||
error: 0.005
|
||||
max_age: 30s
|
||||
age_buckets: 3
|
||||
buf_cap: 1000
|
||||
```
|
||||
|
||||
The default quantiles are 0.99, 0.9, and 0.5.
|
||||
|
||||
The default summary age is 10 minutes, the default number of buckets
|
||||
is 5 and the default buffer size is 500.
|
||||
See also the [`golang_client` docs](https://godoc.org/github.com/prometheus/client_golang/prometheus#SummaryOpts).
|
||||
The `max_summary_age` corresponds to `SummaryOptions.MaxAge`, `summary_age_buckets` to `SummaryOptions.AgeBuckets` and `stream_buffer_size` to `SummaryOptions.BufCap`.
|
||||
|
||||
In the configuration, one may also set the observer type to "histogram". For example,
|
||||
to set the observer type for a single timer metric:
|
||||
|
||||
```yaml
|
||||
mappings:
|
||||
- match: "test.timing.*.*.*"
|
||||
observer_type: histogram
|
||||
histogram_options:
|
||||
buckets: [ 0.01, 0.025, 0.05, 0.1 ]
|
||||
native_histogram_bucket_factor: 1.1
|
||||
native_histogram_max_buckets: 256
|
||||
name: "my_timer"
|
||||
labels:
|
||||
provider: "$2"
|
||||
outcome: "$3"
|
||||
job: "${1}_server"
|
||||
```
|
||||
|
||||
If not set, then the default
|
||||
[Prometheus client
|
||||
values](https://godoc.org/github.com/prometheus/client_golang/prometheus#pkg-variables)
|
||||
are used for the histogram buckets:
|
||||
`[.005, .01, .025, .05, .1, .25, .5, 1, 2.5, 5, 10]`.
|
||||
`+Inf` is added automatically.
|
||||
If your Prometheus server is enabled to scrape native histograms (v2.40.0+),
|
||||
then you can set the `native_histogram_bucket_factor` to configure precision of the
|
||||
buckets in the sparse histogram. More about this in the original [client_golang docs](https://github.com/prometheus/client_golang/blob/449b46435075e6e069e05af920fe028b941033cf/prometheus/histogram.go#L399-L430).
|
||||
Also, a configuration of the maximum number of buckets can be set with `native_histogram_max_buckets`, this
|
||||
avoids the histograms to grow too large in memory. More about this in the original [client_golang docs](https://github.com/prometheus/client_golang/blob/449b46435075e6e069e05af920fe028b941033cf/prometheus/histogram.go#L443-L467).
|
||||
|
||||
`observer_type` is only used when the statsd metric type is a timer, histogram, or distribution.
|
||||
`buckets` is only used when the statsd metric type is one of these, and the `observer_type` is set to `histogram`.
|
||||
|
||||
Timers will be accepted with the `ms` statsd type.
|
||||
Statsd timer data is transmitted in milliseconds, while Prometheus expects the unit to be seconds.
|
||||
The exporter converts all timer observations to seconds.
|
||||
|
||||
Histogram and distribution events (`h` and `d` metric type) are not subject to unit conversion.
|
||||
|
||||
### DogStatsD Client Behavior
|
||||
|
||||
#### `timed()` decorator
|
||||
|
||||
The DogStatsD client's [timed](https://datadogpy.readthedocs.io/en/latest/#datadog.threadstats.base.ThreadStats.timed) decorator emits the metric in seconds but uses the `ms` type.
|
||||
Set [`use_ms=True`](https://datadogpy.readthedocs.io/en/latest/index.html?highlight=use_ms) to send the correct units.
|
||||
|
||||
### Regular expression matching
|
||||
|
||||
Another capability when using YAML configuration is the ability to define matches
|
||||
using raw regular expressions as opposed to the default globbing style of match.
|
||||
This may allow for pulling structured data from otherwise poorly named statsd
|
||||
metrics AND allow for more precise targetting of match rules. When no `match_type`
|
||||
parameter is specified the default value of `glob` will be assumed:
|
||||
|
||||
```yaml
|
||||
mappings:
|
||||
- match: "(.*)\\.(.*)--(.*)\\.status\\.(.*)\\.count"
|
||||
match_type: regex
|
||||
name: "request_total"
|
||||
labels:
|
||||
hostname: "$1"
|
||||
exec: "$2"
|
||||
protocol: "$3"
|
||||
code: "$4"
|
||||
```
|
||||
|
||||
### Global defaults
|
||||
|
||||
One may also set defaults for the observer type, histogram options, summary options, and match type.
|
||||
These will be used by all mappings that do not define them.
|
||||
|
||||
An option that can only be configured in `defaults` is `glob_disable_ordering`, which is `false` if omitted.
|
||||
By setting this to `true`, `glob` match type will not honor the occurance of rules in the mapping rules file and always treat `*` as lower priority than a concrete string.
|
||||
|
||||
Setting `buckets` or `quantiles` in the defaults is deprecated in favor of `histogram_options` and `summary_options`, which will override the deprecated values.
|
||||
|
||||
If `summary_options` is present in a mapping config, it will only override the fields set in the mapping. Unset fields in the mapping will take the values from the defaults.
|
||||
|
||||
```yaml
|
||||
defaults:
|
||||
observer_type: histogram
|
||||
histogram_options:
|
||||
buckets: [.005, .01, .025, .05, .1, .25, .5, 1, 2.5 ]
|
||||
native_histogram_bucket_factor: 1.1
|
||||
native_histogram_max_buckets: 256
|
||||
summary_options:
|
||||
quantiles:
|
||||
- quantile: 0.99
|
||||
error: 0.001
|
||||
- quantile: 0.95
|
||||
error: 0.01
|
||||
- quantile: 0.9
|
||||
error: 0.05
|
||||
- quantile: 0.5
|
||||
error: 0.005
|
||||
max_age: 5m
|
||||
age_buckets: 2
|
||||
buf_cap: 1000
|
||||
match_type: glob
|
||||
glob_disable_ordering: false
|
||||
ttl: 0 # metrics do not expire
|
||||
mappings:
|
||||
# This will be a histogram using the buckets set in `defaults`.
|
||||
- match: "test.timing.*.*.*"
|
||||
name: "my_timer"
|
||||
labels:
|
||||
provider: "$2"
|
||||
outcome: "$3"
|
||||
job: "${1}_server"
|
||||
# This will be a summary using the summary_options set in `defaults`
|
||||
- match: "other.distribution.*.*.*"
|
||||
observer_type: summary
|
||||
name: "other_distribution"
|
||||
labels:
|
||||
provider: "$2"
|
||||
outcome: "$3"
|
||||
job: "${1}_server_other"
|
||||
```
|
||||
|
||||
### `drop` action
|
||||
|
||||
You may also drop metrics by specifying a "drop" action on a match. For
|
||||
example:
|
||||
|
||||
```yaml
|
||||
mappings:
|
||||
# This metric would match as normal.
|
||||
- match: "test.timing.*.*.*"
|
||||
name: "my_timer"
|
||||
labels:
|
||||
provider: "$2"
|
||||
outcome: "$3"
|
||||
job: "${1}_server"
|
||||
# Any metric not matched will be dropped because "." matches all metrics.
|
||||
- match: "."
|
||||
match_type: regex
|
||||
action: drop
|
||||
name: "dropped"
|
||||
```
|
||||
|
||||
You can drop any metric using the normal match syntax.
|
||||
The default action is "map" which does the normal metrics mapping.
|
||||
|
||||
### Explicit metric type mapping
|
||||
|
||||
StatsD allows emitting of different metric types under the same metric name,
|
||||
but the Prometheus client library can't merge those. For this use-case the
|
||||
mapping definition allows you to specify which metric type to match:
|
||||
|
||||
```
|
||||
mappings:
|
||||
- match: "test.foo.*"
|
||||
name: "test_foo"
|
||||
match_metric_type: counter
|
||||
labels:
|
||||
provider: "$1"
|
||||
```
|
||||
|
||||
Possible values for `match_metric_type` are `gauge`, `counter` and `observer`.
|
||||
|
||||
### Mapping cache size and cache replacement policy
|
||||
|
||||
There is a cache used to improve the performance of the metric mapping, that can greatly improvement performance.
|
||||
The cache has a default maximum of 1000 unique statsd metric names -> prometheus metrics mappings that it can store.
|
||||
This maximum can be adjusted using the `statsd.cache-size` flag.
|
||||
|
||||
If the maximum is reached, entries are by default rotated using the [least recently used replacement policy](https://en.wikipedia.org/wiki/Cache_replacement_policies#Least_recently_used_(LRU)). This strategy is optimal when memory is constrained as only the most recent entries are retained.
|
||||
|
||||
Alternatively, you can choose a [random-replacement cache strategy](https://en.wikipedia.org/wiki/Cache_replacement_policies#Random_replacement_(RR)). This is less optimal if the cache is smaller than the cacheable set, but requires less locking. Use this for very high throughput, but make sure to allow for a cache that holds all metrics.
|
||||
|
||||
The optimal cache size is determined by the cardinality of the _incoming_ metrics.
|
||||
|
||||
### Time series expiration
|
||||
|
||||
The `ttl` parameter can be used to define the expiration time for stale metrics.
|
||||
The value is a time duration with valid time units: "ns", "us" (or "µs"),
|
||||
"ms", "s", "m", "h". For example, `ttl: 1m20s`. `0` value is used to indicate
|
||||
metrics that do not expire.
|
||||
|
||||
TTL configuration is stored for each mapped metric name/labels combination
|
||||
whenever new samples are received. This means that you cannot immediately
|
||||
expire a metric only by changing the mapping configuration. At least one
|
||||
sample must be received for updated mappings to take effect.
|
||||
|
||||
### Unit conversions
|
||||
|
||||
The `scale` parameter can be used to define unit conversions for metric values. The value is a floating point number to scale metric values by. This can be useful for converting non-base units (e.g. milliseconds, kilobytes) to base units (e.g. seconds, bytes) as recommended in [prometheus best practices](https://prometheus.io/docs/practices/naming/).
|
||||
|
||||
```yaml
|
||||
mappings:
|
||||
- match: foo.latency_ms
|
||||
name: foo_latency_seconds
|
||||
scale: 0.001
|
||||
- match: bar.processed_kb
|
||||
name: bar_processed_bytes
|
||||
scale: 1024
|
||||
- match: baz.latency_us
|
||||
name: baz_latency_seconds
|
||||
scale: 1e-6
|
||||
```
|
||||
|
||||
### Event flushing configuration
|
||||
|
||||
Internally `statsd_exporter` runs a goroutine for each network listener (UDP, TCP & Unix Socket). These each receive and parse metrics received into an event. For performance purposes, these events are queued internally and flushed to the main exporter goroutine periodically in batches. The size of this queue and the flush criteria can be tuned with the `--statsd.event-queue-size`, `--statsd.event-flush-threshold` and `--statsd.event-flush-interval`. However, the defaults should perform well even for very high traffic environments.
|
||||
|
||||
## Using Docker
|
||||
|
||||
You can deploy this exporter using the [prom/statsd-exporter](https://registry.hub.docker.com/u/prom/statsd-exporter/) Docker image.
|
||||
You can deploy this exporter using the [prom/statsd-exporter](https://registry.hub.docker.com/r/prom/statsd-exporter) Docker image.
|
||||
|
||||
For example:
|
||||
|
||||
```bash
|
||||
docker pull prom/statsd-exporter
|
||||
|
||||
docker run -d -p 9102:9102 -p 9125:9125/udp \
|
||||
-v $PWD/statsd_mapping.conf:/tmp/statsd_mapping.conf \
|
||||
prom/statsd-exporter -statsd.mapping-config=/tmp/statsd_mapping.conf
|
||||
docker run -d -p 9102:9102 -p 9125:9125 -p 9125:9125/udp \
|
||||
-v $PWD/statsd_mapping.yml:/tmp/statsd_mapping.yml \
|
||||
prom/statsd-exporter --statsd.mapping-config=/tmp/statsd_mapping.yml
|
||||
```
|
||||
|
||||
## Library packages
|
||||
|
||||
Parts of the implementation of this exporter are available as separate packages.
|
||||
See the [documentation](https://pkg.go.dev/github.com/prometheus/statsd_exporter/pkg) for details.
|
||||
|
||||
For the time being, there are *no stability guarantees* for library interfaces.
|
||||
We will try to call out any significant changes in the [changelog](https://github.com/prometheus/statsd_exporter/blob/master/CHANGELOG.md).
|
||||
Semantic versioning of the exporter is based on the impact on users of the exporter, not users of the library.
|
||||
|
||||
We encourage re-use of these packages and welcome [issues](https://github.com/prometheus/statsd_exporter/issues?q=is%3Aopen+is%3Aissue+label%3Alibrary) related to their usability as a library.
|
||||
|
||||
[circleci]: https://circleci.com/gh/prometheus/statsd_exporter
|
||||
[quay]: https://quay.io/repository/prometheus/statsd-exporter
|
||||
[hub]: https://hub.docker.com/r/prom/statsd-exporter/
|
||||
|
|
6
SECURITY.md
Normal file
6
SECURITY.md
Normal file
|
@ -0,0 +1,6 @@
|
|||
# Reporting a security issue
|
||||
|
||||
The Prometheus security policy, including how to report vulnerabilities, can be
|
||||
found here:
|
||||
|
||||
<https://prometheus.io/docs/operating/security/>
|
834
bridge_test.go
834
bridge_test.go
|
@ -15,73 +15,486 @@ package main
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/go-kit/log"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
dto "github.com/prometheus/client_model/go"
|
||||
|
||||
"github.com/prometheus/statsd_exporter/pkg/clock"
|
||||
"github.com/prometheus/statsd_exporter/pkg/event"
|
||||
"github.com/prometheus/statsd_exporter/pkg/exporter"
|
||||
"github.com/prometheus/statsd_exporter/pkg/line"
|
||||
"github.com/prometheus/statsd_exporter/pkg/listener"
|
||||
"github.com/prometheus/statsd_exporter/pkg/mapper"
|
||||
)
|
||||
|
||||
func TestHandlePacket(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
name string
|
||||
in string
|
||||
out Events
|
||||
out event.Events
|
||||
}{
|
||||
{
|
||||
name: "empty",
|
||||
}, {
|
||||
name: "simple counter",
|
||||
in: "foo:2|c",
|
||||
out: Events{
|
||||
&CounterEvent{
|
||||
metricName: "foo",
|
||||
value: 2,
|
||||
out: event.Events{
|
||||
&event.CounterEvent{
|
||||
CMetricName: "foo",
|
||||
CValue: 2,
|
||||
CLabels: map[string]string{},
|
||||
},
|
||||
},
|
||||
}, {
|
||||
name: "simple gauge",
|
||||
in: "foo:3|g",
|
||||
out: Events{
|
||||
&GaugeEvent{
|
||||
metricName: "foo",
|
||||
value: 3,
|
||||
out: event.Events{
|
||||
&event.GaugeEvent{
|
||||
GMetricName: "foo",
|
||||
GValue: 3,
|
||||
GLabels: map[string]string{},
|
||||
},
|
||||
},
|
||||
}, {
|
||||
name: "gauge with sampling",
|
||||
in: "foo:3|g|@0.2",
|
||||
out: event.Events{
|
||||
&event.GaugeEvent{
|
||||
GMetricName: "foo",
|
||||
GValue: 3,
|
||||
GLabels: map[string]string{},
|
||||
},
|
||||
},
|
||||
}, {
|
||||
name: "gauge decrement",
|
||||
in: "foo:-10|g",
|
||||
out: event.Events{
|
||||
&event.GaugeEvent{
|
||||
GMetricName: "foo",
|
||||
GValue: -10,
|
||||
GRelative: true,
|
||||
GLabels: map[string]string{},
|
||||
},
|
||||
},
|
||||
}, {
|
||||
name: "gauge increment",
|
||||
in: "foo:+10|g",
|
||||
out: event.Events{
|
||||
&event.GaugeEvent{
|
||||
GMetricName: "foo",
|
||||
GValue: 10,
|
||||
GRelative: true,
|
||||
GLabels: map[string]string{},
|
||||
},
|
||||
},
|
||||
}, {
|
||||
name: "gauge set negative",
|
||||
in: "foo:0|g\nfoo:-1|g",
|
||||
out: event.Events{
|
||||
&event.GaugeEvent{
|
||||
GMetricName: "foo",
|
||||
GValue: 0,
|
||||
GRelative: false,
|
||||
GLabels: map[string]string{},
|
||||
},
|
||||
&event.GaugeEvent{
|
||||
GMetricName: "foo",
|
||||
GValue: -1,
|
||||
GRelative: true,
|
||||
GLabels: map[string]string{},
|
||||
},
|
||||
},
|
||||
}, {
|
||||
// Test the sequence given here https://github.com/statsd/statsd/blob/master/docs/metric_types.md#gauges
|
||||
name: "gauge up and down",
|
||||
in: "gaugor:333|g\ngaugor:-10|g\ngaugor:+4|g",
|
||||
out: event.Events{
|
||||
&event.GaugeEvent{
|
||||
GMetricName: "gaugor",
|
||||
GValue: 333,
|
||||
GRelative: false,
|
||||
GLabels: map[string]string{},
|
||||
},
|
||||
&event.GaugeEvent{
|
||||
GMetricName: "gaugor",
|
||||
GValue: -10,
|
||||
GRelative: true,
|
||||
GLabels: map[string]string{},
|
||||
},
|
||||
&event.GaugeEvent{
|
||||
GMetricName: "gaugor",
|
||||
GValue: 4,
|
||||
GRelative: true,
|
||||
GLabels: map[string]string{},
|
||||
},
|
||||
},
|
||||
}, {
|
||||
name: "simple timer",
|
||||
in: "foo:200|ms",
|
||||
out: Events{
|
||||
&TimerEvent{
|
||||
metricName: "foo",
|
||||
value: 200,
|
||||
out: event.Events{
|
||||
&event.ObserverEvent{
|
||||
OMetricName: "foo",
|
||||
OValue: 0.2,
|
||||
OLabels: map[string]string{},
|
||||
},
|
||||
},
|
||||
}, {
|
||||
name: "simple histogram",
|
||||
in: "foo:200|h",
|
||||
out: event.Events{
|
||||
&event.ObserverEvent{
|
||||
OMetricName: "foo",
|
||||
OValue: 200,
|
||||
OLabels: map[string]string{},
|
||||
},
|
||||
},
|
||||
}, {
|
||||
name: "simple distribution",
|
||||
in: "foo:200|d",
|
||||
out: event.Events{
|
||||
&event.ObserverEvent{
|
||||
OMetricName: "foo",
|
||||
OValue: 200,
|
||||
OLabels: map[string]string{},
|
||||
},
|
||||
},
|
||||
}, {
|
||||
name: "distribution with sampling",
|
||||
in: "foo:0.01|d|@0.2|#tag1:bar,#tag2:baz",
|
||||
out: event.Events{
|
||||
&event.ObserverEvent{
|
||||
OMetricName: "foo",
|
||||
OValue: 0.01,
|
||||
OLabels: map[string]string{"tag1": "bar", "tag2": "baz"},
|
||||
},
|
||||
&event.ObserverEvent{
|
||||
OMetricName: "foo",
|
||||
OValue: 0.01,
|
||||
OLabels: map[string]string{"tag1": "bar", "tag2": "baz"},
|
||||
},
|
||||
&event.ObserverEvent{
|
||||
OMetricName: "foo",
|
||||
OValue: 0.01,
|
||||
OLabels: map[string]string{"tag1": "bar", "tag2": "baz"},
|
||||
},
|
||||
&event.ObserverEvent{
|
||||
OMetricName: "foo",
|
||||
OValue: 0.01,
|
||||
OLabels: map[string]string{"tag1": "bar", "tag2": "baz"},
|
||||
},
|
||||
&event.ObserverEvent{
|
||||
OMetricName: "foo",
|
||||
OValue: 0.01,
|
||||
OLabels: map[string]string{"tag1": "bar", "tag2": "baz"},
|
||||
},
|
||||
},
|
||||
}, {
|
||||
name: "librato tag extension",
|
||||
in: "foo#tag1=bar,tag2=baz:100|c",
|
||||
out: event.Events{
|
||||
&event.CounterEvent{
|
||||
CMetricName: "foo",
|
||||
CValue: 100,
|
||||
CLabels: map[string]string{"tag1": "bar", "tag2": "baz"},
|
||||
},
|
||||
},
|
||||
}, {
|
||||
name: "librato tag extension with tag keys unsupported by prometheus",
|
||||
in: "foo#09digits=0,tag.with.dots=1:100|c",
|
||||
out: event.Events{
|
||||
&event.CounterEvent{
|
||||
CMetricName: "foo",
|
||||
CValue: 100,
|
||||
CLabels: map[string]string{"_09digits": "0", "tag_with_dots": "1"},
|
||||
},
|
||||
},
|
||||
}, {
|
||||
name: "influxdb tag extension",
|
||||
in: "foo,tag1=bar,tag2=baz:100|c",
|
||||
out: event.Events{
|
||||
&event.CounterEvent{
|
||||
CMetricName: "foo",
|
||||
CValue: 100,
|
||||
CLabels: map[string]string{"tag1": "bar", "tag2": "baz"},
|
||||
},
|
||||
},
|
||||
}, {
|
||||
name: "SignalFx tag extension",
|
||||
in: "foo.[tag1=bar,tag2=baz]test:100|c",
|
||||
out: event.Events{
|
||||
&event.CounterEvent{
|
||||
CMetricName: "foo.test",
|
||||
CValue: 100,
|
||||
CLabels: map[string]string{"tag1": "bar", "tag2": "baz"},
|
||||
},
|
||||
},
|
||||
}, {
|
||||
name: "SignalFx tag extension, tags at end of name",
|
||||
in: "foo.test[tag1=bar,tag2=baz]:100|c",
|
||||
out: event.Events{
|
||||
&event.CounterEvent{
|
||||
CMetricName: "foo.test",
|
||||
CValue: 100,
|
||||
CLabels: map[string]string{"tag1": "bar", "tag2": "baz"},
|
||||
},
|
||||
},
|
||||
}, {
|
||||
name: "SignalFx tag extension, tags at beginning of name",
|
||||
in: "[tag1=bar,tag2=baz]foo.test:100|c",
|
||||
out: event.Events{
|
||||
&event.CounterEvent{
|
||||
CMetricName: "foo.test",
|
||||
CValue: 100,
|
||||
CLabels: map[string]string{"tag1": "bar", "tag2": "baz"},
|
||||
},
|
||||
},
|
||||
}, {
|
||||
name: "SignalFx tag extension, no tags",
|
||||
in: "foo.[]test:100|c",
|
||||
out: event.Events{
|
||||
&event.CounterEvent{
|
||||
CMetricName: "foo.test",
|
||||
CValue: 100,
|
||||
CLabels: map[string]string{},
|
||||
},
|
||||
},
|
||||
}, {
|
||||
name: "SignalFx tag extension, non-kv tags",
|
||||
in: "foo.[tag1,tag2]test:100|c",
|
||||
out: event.Events{
|
||||
&event.CounterEvent{
|
||||
CMetricName: "foo.test",
|
||||
CValue: 100,
|
||||
CLabels: map[string]string{},
|
||||
},
|
||||
},
|
||||
}, {
|
||||
name: "SignalFx tag extension, missing closing bracket",
|
||||
in: "[tag1=bar,tag2=bazfoo.test:100|c",
|
||||
out: event.Events{
|
||||
&event.CounterEvent{
|
||||
CMetricName: "[tag1=bar,tag2=bazfoo.test",
|
||||
CValue: 100,
|
||||
CLabels: map[string]string{},
|
||||
},
|
||||
},
|
||||
}, {
|
||||
name: "SignalFx tag extension, missing opening bracket",
|
||||
in: "tag1=bar,tag2=baz]foo.test:100|c",
|
||||
out: event.Events{
|
||||
&event.CounterEvent{
|
||||
CMetricName: "tag1=bar,tag2=baz]foo.test",
|
||||
CValue: 100,
|
||||
CLabels: map[string]string{},
|
||||
},
|
||||
},
|
||||
}, {
|
||||
name: "influxdb tag extension with tag keys unsupported by prometheus",
|
||||
in: "foo,09digits=0,tag.with.dots=1:100|c",
|
||||
out: event.Events{
|
||||
&event.CounterEvent{
|
||||
CMetricName: "foo",
|
||||
CValue: 100,
|
||||
CLabels: map[string]string{"_09digits": "0", "tag_with_dots": "1"},
|
||||
},
|
||||
},
|
||||
}, {
|
||||
name: "datadog tag extension",
|
||||
in: "foo:100|c|#tag1:bar,tag2:baz",
|
||||
out: event.Events{
|
||||
&event.CounterEvent{
|
||||
CMetricName: "foo",
|
||||
CValue: 100,
|
||||
CLabels: map[string]string{"tag1": "bar", "tag2": "baz"},
|
||||
},
|
||||
},
|
||||
}, {
|
||||
name: "datadog tag extension with # in all keys (as sent by datadog php client)",
|
||||
in: "foo:100|c|#tag1:bar,#tag2:baz",
|
||||
out: event.Events{
|
||||
&event.CounterEvent{
|
||||
CMetricName: "foo",
|
||||
CValue: 100,
|
||||
CLabels: map[string]string{"tag1": "bar", "tag2": "baz"},
|
||||
},
|
||||
},
|
||||
}, {
|
||||
name: "datadog tag extension with tag keys unsupported by prometheus",
|
||||
in: "foo:100|c|#09digits:0,tag.with.dots:1",
|
||||
out: event.Events{
|
||||
&event.CounterEvent{
|
||||
CMetricName: "foo",
|
||||
CValue: 100,
|
||||
CLabels: map[string]string{"_09digits": "0", "tag_with_dots": "1"},
|
||||
},
|
||||
},
|
||||
}, {
|
||||
name: "datadog tag extension with valueless tags: ignored",
|
||||
in: "foo:100|c|#tag_without_a_value",
|
||||
out: event.Events{
|
||||
&event.CounterEvent{
|
||||
CMetricName: "foo",
|
||||
CValue: 100,
|
||||
CLabels: map[string]string{},
|
||||
},
|
||||
},
|
||||
}, {
|
||||
name: "datadog tag extension with valueless tags (edge case)",
|
||||
in: "foo:100|c|#tag_without_a_value,tag:value",
|
||||
out: event.Events{
|
||||
&event.CounterEvent{
|
||||
CMetricName: "foo",
|
||||
CValue: 100,
|
||||
CLabels: map[string]string{"tag": "value"},
|
||||
},
|
||||
},
|
||||
}, {
|
||||
name: "datadog tag extension with empty tags (edge case)",
|
||||
in: "foo:100|c|#tag:value,,",
|
||||
out: event.Events{
|
||||
&event.CounterEvent{
|
||||
CMetricName: "foo",
|
||||
CValue: 100,
|
||||
CLabels: map[string]string{"tag": "value"},
|
||||
},
|
||||
},
|
||||
}, {
|
||||
name: "datadog tag extension with sampling",
|
||||
in: "foo:100|c|@0.1|#tag1:bar,#tag2:baz",
|
||||
out: event.Events{
|
||||
&event.CounterEvent{
|
||||
CMetricName: "foo",
|
||||
CValue: 1000,
|
||||
CLabels: map[string]string{"tag1": "bar", "tag2": "baz"},
|
||||
},
|
||||
},
|
||||
}, {
|
||||
name: "librato/dogstatsd mixed tag styles without sampling",
|
||||
in: "foo#tag1=foo,tag3=bing:100|c|#tag1:bar,#tag2:baz",
|
||||
out: event.Events{},
|
||||
}, {
|
||||
name: "signalfx/dogstatsd mixed tag styles without sampling",
|
||||
in: "foo[tag1=foo,tag3=bing]:100|c|#tag1:bar,#tag2:baz",
|
||||
out: event.Events{},
|
||||
}, {
|
||||
name: "influxdb/dogstatsd mixed tag styles without sampling",
|
||||
in: "foo,tag1=foo,tag3=bing:100|c|#tag1:bar,#tag2:baz",
|
||||
out: event.Events{},
|
||||
}, {
|
||||
name: "mixed tag styles with sampling",
|
||||
in: "foo#tag1=foo,tag3=bing:100|c|@0.1|#tag1:bar,#tag2:baz",
|
||||
out: event.Events{},
|
||||
}, {
|
||||
name: "histogram with sampling",
|
||||
in: "foo:0.01|h|@0.2|#tag1:bar,#tag2:baz",
|
||||
out: event.Events{
|
||||
&event.ObserverEvent{
|
||||
OMetricName: "foo",
|
||||
OValue: 0.01,
|
||||
OLabels: map[string]string{"tag1": "bar", "tag2": "baz"},
|
||||
},
|
||||
&event.ObserverEvent{
|
||||
OMetricName: "foo",
|
||||
OValue: 0.01,
|
||||
OLabels: map[string]string{"tag1": "bar", "tag2": "baz"},
|
||||
},
|
||||
&event.ObserverEvent{
|
||||
OMetricName: "foo",
|
||||
OValue: 0.01,
|
||||
OLabels: map[string]string{"tag1": "bar", "tag2": "baz"},
|
||||
},
|
||||
&event.ObserverEvent{
|
||||
OMetricName: "foo",
|
||||
OValue: 0.01,
|
||||
OLabels: map[string]string{"tag1": "bar", "tag2": "baz"},
|
||||
},
|
||||
&event.ObserverEvent{
|
||||
OMetricName: "foo",
|
||||
OValue: 0.01,
|
||||
OLabels: map[string]string{"tag1": "bar", "tag2": "baz"},
|
||||
},
|
||||
},
|
||||
}, {
|
||||
name: "datadog tag extension with multiple colons",
|
||||
in: "foo:100|c|@0.1|#tag1:foo:bar",
|
||||
out: event.Events{
|
||||
&event.CounterEvent{
|
||||
CMetricName: "foo",
|
||||
CValue: 1000,
|
||||
CLabels: map[string]string{"tag1": "foo:bar"},
|
||||
},
|
||||
},
|
||||
}, {
|
||||
name: "datadog tag extension with invalid utf8 tag values",
|
||||
in: "foo:100|c|@0.1|#tag:\xc3\x28invalid",
|
||||
}, {
|
||||
name: "datadog tag extension with both valid and invalid utf8 tag values",
|
||||
in: "foo:100|c|@0.1|#tag1:valid,tag2:\xc3\x28invalid",
|
||||
}, {
|
||||
name: "multiple metrics with invalid datadog utf8 tag values",
|
||||
in: "foo:200|c|#tag:value\nfoo:300|c|#tag:\xc3\x28invalid",
|
||||
out: event.Events{
|
||||
&event.CounterEvent{
|
||||
CMetricName: "foo",
|
||||
CValue: 200,
|
||||
CLabels: map[string]string{"tag": "value"},
|
||||
},
|
||||
},
|
||||
}, {
|
||||
name: "combined multiline metrics",
|
||||
in: "foo:200|ms:300|ms:5|c|@0.1:6|g\nbar:1|c:5|ms",
|
||||
out: Events{
|
||||
&TimerEvent{
|
||||
metricName: "foo",
|
||||
value: 200,
|
||||
out: event.Events{
|
||||
&event.ObserverEvent{
|
||||
OMetricName: "foo",
|
||||
OValue: .200,
|
||||
OLabels: map[string]string{},
|
||||
},
|
||||
&TimerEvent{
|
||||
metricName: "foo",
|
||||
value: 300,
|
||||
&event.ObserverEvent{
|
||||
OMetricName: "foo",
|
||||
OValue: .300,
|
||||
OLabels: map[string]string{},
|
||||
},
|
||||
&CounterEvent{
|
||||
metricName: "foo",
|
||||
value: 50,
|
||||
&event.CounterEvent{
|
||||
CMetricName: "foo",
|
||||
CValue: 50,
|
||||
CLabels: map[string]string{},
|
||||
},
|
||||
&GaugeEvent{
|
||||
metricName: "foo",
|
||||
value: 6,
|
||||
&event.GaugeEvent{
|
||||
GMetricName: "foo",
|
||||
GValue: 6,
|
||||
GLabels: map[string]string{},
|
||||
},
|
||||
&CounterEvent{
|
||||
metricName: "bar",
|
||||
value: 1,
|
||||
&event.CounterEvent{
|
||||
CMetricName: "bar",
|
||||
CValue: 1,
|
||||
CLabels: map[string]string{},
|
||||
},
|
||||
&TimerEvent{
|
||||
metricName: "bar",
|
||||
value: 5,
|
||||
&event.ObserverEvent{
|
||||
OMetricName: "bar",
|
||||
OValue: .005,
|
||||
OLabels: map[string]string{},
|
||||
},
|
||||
},
|
||||
}, {
|
||||
name: "timings with sampling factor",
|
||||
in: "foo.timing:0.5|ms|@0.1",
|
||||
out: event.Events{
|
||||
&event.ObserverEvent{OMetricName: "foo.timing", OValue: 0.0005, OLabels: map[string]string{}},
|
||||
&event.ObserverEvent{OMetricName: "foo.timing", OValue: 0.0005, OLabels: map[string]string{}},
|
||||
&event.ObserverEvent{OMetricName: "foo.timing", OValue: 0.0005, OLabels: map[string]string{}},
|
||||
&event.ObserverEvent{OMetricName: "foo.timing", OValue: 0.0005, OLabels: map[string]string{}},
|
||||
&event.ObserverEvent{OMetricName: "foo.timing", OValue: 0.0005, OLabels: map[string]string{}},
|
||||
&event.ObserverEvent{OMetricName: "foo.timing", OValue: 0.0005, OLabels: map[string]string{}},
|
||||
&event.ObserverEvent{OMetricName: "foo.timing", OValue: 0.0005, OLabels: map[string]string{}},
|
||||
&event.ObserverEvent{OMetricName: "foo.timing", OValue: 0.0005, OLabels: map[string]string{}},
|
||||
&event.ObserverEvent{OMetricName: "foo.timing", OValue: 0.0005, OLabels: map[string]string{}},
|
||||
&event.ObserverEvent{OMetricName: "foo.timing", OValue: 0.0005, OLabels: map[string]string{}},
|
||||
},
|
||||
}, {
|
||||
name: "bad line",
|
||||
in: "foo",
|
||||
|
@ -94,40 +507,359 @@ func TestHandlePacket(t *testing.T) {
|
|||
}, {
|
||||
name: "illegal sampling factor",
|
||||
in: "foo:1|c|@bar",
|
||||
out: event.Events{
|
||||
&event.CounterEvent{
|
||||
CMetricName: "foo",
|
||||
CValue: 1,
|
||||
CLabels: map[string]string{},
|
||||
},
|
||||
},
|
||||
}, {
|
||||
name: "zero sampling factor",
|
||||
in: "foo:2|c|@0",
|
||||
out: Events{
|
||||
&CounterEvent{
|
||||
metricName: "foo",
|
||||
value: 2,
|
||||
out: event.Events{
|
||||
&event.CounterEvent{
|
||||
CMetricName: "foo",
|
||||
CValue: 2,
|
||||
CLabels: map[string]string{},
|
||||
},
|
||||
},
|
||||
}, {
|
||||
name: "illegal stat type",
|
||||
in: "foo:2|t",
|
||||
},
|
||||
{
|
||||
name: "empty metric name",
|
||||
in: ":100|ms",
|
||||
},
|
||||
{
|
||||
name: "empty component",
|
||||
in: "foo:1|c|",
|
||||
},
|
||||
{
|
||||
name: "invalid utf8",
|
||||
in: "invalid\xc3\x28utf8:1|c",
|
||||
},
|
||||
{
|
||||
name: "some invalid utf8",
|
||||
in: "valid_utf8:1|c\ninvalid\xc3\x28utf8:1|c",
|
||||
out: event.Events{
|
||||
&event.CounterEvent{
|
||||
CMetricName: "valid_utf8",
|
||||
CValue: 1,
|
||||
CLabels: map[string]string{},
|
||||
},
|
||||
},
|
||||
}, {
|
||||
name: "ms timer with conversion to seconds",
|
||||
in: "foo:200|ms",
|
||||
out: event.Events{
|
||||
&event.ObserverEvent{
|
||||
OMetricName: "foo",
|
||||
OValue: 0.2,
|
||||
OLabels: map[string]string{},
|
||||
},
|
||||
},
|
||||
}, {
|
||||
name: "histogram with no unit conversion",
|
||||
in: "foo:200|h",
|
||||
out: event.Events{
|
||||
&event.ObserverEvent{
|
||||
OMetricName: "foo",
|
||||
OValue: 200,
|
||||
OLabels: map[string]string{},
|
||||
},
|
||||
},
|
||||
}, {
|
||||
name: "distribution with no unit conversion",
|
||||
in: "foo:200|d",
|
||||
out: event.Events{
|
||||
&event.ObserverEvent{
|
||||
OMetricName: "foo",
|
||||
OValue: 200,
|
||||
OLabels: map[string]string{},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
l := StatsDListener{}
|
||||
events := make(chan Events, 32)
|
||||
for i, scenario := range scenarios {
|
||||
l.handlePacket([]byte(scenario.in), events)
|
||||
parser := line.NewParser()
|
||||
parser.EnableDogstatsdParsing()
|
||||
parser.EnableInfluxdbParsing()
|
||||
parser.EnableLibratoParsing()
|
||||
parser.EnableSignalFXParsing()
|
||||
|
||||
// Flatten actual events.
|
||||
actual := Events{}
|
||||
for i := 0; i < len(events); i++ {
|
||||
actual = append(actual, <-events...)
|
||||
}
|
||||
for k, l := range []statsDPacketHandler{&listener.StatsDUDPListener{
|
||||
Conn: nil,
|
||||
EventHandler: nil,
|
||||
Logger: log.NewNopLogger(),
|
||||
LineParser: parser,
|
||||
UDPPackets: udpPackets,
|
||||
UDPPacketDrops: udpPacketDrops,
|
||||
LinesReceived: linesReceived,
|
||||
EventsFlushed: eventsFlushed,
|
||||
SampleErrors: *sampleErrors,
|
||||
SamplesReceived: samplesReceived,
|
||||
TagErrors: tagErrors,
|
||||
TagsReceived: tagsReceived,
|
||||
}, &mockStatsDTCPListener{listener.StatsDTCPListener{
|
||||
Conn: nil,
|
||||
EventHandler: nil,
|
||||
Logger: log.NewNopLogger(),
|
||||
LineParser: parser,
|
||||
LinesReceived: linesReceived,
|
||||
EventsFlushed: eventsFlushed,
|
||||
SampleErrors: *sampleErrors,
|
||||
SamplesReceived: samplesReceived,
|
||||
TagErrors: tagErrors,
|
||||
TagsReceived: tagsReceived,
|
||||
TCPConnections: tcpConnections,
|
||||
TCPErrors: tcpErrors,
|
||||
TCPLineTooLong: tcpLineTooLong,
|
||||
}, log.NewNopLogger()}} {
|
||||
events := make(chan event.Events, 32)
|
||||
l.SetEventHandler(&event.UnbufferedEventHandler{C: events})
|
||||
for i, scenario := range scenarios {
|
||||
l.HandlePacket([]byte(scenario.in))
|
||||
|
||||
if len(actual) != len(scenario.out) {
|
||||
t.Fatalf("%d. Expected %d events, got %d", i, len(scenario.out), len(actual))
|
||||
}
|
||||
le := len(events)
|
||||
// Flatten actual events.
|
||||
actual := event.Events{}
|
||||
for j := 0; j < le; j++ {
|
||||
actual = append(actual, <-events...)
|
||||
}
|
||||
|
||||
for j, expected := range scenario.out {
|
||||
if fmt.Sprintf("%v", actual[j]) != fmt.Sprintf("%v", expected) {
|
||||
t.Fatalf("%d.%d. Expected %v, got %v", i, j, actual[j], expected)
|
||||
if len(actual) != len(scenario.out) {
|
||||
t.Fatalf("%d.%d. Expected %d events, got %d in scenario '%s'", k, i, len(scenario.out), len(actual), scenario.name)
|
||||
}
|
||||
|
||||
for j, expected := range scenario.out {
|
||||
if !reflect.DeepEqual(&expected, &actual[j]) {
|
||||
t.Fatalf("%d.%d.%d. Expected %#v, got %#v in scenario '%s'", k, i, j, expected, actual[j], scenario.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type statsDPacketHandler interface {
|
||||
HandlePacket(packet []byte)
|
||||
SetEventHandler(eh event.EventHandler)
|
||||
}
|
||||
|
||||
type mockStatsDTCPListener struct {
|
||||
listener.StatsDTCPListener
|
||||
log.Logger
|
||||
}
|
||||
|
||||
func (ml *mockStatsDTCPListener) HandlePacket(packet []byte) {
|
||||
// Forcing IPv4 because the TravisCI build environment does not have IPv6
|
||||
// addresses.
|
||||
lc, err := net.ListenTCP("tcp4", nil)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("mockStatsDTCPListener: listen failed: %v", err))
|
||||
}
|
||||
|
||||
defer lc.Close()
|
||||
|
||||
go func() {
|
||||
cc, err := net.DialTCP("tcp", nil, lc.Addr().(*net.TCPAddr))
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("mockStatsDTCPListener: dial failed: %v", err))
|
||||
}
|
||||
|
||||
defer cc.Close()
|
||||
|
||||
n, err := cc.Write(packet)
|
||||
if err != nil || n != len(packet) {
|
||||
panic(fmt.Sprintf("mockStatsDTCPListener: write failed: %v,%d", err, n))
|
||||
}
|
||||
}()
|
||||
|
||||
sc, err := lc.AcceptTCP()
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("mockStatsDTCPListener: accept failed: %v", err))
|
||||
}
|
||||
ml.HandleConn(sc)
|
||||
}
|
||||
|
||||
// TestTtlExpiration validates expiration of time series.
|
||||
// foobar metric without mapping should expire with default ttl of 1s
|
||||
// bazqux metric should expire with ttl of 2s
|
||||
func TestTtlExpiration(t *testing.T) {
|
||||
// Mock a time.NewTicker
|
||||
tickerCh := make(chan time.Time)
|
||||
clock.ClockInstance = &clock.Clock{
|
||||
TickerCh: tickerCh,
|
||||
}
|
||||
|
||||
config := `
|
||||
defaults:
|
||||
ttl: 1s
|
||||
mappings:
|
||||
- match: bazqux.*
|
||||
name: bazqux
|
||||
ttl: 2s
|
||||
`
|
||||
// Create mapper from config and start an Exporter with a synchronous channel
|
||||
testMapper := &mapper.MetricMapper{}
|
||||
err := testMapper.InitFromYAMLString(config)
|
||||
if err != nil {
|
||||
t.Fatalf("Config load error: %s %s", config, err)
|
||||
}
|
||||
events := make(chan event.Events)
|
||||
defer close(events)
|
||||
go func() {
|
||||
ex := exporter.NewExporter(prometheus.DefaultRegisterer, testMapper, log.NewNopLogger(), eventsActions, eventsUnmapped, errorEventStats, eventStats, conflictingEventStats, metricsCount)
|
||||
ex.Listen(events)
|
||||
}()
|
||||
|
||||
ev := event.Events{
|
||||
// event with default ttl = 1s
|
||||
&event.GaugeEvent{
|
||||
GMetricName: "foobar",
|
||||
GValue: 200,
|
||||
},
|
||||
// event with ttl = 2s from a mapping
|
||||
&event.ObserverEvent{
|
||||
OMetricName: "bazqux.main",
|
||||
OValue: 42,
|
||||
},
|
||||
}
|
||||
|
||||
var metrics []*dto.MetricFamily
|
||||
var foobarValue *float64
|
||||
var bazquxValue *float64
|
||||
|
||||
// Step 1. Send events with statsd metrics.
|
||||
// Send empty Events to wait for events are handled.
|
||||
// saveLabelValues will use fake instant as a lastRegisteredAt time.
|
||||
clock.ClockInstance.Instant = time.Unix(0, 0)
|
||||
events <- ev
|
||||
events <- event.Events{}
|
||||
|
||||
// Check values
|
||||
metrics, err = prometheus.DefaultGatherer.Gather()
|
||||
if err != nil {
|
||||
t.Fatal("Gather should not fail")
|
||||
}
|
||||
foobarValue = getFloat64(metrics, "foobar", prometheus.Labels{})
|
||||
bazquxValue = getFloat64(metrics, "bazqux", prometheus.Labels{})
|
||||
if foobarValue == nil || bazquxValue == nil {
|
||||
t.Fatalf("Gauge `foobar` and Summary `bazqux` should be gathered")
|
||||
}
|
||||
if *foobarValue != 200 {
|
||||
t.Fatalf("Gauge `foobar` observation %f is not expected. Should be 200", *foobarValue)
|
||||
}
|
||||
if *bazquxValue != 42 {
|
||||
t.Fatalf("Summary `bazqux` observation %f is not expected. Should be 42", *bazquxValue)
|
||||
}
|
||||
|
||||
// Step 2. Increase Instant to emulate metrics expiration after 1s
|
||||
clock.ClockInstance.Instant = time.Unix(1, 10)
|
||||
clock.ClockInstance.TickerCh <- time.Unix(0, 0)
|
||||
events <- event.Events{}
|
||||
|
||||
// Check values
|
||||
metrics, err = prometheus.DefaultGatherer.Gather()
|
||||
if err != nil {
|
||||
t.Fatal("Gather should not fail")
|
||||
}
|
||||
foobarValue = getFloat64(metrics, "foobar", prometheus.Labels{})
|
||||
bazquxValue = getFloat64(metrics, "bazqux", prometheus.Labels{})
|
||||
if foobarValue != nil {
|
||||
t.Fatalf("Gauge `foobar` should be expired")
|
||||
}
|
||||
if bazquxValue == nil {
|
||||
t.Fatalf("Summary `bazqux` should be gathered")
|
||||
}
|
||||
if *bazquxValue != 42 {
|
||||
t.Fatalf("Summary `bazqux` observation %f is not expected. Should be 42", *bazquxValue)
|
||||
}
|
||||
|
||||
// Step 3. Increase Instant to emulate metrics expiration after 2s
|
||||
clock.ClockInstance.Instant = time.Unix(2, 200)
|
||||
clock.ClockInstance.TickerCh <- time.Unix(0, 0)
|
||||
events <- event.Events{}
|
||||
|
||||
// Check values
|
||||
metrics, err = prometheus.DefaultGatherer.Gather()
|
||||
if err != nil {
|
||||
t.Fatal("Gather should not fail")
|
||||
}
|
||||
foobarValue = getFloat64(metrics, "foobar", prometheus.Labels{})
|
||||
bazquxValue = getFloat64(metrics, "bazqux", prometheus.Labels{})
|
||||
if bazquxValue != nil {
|
||||
t.Fatalf("Summary `bazqux` should be expired")
|
||||
}
|
||||
if foobarValue != nil {
|
||||
t.Fatalf("Gauge `foobar` should not be gathered after expiration")
|
||||
}
|
||||
}
|
||||
|
||||
// getFloat64 search for metric by name in array of MetricFamily and then search a value by labels.
|
||||
// Method returns a value or nil if metric is not found.
|
||||
func getFloat64(metrics []*dto.MetricFamily, name string, labels prometheus.Labels) *float64 {
|
||||
var metricFamily *dto.MetricFamily
|
||||
for _, m := range metrics {
|
||||
if *m.Name == name {
|
||||
metricFamily = m
|
||||
break
|
||||
}
|
||||
}
|
||||
if metricFamily == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var metric *dto.Metric
|
||||
labelStr := fmt.Sprintf("%v", labels)
|
||||
for _, m := range metricFamily.Metric {
|
||||
l := labelPairsAsLabels(m.GetLabel())
|
||||
ls := fmt.Sprintf("%v", l)
|
||||
if labelStr == ls {
|
||||
metric = m
|
||||
break
|
||||
}
|
||||
}
|
||||
if metric == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var value float64
|
||||
if metric.Gauge != nil {
|
||||
value = metric.Gauge.GetValue()
|
||||
return &value
|
||||
}
|
||||
if metric.Counter != nil {
|
||||
value = metric.Counter.GetValue()
|
||||
return &value
|
||||
}
|
||||
if metric.Histogram != nil {
|
||||
value = metric.Histogram.GetSampleSum()
|
||||
return &value
|
||||
}
|
||||
if metric.Summary != nil {
|
||||
value = metric.Summary.GetSampleSum()
|
||||
return &value
|
||||
}
|
||||
if metric.Untyped != nil {
|
||||
value = metric.Untyped.GetValue()
|
||||
return &value
|
||||
}
|
||||
panic(fmt.Errorf("collected a non-gauge/counter/histogram/summary/untyped metric: %s", metric))
|
||||
}
|
||||
|
||||
func labelPairsAsLabels(pairs []*dto.LabelPair) (labels prometheus.Labels) {
|
||||
labels = prometheus.Labels{}
|
||||
for _, pair := range pairs {
|
||||
if pair.Name == nil {
|
||||
continue
|
||||
}
|
||||
value := ""
|
||||
if pair.Value != nil {
|
||||
value = *pair.Value
|
||||
}
|
||||
labels[*pair.Name] = value
|
||||
}
|
||||
return
|
||||
}
|
||||
|
|
364
exporter.go
364
exporter.go
|
@ -1,364 +0,0 @@
|
|||
// Copyright 2013 The Prometheus Authors
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"hash/fnv"
|
||||
"net"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/common/model"
|
||||
"github.com/prometheus/log"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultHelp = "Metric autogenerated by statsd_exporter."
|
||||
regErrF = "A change of configuration created inconsistent metrics for " +
|
||||
"%q. You have to restart the statsd_exporter, and you should " +
|
||||
"consider the effects on your monitoring setup. Error: %s"
|
||||
)
|
||||
|
||||
var (
|
||||
illegalCharsRE = regexp.MustCompile(`[^a-zA-Z0-9_]`)
|
||||
|
||||
hash = fnv.New64a()
|
||||
strBuf bytes.Buffer // Used for hashing.
|
||||
intBuf = make([]byte, 8)
|
||||
)
|
||||
|
||||
// hashNameAndLabels returns a hash value of the provided name string and all
|
||||
// the label names and values in the provided labels map.
|
||||
//
|
||||
// Not safe for concurrent use! (Uses a shared buffer and hasher to save on
|
||||
// allocations.)
|
||||
func hashNameAndLabels(name string, labels prometheus.Labels) uint64 {
|
||||
hash.Reset()
|
||||
strBuf.Reset()
|
||||
strBuf.WriteString(name)
|
||||
hash.Write(strBuf.Bytes())
|
||||
binary.BigEndian.PutUint64(intBuf, model.LabelsToSignature(labels))
|
||||
hash.Write(intBuf)
|
||||
return hash.Sum64()
|
||||
}
|
||||
|
||||
type CounterContainer struct {
|
||||
Elements map[uint64]prometheus.Counter
|
||||
}
|
||||
|
||||
func NewCounterContainer() *CounterContainer {
|
||||
return &CounterContainer{
|
||||
Elements: make(map[uint64]prometheus.Counter),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *CounterContainer) Get(metricName string, labels prometheus.Labels) prometheus.Counter {
|
||||
hash := hashNameAndLabels(metricName, labels)
|
||||
counter, ok := c.Elements[hash]
|
||||
if !ok {
|
||||
counter = prometheus.NewCounter(prometheus.CounterOpts{
|
||||
Name: metricName,
|
||||
Help: defaultHelp,
|
||||
ConstLabels: labels,
|
||||
})
|
||||
c.Elements[hash] = counter
|
||||
if err := prometheus.Register(counter); err != nil {
|
||||
log.Fatalf(regErrF, metricName, err)
|
||||
}
|
||||
}
|
||||
return counter
|
||||
}
|
||||
|
||||
type GaugeContainer struct {
|
||||
Elements map[uint64]prometheus.Gauge
|
||||
}
|
||||
|
||||
func NewGaugeContainer() *GaugeContainer {
|
||||
return &GaugeContainer{
|
||||
Elements: make(map[uint64]prometheus.Gauge),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *GaugeContainer) Get(metricName string, labels prometheus.Labels) prometheus.Gauge {
|
||||
hash := hashNameAndLabels(metricName, labels)
|
||||
gauge, ok := c.Elements[hash]
|
||||
if !ok {
|
||||
gauge = prometheus.NewGauge(prometheus.GaugeOpts{
|
||||
Name: metricName,
|
||||
Help: defaultHelp,
|
||||
ConstLabels: labels,
|
||||
})
|
||||
c.Elements[hash] = gauge
|
||||
if err := prometheus.Register(gauge); err != nil {
|
||||
log.Fatalf(regErrF, metricName, err)
|
||||
}
|
||||
}
|
||||
return gauge
|
||||
}
|
||||
|
||||
type SummaryContainer struct {
|
||||
Elements map[uint64]prometheus.Summary
|
||||
}
|
||||
|
||||
func NewSummaryContainer() *SummaryContainer {
|
||||
return &SummaryContainer{
|
||||
Elements: make(map[uint64]prometheus.Summary),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *SummaryContainer) Get(metricName string, labels prometheus.Labels) prometheus.Summary {
|
||||
hash := hashNameAndLabels(metricName, labels)
|
||||
summary, ok := c.Elements[hash]
|
||||
if !ok {
|
||||
summary = prometheus.NewSummary(
|
||||
prometheus.SummaryOpts{
|
||||
Name: metricName,
|
||||
Help: defaultHelp,
|
||||
ConstLabels: labels,
|
||||
})
|
||||
c.Elements[hash] = summary
|
||||
if err := prometheus.Register(summary); err != nil {
|
||||
log.Fatalf(regErrF, metricName, err)
|
||||
}
|
||||
}
|
||||
return summary
|
||||
}
|
||||
|
||||
type Event interface {
|
||||
MetricName() string
|
||||
Value() float64
|
||||
}
|
||||
|
||||
type CounterEvent struct {
|
||||
metricName string
|
||||
value float64
|
||||
}
|
||||
|
||||
func (c *CounterEvent) MetricName() string { return c.metricName }
|
||||
func (c *CounterEvent) Value() float64 { return c.value }
|
||||
|
||||
type GaugeEvent struct {
|
||||
metricName string
|
||||
value float64
|
||||
}
|
||||
|
||||
func (g *GaugeEvent) MetricName() string { return g.metricName }
|
||||
func (g *GaugeEvent) Value() float64 { return g.value }
|
||||
|
||||
type TimerEvent struct {
|
||||
metricName string
|
||||
value float64
|
||||
}
|
||||
|
||||
func (t *TimerEvent) MetricName() string { return t.metricName }
|
||||
func (t *TimerEvent) Value() float64 { return t.value }
|
||||
|
||||
type Events []Event
|
||||
|
||||
type Exporter struct {
|
||||
Counters *CounterContainer
|
||||
Gauges *GaugeContainer
|
||||
Summaries *SummaryContainer
|
||||
mapper *metricMapper
|
||||
}
|
||||
|
||||
func escapeMetricName(metricName string) string {
|
||||
// If a metric starts with a digit, prepend an underscore.
|
||||
if metricName[0] >= '0' && metricName[0] <= '9' {
|
||||
metricName = "_" + metricName
|
||||
}
|
||||
|
||||
// Replace all illegal metric chars with underscores.
|
||||
metricName = illegalCharsRE.ReplaceAllString(metricName, "_")
|
||||
return metricName
|
||||
}
|
||||
|
||||
func (b *Exporter) Listen(e <-chan Events) {
|
||||
for {
|
||||
events := <-e
|
||||
for _, event := range events {
|
||||
metricName := ""
|
||||
prometheusLabels := prometheus.Labels{}
|
||||
|
||||
labels, present := b.mapper.getMapping(event.MetricName())
|
||||
if present {
|
||||
metricName = labels["name"]
|
||||
for label, value := range labels {
|
||||
if label != "name" {
|
||||
prometheusLabels[label] = value
|
||||
}
|
||||
}
|
||||
} else {
|
||||
metricName = escapeMetricName(event.MetricName())
|
||||
}
|
||||
|
||||
switch event.(type) {
|
||||
case *CounterEvent:
|
||||
counter := b.Counters.Get(
|
||||
metricName+"_counter",
|
||||
prometheusLabels,
|
||||
)
|
||||
counter.Add(event.Value())
|
||||
|
||||
eventStats.WithLabelValues("counter").Inc()
|
||||
|
||||
case *GaugeEvent:
|
||||
gauge := b.Gauges.Get(
|
||||
metricName+"_gauge",
|
||||
prometheusLabels,
|
||||
)
|
||||
gauge.Set(event.Value())
|
||||
|
||||
eventStats.WithLabelValues("gauge").Inc()
|
||||
|
||||
case *TimerEvent:
|
||||
summary := b.Summaries.Get(
|
||||
metricName+"_timer",
|
||||
prometheusLabels,
|
||||
)
|
||||
summary.Observe(event.Value())
|
||||
|
||||
eventStats.WithLabelValues("timer").Inc()
|
||||
|
||||
default:
|
||||
log.Println("Unsupported event type")
|
||||
eventStats.WithLabelValues("illegal").Inc()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func NewExporter(mapper *metricMapper) *Exporter {
|
||||
return &Exporter{
|
||||
Counters: NewCounterContainer(),
|
||||
Gauges: NewGaugeContainer(),
|
||||
Summaries: NewSummaryContainer(),
|
||||
mapper: mapper,
|
||||
}
|
||||
}
|
||||
|
||||
type StatsDListener struct {
|
||||
conn *net.UDPConn
|
||||
}
|
||||
|
||||
func buildEvent(statType, metric string, value float64) (Event, error) {
|
||||
switch statType {
|
||||
case "c":
|
||||
return &CounterEvent{
|
||||
metricName: metric,
|
||||
value: float64(value),
|
||||
}, nil
|
||||
case "g":
|
||||
return &GaugeEvent{
|
||||
metricName: metric,
|
||||
value: float64(value),
|
||||
}, nil
|
||||
case "ms":
|
||||
return &TimerEvent{
|
||||
metricName: metric,
|
||||
value: float64(value),
|
||||
}, nil
|
||||
case "s":
|
||||
return nil, fmt.Errorf("No support for StatsD sets")
|
||||
default:
|
||||
return nil, fmt.Errorf("Bad stat type %s", statType)
|
||||
}
|
||||
}
|
||||
|
||||
func (l *StatsDListener) Listen(e chan<- Events) {
|
||||
// TODO: evaluate proper size according to MTU
|
||||
var buf [512]byte
|
||||
for {
|
||||
n, _, err := l.conn.ReadFromUDP(buf[0:])
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
l.handlePacket(buf[0:n], e)
|
||||
}
|
||||
}
|
||||
|
||||
func (l *StatsDListener) handlePacket(packet []byte, e chan<- Events) {
|
||||
lines := strings.Split(string(packet), "\n")
|
||||
events := Events{}
|
||||
for _, line := range lines {
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
elements := strings.Split(line, ":")
|
||||
if len(elements) < 2 {
|
||||
networkStats.WithLabelValues("malformed_line").Inc()
|
||||
log.Println("Bad line from StatsD:", line)
|
||||
continue
|
||||
}
|
||||
metric := elements[0]
|
||||
samples := elements[1:]
|
||||
for _, sample := range samples {
|
||||
components := strings.Split(sample, "|")
|
||||
samplingFactor := 1.0
|
||||
if len(components) < 2 || len(components) > 3 {
|
||||
networkStats.WithLabelValues("malformed_component").Inc()
|
||||
log.Println("Bad component on line:", line)
|
||||
continue
|
||||
}
|
||||
valueStr, statType := components[0], components[1]
|
||||
value, err := strconv.ParseFloat(valueStr, 64)
|
||||
if err != nil {
|
||||
log.Printf("Bad value %s on line: %s", valueStr, line)
|
||||
networkStats.WithLabelValues("malformed_value").Inc()
|
||||
continue
|
||||
}
|
||||
|
||||
if len(components) == 3 {
|
||||
if statType != "c" {
|
||||
log.Println("Illegal sampling factor for non-counter metric on line", line)
|
||||
networkStats.WithLabelValues("illegal_sample_factor").Inc()
|
||||
}
|
||||
samplingStr := components[2]
|
||||
if samplingStr[0] != '@' {
|
||||
log.Printf("Invalid sampling factor %s on line %s", samplingStr, line)
|
||||
networkStats.WithLabelValues("invalid_sample_factor").Inc()
|
||||
continue
|
||||
}
|
||||
samplingFactor, err = strconv.ParseFloat(samplingStr[1:], 64)
|
||||
if err != nil {
|
||||
log.Printf("Invalid sampling factor %s on line %s", samplingStr, line)
|
||||
networkStats.WithLabelValues("invalid_sample_factor").Inc()
|
||||
continue
|
||||
}
|
||||
if samplingFactor == 0 {
|
||||
// This should never happen, but avoid division by zero if it does.
|
||||
log.Printf("Invalid zero sampling factor %s on line %s, setting to 1", samplingStr, line)
|
||||
samplingFactor = 1
|
||||
}
|
||||
value /= samplingFactor
|
||||
}
|
||||
|
||||
event, err := buildEvent(statType, metric, value)
|
||||
if err != nil {
|
||||
log.Printf("Error building event on line %s: %s", line, err)
|
||||
networkStats.WithLabelValues("illegal_event").Inc()
|
||||
continue
|
||||
}
|
||||
events = append(events, event)
|
||||
networkStats.WithLabelValues("legal").Inc()
|
||||
}
|
||||
}
|
||||
e <- events
|
||||
}
|
201
exporter_benchmark_test.go
Normal file
201
exporter_benchmark_test.go
Normal file
|
@ -0,0 +1,201 @@
|
|||
// Copyright 2013 The Prometheus Authors
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/go-kit/log"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
|
||||
"github.com/prometheus/statsd_exporter/pkg/event"
|
||||
"github.com/prometheus/statsd_exporter/pkg/exporter"
|
||||
"github.com/prometheus/statsd_exporter/pkg/line"
|
||||
"github.com/prometheus/statsd_exporter/pkg/listener"
|
||||
"github.com/prometheus/statsd_exporter/pkg/mapper"
|
||||
)
|
||||
|
||||
func benchmarkUDPListener(times int, b *testing.B) {
|
||||
input := []string{
|
||||
"foo1:2|c",
|
||||
"foo2:3|g",
|
||||
"foo3:200|ms",
|
||||
"foo4:100|c|#tag1:bar,tag2:baz",
|
||||
"foo5:100|c|#tag1:bar,#tag2:baz",
|
||||
"foo6:100|c|#09digits:0,tag.with.dots:1",
|
||||
"foo10:100|c|@0.1|#tag1:bar,#tag2:baz",
|
||||
"foo11:100|c|@0.1|#tag1:foo:bar",
|
||||
"foo.[foo=bar,dim=val]test:1|g",
|
||||
"foo15:200|ms:300|ms:5|c|@0.1:6|g\nfoo15a:1|c:5|ms",
|
||||
"some_very_useful_metrics_with_quite_a_log_name:13|c",
|
||||
}
|
||||
bytesInput := make([]string, len(input)*times)
|
||||
logger := log.NewNopLogger()
|
||||
for run := 0; run < times; run++ {
|
||||
for i := 0; i < len(input); i++ {
|
||||
bytesInput[run*len(input)+i] = fmt.Sprintf("run%d%s", run, input[i])
|
||||
}
|
||||
}
|
||||
|
||||
parser := line.NewParser()
|
||||
parser.EnableDogstatsdParsing()
|
||||
parser.EnableInfluxdbParsing()
|
||||
parser.EnableLibratoParsing()
|
||||
parser.EnableSignalFXParsing()
|
||||
|
||||
// reset benchmark timer to not measure startup costs
|
||||
b.ResetTimer()
|
||||
|
||||
for n := 0; n < b.N; n++ {
|
||||
// pause benchmark timer for creating the chan and listener
|
||||
b.StopTimer()
|
||||
|
||||
// there are more events than input lines, need bigger buffer
|
||||
events := make(chan event.Events, len(bytesInput)*times*2)
|
||||
udpChan := make(chan []byte, len(bytesInput)*times*2)
|
||||
|
||||
l := listener.StatsDUDPListener{
|
||||
EventHandler: &event.UnbufferedEventHandler{C: events},
|
||||
Logger: logger,
|
||||
LineParser: parser,
|
||||
UDPPackets: udpPackets,
|
||||
LinesReceived: linesReceived,
|
||||
SamplesReceived: samplesReceived,
|
||||
TagsReceived: tagsReceived,
|
||||
UdpPacketQueue: udpChan,
|
||||
}
|
||||
|
||||
// resume benchmark timer
|
||||
b.StartTimer()
|
||||
|
||||
for i := 0; i < times; i++ {
|
||||
for _, line := range bytesInput {
|
||||
l.HandlePacket([]byte(line))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkUDPListener1(b *testing.B) {
|
||||
benchmarkUDPListener(1, b)
|
||||
}
|
||||
func BenchmarkUDPListener5(b *testing.B) {
|
||||
benchmarkUDPListener(5, b)
|
||||
}
|
||||
func BenchmarkUDPListener50(b *testing.B) {
|
||||
benchmarkUDPListener(50, b)
|
||||
}
|
||||
|
||||
func BenchmarkExporterListener(b *testing.B) {
|
||||
events := event.Events{
|
||||
&event.CounterEvent{ // simple counter
|
||||
CMetricName: "counter",
|
||||
CValue: 2,
|
||||
},
|
||||
&event.GaugeEvent{ // simple gauge
|
||||
GMetricName: "gauge",
|
||||
GValue: 10,
|
||||
},
|
||||
&event.ObserverEvent{ // simple timer
|
||||
OMetricName: "timer",
|
||||
OValue: 200,
|
||||
},
|
||||
&event.ObserverEvent{ // simple histogram
|
||||
OMetricName: "histogram.test",
|
||||
OValue: 200,
|
||||
},
|
||||
&event.CounterEvent{ // simple_tags
|
||||
CMetricName: "simple_tags",
|
||||
CValue: 100,
|
||||
CLabels: map[string]string{
|
||||
"alpha": "bar",
|
||||
"bravo": "baz",
|
||||
},
|
||||
},
|
||||
&event.CounterEvent{ // slightly different tags
|
||||
CMetricName: "simple_tags",
|
||||
CValue: 100,
|
||||
CLabels: map[string]string{
|
||||
"alpha": "bar",
|
||||
"charlie": "baz",
|
||||
},
|
||||
},
|
||||
&event.CounterEvent{ // and even more different tags
|
||||
CMetricName: "simple_tags",
|
||||
CValue: 100,
|
||||
CLabels: map[string]string{
|
||||
"alpha": "bar",
|
||||
"bravo": "baz",
|
||||
"golf": "looooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong",
|
||||
},
|
||||
},
|
||||
&event.CounterEvent{ // datadog tag extension with complex tags
|
||||
CMetricName: "foo",
|
||||
CValue: 100,
|
||||
CLabels: map[string]string{
|
||||
"action": "test",
|
||||
"application": "testapp",
|
||||
"application_component": "testcomp",
|
||||
"application_role": "test_role",
|
||||
"category": "category",
|
||||
"controller": "controller",
|
||||
"deployed_to": "production",
|
||||
"kube_deployment": "deploy",
|
||||
"kube_namespace": "kube-production",
|
||||
"method": "get",
|
||||
"version": "5.2.8374",
|
||||
"status": "200",
|
||||
"status_range": "2xx",
|
||||
},
|
||||
},
|
||||
}
|
||||
config := `
|
||||
mappings:
|
||||
- match: histogram.test
|
||||
timer_type: histogram
|
||||
name: "histogram_test"
|
||||
`
|
||||
|
||||
testMapper := &mapper.MetricMapper{}
|
||||
err := testMapper.InitFromYAMLString(config)
|
||||
if err != nil {
|
||||
b.Fatalf("Config load error: %s %s", config, err)
|
||||
}
|
||||
|
||||
ex := exporter.NewExporter(prometheus.DefaultRegisterer, testMapper, log.NewNopLogger(), eventsActions, eventsUnmapped, errorEventStats, eventStats, conflictingEventStats, metricsCount)
|
||||
|
||||
// reset benchmark timer to not measure startup costs
|
||||
b.ResetTimer()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
// pause benchmark timer for creating the chan
|
||||
b.StopTimer()
|
||||
|
||||
ec := make(chan event.Events, 1000)
|
||||
|
||||
// resume benchmark timer
|
||||
b.StartTimer()
|
||||
|
||||
go func() {
|
||||
for i := 0; i < 1000; i++ {
|
||||
ec <- events
|
||||
}
|
||||
close(ec)
|
||||
}()
|
||||
|
||||
ex.Listen(ec)
|
||||
}
|
||||
}
|
36
go.mod
Normal file
36
go.mod
Normal file
|
@ -0,0 +1,36 @@
|
|||
module github.com/prometheus/statsd_exporter
|
||||
|
||||
go 1.20
|
||||
|
||||
require (
|
||||
github.com/alecthomas/kingpin/v2 v2.4.0
|
||||
github.com/go-kit/log v0.2.1
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da
|
||||
github.com/prometheus/client_golang v1.19.0
|
||||
github.com/prometheus/client_model v0.6.0
|
||||
github.com/prometheus/common v0.48.0
|
||||
github.com/prometheus/exporter-toolkit v0.11.0
|
||||
github.com/stvp/go-udp-testing v0.0.0-20201019212854-469649b16807
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||
github.com/coreos/go-systemd/v22 v22.5.0 // indirect
|
||||
github.com/go-logfmt/logfmt v0.6.0 // indirect
|
||||
github.com/golang/protobuf v1.5.3 // indirect
|
||||
github.com/jpillora/backoff v1.0.0 // indirect
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f // indirect
|
||||
github.com/prometheus/procfs v0.12.0 // indirect
|
||||
github.com/xhit/go-str2duration/v2 v2.1.0 // indirect
|
||||
golang.org/x/crypto v0.18.0 // indirect
|
||||
golang.org/x/net v0.20.0 // indirect
|
||||
golang.org/x/oauth2 v0.16.0 // indirect
|
||||
golang.org/x/sync v0.5.0 // indirect
|
||||
golang.org/x/sys v0.16.0 // indirect
|
||||
golang.org/x/text v0.14.0 // indirect
|
||||
google.golang.org/appengine v1.6.7 // indirect
|
||||
google.golang.org/protobuf v1.33.0 // indirect
|
||||
)
|
82
go.sum
Normal file
82
go.sum
Normal file
|
@ -0,0 +1,82 @@
|
|||
github.com/alecthomas/kingpin/v2 v2.4.0 h1:f48lwail6p8zpO1bC4TxtqACaGqHYA22qkHjHpqDjYY=
|
||||
github.com/alecthomas/kingpin/v2 v2.4.0/go.mod h1:0gyi0zQnjuFk8xrkNKamJoyUo382HRL7ATRpFZCw6tE=
|
||||
github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 h1:s6gZFSlWYmbqAuRjVTiNNhvNRfY2Wxp9nhfyel4rklc=
|
||||
github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
||||
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/go-kit/log v0.2.1 h1:MRVx0/zhvdseW+Gza6N9rVzU/IVzaeE1SFI4raAhmBU=
|
||||
github.com/go-kit/log v0.2.1/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0=
|
||||
github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4=
|
||||
github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
||||
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA=
|
||||
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+SVif2QVs3tOP0zanoHgBEVAwHxUSIzRqU=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_golang v1.19.0 h1:ygXvpU1AoN1MhdzckN+PyD9QJOSD4x7kmXYlnfbA6JU=
|
||||
github.com/prometheus/client_golang v1.19.0/go.mod h1:ZRM9uEAypZakd+q/x7+gmsvXdURP+DABIEIjnmDdp+k=
|
||||
github.com/prometheus/client_model v0.6.0 h1:k1v3CzpSRUTrKMppY35TLwPvxHqBu0bYgxZzqGIgaos=
|
||||
github.com/prometheus/client_model v0.6.0/go.mod h1:NTQHnmxFpouOD0DpvP4XujX3CdOAGQPoaGhyTchlyt8=
|
||||
github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSzKKE=
|
||||
github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc=
|
||||
github.com/prometheus/exporter-toolkit v0.11.0 h1:yNTsuZ0aNCNFQ3aFTD2uhPOvr4iD7fdBvKPAEGkNf+g=
|
||||
github.com/prometheus/exporter-toolkit v0.11.0/go.mod h1:BVnENhnNecpwoTLiABx7mrPB/OLRIgN74qlQbV+FK1Q=
|
||||
github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
|
||||
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
|
||||
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
|
||||
github.com/stvp/go-udp-testing v0.0.0-20201019212854-469649b16807 h1:LUsDduamlucuNnWcaTbXQ6aLILFcLXADpOzeEH3U+OI=
|
||||
github.com/stvp/go-udp-testing v0.0.0-20201019212854-469649b16807/go.mod h1:7jxmlfBCDBXRzr0eAQJ48XC1hBu1np4CS5+cHEYfwpc=
|
||||
github.com/xhit/go-str2duration/v2 v2.1.0 h1:lxklc02Drh6ynqX+DdPyp5pCKLUQpRT8bp8Ydu2Bstc=
|
||||
github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc=
|
||||
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
|
||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo=
|
||||
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
|
||||
golang.org/x/oauth2 v0.16.0 h1:aDkGMBSYxElaoP81NpoUoz2oo2R2wHdZpGToUxfyQrQ=
|
||||
golang.org/x/oauth2 v0.16.0/go.mod h1:hqZ+0LWXsiVoZpeld6jVt06P3adbS2Uu911W1SsJv2o=
|
||||
golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
|
||||
golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
|
||||
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
|
||||
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
|
||||
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
105
line_benchmark_test.go
Normal file
105
line_benchmark_test.go
Normal file
|
@ -0,0 +1,105 @@
|
|||
// Copyright 2020 The Prometheus Authors
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/go-kit/log"
|
||||
|
||||
"github.com/prometheus/statsd_exporter/pkg/line"
|
||||
)
|
||||
|
||||
var (
|
||||
// just a grab bag of mixed formats, valid, invalid
|
||||
mixedLines = []string{
|
||||
"foo1:2|c",
|
||||
"foo2:3|g",
|
||||
"foo3:200|ms",
|
||||
"foo4:100|c|#tag1:bar,tag2:baz",
|
||||
"foo5:100|c|#tag1:bar,#tag2:baz",
|
||||
"foo6:100|c|#09digits:0,tag.with.dots:1",
|
||||
"foo10:100|c|@0.1|#tag1:bar,#tag2:baz",
|
||||
"foo11:100|c|@0.1|#tag1:foo:bar",
|
||||
"foo.[foo=bar,dim=val]test:1|g",
|
||||
"foo15:200|ms:300|ms:5|c|@0.1:6|g\nfoo15a:1|c:5|ms",
|
||||
"some_very_useful_metrics_with_quite_a_log_name:13|c",
|
||||
}
|
||||
nopLogger = log.NewNopLogger()
|
||||
)
|
||||
|
||||
func benchmarkLinesToEvents(times int, b *testing.B, input []string) {
|
||||
// always report allocations since this is a hot path
|
||||
b.ReportAllocs()
|
||||
|
||||
parser := line.NewParser()
|
||||
parser.EnableDogstatsdParsing()
|
||||
parser.EnableInfluxdbParsing()
|
||||
parser.EnableLibratoParsing()
|
||||
parser.EnableSignalFXParsing()
|
||||
|
||||
// reset benchmark timer to not measure startup costs
|
||||
b.ResetTimer()
|
||||
|
||||
for n := 0; n < b.N; n++ {
|
||||
for i := 0; i < times; i++ {
|
||||
for _, l := range input {
|
||||
parser.LineToEvents(l, *sampleErrors, samplesReceived, tagErrors, tagsReceived, nopLogger)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Mixed statsd formats
|
||||
func BenchmarkLineToEventsMixed1(b *testing.B) {
|
||||
benchmarkLinesToEvents(1, b, mixedLines)
|
||||
}
|
||||
func BenchmarkLineToEventsMixed5(b *testing.B) {
|
||||
benchmarkLinesToEvents(5, b, mixedLines)
|
||||
}
|
||||
func BenchmarkLineToEventsMixed50(b *testing.B) {
|
||||
benchmarkLinesToEvents(50, b, mixedLines)
|
||||
}
|
||||
|
||||
func BenchmarkLineFormats(b *testing.B) {
|
||||
input := map[string]string{
|
||||
"statsd": "foo1:2|c",
|
||||
"invalidStatsd": "foo1:2|c||",
|
||||
"dogStatsd": "foo1:100|c|#tag1:bar,tag2:baz",
|
||||
"invalidDogStatsd": "foo3:100|c|#09digits:0,tag.with.dots:1",
|
||||
"signalFx": "foo1.[foo=bar1,dim=val1]test:1|g",
|
||||
"invalidSignalFx": "foo1.[foo=bar1,dim=val1test:1|g",
|
||||
"influxDb": "foo1,tag1=bar,tag2=baz:100|c",
|
||||
"invalidInfluxDb": "foo3,tag1=bar,tag2:100|c",
|
||||
}
|
||||
|
||||
parser := line.NewParser()
|
||||
parser.EnableDogstatsdParsing()
|
||||
parser.EnableInfluxdbParsing()
|
||||
parser.EnableLibratoParsing()
|
||||
parser.EnableSignalFXParsing()
|
||||
|
||||
// reset benchmark timer to not measure startup costs
|
||||
b.ResetTimer()
|
||||
|
||||
for name, l := range input {
|
||||
b.Run(name, func(b *testing.B) {
|
||||
// always report allocations since this is a hot path
|
||||
b.ReportAllocs()
|
||||
for n := 0; n < b.N; n++ {
|
||||
parser.LineToEvents(l, *sampleErrors, samplesReceived, tagErrors, tagsReceived, nopLogger)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
598
main.go
598
main.go
|
@ -14,133 +14,555 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"bufio"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
_ "net/http/pprof"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strconv"
|
||||
"syscall"
|
||||
|
||||
"github.com/howeyc/fsnotify"
|
||||
"github.com/alecthomas/kingpin/v2"
|
||||
"github.com/go-kit/log"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/log"
|
||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
"github.com/prometheus/common/promlog"
|
||||
"github.com/prometheus/common/promlog/flag"
|
||||
"github.com/prometheus/common/version"
|
||||
"github.com/prometheus/exporter-toolkit/web"
|
||||
|
||||
"github.com/prometheus/statsd_exporter/pkg/address"
|
||||
"github.com/prometheus/statsd_exporter/pkg/event"
|
||||
"github.com/prometheus/statsd_exporter/pkg/exporter"
|
||||
"github.com/prometheus/statsd_exporter/pkg/level"
|
||||
"github.com/prometheus/statsd_exporter/pkg/line"
|
||||
"github.com/prometheus/statsd_exporter/pkg/listener"
|
||||
"github.com/prometheus/statsd_exporter/pkg/mapper"
|
||||
"github.com/prometheus/statsd_exporter/pkg/mappercache/lru"
|
||||
"github.com/prometheus/statsd_exporter/pkg/mappercache/randomreplacement"
|
||||
"github.com/prometheus/statsd_exporter/pkg/relay"
|
||||
)
|
||||
|
||||
var (
|
||||
listenAddress = flag.String("web.listen-address", ":9102", "The address on which to expose the web interface and generated Prometheus metrics.")
|
||||
metricsEndpoint = flag.String("web.telemetry-path", "/metrics", "Path under which to expose metrics.")
|
||||
statsdListenAddress = flag.String("statsd.listen-address", ":9125", "The UDP address on which to receive statsd metric lines.")
|
||||
mappingConfig = flag.String("statsd.mapping-config", "", "Metric mapping configuration file name.")
|
||||
readBuffer = flag.Int("statsd.read-buffer", 0, "Size (in bytes) of the operating system's transmit read buffer associated with the UDP connection. Please make sure the kernel parameters net.core.rmem_max is set to a value greater than the value specified.")
|
||||
eventStats = promauto.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Name: "statsd_exporter_events_total",
|
||||
Help: "The total number of StatsD events seen.",
|
||||
},
|
||||
[]string{"type"},
|
||||
)
|
||||
eventsFlushed = promauto.NewCounter(
|
||||
prometheus.CounterOpts{
|
||||
Name: "statsd_exporter_event_queue_flushed_total",
|
||||
Help: "Number of times events were flushed to exporter",
|
||||
},
|
||||
)
|
||||
eventsUnmapped = promauto.NewCounter(
|
||||
prometheus.CounterOpts{
|
||||
Name: "statsd_exporter_events_unmapped_total",
|
||||
Help: "The total number of StatsD events no mapping was found for.",
|
||||
})
|
||||
udpPackets = promauto.NewCounter(
|
||||
prometheus.CounterOpts{
|
||||
Name: "statsd_exporter_udp_packets_total",
|
||||
Help: "The total number of StatsD packets received over UDP.",
|
||||
},
|
||||
)
|
||||
udpPacketDrops = promauto.NewCounter(
|
||||
prometheus.CounterOpts{
|
||||
Name: "statsd_exporter_udp_packet_drops_total",
|
||||
Help: "The total number of dropped StatsD packets which received over UDP.",
|
||||
},
|
||||
)
|
||||
tcpConnections = promauto.NewCounter(
|
||||
prometheus.CounterOpts{
|
||||
Name: "statsd_exporter_tcp_connections_total",
|
||||
Help: "The total number of TCP connections handled.",
|
||||
},
|
||||
)
|
||||
tcpErrors = promauto.NewCounter(
|
||||
prometheus.CounterOpts{
|
||||
Name: "statsd_exporter_tcp_connection_errors_total",
|
||||
Help: "The number of errors encountered reading from TCP.",
|
||||
},
|
||||
)
|
||||
tcpLineTooLong = promauto.NewCounter(
|
||||
prometheus.CounterOpts{
|
||||
Name: "statsd_exporter_tcp_too_long_lines_total",
|
||||
Help: "The number of lines discarded due to being too long.",
|
||||
},
|
||||
)
|
||||
unixgramPackets = promauto.NewCounter(
|
||||
prometheus.CounterOpts{
|
||||
Name: "statsd_exporter_unixgram_packets_total",
|
||||
Help: "The total number of StatsD packets received over Unixgram.",
|
||||
},
|
||||
)
|
||||
linesReceived = promauto.NewCounter(
|
||||
prometheus.CounterOpts{
|
||||
Name: "statsd_exporter_lines_total",
|
||||
Help: "The total number of StatsD lines received.",
|
||||
},
|
||||
)
|
||||
samplesReceived = promauto.NewCounter(
|
||||
prometheus.CounterOpts{
|
||||
Name: "statsd_exporter_samples_total",
|
||||
Help: "The total number of StatsD samples received.",
|
||||
},
|
||||
)
|
||||
sampleErrors = promauto.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Name: "statsd_exporter_sample_errors_total",
|
||||
Help: "The total number of errors parsing StatsD samples.",
|
||||
},
|
||||
[]string{"reason"},
|
||||
)
|
||||
tagsReceived = promauto.NewCounter(
|
||||
prometheus.CounterOpts{
|
||||
Name: "statsd_exporter_tags_total",
|
||||
Help: "The total number of DogStatsD tags processed.",
|
||||
},
|
||||
)
|
||||
tagErrors = promauto.NewCounter(
|
||||
prometheus.CounterOpts{
|
||||
Name: "statsd_exporter_tag_errors_total",
|
||||
Help: "The number of errors parsing DogStatsD tags.",
|
||||
},
|
||||
)
|
||||
configLoads = promauto.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Name: "statsd_exporter_config_reloads_total",
|
||||
Help: "The number of configuration reloads.",
|
||||
},
|
||||
[]string{"outcome"},
|
||||
)
|
||||
mappingsCount = promauto.NewGauge(prometheus.GaugeOpts{
|
||||
Name: "statsd_exporter_loaded_mappings",
|
||||
Help: "The current number of configured metric mappings.",
|
||||
})
|
||||
conflictingEventStats = promauto.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Name: "statsd_exporter_events_conflict_total",
|
||||
Help: "The total number of StatsD events with conflicting names.",
|
||||
},
|
||||
[]string{"type"},
|
||||
)
|
||||
errorEventStats = promauto.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Name: "statsd_exporter_events_error_total",
|
||||
Help: "The total number of StatsD events discarded due to errors.",
|
||||
},
|
||||
[]string{"reason"},
|
||||
)
|
||||
eventsActions = promauto.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Name: "statsd_exporter_events_actions_total",
|
||||
Help: "The total number of StatsD events by action.",
|
||||
},
|
||||
[]string{"action"},
|
||||
)
|
||||
metricsCount = promauto.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "statsd_exporter_metrics_total",
|
||||
Help: "The total number of metrics.",
|
||||
},
|
||||
[]string{"type"},
|
||||
)
|
||||
)
|
||||
|
||||
func serveHTTP() {
|
||||
http.Handle(*metricsEndpoint, prometheus.Handler())
|
||||
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Write([]byte(`<html>
|
||||
<head><title>StatsD Bridge</title></head>
|
||||
<body>
|
||||
<h1>StatsD Bridge</h1>
|
||||
<p><a href="` + *metricsEndpoint + `">Metrics</a></p>
|
||||
</body>
|
||||
</html>`))
|
||||
})
|
||||
http.ListenAndServe(*listenAddress, nil)
|
||||
func serveHTTP(mux http.Handler, listenAddress string, logger log.Logger) {
|
||||
level.Error(logger).Log("msg", http.ListenAndServe(listenAddress, mux))
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
func udpAddrFromString(addr string) *net.UDPAddr {
|
||||
host, portStr, err := net.SplitHostPort(addr)
|
||||
if err != nil {
|
||||
log.Fatal("Bad StatsD listening address", addr)
|
||||
}
|
||||
func sighupConfigReloader(fileName string, mapper *mapper.MetricMapper, logger log.Logger) {
|
||||
signals := make(chan os.Signal, 1)
|
||||
signal.Notify(signals, syscall.SIGHUP)
|
||||
|
||||
if host == "" {
|
||||
host = "0.0.0.0"
|
||||
}
|
||||
ip, err := net.ResolveIPAddr("ip", host)
|
||||
if err != nil {
|
||||
log.Fatalf("Unable to resolve %s: %s", host, err)
|
||||
}
|
||||
for s := range signals {
|
||||
if fileName == "" {
|
||||
level.Warn(logger).Log("msg", "Received signal but no mapping config to reload", "signal", s)
|
||||
continue
|
||||
}
|
||||
|
||||
port, err := strconv.Atoi(portStr)
|
||||
if err != nil || port < 0 || port > 65535 {
|
||||
log.Fatalf("Bad port %s: %s", portStr, err)
|
||||
}
|
||||
level.Info(logger).Log("msg", "Received signal, attempting reload", "signal", s)
|
||||
|
||||
return &net.UDPAddr{
|
||||
IP: ip.IP,
|
||||
Port: port,
|
||||
Zone: ip.Zone,
|
||||
reloadConfig(fileName, mapper, logger)
|
||||
}
|
||||
}
|
||||
|
||||
func watchConfig(fileName string, mapper *metricMapper) {
|
||||
watcher, err := fsnotify.NewWatcher()
|
||||
func reloadConfig(fileName string, mapper *mapper.MetricMapper, logger log.Logger) {
|
||||
err := mapper.InitFromFile(fileName)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
level.Info(logger).Log("msg", "Error reloading config", "error", err)
|
||||
configLoads.WithLabelValues("failure").Inc()
|
||||
} else {
|
||||
level.Info(logger).Log("msg", "Config reloaded successfully")
|
||||
configLoads.WithLabelValues("success").Inc()
|
||||
}
|
||||
}
|
||||
|
||||
err = watcher.WatchFlags(fileName, fsnotify.FSN_MODIFY)
|
||||
func dumpFSM(mapper *mapper.MetricMapper, dumpFilename string, logger log.Logger) error {
|
||||
f, err := os.Create(dumpFilename)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
return err
|
||||
}
|
||||
level.Info(logger).Log("msg", "Start dumping FSM", "file_name", dumpFilename)
|
||||
w := bufio.NewWriter(f)
|
||||
mapper.FSM.DumpFSM(w)
|
||||
w.Flush()
|
||||
f.Close()
|
||||
level.Info(logger).Log("msg", "Finish dumping FSM")
|
||||
return nil
|
||||
}
|
||||
|
||||
for {
|
||||
select {
|
||||
case ev := <-watcher.Event:
|
||||
log.Printf("Config file changed (%s), attempting reload", ev)
|
||||
err = mapper.initFromFile(fileName)
|
||||
if err != nil {
|
||||
log.Println("Error reloading config:", err)
|
||||
configLoads.WithLabelValues("failure").Inc()
|
||||
} else {
|
||||
log.Println("Config reloaded successfully")
|
||||
configLoads.WithLabelValues("success").Inc()
|
||||
}
|
||||
// Re-add the file watcher since it can get lost on some changes. E.g.
|
||||
// saving a file with vim results in a RENAME-MODIFY-DELETE event
|
||||
// sequence, after which the newly written file is no longer watched.
|
||||
err = watcher.WatchFlags(fileName, fsnotify.FSN_MODIFY)
|
||||
case err := <-watcher.Error:
|
||||
log.Println("Error watching config:", err)
|
||||
func getCache(cacheSize int, cacheType string, registerer prometheus.Registerer) (mapper.MetricMapperCache, error) {
|
||||
var cache mapper.MetricMapperCache
|
||||
var err error
|
||||
if cacheSize == 0 {
|
||||
return nil, nil
|
||||
} else {
|
||||
switch cacheType {
|
||||
case "lru":
|
||||
cache, err = lru.NewMetricMapperLRUCache(registerer, cacheSize)
|
||||
case "random":
|
||||
cache, err = randomreplacement.NewMetricMapperRRCache(registerer, cacheSize)
|
||||
default:
|
||||
err = fmt.Errorf("unsupported cache type %q", cacheType)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return cache, nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
var (
|
||||
listenAddress = kingpin.Flag("web.listen-address", "The address on which to expose the web interface and generated Prometheus metrics.").Default(":9102").String()
|
||||
enableLifecycle = kingpin.Flag("web.enable-lifecycle", "Enable shutdown and reload via HTTP request.").Default("false").Bool()
|
||||
metricsEndpoint = kingpin.Flag("web.telemetry-path", "Path under which to expose metrics.").Default("/metrics").String()
|
||||
statsdListenUDP = kingpin.Flag("statsd.listen-udp", "The UDP address on which to receive statsd metric lines. \"\" disables it.").Default(":9125").String()
|
||||
statsdListenTCP = kingpin.Flag("statsd.listen-tcp", "The TCP address on which to receive statsd metric lines. \"\" disables it.").Default(":9125").String()
|
||||
statsdListenUnixgram = kingpin.Flag("statsd.listen-unixgram", "The Unixgram socket path to receive statsd metric lines in datagram. \"\" disables it.").Default("").String()
|
||||
// not using Int here because flag displays default in decimal, 0755 will show as 493
|
||||
statsdUnixSocketMode = kingpin.Flag("statsd.unixsocket-mode", "The permission mode of the unix socket.").Default("755").String()
|
||||
mappingConfig = kingpin.Flag("statsd.mapping-config", "Metric mapping configuration file name.").String()
|
||||
readBuffer = kingpin.Flag("statsd.read-buffer", "Size (in bytes) of the operating system's transmit read buffer associated with the UDP or Unixgram connection. Please make sure the kernel parameters net.core.rmem_max is set to a value greater than the value specified.").Int()
|
||||
cacheSize = kingpin.Flag("statsd.cache-size", "Maximum size of your metric mapping cache. Relies on least recently used replacement policy if max size is reached.").Default("1000").Int()
|
||||
cacheType = kingpin.Flag("statsd.cache-type", "Metric mapping cache type. Valid options are \"lru\" and \"random\"").Default("lru").Enum("lru", "random")
|
||||
eventQueueSize = kingpin.Flag("statsd.event-queue-size", "Size of internal queue for processing events.").Default("10000").Uint()
|
||||
eventFlushThreshold = kingpin.Flag("statsd.event-flush-threshold", "Number of events to hold in queue before flushing.").Default("1000").Int()
|
||||
eventFlushInterval = kingpin.Flag("statsd.event-flush-interval", "Maximum time between event queue flushes.").Default("200ms").Duration()
|
||||
dumpFSMPath = kingpin.Flag("debug.dump-fsm", "The path to dump internal FSM generated for glob matching as Dot file.").Default("").String()
|
||||
checkConfig = kingpin.Flag("check-config", "Check configuration and exit.").Default("false").Bool()
|
||||
dogstatsdTagsEnabled = kingpin.Flag("statsd.parse-dogstatsd-tags", "Parse DogStatsd style tags. Enabled by default.").Default("true").Bool()
|
||||
influxdbTagsEnabled = kingpin.Flag("statsd.parse-influxdb-tags", "Parse InfluxDB style tags. Enabled by default.").Default("true").Bool()
|
||||
libratoTagsEnabled = kingpin.Flag("statsd.parse-librato-tags", "Parse Librato style tags. Enabled by default.").Default("true").Bool()
|
||||
signalFXTagsEnabled = kingpin.Flag("statsd.parse-signalfx-tags", "Parse SignalFX style tags. Enabled by default.").Default("true").Bool()
|
||||
relayAddr = kingpin.Flag("statsd.relay.address", "The UDP relay target address (host:port)").String()
|
||||
relayPacketLen = kingpin.Flag("statsd.relay.packet-length", "Maximum relay output packet length to avoid fragmentation").Default("1400").Uint()
|
||||
udpPacketQueueSize = kingpin.Flag("statsd.udp-packet-queue-size", "Size of internal queue for processing UDP packets.").Default("10000").Int()
|
||||
)
|
||||
|
||||
log.Println("Starting StatsD -> Prometheus Exporter...")
|
||||
log.Println("Accepting StatsD Traffic on", *statsdListenAddress)
|
||||
log.Println("Accepting Prometheus Requests on", *listenAddress)
|
||||
promlogConfig := &promlog.Config{}
|
||||
flag.AddFlags(kingpin.CommandLine, promlogConfig)
|
||||
kingpin.Version(version.Print("statsd_exporter"))
|
||||
kingpin.CommandLine.UsageWriter(os.Stdout)
|
||||
kingpin.HelpFlag.Short('h')
|
||||
kingpin.Parse()
|
||||
logger := promlog.New(promlogConfig)
|
||||
if err := level.SetLogLevel(promlogConfig.Level.String()); err != nil {
|
||||
level.Error(logger).Log("msg", "failed to set log level", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
prometheus.MustRegister(version.NewCollector("statsd_exporter"))
|
||||
|
||||
go serveHTTP()
|
||||
parser := line.NewParser()
|
||||
if *dogstatsdTagsEnabled {
|
||||
parser.EnableDogstatsdParsing()
|
||||
}
|
||||
if *influxdbTagsEnabled {
|
||||
parser.EnableInfluxdbParsing()
|
||||
}
|
||||
if *libratoTagsEnabled {
|
||||
parser.EnableLibratoParsing()
|
||||
}
|
||||
if *signalFXTagsEnabled {
|
||||
parser.EnableSignalFXParsing()
|
||||
}
|
||||
|
||||
events := make(chan Events, 1024)
|
||||
level.Info(logger).Log("msg", "Starting StatsD -> Prometheus Exporter", "version", version.Info())
|
||||
level.Info(logger).Log("msg", "Build context", "context", version.BuildContext())
|
||||
|
||||
events := make(chan event.Events, *eventQueueSize)
|
||||
defer close(events)
|
||||
eventQueue := event.NewEventQueue(events, *eventFlushThreshold, *eventFlushInterval, eventsFlushed)
|
||||
|
||||
listenAddr := udpAddrFromString(*statsdListenAddress)
|
||||
conn, err := net.ListenUDP("udp", listenAddr)
|
||||
thisMapper := &mapper.MetricMapper{Registerer: prometheus.DefaultRegisterer, MappingsCount: mappingsCount, Logger: logger}
|
||||
|
||||
cache, err := getCache(*cacheSize, *cacheType, thisMapper.Registerer)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
level.Error(logger).Log("msg", "Unable to setup metric mapper cache", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
thisMapper.UseCache(cache)
|
||||
|
||||
if *readBuffer != 0 {
|
||||
err = conn.SetReadBuffer(*readBuffer)
|
||||
if err != nil {
|
||||
log.Fatal("Error setting UDP read buffer:", err)
|
||||
}
|
||||
}
|
||||
|
||||
l := &StatsDListener{conn: conn}
|
||||
go l.Listen(events)
|
||||
|
||||
mapper := &metricMapper{}
|
||||
if *mappingConfig != "" {
|
||||
err := mapper.initFromFile(*mappingConfig)
|
||||
err := thisMapper.InitFromFile(*mappingConfig)
|
||||
if err != nil {
|
||||
log.Fatal("Error loading config:", err)
|
||||
level.Error(logger).Log("msg", "error loading config", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
if *dumpFSMPath != "" {
|
||||
err := dumpFSM(thisMapper, *dumpFSMPath, logger)
|
||||
if err != nil {
|
||||
level.Error(logger).Log("msg", "error dumping FSM", "error", err)
|
||||
// Failure to dump the FSM is an error (the user asked for it and it
|
||||
// didn't happen) but not fatal (the exporter is fully functional
|
||||
// afterwards).
|
||||
}
|
||||
}
|
||||
go watchConfig(*mappingConfig, mapper)
|
||||
}
|
||||
exporter := NewExporter(mapper)
|
||||
exporter.Listen(events)
|
||||
|
||||
exporter := exporter.NewExporter(prometheus.DefaultRegisterer, thisMapper, logger, eventsActions, eventsUnmapped, errorEventStats, eventStats, conflictingEventStats, metricsCount)
|
||||
|
||||
if *checkConfig {
|
||||
level.Info(logger).Log("msg", "Configuration check successful, exiting")
|
||||
return
|
||||
}
|
||||
|
||||
var relayTarget *relay.Relay
|
||||
if *relayAddr != "" {
|
||||
var err error
|
||||
relayTarget, err = relay.NewRelay(logger, *relayAddr, *relayPacketLen)
|
||||
if err != nil {
|
||||
level.Error(logger).Log("msg", "Unable to create relay", "err", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
level.Info(logger).Log("msg", "Accepting StatsD Traffic", "udp", *statsdListenUDP, "tcp", *statsdListenTCP, "unixgram", *statsdListenUnixgram)
|
||||
level.Info(logger).Log("msg", "Accepting Prometheus Requests", "addr", *listenAddress)
|
||||
|
||||
if *statsdListenUDP == "" && *statsdListenTCP == "" && *statsdListenUnixgram == "" {
|
||||
level.Error(logger).Log("At least one of UDP/TCP/Unixgram listeners must be specified.")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if *statsdListenUDP != "" {
|
||||
udpListenAddr, err := address.UDPAddrFromString(*statsdListenUDP)
|
||||
if err != nil {
|
||||
level.Error(logger).Log("msg", "invalid UDP listen address", "address", *statsdListenUDP, "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
uconn, err := net.ListenUDP("udp", udpListenAddr)
|
||||
if err != nil {
|
||||
level.Error(logger).Log("msg", "failed to start UDP listener", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if *readBuffer != 0 {
|
||||
err = uconn.SetReadBuffer(*readBuffer)
|
||||
if err != nil {
|
||||
level.Error(logger).Log("msg", "error setting UDP read buffer", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
udpPacketQueue := make(chan []byte, *udpPacketQueueSize)
|
||||
|
||||
ul := &listener.StatsDUDPListener{
|
||||
Conn: uconn,
|
||||
EventHandler: eventQueue,
|
||||
Logger: logger,
|
||||
LineParser: parser,
|
||||
UDPPackets: udpPackets,
|
||||
UDPPacketDrops: udpPacketDrops,
|
||||
LinesReceived: linesReceived,
|
||||
EventsFlushed: eventsFlushed,
|
||||
Relay: relayTarget,
|
||||
SampleErrors: *sampleErrors,
|
||||
SamplesReceived: samplesReceived,
|
||||
TagErrors: tagErrors,
|
||||
TagsReceived: tagsReceived,
|
||||
UdpPacketQueue: udpPacketQueue,
|
||||
}
|
||||
|
||||
go ul.Listen()
|
||||
}
|
||||
|
||||
if *statsdListenTCP != "" {
|
||||
tcpListenAddr, err := address.TCPAddrFromString(*statsdListenTCP)
|
||||
if err != nil {
|
||||
level.Error(logger).Log("msg", "invalid TCP listen address", "address", *statsdListenUDP, "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
tconn, err := net.ListenTCP("tcp", tcpListenAddr)
|
||||
if err != nil {
|
||||
level.Error(logger).Log("msg", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer tconn.Close()
|
||||
|
||||
tl := &listener.StatsDTCPListener{
|
||||
Conn: tconn,
|
||||
EventHandler: eventQueue,
|
||||
Logger: logger,
|
||||
LineParser: parser,
|
||||
LinesReceived: linesReceived,
|
||||
EventsFlushed: eventsFlushed,
|
||||
Relay: relayTarget,
|
||||
SampleErrors: *sampleErrors,
|
||||
SamplesReceived: samplesReceived,
|
||||
TagErrors: tagErrors,
|
||||
TagsReceived: tagsReceived,
|
||||
TCPConnections: tcpConnections,
|
||||
TCPErrors: tcpErrors,
|
||||
TCPLineTooLong: tcpLineTooLong,
|
||||
}
|
||||
|
||||
go tl.Listen()
|
||||
}
|
||||
|
||||
if *statsdListenUnixgram != "" {
|
||||
var err error
|
||||
if _, err = os.Stat(*statsdListenUnixgram); !os.IsNotExist(err) {
|
||||
level.Error(logger).Log("msg", "Unixgram socket already exists", "socket_name", *statsdListenUnixgram)
|
||||
os.Exit(1)
|
||||
}
|
||||
uxgconn, err := net.ListenUnixgram("unixgram", &net.UnixAddr{
|
||||
Net: "unixgram",
|
||||
Name: *statsdListenUnixgram,
|
||||
})
|
||||
if err != nil {
|
||||
level.Error(logger).Log("msg", "failed to listen on Unixgram socket", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
defer uxgconn.Close()
|
||||
|
||||
if *readBuffer != 0 {
|
||||
err = uxgconn.SetReadBuffer(*readBuffer)
|
||||
if err != nil {
|
||||
level.Error(logger).Log("msg", "error setting Unixgram read buffer", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
ul := &listener.StatsDUnixgramListener{
|
||||
Conn: uxgconn,
|
||||
EventHandler: eventQueue,
|
||||
Logger: logger,
|
||||
LineParser: parser,
|
||||
UnixgramPackets: unixgramPackets,
|
||||
LinesReceived: linesReceived,
|
||||
EventsFlushed: eventsFlushed,
|
||||
Relay: relayTarget,
|
||||
SampleErrors: *sampleErrors,
|
||||
SamplesReceived: samplesReceived,
|
||||
TagErrors: tagErrors,
|
||||
TagsReceived: tagsReceived,
|
||||
}
|
||||
|
||||
go ul.Listen()
|
||||
|
||||
// if it's an abstract unix domain socket, it won't exist on fs
|
||||
// so we can't chmod it either
|
||||
if _, err := os.Stat(*statsdListenUnixgram); !os.IsNotExist(err) {
|
||||
defer os.Remove(*statsdListenUnixgram)
|
||||
|
||||
// convert the string to octet
|
||||
perm, err := strconv.ParseInt("0"+string(*statsdUnixSocketMode), 8, 32)
|
||||
if err != nil {
|
||||
level.Warn(logger).Log("Bad permission %s: %v, ignoring\n", *statsdUnixSocketMode, err)
|
||||
} else {
|
||||
err = os.Chmod(*statsdListenUnixgram, os.FileMode(perm))
|
||||
if err != nil {
|
||||
level.Warn(logger).Log("Failed to change unixgram socket permission: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mux := http.DefaultServeMux
|
||||
mux.Handle(*metricsEndpoint, promhttp.Handler())
|
||||
if *metricsEndpoint != "/" && *metricsEndpoint != "" {
|
||||
landingConfig := web.LandingConfig{
|
||||
Name: "StatsD Exporter",
|
||||
Description: "Prometheus Exporter for converting StatsD to Prometheus metrics",
|
||||
Version: version.Info(),
|
||||
Links: []web.LandingLinks{
|
||||
{
|
||||
Address: *metricsEndpoint,
|
||||
Text: "Metrics",
|
||||
},
|
||||
},
|
||||
}
|
||||
landingPage, err := web.NewLandingPage(landingConfig)
|
||||
if err != nil {
|
||||
level.Error(logger).Log("err", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
mux.Handle("/", landingPage)
|
||||
}
|
||||
|
||||
quitChan := make(chan struct{}, 1)
|
||||
|
||||
if *enableLifecycle {
|
||||
mux.HandleFunc("/-/reload", func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == http.MethodPut || r.Method == http.MethodPost {
|
||||
fmt.Fprintf(w, "Requesting reload")
|
||||
if *mappingConfig == "" {
|
||||
level.Warn(logger).Log("msg", "Received lifecycle api reload but no mapping config to reload")
|
||||
return
|
||||
}
|
||||
level.Info(logger).Log("msg", "Received lifecycle api reload, attempting reload")
|
||||
reloadConfig(*mappingConfig, thisMapper, logger)
|
||||
}
|
||||
})
|
||||
mux.HandleFunc("/-/quit", func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == http.MethodPut || r.Method == http.MethodPost {
|
||||
fmt.Fprintf(w, "Requesting termination... Goodbye!")
|
||||
quitChan <- struct{}{}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
mux.HandleFunc("/-/healthy", func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == http.MethodGet {
|
||||
level.Debug(logger).Log("msg", "Received health check")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
fmt.Fprintf(w, "Statsd Exporter is Healthy.\n")
|
||||
}
|
||||
})
|
||||
|
||||
mux.HandleFunc("/-/ready", func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == http.MethodGet {
|
||||
level.Debug(logger).Log("msg", "Received ready check")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
fmt.Fprintf(w, "Statsd Exporter is Ready.\n")
|
||||
}
|
||||
})
|
||||
|
||||
go serveHTTP(mux, *listenAddress, logger)
|
||||
|
||||
go sighupConfigReloader(*mappingConfig, thisMapper, logger)
|
||||
go exporter.Listen(events)
|
||||
|
||||
signals := make(chan os.Signal, 1)
|
||||
signal.Notify(signals, os.Interrupt, syscall.SIGTERM)
|
||||
|
||||
// quit if we get a message on either channel
|
||||
select {
|
||||
case sig := <-signals:
|
||||
level.Info(logger).Log("msg", "Received os signal, exiting", "signal", sig.String())
|
||||
case <-quitChan:
|
||||
level.Info(logger).Log("msg", "Received lifecycle api quit, exiting")
|
||||
}
|
||||
}
|
||||
|
|
144
mapper.go
144
mapper.go
|
@ -1,144 +0,0 @@
|
|||
// Copyright 2013 The Prometheus Authors
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
var (
|
||||
identifierRE = `[a-zA-Z_][a-zA-Z0-9_]+`
|
||||
statsdMetricRE = `[a-zA-Z_](-?[a-zA-Z0-9_])+`
|
||||
|
||||
metricLineRE = regexp.MustCompile(`^(\*\.|` + statsdMetricRE + `\.)+(\*|` + statsdMetricRE + `)$`)
|
||||
labelLineRE = regexp.MustCompile(`^(` + identifierRE + `)\s*=\s*"(.*)"$`)
|
||||
metricNameRE = regexp.MustCompile(`^` + identifierRE + `$`)
|
||||
)
|
||||
|
||||
type metricMapping struct {
|
||||
regex *regexp.Regexp
|
||||
labels prometheus.Labels
|
||||
}
|
||||
|
||||
type metricMapper struct {
|
||||
mappings []metricMapping
|
||||
mutex sync.Mutex
|
||||
}
|
||||
|
||||
type configLoadStates int
|
||||
|
||||
const (
|
||||
SEARCHING configLoadStates = iota
|
||||
METRIC_DEFINITION
|
||||
)
|
||||
|
||||
func (m *metricMapper) initFromString(fileContents string) error {
|
||||
lines := strings.Split(fileContents, "\n")
|
||||
state := SEARCHING
|
||||
|
||||
parsedMappings := []metricMapping{}
|
||||
currentMapping := metricMapping{labels: prometheus.Labels{}}
|
||||
for i, line := range lines {
|
||||
line := strings.TrimSpace(line)
|
||||
|
||||
switch state {
|
||||
case SEARCHING:
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
if !metricLineRE.MatchString(line) {
|
||||
return fmt.Errorf("Line %d: expected metric match line, got: %s", i, line)
|
||||
}
|
||||
|
||||
// Translate the glob-style metric match line into a proper regex that we
|
||||
// can use to match metrics later on.
|
||||
metricRe := strings.Replace(line, ".", "\\.", -1)
|
||||
metricRe = strings.Replace(metricRe, "*", "([^.]+)", -1)
|
||||
currentMapping.regex = regexp.MustCompile("^" + metricRe + "$")
|
||||
|
||||
state = METRIC_DEFINITION
|
||||
|
||||
case METRIC_DEFINITION:
|
||||
if line == "" {
|
||||
if len(currentMapping.labels) == 0 {
|
||||
return fmt.Errorf("Line %d: metric mapping didn't set any labels", i)
|
||||
}
|
||||
if _, ok := currentMapping.labels["name"]; !ok {
|
||||
return fmt.Errorf("Line %d: metric mapping didn't set a metric name", i)
|
||||
}
|
||||
|
||||
parsedMappings = append(parsedMappings, currentMapping)
|
||||
|
||||
state = SEARCHING
|
||||
currentMapping = metricMapping{labels: prometheus.Labels{}}
|
||||
continue
|
||||
}
|
||||
|
||||
matches := labelLineRE.FindStringSubmatch(line)
|
||||
if len(matches) != 3 {
|
||||
return fmt.Errorf("Line %d: expected label mapping line, got: %s", i, line)
|
||||
}
|
||||
label, value := matches[1], matches[2]
|
||||
if label == "name" && !metricNameRE.MatchString(value) {
|
||||
return fmt.Errorf("Line %d: metric name '%s' doesn't match regex '%s'", i, value, metricNameRE)
|
||||
}
|
||||
currentMapping.labels[label] = value
|
||||
default:
|
||||
panic("illegal state")
|
||||
}
|
||||
}
|
||||
|
||||
m.mutex.Lock()
|
||||
defer m.mutex.Unlock()
|
||||
m.mappings = parsedMappings
|
||||
|
||||
mappingsCount.Set(float64(len(parsedMappings)))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *metricMapper) initFromFile(fileName string) error {
|
||||
mappingStr, err := ioutil.ReadFile(fileName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return m.initFromString(string(mappingStr))
|
||||
}
|
||||
|
||||
func (m *metricMapper) getMapping(statsdMetric string) (labels prometheus.Labels, present bool) {
|
||||
m.mutex.Lock()
|
||||
defer m.mutex.Unlock()
|
||||
|
||||
for _, mapping := range m.mappings {
|
||||
matches := mapping.regex.FindStringSubmatchIndex(statsdMetric)
|
||||
if len(matches) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
labels := prometheus.Labels{}
|
||||
for label, valueExpr := range mapping.labels {
|
||||
value := mapping.regex.ExpandString([]byte{}, valueExpr, statsdMetric, matches)
|
||||
labels[label] = string(value)
|
||||
}
|
||||
return labels, true
|
||||
}
|
||||
|
||||
return nil, false
|
||||
}
|
164
mapper_test.go
164
mapper_test.go
|
@ -1,164 +0,0 @@
|
|||
// Copyright 2013 The Prometheus Authors
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMetricMapper(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
config string
|
||||
configBad bool
|
||||
mappings map[string]map[string]string
|
||||
}{
|
||||
// Empty config.
|
||||
{},
|
||||
// Config with several mapping definitions.
|
||||
{
|
||||
config: `
|
||||
test.dispatcher.*.*.*
|
||||
name="dispatch_events"
|
||||
processor="$1"
|
||||
action="$2"
|
||||
result="$3"
|
||||
job="test_dispatcher"
|
||||
|
||||
test.my-dispatch-host01.name.dispatcher.*.*.*
|
||||
name="host_dispatch_events"
|
||||
processor="$1"
|
||||
action="$2"
|
||||
result="$3"
|
||||
job="test_dispatcher"
|
||||
|
||||
*.*
|
||||
name="catchall"
|
||||
first="$1"
|
||||
second="$2"
|
||||
third="$3"
|
||||
job="$1-$2-$3"
|
||||
`,
|
||||
mappings: map[string]map[string]string{
|
||||
"test.dispatcher.FooProcessor.send.succeeded": map[string]string{
|
||||
"name": "dispatch_events",
|
||||
"processor": "FooProcessor",
|
||||
"action": "send",
|
||||
"result": "succeeded",
|
||||
"job": "test_dispatcher",
|
||||
},
|
||||
"test.my-dispatch-host01.name.dispatcher.FooProcessor.send.succeeded": map[string]string{
|
||||
"name": "host_dispatch_events",
|
||||
"processor": "FooProcessor",
|
||||
"action": "send",
|
||||
"result": "succeeded",
|
||||
"job": "test_dispatcher",
|
||||
},
|
||||
"foo.bar": map[string]string{
|
||||
"name": "catchall",
|
||||
"first": "foo",
|
||||
"second": "bar",
|
||||
"third": "",
|
||||
"job": "foo-bar-",
|
||||
},
|
||||
"foo.bar.baz": map[string]string{},
|
||||
},
|
||||
},
|
||||
// Config with bad regex reference.
|
||||
{
|
||||
config: `
|
||||
test.*
|
||||
name="name"
|
||||
label="$1_foo"
|
||||
`,
|
||||
mappings: map[string]map[string]string{
|
||||
"test.a": map[string]string{
|
||||
"name": "name",
|
||||
"label": "",
|
||||
},
|
||||
},
|
||||
},
|
||||
// Config with good regex reference.
|
||||
{
|
||||
config: `
|
||||
test.*
|
||||
name="name"
|
||||
label="${1}_foo"
|
||||
`,
|
||||
mappings: map[string]map[string]string{
|
||||
"test.a": map[string]string{
|
||||
"name": "name",
|
||||
"label": "a_foo",
|
||||
},
|
||||
},
|
||||
},
|
||||
// Config with bad metric line.
|
||||
{
|
||||
config: `
|
||||
bad--metric-line.*.*
|
||||
name="foo"
|
||||
`,
|
||||
configBad: true,
|
||||
},
|
||||
// Config with bad label line.
|
||||
{
|
||||
config: `
|
||||
test.*.*
|
||||
name=foo
|
||||
`,
|
||||
configBad: true,
|
||||
},
|
||||
// Config with bad label line.
|
||||
{
|
||||
config: `
|
||||
test.*.*
|
||||
name="foo-name"
|
||||
`,
|
||||
configBad: true,
|
||||
},
|
||||
// Config with bad metric name.
|
||||
{
|
||||
config: `
|
||||
test.*.*
|
||||
name="0foo"
|
||||
`,
|
||||
configBad: true,
|
||||
},
|
||||
}
|
||||
|
||||
mapper := metricMapper{}
|
||||
for i, scenario := range scenarios {
|
||||
err := mapper.initFromString(scenario.config)
|
||||
if err != nil && !scenario.configBad {
|
||||
t.Fatalf("%d. Config load error: %s", i, err)
|
||||
}
|
||||
if err == nil && scenario.configBad {
|
||||
t.Fatalf("%d. Expected bad config, but loaded ok", i)
|
||||
}
|
||||
|
||||
for metric, mapping := range scenario.mappings {
|
||||
labels, present := mapper.getMapping(metric)
|
||||
if len(labels) == 0 && present {
|
||||
t.Fatalf("%d.%q: Expected metric to not be present", i, metric)
|
||||
}
|
||||
if len(labels) != len(mapping) {
|
||||
t.Fatalf("%d.%q: Expected %d labels, got %d", i, metric, len(mapping), len(labels))
|
||||
}
|
||||
for label, value := range labels {
|
||||
if mapping[label] != value {
|
||||
t.Fatalf("%d.%q: Expected labels %v, got %v", i, metric, mapping, labels)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
3
pkg/README.md
Normal file
3
pkg/README.md
Normal file
|
@ -0,0 +1,3 @@
|
|||
The `pkg` directory is deprecated.
|
||||
Please do not add new packages to this directory.
|
||||
Existing packages will be moved elsewhere eventually.
|
66
pkg/address/address.go
Normal file
66
pkg/address/address.go
Normal file
|
@ -0,0 +1,66 @@
|
|||
// Copyright 2013 The Prometheus Authors
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package address
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
func IPPortFromString(addr string) (*net.IPAddr, int, error) {
|
||||
host, portStr, err := net.SplitHostPort(addr)
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("bad StatsD listening address: %s", addr)
|
||||
}
|
||||
|
||||
if host == "" {
|
||||
host = "0.0.0.0"
|
||||
}
|
||||
ip, err := net.ResolveIPAddr("ip", host)
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("unable to resolve %s: %s", host, err)
|
||||
}
|
||||
|
||||
port, err := strconv.Atoi(portStr)
|
||||
if err != nil || port < 0 || port > 65535 {
|
||||
return nil, 0, fmt.Errorf("bad port %s: %s", portStr, err)
|
||||
}
|
||||
|
||||
return ip, port, nil
|
||||
}
|
||||
|
||||
func UDPAddrFromString(addr string) (*net.UDPAddr, error) {
|
||||
ip, port, err := IPPortFromString(addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &net.UDPAddr{
|
||||
IP: ip.IP,
|
||||
Port: port,
|
||||
Zone: ip.Zone,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func TCPAddrFromString(addr string) (*net.TCPAddr, error) {
|
||||
ip, port, err := IPPortFromString(addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &net.TCPAddr{
|
||||
IP: ip.IP,
|
||||
Port: port,
|
||||
Zone: ip.Zone,
|
||||
}, nil
|
||||
}
|
41
pkg/clock/clock.go
Normal file
41
pkg/clock/clock.go
Normal file
|
@ -0,0 +1,41 @@
|
|||
// Copyright 2018 The Prometheus Authors
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package clock
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
var ClockInstance *Clock
|
||||
|
||||
type Clock struct {
|
||||
Instant time.Time
|
||||
TickerCh chan time.Time
|
||||
}
|
||||
|
||||
func Now() time.Time {
|
||||
if ClockInstance == nil {
|
||||
return time.Now()
|
||||
}
|
||||
return ClockInstance.Instant
|
||||
}
|
||||
|
||||
func NewTicker(d time.Duration) *time.Ticker {
|
||||
if ClockInstance == nil || ClockInstance.TickerCh == nil {
|
||||
return time.NewTicker(d)
|
||||
}
|
||||
return &time.Ticker{
|
||||
C: ClockInstance.TickerCh,
|
||||
}
|
||||
}
|
138
pkg/event/event.go
Normal file
138
pkg/event/event.go
Normal file
|
@ -0,0 +1,138 @@
|
|||
// Copyright 2013 The Prometheus Authors
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package event
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/statsd_exporter/pkg/clock"
|
||||
"github.com/prometheus/statsd_exporter/pkg/mapper"
|
||||
)
|
||||
|
||||
type Event interface {
|
||||
MetricName() string
|
||||
Value() float64
|
||||
Labels() map[string]string
|
||||
MetricType() mapper.MetricType
|
||||
}
|
||||
|
||||
type CounterEvent struct {
|
||||
CMetricName string
|
||||
CValue float64
|
||||
CLabels map[string]string
|
||||
}
|
||||
|
||||
func (c *CounterEvent) MetricName() string { return c.CMetricName }
|
||||
func (c *CounterEvent) Value() float64 { return c.CValue }
|
||||
func (c *CounterEvent) Labels() map[string]string { return c.CLabels }
|
||||
func (c *CounterEvent) MetricType() mapper.MetricType { return mapper.MetricTypeCounter }
|
||||
|
||||
type GaugeEvent struct {
|
||||
GMetricName string
|
||||
GValue float64
|
||||
GRelative bool
|
||||
GLabels map[string]string
|
||||
}
|
||||
|
||||
func (g *GaugeEvent) MetricName() string { return g.GMetricName }
|
||||
func (g *GaugeEvent) Value() float64 { return g.GValue }
|
||||
func (g *GaugeEvent) Labels() map[string]string { return g.GLabels }
|
||||
func (g *GaugeEvent) MetricType() mapper.MetricType { return mapper.MetricTypeGauge }
|
||||
|
||||
type ObserverEvent struct {
|
||||
OMetricName string
|
||||
OValue float64
|
||||
OLabels map[string]string
|
||||
}
|
||||
|
||||
func (o *ObserverEvent) MetricName() string { return o.OMetricName }
|
||||
func (o *ObserverEvent) Value() float64 { return o.OValue }
|
||||
func (o *ObserverEvent) Labels() map[string]string { return o.OLabels }
|
||||
func (o *ObserverEvent) MetricType() mapper.MetricType { return mapper.MetricTypeObserver }
|
||||
|
||||
type Events []Event
|
||||
|
||||
type EventQueue struct {
|
||||
C chan Events
|
||||
q Events
|
||||
m sync.Mutex
|
||||
flushTicker *time.Ticker
|
||||
flushThreshold int
|
||||
flushInterval time.Duration
|
||||
eventsFlushed prometheus.Counter
|
||||
}
|
||||
|
||||
type EventHandler interface {
|
||||
Queue(event Events)
|
||||
}
|
||||
|
||||
func NewEventQueue(c chan Events, flushThreshold int, flushInterval time.Duration, eventsFlushed prometheus.Counter) *EventQueue {
|
||||
ticker := clock.NewTicker(flushInterval)
|
||||
eq := &EventQueue{
|
||||
C: c,
|
||||
flushThreshold: flushThreshold,
|
||||
flushInterval: flushInterval,
|
||||
flushTicker: ticker,
|
||||
q: make([]Event, 0, flushThreshold),
|
||||
eventsFlushed: eventsFlushed,
|
||||
}
|
||||
go func() {
|
||||
for {
|
||||
<-ticker.C
|
||||
eq.Flush()
|
||||
}
|
||||
}()
|
||||
return eq
|
||||
}
|
||||
|
||||
func (eq *EventQueue) Queue(events Events) {
|
||||
eq.m.Lock()
|
||||
defer eq.m.Unlock()
|
||||
|
||||
for _, e := range events {
|
||||
eq.q = append(eq.q, e)
|
||||
if len(eq.q) >= eq.flushThreshold {
|
||||
eq.FlushUnlocked()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (eq *EventQueue) Flush() {
|
||||
eq.m.Lock()
|
||||
defer eq.m.Unlock()
|
||||
eq.FlushUnlocked()
|
||||
}
|
||||
|
||||
func (eq *EventQueue) FlushUnlocked() {
|
||||
eq.C <- eq.q
|
||||
eq.q = make([]Event, 0, cap(eq.q))
|
||||
eq.eventsFlushed.Inc()
|
||||
}
|
||||
|
||||
func (eq *EventQueue) Len() int {
|
||||
eq.m.Lock()
|
||||
defer eq.m.Unlock()
|
||||
|
||||
return len(eq.q)
|
||||
}
|
||||
|
||||
type UnbufferedEventHandler struct {
|
||||
C chan Events
|
||||
}
|
||||
|
||||
func (ueh *UnbufferedEventHandler) Queue(events Events) {
|
||||
ueh.C <- events
|
||||
}
|
87
pkg/event/event_test.go
Normal file
87
pkg/event/event_test.go
Normal file
|
@ -0,0 +1,87 @@
|
|||
// Copyright 2013 The Prometheus Authors
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package event
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/statsd_exporter/pkg/clock"
|
||||
)
|
||||
|
||||
var eventsFlushed = prometheus.NewCounter(
|
||||
prometheus.CounterOpts{
|
||||
Name: "statsd_exporter_event_queue_flushed_total",
|
||||
Help: "Number of times events were flushed to exporter",
|
||||
},
|
||||
)
|
||||
|
||||
func TestEventThresholdFlush(t *testing.T) {
|
||||
c := make(chan Events, 100)
|
||||
// We're not going to flush during this test, so the duration doesn't matter.
|
||||
eq := NewEventQueue(c, 5, time.Second, eventsFlushed)
|
||||
e := make(Events, 13)
|
||||
go func() {
|
||||
eq.Queue(e)
|
||||
}()
|
||||
|
||||
batch := <-c
|
||||
if len(batch) != 5 {
|
||||
t.Fatalf("Expected event batch to be 5 elements, but got %v", len(batch))
|
||||
}
|
||||
batch = <-c
|
||||
if len(batch) != 5 {
|
||||
t.Fatalf("Expected event batch to be 5 elements, but got %v", len(batch))
|
||||
}
|
||||
batch = <-c
|
||||
if len(batch) != 3 {
|
||||
t.Fatalf("Expected event batch to be 3 elements, but got %v", len(batch))
|
||||
}
|
||||
}
|
||||
|
||||
func TestEventIntervalFlush(t *testing.T) {
|
||||
// Mock a time.NewTicker
|
||||
tickerCh := make(chan time.Time)
|
||||
clock.ClockInstance = &clock.Clock{
|
||||
TickerCh: tickerCh,
|
||||
}
|
||||
clock.ClockInstance.Instant = time.Unix(0, 0)
|
||||
|
||||
c := make(chan Events, 100)
|
||||
eq := NewEventQueue(c, 1000, time.Second*1000, eventsFlushed)
|
||||
e := make(Events, 10)
|
||||
eq.Queue(e)
|
||||
|
||||
if eq.Len() != 10 {
|
||||
t.Fatal("Expected 10 events to be queued, but got", eq.Len())
|
||||
}
|
||||
|
||||
if len(eq.C) != 0 {
|
||||
t.Fatal("Expected 0 events in the event channel, but got", len(eq.C))
|
||||
}
|
||||
|
||||
// Tick time forward to trigger a flush
|
||||
clock.ClockInstance.Instant = time.Unix(10000, 0)
|
||||
clock.ClockInstance.TickerCh <- time.Unix(10000, 0)
|
||||
|
||||
events := <-eq.C
|
||||
if eq.Len() != 0 {
|
||||
t.Fatal("Expected 0 events to be queued, but got", eq.Len())
|
||||
}
|
||||
|
||||
if len(events) != 10 {
|
||||
t.Fatal("Expected 10 events in the event channel, but got", len(events))
|
||||
}
|
||||
}
|
212
pkg/exporter/exporter.go
Normal file
212
pkg/exporter/exporter.go
Normal file
|
@ -0,0 +1,212 @@
|
|||
// Copyright 2013 The Prometheus Authors
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package exporter
|
||||
|
||||
import (
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/go-kit/log"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
|
||||
"github.com/prometheus/statsd_exporter/pkg/clock"
|
||||
"github.com/prometheus/statsd_exporter/pkg/event"
|
||||
"github.com/prometheus/statsd_exporter/pkg/level"
|
||||
"github.com/prometheus/statsd_exporter/pkg/mapper"
|
||||
"github.com/prometheus/statsd_exporter/pkg/registry"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultHelp = "Metric autogenerated by statsd_exporter."
|
||||
regErrF = "Failed to update metric"
|
||||
)
|
||||
|
||||
type Registry interface {
|
||||
GetCounter(metricName string, labels prometheus.Labels, help string, mapping *mapper.MetricMapping, metricsCount *prometheus.GaugeVec) (prometheus.Counter, error)
|
||||
GetGauge(metricName string, labels prometheus.Labels, help string, mapping *mapper.MetricMapping, metricsCount *prometheus.GaugeVec) (prometheus.Gauge, error)
|
||||
GetHistogram(metricName string, labels prometheus.Labels, help string, mapping *mapper.MetricMapping, metricsCount *prometheus.GaugeVec) (prometheus.Observer, error)
|
||||
GetSummary(metricName string, labels prometheus.Labels, help string, mapping *mapper.MetricMapping, metricsCount *prometheus.GaugeVec) (prometheus.Observer, error)
|
||||
RemoveStaleMetrics()
|
||||
}
|
||||
|
||||
type Exporter struct {
|
||||
Mapper *mapper.MetricMapper
|
||||
Registry Registry
|
||||
Logger log.Logger
|
||||
EventsActions *prometheus.CounterVec
|
||||
EventsUnmapped prometheus.Counter
|
||||
ErrorEventStats *prometheus.CounterVec
|
||||
EventStats *prometheus.CounterVec
|
||||
ConflictingEventStats *prometheus.CounterVec
|
||||
MetricsCount *prometheus.GaugeVec
|
||||
}
|
||||
|
||||
// Listen handles all events sent to the given channel sequentially. It
|
||||
// terminates when the channel is closed.
|
||||
func (b *Exporter) Listen(e <-chan event.Events) {
|
||||
removeStaleMetricsTicker := clock.NewTicker(time.Second)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-removeStaleMetricsTicker.C:
|
||||
b.Registry.RemoveStaleMetrics()
|
||||
case events, ok := <-e:
|
||||
if !ok {
|
||||
level.Debug(b.Logger).Log("msg", "Channel is closed. Break out of Exporter.Listener.")
|
||||
removeStaleMetricsTicker.Stop()
|
||||
return
|
||||
}
|
||||
for _, event := range events {
|
||||
b.handleEvent(event)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// handleEvent processes a single Event according to the configured mapping.
|
||||
func (b *Exporter) handleEvent(thisEvent event.Event) {
|
||||
mapping, labels, present := b.Mapper.GetMapping(thisEvent.MetricName(), thisEvent.MetricType())
|
||||
if mapping == nil {
|
||||
mapping = &mapper.MetricMapping{}
|
||||
if b.Mapper.Defaults.Ttl != 0 {
|
||||
mapping.Ttl = b.Mapper.Defaults.Ttl
|
||||
}
|
||||
}
|
||||
|
||||
if mapping.Action == mapper.ActionTypeDrop {
|
||||
b.EventsActions.WithLabelValues("drop").Inc()
|
||||
return
|
||||
}
|
||||
|
||||
metricName := ""
|
||||
|
||||
help := defaultHelp
|
||||
if mapping.HelpText != "" {
|
||||
help = mapping.HelpText
|
||||
}
|
||||
|
||||
prometheusLabels := thisEvent.Labels()
|
||||
if present {
|
||||
if mapping.Name == "" {
|
||||
level.Debug(b.Logger).Log("msg", "The mapping generates an empty metric name", "metric_name", thisEvent.MetricName(), "match", mapping.Match)
|
||||
b.ErrorEventStats.WithLabelValues("empty_metric_name").Inc()
|
||||
return
|
||||
}
|
||||
metricName = mapper.EscapeMetricName(mapping.Name)
|
||||
for label, value := range labels {
|
||||
if _, ok := prometheusLabels[label]; mapping.HonorLabels && ok {
|
||||
continue
|
||||
}
|
||||
|
||||
prometheusLabels[label] = value
|
||||
}
|
||||
b.EventsActions.WithLabelValues(string(mapping.Action)).Inc()
|
||||
} else {
|
||||
b.EventsUnmapped.Inc()
|
||||
metricName = mapper.EscapeMetricName(thisEvent.MetricName())
|
||||
}
|
||||
|
||||
eventValue := thisEvent.Value()
|
||||
if mapping.Scale.Set {
|
||||
eventValue *= mapping.Scale.Val
|
||||
}
|
||||
|
||||
switch ev := thisEvent.(type) {
|
||||
case *event.CounterEvent:
|
||||
// We don't accept negative values for counters. Incrementing the counter with a negative number
|
||||
// will cause the exporter to panic. Instead we will warn and continue to the next event.
|
||||
if eventValue < 0.0 {
|
||||
level.Debug(b.Logger).Log("msg", "counter must be non-negative value", "metric", metricName, "event_value", eventValue)
|
||||
b.ErrorEventStats.WithLabelValues("illegal_negative_counter").Inc()
|
||||
return
|
||||
}
|
||||
|
||||
counter, err := b.Registry.GetCounter(metricName, prometheusLabels, help, mapping, b.MetricsCount)
|
||||
if err == nil {
|
||||
counter.Add(eventValue)
|
||||
b.EventStats.WithLabelValues("counter").Inc()
|
||||
} else {
|
||||
level.Debug(b.Logger).Log("msg", regErrF, "metric", metricName, "error", err)
|
||||
b.ConflictingEventStats.WithLabelValues("counter").Inc()
|
||||
}
|
||||
|
||||
case *event.GaugeEvent:
|
||||
gauge, err := b.Registry.GetGauge(metricName, prometheusLabels, help, mapping, b.MetricsCount)
|
||||
|
||||
if err == nil {
|
||||
if ev.GRelative {
|
||||
gauge.Add(eventValue)
|
||||
} else {
|
||||
gauge.Set(eventValue)
|
||||
}
|
||||
b.EventStats.WithLabelValues("gauge").Inc()
|
||||
} else {
|
||||
level.Debug(b.Logger).Log("msg", regErrF, "metric", metricName, "error", err)
|
||||
b.ConflictingEventStats.WithLabelValues("gauge").Inc()
|
||||
}
|
||||
|
||||
case *event.ObserverEvent:
|
||||
t := mapper.ObserverTypeDefault
|
||||
if mapping != nil {
|
||||
t = mapping.ObserverType
|
||||
}
|
||||
if t == mapper.ObserverTypeDefault {
|
||||
t = b.Mapper.Defaults.ObserverType
|
||||
}
|
||||
|
||||
switch t {
|
||||
case mapper.ObserverTypeHistogram:
|
||||
histogram, err := b.Registry.GetHistogram(metricName, prometheusLabels, help, mapping, b.MetricsCount)
|
||||
if err == nil {
|
||||
histogram.Observe(eventValue)
|
||||
b.EventStats.WithLabelValues("observer").Inc()
|
||||
} else {
|
||||
level.Debug(b.Logger).Log("msg", regErrF, "metric", metricName, "error", err)
|
||||
b.ConflictingEventStats.WithLabelValues("observer").Inc()
|
||||
}
|
||||
|
||||
case mapper.ObserverTypeDefault, mapper.ObserverTypeSummary:
|
||||
summary, err := b.Registry.GetSummary(metricName, prometheusLabels, help, mapping, b.MetricsCount)
|
||||
if err == nil {
|
||||
summary.Observe(eventValue)
|
||||
b.EventStats.WithLabelValues("observer").Inc()
|
||||
} else {
|
||||
level.Debug(b.Logger).Log("msg", regErrF, "metric", metricName, "error", err)
|
||||
b.ConflictingEventStats.WithLabelValues("observer").Inc()
|
||||
}
|
||||
|
||||
default:
|
||||
level.Error(b.Logger).Log("msg", "unknown observer type", "type", t)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
default:
|
||||
level.Debug(b.Logger).Log("msg", "Unsupported event type")
|
||||
b.EventStats.WithLabelValues("illegal").Inc()
|
||||
}
|
||||
}
|
||||
|
||||
func NewExporter(reg prometheus.Registerer, mapper *mapper.MetricMapper, logger log.Logger, eventsActions *prometheus.CounterVec, eventsUnmapped prometheus.Counter, errorEventStats *prometheus.CounterVec, eventStats *prometheus.CounterVec, conflictingEventStats *prometheus.CounterVec, metricsCount *prometheus.GaugeVec) *Exporter {
|
||||
return &Exporter{
|
||||
Mapper: mapper,
|
||||
Registry: registry.NewRegistry(reg, mapper),
|
||||
Logger: logger,
|
||||
EventsActions: eventsActions,
|
||||
EventsUnmapped: eventsUnmapped,
|
||||
ErrorEventStats: errorEventStats,
|
||||
EventStats: eventStats,
|
||||
ConflictingEventStats: conflictingEventStats,
|
||||
MetricsCount: metricsCount,
|
||||
}
|
||||
}
|
1339
pkg/exporter/exporter_test.go
Normal file
1339
pkg/exporter/exporter_test.go
Normal file
File diff suppressed because it is too large
Load diff
97
pkg/level/level.go
Normal file
97
pkg/level/level.go
Normal file
|
@ -0,0 +1,97 @@
|
|||
// Copyright 2021 The Prometheus Authors
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package level
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/go-kit/log"
|
||||
"github.com/go-kit/log/level"
|
||||
)
|
||||
|
||||
var logLevel = LevelInfo
|
||||
|
||||
// A Level is a logging priority. Higher levels are more important.
|
||||
type Level int
|
||||
|
||||
const (
|
||||
// LevelDebug logs are typically voluminous, and are usually disabled in
|
||||
// production.
|
||||
LevelDebug Level = iota
|
||||
// LevelInfo is the default logging priority.
|
||||
LevelInfo
|
||||
// LevelWarn logs are more important than Info, but don't need individual
|
||||
// human review.
|
||||
LevelWarn
|
||||
// LevelError logs are high-priority. If an application is running smoothly,
|
||||
// it shouldn't generate any error-level logs.
|
||||
LevelError
|
||||
)
|
||||
|
||||
var emptyLogger = &EmptyLogger{}
|
||||
|
||||
type EmptyLogger struct{}
|
||||
|
||||
func (l *EmptyLogger) Log(keyvals ...interface{}) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetLogLevel sets the log level.
|
||||
func SetLogLevel(level string) error {
|
||||
switch level {
|
||||
case "debug":
|
||||
logLevel = LevelDebug
|
||||
case "info":
|
||||
logLevel = LevelInfo
|
||||
case "warn":
|
||||
logLevel = LevelWarn
|
||||
case "error":
|
||||
logLevel = LevelError
|
||||
default:
|
||||
return fmt.Errorf("unrecognized log level %s", level)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Error returns a logger that includes a Key/ErrorValue pair.
|
||||
func Error(logger log.Logger) log.Logger {
|
||||
if logLevel <= LevelError {
|
||||
return level.Error(logger)
|
||||
}
|
||||
return emptyLogger
|
||||
}
|
||||
|
||||
// Warn returns a logger that includes a Key/WarnValue pair.
|
||||
func Warn(logger log.Logger) log.Logger {
|
||||
if logLevel <= LevelWarn {
|
||||
return level.Warn(logger)
|
||||
}
|
||||
return emptyLogger
|
||||
}
|
||||
|
||||
// Info returns a logger that includes a Key/InfoValue pair.
|
||||
func Info(logger log.Logger) log.Logger {
|
||||
if logLevel <= LevelInfo {
|
||||
return level.Info(logger)
|
||||
}
|
||||
return emptyLogger
|
||||
}
|
||||
|
||||
// Debug returns a logger that includes a Key/DebugValue pair.
|
||||
func Debug(logger log.Logger) log.Logger {
|
||||
if logLevel <= LevelDebug {
|
||||
return level.Debug(logger)
|
||||
}
|
||||
return emptyLogger
|
||||
}
|
110
pkg/level/level_test.go
Normal file
110
pkg/level/level_test.go
Normal file
|
@ -0,0 +1,110 @@
|
|||
// Copyright 2021 The Prometheus Authors
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package level
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/go-kit/log"
|
||||
)
|
||||
|
||||
func TestSetLogLevel(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
level string
|
||||
logLevel Level
|
||||
wantErr bool
|
||||
}{
|
||||
{"wrong level", "foo", LevelInfo, true},
|
||||
{"level debug", "debug", LevelDebug, false},
|
||||
{"level info", "info", LevelInfo, false},
|
||||
{"level warn", "warn", LevelWarn, false},
|
||||
{"level error", "error", LevelError, false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if err := SetLogLevel(tt.level); (err != nil) != tt.wantErr {
|
||||
t.Fatalf("Expected log level to be set successfully, but got %v", err)
|
||||
}
|
||||
if tt.logLevel != logLevel {
|
||||
t.Fatalf("Expected log level %v, but got %v", tt.logLevel, logLevel)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestVariousLevels(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
level string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
"level debug",
|
||||
"debug",
|
||||
strings.Join([]string{
|
||||
"level=debug log=debug",
|
||||
"level=info log=info",
|
||||
"level=warn log=warn",
|
||||
"level=error log=error",
|
||||
}, "\n"),
|
||||
},
|
||||
{
|
||||
"level info",
|
||||
"info",
|
||||
strings.Join([]string{
|
||||
"level=info log=info",
|
||||
"level=warn log=warn",
|
||||
"level=error log=error",
|
||||
}, "\n"),
|
||||
},
|
||||
{
|
||||
"level warn",
|
||||
"warn",
|
||||
strings.Join([]string{
|
||||
"level=warn log=warn",
|
||||
"level=error log=error",
|
||||
}, "\n"),
|
||||
},
|
||||
{
|
||||
"level error",
|
||||
"error",
|
||||
strings.Join([]string{
|
||||
"level=error log=error",
|
||||
}, "\n"),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
logger := log.NewLogfmtLogger(&buf)
|
||||
|
||||
if err := SetLogLevel(tt.level); err != nil {
|
||||
t.Fatalf("Expected log level to be set successfully, but got %v", err)
|
||||
}
|
||||
|
||||
Debug(logger).Log("log", "debug")
|
||||
Info(logger).Log("log", "info")
|
||||
Warn(logger).Log("log", "warn")
|
||||
Error(logger).Log("log", "error")
|
||||
|
||||
got := strings.TrimSpace(buf.String())
|
||||
if tt.want != got {
|
||||
t.Fatalf("Expected log output %v, but got %v", tt.want, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
316
pkg/line/line.go
Normal file
316
pkg/line/line.go
Normal file
|
@ -0,0 +1,316 @@
|
|||
// Copyright 2013 The Prometheus Authors
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package line
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/go-kit/log"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
|
||||
"github.com/prometheus/statsd_exporter/pkg/event"
|
||||
"github.com/prometheus/statsd_exporter/pkg/level"
|
||||
"github.com/prometheus/statsd_exporter/pkg/mapper"
|
||||
)
|
||||
|
||||
// Parser is a struct to hold configuration for parsing behavior
|
||||
type Parser struct {
|
||||
DogstatsdTagsEnabled bool
|
||||
InfluxdbTagsEnabled bool
|
||||
LibratoTagsEnabled bool
|
||||
SignalFXTagsEnabled bool
|
||||
}
|
||||
|
||||
// NewParser returns a new line parser
|
||||
func NewParser() *Parser {
|
||||
p := Parser{}
|
||||
return &p
|
||||
}
|
||||
|
||||
// EnableDogstatsdParsing option to enable dogstatsd tag parsing
|
||||
func (p *Parser) EnableDogstatsdParsing() {
|
||||
p.DogstatsdTagsEnabled = true
|
||||
}
|
||||
|
||||
// EnableInfluxdbParsing option to enable influxdb tag parsing
|
||||
func (p *Parser) EnableInfluxdbParsing() {
|
||||
p.InfluxdbTagsEnabled = true
|
||||
}
|
||||
|
||||
// EnableLibratoParsing option to enable librato tag parsing
|
||||
func (p *Parser) EnableLibratoParsing() {
|
||||
p.LibratoTagsEnabled = true
|
||||
}
|
||||
|
||||
// EnableSignalFXParsing option to enable signalfx tag parsing
|
||||
func (p *Parser) EnableSignalFXParsing() {
|
||||
p.SignalFXTagsEnabled = true
|
||||
}
|
||||
|
||||
func buildEvent(statType, metric string, value float64, relative bool, labels map[string]string) (event.Event, error) {
|
||||
switch statType {
|
||||
case "c":
|
||||
return &event.CounterEvent{
|
||||
CMetricName: metric,
|
||||
CValue: float64(value),
|
||||
CLabels: labels,
|
||||
}, nil
|
||||
case "g":
|
||||
return &event.GaugeEvent{
|
||||
GMetricName: metric,
|
||||
GValue: float64(value),
|
||||
GRelative: relative,
|
||||
GLabels: labels,
|
||||
}, nil
|
||||
case "ms":
|
||||
return &event.ObserverEvent{
|
||||
OMetricName: metric,
|
||||
OValue: float64(value) / 1000, // prometheus presumes seconds, statsd millisecond
|
||||
OLabels: labels,
|
||||
}, nil
|
||||
case "h", "d":
|
||||
return &event.ObserverEvent{
|
||||
OMetricName: metric,
|
||||
OValue: float64(value),
|
||||
OLabels: labels,
|
||||
}, nil
|
||||
case "s":
|
||||
return nil, fmt.Errorf("no support for StatsD sets")
|
||||
default:
|
||||
return nil, fmt.Errorf("bad stat type %s", statType)
|
||||
}
|
||||
}
|
||||
|
||||
func parseTag(component, tag string, separator rune, labels map[string]string, tagErrors prometheus.Counter, logger log.Logger) {
|
||||
// Entirely empty tag is an error
|
||||
if len(tag) == 0 {
|
||||
tagErrors.Inc()
|
||||
level.Debug(logger).Log("msg", "Empty name tag", "component", component)
|
||||
return
|
||||
}
|
||||
|
||||
for i, c := range tag {
|
||||
if c == separator {
|
||||
k := tag[:i]
|
||||
v := tag[i+1:]
|
||||
|
||||
if len(k) == 0 || len(v) == 0 {
|
||||
// Empty key or value is an error
|
||||
tagErrors.Inc()
|
||||
level.Debug(logger).Log("msg", "Malformed name tag", "k", k, "v", v, "component", component)
|
||||
} else {
|
||||
labels[mapper.EscapeMetricName(k)] = v
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Missing separator (no value) is an error
|
||||
tagErrors.Inc()
|
||||
level.Debug(logger).Log("msg", "Malformed name tag", "tag", tag, "component", component)
|
||||
}
|
||||
|
||||
func parseNameTags(component string, labels map[string]string, tagErrors prometheus.Counter, logger log.Logger) {
|
||||
lastTagEndIndex := 0
|
||||
for i, c := range component {
|
||||
if c == ',' {
|
||||
tag := component[lastTagEndIndex:i]
|
||||
lastTagEndIndex = i + 1
|
||||
parseTag(component, tag, '=', labels, tagErrors, logger)
|
||||
}
|
||||
}
|
||||
|
||||
// If we're not off the end of the string, add the last tag
|
||||
if lastTagEndIndex < len(component) {
|
||||
tag := component[lastTagEndIndex:]
|
||||
parseTag(component, tag, '=', labels, tagErrors, logger)
|
||||
}
|
||||
}
|
||||
|
||||
func trimLeftHash(s string) string {
|
||||
if s != "" && s[0] == '#' {
|
||||
return s[1:]
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func (p *Parser) ParseDogStatsDTags(component string, labels map[string]string, tagErrors prometheus.Counter, logger log.Logger) {
|
||||
if p.DogstatsdTagsEnabled {
|
||||
lastTagEndIndex := 0
|
||||
for i, c := range component {
|
||||
if c == ',' {
|
||||
tag := component[lastTagEndIndex:i]
|
||||
lastTagEndIndex = i + 1
|
||||
parseTag(component, trimLeftHash(tag), ':', labels, tagErrors, logger)
|
||||
}
|
||||
}
|
||||
|
||||
// If we're not off the end of the string, add the last tag
|
||||
if lastTagEndIndex < len(component) {
|
||||
tag := component[lastTagEndIndex:]
|
||||
parseTag(component, trimLeftHash(tag), ':', labels, tagErrors, logger)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Parser) parseNameAndTags(name string, labels map[string]string, tagErrors prometheus.Counter, logger log.Logger) string {
|
||||
if p.SignalFXTagsEnabled {
|
||||
// check for SignalFx tags first
|
||||
// `[` delimits start of tags by SignalFx
|
||||
// `]` delimits end of tags by SignalFx
|
||||
// https://docs.signalfx.com/en/latest/integrations/agent/monitors/collectd-statsd.html
|
||||
startIdx := strings.IndexRune(name, '[')
|
||||
endIdx := strings.IndexRune(name, ']')
|
||||
|
||||
switch {
|
||||
case startIdx != -1 && endIdx != -1:
|
||||
// good signalfx tags
|
||||
parseNameTags(name[startIdx+1:endIdx], labels, tagErrors, logger)
|
||||
return name[:startIdx] + name[endIdx+1:]
|
||||
case (startIdx != -1) != (endIdx != -1):
|
||||
// only one bracket, return unparsed
|
||||
level.Debug(logger).Log("msg", "invalid SignalFx tags, not parsing", "metric", name)
|
||||
tagErrors.Inc()
|
||||
return name
|
||||
}
|
||||
}
|
||||
|
||||
for i, c := range name {
|
||||
// `#` delimits start of tags by Librato
|
||||
// https://www.librato.com/docs/kb/collect/collection_agents/stastd/#stat-level-tags
|
||||
// `,` delimits start of tags by InfluxDB
|
||||
// https://www.influxdata.com/blog/getting-started-with-sending-statsd-metrics-to-telegraf-influxdb/#introducing-influx-statsd
|
||||
if (c == '#' && p.LibratoTagsEnabled) || (c == ',' && p.InfluxdbTagsEnabled) {
|
||||
parseNameTags(name[i+1:], labels, tagErrors, logger)
|
||||
return name[:i]
|
||||
}
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
||||
func (p *Parser) LineToEvents(line string, sampleErrors prometheus.CounterVec, samplesReceived prometheus.Counter, tagErrors prometheus.Counter, tagsReceived prometheus.Counter, logger log.Logger) event.Events {
|
||||
events := event.Events{}
|
||||
if line == "" {
|
||||
return events
|
||||
}
|
||||
|
||||
elements := strings.SplitN(line, ":", 2)
|
||||
if len(elements) < 2 || len(elements[0]) == 0 || !utf8.ValidString(line) {
|
||||
sampleErrors.WithLabelValues("malformed_line").Inc()
|
||||
level.Debug(logger).Log("msg", "Bad line from StatsD", "line", line)
|
||||
return events
|
||||
}
|
||||
|
||||
labels := map[string]string{}
|
||||
metric := p.parseNameAndTags(elements[0], labels, tagErrors, logger)
|
||||
|
||||
var samples []string
|
||||
if strings.Contains(elements[1], "|#") {
|
||||
// using DogStatsD tags
|
||||
|
||||
// don't allow mixed tagging styles
|
||||
if len(labels) > 0 {
|
||||
sampleErrors.WithLabelValues("mixed_tagging_styles").Inc()
|
||||
level.Debug(logger).Log("msg", "Bad line (multiple tagging styles) from StatsD", "line", line)
|
||||
return events
|
||||
}
|
||||
|
||||
// disable multi-metrics
|
||||
samples = elements[1:]
|
||||
} else {
|
||||
samples = strings.Split(elements[1], ":")
|
||||
}
|
||||
|
||||
samples:
|
||||
for _, sample := range samples {
|
||||
samplesReceived.Inc()
|
||||
components := strings.Split(sample, "|")
|
||||
if len(components) < 2 || len(components) > 4 {
|
||||
sampleErrors.WithLabelValues("malformed_component").Inc()
|
||||
level.Debug(logger).Log("msg", "Bad component", "line", line)
|
||||
continue
|
||||
}
|
||||
valueStr, statType := components[0], components[1]
|
||||
|
||||
var relative = false
|
||||
if strings.Index(valueStr, "+") == 0 || strings.Index(valueStr, "-") == 0 {
|
||||
relative = true
|
||||
}
|
||||
|
||||
value, err := strconv.ParseFloat(valueStr, 64)
|
||||
if err != nil {
|
||||
level.Debug(logger).Log("msg", "Bad value", "value", valueStr, "line", line)
|
||||
sampleErrors.WithLabelValues("malformed_value").Inc()
|
||||
continue
|
||||
}
|
||||
|
||||
multiplyEvents := 1
|
||||
if len(components) >= 3 {
|
||||
for _, component := range components[2:] {
|
||||
if len(component) == 0 {
|
||||
level.Debug(logger).Log("msg", "Empty component", "line", line)
|
||||
sampleErrors.WithLabelValues("malformed_component").Inc()
|
||||
continue samples
|
||||
}
|
||||
}
|
||||
|
||||
for _, component := range components[2:] {
|
||||
switch component[0] {
|
||||
case '@':
|
||||
|
||||
samplingFactor, err := strconv.ParseFloat(component[1:], 64)
|
||||
if err != nil {
|
||||
level.Debug(logger).Log("msg", "Invalid sampling factor", "component", component[1:], "line", line)
|
||||
sampleErrors.WithLabelValues("invalid_sample_factor").Inc()
|
||||
}
|
||||
if samplingFactor == 0 {
|
||||
samplingFactor = 1
|
||||
}
|
||||
|
||||
if statType == "g" {
|
||||
continue
|
||||
} else if statType == "c" {
|
||||
value /= samplingFactor
|
||||
} else if statType == "ms" || statType == "h" || statType == "d" {
|
||||
multiplyEvents = int(1 / samplingFactor)
|
||||
}
|
||||
case '#':
|
||||
p.ParseDogStatsDTags(component[1:], labels, tagErrors, logger)
|
||||
default:
|
||||
level.Debug(logger).Log("msg", "Invalid sampling factor or tag section", "component", components[2], "line", line)
|
||||
sampleErrors.WithLabelValues("invalid_sample_factor").Inc()
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(labels) > 0 {
|
||||
tagsReceived.Inc()
|
||||
}
|
||||
|
||||
for i := 0; i < multiplyEvents; i++ {
|
||||
event, err := buildEvent(statType, metric, value, relative, labels)
|
||||
if err != nil {
|
||||
level.Debug(logger).Log("msg", "Error building event", "line", line, "error", err)
|
||||
sampleErrors.WithLabelValues("illegal_event").Inc()
|
||||
continue
|
||||
}
|
||||
events = append(events, event)
|
||||
}
|
||||
}
|
||||
return events
|
||||
}
|
1703
pkg/line/line_test.go
Normal file
1703
pkg/line/line_test.go
Normal file
File diff suppressed because it is too large
Load diff
219
pkg/listener/listener.go
Normal file
219
pkg/listener/listener.go
Normal file
|
@ -0,0 +1,219 @@
|
|||
// Copyright 2013 The Prometheus Authors
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package listener
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"io"
|
||||
"net"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/go-kit/log"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
|
||||
"github.com/prometheus/statsd_exporter/pkg/event"
|
||||
"github.com/prometheus/statsd_exporter/pkg/level"
|
||||
"github.com/prometheus/statsd_exporter/pkg/relay"
|
||||
)
|
||||
|
||||
type Parser interface {
|
||||
LineToEvents(line string, sampleErrors prometheus.CounterVec, samplesReceived prometheus.Counter, tagErrors prometheus.Counter, tagsReceived prometheus.Counter, logger log.Logger) event.Events
|
||||
}
|
||||
|
||||
type StatsDUDPListener struct {
|
||||
Conn *net.UDPConn
|
||||
EventHandler event.EventHandler
|
||||
Logger log.Logger
|
||||
LineParser Parser
|
||||
UDPPackets prometheus.Counter
|
||||
UDPPacketDrops prometheus.Counter
|
||||
LinesReceived prometheus.Counter
|
||||
EventsFlushed prometheus.Counter
|
||||
Relay *relay.Relay
|
||||
SampleErrors prometheus.CounterVec
|
||||
SamplesReceived prometheus.Counter
|
||||
TagErrors prometheus.Counter
|
||||
TagsReceived prometheus.Counter
|
||||
UdpPacketQueue chan []byte
|
||||
}
|
||||
|
||||
func (l *StatsDUDPListener) SetEventHandler(eh event.EventHandler) {
|
||||
l.EventHandler = eh
|
||||
}
|
||||
|
||||
func (l *StatsDUDPListener) Listen() {
|
||||
buf := make([]byte, 65535)
|
||||
go l.ProcessUdpPacketQueue()
|
||||
for {
|
||||
n, _, err := l.Conn.ReadFromUDP(buf)
|
||||
if err != nil {
|
||||
// https://github.com/golang/go/issues/4373
|
||||
// ignore net: errClosing error as it will occur during shutdown
|
||||
if strings.HasSuffix(err.Error(), "use of closed network connection") {
|
||||
return
|
||||
}
|
||||
level.Error(l.Logger).Log("error", err)
|
||||
return
|
||||
}
|
||||
|
||||
l.EnqueueUdpPacket(buf, n)
|
||||
}
|
||||
}
|
||||
|
||||
func (l *StatsDUDPListener) EnqueueUdpPacket(packet []byte, n int) {
|
||||
l.UDPPackets.Inc()
|
||||
packetCopy := make([]byte, n)
|
||||
copy(packetCopy, packet)
|
||||
select {
|
||||
case l.UdpPacketQueue <- packetCopy:
|
||||
// do nothing
|
||||
default:
|
||||
l.UDPPacketDrops.Inc()
|
||||
}
|
||||
}
|
||||
|
||||
func (l *StatsDUDPListener) ProcessUdpPacketQueue() {
|
||||
for {
|
||||
packet := <-l.UdpPacketQueue
|
||||
l.HandlePacket(packet)
|
||||
}
|
||||
}
|
||||
|
||||
func (l *StatsDUDPListener) HandlePacket(packet []byte) {
|
||||
lines := strings.Split(string(packet), "\n")
|
||||
for _, line := range lines {
|
||||
level.Debug(l.Logger).Log("msg", "Incoming line", "proto", "udp", "line", line)
|
||||
l.LinesReceived.Inc()
|
||||
if l.Relay != nil && len(line) > 0 {
|
||||
l.Relay.RelayLine(line)
|
||||
}
|
||||
l.EventHandler.Queue(l.LineParser.LineToEvents(line, l.SampleErrors, l.SamplesReceived, l.TagErrors, l.TagsReceived, l.Logger))
|
||||
}
|
||||
}
|
||||
|
||||
type StatsDTCPListener struct {
|
||||
Conn *net.TCPListener
|
||||
EventHandler event.EventHandler
|
||||
Logger log.Logger
|
||||
LineParser Parser
|
||||
LinesReceived prometheus.Counter
|
||||
EventsFlushed prometheus.Counter
|
||||
Relay *relay.Relay
|
||||
SampleErrors prometheus.CounterVec
|
||||
SamplesReceived prometheus.Counter
|
||||
TagErrors prometheus.Counter
|
||||
TagsReceived prometheus.Counter
|
||||
TCPConnections prometheus.Counter
|
||||
TCPErrors prometheus.Counter
|
||||
TCPLineTooLong prometheus.Counter
|
||||
}
|
||||
|
||||
func (l *StatsDTCPListener) SetEventHandler(eh event.EventHandler) {
|
||||
l.EventHandler = eh
|
||||
}
|
||||
|
||||
func (l *StatsDTCPListener) Listen() {
|
||||
for {
|
||||
c, err := l.Conn.AcceptTCP()
|
||||
if err != nil {
|
||||
// https://github.com/golang/go/issues/4373
|
||||
// ignore net: errClosing error as it will occur during shutdown
|
||||
if strings.HasSuffix(err.Error(), "use of closed network connection") {
|
||||
return
|
||||
}
|
||||
level.Error(l.Logger).Log("msg", "AcceptTCP failed", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
go l.HandleConn(c)
|
||||
}
|
||||
}
|
||||
|
||||
func (l *StatsDTCPListener) HandleConn(c *net.TCPConn) {
|
||||
defer c.Close()
|
||||
|
||||
l.TCPConnections.Inc()
|
||||
|
||||
r := bufio.NewReader(c)
|
||||
for {
|
||||
line, isPrefix, err := r.ReadLine()
|
||||
if err != nil {
|
||||
if err != io.EOF {
|
||||
l.TCPErrors.Inc()
|
||||
level.Debug(l.Logger).Log("msg", "Read failed", "addr", c.RemoteAddr(), "error", err)
|
||||
}
|
||||
break
|
||||
}
|
||||
level.Debug(l.Logger).Log("msg", "Incoming line", "proto", "tcp", "line", string(line))
|
||||
if isPrefix {
|
||||
l.TCPLineTooLong.Inc()
|
||||
level.Debug(l.Logger).Log("msg", "Read failed: line too long", "addr", c.RemoteAddr())
|
||||
break
|
||||
}
|
||||
l.LinesReceived.Inc()
|
||||
if l.Relay != nil && len(line) > 0 {
|
||||
l.Relay.RelayLine(string(line))
|
||||
}
|
||||
l.EventHandler.Queue(l.LineParser.LineToEvents(string(line), l.SampleErrors, l.SamplesReceived, l.TagErrors, l.TagsReceived, l.Logger))
|
||||
}
|
||||
}
|
||||
|
||||
type StatsDUnixgramListener struct {
|
||||
Conn *net.UnixConn
|
||||
EventHandler event.EventHandler
|
||||
Logger log.Logger
|
||||
LineParser Parser
|
||||
UnixgramPackets prometheus.Counter
|
||||
LinesReceived prometheus.Counter
|
||||
EventsFlushed prometheus.Counter
|
||||
Relay *relay.Relay
|
||||
SampleErrors prometheus.CounterVec
|
||||
SamplesReceived prometheus.Counter
|
||||
TagErrors prometheus.Counter
|
||||
TagsReceived prometheus.Counter
|
||||
}
|
||||
|
||||
func (l *StatsDUnixgramListener) SetEventHandler(eh event.EventHandler) {
|
||||
l.EventHandler = eh
|
||||
}
|
||||
|
||||
func (l *StatsDUnixgramListener) Listen() {
|
||||
buf := make([]byte, 65535)
|
||||
for {
|
||||
n, _, err := l.Conn.ReadFromUnix(buf)
|
||||
if err != nil {
|
||||
// https://github.com/golang/go/issues/4373
|
||||
// ignore net: errClosing error as it will occur during shutdown
|
||||
if strings.HasSuffix(err.Error(), "use of closed network connection") {
|
||||
return
|
||||
}
|
||||
level.Error(l.Logger).Log(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
l.HandlePacket(buf[:n])
|
||||
}
|
||||
}
|
||||
|
||||
func (l *StatsDUnixgramListener) HandlePacket(packet []byte) {
|
||||
l.UnixgramPackets.Inc()
|
||||
lines := strings.Split(string(packet), "\n")
|
||||
for _, line := range lines {
|
||||
level.Debug(l.Logger).Log("msg", "Incoming line", "proto", "unixgram", "line", line)
|
||||
l.LinesReceived.Inc()
|
||||
if l.Relay != nil && len(line) > 0 {
|
||||
l.Relay.RelayLine(line)
|
||||
}
|
||||
l.EventHandler.Queue(l.LineParser.LineToEvents(line, l.SampleErrors, l.SamplesReceived, l.TagErrors, l.TagsReceived, l.Logger))
|
||||
}
|
||||
}
|
42
pkg/mapper/action.go
Normal file
42
pkg/mapper/action.go
Normal file
|
@ -0,0 +1,42 @@
|
|||
// Copyright 2018 The Prometheus Authors
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package mapper
|
||||
|
||||
import "fmt"
|
||||
|
||||
type ActionType string
|
||||
|
||||
const (
|
||||
ActionTypeMap ActionType = "map"
|
||||
ActionTypeDrop ActionType = "drop"
|
||||
ActionTypeDefault ActionType = ""
|
||||
)
|
||||
|
||||
func (t *ActionType) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
var v string
|
||||
|
||||
if err := unmarshal(&v); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch ActionType(v) {
|
||||
case ActionTypeDrop:
|
||||
*t = ActionTypeDrop
|
||||
case ActionTypeMap, ActionTypeDefault:
|
||||
*t = ActionTypeMap
|
||||
default:
|
||||
return fmt.Errorf("invalid action type %q", v)
|
||||
}
|
||||
return nil
|
||||
}
|
86
pkg/mapper/escape.go
Normal file
86
pkg/mapper/escape.go
Normal file
|
@ -0,0 +1,86 @@
|
|||
// Copyright 2020 The Prometheus Authors
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package mapper
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// EscapeMetricName replaces invalid characters in the metric name with "_"
|
||||
// Valid characters are a-z, A-Z, 0-9, and _
|
||||
func EscapeMetricName(metricName string) string {
|
||||
metricLen := len(metricName)
|
||||
if metricLen == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
escaped := false
|
||||
var sb strings.Builder
|
||||
// If a metric starts with a digit, allocate the memory and prepend an
|
||||
// underscore.
|
||||
if metricName[0] >= '0' && metricName[0] <= '9' {
|
||||
escaped = true
|
||||
sb.Grow(metricLen + 1)
|
||||
sb.WriteByte('_')
|
||||
}
|
||||
|
||||
// This is an character replacement method optimized for this limited
|
||||
// use case. It is much faster than using a regex.
|
||||
offset := 0
|
||||
|
||||
var prevChar rune
|
||||
|
||||
for i, c := range metricName {
|
||||
// Seek forward, skipping valid characters until we find one that needs
|
||||
// to be replaced, then add all the characters we've seen so far to the
|
||||
// string.Builder.
|
||||
if (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') ||
|
||||
(c >= '0' && c <= '9') || (c == '_') {
|
||||
// Character is valid, so skip over it without doing anything.
|
||||
} else {
|
||||
// Double-dashes are allowed if there is a corresponding mapping.
|
||||
// For consistency, double-dashes should also be allowed in the default case.
|
||||
if c == '-' && prevChar == '-' {
|
||||
offset = i + utf8.RuneLen(c)
|
||||
continue
|
||||
}
|
||||
|
||||
if !escaped {
|
||||
// Up until now we've been lazy and avoided actually allocating
|
||||
// memory. Unfortunately we've now determined this string needs
|
||||
// escaping, so allocate the buffer for the whole string.
|
||||
escaped = true
|
||||
sb.Grow(metricLen)
|
||||
}
|
||||
sb.WriteString(metricName[offset:i])
|
||||
offset = i + utf8.RuneLen(c)
|
||||
sb.WriteByte('_')
|
||||
}
|
||||
|
||||
prevChar = c
|
||||
}
|
||||
|
||||
if !escaped {
|
||||
// This is the happy path where nothing had to be escaped, so we can
|
||||
// avoid doing anything.
|
||||
return metricName
|
||||
}
|
||||
|
||||
if offset < metricLen {
|
||||
sb.WriteString(metricName[offset:])
|
||||
}
|
||||
|
||||
return sb.String()
|
||||
}
|
60
pkg/mapper/escape_test.go
Normal file
60
pkg/mapper/escape_test.go
Normal file
|
@ -0,0 +1,60 @@
|
|||
// Copyright 2020 The Prometheus Authors
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package mapper
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestEscapeMetricName(t *testing.T) {
|
||||
scenarios := map[string]string{
|
||||
"clean": "clean",
|
||||
"0starts_with_digit": "_0starts_with_digit",
|
||||
"with_underscore": "with_underscore",
|
||||
"with--doubledash": "with_doubledash",
|
||||
"with---multiple-dashes": "with_multiple_dashes",
|
||||
"with.dot": "with_dot",
|
||||
"with😱emoji": "with_emoji",
|
||||
"with.*.multiple": "with___multiple",
|
||||
"test.web-server.foo.bar": "test_web_server_foo_bar",
|
||||
"": "",
|
||||
}
|
||||
|
||||
for in, want := range scenarios {
|
||||
if got := EscapeMetricName(in); want != got {
|
||||
t.Errorf("expected `%s` to be escaped to `%s`, got `%s`", in, want, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkEscapeMetricName(b *testing.B) {
|
||||
scenarios := []string{
|
||||
"clean",
|
||||
"0starts_with_digit",
|
||||
"with_underscore",
|
||||
"with--doubledash",
|
||||
"with---multiple-dashes",
|
||||
"with.dot",
|
||||
"with😱emoji",
|
||||
"with.*.multiple",
|
||||
"test.web-server.foo.bar",
|
||||
"",
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
b.Run(s, func(b *testing.B) {
|
||||
for n := 0; n < b.N; n++ {
|
||||
EscapeMetricName(s)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
132
pkg/mapper/fsm/README.md
Normal file
132
pkg/mapper/fsm/README.md
Normal file
|
@ -0,0 +1,132 @@
|
|||
# FSM Mapping
|
||||
|
||||
## Overview
|
||||
|
||||
This package implements a fast and efficient algorithm for generic glob style
|
||||
string matching using a finite state machine (FSM).
|
||||
|
||||
### Source Hierachy
|
||||
|
||||
```
|
||||
'-- fsm
|
||||
'-- dump.go // functionality to dump the FSM to Dot file
|
||||
'-- formatter.go // format glob templates using captured * groups
|
||||
'-- fsm.go // manipulating and searching of FSM
|
||||
'-- minmax.go // min() max() function for interger
|
||||
```
|
||||
|
||||
## FSM Explained
|
||||
|
||||
Per [Wikipedia](https://en.wikipedia.org/wiki/Finite-state_machine):
|
||||
|
||||
> A finite-state machine (FSM) or finite-state automaton (FSA, plural: automata),
|
||||
> finite automaton, or simply a state machine, is a mathematical model of
|
||||
> computation. It is an abstract machine that can be in exactly one of a finite
|
||||
> number of states at any given time. The FSM can change from one state to
|
||||
> another in response to some external inputs; the change from one state to
|
||||
> another is called a transition. An FSM is defined by a list of its states, its
|
||||
> initial state, and the conditions for each transition.
|
||||
|
||||
In our use case, each *state* is a substring after the input StatsD metric name is splitted by `.`.
|
||||
|
||||
### Add state to FSM
|
||||
|
||||
`func (f *FSM) AddState(match string, matchMetricType string,
|
||||
maxPossibleTransitions int, result interface{}) int`
|
||||
|
||||
At first, the FSM only contains three states, representing three possible metric types:
|
||||
|
||||
____ [gauge]
|
||||
/
|
||||
(start)---- [counter]
|
||||
\
|
||||
'--- [observer]
|
||||
|
||||
|
||||
Adding a rule `client.*.request.count` with type `counter` will make the FSM to be:
|
||||
|
||||
|
||||
____ [gauge]
|
||||
/
|
||||
(start)---- [counter] -- [client] -- [*] -- [request] -- [count] -- {R1}
|
||||
\
|
||||
'--- [observer]
|
||||
|
||||
`{R1}` is short for result 1, which is the match result for `client.*.request.count`.
|
||||
|
||||
Adding a rule `client.*.*.size` with type `counter` will make the FSM to be:
|
||||
|
||||
____ [gauge] __ [request] -- [count] -- {R1}
|
||||
/ /
|
||||
(start)---- [counter] -- [client] -- [*]
|
||||
\ \__ [*] -- [size] -- {R2}
|
||||
'--- [observer]
|
||||
|
||||
|
||||
### Finding a result state in FSM
|
||||
|
||||
`func (f *FSM) GetMapping(statsdMetric string, statsdMetricType string)
|
||||
(*mappingState, []string)`
|
||||
|
||||
For example, when mapping `client.aaa.request.count` with `counter` type in the
|
||||
FSM, the `^1` to `^7` symbols indicate how FSM will traversal in its tree:
|
||||
|
||||
|
||||
____ [gauge] __ [request] -- [count] -- {R1}
|
||||
/ / ^5 ^6 ^7
|
||||
(start)---- [counter] -- [client] -- [*]
|
||||
^1 \ ^2 ^3 \__ [*] -- [size] -- {R2}
|
||||
'--- [observer] ^4
|
||||
|
||||
|
||||
To map `client.bbb.request.size`, FSM will do a backtracking:
|
||||
|
||||
|
||||
____ [gauge] __ [request] -- [count] -- {R1}
|
||||
/ / ^5 ^6
|
||||
(start)---- [counter] -- [client] -- [*]
|
||||
^1 \ ^2 ^3 \__ [*] -- [size] -- {R2}
|
||||
'--- [observer] ^4
|
||||
^7 ^8 ^9
|
||||
|
||||
|
||||
## Debugging
|
||||
|
||||
To see all the states of the current FSM, use `func (f *FSM) DumpFSM(w io.Writer)`
|
||||
to dump into a Dot file. The Dot file can be further renderer into image using:
|
||||
|
||||
```shell
|
||||
$ dot -Tpng dump.dot > dump.png
|
||||
```
|
||||
|
||||
In StatsD exporter, one could use the following:
|
||||
|
||||
```shell
|
||||
$ statsd_exporter --statsd.mapping-config=statsd.rules --debug.dump-fsm=dump.dot
|
||||
$ dot -Tpng dump.dot > dump.png
|
||||
```
|
||||
|
||||
For example, the following rules:
|
||||
|
||||
```yaml
|
||||
mappings:
|
||||
- match: client.*.request.count
|
||||
name: request_count
|
||||
match_metric_type: counter
|
||||
labels:
|
||||
client: $1
|
||||
|
||||
- match: client.*.*.size
|
||||
name: sizes
|
||||
match_metric_type: counter
|
||||
labels:
|
||||
client: $1
|
||||
direction: $2
|
||||
```
|
||||
|
||||
will be rendered as:
|
||||
|
||||
![FSM](fsm.png)
|
||||
|
||||
The `dot` program is part of [Graphviz](https://www.graphviz.org/) and is
|
||||
available in most of popular operating systems.
|
48
pkg/mapper/fsm/dump.go
Normal file
48
pkg/mapper/fsm/dump.go
Normal file
|
@ -0,0 +1,48 @@
|
|||
// Copyright 2018 The Prometheus Authors
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package fsm
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
)
|
||||
|
||||
// DumpFSM accepts a io.writer and write the current FSM into dot file format.
|
||||
func (f *FSM) DumpFSM(w io.Writer) {
|
||||
idx := 0
|
||||
states := make(map[int]*mappingState)
|
||||
states[idx] = f.root
|
||||
|
||||
w.Write([]byte("digraph g {\n"))
|
||||
w.Write([]byte("rankdir=LR\n")) // make it vertical
|
||||
w.Write([]byte("node [ label=\"\",style=filled,fillcolor=white,shape=circle ]\n")) // remove label of node
|
||||
|
||||
for idx < len(states) {
|
||||
for field, transition := range states[idx].transitions {
|
||||
states[len(states)] = transition
|
||||
w.Write([]byte(fmt.Sprintf("%d -> %d [label = \"%s\"];\n", idx, len(states)-1, field)))
|
||||
if idx == 0 {
|
||||
// color for metric types
|
||||
w.Write([]byte(fmt.Sprintf("%d [color=\"#D6B656\",fillcolor=\"#FFF2CC\"];\n", len(states)-1)))
|
||||
} else if transition.transitions == nil || len(transition.transitions) == 0 {
|
||||
// color for end state
|
||||
w.Write([]byte(fmt.Sprintf("%d [color=\"#82B366\",fillcolor=\"#D5E8D4\"];\n", len(states)-1)))
|
||||
}
|
||||
}
|
||||
idx++
|
||||
}
|
||||
// color for start state
|
||||
w.Write([]byte(fmt.Sprintln("0 [color=\"#a94442\",fillcolor=\"#f2dede\"];")))
|
||||
w.Write([]byte("}"))
|
||||
}
|
76
pkg/mapper/fsm/formatter.go
Normal file
76
pkg/mapper/fsm/formatter.go
Normal file
|
@ -0,0 +1,76 @@
|
|||
// Copyright 2018 The Prometheus Authors
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package fsm
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
templateReplaceCaptureRE = regexp.MustCompile(`\$\{?([a-zA-Z0-9_\$]+)\}?`)
|
||||
)
|
||||
|
||||
type TemplateFormatter struct {
|
||||
captureIndexes []int
|
||||
captureCount int
|
||||
fmtString string
|
||||
}
|
||||
|
||||
// NewTemplateFormatter instantiates a TemplateFormatter
|
||||
// from given template string and the maximum amount of captures.
|
||||
func NewTemplateFormatter(template string, captureCount int) *TemplateFormatter {
|
||||
matches := templateReplaceCaptureRE.FindAllStringSubmatch(template, -1)
|
||||
if len(matches) == 0 {
|
||||
// if no regex reference found, keep it as it is
|
||||
return &TemplateFormatter{captureCount: 0, fmtString: template}
|
||||
}
|
||||
|
||||
var indexes []int
|
||||
valueFormatter := template
|
||||
for _, match := range matches {
|
||||
idx, err := strconv.Atoi(match[len(match)-1])
|
||||
if err != nil || idx > captureCount || idx < 1 {
|
||||
// if index larger than captured count or using unsupported named capture group,
|
||||
// replace with empty string
|
||||
valueFormatter = strings.Replace(valueFormatter, match[0], "", -1)
|
||||
} else {
|
||||
valueFormatter = strings.Replace(valueFormatter, match[0], "%s", -1)
|
||||
// note: the regex reference variable $? starts from 1
|
||||
indexes = append(indexes, idx-1)
|
||||
}
|
||||
}
|
||||
return &TemplateFormatter{
|
||||
captureIndexes: indexes,
|
||||
captureCount: len(indexes),
|
||||
fmtString: valueFormatter,
|
||||
}
|
||||
}
|
||||
|
||||
// Format accepts a list containing captured strings and returns the formatted
|
||||
// string using the template stored in current TemplateFormatter.
|
||||
func (formatter *TemplateFormatter) Format(captures []string) string {
|
||||
if formatter.captureCount == 0 {
|
||||
// no label substitution, keep as it is
|
||||
return formatter.fmtString
|
||||
}
|
||||
indexes := formatter.captureIndexes
|
||||
vargs := make([]interface{}, formatter.captureCount)
|
||||
for i, idx := range indexes {
|
||||
vargs[i] = captures[idx]
|
||||
}
|
||||
return fmt.Sprintf(formatter.fmtString, vargs...)
|
||||
}
|
326
pkg/mapper/fsm/fsm.go
Normal file
326
pkg/mapper/fsm/fsm.go
Normal file
|
@ -0,0 +1,326 @@
|
|||
// Copyright 2018 The Prometheus Authors
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package fsm
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/go-kit/log"
|
||||
|
||||
"github.com/prometheus/statsd_exporter/pkg/level"
|
||||
)
|
||||
|
||||
type mappingState struct {
|
||||
transitions map[string]*mappingState
|
||||
minRemainingLength int
|
||||
maxRemainingLength int
|
||||
// result* members are nil unless there's a metric ends with this state
|
||||
Result interface{}
|
||||
ResultPriority int
|
||||
}
|
||||
|
||||
type fsmBacktrackStackCursor struct {
|
||||
fieldIndex int
|
||||
captureIndex int
|
||||
currentCapture string
|
||||
state *mappingState
|
||||
prev *fsmBacktrackStackCursor
|
||||
next *fsmBacktrackStackCursor
|
||||
}
|
||||
|
||||
type FSM struct {
|
||||
root *mappingState
|
||||
metricTypes []string
|
||||
statesCount int
|
||||
BacktrackingNeeded bool
|
||||
OrderingDisabled bool
|
||||
}
|
||||
|
||||
// NewFSM creates a new FSM instance
|
||||
func NewFSM(metricTypes []string, maxPossibleTransitions int, orderingDisabled bool) *FSM {
|
||||
fsm := FSM{}
|
||||
root := &mappingState{}
|
||||
root.transitions = make(map[string]*mappingState, len(metricTypes))
|
||||
|
||||
for _, field := range metricTypes {
|
||||
state := &mappingState{}
|
||||
(*state).transitions = make(map[string]*mappingState, maxPossibleTransitions)
|
||||
root.transitions[string(field)] = state
|
||||
}
|
||||
fsm.OrderingDisabled = orderingDisabled
|
||||
fsm.metricTypes = metricTypes
|
||||
fsm.statesCount = 0
|
||||
fsm.root = root
|
||||
return &fsm
|
||||
}
|
||||
|
||||
// AddState adds a mapping rule into the existing FSM.
|
||||
// The maxPossibleTransitions parameter sets the expected count of transitions left.
|
||||
// The result parameter sets the generic type to be returned when fsm found a match in GetMapping.
|
||||
func (f *FSM) AddState(match string, matchMetricType string, maxPossibleTransitions int, result interface{}) int {
|
||||
// first split by "."
|
||||
matchFields := strings.Split(match, ".")
|
||||
// fill into our FSM
|
||||
roots := []*mappingState{}
|
||||
// first state is the metric type
|
||||
if matchMetricType == "" {
|
||||
// if metricType not specified, connect the start state from all three types
|
||||
for _, metricType := range f.metricTypes {
|
||||
roots = append(roots, f.root.transitions[string(metricType)])
|
||||
}
|
||||
} else {
|
||||
roots = append(roots, f.root.transitions[matchMetricType])
|
||||
}
|
||||
var captureCount int
|
||||
var finalStates []*mappingState
|
||||
// iterating over different start state (different metric types)
|
||||
for _, root := range roots {
|
||||
captureCount = 0
|
||||
// for each start state, connect from start state to end state
|
||||
for i, field := range matchFields {
|
||||
state, prs := root.transitions[field]
|
||||
if !prs {
|
||||
// create a state if it's not exist in the fsm
|
||||
state = &mappingState{}
|
||||
(*state).transitions = make(map[string]*mappingState, maxPossibleTransitions)
|
||||
(*state).maxRemainingLength = len(matchFields) - i - 1
|
||||
(*state).minRemainingLength = len(matchFields) - i - 1
|
||||
root.transitions[field] = state
|
||||
// if this is last field, set result to currentMapping instance
|
||||
if i == len(matchFields)-1 {
|
||||
root.transitions[field].Result = result
|
||||
}
|
||||
} else {
|
||||
(*state).maxRemainingLength = max(len(matchFields)-i-1, (*state).maxRemainingLength)
|
||||
(*state).minRemainingLength = min(len(matchFields)-i-1, (*state).minRemainingLength)
|
||||
}
|
||||
if field == "*" {
|
||||
captureCount++
|
||||
}
|
||||
|
||||
// goto next state
|
||||
root = state
|
||||
}
|
||||
finalStates = append(finalStates, root)
|
||||
}
|
||||
|
||||
for _, state := range finalStates {
|
||||
state.ResultPriority = f.statesCount
|
||||
}
|
||||
|
||||
f.statesCount++
|
||||
|
||||
return captureCount
|
||||
}
|
||||
|
||||
// GetMapping using the fsm to find matching rules according to given statsdMetric and statsdMetricType.
|
||||
// If it finds a match, the final state and the captured strings are returned;
|
||||
// if there's no match found, nil and a empty list will be returned.
|
||||
func (f *FSM) GetMapping(statsdMetric string, statsdMetricType string) (*mappingState, []string) {
|
||||
matchFields := strings.Split(statsdMetric, ".")
|
||||
currentState := f.root.transitions[statsdMetricType]
|
||||
|
||||
// the cursor/pointer in the backtrack stack implemented as a double-linked list
|
||||
var backtrackCursor *fsmBacktrackStackCursor
|
||||
resumeFromBacktrack := false
|
||||
|
||||
// the return variable
|
||||
var finalState *mappingState
|
||||
|
||||
captures := make([]string, len(matchFields))
|
||||
finalCaptures := make([]string, len(matchFields))
|
||||
// keep track of captured group so we don't need to do append() on captures
|
||||
captureIdx := 0
|
||||
filedsCount := len(matchFields)
|
||||
i := 0
|
||||
var state *mappingState
|
||||
for { // the loop for backtracking
|
||||
for { // the loop for a single "depth only" search
|
||||
var present bool
|
||||
// if we resume from backtrack, we should skip this branch in this case
|
||||
// since the state that were saved at the end of this branch
|
||||
if !resumeFromBacktrack {
|
||||
if len(currentState.transitions) > 0 {
|
||||
field := matchFields[i]
|
||||
state, present = currentState.transitions[field]
|
||||
fieldsLeft := filedsCount - i - 1
|
||||
// also compare length upfront to avoid unnecessary loop or backtrack
|
||||
if !present || fieldsLeft > state.maxRemainingLength || fieldsLeft < state.minRemainingLength {
|
||||
state, present = currentState.transitions["*"]
|
||||
if !present || fieldsLeft > state.maxRemainingLength || fieldsLeft < state.minRemainingLength {
|
||||
break
|
||||
} else {
|
||||
captures[captureIdx] = field
|
||||
captureIdx++
|
||||
}
|
||||
} else if f.BacktrackingNeeded {
|
||||
// if backtracking is needed, also check for alternative transition, i.e. *
|
||||
altState, present := currentState.transitions["*"]
|
||||
if !present || fieldsLeft > altState.maxRemainingLength || fieldsLeft < altState.minRemainingLength {
|
||||
} else {
|
||||
// push to backtracking stack
|
||||
newCursor := fsmBacktrackStackCursor{prev: backtrackCursor, state: altState,
|
||||
fieldIndex: i,
|
||||
captureIndex: captureIdx, currentCapture: field,
|
||||
}
|
||||
// if this is not the first time, connect to the previous cursor
|
||||
if backtrackCursor != nil {
|
||||
backtrackCursor.next = &newCursor
|
||||
}
|
||||
backtrackCursor = &newCursor
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// no more transitions for this state
|
||||
break
|
||||
}
|
||||
} // backtrack will resume from here
|
||||
|
||||
// do we reach a final state?
|
||||
if state.Result != nil && i == filedsCount-1 {
|
||||
if f.OrderingDisabled {
|
||||
finalState = state
|
||||
return finalState, captures
|
||||
} else if finalState == nil || finalState.ResultPriority > state.ResultPriority {
|
||||
// if we care about ordering, try to find a result with highest prioity
|
||||
finalState = state
|
||||
// do a deep copy to preserve current captures
|
||||
copy(finalCaptures, captures)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
i++
|
||||
if i >= filedsCount {
|
||||
break
|
||||
}
|
||||
|
||||
resumeFromBacktrack = false
|
||||
currentState = state
|
||||
}
|
||||
if backtrackCursor == nil {
|
||||
// if we are not doing backtracking or all path has been travesaled
|
||||
break
|
||||
} else {
|
||||
// pop one from stack
|
||||
state = backtrackCursor.state
|
||||
currentState = state
|
||||
i = backtrackCursor.fieldIndex
|
||||
captureIdx = backtrackCursor.captureIndex + 1
|
||||
// put the * capture back
|
||||
captures[captureIdx-1] = backtrackCursor.currentCapture
|
||||
backtrackCursor = backtrackCursor.prev
|
||||
if backtrackCursor != nil {
|
||||
// deref for GC
|
||||
backtrackCursor.next = nil
|
||||
}
|
||||
resumeFromBacktrack = true
|
||||
}
|
||||
}
|
||||
return finalState, finalCaptures
|
||||
}
|
||||
|
||||
// TestIfNeedBacktracking tests if backtrack is needed for given list of mappings
|
||||
// and whether ordering is disabled.
|
||||
func TestIfNeedBacktracking(mappings []string, orderingDisabled bool, logger log.Logger) bool {
|
||||
backtrackingNeeded := false
|
||||
// A has * in rules, but there's other transisitions at the same state,
|
||||
// this makes A the cause of backtracking
|
||||
ruleByLength := make(map[int][]string)
|
||||
ruleREByLength := make(map[int][]*regexp.Regexp)
|
||||
|
||||
// first sort rules by length
|
||||
for _, mapping := range mappings {
|
||||
l := len(strings.Split(mapping, "."))
|
||||
ruleByLength[l] = append(ruleByLength[l], mapping)
|
||||
|
||||
metricRe := strings.Replace(mapping, ".", "\\.", -1)
|
||||
metricRe = strings.Replace(metricRe, "*", "([^.]*)", -1)
|
||||
regex, err := regexp.Compile("^" + metricRe + "$")
|
||||
if err != nil {
|
||||
level.Warn(logger).Log("msg", "Invalid match, cannot compile regex in mapping", "mapping", mapping, "err", err)
|
||||
}
|
||||
// put into array no matter there's error or not, we will skip later if regex is nil
|
||||
ruleREByLength[l] = append(ruleREByLength[l], regex)
|
||||
}
|
||||
|
||||
for l, rules := range ruleByLength {
|
||||
if len(rules) == 1 {
|
||||
continue
|
||||
}
|
||||
rulesRE := ruleREByLength[l]
|
||||
for i1, r1 := range rules {
|
||||
currentRuleNeedBacktrack := false
|
||||
re1 := rulesRE[i1]
|
||||
if re1 == nil || !strings.Contains(r1, "*") {
|
||||
continue
|
||||
}
|
||||
// if rule r1 is A.B.C.*.E.*, is there a rule r2 is A.B.C.D.x.x or A.B.C.*.E.F ? (x is any string or *)
|
||||
// if such r2 exists, then to match r1 we will need backtracking
|
||||
for index := 0; index < len(r1); index++ {
|
||||
if r1[index] != '*' {
|
||||
continue
|
||||
}
|
||||
// translate the substring of r1 from 0 to the index of current * into regex
|
||||
// A.B.C.*.E.* will becomes ^A\.B\.C\. and ^A\.B\.C\.\*\.E\.
|
||||
reStr := strings.Replace(r1[:index], ".", "\\.", -1)
|
||||
reStr = strings.Replace(reStr, "*", "\\*", -1)
|
||||
re := regexp.MustCompile("^" + reStr)
|
||||
for i2, r2 := range rules {
|
||||
if i2 == i1 {
|
||||
continue
|
||||
}
|
||||
if len(re.FindStringSubmatchIndex(r2)) > 0 {
|
||||
currentRuleNeedBacktrack = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for i2, r2 := range rules {
|
||||
if i2 != i1 && len(re1.FindStringSubmatchIndex(r2)) > 0 {
|
||||
// log if we care about ordering and the superset occurs before
|
||||
if !orderingDisabled && i1 < i2 {
|
||||
level.Warn(logger).Log("msg", "match is a super set of match but in a lower order, the first will never be matched", "first_match", r1, "second_match", r2)
|
||||
}
|
||||
currentRuleNeedBacktrack = false
|
||||
}
|
||||
}
|
||||
for i2, re2 := range rulesRE {
|
||||
if i2 == i1 || re2 == nil {
|
||||
continue
|
||||
}
|
||||
// if r1 is a subset of other rule, we don't need backtrack
|
||||
// because either we turned on ordering
|
||||
// or we disabled ordering and can't match it even with backtrack
|
||||
if len(re2.FindStringSubmatchIndex(r1)) > 0 {
|
||||
currentRuleNeedBacktrack = false
|
||||
}
|
||||
}
|
||||
|
||||
if currentRuleNeedBacktrack {
|
||||
level.Warn(logger).Log("msg", "backtracking required because of match. Performance may be degraded", "match", r1)
|
||||
backtrackingNeeded = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// backtracking will always be needed if ordering of rules is not disabled
|
||||
// since transistions are stored in (unordered) map
|
||||
// note: don't move this branch to the beginning of this function
|
||||
// since we need logs for superset rules
|
||||
|
||||
return !orderingDisabled || backtrackingNeeded
|
||||
}
|
BIN
pkg/mapper/fsm/fsm.png
Normal file
BIN
pkg/mapper/fsm/fsm.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 33 KiB |
30
pkg/mapper/fsm/minmax.go
Normal file
30
pkg/mapper/fsm/minmax.go
Normal file
|
@ -0,0 +1,30 @@
|
|||
// Copyright 2018 The Prometheus Authors
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package fsm
|
||||
|
||||
// min and max implementation for integer
|
||||
|
||||
func min(x, y int) int {
|
||||
if x < y {
|
||||
return x
|
||||
}
|
||||
return y
|
||||
}
|
||||
|
||||
func max(x, y int) int {
|
||||
if x > y {
|
||||
return x
|
||||
}
|
||||
return y
|
||||
}
|
394
pkg/mapper/mapper.go
Normal file
394
pkg/mapper/mapper.go
Normal file
|
@ -0,0 +1,394 @@
|
|||
// Copyright 2013 The Prometheus Authors
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package mapper
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"regexp"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/go-kit/log"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"gopkg.in/yaml.v2"
|
||||
|
||||
"github.com/prometheus/statsd_exporter/pkg/level"
|
||||
"github.com/prometheus/statsd_exporter/pkg/mapper/fsm"
|
||||
)
|
||||
|
||||
var (
|
||||
// The first segment of a match cannot start with a number
|
||||
statsdMetricRE = `[a-zA-Z_]([a-zA-Z0-9_\-])*`
|
||||
// The subsequent segments of a match can start with a number
|
||||
// See https://github.com/prometheus/statsd_exporter/issues/328
|
||||
statsdMetricSubsequentRE = `[a-zA-Z0-9_]([a-zA-Z0-9_\-])*`
|
||||
templateReplaceRE = `(\$\{?\d+\}?)`
|
||||
|
||||
metricLineRE = regexp.MustCompile(`^(\*|` + statsdMetricRE + `)(\.\*|\.` + statsdMetricSubsequentRE + `)*$`)
|
||||
metricNameRE = regexp.MustCompile(`^([a-zA-Z_]|` + templateReplaceRE + `)([a-zA-Z0-9_]|` + templateReplaceRE + `)*$`)
|
||||
labelNameRE = regexp.MustCompile(`^[a-zA-Z_][a-zA-Z0-9_]+$`)
|
||||
)
|
||||
|
||||
type MetricMapper struct {
|
||||
Registerer prometheus.Registerer
|
||||
Defaults MapperConfigDefaults `yaml:"defaults"`
|
||||
Mappings []MetricMapping `yaml:"mappings"`
|
||||
FSM *fsm.FSM
|
||||
doFSM bool
|
||||
doRegex bool
|
||||
cache MetricMapperCache
|
||||
mutex sync.RWMutex
|
||||
|
||||
MappingsCount prometheus.Gauge
|
||||
|
||||
Logger log.Logger
|
||||
}
|
||||
|
||||
type SummaryOptions struct {
|
||||
Quantiles []MetricObjective `yaml:"quantiles"`
|
||||
MaxAge time.Duration `yaml:"max_age"`
|
||||
AgeBuckets uint32 `yaml:"age_buckets"`
|
||||
BufCap uint32 `yaml:"buf_cap"`
|
||||
}
|
||||
|
||||
type HistogramOptions struct {
|
||||
Buckets []float64 `yaml:"buckets"`
|
||||
NativeHistogramBucketFactor float64 `yaml:"native_histogram_bucket_factor"`
|
||||
NativeHistogramMaxBuckets uint32 `yaml:"native_histogram_max_buckets"`
|
||||
}
|
||||
|
||||
type MetricObjective struct {
|
||||
Quantile float64 `yaml:"quantile"`
|
||||
Error float64 `yaml:"error"`
|
||||
}
|
||||
|
||||
var defaultQuantiles = []MetricObjective{
|
||||
{Quantile: 0.5, Error: 0.05},
|
||||
{Quantile: 0.9, Error: 0.01},
|
||||
{Quantile: 0.99, Error: 0.001},
|
||||
}
|
||||
|
||||
func (m *MetricMapper) InitFromYAMLString(fileContents string) error {
|
||||
var n MetricMapper
|
||||
|
||||
if err := yaml.Unmarshal([]byte(fileContents), &n); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(n.Defaults.HistogramOptions.Buckets) == 0 {
|
||||
n.Defaults.HistogramOptions.Buckets = prometheus.DefBuckets
|
||||
}
|
||||
if n.Defaults.HistogramOptions.NativeHistogramBucketFactor == 0 {
|
||||
n.Defaults.HistogramOptions.NativeHistogramBucketFactor = 1.1
|
||||
}
|
||||
if n.Defaults.HistogramOptions.NativeHistogramMaxBuckets <= 0 {
|
||||
n.Defaults.HistogramOptions.NativeHistogramMaxBuckets = 256
|
||||
}
|
||||
|
||||
if len(n.Defaults.SummaryOptions.Quantiles) == 0 {
|
||||
n.Defaults.SummaryOptions.Quantiles = defaultQuantiles
|
||||
}
|
||||
|
||||
if n.Defaults.MatchType == MatchTypeDefault {
|
||||
n.Defaults.MatchType = MatchTypeGlob
|
||||
}
|
||||
|
||||
remainingMappingsCount := len(n.Mappings)
|
||||
|
||||
n.FSM = fsm.NewFSM([]string{string(MetricTypeCounter), string(MetricTypeGauge), string(MetricTypeObserver)},
|
||||
remainingMappingsCount, n.Defaults.GlobDisableOrdering)
|
||||
|
||||
for i := range n.Mappings {
|
||||
remainingMappingsCount--
|
||||
|
||||
currentMapping := &n.Mappings[i]
|
||||
|
||||
// check that label is correct
|
||||
for k := range currentMapping.Labels {
|
||||
if !labelNameRE.MatchString(k) {
|
||||
return fmt.Errorf("invalid label key: %s", k)
|
||||
}
|
||||
}
|
||||
|
||||
if currentMapping.Name == "" {
|
||||
return fmt.Errorf("line %d: metric mapping didn't set a metric name", i)
|
||||
}
|
||||
|
||||
if !metricNameRE.MatchString(currentMapping.Name) {
|
||||
return fmt.Errorf("metric name '%s' doesn't match regex '%s'", currentMapping.Name, metricNameRE)
|
||||
}
|
||||
|
||||
if currentMapping.MatchType == "" {
|
||||
currentMapping.MatchType = n.Defaults.MatchType
|
||||
}
|
||||
|
||||
if currentMapping.Action == "" {
|
||||
currentMapping.Action = ActionTypeMap
|
||||
}
|
||||
|
||||
if currentMapping.MatchType == MatchTypeGlob {
|
||||
n.doFSM = true
|
||||
if !metricLineRE.MatchString(currentMapping.Match) {
|
||||
return fmt.Errorf("invalid match: %s", currentMapping.Match)
|
||||
}
|
||||
|
||||
captureCount := n.FSM.AddState(currentMapping.Match, string(currentMapping.MatchMetricType),
|
||||
remainingMappingsCount, currentMapping)
|
||||
|
||||
currentMapping.nameFormatter = fsm.NewTemplateFormatter(currentMapping.Name, captureCount)
|
||||
|
||||
labelKeys := make([]string, len(currentMapping.Labels))
|
||||
labelFormatters := make([]*fsm.TemplateFormatter, len(currentMapping.Labels))
|
||||
labelIndex := 0
|
||||
for label, valueExpr := range currentMapping.Labels {
|
||||
labelKeys[labelIndex] = label
|
||||
labelFormatters[labelIndex] = fsm.NewTemplateFormatter(valueExpr, captureCount)
|
||||
labelIndex++
|
||||
}
|
||||
currentMapping.labelFormatters = labelFormatters
|
||||
currentMapping.labelKeys = labelKeys
|
||||
} else {
|
||||
if regex, err := regexp.Compile(currentMapping.Match); err != nil {
|
||||
return fmt.Errorf("invalid regex %s in mapping: %v", currentMapping.Match, err)
|
||||
} else {
|
||||
currentMapping.regex = regex
|
||||
}
|
||||
n.doRegex = true
|
||||
}
|
||||
|
||||
if currentMapping.ObserverType == "" {
|
||||
currentMapping.ObserverType = n.Defaults.ObserverType
|
||||
}
|
||||
|
||||
if currentMapping.LegacyQuantiles != nil &&
|
||||
(currentMapping.SummaryOptions == nil || currentMapping.SummaryOptions.Quantiles != nil) {
|
||||
level.Warn(m.Logger).Log("msg", "using the top level quantiles is deprecated. Please use quantiles in the summary_options hierarchy")
|
||||
}
|
||||
|
||||
if currentMapping.LegacyBuckets != nil &&
|
||||
(currentMapping.HistogramOptions == nil || currentMapping.HistogramOptions.Buckets != nil) {
|
||||
level.Warn(m.Logger).Log("msg", "using the top level buckets is deprecated. Please use buckets in the histogram_options hierarchy")
|
||||
}
|
||||
|
||||
if currentMapping.SummaryOptions != nil &&
|
||||
currentMapping.LegacyQuantiles != nil &&
|
||||
currentMapping.SummaryOptions.Quantiles != nil {
|
||||
return fmt.Errorf("cannot use quantiles in both the top level and summary options at the same time in %s", currentMapping.Match)
|
||||
}
|
||||
|
||||
if currentMapping.HistogramOptions != nil &&
|
||||
currentMapping.LegacyBuckets != nil &&
|
||||
currentMapping.HistogramOptions.Buckets != nil {
|
||||
return fmt.Errorf("cannot use buckets in both the top level and histogram options at the same time in %s", currentMapping.Match)
|
||||
}
|
||||
|
||||
if currentMapping.ObserverType == ObserverTypeHistogram {
|
||||
if currentMapping.SummaryOptions != nil {
|
||||
return fmt.Errorf("cannot use histogram observer and summary options at the same time")
|
||||
}
|
||||
if currentMapping.HistogramOptions == nil {
|
||||
currentMapping.HistogramOptions = &HistogramOptions{}
|
||||
}
|
||||
if currentMapping.LegacyBuckets != nil && len(currentMapping.LegacyBuckets) != 0 {
|
||||
currentMapping.HistogramOptions.Buckets = currentMapping.LegacyBuckets
|
||||
}
|
||||
if currentMapping.HistogramOptions.Buckets == nil || len(currentMapping.HistogramOptions.Buckets) == 0 {
|
||||
currentMapping.HistogramOptions.Buckets = n.Defaults.HistogramOptions.Buckets
|
||||
}
|
||||
}
|
||||
|
||||
if currentMapping.ObserverType == ObserverTypeSummary {
|
||||
if currentMapping.HistogramOptions != nil {
|
||||
return fmt.Errorf("cannot use summary observer and histogram options at the same time")
|
||||
}
|
||||
if currentMapping.SummaryOptions == nil {
|
||||
currentMapping.SummaryOptions = &SummaryOptions{}
|
||||
}
|
||||
if currentMapping.LegacyQuantiles != nil && len(currentMapping.LegacyQuantiles) != 0 {
|
||||
currentMapping.SummaryOptions.Quantiles = currentMapping.LegacyQuantiles
|
||||
}
|
||||
if currentMapping.SummaryOptions.Quantiles == nil || len(currentMapping.SummaryOptions.Quantiles) == 0 {
|
||||
currentMapping.SummaryOptions.Quantiles = n.Defaults.SummaryOptions.Quantiles
|
||||
}
|
||||
if currentMapping.SummaryOptions.MaxAge == 0 {
|
||||
currentMapping.SummaryOptions.MaxAge = n.Defaults.SummaryOptions.MaxAge
|
||||
}
|
||||
if currentMapping.SummaryOptions.AgeBuckets == 0 {
|
||||
currentMapping.SummaryOptions.AgeBuckets = n.Defaults.SummaryOptions.AgeBuckets
|
||||
}
|
||||
if currentMapping.SummaryOptions.BufCap == 0 {
|
||||
currentMapping.SummaryOptions.BufCap = n.Defaults.SummaryOptions.BufCap
|
||||
}
|
||||
}
|
||||
|
||||
if currentMapping.Ttl == 0 && n.Defaults.Ttl > 0 {
|
||||
currentMapping.Ttl = n.Defaults.Ttl
|
||||
}
|
||||
}
|
||||
|
||||
m.mutex.Lock()
|
||||
defer m.mutex.Unlock()
|
||||
|
||||
if m.Logger == nil {
|
||||
m.Logger = log.NewNopLogger()
|
||||
}
|
||||
|
||||
m.Defaults = n.Defaults
|
||||
m.Mappings = n.Mappings
|
||||
|
||||
// Reset the cache since this function can be used to reload config
|
||||
if m.cache != nil {
|
||||
m.cache.Reset()
|
||||
}
|
||||
|
||||
if n.doFSM {
|
||||
var mappings []string
|
||||
for _, mapping := range n.Mappings {
|
||||
if mapping.MatchType == MatchTypeGlob {
|
||||
mappings = append(mappings, mapping.Match)
|
||||
}
|
||||
}
|
||||
n.FSM.BacktrackingNeeded = fsm.TestIfNeedBacktracking(mappings, n.FSM.OrderingDisabled, m.Logger)
|
||||
|
||||
m.FSM = n.FSM
|
||||
m.doRegex = n.doRegex
|
||||
}
|
||||
m.doFSM = n.doFSM
|
||||
|
||||
if m.MappingsCount != nil {
|
||||
m.MappingsCount.Set(float64(len(n.Mappings)))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MetricMapper) InitFromFile(fileName string) error {
|
||||
mappingStr, err := os.ReadFile(fileName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return m.InitFromYAMLString(string(mappingStr))
|
||||
}
|
||||
|
||||
// UseCache tells the mapper to use a cache that implements the MetricMapperCache interface.
|
||||
// This cache MUST be thread-safe!
|
||||
func (m *MetricMapper) UseCache(cache MetricMapperCache) {
|
||||
m.mutex.Lock()
|
||||
defer m.mutex.Unlock()
|
||||
m.cache = cache
|
||||
}
|
||||
|
||||
func (m *MetricMapper) GetMapping(statsdMetric string, statsdMetricType MetricType) (*MetricMapping, prometheus.Labels, bool) {
|
||||
m.mutex.RLock()
|
||||
defer m.mutex.RUnlock()
|
||||
|
||||
// only use a cache if one is present
|
||||
if m.cache != nil {
|
||||
result, cached := m.cache.Get(formatKey(statsdMetric, statsdMetricType))
|
||||
if cached {
|
||||
r := result.(MetricMapperCacheResult)
|
||||
return r.Mapping, r.Labels, r.Matched
|
||||
}
|
||||
}
|
||||
|
||||
// glob matching
|
||||
if m.doFSM {
|
||||
finalState, captures := m.FSM.GetMapping(statsdMetric, string(statsdMetricType))
|
||||
if finalState != nil && finalState.Result != nil {
|
||||
v := finalState.Result.(*MetricMapping)
|
||||
result := copyMetricMapping(v)
|
||||
result.Name = result.nameFormatter.Format(captures)
|
||||
|
||||
labels := prometheus.Labels{}
|
||||
for index, formatter := range result.labelFormatters {
|
||||
labels[result.labelKeys[index]] = formatter.Format(captures)
|
||||
}
|
||||
|
||||
r := MetricMapperCacheResult{
|
||||
Mapping: result,
|
||||
Matched: true,
|
||||
Labels: labels,
|
||||
}
|
||||
// add match to cache
|
||||
if m.cache != nil {
|
||||
m.cache.Add(formatKey(statsdMetric, statsdMetricType), r)
|
||||
}
|
||||
|
||||
return result, labels, true
|
||||
} else if !m.doRegex {
|
||||
// if there's no regex match type, return immediately
|
||||
// Add miss to cache
|
||||
if m.cache != nil {
|
||||
m.cache.Add(formatKey(statsdMetric, statsdMetricType), MetricMapperCacheResult{})
|
||||
}
|
||||
return nil, nil, false
|
||||
}
|
||||
}
|
||||
|
||||
// regex matching
|
||||
for _, mapping := range m.Mappings {
|
||||
// if a rule don't have regex matching type, the regex field is unset
|
||||
if mapping.regex == nil {
|
||||
continue
|
||||
}
|
||||
matches := mapping.regex.FindStringSubmatchIndex(statsdMetric)
|
||||
if len(matches) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
mapping.Name = string(mapping.regex.ExpandString(
|
||||
[]byte{},
|
||||
mapping.Name,
|
||||
statsdMetric,
|
||||
matches,
|
||||
))
|
||||
|
||||
if mt := mapping.MatchMetricType; mt != "" && mt != statsdMetricType {
|
||||
continue
|
||||
}
|
||||
|
||||
labels := prometheus.Labels{}
|
||||
for label, valueExpr := range mapping.Labels {
|
||||
value := mapping.regex.ExpandString([]byte{}, valueExpr, statsdMetric, matches)
|
||||
labels[label] = string(value)
|
||||
}
|
||||
|
||||
r := MetricMapperCacheResult{
|
||||
Mapping: &mapping,
|
||||
Matched: true,
|
||||
Labels: labels,
|
||||
}
|
||||
// Add Match to cache
|
||||
if m.cache != nil {
|
||||
m.cache.Add(formatKey(statsdMetric, statsdMetricType), r)
|
||||
}
|
||||
|
||||
return &mapping, labels, true
|
||||
}
|
||||
|
||||
// Add Miss to cache
|
||||
if m.cache != nil {
|
||||
m.cache.Add(formatKey(statsdMetric, statsdMetricType), MetricMapperCacheResult{})
|
||||
}
|
||||
return nil, nil, false
|
||||
}
|
||||
|
||||
// make a shallow copy so that we do not overwrite name
|
||||
// as multiple names can be matched by same mapping
|
||||
func copyMetricMapping(in *MetricMapping) *MetricMapping {
|
||||
out := *in
|
||||
return &out
|
||||
}
|
1023
pkg/mapper/mapper_benchmark_test.go
Normal file
1023
pkg/mapper/mapper_benchmark_test.go
Normal file
File diff suppressed because it is too large
Load diff
74
pkg/mapper/mapper_cache.go
Normal file
74
pkg/mapper/mapper_cache.go
Normal file
|
@ -0,0 +1,74 @@
|
|||
// Copyright 2019 The Prometheus Authors
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package mapper
|
||||
|
||||
import (
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
type CacheMetrics struct {
|
||||
CacheLength prometheus.Gauge
|
||||
CacheGetsTotal prometheus.Counter
|
||||
CacheHitsTotal prometheus.Counter
|
||||
}
|
||||
|
||||
func NewCacheMetrics(reg prometheus.Registerer) *CacheMetrics {
|
||||
var m CacheMetrics
|
||||
|
||||
m.CacheLength = prometheus.NewGauge(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "statsd_metric_mapper_cache_length",
|
||||
Help: "The count of unique metrics currently cached.",
|
||||
},
|
||||
)
|
||||
m.CacheGetsTotal = prometheus.NewCounter(
|
||||
prometheus.CounterOpts{
|
||||
Name: "statsd_metric_mapper_cache_gets_total",
|
||||
Help: "The count of total metric cache gets.",
|
||||
},
|
||||
)
|
||||
m.CacheHitsTotal = prometheus.NewCounter(
|
||||
prometheus.CounterOpts{
|
||||
Name: "statsd_metric_mapper_cache_hits_total",
|
||||
Help: "The count of total metric cache hits.",
|
||||
},
|
||||
)
|
||||
|
||||
if reg != nil {
|
||||
reg.MustRegister(m.CacheLength)
|
||||
reg.MustRegister(m.CacheGetsTotal)
|
||||
reg.MustRegister(m.CacheHitsTotal)
|
||||
}
|
||||
return &m
|
||||
}
|
||||
|
||||
type MetricMapperCacheResult struct {
|
||||
Mapping *MetricMapping
|
||||
Matched bool
|
||||
Labels prometheus.Labels
|
||||
}
|
||||
|
||||
// MetricMapperCache MUST be thread-safe and should be instrumented with CacheMetrics
|
||||
type MetricMapperCache interface {
|
||||
// Get a cached result
|
||||
Get(metricKey string) (interface{}, bool)
|
||||
// Add a statsd MetricMapperResult to the cache
|
||||
Add(metricKey string, result interface{}) // Add an item to the cache
|
||||
// Reset clears the cache for config reloads
|
||||
Reset()
|
||||
}
|
||||
|
||||
func formatKey(metricString string, metricType MetricType) string {
|
||||
return string(metricType) + "." + metricString
|
||||
}
|
72
pkg/mapper/mapper_defaults.go
Normal file
72
pkg/mapper/mapper_defaults.go
Normal file
|
@ -0,0 +1,72 @@
|
|||
// Copyright 2020 The Prometheus Authors
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package mapper
|
||||
|
||||
import "time"
|
||||
|
||||
type MapperConfigDefaults struct {
|
||||
ObserverType ObserverType `yaml:"observer_type"`
|
||||
MatchType MatchType `yaml:"match_type"`
|
||||
GlobDisableOrdering bool `yaml:"glob_disable_ordering"`
|
||||
Ttl time.Duration `yaml:"ttl"`
|
||||
SummaryOptions SummaryOptions `yaml:"summary_options"`
|
||||
HistogramOptions HistogramOptions `yaml:"histogram_options"`
|
||||
}
|
||||
|
||||
// mapperConfigDefaultsAlias is used to unmarshal the yaml config into mapperConfigDefaults and allows deprecated fields
|
||||
type mapperConfigDefaultsAlias struct {
|
||||
ObserverType ObserverType `yaml:"observer_type"`
|
||||
TimerType ObserverType `yaml:"timer_type,omitempty"` // DEPRECATED - field only present to preserve backwards compatibility in configs
|
||||
Buckets []float64 `yaml:"buckets"` // DEPRECATED - field only present to preserve backwards compatibility in configs
|
||||
Quantiles []MetricObjective `yaml:"quantiles"` // DEPRECATED - field only present to preserve backwards compatibility in configs
|
||||
MatchType MatchType `yaml:"match_type"`
|
||||
GlobDisableOrdering bool `yaml:"glob_disable_ordering"`
|
||||
Ttl time.Duration `yaml:"ttl"`
|
||||
SummaryOptions SummaryOptions `yaml:"summary_options"`
|
||||
HistogramOptions HistogramOptions `yaml:"histogram_options"`
|
||||
}
|
||||
|
||||
// UnmarshalYAML is a custom unmarshal function to allow use of deprecated config keys
|
||||
// observer_type will override timer_type
|
||||
func (d *MapperConfigDefaults) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
var tmp mapperConfigDefaultsAlias
|
||||
if err := unmarshal(&tmp); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Copy defaults
|
||||
d.ObserverType = tmp.ObserverType
|
||||
d.MatchType = tmp.MatchType
|
||||
d.GlobDisableOrdering = tmp.GlobDisableOrdering
|
||||
d.Ttl = tmp.Ttl
|
||||
d.SummaryOptions = tmp.SummaryOptions
|
||||
d.HistogramOptions = tmp.HistogramOptions
|
||||
|
||||
// Use deprecated TimerType if necessary
|
||||
if tmp.ObserverType == "" {
|
||||
d.ObserverType = tmp.TimerType
|
||||
}
|
||||
|
||||
// Use deprecated quantiles if necessary
|
||||
if len(tmp.SummaryOptions.Quantiles) == 0 && len(tmp.Quantiles) > 0 {
|
||||
d.SummaryOptions = SummaryOptions{Quantiles: tmp.Quantiles}
|
||||
}
|
||||
|
||||
// Use deprecated buckets if necessary
|
||||
if len(tmp.HistogramOptions.Buckets) == 0 && len(tmp.Buckets) > 0 {
|
||||
d.HistogramOptions = HistogramOptions{Buckets: tmp.Buckets}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
1762
pkg/mapper/mapper_test.go
Normal file
1762
pkg/mapper/mapper_test.go
Normal file
File diff suppressed because it is too large
Load diff
102
pkg/mapper/mapping.go
Normal file
102
pkg/mapper/mapping.go
Normal file
|
@ -0,0 +1,102 @@
|
|||
// Copyright 2020 The Prometheus Authors
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either xpress or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package mapper
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"time"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
|
||||
"github.com/prometheus/statsd_exporter/pkg/mapper/fsm"
|
||||
)
|
||||
|
||||
type MetricMapping struct {
|
||||
Match string `yaml:"match"`
|
||||
Name string `yaml:"name"`
|
||||
nameFormatter *fsm.TemplateFormatter
|
||||
regex *regexp.Regexp
|
||||
Labels prometheus.Labels `yaml:"labels"`
|
||||
HonorLabels bool `yaml:"honor_labels"`
|
||||
labelKeys []string
|
||||
labelFormatters []*fsm.TemplateFormatter
|
||||
ObserverType ObserverType `yaml:"observer_type"`
|
||||
TimerType ObserverType `yaml:"timer_type,omitempty"` // DEPRECATED - field only present to preserve backwards compatibility in configs. Always empty
|
||||
LegacyBuckets []float64 `yaml:"buckets"`
|
||||
LegacyQuantiles []MetricObjective `yaml:"quantiles"`
|
||||
MatchType MatchType `yaml:"match_type"`
|
||||
HelpText string `yaml:"help"`
|
||||
Action ActionType `yaml:"action"`
|
||||
MatchMetricType MetricType `yaml:"match_metric_type"`
|
||||
Ttl time.Duration `yaml:"ttl"`
|
||||
SummaryOptions *SummaryOptions `yaml:"summary_options"`
|
||||
HistogramOptions *HistogramOptions `yaml:"histogram_options"`
|
||||
Scale MaybeFloat64 `yaml:"scale"`
|
||||
}
|
||||
|
||||
// UnmarshalYAML is a custom unmarshal function to allow use of deprecated config keys
|
||||
// observer_type will override timer_type
|
||||
func (m *MetricMapping) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
type MetricMappingAlias MetricMapping
|
||||
var tmp MetricMappingAlias
|
||||
if err := unmarshal(&tmp); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Copy defaults
|
||||
m.Match = tmp.Match
|
||||
m.Name = tmp.Name
|
||||
m.Labels = tmp.Labels
|
||||
m.HonorLabels = tmp.HonorLabels
|
||||
m.ObserverType = tmp.ObserverType
|
||||
m.LegacyBuckets = tmp.LegacyBuckets
|
||||
m.LegacyQuantiles = tmp.LegacyQuantiles
|
||||
m.MatchType = tmp.MatchType
|
||||
m.HelpText = tmp.HelpText
|
||||
m.Action = tmp.Action
|
||||
m.MatchMetricType = tmp.MatchMetricType
|
||||
m.Ttl = tmp.Ttl
|
||||
m.SummaryOptions = tmp.SummaryOptions
|
||||
m.HistogramOptions = tmp.HistogramOptions
|
||||
m.Scale = tmp.Scale
|
||||
|
||||
// Use deprecated TimerType if necessary
|
||||
if tmp.ObserverType == "" {
|
||||
m.ObserverType = tmp.TimerType
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type MaybeFloat64 struct {
|
||||
Set bool
|
||||
Val float64
|
||||
}
|
||||
|
||||
func (m *MaybeFloat64) MarshalYAML() (interface{}, error) {
|
||||
if m.Set {
|
||||
return m.Val, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (m *MaybeFloat64) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
var tmp float64
|
||||
if err := unmarshal(&tmp); err != nil {
|
||||
return err
|
||||
}
|
||||
m.Val = tmp
|
||||
m.Set = true
|
||||
return nil
|
||||
}
|
41
pkg/mapper/match.go
Normal file
41
pkg/mapper/match.go
Normal file
|
@ -0,0 +1,41 @@
|
|||
// Copyright 2013 The Prometheus Authors
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package mapper
|
||||
|
||||
import "fmt"
|
||||
|
||||
type MatchType string
|
||||
|
||||
const (
|
||||
MatchTypeGlob MatchType = "glob"
|
||||
MatchTypeRegex MatchType = "regex"
|
||||
MatchTypeDefault MatchType = ""
|
||||
)
|
||||
|
||||
func (t *MatchType) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
var v string
|
||||
if err := unmarshal(&v); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch MatchType(v) {
|
||||
case MatchTypeRegex:
|
||||
*t = MatchTypeRegex
|
||||
case MatchTypeGlob, MatchTypeDefault:
|
||||
*t = MatchTypeGlob
|
||||
default:
|
||||
return fmt.Errorf("invalid match type %q", v)
|
||||
}
|
||||
return nil
|
||||
}
|
46
pkg/mapper/metric_type.go
Normal file
46
pkg/mapper/metric_type.go
Normal file
|
@ -0,0 +1,46 @@
|
|||
// Copyright 2018 The Prometheus Authors
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package mapper
|
||||
|
||||
import "fmt"
|
||||
|
||||
type MetricType string
|
||||
|
||||
const (
|
||||
MetricTypeCounter MetricType = "counter"
|
||||
MetricTypeGauge MetricType = "gauge"
|
||||
MetricTypeObserver MetricType = "observer"
|
||||
MetricTypeTimer MetricType = "timer" // DEPRECATED
|
||||
)
|
||||
|
||||
func (m *MetricType) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
var v string
|
||||
if err := unmarshal(&v); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch MetricType(v) {
|
||||
case MetricTypeCounter:
|
||||
*m = MetricTypeCounter
|
||||
case MetricTypeGauge:
|
||||
*m = MetricTypeGauge
|
||||
case MetricTypeObserver:
|
||||
*m = MetricTypeObserver
|
||||
case MetricTypeTimer:
|
||||
*m = MetricTypeObserver
|
||||
default:
|
||||
return fmt.Errorf("invalid metric type '%s'", v)
|
||||
}
|
||||
return nil
|
||||
}
|
41
pkg/mapper/observer.go
Normal file
41
pkg/mapper/observer.go
Normal file
|
@ -0,0 +1,41 @@
|
|||
// Copyright 2013 The Prometheus Authors
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package mapper
|
||||
|
||||
import "fmt"
|
||||
|
||||
type ObserverType string
|
||||
|
||||
const (
|
||||
ObserverTypeHistogram ObserverType = "histogram"
|
||||
ObserverTypeSummary ObserverType = "summary"
|
||||
ObserverTypeDefault ObserverType = ""
|
||||
)
|
||||
|
||||
func (t *ObserverType) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
var v string
|
||||
if err := unmarshal(&v); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch ObserverType(v) {
|
||||
case ObserverTypeHistogram:
|
||||
*t = ObserverTypeHistogram
|
||||
case ObserverTypeSummary, ObserverTypeDefault:
|
||||
*t = ObserverTypeSummary
|
||||
default:
|
||||
return fmt.Errorf("invalid observer type '%s'", v)
|
||||
}
|
||||
return nil
|
||||
}
|
103
pkg/mappercache/lru/lru.go
Normal file
103
pkg/mappercache/lru/lru.go
Normal file
|
@ -0,0 +1,103 @@
|
|||
// Copyright 2021 The Prometheus Authors
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package lru
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
|
||||
"github.com/golang/groupcache/lru"
|
||||
|
||||
"github.com/prometheus/statsd_exporter/pkg/mappercache"
|
||||
)
|
||||
|
||||
type metricMapperLRUCache struct {
|
||||
cache *lruCache
|
||||
metrics *mappercache.CacheMetrics
|
||||
}
|
||||
|
||||
func NewMetricMapperLRUCache(reg prometheus.Registerer, size int) (*metricMapperLRUCache, error) {
|
||||
if size <= 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
metrics := mappercache.NewCacheMetrics(reg)
|
||||
cache := newLruCache(size)
|
||||
|
||||
return &metricMapperLRUCache{metrics: metrics, cache: cache}, nil
|
||||
}
|
||||
|
||||
func (m *metricMapperLRUCache) Get(metricKey string) (interface{}, bool) {
|
||||
m.metrics.CacheGetsTotal.Inc()
|
||||
if result, ok := m.cache.Get(metricKey); ok {
|
||||
m.metrics.CacheHitsTotal.Inc()
|
||||
return result, true
|
||||
} else {
|
||||
return nil, false
|
||||
}
|
||||
}
|
||||
|
||||
func (m *metricMapperLRUCache) Add(metricKey string, result interface{}) {
|
||||
go m.trackCacheLength()
|
||||
m.cache.Add(metricKey, result)
|
||||
}
|
||||
|
||||
func (m *metricMapperLRUCache) trackCacheLength() {
|
||||
m.metrics.CacheLength.Set(float64(m.cache.Len()))
|
||||
}
|
||||
|
||||
func (m *metricMapperLRUCache) Reset() {
|
||||
m.cache.Clear()
|
||||
m.metrics.CacheLength.Set(0)
|
||||
}
|
||||
|
||||
type lruCache struct {
|
||||
cache *lru.Cache
|
||||
lock sync.RWMutex
|
||||
}
|
||||
|
||||
func newLruCache(maxEntries int) *lruCache {
|
||||
return &lruCache{
|
||||
cache: lru.New(maxEntries),
|
||||
}
|
||||
}
|
||||
|
||||
func (l *lruCache) Get(key string) (interface{}, bool) {
|
||||
l.lock.RLock()
|
||||
defer l.lock.RUnlock()
|
||||
|
||||
return l.cache.Get(key)
|
||||
}
|
||||
|
||||
func (l *lruCache) Add(key string, value interface{}) {
|
||||
l.lock.Lock()
|
||||
defer l.lock.Unlock()
|
||||
|
||||
l.cache.Add(key, value)
|
||||
}
|
||||
|
||||
func (l *lruCache) Len() int {
|
||||
l.lock.RLock()
|
||||
defer l.lock.RUnlock()
|
||||
|
||||
return l.cache.Len()
|
||||
}
|
||||
|
||||
func (l *lruCache) Clear() {
|
||||
l.lock.Lock()
|
||||
defer l.lock.Unlock()
|
||||
|
||||
l.cache.Clear()
|
||||
}
|
52
pkg/mappercache/metrics.go
Normal file
52
pkg/mappercache/metrics.go
Normal file
|
@ -0,0 +1,52 @@
|
|||
// Copyright 2021 The Prometheus Authors
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package mappercache
|
||||
|
||||
import "github.com/prometheus/client_golang/prometheus"
|
||||
|
||||
type CacheMetrics struct {
|
||||
CacheLength prometheus.Gauge
|
||||
CacheGetsTotal prometheus.Counter
|
||||
CacheHitsTotal prometheus.Counter
|
||||
}
|
||||
|
||||
func NewCacheMetrics(reg prometheus.Registerer) *CacheMetrics {
|
||||
var m CacheMetrics
|
||||
|
||||
m.CacheLength = prometheus.NewGauge(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "statsd_metric_mapper_cache_length",
|
||||
Help: "The count of unique metrics currently cached.",
|
||||
},
|
||||
)
|
||||
m.CacheGetsTotal = prometheus.NewCounter(
|
||||
prometheus.CounterOpts{
|
||||
Name: "statsd_metric_mapper_cache_gets_total",
|
||||
Help: "The count of total metric cache gets.",
|
||||
},
|
||||
)
|
||||
m.CacheHitsTotal = prometheus.NewCounter(
|
||||
prometheus.CounterOpts{
|
||||
Name: "statsd_metric_mapper_cache_hits_total",
|
||||
Help: "The count of total metric cache hits.",
|
||||
},
|
||||
)
|
||||
|
||||
if reg != nil {
|
||||
reg.MustRegister(m.CacheLength)
|
||||
reg.MustRegister(m.CacheGetsTotal)
|
||||
reg.MustRegister(m.CacheHitsTotal)
|
||||
}
|
||||
return &m
|
||||
}
|
83
pkg/mappercache/randomreplacement/randomreplacement.go
Normal file
83
pkg/mappercache/randomreplacement/randomreplacement.go
Normal file
|
@ -0,0 +1,83 @@
|
|||
// Copyright 2021 The Prometheus Authors
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package randomreplacement
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
|
||||
"github.com/prometheus/statsd_exporter/pkg/mappercache"
|
||||
)
|
||||
|
||||
type metricMapperRRCache struct {
|
||||
lock sync.RWMutex
|
||||
size int
|
||||
items map[string]interface{}
|
||||
metrics *mappercache.CacheMetrics
|
||||
}
|
||||
|
||||
func NewMetricMapperRRCache(reg prometheus.Registerer, size int) (*metricMapperRRCache, error) {
|
||||
if size <= 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
metrics := mappercache.NewCacheMetrics(reg)
|
||||
c := &metricMapperRRCache{
|
||||
items: make(map[string]interface{}, size+1),
|
||||
size: size,
|
||||
metrics: metrics,
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func (m *metricMapperRRCache) Get(metricKey string) (interface{}, bool) {
|
||||
m.lock.RLock()
|
||||
result, ok := m.items[metricKey]
|
||||
m.lock.RUnlock()
|
||||
|
||||
return result, ok
|
||||
}
|
||||
|
||||
func (m *metricMapperRRCache) Add(metricKey string, result interface{}) {
|
||||
go m.trackCacheLength()
|
||||
|
||||
m.lock.Lock()
|
||||
|
||||
m.items[metricKey] = result
|
||||
|
||||
// evict an item if needed
|
||||
if len(m.items) > m.size {
|
||||
for k := range m.items {
|
||||
delete(m.items, k)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
m.lock.Unlock()
|
||||
}
|
||||
|
||||
func (m *metricMapperRRCache) Reset() {
|
||||
m.lock.Lock()
|
||||
defer m.lock.Unlock()
|
||||
m.items = make(map[string]interface{}, m.size+1)
|
||||
m.metrics.CacheLength.Set(0)
|
||||
}
|
||||
|
||||
func (m *metricMapperRRCache) trackCacheLength() {
|
||||
m.lock.RLock()
|
||||
length := len(m.items)
|
||||
m.lock.RUnlock()
|
||||
m.metrics.CacheLength.Set(float64(length))
|
||||
}
|
67
pkg/metrics/metrics.go
Normal file
67
pkg/metrics/metrics.go
Normal file
|
@ -0,0 +1,67 @@
|
|||
// Copyright 2013 The Prometheus Authors
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package metrics
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
type MetricType int
|
||||
|
||||
const (
|
||||
CounterMetricType MetricType = iota
|
||||
GaugeMetricType
|
||||
SummaryMetricType
|
||||
HistogramMetricType
|
||||
)
|
||||
|
||||
type NameHash uint64
|
||||
|
||||
type ValueHash uint64
|
||||
|
||||
type LabelHash struct {
|
||||
// This is a hash over the label names
|
||||
Names NameHash
|
||||
// This is a hash over the label names + label values
|
||||
Values ValueHash
|
||||
}
|
||||
|
||||
type MetricHolder interface{}
|
||||
|
||||
type VectorHolder interface {
|
||||
Delete(label prometheus.Labels) bool
|
||||
}
|
||||
|
||||
type Vector struct {
|
||||
Holder VectorHolder
|
||||
RefCount uint64
|
||||
}
|
||||
|
||||
type Metric struct {
|
||||
MetricType MetricType
|
||||
// Vectors key is the hash of the label names
|
||||
Vectors map[NameHash]*Vector
|
||||
// Metrics key is a hash of the label names + label values
|
||||
Metrics map[ValueHash]*RegisteredMetric
|
||||
}
|
||||
|
||||
type RegisteredMetric struct {
|
||||
LastRegisteredAt time.Time
|
||||
Labels prometheus.Labels
|
||||
TTL time.Duration
|
||||
Metric MetricHolder
|
||||
VecKey NameHash
|
||||
}
|
428
pkg/registry/registry.go
Normal file
428
pkg/registry/registry.go
Normal file
|
@ -0,0 +1,428 @@
|
|||
// Copyright 2013 The Prometheus Authors
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package registry
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"hash"
|
||||
"hash/fnv"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/common/model"
|
||||
|
||||
"github.com/prometheus/statsd_exporter/pkg/clock"
|
||||
"github.com/prometheus/statsd_exporter/pkg/mapper"
|
||||
"github.com/prometheus/statsd_exporter/pkg/metrics"
|
||||
)
|
||||
|
||||
// uncheckedCollector wraps a Collector but its Describe method yields no Desc.
|
||||
// This allows incoming metrics to have inconsistent label sets
|
||||
type uncheckedCollector struct {
|
||||
c prometheus.Collector
|
||||
}
|
||||
|
||||
func (u uncheckedCollector) Describe(_ chan<- *prometheus.Desc) {}
|
||||
func (u uncheckedCollector) Collect(c chan<- prometheus.Metric) {
|
||||
u.c.Collect(c)
|
||||
}
|
||||
|
||||
type Registry struct {
|
||||
Registerer prometheus.Registerer
|
||||
Metrics map[string]metrics.Metric
|
||||
Mapper *mapper.MetricMapper
|
||||
// The below value and label variables are allocated in the registry struct
|
||||
// so that we don't have to allocate them every time have to compute a label
|
||||
// hash.
|
||||
ValueBuf, NameBuf bytes.Buffer
|
||||
Hasher hash.Hash64
|
||||
}
|
||||
|
||||
func NewRegistry(reg prometheus.Registerer, mapper *mapper.MetricMapper) *Registry {
|
||||
return &Registry{
|
||||
Registerer: reg,
|
||||
Metrics: make(map[string]metrics.Metric),
|
||||
Mapper: mapper,
|
||||
Hasher: fnv.New64a(),
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Registry) MetricConflicts(metricName string, metricType metrics.MetricType) bool {
|
||||
vector, hasMetrics := r.Metrics[metricName]
|
||||
if !hasMetrics {
|
||||
// No metrics.Metric with this name exists
|
||||
return false
|
||||
}
|
||||
|
||||
if vector.MetricType == metricType {
|
||||
// We've found a copy of this metrics.Metric with this type, but different
|
||||
// labels, so it's safe to create a new one.
|
||||
return false
|
||||
}
|
||||
|
||||
// The metrics.Metric exists, but it's of a different type than we're trying to
|
||||
// create.
|
||||
return true
|
||||
}
|
||||
|
||||
func (r *Registry) StoreCounter(metricName string, hash metrics.LabelHash, labels prometheus.Labels, vec *prometheus.CounterVec, c prometheus.Counter, ttl time.Duration) {
|
||||
r.Store(metricName, hash, labels, vec, c, metrics.CounterMetricType, ttl)
|
||||
}
|
||||
|
||||
func (r *Registry) StoreGauge(metricName string, hash metrics.LabelHash, labels prometheus.Labels, vec *prometheus.GaugeVec, g prometheus.Gauge, ttl time.Duration) {
|
||||
r.Store(metricName, hash, labels, vec, g, metrics.GaugeMetricType, ttl)
|
||||
}
|
||||
|
||||
func (r *Registry) StoreHistogram(metricName string, hash metrics.LabelHash, labels prometheus.Labels, vec *prometheus.HistogramVec, o prometheus.Observer, ttl time.Duration) {
|
||||
r.Store(metricName, hash, labels, vec, o, metrics.HistogramMetricType, ttl)
|
||||
}
|
||||
|
||||
func (r *Registry) StoreSummary(metricName string, hash metrics.LabelHash, labels prometheus.Labels, vec *prometheus.SummaryVec, o prometheus.Observer, ttl time.Duration) {
|
||||
r.Store(metricName, hash, labels, vec, o, metrics.SummaryMetricType, ttl)
|
||||
}
|
||||
|
||||
func (r *Registry) Store(metricName string, hash metrics.LabelHash, labels prometheus.Labels, vh metrics.VectorHolder, mh metrics.MetricHolder, metricType metrics.MetricType, ttl time.Duration) {
|
||||
metric, hasMetrics := r.Metrics[metricName]
|
||||
if !hasMetrics {
|
||||
metric.MetricType = metricType
|
||||
metric.Vectors = make(map[metrics.NameHash]*metrics.Vector)
|
||||
metric.Metrics = make(map[metrics.ValueHash]*metrics.RegisteredMetric)
|
||||
|
||||
r.Metrics[metricName] = metric
|
||||
}
|
||||
|
||||
v, ok := metric.Vectors[hash.Names]
|
||||
if !ok {
|
||||
v = &metrics.Vector{Holder: vh}
|
||||
metric.Vectors[hash.Names] = v
|
||||
}
|
||||
|
||||
now := clock.Now()
|
||||
rm, ok := metric.Metrics[hash.Values]
|
||||
if !ok {
|
||||
rm = &metrics.RegisteredMetric{
|
||||
LastRegisteredAt: now,
|
||||
Labels: labels,
|
||||
TTL: ttl,
|
||||
Metric: mh,
|
||||
VecKey: hash.Names,
|
||||
}
|
||||
metric.Metrics[hash.Values] = rm
|
||||
v.RefCount++
|
||||
return
|
||||
}
|
||||
rm.LastRegisteredAt = now
|
||||
// Update ttl from mapping
|
||||
rm.TTL = ttl
|
||||
}
|
||||
|
||||
func (r *Registry) Get(metricName string, hash metrics.LabelHash, metricType metrics.MetricType) (metrics.VectorHolder, metrics.MetricHolder) {
|
||||
metric, hasMetric := r.Metrics[metricName]
|
||||
|
||||
if !hasMetric {
|
||||
return nil, nil
|
||||
}
|
||||
if metric.MetricType != metricType {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
rm, ok := metric.Metrics[hash.Values]
|
||||
if ok {
|
||||
now := clock.Now()
|
||||
rm.LastRegisteredAt = now
|
||||
return metric.Vectors[hash.Names].Holder, rm.Metric
|
||||
}
|
||||
|
||||
vector, ok := metric.Vectors[hash.Names]
|
||||
if ok {
|
||||
return vector.Holder, nil
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (r *Registry) GetCounter(metricName string, labels prometheus.Labels, help string, mapping *mapper.MetricMapping, metricsCount *prometheus.GaugeVec) (prometheus.Counter, error) {
|
||||
hash, labelNames := r.HashLabels(labels)
|
||||
vh, mh := r.Get(metricName, hash, metrics.CounterMetricType)
|
||||
if mh != nil {
|
||||
return mh.(prometheus.Counter), nil
|
||||
}
|
||||
|
||||
if r.MetricConflicts(metricName, metrics.CounterMetricType) {
|
||||
return nil, fmt.Errorf("metric with name %s is already registered", metricName)
|
||||
}
|
||||
|
||||
err := r.checkHistogramNameCollision(metricName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var counterVec *prometheus.CounterVec
|
||||
if vh == nil {
|
||||
metricsCount.WithLabelValues("counter").Inc()
|
||||
counterVec = prometheus.NewCounterVec(prometheus.CounterOpts{
|
||||
Name: metricName,
|
||||
Help: help,
|
||||
}, labelNames)
|
||||
|
||||
if err := r.Registerer.Register(uncheckedCollector{counterVec}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
counterVec = vh.(*prometheus.CounterVec)
|
||||
}
|
||||
|
||||
var counter prometheus.Counter
|
||||
if counter, err = counterVec.GetMetricWith(labels); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r.StoreCounter(metricName, hash, labels, counterVec, counter, mapping.Ttl)
|
||||
|
||||
return counter, nil
|
||||
}
|
||||
|
||||
func (r *Registry) checkHistogramNameCollision(metricName string) error {
|
||||
histogramSuffixes := []string{"_bucket", "_count", "_sum"}
|
||||
for _, suffix := range histogramSuffixes {
|
||||
if strings.HasSuffix(metricName, suffix) {
|
||||
if r.MetricConflicts(strings.TrimSuffix(metricName, suffix), metrics.CounterMetricType) {
|
||||
return fmt.Errorf("metric with name %s is already registered", metricName)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Registry) GetGauge(metricName string, labels prometheus.Labels, help string, mapping *mapper.MetricMapping, metricsCount *prometheus.GaugeVec) (prometheus.Gauge, error) {
|
||||
hash, labelNames := r.HashLabels(labels)
|
||||
vh, mh := r.Get(metricName, hash, metrics.GaugeMetricType)
|
||||
if mh != nil {
|
||||
return mh.(prometheus.Gauge), nil
|
||||
}
|
||||
|
||||
if r.MetricConflicts(metricName, metrics.GaugeMetricType) {
|
||||
return nil, fmt.Errorf("metrics.Metric with name %s is already registered", metricName)
|
||||
}
|
||||
|
||||
err := r.checkHistogramNameCollision(metricName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("metrics.Metric with name %s is already registered", metricName)
|
||||
}
|
||||
|
||||
var gaugeVec *prometheus.GaugeVec
|
||||
if vh == nil {
|
||||
metricsCount.WithLabelValues("gauge").Inc()
|
||||
gaugeVec = prometheus.NewGaugeVec(prometheus.GaugeOpts{
|
||||
Name: metricName,
|
||||
Help: help,
|
||||
}, labelNames)
|
||||
|
||||
if err := r.Registerer.Register(uncheckedCollector{gaugeVec}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
gaugeVec = vh.(*prometheus.GaugeVec)
|
||||
}
|
||||
|
||||
var gauge prometheus.Gauge
|
||||
if gauge, err = gaugeVec.GetMetricWith(labels); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r.StoreGauge(metricName, hash, labels, gaugeVec, gauge, mapping.Ttl)
|
||||
|
||||
return gauge, nil
|
||||
}
|
||||
|
||||
func (r *Registry) GetHistogram(metricName string, labels prometheus.Labels, help string, mapping *mapper.MetricMapping, metricsCount *prometheus.GaugeVec) (prometheus.Observer, error) {
|
||||
hash, labelNames := r.HashLabels(labels)
|
||||
vh, mh := r.Get(metricName, hash, metrics.HistogramMetricType)
|
||||
if mh != nil {
|
||||
return mh.(prometheus.Observer), nil
|
||||
}
|
||||
|
||||
if r.MetricConflicts(metricName, metrics.HistogramMetricType) {
|
||||
return nil, fmt.Errorf("metrics.Metric with name %s is already registered", metricName)
|
||||
}
|
||||
if r.MetricConflicts(metricName+"_sum", metrics.HistogramMetricType) {
|
||||
return nil, fmt.Errorf("metrics.Metric with name %s is already registered", metricName)
|
||||
}
|
||||
if r.MetricConflicts(metricName+"_count", metrics.HistogramMetricType) {
|
||||
return nil, fmt.Errorf("metrics.Metric with name %s is already registered", metricName)
|
||||
}
|
||||
if r.MetricConflicts(metricName+"_bucket", metrics.HistogramMetricType) {
|
||||
return nil, fmt.Errorf("metrics.Metric with name %s is already registered", metricName)
|
||||
}
|
||||
|
||||
var histogramVec *prometheus.HistogramVec
|
||||
if vh == nil {
|
||||
metricsCount.WithLabelValues("histogram").Inc()
|
||||
buckets := r.Mapper.Defaults.HistogramOptions.Buckets
|
||||
if mapping.HistogramOptions != nil && len(mapping.HistogramOptions.Buckets) > 0 {
|
||||
buckets = mapping.HistogramOptions.Buckets
|
||||
}
|
||||
|
||||
bucketFactor := r.Mapper.Defaults.HistogramOptions.NativeHistogramBucketFactor
|
||||
if mapping.HistogramOptions != nil && mapping.HistogramOptions.NativeHistogramBucketFactor > 0 {
|
||||
bucketFactor = mapping.HistogramOptions.NativeHistogramBucketFactor
|
||||
}
|
||||
|
||||
maxBuckets := r.Mapper.Defaults.HistogramOptions.NativeHistogramMaxBuckets
|
||||
if mapping.HistogramOptions != nil && mapping.HistogramOptions.NativeHistogramMaxBuckets > 0 {
|
||||
maxBuckets = mapping.HistogramOptions.NativeHistogramMaxBuckets
|
||||
}
|
||||
histogramVec = prometheus.NewHistogramVec(prometheus.HistogramOpts{
|
||||
Name: metricName,
|
||||
Help: help,
|
||||
Buckets: buckets,
|
||||
NativeHistogramBucketFactor: bucketFactor,
|
||||
NativeHistogramMaxBucketNumber: maxBuckets,
|
||||
}, labelNames)
|
||||
|
||||
if err := r.Registerer.Register(uncheckedCollector{histogramVec}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
histogramVec = vh.(*prometheus.HistogramVec)
|
||||
}
|
||||
|
||||
var observer prometheus.Observer
|
||||
var err error
|
||||
if observer, err = histogramVec.GetMetricWith(labels); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r.StoreHistogram(metricName, hash, labels, histogramVec, observer, mapping.Ttl)
|
||||
|
||||
return observer, nil
|
||||
}
|
||||
|
||||
func (r *Registry) GetSummary(metricName string, labels prometheus.Labels, help string, mapping *mapper.MetricMapping, metricsCount *prometheus.GaugeVec) (prometheus.Observer, error) {
|
||||
hash, labelNames := r.HashLabels(labels)
|
||||
vh, mh := r.Get(metricName, hash, metrics.SummaryMetricType)
|
||||
if mh != nil {
|
||||
return mh.(prometheus.Observer), nil
|
||||
}
|
||||
|
||||
if r.MetricConflicts(metricName, metrics.SummaryMetricType) {
|
||||
return nil, fmt.Errorf("metrics.Metric with name %s is already registered", metricName)
|
||||
}
|
||||
if r.MetricConflicts(metricName+"_sum", metrics.SummaryMetricType) {
|
||||
return nil, fmt.Errorf("metrics.Metric with name %s is already registered", metricName)
|
||||
}
|
||||
if r.MetricConflicts(metricName+"_count", metrics.SummaryMetricType) {
|
||||
return nil, fmt.Errorf("metrics.Metric with name %s is already registered", metricName)
|
||||
}
|
||||
|
||||
var summaryVec *prometheus.SummaryVec
|
||||
if vh == nil {
|
||||
metricsCount.WithLabelValues("summary").Inc()
|
||||
quantiles := r.Mapper.Defaults.SummaryOptions.Quantiles
|
||||
if mapping != nil && mapping.SummaryOptions != nil && len(mapping.SummaryOptions.Quantiles) > 0 {
|
||||
quantiles = mapping.SummaryOptions.Quantiles
|
||||
}
|
||||
|
||||
summaryOptions := mapper.SummaryOptions{
|
||||
MaxAge: r.Mapper.Defaults.SummaryOptions.MaxAge,
|
||||
AgeBuckets: r.Mapper.Defaults.SummaryOptions.AgeBuckets,
|
||||
BufCap: r.Mapper.Defaults.SummaryOptions.BufCap,
|
||||
}
|
||||
|
||||
if mapping != nil && mapping.SummaryOptions != nil {
|
||||
summaryOptions = *mapping.SummaryOptions
|
||||
}
|
||||
|
||||
objectives := make(map[float64]float64)
|
||||
for _, q := range quantiles {
|
||||
objectives[q.Quantile] = q.Error
|
||||
}
|
||||
// In the case of no mapping file, explicitly define the default quantiles
|
||||
if len(objectives) == 0 {
|
||||
objectives = map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.99: 0.001}
|
||||
}
|
||||
summaryVec = prometheus.NewSummaryVec(prometheus.SummaryOpts{
|
||||
Name: metricName,
|
||||
Help: help,
|
||||
Objectives: objectives,
|
||||
MaxAge: summaryOptions.MaxAge,
|
||||
AgeBuckets: summaryOptions.AgeBuckets,
|
||||
BufCap: summaryOptions.BufCap,
|
||||
}, labelNames)
|
||||
|
||||
if err := r.Registerer.Register(uncheckedCollector{summaryVec}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
summaryVec = vh.(*prometheus.SummaryVec)
|
||||
}
|
||||
|
||||
var observer prometheus.Observer
|
||||
var err error
|
||||
if observer, err = summaryVec.GetMetricWith(labels); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r.StoreSummary(metricName, hash, labels, summaryVec, observer, mapping.Ttl)
|
||||
|
||||
return observer, nil
|
||||
}
|
||||
|
||||
func (r *Registry) RemoveStaleMetrics() {
|
||||
now := clock.Now()
|
||||
// delete timeseries with expired ttl
|
||||
for _, metric := range r.Metrics {
|
||||
for hash, rm := range metric.Metrics {
|
||||
if rm.TTL == 0 {
|
||||
continue
|
||||
}
|
||||
if rm.LastRegisteredAt.Add(rm.TTL).Before(now) {
|
||||
metric.Vectors[rm.VecKey].Holder.Delete(rm.Labels)
|
||||
metric.Vectors[rm.VecKey].RefCount--
|
||||
delete(metric.Metrics, hash)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Calculates a hash of both the label names and values.
|
||||
func (r *Registry) HashLabels(labels prometheus.Labels) (metrics.LabelHash, []string) {
|
||||
r.Hasher.Reset()
|
||||
r.NameBuf.Reset()
|
||||
r.ValueBuf.Reset()
|
||||
labelNames := make([]string, 0, len(labels))
|
||||
|
||||
for labelName := range labels {
|
||||
labelNames = append(labelNames, labelName)
|
||||
}
|
||||
sort.Strings(labelNames)
|
||||
|
||||
r.ValueBuf.WriteByte(model.SeparatorByte)
|
||||
for _, labelName := range labelNames {
|
||||
r.ValueBuf.WriteString(labels[labelName])
|
||||
r.ValueBuf.WriteByte(model.SeparatorByte)
|
||||
|
||||
r.NameBuf.WriteString(labelName)
|
||||
r.NameBuf.WriteByte(model.SeparatorByte)
|
||||
}
|
||||
|
||||
lh := metrics.LabelHash{}
|
||||
r.Hasher.Write(r.NameBuf.Bytes())
|
||||
lh.Names = metrics.NameHash(r.Hasher.Sum64())
|
||||
|
||||
// Now add the values to the names we've already hashed.
|
||||
r.Hasher.Write(r.ValueBuf.Bytes())
|
||||
lh.Values = metrics.ValueHash(r.Hasher.Sum64())
|
||||
|
||||
return lh, labelNames
|
||||
}
|
167
pkg/relay/relay.go
Normal file
167
pkg/relay/relay.go
Normal file
|
@ -0,0 +1,167 @@
|
|||
// Copyright 2021 The Prometheus Authors
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package relay
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/prometheus/statsd_exporter/pkg/clock"
|
||||
|
||||
"github.com/go-kit/log"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||
"github.com/prometheus/statsd_exporter/pkg/level"
|
||||
)
|
||||
|
||||
type Relay struct {
|
||||
addr *net.UDPAddr
|
||||
bufferChannel chan []byte
|
||||
conn *net.UDPConn
|
||||
logger log.Logger
|
||||
packetLength uint
|
||||
|
||||
packetsTotal prometheus.Counter
|
||||
longLinesTotal prometheus.Counter
|
||||
relayedLinesTotal prometheus.Counter
|
||||
}
|
||||
|
||||
var (
|
||||
relayPacketsTotal = promauto.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Name: "statsd_exporter_relay_packets_total",
|
||||
Help: "The number of StatsD packets relayed.",
|
||||
},
|
||||
[]string{"target"},
|
||||
)
|
||||
relayLongLinesTotal = promauto.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Name: "statsd_exporter_relay_long_lines_total",
|
||||
Help: "The number lines that were too long to relay.",
|
||||
},
|
||||
[]string{"target"},
|
||||
)
|
||||
relayLinesRelayedTotal = promauto.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Name: "statsd_exporter_relay_lines_relayed_total",
|
||||
Help: "The number of lines that were buffered to be relayed.",
|
||||
},
|
||||
[]string{"target"},
|
||||
)
|
||||
)
|
||||
|
||||
// NewRelay creates a statsd UDP relay. It can be used to send copies of statsd raw
|
||||
// lines to a separate service.
|
||||
func NewRelay(l log.Logger, target string, packetLength uint) (*Relay, error) {
|
||||
addr, err := net.ResolveUDPAddr("udp", target)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to resolve target %s, err: %w", target, err)
|
||||
}
|
||||
conn, err := net.ListenUDP("udp", nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to listen on UDP, err: %w", err)
|
||||
}
|
||||
|
||||
c := make(chan []byte, 100)
|
||||
|
||||
r := Relay{
|
||||
addr: addr,
|
||||
bufferChannel: c,
|
||||
conn: conn,
|
||||
logger: l,
|
||||
packetLength: packetLength,
|
||||
|
||||
packetsTotal: relayPacketsTotal.WithLabelValues(target),
|
||||
longLinesTotal: relayLongLinesTotal.WithLabelValues(target),
|
||||
relayedLinesTotal: relayLinesRelayedTotal.WithLabelValues(target),
|
||||
}
|
||||
|
||||
// Startup the UDP sender.
|
||||
go r.relayOutput()
|
||||
|
||||
return &r, nil
|
||||
}
|
||||
|
||||
// relayOutput buffers statsd lines and sends them to the relay target.
|
||||
func (r *Relay) relayOutput() {
|
||||
var buffer bytes.Buffer
|
||||
var err error
|
||||
|
||||
relayInterval := clock.NewTicker(1 * time.Second)
|
||||
defer relayInterval.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-relayInterval.C:
|
||||
err = r.sendPacket(buffer.Bytes())
|
||||
if err != nil {
|
||||
level.Error(r.logger).Log("msg", "Error sending UDP packet", "error", err)
|
||||
return
|
||||
}
|
||||
// Clear out the buffer.
|
||||
buffer.Reset()
|
||||
case b := <-r.bufferChannel:
|
||||
if uint(len(b)+buffer.Len()) > r.packetLength {
|
||||
level.Debug(r.logger).Log("msg", "Buffer full, sending packet", "length", buffer.Len())
|
||||
err = r.sendPacket(buffer.Bytes())
|
||||
if err != nil {
|
||||
level.Error(r.logger).Log("msg", "Error sending UDP packet", "error", err)
|
||||
return
|
||||
}
|
||||
// Seed the new buffer with the new line.
|
||||
buffer.Reset()
|
||||
buffer.Write(b)
|
||||
} else {
|
||||
level.Debug(r.logger).Log("msg", "Adding line to buffer", "line", string(b))
|
||||
buffer.Write(b)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// sendPacket sends a single relay line to the destination target.
|
||||
func (r *Relay) sendPacket(buf []byte) error {
|
||||
if len(buf) == 0 {
|
||||
level.Debug(r.logger).Log("msg", "Empty buffer, nothing to send")
|
||||
return nil
|
||||
}
|
||||
level.Debug(r.logger).Log("msg", "Sending packet", "length", len(buf), "data", string(buf))
|
||||
_, err := r.conn.WriteToUDP(buf, r.addr)
|
||||
r.packetsTotal.Inc()
|
||||
return err
|
||||
}
|
||||
|
||||
// RelayLine processes a single statsd line and forwards it to the relay target.
|
||||
func (r *Relay) RelayLine(l string) {
|
||||
lineLength := uint(len(l))
|
||||
if lineLength == 0 {
|
||||
level.Debug(r.logger).Log("msg", "Empty line, not relaying")
|
||||
return
|
||||
}
|
||||
if lineLength > r.packetLength-1 {
|
||||
level.Warn(r.logger).Log("msg", "line too long, not relaying", "length", lineLength, "max", r.packetLength)
|
||||
r.longLinesTotal.Inc()
|
||||
return
|
||||
}
|
||||
level.Debug(r.logger).Log("msg", "Relaying line", "line", string(l))
|
||||
if !strings.HasSuffix(l, "\n") {
|
||||
l = l + "\n"
|
||||
}
|
||||
r.relayedLinesTotal.Inc()
|
||||
r.bufferChannel <- []byte(l)
|
||||
}
|
176
pkg/relay/relay_test.go
Normal file
176
pkg/relay/relay_test.go
Normal file
|
@ -0,0 +1,176 @@
|
|||
// Copyright 2022 The Prometheus Authors
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package relay
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/go-kit/log"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
dto "github.com/prometheus/client_model/go"
|
||||
"github.com/prometheus/statsd_exporter/pkg/clock"
|
||||
"github.com/stvp/go-udp-testing"
|
||||
)
|
||||
|
||||
func TestRelay_RelayLine(t *testing.T) {
|
||||
type args struct {
|
||||
lines []string
|
||||
expected string
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
}{
|
||||
{
|
||||
name: "multiple lines",
|
||||
args: args{
|
||||
lines: []string{"foo5:100|c|#tag1:bar,#tag2:baz", "foo2:200|c|#tag1:bar,#tag2:baz"},
|
||||
expected: "foo5:100|c|#tag1:bar,#tag2:baz\n",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
udp.SetAddr(":1160")
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
tickerCh := make(chan time.Time)
|
||||
clock.ClockInstance = &clock.Clock{
|
||||
TickerCh: tickerCh,
|
||||
}
|
||||
clock.ClockInstance.Instant = time.Unix(0, 0)
|
||||
|
||||
logger := log.NewNopLogger()
|
||||
r, err := NewRelay(
|
||||
logger,
|
||||
"localhost:1160",
|
||||
200,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Did not expect error while creating relay.")
|
||||
}
|
||||
|
||||
udp.ShouldReceive(t, tt.args.expected, func() {
|
||||
for _, line := range tt.args.lines {
|
||||
r.RelayLine(line)
|
||||
}
|
||||
|
||||
for goSchedTimes := 0; goSchedTimes < 1000; goSchedTimes++ {
|
||||
if len(r.bufferChannel) == 0 {
|
||||
break
|
||||
}
|
||||
runtime.Gosched()
|
||||
}
|
||||
|
||||
// Tick time forward to trigger a packet send.
|
||||
clock.ClockInstance.Instant = time.Unix(1, 10)
|
||||
clock.ClockInstance.TickerCh <- time.Unix(0, 0)
|
||||
})
|
||||
|
||||
metrics, err := prometheus.DefaultGatherer.Gather()
|
||||
if err != nil {
|
||||
t.Fatalf("Cannot gather from DefaultGatherer: %v", err)
|
||||
}
|
||||
|
||||
metricNames := map[string]float64{
|
||||
"statsd_exporter_relay_long_lines_total": 0,
|
||||
"statsd_exporter_relay_lines_relayed_total": float64(len(tt.args.lines)),
|
||||
}
|
||||
for metricName, expectedValue := range metricNames {
|
||||
metric := getFloat64(metrics, metricName, prometheus.Labels{"target": "localhost:1160"})
|
||||
|
||||
if metric == nil {
|
||||
t.Fatalf("Could not find time series with first label set for metric: %s", metricName)
|
||||
}
|
||||
if *metric != expectedValue {
|
||||
t.Errorf("Expected metric %s to be %f, got %f", metricName, expectedValue, *metric)
|
||||
}
|
||||
}
|
||||
|
||||
prometheus.Unregister(relayLongLinesTotal)
|
||||
prometheus.Unregister(relayLinesRelayedTotal)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// getFloat64 search for metric by name in array of MetricFamily and then search a value by labels.
|
||||
// Method returns a value or nil if metric is not found.
|
||||
func getFloat64(metrics []*dto.MetricFamily, name string, labels prometheus.Labels) *float64 {
|
||||
var metricFamily *dto.MetricFamily
|
||||
for _, m := range metrics {
|
||||
if *m.Name == name {
|
||||
metricFamily = m
|
||||
break
|
||||
}
|
||||
}
|
||||
if metricFamily == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var metric *dto.Metric
|
||||
labelStr := fmt.Sprintf("%v", labels)
|
||||
for _, m := range metricFamily.Metric {
|
||||
l := labelPairsAsLabels(m.GetLabel())
|
||||
ls := fmt.Sprintf("%v", l)
|
||||
if labelStr == ls {
|
||||
metric = m
|
||||
break
|
||||
}
|
||||
}
|
||||
if metric == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var value float64
|
||||
if metric.Gauge != nil {
|
||||
value = metric.Gauge.GetValue()
|
||||
return &value
|
||||
}
|
||||
if metric.Counter != nil {
|
||||
value = metric.Counter.GetValue()
|
||||
return &value
|
||||
}
|
||||
if metric.Histogram != nil {
|
||||
value = metric.Histogram.GetSampleSum()
|
||||
return &value
|
||||
}
|
||||
if metric.Summary != nil {
|
||||
value = metric.Summary.GetSampleSum()
|
||||
return &value
|
||||
}
|
||||
if metric.Untyped != nil {
|
||||
value = metric.Untyped.GetValue()
|
||||
return &value
|
||||
}
|
||||
panic(fmt.Errorf("collected a non-gauge/counter/histogram/summary/untyped metric: %s", metric))
|
||||
}
|
||||
|
||||
func labelPairsAsLabels(pairs []*dto.LabelPair) (labels prometheus.Labels) {
|
||||
labels = prometheus.Labels{}
|
||||
for _, pair := range pairs {
|
||||
if pair.Name == nil {
|
||||
continue
|
||||
}
|
||||
value := ""
|
||||
if pair.Value != nil {
|
||||
value = *pair.Value
|
||||
}
|
||||
labels[*pair.Name] = value
|
||||
}
|
||||
return
|
||||
}
|
53
telemetry.go
53
telemetry.go
|
@ -1,53 +0,0 @@
|
|||
// Copyright 2013 The Prometheus Authors
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
var (
|
||||
eventStats = prometheus.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Name: "statsd_exporter_events_total",
|
||||
Help: "The total number of StatsD events seen.",
|
||||
},
|
||||
[]string{"type"},
|
||||
)
|
||||
networkStats = prometheus.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Name: "statsd_exporter_packets_total",
|
||||
Help: "The total number of StatsD packets seen.",
|
||||
},
|
||||
[]string{"type"},
|
||||
)
|
||||
configLoads = prometheus.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Name: "statsd_exporter_config_reloads_total",
|
||||
Help: "The number of configuration reloads.",
|
||||
},
|
||||
[]string{"outcome"},
|
||||
)
|
||||
mappingsCount = prometheus.NewGauge(prometheus.GaugeOpts{
|
||||
Name: "statsd_exporter_loaded_mappings_count",
|
||||
Help: "The number of configured metric mappings.",
|
||||
})
|
||||
)
|
||||
|
||||
func init() {
|
||||
prometheus.MustRegister(eventStats)
|
||||
prometheus.MustRegister(networkStats)
|
||||
prometheus.MustRegister(configLoads)
|
||||
prometheus.MustRegister(mappingsCount)
|
||||
}
|
Loading…
Reference in a new issue