scepter 0.1.5

Composable primitives for planet-scale time-series routing, indexing, and aggregation.
Documentation
use std::collections::HashMap;

use crate::key::{KeyEncoder, LexicographicKey};

/// Logical type for schema fields and metric values.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ValueKind {
    /// Boolean value.
    Bool,
    /// Signed 64-bit integer.
    I64,
    /// 64-bit floating-point value.
    F64,
    /// UTF-8 string.
    String,
    /// Histogram-like distribution value.
    Distribution,
    /// Tuple of other value kinds.
    Tuple(Vec<ValueKind>),
}

/// Metric accumulation semantics.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum MetricKind {
    /// Point-in-time metric.
    Gauge,
    /// Monotonically cumulative metric.
    Cumulative,
}

/// Named schema field.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Field {
    /// Field name.
    pub name: String,
    /// Field value kind.
    pub kind: ValueKind,
}

impl Field {
    /// Creates a new named field.
    pub fn new(name: impl Into<String>, kind: ValueKind) -> Self {
        Self {
            name: name.into(),
            kind,
        }
    }
}

/// Schema for monitored entities that produce time series.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TargetSchema {
    /// Target schema name.
    pub name: String,
    /// Ordered target fields.
    pub fields: Vec<Field>,
    /// Field that maps targets to zones or locations.
    pub location_field: String,
}

impl TargetSchema {
    /// Creates a target schema.
    pub fn new(
        name: impl Into<String>,
        fields: Vec<Field>,
        location_field: impl Into<String>,
    ) -> Self {
        Self {
            name: name.into(),
            fields,
            location_field: location_field.into(),
        }
    }

    /// Returns true if the schema defines a field named `name`.
    pub fn has_field(&self, name: &str) -> bool {
        self.fields.iter().any(|field| field.name == name)
    }
}

/// Schema for one metric family.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MetricSchema {
    /// Metric name.
    pub name: String,
    /// Ordered metric fields.
    pub fields: Vec<Field>,
    /// Metric value type.
    pub value_kind: ValueKind,
    /// Gauge or cumulative behavior.
    pub metric_kind: MetricKind,
}

impl MetricSchema {
    /// Creates a metric schema.
    pub fn new(
        name: impl Into<String>,
        fields: Vec<Field>,
        value_kind: ValueKind,
        metric_kind: MetricKind,
    ) -> Self {
        Self {
            name: name.into(),
            fields,
            value_kind,
            metric_kind,
        }
    }
}

/// Concrete field value that can participate in key encoding.
#[derive(Debug, Clone, PartialEq)]
pub enum FieldValue {
    /// Boolean value.
    Bool(bool),
    /// Signed integer value.
    I64(i64),
    /// Unsigned integer value.
    U64(u64),
    /// Floating-point value.
    F64(f64),
    /// String value.
    String(String),
}

impl LexicographicKey for FieldValue {
    fn encode_key(&self, out: &mut Vec<u8>) {
        match self {
            Self::Bool(value) => value.encode_key(out),
            Self::I64(value) => value.encode_key(out),
            Self::U64(value) => value.encode_key(out),
            Self::F64(value) => value.encode_key(out),
            Self::String(value) => value.encode_key(out),
        }
    }
}

/// Full logical key for a time series.
#[derive(Debug, Clone, PartialEq)]
pub struct TimeSeriesKey {
    /// Target schema name.
    pub target_schema: String,
    /// Ordered target field values.
    pub target_fields: Vec<FieldValue>,
    /// Metric name.
    pub metric_name: String,
    /// Ordered metric field values.
    pub metric_fields: Vec<FieldValue>,
}

impl TimeSeriesKey {
    /// Encodes only the target portion of the key.
    pub fn target_key(&self) -> Vec<u8> {
        let mut encoder = KeyEncoder::new();
        encoder.push_field(self.target_schema.as_str());
        for field in &self.target_fields {
            encoder.push_field(field);
        }
        encoder.finish()
    }

    /// Encodes the full target plus metric time-series key.
    pub fn series_key(&self) -> Vec<u8> {
        let mut encoder = KeyEncoder::new();
        encoder.push_field(self.target_schema.as_str());
        for field in &self.target_fields {
            encoder.push_field(field);
        }
        encoder.push_field(self.metric_name.as_str());
        for field in &self.metric_fields {
            encoder.push_field(field);
        }
        encoder.finish()
    }
}

/// Maps target location values to storage/query zones.
#[derive(Debug, Clone, Default)]
pub struct LocationResolver {
    zones: HashMap<String, String>,
}

impl LocationResolver {
    /// Creates an empty resolver.
    pub fn new() -> Self {
        Self::default()
    }

    /// Associates one location value with a zone.
    pub fn insert(&mut self, location: impl Into<String>, zone: impl Into<String>) {
        self.zones.insert(location.into(), zone.into());
    }

    /// Returns the zone for `location`.
    pub fn zone_for(&self, location: &str) -> Option<&str> {
        self.zones.get(location).map(String::as_str)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn time_series_key_has_target_and_full_series_forms() {
        let key = TimeSeriesKey {
            target_schema: "ComputeTask".to_owned(),
            target_fields: vec![FieldValue::String("aa".to_owned())],
            metric_name: "/rpc/server/latency".to_owned(),
            metric_fields: vec![FieldValue::String("Query".to_owned())],
        };

        assert!(key.target_key() < key.series_key());
    }

    #[test]
    fn location_resolver_maps_locations_to_zones() {
        let mut resolver = LocationResolver::new();
        resolver.insert("aa", "zone-west");

        assert_eq!(resolver.zone_for("aa"), Some("zone-west"));
    }
}