Compare commits

...

50 Commits
1.2.3 ... 4.2.1

Author SHA1 Message Date
Konstantin Pogorelov
0606bf5f4f remove a duplicate cluster docs piece, versio 4.2.1 2018-08-26 14:00:25 +02:00
Konstantin Pogorelov
ec9835270f version 4.2.0 2018-08-25 13:11:31 +02:00
Konstantin Pogorelov
1cf8c86acb Squashed commit of the following:
commit bc65dc45cb
Author: Konstantin Pogorelov <or@pluseq.com>
Date:   Sat Aug 25 12:57:15 2018 +0200

    #16 fix typo in docs

commit 2003e7743f
Author: Konstantin Pogorelov <or@pluseq.com>
Date:   Sat Aug 25 12:44:24 2018 +0200

    #16 fix unit test (cluster_metrics/metrics_cluster), cover the error branch

commit 568c87216a
Author: Konstantin Pogorelov <or@pluseq.com>
Date:   Sat Aug 25 12:42:53 2018 +0200

    #16 remove unnecessary check for existance after new, break on error with 500

commit 98be36244e
Author: Konstantin Pogorelov <or@pluseq.com>
Date:   Sat Aug 25 12:40:15 2018 +0200

    #16 make the cluster example runnable
2018-08-25 13:07:36 +02:00
Konstantin Pogorelov
43b51d4ab1 fix jasme path (again) 2018-08-25 12:58:44 +02:00
Konstantin Pogorelov
f6e87b7697 revert way jasme was called, add npm run coverage 2018-08-25 12:58:34 +02:00
Konstantin Pogorelov
3be8d53a27 fix newlines in index.spec.js 2018-08-25 12:58:24 +02:00
Konstantin Pogorelov
34e6a21be1 update package-lock.json 2018-08-25 12:58:20 +02:00
Adam Yost
02fcda4721 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-25 12:58:09 +02:00
Konstantin Pogorelov
5e0cd75673 add normalizePath option as tuple array, improve docs and advanced example, version to 4.1.0 2018-08-09 11:27:04 +02:00
Konstantin Pogorelov
d292dcab33 remove changelog from readme as it duplicates github releases function 2018-08-08 12:00:29 +02:00
Konstantin Pogorelov
84f99cc49c add urlValueParser option, update docs accordingly 2018-08-08 11:23:27 +02:00
Konstantin Pogorelov
fffe35ce5e update dependencies, use url-value-parser v2 2018-08-08 11:22:03 +02:00
Konstantin Pogorelov
06341c227a wip: 4.0.0, prom-client to v11, url-value-parser to v2 2018-08-07 17:55:25 +02:00
Konstantin Pogorelov
45b8f373be include collectDefaultMetrics in kraken example 2018-01-26 17:40:05 +01:00
Konstantin Pogorelov
4ee269faee make test more specific 2018-01-23 16:58:49 +01:00
Konstantin Pogorelov
e4d6113ff2 replace node 7 with node 8 in .travis.yml 2018-01-23 16:52:12 +01:00
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
Konstantin Pogorelov
5b1aa494cb version 2.2.0 - normalizeStatus draft 2017-03-28 17:14:33 +02:00
The Experimentalist
65549a769b Merge pull request #4 from PauloPaquielli/feature/normalizeStatusCode 2017-03-28 17:08:57 +02:00
Paulo Duarte
de83ac09a0 Remove if unnecessary 2017-03-27 17:35:13 -03:00
Paulo Duarte
52865dfb02 Back version to prom-client 2017-03-27 17:17:59 -03:00
Paulo Duarte
5c6ed64a31 Fix lint error 2017-03-27 17:16:51 -03:00
Paulo Duarte
d8c6492163 Implement test 2017-03-27 12:03:53 -03:00
Paulo Duarte
c92b85ae96 Update prom-client 2017-03-24 11:08:29 -03:00
Paulo Duarte
48f8b992fd Make normalizeStatusCode generic 2017-03-24 02:18:41 -03:00
Paulo Duarte
61e4343a8c Implements group in status code metrics 2017-03-23 14:12:29 -03:00
Konstantin Pogorelov
7b89690d3b add missing spaces to readme 2017-01-19 17:17:08 +01:00
Konstantin Pogorelov
40db5cacbd Merge branch 'master' of github.com:jochen-schweizer/express-prom-bundle
Conflicts:
	README.md
	package.json
