diff --git a/README.md b/README.md index 8192b1d..23491ab 100644 --- a/README.md +++ b/README.md @@ -52,10 +52,13 @@ Which labels to include in `http_request_duration_seconds` metric: Extra transformation callbacks: +* **normalizePath**: `function(req)` or `Array` + * if function is provided, then it should generate path value from express `req` + * if array is provided, then it should be an array of tuples `[regex, replacement]`. The `regex` can be a string and is automatically converted into JS regex. + * ... see more details in the section below * **urlValueParser**: options passed when instantiating [url-value-parser](https://github.com/disjunction/url-value-parser). This is the easiest way to customize which parts of the URL should be replaced with "#val". See the [docs](https://github.com/disjunction/url-value-parser) of url-value-parser module for details. -* **normalizePath**: `function(req)` generates path values from express `req` (see details below) * **formatStatusCode**: `function(res)` producing final status code from express `res` object, e.g. you can combine `200`, `201` and `204` to just `2xx`. * **transformLabels**: `function(labels, req, res)` transforms the **labels** object, e.g. setting dynamic values to **customLabels** @@ -67,12 +70,6 @@ Other options: to keep `express-prom-bundle` runnable using confit (e.g. with kraken.js) without writing any JS code, see [advanced example](https://github.com/jochen-schweizer/express-prom-bundle/blob/master/advanced-example.js) -Deprecated: - -* **whitelist**, **blacklist**: array of strings or regexp specifying which metrics to include/exclude (there are only 2 metrics) -* **excludeRoutes**: array of strings or regexp specifying which routes should be skipped for `http_request_duration_seconds` metric. It uses `req.originalUrl` as subject when checking. You want to use express or meddleware features instead of this option. -* **httpDurationMetricName**: name of the request duration histogram metric. (Default: `http_request_duration_seconds`) - ### More details on includePath option Let's say you want to have latency statistics by URL path, @@ -83,10 +80,33 @@ like `/user/12352/profile`. So what we actually need is a path template. The module tries to figure out what parts of the path are values or IDs, and what is an actual path. The example mentioned before would be normalized to `/user/#val/profile` and that will become the value for the label. +These conversions are handled by `normalizePath` function. -You can override this magical behavior and define your own function by -providing an optional callback using **normalizePath** option. -You can also replace the default **normalizePath** function globally. +You can extend this magical behavior by providing +additional RegExp rules to be performed, +or override `normalizePath` with your own function. + +#### Example 1 (add custom RegExp): + +```javascript +app.use(promBundle({ + normalizePath: [ + // collect paths like "/customer/johnbobson" as just one "/custom/#name" + ['^/customer/.*', '/customer/#name'], + + // collect paths like "/bobjohnson/order-list" as just one "/#name/order-list" + ['^.*/order-list', '/#name/order-list'] + ], + urlValueParser: { + minHexLength: 5, + extraMasks: [ + 'ORD[0-9]{5,}' // replace strings like ORD1243423, ORD673562 as #val + ] + } +})); +``` + +#### Example 2 (override normalizePath function): ```javascript app.use(promBundle(/* options? */)); @@ -96,8 +116,8 @@ app.use(promBundle(/* options? */)); const originalNormalize = promBundle.normalizePath; promBundle.normalizePath = (req, opts) => { const path = originalNormalize(req, opts); - // count all docs (no matter which file) as a single path - return path.match(/^\/docs/) ? '/docs/*' : path; + // count all docs as one path, but /docs/login as a separate one + return (path.match(/^\/docs/) && !path.match(/^\/login/)) ? '/docs/*' : path; }; ``` @@ -106,7 +126,6 @@ For more details: * [normalizePath.js](https://github.com/jochen-schweizer/express-prom-bundle/blob/master/src/normalizePath.js) - source code for path processing - ## express example setup std. metrics but exclude `up`-metric: @@ -178,7 +197,7 @@ while replacing all HEX values starting from 5 characters and all emails in the "urlValueParser": { "minHexLength": 5, "extraMasks": [ - "^[^@]+@[^@]+\\.[^@]+$" + "^[0-9]+\\.[0-9]+\\.[0-9]+$" ] } } diff --git a/advanced-example.js b/advanced-example.js index 9ce4ba9..ce49f40 100644 --- a/advanced-example.js +++ b/advanced-example.js @@ -5,7 +5,6 @@ const app = express(); const promBundle = require('express-prom-bundle'); const bundle = promBundle({ - blacklist: [/up/], buckets: [0.1, 0.4, 0.7], includeMethod: true, includePath: true, @@ -19,9 +18,12 @@ const bundle = promBundle({ urlValueParser: { minHexLength: 5, extraMasks: [ - "^[^@]+@[^@]+\\.[^@]+$" + "^[0-9]+\\.[0-9]+\\.[0-9]+$" // replace dot-separated dates with #val ] - } + }, + normalizePath: [ + ['^/foo', '/example'] // replace /foo with /example + ] }); app.use(bundle); @@ -46,7 +48,7 @@ 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/john%40example.com\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' diff --git a/package.json b/package.json index b5d3954..0d0a040 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "express-prom-bundle", - "version": "4.0.0", + "version": "4.1.0", "description": "express middleware with popular prometheus metrics in one bundle", "main": "src/index.js", "keywords": [ diff --git a/spec/index.spec.js b/spec/index.spec.js index 1f7f8f9..ae507be 100644 --- a/spec/index.spec.js +++ b/spec/index.spec.js @@ -185,50 +185,78 @@ describe('index', () => { }); }); - it('normalizePath can be replaced gloablly', done => { - const app = express(); - const original = bundle.normalizePath; - bundle.normalizePath = () => 'dummy'; - const instance = bundle({ - includePath: true, - }); - app.use(instance); - 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(/"dummy"/m); - bundle.normalizePath = original; - done(); - }); - }); - }); + describe('usage of normalizePath()', () => { - it('normalizePath can be overridden', done => { - const app = express(); - const instance = bundle({ - includePath: true, - normalizePath: req => req.originalUrl + '-suffixed' - }); - app.use(instance); - 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(/"\/test-suffixed"/m); - done(); - }); + it('normalizePath can be replaced gloablly', done => { + const app = express(); + const original = bundle.normalizePath; + bundle.normalizePath = () => 'dummy'; + const instance = bundle({ + includePath: true, }); + app.use(instance); + 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(/"dummy"/m); + bundle.normalizePath = original; + done(); + }); + }); + }); + + it('normalizePath function can be overridden', done => { + const app = express(); + const instance = bundle({ + includePath: true, + normalizePath: req => req.originalUrl + '-suffixed' + }); + app.use(instance); + 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(/"\/test-suffixed"/m); + done(); + }); + }); + }); + + it('normalizePath can be passed as an array of [regex, replacement]', done => { + const app = express(); + const instance = bundle({ + includePath: true, + normalizePath: [ + ['^/docs/whatever/.*$', '/docs'], + [/^\/docs$/, '/mocks'] + ] + }); + app.use(instance); + app.use('/docs/whatever/:andmore', (req, res) => res.send('it worked')); + const agent = supertest(app); + agent + .get('/docs/whatever/unimportant') + .end(() => { + agent + .get('/metrics') + .end((err, res) => { + expect(res.status).toBe(200); + expect(res.text).toMatch(/"\/mocks"/m); + done(); + }); + }); + }); }); it('formatStatusCode can be overridden', done => { diff --git a/spec/normalizePath.spec.js b/spec/normalizePath.spec.js index 583cc31..1bb2337 100644 --- a/spec/normalizePath.spec.js +++ b/spec/normalizePath.spec.js @@ -8,4 +8,24 @@ describe('normalizePath', () => { expect(normalizePath({url: '/a/12345'})) .toBe('/a/#val'); }); + + it('uses normalizePath option', () => { + const url = '/hello/world/i/am/finally/free!!!'; + const result = normalizePath({url}, { + normalizePath: [ + ['/hello','/goodbye'], + ['[^/]+$','happy'], + ] + }); + expect(result).toBe('/goodbye/world/i/am/finally/happy'); + }); + + it('throws error is bad tuples provided as normalizePath', () => { + const subject = () => normalizePath({url: '/test'}, { + normalizePath: [ + ['/hello','/goodbye', 'test'] + ] + }); + expect(subject).toThrow(); + }); }); diff --git a/src/index.js b/src/index.js index b582510..cc4ba6c 100644 --- a/src/index.js +++ b/src/index.js @@ -138,7 +138,9 @@ function main(opts) { labels.method = req.method; } if (opts.includePath) { - labels.path = opts.normalizePath(req, opts); + labels.path = typeof opts.normalizePath == 'function' + ? opts.normalizePath(req, opts) + : main.normalizePath(req, opts); } if (opts.customLabels) { Object.assign(labels, opts.customLabels); diff --git a/src/normalizePath.js b/src/normalizePath.js index d40fd18..f2a0f57 100644 --- a/src/normalizePath.js +++ b/src/normalizePath.js @@ -10,7 +10,18 @@ module.exports = function(req, opts) { // originalUrl is taken, because url and path can be changed // by middlewares such as 'router'. Note: this function is called onFinish /// i.e. always in the tail of the middleware chain - const path = url.parse(req.originalUrl || req.url).pathname; + let path = url.parse(req.originalUrl || req.url).pathname; + + const normalizePath = opts && opts.normalizePath; + if (Array.isArray(normalizePath)) { + for (const tuple of normalizePath) { + if (!Array.isArray(tuple) || tuple.length !== 2) { + throw new Error('Bad tuple provided in normalizePath option, expected: [regex, replacement]'); + } + const regex = typeof tuple[0] === 'string' ? RegExp(tuple[0]) : tuple[0]; + path = path.replace(regex, tuple[1]); + } + } if (!urlValueParser) { urlValueParser = new UrlValueParser(opts && opts.urlValueParser);