Skip to content

feat(metrics): Add native histograms and Prometheus protobuf exposition - #856

Open
ethanolchik wants to merge 3 commits into
prometheus:mainfrom
ethanolchik:feat/native-histograms
Open

feat(metrics): Add native histograms and Prometheus protobuf exposition#856
ethanolchik wants to merge 3 commits into
prometheus:mainfrom
ethanolchik:feat/native-histograms

Conversation

@ethanolchik

Copy link
Copy Markdown

Applications currently cannot expose native histogram samples from this client. This adds opt-in native collection to Histogram and the Prometheus protobuf exposition needed to scrape it.

Fixes #576.

const registry = new client.Registry(
  client.Registry.PROMETHEUS_PROTOBUF_CONTENT_TYPE,
);
const histogram = new client.Histogram({
  name: 'request_duration_seconds',
  help: 'Request duration',
  nativeHistogramBucketFactor: 1.1,
  buckets: [],
  registers: [registry],
});

Existing histogram configurations retain classic behavior. Native histograms can retain explicit classic buckets for migration, or use buckets: [] for native-only protobuf output. Protobuf registry methods return a Buffer, represented as Uint8Array in the public TypeScript declarations.

The implementation includes:

  • Standard exponential schemas -4 through 8, positive and negative sparse buckets, a configurable zero bucket, and native exemplars. The default budget of 160 populated buckets reduces resolution as needed and remains a soft limit at schema -4.
  • Native JSON snapshots and sum, first, and omit aggregation through registries, clusters, and workers, including reconciliation of different schemas and zero thresholds.
  • Protobuf encoding for native histograms and existing metric types. protobufjs/light uses the checked-in lib/metrics.json descriptor; lib/metrics.proto and npm run generate-protobuf make its source and regeneration available.
  • Public types, regression tests, configuration and migration documentation, a runnable HTTP example, and changelog entries.

Validation performed locally:

  • 761 tests in 34 suites passed on Node 22.23.2, 24.21.0, and 26.4.0. The configured Bun CI command also passed.
  • The Linux check workflow passed through act on Node 24.21.0: ESLint, Prettier, and TypeScript.
  • npm run benchmarks completed on macOS/Node 26.4.0. A focused registry comparison against upstream, with increased sampling, found no significant regression above a 5% threshold; default-label cases measured roughly 2–4% overhead.
  • Manual interoperability checks with Prometheus 3.12.0 covered native counts, sums, quantiles, signed and empty histograms, worker aggregation, classic coexistence, and other metric types.
  • Protobuf descriptor regeneration matched the checked-in file, and the package dry run included the runtime schema and implementation files.

The existing benchmark suite covers classic metrics. Dedicated native workload benchmarks and application-specific rollout validation remain follow-up work. Applications select the protobuf response format themselves; HTTP Accept negotiation is outside this change.

Developed with AI assistance (Codex), also disclosed in the commit trailer.

