use std::fmt;
pub(super) const MAX_NAME_BYTES: usize = 128;
pub(super) const MAX_LABEL_NAME_BYTES: usize = 64;
pub(super) const MAX_LABEL_VALUE_BYTES: usize = 256;
pub(super) const MAX_HELP_BYTES: usize = 512;
pub(super) const MAX_LABELS: usize = 16;
pub(super) const MAX_SERIES_PER_SENDER: usize = 1024;
pub(super) const RESERVED_NAME_PREFIX: &str = "shep_";
pub(super) const SENDER_LABELS: [&str; 5] = ["sheep", "id", "fold", "dog", "source"];
pub(super) const REFUSED_TOTAL: &str = "shep_channel_samples_refused_total";
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub(crate) enum RefusalReason {
ReservedName,
InvalidName,
InvalidLabel,
ReservedLabel,
TooLong,
SeriesCap,
TypeConflict,
}
impl RefusalReason {
pub(crate) fn label(self) -> &'static str {
match self {
Self::ReservedName => "reserved_name",
Self::InvalidName => "invalid_name",
Self::InvalidLabel => "invalid_label",
Self::ReservedLabel => "reserved_label",
Self::TooLong => "too_long",
Self::SeriesCap => "series_cap",
Self::TypeConflict => "type_conflict",
}
}
fn explain(self) -> &'static str {
match self {
Self::ReservedName => "the `shep_` prefix belongs to the shepherd",
Self::InvalidName => {
"the name must match `[a-zA-Z_:][a-zA-Z0-9_:]*` and stay under 128 bytes"
}
Self::InvalidLabel => {
"a label name must match `[a-zA-Z_][a-zA-Z0-9_]*`, stay under 64 bytes and not start with `__`"
}
Self::ReservedLabel => "the label name is one this dog writes for the sender",
Self::TooLong => {
"the sample is over 16 labels, or a label value or help text is over its cap"
}
Self::SeriesCap => {
"this sender already holds 1024 series, and a stable series is never evicted for a new one"
}
Self::TypeConflict => "this name is already held as the other kind",
}
}
}
impl fmt::Display for RefusalReason {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.label())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct Refusal {
pub(crate) sender: u32,
name: String,
pub(crate) reason: RefusalReason,
pub(crate) first: bool,
}
impl Refusal {
pub(crate) fn new(sender: u32, name: &str, reason: RefusalReason, first: bool) -> Self {
Self {
sender,
name: name.to_string(),
reason,
first,
}
}
}
impl fmt::Display for Refusal {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"refused a sample from sender {} named {:?} ({}): {}",
self.sender,
self.name,
self.reason,
self.reason.explain(),
)
}
}
pub(crate) fn refusal_for(
name: &str,
labels: &[(String, String)],
help: Option<&str>,
) -> Option<RefusalReason> {
if name.starts_with(RESERVED_NAME_PREFIX) {
return Some(RefusalReason::ReservedName);
}
if name.len() > MAX_NAME_BYTES || !valid_metric_name(name) {
return Some(RefusalReason::InvalidName);
}
for (key, _) in labels {
if key.starts_with("__") || key.len() > MAX_LABEL_NAME_BYTES || !valid_label_name(key) {
return Some(RefusalReason::InvalidLabel);
}
if SENDER_LABELS.contains(&key.as_str()) {
return Some(RefusalReason::ReservedLabel);
}
}
if labels.len() > MAX_LABELS
|| labels
.iter()
.any(|(_, value)| value.len() > MAX_LABEL_VALUE_BYTES)
|| help.is_some_and(|text| text.len() > MAX_HELP_BYTES)
{
return Some(RefusalReason::TooLong);
}
None
}
fn valid_metric_name(name: &str) -> bool {
let mut chars = name.chars();
let Some(first) = chars.next() else {
return false;
};
(first.is_ascii_alphabetic() || first == '_' || first == ':')
&& chars.all(|ch| ch.is_ascii_alphanumeric() || ch == '_' || ch == ':')
}
fn valid_label_name(name: &str) -> bool {
let mut chars = name.chars();
let Some(first) = chars.next() else {
return false;
};
(first.is_ascii_alphabetic() || first == '_')
&& chars.all(|ch| ch.is_ascii_alphanumeric() || ch == '_')
}
#[cfg(test)]
mod tests {
use super::*;
fn labels(pairs: &[(&str, &str)]) -> Vec<(String, String)> {
pairs
.iter()
.map(|(key, value)| (key.to_string(), value.to_string()))
.collect()
}
#[test]
fn the_published_grammar_is_accepted_at_its_edges() {
let at_the_name_cap = "n".repeat(MAX_NAME_BYTES);
let at_the_label_cap = "k".repeat(MAX_LABEL_NAME_BYTES);
let at_the_value_cap = "v".repeat(MAX_LABEL_VALUE_BYTES);
let at_the_help_cap = "h".repeat(MAX_HELP_BYTES);
let at_the_label_count = (0..MAX_LABELS)
.map(|i| (format!("k{i}"), at_the_value_cap.clone()))
.collect::<Vec<_>>();
for name in [
"rps",
"_rps",
"r:ps",
"r_ps2",
"a:b_c9",
at_the_name_cap.as_str(),
] {
assert_eq!(
refusal_for(name, &[], None),
None,
"{name} must be accepted"
);
}
assert_eq!(
refusal_for("queue_depth", &at_the_label_count, Some(&at_the_help_cap)),
None,
"16 labels, a 256-byte value and 512 bytes of help is the widest legal sample"
);
assert_eq!(
refusal_for(
"queue_depth",
&labels(&[("_key", "v"), (&at_the_label_cap, "v")]),
None
),
None,
"a leading single underscore is the grammar's own spelling"
);
}
#[test]
fn a_name_the_dog_already_uses_is_refused() {
assert_eq!(
refusal_for("shep_sheep_uptime_seconds", &[], None),
Some(RefusalReason::ReservedName)
);
assert_eq!(
refusal_for("shep_mine", &[], None),
Some(RefusalReason::ReservedName)
);
}
#[test]
fn a_name_outside_the_grammar_or_the_cap_is_refused() {
let too_long = "n".repeat(MAX_NAME_BYTES + 1);
for name in ["9lives", "r ps", "r-ps", "", "\u{e9}lan", too_long.as_str()] {
assert_eq!(
refusal_for(name, &[], None),
Some(RefusalReason::InvalidName),
"{name:?}"
);
}
}
#[test]
fn a_label_name_outside_the_grammar_or_the_cap_is_refused() {
let too_long = "k".repeat(MAX_LABEL_NAME_BYTES + 1);
for key in [
"9key",
"a key",
"a-key",
"",
"__internal",
too_long.as_str(),
] {
assert_eq!(
refusal_for("queue_depth", &labels(&[(key, "v")]), None),
Some(RefusalReason::InvalidLabel),
"{key:?}"
);
}
}
#[test]
fn a_label_named_like_a_sender_label_is_refused() {
for key in SENDER_LABELS {
assert_eq!(
refusal_for("queue_depth", &labels(&[(key, "web")]), None),
Some(RefusalReason::ReservedLabel),
"{key:?}"
);
}
}
#[test]
fn a_sample_over_a_cap_is_refused() {
let too_many = (0..=MAX_LABELS)
.map(|i| (format!("k{i}"), "v".to_string()))
.collect::<Vec<_>>();
assert_eq!(
refusal_for("queue_depth", &too_many, None),
Some(RefusalReason::TooLong)
);
let long_value = "v".repeat(MAX_LABEL_VALUE_BYTES + 1);
assert_eq!(
refusal_for("queue_depth", &labels(&[("model", &long_value)]), None),
Some(RefusalReason::TooLong)
);
let long_help = "h".repeat(MAX_HELP_BYTES + 1);
assert_eq!(
refusal_for("queue_depth", &[], Some(&long_help)),
Some(RefusalReason::TooLong)
);
}
#[test]
fn a_refusal_names_the_rule_it_broke() {
let refusal = Refusal::new(7, "r ps", RefusalReason::InvalidName, true);
let line = refusal.to_string();
assert_eq!(
line,
"refused a sample from sender 7 named \"r ps\" (invalid_name): the name must match `[a-zA-Z_:][a-zA-Z0-9_:]*` and stay under 128 bytes"
);
assert_eq!(
Refusal::new(1, "a\nb", RefusalReason::TooLong, true).to_string(),
"refused a sample from sender 1 named \"a\\nb\" (too_long): the sample is over 16 labels, or a label value or help text is over its cap"
);
}
}