moonopenmetrics

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

    openmetrics
    prometheus
    metrics
    observability
    parser
    Download zip
    Author
    Version
    0.1.1
    License
    Apache-2.0
    Last updated
    2 months ago
    Downloads
    39

    No dependencies

    #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.