add normalizePath option as tuple array, improve docs and advanced example, version to 4.1.0

This commit is contained in:
Konstantin Pogorelov
2018-08-09 11:27:04 +02:00
parent d292dcab33
commit 5e0cd75673
7 changed files with 145 additions and 63 deletions

View File

@@ -52,10 +52,13 @@ Which labels to include in `http_request_duration_seconds` metric:
Extra transformation callbacks: 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). * **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". 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. 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`. * **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** * **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, 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) 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 ### More details on includePath option
Let's say you want to have latency statistics by URL path, 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, 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 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. 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 You can extend this magical behavior by providing
providing an optional callback using **normalizePath** option. additional RegExp rules to be performed,
You can also replace the default **normalizePath** function globally. 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 ```javascript
app.use(promBundle(/* options? */)); app.use(promBundle(/* options? */));
@@ -96,8 +116,8 @@ app.use(promBundle(/* options? */));
const originalNormalize = promBundle.normalizePath; const originalNormalize = promBundle.normalizePath;
promBundle.normalizePath = (req, opts) => { promBundle.normalizePath = (req, opts) => {
const path = originalNormalize(req, opts); const path = originalNormalize(req, opts);
// count all docs (no matter which file) as a single path // count all docs as one path, but /docs/login as a separate one
return path.match(/^\/docs/) ? '/docs/*' : path; 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 * [normalizePath.js](https://github.com/jochen-schweizer/express-prom-bundle/blob/master/src/normalizePath.js) - source code for path processing
## express example ## express example
setup std. metrics but exclude `up`-metric: 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": { "urlValueParser": {
"minHexLength": 5, "minHexLength": 5,
"extraMasks": [ "extraMasks": [
"^[^@]+@[^@]+\\.[^@]+$" "^[0-9]+\\.[0-9]+\\.[0-9]+$"
] ]
} }
} }

View File

@@ -5,7 +5,6 @@ const app = express();
const promBundle = require('express-prom-bundle'); const promBundle = require('express-prom-bundle');
const bundle = promBundle({ const bundle = promBundle({
blacklist: [/up/],
buckets: [0.1, 0.4, 0.7], buckets: [0.1, 0.4, 0.7],
includeMethod: true, includeMethod: true,
includePath: true, includePath: true,
@@ -19,9 +18,12 @@ const bundle = promBundle({
urlValueParser: { urlValueParser: {
minHexLength: 5, minHexLength: 5,
extraMasks: [ extraMasks: [
"^[^@]+@[^@]+\\.[^@]+$" "^[0-9]+\\.[0-9]+\\.[0-9]+$" // replace dot-separated dates with #val
] ]
} },
normalizePath: [
['^/foo', '/example'] // replace /foo with /example
]
}); });
app.use(bundle); app.use(bundle);
@@ -46,7 +48,7 @@ app.listen(3000, () => console.info( // eslint-disable-line
'listening on 3000\n' 'listening on 3000\n'
+ 'test in shell console\n\n' + 'test in shell console\n\n'
+ 'curl localhost:3000/foo/1234\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 -X DELETE localhost:3000/foo/5432\n'
+ 'curl localhost:3000/bar\n' + 'curl localhost:3000/bar\n'
+ 'curl localhost:3000/metrics\n' + 'curl localhost:3000/metrics\n'

View File

@@ -1,6 +1,6 @@
{ {
"name": "express-prom-bundle", "name": "express-prom-bundle",
"version": "4.0.0", "version": "4.1.0",
"description": "express middleware with popular prometheus metrics in one bundle", "description": "express middleware with popular prometheus metrics in one bundle",
"main": "src/index.js", "main": "src/index.js",
"keywords": [ "keywords": [

View File

@@ -185,50 +185,78 @@ describe('index', () => {
}); });
}); });
it('normalizePath can be replaced gloablly', done => { describe('usage of normalizePath()', () => {
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 can be overridden', done => { it('normalizePath can be replaced gloablly', done => {
const app = express(); const app = express();
const instance = bundle({ const original = bundle.normalizePath;
includePath: true, bundle.normalizePath = () => 'dummy';
normalizePath: req => req.originalUrl + '-suffixed' 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(/"\/test-suffixed"/m);
done();
});
}); });
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 => { it('formatStatusCode can be overridden', done => {

View File

@@ -8,4 +8,24 @@ describe('normalizePath', () => {
expect(normalizePath({url: '/a/12345'})) expect(normalizePath({url: '/a/12345'}))
.toBe('/a/#val'); .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();
});
}); });

View File

@@ -138,7 +138,9 @@ function main(opts) {
labels.method = req.method; labels.method = req.method;
} }
if (opts.includePath) { 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) { if (opts.customLabels) {
Object.assign(labels, opts.customLabels); Object.assign(labels, opts.customLabels);

View File

@@ -10,7 +10,18 @@ module.exports = function(req, opts) {
// originalUrl is taken, because url and path can be changed // originalUrl is taken, because url and path can be changed
// by middlewares such as 'router'. Note: this function is called onFinish // by middlewares such as 'router'. Note: this function is called onFinish
/// i.e. always in the tail of the middleware chain /// 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) { if (!urlValueParser) {
urlValueParser = new UrlValueParser(opts && opts.urlValueParser); urlValueParser = new UrlValueParser(opts && opts.urlValueParser);