Skip to content

Commit

Permalink
Add support for OpenMetrics format
Browse files Browse the repository at this point in the history
- Added support for OpenMetrics format, including exemplars
- Added support for exemplars with OpenTelemetry tracing data on
  default metrics
- Added the option of passing params as one object to observe() and inc()
methods
  • Loading branch information
voltbit committed Jan 12, 2022
1 parent 3b139f9 commit e9f6c44
Show file tree
Hide file tree
Showing 42 changed files with 1,009 additions and 151 deletions.
9 changes: 9 additions & 0 deletions CHANGELOG.md
Expand Up @@ -11,6 +11,15 @@ project adheres to [Semantic Versioning](http://semver.org/).

### Changed

- feat: support for OpenMetrics registries, including exemplars

- feat: out of the box support for exemplars populated with telemetry data on
default metrics when OpenTelemetry is loaded in the project

- feat: new option for calling `observe()` and `inc()` methods on the histogram
and counter metric types that can be passed an object of format
`{labels: a, value: b, exemplarLabels: c}`

- The `done()` functions returned by `gauge.startTimer()` and
`summary.startTimer()` now return the timed duration. Histograms already had
this behavior.
Expand Down
47 changes: 44 additions & 3 deletions README.md
Expand Up @@ -390,6 +390,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).

### 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 @@ -404,6 +445,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 @@ -554,9 +598,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
74 changes: 74 additions & 0 deletions example/exemplars.js
@@ -0,0 +1,74 @@
'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() {
// should only use exemplars with OpenMetrics registry types
register.setContentType(Registry.OPENMETRICS_CONTENT_TYPE);

makeCounters();
makeHistograms();

console.log(await register.metrics());
}

main();
30 changes: 26 additions & 4 deletions index.d.ts
Expand Up @@ -3,6 +3,10 @@

/**
* Container for all registered metrics
* @property {string} PROMETHEUS_CONTENT_TYPE - Content-Type of Prometheus
* registry type
* @property {string} OPENMETRICS_CONTENT_TYPE - Content-Type of OpenMetrics
* registry type.
*/
export class Registry {
/**
Expand Down Expand Up @@ -66,6 +70,13 @@ export class Registry {
*/
contentType: string;

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

/**
* Merge registers
* @param registers The registers you want to merge together
Expand All @@ -80,10 +91,21 @@ 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: string;

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

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

export class AggregatorRegistry extends Registry {
/**
* Gets aggregated metrics for all workers.
Expand Down Expand Up @@ -571,23 +593,23 @@ export class Pushgateway {
*/
pushAdd(
params: Pushgateway.Parameters,
): Promise<{ resp?: unknown, body?: unknown }>;
): Promise<{ resp?: unknown; body?: unknown }>;

/**
* Overwrite all metric (using PUT to Pushgateway)
* @param params Push parameters
*/
push(
params: Pushgateway.Parameters,
): Promise<{ resp?: unknown, body?: unknown }>;
): Promise<{ resp?: unknown; body?: unknown }>;

/**
* Delete all metrics for jobName
* @param params Push parameters
*/
delete(
params: Pushgateway.Parameters,
): Promise<{ resp?: unknown, body?: unknown }>;
): Promise<{ resp?: unknown; body?: unknown }>;
}

export namespace Pushgateway {
Expand Down
2 changes: 2 additions & 0 deletions index.js
Expand Up @@ -8,6 +8,8 @@
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
19 changes: 16 additions & 3 deletions lib/cluster.js
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,32 @@ 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();

if (registryType === Registry.OPENMETRICS_CONTENT_TYPE) {
aggregatedRegistry.setContentType(registryType);
}

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

0 comments on commit e9f6c44

Please sign in to comment.