moonopenmetrics

OpenMetrics 1.0 text parser, canonical encoder, semantic validator, and document merge toolkit for MoonBit.

openmetrics
prometheus
metrics
observability
parser
moon add JYPeng487/moonopenmetrics@0.1.1
Download zip
Author
Version
0.1.1
License
Apache-2.0
Last updated
8 days ago
Downloads
7
README

#moonopenmetrics

CI

moonopenmetrics is a pure MoonBit implementation of the OpenMetrics 1.0 text exposition format. It turns scrape text into typed metric families, emits a stable canonical form, reports cross-line semantic errors, and includes a deterministic in-memory registry.

The library has no network stack, background thread, global clock, or runtime service dependency. Its parsing, validation, aggregation, and encoding logic can therefore be reused from JS, Wasm, Wasm-GC, and native programs.

#What it provides

  • HELP, TYPE, UNIT, sample, exemplar, timestamp, comment, and EOF parsing
  • Counter, gauge, histogram, gauge-histogram, summary, info, state-set, and unknown family models
  • OpenMetrics NaN, +Inf, -Inf, decimal, and scientific numbers
  • Quoted label escaping and HELP escaping with useful line/column failures
  • Canonical output with stable family, sample, and label order
  • Semantic checks for family suffixes, duplicate series/labels, counter totals, metric/family/point interleaving, cumulative histogram buckets/counts, summary quantiles, created/timestamp ordering, info/state-set values, exemplar placement/size, unit naming, family-name collisions, and mandatory EOF
  • An in-memory registry with counter, gauge, histogram, gauge-histogram, summary, info, and state-set collectors
  • Snapshot merge policies, family selection, label selection, and structural summary reports
  • A runnable validate / normalize command demonstration
  • Black-box tests for valid inputs, malformed inputs, edge cases, round trips, validation diagnostics, and registry aggregation

#Install

moon add JYPeng487/moonopenmetrics

Import the package from moon.pkg:

import { "JYPeng487/moonopenmetrics", }

#Parse, validate, and normalize

///|
test {
let input =
#|# TYPE requests counter
#|# HELP requests processed requests
#|requests_total{method="GET"} 3
#|# EOF
#|
let document = match @moonopenmetrics.parse(input) {
Ok(document) => document
Err(error) => fail(error.to_string())
}
let report = @moonopenmetrics.validate(document)
inspect(report.error_count, content="0")
inspect(document.families.length(), content="1")
inspect(@moonopenmetrics.encode(document).contains("# EOF"), content="true")
}

parse is intentionally syntax-oriented: it returns a document without # EOF and records has_eof=false. Call validate before accepting a document when semantic diagnostics matter. This is a documented validation profile, not a formal conformance certificate; see the compatibility boundary below. normalize combines parse and canonical encode without discarding a syntax error.

#Record application metrics

///|
test {
let registry = @moonopenmetrics.Registry::new()
match registry.register("requests", Counter, help="processed requests") {
Ok(_) => ()
Err(error) => fail(error)
}
match
registry.increment_counter("requests", delta=2.0, labels=[
@moonopenmetrics.Label::new("method", "GET"),
]) {
Ok(_) => ()
Err(error) => fail(error)
}
let text = registry.to_text()
inspect(text.contains("requests_total{method=\"GET\"} 2"), content="true")
inspect(
@moonopenmetrics.validate(registry.snapshot()).error_count,
content="0",
)
}

For histograms, pass non-negative, finite, and strictly increasing upper bounds. Reuse the same bucket schema for later observations with the same label set. The registry creates the required positive-infinity bucket and maintains cumulative _bucket, _sum, and _count samples:

///|
test {
let registry = @moonopenmetrics.Registry::new()
match
registry.register(
"latency_seconds",
Histogram,
help="request latency",
unit="seconds",
) {
Ok(_) => ()
Err(error) => fail(error)
}
match registry.observe_histogram("latency_seconds", 0.2, [0.1, 0.5, 1.0]) {
Ok(_) => ()
Err(error) => fail(error)
}
inspect(
registry.to_text().contains("latency_seconds_bucket{le=\"+Inf\"} 1"),
content="true",
)
}

#Runnable command

