1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-02-28 00:17:12 +01:00

feat: Datadog integration (#820)

fixes: #815 

Co-authored-by: Ivar Conradi Østhus <ivarconr@gmail.com>
This commit is contained in:
R Ashwin 2021-05-04 01:38:14 +05:30 committed by GitHub
parent 0bed8f605e
commit d61c7242d8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 296 additions and 5 deletions

38
docs/addons/datadog.md Normal file
View File

@ -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.

View File

@ -30,4 +30,4 @@ Unleash Microsoft Teams addon takes the following parameters.
#### Tags #### 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.

View File

@ -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"}'

Binary file not shown.

View File

@ -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',
},
],
};

114
src/lib/addons/datadog.js Normal file
View File

@ -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;

View File

@ -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);
});

View File

@ -1,7 +1,8 @@
const webhook = require('./webhook'); const webhook = require('./webhook');
const slackAddon = require('./slack'); const slackAddon = require('./slack');
const teamsAddon = require('./teams'); const teamsAddon = require('./teams');
const datadogAddon = require('./datadog');
const addons = [webhook, slackAddon, teamsAddon]; const addons = [webhook, slackAddon, teamsAddon, datadogAddon];
module.exports = addons; module.exports = addons;

View File

@ -76,8 +76,10 @@ class TeamsAddon extends Addon {
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body), body: JSON.stringify(body),
}; };
const result = await this.fetchRetry(url, requestOpts); const res = await this.fetchRetry(url, requestOpts);
this.logger.info(`Handled event ${event.type}. Status codes=${result}`); this.logger.info(
`Handled event ${event.type}. Status codes=${res.status}`,
);
} }
featureLink(event) { featureLink(event) {

View File

@ -29,7 +29,7 @@ test.serial('gets all addons', async t => {
.expect(200) .expect(200)
.expect(res => { .expect(res => {
t.is(res.body.addons.length, 0, 'expected 0 configured addons'); 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'); t.is(res.body.providers[0].name, 'webhook');
}); });
}); });