diff --git a/.eslintrc b/.eslintrc index aea29c7..389bd46 100644 --- a/.eslintrc +++ b/.eslintrc @@ -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 + } } diff --git a/advanced-example.js b/advanced-example.js index d833a3d..0a6c8e1 100644 --- a/advanced-example.js +++ b/advanced-example.js @@ -1,45 +1,40 @@ -"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 + prefix: 'demo_app:something:', + blacklist: [/up/], + buckets: [0.1, 0.4, 0.7], + includeMethod: true, + includePath: true }); app.use(bundle); // 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); -// 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 -X DELETE localhost:3000/foo/5432\n' + + 'curl localhost:3000/bar\n' + + 'curl localhost:3000/metrics\n' )); diff --git a/package.json b/package.json index 2030b59..7861ccb 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "license": "MIT", "dependencies": { "on-finished": "^2.3.0", - "prom-client": "^6.2.0", + "prom-client": "^6.3.0", "url-value-parser": "^1.0.0" }, "devDependencies": { diff --git a/spec/PromFactorySpec.js b/spec/PromFactorySpec.js deleted file mode 100644 index 91f9490..0000000 --- a/spec/PromFactorySpec.js +++ /dev/null @@ -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); - }); -}); diff --git a/spec/index.spec.js b/spec/index.spec.js new file mode 100644 index 0000000..6e60128 --- /dev/null +++ b/spec/index.spec.js @@ -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(); + }); + }); + }); +}); diff --git a/spec/indexSpec.js b/spec/indexSpec.js deleted file mode 100644 index 4a6ce7f..0000000 --- a/spec/indexSpec.js +++ /dev/null @@ -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(); - }); - }); - }); -}); diff --git a/spec/normalizePath.spec.js b/spec/normalizePath.spec.js new file mode 100644 index 0000000..be736fb --- /dev/null +++ b/spec/normalizePath.spec.js @@ -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'); + }); +}); diff --git a/spec/normalizePathSpec.js b/spec/normalizePathSpec.js deleted file mode 100644 index 0166cd5..0000000 --- a/spec/normalizePathSpec.js +++ /dev/null @@ -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"); - }); -}); diff --git a/src/PromFactory.js b/src/PromFactory.js deleted file mode 100644 index 1d06508..0000000 --- a/src/PromFactory.js +++ /dev/null @@ -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); - } -}; diff --git a/src/index.js b/src/index.js index 22fbf64..13e5534 100644 --- a/src/index.js +++ b/src/index.js @@ -1,164 +1,145 @@ -"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 url = require('url'); +const promClient = require('prom-client'); +const normalizePath = require('./normalizePath'); 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 main(opts) { - opts = Object.assign({ autoregister: true }, opts || {} ); - if (arguments[2] && arguments[1] && arguments[1].send) { - arguments[1].status(500) - .send("

500 Error

\n" - + "

Unexpected 3rd param in express-prom-bundle.\n" - + "

Did you just put express-prom-bundle into app.use " - + "without calling it as a function first?"); - return; + opts = Object.assign({autoregister: true}, opts || {}); + if (arguments[2] && arguments[1] && arguments[1].send) { + arguments[1].status(500) + .send('

500 Error

\n' + + '

Unexpected 3rd param in express-prom-bundle.\n' + + '

Did you just put express-prom-bundle into app.use ' + + 'without calling it as a function first?'); + 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 - // 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[httpMtricName]) { + labels = {'status_code': 0}; + let timer = metrics[httpMtricName].startTimer(labels); + onFinished(res, () => { + labels.status_code = res.statusCode; + if (opts.includeMethod) { + labels.method = req.method; } - }; - - const metrics = {}; - const names = prepareMetricNames(opts, metricTemplates); - - for (let name of names) { - metrics[name] = metricTemplates[name](); + if (opts.includePath) { + labels.path = normalizePath(req, opts); + } + 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; diff --git a/src/normalizePath.js b/src/normalizePath.js index e8d5ffc..2a9332f 100644 --- a/src/normalizePath.js +++ b/src/normalizePath.js @@ -1,26 +1,27 @@ -"use strict"; +'use strict'; -const UrlValueParser = require("url-value-parser"); -const url = require("url"); +const UrlValueParser = require('url-value-parser'); +const url = require('url'); let urlValueParser; module.exports = function(req, opts) { - opts = 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 - const path = url.parse(req.originalUrl).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; - if (opts.normalizePath !== undefined && !opts.normalizePath) { - return path; - } - if (typeof opts.normalizePath === "function") { - return opts.normalizePath(req, opts); - } + 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(); + } + return urlValueParser.replacePathValues(path); }; +