From cd33d328b5aeef7c5c1ec3964c530c83db33724a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20Gierski?= Date: Fri, 31 Aug 2018 16:53:11 +0200 Subject: [PATCH] add comments, clean the code, add sample data --- fetchTest.js | 259 ------------------------------------- generateArtilleryConfig.js | 6 +- runTest.js | 126 +++++------------- scenarioProcessor.js | 131 +++++++++++++++++++ serverless.yml | 16 +-- 5 files changed, 173 insertions(+), 365 deletions(-) delete mode 100644 fetchTest.js create mode 100644 scenarioProcessor.js diff --git a/fetchTest.js b/fetchTest.js deleted file mode 100644 index 34f0b80..0000000 --- a/fetchTest.js +++ /dev/null @@ -1,259 +0,0 @@ -const Influx = require('influx') -const throttle = require('lodash/throttle') -const get = require('lodash/get') -const faker = require('faker') - -const DEBUG = true - -const influx = new Influx.InfluxDB({ - host: process.env.INFLUX_HOST, - username: process.env.INFLUX_USER, - password: process.env.INFLUX_PASSWORD, - database: process.env.INFLUX_DB, - schema: [ - { - measurement: 'RequestSent', - fields: { - test: Influx.FieldType.STRING, - }, - tags: [] - }, - { - measurement: 'Errors', - fields: { - step: Influx.FieldType.STRING, - errorType: Influx.FieldType.STRING, - }, - tags: [] - }, - { - measurement: 'ReservationStatus', - fields: { - status: Influx.FieldType.STRING, - number: Influx.FieldType.INTEGER, - reservationId: Influx.FieldType.STRING, - }, - tags: [] - } - ] -}) - -let points = [] - -sendPointsToInflux = () => { - influx.writePoints(points, { - precision: 'ms' - }).catch(err => { - console.log(`Error saving data to InfluxDB! ${err.stack}`) - }) - points = [] - console.log('Sent requests reported to InfluxDB.') -} - -const throttledSendPointsToInflux = throttle(sendPointsToInflux, 10000, { trailing: true, leading: false }) - -module.exports.writeDataToDB = (context, ee, next) => { - sendPointsToInflux() - return next() -} - -module.exports.generateNewUserEmailInFunction = (context, ee, next) => { - const randomFirstName = faker.name.firstName() - const randomLastName = faker.name.lastName() - const randomString = faker.random.uuid().substring(0, 8) - const newUserEmail = `${randomFirstName}.${randomLastName}-${randomString}@xolvioLoadTest.com` - console.log('230 newUserEmail', newUserEmail); - context.vars.firstName = randomFirstName - context.vars.lastName = randomLastName - context.vars.newUserEmail = newUserEmail - return next() -} - -// module.exports.generateNewUserEmailInRequest = (requestParams, context, ee, next) => { -// const randomFirstName = faker.name.firstName() -// const randomLastName = faker.name.lastName() -// const randomString = faker.random.uuid().substring(0, 8) -// const newUserEmail = `${randomFirstName}.${randomLastName}-${randomString}@xolvioLoadTest.com` -// console.log('230 newUserEmail', newUserEmail); -// context.vars.firstName = randomFirstName -// context.vars.lastName = randomLastName -// context.vars.newUserEmail = newUserEmail -// return next() -// } - -module.exports.logRequestSentToInflux = (requestParams, context, ee, next) => { - points.push({ - measurement: 'RequestSent', - fields: { test: 'A' }, - tags: {}, - timestamp: (new Date()).getTime() - }) - - throttledSendPointsToInflux() - return next() -} - -// --------- SUBMIT RESERVATION HOOKS --------- - -module.exports.logResponseSubmitReservation = (requestParams, response, context, ee, next) => { - - console.log('199 requestParams', requestParams); - - const responseJSON = JSON.parse(response.body) - - const errors = get(responseJSON, 'data.errors') - if (errors) { - const statusCode = get(responseJSON, 'data.errors[0].statusCode') - console.log('179 ERROR statusCode', statusCode) - } - - if (DEBUG) { - const reservationId = get(responseJSON, 'data.submitReservation.reservationId') - - if (reservationId) { - console.log('186 CREATED RESERVATION reservationId', reservationId) - } else { - console.log('188 response.body', response.body) - } - } - - return next() -} - -module.exports.logErrorsInSubmitReservationForNewCustomers = (requestParams, context, ee, next) => { - ee.on('error', handleSubmitReservationForNewCustomersErrors) - return next() -} - -module.exports.logErrorsInSubmitReservationForExistingCustomers = (requestParams, context, ee, next) => { - ee.on('error', handleSubmitReservationForExistingCustomersErrors) - return next() -} - -// --------- END SUBMIT RESERVATION HOOKS --------- - -// --------- GET RESERVATION HOOKS --------- - -module.exports.logResponseGetReservation = (requestParams, response, context, ee, next) => { - const responseJSON = JSON.parse(response.body) - - const errors = get(responseJSON, 'data.errors') - if (errors) { - const statusCode = get(responseJSON, 'data.errors[0].statusCode') - console.log('157 ERROR statusCode', statusCode) - } - - if (DEBUG) { - const reservationId = get(responseJSON, 'data.getReservation.publicReservationId') - const reservationStatus = get(responseJSON, 'data.getReservation.reservationStatus') - - if (reservationId && reservationStatus) { - console.log('165 reservationStatus && reservationId:', reservationStatus, reservationId) - } else { - console.log('167 response.body', response.body) - } - } - return next() -} - -module.exports.logGetReservationToInflux = (requestParams, response, context, ee, next) => { - const responseJSON = JSON.parse(response.body) - const reservationId = get(responseJSON, 'data.getReservation.publicReservationId') - const reservationStatus = get(responseJSON, 'data.getReservation.reservationStatus') - - if (reservationId && reservationStatus) { - points.push({ - measurement: 'ReservationStatus', - fields: { reservationId, status: reservationStatus, number: 1 }, - tags: {}, - timestamp: (new Date()).getTime() - }) - - throttledSendPointsToInflux() - } - - return next() -} - -module.exports.logErrorsInGetReservationPreValidation = (requestParams, context, ee, next) => { - ee.on('error', handleGetReservationPreValidationErrors) - return next() -} - -module.exports.logErrorsInGetReservationPostValidation = (requestParams, context, ee, next) => { - ee.on('error', handleGetReservationPostValidationErrors) - return next() -} - -// --------- END GET RESERVATION HOOKS --------- - -// --------- VALIDATE PAYMENT HOOKS --------- - -module.exports.logErrorsInProcessValidatePayment = (requestParams, context, ee, next) => { - ee.on('error', handleProcessValidatePaymentErrors) - return next() -} - -module.exports.logResponseValidateReservation = (requestParams, response, context, ee, next) => { - if (response.statusCode != '200') { - handleProcessValidatePaymentErrors(response.statusCode) - console.log('215 response.statusCode', response.statusCode) - } - return next() -} - -// --------- END VALIDATE PAYMENT HOOKS --------- - -// --------- ERROR HANDLERS --------- - -const handleSubmitReservationForNewCustomersErrors = (error) => { - console.log('117 handleSubmitReservationForNewCustomersErrors', error) - points.push({ - measurement: 'Errors', - fields: { step: 'submitReservationNewCustomer', errorType: error }, - tags: {}, - timestamp: (new Date()).getTime() - }) -} - -const handleSubmitReservationForExistingCustomersErrors = (error) => { - console.log('117 handleSubmitReservationForExistingCustomersErrors', error) - points.push({ - measurement: 'Errors', - fields: { step: 'submitReservationExistingCustomer', errorType: error }, - tags: {}, - timestamp: (new Date()).getTime() - }) -} - -const handleGetReservationPreValidationErrors = (error) => { - console.log('137 handleGetReservationPreValidationErrors', error) - points.push({ - measurement: 'Errors', - fields: { step: 'getReservation', errorType: error }, - tags: {}, - timestamp: (new Date()).getTime() - }) -} - -const handleProcessValidatePaymentErrors = (error) => { - console.log('147 handleProcessValidatePaymentErrors', error) - points.push({ - measurement: 'Errors', - fields: { step: 'processValidatePayment', errorType: error }, - tags: {}, - timestamp: (new Date()).getTime() - }) -} - -const handleGetReservationPostValidationErrors = (error) => { - console.log('157 handleGetReservationPostValidationErrors', error) - points.push({ - measurement: 'Errors', - fields: { step: 'getReservation2', errorType: error }, - tags: {}, - timestamp: (new Date()).getTime() - }) -} - -// --------- END ERROR HANDLERS --------- \ No newline at end of file diff --git a/generateArtilleryConfig.js b/generateArtilleryConfig.js index 294b190..6fbcc88 100644 --- a/generateArtilleryConfig.js +++ b/generateArtilleryConfig.js @@ -23,7 +23,7 @@ module.exports.generateArtilleryScript = function (options) { return { config: { target: `${opts.protocol}://${opts.auth ? `${opts.auth}@` : ''}${opts.host}`, - processor: 'fetchTest.js', + processor: 'scenarioProcessor.js', // here we define an external js file with custom hooks and functions which can be run during the scenario phases: [ { duration: opts.duration, @@ -120,7 +120,7 @@ module.exports.generateLoadPhases = function (startLoad = 10, endLoad = 1000, ra module.exports.generateSoakPhases = function (load = 10, duration = 5 * 60) { return [ - // warmUpPhase, + warmUpPhase, { duration: duration, arrivalRate: load @@ -159,4 +159,4 @@ module.exports.generateSpikePhases = function (consistentLoad = 30, spikeLoad = } return phases -} \ No newline at end of file +} diff --git a/runTest.js b/runTest.js index 76b37fe..f45ecb9 100644 --- a/runTest.js +++ b/runTest.js @@ -5,96 +5,33 @@ const { generateSoakPhases, generateSpikePhases, generateStressPhases -} = require("./generateArtilleryConfig"); - -const { submitReservationExistingCustomer, processValidatePayment, getReservation, submitReservationNewCustomer } = require('./queriesParams') +} = require('./generateArtilleryConfig') const JSONreplacer = null const JSONspacer = 2 -const submitReservationNewCustomerAndValidatePaymentScenario = { - flow: [ - { - function: 'generateNewUserEmailInFunction', - }, - { - post: { - url: submitReservationNewCustomer.endpoint, - body: submitReservationNewCustomer.command, - headers: submitReservationNewCustomer.headers, - capture: submitReservationNewCustomer.capture, - beforeRequest: ['logErrorsInSubmitReservationForNewCustomers', 'logRequestSentToInflux'], - afterResponse: 'logResponseSubmitReservation' - }, - }, - { - post: { - url: getReservation.endpoint, - body: getReservation.command, - headers: getReservation.headers, - beforeRequest: ['logErrorsInGetReservationPreValidation'], - afterResponse: ['logResponseGetReservation', 'logGetReservationToInflux'], - }, - }, - { - post: { - url: processValidatePayment.endpoint, - body: processValidatePayment.command, - headers: processValidatePayment.headers, - beforeRequest: ['logErrorsInProcessValidatePayment'], - afterResponse: 'logResponseValidateReservation', - }, - }, - { - post: { - url: getReservation.endpoint, - body: getReservation.command, - headers: getReservation.headers, - beforeRequest: ['logErrorsInGetReservationPostValidation'], - afterResponse: ['logResponseGetReservation', 'logGetReservationToInflux'], - }, - } - ], - afterScenario: 'writeDataToDB' -} - -const submitReservationExistingCustomerAndValidatePaymentScenario = { +const exampleScenario = { flow: [ { - post: { - url: submitReservationExistingCustomer.endpoint, - body: submitReservationExistingCustomer.command, - headers: submitReservationExistingCustomer.headers, - capture: submitReservationExistingCustomer.capture, - beforeRequest: ['logErrorsInSubmitReservationForExistingCustomers', 'logRequestSentToInflux'], - afterResponse: 'logResponseSubmitReservation' - }, - }, - { - post: { - url: getReservation.endpoint, - body: getReservation.command, - headers: getReservation.headers, - beforeRequest: ['logErrorsInGetReservationPreValidation'], - afterResponse: ['logResponseGetReservation', 'logGetReservationToInflux'], - }, - }, - { - post: { - url: processValidatePayment.endpoint, - body: processValidatePayment.command, - headers: processValidatePayment.headers, - beforeRequest: ['logErrorsInProcessValidatePayment'], - afterResponse: 'logResponseValidateReservation', - }, + function: 'generateNewUserEmailInFunction' // here we use faker to generate a random new user data }, { post: { - url: getReservation.endpoint, - body: getReservation.command, - headers: getReservation.headers, - beforeRequest: ['logErrorsInGetReservationPostValidation'], - afterResponse: ['logResponseGetReservation', 'logGetReservationToInflux'], + url: '/graphql', // or full URL with https:// + body: JSON.stringify( // string with JSON query/mutation goes here + { + email: '{{ newUserEmail }}', // here we use variables created by faker in a function above + firstName: '{{ firstName }}', + lastName: '{{ lastName }}' + } + ), + headers: { 'content-type': 'application/json' }, // we define that the body of POST is a JSON, for example for graphql + capture: { // it's possible to capture data from response and store it for later usage in scenario + json: '$.data.newCustomer.newAccountId', // use JSONpath here + as: 'newAccountId' // set the variable name here + }, + beforeRequest: ['logRequestToConsole', 'logRequestSentToInflux', 'logErrorsInStep'], + afterResponse: 'logResponseToConsole' }, } ], @@ -102,9 +39,8 @@ const submitReservationExistingCustomerAndValidatePaymentScenario = { } const scenarios = [ - // submitReservationNewCustomerAndValidatePaymentScenario, - submitReservationExistingCustomerAndValidatePaymentScenario -]; + exampleScenario +] function saveScriptToFile (script, fileName) { const scriptData = JSON.stringify(script, JSONreplacer, JSONspacer) @@ -115,8 +51,8 @@ function saveScriptToFile (script, fileName) { function executeArtillery (fileName, local = false) { console.log('Starting test on target:', process.env.LOADTEST_TARGET) - const exec = require('child_process').exec; - let child; + const exec = require('child_process').exec + let child if (local) { child = exec(`artillery run ${fileName}`) } else { @@ -127,7 +63,7 @@ function executeArtillery (fileName, local = false) { }) } -function runLoadTest ({local} = {}) { +function runLoadTest ({ local } = {}) { const phases = generateLoadPhases(startLoad = 1, endLoad = 10, rampUpTime = 60) const totalTime = calculateTotalTime(phases) console.log('Total load test duration: ', totalTime) @@ -142,7 +78,7 @@ function runLoadTest ({local} = {}) { executeArtillery(fileName, local) } -function runStressTest ({local} = {}) { +function runStressTest ({ local } = {}) { const phases = generateStressPhases(startLoad = 10, endLoad = 100, rampUpTimePerStep = 15, flatDurationPerStep = 30, numberOfSteps = 8, rampUpType = 'linear') const totalTime = calculateTotalTime(phases) console.log('Total load test duration: ', totalTime) @@ -157,7 +93,7 @@ function runStressTest ({local} = {}) { executeArtillery(fileName, local) } -function runSoakTest ({local} = {}) { +function runSoakTest ({ local } = {}) { const phases = generateSoakPhases(load = 1, duration = 1) const totalTime = calculateTotalTime(phases) console.log('Total load test duration: ', totalTime) @@ -172,7 +108,7 @@ function runSoakTest ({local} = {}) { executeArtillery(fileName, local) } -function runSpikeTest ({local} = {}) { +function runSpikeTest ({ local } = {}) { const phases = generateSpikePhases(consistentLoad = 30, spikeLoad = 300, loadTestDuration = 400, spikeDuration = 5, numberOfSpikes = 4) const totalTime = calculateTotalTime(phases) console.log('Total load test duration: ', totalTime) @@ -207,21 +143,21 @@ switch (testToRun) { break case 'loadTestLocal': - runLoadTest({local: true}) + runLoadTest({ local: true }) break case 'stressTestLocal': - runStressTest({local: true}) + runStressTest({ local: true }) break case 'soakTestLocal': - runSoakTest({local: true}) + runSoakTest({ local: true }) break case 'spikeTestLocal': - runSpikeTest({local: true}) + runSpikeTest({ local: true }) break default: - throw 'You can start one of: loadTest, stressTest, soakTest, spikeTest' + throw 'You can start one of: loadTest, stressTest, soakTest, spikeTest, loadTestLocal, stressTestLocal, soakTestLocal, spikeTestLocal' } diff --git a/scenarioProcessor.js b/scenarioProcessor.js new file mode 100644 index 0000000..9c1ed69 --- /dev/null +++ b/scenarioProcessor.js @@ -0,0 +1,131 @@ +const Influx = require('influx') +const throttle = require('lodash/throttle') +const get = require('lodash/get') +const faker = require('faker') + +const DEBUG = true + +const influx = new Influx.InfluxDB({ // here we create an InfluxDB client, it works great with Graphana visualisation tool + host: process.env.INFLUX_HOST, + username: process.env.INFLUX_USER, + password: process.env.INFLUX_PASSWORD, + database: process.env.INFLUX_DB, + schema: [ + { + measurement: 'RequestSent', + fields: { + test: Influx.FieldType.STRING, + }, + tags: [] + }, + { + measurement: 'Errors', + fields: { + step: Influx.FieldType.STRING, + errorType: Influx.FieldType.STRING, + }, + tags: [] + } + ] +}) + +let points = [] // this is an array for data points we send to DB later + +sendPointsToInflux = () => { // here we send our data to DB + influx.writePoints(points, { + precision: 'ms' + }).catch(err => { + console.log(`Error saving data to InfluxDB! ${err.stack}`) + }) + points = [] // after sending points to DB we clean the array to avoid sending the same points again + console.log('Sent requests reported to InfluxDB.') +} + +const sendDataToDbEveryMs = 10000 +const throttledSendPointsToInflux = throttle(sendPointsToInflux, sendDataToDbEveryMs, { + trailing: true, + leading: false +}) // we create a throttled DB interface to prevent DB from choaking on connections + +module.exports.writeDataToDB = (context, ee, next) => { + sendPointsToInflux() + return next() // we need to call next to progress in the scenario +} + +module.exports.generateNewUserEmailInFunction = (context, ee, next) => { // here we use faker to generate random user data + const randomFirstName = faker.name.firstName() + const randomLastName = faker.name.lastName() + const randomString = faker.random.uuid().substring(0, 8) + const newUserEmail = `${randomFirstName}.${randomLastName}-${randomString}@testDomain.com` + console.log('newUserEmail', newUserEmail) + context.vars.firstName = randomFirstName // context is shared between steps during the whole scenario run + context.vars.lastName = randomLastName + context.vars.newUserEmail = newUserEmail + return next() +} + +module.exports.logRequestSentToInflux = (requestParams, context, ee, next) => { // here we log request send timestamp + points.push({ + measurement: 'RequestSent', + fields: { test: 'A' }, + tags: {}, + timestamp: (new Date()).getTime() + }) + + throttledSendPointsToInflux() + return next() +} + +// --------- STEP HOOKS --------- + +module.exports.logRequestToConsole = (requestParams, context, ee, next) => { + // here we run a custom hook to log data used in request + // requestParams holds data that will be sent over HTTP + // context holds Artillery configuration and variables + // ee is an EventEmitter which can be used to hook additional functions to HTTP engine + + if (DEBUG) { + console.log('requestParams', requestParams) + } + return next() +} + +module.exports.logResponseToConsole = (requestParams, response, context, ee, next) => { + // this is a hook after we get the response, we have an additional parameter response here + + const responseJSON = JSON.parse(response.body) + + const errors = get(responseJSON, 'data.errors') + if (errors) { + const statusCode = get(responseJSON, 'data.errors[0].statusCode') + console.log('ERROR statusCode', statusCode) + } + + if (DEBUG) { + console.log('response.body', response.body) + } + return next() +} + +module.exports.logErrorsInStep = (requestParams, context, ee, next) => { + // here we define an EventEmitter with on error event handler to log errors to DB + + ee.on('error', handleStepErrors) + return next() +} + +// --------- END STEP HOOKS --------- + +// --------- ERROR HANDLERS --------- + +const handleStepErrors = (error) => { + console.log('error in HTTP request', error) + points.push({ + measurement: 'Errors', + fields: { step: 'getReservation', errorType: error }, + tags: {}, + timestamp: (new Date()).getTime() + }) +} + +// --------- END ERROR HANDLERS --------- diff --git a/serverless.yml b/serverless.yml index ab74e6a..98591bb 100644 --- a/serverless.yml +++ b/serverless.yml @@ -10,7 +10,7 @@ # If the following value is changed, your service may be duplicated (this value is used to build the CloudFormation # Template script's name) -service: serverless-artillery-Y19qTgc1Y +service: serverless-artillery-1234RandomId provider: # Using Node JS v4.3 on AWS name: aws @@ -33,13 +33,13 @@ functions: loadGenerator: # !!Do not edit this name!! handler: handler.handler # the serverlessArtilleryLoadTester handler() method can be found in the handler.js source file timeout: 300 # set timeout to be 5 minutes (max for Lambda) - vpc: - securityGroupIds: - - sg-0904cde88f02debad - subnetIds: - - subnet-0e9bc98b7a11e5a45 +# vpc: # Lambdas can be set to run from a private subnet and pass all the requests to 1 IP address +# securityGroupIds: +# - sg-XXXXXXXXXXXXXX +# subnetIds: +# - subnet-XXXXXXXXXXXXXX environment: - INFLUX_HOST: ${env:INFLUX_HOST} - INFLUX_USER: ${env:INFLUX_USER} + INFLUX_HOST: ${env:INFLUX_HOST} # it's possible to pass env variables to lambda functions + INFLUX_USER: ${env:INFLUX_USER} # you can also pass your env variables to lambda variables INFLUX_PASSWORD: ${env:INFLUX_PASSWORD} INFLUX_DB: ${env:INFLUX_DB}