diff --git a/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/WatcherFlyout.tsx b/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/WatcherFlyout.tsx deleted file mode 100644 index 26cff5e71b610..0000000000000 --- a/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/WatcherFlyout.tsx +++ /dev/null @@ -1,635 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { - EuiButton, - EuiFieldNumber, - EuiFieldText, - EuiFlexGroup, - EuiFlexItem, - EuiFlyout, - EuiFlyoutBody, - EuiFlyoutFooter, - EuiFlyoutHeader, - EuiForm, - EuiFormRow, - EuiLink, - EuiRadio, - EuiSelect, - EuiSpacer, - EuiSwitch, - EuiText, - EuiTitle, -} from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { padStart, range } from 'lodash'; -import moment from 'moment-timezone'; -import React, { Component } from 'react'; -import styled from 'styled-components'; -import { toMountPoint } from '../../../../../../../../src/plugins/kibana_react/public'; -import { IUrlParams } from '../../../../context/UrlParamsContext/types'; -import { KibanaLink } from '../../../shared/Links/KibanaLink'; -import { createErrorGroupWatch, Schedule } from './createErrorGroupWatch'; -import { ElasticDocsLink } from '../../../shared/Links/ElasticDocsLink'; -import { ApmPluginContext } from '../../../../context/ApmPluginContext'; -import { getApmIndexPatternTitle } from '../../../../services/rest/index_pattern'; - -type ScheduleKey = keyof Schedule; - -const SmallInput = styled.div` - .euiFormRow { - max-width: 85px; - } - .euiFormHelpText { - width: 200px; - } -`; - -interface WatcherFlyoutProps { - urlParams: IUrlParams; - onClose: () => void; - isOpen: boolean; -} - -type IntervalUnit = 'm' | 'h'; - -interface WatcherFlyoutState { - schedule: ScheduleKey; - threshold: number; - actions: { - slack: boolean; - email: boolean; - }; - interval: { - value: number; - unit: IntervalUnit; - }; - daily: string; - emails: string; - slackUrl: string; -} - -export class WatcherFlyout extends Component< - WatcherFlyoutProps, - WatcherFlyoutState -> { - static contextType = ApmPluginContext; - context!: React.ContextType; - public state: WatcherFlyoutState = { - schedule: 'daily', - threshold: 10, - actions: { - slack: false, - email: false, - }, - interval: { - value: 10, - unit: 'm', - }, - daily: '08:00', - emails: '', - slackUrl: '', - }; - - public onChangeSchedule = (schedule: ScheduleKey) => { - this.setState({ schedule }); - }; - - public onChangeThreshold = (event: React.ChangeEvent) => { - this.setState({ - threshold: parseInt(event.target.value, 10), - }); - }; - - public onChangeDailyUnit = (event: React.ChangeEvent) => { - this.setState({ - daily: event.target.value, - }); - }; - - public onChangeIntervalValue = ( - event: React.ChangeEvent - ) => { - this.setState({ - interval: { - value: parseInt(event.target.value, 10), - unit: this.state.interval.unit, - }, - }); - }; - - public onChangeIntervalUnit = ( - event: React.ChangeEvent - ) => { - this.setState({ - interval: { - value: this.state.interval.value, - unit: event.target.value as IntervalUnit, - }, - }); - }; - - public onChangeAction = (actionName: 'slack' | 'email') => { - this.setState({ - actions: { - ...this.state.actions, - [actionName]: !this.state.actions[actionName], - }, - }); - }; - - public onChangeEmails = (event: React.ChangeEvent) => { - this.setState({ emails: event.target.value }); - }; - - public onChangeSlackUrl = (event: React.ChangeEvent) => { - this.setState({ slackUrl: event.target.value }); - }; - - public createWatch = () => { - const { serviceName } = this.props.urlParams; - const { core } = this.context; - - if (!serviceName) { - return; - } - - const emails = this.state.actions.email - ? this.state.emails - .split(',') - .map((email) => email.trim()) - .filter((email) => !!email) - : []; - - const slackUrl = this.state.actions.slack ? this.state.slackUrl : ''; - - const schedule = - this.state.schedule === 'interval' - ? { - interval: `${this.state.interval.value}${this.state.interval.unit}`, - } - : { - daily: { at: `${this.state.daily}` }, - }; - - const timeRange = - this.state.schedule === 'interval' - ? { - value: this.state.interval.value, - unit: this.state.interval.unit, - } - : { - value: 24, - unit: 'h', - }; - - return getApmIndexPatternTitle() - .then((indexPatternTitle) => { - return createErrorGroupWatch({ - http: core.http, - emails, - schedule, - serviceName, - slackUrl, - threshold: this.state.threshold, - timeRange, - apmIndexPatternTitle: indexPatternTitle, - }).then((id: string) => { - this.props.onClose(); - this.addSuccessToast(id); - }); - }) - .catch((e) => { - // eslint-disable-next-line - console.error(e); - this.addErrorToast(); - }); - }; - - public addErrorToast = () => { - const { core } = this.context; - - core.notifications.toasts.addWarning({ - title: i18n.translate( - 'xpack.apm.serviceDetails.enableErrorReportsPanel.watchCreationFailedNotificationTitle', - { - defaultMessage: 'Watch creation failed', - } - ), - text: toMountPoint( -

- {i18n.translate( - 'xpack.apm.serviceDetails.enableErrorReportsPanel.watchCreationFailedNotificationText', - { - defaultMessage: - 'Make sure your user has permission to create watches.', - } - )} -

- ), - }); - }; - - public addSuccessToast = (id: string) => { - const { core } = this.context; - - core.notifications.toasts.addSuccess({ - title: i18n.translate( - 'xpack.apm.serviceDetails.enableErrorReportsPanel.watchCreatedNotificationTitle', - { - defaultMessage: 'New watch created!', - } - ), - text: toMountPoint( -

- {i18n.translate( - 'xpack.apm.serviceDetails.enableErrorReportsPanel.watchCreatedNotificationText', - { - defaultMessage: - 'The watch is now ready and will send error reports for {serviceName}.', - values: { - serviceName: this.props.urlParams.serviceName, - }, - } - )}{' '} - - - {i18n.translate( - 'xpack.apm.serviceDetails.enableErrorReportsPanel.watchCreatedNotificationText.viewWatchLinkText', - { - defaultMessage: 'View watch', - } - )} - - -

- ), - }); - }; - - public render() { - if (!this.props.isOpen) { - return null; - } - - const dailyTime = this.state.daily; - const inputTime = `${dailyTime}Z`; // Add tz to make into UTC - const inputFormat = 'HH:mmZ'; // Parse as 24 hour w. tz - const dailyTimeFormatted = moment(inputTime, inputFormat).format('HH:mm'); // Format as 24h - const dailyTime12HourFormatted = moment(inputTime, inputFormat).format( - 'hh:mm A (z)' - ); // Format as 12h w. tz - - // Generate UTC hours for Daily Report select field - const intervalHours = range(24).map((i) => { - const hour = padStart(i.toString(), 2, '0'); - return { value: `${hour}:00`, text: `${hour}:00 UTC` }; - }); - - const flyoutBody = ( - -

- - {i18n.translate( - 'xpack.apm.serviceDetails.enableErrorReportsPanel.formDescription.documentationLinkText', - { - defaultMessage: 'documentation', - } - )} - - ), - }} - /> -

