Skip to content

Commit

Permalink
Add openmetrics and exemplars support (#544)
Browse files Browse the repository at this point in the history
Co-authored-by: Andrei Dobre <andreidobre.web@gmail.com>
  • Loading branch information
2 people authored and SimenB committed Mar 9, 2023
1 parent 814128a commit 484a4ba
Show file tree
Hide file tree
Showing 42 changed files with 1,657 additions and 708 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ project adheres to [Semantic Versioning](http://semver.org/).

### Added

- Support for OpenMetrics and Exemplars

## [14.2.0] - 2023-03-06

### Changed
Expand Down
47 changes: 44 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -393,6 +393,47 @@ Default labels will be overridden if there is a name conflict.

`register.clear()` will clear default labels.

### Exemplars

The exemplars defined in the OpenMetrics specification can be enabled on Counter
and Histogram metric types. The default metrics have support for OpenTelemetry,
they will populate the exemplars with the labels `{traceId, spanId}` and their
corresponding values.

The format for `inc()` and `observe()` calls are different if exemplars are
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).

### Registy type

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

```js
const Prometheus = require('prom-client');
Prometheus.register.setContentType(
Prometheus.Registry.OPENMETRICS_CONTENT_TYPE,
);
```

Currently available registry types are defined by the content types:

**PROMETHEUS_CONTENT_TYPE** - version 0.0.4 of the original Prometheus metrics,
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).

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.

The `contentType` constant exposed by the module returns the default content
type when creating a new registry, currently defaults to Prometheus type.

### Multiple registries

