diff --git a/.eslintrc b/.eslintrc index 76804890..0d5911c1 100644 --- a/.eslintrc +++ b/.eslintrc @@ -2,7 +2,8 @@ "plugins": ["prettier"], "extends": "eslint:recommended", "env": { - "node": true + "node": true, + "es6": true }, "parserOptions": { "ecmaVersion": 2015 @@ -55,6 +56,12 @@ "no-shadow": "off", "no-unused-expressions": "off" } + }, + { + "files": ["example/**/*.js"], + "rules": { + "no-console": "off" + } } ] } diff --git a/README.md b/README.md index 83dfa831..c04643d8 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,17 @@ A prometheus client for node.js that supports histogram, summaries, gauges and c ### Usage -See example folder for a sample usage. The library does not bundle any web framework, to expose the metrics just return the metrics() function in the registry. +See example folder for a sample usage. The library does not bundle any web framework, to expose the metrics just return the `metrics()` function in the registry. + +#### Usage with Node.js's `cluster` module + +Node.js's `cluster` module spawns multiple processes and hands off socket connections to those workers. Returning metrics from a worker's local registry will only reveal that individual worker's metrics, which is generally undesirable. To solve this, you can aggregate all of the workers' metrics in the master process. See `example/cluster.js` for an example. + +Default metrics use sensible aggregation methods. Custom metrics are summed across workers by default. To use a different aggregation method, set the `aggregator` property in the metric config to one of 'sum', 'first', 'min', 'max', 'average' or 'omit'. (See `lib/metrics/version.js` for an example.) + +If you need to expose metrics about an individual worker, you can include a value that is unique to the worker (such as the worker ID or process ID) in a label. (See `example/server.js` for an example using `worker_${cluster.worker.id}` as a label value.) + +Note: You must use the default global registry in cluster mode. ### API @@ -248,7 +258,7 @@ You can prevent this by setting last parameter when creating the metric to `fals Using non-global registries requires creating Registry instance and adding it inside `registers` inside the configuration object. Alternatively you can pass an empty `registers` array and register it manually. -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. +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. ```js const client = require('prom-client'); @@ -280,6 +290,22 @@ If you need to get a reference to a previously registered metric, you can use `r You can remove all metrics by calling `register.clear()`. You can also remove a single metric by calling `register.removeSingleMetric(*name of metric*)`. +##### Cluster metrics + +You can get aggregated metrics for all workers in a node.js cluster with `register.clusterMetrics()`. This method both returns a promise and accepts a callback, both of which resolve with a metrics string suitable for Prometheus to consume. + +```js +register.clusterMetrics() + .then(metrics => { /* ... */ }) + .catch(err => { /* ... */ }); + +// - or - + +register.clusterMetrics((err, metrics) => { + // ... +}); +``` + #### Pushgateway It is possible to push metrics via a [Pushgateway](https://github.com/prometheus/pushgateway). diff --git a/example/cluster.js b/example/cluster.js new file mode 100644 index 00000000..99cc0981 --- /dev/null +++ b/example/cluster.js @@ -0,0 +1,28 @@ +'use strict'; + +const cluster = require('cluster'); +const express = require('express'); +const metricsServer = express(); +const AggregatorRegistry = require('../').AggregatorRegistry; +const aggregatorRegistry = new AggregatorRegistry(); + +if (cluster.isMaster) { + for (let i = 0; i < 4; i++) { + cluster.fork(); + } + + metricsServer.get('/cluster_metrics', (req, res) => { + aggregatorRegistry.clusterMetrics((err, metrics) => { + if (err) console.log(err); + res.set('Content-Type', aggregatorRegistry.contentType); + res.send(metrics); + }); + }); + + metricsServer.listen(3001); + console.log( + 'Cluster metrics server listening to 3001, metrics exposed on /cluster_metrics' + ); +} else { + require('./server.js'); +} diff --git a/example/server.js b/example/server.js index 65e9aac3..890be2a1 100644 --- a/example/server.js +++ b/example/server.js @@ -1,6 +1,7 @@ 'use strict'; const express = require('express'); +const cluster = require('cluster'); const server = express(); const register = require('../').register; @@ -48,6 +49,13 @@ setInterval(() => { g.labels('post', '300').inc(); }, 100); +if (cluster.isWorker) { + // Expose some worker-specific metric as an example + setInterval(() => { + c.inc({ code: `worker_${cluster.worker.id}` }); + }, 2000); +} + server.get('/metrics', (req, res) => { res.set('Content-Type', register.contentType); res.end(register.metrics()); @@ -61,6 +69,5 @@ server.get('/metrics/counter', (req, res) => { //Enable collection of default metrics require('../').collectDefaultMetrics(); -//eslint-disable-next-line no-console console.log('Server listening to 3000, metrics exposed on /metrics endpoint'); server.listen(3000); diff --git a/index.d.ts b/index.d.ts index 15b1fd33..93d4afc6 100644 --- a/index.d.ts +++ b/index.d.ts @@ -73,11 +73,40 @@ export class Registry { */ export const register: Registry; +export class AggregatorRegistry extends Registry { + /** + * Gets aggregated metrics for all workers. The optional callback and + * returned Promise resolve with the same value; either may be used. + * @param {Function} callback? (err, metrics) => any + * @return {Promise} Promise that resolves with the aggregated + * metrics. + */ + clusterMetrics( + cb: (err: Error | null, metrics?: string) => any + ): Promise; + + /** + * Creates a new Registry instance from an array of metrics that were + * created by `registry.getMetricsAsJSON()`. Metrics are aggregated using + * the method specified by their `aggregator` property, or by summation if + * `aggregator` is undefined. + * @param {Array} metricsArr Array of metrics, each of which created by + * `registry.getMetricsAsJSON()`. + * @return {Registry} aggregated registry. + */ + static aggregate(metricsArr: Array): Registry; +} + /** * General metric type */ export type Metric = Counter | Gauge | Summary | Histogram; +/** + * Aggregation methods, used for aggregating metrics in a Node.js cluster. + */ +export type Aggregator = 'omit' | 'sum' | 'first' | 'min' | 'max' | 'average'; + export enum MetricType { Counter, Gauge, @@ -89,6 +118,7 @@ interface metric { name: string; help: string; type: MetricType; + aggregator: Aggregator; } interface labelValues { @@ -100,6 +130,7 @@ export interface CounterConfiguration { help: string; labelNames?: string[]; registers?: Registry[]; + aggregator?: Aggregator; } /** @@ -158,6 +189,7 @@ export interface GaugeConfiguration { help: string; labelNames?: string[]; registers?: Registry[]; + aggregator?: Aggregator; } /** @@ -285,6 +317,7 @@ export interface HistogramConfiguration { labelNames?: string[]; buckets?: number[]; registers?: Registry[]; + aggregator?: Aggregator; } /** @@ -378,6 +411,7 @@ export interface SummaryConfiguration { labelNames?: string[]; percentiles?: number[]; registers?: Registry[]; + aggregator?: Aggregator; } /** diff --git a/index.js b/index.js index d1a1326b..a24ae52a 100644 --- a/index.js +++ b/index.js @@ -19,3 +19,6 @@ exports.linearBuckets = require('./lib/bucketGenerators').linearBuckets; exports.exponentialBuckets = require('./lib/bucketGenerators').exponentialBuckets; exports.collectDefaultMetrics = require('./lib/defaultMetrics'); + +exports.aggregators = require('./lib/metricAggregators').aggregators; +exports.AggregatorRegistry = require('./lib/cluster'); diff --git a/lib/cluster.js b/lib/cluster.js new file mode 100644 index 00000000..d10b4cfd --- /dev/null +++ b/lib/cluster.js @@ -0,0 +1,146 @@ +'use strict'; + +/** + * Extends the Registry class with a `clusterMetrics` method that returns + * aggregated metrics for all workers. + * + * In cluster workers, listens for and responds to requests for metrics by the + * cluster master. + */ + +const cluster = require('cluster'); +const Registry = require('./registry'); +const util = require('./util'); +const aggregators = require('./metricAggregators').aggregators; + +const GET_METRICS_REQ = 'prom-client:getMetricsReq'; +const GET_METRICS_RES = 'prom-client:getMetricsRes'; + +let requestCtr = 0; // Concurrency control +const requests = new Map(); // Pending requests for workers' local metrics. + +class AggregatorRegistry extends Registry { + /** + * Gets aggregated metrics for all workers. The optional callback and + * returned Promise resolve with the same value; either may be used. + * @param {Function} callback? (err, metrics) => any + * @return {Promise} Promise that resolves with the aggregated + * metrics. + */ + clusterMetrics(callback) { + const requestId = requestCtr++; + + callback = callback || function() {}; + + return new Promise((resolve, reject) => { + const request = { + responses: [], + pending: Object.keys(cluster.workers).length, + callback, + resolve, + reject, + errorTimeout: setTimeout(() => { + request.failed = true; + const err = new Error('Operation timed out.'); + request.callback(err); + reject(err); + }, 5000), + failed: false + }; + requests.set(requestId, request); + + const message = { + type: GET_METRICS_REQ, + requestId + }; + for (const id in cluster.workers) cluster.workers[id].send(message); + }); + } + + /** + * Creates a new Registry instance from an array of metrics that were + * created by `registry.getMetricsAsJSON()`. Metrics are aggregated using + * the method specified by their `aggregator` property, or by summation if + * `aggregator` is undefined. + * @param {Array} metricsArr Array of metrics, each of which created by + * `registry.getMetricsAsJSON()`. + * @return {Registry} aggregated registry. + */ + static aggregate(metricsArr) { + const aggregatedRegistry = new Registry(); + const metricsByName = new util.Grouper(); + + // Gather by name + metricsArr.forEach(metrics => { + metrics.forEach(metric => { + metricsByName.add(metric.name, metric); + }); + }); + + // Aggregate gathered metrics. Default to summation. + metricsByName.forEach(metrics => { + const aggregatorName = metrics[0].aggregator || 'sum'; + const aggregatorFn = aggregators[aggregatorName]; + if (typeof aggregatorFn !== 'function') { + throw new Error(`'${aggregatorName}' is not a defined aggregator.`); + } + const aggregatedMetric = aggregatorFn(metrics); + // NB: The 'omit' aggregator returns undefined. + if (aggregatedMetric) { + const aggregatedMetricWrapper = Object.assign( + { + get: () => aggregatedMetric + }, + aggregatedMetric + ); + aggregatedRegistry.registerMetric(aggregatedMetricWrapper); + } + }); + + return aggregatedRegistry; + } +} + +if (cluster.isMaster) { + // Listen for worker responses to requests for local metrics + cluster.on('message', (worker, message) => { + if (arguments.length === 2) { + // pre-Node.js v6.0 + message = worker; + worker = undefined; + } + + if (message.type === GET_METRICS_RES) { + const request = requests.get(message.requestId); + request.responses.push(message.metrics); + request.pending--; + + if (request.pending === 0) { + // finalize + requests.delete(message.requestId); + clearTimeout(request.errorTimeout); + + if (request.failed) return; // Callback already run with Error. + + const registry = AggregatorRegistry.aggregate(request.responses); + const promString = registry.metrics(); + request.callback(null, promString); + request.resolve(promString); + } + } + }); +} else if (cluster.isWorker) { + // Respond to master's requests for worker's local metrics. + process.on('message', message => { + if (message.type === GET_METRICS_REQ) { + process.send({ + type: GET_METRICS_RES, + requestId: message.requestId, + // TODO see if we can support the non-global registry also. + metrics: Registry.globalRegistry.getMetricsAsJSON() + }); + } + }); +} + +module.exports = AggregatorRegistry; diff --git a/lib/counter.js b/lib/counter.js index 2f95c664..f65c1298 100644 --- a/lib/counter.js +++ b/lib/counter.js @@ -73,6 +73,7 @@ class Counter { } this.help = config.help; + this.aggregator = config.aggregator || 'sum'; config.registers.forEach(registryInstance => registryInstance.registerMetric(this) @@ -100,7 +101,8 @@ class Counter { help: this.help, name: this.name, type, - values: getProperties(this.hashMap) + values: getProperties(this.hashMap), + aggregator: this.aggregator }; } diff --git a/lib/gauge.js b/lib/gauge.js index 52078263..8d2129e7 100644 --- a/lib/gauge.js +++ b/lib/gauge.js @@ -69,6 +69,7 @@ class Gauge { this.hashMap = createValue({}, 0, {}); } this.help = config.help; + this.aggregator = config.aggregator || 'sum'; config.registers.forEach(registryInstance => registryInstance.registerMetric(this) @@ -90,24 +91,23 @@ class Gauge { } /** - * Increment a gauge value - * @param {object} labels - Object with labels where key is the label key and value is label value. Can only be one level deep - * @param {Number} value - Value to increment - if omitted, increment with 1 - * @param {(Number|Date)} timestamp - Timestamp to set the gauge to - * @returns {void} - */ + * Increment a gauge value + * @param {object} labels - Object with labels where key is the label key and value is label value. Can only be one level deep + * @param {Number} value - Value to increment - if omitted, increment with 1 + * @param {(Number|Date)} timestamp - Timestamp to set the gauge to + * @returns {void} + */ inc(labels, value, timestamp) { inc.call(this, labels)(value, timestamp); } /** - - * Decrement a gauge value - * @param {object} labels - Object with labels where key is the label key and value is label value. Can only be one level deep - * @param {Number} value - Value to decrement - if omitted, decrement with 1 - * @param {(Number|Date)} timestamp - Timestamp to set the gauge to - * @returns {void} - */ + * Decrement a gauge value + * @param {object} labels - Object with labels where key is the label key and value is label value. Can only be one level deep + * @param {Number} value - Value to decrement - if omitted, decrement with 1 + * @param {(Number|Date)} timestamp - Timestamp to set the gauge to + * @returns {void} + */ dec(labels, value, timestamp) { dec.call(this, labels)(value, timestamp); } @@ -140,7 +140,8 @@ class Gauge { help: this.help, name: this.name, type, - values: getProperties(this.hashMap) + values: getProperties(this.hashMap), + aggregator: this.aggregator }; } diff --git a/lib/histogram.js b/lib/histogram.js index a393d278..bd11146f 100644 --- a/lib/histogram.js +++ b/lib/histogram.js @@ -64,6 +64,7 @@ class Histogram { this.name = config.name; this.help = config.help; + this.aggregate = config.aggregator || 'sum'; this.upperBounds = config.buckets; this.bucketValues = this.upperBounds.reduce((acc, upperBound) => { @@ -113,7 +114,8 @@ class Histogram { name: this.name, help: this.help, type, - values + values, + aggregator: this.aggregator }; } diff --git a/lib/metricAggregators.js b/lib/metricAggregators.js new file mode 100644 index 00000000..3ed406c0 --- /dev/null +++ b/lib/metricAggregators.js @@ -0,0 +1,80 @@ +'use strict'; + +const util = require('./util'); + +/** + * Returns a new function that applies the `aggregatorFn` to the values. + * @param {Function} aggregatorFn function to apply to values. + * @return {Function} aggregator function + */ +function AggregatorFactory(aggregatorFn) { + return metrics => { + if (metrics.length === 0) return; + const result = { + help: metrics[0].help, + name: metrics[0].name, + type: metrics[0].type, + values: [] + }; + // Gather metrics by labels. + const byLabels = new util.Grouper(); + metrics.forEach(metric => { + metric.values.forEach(value => { + const key = util.hashObject(value.labels); + byLabels.add(key, value); + }); + }); + // Apply aggregator function to gathered metrics. + byLabels.forEach(values => { + if (values.length === 0) return; + const valObj = { + value: aggregatorFn(values), + labels: values[0].labels + }; + if (values[0].metricName) { + valObj.metricName = values[0].metricName; + } + // NB: Timestamps are omitted. + result.values.push(valObj); + }); + return result; + }; +} +// Export for users to define their own aggregation methods. +exports.AggregatorFactory = AggregatorFactory; + +/** + * Functions that can be used to aggregate metrics from multiple registries. + */ +exports.aggregators = { + /** + * @return The sum of values. + */ + sum: AggregatorFactory(v => v.reduce((p, c) => p + c.value, 0)), + /** + * @return The first value. + */ + first: AggregatorFactory(v => v[0].value), + /** + * @return {undefined} Undefined; omits the metric. + */ + omit: () => {}, + /** + * @return The arithmetic mean of the values. + */ + average: AggregatorFactory( + v => v.reduce((p, c) => p + c.value, 0) / v.length + ), + /** + * @return The minimum of the values. + */ + min: AggregatorFactory(v => + v.reduce((p, c) => Math.min(p, c.value), Infinity) + ), + /** + * @return The maximum of the values. + */ + max: AggregatorFactory(v => + v.reduce((p, c) => Math.max(p, c.value), -Infinity) + ) +}; diff --git a/lib/metrics/eventLoopLag.js b/lib/metrics/eventLoopLag.js index 6208a3d3..c234a447 100644 --- a/lib/metrics/eventLoopLag.js +++ b/lib/metrics/eventLoopLag.js @@ -16,7 +16,8 @@ module.exports = registry => { const gauge = new Gauge({ name: NODEJS_EVENTLOOP_LAG, help: 'Lag of event loop in seconds.', - registers: registry ? [registry] : undefined + registers: registry ? [registry] : undefined, + aggregator: 'average' }); return () => { diff --git a/lib/metrics/processStartTime.js b/lib/metrics/processStartTime.js index d5fa23bf..6120fb72 100644 --- a/lib/metrics/processStartTime.js +++ b/lib/metrics/processStartTime.js @@ -9,7 +9,8 @@ module.exports = registry => { const cpuUserGauge = new Gauge({ name: PROCESS_START_TIME, help: 'Start time of the process since unix epoch in seconds.', - registers: registry ? [registry] : undefined + registers: registry ? [registry] : undefined, + aggregator: 'omit' }); let isSet = false; diff --git a/lib/metrics/version.js b/lib/metrics/version.js index 9b111121..065f5517 100644 --- a/lib/metrics/version.js +++ b/lib/metrics/version.js @@ -11,7 +11,8 @@ module.exports = registry => { name: NODE_VERSION_INFO, help: 'Node.js version info.', labelNames: ['version', 'major', 'minor', 'patch'], - registers: registry ? [registry] : undefined + registers: registry ? [registry] : undefined, + aggregator: 'first' }); let isSet = false; diff --git a/lib/summary.js b/lib/summary.js index 8c07c821..3aac0e79 100644 --- a/lib/summary.js +++ b/lib/summary.js @@ -65,6 +65,7 @@ class Summary { this.name = config.name; this.help = config.help; + this.aggregator = config.aggregator || 'sum'; this.percentiles = config.percentiles; this.hashMap = {}; @@ -111,7 +112,8 @@ class Summary { name: this.name, help: this.help, type, - values + values, + aggregator: this.aggregator }; } diff --git a/lib/util.js b/lib/util.js index 0147de86..608acfa5 100644 --- a/lib/util.js +++ b/lib/util.js @@ -89,3 +89,21 @@ exports.printDeprecationCollectDefaultMetricsNumber = timeout => { `prom-client - A number to defaultMetrics is deprecated, please use \`collectDefaultMetrics({ timeout: ${timeout} })\`.` ); }; + +class Grouper extends Map { + /** + * Adds the `value` to the `key`'s array of values. + * @param {*} key Key to set. + * @param {*} value Value to add to `key`'s array. + * @returns {undefined} undefined. + */ + add(key, value) { + if (this.has(key)) { + this.get(key).push(value); + } else { + this.set(key, [value]); + } + } +} + +exports.Grouper = Grouper; diff --git a/test/aggregatorsTest.js b/test/aggregatorsTest.js new file mode 100644 index 00000000..683c6d29 --- /dev/null +++ b/test/aggregatorsTest.js @@ -0,0 +1,91 @@ +'use strict'; + +xdescribe('aggregators', () => { + const aggregators = require('../index').aggregators; + const metrics = [ + { + help: 'metric_help', + name: 'metric_name', + type: 'does not matter', + values: [{ labels: [], value: 1 }, { labels: ['label1'], value: 2 }] + }, + { + help: 'metric_help', + name: 'metric_name', + type: 'does not matter', + values: [{ labels: [], value: 3 }, { labels: ['label1'], value: 4 }] + } + ]; + + describe('sum', () => { + it('properly sums values', () => { + const result = aggregators.sum(metrics); + expect(result.help).toBe('metric_help'); + expect(result.name).toBe('metric_name'); + expect(result.type).toBe('does not matter'); + expect(result.values).toEqual([ + { value: 4, labels: [] }, + { value: 6, labels: ['label1'] } + ]); + }); + }); + + describe('first', () => { + it('takes the first value', () => { + const result = aggregators.first(metrics); + expect(result.help).toBe('metric_help'); + expect(result.name).toBe('metric_name'); + expect(result.type).toBe('does not matter'); + expect(result.values).toEqual([ + { value: 1, labels: [] }, + { value: 2, labels: ['label1'] } + ]); + }); + }); + + describe('omit', () => { + it('returns undefined', () => { + const result = aggregators.omit(metrics); + expect(result).toBeUndefined(); + }); + }); + + describe('average', () => { + it('properly averages values', () => { + const result = aggregators.average(metrics); + expect(result.help).toBe('metric_help'); + expect(result.name).toBe('metric_name'); + expect(result.type).toBe('does not matter'); + expect(result.values).toEqual([ + { value: 2, labels: [] }, + { value: 3, labels: ['label1'] } + ]); + }); + }); + + describe('min', () => { + it('takes the minimum of the values', () => { + const result = aggregators.min(metrics); + expect(result.help).toBe('metric_help'); + expect(result.name).toBe('metric_name'); + expect(result.type).toBe('does not matter'); + expect(result.values).toEqual([ + { value: 1, labels: [] }, + { value: 2, labels: ['label1'] } + ]); + }); + }); + + describe('max', () => { + it('takes the maximum of the values', () => { + const result = aggregators.max(metrics); + expect(result.help).toBe('metric_help'); + expect(result.name).toBe('metric_name'); + expect(result.type).toBe('does not matter'); + expect(result.values).toEqual([ + { value: 3, labels: [] }, + { value: 4, labels: ['label1'] } + ]); + }); + }); +}); diff --git a/test/clusterTest.js b/test/clusterTest.js new file mode 100644 index 00000000..e1f73f3d --- /dev/null +++ b/test/clusterTest.js @@ -0,0 +1,203 @@ +'use strict'; + +describe('AggregatorRegistry', () => { + describe('aggregatorRegistry.clusterMetrics()', () => {}); + + describe('AggregatorRegistry.aggregate()', () => { + const Registry = require('../lib/cluster'); + // These mimic the output of `getMetricsAsJSON`. + const metricsArr1 = [ + { + name: 'test_histogram', + help: 'Example of a histogram', + type: 'histogram', + values: [ + { + labels: { le: 0.1, code: '300' }, + value: 0, + metricName: 'test_histogram_bucket' + }, + { + labels: { le: 10, code: '300' }, + value: 1.6486727018068046, + metricName: 'test_histogram_bucket' + } + ] + }, + { + help: 'Example of a gauge', + name: 'test_gauge', + type: 'gauge', + values: [ + { value: 0.47, labels: { method: 'get', code: 200 } }, + { value: 0.64, labels: {} }, + { value: 23, labels: { method: 'post', code: '300' } } + ] + }, + { + help: 'Start time of the process since unix epoch in seconds.', + name: 'process_start_time_seconds', + type: 'gauge', + values: [{ value: 1502075832, labels: {} }], + aggregator: 'omit' + }, + { + help: 'Lag of event loop in seconds.', + name: 'nodejs_eventloop_lag_seconds', + type: 'gauge', + values: [{ value: 0.009, labels: {}, timestamp: 1502075832298 }], + aggregator: 'average' + }, + { + help: 'Node.js version info.', + name: 'nodejs_version_info', + type: 'gauge', + values: [ + { + value: 1, + labels: { version: 'v6.11.1', major: 6, minor: 11, patch: 1 } + } + ], + aggregator: 'first' + } + ]; + const metricsArr2 = [ + { + name: 'test_histogram', + help: 'Example of a histogram', + type: 'histogram', + values: [ + { + labels: { le: 0.1, code: '300' }, + value: 0.235151, + metricName: 'test_histogram_bucket' + }, + { + labels: { le: 10, code: '300' }, + value: 1.192591, + metricName: 'test_histogram_bucket' + } + ] + }, + { + help: 'Example of a gauge', + name: 'test_gauge', + type: 'gauge', + values: [ + { value: 0.02, labels: { method: 'get', code: 200 } }, + { value: 0.24, labels: {} }, + { value: 51, labels: { method: 'post', code: '300' } } + ] + }, + { + help: 'Start time of the process since unix epoch in seconds.', + name: 'process_start_time_seconds', + type: 'gauge', + values: [{ value: 1502075849, labels: {} }], + aggregator: 'omit' + }, + { + help: 'Lag of event loop in seconds.', + name: 'nodejs_eventloop_lag_seconds', + type: 'gauge', + values: [{ value: 0.008, labels: {}, timestamp: 1502075832321 }], + aggregator: 'average' + }, + { + help: 'Node.js version info.', + name: 'nodejs_version_info', + type: 'gauge', + values: [ + { + value: 1, + labels: { version: 'v6.11.1', major: 6, minor: 11, patch: 1 } + } + ], + aggregator: 'first' + } + ]; + + const aggregated = Registry.aggregate([metricsArr1, metricsArr2]); + + it('defaults to summation, preserves histogram bins', () => { + const histogram = aggregated.getSingleMetric('test_histogram').get(); + expect(histogram).toEqual({ + name: 'test_histogram', + help: 'Example of a histogram', + type: 'histogram', + values: [ + { + labels: { le: 0.1, code: '300' }, + value: 0.235151, + metricName: 'test_histogram_bucket' + }, + { + labels: { le: 10, code: '300' }, + value: 2.8412637018068043, + metricName: 'test_histogram_bucket' + } + ] + }); + }); + + it('defaults to summation, works for gauges', () => { + const gauge = aggregated.getSingleMetric('test_gauge').get(); + expect(gauge).toEqual({ + help: 'Example of a gauge', + name: 'test_gauge', + type: 'gauge', + values: [ + { value: 0.49, labels: { method: 'get', code: 200 } }, + { value: 0.88, labels: {} }, + { value: 74, labels: { method: 'post', code: '300' } } + ] + }); + }); + + it('uses `aggregate` method defined for process_start_time', () => { + const procStartTime = aggregated.getSingleMetric( + 'process_start_time_seconds' + ); + expect(procStartTime).toBeUndefined(); + }); + + it('uses `aggregate` method defined for nodejs_evnetloop_lag_seconds', () => { + const ell = aggregated + .getSingleMetric('nodejs_eventloop_lag_seconds') + .get(); + expect(ell).toEqual({ + help: 'Lag of event loop in seconds.', + name: 'nodejs_eventloop_lag_seconds', + type: 'gauge', + values: [{ value: 0.0085, labels: {} }] + }); + }); + + it('uses `aggregate` method defined for nodejs_evnetloop_lag_seconds', () => { + const ell = aggregated + .getSingleMetric('nodejs_eventloop_lag_seconds') + .get(); + expect(ell).toEqual({ + help: 'Lag of event loop in seconds.', + name: 'nodejs_eventloop_lag_seconds', + type: 'gauge', + values: [{ value: 0.0085, labels: {} }] + }); + }); + + it('uses `aggregate` method defined for nodejs_version_info', () => { + const version = aggregated.getSingleMetric('nodejs_version_info').get(); + expect(version).toEqual({ + help: 'Node.js version info.', + name: 'nodejs_version_info', + type: 'gauge', + values: [ + { + value: 1, + labels: { version: 'v6.11.1', major: 6, minor: 11, patch: 1 } + } + ] + }); + }); + }); +});