prometheus-client-mmap 1.2.4-aarch64-linux-gnu

Sign up to get free protection for your applications and to get access to all the features.
Files changed (52) hide show
  1. checksums.yaml +7 -0
  2. data/.tool-versions +1 -0
  3. data/README.md +281 -0
  4. data/ext/fast_mmaped_file_rs/Cargo.toml +35 -0
  5. data/ext/fast_mmaped_file_rs/README.md +52 -0
  6. data/ext/fast_mmaped_file_rs/build.rs +5 -0
  7. data/ext/fast_mmaped_file_rs/extconf.rb +28 -0
  8. data/ext/fast_mmaped_file_rs/src/error.rs +174 -0
  9. data/ext/fast_mmaped_file_rs/src/file_entry.rs +784 -0
  10. data/ext/fast_mmaped_file_rs/src/file_info.rs +240 -0
  11. data/ext/fast_mmaped_file_rs/src/lib.rs +78 -0
  12. data/ext/fast_mmaped_file_rs/src/macros.rs +14 -0
  13. data/ext/fast_mmaped_file_rs/src/map.rs +492 -0
  14. data/ext/fast_mmaped_file_rs/src/mmap/inner.rs +704 -0
  15. data/ext/fast_mmaped_file_rs/src/mmap.rs +891 -0
  16. data/ext/fast_mmaped_file_rs/src/raw_entry.rs +473 -0
  17. data/ext/fast_mmaped_file_rs/src/testhelper.rs +222 -0
  18. data/ext/fast_mmaped_file_rs/src/util.rs +121 -0
  19. data/lib/3.1/fast_mmaped_file_rs.so +0 -0
  20. data/lib/3.2/fast_mmaped_file_rs.so +0 -0
  21. data/lib/3.3/fast_mmaped_file_rs.so +0 -0
  22. data/lib/3.4/fast_mmaped_file_rs.so +0 -0
  23. data/lib/prometheus/client/configuration.rb +23 -0
  24. data/lib/prometheus/client/counter.rb +27 -0
  25. data/lib/prometheus/client/formats/text.rb +85 -0
  26. data/lib/prometheus/client/gauge.rb +40 -0
  27. data/lib/prometheus/client/helper/entry_parser.rb +132 -0
  28. data/lib/prometheus/client/helper/file_locker.rb +50 -0
  29. data/lib/prometheus/client/helper/json_parser.rb +23 -0
  30. data/lib/prometheus/client/helper/metrics_processing.rb +45 -0
  31. data/lib/prometheus/client/helper/metrics_representation.rb +51 -0
  32. data/lib/prometheus/client/helper/mmaped_file.rb +64 -0
  33. data/lib/prometheus/client/helper/plain_file.rb +29 -0
  34. data/lib/prometheus/client/histogram.rb +80 -0
  35. data/lib/prometheus/client/label_set_validator.rb +85 -0
  36. data/lib/prometheus/client/metric.rb +80 -0
  37. data/lib/prometheus/client/mmaped_dict.rb +79 -0
  38. data/lib/prometheus/client/mmaped_value.rb +154 -0
  39. data/lib/prometheus/client/page_size.rb +17 -0
  40. data/lib/prometheus/client/push.rb +203 -0
  41. data/lib/prometheus/client/rack/collector.rb +88 -0
  42. data/lib/prometheus/client/rack/exporter.rb +96 -0
  43. data/lib/prometheus/client/registry.rb +65 -0
  44. data/lib/prometheus/client/simple_value.rb +31 -0
  45. data/lib/prometheus/client/summary.rb +69 -0
  46. data/lib/prometheus/client/support/puma.rb +44 -0
  47. data/lib/prometheus/client/support/unicorn.rb +35 -0
  48. data/lib/prometheus/client/uses_value_type.rb +20 -0
  49. data/lib/prometheus/client/version.rb +5 -0
  50. data/lib/prometheus/client.rb +58 -0
  51. data/lib/prometheus.rb +3 -0
  52. metadata +249 -0
