moonp8s Examples · five programs, run Examples GitHub
Examples

Four programs,
as they ran.

The source on the left is the whole file. The output on the right is what this library printed when that file was run — not a sketch of what it might print.

Programs
five
Output
captured, not written
Run
moon run src/examples/…
01

A counter.

src/examples/04-counter/main.mbt

moon run src/examples/04-counter

// Copyright 2026 Leo Cheng
// SPDX-License-Identifier: Apache-2.0

///|
/// A counter only goes up, so the only way to read it is to scrape it twice. This
/// program prints one scrape.
fn main raise {
  let registry = @metric.Registry::new()
  let jobs = registry.counter("jobs_processed", "Jobs finished.")
  jobs.inc()
  jobs.inc(by=11.0)
  println(@expo.text(registry.collect()))
}

output

# HELP jobs_processed_total Jobs finished. # TYPE jobs_processed_total counter jobs_processed_total 12.0

The registered name was jobs_processed; the wire name carries _total, because that is what a counter is called in the exposition format.

02

A gauge.

src/examples/05-gauge/main.mbt

moon run src/examples/05-gauge

// Copyright 2026 Leo Cheng
// SPDX-License-Identifier: Apache-2.0

///|
/// A gauge is the number at the moment of the scrape: it can go down, and it can be
/// taken from a function instead of stored.
fn main raise {
  let registry = @metric.Registry::new()
  let running = registry.gauge("jobs_running", "Jobs running now.")
  running.set(3.0)
  running.dec()
  println(@expo.text(registry.collect()))
}

output

# HELP jobs_running Jobs running now. # TYPE jobs_running gauge jobs_running 2.0

One gauge, one line. A gauge is the only kind whose name is written exactly as it was registered — no suffix, no rewrite.

03

A histogram.

src/examples/06-histogram/main.mbt

moon run src/examples/06-histogram

// Copyright 2026 Leo Cheng
// SPDX-License-Identifier: Apache-2.0

///|
/// Two observations, counted into the fourteen bounds the official clients share.
/// The buckets are cumulative on the wire: `le="0.5"` counts everything at or below
/// half a second.
fn main raise {
  let registry = @metric.Registry::new()
  let latency = registry.histogram(
    "request_duration_seconds",
    "Request latency.",
  )
  latency.observe(0.041)
  latency.observe(0.317)
  println(@expo.text(registry.collect()))
}

output

# HELP request_duration_seconds Request latency. # TYPE request_duration_seconds histogram request_duration_seconds_bucket{le="0.005"} 0.0 request_duration_seconds_bucket{le="0.01"} 0.0 request_duration_seconds_bucket{le="0.025"} 0.0 request_duration_seconds_bucket{le="0.05"} 1.0 request_duration_seconds_bucket{le="0.075"} 1.0 request_duration_seconds_bucket{le="0.1"} 1.0 request_duration_seconds_bucket{le="0.25"} 1.0 request_duration_seconds_bucket{le="0.5"} 2.0 request_duration_seconds_bucket{le="0.75"} 2.0 request_duration_seconds_bucket{le="1.0"} 2.0 request_duration_seconds_bucket{le="2.5"} 2.0 request_duration_seconds_bucket{le="5.0"} 2.0 request_duration_seconds_bucket{le="7.5"} 2.0 request_duration_seconds_bucket{le="10.0"} 2.0 request_duration_seconds_bucket{le="+Inf"} 2.0 request_duration_seconds_count 2.0 request_duration_seconds_sum 0.358

The first observation, 41 ms, crosses the first four steps at once, so the count jumps at le="0.05". The second, 317 ms, only moves the steps above 0.25.

04

Both formats.

src/examples/07-formats/main.mbt

moon run src/examples/07-formats

// Copyright 2026 Leo Cheng
// SPDX-License-Identifier: Apache-2.0

///|
/// One snapshot, two formats. The counts are the same object; only the spelling of
/// the timestamp and the ending differ.
fn main raise {
  let registry = @metric.Registry::new()
  let latency = registry.histogram(
    "request_duration_seconds",
    "Request latency.",
  )
  latency.observe(0.041)
  latency.observe(0.317)
  let families = registry.collect(at=1_700_000_015.0)
  println("=== 0.0.4 ===")
  println(@expo.text(families))
  println("=== 1.0 ===")
  println(@expo.openmetrics(families))
}

output · the tail of each, where they differ

=== 0.0.4 === request_duration_seconds_bucket{le="+Inf"} 2.0 1700000015000 request_duration_seconds_count 2.0 1700000015000 request_duration_seconds_sum 0.358 1700000015000 === 1.0 === request_duration_seconds_bucket{le="+Inf"} 2.0 1.700000015e+09 request_duration_seconds_count 2.0 1.700000015e+09 request_duration_seconds_sum 0.358 1.700000015e+09 # EOF

Same three numbers, twice. The first format counts in milliseconds and says nothing at the end; the second counts in seconds and closes with # EOF, which is how a reader knows the body is whole.

05

Three workers.

src/examples/09-multiprocess/main.mbt

moon run src/examples/09-multiprocess

///|
/// The work one worker did. The numbers differ per process so the merge has
/// something to add up.
fn registry_for(pid : Int) -> @metric.Registry raise @metric.Refused {
  let registry = @metric.Registry::new(
    prefix="p8s",
    labels=[("service", "checkout")],
  )
  let orders = registry.counter("orders", "Orders placed.")
  // ... a labelled counter, a histogram, a summary, a gauge
  orders.inc(by=(pid * 10).to_double())
  registry
}

///|
async fn flush(dir : String, pid : Int) -> Unit raise @metric.Refused {
  let registry = registry_for(pid)
  let written = @directory.write(
    dir, pid, registry.collect(),
    modes=[("p8s_queue_depth", @multiproc.Mode::Sum)],
  )
  println("worker " + pid.to_string() + ": "
    + written.files.to_string() + " files")
}

output · three files each, then one scrape

worker 1: 4 files, 12 values worker 2: 4 files, 12 values worker 3: 4 files, 12 values # HELP p8s_orders_total Orders placed, by response code. # TYPE p8s_orders_total counter p8s_orders_total{service="checkout"} 60.0 # HELP p8s_queue_depth Jobs waiting in this worker. # TYPE p8s_queue_depth gauge p8s_queue_depth{service="checkout"} 18.0 # HELP p8s_request_seconds Seconds a request took. # TYPE p8s_request_seconds histogram p8s_request_seconds_bucket{le="+Inf",service="checkout"} 6.0 p8s_request_seconds_count{service="checkout"} 6.0 p8s_request_seconds_sum{service="checkout"} 1.1

Ten orders, twenty, thirty: the counter adds them up. The queue depth was three, six and nine, and Mode::Sum answers eighteen, which is what the pool as a whole is holding. Six requests went through three processes and the histogram kept one set of edges.