1use std::fmt;
2
3use datadog_protos::metrics::{v3::Payload as V3Payload, Dogsketch, MetricPayload, MetricType, SketchPayload};
4use ddsketch::DDSketch;
5use float_cmp::ApproxEqRatio as _;
6use saluki_error::{generic_error, GenericError};
7use serde::{Deserialize, Serialize};
8
9#[derive(Deserialize)]
11struct V1SeriesEnvelope {
12 series: Vec<V1Serie>,
13}
14
15#[derive(Deserialize)]
21struct V1Serie {
22 metric: String,
23 points: Vec<(i64, f64)>,
24 #[serde(default)]
25 tags: Vec<String>,
26 #[serde(default)]
27 host: String,
28 #[serde(default)]
29 device: String,
30 #[serde(rename = "type", default)]
31 mtype: String,
32 #[serde(default)]
33 interval: i64,
34}
35
36#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
42pub struct MetricContext {
43 name: String,
44 tags: Vec<String>,
45}
46
47impl MetricContext {
48 pub fn name(&self) -> &str {
50 &self.name
51 }
52
53 pub fn tags(&self) -> &[String] {
57 &self.tags
58 }
59
60 pub fn into_parts(self) -> (String, Vec<String>) {
62 (self.name, self.tags)
63 }
64}
65
66impl fmt::Display for MetricContext {
67 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
68 write!(f, "{}", self.name)?;
69
70 if !self.tags.is_empty() {
71 write!(f, " {{{}}}", self.tags.join(", "))?;
72 }
73
74 Ok(())
75 }
76}
77
78#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
80pub struct Metric {
81 context: MetricContext,
82 values: Vec<(u64, MetricValue)>,
83}
84
85impl Metric {
86 pub fn context(&self) -> &MetricContext {
88 &self.context
89 }
90
91 pub fn values(&self) -> &[(u64, MetricValue)] {
93 &self.values
94 }
95}
96
97#[derive(Clone, Debug, Deserialize, Serialize)]
120#[serde(tag = "mtype")]
121pub enum MetricValue {
122 Count {
124 value: f64,
126 },
127
128 Rate {
133 interval: u64,
135
136 value: f64,
138 },
139
140 Gauge {
142 value: f64,
144 },
145
146 Sketch {
148 sketch: DDSketch,
150 },
151}
152
153impl PartialEq for MetricValue {
154 fn eq(&self, other: &Self) -> bool {
155 const RATIO_ERROR: f64 = 0.00000001;
157
158 match (self, other) {
159 (MetricValue::Count { value: value_a }, MetricValue::Count { value: value_b }) => {
160 value_a.approx_eq_ratio(value_b, RATIO_ERROR)
161 }
162 (
163 MetricValue::Rate {
164 interval: interval_a,
165 value: value_a,
166 },
167 MetricValue::Rate {
168 interval: interval_b,
169 value: value_b,
170 },
171 ) => interval_a == interval_b && value_a.approx_eq_ratio(value_b, RATIO_ERROR),
172 (MetricValue::Gauge { value: value_a }, MetricValue::Gauge { value: value_b }) => {
173 value_a.approx_eq_ratio(value_b, RATIO_ERROR)
174 }
175 (MetricValue::Sketch { sketch: sketch_a }, MetricValue::Sketch { sketch: sketch_b }) => {
176 approx_eq_ratio_optional(sketch_a.min(), sketch_b.min(), RATIO_ERROR)
177 && approx_eq_ratio_optional(sketch_a.max(), sketch_b.max(), RATIO_ERROR)
178 && approx_eq_ratio_optional(sketch_a.avg(), sketch_b.avg(), RATIO_ERROR)
179 && approx_eq_ratio_optional(sketch_a.sum(), sketch_b.sum(), RATIO_ERROR)
180 && sketch_a.count() == sketch_b.count()
181 && sketch_a.bin_count() == sketch_b.bin_count()
182 }
183 _ => false,
184 }
185 }
186}
187
188impl Eq for MetricValue {}
189
190impl Metric {
191 pub fn try_from_series_v1(payload: &[u8]) -> Result<Vec<Self>, GenericError> {
202 let envelope: V1SeriesEnvelope = serde_json::from_slice(payload)
203 .map_err(|e| generic_error!("Failed to parse V1 series JSON payload: {}", e))?;
204
205 let mut metrics = Vec::with_capacity(envelope.series.len());
206
207 for serie in envelope.series {
208 let mut tags = serie.tags;
209 if !serie.host.is_empty() {
210 tags.push(format!("host:{}", serie.host));
211 }
212 if !serie.device.is_empty() {
213 tags.push(format!("device:{}", serie.device));
214 }
215
216 let mut values = Vec::with_capacity(serie.points.len());
217 for (ts, value) in serie.points {
218 let timestamp =
219 u64::try_from(ts).map_err(|_| generic_error!("Invalid timestamp in V1 series payload: {}", ts))?;
220
221 let metric_value = match serie.mtype.as_str() {
222 "count" => MetricValue::Count { value },
223 "rate" => MetricValue::Rate {
224 interval: serie.interval as u64,
225 value,
226 },
227 "gauge" => MetricValue::Gauge { value },
228 other => {
229 return Err(generic_error!(
230 "Unknown metric type '{}' in V1 series payload (metric '{}')",
231 other,
232 serie.metric
233 ));
234 }
235 };
236 values.push((timestamp, metric_value));
237 }
238
239 metrics.push(Metric {
240 context: MetricContext {
241 name: serie.metric,
242 tags,
243 },
244 values,
245 });
246 }
247
248 Ok(metrics)
249 }
250
251 pub fn try_from_series_v2(payload: MetricPayload) -> Result<Vec<Self>, GenericError> {
257 let mut metrics = Vec::new();
258
259 for series in payload.series {
260 let name = series.metric().to_string();
261 let mut tags: Vec<String> = series.tags().iter().map(|tag| tag.to_string()).collect();
262 if let Some(host) = series.resources.iter().find(|r| r.type_() == "host") {
267 let host_name = host.name();
268 if !host_name.is_empty() {
269 tags.push(format!("host:{}", host_name));
270 }
271 }
272 let mut values = Vec::new();
273
274 match series.type_() {
275 MetricType::UNSPECIFIED => {
276 return Err(generic_error!("Received metric series with UNSPECIFIED type."));
277 }
278 MetricType::COUNT => {
279 for point in series.points {
280 let timestamp = u64::try_from(point.timestamp)
281 .map_err(|_| generic_error!("Invalid timestamp for point: {}", point.timestamp))?;
282 values.push((timestamp, MetricValue::Count { value: point.value }));
283 }
284 }
285 MetricType::RATE => {
286 for point in series.points {
287 let timestamp = u64::try_from(point.timestamp)
288 .map_err(|_| generic_error!("Invalid timestamp for point: {}", point.timestamp))?;
289 values.push((
290 timestamp,
291 MetricValue::Rate {
292 interval: series.interval as u64,
293 value: point.value,
294 },
295 ));
296 }
297 }
298 MetricType::GAUGE => {
299 for point in series.points {
300 let timestamp = u64::try_from(point.timestamp)
301 .map_err(|_| generic_error!("Invalid timestamp for point: {}", point.timestamp))?;
302 values.push((timestamp, MetricValue::Gauge { value: point.value }));
303 }
304 }
305 }
306
307 metrics.push(Metric {
308 context: MetricContext { name, tags },
309 values,
310 })
311 }
312
313 Ok(metrics)
314 }
315
316 pub fn try_from_sketch(payload: SketchPayload) -> Result<Vec<Self>, GenericError> {
322 let mut metrics = Vec::new();
323
324 for sketch in payload.sketches {
325 let name = sketch.metric().to_string();
326 let mut tags: Vec<String> = sketch.tags().iter().map(|tag| tag.to_string()).collect();
327 let host = sketch.host();
330 if !host.is_empty() {
331 tags.push(format!("host:{}", host));
332 }
333 let mut values = Vec::new();
334
335 for dogsketch in sketch.dogsketches {
336 let timestamp = u64::try_from(dogsketch.ts)
337 .map_err(|_| generic_error!("Invalid timestamp for sketch: {}", dogsketch.ts))?;
338 let sketch = DDSketch::try_from(dogsketch)
339 .map_err(|e| generic_error!("Failed to convert DogSketch to DDSketch: {}", e))?;
340 values.push((timestamp, MetricValue::Sketch { sketch }));
341 }
342
343 metrics.push(Metric {
344 context: MetricContext { name, tags },
345 values,
346 })
347 }
348
349 Ok(metrics)
350 }
351}
352
353const V3_METRIC_TYPE_COUNT: u64 = 1;
355const V3_METRIC_TYPE_RATE: u64 = 2;
356const V3_METRIC_TYPE_GAUGE: u64 = 3;
357const V3_METRIC_TYPE_SKETCH: u64 = 4;
358
359const V3_VALUE_TYPE_ZERO: u64 = 0x00;
361const V3_VALUE_TYPE_SINT64: u64 = 0x10;
362const V3_VALUE_TYPE_FLOAT32: u64 = 0x20;
363const V3_VALUE_TYPE_FLOAT64: u64 = 0x30;
364
365struct V3ValueCursors {
367 timestamp: usize,
368 sint64: usize,
369 float32: usize,
370 float64: usize,
371 sketch_point: usize,
372 sketch_bin_key: usize,
373 sketch_bin_cnt: usize,
374}
375
376impl V3ValueCursors {
377 fn new() -> Self {
378 Self {
379 timestamp: 0,
380 sint64: 0,
381 float32: 0,
382 float64: 0,
383 sketch_point: 0,
384 sketch_bin_key: 0,
385 sketch_bin_cnt: 0,
386 }
387 }
388}
389
390impl Metric {
391 pub fn try_from_v3(mut payload: V3Payload) -> Result<Vec<Self>, GenericError> {
399 let data = payload
400 .metricData
401 .take()
402 .ok_or_else(|| generic_error!("V3 payload missing metricData"))?;
403
404 let num_metrics = data.types.len();
405 if num_metrics == 0 {
406 return Ok(Vec::new());
407 }
408
409 let names_dict = parse_dict_strings(&data.dictNameStr)?;
411 let tags_dict = parse_dict_strings(&data.dictTagStr)?;
412 let tagsets_dict = parse_tagsets(&data.dictTagsets, &tags_dict)?;
413 let resources_dict = parse_resources(
414 &data.dictResourceLen,
415 &data.dictResourceType,
416 &data.dictResourceName,
417 &parse_dict_strings(&data.dictResourceStr)?,
418 )?;
419
420 let mut name_refs = data.nameRefs;
422 let mut tagset_refs = data.tagsetRefs;
423 let mut resources_refs = data.resourcesRefs;
424 let mut timestamps = data.timestamps;
425 delta_decode(&mut name_refs);
426 delta_decode(&mut tagset_refs);
427 delta_decode(&mut resources_refs);
428 delta_decode(&mut timestamps);
429
430 let mut sketch_bin_keys = data.sketchBinKeys;
433
434 let mut cursors = V3ValueCursors::new();
435 let mut metrics = Vec::with_capacity(num_metrics);
436
437 for i in 0..num_metrics {
438 let type_field = data
439 .types
440 .get(i)
441 .copied()
442 .ok_or_else(|| generic_error!("Ran out of metric types"))?;
443 let metric_type = type_field & 0x0F;
444 let value_type = type_field & 0xF0;
445 let num_points = data
446 .numPoints
447 .get(i)
448 .copied()
449 .ok_or_else(|| generic_error!("Ran out of numPoints"))
450 .and_then(|num_points| u64_to_usize(num_points, "numPoints"))?;
451
452 let name_ref = name_refs
454 .get(i)
455 .copied()
456 .ok_or_else(|| generic_error!("Ran out of nameRefs"))
457 .and_then(|name_ref| i64_to_usize(name_ref, "name ref"))?;
458 let name = if name_ref == 0 {
459 String::new()
460 } else {
461 names_dict
462 .get(name_ref - 1)
463 .ok_or_else(|| generic_error!("Invalid name ref {} (dict size {})", name_ref, names_dict.len()))?
464 .clone()
465 };
466
467 let tagset_ref = tagset_refs
469 .get(i)
470 .copied()
471 .ok_or_else(|| generic_error!("Ran out of tagsetRefs"))
472 .and_then(|tagset_ref| i64_to_usize(tagset_ref, "tagset ref"))?;
473 let mut tags = if tagset_ref == 0 {
474 Vec::new()
475 } else {
476 tagsets_dict
477 .get(tagset_ref - 1)
478 .ok_or_else(|| {
479 generic_error!("Invalid tagset ref {} (dict size {})", tagset_ref, tagsets_dict.len())
480 })?
481 .clone()
482 };
483
484 let resource_ref = resources_refs
485 .get(i)
486 .copied()
487 .map(|resource_ref| i64_to_usize(resource_ref, "resource ref"))
488 .transpose()?
489 .unwrap_or(0);
490 if resource_ref != 0 {
491 let resources = resources_dict.get(resource_ref - 1).ok_or_else(|| {
492 generic_error!(
493 "Invalid resource ref {} (dict size {})",
494 resource_ref,
495 resources_dict.len()
496 )
497 })?;
498 if let Some((_, host_name)) = resources
499 .iter()
500 .find(|(resource_type, resource_name)| resource_type == "host" && !resource_name.is_empty())
501 {
502 tags.push(format!("host:{}", host_name));
503 }
504 }
505
506 let mut values = Vec::with_capacity(num_points);
507
508 if metric_type == V3_METRIC_TYPE_SKETCH {
509 for _ in 0..num_points {
510 let ts = *timestamps
512 .get(cursors.timestamp)
513 .ok_or_else(|| generic_error!("Ran out of timestamps"))?;
514 let timestamp = u64::try_from(ts).map_err(|_| generic_error!("Invalid timestamp: {}", ts))?;
515 cursors.timestamp += 1;
516
517 let sum = read_value(
520 value_type,
521 &mut cursors,
522 &data.valsSint64,
523 &data.valsFloat32,
524 &data.valsFloat64,
525 )?;
526 let min = read_value(
527 value_type,
528 &mut cursors,
529 &data.valsSint64,
530 &data.valsFloat32,
531 &data.valsFloat64,
532 )?;
533 let max = read_value(
534 value_type,
535 &mut cursors,
536 &data.valsSint64,
537 &data.valsFloat32,
538 &data.valsFloat64,
539 )?;
540 let cnt = *data
541 .valsSint64
542 .get(cursors.sint64)
543 .ok_or_else(|| generic_error!("Ran out of sint64 values for sketch count"))?;
544 cursors.sint64 += 1;
545 let avg = if cnt != 0 { sum / cnt as f64 } else { 0.0 };
546
547 let num_bins = *data
549 .sketchNumBins
550 .get(cursors.sketch_point)
551 .ok_or_else(|| generic_error!("Ran out of sketchNumBins"))?
552 as usize;
553 cursors.sketch_point += 1;
554
555 let bin_key_start = cursors.sketch_bin_key;
556 let bin_key_end = bin_key_start + num_bins;
557 if bin_key_end > sketch_bin_keys.len() {
558 return Err(generic_error!("Ran out of sketch bin keys"));
559 }
560
561 delta_decode_i32(&mut sketch_bin_keys[bin_key_start..bin_key_end]);
563
564 let k: Vec<i32> = sketch_bin_keys[bin_key_start..bin_key_end].to_vec();
565 cursors.sketch_bin_key = bin_key_end;
566
567 let bin_cnt_start = cursors.sketch_bin_cnt;
568 let bin_cnt_end = bin_cnt_start + num_bins;
569 if bin_cnt_end > data.sketchBinCnts.len() {
570 return Err(generic_error!("Ran out of sketch bin counts"));
571 }
572 let n: Vec<u32> = data.sketchBinCnts[bin_cnt_start..bin_cnt_end].to_vec();
573 cursors.sketch_bin_cnt = bin_cnt_end;
574
575 let mut dogsketch = Dogsketch::new();
577 dogsketch.ts = ts;
578 dogsketch.cnt = cnt;
579 dogsketch.min = min;
580 dogsketch.max = max;
581 dogsketch.avg = avg;
582 dogsketch.sum = sum;
583 dogsketch.set_k(k);
584 dogsketch.set_n(n);
585
586 let sketch = DDSketch::try_from(dogsketch)
587 .map_err(|e| generic_error!("Failed to convert v3 sketch to DDSketch: {}", e))?;
588 values.push((timestamp, MetricValue::Sketch { sketch }));
589 }
590 } else {
591 for _ in 0..num_points {
592 let ts = *timestamps
594 .get(cursors.timestamp)
595 .ok_or_else(|| generic_error!("Ran out of timestamps"))?;
596 let timestamp = u64::try_from(ts).map_err(|_| generic_error!("Invalid timestamp: {}", ts))?;
597 cursors.timestamp += 1;
598
599 let value = read_value(
601 value_type,
602 &mut cursors,
603 &data.valsSint64,
604 &data.valsFloat32,
605 &data.valsFloat64,
606 )?;
607
608 let metric_value = match metric_type {
609 V3_METRIC_TYPE_COUNT => MetricValue::Count { value },
610 V3_METRIC_TYPE_RATE => MetricValue::Rate {
611 interval: data
612 .intervals
613 .get(i)
614 .copied()
615 .ok_or_else(|| generic_error!("Ran out of intervals"))?,
616 value,
617 },
618 V3_METRIC_TYPE_GAUGE => MetricValue::Gauge { value },
619 other => return Err(generic_error!("Unknown v3 metric type: {}", other)),
620 };
621
622 values.push((timestamp, metric_value));
623 }
624 }
625
626 metrics.push(Metric {
627 context: MetricContext { name, tags },
628 values,
629 });
630 }
631
632 Ok(metrics)
633 }
634}
635
636fn delta_decode(s: &mut [i64]) {
638 for i in 1..s.len() {
639 s[i] += s[i - 1];
640 }
641}
642
643fn delta_decode_i32(s: &mut [i32]) {
645 for i in 1..s.len() {
646 s[i] += s[i - 1];
647 }
648}
649
650fn read_varint(data: &[u8]) -> Result<(u64, usize), GenericError> {
652 let mut value: u64 = 0;
653 let mut shift = 0;
654 for (i, &byte) in data.iter().enumerate() {
655 value |= ((byte & 0x7F) as u64) << shift;
656 if byte & 0x80 == 0 {
657 return Ok((value, i + 1));
658 }
659 shift += 7;
660 if shift >= 64 {
661 return Err(generic_error!("Varint too large"));
662 }
663 }
664 Err(generic_error!("Unexpected end of data reading varint"))
665}
666
667fn parse_dict_strings(data: &[u8]) -> Result<Vec<String>, GenericError> {
669 let mut strings = Vec::new();
670 let mut offset = 0;
671 while offset < data.len() {
672 let (len, varint_size) = read_varint(&data[offset..])?;
673 offset += varint_size;
674 let len = len as usize;
675 if offset + len > data.len() {
676 return Err(generic_error!("Dictionary string extends past end of buffer"));
677 }
678 let s = simdutf8::basic::from_utf8(&data[offset..offset + len])
679 .map_err(|e| generic_error!("Invalid UTF-8 in dictionary string: {}", e))?;
680 strings.push(s.to_string());
681 offset += len;
682 }
683 Ok(strings)
684}
685
686fn parse_tagsets(dict_tagsets: &[i64], tags_dict: &[String]) -> Result<Vec<Vec<String>>, GenericError> {
699 let mut tagsets: Vec<Vec<String>> = Vec::new();
700 let mut offset = 0;
701 while offset < dict_tagsets.len() {
702 let count = i64_to_usize(dict_tagsets[offset], "tagset length")?;
703 offset += 1;
704 if offset + count > dict_tagsets.len() {
705 return Err(generic_error!("Tagset extends past end of dictTagsets array"));
706 }
707
708 let mut entries: Vec<i64> = dict_tagsets[offset..offset + count].to_vec();
710 delta_decode(&mut entries);
711
712 let mut tags = Vec::with_capacity(count);
713 for &entry in &entries {
714 match entry.cmp(&0) {
715 std::cmp::Ordering::Less => {
716 let prefix_id = i64_to_usize(-entry, "tagset prefix reference")?;
718 let prefix_idx = prefix_id
719 .checked_sub(1)
720 .ok_or_else(|| generic_error!("Invalid zero tagset prefix reference"))?;
721 let prefix_tags = tagsets.get(prefix_idx).ok_or_else(|| {
722 generic_error!(
723 "Invalid tagset prefix reference {} (only {} tagsets parsed so far)",
724 prefix_id,
725 tagsets.len()
726 )
727 })?;
728 tags.extend(prefix_tags.iter().cloned());
729 }
730 std::cmp::Ordering::Equal => continue,
731 std::cmp::Ordering::Greater => {
732 let idx = i64_to_usize(entry, "tag index")?;
733 let tag = tags_dict
734 .get(idx - 1)
735 .ok_or_else(|| generic_error!("Invalid tag index {} (dict size {})", idx, tags_dict.len()))?;
736 tags.push(tag.clone());
737 }
738 }
739 }
740 tagsets.push(tags);
741 offset += count;
742 }
743 Ok(tagsets)
744}
745
746fn u64_to_usize(value: u64, field: &str) -> Result<usize, GenericError> {
747 usize::try_from(value).map_err(|_| generic_error!("Invalid {}: {}", field, value))
748}
749
750fn i64_to_usize(value: i64, field: &str) -> Result<usize, GenericError> {
751 usize::try_from(value).map_err(|_| generic_error!("Invalid negative {}: {}", field, value))
752}
753
754fn parse_resources(
758 dict_resource_len: &[i64], dict_resource_type: &[i64], dict_resource_name: &[i64], resource_strings: &[String],
759) -> Result<Vec<Vec<(String, String)>>, GenericError> {
760 let mut resources = Vec::with_capacity(dict_resource_len.len());
761 let mut offset = 0;
762
763 for &count in dict_resource_len {
764 let count = usize::try_from(count).map_err(|_| generic_error!("Invalid negative resource count: {}", count))?;
765 if offset + count > dict_resource_type.len() || offset + count > dict_resource_name.len() {
766 return Err(generic_error!("Resource set extends past resource dictionary arrays"));
767 }
768
769 let mut type_indices = dict_resource_type[offset..offset + count].to_vec();
770 let mut name_indices = dict_resource_name[offset..offset + count].to_vec();
771 delta_decode(&mut type_indices);
772 delta_decode(&mut name_indices);
773
774 let mut resource_set = Vec::with_capacity(count);
775 for (&type_idx, &name_idx) in type_indices.iter().zip(name_indices.iter()) {
776 let resource_type = resource_strings
777 .get(resource_index(type_idx)?)
778 .ok_or_else(|| generic_error!("Invalid resource type index {}", type_idx))?
779 .clone();
780 let resource_name = resource_strings
781 .get(resource_index(name_idx)?)
782 .ok_or_else(|| generic_error!("Invalid resource name index {}", name_idx))?
783 .clone();
784 resource_set.push((resource_type, resource_name));
785 }
786
787 resources.push(resource_set);
788 offset += count;
789 }
790
791 Ok(resources)
792}
793
794fn resource_index(idx: i64) -> Result<usize, GenericError> {
795 let idx = usize::try_from(idx).map_err(|_| generic_error!("Invalid negative resource index: {}", idx))?;
796 idx.checked_sub(1)
797 .ok_or_else(|| generic_error!("Invalid zero resource index"))
798}
799
800fn read_value(
802 value_type: u64, cursors: &mut V3ValueCursors, vals_sint64: &[i64], vals_float32: &[f32], vals_float64: &[f64],
803) -> Result<f64, GenericError> {
804 match value_type {
805 V3_VALUE_TYPE_ZERO => Ok(0.0),
806 V3_VALUE_TYPE_SINT64 => {
807 let v = *vals_sint64
808 .get(cursors.sint64)
809 .ok_or_else(|| generic_error!("Ran out of sint64 values"))?;
810 cursors.sint64 += 1;
811 Ok(v as f64)
812 }
813 V3_VALUE_TYPE_FLOAT32 => {
814 let v = *vals_float32
815 .get(cursors.float32)
816 .ok_or_else(|| generic_error!("Ran out of float32 values"))?;
817 cursors.float32 += 1;
818 Ok(v as f64)
819 }
820 V3_VALUE_TYPE_FLOAT64 => {
821 let v = *vals_float64
822 .get(cursors.float64)
823 .ok_or_else(|| generic_error!("Ran out of float64 values"))?;
824 cursors.float64 += 1;
825 Ok(v)
826 }
827 _ => Err(generic_error!("Unknown v3 value type: {:#x}", value_type)),
828 }
829}
830
831fn approx_eq_ratio_optional(a: Option<f64>, b: Option<f64>, ratio: f64) -> bool {
832 match (a, b) {
833 (Some(a), Some(b)) => a.approx_eq_ratio(&b, ratio),
834 (None, None) => true,
835 _ => false,
836 }
837}
838
839#[cfg(test)]
840mod tests {
841 use super::*;
842
843 #[test]
844 fn try_from_series_v1_parses_count_gauge_rate() {
845 let body = br#"{"series":[
846 {"metric":"a.count","points":[[100,5.0]],"tags":["env:prod"],"host":"h","type":"count","interval":0},
847 {"metric":"a.gauge","points":[[101,12.0]],"tags":[],"host":"h","type":"gauge","interval":0},
848 {"metric":"a.rate","points":[[102,3.0]],"tags":[],"host":"h","type":"rate","interval":10}
849 ]}"#;
850
851 let metrics = Metric::try_from_series_v1(body).expect("parse should succeed");
852 assert_eq!(metrics.len(), 3);
853
854 assert_eq!(metrics[0].context.name, "a.count");
855 assert!(metrics[0].context.tags.contains(&"env:prod".to_string()));
856 assert!(metrics[0].context.tags.contains(&"host:h".to_string()));
857 assert_eq!(metrics[0].values, vec![(100, MetricValue::Count { value: 5.0 })]);
858
859 assert_eq!(metrics[1].context.name, "a.gauge");
860 assert_eq!(metrics[1].context.tags, vec!["host:h".to_string()]);
861 assert_eq!(metrics[1].values, vec![(101, MetricValue::Gauge { value: 12.0 })]);
862
863 assert_eq!(metrics[2].context.name, "a.rate");
864 assert_eq!(metrics[2].context.tags, vec!["host:h".to_string()]);
865 assert_eq!(
866 metrics[2].values,
867 vec![(
868 102,
869 MetricValue::Rate {
870 interval: 10,
871 value: 3.0
872 }
873 )]
874 );
875 }
876
877 #[test]
878 fn try_from_series_v1_omits_host_tag_when_empty() {
879 let body = br#"{"series":[
882 {"metric":"m","points":[[1,1.0]],"tags":[],"type":"count","interval":0}
883 ]}"#;
884
885 let metrics = Metric::try_from_series_v1(body).expect("parse should succeed");
886 assert!(!metrics[0].context.tags.iter().any(|t| t.starts_with("host:")));
887 }
888
889 #[test]
890 fn try_from_series_v1_reinjects_device_tag() {
891 let body = br#"{"series":[
892 {"metric":"m","points":[[1,1.0]],"tags":["env:prod"],"host":"h","device":"eth0","type":"count","interval":0}
893 ]}"#;
894
895 let metrics = Metric::try_from_series_v1(body).expect("parse should succeed");
896 assert!(metrics[0].context.tags.contains(&"env:prod".to_string()));
897 assert!(metrics[0].context.tags.contains(&"device:eth0".to_string()));
898 }
899
900 #[test]
901 fn try_from_series_v1_rejects_unknown_type() {
902 let body = br#"{"series":[
903 {"metric":"m","points":[[1,1.0]],"tags":[],"host":"h","type":"weird","interval":0}
904 ]}"#;
905
906 assert!(Metric::try_from_series_v1(body).is_err());
907 }
908
909 #[test]
910 fn try_from_series_v2_folds_host_into_tags() {
911 use datadog_protos::metrics::metric_payload::{
912 MetricPoint, MetricSeries, MetricType as ProtoMetricType, Resource,
913 };
914 use datadog_protos::metrics::MetricPayload;
915
916 let mut payload = MetricPayload::new();
917
918 let mut series = MetricSeries::new();
919 series.set_metric("my.metric".into());
920 series.set_type(ProtoMetricType::COUNT);
921 series.tags.push("env:prod".into());
922
923 let mut host_res = Resource::new();
924 host_res.set_type("host".into());
925 host_res.set_name("server-1".into());
926 series.resources.push(host_res);
927
928 let mut device_res = Resource::new();
930 device_res.set_type("device".into());
931 device_res.set_name("eth0".into());
932 series.resources.push(device_res);
933
934 let mut point = MetricPoint::new();
935 point.value = 1.0;
936 point.timestamp = 1;
937 series.points.push(point);
938
939 payload.series.push(series);
940
941 let metrics = Metric::try_from_series_v2(payload).expect("parse should succeed");
942 assert_eq!(metrics.len(), 1);
943 assert!(metrics[0].context.tags.contains(&"host:server-1".to_string()));
944 assert!(metrics[0].context.tags.contains(&"env:prod".to_string()));
945 assert!(!metrics[0].context.tags.iter().any(|t| t.starts_with("device:")));
946 }
947
948 #[test]
949 fn try_from_sketch_folds_host_into_tags() {
950 use datadog_protos::metrics::sketch_payload::Sketch;
951 use datadog_protos::metrics::SketchPayload;
952
953 let mut payload = SketchPayload::new();
954 let mut sketch = Sketch::new();
955 sketch.set_metric("my.metric".into());
956 sketch.set_host("server-1".into());
957 sketch.tags.push("env:prod".into());
958 payload.sketches.push(sketch);
959
960 let metrics = Metric::try_from_sketch(payload).expect("parse should succeed");
961 assert_eq!(metrics.len(), 1);
962 assert!(metrics[0].context.tags.contains(&"host:server-1".to_string()));
963 assert!(metrics[0].context.tags.contains(&"env:prod".to_string()));
964 }
965
966 #[test]
967 fn try_from_v3_folds_host_resource_into_tags() {
968 use datadog_protos::metrics::v3::{MetricData, Payload};
969
970 let mut data = MetricData::new();
971 data.dictNameStr = length_prefixed_strings(["my.metric"]);
972 data.dictTagStr = length_prefixed_strings(["env:prod"]);
973 data.dictTagsets = vec![1, 1];
974 data.dictResourceStr = length_prefixed_strings(["host", "server-1", "device", "eth0"]);
975 data.dictResourceLen = vec![2];
976 data.dictResourceType = vec![1, 2];
977 data.dictResourceName = vec![2, 2];
978 data.types = vec![V3_METRIC_TYPE_COUNT | V3_VALUE_TYPE_ZERO];
979 data.nameRefs = vec![1];
980 data.tagsetRefs = vec![1];
981 data.resourcesRefs = vec![1];
982 data.intervals = vec![0];
983 data.numPoints = vec![1];
984 data.timestamps = vec![1];
985
986 let mut payload = Payload::new();
987 payload.metricData = Some(data).into();
988
989 let metrics = Metric::try_from_v3(payload).expect("parse should succeed");
990 assert_eq!(metrics.len(), 1);
991 assert!(metrics[0].context.tags.contains(&"env:prod".to_string()));
992 assert!(metrics[0].context.tags.contains(&"host:server-1".to_string()));
993 assert!(!metrics[0].context.tags.iter().any(|tag| tag.starts_with("device:")));
994 }
995
996 #[test]
997 fn try_from_v3_decodes_integer_sketch_summary_order() {
998 use datadog_protos::metrics::v3::{MetricData, Payload};
999
1000 let mut data = MetricData::new();
1001 data.dictNameStr = length_prefixed_strings(["my.sketch"]);
1002 data.types = vec![V3_METRIC_TYPE_SKETCH | V3_VALUE_TYPE_SINT64];
1003 data.nameRefs = vec![1];
1004 data.tagsetRefs = vec![0];
1005 data.resourcesRefs = vec![0];
1006 data.intervals = vec![0];
1007 data.numPoints = vec![1];
1008 data.timestamps = vec![123];
1009 data.valsSint64 = vec![10, 1, 4, 4];
1011 data.sketchNumBins = vec![1];
1012 data.sketchBinKeys = vec![0];
1013 data.sketchBinCnts = vec![4];
1014
1015 let mut payload = Payload::new();
1016 payload.metricData = Some(data).into();
1017
1018 let metrics = Metric::try_from_v3(payload).expect("parse should succeed");
1019 assert_eq!(metrics.len(), 1);
1020
1021 let MetricValue::Sketch { sketch } = &metrics[0].values[0].1 else {
1022 panic!("expected sketch value");
1023 };
1024 assert_eq!(sketch.count(), 4);
1025 assert_eq!(sketch.sum(), Some(10.0));
1026 assert_eq!(sketch.min(), Some(1.0));
1027 assert_eq!(sketch.max(), Some(4.0));
1028 assert_eq!(sketch.avg(), Some(2.5));
1029 }
1030
1031 fn length_prefixed_strings(strings: impl IntoIterator<Item = &'static str>) -> Vec<u8> {
1032 let mut bytes = Vec::new();
1033 for s in strings {
1034 bytes.push(s.len() as u8);
1035 bytes.extend_from_slice(s.as_bytes());
1036 }
1037 bytes
1038 }
1039
1040 #[test]
1041 fn parse_tagsets_resolves_positive_indices() {
1042 let tags_dict = vec!["a:1".to_string(), "b:2".to_string(), "c:3".to_string()];
1045 let dict_tagsets = vec![1, 1, 2, 1, 1 ];
1046 let tagsets = parse_tagsets(&dict_tagsets, &tags_dict).expect("parse should succeed");
1047 assert_eq!(
1048 tagsets,
1049 vec![vec!["a:1".to_string()], vec!["a:1".to_string(), "b:2".to_string()]]
1050 );
1051 }
1052
1053 #[test]
1054 fn parse_tagsets_resolves_negative_prefix_reference() {
1055 let tags_dict = vec!["a:1".to_string(), "b:2".to_string()];
1059 let dict_tagsets = vec![
1060 1, 1, 2, -1, 3, ];
1063 let tagsets = parse_tagsets(&dict_tagsets, &tags_dict).expect("parse should succeed");
1064 assert_eq!(tagsets.len(), 2);
1065 assert_eq!(tagsets[0], vec!["a:1".to_string()]);
1066 assert_eq!(tagsets[1], vec!["a:1".to_string(), "b:2".to_string()]);
1068 }
1069
1070 #[test]
1071 fn parse_tagsets_rejects_forward_prefix_reference() {
1072 let tags_dict = vec!["a:1".to_string()];
1074 let dict_tagsets = vec![1, -5];
1075 assert!(parse_tagsets(&dict_tagsets, &tags_dict).is_err());
1076 }
1077}