lodestone-proto 0.0.32

Versioned protocol envelopes and transport-neutral requests for Lodestone
Documentation
//! Canonical projection from the generated native metric wire into the
//! transport-neutral metric model.

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(())
    }
}