2017-01-19 16:54:58 +01:00
Konstantin Pogorelov
43334b923f deprecate excludeRoutes, use originalUrl when matching own /metrics path, readme adjustments, version 2.1.0 2017-01-19 16:52:15 +01:00
Konstantin Pogorelov
20eb668e36 export and make replaceable normalizePath(), fix/extend readme, version 2.0.2 2017-01-04 23:41:09 +01:00
Konstantin Pogorelov
53c4505378 fix typo in the readme 2016-12-11 20:41:40 +01:00
Konstantin Pogorelov
b0aa05d42b fixes, polishing, docs update, bump to 2.0.0 2016-12-11 20:22:55 +01:00
Konstantin Pogorelov
1e9300ebf3 niterim state for 2.0 development 2016-12-08 18:13:13 +01:00
16 changed files with 3593 additions and 649 deletions

View File

@@ -1,42 +1,54 @@
{
"env": {
"node": true,
"es6": true
},
"parserOptions": {
"ecmaVersion": 6,
"sourceType": "module"
},
"extends": "eslint:recommended",
"globals": {
"app": true,
"fetch": true
},
"rules": {
"no-cond-assign": 0,
"no-constant-condition": 0,
"no-empty": 0,
"no-fallthrough": 0,
"no-unused-vars": 1,
"no-console": 1,
"env": {
"node": true,
"es6": true
},
"semi": 2,
"curly": 2,
"consistent-this": [2, "self"],
"indent": [ 2, 4, { "SwitchCase": 1 } ],
"linebreak-style": [2, "unix"],
"no-nested-ternary": 2,
"extends": "eslint:recommended",
"new-parens": 2,
"no-dupe-class-members": 2,
"require-yield": 2,
"arrow-spacing": 1,
"no-var": 2,
"rules": {
"array-bracket-spacing": [2, "never"],
"block-scoped-var": 2,
"brace-style": [2, "1tbs"],
"computed-property-spacing": [2, "never"],
"curly": 2,
"eol-last": 2,
"eqeqeq": [2, "smart"],
"max-depth": [1, 3],
"new-cap": 1,
"no-extend-native": 2,
"no-mixed-spaces-and-tabs": 2,
"no-trailing-spaces": 1,
"no-unused-vars": 1,
"no-use-before-define": [2, "nofunc"],
"object-curly-spacing": [2, "never"],
"quotes": [1, "single", "avoid-escape"],
"semi": [2, "always"],
"keyword-spacing": [2, {"before": true, "after": true}],
"space-unary-ops": 2,
"no-console": [1, { allow: ["info", "warn", "error"] }],
"no-multi-spaces": 1,
"space-return-throw-case": 0,
"space-infix-ops": [1, {"int32Hint": false}],
"brace-style": 1,
"space-before-blocks": 1,
"operator-linebreak": [1, "before"],
"no-unneeded-ternary": 1,
"no-lonely-if": 1,
"key-spacing": 1,
"quotes": [1, "double", "avoid-escape"],
"no-trailing-spaces": [1, { "skipBlankLines": true }]
}
"max-len": [1, 120],
"max-statements": [1, 50],
"consistent-this": [2, "self"],
"no-var": 2,
"no-dupe-class-members": 2,
"operator-linebreak": [1, "before"],
"no-unneeded-ternary": [1, {"defaultAssignment": false}],
"no-lonely-if": 1,
"linebreak-style": [2, "unix"],
"no-nested-ternary": 2,
"require-yield": 2
}
}

View File

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

212
README.md
View File

