diff --git a/.gitignore b/.gitignore index 2b46987..5c7c5c3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ .npmrc node_modules coverage +/.vscode + diff --git a/.travis.yml b/.travis.yml index 276425a..045b41b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,3 +2,4 @@ language: node_js node_js: - "6" - "8" + - "10" diff --git a/README.md b/README.md index b638ff4..29edd83 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Express middleware with popular prometheus metrics in one bundle. It's also compatible with koa v1 and v2 (see below). -Internally it uses **prom-client**. See: https://github.com/siimon/prom-client +Since version 5 it uses **prom-client** as a peer dependency. See: https://github.com/siimon/prom-client Included metrics: @@ -14,7 +14,7 @@ Included metrics: ## Install ``` -npm install express-prom-bundle +npm install prom-client express-prom-bundle ``` ## Sample Usage @@ -49,6 +49,8 @@ Which labels to include in `http_request_duration_seconds` metric: * **includePath**: URL path (see importent details below), default: **false** * **customLabels**: an object containing extra labels, e.g. ```{project_name: 'hello_world'}```. Most useful together with **transformLabels** callback, otherwise it's better to use native Prometheus relabeling. +* **includeUp**: include an auxiliary "up"-metric which always returns 1, default: **true** +* **metricsPath**: replace the `/metrics` route with a **regex** or exact **string**. Note: it is highly recommended to just stick to the default Extra transformation callbacks: diff --git a/advanced-example.js b/advanced-example.js index ce49f40..53a53b8 100644 --- a/advanced-example.js +++ b/advanced-example.js @@ -2,6 +2,7 @@ const express = require('express'); const app = express(); +const promClient = require('prom-client'); const promBundle = require('express-prom-bundle'); const bundle = promBundle({ @@ -10,6 +11,7 @@ const bundle = promBundle({ includePath: true, customLabels: {year: null}, transformLabels: labels => Object.assign(labels, {year: new Date().getFullYear()}), + metricsPath: '/prometheus', promClient: { collectDefaultMetrics: { timeout: 1000 @@ -29,7 +31,7 @@ const bundle = promBundle({ app.use(bundle); // native prom-client metric (no prefix) -const c1 = new bundle.promClient.Counter({name: 'c1', help: 'c1 help'}); +const c1 = new promClient.Counter({name: 'c1', help: 'c1 help'}); c1.inc(10); app.get('/foo/:id', (req, res) => { @@ -45,11 +47,13 @@ app.delete('/foo/:id', (req, res) => { app.get('/bar', (req, res) => res.send('bar response\n')); app.listen(3000, () => console.info( // eslint-disable-line - 'listening on 3000\n' - + 'test in shell console\n\n' - + 'curl localhost:3000/foo/1234\n' - + 'curl localhost:3000/foo/09.08.2018\n' - + 'curl -X DELETE localhost:3000/foo/5432\n' - + 'curl localhost:3000/bar\n' - + 'curl localhost:3000/metrics\n' + `listening on 3000 +test in shell console: + +curl localhost:3000/foo/1234 +curl localhost:3000/foo/09.08.2018 +curl -X DELETE localhost:3000/foo/5432 +curl localhost:3000/bar +curl localhost:3000/prometheus +` )); diff --git a/spec/index.spec.js b/spec/index.spec.js index 7feda7b..d27ea6e 100644 --- a/spec/index.spec.js +++ b/spec/index.spec.js @@ -18,7 +18,7 @@ describe('index', () => { it('metrics returns up=1', done => { const app = express(); const bundled = bundle({ - whitelist: ['up'] + excludeRoutes: ['/irrelevant', /at.all/] }); app.use(bundled); app.use('/test', (req, res) => res.send('it worked')); @@ -35,6 +35,64 @@ describe('index', () => { }); }); + it('"up"-metric can be excluded', done => { + const app = express(); + const bundled = bundle({ + includeUp: false + }); + app.use(bundled); + app.use('/test', (req, res) => res.send('it worked')); + + const agent = supertest(app); + agent.get('/test').end(() => { + agent + .get('/metrics') + .end((err, res) => { + expect(res.status).toBe(200); + expect(res.text).not.toMatch(/up\s1/); + done(); + }); + }); + }); + + it('metrics path can be defined with a regex', done => { + const app = express(); + const bundled = bundle({ + metricsPath: /^\/prometheus$/ + }); + app.use(bundled); + app.use('/test', (req, res) => res.send('it worked')); + + const agent = supertest(app); + agent.get('/test').end(() => { + agent + .get('/prometheus') + .end((err, res) => { + expect(res.status).toBe(200); + expect(res.text).toMatch(/up\s1/); + done(); + }); + }); + }); + + it('metrics path can be defined as regexp', done => { + const app = express(); + const bundled = bundle(); + app.use(bundled); + app.use('/test', (req, res) => res.send('it worked')); + + const agent = supertest(app); + agent.get('/test').end(() => { + agent + .get('/metrics') + .end((err, res) => { + expect(res.status).toBe(200); + expect(res.text).toMatch(/up\s1/); + done(); + }); + }); + }); + it('httpDurationMetricName overrides histogram metric name', done => { const app = express(); const bundled = bundle({ @@ -52,9 +110,7 @@ describe('index', () => { it('metrics should be attached to /metrics by default', done => { const app = express(); - const bundled = bundle({ - whitelist: ['up'] - }); + const bundled = bundle(); app.use(bundled); const agent = supertest(app); @@ -83,27 +139,6 @@ describe('index', () => { }); }); - it('metrics can be filtered using exect match', () => { - const instance = bundle({blacklist: ['up']}); - expect(instance.metrics.up).not.toBeDefined(); - expect(instance.metrics.http_request_duration_seconds).toBeDefined(); - }); - it('metrics can be filtered using regex', () => { - const instance = bundle({blacklist: [/http/]}); - expect(instance.metrics.up).toBeDefined(); - expect(instance.metrics.http_request_duration_seconds).not.toBeDefined(); - }); - it('metrics can be whitelisted', () => { - const instance = bundle({whitelist: [/^up$/]}); - expect(instance.metrics.up).toBeDefined(); - expect(instance.metrics.nodejs_memory_heap_total_bytes).not.toBeDefined(); - expect(instance.metrics.http_request_duration_seconds).not.toBeDefined(); - }); - it('throws on both white and blacklist', () => { - expect(() => { - bundle({whitelist: [/up/], blacklist: [/up/]}); - }).toThrow(); - }); it('returns error 500 on incorrect middleware usage', done => { const app = express(); app.use(bundle); @@ -140,22 +175,27 @@ describe('index', () => { it('filters out the excludeRoutes', done => { const app = express(); const instance = bundle({ - excludeRoutes: ['/test'] + excludeRoutes: ['/test', /bad.word/] }); app.use(instance); app.use('/test', (req, res) => res.send('it worked')); + app.use('/some/bad-word', (req, res) => res.send('it worked too')); const agent = supertest(app); agent .get('/test') .end(() => { - const metricHashMap = instance.metrics.http_request_duration_seconds.hashMap; - expect(metricHashMap['status_code:200']).not.toBeDefined(); - agent - .get('/metrics') - .end((err, res) => { - expect(res.status).toBe(200); - done(); + .get('/some/bad-word') + .end(() => { + const metricHashMap = instance.metrics.http_request_duration_seconds.hashMap; + expect(metricHashMap['status_code:200']).not.toBeDefined(); + + agent + .get('/metrics') + .end((err, res) => { + expect(res.status).toBe(200); + done(); + }); }); }); }); @@ -186,27 +226,6 @@ describe('index', () => { }); }); - it('metric type summary works', done => { - const app = express(); - const bundled = bundle({ - metricType: 'summary', - percentiles: [0.5, 0.85, 0.99], - }); - app.use(bundled); - app.use('/test', (req, res) => res.send('it worked')); - - const agent = supertest(app); - agent.get('/test').end(() => { - agent - .get('/metrics') - .end((err, res) => { - expect(res.status).toBe(200); - expect(res.text).toMatch(/quantile="0.85"/); - done(); - }); - }); - }); - it('metric type histogram works', done => { const app = express(); const bundled = bundle({ @@ -400,9 +419,7 @@ describe('index', () => { it('Koa: metrics returns up=1', done => { const app = new Koa(); - const bundled = bundle({ - whitelist: ['up'] - }); + const bundled = bundle(); app.use(c2k(bundled)); app.use(function(ctx, next) { @@ -468,4 +485,47 @@ describe('index', () => { }); }, 6000); }); + + describe('metricType: summary', () => { + it('metric type summary works', done => { + const app = express(); + const bundled = bundle({ + metricType: 'summary' + }); + app.use(bundled); + app.use('/test', (req, res) => res.send('it worked')); + + const agent = supertest(app); + agent.get('/test').end(() => { + agent + .get('/metrics') + .end((err, res) => { + expect(res.status).toBe(200); + expect(res.text).toMatch(/quantile="0.98"/); + done(); + }); + }); + }); + + it('custom pecentiles work', done => { + const app = express(); + const bundled = bundle({ + metricType: 'summary', + percentiles: [0.5, 0.85, 0.99], + }); + app.use(bundled); + app.use('/test', (req, res) => res.send('it worked')); + + const agent = supertest(app); + agent.get('/test').end(() => { + agent + .get('/metrics') + .end((err, res) => { + expect(res.status).toBe(200); + expect(res.text).toMatch(/quantile="0.85"/); + done(); + }); + }); + }); + }); }); diff --git a/src/index.js b/src/index.js index eac05cf..a5d1227 100644 --- a/src/index.js +++ b/src/index.js @@ -1,11 +1,10 @@ -'use strict'; const onFinished = require('on-finished'); const promClient = require('prom-client'); const normalizePath = require('./normalizePath'); const normalizeStatusCode = require('./normalizeStatusCode'); function matchVsRegExps(element, regexps) { - for (let regexp of regexps) { + for (const regexp of regexps) { if (regexp instanceof RegExp) { if (element.match(regexp)) { return true; @@ -17,31 +16,10 @@ function matchVsRegExps(element, regexps) { return false; } -function filterArrayByRegExps(array, regexps) { - return array.filter(element => { - return matchVsRegExps(element, regexps); - }); -} - -function prepareMetricNames(opts, metricTemplates) { - const names = Object.keys(metricTemplates); - if (opts.whitelist) { - if (opts.blacklist) { - throw new Error('you cannot have whitelist and blacklist at the same time'); - } - return filterArrayByRegExps(names, opts.whitelist); - } - if (opts.blacklist) { - const blacklisted = filterArrayByRegExps(names, opts.blacklist); - return names.filter(name => blacklisted.indexOf(name) === -1); - } - return names; -} - function clusterMetrics() { const aggregatorRegistry = new promClient.AggregatorRegistry(); - const metricsMiddleware = function(req, res, next) { + const metricsMiddleware = function(req, res) { aggregatorRegistry.clusterMetrics((err, clusterMetrics) => { if (err) { console.error(err); @@ -56,17 +34,6 @@ function clusterMetrics() { } function main(opts) { - opts = Object.assign( - { - autoregister: true, - includeStatusCode: true, - normalizePath: main.normalizePath, - formatStatusCode: main.normalizeStatusCode, - metricType: 'histogram', - promClient: {} - }, - opts - ); if (arguments[2] && arguments[1] && arguments[1].send) { arguments[1].status(500) .send('