pub const DEFAULT_MAX_LABELS: usize = 10;
pub const DEFAULT_BUCKETS: [f64; 11] = [
0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0,
];
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MetricType {
Gauge,
Counter,
Histogram,
}
impl MetricType {
pub fn name(&self) -> &'static str {
match self {
Self::Gauge => "gauge",
Self::Counter => "counter",
Self::Histogram => "histogram",
}
}
}
#[derive(Debug, Clone, Default)]
pub struct Labels {
pairs: Vec<(String, String)>,
}
impl Labels {
pub fn new() -> Self {
Self { pairs: Vec::new() }
}
pub fn add(mut self, key: &str, value: &str) -> Self {
self.pairs.push((key.to_string(), value.to_string()));
self
}
pub fn format(&self) -> String {
if self.pairs.is_empty() {
return String::new();
}
let parts: Vec<String> = self
.pairs
.iter()
.map(|(k, v)| format!("{}=\"{}\"", k, escape_label_value(v)))
.collect();
format!("{{{}}}", parts.join(","))
}
pub fn len(&self) -> usize {
self.pairs.len()
}
pub fn is_empty(&self) -> bool {
self.pairs.is_empty()
}
}
pub fn escape_label_value(s: &str) -> String {
s.replace('\\', "\\\\")
.replace('"', "\\\"")
.replace('\n', "\\n")
}
#[derive(Debug, Clone)]
pub struct HistogramBuckets {
pub boundaries: Vec<f64>,
pub counts: Vec<u64>,
pub sum: f64,
pub count: u64,
}
impl Default for HistogramBuckets {
fn default() -> Self {
Self::with_buckets(&DEFAULT_BUCKETS)
}
}
impl HistogramBuckets {
pub fn with_buckets(boundaries: &[f64]) -> Self {
Self {
boundaries: boundaries.to_vec(),
counts: vec![0; boundaries.len()],
sum: 0.0,
count: 0,
}
}
pub fn observe(&mut self, value: f64) {
self.sum += value;
self.count += 1;
for (i, &boundary) in self.boundaries.iter().enumerate() {
if value <= boundary {
self.counts[i] += 1;
}
}
}
pub fn format(&self, name: &str, labels: &Labels) -> String {
let mut lines = Vec::new();
let label_str = labels.format();
let mut cumulative = 0u64;
for (i, &boundary) in self.boundaries.iter().enumerate() {
cumulative += self.counts[i];
let bucket_label = if label_str.is_empty() {
format!("{{le=\"{}\"}}", boundary)
} else {
format!(
"{{le=\"{}\",{}}}",
boundary,
&label_str[1..label_str.len() - 1]
)
};
lines.push(format!("{}_bucket{} {}", name, bucket_label, cumulative));
}
let inf_label = if label_str.is_empty() {
"{le=\"+Inf\"}".to_string()
} else {
format!("{{le=\"+Inf\",{}}}", &label_str[1..label_str.len() - 1])
};
lines.push(format!("{}_bucket{} {}", name, inf_label, self.count));
lines.push(format!("{}_sum{} {}", name, label_str, self.sum));
lines.push(format!("{}_count{} {}", name, label_str, self.count));
lines.join("\n")
}
}
#[derive(Debug, Clone)]
pub struct MetricDef {
pub name: String,
pub help: String,
pub metric_type: MetricType,
}
impl MetricDef {
pub fn new(name: &str, help: &str, metric_type: MetricType) -> Self {
Self {
name: name.to_string(),
help: help.to_string(),
metric_type,
}
}
pub fn format_help(&self) -> String {
format!("# HELP {} {}", self.name, self.help)
}
pub fn format_type(&self) -> String {
format!("# TYPE {} {}", self.name, self.metric_type.name())
}
}
#[derive(Debug, Clone)]
pub struct GaugeValue {
pub value: f64,
pub labels: Labels,
pub timestamp: Option<u64>,
}
impl GaugeValue {
pub fn new(value: f64) -> Self {
Self {
value,
labels: Labels::new(),
timestamp: None,
}
}
pub fn with_labels(mut self, labels: Labels) -> Self {
self.labels = labels;
self
}
pub fn with_timestamp(mut self, ts: u64) -> Self {
self.timestamp = Some(ts);
self
}
pub fn format(&self, name: &str) -> String {
let label_str = self.labels.format();
let ts_str = self
.timestamp
.map(|t| format!(" {}", t))
.unwrap_or_default();
format!("{}{} {}{}", name, label_str, self.value, ts_str)
}
}
#[derive(Debug, Clone)]
pub struct CounterValue {
pub value: u64,
pub labels: Labels,
}
impl CounterValue {
pub fn new(value: u64) -> Self {
Self {
value,
labels: Labels::new(),
}
}
pub fn with_labels(mut self, labels: Labels) -> Self {
self.labels = labels;
self
}
pub fn format(&self, name: &str) -> String {
let label_str = self.labels.format();
format!("{}{} {}", name, label_str, self.value)
}
}
#[derive(Debug, Clone)]
pub struct HistogramValue {
pub buckets: HistogramBuckets,
pub labels: Labels,
}
impl HistogramValue {
pub fn new() -> Self {
Self {
buckets: HistogramBuckets::default(),
labels: Labels::new(),
}
}
pub fn with_buckets(boundaries: &[f64]) -> Self {
Self {
buckets: HistogramBuckets::with_buckets(boundaries),
labels: Labels::new(),
}
}
pub fn with_labels(mut self, labels: Labels) -> Self {
self.labels = labels;
self
}
pub fn observe(&mut self, value: f64) {
self.buckets.observe(value);
}
pub fn format(&self, name: &str) -> String {
self.buckets.format(name, &self.labels)
}
}
impl Default for HistogramValue {
fn default() -> Self {
Self::new()
}
}
pub fn validate_metric_name(name: &str) -> bool {
if name.is_empty() {
return false;
}
let first = name.chars().next().expect("non-empty string");
if !first.is_ascii_lowercase() && first != '_' {
return false;
}
name.chars()
.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_')
}