@@ -2,15 +2,14 @@
# 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
Included metrics:
* `up`: normally is just 1
* `nodejs_memory_heap_total_bytes` and `nodejs_memory_heap_used_bytes`
* `http_request_seconds`: http latency histogram labeled with `status_code`
* `http_request_duration_seconds`: http latency histogram labeled with `status_code`, `method` and `path`
## Install
@@ -18,12 +17,12 @@ Included metrics:
npm install express-prom-bundle
```
## Usage
## Sample Usage
```javascript
const promBundle = require("express-prom-bundle"),
metricsMiddleware = promBundle({/* options */ }),
app = require("express")();
const promBundle = require("express-prom-bundle");
const app = require("express")();
const metricsMiddleware = promBundle({includeMethod: true});
app.use(metricsMiddleware);
app.use(/* your middleware */);
@@ -35,7 +34,7 @@ app.listen(3000);
**ALERT!**
The order in wich the routes are registered is important, since
The order in which the routes are registered is important, since
**only the routes registered after the express-prom-bundle will be measured**
You can use this to your advantage to bypass some of the routes.
@@ -43,28 +42,88 @@ See the example below.
## Options
* **prefix**: prefix added to every metric name
* **whitelist**, **blacklist**: array of strings or regexp specifying which metrics to include/exclude
* **buckets**: buckets used for `http_request_seconds` histogram
* **includeMethod**: include HTTP method (GET, PUT, ...) as a label to `http_request_seconds`
* **includePath**: include URL path as a label - **EXPERIMENTAL!** (see below)
* **normalizePath**: boolean or `function(req)` - path normalization for `includePath` option
* **excludeRoutes**: array of strings or regexp specifying which routes should be skipped for `http_request_seconds` metric. It uses `req.path` as subject when checking
* **autoregister**: if `/metrics` endpoint should be registered. It is (Default: **true**)
* **keepDefaultMetrics**: if default metrics provided by **prom-client** should be probed and delivered. (Default: **false**)
Which labels to include in `http_request_duration_seconds` metric:
### includePath option
* **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.
The goal is to have separate latency statistics by URL path, e.g. `/my-app/user/`, `/products/by-category` etc.
Extra transformation callbacks:
But 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 automatically 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.
* **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**
You can override this magical behavior and create define your own function by providing an optional callback **normalizePath**.
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)
### More details on includePath option
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 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 = promBundle.normalizePath;
promBundle.normalizePath = (req, opts) => {
const path = originalNormalize(req, opts);
// count all docs as one path, but /docs/login as a separate one
return (path.match(/^\/docs/) && !path.match(/^\/login/)) ? '/docs/*' : path;
};
```
For more details:
* [url-value-parser](https://www.npmjs.com/package/url-value-parser) - magic behind automatic path normalization
* [normalizePath.js](https://github.com/jochen-schweizer/express-prom-bundle/blob/master/src/normalizePath.js) - source code for path processing, for you
* [normalizePath.js](https://github.com/jochen-schweizer/express-prom-bundle/blob/master/src/normalizePath.js) - source code for path processing
## express example
@@ -72,23 +131,20 @@ For more details:
setup std. metrics but exclude `up`-metric:
```javascript
"use strict";
const express = require("express"),
app = express(),
promBundle = require("express-prom-bundle");
const express = require("express");
const app = express();
const promBundle = require("express-prom-bundle");
// calls to this route will not appear in metrics
// because it's applied before promBundle
app.get("/status", (req, res) => res.send("i am healthy"));
app.use(promBundle({
prefix: "demo_app:something",
excludeRoutes: ["/foo"]
}));
// register metrics collection for all routes
// ... except those starting with /foo
app.use("/((?!foo))*", promBundle({includePath: true}));
// this call will NOT appear in metrics, because it matches excludeRoutes
// this call will NOT appear in metrics,
// because express will skip the metrics middleware
app.get("/foo", (req, res) => res.send("bar"));
// calls to this route will appear in metrics
@@ -99,26 +155,94 @@ 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"),
koa = require("koa"),
c2k = require("koa-connect"),
metricsMiddleware = promBundle({/* options */ });
const promBundle = require("express-prom-bundle");
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);
```
## Changelog
## using with cluster
* **1.2.1**
* upgrade prom-client to 6.1.2
* add options: includeMethod, includePath, keepDefaultMetrics
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.
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
{
"middleware": {
"expressPromBundle": {
"route": "/((?!status|favicon.ico|robots.txt))*",
"priority": 0,
"module": {
"name": "express-prom-bundle",
"arguments": [
{
"includeMethod": true,
"includePath": true,
"buckets": [0.1, 1, 5],
"promClient": {
"collectDefaultMetrics": {
"timeout": 2000
}
},
"urlValueParser": {
"minHexLength": 5,
"extraMasks": [
"^[0-9]+\\.[0-9]+\\.[0-9]+$"
]
}
}
]
}
}
}
}
```
## License