Run the bundled valid counter/histogram exposition:

moon run cmd/main

The explicit commands are:

moon run cmd/main -- validate moon run cmd/main -- normalize moon run cmd/main -- help

With no text argument they use the bundled demonstration. A short, shell-safe document can be supplied as the next argument:

moon run cmd/main -- validate "# EOF"

Some moon run and shell combinations reconstruct multiline arguments and can strip quotes inside label sets. For full files, read the file in the host application and pass its contents directly to parse; the library itself never reads a file or terminates a process. The command aborts on syntax errors or semantic validation errors, so its bundled and shell-safe cases can be used as a small CI smoke test.

#Data model

TypePurpose
DocumentOrdered families plus presence of the EOF marker
MetricFamilyHELP, TYPE, UNIT, and the family's samples
SampleName, labels, value, optional fractional Unix-second timestamp and exemplar
ExemplarObservation labels, value, and optional fractional Unix-second timestamp
ValidationReportStructured issues plus error/warning counts
RegistryExplicit, deterministic in-memory aggregation

All concrete data fields are readable. Constructors such as Label::new, Sample::new, MetricFamily::new, and Registry::new support programmatic documents as well as parsed text.

This project is an independently written MoonBit implementation based on the public OpenMetrics 1.0 specification. The specification repository is licensed under Apache-2.0. The reference scope is the text grammar, data model, and semantic requirements; this project is not a line-by-line port and contains no copied Prometheus implementation code.

The independently implemented scope includes the parser, typed document model, semantic validator, normalizer and canonical encoder, merge/select/report operations, and the validation/normalization CLI.

The Mooncakes package mohongquan0630/moon-prometheus (GitHub: LunarMetrics) is oriented toward registering, collecting, and exporting application metrics. moonopenmetrics is instead centered on a text-processing toolchain for reading, validating, normalizing, and transforming OpenMetrics documents. Its in-memory Registry is an explicit document-construction helper rather than a networked collection service. The projects therefore overlap in standard metric concepts and text output, but serve different primary workflows.

#Compatibility boundary

The implementation targets the widely interoperable ASCII identifier grammar used by Prometheus and OpenMetrics text:

  • metric: [A-Za-z_:][A-Za-z0-9_:]*
  • label: [A-Za-z_][A-Za-z0-9_]*

Label values and HELP text are Unicode. The optional quoted UTF-8 identifier extension is not implemented in version 0.1.0. The parser is canonicalizing, not lossless: unknown comments and original whitespace are not retained. Undefined backslash escapes are rejected instead of being preserved.

The validator checks bucket monotonicity and infinity-bucket/count equality independently for each non-le label group, plus quantile range and value consistency for each summary label group. Empty families and exemplar label sets are accepted; repeated metric points require explicit, strictly increasing timestamps. Histogram sum/count and summary components follow their optional OpenMetrics rules. Protobuf exchange, HTTP serving/scraping, file I/O, and concurrent mutation remain outside this text-format library.

See docs/compatibility.md for the detailed support matrix and design decisions.

#Reproduce locally

Using the MoonBit toolchain:

moon fmt --check moon info --target all moon check --target all --deny-warn moon test --target all --deny-warn moon build --target all moon run cmd/main moon package

The repository CI runs the same formatting, interface, build, test, runnable example, packaging, and no-drift checks.

#Project layout

model.mbt public domain model text.mbt / number.mbt lexical rules and escaping labels.mbt label parser and encoder sample_parser.mbt sample, timestamp, and exemplar grammar parser.mbt / encoder.mbt complete document conversion validation.mbt cross-line semantic checks registry.mbt deterministic aggregation merge.mbt snapshot merge and selection report.mbt structural and diagnostic reports cmd/main runnable validator/normalizer examples/complete.om reproducible fixture

#License

Apache-2.0. See the originality and specification-source section above for the reference scope.

#
ConflictPolicy

pub(all) enum ConflictPolicy {
Reject
KeepFirst
KeepLast
} derive(Eq)

Resolution policy for duplicate series while merging snapshots.

#
Document

pub(all) struct Document {
families : Array[MetricFamily]
has_eof : Bool
} derive(Eq)

A parsed OpenMetrics document.

