Skip to content

Commit

Permalink
feat(webhooks): Webhooks discord (#536)
Browse files Browse the repository at this point in the history
  • Loading branch information
syncush authored Nov 1, 2020
1 parent be4143d commit 9bae29a
Show file tree
Hide file tree
Showing 13 changed files with 556 additions and 251 deletions.
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

0 comments on commit 9bae29a

Please sign in to comment.