@@ -0,0 +1,784 @@
1
+ use magnus::Symbol;
2
+ use serde::Deserialize;
3
+ use serde_json::value::RawValue;
4
+ use smallvec::SmallVec;
5
+ use std::fmt::Write;
6
+ use std::str;
7
+
8
+ use crate::error::{MmapError, RubyError};
9
+ use crate::file_info::FileInfo;
10
+ use crate::raw_entry::RawEntry;
11
+ use crate::Result;
12
+ use crate::{SYM_GAUGE, SYM_LIVESUM, SYM_MAX, SYM_MIN};
13
+
14
+ /// A metrics entry extracted from a `*.db` file.
15
+ #[derive(Clone, Debug)]
16
+ pub struct FileEntry {
17
+ pub data: EntryData,
18
+ pub meta: EntryMetadata,
19
+ }
20
+
21
+ /// String slices pointing to the fields of a borrowed `Entry`'s JSON data.
22
+ #[derive(Deserialize, Debug)]
23
+ pub struct MetricText<'a> {
24
+ pub family_name: &'a str,
25
+ pub metric_name: &'a str,
26
+ pub labels: SmallVec<[&'a str; 4]>,
27
+ #[serde(borrow)]
28
+ pub values: SmallVec<[&'a RawValue; 4]>,
29
+ }
30
+
31
+ /// The primary data payload for a `FileEntry`, the JSON string and the
32
+ /// associated pid, if significant. Used as the key for `EntryMap`.
33
+ #[derive(Hash, PartialEq, Eq, PartialOrd, Ord, Clone, Debug)]
34
+ pub struct EntryData {
35
+ pub json: String,
36
+ pub pid: Option<String>,
37
+ }
38
+
39
+ impl<'a> PartialEq<BorrowedData<'a>> for EntryData {
40
+ fn eq(&self, other: &BorrowedData) -> bool {
41
+ self.pid.as_deref() == other.pid && self.json == other.json
42
+ }
43
+ }
44
+
45
+ impl<'a> TryFrom<BorrowedData<'a>> for EntryData {
46
+ type Error = MmapError;
47
+
48
+ fn try_from(borrowed: BorrowedData) -> Result<Self> {
49
+ let mut json = String::new();
50
+ if json.try_reserve_exact(borrowed.json.len()).is_err() {
51
+ return Err(MmapError::OutOfMemory(borrowed.json.len()));
52
+ }
53
+ json.push_str(borrowed.json);
54
+
55
+ Ok(Self {
56
+ json,
57
+ // Don't bother checking for allocation failure, typically ~10 bytes
58
+ pid: borrowed.pid.map(|p| p.to_string()),
59
+ })
60
+ }
61
+ }
62
+
63
+ /// A borrowed copy of the JSON string and pid for a `FileEntry`. We use this
64
+ /// to check if a given string/pid combination is present in the `EntryMap`,
65
+ /// copying them to owned values only when needed.
66
+ #[derive(Hash, PartialEq, Eq, PartialOrd, Ord, Clone, Debug)]
67
+ pub struct BorrowedData<'a> {
68
+ pub json: &'a str,
69
+ pub pid: Option<&'a str>,
70
+ }
71
+
72
+ impl<'a> BorrowedData<'a> {
73
+ pub fn new(
74
+ raw_entry: &'a RawEntry,
75
+ file_info: &'a FileInfo,
76
+ pid_significant: bool,
77
+ ) -> Result<Self> {
78
+ let json = str::from_utf8(raw_entry.json())
79
+ .map_err(|e| MmapError::Encoding(format!("invalid UTF-8 in entry JSON: {e}")))?;
80
+
81
+ let pid = if pid_significant {
82
+ Some(file_info.pid.as_str())
83
+ } else {
84
+ None
85
+ };
86
+
87
+ Ok(Self { json, pid })
88
+ }
89
+ }
90
+
91
+ /// The metadata associated with a `FileEntry`. The value in `EntryMap`.
92
+ #[derive(Clone, Debug)]
93
+ pub struct EntryMetadata {
94
+ pub multiprocess_mode: Symbol,
95
+ pub type_: Symbol,
96
+ pub value: f64,
97
+ }
98
+
99
+ impl EntryMetadata {
100
+ /// Construct a new `FileEntry`, copying the JSON string from the `RawEntry`
101
+ /// into an internal buffer.
102
+ pub fn new(mmap_entry: &RawEntry, file: &FileInfo) -> Result<Self> {
103
+ let value = mmap_entry.value();
104
+
105
+ Ok(EntryMetadata {
106
+ multiprocess_mode: file.multiprocess_mode,
107
+ type_: file.type_,
108
+ value,
109
+ })
110
+ }
111
+
112
+ /// Combine values with another `EntryMetadata`.
113
+ pub fn merge(&mut self, other: &Self) {
114
+ if self.type_ == SYM_GAUGE {
115
+ match self.multiprocess_mode {
116
+ s if s == SYM_MIN => self.value = self.value.min(other.value),
117
+ s if s == SYM_MAX => self.value = self.value.max(other.value),
118
+ s if s == SYM_LIVESUM => self.value += other.value,
119
+ _ => self.value = other.value,
120
+ }
121
+ } else {
122
+ self.value += other.value;
123
+ }
124
+ }
125
+
126
+ /// Validate if pid is significant for metric.
127
+ pub fn is_pid_significant(&self) -> bool {
128
+ let mp = self.multiprocess_mode;
129
+
130
+ self.type_ == SYM_GAUGE && !(mp == SYM_MIN || mp == SYM_MAX || mp == SYM_LIVESUM)
131
+ }
132
+ }
133
+
134
+ impl FileEntry {
135
+ /// Convert the sorted entries into a String in Prometheus metrics format.
136
+ pub fn entries_to_string(entries: Vec<FileEntry>) -> Result<String> {
137
+ // We guesstimate that lines are ~100 bytes long, preallocate the string to
138
+ // roughly that size.
139
+ let mut out = String::new();
140
+ out.try_reserve(entries.len() * 128)
141
+ .map_err(|_| MmapError::OutOfMemory(entries.len() * 128))?;
142
+
143
+ let mut prev_name: Option<String> = None;
144
+
145
+ let entry_count = entries.len();
146
+ let mut processed_count = 0;
147
+
148
+ for entry in entries {
149
+ let metrics_data = match serde_json::from_str::<MetricText>(&entry.data.json) {
150
+ Ok(m) => {
151
+ if m.labels.len() != m.values.len() {
152
+ continue;
153
+ }
154
+ m
155
+ }
156
+ // We don't exit the function here so the total number of invalid
157
+ // entries can be calculated below.
158
+ Err(_) => continue,
159
+ };
160
+
161
+ match prev_name.as_ref() {
162
+ Some(p) if p == metrics_data.family_name => {}
163
+ _ => {
164
+ entry.append_header(metrics_data.family_name, &mut out);
165
+ prev_name = Some(metrics_data.family_name.to_owned());
166
+ }
167
+ }
168
+
169
+ entry.append_entry(metrics_data, &mut out)?;
170
+
171
+ writeln!(&mut out, " {}", entry.meta.value)
172
+ .map_err(|e| MmapError::Other(format!("Failed to append to output: {e}")))?;
173
+
174
+ processed_count += 1;
175
+ }
176
+
177
+ if processed_count != entry_count {
178
+ return Err(MmapError::legacy(
179
+ format!("Processed entries {processed_count} != map entries {entry_count}"),
180
+ RubyError::Runtime,
181
+ ));
182
+ }
183
+
184
+ Ok(out)
185
+ }
186
+
187
+ fn append_header(&self, family_name: &str, out: &mut String) {
188
+ out.push_str("# HELP ");
189
+ out.push_str(family_name);
190
+ out.push_str(" Multiprocess metric\n");
191
+
192
+ out.push_str("# TYPE ");
193
+ out.push_str(family_name);
194
+ out.push(' ');
195
+
196
+ out.push_str(&self.meta.type_.name().expect("name was invalid UTF-8"));
197
+ out.push('\n');
198
+ }
199
+
200
+ fn append_entry(&self, json_data: MetricText, out: &mut String) -> Result<()> {
201
+ out.push_str(json_data.metric_name);
202
+
203
+ if json_data.labels.is_empty() {
204
+ if let Some(pid) = self.data.pid.as_ref() {
205
+ out.push_str("{pid=\"");
206
+ out.push_str(pid);
207
+ out.push_str("\"}");
208
+ }
209
+
210
+ return Ok(());
211
+ }
212
+
213
+ out.push('{');
214
+
215
+ let it = json_data.labels.iter().zip(json_data.values.iter());
216
+
217
+ for (i, (&key, val)) in it.enumerate() {
218
+ out.push_str(key);
219
+ out.push('=');
220
+
221
+ match val.get() {
222
+ "null" => out.push_str("\"\""),
223
+ s if s.starts_with('"') => out.push_str(s),
224
+ s => {
225
+ // Quote numeric values.
226
+ out.push('"');
227
+ out.push_str(s);
228
+ out.push('"');
229
+ }
230
+ }
231
+
232
+ if i < json_data.labels.len() - 1 {
233
+ out.push(',');
234
+ }
235
+ }
236
+
237
+ if let Some(pid) = self.data.pid.as_ref() {
238
+ out.push_str(",pid=\"");
239
+ out.push_str(pid);
240
+ out.push('"');
241
+ }
242
+
243
+ out.push('}');
244
+
245
+ Ok(())
246
+ }
247
+ }
248
+
249
+ #[cfg(test)]
250
+ mod test {
251
+ use bstr::BString;
252
+ use indoc::indoc;
253
+
254
+ use super::*;
255
+ use crate::file_info::FileInfo;
256
+ use crate::raw_entry::RawEntry;
257
+ use crate::testhelper::{TestEntry, TestFile};
258
+
259
+ #[test]
260
+ fn test_entries_to_string() {
261
+ struct TestCase {
262
+ name: &'static str,
263
+ multiprocess_mode: &'static str,
264
+ json: &'static [&'static str],
265
+ values: &'static [f64],
266
+ pids: &'static [&'static str],
267
+ expected_out: Option<&'static str>,
268
+ expected_err: Option<MmapError>,
269
+ }
270
+
271
+ let _cleanup = unsafe { magnus::embed::init() };
272
+ let ruby = magnus::Ruby::get().unwrap();
273
+ crate::init(&ruby).unwrap();
274
+
275
+ let tc = vec![
276
+ TestCase {
277
+ name: "one metric, pid significant",
278
+ multiprocess_mode: "all",
279
+ json: &[r#"["family","name",["label_a","label_b"],["value_a","value_b"]]"#],
280
+ values: &[1.0],
281
+ pids: &["worker-1"],
282
+ expected_out: Some(indoc! {r##"# HELP family Multiprocess metric
283
+ # TYPE family gauge
284
+ name{label_a="value_a",label_b="value_b",pid="worker-1"} 1
285
+ "##}),
286
+ expected_err: None,
287
+ },
288
+ TestCase {
289
+ name: "one metric, no pid",
290
+ multiprocess_mode: "min",
291
+ json: &[r#"["family","name",["label_a","label_b"],["value_a","value_b"]]"#],
292
+ values: &[1.0],
293
+ pids: &["worker-1"],
294
+ expected_out: Some(indoc! {r##"# HELP family Multiprocess metric
295
+ # TYPE family gauge
296
+ name{label_a="value_a",label_b="value_b"} 1
297
+ "##}),
298
+ expected_err: None,
299
+ },
300
+ TestCase {
301
+ name: "many labels",
302
+ multiprocess_mode: "min",
303
+ json: &[
304
+ r#"["family","name",["label_a","label_b","label_c","label_d","label_e"],["value_a","value_b","value_c","value_d","value_e"]]"#,
305
+ ],
306
+ values: &[1.0],
307
+ pids: &["worker-1"],
308
+ expected_out: Some(indoc! {r##"# HELP family Multiprocess metric
309
+ # TYPE family gauge
310
+ name{label_a="value_a",label_b="value_b",label_c="value_c",label_d="value_d",label_e="value_e"} 1
311
+ "##}),
312
+ expected_err: None,
313
+ },
314
+ TestCase {
315
+ name: "floating point shown",
316
+ multiprocess_mode: "min",
317
+ json: &[r#"["family","name",["label_a","label_b"],["value_a","value_b"]]"#],
318
+ values: &[1.5],
319
+ pids: &["worker-1"],
320
+ expected_out: Some(indoc! {r##"# HELP family Multiprocess metric
321
+ # TYPE family gauge
322
+ name{label_a="value_a",label_b="value_b"} 1.5
323
+ "##}),
324
+ expected_err: None,
325
+ },
326
+ TestCase {
327
+ name: "numeric value",
328
+ multiprocess_mode: "min",
329
+ json: &[
330
+ r#"["family","name",["label_a","label_b","label_c"],["value_a",403,-0.2E5]]"#,
331
+ ],
332
+ values: &[1.5],
333
+ pids: &["worker-1"],
334
+ expected_out: Some(indoc! {r##"# HELP family Multiprocess metric
335
+ # TYPE family gauge
336
+ name{label_a="value_a",label_b="403",label_c="-0.2E5"} 1.5
337
+ "##}),
338
+ expected_err: None,
339
+ },
340
+ TestCase {
341
+ name: "null value",
342
+ multiprocess_mode: "min",
343
+ json: &[r#"["family","name",["label_a","label_b"],["value_a",null]]"#],
344
+ values: &[1.5],
345
+ pids: &["worker-1"],
346
+ expected_out: Some(indoc! {r##"# HELP family Multiprocess metric
347
+ # TYPE family gauge
348
+ name{label_a="value_a",label_b=""} 1.5
349
+ "##}),
350
+ expected_err: None,
351
+ },
352
+ TestCase {
353
+ name: "comma in value",
354
+ multiprocess_mode: "min",
355
+ json: &[r#"["family","name",["label_a","label_b"],["value_a","value,_b"]]"#],
356
+ values: &[1.5],
357
+ pids: &["worker-1"],
358
+ expected_out: Some(indoc! {r##"# HELP family Multiprocess metric
359
+ # TYPE family gauge
360
+ name{label_a="value_a",label_b="value,_b"} 1.5
361
+ "##}),
362
+ expected_err: None,
363
+ },
364
+ TestCase {
365
+ name: "no labels, pid significant",
366
+ multiprocess_mode: "all",
367
+ json: &[r#"["family","name",[],[]]"#],
368
+ values: &[1.0],
369
+ pids: &["worker-1"],
370
+ expected_out: Some(indoc! {r##"# HELP family Multiprocess metric
371
+ # TYPE family gauge
372
+ name{pid="worker-1"} 1
373
+ "##}),
374
+ expected_err: None,
375
+ },
376
+ TestCase {
377
+ name: "no labels, no pid",
378
+ multiprocess_mode: "min",
379
+ json: &[r#"["family","name",[],[]]"#],
380
+ values: &[1.0],
381
+ pids: &["worker-1"],
382
+ expected_out: Some(indoc! {r##"# HELP family Multiprocess metric
383
+ # TYPE family gauge
384
+ name 1
385
+ "##}),
386
+ expected_err: None,
387
+ },
388
+ TestCase {
389
+ name: "two metrics, same family, pid significant",
390
+ multiprocess_mode: "all",
391
+ json: &[
392
+ r#"["family","first",["label_a","label_b"],["value_a","value_b"]]"#,
393
+ r#"["family","second",["label_a","label_b"],["value_a","value_b"]]"#,
394
+ ],
395
+ values: &[1.0, 2.0],
396
+ pids: &["worker-1", "worker-1"],
397
+ expected_out: Some(indoc! {r##"# HELP family Multiprocess metric
398
+ # TYPE family gauge
399
+ first{label_a="value_a",label_b="value_b",pid="worker-1"} 1
400
+ second{label_a="value_a",label_b="value_b",pid="worker-1"} 2
401
+ "##}),
402
+ expected_err: None,
403
+ },
404
+ TestCase {
405
+ name: "two metrics, different family, pid significant",
406
+ multiprocess_mode: "min",
407
+ json: &[
408
+ r#"["first_family","first_name",["label_a","label_b"],["value_a","value_b"]]"#,
409
+ r#"["second_family","second_name",["label_a","label_b"],["value_a","value_b"]]"#,
410
+ ],
411
+ values: &[1.0, 2.0],
412
+ pids: &["worker-1", "worker-1"],
413
+ expected_out: Some(indoc! {r##"# HELP first_family Multiprocess metric
414
+ # TYPE first_family gauge
415
+ first_name{label_a="value_a",label_b="value_b"} 1
416
+ # HELP second_family Multiprocess metric
417
+ # TYPE second_family gauge
418
+ second_name{label_a="value_a",label_b="value_b"} 2
419
+ "##}),
420
+ expected_err: None,
421
+ },
422
+ TestCase {
423
+ name: "three metrics, two different families, pid significant",
424
+ multiprocess_mode: "all",
425
+ json: &[
426
+ r#"["first_family","first_name",["label_a","label_b"],["value_a","value_b"]]"#,
427
+ r#"["first_family","second_name",["label_a","label_b"],["value_a","value_b"]]"#,
428
+ r#"["second_family","second_name",["label_a","label_b"],["value_a","value_b"]]"#,
429
+ ],
430
+ values: &[1.0, 2.0, 3.0],
431
+ pids: &["worker-1", "worker-1", "worker-1"],
432
+ expected_out: Some(indoc! {r##"# HELP first_family Multiprocess metric
433
+ # TYPE first_family gauge
434
+ first_name{label_a="value_a",label_b="value_b",pid="worker-1"} 1
435
+ second_name{label_a="value_a",label_b="value_b",pid="worker-1"} 2
436
+ # HELP second_family Multiprocess metric
437
+ # TYPE second_family gauge
438
+ second_name{label_a="value_a",label_b="value_b",pid="worker-1"} 3
439
+ "##}),
440
+ expected_err: None,
441
+ },
442
+ TestCase {
443
+ name: "same metrics, pid significant, separate workers",
444
+ multiprocess_mode: "all",
445
+ json: &[
446
+ r#"["first_family","first_name",["label_a","label_b"],["value_a","value_b"]]"#,
447
+ r#"["first_family","first_name",["label_a","label_b"],["value_a","value_b"]]"#,
448
+ ],
449
+ values: &[1.0, 2.0],
450
+ pids: &["worker-1", "worker-2"],
451
+ expected_out: Some(indoc! {r##"# HELP first_family Multiprocess metric
452
+ # TYPE first_family gauge
453
+ first_name{label_a="value_a",label_b="value_b",pid="worker-1"} 1
454
+ first_name{label_a="value_a",label_b="value_b",pid="worker-2"} 2
455
+ "##}),
456
+ expected_err: None,
457
+ },
458
+ TestCase {
459
+ name: "same metrics, pid not significant, separate workers",
460
+ multiprocess_mode: "max",
461
+ json: &[
462
+ r#"["first_family","first_name",["label_a","label_b"],["value_a","value_b"]]"#,
463
+ r#"["first_family","first_name",["label_a","label_b"],["value_a","value_b"]]"#,
464
+ ],
465
+ values: &[1.0, 2.0],
466
+ pids: &["worker-1", "worker-2"],
467
+ expected_out: Some(indoc! {r##"# HELP first_family Multiprocess metric
468
+ # TYPE first_family gauge
469
+ first_name{label_a="value_a",label_b="value_b"} 1
470
+ first_name{label_a="value_a",label_b="value_b"} 2
471
+ "##}),
472
+ expected_err: None,
473
+ },
474
+ TestCase {
475
+ name: "entry fails to parse",
476
+ multiprocess_mode: "min",
477
+ json: &[
478
+ r#"["first_family","first_name",["label_a","label_b"],["value_a","value_b"]]"#,
479
+ r#"[not valid"#,
480
+ ],
481
+ values: &[1.0, 2.0],
482
+ pids: &["worker-1", "worker-1"],
483
+ expected_out: None,
484
+ expected_err: Some(MmapError::legacy(
485
+ "Processed entries 1 != map entries 2".to_owned(),
486
+ RubyError::Runtime,
487
+ )),
488
+ },
489
+ TestCase {
490
+ name: "too many values",
491
+ multiprocess_mode: "min",
492
+ json: &[r#"["family","name",["label_a"],["value_a","value,_b"]]"#],
493
+ values: &[1.5],
494
+ pids: &["worker-1"],
495
+ expected_out: None,
496
+ expected_err: Some(MmapError::legacy(
497
+ "Processed entries 0 != map entries 1".to_owned(),
498
+ RubyError::Runtime,
499
+ )),
500
+ },
501
+ TestCase {
502
+ name: "no values",
503
+ multiprocess_mode: "min",
504
+ json: &[r#"["family","name",["label_a"]]"#],
505
+ values: &[1.5],
506
+ pids: &["worker-1"],
507
+ expected_out: None,
508
+ expected_err: Some(MmapError::legacy(
509
+ "Processed entries 0 != map entries 1".to_owned(),
510
+ RubyError::Runtime,
511
+ )),
512
+ },
513
+ TestCase {
514
+ name: "no labels or values",
515
+ multiprocess_mode: "min",
516
+ json: &[r#"["family","name","foo"]"#],
517
+ values: &[1.5],
518
+ pids: &["worker-1"],
519
+ expected_out: None,
520
+ expected_err: Some(MmapError::legacy(
521
+ "Processed entries 0 != map entries 1".to_owned(),
522
+ RubyError::Runtime,
523
+ )),
524
+ },
525
+ TestCase {
526
+ name: "too many leading brackets",
527
+ multiprocess_mode: "min",
528
+ json: &[r#"[["family","name",["label_a","label_b"],["value_a","value_b"]]"#],
529
+ values: &[1.5],
530
+ pids: &["worker-1"],
531
+ expected_out: None,
532
+ expected_err: Some(MmapError::legacy(
533
+ "Processed entries 0 != map entries 1".to_owned(),
534
+ RubyError::Runtime,
535
+ )),
536
+ },
537
+ TestCase {
538
+ name: "too many trailing brackets",
539
+ multiprocess_mode: "min",
540
+ json: &[r#"["family","name",["label_a","label_b"],["value_a","value_b"]]]"#],
541
+ values: &[1.5],
542
+ pids: &["worker-1"],
543
+ expected_out: None,
544
+ expected_err: Some(MmapError::legacy(
545
+ "Processed entries 0 != map entries 1".to_owned(),
546
+ RubyError::Runtime,
547
+ )),
548
+ },
549
+ TestCase {
550
+ name: "too many leading label brackets",
551
+ multiprocess_mode: "min",
552
+ json: &[r#"["family","name",[["label_a","label_b"],["value_a","value_b"]]"#],
553
+ values: &[1.5],
554
+ pids: &["worker-1"],
555
+ expected_out: None,
556
+ expected_err: Some(MmapError::legacy(
557
+ "Processed entries 0 != map entries 1".to_owned(),
558
+ RubyError::Runtime,
559
+ )),
560
+ },
561
+ TestCase {
562
+ name: "too many leading label brackets",
563
+ multiprocess_mode: "min",
564
+ json: &[r#"["family","name",[["label_a","label_b"],["value_a","value_b"]]"#],
565
+ values: &[1.5],
566
+ pids: &["worker-1"],
567
+ expected_out: None,
568
+ expected_err: Some(MmapError::legacy(
569
+ "Processed entries 0 != map entries 1".to_owned(),
570
+ RubyError::Runtime,
571
+ )),
572
+ },
573
+ TestCase {
574
+ name: "too many leading value brackets",
575
+ multiprocess_mode: "min",
576
+ json: &[r#"["family","name",["label_a","label_b"],[["value_a","value_b"]]"#],
577
+ values: &[1.5],
578
+ pids: &["worker-1"],
579
+ expected_out: None,
580
+ expected_err: Some(MmapError::legacy(
581
+ "Processed entries 0 != map entries 1".to_owned(),
582
+ RubyError::Runtime,
583
+ )),
584
+ },
585
+ TestCase {
586
+ name: "misplaced bracket",
587
+ multiprocess_mode: "min",
588
+ json: &[r#"["family","name",["label_a","label_b"],]["value_a","value_b"]]"#],
589
+ values: &[1.5],
590
+ pids: &["worker-1"],
591
+ expected_out: None,
592
+ expected_err: Some(MmapError::legacy(
593
+ "Processed entries 0 != map entries 1".to_owned(),
594
+ RubyError::Runtime,
595
+ )),
596
+ },
597
+ TestCase {
598
+ name: "comma in numeric",
599
+ multiprocess_mode: "min",
600
+ json: &[r#"["family","name",["label_a","label_b"],["value_a",403,0]]"#],
601
+ values: &[1.5],
602
+ pids: &["worker-1"],
603
+ expected_out: None,
604
+ expected_err: Some(MmapError::legacy(
605
+ "Processed entries 0 != map entries 1".to_owned(),
606
+ RubyError::Runtime,
607
+ )),
608
+ },
609
+ TestCase {
610
+ name: "non-e letter in numeric",
611
+ multiprocess_mode: "min",
612
+ json: &[r#"["family","name",["label_a","label_b"],["value_a",-2.0c5]]"#],
613
+ values: &[1.5],
614
+ pids: &["worker-1"],
615
+ expected_out: None,
616
+ expected_err: Some(MmapError::legacy(
617
+ "Processed entries 0 != map entries 1".to_owned(),
618
+ RubyError::Runtime,
619
+ )),
620
+ },
621
+ ];
622
+
623
+ for case in tc {
624
+ let name = case.name;
625
+
626
+ let input_bytes: Vec<BString> = case
627
+ .json
628
+ .iter()
629
+ .zip(case.values)
630
+ .map(|(&s, &value)| TestEntry::new(s, value).as_bstring())
631
+ .collect();
632
+
633
+ let mut file_infos = Vec::new();
634
+ for pid in case.pids {
635
+ let TestFile {
636
+ file,
637
+ path,
638
+ dir: _dir,
639
+ } = TestFile::new(b"foobar");
640
+
641
+ let info = FileInfo {
642
+ file,
643
+ path,
644
+ len: case.json.len(),
645
+ multiprocess_mode: Symbol::new(case.multiprocess_mode),
646
+ type_: Symbol::new("gauge"),
647
+ pid: pid.to_string(),
648
+ };
649
+ file_infos.push(info);
650
+ }
651
+
652
+ let file_entries: Vec<FileEntry> = input_bytes
653
+ .iter()
654
+ .map(|s| RawEntry::from_slice(s).unwrap())
655
+ .zip(file_infos)
656
+ .map(|(entry, info)| {
657
+ let meta = EntryMetadata::new(&entry, &info).unwrap();
658
+ let borrowed =
659
+ BorrowedData::new(&entry, &info, meta.is_pid_significant()).unwrap();
660
+ let data = EntryData::try_from(borrowed).unwrap();
661
+ FileEntry { data, meta }
662
+ })
663
+ .collect();
664
+
665
+ let output = FileEntry::entries_to_string(file_entries);
666
+
667
+ if let Some(expected_out) = case.expected_out {
668
+ assert_eq!(
669
+ expected_out,
670
+ output.as_ref().unwrap(),
671
+ "test case: {name} - output"
672
+ );
673
+ }
674
+
675
+ if let Some(expected_err) = case.expected_err {
676
+ assert_eq!(
677
+ expected_err,
678
+ output.unwrap_err(),
679
+ "test case: {name} - error"
680
+ );
681
+ }
682
+ }
683
+ }
684
+
685
+ #[test]
686
+ fn test_merge() {
687
+ struct TestCase {
688
+ name: &'static str,
689
+ metric_type: &'static str,
690
+ multiprocess_mode: &'static str,
691
+ values: &'static [f64],
692
+ expected_value: f64,
693
+ }
694
+
695
+ let _cleanup = unsafe { magnus::embed::init() };
696
+ let ruby = magnus::Ruby::get().unwrap();
697
+ crate::init(&ruby).unwrap();
698
+
699
+ let tc = vec![
700
+ TestCase {
701
+ name: "gauge max",
702
+ metric_type: "gauge",
703
+ multiprocess_mode: "max",
704
+ values: &[1.0, 5.0],
705
+ expected_value: 5.0,
706
+ },
707
+ TestCase {
708
+ name: "gauge min",
709
+ metric_type: "gauge",
710
+ multiprocess_mode: "min",
711
+ values: &[1.0, 5.0],
712
+ expected_value: 1.0,
713
+ },
714
+ TestCase {
715
+ name: "gauge livesum",
716
+ metric_type: "gauge",
717
+ multiprocess_mode: "livesum",
718
+ values: &[1.0, 5.0],
719
+ expected_value: 6.0,
720
+ },
721
+ TestCase {
722
+ name: "gauge all",
723
+ metric_type: "gauge",
724
+ multiprocess_mode: "all",
725
+ values: &[1.0, 5.0],
726
+ expected_value: 5.0,
727
+ },
728
+ TestCase {
729
+ name: "not a gauge",
730
+ metric_type: "histogram",
731
+ multiprocess_mode: "max",
732
+ values: &[1.0, 5.0],
733
+ expected_value: 6.0,
734
+ },
735
+ ];
736
+
737
+ for case in tc {
738
+ let name = case.name;
739
+ let json = r#"["family","metric",["label_a","label_b"],["value_a","value_b"]]"#;
740
+
741
+ let TestFile {
742
+ file,
743
+ path,
744
+ dir: _dir,
745
+ } = TestFile::new(b"foobar");
746
+
747
+ let info = FileInfo {
748
+ file,
749
+ path,
750
+ len: json.len(),
751
+ multiprocess_mode: Symbol::new(case.multiprocess_mode),
752
+ type_: Symbol::new(case.metric_type),
753
+ pid: "worker-1".to_string(),
754
+ };
755
+
756
+ let input_bytes: Vec<BString> = case
757
+ .values
758
+ .iter()
759
+ .map(|&value| TestEntry::new(json, value).as_bstring())
760
+ .collect();
761
+
762
+ let entries: Vec<FileEntry> = input_bytes
763
+ .iter()
764
+ .map(|s| RawEntry::from_slice(s).unwrap())
765
+ .map(|entry| {
766
+ let meta = EntryMetadata::new(&entry, &info).unwrap();
767
+ let borrowed =
768
+ BorrowedData::new(&entry, &info, meta.is_pid_significant()).unwrap();
769
+ let data = EntryData::try_from(borrowed).unwrap();
770
+ FileEntry { data, meta }
771
+ })
772
+ .collect();
773
+
774
+ let mut entry_a = entries[0].clone();
775
+ let entry_b = entries[1].clone();
776
+ entry_a.meta.merge(&entry_b.meta);
777
+
778
+ assert_eq!(
779
+ case.expected_value, entry_a.meta.value,
780
+ "test case: {name} - value"
781
+ );
782
+ }
783
+ }
784
+ }