#[cfg(feature = "vrl")]
use std::convert::TryFrom;
#[cfg(feature = "vrl")]
use vrl::compiler::value::VrlValueConvert;
use std::{
convert::AsRef,
fmt::{self, Display, Formatter},
num::NonZeroU32,
};
use chrono::{DateTime, Utc};
use vector_common::{
byte_size_of::ByteSizeOf,
internal_event::{OptionalTag, TaggedEventsSent},
json_size::JsonSize,
request_metadata::GetEventCountTags,
EventDataEq,
};
use vector_config::configurable_component;
use super::{
estimated_json_encoded_size_of::EstimatedJsonEncodedSizeOf, BatchNotifier, EventFinalizer,
EventFinalizers, EventMetadata, Finalizable,
};
use crate::config::telemetry;
#[cfg(any(test, feature = "test"))]
mod arbitrary;
mod data;
pub use self::data::*;
mod series;
pub use self::series::*;
mod tags;
pub use self::tags::*;
mod value;
pub use self::value::*;
#[macro_export]
macro_rules! metric_tags {
() => { $crate::event::MetricTags::default() };
($($key:expr => $value:expr,)+) => { $crate::metric_tags!($($key => $value),+) };
($($key:expr => $value:expr),*) => {
[
$( ($key.into(), $crate::event::metric::TagValue::from($value)), )*
].into_iter().collect::<$crate::event::MetricTags>()
};
}
#[configurable_component]
#[derive(Clone, Debug, PartialEq)]
pub struct Metric {
#[serde(flatten)]
pub(super) series: MetricSeries,
#[serde(flatten)]
pub(super) data: MetricData,
#[serde(skip, default = "EventMetadata::default")]
metadata: EventMetadata,
}
impl Metric {
pub fn new<T: Into<String>>(name: T, kind: MetricKind, value: MetricValue) -> Self {
Self::new_with_metadata(name, kind, value, EventMetadata::default())
}
pub fn new_with_metadata<T: Into<String>>(
name: T,
kind: MetricKind,
value: MetricValue,
metadata: EventMetadata,
) -> Self {
Self {
series: MetricSeries {
name: MetricName {
name: name.into(),
namespace: None,
},
tags: None,
},
data: MetricData {
time: MetricTime {
timestamp: None,
interval_ms: None,
},
kind,
value,
},
metadata,
}
}
#[inline]
#[must_use]
pub fn with_name(mut self, name: impl Into<String>) -> Self {
self.series.name.name = name.into();
self
}
#[inline]
#[must_use]
pub fn with_namespace<T: Into<String>>(mut self, namespace: Option<T>) -> Self {
self.series.name.namespace = namespace.map(Into::into);
self
}
#[inline]
#[must_use]
pub fn with_timestamp(mut self, timestamp: Option<DateTime<Utc>>) -> Self {
self.data.time.timestamp = timestamp;
self
}
#[inline]
#[must_use]
pub fn with_interval_ms(mut self, interval_ms: Option<NonZeroU32>) -> Self {
self.data.time.interval_ms = interval_ms;
self
}
pub fn add_finalizer(&mut self, finalizer: EventFinalizer) {
self.metadata.add_finalizer(finalizer);
}
#[must_use]
pub fn with_batch_notifier(mut self, batch: &BatchNotifier) -> Self {
self.metadata = self.metadata.with_batch_notifier(batch);
self
}
#[must_use]
pub fn with_batch_notifier_option(mut self, batch: &Option<BatchNotifier>) -> Self {
self.metadata = self.metadata.with_batch_notifier_option(batch);
self
}
#[inline]
#[must_use]
pub fn with_tags(mut self, tags: Option<MetricTags>) -> Self {
self.series.tags = tags;
self
}
#[inline]
#[must_use]
pub fn with_value(mut self, value: MetricValue) -> Self {
self.data.value = value;
self
}
pub fn series(&self) -> &MetricSeries {
&self.series
}
pub fn data(&self) -> &MetricData {
&self.data
}
pub fn data_mut(&mut self) -> &mut MetricData {
&mut self.data
}
pub fn metadata(&self) -> &EventMetadata {
&self.metadata
}
pub fn metadata_mut(&mut self) -> &mut EventMetadata {
&mut self.metadata
}
#[inline]
pub fn name(&self) -> &str {
&self.series.name.name
}
#[inline]
pub fn namespace(&self) -> Option<&str> {
self.series.name.namespace.as_deref()
}
#[inline]
pub fn take_namespace(&mut self) -> Option<String> {
self.series.name.namespace.take()
}
#[inline]
pub fn tags(&self) -> Option<&MetricTags> {
self.series.tags.as_ref()
}
#[inline]
pub fn tags_mut(&mut self) -> Option<&mut MetricTags> {
self.series.tags.as_mut()
}
#[inline]
pub fn timestamp(&self) -> Option<DateTime<Utc>> {
self.data.time.timestamp
}
#[inline]
pub fn interval_ms(&self) -> Option<NonZeroU32> {
self.data.time.interval_ms
}
#[inline]
pub fn value(&self) -> &MetricValue {
&self.data.value
}
#[inline]
pub fn value_mut(&mut self) -> &mut MetricValue {
&mut self.data.value
}
#[inline]
pub fn kind(&self) -> MetricKind {
self.data.kind
}
#[inline]
pub fn time(&self) -> MetricTime {
self.data.time
}
#[inline]
pub fn into_parts(self) -> (MetricSeries, MetricData, EventMetadata) {
(self.series, self.data, self.metadata)
}
#[inline]
pub fn from_parts(series: MetricSeries, data: MetricData, metadata: EventMetadata) -> Self {
Self {
series,
data,
metadata,
}
}
#[must_use]
pub fn into_absolute(self) -> Self {
Self {
series: self.series,
data: self.data.into_absolute(),
metadata: self.metadata,
}
}
#[must_use]
pub fn into_incremental(self) -> Self {
Self {
series: self.series,
data: self.data.into_incremental(),
metadata: self.metadata,
}
}
#[allow(clippy::cast_precision_loss)]
pub(crate) fn from_metric_kv(
key: &metrics::Key,
value: MetricValue,
timestamp: DateTime<Utc>,
) -> Self {
let labels = key
.labels()
.map(|label| (String::from(label.key()), String::from(label.value())))
.collect::<MetricTags>();
Self::new(key.name().to_string(), MetricKind::Absolute, value)
.with_namespace(Some("vector"))
.with_timestamp(Some(timestamp))
.with_tags((!labels.is_empty()).then_some(labels))
}
pub fn remove_tag(&mut self, key: &str) -> Option<String> {
self.series.remove_tag(key)
}
pub fn remove_tags(&mut self) {
self.series.remove_tags();
}
pub fn tag_matches(&self, name: &str, value: &str) -> bool {
self.tags()
.filter(|t| t.get(name).filter(|v| *v == value).is_some())
.is_some()
}
pub fn tag_value(&self, name: &str) -> Option<String> {
self.tags().and_then(|t| t.get(name)).map(ToOwned::to_owned)
}
pub fn replace_tag(&mut self, name: String, value: String) -> Option<String> {
self.series.replace_tag(name, value)
}
pub fn set_multi_value_tag(
&mut self,
name: String,
values: impl IntoIterator<Item = TagValue>,
) {
self.series.set_multi_value_tag(name, values);
}
pub fn zero(&mut self) {
self.data.zero();
}
#[must_use]
pub fn add(&mut self, other: impl AsRef<MetricData>) -> bool {
self.data.add(other.as_ref())
}
#[must_use]
pub fn update(&mut self, other: impl AsRef<MetricData>) -> bool {
self.data.update(other.as_ref())
}
#[must_use]
pub fn subtract(&mut self, other: impl AsRef<MetricData>) -> bool {
self.data.subtract(other.as_ref())
}
pub fn reduce_tags_to_single(&mut self) {
if let Some(tags) = &mut self.series.tags {
tags.reduce_to_single();
if tags.is_empty() {
self.series.tags = None;
}
}
}
}
impl AsRef<MetricData> for Metric {
fn as_ref(&self) -> &MetricData {
&self.data
}
}
impl AsRef<MetricValue> for Metric {
fn as_ref(&self) -> &MetricValue {
&self.data.value
}
}
impl Display for Metric {
fn fmt(&self, fmt: &mut Formatter<'_>) -> Result<(), fmt::Error> {
if let Some(timestamp) = &self.data.time.timestamp {
write!(fmt, "{timestamp:?} ")?;
}
let kind = match self.data.kind {
MetricKind::Absolute => '=',
MetricKind::Incremental => '+',
};
self.series.fmt(fmt)?;
write!(fmt, " {kind} ")?;
self.data.value.fmt(fmt)
}
}
impl EventDataEq for Metric {
fn event_data_eq(&self, other: &Self) -> bool {
self.series == other.series
&& self.data == other.data
&& self.metadata.event_data_eq(&other.metadata)
}
}
impl ByteSizeOf for Metric {
fn allocated_bytes(&self) -> usize {
self.series.allocated_bytes()
+ self.data.allocated_bytes()
+ self.metadata.allocated_bytes()
}
}
impl EstimatedJsonEncodedSizeOf for Metric {
fn estimated_json_encoded_size_of(&self) -> JsonSize {
self.size_of().into()
}
}
impl Finalizable for Metric {
fn take_finalizers(&mut self) -> EventFinalizers {
self.metadata.take_finalizers()
}
}
impl GetEventCountTags for Metric {
fn get_tags(&self) -> TaggedEventsSent {
let source = if telemetry().tags().emit_source {
self.metadata().source_id().cloned().into()
} else {
OptionalTag::Ignored
};
let service = if telemetry().tags().emit_service {
self.tags()
.and_then(|tags| tags.get("service").map(ToString::to_string))
.into()
} else {
OptionalTag::Ignored
};
TaggedEventsSent { source, service }
}
}
#[configurable_component]
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, PartialOrd)]
#[serde(rename_all = "snake_case")]
pub enum MetricKind {
Incremental,
Absolute,
}
#[cfg(feature = "vrl")]
impl TryFrom<vrl::value::Value> for MetricKind {
type Error = String;
fn try_from(value: vrl::value::Value) -> Result<Self, Self::Error> {
let value = value.try_bytes().map_err(|e| e.to_string())?;
match std::str::from_utf8(&value).map_err(|e| e.to_string())? {
"incremental" => Ok(Self::Incremental),
"absolute" => Ok(Self::Absolute),
value => Err(format!(
"invalid metric kind {value}, metric kind must be `absolute` or `incremental`"
)),
}
}
}
#[cfg(feature = "vrl")]
impl From<MetricKind> for vrl::value::Value {
fn from(kind: MetricKind) -> Self {
match kind {
MetricKind::Incremental => "incremental".into(),
MetricKind::Absolute => "absolute".into(),
}
}
}
#[macro_export]
macro_rules! samples {
( $( $value:expr => $rate:expr ),* ) => {
vec![ $( $crate::event::metric::Sample { value: $value, rate: $rate }, )* ]
}
}
#[macro_export]
macro_rules! buckets {
( $( $limit:expr => $count:expr ),* ) => {
vec![ $( $crate::event::metric::Bucket { upper_limit: $limit, count: $count }, )* ]
}
}
#[macro_export]
macro_rules! quantiles {
( $( $q:expr => $value:expr ),* ) => {
vec![ $( $crate::event::metric::Quantile { quantile: $q, value: $value }, )* ]
}
}
#[inline]
pub(crate) fn zip_samples(
values: impl IntoIterator<Item = f64>,
rates: impl IntoIterator<Item = u32>,
) -> Vec<Sample> {
values
.into_iter()
.zip(rates)
.map(|(value, rate)| Sample { value, rate })
.collect()
}
#[inline]
pub(crate) fn zip_buckets(
limits: impl IntoIterator<Item = f64>,
counts: impl IntoIterator<Item = u64>,
) -> Vec<Bucket> {
limits
.into_iter()
.zip(counts)
.map(|(upper_limit, count)| Bucket { upper_limit, count })
.collect()
}
#[inline]
pub(crate) fn zip_quantiles(
quantiles: impl IntoIterator<Item = f64>,
values: impl IntoIterator<Item = f64>,
) -> Vec<Quantile> {
quantiles
.into_iter()
.zip(values)
.map(|(quantile, value)| Quantile { quantile, value })
.collect()
}
fn write_list<I, T, W>(
fmt: &mut Formatter<'_>,
sep: &str,
items: I,
writer: W,
) -> Result<(), fmt::Error>
where
I: IntoIterator<Item = T>,
W: Fn(&mut Formatter<'_>, T) -> Result<(), fmt::Error>,
{
let mut this_sep = "";
for item in items {
write!(fmt, "{this_sep}")?;
writer(fmt, item)?;
this_sep = sep;
}
Ok(())
}
fn write_word(fmt: &mut Formatter<'_>, word: &str) -> Result<(), fmt::Error> {
if word.contains(|c: char| !c.is_ascii_alphanumeric() && c != '_') {
write!(fmt, "{word:?}")
} else {
write!(fmt, "{word}")
}
}
pub fn samples_to_buckets(samples: &[Sample], buckets: &[f64]) -> (Vec<Bucket>, u64, f64) {
let mut counts = vec![0; buckets.len()];
let mut sum = 0.0;
let mut count = 0;
for sample in samples {
let rate = u64::from(sample.rate);
if let Some((i, _)) = buckets
.iter()
.enumerate()
.find(|&(_, b)| *b >= sample.value)
{
counts[i] += rate;
}
sum += sample.value * f64::from(sample.rate);
count += rate;
}
let buckets = buckets
.iter()
.zip(counts.iter())
.map(|(b, c)| Bucket {
upper_limit: *b,
count: *c,
})
.collect();
(buckets, count, sum)
}
#[cfg(test)]
mod test {
use std::collections::BTreeSet;
use chrono::{offset::TimeZone, DateTime, Timelike, Utc};
use similar_asserts::assert_eq;
use super::*;
fn ts() -> DateTime<Utc> {
Utc.with_ymd_and_hms(2018, 11, 14, 8, 9, 10)
.single()
.and_then(|t| t.with_nanosecond(11))
.expect("invalid timestamp")
}
fn tags() -> MetricTags {
metric_tags!(
"normal_tag" => "value",
"true_tag" => "true",
"empty_tag" => "",
)
}
#[test]
fn merge_counters() {
let mut counter = Metric::new(
"counter",
MetricKind::Incremental,
MetricValue::Counter { value: 1.0 },
);
let delta = Metric::new(
"counter",
MetricKind::Incremental,
MetricValue::Counter { value: 2.0 },
)
.with_namespace(Some("vector"))
.with_tags(Some(tags()))
.with_timestamp(Some(ts()));
let expected = counter
.clone()
.with_value(MetricValue::Counter { value: 3.0 })
.with_timestamp(Some(ts()));
assert!(counter.data.add(&delta.data));
assert_eq!(counter, expected);
}
#[test]
fn merge_gauges() {
let mut gauge = Metric::new(
"gauge",
MetricKind::Incremental,
MetricValue::Gauge { value: 1.0 },
);
let delta = Metric::new(
"gauge",
MetricKind::Incremental,
MetricValue::Gauge { value: -2.0 },
)
.with_namespace(Some("vector"))
.with_tags(Some(tags()))
.with_timestamp(Some(ts()));
let expected = gauge
.clone()
.with_value(MetricValue::Gauge { value: -1.0 })
.with_timestamp(Some(ts()));
assert!(gauge.data.add(&delta.data));
assert_eq!(gauge, expected);
}
#[test]
fn merge_sets() {
let mut set = Metric::new(
"set",
MetricKind::Incremental,
MetricValue::Set {
values: vec!["old".into()].into_iter().collect(),
},
);
let delta = Metric::new(
"set",
MetricKind::Incremental,
MetricValue::Set {
values: vec!["new".into()].into_iter().collect(),
},
)
.with_namespace(Some("vector"))
.with_tags(Some(tags()))
.with_timestamp(Some(ts()));
let expected = set
.clone()
.with_value(MetricValue::Set {
values: vec!["old".into(), "new".into()].into_iter().collect(),
})
.with_timestamp(Some(ts()));
assert!(set.data.add(&delta.data));
assert_eq!(set, expected);
}
#[test]
fn merge_histograms() {
let mut dist = Metric::new(
"hist",
MetricKind::Incremental,
MetricValue::Distribution {
samples: samples![1.0 => 10],
statistic: StatisticKind::Histogram,
},
);
let delta = Metric::new(
"hist",
MetricKind::Incremental,
MetricValue::Distribution {
samples: samples![1.0 => 20],
statistic: StatisticKind::Histogram,
},
)
.with_namespace(Some("vector"))
.with_tags(Some(tags()))
.with_timestamp(Some(ts()));
let expected = dist
.clone()
.with_value(MetricValue::Distribution {
samples: samples![1.0 => 10, 1.0 => 20],
statistic: StatisticKind::Histogram,
})
.with_timestamp(Some(ts()));
assert!(dist.data.add(&delta.data));
assert_eq!(dist, expected);
}
#[test]
fn subtract_counters() {
let old_counter = Metric::new(
"counter",
MetricKind::Absolute,
MetricValue::Counter { value: 4.0 },
);
let mut new_counter = Metric::new(
"counter",
MetricKind::Absolute,
MetricValue::Counter { value: 6.0 },
);
assert!(new_counter.subtract(&old_counter));
assert_eq!(new_counter.value(), &MetricValue::Counter { value: 2.0 });
let old_counter = Metric::new(
"counter",
MetricKind::Absolute,
MetricValue::Counter { value: 6.0 },
);
let mut new_reset_counter = Metric::new(
"counter",
MetricKind::Absolute,
MetricValue::Counter { value: 1.0 },
);
assert!(!new_reset_counter.subtract(&old_counter));
}
#[test]
fn subtract_aggregated_histograms() {
let old_histogram = Metric::new(
"histogram",
MetricKind::Absolute,
MetricValue::AggregatedHistogram {
count: 1,
sum: 1.0,
buckets: buckets!(2.0 => 1),
},
);
let mut new_histogram = Metric::new(
"histogram",
MetricKind::Absolute,
MetricValue::AggregatedHistogram {
count: 3,
sum: 3.0,
buckets: buckets!(2.0 => 3),
},
);
assert!(new_histogram.subtract(&old_histogram));
assert_eq!(
new_histogram.value(),
&MetricValue::AggregatedHistogram {
count: 2,
sum: 2.0,
buckets: buckets!(2.0 => 2),
}
);
let old_histogram = Metric::new(
"histogram",
MetricKind::Absolute,
MetricValue::AggregatedHistogram {
count: 3,
sum: 3.0,
buckets: buckets!(2.0 => 3),
},
);
let mut new_reset_histogram = Metric::new(
"histogram",
MetricKind::Absolute,
MetricValue::AggregatedHistogram {
count: 1,
sum: 1.0,
buckets: buckets!(2.0 => 1),
},
);
assert!(!new_reset_histogram.subtract(&old_histogram));
}
#[test]
#[allow(clippy::too_many_lines)]
fn display() {
assert_eq!(
format!(
"{}",
Metric::new(
"one",
MetricKind::Absolute,
MetricValue::Counter { value: 1.23 },
)
.with_tags(Some(tags()))
),
r#"one{empty_tag="",normal_tag="value",true_tag="true"} = 1.23"#
);
assert_eq!(
format!(
"{}",
Metric::new(
"two word",
MetricKind::Incremental,
MetricValue::Gauge { value: 2.0 }
)
.with_timestamp(Some(ts()))
),
r#"2018-11-14T08:09:10.000000011Z "two word"{} + 2"#
);
assert_eq!(
format!(
"{}",
Metric::new(
"namespace",
MetricKind::Absolute,
MetricValue::Counter { value: 1.23 },
)
.with_namespace(Some("vector"))
),
r"vector_namespace{} = 1.23"
);
assert_eq!(
format!(
"{}",
Metric::new(
"namespace",
MetricKind::Absolute,
MetricValue::Counter { value: 1.23 },
)
.with_namespace(Some("vector host"))
),
r#""vector host"_namespace{} = 1.23"#
);
let mut values = BTreeSet::<String>::new();
values.insert("v1".into());
values.insert("v2_two".into());
values.insert("thrəë".into());
values.insert("four=4".into());
assert_eq!(
format!(
"{}",
Metric::new("three", MetricKind::Absolute, MetricValue::Set { values })
),
r#"three{} = "four=4" "thrəë" v1 v2_two"#
);
assert_eq!(
format!(
"{}",
Metric::new(
"four",
MetricKind::Absolute,
MetricValue::Distribution {
samples: samples![1.0 => 3, 2.0 => 4],
statistic: StatisticKind::Histogram,
}
)
),
r"four{} = histogram 3@1 4@2"
);
assert_eq!(
format!(
"{}",
Metric::new(
"five",
MetricKind::Absolute,
MetricValue::AggregatedHistogram {
buckets: buckets![51.0 => 53, 52.0 => 54],
count: 107,
sum: 103.0,
}
)
),
r"five{} = count=107 sum=103 53@51 54@52"
);
assert_eq!(
format!(
"{}",
Metric::new(
"six",
MetricKind::Absolute,
MetricValue::AggregatedSummary {
quantiles: quantiles![1.0 => 63.0, 2.0 => 64.0],
count: 2,
sum: 127.0,
}
)
),
r"six{} = count=2 sum=127 1@63 2@64"
);
}
#[test]
fn quantile_to_percentile_string() {
let quantiles = [
(-1.0, "0"),
(0.0, "0"),
(0.25, "25"),
(0.50, "50"),
(0.999, "999"),
(0.9999, "9999"),
(0.99999, "9999"),
(1.0, "100"),
(3.0, "100"),
];
for (quantile, expected) in quantiles {
let quantile = Quantile {
quantile,
value: 1.0,
};
let result = quantile.to_percentile_string();
assert_eq!(result, expected);
}
}
#[test]
fn quantile_to_string() {
let quantiles = [
(-1.0, "0"),
(0.0, "0"),
(0.25, "0.25"),
(0.50, "0.5"),
(0.999, "0.999"),
(0.9999, "0.9999"),
(0.99999, "0.9999"),
(1.0, "1"),
(3.0, "1"),
];
for (quantile, expected) in quantiles {
let quantile = Quantile {
quantile,
value: 1.0,
};
let result = quantile.to_quantile_string();
assert_eq!(result, expected);
}
}
#[test]
fn value_conversions() {
let counter_value = MetricValue::Counter { value: 3.13 };
assert_eq!(counter_value.distribution_to_agg_histogram(&[1.0]), None);
let counter_value = MetricValue::Counter { value: 3.13 };
assert_eq!(counter_value.distribution_to_sketch(), None);
let distrib_value = MetricValue::Distribution {
samples: samples!(1.0 => 10, 2.0 => 5, 5.0 => 2),
statistic: StatisticKind::Summary,
};
let converted = distrib_value.distribution_to_agg_histogram(&[1.0, 5.0, 10.0]);
assert_eq!(
converted,
Some(MetricValue::AggregatedHistogram {
buckets: vec![
Bucket {
upper_limit: 1.0,
count: 10,
},
Bucket {
upper_limit: 5.0,
count: 7,
},
Bucket {
upper_limit: 10.0,
count: 0,
},
],
sum: 30.0,
count: 17,
})
);
let distrib_value = MetricValue::Distribution {
samples: samples!(1.0 => 1),
statistic: StatisticKind::Summary,
};
let converted = distrib_value.distribution_to_sketch();
assert!(matches!(converted, Some(MetricValue::Sketch { .. })));
}
#[test]
fn merge_non_contiguous_interval() {
let mut gauge = Metric::new(
"gauge",
MetricKind::Incremental,
MetricValue::Gauge { value: 12.0 },
)
.with_timestamp(Some(ts()))
.with_interval_ms(std::num::NonZeroU32::new(10));
let delta = Metric::new(
"gauge",
MetricKind::Incremental,
MetricValue::Gauge { value: -5.0 },
)
.with_timestamp(Some(ts() + chrono::Duration::milliseconds(20)))
.with_interval_ms(std::num::NonZeroU32::new(15));
let expected = gauge
.clone()
.with_value(MetricValue::Gauge { value: 7.0 })
.with_timestamp(Some(ts()))
.with_interval_ms(std::num::NonZeroU32::new(35));
assert!(gauge.data.add(&delta.data));
assert_eq!(gauge, expected);
}
#[test]
fn merge_contiguous_interval() {
let mut gauge = Metric::new(
"gauge",
MetricKind::Incremental,
MetricValue::Gauge { value: 12.0 },
)
.with_timestamp(Some(ts()))
.with_interval_ms(std::num::NonZeroU32::new(10));
let delta = Metric::new(
"gauge",
MetricKind::Incremental,
MetricValue::Gauge { value: -5.0 },
)
.with_timestamp(Some(ts() + chrono::Duration::milliseconds(5)))
.with_interval_ms(std::num::NonZeroU32::new(15));
let expected = gauge
.clone()
.with_value(MetricValue::Gauge { value: 7.0 })
.with_timestamp(Some(ts()))
.with_interval_ms(std::num::NonZeroU32::new(20));
assert!(gauge.data.add(&delta.data));
assert_eq!(gauge, expected);
}
}