diff --git a/x-pack/package.json b/x-pack/package.json
index 645ad7bdee55..b652849340d5 100644
--- a/x-pack/package.json
+++ b/x-pack/package.json
@@ -53,6 +53,7 @@
"jest-cli": "^22.4.3",
"jest-styled-components": "^5.0.1",
"mocha": "^5.0.5",
+ "mustache": "^2.3.0",
"node-fetch": "^2.1.2",
"pdf-image": "1.1.0",
"pixelmatch": "4.0.2",
diff --git a/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/__test__/List.test.js b/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/__test__/List.test.js
index 3c3496384392..b4ab336f378b 100644
--- a/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/__test__/List.test.js
+++ b/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/__test__/List.test.js
@@ -25,7 +25,7 @@ describe('ErrorGroupOverview -> List', () => {
const storeState = {};
const wrapper = mount(
-
+
,
storeState
);
diff --git a/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/Watcher/WatcherFlyOut.js b/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/Watcher/WatcherFlyOut.js
index 5463eb892f7c..ec5b0595b965 100644
--- a/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/Watcher/WatcherFlyOut.js
+++ b/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/Watcher/WatcherFlyOut.js
@@ -149,8 +149,14 @@ export default class WatcherFlyout extends Component {
const timeRange =
this.state.schedule === 'interval'
- ? `now-${this.state.interval.value}${this.state.interval.unit}`
- : 'now-24h';
+ ? {
+ value: this.state.interval.value,
+ unit: this.state.interval.unit
+ }
+ : {
+ value: 24,
+ unit: 'h'
+ };
return createErrorGroupWatch({
emails,
diff --git a/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/Watcher/__test__/__snapshots__/createErrorGroupWatch.test.js.snap b/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/Watcher/__test__/__snapshots__/createErrorGroupWatch.test.js.snap
index b5d265804eb8..9a9e7964d5cc 100644
--- a/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/Watcher/__test__/__snapshots__/createErrorGroupWatch.test.js.snap
+++ b/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/Watcher/__test__/__snapshots__/createErrorGroupWatch.test.js.snap
@@ -1,31 +1,53 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`createErrorGroupWatch should call createWatch with correct args 1`] = `
+exports[`createErrorGroupWatch should format email correctly 1`] = `
+"Your service \\"opbeans-node\\" has error groups which exceeds 10 occurrences within \\"24h\\"
+
+
+this is a string
+N/A
+7761 occurrences
+
+foo
+ (server/coffee.js)
+7752 occurrences
+
+socket hang up
+createHangUpError (_http_client.js)
+3887 occurrences
+
+this will not get captured by express
+ (server/coffee.js)
+3886 occurrences
+"
+`;
+
+exports[`createErrorGroupWatch should format entire template correctly 1`] = `
Object {
"actions": Object {
"email": Object {
"email": Object {
"body": Object {
- "html": "Your service \\"{{ctx.metadata.serviceName}}\\" has error groups which exceeds {{ctx.metadata.threshold}} occurrences within \\"{{ctx.metadata.timeRangeHumanReadable}}\\"
{{#ctx.payload.aggregations.error_groups.buckets}}
{{sample.hits.hits.0._source.error.log.message}}{{^sample.hits.hits.0._source.error.log.message}}{{sample.hits.hits.0._source.error.exception.message}}{{/sample.hits.hits.0._source.error.log.message}}
{{sample.hits.hits.0._source.error.culprit}}{{^sample.hits.hits.0._source.error.culprit}}N/A{{/sample.hits.hits.0._source.error.culprit}}
{{doc_count}} occurrences
{{/ctx.payload.aggregations.error_groups.buckets}}",
+ "html": "Your service \\"opbeans-node\\" has error groups which exceeds 10 occurrences within \\"24h\\"
this is a string
N/A
7761 occurrences
foo
(server/coffee.js)
7752 occurrences
socket hang up
createHangUpError (_http_client.js)
3887 occurrences
this will not get captured by express
(server/coffee.js)
3886 occurrences
",
},
- "subject": "\\"{{ctx.metadata.serviceName}}\\" has error groups which exceeds the threshold",
- "to": "{{ctx.metadata.emails}}",
+ "subject": "\\"opbeans-node\\" has error groups which exceeds the threshold",
+ "to": "my@email.dk",
},
},
"log_error": Object {
"logging": Object {
- "text": "Your service \\"{{ctx.metadata.serviceName}}\\" has error groups which exceeds {{ctx.metadata.threshold}} occurrences within \\"{{ctx.metadata.timeRangeHumanReadable}}\\"
{{#ctx.payload.aggregations.error_groups.buckets}}
{{sample.hits.hits.0._source.error.log.message}}{{^sample.hits.hits.0._source.error.log.message}}{{sample.hits.hits.0._source.error.exception.message}}{{/sample.hits.hits.0._source.error.log.message}}
{{sample.hits.hits.0._source.error.culprit}}{{^sample.hits.hits.0._source.error.culprit}}N/A{{/sample.hits.hits.0._source.error.culprit}}
{{doc_count}} occurrences
{{/ctx.payload.aggregations.error_groups.buckets}}",
+ "text": "Your service \\"opbeans-node\\" has error groups which exceeds 10 occurrences within \\"24h\\"
this is a string
N/A
7761 occurrences
foo
(server/coffee.js)
7752 occurrences
socket hang up
createHangUpError (_http_client.js)
3887 occurrences
this will not get captured by express
(server/coffee.js)
3886 occurrences
",
},
},
"slack_webhook": Object {
"webhook": Object {
- "body": "{\\"text\\":\\"Your service \\\\\\"{{ctx.metadata.serviceName}}\\\\\\" has error groups which exceeds {{ctx.metadata.threshold}} occurrences within \\\\\\"{{ctx.metadata.timeRangeHumanReadable}}\\\\\\"\\\\n{{#ctx.payload.aggregations.error_groups.buckets}}\\\\n>*{{sample.hits.hits.0._source.error.log.message}}{{^sample.hits.hits.0._source.error.log.message}}{{sample.hits.hits.0._source.error.exception.message}}{{/sample.hits.hits.0._source.error.log.message}}*\\\\n>{{#sample.hits.hits.0._source.error.culprit}}\`{{sample.hits.hits.0._source.error.culprit}}\`{{/sample.hits.hits.0._source.error.culprit}}{{^sample.hits.hits.0._source.error.culprit}}N/A{{/sample.hits.hits.0._source.error.culprit}}\\\\n>{{doc_count}} occurrences\\\\n{{/ctx.payload.aggregations.error_groups.buckets}}\\"}",
+ "body": "{\\"text\\":\\"Your service \\\\\\"opbeans-node\\\\\\" has error groups which exceeds 10 occurrences within \\\\\\"24h\\\\\\"\\\\n\\\\n>*this is a string*\\\\n>N/A\\\\n>7761 occurrences\\\\n\\\\n>*foo*\\\\n>\` (server/coffee.js)\`\\\\n>7752 occurrences\\\\n\\\\n>*socket hang up*\\\\n>\`createHangUpError (_http_client.js)\`\\\\n>3887 occurrences\\\\n\\\\n>*this will not get captured by express*\\\\n>\` (server/coffee.js)\`\\\\n>3886 occurrences\\\\n\\"}",
"headers": Object {
"Content-Type": "application/json",
},
"host": "hooks.slack.com",
"method": "POST",
- "path": "{{ctx.metadata.slackUrlPath}}",
+ "path": "/services/slackid1/slackid2/slackid3",
"port": 443,
"scheme": "https",
},
@@ -64,7 +86,7 @@ Object {
},
"terms": Object {
"field": "error.grouping_key",
- "min_doc_count": "{{ctx.metadata.threshold}}",
+ "min_doc_count": "10",
"order": Object {
"_count": "desc",
},
@@ -77,7 +99,7 @@ Object {
"filter": Array [
Object {
"term": Object {
- "context.service.name": "{{ctx.metadata.serviceName}}",
+ "context.service.name": "opbeans-node",
},
},
Object {
@@ -88,7 +110,7 @@ Object {
Object {
"range": Object {
"@timestamp": Object {
- "gte": "{{ctx.metadata.timeRange}}",
+ "gte": "now-24h",
},
},
},
@@ -110,8 +132,8 @@ Object {
"serviceName": "opbeans-node",
"slackUrlPath": "/services/slackid1/slackid2/slackid3",
"threshold": 10,
- "timeRange": "now-24h",
- "timeRangeHumanReadable": "24h",
+ "timeRangeUnit": "h",
+ "timeRangeValue": 24,
"trigger": "This value must be changed in trigger section",
},
"trigger": Object {
@@ -125,10 +147,22 @@ Object {
`;
exports[`createErrorGroupWatch should format slack message correctly 1`] = `
-"Your service \\"{{ctx.metadata.serviceName}}\\" has error groups which exceeds {{ctx.metadata.threshold}} occurrences within \\"{{ctx.metadata.timeRangeHumanReadable}}\\"
-{{#ctx.payload.aggregations.error_groups.buckets}}
->*{{sample.hits.hits.0._source.error.log.message}}{{^sample.hits.hits.0._source.error.log.message}}{{sample.hits.hits.0._source.error.exception.message}}{{/sample.hits.hits.0._source.error.log.message}}*
->{{#sample.hits.hits.0._source.error.culprit}}\`{{sample.hits.hits.0._source.error.culprit}}\`{{/sample.hits.hits.0._source.error.culprit}}{{^sample.hits.hits.0._source.error.culprit}}N/A{{/sample.hits.hits.0._source.error.culprit}}
->{{doc_count}} occurrences
-{{/ctx.payload.aggregations.error_groups.buckets}}"
+"Your service \\"opbeans-node\\" has error groups which exceeds 10 occurrences within \\"24h\\"
+
+>*this is a string*
+>N/A
+>7761 occurrences
+
+>*foo*
+>\` (server/coffee.js)\`
+>7752 occurrences
+
+>*socket hang up*
+>\`createHangUpError (_http_client.js)\`
+>3887 occurrences
+
+>*this will not get captured by express*
+>\` (server/coffee.js)\`
+>3886 occurrences
+"
`;
diff --git a/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/Watcher/__test__/createErrorGroupWatch.test.js b/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/Watcher/__test__/createErrorGroupWatch.test.js
index 8cfecbf0d988..57a1c59e9233 100644
--- a/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/Watcher/__test__/createErrorGroupWatch.test.js
+++ b/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/Watcher/__test__/createErrorGroupWatch.test.js
@@ -5,16 +5,27 @@
*/
import { createErrorGroupWatch } from '../createErrorGroupWatch';
+import mustache from 'mustache';
import chrome from 'ui/chrome';
import * as rest from '../../../../../services/rest';
+import { isObject, isArray, isString } from 'lodash';
+import esResponse from './esResponse.json';
+
+jest.mock('uuid', () => ({
+ v4: jest.fn(() => 'mocked-uuid')
+}));
+
+// disable html escaping since this is also disabled in watcher\s mustache implementation
+mustache.escape = value => value;
describe('createErrorGroupWatch', () => {
- let res;
+ let createWatchResponse;
+ let tmpl;
beforeEach(async () => {
chrome.getInjected = jest.fn().mockReturnValue('myIndexPattern');
jest.spyOn(rest, 'createWatch').mockReturnValue();
- res = await createErrorGroupWatch({
+ createWatchResponse = await createErrorGroupWatch({
emails: ['my@email.dk'],
schedule: {
daily: {
@@ -24,27 +35,70 @@ describe('createErrorGroupWatch', () => {
serviceName: 'opbeans-node',
slackUrl: 'https://hooks.slack.com/services/slackid1/slackid2/slackid3',
threshold: 10,
- timeRange: 'now-24h'
+ timeRange: { value: 24, unit: 'h' }
});
+
+ const watchBody = rest.createWatch.mock.calls[0][1];
+ const templateCtx = {
+ payload: esResponse,
+ metadata: watchBody.metadata
+ };
+
+ tmpl = renderMustache(rest.createWatch.mock.calls[0][1], templateCtx);
});
afterEach(() => jest.restoreAllMocks());
it('should call createWatch with correct args', () => {
- expect(rest.createWatch.mock.calls[0][0]).toContain('apm-');
- expect(rest.createWatch.mock.calls[0][1]).toMatchSnapshot();
+ expect(rest.createWatch.mock.calls[0][0]).toBe('apm-mocked-uuid');
});
it('should format slack message correctly', () => {
+ expect(tmpl.actions.slack_webhook.webhook.path).toBe(
+ '/services/slackid1/slackid2/slackid3'
+ );
expect(
- JSON.parse(
- rest.createWatch.mock.calls[0][1].actions.slack_webhook.webhook.body
- ).text
+ JSON.parse(tmpl.actions.slack_webhook.webhook.body).text
).toMatchSnapshot();
});
+ it('should format email correctly', () => {
+ expect(tmpl.actions.email.email.to).toBe('my@email.dk');
+ expect(tmpl.actions.email.email.subject).toBe(
+ '"opbeans-node" has error groups which exceeds the threshold'
+ );
+ expect(
+ tmpl.actions.email.email.body.html.replace(/
/g, '\n')
+ ).toMatchSnapshot();
+ });
+
+ it('should format entire template correctly', () => {
+ expect(tmpl).toMatchSnapshot();
+ });
+
it('should return watch id', async () => {
const id = rest.createWatch.mock.calls[0][0];
- expect(res).toEqual(id);
+ expect(createWatchResponse).toEqual(id);
});
});
+
+// Recusively iterate a nested structure and render strings as mustache templates
+function renderMustache(input, ctx) {
+ if (isString(input)) {
+ return mustache.render(input, { ctx });
+ }
+
+ if (isArray(input)) {
+ return input.map(itemValue => renderMustache(itemValue, ctx));
+ }
+
+ if (isObject(input)) {
+ return Object.keys(input).reduce((acc, key) => {
+ const value = input[key];
+
+ return { ...acc, [key]: renderMustache(value, ctx) };
+ }, {});
+ }
+
+ return input;
+}
diff --git a/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/Watcher/__test__/esResponse.json b/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/Watcher/__test__/esResponse.json
new file mode 100644
index 000000000000..b2aa6c545b16
--- /dev/null
+++ b/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/Watcher/__test__/esResponse.json
@@ -0,0 +1,141 @@
+{
+ "took": 454,
+ "timed_out": false,
+ "_shards": {
+ "total": 10,
+ "successful": 10,
+ "skipped": 0,
+ "failed": 0
+ },
+ "hits": {
+ "total": 23287,
+ "max_score": 0,
+ "hits": []
+ },
+ "aggregations": {
+ "error_groups": {
+ "doc_count_error_upper_bound": 0,
+ "sum_other_doc_count": 0,
+ "buckets": [
+ {
+ "key": "63925d00b445cdf4b532dd09d185f5c6",
+ "doc_count": 7761,
+ "sample": {
+ "hits": {
+ "total": 7761,
+ "max_score": null,
+ "hits": [
+ {
+ "_index": "apm-7.0.0-alpha1-error-2018.04.25",
+ "_type": "doc",
+ "_id": "qH7C_WIBcmGuKeCHJvvT",
+ "_score": null,
+ "_source": {
+ "@timestamp": "2018-04-25T17:03:02.296Z",
+ "error": {
+ "log": {
+ "message": "this is a string"
+ },
+ "grouping_key": "63925d00b445cdf4b532dd09d185f5c6"
+ }
+ },
+ "sort": [1524675782296]
+ }
+ ]
+ }
+ }
+ },
+ {
+ "key": "89bb1a1f644c7f4bbe8d1781b5cb5fd5",
+ "doc_count": 7752,
+ "sample": {
+ "hits": {
+ "total": 7752,
+ "max_score": null,
+ "hits": [
+ {
+ "_index": "apm-7.0.0-alpha1-error-2018.04.25",
+ "_type": "doc",
+ "_id": "_3_D_WIBcmGuKeCHFwOW",
+ "_score": null,
+ "_source": {
+ "@timestamp": "2018-04-25T17:04:03.504Z",
+ "error": {
+ "exception": {
+ "handled": true,
+ "message": "foo"
+ },
+ "culprit": " (server/coffee.js)",
+ "grouping_key": "89bb1a1f644c7f4bbe8d1781b5cb5fd5"
+ }
+ },
+ "sort": [1524675843504]
+ }
+ ]
+ }
+ }
+ },
+ {
+ "key": "7a17ea60604e3531bd8de58645b8631f",
+ "doc_count": 3887,
+ "sample": {
+ "hits": {
+ "total": 3887,
+ "max_score": null,
+ "hits": [
+ {
+ "_index": "apm-7.0.0-alpha1-error-2018.04.25",
+ "_type": "doc",
+ "_id": "dn_D_WIBcmGuKeCHQgXJ",
+ "_score": null,
+ "_source": {
+ "@timestamp": "2018-04-25T17:04:14.575Z",
+ "error": {
+ "exception": {
+ "handled": false,
+ "message": "socket hang up"
+ },
+ "culprit": "createHangUpError (_http_client.js)",
+ "grouping_key": "7a17ea60604e3531bd8de58645b8631f"
+ }
+ },
+ "sort": [1524675854575]
+ }
+ ]
+ }
+ }
+ },
+ {
+ "key": "b9e1027f29c221763f864f6fa2ad9f5e",
+ "doc_count": 3886,
+ "sample": {
+ "hits": {
+ "total": 3886,
+ "max_score": null,
+ "hits": [
+ {
+ "_index": "apm-7.0.0-alpha1-error-2018.04.25",
+ "_type": "doc",
+ "_id": "dX_D_WIBcmGuKeCHQgXJ",
+ "_score": null,
+ "_source": {
+ "@timestamp": "2018-04-25T17:04:14.533Z",
+ "error": {
+ "exception": {
+ "handled": false,
+ "message": "this will not get captured by express"
+ },
+ "culprit": " (server/coffee.js)",
+ "grouping_key": "b9e1027f29c221763f864f6fa2ad9f5e"
+ }
+ },
+ "sort": [1524675854533]
+ }
+ ]
+ }
+ }
+ }
+ ]
+ }
+ }
+}
diff --git a/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/Watcher/createErrorGroupWatch.js b/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/Watcher/createErrorGroupWatch.js
index 019fa767030c..3922b26f83ed 100644
--- a/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/Watcher/createErrorGroupWatch.js
+++ b/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/Watcher/createErrorGroupWatch.js
@@ -38,7 +38,7 @@ export async function createErrorGroupWatch({
const apmIndexPattern = chrome.getInjected('apmIndexPattern');
const slackUrlPath = getSlackPathUrl(slackUrl);
- const emailTemplate = `Your service "{{ctx.metadata.serviceName}}" has error groups which exceeds {{ctx.metadata.threshold}} occurrences within "{{ctx.metadata.timeRangeHumanReadable}}"
+ const emailTemplate = `Your service "{{ctx.metadata.serviceName}}" has error groups which exceeds {{ctx.metadata.threshold}} occurrences within "{{ctx.metadata.timeRangeValue}}{{ctx.metadata.timeRangeUnit}}"
{{#ctx.payload.aggregations.error_groups.buckets}}
{{sample.hits.hits.0._source.error.log.message}}{{^sample.hits.hits.0._source.error.log.message}}{{sample.hits.hits.0._source.error.exception.message}}{{/sample.hits.hits.0._source.error.log.message}}
@@ -46,7 +46,7 @@ export async function createErrorGroupWatch({
{{doc_count}} occurrences
{{/ctx.payload.aggregations.error_groups.buckets}}`.replace(/\n/g, '
');
- const slackTemplate = `Your service "{{ctx.metadata.serviceName}}" has error groups which exceeds {{ctx.metadata.threshold}} occurrences within "{{ctx.metadata.timeRangeHumanReadable}}"
+ const slackTemplate = `Your service "{{ctx.metadata.serviceName}}" has error groups which exceeds {{ctx.metadata.threshold}} occurrences within "{{ctx.metadata.timeRangeValue}}{{ctx.metadata.timeRangeUnit}}"
{{#ctx.payload.aggregations.error_groups.buckets}}
>*{{sample.hits.hits.0._source.error.log.message}}{{^sample.hits.hits.0._source.error.log.message}}{{sample.hits.hits.0._source.error.exception.message}}{{/sample.hits.hits.0._source.error.log.message}}*
>{{#sample.hits.hits.0._source.error.culprit}}\`{{sample.hits.hits.0._source.error.culprit}}\`{{/sample.hits.hits.0._source.error.culprit}}{{^sample.hits.hits.0._source.error.culprit}}N/A{{/sample.hits.hits.0._source.error.culprit}}
@@ -59,8 +59,8 @@ export async function createErrorGroupWatch({
trigger: 'This value must be changed in trigger section',
serviceName,
threshold,
- timeRange,
- timeRangeHumanReadable: timeRange.replace('now-', ''),
+ timeRangeValue: timeRange.value,
+ timeRangeUnit: timeRange.unit,
slackUrlPath
},
trigger: {
@@ -80,7 +80,8 @@ export async function createErrorGroupWatch({
{
range: {
'@timestamp': {
- gte: '{{ctx.metadata.timeRange}}'
+ gte:
+ 'now-{{ctx.metadata.timeRangeValue}}{{ctx.metadata.timeRangeUnit}}'
}
}
}
diff --git a/x-pack/yarn.lock b/x-pack/yarn.lock
index 6a76f5d7f6b0..b33007ceb65d 100644
--- a/x-pack/yarn.lock
+++ b/x-pack/yarn.lock
@@ -5175,6 +5175,10 @@ multipipe@^0.1.2:
dependencies:
duplexer2 "0.0.2"
+mustache@^2.3.0:
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/mustache/-/mustache-2.3.0.tgz#4028f7778b17708a489930a6e52ac3bca0da41d0"
+
mute-stream@0.0.6:
version "0.0.6"
resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.6.tgz#48962b19e169fd1dfc240b3f1e7317627bbc47db"