By default, metrics are automatically registered to the global registry (located
Expand All @@ -407,6 +448,9 @@ Registry has a `merge` function that enables you to expose multiple registries
on the same endpoint. If the same metric name exists in both registries, an
error will be thrown.

Merging registries of different types is undefined. The user needs to make sure
all used registries have the same type (Prometheus or OpenMetrics versions).

```js
const client = require('prom-client');
const registry = new client.Registry();
Expand Down Expand Up @@ -557,9 +601,6 @@ new client.Histogram({
});
```

The content-type prometheus expects is also exported as a constant, both on the
`register` and from the main file of this project, called `contentType`.

### Garbage Collection Metrics

To avoid native dependencies in this module, GC statistics for bytes reclaimed
Expand Down
89 changes: 89 additions & 0 deletions example/exemplars.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
'use strict';

const { register, Registry, Counter, Histogram } = require('..');

async function makeCounters() {
const c = new Counter({
name: 'test_counter_exemplar',
help: 'Example of a counter with exemplar',
labelNames: ['code'],
enableExemplars: true,
});

const exemplarLabels = { traceId: '888', spanId: 'jjj' };

c.inc({
labels: { code: 300 },
value: 1,
exemplarLabels,
});
c.inc({
labels: { code: 200 },
exemplarLabels,
});

c.inc({ exemplarLabels });
c.inc();
}

async function makeHistograms() {
const h = new Histogram({
name: 'test_histogram_exemplar',
help: 'Example of a histogram with exemplar',
labelNames: ['code'],
enableExemplars: true,
});

const exemplarLabels = { traceId: '111', spanId: 'zzz' };

h.observe({
labels: { code: '200' },
value: 1,
exemplarLabels,
});

h.observe({
labels: { code: '200' },
value: 3,
exemplarLabels,
});

h.observe({
labels: { code: '200' },
value: 0.3,
exemplarLabels,
});

h.observe({
labels: { code: '200' },
value: 300,
exemplarLabels,
});
}

async function main() {
// exemplars will be shown only by OpenMetrics registry types
register.setContentType(Registry.OPENMETRICS_CONTENT_TYPE);

makeCounters();
makeHistograms();

console.log(await register.metrics());
console.log('---');

// if you dont want to set the default registry to OpenMetrics type then you need to create a new registry and assign it to the metric

register.setContentType(Registry.PROMETHEUS_CONTENT_TYPE);
const omReg = new Registry(Registry.OPENMETRICS_CONTENT_TYPE);
const c = new Counter({
name: 'counter_with_exemplar',
help: 'Example of a counter',
labelNames: ['code'],
registers: [omReg],
enableExemplars: true,
});
c.inc({ labels: { code: '200' }, exemplarLabels: { traceId: 'traceA' } });
console.log(await omReg.metrics());
}

main();
73 changes: 68 additions & 5 deletions index.d.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,27 @@
// Type definitions for prom-client
// Definitions by: Simon Nyberg http://twitter.com/siimon_nyberg

export type Charset = 'utf-8';

export type PrometheusMIME = 'text/plain';
export type PrometheusMetricsVersion = '0.0.4';

export type OpenMetricsMIME = 'application/openmetrics-text';
export type OpenMetricsVersion = '1.0.0';

export type PrometheusContentType =
`${OpenMetricsMIME}; version=${OpenMetricsVersion}; charset=${Charset}`;
export type OpenMetricsContentType =
`${PrometheusMIME}; version=${PrometheusMetricsVersion}; charset=${Charset}`;

export type RegistryContentType =
| PrometheusContentType
| OpenMetricsContentType;

/**
* Container for all registered metrics
*/
export class Registry {
export class Registry<RegistryContentType = PrometheusContentType> {
/**
* Get string representation for all metrics
*/
Expand Down Expand Up @@ -64,7 +81,14 @@ export class Registry {
/**
* Gets the Content-Type of the metrics for use in the response headers.
*/
contentType: string;
contentType: RegistryContentType;

/**
* Set the content type of a registry. Used to change between Prometheus and
* OpenMetrics versions.
* @param contentType The type of the registry
*/
setContentType(contentType: RegistryContentType): void;

/**
* Merge registers
Expand All @@ -80,9 +104,20 @@ export type Collector = () => void;
export const register: Registry;

/**
* The Content-Type of the metrics for use in the response headers.
* HTTP Content-Type for metrics response headers, defaults to Prometheus text
* format.
*/
export const contentType: RegistryContentType;

/**
* HTTP Prometheus Content-Type for metrics response headers.
*/
export const contentType: string;
export const prometheusContentType: PrometheusContentType;

/**
* HTTP OpenMetrics Content-Type for metrics response headers.
*/
export const openMetricsContentType: OpenMetricsContentType;

export class AggregatorRegistry extends Registry {
/**
Expand Down Expand Up @@ -164,16 +199,32 @@ interface MetricConfiguration<T extends string> {
name: string;
help: string;
labelNames?: T[] | readonly T[];
registers?: Registry[];
registers?: (
| Registry<PrometheusContentType>
| Registry<OpenMetricsContentType>
)[];
aggregator?: Aggregator;
collect?: CollectFunction<any>;
enableExemplars?: boolean;
}

export interface CounterConfiguration<T extends string>
extends MetricConfiguration<T> {
collect?: CollectFunction<Counter<T>>;
}

export interface IncreaseDataWithExemplar<T extends string> {
value?: number;
labels?: LabelValues<T>;
exemplarLabels?: LabelValues<T>;
}

export interface ObserveDataWithExemplar<T extends string> {
value: number;
labels?: LabelValues<T>;
exemplarLabels?: LabelValues<T>;
}

/**
* A counter is a cumulative metric that represents a single numerical value that only ever goes up
*/
Expand All @@ -196,6 +247,12 @@ export class Counter<T extends string = string> {
*/
inc(value?: number): void;

/**
* Increment with exemplars
* @param incData Object with labels, value and exemplars for an increase
*/
inc(incData: IncreaseDataWithExemplar<T>): void;

/**
* Get counter metric object
*/
Expand Down Expand Up @@ -410,6 +467,12 @@ export class Histogram<T extends string = string> {
*/
observe(labels: LabelValues<T>, value: number): void;

/**
* Observe with exemplars
* @param observeData Object with labels, value and exemplars for an observation
*/
observe(observeData: ObserveDataWithExemplar<T>): void;

/**
* Get histogram metric object
*/
Expand Down
4 changes: 4 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@
exports.register = require('./lib/registry').globalRegistry;
exports.Registry = require('./lib/registry');
exports.contentType = require('./lib/registry').globalRegistry.contentType;
exports.prometheusContentType =
require('./lib/registry').PROMETHEUS_CONTENT_TYPE;
exports.openMetricsContentType =
require('./lib/registry').OPENMETRICS_CONTENT_TYPE;
exports.validateMetricName = require('./lib/validation').validateMetricName;

exports.Counter = require('./lib/counter');
Expand Down
17 changes: 14 additions & 3 deletions lib/cluster.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ let listenersAdded = false;
const requests = new Map(); // Pending requests for workers' local metrics.

class AggregatorRegistry extends Registry {
constructor() {
super();
constructor(regContentType = Registry.PROMETHEUS_CONTENT_TYPE) {
super(regContentType);
addListeners();
}

Expand Down Expand Up @@ -84,19 +84,30 @@ class AggregatorRegistry extends Registry {
});
}

get contentType() {
return super.contentType;
}

/**
* Creates a new Registry instance from an array of metrics that were
* created by `registry.getMetricsAsJSON()`. Metrics are aggregated using
* the method specified by their `aggregator` property, or by summation if
* `aggregator` is undefined.
* @param {Array} metricsArr Array of metrics, each of which created by
* `registry.getMetricsAsJSON()`.
* @param {string} registryType content type of the new registry. Defaults
* to PROMETHEUS_CONTENT_TYPE.
* @return {Registry} aggregated registry.
*/
static aggregate(metricsArr) {
static aggregate(
metricsArr,
registryType = Registry.PROMETHEUS_CONTENT_TYPE,
) {
const aggregatedRegistry = new Registry();
const metricsByName = new Grouper();

aggregatedRegistry.setContentType(registryType);

// Gather by name
metricsArr.forEach(metrics => {
metrics.forEach(metric => {
Expand Down
Loading

0 comments on commit 484a4ba

Please sign in to comment.