Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Webhooks discord #536

Merged
merged 12 commits into from
Nov 1, 2020
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ It has a simple, one-click installation, built with support for Kubernetes, DC/O
| Cloud Native | :sparkle: |Predator is built to take advantage of Kubernetes and DC/OS. It's integrated with those platforms and can manage the load generators lifecycles by itself.
| Prometheus/Influx integration | :sparkle: |Predator comes integrated with Prometheus and Influx. Simply configure it through the predator REST API or using the UI.
| Compare Multiple tests results | :sparkle: |Built-in dashboard to compare multiple test runs at once.
| Webhooks API | :new: |supported in Slack, Microsoft Teams, or JSON format for an easy server to server integration.
| Webhooks API | :new: |supported in Slack, Microsoft Teams, Discord or JSON format for an easy server to server integration.

-----------------------------------------------------

Expand Down
1 change: 1 addition & 0 deletions docs/devguide/docs/swagger-docs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2769,6 +2769,7 @@ components:
- slack
- json
- teams
- discord
test_webhook_response:
type: object
required:
Expand Down
3 changes: 3 additions & 0 deletions docs/devguide/docs/webhooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ For server to server integration, webhooks can also be sent as an HTTP `POST` re
### TEAMS
Webhooks can be sent in as a Microsoft Teams message to any Teams channel with a proper incoming webhook URL.

### DISCORD
Webhooks can be sent in as a Discord message to any Discord channel with a proper incoming webhook URL.

## Example
A global webhook created in Slack format that will invoke a message to the configured Slack channel's URL on every test run that's in the following phases:

Expand Down
1 change: 1 addition & 0 deletions docs/openapi3.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2817,6 +2817,7 @@ components:
- slack
- json
- teams
- discord
test_webhook_response:
type: object
required:
Expand Down
10 changes: 7 additions & 3 deletions src/common/consts.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
const EVENT_FORMAT_TYPE_SLACK = 'slack';
const EVENT_FORMAT_TYPE_JSON = 'json';
const EVENT_FORMAT_TYPE_TEAMS = 'teams';
const EVENT_FORMAT_TYPE_DISCORD = 'discord';
const WEBHOOK_EVENT_TYPE_STARTED = 'started';
const WEBHOOK_EVENT_TYPE_FINISHED = 'finished';
const WEBHOOK_EVENT_TYPE_API_FAILURE = 'api_failure';
Expand All @@ -10,7 +11,7 @@ const WEBHOOK_EVENT_TYPE_IN_PROGRESS = 'in_progress';
const WEBHOOK_EVENT_TYPE_BENCHMARK_PASSED = 'benchmark_passed';
const WEBHOOK_EVENT_TYPE_BENCHMARK_FAILED = 'benchmark_failed';
const WEBHOOK_SLACK_DEFAULT_MESSAGE_ICON = ':muscle:';
const WEBHOOK_SLACK_DEFAULT_REPORTER_NAME = 'reporter';
const WEBHOOK_DEFAULT_REPORTER_NAME = 'Predator';
const WEBHOOK_TEAMS_DEFAULT_THEME_COLOR = '957c58';

