vinted-prometheus-client-mmap 1.5.0-x86_64-linux

Sign up to get free protection for your applications and to get access to all the features.
Files changed (57) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +5 -0
  3. data/ext/fast_mmaped_file_rs/Cargo.toml +40 -0
  4. data/ext/fast_mmaped_file_rs/README.md +52 -0
  5. data/ext/fast_mmaped_file_rs/build.rs +7 -0
  6. data/ext/fast_mmaped_file_rs/extconf.rb +28 -0
  7. data/ext/fast_mmaped_file_rs/src/error.rs +174 -0
  8. data/ext/fast_mmaped_file_rs/src/exemplars.rs +25 -0
  9. data/ext/fast_mmaped_file_rs/src/file_entry.rs +1252 -0
  10. data/ext/fast_mmaped_file_rs/src/file_info.rs +240 -0
  11. data/ext/fast_mmaped_file_rs/src/lib.rs +89 -0
  12. data/ext/fast_mmaped_file_rs/src/macros.rs +14 -0
  13. data/ext/fast_mmaped_file_rs/src/map.rs +519 -0
  14. data/ext/fast_mmaped_file_rs/src/metrics.proto +153 -0
  15. data/ext/fast_mmaped_file_rs/src/mmap/inner.rs +775 -0
  16. data/ext/fast_mmaped_file_rs/src/mmap.rs +977 -0
  17. data/ext/fast_mmaped_file_rs/src/raw_entry.rs +547 -0
  18. data/ext/fast_mmaped_file_rs/src/testhelper.rs +222 -0
  19. data/ext/fast_mmaped_file_rs/src/util.rs +140 -0
  20. data/lib/.DS_Store +0 -0
  21. data/lib/2.7/fast_mmaped_file_rs.so +0 -0
  22. data/lib/3.0/fast_mmaped_file_rs.so +0 -0
  23. data/lib/3.1/fast_mmaped_file_rs.so +0 -0
  24. data/lib/3.2/fast_mmaped_file_rs.so +0 -0
  25. data/lib/3.3/fast_mmaped_file_rs.so +0 -0
  26. data/lib/prometheus/.DS_Store +0 -0
  27. data/lib/prometheus/client/configuration.rb +24 -0
  28. data/lib/prometheus/client/counter.rb +27 -0
  29. data/lib/prometheus/client/formats/protobuf.rb +93 -0
  30. data/lib/prometheus/client/formats/text.rb +85 -0
  31. data/lib/prometheus/client/gauge.rb +40 -0
  32. data/lib/prometheus/client/helper/entry_parser.rb +132 -0
  33. data/lib/prometheus/client/helper/file_locker.rb +50 -0
  34. data/lib/prometheus/client/helper/json_parser.rb +23 -0
  35. data/lib/prometheus/client/helper/metrics_processing.rb +45 -0
  36. data/lib/prometheus/client/helper/metrics_representation.rb +51 -0
  37. data/lib/prometheus/client/helper/mmaped_file.rb +64 -0
  38. data/lib/prometheus/client/helper/plain_file.rb +29 -0
  39. data/lib/prometheus/client/histogram.rb +80 -0
  40. data/lib/prometheus/client/label_set_validator.rb +85 -0
  41. data/lib/prometheus/client/metric.rb +80 -0
  42. data/lib/prometheus/client/mmaped_dict.rb +83 -0
  43. data/lib/prometheus/client/mmaped_value.rb +164 -0
  44. data/lib/prometheus/client/page_size.rb +17 -0
  45. data/lib/prometheus/client/push.rb +203 -0
  46. data/lib/prometheus/client/rack/collector.rb +88 -0
  47. data/lib/prometheus/client/rack/exporter.rb +102 -0
  48. data/lib/prometheus/client/registry.rb +65 -0
  49. data/lib/prometheus/client/simple_value.rb +31 -0
  50. data/lib/prometheus/client/summary.rb +69 -0
  51. data/lib/prometheus/client/support/puma.rb +44 -0
  52. data/lib/prometheus/client/support/unicorn.rb +35 -0
  53. data/lib/prometheus/client/uses_value_type.rb +20 -0
  54. data/lib/prometheus/client/version.rb +5 -0
  55. data/lib/prometheus/client.rb +58 -0
  56. data/lib/prometheus.rb +3 -0
  57. metadata +210 -0