has_eof records whether the mandatory # EOF marker was present. The parser preserves a document without the marker so callers can choose between lenient inspection and strict validation.

#
Document::family

fn Document::family(self : Document, name : StringView) -> MetricFamily?

Find a family by its declared name.

#
Document::new

fn Document::new() -> Document

Create an empty document.

#
Document::select_families

fn Document::select_families(self : Document, names : Array[String]) -> Document

Copy only named metric families into a new document.

#
Document::select_labels

fn Document::select_labels(self : Document, required : Array[Label]) -> Document

Copy a document while retaining only samples that contain all requested label pairs. Empty families are omitted.

#
Document::summary

fn Document::summary(self : Document) -> MetricsSummary

Return structural counts for this document.

#
Document::to_text

fn Document::to_text(self : Document, include_eof? : Bool) -> String

Encode this document in canonical form.

#
Exemplar

pub(all) struct Exemplar {
labels : Array[Label]
value : Double
timestamp : Double?
} derive(Eq)

An OpenMetrics exemplar attached to a sample.

An exemplar contains its own label set, a numeric observation and an optional Unix timestamp in seconds.

#
Exemplar::new

fn Exemplar::new(labels : Array[Label], value : Double, timestamp? : Double) -> Exemplar

Create an exemplar without a timestamp.

#
Label

pub(all) struct Label {
name : String
value : String
} derive(Eq)

A label name/value pair attached to a sample or exemplar.

#
Label::new

fn Label::new(name : String, value : String) -> Label

Create a label.

#
MergeError

pub(all) struct MergeError {
family : String
sample : String?
message : String
} derive(Eq)

Failure returned by document merging.

#
MergeError::to_string

fn MergeError::to_string(self : MergeError) -> String

Render a compact merge failure.

#
MetricFamily

pub(all) struct MetricFamily {
name : String
help : String?
metric_type : MetricType
unit : String?
samples : Array[Sample]
} derive(Eq)

Metadata and samples belonging to one metric family.

#
MetricFamily::add_sample

fn MetricFamily::add_sample(self : MetricFamily, sample : Sample) -> MetricFamily

Return a copy of a family with one extra sample.

#
MetricFamily::new

fn MetricFamily::new(name : String, metric_type? : MetricType, help? : String, unit? : String) -> MetricFamily

Create an empty metric family.

#
MetricType

pub(all) enum MetricType {
Unknown
Counter
Gauge
Histogram
GaugeHistogram
Summary
Info
StateSet
} derive(Eq)

Metric kinds defined by OpenMetrics 1.0.

Unknown is useful for untyped input where no # TYPE directive was present. GaugeHistogram is kept distinct from a regular histogram because its _gsum/_gcount suffixes have different validation rules.

#
MetricType::from_keyword

fn MetricType::from_keyword(keyword : StringView) -> Result[MetricType, String]

Parse a # TYPE keyword.

#
MetricType::to_keyword

fn MetricType::to_keyword(self : MetricType) -> String

Convert a metric type into its wire-format keyword.

#
MetricsSummary

pub(all) struct MetricsSummary {
family_count : Int
sample_count : Int
label_count : Int
exemplar_count : Int
counter_count : Int
gauge_count : Int
histogram_count : Int
gauge_histogram_count : Int
summary_count : Int
info_count : Int
state_set_count : Int
unknown_count : Int
} derive(Eq)

Structural counts for an exposition document.

#
MetricsSummary::to_text

fn MetricsSummary::to_text(self : MetricsSummary) -> String

Render summary counts on one stable machine-readable line.

#
ParseError

pub(all) struct ParseError {
line : Int
column : Int
message : String
} derive(Eq)

Syntax failure returned by the line-oriented parser.

#
ParseError::new

fn ParseError::new(line : Int, column : Int, message : String) -> ParseError

Create a parser error at a one-based line and column.

#
ParseError::to_string

fn ParseError::to_string(self : ParseError) -> String

Render a compact human-readable parser error.

#
Registry

pub struct Registry {
// private fields
}

A deterministic in-memory metric registry.

The registry deliberately has no clock, threads, network or process-global state. Callers supply labels and values, then snapshot or encode the result.

#
Registry::increment_counter

