mirror of
https://github.com/BreizhHardware/express-prom-bundle.git
synced 2026-01-19 00:37:36 +01:00
Compare commits
51 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b00d88c4a7 | ||
|
|
11988cb025 | ||
|
|
df6854ee04 | ||
|
|
48d883b7f2 | ||
|
|
b835e90560 | ||
|
|
088ff5a063 | ||
|
|
fd12de6ea3 | ||
|
|
0b8706dd42 | ||
|
|
982ff86543 | ||
|
|
5f39d8f357 | ||
|
|
d405e3f584 | ||
|
|
ff147d8fc4 | ||
|
|
43f5a2b04f | ||
|
|
09ee5d954a | ||
|
|
33cca0d2cf | ||
|
|
2040c043fd | ||
|
|
f6466d007b | ||
|
|
e68466d7af | ||
|
|
6b49ffab08 | ||
|
|
cb128c7520 | ||
|
|
d336165848 | ||
|
|
cfe0065146 | ||
|
|
963a66a25e | ||
|
|
389684b426 | ||
|
|
846ccfc641 | ||
|
|
17fe5f4c70 | ||
|
|
66ef4e3176 | ||
|
|
f55d397a31 | ||
|
|
68ad108f77 | ||
|
|
6000056f7a | ||
|
|
48856afbe3 | ||
|
|
e035e2b991 | ||
|
|
c6d5964768 | ||
|
|
94722e908b | ||
|
|
00b8369329 | ||
|
|
59221f891b | ||
|
|
3a0b2caf61 | ||
|
|
e7d004f0cc | ||
|
|
0dd3116f23 | ||
|
|
0606bf5f4f | ||
|
|
6054824c67 | ||
|
|
01c78bcc1d | ||
|
|
bc65dc45cb | ||
|
|
8cae5b6ef3 | ||
|
|
2003e7743f | ||
|
|
568c87216a | ||
|
|
98be36244e | ||
|
|
6ff1204db4 | ||
|
|
f71d837660 | ||
|
|
4aa2bfa6ae | ||
|
|
1fff877787 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,3 +1,5 @@
|
||||
.npmrc
|
||||
node_modules
|
||||
coverage
|
||||
/.vscode
|
||||
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
advanced-example.js
|
||||
docker-compose.yml
|
||||
spec
|
||||
.travis.yml
|
||||
.eslintrc
|
||||
coverage
|
||||
@@ -2,3 +2,8 @@ language: node_js
|
||||
node_js:
|
||||
- "6"
|
||||
- "8"
|
||||
- "10"
|
||||
notifications:
|
||||
email: false
|
||||
before_install:
|
||||
- npm install prom-client
|
||||
|
||||
54
README.md
54
README.md
@@ -4,17 +4,17 @@
|
||||
|
||||
Express middleware with popular prometheus metrics in one bundle. It's also compatible with koa v1 and v2 (see below).
|
||||
|
||||
Internally it uses **prom-client**. See: https://github.com/siimon/prom-client
|
||||
Since version 5 it uses **prom-client** as a peer dependency. See: https://github.com/siimon/prom-client
|
||||
|
||||
Included metrics:
|
||||
|
||||
* `up`: normally is just 1
|
||||
* `http_request_duration_seconds`: http latency histogram labeled with `status_code`, `method` and `path`
|
||||
* `http_request_duration_seconds`: http latency histogram/summary labeled with `status_code`, `method` and `path`
|
||||
|
||||
## Install
|
||||
|
||||
```
|
||||
npm install express-prom-bundle
|
||||
npm install prom-client express-prom-bundle
|
||||
```
|
||||
|
||||
## Sample Usage
|
||||
@@ -40,28 +40,6 @@ The order in which the routes are registered is important, since
|
||||
You can use this to your advantage to bypass some of the routes.
|
||||
See the example below.
|
||||
|
||||
## Usage with Node Cluster
|
||||
``` javascript
|
||||
if (cluster.isMaster) {
|
||||
const numCPUs = Math.max(2, os.cpus().length);
|
||||
const workers: cluster.Worker[] = [];
|
||||
for (let i=1; i < numCPUs; i++) {
|
||||
const worker = forkWorker();
|
||||
workers.push(worker);
|
||||
}
|
||||
const metricsApp = express();
|
||||
metricsApp.use('/cluster_metrics', promBundle.clusterMetrics());
|
||||
metricsApp.listen(9999);
|
||||
console.log('metrics listening on 9999'); // call localhost:9999/cluster_metrics for aggregated metrics
|
||||
} else {
|
||||
const app = express();
|
||||
app.use(promBundle({includeMethod: true});
|
||||
app.use('/api', require('./api'));
|
||||
app.listen(3000);
|
||||
}
|
||||
```
|
||||
The code the master process runs will expose an API with a single endpoint `/cluster_metrics` which returns an aggregate of all metrics from all the workers.
|
||||
|
||||
## Options
|
||||
|
||||
Which labels to include in `http_request_duration_seconds` metric:
|
||||
@@ -71,8 +49,25 @@ 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
|
||||
* **metricType**: histogram/summary selection. See more details below
|
||||
|
||||
Extra transformation callbacks:
|
||||
### metricType option ###
|
||||
|
||||
Two metric types are supported for `http_request_duration_seconds` metric:
|
||||
* [histogram](https://prometheus.io/docs/concepts/metric_types/#histogram) (default)
|
||||
* [summary](https://prometheus.io/docs/concepts/metric_types/#summary)
|
||||
|
||||
Additional options for **histogram**:
|
||||
* **buckets**: buckets used for the `http_request_duration_seconds` histogram
|
||||
|
||||
Additional options for **summary**:
|
||||
* **percentiles**: percentiles used for `http_request_duration_seconds` summary
|
||||
* **ageBuckets**: ageBuckets configures how many buckets we have in our sliding window for the summary
|
||||
* **maxAgeSeconds**: the maxAgeSeconds will tell how old a bucket can be before it is reset
|
||||
|
||||
### Transformation callbacks ###
|
||||
|
||||
* **normalizePath**: `function(req)` or `Array`
|
||||
* if function is provided, then it should generate path value from express `req`
|
||||
@@ -84,9 +79,8 @@ Extra transformation callbacks:
|
||||
* **formatStatusCode**: `function(res)` producing final status code from express `res` object, e.g. you can combine `200`, `201` and `204` to just `2xx`.
|
||||
* **transformLabels**: `function(labels, req, res)` transforms the **labels** object, e.g. setting dynamic values to **customLabels**
|
||||
|
||||
Other options:
|
||||
### Other options ###
|
||||
|
||||
* **buckets**: buckets used for `http_request_duration_seconds` histogram
|
||||
* **autoregister**: if `/metrics` endpoint should be registered. (Default: **true**)
|
||||
* **promClient**: options for promClient startup, e.g. **collectDefaultMetrics**. This option was added
|
||||
to keep `express-prom-bundle` runnable using confit (e.g. with kraken.js) without writing any JS code,
|
||||
@@ -232,7 +226,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
|
||||
{
|
||||
@@ -255,7 +249,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]+$"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
`
|
||||
));
|
||||
|
||||
47
index.d.ts
vendored
Normal file
47
index.d.ts
vendored
Normal file
@@ -0,0 +1,47 @@
|
||||
import * as express from "express";
|
||||
import { DefaultMetricsCollectorConfiguration } from "prom-client";
|
||||
|
||||
type NormalizePathRegexs = [string, string];
|
||||
|
||||
interface Labels {
|
||||
[key: string]: string | number;
|
||||
}
|
||||
|
||||
interface Opts {
|
||||
autoregister?: boolean;
|
||||
buckets?: number[];
|
||||
|
||||
customLabels?: { [key: string]: any };
|
||||
|
||||
includeStatusCode?: boolean;
|
||||
includeMethod?: boolean;
|
||||
includePath?: boolean;
|
||||
includeUp?: boolean;
|
||||
|
||||
metricType?: "summary" | "histogram";
|
||||
metricsPath?: string;
|
||||
promClient?: DefaultMetricsCollectorConfiguration;
|
||||
normalizePath?: NormalizePathRegexs;
|
||||
formatStatusCode?: (res: express.Response) => number | string;
|
||||
|
||||
transformLabels?: (
|
||||
labels: Labels,
|
||||
req: express.Request,
|
||||
res: express.Response
|
||||
) => Labels;
|
||||
|
||||
// https://github.com/disjunction/url-value-parser#options
|
||||
urlValueParser?: {
|
||||
minHexLength?: number;
|
||||
minBase64Length?: number;
|
||||
replaceMasks?: string[];
|
||||
extraMasks?: string[];
|
||||
};
|
||||
}
|
||||
|
||||
export function normalizePath(
|
||||
req: express.Request,
|
||||
opts?: { normalizePath?: NormalizePathRegexs }
|
||||
): string;
|
||||
|
||||
export default function(opts: Opts): express.RequestHandler;
|
||||
1147
package-lock.json
generated
1147
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
22
package.json
22
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "express-prom-bundle",
|
||||
"version": "4.2.0",
|
||||
"version": "5.1.4",
|
||||
"description": "express middleware with popular prometheus metrics in one bundle",
|
||||
"main": "src/index.js",
|
||||
"keywords": [
|
||||
@@ -10,6 +10,10 @@
|
||||
"path",
|
||||
"method"
|
||||
],
|
||||
"files": [
|
||||
"src",
|
||||
"index.d.ts"
|
||||
],
|
||||
"scripts": {
|
||||
"test": "node_modules/jasme/run.js",
|
||||
"coverage": "make coverage"
|
||||
@@ -18,20 +22,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"
|
||||
|
||||
@@ -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) {
|
||||
@@ -407,7 +472,10 @@ describe('index', () => {
|
||||
const agent = supertest(app);
|
||||
|
||||
// create a fake worker, which would not respond in time
|
||||
cluster.workers = [{send: () => {}}];
|
||||
cluster.workers = [{
|
||||
isConnected: () => true,
|
||||
send: () => {}
|
||||
}];
|
||||
|
||||
const errorSpy = spyOn(console, 'error'); // mute console.error
|
||||
|
||||
@@ -420,4 +488,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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
175
src/index.js
175
src/index.js
@@ -1,11 +1,10 @@
|
||||
'use strict';
|
||||
const onFinished = require('on-finished');
|
||||
const promClient = require('prom-client');
|
||||
const normalizePath = require('./normalizePath');
|
||||
const normalizeStatusCode = require('./normalizeStatusCode');
|
||||
|
||||
function matchVsRegExps(element, regexps) {
|
||||
for (let regexp of regexps) {
|
||||
for (const regexp of regexps) {
|
||||
if (regexp instanceof RegExp) {
|
||||
if (element.match(regexp)) {
|
||||
return true;
|
||||
@@ -17,31 +16,10 @@ function matchVsRegExps(element, regexps) {
|
||||
return false;
|
||||
}
|
||||
|
||||
function filterArrayByRegExps(array, regexps) {
|
||||
return array.filter(element => {
|
||||
return matchVsRegExps(element, regexps);
|
||||
});
|
||||
}
|
||||
|
||||
function prepareMetricNames(opts, metricTemplates) {
|
||||
const names = Object.keys(metricTemplates);
|
||||
if (opts.whitelist) {
|
||||
if (opts.blacklist) {
|
||||
throw new Error('you cannot have whitelist and blacklist at the same time');
|
||||
}
|
||||
return filterArrayByRegExps(names, opts.whitelist);
|
||||
}
|
||||
if (opts.blacklist) {
|
||||
const blacklisted = filterArrayByRegExps(names, opts.blacklist);
|
||||
return names.filter(name => blacklisted.indexOf(name) === -1);
|
||||
}
|
||||
return names;
|
||||
}
|
||||
|
||||
function clusterMetrics() {
|
||||
const aggregatorRegistry = new promClient.AggregatorRegistry();
|
||||
|
||||
const metricsMiddleware = function(req, res, next) {
|
||||
const metricsMiddleware = function(req, res) {
|
||||
aggregatorRegistry.clusterMetrics((err, clusterMetrics) => {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
@@ -56,16 +34,6 @@ function clusterMetrics() {
|
||||
}
|
||||
|
||||
function main(opts) {
|
||||
opts = Object.assign(
|
||||
{
|
||||
autoregister: true,
|
||||
includeStatusCode: true,
|
||||
normalizePath: main.normalizePath,
|
||||
formatStatusCode: main.normalizeStatusCode,
|
||||
promClient: {}
|
||||
},
|
||||
opts
|
||||
);
|
||||
if (arguments[2] && arguments[1] && arguments[1].send) {
|
||||
arguments[1].status(500)
|
||||
.send('<h1>500 Error</h1>\n'
|
||||
@@ -75,12 +43,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,48 @@ function main(opts) {
|
||||
|
||||
const httpMetricName = opts.httpDurationMetricName || 'http_request_duration_seconds';
|
||||
|
||||
const metricTemplates = {
|
||||
'up': () => new promClient.Gauge({
|
||||
name: 'up',
|
||||
help: '1 = up, 0 = not up'
|
||||
}),
|
||||
[httpMetricName]: () => {
|
||||
const labels = ['status_code'];
|
||||
if (opts.includeMethod) {
|
||||
labels.push('method');
|
||||
}
|
||||
if (opts.includePath) {
|
||||
labels.push('path');
|
||||
}
|
||||
if (opts.customLabels){
|
||||
labels.push.apply(labels, Object.keys(opts.customLabels));
|
||||
}
|
||||
const metric = new promClient.Histogram({
|
||||
function makeHttpMetric() {
|
||||
const labels = ['status_code'];
|
||||
if (opts.includeMethod) {
|
||||
labels.push('method');
|
||||
}
|
||||
if (opts.includePath) {
|
||||
labels.push('path');
|
||||
}
|
||||
if (opts.customLabels) {
|
||||
labels.push.apply(labels, Object.keys(opts.customLabels));
|
||||
}
|
||||
|
||||
if (opts.metricType === 'summary') {
|
||||
return new promClient.Summary({
|
||||
name: httpMetricName,
|
||||
help: 'duration summary of http responses labeled with: ' + labels.join(', '),
|
||||
labelNames: labels,
|
||||
percentiles: opts.percentiles || [0.5, 0.75, 0.95, 0.98, 0.99, 0.999],
|
||||
maxAgeSeconds: opts.maxAgeSeconds,
|
||||
ageBuckets: opts.ageBuckets
|
||||
});
|
||||
} 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 +123,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 +137,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;
|
||||
|
||||
Reference in New Issue
Block a user