View File

@@ -1,45 +1,55 @@
"use strict";
'use strict';
const express = require("express");
const express = require('express');
const app = express();
const promBundle = require("express-prom-bundle");
const promBundle = require('express-prom-bundle');
const bundle = promBundle({
prefix: "demo_app:something:",
blacklist: [/up/],
buckets: [0.1, 0.4, 0.7],
includeMethod: true,
includePath: true,
keepDefaultMetrics: false
buckets: [0.1, 0.4, 0.7],
includeMethod: 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);
// create metric using factory (w/ prefix)
const c2 = bundle.factory.newCounter("c2", "c2 help");
c2.inc(20);
app.get("/foo/:id", (req, res) => {
setTimeout(() => {
res.send("foo response\n");
}, 500);
app.get('/foo/:id', (req, res) => {
setTimeout(() => {
res.send('foo response\n');
}, 500);
});
app.delete("/foo/:id", (req, res) => {
setTimeout(() => {
res.send("foo deleted\n");
}, 300);
app.delete('/foo/:id', (req, res) => {
setTimeout(() => {
res.send('foo deleted\n');
}, 300);
});
app.get("/bar", (req, res) => res.send("bar response\n"));
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 -X DELETE localhost:3000/foo/5432\n"
+ "curl localhost:3000/bar\n"
+ "curl localhost:3000/metrics\n"
'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

File diff suppressed because it is too large Load Diff

View File

@@ -1,37 +1,42 @@
{
"name": "express-prom-bundle",
"version": "1.2.3",
"version": "4.2.1",
"description": "express middleware with popular prometheus metrics in one bundle",
"main": "src/index.js",
"keywords": [
"prometheus",
"metrics",
"express",
"bundle"
"path",
"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.2.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"
}
}

View File

@@ -1,65 +0,0 @@
"use strict";
/* eslint-env jasmine */
const PromFactory = require("../src/PromFactory");
describe("PromFactory", () => {
let factory;
beforeEach(() => {
factory = new PromFactory();
});
it("creates Counter", () => {
const metric = factory.newCounter(
"test1",
"help for test1",
["label1", "label2"]
);
expect(metric.name).toBe("test1");
expect(metric.help).toBe("help for test1");
expect(metric.labelNames).toEqual(["label1", "label2"]);
});
it("creates Gauge", () => {
const metric = factory.newGauge(
"test2",
"help for test2",
["label1", "label2"]
);
expect(metric.name).toBe("test2");
expect(metric.help).toBe("help for test2");
expect(metric.labelNames).toEqual(["label1", "label2"]);
});
it("creates Histogram with labels", () => {
const metric = factory.newHistogram(
"test3",
"help for test3",
["label1", "label2"],
{buckets: [1, 2, 3]}
);
expect(metric.name).toBe("test3");
expect(metric.help).toBe("help for test3");
expect(metric.labelNames).toEqual(["label1", "label2"]);
expect(metric.bucketValues).toEqual({"1": 0, "2": 0, "3": 0});
});
it("creates Summary without labels", () => {
const metric = factory.newSummary(
"test4",
"help for test4",
{percentiles: [0.1, 0.5]}
);
expect(metric.name).toBe("test4");
expect(metric.help).toBe("help for test4");
expect(metric.percentiles).toEqual([0.1, 0.5]);
});
it("when regsitered with same name, just return old instance", () => {
const metric1 = factory.newSummary(
"test4",
"help for test4",
{percentiles: [0.1, 0.5]}
);
const metric2 = factory.newSummary(
"test4",
"help for test4",
{percentiles: [0.1, 0.5]}
);
expect(metric1).toBe(metric2);
});
});

423
spec/index.spec.js Normal file
View File

@@ -0,0 +1,423 @@
'use strict';
/* eslint-env jasmine */
const express = require('express');
const supertest = require('supertest');
const bundle = require('../');
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(() => {
promClient.register.clear();
});
it('metrics returns up=1', done => {
const app = express();
const bundled = bundle({
whitelist: ['up']
});
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({
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({
whitelist: ['up']
});
app.use(bundled);
const agent = supertest(app);
agent.get('/metrics')
.end((err, res) => {
expect(res.status).toBe(200);
done();
});
});
it('metrics can be attached to /metrics programatically', done => {
const app = express();
const bundled = bundle({
autoregister: false
});
app.use(bundled.metricsMiddleware);
app.use(bundled);
app.use('/test', (req, res) => res.send('it worked'));
const agent = supertest(app);
agent.get('/metrics')
.end((err, res) => {
expect(res.status).toBe(200);
done();
});
});
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);
supertest(app)
.get('/metrics')
.end((err, res) => {
expect(res.status).toBe(500);
done();
});
});
it('http latency gets counted', done => {
const app = express();
const instance = bundle();
app.use(instance);
app.use('/test', (req, res) => res.send('it worked'));
const agent = supertest(app);
agent
.get('/test')
.end(() => {
const metricHashMap = instance.metrics.http_request_duration_seconds.hashMap;
expect(metricHashMap['status_code:200']).toBeDefined();
const labeled = metricHashMap['status_code:200'];
expect(labeled.count).toBe(1);
agent
.get('/metrics')
.end((err, res) => {
expect(res.status).toBe(200);
done();
});
});
});
it('filters out the excludeRoutes', done => {
const app = express();
const instance = bundle({
excludeRoutes: ['/test']
});
app.use(instance);
app.use('/test', (req, res) => res.send('it worked'));
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();
});
});
});
it('complains about deprecated options', () => {
expect(() => bundle({prefix: 'hello'})).toThrow();
});
it('tolerates includePath, includeMethod, includeCustomLabels', done => {
const app = express();
const instance = bundle({
includePath: true,
includeMethod: true,
includeCustomLabels: {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);
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 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 = new Koa();
const bundled = bundle({
whitelist: ['up']
});
app.use(c2k(bundled));
app.use(function(ctx, next) {
return next().then(() => ctx.body = 'it worked');
});
const agent = supertestKoa(app);
agent.get('/test').end(() => {
agent
.get('/metrics')
.end((err, res) => {
expect(res.status).toBe(200);
expect(res.text).toMatch(/^up\s1/m);
done();
});
});
});
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);
});
});

