Compare commits

...

18 Commits
2.2.0 ... 3.3.0

Author SHA1 Message Date
Konstantin Pogorelov
06f55c9ab8 add promClient option for smuggling collectDefaultMetrics, upgrade prom-client to ~10.2.2, version 3.3.0 2018-01-23 16:43:38 +01:00
Konstantin Pogorelov
c8996a7730 version 3.2.1 2017-10-19 20:26:56 +02:00
Konstantin Pogorelov
aed7edc684 remove fixed version of prom-client from readme 2017-10-19 20:25:40 +02:00
Muhammad Saiful Islam
4a840cfce6 minor typo (#13) 2017-10-19 20:23:36 +02:00
Konstantin Pogorelov
d12248dcaf upgrade prom-client, update package-lock.json 2017-09-23 12:31:12 +02:00
Konstantin Pogorelov
68eb617f88 version 3.2.0 2017-09-23 12:20:12 +02:00
Konstantin Pogorelov
bb0e453078 extends docs in regards of: customLabels, transformLabels, httpDurationMetricName 2017-09-23 12:19:56 +02:00
Konstantin Pogorelov
b0928b1b94 make template name consistent with httpDurationMetricName, fix typo in variable name 2017-09-23 12:18:00 +02:00
Konstantin Pogorelov
26f9f5ca10 add customLabels and transformLabels to advanced example 2017-09-23 12:13:14 +02:00
Konstantin Pogorelov
a261ab76a3 split includeCustomLabels into customLabels and transformLabels 2017-09-23 12:03:23 +02:00
Konstantin Pogorelov
8318e0ec1d fix typo 2017-09-23 11:49:13 +02:00
Konstantin Pogorelov
d58b434f0a fix codestyle 2017-09-23 11:35:30 +02:00
Eric Uldall
47fd051aba added customLabels feature to main() 2017-09-22 15:21:32 -07:00
Konstantin Pogorelov
313673f16f inc minor version, update README 2017-07-11 13:21:06 +02:00
Jonathan Creamer
bbd62e34d6 Upgrade prom-client... again (#9)
* Update package.json

* Update call to Histogram

* Update package.json

* Update index.js

* Update index.js
2017-07-11 11:41:40 +02:00
Konstantin Pogorelov
b8ba87009e extend unittests, simplify API, update README, use npm5 and package-lock.json, add node7 to travis config 2017-06-04 16:31:26 +02:00
Konstantin Pogorelov
1cc588c2da stop supporting node v4 and v5 2017-05-31 16:32:45 +02:00
Konstantin Pogorelov
5b1517ca91 #7 upgrade dependencies, workaround for koa-connect, update docs 2017-05-31 15:27:08 +02:00
11 changed files with 2859 additions and 122 deletions

View File

@@ -1,5 +1,4 @@
language: node_js
node_js:
- "6"
- "5"
- "4"
- "7"

View File

@@ -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 */);
@@ -44,17 +42,38 @@ See the example below.
## 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)` generates path values from express `req` (see details below)
* **formatStatusCode**: `function(res)` producing final status code from express `res` object, e.g. you can combine `200`, `201` and `204` to just `2xx`.
* **transformLabels**: `function(labels, req, res)` transforms the **labels** object, e.g. setting dynamic values to **customLabels**
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)
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
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.
@@ -65,16 +84,14 @@ normalized to `/user/#val/profile` and that will become the value for the label.
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`.
```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;
@@ -116,15 +133,15 @@ 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 */);
@@ -157,8 +174,32 @@ Here is meddleware config sample, which can be used in a standard **kraken.js**
## Changelog
* **3.3.0**
* added option **promClient** to be able to call collectDefaultMetrics
* upgrade **prom-client** to ~10.2.2 (switch to semver "approximately")
* **3.2.0**
* added options **customLabels**, **transformLabels**
* upgrade **prom-client** to 10.1.0
* **3.1.0**
* upgrade **prom-client** to 10.0.0
* **3.0.0**
* upgrade dependencies, most notably **prom-client** to 9.0.0
* switch to koa v2 in koa unittest
* only node v6 or higher is supported (stop supporting node v4 and v5)
* switch to npm5 and use package-lock.json
* options added: includeStatusCode, formatStatusCode
* **2.1.0**
* deprecate **excludeRoutes**, use **req.originalPath** instead of **req.path**
* deprecate **excludeRoutes**, use **req.originalUrl** 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/

View File

