Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add openmetrics and exemplars support #482

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
47 changes: 44 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
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 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 @@ -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).
Comment on lines +448 to +449
Copy link
Collaborator

Choose a reason for hiding this comment

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

again, can we throw a usweful error?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Copy link
Collaborator

Choose a reason for hiding this comment

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

Docs should be updated to say it throws, no? Not undefined behavior


```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
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();
66 changes: 66 additions & 0 deletions example/exemplars.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import * as prom from '../index';
Copy link
Collaborator

Choose a reason for hiding this comment

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

I don't think we need both a JS and a TS example - could you remove this one?


async function prometheusRegistry() {
let reg = new prom.Registry();

let counter = new prom.Counter({
name: 'test_counter',
help: 'counter help message',
registers: [reg],
labelNames: ['code'],
});

let hist = new prom.Histogram({
name: 'test_histogram',
help: 'histogram help message',
registers: [reg],
labelNames: ['code'],
});

counter.inc({ code: '300' }, 2);
hist.observe({ code: '200' }, 1);

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

async function openMetricsRegistry() {
let reg = new prom.Registry<prom.OpenMetricsContentType>();
reg.setContentType(prom.openMetricsContentType);

let counter = new prom.Counter({
name: 'test_counter',
help: 'counter help message',
registers: [reg],
labelNames: ['code'],
enableExemplars: true,
});

let hist = new prom.Histogram({
name: 'test_histogram',
help: 'histogram help message',
registers: [reg],
labelNames: ['code'],
enableExemplars: true,
});

counter.inc(<prom.IncreaseDataWithExemplar<string>>{
value: 2,
labels: { code: '300' },
exemplarLabels: { traceID: 'traceA' },
});

hist.observe(<prom.ObserveDataWithExemplar<string>>{
value: 1,
labels: { code: '200' },
exemplarLabels: { traceID: 'traceA' },
});

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

async function main() {
prometheusRegistry();
openMetricsRegistry();
}

main();
70 changes: 65 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 prometheusContentType: PrometheusContentType;

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

export class AggregatorRegistry extends Registry {
/**
Expand Down Expand Up @@ -150,16 +185,29 @@ interface MetricConfiguration<T extends string> {
name: string;
help: string;
labelNames?: T[] | readonly T[];
registers?: Registry[];
registers?: Registry<PrometheusContentType | OpenMetricsContentType>[];
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
registers?: Registry<PrometheusContentType | OpenMetricsContentType>[];
registers?: (Registry<PrometheusContentType> | Registry<OpenMetricsContentType>)[];

right? A registry itself cannot contain either

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 @@ -182,6 +230,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;

/**
* Return the child for given labels
* @param values Label values
Expand Down Expand Up @@ -386,6 +440,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;

/**
* Start a timer. Calling the returned function will observe the duration in
* seconds in the histogram.
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