From 870c0f0630f95d50e232593801cdb3ddefbbf13f Mon Sep 17 00:00:00 2001 From: Karl O'Dwyer Date: Mon, 6 Mar 2023 14:28:12 +0000 Subject: [PATCH] Add openmetrics and exemplars support (#544) Co-authored-by: Andrei Dobre --- CHANGELOG.md | 1 + README.md | 47 +- example/exemplars.js | 89 ++ index.d.ts | 73 +- index.js | 4 + lib/cluster.js | 17 +- lib/counter.js | 69 +- lib/exemplar.js | 37 + lib/gauge.js | 8 +- lib/histogram.js | 78 +- lib/metric.js | 15 +- lib/metrics/gc.js | 2 +- lib/metrics/processCpuTotal.js | 41 +- lib/registry.js | 122 +- lib/summary.js | 5 +- lib/util.js | 4 + package.json | 1 + test/__snapshots__/counterTest.js.snap | 16 +- test/__snapshots__/gaugeTest.js.snap | 4 +- test/__snapshots__/histogramTest.js.snap | 16 +- test/__snapshots__/registerTest.js.snap | 55 +- test/__snapshots__/summaryTest.js.snap | 12 +- test/clusterTest.js | 15 +- test/counterTest.js | 14 +- test/defaultMetricsTest.js | 14 +- test/exemplarsTest.js | 131 ++ test/gaugeTest.js | 14 +- test/histogramTest.js | 14 +- test/metrics/eventLoopLagTest.js | 13 +- test/metrics/gcTest.js | 13 +- test/metrics/heapSizeAndUsedTest.js | 11 +- test/metrics/heapSpacesSizeAndUsedTest.js | 10 +- test/metrics/maxFileDescriptorsTest.js | 16 +- test/metrics/processHandlesTest.js | 13 +- .../metrics/processOpenFileDescriptorsTest.js | 24 +- test/metrics/processRequestsTest.js | 13 +- test/metrics/processStartTimeTest.js | 13 +- test/metrics/versionTest.js | 14 +- test/pushgatewayTest.js | 66 +- test/pushgatewayWithPathTest.js | 14 +- test/registerTest.js | 1171 +++++++++-------- test/summaryTest.js | 18 +- 42 files changed, 1631 insertions(+), 696 deletions(-) create mode 100644 example/exemplars.js create mode 100644 lib/exemplar.js create mode 100644 test/exemplarsTest.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 89b0707b..e3748a14 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ project adheres to [Semantic Versioning](http://semver.org/). ### Added - Add `get` method to type definitions of metric classes +- Support for OpenMetrics and Exemplars ## [14.1.1] - 2022-12-31 diff --git a/README.md b/README.md index 4f4faa30..4b909e43 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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(); @@ -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 diff --git a/example/exemplars.js b/example/exemplars.js new file mode 100644 index 00000000..29f779e7 --- /dev/null +++ b/example/exemplars.js @@ -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(); diff --git a/index.d.ts b/index.d.ts index e0435578..7bb196d8 100644 --- a/index.d.ts +++ b/index.d.ts @@ -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 { /** * Get string representation for all metrics */ @@ -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 @@ -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 { /** @@ -164,9 +199,13 @@ interface MetricConfiguration { name: string; help: string; labelNames?: T[] | readonly T[]; - registers?: Registry[]; + registers?: ( + | Registry + | Registry + )[]; aggregator?: Aggregator; collect?: CollectFunction; + enableExemplars?: boolean; } export interface CounterConfiguration @@ -174,6 +213,18 @@ export interface CounterConfiguration collect?: CollectFunction>; } +export interface IncreaseDataWithExemplar { + value?: number; + labels?: LabelValues; + exemplarLabels?: LabelValues; +} + +export interface ObserveDataWithExemplar { + value: number; + labels?: LabelValues; + exemplarLabels?: LabelValues; +} + /** * A counter is a cumulative metric that represents a single numerical value that only ever goes up */ @@ -196,6 +247,12 @@ export class Counter { */ inc(value?: number): void; + /** + * Increment with exemplars + * @param incData Object with labels, value and exemplars for an increase + */ + inc(incData: IncreaseDataWithExemplar): void; + /** * Get counter metric object */ @@ -410,6 +467,12 @@ export class Histogram { */ observe(labels: LabelValues, value: number): void; + /** + * Observe with exemplars + * @param observeData Object with labels, value and exemplars for an observation + */ + observe(observeData: ObserveDataWithExemplar): void; + /** * Get histogram metric object */ diff --git a/index.js b/index.js index 3199417f..7f6e167a 100644 --- a/index.js +++ b/index.js @@ -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'); diff --git a/lib/cluster.js b/lib/cluster.js index cb564ede..5cb707ed 100644 --- a/lib/cluster.js +++ b/lib/cluster.js @@ -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(); } @@ -84,6 +84,10 @@ 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 @@ -91,12 +95,19 @@ class AggregatorRegistry extends Registry { * `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 => { diff --git a/lib/counter.js b/lib/counter.js index f711a3b9..4204bf5b 100644 --- a/lib/counter.js +++ b/lib/counter.js @@ -4,20 +4,41 @@ 'use strict'; const util = require('util'); -const type = 'counter'; -const { hashObject, isObject, getLabels, removeLabels } = require('./util'); +const { + hashObject, + isObject, + getLabels, + removeLabels, + nowTimestamp, +} = require('./util'); const { validateLabel } = require('./validation'); const { Metric } = require('./metric'); +const Exemplar = require('./exemplar'); class Counter extends Metric { + constructor(config) { + super(config); + this.type = 'counter'; + this.defaultLabels = {}; + this.defaultValue = 1; + this.defaultExemplarLabelSet = {}; + if (config.enableExemplars) { + this.enableExemplars = true; + this.inc = this.incWithExemplar; + } else { + this.inc = this.incWithoutExemplar; + } + } + /** * Increment counter * @param {object} labels - What label you want to be incremented * @param {Number} value - Value to increment, if omitted increment with 1 - * @returns {void} + * @returns {object} results - object with information about the inc operation + * @returns {string} results.labelHash - hash representation of the labels */ - inc(labels, value) { - let hash; + incWithoutExemplar(labels, value) { + let hash = ''; if (isObject(labels)) { hash = hashObject(labels); validateLabel(this.labelNames, labels); @@ -36,6 +57,41 @@ class Counter extends Metric { if (value === null || value === undefined) value = 1; setValue(this.hashMap, value, labels, hash); + + return { labelHash: hash }; + } + + /** + * Increment counter with exemplar, same as inc but accepts labels for an + * exemplar. + * If no label is provided the current exemplar labels are kept unchanged + * (defaults to empty set). + * + * @param {object} incOpts - Object with options about what metric to increase + * @param {object} incOpts.labels - What label you want to be incremented, + * defaults to null (metric with no labels) + * @param {Number} incOpts.value - Value to increment, defaults to 1 + * @param {object} incOpts.exemplarLabels - Key-value labels for the + * exemplar, defaults to empty set {} + * @returns {void} + */ + incWithExemplar({ + labels = this.defaultLabels, + value = this.defaultValue, + exemplarLabels = this.defaultExemplarLabelSet, + } = {}) { + const res = this.incWithoutExemplar(labels, value); + this.updateExemplar(exemplarLabels, value, res.labelHash); + } + + updateExemplar(exemplarLabels, value, hash) { + if (!isObject(this.hashMap[hash].exemplar)) { + this.hashMap[hash].exemplar = new Exemplar(); + } + this.hashMap[hash].exemplar.validateExemplarLabelSet(exemplarLabels); + this.hashMap[hash].exemplar.labelSet = exemplarLabels; + this.hashMap[hash].exemplar.value = value ? value : 1; + this.hashMap[hash].exemplar.timestamp = nowTimestamp(); } /** @@ -54,10 +110,11 @@ class Counter extends Metric { const v = this.collect(); if (v instanceof Promise) await v; } + return { help: this.help, name: this.name, - type, + type: this.type, values: Object.values(this.hashMap), aggregator: this.aggregator, }; diff --git a/lib/exemplar.js b/lib/exemplar.js new file mode 100644 index 00000000..4a090f7e --- /dev/null +++ b/lib/exemplar.js @@ -0,0 +1,37 @@ +'use strict'; + +/** + * Class representing an OpenMetrics exemplar. + * + * @property {object} labelSet + * @property {number} value + * @property {number} [timestamp] + * */ +class Exemplar { + constructor(labelSet = {}, value = null) { + this.labelSet = labelSet; + this.value = value; + } + + /** + * Validation for the label set format. + * https://github.com/OpenObservability/OpenMetrics/blob/d99b705f611b75fec8f450b05e344e02eea6921d/specification/OpenMetrics.md#exemplars + * + * @param {object} labelSet - Exemplar labels. + * @throws {RangeError} + * @return {void} + */ + validateExemplarLabelSet(labelSet) { + let res = ''; + for (const [labelName, labelValue] of Object.entries(labelSet)) { + res += `${labelName}${labelValue}`; + } + if (res.length > 128) { + throw new RangeError( + 'Label set size must be smaller than 128 UTF-8 chars', + ); + } + } +} + +module.exports = Exemplar; diff --git a/lib/gauge.js b/lib/gauge.js index 1540abb4..77327b6b 100644 --- a/lib/gauge.js +++ b/lib/gauge.js @@ -4,7 +4,6 @@ 'use strict'; const util = require('util'); -const type = 'gauge'; const { setValue, @@ -18,6 +17,11 @@ const { validateLabel } = require('./validation'); const { Metric } = require('./metric'); class Gauge extends Metric { + constructor(config) { + super(config); + this.type = 'gauge'; + } + /** * Set a gauge to a value * @param {object} labels - Object with labels and their values @@ -109,7 +113,7 @@ class Gauge extends Metric { return { help: this.help, name: this.name, - type, + type: this.type, values: Object.values(this.hashMap), aggregator: this.aggregator, }; diff --git a/lib/histogram.js b/lib/histogram.js index ee11352d..66ba4fc8 100644 --- a/lib/histogram.js +++ b/lib/histogram.js @@ -4,10 +4,16 @@ 'use strict'; const util = require('util'); -const type = 'histogram'; -const { getLabels, hashObject, isObject, removeLabels } = require('./util'); +const { + getLabels, + hashObject, + isObject, + removeLabels, + nowTimestamp, +} = require('./util'); const { validateLabel } = require('./validation'); const { Metric } = require('./metric'); +const Exemplar = require('./exemplar'); class Histogram extends Metric { constructor(config) { @@ -15,6 +21,16 @@ class Histogram extends Metric { buckets: [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10], }); + this.type = 'histogram'; + this.defaultLabels = {}; + this.defaultExemplarLabelSet = {}; + + if (config.enableExemplars) { + this.observe = this.observeWithExemplar; + } else { + this.observe = this.observeWithoutExemplar; + } + for (const label of this.labelNames) { if (label === 'le') { throw new Error('le is a reserved label keyword'); @@ -27,7 +43,13 @@ class Histogram extends Metric { return acc; }, {}); + this.bucketExemplars = this.upperBounds.reduce((acc, upperBound) => { + acc[upperBound] = null; + return acc; + }, {}); + Object.freeze(this.bucketValues); + Object.freeze(this.bucketExemplars); Object.freeze(this.upperBounds); if (this.labelNames.length === 0) { @@ -35,6 +57,7 @@ class Histogram extends Metric { [hashObject({})]: createBaseValues( {}, Object.assign({}, this.bucketValues), + Object.assign({}, this.bucketExemplars), ), }; } @@ -46,10 +69,33 @@ class Histogram extends Metric { * @param {Number} value - Value to observe in the histogram * @returns {void} */ - observe(labels, value) { + observeWithoutExemplar(labels, value) { observe.call(this, labels === 0 ? 0 : labels || {})(value); } + observeWithExemplar({ + labels = this.defaultLabels, + value, + exemplarLabels = this.defaultExemplarLabelSet, + } = {}) { + observe.call(this, labels === 0 ? 0 : labels || {})(value); + this.updateExemplar(labels, value, exemplarLabels); + } + + updateExemplar(labels, value, exemplarLabels) { + const hash = hashObject(labels); + const b = findBound(this.upperBounds, value); + if (!isObject(this.hashMap[hash].bucketExemplars[b])) { + this.hashMap[hash].bucketExemplars[b] = new Exemplar(); + } + this.hashMap[hash].bucketExemplars[b].validateExemplarLabelSet( + exemplarLabels, + ); + this.hashMap[hash].bucketExemplars[b].labelSet = exemplarLabels; + this.hashMap[hash].bucketExemplars[b].value = value; + this.hashMap[hash].bucketExemplars[b].timestamp = nowTimestamp(); + } + async get() { if (this.collect) { const v = this.collect(); @@ -63,7 +109,7 @@ class Histogram extends Metric { return { name: this.name, help: this.help, - type, + type: this.type, values, aggregator: this.aggregator, }; @@ -83,6 +129,7 @@ class Histogram extends Metric { this.hashMap[hash] = createBaseValues( labels, Object.assign({}, this.bucketValues), + Object.assign({}, this.bucketExemplars), ); } @@ -129,11 +176,12 @@ function startTimer(startLabels) { }; } -function setValuePair(labels, value, metricName) { +function setValuePair(labels, value, metricName, exemplar) { return { labels, value, metricName, + exemplar, }; } @@ -164,6 +212,7 @@ function observe(labels) { valueFromMap = createBaseValues( labelValuePair.labels, Object.assign({}, this.bucketValues), + Object.assign({}, this.bucketExemplars), ); } @@ -180,10 +229,11 @@ function observe(labels) { }; } -function createBaseValues(labels, bucketValues) { +function createBaseValues(labels, bucketValues, bucketExemplars) { return { labels, bucketValues, + bucketExemplars, sum: 0, count: 0, }; @@ -213,7 +263,14 @@ function extractBucketValuesForExport(histogram) { for (const labelName of bucketLabelNames) { lbls[labelName] = bucketData.labels[labelName]; } - buckets.push(setValuePair(lbls, acc, `${histogram.name}_bucket`)); + buckets.push( + setValuePair( + lbls, + acc, + `${histogram.name}_bucket`, + bucketData.bucketExemplars[upperBound], + ), + ); } return { buckets, data: bucketData }; }; @@ -228,7 +285,12 @@ function addSumAndCountForExport(histogram) { infLabel[label] = d.data.labels[label]; } acc.push( - setValuePair(infLabel, d.data.count, `${histogram.name}_bucket`), + setValuePair( + infLabel, + d.data.count, + `${histogram.name}_bucket`, + d.data.bucketExemplars['-1'], + ), setValuePair(d.data.labels, d.data.sum, `${histogram.name}_sum`), setValuePair(d.data.labels, d.data.count, `${histogram.name}_count`), ); diff --git a/lib/metric.js b/lib/metric.js index 95b04321..2e0f4191 100644 --- a/lib/metric.js +++ b/lib/metric.js @@ -1,6 +1,6 @@ 'use strict'; -const { globalRegistry } = require('./registry'); +const Registry = require('./registry'); const { isObject } = require('./util'); const { validateMetricName, validateLabelName } = require('./validation'); @@ -16,15 +16,16 @@ class Metric { this, { labelNames: [], - registers: [globalRegistry], + registers: [Registry.globalRegistry], aggregator: 'sum', + enableExemplars: false, }, defaults, config, ); if (!this.registers) { // in case config.registers is `undefined` - this.registers = [globalRegistry]; + this.registers = [Registry.globalRegistry]; } if (!this.help) { throw new Error('Missing mandatory help parameter'); @@ -45,6 +46,14 @@ class Metric { this.reset(); for (const register of this.registers) { + if ( + this.enableExemplars && + register.contentType === Registry.PROMETHEUS_CONTENT_TYPE + ) { + throw new TypeError( + 'Exemplars are supported only on OpenMetrics registries', + ); + } register.registerMetric(this); } } diff --git a/lib/metrics/gc.js b/lib/metrics/gc.js index 0d2e2839..f26818ab 100644 --- a/lib/metrics/gc.js +++ b/lib/metrics/gc.js @@ -37,6 +37,7 @@ module.exports = (registry, config = {}) => { name: namePrefix + NODEJS_GC_DURATION_SECONDS, help: 'Garbage collection duration by kind, one of major, minor, incremental or weakcb.', labelNames: ['kind', ...labelNames], + enableExemplars: false, buckets, registers: registry ? [registry] : undefined, }); @@ -47,7 +48,6 @@ module.exports = (registry, config = {}) => { // Node >= 16 uses entry.detail.kind // See: https://nodejs.org/docs/latest-v16.x/api/deprecations.html#deprecations_dep0152_extension_performanceentry_properties const kind = entry.detail ? kinds[entry.detail.kind] : kinds[entry.kind]; - // Convert duration from milliseconds to seconds gcHistogram.observe(Object.assign({ kind }, labels), entry.duration / 1000); }); diff --git a/lib/metrics/processCpuTotal.js b/lib/metrics/processCpuTotal.js index ef0aa959..d0213094 100644 --- a/lib/metrics/processCpuTotal.js +++ b/lib/metrics/processCpuTotal.js @@ -1,6 +1,8 @@ 'use strict'; +const OtelApi = require('@opentelemetry/api'); const Counter = require('../counter'); + const PROCESS_CPU_USER_SECONDS = 'process_cpu_user_seconds_total'; const PROCESS_CPU_SYSTEM_SECONDS = 'process_cpu_system_seconds_total'; const PROCESS_CPU_SECONDS = 'process_cpu_seconds_total'; @@ -9,6 +11,7 @@ module.exports = (registry, config = {}) => { const registers = registry ? [registry] : undefined; const namePrefix = config.prefix ? config.prefix : ''; const labels = config.labels ? config.labels : {}; + const exemplars = config.enableExemplars ? config.enableExemplars : false; const labelNames = Object.keys(labels); let lastCpuUsage = process.cpuUsage(); @@ -16,6 +19,7 @@ module.exports = (registry, config = {}) => { const cpuUserUsageCounter = new Counter({ name: namePrefix + PROCESS_CPU_USER_SECONDS, help: 'Total user CPU time spent in seconds.', + enableExemplars: exemplars, registers, labelNames, // Use this one metric's `collect` to set all metrics' values. @@ -27,20 +31,51 @@ module.exports = (registry, config = {}) => { lastCpuUsage = cpuUsage; - cpuUserUsageCounter.inc(labels, userUsageMicros / 1e6); - cpuSystemUsageCounter.inc(labels, systemUsageMicros / 1e6); - cpuUsageCounter.inc(labels, (userUsageMicros + systemUsageMicros) / 1e6); + if (this.enableExemplars) { + let exemplarLabels = {}; + const currentSpan = OtelApi.trace.getSpan(OtelApi.context.active()); + if (currentSpan) { + exemplarLabels = { + traceId: currentSpan.spanContext().traceId, + spanId: currentSpan.spanContext().spanId, + }; + } + cpuUserUsageCounter.inc({ + labels, + value: userUsageMicros / 1e6, + exemplarLabels, + }); + cpuSystemUsageCounter.inc({ + labels, + value: systemUsageMicros / 1e6, + exemplarLabels, + }); + cpuUsageCounter.inc({ + labels, + value: (userUsageMicros + systemUsageMicros) / 1e6, + exemplarLabels, + }); + } else { + cpuUserUsageCounter.inc(labels, userUsageMicros / 1e6); + cpuSystemUsageCounter.inc(labels, systemUsageMicros / 1e6); + cpuUsageCounter.inc( + labels, + (userUsageMicros + systemUsageMicros) / 1e6, + ); + } }, }); const cpuSystemUsageCounter = new Counter({ name: namePrefix + PROCESS_CPU_SYSTEM_SECONDS, help: 'Total system CPU time spent in seconds.', + enableExemplars: exemplars, registers, labelNames, }); const cpuUsageCounter = new Counter({ name: namePrefix + PROCESS_CPU_SECONDS, help: 'Total user and system CPU time spent in seconds.', + enableExemplars: exemplars, registers, labelNames, }); diff --git a/lib/registry.js b/lib/registry.js index 77670f43..2b151809 100644 --- a/lib/registry.js +++ b/lib/registry.js @@ -1,4 +1,5 @@ 'use strict'; + const { getValueAsString } = require('./util'); function escapeString(str) { @@ -10,27 +11,44 @@ function escapeLabelValue(str) { } return escapeString(str).replace(/"/g, '\\"'); } +function standardizeCounterName(name) { + if (name.endsWith('_total')) { + return name.replace('_total', ''); + } + return name; +} class Registry { - constructor() { + static get PROMETHEUS_CONTENT_TYPE() { + return 'text/plain; version=0.0.4; charset=utf-8'; + } + + static get OPENMETRICS_CONTENT_TYPE() { + return 'application/openmetrics-text; version=1.0.0; charset=utf-8'; + } + + constructor(regContentType = Registry.PROMETHEUS_CONTENT_TYPE) { this._metrics = {}; this._collectors = []; this._defaultLabels = {}; + if ( + regContentType !== Registry.PROMETHEUS_CONTENT_TYPE && + regContentType !== Registry.OPENMETRICS_CONTENT_TYPE + ) { + throw new TypeError(`Content type ${regContentType} is unsupported`); + } + this._contentType = regContentType; } getMetricsAsArray() { return Object.values(this._metrics); } - async getMetricAsPrometheusString(metric) { - const item = await metric.get(); - const name = escapeString(item.name); - const help = `# HELP ${name} ${escapeString(item.help)}`; - const type = `# TYPE ${name} ${item.type}`; + getLabelSetAsString(metric) { const defaultLabelNames = Object.keys(this._defaultLabels); - let values = ''; - for (const val of item.values || []) { + + for (const val of metric.values || []) { val.labels = val.labels || {}; if (defaultLabelNames.length > 0) { @@ -43,7 +61,13 @@ class Registry { } } - let metricName = val.metricName || item.name; + let metricName = val.metricName || metric.name; + if ( + this.contentType === Registry.OPENMETRICS_CONTENT_TYPE && + metric.type === 'counter' + ) { + metricName = `${metricName}_total`; + } const keys = Object.keys(val.labels); const size = keys.length; @@ -56,10 +80,47 @@ class Registry { labels += `${keys[i]}="${escapeLabelValue(val.labels[keys[i]])}"`; metricName += `{${labels}}`; } - - values += `${metricName} ${getValueAsString(val.value)}\n`; + values += `${metricName} ${getValueAsString(val.value)}`; + if ( + val.exemplar && + this.contentType === Registry.OPENMETRICS_CONTENT_TYPE + ) { + const exemplarKeys = Object.keys(val.exemplar.labelSet); + const exemplarSize = exemplarKeys.length; + if (exemplarSize > 0) { + let labels = ''; + let i = 0; + for (; i < exemplarSize - 1; i++) { + labels += `${exemplarKeys[i]}="${escapeLabelValue( + val.exemplar.labelSet[exemplarKeys[i]], + )}",`; + } + labels += `${exemplarKeys[i]}="${escapeLabelValue( + val.exemplar.labelSet[exemplarKeys[i]], + )}"`; + values += ` # {${labels}} ${getValueAsString(val.exemplar.value)} ${ + val.exemplar.timestamp + }`; + } else { + values += ` # {} ${getValueAsString(val.exemplar.value)} ${ + val.exemplar.timestamp + }`; + } + } + values += '\n'; } + return values; + } + + async getMetricsAsString(metrics) { + const metric = await metrics.get(); + + const name = escapeString(metric.name); + const help = `# HELP ${name} ${escapeString(metric.help)}`; + const type = `# TYPE ${name} ${metric.type}`; + const values = this.getLabelSetAsString(metric); + return `${help}\n${type}\n${values}`.trim(); } @@ -67,12 +128,22 @@ class Registry { const promises = []; for (const metric of this.getMetricsAsArray()) { - promises.push(this.getMetricAsPrometheusString(metric)); + if ( + this.contentType === Registry.OPENMETRICS_CONTENT_TYPE && + metric.type === 'counter' + ) { + metric.name = standardizeCounterName(metric.name); + } + promises.push(this.getMetricsAsString(metric)); } const resolves = await Promise.all(promises); - return `${resolves.join('\n\n')}\n`; + if (this.contentType === Registry.OPENMETRICS_CONTENT_TYPE) { + return `${resolves.join('\n')}\n# EOF\n`; + } else { + return `${resolves.join('\n\n')}\n`; + } } registerMetric(metric) { @@ -126,7 +197,7 @@ class Registry { } getSingleMetricAsString(name) { - return this.getMetricAsPrometheusString(this._metrics[name]); + return this.getMetricsAsString(this._metrics[name]); } getSingleMetric(name) { @@ -144,11 +215,30 @@ class Registry { } get contentType() { - return 'text/plain; version=0.0.4; charset=utf-8'; + return this._contentType; + } + + setContentType(metricsContentType) { + if ( + metricsContentType === Registry.OPENMETRICS_CONTENT_TYPE || + metricsContentType === Registry.PROMETHEUS_CONTENT_TYPE + ) { + this._contentType = metricsContentType; + } else { + throw new Error(`Content type ${metricsContentType} is unsupported`); + } } static merge(registers) { - const mergedRegistry = new Registry(); + const regType = registers[0].contentType; + for (const reg of registers) { + if (reg.contentType !== regType) { + throw new Error( + 'Registers can only be merged if they have the same content type', + ); + } + } + const mergedRegistry = new Registry(regType); const metricsToMerge = registers.reduce( (acc, reg) => acc.concat(reg.getMetricsAsArray()), diff --git a/lib/summary.js b/lib/summary.js index 9ca3e096..6405d94d 100644 --- a/lib/summary.js +++ b/lib/summary.js @@ -4,7 +4,6 @@ 'use strict'; const util = require('util'); -const type = 'summary'; const { getLabels, hashObject, removeLabels } = require('./util'); const { validateLabel } = require('./validation'); const { Metric } = require('./metric'); @@ -20,6 +19,8 @@ class Summary extends Metric { hashMap: {}, }); + this.type = 'summary'; + for (const label of this.labelNames) { if (label === 'quantile') throw new Error('quantile is a reserved label keyword'); @@ -65,7 +66,7 @@ class Summary extends Metric { return { name: this.name, help: this.help, - type, + type: this.type, values, aggregator: this.aggregator, }; diff --git a/lib/util.js b/lib/util.js index dd626998..c9e3cb3b 100644 --- a/lib/util.js +++ b/lib/util.js @@ -87,6 +87,10 @@ exports.isObject = function isObject(obj) { return obj === Object(obj); }; +exports.nowTimestamp = function nowTimestamp() { + return Date.now() / 1000; +}; + class Grouper extends Map { /** * Adds the `value` to the `key`'s array of values. diff --git a/package.json b/package.json index a76f6eb9..4255332b 100644 --- a/package.json +++ b/package.json @@ -47,6 +47,7 @@ "typescript": "^4.0.2" }, "dependencies": { + "@opentelemetry/api": "^1.4.0", "tdigest": "^0.1.1" }, "types": "./index.d.ts", diff --git a/test/__snapshots__/counterTest.js.snap b/test/__snapshots__/counterTest.js.snap index e68024c3..1b59e42f 100644 --- a/test/__snapshots__/counterTest.js.snap +++ b/test/__snapshots__/counterTest.js.snap @@ -1,9 +1,17 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`counter remove should throw error if label lengths does not match 1`] = `"Invalid number of arguments"`; +exports[`counter with OpenMetrics registry remove should throw error if label lengths does not match 1`] = `"Invalid number of arguments"`; -exports[`counter with params as object labels should throw error if label lengths does not match 1`] = `"Invalid number of arguments"`; +exports[`counter with OpenMetrics registry with params as object labels should throw error if label lengths does not match 1`] = `"Invalid number of arguments"`; -exports[`counter with params as object should not be possible to decrease a counter 1`] = `"It is not possible to decrease a counter"`; +exports[`counter with OpenMetrics registry with params as object should not be possible to decrease a counter 1`] = `"It is not possible to decrease a counter"`; -exports[`counter with params as object should throw an error when the value is not a number 1`] = `"Value is not a valid number: 3ms"`; +exports[`counter with OpenMetrics registry with params as object should throw an error when the value is not a number 1`] = `"Value is not a valid number: 3ms"`; + +exports[`counter with Prometheus registry remove should throw error if label lengths does not match 1`] = `"Invalid number of arguments"`; + +exports[`counter with Prometheus registry with params as object labels should throw error if label lengths does not match 1`] = `"Invalid number of arguments"`; + +exports[`counter with Prometheus registry with params as object should not be possible to decrease a counter 1`] = `"It is not possible to decrease a counter"`; + +exports[`counter with Prometheus registry with params as object should throw an error when the value is not a number 1`] = `"Value is not a valid number: 3ms"`; diff --git a/test/__snapshots__/gaugeTest.js.snap b/test/__snapshots__/gaugeTest.js.snap index ed9dd322..e0ebcdfd 100644 --- a/test/__snapshots__/gaugeTest.js.snap +++ b/test/__snapshots__/gaugeTest.js.snap @@ -1,3 +1,5 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`gauge global registry with parameters as object should not allow non numbers 1`] = `"Value is not a valid number: asd"`; +exports[`gauge with OpenMetrics registry global registry with parameters as object should not allow non numbers 1`] = `"Value is not a valid number: asd"`; + +exports[`gauge with Prometheus registry global registry with parameters as object should not allow non numbers 1`] = `"Value is not a valid number: asd"`; diff --git a/test/__snapshots__/histogramTest.js.snap b/test/__snapshots__/histogramTest.js.snap index ddcc1499..075148fd 100644 --- a/test/__snapshots__/histogramTest.js.snap +++ b/test/__snapshots__/histogramTest.js.snap @@ -1,9 +1,17 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`histogram with object as params with global registry labels should not allow different number of labels 1`] = `"Invalid number of arguments"`; +exports[`histogram with OpenMetrics registry with object as params with global registry labels should not allow different number of labels 1`] = `"Invalid number of arguments"`; -exports[`histogram with object as params with global registry remove should throw error if label lengths does not match 1`] = `"Invalid number of arguments"`; +exports[`histogram with OpenMetrics registry with object as params with global registry remove should throw error if label lengths does not match 1`] = `"Invalid number of arguments"`; -exports[`histogram with object as params with global registry should not allow le as a custom label 1`] = `"le is a reserved label keyword"`; +exports[`histogram with OpenMetrics registry with object as params with global registry should not allow le as a custom label 1`] = `"le is a reserved label keyword"`; -exports[`histogram with object as params with global registry should not allow non numbers 1`] = `"Value is not a valid number: asd"`; +exports[`histogram with OpenMetrics registry with object as params with global registry should not allow non numbers 1`] = `"Value is not a valid number: asd"`; + +exports[`histogram with Prometheus registry with object as params with global registry labels should not allow different number of labels 1`] = `"Invalid number of arguments"`; + +exports[`histogram with Prometheus registry with object as params with global registry remove should throw error if label lengths does not match 1`] = `"Invalid number of arguments"`; + +exports[`histogram with Prometheus registry with object as params with global registry should not allow le as a custom label 1`] = `"le is a reserved label keyword"`; + +exports[`histogram with Prometheus registry with object as params with global registry should not allow non numbers 1`] = `"Value is not a valid number: asd"`; diff --git a/test/__snapshots__/registerTest.js.snap b/test/__snapshots__/registerTest.js.snap index 47548530..000b1446 100644 --- a/test/__snapshots__/registerTest.js.snap +++ b/test/__snapshots__/registerTest.js.snap @@ -1,6 +1,57 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`register should not output all initialized metrics at value 0 if labels 1`] = ` +exports[`Register with OpenMetrics type should not output all initialized metrics at value 0 if labels 1`] = ` +"# HELP counter help +# TYPE counter counter +# HELP gauge help +# TYPE gauge gauge +# HELP histogram help +# TYPE histogram histogram +# HELP summary help +# TYPE summary summary +# EOF +" +`; + +exports[`Register with OpenMetrics type should output all initialized metrics at value 0 1`] = ` +"# HELP counter help +# TYPE counter counter +counter_total 0 +# HELP gauge help +# TYPE gauge gauge +gauge 0 +# HELP histogram help +# TYPE histogram histogram +histogram_bucket{le="0.005"} 0 +histogram_bucket{le="0.01"} 0 +histogram_bucket{le="0.025"} 0 +histogram_bucket{le="0.05"} 0 +histogram_bucket{le="0.1"} 0 +histogram_bucket{le="0.25"} 0 +histogram_bucket{le="0.5"} 0 +histogram_bucket{le="1"} 0 +histogram_bucket{le="2.5"} 0 +histogram_bucket{le="5"} 0 +histogram_bucket{le="10"} 0 +histogram_bucket{le="+Inf"} 0 +histogram_sum 0 +histogram_count 0 +# HELP summary help +# TYPE summary summary +summary{quantile="0.01"} 0 +summary{quantile="0.05"} 0 +summary{quantile="0.5"} 0 +summary{quantile="0.9"} 0 +summary{quantile="0.95"} 0 +summary{quantile="0.99"} 0 +summary{quantile="0.999"} 0 +summary_sum 0 +summary_count 0 +# EOF +" +`; + +exports[`Register with Prometheus type should not output all initialized metrics at value 0 if labels 1`] = ` "# HELP counter help # TYPE counter counter @@ -15,7 +66,7 @@ exports[`register should not output all initialized metrics at value 0 if labels " `; -exports[`register should output all initialized metrics at value 0 1`] = ` +exports[`Register with Prometheus type should output all initialized metrics at value 0 1`] = ` "# HELP counter help # TYPE counter counter counter 0 diff --git a/test/__snapshots__/summaryTest.js.snap b/test/__snapshots__/summaryTest.js.snap index 45bea2e8..9dade939 100644 --- a/test/__snapshots__/summaryTest.js.snap +++ b/test/__snapshots__/summaryTest.js.snap @@ -1,7 +1,13 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`summary global registry with param as object labels should throw error if label lengths does not match 1`] = `"Invalid number of arguments"`; +exports[`summary with OpenMetrics registry global registry with param as object labels should throw error if label lengths does not match 1`] = `"Invalid number of arguments"`; -exports[`summary global registry with param as object remove should throw error if label lengths does not match 1`] = `"Invalid number of arguments"`; +exports[`summary with OpenMetrics registry global registry with param as object remove should throw error if label lengths does not match 1`] = `"Invalid number of arguments"`; -exports[`summary global registry with param as object should validate labels when observing 1`] = `"Added label "baz" is not included in initial labelset: [ 'foo' ]"`; +exports[`summary with OpenMetrics registry global registry with param as object should validate labels when observing 1`] = `"Added label "baz" is not included in initial labelset: [ 'foo' ]"`; + +exports[`summary with Prometheus registry global registry with param as object labels should throw error if label lengths does not match 1`] = `"Invalid number of arguments"`; + +exports[`summary with Prometheus registry global registry with param as object remove should throw error if label lengths does not match 1`] = `"Invalid number of arguments"`; + +exports[`summary with Prometheus registry global registry with param as object should validate labels when observing 1`] = `"Added label "baz" is not included in initial labelset: [ 'foo' ]"`; \ No newline at end of file diff --git a/test/clusterTest.js b/test/clusterTest.js index 8569ea99..d1a58314 100644 --- a/test/clusterTest.js +++ b/test/clusterTest.js @@ -2,8 +2,16 @@ const cluster = require('cluster'); const process = require('process'); +const Registry = require('../lib/cluster'); + +describe.each([ + ['Prometheus', Registry.PROMETHEUS_CONTENT_TYPE], + ['OpenMetrics', Registry.OPENMETRICS_CONTENT_TYPE], +])('%s AggregatorRegistry', (tag, regType) => { + beforeEach(() => { + Registry.globalRegistry.setContentType(regType); + }); -describe('AggregatorRegistry', () => { it('requiring the cluster should not add any listeners on the cluster module', () => { const originalListenerCount = cluster.listenerCount('message'); @@ -35,14 +43,13 @@ describe('AggregatorRegistry', () => { describe('aggregatorRegistry.clusterMetrics()', () => { it('works properly if there are no cluster workers', async () => { const AggregatorRegistry = require('../lib/cluster'); - const ar = new AggregatorRegistry(); + const ar = new AggregatorRegistry(regType); const metrics = await ar.clusterMetrics(); expect(metrics).toEqual(''); }); }); describe('AggregatorRegistry.aggregate()', () => { - const Registry = require('../lib/cluster'); // These mimic the output of `getMetricsAsJSON`. const metricsArr1 = [ { @@ -159,7 +166,7 @@ describe('AggregatorRegistry', () => { }, ]; - const aggregated = Registry.aggregate([metricsArr1, metricsArr2]); + const aggregated = Registry.aggregate([metricsArr1, metricsArr2], regType); it('defaults to summation, preserves histogram bins', async () => { const histogram = aggregated.getSingleMetric('test_histogram').get(); diff --git a/test/counterTest.js b/test/counterTest.js index ff16e512..0ee0fc2d 100644 --- a/test/counterTest.js +++ b/test/counterTest.js @@ -1,11 +1,19 @@ 'use strict'; -describe('counter', () => { +const Registry = require('../index').Registry; + +describe.each([ + ['Prometheus', Registry.PROMETHEUS_CONTENT_TYPE], + ['OpenMetrics', Registry.OPENMETRICS_CONTENT_TYPE], +])('counter with %s registry', (tag, regType) => { const Counter = require('../index').Counter; - const Registry = require('../index').Registry; const globalRegistry = require('../index').register; let instance; + beforeEach(() => { + globalRegistry.setContentType(regType); + }); + describe('with params as object', () => { beforeEach(() => { instance = new Counter({ name: 'gauge_test', help: 'test' }); @@ -168,7 +176,7 @@ describe('counter', () => { describe('registry instance', () => { let registryInstance; beforeEach(() => { - registryInstance = new Registry(); + registryInstance = new Registry(regType); instance = new Counter({ name: 'gauge_test', help: 'test', diff --git a/test/defaultMetricsTest.js b/test/defaultMetricsTest.js index 15fe7095..da01265a 100644 --- a/test/defaultMetricsTest.js +++ b/test/defaultMetricsTest.js @@ -1,8 +1,12 @@ 'use strict'; -describe('collectDefaultMetrics', () => { +const Registry = require('../index').Registry; + +describe.each([ + ['Prometheus', Registry.PROMETHEUS_CONTENT_TYPE], + ['OpenMetrics', Registry.OPENMETRICS_CONTENT_TYPE], +])('collectDefaultMetrics with %s registry', (tag, regType) => { const register = require('../index').register; - const Registry = require('../index').Registry; const collectDefaultMetrics = require('../index').collectDefaultMetrics; let cpuUsage; @@ -34,6 +38,10 @@ describe('collectDefaultMetrics', () => { } }); + beforeEach(() => { + register.setContentType(regType); + }); + afterEach(() => { register.clear(); }); @@ -95,7 +103,7 @@ describe('collectDefaultMetrics', () => { describe('custom registry', () => { it('should allow to register metrics to custom registry', async () => { - const registry = new Registry(); + const registry = new Registry(regType); expect(await register.getMetricsAsJSON()).toHaveLength(0); expect(await registry.getMetricsAsJSON()).toHaveLength(0); diff --git a/test/exemplarsTest.js b/test/exemplarsTest.js new file mode 100644 index 00000000..ef5c2254 --- /dev/null +++ b/test/exemplarsTest.js @@ -0,0 +1,131 @@ +'use strict'; + +const Registry = require('../index').Registry; +const globalRegistry = require('../index').register; +const Histogram = require('../index').Histogram; +const Counter = require('../index').Counter; + +describe('Exemplars', () => { + it('should throw when using with Prometheus registry', async () => { + globalRegistry.setContentType(Registry.PROMETHEUS_CONTENT_TYPE); + expect(() => { + const counterInstance = new Counter({ + name: 'counter_exemplar_test', + help: 'help', + labelNames: ['method', 'code'], + enableExemplars: true, + }); + }).toThrowError('Exemplars are supported only on OpenMetrics registries'); + }); + describe.each([['OpenMetrics', Registry.OPENMETRICS_CONTENT_TYPE]])( + 'with %s registry', + (tag, regType) => { + beforeEach(() => { + globalRegistry.setContentType(regType); + }); + + it('should make counter with exemplar', async () => { + const counterInstance = new Counter({ + name: 'counter_exemplar_test', + help: 'help', + labelNames: ['method', 'code'], + enableExemplars: true, + }); + counterInstance.inc({ + value: 2, + labels: { method: 'get', code: '200' }, + exemplarLabels: { traceId: 'trace_id_test', spanId: 'span_id_test' }, + }); + const vals = await counterInstance.get(); + expect(vals.values[0].value).toEqual(2); + expect(vals.values[0].exemplar.value).toEqual(2); + expect(vals.values[0].exemplar.labelSet.traceId).toEqual( + 'trace_id_test', + ); + }); + + it('should make histogram with exemplars on multiple buckets', async () => { + const histogramInstance = new Histogram({ + name: 'histogram_exemplar_test', + help: 'test', + labelNames: ['method', 'code'], + enableExemplars: true, + }); + + histogramInstance.observe({ + value: 0.007, + labels: { method: 'get', code: '200' }, + exemplarLabels: { + traceId: 'trace_id_test_1', + spanId: 'span_id_test_1', + }, + }); + histogramInstance.observe({ + value: 0.4, + labels: { method: 'get', code: '200' }, + exemplarLabels: { + traceId: 'trace_id_test_2', + spanId: 'span_id_test_2', + }, + }); + histogramInstance.observe({ + value: 11, + labels: { method: 'get', code: '200' }, + exemplarLabels: { + traceId: 'trace_id_test_3', + spanId: 'span_id_test_3', + }, + }); + + const vals = (await histogramInstance.get()).values; + + expect(getValuesByLabel(0.005, vals)[0].value).toEqual(0); + expect(getValuesByLabel(0.005, vals)[0].exemplar).toEqual(null); + + expect(getValuesByLabel(0.5, vals)[0].value).toEqual(2); + expect( + getValuesByLabel(0.5, vals)[0].exemplar.labelSet.traceId, + ).toEqual('trace_id_test_2'); + expect(getValuesByLabel(0.5, vals)[0].exemplar.value).toEqual(0.4); + + expect(getValuesByLabel(10, vals)[0].value).toEqual(2); + expect(getValuesByLabel(10, vals)[0].exemplar).toEqual(null); + + expect(getValuesByLabel('+Inf', vals)[0].value).toEqual(3); + expect( + getValuesByLabel('+Inf', vals)[0].exemplar.labelSet.traceId, + ).toEqual('trace_id_test_3'); + expect(getValuesByLabel('+Inf', vals)[0].exemplar.value).toEqual(11); + }); + + it('should throw if exemplar is too long', async () => { + const histogramInstance = new Histogram({ + name: 'histogram_too_long_exemplar_test', + help: 'test', + labelNames: ['method', 'code'], + enableExemplars: true, + }); + + expect(() => { + histogramInstance.observe({ + value: 0.007, + labels: { method: 'get', code: '200' }, + exemplarLabels: { + traceId: 'j'.repeat(100), + spanId: 'j'.repeat(100), + }, + }); + }).toThrowError('Label set size must be smaller than 128 UTF-8 chars'); + }); + + function getValuesByLabel(label, values, key) { + return values.reduce((acc, val) => { + if (val.labels && val.labels[key || 'le'] === label) { + acc.push(val); + } + return acc; + }, []); + } + }, + ); +}); diff --git a/test/gaugeTest.js b/test/gaugeTest.js index 3657b950..c7d9ec59 100644 --- a/test/gaugeTest.js +++ b/test/gaugeTest.js @@ -1,11 +1,19 @@ 'use strict'; -describe('gauge', () => { +const Registry = require('../index').Registry; + +describe.each([ + ['Prometheus', Registry.PROMETHEUS_CONTENT_TYPE], + ['OpenMetrics', Registry.OPENMETRICS_CONTENT_TYPE], +])('gauge with %s registry', (tag, regType) => { const Gauge = require('../index').Gauge; - const Registry = require('../index').Registry; const globalRegistry = require('../index').register; let instance; + beforeEach(() => { + globalRegistry.setContentType(regType); + }); + describe('global registry', () => { afterEach(() => { globalRegistry.clear(); @@ -208,7 +216,7 @@ describe('gauge', () => { describe('registry instance', () => { let registryInstance; beforeEach(() => { - registryInstance = new Registry(); + registryInstance = new Registry(regType); instance = new Gauge({ name: 'gauge_test', help: 'help', diff --git a/test/histogramTest.js b/test/histogramTest.js index 06978ad5..5ec89f97 100644 --- a/test/histogramTest.js +++ b/test/histogramTest.js @@ -1,11 +1,19 @@ 'use strict'; -describe('histogram', () => { +const Registry = require('../index').Registry; + +describe.each([ + ['Prometheus', Registry.PROMETHEUS_CONTENT_TYPE], + ['OpenMetrics', Registry.OPENMETRICS_CONTENT_TYPE], +])('histogram with %s registry', (tag, regType) => { const Histogram = require('../index').Histogram; - const Registry = require('../index').Registry; const globalRegistry = require('../index').register; let instance; + beforeEach(() => { + globalRegistry.setContentType(regType); + }); + afterEach(() => { instance = null; globalRegistry.clear(); @@ -435,7 +443,7 @@ describe('histogram', () => { describe('registry instance', () => { let registryInstance; beforeEach(() => { - registryInstance = new Registry(); + registryInstance = new Registry(regType); instance = new Histogram({ name: 'test_histogram', help: 'test', diff --git a/test/metrics/eventLoopLagTest.js b/test/metrics/eventLoopLagTest.js index 8662c299..7c347bb0 100644 --- a/test/metrics/eventLoopLagTest.js +++ b/test/metrics/eventLoopLagTest.js @@ -1,6 +1,11 @@ 'use strict'; -describe('eventLoopLag', () => { +const Registry = require('../../index').Registry; + +describe.each([ + ['Prometheus', Registry.PROMETHEUS_CONTENT_TYPE], + ['OpenMetrics', Registry.OPENMETRICS_CONTENT_TYPE], +])('eventLoopLag with %s registry', (tag, regType) => { const register = require('../../index').register; const eventLoopLag = require('../../lib/metrics/eventLoopLag'); @@ -8,11 +13,15 @@ describe('eventLoopLag', () => { register.clear(); }); + beforeEach(() => { + register.setContentType(regType); + }); + afterEach(() => { register.clear(); }); - it('should add metric to the registry', async () => { + it(`should add metric to the ${tag} registry`, async () => { expect(await register.getMetricsAsJSON()).toHaveLength(0); eventLoopLag(); diff --git a/test/metrics/gcTest.js b/test/metrics/gcTest.js index e9bbee8f..81253cfb 100644 --- a/test/metrics/gcTest.js +++ b/test/metrics/gcTest.js @@ -1,6 +1,11 @@ 'use strict'; -describe('gc', () => { +const Registry = require('../../index').Registry; + +describe.each([ + ['Prometheus', Registry.PROMETHEUS_CONTENT_TYPE], + ['OpenMetrics', Registry.OPENMETRICS_CONTENT_TYPE], +])('gc with %s registry', (tag, regType) => { const register = require('../../index').register; const processHandles = require('../../lib/metrics/gc'); @@ -8,11 +13,15 @@ describe('gc', () => { register.clear(); }); + beforeEach(() => { + register.setContentType(regType); + }); + afterEach(() => { register.clear(); }); - it('should add metric to the registry', async () => { + it(`should add metric to the ${tag} registry`, async () => { expect(await register.getMetricsAsJSON()).toHaveLength(0); processHandles(); diff --git a/test/metrics/heapSizeAndUsedTest.js b/test/metrics/heapSizeAndUsedTest.js index 6ac37bdc..3c917ed6 100644 --- a/test/metrics/heapSizeAndUsedTest.js +++ b/test/metrics/heapSizeAndUsedTest.js @@ -1,10 +1,19 @@ 'use strict'; -describe('heapSizeAndUsed', () => { +const Registry = require('../../index').Registry; + +describe.each([ + ['Prometheus', Registry.PROMETHEUS_CONTENT_TYPE], + ['OpenMetrics', Registry.OPENMETRICS_CONTENT_TYPE], +])('heapSizeAndUsed with %s registry', (tag, regType) => { const heapSizeAndUsed = require('../../lib/metrics/heapSizeAndUsed'); const globalRegistry = require('../../lib/registry').globalRegistry; const memoryUsedFn = process.memoryUsage; + beforeEach(() => { + globalRegistry.setContentType(regType); + }); + afterEach(() => { process.memoryUsage = memoryUsedFn; globalRegistry.clear(); diff --git a/test/metrics/heapSpacesSizeAndUsedTest.js b/test/metrics/heapSpacesSizeAndUsedTest.js index be984563..c4c06ce7 100644 --- a/test/metrics/heapSpacesSizeAndUsedTest.js +++ b/test/metrics/heapSpacesSizeAndUsedTest.js @@ -1,5 +1,7 @@ 'use strict'; +const Registry = require('../../index').Registry; + jest.mock('v8', () => { return { getHeapSpaceStatistics() { @@ -44,19 +46,23 @@ jest.mock('v8', () => { }; }); -describe('heapSpacesSizeAndUsed', () => { +describe.each([ + ['Prometheus', Registry.PROMETHEUS_CONTENT_TYPE], + ['OpenMetrics', Registry.OPENMETRICS_CONTENT_TYPE], +])('heapSpacesSizeAndUsed with %s registry', (tag, regType) => { let heapSpacesSizeAndUsed; const globalRegistry = require('../../lib/registry').globalRegistry; beforeEach(() => { heapSpacesSizeAndUsed = require('../../lib/metrics/heapSpacesSizeAndUsed'); + globalRegistry.setContentType(regType); }); afterEach(() => { globalRegistry.clear(); }); - it('should set total heap spaces size gauges with values from v8', async () => { + it(`should set total heap spaces size gauges with values from v8 with ${tag} registry`, async () => { expect(await globalRegistry.getMetricsAsJSON()).toHaveLength(0); heapSpacesSizeAndUsed(); diff --git a/test/metrics/maxFileDescriptorsTest.js b/test/metrics/maxFileDescriptorsTest.js index f89aa45b..3921b871 100644 --- a/test/metrics/maxFileDescriptorsTest.js +++ b/test/metrics/maxFileDescriptorsTest.js @@ -1,8 +1,12 @@ 'use strict'; const exec = require('child_process').execSync; +const Registry = require('../../index').Registry; -describe('processMaxFileDescriptors', () => { +describe.each([ + ['Prometheus', Registry.PROMETHEUS_CONTENT_TYPE], + ['OpenMetrics', Registry.OPENMETRICS_CONTENT_TYPE], +])('processMaxFileDescriptors with %s registry', (tag, regType) => { const register = require('../../index').register; const processMaxFileDescriptors = require('../../lib/metrics/processMaxFileDescriptors'); @@ -10,12 +14,16 @@ describe('processMaxFileDescriptors', () => { register.clear(); }); + beforeEach(() => { + register.setContentType(regType); + }); + afterEach(() => { register.clear(); }); if (process.platform !== 'linux') { - it('should not add metric to the registry', async () => { + it(`should not add metric to the ${tag} registry`, async () => { expect(await register.getMetricsAsJSON()).toHaveLength(0); processMaxFileDescriptors(); @@ -23,7 +31,7 @@ describe('processMaxFileDescriptors', () => { expect(await register.getMetricsAsJSON()).toHaveLength(0); }); } else { - it('should add metric to the registry', async () => { + it(`should add metric to the ${tag} registry`, async () => { expect(await register.getMetricsAsJSON()).toHaveLength(0); processMaxFileDescriptors(); @@ -39,7 +47,7 @@ describe('processMaxFileDescriptors', () => { expect(metrics[0].values).toHaveLength(1); }); - it('should have a reasonable metric value', async () => { + it(`should have a reasonable metric value with ${tag} registry`, async () => { const maxFiles = Number(exec('ulimit -Hn', { encoding: 'utf8' })); expect(await register.getMetricsAsJSON()).toHaveLength(0); diff --git a/test/metrics/processHandlesTest.js b/test/metrics/processHandlesTest.js index 614b34d8..dd03404c 100644 --- a/test/metrics/processHandlesTest.js +++ b/test/metrics/processHandlesTest.js @@ -1,6 +1,11 @@ 'use strict'; -describe('processHandles', () => { +const Registry = require('../../index').Registry; + +describe.each([ + ['Prometheus', Registry.PROMETHEUS_CONTENT_TYPE], + ['OpenMetrics', Registry.OPENMETRICS_CONTENT_TYPE], +])('processHandles with %s registry', (tag, regType) => { const register = require('../../index').register; const processHandles = require('../../lib/metrics/processHandles'); @@ -8,11 +13,15 @@ describe('processHandles', () => { register.clear(); }); + beforeEach(() => { + register.setContentType(regType); + }); + afterEach(() => { register.clear(); }); - it('should add metric to the registry', async () => { + it(`should add metric to the ${tag} registry`, async () => { expect(await register.getMetricsAsJSON()).toHaveLength(0); processHandles(); diff --git a/test/metrics/processOpenFileDescriptorsTest.js b/test/metrics/processOpenFileDescriptorsTest.js index 1309d555..7cc6c50b 100644 --- a/test/metrics/processOpenFileDescriptorsTest.js +++ b/test/metrics/processOpenFileDescriptorsTest.js @@ -1,24 +1,32 @@ 'use strict'; -describe('processOpenFileDescriptors', () => { +const Registry = require('../../index').Registry; + +jest.mock( + 'process', + () => Object.assign({}, jest.requireActual('process'), { platform: 'linux' }), // This metric only works on Linux +); + +describe.each([ + ['Prometheus', Registry.PROMETHEUS_CONTENT_TYPE], + ['OpenMetrics', Registry.OPENMETRICS_CONTENT_TYPE], +])('processOpenFileDescriptors with %s registry', (tag, regType) => { const register = require('../../index').register; const processOpenFileDescriptors = require('../../lib/metrics/processOpenFileDescriptors'); - jest.mock( - 'process', - () => - Object.assign({}, jest.requireActual('process'), { platform: 'linux' }), // This metric only works on Linux - ); - beforeAll(() => { register.clear(); }); + beforeEach(() => { + register.setContentType(regType); + }); + afterEach(() => { register.clear(); }); - it('should add metric to the registry', async () => { + it(`should add metric to the ${tag} registry`, async () => { expect(await register.getMetricsAsJSON()).toHaveLength(0); processOpenFileDescriptors(); diff --git a/test/metrics/processRequestsTest.js b/test/metrics/processRequestsTest.js index 7e9afe0e..c28a51d2 100644 --- a/test/metrics/processRequestsTest.js +++ b/test/metrics/processRequestsTest.js @@ -1,6 +1,11 @@ 'use strict'; -describe('processRequests', () => { +const Registry = require('../../index').Registry; + +describe.each([ + ['Prometheus', Registry.PROMETHEUS_CONTENT_TYPE], + ['OpenMetrics', Registry.OPENMETRICS_CONTENT_TYPE], +])('processRequests with %s registry', (tag, regType) => { const register = require('../../index').register; const processRequests = require('../../lib/metrics/processRequests'); @@ -8,11 +13,15 @@ describe('processRequests', () => { register.clear(); }); + beforeEach(() => { + register.setContentType(regType); + }); + afterEach(() => { register.clear(); }); - it('should add metric to the registry', async () => { + it(`should add metric to the ${tag} registry`, async () => { expect(await register.getMetricsAsJSON()).toHaveLength(0); processRequests(); diff --git a/test/metrics/processStartTimeTest.js b/test/metrics/processStartTimeTest.js index e51fa887..bee5cdab 100644 --- a/test/metrics/processStartTimeTest.js +++ b/test/metrics/processStartTimeTest.js @@ -1,6 +1,11 @@ 'use strict'; -describe('processStartTime', () => { +const Registry = require('../../index').Registry; + +describe.each([ + ['Prometheus', Registry.PROMETHEUS_CONTENT_TYPE], + ['OpenMetrics', Registry.OPENMETRICS_CONTENT_TYPE], +])('processStartTime with %s registry', (tag, regType) => { const register = require('../../index').register; const op = require('../../lib/metrics/processStartTime'); @@ -8,11 +13,15 @@ describe('processStartTime', () => { register.clear(); }); + beforeEach(() => { + register.setContentType(regType); + }); + afterEach(() => { register.clear(); }); - it('should add metric to the registry', async () => { + it(`should add metric to the ${tag} registry`, async () => { expect(await register.getMetricsAsJSON()).toHaveLength(0); op(); diff --git a/test/metrics/versionTest.js b/test/metrics/versionTest.js index 42abcc03..0a28fb0e 100644 --- a/test/metrics/versionTest.js +++ b/test/metrics/versionTest.js @@ -1,5 +1,6 @@ 'use strict'; +const Registry = require('../../index').Registry; const nodeVersion = process.version; const versionSegments = nodeVersion.slice(1).split('.').map(Number); @@ -15,7 +16,10 @@ function expectVersionMetrics(metrics) { expect(metrics[0].values[0].labels.patch).toEqual(versionSegments[2]); } -describe('version', () => { +describe.each([ + ['Prometheus', Registry.PROMETHEUS_CONTENT_TYPE], + ['OpenMetrics', Registry.OPENMETRICS_CONTENT_TYPE], +])('version with %s registry', (tag, regType) => { const register = require('../../index').register; const version = require('../../lib/metrics/version'); @@ -23,11 +27,15 @@ describe('version', () => { register.clear(); }); + beforeEach(() => { + register.setContentType(regType); + }); + afterEach(() => { register.clear(); }); - it('should add metric to the registry', async () => { + it(`should add metric to the ${tag} registry`, async () => { expect(await register.getMetricsAsJSON()).toHaveLength(0); expect(typeof versionSegments[0]).toEqual('number'); expect(typeof versionSegments[1]).toEqual('number'); @@ -39,7 +47,7 @@ describe('version', () => { expectVersionMetrics(metrics); }); - it('should still be present after resetting the registry #238', async () => { + it(`should still be present after resetting the ${tag} registry #238`, async () => { const collector = version(); expectVersionMetrics(await register.getMetricsAsJSON()); register.resetMetrics(); diff --git a/test/pushgatewayTest.js b/test/pushgatewayTest.js index 6e84f8a2..f61f480e 100644 --- a/test/pushgatewayTest.js +++ b/test/pushgatewayTest.js @@ -3,21 +3,33 @@ const nock = require('nock'); const { gzipSync } = require('zlib'); -describe('pushgateway', () => { +const Registry = require('../index').Registry; + +describe.each([ + ['Prometheus', Registry.PROMETHEUS_CONTENT_TYPE], + ['OpenMetrics', Registry.OPENMETRICS_CONTENT_TYPE], +])('pushgateway with %s registry', (tag, regType) => { const Pushgateway = require('../index').Pushgateway; const register = require('../index').register; - const Registry = require('../index').Registry; let instance; let registry = undefined; + beforeEach(() => { + register.setContentType(regType); + }); + const tests = function () { + let body; + if (regType === Registry.OPENMETRICS_CONTENT_TYPE) { + body = '# HELP test test\n# TYPE test counter\ntest_total 100\n# EOF\n'; + } else { + body = '# HELP test test\n# TYPE test counter\ntest 100\n'; + } + describe('pushAdd', () => { it('should push metrics', () => { const mockHttp = nock('http://192.168.99.100:9091') - .post( - '/metrics/job/testJob', - '# HELP test test\n# TYPE test counter\ntest 100\n', - ) + .post('/metrics/job/testJob', body) .reply(200); return instance.pushAdd({ jobName: 'testJob' }).then(() => { @@ -27,10 +39,7 @@ describe('pushgateway', () => { it('should use groupings', () => { const mockHttp = nock('http://192.168.99.100:9091') - .post( - '/metrics/job/testJob/key/value', - '# HELP test test\n# TYPE test counter\ntest 100\n', - ) + .post('/metrics/job/testJob/key/value', body) .reply(200); return instance @@ -45,10 +54,7 @@ describe('pushgateway', () => { it('should escape groupings', () => { const mockHttp = nock('http://192.168.99.100:9091') - .post( - '/metrics/job/testJob/key/va%26lue', - '# HELP test test\n# TYPE test counter\ntest 100\n', - ) + .post('/metrics/job/testJob/key/va%26lue', body) .reply(200); return instance @@ -65,10 +71,7 @@ describe('pushgateway', () => { describe('push', () => { it('should push with PUT', () => { const mockHttp = nock('http://192.168.99.100:9091') - .put( - '/metrics/job/testJob', - '# HELP test test\n# TYPE test counter\ntest 100\n', - ) + .put('/metrics/job/testJob', body) .reply(200); return instance.push({ jobName: 'testJob' }).then(() => { @@ -78,10 +81,7 @@ describe('pushgateway', () => { it('should uri encode url', () => { const mockHttp = nock('http://192.168.99.100:9091') - .put( - '/metrics/job/test%26Job', - '# HELP test test\n# TYPE test counter\ntest 100\n', - ) + .put('/metrics/job/test%26Job', body) .reply(200); return instance.push({ jobName: 'test&Job' }).then(() => { @@ -117,10 +117,7 @@ describe('pushgateway', () => { it('pushAdd should send POST request with basic auth data', () => { const mockHttp = nock(`http://${auth}@192.168.99.100:9091`) - .post( - '/metrics/job/testJob', - '# HELP test test\n# TYPE test counter\ntest 100\n', - ) + .post('/metrics/job/testJob', body) .reply(200); return instance.pushAdd({ jobName: 'testJob' }).then(() => { @@ -130,10 +127,7 @@ describe('pushgateway', () => { it('push should send PUT request with basic auth data', () => { const mockHttp = nock(`http://${auth}@192.168.99.100:9091`) - .put( - '/metrics/job/testJob', - '# HELP test test\n# TYPE test counter\ntest 100\n', - ) + .put('/metrics/job/testJob', body) .reply(200); return instance.push({ jobName: 'testJob' }).then(() => { @@ -158,10 +152,7 @@ describe('pushgateway', () => { 'unit-test': '1', }, }) - .put( - '/metrics/job/testJob', - '# HELP test test\n# TYPE test counter\ntest 100\n', - ) + .put('/metrics/job/testJob', body) .reply(200); instance = new Pushgateway( @@ -185,10 +176,7 @@ describe('pushgateway', () => { 'Content-Encoding': 'gzip', }, }) - .post( - '/metrics/job/testJob', - gzipSync('# HELP test test\n# TYPE test counter\ntest 100\n'), - ) + .post('/metrics/job/testJob', gzipSync(body)) .reply(200); instance = new Pushgateway( @@ -229,7 +217,7 @@ describe('pushgateway', () => { }); beforeEach(() => { - registry = new Registry(); + registry = new Registry(regType); instance = new Pushgateway('http://192.168.99.100:9091', null, registry); const promeClient = require('../index'); const cnt = new promeClient.Counter({ diff --git a/test/pushgatewayWithPathTest.js b/test/pushgatewayWithPathTest.js index 36a9df73..df274eb4 100644 --- a/test/pushgatewayWithPathTest.js +++ b/test/pushgatewayWithPathTest.js @@ -16,13 +16,21 @@ jest.mock('http', () => { }; }); -describe('pushgateway with path', () => { +const Registry = require('../index').Registry; + +describe.each([ + ['Prometheus', Registry.PROMETHEUS_CONTENT_TYPE], + ['OpenMetrics', Registry.OPENMETRICS_CONTENT_TYPE], +])('pushgateway with path and %s registry', (tag, regType) => { const Pushgateway = require('../index').Pushgateway; const register = require('../index').register; - const Registry = require('../index').Registry; let instance; let registry = undefined; + beforeEach(() => { + register.setContentType(regType); + }); + const tests = function () { describe('pushAdd', () => { it('should push metrics', () => { @@ -164,7 +172,7 @@ describe('pushgateway with path', () => { mockHttp.mockClear(); }); beforeEach(() => { - registry = new Registry(); + registry = new Registry(regType); instance = new Pushgateway(pushGatewayFullURL, null, registry); const promClient = require('../index'); const cnt = new promClient.Counter({ diff --git a/test/registerTest.js b/test/registerTest.js index 81d38c00..1e4623cf 100644 --- a/test/registerTest.js +++ b/test/registerTest.js @@ -1,646 +1,735 @@ 'use strict'; -describe('register', () => { - const register = require('../index').register; - const Counter = require('../index').Counter; - const Gauge = require('../index').Gauge; - const Histogram = require('../index').Histogram; - const Summary = require('../index').Summary; - - beforeEach(() => { - register.clear(); - }); - - describe('should output a counter metric', () => { - let output; - beforeEach(async () => { - register.registerMetric(getMetric()); - output = (await register.metrics()).split('\n'); - }); - - it('with help as first item', () => { - expect(output[0]).toEqual('# HELP test_metric A test metric'); - }); - it('with type as second item', () => { - expect(output[1]).toEqual('# TYPE test_metric counter'); - }); - it('with first value of the metric as third item', () => { - expect(output[2]).toEqual('test_metric{label="hello",code="303"} 12'); - }); - it('with second value of the metric as fourth item', () => { - expect(output[3]).toEqual('test_metric{label="bye",code="404"} 34'); - }); +const Registry = require('../index').Registry; +const register = require('../index').register; + +describe('Register', () => { + const contentTypeTestStr = + 'application/openmetrics-text; version=42.0.0; charset=utf-8'; + const expectedContentTypeErrStr = `Content type ${contentTypeTestStr} is unsupported`; + it('should throw if set to an unsupported type', () => { + expect(() => { + register.setContentType(contentTypeTestStr); + }).toThrowError(expectedContentTypeErrStr); }); - it('should throw on more than one metric', () => { - register.registerMetric(getMetric()); - + it('should throw if created with an unsupported type', () => { expect(() => { - register.registerMetric(getMetric()); - }).toThrowError( - 'A metric with the name test_metric has already been registered.', - ); + new Registry(contentTypeTestStr); + }).toThrowError(expectedContentTypeErrStr); }); - it('should handle and output a metric with a NaN value', async () => { - register.registerMetric({ - async get() { - return { - name: 'test_metric', - type: 'gauge', - help: 'A test metric', - values: [ - { - value: NaN, - }, - ], - }; - }, - }); - const lines = (await register.metrics()).split('\n'); - expect(lines).toHaveLength(4); - expect(lines[2]).toEqual('test_metric Nan'); - }); + describe.each([ + ['Prometheus', Registry.PROMETHEUS_CONTENT_TYPE], + ['OpenMetrics', Registry.OPENMETRICS_CONTENT_TYPE], + ])('with %s type', (tag, regType) => { + const Counter = require('../index').Counter; + const Gauge = require('../index').Gauge; + const Histogram = require('../index').Histogram; + const Summary = require('../index').Summary; - it('should handle and output a metric with an +Infinity value', async () => { - register.registerMetric({ - async get() { - return { - name: 'test_metric', - type: 'gauge', - help: 'A test metric', - values: [ - { - value: Infinity, - }, - ], - }; - }, + beforeEach(() => { + register.setContentType(regType); + register.clear(); }); - const lines = (await register.metrics()).split('\n'); - expect(lines).toHaveLength(4); - expect(lines[2]).toEqual('test_metric +Inf'); - }); - it('should handle and output a metric with an -Infinity value', async () => { - register.registerMetric({ - async get() { - return { - name: 'test_metric', - type: 'gauge', - help: 'A test metric', - values: [ - { - value: -Infinity, - }, - ], - }; - }, - }); - const lines = (await register.metrics()).split('\n'); - expect(lines).toHaveLength(4); - expect(lines[2]).toEqual('test_metric -Inf'); - }); + describe('should output a counter metric', () => { + let output; + beforeEach(async () => { + register.registerMetric(getMetric()); + output = (await register.metrics()).split('\n'); + }); - it('should handle a metric without labels', async () => { - register.registerMetric({ - async get() { - return { - name: 'test_metric', - type: 'counter', - help: 'A test metric', - values: [ - { - value: 1, - }, - ], - }; - }, + it('with help as first item', () => { + expect(output[0]).toEqual('# HELP test_metric A test metric'); + }); + it('with type as second item', () => { + expect(output[1]).toEqual('# TYPE test_metric counter'); + }); + it('with first value of the metric as third item', () => { + if (register.contentType === Registry.OPENMETRICS_CONTENT_TYPE) { + expect(output[2]).toEqual( + 'test_metric_total{label="hello",code="303"} 12', + ); + } else { + expect(output[2]).toEqual('test_metric{label="hello",code="303"} 12'); + } + }); + it('with second value of the metric as fourth item', () => { + if (register.contentType === Registry.OPENMETRICS_CONTENT_TYPE) { + expect(output[3]).toEqual( + 'test_metric_total{label="bye",code="404"} 34', + ); + } else { + expect(output[3]).toEqual('test_metric{label="bye",code="404"} 34'); + } + }); }); - expect((await register.metrics()).split('\n')).toHaveLength(4); - }); - it('should handle a metric with default labels', async () => { - register.setDefaultLabels({ testLabel: 'testValue' }); - register.registerMetric({ - async get() { - return { - name: 'test_metric', - type: 'counter', - help: 'A test metric', - values: [{ value: 1 }], - }; - }, - }); + it('should throw on more than one metric', () => { + register.registerMetric(getMetric()); - const output = (await register.metrics()).split('\n')[2]; - expect(output).toEqual('test_metric{testLabel="testValue"} 1'); - }); + expect(() => { + register.registerMetric(getMetric()); + }).toThrowError( + 'A metric with the name test_metric has already been registered.', + ); + }); - it('labeled metrics should take precidence over defaulted', async () => { - register.setDefaultLabels({ testLabel: 'testValue' }); - register.registerMetric({ - async get() { - return { - name: 'test_metric', - type: 'counter', - help: 'A test metric', - values: [ - { - value: 1, - labels: { - testLabel: 'overlapped', - anotherLabel: 'value123', + it('should handle and output a metric with a NaN value', async () => { + register.registerMetric({ + async get() { + return { + name: 'test_metric', + type: 'gauge', + help: 'A test metric', + values: [ + { + value: NaN, }, - }, - ], - }; - }, + ], + }; + }, + }); + const lines = (await register.metrics()).split('\n'); + if (regType === Registry.OPENMETRICS_CONTENT_TYPE) { + expect(lines).toHaveLength(5); + } else { + expect(lines).toHaveLength(4); + } + expect(lines[2]).toEqual('test_metric Nan'); }); - expect((await register.metrics()).split('\n')[2]).toEqual( - 'test_metric{testLabel="overlapped",anotherLabel="value123"} 1', - ); - }); - - it('should output all initialized metrics at value 0', async () => { - new Counter({ name: 'counter', help: 'help' }); - new Gauge({ name: 'gauge', help: 'help' }); - new Histogram({ name: 'histogram', help: 'help' }); - new Summary({ name: 'summary', help: 'help' }); - - expect(await register.metrics()).toMatchSnapshot(); - }); - - it('should not output all initialized metrics at value 0 if labels', async () => { - new Counter({ name: 'counter', help: 'help', labelNames: ['label'] }); - new Gauge({ name: 'gauge', help: 'help', labelNames: ['label'] }); - new Histogram({ name: 'histogram', help: 'help', labelNames: ['label'] }); - new Summary({ name: 'summary', help: 'help', labelNames: ['label'] }); - - expect(await register.metrics()).toMatchSnapshot(); - }); - - describe('should escape', () => { - let escapedResult; - beforeEach(async () => { + it('should handle and output a metric with an +Infinity value', async () => { register.registerMetric({ async get() { return { - name: 'test_"_\\_\n_metric', - help: 'help_help', - type: 'counter', + name: 'test_metric', + type: 'gauge', + help: 'A test metric', + values: [ + { + value: Infinity, + }, + ], }; }, }); - escapedResult = await register.metrics(); - }); - it('backslash to \\\\', () => { - expect(escapedResult).toMatch(/\\\\/); - }); - it('newline to \\\\n', () => { - expect(escapedResult).toMatch(/\n/); + const lines = (await register.metrics()).split('\n'); + if (regType === Registry.OPENMETRICS_CONTENT_TYPE) { + expect(lines).toHaveLength(5); + } else { + expect(lines).toHaveLength(4); + } + expect(lines[2]).toEqual('test_metric +Inf'); }); - }); - it('should escape " in label values', async () => { - register.registerMetric({ - async get() { - return { - name: 'test_metric', - type: 'counter', - help: 'A test metric', - values: [ - { - value: 12, - labels: { - label: 'hello', - code: '3"03', + it('should handle and output a metric with an -Infinity value', async () => { + register.registerMetric({ + async get() { + return { + name: 'test_metric', + type: 'gauge', + help: 'A test metric', + values: [ + { + value: -Infinity, }, - }, - ], - }; - }, + ], + }; + }, + }); + const lines = (await register.metrics()).split('\n'); + if (regType === Registry.OPENMETRICS_CONTENT_TYPE) { + expect(lines).toHaveLength(5); + } else { + expect(lines).toHaveLength(4); + } + expect(lines[2]).toEqual('test_metric -Inf'); }); - const escapedResult = await register.metrics(); - expect(escapedResult).toMatch(/\\"/); - }); - - describe('should output metrics as JSON', () => { - it('should output metrics as JSON', async () => { - register.registerMetric(getMetric()); - const output = await register.getMetricsAsJSON(); - expect(output.length).toEqual(1); - expect(output[0].name).toEqual('test_metric'); - expect(output[0].type).toEqual('counter'); - expect(output[0].help).toEqual('A test metric'); - expect(output[0].values.length).toEqual(2); + it('should handle a metric without labels', async () => { + register.registerMetric({ + async get() { + return { + name: 'test_metric', + type: 'counter', + help: 'A test metric', + values: [ + { + value: 1, + }, + ], + }; + }, + }); + if (regType === Registry.OPENMETRICS_CONTENT_TYPE) { + expect((await register.metrics()).split('\n')).toHaveLength(5); + } else { + expect((await register.metrics()).split('\n')).toHaveLength(4); + } }); - it('should add default labels to JSON', async () => { - register.registerMetric(getMetric()); - register.setDefaultLabels({ - defaultRegistryLabel: 'testValue', + it('should handle a metric with default labels', async () => { + register.setDefaultLabels({ testLabel: 'testValue' }); + register.registerMetric({ + async get() { + return { + name: 'test_metric', + type: 'counter', + help: 'A test metric', + values: [{ value: 1 }], + }; + }, }); - const output = await register.getMetricsAsJSON(); - expect(output.length).toEqual(1); - expect(output[0].name).toEqual('test_metric'); - expect(output[0].type).toEqual('counter'); - expect(output[0].help).toEqual('A test metric'); - expect(output[0].values.length).toEqual(2); - expect(output[0].values[0].labels).toEqual({ - code: '303', - label: 'hello', - defaultRegistryLabel: 'testValue', - }); + const output = (await register.metrics()).split('\n')[2]; + if (regType === Registry.OPENMETRICS_CONTENT_TYPE) { + expect(output).toEqual('test_metric_total{testLabel="testValue"} 1'); + } else { + expect(output).toEqual('test_metric{testLabel="testValue"} 1'); + } }); - }); - - it('should allow removing single metrics', async () => { - register.registerMetric(getMetric()); - register.registerMetric(getMetric('some other name')); - - let output = await register.getMetricsAsJSON(); - expect(output.length).toEqual(2); - - register.removeSingleMetric('test_metric'); - output = await register.getMetricsAsJSON(); + it('labeled metrics should take precidence over defaulted', async () => { + register.setDefaultLabels({ testLabel: 'testValue' }); + register.registerMetric({ + async get() { + return { + name: 'test_metric', + type: 'counter', + help: 'A test metric', + values: [ + { + value: 1, + labels: { + testLabel: 'overlapped', + anotherLabel: 'value123', + }, + }, + ], + }; + }, + }); - expect(output.length).toEqual(1); - expect(output[0].name).toEqual('some other name'); - }); + if (regType === Registry.OPENMETRICS_CONTENT_TYPE) { + expect((await register.metrics()).split('\n')[2]).toEqual( + 'test_metric_total{testLabel="overlapped",anotherLabel="value123"} 1', + ); + } else { + expect((await register.metrics()).split('\n')[2]).toEqual( + 'test_metric{testLabel="overlapped",anotherLabel="value123"} 1', + ); + } + }); - it('should allow getting single metrics', () => { - const metric = getMetric(); - register.registerMetric(metric); + it('should output all initialized metrics at value 0', async () => { + new Counter({ name: 'counter', help: 'help' }); + new Gauge({ name: 'gauge', help: 'help' }); + new Histogram({ name: 'histogram', help: 'help' }); + new Summary({ name: 'summary', help: 'help' }); - const output = register.getSingleMetric('test_metric'); - expect(output).toEqual(metric); - }); + expect(await register.metrics()).toMatchSnapshot(); + }); - it('should allow gettings metrics', async () => { - const metric = getMetric(); - register.registerMetric(metric); - const metrics = await register.metrics(); + it('should not output all initialized metrics at value 0 if labels', async () => { + new Counter({ name: 'counter', help: 'help', labelNames: ['label'] }); + new Gauge({ name: 'gauge', help: 'help', labelNames: ['label'] }); + new Histogram({ name: 'histogram', help: 'help', labelNames: ['label'] }); + new Summary({ name: 'summary', help: 'help', labelNames: ['label'] }); - expect(metrics.split('\n')[3]).toEqual( - 'test_metric{label="bye",code="404"} 34', - ); - }); + expect(await register.metrics()).toMatchSnapshot(); + }); - describe('resetting', () => { - it('should allow resetting all metrics', async () => { - const counter = new Counter({ - name: 'test_counter', - help: 'test metric', - labelNames: ['serial', 'active'], - }); - const gauge = new Gauge({ - name: 'test_gauge', - help: 'Another test metric', - labelNames: ['level'], + describe('should escape', () => { + let escapedResult; + beforeEach(async () => { + register.registerMetric({ + async get() { + return { + name: 'test_"_\\_\n_metric', + help: 'help_help', + type: 'counter', + }; + }, + }); + escapedResult = await register.metrics(); }); - const histo = new Histogram({ - name: 'test_histo', - help: 'test', + it('backslash to \\\\', () => { + expect(escapedResult).toMatch(/\\\\/); }); - const summ = new Summary({ - name: 'test_summ', - help: 'test', - percentiles: [0.5], + it('newline to \\\\n', () => { + expect(escapedResult).toMatch(/\n/); }); - register.registerMetric(counter); - register.registerMetric(gauge); - register.registerMetric(histo); - register.registerMetric(summ); - - counter.inc({ serial: '12345', active: 'yes' }, 12); - gauge.set({ level: 'low' }, -12); - histo.observe(1); - summ.observe(100); - - register.resetMetrics(); - - const same_counter = register.getSingleMetric('test_counter'); - expect((await same_counter.get()).values).toEqual([]); - - const same_gauge = register.getSingleMetric('test_gauge'); - expect((await same_gauge.get()).values).toEqual([]); - - const same_histo = register.getSingleMetric('test_histo'); - expect((await same_histo.get()).values).toEqual([]); - - const same_summ = register.getSingleMetric('test_summ'); - expect((await same_summ.get()).values[0].value).toEqual(0); }); - }); - - describe('Registry with default labels', () => { - const Registry = require('../lib/registry'); - - describe('mutation tests', () => { - describe('registry.metrics()', () => { - it('should not throw with default labels (counter)', async () => { - const r = new Registry(); - r.setDefaultLabels({ - env: 'development', - }); - const counter = new Counter({ - name: 'my_counter', - help: 'my counter', - registers: [r], - labelNames: ['type'], - }); + it('should escape " in label values', async () => { + register.registerMetric({ + async get() { + return { + name: 'test_metric', + type: 'counter', + help: 'A test metric', + values: [ + { + value: 12, + labels: { + label: 'hello', + code: '3"03', + }, + }, + ], + }; + }, + }); + const escapedResult = await register.metrics(); + expect(escapedResult).toMatch(/\\"/); + }); - const myCounter = counter.labels('myType'); + describe('should output metrics as JSON', () => { + it('should output metrics as JSON', async () => { + register.registerMetric(getMetric()); + const output = await register.getMetricsAsJSON(); - myCounter.inc(); + expect(output.length).toEqual(1); + expect(output[0].name).toEqual('test_metric'); + expect(output[0].type).toEqual('counter'); + expect(output[0].help).toEqual('A test metric'); + expect(output[0].values.length).toEqual(2); + }); - const metrics = await r.metrics(); - const lines = metrics.split('\n'); - expect(lines).toContain( - 'my_counter{type="myType",env="development"} 1', - ); + it('should add default labels to JSON', async () => { + register.registerMetric(getMetric()); + register.setDefaultLabels({ + defaultRegistryLabel: 'testValue', + }); + const output = await register.getMetricsAsJSON(); + + expect(output.length).toEqual(1); + expect(output[0].name).toEqual('test_metric'); + expect(output[0].type).toEqual('counter'); + expect(output[0].help).toEqual('A test metric'); + expect(output[0].values.length).toEqual(2); + expect(output[0].values[0].labels).toEqual({ + code: '303', + label: 'hello', + defaultRegistryLabel: 'testValue', + }); + }); + }); - myCounter.inc(); + it('should allow removing single metrics', async () => { + register.registerMetric(getMetric()); + register.registerMetric(getMetric('some other name')); - const metrics2 = await r.metrics(); - const lines2 = metrics2.split('\n'); - expect(lines2).toContain( - 'my_counter{type="myType",env="development"} 2', - ); - }); + let output = await register.getMetricsAsJSON(); + expect(output.length).toEqual(2); - it('should not throw with default labels (gauge)', async () => { - const r = new Registry(); - r.setDefaultLabels({ - env: 'development', - }); + register.removeSingleMetric('test_metric'); - const gauge = new Gauge({ - name: 'my_gauge', - help: 'my gauge', - registers: [r], - labelNames: ['type'], - }); + output = await register.getMetricsAsJSON(); - const myGauge = gauge.labels('myType'); + expect(output.length).toEqual(1); + expect(output[0].name).toEqual('some other name'); + }); - myGauge.inc(1); + it('should allow getting single metrics', () => { + const metric = getMetric(); + register.registerMetric(metric); - const metrics = await r.metrics(); - const lines = metrics.split('\n'); - expect(lines).toContain( - 'my_gauge{type="myType",env="development"} 1', - ); + const output = register.getSingleMetric('test_metric'); + expect(output).toEqual(metric); + }); - myGauge.inc(2); + it('should allow getting metrics', async () => { + const metric = getMetric(); + register.registerMetric(metric); + const metrics = await register.metrics(); + + if (regType === Registry.OPENMETRICS_CONTENT_TYPE) { + expect(metrics.split('\n')[3]).toEqual( + 'test_metric_total{label="bye",code="404"} 34', + ); + } else { + expect(metrics.split('\n')[3]).toEqual( + 'test_metric{label="bye",code="404"} 34', + ); + } + }); - const metrics2 = await r.metrics(); - const lines2 = metrics2.split('\n'); - expect(lines2).toContain( - 'my_gauge{type="myType",env="development"} 3', - ); + describe('resetting', () => { + it('should allow resetting all metrics', async () => { + const counter = new Counter({ + name: 'test_counter', + help: 'test metric', + labelNames: ['serial', 'active'], }); + const gauge = new Gauge({ + name: 'test_gauge', + help: 'Another test metric', + labelNames: ['level'], + }); + const histo = new Histogram({ + name: 'test_histo', + help: 'test', + }); + const summ = new Summary({ + name: 'test_summ', + help: 'test', + percentiles: [0.5], + }); + register.registerMetric(counter); + register.registerMetric(gauge); + register.registerMetric(histo); + register.registerMetric(summ); - it('should not throw with default labels (histogram)', async () => { - const r = new Registry(); - r.setDefaultLabels({ - env: 'development', - }); - - const hist = new Histogram({ - name: 'my_histogram', - help: 'my histogram', - registers: [r], - labelNames: ['type'], - }); + counter.inc({ serial: '12345', active: 'yes' }, 12); + gauge.set({ level: 'low' }, -12); + histo.observe(1); + summ.observe(100); - const myHist = hist.labels('myType'); + register.resetMetrics(); - myHist.observe(1); + const same_counter = register.getSingleMetric('test_counter'); + expect((await same_counter.get()).values).toEqual([]); - const metrics = await r.metrics(); - const lines = metrics.split('\n'); - expect(lines).toContain( - 'my_histogram_bucket{le="1",type="myType",env="development"} 1', - ); + const same_gauge = register.getSingleMetric('test_gauge'); + expect((await same_gauge.get()).values).toEqual([]); - myHist.observe(1); + const same_histo = register.getSingleMetric('test_histo'); + expect((await same_histo.get()).values).toEqual([]); - const metrics2 = await r.metrics(); - const lines2 = metrics2.split('\n'); - expect(lines2).toContain( - 'my_histogram_bucket{le="1",type="myType",env="development"} 2', - ); - }); + const same_summ = register.getSingleMetric('test_summ'); + expect((await same_summ.get()).values[0].value).toEqual(0); }); + }); - describe('registry.getMetricsAsJSON()', () => { - it('should not throw with default labels (counter)', async () => { - const r = new Registry(); - r.setDefaultLabels({ - env: 'development', + describe('Registry with default labels', () => { + const Registry = require('../lib/registry'); + + describe('mutation tests', () => { + describe('registry.metrics()', () => { + it('should not throw with default labels (counter)', async () => { + const r = new Registry(regType); + r.setDefaultLabels({ + env: 'development', + }); + + const counter = new Counter({ + name: 'my_counter', + help: 'my counter', + registers: [r], + labelNames: ['type'], + }); + + const myCounter = counter.labels('myType'); + + myCounter.inc(); + + const metrics = await r.metrics(); + const lines = metrics.split('\n'); + if (regType === Registry.OPENMETRICS_CONTENT_TYPE) { + expect(lines).toContain( + 'my_counter_total{type="myType",env="development"} 1', + ); + } else { + expect(lines).toContain( + 'my_counter{type="myType",env="development"} 1', + ); + } + + myCounter.inc(); + + const metrics2 = await r.metrics(); + const lines2 = metrics2.split('\n'); + if (regType === Registry.OPENMETRICS_CONTENT_TYPE) { + expect(lines2).toContain( + 'my_counter_total{type="myType",env="development"} 2', + ); + } else { + expect(lines2).toContain( + 'my_counter{type="myType",env="development"} 2', + ); + } }); - const counter = new Counter({ - name: 'my_counter', - help: 'my counter', - registers: [r], - labelNames: ['type'], - }); + it('should not throw with default labels (gauge)', async () => { + const r = new Registry(regType); + r.setDefaultLabels({ + env: 'development', + }); - const myCounter = counter.labels('myType'); + const gauge = new Gauge({ + name: 'my_gauge', + help: 'my gauge', + registers: [r], + labelNames: ['type'], + }); - myCounter.inc(); + const myGauge = gauge.labels('myType'); - const metrics = await r.getMetricsAsJSON(); - expect(metrics).toContainEqual({ - aggregator: 'sum', - help: 'my counter', - name: 'my_counter', - type: 'counter', - values: [ - { - labels: { env: 'development', type: 'myType' }, - value: 1, - }, - ], - }); + myGauge.inc(1); - myCounter.inc(); + const metrics = await r.metrics(); + const lines = metrics.split('\n'); + expect(lines).toContain( + 'my_gauge{type="myType",env="development"} 1', + ); - const metrics2 = await r.getMetricsAsJSON(); - expect(metrics2).toContainEqual({ - aggregator: 'sum', - help: 'my counter', - name: 'my_counter', - type: 'counter', - values: [ - { - labels: { env: 'development', type: 'myType' }, - value: 2, - }, - ], - }); - }); + myGauge.inc(2); - it('should not throw with default labels (gauge)', async () => { - const r = new Registry(); - r.setDefaultLabels({ - env: 'development', + const metrics2 = await r.metrics(); + const lines2 = metrics2.split('\n'); + expect(lines2).toContain( + 'my_gauge{type="myType",env="development"} 3', + ); }); - const gauge = new Gauge({ - name: 'my_gauge', - help: 'my gauge', - registers: [r], - labelNames: ['type'], - }); + it('should not throw with default labels (histogram)', async () => { + const r = new Registry(regType); + r.setDefaultLabels({ + env: 'development', + }); - const myGauge = gauge.labels('myType'); + const hist = new Histogram({ + name: 'my_histogram', + help: 'my histogram', + registers: [r], + labelNames: ['type'], + }); - myGauge.inc(1); + const myHist = hist.labels('myType'); - const metrics = await r.getMetricsAsJSON(); - expect(metrics).toContainEqual({ - aggregator: 'sum', - help: 'my gauge', - name: 'my_gauge', - type: 'gauge', - values: [ - { - labels: { env: 'development', type: 'myType' }, - value: 1, - }, - ], - }); + myHist.observe(1); - myGauge.inc(2); + const metrics = await r.metrics(); + const lines = metrics.split('\n'); + expect(lines).toContain( + 'my_histogram_bucket{le="1",type="myType",env="development"} 1', + ); - const metrics2 = await r.getMetricsAsJSON(); - expect(metrics2).toContainEqual({ - aggregator: 'sum', - help: 'my gauge', - name: 'my_gauge', - type: 'gauge', - values: [ - { - labels: { env: 'development', type: 'myType' }, - value: 3, - }, - ], + myHist.observe(1); + + const metrics2 = await r.metrics(); + const lines2 = metrics2.split('\n'); + expect(lines2).toContain( + 'my_histogram_bucket{le="1",type="myType",env="development"} 2', + ); }); }); - it('should not throw with default labels (histogram)', async () => { - const r = new Registry(); - r.setDefaultLabels({ - env: 'development', + describe('registry.getMetricsAsJSON()', () => { + it('should not throw with default labels (counter)', async () => { + const r = new Registry(regType); + r.setDefaultLabels({ + env: 'development', + }); + + const counter = new Counter({ + name: 'my_counter', + help: 'my counter', + registers: [r], + labelNames: ['type'], + }); + + const myCounter = counter.labels('myType'); + + myCounter.inc(); + + const metrics = await r.getMetricsAsJSON(); + expect(metrics).toContainEqual({ + aggregator: 'sum', + help: 'my counter', + name: 'my_counter', + type: 'counter', + values: [ + { + labels: { env: 'development', type: 'myType' }, + value: 1, + }, + ], + }); + + myCounter.inc(); + + const metrics2 = await r.getMetricsAsJSON(); + expect(metrics2).toContainEqual({ + aggregator: 'sum', + help: 'my counter', + name: 'my_counter', + type: 'counter', + values: [ + { + labels: { env: 'development', type: 'myType' }, + value: 2, + }, + ], + }); }); - const hist = new Histogram({ - name: 'my_histogram', - help: 'my histogram', - registers: [r], - labelNames: ['type'], + it('should not throw with default labels (gauge)', async () => { + const r = new Registry(regType); + r.setDefaultLabels({ + env: 'development', + }); + + const gauge = new Gauge({ + name: 'my_gauge', + help: 'my gauge', + registers: [r], + labelNames: ['type'], + }); + + const myGauge = gauge.labels('myType'); + + myGauge.inc(1); + + const metrics = await r.getMetricsAsJSON(); + expect(metrics).toContainEqual({ + aggregator: 'sum', + help: 'my gauge', + name: 'my_gauge', + type: 'gauge', + values: [ + { + labels: { env: 'development', type: 'myType' }, + value: 1, + }, + ], + }); + + myGauge.inc(2); + + const metrics2 = await r.getMetricsAsJSON(); + expect(metrics2).toContainEqual({ + aggregator: 'sum', + help: 'my gauge', + name: 'my_gauge', + type: 'gauge', + values: [ + { + labels: { env: 'development', type: 'myType' }, + value: 3, + }, + ], + }); }); - const myHist = hist.labels('myType'); - - myHist.observe(1); - - const metrics = await r.getMetricsAsJSON(); - // NOTE: at this test we don't need to check exacte JSON schema - expect(metrics[0].values).toContainEqual({ - labels: { env: 'development', le: 1, type: 'myType' }, - metricName: 'my_histogram_bucket', - value: 1, - }); - - myHist.observe(1); - - const metrics2 = await r.getMetricsAsJSON(); - // NOTE: at this test we don't need to check exacte JSON schema - expect(metrics2[0].values).toContainEqual({ - labels: { env: 'development', le: 1, type: 'myType' }, - metricName: 'my_histogram_bucket', - value: 2, + it('should not throw with default labels (histogram)', async () => { + const r = new Registry(regType); + r.setDefaultLabels({ + env: 'development', + }); + + const hist = new Histogram({ + name: 'my_histogram', + help: 'my histogram', + registers: [r], + labelNames: ['type'], + }); + + const myHist = hist.labels('myType'); + + myHist.observe(1); + + const metrics = await r.getMetricsAsJSON(); + // NOTE: at this test we don't need to check exact JSON schema + expect(metrics[0].values).toContainEqual({ + exemplar: null, + labels: { env: 'development', le: 1, type: 'myType' }, + metricName: 'my_histogram_bucket', + value: 1, + }); + + myHist.observe(1); + + const metrics2 = await r.getMetricsAsJSON(); + // NOTE: at this test we don't need to check exact JSON schema + expect(metrics2[0].values).toContainEqual({ + exemplar: null, + labels: { env: 'development', le: 1, type: 'myType' }, + metricName: 'my_histogram_bucket', + value: 2, + }); }); }); }); }); - }); - describe('merging', () => { - const Registry = require('../lib/registry'); - let registryOne; - let registryTwo; + describe('merging', () => { + const Registry = require('../lib/registry'); + let registryOne; + let registryTwo; - beforeEach(() => { - registryOne = new Registry(); - registryTwo = new Registry(); - }); + beforeEach(() => { + registryOne = new Registry(regType); + registryTwo = new Registry(regType); + }); - it('should merge all provided registers', async () => { - registryOne.registerMetric(getMetric('one')); - registryTwo.registerMetric(getMetric('two')); + it('should merge all provided registers', async () => { + registryOne.registerMetric(getMetric('one')); + registryTwo.registerMetric(getMetric('two')); - const merged = await Registry.merge([ - registryOne, - registryTwo, - ]).getMetricsAsJSON(); - expect(merged).toHaveLength(2); - }); + const merged = await Registry.merge([ + registryOne, + registryTwo, + ]).getMetricsAsJSON(); + expect(merged).toHaveLength(2); + }); - it('should throw if same name exists on both registers', () => { - registryOne.registerMetric(getMetric()); - registryTwo.registerMetric(getMetric()); + it('should throw if same name exists on both registers', () => { + registryOne.registerMetric(getMetric()); + registryTwo.registerMetric(getMetric()); - const fn = function () { - Registry.merge([registryOne, registryTwo]); - }; + const fn = function () { + Registry.merge([registryOne, registryTwo]); + }; - expect(fn).toThrowError(Error); - }); - }); + expect(fn).toThrowError(Error); + }); - it('should have the same contentType as the module', () => { - const moduleWideContentType = require('../').contentType; - expect(register.contentType).toEqual(moduleWideContentType); - }); + it('should throw if merging different types of registers', () => { + registryOne.setContentType(Registry.PROMETHEUS_CONTENT_TYPE); + registryTwo.setContentType(Registry.OPENMETRICS_CONTENT_TYPE); - function getMetric(name) { - name = name || 'test_metric'; - return { - name, - async get() { - return { - name, - type: 'counter', - help: 'A test metric', - values: [ - { - value: 12, - labels: { - label: 'hello', - code: '303', + const fn = function () { + Registry.merge([registryOne, registryTwo]); + }; + + expect(fn).toThrowError( + 'Registers can only be merged if they have the same content type', + ); + }); + }); + + function getMetric(name) { + name = name || 'test_metric'; + return { + name, + async get() { + return { + name, + type: 'counter', + help: 'A test metric', + values: [ + { + value: 12, + labels: { + label: 'hello', + code: '303', + }, }, - }, - { - value: 34, - labels: { - label: 'bye', - code: '404', + { + value: 34, + labels: { + label: 'bye', + code: '404', + }, }, - }, - ], - }; - }, - }; - } + ], + }; + }, + }; + } + }); }); diff --git a/test/summaryTest.js b/test/summaryTest.js index 10ad4b0a..79c56462 100644 --- a/test/summaryTest.js +++ b/test/summaryTest.js @@ -1,11 +1,23 @@ 'use strict'; -describe('summary', () => { +const Registry = require('../index').Registry; + +describe.each([ + ['Prometheus', Registry.PROMETHEUS_CONTENT_TYPE], + ['OpenMetrics', Registry.OPENMETRICS_CONTENT_TYPE], +])('summary with %s registry', (tag, regType) => { const Summary = require('../index').Summary; - const Registry = require('../index').Registry; const globalRegistry = require('../index').register; let instance; + beforeEach(() => { + globalRegistry.setContentType(regType); + }); + + afterEach(() => { + globalRegistry.clear(); + }); + describe('global registry', () => { afterEach(() => { globalRegistry.clear(); @@ -464,7 +476,7 @@ describe('summary', () => { describe('registry instance', () => { let registryInstance; beforeEach(() => { - registryInstance = new Registry(); + registryInstance = new Registry(regType); instance = new Summary({ name: 'summary_test', help: 'test',