@@ -0,0 +1,1252 @@
1
+ use core::panic;
2
+ use magnus::Symbol;
3
+ use serde::Deserialize;
4
+ use serde_json::value::RawValue;
5
+ use smallvec::SmallVec;
6
+ use std::fmt::Write;
7
+ use std::str;
8
+
9
+ use crate::error::{MmapError, RubyError};
10
+ use crate::exemplars::Exemplar;
11
+ use crate::file_info::FileInfo;
12
+ use crate::raw_entry::RawEntry;
13
+ use crate::Result;
14
+ use crate::{SYM_GAUGE, SYM_LIVESUM, SYM_MAX, SYM_MIN};
15
+ use std::io::Cursor;
16
+ use varint_rs::VarintWriter;
17
+
18
+ pub mod io {
19
+ pub mod prometheus {
20
+ pub mod client {
21
+ include!(concat!(env!("OUT_DIR"), "/io.prometheus.client.rs"));
22
+ }
23
+ }
24
+ }
25
+
26
+ /// A metrics entry extracted from a `*.db` file.
27
+ #[derive(Clone, Debug)]
28
+ pub struct FileEntry {
29
+ pub data: EntryData,
30
+ pub meta: EntryMetadata,
31
+ }
32
+
33
+ /// String slices pointing to the fields of a borrowed `Entry`'s JSON data.
34
+ #[derive(Deserialize, Debug, Clone)]
35
+ pub struct MetricText<'a> {
36
+ pub family_name: &'a str,
37
+ pub metric_name: &'a str,
38
+ pub labels: SmallVec<[&'a str; 4]>,
39
+ #[serde(borrow)]
40
+ pub values: SmallVec<[&'a RawValue; 4]>,
41
+ }
42
+
43
+ /// The primary data payload for a `FileEntry`, the JSON string and the
44
+ /// associated pid, if significant. Used as the key for `EntryMap`.
45
+ #[derive(Hash, PartialEq, Eq, PartialOrd, Ord, Clone, Debug)]
46
+ pub struct EntryData {
47
+ pub json: String,
48
+ pub pid: Option<String>,
49
+ }
50
+
51
+ impl<'a> PartialEq<BorrowedData<'a>> for EntryData {
52
+ fn eq(&self, other: &BorrowedData) -> bool {
53
+ self.pid.as_deref() == other.pid && self.json == other.json
54
+ }
55
+ }
56
+
57
+ impl<'a> TryFrom<BorrowedData<'a>> for EntryData {
58
+ type Error = MmapError;
59
+
60
+ fn try_from(borrowed: BorrowedData) -> Result<Self> {
61
+ let mut json = String::new();
62
+ if json.try_reserve_exact(borrowed.json.len()).is_err() {
63
+ return Err(MmapError::OutOfMemory(borrowed.json.len()));
64
+ }
65
+ json.push_str(borrowed.json);
66
+
67
+ Ok(Self {
68
+ json,
69
+ // Don't bother checking for allocation failure, typically ~10 bytes
70
+ pid: borrowed.pid.map(|p| p.to_string()),
71
+ })
72
+ }
73
+ }
74
+
75
+ /// A borrowed copy of the JSON string and pid for a `FileEntry`. We use this
76
+ /// to check if a given string/pid combination is present in the `EntryMap`,
77
+ /// copying them to owned values only when needed.
78
+ #[derive(Hash, PartialEq, Eq, PartialOrd, Ord, Clone, Debug)]
79
+ pub struct BorrowedData<'a> {
80
+ pub json: &'a str,
81
+ pub pid: Option<&'a str>,
82
+ }
83
+
84
+ impl<'a> BorrowedData<'a> {
85
+ pub fn new(
86
+ raw_entry: &'a RawEntry,
87
+ file_info: &'a FileInfo,
88
+ pid_significant: bool,
89
+ ) -> Result<Self> {
90
+ let json = str::from_utf8(raw_entry.json())
91
+ .map_err(|e| MmapError::Encoding(format!("invalid UTF-8 in entry JSON: {e}")))?;
92
+
93
+ let pid = if pid_significant {
94
+ Some(file_info.pid.as_str())
95
+ } else {
96
+ None
97
+ };
98
+
99
+ Ok(Self { json, pid })
100
+ }
101
+ }
102
+
103
+ /// The metadata associated with a `FileEntry`. The value in `EntryMap`.
104
+ #[derive(Clone, Debug)]
105
+ pub struct EntryMetadata {
106
+ pub multiprocess_mode: Symbol,
107
+ pub type_: Symbol,
108
+ pub value: Option<f64>,
109
+ pub ex: Option<Exemplar>,
110
+ }
111
+
112
+ impl EntryMetadata {
113
+ /// Construct a new `FileEntry`, copying the JSON string from the `RawEntry`
114
+ /// into an internal buffer.
115
+ pub fn new(mmap_entry: &RawEntry, file: &FileInfo) -> Result<Self> {
116
+ if file.type_.to_string() == "exemplar" {
117
+ let ex = mmap_entry.exemplar();
118
+
119
+
120
+
121
+ return Ok(EntryMetadata {
122
+ multiprocess_mode: file.multiprocess_mode,
123
+ type_: file.type_,
124
+ value: None,
125
+ ex: Some(ex),
126
+ })
127
+ }
128
+
129
+ let value = mmap_entry.value();
130
+
131
+ Ok(EntryMetadata {
132
+ multiprocess_mode: file.multiprocess_mode,
133
+ type_: file.type_,
134
+ value: Some(value),
135
+ ex: None,
136
+ })
137
+ }
138
+
139
+ /// Combine values with another `EntryMetadata`.
140
+ pub fn merge(&mut self, other: &Self) {
141
+ if other.ex.is_some() {
142
+ let otherex = other.ex.clone().unwrap();
143
+
144
+ if self.ex.is_some() {
145
+ let selfex = self.ex.clone().unwrap();
146
+
147
+ if selfex.timestamp < otherex.timestamp {
148
+ self.ex = other.ex.clone();
149
+ }
150
+ } else {
151
+ self.ex = other.ex.clone();
152
+ }
153
+ }
154
+ if other.value.is_some() {
155
+ if self.value.is_none() {
156
+ self.value = other.value;
157
+ } else {
158
+ let other_value = other.value.unwrap();
159
+ let self_value = self.value.unwrap();
160
+
161
+ if self.type_ == SYM_GAUGE {
162
+ match self.multiprocess_mode {
163
+ s if s == SYM_MIN => self.value = Some(self_value.min(other_value)),
164
+ s if s == SYM_MAX => self.value = Some(self_value.max(other_value)),
165
+ s if s == SYM_LIVESUM => self.value = Some(self_value + other_value),
166
+ _ => self.value = Some(other_value),
167
+ }
168
+ } else {
169
+ self.value = Some(self_value + other_value);
170
+ }
171
+ }
172
+ }
173
+
174
+ }
175
+
176
+ /// Validate if pid is significant for metric.
177
+ pub fn is_pid_significant(&self) -> bool {
178
+ let mp = self.multiprocess_mode;
179
+
180
+ self.type_ == SYM_GAUGE && !(mp == SYM_MIN || mp == SYM_MAX || mp == SYM_LIVESUM)
181
+ }
182
+ }
183
+
184
+ use crate::io::prometheus::client::MetricType::{Counter, Gauge, Histogram, Summary};
185
+ use itertools::Itertools;
186
+ use prost::Message;
187
+ use std::collections::hash_map::DefaultHasher;
188
+ use std::collections::HashMap;
189
+ use std::hash::Hash;
190
+ use std::hash::Hasher;
191
+
192
+ use std::io::Write as OtherWrite;
193
+
194
+ fn exemplar_to_proto(e: &Exemplar) -> io::prometheus::client::Exemplar {
195
+ let seconds = e.timestamp / (1000 * 1000 * 1000);
196
+ let nanos = e.timestamp % (1000 * 1000 * 1000);
197
+
198
+ io::prometheus::client::Exemplar {
199
+ label: vec![io::prometheus::client::LabelPair {
200
+ name: Some(e.label_name.clone()),
201
+ value: Some(e.label_value.clone()),
202
+ }],
203
+ value: Some(e.value),
204
+ timestamp: Some(prost_types::Timestamp {
205
+ seconds: seconds as i64,
206
+ nanos: nanos as i32,
207
+ }),
208
+ }
209
+ }
210
+
211
+ impl FileEntry {
212
+ pub fn trim_quotes(s: &str) -> String {
213
+ let mut chars = s.chars();
214
+
215
+ if s.starts_with('"') {
216
+ chars.next();
217
+ }
218
+ if s.ends_with('"') {
219
+ chars.next_back();
220
+ }
221
+
222
+ chars.as_str().to_string()
223
+ }
224
+
225
+ pub fn entries_to_protobuf(entries: Vec<FileEntry>) -> Result<String> {
226
+ let mut buffer: Cursor<Vec<u8>> = Cursor::new(Vec::new());
227
+ let mut mtrcs: HashMap<u64, io::prometheus::client::Metric> = HashMap::new();
228
+ let mut metric_types = HashMap::new();
229
+ let mut metric_names = HashMap::new();
230
+
231
+ entries
232
+ .iter()
233
+ // TODO: Don't just unwrap. Handle the error gracefully.
234
+ .map(|v| {
235
+ (
236
+ v,
237
+ serde_json::from_str::<MetricText>(&v.data.json)
238
+ .expect("cannot parse json entry"),
239
+ v.meta.type_.name().expect("getting name").into_owned(),
240
+ )
241
+ })
242
+ .filter(|v| v.1.labels.len() == v.1.values.len())
243
+ .group_by(|v| v.1.family_name)
244
+ .into_iter()
245
+ .for_each(|(_, group)| {
246
+ // NOTE(GiedriusS): different dynamic labels fall under the same
247
+ // metric group.
248
+
249
+ 'outer: for gr in group {
250
+ let metric_type = gr.2;
251
+
252
+ let lbls =
253
+ gr.1.labels
254
+ .iter()
255
+ .map(|l| Self::trim_quotes(l))
256
+ .zip(gr.1.values.iter().map(|v| Self::trim_quotes(v.get())));
257
+
258
+ let mut m = io::prometheus::client::Metric {
259
+ label: lbls
260
+ .clone()
261
+ .map(|l| io::prometheus::client::LabelPair {
262
+ name: Some(l.0),
263
+ value: Some(l.1.to_string()),
264
+ })
265
+ .collect::<Vec<io::prometheus::client::LabelPair>>(),
266
+ gauge: None,
267
+ counter: None,
268
+ summary: None,
269
+ untyped: None,
270
+ histogram: None,
271
+ timestamp_ms: None,
272
+ };
273
+
274
+ match metric_type.as_str() {
275
+ "counter" => {
276
+ let mut hasher = DefaultHasher::new();
277
+
278
+ // Iterate over the tuples and hash their elements
279
+ for (a, b) in lbls {
280
+ a.hash(&mut hasher);
281
+ b.hash(&mut hasher);
282
+ }
283
+ "counter".hash(&mut hasher);
284
+
285
+ // Get the final u64 hash value
286
+ let hash_value = hasher.finish();
287
+
288
+ m.counter = Some(io::prometheus::client::Counter {
289
+ value: gr.0.meta.value,
290
+ created_timestamp: None,
291
+ exemplar: None,
292
+ });
293
+
294
+ if gr.0.meta.ex.is_some() {
295
+ m.counter.as_mut().unwrap().exemplar =
296
+ Some(exemplar_to_proto(gr.0.meta.ex.as_ref().unwrap()));
297
+ }
298
+
299
+ mtrcs.insert(hash_value, m);
300
+ metric_types.insert(hash_value, "counter");
301
+ metric_names.insert(hash_value, gr.1.metric_name);
302
+ }
303
+ "gauge" => {
304
+ let mut hasher = DefaultHasher::new();
305
+
306
+ // Iterate over the tuples and hash their elements
307
+ for (a, b) in lbls {
308
+ a.hash(&mut hasher);
309
+ b.hash(&mut hasher);
310
+ }
311
+ "gauge".hash(&mut hasher);
312
+
313
+ let hash_value = hasher.finish();
314
+
315
+ m.gauge = Some(io::prometheus::client::Gauge {
316
+ value: gr.0.meta.value,
317
+ });
318
+ mtrcs.insert(hash_value, m);
319
+ metric_types.insert(hash_value, "gauge");
320
+ metric_names.insert(hash_value, gr.1.metric_name);
321
+ }
322
+ "histogram" => {
323
+ let mut hasher = DefaultHasher::new();
324
+
325
+ let mut le: Option<f64> = None;
326
+
327
+ // Iterate over the tuples and hash their elements
328
+ for (a, b) in lbls {
329
+ if a != "le" {
330
+ a.hash(&mut hasher);
331
+ b.hash(&mut hasher);
332
+ }
333
+
334
+ // Safe to ignore +Inf bound.
335
+ if a == "le" {
336
+ if b == "+Inf" {
337
+ continue 'outer;
338
+ }
339
+ let leparsed = b.parse::<f64>();
340
+ match leparsed {
341
+ Ok(p) => le = Some(p),
342
+ Err(e) => panic!("failed to parse {} due to {}", b, e),
343
+ }
344
+ }
345
+ }
346
+ "histogram".hash(&mut hasher);
347
+
348
+ let hash_value = hasher.finish();
349
+
350
+ match mtrcs.get_mut(&hash_value) {
351
+ Some(v) => {
352
+ let hs =
353
+ v.histogram.as_mut().expect("getting mutable histogram");
354
+
355
+ for bucket in &mut hs.bucket {
356
+ if bucket.upper_bound != le {
357
+ continue;
358
+ }
359
+
360
+ let mut curf: f64 =
361
+ bucket.cumulative_count_float.unwrap_or_default();
362
+ curf += gr.0.meta.value.unwrap();
363
+
364
+ bucket.cumulative_count_float = Some(curf);
365
+
366
+ if gr.0.meta.ex.is_some() {
367
+ bucket.exemplar =
368
+ Some(exemplar_to_proto(gr.0.meta.ex.as_ref().unwrap()));
369
+ }
370
+ }
371
+ }
372
+ None => {
373
+ let mut final_metric_name = gr.1.metric_name;
374
+
375
+ if let Some(stripped) =
376
+ final_metric_name.strip_suffix("_bucket")
377
+ {
378
+ final_metric_name = stripped;
379
+ }
380
+ if let Some(stripped) = final_metric_name.strip_suffix("_sum") {
381
+ final_metric_name = stripped;
382
+ }
383
+ if let Some(stripped) = final_metric_name.strip_suffix("_count")
384
+ {
385
+ final_metric_name = stripped;
386
+ }
387
+
388
+ let mut buckets = vec![io::prometheus::client::Bucket {
389
+ cumulative_count: None,
390
+ cumulative_count_float: gr.0.meta.value,
391
+ upper_bound: Some(
392
+ le.expect(
393
+ &format!("got no LE for {}", gr.1.metric_name)
394
+ .to_string(),
395
+ ),
396
+ ),
397
+ exemplar: None,
398
+ }];
399
+
400
+ if gr.0.meta.ex.is_some() {
401
+ buckets[0].exemplar =
402
+ Some(exemplar_to_proto(gr.0.meta.ex.as_ref().unwrap()));
403
+ }
404
+ m.label = m
405
+ .label
406
+ .into_iter()
407
+ .filter(|l| l.name != Some("le".to_string()))
408
+ .collect_vec();
409
+ // Create a new metric.
410
+ m.histogram = Some(io::prometheus::client::Histogram {
411
+ // All native histogram fields.
412
+ sample_count: None,
413
+ sample_count_float: None,
414
+ sample_sum: None,
415
+ created_timestamp: None,
416
+ schema: None,
417
+ zero_count: None,
418
+ zero_count_float: None,
419
+ zero_threshold: None,
420
+ negative_count: vec![],
421
+ negative_delta: vec![],
422
+ negative_span: vec![],
423
+ positive_count: vec![],
424
+ positive_delta: vec![],
425
+ positive_span: vec![],
426
+ // All classic histogram fields.
427
+ bucket: buckets,
428
+ });
429
+ mtrcs.insert(hash_value, m);
430
+ metric_types.insert(hash_value, "histogram");
431
+ metric_names.insert(hash_value, final_metric_name);
432
+ }
433
+ }
434
+ }
435
+ "summary" => {
436
+ let mut hasher = DefaultHasher::new();
437
+
438
+ let mut quantile: Option<f64> = None;
439
+
440
+ // Iterate over the tuples and hash their elements
441
+ for (a, b) in lbls {
442
+ if a != "quantile" {
443
+ a.hash(&mut hasher);
444
+ b.hash(&mut hasher);
445
+ }
446
+ if a == "quantile" {
447
+ let quantileparsed = b.parse::<f64>();
448
+ match quantileparsed {
449
+ Ok(p) => quantile = Some(p),
450
+ Err(e) => {
451
+ panic!("failed to parse quantile {} due to {}", b, e)
452
+ }
453
+ }
454
+ }
455
+ }
456
+ "summary".hash(&mut hasher);
457
+ let hash_value = hasher.finish();
458
+
459
+ match mtrcs.get_mut(&hash_value) {
460
+ Some(v) => {
461
+ // Go through and edit buckets.
462
+ let smry = v.summary.as_mut().expect(
463
+ &format!(
464
+ "getting mutable summary for {}",
465
+ gr.1.metric_name
466
+ )
467
+ .to_string(),
468
+ );
469
+
470
+ if gr.1.metric_name.ends_with("_count") {
471
+ let samplecount = smry.sample_count.unwrap_or_default();
472
+ smry.sample_count =
473
+ Some((gr.0.meta.value.unwrap() as u64) + samplecount);
474
+ } else if gr.1.metric_name.ends_with("_sum") {
475
+ let samplesum: f64 = smry.sample_sum.unwrap_or_default();
476
+ smry.sample_sum = Some(gr.0.meta.value.unwrap() + samplesum);
477
+ } else {
478
+ let mut found_quantile = false;
479
+ for qntl in &mut smry.quantile {
480
+ if qntl.quantile != quantile {
481
+ continue;
482
+ }
483
+
484
+ let mut curq: f64 = qntl.quantile.unwrap_or_default();
485
+ curq += gr.0.meta.value.unwrap();
486
+
487
+ qntl.quantile = Some(curq);
488
+ found_quantile = true;
489
+ }
490
+
491
+ if !found_quantile {
492
+ smry.quantile.push(io::prometheus::client::Quantile {
493
+ quantile: quantile,
494
+ value: gr.0.meta.value,
495
+ });
496
+ }
497
+ }
498
+ }
499
+ None => {
500
+ m.label = m
501
+ .label
502
+ .into_iter()
503
+ .filter(|l| l.name != Some("quantile".to_string()))
504
+ .collect_vec();
505
+
506
+ let mut final_metric_name = gr.1.metric_name;
507
+ // If quantile then add to quantiles.
508
+ // if ends with _count then add it to count.
509
+ // If ends with _sum then add it to sum.
510
+ if gr.1.metric_name.ends_with("_count") {
511
+ final_metric_name =
512
+ gr.1.metric_name.strip_suffix("_count").unwrap();
513
+ m.summary = Some(io::prometheus::client::Summary {
514
+ quantile: vec![],
515
+ sample_count: Some(gr.0.meta.value.unwrap() as u64),
516
+ sample_sum: None,
517
+ created_timestamp: None,
518
+ });
519
+ } else if gr.1.metric_name.ends_with("_sum") {
520
+ final_metric_name =
521
+ gr.1.metric_name.strip_suffix("_sum").unwrap();
522
+ m.summary = Some(io::prometheus::client::Summary {
523
+ quantile: vec![],
524
+ sample_sum: Some(gr.0.meta.value.unwrap()),
525
+ sample_count: None,
526
+ created_timestamp: None,
527
+ });
528
+ } else {
529
+ let quantiles = vec![io::prometheus::client::Quantile {
530
+ quantile: quantile,
531
+ value: gr.0.meta.value,
532
+ }];
533
+ m.summary = Some(io::prometheus::client::Summary {
534
+ quantile: quantiles,
535
+ sample_count: None,
536
+ sample_sum: None,
537
+ created_timestamp: None,
538
+ });
539
+ }
540
+
541
+ mtrcs.insert(hash_value, m);
542
+ metric_types.insert(hash_value, "summary");
543
+ metric_names.insert(hash_value, final_metric_name);
544
+ }
545
+ }
546
+ }
547
+ "exemplar" => {
548
+ // Exemplars are handled later on.
549
+ }
550
+ mtype => {
551
+ panic!("unhandled metric type {}", mtype)
552
+ }
553
+ }
554
+ }
555
+ });
556
+
557
+ mtrcs.iter().for_each(|mtrc| {
558
+ let metric_name = metric_names.get(mtrc.0).expect("getting metric name");
559
+ let metric_type = metric_types.get(mtrc.0).expect("getting metric type");
560
+
561
+ let protobuf_mf = io::prometheus::client::MetricFamily {
562
+ name: Some(metric_name.to_string()),
563
+ help: Some("Multiprocess metric".to_string()),
564
+ r#type: match metric_type.to_string().as_str() {
565
+ "counter" => Some(Counter.into()),
566
+ "gauge" => Some(Gauge.into()),
567
+ "histogram" => Some(Histogram.into()),
568
+ "summary" => Some(Summary.into()),
569
+ mtype => panic!("unhandled metric type {}", mtype),
570
+ },
571
+ metric: vec![mtrc.1.clone()],
572
+ };
573
+
574
+ let encoded_mf = protobuf_mf.encode_to_vec();
575
+
576
+ buffer
577
+ .write_u32_varint(
578
+ encoded_mf
579
+ .len()
580
+ .try_into()
581
+ .expect("failed to encode metricfamily"),
582
+ )
583
+ .unwrap();
584
+ buffer
585
+ .write_all(&encoded_mf)
586
+ .expect("failed to write output");
587
+ });
588
+
589
+ // NOTE: Rust strings are bytes encoded in UTF-8. Ruby doesn't have such
590
+ // invariant. So, let's convert those bytes to a string since everything ends
591
+ // up as a string in Ruby.
592
+ unsafe { Ok(str::from_utf8_unchecked(buffer.get_ref()).to_string()) }
593
+ }
594
+
595
+
596
+
597
+ /// Convert the sorted entries into a String in Prometheus metrics format.
598
+ pub fn entries_to_string(entries: Vec<FileEntry>) -> Result<String> {
599
+ // We guesstimate that lines are ~100 bytes long, preallocate the string to
600
+ // roughly that size.
601
+ let mut out = String::new();
602
+ out.try_reserve(entries.len() * 128)
603
+ .map_err(|_| MmapError::OutOfMemory(entries.len() * 128))?;
604
+
605
+ let mut prev_name: Option<String> = None;
606
+
607
+ let entry_count = entries.len();
608
+ let mut processed_count = 0;
609
+
610
+ for entry in entries {
611
+ let metrics_data = match serde_json::from_str::<MetricText>(&entry.data.json) {
612
+ Ok(m) => {
613
+ if m.labels.len() != m.values.len() {
614
+ continue;
615
+ }
616
+ m
617
+ }
618
+ // We don't exit the function here so the total number of invalid
619
+ // entries can be calculated below.
620
+ Err(_) => continue,
621
+ };
622
+
623
+ match prev_name.as_ref() {
624
+ Some(p) if p == metrics_data.family_name => {}
625
+ _ => {
626
+ entry.append_header(metrics_data.family_name, &mut out);
627
+ prev_name = Some(metrics_data.family_name.to_owned());
628
+ }
629
+ }
630
+
631
+ entry.append_entry(metrics_data, &mut out)?;
632
+
633
+ writeln!(&mut out, " {}", entry.meta.value.unwrap())
634
+ .map_err(|e| MmapError::Other(format!("Failed to append to output: {e}")))?;
635
+
636
+ processed_count += 1;
637
+ }
638
+
639
+ if processed_count != entry_count {
640
+ return Err(MmapError::legacy(
641
+ format!("Processed entries {processed_count} != map entries {entry_count}"),
642
+ RubyError::Runtime,
643
+ ));
644
+ }
645
+
646
+ Ok(out)
647
+ }
648
+
649
+ fn append_header(&self, family_name: &str, out: &mut String) {
650
+ out.push_str("# HELP ");
651
+ out.push_str(family_name);
652
+ out.push_str(" Multiprocess metric\n");
653
+
654
+ out.push_str("# TYPE ");
655
+ out.push_str(family_name);
656
+ out.push(' ');
657
+
658
+ out.push_str(&self.meta.type_.name().expect("name was invalid UTF-8"));
659
+ out.push('\n');
660
+ }
661
+
662
+ fn append_entry(&self, json_data: MetricText, out: &mut String) -> Result<()> {
663
+ out.push_str(json_data.metric_name);
664
+
665
+ if json_data.labels.is_empty() {
666
+ if let Some(pid) = self.data.pid.as_ref() {
667
+ out.push_str("{pid=\"");
668
+ out.push_str(pid);
669
+ out.push_str("\"}");
670
+ }
671
+
672
+ return Ok(());
673
+ }
674
+
675
+ out.push('{');
676
+
677
+ let it = json_data.labels.iter().zip(json_data.values.iter());
678
+
679
+ for (i, (&key, val)) in it.enumerate() {
680
+ out.push_str(key);
681
+ out.push('=');
682
+
683
+ match val.get() {
684
+ "null" => out.push_str("\"\""),
685
+ s if s.starts_with('"') => out.push_str(s),
686
+ s => {
687
+ // Quote numeric values.
688
+ out.push('"');
689
+ out.push_str(s);
690
+ out.push('"');
691
+ }
692
+ }
693
+
694
+ if i < json_data.labels.len() - 1 {
695
+ out.push(',');
696
+ }
697
+ }
698
+
699
+ if let Some(pid) = self.data.pid.as_ref() {
700
+ out.push_str(",pid=\"");
701
+ out.push_str(pid);
702
+ out.push('"');
703
+ }
704
+
705
+ out.push('}');
706
+
707
+ Ok(())
708
+ }
709
+ }
710
+
711
+ #[cfg(test)]
712
+ mod test {
713
+ use bstr::BString;
714
+ use indoc::indoc;
715
+
716
+ use super::*;
717
+ use crate::file_info::FileInfo;
718
+ use crate::raw_entry::RawEntry;
719
+ use crate::testhelper::{TestEntry, TestFile};
720
+
721
+ #[test]
722
+ fn test_trim_quotes() {
723
+ assert_eq!("foo", FileEntry::trim_quotes("foo"));
724
+ assert_eq!("foo", FileEntry::trim_quotes("\"foo\""));
725
+ }
726
+
727
+ #[test]
728
+ fn test_entries_to_string() {
729
+ struct TestCase {
730
+ name: &'static str,
731
+ multiprocess_mode: &'static str,
732
+ json: &'static [&'static str],
733
+ values: &'static [f64],
734
+ pids: &'static [&'static str],
735
+ expected_out: Option<&'static str>,
736
+ expected_err: Option<MmapError>,
737
+ }
738
+
739
+ let _cleanup = unsafe { magnus::embed::init() };
740
+ let ruby = magnus::Ruby::get().unwrap();
741
+ crate::init(&ruby).unwrap();
742
+
743
+ let tc = vec![
744
+ TestCase {
745
+ name: "one metric, pid significant",
746
+ multiprocess_mode: "all",
747
+ json: &[r#"["family","name",["label_a","label_b"],["value_a","value_b"]]"#],
748
+ values: &[1.0],
749
+ pids: &["worker-1"],
750
+ expected_out: Some(indoc! {r##"# HELP family Multiprocess metric
751
+ # TYPE family gauge
752
+ name{label_a="value_a",label_b="value_b",pid="worker-1"} 1
753
+ "##}),
754
+ expected_err: None,
755
+ },
756
+ TestCase {
757
+ name: "one metric, no pid",
758
+ multiprocess_mode: "min",
759
+ json: &[r#"["family","name",["label_a","label_b"],["value_a","value_b"]]"#],
760
+ values: &[1.0],
761
+ pids: &["worker-1"],
762
+ expected_out: Some(indoc! {r##"# HELP family Multiprocess metric
763
+ # TYPE family gauge
764
+ name{label_a="value_a",label_b="value_b"} 1
765
+ "##}),
766
+ expected_err: None,
767
+ },
768
+ TestCase {
769
+ name: "many labels",
770
+ multiprocess_mode: "min",
771
+ json: &[
772
+ r#"["family","name",["label_a","label_b","label_c","label_d","label_e"],["value_a","value_b","value_c","value_d","value_e"]]"#,
773
+ ],
774
+ values: &[1.0],
775
+ pids: &["worker-1"],
776
+ expected_out: Some(indoc! {r##"# HELP family Multiprocess metric
777
+ # TYPE family gauge
778
+ name{label_a="value_a",label_b="value_b",label_c="value_c",label_d="value_d",label_e="value_e"} 1
779
+ "##}),
780
+ expected_err: None,
781
+ },
782
+ TestCase {
783
+ name: "floating point shown",
784
+ multiprocess_mode: "min",
785
+ json: &[r#"["family","name",["label_a","label_b"],["value_a","value_b"]]"#],
786
+ values: &[1.5],
787
+ pids: &["worker-1"],
788
+ expected_out: Some(indoc! {r##"# HELP family Multiprocess metric
789
+ # TYPE family gauge
790
+ name{label_a="value_a",label_b="value_b"} 1.5
791
+ "##}),
792
+ expected_err: None,
793
+ },
794
+ TestCase {
795
+ name: "numeric value",
796
+ multiprocess_mode: "min",
797
+ json: &[
798
+ r#"["family","name",["label_a","label_b","label_c"],["value_a",403,-0.2E5]]"#,
799
+ ],
800
+ values: &[1.5],
801
+ pids: &["worker-1"],
802
+ expected_out: Some(indoc! {r##"# HELP family Multiprocess metric
803
+ # TYPE family gauge
804
+ name{label_a="value_a",label_b="403",label_c="-0.2E5"} 1.5
805
+ "##}),
806
+ expected_err: None,
807
+ },
808
+ TestCase {
809
+ name: "null value",
810
+ multiprocess_mode: "min",
811
+ json: &[r#"["family","name",["label_a","label_b"],["value_a",null]]"#],
812
+ values: &[1.5],
813
+ pids: &["worker-1"],
814
+ expected_out: Some(indoc! {r##"# HELP family Multiprocess metric
815
+ # TYPE family gauge
816
+ name{label_a="value_a",label_b=""} 1.5
817
+ "##}),
818
+ expected_err: None,
819
+ },
820
+ TestCase {
821
+ name: "comma in value",
822
+ multiprocess_mode: "min",
823
+ json: &[r#"["family","name",["label_a","label_b"],["value_a","value,_b"]]"#],
824
+ values: &[1.5],
825
+ pids: &["worker-1"],
826
+ expected_out: Some(indoc! {r##"# HELP family Multiprocess metric
827
+ # TYPE family gauge
828
+ name{label_a="value_a",label_b="value,_b"} 1.5
829
+ "##}),
830
+ expected_err: None,
831
+ },
832
+ TestCase {
833
+ name: "no labels, pid significant",
834
+ multiprocess_mode: "all",
835
+ json: &[r#"["family","name",[],[]]"#],
836
+ values: &[1.0],
837
+ pids: &["worker-1"],
838
+ expected_out: Some(indoc! {r##"# HELP family Multiprocess metric
839
+ # TYPE family gauge
840
+ name{pid="worker-1"} 1
841
+ "##}),
842
+ expected_err: None,
843
+ },
844
+ TestCase {
845
+ name: "no labels, no pid",
846
+ multiprocess_mode: "min",
847
+ json: &[r#"["family","name",[],[]]"#],
848
+ values: &[1.0],
849
+ pids: &["worker-1"],
850
+ expected_out: Some(indoc! {r##"# HELP family Multiprocess metric
851
+ # TYPE family gauge
852
+ name 1
853
+ "##}),
854
+ expected_err: None,
855
+ },
856
+ TestCase {
857
+ name: "two metrics, same family, pid significant",
858
+ multiprocess_mode: "all",
859
+ json: &[
860
+ r#"["family","first",["label_a","label_b"],["value_a","value_b"]]"#,
861
+ r#"["family","second",["label_a","label_b"],["value_a","value_b"]]"#,
862
+ ],
863
+ values: &[1.0, 2.0],
864
+ pids: &["worker-1", "worker-1"],
865
+ expected_out: Some(indoc! {r##"# HELP family Multiprocess metric
866
+ # TYPE family gauge
867
+ first{label_a="value_a",label_b="value_b",pid="worker-1"} 1
868
+ second{label_a="value_a",label_b="value_b",pid="worker-1"} 2
869
+ "##}),
870
+ expected_err: None,
871
+ },
872
+ TestCase {
873
+ name: "two metrics, different family, pid significant",
874
+ multiprocess_mode: "min",
875
+ json: &[
876
+ r#"["first_family","first_name",["label_a","label_b"],["value_a","value_b"]]"#,
877
+ r#"["second_family","second_name",["label_a","label_b"],["value_a","value_b"]]"#,
878
+ ],
879
+ values: &[1.0, 2.0],
880
+ pids: &["worker-1", "worker-1"],
881
+ expected_out: Some(indoc! {r##"# HELP first_family Multiprocess metric
882
+ # TYPE first_family gauge
883
+ first_name{label_a="value_a",label_b="value_b"} 1
884
+ # HELP second_family Multiprocess metric
885
+ # TYPE second_family gauge
886
+ second_name{label_a="value_a",label_b="value_b"} 2
887
+ "##}),
888
+ expected_err: None,
889
+ },
890
+ TestCase {
891
+ name: "three metrics, two different families, pid significant",
892
+ multiprocess_mode: "all",
893
+ json: &[
894
+ r#"["first_family","first_name",["label_a","label_b"],["value_a","value_b"]]"#,
895
+ r#"["first_family","second_name",["label_a","label_b"],["value_a","value_b"]]"#,
896
+ r#"["second_family","second_name",["label_a","label_b"],["value_a","value_b"]]"#,
897
+ ],
898
+ values: &[1.0, 2.0, 3.0],
899
+ pids: &["worker-1", "worker-1", "worker-1"],
900
+ expected_out: Some(indoc! {r##"# HELP first_family Multiprocess metric
901
+ # TYPE first_family gauge
902
+ first_name{label_a="value_a",label_b="value_b",pid="worker-1"} 1
903
+ second_name{label_a="value_a",label_b="value_b",pid="worker-1"} 2
904
+ # HELP second_family Multiprocess metric
905
+ # TYPE second_family gauge
906
+ second_name{label_a="value_a",label_b="value_b",pid="worker-1"} 3
907
+ "##}),
908
+ expected_err: None,
909
+ },
910
+ TestCase {
911
+ name: "same metrics, pid significant, separate workers",
912
+ multiprocess_mode: "all",
913
+ json: &[
914
+ r#"["first_family","first_name",["label_a","label_b"],["value_a","value_b"]]"#,
915
+ r#"["first_family","first_name",["label_a","label_b"],["value_a","value_b"]]"#,
916
+ ],
917
+ values: &[1.0, 2.0],
918
+ pids: &["worker-1", "worker-2"],
919
+ expected_out: Some(indoc! {r##"# HELP first_family Multiprocess metric
920
+ # TYPE first_family gauge
921
+ first_name{label_a="value_a",label_b="value_b",pid="worker-1"} 1
922
+ first_name{label_a="value_a",label_b="value_b",pid="worker-2"} 2
923
+ "##}),
924
+ expected_err: None,
925
+ },
926
+ TestCase {
927
+ name: "same metrics, pid not significant, separate workers",
928
+ multiprocess_mode: "max",
929
+ json: &[
930
+ r#"["first_family","first_name",["label_a","label_b"],["value_a","value_b"]]"#,
931
+ r#"["first_family","first_name",["label_a","label_b"],["value_a","value_b"]]"#,
932
+ ],
933
+ values: &[1.0, 2.0],
934
+ pids: &["worker-1", "worker-2"],
935
+ expected_out: Some(indoc! {r##"# HELP first_family Multiprocess metric
936
+ # TYPE first_family gauge
937
+ first_name{label_a="value_a",label_b="value_b"} 1
938
+ first_name{label_a="value_a",label_b="value_b"} 2
939
+ "##}),
940
+ expected_err: None,
941
+ },
942
+ TestCase {
943
+ name: "entry fails to parse",
944
+ multiprocess_mode: "min",
945
+ json: &[
946
+ r#"["first_family","first_name",["label_a","label_b"],["value_a","value_b"]]"#,
947
+ r#"[not valid"#,
948
+ ],
949
+ values: &[1.0, 2.0],
950
+ pids: &["worker-1", "worker-1"],
951
+ expected_out: None,
952
+ expected_err: Some(MmapError::legacy(
953
+ "Processed entries 1 != map entries 2".to_owned(),
954
+ RubyError::Runtime,
955
+ )),
956
+ },
957
+ TestCase {
958
+ name: "too many values",
959
+ multiprocess_mode: "min",
960
+ json: &[r#"["family","name",["label_a"],["value_a","value,_b"]]"#],
961
+ values: &[1.5],
962
+ pids: &["worker-1"],
963
+ expected_out: None,
964
+ expected_err: Some(MmapError::legacy(
965
+ "Processed entries 0 != map entries 1".to_owned(),
966
+ RubyError::Runtime,
967
+ )),
968
+ },
969
+ TestCase {
970
+ name: "no values",
971
+ multiprocess_mode: "min",
972
+ json: &[r#"["family","name",["label_a"]]"#],
973
+ values: &[1.5],
974
+ pids: &["worker-1"],
975
+ expected_out: None,
976
+ expected_err: Some(MmapError::legacy(
977
+ "Processed entries 0 != map entries 1".to_owned(),
978
+ RubyError::Runtime,
979
+ )),
980
+ },
981
+ TestCase {
982
+ name: "no labels or values",
983
+ multiprocess_mode: "min",
984
+ json: &[r#"["family","name","foo"]"#],
985
+ values: &[1.5],
986
+ pids: &["worker-1"],
987
+ expected_out: None,
988
+ expected_err: Some(MmapError::legacy(
989
+ "Processed entries 0 != map entries 1".to_owned(),
990
+ RubyError::Runtime,
991
+ )),
992
+ },
993
+ TestCase {
994
+ name: "too many leading brackets",
995
+ multiprocess_mode: "min",
996
+ json: &[r#"[["family","name",["label_a","label_b"],["value_a","value_b"]]"#],
997
+ values: &[1.5],
998
+ pids: &["worker-1"],
999
+ expected_out: None,
1000
+ expected_err: Some(MmapError::legacy(
1001
+ "Processed entries 0 != map entries 1".to_owned(),
1002
+ RubyError::Runtime,
1003
+ )),
1004
+ },
1005
+ TestCase {
1006
+ name: "too many trailing brackets",
1007
+ multiprocess_mode: "min",
1008
+ json: &[r#"["family","name",["label_a","label_b"],["value_a","value_b"]]]"#],
1009
+ values: &[1.5],
1010
+ pids: &["worker-1"],
1011
+ expected_out: None,
1012
+ expected_err: Some(MmapError::legacy(
1013
+ "Processed entries 0 != map entries 1".to_owned(),
1014
+ RubyError::Runtime,
1015
+ )),
1016
+ },
1017
+ TestCase {
1018
+ name: "too many leading label brackets",
1019
+ multiprocess_mode: "min",
1020
+ json: &[r#"["family","name",[["label_a","label_b"],["value_a","value_b"]]"#],
1021
+ values: &[1.5],
1022
+ pids: &["worker-1"],
1023
+ expected_out: None,
1024
+ expected_err: Some(MmapError::legacy(
1025
+ "Processed entries 0 != map entries 1".to_owned(),
1026
+ RubyError::Runtime,
1027
+ )),
1028
+ },
1029
+ TestCase {
1030
+ name: "too many leading label brackets",
1031
+ multiprocess_mode: "min",
1032
+ json: &[r#"["family","name",[["label_a","label_b"],["value_a","value_b"]]"#],
1033
+ values: &[1.5],
1034
+ pids: &["worker-1"],
1035
+ expected_out: None,
1036
+ expected_err: Some(MmapError::legacy(
1037
+ "Processed entries 0 != map entries 1".to_owned(),
1038
+ RubyError::Runtime,
1039
+ )),
1040
+ },
1041
+ TestCase {
1042
+ name: "too many leading value brackets",
1043
+ multiprocess_mode: "min",
1044
+ json: &[r#"["family","name",["label_a","label_b"],[["value_a","value_b"]]"#],
1045
+ values: &[1.5],
1046
+ pids: &["worker-1"],
1047
+ expected_out: None,
1048
+ expected_err: Some(MmapError::legacy(
1049
+ "Processed entries 0 != map entries 1".to_owned(),
1050
+ RubyError::Runtime,
1051
+ )),
1052
+ },
1053
+ TestCase {
1054
+ name: "misplaced bracket",
1055
+ multiprocess_mode: "min",
1056
+ json: &[r#"["family","name",["label_a","label_b"],]["value_a","value_b"]]"#],
1057
+ values: &[1.5],
1058
+ pids: &["worker-1"],
1059
+ expected_out: None,
1060
+ expected_err: Some(MmapError::legacy(
1061
+ "Processed entries 0 != map entries 1".to_owned(),
1062
+ RubyError::Runtime,
1063
+ )),
1064
+ },
1065
+ TestCase {
1066
+ name: "comma in numeric",
1067
+ multiprocess_mode: "min",
1068
+ json: &[r#"["family","name",["label_a","label_b"],["value_a",403,0]]"#],
1069
+ values: &[1.5],
1070
+ pids: &["worker-1"],
1071
+ expected_out: None,
1072
+ expected_err: Some(MmapError::legacy(
1073
+ "Processed entries 0 != map entries 1".to_owned(),
1074
+ RubyError::Runtime,
1075
+ )),
1076
+ },
1077
+ TestCase {
1078
+ name: "non-e letter in numeric",
1079
+ multiprocess_mode: "min",
1080
+ json: &[r#"["family","name",["label_a","label_b"],["value_a",-2.0c5]]"#],
1081
+ values: &[1.5],
1082
+ pids: &["worker-1"],
1083
+ expected_out: None,
1084
+ expected_err: Some(MmapError::legacy(
1085
+ "Processed entries 0 != map entries 1".to_owned(),
1086
+ RubyError::Runtime,
1087
+ )),
1088
+ },
1089
+ ];
1090
+
1091
+ for case in tc {
1092
+ let name = case.name;
1093
+
1094
+ let input_bytes: Vec<BString> = case
1095
+ .json
1096
+ .iter()
1097
+ .zip(case.values)
1098
+ .map(|(&s, &value)| TestEntry::new(s, value).as_bstring())
1099
+ .collect();
1100
+
1101
+ let mut file_infos = Vec::new();
1102
+ for pid in case.pids {
1103
+ let TestFile {
1104
+ file,
1105
+ path,
1106
+ dir: _dir,
1107
+ } = TestFile::new(b"foobar");
1108
+
1109
+ let info = FileInfo {
1110
+ file,
1111
+ path,
1112
+ len: case.json.len(),
1113
+ multiprocess_mode: Symbol::new(case.multiprocess_mode),
1114
+ type_: Symbol::new("gauge"),
1115
+ pid: pid.to_string(),
1116
+ };
1117
+ file_infos.push(info);
1118
+ }
1119
+
1120
+ let file_entries: Vec<FileEntry> = input_bytes
1121
+ .iter()
1122
+ .map(|s| RawEntry::from_slice(s).unwrap())
1123
+ .zip(file_infos)
1124
+ .map(|(entry, info)| {
1125
+ let meta = EntryMetadata::new(&entry, &info).unwrap();
1126
+ let borrowed =
1127
+ BorrowedData::new(&entry, &info, meta.is_pid_significant()).unwrap();
1128
+ let data = EntryData::try_from(borrowed).unwrap();
1129
+ FileEntry { data, meta }
1130
+ })
1131
+ .collect();
1132
+
1133
+ let output = FileEntry::entries_to_string(file_entries);
1134
+
1135
+ if let Some(expected_out) = case.expected_out {
1136
+ assert_eq!(
1137
+ expected_out,
1138
+ output.as_ref().unwrap(),
1139
+ "test case: {name} - output"
1140
+ );
1141
+ }
1142
+
1143
+ if let Some(expected_err) = case.expected_err {
1144
+ assert_eq!(
1145
+ expected_err,
1146
+ output.unwrap_err(),
1147
+ "test case: {name} - error"
1148
+ );
1149
+ }
1150
+ }
1151
+ }
1152
+
1153
+ #[test]
1154
+ fn test_merge() {
1155
+ struct TestCase {
1156
+ name: &'static str,
1157
+ metric_type: &'static str,
1158
+ multiprocess_mode: &'static str,
1159
+ values: &'static [f64],
1160
+ expected_value: f64,
1161
+ }
1162
+
1163
+ let _cleanup = unsafe { magnus::embed::init() };
1164
+ let ruby = magnus::Ruby::get().unwrap();
1165
+ crate::init(&ruby).unwrap();
1166
+
1167
+ let tc = vec![
1168
+ TestCase {
1169
+ name: "gauge max",
1170
+ metric_type: "gauge",
1171
+ multiprocess_mode: "max",
1172
+ values: &[1.0, 5.0],
1173
+ expected_value: 5.0,
1174
+ },
1175
+ TestCase {
1176
+ name: "gauge min",
1177
+ metric_type: "gauge",
1178
+ multiprocess_mode: "min",
1179
+ values: &[1.0, 5.0],
1180
+ expected_value: 1.0,
1181
+ },
1182
+ TestCase {
1183
+ name: "gauge livesum",
1184
+ metric_type: "gauge",
1185
+ multiprocess_mode: "livesum",
1186
+ values: &[1.0, 5.0],
1187
+ expected_value: 6.0,
1188
+ },
1189
+ TestCase {
1190
+ name: "gauge all",
1191
+ metric_type: "gauge",
1192
+ multiprocess_mode: "all",
1193
+ values: &[1.0, 5.0],
1194
+ expected_value: 5.0,
1195
+ },
1196
+ TestCase {
1197
+ name: "not a gauge",
1198
+ metric_type: "histogram",
1199
+ multiprocess_mode: "max",
1200
+ values: &[1.0, 5.0],
1201
+ expected_value: 6.0,
1202
+ },
1203
+ ];
1204
+
1205
+ for case in tc {
1206
+ let name = case.name;
1207
+ let json = r#"["family","metric",["label_a","label_b"],["value_a","value_b"]]"#;
1208
+
1209
+ let TestFile {
1210
+ file,
1211
+ path,
1212
+ dir: _dir,
1213
+ } = TestFile::new(b"foobar");
1214
+
1215
+ let info = FileInfo {
1216
+ file,
1217
+ path,
1218
+ len: json.len(),
1219
+ multiprocess_mode: Symbol::new(case.multiprocess_mode),
1220
+ type_: Symbol::new(case.metric_type),
1221
+ pid: "worker-1".to_string(),
1222
+ };
1223
+
1224
+ let input_bytes: Vec<BString> = case
1225
+ .values
1226
+ .iter()
1227
+ .map(|&value| TestEntry::new(json, value).as_bstring())
1228
+ .collect();
1229
+
1230
+ let entries: Vec<FileEntry> = input_bytes
1231
+ .iter()
1232
+ .map(|s| RawEntry::from_slice(s).unwrap())
1233
+ .map(|entry| {
1234
+ let meta = EntryMetadata::new(&entry, &info).unwrap();
1235
+ let borrowed =
1236
+ BorrowedData::new(&entry, &info, meta.is_pid_significant()).unwrap();
1237
+ let data = EntryData::try_from(borrowed).unwrap();
1238
+ FileEntry { data, meta }
1239
+ })
1240
+ .collect();
1241
+
1242
+ let mut entry_a = entries[0].clone();
1243
+ let entry_b = entries[1].clone();
1244
+ entry_a.meta.merge(&entry_b.meta);
1245
+
1246
+ assert_eq!(
1247
+ case.expected_value, entry_a.meta.value.unwrap(),
1248
+ "test case: {name} - value"
1249
+ );
1250
+ }
1251
+ }
1252
+ }