fn Registry::increment_counter(self : Registry, name : StringView, delta? : Double, labels? : Array[Label]) -> Result[Unit, String]

Increment a counter series. Negative deltas are rejected.

#
Registry::length

fn Registry::length(self : Registry) -> Int

Return the number of registered families.

#
Registry::new

fn Registry::new() -> Registry

Create an empty registry.

#
Registry::observe_histogram

fn Registry::observe_histogram(self : Registry, name : StringView, value : Double, buckets : Array[Double], labels? : Array[Label]) -> Result[Unit, String]

Observe a value in a histogram series.

buckets contains finite upper bounds in strictly increasing order. The required +Inf bucket, _sum and _count series are maintained automatically.

#
Registry::register

fn Registry::register(self : Registry, name : String, metric_type : MetricType, help? : String, unit? : String) -> Result[Unit, String]

Register metadata for a new metric family.

#
Registry::remove_family

fn Registry::remove_family(self : Registry, name : StringView) -> Bool

Remove an entire family and return whether it existed.

#
Registry::reset_family

fn Registry::reset_family(self : Registry, name : StringView) -> Result[Unit, String]

Remove all samples from a registered family while retaining its metadata.

#
Registry::set_gauge

fn Registry::set_gauge(self : Registry, name : StringView, value : Double, labels? : Array[Label]) -> Result[Unit, String]

Set a gauge series to an absolute value.

#
Registry::set_gauge_histogram

fn Registry::set_gauge_histogram(self : Registry, name : StringView, buckets : Array[(Double, Double)], sum : Double, count : Double, labels? : Array[Label]) -> Result[Unit, String]

Replace one complete gauge-histogram label group.

Each tuple contains a finite upper bound and its cumulative bucket count.

#
Registry::set_info

fn Registry::set_info(self : Registry, name : StringView, labels? : Array[Label]) -> Result[Unit, String]

Set an info series. Info samples always carry the value 1.

#
Registry::set_sample

fn Registry::set_sample(self : Registry, family_name : StringView, sample : Sample) -> Result[Unit, String]

Set or replace an arbitrary sample in a registered family.

This lower-level operation is useful for summary and info collectors.

#
Registry::set_state

fn Registry::set_state(self : Registry, name : StringView, state : String, enabled : Bool, labels? : Array[Label]) -> Result[Unit, String]

Set one state in a state-set family.

#
Registry::set_summary

fn Registry::set_summary(self : Registry, name : StringView, quantiles : Array[(Double, Double)], sum : Double, count : Double, labels? : Array[Label]) -> Result[Unit, String]

Replace one complete summary label group.

Quantile keys must be strictly increasing from zero to one, and observed quantile values must be non-decreasing.

#
Registry::snapshot

fn Registry::snapshot(self : Registry) -> Document

Copy the current registry into an OpenMetrics document.

#
Registry::to_text

fn Registry::to_text(self : Registry) -> String

Encode a snapshot of the registry.

#
Sample

pub(all) struct Sample {
name : String
labels : Array[Label]
value : Double
timestamp : Double?
exemplar : Exemplar?
} derive(Eq)

One sample line in an exposition document.

#
Sample::label

fn Sample::label(self : Sample, name : StringView) -> String?

Return the first value for a label name.

#
Sample::new

fn Sample::new(name : String, value : Double, labels? : Array[Label], timestamp? : Double, exemplar? : Exemplar) -> Sample

Create a sample.

#
Severity

pub(all) enum Severity {
Warning
Error
} derive(Eq)

Severity of a semantic validation issue.

#
ValidationIssue

pub(all) struct ValidationIssue {
severity : Severity
code : String
family : String
sample : String?
message : String
} derive(Eq)

A machine-readable semantic validation issue.

#
ValidationIssue::error

fn ValidationIssue::error(code : String, family : String, message : String, sample? : String) -> ValidationIssue

Create a validation error.

#
ValidationIssue::to_text

fn ValidationIssue::to_text(self : ValidationIssue) -> String

Render one validation issue without relying on debug formatting.

#
ValidationIssue::warning

fn ValidationIssue::warning(code : String, family : String, message : String, sample? : String) -> ValidationIssue

Create a validation warning.

