Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ project adheres to [Semantic Versioning](http://semver.org/).

### Added

- Opt-in native histograms with configurable exponential buckets, a zero bucket,
bucket-count limits, exemplars, and worker/cluster aggregation.
- Prometheus protobuf registries for native and classic metrics, with public
content-type constants and TypeScript support for binary output.

## [0.16.0] - 2026-08-24

This release marks our first release as a Prometheus subproject.
Expand Down
49 changes: 45 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,41 @@ xhrRequest(function (err, res) {
});
```

##### Native histograms

Enable native buckets with `nativeHistogramBucketFactor` and expose the registry
using Prometheus protobuf:

```js
const registry = new client.Registry(
client.Registry.PROMETHEUS_PROTOBUF_CONTENT_TYPE,
);
const histogram = new client.Histogram({
name: 'request_duration_seconds',
help: 'Time spent handling requests',
nativeHistogramBucketFactor: 1.1,
buckets: [],
registers: [registry],
});
histogram.observe(0.125);

res.setHeader('Content-Type', registry.contentType);
res.end(await registry.metrics()); // A Buffer for protobuf registries
```

Native buckets cover positive and negative values using exponential buckets and
a zero bucket. The default zero threshold is `2 ** -128`, configurable with
`nativeHistogramZeroThreshold`. The default budget of 160 populated buckets per
label set can be configured with `nativeHistogramMaxBucketNumber` (0 disables
the budget). When needed, resolution is reduced down to schema -4; at that
minimum resolution the budget is a soft limit.

Classic buckets are retained by default. Set `buckets: []` for native-only
protobuf output. Prometheus text and OpenMetrics 1.0 text expose only the classic
representation. Prometheus must also be configured to scrape native histograms.
See the [native histogram guide](docs/native-histograms.md) for migration,
worker aggregation, exemplars, and configuration details.

#### Summary

Summaries calculate percentiles of observed values.
Expand Down Expand Up @@ -397,11 +432,12 @@ enabled. They get a single object with the format
`{labels, value, exemplarLabels}`.

When using exemplars, the registry used for metrics should be set to OpenMetrics
type (including the global or default registry if no registries are specified).
or Prometheus protobuf (including the global or default registry if no registries
are specified).

### Registry type

The library supports both the old Prometheus format and the OpenMetrics format.
The library supports Prometheus text, OpenMetrics text, and Prometheus protobuf.
The format can be set per registry. For default metrics:

```js
Expand All @@ -419,9 +455,14 @@ this is currently the default registry type.
**OPENMETRICS_CONTENT_TYPE** - defaults to version 1.0.0 of the
[OpenMetrics standard](https://github.com/OpenObservability/OpenMetrics/blob/d99b705f611b75fec8f450b05e344e02eea6921d/specification/OpenMetrics.md).

**PROMETHEUS_PROTOBUF_CONTENT_TYPE** - length-delimited Prometheus protobuf,
including native histograms. Registry serialization methods return a `Buffer`
for this format.

The HTTP Content-Type string for each registry type is exposed both at module
level (`prometheusContentType` and `openMetricsContentType`) and as static
properties on the `Registry` object.
level (`prometheusContentType`, `openMetricsContentType`, and
`prometheusProtobufContentType`) and as static properties on the `Registry`
object.

The `contentType` constant exposed by the module returns the default content
type when creating a new registry, currently defaults to Prometheus type.
Expand Down
188 changes: 188 additions & 0 deletions docs/native-histograms.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
# Native histograms

Native histograms collect observations in sparse exponential buckets and expose
them as native histogram samples in Prometheus. This guide covers collection,
scrape encoding, worker aggregation, and application configuration.

## API and compatibility

Native buckets are an opt-in extension of `Histogram`. Existing observations,
labels, timers, `collect()`, `zero()`, `remove()`, and `reset()` use the same API.
Histograms without native options retain their existing classic buckets and text
output.

| Option | Default | Behavior |
| -------------------------------- | ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| `nativeHistogramBucketFactor` | Disabled | A value greater than 1 enables native buckets. The recommended value is 1.1, selecting schema 3. Values between 0 and 1 disable native buckets. |
| `nativeHistogramZeroThreshold` | `2 ** -128` | Values in the inclusive interval `[-threshold, threshold]` enter the zero bucket. Zero is supported explicitly. |
| `nativeHistogramMaxBucketNumber` | 160 | Budget for populated positive and negative buckets combined, per label set. Zero disables the budget. |
| `buckets` | Existing classic defaults | Classic buckets are collected alongside native buckets. Use `[]` to omit classic buckets from protobuf. |

The growth factor selects a standard exponential schema from -4 through 8. Each
power of two is divided into `2 ** schema` buckets for positive schemas. The
supported schemas are clamped at either end for exceptionally large or small
factors. Positive and negative values use separate sparse bucket maps. Bucket
boundaries are inclusive away from zero and match Prometheus's reference
significands, including adjacent floating-point values and subnormal numbers.

When the budget is exceeded, neighboring buckets are merged and the schema is
reduced until the budget is met or schema -4 is reached. All observations, the
sum, count, and creation time are retained. The budget is a **soft limit at schema
-4**: a sufficiently small budget or broad distribution can still exceed it.
Reducing resolution never changes classic buckets. Explicit `zero()` and
`reset()` restore the configured resolution when a label set is initialized
again. There are no automatic resets or automatic zero-threshold increases during
collection.

Observation validation matches existing histograms: only finite JavaScript
numbers are accepted. Counts and sums use JavaScript numbers, with the same
numeric precision limits as existing metrics.

## Scrape format

Native histograms require the length-delimited Prometheus protobuf format.
Prometheus text 0.0.4 and OpenMetrics text 1.0 cannot carry native buckets. Those
registries continue to expose the classic representation, including the implicit
`+Inf`, sum, and count even when `buckets: []` is used.

```js
const client = require('@prometheus-io/client');

const registry = new client.Registry(
client.Registry.PROMETHEUS_PROTOBUF_CONTENT_TYPE,
);
const duration = new client.Histogram({
name: 'http_request_duration_seconds',
help: 'Time spent handling requests',
labelNames: ['method'],
nativeHistogramBucketFactor: 1.1,
nativeHistogramMaxBucketNumber: 160,
buckets: [],
registers: [registry],
});

duration.observe({ method: 'GET' }, 0.125);

// In the application's metrics handler:
res.setHeader('Content-Type', registry.contentType);
res.end(await registry.metrics());
```

The content type is also exported as `client.prometheusProtobufContentType`.
For protobuf registries, `metrics()`, `getMetricsAsString()`, and
`getSingleMetricAsString()` return a Node.js `Buffer`. Send these bytes directly;
converting them to UTF-8 corrupts the response. Their TypeScript representation
is `Uint8Array`, so consuming the types does not require Node ambient types.
Text registries retain string return types.

Constructing a registry with the content type infers its TypeScript return type.
When changing an existing registry, the return value of
`registry.setContentType(contentType)` carries the new type. For code that keeps
one reference while switching formats dynamically, declare it as
`Registry<RegistryContentType>` to account for both strings and bytes.

A protobuf response can contain counters, gauges, summaries, classic histograms,
and native histograms together. It contains one length-delimited
`io.prometheus.client.MetricFamily` per metric family, with no text separators or
`# EOF` marker. Registry default labels and metric labels have the same
precedence as text exposition. A protobuf registry also works with `Pushgateway`;
the client sets the matching content type.

Fractional classic histogram counts produced by aggregation use protobuf's
floating-point count fields. The protobuf schema requires integer summary
counts, so summary aggregation that produces a fractional count fails explicitly
instead of truncating it.

The HTTP framework remains responsible for choosing the registry format. This
addition does not introduce HTTP Accept-header negotiation.

## Exemplars and snapshots

`enableExemplars: true` works on protobuf registries as well as OpenMetrics text
registries. Native histograms retain the most recent ten timestamped exemplars
per label set, independently of classic bucket exemplars. The existing
`observe({ labels, value, exemplarLabels })` and timer APIs apply.

`Histogram.get()` and `Registry.getMetricsAsJSON()` keep their existing
`values` array for classic samples and add `nativeHistograms` when enabled.
Each native snapshot contains labels, count, sum, schema, zero threshold and
count, positive/negative spans and deltas, creation timestamp, and exemplars.
These are plain JSON-compatible values; modifying a native snapshot does not
modify the collector.

## Worker and cluster aggregation

`Registry.aggregate()`, `ClusterRegistry`, and `WorkerRegistry` preserve native
snapshots, including accumulated statistics from orderly worker shutdowns. Set
the aggregating registry's content type to protobuf. Workers may use text
registries locally: their JSON snapshots still contain native data.

Summation groups by label set, reduces buckets to the lowest common schema, and
reconciles zero thresholds. If an increased zero threshold cuts through a
populated bucket, aggregation includes that entire bucket in the zero bucket.
It never estimates how to split an existing bucket's population.

Native histograms support `sum`, `first`, and `omit` aggregators. Other
aggregators fail explicitly. All contributing definitions of a metric family
must enable native histograms; mixing native and classic-only definitions is
rejected. Configure the same classic bucket boundaries across workers when
collecting classic buckets alongside native ones.

## Application integration

1. Upgrade the client and enable native buckets on the desired histograms.
2. Configure the metrics handler to send the protobuf registry's bytes and
content type. [The HTTP example](../example/native-histogram.js) demonstrates
this without a framework.
3. Enable native histogram ingestion in Prometheus. For Prometheus 3.8 and later:

```yaml
scrape_configs:
- job_name: example
scrape_native_histograms: true
static_configs:
- targets: ['localhost:3000']
```

Older Prometheus releases starting at 2.40 require their native-histograms
feature flag instead. Follow the configuration for the deployed release.

4. During migration, keep explicit classic buckets and set
`always_scrape_classic_histograms: true` in the scrape job to retain both
representations. This preserves existing dashboards while native queries
are introduced.
5. Query the native metric directly, without the classic `_bucket` suffix or
`le` grouping:

```promql
histogram_quantile(
0.95,
sum by (method) (rate(http_request_duration_seconds[5m]))
)
```

## Implementation and verification

| Area | Implementation |
| ----------- | --------------------------------------------------------------------------------------------- |
| Collection | Sparse maps, reference bucket boundaries, coarsening, exemplars |
| Export | Public JSON snapshots plus protobuf MetricFamily encoding |
| Aggregation | Common resolution and zero threshold, JSON/worker compatibility |
| API | Histogram options, content-type constants, binary registry return types |
| Tests | Numeric boundaries, lifecycle, limits, independent protobuf decoding, aggregation, TypeScript |
| Integration | Runnable HTTP endpoint and native scraping by Prometheus |

The protobuf codec uses `protobufjs/light` and a checked-in JSON descriptor, so
serialization does not read files at runtime. The original upstream
`lib/metrics.proto` is retained for provenance and independent decoding tests.
Run `npm run generate-protobuf` after updating that schema.

The initial scope excludes native histograms with custom buckets (schema -53),
gauge histograms, OpenMetrics 2.0, OTLP/remote-write export, automatic reset
scheduling, and configurable exemplar sampling policies.

References:

- [Prometheus native histogram specification](https://prometheus.io/docs/specs/native_histograms/)
- [Prometheus protobuf schema](https://github.com/prometheus/client_model/blob/master/io/prometheus/client/metrics.proto)
- [Go client histogram implementation](https://github.com/prometheus/client_golang/blob/main/prometheus/histogram.go)
54 changes: 54 additions & 0 deletions example/native-histogram.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// Copyright The Prometheus Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0

// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

'use strict';

const http = require('node:http');
const client = require('../index');

const registry = new client.Registry(
client.Registry.PROMETHEUS_PROTOBUF_CONTENT_TYPE,
);
client.collectDefaultMetrics({ register: registry });

const duration = new client.Histogram({
name: 'http_request_duration_seconds',
help: 'Time spent handling requests',
labelNames: ['method'],
nativeHistogramBucketFactor: 1.1,
nativeHistogramMaxBucketNumber: 160,
buckets: [],
registers: [registry],
});

http
.createServer(async (req, res) => {
if (req.url === '/metrics') {
try {
const metrics = await registry.metrics();
res.writeHead(200, { 'Content-Type': registry.contentType });
res.end(metrics);
} catch (error) {
res.writeHead(500);
res.end(error.message);
}
return;
}

const end = duration.startTimer({ method: req.method });
res.writeHead(204);
res.end();
end();
})
.listen(Number(process.env.PORT ?? 3000));
Loading