From b8bc414a64503805d2da44ddcbf45737c90247a5 Mon Sep 17 00:00:00 2001 From: Dave Kelsey Date: Thu, 20 Sep 2018 09:00:28 +0100 Subject: [PATCH] [Master] dynamic logging, ability to disable explorer (#4313) * dynamic logging, ability to disable explorer closes #4308 closes #4307 Signed-off-by: Dave Kelsey * fix test due to previous PR Signed-off-by: Dave Kelsey --- packages/composer-rest-server/cli.js | 16 ++ packages/composer-rest-server/lib/util.js | 11 ++ .../server/boot/composer-discovery.js | 97 ++++++++++++ .../composer-rest-server/server/boot/root.js | 10 +- .../composer-rest-server/server/composer.json | 1 + .../composer-rest-server/server/server.js | 26 ++- .../composer-rest-server/server/servercmd.js | 7 + packages/composer-rest-server/test/cli.js | 149 ++++++++++++++++++ .../composer-rest-server/test/lib/util.js | 2 +- packages/composer-rest-server/test/root.js | 24 ++- .../test/server/server.js | 24 ++- .../test/server/servercmd.js | 45 ++++-- packages/composer-rest-server/test/system.js | 40 +++++ .../integrating/getting-started-rest-api.md | 28 ++++ .../lib/businessnetworkconnector.js | 31 ++++ .../test/businessnetworkconnector.js | 89 ++++++++++- 16 files changed, 579 insertions(+), 21 deletions(-) diff --git a/packages/composer-rest-server/cli.js b/packages/composer-rest-server/cli.js index 95168d62ad..f55c31eb3b 100755 --- a/packages/composer-rest-server/cli.js +++ b/packages/composer-rest-server/cli.js @@ -39,6 +39,8 @@ const yargs = require('yargs') .option('t', { alias: 'tls', describe: 'Enable TLS security for the REST API', type: 'boolean', default: process.env.COMPOSER_TLS || false }) .option('e', { alias: 'tlscert', describe: 'File containing the TLS certificate', type: 'string', default: process.env.COMPOSER_TLS_CERTIFICATE || defaultTlsCertificate }) .option('k', { alias: 'tlskey', describe: 'File containing the TLS private key', type: 'string', default: process.env.COMPOSER_TLS_KEY || defaultTlsKey }) + .option('u', { alias: 'explorer', describe: 'Enable the test explorer web interface', type: 'boolean', default: process.env.COMPOSER_USEEXPLORER || true }) + .option('d', { alias: 'loggingkey', describe: 'Specify the key to enable dynamic logging for the rest server (just pressing enter will not enable this feature)', type: 'string', default: process.env.COMPOSER_LOGGINGKEY || undefined }) .alias('v', 'version') .version(version) .help('h') @@ -65,6 +67,8 @@ if (interactive) { apikey: answers.apikey, authentication: answers.authentication, multiuser: answers.multiuser, + explorer: answers.explorer, + loggingkey: answers.loggingkey, websockets: answers.websockets, tls: answers.tls, tlscert: answers.tlscert, @@ -79,6 +83,8 @@ if (interactive) { '-y': 'apikey', '-a': 'authentication', '-m': 'multiuser', + '-u': 'explorer', + '-d': 'loggingkey', '-w': 'websockets', '-t': 'tls', '-e': 'tlscert', @@ -113,6 +119,8 @@ if (interactive) { apikey: yargs.y, authentication: yargs.a, multiuser: yargs.m, + explorer: yargs.u, + loggingkey: yargs.d, websockets: yargs.w, tls: yargs.t, tlscert: yargs.e, @@ -135,15 +143,23 @@ module.exports = promise.then((composer) => { return server.listen(app.get('port'), () => { app.emit('started'); let baseUrl = app.get('url').replace(/\/$/, ''); + // eslint-disable-next-line no-console console.log('Web server listening at: %s', baseUrl); if (app.get('loopback-component-explorer')) { let explorerPath = app.get('loopback-component-explorer').mountPath; + // eslint-disable-next-line no-console console.log('Browse your REST API at %s%s', baseUrl, explorerPath); } + const composerConfig = app.get('composer'); + if (composerConfig && composerConfig.loggingkey) { + // eslint-disable-next-line no-console + console.log('Rest Server dynamic logging is enabled'); + } }); }) .catch((error) => { + // eslint-disable-next-line no-console console.error(error); process.exit(1); }); diff --git a/packages/composer-rest-server/lib/util.js b/packages/composer-rest-server/lib/util.js index 94eadb5e3d..38b1026760 100644 --- a/packages/composer-rest-server/lib/util.js +++ b/packages/composer-rest-server/lib/util.js @@ -86,6 +86,17 @@ class Util { return answers.authentication; } }, + { + name: 'explorer', + type: 'confirm', + message: 'Specify if you want to enable the explorer test interface:', + default: true + }, + { + name: 'loggingkey', + type: 'input', + message: 'Specify a key if you want to enable dynamic logging:' + }, { name: 'websockets', type: 'confirm', diff --git a/packages/composer-rest-server/server/boot/composer-discovery.js b/packages/composer-rest-server/server/boot/composer-discovery.js index dcff176dc1..010e851fc5 100644 --- a/packages/composer-rest-server/server/boot/composer-discovery.js +++ b/packages/composer-rest-server/server/boot/composer-discovery.js @@ -482,6 +482,68 @@ function registerGetHistorianRecordsByIDMethod(app, dataSource, System, connecto } +/** + * Register the 'setLogLevel' Composer system method. + * @param {Object} app The LoopBack application. + * @param {Object} dataSource The LoopBack data source. + */ +function registerSetLogLevelMethod(app, dataSource) { + const Admin = app.models.Admin; + const connector = dataSource.connector; + + // Define and register the method. + Admin.setLogLevel = (key, newlevel, outputToConsole, outputToFile, callback) => { + if (app.get('composer').loggingkey === key) { + connector.setLogLevel(newlevel, outputToConsole, outputToFile, callback); + } else { + throw new Error('Unauthorized'); + } + }; + Admin.remoteMethod( + 'setLogLevel', { + description: 'set logging level on rest server', + accepts: [{ + arg: 'loggingkey', + type: 'string', + required: true, + http: { + source: 'path' + } + }, { + arg: 'newlevel', + type: 'string', + required: true, + http: { + source: 'path' + } + }, { + arg: 'outputToConsole', + type: 'boolean', + required: true, + http: { + source: 'path' + } + }, { + arg: 'outputToFile', + type: 'boolean', + required: true, + http: { + source: 'path' + } + }], + returns: { + type: [ 'object' ], + root: true + }, + http: { + verb: 'post', + path: '/loglevel/:loggingkey/:newlevel/:outputToConsole/:outputToFile' + } + } + ); + +} + /** * Discover all of the model definitions in the specified LoopBack data source. * @param {Object} dataSource The LoopBack data source. @@ -638,6 +700,33 @@ function createSystemModel(app, dataSource) { } +/** + * Create all of the Composer system models. + * @param {Object} app The LoopBack application. + * @param {Object} dataSource The LoopBack data source. + */ +function createAdminModel(app, dataSource) { + + // Create the system model schema. + let modelSchema = { + name: 'Admin', + description: 'Rest server methods', + plural: '/admin', + base: 'Model' + }; + modelSchema = updateModelSchema(modelSchema); + + // Create the system model which is an anchor for all system methods. + const Admin = app.loopback.createModel(modelSchema); + + // Register the system model. + app.model(Admin, { + dataSource: dataSource, + public: true + }); + +} + /** * Create all of the Composer system models. * @param {Object} app The LoopBack application. @@ -687,6 +776,7 @@ function registerSystemMethods(app, dataSource) { registerGetAllHistorianRecordsMethod, registerGetHistorianRecordsByIDMethod ]; + registerMethods.forEach((registerMethod) => { registerMethod(app, dataSource, System, connector); }); @@ -839,6 +929,13 @@ module.exports = function (app, callback) { // Register the system methods. registerSystemMethods(app, dataSource); + createAdminModel(app, dataSource); + // enable dynamic logging support if requested + if (app.settings.composer.loggingkey) { + registerSetLogLevelMethod(app, dataSource); + } + + // Create the query model createQueryModel(app, dataSource); diff --git a/packages/composer-rest-server/server/boot/root.js b/packages/composer-rest-server/server/boot/root.js index 93964f57a6..56edc5a9da 100644 --- a/packages/composer-rest-server/server/boot/root.js +++ b/packages/composer-rest-server/server/boot/root.js @@ -17,7 +17,15 @@ module.exports = function(server) { let router = server.loopback.Router(); router.get('/', (req, res, next) => { - res.redirect('/explorer/'); + const useExplorer = server.settings.composer.explorer === undefined || + server.settings.composer.explorer === null || + server.settings.composer.explorer === true; + + if (useExplorer) { + res.redirect('/explorer/'); + } else { + res.redirect('/status/'); + } }); router.get('/status', server.loopback.status()); server.use(router); diff --git a/packages/composer-rest-server/server/composer.json b/packages/composer-rest-server/server/composer.json index 8ce2474493..b21c20283f 100644 --- a/packages/composer-rest-server/server/composer.json +++ b/packages/composer-rest-server/server/composer.json @@ -7,6 +7,7 @@ "port": 3000, "authentication": false, "multiuser": false, + "explorer": true, "websockets": true, "tls": false, "tlscert": "/path/to/cert.pem", diff --git a/packages/composer-rest-server/server/server.js b/packages/composer-rest-server/server/server.js index 6888940dc3..98442c25fa 100755 --- a/packages/composer-rest-server/server/server.js +++ b/packages/composer-rest-server/server/server.js @@ -45,6 +45,7 @@ module.exports = function (composer) { // model dependent on whether or not the multiple user option has been specified. const models = require('./model-config.json'); const multiuser = !!composer.multiuser; + const useExplorer = composer.explorer === undefined || composer.explorer === null || composer.explorer === true; models.Card.public = multiuser; // Allow environment variable overrides for the datasources.json file. @@ -54,17 +55,22 @@ module.exports = function (composer) { } // Call the boot process which will load all models and execute all boot scripts. + let loopbackExplorer = null; + if (useExplorer) { + loopbackExplorer = { + mountPath: '/explorer', + uiDirs: [ + path.resolve(__dirname, '..', 'public') + ] + }; + } + const bootOptions = { appRootDir: __dirname, models: models, dataSources: dataSources, components: { - 'loopback-component-explorer': { - mountPath: '/explorer', - uiDirs: [ - path.resolve(__dirname, '..', 'public') - ] - } + 'loopback-component-explorer': loopbackExplorer } }; boot(app, bootOptions, (error) => { @@ -221,15 +227,23 @@ if (require.main === module) { return server.listen(() => { app.emit('started'); let baseUrl = app.get('url').replace(/\/$/, ''); + // eslint-disable-next-line no-console console.log('Web server listening at: %s', baseUrl); if (app.get('loopback-component-explorer')) { let explorerPath = app.get('loopback-component-explorer').mountPath; + // eslint-disable-next-line no-console console.log('Browse your REST API at %s%s', baseUrl, explorerPath); } + const composerConfig = app.get('composer'); + if (composerConfig && composerConfig.loggingkey) { + // eslint-disable-next-line no-console + console.log('Rest Server dynamic logging is enabled'); + } }); }) .catch((error) => { + // eslint-disable-next-line no-console console.error(error); process.exit(1); }); diff --git a/packages/composer-rest-server/server/servercmd.js b/packages/composer-rest-server/server/servercmd.js index d53a9f0574..f65495f486 100644 --- a/packages/composer-rest-server/server/servercmd.js +++ b/packages/composer-rest-server/server/servercmd.js @@ -40,12 +40,19 @@ function startRestServer(composer){ return app.listen(function () { app.emit('started'); let baseUrl = app.get('url').replace(/\/$/, ''); + // eslint-disable-next-line no-console console.log('Web server listening at: %s', baseUrl); /* istanbul ignore next */ if (app.get('loopback-component-explorer')) { let explorerPath = app.get('loopback-component-explorer').mountPath; + // eslint-disable-next-line no-console console.log('Browse your REST API at %s%s', baseUrl, explorerPath); } + const composerConfig = app.get('composer'); + if (composerConfig && composerConfig.loggingkey) { + // eslint-disable-next-line no-console + console.log('Rest Server dynamic logging is enabled'); + } }); }); diff --git a/packages/composer-rest-server/test/cli.js b/packages/composer-rest-server/test/cli.js index 10c7279619..d90a4b8851 100644 --- a/packages/composer-rest-server/test/cli.js +++ b/packages/composer-rest-server/test/cli.js @@ -86,6 +86,8 @@ describe('composer-rest-server CLI unit tests', () => { sinon.assert.calledOnce(Util.getConnectionSettings); const settings = { card: 'admin@org-acme-biznet', + explorer: undefined, + loggingkey: undefined, namespaces: 'always', apikey: undefined, authentication: false, @@ -155,6 +157,8 @@ describe('composer-rest-server CLI unit tests', () => { sinon.assert.notCalled(Util.getConnectionSettings); const settings = { card: 'admin@org-acme-biznet', + explorer: true, + loggingkey: undefined, namespaces: 'always', apikey: undefined, port: undefined, @@ -201,6 +205,8 @@ describe('composer-rest-server CLI unit tests', () => { sinon.assert.notCalled(Util.getConnectionSettings); const settings = { card: 'admin@org-acme-biznet', + explorer: true, + loggingkey: undefined, namespaces: 'always', apikey: APIKEY, port: undefined, @@ -246,6 +252,8 @@ describe('composer-rest-server CLI unit tests', () => { sinon.assert.notCalled(Util.getConnectionSettings); const settings = { card: 'admin@org-acme-biznet', + explorer: true, + loggingkey: undefined, namespaces: 'always', apikey: undefined, port: undefined, @@ -291,6 +299,8 @@ describe('composer-rest-server CLI unit tests', () => { sinon.assert.notCalled(Util.getConnectionSettings); const settings = { card: 'admin@org-acme-biznet', + explorer: true, + loggingkey: undefined, namespaces: 'always', port: undefined, apikey: undefined, @@ -343,9 +353,105 @@ describe('composer-rest-server CLI unit tests', () => { sinon.assert.calledWith(emit, 'started'); sinon.assert.calledWith(console.log, sinon.match(/Web server listening at/)); sinon.assert.calledWith(console.log, sinon.match(/Browse your REST API at/)); + sinon.assert.neverCalledWith(console.log, sinon.match(/dynamic logging/)); }); }); + it('should check the explorer flag and set it as requested', () => { + let listen = sinon.stub(); + let get = sinon.stub(); + get.withArgs('port').returns(3000); + process.argv = [ + process.argv0, 'cli.js', + '-c', 'admin@org-acme-biznet', + '-u', 'false' + ]; + delete require.cache[require.resolve('yargs')]; + const server = sinon.stub().resolves({ + app: { + get + }, + server: { + listen + } + }); + return proxyquire('../cli', { + clear: () => { }, + chalk: { + yellow: () => { return ''; } + }, + './server/server': server + }).then(() => { + sinon.assert.notCalled(Util.getConnectionSettings); + const settings = { + card: 'admin@org-acme-biznet', + explorer: false, + loggingkey: undefined, + namespaces: 'always', + apikey: undefined, + port: undefined, + authentication: false, + multiuser: false, + websockets: true, + tls: false, + tlscert: defaultTlsCertificate, + tlskey: defaultTlsKey + }; + sinon.assert.calledWith(server, settings); + sinon.assert.calledOnce(listen); + listen.args[0][0].should.equal(3000); + listen.args[0][1].should.be.a('function'); + }); + }); + + it('should check the logging flag and set it as requested', () => { + let listen = sinon.stub(); + let get = sinon.stub(); + get.withArgs('port').returns(3000); + process.argv = [ + process.argv0, 'cli.js', + '-c', 'admin@org-acme-biznet', + '-d', '123457' + ]; + delete require.cache[require.resolve('yargs')]; + const server = sinon.stub().resolves({ + app: { + get + }, + server: { + listen + } + }); + return proxyquire('../cli', { + clear: () => { }, + chalk: { + yellow: () => { return ''; } + }, + './server/server': server + }).then(() => { + sinon.assert.notCalled(Util.getConnectionSettings); + const settings = { + card: 'admin@org-acme-biznet', + explorer: true, + loggingkey: '123457', + namespaces: 'always', + apikey: undefined, + port: undefined, + authentication: false, + multiuser: false, + websockets: true, + tls: false, + tlscert: defaultTlsCertificate, + tlskey: defaultTlsKey + }; + sinon.assert.calledWith(server, settings); + sinon.assert.calledOnce(listen); + listen.args[0][0].should.equal(3000); + listen.args[0][1].should.be.a('function'); + }); + }); + + it('should start and log information when running without explorer', () => { let listen = sinon.stub(); let emit = sinon.stub(); @@ -380,6 +486,49 @@ describe('composer-rest-server CLI unit tests', () => { sinon.assert.calledWith(emit, 'started'); sinon.assert.calledWith(console.log, sinon.match(/Web server listening at/)); sinon.assert.neverCalledWith(console.log, sinon.match(/Browse your REST API at/)); + sinon.assert.neverCalledWith(console.log, sinon.match(/dynamic logging/)); + }); + }); + + + it('should start and log information when dynamic logging enabled', () => { + let listen = sinon.stub(); + let emit = sinon.stub(); + let get = sinon.stub(); + get.withArgs('port').returns(3000); + get.withArgs('url').returns('http://localhost:3000'); + get.withArgs('loopback-component-explorer').returns(true); + get.withArgs('composer').returns({loggingkey: '1234'}); + process.argv = [ + process.argv0, 'cli.js', + '-c', 'admin@org-acme-biznet', + '-d', '1234' + ]; + delete require.cache[require.resolve('yargs')]; + const server = sinon.stub().resolves({ + app: { + emit, + get + }, + server: { + listen + } + }); + return proxyquire('../cli', { + clear: () => { }, + chalk: { + yellow: () => { return ''; } + }, + './server/server': server + }).then(() => { + sinon.assert.calledOnce(listen); + listen.args[0][0].should.equal(3000); + listen.args[0][1](); + sinon.assert.calledOnce(emit); + sinon.assert.calledWith(emit, 'started'); + sinon.assert.calledWith(console.log, sinon.match(/Web server listening at/)); + sinon.assert.calledWith(console.log, sinon.match(/Browse your REST API at/)); + sinon.assert.calledWith(console.log, sinon.match(/dynamic logging/)); }); }); diff --git a/packages/composer-rest-server/test/lib/util.js b/packages/composer-rest-server/test/lib/util.js index a4076c0564..ad78208a29 100644 --- a/packages/composer-rest-server/test/lib/util.js +++ b/packages/composer-rest-server/test/lib/util.js @@ -63,7 +63,7 @@ describe('Util', () => { it('should interactively ask for the connection settings', async () => { const questions = await getAllQuestions(); const names = questions.map((question) => question.name); - names.should.include.members(['card', 'namespaces', 'apikey', 'authentication', 'multiuser', 'websockets', 'tls', 'tlscert', 'tlskey']); + names.should.include.members(['card', 'namespaces', 'apikey', 'authentication', 'multiuser', 'websockets', 'tls', 'tlscert', 'tlskey', 'explorer', 'loggingkey']); }); it('should validate the length of the business network card', async () => { diff --git a/packages/composer-rest-server/test/root.js b/packages/composer-rest-server/test/root.js index 8392b56658..60e3abd034 100644 --- a/packages/composer-rest-server/test/root.js +++ b/packages/composer-rest-server/test/root.js @@ -27,6 +27,7 @@ chai.use(require('chai-http')); describe('Root REST API unit tests', () => { let app; + let app2; let idCard; let adminConnection; @@ -67,6 +68,17 @@ describe('Root REST API unit tests', () => { }) .then((result) => { app = result.app; + }) + .then(() => { + return server({ + card: 'admin@bond-network', + explorer: false, + cardStore, + namespaces: 'never' + }); + }) + .then((result) => { + app2 = result.app; }); }); @@ -76,7 +88,7 @@ describe('Root REST API unit tests', () => { describe('GET /', () => { - it('should redirect to the REST API explorer', () => { + it('should redirect to the REST API explorer if explorer enabled', () => { return chai.request(app) .get('/') .then((res) => { @@ -85,6 +97,16 @@ describe('Root REST API unit tests', () => { }); }); + it('should redirect to the REST API status if explorer disabled', () => { + return chai.request(app2) + .get('/') + .then((res) => { + res.redirects.should.have.lengthOf(1); + res.redirects[0].should.match(/\/status\/$/); + }); + }); + + }); describe('GET /status', () => { diff --git a/packages/composer-rest-server/test/server/server.js b/packages/composer-rest-server/test/server/server.js index c6082e1a12..23c8b30a43 100644 --- a/packages/composer-rest-server/test/server/server.js +++ b/packages/composer-rest-server/test/server/server.js @@ -25,7 +25,7 @@ const server = require('../../server/server'); const WebSocket = require('ws'); const chai = require('chai'); -chai.should(); +const should = chai.should(); chai.use(require('chai-as-promised')); const sinon = require('sinon'); @@ -242,6 +242,28 @@ describe('server', () => { }); }); + it('should have explorer by default', () => { + composerConfig.authentication = true; + return server(composerConfig) + .then((result) => { + result.app.should.exist; + result.server.should.exist; + result.app.settings['loopback-component-explorer'].should.exist; + }); + }); + + it('should disable explorer if requested', () => { + composerConfig.authentication = true; + composerConfig.explorer = false; + return server(composerConfig) + .then((result) => { + result.app.should.exist; + result.server.should.exist; + should.not.exist(result.app.settings['loopback-component-explorer']); + }); + }); + + it('should enable authentication if specified with providers loaded from the environment', () => { process.env.COMPOSER_PROVIDERS = JSON.stringify({ ldap: { diff --git a/packages/composer-rest-server/test/server/servercmd.js b/packages/composer-rest-server/test/server/servercmd.js index ff9a2ba666..7dfa6a801c 100644 --- a/packages/composer-rest-server/test/server/servercmd.js +++ b/packages/composer-rest-server/test/server/servercmd.js @@ -55,11 +55,7 @@ describe('servercmd', () => { get.withArgs('url').returns('http://localhost:3000'); get.withArgs('loopback-component-explorer').returns(true); process.argv = [ - process.argv0, 'cli.js', - '-p', 'defaultProfile', - '-n', 'org-acme-biznet', - '-i', 'admin', - '-s', 'adminpw' + process.argv0, 'cli.js' ]; delete require.cache[require.resolve('yargs')]; const server = sinon.stub(); @@ -78,6 +74,7 @@ describe('servercmd', () => { sinon.assert.calledWith(emit, 'started'); sinon.assert.calledWith(console.log, sinon.match(/Web server listening at/)); sinon.assert.calledWith(console.log, sinon.match(/Browse your REST API at/)); + sinon.assert.neverCalledWith(console.log, sinon.match(/dynamic logging/)); }); }); @@ -87,11 +84,7 @@ describe('servercmd', () => { let get = sinon.stub(); get.withArgs('url').returns('http://localhost:3000'); process.argv = [ - process.argv0, 'cli.js', - '-p', 'defaultProfile', - '-n', 'org-acme-biznet', - '-i', 'admin', - '-s', 'adminpw' + process.argv0, 'cli.js' ]; delete require.cache[require.resolve('yargs')]; const server = sinon.stub(); @@ -110,7 +103,39 @@ describe('servercmd', () => { sinon.assert.calledWith(emit, 'started'); sinon.assert.calledWith(console.log, sinon.match(/Web server listening at/)); sinon.assert.neverCalledWith(console.log, sinon.match(/Browse your REST API at/)); + sinon.assert.neverCalledWith(console.log, sinon.match(/dynamic logging/)); }); }); + it('should start and log information when dynamic debug enabled', () => { + let listen = sinon.stub(); + let emit = sinon.stub(); + let get = sinon.stub(); + get.withArgs('url').returns('http://localhost:3000'); + get.withArgs('composer').returns({loggingkey: '1234'}); + process.argv = [ + process.argv0, 'cli.js' + ]; + delete require.cache[require.resolve('yargs')]; + const server = sinon.stub(); + server.resolves({ + listen: listen, + emit: emit, + get: get + }); + return proxyquire('../../server/servercmd', { + './server': server + }).startRestServer(composerConfig) + .then(() => { + sinon.assert.calledOnce(listen); + listen.args[0][0](); + sinon.assert.calledOnce(emit); + sinon.assert.calledWith(emit, 'started'); + sinon.assert.calledWith(console.log, sinon.match(/Web server listening at/)); + sinon.assert.calledWith(console.log, sinon.match(/dynamic logging/)); + sinon.assert.neverCalledWith(console.log, sinon.match(/Browse your REST API at/)); + }); + }); + + }); diff --git a/packages/composer-rest-server/test/system.js b/packages/composer-rest-server/test/system.js index 21ea593ab5..f3ab57abe4 100644 --- a/packages/composer-rest-server/test/system.js +++ b/packages/composer-rest-server/test/system.js @@ -135,6 +135,7 @@ describe('System REST API unit tests', () => { return server({ card: 'admin@bond-network', cardStore, + loggingkey: '123457', namespaces: 'never' }); }) @@ -362,6 +363,45 @@ describe('System REST API unit tests', () => { }); + describe('POST /loglevel', () => { + + it('should allow changing of log levels', async () => { + let res = await chai.request(app) + .post('/api/admin/loglevel/123457/composer%5Bdebug%5D%3A*/true/false'); + res.should.be.json; + let output = res.body; + output.should.deep.equal({ + oldLevel: 'composer[warn]:*', + newLevel: 'composer[debug]:*', + oldConsoleLevel: 'none', + newConsoleLevel: 'silly', + oldFileLevel: 'silly', + newFileLevel: 'none' + }); + + res = await chai.request(app) + .post('/api/admin/loglevel/123457/composer%5Berror%5D%3A*/false/true'); + res.should.be.json; + output = res.body; + output.should.deep.equal({ + oldLevel: 'composer[debug]:*', + newLevel: 'composer[error]:*', + oldConsoleLevel: 'silly', + newConsoleLevel: 'none', + oldFileLevel: 'none', + newFileLevel: 'silly' + }); + }); + + it('should reject if key is not correct', () => { + return chai.request(app) + .post('/api/admin/loglevel/654321/composer%5Bdebug%5D%3A*/true/false') + .catch((err) => { + err.response.should.have.status(500); + }); + }); + }); + describe('GET /historian', () => { it('should return all of the transactions', () => { diff --git a/packages/composer-website/jekylldocs/integrating/getting-started-rest-api.md b/packages/composer-website/jekylldocs/integrating/getting-started-rest-api.md index e874e68b03..6cc3a3657d 100644 --- a/packages/composer-website/jekylldocs/integrating/getting-started-rest-api.md +++ b/packages/composer-website/jekylldocs/integrating/getting-started-rest-api.md @@ -69,10 +69,38 @@ Options: -t, --tls Enable TLS security for the REST API [boolean] [default: false] -e, --tlscert File containing the TLS certificate [string] [default: "/usr/local/lib/node_modules/composer-rest-server/cert.pem"] -k, --tlskey File containing the TLS private key [string] [default: "/usr/local/lib/node_modules/composer-rest-server/key.pem"] + -u, --explorer Enable the test explorer web interface [boolean] [default: true] + -d, --loggingkey Specify the key to enable dynamic logging for the rest server (just pressing enter will not enable this feature) [string] -h, --help Show help [boolean] -v, --version Show version number [boolean] ``` +#### Dynamic Rest Server logging +Logging of the rest server can be controlled in the same way as other composer applications. However to do so requires the rest server to be started with environment variables set and at that point it logs continuously. This has disadvantages of creating large logs, reducing performance of the rest server and if logging is required the rest server needs to be restarted which may be a cumbersome task to achieve. + +You can enable dynamic rest server logging by specifying a key. This key is used as part of the URL path. This means that unless someone knows the key, even if they are authenticated, they cannot change the logging of the rest server. + +For example if a key of 45645-575835-A58684 has been set for the logging key, you can either use the rest server explorer (under the Admin section) to alter logging or write an application or use `curl`. For example in a simple case, assuming authentication is disabled you could do + +``` +curl -X POST 'http://localhost:3000/api/admin/loglevel/45645-575835-A58684/composer%5Bdebug%5D%3A*/true/false' +``` + +This sets the debug to `composer[debug]:*`, the first boolean value says whether to send output to the console, the second boolean value says whether to send it to the file system. In the above example it states to send logging to the console and not to the file system. The response looks like this + +``` +{ + "oldlevel":"composer[error]:*", + "newlevel":"composer[debug]:*", + "oldConsoleLevel":"none", + "newConsoleLevel":"silly", + "oldFileLevel":"silly", + "newFileLevel":"none" +} +``` +indicating what the values were before and what they are now. + + #### Looking at the generated APIs Launch your browser and go to the URL given (http://0.0.0.0:3000/explorer). You'll see a screen similar to this. diff --git a/packages/loopback-connector-composer/lib/businessnetworkconnector.js b/packages/loopback-connector-composer/lib/businessnetworkconnector.js index b2c37e07c9..4cb8738da2 100644 --- a/packages/loopback-connector-composer/lib/businessnetworkconnector.js +++ b/packages/loopback-connector-composer/lib/businessnetworkconnector.js @@ -29,6 +29,7 @@ const TransactionDeclaration = require('composer-common').TransactionDeclaration const QueryAnalyzer = require('composer-common').QueryAnalyzer; const util = require('util'); const FilterParser = require('./filterparser'); +const Logger = require('composer-common').Logger; /** * A Loopback connector for exposing the Blockchain Solution Framework to Loopback enabled applications. @@ -1035,6 +1036,36 @@ class BusinessNetworkConnector extends Connector { }); } + /** + * provide dynamic debug capabilities for loopback applications + * @param {string} newlevel the new logging level + * @param {boolean} outputToConsole true to ensure all output to the console + * @param {boolean} outputToFile true to ensure all output to the configured log file + * @param {function} callback The callback to call when complete. + */ + setLogLevel(newlevel, outputToConsole, outputToFile, callback) { + debug('setLogLevel',newlevel, outputToConsole); + const currentLogCfg = Logger.getLoggerCfg(); + const currentLogSetting = currentLogCfg.debug; + const currentLogConsole = currentLogCfg.console.maxLevel; + const currentLogFile = currentLogCfg.file.maxLevel; + currentLogCfg.debug = newlevel; + + currentLogCfg.console.maxLevel = outputToConsole ? 'silly' : 'none'; + currentLogCfg.file.maxLevel = outputToFile ? 'silly' : 'none'; + + Logger.setLoggerCfg(currentLogCfg, true); + const result = { + oldLevel: currentLogSetting, + newLevel: newlevel, + oldConsoleLevel: currentLogConsole, + newConsoleLevel: currentLogCfg.console.maxLevel, + oldFileLevel: currentLogFile, + newFileLevel: currentLogCfg.file.maxLevel + }; + callback(null, result); + } + /** * Execute a named query and returns the results * @param {string} queryName The name of the query to execute diff --git a/packages/loopback-connector-composer/test/businessnetworkconnector.js b/packages/loopback-connector-composer/test/businessnetworkconnector.js index a65aa865c2..8853a803f2 100644 --- a/packages/loopback-connector-composer/test/businessnetworkconnector.js +++ b/packages/loopback-connector-composer/test/businessnetworkconnector.js @@ -37,6 +37,7 @@ const Serializer = require('composer-common').Serializer; const TransactionRegistry = require('composer-client/lib/transactionregistry'); const Historian = require('composer-client/lib/historian'); const TypeNotFoundException = require('composer-common/lib/typenotfoundexception'); +const Logger = require('composer-common').Logger; const chai = require('chai'); const should = chai.should(); @@ -60,7 +61,7 @@ describe('BusinessNetworkConnector', () => { o Long theLong optional --> Member theMember optional } - + enum BaseEnum { o WOW o SUCH @@ -2783,6 +2784,92 @@ describe('BusinessNetworkConnector', () => { }); + describe('#setLogLevel', () => { + + + beforeEach(() => { + }); + + it('should change logging settings 1', () => { + sandbox.stub(Logger, 'getLoggerCfg').returns({ + debug: 'composer[error]:*', + console: { + maxLevel: 'none' + }, + file: { + maxLevel: 'silly' + } + }); + const expectedNewLogger = { + debug: 'debugstr', + console: { + maxLevel: 'silly' + }, + file: { + maxLevel: 'none' + } + }; + const expectedResult = { + oldLevel: 'composer[error]:*', + newLevel: 'debugstr', + oldConsoleLevel: 'none', + newConsoleLevel: 'silly', + oldFileLevel: 'silly', + newFileLevel: 'none' + }; + + sandbox.stub(Logger, 'setLoggerCfg'); + const cb = sinon.stub(); + + testConnector.setLogLevel('debugstr', true, false, cb); + sinon.assert.calledOnce(cb); + sinon.assert.calledWith(cb, null, expectedResult); + sinon.assert.calledOnce(Logger.getLoggerCfg); + sinon.assert.calledOnce(Logger.setLoggerCfg); + sinon.assert.calledWith(Logger.setLoggerCfg, expectedNewLogger, true); + }); + + it('should change logging settings 2', () => { + sandbox.stub(Logger, 'getLoggerCfg').returns({ + debug: 'composer[error]:*', + console: { + maxLevel: 'silly' + }, + file: { + maxLevel: 'none' + } + }); + const expectedNewLogger = { + debug: 'debugstr', + console: { + maxLevel: 'none' + }, + file: { + maxLevel: 'silly' + } + }; + const expectedResult = { + oldLevel: 'composer[error]:*', + newLevel: 'debugstr', + oldConsoleLevel: 'silly', + newConsoleLevel: 'none', + oldFileLevel: 'none', + newFileLevel: 'silly' + }; + + sandbox.stub(Logger, 'setLoggerCfg'); + const cb = sinon.stub(); + + testConnector.setLogLevel('debugstr', false, true, cb); + sinon.assert.calledOnce(cb); + sinon.assert.calledWith(cb, null, expectedResult); + sinon.assert.calledOnce(Logger.getLoggerCfg); + sinon.assert.calledOnce(Logger.setLoggerCfg); + sinon.assert.calledWith(Logger.setLoggerCfg, expectedNewLogger, true); + }); + + }); + describe('#executeQuery', () => { beforeEach(() => {