prometheus-client-mmap 0.19.1 → 0.20.1

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