Counted, not
remembered.
moonp8s is Prometheus metrics for MoonBit. Fifteen seconds of samples become one field of counters — and the two text formats a scrape can read.
Everything a scrape reads is one line.
A name, its labels in braces, a number and the instant it was read. The rest of this page is that line, arranged. The specimen below was written by the encoder in this repository — copied, not typed.
—— — —
written by this library, not typed here
text/plain; version=0.0.4; charset=utf-8
Two formats, one model. The compatibility format writes
milliseconds and stops there. OpenMetrics writes seconds, a
# UNIT line, exemplars and a closing # EOF.
Both come out of the same collected families, so a switch between
them cannot change a count.
The number above is cumulative on purpose. A histogram is not a
bucket count but a budget: le="0.1" says how many
samples took 100 milliseconds or less, so a reader never has to add
steps to answer the question it came with.
The edges decide the answer.
The same 1,027 samples, cut three ways. The count, the sum and the rate do not move — the median does, because a histogram answers with the edge it was given. Narrower steps are not a better answer; they are a more expensive question.
- samples
- —
- sum
- —
- median
- —
- p90
- —
the count is the same in all three
Three things this library will not decide for you.
When a scrape happens, what is allowed to take a name, and which format leaves the process. All three are handed in, which is why the same code serves a server, a replay of last night, and a test that needs the bytes to be the same twice.
The clock
A collection is stamped with the moment the caller says it happened. Nothing here reads a clock, so a window replayed from a fixture carries the fixture's time, and two runs of a test compare equal.
let families = registry.collect(
at=1_700_000_015.0,
)
// samples without a time of their own
// are stamped with it; a backfilled
// sample keeps the date it was given.
The names
A registry is the only place a name is spent. Register twice and the second attempt is refused with the name it would have collided with, rather than the two quietly merging into one series.
registry.register(
"jobs", "Jobs seen.", counter,
)
// Refused: "metric already
// registered: jobs"
The format
The encoder is chosen at the edge, from the request's own
Accept. One collected snapshot can leave as the
compatibility text or as OpenMetrics; the counts are the same object.
@expo.negotiate(accept)
// Text004 · OpenMetrics100
@expo.text(families)
@expo.openmetrics(families)
// same counts, different edges
Four processes, four files, one answer.
A pool of workers each counts what it saw. No worker reads another's memory, and the scrape does not wait for any of them: it reads the directory and adds up what is there. Everything the merge has to decide is decided by a name — of a file, of a mode.
The file
A worker writes its totals where the scrape can find them, one file per kind, named after its process. A file is written beside its final name and renamed into place, so a scrape never reads half of one.
@directory.write(
dir, pid, registry.collect(),
)
// counter_7.db · histogram_7.db
// gauge_sum_7.db · summary_7.db
The mode
A counter adds up, and so does a bucket. A gauge does not: three workers' queue depths are three answers to one question, and which answer is wanted is the caller's to say — the scrape cannot know.
@directory.write(
dir, pid, families,
modes=[("queue_depth", @multiproc.Mode::Sum)],
)
// all · min · max · sum · mostrecent
// each with a live spelling beside it
The last one out
A live gauge describes a process that is running now, so when the supervisor says the process is gone, the file goes with it. A counter stays: what the process counted is still true after it exits.
@directory.mark_dead(dir, pid)
// gauge_livesum_7.db removed
// counter_7.db and its totals stay
The index.
Seven packages and a facade. Import the package that owns the type you construct: a re-exported type arrives without its constructors. The complete reference, down to the last derived method, is on the API plate.