From 45310089c91dd7894086c4f6735f026ab0e2950f Mon Sep 17 00:00:00 2001 From: David Bradford Date: Tue, 10 Jan 2017 12:02:47 -0600 Subject: [PATCH 01/14] Issue 9: Add support to create any type of synthetic --- bin/cmds/create.js | 47 +++++++- lib/orchestrator/CreateMonitorOrchestrator.js | 46 ++++---- lib/service/NewRelicService.js | 26 +++-- lib/service/SyntheticsListFileService.js | 12 +- .../CreateMonitorOrchestratorTest.js | 30 ++++- test/lib/service/NewRelicSerivceTest.js | 70 ++++++++++++ .../service/SyntheticsListFileServiceTest.js | 106 ++++++++++++++++++ 7 files changed, 298 insertions(+), 39 deletions(-) diff --git a/bin/cmds/create.js b/bin/cmds/create.js index cdf537a..683fd47 100644 --- a/bin/cmds/create.js +++ b/bin/cmds/create.js @@ -1,5 +1,6 @@ const dependencies = require('../../lib/dependency'); const logger = require('winston'); +const _ = require('lodash'); exports.command = 'create'; exports.desc = 'Create new synthetics monitor'; @@ -11,27 +12,59 @@ exports.builder = { }, filename: { alias: 'f', - desc: 'Filename to place synthetic code', - demand: 1 + desc: 'Filename to place synthetic code (require for SCRIPT_API and SCRIPT_BROWSER synthetics)' }, type: { alias: 't', desc: 'Type of synthetic to create', + choices: ['SIMPLE', 'BROWSER', 'SCRIPT_BROWSER', 'SCRIPT_API'], default: 'SCRIPT_BROWSER' }, frequency: { - desc: 'Frequency to run synthetic', - default: 10 + desc: 'Frequency to run synthetic(in minutes)', + choices: [1, 5, 10, 15, 30, 60, 360, 720, 1440], + default: 10, + type: 'number' }, locations: { desc: 'Locations to run synthetic', - default: ['AWS_US_WEST_1'] + default: ['AWS_US_WEST_1'], + type: 'array' }, + uri: { + alias: 'u', + desc: 'URI for synthetic (required for SIMPLE and BROWSER synthetics)', + type: 'string' + } +} + +function validate(argv) { + if ((argv.type === 'SIMPLE') || (argv.type === 'BROWSER')) { + if (_.isNil(argv.uri)) { + throw new Error('ERROR: Missing uri argument'); + } + + if (!_.isNil(argv.filename)) { + throw new Error('ERROR: Unexpected filename argument'); + } + } + + if ((argv.type ==='SCRIPT_API') || (argv.type === 'SCRIPT_BROWSER')) { + if (_.isNil(argv.filename)) { + throw new Error('ERROR: Missing filename argument'); + } + + if (!_.isNil(argv.uri)) { + throw new Error('ERROR: Unexpected uri argument'); + } + } } exports.handler = function (argv) { require('../../lib/config/LoggingConfig')(argv); + validate(argv); + const config = require('../../lib/config/SyntheticsConfig').getConfig(argv); logger.verbose('Create: ' + argv.name + ':' + argv.filename); @@ -42,6 +75,8 @@ exports.handler = function (argv) { argv.locations, argv.type, argv.frequency, - argv.filename + argv.filename, + null, + argv.uri ); } \ No newline at end of file diff --git a/lib/orchestrator/CreateMonitorOrchestrator.js b/lib/orchestrator/CreateMonitorOrchestrator.js index 5c80ed6..d203014 100644 --- a/lib/orchestrator/CreateMonitorOrchestrator.js +++ b/lib/orchestrator/CreateMonitorOrchestrator.js @@ -7,17 +7,21 @@ function syntheticUrlToId(syntheticUrl) { return _.last(syntheticUrl.split('/')); } -function createSyntheticFile(syntheticsFileService, filename, initialContent, callback) { - syntheticsFileService.exists(filename, (exists) => { - if (!exists) { - logger.verbose('Synthetics file does not exist, creating: ' + filename); - syntheticsFileService.createFile(filename, initialContent, function (nBytes, err) { - callback(err); - }); - } else { - callback(null); - } - }); +function createSyntheticFile(syntheticsFileService, type, filename, initialContent, callback) { + if ((type === 'SIMPLE') || (type === 'BROWSER')) { + callback(); + } else { + syntheticsFileService.exists(filename, (exists) => { + if (!exists) { + logger.verbose('Synthetics file does not exist, creating: ' + filename); + syntheticsFileService.createFile(filename, initialContent, function (nBytes, err) { + callback(err); + }); + } else { + callback(null); + } + }); + } } function createSyntheticInNewRelic( @@ -29,16 +33,18 @@ function createSyntheticInNewRelic( frequency, filename, status, + uri, callback ) { async.waterfall([ (next) => { - newRelicService.createSynthetic(name, locations, frequency, status, (syntheticUrl, err) => { + newRelicService.createSynthetic(name, locations, frequency, status, type, (syntheticUrl, err) => { if (err) { return next(err); } const syntheticId = syntheticUrlToId(syntheticUrl); next(null, syntheticId); - }); + }, + uri); }, (id, next) => { @@ -48,8 +54,7 @@ function createSyntheticInNewRelic( }); } ], (err) => { - if (err) { throw err; } - callback(); + callback(err); }); } @@ -61,20 +66,22 @@ class CreateMonitorOrchestrator { this.defaults = defaults; } - createNewMonitor(name, locations, type, frequency, filename, callback) { + createNewMonitor(name, locations, type, frequency, filename, callback, uri) { logger.verbose('CreateMonitorOrchestrator.createNewMonitor: start'); logger.debug( 'name: ' + name + ', locations: ' + locations + ', type: ' + type + ', frequency: ' + frequency + - ', filename: ' + filename + ', filename: ' + filename + + ', uri: ' + uri ); async.parallel([ function (next) { createSyntheticFile( this.syntheticsFileService, + type, filename, this.defaults.syntheticsContent, next @@ -91,15 +98,16 @@ class CreateMonitorOrchestrator { frequency, filename, 'ENABLED', + uri, next ); }.bind(this) ], (err) => { - if (err) { throw err; } + if (err) { logger.error(err); throw err; } logger.verbose('CreateMonitorOrchestrator.createNewMonitor: complete'); - if (callback !== undefined) { + if (!_.isNil(callback)) { callback(); } }); diff --git a/lib/service/NewRelicService.js b/lib/service/NewRelicService.js index 8db51e4..8fb7662 100644 --- a/lib/service/NewRelicService.js +++ b/lib/service/NewRelicService.js @@ -26,7 +26,7 @@ class SyntheticsService { } _sendRequestToNewRelic(options, expectedResponse, operation, callback) { - logger.verbose('SyntheticsService._sendRequestToNewRelic: start: ' + operation); + logger.verbose('NewRelicService._sendRequestToNewRelic: start: ' + operation); logger.debug('Options: ' + JSON.stringify(options)); if (this.apikey === undefined) { @@ -52,7 +52,7 @@ class SyntheticsService { } getSynthetic(id, callback) { - logger.verbose('SyntheticsService.getSynthetic: ' + id); + logger.verbose('NewRelicService.getSynthetic: ' + id); const options = this._getOptions({ url: MONITORS_URL + '/' + id @@ -73,17 +73,29 @@ class SyntheticsService { ); } - createSynthetic(name, locations, frequency, status, callback) { - logger.verbose('SyntheticsService.createSynthetic: ' + name); + createSynthetic(name, locations, frequency, status, type, callback, uri) { + logger.verbose('NewRelicService.createSynthetic: ' + name); const paramObject = { name: name, - type: 'SCRIPT_BROWSER', + type: type, frequency: frequency, locations: locations, status: status }; + logger.debug('parameters: %s', JSON.stringify(paramObject)); + + + if ((type === 'SIMPLE') || (type === 'BROWSER')) { + logger.debug('uri: ' + uri); + if (_.isNil(uri)) { + return callback(null, 'Error: Missing uri parameter'); + } + + paramObject.uri = uri; + } + const options = this._getOptions({ url: MONITORS_URL, method: 'POST', @@ -105,7 +117,7 @@ class SyntheticsService { } getMonitorScript(id, callback) { - logger.verbose('SyntheticsService.getMonitorScript: ' + id); + logger.verbose('NewRelicService.getMonitorScript: ' + id); const options = this._getOptions({ url: 'http://' + SYNTHETICS_HOST + MONITORS_ENDPOINT +'/' + id + '/script', @@ -128,7 +140,7 @@ class SyntheticsService { } updateMonitorScript(id, base64Script, callback) { - logger.verbose('SyntheticsService.updateMonitorScript: ' + id); + logger.verbose('NewRelicService.updateMonitorScript: ' + id); const requestParam = { scriptText: base64Script diff --git a/lib/service/SyntheticsListFileService.js b/lib/service/SyntheticsListFileService.js index 76d1741..f06569e 100644 --- a/lib/service/SyntheticsListFileService.js +++ b/lib/service/SyntheticsListFileService.js @@ -53,7 +53,7 @@ function addSyntheticAndWrite( fileService, callback ) { - readSyntheticsListFile(syntheticsListFile, fileService, function (syntheticJson, err) { + readSyntheticsListFile(syntheticsListFile, fileService, (syntheticJson, err) => { if (err) { return callback(err); } if (syntheticJson[syntheticName] !== undefined) { @@ -61,10 +61,13 @@ function addSyntheticAndWrite( } syntheticJson[syntheticName] = { - id: syntheticId, - filename: syntheticFilename + id: syntheticId }; + if (!_.isNil(syntheticFilename)) { + syntheticJson[syntheticName].filename = syntheticFilename; + } + writeSyntheticsListFile(syntheticsListFile, syntheticJson, fileService, function (err) { return callback(err); }); @@ -116,7 +119,8 @@ class SyntheticsListFileService { logger.verbose('SyntheticsListFileService.getSynthetic: start'); logger.debug('syntheticName: %s', syntheticName); - readSyntheticsListFile(this.syntheticsListFile, this.fileService, function (syntheticsJson) { + readSyntheticsListFile(this.syntheticsListFile, this.fileService, (syntheticsJson, err) => { + if (err) { return callback(null, err); } if (syntheticsJson[syntheticName] === undefined) { return callback(null, 'Could not find info for synthetic: ' + syntheticName); } diff --git a/test/lib/orchestrator/CreateMonitorOrchestratorTest.js b/test/lib/orchestrator/CreateMonitorOrchestratorTest.js index b30032f..1d450f7 100644 --- a/test/lib/orchestrator/CreateMonitorOrchestratorTest.js +++ b/test/lib/orchestrator/CreateMonitorOrchestratorTest.js @@ -95,7 +95,9 @@ describe('CreateMonitorOrchestrator', function () { td.matchers.isA(Array), td.matchers.isA(Number), td.matchers.isA(String), - td.callback + td.matchers.isA(String), + td.callback, + undefined )).thenCallback('http://newrelic/id'); td.when(syntheticsFileServiceMock.exists(expectedFilename, td.callback)).thenCallback(true); td.when(syntheticsListFileServiceMock.addSynthetic( @@ -116,7 +118,9 @@ describe('CreateMonitorOrchestrator', function () { locations, frequency, 'ENABLED', - td.callback + type, + td.callback, + undefined ); }); }); @@ -138,7 +142,9 @@ describe('CreateMonitorOrchestrator', function () { td.matchers.isA(Array), td.matchers.isA(Number), td.matchers.isA(String), - td.callback + td.matchers.isA(String), + td.callback, + undefined )).thenCallback(null, expectedError); td.when(syntheticsFileServiceMock.exists(expectedFilename, td.callback)).thenCallback(true); td.when(syntheticsListFileServiceMock.addSynthetic( @@ -165,4 +171,22 @@ describe('CreateMonitorOrchestrator', function () { }).should.throw(expectedError); }); + it ('should not create a synthetics file for SIMPLE synthetics', () => { + const type = 'SIMPLE'; + const syntheticsFileServiceMock = { + exists: td.function(), + createFile: td.function() + }; + + const createMonitorOrchestrator = createMonitorOrchestratorFactory( + syntheticsFileServiceMock, + newRelicServiceMock, + syntheticsListFileServiceMock, + defaultsMock + ); + + createMonitorOrchestrator.createNewMonitor(monitorName, locations, type, frequency, expectedFilename); + + syntheticsFileServiceMock.exists.should.not.have.been.called; + }); }); \ No newline at end of file diff --git a/test/lib/service/NewRelicSerivceTest.js b/test/lib/service/NewRelicSerivceTest.js index f2e0419..0825772 100644 --- a/test/lib/service/NewRelicSerivceTest.js +++ b/test/lib/service/NewRelicSerivceTest.js @@ -14,6 +14,7 @@ describe('NewRelicService', () => { const expectedFrequency = 10; const expectedStatus = 'DISABLED'; const expectedUrl = 'http://newrelic/id'; + const expectedType = 'SCRIPTED_BROWSER'; const requestMock = { write: td.function(), @@ -42,6 +43,7 @@ describe('NewRelicService', () => { expectedLocations, expectedFrequency, expectedStatus, + expectedType, (syntheticUrl) => { syntheticUrl.should.equals(expectedUrl); } @@ -72,6 +74,7 @@ describe('NewRelicService', () => { expectedLocations, expectedFrequency, expectedStatus, + expectedType, (syntheticUrl, err) => { err.should.equal(expectedStatusMessage); } @@ -254,4 +257,71 @@ describe('NewRelicService', () => { err.should.equal(expectedError); }); }); + + it ('should fail to create a SIMPLE synthetic without a uri', () => { + const type = 'SIMPLE'; + const requestMock = td.function(); + const newRelicService = newRelicServiceFactory(expectedApiKey, requestMock); + + newRelicService.createSynthetic( + expectedName, + expectedLocations, + expectedFrequency, + expectedStatus, + type, + (syntheticUrl, err) => { + err.should.equals('Error: Missing uri parameter'); + } + ); + }); + + it ('should fail to create a BROWSER synthetic without a uri', () => { + const type = 'BROWSER'; + const requestMock = td.function(); + const newRelicService = newRelicServiceFactory(expectedApiKey, requestMock); + + newRelicService.createSynthetic( + expectedName, + expectedLocations, + expectedFrequency, + expectedStatus, + type, + (syntheticUrl, err) => { + err.should.equals('Error: Missing uri parameter'); + } + ); + }); + + it ('should POST to NR when creating a SIMPLE synthetic', () => { + const type = 'SIMPLE'; + const uri = 'http://simple.uri.com/'; + + const responseMock = { + statusCode: 201, + headers: { + location: expectedUrl + } + }; + + const requestMock = td.function(); + + td.when(requestMock( + td.matchers.isA(Object), + td.callback + )).thenCallback(null, responseMock); + + const newRelicService = newRelicServiceFactory(expectedApiKey, requestMock); + + newRelicService.createSynthetic( + expectedName, + expectedLocations, + expectedFrequency, + expectedStatus, + type, + (syntheticUrl) => { + syntheticUrl.should.equals(expectedUrl); + }, + uri + ); + }); }); \ No newline at end of file diff --git a/test/lib/service/SyntheticsListFileServiceTest.js b/test/lib/service/SyntheticsListFileServiceTest.js index d48e01b..bb7edda 100644 --- a/test/lib/service/SyntheticsListFileServiceTest.js +++ b/test/lib/service/SyntheticsListFileServiceTest.js @@ -251,4 +251,110 @@ describe('SyntheticsListFileService', function () { err.should.equal('Could not find info for synthetic filename: ' + unknownSyntheticFilename); }); }); + + it ('should not require a filename for some synthetics', () => { + const fileServiceMock = { + exists: function (filename, callback) { + filename.should.equal(expectedSyntheticsListFile); + callback(true); + }, + writeFile: td.function(), + getFileContent: function (filename, callback) { + filename.should.equal(expectedSyntheticsListFile); + callback('{}'); + } + }; + + td.when(fileServiceMock.writeFile( + expectedSyntheticsListFile, + td.matchers.contains(expectedSyntheticId), + td.callback + )).thenCallback(null); + + const syntheticsListFileService = syntheticsListFileServiceFactory(expectedSyntheticsListFile, fileServiceMock); + + syntheticsListFileService.addSynthetic(expectedSyntheticId, expectedSynthetic, null, () => { + td.verify(fileServiceMock.writeFile( + expectedSyntheticsListFile, + td.matchers.contains(expectedSyntheticId), + td.callback + )); + }); + }); + + it ('should fail if unable to create Synthetics List File', function () { + const expectedError = 'error writing file'; + + const fileServiceMock = { + exists: function (filename, callback) { + filename.should.equals(expectedSyntheticsListFile); + callback(false); + }, + writeFile: td.function(), + getFileContent: td.function() + } + + td.when(fileServiceMock.writeFile( + td.matchers.isA(String), + td.matchers.isA(String), + td.callback + )).thenCallback(null, expectedError); + + td.when(fileServiceMock.getFileContent( + td.matchers.isA(String), + td.callback + )).thenCallback('{}'); + + const syntheticsListFileService = syntheticsListFileServiceFactory(expectedSyntheticsListFile, fileServiceMock); + + syntheticsListFileService.addSynthetic(expectedSyntheticId, expectedSynthetic, expectedSyntheticFilename, (err) => { + err.should.equals(expectedError); + }); + }); + + it ('should fail getSynthetic if synthetics file cannot be read', () => { + const expectedError = 'error reading file'; + const fileServiceMock = { + exists: function (filename, callback) { + filename.should.equal(expectedSyntheticsListFile); + callback(true); + }, + writeFile: td.function(), + getFileContent: td.function() + }; + + td.when(fileServiceMock.getFileContent( + td.matchers.isA(String), + td.callback + )).thenCallback(null, expectedError); + + const syntheticsListFileService = syntheticsListFileServiceFactory(expectedSyntheticsListFile, fileServiceMock); + + syntheticsListFileService.getSynthetic(expectedSynthetic, (syntheticInfo, err) => { + err.should.equals(expectedError); + }); + }); + + it ('should fail addSynthetic if synthetics file cannot be read', () => { + const expectedError = 'error reading file'; + const fileServiceMock = { + exists: function (filename, callback) { + filename.should.equal(expectedSyntheticsListFile); + callback(true); + }, + writeFile: td.function(), + getFileContent: td.function() + }; + + td.when(fileServiceMock.getFileContent( + td.matchers.isA(String), + td.callback + )).thenCallback(null, expectedError); + + const syntheticsListFileService = syntheticsListFileServiceFactory(expectedSyntheticsListFile, fileServiceMock); + + syntheticsListFileService.addSynthetic(expectedSyntheticId, expectedSynthetic, expectedSyntheticFilename, (err) => { + err.should.equals(expectedError); + }); + }); }); \ No newline at end of file From 1fc77ed6fcb88c09f8fd757e57f64230c8e6d719 Mon Sep 17 00:00:00 2001 From: David Bradford Date: Tue, 10 Jan 2017 16:15:54 -0600 Subject: [PATCH 02/14] Issue 15: Make synthetics.json human readable --- lib/service/SyntheticsListFileService.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/service/SyntheticsListFileService.js b/lib/service/SyntheticsListFileService.js index f06569e..8259ce4 100644 --- a/lib/service/SyntheticsListFileService.js +++ b/lib/service/SyntheticsListFileService.js @@ -37,7 +37,7 @@ function readSyntheticsListFile(syntheticsListFile, fileService, callback) { function writeSyntheticsListFile(syntheticsListFile, syntheticJson, fileService, callback) { fileService.writeFile( syntheticsListFile, - JSON.stringify(syntheticJson), + JSON.stringify(syntheticJson, null, 4), function (bytesWritten, err) { logger.debug("Write Synthetics List file complete"); callback(err); From 531cd31b66941cf8e7c15aab02cd80b543c638cd Mon Sep 17 00:00:00 2001 From: David Bradford Date: Tue, 10 Jan 2017 16:46:34 -0600 Subject: [PATCH 03/14] Issue 13: Add Contributing.md file --- CONTRIBUTING.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..03852d1 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,17 @@ +# Contributing + +When contributing to this repository, please first discuss the change you wish to make via github issues before making the change. + +Please note we have a code of conduct, please follow it in all your interactions with the project. + +## Pull Request Process + +* Update the README.md with details of changes to the interface. +* Ensure the travis build is passing for the given change (npm test). +* Increase the version numbers in any examples files and the README.md to the new version that this pull request would represent. The versioning scheme we use is http://semver.org/ +* Ensure any correlated issue numbers are included in the pull request. + +## Code of Conduct + +http://contributor-covenant.org/version/1/2/0/ + From 74c0489e5c3ee261645d00c5c1a0c6680c2e62d4 Mon Sep 17 00:00:00 2001 From: David Bradford Date: Thu, 12 Jan 2017 10:02:00 -0600 Subject: [PATCH 04/14] Update README --- README.md | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 1a15bad..e04ad57 100644 --- a/README.md +++ b/README.md @@ -92,26 +92,29 @@ These options can be used with any command: ### Create a new synthetic ``` -synthmanager create --name --file +synthmanager create ``` -Create a synthetic in New Relic and a file to contain the synthetic code. +Create a synthetic in New Relic and a file to contain the synthetic code. Possible options: * --name - Name of synthetic. This is the name used in New Relic as well as how it should be refered to by other commands -* --file - Filename where the synthethics code should go. This file will be created under the 'synthetics' directory. The file should not already exist. +* --filename - Filename where the synthethics code should go. This file will be created under the 'synthetics' directory. The file should not already exist (required for SCRIPT_BROWSER and SCRIPT_API synthetics). * --frequency - Frequency to run the synthetic in minutes. This should be an integer. Possible values are: 1, 5, 10, 15, 30, 60, 360, 720, or 1440. The default is 10. * --locations - Locations to run the synthetic. This can be specified multiple times to specify multiple locations. +* --type - Type of synthetic to create. Possible values are: SIMPLE, BROWSER, SCRIPT_BROWSER, SCRIPT_API. +* --uri - URI that synthetic should check (required for SIMPLE and BROWSER synthetics). ### Update New Relic with synthetics code ``` -synthmanager update --name +synthmanager update ``` Update New Relic with the latest synthetic code for the specified synthetic. * --name - name of synthetic to update. This should be the name used when the synthetic was created. +* --filename - filename of synthetic to update. ### Import a synthetic from New Relic @@ -119,9 +122,12 @@ Update New Relic with the latest synthetic code for the specified synthetic. In order to import a New Relic Synthetic, the synthetic id is needed. This can be obtained from the New Relic Synthetics website. Navigate to the Synthetic to import and the id will be the last part of the URL (It is made up of 5 hexidecimal numbers separated by dashes). ``` -synthmanager import --name --id --file +synthmanager import ``` +--id - ID of synthetic in New Relic (this is part of the url when viewing the synthetic in New Relic) +--filename - File to store the synthetic code. + Import an existing synthetic from New Relic. From 7f1f6285b067b4edc8ede39b483d21a21a496810 Mon Sep 17 00:00:00 2001 From: David Bradford Date: Tue, 24 Jan 2017 22:14:04 -0600 Subject: [PATCH 05/14] Add command to list available locations --- bin/cmds/locations.js | 17 +++++++++++++ lib/dependency.js | 8 +++++- lib/orchestrator/CreateMonitorOrchestrator.js | 6 +++++ lib/orchestrator/ListLocationsOrchestrator.js | 25 +++++++++++++++++++ lib/service/NewRelicService.js | 25 +++++++++++++++++++ 5 files changed, 80 insertions(+), 1 deletion(-) create mode 100644 bin/cmds/locations.js create mode 100644 lib/orchestrator/ListLocationsOrchestrator.js diff --git a/bin/cmds/locations.js b/bin/cmds/locations.js new file mode 100644 index 0000000..d92f12c --- /dev/null +++ b/bin/cmds/locations.js @@ -0,0 +1,17 @@ +const dependencies = require('../../lib/dependency'); +const logger = require('winston'); +const _ = require('lodash'); + +exports.command = 'locations'; +exports.desc = 'List available locations for synthetics to run'; + +exports.handler = function (argv) { + require('../../lib/config/LoggingConfig')(argv); + + const config = require('../../lib/config/SyntheticsConfig').getConfig(argv); + + logger.verbose('Locations'); + logger.verbose(argv); + + dependencies(config).listLocationsOrchestrator.listLocations(); +} \ No newline at end of file diff --git a/lib/dependency.js b/lib/dependency.js index f5d1742..b0b81c6 100644 --- a/lib/dependency.js +++ b/lib/dependency.js @@ -12,6 +12,7 @@ const syntheticsListFileServiceFactory = require('./service/SyntheticsListFileSe const createMonitorOrchestratorFactory = require('./orchestrator/CreateMonitorOrchestrator'); const updateMonitorOrchestratorFactory = require('./orchestrator/UpdateMonitorOrchestrator'); const importMonitorOrchestratorFactory = require('./orchestrator/ImportMonitorOrchestrator'); +const listLocationsOrchestratorFactory = require('./orchestrator/ListLocationsOrchestrator'); module.exports = (config) => { const fileService = fileServiceFactory(fs, mkdirp); @@ -43,6 +44,10 @@ module.exports = (config) => { newRelicService, defaults ); + const listLocationsOrchestrator = listLocationsOrchestratorFactory( + newRelicService + ); + return { fileService: fileService, @@ -51,6 +56,7 @@ module.exports = (config) => { syntheticsListFileService: syntheticsListFileService, createMonitorOrchestrator: createMonitorOrchestrator, updateMonitorOrchestrator: updateMonitorOrchestrator, - importMonitorOrchestrator: importMonitorOrchestrator + importMonitorOrchestrator: importMonitorOrchestrator, + listLocationsOrchestrator: listLocationsOrchestrator }; }; diff --git a/lib/orchestrator/CreateMonitorOrchestrator.js b/lib/orchestrator/CreateMonitorOrchestrator.js index d203014..c937d0d 100644 --- a/lib/orchestrator/CreateMonitorOrchestrator.js +++ b/lib/orchestrator/CreateMonitorOrchestrator.js @@ -113,6 +113,12 @@ class CreateMonitorOrchestrator { }); } + getLocations() { + this.newRelicService.getAvailableLocations((err) => { + + }); + } + } module.exports = (syntheticsFileService, newRelicService, syntheticsListFileService, defaults) => { diff --git a/lib/orchestrator/ListLocationsOrchestrator.js b/lib/orchestrator/ListLocationsOrchestrator.js new file mode 100644 index 0000000..550885c --- /dev/null +++ b/lib/orchestrator/ListLocationsOrchestrator.js @@ -0,0 +1,25 @@ +const logger = require('winston'); +const _ = require('lodash'); + +class ListLocationsOrchestrator { + constructor(newRelicService) { + this.newRelicService = newRelicService; + } + + listLocations() { + logger.verbose('ListLocationsOrchestrator.listLocations: start'); + + this.newRelicService.getAvailableLocations((err, locationList) => { + if (err) { logger.error(err); throw err; } + + _.forEach(locationList, (location) => { + console.log('%s: %s', location.label, location.name); + }); + }); + } + +} + +module.exports = (newRelicService) => { + return new ListLocationsOrchestrator(newRelicService); +}; \ No newline at end of file diff --git a/lib/service/NewRelicService.js b/lib/service/NewRelicService.js index 8fb7662..7a2e91b 100644 --- a/lib/service/NewRelicService.js +++ b/lib/service/NewRelicService.js @@ -4,6 +4,8 @@ const logger = require('winston'); const SYNTHETICS_HOST = 'synthetics.newrelic.com'; const MONITORS_ENDPOINT = '/synthetics/api/v3/monitors'; const MONITORS_URL = 'https://' + SYNTHETICS_HOST + MONITORS_ENDPOINT; +const ALERTS_ENDPOINT = '/synthetics/api/v1/locations'; +const ALERTS_URL = 'https://' + SYNTHETICS_HOST + ALERTS_ENDPOINT; class SyntheticsService { constructor(apikey, request) { @@ -165,6 +167,29 @@ class SyntheticsService { } ); } + + getAvailableLocations(callback) { + logger.verbose('NewRelicService.getAvailableLocation'); + + const options = this._getOptions({ + url: ALERTS_URL + }); + + this._sendRequestToNewRelic( + options, + 200, + 'geting alert values', + (err, body, response) => { + if (err) { + return callback(err); + } + + logger.debug(body); + + return callback(null, JSON.parse(body)); + } + ) + } } module.exports = (apikey, request) => { return new SyntheticsService(apikey, request); }; \ No newline at end of file From 0ad3a27944e8fb076eaf9adfa8254d3f146776b1 Mon Sep 17 00:00:00 2001 From: David Bradford Date: Wed, 25 Jan 2017 08:45:41 -0600 Subject: [PATCH 06/14] Add missing ; --- lib/service/NewRelicService.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/service/NewRelicService.js b/lib/service/NewRelicService.js index 7a2e91b..65a2487 100644 --- a/lib/service/NewRelicService.js +++ b/lib/service/NewRelicService.js @@ -188,7 +188,7 @@ class SyntheticsService { return callback(null, JSON.parse(body)); } - ) + ); } } From 8f79f519b3e0008ddc5934acba8fdb9bf1d6db58 Mon Sep 17 00:00:00 2001 From: David Bradford Date: Thu, 26 Jan 2017 13:35:51 -0600 Subject: [PATCH 07/14] Added tests for Listing locations --- lib/orchestrator/CreateMonitorOrchestrator.js | 7 --- lib/service/NewRelicService.js | 2 +- .../ListLocationsOrchestratorTest.js | 40 +++++++++++++++++ test/lib/service/NewRelicSerivceTest.js | 43 +++++++++++++++++++ 4 files changed, 84 insertions(+), 8 deletions(-) create mode 100644 test/lib/orchestrator/ListLocationsOrchestratorTest.js diff --git a/lib/orchestrator/CreateMonitorOrchestrator.js b/lib/orchestrator/CreateMonitorOrchestrator.js index c937d0d..9d1863b 100644 --- a/lib/orchestrator/CreateMonitorOrchestrator.js +++ b/lib/orchestrator/CreateMonitorOrchestrator.js @@ -112,13 +112,6 @@ class CreateMonitorOrchestrator { } }); } - - getLocations() { - this.newRelicService.getAvailableLocations((err) => { - - }); - } - } module.exports = (syntheticsFileService, newRelicService, syntheticsListFileService, defaults) => { diff --git a/lib/service/NewRelicService.js b/lib/service/NewRelicService.js index 65a2487..2abfd6d 100644 --- a/lib/service/NewRelicService.js +++ b/lib/service/NewRelicService.js @@ -178,7 +178,7 @@ class SyntheticsService { this._sendRequestToNewRelic( options, 200, - 'geting alert values', + 'getting location values', (err, body, response) => { if (err) { return callback(err); diff --git a/test/lib/orchestrator/ListLocationsOrchestratorTest.js b/test/lib/orchestrator/ListLocationsOrchestratorTest.js new file mode 100644 index 0000000..71a538d --- /dev/null +++ b/test/lib/orchestrator/ListLocationsOrchestratorTest.js @@ -0,0 +1,40 @@ +const td = require('testdouble'); +const chai = require('chai'); +const tdChai = require('testdouble-chai'); + +chai.should(); +chai.use(tdChai(td)); + +const listLocationsOrchestratorFactory = require('../../../lib/orchestrator/ListLocationsOrchestrator'); + +describe('ListLocationsOrchestrator', () => { + it ('should get a list of locations from NR', () => { + const expectedLocations = [{label: "location", name: "name" }, {label: "location2", name: "name2"}]; + + const newRelicServiceMock = { + getAvailableLocations: (callback) => { + callback(null, expectedLocations); + } + } + + const listLocationsOrchestrator = listLocationsOrchestratorFactory(newRelicServiceMock); + + listLocationsOrchestrator.listLocations(); + }); + + it ('should throw an error if NR throws an error', () => { + const expectedError = "Could not list locations"; + + const newRelicServiceMock = { + getAvailableLocations: (callback) => { + callback(expectedError); + } + } + + const listLocationsOrchestrator = listLocationsOrchestratorFactory(newRelicServiceMock); + + (() => { + listLocationsOrchestrator.listLocations(); + }).should.throw(expectedError); + }); +}); \ No newline at end of file diff --git a/test/lib/service/NewRelicSerivceTest.js b/test/lib/service/NewRelicSerivceTest.js index 0825772..d41b99b 100644 --- a/test/lib/service/NewRelicSerivceTest.js +++ b/test/lib/service/NewRelicSerivceTest.js @@ -324,4 +324,47 @@ describe('NewRelicService', () => { uri ); }); + + it ('should GET to NR when listing locations', () => { + const expectedLocationsList = JSON.stringify({ locations: "list of locations" }); + const responseMock = { + statusCode: 200 + }; + + const requestMock = td.function(); + + td.when(requestMock( + td.matchers.isA(Object), + td.callback + )).thenCallback(null, responseMock, expectedLocationsList); + + const newRelicService = newRelicServiceFactory(expectedApiKey, requestMock); + + newRelicService.getAvailableLocations( + (err, locationsList) => { + JSON.stringify(locationsList).should.be.equal(expectedLocationsList); + } + ); + }); + + it ('should fail if NR throws an error when getting locations', () => { + const responseMock = { + statusCode: 500 + }; + + const requestMock = td.function(); + + td.when(requestMock( + td.matchers.isA(Object), + td.callback + )).thenCallback(null, responseMock); + + const newRelicService = newRelicServiceFactory(expectedApiKey, requestMock); + + newRelicService.getAvailableLocations( + (err, locationsList) => { + err.should.be.equal('Error getting location values synthetic: 500'); + } + ); + }); }); \ No newline at end of file From a3cf45da73989205961224f994984909f480b591 Mon Sep 17 00:00:00 2001 From: David Bradford Date: Thu, 26 Jan 2017 15:37:35 -0600 Subject: [PATCH 08/14] Better Error handling --- lib/service/NewRelicService.js | 20 +++++++++++++- package.json | 1 + test/lib/service/NewRelicSerivceTest.js | 35 ++++++++++++++++++------- 3 files changed, 46 insertions(+), 10 deletions(-) diff --git a/lib/service/NewRelicService.js b/lib/service/NewRelicService.js index 2abfd6d..e9a9bfc 100644 --- a/lib/service/NewRelicService.js +++ b/lib/service/NewRelicService.js @@ -1,5 +1,6 @@ const _ = require('lodash'); const logger = require('winston'); +const htmlToText = require('html-to-text'); const SYNTHETICS_HOST = 'synthetics.newrelic.com'; const MONITORS_ENDPOINT = '/synthetics/api/v3/monitors'; @@ -45,7 +46,24 @@ class SyntheticsService { if (response.statusCode !== expectedResponse) { logger.error('Error %s synthetic: %d', operation, response.statusCode); - return callback('Error ' + operation + ' synthetic: ' + response.statusCode); + logger.debug(response.headers); + logger.debug(body); + + const _getError = (response, body) => { + const responseType = response.headers['content-type']; + logger.debug('content-type: ' + responseType); + if (responseType === 'application/json') { + return _.reduce(JSON.parse(body).errors, (acc, err) => { + return acc + ' : ' + err.error; + }, 'New Relic Response'); + } else if (responseType.indexOf('text/html') != -1) { + return 'New Relic Response : ' + htmlToText.fromString(body); + } + + return 'Unknown Error'; + }; + + return callback('Error ' + operation + ' synthetic: ' + response.statusCode + '; ' + _getError(response, body)); } return callback(null, body, response); diff --git a/package.json b/package.json index 07a90c9..263c3ac 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ }, "dependencies": { "async": "2.1.2", + "html-to-text": "^3.0.0", "lodash": "4.17.1", "mkdirp": "0.5.1", "request": "2.79.0", diff --git a/test/lib/service/NewRelicSerivceTest.js b/test/lib/service/NewRelicSerivceTest.js index d41b99b..aed6918 100644 --- a/test/lib/service/NewRelicSerivceTest.js +++ b/test/lib/service/NewRelicSerivceTest.js @@ -109,16 +109,21 @@ describe('NewRelicService', () => { const expectedId = 'syntheticId'; const expectedContent = 'new relic synthetic content'; const expectedStatusCode = 500; + const errorMessage = 'error updating synthetic'; + const errorHtml = '

' + errorMessage + '

'; const requestMock = td.function(); const responseMock = { - statusCode: expectedStatusCode + statusCode: expectedStatusCode, + headers: { + 'content-type': 'other' + } }; td.when(requestMock( td.matchers.isA(Object), td.callback - )).thenCallback(null, responseMock); + )).thenCallback(null, responseMock, errorHtml); const newRelicService = newRelicServiceFactory(expectedApiKey, requestMock); @@ -126,7 +131,7 @@ describe('NewRelicService', () => { expectedId, expectedContent, (err) => { - err.should.equal('Error updating code for synthetic: ' + expectedStatusCode); + err.should.equal('Error updating code for synthetic: ' + expectedStatusCode + '; Unknown Error'); } ); }); @@ -153,21 +158,26 @@ describe('NewRelicService', () => { const expectedStatusCode = 404; const expectedSyntheticId = 'syntheticId'; const expectedError = 'Error retrieving synthetic: ' + expectedStatusCode; + const errorMessage = 'Cannot find synthetic'; + const errorHtml = '

' + errorMessage + '

'; const requestMock = td.function(); const responseMock = { - statusCode: expectedStatusCode + statusCode: expectedStatusCode, + headers: { + 'content-type': 'text/html' + } }; td.when(requestMock( td.matchers.isA(Object), td.callback - )).thenCallback(null, responseMock); + )).thenCallback(null, responseMock, errorHtml); const newRelicService = newRelicServiceFactory(expectedApiKey, requestMock); newRelicService.getSynthetic(expectedSyntheticId, (body, err) => { - err.should.be.equal(expectedError); + err.should.be.equal(expectedError + '; New Relic Response : ' + errorMessage); }); }); @@ -348,22 +358,29 @@ describe('NewRelicService', () => { }); it ('should fail if NR throws an error when getting locations', () => { + const errorMessage = 'error getting locations'; + const responseMock = { - statusCode: 500 + statusCode: 500, + headers: { + 'content-type': 'application/json' + } }; + const expectedBody = JSON.stringify({errors: [{error: errorMessage}]}); + const requestMock = td.function(); td.when(requestMock( td.matchers.isA(Object), td.callback - )).thenCallback(null, responseMock); + )).thenCallback(null, responseMock, expectedBody); const newRelicService = newRelicServiceFactory(expectedApiKey, requestMock); newRelicService.getAvailableLocations( (err, locationsList) => { - err.should.be.equal('Error getting location values synthetic: 500'); + err.should.be.equal('Error getting location values synthetic: 500; New Relic Response : ' + errorMessage); } ); }); From e70712365c74bedb7675c622972e03608fc6811b Mon Sep 17 00:00:00 2001 From: David Bradford Date: Thu, 26 Jan 2017 16:13:47 -0600 Subject: [PATCH 09/14] Add developer documentation --- DEVELOPING.md | 42 ++++++++++++++++++++++++++++++++++++++++++ package.json | 4 ++++ 2 files changed, 46 insertions(+) create mode 100644 DEVELOPING.md diff --git a/DEVELOPING.md b/DEVELOPING.md new file mode 100644 index 0000000..3f99654 --- /dev/null +++ b/DEVELOPING.md @@ -0,0 +1,42 @@ +# Development information + +## Prerequisites + +* node.js v6 or higher + +## Code Structure + +* bin/ + * command line interface and command line parsing code. bin/synthmanager.js is the entry point. yargs is used to do the command line parsing. +* lib/ + * code that implements the command line functionality. +* test/ + * tests. The tests use mocha, chai and testdouble.js. +* index.js + * library entrypoint that provides webdriver functionality for running tests locally. + +## Command Tasks + +### Linting + +``` +gulp lint +``` + +### Testing + +``` +gulp test +``` + +### Lint and Test in one Tasks + +``` +gulp +``` + +or + +``` +npm test +``` \ No newline at end of file diff --git a/package.json b/package.json index 263c3ac..139273e 100644 --- a/package.json +++ b/package.json @@ -33,5 +33,9 @@ "mocha": "3.1.2", "testdouble": "1.9.0", "testdouble-chai": "0.4.0" + }, + "repository": { + "type": "git", + "url": "https://github.com/yodle/new-relic-synthetics-manager" } } From 50fbae0bdd274ed55217b9519bd6c03d4883e2da Mon Sep 17 00:00:00 2001 From: David Bradford Date: Fri, 27 Jan 2017 09:50:01 -0600 Subject: [PATCH 10/14] Add support for changing synthetic configurations --- DEVELOPING.md | 3 +- bin/cmds/config.js | 86 ++++++++ lib/dependency.js | 8 +- lib/orchestrator/ChangeConfigOrchestrator.js | 55 +++++ lib/service/NewRelicService.js | 47 +++++ package.json | 2 +- .../ChangeConfigOrchestratorTest.js | 192 ++++++++++++++++++ test/lib/service/NewRelicSerivceTest.js | 70 +++++++ 8 files changed, 460 insertions(+), 3 deletions(-) create mode 100644 bin/cmds/config.js create mode 100644 lib/orchestrator/ChangeConfigOrchestrator.js create mode 100644 test/lib/orchestrator/ChangeConfigOrchestratorTest.js diff --git a/DEVELOPING.md b/DEVELOPING.md index 3f99654..3fc179a 100644 --- a/DEVELOPING.md +++ b/DEVELOPING.md @@ -39,4 +39,5 @@ or ``` npm test -``` \ No newline at end of file +``` + diff --git a/bin/cmds/config.js b/bin/cmds/config.js new file mode 100644 index 0000000..5f829d6 --- /dev/null +++ b/bin/cmds/config.js @@ -0,0 +1,86 @@ +const dependencies = require('../../lib/dependency'); +const logger = require('winston'); +const _ = require('lodash'); + +exports.command = 'config'; +exports.desc = 'Change configuration options of a synthetic'; +exports.builder = { + name: { + alias: 'n', + desc: 'Name of synthetic to configure', + type: 'string', + }, + id: { + alias: 'i', + desc: 'Id of synthetic to configure', + type: 'string', + }, + frequency: { + desc: 'Frequency to run synthetic(in minutes)', + choices: [1, 5, 10, 15, 30, 60, 360, 720, 1440], + type: 'number', + }, + locations: { + desc: 'Locations to run synthetic', + type: 'array', + }, + uri: { + alias: 'u', + desc: 'URI for synthetic', + type: 'string', + }, + status: { + alias: 's', + desc: 'Is the synthetic enabled?', + choices: ['ENABLED', 'DISABLED', 'MUTED'], + }, + rename: { + desc: 'New name to use for synthetic', + type: 'string', + } +} + +function validate(argv) { + if (_.isNil(argv.name) && _.isNil(argv.id)) { + throw new Error('ERROR: Either name or id must be specified'); + } + + const allOptions = [argv.frequency, argv.locations, argv.uri, argv.status, argv.rename]; + + if (_.every(allOptions, _.isNil)) { + throw new Error('Error: No changes specified'); + } +} + +exports.handler = function (argv) { + require('../../lib/config/LoggingConfig')(argv); + + validate(argv); + + const config = require('../../lib/config/SyntheticsConfig').getConfig(argv); + + logger.verbose('Config: ' + argv.name + ':' + argv.id); + logger.verbose(argv); + + const changeConfigOrchestrator = dependencies(config).changeConfigOrchestrator; + + if (!_.isNil(argv.id)) { + changeConfigOrchestrator.changeConfigurationById( + argv.id, + argv.frequency, + argv.locations, + argv.uri, + argv.status, + argv.rename + ); + } else if (!_.isNil(argv.name)) { + changeConfigOrchestrator.changeConfigurationByName( + argv.name, + argv.frequency, + argv.locations, + argv.uri, + argv.status, + argv.rename + ); + } +} \ No newline at end of file diff --git a/lib/dependency.js b/lib/dependency.js index b0b81c6..995d7a6 100644 --- a/lib/dependency.js +++ b/lib/dependency.js @@ -13,6 +13,7 @@ const createMonitorOrchestratorFactory = require('./orchestrator/CreateMonitorOr const updateMonitorOrchestratorFactory = require('./orchestrator/UpdateMonitorOrchestrator'); const importMonitorOrchestratorFactory = require('./orchestrator/ImportMonitorOrchestrator'); const listLocationsOrchestratorFactory = require('./orchestrator/ListLocationsOrchestrator'); +const changeConfigOrchestratorFactory = require('./orchestrator/ChangeConfigOrchestrator'); module.exports = (config) => { const fileService = fileServiceFactory(fs, mkdirp); @@ -47,6 +48,10 @@ module.exports = (config) => { const listLocationsOrchestrator = listLocationsOrchestratorFactory( newRelicService ); + const changeConfigOrchestrator = changeConfigOrchestratorFactory( + newRelicService, + syntheticsListFileService + ); return { @@ -57,6 +62,7 @@ module.exports = (config) => { createMonitorOrchestrator: createMonitorOrchestrator, updateMonitorOrchestrator: updateMonitorOrchestrator, importMonitorOrchestrator: importMonitorOrchestrator, - listLocationsOrchestrator: listLocationsOrchestrator + listLocationsOrchestrator: listLocationsOrchestrator, + changeConfigOrchestrator: changeConfigOrchestrator, }; }; diff --git a/lib/orchestrator/ChangeConfigOrchestrator.js b/lib/orchestrator/ChangeConfigOrchestrator.js new file mode 100644 index 0000000..f5c4b73 --- /dev/null +++ b/lib/orchestrator/ChangeConfigOrchestrator.js @@ -0,0 +1,55 @@ +const async = require('async'); +const logger = require('winston'); +const _ = require('lodash'); + +class ChangeConfigOrchestrator { + constructor(newRelicService, syntheticsListFileService) { + this.newRelicService = newRelicService; + this.syntheticsListFileService = syntheticsListFileService; + } + + changeConfigurationById(id, frequency, locations, uri, status, rename, callback) { + logger.verbose('ChangeConfigOrchestrator.changeConfigurationById'); + + this.newRelicService.updateMonitorSettings(id, frequency, locations, uri, status, rename, (err) => { + if (err) { + if (callback) { + callback(err); + } else { + logger.error(err); + throw new Error(err); + } + } + }); + + } + + changeConfigurationByName(name, frequency, locations, uri, status, rename) { + logger.verbose('ChangeConfigOrchestrator.changeConfigurationByName'); + + async.waterfall([ + ((next) => { + this.syntheticsListFileService.getSynthetic(name, (syntheticInfo, err) => { + if (err) { next(err); } + + const syntheticId = syntheticInfo.id; + next(null, syntheticId); + }); + }).bind(this), + + ((id, next) => { + this.changeConfigurationById(id, frequency, locations, uri, status, rename, next); + }).bind(this), + + ], (err) => { + if (err) { + logger.error(err); + throw new Error(err); + } + }); + } +} + +module.exports = (newRelicService, syntheticsListFileService) => { + return new ChangeConfigOrchestrator(newRelicService, syntheticsListFileService); +}; \ No newline at end of file diff --git a/lib/service/NewRelicService.js b/lib/service/NewRelicService.js index e9a9bfc..0e1cb21 100644 --- a/lib/service/NewRelicService.js +++ b/lib/service/NewRelicService.js @@ -208,6 +208,53 @@ class SyntheticsService { } ); } + + updateMonitorSettings(id, frequency, locations, uri, status, rename, callback) { + logger.verbose('NewRelicService.updateMonitorSettings: ' + id); + + var requestParam = {}; + + if (!_.isNil(frequency)) { + requestParam.frequency = frequency; + } + + if (!_.isNil(locations)) { + requestParam.locations = locations; + } + + if (!_.isNil(uri)) { + requestParam.uri = uri; + } + + if (!_.isNil(status)) { + requestParam.status = status; + } + + if (!_.isNil(rename)) { + requestParam.name = rename; + } + + logger.debug('request parameters: ' + JSON.stringify(requestParam)); + + const options = this._getOptions({ + url: MONITORS_URL + '/' + id, + method: 'PATCH', + body: JSON.stringify(requestParam) + }); + + this._sendRequestToNewRelic( + options, + 204, + 'updating synthetic configuration', + (err, body, response) => { + if (err) { + return callback(err); + } + + return callback(); + } + ); + } } module.exports = (apikey, request) => { return new SyntheticsService(apikey, request); }; \ No newline at end of file diff --git a/package.json b/package.json index 139273e..d1fb77a 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,6 @@ }, "repository": { "type": "git", - "url": "https://github.com/yodle/new-relic-synthetics-manager" + "url": "https://github.com/yodle/new-relic-synthetics-manager.git" } } diff --git a/test/lib/orchestrator/ChangeConfigOrchestratorTest.js b/test/lib/orchestrator/ChangeConfigOrchestratorTest.js new file mode 100644 index 0000000..b8fba37 --- /dev/null +++ b/test/lib/orchestrator/ChangeConfigOrchestratorTest.js @@ -0,0 +1,192 @@ +const td = require('testdouble'); +const chai = require('chai'); +const tdChai = require('testdouble-chai'); + +chai.should(); +chai.use(tdChai(td)); + +const changeConfigOrchestratorFactory = require('../../../lib/orchestrator/ChangeConfigOrchestrator'); + +describe('ChangeConfigOrchestrator', () => { + const expectedId = 'syntheticId'; + const expectedFrequency = 5; + const expectedLocations = ['location1', 'location2']; + const expectedUri = 'http://newuri.com'; + const expectedStatus = 'ENABLED'; + const expectedNewName = 'newSyntheticName'; + + it ('call to NR to change configuration by id', () => { + + const newRelicServiceMock = { + updateMonitorSettings: td.function() + } + + td.when(newRelicServiceMock.updateMonitorSettings( + td.matchers.isA(String), + td.matchers.isA(Number), + td.matchers.isA(Array), + td.matchers.isA(String), + td.matchers.isA(String), + td.matchers.isA(String), + td.callback + )).thenCallback(); + + const syntheticsListFileService = {}; + + const changeConfigOrchestrator = changeConfigOrchestratorFactory(newRelicServiceMock, syntheticsListFileService); + + changeConfigOrchestrator.changeConfigurationById( + expectedId, expectedFrequency, expectedLocations, expectedUri, expectedStatus, expectedNewName, (err) => { + newRelicServiceMock.updateMonitorSettings.should.have.been.calledWith( + expectedId, + expectedFrequency, + expectedLocations, + expectedUri, + expectedStatus, + expectedNewName, + td.callback + ); + } + ); + }); + + it ('should show an error when NR has an error by id', () => { + const expectedError = 'error with NR'; + + const newRelicServiceMock = { + updateMonitorSettings: td.function() + } + + td.when(newRelicServiceMock.updateMonitorSettings( + td.matchers.isA(String), + td.matchers.isA(Number), + td.matchers.isA(Array), + td.matchers.isA(String), + td.matchers.isA(String), + td.matchers.isA(String), + td.callback + )).thenCallback(expectedError); + + const syntheticsListFileService = {}; + + const changeConfigOrchestrator = changeConfigOrchestratorFactory(newRelicServiceMock, syntheticsListFileService); + + (() => { + changeConfigOrchestrator.changeConfigurationById( + expectedId, expectedFrequency, expectedLocations, expectedUri, expectedStatus, expectedNewName + ); + }).should.throw(expectedError); + }); + + it ('should show an error when NR has an error by id using callback', () => { + const expectedError = 'error with NR'; + + const newRelicServiceMock = { + updateMonitorSettings: td.function() + } + + td.when(newRelicServiceMock.updateMonitorSettings( + td.matchers.isA(String), + td.matchers.isA(Number), + td.matchers.isA(Array), + td.matchers.isA(String), + td.matchers.isA(String), + td.matchers.isA(String), + td.callback + )).thenCallback(expectedError); + + const syntheticsListFileService = {}; + + const changeConfigOrchestrator = changeConfigOrchestratorFactory(newRelicServiceMock, syntheticsListFileService); + + changeConfigOrchestrator.changeConfigurationById( + expectedId, expectedFrequency, expectedLocations, expectedUri, expectedStatus, expectedNewName, (err) => { + err.should.be.equal(expectedError); + } + ); + }); + + it ('call to NR to change configuration by name', () => { + const expectedName = 'syntheticName'; + + const newRelicServiceMock = { + updateMonitorSettings: td.function() + } + + td.when(newRelicServiceMock.updateMonitorSettings( + td.matchers.isA(String), + td.matchers.isA(Number), + td.matchers.isA(Array), + td.matchers.isA(String), + td.matchers.isA(String), + td.matchers.isA(String), + td.callback + )).thenCallback(); + + const syntheticsListFileService = { + getSynthetic: td.function() + }; + + td.when(syntheticsListFileService.getSynthetic( + expectedName, + td.callback + )).thenCallback({id: expectedId}); + + const changeConfigOrchestrator = changeConfigOrchestratorFactory(newRelicServiceMock, syntheticsListFileService); + + changeConfigOrchestrator.changeConfigurationByName( + expectedName, + expectedFrequency, + expectedLocations, + expectedUri, + expectedStatus, + expectedNewName + ); + + newRelicServiceMock.updateMonitorSettings.should.have.been.calledWith( + expectedId, + expectedFrequency, + expectedLocations, + expectedUri, + expectedStatus, + expectedNewName, + td.callback + ); + }); + + it ('should show an error when synthetic name cannot be found', () => { + const expectedName = 'syntheticName'; + const expectedError = 'cannot find synthetic'; + + const newRelicServiceMock = { + updateMonitorSettings: td.function() + } + + td.when(newRelicServiceMock.updateMonitorSettings( + td.matchers.isA(String), + td.matchers.isA(Number), + td.matchers.isA(Array), + td.matchers.isA(String), + td.matchers.isA(String), + td.matchers.isA(String), + td.callback + )).thenCallback(expectedError); + + const syntheticsListFileService = { + getSynthetic: td.function() + }; + + td.when(syntheticsListFileService.getSynthetic( + expectedName, + td.callback + )).thenCallback(null, expectedError); + + const changeConfigOrchestrator = changeConfigOrchestratorFactory(newRelicServiceMock, syntheticsListFileService); + + (() => { + changeConfigOrchestrator.changeConfigurationByName( + expectedName, expectedFrequency, expectedLocations, expectedUri, expectedStatus, expectedNewName + ); + }).should.throw(expectedError); + }); +}); \ No newline at end of file diff --git a/test/lib/service/NewRelicSerivceTest.js b/test/lib/service/NewRelicSerivceTest.js index aed6918..86823ad 100644 --- a/test/lib/service/NewRelicSerivceTest.js +++ b/test/lib/service/NewRelicSerivceTest.js @@ -384,4 +384,74 @@ describe('NewRelicService', () => { } ); }); + + it ('should PATCH to NR when changing configuration', () => { + const expectedId = 'syntheticId'; + const frequency = 5; + const locations = ['location1', 'location2']; + const uri = 'http://theuri.com'; + const status = 'ENABLED'; + const newName = 'syntheticName'; + const responseMock = { + statusCode: 204 + }; + + const requestMock = td.function(); + + td.when(requestMock( + td.matchers.isA(Object), + td.callback + )).thenCallback(null, responseMock, null); + + const newRelicService = newRelicServiceFactory(expectedApiKey, requestMock); + + newRelicService.updateMonitorSettings( + expectedId, + frequency, + locations, + uri, + status, + newName, + (err) => { + requestMock.should.have.been.calledWith( + td.matchers.isA(Object), + td.callback + ); + } + ); + }); + + it ('should return error when NR errors on synthetic config change', () => { + const expectedId = 'syntheticId'; + const frequency = 5; + const locations = null; + const uri = 'http://theuri.com'; + const status = null; + const newName = 'syntheticName'; + const expectedError = 'error changing synthetic config'; + const responseMock = { + statusCode: 500 + }; + + const requestMock = td.function(); + + td.when(requestMock( + td.matchers.isA(Object), + td.callback + )).thenCallback(expectedError, responseMock, null); + + const newRelicService = newRelicServiceFactory(expectedApiKey, requestMock); + + newRelicService.updateMonitorSettings( + expectedId, + frequency, + locations, + uri, + status, + newName, + (err) => { + err.should.be.equal(expectedError); + } + ); + }); }); \ No newline at end of file From 996d549d147f61e53c3efdd0d4b07d9679cd1fee Mon Sep 17 00:00:00 2001 From: David Bradford Date: Mon, 30 Jan 2017 09:52:24 -0600 Subject: [PATCH 11/14] Add ability to change alerting configuration --- bin/cmds/config.js | 20 +++- lib/orchestrator/ChangeConfigOrchestrator.js | 48 ++++++++-- lib/service/NewRelicService.js | 60 +++++++++++- .../ChangeConfigOrchestratorTest.js | 60 +++++++++++- test/lib/service/NewRelicSerivceTest.js | 92 +++++++++++++++++++ 5 files changed, 266 insertions(+), 14 deletions(-) diff --git a/bin/cmds/config.js b/bin/cmds/config.js index 5f829d6..12d2334 100644 --- a/bin/cmds/config.js +++ b/bin/cmds/config.js @@ -37,7 +37,15 @@ exports.builder = { rename: { desc: 'New name to use for synthetic', type: 'string', - } + }, + addemail: { + desc: 'Add emails to alerting for synthetics (parameter can be specified multiple times)', + type: 'array', + }, + rmemail: { + desc: 'Remove email from alerting for synthetics', + type: 'string', + }, } function validate(argv) { @@ -45,7 +53,7 @@ function validate(argv) { throw new Error('ERROR: Either name or id must be specified'); } - const allOptions = [argv.frequency, argv.locations, argv.uri, argv.status, argv.rename]; + const allOptions = [argv.frequency, argv.locations, argv.uri, argv.status, argv.rename, argv.addemail, argv.rmemail]; if (_.every(allOptions, _.isNil)) { throw new Error('Error: No changes specified'); @@ -71,7 +79,9 @@ exports.handler = function (argv) { argv.locations, argv.uri, argv.status, - argv.rename + argv.rename, + argv.addemail, + argv.rmemail ); } else if (!_.isNil(argv.name)) { changeConfigOrchestrator.changeConfigurationByName( @@ -80,7 +90,9 @@ exports.handler = function (argv) { argv.locations, argv.uri, argv.status, - argv.rename + argv.rename, + argv.addemail, + argv.rmemail ); } } \ No newline at end of file diff --git a/lib/orchestrator/ChangeConfigOrchestrator.js b/lib/orchestrator/ChangeConfigOrchestrator.js index f5c4b73..344fdaa 100644 --- a/lib/orchestrator/ChangeConfigOrchestrator.js +++ b/lib/orchestrator/ChangeConfigOrchestrator.js @@ -8,13 +8,49 @@ class ChangeConfigOrchestrator { this.syntheticsListFileService = syntheticsListFileService; } - changeConfigurationById(id, frequency, locations, uri, status, rename, callback) { + changeConfigurationById(id, frequency, locations, uri, status, rename, addAlertEmails, rmAlertEmail, callback) { logger.verbose('ChangeConfigOrchestrator.changeConfigurationById'); - this.newRelicService.updateMonitorSettings(id, frequency, locations, uri, status, rename, (err) => { + async.parallel([ + ((next) => { + if (!_.isNil(frequency) || + !_.isNil(locations) || + !_.isNil(uri) || + !_.isNil(status) || + !_.isNil(rename) + ) { + this.newRelicService.updateMonitorSettings( + id, + frequency, + locations, + uri, + status, + rename, + next + ); + } + }).bind(this), + + ((next) => { + if (!_.isNil(addAlertEmails)) { + this.newRelicService.addAlertEmails( + id, + addAlertEmails, + next + ); + } + }).bind(this), + + ((next) => { + if (!_.isNil(rmAlertEmail)) { + this.newRelicService.removeAlertEmail(id, rmAlertEmail, next); + } + }).bind(this) + + ], (err) => { if (err) { - if (callback) { - callback(err); + if (callback) { + callback(err); } else { logger.error(err); throw new Error(err); @@ -24,7 +60,7 @@ class ChangeConfigOrchestrator { } - changeConfigurationByName(name, frequency, locations, uri, status, rename) { + changeConfigurationByName(name, frequency, locations, uri, status, rename, addAlertEmails, rmAlertEmail) { logger.verbose('ChangeConfigOrchestrator.changeConfigurationByName'); async.waterfall([ @@ -38,7 +74,7 @@ class ChangeConfigOrchestrator { }).bind(this), ((id, next) => { - this.changeConfigurationById(id, frequency, locations, uri, status, rename, next); + this.changeConfigurationById(id, frequency, locations, uri, status, rename, addAlertEmails, rmAlertEmail, next); }).bind(this), ], (err) => { diff --git a/lib/service/NewRelicService.js b/lib/service/NewRelicService.js index 0e1cb21..6873e5e 100644 --- a/lib/service/NewRelicService.js +++ b/lib/service/NewRelicService.js @@ -5,7 +5,9 @@ const htmlToText = require('html-to-text'); const SYNTHETICS_HOST = 'synthetics.newrelic.com'; const MONITORS_ENDPOINT = '/synthetics/api/v3/monitors'; const MONITORS_URL = 'https://' + SYNTHETICS_HOST + MONITORS_ENDPOINT; -const ALERTS_ENDPOINT = '/synthetics/api/v1/locations'; +const LOCATIONS_ENDPOINT = '/synthetics/api/v1/locations'; +const LOCATIONS_URL = 'https://' + SYNTHETICS_HOST + LOCATIONS_ENDPOINT; +const ALERTS_ENDPOINT = '/synthetics/api/v1/monitors'; const ALERTS_URL = 'https://' + SYNTHETICS_HOST + ALERTS_ENDPOINT; class SyntheticsService { @@ -190,7 +192,7 @@ class SyntheticsService { logger.verbose('NewRelicService.getAvailableLocation'); const options = this._getOptions({ - url: ALERTS_URL + url: LOCATIONS_URL }); this._sendRequestToNewRelic( @@ -255,6 +257,60 @@ class SyntheticsService { } ); } + + addAlertEmails(id, emails, callback) { + logger.verbose('NewRelicService.addAlertEmails: ' + id); + + const requestParam = { + count: emails.length, + emails: emails + }; + + logger.debug('require parameters: ' + JSON.stringify(requestParam)); + + const options = this._getOptions({ + url: ALERTS_URL + '/' + id + '/notifications', + method: 'POST', + body: JSON.stringify(requestParam), + }); + + this._sendRequestToNewRelic( + options, + 204, + 'adding alert emails', + (err, body, response) => { + if (err) { + return callback(err); + } + + return callback(); + } + ); + } + + removeAlertEmail(id, email, callback) { + logger.verbose('NewRelicService.removeAlertEmail: ' + id); + + logger.debug('email: ' + email); + + const options = this._getOptions({ + url: ALERTS_URL + '/' + id + '/notifications/' + encodeURIComponent(email), + method: 'DELETE' + }); + + this._sendRequestToNewRelic( + options, + 204, + 'removing alert email', + (err, body, response) => { + if (err) { + return callback(err); + } + + return callback(); + } + ); + } } module.exports = (apikey, request) => { return new SyntheticsService(apikey, request); }; \ No newline at end of file diff --git a/test/lib/orchestrator/ChangeConfigOrchestratorTest.js b/test/lib/orchestrator/ChangeConfigOrchestratorTest.js index b8fba37..850e204 100644 --- a/test/lib/orchestrator/ChangeConfigOrchestratorTest.js +++ b/test/lib/orchestrator/ChangeConfigOrchestratorTest.js @@ -36,7 +36,7 @@ describe('ChangeConfigOrchestrator', () => { const changeConfigOrchestrator = changeConfigOrchestratorFactory(newRelicServiceMock, syntheticsListFileService); changeConfigOrchestrator.changeConfigurationById( - expectedId, expectedFrequency, expectedLocations, expectedUri, expectedStatus, expectedNewName, (err) => { + expectedId, expectedFrequency, expectedLocations, expectedUri, expectedStatus, expectedNewName, null, null, (err) => { newRelicServiceMock.updateMonitorSettings.should.have.been.calledWith( expectedId, expectedFrequency, @@ -100,7 +100,7 @@ describe('ChangeConfigOrchestrator', () => { const changeConfigOrchestrator = changeConfigOrchestratorFactory(newRelicServiceMock, syntheticsListFileService); changeConfigOrchestrator.changeConfigurationById( - expectedId, expectedFrequency, expectedLocations, expectedUri, expectedStatus, expectedNewName, (err) => { + expectedId, expectedFrequency, expectedLocations, expectedUri, expectedStatus, expectedNewName, null, null, (err) => { err.should.be.equal(expectedError); } ); @@ -189,4 +189,60 @@ describe('ChangeConfigOrchestrator', () => { ); }).should.throw(expectedError); }); + + it ('call to NR to add alert emails by id', () => { + const expectedEmails = ['email1@email.com', 'email2@email.com']; + + const newRelicServiceMock = { + addAlertEmails: td.function() + } + + td.when(newRelicServiceMock.addAlertEmails( + td.matchers.isA(String), + td.matchers.isA(Array), + td.callback + )).thenCallback(); + + const syntheticsListFileService = {}; + + const changeConfigOrchestrator = changeConfigOrchestratorFactory(newRelicServiceMock, syntheticsListFileService); + + changeConfigOrchestrator.changeConfigurationById( + expectedId, null, null, null, null, null, expectedEmails, null, (err) => { + newRelicServiceMock.addAlertEmails.should.have.been.calledWith( + expectedId, + expectedEmails, + td.callback + ); + } + ); + }); + + it ('call to NR to remove alert email by id', () => { + const expectedEmail = 'email1@email.com'; + + const newRelicServiceMock = { + removeAlertEmail: td.function() + } + + td.when(newRelicServiceMock.removeAlertEmail( + td.matchers.isA(String), + td.matchers.isA(String), + td.callback + )).thenCallback(); + + const syntheticsListFileService = {}; + + const changeConfigOrchestrator = changeConfigOrchestratorFactory(newRelicServiceMock, syntheticsListFileService); + + changeConfigOrchestrator.changeConfigurationById( + expectedId, null, null, null, null, null, null, expectedEmail, (err) => { + newRelicServiceMock.removeAlertEmail.should.have.been.calledWith( + expectedId, + expectedEmail, + td.callback + ); + } + ); + }); }); \ No newline at end of file diff --git a/test/lib/service/NewRelicSerivceTest.js b/test/lib/service/NewRelicSerivceTest.js index 86823ad..52c4882 100644 --- a/test/lib/service/NewRelicSerivceTest.js +++ b/test/lib/service/NewRelicSerivceTest.js @@ -454,4 +454,96 @@ describe('NewRelicService', () => { } ); }); + + it ('should POST to NR when adding alert emails', () => { + const expectedId = 'syntheticId'; + const expectedEmails = ['email1@email.com', 'email2@email.com']; + const responseMock = { + statusCode: 204 + }; + + const requestMock = td.function(); + + td.when(requestMock( + td.matchers.isA(Object), + td.callback + )).thenCallback(null, responseMock, null); + + const newRelicService = newRelicServiceFactory(expectedApiKey, requestMock); + + newRelicService.addAlertEmails(expectedId, expectedEmails, (err) => { + requestMock.should.have.been.calledWith( + td.matchers.isA(Object), + td.callback + ); + }); + }); + + it ('should return error when NR errors on adding emails', () => { + const expectedId = 'syntheticId'; + const expectedEmails = ['email1@email.com', 'email2@email.com']; + const expectedError = 'error changing synthetic config'; + const responseMock = { + statusCode: 500 + }; + + const requestMock = td.function(); + + td.when(requestMock( + td.matchers.isA(Object), + td.callback + )).thenCallback(expectedError, responseMock, null); + + const newRelicService = newRelicServiceFactory(expectedApiKey, requestMock); + + newRelicService.addAlertEmails(expectedId, expectedEmails, (err) => { + err.should.be.equal(expectedError); + }); + }); + + it ('should POST to NR when removing alert emails', () => { + const expectedId = 'syntheticId'; + const expectedEmail = 'email1@email.com'; + const responseMock = { + statusCode: 204 + }; + + const requestMock = td.function(); + + td.when(requestMock( + td.matchers.isA(Object), + td.callback + )).thenCallback(null, responseMock, null); + + const newRelicService = newRelicServiceFactory(expectedApiKey, requestMock); + + newRelicService.removeAlertEmail(expectedId, expectedEmail, (err) => { + requestMock.should.have.been.calledWith( + td.matchers.isA(Object), + td.callback + ); + }); + }); + + it ('should return error when NR errors on removing emails', () => { + const expectedId = 'syntheticId'; + const expectedEmail = 'email1@email.com'; + const expectedError = 'error changing synthetic config'; + const responseMock = { + statusCode: 500 + }; + + const requestMock = td.function(); + + td.when(requestMock( + td.matchers.isA(Object), + td.callback + )).thenCallback(expectedError, responseMock, null); + + const newRelicService = newRelicServiceFactory(expectedApiKey, requestMock); + + newRelicService.removeAlertEmail(expectedId, expectedEmail, (err) => { + err.should.be.equal(expectedError); + }); + }); }); \ No newline at end of file From 65e0c2de9d9e1714436ae317a8a62dcd15f5686b Mon Sep 17 00:00:00 2001 From: David Bradford Date: Tue, 7 Feb 2017 17:37:01 -0600 Subject: [PATCH 12/14] Add ability to add alerting when creating a synthetic --- bin/cmds/create.js | 8 +++++++- lib/orchestrator/CreateMonitorOrchestrator.js | 19 ++++++++++++++++--- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/bin/cmds/create.js b/bin/cmds/create.js index 683fd47..769727c 100644 --- a/bin/cmds/create.js +++ b/bin/cmds/create.js @@ -35,6 +35,11 @@ exports.builder = { alias: 'u', desc: 'URI for synthetic (required for SIMPLE and BROWSER synthetics)', type: 'string' + }, + emails: { + alias: 'e', + desc: 'Emails to send synthetic alerts to (can be specified multiple times)', + type: 'array', } } @@ -77,6 +82,7 @@ exports.handler = function (argv) { argv.frequency, argv.filename, null, - argv.uri + argv.uri, + argv.emails ); } \ No newline at end of file diff --git a/lib/orchestrator/CreateMonitorOrchestrator.js b/lib/orchestrator/CreateMonitorOrchestrator.js index 9d1863b..649b31b 100644 --- a/lib/orchestrator/CreateMonitorOrchestrator.js +++ b/lib/orchestrator/CreateMonitorOrchestrator.js @@ -34,6 +34,7 @@ function createSyntheticInNewRelic( filename, status, uri, + emails, callback ) { async.waterfall([ @@ -50,9 +51,19 @@ function createSyntheticInNewRelic( (id, next) => { logger.debug('Adding Synthetic to file: ' + id + ' ' + filename); syntheticsListFileService.addSynthetic(id, name, filename, (err) => { - next(err); + next(err, id); }); + }, + + (id, next) => { + if (!_.isNil(emails)) { + logger.debug('Adding alerting for synthetic: ' + id); + newRelicService.addAlertEmails(id, emails, next); + } else { + next(); + } } + ], (err) => { callback(err); }); @@ -66,7 +77,7 @@ class CreateMonitorOrchestrator { this.defaults = defaults; } - createNewMonitor(name, locations, type, frequency, filename, callback, uri) { + createNewMonitor(name, locations, type, frequency, filename, callback, uri, alertEmails) { logger.verbose('CreateMonitorOrchestrator.createNewMonitor: start'); logger.debug( 'name: ' + name + @@ -74,7 +85,8 @@ class CreateMonitorOrchestrator { ', type: ' + type + ', frequency: ' + frequency + ', filename: ' + filename + - ', uri: ' + uri + ', uri: ' + uri + + ', emails: ' + alertEmails ); async.parallel([ @@ -99,6 +111,7 @@ class CreateMonitorOrchestrator { filename, 'ENABLED', uri, + alertEmails, next ); }.bind(this) From 2fba51a87fd00fe984d415bb280dee84fbe0baea Mon Sep 17 00:00:00 2001 From: David Bradford Date: Tue, 7 Feb 2017 21:52:10 -0600 Subject: [PATCH 13/14] Add test for adding alerts on create --- .../CreateMonitorOrchestratorTest.js | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/test/lib/orchestrator/CreateMonitorOrchestratorTest.js b/test/lib/orchestrator/CreateMonitorOrchestratorTest.js index 1d450f7..8f33c57 100644 --- a/test/lib/orchestrator/CreateMonitorOrchestratorTest.js +++ b/test/lib/orchestrator/CreateMonitorOrchestratorTest.js @@ -189,4 +189,70 @@ describe('CreateMonitorOrchestrator', function () { syntheticsFileServiceMock.exists.should.not.have.been.called; }); + + it ('should call into new relic for adding alerting', function () { + const expectedId = 'syntheticId'; + const alertEmails = ['email@domain.com']; + + const syntheticsFileServiceMock = { + exists: td.function(), + createFile: td.function() + }; + + const newRelicServiceMock = { + createSynthetic: td.function(), + addAlertEmails: td.function() + }; + + td.when(newRelicServiceMock.createSynthetic( + td.matchers.isA(String), + td.matchers.isA(Array), + td.matchers.isA(Number), + td.matchers.isA(String), + td.matchers.isA(String), + td.callback, + td.matchers.anything() + )).thenCallback('http://newrelic/' + expectedId); + td.when(syntheticsFileServiceMock.exists( + td.matchers.isA(String), + td.callback + )).thenCallback(true); + td.when(syntheticsListFileServiceMock.addSynthetic( + td.matchers.isA(String), + td.matchers.isA(String), + td.matchers.isA(String), + td.callback + )).thenCallback(); + td.when(newRelicServiceMock.addAlertEmails( + td.matchers.isA(String), + td.matchers.isA(Array), + td.callback + )).thenCallback(); + + const createMonitorOrchestrator = createMonitorOrchestratorFactory( + syntheticsFileServiceMock, + newRelicServiceMock, + syntheticsListFileServiceMock, + defaultsMock + ); + + createMonitorOrchestrator.createNewMonitor(monitorName, locations, type, frequency, expectedFilename, () => { + newRelicServiceMock.createSynthetic.should.have.been.calledWith( + monitorName, + locations, + frequency, + 'ENABLED', + type, + td.callback, + null + ); + newRelicServiceMock.addAlertEmails.should.have.been.calledWith( + expectedId, + alertEmails, + td.callback + ); + }, + null, + alertEmails); + }); }); \ No newline at end of file From 609ddf348dbeb407251a38648397e89ece562f85 Mon Sep 17 00:00:00 2001 From: David Bradford Date: Wed, 8 Feb 2017 17:58:30 -0600 Subject: [PATCH 14/14] Updated Changelog and readme --- CHANGELOG.md | 11 +++++++++++ README.md | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d275a93..3c8b335 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,14 @@ +# 2.0.0 / 2017-02-09 + +* Format synthetics.json file to make it human readable +* Add Developer documentation +* Add Contribution guide +* Add ability to update alerting +* Add command to get available locations +* Add ability to update synthetics configuration +* Add support for non-SCRIPTED_BROWSER synthetics +* Better handling of New Relic errors + # 1.0.1 / 2017-01-04 * Updated README diff --git a/README.md b/README.md index e04ad57..a9c3098 100644 --- a/README.md +++ b/README.md @@ -103,6 +103,7 @@ Create a synthetic in New Relic and a file to contain the synthetic code. Possib * --locations - Locations to run the synthetic. This can be specified multiple times to specify multiple locations. * --type - Type of synthetic to create. Possible values are: SIMPLE, BROWSER, SCRIPT_BROWSER, SCRIPT_API. * --uri - URI that synthetic should check (required for SIMPLE and BROWSER synthetics). +* --emails - Email to send synthetic alerts to (can be specified multiple times). ### Update New Relic with synthetics code @@ -131,6 +132,38 @@ synthmanager import Import an existing synthetic from New Relic. +### See a list of available loctions + +Synthetics run from specified locations. You can get a list of available locations from the following command. + +``` +synthmanager locations +``` + +### Change synthetics configuration + +A synthetics configuration can be changed with the following command: + +``` +synthmanager config +``` + +The synthetic to change must be specified with one of the following options: + +* --name - name of synthetic to change. +* --id - id of synthetic to change. + +The following configuration changes can be made: + +* --frequency - Frequency to run the synthetic in minutes. This should be an integer. Possible values are: 1, 5, 10, 15, 30, 60, 360, 720, or 1440. The default is 10. +* --locations - Locations to run the synthetic. This can be specified multiple times to specify multiple locations. +* --uri - URI that synthetic should check (only for SIMPLE and BROWSER synthetics). +* --status - Is the synthetic enabled? (possible values: "ENABLED", "DISABLED", "MUTED") +* --rename - Change the name of the synthetic. +* --addemail - Add the specified email to alerting for the synthetic (this option can be specified multiple times). +* --rmemail - Remove the specified email from alerting for the synthetic. + + ## Configuration Configuration options can be changed by adding a 'synthetics.config.json' file in the base of the project.