@@ -4,22 +4,24 @@ 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
}
}
});
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) => {

2581
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "express-prom-bundle",
"version": "2.2.0",
"version": "3.3.0",
"description": "express middleware with popular prometheus metrics in one bundle",
"main": "src/index.js",
"keywords": [
@@ -17,22 +17,25 @@
"license": "MIT",
"dependencies": {
"on-finished": "^2.3.0",
"prom-client": "^6.3.0",
"prom-client": "~10.2.2",
"url-value-parser": "^1.0.0"
},
"devDependencies": {
"coveralls": "^2.11.15",
"eslint": "^3.11.1",
"express": "^4.14.0",
"coveralls": "^2.13.1",
"eslint": "^3.19.0",
"express": "^4.15.3",
"istanbul": "^0.4.5",
"jasme": "^5.2.0",
"koa": "^1.2.4",
"koa-connect": "^1.0.0",
"supertest": "^2.0.1",
"koa": "^2.2.0",
"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"
}
}

View File

@@ -4,7 +4,7 @@
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');
@@ -34,6 +34,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 +163,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,7 +185,7 @@ describe('index', () => {
});
});
it('normalizePath can be replaced', done => {
it('normalizePath can be replaced gloablly', done => {
const app = express();
const original = bundle.normalizePath;
bundle.normalizePath = () => 'dummy';
@@ -193,18 +209,127 @@ describe('index', () => {
});
});
it('normalizePath can be overridden', done => {
const app = express();
const instance = bundle({
includePath: true,
normalizePath: req => req.originalUrl + '-suffixed'
});
app.use(instance);
app.use('/test', (req, res) => res.send('it worked'));
const agent = supertest(app);
agent
.get('/test')
.end(() => {
agent
.get('/metrics')
.end((err, res) => {
expect(res.status).toBe(200);
expect(res.text).toMatch(/"\/test-suffixed"/m);
done();
});
});
});
it('formatStatusCode can be overridden', done => {
const app = express();
const instance = bundle({
formatStatusCode: () => 555
});
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(/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 +343,16 @@ describe('index', () => {
});
});
});
it('calls promClient.collectDefaultMetrics', () => {
const spy = spyOn(promClient, 'collectDefaultMetrics');
bundle({
promClient: {
collectDefaultMetrics: {
timeout: 3000
}
}
});
expect(spy).toHaveBeenCalledWith({timeout: 3000});
});
});

View File

@@ -4,25 +4,8 @@
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');
});
});

View File

@@ -4,20 +4,9 @@
const normalizeStatusCode = require('../src/normalizeStatusCode');
describe('normalizeStatusCode', () => {
it('returns original if disabled in opts', () => {
expect(
normalizeStatusCode({status_code: 404}, {normalizeStatusCode: false})
).toBe(404);
});
it('returns run callback if configured', () => {
expect(
normalizeStatusCode(
{status_code: 500},
{
formatStatusCode: res => String(res.status_code).slice(0, -2) + 'xx'
}
)
).toBe('5xx');
normalizeStatusCode({status_code: 500})
).toBe(500);
});
});

View File

@@ -39,7 +39,16 @@ function prepareMetricNames(opts, metricTemplates) {
}
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'
@@ -58,14 +67,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');
@@ -73,14 +86,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;
}
};
@@ -102,7 +116,7 @@ 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\/?$/)) {
@@ -113,21 +127,24 @@ function main(opts) {
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, () => {
if (opts.normalizeStatusCode) {
labels.status_code = main.normalizeStatusCode(res, opts);
} else {
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 = opts.normalizePath(req, opts);
}
if (opts.customLabels) {
Object.assign(labels, opts.customLabels);
}
if (opts.transformLabels) {
opts.transformLabels(labels, req, res);
}
timer();
});

View File

@@ -4,20 +4,11 @@ const UrlValueParser = require('url-value-parser');
const url = require('url');
let urlValueParser;
module.exports = function(req, opts) {
opts = opts || {};
module.exports = function(req) {
// 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;
if (opts.normalizePath !== undefined && !opts.normalizePath) {
return path;
}
if (typeof opts.normalizePath === 'function') {
return opts.normalizePath(req, opts);
}
const path = url.parse(req.originalUrl || req.url).pathname;
if (!urlValueParser) {
urlValueParser = new UrlValueParser();

View File

@@ -1,11 +1,5 @@
'use strict';
module.exports = function(res, opts) {
opts = opts || {};
if (typeof opts.formatStatusCode === 'function') {
return opts.formatStatusCode(res, opts);
}
return res.status_code;
module.exports = function(res) {
return res.status_code || res.statusCode;
};