prometheus-client-mmap 0.19.1 → 0.20.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,544 @@
1
+ use magnus::{StaticSymbol, Symbol};
2
+
3
+ use crate::error::{MmapError, RubyError};
4
+ use crate::file_info::FileInfo;
5
+ use crate::parser::{self, MetricText};
6
+ use crate::raw_entry::RawEntry;
7
+ use crate::Result;
8
+ use crate::{SYM_GAUGE, SYM_LIVESUM, SYM_MAX, SYM_MIN};
9
+
10
+ /// A metrics entry extracted from a `*.db` file.
11
+ #[derive(Clone, Debug)]
12
+ pub struct FileEntry {
13
+ pub data: EntryData,
14
+ pub meta: EntryMetadata,
15
+ }
16
+
17
+ /// The primary data payload for a `FileEntry`, the JSON string and the
18
+ /// associated pid, if significant. Used as the key for `EntryMap`.
19
+ #[derive(Hash, PartialEq, Eq, PartialOrd, Ord, Clone, Debug)]
20
+ pub struct EntryData {
21
+ pub json: String,
22
+ pub pid: Option<String>,
23
+ }
24
+
25
+ impl EntryData {
26
+ pub fn new(raw_entry: &RawEntry, file_info: &FileInfo, pid_significant: bool) -> Result<Self> {
27
+ let mut buf = Vec::new();
28
+
29
+ if buf.try_reserve_exact(raw_entry.encoded_len()).is_err() {
30
+ return Err(MmapError::OutOfMemory(raw_entry.encoded_len()));
31
+ }
32
+
33
+ buf.resize(raw_entry.encoded_len(), 0);
34
+ buf.copy_from_slice(raw_entry.json());
35
+
36
+ let json = String::from_utf8(buf)
37
+ .map_err(|e| MmapError::Encoding(format!("invalid UTF-8 in entry JSON: {e}")))?;
38
+
39
+ let pid = if pid_significant {
40
+ Some(file_info.pid.clone())
41
+ } else {
42
+ None
43
+ };
44
+
45
+ Ok(EntryData { json, pid })
46
+ }
47
+ }
48
+
49
+ /// The metadata associated with a `FileEntry`. The value in `EntryMap`.
50
+ #[derive(Clone, Debug)]
51
+ pub struct EntryMetadata {
52
+ pub multiprocess_mode: Symbol,
53
+ pub type_: StaticSymbol,
54
+ pub value: f64,
55
+ }
56
+
57
+ impl EntryMetadata {
58
+ /// Construct a new `FileEntry`, copying the JSON string from the `RawEntry`
59
+ /// into an internal buffer.
60
+ pub fn new(mmap_entry: &RawEntry, file: &FileInfo) -> Result<Self> {
61
+ let value = mmap_entry.value();
62
+
63
+ Ok(EntryMetadata {
64
+ multiprocess_mode: file.multiprocess_mode,
65
+ type_: file.type_,
66
+ value,
67
+ })
68
+ }
69
+
70
+ /// Combine values with another `EntryMetadata`.
71
+ pub fn merge(&mut self, other: &Self) {
72
+ if self.type_ == SYM_GAUGE {
73
+ match self.multiprocess_mode {
74
+ s if s == SYM_MIN => self.value = self.value.min(other.value),
75
+ s if s == SYM_MAX => self.value = self.value.max(other.value),
76
+ s if s == SYM_LIVESUM => self.value += other.value,
77
+ _ => self.value = other.value,
78
+ }
79
+ } else {
80
+ self.value += other.value;
81
+ }
82
+ }
83
+
84
+ /// Validate if pid is significant for metric.
85
+ pub fn is_pid_significant(&self) -> bool {
86
+ let mp = self.multiprocess_mode;
87
+
88
+ self.type_ == SYM_GAUGE && !(mp == SYM_MIN || mp == SYM_MAX || mp == SYM_LIVESUM)
89
+ }
90
+ }
91
+
92
+ impl FileEntry {
93
+ /// Convert the sorted entries into a String in Prometheus metrics format.
94
+ pub fn entries_to_string(entries: Vec<FileEntry>) -> Result<String> {
95
+ // We guesstimate that lines are ~100 bytes long, preallocate the string to
96
+ // roughly that size.
97
+ let mut out = String::new();
98
+ out.try_reserve(entries.len() * 128)
99
+ .map_err(|_| MmapError::OutOfMemory(entries.len() * 128))?;
100
+
101
+ let mut prev_name: Option<String> = None;
102
+
103
+ let entry_count = entries.len();
104
+ let mut processed_count = 0;
105
+
106
+ for entry in entries {
107
+ let metrics_data = match parser::parse_metrics(&entry.data.json) {
108
+ Some(m) => m,
109
+ // We don't exit the function here so the total number of invalid
110
+ // entries can be calculated below.
111
+ None => continue,
112
+ };
113
+
114
+ match prev_name.as_ref() {
115
+ Some(p) if p == metrics_data.family_name => {}
116
+ _ => {
117
+ entry.append_header(metrics_data.family_name, &mut out);
118
+ prev_name = Some(metrics_data.family_name.to_owned());
119
+ }
120
+ }
121
+
122
+ entry.append_entry(metrics_data, &mut out)?;
123
+
124
+ out.push_str(&format!(" {}\n", entry.meta.value));
125
+
126
+ processed_count += 1;
127
+ }
128
+
129
+ if processed_count != entry_count {
130
+ return Err(MmapError::legacy(
131
+ format!("Processed entries {processed_count} != map entries {entry_count}"),
132
+ RubyError::Runtime,
133
+ ));
134
+ }
135
+
136
+ Ok(out)
137
+ }
138
+
139
+ fn append_header(&self, family_name: &str, out: &mut String) {
140
+ out.push_str("# HELP ");
141
+ out.push_str(family_name);
142
+ out.push_str(" Multiprocess metric\n");
143
+
144
+ out.push_str("# TYPE ");
145
+ out.push_str(family_name);
146
+ out.push(' ');
147
+
148
+ out.push_str(self.meta.type_.name().expect("name was invalid UTF-8"));
149
+ out.push('\n');
150
+ }
151
+
152
+ fn append_entry(&self, json_data: MetricText, out: &mut String) -> Result<()> {
153
+ out.push_str(json_data.metric_name);
154
+
155
+ if json_data.labels.is_empty() {
156
+ if let Some(pid) = self.data.pid.as_ref() {
157
+ out.push_str("{pid=\"");
158
+ out.push_str(pid);
159
+ out.push_str("\"}");
160
+ }
161
+
162
+ return Ok(());
163
+ }
164
+
165
+ out.push('{');
166
+
167
+ let it = json_data.labels.iter().zip(json_data.values.iter());
168
+
169
+ for (i, (&key, &val)) in it.enumerate() {
170
+ out.push_str(key);
171
+
172
+ out.push_str("=\"");
173
+
174
+ // `null` values will be output as `""`.
175
+ if val != "null" {
176
+ out.push_str(val);
177
+ }
178
+ out.push('"');
179
+
180
+ if i < json_data.labels.len() - 1 {
181
+ out.push(',');
182
+ }
183
+ }
184
+
185
+ if let Some(pid) = self.data.pid.as_ref() {
186
+ out.push_str(",pid=\"");
187
+ out.push_str(pid);
188
+ out.push('"');
189
+ }
190
+
191
+ out.push('}');
192
+
193
+ Ok(())
194
+ }
195
+ }
196
+
197
+ #[cfg(test)]
198
+ mod test {
199
+ use bstr::BString;
200
+ use indoc::indoc;
201
+
202
+ use super::*;
203
+ use crate::file_info::FileInfo;
204
+ use crate::raw_entry::RawEntry;
205
+ use crate::testhelper::{TestEntry, TestFile};
206
+
207
+ #[test]
208
+ fn test_entries_to_string() {
209
+ struct TestCase {
210
+ name: &'static str,
211
+ multiprocess_mode: &'static str,
212
+ json: &'static [&'static str],
213
+ values: &'static [f64],
214
+ pids: &'static [&'static str],
215
+ expected_out: Option<&'static str>,
216
+ expected_err: Option<MmapError>,
217
+ }
218
+
219
+ let _cleanup = unsafe { magnus::embed::init() };
220
+ let ruby = magnus::Ruby::get().unwrap();
221
+ crate::init(&ruby).unwrap();
222
+
223
+ let tc = vec![
224
+ TestCase {
225
+ name: "one metric, pid significant",
226
+ multiprocess_mode: "all",
227
+ json: &[r#"["family","name",["label_a","label_b"],["value_a","value_b"]]"#],
228
+ values: &[1.0],
229
+ pids: &["worker-1"],
230
+ expected_out: Some(indoc! {r##"# HELP family Multiprocess metric
231
+ # TYPE family gauge
232
+ name{label_a="value_a",label_b="value_b",pid="worker-1"} 1
233
+ "##}),
234
+ expected_err: None,
235
+ },
236
+ TestCase {
237
+ name: "one metric, no pid",
238
+ multiprocess_mode: "min",
239
+ json: &[r#"["family","name",["label_a","label_b"],["value_a","value_b"]]"#],
240
+ values: &[1.0],
241
+ pids: &["worker-1"],
242
+ expected_out: Some(indoc! {r##"# HELP family Multiprocess metric
243
+ # TYPE family gauge
244
+ name{label_a="value_a",label_b="value_b"} 1
245
+ "##}),
246
+ expected_err: None,
247
+ },
248
+ TestCase {
249
+ name: "floating point shown",
250
+ multiprocess_mode: "min",
251
+ json: &[r#"["family","name",["label_a","label_b"],["value_a","value_b"]]"#],
252
+ values: &[1.5],
253
+ pids: &["worker-1"],
254
+ expected_out: Some(indoc! {r##"# HELP family Multiprocess metric
255
+ # TYPE family gauge
256
+ name{label_a="value_a",label_b="value_b"} 1.5
257
+ "##}),
258
+ expected_err: None,
259
+ },
260
+ TestCase {
261
+ name: "no labels, pid significant",
262
+ multiprocess_mode: "all",
263
+ json: &[r#"["family","name",[],[]]"#],
264
+ values: &[1.0],
265
+ pids: &["worker-1"],
266
+ expected_out: Some(indoc! {r##"# HELP family Multiprocess metric
267
+ # TYPE family gauge
268
+ name{pid="worker-1"} 1
269
+ "##}),
270
+ expected_err: None,
271
+ },
272
+ TestCase {
273
+ name: "no labels, no pid",
274
+ multiprocess_mode: "min",
275
+ json: &[r#"["family","name",[],[]]"#],
276
+ values: &[1.0],
277
+ pids: &["worker-1"],
278
+ expected_out: Some(indoc! {r##"# HELP family Multiprocess metric
279
+ # TYPE family gauge
280
+ name 1
281
+ "##}),
282
+ expected_err: None,
283
+ },
284
+ TestCase {
285
+ name: "two metrics, same family, pid significant",
286
+ multiprocess_mode: "all",
287
+ json: &[
288
+ r#"["family","first",["label_a","label_b"],["value_a","value_b"]]"#,
289
+ r#"["family","second",["label_a","label_b"],["value_a","value_b"]]"#,
290
+ ],
291
+ values: &[1.0, 2.0],
292
+ pids: &["worker-1", "worker-1"],
293
+ expected_out: Some(indoc! {r##"# HELP family Multiprocess metric
294
+ # TYPE family gauge
295
+ first{label_a="value_a",label_b="value_b",pid="worker-1"} 1
296
+ second{label_a="value_a",label_b="value_b",pid="worker-1"} 2
297
+ "##}),
298
+ expected_err: None,
299
+ },
300
+ TestCase {
301
+ name: "two metrics, different family, pid significant",
302
+ multiprocess_mode: "min",
303
+ json: &[
304
+ r#"["first_family","first_name",["label_a","label_b"],["value_a","value_b"]]"#,
305
+ r#"["second_family","second_name",["label_a","label_b"],["value_a","value_b"]]"#,
306
+ ],
307
+ values: &[1.0, 2.0],
308
+ pids: &["worker-1", "worker-1"],
309
+ expected_out: Some(indoc! {r##"# HELP first_family Multiprocess metric
310
+ # TYPE first_family gauge
311
+ first_name{label_a="value_a",label_b="value_b"} 1
312
+ # HELP second_family Multiprocess metric
313
+ # TYPE second_family gauge
314
+ second_name{label_a="value_a",label_b="value_b"} 2
315
+ "##}),
316
+ expected_err: None,
317
+ },
318
+ TestCase {
319
+ name: "three metrics, two different families, pid significant",
320
+ multiprocess_mode: "all",
321
+ json: &[
322
+ r#"["first_family","first_name",["label_a","label_b"],["value_a","value_b"]]"#,
323
+ r#"["first_family","second_name",["label_a","label_b"],["value_a","value_b"]]"#,
324
+ r#"["second_family","second_name",["label_a","label_b"],["value_a","value_b"]]"#,
325
+ ],
326
+ values: &[1.0, 2.0, 3.0],
327
+ pids: &["worker-1", "worker-1", "worker-1"],
328
+ expected_out: Some(indoc! {r##"# HELP first_family Multiprocess metric
329
+ # TYPE first_family gauge
330
+ first_name{label_a="value_a",label_b="value_b",pid="worker-1"} 1
331
+ second_name{label_a="value_a",label_b="value_b",pid="worker-1"} 2
332
+ # HELP second_family Multiprocess metric
333
+ # TYPE second_family gauge
334
+ second_name{label_a="value_a",label_b="value_b",pid="worker-1"} 3
335
+ "##}),
336
+ expected_err: None,
337
+ },
338
+ TestCase {
339
+ name: "same metrics, pid significant, separate workers",
340
+ multiprocess_mode: "all",
341
+ json: &[
342
+ r#"["first_family","first_name",["label_a","label_b"],["value_a","value_b"]]"#,
343
+ r#"["first_family","first_name",["label_a","label_b"],["value_a","value_b"]]"#,
344
+ ],
345
+ values: &[1.0, 2.0],
346
+ pids: &["worker-1", "worker-2"],
347
+ expected_out: Some(indoc! {r##"# HELP first_family Multiprocess metric
348
+ # TYPE first_family gauge
349
+ first_name{label_a="value_a",label_b="value_b",pid="worker-1"} 1
350
+ first_name{label_a="value_a",label_b="value_b",pid="worker-2"} 2
351
+ "##}),
352
+ expected_err: None,
353
+ },
354
+ TestCase {
355
+ name: "same metrics, pid not significant, separate workers",
356
+ multiprocess_mode: "max",
357
+ json: &[
358
+ r#"["first_family","first_name",["label_a","label_b"],["value_a","value_b"]]"#,
359
+ r#"["first_family","first_name",["label_a","label_b"],["value_a","value_b"]]"#,
360
+ ],
361
+ values: &[1.0, 2.0],
362
+ pids: &["worker-1", "worker-2"],
363
+ expected_out: Some(indoc! {r##"# HELP first_family Multiprocess metric
364
+ # TYPE first_family gauge
365
+ first_name{label_a="value_a",label_b="value_b"} 1
366
+ first_name{label_a="value_a",label_b="value_b"} 2
367
+ "##}),
368
+ expected_err: None,
369
+ },
370
+ TestCase {
371
+ name: "entry fails to parse",
372
+ multiprocess_mode: "min",
373
+ json: &[
374
+ r#"["first_family","first_name",["label_a","label_b"],["value_a","value_b"]]"#,
375
+ r#"[not valid"#,
376
+ ],
377
+ values: &[1.0, 2.0],
378
+ pids: &["worker-1", "worker-1"],
379
+ expected_out: None,
380
+ expected_err: Some(MmapError::legacy(
381
+ "Processed entries 1 != map entries 2".to_owned(),
382
+ RubyError::Runtime,
383
+ )),
384
+ },
385
+ ];
386
+
387
+ for case in tc {
388
+ let name = case.name;
389
+
390
+ let input_bytes: Vec<BString> = case
391
+ .json
392
+ .iter()
393
+ .zip(case.values)
394
+ .map(|(&s, &value)| TestEntry::new(s, value).as_bstring())
395
+ .collect();
396
+
397
+ let mut file_infos = Vec::new();
398
+ for pid in case.pids {
399
+ let TestFile {
400
+ file,
401
+ path,
402
+ dir: _dir,
403
+ } = TestFile::new(b"foobar");
404
+
405
+ let info = FileInfo {
406
+ file,
407
+ path,
408
+ len: case.json.len(),
409
+ multiprocess_mode: Symbol::new(case.multiprocess_mode),
410
+ type_: StaticSymbol::new("gauge"),
411
+ pid: pid.to_string(),
412
+ };
413
+ file_infos.push(info);
414
+ }
415
+
416
+ let file_entries: Vec<FileEntry> = input_bytes
417
+ .iter()
418
+ .map(|s| RawEntry::from_slice(s).unwrap())
419
+ .zip(file_infos)
420
+ .map(|(entry, info)| {
421
+ let meta = EntryMetadata::new(&entry, &info).unwrap();
422
+ let data = EntryData::new(&entry, &info, meta.is_pid_significant()).unwrap();
423
+ FileEntry { data, meta }
424
+ })
425
+ .collect();
426
+
427
+ let output = FileEntry::entries_to_string(file_entries);
428
+
429
+ if let Some(expected_out) = case.expected_out {
430
+ assert_eq!(
431
+ expected_out,
432
+ output.as_ref().unwrap(),
433
+ "test case: {name} - output"
434
+ );
435
+ }
436
+
437
+ if let Some(expected_err) = case.expected_err {
438
+ assert_eq!(
439
+ expected_err,
440
+ output.unwrap_err(),
441
+ "test case: {name} - error"
442
+ );
443
+ }
444
+ }
445
+ }
446
+
447
+ #[test]
448
+ fn test_merge() {
449
+ struct TestCase {
450
+ name: &'static str,
451
+ metric_type: &'static str,
452
+ multiprocess_mode: &'static str,
453
+ values: &'static [f64],
454
+ expected_value: f64,
455
+ }
456
+
457
+ let _cleanup = unsafe { magnus::embed::init() };
458
+ let ruby = magnus::Ruby::get().unwrap();
459
+ crate::init(&ruby).unwrap();
460
+
461
+ let tc = vec![
462
+ TestCase {
463
+ name: "gauge max",
464
+ metric_type: "gauge",
465
+ multiprocess_mode: "max",
466
+ values: &[1.0, 5.0],
467
+ expected_value: 5.0,
468
+ },
469
+ TestCase {
470
+ name: "gauge min",
471
+ metric_type: "gauge",
472
+ multiprocess_mode: "min",
473
+ values: &[1.0, 5.0],
474
+ expected_value: 1.0,
475
+ },
476
+ TestCase {
477
+ name: "gauge livesum",
478
+ metric_type: "gauge",
479
+ multiprocess_mode: "livesum",
480
+ values: &[1.0, 5.0],
481
+ expected_value: 6.0,
482
+ },
483
+ TestCase {
484
+ name: "gauge all",
485
+ metric_type: "gauge",
486
+ multiprocess_mode: "all",
487
+ values: &[1.0, 5.0],
488
+ expected_value: 5.0,
489
+ },
490
+ TestCase {
491
+ name: "not a gauge",
492
+ metric_type: "histogram",
493
+ multiprocess_mode: "max",
494
+ values: &[1.0, 5.0],
495
+ expected_value: 6.0,
496
+ },
497
+ ];
498
+
499
+ for case in tc {
500
+ let name = case.name;
501
+ let json = r#"["family","metric",["label_a","label_b"],["value_a","value_b"]]"#;
502
+
503
+ let TestFile {
504
+ file,
505
+ path,
506
+ dir: _dir,
507
+ } = TestFile::new(b"foobar");
508
+
509
+ let info = FileInfo {
510
+ file,
511
+ path,
512
+ len: json.len(),
513
+ multiprocess_mode: Symbol::new(case.multiprocess_mode),
514
+ type_: StaticSymbol::new(case.metric_type),
515
+ pid: "worker-1".to_string(),
516
+ };
517
+
518
+ let input_bytes: Vec<BString> = case
519
+ .values
520
+ .iter()
521
+ .map(|&value| TestEntry::new(json, value).as_bstring())
522
+ .collect();
523
+
524
+ let entries: Vec<FileEntry> = input_bytes
525
+ .iter()
526
+ .map(|s| RawEntry::from_slice(s).unwrap())
527
+ .map(|entry| {
528
+ let meta = EntryMetadata::new(&entry, &info).unwrap();
529
+ let data = EntryData::new(&entry, &info, meta.is_pid_significant()).unwrap();
530
+ FileEntry { data, meta }
531
+ })
532
+ .collect();
533
+
534
+ let mut entry_a = entries[0].clone();
535
+ let entry_b = entries[1].clone();
536
+ entry_a.meta.merge(&entry_b.meta);
537
+
538
+ assert_eq!(
539
+ case.expected_value, entry_a.meta.value,
540
+ "test case: {name} - value"
541
+ );
542
+ }
543
+ }
544
+ }