Skip to content

Commit 54393f6

Browse files
karlodwyervoltbit
authored andcommitted
Add openmetrics and exemplars support (#544)
Co-authored-by: Andrei Dobre <[email protected]>
1 parent 814128a commit 54393f6

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+1657
-708
lines changed

CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ project adheres to [Semantic Versioning](http://semver.org/).
1717
avoid an unnecessarily complex regex.
1818

1919
### Added
20+
- Support for OpenMetrics and Exemplars
21+
-
2022

2123
## [14.2.0] - 2023-03-06
2224

README.md

+44-3
Original file line numberDiff line numberDiff line change
@@ -393,6 +393,47 @@ Default labels will be overridden if there is a name conflict.
393393

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

396+
### Exemplars
397+
398+
The exemplars defined in the OpenMetrics specification can be enabled on Counter
399+
and Histogram metric types. The default metrics have support for OpenTelemetry,
400+
they will populate the exemplars with the labels `{traceId, spanId}` and their
401+
corresponding values.
402+
403+
The format for `inc()` and `observe()` calls are different if exemplars are
404+
enabled. They get a single object with the format
405+
`{labels, value, exemplarLabels}`.
406+
407+
When using exemplars, the registry used for metrics should be set to OpenMetrics
408+
type (including the global or default registry if no registries are specified).
409+
410+
### Registy type
411+
412+
The library supports both the old Prometheus format and the OpenMetrics format.
413+
The format can be set per registry. For default metrics:
414+
415+
```js
416+
const Prometheus = require('prom-client');
417+
Prometheus.register.setContentType(
418+
Prometheus.Registry.OPENMETRICS_CONTENT_TYPE,
419+
);
420+
```
421+
422+
Currently available registry types are defined by the content types:
423+
424+
**PROMETHEUS_CONTENT_TYPE** - version 0.0.4 of the original Prometheus metrics,
425+
this is currently the default registry type.
426+
427+
**OPENMETRICS_CONTENT_TYPE** - defaults to version 1.0.0 of the
428+
[OpenMetrics standard](https://github.com/OpenObservability/OpenMetrics/blob/d99b705f611b75fec8f450b05e344e02eea6921d/specification/OpenMetrics.md).
429+
430+
The HTTP Content-Type string for each registry type is exposed both at module
431+
level (`prometheusContentType` and `openMetricsContentType`) and as static
432+
properties on the `Registry` object.
433+
434+
The `contentType` constant exposed by the module returns the default content
435+
type when creating a new registry, currently defaults to Prometheus type.
436+
396437
### Multiple registries
397438

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

451+
Merging registries of different types is undefined. The user needs to make sure
452+
all used registries have the same type (Prometheus or OpenMetrics versions).
453+
410454
```js
411455
const client = require('prom-client');
412456
const registry = new client.Registry();
@@ -557,9 +601,6 @@ new client.Histogram({
557601
});
558602
```
559603

560-
The content-type prometheus expects is also exported as a constant, both on the
561-
`register` and from the main file of this project, called `contentType`.
562-
563604
### Garbage Collection Metrics
564605

565606
To avoid native dependencies in this module, GC statistics for bytes reclaimed

example/exemplars.js

+89
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
'use strict';
2+
3+
const { register, Registry, Counter, Histogram } = require('..');
4+
5+
async function makeCounters() {
6+
const c = new Counter({
7+
name: 'test_counter_exemplar',
8+
help: 'Example of a counter with exemplar',
9+
labelNames: ['code'],
10+
enableExemplars: true,
11+
});
12+
13+
const exemplarLabels = { traceId: '888', spanId: 'jjj' };
14+
15+
c.inc({
16+
labels: { code: 300 },
17+
value: 1,
18+
exemplarLabels,
19+
});
20+
c.inc({
21+
labels: { code: 200 },
22+
exemplarLabels,
23+
});
24+
25+
c.inc({ exemplarLabels });
26+
c.inc();
27+
}
28+
29+
async function makeHistograms() {
30+
const h = new Histogram({
31+
name: 'test_histogram_exemplar',
32+
help: 'Example of a histogram with exemplar',
33+
labelNames: ['code'],
34+
enableExemplars: true,
35+
});
36+
37+
const exemplarLabels = { traceId: '111', spanId: 'zzz' };
38+
39+
h.observe({
40+
labels: { code: '200' },
41+
value: 1,
42+
exemplarLabels,
43+
});
44+
45+
h.observe({
46+
labels: { code: '200' },
47+
value: 3,
48+
exemplarLabels,
49+
});
50+
51+
h.observe({
52+
labels: { code: '200' },
53+
value: 0.3,
54+
exemplarLabels,
55+
});
56+
57+
h.observe({
58+
labels: { code: '200' },
59+
value: 300,
60+
exemplarLabels,
61+
});
62+
}
63+
64+
async function main() {
65+
// exemplars will be shown only by OpenMetrics registry types
66+
register.setContentType(Registry.OPENMETRICS_CONTENT_TYPE);
67+
68+
makeCounters();
69+
makeHistograms();
70+
71+
console.log(await register.metrics());
72+
console.log('---');
73+
74+
// 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
75+
76+
register.setContentType(Registry.PROMETHEUS_CONTENT_TYPE);
77+
const omReg = new Registry(Registry.OPENMETRICS_CONTENT_TYPE);
78+
const c = new Counter({
79+
name: 'counter_with_exemplar',
80+
help: 'Example of a counter',
81+
labelNames: ['code'],
82+
registers: [omReg],
83+
enableExemplars: true,
84+
});
85+
c.inc({ labels: { code: '200' }, exemplarLabels: { traceId: 'traceA' } });
86+
console.log(await omReg.metrics());
87+
}
88+
89+
main();

index.d.ts

+68-5
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,27 @@
11
// Type definitions for prom-client
22
// Definitions by: Simon Nyberg http://twitter.com/siimon_nyberg
33

4+
export type Charset = 'utf-8';
5+
6+
export type PrometheusMIME = 'text/plain';
7+
export type PrometheusMetricsVersion = '0.0.4';
8+
9+
export type OpenMetricsMIME = 'application/openmetrics-text';
10+
export type OpenMetricsVersion = '1.0.0';
11+
12+
export type PrometheusContentType =
13+
`${OpenMetricsMIME}; version=${OpenMetricsVersion}; charset=${Charset}`;
14+
export type OpenMetricsContentType =
15+
`${PrometheusMIME}; version=${PrometheusMetricsVersion}; charset=${Charset}`;
16+
17+
export type RegistryContentType =
18+
| PrometheusContentType
19+
| OpenMetricsContentType;
20+
421
/**
522
* Container for all registered metrics
623
*/
7-
export class Registry {
24+
export class Registry<RegistryContentType = PrometheusContentType> {
825
/**
926
* Get string representation for all metrics
1027
*/
@@ -64,7 +81,14 @@ export class Registry {
6481
/**
6582
* Gets the Content-Type of the metrics for use in the response headers.
6683
*/
67-
contentType: string;
84+
contentType: RegistryContentType;
85+
86+
/**
87+
* Set the content type of a registry. Used to change between Prometheus and
88+
* OpenMetrics versions.
89+
* @param contentType The type of the registry
90+
*/
91+
setContentType(contentType: RegistryContentType): void;
6892

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

82106
/**
83-
* The Content-Type of the metrics for use in the response headers.
107+
* HTTP Content-Type for metrics response headers, defaults to Prometheus text
108+
* format.
109+
*/
110+
export const contentType: RegistryContentType;
111+
112+
/**
113+
* HTTP Prometheus Content-Type for metrics response headers.
84114
*/
85-
export const contentType: string;
115+
export const prometheusContentType: PrometheusContentType;
116+
117+
/**
118+
* HTTP OpenMetrics Content-Type for metrics response headers.
119+
*/
120+
export const openMetricsContentType: OpenMetricsContentType;
86121

87122
export class AggregatorRegistry extends Registry {
88123
/**
@@ -164,16 +199,32 @@ interface MetricConfiguration<T extends string> {
164199
name: string;
165200
help: string;
166201
labelNames?: T[] | readonly T[];
167-
registers?: Registry[];
202+
registers?: (
203+
| Registry<PrometheusContentType>
204+
| Registry<OpenMetricsContentType>
205+
)[];
168206
aggregator?: Aggregator;
169207
collect?: CollectFunction<any>;
208+
enableExemplars?: boolean;
170209
}
171210

172211
export interface CounterConfiguration<T extends string>
173212
extends MetricConfiguration<T> {
174213
collect?: CollectFunction<Counter<T>>;
175214
}
176215

216+
export interface IncreaseDataWithExemplar<T extends string> {
217+
value?: number;
218+
labels?: LabelValues<T>;
219+
exemplarLabels?: LabelValues<T>;
220+
}
221+
222+
export interface ObserveDataWithExemplar<T extends string> {
223+
value: number;
224+
labels?: LabelValues<T>;
225+
exemplarLabels?: LabelValues<T>;
226+
}
227+
177228
/**
178229
* A counter is a cumulative metric that represents a single numerical value that only ever goes up
179230
*/
@@ -196,6 +247,12 @@ export class Counter<T extends string = string> {
196247
*/
197248
inc(value?: number): void;
198249

250+
/**
251+
* Increment with exemplars
252+
* @param incData Object with labels, value and exemplars for an increase
253+
*/
254+
inc(incData: IncreaseDataWithExemplar<T>): void;
255+
199256
/**
200257
* Get counter metric object
201258
*/
@@ -410,6 +467,12 @@ export class Histogram<T extends string = string> {
410467
*/
411468
observe(labels: LabelValues<T>, value: number): void;
412469

470+
/**
471+
* Observe with exemplars
472+
* @param observeData Object with labels, value and exemplars for an observation
473+
*/
474+
observe(observeData: ObserveDataWithExemplar<T>): void;
475+
413476
/**
414477
* Get histogram metric object
415478
*/

index.js

+4
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@
88
exports.register = require('./lib/registry').globalRegistry;
99
exports.Registry = require('./lib/registry');
1010
exports.contentType = require('./lib/registry').globalRegistry.contentType;
11+
exports.prometheusContentType =
12+
require('./lib/registry').PROMETHEUS_CONTENT_TYPE;
13+
exports.openMetricsContentType =
14+
require('./lib/registry').OPENMETRICS_CONTENT_TYPE;
1115
exports.validateMetricName = require('./lib/validation').validateMetricName;
1216

1317
exports.Counter = require('./lib/counter');

lib/cluster.js

+14-3
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,8 @@ let listenersAdded = false;
2828
const requests = new Map(); // Pending requests for workers' local metrics.
2929

3030
class AggregatorRegistry extends Registry {
31-
constructor() {
32-
super();
31+
constructor(regContentType = Registry.PROMETHEUS_CONTENT_TYPE) {
32+
super(regContentType);
3333
addListeners();
3434
}
3535

@@ -84,19 +84,30 @@ class AggregatorRegistry extends Registry {
8484
});
8585
}
8686

87+
get contentType() {
88+
return super.contentType;
89+
}
90+
8791
/**
8892
* Creates a new Registry instance from an array of metrics that were
8993
* created by `registry.getMetricsAsJSON()`. Metrics are aggregated using
9094
* the method specified by their `aggregator` property, or by summation if
9195
* `aggregator` is undefined.
9296
* @param {Array} metricsArr Array of metrics, each of which created by
9397
* `registry.getMetricsAsJSON()`.
98+
* @param {string} registryType content type of the new registry. Defaults
99+
* to PROMETHEUS_CONTENT_TYPE.
94100
* @return {Registry} aggregated registry.
95101
*/
96-
static aggregate(metricsArr) {
102+
static aggregate(
103+
metricsArr,
104+
registryType = Registry.PROMETHEUS_CONTENT_TYPE,
105+
) {
97106
const aggregatedRegistry = new Registry();
98107
const metricsByName = new Grouper();
99108

109+
aggregatedRegistry.setContentType(registryType);
110+
100111
// Gather by name
101112
metricsArr.forEach(metrics => {
102113
metrics.forEach(metric => {

0 commit comments

Comments
 (0)