#
ValidationReport

pub(all) struct ValidationReport {
issues : Array[ValidationIssue]
error_count : Int
warning_count : Int
} derive(Eq)

Aggregated result of semantic validation.

#
ValidationReport::is_valid

fn ValidationReport::is_valid(self : ValidationReport) -> Bool

Return whether the document passed without semantic errors.

#
ValidationReport::to_text

fn ValidationReport::to_text(self : ValidationReport) -> String

Render all issues followed by an error/warning total.

#
encode

fn encode(document : Document, include_eof? : Bool) -> String

Encode a document in a stable canonical layout.

Family, sample and label order are preserved. Metadata is reordered as TYPE, UNIT, HELP, then samples. The output ends with # EOF by default.

#
encode_label_set

fn encode_label_set(labels : Array[Label]) -> String

Encode labels in their existing order.

#
encode_sample

fn encode_sample(sample : Sample) -> String

Encode one sample line without assuming a metric-family type.

#
escape_help

fn escape_help(value : StringView) -> String

Escape HELP text using the quoted-string escapes from OpenMetrics.

#
escape_label_value

fn escape_label_value(value : StringView) -> String

Escape a label value for a quoted OpenMetrics label string.

#
format_number

fn format_number(value : Double) -> String

Emit an OpenMetrics floating-point token.

#
is_valid

fn is_valid(document : Document) -> Bool

Return true when semantic validation finds no errors.

#
is_valid_label_name

fn is_valid_label_name(name : StringView) -> Bool

Return whether a string is a legacy OpenMetrics label identifier.

#
is_valid_metric_name

fn is_valid_metric_name(name : StringView) -> Bool

Return whether a string is a legacy OpenMetrics metric identifier.

This intentionally accepts the interoperable ASCII form used by the Prometheus/OpenMetrics text grammar: [A-Za-z_:][A-Za-z0-9_:]*.

#
merge

fn merge(left : Document, right : Document, policy? : ConflictPolicy) -> Result[Document, MergeError]

Merge two parsed snapshots.

Metadata for a shared family must be compatible. A concrete TYPE wins over unknown; conflicting concrete types, HELP strings, or UNIT values fail. policy only resolves duplicate series with the same name and label set.

#
merge_all

fn merge_all(documents : Array[Document], policy? : ConflictPolicy) -> Result[Document, MergeError]

Merge any number of documents from left to right.

#
normalize

fn normalize(input : StringView) -> Result[String, ParseError]

Parse and immediately re-encode an exposition document.

#
parse

fn parse(input : StringView) -> Result[Document, ParseError]

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.

#
parse_document

fn parse_document(input : StringView) -> Result[Document, ParseError]

Descriptive alias for parse.

#
parse_label_set

fn parse_label_set(input : StringView) -> Result[Array[Label], String]

Parse one complete OpenMetrics label set.

Whitespace around separators is accepted, while a trailing comma is rejected.

#
parse_number

fn parse_number(value : StringView) -> Result[Double, String]

Parse an OpenMetrics floating-point token.

The grammar admits regular decimal/scientific notation plus NaN, +Inf and -Inf.

#
parse_sample_line

fn parse_sample_line(input : StringView, line? : Int) -> Result[Sample, ParseError]

Parse one complete OpenMetrics sample line.

The accepted shape is: name{labels} value [timestamp] [# {exemplar_labels} value [timestamp]].

#
parse_timestamp

fn parse_timestamp(value : StringView) -> Result[Double, String]

Parse an optional Unix timestamp in seconds.

OpenMetrics timestamps use the finite realnumber grammar and may contain fractional seconds.

#
summarize

fn summarize(document : Document) -> MetricsSummary

Count families, samples, labels, exemplars, and family types.

#
unescape_help

fn unescape_help(value : StringView) -> Result[String, String]

Decode escaped HELP text.

#
unescape_label_value

fn unescape_label_value(value : StringView) -> Result[String, String]

Decode the contents of a quoted label value.

#
validate

fn validate(document : Document) -> ValidationReport

Validate document-level and metric-family semantics.

Powered by MoonBit

Site sourceReport issuePackagesBuild queueSkillsStatistics

© 2026 mooncakes.io