use std::collections::{BTreeSet, HashMap};
use std::hash::Hasher;
use fnv::FnvHasher;
use crate::errors::{Error, Result};
use crate::metrics::SEPARATOR_BYTE;
use crate::proto::LabelPair;
fn is_valid_metric_name(name: &str) -> bool {
fn valid_start(c: char) -> bool {
c.is_ascii()
&& match c as u8 {
b'a'..=b'z' | b'A'..=b'Z' | b'_' | b':' => true,
_ => false,
}
}
fn valid_char(c: char) -> bool {
c.is_ascii()
&& match c as u8 {
b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9' | b'_' | b':' => true,
_ => false,
}
}
name.starts_with(valid_start) && !name.contains(|c| !valid_char(c))
}
fn is_valid_label_name(name: &str) -> bool {
fn valid_start(c: char) -> bool {
c.is_ascii()
&& match c as u8 {
b'a'..=b'z' | b'A'..=b'Z' | b'_' => true,
_ => false,
}
}
fn valid_char(c: char) -> bool {
c.is_ascii()
&& match c as u8 {
b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9' | b'_' => true,
_ => false,
}
}
name.starts_with(valid_start) && !name.contains(|c| !valid_char(c))
}
#[derive(Clone, Debug)]
pub struct Desc {
pub fq_name: String,
pub help: String,
pub const_label_pairs: Vec<LabelPair>,
pub variable_labels: Vec<String>,
pub id: u64,
pub dim_hash: u64,
}
impl Desc {
pub fn new(
fq_name: String,
help: String,
variable_labels: Vec<String>,
const_labels: HashMap<String, String>,
) -> Result<Desc> {
let mut desc = Desc {
fq_name: fq_name.clone(),
help,
const_label_pairs: Vec::with_capacity(const_labels.len()),
variable_labels,
id: 0,
dim_hash: 0,
};
if desc.help.is_empty() {
return Err(Error::Msg("empty help string".into()));
}
if !is_valid_metric_name(&desc.fq_name) {
return Err(Error::Msg(format!(
"'{}' is not a valid metric name",
desc.fq_name
)));
}
let mut label_values = Vec::with_capacity(const_labels.len() + 1);
label_values.push(fq_name);
let mut label_names = BTreeSet::new();
for label_name in const_labels.keys() {
if !is_valid_label_name(label_name) {
return Err(Error::Msg(format!(
"'{}' is not a valid label name",
&label_name
)));
}
if !label_names.insert(label_name.clone()) {
return Err(Error::Msg(format!(
"duplicate const label name {}",
label_name
)));
}
}
for label_name in &label_names {
label_values.push(const_labels.get(label_name).cloned().unwrap());
}
for label_name in &desc.variable_labels {
if !is_valid_label_name(label_name) {
return Err(Error::Msg(format!(
"'{}' is not a valid label name",
&label_name
)));
}
if !label_names.insert(format!("${}", label_name)) {
return Err(Error::Msg(format!(
"duplicate variable label name {}",
label_name
)));
}
}
let mut vh = FnvHasher::default();
for val in &label_values {
vh.write(val.as_bytes());
vh.write_u8(SEPARATOR_BYTE);
}
desc.id = vh.finish();
let mut lh = FnvHasher::default();
lh.write(desc.help.as_bytes());
lh.write_u8(SEPARATOR_BYTE);
for label_name in &label_names {
lh.write(label_name.as_bytes());
lh.write_u8(SEPARATOR_BYTE);
}
desc.dim_hash = lh.finish();
for (key, value) in const_labels {
let mut label_pair = LabelPair::default();
label_pair.set_name(key);
label_pair.set_value(value);
desc.const_label_pairs.push(label_pair);
}
desc.const_label_pairs.sort();
Ok(desc)
}
}
pub trait Describer {
fn describe(&self) -> Result<Desc>;
}
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use crate::desc::{is_valid_label_name, is_valid_metric_name, Desc};
use crate::errors::Error;
#[test]
fn test_is_valid_metric_name() {
let tbl = [
(":", true),
("_", true),
("a", true),
(":9", true),
("_9", true),
("a9", true),
("a_b_9_d:x_", true),
("9", false),
("9:", false),
("9_", false),
("9a", false),
("a-", false),
];
for &(name, expected) in &tbl {
assert_eq!(is_valid_metric_name(name), expected);
}
}
#[test]
fn test_is_valid_label_name() {
let tbl = [
("_", true),
("a", true),
("_9", true),
("a9", true),
("a_b_9_dx_", true),
(":", false),
(":9", false),
("9", false),
("9:", false),
("9_", false),
("9a", false),
("a-", false),
("a_b_9_d:x_", false),
];
for &(name, expected) in &tbl {
assert_eq!(is_valid_label_name(name), expected);
}
}
#[test]
fn test_invalid_const_label_name() {
for &name in &["-dash", "9gag", ":colon", "colon:", "has space"] {
let res = Desc::new(
"name".into(),
"help".into(),
vec![name.into()],
HashMap::new(),
)
.err()
.expect(format!("expected error for {}", name).as_ref());
match res {
Error::Msg(msg) => assert_eq!(msg, format!("'{}' is not a valid label name", name)),
other => panic!(other),
};
}
}
#[test]
fn test_invalid_variable_label_name() {
for &name in &["-dash", "9gag", ":colon", "colon:", "has space"] {
let mut labels = HashMap::new();
labels.insert(name.into(), "value".into());
let res = Desc::new("name".into(), "help".into(), vec![], labels)
.err()
.expect(format!("expected error for {}", name).as_ref());
match res {
Error::Msg(msg) => assert_eq!(msg, format!("'{}' is not a valid label name", name)),
other => panic!(other),
};
}
}
#[test]
fn test_invalid_metric_name() {
for &name in &["-dash", "9gag", "has space"] {
let res = Desc::new(name.into(), "help".into(), vec![], HashMap::new())
.err()
.expect(format!("expected error for {}", name).as_ref());
match res {
Error::Msg(msg) => {
assert_eq!(msg, format!("'{}' is not a valid metric name", name))
}
other => panic!(other),
};
}
}
}