prometheus-client-mmap 0.19.1 → 0.20.1

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,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
+ }