mirror of
https://github.com/BreizhHardware/express-prom-bundle.git
synced 2026-03-18 21:30:38 +01:00
niterim state for 2.0 development
This commit is contained in:
82
.eslintrc
82
.eslintrc
@@ -1,42 +1,54 @@
|
|||||||
{
|
{
|
||||||
"env": {
|
"parserOptions": {
|
||||||
"node": true,
|
"ecmaVersion": 6,
|
||||||
"es6": true
|
"sourceType": "module"
|
||||||
},
|
},
|
||||||
|
|
||||||
"extends": "eslint:recommended",
|
"globals": {
|
||||||
|
"app": true,
|
||||||
|
"fetch": true
|
||||||
|
},
|
||||||
|
|
||||||
"rules": {
|
"env": {
|
||||||
"no-cond-assign": 0,
|
"node": true,
|
||||||
"no-constant-condition": 0,
|
"es6": true
|
||||||
"no-empty": 0,
|
},
|
||||||
"no-fallthrough": 0,
|
|
||||||
"no-unused-vars": 1,
|
|
||||||
"no-console": 1,
|
|
||||||
|
|
||||||
"semi": 2,
|
"extends": "eslint:recommended",
|
||||||
"curly": 2,
|
|
||||||
"consistent-this": [2, "self"],
|
|
||||||
"indent": [ 2, 4, { "SwitchCase": 1 } ],
|
|
||||||
"linebreak-style": [2, "unix"],
|
|
||||||
"no-nested-ternary": 2,
|
|
||||||
|
|
||||||
"new-parens": 2,
|
"rules": {
|
||||||
"no-dupe-class-members": 2,
|
"array-bracket-spacing": [2, "never"],
|
||||||
"require-yield": 2,
|
"block-scoped-var": 2,
|
||||||
"arrow-spacing": 1,
|
"brace-style": [2, "1tbs"],
|
||||||
"no-var": 2,
|
"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,
|
"max-len": [1, 120],
|
||||||
"space-return-throw-case": 0,
|
"max-statements": [1, 50],
|
||||||
"space-infix-ops": [1, {"int32Hint": false}],
|
|
||||||
"brace-style": 1,
|
"consistent-this": [2, "self"],
|
||||||
"space-before-blocks": 1,
|
"no-var": 2,
|
||||||
"operator-linebreak": [1, "before"],
|
"no-dupe-class-members": 2,
|
||||||
"no-unneeded-ternary": 1,
|
"operator-linebreak": [1, "before"],
|
||||||
"no-lonely-if": 1,
|
"no-unneeded-ternary": [1, {"defaultAssignment": false}],
|
||||||
"key-spacing": 1,
|
"no-lonely-if": 1,
|
||||||
"quotes": [1, "double", "avoid-escape"],
|
"linebreak-style": [2, "unix"],
|
||||||
"no-trailing-spaces": [1, { "skipBlankLines": true }]
|
"no-nested-ternary": 2,
|
||||||
}
|
"require-yield": 2
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,45 +1,40 @@
|
|||||||
"use strict";
|
'use strict';
|
||||||
|
|
||||||
const express = require("express");
|
const express = require('express');
|
||||||
const app = express();
|
const app = express();
|
||||||
const promBundle = require("express-prom-bundle");
|
const promBundle = require('express-prom-bundle');
|
||||||
|
|
||||||
const bundle = promBundle({
|
const bundle = promBundle({
|
||||||
prefix: "demo_app:something:",
|
prefix: 'demo_app:something:',
|
||||||
blacklist: [/up/],
|
blacklist: [/up/],
|
||||||
buckets: [0.1, 0.4, 0.7],
|
buckets: [0.1, 0.4, 0.7],
|
||||||
includeMethod: true,
|
includeMethod: true,
|
||||||
includePath: true,
|
includePath: true
|
||||||
keepDefaultMetrics: false
|
|
||||||
});
|
});
|
||||||
|
|
||||||
app.use(bundle);
|
app.use(bundle);
|
||||||
|
|
||||||
// native prom-client metric (no prefix)
|
// native prom-client metric (no prefix)
|
||||||
const c1 = new bundle.promClient.Counter("c1", "c1 help");
|
const c1 = new bundle.promClient.Counter('c1', 'c1 help');
|
||||||
c1.inc(10);
|
c1.inc(10);
|
||||||
|
|
||||||
// create metric using factory (w/ prefix)
|
app.get('/foo/:id', (req, res) => {
|
||||||
const c2 = bundle.factory.newCounter("c2", "c2 help");
|
setTimeout(() => {
|
||||||
c2.inc(20);
|
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) => {
|
app.delete('/foo/:id', (req, res) => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
res.send("foo deleted\n");
|
res.send('foo deleted\n');
|
||||||
}, 300);
|
}, 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
|
app.listen(3000, () => console.info( // eslint-disable-line
|
||||||
"listening on 3000\n"
|
'listening on 3000\n'
|
||||||
+ "test in shell console\n\n"
|
+ 'test in shell console\n\n'
|
||||||
+ "curl localhost:3000/foo/1234\n"
|
+ 'curl localhost:3000/foo/1234\n'
|
||||||
+ "curl -X DELETE localhost:3000/foo/5432\n"
|
+ 'curl -X DELETE localhost:3000/foo/5432\n'
|
||||||
+ "curl localhost:3000/bar\n"
|
+ 'curl localhost:3000/bar\n'
|
||||||
+ "curl localhost:3000/metrics\n"
|
+ 'curl localhost:3000/metrics\n'
|
||||||
));
|
));
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"on-finished": "^2.3.0",
|
"on-finished": "^2.3.0",
|
||||||
"prom-client": "^6.2.0",
|
"prom-client": "^6.3.0",
|
||||||
"url-value-parser": "^1.0.0"
|
"url-value-parser": "^1.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
213
spec/index.spec.js
Normal file
213
spec/index.spec.js
Normal file
@@ -0,0 +1,213 @@
|
|||||||
|
'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({
|
||||||
|
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('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.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('removes metrics on start with ', () => {
|
||||||
|
new promClient.Counter('foo', 'bar');
|
||||||
|
expect(promClient.getSingleMetric('foo')).toBeDefined();
|
||||||
|
bundle();
|
||||||
|
expect(promClient.getSingleMetric('foo')).not.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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
28
spec/normalizePath.spec.js
Normal file
28
spec/normalizePath.spec.js
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
'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');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
259
src/index.js
259
src/index.js
@@ -1,164 +1,145 @@
|
|||||||
"use strict";
|
'use strict';
|
||||||
|
const onFinished = require('on-finished');
|
||||||
const PromFactory = require("./PromFactory");
|
const url = require('url');
|
||||||
const onFinished = require("on-finished");
|
const promClient = require('prom-client');
|
||||||
const url = require("url");
|
const normalizePath = require('./normalizePath');
|
||||||
const promClient = require("prom-client");
|
|
||||||
const normalizePath = require("./normalizePath");
|
|
||||||
|
|
||||||
function matchVsRegExps(element, regexps) {
|
function matchVsRegExps(element, regexps) {
|
||||||
for (let regexp of regexps) {
|
for (let regexp of regexps) {
|
||||||
if (regexp instanceof RegExp) {
|
if (regexp instanceof RegExp) {
|
||||||
if (element.match(regexp)) {
|
if (element.match(regexp)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
} else if (element == regexp) {
|
} else if (element === regexp) {
|
||||||
return true;
|
return true;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return false;
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function filterArrayByRegExps(array, regexps) {
|
function filterArrayByRegExps(array, regexps) {
|
||||||
return array.filter(element => {
|
return array.filter(element => {
|
||||||
return matchVsRegExps(element, regexps);
|
return matchVsRegExps(element, regexps);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function prepareMetricNames(opts, metricTemplates) {
|
function prepareMetricNames(opts, metricTemplates) {
|
||||||
const names = Object.keys(metricTemplates);
|
const names = Object.keys(metricTemplates);
|
||||||
if (opts.whitelist) {
|
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) {
|
if (opts.blacklist) {
|
||||||
const blacklisted = filterArrayByRegExps(names, opts.blacklist);
|
throw new Error('you cannot have whitelist and blacklist at the same time');
|
||||||
return names.filter(name => blacklisted.indexOf(name) === -1);
|
|
||||||
}
|
}
|
||||||
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 main(opts) {
|
function main(opts) {
|
||||||
opts = Object.assign({ autoregister: true }, opts || {} );
|
opts = Object.assign({autoregister: true}, opts || {});
|
||||||
if (arguments[2] && arguments[1] && arguments[1].send) {
|
if (arguments[2] && arguments[1] && arguments[1].send) {
|
||||||
arguments[1].status(500)
|
arguments[1].status(500)
|
||||||
.send("<h1>500 Error</h1>\n"
|
.send('<h1>500 Error</h1>\n'
|
||||||
+ "<p>Unexpected 3rd param in express-prom-bundle.\n"
|
+ '<p>Unexpected 3rd param in express-prom-bundle.\n'
|
||||||
+ "<p>Did you just put express-prom-bundle into app.use "
|
+ '<p>Did you just put express-prom-bundle into app.use '
|
||||||
+ "without calling it as a function first?");
|
+ 'without calling it as a function first?');
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// this is a really messy hack but needed for compatibility with v1
|
||||||
|
// will be completely removed in v2
|
||||||
|
if (opts.keepDefaultMetrics === false) {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const httpMtricName = opts.httpDurationMetricName || 'http_request_duration_seconds';
|
||||||
|
|
||||||
|
const metricTemplates = {
|
||||||
|
'up': () => new promClient.Gauge(
|
||||||
|
'up',
|
||||||
|
'1 = up, 0 = not up'
|
||||||
|
),
|
||||||
|
'http_request_seconds': () => {
|
||||||
|
const labels = ['status_code'];
|
||||||
|
if (opts.includeMethod) {
|
||||||
|
labels.push('method');
|
||||||
|
}
|
||||||
|
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]
|
||||||
|
}
|
||||||
|
);
|
||||||
|
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.path || url.parse(req.url).pathname;
|
||||||
|
let labels;
|
||||||
|
|
||||||
|
if (opts.autoregister && path === '/metrics') {
|
||||||
|
return metricsMiddleware(req,res);
|
||||||
}
|
}
|
||||||
|
|
||||||
// this is a really messy hack but needed for compatibility with v1
|
if (opts.excludeRoutes && matchVsRegExps(path, opts.excludeRoutes)) {
|
||||||
// will be completely removed in v2
|
return next();
|
||||||
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);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const factory = new PromFactory(opts);
|
if (metrics[httpMtricName]) {
|
||||||
|
labels = {'status_code': 0};
|
||||||
const metricTemplates = {
|
let timer = metrics[httpMtricName].startTimer(labels);
|
||||||
"up": () => factory.newGauge(
|
onFinished(res, () => {
|
||||||
"up",
|
labels.status_code = res.statusCode;
|
||||||
"1 = up, 0 = not up"
|
if (opts.includeMethod) {
|
||||||
),
|
labels.method = req.method;
|
||||||
"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 (opts.includePath) {
|
||||||
|
labels.path = normalizePath(req, opts);
|
||||||
const metrics = {};
|
}
|
||||||
const names = prepareMetricNames(opts, metricTemplates);
|
timer();
|
||||||
|
});
|
||||||
for (let name of names) {
|
|
||||||
metrics[name] = metricTemplates[name]();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (metrics.up) {
|
next();
|
||||||
metrics.up.set(1);
|
};
|
||||||
}
|
|
||||||
|
|
||||||
const metricsMiddleware = function(req,res) {
|
middleware.metricTemplates = metricTemplates;
|
||||||
let memoryUsage = process.memoryUsage();
|
middleware.metrics = metrics;
|
||||||
if (metrics["nodejs_memory_heap_total_bytes"]) {
|
middleware.promClient = promClient;
|
||||||
metrics["nodejs_memory_heap_total_bytes"].set(memoryUsage.heapTotal);
|
middleware.metricsMiddleware = metricsMiddleware;
|
||||||
}
|
return middleware;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
main.promClient = promClient;
|
main.promClient = promClient;
|
||||||
|
|||||||
@@ -1,26 +1,27 @@
|
|||||||
"use strict";
|
'use strict';
|
||||||
|
|
||||||
const UrlValueParser = require("url-value-parser");
|
const UrlValueParser = require('url-value-parser');
|
||||||
const url = require("url");
|
const url = require('url');
|
||||||
let urlValueParser;
|
let urlValueParser;
|
||||||
|
|
||||||
module.exports = function(req, opts) {
|
module.exports = function(req, opts) {
|
||||||
opts = opts || {};
|
opts = opts || {};
|
||||||
|
|
||||||
// originalUrl is taken, because url and path can be changed
|
// originalUrl is taken, because url and path can be changed
|
||||||
// by middlewares such as "router". Note: this function is called onFinish
|
// by middlewares such as 'router'. Note: this function is called onFinish
|
||||||
/// i.e. always in the tail of the middleware chain
|
/// i.e. always in the tail of the middleware chain
|
||||||
const path = url.parse(req.originalUrl).pathname;
|
const path = url.parse(req.originalUrl).pathname;
|
||||||
|
|
||||||
if (opts.normalizePath !== undefined && !opts.normalizePath) {
|
if (opts.normalizePath !== undefined && !opts.normalizePath) {
|
||||||
return path;
|
return path;
|
||||||
}
|
}
|
||||||
if (typeof opts.normalizePath === "function") {
|
if (typeof opts.normalizePath === 'function') {
|
||||||
return opts.normalizePath(req, opts);
|
return opts.normalizePath(req, opts);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!urlValueParser) {
|
if (!urlValueParser) {
|
||||||
urlValueParser = new UrlValueParser();
|
urlValueParser = new UrlValueParser();
|
||||||
}
|
}
|
||||||
return urlValueParser.replacePathValues(path);
|
return urlValueParser.replacePathValues(path);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user