- - -

- {i18n.translate( - 'xpack.apm.serviceDetails.enableErrorReportsPanel.conditionTitle', - { - defaultMessage: 'Condition', - } - )} -

- - - - -

- {i18n.translate( - 'xpack.apm.serviceDetails.enableErrorReportsPanel.triggerScheduleTitle', - { - defaultMessage: 'Trigger schedule', - } - )} -

- - {i18n.translate( - 'xpack.apm.serviceDetails.enableErrorReportsPanel.triggerScheduleDescription', - { - defaultMessage: - 'Choose the time interval for the report, when the threshold is exceeded.', - } - )} - - - this.onChangeSchedule('daily')} - checked={this.state.schedule === 'daily'} - /> - - - - - - this.onChangeSchedule('interval')} - checked={this.state.schedule === 'interval'} - /> - - - - - - - - - - - - - - - -

- {i18n.translate( - 'xpack.apm.serviceDetails.enableErrorReportsPanel.actionsTitle', - { - defaultMessage: 'Actions', - } - )} -

- - {i18n.translate( - 'xpack.apm.serviceDetails.enableErrorReportsPanel.actionsDescription', - { - defaultMessage: - 'Reports can be sent by email or posted to a Slack channel. Each report will include the top 10 errors sorted by occurrence.', - } - )} - - - this.onChangeAction('email')} - /> - - {this.state.actions.email && ( - - - {i18n.translate( - 'xpack.apm.serviceDetails.enableErrorReportsPanel.recipientsHelpText.documentationLinkText', - { - defaultMessage: 'documentation', - } - )} - - ), - }} - /> - - } - > - - - )} - - this.onChangeAction('slack')} - /> - - {this.state.actions.slack && ( - - - {i18n.translate( - 'xpack.apm.serviceDetails.enableErrorReportsPanel.slackWebhookURLHelpText.documentationLinkText', - { - defaultMessage: 'documentation', - } - )} - - ), - }} - /> - - } - > - - - )} -
-
- ); - - return ( - - - -

- {i18n.translate( - 'xpack.apm.serviceDetails.enableErrorReportsPanel.enableErrorReportsTitle', - { - defaultMessage: 'Enable error reports', - } - )} -

-
-
- {flyoutBody} - - - - this.createWatch()} - fill - disabled={ - !this.state.actions.email && !this.state.actions.slack - } - > - {i18n.translate( - 'xpack.apm.serviceDetails.enableErrorReportsPanel.createWatchButtonLabel', - { - defaultMessage: 'Create watch', - } - )} - - - - -
- ); - } -} diff --git a/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/__test__/__snapshots__/createErrorGroupWatch.test.ts.snap b/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/__test__/__snapshots__/createErrorGroupWatch.test.ts.snap deleted file mode 100644 index 88f254747c686..0000000000000 --- a/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/__test__/__snapshots__/createErrorGroupWatch.test.ts.snap +++ /dev/null @@ -1,169 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -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 slack message 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 template correctly 1`] = ` -Object { - "actions": Object { - "email": Object { - "email": Object { - "body": Object { - "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": "\\"opbeans-node\\" has error groups which exceeds the threshold", - "to": "my@email.dk,mySecond@email.dk", - }, - }, - "log_error": Object { - "logging": Object { - "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": "__json__::{\\"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": "/services/slackid1/slackid2/slackid3", - "port": 443, - "scheme": "https", - }, - }, - }, - "condition": Object { - "script": Object { - "source": "return ctx.payload.aggregations.error_groups.buckets.length > 0", - }, - }, - "input": Object { - "search": Object { - "request": Object { - "body": Object { - "aggs": Object { - "error_groups": Object { - "aggs": Object { - "sample": Object { - "top_hits": Object { - "_source": Array [ - "error.log.message", - "error.exception.message", - "error.exception.handled", - "error.culprit", - "error.grouping_key", - "@timestamp", - ], - "size": 1, - "sort": Array [ - Object { - "@timestamp": "desc", - }, - ], - }, - }, - }, - "terms": Object { - "field": "error.grouping_key", - "min_doc_count": "10", - "order": Object { - "_count": "desc", - }, - "size": 10, - }, - }, - }, - "query": Object { - "bool": Object { - "filter": Array [ - Object { - "term": Object { - "service.name": "opbeans-node", - }, - }, - Object { - "term": Object { - "processor.event": "error", - }, - }, - Object { - "range": Object { - "@timestamp": Object { - "gte": "now-24h", - }, - }, - }, - ], - }, - }, - "size": 0, - }, - "indices": Array [ - "myIndexPattern", - ], - }, - }, - }, - "metadata": Object { - "emails": Array [ - "my@email.dk", - "mySecond@email.dk", - ], - "serviceName": "opbeans-node", - "slackUrlPath": "/services/slackid1/slackid2/slackid3", - "threshold": 10, - "timeRangeUnit": "h", - "timeRangeValue": 24, - "trigger": "This value must be changed in trigger section", - }, - "trigger": Object { - "schedule": Object { - "daily": Object { - "at": "08:00", - }, - }, - }, -} -`; diff --git a/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/__test__/createErrorGroupWatch.test.ts b/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/__test__/createErrorGroupWatch.test.ts deleted file mode 100644 index 054476af28de1..0000000000000 --- a/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/__test__/createErrorGroupWatch.test.ts +++ /dev/null @@ -1,120 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { isArray, isObject, isString } from 'lodash'; -import mustache from 'mustache'; -import uuid from 'uuid'; -import * as rest from '../../../../../services/rest/watcher'; -import { createErrorGroupWatch } from '../createErrorGroupWatch'; -import { esResponse } from './esResponse'; -import { HttpSetup } from 'kibana/public'; - -// disable html escaping since this is also disabled in watcher\s mustache implementation -mustache.escape = (value) => value; - -jest.mock('../../../../../services/rest/callApi', () => ({ - callApi: () => Promise.resolve(null), -})); - -describe('createErrorGroupWatch', () => { - let createWatchResponse: string; - let tmpl: any; - const createWatchSpy = jest - .spyOn(rest, 'createWatch') - .mockResolvedValue(undefined); - - beforeEach(async () => { - jest.spyOn(uuid, 'v4').mockReturnValue(Buffer.from('mocked-uuid')); - - createWatchResponse = await createErrorGroupWatch({ - http: {} as HttpSetup, - emails: ['my@email.dk', 'mySecond@email.dk'], - schedule: { - daily: { - at: '08:00', - }, - }, - serviceName: 'opbeans-node', - slackUrl: 'https://hooks.slack.com/services/slackid1/slackid2/slackid3', - threshold: 10, - timeRange: { value: 24, unit: 'h' }, - apmIndexPatternTitle: 'myIndexPattern', - }); - - const watchBody = createWatchSpy.mock.calls[0][0].watch; - const templateCtx = { - payload: esResponse, - metadata: watchBody.metadata, - }; - - tmpl = renderMustache(createWatchSpy.mock.calls[0][0].watch, templateCtx); - }); - - afterEach(() => jest.restoreAllMocks()); - - it('should call createWatch with correct args', () => { - expect(createWatchSpy.mock.calls[0][0].id).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(tmpl.actions.slack_webhook.webhook.body.slice(10)).text - ).toMatchSnapshot(); - }); - - it('should format email correctly', () => { - expect(tmpl.actions.email.email.to).toEqual( - 'my@email.dk,mySecond@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 template correctly', () => { - expect(tmpl).toMatchSnapshot(); - }); - - it('should return watch id', async () => { - const id = createWatchSpy.mock.calls[0][0].id; - expect(createWatchResponse).toEqual(id); - }); -}); - -// Recursively iterate a nested structure and render strings as mustache templates -type InputOutput = string | string[] | Record; -function renderMustache( - input: InputOutput, - ctx: Record -): InputOutput { - if (isString(input)) { - return mustache.render(input, { - ctx, - join: () => (text: string, render: any) => render(`{{${text}}}`, { ctx }), - }); - } - - if (isArray(input)) { - return input.map((itemValue) => renderMustache(itemValue, ctx)); - } - - if (isObject(input)) { - return Object.keys(input).reduce((acc, key) => { - const value = (input as any)[key]; - - return { ...acc, [key]: renderMustache(value, ctx) }; - }, {}); - } - - return input; -} diff --git a/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/__test__/esResponse.ts b/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/__test__/esResponse.ts deleted file mode 100644 index 8a7a0cee2d949..0000000000000 --- a/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/__test__/esResponse.ts +++ /dev/null @@ -1,153 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export const esResponse = { - 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/ServiceDetails/ServiceIntegrations/createErrorGroupWatch.ts b/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/createErrorGroupWatch.ts deleted file mode 100644 index 151c4abb9fce3..0000000000000 --- a/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/createErrorGroupWatch.ts +++ /dev/null @@ -1,261 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { i18n } from '@kbn/i18n'; -import { isEmpty } from 'lodash'; -import url from 'url'; -import uuid from 'uuid'; -import { HttpSetup } from 'kibana/public'; -import { - ERROR_CULPRIT, - ERROR_EXC_HANDLED, - ERROR_EXC_MESSAGE, - ERROR_GROUP_ID, - ERROR_LOG_MESSAGE, - PROCESSOR_EVENT, - SERVICE_NAME, -} from '../../../../../common/elasticsearch_fieldnames'; -import { createWatch } from '../../../../services/rest/watcher'; - -function getSlackPathUrl(slackUrl?: string) { - if (slackUrl) { - const { path } = url.parse(slackUrl); - return path; - } -} - -export interface Schedule { - interval?: string; - daily?: { - at: string; - }; -} - -interface Arguments { - http: HttpSetup; - emails: string[]; - schedule: Schedule; - serviceName: string; - slackUrl?: string; - threshold: number; - timeRange: { - value: number; - unit: string; - }; - apmIndexPatternTitle: string; -} - -interface Actions { - log_error: { logging: { text: string } }; - slack_webhook?: Record; - email?: Record; -} - -export async function createErrorGroupWatch({ - http, - emails = [], - schedule, - serviceName, - slackUrl, - threshold, - timeRange, - apmIndexPatternTitle, -}: Arguments) { - const id = `apm-${uuid.v4()}`; - - const slackUrlPath = getSlackPathUrl(slackUrl); - const emailTemplate = i18n.translate( - 'xpack.apm.serviceDetails.enableErrorReportsPanel.emailTemplateText', - { - defaultMessage: - 'Your service {serviceName} has error groups which exceeds {threshold} occurrences within {timeRange}{br}' + - '{br}' + - '{errorGroupsBuckets}{br}' + - '{errorLogMessage}{br}' + - '{errorCulprit}N/A{slashErrorCulprit}{br}' + - '{docCountParam} occurrences{br}' + - '{slashErrorGroupsBucket}', - values: { - serviceName: '"{{ctx.metadata.serviceName}}"', - threshold: '{{ctx.metadata.threshold}}', - timeRange: - '"{{ctx.metadata.timeRangeValue}}{{ctx.metadata.timeRangeUnit}}"', - errorGroupsBuckets: - '{{#ctx.payload.aggregations.error_groups.buckets}}', - errorLogMessage: - '{{sample.hits.hits.0._source.error.log.message}}{{^sample.hits.hits.0._source.error.log.message}}{{sample.hits.hits.0._source.error.exception.0.message}}{{/sample.hits.hits.0._source.error.log.message}}', - errorCulprit: - '{{sample.hits.hits.0._source.error.culprit}}{{^sample.hits.hits.0._source.error.culprit}}', - slashErrorCulprit: '{{/sample.hits.hits.0._source.error.culprit}}', - docCountParam: '{{doc_count}}', - slashErrorGroupsBucket: - '{{/ctx.payload.aggregations.error_groups.buckets}}', - br: '
', - }, - } - ); - - const slackTemplate = i18n.translate( - 'xpack.apm.serviceDetails.enableErrorReportsPanel.slackTemplateText', - { - defaultMessage: `Your service {serviceName} has error groups which exceeds {threshold} occurrences within {timeRange} -{errorGroupsBuckets} -{errorLogMessage} -{errorCulprit}N/A{slashErrorCulprit} -{docCountParam} occurrences -{slashErrorGroupsBucket}`, - values: { - serviceName: '"{{ctx.metadata.serviceName}}"', - threshold: '{{ctx.metadata.threshold}}', - timeRange: - '"{{ctx.metadata.timeRangeValue}}{{ctx.metadata.timeRangeUnit}}"', - errorGroupsBuckets: - '{{#ctx.payload.aggregations.error_groups.buckets}}', - errorLogMessage: - '>*{{sample.hits.hits.0._source.error.log.message}}{{^sample.hits.hits.0._source.error.log.message}}{{sample.hits.hits.0._source.error.exception.0.message}}{{/sample.hits.hits.0._source.error.log.message}}*', - errorCulprit: - '>{{#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}}', - slashErrorCulprit: '{{/sample.hits.hits.0._source.error.culprit}}', - docCountParam: '>{{doc_count}}', - slashErrorGroupsBucket: - '{{/ctx.payload.aggregations.error_groups.buckets}}', - }, - } - ); - - const actions: Actions = { - log_error: { logging: { text: emailTemplate } }, - }; - - const body = { - metadata: { - emails, - trigger: i18n.translate( - 'xpack.apm.serviceDetails.enableErrorReportsPanel.triggerText', - { - defaultMessage: 'This value must be changed in trigger section', - } - ), - serviceName, - threshold, - timeRangeValue: timeRange.value, - timeRangeUnit: timeRange.unit, - slackUrlPath, - }, - trigger: { - schedule, - }, - input: { - search: { - request: { - indices: [apmIndexPatternTitle], - body: { - size: 0, - query: { - bool: { - filter: [ - { term: { [SERVICE_NAME]: '{{ctx.metadata.serviceName}}' } }, - { term: { [PROCESSOR_EVENT]: 'error' } }, - { - range: { - '@timestamp': { - gte: - 'now-{{ctx.metadata.timeRangeValue}}{{ctx.metadata.timeRangeUnit}}', - }, - }, - }, - ], - }, - }, - aggs: { - error_groups: { - terms: { - min_doc_count: '{{ctx.metadata.threshold}}', - field: ERROR_GROUP_ID, - size: 10, - order: { - _count: 'desc', - }, - }, - aggs: { - sample: { - top_hits: { - _source: [ - ERROR_LOG_MESSAGE, - ERROR_EXC_MESSAGE, - ERROR_EXC_HANDLED, - ERROR_CULPRIT, - ERROR_GROUP_ID, - '@timestamp', - ], - sort: [ - { - '@timestamp': 'desc', - }, - ], - size: 1, - }, - }, - }, - }, - }, - }, - }, - }, - }, - condition: { - script: { - source: - 'return ctx.payload.aggregations.error_groups.buckets.length > 0', - }, - }, - actions, - }; - - if (slackUrlPath) { - body.actions.slack_webhook = { - webhook: { - scheme: 'https', - host: 'hooks.slack.com', - port: 443, - method: 'POST', - path: '{{ctx.metadata.slackUrlPath}}', - headers: { - 'Content-Type': 'application/json', - }, - body: `__json__::${JSON.stringify({ - text: slackTemplate, - })}`, - }, - }; - } - - if (!isEmpty(emails)) { - body.actions.email = { - email: { - to: '{{#join}}ctx.metadata.emails{{/join}}', - subject: i18n.translate( - 'xpack.apm.serviceDetails.enableErrorReportsPanel.emailSubjectText', - { - defaultMessage: - '{serviceName} has error groups which exceeds the threshold', - values: { serviceName: '"{{ctx.metadata.serviceName}}"' }, - } - ), - body: { - html: emailTemplate, - }, - }, - }; - } - - await createWatch({ - http, - id, - watch: body, - }); - return id; -} diff --git a/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/index.tsx b/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/index.tsx deleted file mode 100644 index 0a7dcbd0be3df..0000000000000 --- a/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/index.tsx +++ /dev/null @@ -1,122 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { EuiButtonEmpty, EuiContextMenu, EuiPopover } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import React from 'react'; -import { IUrlParams } from '../../../../context/UrlParamsContext/types'; -import { WatcherFlyout } from './WatcherFlyout'; -import { ApmPluginContext } from '../../../../context/ApmPluginContext'; - -interface Props { - urlParams: IUrlParams; -} -interface State { - isPopoverOpen: boolean; - activeFlyout: FlyoutName; -} -type FlyoutName = null | 'Watcher'; - -export class ServiceIntegrations extends React.Component { - static contextType = ApmPluginContext; - context!: React.ContextType; - - public state: State = { isPopoverOpen: false, activeFlyout: null }; - - public getWatcherPanelItems = () => { - const { core } = this.context; - - return [ - { - name: i18n.translate( - 'xpack.apm.serviceDetails.integrationsMenu.enableWatcherErrorReportsButtonLabel', - { - defaultMessage: 'Enable watcher error reports', - } - ), - icon: 'watchesApp', - onClick: () => { - this.closePopover(); - this.openFlyout('Watcher'); - }, - }, - { - name: i18n.translate( - 'xpack.apm.serviceDetails.integrationsMenu.viewWatchesButtonLabel', - { - defaultMessage: 'View existing watches', - } - ), - icon: 'watchesApp', - href: core.http.basePath.prepend( - '/app/management/insightsAndAlerting/watcher' - ), - target: '_blank', - onClick: () => this.closePopover(), - }, - ]; - }; - - public openPopover = () => - this.setState({ - isPopoverOpen: true, - }); - - public closePopover = () => - this.setState({ - isPopoverOpen: false, - }); - - public openFlyout = (name: FlyoutName) => - this.setState({ activeFlyout: name }); - - public closeFlyouts = () => this.setState({ activeFlyout: null }); - - public render() { - const button = ( - - {i18n.translate( - 'xpack.apm.serviceDetails.integrationsMenu.integrationsButtonLabel', - { - defaultMessage: 'Integrations', - } - )} - - ); - - return ( - <> - - - - - - ); - } -} diff --git a/x-pack/plugins/apm/public/components/app/ServiceDetails/index.tsx b/x-pack/plugins/apm/public/components/app/ServiceDetails/index.tsx index 2d52ad88d20dc..4488a962d0ba8 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceDetails/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceDetails/index.tsx @@ -14,7 +14,6 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { ApmHeader } from '../../shared/ApmHeader'; import { ServiceDetailTabs } from './ServiceDetailTabs'; -import { ServiceIntegrations } from './ServiceIntegrations'; import { useUrlParams } from '../../../hooks/useUrlParams'; import { AlertIntegrations } from './AlertIntegrations'; import { useApmPluginContext } from '../../../hooks/useApmPluginContext'; @@ -54,9 +53,6 @@ export function ServiceDetails({ tab }: Props) {

{serviceName}

- - - {isAlertingAvailable && (