mirror of
https://github.com/BreizhHardware/express-prom-bundle.git
synced 2026-01-19 00:37:36 +01:00
Compare commits
42 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ec9835270f | ||
|
|
1cf8c86acb | ||
|
|
43b51d4ab1 | ||
|
|
f6e87b7697 | ||
|
|
3be8d53a27 | ||
|
|
34e6a21be1 | ||
|
|
02fcda4721 | ||
|
|
5e0cd75673 | ||
|
|
d292dcab33 | ||
|
|
84f99cc49c | ||
|
|
fffe35ce5e | ||
|
|
06341c227a | ||
|
|
45b8f373be | ||
|
|
4ee269faee | ||
|
|
e4d6113ff2 | ||
|
|
06f55c9ab8 | ||
|
|
c8996a7730 | ||
|
|
aed7edc684 | ||
|
|
4a840cfce6 | ||
|
|
d12248dcaf | ||
|
|
68eb617f88 | ||
|
|
bb0e453078 | ||
|
|
b0928b1b94 | ||
|
|
26f9f5ca10 | ||
|
|
a261ab76a3 | ||
|
|
8318e0ec1d | ||
|
|
d58b434f0a | ||
|
|
47fd051aba | ||
|
|
313673f16f | ||
|
|
bbd62e34d6 | ||
|
|
b8ba87009e | ||
|
|
1cc588c2da | ||
|
|
5b1517ca91 | ||
|
|
5b1aa494cb | ||
|
|
65549a769b | ||
|
|
de83ac09a0 | ||
|
|
52865dfb02 | ||
|
|
5c6ed64a31 | ||
|
|
d8c6492163 | ||
|
|
c92b85ae96 | ||
|
|
48f8b992fd | ||
|
|
61e4343a8c |
@@ -1,5 +1,4 @@
|
||||
language: node_js
|
||||
node_js:
|
||||
- "6"
|
||||
- "5"
|
||||
- "4"
|
||||
- "8"
|
||||
|
||||
185
README.md
185
README.md
@@ -2,7 +2,7 @@
|
||||
|
||||
# express prometheus bundle
|
||||
|
||||
Express middleware with popular prometheus metrics in one bundle. It's also compatible with koa v1 (see below).
|
||||
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
|
||||
|
||||
@@ -11,20 +11,18 @@ Included metrics:
|
||||
* `up`: normally is just 1
|
||||
* `http_request_duration_seconds`: http latency histogram labeled with `status_code`, `method` and `path`
|
||||
|
||||
**Please note version 2.x is NOT backwards compatible with 1.x**
|
||||
|
||||
## Install
|
||||
|
||||
```
|
||||
npm install express-prom-bundle
|
||||
```
|
||||
|
||||
## Usage
|
||||
## Sample Usage
|
||||
|
||||
```javascript
|
||||
const promBundle = require("express-prom-bundle");
|
||||
const metricsMiddleware = promBundle({/* options */ });
|
||||
const app = require("express")();
|
||||
const metricsMiddleware = promBundle({includeMethod: true});
|
||||
|
||||
app.use(metricsMiddleware);
|
||||
app.use(/* your middleware */);
|
||||
@@ -42,42 +40,106 @@ The order in which the routes are registered is important, since
|
||||
You can use this to your advantage to bypass some of the routes.
|
||||
See the example below.
|
||||
|
||||
## Usage with Node Cluster
|
||||
``` javascript
|
||||
if (cluster.isMaster) {
|
||||
const numCPUs = Math.max(2, os.cpus().length);
|
||||
const workers: cluster.Worker[] = [];
|
||||
for (let i=1; i < numCPUs; i++) {
|
||||
const worker = forkWorker();
|
||||
workers.push(worker);
|
||||
}
|
||||
const metricsApp = express();
|
||||
metricsApp.use('/cluster_metrics', promBundle.clusterMetrics());
|
||||
metricsApp.listen(9999);
|
||||
console.log('metrics listening on 9999'); // call localhost:9999/cluster_metrics for aggregated metrics
|
||||
} else {
|
||||
const app = express();
|
||||
app.use(promBundle({includeMethod: true});
|
||||
app.use('/api', require('./api'));
|
||||
app.listen(3000);
|
||||
}
|
||||
```
|
||||
The code the master process runs will expose an API with a single endpoint `/cluster_metrics` which returns an aggregate of all metrics from all the workers.
|
||||
|
||||
## Options
|
||||
|
||||
* **buckets**: buckets used for `http_request_seconds` histogram
|
||||
* **includeMethod**: include HTTP method (GET, PUT, ...) as a label to `http_request_duration_seconds`
|
||||
* **includePath**: include URL path as a label (see below)
|
||||
* **normalizePath**: boolean or `function(req)` - path normalization for `includePath` option
|
||||
Which labels to include in `http_request_duration_seconds` metric:
|
||||
|
||||
* **includeStatusCode**: HTTP status code (200, 400, 404 etc.), default: **true**
|
||||
* **includeMethod**: HTTP method (GET, PUT, ...), default: **false**
|
||||
* **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.
|
||||
|
||||
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.
|
||||
* **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**
|
||||
|
||||
Other options:
|
||||
|
||||
* **buckets**: buckets used for `http_request_duration_seconds` histogram
|
||||
* **autoregister**: if `/metrics` endpoint should be registered. (Default: **true**)
|
||||
* **whitelist**, **blacklist**: array of strings or regexp specifying which metrics to include/exclude
|
||||
* **excludeRoutes**: (deprecated) 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 normally use express or meddleware features instead of this options.
|
||||
* **promClient**: options for promClient startup, e.g. **collectDefaultMetrics**. This option was added
|
||||
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)
|
||||
|
||||
### More details on includePath option
|
||||
|
||||
The goal is to have separate latency statistics by URL path, e.g. `/my-app/user/`, `/products/by-category` etc.
|
||||
Let's say you want to have latency statistics by URL path,
|
||||
e.g. separate metrics for `/my-app/user/`, `/products/by-category` etc.
|
||||
|
||||
Just taking `req.path` as a label value won't work as IDs are often part of the URL,
|
||||
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.
|
||||
This is handy if the rest of the middleware is done elsewhere
|
||||
e.g. via `kraken.js meddleware`.
|
||||
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? */));
|
||||
|
||||
// let's reuse the existing one and just add some
|
||||
// functionality on top
|
||||
const originalNormalize = promBunle.normalizePath;
|
||||
promBunle.normalizePath = (req, opts) => {
|
||||
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;
|
||||
};
|
||||
```
|
||||
|
||||
@@ -86,7 +148,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:
|
||||
@@ -116,24 +177,62 @@ app.listen(3000);
|
||||
|
||||
See an [advanced example on github](https://github.com/jochen-schweizer/express-prom-bundle/blob/master/advanced-example.js)
|
||||
|
||||
## koa v1 example
|
||||
## koa v2 example
|
||||
|
||||
```javascript
|
||||
const promBundle = require("express-prom-bundle");
|
||||
const koa = require("koa");
|
||||
const Koa = require("koa");
|
||||
const c2k = require("koa-connect");
|
||||
const metricsMiddleware = promBundle({/* options */ });
|
||||
|
||||
const app = koa();
|
||||
const app = new Koa();
|
||||
|
||||
app.use(c2k(metricsMiddleware));
|
||||
app.use(/* your middleware */);
|
||||
app.listen(3000);
|
||||
```
|
||||
|
||||
## using with cluster
|
||||
|
||||
You'll need to use an additional **clusterMetrics()** middleware.
|
||||
|
||||
In the example below the master process will expose an API with a single endpoint `/metrics`
|
||||
which returns an aggregate of all metrics from all the workers.
|
||||
|
||||
``` javascript
|
||||
const cluster = require('cluster');
|
||||
const promBundle = require('./src/index');
|
||||
const numCPUs = Math.max(2, require('os').cpus().length);
|
||||
const express = require('express');
|
||||
|
||||
if (cluster.isMaster) {
|
||||
for (let i = 1; i < numCPUs; i++) {
|
||||
cluster.fork();
|
||||
}
|
||||
|
||||
const metricsApp = express();
|
||||
metricsApp.use('/metrics', promBundle.clusterMetrics());
|
||||
metricsApp.listen(9999);
|
||||
|
||||
console.log('cluster metrics listening on 9999');
|
||||
console.log('call localhost:9999/metrics for aggregated metrics');
|
||||
} else {
|
||||
const app = express();
|
||||
app.use(promBundle({
|
||||
autoregister: false, // disable /metrics for single workers
|
||||
includeMethod: true
|
||||
}));
|
||||
app.use((req, res) => res.send(`hello from pid ${process.pid}\n`));
|
||||
app.listen(3000);
|
||||
console.log(`worker ${process.pid} listening on 3000`);
|
||||
}
|
||||
```
|
||||
|
||||
## using with kraken.js
|
||||
|
||||
Here is meddleware config sample, which can be used in a standard **kraken.js** application:
|
||||
Here is meddleware config sample, which can be used in a standard **kraken.js** application.
|
||||
In this case the stats for URI paths and HTTP methods are collected separately,
|
||||
while replacing all HEX values starting from 5 characters and all emails in the path as #val.
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -146,7 +245,19 @@ Here is meddleware config sample, which can be used in a standard **kraken.js**
|
||||
"arguments": [
|
||||
{
|
||||
"includeMethod": true,
|
||||
"buckets": [0.1, 1, 5]
|
||||
"includePath": true,
|
||||
"buckets": [0.1, 1, 5],
|
||||
"promClient": {
|
||||
"collectDefaultMetrics": {
|
||||
"timeout": 2000
|
||||
}
|
||||
},
|
||||
"urlValueParser": {
|
||||
"minHexLength": 5,
|
||||
"extraMasks": [
|
||||
"^[0-9]+\\.[0-9]+\\.[0-9]+$"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -155,26 +266,6 @@ Here is meddleware config sample, which can be used in a standard **kraken.js**
|
||||
}
|
||||
```
|
||||
|
||||
## Changelog
|
||||
|
||||
* **2.1.0**
|
||||
* deprecate **excludeRoutes**, use **req.originalPath** instead of **req.path**
|
||||
* **2.0.0**
|
||||
* the reason for the version lift were:
|
||||
* compliance to official naming recommendation: https://prometheus.io/docs/practices/naming/
|
||||
* stopping promotion of an anti-pattern - see https://groups.google.com/d/msg/prometheus-developers/XjlOnDCK9qc/ovKzV3AIBwAJ
|
||||
* dealing with **prom-client** being a singleton with a built-in registry
|
||||
* main histogram metric renamed from `http_request_seconds` to `http_request_duration_seconds`
|
||||
* options removed: **prefix**, **keepDefaultMetrics**
|
||||
* factory removed (as the only reason of it was adding the prefix)
|
||||
* upgrade prom-client to 6.3.0
|
||||
* code style changed to the one closer to express
|
||||
|
||||
|
||||
* **1.2.1**
|
||||
* upgrade prom-client to 6.1.2
|
||||
* add options: includeMethod, includePath, keepDefaultMetrics
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
|
||||
@@ -4,22 +4,32 @@ const express = require('express');
|
||||
const app = express();
|
||||
const promBundle = require('express-prom-bundle');
|
||||
|
||||
// here we want to remove default metrics provided in prom-client
|
||||
// this must be done before initializing promBundle
|
||||
clearInterval(promBundle.promClient.defaultMetrics());
|
||||
promBundle.promClient.register.clear();
|
||||
|
||||
const bundle = promBundle({
|
||||
blacklist: [/up/],
|
||||
buckets: [0.1, 0.4, 0.7],
|
||||
includeMethod: true,
|
||||
includePath: true
|
||||
includePath: true,
|
||||
customLabels: {year: null},
|
||||
transformLabels: labels => Object.assign(labels, {year: new Date().getFullYear()}),
|
||||
promClient: {
|
||||
collectDefaultMetrics: {
|
||||
timeout: 1000
|
||||
}
|
||||
},
|
||||
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);
|
||||
|
||||
// native prom-client metric (no prefix)
|
||||
const c1 = new bundle.promClient.Counter('c1', 'c1 help');
|
||||
const c1 = new bundle.promClient.Counter({name: 'c1', help: 'c1 help'});
|
||||
c1.inc(10);
|
||||
|
||||
app.get('/foo/:id', (req, res) => {
|
||||
@@ -38,6 +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/09.08.2018\n'
|
||||
+ 'curl -X DELETE localhost:3000/foo/5432\n'
|
||||
+ 'curl localhost:3000/bar\n'
|
||||
+ 'curl localhost:3000/metrics\n'
|
||||
|
||||
2668
package-lock.json
generated
Normal file
2668
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
24
package.json
24
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "express-prom-bundle",
|
||||
"version": "2.1.0",
|
||||
"version": "4.2.0",
|
||||
"description": "express middleware with popular prometheus metrics in one bundle",
|
||||
"main": "src/index.js",
|
||||
"keywords": [
|
||||
@@ -11,28 +11,32 @@
|
||||
"method"
|
||||
],
|
||||
"scripts": {
|
||||
"test": "node_modules/jasme/run.js"
|
||||
"test": "node_modules/jasme/run.js",
|
||||
"coverage": "make coverage"
|
||||
},
|
||||
"author": "Konstantin Pogorelov <or@pluseq.com>",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"on-finished": "^2.3.0",
|
||||
"prom-client": "^6.3.0",
|
||||
"url-value-parser": "^1.0.0"
|
||||
"prom-client": "~11.1.1",
|
||||
"url-value-parser": "^2.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"coveralls": "^2.11.15",
|
||||
"eslint": "^3.11.1",
|
||||
"express": "^4.14.0",
|
||||
"coveralls": "^3.0.2",
|
||||
"eslint": "^5.3.0",
|
||||
"express": "^4.16.3",
|
||||
"istanbul": "^0.4.5",
|
||||
"jasme": "^5.2.0",
|
||||
"koa": "^1.2.4",
|
||||
"koa-connect": "^1.0.0",
|
||||
"supertest": "^2.0.1",
|
||||
"koa": "^2.5.2",
|
||||
"koa-connect": "^2.0.0",
|
||||
"supertest": "^3.0.0",
|
||||
"supertest-koa-agent": "^0.3.0"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/jochen-schweizer/express-prom-bundle.git"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,10 +4,11 @@
|
||||
const express = require('express');
|
||||
const supertest = require('supertest');
|
||||
const bundle = require('../');
|
||||
const koa = require('koa');
|
||||
const Koa = require('koa');
|
||||
const c2k = require('koa-connect');
|
||||
const supertestKoa = require('supertest-koa-agent');
|
||||
const promClient = require('prom-client');
|
||||
const cluster = require('cluster');
|
||||
|
||||
describe('index', () => {
|
||||
beforeEach(() => {
|
||||
@@ -34,6 +35,21 @@ describe('index', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('httpDurationMetricName overrides histogram metric name', done => {
|
||||
const app = express();
|
||||
const bundled = bundle({
|
||||
httpDurationMetricName: 'my_http_duration'
|
||||
});
|
||||
app.use(bundled);
|
||||
|
||||
const agent = supertest(app);
|
||||
agent.get('/metrics')
|
||||
.end((err, res) => {
|
||||
expect(res.text).toMatch(/my_http_duration/m);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('metrics should be attached to /metrics by default', done => {
|
||||
const app = express();
|
||||
const bundled = bundle({
|
||||
@@ -148,11 +164,12 @@ describe('index', () => {
|
||||
expect(() => bundle({prefix: 'hello'})).toThrow();
|
||||
});
|
||||
|
||||
it('tolerates includePath, includeMethod', done => {
|
||||
it('tolerates includePath, includeMethod, includeCustomLabels', done => {
|
||||
const app = express();
|
||||
const instance = bundle({
|
||||
includePath: true,
|
||||
includeMethod: true
|
||||
includeMethod: true,
|
||||
includeCustomLabels: {foo: 'bar'}
|
||||
});
|
||||
app.use(instance);
|
||||
app.use('/test', (req, res) => res.send('it worked'));
|
||||
@@ -169,12 +186,84 @@ describe('index', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('normalizePath can be replaced', done => {
|
||||
describe('usage of normalizePath()', () => {
|
||||
|
||||
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 => {
|
||||
const app = express();
|
||||
const original = bundle.normalizePath;
|
||||
bundle.normalizePath = () => 'dummy';
|
||||
const instance = bundle({
|
||||
includePath: true,
|
||||
formatStatusCode: () => 555
|
||||
});
|
||||
app.use(instance);
|
||||
app.use('/test', (req, res) => res.send('it worked'));
|
||||
@@ -186,25 +275,90 @@ describe('index', () => {
|
||||
.get('/metrics')
|
||||
.end((err, res) => {
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.text).toMatch(/"dummy"/m);
|
||||
bundle.normalizePath = original;
|
||||
expect(res.text).toMatch(/555/);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('includeStatusCode=false removes status_code label from metrics', done => {
|
||||
const app = express();
|
||||
const instance = bundle({
|
||||
includeStatusCode: false
|
||||
});
|
||||
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).not.toMatch(/="200"/);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('customLabels={foo: "bar"} adds foo="bar" label to metrics', done => {
|
||||
const app = express();
|
||||
const instance = bundle({
|
||||
customLabels: {foo: 'bar'}
|
||||
});
|
||||
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(/foo="bar"/);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('tarnsformLabels can set label values', done => {
|
||||
const app = express();
|
||||
const instance = bundle({
|
||||
includePath: true,
|
||||
customLabels: {foo: 'bar'},
|
||||
transformLabels: labels => {
|
||||
labels.foo = 'baz';
|
||||
labels.path += '/ok';
|
||||
}
|
||||
});
|
||||
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(/foo="baz"/);
|
||||
expect(res.text).toMatch(/path="\/test\/ok"/);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('Koa: metrics returns up=1', done => {
|
||||
const app = koa();
|
||||
const app = new Koa();
|
||||
const bundled = bundle({
|
||||
whitelist: ['up']
|
||||
});
|
||||
app.use(c2k(bundled));
|
||||
|
||||
app.use(function*(next) {
|
||||
if (this.path !== 'test') {
|
||||
return yield next;
|
||||
}
|
||||
this.body = 'it worked';
|
||||
app.use(function(ctx, next) {
|
||||
return next().then(() => ctx.body = 'it worked');
|
||||
});
|
||||
|
||||
const agent = supertestKoa(app);
|
||||
@@ -218,4 +372,52 @@ describe('index', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('calls promClient.collectDefaultMetrics', () => {
|
||||
const spy = spyOn(promClient, 'collectDefaultMetrics');
|
||||
bundle({
|
||||
promClient: {
|
||||
collectDefaultMetrics: {
|
||||
timeout: 3000
|
||||
}
|
||||
}
|
||||
});
|
||||
expect(spy).toHaveBeenCalledWith({timeout: 3000});
|
||||
});
|
||||
|
||||
describe('usage of clusterMetrics()', () => {
|
||||
it('clusterMetrics returns 200 even without a cluster', (done) => {
|
||||
const app = express();
|
||||
|
||||
cluster.workers = [];
|
||||
|
||||
app.use('/cluster_metrics', bundle.clusterMetrics());
|
||||
const agent = supertest(app);
|
||||
agent
|
||||
.get('/cluster_metrics')
|
||||
.end((err, res) => {
|
||||
expect(res.status).toBe(200);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('clusterMetrics returns 500 in case of an error', (done) => {
|
||||
const app = express();
|
||||
app.use('/cluster_metrics', bundle.clusterMetrics());
|
||||
const agent = supertest(app);
|
||||
|
||||
// create a fake worker, which would not respond in time
|
||||
cluster.workers = [{send: () => {}}];
|
||||
|
||||
const errorSpy = spyOn(console, 'error'); // mute console.error
|
||||
|
||||
agent
|
||||
.get('/cluster_metrics')
|
||||
.end((err, res) => {
|
||||
expect(res.status).toBe(500);
|
||||
expect(errorSpy).toHaveBeenCalled();
|
||||
done();
|
||||
});
|
||||
}, 6000);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,25 +4,28 @@
|
||||
const normalizePath = require('../src/normalizePath');
|
||||
|
||||
describe('normalizePath', () => {
|
||||
it('returns original if disabled in opts', () => {
|
||||
expect(
|
||||
normalizePath({originalUrl: '/a/12345'}, {normalizePath: false})
|
||||
).toBe('/a/12345');
|
||||
});
|
||||
|
||||
it('returns run callback if configured', () => {
|
||||
expect(
|
||||
normalizePath(
|
||||
{originalUrl: '/a/12345'},
|
||||
{
|
||||
normalizePath: req => req.originalUrl + '-ok'
|
||||
}
|
||||
)
|
||||
).toBe('/a/12345-ok');
|
||||
});
|
||||
|
||||
it('uses UrlValueParser by default', () => {
|
||||
expect(normalizePath({originalUrl: '/a/12345'}))
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
||||
12
spec/normalizeStatusCode.spec.js
Normal file
12
spec/normalizeStatusCode.spec.js
Normal file
@@ -0,0 +1,12 @@
|
||||
'use strict';
|
||||
/* eslint-env jasmine */
|
||||
|
||||
const normalizeStatusCode = require('../src/normalizeStatusCode');
|
||||
|
||||
describe('normalizeStatusCode', () => {
|
||||
it('returns run callback if configured', () => {
|
||||
expect(
|
||||
normalizeStatusCode({status_code: 500})
|
||||
).toBe(500);
|
||||
});
|
||||
});
|
||||
88
src/index.js
88
src/index.js
@@ -2,6 +2,7 @@
|
||||
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) {
|
||||
@@ -37,8 +38,34 @@ function prepareMetricNames(opts, metricTemplates) {
|
||||
return names;
|
||||
}
|
||||
|
||||
function clusterMetrics() {
|
||||
const aggregatorRegistry = new promClient.AggregatorRegistry();
|
||||
|
||||
const metricsMiddleware = function(req, res, next) {
|
||||
aggregatorRegistry.clusterMetrics((err, clusterMetrics) => {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
return res.sendStatus(500);
|
||||
}
|
||||
res.set('Content-Type', aggregatorRegistry.contentType);
|
||||
res.send(clusterMetrics);
|
||||
});
|
||||
};
|
||||
|
||||
return metricsMiddleware;
|
||||
}
|
||||
|
||||
function main(opts) {
|
||||
opts = Object.assign({autoregister: true}, opts);
|
||||
opts = Object.assign(
|
||||
{
|
||||
autoregister: true,
|
||||
includeStatusCode: true,
|
||||
normalizePath: main.normalizePath,
|
||||
formatStatusCode: main.normalizeStatusCode,
|
||||
promClient: {}
|
||||
},
|
||||
opts
|
||||
);
|
||||
if (arguments[2] && arguments[1] && arguments[1].send) {
|
||||
arguments[1].status(500)
|
||||
.send('<h1>500 Error</h1>\n'
|
||||
@@ -57,14 +84,18 @@ function main(opts) {
|
||||
);
|
||||
}
|
||||
|
||||
const httpMtricName = opts.httpDurationMetricName || 'http_request_duration_seconds';
|
||||
if (opts.promClient.collectDefaultMetrics) {
|
||||
promClient.collectDefaultMetrics(opts.promClient.collectDefaultMetrics);
|
||||
}
|
||||
|
||||
const httpMetricName = opts.httpDurationMetricName || 'http_request_duration_seconds';
|
||||
|
||||
const metricTemplates = {
|
||||
'up': () => new promClient.Gauge(
|
||||
'up',
|
||||
'1 = up, 0 = not up'
|
||||
),
|
||||
'http_request_duration_seconds': () => {
|
||||
'up': () => new promClient.Gauge({
|
||||
name: 'up',
|
||||
help: '1 = up, 0 = not up'
|
||||
}),
|
||||
[httpMetricName]: () => {
|
||||
const labels = ['status_code'];
|
||||
if (opts.includeMethod) {
|
||||
labels.push('method');
|
||||
@@ -72,14 +103,15 @@ function main(opts) {
|
||||
if (opts.includePath) {
|
||||
labels.push('path');
|
||||
}
|
||||
const metric = new promClient.Histogram(
|
||||
httpMtricName,
|
||||
'duration histogram of http responses labeled with: ' + labels.join(', '),
|
||||
labels,
|
||||
{
|
||||
buckets: opts.buckets || [0.003, 0.03, 0.1, 0.3, 1.5, 10]
|
||||
}
|
||||
);
|
||||
if (opts.customLabels){
|
||||
labels.push.apply(labels, Object.keys(opts.customLabels));
|
||||
}
|
||||
const metric = new promClient.Histogram({
|
||||
name: httpMetricName,
|
||||
help: 'duration histogram of http responses labeled with: ' + labels.join(', '),
|
||||
labelNames: labels,
|
||||
buckets: opts.buckets || [0.003, 0.03, 0.1, 0.3, 1.5, 10]
|
||||
});
|
||||
return metric;
|
||||
}
|
||||
};
|
||||
@@ -101,27 +133,37 @@ function main(opts) {
|
||||
};
|
||||
|
||||
const middleware = function (req, res, next) {
|
||||
const path = req.originalUrl;
|
||||
const path = req.originalUrl || req.url; // originalUrl gets lost in koa-connect?
|
||||
let labels;
|
||||
|
||||
if (opts.autoregister && path.match(/^\/metrics\/?$/)) {
|
||||
return metricsMiddleware(req, res);
|
||||
return metricsMiddleware(req, res);
|
||||
}
|
||||
|
||||
if (opts.excludeRoutes && matchVsRegExps(path, opts.excludeRoutes)) {
|
||||
return next();
|
||||
}
|
||||
|
||||
if (metrics[httpMtricName]) {
|
||||
labels = {'status_code': 0};
|
||||
let timer = metrics[httpMtricName].startTimer(labels);
|
||||
if (metrics[httpMetricName]) {
|
||||
labels = {};
|
||||
let timer = metrics[httpMetricName].startTimer(labels);
|
||||
onFinished(res, () => {
|
||||
labels.status_code = res.statusCode;
|
||||
if (opts.includeStatusCode) {
|
||||
labels.status_code = opts.formatStatusCode(res, opts);
|
||||
}
|
||||
if (opts.includeMethod) {
|
||||
labels.method = req.method;
|
||||
}
|
||||
if (opts.includePath) {
|
||||
labels.path = main.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);
|
||||
}
|
||||
if (opts.transformLabels) {
|
||||
opts.transformLabels(labels, req, res);
|
||||
}
|
||||
timer();
|
||||
});
|
||||
@@ -139,4 +181,6 @@ function main(opts) {
|
||||
|
||||
main.promClient = promClient;
|
||||
main.normalizePath = normalizePath;
|
||||
main.normalizeStatusCode = normalizeStatusCode;
|
||||
main.clusterMetrics = clusterMetrics;
|
||||
module.exports = main;
|
||||
|
||||
@@ -2,26 +2,29 @@
|
||||
|
||||
const UrlValueParser = require('url-value-parser');
|
||||
const url = require('url');
|
||||
|
||||
// ATTENTION! urlValueParser is a singleton!
|
||||
let urlValueParser;
|
||||
|
||||
module.exports = function(req, opts) {
|
||||
opts = 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).pathname;
|
||||
let path = url.parse(req.originalUrl || req.url).pathname;
|
||||
|
||||
if (opts.normalizePath !== undefined && !opts.normalizePath) {
|
||||
return path;
|
||||
}
|
||||
if (typeof opts.normalizePath === 'function') {
|
||||
return opts.normalizePath(req, opts);
|
||||
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();
|
||||
urlValueParser = new UrlValueParser(opts && opts.urlValueParser);
|
||||
}
|
||||
return urlValueParser.replacePathValues(path);
|
||||
};
|
||||
|
||||
|
||||
5
src/normalizeStatusCode.js
Normal file
5
src/normalizeStatusCode.js
Normal file
@@ -0,0 +1,5 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = function(res) {
|
||||
return res.status_code || res.statusCode;
|
||||
};
|
||||
Reference in New Issue
Block a user