Compare commits

...

24 Commits
4.2.1 ... 5.0.2

Author SHA1 Message Date
Konstantin Pogorelov
66ef4e3176 bump 5.0.2 2018-12-24 12:50:44 +01:00
Konstantin Pogorelov
f55d397a31 disable notifications in travis-ci 2018-12-24 12:50:25 +01:00
Konstantin Pogorelov
68ad108f77 correct sample usage 2018-12-24 12:49:41 +01:00
Konstantin Pogorelov
6000056f7a bump 5.0.1 2018-12-24 02:26:14 +01:00
Konstantin Pogorelov
48856afbe3 add .vscode and Makefile to .npmignore 2018-12-24 02:18:48 +01:00
Konstantin Pogorelov
e035e2b991 add before_install to .travis.yml for peer dependency 2018-12-24 02:12:03 +01:00
Konstantin Pogorelov
c6d5964768 add metricPath and includeUp options, make whitelist/blacklist obsolete, simnplify code, update unit tests and code, add node10 to .travis.yml 2018-12-24 02:06:16 +01:00
Konstantin Pogorelov
94722e908b make prom-client a peer dependency, upgrade all packages, bump 5.0.0 2018-12-23 21:39:00 +01:00
Konstantin Pogorelov
00b8369329 Merge branch 'develop' 2018-12-23 18:48:10 +01:00
Konstantin Pogorelov
59221f891b bump 4.3.0 2018-12-23 18:41:02 +01:00
Konstantin Pogorelov
3a0b2caf61 upgrade prom-cliet to 11.2.1 (latest) 2018-12-23 18:40:28 +01:00
Konstantin Pogorelov
e7d004f0cc rename metricsType -> metricType, move corresponding readme block 2 paragraphs lower 2018-12-23 16:45:38 +01:00
Chen Li
0dd3116f23 Feature/add metrics type summary (#24)
* add metric type summary

* add tests for percentile option

* throw errors for unknown metricType

* set histogram as default metrics type
2018-12-23 16:36:47 +01:00
Konstantin Pogorelov
6054824c67 Merge branch 'master' into develop 2018-08-25 14:43:58 +02:00
Konstantin Pogorelov
01c78bcc1d Merge branch 'master' into develop 2018-08-25 13:10:39 +02:00
Konstantin Pogorelov
bc65dc45cb #16 fix typo in docs 2018-08-25 12:57:15 +02:00
Konstantin Pogorelov
8cae5b6ef3 fix jasme path (again) 2018-08-25 12:52:38 +02:00
Konstantin Pogorelov
2003e7743f #16 fix unit test (cluster_metrics/metrics_cluster), cover the error branch 2018-08-25 12:44:24 +02:00
Konstantin Pogorelov
568c87216a #16 remove unnecessary check for existance after new, break on error with 500 2018-08-25 12:42:53 +02:00
Konstantin Pogorelov
98be36244e #16 make the cluster example runnable 2018-08-25 12:40:15 +02:00
Konstantin Pogorelov
6ff1204db4 revert way jasme was called, add npm run coverage 2018-08-25 09:49:28 +02:00
Konstantin Pogorelov
f71d837660 fix newlines in index.spec.js 2018-08-25 09:45:09 +02:00
Konstantin Pogorelov
4aa2bfa6ae update package-lock.json 2018-08-24 23:48:22 +02:00
Adam Yost
1fff877787 Add support for clusterMaster option re: #16 (#17)
* Add support for clusterMaster option re: #16
* Add Cluster instructions to README
* Use the approach recommended in PR
* use console.error for errors
* Update with new method signature
* add code coverage for new clusterMetrics middleware
2018-08-24 23:36:23 +02:00
9 changed files with 700 additions and 864 deletions

2
.gitignore vendored
View File

@@ -1,3 +1,5 @@
.npmrc
node_modules
coverage
/.vscode

View File

@@ -4,3 +4,5 @@ spec
.travis.yml
.eslintrc
coverage
.vscode
Makefile

View File

@@ -2,3 +2,8 @@ language: node_js
node_js:
- "6"
- "8"
- "10"
notifications:
email: false
before_install:
- npm install prom-client

View File

@@ -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
@@ -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:
@@ -62,9 +64,14 @@ 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**
Metric type:
* **metricType**: two metric types are supported for `http_request_duration_seconds` metric: [histogram](https://prometheus.io/docs/concepts/metric_types/#histogram) and [summary](https://prometheus.io/docs/concepts/metric_types/#summary), default: **histogram**
Other options:
* **buckets**: buckets used for `http_request_duration_seconds` histogram
* **percentiles**: percentiles used for `http_request_duration_seconds` summary
* **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,
@@ -210,7 +217,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
{
@@ -233,7 +240,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]+$"
"^[0-9]+\\.[0-9]+\\.[0-9]+\\.[0-9]+$"
]
}
}

View File

@@ -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
`
));

1147
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "express-prom-bundle",
"version": "4.2.1",
"version": "5.0.2",
"description": "express middleware with popular prometheus metrics in one bundle",
"main": "src/index.js",
"keywords": [
@@ -18,20 +18,22 @@
"license": "MIT",
"dependencies": {
"on-finished": "^2.3.0",
"prom-client": "~11.1.1",
"url-value-parser": "^2.0.0"
},
"devDependencies": {
"coveralls": "^3.0.2",
"eslint": "^5.3.0",
"express": "^4.16.3",
"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",
"jasme": "^6.0.0",
"koa": "^2.6.2",
"koa-connect": "^2.0.1",
"supertest": "^3.3.0",
"supertest-koa-agent": "^0.3.0"
},
"peerDependencies": {
"prom-client": "^11.1.2"
},
"repository": {
"type": "git",
"url": "https://github.com/jochen-schweizer/express-prom-bundle.git"

View File

@@ -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) {
@@ -420,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();
});
});
});
});
});

View File

@@ -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,27 @@ 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: {}
}, 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,40 +73,46 @@ 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]
});
} 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]
});
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'
});
metrics.up.set(1);
}
@@ -132,11 +121,13 @@ function main(opts) {
res.end(promClient.register.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 +135,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;