///|
/// Errors raised before a label vector creates or looks up a child metric.
pub(all) suberror VectorError {
InvalidLabelName(String)
ReservedLabelName(String)
DuplicateLabelName(String)
LabelCountMismatch(expected~ : Int, actual~ : Int)
InvalidCardinalityLimit(Int)
MissingLabel(String)
UnexpectedLabel(String)
CardinalityLimitExceeded(limit~ : Int, current~ : Int)
MetricKindMismatch
MetricConstructionFailed(String)
} derive(Debug, Eq)
///|
/// Validated variable-label layout shared by CounterVec/GaugeVec/HistogramVec.
///
/// The concrete vector types are P0 implementation work; this type fixes their
/// collision-free child-key and cardinality contracts at the package boundary.
pub struct LabelSchema {
names : Array[String]
cardinality_limit : Int?
}
///|
pub fn LabelSchema::new(
names : Array[String],
cardinality_limit? : Int,
) -> LabelSchema raise VectorError {
for index, name in names {
validate_schema_label_name(name)
for previous in 0.. raise InvalidCardinalityLimit(limit)
_ => ()
}
{ names: names.copy(), cardinality_limit }
}
///|
pub fn LabelSchema::names(self : LabelSchema) -> Array[String] {
self.names.copy()
}
///|
pub fn LabelSchema::cardinality_limit(self : LabelSchema) -> Int? {
self.cardinality_limit
}
///|
pub fn LabelSchema::encode_key(
self : LabelSchema,
values : Array[String],
) -> String raise VectorError {
if values.length() != self.names.length() {
raise LabelCountMismatch(
expected=self.names.length(),
actual=values.length(),
)
}
encode_label_values(values)
}
///|
/// Convert an unordered label map into this schema's positional value array.
///
/// A map must contain exactly the declared names. This prevents a missing
/// label from silently becoming the empty string and prevents typo labels from
/// creating an unbounded accidental dimension.
pub fn LabelSchema::values_from_labels(
self : LabelSchema,
labels : Map[String, String],
) -> Array[String] raise VectorError {
for name in self.names {
if !labels.contains(name) {
raise MissingLabel(name)
}
}
for name, _ in labels {
if !schema_contains_name(self.names, name) {
raise UnexpectedLabel(name)
}
}
let values = []
for name in self.names {
values.push(labels[name])
}
values
}
///|
fn is_ascii_letter(char : Char) -> Bool {
match char {
'a'..='z' | 'A'..='Z' => true
_ => false
}
}
///|
fn is_ascii_digit(char : Char) -> Bool {
match char {
'0'..='9' => true
_ => false
}
}
///|
fn validate_schema_label_name(name : String) -> Unit raise VectorError {
if name.has_prefix("__") {
raise ReservedLabelName(name)
}
if name.is_empty() {
raise InvalidLabelName(name)
}
let mut index = 0
for char in name {
let valid = if index == 0 {
is_ascii_letter(char) || char == '_'
} else {
is_ascii_letter(char) || is_ascii_digit(char) || char == '_'
}
if !valid {
raise InvalidLabelName(name)
}
index += 1
}
}
///|
fn schema_contains_name(names : Array[String], name : String) -> Bool {
for current in names {
if current == name {
return true
}
}
false
}