From fdc24f5b3f607886b25350c29d803878261b2aac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Louv-Jansen?= Date: Thu, 26 Apr 2018 17:44:31 +0200 Subject: [PATCH] [APM] Minor watcher improvements (#18602) * [APM] Improve watcher tests * Update tests --- x-pack/package.json | 1 + .../List/__test__/List.test.js | 2 +- .../Watcher/WatcherFlyOut.js | 10 +- .../createErrorGroupWatch.test.js.snap | 70 ++++++--- .../__test__/createErrorGroupWatch.test.js | 72 +++++++-- .../Watcher/__test__/esResponse.json | 141 ++++++++++++++++++ .../Watcher/createErrorGroupWatch.js | 11 +- x-pack/yarn.lock | 4 + 8 files changed, 276 insertions(+), 35 deletions(-) create mode 100644 x-pack/plugins/apm/public/components/app/ErrorGroupOverview/Watcher/__test__/esResponse.json 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"