diff --git a/README.md b/README.md index 188a7488..99f1106a 100644 --- a/README.md +++ b/README.md @@ -259,6 +259,27 @@ app.use('/metrics', (req, res) => { }); ``` +The `prometheusRegistry` option allows to provide a existing +[prom-client](https://github.com/siimon/prom-client) registry. +The metrics about the circuit will be added to the provided registry instead +of the global registry. +The [default metrics](https://github.com/siimon/prom-client#default-metrics) +will not be added to the provided registry. + +```js +const opossum = require('opossum'); +const { Registry } = require('prom-client'); + +// Create a registry +const prometheusRegistry = new Registry(); + +// create a circuit +const circuit = opossum(functionThatMightFail, { + usePrometheus: true, + prometheusRegistry +}); +``` + #### Hystrix **NOTE: Hystrix metrics are deprecated** diff --git a/lib/circuit.js b/lib/circuit.js index 8b590eba..d7c4cbf6 100644 --- a/lib/circuit.js +++ b/lib/circuit.js @@ -186,7 +186,10 @@ class CircuitBreaker extends EventEmitter { // Add Prometheus metrics if not running in a web env if (PrometheusMetrics && options.usePrometheus) { - this[PROMETHEUS_METRICS] = new PrometheusMetrics(this); + this[PROMETHEUS_METRICS] = new PrometheusMetrics( + this, + options.prometheusRegistry + ); } } diff --git a/lib/prometheus-metrics.js b/lib/prometheus-metrics.js index 72e96c93..6b8f94aa 100644 --- a/lib/prometheus-metrics.js +++ b/lib/prometheus-metrics.js @@ -14,19 +14,23 @@ function normalizePrefix(prefixName) { } class PrometheusMetrics { - constructor (circuit) { + constructor (circuit, registry) { this.circuit = circuit; + this._registry = registry || client.register this._client = client; this.counters = []; const prefix = normalizePrefix(this.circuit.name); - this.interval = this._client - .collectDefaultMetrics({ prefix, timeout: 5000 }); + if (!registry) { + this.interval = this._client + .collectDefaultMetrics({ prefix, timeout: 5000 }); + } for (let eventName of this.circuit.eventNames()) { const counter = new this._client.Counter({ name: `${prefix}${eventName}`, - help: `A count of the ${circuit.name} circuit's ${eventName} event` + help: `A count of the ${circuit.name} circuit's ${eventName} event`, + registers: [this._registry] }); this.circuit.on(eventName, _ => { counter.inc(); @@ -37,11 +41,11 @@ class PrometheusMetrics { clear () { clearInterval(this.interval); - this._client.register.clear(); + this._registry.clear(); } get metrics () { - return this._client.register.metrics(); + return this._registry.metrics(); } get client () { diff --git a/test/prometheus-test.js b/test/prometheus-test.js index b631af34..b1c87943 100644 --- a/test/prometheus-test.js +++ b/test/prometheus-test.js @@ -1,18 +1,21 @@ 'use strict'; const test = require('tape'); -const cb = require('../'); +const circuitBreaker = require('../'); const { passFail } = require('./common'); +const client = require('prom-client'); + +const { Registry } = client; test('Factory metrics func does not fail if no circuits yet', t => { t.plan(1); - t.equal(cb.metrics(), undefined); + t.equal(circuitBreaker.metrics(), undefined); t.end(); }); test('A circuit provides prometheus metrics when not in a web env', t => { t.plan(1); - const circuit = cb(passFail, {usePrometheus: true}); + const circuit = circuitBreaker(passFail, {usePrometheus: true}); t.ok(process.env.WEB ? circuit.metrics : !!circuit.metrics); circuit.metrics.clear(); t.end(); @@ -20,7 +23,7 @@ test('A circuit provides prometheus metrics when not in a web env', t => { test('Does not load Prometheus when the option is not provided', t => { t.plan(1); - const circuit = cb(passFail); + const circuit = circuitBreaker(passFail); t.ok(!circuit.metrics); circuit.shutdown(); t.end(); @@ -28,19 +31,39 @@ test('Does not load Prometheus when the option is not provided', t => { test('The factory function provides access to metrics for all circuits', t => { t.plan(4); - const c1 = cb(passFail, { usePrometheus: true, name: 'fred' }); - const c2 = cb(passFail, { usePrometheus: true, name: 'bob' }); + const c1 = circuitBreaker(passFail, { usePrometheus: true, name: 'fred' }); + const c2 = circuitBreaker(passFail, { usePrometheus: true, name: 'bob' }); + t.equal(c1.name, 'fred'); + t.equal(c2.name, 'bob'); + t.ok(/circuit_fred_/.test(circuitBreaker.metrics())); + t.ok(/circuit_bob_/.test(circuitBreaker.metrics())); + t.end(); +}); + +test('The factory function uses a custom prom-client registry', t => { + t.plan(4); + const registry = new Registry(); + const c1 = circuitBreaker(passFail, { + usePrometheus: true, + name: 'fred', + prometheusRegistry: registry + }); + const c2 = circuitBreaker(passFail, { + usePrometheus: true, + name: 'bob', + prometheusRegistry: registry + }); t.equal(c1.name, 'fred'); t.equal(c2.name, 'bob'); - t.ok(/circuit_fred_/.test(cb.metrics())); - t.ok(/circuit_bob_/.test(cb.metrics())); + t.ok(/circuit_fred_/.test(registry.metrics())); + t.ok(/circuit_bob_/.test(registry.metrics())); t.end(); }); // All of the additional tests only make sense when running in a Node.js context if (!process.env.WEB) { test('Circuit fire/success/failure are counted', t => { - const circuit = cb(passFail, {usePrometheus: true}); + const circuit = circuitBreaker(passFail, {usePrometheus: true}); const fire = /circuit_passFail_fire 2/; const success = /circuit_passFail_success 1/; const failure = /circuit_passFail_failure 1/; @@ -59,7 +82,7 @@ if (!process.env.WEB) { }); test('Metrics are enabled for all circuit events', t => { - const circuit = cb(passFail, {usePrometheus: true}); + const circuit = circuitBreaker(passFail, {usePrometheus: true}); const metrics = circuit.metrics.metrics; t.plan(circuit.eventNames().length); for (let name of circuit.eventNames()) { @@ -71,7 +94,7 @@ if (!process.env.WEB) { }); test('Default prometheus metrics are enabled', t => { - const circuit = cb(passFail, {usePrometheus: true}); + const circuit = circuitBreaker(passFail, {usePrometheus: true}); const metrics = circuit.metrics.metrics; const names = [ 'process_cpu_seconds_total', @@ -91,8 +114,33 @@ if (!process.env.WEB) { t.end(); }); + test('Should not add default metrics to custom registry', t => { + const registry = new Registry(); + const circuit = circuitBreaker(passFail, { + usePrometheus: true, + prometheusRegistry: registry + }); + const metrics = circuit.metrics.metrics; + const names = [ + 'process_cpu_seconds_total', + 'process_open_fds', + 'process_max_fds', + 'process_virtual_memory_bytes', + 'process_resident_memory_bytes', + 'process_heap_bytes', + 'process_start_time_seconds' + ]; + t.plan(names.length); + for (let name of names) { + const match = new RegExp(`circuit_passFail_${name}`); + t.notOk(match.test(metrics), name); + } + circuit.metrics.clear(); + t.end(); + }); + test('Node.js specific metrics are enabled', t => { - const circuit = cb(passFail, {usePrometheus: true}); + const circuit = circuitBreaker(passFail, {usePrometheus: true}); const metrics = circuit.metrics.metrics; const names = [ 'nodejs_eventloop_lag',