View File

@@ -1,231 +0,0 @@
"use strict";
/* eslint-env jasmine */
const express = require("express");
const supertest = require("supertest");
const bundle = require("../");
const koa = require("koa");
const c2k = require("koa-connect");
const supertestKoa = require("supertest-koa-agent");
const promClient = require("prom-client");
// had to reinvent, because getSingleMetric() is still not in npm
function myGetSingleMetric(name) {
let returnMetric;
promClient.register.getMetricsAsJSON().forEach(metric => {
if (metric.name === name) {
returnMetric = metric;
}
});
return returnMetric;
}
describe("index", () => {
beforeEach(() => {
promClient.register.clear();
});
it("metrics returns up=1", done => {
const app = express();
const bundled = bundle({
prefix: "hello:",
whitelist: ["up"]
});
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(/hello:up\s1/);
done();
});
});
});
it("metrics should be attached to /metrics by default", done => {
const app = express();
const bundled = bundle({
prefix: "hello:",
whitelist: ["up"]
});
app.use(bundled);
const agent = supertest(app);
agent.get("/metrics")
.end((err, res) => {
expect(res.status).toBe(200);
done();
});
});
it("metrics can be attached to /metrics programatically", done => {
const app = express();
const bundled = bundle({
autoregister: false
});
app.use(bundled.metricsMiddleware);
app.use(bundled);
app.use("/test", (req, res) => res.send("it worked"));
const agent = supertest(app);
agent.get("/metrics")
.end((err, res) => {
expect(res.status).toBe(200);
done();
});
});
it("metrics can be filtered using exect match", () => {
const instance = bundle({blacklist: ["up"]});
expect(instance.metrics.up).not.toBeDefined();
expect(instance.metrics.nodejs_memory_heap_total_bytes).toBeDefined();
});
it("metrics can be filtered using regex", () => {
const instance = bundle({blacklist: [/memory/]});
expect(instance.metrics.up).toBeDefined();
expect(instance.metrics.nodejs_memory_heap_total_bytes).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_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);
supertest(app)
.get("/metrics")
.end((err, res) => {
expect(res.status).toBe(500);
done();
});
});
it("http latency gets counted", done => {
const app = express();
const instance = bundle();
app.use(instance);
app.use("/test", (req, res) => res.send("it worked"));
const agent = supertest(app);
agent
.get("/test")
.end(() => {
const metricHashMap = instance.metrics.http_request_seconds.hashMap;
expect(metricHashMap["status_code:200"]).toBeDefined();
const labeled = metricHashMap["status_code:200"];
expect(labeled.count).toBe(1);
agent
.get("/metrics")
.end((err, res) => {
expect(res.status).toBe(200);
done();
});
});
});
it("filters out the excludeRoutes", done => {
const app = express();
const instance = bundle({
excludeRoutes: ["/test"]
});
app.use(instance);
app.use("/test", (req, res) => res.send("it worked"));
const agent = supertest(app);
agent
.get("/test")
.end(() => {
const metricHashMap = instance.metrics.http_request_seconds.hashMap;
expect(metricHashMap["status_code:200"]).not.toBeDefined();
agent
.get("/metrics")
.end((err, res) => {
expect(res.status).toBe(200);
done();
});
});
});
describe("initial metrics removal", () => {
it("removes unexpected metrics on start with no prefix", () => {
new promClient.Counter("foo", "bar");
expect(myGetSingleMetric("foo")).toBeDefined();
bundle();
expect(myGetSingleMetric("foo")).not.toBeDefined();
});
it("removes unexpected metrics on start with a prefix", () => {
new promClient.Counter("foo", "bar");
expect(myGetSingleMetric("foo")).toBeDefined();
bundle({prefix: "some_test_"});
expect(myGetSingleMetric("foo")).not.toBeDefined();
});
it("doesnt remove metrics with matched prefix", () => {
new promClient.Counter("some_test_foo", "bar");
expect(myGetSingleMetric("some_test_foo")).toBeDefined();
bundle({prefix: "some_test_"});
expect(myGetSingleMetric("some_test_foo")).toBeDefined();
});
});
it("tolerates includePath, includeMethod and keepDefaultMetrics", done => {
const app = express();
const instance = bundle({
includePath: true,
includeMethod: true,
keepDefaultMetrics: 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);
done();
});
});
});
it("Koa: metrics returns up=1", done => {
const app = koa();
const bundled = bundle({
prefix: "hello:",
whitelist: ["up"]
});
app.use(c2k(bundled));
app.use(function*(next) {
if (this.path !== "test") {
return yield next;
}
this.body = "it worked";
});
const agent = supertestKoa(app);
agent.get("/test").end(() => {
agent
.get("/metrics")
.end((err, res) => {
expect(res.status).toBe(200);
expect(res.text).toMatch(/hello:up\s1/);
done();
});
});
});
});

