From d8230f0314e4bcd28f6dccc6a8ca8ef7878a900c Mon Sep 17 00:00:00 2001 From: Duncan Harvey <35278470+duncanpharvey@users.noreply.github.com> Date: Mon, 9 Sep 2024 10:42:15 -0400 Subject: [PATCH 01/28] Add DogStatsD Package (#606) * feat: upstream dogstatsd from datadog lambda extension * add: stand alone dogstatsd binary as example * fix: imports * fix: make test utils public * fix: more test utils public * fix: make entry public * fix: clippy * fix: make test config public * Revert "fix: make test config public" This reverts commit 7dc979979d85c665767906030f91253725850f30. * fix: make fields public for tests and assertions * use reqwest 0.12.4 in Cargo.lock * add copyright headers * apply formatting * update license * upgrade rust to 1.77.2 * install protoc binary for lint, test, and miri workflows * install protoc binary for cross on centos7 * add dogstatsd to alpine build and upgrade alpine version * install protoc binary for coverage * use rust 1.76.0 and forked saluki-backport repo * remove package specific toolchain * refactor protoc install script to pass install path as an argument * install protoc for benchmark ci job * remove unzip install since it is now part of base image * add dogstatsd integration test * fix license * fix merge * fix lint error * use localhost address for dogstatsd test * use port 18125 for dogstatsd test * disable dogstatsd test for miri * dogstatsd cleanup * fix license * fix merge conflicts * update alpine base image to 3.19.3 --------- Co-authored-by: alexgallotta <5581237+alexgallotta@users.noreply.github.com> --- Cargo.toml | 28 ++ src/aggregator.rs | 795 ++++++++++++++++++++++++++++++++++++++ src/constants.rs | 19 + src/datadog.rs | 189 +++++++++ src/dogstatsd.rs | 201 ++++++++++ src/errors.rs | 33 ++ src/flusher.rs | 42 ++ src/lib.rs | 10 + src/metric.rs | 393 +++++++++++++++++++ tests/integration_test.rs | 88 +++++ 10 files changed, 1798 insertions(+) create mode 100644 Cargo.toml create mode 100644 src/aggregator.rs create mode 100644 src/constants.rs create mode 100644 src/datadog.rs create mode 100644 src/dogstatsd.rs create mode 100644 src/errors.rs create mode 100644 src/flusher.rs create mode 100644 src/lib.rs create mode 100644 src/metric.rs create mode 100644 tests/integration_test.rs diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 00000000..1540ae9a --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "dogstatsd" +rust-version.workspace = true +edition.workspace = true +version.workspace = true +license.workspace = true + +[lib] +bench = false + +[dependencies] +datadog-protos = { version = "0.1.0", default-features = false, git = "https://github.com/DataDog/saluki-backport/" } +ddsketch-agent = { version = "0.1.0", default-features = false, git = "https://github.com/DataDog/saluki-backport/" } +hashbrown = { version = "0.14.3", default-features = false, features = ["inline-more"] } +protobuf = { version = "3.5.0", default-features = false } +ustr = { version = "1.0.0", default-features = false } +fnv = { version = "1.0.7", default-features = false } +reqwest = { version = "0.12.4", features = ["json", "http2", "rustls-tls"], default-features = false } +serde = { version = "1.0.197", default-features = false, features = ["derive"] } +serde_json = { version = "1.0.116", default-features = false, features = ["alloc"] } +thiserror = { version = "1.0.58", default-features = false } +tokio = { version = "1.37.0", default-features = false, features = ["macros", "rt-multi-thread"] } +tokio-util = { version = "0.7.11", default-features = false } +tracing = { version = "0.1.40", default-features = false } + +[dev-dependencies] +mockito = { version = "1.5.0", default-features = false } +proptest = "1.4.0" diff --git a/src/aggregator.rs b/src/aggregator.rs new file mode 100644 index 00000000..506aadd4 --- /dev/null +++ b/src/aggregator.rs @@ -0,0 +1,795 @@ +// Copyright 2023-Present Datadog, Inc. https://www.datadoghq.com/ +// SPDX-License-Identifier: Apache-2.0 + +//! The aggregation of metrics. + +use crate::constants; +use crate::datadog::{self, Metric as MetricToShip, Series}; +use crate::errors; +use crate::metric::{self, Metric as DogstatsdMetric, Type}; +use std::time; + +use datadog_protos::metrics::{Dogsketch, Sketch, SketchPayload}; +use ddsketch_agent::DDSketch; +use hashbrown::hash_table; +use protobuf::Message; +use tracing::{error, warn}; +use ustr::Ustr; + +#[derive(Debug, Clone)] +pub struct Entry { + id: u64, + name: Ustr, + tags: Option, + pub metric_value: MetricValue, +} + +#[derive(Debug, Clone)] +pub enum MetricValue { + Count(f64), + Gauge(f64), + Distribution(DDSketch), +} + +impl MetricValue { + fn insert_metric(&mut self, metric: &DogstatsdMetric) { + // safe because we know there's at least one value when we parse + match self { + MetricValue::Count(count) => *count += metric.first_value().unwrap_or_default(), + MetricValue::Gauge(gauge) => *gauge = metric.first_value().unwrap_or_default(), + MetricValue::Distribution(distribution) => { + distribution.insert(metric.first_value().unwrap_or_default()); + } + } + } + + pub fn get_value(&self) -> Option { + match self { + MetricValue::Count(count) => Some(*count), + MetricValue::Gauge(gauge) => Some(*gauge), + MetricValue::Distribution(_) => None, + } + } + + pub fn get_sketch(&self) -> Option<&DDSketch> { + match self { + MetricValue::Distribution(distribution) => Some(distribution), + _ => None, + } + } +} + +impl Entry { + fn new_from_metric(id: u64, metric: &DogstatsdMetric) -> Self { + let mut metric_value = match metric.kind { + Type::Count => MetricValue::Count(0.0), + Type::Gauge => MetricValue::Gauge(0.0), + Type::Distribution => MetricValue::Distribution(DDSketch::default()), + }; + metric_value.insert_metric(metric); + Self { + id, + name: metric.name, + tags: metric.tags, + metric_value, + } + } + + /// Return an iterator over key, value pairs + fn tag(&self) -> impl Iterator { + self.tags.into_iter().filter_map(|tags| { + let mut split = tags.split(','); + match (split.next(), split.next()) { + (Some(k), Some(v)) => Some((Ustr::from(k), Ustr::from(v))), + _ => None, // Skip tags that lack the proper format + } + }) + } +} + +#[derive(Clone)] +// NOTE by construction we know that intervals and contexts do not explore the +// full space of usize but the type system limits how we can express this today. +pub struct Aggregator { + tags: Vec, + map: hash_table::HashTable, + max_batch_entries_single_metric: usize, + max_batch_bytes_single_metric: u64, + max_batch_entries_sketch_metric: usize, + max_batch_bytes_sketch_metric: u64, + max_context: usize, +} + +impl Aggregator { + /// Create a new instance of `Aggregator` + /// + /// # Errors + /// + /// Will fail at runtime if the type `INTERVALS` and `CONTEXTS` exceed their + /// counterparts in `constants`. This would be better as a compile-time + /// issue, although leaving this open allows for runtime configuration. + #[allow(clippy::cast_precision_loss)] + pub fn new(tags: Vec, max_context: usize) -> Result { + if max_context > constants::MAX_CONTEXTS { + return Err(errors::Creation::Contexts); + } + Ok(Self { + tags, + map: hash_table::HashTable::new(), + max_batch_entries_single_metric: constants::MAX_ENTRIES_SINGLE_METRIC, + max_batch_bytes_single_metric: constants::MAX_SIZE_BYTES_SINGLE_METRIC, + max_batch_entries_sketch_metric: constants::MAX_ENTRIES_SKETCH_METRIC, + max_batch_bytes_sketch_metric: constants::MAX_SIZE_SKETCH_METRIC, + max_context, + }) + } + + /// Insert a `Metric` into the `Aggregator` at the current interval + /// + /// # Errors + /// + /// Function will return overflow error if more than + /// `min(constants::MAX_CONTEXTS, CONTEXTS)` is exceeded. + pub fn insert(&mut self, metric: &DogstatsdMetric) -> Result<(), errors::Insert> { + let id = metric::id(metric.name, metric.tags); + let len = self.map.len(); + + match self + .map + .entry(id, |m| m.id == id, |m| metric::id(m.name, m.tags)) + { + hash_table::Entry::Vacant(entry) => { + if len >= self.max_context { + return Err(errors::Insert::Overflow); + } + let ent = Entry::new_from_metric(id, metric); + entry.insert(ent); + } + hash_table::Entry::Occupied(mut entry) => { + entry.get_mut().metric_value.insert_metric(metric); + } + } + Ok(()) + } + + pub fn clear(&mut self) { + self.map.clear(); + } + + #[must_use] + pub fn distributions_to_protobuf(&self) -> SketchPayload { + let now = time::SystemTime::now() + .duration_since(time::UNIX_EPOCH) + .expect("unable to poll clock, unrecoverable") + .as_secs() + .try_into() + .unwrap_or_default(); + let mut sketch_payload = SketchPayload::new(); + + self.map + .iter() + .filter_map(|entry| match entry.metric_value { + MetricValue::Distribution(_) => build_sketch(now, entry, &self.tags), + _ => None, + }) + .for_each(|sketch| sketch_payload.sketches.push(sketch)); + sketch_payload + } + + #[must_use] + pub fn consume_distributions(&mut self) -> Vec { + let now = time::SystemTime::now() + .duration_since(time::UNIX_EPOCH) + .expect("unable to poll clock, unrecoverable") + .as_secs() + .try_into() + .unwrap_or_default(); + let mut batched_payloads = Vec::new(); + let mut sketch_payload = SketchPayload::new(); + let mut this_batch_size = 0u64; + for sketch in self + .map + .extract_if(|entry| { + if let MetricValue::Distribution(_) = entry.metric_value { + return true; + } + false + }) + .filter_map(|entry| build_sketch(now, &entry, &self.tags)) + { + let next_chunk_size = sketch.compute_size(); + + if (sketch_payload.sketches.len() >= self.max_batch_entries_sketch_metric) + || (this_batch_size + next_chunk_size >= self.max_batch_bytes_sketch_metric) + { + if this_batch_size == 0 { + warn!("Only one distribution exceeds max batch size, adding it anyway: {:?} with {}", sketch.metric, next_chunk_size); + } else { + batched_payloads.push(sketch_payload); + sketch_payload = SketchPayload::new(); + this_batch_size = 0u64; + } + } + this_batch_size += next_chunk_size; + sketch_payload.sketches.push(sketch); + } + if !sketch_payload.sketches.is_empty() { + batched_payloads.push(sketch_payload); + } + batched_payloads + } + + #[must_use] + pub fn to_series(&self) -> Series { + let mut series_payload = Series { + series: Vec::with_capacity(1_024), + }; + + self.map + .iter() + .filter_map(|entry| match entry.metric_value { + MetricValue::Distribution(_) => None, + _ => build_metric(entry, &self.tags), + }) + .for_each(|metric| series_payload.series.push(metric)); + series_payload + } + + #[must_use] + pub fn consume_metrics(&mut self) -> Vec { + let mut batched_payloads = Vec::new(); + let mut series_payload = Series { + series: Vec::with_capacity(1_024), + }; + let mut this_batch_size = 0u64; + for metric in self + .map + .extract_if(|entry| { + if let MetricValue::Distribution(_) = entry.metric_value { + return false; + } + true + }) + .filter_map(|entry| build_metric(&entry, &self.tags)) + { + // TODO serialization is made twice for each point. If we return a Vec we can avoid + // that + let serialized_metric_size = match serde_json::to_vec(&metric) { + Ok(serialized_metric) => serialized_metric.len() as u64, + Err(e) => { + error!("failed to serialize metric: {:?}", e); + 0u64 + } + }; + + if serialized_metric_size > 0 { + if (series_payload.series.len() >= self.max_batch_entries_single_metric) + || (this_batch_size + serialized_metric_size + >= self.max_batch_bytes_single_metric) + { + if this_batch_size == 0 { + warn!("Only one metric exceeds max batch size, adding it anyway: {:?} with {}", metric.metric, serialized_metric_size); + } else { + batched_payloads.push(series_payload); + series_payload = Series { + series: Vec::with_capacity(1_024), + }; + this_batch_size = 0u64; + } + } + series_payload.series.push(metric); + this_batch_size += serialized_metric_size; + } + } + + if !series_payload.series.is_empty() { + batched_payloads.push(series_payload); + } + batched_payloads + } + + pub fn get_entry_by_id(&self, name: Ustr, tags: Option) -> Option<&Entry> { + let id = metric::id(name, tags); + self.map.find(id, |m| m.id == id) + } +} + +fn build_sketch(now: i64, entry: &Entry, base_tag_vec: &[String]) -> Option { + let sketch = entry.metric_value.get_sketch()?; + let mut dogsketch = Dogsketch::default(); + sketch.merge_to_dogsketch(&mut dogsketch); + // TODO(Astuyve) allow users to specify timestamp + dogsketch.set_ts(now); + let mut sketch = Sketch::default(); + sketch.set_dogsketches(vec![dogsketch]); + let name = entry.name.to_string(); + sketch.set_metric(name.clone().into()); + let mut tags = tags_string_to_vector(entry.tags); + tags.extend(base_tag_vec.to_owned()); // TODO split on comma + sketch.set_tags(tags.into_iter().map(std::convert::Into::into).collect()); + Some(sketch) +} + +fn build_metric(entry: &Entry, base_tag_vec: &[String]) -> Option { + let mut resources = Vec::with_capacity(constants::MAX_TAGS); + for (name, kind) in entry.tag() { + let resource = datadog::Resource { + name: name.as_str(), + kind: kind.as_str(), + }; + resources.push(resource); + } + let kind = match entry.metric_value { + MetricValue::Count(_) => datadog::DdMetricKind::Count, + MetricValue::Gauge(_) => datadog::DdMetricKind::Gauge, + MetricValue::Distribution(_) => unreachable!(), + }; + let point = datadog::Point { + value: entry.metric_value.get_value()?, + // TODO(astuyve) allow user to specify timestamp + timestamp: time::SystemTime::now() + .duration_since(time::UNIX_EPOCH) + .expect("unable to poll clock, unrecoverable") + .as_secs(), + }; + + let mut final_tags = Vec::new(); + // TODO + // These tags are interned so we don't need to clone them here but we're just doing it + // because it's easier than dealing with the lifetimes. + if let Some(tags) = entry.tags { + final_tags = tags.split(',').map(ToString::to_string).collect(); + } + final_tags.extend(base_tag_vec.to_owned()); + Some(MetricToShip { + metric: entry.name.as_str(), + resources, + kind, + points: [point; 1], + tags: final_tags, + }) +} + +fn tags_string_to_vector(tags: Option) -> Vec { + if tags.is_none() { + return Vec::new(); + } + tags.unwrap_or_default() + .split(',') + .map(ToString::to_string) + .collect() +} + +#[cfg(test)] +#[allow(clippy::unwrap_used)] +pub mod tests { + use crate::aggregator::Aggregator; + use crate::metric; + use crate::metric::Metric; + use datadog_protos::metrics::SketchPayload; + use hashbrown::hash_table; + use protobuf::Message; + use std::sync::Mutex; + + const PRECISION: f64 = 0.000_000_01; + + const SINGLE_METRIC_SIZE: usize = 187; + const SINGLE_DISTRIBUTION_SIZE: u64 = 135; + const DEFAULT_TAGS: &[&str] = &[ + "dd_extension_version:63-next", + "architecture:x86_64", + "_dd.compute_stats:1", + ]; + + pub fn assert_value(aggregator_mutex: &Mutex, metric_id: &str, value: f64) { + let aggregator = aggregator_mutex.lock().unwrap(); + if let Some(e) = aggregator.get_entry_by_id(metric_id.into(), None) { + let metric = e.metric_value.get_value().unwrap(); + assert!((metric - value).abs() < PRECISION); + } else { + panic!("{}", format!("{metric_id} not found")); + } + } + + pub fn assert_sketch(aggregator_mutex: &Mutex, metric_id: &str, value: f64) { + let aggregator = aggregator_mutex.lock().unwrap(); + if let Some(e) = aggregator.get_entry_by_id(metric_id.into(), None) { + let metric = e.metric_value.get_sketch().unwrap(); + assert!((metric.max().unwrap() - value).abs() < PRECISION); + assert!((metric.min().unwrap() - value).abs() < PRECISION); + assert!((metric.sum().unwrap() - value).abs() < PRECISION); + assert!((metric.avg().unwrap() - value).abs() < PRECISION); + } else { + panic!("{}", format!("{metric_id} not found")); + } + } + + #[test] + fn insertion() { + let mut aggregator = Aggregator::new(Vec::new(), 2).unwrap(); + + let metric1 = Metric::parse("test:1|c|k:v").expect("metric parse failed"); + let metric2 = Metric::parse("foo:1|c|k:v").expect("metric parse failed"); + + assert!(aggregator.insert(&metric1).is_ok()); + assert!(aggregator.insert(&metric2).is_ok()); + + // Both unique contexts get one slot. + assert_eq!(aggregator.map.len(), 2); + } + + #[test] + fn distribution_insertion() { + let mut aggregator = Aggregator::new(Vec::new(), 2).unwrap(); + + let metric1 = Metric::parse("test:1|d|k:v").expect("metric parse failed"); + let metric2 = Metric::parse("foo:1|d|k:v").expect("metric parse failed"); + + assert!(aggregator.insert(&metric1).is_ok()); + assert!(aggregator.insert(&metric2).is_ok()); + + // Both unique contexts get one slot. + assert_eq!(aggregator.map.len(), 2); + } + + #[test] + fn overflow() { + let mut aggregator = Aggregator::new(Vec::new(), 2).unwrap(); + + let metric1 = Metric::parse("test:1|c|k:v").expect("metric parse failed"); + let metric2 = Metric::parse("foo:1|c|k:v").expect("metric parse failed"); + let metric3 = Metric::parse("bar:1|c|k:v").expect("metric parse failed"); + + let id1 = metric::id(metric1.name, metric1.tags); + let id2 = metric::id(metric2.name, metric2.tags); + let id3 = metric::id(metric3.name, metric3.tags); + + assert_ne!(id1, id2); + assert_ne!(id1, id3); + assert_ne!(id2, id3); + + assert!(aggregator.insert(&metric1).is_ok()); + assert_eq!(aggregator.map.len(), 1); + + assert!(aggregator.insert(&metric2).is_ok()); + assert!(aggregator.insert(&metric2).is_ok()); + assert!(aggregator.insert(&metric2).is_ok()); + assert_eq!(aggregator.map.len(), 2); + + assert!(aggregator.insert(&metric3).is_err()); + assert_eq!(aggregator.map.len(), 2); + } + + #[test] + #[allow(clippy::float_cmp)] + fn clear() { + let mut aggregator = Aggregator::new(Vec::new(), 2).unwrap(); + + let metric1 = Metric::parse("test:3|c|k:v").expect("metric parse failed"); + let metric2 = Metric::parse("foo:5|c|k:v").expect("metric parse failed"); + + assert!(aggregator.insert(&metric1).is_ok()); + assert!(aggregator.insert(&metric2).is_ok()); + + assert_eq!(aggregator.map.len(), 2); + if let Some(v) = aggregator.get_entry_by_id("foo".into(), None) { + assert_eq!(v.metric_value.get_value().unwrap(), 5f64); + } else { + panic!("failed to get value by id"); + } + + if let Some(v) = aggregator.get_entry_by_id("test".into(), None) { + assert_eq!(v.metric_value.get_value().unwrap(), 3f64); + } else { + panic!("failed to get value by id"); + } + + aggregator.clear(); + assert_eq!(aggregator.map.len(), 0); + } + + #[test] + fn to_series() { + let mut aggregator = Aggregator::new(Vec::new(), 2).unwrap(); + + let metric1 = Metric::parse("test:1|c|k:v").expect("metric parse failed"); + let metric2 = Metric::parse("foo:1|c|k:v").expect("metric parse failed"); + let metric3 = Metric::parse("bar:1|c|k:v").expect("metric parse failed"); + + assert!(aggregator.insert(&metric1).is_ok()); + assert!(aggregator.insert(&metric2).is_ok()); + + assert_eq!(aggregator.map.len(), 2); + assert_eq!(aggregator.to_series().len(), 2); + assert_eq!(aggregator.map.len(), 2); + assert_eq!(aggregator.to_series().len(), 2); + assert_eq!(aggregator.map.len(), 2); + + assert!(aggregator.insert(&metric3).is_err()); + assert_eq!(aggregator.to_series().len(), 2); + } + + #[test] + fn distributions_to_protobuf() { + let mut aggregator = Aggregator::new(Vec::new(), 2).unwrap(); + + let metric1 = Metric::parse("test:1|d|k:v").expect("metric parse failed"); + let metric2 = Metric::parse("foo:1|d|k:v").expect("metric parse failed"); + + assert!(aggregator.insert(&metric1).is_ok()); + assert!(aggregator.insert(&metric2).is_ok()); + + assert_eq!(aggregator.map.len(), 2); + assert_eq!(aggregator.distributions_to_protobuf().sketches().len(), 2); + assert_eq!(aggregator.map.len(), 2); + assert_eq!(aggregator.distributions_to_protobuf().sketches().len(), 2); + assert_eq!(aggregator.map.len(), 2); + } + + #[test] + fn consume_distributions_ignore_single_metrics() { + let mut aggregator = Aggregator::new(Vec::new(), 1_000).unwrap(); + assert_eq!(aggregator.distributions_to_protobuf().sketches.len(), 0); + + assert!(aggregator + .insert( + &Metric::parse("test1:1|d|k:v".to_string().as_str()).expect("metric parse failed") + ) + .is_ok()); + assert_eq!(aggregator.distributions_to_protobuf().sketches.len(), 1); + + assert!(aggregator + .insert(&Metric::parse("foo:1|c|k:v").expect("metric parse failed")) + .is_ok()); + assert_eq!(aggregator.distributions_to_protobuf().sketches.len(), 1); + } + + #[test] + fn consume_distributions_batch_entries() { + let max_batch = 5; + let tot = 12; + let mut aggregator = Aggregator { + tags: Vec::new(), + map: hash_table::HashTable::new(), + max_batch_entries_single_metric: 1_000, + max_batch_bytes_single_metric: 1_000, + max_batch_entries_sketch_metric: max_batch, + max_batch_bytes_sketch_metric: 1_500, + max_context: 1_000, + }; + + add_metrics(tot, &mut aggregator, "d".to_string()); + let batched = aggregator.consume_distributions(); + assert_eq!(aggregator.consume_distributions().len(), 0); + + assert_eq!(batched.len(), 3); + assert_eq!(batched.first().unwrap().sketches.len(), max_batch); + assert_eq!(batched.get(1).unwrap().sketches.len(), max_batch); + assert_eq!(batched.get(2).unwrap().sketches.len(), tot - max_batch * 2); + } + + #[test] + fn consume_distributions_batch_bytes() { + let expected_distribution_per_batch = 2; + let total_number_of_distributions = 5; + let max_bytes = SINGLE_METRIC_SIZE * expected_distribution_per_batch + 11; + let mut aggregator = Aggregator { + tags: DEFAULT_TAGS + .to_vec() + .iter() + .map(ToString::to_string) + .collect(), + map: hash_table::HashTable::new(), + max_batch_entries_single_metric: 1_000, + max_batch_bytes_single_metric: 1_000, + max_batch_entries_sketch_metric: 1_000, + max_batch_bytes_sketch_metric: max_bytes as u64, + max_context: 1_000, + }; + + add_metrics( + total_number_of_distributions, + &mut aggregator, + "d".to_string(), + ); + let batched = aggregator.consume_distributions(); + + assert_eq!( + batched.len(), + total_number_of_distributions / expected_distribution_per_batch + 1 + ); + assert_eq!( + batched.first().unwrap().compute_size(), + SINGLE_DISTRIBUTION_SIZE * expected_distribution_per_batch as u64 + ); + assert_eq!( + batched.get(1).unwrap().compute_size(), + SINGLE_DISTRIBUTION_SIZE * expected_distribution_per_batch as u64 + ); + assert_eq!( + batched.get(2).unwrap().compute_size(), + SINGLE_DISTRIBUTION_SIZE + ); + } + + #[test] + fn consume_distribution_one_element_bigger_than_max_size() { + let max_bytes = 1; + let tot = 5; + let mut aggregator = Aggregator { + tags: DEFAULT_TAGS + .to_vec() + .iter() + .map(ToString::to_string) + .collect(), + map: hash_table::HashTable::new(), + max_batch_entries_single_metric: 1_000, + max_batch_bytes_single_metric: 1_000, + max_batch_entries_sketch_metric: 1_000, + max_batch_bytes_sketch_metric: max_bytes, + max_context: 1_000, + }; + + add_metrics(tot, &mut aggregator, "d".to_string()); + let batched = aggregator.consume_distributions(); + + assert_eq!(batched.len(), tot); + for a_batch in batched { + assert_eq!(a_batch.compute_size(), SINGLE_DISTRIBUTION_SIZE); + } + } + + fn add_metrics(tot: usize, aggregator: &mut Aggregator, counter_or_distro: String) { + for i in 1..=tot { + assert!(aggregator + .insert( + &Metric::parse(format!("test{i}:{i}|{counter_or_distro}|k:v").as_str()) + .expect("metric parse failed") + ) + .is_ok()); + } + } + + #[test] + fn consume_series_ignore_distribution() { + let mut aggregator = Aggregator::new(Vec::new(), 1_000).unwrap(); + + assert_eq!(aggregator.consume_metrics().len(), 0); + + assert!(aggregator + .insert( + &Metric::parse("test1:1|c|k:v".to_string().as_str()).expect("metric parse failed") + ) + .is_ok()); + assert_eq!(aggregator.consume_distributions().len(), 0); + assert_eq!(aggregator.consume_metrics().len(), 1); + assert_eq!(aggregator.consume_metrics().len(), 0); + + assert!(aggregator + .insert( + &Metric::parse("test1:1|c|k:v".to_string().as_str()).expect("metric parse failed") + ) + .is_ok()); + assert!(aggregator + .insert(&Metric::parse("foo:1|d|k:v").expect("metric parse failed")) + .is_ok()); + assert_eq!(aggregator.consume_metrics().len(), 1); + assert_eq!(aggregator.consume_distributions().len(), 1); + assert_eq!(aggregator.consume_distributions().len(), 0); + } + + #[test] + fn consume_series_batch_entries() { + let max_batch = 5; + let tot = 13; + let mut aggregator = Aggregator { + tags: Vec::new(), + map: hash_table::HashTable::new(), + max_batch_entries_single_metric: max_batch, + max_batch_bytes_single_metric: 10_000, + max_batch_entries_sketch_metric: 1_000, + max_batch_bytes_sketch_metric: 1_500, + max_context: 1_000, + }; + + add_metrics(tot, &mut aggregator, "c".to_string()); + + let batched = aggregator.consume_metrics(); + assert_eq!(batched.len(), 3); + assert_eq!(batched.first().unwrap().series.len(), max_batch); + assert_eq!(batched.get(1).unwrap().series.len(), max_batch); + assert_eq!(batched.get(2).unwrap().series.len(), tot - max_batch * 2); + + assert_eq!(aggregator.consume_metrics().len(), 0); + } + + #[test] + fn consume_metrics_batch_bytes() { + let expected_metrics_per_batch = 2; + let total_number_of_metrics = 5; + let two_metrics_size = 362; + let max_bytes = SINGLE_METRIC_SIZE * expected_metrics_per_batch + 13; + let mut aggregator = Aggregator { + tags: DEFAULT_TAGS + .to_vec() + .iter() + .map(ToString::to_string) + .collect(), + map: hash_table::HashTable::new(), + max_batch_entries_single_metric: 1_000, + max_batch_bytes_single_metric: max_bytes as u64, + max_batch_entries_sketch_metric: 1_000, + max_batch_bytes_sketch_metric: 1_000, + max_context: 1_000, + }; + + add_metrics(total_number_of_metrics, &mut aggregator, "c".to_string()); + let batched = aggregator.consume_metrics(); + + assert_eq!( + batched.len(), + total_number_of_metrics / expected_metrics_per_batch + 1 + ); + assert_eq!( + serde_json::to_vec(batched.first().unwrap()).unwrap().len(), + two_metrics_size + ); + assert_eq!( + serde_json::to_vec(batched.get(1).unwrap()).unwrap().len(), + two_metrics_size + ); + assert_eq!( + serde_json::to_vec(batched.get(2).unwrap()).unwrap().len(), + SINGLE_METRIC_SIZE + ); + } + + #[test] + fn consume_series_one_element_bigger_than_max_size() { + let max_bytes = 1; + let tot = 5; + let mut aggregator = Aggregator { + tags: DEFAULT_TAGS + .to_vec() + .iter() + .map(ToString::to_string) + .collect(), + map: hash_table::HashTable::new(), + max_batch_entries_single_metric: 1_000, + max_batch_bytes_single_metric: max_bytes, + max_batch_entries_sketch_metric: 1_000, + max_batch_bytes_sketch_metric: 1_000, + max_context: 1_000, + }; + + add_metrics(tot, &mut aggregator, "c".to_string()); + let batched = aggregator.consume_metrics(); + + assert_eq!(batched.len(), tot); + for a_batch in batched { + assert_eq!( + serde_json::to_vec(&a_batch).unwrap().len(), + SINGLE_METRIC_SIZE + ); + } + } + + #[test] + fn distribution_serialized_deserialized() { + let mut aggregator = Aggregator::new(Vec::new(), 1_000).unwrap(); + + add_metrics(10, &mut aggregator, "d".to_string()); + let distribution = aggregator.distributions_to_protobuf(); + assert_eq!(distribution.sketches().len(), 10); + + let serialized = distribution + .write_to_bytes() + .expect("Can't serialized proto"); + + let deserialized = + SketchPayload::parse_from_bytes(serialized.as_slice()).expect("failed to parse proto"); + + assert_eq!(deserialized.sketches().len(), 10); + assert_eq!(deserialized, distribution); + } +} diff --git a/src/constants.rs b/src/constants.rs new file mode 100644 index 00000000..70e17d24 --- /dev/null +++ b/src/constants.rs @@ -0,0 +1,19 @@ +// Copyright 2023-Present Datadog, Inc. https://www.datadoghq.com/ +// SPDX-License-Identifier: Apache-2.0 + +/// The maximum tags that a `Metric` may hold. +pub const MAX_TAGS: usize = 32; + +pub const CONTEXTS: usize = 1024; + +pub static MAX_CONTEXTS: usize = 65_536; // 2**16, arbitrary + +const MB: u64 = 1_024 * 1_024; + +pub(crate) const MAX_ENTRIES_SINGLE_METRIC: usize = 1_000; + +pub(crate) const MAX_SIZE_BYTES_SINGLE_METRIC: u64 = 5 * MB; + +pub(crate) const MAX_ENTRIES_SKETCH_METRIC: usize = 1_000; + +pub(crate) const MAX_SIZE_SKETCH_METRIC: u64 = 62 * MB; diff --git a/src/datadog.rs b/src/datadog.rs new file mode 100644 index 00000000..3a97c0ff --- /dev/null +++ b/src/datadog.rs @@ -0,0 +1,189 @@ +// Copyright 2023-Present Datadog, Inc. https://www.datadoghq.com/ +// SPDX-License-Identifier: Apache-2.0 + +//!Types to serialize data into the Datadog API + +use datadog_protos::metrics::SketchPayload; +use protobuf::Message; +use reqwest; +use serde::{Serialize, Serializer}; +use serde_json; +use tracing::{debug, error}; + +/// Interface for the `DogStatsD` metrics intake API. +#[derive(Debug)] +pub struct DdApi { + api_key: String, + fqdn_site: String, + client: reqwest::Client, +} +/// Error relating to `ship` +#[derive(thiserror::Error, Debug)] +pub enum ShipError { + #[error("Failed to push to API with status {status}: {body}")] + /// Datadog API failure + Failure { + /// HTTP status code + status: u16, + /// HTTP body that failed + body: String, + }, + + /// Json + #[error(transparent)] + Json(#[from] serde_json::Error), +} + +impl DdApi { + #[must_use] + pub fn new(api_key: String, site: String) -> Self { + DdApi { + api_key, + fqdn_site: site, + client: reqwest::Client::new(), + } + } + + /// Ship a serialized series to the API, blocking + pub async fn ship_series(&self, series: &Series) { + let body = serde_json::to_vec(&series).expect("failed to serialize series"); + debug!("Sending body: {:?}", &series); + + let url = format!("{}/api/v2/series", &self.fqdn_site); + let resp = self + .client + .post(&url) + .header("DD-API-KEY", &self.api_key) + .header("Content-Type", "application/json") + .body(body) + .send() + .await; + + match resp { + Ok(resp) => match resp.status() { + reqwest::StatusCode::ACCEPTED => {} + unexpected_status_code => { + debug!( + "{}: Failed to push to API: {:?}", + unexpected_status_code, + resp.text().await.unwrap_or_default() + ); + } + }, + Err(e) => { + debug!("500: Failed to push to API: {:?}", e); + } + }; + } + + pub async fn ship_distributions(&self, sketches: &SketchPayload) { + let url = format!("{}/api/beta/sketches", &self.fqdn_site); + debug!("Sending distributions: {:?}", &sketches); + // TODO maybe go to coded output stream if we incrementally + // add sketch payloads to the buffer + // something like this, but fix the utf-8 encoding issue + // { + // let mut output_stream = CodedOutputStream::vec(&mut buf); + // let _ = output_stream.write_tag(1, protobuf::rt::WireType::LengthDelimited); + // let _ = output_stream.write_message_no_tag(&sketches); + // TODO not working, has utf-8 encoding issue in dist-intake + //} + let resp = self + .client + .post(&url) + .header("DD-API-KEY", &self.api_key) + .header("Content-Type", "application/x-protobuf") + .body(sketches.write_to_bytes().expect("can't write to buffer")) + .send() + .await; + match resp { + Ok(resp) => match resp.status() { + reqwest::StatusCode::ACCEPTED => {} + unexpected_status_code => { + debug!( + "{}: Failed to push to API: {:?}", + unexpected_status_code, + resp.text().await.unwrap_or_default() + ); + } + }, + Err(e) => { + debug!("500: Failed to push to API: {:?}", e); + } + }; + } +} + +#[derive(Debug, Serialize, Clone, Copy)] +/// A single point in time +pub(crate) struct Point { + /// The time at which the point exists + pub(crate) timestamp: u64, + /// The point's value + pub(crate) value: f64, +} + +#[derive(Debug, Serialize)] +/// A named resource +pub(crate) struct Resource { + /// The name of this resource + pub(crate) name: &'static str, + #[serde(rename = "type")] + /// The kind of this resource + pub(crate) kind: &'static str, +} + +#[derive(Debug, Clone, Copy)] +/// The kinds of metrics the Datadog API supports +pub(crate) enum DdMetricKind { + /// An accumulating sum + Count, + /// An instantaneous value + Gauge, +} + +impl Serialize for DdMetricKind { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + match *self { + DdMetricKind::Count => serializer.serialize_u32(0), + DdMetricKind::Gauge => serializer.serialize_u32(1), + } + } +} + +#[derive(Debug, Serialize)] +#[allow(clippy::struct_field_names)] +/// A named collection of `Point` instances. +pub(crate) struct Metric { + /// The name of the point collection + pub(crate) metric: &'static str, + /// The collection of points + pub(crate) points: [Point; 1], + /// The resources associated with the points + pub(crate) resources: Vec, + #[serde(rename = "type")] + /// The kind of metric + pub(crate) kind: DdMetricKind, + pub(crate) tags: Vec, +} + +#[derive(Debug, Serialize)] +/// A collection of metrics as defined by the Datadog Metrics API. +// NOTE we have a number of `Vec` instances in this implementation that could +// otherwise be arrays, given that we have constants. Serializing to JSON would +// require us to avoid serializing None or Uninit values, so there's some custom +// work that's needed. For protobuf this more or less goes away. +pub struct Series { + /// The collection itself + pub(crate) series: Vec, +} + +impl Series { + #[cfg(test)] + pub(crate) fn len(&self) -> usize { + self.series.len() + } +} diff --git a/src/dogstatsd.rs b/src/dogstatsd.rs new file mode 100644 index 00000000..4d1101bb --- /dev/null +++ b/src/dogstatsd.rs @@ -0,0 +1,201 @@ +// Copyright 2023-Present Datadog, Inc. https://www.datadoghq.com/ +// SPDX-License-Identifier: Apache-2.0 + +use std::net::SocketAddr; +use std::str::Split; +use std::sync::{Arc, Mutex}; + +use tracing::{debug, error}; + +use crate::aggregator::Aggregator; +use crate::metric::Metric; + +pub struct DogStatsD { + cancel_token: tokio_util::sync::CancellationToken, + aggregator: Arc>, + buffer_reader: BufferReader, +} + +pub struct DogStatsDConfig { + pub host: String, + pub port: u16, +} + +enum BufferReader { + UdpSocketReader(tokio::net::UdpSocket), + #[allow(dead_code)] + MirrorReader(Vec, SocketAddr), +} + +impl BufferReader { + async fn read(&self) -> std::io::Result<(Vec, SocketAddr)> { + match self { + BufferReader::UdpSocketReader(socket) => { + // TODO(astuyve) this should be dynamic + let mut buf = [0; 1024]; // todo, do we want to make this dynamic? (not sure) + let (amt, src) = socket + .recv_from(&mut buf) + .await + .expect("didn't receive data"); + Ok((buf[..amt].to_owned(), src)) + } + BufferReader::MirrorReader(data, socket) => Ok((data.clone(), *socket)), + } + } +} + +impl DogStatsD { + #[must_use] + pub async fn new( + config: &DogStatsDConfig, + aggregator: Arc>, + cancel_token: tokio_util::sync::CancellationToken, + ) -> DogStatsD { + let addr = format!("{}:{}", config.host, config.port); + // TODO (UDS socket) + let socket = tokio::net::UdpSocket::bind(addr) + .await + .expect("couldn't bind to address"); + DogStatsD { + cancel_token, + aggregator, + buffer_reader: BufferReader::UdpSocketReader(socket), + } + } + + pub async fn spin(self) { + let mut spin_cancelled = false; + while !spin_cancelled { + self.consume_statsd().await; + spin_cancelled = self.cancel_token.is_cancelled(); + } + } + + async fn consume_statsd(&self) { + let (buf, src) = self + .buffer_reader + .read() + .await + .expect("didn't receive data"); + let msgs = std::str::from_utf8(&buf).expect("couldn't parse as string"); + debug!("Received message: {} from {}", msgs, src); + let statsd_metric_strings = msgs.split('\n'); + self.insert_metrics(statsd_metric_strings); + } + + fn insert_metrics(&self, msg: Split) { + let all_valid_metrics: Vec = msg + .filter(|m| !m.is_empty()) + .map(|m| m.replace('\n', "")) + .filter_map(|m| match Metric::parse(m.as_str()) { + Ok(metric) => Some(metric), + Err(e) => { + error!("Failed to parse metric {}: {}", m, e); + None + } + }) + .collect(); + if !all_valid_metrics.is_empty() { + let mut guarded_aggregator = self.aggregator.lock().expect("lock poisoned"); + for a_valid_value in all_valid_metrics { + let _ = guarded_aggregator.insert(&a_valid_value); + } + } + } +} + +#[cfg(test)] +#[allow(clippy::unwrap_used)] +mod tests { + use crate::aggregator::tests::assert_sketch; + use crate::aggregator::tests::assert_value; + use crate::aggregator::Aggregator; + use crate::dogstatsd::{BufferReader, DogStatsD}; + use std::net::{IpAddr, Ipv4Addr, SocketAddr}; + use std::sync::{Arc, Mutex}; + + #[tokio::test] + async fn test_dogstatsd_multi_distribution() { + let locked_aggregator = setup_dogstatsd( + "single_machine_performance.rouster.api.series_v2.payload_size_bytes:269942|d +single_machine_performance.rouster.metrics_min_timestamp_latency:1426.90870216|d +single_machine_performance.rouster.metrics_max_timestamp_latency:1376.90870216|d +", + ) + .await; + let aggregator = locked_aggregator.lock().expect("lock poisoned"); + + let parsed_metrics = aggregator.distributions_to_protobuf(); + + assert_eq!(parsed_metrics.sketches.len(), 3); + assert_eq!(aggregator.to_series().len(), 0); + drop(aggregator); + + assert_sketch( + &locked_aggregator, + "single_machine_performance.rouster.api.series_v2.payload_size_bytes", + 269_942_f64, + ); + assert_sketch( + &locked_aggregator, + "single_machine_performance.rouster.metrics_min_timestamp_latency", + 1_426.908_702_16, + ); + assert_sketch( + &locked_aggregator, + "single_machine_performance.rouster.metrics_max_timestamp_latency", + 1_376.908_702_16, + ); + } + + #[tokio::test] + async fn test_dogstatsd_multi_metric() { + let locked_aggregator = setup_dogstatsd( + "metric1:1|c\nmetric2:2|c|tag2:val2\nmetric3:3|c||tag3:val3,tag4:val4\n", + ) + .await; + let aggregator = locked_aggregator.lock().expect("lock poisoned"); + + let parsed_metrics = aggregator.to_series(); + + assert_eq!(parsed_metrics.len(), 3); + assert_eq!(aggregator.distributions_to_protobuf().sketches.len(), 0); + drop(aggregator); + + assert_value(&locked_aggregator, "metric1", 1.0); + assert_value(&locked_aggregator, "metric2", 2.0); + assert_value(&locked_aggregator, "metric3", 3.0); + } + + #[tokio::test] + async fn test_dogstatsd_single_metric() { + let locked_aggregator = setup_dogstatsd("metric123:99123|c").await; + let aggregator = locked_aggregator.lock().expect("lock poisoned"); + let parsed_metrics = aggregator.to_series(); + + assert_eq!(parsed_metrics.len(), 1); + assert_eq!(aggregator.distributions_to_protobuf().sketches.len(), 0); + drop(aggregator); + + assert_value(&locked_aggregator, "metric123", 99_123.0); + } + + async fn setup_dogstatsd(statsd_string: &str) -> Arc> { + let aggregator_arc = Arc::new(Mutex::new( + Aggregator::new(Vec::new(), 1_024).expect("aggregator creation failed"), + )); + let cancel_token = tokio_util::sync::CancellationToken::new(); + + let dogstatsd = DogStatsD { + cancel_token, + aggregator: Arc::clone(&aggregator_arc), + buffer_reader: BufferReader::MirrorReader( + statsd_string.as_bytes().to_vec(), + SocketAddr::new(IpAddr::V4(Ipv4Addr::new(111, 112, 113, 114)), 0), + ), + }; + dogstatsd.consume_statsd().await; + + aggregator_arc + } +} diff --git a/src/errors.rs b/src/errors.rs new file mode 100644 index 00000000..a97b35a1 --- /dev/null +++ b/src/errors.rs @@ -0,0 +1,33 @@ +// Copyright 2023-Present Datadog, Inc. https://www.datadoghq.com/ +// SPDX-License-Identifier: Apache-2.0 + +//! Error types for `metrics` module + +/// Errors for the function [`crate::metric::Metric::parse`] +#[derive(Debug, thiserror::Error, Clone, Copy, Eq, PartialEq)] +pub enum ParseError { + /// Parse failure given in text + #[error("parse failure: {0}")] + Raw(&'static str), +} + +/// Failure to create a new `Aggregator` +#[derive(Debug, thiserror::Error, Clone, Copy)] +pub enum Creation { + /// The specified context max is too large given our constants. Indicates a + /// serious programming error. + #[error("context max is too large")] + Contexts, +} + +/// Failures from `Aggregator::insert` +#[derive(Debug, thiserror::Error)] +pub enum Insert { + /// The current interval is full and no further metrics can be inserted. The + /// inserted metric is returned. + #[error("interval is full")] + Overflow, + /// Unable to parse passed values + #[error(transparent)] + ValuesIteration(#[from] std::num::ParseFloatError), +} diff --git a/src/flusher.rs b/src/flusher.rs new file mode 100644 index 00000000..7969c7f8 --- /dev/null +++ b/src/flusher.rs @@ -0,0 +1,42 @@ +// Copyright 2023-Present Datadog, Inc. https://www.datadoghq.com/ +// SPDX-License-Identifier: Apache-2.0 + +use crate::aggregator::Aggregator; +use crate::datadog; +use std::sync::{Arc, Mutex}; + +pub struct Flusher { + dd_api: datadog::DdApi, + aggregator: Arc>, +} + +#[inline] +#[must_use] +pub fn build_fqdn_metrics(site: String) -> String { + format!("https://api.{site}") +} + +#[allow(clippy::await_holding_lock)] +impl Flusher { + pub fn new(api_key: String, aggregator: Arc>, site: String) -> Self { + let dd_api = datadog::DdApi::new(api_key, site); + Flusher { dd_api, aggregator } + } + + pub async fn flush(&mut self) { + let (all_series, all_distributions) = { + let mut aggregator = self.aggregator.lock().expect("lock poisoned"); + ( + aggregator.consume_metrics(), + aggregator.consume_distributions(), + ) + }; + for a_batch in all_series { + self.dd_api.ship_series(&a_batch).await; + // TODO(astuyve) retry and do not panic + } + for a_batch in all_distributions { + self.dd_api.ship_distributions(&a_batch).await; + } + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 00000000..fe9467c0 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,10 @@ +// Copyright 2023-Present Datadog, Inc. https://www.datadoghq.com/ +// SPDX-License-Identifier: Apache-2.0 + +pub mod aggregator; +pub mod constants; +pub mod datadog; +pub mod dogstatsd; +pub mod errors; +pub mod flusher; +pub mod metric; diff --git a/src/metric.rs b/src/metric.rs new file mode 100644 index 00000000..38894ef6 --- /dev/null +++ b/src/metric.rs @@ -0,0 +1,393 @@ +// Copyright 2023-Present Datadog, Inc. https://www.datadoghq.com/ +// SPDX-License-Identifier: Apache-2.0 + +use crate::constants; +use crate::errors::ParseError; +use fnv::FnvHasher; +use std::hash::{Hash, Hasher}; +use ustr::Ustr; +#[derive(Debug, Clone, Copy, Eq, PartialEq)] +/// Determine what kind/type of a metric has come in +pub enum Type { + /// Dogstatsd 'count' metric type, monotonically increasing counter + Count, + /// Dogstatsd 'gauge' metric type, point-in-time value + Gauge, + /// Dogstatsd 'distribution' metric type, histogram + Distribution, +} + +/// Representation of a dogstatsd Metric +/// +/// For now this implementation covers only counters and gauges. We hope this is +/// enough to demonstrate the impact of this program's design goals. +#[derive(Clone, Copy, Debug)] +pub struct Metric { + /// Name of the metric. + /// + /// Never more bytes than `constants::MAX_METRIC_NAME_BYTES`, + /// enforced by construction. Note utf8 issues. + pub(crate) name: Ustr, + /// What kind/type of metric this is. + pub(crate) kind: Type, + /// Values of the metric. A singular value may be either a floating point or + /// a integer. Although undocumented we assume 64 bit. A single metric may + /// encode multiple values a time in a message. There must be at least one + /// value here, meaning that there is guaranteed to be a Some value in the + /// 0th index. + /// + /// Parsing of the values to an integer type is deferred until the last + /// moment. + /// + /// Never longer than `constants::MAX_VALUE_BYTES`. Note utf8 issues. + values: Ustr, + /// Tags of the metric. + /// + /// The key is never longer than `constants::MAX_TAG_KEY_BYTES`, the value + /// never more than `constants::MAX_TAG_VALUE_BYTES`. These are enforced by + /// the parser. We assume here that tags are not sent in random order by the + /// clien or that, if they are, the API will tidy that up. That is `a:1,b:2` + /// is a different tagset from `b:2,a:1`. + pub(crate) tags: Option, +} + +impl Metric { + #[must_use] + pub fn new(name: Ustr, kind: Type, values: Ustr, tags: Option) -> Self { + Self { + name, + kind, + values, + tags, + } + } + /// Parse a metric from given input. + /// + /// This function parses a passed `&str` into a `Metric`. We assume that + /// `DogStatsD` metrics must be utf8 and are not ascii or some other encoding. + /// + /// # Errors + /// + /// This function will return with an error if the input violates any of the + /// limits in [`constants`]. Any non-viable input will be discarded. + /// example aj-test.increment:1|c|#user:aj-test from 127.0.0.1:50983 + pub fn parse(input: &str) -> Result { + // TODO must enforce / exploit constraints given in `constants`. + let mut sections = input.split('|'); + + let nv_section = sections + .next() + .ok_or(ParseError::Raw("Missing metric name and value"))?; + + let (name, values) = nv_section + .split_once(':') + .ok_or(ParseError::Raw("Missing name, value section"))?; + + let kind_section = sections + .next() + .ok_or(ParseError::Raw("Missing metric type"))?; + let kind = match kind_section { + "c" => Type::Count, + "g" => Type::Gauge, + "d" => Type::Distribution, + _ => { + return Err(ParseError::Raw("Unsupported metric type")); + } + }; + + let mut tags = None; + for section in sections { + if section.starts_with('@') { + // Sample rate section, skip for now. + continue; + } + if let Some(tags_section) = section.strip_prefix('#') { + let tag_parts = tags_section.split(','); + // Validate that the tags have the right form. + for (i, part) in tag_parts.filter(|s| !s.is_empty()).enumerate() { + if i >= constants::MAX_TAGS { + return Err(ParseError::Raw("Too many tags")); + } + if !part.contains(':') { + return Err(ParseError::Raw("Invalid tag format")); + } + } + tags = Some(tags_section); + break; + } + } + + Ok(Metric { + name: Ustr::from(name), + kind, + values: Ustr::from(values), + tags: tags.map(Ustr::from), + }) + } + /// Return an iterator over values + pub(crate) fn values( + &self, + ) -> impl Iterator> + '_ { + self.values.split(':').map(|b: &str| { + let num = b.parse::()?; + Ok(num) + }) + } + + pub(crate) fn first_value(&self) -> Result { + match self.values().next() { + Some(v) => match v { + Ok(v) => Ok(v), + Err(_e) => Err(ParseError::Raw("Failed to parse value as float")), + }, + None => Err(ParseError::Raw("No value")), + } + } + + #[allow(dead_code)] + pub(crate) fn tags(&self) -> Vec { + self.tags + .unwrap_or_default() + .split(',') + .map(std::string::ToString::to_string) + .collect() + } + + #[cfg(test)] + fn raw_values(&self) -> &str { + self.values.as_str() + } + + #[cfg(test)] + fn raw_name(&self) -> &str { + self.name.as_str() + } + + #[cfg(test)] + fn raw_tagset(&self) -> Option<&str> { + self.tags.map(|t| t.as_str()) + } +} + +/// Create an ID given a name and tagset. +/// +/// This function constructs a hash of the name, the tagset and that hash is +/// identical no matter the internal order of the tagset. That is, we consider a +/// tagset like "a:1,b:2,c:3" to be idential to "b:2,c:3,a:1" to "c:3,a:1,b:2" +/// etc. This implies that we must sort the tagset after parsing it, which we +/// do. Note however that we _do not_ handle duplicate tags, so "a:1,a:1" will +/// hash to a distinct ID than "a:1". +/// +/// Note also that because we take `Ustr` arguments its possible that we've +/// interned many possible combinations of a tagset, even if they are identical +/// from the point of view of this function. +#[inline] +#[must_use] +pub fn id(name: Ustr, tagset: Option) -> u64 { + let mut hasher = FnvHasher::default(); + + name.hash(&mut hasher); + // We sort tags. This is in feature parity with DogStatsD and also means + // that we avoid storing the same context multiple times because users have + // passed tags in differeing order through time. + if let Some(tagset) = tagset { + let mut tag_count = 0; + let mut scratch = [None; constants::MAX_TAGS]; + for kv in tagset.split(',') { + if let Some((k, v)) = kv.split_once(':') { + scratch[tag_count] = Some((Ustr::from(k), Ustr::from(v))); + tag_count += 1; + } + } + scratch[..tag_count].sort_unstable(); + // With the tags sorted -- note they're Copy -- we hash the whole kit. + for kv in scratch[..tag_count].iter().flatten() { + kv.0.as_bytes().hash(&mut hasher); + kv.1.as_bytes().hash(&mut hasher); + } + } + hasher.finish() +} +// :::||@|#:, +// :|T|c: +// +// Types: +// * c -- COUNT, allows packed values, summed +// * g -- GAUGE, allows packed values, last one wins +// +// SAMPLE_RATE ignored for the sake of simplicity. + +#[cfg(test)] +#[allow(clippy::unwrap_used)] +mod tests { + use proptest::{collection, option, strategy::Strategy, string::string_regex}; + use ustr::Ustr; + + use crate::metric::id; + + use super::{Metric, ParseError}; + + fn metric_name() -> impl Strategy { + string_regex("[a-zA-Z0-9.-]{1,128}").unwrap() + } + + fn metric_values() -> impl Strategy { + string_regex("[0-9]+(:[0-9]){0,8}").unwrap() + } + + fn metric_type() -> impl Strategy { + string_regex("g|c").unwrap() + } + + fn metric_tagset() -> impl Strategy> { + option::of( + string_regex("[a-zA-Z]{1,64}:[a-zA-Z]{1,64}(,[a-zA-Z]{1,64}:[a-zA-Z]{1,64}){0,31}") + .unwrap(), + ) + } + + fn metric_tags() -> impl Strategy> { + collection::vec(("[a-z]{1,8}", "[A-Z]{1,8}"), 0..32) + } + + proptest::proptest! { + // For any valid name, tags et al the parse routine is able to parse an + // encoded metric line. + #[test] + fn parse_valid_inputs( + name in metric_name(), + values in metric_values(), + mtype in metric_type(), + tagset in metric_tagset() + ) { + let input = if let Some(ref tagset) = tagset { + format!("{name}:{values}|{mtype}|#{tagset}") + } else { + format!("{name}:{values}|{mtype}") + }; + let metric = Metric::parse(&input).unwrap(); + assert_eq!(name, metric.raw_name()); + assert_eq!(values, metric.raw_values()); + assert_eq!(tagset, metric.raw_tagset().map(String::from)); + } + + #[test] + fn parse_missing_name_and_value( + mtype in metric_type(), + tagset in metric_tagset() + ) { + let input = if let Some(ref tagset) = tagset { + format!("|{mtype}|#{tagset}") + } else { + format!("|{mtype}") + }; + let result = Metric::parse(&input); + + assert!(result.is_err()); + assert_eq!( + result.unwrap_err(), + ParseError::Raw("Missing name, value section") + ); + } + + #[test] + fn parse_invalid_name_and_value_format( + name in metric_name(), + values in metric_values(), + mtype in metric_type(), + tagset in metric_tagset() + ) { + // If there is a ':' in the values we cannot distinguish where the + // name and the first value are. + let value = values.split(':').next().unwrap(); + let input = if let Some(ref tagset) = tagset { + format!("{name}{value}|{mtype}|#{tagset}") + } else { + format!("{name}{value}|{mtype}") + }; + let result = Metric::parse(&input); + + assert!(result.is_err()); + assert_eq!( + result.unwrap_err(), + ParseError::Raw("Missing name, value section") + ); + } + + #[test] + fn parse_unsupported_metric_type( + name in metric_name(), + values in metric_values(), + mtype in "[abefhijklmnopqrstuvwxyz]", + tagset in metric_tagset() + ) { + let input = if let Some(ref tagset) = tagset { + format!("{name}:{values}|{mtype}|#{tagset}") + } else { + format!("{name}:{values}|{mtype}") + }; + let result = Metric::parse(&input); + + assert!(result.is_err()); + assert_eq!( + result.unwrap_err(), + ParseError::Raw("Unsupported metric type") + ); + } + + // The ID of a name, tagset is the same even if the tagset is in a + // distinct order. + // For any valid name, tags et al the parse routine is able to parse an + // encoded metric line. + #[test] + fn id_consistent(name in metric_name(), + mut tags in metric_tags()) { + let mut tagset1 = String::new(); + let mut tagset2 = String::new(); + + for (k,v) in &tags { + tagset1.push_str(k); + tagset1.push(':'); + tagset1.push_str(v); + tagset1.push(','); + } + tags.reverse(); + for (k,v) in &tags { + tagset2.push_str(k); + tagset2.push(':'); + tagset2.push_str(v); + tagset2.push(','); + } + if !tags.is_empty() { + tagset1.pop(); + tagset2.pop(); + } + + let id1 = id(Ustr::from(&name), Some(Ustr::from(&tagset1))); + let id2 = id(Ustr::from(&name), Some(Ustr::from(&tagset2))); + + assert_eq!(id1, id2); + } + } + + #[test] + fn parse_too_many_tags() { + // 33 + assert_eq!(Metric::parse("foo:1|g|#a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3").unwrap_err(), + ParseError::Raw("Too many tags")); + + // 32 + assert!(Metric::parse("foo:1|g|#a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2").is_ok()); + + // 31 + assert!(Metric::parse("foo:1|g|#a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1").is_ok()); + + // 30 + assert!(Metric::parse("foo:1|g|#a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3").is_ok()); + } + + #[test] + fn invalid_dogstatsd_no_panic() { + assert!(Metric::parse("somerandomstring|c+a;slda").is_err()); + } +} diff --git a/tests/integration_test.rs b/tests/integration_test.rs new file mode 100644 index 00000000..689887ca --- /dev/null +++ b/tests/integration_test.rs @@ -0,0 +1,88 @@ +// Copyright 2023-Present Datadog, Inc. https://www.datadoghq.com/ +// SPDX-License-Identifier: Apache-2.0 + +use dogstatsd::{ + aggregator::Aggregator as MetricsAggregator, + constants::CONTEXTS, + dogstatsd::{DogStatsD, DogStatsDConfig}, + flusher::Flusher, +}; +use mockito::Server; +use std::sync::{Arc, Mutex}; +use tokio::{ + net::UdpSocket, + time::{sleep, timeout, Duration}, +}; +use tokio_util::sync::CancellationToken; + +#[cfg(test)] +#[cfg(not(miri))] +#[tokio::test] +async fn dogstatsd_server_ships_series() { + let mut mock_server = Server::new_async().await; + + let mock = mock_server + .mock("POST", "/api/v2/series") + .match_header("DD-API-KEY", "mock-api-key") + .match_header("Content-Type", "application/json") + .with_status(202) + .create_async() + .await; + + let metrics_aggr = Arc::new(Mutex::new( + MetricsAggregator::new(Vec::new(), CONTEXTS).expect("failed to create aggregator"), + )); + + let _ = start_dogstatsd(&metrics_aggr).await; + + let mut metrics_flusher = Flusher::new( + "mock-api-key".to_string(), + Arc::clone(&metrics_aggr), + mock_server.url(), + ); + + let server_address = "127.0.0.1:18125"; + let socket = UdpSocket::bind("0.0.0.0:0") + .await + .expect("unable to bind UDP socket"); + let metric = "custom_metric:1|g"; + + socket + .send_to(metric.as_bytes(), &server_address) + .await + .expect("unable to send metric"); + + let flush = async { + while !mock.matched() { + sleep(Duration::from_millis(100)).await; + metrics_flusher.flush().await; + } + }; + + let result = timeout(Duration::from_millis(1000), flush).await; + + match result { + Ok(_) => mock.assert(), + Err(_) => panic!("timed out before server received metric flush"), + } +} + +async fn start_dogstatsd(metrics_aggr: &Arc>) -> CancellationToken { + let dogstatsd_config = DogStatsDConfig { + host: "127.0.0.1".to_string(), + port: 18125, + }; + let dogstatsd_cancel_token = tokio_util::sync::CancellationToken::new(); + let dogstatsd_client = DogStatsD::new( + &dogstatsd_config, + Arc::clone(metrics_aggr), + dogstatsd_cancel_token.clone(), + ) + .await; + + tokio::spawn(async move { + dogstatsd_client.spin().await; + }); + + dogstatsd_cancel_token +} From 512cb243d0e7fb0a9158620abfb527b0e57b9f52 Mon Sep 17 00:00:00 2001 From: Duncan Harvey <35278470+duncanpharvey@users.noreply.github.com> Date: Thu, 12 Sep 2024 03:40:37 -0400 Subject: [PATCH 02/28] adds readme for dogstatsd (#621) --- README.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 00000000..fa5deb7e --- /dev/null +++ b/README.md @@ -0,0 +1,14 @@ +# DogStatsD + +Provides a DogStatsD implementation which uses [Saluki](https://github.com/DataDog/saluki) for distribution metrics. + +## Status +This project is in beta and possible frequent changes should be expected. It's primary purpose is for Serverless to send metrics from AWS Lambda Functions, Azure Functions, and Azure Spring Apps. It is still considered unstable for general purposes. + +- No UDS support +- Uses `ustr`, so prone to memory leaks +- Arbitrary constraints in https://github.com/DataDog/libdatadog/blob/main/dogstatsd/src/constants.rs + +## Additional Notes + +Upstreamed from [Bottlecap](https://github.com/DataDog/datadog-lambda-extension/tree/main/bottlecap) From 95f1f14233b1f5547d8e0d75bdfb1fd54b901f67 Mon Sep 17 00:00:00 2001 From: AJ Stuyvenberg Date: Mon, 16 Sep 2024 13:40:37 -0400 Subject: [PATCH 03/28] feat: Support DD_HTTP_PROXY and DD_HTTPS_PROXY (#631) * feat: Support DD_HTTP_PROXY and DD_HTTPS_PROXY * fix: fmt * feat: honor dd_proxy --- src/datadog.rs | 35 +++++++++++++++++++++++++++++++++-- src/flusher.rs | 10 ++++++++-- tests/integration_test.rs | 2 ++ 3 files changed, 43 insertions(+), 4 deletions(-) diff --git a/src/datadog.rs b/src/datadog.rs index 3a97c0ff..f7cec11e 100644 --- a/src/datadog.rs +++ b/src/datadog.rs @@ -34,13 +34,44 @@ pub enum ShipError { Json(#[from] serde_json::Error), } +fn build_http_client( + http_proxy: Option, + https_proxy: Option, +) -> Result { + let client = reqwest::Client::builder(); + if let Some(proxy_uri) = http_proxy { + let proxy = reqwest::Proxy::http(proxy_uri)?; + client.proxy(proxy).build() + } else if let Some(proxy_uri) = https_proxy { + let proxy = reqwest::Proxy::https(proxy_uri)?; + client.proxy(proxy).build() + } else { + client.build() + } +} + impl DdApi { #[must_use] - pub fn new(api_key: String, site: String) -> Self { + pub fn new( + api_key: String, + site: String, + http_proxy: Option, + https_proxy: Option, + ) -> Self { + let client = match build_http_client(http_proxy, https_proxy) { + Ok(client) => client, + Err(e) => { + error!( + "Unable to parse proxy configuration: {}, no proxy will be used", + e + ); + reqwest::Client::new() + } + }; DdApi { api_key, fqdn_site: site, - client: reqwest::Client::new(), + client, } } diff --git a/src/flusher.rs b/src/flusher.rs index 7969c7f8..21dbacf2 100644 --- a/src/flusher.rs +++ b/src/flusher.rs @@ -18,8 +18,14 @@ pub fn build_fqdn_metrics(site: String) -> String { #[allow(clippy::await_holding_lock)] impl Flusher { - pub fn new(api_key: String, aggregator: Arc>, site: String) -> Self { - let dd_api = datadog::DdApi::new(api_key, site); + pub fn new( + api_key: String, + aggregator: Arc>, + site: String, + http_proxy: Option, + https_proxy: Option, + ) -> Self { + let dd_api = datadog::DdApi::new(api_key, site, http_proxy, https_proxy); Flusher { dd_api, aggregator } } diff --git a/tests/integration_test.rs b/tests/integration_test.rs index 689887ca..e36eb3bf 100644 --- a/tests/integration_test.rs +++ b/tests/integration_test.rs @@ -39,6 +39,8 @@ async fn dogstatsd_server_ships_series() { "mock-api-key".to_string(), Arc::clone(&metrics_aggr), mock_server.url(), + None, + None, ); let server_address = "127.0.0.1:18125"; From 645b90ca5b39f43b84b27337d6f4a1eb59ff42c1 Mon Sep 17 00:00:00 2001 From: alexgallotta <5581237+alexgallotta@users.noreply.github.com> Date: Mon, 23 Sep 2024 15:20:24 -0400 Subject: [PATCH 04/28] Upstream dogstatsd refactors (#617) * feat: upstream dogstatsd from datadog lambda extension * add: stand alone dogstatsd binary as example * fix: imports * fix: make test utils public * fix: more test utils public * fix: make entry public * fix: clippy * fix: make test config public * Revert "fix: make test config public" This reverts commit 7dc979979d85c665767906030f91253725850f30. * fix: make fields public for tests and assertions * refactor: remove unused error * refactor: do one step parsing * fix: duplicate dependencies from conflicts * refactor: use type for sorted tags and ustr * fix: statsd parsing and tests * format: format and clippy test: fix test format with # for tags. It also changed the byte wise test expected result * fix: use only minimum regexp * fix: increase to 10k context size, lost in refactors/moving * refactor: update license * fix: remove custom toolchain * style: fix comment too long not picked up by rustfmt * fix: restore previous cargo.lock * fix: missing error dep * fix: use empty tags * fix: update license * fix: nightly rustc 1.83 format * fix: clippy * fix: do not use a map, support duplicate tag values --------- Co-authored-by: Taegyun Kim --- Cargo.toml | 1 + src/aggregator.rs | 314 ++++++++++++------------------- src/constants.rs | 2 +- src/datadog.rs | 16 -- src/dogstatsd.rs | 19 +- src/metric.rs | 379 +++++++++++++++++++++----------------- tests/integration_test.rs | 4 +- 7 files changed, 345 insertions(+), 390 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 1540ae9a..30f3b7cc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,6 +22,7 @@ thiserror = { version = "1.0.58", default-features = false } tokio = { version = "1.37.0", default-features = false, features = ["macros", "rt-multi-thread"] } tokio-util = { version = "0.7.11", default-features = false } tracing = { version = "0.1.40", default-features = false } +regex = { version = "1.10.6", default-features = false } [dev-dependencies] mockito = { version = "1.5.0", default-features = false } diff --git a/src/aggregator.rs b/src/aggregator.rs index 506aadd4..380d9301 100644 --- a/src/aggregator.rs +++ b/src/aggregator.rs @@ -6,7 +6,7 @@ use crate::constants; use crate::datadog::{self, Metric as MetricToShip, Series}; use crate::errors; -use crate::metric::{self, Metric as DogstatsdMetric, Type}; +use crate::metric::{self, Metric, MetricValue, SortedTags}; use std::time; use datadog_protos::metrics::{Dogsketch, Sketch, SketchPayload}; @@ -16,29 +16,16 @@ use protobuf::Message; use tracing::{error, warn}; use ustr::Ustr; -#[derive(Debug, Clone)] -pub struct Entry { - id: u64, - name: Ustr, - tags: Option, - pub metric_value: MetricValue, -} - -#[derive(Debug, Clone)] -pub enum MetricValue { - Count(f64), - Gauge(f64), - Distribution(DDSketch), -} - impl MetricValue { - fn insert_metric(&mut self, metric: &DogstatsdMetric) { + fn aggregate(&mut self, metric: Metric) { // safe because we know there's at least one value when we parse match self { - MetricValue::Count(count) => *count += metric.first_value().unwrap_or_default(), - MetricValue::Gauge(gauge) => *gauge = metric.first_value().unwrap_or_default(), + MetricValue::Count(count) => *count += metric.value.get_value().unwrap_or_default(), + MetricValue::Gauge(gauge) => *gauge = metric.value.get_value().unwrap_or_default(), MetricValue::Distribution(distribution) => { - distribution.insert(metric.first_value().unwrap_or_default()); + if let Some(value) = metric.value.get_sketch() { + distribution.merge(value); + } } } } @@ -59,40 +46,12 @@ impl MetricValue { } } -impl Entry { - fn new_from_metric(id: u64, metric: &DogstatsdMetric) -> Self { - let mut metric_value = match metric.kind { - Type::Count => MetricValue::Count(0.0), - Type::Gauge => MetricValue::Gauge(0.0), - Type::Distribution => MetricValue::Distribution(DDSketch::default()), - }; - metric_value.insert_metric(metric); - Self { - id, - name: metric.name, - tags: metric.tags, - metric_value, - } - } - - /// Return an iterator over key, value pairs - fn tag(&self) -> impl Iterator { - self.tags.into_iter().filter_map(|tags| { - let mut split = tags.split(','); - match (split.next(), split.next()) { - (Some(k), Some(v)) => Some((Ustr::from(k), Ustr::from(v))), - _ => None, // Skip tags that lack the proper format - } - }) - } -} - #[derive(Clone)] // NOTE by construction we know that intervals and contexts do not explore the // full space of usize but the type system limits how we can express this today. pub struct Aggregator { - tags: Vec, - map: hash_table::HashTable, + tags: SortedTags, + map: hash_table::HashTable, max_batch_entries_single_metric: usize, max_batch_bytes_single_metric: u64, max_batch_entries_sketch_metric: usize, @@ -109,7 +68,7 @@ impl Aggregator { /// counterparts in `constants`. This would be better as a compile-time /// issue, although leaving this open allows for runtime configuration. #[allow(clippy::cast_precision_loss)] - pub fn new(tags: Vec, max_context: usize) -> Result { + pub fn new(tags: SortedTags, max_context: usize) -> Result { if max_context > constants::MAX_CONTEXTS { return Err(errors::Creation::Contexts); } @@ -130,23 +89,22 @@ impl Aggregator { /// /// Function will return overflow error if more than /// `min(constants::MAX_CONTEXTS, CONTEXTS)` is exceeded. - pub fn insert(&mut self, metric: &DogstatsdMetric) -> Result<(), errors::Insert> { - let id = metric::id(metric.name, metric.tags); + pub fn insert(&mut self, metric: Metric) -> Result<(), errors::Insert> { + let id = metric::id(metric.name, &metric.tags); let len = self.map.len(); match self .map - .entry(id, |m| m.id == id, |m| metric::id(m.name, m.tags)) + .entry(id, |m| m.id == id, |m| metric::id(m.name, &m.tags)) { hash_table::Entry::Vacant(entry) => { if len >= self.max_context { return Err(errors::Insert::Overflow); } - let ent = Entry::new_from_metric(id, metric); - entry.insert(ent); + entry.insert(metric); } hash_table::Entry::Occupied(mut entry) => { - entry.get_mut().metric_value.insert_metric(metric); + entry.get_mut().value.aggregate(metric); } } Ok(()) @@ -168,7 +126,7 @@ impl Aggregator { self.map .iter() - .filter_map(|entry| match entry.metric_value { + .filter_map(|entry| match entry.value { MetricValue::Distribution(_) => build_sketch(now, entry, &self.tags), _ => None, }) @@ -190,7 +148,7 @@ impl Aggregator { for sketch in self .map .extract_if(|entry| { - if let MetricValue::Distribution(_) = entry.metric_value { + if let MetricValue::Distribution(_) = entry.value { return true; } false @@ -227,7 +185,7 @@ impl Aggregator { self.map .iter() - .filter_map(|entry| match entry.metric_value { + .filter_map(|entry| match entry.value { MetricValue::Distribution(_) => None, _ => build_metric(entry, &self.tags), }) @@ -245,7 +203,7 @@ impl Aggregator { for metric in self .map .extract_if(|entry| { - if let MetricValue::Distribution(_) = entry.metric_value { + if let MetricValue::Distribution(_) = entry.value { return false; } true @@ -288,14 +246,14 @@ impl Aggregator { batched_payloads } - pub fn get_entry_by_id(&self, name: Ustr, tags: Option) -> Option<&Entry> { + pub fn get_entry_by_id(&self, name: Ustr, tags: &SortedTags) -> Option<&Metric> { let id = metric::id(name, tags); self.map.find(id, |m| m.id == id) } } -fn build_sketch(now: i64, entry: &Entry, base_tag_vec: &[String]) -> Option { - let sketch = entry.metric_value.get_sketch()?; +fn build_sketch(now: i64, entry: &Metric, base_tag_vec: &SortedTags) -> Option { + let sketch = entry.value.get_sketch()?; let mut dogsketch = Dogsketch::default(); sketch.merge_to_dogsketch(&mut dogsketch); // TODO(Astuyve) allow users to specify timestamp @@ -304,28 +262,21 @@ fn build_sketch(now: i64, entry: &Entry, base_tag_vec: &[String]) -> Option Option { - let mut resources = Vec::with_capacity(constants::MAX_TAGS); - for (name, kind) in entry.tag() { - let resource = datadog::Resource { - name: name.as_str(), - kind: kind.as_str(), - }; - resources.push(resource); - } - let kind = match entry.metric_value { +fn build_metric(entry: &Metric, base_tag_vec: &SortedTags) -> Option { + let resources = entry.tags.to_resources(); + let kind = match entry.value { MetricValue::Count(_) => datadog::DdMetricKind::Count, MetricValue::Gauge(_) => datadog::DdMetricKind::Gauge, MetricValue::Distribution(_) => unreachable!(), }; let point = datadog::Point { - value: entry.metric_value.get_value()?, + value: entry.value.get_value()?, // TODO(astuyve) allow user to specify timestamp timestamp: time::SystemTime::now() .duration_since(time::UNIX_EPOCH) @@ -333,39 +284,24 @@ fn build_metric(entry: &Entry, base_tag_vec: &[String]) -> Option .as_secs(), }; - let mut final_tags = Vec::new(); - // TODO - // These tags are interned so we don't need to clone them here but we're just doing it - // because it's easier than dealing with the lifetimes. - if let Some(tags) = entry.tags { - final_tags = tags.split(',').map(ToString::to_string).collect(); - } - final_tags.extend(base_tag_vec.to_owned()); + let mut tags = entry.tags.clone(); + tags.extend(base_tag_vec); + Some(MetricToShip { metric: entry.name.as_str(), resources, kind, points: [point; 1], - tags: final_tags, + tags: tags.to_strings(), }) } -fn tags_string_to_vector(tags: Option) -> Vec { - if tags.is_none() { - return Vec::new(); - } - tags.unwrap_or_default() - .split(',') - .map(ToString::to_string) - .collect() -} - #[cfg(test)] #[allow(clippy::unwrap_used)] pub mod tests { use crate::aggregator::Aggregator; use crate::metric; - use crate::metric::Metric; + use crate::metric::{parse, SortedTags, EMPTY_TAGS}; use datadog_protos::metrics::SketchPayload; use hashbrown::hash_table; use protobuf::Message; @@ -373,18 +309,22 @@ pub mod tests { const PRECISION: f64 = 0.000_000_01; - const SINGLE_METRIC_SIZE: usize = 187; - const SINGLE_DISTRIBUTION_SIZE: u64 = 135; - const DEFAULT_TAGS: &[&str] = &[ - "dd_extension_version:63-next", - "architecture:x86_64", - "_dd.compute_stats:1", - ]; - - pub fn assert_value(aggregator_mutex: &Mutex, metric_id: &str, value: f64) { + const SINGLE_METRIC_SIZE: usize = 216; // taken from the test, size of a serialized metric with one tag and 1 digit counter value + const SINGLE_DISTRIBUTION_SIZE: u64 = 140; + const DEFAULT_TAGS: &str = + "dd_extension_version:63-next,architecture:x86_64,_dd.compute_stats:1"; + + pub fn assert_value( + aggregator_mutex: &Mutex, + metric_id: &str, + value: f64, + tags: &str, + ) { let aggregator = aggregator_mutex.lock().unwrap(); - if let Some(e) = aggregator.get_entry_by_id(metric_id.into(), None) { - let metric = e.metric_value.get_value().unwrap(); + if let Some(e) = + aggregator.get_entry_by_id(metric_id.into(), &SortedTags::parse(tags).unwrap()) + { + let metric = e.value.get_value().unwrap(); assert!((metric - value).abs() < PRECISION); } else { panic!("{}", format!("{metric_id} not found")); @@ -393,8 +333,8 @@ pub mod tests { pub fn assert_sketch(aggregator_mutex: &Mutex, metric_id: &str, value: f64) { let aggregator = aggregator_mutex.lock().unwrap(); - if let Some(e) = aggregator.get_entry_by_id(metric_id.into(), None) { - let metric = e.metric_value.get_sketch().unwrap(); + if let Some(e) = aggregator.get_entry_by_id(metric_id.into(), &EMPTY_TAGS) { + let metric = e.value.get_sketch().unwrap(); assert!((metric.max().unwrap() - value).abs() < PRECISION); assert!((metric.min().unwrap() - value).abs() < PRECISION); assert!((metric.sum().unwrap() - value).abs() < PRECISION); @@ -406,13 +346,13 @@ pub mod tests { #[test] fn insertion() { - let mut aggregator = Aggregator::new(Vec::new(), 2).unwrap(); + let mut aggregator = Aggregator::new(EMPTY_TAGS, 2).unwrap(); - let metric1 = Metric::parse("test:1|c|k:v").expect("metric parse failed"); - let metric2 = Metric::parse("foo:1|c|k:v").expect("metric parse failed"); + let metric1 = parse("test:1|c|#k:v").expect("metric parse failed"); + let metric2 = parse("foo:1|c|#k:v").expect("metric parse failed"); - assert!(aggregator.insert(&metric1).is_ok()); - assert!(aggregator.insert(&metric2).is_ok()); + assert!(aggregator.insert(metric1).is_ok()); + assert!(aggregator.insert(metric2).is_ok()); // Both unique contexts get one slot. assert_eq!(aggregator.map.len(), 2); @@ -420,13 +360,13 @@ pub mod tests { #[test] fn distribution_insertion() { - let mut aggregator = Aggregator::new(Vec::new(), 2).unwrap(); + let mut aggregator = Aggregator::new(EMPTY_TAGS, 2).unwrap(); - let metric1 = Metric::parse("test:1|d|k:v").expect("metric parse failed"); - let metric2 = Metric::parse("foo:1|d|k:v").expect("metric parse failed"); + let metric1 = parse("test:1|d|#k:v").expect("metric parse failed"); + let metric2 = parse("foo:1|d|#k:v").expect("metric parse failed"); - assert!(aggregator.insert(&metric1).is_ok()); - assert!(aggregator.insert(&metric2).is_ok()); + assert!(aggregator.insert(metric1).is_ok()); + assert!(aggregator.insert(metric2).is_ok()); // Both unique contexts get one slot. assert_eq!(aggregator.map.len(), 2); @@ -434,52 +374,56 @@ pub mod tests { #[test] fn overflow() { - let mut aggregator = Aggregator::new(Vec::new(), 2).unwrap(); + let mut aggregator = Aggregator::new(EMPTY_TAGS, 2).unwrap(); - let metric1 = Metric::parse("test:1|c|k:v").expect("metric parse failed"); - let metric2 = Metric::parse("foo:1|c|k:v").expect("metric parse failed"); - let metric3 = Metric::parse("bar:1|c|k:v").expect("metric parse failed"); + let metric1 = parse("test:1|c|#k:v").expect("metric parse failed"); + let metric2 = parse("foo:1|c|#k:v").expect("metric parse failed"); + let metric3 = parse("bar:1|c|#k:v").expect("metric parse failed"); - let id1 = metric::id(metric1.name, metric1.tags); - let id2 = metric::id(metric2.name, metric2.tags); - let id3 = metric::id(metric3.name, metric3.tags); + let id1 = metric::id(metric1.name, &metric1.tags); + let id2 = metric::id(metric2.name, &metric2.tags); + let id3 = metric::id(metric3.name, &metric3.tags); assert_ne!(id1, id2); assert_ne!(id1, id3); assert_ne!(id2, id3); - assert!(aggregator.insert(&metric1).is_ok()); + assert!(aggregator.insert(metric1).is_ok()); assert_eq!(aggregator.map.len(), 1); - assert!(aggregator.insert(&metric2).is_ok()); - assert!(aggregator.insert(&metric2).is_ok()); - assert!(aggregator.insert(&metric2).is_ok()); + assert!(aggregator.insert(metric2.clone()).is_ok()); + assert!(aggregator.insert(metric2.clone()).is_ok()); + assert!(aggregator.insert(metric2).is_ok()); assert_eq!(aggregator.map.len(), 2); - assert!(aggregator.insert(&metric3).is_err()); + assert!(aggregator.insert(metric3).is_err()); assert_eq!(aggregator.map.len(), 2); } #[test] #[allow(clippy::float_cmp)] fn clear() { - let mut aggregator = Aggregator::new(Vec::new(), 2).unwrap(); + let mut aggregator = Aggregator::new(EMPTY_TAGS, 2).unwrap(); - let metric1 = Metric::parse("test:3|c|k:v").expect("metric parse failed"); - let metric2 = Metric::parse("foo:5|c|k:v").expect("metric parse failed"); + let metric1 = parse("test:3|c|#k1:v1").expect("metric parse failed"); + let metric2 = parse("foo:5|c|#k2:v2").expect("metric parse failed"); - assert!(aggregator.insert(&metric1).is_ok()); - assert!(aggregator.insert(&metric2).is_ok()); + assert!(aggregator.insert(metric1).is_ok()); + assert!(aggregator.insert(metric2).is_ok()); assert_eq!(aggregator.map.len(), 2); - if let Some(v) = aggregator.get_entry_by_id("foo".into(), None) { - assert_eq!(v.metric_value.get_value().unwrap(), 5f64); + if let Some(v) = + aggregator.get_entry_by_id("foo".into(), &SortedTags::parse("k2:v2").unwrap()) + { + assert_eq!(v.value.get_value().unwrap(), 5f64); } else { panic!("failed to get value by id"); } - if let Some(v) = aggregator.get_entry_by_id("test".into(), None) { - assert_eq!(v.metric_value.get_value().unwrap(), 3f64); + if let Some(v) = + aggregator.get_entry_by_id("test".into(), &SortedTags::parse("k1:v1").unwrap()) + { + assert_eq!(v.value.get_value().unwrap(), 3f64); } else { panic!("failed to get value by id"); } @@ -490,14 +434,14 @@ pub mod tests { #[test] fn to_series() { - let mut aggregator = Aggregator::new(Vec::new(), 2).unwrap(); + let mut aggregator = Aggregator::new(EMPTY_TAGS, 2).unwrap(); - let metric1 = Metric::parse("test:1|c|k:v").expect("metric parse failed"); - let metric2 = Metric::parse("foo:1|c|k:v").expect("metric parse failed"); - let metric3 = Metric::parse("bar:1|c|k:v").expect("metric parse failed"); + let metric1 = parse("test:1|c|#k:v").expect("metric parse failed"); + let metric2 = parse("foo:1|c|#k:v").expect("metric parse failed"); + let metric3 = parse("bar:1|c|#k:v").expect("metric parse failed"); - assert!(aggregator.insert(&metric1).is_ok()); - assert!(aggregator.insert(&metric2).is_ok()); + assert!(aggregator.insert(metric1).is_ok()); + assert!(aggregator.insert(metric2).is_ok()); assert_eq!(aggregator.map.len(), 2); assert_eq!(aggregator.to_series().len(), 2); @@ -505,19 +449,19 @@ pub mod tests { assert_eq!(aggregator.to_series().len(), 2); assert_eq!(aggregator.map.len(), 2); - assert!(aggregator.insert(&metric3).is_err()); + assert!(aggregator.insert(metric3).is_err()); assert_eq!(aggregator.to_series().len(), 2); } #[test] fn distributions_to_protobuf() { - let mut aggregator = Aggregator::new(Vec::new(), 2).unwrap(); + let mut aggregator = Aggregator::new(EMPTY_TAGS, 2).unwrap(); - let metric1 = Metric::parse("test:1|d|k:v").expect("metric parse failed"); - let metric2 = Metric::parse("foo:1|d|k:v").expect("metric parse failed"); + let metric1 = parse("test:1|d|#k:v").expect("metric parse failed"); + let metric2 = parse("foo:1|d|#k:v").expect("metric parse failed"); - assert!(aggregator.insert(&metric1).is_ok()); - assert!(aggregator.insert(&metric2).is_ok()); + assert!(aggregator.insert(metric1).is_ok()); + assert!(aggregator.insert(metric2).is_ok()); assert_eq!(aggregator.map.len(), 2); assert_eq!(aggregator.distributions_to_protobuf().sketches().len(), 2); @@ -528,18 +472,16 @@ pub mod tests { #[test] fn consume_distributions_ignore_single_metrics() { - let mut aggregator = Aggregator::new(Vec::new(), 1_000).unwrap(); + let mut aggregator = Aggregator::new(EMPTY_TAGS, 1_000).unwrap(); assert_eq!(aggregator.distributions_to_protobuf().sketches.len(), 0); assert!(aggregator - .insert( - &Metric::parse("test1:1|d|k:v".to_string().as_str()).expect("metric parse failed") - ) + .insert(parse("test1:1|d|#k:v".to_string().as_str()).expect("metric parse failed")) .is_ok()); assert_eq!(aggregator.distributions_to_protobuf().sketches.len(), 1); assert!(aggregator - .insert(&Metric::parse("foo:1|c|k:v").expect("metric parse failed")) + .insert(parse("foo:1|c|#k:v").expect("metric parse failed")) .is_ok()); assert_eq!(aggregator.distributions_to_protobuf().sketches.len(), 1); } @@ -549,7 +491,7 @@ pub mod tests { let max_batch = 5; let tot = 12; let mut aggregator = Aggregator { - tags: Vec::new(), + tags: EMPTY_TAGS, map: hash_table::HashTable::new(), max_batch_entries_single_metric: 1_000, max_batch_bytes_single_metric: 1_000, @@ -572,18 +514,14 @@ pub mod tests { fn consume_distributions_batch_bytes() { let expected_distribution_per_batch = 2; let total_number_of_distributions = 5; - let max_bytes = SINGLE_METRIC_SIZE * expected_distribution_per_batch + 11; + let max_bytes = SINGLE_DISTRIBUTION_SIZE * expected_distribution_per_batch as u64; let mut aggregator = Aggregator { - tags: DEFAULT_TAGS - .to_vec() - .iter() - .map(ToString::to_string) - .collect(), + tags: to_sorted_tags(), map: hash_table::HashTable::new(), max_batch_entries_single_metric: 1_000, max_batch_bytes_single_metric: 1_000, max_batch_entries_sketch_metric: 1_000, - max_batch_bytes_sketch_metric: max_bytes as u64, + max_batch_bytes_sketch_metric: max_bytes, max_context: 1_000, }; @@ -612,16 +550,16 @@ pub mod tests { ); } + fn to_sorted_tags() -> SortedTags { + SortedTags::parse(DEFAULT_TAGS).unwrap() + } + #[test] fn consume_distribution_one_element_bigger_than_max_size() { let max_bytes = 1; let tot = 5; let mut aggregator = Aggregator { - tags: DEFAULT_TAGS - .to_vec() - .iter() - .map(ToString::to_string) - .collect(), + tags: to_sorted_tags(), map: hash_table::HashTable::new(), max_batch_entries_single_metric: 1_000, max_batch_bytes_single_metric: 1_000, @@ -643,7 +581,7 @@ pub mod tests { for i in 1..=tot { assert!(aggregator .insert( - &Metric::parse(format!("test{i}:{i}|{counter_or_distro}|k:v").as_str()) + parse(format!("test{i}:{i}|{counter_or_distro}|#k:v").as_str()) .expect("metric parse failed") ) .is_ok()); @@ -652,26 +590,22 @@ pub mod tests { #[test] fn consume_series_ignore_distribution() { - let mut aggregator = Aggregator::new(Vec::new(), 1_000).unwrap(); + let mut aggregator = Aggregator::new(EMPTY_TAGS, 1_000).unwrap(); assert_eq!(aggregator.consume_metrics().len(), 0); assert!(aggregator - .insert( - &Metric::parse("test1:1|c|k:v".to_string().as_str()).expect("metric parse failed") - ) + .insert(parse("test1:1|c|#k:v".to_string().as_str()).expect("metric parse failed")) .is_ok()); assert_eq!(aggregator.consume_distributions().len(), 0); assert_eq!(aggregator.consume_metrics().len(), 1); assert_eq!(aggregator.consume_metrics().len(), 0); assert!(aggregator - .insert( - &Metric::parse("test1:1|c|k:v".to_string().as_str()).expect("metric parse failed") - ) + .insert(parse("test1:1|c|#k:v".to_string().as_str()).expect("metric parse failed")) .is_ok()); assert!(aggregator - .insert(&Metric::parse("foo:1|d|k:v").expect("metric parse failed")) + .insert(parse("foo:1|d|#k:v").expect("metric parse failed")) .is_ok()); assert_eq!(aggregator.consume_metrics().len(), 1); assert_eq!(aggregator.consume_distributions().len(), 1); @@ -683,7 +617,7 @@ pub mod tests { let max_batch = 5; let tot = 13; let mut aggregator = Aggregator { - tags: Vec::new(), + tags: EMPTY_TAGS, map: hash_table::HashTable::new(), max_batch_entries_single_metric: max_batch, max_batch_bytes_single_metric: 10_000, @@ -707,14 +641,10 @@ pub mod tests { fn consume_metrics_batch_bytes() { let expected_metrics_per_batch = 2; let total_number_of_metrics = 5; - let two_metrics_size = 362; + let two_metrics_size = 420; let max_bytes = SINGLE_METRIC_SIZE * expected_metrics_per_batch + 13; let mut aggregator = Aggregator { - tags: DEFAULT_TAGS - .to_vec() - .iter() - .map(ToString::to_string) - .collect(), + tags: to_sorted_tags(), map: hash_table::HashTable::new(), max_batch_entries_single_metric: 1_000, max_batch_bytes_single_metric: max_bytes as u64, @@ -749,11 +679,7 @@ pub mod tests { let max_bytes = 1; let tot = 5; let mut aggregator = Aggregator { - tags: DEFAULT_TAGS - .to_vec() - .iter() - .map(ToString::to_string) - .collect(), + tags: to_sorted_tags(), map: hash_table::HashTable::new(), max_batch_entries_single_metric: 1_000, max_batch_bytes_single_metric: max_bytes, @@ -776,7 +702,7 @@ pub mod tests { #[test] fn distribution_serialized_deserialized() { - let mut aggregator = Aggregator::new(Vec::new(), 1_000).unwrap(); + let mut aggregator = Aggregator::new(EMPTY_TAGS, 1_000).unwrap(); add_metrics(10, &mut aggregator, "d".to_string()); let distribution = aggregator.distributions_to_protobuf(); diff --git a/src/constants.rs b/src/constants.rs index 70e17d24..c93ad965 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -4,7 +4,7 @@ /// The maximum tags that a `Metric` may hold. pub const MAX_TAGS: usize = 32; -pub const CONTEXTS: usize = 1024; +pub const CONTEXTS: usize = 10_240; pub static MAX_CONTEXTS: usize = 65_536; // 2**16, arbitrary diff --git a/src/datadog.rs b/src/datadog.rs index f7cec11e..810284a9 100644 --- a/src/datadog.rs +++ b/src/datadog.rs @@ -17,22 +17,6 @@ pub struct DdApi { fqdn_site: String, client: reqwest::Client, } -/// Error relating to `ship` -#[derive(thiserror::Error, Debug)] -pub enum ShipError { - #[error("Failed to push to API with status {status}: {body}")] - /// Datadog API failure - Failure { - /// HTTP status code - status: u16, - /// HTTP body that failed - body: String, - }, - - /// Json - #[error(transparent)] - Json(#[from] serde_json::Error), -} fn build_http_client( http_proxy: Option, diff --git a/src/dogstatsd.rs b/src/dogstatsd.rs index 4d1101bb..7173d328 100644 --- a/src/dogstatsd.rs +++ b/src/dogstatsd.rs @@ -8,7 +8,7 @@ use std::sync::{Arc, Mutex}; use tracing::{debug, error}; use crate::aggregator::Aggregator; -use crate::metric::Metric; +use crate::metric::{parse, Metric}; pub struct DogStatsD { cancel_token: tokio_util::sync::CancellationToken, @@ -87,7 +87,7 @@ impl DogStatsD { let all_valid_metrics: Vec = msg .filter(|m| !m.is_empty()) .map(|m| m.replace('\n', "")) - .filter_map(|m| match Metric::parse(m.as_str()) { + .filter_map(|m| match parse(m.as_str()) { Ok(metric) => Some(metric), Err(e) => { error!("Failed to parse metric {}: {}", m, e); @@ -98,7 +98,7 @@ impl DogStatsD { if !all_valid_metrics.is_empty() { let mut guarded_aggregator = self.aggregator.lock().expect("lock poisoned"); for a_valid_value in all_valid_metrics { - let _ = guarded_aggregator.insert(&a_valid_value); + let _ = guarded_aggregator.insert(a_valid_value); } } } @@ -111,6 +111,7 @@ mod tests { use crate::aggregator::tests::assert_value; use crate::aggregator::Aggregator; use crate::dogstatsd::{BufferReader, DogStatsD}; + use crate::metric::EMPTY_TAGS; use std::net::{IpAddr, Ipv4Addr, SocketAddr}; use std::sync::{Arc, Mutex}; @@ -151,7 +152,7 @@ single_machine_performance.rouster.metrics_max_timestamp_latency:1376.90870216|d #[tokio::test] async fn test_dogstatsd_multi_metric() { let locked_aggregator = setup_dogstatsd( - "metric1:1|c\nmetric2:2|c|tag2:val2\nmetric3:3|c||tag3:val3,tag4:val4\n", + "metric3:3|c|#tag3:val3,tag4:val4\nmetric1:1|c\nmetric2:2|c|#tag2:val2\n", ) .await; let aggregator = locked_aggregator.lock().expect("lock poisoned"); @@ -162,9 +163,9 @@ single_machine_performance.rouster.metrics_max_timestamp_latency:1376.90870216|d assert_eq!(aggregator.distributions_to_protobuf().sketches.len(), 0); drop(aggregator); - assert_value(&locked_aggregator, "metric1", 1.0); - assert_value(&locked_aggregator, "metric2", 2.0); - assert_value(&locked_aggregator, "metric3", 3.0); + assert_value(&locked_aggregator, "metric1", 1.0, ""); + assert_value(&locked_aggregator, "metric2", 2.0, "tag2:val2"); + assert_value(&locked_aggregator, "metric3", 3.0, "tag3:val3,tag4:val4"); } #[tokio::test] @@ -177,12 +178,12 @@ single_machine_performance.rouster.metrics_max_timestamp_latency:1376.90870216|d assert_eq!(aggregator.distributions_to_protobuf().sketches.len(), 0); drop(aggregator); - assert_value(&locked_aggregator, "metric123", 99_123.0); + assert_value(&locked_aggregator, "metric123", 99_123.0, ""); } async fn setup_dogstatsd(statsd_string: &str) -> Arc> { let aggregator_arc = Arc::new(Mutex::new( - Aggregator::new(Vec::new(), 1_024).expect("aggregator creation failed"), + Aggregator::new(EMPTY_TAGS, 1_024).expect("aggregator creation failed"), )); let cancel_token = tokio_util::sync::CancellationToken::new(); diff --git a/src/metric.rs b/src/metric.rs index 38894ef6..4ce7ac5a 100644 --- a/src/metric.rs +++ b/src/metric.rs @@ -1,35 +1,108 @@ // Copyright 2023-Present Datadog, Inc. https://www.datadoghq.com/ // SPDX-License-Identifier: Apache-2.0 -use crate::constants; use crate::errors::ParseError; +use crate::{constants, datadog}; +use ddsketch_agent::DDSketch; use fnv::FnvHasher; +use protobuf::Chars; +use regex::Regex; use std::hash::{Hash, Hasher}; use ustr::Ustr; -#[derive(Debug, Clone, Copy, Eq, PartialEq)] -/// Determine what kind/type of a metric has come in -pub enum Type { + +pub const EMPTY_TAGS: SortedTags = SortedTags { values: Vec::new() }; + +#[derive(Clone, Debug)] +pub enum MetricValue { /// Dogstatsd 'count' metric type, monotonically increasing counter - Count, + Count(f64), /// Dogstatsd 'gauge' metric type, point-in-time value - Gauge, + Gauge(f64), /// Dogstatsd 'distribution' metric type, histogram - Distribution, + Distribution(DDSketch), +} + +#[derive(Clone, Debug)] +pub struct SortedTags { + // We sort tags. This is in feature parity with DogStatsD and also means + // that we avoid storing the same context multiple times because users have + // passed tags in different order through time. + values: Vec<(Ustr, Ustr)>, +} + +impl SortedTags { + pub fn extend(&mut self, other: &SortedTags) { + self.values.extend_from_slice(&other.values); + self.values.dedup(); + self.values.sort_unstable(); + } + + pub fn is_empty(&self) -> bool { + self.values.is_empty() + } + + pub fn parse(tags_section: &str) -> Result { + let tag_parts = tags_section.split(','); + let mut parsed_tags = Vec::new(); + // Validate that the tags have the right form. + for (i, part) in tag_parts.filter(|s| !s.is_empty()).enumerate() { + if i >= constants::MAX_TAGS { + return Err(ParseError::Raw("Too many tags")); + } + if !part.contains(':') { + return Err(ParseError::Raw("Invalid tag format")); + } + if let Some((k, v)) = part.split_once(':') { + parsed_tags.push((Ustr::from(k), Ustr::from(v))); + } + } + parsed_tags.dedup(); + parsed_tags.sort_unstable(); + Ok(SortedTags { + values: parsed_tags, + }) + } + + pub fn to_chars(&self) -> Vec { + let mut tags_as_chars = Vec::new(); + for (k, v) in &self.values { + tags_as_chars.push(format!("{}:{}", k, v).into()); + } + tags_as_chars + } + + pub fn to_strings(&self) -> Vec { + let mut tags_as_vec = Vec::new(); + for (k, v) in &self.values { + tags_as_vec.push(format!("{}:{}", k, v)); + } + tags_as_vec + } + + pub(crate) fn to_resources(&self) -> Vec { + let mut resources = Vec::with_capacity(constants::MAX_TAGS); + for (name, kind) in &self.values { + let resource = datadog::Resource { + name: name.as_str(), + kind: kind.as_str(), + }; + resources.push(resource); + } + resources + } } /// Representation of a dogstatsd Metric /// /// For now this implementation covers only counters and gauges. We hope this is /// enough to demonstrate the impact of this program's design goals. -#[derive(Clone, Copy, Debug)] +#[derive(Clone, Debug)] pub struct Metric { /// Name of the metric. /// /// Never more bytes than `constants::MAX_METRIC_NAME_BYTES`, /// enforced by construction. Note utf8 issues. - pub(crate) name: Ustr, - /// What kind/type of metric this is. - pub(crate) kind: Type, + pub name: Ustr, /// Values of the metric. A singular value may be either a floating point or /// a integer. Although undocumented we assume 64 bit. A single metric may /// encode multiple values a time in a message. There must be at least one @@ -40,7 +113,7 @@ pub struct Metric { /// moment. /// /// Never longer than `constants::MAX_VALUE_BYTES`. Note utf8 issues. - values: Ustr, + pub value: MetricValue, /// Tags of the metric. /// /// The key is never longer than `constants::MAX_TAG_KEY_BYTES`, the value @@ -48,124 +121,70 @@ pub struct Metric { /// the parser. We assume here that tags are not sent in random order by the /// clien or that, if they are, the API will tidy that up. That is `a:1,b:2` /// is a different tagset from `b:2,a:1`. - pub(crate) tags: Option, -} + pub tags: SortedTags, -impl Metric { - #[must_use] - pub fn new(name: Ustr, kind: Type, values: Ustr, tags: Option) -> Self { - Self { - name, - kind, - values, - tags, - } - } - /// Parse a metric from given input. - /// - /// This function parses a passed `&str` into a `Metric`. We assume that - /// `DogStatsD` metrics must be utf8 and are not ascii or some other encoding. - /// - /// # Errors - /// - /// This function will return with an error if the input violates any of the - /// limits in [`constants`]. Any non-viable input will be discarded. - /// example aj-test.increment:1|c|#user:aj-test from 127.0.0.1:50983 - pub fn parse(input: &str) -> Result { - // TODO must enforce / exploit constraints given in `constants`. - let mut sections = input.split('|'); - - let nv_section = sections - .next() - .ok_or(ParseError::Raw("Missing metric name and value"))?; - - let (name, values) = nv_section - .split_once(':') - .ok_or(ParseError::Raw("Missing name, value section"))?; - - let kind_section = sections - .next() - .ok_or(ParseError::Raw("Missing metric type"))?; - let kind = match kind_section { - "c" => Type::Count, - "g" => Type::Gauge, - "d" => Type::Distribution, - _ => { - return Err(ParseError::Raw("Unsupported metric type")); - } - }; + /// ID given a name and tagset. + pub(crate) id: u64, +} - let mut tags = None; - for section in sections { - if section.starts_with('@') { - // Sample rate section, skip for now. - continue; +/// Parse a metric from given input. +/// +/// This function parses a passed `&str` into a `Metric`. We assume that +/// `DogStatsD` metrics must be utf8 and are not ascii or some other encoding. +/// +/// # Errors +/// +/// This function will return with an error if the input violates any of the +/// limits in [`constants`]. Any non-viable input will be discarded. +/// example aj-test.increment:1|c|#user:aj-test from 127.0.0.1:50983 +pub fn parse(input: &str) -> Result { + // TODO must enforce / exploit constraints given in `constants`. + if let Ok(re) = Regex::new( + r"^(?P[^:]+):(?P[^|]+)\|(?P[cgd])(?:\|@(?P[\d.]+))?(?:\|#(?P[^|]+))?$", + ) { + if let Some(caps) = re.captures(input) { + // unused for now + // let sample_rate = caps.name("sample_rate").map(|m| m.as_str()); + + let tags; + if let Some(tags_section) = caps.name("tags") { + tags = SortedTags::parse(tags_section.as_str())?; + } else { + tags = EMPTY_TAGS; } - if let Some(tags_section) = section.strip_prefix('#') { - let tag_parts = tags_section.split(','); - // Validate that the tags have the right form. - for (i, part) in tag_parts.filter(|s| !s.is_empty()).enumerate() { - if i >= constants::MAX_TAGS { - return Err(ParseError::Raw("Too many tags")); - } - if !part.contains(':') { - return Err(ParseError::Raw("Invalid tag format")); - } + let val = first_value(caps.name("values").unwrap().as_str())?; + let metric_value = match caps.name("type").unwrap().as_str() { + "c" => MetricValue::Count(val), + "g" => MetricValue::Gauge(val), + "d" => { + let sketch = &mut DDSketch::default(); + sketch.insert(val); + MetricValue::Distribution(sketch.to_owned()) } - tags = Some(tags_section); - break; - } - } - - Ok(Metric { - name: Ustr::from(name), - kind, - values: Ustr::from(values), - tags: tags.map(Ustr::from), - }) - } - /// Return an iterator over values - pub(crate) fn values( - &self, - ) -> impl Iterator> + '_ { - self.values.split(':').map(|b: &str| { - let num = b.parse::()?; - Ok(num) - }) - } - - pub(crate) fn first_value(&self) -> Result { - match self.values().next() { - Some(v) => match v { - Ok(v) => Ok(v), - Err(_e) => Err(ParseError::Raw("Failed to parse value as float")), - }, - None => Err(ParseError::Raw("No value")), + _ => { + return Err(ParseError::Raw("Unsupported metric type")); + } + }; + let name = Ustr::from(caps.name("name").unwrap().as_str()); + let id = id(name, &tags); + return Ok(Metric { + name, + value: metric_value, + tags, + id, + }); } } + Err(ParseError::Raw("Invalid metric format")) +} - #[allow(dead_code)] - pub(crate) fn tags(&self) -> Vec { - self.tags - .unwrap_or_default() - .split(',') - .map(std::string::ToString::to_string) - .collect() - } - - #[cfg(test)] - fn raw_values(&self) -> &str { - self.values.as_str() - } - - #[cfg(test)] - fn raw_name(&self) -> &str { - self.name.as_str() - } - - #[cfg(test)] - fn raw_tagset(&self) -> Option<&str> { - self.tags.map(|t| t.as_str()) +fn first_value(values: &str) -> Result { + match values.split(':').next() { + Some(v) => match v.parse::() { + Ok(v) => Ok(v), + Err(_) => Err(ParseError::Raw("Invalid value")), + }, + None => Err(ParseError::Raw("Missing value")), } } @@ -175,36 +194,21 @@ impl Metric { /// identical no matter the internal order of the tagset. That is, we consider a /// tagset like "a:1,b:2,c:3" to be idential to "b:2,c:3,a:1" to "c:3,a:1,b:2" /// etc. This implies that we must sort the tagset after parsing it, which we -/// do. Note however that we _do not_ handle duplicate tags, so "a:1,a:1" will -/// hash to a distinct ID than "a:1". +/// do. Duplicate tags are removed, so "a:1,a:1" will +/// hash to the same ID of "a:1". /// /// Note also that because we take `Ustr` arguments its possible that we've /// interned many possible combinations of a tagset, even if they are identical /// from the point of view of this function. #[inline] #[must_use] -pub fn id(name: Ustr, tagset: Option) -> u64 { +pub fn id(name: Ustr, tags: &SortedTags) -> u64 { let mut hasher = FnvHasher::default(); name.hash(&mut hasher); - // We sort tags. This is in feature parity with DogStatsD and also means - // that we avoid storing the same context multiple times because users have - // passed tags in differeing order through time. - if let Some(tagset) = tagset { - let mut tag_count = 0; - let mut scratch = [None; constants::MAX_TAGS]; - for kv in tagset.split(',') { - if let Some((k, v)) = kv.split_once(':') { - scratch[tag_count] = Some((Ustr::from(k), Ustr::from(v))); - tag_count += 1; - } - } - scratch[..tag_count].sort_unstable(); - // With the tags sorted -- note they're Copy -- we hash the whole kit. - for kv in scratch[..tag_count].iter().flatten() { - kv.0.as_bytes().hash(&mut hasher); - kv.1.as_bytes().hash(&mut hasher); - } + for kv in tags.values.iter() { + kv.0.as_bytes().hash(&mut hasher); + kv.1.as_bytes().hash(&mut hasher); } hasher.finish() } @@ -223,9 +227,9 @@ mod tests { use proptest::{collection, option, strategy::Strategy, string::string_regex}; use ustr::Ustr; - use crate::metric::id; + use crate::metric::{id, parse, MetricValue, SortedTags}; - use super::{Metric, ParseError}; + use super::ParseError; fn metric_name() -> impl Strategy { string_regex("[a-zA-Z0-9.-]{1,128}").unwrap() @@ -265,10 +269,53 @@ mod tests { } else { format!("{name}:{values}|{mtype}") }; - let metric = Metric::parse(&input).unwrap(); - assert_eq!(name, metric.raw_name()); - assert_eq!(values, metric.raw_values()); - assert_eq!(tagset, metric.raw_tagset().map(String::from)); + let metric = parse(&input).unwrap(); + assert_eq!(name, metric.name.as_str()); + + if let Some(tags) = tagset { + let parsed_metric_tags : SortedTags= metric.tags.clone(); + assert_eq!(tags.split(',').count(), parsed_metric_tags.values.len()); + tags.split(',').for_each(|kv| { + let (original_key, original_value) = kv.split_once(':').unwrap(); + let mut found = false; + for (k,v) in parsed_metric_tags.values.iter() { + // TODO not sure who to handle duplicate keys. To make the test pass, just find any match instead of first + if *k == Ustr::from(original_key) && *v == Ustr::from(original_value) { + found = true; + } + } + assert!(found); + }); + } else { + assert!(metric.tags.is_empty()); + } + + match mtype.as_str() { + "c" => { + if let MetricValue::Count(v) = metric.value { + assert_eq!(v, values.split(':').next().unwrap().parse::().unwrap()); + } else { + panic!("Expected count metric"); + } + } + "g" => { + if let MetricValue::Gauge(v) = metric.value { + assert_eq!(v, values.split(':').next().unwrap().parse::().unwrap()); + } else { + panic!("Expected gauge metric"); + } + } + "d" => { + if let MetricValue::Distribution(d) = metric.value { + assert_eq!(d.min().unwrap(), values.split(':').next().unwrap().parse::().unwrap()); + } else { + panic!("Expected distribution metric"); + } + } + _ => { + panic!("Invalid metric format"); + } + } } #[test] @@ -281,13 +328,9 @@ mod tests { } else { format!("|{mtype}") }; - let result = Metric::parse(&input); + let result = parse(&input); - assert!(result.is_err()); - assert_eq!( - result.unwrap_err(), - ParseError::Raw("Missing name, value section") - ); + assert_eq!(result.unwrap_err(),ParseError::Raw("Invalid metric format")); } #[test] @@ -305,12 +348,11 @@ mod tests { } else { format!("{name}{value}|{mtype}") }; - let result = Metric::parse(&input); + let result = parse(&input); - assert!(result.is_err()); assert_eq!( result.unwrap_err(), - ParseError::Raw("Missing name, value section") + ParseError::Raw("Invalid metric format") ); } @@ -326,12 +368,11 @@ mod tests { } else { format!("{name}:{values}|{mtype}") }; - let result = Metric::parse(&input); + let result = parse(&input); - assert!(result.is_err()); assert_eq!( result.unwrap_err(), - ParseError::Raw("Unsupported metric type") + ParseError::Raw("Invalid metric format") ); } @@ -363,8 +404,8 @@ mod tests { tagset2.pop(); } - let id1 = id(Ustr::from(&name), Some(Ustr::from(&tagset1))); - let id2 = id(Ustr::from(&name), Some(Ustr::from(&tagset2))); + let id1 = id(Ustr::from(&name), &SortedTags::parse(&tagset1).unwrap()); + let id2 = id(Ustr::from(&name), &SortedTags::parse(&tagset2).unwrap()); assert_eq!(id1, id2); } @@ -373,21 +414,21 @@ mod tests { #[test] fn parse_too_many_tags() { // 33 - assert_eq!(Metric::parse("foo:1|g|#a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3").unwrap_err(), + assert_eq!(parse("foo:1|g|#a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3").unwrap_err(), ParseError::Raw("Too many tags")); // 32 - assert!(Metric::parse("foo:1|g|#a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2").is_ok()); + assert!(parse("foo:1|g|#a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2").is_ok()); // 31 - assert!(Metric::parse("foo:1|g|#a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1").is_ok()); + assert!(parse("foo:1|g|#a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1").is_ok()); // 30 - assert!(Metric::parse("foo:1|g|#a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3").is_ok()); + assert!(parse("foo:1|g|#a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3").is_ok()); } #[test] fn invalid_dogstatsd_no_panic() { - assert!(Metric::parse("somerandomstring|c+a;slda").is_err()); + assert!(parse("somerandomstring|c+a;slda").is_err()); } } diff --git a/tests/integration_test.rs b/tests/integration_test.rs index e36eb3bf..bb01466f 100644 --- a/tests/integration_test.rs +++ b/tests/integration_test.rs @@ -1,6 +1,7 @@ // Copyright 2023-Present Datadog, Inc. https://www.datadoghq.com/ // SPDX-License-Identifier: Apache-2.0 +use dogstatsd::metric::SortedTags; use dogstatsd::{ aggregator::Aggregator as MetricsAggregator, constants::CONTEXTS, @@ -30,7 +31,8 @@ async fn dogstatsd_server_ships_series() { .await; let metrics_aggr = Arc::new(Mutex::new( - MetricsAggregator::new(Vec::new(), CONTEXTS).expect("failed to create aggregator"), + MetricsAggregator::new(SortedTags::parse("sometkey:somevalue").unwrap(), CONTEXTS) + .expect("failed to create aggregator"), )); let _ = start_dogstatsd(&metrics_aggr).await; From 342c0549fe1e3d2e7bdd4dfd4373cf6a5fc9704e Mon Sep 17 00:00:00 2001 From: AJ Stuyvenberg Date: Wed, 2 Oct 2024 15:53:42 -0400 Subject: [PATCH 05/28] Support http-proxy for trace agent, remove proxy from dsd (#658) * feat: support HTTPS_PROXY for traces * fix: https_proxy only. * fix: maybe native-tls works in lambda? * feat: try rust-tls * fix: rusttls oops * fix: reee it's rustls why * fix: default-features must be false * WIP: debug log, will revert * fix: reqwest honors system proxies, hyper doesn't seem to. Only proxy https traffic * fix: revert hyper-proxy, just use system proxy * fix: revert proxy, use system * fix: revert hyper-proxy, use system proxy * fix: revert senddata proxy change from tests * Revert "fix: revert senddata proxy change from tests" This reverts commit 105000853f86dd46c39914434907452d9ca05b60. * Revert "fix: revert hyper-proxy, use system proxy" This reverts commit d9ebdc7e3cd68f046a4e8d981ac0564b34458983. * Revert "fix: revert proxy, use system" This reverts commit e4a8e18c14b56f8b889aec23b40cf3ccaf511639. * Revert "fix: revert hyper-proxy, just use system proxy" This reverts commit f8ed3005f75bdb3a4a4fb35dc52f499206b8d2b9. * fix: re-commit tests * fix: fmt * feat: license * feat: Wrap proxy in feature flag * fix: fmt * fix: not sure why the arg is needed in create_send_data * fix: no api changes for public interfaces * fix: None * fix: allow unused * fix: None for update_send_results_example * fix: remove unused error import --- src/datadog.rs | 37 +++---------------------------------- src/flusher.rs | 10 ++-------- tests/integration_test.rs | 2 -- 3 files changed, 5 insertions(+), 44 deletions(-) diff --git a/src/datadog.rs b/src/datadog.rs index 810284a9..b9588297 100644 --- a/src/datadog.rs +++ b/src/datadog.rs @@ -8,7 +8,7 @@ use protobuf::Message; use reqwest; use serde::{Serialize, Serializer}; use serde_json; -use tracing::{debug, error}; +use tracing::debug; /// Interface for the `DogStatsD` metrics intake API. #[derive(Debug)] @@ -18,44 +18,13 @@ pub struct DdApi { client: reqwest::Client, } -fn build_http_client( - http_proxy: Option, - https_proxy: Option, -) -> Result { - let client = reqwest::Client::builder(); - if let Some(proxy_uri) = http_proxy { - let proxy = reqwest::Proxy::http(proxy_uri)?; - client.proxy(proxy).build() - } else if let Some(proxy_uri) = https_proxy { - let proxy = reqwest::Proxy::https(proxy_uri)?; - client.proxy(proxy).build() - } else { - client.build() - } -} - impl DdApi { #[must_use] - pub fn new( - api_key: String, - site: String, - http_proxy: Option, - https_proxy: Option, - ) -> Self { - let client = match build_http_client(http_proxy, https_proxy) { - Ok(client) => client, - Err(e) => { - error!( - "Unable to parse proxy configuration: {}, no proxy will be used", - e - ); - reqwest::Client::new() - } - }; + pub fn new(api_key: String, site: String) -> Self { DdApi { api_key, fqdn_site: site, - client, + client: reqwest::Client::new(), } } diff --git a/src/flusher.rs b/src/flusher.rs index 21dbacf2..7969c7f8 100644 --- a/src/flusher.rs +++ b/src/flusher.rs @@ -18,14 +18,8 @@ pub fn build_fqdn_metrics(site: String) -> String { #[allow(clippy::await_holding_lock)] impl Flusher { - pub fn new( - api_key: String, - aggregator: Arc>, - site: String, - http_proxy: Option, - https_proxy: Option, - ) -> Self { - let dd_api = datadog::DdApi::new(api_key, site, http_proxy, https_proxy); + pub fn new(api_key: String, aggregator: Arc>, site: String) -> Self { + let dd_api = datadog::DdApi::new(api_key, site); Flusher { dd_api, aggregator } } diff --git a/tests/integration_test.rs b/tests/integration_test.rs index bb01466f..1acb6543 100644 --- a/tests/integration_test.rs +++ b/tests/integration_test.rs @@ -41,8 +41,6 @@ async fn dogstatsd_server_ships_series() { "mock-api-key".to_string(), Arc::clone(&metrics_aggr), mock_server.url(), - None, - None, ); let server_address = "127.0.0.1:18125"; From 66277a3e4361bf183546065f3df689d1294d620d Mon Sep 17 00:00:00 2001 From: AJ Stuyvenberg Date: Wed, 2 Oct 2024 20:39:08 -0400 Subject: [PATCH 06/28] test: ignore dogstatsd tests under miri (#661) * feat: Map over sketches attr instead of assigning in map, which Levi thinks avoids a memcpy * fix: lint --- src/aggregator.rs | 20 ++++++++++++++++++-- src/dogstatsd.rs | 3 +++ src/metric.rs | 7 +++++++ 3 files changed, 28 insertions(+), 2 deletions(-) diff --git a/src/aggregator.rs b/src/aggregator.rs index 380d9301..dc6be010 100644 --- a/src/aggregator.rs +++ b/src/aggregator.rs @@ -124,13 +124,14 @@ impl Aggregator { .unwrap_or_default(); let mut sketch_payload = SketchPayload::new(); - self.map + sketch_payload.sketches = self + .map .iter() .filter_map(|entry| match entry.value { MetricValue::Distribution(_) => build_sketch(now, entry, &self.tags), _ => None, }) - .for_each(|sketch| sketch_payload.sketches.push(sketch)); + .collect(); sketch_payload } @@ -345,6 +346,7 @@ pub mod tests { } #[test] + #[cfg_attr(miri, ignore)] fn insertion() { let mut aggregator = Aggregator::new(EMPTY_TAGS, 2).unwrap(); @@ -359,6 +361,7 @@ pub mod tests { } #[test] + #[cfg_attr(miri, ignore)] fn distribution_insertion() { let mut aggregator = Aggregator::new(EMPTY_TAGS, 2).unwrap(); @@ -373,6 +376,7 @@ pub mod tests { } #[test] + #[cfg_attr(miri, ignore)] fn overflow() { let mut aggregator = Aggregator::new(EMPTY_TAGS, 2).unwrap(); @@ -402,6 +406,7 @@ pub mod tests { #[test] #[allow(clippy::float_cmp)] + #[cfg_attr(miri, ignore)] fn clear() { let mut aggregator = Aggregator::new(EMPTY_TAGS, 2).unwrap(); @@ -433,6 +438,7 @@ pub mod tests { } #[test] + #[cfg_attr(miri, ignore)] fn to_series() { let mut aggregator = Aggregator::new(EMPTY_TAGS, 2).unwrap(); @@ -454,6 +460,7 @@ pub mod tests { } #[test] + #[cfg_attr(miri, ignore)] fn distributions_to_protobuf() { let mut aggregator = Aggregator::new(EMPTY_TAGS, 2).unwrap(); @@ -471,6 +478,7 @@ pub mod tests { } #[test] + #[cfg_attr(miri, ignore)] fn consume_distributions_ignore_single_metrics() { let mut aggregator = Aggregator::new(EMPTY_TAGS, 1_000).unwrap(); assert_eq!(aggregator.distributions_to_protobuf().sketches.len(), 0); @@ -487,6 +495,7 @@ pub mod tests { } #[test] + #[cfg_attr(miri, ignore)] fn consume_distributions_batch_entries() { let max_batch = 5; let tot = 12; @@ -511,6 +520,7 @@ pub mod tests { } #[test] + #[cfg_attr(miri, ignore)] fn consume_distributions_batch_bytes() { let expected_distribution_per_batch = 2; let total_number_of_distributions = 5; @@ -555,6 +565,7 @@ pub mod tests { } #[test] + #[cfg_attr(miri, ignore)] fn consume_distribution_one_element_bigger_than_max_size() { let max_bytes = 1; let tot = 5; @@ -589,6 +600,7 @@ pub mod tests { } #[test] + #[cfg_attr(miri, ignore)] fn consume_series_ignore_distribution() { let mut aggregator = Aggregator::new(EMPTY_TAGS, 1_000).unwrap(); @@ -613,6 +625,7 @@ pub mod tests { } #[test] + #[cfg_attr(miri, ignore)] fn consume_series_batch_entries() { let max_batch = 5; let tot = 13; @@ -638,6 +651,7 @@ pub mod tests { } #[test] + #[cfg_attr(miri, ignore)] fn consume_metrics_batch_bytes() { let expected_metrics_per_batch = 2; let total_number_of_metrics = 5; @@ -675,6 +689,7 @@ pub mod tests { } #[test] + #[cfg_attr(miri, ignore)] fn consume_series_one_element_bigger_than_max_size() { let max_bytes = 1; let tot = 5; @@ -701,6 +716,7 @@ pub mod tests { } #[test] + #[cfg_attr(miri, ignore)] fn distribution_serialized_deserialized() { let mut aggregator = Aggregator::new(EMPTY_TAGS, 1_000).unwrap(); diff --git a/src/dogstatsd.rs b/src/dogstatsd.rs index 7173d328..0b28f4c6 100644 --- a/src/dogstatsd.rs +++ b/src/dogstatsd.rs @@ -116,6 +116,7 @@ mod tests { use std::sync::{Arc, Mutex}; #[tokio::test] + #[cfg_attr(miri, ignore)] async fn test_dogstatsd_multi_distribution() { let locked_aggregator = setup_dogstatsd( "single_machine_performance.rouster.api.series_v2.payload_size_bytes:269942|d @@ -150,6 +151,7 @@ single_machine_performance.rouster.metrics_max_timestamp_latency:1376.90870216|d } #[tokio::test] + #[cfg_attr(miri, ignore)] async fn test_dogstatsd_multi_metric() { let locked_aggregator = setup_dogstatsd( "metric3:3|c|#tag3:val3,tag4:val4\nmetric1:1|c\nmetric2:2|c|#tag2:val2\n", @@ -169,6 +171,7 @@ single_machine_performance.rouster.metrics_max_timestamp_latency:1376.90870216|d } #[tokio::test] + #[cfg_attr(miri, ignore)] async fn test_dogstatsd_single_metric() { let locked_aggregator = setup_dogstatsd("metric123:99123|c").await; let aggregator = locked_aggregator.lock().expect("lock poisoned"); diff --git a/src/metric.rs b/src/metric.rs index 4ce7ac5a..e0fcd122 100644 --- a/src/metric.rs +++ b/src/metric.rs @@ -258,6 +258,7 @@ mod tests { // For any valid name, tags et al the parse routine is able to parse an // encoded metric line. #[test] + #[cfg_attr(miri, ignore)] fn parse_valid_inputs( name in metric_name(), values in metric_values(), @@ -319,6 +320,7 @@ mod tests { } #[test] + #[cfg_attr(miri, ignore)] fn parse_missing_name_and_value( mtype in metric_type(), tagset in metric_tagset() @@ -334,6 +336,7 @@ mod tests { } #[test] + #[cfg_attr(miri, ignore)] fn parse_invalid_name_and_value_format( name in metric_name(), values in metric_values(), @@ -357,6 +360,7 @@ mod tests { } #[test] + #[cfg_attr(miri, ignore)] fn parse_unsupported_metric_type( name in metric_name(), values in metric_values(), @@ -381,6 +385,7 @@ mod tests { // For any valid name, tags et al the parse routine is able to parse an // encoded metric line. #[test] + #[cfg_attr(miri, ignore)] fn id_consistent(name in metric_name(), mut tags in metric_tags()) { let mut tagset1 = String::new(); @@ -412,6 +417,7 @@ mod tests { } #[test] + #[cfg_attr(miri, ignore)] fn parse_too_many_tags() { // 33 assert_eq!(parse("foo:1|g|#a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3").unwrap_err(), @@ -428,6 +434,7 @@ mod tests { } #[test] + #[cfg_attr(miri, ignore)] fn invalid_dogstatsd_no_panic() { assert!(parse("somerandomstring|c+a;slda").is_err()); } From b56ca77bcc9353f4edb7cbe559d0a9507ffc9842 Mon Sep 17 00:00:00 2001 From: alexgallotta <5581237+alexgallotta@users.noreply.github.com> Date: Thu, 3 Oct 2024 10:33:05 -0400 Subject: [PATCH 07/28] Fix dogstatsd aggregator (#654) * fix: id public and use optional for tags * change: utility to easily create values --- src/aggregator.rs | 44 ++++++++++++++++++++++---------------- src/metric.rs | 54 ++++++++++++++++++++++++++++++++++++----------- 2 files changed, 68 insertions(+), 30 deletions(-) diff --git a/src/aggregator.rs b/src/aggregator.rs index dc6be010..b8edfaaa 100644 --- a/src/aggregator.rs +++ b/src/aggregator.rs @@ -19,6 +19,7 @@ use ustr::Ustr; impl MetricValue { fn aggregate(&mut self, metric: Metric) { // safe because we know there's at least one value when we parse + // TODO aggregating different types should return error match self { MetricValue::Count(count) => *count += metric.value.get_value().unwrap_or_default(), MetricValue::Gauge(gauge) => *gauge = metric.value.get_value().unwrap_or_default(), @@ -128,7 +129,7 @@ impl Aggregator { .map .iter() .filter_map(|entry| match entry.value { - MetricValue::Distribution(_) => build_sketch(now, entry, &self.tags), + MetricValue::Distribution(_) => build_sketch(now, entry, self.tags.clone()), _ => None, }) .collect(); @@ -154,7 +155,7 @@ impl Aggregator { } false }) - .filter_map(|entry| build_sketch(now, &entry, &self.tags)) + .filter_map(|entry| build_sketch(now, &entry, self.tags.clone())) { let next_chunk_size = sketch.compute_size(); @@ -188,7 +189,7 @@ impl Aggregator { .iter() .filter_map(|entry| match entry.value { MetricValue::Distribution(_) => None, - _ => build_metric(entry, &self.tags), + _ => build_metric(entry, self.tags.clone()), }) .for_each(|metric| series_payload.series.push(metric)); series_payload @@ -209,7 +210,7 @@ impl Aggregator { } true }) - .filter_map(|entry| build_metric(&entry, &self.tags)) + .filter_map(|entry| build_metric(&entry, self.tags.clone())) { // TODO serialization is made twice for each point. If we return a Vec we can avoid // that @@ -247,13 +248,13 @@ impl Aggregator { batched_payloads } - pub fn get_entry_by_id(&self, name: Ustr, tags: &SortedTags) -> Option<&Metric> { + pub fn get_entry_by_id(&self, name: Ustr, tags: &Option) -> Option<&Metric> { let id = metric::id(name, tags); self.map.find(id, |m| m.id == id) } } -fn build_sketch(now: i64, entry: &Metric, base_tag_vec: &SortedTags) -> Option { +fn build_sketch(now: i64, entry: &Metric, mut base_tag_vec: SortedTags) -> Option { let sketch = entry.value.get_sketch()?; let mut dogsketch = Dogsketch::default(); sketch.merge_to_dogsketch(&mut dogsketch); @@ -263,14 +264,20 @@ fn build_sketch(now: i64, entry: &Metric, base_tag_vec: &SortedTags) -> Option Option { - let resources = entry.tags.to_resources(); +fn build_metric(entry: &Metric, mut base_tag_vec: SortedTags) -> Option { + let resources; + if let Some(tags) = entry.tags.clone() { + resources = tags.to_resources(); + } else { + resources = Vec::new(); + } let kind = match entry.value { MetricValue::Count(_) => datadog::DdMetricKind::Count, MetricValue::Gauge(_) => datadog::DdMetricKind::Gauge, @@ -285,15 +292,16 @@ fn build_metric(entry: &Metric, base_tag_vec: &SortedTags) -> Option, metric_id: &str, value: f64) { let aggregator = aggregator_mutex.lock().unwrap(); - if let Some(e) = aggregator.get_entry_by_id(metric_id.into(), &EMPTY_TAGS) { + if let Some(e) = aggregator.get_entry_by_id(metric_id.into(), &None) { let metric = e.value.get_sketch().unwrap(); assert!((metric.max().unwrap() - value).abs() < PRECISION); assert!((metric.min().unwrap() - value).abs() < PRECISION); @@ -418,7 +426,7 @@ pub mod tests { assert_eq!(aggregator.map.len(), 2); if let Some(v) = - aggregator.get_entry_by_id("foo".into(), &SortedTags::parse("k2:v2").unwrap()) + aggregator.get_entry_by_id("foo".into(), &Some(SortedTags::parse("k2:v2").unwrap())) { assert_eq!(v.value.get_value().unwrap(), 5f64); } else { @@ -426,7 +434,7 @@ pub mod tests { } if let Some(v) = - aggregator.get_entry_by_id("test".into(), &SortedTags::parse("k1:v1").unwrap()) + aggregator.get_entry_by_id("test".into(), &Some(SortedTags::parse("k1:v1").unwrap())) { assert_eq!(v.value.get_value().unwrap(), 3f64); } else { diff --git a/src/metric.rs b/src/metric.rs index e0fcd122..3c8ab2fd 100644 --- a/src/metric.rs +++ b/src/metric.rs @@ -22,6 +22,22 @@ pub enum MetricValue { Distribution(DDSketch), } +impl MetricValue { + pub fn count(v: f64) -> MetricValue { + MetricValue::Count(v) + } + + pub fn gauge(v: f64) -> MetricValue { + MetricValue::Gauge(v) + } + + pub fn distribution(v: f64) -> MetricValue { + let sketch = &mut DDSketch::default(); + sketch.insert(v); + MetricValue::Distribution(sketch.to_owned()) + } +} + #[derive(Clone, Debug)] pub struct SortedTags { // We sort tags. This is in feature parity with DogStatsD and also means @@ -121,10 +137,22 @@ pub struct Metric { /// the parser. We assume here that tags are not sent in random order by the /// clien or that, if they are, the API will tidy that up. That is `a:1,b:2` /// is a different tagset from `b:2,a:1`. - pub tags: SortedTags, + pub tags: Option, /// ID given a name and tagset. - pub(crate) id: u64, + pub id: u64, +} + +impl Metric { + pub fn new(name: Ustr, value: MetricValue, tags: Option) -> Metric { + let id = id(name, &tags); + Metric { + name, + value, + tags, + id, + } + } } /// Parse a metric from given input. @@ -148,9 +176,9 @@ pub fn parse(input: &str) -> Result { let tags; if let Some(tags_section) = caps.name("tags") { - tags = SortedTags::parse(tags_section.as_str())?; + tags = Some(SortedTags::parse(tags_section.as_str())?); } else { - tags = EMPTY_TAGS; + tags = None; } let val = first_value(caps.name("values").unwrap().as_str())?; let metric_value = match caps.name("type").unwrap().as_str() { @@ -202,13 +230,15 @@ fn first_value(values: &str) -> Result { /// from the point of view of this function. #[inline] #[must_use] -pub fn id(name: Ustr, tags: &SortedTags) -> u64 { +pub fn id(name: Ustr, tags: &Option) -> u64 { let mut hasher = FnvHasher::default(); name.hash(&mut hasher); - for kv in tags.values.iter() { - kv.0.as_bytes().hash(&mut hasher); - kv.1.as_bytes().hash(&mut hasher); + if let Some(tags_present) = tags { + for kv in tags_present.values.iter() { + kv.0.as_bytes().hash(&mut hasher); + kv.1.as_bytes().hash(&mut hasher); + } } hasher.finish() } @@ -274,7 +304,7 @@ mod tests { assert_eq!(name, metric.name.as_str()); if let Some(tags) = tagset { - let parsed_metric_tags : SortedTags= metric.tags.clone(); + let parsed_metric_tags : SortedTags = metric.tags.unwrap(); assert_eq!(tags.split(',').count(), parsed_metric_tags.values.len()); tags.split(',').for_each(|kv| { let (original_key, original_value) = kv.split_once(':').unwrap(); @@ -288,7 +318,7 @@ mod tests { assert!(found); }); } else { - assert!(metric.tags.is_empty()); + assert!(metric.tags.is_none()); } match mtype.as_str() { @@ -409,8 +439,8 @@ mod tests { tagset2.pop(); } - let id1 = id(Ustr::from(&name), &SortedTags::parse(&tagset1).unwrap()); - let id2 = id(Ustr::from(&name), &SortedTags::parse(&tagset2).unwrap()); + let id1 = id(Ustr::from(&name), &Some(SortedTags::parse(&tagset1).unwrap())); + let id2 = id(Ustr::from(&name), &Some(SortedTags::parse(&tagset2).unwrap())); assert_eq!(id1, id2); } From 088060e9c82870464a5433bd244d7701e70a86a7 Mon Sep 17 00:00:00 2001 From: Ivo Anjo Date: Wed, 16 Oct 2024 13:41:23 +0100 Subject: [PATCH 08/28] Explicitly pin version of deps from saluki-backport repo This way we're more explicit about upgrades, rather than upgrades happening "accidentally" if the `Cargo.lock` gets refreshed. --- Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 30f3b7cc..fb2ad1ce 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,8 +9,8 @@ license.workspace = true bench = false [dependencies] -datadog-protos = { version = "0.1.0", default-features = false, git = "https://github.com/DataDog/saluki-backport/" } -ddsketch-agent = { version = "0.1.0", default-features = false, git = "https://github.com/DataDog/saluki-backport/" } +datadog-protos = { version = "0.1.0", default-features = false, git = "https://github.com/DataDog/saluki-backport/", rev = "3c5d87ab82dea4a1a98ef0c60fb3659ca35c2751" } +ddsketch-agent = { version = "0.1.0", default-features = false, git = "https://github.com/DataDog/saluki-backport/", rev = "3c5d87ab82dea4a1a98ef0c60fb3659ca35c2751" } hashbrown = { version = "0.14.3", default-features = false, features = ["inline-more"] } protobuf = { version = "3.5.0", default-features = false } ustr = { version = "1.0.0", default-features = false } From a14916aa8369188bec62cce80b8cd6bb23ef9907 Mon Sep 17 00:00:00 2001 From: AJ Stuyvenberg Date: Wed, 16 Oct 2024 16:28:20 -0400 Subject: [PATCH 09/28] feat: Prefer DD_PROXY_HTTPS over HTTPS_PROXY (#673) * feat: Prefer DD_PROXY_HTTPS over HTTPS_PROXY * fix: no proxy on ints * fix: clippy thx --- src/datadog.rs | 21 ++++++++++++++++++--- src/flusher.rs | 9 +++++++-- tests/integration_test.rs | 1 + 3 files changed, 26 insertions(+), 5 deletions(-) diff --git a/src/datadog.rs b/src/datadog.rs index b9588297..e7302445 100644 --- a/src/datadog.rs +++ b/src/datadog.rs @@ -8,7 +8,7 @@ use protobuf::Message; use reqwest; use serde::{Serialize, Serializer}; use serde_json; -use tracing::debug; +use tracing::{debug, error}; /// Interface for the `DogStatsD` metrics intake API. #[derive(Debug)] @@ -20,11 +20,18 @@ pub struct DdApi { impl DdApi { #[must_use] - pub fn new(api_key: String, site: String) -> Self { + pub fn new(api_key: String, site: String, https_proxy: Option) -> Self { + let client = match Self::build_client(https_proxy) { + Ok(client) => client, + Err(e) => { + error!("Unable to parse proxy URL, no proxy will be used. {:?}", e); + reqwest::Client::new() + } + }; DdApi { api_key, fqdn_site: site, - client: reqwest::Client::new(), + client, } } @@ -96,6 +103,14 @@ impl DdApi { } }; } + + fn build_client(https_proxy: Option) -> Result { + let mut builder = reqwest::Client::builder(); + if let Some(proxy) = https_proxy { + builder = builder.proxy(reqwest::Proxy::https(proxy)?); + } + builder.build() + } } #[derive(Debug, Serialize, Clone, Copy)] diff --git a/src/flusher.rs b/src/flusher.rs index 7969c7f8..601d223b 100644 --- a/src/flusher.rs +++ b/src/flusher.rs @@ -18,8 +18,13 @@ pub fn build_fqdn_metrics(site: String) -> String { #[allow(clippy::await_holding_lock)] impl Flusher { - pub fn new(api_key: String, aggregator: Arc>, site: String) -> Self { - let dd_api = datadog::DdApi::new(api_key, site); + pub fn new( + api_key: String, + aggregator: Arc>, + site: String, + https_proxy: Option, + ) -> Self { + let dd_api = datadog::DdApi::new(api_key, site, https_proxy); Flusher { dd_api, aggregator } } diff --git a/tests/integration_test.rs b/tests/integration_test.rs index 1acb6543..7ffce541 100644 --- a/tests/integration_test.rs +++ b/tests/integration_test.rs @@ -41,6 +41,7 @@ async fn dogstatsd_server_ships_series() { "mock-api-key".to_string(), Arc::clone(&metrics_aggr), mock_server.url(), + None, ); let server_address = "127.0.0.1:18125"; From 026dcf07c664be35ee51748875bf56f2592d4e13 Mon Sep 17 00:00:00 2001 From: Duncan Harvey <35278470+duncanpharvey@users.noreply.github.com> Date: Wed, 30 Oct 2024 15:03:41 -0400 Subject: [PATCH 10/28] Increase DogStatsD Buffer Size and Pattern Match Container Ids (#698) * show dogstatsd logs in serverless mini agent * increase buffer size to 8192 bytes * update metric pattern to exclude service checks and to account for container ids * exclude events from being parsed * add comment for 8KB max buffer size matching default value in Go Agent * lazily initialize static regex for dogstatsd metrics * minor refactors * add unit tests * remove explicit drops --- Cargo.toml | 2 ++ src/dogstatsd.rs | 31 +++++++++++++++++-- src/metric.rs | 77 +++++++++++++++++++++++++++--------------------- 3 files changed, 74 insertions(+), 36 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index fb2ad1ce..51e1cb16 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,7 @@ hashbrown = { version = "0.14.3", default-features = false, features = ["inline- protobuf = { version = "3.5.0", default-features = false } ustr = { version = "1.0.0", default-features = false } fnv = { version = "1.0.7", default-features = false } +lazy_static = { version = "1.5.0", default-features = false } reqwest = { version = "0.12.4", features = ["json", "http2", "rustls-tls"], default-features = false } serde = { version = "1.0.197", default-features = false, features = ["derive"] } serde_json = { version = "1.0.116", default-features = false, features = ["alloc"] } @@ -27,3 +28,4 @@ regex = { version = "1.10.6", default-features = false } [dev-dependencies] mockito = { version = "1.5.0", default-features = false } proptest = "1.4.0" +tracing-test = { version = "0.2.5", default-features = false } diff --git a/src/dogstatsd.rs b/src/dogstatsd.rs index 0b28f4c6..2aef70b1 100644 --- a/src/dogstatsd.rs +++ b/src/dogstatsd.rs @@ -32,7 +32,9 @@ impl BufferReader { match self { BufferReader::UdpSocketReader(socket) => { // TODO(astuyve) this should be dynamic - let mut buf = [0; 1024]; // todo, do we want to make this dynamic? (not sure) + // Max buffer size is configurable in Go Agent and the default is 8KB + // https://github.com/DataDog/datadog-agent/blob/85939a62b5580b2a15549f6936f257e61c5aa153/pkg/config/config_template.yaml#L2154-L2158 + let mut buf = [0; 8192]; let (amt, src) = socket .recv_from(&mut buf) .await @@ -85,7 +87,7 @@ impl DogStatsD { fn insert_metrics(&self, msg: Split) { let all_valid_metrics: Vec = msg - .filter(|m| !m.is_empty()) + .filter(|m| !m.is_empty() && !m.starts_with("_sc|") && !m.starts_with("_e{")) // exclude empty messages, service checks, and events .map(|m| m.replace('\n', "")) .filter_map(|m| match parse(m.as_str()) { Ok(metric) => Some(metric), @@ -114,6 +116,7 @@ mod tests { use crate::metric::EMPTY_TAGS; use std::net::{IpAddr, Ipv4Addr, SocketAddr}; use std::sync::{Arc, Mutex}; + use tracing_test::traced_test; #[tokio::test] #[cfg_attr(miri, ignore)] @@ -184,6 +187,30 @@ single_machine_performance.rouster.metrics_max_timestamp_latency:1376.90870216|d assert_value(&locked_aggregator, "metric123", 99_123.0, ""); } + #[tokio::test] + #[traced_test] + #[cfg_attr(miri, ignore)] + async fn test_dogstatsd_filter_service_check() { + let locked_aggregator = setup_dogstatsd("_sc|servicecheck|0").await; + let aggregator = locked_aggregator.lock().expect("lock poisoned"); + let parsed_metrics = aggregator.to_series(); + + assert!(!logs_contain("Failed to parse metric")); + assert_eq!(parsed_metrics.len(), 0); + } + + #[tokio::test] + #[traced_test] + #[cfg_attr(miri, ignore)] + async fn test_dogstatsd_filter_event() { + let locked_aggregator = setup_dogstatsd("_e{5,10}:event|test event").await; + let aggregator = locked_aggregator.lock().expect("lock poisoned"); + let parsed_metrics = aggregator.to_series(); + + assert!(!logs_contain("Failed to parse metric")); + assert_eq!(parsed_metrics.len(), 0); + } + async fn setup_dogstatsd(statsd_string: &str) -> Arc> { let aggregator_arc = Arc::new(Mutex::new( Aggregator::new(EMPTY_TAGS, 1_024).expect("aggregator creation failed"), diff --git a/src/metric.rs b/src/metric.rs index 3c8ab2fd..667d502f 100644 --- a/src/metric.rs +++ b/src/metric.rs @@ -5,6 +5,7 @@ use crate::errors::ParseError; use crate::{constants, datadog}; use ddsketch_agent::DDSketch; use fnv::FnvHasher; +use lazy_static::lazy_static; use protobuf::Chars; use regex::Regex; use std::hash::{Hash, Hasher}; @@ -12,6 +13,12 @@ use ustr::Ustr; pub const EMPTY_TAGS: SortedTags = SortedTags { values: Vec::new() }; +lazy_static! { + static ref METRIC_REGEX: regex::Regex = Regex::new( + r"^(?P[^:]+):(?P[^|]+)\|(?P[cgd])(?:\|@(?P[\d.]+))?(?:\|#(?P[^|]+))?(?:\|c:(?P[^|]+))?$", + ).expect("Failed to create metric regex"); +} + #[derive(Clone, Debug)] pub enum MetricValue { /// Dogstatsd 'count' metric type, monotonically increasing counter @@ -167,41 +174,37 @@ impl Metric { /// example aj-test.increment:1|c|#user:aj-test from 127.0.0.1:50983 pub fn parse(input: &str) -> Result { // TODO must enforce / exploit constraints given in `constants`. - if let Ok(re) = Regex::new( - r"^(?P[^:]+):(?P[^|]+)\|(?P[cgd])(?:\|@(?P[\d.]+))?(?:\|#(?P[^|]+))?$", - ) { - if let Some(caps) = re.captures(input) { - // unused for now - // let sample_rate = caps.name("sample_rate").map(|m| m.as_str()); - - let tags; - if let Some(tags_section) = caps.name("tags") { - tags = Some(SortedTags::parse(tags_section.as_str())?); - } else { - tags = None; - } - let val = first_value(caps.name("values").unwrap().as_str())?; - let metric_value = match caps.name("type").unwrap().as_str() { - "c" => MetricValue::Count(val), - "g" => MetricValue::Gauge(val), - "d" => { - let sketch = &mut DDSketch::default(); - sketch.insert(val); - MetricValue::Distribution(sketch.to_owned()) - } - _ => { - return Err(ParseError::Raw("Unsupported metric type")); - } - }; - let name = Ustr::from(caps.name("name").unwrap().as_str()); - let id = id(name, &tags); - return Ok(Metric { - name, - value: metric_value, - tags, - id, - }); + if let Some(caps) = METRIC_REGEX.captures(input) { + // unused for now + // let sample_rate = caps.name("sample_rate").map(|m| m.as_str()); + + let tags; + if let Some(tags_section) = caps.name("tags") { + tags = Some(SortedTags::parse(tags_section.as_str())?); + } else { + tags = None; } + let val = first_value(caps.name("values").unwrap().as_str())?; + let metric_value = match caps.name("type").unwrap().as_str() { + "c" => MetricValue::Count(val), + "g" => MetricValue::Gauge(val), + "d" => { + let sketch = &mut DDSketch::default(); + sketch.insert(val); + MetricValue::Distribution(sketch.to_owned()) + } + _ => { + return Err(ParseError::Raw("Unsupported metric type")); + } + }; + let name = Ustr::from(caps.name("name").unwrap().as_str()); + let id = id(name, &tags); + return Ok(Metric { + name, + value: metric_value, + tags, + id, + }); } Err(ParseError::Raw("Invalid metric format")) } @@ -468,4 +471,10 @@ mod tests { fn invalid_dogstatsd_no_panic() { assert!(parse("somerandomstring|c+a;slda").is_err()); } + + #[test] + #[cfg_attr(miri, ignore)] + fn parse_container_id() { + assert!(parse("containerid.metric:0|c|#env:dev,client_transport:udp|c:0000000000000000000000000000000000000000000000000000000000000000").is_ok()); + } } From 7a3b41dd010fa5624fed39b26d25d23e6e8bdb73 Mon Sep 17 00:00:00 2001 From: Daniel Schwartz-Narbonne Date: Wed, 20 Nov 2024 15:13:41 -0500 Subject: [PATCH 11/28] [chore] Use elapsed() if possible when calculating durations (#750) --- src/aggregator.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/aggregator.rs b/src/aggregator.rs index b8edfaaa..0f82617e 100644 --- a/src/aggregator.rs +++ b/src/aggregator.rs @@ -117,8 +117,8 @@ impl Aggregator { #[must_use] pub fn distributions_to_protobuf(&self) -> SketchPayload { - let now = time::SystemTime::now() - .duration_since(time::UNIX_EPOCH) + let now = time::UNIX_EPOCH + .elapsed() .expect("unable to poll clock, unrecoverable") .as_secs() .try_into() @@ -138,8 +138,8 @@ impl Aggregator { #[must_use] pub fn consume_distributions(&mut self) -> Vec { - let now = time::SystemTime::now() - .duration_since(time::UNIX_EPOCH) + let now = time::UNIX_EPOCH + .elapsed() .expect("unable to poll clock, unrecoverable") .as_secs() .try_into() @@ -286,8 +286,8 @@ fn build_metric(entry: &Metric, mut base_tag_vec: SortedTags) -> Option Date: Mon, 16 Dec 2024 13:19:33 -0500 Subject: [PATCH 12/28] fix key val resource order in dogstatsd (#803) --- src/metric.rs | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/src/metric.rs b/src/metric.rs index 667d502f..ca6bae59 100644 --- a/src/metric.rs +++ b/src/metric.rs @@ -104,10 +104,10 @@ impl SortedTags { pub(crate) fn to_resources(&self) -> Vec { let mut resources = Vec::with_capacity(constants::MAX_TAGS); - for (name, kind) in &self.values { + for (kind, name) in &self.values { let resource = datadog::Resource { - name: name.as_str(), - kind: kind.as_str(), + name: name.as_str(), // val + kind: kind.as_str(), // key }; resources.push(resource); } @@ -447,6 +447,21 @@ mod tests { assert_eq!(id1, id2); } + + #[test] + #[cfg_attr(miri, ignore)] + fn resources_key_val_order(tags in metric_tags()) { + let sorted_tags = SortedTags { values: tags.into_iter() + .map(|(kind, name)| (Ustr::from(&kind), Ustr::from(&name))) + .collect() }; + + let resources = sorted_tags.to_resources(); + + for (i, resource) in resources.iter().enumerate() { + assert_eq!(resource.kind, sorted_tags.values[i].0); + assert_eq!(resource.name, sorted_tags.values[i].1); + } + } } #[test] From 118bfbabf04d347217b89540f19f728cb68b1b9a Mon Sep 17 00:00:00 2001 From: alexgallotta <5581237+alexgallotta@users.noreply.github.com> Date: Thu, 19 Dec 2024 15:58:57 -0500 Subject: [PATCH 13/28] Svls 6036 set timeouts (#800) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * add fixed timeout to verify behavior * add number of series and distro to flush * tmp: add info on trace retries * add more tmp debug log * fix debug log * Revert "fix debug log" This reverts commit 4b44bb37ec4cc658d37fbeea820d399a4e924788. * Revert "add more tmp debug log" This reverts commit 536917b8c2f990365058c90a580937ee5e5f1894. * Revert "tmp: add info on trace retries" This reverts commit 5e4790794decea0f3f7e1ab523a9ab36e39820db. * add timeout for client * add todo * fmt * add constant for timeout duration --------- Co-authored-by: jordan gonzález <30836115+duncanista@users.noreply.github.com> --- src/datadog.rs | 17 +++++++++++++---- src/flusher.rs | 13 ++++++++++++- tests/integration_test.rs | 1 + 3 files changed, 26 insertions(+), 5 deletions(-) diff --git a/src/datadog.rs b/src/datadog.rs index e7302445..95608fed 100644 --- a/src/datadog.rs +++ b/src/datadog.rs @@ -8,6 +8,7 @@ use protobuf::Message; use reqwest; use serde::{Serialize, Serializer}; use serde_json; +use std::time::Duration; use tracing::{debug, error}; /// Interface for the `DogStatsD` metrics intake API. @@ -20,8 +21,13 @@ pub struct DdApi { impl DdApi { #[must_use] - pub fn new(api_key: String, site: String, https_proxy: Option) -> Self { - let client = match Self::build_client(https_proxy) { + pub fn new( + api_key: String, + site: String, + https_proxy: Option, + timeout: Duration, + ) -> Self { + let client = match Self::build_client(https_proxy, timeout) { Ok(client) => client, Err(e) => { error!("Unable to parse proxy URL, no proxy will be used. {:?}", e); @@ -104,8 +110,11 @@ impl DdApi { }; } - fn build_client(https_proxy: Option) -> Result { - let mut builder = reqwest::Client::builder(); + fn build_client( + https_proxy: Option, + timeout: Duration, + ) -> Result { + let mut builder = reqwest::Client::builder().timeout(timeout); if let Some(proxy) = https_proxy { builder = builder.proxy(reqwest::Proxy::https(proxy)?); } diff --git a/src/flusher.rs b/src/flusher.rs index 601d223b..fe40f32c 100644 --- a/src/flusher.rs +++ b/src/flusher.rs @@ -4,6 +4,8 @@ use crate::aggregator::Aggregator; use crate::datadog; use std::sync::{Arc, Mutex}; +use std::time::Duration; +use tracing::debug; pub struct Flusher { dd_api: datadog::DdApi, @@ -23,8 +25,9 @@ impl Flusher { aggregator: Arc>, site: String, https_proxy: Option, + timeout: Duration, ) -> Self { - let dd_api = datadog::DdApi::new(api_key, site, https_proxy); + let dd_api = datadog::DdApi::new(api_key, site, https_proxy, timeout); Flusher { dd_api, aggregator } } @@ -36,6 +39,14 @@ impl Flusher { aggregator.consume_distributions(), ) }; + + let n_series = all_series.len(); + let n_distributions = all_distributions.len(); + + debug!("Flushing {n_series} series and {n_distributions} distributions"); + + // TODO: client timeout is for each invocation, so NxM times with N time series batches and + // M distro batches for a_batch in all_series { self.dd_api.ship_series(&a_batch).await; // TODO(astuyve) retry and do not panic diff --git a/tests/integration_test.rs b/tests/integration_test.rs index 7ffce541..40ef9f05 100644 --- a/tests/integration_test.rs +++ b/tests/integration_test.rs @@ -42,6 +42,7 @@ async fn dogstatsd_server_ships_series() { Arc::clone(&metrics_aggr), mock_server.url(), None, + std::time::Duration::from_secs(5), ); let server_address = "127.0.0.1:18125"; From 97bf33b9279ca9435ece309ad6f9f4d6ea658ea4 Mon Sep 17 00:00:00 2001 From: Duncan Harvey <35278470+duncanpharvey@users.noreply.github.com> Date: Fri, 20 Dec 2024 16:03:55 -0500 Subject: [PATCH 14/28] [dogstatsd] Use Saluki as a dependency (#804) * use saluki in dogstatsd * update to latest saluki commit sha --- Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 51e1cb16..bc3de340 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,8 +9,8 @@ license.workspace = true bench = false [dependencies] -datadog-protos = { version = "0.1.0", default-features = false, git = "https://github.com/DataDog/saluki-backport/", rev = "3c5d87ab82dea4a1a98ef0c60fb3659ca35c2751" } -ddsketch-agent = { version = "0.1.0", default-features = false, git = "https://github.com/DataDog/saluki-backport/", rev = "3c5d87ab82dea4a1a98ef0c60fb3659ca35c2751" } +datadog-protos = { version = "0.1.0", default-features = false, git = "https://github.com/DataDog/saluki/", rev = "c89b58e5784b985819baf11f13f7d35876741222" } +ddsketch-agent = { version = "0.1.0", default-features = false, git = "https://github.com/DataDog/saluki/", rev = "c89b58e5784b985819baf11f13f7d35876741222" } hashbrown = { version = "0.14.3", default-features = false, features = ["inline-more"] } protobuf = { version = "3.5.0", default-features = false } ustr = { version = "1.0.0", default-features = false } From ecee80970c8c75290edbdbc773d3c9a3ed34144b Mon Sep 17 00:00:00 2001 From: alexgallotta <5581237+alexgallotta@users.noreply.github.com> Date: Wed, 15 Jan 2025 10:49:09 -0500 Subject: [PATCH 15/28] to not log error on unsupported metric type (#777) * to not log error on unsupported metric type * avoid log altogether * fix: better error handling and log warn for ignored metric types --- src/dogstatsd.rs | 11 ++++++++--- src/errors.rs | 6 ++++-- src/metric.rs | 48 +++++++++++++++++++++++++++++++----------------- 3 files changed, 43 insertions(+), 22 deletions(-) diff --git a/src/dogstatsd.rs b/src/dogstatsd.rs index 2aef70b1..4b9dea4d 100644 --- a/src/dogstatsd.rs +++ b/src/dogstatsd.rs @@ -5,10 +5,10 @@ use std::net::SocketAddr; use std::str::Split; use std::sync::{Arc, Mutex}; -use tracing::{debug, error}; - use crate::aggregator::Aggregator; +use crate::errors::ParseError::UnsupportedType; use crate::metric::{parse, Metric}; +use tracing::{debug, error}; pub struct DogStatsD { cancel_token: tokio_util::sync::CancellationToken, @@ -92,7 +92,12 @@ impl DogStatsD { .filter_map(|m| match parse(m.as_str()) { Ok(metric) => Some(metric), Err(e) => { - error!("Failed to parse metric {}: {}", m, e); + // unsupported type is quite common with dd_trace metrics. Avoid perf issue and + // log spam in that case + match e { + UnsupportedType(_) => debug!("Unsupported metric type: {}. {}", m, e), + _ => error!("Failed to parse metric {}: {}", m, e), + } None } }) diff --git a/src/errors.rs b/src/errors.rs index a97b35a1..fffcca84 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -4,11 +4,13 @@ //! Error types for `metrics` module /// Errors for the function [`crate::metric::Metric::parse`] -#[derive(Debug, thiserror::Error, Clone, Copy, Eq, PartialEq)] +#[derive(Debug, thiserror::Error, PartialEq)] pub enum ParseError { /// Parse failure given in text #[error("parse failure: {0}")] - Raw(&'static str), + Raw(String), + #[error("unsupported metric type: {0}")] + UnsupportedType(String), } /// Failure to create a new `Aggregator` diff --git a/src/metric.rs b/src/metric.rs index ca6bae59..2f89c875 100644 --- a/src/metric.rs +++ b/src/metric.rs @@ -14,8 +14,8 @@ use ustr::Ustr; pub const EMPTY_TAGS: SortedTags = SortedTags { values: Vec::new() }; lazy_static! { - static ref METRIC_REGEX: regex::Regex = Regex::new( - r"^(?P[^:]+):(?P[^|]+)\|(?P[cgd])(?:\|@(?P[\d.]+))?(?:\|#(?P[^|]+))?(?:\|c:(?P[^|]+))?$", + static ref METRIC_REGEX: Regex = Regex::new( + r"^(?P[^:]+):(?P[^|]+)\|(?P[a-zA-Z]+)(?:\|@(?P[\d.]+))?(?:\|#(?P[^|]+))?(?:\|c:(?P[^|]+))?$", ).expect("Failed to create metric regex"); } @@ -70,10 +70,10 @@ impl SortedTags { // Validate that the tags have the right form. for (i, part) in tag_parts.filter(|s| !s.is_empty()).enumerate() { if i >= constants::MAX_TAGS { - return Err(ParseError::Raw("Too many tags")); + return Err(ParseError::Raw(format!("Too many tags, more than {i}"))); } if !part.contains(':') { - return Err(ParseError::Raw("Invalid tag format")); + return Err(ParseError::Raw("Invalid tag format".to_string())); } if let Some((k, v)) = part.split_once(':') { parsed_tags.push((Ustr::from(k), Ustr::from(v))); @@ -185,7 +185,8 @@ pub fn parse(input: &str) -> Result { tags = None; } let val = first_value(caps.name("values").unwrap().as_str())?; - let metric_value = match caps.name("type").unwrap().as_str() { + let t = caps.name("type").unwrap().as_str(); + let metric_value = match t { "c" => MetricValue::Count(val), "g" => MetricValue::Gauge(val), "d" => { @@ -193,8 +194,11 @@ pub fn parse(input: &str) -> Result { sketch.insert(val); MetricValue::Distribution(sketch.to_owned()) } + "h" | "s" | "ms" => { + return Err(ParseError::UnsupportedType(t.to_string())); + } _ => { - return Err(ParseError::Raw("Unsupported metric type")); + return Err(ParseError::Raw(format!("Invalid metric type: {t}"))); } }; let name = Ustr::from(caps.name("name").unwrap().as_str()); @@ -206,16 +210,16 @@ pub fn parse(input: &str) -> Result { id, }); } - Err(ParseError::Raw("Invalid metric format")) + Err(ParseError::Raw(format!("Invalid metric format {input}"))) } fn first_value(values: &str) -> Result { match values.split(':').next() { Some(v) => match v.parse::() { Ok(v) => Ok(v), - Err(_) => Err(ParseError::Raw("Invalid value")), + Err(e) => Err(ParseError::Raw(format!("Invalid value {e}"))), }, - None => Err(ParseError::Raw("Missing value")), + None => Err(ParseError::Raw("Missing value".to_string())), } } @@ -365,7 +369,7 @@ mod tests { }; let result = parse(&input); - assert_eq!(result.unwrap_err(),ParseError::Raw("Invalid metric format")); + assert_eq!(result.unwrap_err(),ParseError::Raw(format!("Invalid metric format {input}"))); } #[test] @@ -386,10 +390,9 @@ mod tests { }; let result = parse(&input); - assert_eq!( - result.unwrap_err(), - ParseError::Raw("Invalid metric format") - ); + let verify = result.unwrap_err().to_string(); + println!("{}", verify); + assert!(verify.starts_with("parse failure: Invalid metric format ")); } #[test] @@ -397,7 +400,7 @@ mod tests { fn parse_unsupported_metric_type( name in metric_name(), values in metric_values(), - mtype in "[abefhijklmnopqrstuvwxyz]", + mtype in "[abefijklmnopqrtuvwxyz]", tagset in metric_tagset() ) { let input = if let Some(ref tagset) = tagset { @@ -409,7 +412,7 @@ mod tests { assert_eq!( result.unwrap_err(), - ParseError::Raw("Invalid metric format") + ParseError::Raw(format!("Invalid metric type: {mtype}")) ); } @@ -469,7 +472,7 @@ mod tests { fn parse_too_many_tags() { // 33 assert_eq!(parse("foo:1|g|#a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3").unwrap_err(), - ParseError::Raw("Too many tags")); + ParseError::Raw("Too many tags, more than 32".to_string())); // 32 assert!(parse("foo:1|g|#a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2").is_ok()); @@ -492,4 +495,15 @@ mod tests { fn parse_container_id() { assert!(parse("containerid.metric:0|c|#env:dev,client_transport:udp|c:0000000000000000000000000000000000000000000000000000000000000000").is_ok()); } + + #[test] + fn parse_tracer_metric() { + let input = "datadog.tracer.flush_duration:0.785551|ms|#lang:go,lang_version:go1.23.2,env:redacted_env,_dd.origin:lambda,runtime-id:redacted_runtime,tracer_version:v1.70.1,service:redacted_service,env:redacted_env,service:redacted_service,version:redacted_version"; + let expected_error = "ms".to_string(); + if let ParseError::UnsupportedType(actual_error) = parse(input).unwrap_err() { + assert_eq!(actual_error, expected_error); + } else { + panic!("Expected UnsupportedType error"); + } + } } From 1048f57ff185a01fe1a00353ca897439695bcaf7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?jordan=20gonz=C3=A1lez?= <30836115+duncanista@users.noreply.github.com> Date: Wed, 15 Jan 2025 17:51:53 -0500 Subject: [PATCH 16/28] [dogstatsd] hardcode filter metric (#761) * hardcode filter metric * update `cc` to be `^1` * add todo --- src/dogstatsd.rs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/dogstatsd.rs b/src/dogstatsd.rs index 4b9dea4d..41456966 100644 --- a/src/dogstatsd.rs +++ b/src/dogstatsd.rs @@ -87,7 +87,16 @@ impl DogStatsD { fn insert_metrics(&self, msg: Split) { let all_valid_metrics: Vec = msg - .filter(|m| !m.is_empty() && !m.starts_with("_sc|") && !m.starts_with("_e{")) // exclude empty messages, service checks, and events + .filter(|m| { + !m.is_empty() + && !m.starts_with("_sc|") + && !m.starts_with("_e{") + // todo(serverless): remove this hack, and create a blocklist for metrics + // or another mechanism for this. + // + // avoid metric duplication with lambda layer + && !m.starts_with("aws.lambda.enhanced.invocations") + }) // exclude empty messages, service checks, and events .map(|m| m.replace('\n', "")) .filter_map(|m| match parse(m.as_str()) { Ok(metric) => Some(metric), From a05e5dcc993dcd5e144e73a90a44e1fda917901d Mon Sep 17 00:00:00 2001 From: Aleksandr Pasechnik Date: Thu, 16 Jan 2025 15:26:31 -0500 Subject: [PATCH 17/28] chore: [SVLS-5992] clearer dogstatsd Flusher API (#822) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: jordan gonzález <30836115+duncanista@users.noreply.github.com> --- Cargo.toml | 1 + src/datadog.rs | 187 +++++++++++++++++++++++++++++++++++++- src/flusher.rs | 33 ++++--- tests/integration_test.rs | 24 +++-- 4 files changed, 218 insertions(+), 27 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index bc3de340..0e98a17d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,7 @@ bench = false [dependencies] datadog-protos = { version = "0.1.0", default-features = false, git = "https://github.com/DataDog/saluki/", rev = "c89b58e5784b985819baf11f13f7d35876741222" } ddsketch-agent = { version = "0.1.0", default-features = false, git = "https://github.com/DataDog/saluki/", rev = "c89b58e5784b985819baf11f13f7d35876741222" } +derive_more = { version = "1.0.0", features = ["display", "into"] } hashbrown = { version = "0.14.3", default-features = false, features = ["inline-more"] } protobuf = { version = "3.5.0", default-features = false } ustr = { version = "1.0.0", default-features = false } diff --git a/src/datadog.rs b/src/datadog.rs index 95608fed..f04bf207 100644 --- a/src/datadog.rs +++ b/src/datadog.rs @@ -4,18 +4,125 @@ //!Types to serialize data into the Datadog API use datadog_protos::metrics::SketchPayload; +use derive_more::{Display, Into}; +use lazy_static::lazy_static; use protobuf::Message; +use regex::Regex; use reqwest; use serde::{Serialize, Serializer}; use serde_json; use std::time::Duration; use tracing::{debug, error}; +lazy_static! { + static ref SITE_RE: Regex = Regex::new(r"^[a-zA-Z0-9._:-]+$").expect("invalid regex"); + static ref URL_PREFIX_RE: Regex = + Regex::new(r"^https?://[a-zA-Z0-9._:-]+$").expect("invalid regex"); +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Display, Into)] +pub struct Site(String); + +#[derive(thiserror::Error, Debug, Clone, PartialEq)] +#[error("Invalid site: {0}")] +pub struct SiteError(String); + +impl Site { + pub fn new(site: String) -> Result { + // Datadog sites are generally domain names. In particular, they shouldn't have any slashes + // in them. We expect this to be coming from a `DD_SITE` environment variable or the `site` + // config field. + if SITE_RE.is_match(&site) { + Ok(Site(site)) + } else { + Err(SiteError(site)) + } + } +} + +#[derive(thiserror::Error, Debug, Clone, PartialEq)] +#[error("Invalid URL prefix: {0}")] +pub struct UrlPrefixError(String); + +fn validate_url_prefix(prefix: &str) -> Result<(), UrlPrefixError> { + if URL_PREFIX_RE.is_match(prefix) { + Ok(()) + } else { + Err(UrlPrefixError(prefix.to_owned())) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Display, Into)] +pub struct DdUrl(String); + +impl DdUrl { + pub fn new(prefix: String) -> Result { + validate_url_prefix(&prefix)?; + Ok(Self(prefix)) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Display, Into)] +pub struct DdDdUrl(String); + +impl DdDdUrl { + pub fn new(prefix: String) -> Result { + validate_url_prefix(&prefix)?; + Ok(Self(prefix)) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Display, Into)] +pub struct MetricsIntakeUrlPrefixOverride(String); + +impl MetricsIntakeUrlPrefixOverride { + pub fn maybe_new(dd_url: Option, dd_dd_url: Option) -> Option { + match (dd_url, dd_dd_url) { + (None, None) => None, + (_, Some(dd_dd_url)) => Some(Self(dd_dd_url.into())), + (Some(dd_url), None) => Some(Self(dd_url.into())), + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Display)] +pub struct MetricsIntakeUrlPrefix(String); + +#[derive(thiserror::Error, Debug, Clone, PartialEq)] +#[error("Missing intake URL configuration")] +pub struct MissingIntakeUrlError; + +impl MetricsIntakeUrlPrefix { + #[inline] + pub fn new( + site: Option, + overridden_prefix: Option, + ) -> Result { + match (site, overridden_prefix) { + (None, None) => Err(MissingIntakeUrlError), + (_, Some(prefix)) => Ok(Self::new_expect_validated(prefix.into())), + (Some(site), None) => Ok(Self::from_site(site)), + } + } + + #[inline] + fn new_expect_validated(validated_prefix: String) -> Self { + validate_url_prefix(&validated_prefix).expect("Invalid URL prefix"); + + Self(validated_prefix) + } + + #[inline] + fn from_site(site: Site) -> Self { + Self(format!("https://api.{}", site)) + } +} + /// Interface for the `DogStatsD` metrics intake API. #[derive(Debug)] pub struct DdApi { api_key: String, - fqdn_site: String, + metrics_intake_url_prefix: MetricsIntakeUrlPrefix, client: reqwest::Client, } @@ -23,7 +130,7 @@ impl DdApi { #[must_use] pub fn new( api_key: String, - site: String, + metrics_intake_url_prefix: MetricsIntakeUrlPrefix, https_proxy: Option, timeout: Duration, ) -> Self { @@ -36,7 +143,7 @@ impl DdApi { }; DdApi { api_key, - fqdn_site: site, + metrics_intake_url_prefix, client, } } @@ -46,7 +153,7 @@ impl DdApi { let body = serde_json::to_vec(&series).expect("failed to serialize series"); debug!("Sending body: {:?}", &series); - let url = format!("{}/api/v2/series", &self.fqdn_site); + let url = format!("{}/api/v2/series", &self.metrics_intake_url_prefix); let resp = self .client .post(&url) @@ -74,7 +181,7 @@ impl DdApi { } pub async fn ship_distributions(&self, sketches: &SketchPayload) { - let url = format!("{}/api/beta/sketches", &self.fqdn_site); + let url = format!("{}/api/beta/sketches", &self.metrics_intake_url_prefix); debug!("Sending distributions: {:?}", &sketches); // TODO maybe go to coded output stream if we incrementally // add sketch payloads to the buffer @@ -195,3 +302,73 @@ impl Series { self.series.len() } } + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn override_can_be_empty() { + assert_eq!(MetricsIntakeUrlPrefixOverride::maybe_new(None, None), None); + } + + #[test] + fn override_prefers_dd_dd_url() { + assert_eq!( + MetricsIntakeUrlPrefixOverride::maybe_new( + Some(DdUrl::new("http://a_dd_url".to_string()).unwrap()), + Some(DdDdUrl::new("https://a_dd_dd_url".to_string()).unwrap()) + ), + Some(MetricsIntakeUrlPrefixOverride( + "https://a_dd_dd_url".to_string() + )) + ); + } + + #[test] + fn override_will_take_dd_url() { + assert_eq!( + MetricsIntakeUrlPrefixOverride::maybe_new( + Some(DdUrl::new("http://a_dd_url".to_string()).unwrap()), + None + ), + Some(MetricsIntakeUrlPrefixOverride( + "http://a_dd_url".to_string() + )) + ); + } + + #[test] + fn test_intake_url_prefix_new_requires_something() { + assert_eq!( + MetricsIntakeUrlPrefix::new(None, None), + Err(MissingIntakeUrlError) + ); + } + + #[test] + fn test_intake_url_prefix_new_picks_the_override() { + assert_eq!( + MetricsIntakeUrlPrefix::new( + Some(Site::new("a_site".to_string()).unwrap()), + MetricsIntakeUrlPrefixOverride::maybe_new( + Some(DdUrl::new("http://a_dd_url".to_string()).unwrap()), + None + ), + ), + Ok(MetricsIntakeUrlPrefix::new_expect_validated( + "http://a_dd_url".to_string() + )) + ); + } + + #[test] + fn test_intake_url_prefix_new_picks_site_as_a_fallback() { + assert_eq!( + MetricsIntakeUrlPrefix::new(Some(Site::new("a_site".to_string()).unwrap()), None,), + Ok(MetricsIntakeUrlPrefix::new_expect_validated( + "https://api.a_site".to_string() + )) + ); + } +} diff --git a/src/flusher.rs b/src/flusher.rs index fe40f32c..98875b20 100644 --- a/src/flusher.rs +++ b/src/flusher.rs @@ -3,32 +3,37 @@ use crate::aggregator::Aggregator; use crate::datadog; +use datadog::{DdApi, MetricsIntakeUrlPrefix}; use std::sync::{Arc, Mutex}; use std::time::Duration; use tracing::debug; pub struct Flusher { - dd_api: datadog::DdApi, + dd_api: DdApi, aggregator: Arc>, } -#[inline] -#[must_use] -pub fn build_fqdn_metrics(site: String) -> String { - format!("https://api.{site}") +pub struct FlusherConfig { + pub api_key: String, + pub aggregator: Arc>, + pub metrics_intake_url_prefix: MetricsIntakeUrlPrefix, + pub https_proxy: Option, + pub timeout: Duration, } #[allow(clippy::await_holding_lock)] impl Flusher { - pub fn new( - api_key: String, - aggregator: Arc>, - site: String, - https_proxy: Option, - timeout: Duration, - ) -> Self { - let dd_api = datadog::DdApi::new(api_key, site, https_proxy, timeout); - Flusher { dd_api, aggregator } + pub fn new(config: FlusherConfig) -> Self { + let dd_api = DdApi::new( + config.api_key, + config.metrics_intake_url_prefix, + config.https_proxy, + config.timeout, + ); + Flusher { + dd_api, + aggregator: config.aggregator, + } } pub async fn flush(&mut self) { diff --git a/tests/integration_test.rs b/tests/integration_test.rs index 40ef9f05..54007b35 100644 --- a/tests/integration_test.rs +++ b/tests/integration_test.rs @@ -5,8 +5,9 @@ use dogstatsd::metric::SortedTags; use dogstatsd::{ aggregator::Aggregator as MetricsAggregator, constants::CONTEXTS, + datadog::{DdDdUrl, MetricsIntakeUrlPrefix, MetricsIntakeUrlPrefixOverride}, dogstatsd::{DogStatsD, DogStatsDConfig}, - flusher::Flusher, + flusher::{Flusher, FlusherConfig}, }; use mockito::Server; use std::sync::{Arc, Mutex}; @@ -37,13 +38,20 @@ async fn dogstatsd_server_ships_series() { let _ = start_dogstatsd(&metrics_aggr).await; - let mut metrics_flusher = Flusher::new( - "mock-api-key".to_string(), - Arc::clone(&metrics_aggr), - mock_server.url(), - None, - std::time::Duration::from_secs(5), - ); + let mut metrics_flusher = Flusher::new(FlusherConfig { + api_key: "mock-api-key".to_string(), + aggregator: Arc::clone(&metrics_aggr), + metrics_intake_url_prefix: MetricsIntakeUrlPrefix::new( + None, + MetricsIntakeUrlPrefixOverride::maybe_new( + None, + Some(DdDdUrl::new(mock_server.url()).expect("failed to create URL")), + ), + ) + .expect("failed to create URL"), + https_proxy: None, + timeout: std::time::Duration::from_secs(5), + }); let server_address = "127.0.0.1:18125"; let socket = UdpSocket::bind("0.0.0.0:0") From bf89fd15d9fef0aafb04b0cb7826b64e7482ae50 Mon Sep 17 00:00:00 2001 From: alexgallotta <5581237+alexgallotta@users.noreply.github.com> Date: Mon, 27 Jan 2025 12:41:12 -0500 Subject: [PATCH 18/28] resources are only dd.internal.resource tags (#829) * resources are only dd.internal.resource tags * improve test --- src/aggregator.rs | 7 ++++--- src/datadog.rs | 4 ++-- src/metric.rs | 29 +++++++++++++++++++++++------ 3 files changed, 29 insertions(+), 11 deletions(-) diff --git a/src/aggregator.rs b/src/aggregator.rs index 0f82617e..196a9b76 100644 --- a/src/aggregator.rs +++ b/src/aggregator.rs @@ -318,7 +318,7 @@ pub mod tests { const PRECISION: f64 = 0.000_000_01; - const SINGLE_METRIC_SIZE: usize = 216; // taken from the test, size of a serialized metric with one tag and 1 digit counter value + const SINGLE_METRIC_SIZE: usize = 193; // taken from the test, size of a serialized metric with one tag and 1 digit counter value const SINGLE_DISTRIBUTION_SIZE: u64 = 140; const DEFAULT_TAGS: &str = "dd_extension_version:63-next,architecture:x86_64,_dd.compute_stats:1"; @@ -450,7 +450,7 @@ pub mod tests { fn to_series() { let mut aggregator = Aggregator::new(EMPTY_TAGS, 2).unwrap(); - let metric1 = parse("test:1|c|#k:v").expect("metric parse failed"); + let metric1 = parse("test:1|c|#k1:v1,k2:v2").expect("metric parse failed"); let metric2 = parse("foo:1|c|#k:v").expect("metric parse failed"); let metric3 = parse("bar:1|c|#k:v").expect("metric parse failed"); @@ -459,6 +459,7 @@ pub mod tests { assert_eq!(aggregator.map.len(), 2); assert_eq!(aggregator.to_series().len(), 2); + // to_series should not mutate the state assert_eq!(aggregator.map.len(), 2); assert_eq!(aggregator.to_series().len(), 2); assert_eq!(aggregator.map.len(), 2); @@ -663,7 +664,7 @@ pub mod tests { fn consume_metrics_batch_bytes() { let expected_metrics_per_batch = 2; let total_number_of_metrics = 5; - let two_metrics_size = 420; + let two_metrics_size = 374; let max_bytes = SINGLE_METRIC_SIZE * expected_metrics_per_batch + 13; let mut aggregator = Aggregator { tags: to_sorted_tags(), diff --git a/src/datadog.rs b/src/datadog.rs index f04bf207..41c40a51 100644 --- a/src/datadog.rs +++ b/src/datadog.rs @@ -242,10 +242,10 @@ pub(crate) struct Point { /// A named resource pub(crate) struct Resource { /// The name of this resource - pub(crate) name: &'static str, + pub(crate) name: String, #[serde(rename = "type")] /// The kind of this resource - pub(crate) kind: &'static str, + pub(crate) kind: String, } #[derive(Debug, Clone, Copy)] diff --git a/src/metric.rs b/src/metric.rs index 2f89c875..717102e7 100644 --- a/src/metric.rs +++ b/src/metric.rs @@ -104,12 +104,18 @@ impl SortedTags { pub(crate) fn to_resources(&self) -> Vec { let mut resources = Vec::with_capacity(constants::MAX_TAGS); - for (kind, name) in &self.values { - let resource = datadog::Resource { - name: name.as_str(), // val - kind: kind.as_str(), // key - }; - resources.push(resource); + for (key, val) in &self.values { + if key == "dd.internal.resource" { + //anything coming in via dd.internal.resource: has to be a key/value pair + // (e.g., dd.internal.resource:key:value) + if let Some(valid_name_kind) = val.split_once(':') { + let resource = datadog::Resource { + name: valid_name_kind.0.to_string(), + kind: valid_name_kind.1.to_string(), + }; + resources.push(resource); + } + } } resources } @@ -506,4 +512,15 @@ mod tests { panic!("Expected UnsupportedType error"); } } + + #[test] + fn sorting_tags() { + let mut tags = SortedTags::parse("z:z0,b:b2,c:c3").unwrap(); + tags.extend(&SortedTags::parse("z1:z11,d:d4,e:e5,f:f6").unwrap()); + tags.extend(&SortedTags::parse("a:a1").unwrap()); + assert_eq!(tags.values.len(), 8); + let first_element = tags.values.first().unwrap(); + assert_eq!(first_element.0, Ustr::from("a")); + assert_eq!(first_element.1, Ustr::from("a1")); + } } From 717f4919332fd77ecb46fde611a496dd150bc2cc Mon Sep 17 00:00:00 2001 From: alexgallotta <5581237+alexgallotta@users.noreply.github.com> Date: Mon, 27 Jan 2025 16:18:37 -0500 Subject: [PATCH 19/28] fix: support tag that are not key-value pairs (#826) * fix: support tag that are not key-value pairs * fix comment * add test for tag with multiple columns * use more efficient string concatenation * clippy --- src/metric.rs | 50 ++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 44 insertions(+), 6 deletions(-) diff --git a/src/metric.rs b/src/metric.rs index 717102e7..dcfb23c6 100644 --- a/src/metric.rs +++ b/src/metric.rs @@ -73,9 +73,8 @@ impl SortedTags { return Err(ParseError::Raw(format!("Too many tags, more than {i}"))); } if !part.contains(':') { - return Err(ParseError::Raw("Invalid tag format".to_string())); - } - if let Some((k, v)) = part.split_once(':') { + parsed_tags.push((Ustr::from(part), Ustr::from(""))); + } else if let Some((k, v)) = part.split_once(':') { parsed_tags.push((Ustr::from(k), Ustr::from(v))); } } @@ -89,7 +88,15 @@ impl SortedTags { pub fn to_chars(&self) -> Vec { let mut tags_as_chars = Vec::new(); for (k, v) in &self.values { - tags_as_chars.push(format!("{}:{}", k, v).into()); + if v.is_empty() { + tags_as_chars.push(Chars::from(k.to_string())); + } else { + let mut a_tag = String::with_capacity(k.len() + v.len() + 1); + a_tag.push_str(k); + a_tag.push(':'); + a_tag.push_str(v); + tags_as_chars.push(a_tag.into()); + } } tags_as_chars } @@ -97,7 +104,15 @@ impl SortedTags { pub fn to_strings(&self) -> Vec { let mut tags_as_vec = Vec::new(); for (k, v) in &self.values { - tags_as_vec.push(format!("{}:{}", k, v)); + if v.is_empty() { + tags_as_vec.push(k.to_string()); + } else { + let mut a_tag = String::with_capacity(k.len() + v.len() + 1); + a_tag.push_str(k); + a_tag.push(':'); + a_tag.push_str(v); + tags_as_vec.push(a_tag); + } } tags_as_vec } @@ -256,7 +271,7 @@ pub fn id(name: Ustr, tags: &Option) -> u64 { hasher.finish() } // :::||@|#:, -// :|T|c: +// :,|T|c: // // Types: // * c -- COUNT, allows packed values, summed @@ -502,6 +517,29 @@ mod tests { assert!(parse("containerid.metric:0|c|#env:dev,client_transport:udp|c:0000000000000000000000000000000000000000000000000000000000000000").is_ok()); } + #[test] + fn parse_tag_no_value() { + let result = parse("datadog.tracer.flush_triggered:1|c|#lang:go,lang_version:go1.22.10,_dd.origin:lambda,runtime-id:d66f501c-d09b-4d0d-970f-515235c4eb56,v1.65.1,service:aws.lambda,reason:scheduled"); + assert!(result.is_ok()); + assert!(result + .unwrap() + .tags + .unwrap() + .values + .iter() + .any(|(k, v)| k == "v1.65.1" && v.is_empty())); + } + + #[test] + fn parse_tag_multi_column() { + let result = parse("datadog.tracer.flush_triggered:1|c|#lang:go:and:something:else"); + assert!(result.is_ok()); + assert_eq!( + result.unwrap().tags.unwrap().values[0], + (Ustr::from("lang"), Ustr::from("go:and:something:else")) + ); + } + #[test] fn parse_tracer_metric() { let input = "datadog.tracer.flush_duration:0.785551|ms|#lang:go,lang_version:go1.23.2,env:redacted_env,_dd.origin:lambda,runtime-id:redacted_runtime,tracer_version:v1.70.1,service:redacted_service,env:redacted_env,service:redacted_service,version:redacted_version"; From 5846df08befdae1ffca61f6bca41e998270306ff Mon Sep 17 00:00:00 2001 From: alexgallotta <5581237+alexgallotta@users.noreply.github.com> Date: Thu, 20 Feb 2025 12:23:52 -0500 Subject: [PATCH 20/28] Svls 6036 respect timeouts (#851) * tmp: disable flushing * try flushing only once on 500 error and do sketch/distro shipping in parallel * remove stale comment * avoid panic * make sure the client has timeout, or explicitely communicate failure * formatting * formatting * wait for flush to terminate * remove unused depdency * use inspect err instead of verbose match * propagate result * format * use map_err * cleanup style --- src/datadog.rs | 116 +++++++++++++++++++++---------------------------- src/flusher.rs | 66 ++++++++++++++++++++++++---- 2 files changed, 108 insertions(+), 74 deletions(-) diff --git a/src/datadog.rs b/src/datadog.rs index 41c40a51..e2641738 100644 --- a/src/datadog.rs +++ b/src/datadog.rs @@ -3,12 +3,14 @@ //!Types to serialize data into the Datadog API +use crate::flusher::ShippingError; use datadog_protos::metrics::SketchPayload; use derive_more::{Display, Into}; use lazy_static::lazy_static; use protobuf::Message; use regex::Regex; use reqwest; +use reqwest::{Client, Response}; use serde::{Serialize, Serializer}; use serde_json; use std::time::Duration; @@ -114,16 +116,16 @@ impl MetricsIntakeUrlPrefix { #[inline] fn from_site(site: Site) -> Self { - Self(format!("https://api.{}", site)) + Self(format!("https://api.{site}")) } } /// Interface for the `DogStatsD` metrics intake API. -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct DdApi { api_key: String, metrics_intake_url_prefix: MetricsIntakeUrlPrefix, - client: reqwest::Client, + client: Option, } impl DdApi { @@ -134,13 +136,11 @@ impl DdApi { https_proxy: Option, timeout: Duration, ) -> Self { - let client = match Self::build_client(https_proxy, timeout) { - Ok(client) => client, - Err(e) => { - error!("Unable to parse proxy URL, no proxy will be used. {:?}", e); - reqwest::Client::new() - } - }; + let client = build_client(https_proxy, timeout) + .inspect_err(|e| { + error!("Unable to create client {:?}", e); + }) + .ok(); DdApi { api_key, metrics_intake_url_prefix, @@ -149,40 +149,25 @@ impl DdApi { } /// Ship a serialized series to the API, blocking - pub async fn ship_series(&self, series: &Series) { - let body = serde_json::to_vec(&series).expect("failed to serialize series"); - debug!("Sending body: {:?}", &series); - + pub async fn ship_series(&self, series: &Series) -> Result { let url = format!("{}/api/v2/series", &self.metrics_intake_url_prefix); - let resp = self - .client - .post(&url) - .header("DD-API-KEY", &self.api_key) - .header("Content-Type", "application/json") - .body(body) - .send() - .await; - - match resp { - Ok(resp) => match resp.status() { - reqwest::StatusCode::ACCEPTED => {} - unexpected_status_code => { - debug!( - "{}: Failed to push to API: {:?}", - unexpected_status_code, - resp.text().await.unwrap_or_default() - ); - } - }, - Err(e) => { - debug!("500: Failed to push to API: {:?}", e); - } - }; + let safe_body = serde_json::to_vec(&series) + .map_err(|e| ShippingError::Payload(format!("Failed to serialize series: {e}")))?; + debug!("Sending body: {:?}", &series); + self.ship_data(url, safe_body, "application/json").await } - pub async fn ship_distributions(&self, sketches: &SketchPayload) { + pub async fn ship_distributions( + &self, + sketches: &SketchPayload, + ) -> Result { let url = format!("{}/api/beta/sketches", &self.metrics_intake_url_prefix); + let safe_body = sketches + .write_to_bytes() + .map_err(|e| ShippingError::Payload(format!("Failed to serialize series: {e}")))?; debug!("Sending distributions: {:?}", &sketches); + self.ship_data(url, safe_body, "application/x-protobuf") + .await // TODO maybe go to coded output stream if we incrementally // add sketch payloads to the buffer // something like this, but fix the utf-8 encoding issue @@ -192,41 +177,40 @@ impl DdApi { // let _ = output_stream.write_message_no_tag(&sketches); // TODO not working, has utf-8 encoding issue in dist-intake //} - let resp = self + } + + async fn ship_data( + &self, + url: String, + body: Vec, + content_type: &str, + ) -> Result { + let client = &self .client + .as_ref() + .ok_or_else(|| ShippingError::Destination(None, "No client".to_string()))?; + let start = std::time::Instant::now(); + + let resp = client .post(&url) .header("DD-API-KEY", &self.api_key) - .header("Content-Type", "application/x-protobuf") - .body(sketches.write_to_bytes().expect("can't write to buffer")) + .header("Content-Type", content_type) + .body(body) .send() .await; - match resp { - Ok(resp) => match resp.status() { - reqwest::StatusCode::ACCEPTED => {} - unexpected_status_code => { - debug!( - "{}: Failed to push to API: {:?}", - unexpected_status_code, - resp.text().await.unwrap_or_default() - ); - } - }, - Err(e) => { - debug!("500: Failed to push to API: {:?}", e); - } - }; + + let elapsed = start.elapsed(); + debug!("Request to {} took {}ms", url, elapsed.as_millis()); + resp.map_err(|e| ShippingError::Destination(e.status(), format!("Cannot reach {url}"))) } +} - fn build_client( - https_proxy: Option, - timeout: Duration, - ) -> Result { - let mut builder = reqwest::Client::builder().timeout(timeout); - if let Some(proxy) = https_proxy { - builder = builder.proxy(reqwest::Proxy::https(proxy)?); - } - builder.build() +fn build_client(https_proxy: Option, timeout: Duration) -> Result { + let mut builder = Client::builder().timeout(timeout); + if let Some(proxy) = https_proxy { + builder = builder.proxy(reqwest::Proxy::https(proxy)?); } + builder.build() } #[derive(Debug, Serialize, Clone, Copy)] diff --git a/src/flusher.rs b/src/flusher.rs index 98875b20..92a0d8c8 100644 --- a/src/flusher.rs +++ b/src/flusher.rs @@ -4,9 +4,10 @@ use crate::aggregator::Aggregator; use crate::datadog; use datadog::{DdApi, MetricsIntakeUrlPrefix}; +use reqwest::{Response, StatusCode}; use std::sync::{Arc, Mutex}; use std::time::Duration; -use tracing::debug; +use tracing::{debug, error}; pub struct Flusher { dd_api: DdApi, @@ -50,14 +51,63 @@ impl Flusher { debug!("Flushing {n_series} series and {n_distributions} distributions"); - // TODO: client timeout is for each invocation, so NxM times with N time series batches and - // M distro batches - for a_batch in all_series { - self.dd_api.ship_series(&a_batch).await; - // TODO(astuyve) retry and do not panic + let dd_api_clone = self.dd_api.clone(); + let series_handle = tokio::spawn(async move { + for a_batch in all_series { + let continue_shipping = + should_try_next_batch(dd_api_clone.ship_series(&a_batch).await).await; + if !continue_shipping { + break; + } + } + }); + let dd_api_clone = self.dd_api.clone(); + let distributions_handle = tokio::spawn(async move { + for a_batch in all_distributions { + let continue_shipping = + should_try_next_batch(dd_api_clone.ship_distributions(&a_batch).await).await; + if !continue_shipping { + break; + } + } + }); + + match tokio::try_join!(series_handle, distributions_handle) { + Ok(_) => { + debug!("Successfully flushed {n_series} series and {n_distributions} distributions") + } + Err(err) => { + error!("Failed to flush metrics{err}") + } + }; + } +} + +pub enum ShippingError { + Payload(String), + Destination(Option, String), +} + +async fn should_try_next_batch(resp: Result) -> bool { + match resp { + Ok(resp_payload) => match resp_payload.status() { + StatusCode::ACCEPTED => true, + unexpected_status_code => { + error!( + "{}: Failed to push to API: {:?}", + unexpected_status_code, + resp_payload.text().await.unwrap_or_default() + ); + true + } + }, + Err(ShippingError::Payload(msg)) => { + error!("Failed to prepare payload. Data dropped: {}", msg); + true } - for a_batch in all_distributions { - self.dd_api.ship_distributions(&a_batch).await; + Err(ShippingError::Destination(sc, msg)) => { + error!("Error shipping data: {:?} {}", sc, msg); + false } } } From 163b0813a1504c1067d2f60d59352fd5cfd615eb Mon Sep 17 00:00:00 2001 From: AJ Stuyvenberg Date: Thu, 27 Feb 2025 16:09:53 -0500 Subject: [PATCH 21/28] feat(dogstatsd): compress metrics (#901) Co-authored-by: Levi Morrison --- Cargo.toml | 1 + src/datadog.rs | 25 ++++++++++++++++++++----- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 0e98a17d..112cd101 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,6 +25,7 @@ tokio = { version = "1.37.0", default-features = false, features = ["macros", "r tokio-util = { version = "0.7.11", default-features = false } tracing = { version = "0.1.40", default-features = false } regex = { version = "1.10.6", default-features = false } +zstd = { version = "0.13.3", default-features = false } [dev-dependencies] mockito = { version = "1.5.0", default-features = false } diff --git a/src/datadog.rs b/src/datadog.rs index e2641738..0b41cbb7 100644 --- a/src/datadog.rs +++ b/src/datadog.rs @@ -13,8 +13,10 @@ use reqwest; use reqwest::{Client, Response}; use serde::{Serialize, Serializer}; use serde_json; +use std::io::Write; use std::time::Duration; use tracing::{debug, error}; +use zstd::stream::write::Encoder; lazy_static! { static ref SITE_RE: Regex = Regex::new(r"^[a-zA-Z0-9._:-]+$").expect("invalid regex"); @@ -191,13 +193,26 @@ impl DdApi { .ok_or_else(|| ShippingError::Destination(None, "No client".to_string()))?; let start = std::time::Instant::now(); - let resp = client + let result = (|| -> std::io::Result> { + let mut encoder = Encoder::new(Vec::new(), 6)?; + encoder.write_all(&body)?; + encoder.finish() + })(); + + let mut builder = client .post(&url) .header("DD-API-KEY", &self.api_key) - .header("Content-Type", content_type) - .body(body) - .send() - .await; + .header("Content-Type", content_type); + + builder = match result { + Ok(compressed) => builder.header("Content-Encoding", "zstd").body(compressed), + Err(err) => { + debug!("Sending uncompressed data, failed to compress: {err}"); + builder.body(body) + } + }; + + let resp = builder.send().await; let elapsed = start.elapsed(); debug!("Request to {} took {}ms", url, elapsed.as_millis()); From 6fab75860c8bac030f49a5427252896c5be19c94 Mon Sep 17 00:00:00 2001 From: AJ Stuyvenberg Date: Fri, 28 Feb 2025 16:39:01 -0500 Subject: [PATCH 22/28] Aj/honor metric timestamps (#904) * feat: set timestamp when metric is parsed * feat: Ten second rolling buckets * debuggin * remove debug line * fix: remove time from aggr * feat: Clean up parse code, refactor floored buckets to metrics * feat: comment docs * feat: Cleanup impl * fix: call method --- src/aggregator.rs | 101 +++++++++++++++++++++++++--------------------- src/dogstatsd.rs | 38 ++++++++++++----- src/metric.rs | 91 ++++++++++++++++++++++++++++++++++++----- 3 files changed, 166 insertions(+), 64 deletions(-) diff --git a/src/aggregator.rs b/src/aggregator.rs index 196a9b76..f31a3b0f 100644 --- a/src/aggregator.rs +++ b/src/aggregator.rs @@ -7,7 +7,6 @@ use crate::constants; use crate::datadog::{self, Metric as MetricToShip, Series}; use crate::errors; use crate::metric::{self, Metric, MetricValue, SortedTags}; -use std::time; use datadog_protos::metrics::{Dogsketch, Sketch, SketchPayload}; use ddsketch_agent::DDSketch; @@ -91,13 +90,14 @@ impl Aggregator { /// Function will return overflow error if more than /// `min(constants::MAX_CONTEXTS, CONTEXTS)` is exceeded. pub fn insert(&mut self, metric: Metric) -> Result<(), errors::Insert> { - let id = metric::id(metric.name, &metric.tags); + let id = metric::id(metric.name, &metric.tags, metric.timestamp); let len = self.map.len(); - match self - .map - .entry(id, |m| m.id == id, |m| metric::id(m.name, &m.tags)) - { + match self.map.entry( + id, + |m| m.id == id, + |m| metric::id(m.name, &m.tags, m.timestamp), + ) { hash_table::Entry::Vacant(entry) => { if len >= self.max_context { return Err(errors::Insert::Overflow); @@ -117,19 +117,13 @@ impl Aggregator { #[must_use] pub fn distributions_to_protobuf(&self) -> SketchPayload { - let now = time::UNIX_EPOCH - .elapsed() - .expect("unable to poll clock, unrecoverable") - .as_secs() - .try_into() - .unwrap_or_default(); let mut sketch_payload = SketchPayload::new(); sketch_payload.sketches = self .map .iter() .filter_map(|entry| match entry.value { - MetricValue::Distribution(_) => build_sketch(now, entry, self.tags.clone()), + MetricValue::Distribution(_) => build_sketch(entry, self.tags.clone()), _ => None, }) .collect(); @@ -138,12 +132,6 @@ impl Aggregator { #[must_use] pub fn consume_distributions(&mut self) -> Vec { - let now = time::UNIX_EPOCH - .elapsed() - .expect("unable to poll clock, unrecoverable") - .as_secs() - .try_into() - .unwrap_or_default(); let mut batched_payloads = Vec::new(); let mut sketch_payload = SketchPayload::new(); let mut this_batch_size = 0u64; @@ -155,7 +143,7 @@ impl Aggregator { } false }) - .filter_map(|entry| build_sketch(now, &entry, self.tags.clone())) + .filter_map(|entry| build_sketch(&entry, self.tags.clone())) { let next_chunk_size = sketch.compute_size(); @@ -248,18 +236,23 @@ impl Aggregator { batched_payloads } - pub fn get_entry_by_id(&self, name: Ustr, tags: &Option) -> Option<&Metric> { - let id = metric::id(name, tags); + pub fn get_entry_by_id( + &self, + name: Ustr, + tags: &Option, + timestamp: i64, + ) -> Option<&Metric> { + let id = metric::id(name, tags, timestamp); self.map.find(id, |m| m.id == id) } } -fn build_sketch(now: i64, entry: &Metric, mut base_tag_vec: SortedTags) -> Option { +fn build_sketch(entry: &Metric, mut base_tag_vec: SortedTags) -> Option { let sketch = entry.value.get_sketch()?; let mut dogsketch = Dogsketch::default(); sketch.merge_to_dogsketch(&mut dogsketch); // TODO(Astuyve) allow users to specify timestamp - dogsketch.set_ts(now); + dogsketch.set_ts(entry.timestamp); let mut sketch = Sketch::default(); sketch.set_dogsketches(vec![dogsketch]); let name = entry.name.to_string(); @@ -286,10 +279,7 @@ fn build_metric(entry: &Metric, mut base_tag_vec: SortedTags) -> Option, metric_id: &str, value: f64) { + pub fn assert_sketch( + aggregator_mutex: &Mutex, + metric_id: &str, + value: f64, + timestamp: i64, + ) { let aggregator = aggregator_mutex.lock().unwrap(); - if let Some(e) = aggregator.get_entry_by_id(metric_id.into(), &None) { + if let Some(e) = aggregator.get_entry_by_id(metric_id.into(), &None, timestamp) { let metric = e.value.get_sketch().unwrap(); assert!((metric.max().unwrap() - value).abs() < PRECISION); assert!((metric.min().unwrap() - value).abs() < PRECISION); @@ -387,14 +385,20 @@ pub mod tests { #[cfg_attr(miri, ignore)] fn overflow() { let mut aggregator = Aggregator::new(EMPTY_TAGS, 2).unwrap(); - + let mut now = std::time::UNIX_EPOCH + .elapsed() + .expect("unable to poll clock, unrecoverable") + .as_secs() + .try_into() + .unwrap_or_default(); + now = (now / 10) * 10; let metric1 = parse("test:1|c|#k:v").expect("metric parse failed"); let metric2 = parse("foo:1|c|#k:v").expect("metric parse failed"); let metric3 = parse("bar:1|c|#k:v").expect("metric parse failed"); - let id1 = metric::id(metric1.name, &metric1.tags); - let id2 = metric::id(metric2.name, &metric2.tags); - let id3 = metric::id(metric3.name, &metric3.tags); + let id1 = metric::id(metric1.name, &metric1.tags, now); + let id2 = metric::id(metric2.name, &metric2.tags, now); + let id3 = metric::id(metric3.name, &metric3.tags, now); assert_ne!(id1, id2); assert_ne!(id1, id3); @@ -417,25 +421,30 @@ pub mod tests { #[cfg_attr(miri, ignore)] fn clear() { let mut aggregator = Aggregator::new(EMPTY_TAGS, 2).unwrap(); - - let metric1 = parse("test:3|c|#k1:v1").expect("metric parse failed"); - let metric2 = parse("foo:5|c|#k2:v2").expect("metric parse failed"); + let mut now = 1656581409; + now = (now / 10) * 10; + let metric1 = parse("test:3|c|#k1:v1|T1656581409").expect("metric parse failed"); + let metric2 = parse("foo:5|c|#k2:v2|T1656581409").expect("metric parse failed"); assert!(aggregator.insert(metric1).is_ok()); assert!(aggregator.insert(metric2).is_ok()); assert_eq!(aggregator.map.len(), 2); - if let Some(v) = - aggregator.get_entry_by_id("foo".into(), &Some(SortedTags::parse("k2:v2").unwrap())) - { + if let Some(v) = aggregator.get_entry_by_id( + "foo".into(), + &Some(SortedTags::parse("k2:v2").unwrap()), + now, + ) { assert_eq!(v.value.get_value().unwrap(), 5f64); } else { panic!("failed to get value by id"); } - if let Some(v) = - aggregator.get_entry_by_id("test".into(), &Some(SortedTags::parse("k1:v1").unwrap())) - { + if let Some(v) = aggregator.get_entry_by_id( + "test".into(), + &Some(SortedTags::parse("k1:v1").unwrap()), + now, + ) { assert_eq!(v.value.get_value().unwrap(), 3f64); } else { panic!("failed to get value by id"); diff --git a/src/dogstatsd.rs b/src/dogstatsd.rs index 41456966..236c82d0 100644 --- a/src/dogstatsd.rs +++ b/src/dogstatsd.rs @@ -136,9 +136,9 @@ mod tests { #[cfg_attr(miri, ignore)] async fn test_dogstatsd_multi_distribution() { let locked_aggregator = setup_dogstatsd( - "single_machine_performance.rouster.api.series_v2.payload_size_bytes:269942|d -single_machine_performance.rouster.metrics_min_timestamp_latency:1426.90870216|d -single_machine_performance.rouster.metrics_max_timestamp_latency:1376.90870216|d + "single_machine_performance.rouster.api.series_v2.payload_size_bytes:269942|d|T1656581409 +single_machine_performance.rouster.metrics_min_timestamp_latency:1426.90870216|d|T1656581409 +single_machine_performance.rouster.metrics_max_timestamp_latency:1376.90870216|d|T1656581409 ", ) .await; @@ -154,24 +154,38 @@ single_machine_performance.rouster.metrics_max_timestamp_latency:1376.90870216|d &locked_aggregator, "single_machine_performance.rouster.api.series_v2.payload_size_bytes", 269_942_f64, + 1656581400, ); assert_sketch( &locked_aggregator, "single_machine_performance.rouster.metrics_min_timestamp_latency", 1_426.908_702_16, + 1656581400, ); assert_sketch( &locked_aggregator, "single_machine_performance.rouster.metrics_max_timestamp_latency", 1_376.908_702_16, + 1656581400, ); } #[tokio::test] #[cfg_attr(miri, ignore)] async fn test_dogstatsd_multi_metric() { + let mut now = std::time::UNIX_EPOCH + .elapsed() + .expect("unable to poll clock, unrecoverable") + .as_secs() + .try_into() + .unwrap_or_default(); + now = (now / 10) * 10; let locked_aggregator = setup_dogstatsd( - "metric3:3|c|#tag3:val3,tag4:val4\nmetric1:1|c\nmetric2:2|c|#tag2:val2\n", + format!( + "metric3:3|c|#tag3:val3,tag4:val4\nmetric1:1|c\nmetric2:2|c|#tag2:val2|T{:}\n", + now + ) + .as_str(), ) .await; let aggregator = locked_aggregator.lock().expect("lock poisoned"); @@ -182,15 +196,21 @@ single_machine_performance.rouster.metrics_max_timestamp_latency:1376.90870216|d assert_eq!(aggregator.distributions_to_protobuf().sketches.len(), 0); drop(aggregator); - assert_value(&locked_aggregator, "metric1", 1.0, ""); - assert_value(&locked_aggregator, "metric2", 2.0, "tag2:val2"); - assert_value(&locked_aggregator, "metric3", 3.0, "tag3:val3,tag4:val4"); + assert_value(&locked_aggregator, "metric1", 1.0, "", now); + assert_value(&locked_aggregator, "metric2", 2.0, "tag2:val2", now); + assert_value( + &locked_aggregator, + "metric3", + 3.0, + "tag3:val3,tag4:val4", + now, + ); } #[tokio::test] #[cfg_attr(miri, ignore)] async fn test_dogstatsd_single_metric() { - let locked_aggregator = setup_dogstatsd("metric123:99123|c").await; + let locked_aggregator = setup_dogstatsd("metric123:99123|c|T1656581409").await; let aggregator = locked_aggregator.lock().expect("lock poisoned"); let parsed_metrics = aggregator.to_series(); @@ -198,7 +218,7 @@ single_machine_performance.rouster.metrics_max_timestamp_latency:1376.90870216|d assert_eq!(aggregator.distributions_to_protobuf().sketches.len(), 0); drop(aggregator); - assert_value(&locked_aggregator, "metric123", 99_123.0, ""); + assert_value(&locked_aggregator, "metric123", 99_123.0, "", 1656581400); } #[tokio::test] diff --git a/src/metric.rs b/src/metric.rs index dcfb23c6..f37943af 100644 --- a/src/metric.rs +++ b/src/metric.rs @@ -13,9 +13,10 @@ use ustr::Ustr; pub const EMPTY_TAGS: SortedTags = SortedTags { values: Vec::new() }; +// https://docs.datadoghq.com/developers/dogstatsd/datagram_shell?tab=metrics#dogstatsd-protocol-v13 lazy_static! { static ref METRIC_REGEX: Regex = Regex::new( - r"^(?P[^:]+):(?P[^|]+)\|(?P[a-zA-Z]+)(?:\|@(?P[\d.]+))?(?:\|#(?P[^|]+))?(?:\|c:(?P[^|]+))?$", + r"^(?P[^:]+):(?P[^|]+)\|(?P[a-zA-Z]+)(?:\|@(?P[\d.]+))?(?:\|#(?P[^|]+))?(?:\|c:(?P[^|]+))?(?:\|T(?P[^|]+))?$", ).expect("Failed to create metric regex"); } @@ -169,20 +170,52 @@ pub struct Metric { /// ID given a name and tagset. pub id: u64, + // Timestamp + pub timestamp: i64, } impl Metric { - pub fn new(name: Ustr, value: MetricValue, tags: Option) -> Metric { - let id = id(name, &tags); + pub fn new( + name: Ustr, + value: MetricValue, + tags: Option, + timestamp: Option, + ) -> Metric { + let parsed_timestamp = timestamp_to_bucket(timestamp.unwrap_or_else(|| { + std::time::UNIX_EPOCH + .elapsed() + .expect("unable to poll clock, unrecoverable") + .as_secs() + .try_into() + .unwrap_or_default() + })); + + let id = id(name, &tags, parsed_timestamp); Metric { name, value, tags, id, + timestamp: parsed_timestamp, } } } +// Round down to the nearest 10 seconds +// to form a bucket of metric contexts aggregated per 10s +pub fn timestamp_to_bucket(timestamp: i64) -> i64 { + let now_seconds: i64 = std::time::UNIX_EPOCH + .elapsed() + .expect("unable to poll clock, unrecoverable") + .as_secs() + .try_into() + .unwrap_or_default(); + if timestamp > now_seconds { + return (now_seconds / 10) * 10; + } + (timestamp / 10) * 10 +} + /// Parse a metric from given input. /// /// This function parses a passed `&str` into a `Metric`. We assume that @@ -207,6 +240,17 @@ pub fn parse(input: &str) -> Result { } let val = first_value(caps.name("values").unwrap().as_str())?; let t = caps.name("type").unwrap().as_str(); + let now = std::time::UNIX_EPOCH + .elapsed() + .expect("unable to poll clock, unrecoverable") + .as_secs() + .try_into() + .unwrap_or_default(); + // let Metric::new() handle bucketing the timestamp + let parsed_timestamp: i64 = match caps.name("timestamp") { + Some(ts) => timestamp_to_bucket(ts.as_str().parse().unwrap_or(now)), + None => timestamp_to_bucket(now), + }; let metric_value = match t { "c" => MetricValue::Count(val), "g" => MetricValue::Gauge(val), @@ -223,12 +267,13 @@ pub fn parse(input: &str) -> Result { } }; let name = Ustr::from(caps.name("name").unwrap().as_str()); - let id = id(name, &tags); + let id = id(name, &tags, parsed_timestamp); return Ok(Metric { name, value: metric_value, tags, id, + timestamp: parsed_timestamp, }); } Err(ParseError::Raw(format!("Invalid metric format {input}"))) @@ -258,10 +303,11 @@ fn first_value(values: &str) -> Result { /// from the point of view of this function. #[inline] #[must_use] -pub fn id(name: Ustr, tags: &Option) -> u64 { +pub fn id(name: Ustr, tags: &Option, timestamp: i64) -> u64 { let mut hasher = FnvHasher::default(); name.hash(&mut hasher); + timestamp.hash(&mut hasher); if let Some(tags_present) = tags { for kv in tags_present.values.iter() { kv.0.as_bytes().hash(&mut hasher); @@ -285,7 +331,7 @@ mod tests { use proptest::{collection, option, strategy::Strategy, string::string_regex}; use ustr::Ustr; - use crate::metric::{id, parse, MetricValue, SortedTags}; + use crate::metric::{id, parse, timestamp_to_bucket, MetricValue, SortedTags}; use super::ParseError; @@ -412,7 +458,6 @@ mod tests { let result = parse(&input); let verify = result.unwrap_err().to_string(); - println!("{}", verify); assert!(verify.starts_with("parse failure: Invalid metric format ")); } @@ -447,6 +492,7 @@ mod tests { mut tags in metric_tags()) { let mut tagset1 = String::new(); let mut tagset2 = String::new(); + let now = timestamp_to_bucket(std::time::UNIX_EPOCH.elapsed().expect("unable to poll clock, unrecoverable").as_secs().try_into().unwrap_or_default()); for (k,v) in &tags { tagset1.push_str(k); @@ -466,8 +512,8 @@ mod tests { tagset2.pop(); } - let id1 = id(Ustr::from(&name), &Some(SortedTags::parse(&tagset1).unwrap())); - let id2 = id(Ustr::from(&name), &Some(SortedTags::parse(&tagset2).unwrap())); + let id1 = id(Ustr::from(&name), &Some(SortedTags::parse(&tagset1).unwrap()), now); + let id2 = id(Ustr::from(&name), &Some(SortedTags::parse(&tagset2).unwrap()), now); assert_eq!(id1, id2); } @@ -551,6 +597,33 @@ mod tests { } } + #[test] + fn parse_metric_timestamp() { + // Important to test that we round down to the nearest 10 seconds + // for our buckets + let input = "page.views:15|c|#env:dev|T1656581409"; + let metric = parse(input).unwrap(); + assert_eq!(metric.timestamp, 1656581400); + } + + #[test] + fn parse_metric_no_timestamp() { + // *wince* this could be a race condition + // we round the timestamp down to a 10s bucket and I want to test now + // but if the timestamp rolls over to the next bucket time and the test + // is somehow slower than 1s then the test will fail. + // come bug me if I wrecked your CI run + let input = "page.views:15|c|#env:dev"; + let metric = parse(input).unwrap(); + let now: i64 = std::time::UNIX_EPOCH + .elapsed() + .expect("unable to poll clock, unrecoverable") + .as_secs() + .try_into() + .unwrap_or_default(); + assert_eq!(metric.timestamp, (now / 10) * 10); + } + #[test] fn sorting_tags() { let mut tags = SortedTags::parse("z:z0,b:b2,c:c3").unwrap(); From 6703eba9695fc25b5f73fbad43d29ec19e1c1bf6 Mon Sep 17 00:00:00 2001 From: Edmund Kump Date: Fri, 14 Mar 2025 12:58:20 -0400 Subject: [PATCH 23/28] ekump/APMSP-1827 add warnings for panics (#915) Add clippy warnings and allows for panic macros to most crates Add an extension crate for mutex in ddcommon to isolate the unwrap to one location in code to avoid the allow annotations. We aren't going to stop unwrapping mutexes anytime soon. Replace use of lazy_static with OnceLock for ddcommon, ddtelemetry, live-debugger, tools, sidecar --- Cargo.toml | 1 - src/datadog.rs | 21 ++++++++++++++------- src/dogstatsd.rs | 8 ++++++++ src/flusher.rs | 1 + src/lib.rs | 6 ++++++ src/metric.rs | 26 ++++++++++++++++++++------ 6 files changed, 49 insertions(+), 14 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 112cd101..4e321887 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,7 +16,6 @@ hashbrown = { version = "0.14.3", default-features = false, features = ["inline- protobuf = { version = "3.5.0", default-features = false } ustr = { version = "1.0.0", default-features = false } fnv = { version = "1.0.7", default-features = false } -lazy_static = { version = "1.5.0", default-features = false } reqwest = { version = "0.12.4", features = ["json", "http2", "rustls-tls"], default-features = false } serde = { version = "1.0.197", default-features = false, features = ["derive"] } serde_json = { version = "1.0.116", default-features = false, features = ["alloc"] } diff --git a/src/datadog.rs b/src/datadog.rs index 0b41cbb7..3cfd7ab0 100644 --- a/src/datadog.rs +++ b/src/datadog.rs @@ -6,7 +6,6 @@ use crate::flusher::ShippingError; use datadog_protos::metrics::SketchPayload; use derive_more::{Display, Into}; -use lazy_static::lazy_static; use protobuf::Message; use regex::Regex; use reqwest; @@ -14,14 +13,21 @@ use reqwest::{Client, Response}; use serde::{Serialize, Serializer}; use serde_json; use std::io::Write; +use std::sync::OnceLock; use std::time::Duration; use tracing::{debug, error}; use zstd::stream::write::Encoder; -lazy_static! { - static ref SITE_RE: Regex = Regex::new(r"^[a-zA-Z0-9._:-]+$").expect("invalid regex"); - static ref URL_PREFIX_RE: Regex = - Regex::new(r"^https?://[a-zA-Z0-9._:-]+$").expect("invalid regex"); +// TODO: Move to the more ergonomic LazyLock when MSRV is 1.80 +static SITE_RE: OnceLock = OnceLock::new(); +fn get_site_re() -> &'static Regex { + #[allow(clippy::expect_used)] + SITE_RE.get_or_init(|| Regex::new(r"^[a-zA-Z0-9._:-]+$").expect("invalid regex")) +} +static URL_PREFIX_RE: OnceLock = OnceLock::new(); +fn get_url_prefix_re() -> &'static Regex { + #[allow(clippy::expect_used)] + URL_PREFIX_RE.get_or_init(|| Regex::new(r"^https?://[a-zA-Z0-9._:-]+$").expect("invalid regex")) } #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Display, Into)] @@ -36,7 +42,7 @@ impl Site { // Datadog sites are generally domain names. In particular, they shouldn't have any slashes // in them. We expect this to be coming from a `DD_SITE` environment variable or the `site` // config field. - if SITE_RE.is_match(&site) { + if get_site_re().is_match(&site) { Ok(Site(site)) } else { Err(SiteError(site)) @@ -49,7 +55,7 @@ impl Site { pub struct UrlPrefixError(String); fn validate_url_prefix(prefix: &str) -> Result<(), UrlPrefixError> { - if URL_PREFIX_RE.is_match(prefix) { + if get_url_prefix_re().is_match(prefix) { Ok(()) } else { Err(UrlPrefixError(prefix.to_owned())) @@ -111,6 +117,7 @@ impl MetricsIntakeUrlPrefix { #[inline] fn new_expect_validated(validated_prefix: String) -> Self { + #[allow(clippy::expect_used)] validate_url_prefix(&validated_prefix).expect("Invalid URL prefix"); Self(validated_prefix) diff --git a/src/dogstatsd.rs b/src/dogstatsd.rs index 236c82d0..ea8b3c80 100644 --- a/src/dogstatsd.rs +++ b/src/dogstatsd.rs @@ -35,6 +35,8 @@ impl BufferReader { // Max buffer size is configurable in Go Agent and the default is 8KB // https://github.com/DataDog/datadog-agent/blob/85939a62b5580b2a15549f6936f257e61c5aa153/pkg/config/config_template.yaml#L2154-L2158 let mut buf = [0; 8192]; + + #[allow(clippy::expect_used)] let (amt, src) = socket .recv_from(&mut buf) .await @@ -54,7 +56,9 @@ impl DogStatsD { cancel_token: tokio_util::sync::CancellationToken, ) -> DogStatsD { let addr = format!("{}:{}", config.host, config.port); + // TODO (UDS socket) + #[allow(clippy::expect_used)] let socket = tokio::net::UdpSocket::bind(addr) .await .expect("couldn't bind to address"); @@ -74,11 +78,14 @@ impl DogStatsD { } async fn consume_statsd(&self) { + #[allow(clippy::expect_used)] let (buf, src) = self .buffer_reader .read() .await .expect("didn't receive data"); + + #[allow(clippy::expect_used)] let msgs = std::str::from_utf8(&buf).expect("couldn't parse as string"); debug!("Received message: {} from {}", msgs, src); let statsd_metric_strings = msgs.split('\n'); @@ -112,6 +119,7 @@ impl DogStatsD { }) .collect(); if !all_valid_metrics.is_empty() { + #[allow(clippy::expect_used)] let mut guarded_aggregator = self.aggregator.lock().expect("lock poisoned"); for a_valid_value in all_valid_metrics { let _ = guarded_aggregator.insert(a_valid_value); diff --git a/src/flusher.rs b/src/flusher.rs index 92a0d8c8..66ecc340 100644 --- a/src/flusher.rs +++ b/src/flusher.rs @@ -39,6 +39,7 @@ impl Flusher { pub async fn flush(&mut self) { let (all_series, all_distributions) = { + #[allow(clippy::expect_used)] let mut aggregator = self.aggregator.lock().expect("lock poisoned"); ( aggregator.consume_metrics(), diff --git a/src/lib.rs b/src/lib.rs index fe9467c0..4009db14 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,12 @@ // Copyright 2023-Present Datadog, Inc. https://www.datadoghq.com/ // SPDX-License-Identifier: Apache-2.0 +#![cfg_attr(not(test), deny(clippy::panic))] +#![cfg_attr(not(test), deny(clippy::unwrap_used))] +#![cfg_attr(not(test), deny(clippy::expect_used))] +#![cfg_attr(not(test), deny(clippy::todo))] +#![cfg_attr(not(test), deny(clippy::unimplemented))] + pub mod aggregator; pub mod constants; pub mod datadog; diff --git a/src/metric.rs b/src/metric.rs index f37943af..e4a67dc6 100644 --- a/src/metric.rs +++ b/src/metric.rs @@ -5,19 +5,24 @@ use crate::errors::ParseError; use crate::{constants, datadog}; use ddsketch_agent::DDSketch; use fnv::FnvHasher; -use lazy_static::lazy_static; use protobuf::Chars; use regex::Regex; use std::hash::{Hash, Hasher}; +use std::sync::OnceLock; use ustr::Ustr; pub const EMPTY_TAGS: SortedTags = SortedTags { values: Vec::new() }; // https://docs.datadoghq.com/developers/dogstatsd/datagram_shell?tab=metrics#dogstatsd-protocol-v13 -lazy_static! { - static ref METRIC_REGEX: Regex = Regex::new( - r"^(?P[^:]+):(?P[^|]+)\|(?P[a-zA-Z]+)(?:\|@(?P[\d.]+))?(?:\|#(?P[^|]+))?(?:\|c:(?P[^|]+))?(?:\|T(?P[^|]+))?$", - ).expect("Failed to create metric regex"); +static METRIC_REGEX: OnceLock = OnceLock::new(); +fn get_metric_regex() -> &'static Regex { + #[allow(clippy::expect_used)] + METRIC_REGEX.get_or_init(|| { + Regex::new( + r"^(?P[^:]+):(?P[^|]+)\|(?P[a-zA-Z]+)(?:\|@(?P[\d.]+))?(?:\|#(?P[^|]+))?(?:\|c:(?P[^|]+))?(?:\|T(?P[^|]+))?$", + ) + .expect("Failed to create metric regex") + }) } #[derive(Clone, Debug)] @@ -181,6 +186,7 @@ impl Metric { tags: Option, timestamp: Option, ) -> Metric { + #[allow(clippy::expect_used)] let parsed_timestamp = timestamp_to_bucket(timestamp.unwrap_or_else(|| { std::time::UNIX_EPOCH .elapsed() @@ -204,6 +210,7 @@ impl Metric { // Round down to the nearest 10 seconds // to form a bucket of metric contexts aggregated per 10s pub fn timestamp_to_bucket(timestamp: i64) -> i64 { + #[allow(clippy::expect_used)] let now_seconds: i64 = std::time::UNIX_EPOCH .elapsed() .expect("unable to poll clock, unrecoverable") @@ -228,7 +235,7 @@ pub fn timestamp_to_bucket(timestamp: i64) -> i64 { /// example aj-test.increment:1|c|#user:aj-test from 127.0.0.1:50983 pub fn parse(input: &str) -> Result { // TODO must enforce / exploit constraints given in `constants`. - if let Some(caps) = METRIC_REGEX.captures(input) { + if let Some(caps) = get_metric_regex().captures(input) { // unused for now // let sample_rate = caps.name("sample_rate").map(|m| m.as_str()); @@ -238,8 +245,14 @@ pub fn parse(input: &str) -> Result { } else { tags = None; } + + #[allow(clippy::unwrap_used)] let val = first_value(caps.name("values").unwrap().as_str())?; + + #[allow(clippy::unwrap_used)] let t = caps.name("type").unwrap().as_str(); + + #[allow(clippy::expect_used)] let now = std::time::UNIX_EPOCH .elapsed() .expect("unable to poll clock, unrecoverable") @@ -266,6 +279,7 @@ pub fn parse(input: &str) -> Result { return Err(ParseError::Raw(format!("Invalid metric type: {t}"))); } }; + #[allow(clippy::unwrap_used)] let name = Ustr::from(caps.name("name").unwrap().as_str()); let id = id(name, &tags, parsed_timestamp); return Ok(Metric { From a34de4c73938e9b3453fb77f00de9faa502e2ee8 Mon Sep 17 00:00:00 2001 From: AJ Stuyvenberg Date: Wed, 19 Mar 2025 12:51:30 -0400 Subject: [PATCH 24/28] Push retry strategy into client (#940) (serverless) feature: Push retry strategy to libdatadog to avoid re-compressing --- src/datadog.rs | 62 +++++++++++++- src/flusher.rs | 5 +- tests/integration_test.rs | 168 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 231 insertions(+), 4 deletions(-) diff --git a/src/datadog.rs b/src/datadog.rs index 3cfd7ab0..4bbcd13d 100644 --- a/src/datadog.rs +++ b/src/datadog.rs @@ -135,6 +135,7 @@ pub struct DdApi { api_key: String, metrics_intake_url_prefix: MetricsIntakeUrlPrefix, client: Option, + retry_strategy: RetryStrategy, } impl DdApi { @@ -144,6 +145,7 @@ impl DdApi { metrics_intake_url_prefix: MetricsIntakeUrlPrefix, https_proxy: Option, timeout: Duration, + retry_strategy: RetryStrategy, ) -> Self { let client = build_client(https_proxy, timeout) .inspect_err(|e| { @@ -154,6 +156,7 @@ impl DdApi { api_key, metrics_intake_url_prefix, client, + retry_strategy, } } @@ -219,12 +222,67 @@ impl DdApi { } }; - let resp = builder.send().await; + let resp = self.send_with_retry(builder).await; let elapsed = start.elapsed(); debug!("Request to {} took {}ms", url, elapsed.as_millis()); - resp.map_err(|e| ShippingError::Destination(e.status(), format!("Cannot reach {url}"))) + resp } + + async fn send_with_retry( + &self, + builder: reqwest::RequestBuilder, + ) -> Result { + let mut attempts = 0; + loop { + attempts += 1; + let cloned_builder = match builder.try_clone() { + Some(b) => b, + None => { + return Err(ShippingError::Destination( + None, + "Failed to clone request".to_string(), + )); + } + }; + + let response = cloned_builder.send().await; + match response { + Ok(response) if response.status().is_success() => { + return Ok(response); + } + _ => {} + } + + match self.retry_strategy { + RetryStrategy::LinearBackoff(max_attempts, _) + | RetryStrategy::Immediate(max_attempts) + if attempts >= max_attempts => + { + let status = match response { + Ok(response) => Some(response.status()), + Err(err) => err.status(), + }; + // handle if status code missing like timeout + return Err(ShippingError::Destination( + status, + format!("Failed to send request after {} attempts", max_attempts) + .to_string(), + )); + } + RetryStrategy::LinearBackoff(_, delay) => { + tokio::time::sleep(Duration::from_millis(delay)).await; + } + _ => {} + } + } + } +} + +#[derive(Debug, Clone)] +pub enum RetryStrategy { + Immediate(u64), // attempts + LinearBackoff(u64, u64), // attempts, delay } fn build_client(https_proxy: Option, timeout: Duration) -> Result { diff --git a/src/flusher.rs b/src/flusher.rs index 66ecc340..35a3f8c3 100644 --- a/src/flusher.rs +++ b/src/flusher.rs @@ -2,8 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 use crate::aggregator::Aggregator; -use crate::datadog; -use datadog::{DdApi, MetricsIntakeUrlPrefix}; +use crate::datadog::{DdApi, MetricsIntakeUrlPrefix, RetryStrategy}; use reqwest::{Response, StatusCode}; use std::sync::{Arc, Mutex}; use std::time::Duration; @@ -20,6 +19,7 @@ pub struct FlusherConfig { pub metrics_intake_url_prefix: MetricsIntakeUrlPrefix, pub https_proxy: Option, pub timeout: Duration, + pub retry_strategy: RetryStrategy, } #[allow(clippy::await_holding_lock)] @@ -30,6 +30,7 @@ impl Flusher { config.metrics_intake_url_prefix, config.https_proxy, config.timeout, + config.retry_strategy, ); Flusher { dd_api, diff --git a/tests/integration_test.rs b/tests/integration_test.rs index 54007b35..641c3ebd 100644 --- a/tests/integration_test.rs +++ b/tests/integration_test.rs @@ -21,6 +21,8 @@ use tokio_util::sync::CancellationToken; #[cfg(not(miri))] #[tokio::test] async fn dogstatsd_server_ships_series() { + use dogstatsd::datadog::RetryStrategy; + let mut mock_server = Server::new_async().await; let mock = mock_server @@ -51,6 +53,7 @@ async fn dogstatsd_server_ships_series() { .expect("failed to create URL"), https_proxy: None, timeout: std::time::Duration::from_secs(5), + retry_strategy: RetryStrategy::Immediate(3), }); let server_address = "127.0.0.1:18125"; @@ -98,3 +101,168 @@ async fn start_dogstatsd(metrics_aggr: &Arc>) -> Cancel dogstatsd_cancel_token } + +#[cfg(test)] +#[cfg(not(miri))] +#[tokio::test] +async fn test_send_with_retry_immediate_failure() { + use dogstatsd::datadog::{DdApi, DdDdUrl, RetryStrategy}; + use dogstatsd::metric::{parse, SortedTags}; + + let mut server = Server::new_async().await; + let mock = server + .mock("POST", "/api/v2/series") + .with_status(500) + .with_body("Internal Server Error") + .expect(3) + .create_async() + .await; + + let retry_strategy = RetryStrategy::Immediate(3); + let dd_api = DdApi::new( + "test_key".to_string(), + MetricsIntakeUrlPrefix::new( + None, + MetricsIntakeUrlPrefixOverride::maybe_new( + None, + Some(DdDdUrl::new(server.url()).expect("failed to create URL")), + ), + ) + .expect("failed to create URL"), + None, + Duration::from_secs(1), + retry_strategy.clone(), + ); + + // Create a series using the Aggregator + let mut aggregator = MetricsAggregator::new(SortedTags::parse("test:value").unwrap(), 1) + .expect("failed to create aggregator"); + let metric = parse("test:1|c").expect("failed to parse metric"); + aggregator.insert(metric).expect("failed to insert metric"); + let series = aggregator.to_series(); + + let result = dd_api.ship_series(&series).await; + + // The result should be an error since we got a 500 response + assert!(result.is_err()); + + // Verify that the mock was called exactly 3 times + mock.assert_async().await; +} + +#[cfg(test)] +#[cfg(not(miri))] +#[tokio::test] +async fn test_send_with_retry_linear_backoff_success() { + use dogstatsd::datadog::{DdApi, DdDdUrl, RetryStrategy}; + use dogstatsd::metric::{parse, SortedTags}; + + let mut server = Server::new_async().await; + let mock = server + .mock("POST", "/api/v2/series") + .with_status(500) + .with_body("Internal Server Error") + .expect(1) + .create_async() + .await; + + let success_mock = server + .mock("POST", "/api/v2/series") + .with_status(200) + .with_body("Success") + .expect(1) + .create_async() + .await; + + let retry_strategy = RetryStrategy::LinearBackoff(3, 1); // 3 attempts, 1ms delay + let dd_api = DdApi::new( + "test_key".to_string(), + MetricsIntakeUrlPrefix::new( + None, + MetricsIntakeUrlPrefixOverride::maybe_new( + None, + Some(DdDdUrl::new(server.url()).expect("failed to create URL")), + ), + ) + .expect("failed to create URL"), + None, + Duration::from_secs(1), + retry_strategy.clone(), + ); + + // Create a series using the Aggregator + let mut aggregator = MetricsAggregator::new(SortedTags::parse("test:value").unwrap(), 1) + .expect("failed to create aggregator"); + let metric = parse("test:1|c").expect("failed to parse metric"); + aggregator.insert(metric).expect("failed to insert metric"); + let series = aggregator.to_series(); + + let result = dd_api.ship_series(&series).await; + + // The result should be Ok since we got a 200 response on retry + assert!(result.is_ok()); + if let Ok(response) = result { + assert_eq!(response.status(), reqwest::StatusCode::OK); + } else { + panic!("Expected Ok result"); + } + + // Verify that both mocks were called exactly once + mock.assert_async().await; + success_mock.assert_async().await; +} + +#[cfg(test)] +#[cfg(not(miri))] +#[tokio::test] +async fn test_send_with_retry_immediate_failure_after_one_attempt() { + use dogstatsd::datadog::{DdApi, DdDdUrl, RetryStrategy}; + use dogstatsd::flusher::ShippingError; + use dogstatsd::metric::{parse, SortedTags}; + + let mut server = Server::new_async().await; + let mock = server + .mock("POST", "/api/v2/series") + .with_status(500) + .with_body("Internal Server Error") + .expect(1) + .create_async() + .await; + + let retry_strategy = RetryStrategy::Immediate(1); // Only 1 attempt + let dd_api = DdApi::new( + "test_key".to_string(), + MetricsIntakeUrlPrefix::new( + None, + MetricsIntakeUrlPrefixOverride::maybe_new( + None, + Some(DdDdUrl::new(server.url()).expect("failed to create URL")), + ), + ) + .expect("failed to create URL"), + None, + Duration::from_secs(1), + retry_strategy.clone(), + ); + + // Create a series using the Aggregator + let mut aggregator = MetricsAggregator::new(SortedTags::parse("test:value").unwrap(), 1) + .expect("failed to create aggregator"); + let metric = parse("test:1|c").expect("failed to parse metric"); + aggregator.insert(metric).expect("failed to insert metric"); + let series = aggregator.to_series(); + + let result = dd_api.ship_series(&series).await; + + // The result should be an error since we got a 500 response + assert!(result.is_err()); + if let Err(ShippingError::Destination(Some(status), msg)) = result { + assert_eq!(status, reqwest::StatusCode::INTERNAL_SERVER_ERROR); + assert_eq!(msg, "Failed to send request after 1 attempts"); + } else { + panic!("Expected ShippingError::Destination with status 500"); + } + + // Verify that the mock was called exactly once + mock.assert_async().await; +} From e9e3fcb9be11b07c1cf870b27d1a5530dbbbcecf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?jordan=20gonz=C3=A1lez?= <30836115+duncanista@users.noreply.github.com> Date: Wed, 19 Mar 2025 13:25:12 -0400 Subject: [PATCH 25/28] [dogstatsd] optimize `parse` function in `SortedTags` (#943) dogstatsd - optimize `parse` function * `MAX_TAGS` to `100` based on internal docs, the limit for custom tags is 100 --------- Co-authored-by: Edmund Kump --- src/constants.rs | 2 +- src/metric.rs | 43 ++++++++++++++++++++++++------------------- 2 files changed, 25 insertions(+), 20 deletions(-) diff --git a/src/constants.rs b/src/constants.rs index c93ad965..adf26316 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 /// The maximum tags that a `Metric` may hold. -pub const MAX_TAGS: usize = 32; +pub const MAX_TAGS: usize = 100; pub const CONTEXTS: usize = 10_240; diff --git a/src/metric.rs b/src/metric.rs index e4a67dc6..08d68ab0 100644 --- a/src/metric.rs +++ b/src/metric.rs @@ -71,20 +71,27 @@ impl SortedTags { } pub fn parse(tags_section: &str) -> Result { - let tag_parts = tags_section.split(','); - let mut parsed_tags = Vec::new(); - // Validate that the tags have the right form. - for (i, part) in tag_parts.filter(|s| !s.is_empty()).enumerate() { - if i >= constants::MAX_TAGS { - return Err(ParseError::Raw(format!("Too many tags, more than {i}"))); - } - if !part.contains(':') { - parsed_tags.push((Ustr::from(part), Ustr::from(""))); - } else if let Some((k, v)) = part.split_once(':') { + let total_tags = tags_section.bytes().filter(|&b| b == b',').count() + 1; + let mut parsed_tags = Vec::with_capacity(total_tags); + + for part in tags_section.split(',').filter(|s| !s.is_empty()) { + if let Some(i) = part.find(':') { + // Avoid creating a new string via split_once + let (k, v) = (&part[..i], &part[i + 1..]); parsed_tags.push((Ustr::from(k), Ustr::from(v))); + } else { + parsed_tags.push((Ustr::from(part), Ustr::from(""))); } } + parsed_tags.dedup(); + if parsed_tags.len() > constants::MAX_TAGS { + return Err(ParseError::Raw(format!( + "Too many tags, more than {c}", + c = constants::MAX_TAGS + ))); + } + parsed_tags.sort_unstable(); Ok(SortedTags { values: parsed_tags, @@ -551,15 +558,13 @@ mod tests { #[test] #[cfg_attr(miri, ignore)] fn parse_too_many_tags() { - // 33 - assert_eq!(parse("foo:1|g|#a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3").unwrap_err(), - ParseError::Raw("Too many tags, more than 32".to_string())); - - // 32 - assert!(parse("foo:1|g|#a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2").is_ok()); - - // 31 - assert!(parse("foo:1|g|#a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1").is_ok()); + // 101 + assert_eq!( + parse( + "foo:1|g|#a:1,b:2,c:3,d:4,e:5,f:6,g:7,h:8,i:9,j:10,k:11,l:12,m:13,n:14,o:15,p:16,q:17,r:18,s:19,t:20,u:21,v:22,w:23,x:24,y:25,z:26,aa:27,ab:28,ac:29,ad:30,ae:31,af:32,ag:33,ah:34,ai:35,aj:36,ak:37,al:38,am:39,an:40,ao:41,ap:42,aq:43,ar:44,as:45,at:46,au:47,av:48,aw:49,ax:50,ay:51,az:52,ba:53,bb:54,bc:55,bd:56,be:57,bf:58,bg:59,bh:60,bi:61,bj:62,bk:63,bl:64,bm:65,bn:66,bo:67,bp:68,bq:69,br:70,bs:71,bt:72,bu:73,bv:74,bw:75,bx:76,by:77,bz:78,ca:79,cb:80,cc:81,cd:82,ce:83,cf:84,cg:85,ch:86,ci:87,cj:88,ck:89,cl:90,cm:91,cn:92,co:93,cp:94,cq:95,cr:96,cs:97,ct:98,cu:99,cv:100,cw:101" + ).unwrap_err(), + ParseError::Raw("Too many tags, more than 100".to_string()) + ); // 30 assert!(parse("foo:1|g|#a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3,a:1,b:2,c:3").is_ok()); From 1073537e6cb62818e99638e1a41bf3e5f6d6f493 Mon Sep 17 00:00:00 2001 From: Duncan Harvey Date: Wed, 19 Mar 2025 15:06:18 -0400 Subject: [PATCH 26/28] remove version from dogstatsd Cargo.toml --- crates/dogstatsd/Cargo.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/dogstatsd/Cargo.toml b/crates/dogstatsd/Cargo.toml index 4e321887..9dadb389 100644 --- a/crates/dogstatsd/Cargo.toml +++ b/crates/dogstatsd/Cargo.toml @@ -2,7 +2,6 @@ name = "dogstatsd" rust-version.workspace = true edition.workspace = true -version.workspace = true license.workspace = true [lib] From 3e70c29166eada5c11bb31d627504602e358d91a Mon Sep 17 00:00:00 2001 From: Duncan Harvey Date: Wed, 19 Mar 2025 15:07:22 -0400 Subject: [PATCH 27/28] use relative link in dogstatsd readme --- crates/dogstatsd/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/dogstatsd/README.md b/crates/dogstatsd/README.md index fa5deb7e..dd0c3e59 100644 --- a/crates/dogstatsd/README.md +++ b/crates/dogstatsd/README.md @@ -7,7 +7,7 @@ This project is in beta and possible frequent changes should be expected. It's p - No UDS support - Uses `ustr`, so prone to memory leaks -- Arbitrary constraints in https://github.com/DataDog/libdatadog/blob/main/dogstatsd/src/constants.rs +- Arbitrary constraints in [src/constants.rs](src/constants.rs) ## Additional Notes From 78dc404c342b5df7f1371441252d438745ca1f79 Mon Sep 17 00:00:00 2001 From: Duncan Harvey Date: Wed, 19 Mar 2025 15:36:32 -0400 Subject: [PATCH 28/28] generate Cargo.lock for dogstatsd crate --- Cargo.lock | 2484 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 2484 insertions(+) create mode 100644 Cargo.lock diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 00000000..278bffdb --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,2484 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "addr2line" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + +[[package]] +name = "ahash" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy 0.7.35", +] + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "anyhow" +version = "1.0.97" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f" + +[[package]] +name = "assert-json-diff" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e4f2b81832e72834d7518d8487a0396a28cc408186a2e8854c0f98011faf12" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "async-trait" +version = "0.1.88" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" + +[[package]] +name = "backtrace" +version = "0.3.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets 0.52.6", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + +[[package]] +name = "bitflags" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" + +[[package]] +name = "bumpalo" +version = "3.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" + +[[package]] +name = "cc" +version = "1.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be714c154be609ec7f5dad223a33bf1482fff90472de28f7362806e6d4832b8c" +dependencies = [ + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "datadog-protos" +version = "0.1.0" +source = "git+https://github.com/DataDog/saluki/?rev=c89b58e5784b985819baf11f13f7d35876741222#c89b58e5784b985819baf11f13f7d35876741222" +dependencies = [ + "bytes", + "prost", + "protobuf", + "protobuf-codegen", + "tonic", + "tonic-build", +] + +[[package]] +name = "ddsketch-agent" +version = "0.1.0" +source = "git+https://github.com/DataDog/saluki/?rev=c89b58e5784b985819baf11f13f7d35876741222#c89b58e5784b985819baf11f13f7d35876741222" +dependencies = [ + "datadog-protos", + "float-cmp", + "ordered-float", + "smallvec", +] + +[[package]] +name = "derive_more" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a9b99b9cbbe49445b21764dc0625032a89b145a2642e67603e1c936f5458d05" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "unicode-xid", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "dogstatsd" +version = "0.0.0" +dependencies = [ + "datadog-protos", + "ddsketch-agent", + "derive_more", + "fnv", + "hashbrown 0.14.5", + "mockito", + "proptest", + "protobuf", + "regex", + "reqwest", + "serde", + "serde_json", + "thiserror 1.0.69", + "tokio", + "tokio-util", + "tracing", + "tracing-test", + "ustr", + "zstd", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "fixedbitset" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" + +[[package]] +name = "float-cmp" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b09cf3155332e944990140d967ff5eceb70df778b34f77d8075db46e4704e6d8" +dependencies = [ + "num-traits", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "pin-utils", +] + +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi", + "wasi 0.14.2+wasi-0.2.4", + "wasm-bindgen", +] + +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" + +[[package]] +name = "h2" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5017294ff4bb30944501348f6f8e42e6ad28f42c8bbef7a74029aff064a4e3c2" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + +[[package]] +name = "hashbrown" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "home" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "http" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d191583f3da1305256f22463b9bb0471acad48a4e534a5218b9963e9c1f59b2" +dependencies = [ + "futures-util", + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", + "webpki-roots", +] + +[[package]] +name = "hyper-util" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "icu_collections" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locid" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_locid_transform" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_locid_transform_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_locid_transform_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" + +[[package]] +name = "icu_normalizer" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "utf16_iter", + "utf8_iter", + "write16", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" + +[[package]] +name = "icu_properties" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locid_transform", + "icu_properties_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" + +[[package]] +name = "icu_provider" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_provider_macros", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_provider_macros" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "idna" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3954d50fe15b02142bf25d3b8bdadb634ec3948f103d04ffe3031bc8fe9d7058" +dependencies = [ + "equivalent", + "hashbrown 0.15.2", +] + +[[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "jobserver" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" +dependencies = [ + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.171" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6" + +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + +[[package]] +name = "linux-raw-sys" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe7db12097d22ec582439daf8618b8fdd1a7bef6270e9af3b1ebcd30893cf413" + +[[package]] +name = "litemap" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23fb14cb19457329c82206317a5663005a4d404783dc74f4252769b0d5f42856" + +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30bde2b3dc3671ae49d8e2e9f044c7c005836e7a023ee57cffa25ab82764bb9e" + +[[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata 0.1.10", +] + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "miniz_oxide" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e3e04debbb59698c15bacbb6d93584a8c0ca9cc3213cb423d31f760d8843ce5" +dependencies = [ + "adler2", +] + +[[package]] +name = "mio" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" +dependencies = [ + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", + "windows-sys 0.52.0", +] + +[[package]] +name = "mockito" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7760e0e418d9b7e5777c0374009ca4c93861b9066f18cb334a20ce50ab63aa48" +dependencies = [ + "assert-json-diff", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "log", + "rand 0.9.0", + "regex", + "serde_json", + "serde_urlencoded", + "similar", + "tokio", +] + +[[package]] +name = "multimap" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "defc4c55412d89136f966bbb339008b474350e5e6e78d2714439c386b3137a03" + +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "object" +version = "0.36.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d75b0bedcc4fe52caa0e03d9f1151a323e4aa5e2d78ba3580400cd3c9e2bc4bc" + +[[package]] +name = "ordered-float" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bb71e1b3fa6ca1c61f383464aaf2bb0e2f8e772a1f01d486832464de363b951" +dependencies = [ + "num-traits", +] + +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.52.6", +] + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "petgraph" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772" +dependencies = [ + "fixedbitset", + "indexmap", +] + +[[package]] +name = "pin-project" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy 0.8.23", +] + +[[package]] +name = "prettyplease" +version = "0.2.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5316f57387668042f561aae71480de936257848f9c43ce528e311d89a07cadeb" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.94" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "proptest" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14cae93065090804185d3b75f0bf93b8eeda30c7a9b4a33d3bdb3988d6229e50" +dependencies = [ + "bit-set", + "bit-vec", + "bitflags", + "lazy_static", + "num-traits", + "rand 0.8.5", + "rand_chacha 0.3.1", + "rand_xorshift", + "regex-syntax 0.8.5", + "rusty-fork", + "tempfile", + "unarray", +] + +[[package]] +name = "prost" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2796faa41db3ec313a31f7624d9286acf277b52de526150b7e69f3debf891ee5" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-build" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be769465445e8c1474e9c5dac2018218498557af32d9ed057325ec9a41ae81bf" +dependencies = [ + "heck", + "itertools", + "log", + "multimap", + "once_cell", + "petgraph", + "prettyplease", + "prost", + "prost-types", + "regex", + "syn", + "tempfile", +] + +[[package]] +name = "prost-derive" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d" +dependencies = [ + "anyhow", + "itertools", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "prost-types" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52c2c1bf36ddb1a1c396b3601a3cec27c2462e45f07c386894ec3ccf5332bd16" +dependencies = [ + "prost", +] + +[[package]] +name = "protobuf" +version = "3.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d65a1d4ddae7d8b5de68153b48f6aa3bba8cb002b243dbdbc55a5afbc98f99f4" +dependencies = [ + "bytes", + "once_cell", + "protobuf-support", + "thiserror 1.0.69", +] + +[[package]] +name = "protobuf-codegen" +version = "3.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d3976825c0014bbd2f3b34f0001876604fe87e0c86cd8fa54251530f1544ace" +dependencies = [ + "anyhow", + "once_cell", + "protobuf", + "protobuf-parse", + "regex", + "tempfile", + "thiserror 1.0.69", +] + +[[package]] +name = "protobuf-parse" +version = "3.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4aeaa1f2460f1d348eeaeed86aea999ce98c1bded6f089ff8514c9d9dbdc973" +dependencies = [ + "anyhow", + "indexmap", + "log", + "protobuf", + "protobuf-support", + "tempfile", + "thiserror 1.0.69", + "which", +] + +[[package]] +name = "protobuf-support" +version = "3.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e36c2f31e0a47f9280fb347ef5e461ffcd2c52dd520d8e216b52f93b0b0d7d6" +dependencies = [ + "thiserror 1.0.69", +] + +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + +[[package]] +name = "quinn" +version = "0.11.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3bd15a6f2967aef83887dcb9fec0014580467e33720d073560cf015a5683012" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror 2.0.12", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b820744eb4dc9b57a3398183639c511b5a26d2ed702cedd3febaa1393caa22cc" +dependencies = [ + "bytes", + "getrandom 0.3.2", + "rand 0.9.0", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.12", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e46f3055866785f6b92bc6164b76be02ca8f2eb4b002c0354b28cf4c119e5944" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.59.0", +] + +[[package]] +name = "quote" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.3", + "zerocopy 0.8.23", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.3", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.15", +] + +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom 0.3.2", +] + +[[package]] +name = "rand_xorshift" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d25bf25ec5ae4a3f1b92f929810509a2f53d7dca2f50b794ff57e3face536c8f" +dependencies = [ + "rand_core 0.6.4", +] + +[[package]] +name = "redox_syscall" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b8c0c260b63a8219631167be35e6a988e9554dbd323f8bd08439c8ed1302bd1" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata 0.4.9", + "regex-syntax 0.8.5", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax 0.6.29", +] + +[[package]] +name = "regex-automata" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax 0.8.5", +] + +[[package]] +name = "regex-syntax" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + +[[package]] +name = "reqwest" +version = "0.12.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d19c46a6fdd48bc4dab94b6103fccc55d34c67cc0ad04653aad4ea2a07cd7bbb" +dependencies = [ + "base64", + "bytes", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "ipnet", + "js-sys", + "log", + "mime", + "once_cell", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pemfile", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tower", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots", + "windows-registry", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.15", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustix" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e56a18552996ac8d29ecc3b190b4fdbb2d91ca4ec396de7bbffaf43f3d637e96" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys 0.9.3", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustls" +version = "0.23.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "822ee9188ac4ec04a2f0531e55d035fb2de73f18b41a63c70c2712503b6fb13c" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pemfile" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "rustls-pki-types" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "917ce264624a4b4db1c364dcc35bfca9ded014d0a958cd47ad3e960e988ea51c" +dependencies = [ + "web-time", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0aa4eeac2588ffff23e9d7a7e9b3f971c5fb5b7ebc9452745e0c232c64f83b2f" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" + +[[package]] +name = "rusty-fork" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb3dcc6e454c328bb824492db107ab7c0ae8fcffe4ad210136ef014458c1bc4f" +dependencies = [ + "fnv", + "quick-error", + "tempfile", + "wait-timeout", +] + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "serde" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.140" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "similar" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd" + +[[package]] +name = "socket2" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tempfile" +version = "3.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7437ac7763b9b123ccf33c338a5cc1bac6f69b45a136c19bdd8a65e3916435bf" +dependencies = [ + "fastrand", + "getrandom 0.3.2", + "once_cell", + "rustix 1.0.3", + "windows-sys 0.59.0", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +dependencies = [ + "thiserror-impl 2.0.12", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thread_local" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +dependencies = [ + "cfg-if", + "once_cell", +] + +[[package]] +name = "tinystr" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09b3661f17e86524eccd4371ab0429194e0d7c008abb45f7a7495b1719463c71" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.44.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f382da615b842244d4b8738c82ed1275e6c5dd90c459a30941cd07080b06c91a" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "tokio-macros", + "windows-sys 0.52.0", +] + +[[package]] +name = "tokio-macros" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-stream" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b9590b93e6fcc1739458317cccd391ad3955e2bde8913edf6f95f9e65a8f034" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tonic" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877c5b330756d856ffcc4553ab34a5684481ade925ecc54bcd1bf02b1d0d4d52" +dependencies = [ + "async-trait", + "base64", + "bytes", + "http", + "http-body", + "http-body-util", + "percent-encoding", + "pin-project", + "prost", + "tokio-stream", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tonic-build" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9557ce109ea773b399c9b9e5dca39294110b74f1f342cb347a80d1fce8c26a11" +dependencies = [ + "prettyplease", + "proc-macro2", + "prost-build", + "prost-types", + "quote", + "syn", +] + +[[package]] +name = "tower" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "tracing-test" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "557b891436fe0d5e0e363427fc7f217abf9ccd510d5136549847bdcbcd011d68" +dependencies = [ + "tracing-core", + "tracing-subscriber", + "tracing-test-macro", +] + +[[package]] +name = "tracing-test-macro" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04659ddb06c87d233c566112c1c9c5b9e98256d9af50ec3bc9c8327f873a7568" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "unarray" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" + +[[package]] +name = "unicode-ident" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "ustr" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18b19e258aa08450f93369cf56dd78063586adf19e92a75b338a800f799a0208" +dependencies = [ + "ahash", + "byteorder", + "lazy_static", + "parking_lot", +] + +[[package]] +name = "utf16_iter" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wait-timeout" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" +dependencies = [ + "libc", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasi" +version = "0.14.2+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +dependencies = [ + "wit-bindgen-rt", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" +dependencies = [ + "cfg-if", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-roots" +version = "0.26.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2210b291f7ea53617fbafcc4939f10914214ec15aace5ba62293a668f322c5c9" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "which" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" +dependencies = [ + "either", + "home", + "once_cell", + "rustix 0.38.44", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-link" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" + +[[package]] +name = "windows-registry" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4286ad90ddb45071efd1a66dfa43eb02dd0dfbae1545ad6cc3c51cf34d7e8ba3" +dependencies = [ + "windows-result", + "windows-strings", + "windows-targets 0.53.0", +] + +[[package]] +name = "windows-result" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c64fd11a4fd95df68efcfee5f44a294fe71b8bc6a91993e2791938abcc712252" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1e4c7e8ceaaf9cb7d7507c974735728ab453b67ef8f18febdd7c11fe59dca8b" +dependencies = [ + "windows_aarch64_gnullvm 0.53.0", + "windows_aarch64_msvc 0.53.0", + "windows_i686_gnu 0.53.0", + "windows_i686_gnullvm 0.53.0", + "windows_i686_msvc 0.53.0", + "windows_x86_64_gnu 0.53.0", + "windows_x86_64_gnullvm 0.53.0", + "windows_x86_64_msvc 0.53.0", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" + +[[package]] +name = "wit-bindgen-rt" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +dependencies = [ + "bitflags", +] + +[[package]] +name = "write16" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" + +[[package]] +name = "writeable" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" + +[[package]] +name = "yoke" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "zerocopy-derive 0.7.35", +] + +[[package]] +name = "zerocopy" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd97444d05a4328b90e75e503a34bad781f14e28a823ad3557f0750df1ebcbc6" +dependencies = [ + "zerocopy-derive 0.8.23", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6352c01d0edd5db859a63e2605f4ea3183ddbd15e2c4a9e7d32184df75e4f154" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" + +[[package]] +name = "zerovec" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zstd" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3051792fbdc2e1e143244dc28c60f73d8470e93f3f9cbd0ead44da5ed802722" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.14+zstd.1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fb060d4926e4ac3a3ad15d864e99ceb5f343c6b34f5bd6d81ae6ed417311be5" +dependencies = [ + "cc", + "pkg-config", +]