diff --git a/docs/api/index.md b/docs/api/index.md index ea1864d332..c0d2388a31 100644 --- a/docs/api/index.md +++ b/docs/api/index.md @@ -7,4 +7,8 @@ Version: 1.0 * [Feature Toggles API](feature-toggles-api.md) * [Strategies API](strategies-api.md) * [Events API](events-api.md) -* [Metrics API](metrics-api.md) \ No newline at end of file +* [Metrics API](metrics-api.md) + + +Others: +* [Internal Backstage API](internal-backstage-api.ms) \ No newline at end of file diff --git a/docs/api/internal-backstage-api.md b/docs/api/internal-backstage-api.md new file mode 100644 index 0000000000..5146d8baaf --- /dev/null +++ b/docs/api/internal-backstage-api.md @@ -0,0 +1,8 @@ +# Internal Backstage API + +`GET http://unleash.host.com/internal-backstage/prometheus` + +Unleash uses prometheus internally to collect metrics. These are +available on the given url if the `serverMetrics` option is enabled (default=true). + +[Read more about Prometheus](https://prometheus.io/) \ No newline at end of file diff --git a/lib/events.js b/lib/events.js new file mode 100644 index 0000000000..9a813220a2 --- /dev/null +++ b/lib/events.js @@ -0,0 +1,6 @@ +module.exports = { + TOGGLES_FETCH: 'toggles:fetch', + TOGGLES_CREATE: 'toggles:create', + CLIENT_REGISTER: 'client:register', + CLIENT_METRICS: 'toggles:metrics', +} \ No newline at end of file diff --git a/lib/metrics.js b/lib/metrics.js new file mode 100644 index 0000000000..f5c51dac70 --- /dev/null +++ b/lib/metrics.js @@ -0,0 +1,24 @@ +const events = require('./events'); + +exports.startMonitoring = (enable, eventBus) => { + if (!enable) { + return; + } + + const client = require('prom-client'); + const toggleFetch = new client.Counter('toggles_fetch_counter', 'Number of fetch toggles request'); + const clientRegister = new client.Counter('client_register_counter', 'Number client register requests'); + const clientMetrics = new client.Counter('client_metrics_counter', 'Number client metrics requests'); + + eventBus.on(events.TOGGLES_FETCH, () => { + toggleFetch.inc(); + }); + + eventBus.on(events.CLIENT_REGISTER, () => { + clientRegister.inc(); + }); + + eventBus.on(events.CLIENT_METRICS, () => { + clientMetrics.inc(); + }); +}; \ No newline at end of file diff --git a/lib/options.js b/lib/options.js new file mode 100644 index 0000000000..0866269748 --- /dev/null +++ b/lib/options.js @@ -0,0 +1,18 @@ +'use strict'; + +const DEFAULT_OPTIONS = { + databaseUri: process.env.DATABASE_URL || 'postgres://unleash_user:passord@localhost:5432/unleash', + port: process.env.HTTP_PORT || process.env.PORT || 4242, + baseUriPath: process.env.BASE_URI_PATH || '', + serverMetrics: true, +}; + +module.exports = { + createOptions: (opts) => { + const options = Object.assign({}, DEFAULT_OPTIONS, opts); + if (!options.databaseUri) { + throw new Error('You must either pass databaseUri option or set environemnt variable DATABASE_URL'); + } + return options; + } +} \ No newline at end of file diff --git a/lib/routes/backstage.js b/lib/routes/backstage.js new file mode 100644 index 0000000000..e8edabb757 --- /dev/null +++ b/lib/routes/backstage.js @@ -0,0 +1,12 @@ +'use strict'; + +const prometheusRegister = require('prom-client/lib/register'); + +module.exports = function (app, config) { + if(config.serverMetrics) { + app.get('/internal-backstage/prometheus', (req, res) => { + res.writeHead(200, { 'Content-Type': 'text/plain' }); + res.end(prometheusRegister.metrics()); + }); + } +}; diff --git a/lib/routes/feature.js b/lib/routes/feature.js index ae59d8a507..527efd6be0 100644 --- a/lib/routes/feature.js +++ b/lib/routes/feature.js @@ -8,6 +8,8 @@ const ValidationError = require('../error/validation-error.js'); const validateRequest = require('../error/validate-request'); const extractUser = require('../extract-user'); +const { TOGGLES_FETCH } = require('../events'); + const legacyFeatureMapper = require('../data-helper/legacy-feature-mapper'); const version = 1; @@ -37,8 +39,11 @@ const handleErrors = (req, res, error) => { module.exports = function (app, config) { const { featureToggleStore, eventStore } = config.stores; + const { eventBus } = config; app.get('/features', (req, res) => { + eventBus.emit(TOGGLES_FETCH); + featureToggleStore.getFeatures() .then((features) => features.map(legacyFeatureMapper.addOldFields)) .then(features => res.json({ version, features })); diff --git a/lib/routes/index.js b/lib/routes/index.js index c8bcbf82d7..5491beaf85 100644 --- a/lib/routes/index.js +++ b/lib/routes/index.js @@ -13,4 +13,5 @@ exports.createAPI = function (router, config) { exports.createLegacy = function (router, config) { require('./feature')(router, config); require('./health-check')(router, config); + require('./backstage')(router, config); }; diff --git a/lib/routes/metrics.js b/lib/routes/metrics.js index 31e46e999b..36d393ee2b 100644 --- a/lib/routes/metrics.js +++ b/lib/routes/metrics.js @@ -4,7 +4,7 @@ const logger = require('../logger'); const ClientMetrics = require('../client-metrics'); const joi = require('joi'); const { clientMetricsSchema, clientRegisterSchema } = require('./metrics-schema'); - +const { CLIENT_REGISTER, CLIENT_METRICS } = require('../events'); /* * TODO: * - always catch errors and always return a response to client! @@ -18,6 +18,8 @@ module.exports = function (app, config) { clientStrategyStore, clientInstanceStore, } = config.stores; + + const { eventBus } = config; const metrics = new ClientMetrics(clientMetricsStore); @@ -39,6 +41,8 @@ module.exports = function (app, config) { return res.status(400).json(err); } + eventBus.emit(CLIENT_METRICS); + clientMetricsStore .insert(cleaned) .then(() => clientInstanceStore.insert({ @@ -61,6 +65,8 @@ module.exports = function (app, config) { return res.status(400).json(err); } + eventBus.emit(CLIENT_REGISTER); + clientStrategyStore .insert(cleaned.appName, cleaned.strategies) .then(() => clientInstanceStore.insert({ diff --git a/lib/server-impl.js b/lib/server-impl.js index bd3b020b25..c4ec627c35 100644 --- a/lib/server-impl.js +++ b/lib/server-impl.js @@ -1,40 +1,42 @@ 'use strict'; +const { EventEmitter } = require('events'); + const logger = require('./logger'); const migrator = require('../migrator'); -const { createStores } = require('./db'); const getApp = require('./app'); +const events = require('./events'); -const DEFAULT_OPTIONS = { - databaseUri: process.env.DATABASE_URL || 'postgres://unleash_user:passord@localhost:5432/unleash', - port: process.env.HTTP_PORT || process.env.PORT || 4242, - baseUriPath: process.env.BASE_URI_PATH || '', -}; +const { startMonitoring } = require('./metrics'); +const { createStores } = require('./db'); +const { createOptions } = require('./options'); function createApp (options) { // Database dependecies (statefull) const stores = createStores(options); + const eventBus = new EventEmitter(); const config = { + prometheusPath: options.prometheusPath, baseUriPath: options.baseUriPath, port: options.port, publicFolder: options.publicFolder, stores, + eventBus, }; const app = getApp(config); const server = app.listen(app.get('port'), () => { logger.info(`Unleash started on ${app.get('port')}`); }); - return { app, server }; + + startMonitoring(options.serverMetrics, eventBus); + + return { app, server, eventBus }; } function start (opts) { - const options = Object.assign({}, DEFAULT_OPTIONS, opts); - - if (!options.databaseUri) { - throw new Error('You must either pass databaseUri option or set environemnt variable DATABASE_URL'); - } + const options = createOptions(opts); return migrator({ databaseUri: options.databaseUri }) .catch(err => logger.error('failed to migrate db', err)) diff --git a/package.json b/package.json index 14aa2b36e8..e8272beec9 100644 --- a/package.json +++ b/package.json @@ -71,6 +71,7 @@ "moment": "^2.15.2", "parse-database-url": "^0.3.0", "pg": "^6.1.0", + "prom-client": "^6.1.2", "serve-favicon": "^2.3.0", "unleash-frontend": "github:unleash/unleash-frontend", "yallist": "^2.0.0" diff --git a/test/e2e/helpers/test-helper.js b/test/e2e/helpers/test-helper.js index 8edd612292..e1e36a5167 100644 --- a/test/e2e/helpers/test-helper.js +++ b/test/e2e/helpers/test-helper.js @@ -15,6 +15,9 @@ require('db-migrate-shared').log.silence(true); // because of migrator bug delete process.env.DATABASE_URL; +const { EventEmitter } = require('events'); +const eventBus = new EventEmitter(); + function createApp (databaseSchema = 'test') { const options = { databaseUri: require('./database-config').getDatabaseUri(), @@ -29,7 +32,7 @@ function createApp (databaseSchema = 'test') { .then(() => { db.destroy(); const stores = createStores(options); - const app = getApp({ stores }); + const app = getApp({ stores, eventBus }); return { stores, request: supertest(app), diff --git a/test/unit/routes/backstage.test.js b/test/unit/routes/backstage.test.js new file mode 100644 index 0000000000..ea04d2c61b --- /dev/null +++ b/test/unit/routes/backstage.test.js @@ -0,0 +1,31 @@ +'use strict'; + +const test = require('ava'); +const store = require('./fixtures/store'); +const supertest = require('supertest'); +const logger = require('../../../lib/logger'); +const getApp = require('../../../lib/app'); + +const { EventEmitter } = require('events'); +const eventBus = new EventEmitter(); + +test.beforeEach(() => { + logger.setLevel('FATAL'); +}); + +test('should use enable prometheus', t => { + const stores = store.createStores(); + const app = getApp({ + baseUriPath: '', + serverMetrics: true, + stores, + eventBus, + }); + + const request = supertest(app); + + return request + .get('/internal-backstage/prometheus') + .expect('Content-Type', /text/) + .expect(200) +}); diff --git a/test/unit/routes/feature.test.js b/test/unit/routes/feature.test.js index 1972673c11..2d17f56ed2 100644 --- a/test/unit/routes/feature.test.js +++ b/test/unit/routes/feature.test.js @@ -6,6 +6,9 @@ const supertest = require('supertest'); const logger = require('../../../lib/logger'); const getApp = require('../../../lib/app'); +const { EventEmitter } = require('events'); +const eventBus = new EventEmitter(); + test.beforeEach(() => { logger.setLevel('FATAL'); }); @@ -16,6 +19,7 @@ function getSetup () { const app = getApp({ baseUriPath: base, stores, + eventBus, }); return { diff --git a/test/unit/routes/health-check.test.js b/test/unit/routes/health-check.test.js index e40481ce15..29063a446d 100644 --- a/test/unit/routes/health-check.test.js +++ b/test/unit/routes/health-check.test.js @@ -6,6 +6,9 @@ const supertest = require('supertest'); const logger = require('../../../lib/logger'); const getApp = require('../../../lib/app'); +const { EventEmitter } = require('events'); +const eventBus = new EventEmitter(); + test.beforeEach(() => { logger.setLevel('FATAL'); }); @@ -17,6 +20,7 @@ function getSetup () { const app = getApp({ baseUriPath: '', stores, + eventBus, }); return { diff --git a/test/unit/routes/metrics.test.js b/test/unit/routes/metrics.test.js index 39e67a9222..610f47594b 100644 --- a/test/unit/routes/metrics.test.js +++ b/test/unit/routes/metrics.test.js @@ -6,6 +6,9 @@ const supertest = require('supertest'); const logger = require('../../../lib/logger'); const getApp = require('../../../lib/app'); +const { EventEmitter } = require('events'); +const eventBus = new EventEmitter(); + test.beforeEach(() => { logger.setLevel('FATAL'); }); @@ -15,6 +18,7 @@ function getSetup () { const app = getApp({ baseUriPath: '', stores, + eventBus, }); return { diff --git a/test/unit/routes/strategies.test.js b/test/unit/routes/strategies.test.js index 14bc29ad2e..219854e6cf 100644 --- a/test/unit/routes/strategies.test.js +++ b/test/unit/routes/strategies.test.js @@ -6,6 +6,9 @@ const supertest = require('supertest'); const logger = require('../../../lib/logger'); const getApp = require('../../../lib/app'); +const { EventEmitter } = require('events'); +const eventBus = new EventEmitter(); + test.beforeEach(() => { logger.setLevel('FATAL'); }); @@ -15,6 +18,7 @@ test('should add version numbers for /stategies', t => { const app = getApp({ baseUriPath: '', stores, + eventBus, }); const request = supertest(app);