1use hashbrown::{HashMap, HashSet};
9use libdd_trace_obfuscation::ip_address::quantize_peer_ip_addresses;
10use libdd_trace_protobuf::pb;
11use std::{
12 borrow::{Borrow, Cow},
13 hash::{DefaultHasher, Hash, Hasher as _},
14};
15use tracing::warn;
16
17use crate::span_concentrator::{cardinality_limit_telemetry::CollapsedFieldSet, StatSpan};
18
19use super::{
20 cardinality_limit_telemetry::{self, CollapsedFieldsMetrics},
21 CardinalityLimitConfig,
22};
23
24pub const TRACER_BLOCKED_VALUE: &str = "tracer_blocked_value";
26
27const TAG_STATUS_CODE: &str = "http.status_code";
28const TAG_METHOD: &str = "http.method";
29const TAG_STATUS_CODE_OTEL: &str = "http.response.status_code";
36const TAG_METHOD_OTEL: &str = "http.request.method";
37const ADDITIONAL_METRIC_TAG_VALUE_MAX_LEN: usize = 200;
38const TAG_SYNTHETICS: &str = "synthetics";
39const TAG_SPANKIND: &str = "span.kind";
40const TAG_ORIGIN: &str = "_dd.origin";
41const TAG_SVC_SRC: &str = "_dd.svc_src";
42const GRPC_STATUS_CODE_FIELD: &[&str] = &[
43 "rpc.grpc.status_code",
44 "grpc.code",
45 "rpc.grpc.status.code",
46 "grpc.status.code",
47];
48
49#[derive(
57 Clone, Default, Hash, Eq, PartialEq, Debug, PartialOrd, serde::Serialize, serde::Deserialize,
58)]
59pub struct FixedAggregationKey<T> {
60 pub resource_name: T,
61 pub service_name: T,
62 pub operation_name: T,
63 pub span_type: T,
64 pub span_kind: T,
65 pub http_method: T,
66 pub http_endpoint: T,
67 pub service_source: T,
68 pub http_status_code: u32,
69 pub grpc_status_code: Option<u8>,
70 pub is_synthetics_request: bool,
71 pub is_trace_root: pb::Trilean,
72}
73
74impl<T> FixedAggregationKey<T> {
75 pub fn convert<'a, V: 'a, I: ?Sized + 'a, F: Fn(&'a I) -> V>(
77 &'a self,
78 f: F,
79 ) -> FixedAggregationKey<V>
80 where
81 T: Borrow<I>,
82 {
83 FixedAggregationKey {
84 resource_name: f(self.resource_name.borrow()),
85 service_name: f(self.service_name.borrow()),
86 operation_name: f(self.operation_name.borrow()),
87 span_type: f(self.span_type.borrow()),
88 span_kind: f(self.span_kind.borrow()),
89 http_method: f(self.http_method.borrow()),
90 http_endpoint: f(self.http_endpoint.borrow()),
91 service_source: f(self.service_source.borrow()),
92 http_status_code: self.http_status_code,
93 grpc_status_code: self.grpc_status_code,
94 is_synthetics_request: self.is_synthetics_request,
95 is_trace_root: self.is_trace_root,
96 }
97 }
98}
99
100#[derive(Clone, Hash, PartialEq, Eq)]
101pub(super) struct BorrowedAggregationKey<'a> {
103 fixed: FixedAggregationKey<&'a str>,
104 peer_tags: Vec<(&'a str, Cow<'a, str>)>,
105 additional_metric_tags: Vec<(&'a str, &'a str)>,
106}
107
108impl hashbrown::Equivalent<OwnedAggregationKey> for BorrowedAggregationKey<'_> {
109 #[inline]
110 fn equivalent(&self, other: &OwnedAggregationKey) -> bool {
111 self.fixed == other.fixed.convert(|s| s)
112 && self.peer_tags.len() == other.peer_tags.len()
113 && self
114 .peer_tags
115 .iter()
116 .zip(other.peer_tags.iter())
117 .all(|((k1, v1), (k2, v2))| k1 == k2 && v1 == v2)
118 && self.additional_metric_tags.len() == other.additional_metric_tags.len()
119 && self
120 .additional_metric_tags
121 .iter()
122 .zip(other.additional_metric_tags.iter())
123 .all(|((k1, v1), (k2, v2))| k1 == k2 && v1 == v2)
124 }
125}
126
127#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Default)]
128pub(super) struct OwnedAggregationKey {
136 fixed: FixedAggregationKey<String>,
137 peer_tags: Vec<(String, String)>,
138 additional_metric_tags: Vec<(String, String)>,
139}
140
141impl From<&BorrowedAggregationKey<'_>> for OwnedAggregationKey {
142 fn from(value: &BorrowedAggregationKey<'_>) -> Self {
143 OwnedAggregationKey {
144 fixed: value.fixed.convert(str::to_owned),
145 peer_tags: value
146 .peer_tags
147 .iter()
148 .map(|(k, v)| (k.to_string(), v.to_string()))
149 .collect(),
150 additional_metric_tags: value
151 .additional_metric_tags
152 .iter()
153 .map(|(k, v)| (k.to_string(), v.to_string()))
154 .collect(),
155 }
156 }
157}
158
159fn float_to_int(f: f64) -> Option<u8> {
160 if f.floor() != f {
161 return None;
162 }
163 if f < 0.0 || (u8::MAX as f64) < f {
164 return None;
165 }
166 Some(f as u8)
167}
168
169fn float_to_u32(f: f64) -> Option<u32> {
170 if !f.is_finite() || f.floor() != f || f < 0.0 || (u32::MAX as f64) < f {
171 return None;
172 }
173 Some(f as u32)
175}
176
177fn is_valid_http_status_code(code: &u32) -> bool {
178 (100..600).contains(code)
179}
180
181fn get_http_method<'a>(span: &'a impl StatSpan<'a>) -> &'a str {
186 for key in [TAG_METHOD, TAG_METHOD_OTEL] {
187 if let Some(value) = span.get_meta(key) {
188 if !value.is_empty() {
189 return value;
190 }
191 }
192 }
193
194 ""
195}
196
197fn get_http_status_code<'a>(span: &'a impl StatSpan<'a>) -> u32 {
203 for key in [TAG_STATUS_CODE, TAG_STATUS_CODE_OTEL] {
204 if let Some(value) = span.get_metrics(key) {
205 if key == TAG_STATUS_CODE {
206 return value as u32;
208 }
209 if let Some(code) = float_to_u32(value).filter(is_valid_http_status_code) {
210 return code;
211 }
212 }
213
214 if let Some(value) = span.get_meta(key) {
215 if key == TAG_STATUS_CODE {
216 return value.parse().unwrap_or_default();
218 }
219 if let Some(code) = value.parse().ok().filter(is_valid_http_status_code) {
220 return code;
221 }
222 }
223 }
224
225 0
226}
227
228fn get_grpc_status_code<'a>(span: &'a impl StatSpan<'a>) -> Option<u8> {
229 for key in GRPC_STATUS_CODE_FIELD {
230 if let Some(val) = span.get_meta(key) {
231 if let Some(code) = grpc_status_str_to_int_value(val) {
232 return Some(code);
233 }
234 }
235 }
236
237 for key in GRPC_STATUS_CODE_FIELD {
238 if let Some(val) = span.get_metrics(key) {
239 if let Some(code) = float_to_int(val) {
240 return Some(code);
241 }
242 }
243 }
244
245 None
246}
247
248fn grpc_status_str_to_int_value(v: &str) -> Option<u8> {
249 if let Ok(status) = v.parse() {
250 return Some(status);
251 }
252 let mut status_uppercase = [0u8; 32];
253 let mut status = v.trim_start_matches("StatusCode.");
254
255 let mut needs_upcasing = false;
256 for b in status.as_bytes() {
257 if !b.is_ascii() {
258 return None;
259 }
260 needs_upcasing |= b.is_ascii_lowercase()
261 }
262 if needs_upcasing {
263 for (c, d) in status.as_bytes().iter().zip(&mut status_uppercase) {
264 *d = c.to_ascii_uppercase();
265 }
266 status = std::str::from_utf8(&status_uppercase[0..status.len().min(status_uppercase.len())])
267 .ok()?
268 }
269
270 match status {
271 "OK" => return Some(0),
272 "CANCELLED" | "CANCELED" => return Some(1),
273 "UNKNOWN" => return Some(2),
274 "INVALID_ARGUMENT" | "INVALIDARGUMENT" => return Some(3),
275 "DEADLINE_EXCEEDED" | "DEADLINEEXCEEDED" => return Some(4),
276 "NOT_FOUND" | "NOTFOUND" => return Some(5),
277 "ALREADY_EXISTS" | "ALREADYEXISTS" => return Some(6),
278 "PERMISSION_DENIED" | "PERMISSIONDENIED" => return Some(7),
279 "UNAUTHENTICATED" => return Some(16),
280 "RESOURCE_EXHAUSTED" | "RESOURCEEXHAUSTED" => return Some(8),
281 "FAILED_PRECONDITION" | "FAILEDPRECONDITION" => return Some(9),
282 "ABORTED" => return Some(10),
283 "OUT_OF_RANGE" | "OUTOFRANGE" => return Some(11),
284 "UNIMPLEMENTED" => return Some(12),
285 "INTERNAL" => return Some(13),
286 "UNAVAILABLE" => return Some(14),
287 "DATA_LOSS" | "DATALOSS" => return Some(15),
288 _ => {}
289 }
290 None
291}
292
293impl<'a> BorrowedAggregationKey<'a> {
294 pub(super) fn from_span<T: StatSpan<'a>>(
301 span: &'a T,
302 peer_tag_keys: &'a [String],
303 additional_metric_tag_keys: &'a [String],
304 ) -> Self {
305 Self::from_obfuscated_span(
306 span.resource(),
307 span,
308 peer_tag_keys,
309 additional_metric_tag_keys,
310 )
311 }
312
313 pub(crate) fn from_obfuscated_span<'b, T>(
314 resource_name: &'a str,
315 span: &'b T,
316 peer_tag_keys: &'b [String],
317 additional_metric_tag_keys: &'b [String],
318 ) -> BorrowedAggregationKey<'a>
319 where
320 T: StatSpan<'b>,
321 'b: 'a,
323 {
324 let span_kind = span.get_meta(TAG_SPANKIND).unwrap_or_default();
325 let peer_tags = if should_track_peer_tags(span_kind) {
326 peer_tag_keys
329 .iter()
330 .filter_map(|key| {
331 let value = span.get_meta(key.as_str())?;
332 Some((key.as_str(), quantize_peer_ip_addresses(value)))
333 })
334 .collect()
335 } else if let Some(base_service) = span.get_meta("_dd.base_service") {
336 vec![("_dd.base_service", Cow::Borrowed(base_service))]
338 } else {
339 vec![]
340 };
341
342 let http_method = get_http_method(span);
346
347 let http_endpoint = span
348 .get_meta("http.route")
349 .filter(|value| !value.is_empty())
350 .or_else(|| {
351 span.get_meta("http.endpoint")
352 .filter(|value| !value.is_empty())
353 })
354 .unwrap_or_default();
355
356 let status_code = get_http_status_code(span);
357
358 let grpc_status_code = get_grpc_status_code(span);
359 let service_source = span.get_meta(TAG_SVC_SRC).unwrap_or_default();
360
361 let additional_metric_tags: Vec<(&'a str, &'a str)> = additional_metric_tag_keys
362 .iter()
363 .filter_map(|key| match span.get_meta(key.as_str()) {
364 Some(v) if !v.is_empty() => {
365 if v.len() > ADDITIONAL_METRIC_TAG_VALUE_MAX_LEN
368 && v.chars().nth(ADDITIONAL_METRIC_TAG_VALUE_MAX_LEN).is_some()
369 {
370 warn!(
371 "additional_metric_tags: value for key '{}' exceeds {} characters; substituting tracer_blocked_value",
372 key, ADDITIONAL_METRIC_TAG_VALUE_MAX_LEN,
373 );
374 Some((key.as_str(), TRACER_BLOCKED_VALUE))
375 } else {
376 Some((key.as_str(), v))
377 }
378 }
379 _ => None,
380 })
381 .collect();
382
383 Self {
384 fixed: FixedAggregationKey {
385 resource_name,
386 service_name: span.service(),
387 operation_name: span.name(),
388 span_type: span.r#type(),
389 span_kind,
390 http_method,
391 http_endpoint,
392 service_source,
393 http_status_code: status_code,
394 grpc_status_code,
395 is_synthetics_request: span
396 .get_meta(TAG_ORIGIN)
397 .is_some_and(|origin| origin.starts_with(TAG_SYNTHETICS)),
398 is_trace_root: if span.is_trace_root() {
399 pb::Trilean::True
400 } else {
401 pb::Trilean::False
402 },
403 },
404 peer_tags,
405 additional_metric_tags,
406 }
407 }
408
409 #[cfg_attr(not(feature = "stats-obfuscation"), allow(unused))]
413 pub(crate) fn truncate(&mut self, big_resource: bool) {
414 let resource_length_limit = if big_resource { 15_000 } else { 5000 };
415 self.fixed.resource_name = slice_up_to(self.fixed.resource_name, resource_length_limit);
416 self.fixed.service_name = slice_up_to(self.fixed.service_name, 100);
417 self.fixed.operation_name = slice_up_to(self.fixed.operation_name, 100);
418 self.fixed.span_type = slice_up_to(self.fixed.span_type, 100);
419 }
420}
421
422fn slice_up_to(s: &str, max_len: usize) -> &str {
424 if max_len >= s.len() {
425 return s;
426 }
427 let mut idx = max_len;
430 while !s.is_char_boundary(idx) {
431 idx -= 1;
432 }
433 &s[..idx]
434}
435
436impl OwnedAggregationKey {
437 pub(super) fn overflow_key() -> Self {
439 OwnedAggregationKey {
440 fixed: FixedAggregationKey {
441 resource_name: TRACER_BLOCKED_VALUE.to_owned(),
442 service_name: TRACER_BLOCKED_VALUE.to_owned(),
443 operation_name: TRACER_BLOCKED_VALUE.to_owned(),
444 span_type: TRACER_BLOCKED_VALUE.to_owned(),
445 span_kind: TRACER_BLOCKED_VALUE.to_owned(),
446 http_method: TRACER_BLOCKED_VALUE.to_owned(),
447 http_endpoint: TRACER_BLOCKED_VALUE.to_owned(),
448 service_source: TRACER_BLOCKED_VALUE.to_owned(),
449 http_status_code: 0,
450 grpc_status_code: None,
451 is_synthetics_request: false,
452 is_trace_root: pb::Trilean::NotSet,
453 },
454 peer_tags: vec![(TRACER_BLOCKED_VALUE.to_owned(), "".to_owned())],
455 additional_metric_tags: vec![(TRACER_BLOCKED_VALUE.to_owned(), "".to_owned())],
456 }
457 }
458}
459
460impl From<pb::ClientGroupedStats> for OwnedAggregationKey {
461 fn from(value: pb::ClientGroupedStats) -> Self {
462 Self {
463 fixed: FixedAggregationKey {
464 resource_name: value.resource,
465 service_name: value.service,
466 operation_name: value.name,
467 span_type: value.r#type,
468 span_kind: value.span_kind,
469 http_method: value.http_method,
470 http_endpoint: value.http_endpoint,
471 service_source: value.service_source,
472 http_status_code: value.http_status_code,
473 grpc_status_code: value.grpc_status_code.parse().ok(),
474 is_synthetics_request: value.synthetics,
475 is_trace_root: pb::Trilean::try_from(value.is_trace_root)
476 .unwrap_or(pb::Trilean::NotSet),
477 },
478 peer_tags: value
479 .peer_tags
480 .into_iter()
481 .filter_map(|t| {
482 let (key, value) = t.split_once(':')?;
483 Some((key.to_string(), value.to_string()))
484 })
485 .collect(),
486 additional_metric_tags: value
487 .additional_metric_tags
488 .into_iter()
489 .filter_map(|t| {
490 let (key, value) = t.split_once(':')?;
491 Some((key.to_string(), value.to_string()))
492 })
493 .collect(),
494 }
495 }
496}
497
498fn should_track_peer_tags(span_kind: &str) -> bool {
500 matches!(
501 span_kind.to_lowercase().as_str(),
502 "client" | "producer" | "consumer"
503 )
504}
505
506#[derive(Debug, Default, Clone)]
508pub(super) struct GroupedStats {
509 hits: u64,
510 errors: u64,
511 duration: u64,
512 top_level_hits: u64,
513 ok_summary: libdd_ddsketch::DDSketch,
514 error_summary: libdd_ddsketch::DDSketch,
515 ok_duration: u64,
518 ok_min: u64,
519 ok_max: u64,
520 error_duration: u64,
521 error_min: u64,
522 error_max: u64,
523}
524
525impl GroupedStats {
526 fn insert(&mut self, duration: i64, is_error: bool, is_top_level: bool) {
528 self.hits += 1;
529 self.duration += duration as u64;
530 let d = duration as u64;
531 if is_error {
532 self.errors += 1;
533 let _ = self.error_summary.add(duration as f64);
534 self.error_duration += d;
535 self.error_min = if self.errors == 1 {
536 d
537 } else {
538 self.error_min.min(d)
539 };
540 self.error_max = self.error_max.max(d);
541 } else {
542 let _ = self.ok_summary.add(duration as f64);
543 self.ok_duration += d;
544 let ok_count = self.hits - self.errors;
545 self.ok_min = if ok_count == 1 { d } else { self.ok_min.min(d) };
546 self.ok_max = self.ok_max.max(d);
547 }
548 if is_top_level {
549 self.top_level_hits += 1;
550 }
551 }
552}
553
554#[derive(Debug, Clone, Copy, Default)]
558pub struct OtlpExactCell {
559 pub count: u64,
560 pub duration_ns: u64,
561 pub min_ns: u64,
562 pub max_ns: u64,
563}
564
565#[derive(Debug, Clone, Default)]
568pub struct OtlpExactGroup {
569 pub ok: OtlpExactCell,
570 pub error: OtlpExactCell,
571}
572
573#[derive(Debug, Clone)]
576pub struct OtlpStatsBucket {
577 pub bucket: pb::ClientStatsBucket,
578 pub exact: Vec<OtlpExactGroup>,
579}
580
581#[derive(Debug, Clone)]
584pub(super) struct StatsBucket {
585 data: HashMap<OwnedAggregationKey, GroupedStats>,
586 start: u64,
587 cardinality_limits: CardinalityLimitConfig,
590 distinct_resources: HashSet<u64>,
595 distinct_http_endpoints: HashSet<u64>,
596 distinct_peer_tags: HashSet<u64>,
597 distinct_additional_tags: HashSet<u64>,
598 collapsed_count: u64,
600 collapsed_fields_metrics: CollapsedFieldsMetrics,
601 #[cfg(feature = "stats-obfuscation")]
604 pub(super) obfuscated: bool,
605}
606
607impl StatsBucket {
608 pub(super) fn new(
612 start_timestamp: u64,
613 cardinality_limits: CardinalityLimitConfig,
614 #[cfg(feature = "stats-obfuscation")] obfuscation_enabled: bool,
615 ) -> Self {
616 Self {
617 data: HashMap::new(),
618 start: start_timestamp,
619 cardinality_limits,
620 collapsed_count: 0,
621 #[cfg(feature = "stats-obfuscation")]
622 obfuscated: obfuscation_enabled,
623 distinct_resources: HashSet::new(),
624 distinct_http_endpoints: HashSet::new(),
625 distinct_peer_tags: HashSet::new(),
626 distinct_additional_tags: HashSet::new(),
627 collapsed_fields_metrics: cardinality_limit_telemetry::CollapsedFieldsMetrics::zero(),
628 }
629 }
630
631 pub fn collapsed_fields_metrics(&self) -> cardinality_limit_telemetry::CollapsedFieldsMetrics {
633 self.collapsed_fields_metrics
634 }
635
636 pub(super) fn collapsed_count(&self) -> u64 {
638 self.collapsed_count
639 }
640
641 pub(super) fn insert(
647 &mut self,
648 mut key: BorrowedAggregationKey<'_>,
649 duration: i64,
650 is_error: bool,
651 is_top_level: bool,
652 ) {
653 self.collapse_key_fields_cardinality(&mut key);
655
656 let len_before_insert = self.data.len();
659
660 match self.data.entry_ref(&key) {
661 hashbrown::hash_map::EntryRef::Occupied(mut e) => {
663 e.get_mut().insert(duration, is_error, is_top_level);
664 }
665 hashbrown::hash_map::EntryRef::Vacant(e) => {
666 if len_before_insert >= self.cardinality_limits.whole_key_limit {
669 self.collapsed_count += 1;
670 self.data
671 .entry(OwnedAggregationKey::overflow_key())
672 .or_default()
673 .insert(duration, is_error, is_top_level);
674 return;
675 }
676 e.insert(GroupedStats::default())
678 .insert(duration, is_error, is_top_level);
679 }
680 }
681 }
682
683 fn collapse_key_fields_cardinality(&mut self, key: &mut BorrowedAggregationKey<'_>) {
685 use hashbrown::hash_set::Entry;
686 fn hash(input: &impl Hash) -> u64 {
687 let mut hasher = DefaultHasher::new();
688 input.hash(&mut hasher);
689 hasher.finish()
690 }
691
692 let mut collapsed_fields = CollapsedFieldSet::empty();
693
694 let resource_hash = hash(&key.fixed.resource_name);
695 let resources_count = self.distinct_resources.len();
696 if let Entry::Vacant(slot) = self.distinct_resources.entry(resource_hash) {
697 if resources_count >= self.cardinality_limits.resource_limit {
698 key.fixed.resource_name = TRACER_BLOCKED_VALUE;
699 collapsed_fields.add(CollapsedFieldSet::RESOURCE_NAME);
700 } else {
701 slot.insert();
702 }
703 }
704
705 let http_endpoint_hash = hash(&key.fixed.http_endpoint);
706 let http_endpoints_count = self.distinct_http_endpoints.len();
707 if let Entry::Vacant(slot) = self.distinct_http_endpoints.entry(http_endpoint_hash) {
708 if http_endpoints_count >= self.cardinality_limits.http_endpoint_limit {
709 key.fixed.http_endpoint = TRACER_BLOCKED_VALUE;
710 collapsed_fields.add(CollapsedFieldSet::HTTP_ENDPOINT);
711 } else {
712 slot.insert();
713 }
714 }
715
716 let peer_tags_hash = hash(&key.peer_tags);
717 let peer_tags_count = self.distinct_peer_tags.len();
718 if let Entry::Vacant(slot) = self.distinct_peer_tags.entry(peer_tags_hash) {
719 if peer_tags_count >= self.cardinality_limits.peer_tags_limit {
720 key.peer_tags = vec![(TRACER_BLOCKED_VALUE, Cow::Borrowed(""))];
721 collapsed_fields.add(CollapsedFieldSet::PEER_TAGS);
722 } else {
723 slot.insert();
724 }
725 }
726
727 let additional_tags_hash = hash(&key.additional_metric_tags);
728 let additional_tags_count = self.distinct_additional_tags.len();
729 if let Entry::Vacant(slot) = self.distinct_additional_tags.entry(additional_tags_hash) {
730 if additional_tags_count >= self.cardinality_limits.additional_tags_limit {
731 key.additional_metric_tags = vec![(TRACER_BLOCKED_VALUE, "")];
732 collapsed_fields.add(CollapsedFieldSet::ADDITIONAL_TAGS);
733 } else {
734 slot.insert();
735 }
736 }
737 self.collapsed_fields_metrics.increment(collapsed_fields);
738 }
739
740 pub(super) fn flush(self, bucket_duration: u64) -> pb::ClientStatsBucket {
743 self.flush_with_otlp_exact(bucket_duration).bucket
744 }
745
746 pub(super) fn flush_with_otlp_exact(self, bucket_duration: u64) -> OtlpStatsBucket {
749 let mut stats = Vec::with_capacity(self.data.len());
750 let mut exact = Vec::with_capacity(self.data.len());
751 for (k, g) in self.data {
752 exact.push(OtlpExactGroup {
753 ok: OtlpExactCell {
754 count: g.hits.saturating_sub(g.errors),
755 duration_ns: g.ok_duration,
756 min_ns: g.ok_min,
757 max_ns: g.ok_max,
758 },
759 error: OtlpExactCell {
760 count: g.errors,
761 duration_ns: g.error_duration,
762 min_ns: g.error_min,
763 max_ns: g.error_max,
764 },
765 });
766 stats.push(encode_grouped_stats(k, g));
767 }
768 OtlpStatsBucket {
769 bucket: pb::ClientStatsBucket {
770 start: self.start,
771 duration: bucket_duration,
772 stats,
773 agent_time_shift: 0,
774 },
775 exact,
776 }
777 }
778}
779
780fn encode_grouped_stats(key: OwnedAggregationKey, group: GroupedStats) -> pb::ClientGroupedStats {
782 let f = key.fixed;
783 pb::ClientGroupedStats {
784 service: f.service_name,
785 name: f.operation_name,
786 resource: f.resource_name,
787 http_status_code: f.http_status_code,
788 r#type: f.span_type,
789 db_type: String::new(), hits: group.hits,
792 errors: group.errors,
793 duration: group.duration,
794
795 ok_summary: group.ok_summary.encode_to_vec(),
796 error_summary: group.error_summary.encode_to_vec(),
797 synthetics: f.is_synthetics_request,
798 top_level_hits: group.top_level_hits,
799 span_kind: f.span_kind,
800
801 peer_tags: key
802 .peer_tags
803 .into_iter()
804 .map(|(k, v)| {
805 if v.is_empty() {
806 k.to_string()
807 } else {
808 format!("{k}:{v}")
809 }
810 })
811 .collect(),
812 is_trace_root: f.is_trace_root.into(),
813 http_method: f.http_method,
814 http_endpoint: f.http_endpoint,
815 grpc_status_code: f
816 .grpc_status_code
817 .map(|c| c.to_string())
818 .unwrap_or_default(),
819 service_source: f.service_source,
820 span_derived_primary_tags: vec![],
821 additional_metric_tags: key
822 .additional_metric_tags
823 .into_iter()
824 .map(|(k, v)| format!("{k}:{v}"))
825 .collect(),
826 }
827}
828
829#[cfg(test)]
830mod tests {
831 use libdd_trace_utils::span::v04::{SpanBytes, SpanSlice};
832
833 use super::*;
834 use libdd_trace_protobuf::pb;
835 use std::hash::Hash;
836
837 fn get_hash(v: &impl Hash) -> u64 {
838 use std::hash::Hasher;
839 let mut hasher = std::hash::DefaultHasher::new();
840 v.hash(&mut hasher);
841 hasher.finish()
842 }
843
844 impl FixedAggregationKey<String> {
845 fn into_key(self) -> OwnedAggregationKey {
846 OwnedAggregationKey {
847 fixed: self,
848 peer_tags: vec![],
849 additional_metric_tags: vec![],
850 }
851 }
852 fn into_key_with_peers(self, peer_tags: Vec<(String, String)>) -> OwnedAggregationKey {
853 OwnedAggregationKey {
854 fixed: self,
855 peer_tags,
856 additional_metric_tags: vec![],
857 }
858 }
859 }
860
861 #[test]
862 fn test_float_to_u32_rejects_malformed_values() {
863 assert_eq!(float_to_u32(0.0), Some(0));
864 assert_eq!(float_to_u32(500.0), Some(500));
865 assert_eq!(float_to_u32(u32::MAX as f64), Some(u32::MAX));
866 assert_eq!(float_to_u32(f64::NAN), None);
867 assert_eq!(float_to_u32(f64::INFINITY), None);
868 assert_eq!(float_to_u32(-1.0), None);
869 assert_eq!(float_to_u32(1.5), None);
870 assert_eq!(float_to_u32((u32::MAX as f64) + 1.0), None);
871 }
872
873 #[test]
874 fn test_aggregation_key_from_span() {
875 let test_cases: Vec<(SpanBytes, OwnedAggregationKey)> = vec![
876 (
878 SpanBytes {
879 service: "service".into(),
880 name: "op".into(),
881 resource: "res".into(),
882 span_id: 1,
883 parent_id: 0,
884 ..Default::default()
885 },
886 FixedAggregationKey {
887 service_name: "service".into(),
888 operation_name: "op".into(),
889 resource_name: "res".into(),
890 is_trace_root: pb::Trilean::True,
891 ..Default::default()
892 }
893 .into_key(),
894 ),
895 (
897 SpanBytes {
898 service: "service".into(),
899 name: "op".into(),
900 resource: "res".into(),
901 span_id: 1,
902 parent_id: 0,
903 meta: vec![("http.response.status_code".into(), "418".into())].into(),
904 metrics: vec![("http.status_code".into(), f64::NAN)].into(),
905 ..Default::default()
906 },
907 FixedAggregationKey {
908 service_name: "service".into(),
909 operation_name: "op".into(),
910 resource_name: "res".into(),
911 is_synthetics_request: false,
912 is_trace_root: pb::Trilean::True,
913 http_status_code: 0,
914 ..Default::default()
915 }
916 .into_key(),
917 ),
918 (
920 SpanBytes {
921 service: "service".into(),
922 name: "op".into(),
923 resource: "res".into(),
924 span_id: 1,
925 parent_id: 0,
926 meta: vec![("http.response.status_code".into(), "418".into())].into(),
927 metrics: vec![("http.status_code".into(), 600.0)].into(),
928 ..Default::default()
929 },
930 FixedAggregationKey {
931 service_name: "service".into(),
932 operation_name: "op".into(),
933 resource_name: "res".into(),
934 is_synthetics_request: false,
935 is_trace_root: pb::Trilean::True,
936 http_status_code: 600,
937 ..Default::default()
938 }
939 .into_key(),
940 ),
941 (
943 SpanBytes {
944 service: "service".into(),
945 name: "op".into(),
946 resource: "res".into(),
947 span_id: 1,
948 parent_id: 0,
949 meta: vec![("http.response.status_code".into(), "418".into())].into(),
950 metrics: vec![("http.status_code".into(), 1.5)].into(),
951 ..Default::default()
952 },
953 FixedAggregationKey {
954 service_name: "service".into(),
955 operation_name: "op".into(),
956 resource_name: "res".into(),
957 is_synthetics_request: false,
958 is_trace_root: pb::Trilean::True,
959 http_status_code: 1,
960 ..Default::default()
961 }
962 .into_key(),
963 ),
964 (
966 SpanBytes {
967 service: "service".into(),
968 name: "op".into(),
969 resource: "res".into(),
970 span_id: 1,
971 parent_id: 0,
972 meta: vec![("http.status_code".into(), "99".into())].into(),
973 metrics: vec![("http.response.status_code".into(), 418.0)].into(),
974 ..Default::default()
975 },
976 FixedAggregationKey {
977 service_name: "service".into(),
978 operation_name: "op".into(),
979 resource_name: "res".into(),
980 is_synthetics_request: false,
981 is_trace_root: pb::Trilean::True,
982 http_status_code: 99,
983 ..Default::default()
984 }
985 .into_key(),
986 ),
987 (
989 SpanBytes {
990 service: "service".into(),
991 name: "op".into(),
992 resource: "res".into(),
993 span_id: 1,
994 parent_id: 0,
995 metrics: vec![("http.response.status_code".into(), 600.0)].into(),
996 ..Default::default()
997 },
998 FixedAggregationKey {
999 service_name: "service".into(),
1000 operation_name: "op".into(),
1001 resource_name: "res".into(),
1002 is_synthetics_request: false,
1003 is_trace_root: pb::Trilean::True,
1004 http_status_code: 0,
1005 ..Default::default()
1006 }
1007 .into_key(),
1008 ),
1009 (
1011 SpanBytes {
1012 service: "service".into(),
1013 name: "op".into(),
1014 resource: "res".into(),
1015 span_id: 1,
1016 parent_id: 0,
1017 meta: vec![("span.kind".into(), "client".into())].into(),
1018 ..Default::default()
1019 },
1020 FixedAggregationKey {
1021 service_name: "service".into(),
1022 operation_name: "op".into(),
1023 resource_name: "res".into(),
1024 span_kind: "client".into(),
1025 is_trace_root: pb::Trilean::True,
1026 ..Default::default()
1027 }
1028 .into_key(),
1029 ),
1030 (
1032 SpanBytes {
1033 service: "service".into(),
1034 name: "op".into(),
1035 resource: "res".into(),
1036 span_id: 1,
1037 parent_id: 0,
1038 meta: vec![
1039 ("span.kind".into(), "client".into()),
1040 ("aws.s3.bucket".into(), "bucket-a".into()),
1041 ]
1042 .into(),
1043 ..Default::default()
1044 },
1045 FixedAggregationKey {
1046 service_name: "service".into(),
1047 operation_name: "op".into(),
1048 resource_name: "res".into(),
1049 span_kind: "client".into(),
1050 is_trace_root: pb::Trilean::True,
1051 ..Default::default()
1052 }
1053 .into_key(),
1054 ),
1055 (
1057 SpanBytes {
1058 service: "service".into(),
1059 name: "op".into(),
1060 resource: "res".into(),
1061 span_id: 1,
1062 parent_id: 0,
1063 meta: vec![
1064 ("span.kind".into(), "producer".into()),
1065 ("aws.s3.bucket".into(), "bucket-a".into()),
1066 ("db.instance".into(), "dynamo.test.us1".into()),
1067 ("db.system".into(), "dynamodb".into()),
1068 ]
1069 .into(),
1070 ..Default::default()
1071 },
1072 FixedAggregationKey {
1073 service_name: "service".into(),
1074 operation_name: "op".into(),
1075 resource_name: "res".into(),
1076 span_kind: "producer".into(),
1077 is_trace_root: pb::Trilean::True,
1078 ..Default::default()
1079 }
1080 .into_key(),
1081 ),
1082 (
1085 SpanBytes {
1086 service: "service".into(),
1087 name: "op".into(),
1088 resource: "res".into(),
1089 span_id: 1,
1090 parent_id: 0,
1091 meta: vec![
1092 ("span.kind".into(), "server".into()),
1093 ("aws.s3.bucket".into(), "bucket-a".into()),
1094 ("db.instance".into(), "dynamo.test.us1".into()),
1095 ("db.system".into(), "dynamodb".into()),
1096 ]
1097 .into(),
1098 ..Default::default()
1099 },
1100 FixedAggregationKey {
1101 service_name: "service".into(),
1102 operation_name: "op".into(),
1103 resource_name: "res".into(),
1104 span_kind: "server".into(),
1105 is_trace_root: pb::Trilean::True,
1106 ..Default::default()
1107 }
1108 .into_key(),
1109 ),
1110 (
1112 SpanBytes {
1113 service: "service".into(),
1114 name: "op".into(),
1115 resource: "res".into(),
1116 span_id: 1,
1117 parent_id: 0,
1118 meta: vec![("_dd.origin".into(), "synthetics-browser".into())].into(),
1119 ..Default::default()
1120 },
1121 FixedAggregationKey {
1122 service_name: "service".into(),
1123 operation_name: "op".into(),
1124 resource_name: "res".into(),
1125 is_synthetics_request: true,
1126 is_trace_root: pb::Trilean::True,
1127 ..Default::default()
1128 }
1129 .into_key(),
1130 ),
1131 (
1133 SpanBytes {
1134 service: "service".into(),
1135 name: "op".into(),
1136 resource: "res".into(),
1137 span_id: 1,
1138 parent_id: 0,
1139 meta: vec![("http.status_code".into(), "418".into())].into(),
1140 ..Default::default()
1141 },
1142 FixedAggregationKey {
1143 service_name: "service".into(),
1144 operation_name: "op".into(),
1145 resource_name: "res".into(),
1146 is_synthetics_request: false,
1147 is_trace_root: pb::Trilean::True,
1148 http_status_code: 418,
1149 ..Default::default()
1150 }
1151 .into_key(),
1152 ),
1153 (
1155 SpanBytes {
1156 service: "service".into(),
1157 name: "op".into(),
1158 resource: "res".into(),
1159 span_id: 1,
1160 parent_id: 0,
1161 meta: vec![("http.status_code".into(), "x".into())].into(),
1162 ..Default::default()
1163 },
1164 FixedAggregationKey {
1165 service_name: "service".into(),
1166 operation_name: "op".into(),
1167 resource_name: "res".into(),
1168 is_synthetics_request: false,
1169 is_trace_root: pb::Trilean::True,
1170 ..Default::default()
1171 }
1172 .into_key(),
1173 ),
1174 (
1176 SpanBytes {
1177 service: "service".into(),
1178 name: "op".into(),
1179 resource: "res".into(),
1180 span_id: 1,
1181 parent_id: 0,
1182 metrics: vec![("http.status_code".into(), 418.0)].into(),
1183 ..Default::default()
1184 },
1185 FixedAggregationKey {
1186 service_name: "service".into(),
1187 operation_name: "op".into(),
1188 resource_name: "res".into(),
1189 is_synthetics_request: false,
1190 is_trace_root: pb::Trilean::True,
1191 http_status_code: 418,
1192 ..Default::default()
1193 }
1194 .into_key(),
1195 ),
1196 (
1199 SpanBytes {
1200 service: "service".into(),
1201 name: "op".into(),
1202 resource: "res".into(),
1203 span_id: 1,
1204 parent_id: 0,
1205 metrics: vec![("http.response.status_code".into(), 418.0)].into(),
1206 ..Default::default()
1207 },
1208 FixedAggregationKey {
1209 service_name: "service".into(),
1210 operation_name: "op".into(),
1211 resource_name: "res".into(),
1212 is_synthetics_request: false,
1213 is_trace_root: pb::Trilean::True,
1214 http_status_code: 418,
1215 ..Default::default()
1216 }
1217 .into_key(),
1218 ),
1219 (
1222 SpanBytes {
1223 service: "service".into(),
1224 name: "op".into(),
1225 resource: "res".into(),
1226 span_id: 1,
1227 parent_id: 0,
1228 meta: vec![("http.response.status_code".into(), "418".into())].into(),
1229 ..Default::default()
1230 },
1231 FixedAggregationKey {
1232 service_name: "service".into(),
1233 operation_name: "op".into(),
1234 resource_name: "res".into(),
1235 is_synthetics_request: false,
1236 is_trace_root: pb::Trilean::True,
1237 http_status_code: 418,
1238 ..Default::default()
1239 }
1240 .into_key(),
1241 ),
1242 (
1245 SpanBytes {
1246 service: "service".into(),
1247 name: "op".into(),
1248 resource: "res".into(),
1249 span_id: 1,
1250 parent_id: 0,
1251 meta: vec![("http.status_code".into(), "500".into())].into(),
1252 metrics: vec![("http.response.status_code".into(), 418.0)].into(),
1253 ..Default::default()
1254 },
1255 FixedAggregationKey {
1256 service_name: "service".into(),
1257 operation_name: "op".into(),
1258 resource_name: "res".into(),
1259 is_synthetics_request: false,
1260 is_trace_root: pb::Trilean::True,
1261 http_status_code: 500,
1262 ..Default::default()
1263 }
1264 .into_key(),
1265 ),
1266 (
1268 SpanBytes {
1269 service: "service".into(),
1270 name: "op".into(),
1271 resource: "res".into(),
1272 span_id: 1,
1273 parent_id: 0,
1274 meta: vec![
1275 ("http.status_code".into(), "x".into()),
1276 ("http.response.status_code".into(), "418".into()),
1277 ]
1278 .into(),
1279 ..Default::default()
1280 },
1281 FixedAggregationKey {
1282 service_name: "service".into(),
1283 operation_name: "op".into(),
1284 resource_name: "res".into(),
1285 is_synthetics_request: false,
1286 is_trace_root: pb::Trilean::True,
1287 http_status_code: 0,
1288 ..Default::default()
1289 }
1290 .into_key(),
1291 ),
1292 (
1294 SpanBytes {
1295 service: "service".into(),
1296 name: "op".into(),
1297 resource: "res".into(),
1298 span_id: 1,
1299 parent_id: 0,
1300 meta: vec![("http.status_code".into(), "500".into())].into(),
1301 metrics: vec![("http.status_code".into(), f64::NAN)].into(),
1302 ..Default::default()
1303 },
1304 FixedAggregationKey {
1305 service_name: "service".into(),
1306 operation_name: "op".into(),
1307 resource_name: "res".into(),
1308 is_synthetics_request: false,
1309 is_trace_root: pb::Trilean::True,
1310 http_status_code: 0,
1311 ..Default::default()
1312 }
1313 .into_key(),
1314 ),
1315 (
1317 SpanBytes {
1318 service: "service".into(),
1319 name: "op".into(),
1320 resource: "res".into(),
1321 span_id: 1,
1322 parent_id: 0,
1323 meta: vec![
1324 ("http.method".into(), "".into()),
1325 ("http.request.method".into(), "GET".into()),
1326 ]
1327 .into(),
1328 ..Default::default()
1329 },
1330 FixedAggregationKey {
1331 service_name: "service".into(),
1332 operation_name: "op".into(),
1333 resource_name: "res".into(),
1334 is_synthetics_request: false,
1335 is_trace_root: pb::Trilean::True,
1336 http_method: "GET".into(),
1337 ..Default::default()
1338 }
1339 .into_key(),
1340 ),
1341 (
1343 SpanBytes {
1344 service: "service".into(),
1345 name: "op".into(),
1346 resource: "res".into(),
1347 span_id: 1,
1348 parent_id: 0,
1349 meta: vec![
1350 ("http.status_code".into(), "500".into()),
1351 ("http.response.status_code".into(), "418".into()),
1352 ("http.method".into(), "GET".into()),
1353 ("http.request.method".into(), "POST".into()),
1354 ]
1355 .into(),
1356 ..Default::default()
1357 },
1358 FixedAggregationKey {
1359 service_name: "service".into(),
1360 operation_name: "op".into(),
1361 resource_name: "res".into(),
1362 is_synthetics_request: false,
1363 is_trace_root: pb::Trilean::True,
1364 http_status_code: 500,
1365 http_method: "GET".into(),
1366 ..Default::default()
1367 }
1368 .into_key(),
1369 ),
1370 (
1373 SpanBytes {
1374 service: "service".into(),
1375 name: "op".into(),
1376 resource: "GET /api/v1/users".into(),
1377 span_id: 1,
1378 parent_id: 0,
1379 meta: vec![
1380 ("http.request.method".into(), "GET".into()),
1381 ("http.route".into(), "/api/v1/users".into()),
1382 ]
1383 .into(),
1384 ..Default::default()
1385 },
1386 FixedAggregationKey {
1387 service_name: "service".into(),
1388 operation_name: "op".into(),
1389 resource_name: "GET /api/v1/users".into(),
1390 is_synthetics_request: false,
1391 is_trace_root: pb::Trilean::True,
1392 http_method: "GET".into(),
1393 http_endpoint: "/api/v1/users".into(),
1394 ..Default::default()
1395 }
1396 .into_key(),
1397 ),
1398 (
1400 SpanBytes {
1401 service: "service".into(),
1402 name: "op".into(),
1403 resource: "GET /api/v1/users".into(),
1404 span_id: 1,
1405 parent_id: 0,
1406 meta: vec![
1407 ("http.method".into(), "GET".into()),
1408 ("http.route".into(), "/api/v1/users".into()),
1409 ]
1410 .into(),
1411 ..Default::default()
1412 },
1413 FixedAggregationKey {
1414 service_name: "service".into(),
1415 operation_name: "op".into(),
1416 resource_name: "GET /api/v1/users".into(),
1417 http_method: "GET".into(),
1418 http_endpoint: "/api/v1/users".into(),
1419 is_synthetics_request: false,
1420 is_trace_root: pb::Trilean::True,
1421 ..Default::default()
1422 }
1423 .into_key(),
1424 ),
1425 (
1427 SpanBytes {
1428 service: "service".into(),
1429 name: "op".into(),
1430 resource: "POST /users/create".into(),
1431 span_id: 1,
1432 parent_id: 0,
1433 meta: vec![
1434 ("http.method".into(), "POST".into()),
1435 ("http.route".into(), "/users/create".into()),
1436 ("http.endpoint".into(), "/users/create2".into()),
1437 ]
1438 .into(),
1439 ..Default::default()
1440 },
1441 FixedAggregationKey {
1442 service_name: "service".into(),
1443 operation_name: "op".into(),
1444 resource_name: "POST /users/create".into(),
1445 http_method: "POST".into(),
1446 http_endpoint: "/users/create".into(),
1447 is_synthetics_request: false,
1448 is_trace_root: pb::Trilean::True,
1449 ..Default::default()
1450 }
1451 .into_key(),
1452 ),
1453 (
1455 SpanBytes {
1456 service: "service".into(),
1457 name: "op".into(),
1458 resource: "POST /users/create".into(),
1459 span_id: 1,
1460 parent_id: 0,
1461 meta: vec![
1462 ("http.method".into(), "POST".into()),
1463 ("http.route".into(), "".into()),
1464 ("http.endpoint".into(), "/users/create2".into()),
1465 ]
1466 .into(),
1467 ..Default::default()
1468 },
1469 FixedAggregationKey {
1470 service_name: "service".into(),
1471 operation_name: "op".into(),
1472 resource_name: "POST /users/create".into(),
1473 http_method: "POST".into(),
1474 http_endpoint: "/users/create2".into(),
1475 is_synthetics_request: false,
1476 is_trace_root: pb::Trilean::True,
1477 ..Default::default()
1478 }
1479 .into_key(),
1480 ),
1481 (
1483 SpanBytes {
1484 meta: vec![("rpc.grpc.status_code".into(), "OK".into())].into(),
1485 ..Default::default()
1486 },
1487 FixedAggregationKey {
1488 grpc_status_code: Some(0),
1489 is_trace_root: pb::Trilean::True,
1490 ..Default::default()
1491 }
1492 .into_key(),
1493 ),
1494 (
1496 SpanBytes {
1497 meta: vec![("grpc.method.name".into(), "/pkg.Svc/Method".into())].into(),
1498 ..Default::default()
1499 },
1500 FixedAggregationKey {
1501 is_trace_root: pb::Trilean::True,
1502 ..Default::default()
1503 }
1504 .into_key(),
1505 ),
1506 (
1508 SpanBytes {
1509 meta: vec![("rpc.grpc.status_code".into(), "14".into())].into(),
1510 ..Default::default()
1511 },
1512 FixedAggregationKey {
1513 grpc_status_code: Some(14),
1514 is_trace_root: pb::Trilean::True,
1515 ..Default::default()
1516 }
1517 .into_key(),
1518 ),
1519 (
1521 SpanBytes {
1522 meta: vec![("grpc.code".into(), "StatusCode.UNAVAILABLE".into())].into(),
1523 ..Default::default()
1524 },
1525 FixedAggregationKey {
1526 grpc_status_code: Some(14),
1527 is_trace_root: pb::Trilean::True,
1528 ..Default::default()
1529 }
1530 .into_key(),
1531 ),
1532 (
1534 SpanBytes {
1535 meta: vec![("rpc.grpc.status_code".into(), "PERMISSION_DENIED".into())].into(),
1536 metrics: vec![("rpc.grpc.status_code".into(), 2.0)].into(),
1537 ..Default::default()
1538 },
1539 FixedAggregationKey {
1540 grpc_status_code: Some(7),
1541 is_trace_root: pb::Trilean::True,
1542 ..Default::default()
1543 }
1544 .into_key(),
1545 ),
1546 (
1548 SpanBytes {
1549 metrics: vec![("grpc.code".into(), 3.0)].into(),
1550 ..Default::default()
1551 },
1552 FixedAggregationKey {
1553 grpc_status_code: Some(3),
1554 is_trace_root: pb::Trilean::True,
1555 ..Default::default()
1556 }
1557 .into_key(),
1558 ),
1559 (
1561 SpanBytes {
1562 meta: vec![("rpc.grpc.status_code".into(), "NOPE".into())].into(),
1563 ..Default::default()
1564 },
1565 FixedAggregationKey {
1566 is_trace_root: pb::Trilean::True,
1567 ..Default::default()
1568 }
1569 .into_key(),
1570 ),
1571 (
1573 SpanBytes {
1574 service: "my-service".into(),
1575 name: "op".into(),
1576 resource: "res".into(),
1577 span_id: 1,
1578 parent_id: 0,
1579 meta: vec![("_dd.svc_src".into(), "redis".into())].into(),
1580 ..Default::default()
1581 },
1582 FixedAggregationKey {
1583 service_name: "my-service".into(),
1584 operation_name: "op".into(),
1585 resource_name: "res".into(),
1586 is_trace_root: pb::Trilean::True,
1587 service_source: "redis".into(),
1588 ..Default::default()
1589 }
1590 .into_key(),
1591 ),
1592 (
1594 SpanBytes {
1595 service: "my-service".into(),
1596 name: "op".into(),
1597 resource: "res".into(),
1598 span_id: 1,
1599 parent_id: 0,
1600 meta: vec![("_dd.svc_src".into(), "opt.split_by_tag".into())].into(),
1601 ..Default::default()
1602 },
1603 FixedAggregationKey {
1604 service_name: "my-service".into(),
1605 operation_name: "op".into(),
1606 resource_name: "res".into(),
1607 is_trace_root: pb::Trilean::True,
1608 service_source: "opt.split_by_tag".into(),
1609 ..Default::default()
1610 }
1611 .into_key(),
1612 ),
1613 (
1615 SpanBytes {
1616 service: "my-service".into(),
1617 name: "op".into(),
1618 resource: "res".into(),
1619 span_id: 1,
1620 parent_id: 0,
1621 ..Default::default()
1622 },
1623 FixedAggregationKey {
1624 service_name: "my-service".into(),
1625 operation_name: "op".into(),
1626 resource_name: "res".into(),
1627 is_trace_root: pb::Trilean::True,
1628 service_source: "".into(),
1629 ..Default::default()
1630 }
1631 .into_key(),
1632 ),
1633 ];
1634
1635 let test_peer_tags = vec![
1636 "aws.s3.bucket".to_string(),
1637 "db.instance".to_string(),
1638 "db.system".to_string(),
1639 ];
1640
1641 let test_cases_with_peer_tags: Vec<(SpanSlice, OwnedAggregationKey)> = vec![
1642 (
1644 SpanSlice {
1645 service: Cow::Borrowed("service"),
1646 name: Cow::Borrowed("op"),
1647 resource: Cow::Borrowed("res"),
1648 span_id: 1,
1649 parent_id: 0,
1650 meta: vec![
1651 (Cow::Borrowed("span.kind"), Cow::Borrowed("client")),
1652 (Cow::Borrowed("aws.s3.bucket"), Cow::Borrowed("bucket-a")),
1653 ]
1654 .into(),
1655 ..Default::default()
1656 },
1657 FixedAggregationKey {
1658 service_name: "service".into(),
1659 operation_name: "op".into(),
1660 resource_name: "res".into(),
1661 span_kind: "client".into(),
1662 is_trace_root: pb::Trilean::True,
1663 ..Default::default()
1664 }
1665 .into_key_with_peers(vec![("aws.s3.bucket".into(), "bucket-a".into())]),
1666 ),
1667 (
1669 SpanSlice {
1670 service: Cow::Borrowed("service"),
1671 name: Cow::Borrowed("op"),
1672 resource: Cow::Borrowed("res"),
1673 span_id: 1,
1674 parent_id: 0,
1675 meta: vec![
1676 (Cow::Borrowed("span.kind"), Cow::Borrowed("producer")),
1677 (Cow::Borrowed("aws.s3.bucket"), Cow::Borrowed("bucket-a")),
1678 (
1679 Cow::Borrowed("db.instance"),
1680 Cow::Borrowed("dynamo.test.us1"),
1681 ),
1682 (Cow::Borrowed("db.system"), Cow::Borrowed("dynamodb")),
1683 ]
1684 .into(),
1685 ..Default::default()
1686 },
1687 FixedAggregationKey {
1688 service_name: "service".into(),
1689 operation_name: "op".into(),
1690 resource_name: "res".into(),
1691 span_kind: "producer".into(),
1692 is_trace_root: pb::Trilean::True,
1693 ..Default::default()
1694 }
1695 .into_key_with_peers(vec![
1696 ("aws.s3.bucket".into(), "bucket-a".into()),
1697 ("db.instance".into(), "dynamo.test.us1".into()),
1698 ("db.system".into(), "dynamodb".into()),
1699 ]),
1700 ),
1701 (
1704 SpanSlice {
1705 service: Cow::Borrowed("service"),
1706 name: Cow::Borrowed("op"),
1707 resource: Cow::Borrowed("res"),
1708 span_id: 1,
1709 parent_id: 0,
1710 meta: vec![
1711 (Cow::Borrowed("span.kind"), Cow::Borrowed("server")),
1712 (Cow::Borrowed("aws.s3.bucket"), Cow::Borrowed("bucket-a")),
1713 (
1714 Cow::Borrowed("db.instance"),
1715 Cow::Borrowed("dynamo.test.us1"),
1716 ),
1717 (Cow::Borrowed("db.system"), Cow::Borrowed("dynamodb")),
1718 ]
1719 .into(),
1720 ..Default::default()
1721 },
1722 FixedAggregationKey {
1723 service_name: "service".into(),
1724 operation_name: "op".into(),
1725 resource_name: "res".into(),
1726 span_kind: "server".into(),
1727 is_trace_root: pb::Trilean::True,
1728 ..Default::default()
1729 }
1730 .into_key(),
1731 ),
1732 ];
1733
1734 for (span, expected_key) in test_cases {
1735 let borrowed_key = BorrowedAggregationKey::from_span(&span, &[], &[]);
1736 assert_eq!(
1737 OwnedAggregationKey::from(&borrowed_key),
1738 expected_key,
1739 "for span {span:?}"
1740 );
1741 assert_eq!(
1742 get_hash(&borrowed_key),
1743 get_hash(&OwnedAggregationKey::from(&borrowed_key))
1744 );
1745 }
1746
1747 for (span, expected_key) in test_cases_with_peer_tags {
1748 let borrowed_key =
1749 BorrowedAggregationKey::from_span(&span, test_peer_tags.as_slice(), &[]);
1750 assert_eq!(OwnedAggregationKey::from(&borrowed_key), expected_key);
1751 assert_eq!(
1752 get_hash(&borrowed_key),
1753 get_hash(&OwnedAggregationKey::from(&borrowed_key))
1754 );
1755 }
1756 }
1757
1758 #[test]
1759 fn test_peer_tag_ip_quantization_in_aggregation_key() {
1760 let peer_tag_keys = vec!["peer.hostname".to_string(), "db.instance".to_string()];
1761
1762 let span_ipv4 = SpanSlice {
1764 service: Cow::Borrowed("service"),
1765 name: Cow::Borrowed("op"),
1766 resource: Cow::Borrowed("res"),
1767 span_id: 1,
1768 parent_id: 0,
1769 meta: vec![
1770 (Cow::Borrowed("span.kind"), Cow::Borrowed("client")),
1771 (Cow::Borrowed("peer.hostname"), Cow::Borrowed("10.1.2.3")),
1772 (Cow::Borrowed("db.instance"), Cow::Borrowed("my-db")),
1773 ]
1774 .into(),
1775 ..Default::default()
1776 };
1777 let key = BorrowedAggregationKey::from_span(&span_ipv4, &peer_tag_keys, &[]);
1778 let owned = OwnedAggregationKey::from(&key);
1779 assert_eq!(
1780 owned.peer_tags,
1781 vec![
1782 (
1783 "peer.hostname".to_string(),
1784 "blocked-ip-address".to_string()
1785 ),
1786 ("db.instance".to_string(), "my-db".to_string()),
1787 ]
1788 );
1789
1790 let span_ipv6 = SpanSlice {
1792 service: Cow::Borrowed("service"),
1793 name: Cow::Borrowed("op"),
1794 resource: Cow::Borrowed("res"),
1795 span_id: 1,
1796 parent_id: 0,
1797 meta: vec![
1798 (Cow::Borrowed("span.kind"), Cow::Borrowed("client")),
1799 (
1800 Cow::Borrowed("peer.hostname"),
1801 Cow::Borrowed("2001:db8:3333:4444:CCCC:DDDD:EEEE:FFFF"),
1802 ),
1803 ]
1804 .into(),
1805 ..Default::default()
1806 };
1807 let ipv6_keys = vec!["peer.hostname".to_string()];
1808 let key = BorrowedAggregationKey::from_span(&span_ipv6, &ipv6_keys, &[]);
1809 let owned = OwnedAggregationKey::from(&key);
1810 assert_eq!(
1811 owned.peer_tags,
1812 vec![(
1813 "peer.hostname".to_string(),
1814 "blocked-ip-address".to_string()
1815 )]
1816 );
1817
1818 let span_non_ip = SpanSlice {
1820 service: Cow::Borrowed("service"),
1821 name: Cow::Borrowed("op"),
1822 resource: Cow::Borrowed("res"),
1823 span_id: 1,
1824 parent_id: 0,
1825 meta: vec![
1826 (Cow::Borrowed("span.kind"), Cow::Borrowed("client")),
1827 (
1828 Cow::Borrowed("db.instance"),
1829 Cow::Borrowed("dynamo.test.us1"),
1830 ),
1831 ]
1832 .into(),
1833 ..Default::default()
1834 };
1835 let non_ip_keys = vec!["db.instance".to_string()];
1836 let key = BorrowedAggregationKey::from_span(&span_non_ip, &non_ip_keys, &[]);
1837 let owned = OwnedAggregationKey::from(&key);
1838 assert_eq!(
1839 owned.peer_tags,
1840 vec![("db.instance".to_string(), "dynamo.test.us1".to_string())]
1841 );
1842 }
1843
1844 #[test]
1845 fn test_grpc_status_str_to_int_value() {
1846 assert_eq!(grpc_status_str_to_int_value("0"), Some(0));
1848 assert_eq!(grpc_status_str_to_int_value("14"), Some(14));
1849 assert_eq!(grpc_status_str_to_int_value("255"), Some(255));
1850 assert_eq!(grpc_status_str_to_int_value("256"), None);
1851 assert_eq!(grpc_status_str_to_int_value("-1"), None);
1852
1853 assert_eq!(grpc_status_str_to_int_value("OK"), Some(0));
1855 assert_eq!(grpc_status_str_to_int_value("CANCELLED"), Some(1));
1856 assert_eq!(grpc_status_str_to_int_value("UNKNOWN"), Some(2));
1857 assert_eq!(grpc_status_str_to_int_value("INVALID_ARGUMENT"), Some(3));
1858 assert_eq!(grpc_status_str_to_int_value("DEADLINE_EXCEEDED"), Some(4));
1859 assert_eq!(grpc_status_str_to_int_value("NOT_FOUND"), Some(5));
1860 assert_eq!(grpc_status_str_to_int_value("ALREADY_EXISTS"), Some(6));
1861 assert_eq!(grpc_status_str_to_int_value("PERMISSION_DENIED"), Some(7));
1862 assert_eq!(grpc_status_str_to_int_value("UNAUTHENTICATED"), Some(16));
1863 assert_eq!(grpc_status_str_to_int_value("RESOURCE_EXHAUSTED"), Some(8));
1864 assert_eq!(grpc_status_str_to_int_value("FAILED_PRECONDITION"), Some(9));
1865 assert_eq!(grpc_status_str_to_int_value("ABORTED"), Some(10));
1866 assert_eq!(grpc_status_str_to_int_value("OUT_OF_RANGE"), Some(11));
1867 assert_eq!(grpc_status_str_to_int_value("UNIMPLEMENTED"), Some(12));
1868 assert_eq!(grpc_status_str_to_int_value("INTERNAL"), Some(13));
1869 assert_eq!(grpc_status_str_to_int_value("UNAVAILABLE"), Some(14));
1870 assert_eq!(grpc_status_str_to_int_value("DATA_LOSS"), Some(15));
1871
1872 assert_eq!(grpc_status_str_to_int_value("ok"), Some(0));
1874 assert_eq!(grpc_status_str_to_int_value("Cancelled"), Some(1));
1875 assert_eq!(grpc_status_str_to_int_value("not_found"), Some(5));
1876
1877 assert_eq!(grpc_status_str_to_int_value("StatusCode.OK"), Some(0));
1879 assert_eq!(
1880 grpc_status_str_to_int_value("StatusCode.UNAVAILABLE"),
1881 Some(14)
1882 );
1883 assert_eq!(
1884 grpc_status_str_to_int_value("StatusCode.not_found"),
1885 Some(5)
1886 );
1887
1888 assert_eq!(grpc_status_str_to_int_value("CANCELED"), Some(1));
1890
1891 assert_eq!(grpc_status_str_to_int_value("NOPE"), None);
1893 assert_eq!(grpc_status_str_to_int_value(""), None);
1894 assert_eq!(
1895 grpc_status_str_to_int_value("this_is_a_kinda_long_string_that_needs_upcasing"),
1896 None
1897 );
1898
1899 assert_eq!(grpc_status_str_to_int_value("🤣"), None);
1901 }
1902}