View File

@@ -0,0 +1,31 @@
'use strict';
/* eslint-env jasmine */
const normalizePath = require('../src/normalizePath');
describe('normalizePath', () => {
it('uses UrlValueParser by default', () => {
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();
});
});

View File

@@ -1,28 +0,0 @@
"use strict";
/* eslint-env jasmine */
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"}))
.toBe("/a/#val");
});
});

View 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);
});
});

View File

@@ -1,46 +0,0 @@
"use strict";
module.exports = class {
constructor(opts) {
this.opts = opts || {};
this.promClient = this.opts.promClient || require("prom-client");
}
makeRealName(name) {
return (this.opts.prefix || "") + name;
}
makeMetric(TheClass, args) {
// convert pseudo-array
const applyParams = Array.prototype.slice.call(args);
const name = applyParams[0];
const realName = this.makeRealName(name);
const existing = this.promClient.register.getSingleMetric(realName);
if (existing) {
return existing;
}
applyParams[0] = realName;
applyParams.unshift(null); // add some dummy context for apply
// call constructor with variable params
return new (Function.prototype.bind.apply(TheClass, applyParams)); // eslint-disable-line
}
newCounter() {
return this.makeMetric(this.promClient.Counter, arguments);
}
newGauge() {
return this.makeMetric(this.promClient.Gauge, arguments);
}
newHistogram() {
return this.makeMetric(this.promClient.Histogram, arguments);
}
newSummary() {
return this.makeMetric(this.promClient.Summary, arguments);
}
};

