mirror of
https://github.com/BreizhHardware/express-prom-bundle.git
synced 2026-01-19 00:37:36 +01:00
Compare commits
89 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c55d5f4862 | ||
|
|
75b1e2cafa | ||
|
|
e2a8869b7f | ||
|
|
c64772dfc9 | ||
|
|
c1b1022fb5 | ||
|
|
16dfa7f926 | ||
|
|
7e58bd3d06 | ||
|
|
adf15f03f4 | ||
|
|
6629c09dfa | ||
|
|
fe2d1d25ec | ||
|
|
a5d63925ce | ||
|
|
43ba2091d3 | ||
|
|
1909018c17 | ||
|
|
52f3a955df | ||
|
|
e2800e7d30 | ||
|
|
fff0384605 | ||
|
|
514745b23a | ||
|
|
77730b9df5 | ||
|
|
6d9da12d97 | ||
|
|
f1f36f0fb7 | ||
|
|
d2f0088f3c | ||
|
|
4f89424c79 | ||
|
|
33765b3654 | ||
|
|
1a8f38c983 | ||
|
|
23cedb9f05 | ||
|
|
20c6a7de24 | ||
|
|
0a1fda31de | ||
|
|
7654a4ec79 | ||
|
|
faeeb7b57b | ||
|
|
a21bd439cf | ||
|
|
90f2d90e46 | ||
|
|
71c0e52020 | ||
|
|
78e25d6587 | ||
|
|
02f3fe9008 | ||
|
|
2a5e25dd77 | ||
|
|
f770ab800d | ||
|
|
6041bee4dd | ||
|
|
a92b4cfbe6 | ||
|
|
8df2778337 | ||
|
|
b00d88c4a7 | ||
|
|
11988cb025 | ||
|
|
df6854ee04 | ||
|
|
48d883b7f2 | ||
|
|
b835e90560 | ||
|
|
088ff5a063 | ||
|
|
fd12de6ea3 | ||
|
|
0b8706dd42 | ||
|
|
982ff86543 | ||
|
|
5f39d8f357 | ||
|
|
d405e3f584 | ||
|
|
ff147d8fc4 | ||
|
|
43f5a2b04f | ||
|
|
09ee5d954a | ||
|
|
33cca0d2cf | ||
|
|
2040c043fd | ||
|
|
f6466d007b | ||
|
|
e68466d7af | ||
|
|
6b49ffab08 | ||
|
|
cb128c7520 | ||
|
|
d336165848 | ||
|
|
cfe0065146 | ||
|
|
963a66a25e | ||
|
|
389684b426 | ||
|
|
846ccfc641 | ||
|
|
17fe5f4c70 | ||
|
|
66ef4e3176 | ||
|
|
f55d397a31 | ||
|
|
68ad108f77 | ||
|
|
6000056f7a | ||
|
|
48856afbe3 | ||
|
|
e035e2b991 | ||
|
|
c6d5964768 | ||
|
|
94722e908b | ||
|
|
00b8369329 | ||
|
|
59221f891b | ||
|
|
3a0b2caf61 | ||
|
|
e7d004f0cc | ||
|
|
0dd3116f23 | ||
|
|
6054824c67 | ||
|
|
01c78bcc1d | ||
|
|
bc65dc45cb | ||
|
|
8cae5b6ef3 | ||
|
|
2003e7743f | ||
|
|
568c87216a | ||
|
|
98be36244e | ||
|
|
6ff1204db4 | ||
|
|
f71d837660 | ||
|
|
4aa2bfa6ae | ||
|
|
1fff877787 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,3 +1,5 @@
|
||||
.npmrc
|
||||
node_modules
|
||||
coverage
|
||||
/.vscode
|
||||
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
advanced-example.js
|
||||
docker-compose.yml
|
||||
spec
|
||||
.travis.yml
|
||||
.eslintrc
|
||||
coverage
|
||||
13
.travis.yml
13
.travis.yml
@@ -1,4 +1,13 @@
|
||||
language: node_js
|
||||
node_js:
|
||||
- "6"
|
||||
- "8"
|
||||
- "10"
|
||||
- "12"
|
||||
- "13"
|
||||
notifications:
|
||||
email: false
|
||||
before_install:
|
||||
- npm install prom-client
|
||||
script:
|
||||
- npm test
|
||||
- npm run dtslint
|
||||
|
||||
|
||||
1
Makefile
1
Makefile
@@ -4,6 +4,7 @@ test:
|
||||
./node_modules/jasme/run.js
|
||||
lint:
|
||||
node_modules/eslint/bin/eslint.js src
|
||||
node_modules/.bin/dtslint types
|
||||
coverage:
|
||||
node_modules/istanbul/lib/cli.js cover \
|
||||
-i 'src/*' \
|
||||
|
||||
37
README.md
37
README.md
@@ -4,17 +4,17 @@
|
||||
|
||||
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:
|
||||
|
||||
* `up`: normally is just 1
|
||||
* `http_request_duration_seconds`: http latency histogram labeled with `status_code`, `method` and `path`
|
||||
* `http_request_duration_seconds`: http latency histogram/summary labeled with `status_code`, `method` and `path`
|
||||
|
||||
## Install
|
||||
|
||||
```
|
||||
npm install express-prom-bundle
|
||||
npm install prom-client express-prom-bundle
|
||||
```
|
||||
|
||||
## Sample Usage
|
||||
@@ -46,11 +46,29 @@ 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**
|
||||
* **includePath**: URL path (see important 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
|
||||
* **metricType**: histogram/summary selection. See more details below
|
||||
|
||||
Extra transformation callbacks:
|
||||
|
||||
### metricType option ###
|
||||
|
||||
Two metric types are supported for `http_request_duration_seconds` metric:
|
||||
* [histogram](https://prometheus.io/docs/concepts/metric_types/#histogram) (default)
|
||||
* [summary](https://prometheus.io/docs/concepts/metric_types/#summary)
|
||||
|
||||
Additional options for **histogram**:
|
||||
* **buckets**: buckets used for the `http_request_duration_seconds` histogram
|
||||
|
||||
Additional options for **summary**:
|
||||
* **percentiles**: percentiles used for `http_request_duration_seconds` summary
|
||||
* **ageBuckets**: ageBuckets configures how many buckets we have in our sliding window for the summary
|
||||
* **maxAgeSeconds**: the maxAgeSeconds will tell how old a bucket can be before it is reset
|
||||
|
||||
### Transformation callbacks ###
|
||||
|
||||
* **normalizePath**: `function(req)` or `Array`
|
||||
* if function is provided, then it should generate path value from express `req`
|
||||
@@ -62,13 +80,13 @@ Extra transformation callbacks:
|
||||
* **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:
|
||||
### Other options ###
|
||||
|
||||
* **buckets**: buckets used for `http_request_duration_seconds` histogram
|
||||
* **autoregister**: if `/metrics` endpoint should be registered. (Default: **true**)
|
||||
* **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)
|
||||
* **promRegistry**: Optional `promClient.Registry` instance to attach metrics to. Defaults to global `promClient.register`.
|
||||
|
||||
### More details on includePath option
|
||||
|
||||
@@ -210,7 +228,7 @@ if (cluster.isMaster) {
|
||||
|
||||
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.
|
||||
while replacing all HEX values starting from 5 characters and all IP addresses in the path as #val.
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -227,13 +245,12 @@ while replacing all HEX values starting from 5 characters and all emails in the
|
||||
"buckets": [0.1, 1, 5],
|
||||
"promClient": {
|
||||
"collectDefaultMetrics": {
|
||||
"timeout": 2000
|
||||
}
|
||||
},
|
||||
"urlValueParser": {
|
||||
"minHexLength": 5,
|
||||
"extraMasks": [
|
||||
"^[0-9]+\\.[0-9]+\\.[0-9]+$"
|
||||
"^[0-9]+\\.[0-9]+\\.[0-9]+\\.[0-9]+$"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,9 +11,9 @@ const bundle = promBundle({
|
||||
includePath: true,
|
||||
customLabels: {year: null},
|
||||
transformLabels: labels => Object.assign(labels, {year: new Date().getFullYear()}),
|
||||
metricsPath: '/prometheus',
|
||||
promClient: {
|
||||
collectDefaultMetrics: {
|
||||
timeout: 1000
|
||||
}
|
||||
},
|
||||
urlValueParser: {
|
||||
@@ -29,7 +30,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 +46,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
|
||||
`
|
||||
));
|
||||
|
||||
1665
package-lock.json
generated
1665
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
34
package.json
34
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "express-prom-bundle",
|
||||
"version": "4.2.1",
|
||||
"version": "6.2.0",
|
||||
"description": "express middleware with popular prometheus metrics in one bundle",
|
||||
"main": "src/index.js",
|
||||
"keywords": [
|
||||
@@ -10,33 +10,45 @@
|
||||
"path",
|
||||
"method"
|
||||
],
|
||||
"files": [
|
||||
"src",
|
||||
"types/index.d.ts"
|
||||
],
|
||||
"types": "types",
|
||||
"scripts": {
|
||||
"test": "node_modules/jasme/run.js",
|
||||
"coverage": "make coverage"
|
||||
"coverage": "make coverage",
|
||||
"dtslint": "dtslint types"
|
||||
},
|
||||
"author": "Konstantin Pogorelov <or@pluseq.com>",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"on-finished": "^2.3.0",
|
||||
"prom-client": "~11.1.1",
|
||||
"url-value-parser": "^2.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/express": "^4.16.1",
|
||||
"coveralls": "^3.0.2",
|
||||
"eslint": "^5.3.0",
|
||||
"express": "^4.16.3",
|
||||
"dtslint": "^0.7.1",
|
||||
"eslint": "^5.11.0",
|
||||
"express": "^4.16.4",
|
||||
"istanbul": "^0.4.5",
|
||||
"jasme": "^5.2.0",
|
||||
"koa": "^2.5.2",
|
||||
"koa-connect": "^2.0.0",
|
||||
"supertest": "^3.0.0",
|
||||
"supertest-koa-agent": "^0.3.0"
|
||||
"jasme": "^6.0.0",
|
||||
"koa": "^2.6.2",
|
||||
"koa-connect": "^2.0.1",
|
||||
"prom-client": "^12.0.0",
|
||||
"supertest": "^3.3.0",
|
||||
"supertest-koa-agent": "^0.3.0",
|
||||
"typescript": "^3.4.5"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"prom-client": "^12.0.0"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/jochen-schweizer/express-prom-bundle.git"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
"node": ">=10"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,6 +226,33 @@ describe('index', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('metric type histogram works', done => {
|
||||
const app = express();
|
||||
const bundled = bundle({
|
||||
metricType: 'histogram',
|
||||
buckets: [10, 100],
|
||||
});
|
||||
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(/le="100"/);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('throws on unknown metricType ', () => {
|
||||
expect(() => {
|
||||
bundle({metricType: 'hello'});
|
||||
}).toThrow();
|
||||
});
|
||||
|
||||
describe('usage of normalizePath()', () => {
|
||||
|
||||
it('normalizePath can be replaced gloablly', done => {
|
||||
@@ -352,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) {
|
||||
@@ -378,11 +443,10 @@ describe('index', () => {
|
||||
bundle({
|
||||
promClient: {
|
||||
collectDefaultMetrics: {
|
||||
timeout: 3000
|
||||
}
|
||||
}
|
||||
});
|
||||
expect(spy).toHaveBeenCalledWith({timeout: 3000});
|
||||
expect(spy).toHaveBeenCalledWith({});
|
||||
});
|
||||
|
||||
describe('usage of clusterMetrics()', () => {
|
||||
@@ -407,7 +471,10 @@ describe('index', () => {
|
||||
const agent = supertest(app);
|
||||
|
||||
// create a fake worker, which would not respond in time
|
||||
cluster.workers = [{send: () => {}}];
|
||||
cluster.workers = [{
|
||||
isConnected: () => true,
|
||||
send: () => {}
|
||||
}];
|
||||
|
||||
const errorSpy = spyOn(console, 'error'); // mute console.error
|
||||
|
||||
@@ -420,4 +487,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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
183
src/index.js
183
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,16 +34,6 @@ function clusterMetrics() {
|
||||
}
|
||||
|
||||
function main(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'
|
||||
@@ -75,12 +43,28 @@ function main(opts) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (opts.prefix || opts.keepDefaultMetrics !== undefined) {
|
||||
opts = Object.assign(
|
||||
{
|
||||
autoregister: true,
|
||||
includeStatusCode: true,
|
||||
normalizePath: main.normalizePath,
|
||||
formatStatusCode: main.normalizeStatusCode,
|
||||
metricType: 'histogram',
|
||||
promClient: {},
|
||||
promRegistry: promClient.register
|
||||
}, opts
|
||||
);
|
||||
|
||||
if (opts.prefix
|
||||
|| opts.keepDefaultMetrics !== undefined
|
||||
|| opts.whitelist !== undefined
|
||||
|| opts.blacklist !== undefined
|
||||
) {
|
||||
throw new Error(
|
||||
'express-prom-bundle detected obsolete options:'
|
||||
+ 'prefix and/or keepDefaultMetrics. '
|
||||
'express-prom-bundle detected one of the obsolete options: '
|
||||
+ 'prefix, keepDefaultMetrics, whitelist, blacklist. '
|
||||
+ 'Please refer to oficial docs. '
|
||||
+ 'Most likely you upgraded the module without necessary code changes'
|
||||
+ 'Most likely you upgraded the module without the necessary code changes'
|
||||
);
|
||||
}
|
||||
|
||||
@@ -90,53 +74,66 @@ function main(opts) {
|
||||
|
||||
const httpMetricName = opts.httpDurationMetricName || 'http_request_duration_seconds';
|
||||
|
||||
const metricTemplates = {
|
||||
'up': () => new promClient.Gauge({
|
||||
name: 'up',
|
||||
help: '1 = up, 0 = not up'
|
||||
}),
|
||||
[httpMetricName]: () => {
|
||||
const labels = ['status_code'];
|
||||
if (opts.includeMethod) {
|
||||
labels.push('method');
|
||||
}
|
||||
if (opts.includePath) {
|
||||
labels.push('path');
|
||||
}
|
||||
if (opts.customLabels){
|
||||
labels.push.apply(labels, Object.keys(opts.customLabels));
|
||||
}
|
||||
const metric = new promClient.Histogram({
|
||||
function makeHttpMetric() {
|
||||
const labels = ['status_code'];
|
||||
if (opts.includeMethod) {
|
||||
labels.push('method');
|
||||
}
|
||||
if (opts.includePath) {
|
||||
labels.push('path');
|
||||
}
|
||||
if (opts.customLabels) {
|
||||
labels.push.apply(labels, Object.keys(opts.customLabels));
|
||||
}
|
||||
|
||||
if (opts.metricType === 'summary') {
|
||||
return new promClient.Summary({
|
||||
name: httpMetricName,
|
||||
help: 'duration summary of http responses labeled with: ' + labels.join(', '),
|
||||
labelNames: labels,
|
||||
percentiles: opts.percentiles || [0.5, 0.75, 0.95, 0.98, 0.99, 0.999],
|
||||
maxAgeSeconds: opts.maxAgeSeconds,
|
||||
ageBuckets: opts.ageBuckets,
|
||||
registers: [opts.promRegistry]
|
||||
});
|
||||
} else if (opts.metricType === 'histogram' || !opts.metricType) {
|
||||
return 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]
|
||||
buckets: opts.buckets || [0.003, 0.03, 0.1, 0.3, 1.5, 10],
|
||||
registers: [opts.promRegistry]
|
||||
});
|
||||
return metric;
|
||||
} else {
|
||||
throw new Error('metricType option must be histogram or summary');
|
||||
}
|
||||
};
|
||||
|
||||
const metrics = {};
|
||||
const names = prepareMetricNames(opts, metricTemplates);
|
||||
|
||||
for (let name of names) {
|
||||
metrics[name] = metricTemplates[name]();
|
||||
}
|
||||
|
||||
if (metrics.up) {
|
||||
const metrics = {
|
||||
[httpMetricName]: makeHttpMetric()
|
||||
};
|
||||
|
||||
if (opts.includeUp !== false) {
|
||||
metrics.up = new promClient.Gauge({
|
||||
name: 'up',
|
||||
help: '1 = up, 0 = not up',
|
||||
registers: [opts.promRegistry]
|
||||
});
|
||||
metrics.up.set(1);
|
||||
}
|
||||
|
||||
const metricsMiddleware = function(req, res) {
|
||||
res.writeHead(200, {'Content-Type': 'text/plain'});
|
||||
res.end(promClient.register.metrics());
|
||||
res.end(opts.promRegistry.metrics());
|
||||
};
|
||||
|
||||
const metricsMatch = opts.metricsPath instanceof RegExp ? opts.metricsPath
|
||||
: new RegExp('^' + (opts.metricsPath || '/metrics') + '/?$');
|
||||
|
||||
const middleware = function (req, res, next) {
|
||||
const path = req.originalUrl || req.url; // originalUrl gets lost in koa-connect?
|
||||
let labels;
|
||||
|
||||
if (opts.autoregister && path.match(/^\/metrics\/?$/)) {
|
||||
if (opts.autoregister && path.match(metricsMatch)) {
|
||||
return metricsMiddleware(req, res);
|
||||
}
|
||||
|
||||
@@ -144,42 +141,42 @@ function main(opts) {
|
||||
return next();
|
||||
}
|
||||
|
||||
if (metrics[httpMetricName]) {
|
||||
labels = {};
|
||||
let timer = metrics[httpMetricName].startTimer(labels);
|
||||
onFinished(res, () => {
|
||||
if (opts.includeStatusCode) {
|
||||
labels.status_code = opts.formatStatusCode(res, opts);
|
||||
}
|
||||
if (opts.includeMethod) {
|
||||
labels.method = req.method;
|
||||
}
|
||||
if (opts.includePath) {
|
||||
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();
|
||||
});
|
||||
}
|
||||
const labels = {};
|
||||
const timer = metrics[httpMetricName].startTimer(labels);
|
||||
|
||||
onFinished(res, () => {
|
||||
if (opts.includeStatusCode) {
|
||||
labels.status_code = opts.formatStatusCode(res, opts);
|
||||
}
|
||||
if (opts.includeMethod) {
|
||||
labels.method = req.method;
|
||||
}
|
||||
if (opts.includePath) {
|
||||
labels.path = opts.normalizePath instanceof 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();
|
||||
});
|
||||
|
||||
next();
|
||||
};
|
||||
|
||||
middleware.metricTemplates = metricTemplates;
|
||||
middleware.metrics = metrics;
|
||||
middleware.promClient = promClient;
|
||||
middleware.metricsMiddleware = metricsMiddleware;
|
||||
return middleware;
|
||||
}
|
||||
|
||||
// this is kept only for compatibility with the code relying on older version
|
||||
main.promClient = promClient;
|
||||
|
||||
main.normalizePath = normalizePath;
|
||||
main.normalizeStatusCode = normalizeStatusCode;
|
||||
main.clusterMetrics = clusterMetrics;
|
||||
|
||||
64
types/index.d.ts
vendored
Normal file
64
types/index.d.ts
vendored
Normal file
@@ -0,0 +1,64 @@
|
||||
// TypeScript Version: 2.8
|
||||
|
||||
import { Request, RequestHandler, Response } from 'express';
|
||||
import { DefaultMetricsCollectorConfiguration, Registry } from 'prom-client';
|
||||
|
||||
export {};
|
||||
|
||||
export = express_prom_bundle;
|
||||
|
||||
declare namespace express_prom_bundle {
|
||||
interface Labels {
|
||||
[key: string]: string | number;
|
||||
}
|
||||
|
||||
type NormalizePathEntry = [string | RegExp, string];
|
||||
type NormalizePathFn = (req: Request, opts: Opts) => string;
|
||||
type NormalizeStatusCodeFn = (res: Response) => number | string;
|
||||
type TransformLabelsFn = (labels: Labels, req: Request, res: Response) => void;
|
||||
|
||||
interface Opts {
|
||||
autoregister?: boolean;
|
||||
buckets?: number[];
|
||||
|
||||
customLabels?: { [key: string]: any };
|
||||
|
||||
includeStatusCode?: boolean;
|
||||
includeMethod?: boolean;
|
||||
includePath?: boolean;
|
||||
includeUp?: boolean;
|
||||
|
||||
metricType?: 'summary' | 'histogram';
|
||||
metricsPath?: string;
|
||||
httpDurationMetricName?: string;
|
||||
promClient?: { collectDefaultMetrics?: DefaultMetricsCollectorConfiguration };
|
||||
promRegistry?: Registry;
|
||||
normalizePath?: NormalizePathEntry[] | NormalizePathFn;
|
||||
formatStatusCode?: NormalizeStatusCodeFn;
|
||||
transformLabels?: TransformLabelsFn;
|
||||
|
||||
// https://github.com/disjunction/url-value-parser#options
|
||||
urlValueParser?: {
|
||||
minHexLength?: number;
|
||||
minBase64Length?: number;
|
||||
replaceMasks?: string[];
|
||||
extraMasks?: string[];
|
||||
};
|
||||
}
|
||||
|
||||
interface Middleware extends RequestHandler {
|
||||
metricsMiddleware: RequestHandler;
|
||||
}
|
||||
|
||||
const normalizePath: NormalizePathFn;
|
||||
const normalizeStatusCode: NormalizeStatusCodeFn;
|
||||
|
||||
function clusterMetrics(): RequestHandler;
|
||||
}
|
||||
|
||||
interface express_prom_bundle {
|
||||
normalizePath: express_prom_bundle.NormalizePathFn;
|
||||
normalizeStatusCode: express_prom_bundle.NormalizeStatusCodeFn;
|
||||
}
|
||||
|
||||
declare function express_prom_bundle(opts: express_prom_bundle.Opts): express_prom_bundle.Middleware;
|
||||
79
types/test.ts
Normal file
79
types/test.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import { Request, RequestHandler, Response } from 'express';
|
||||
import * as promClient from 'prom-client';
|
||||
|
||||
import * as promBundle from 'express-prom-bundle';
|
||||
|
||||
// $ExpectType Middleware
|
||||
const middleware: RequestHandler = promBundle({ includeMethod: true });
|
||||
|
||||
// $ExpectType: string
|
||||
middleware.name;
|
||||
|
||||
promClient.register.clear();
|
||||
|
||||
// $ExpectType Middleware
|
||||
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
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
promClient.register.clear();
|
||||
|
||||
// $ExpectType Middleware
|
||||
promBundle({
|
||||
buckets: [0.1, 0.4, 0.7],
|
||||
includeMethod: true,
|
||||
includePath: true,
|
||||
customLabels: { year: null },
|
||||
transformLabels: (labels: promBundle.Labels) => ({
|
||||
...labels,
|
||||
year: new Date().getFullYear()
|
||||
}),
|
||||
metricType: 'summary',
|
||||
metricsPath: '/prometheus',
|
||||
promClient: {
|
||||
collectDefaultMetrics: {
|
||||
}
|
||||
},
|
||||
promRegistry: new promClient.Registry(),
|
||||
urlValueParser: {
|
||||
minHexLength: 5,
|
||||
extraMasks: [
|
||||
'^[0-9]+\\.[0-9]+\\.[0-9]+$' // replace dot-separated dates with #val
|
||||
]
|
||||
},
|
||||
normalizePath: [
|
||||
['^/foo', '/example'] // replace /foo with /example
|
||||
],
|
||||
formatStatusCode: (res: Response) => res.statusCode + 100
|
||||
});
|
||||
|
||||
// TypeScript workaround to write a readonly field
|
||||
type Writable<T> = { -readonly [K in keyof T]: T[K] };
|
||||
const wPromBundle: Writable<promBundle> = promBundle;
|
||||
|
||||
wPromBundle.normalizePath = (req: Request, opts: promBundle.Opts) => {
|
||||
const path = promBundle.normalizePath(req, opts);
|
||||
|
||||
// count all docs as one path, but /docs/login as a separate one
|
||||
return path.match(/^\/docs/) && !path.match(/^\/login/) ? '/docs/*' : path;
|
||||
};
|
||||
|
||||
wPromBundle.normalizeStatusCode = (res: Response) => res.statusCode.toString();
|
||||
|
||||
// $ExpectType RequestHandler
|
||||
promBundle.clusterMetrics();
|
||||
|
||||
// Missing test
|
||||
// const stringReturn: string = promBundle.normalizePath({}, {});
|
||||
14
types/tsconfig.json
Normal file
14
types/tsconfig.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"lib": ["es6"],
|
||||
"noImplicitAny": true,
|
||||
"noImplicitThis": true,
|
||||
"strictNullChecks": true,
|
||||
"strictFunctionTypes": true,
|
||||
"baseUrl": ".",
|
||||
"paths": { "express-prom-bundle": ["."] },
|
||||
"noEmit": true,
|
||||
"forceConsistentCasingInFileNames": true
|
||||
}
|
||||
}
|
||||
3
types/tslint.json
Normal file
3
types/tslint.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": "dtslint/dtslint.json"
|
||||
}
|
||||
Reference in New Issue
Block a user