///|
priv enum ParsedDirective {
HelpDirective(String, String)
TypeDirective(String, MetricType)
UnitDirective(String, String)
EofDirective
IgnoredComment
}
///|
fn directive_error(
line : Int,
message : String,
) -> Result[ParsedDirective, ParseError] {
Err(ParseError::new(line, 1, message))
}
///|
fn parse_directive_head(
payload : StringView,
line : Int,
preserve_value_prefix? : Bool = false,
allow_empty? : Bool = false,
) -> Result[(String, String), ParseError] {
let cursor = TextCursor::new(payload)
let name = cursor.read_metric_name()
if !is_valid_metric_name(name) {
return Err(ParseError::new(line, 1, "invalid directive metric name"))
}
let has_separator = if preserve_value_prefix {
match cursor.next() {
Some(' ' | '\t') => true
_ => false
}
} else {
cursor.take_horizontal_space()
}
if !has_separator {
return Err(
ParseError::new(line, 1, "directive is missing its value after '\{name}'"),
)
}
let value = cursor.remaining()
if value == "" && !allow_empty {
return Err(ParseError::new(line, 1, "directive value must not be empty"))
}
Ok((name, value))
}
///|
fn parse_directive(
line_text : StringView,
line : Int,
) -> Result[ParsedDirective, ParseError] {
let text = line_text.trim_start()
if text == "# EOF" {
return Ok(EofDirective)
}
if text.has_prefix("# HELP ") {
let payload = text.sub(start=7)
let (name, encoded) = match
parse_directive_head(
payload,
line,
preserve_value_prefix=true,
allow_empty=true,
) {
Ok(pair) => pair
Err(error) => return Err(error)
}
if encoded == "" {
return Ok(IgnoredComment)
}
let help = match unescape_help(encoded) {
Ok(value) => value
Err(error) => return directive_error(line, error)
}
return Ok(HelpDirective(name, help))
}
if text.has_prefix("# TYPE ") {
let payload = text.sub(start=7)
let (name, keyword) = match parse_directive_head(payload, line) {
Ok(pair) => pair
Err(error) => return Err(error)
}
if keyword.contains(" ") || keyword.contains("\t") {
return directive_error(line, "TYPE directive has extra fields")
}
let metric_type = match MetricType::from_keyword(keyword) {
Ok(value) => value
Err(error) => return directive_error(line, error)
}
return Ok(TypeDirective(name, metric_type))
}
if text.has_prefix("# UNIT ") {
let payload = text.sub(start=7)
let (name, unit) = match
parse_directive_head(payload, line, allow_empty=true) {
Ok(pair) => pair
Err(error) => return Err(error)
}
if unit == "" {
return Ok(IgnoredComment)
}
if unit.contains(" ") || unit.contains("\t") {
return directive_error(line, "UNIT directive has extra fields")
}
return Ok(UnitDirective(name, unit))
}
Ok(IgnoredComment)
}
///|
fn string_array_contains(values : Array[String], value : StringView) -> Bool {
for item in values {
if value.equal_to_string(item) {
return true
}
}
false
}
///|
fn enter_family_sequence(
active : String?,
closed : Array[String],
name : String,
line : Int,
) -> Result[String, ParseError] {
match active {
None => Ok(name)
Some(current) =>
if current == name {
Ok(current)
} else if string_array_contains(closed, name) {
Err(
ParseError::new(
line,
1,
"metric family '\{name}' is interleaved with another family",
),
)
} else {
closed.push(current)
Ok(name)
}
}
}
///|
fn find_family_index(families : Array[MetricFamily], name : StringView) -> Int {
for index in 0.. Int {
let found = find_family_index(families, name)
if found >= 0 {
found
} else {
families.push(MetricFamily::new(name))
families.length() - 1
}
}
///|
fn sample_matches_family(family : MetricFamily, sample_name : String) -> Bool {
match family.metric_type {
Counter =>
sample_name == "\{family.name}_total" ||
sample_name == "\{family.name}_created"
Histogram =>
sample_name == "\{family.name}_bucket" ||
sample_name == "\{family.name}_sum" ||
sample_name == "\{family.name}_count" ||
sample_name == "\{family.name}_created"
GaugeHistogram =>
sample_name == "\{family.name}_bucket" ||
sample_name == "\{family.name}_gsum" ||
sample_name == "\{family.name}_gcount"
Summary =>
sample_name == family.name ||
sample_name == "\{family.name}_sum" ||
sample_name == "\{family.name}_count" ||
sample_name == "\{family.name}_created"
Info => sample_name == "\{family.name}_info"
Unknown | Gauge | StateSet => sample_name == family.name
}
}
///|
fn find_sample_family(
families : Array[MetricFamily],
sample : Sample,
active_family : String?,
) -> Int {
match active_family {
Some(name) => {
let active = find_family_index(families, name)
if active >= 0 && sample_matches_family(families[active], sample.name) {
return active
}
}
None => ()
}
let exact = find_family_index(families, sample.name)
if exact >= 0 && sample_matches_family(families[exact], sample.name) {
return exact
}
for index in 0.. Result[Unit, ParseError] {
if family.samples.is_empty() {
Ok(())
} else {
Err(
ParseError::new(
line,
1,
"metadata for family '\{family.name}' must precede its samples",
),
)
}
}
///|
/// Parse an OpenMetrics text exposition document.
///
/// Unknown `#` comments are ignored. Missing `# EOF` is preserved as
/// `Document.has_eof = false` and reported by `validate`, allowing callers to
/// inspect partially written scrape output.
pub fn parse(input : StringView) -> Result[Document, ParseError] {
let families : Array[MetricFamily] = []
let closed_families : Array[String] = []
let seen_type_names : Array[String] = []
let mut active_family : String? = None
let mut line_number = 0
let mut has_eof = false
for raw_line in input.split("\n") {
line_number += 1
let line_text = raw_line.trim_end(chars="\r")
if line_text.trim().is_empty() {
continue
}
if has_eof {
return Err(
ParseError::new(
line_number, 1, "non-empty content is not allowed after # EOF",
),
)
}
if line_text.trim_start().has_prefix("#") {
let directive = match parse_directive(line_text, line_number) {
Ok(value) => value
Err(error) => return Err(error)
}
match directive {
IgnoredComment => ()
EofDirective => has_eof = true
HelpDirective(name, help) => {
active_family = Some(
match
enter_family_sequence(
active_family, closed_families, name, line_number,
) {
Ok(value) => value
Err(error) => return Err(error)
},
)
let index = ensure_family(families, name)
let family = families[index]
match reject_late_metadata(family, line_number) {
Err(error) => return Err(error)
Ok(_) => ()
}
if family.help is Some(_) {
return Err(
ParseError::new(
line_number,
1,
"duplicate HELP directive for '\{family.name}'",
),
)
}
families[index] = { ..family, help: Some(help) }
}
TypeDirective(name, metric_type) => {
active_family = Some(
match
enter_family_sequence(
active_family, closed_families, name, line_number,
) {
Ok(value) => value
Err(error) => return Err(error)
},
)
let index = ensure_family(families, name)
let family = families[index]
match reject_late_metadata(family, line_number) {
Err(error) => return Err(error)
Ok(_) => ()
}
if string_array_contains(seen_type_names, name) {
return Err(
ParseError::new(
line_number,
1,
"duplicate TYPE directive for '\{family.name}'",
),
)
}
seen_type_names.push(name)
families[index] = { ..family, metric_type, }
}
UnitDirective(name, unit) => {
active_family = Some(
match
enter_family_sequence(
active_family, closed_families, name, line_number,
) {
Ok(value) => value
Err(error) => return Err(error)
},
)
let index = ensure_family(families, name)
let family = families[index]
match reject_late_metadata(family, line_number) {
Err(error) => return Err(error)
Ok(_) => ()
}
if family.unit is Some(_) {
return Err(
ParseError::new(
line_number,
1,
"duplicate UNIT directive for '\{family.name}'",
),
)
}
families[index] = { ..family, unit: Some(unit) }
}
}
} else {
let sample = match parse_sample_line(line_text, line=line_number) {
Ok(value) => value
Err(error) => return Err(error)
}
let found = find_sample_family(families, sample, active_family)
let index = if found >= 0 {
found
} else {
ensure_family(families, sample.name)
}
let family = families[index]
active_family = Some(
match
enter_family_sequence(
active_family,
closed_families,
family.name,
line_number,
) {
Ok(value) => value
Err(error) => return Err(error)
},
)
families[index] = family.add_sample(sample)
}
}
Ok({ families, has_eof })
}
///|
/// Descriptive alias for `parse`.
pub fn parse_document(input : StringView) -> Result[Document, ParseError] {
parse(input)
}