prometheus-client-mmap 0.19.1 → 0.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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
+ }