use alloc::string::String;
use alloc::vec::Vec;
use core::fmt;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TelemetryError {
NotInitialized,
MetricNotFound(MetricId),
MetricExists(String),
InvalidName(String),
LabelMismatch {
expected: usize,
got: usize,
},
InvalidBuckets,
}
impl fmt::Display for TelemetryError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::NotInitialized => write!(f, "Telemetry not initialized"),
Self::MetricNotFound(id) => write!(f, "Metric not found: {:?}", id),
Self::MetricExists(name) => write!(f, "Metric already exists: {}", name),
Self::InvalidName(name) => write!(f, "Invalid metric name: {}", name),
Self::LabelMismatch { expected, got } => {
write!(
f,
"Label count mismatch: expected {}, got {}",
expected, got
)
}
Self::InvalidBuckets => write!(f, "Invalid bucket configuration"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct MetricId(pub u64);
impl MetricId {
pub fn new(id: u64) -> Self {
Self(id)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MetricType {
Counter,
Gauge,
Histogram,
Summary,
}
impl MetricType {
pub fn prometheus_type(&self) -> &'static str {
match self {
Self::Counter => "counter",
Self::Gauge => "gauge",
Self::Histogram => "histogram",
Self::Summary => "summary",
}
}
}
#[derive(Debug, Clone)]
pub struct MetricInfo {
pub id: MetricId,
pub name: String,
pub help: String,
pub metric_type: MetricType,
pub labels: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct HistogramBucket {
pub le: f64,
pub count: u64,
}
#[derive(Debug, Clone, Default)]
pub struct HistogramData {
pub buckets: Vec<HistogramBucket>,
pub sum: f64,
pub count: u64,
}
impl HistogramData {
pub fn new(bucket_bounds: &[f64]) -> Self {
let buckets = bucket_bounds
.iter()
.map(|&le| HistogramBucket { le, count: 0 })
.collect();
Self {
buckets,
sum: 0.0,
count: 0,
}
}
pub fn observe(&mut self, value: f64) {
self.sum += value;
self.count += 1;
for bucket in &mut self.buckets {
if value <= bucket.le {
bucket.count += 1;
}
}
}
pub fn reset(&mut self) {
self.sum = 0.0;
self.count = 0;
for bucket in &mut self.buckets {
bucket.count = 0;
}
}
}
#[derive(Debug, Clone)]
pub struct SummaryQuantile {
pub quantile: f64,
pub value: f64,
}
#[derive(Debug, Clone, Default)]
pub struct SummaryData {
pub quantiles: Vec<SummaryQuantile>,
pub sum: f64,
pub count: u64,
observations: Vec<f64>,
max_observations: usize,
}
impl SummaryData {
pub fn new(max_observations: usize) -> Self {
Self {
quantiles: Vec::new(),
sum: 0.0,
count: 0,
observations: Vec::new(),
max_observations,
}
}
pub fn observe(&mut self, value: f64) {
self.sum += value;
self.count += 1;
if self.observations.len() < self.max_observations {
self.observations.push(value);
}
}
pub fn calculate_quantiles(&mut self, quantiles: &[f64]) {
if self.observations.is_empty() {
return;
}
self.observations
.sort_by(|a, b| a.partial_cmp(b).unwrap_or(core::cmp::Ordering::Equal));
self.quantiles.clear();
for &q in quantiles {
let idx =
((q * self.observations.len() as f64) as usize).min(self.observations.len() - 1);
self.quantiles.push(SummaryQuantile {
quantile: q,
value: self.observations[idx],
});
}
}
pub fn reset(&mut self) {
self.sum = 0.0;
self.count = 0;
self.observations.clear();
self.quantiles.clear();
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct LabelPair {
pub name: String,
pub value: String,
}
impl LabelPair {
pub fn new(name: impl Into<String>, value: impl Into<String>) -> Self {
Self {
name: name.into(),
value: value.into(),
}
}
}
pub const DEFAULT_BUCKETS: &[f64] = &[
0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0,
];
pub const BYTES_BUCKETS: &[f64] = &[
1024.0, 4096.0, 16384.0, 65536.0, 262144.0, 1048576.0, 4194304.0, 16777216.0, 67108864.0, 268435456.0, 1073741824.0, ];
pub fn validate_metric_name(name: &str) -> Result<(), TelemetryError> {
if name.is_empty() {
return Err(TelemetryError::InvalidName("Empty name".to_string()));
}
let first = name.chars().next().unwrap();
if !first.is_ascii_alphabetic() && first != '_' {
return Err(TelemetryError::InvalidName(alloc::format!(
"Must start with letter or underscore: {}",
name
)));
}
for c in name.chars() {
if !c.is_ascii_alphanumeric() && c != '_' {
return Err(TelemetryError::InvalidName(alloc::format!(
"Invalid character '{}' in: {}",
c,
name
)));
}
}
Ok(())
}
use alloc::string::ToString;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_validate_metric_name() {
assert!(validate_metric_name("lcpfs_read_ops").is_ok());
assert!(validate_metric_name("_private_metric").is_ok());
assert!(validate_metric_name("metric123").is_ok());
assert!(validate_metric_name("").is_err());
assert!(validate_metric_name("123metric").is_err());
assert!(validate_metric_name("metric-name").is_err());
}
#[test]
fn test_histogram_observe() {
let mut hist = HistogramData::new(&[1.0, 5.0, 10.0]);
hist.observe(0.5);
hist.observe(3.0);
hist.observe(7.0);
assert_eq!(hist.count, 3);
assert!((hist.sum - 10.5).abs() < 0.001);
assert_eq!(hist.buckets[0].count, 1);
assert_eq!(hist.buckets[1].count, 2);
assert_eq!(hist.buckets[2].count, 3);
}
#[test]
fn test_metric_type() {
assert_eq!(MetricType::Counter.prometheus_type(), "counter");
assert_eq!(MetricType::Histogram.prometheus_type(), "histogram");
}
}