View File

@@ -1,165 +1,186 @@
"use strict";
const PromFactory = require("./PromFactory");
const onFinished = require("on-finished");
const url = require("url");
const promClient = require("prom-client");
const normalizePath = require("./normalizePath");
'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) {
if (regexp instanceof RegExp) {
if (element.match(regexp)) {
return true;
}
} else if (element == regexp) {
return true;
}
for (let regexp of regexps) {
if (regexp instanceof RegExp) {
if (element.match(regexp)) {
return true;
}
} else if (element === regexp) {
return true;
}
return false;
}
return false;
}
function filterArrayByRegExps(array, regexps) {
return array.filter(element => {
return matchVsRegExps(element, 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);
}
const names = Object.keys(metricTemplates);
if (opts.whitelist) {
if (opts.blacklist) {
const blacklisted = filterArrayByRegExps(names, opts.blacklist);
return names.filter(name => blacklisted.indexOf(name) === -1);
throw new Error('you cannot have whitelist and blacklist at the same time');
}
return names;
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) {
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 || {} );
if (arguments[2] && arguments[1] && arguments[1].send) {
arguments[1].status(500)
.send("<h1>500 Error</h1>\n"
+ "<p>Unexpected 3rd param in express-prom-bundle.\n"
+ "<p>Did you just put express-prom-bundle into app.use "
+ "without calling it as a function first?");
return;
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'
+ '<p>Unexpected 3rd param in express-prom-bundle.\n'
+ '<p>Did you just put express-prom-bundle into app.use '
+ 'without calling it as a function first?');
return;
}
if (opts.prefix || opts.keepDefaultMetrics !== undefined) {
throw new Error(
'express-prom-bundle detected obsolete options:'
+ 'prefix and/or keepDefaultMetrics. '
+ 'Please refer to oficial docs. '
+ 'Most likely you upgraded the module without necessary code changes'
);
}
if (opts.promClient.collectDefaultMetrics) {
promClient.collectDefaultMetrics(opts.promClient.collectDefaultMetrics);
}
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({
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;
}
};
const metrics = {};
const names = prepareMetricNames(opts, metricTemplates);
for (let name of names) {
metrics[name] = metricTemplates[name]();
}
if (metrics.up) {
metrics.up.set(1);
}
const metricsMiddleware = function(req, res) {
res.writeHead(200, {'Content-Type': 'text/plain'});
res.end(promClient.register.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\/?$/)) {
return metricsMiddleware(req, res);
}
// this is a really messy hack but needed for compatibility with v1
// will be completely removed in v2
if (!opts.keepDefaultMetrics) {
const metrics = promClient.register.getMetricsAsJSON();
clearInterval(promClient.defaultMetrics());
metrics.forEach(metric => {
if (!opts.prefix || metric.name.substr(0, opts.prefix.length) != opts.prefix) {
promClient.register.removeSingleMetric(metric.name);
}
});
if (opts.excludeRoutes && matchVsRegExps(path, opts.excludeRoutes)) {
return next();
}
const factory = new PromFactory(opts);
const metricTemplates = {
"up": () => factory.newGauge(
"up",
"1 = up, 0 = not up"
),
"nodejs_memory_heap_total_bytes": () => factory.newGauge(
"nodejs_memory_heap_total_bytes",
"value of process.memoryUsage().heapTotal"
),
"nodejs_memory_heap_used_bytes": () => factory.newGauge(
"nodejs_memory_heap_used_bytes",
"value of process.memoryUsage().heapUsed"
),
"http_request_seconds": () => {
const labels = ["status_code"];
if (opts.includeMethod) {
labels.push("method");
}
if (opts.includePath) {
labels.push("path");
}
const metric = factory.newHistogram(
"http_request_seconds",
"number of http responses labeled with status code",
labels,
{
buckets: opts.buckets || [0.003, 0.03, 0.1, 0.3, 1.5, 10]
}
);
return metric;
if (metrics[httpMetricName]) {
labels = {};
let timer = metrics[httpMetricName].startTimer(labels);
onFinished(res, () => {
if (opts.includeStatusCode) {
labels.status_code = opts.formatStatusCode(res, opts);
}
};
const metrics = {};
const names = prepareMetricNames(opts, metricTemplates);
for (let name of names) {
metrics[name] = metricTemplates[name]();
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();
});
}
if (metrics.up) {
metrics.up.set(1);
}
next();
};
const metricsMiddleware = function(req,res) {
let memoryUsage = process.memoryUsage();
if (metrics["nodejs_memory_heap_total_bytes"]) {
metrics["nodejs_memory_heap_total_bytes"].set(memoryUsage.heapTotal);
}
if (metrics["nodejs_memory_heap_used_bytes"]) {
metrics["nodejs_memory_heap_used_bytes"].set(memoryUsage.heapUsed);
}
res.writeHead(200, {"Content-Type": "text/plain"});
res.end(factory.promClient.register.metrics());
};
const middleware = function (req, res, next) {
const path = req.path || url.parse(req.url).pathname;
let labels;
if (opts.autoregister && path == "/metrics") {
return metricsMiddleware(req,res);
}
if (opts.excludeRoutes && matchVsRegExps(path, opts.excludeRoutes)) {
return next();
}
if (metrics["http_request_seconds"]) {
labels = {"status_code": 0};
let timer = metrics["http_request_seconds"].startTimer(labels);
onFinished(res, () => {
labels.status_code = res.statusCode;
if (opts.includeMethod) {
labels.method = req.method;
}
if (opts.includePath) {
labels.path = normalizePath(req, opts);
}
timer();
});
}
next();
};
middleware.factory = factory;
middleware.metricTemplates = metricTemplates;
middleware.metrics = metrics;
middleware.promClient = factory.promClient;
middleware.metricsMiddleware = metricsMiddleware;
return middleware;
middleware.metricTemplates = metricTemplates;
middleware.metrics = metrics;
middleware.promClient = promClient;
middleware.metricsMiddleware = metricsMiddleware;
return middleware;
}
main.promClient = promClient;
main.normalizePath = normalizePath;
main.normalizeStatusCode = normalizeStatusCode;
main.clusterMetrics = clusterMetrics;
module.exports = main;

View File

@@ -1,26 +1,30 @@
"use strict";
'use strict';
const UrlValueParser = require("url-value-parser");
const url = require("url");
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
let path = url.parse(req.originalUrl || req.url).pathname;
// 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;
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 (opts.normalizePath !== undefined && !opts.normalizePath) {
return path;
}
if (typeof opts.normalizePath === "function") {
return opts.normalizePath(req, opts);
}
if (!urlValueParser) {
urlValueParser = new UrlValueParser();
}
return urlValueParser.replacePathValues(path);
if (!urlValueParser) {
urlValueParser = new UrlValueParser(opts && opts.urlValueParser);
}
return urlValueParser.replacePathValues(path);
};

View File

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