@ethanolchik ethanolchik changed the title Add native histograms and Prometheus protobuf exposition feat(metrics): Add native histograms and Prometheus protobuf exposition Sep 10, 2026
Comment thread lib/histogram.js Outdated
Comment thread lib/metricAggregators.js
* @returns {Function} aggregator function
*/
function AggregatorFactory(aggregatorFn) {
function AggregatorFactory(aggregatorFn, nativeAggregatorFn) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see why you need the second parameter.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added it so sum and first can pass their native histogram aggregators while keeping the existing classic histogram callbacks unchanged. The existing callbacks returns numbers whereas the native callbacks return native histogram snapshots. It's an optional field but I'm happy to structure it differently.

Comment thread lib/metricAggregators.js Outdated
function AggregatorFactory(aggregatorFn, nativeAggregatorFn) {
return metrics => {
if (metrics.length === 0) return;
const hasNativeHistograms = metrics.some(

@jdmarshall jdmarshall Sep 11, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And this check runs on every single metric, on every single scrape. This is not when and where to do a sanity check. That should be farther up the chain.

Also why would this happen? You get one histogram with a particular name. Either all of the values will be natives or they won't, right?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah true, this was probably more defensive than necessary. I've changed it so it uses the first snapshot only.

Comment thread lib/registry.js Outdated
Comment thread lib/registry.js
if (defaultLabelNames !== undefined) {
for (const labelName of defaultLabelNames) {
seriesLabels[labelName] ??= this._defaultLabels[labelName];
if (

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is this fixing?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This fixes a case where a histogram label is null or undefined and the registry has a default for that label. The existing line applies the default to seriesLabels, but the formatter uses the value from sharedLabels instead, so the default is ignored.

I've moved this change into a separate commit in line with your other comment.

Comment thread lib/registry.js Outdated
}

async getMetricsAsString(metrics) {
async getMetricsAsString(metrics, contentType = this.contentType) {

@jdmarshall jdmarshall Sep 11, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

since encodeMetricFamily doesn't return a string, this is not the way to wire this up.

You're also returning from the middle of a function now.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah yeah true, that's my bad. I've moved protobuf encoding into metrics() where the output format is selected.

Comment thread lib/registry.js Outdated
Comment on lines +73 to +76
if (contentType === Registry.PROMETHEUS_PROTOBUF_CONTENT_TYPE) {
const { encodeMetricFamily } = require('./protobuf');
return encodeMetricFamily(metric, this._defaultLabels);
}

@jdmarshall jdmarshall Sep 11, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be hoisted up to the calling function, which fixes the early exit and the mismatched function name and method signature.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok sure, I've moved this into metrics().

Comment thread lib/registry.js Outdated
return encodeMetricFamily(metric, this._defaultLabels);
}

const isOpenMetrics = contentType === Registry.OPENMETRICS_CONTENT_TYPE;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You've got way too many changes in a single commit. I don't see how this one or the one I questioned below are part of native histograms. I know some devs prefer large commits that tie to the ticket, but I've yet to meet one who actually does forensics in git history so I'm pretty sure that's a Chesterton's Fence situation.

If you're going to fix other bugs in a driveby I'd prefer the be done as a separate commit in the same PR. It'll also help because there's already an open PR touching some of the label code and this is going to make a hash of things.

@ethanolchik ethanolchik Sep 12, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok yeah I totally understand. I've split it into three commits now:

  • preserve counter names during OpenMetrics output (c45baee)
  • apply defaults to nullish histogram labels (43a3419)
  • native histograms and protobuf (6924fdf)

Comment thread lib/registry.js Outdated
@@ -223,9 +250,11 @@ class Registry {
setContentType(metricsContentType) {
if (

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have two implementations of this.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah yeah cheers, all fixed now. I moved the content-type check into a shared helper that both the constructor and setContentType() use.

@jdmarshall

Copy link
Copy Markdown
Contributor

So am I correct in thinking that native histograms are incompatible with the default Prometheus data type? If so then I'm not sure how to keep this compatible with worker.shutdown()

ethanolchik and others added 3 commits September 12, 2026 12:44
Signed-off-by: Ethan Olchik <eitan.olchik@gmail.com>
Signed-off-by: Ethan Olchik <eitan.olchik@gmail.com>
Extend Histogram with opt-in exponential native buckets, exemplars, and
resolution reduction. Preserve native snapshots through registry, worker,
and cluster aggregation.

Add protobuf exposition for all existing metric types, binary registry
return types, schema generation, documentation, and an HTTP example.

Fixes prometheus#576

Assisted-by: Codex
Signed-off-by: Ethan Olchik <eolchik@cloudflare.com>
@ethanolchik
ethanolchik force-pushed the feat/native-histograms branch from faff229 to 6924fdf Compare September 12, 2026 18:03
@ethanolchik

Copy link
Copy Markdown
Author

So am I correct in thinking that native histograms are incompatible with the default Prometheus data type? If so then I'm not sure how to keep this compatible with worker.shutdown()

Well, I though that because worker.shutdown() transfers JSON snapshots rather than formatted text, the native data is included regardless of the worker registry’s content type and preserved as objects by the coordinator. So Protobuf is
only needed on the registry serving the final scrape to expose the native buckets.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support for native histograms

2 participants