prometheus-client-mmap 0.19.1 → 0.21.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,346 @@
1
+ use smallvec::SmallVec;
2
+ use std::str;
3
+
4
+ /// String slices pointing to the fields of a borrowed `Entry`'s JSON data.
5
+ #[derive(PartialEq, Debug)]
6
+ pub struct MetricText<'a> {
7
+ pub family_name: &'a str,
8
+ pub metric_name: &'a str,
9
+ pub labels: SmallVec<[&'a str; 4]>,
10
+ pub values: SmallVec<[&'a str; 4]>,
11
+ }
12
+
13
+ #[derive(PartialEq, Debug)]
14
+ struct MetricNames<'a> {
15
+ label_json: &'a str,
16
+ family_name: &'a str,
17
+ metric_name: &'a str,
18
+ }
19
+
20
+ #[derive(PartialEq, Debug)]
21
+ struct MetricLabelVals<'a> {
22
+ labels: SmallVec<[&'a str; 4]>,
23
+ values: SmallVec<[&'a str; 4]>,
24
+ }
25
+
26
+ /// Parse Prometheus metric data stored in the following format:
27
+ ///
28
+ /// ["metric","name",["label_a","label_b"],["value_a","value_b"]]
29
+ ///
30
+ /// There will be 1-8 trailing spaces to ensure at least a one-byte
31
+ /// gap between the json and value, and to pad to an 8-byte alignment.
32
+ /// We strip the surrounding double quotes from all items. Values may
33
+ /// or may not have surrounding double quotes, depending on their type.
34
+ pub fn parse_metrics(json: &str) -> Option<MetricText> {
35
+ // It would be preferable to use `serde_json` here, but the values
36
+ // may be strings, numbers, or null, and so don't parse easily to a
37
+ // defined struct. Using `serde_json::Value` is an option, but since
38
+ // we're just copying the literal values into a buffer this will be
39
+ // inefficient and verbose.
40
+
41
+ // Trim trailing spaces from string before processing. We use
42
+ // `trim_end_matches()` instead of `trim_end()` because we know the
43
+ // trailing bytes are always ASCII 0x20 bytes. `trim_end()` will also
44
+ // check for unicode spaces and consume a few more CPU cycles.
45
+ let trimmed = json.trim_end_matches(' ');
46
+
47
+ let names = parse_names(trimmed)?;
48
+ let label_json = names.label_json;
49
+
50
+ let label_vals = parse_label_values(label_json)?;
51
+
52
+ Some(MetricText {
53
+ family_name: names.family_name,
54
+ metric_name: names.metric_name,
55
+ labels: label_vals.labels,
56
+ values: label_vals.values,
57
+ })
58
+ }
59
+
60
+ fn parse_names(json: &str) -> Option<MetricNames> {
61
+ // Starting with: ["family_name","metric_name",[...
62
+ if !json.starts_with("[\"") {
63
+ return None;
64
+ }
65
+
66
+ // Now: family_name","metric_name",[...
67
+ let remainder = json.get(2..)?;
68
+
69
+ let names_end = remainder.find('[')?;
70
+
71
+ // Save the rest of the slice to parse for labels later.
72
+ let label_json = remainder.get(names_end..)?;
73
+
74
+ // Now: family_name","metric_name",
75
+ let remainder = remainder.get(..names_end)?;
76
+
77
+ // Split on commas into:
78
+ // family_name","metric_name",
79
+ // ^^^^one^^^^^ ^^^^^two^^^^^
80
+ let mut token_iter = remainder.split(',');
81
+
82
+ // Captured: family_name","metric_name",
83
+ // ^^^^^^^^^^^
84
+ let family_name = token_iter.next()?.trim_end_matches('"');
85
+
86
+ // Captured: "family_name","metric_name",
87
+ // ^^^^^^^^^^^
88
+ let metric_name = token_iter.next()?.trim_matches('"');
89
+
90
+ // Confirm the final entry of the iter is empty, the the trailing ','.
91
+ if !token_iter.next()?.is_empty() {
92
+ return None;
93
+ }
94
+
95
+ Some(MetricNames {
96
+ label_json,
97
+ family_name,
98
+ metric_name,
99
+ })
100
+ }
101
+
102
+ fn parse_label_values(json: &str) -> Option<MetricLabelVals> {
103
+ // Starting with: ["label_a","label_b"],["value_a", "value_b"]]
104
+ if !(json.starts_with('[') && json.ends_with("]]")) {
105
+ return None;
106
+ }
107
+
108
+ // Validate we either have the start of a label string or an
109
+ // empty array, e.g. `["` or `[]`.
110
+ if !matches!(json.as_bytes().get(1)?, b'"' | b']') {
111
+ return None;
112
+ }
113
+
114
+ // Now: "label_a","label_b"
115
+ let labels_end = json.find(']')?;
116
+ let label_range = json.get(1..labels_end)?;
117
+
118
+ let mut labels = SmallVec::new();
119
+
120
+ // Split on commas into:
121
+ // "label_a","label_b"
122
+ // ^^^one^^^ ^^^two^^^
123
+ for label in label_range.split(',') {
124
+ // Captured: "label_a","label_b"
125
+ // ^^^^^^^
126
+ // If there are no labels, e.g. `[][]`, then don't capture anything.
127
+ if !label.is_empty() {
128
+ labels.push(label.trim_matches('"'));
129
+ }
130
+ }
131
+
132
+ // Now: ],["value_a", "value_b"]]
133
+ let mut values_range = json.get(labels_end..)?;
134
+
135
+ // Validate we have a separating comma with one and only one leading bracket.
136
+ if !(values_range.starts_with("],[") && values_range.as_bytes().get(3)? != &b'[') {
137
+ return None;
138
+ }
139
+
140
+ // Now: "value_a", "value_b"]]
141
+ values_range = values_range.get(3..)?;
142
+
143
+ let values_end = values_range.find(']')?;
144
+
145
+ // Validate we have only two trailing brackets.
146
+ if values_range.get(values_end..)?.len() > 2 {
147
+ return None;
148
+ }
149
+
150
+ // Now: "value_a", "value_b"
151
+ values_range = values_range.get(..values_end)?;
152
+
153
+ let mut values = SmallVec::new();
154
+
155
+ // Split on commas into:
156
+ // "value_a","value_b"
157
+ // ^^^one^^^ ^^^two^^^
158
+ for value in values_range.split(',') {
159
+ // Captured: "value_a","value_b"
160
+ // ^^^^^^^^^
161
+ // If there are no values, e.g. `[][]`, then don't capture anything.
162
+ if !value.is_empty() {
163
+ values.push(value.trim_matches('"'));
164
+ }
165
+ }
166
+
167
+ if values.len() != labels.len() {
168
+ return None;
169
+ }
170
+
171
+ Some(MetricLabelVals { labels, values })
172
+ }
173
+
174
+ #[cfg(test)]
175
+ mod test {
176
+ use smallvec::smallvec;
177
+
178
+ use super::*;
179
+
180
+ struct TestCase {
181
+ name: &'static str,
182
+ input: &'static str,
183
+ expected: Option<MetricText<'static>>,
184
+ }
185
+
186
+ #[test]
187
+ fn valid_json() {
188
+ let tc = vec![
189
+ TestCase {
190
+ name: "basic",
191
+ input: r#"["metric","name",["label_a","label_b"],["value_a","value_b"]]"#,
192
+ expected: Some(MetricText {
193
+ family_name: "metric",
194
+ metric_name: "name",
195
+ labels: smallvec!["label_a", "label_b"],
196
+ values: smallvec!["value_a", "value_b"],
197
+ }),
198
+ },
199
+ TestCase {
200
+ name: "many labels",
201
+ input: r#"["metric","name",["label_a","label_b","label_c","label_d","label_e"],["value_a","value_b","value_c","value_d","value_e"]]"#,
202
+
203
+ expected: Some(MetricText {
204
+ family_name: "metric",
205
+ metric_name: "name",
206
+ labels: smallvec!["label_a", "label_b", "label_c", "label_d", "label_e"],
207
+ values: smallvec!["value_a", "value_b", "value_c", "value_d", "value_e"],
208
+ }),
209
+ },
210
+ TestCase {
211
+ name: "numeric value",
212
+ input: r#"["metric","name",["label_a","label_b"],["value_a",403]]"#,
213
+ expected: Some(MetricText {
214
+ family_name: "metric",
215
+ metric_name: "name",
216
+ labels: smallvec!["label_a", "label_b"],
217
+ values: smallvec!["value_a", "403"],
218
+ }),
219
+ },
220
+ TestCase {
221
+ name: "null value",
222
+ input: r#"["metric","name",["label_a","label_b"],[null,"value_b"]]"#,
223
+ expected: Some(MetricText {
224
+ family_name: "metric",
225
+ metric_name: "name",
226
+ labels: smallvec!["label_a", "label_b"],
227
+ values: smallvec!["null", "value_b"],
228
+ }),
229
+ },
230
+ TestCase {
231
+ name: "no labels",
232
+ input: r#"["metric","name",[],[]]"#,
233
+ expected: Some(MetricText {
234
+ family_name: "metric",
235
+ metric_name: "name",
236
+ labels: smallvec![],
237
+ values: smallvec![],
238
+ }),
239
+ },
240
+ ];
241
+
242
+ for case in tc {
243
+ assert_eq!(
244
+ parse_metrics(case.input),
245
+ case.expected,
246
+ "test case: {}",
247
+ case.name,
248
+ );
249
+ }
250
+ }
251
+
252
+ #[test]
253
+ fn invalid_json() {
254
+ let tc = vec![
255
+ TestCase {
256
+ name: "not json",
257
+ input: "hello, world",
258
+ expected: None,
259
+ },
260
+ TestCase {
261
+ name: "no names",
262
+ input: r#"[["label_a","label_b"],["value_a","value_b"]]"#,
263
+ expected: None,
264
+ },
265
+ TestCase {
266
+ name: "too many names",
267
+ input: r#"["metric","name","unexpected_name",["label_a","label_b"],["value_a","value_b"]]"#,
268
+ expected: None,
269
+ },
270
+ TestCase {
271
+ name: "too many labels",
272
+ input: r#"["metric","name","unexpected_name",["label_a","label_b","label_c"],["value_a","value_b"]]"#,
273
+ expected: None,
274
+ },
275
+ TestCase {
276
+ name: "too many values",
277
+ input: r#"["metric","name",["label_a","label_b"],["value_a","value_b",null]]"#,
278
+ expected: None,
279
+ },
280
+ TestCase {
281
+ name: "no values",
282
+ input: r#"["metric","name",["label_a","label_b"]"#,
283
+ expected: None,
284
+ },
285
+ TestCase {
286
+ name: "no arrays",
287
+ input: r#"["metric","name","label_a","value_a"]"#,
288
+ expected: None,
289
+ },
290
+ TestCase {
291
+ name: "too many leading brackets",
292
+ input: r#"[["metric","name",["label_a","label_b"],["value_a","value_b"]]"#,
293
+ expected: None,
294
+ },
295
+ TestCase {
296
+ name: "too many trailing brackets",
297
+ input: r#"["metric","name",["label_a","label_b"],["value_a","value_b"]]]"#,
298
+ expected: None,
299
+ },
300
+ TestCase {
301
+ name: "too many leading label brackets",
302
+ input: r#"["metric","name",[["label_a","label_b"],["value_a","value_b"]]"#,
303
+ expected: None,
304
+ },
305
+ TestCase {
306
+ name: "too many trailing label brackets",
307
+ input: r#"["metric","name",["label_a","label_b"]],["value_a","value_b"]]"#,
308
+ expected: None,
309
+ },
310
+ TestCase {
311
+ name: "too many leading value brackets",
312
+ input: r#"["metric","name",["label_a","label_b"],[["value_a","value_b"]]"#,
313
+ expected: None,
314
+ },
315
+ TestCase {
316
+ name: "comma in family name",
317
+ input: r#"["met,ric","name",["label_a","label_b"],["value_a","value_b"]]"#,
318
+ expected: None,
319
+ },
320
+ TestCase {
321
+ name: "comma in metric name",
322
+ input: r#"["metric","na,me",["label_a","label_b"],["value_a","value_b"]]"#,
323
+ expected: None,
324
+ },
325
+ TestCase {
326
+ name: "comma in value",
327
+ input: r#"["metric","na,me",["label_a","label_b"],["val,ue_a","value_b"]]"#,
328
+ expected: None,
329
+ },
330
+ TestCase {
331
+ name: "comma in numeric value",
332
+ input: r#"["metric","name",["label_a","label_b"],[400,0,"value_b"]]"#,
333
+ expected: None,
334
+ },
335
+ ];
336
+
337
+ for case in tc {
338
+ assert_eq!(
339
+ case.expected,
340
+ parse_metrics(case.input),
341
+ "test case: {}",
342
+ case.name,
343
+ );
344
+ }
345
+ }
346
+ }