From d61c7242d8ca447f9cd147f71e06edacb3a9e54f Mon Sep 17 00:00:00 2001 From: R Ashwin Date: Tue, 4 May 2021 01:38:14 +0530 Subject: [PATCH] feat: Datadog integration (#820) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fixes: #815 Co-authored-by: Ivar Conradi Ă˜sthus --- docs/addons/datadog.md | 38 ++++++ docs/addons/teams.md | 2 +- snapshots/src/lib/addons/datadog.test.js.md | 17 +++ snapshots/src/lib/addons/datadog.test.js.snap | Bin 0 -> 378 bytes src/lib/addons/datadog-definition.js | 52 ++++++++ src/lib/addons/datadog.js | 114 ++++++++++++++++++ src/lib/addons/datadog.test.js | 67 ++++++++++ src/lib/addons/index.js | 3 +- src/lib/addons/teams.js | 6 +- src/test/e2e/api/admin/addon.e2e.test.js | 2 +- 10 files changed, 296 insertions(+), 5 deletions(-) create mode 100644 docs/addons/datadog.md create mode 100644 snapshots/src/lib/addons/datadog.test.js.md create mode 100644 snapshots/src/lib/addons/datadog.test.js.snap create mode 100644 src/lib/addons/datadog-definition.js create mode 100644 src/lib/addons/datadog.js create mode 100644 src/lib/addons/datadog.test.js diff --git a/docs/addons/datadog.md b/docs/addons/datadog.md new file mode 100644 index 0000000000..03f4c04eaa --- /dev/null +++ b/docs/addons/datadog.md @@ -0,0 +1,38 @@ +--- +id: datadog +title: Datadog +--- + +> This feature was introduced in \_Unleash v4.0.x. + +The Datadog addon allows Unleash to post Updates when a feature toggle is updated. To set up this addon, you need to set up a webhook connector for your channel. You can follow [Submitting events to Datadog](https://docs.datadoghq.com/api/latest/events/#post-an-event) on how to do that. + +The Datadog addon will perform a single retry if the HTTP POST against the Datadog Webhook URL fails (either a 50x or network error). Duplicate events may happen, and you should never assume events always comes in order. + +## Configuration + +#### Events + +You can choose to trigger updates for the following events (we might add more event types in the future): + +- feature-created +- feature-updated +- feature-archived +- feature-revived +- feature-stale-on +- feature-stale-off + +#### Parameters + +Unleash Datadog addon takes the following parameters. + +- **Datadog Events URL** - This property is optional. The default url is https://api.datadoghq.com/api/v1/events. Needs to be changed if you are not not on the US1 [Datadog site](https://docs.datadoghq.com/getting_started/site/). Possible alternatives: + - EU: https://app.datadoghq.eu/api/v1/events + - US1: https://app.datadoghq.com/api/v1/events + - US3: https://us3.datadoghq.com/api/v1/events + - US1-FED: https://app.ddog-gov.com/api/v1/events +- **DD API KEY** - This is a required property. + +#### Tags + +Datadog's incoming webhooks are app specific. You will be able to create multiple addons to support messaging on different apps. diff --git a/docs/addons/teams.md b/docs/addons/teams.md index e8527a286a..6a75668ef9 100644 --- a/docs/addons/teams.md +++ b/docs/addons/teams.md @@ -30,4 +30,4 @@ Unleash Microsoft Teams addon takes the following parameters. #### Tags -Microsoft teams's income webhooks are channel specific. You will be able to create multiple addons to support messaging on multiple channels. +Microsoft teams's incoming webhooks are channel specific. You will be able to create multiple addons to support messaging on multiple channels. diff --git a/snapshots/src/lib/addons/datadog.test.js.md b/snapshots/src/lib/addons/datadog.test.js.md new file mode 100644 index 0000000000..8844ec68a7 --- /dev/null +++ b/snapshots/src/lib/addons/datadog.test.js.md @@ -0,0 +1,17 @@ +# Snapshot report for `src/lib/addons/datadog.test.js` + +The actual snapshot is saved in `datadog.test.js.snap`. + +Generated by [AVA](https://avajs.dev). + +## Should call datadog webhook + +> Snapshot 1 + + '{"text":"%%% \\n some@user.com created feature toggle [some-toggle](http://some-url.com/#/features/strategies/some-toggle)\\n**Enabled**: no | **Type**: undefined | **Project**: undefined\\n**Activation strategies**: ```- name: default\\n``` \\n %%% ","title":"Unleash notification update"}' + +## Should call datadog webhook for archived toggle + +> Snapshot 1 + + '{"text":"%%% \\n The feature toggle *[some-toggle](http://some-url.com/#/archive/strategies/some-toggle)* was *archived* by some@user.com. \\n %%% ","title":"Unleash notification update"}' diff --git a/snapshots/src/lib/addons/datadog.test.js.snap b/snapshots/src/lib/addons/datadog.test.js.snap new file mode 100644 index 0000000000000000000000000000000000000000..da521e2b0916e334349828bdbae65c18ec27d72c GIT binary patch literal 378 zcmV-=0fqiSRzV?t-gU^&xluCU~sx`Zd>25zZTf|`OX?che9p{|5!2SSa4FoKo| zxkjM0-A)h=EXx}v^_7^Y8~FYJ%XMOf{<1%kWPV24)M!h4{E@BJ6z5w?G!u+C4?v}G z2Ao&>U9@SgB5X-zT3d8Sr-vBQ*oD<^h9rA}WNP01-NmvRjfMwGbT9xz7kNT8WiUIS zozpG5Bq>2N_MS>mWU=W_vL(Y|vAi3ZV($5vS9fgEt7?ps&hWojBXk(c9{%PGhhvce YAF(11&HnheRQ@~17Xov_tS$lo09n+nga7~l literal 0 HcmV?d00001 diff --git a/src/lib/addons/datadog-definition.js b/src/lib/addons/datadog-definition.js new file mode 100644 index 0000000000..9709fe38e8 --- /dev/null +++ b/src/lib/addons/datadog-definition.js @@ -0,0 +1,52 @@ +'use strict'; + +const { + FEATURE_CREATED, + FEATURE_UPDATED, + FEATURE_ARCHIVED, + FEATURE_REVIVED, + FEATURE_STALE_ON, + FEATURE_STALE_OFF, +} = require('../types/events'); + +module.exports = { + name: 'datadog', + displayName: 'Datadog', + description: 'Allows Unleash to post updates to Datadog.', + documentationUrl: 'https://docs.getunleash.io/docs/addons/datadog', + parameters: [ + { + name: 'url', + displayName: 'Datadog Events URL', + description: + 'Default url: https://api.datadoghq.com/api/v1/events. Needs to be changed if your not using the US1 site.', + type: 'url', + required: false, + }, + { + name: 'apiKey', + displayName: 'DD API KEY', + placeholder: 'j96c23b0f12a6b3434a8d710110bd862', + description: 'Api key from datadog', + type: 'text', + required: true, + sensitive: true, + }, + ], + events: [ + FEATURE_CREATED, + FEATURE_UPDATED, + FEATURE_ARCHIVED, + FEATURE_REVIVED, + FEATURE_STALE_ON, + FEATURE_STALE_OFF, + ], + tagTypes: [ + { + name: 'datadog', + description: + 'All Datadog tags added to a specific feature are sent to datadog event stream.', + icon: 'D', + }, + ], +}; diff --git a/src/lib/addons/datadog.js b/src/lib/addons/datadog.js new file mode 100644 index 0000000000..c7acc0c5d4 --- /dev/null +++ b/src/lib/addons/datadog.js @@ -0,0 +1,114 @@ +'use strict'; + +const YAML = require('js-yaml'); +const Addon = require('./addon'); + +const { + FEATURE_CREATED, + FEATURE_UPDATED, + FEATURE_ARCHIVED, + FEATURE_REVIVED, + FEATURE_STALE_ON, + FEATURE_STALE_OFF, +} = require('../types/events'); + +const definition = require('./datadog-definition'); + +class DatadogAddon extends Addon { + constructor(args) { + super(definition, args); + this.unleashUrl = args.unleashUrl; + } + + async handleEvent(event, parameters) { + const { + url = 'https://api.datadoghq.com/api/v1/events', + apiKey, + } = parameters; + let text; + + if ([FEATURE_ARCHIVED, FEATURE_REVIVED].includes(event.type)) { + text = this.generateArchivedText(event); + } else if ([FEATURE_STALE_ON, FEATURE_STALE_OFF].includes(event.type)) { + text = this.generateStaleText(event); + } else { + text = this.generateText(event); + } + + const { tags: eventTags } = event; + const tags = + eventTags && eventTags.map(tag => `${tag.value}:${tag.type}`); + const body = { + text: `%%% \n ${text} \n %%% `, + title: 'Unleash notification update', + tags, + }; + + const requestOpts = { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'DD-API-KEY': apiKey, + }, + body: JSON.stringify(body), + }; + const res = await this.fetchRetry(url, requestOpts); + this.logger.info( + `Handled event ${event.type}. Status codes=${res.status}`, + ); + } + + featureLink(event) { + const path = event.type === FEATURE_ARCHIVED ? 'archive' : 'features'; + return `${this.unleashUrl}/#/${path}/strategies/${event.data.name}`; + } + + generateStaleText(event) { + const { createdBy, data, type } = event; + const isStale = type === FEATURE_STALE_ON; + const feature = `[${data.name}](${this.featureLink(event)})`; + + if (isStale) { + return `The feature toggle *${feature}* is now *ready to be removed* from the code. +This was changed by ${createdBy}.`; + } + return `The feature toggle *${feature}* was *unmarked as stale* by ${createdBy}.`; + } + + generateArchivedText(event) { + const { createdBy, data, type } = event; + const action = type === FEATURE_ARCHIVED ? 'archived' : 'revived'; + const feature = `[${data.name}](${this.featureLink(event)})`; + return `The feature toggle *${feature}* was *${action}* by ${createdBy}.`; + } + + generateText(event) { + const { createdBy, data, type } = event; + const action = this.getAction(type); + const feature = `[${data.name}](${this.featureLink(event)})`; + const enabled = `**Enabled**: ${data.enabled ? 'yes' : 'no'}`; + const stale = data.stale ? '("stale")' : ''; + const typeStr = `**Type**: ${data.type}`; + const project = `**Project**: ${data.project}`; + const strategies = `**Activation strategies**: \`\`\`${YAML.safeDump( + data.strategies, + { skipInvalid: true }, + )}\`\`\``; + return `${createdBy} ${action} feature toggle ${feature} +${enabled}${stale} | ${typeStr} | ${project} +${strategies}`; + } + + getAction(type) { + switch (type) { + case FEATURE_CREATED: + return 'created'; + case FEATURE_UPDATED: + return 'updated'; + default: + return type; + } + } +} + +module.exports = DatadogAddon; diff --git a/src/lib/addons/datadog.test.js b/src/lib/addons/datadog.test.js new file mode 100644 index 0000000000..7dcb97738a --- /dev/null +++ b/src/lib/addons/datadog.test.js @@ -0,0 +1,67 @@ +const test = require('ava'); +const proxyquire = require('proxyquire').noCallThru(); +const { FEATURE_CREATED, FEATURE_ARCHIVED } = require('../types/events'); + +const DatadogAddon = proxyquire.load('./datadog', { + './addon': class Addon { + constructor(definition, { getLogger }) { + this.logger = getLogger('addon/test'); + this.fetchRetryCalls = []; + } + + async fetchRetry(url, options, retries, backoff) { + this.fetchRetryCalls.push({ url, options, retries, backoff }); + return Promise.resolve({ status: 200 }); + } + }, +}); + +const noLogger = require('../../test/fixtures/no-logger'); + +test('Should call datadog webhook', async t => { + const addon = new DatadogAddon({ + getLogger: noLogger, + unleashUrl: 'http://some-url.com', + }); + const event = { + type: FEATURE_CREATED, + createdBy: 'some@user.com', + data: { + name: 'some-toggle', + enabled: false, + strategies: [{ name: 'default' }], + }, + }; + + const parameters = { + url: 'http://api.datadoghq.com/api/v1/events', + }; + + await addon.handleEvent(event, parameters); + t.is(addon.fetchRetryCalls.length, 1); + t.is(addon.fetchRetryCalls[0].url, parameters.url); + t.snapshot(addon.fetchRetryCalls[0].options.body); +}); + +test('Should call datadog webhook for archived toggle', async t => { + const addon = new DatadogAddon({ + getLogger: noLogger, + unleashUrl: 'http://some-url.com', + }); + const event = { + type: FEATURE_ARCHIVED, + createdBy: 'some@user.com', + data: { + name: 'some-toggle', + }, + }; + + const parameters = { + url: 'http://api.datadoghq.com/api/v1/events', + }; + + await addon.handleEvent(event, parameters); + t.is(addon.fetchRetryCalls.length, 1); + t.is(addon.fetchRetryCalls[0].url, parameters.url); + t.snapshot(addon.fetchRetryCalls[0].options.body); +}); diff --git a/src/lib/addons/index.js b/src/lib/addons/index.js index 1f1d1035dc..4c839704ee 100644 --- a/src/lib/addons/index.js +++ b/src/lib/addons/index.js @@ -1,7 +1,8 @@ const webhook = require('./webhook'); const slackAddon = require('./slack'); const teamsAddon = require('./teams'); +const datadogAddon = require('./datadog'); -const addons = [webhook, slackAddon, teamsAddon]; +const addons = [webhook, slackAddon, teamsAddon, datadogAddon]; module.exports = addons; diff --git a/src/lib/addons/teams.js b/src/lib/addons/teams.js index f31d835097..13d1c07248 100644 --- a/src/lib/addons/teams.js +++ b/src/lib/addons/teams.js @@ -76,8 +76,10 @@ class TeamsAddon extends Addon { headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), }; - const result = await this.fetchRetry(url, requestOpts); - this.logger.info(`Handled event ${event.type}. Status codes=${result}`); + const res = await this.fetchRetry(url, requestOpts); + this.logger.info( + `Handled event ${event.type}. Status codes=${res.status}`, + ); } featureLink(event) { diff --git a/src/test/e2e/api/admin/addon.e2e.test.js b/src/test/e2e/api/admin/addon.e2e.test.js index 595a9b93bc..99c016c078 100644 --- a/src/test/e2e/api/admin/addon.e2e.test.js +++ b/src/test/e2e/api/admin/addon.e2e.test.js @@ -29,7 +29,7 @@ test.serial('gets all addons', async t => { .expect(200) .expect(res => { t.is(res.body.addons.length, 0, 'expected 0 configured addons'); - t.is(res.body.providers.length, 3, 'expected 3 addon providers'); + t.is(res.body.providers.length, 4, 'expected 4 addon providers'); t.is(res.body.providers[0].name, 'webhook'); }); });