module.exports = {
Expand All @@ -30,13 +31,15 @@ module.exports = {
EVENT_FORMAT_TYPE_SLACK,
EVENT_FORMAT_TYPE_JSON,
EVENT_FORMAT_TYPE_TEAMS,
EVENT_FORMAT_TYPE_DISCORD,
WEBHOOK_SLACK_DEFAULT_MESSAGE_ICON,
WEBHOOK_SLACK_DEFAULT_REPORTER_NAME,
WEBHOOK_DEFAULT_REPORTER_NAME,
WEBHOOK_TEAMS_DEFAULT_THEME_COLOR,
EVENT_FORMAT_TYPES: [
EVENT_FORMAT_TYPE_SLACK,
EVENT_FORMAT_TYPE_JSON,
EVENT_FORMAT_TYPE_TEAMS
EVENT_FORMAT_TYPE_TEAMS,
EVENT_FORMAT_TYPE_DISCORD
],
WEBHOOK_EVENT_TYPES: [
WEBHOOK_EVENT_TYPE_STARTED,
Expand All @@ -61,6 +64,7 @@ module.exports = {
METRONOME: 'METRONOME',
DOCKER: 'DOCKER',
WEBHOOK_TEST_MESSAGE: 'Hello From Predator! Wuff! Wuff!',
WEBHOOK_GRAVATAR_URL: 'https://www.gravatar.com/avatar/af577df746d71dfc4a7ab9f76202f9b8',
CONFIG: {
GRFANA_URL: 'grafana_url',
DELAY_RUNNER_MS: 'delay_runner_ms',
Expand Down
26 changes: 26 additions & 0 deletions src/common/emojiHandler.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
const slackEmojis = require('slack-emojis');
const teamsEmojis = require('./teams-emojis');

const {
EVENT_FORMAT_TYPES,
EVENT_FORMAT_TYPE_SLACK,
EVENT_FORMAT_TYPE_TEAMS,
EVENT_FORMAT_TYPE_DISCORD
} = require('./consts');

module.exports = function(format){
switch (format) {
case EVENT_FORMAT_TYPE_SLACK: {
return slackEmojis;
}
case EVENT_FORMAT_TYPE_TEAMS: {
return teamsEmojis;
}
case EVENT_FORMAT_TYPE_DISCORD:{
return slackEmojis;
}
default: {
throw new Error(`Unrecognized webhook format: ${format}, available options: ${EVENT_FORMAT_TYPES.join()}`);
}
}
};
4 changes: 2 additions & 2 deletions src/common/teams-emojis.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ module.exports = {
ANGUISHED: '😧',
OPEN_MOUTH: '😮',
GRIMACING: '😬',
CRYING: '😢',
CRY: '😢',
SMILE: '😃',
SUNGLASSES: '😎',
FIRE: '&#x1F525',
HAMMER: '🔨',
HAMMER_AND_WRENCH: '🔨',
ROCKET: '🚀',
SKULL: '💀'
};
122 changes: 102 additions & 20 deletions src/webhooks/models/webhooksFormatter.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
const cloneDeep = require('lodash/cloneDeep');
const slackEmojis = require('slack-emojis');
const teamsEmojis = require('../../common/teams-emojis');
const emojiHandler = require('../../common/emojiHandler');

const {
EVENT_FORMAT_TYPES,
EVENT_FORMAT_TYPE_JSON,
EVENT_FORMAT_TYPE_SLACK,
EVENT_FORMAT_TYPE_TEAMS,
EVENT_FORMAT_TYPE_DISCORD,
WEBHOOK_GRAVATAR_URL,
WEBHOOK_EVENT_TYPES,
WEBHOOK_EVENT_TYPE_STARTED,
WEBHOOK_EVENT_TYPE_FINISHED,
Expand All @@ -16,23 +17,28 @@ const {
WEBHOOK_EVENT_TYPE_BENCHMARK_FAILED,
WEBHOOK_EVENT_TYPE_BENCHMARK_PASSED,
WEBHOOK_SLACK_DEFAULT_MESSAGE_ICON,
WEBHOOK_SLACK_DEFAULT_REPORTER_NAME,
WEBHOOK_DEFAULT_REPORTER_NAME,
WEBHOOK_EVENT_TYPE_IN_PROGRESS,
WEBHOOK_TEAMS_DEFAULT_THEME_COLOR,
WEBHOOK_TEST_MESSAGE
} = require('../../common/consts');
const statsFormatter = require('./statsFormatter');

function getGravatarUrlWithIconSize(size) {
return `${WEBHOOK_GRAVATAR_URL}?s=${size}`;
}

function unknownWebhookEventTypeError(badWebhookEventTypeValue) {
return new Error(`Unrecognized webhook event: ${badWebhookEventTypeValue}, must be one of the following: ${WEBHOOK_EVENT_TYPES.join(', ')}`);
}

function getThresholdMessage(state, { isSlack, testName, benchmarkThreshold, lastScores, aggregatedReport, score }) {
function getThresholdMessage(state, { emoji, testName, benchmarkThreshold, lastScores, aggregatedReport, score }) {
let resultText = 'above';
let icon = isSlack ? slackEmojis.ROCKET : teamsEmojis.ROCKET;
let icon = null;
icon = emoji.ROCKET;
if (state === WEBHOOK_EVENT_TYPE_BENCHMARK_FAILED) {
resultText = 'below';
icon = isSlack ? slackEmojis.CRY : teamsEmojis.CRYING;
icon = emoji.CRY;
}
return `${icon} *Test ${testName} got a score of ${score.toFixed(1)}` +
` this is ${resultText} the threshold of ${benchmarkThreshold}. ${lastScores.length > 0 ? `last 3 scores are: ${lastScores.join()}` : 'no last score to show'}` +
Expand All @@ -43,14 +49,22 @@ function slackWebhookFormat(message, options = {}) {
return {
text: message,
icon_emoji: options.icon || WEBHOOK_SLACK_DEFAULT_MESSAGE_ICON,
username: WEBHOOK_SLACK_DEFAULT_REPORTER_NAME
username: WEBHOOK_DEFAULT_REPORTER_NAME
};
}

function teamsWebhookFormat(message) {
return {
themeColor: WEBHOOK_TEAMS_DEFAULT_THEME_COLOR,
text: message.replace(/\n/g, " \n")
text: message.replace(/\n/g, ' \n')
};
}

function discordWebhookFormat(message) {
return {
content: message,
username: WEBHOOK_DEFAULT_REPORTER_NAME,
avatar_url: getGravatarUrlWithIconSize(128)
};
}

Expand Down Expand Up @@ -79,6 +93,7 @@ function slack(event, testId, jobId, report, additionalInfo, options) {
grafana_report: grafanaReport
} = report;
const { score, aggregatedReport, reportBenchmark, benchmarkThreshold, lastScores, stats } = additionalInfo;
const emoji = emojiHandler(EVENT_FORMAT_TYPE_SLACK);
switch (event) {
case WEBHOOK_EVENT_TYPE_STARTED: {
const rampToMessage = `, ramp to: ${rampTo} scenarios per second`;
Expand Down Expand Up @@ -109,16 +124,15 @@ function slack(event, testId, jobId, report, additionalInfo, options) {
}
case WEBHOOK_EVENT_TYPE_BENCHMARK_FAILED:
case WEBHOOK_EVENT_TYPE_BENCHMARK_PASSED: {
let isSlack = true;
message = getThresholdMessage(event, { isSlack, testName, lastScores, aggregatedReport, benchmarkThreshold, score });
message = getThresholdMessage(event, { emoji, testName, lastScores, aggregatedReport, benchmarkThreshold, score });
break;
}
case WEBHOOK_EVENT_TYPE_IN_PROGRESS: {
message = `${slackEmojis.HAMMER_AND_WRENCH} *Test ${testName} with id: ${testId} is in progress!*`;
message = `${emoji.HAMMER_AND_WRENCH} *Test ${testName} with id: ${testId} is in progress!*`;
break;
}
case WEBHOOK_EVENT_TYPE_API_FAILURE: {
message = `${slackEmojis.BOOM} *Test ${testName} with id: ${testId} has encountered an API failure!* ${slackEmojis.SKULL}`;
message = `${emoji.BOOM} *Test ${testName} with id: ${testId} has encountered an API failure!* ${emoji.SKULL}`;
break;
}
default: {
Expand All @@ -141,47 +155,47 @@ function teams(event, testId, jobId, report, additionalInfo, options) {
grafana_report: grafanaReport
} = report;
const { score, aggregatedReport, reportBenchmark, benchmarkThreshold, lastScores, stats } = additionalInfo;
const emoji = emojiHandler(EVENT_FORMAT_TYPE_TEAMS);
switch (event) {
case WEBHOOK_EVENT_TYPE_STARTED: {
const rampToMessage = `, ramp to: ${rampTo} scenarios per second`;
let requestRateMessage = arrivalRate ? `arrival rate: ${arrivalRate} scenarios per second` : `arrival count: ${arrivalCount} scenarios`;
requestRateMessage = rampTo ? requestRateMessage + rampToMessage : requestRateMessage;

message = `${teamsEmojis.SMILE} *Test ${testName} with id: ${testId} has started*.`;
message = `${emoji.SMILE} *Test ${testName} with id: ${testId} has started*.`;
message += `\n*test configuration:* environment: ${environment} duration: ${duration} seconds, ${requestRateMessage}, number of runners: ${parallelism}`;
break;
}
case WEBHOOK_EVENT_TYPE_FINISHED: {
message = `${teamsEmojis.SUNGLASSES} *Test ${testName} with id: ${testId} is finished.*`;
message = `${emoji.SUNGLASSES} *Test ${testName} with id: ${testId} is finished.*`;
message += `\n${statsFormatter.getStatsFormatted('aggregate', aggregatedReport, reportBenchmark)}`;
if (grafanaReport) {
message += `\n<${grafanaReport} | View final grafana dashboard report>`;
}
break;
}
case WEBHOOK_EVENT_TYPE_FAILED: {
message = `${teamsEmojis.ANGUISHED} *Test with id: ${testId} Failed*.`;
message = `${emoji.ANGUISHED} *Test with id: ${testId} Failed*.`;
message += `\ntest configuration:\n
environment: ${environment}\n
${stats.data}`;
break;
}
case WEBHOOK_EVENT_TYPE_ABORTED: {
message = `${teamsEmojis.ANGUISHED} *Test ${testName} with id: ${testId} was aborted.*`;
message = `${emoji.ANGUISHED} *Test ${testName} with id: ${testId} was aborted.*`;
break;
}
case WEBHOOK_EVENT_TYPE_BENCHMARK_FAILED:
case WEBHOOK_EVENT_TYPE_BENCHMARK_PASSED: {
let isSlack = false;
message = getThresholdMessage(event, { isSlack, testName, lastScores, aggregatedReport, benchmarkThreshold, score });
message = getThresholdMessage(event, { emoji, testName, lastScores, aggregatedReport, benchmarkThreshold, score });
break;
}
case WEBHOOK_EVENT_TYPE_IN_PROGRESS: {
message = `${teamsEmojis.HAMMER} *Test ${testName} with id: ${testId} is in progress!*`;
message = `${emoji.HAMMER_AND_WRENCH} *Test ${testName} with id: ${testId} is in progress!*`;
break;
}
case WEBHOOK_EVENT_TYPE_API_FAILURE: {
message = `${teamsEmojis.FIRE} *Test ${testName} with id: ${testId} has encountered an API failure!* ${teamsEmojis.SKULL}`;
message = `${emoji.FIRE} *Test ${testName} with id: ${testId} has encountered an API failure!* ${emoji.SKULL}`;
break;
}
default: {
Expand All @@ -191,6 +205,68 @@ function teams(event, testId, jobId, report, additionalInfo, options) {
return teamsWebhookFormat(message);
}

function discord(event, testId, jobId, report, additionalInfo, options) {
let message = null;
const {
environment,
duration,
parallelism = 1,
ramp_to: rampTo,
arrival_rate: arrivalRate,
arrival_count: arrivalCount,
test_name: testName,
grafana_report: grafanaReport
} = report;
const { score, aggregatedReport, reportBenchmark, benchmarkThreshold, lastScores, stats } = additionalInfo;
const emoji = emojiHandler(EVENT_FORMAT_TYPE_DISCORD);
switch (event) {
case WEBHOOK_EVENT_TYPE_STARTED: {
const rampToMessage = `, ramp to: ${rampTo} scenarios per second`;
let requestRateMessage = arrivalRate ? `arrival rate: ${arrivalRate} scenarios per second` : `arrival count: ${arrivalCount} scenarios`;
requestRateMessage = rampTo ? requestRateMessage + rampToMessage : requestRateMessage;

message = `🤓 *Test ${testName} with id: ${testId} has started*.\n
*test configuration:* environment: ${environment} duration: ${duration} seconds, ${requestRateMessage}, number of runners: ${parallelism}`;
break;
}
case WEBHOOK_EVENT_TYPE_FINISHED: {
message = `😎 *Test ${testName} with id: ${testId} is finished.*\n ${statsFormatter.getStatsFormatted('aggregate', aggregatedReport, reportBenchmark)}\n`;
if (grafanaReport) {
message += `<${grafanaReport} | View final grafana dashboard report>`;
}
break;
}
case WEBHOOK_EVENT_TYPE_FAILED: {
message = `😞 *Test with id: ${testId} Failed*.\n
test configuration:\n
environment: ${environment}\n
${stats.data}`;
break;
}
case WEBHOOK_EVENT_TYPE_ABORTED: {
message = `😢 *Test ${testName} with id: ${testId} was aborted.*\n`;
break;
}
case WEBHOOK_EVENT_TYPE_BENCHMARK_FAILED:
case WEBHOOK_EVENT_TYPE_BENCHMARK_PASSED: {
message = getThresholdMessage(event, { emoji, testName, lastScores, aggregatedReport, benchmarkThreshold, score });
break;
}
case WEBHOOK_EVENT_TYPE_IN_PROGRESS: {
message = `${emoji.HAMMER_AND_WRENCH} *Test ${testName} with id: ${testId} is in progress!*`;
break;
}
case WEBHOOK_EVENT_TYPE_API_FAILURE: {
message = `${emoji.BOOM} *Test ${testName} with id: ${testId} has encountered an API failure!* ${emoji.SKULL}`;
break;
}
default: {
throw unknownWebhookEventTypeError();
}
}
return discordWebhookFormat(message);
}

module.exports.format = function(format, eventType, jobId, testId, report, additionalInfo = {}, options = {}) {
if (!WEBHOOK_EVENT_TYPES.includes(eventType)) {
throw unknownWebhookEventTypeError(eventType);
Expand All @@ -205,6 +281,9 @@ module.exports.format = function(format, eventType, jobId, testId, report, addit
case EVENT_FORMAT_TYPE_TEAMS: {
return teams(eventType, testId, jobId, report, additionalInfo, options);
}
case EVENT_FORMAT_TYPE_DISCORD:{
return discord(eventType, testId, jobId, report, additionalInfo, options);
}
default: {
throw new Error(`Unrecognized webhook format: ${format}, available options: ${EVENT_FORMAT_TYPES.join()}`);
}
Expand All @@ -223,6 +302,9 @@ module.exports.formatSimpleMessage = function(format) {
case EVENT_FORMAT_TYPE_TEAMS: {
return teamsWebhookFormat(simpleMessage);
}
case EVENT_FORMAT_TYPE_DISCORD: {
return discordWebhookFormat(simpleMessage);
}
default: {
throw new Error(`Unrecognized webhook format: ${format}, available options: ${EVENT_FORMAT_TYPES.join()}`);
}
Expand Down
Loading