From 616e5daacc3fb22027769ba73fb92981444a7e6a Mon Sep 17 00:00:00 2001 From: Konstantin Pogorelov Date: Mon, 18 Apr 2016 02:50:26 +0200 Subject: [PATCH] intial --- .eslintrc | 42 +++++++++++++++++++++ .gitignore | 1 + .npmignore | 4 ++ .travis.yml | 4 ++ README.md | 9 +++++ example.js | 13 +++++++ package.json | 21 +++++++++++ src/PrometheusHelper.js | 51 +++++++++++++++++++++++++ src/index.js | 84 +++++++++++++++++++++++++++++++++++++++++ 9 files changed, 229 insertions(+) create mode 100644 .eslintrc create mode 100644 .gitignore create mode 100644 .npmignore create mode 100644 .travis.yml create mode 100644 README.md create mode 100644 example.js create mode 100644 package.json create mode 100644 src/PrometheusHelper.js create mode 100644 src/index.js diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..aea29c7 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,42 @@ +{ + "env": { + "node": true, + "es6": true + }, + + "extends": "eslint:recommended", + + "rules": { + "no-cond-assign": 0, + "no-constant-condition": 0, + "no-empty": 0, + "no-fallthrough": 0, + "no-unused-vars": 1, + "no-console": 1, + + "semi": 2, + "curly": 2, + "consistent-this": [2, "self"], + "indent": [ 2, 4, { "SwitchCase": 1 } ], + "linebreak-style": [2, "unix"], + "no-nested-ternary": 2, + + "new-parens": 2, + "no-dupe-class-members": 2, + "require-yield": 2, + "arrow-spacing": 1, + "no-var": 2, + + "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 }] + } +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3c3629e --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +node_modules diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..3d0ac23 --- /dev/null +++ b/.npmignore @@ -0,0 +1,4 @@ +docker-compose.yml +test +.travis.yml +.eslintrc \ No newline at end of file diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..29013be --- /dev/null +++ b/.travis.yml @@ -0,0 +1,4 @@ +language: node_js +node_js: + - "5" + - "4" diff --git a/README.md b/README.md new file mode 100644 index 0000000..9f7df3c --- /dev/null +++ b/README.md @@ -0,0 +1,9 @@ +# express prometheus bundle + +A bundle of standard metrics for an express application. + +Included metrics: + +* **up**: normally is just 1 +* **nodejs_memory_heap_total_bytes** and **nodejs_memory_heap_used_bytes** +* **http_request_total**: count of http requests labeled with status_code \ No newline at end of file diff --git a/example.js b/example.js new file mode 100644 index 0000000..e6a7252 --- /dev/null +++ b/example.js @@ -0,0 +1,13 @@ +"use strict"; + +const express = require("express"), + app = express(), + promBundle = require("."); + +app.use(promBundle({ + prefix: "demo_app:something" +})); + +app.get("/hello", (req, res) => res.send("ok")); + +app.listen(3000); \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..b4483ce --- /dev/null +++ b/package.json @@ -0,0 +1,21 @@ +{ + "name": "express-prom-bundle", + "version": "1.0.0", + "description": "express middleware with standard prometheus metrics in one bundle", + "main": "src/index.js", + "directories": { + "test": "test" + }, + "scripts": { + "test": "node_modules/jasme/run.js" + }, + "author": "Konstantin Pogorelov (https://github.com/disjunction)", + "license": "ISC", + "dependencies": { + "on-finished": "^2.3.0", + "prom-client": "^3.4.0" + }, + "devDependencies": { + "eslint": "^2.8.0" + } +} diff --git a/src/PrometheusHelper.js b/src/PrometheusHelper.js new file mode 100644 index 0000000..7e5f41e --- /dev/null +++ b/src/PrometheusHelper.js @@ -0,0 +1,51 @@ +"use strict"; + +const onFinished = require("on-finished"); + +module.exports = class { + constructor(opts, promClient) { + this.opts = opts || {}; + this.promClient = this.opts.promClient || require("prom-client"); + this.metrics = {}; + } + + metricExists(name) { + return !!this.metrics[name]; + } + + checkDuplicate(name) { + if (this.metricExists(name)) { + throw new Error("trying to add already existing metric: " + name); + } + } + + makeRealName(name) { + const prefix = this.opts.prefix ? (this.opts.prefix + ":") : ""; + return prefix + name; + } + + makeMetric(TheClass, name, description, param) { + this.checkDuplicate(name); + const realName = this.makeRealName(name); + this.metrics[name] = new TheClass( + realName, description, param + ); + return this.metrics[name]; + } + + newCounter(name, description, labels) { + return this.makeMetric(this.promClient.Counter, name, description, labels); + } + + newGauge(name, description, labels) { + return this.makeMetric(this.promClient.Gauge, name, description, labels); + } + + newHistogram(name, description, options) { + return this.makeMetric(this.promClient.Histogram, name, description, options); + } + + newSummary(name, description, options) { + return this.makeMetric(this.promClient.Histogram, name, description, options); + } +}; \ No newline at end of file diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..6be8e29 --- /dev/null +++ b/src/index.js @@ -0,0 +1,84 @@ +"use strict"; + +const + PrometheusHelper = require("./PrometheusHelper"), + onFinished = require("on-finished"); + +function filterArrayByRegExps(array, regexps) { + let compiled = regexps.map(regexp => new RegExp(regexp)); + return array.filter(element => { + for (let regexp of compiled) { + if (element.match(regexp)) { + return true; + } + } + return false; + }); +} + +function main(opts) { + if (arguments[2] && arguments[1] && arguments[1].send) { + arguments[1].status(500) + .send("

500 Error

\n" + + "

Unexapected 3d param.\n" + + "

Did you just put express-prom-bundle into app.use " + + "without calling it as a function first?"); + return; + } + + let helper = new PrometheusHelper(opts); + let metricTemplates = { + "up": () => helper.newGauge( + "up", + "1 = up, 0 = not up" + ), + "nodejs_memory_heap_total_bytes": () => helper.newGauge( + "nodejs_memory_heap_total_bytes", + "value of process.memoryUsage().heapTotal" + ), + "nodejs_memory_heap_used_bytes": () => helper.newGauge( + "nodejs_memory_heap_used_bytes", + "value of process.memoryUsage().heapUsed" + ), + "http_request_total": () => helper.newCounter( + "http_request_total", + "number of http responses labeled with status code", + ["status_code"] + ) + }; + + const metrics = {}; + + for (let name of Object.keys(metricTemplates)) { + metrics[name] = metricTemplates[name](); + } + + metrics.up.set(1); + + let middleware = function (req, res, next) { + if (req.path == "/metrics") { + let memoryUsage = process.memoryUsage(); + metrics["nodejs_memory_heap_total_bytes"].set(memoryUsage.heapTotal); + metrics["nodejs_memory_heap_used_bytes"].set(memoryUsage.heapUsed); + + res.contentType("text/plain") + .send(helper.promClient.register.metrics()); + return; + } + + onFinished(res, () => { + if (res.statusCode) { + metrics["http_request_total"].inc({"status_code": res.statusCode}); + } + }); + + next(); + }; + + middleware.helper = helper; + middleware.metricTemplates = metricTemplates; + + return middleware; +} + +module.exports = main; \ No newline at end of file