use std::error::Error;
use std::fmt::{self, Display, Formatter};
use lodestone_types::{
MetricAttributeV1, MetricAttributeValueV1, MetricDataV1, MetricExemplarV1, MetricExponentialBucketsV1,
MetricInstrumentationScopeV1, MetricNumberV1, MetricPointV1, MetricTemporalityV1,
};
use crate::{
I64, MetricAttribute, MetricAttributeValue, MetricData, MetricExemplar, MetricExponentialBuckets, MetricNumber,
MetricPoint, MetricTemporality, U64,
};
pub fn metric_point_from_wire(value: &MetricPoint) -> Result<MetricPointV1, NativeMetricWireError> {
Ok(MetricPointV1 {
format_version: u16::try_from(value.format_version).map_err(|_| NativeMetricWireError::Integer)?,
resource_attributes: metric_attributes_from_wire(&value.resource_attributes)?,
instrumentation_scope: MetricInstrumentationScopeV1 {
name: value.instrumentation_scope.name.clone(),
version: value.instrumentation_scope.version.clone(),
schema_url: value.instrumentation_scope.schema_url.clone(),
attributes: metric_attributes_from_wire(&value.instrumentation_scope.attributes)?,
},
metric_name: value.metric_name.clone(),
description: value.description.clone(),
unit: value.unit.clone(),
series_attributes: metric_attributes_from_wire(&value.series_attributes)?,
timestamp_unix_nanos: parse_u64(&value.timestamp_unix_nanos)?,
start_timestamp_unix_nanos: value.start_timestamp_unix_nanos.as_ref().map(parse_u64).transpose()?,
data: metric_data_from_wire(&value.data)?,
exemplars: value
.exemplars
.iter()
.map(metric_exemplar_from_wire)
.collect::<Result<Vec<_>, _>>()?,
})
}
#[must_use]
pub fn metric_point_to_wire(value: MetricPointV1) -> MetricPoint {
MetricPoint {
format_version: u32::from(value.format_version),
resource_attributes: metric_attributes_to_wire(value.resource_attributes),
instrumentation_scope: crate::MetricInstrumentationScope {
name: value.instrumentation_scope.name,
version: value.instrumentation_scope.version,
schema_url: value.instrumentation_scope.schema_url,
attributes: metric_attributes_to_wire(value.instrumentation_scope.attributes),
},
metric_name: value.metric_name,
description: value.description,
unit: value.unit,
series_attributes: metric_attributes_to_wire(value.series_attributes),
timestamp_unix_nanos: wire_u64(value.timestamp_unix_nanos),
start_timestamp_unix_nanos: value.start_timestamp_unix_nanos.map(wire_u64),
data: metric_data_to_wire(value.data),
exemplars: value
.exemplars
.into_iter()
.map(|exemplar| MetricExemplar {
timestamp_unix_nanos: wire_u64(exemplar.timestamp_unix_nanos),
value: metric_number_to_wire(exemplar.value),
trace_id: exemplar.trace_id,
span_id: exemplar.span_id,
filtered_attributes: metric_attributes_to_wire(exemplar.filtered_attributes),
})
.collect(),
}
}
fn metric_attributes_to_wire(values: Vec<MetricAttributeV1>) -> Vec<MetricAttribute> {
values
.into_iter()
.map(|attribute| MetricAttribute {
key: attribute.key,
value: match attribute.value {
MetricAttributeValueV1::String { value } => MetricAttributeValue::String { value },
MetricAttributeValueV1::Boolean { value } => MetricAttributeValue::Boolean { value },
MetricAttributeValueV1::I64 { value } => MetricAttributeValue::I64 { value: wire_i64(value) },
MetricAttributeValueV1::U64 { value } => MetricAttributeValue::U64 { value: wire_u64(value) },
MetricAttributeValueV1::F64Bits { value } => MetricAttributeValue::F64Bits { value: wire_u64(value) },
},
})
.collect()
}
fn metric_number_to_wire(value: MetricNumberV1) -> MetricNumber {
match value {
MetricNumberV1::I64 { value } => MetricNumber::I64 { value: wire_i64(value) },
MetricNumberV1::F64Bits { value } => MetricNumber::F64Bits { value: wire_u64(value) },
}
}
const fn metric_temporality_to_wire(value: MetricTemporalityV1) -> MetricTemporality {
match value {
MetricTemporalityV1::Cumulative => MetricTemporality::Cumulative,
MetricTemporalityV1::Delta => MetricTemporality::Delta,
}
}
fn metric_exponential_buckets_to_wire(value: MetricExponentialBucketsV1) -> MetricExponentialBuckets {
MetricExponentialBuckets {
offset: value.offset,
counts: value.counts.into_iter().map(wire_u64).collect(),
}
}
fn metric_data_to_wire(value: MetricDataV1) -> MetricData {
match value {
MetricDataV1::Gauge { value } => MetricData::Gauge {
value: metric_number_to_wire(value),
},
MetricDataV1::Sum {
value,
temporality,
monotonic,
} => MetricData::Sum {
value: metric_number_to_wire(value),
temporality: metric_temporality_to_wire(temporality),
monotonic,
},
MetricDataV1::ExplicitHistogram {
temporality,
count,
sum,
bounds_f64_bits,
bucket_counts,
} => MetricData::ExplicitHistogram {
temporality: metric_temporality_to_wire(temporality),
count: wire_u64(count),
sum: sum.map(metric_number_to_wire),
bounds_f64_bits: bounds_f64_bits.into_iter().map(wire_u64).collect(),
bucket_counts: bucket_counts.into_iter().map(wire_u64).collect(),
},
MetricDataV1::ExponentialHistogram {
temporality,
count,
sum,
scale,
zero_count,
positive,
negative,
} => MetricData::ExponentialHistogram {
temporality: metric_temporality_to_wire(temporality),
count: wire_u64(count),
sum: sum.map(metric_number_to_wire),
scale,
zero_count: wire_u64(zero_count),
positive: metric_exponential_buckets_to_wire(positive),
negative: metric_exponential_buckets_to_wire(negative),
},
}
}
fn wire_u64(value: u64) -> U64 {
U64 {
value: value.to_string(),
}
}
fn wire_i64(value: i64) -> I64 {
I64 {
value: value.to_string(),
}
}
fn metric_attributes_from_wire(values: &[MetricAttribute]) -> Result<Vec<MetricAttributeV1>, NativeMetricWireError> {
values
.iter()
.map(|attribute| {
Ok(MetricAttributeV1 {
key: attribute.key.clone(),
value: match &attribute.value {
MetricAttributeValue::String { value } => MetricAttributeValueV1::String { value: value.clone() },
MetricAttributeValue::Boolean { value } => MetricAttributeValueV1::Boolean { value: *value },
MetricAttributeValue::I64 { value } => MetricAttributeValueV1::I64 {
value: parse_i64(value)?,
},
MetricAttributeValue::U64 { value } => MetricAttributeValueV1::U64 {
value: parse_u64(value)?,
},
MetricAttributeValue::F64Bits { value } => MetricAttributeValueV1::F64Bits {
value: parse_u64(value)?,
},
},
})
})
.collect()
}
fn metric_number_from_wire(value: &MetricNumber) -> Result<MetricNumberV1, NativeMetricWireError> {
Ok(match value {
MetricNumber::I64 { value } => MetricNumberV1::I64 {
value: parse_i64(value)?,
},
MetricNumber::F64Bits { value } => MetricNumberV1::F64Bits {
value: parse_u64(value)?,
},
})
}
const fn metric_temporality_from_wire(value: MetricTemporality) -> MetricTemporalityV1 {
match value {
MetricTemporality::Cumulative => MetricTemporalityV1::Cumulative,
MetricTemporality::Delta => MetricTemporalityV1::Delta,
}
}
fn metric_exponential_buckets_from_wire(
value: &MetricExponentialBuckets,
) -> Result<MetricExponentialBucketsV1, NativeMetricWireError> {
Ok(MetricExponentialBucketsV1 {
offset: value.offset,
counts: value.counts.iter().map(parse_u64).collect::<Result<Vec<_>, _>>()?,
})
}
fn metric_data_from_wire(value: &MetricData) -> Result<MetricDataV1, NativeMetricWireError> {
Ok(match value {
MetricData::Gauge { value } => MetricDataV1::Gauge {
value: metric_number_from_wire(value)?,
},
MetricData::Sum {
value,
temporality,
monotonic,
} => MetricDataV1::Sum {
value: metric_number_from_wire(value)?,
temporality: metric_temporality_from_wire(*temporality),
monotonic: *monotonic,
},
MetricData::ExplicitHistogram {
temporality,
count,
sum,
bounds_f64_bits,
bucket_counts,
} => MetricDataV1::ExplicitHistogram {
temporality: metric_temporality_from_wire(*temporality),
count: parse_u64(count)?,
sum: sum.as_ref().map(metric_number_from_wire).transpose()?,
bounds_f64_bits: bounds_f64_bits.iter().map(parse_u64).collect::<Result<Vec<_>, _>>()?,
bucket_counts: bucket_counts.iter().map(parse_u64).collect::<Result<Vec<_>, _>>()?,
},
MetricData::ExponentialHistogram {
temporality,
count,
sum,
scale,
zero_count,
positive,
negative,
} => MetricDataV1::ExponentialHistogram {
temporality: metric_temporality_from_wire(*temporality),
count: parse_u64(count)?,
sum: sum.as_ref().map(metric_number_from_wire).transpose()?,
scale: *scale,
zero_count: parse_u64(zero_count)?,
positive: metric_exponential_buckets_from_wire(positive)?,
negative: metric_exponential_buckets_from_wire(negative)?,
},
})
}
fn metric_exemplar_from_wire(value: &MetricExemplar) -> Result<MetricExemplarV1, NativeMetricWireError> {
Ok(MetricExemplarV1 {
timestamp_unix_nanos: parse_u64(&value.timestamp_unix_nanos)?,
value: metric_number_from_wire(&value.value)?,
trace_id: value.trace_id.clone(),
span_id: value.span_id.clone(),
filtered_attributes: metric_attributes_from_wire(&value.filtered_attributes)?,
})
}
fn parse_u64(value: &U64) -> Result<u64, NativeMetricWireError> {
value.value.parse().map_err(|_| NativeMetricWireError::Integer)
}
fn parse_i64(value: &I64) -> Result<i64, NativeMetricWireError> {
value.value.parse().map_err(|_| NativeMetricWireError::Integer)
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum NativeMetricWireError {
Integer,
}
impl Display for NativeMetricWireError {
fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
formatter.write_str("native metric wire contains a noncanonical integer")
}
}
impl Error for NativeMetricWireError {}
#[cfg(test)]
mod tests {
use super::*;
fn point(data: MetricDataV1) -> MetricPointV1 {
MetricPointV1 {
format_version: 1,
resource_attributes: vec![
MetricAttributeV1 {
key: "enabled".to_owned(),
value: MetricAttributeValueV1::Boolean { value: true },
},
MetricAttributeV1 {
key: "limit".to_owned(),
value: MetricAttributeValueV1::U64 { value: u64::MAX },
},
],
instrumentation_scope: MetricInstrumentationScopeV1 {
name: "test".to_owned(),
version: Some("1".to_owned()),
schema_url: Some("https://example.invalid/schema".to_owned()),
attributes: vec![MetricAttributeV1 {
key: "signed".to_owned(),
value: MetricAttributeValueV1::I64 { value: i64::MIN },
}],
},
metric_name: "requests".to_owned(),
description: Some("request count".to_owned()),
unit: "1".to_owned(),
series_attributes: vec![MetricAttributeV1 {
key: "ratio".to_owned(),
value: MetricAttributeValueV1::F64Bits {
value: 1.5_f64.to_bits(),
},
}],
timestamp_unix_nanos: u64::MAX,
start_timestamp_unix_nanos: Some(1),
data,
exemplars: vec![MetricExemplarV1 {
timestamp_unix_nanos: 7,
value: MetricNumberV1::I64 { value: -3 },
trace_id: Some("trace".to_owned()),
span_id: Some("span".to_owned()),
filtered_attributes: vec![MetricAttributeV1 {
key: "text".to_owned(),
value: MetricAttributeValueV1::String {
value: "value".to_owned(),
},
}],
}],
}
}
#[test]
fn model_to_wire_round_trips_every_metric_data_variant() -> Result<(), Box<dyn Error>> {
let variants = vec![
MetricDataV1::Gauge {
value: MetricNumberV1::I64 { value: -1 },
},
MetricDataV1::Sum {
value: MetricNumberV1::F64Bits {
value: 2.5_f64.to_bits(),
},
temporality: MetricTemporalityV1::Cumulative,
monotonic: true,
},
MetricDataV1::ExplicitHistogram {
temporality: MetricTemporalityV1::Delta,
count: 3,
sum: Some(MetricNumberV1::I64 { value: 9 }),
bounds_f64_bits: vec![1.0_f64.to_bits(), 2.0_f64.to_bits()],
bucket_counts: vec![1, 1, 1],
},
MetricDataV1::ExponentialHistogram {
temporality: MetricTemporalityV1::Cumulative,
count: 4,
sum: None,
scale: 2,
zero_count: 1,
positive: MetricExponentialBucketsV1 {
offset: -1,
counts: vec![2],
},
negative: MetricExponentialBucketsV1 {
offset: 1,
counts: vec![1],
},
},
];
for data in variants {
let expected = point(data);
let wire = metric_point_to_wire(expected.clone());
assert_eq!(metric_point_from_wire(&wire)?, expected);
}
Ok(())
}
}