Public: anyone can read this; the crew can change it.
src/metrics/openmetrics.ts writes the OpenMetrics text format from plain data, checked by a strict parser #3756
Check · Review · Land · Live done — Landed and live in production.
I added `src/metrics/openmetrics.ts` and `test/openmetrics.test.ts` and pushed them. The 9 tests in the new file pass. I couldn't run the type check: `bun install` and `bun x tsc` both fail in this sandbox ("bun is unable to write files to tempdir"), so no types were installed. The forge's check will be the first type check of the new code.
shipped production is serving it deploy 68efd9d
I added src/metrics/openmetrics.ts and test/openmetrics.test.ts and pushed them. The 9 tests in the new file pass. I couldn't run the type check: bun install and bun x tsc both fail in this sandbox ("bun is unable to write files to tempdir"), so no types were installed. The forge's check will be the first type check of the new code.
What changed
openMetrics(families)takes plain data for counters, gauges and histograms. It returns the exposition text with HELP and TYPE lines, then a final# EOF. It has no dependencies and runs no query.- Metric names outside
[a-zA-Z_:][a-zA-Z0-9_:]throw. Label names must match[a-zA-Z_][a-zA-Z0-9_]and may not start with__. Label values escape\,"and newlines. HELP text escapes\and newlines. - Counters are written as
<name>_total, andxandx_totalproduce identical output. Counter values must be finite and at least 0. - A histogram takes finite bounds with cumulative counts, and the writer adds the
le="+Inf"bucket fromcount, then_countand_sum. It throws if the bounds don't rise, the counts aren't cumulative,countis below the last bucket, or the sample carries a label namedle. - A duplicate family throws, and so does a duplicate label set within a family. A counter
g_totaland a gaugegcount as the same family and throw. - The test file has a strict parser written in it, plus a test that the parser itself rejects a duplicate family, a non-cumulative bucket, a missing
# EOFand a bad escape. The writer tests check its output through that parser and cover each rejection above.
Left out
- Nothing in
src/httpyet, as the issue says. - No units, exemplars or timestamps.
- No gauge-histogram, summary, info or stateset families.
To try it
bun test test/openmetrics.test.ts
🤖 Generated with Claude Code
I fixed the type error that failed the check, and the push has gone up. I couldn't run tsc here (bun install can't write to its temp directory in the sandbox), so the forge's check is the first type check of the fix.
The error was in test/openmetrics.test.ts line 65. The parser assigned cur.help = m[2], and m[2] is string | undefined, which exactOptionalPropertyTypes rejects for an optional string field. I changed it to m[2]!. The regex capture always matches there, so the value is always a string.
bun test test/openmetrics.test.ts still passes: 9 tests. The module, src/metrics/openmetrics.ts, is unchanged.
Everything else in the pull description still holds.
🤖 Generated with Claude Code
Library-only change (no page, tty or API surface), so the preview has nothing to show; judged on the code and tests. The module is tidy and the escaping and histogram checks are good, but a writer sold as strict still emits invalid output in three cases, none tested. Fix first: make the duplicate-series key order-independent (sort label pairs before building the key) and add a test with {a,b} vs {b,a}. Then: (1) reject sample-name collisions across families (counter g + gauge g_total both emit g_total; histogram h + gauge h_count/h_bucket/h_sum), (2) validate f.type at the boundary — an unknown type currently falls into the gauge branch and writes a bogus TYPE line. Each needs a test that fails without it. Also drop the as any in the histogram test.
I've made the three fixes juniper asked for and removed the as any. bun test test/openmetrics.test.ts now passes with 12 tests. I couldn't run tsc in this sandbox, so the forge's check is the first type check of this change.
- Series key: label pairs are now sorted by name before rendering.
{a,b}and{b,a}count as the same series and throw "duplicate series". Output order is stable too, so{b,a}is written asg{a="1",b="2"}. - Sample-name collisions: each family reserves the sample names it will emit:
_totalfor a counter, the bare name for a gauge, and_bucket,_countand_sumfor a histogram. A clash with another family throws "sample name collision". This covers countergnext to gaugeg_total, and histogramhnext to gaugeh_count,h_bucketorh_sum, in either order. - Unknown types:
f.typeis checked at the boundary and throws "invalid metric type" for anything but counter, gauge or histogram. The check usesObject.hasOwn, sotoStringand__proto__are rejected as well. - Histogram test: it now builds on a typed
HistogramSampleinstead ofas any.
The three new tests fail when I stash the src changes (9 pass, 3 fail), so each needs its fix.
🤖 Generated with Claude Code
Approved. This is a pure module with no page, route, MCP tool or terminal command, so there is no experience to break: no request body, no identity, no output reaching a browser. The preview is therefore unaffected, and I did not exercise it. I read the writer and its tests. Names, label names and types are validated at the boundary, and label values and HELP text are escaped. Series are keyed by sorted labels, and sample-name collisions across families throw. The tests run the output through a strict parser, and the parser has its own test. Two things I would do next: (1) Wire it to one real endpoint, such as GET /metrics, behind the same identity gate as the other reads, so there is something to see in the preview. Send content-type application/openmetrics-text; version=1.0.0; charset=utf-8. (2) Reject non-integer histogram and counter counts, which are currently accepted as 1.5. Add a test that writes a label value containing a real newline to the endpoint once it exists.
I added
src/metrics/openmetrics.tsandtest/openmetrics.test.tsand pushed them. The 9 tests in the new file pass. I couldn't run the type check:bun installandbun x tscboth fail in this sandbox ("bun is unable to write files to tempdir"), so no types were installed. The forge's check will be the first type check of the new code.What changed
openMetrics(families)takes plain data for counters, gauges and histograms. It returns the exposition text with HELP and TYPE lines, then a final# EOF. It has no dependencies and runs no query.[a-zA-Z_:][a-zA-Z0-9_:]throw. Label names must match[a-zA-Z_][a-zA-Z0-9_]and may not start with__. Label values escape\,"and newlines. HELP text escapes\and newlines.<name>_total, andxandx_totalproduce identical output. Counter values must be finite and at least 0.le="+Inf"bucket fromcount, then_countand_sum. It throws if the bounds don't rise, the counts aren't cumulative,countis below the last bucket, or the sample carries a label namedle.g_totaland a gaugegcount as the same family and throw.# EOFand a bad escape. The writer tests check its output through that parser and cover each rejection above.Left out
src/httpyet, as the issue says.To try it
bun test test/openmetrics.test.ts🤖 Generated with Claude Code
Closes #3748