From 94f9730d5608b2987f243e5cde24b16051d8fe6b Mon Sep 17 00:00:00 2001 From: cauemarcondes Date: Tue, 22 Sep 2020 15:32:47 +0200 Subject: [PATCH 1/4] adding alert to service page --- .../AlertingFlyout/index.tsx | 4 +- .../alerting/ServiceAlertTrigger/index.tsx | 16 +- .../index.tsx | 6 +- .../index.tsx | 2 +- .../components/alerting/fields.test.tsx | 50 +++++ .../apm/public/components/alerting/fields.tsx | 12 +- .../app/Home/AlertIntegrations/index.tsx | 186 ++++++++++++++++++ .../apm/public/components/app/Home/index.tsx | 21 +- .../AlertIntegrations/index.tsx | 4 +- .../components/app/ServiceDetails/index.tsx | 19 +- .../public/hooks/use_alerting_integrations.ts | 31 +++ .../alerts/register_error_count_alert_type.ts | 29 ++- ...transaction_duration_anomaly_alert_type.ts | 55 ++++-- ...ister_transaction_error_rate_alert_type.ts | 40 +++- .../apm/server/lib/alerts/utils.test.ts | 15 ++ x-pack/plugins/apm/server/lib/alerts/utils.ts | 11 ++ 16 files changed, 447 insertions(+), 54 deletions(-) rename x-pack/plugins/apm/public/components/{app/ServiceDetails/AlertIntegrations => alerting}/AlertingFlyout/index.tsx (86%) create mode 100644 x-pack/plugins/apm/public/components/alerting/fields.test.tsx create mode 100644 x-pack/plugins/apm/public/components/app/Home/AlertIntegrations/index.tsx create mode 100644 x-pack/plugins/apm/public/hooks/use_alerting_integrations.ts create mode 100644 x-pack/plugins/apm/server/lib/alerts/utils.test.ts create mode 100644 x-pack/plugins/apm/server/lib/alerts/utils.ts diff --git a/x-pack/plugins/apm/public/components/app/ServiceDetails/AlertIntegrations/AlertingFlyout/index.tsx b/x-pack/plugins/apm/public/components/alerting/AlertingFlyout/index.tsx similarity index 86% rename from x-pack/plugins/apm/public/components/app/ServiceDetails/AlertIntegrations/AlertingFlyout/index.tsx rename to x-pack/plugins/apm/public/components/alerting/AlertingFlyout/index.tsx index ad3f1696ad5e3..3bee6b2388264 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceDetails/AlertIntegrations/AlertingFlyout/index.tsx +++ b/x-pack/plugins/apm/public/components/alerting/AlertingFlyout/index.tsx @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ import React from 'react'; -import { AlertType } from '../../../../../../common/alert_types'; -import { AlertAdd } from '../../../../../../../triggers_actions_ui/public'; +import { AlertType } from '../../../../common/alert_types'; +import { AlertAdd } from '../../../../../triggers_actions_ui/public'; type AlertAddProps = React.ComponentProps; diff --git a/x-pack/plugins/apm/public/components/alerting/ServiceAlertTrigger/index.tsx b/x-pack/plugins/apm/public/components/alerting/ServiceAlertTrigger/index.tsx index 86dc7f5a90475..b4d3e8f3ad241 100644 --- a/x-pack/plugins/apm/public/components/alerting/ServiceAlertTrigger/index.tsx +++ b/x-pack/plugins/apm/public/components/alerting/ServiceAlertTrigger/index.tsx @@ -34,11 +34,17 @@ export function ServiceAlertTrigger(props: Props) { useEffect(() => { // we only want to run this on mount to set default values - setAlertProperty('name', `${alertTypeName} | ${params.serviceName}`); - setAlertProperty('tags', [ - 'apm', - `service.name:${params.serviceName}`.toLowerCase(), - ]); + + const alertName = params.serviceName + ? `${alertTypeName} | ${params.serviceName}` + : alertTypeName; + setAlertProperty('name', alertName); + + const tags = ['apm']; + if (params.serviceName) { + tags.push(`service.name:${params.serviceName}`.toLowerCase()); + } + setAlertProperty('tags', tags); Object.keys(params).forEach((key) => { setAlertParams(key, params[key]); }); diff --git a/x-pack/plugins/apm/public/components/alerting/TransactionDurationAnomalyAlertTrigger/index.tsx b/x-pack/plugins/apm/public/components/alerting/TransactionDurationAnomalyAlertTrigger/index.tsx index ca1f55e9d391a..d497341e6305c 100644 --- a/x-pack/plugins/apm/public/components/alerting/TransactionDurationAnomalyAlertTrigger/index.tsx +++ b/x-pack/plugins/apm/public/components/alerting/TransactionDurationAnomalyAlertTrigger/index.tsx @@ -32,8 +32,8 @@ import { interface Params { windowSize: number; windowUnit: string; - serviceName: string; - transactionType: string; + serviceName?: string; + transactionType?: string; environment: string; anomalySeverityType: | ANOMALY_SEVERITY.CRITICAL @@ -59,7 +59,7 @@ export function TransactionDurationAnomalyAlertTrigger(props: Props) { [TRANSACTION_PAGE_LOAD, TRANSACTION_REQUEST].includes(transactionType) ); - if (!supportedTransactionTypes.length || !serviceName) { + if (serviceName && !supportedTransactionTypes.length) { return null; } diff --git a/x-pack/plugins/apm/public/components/alerting/TransactionErrorRateAlertTrigger/index.tsx b/x-pack/plugins/apm/public/components/alerting/TransactionErrorRateAlertTrigger/index.tsx index 4dbf4dc10a907..7f4937ce35478 100644 --- a/x-pack/plugins/apm/public/components/alerting/TransactionErrorRateAlertTrigger/index.tsx +++ b/x-pack/plugins/apm/public/components/alerting/TransactionErrorRateAlertTrigger/index.tsx @@ -43,7 +43,7 @@ export function TransactionErrorRateAlertTrigger(props: Props) { const { start, end, transactionType } = urlParams; const { environmentOptions } = useEnvironments({ serviceName, start, end }); - if (!transactionTypes.length || !serviceName) { + if (serviceName && !transactionTypes.length) { return null; } diff --git a/x-pack/plugins/apm/public/components/alerting/fields.test.tsx b/x-pack/plugins/apm/public/components/alerting/fields.test.tsx new file mode 100644 index 0000000000000..1739d3146a8c5 --- /dev/null +++ b/x-pack/plugins/apm/public/components/alerting/fields.test.tsx @@ -0,0 +1,50 @@ +/* + * 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 React from 'react'; +import { ServiceField, TransactionTypeField } from './fields'; +import { render } from '@testing-library/react'; +import { expectTextsInDocument } from '../../utils/testHelpers'; + +describe('alerting fields', () => { + describe('Service Fiels', () => { + it('renders with value', () => { + const component = render(); + expectTextsInDocument(component, ['foo']); + }); + it('renders with All when value is not defined', () => { + const component = render(); + expectTextsInDocument(component, ['All']); + }); + }); + describe('Transaction Type Field', () => { + it('renders select field when multiple options available', () => { + const options = [ + { text: 'Foo', value: 'foo' }, + { text: 'Bar', value: 'bar' }, + ]; + const component = render( + + ); + expectTextsInDocument(component, ['Foo']); + }); + it('renders read-only field when single option available', () => { + const options = [{ text: 'Bar', value: 'bar' }]; + const component = render( + + ); + expectTextsInDocument(component, ['Bar']); + }); + it('renders read-only All option when no option available', () => { + const component = render(); + expectTextsInDocument(component, ['All']); + }); + + it('renders current value when available', () => { + const component = render(); + expectTextsInDocument(component, ['foo']); + }); + }); +}); diff --git a/x-pack/plugins/apm/public/components/alerting/fields.tsx b/x-pack/plugins/apm/public/components/alerting/fields.tsx index e145d03671a18..70bee132094db 100644 --- a/x-pack/plugins/apm/public/components/alerting/fields.tsx +++ b/x-pack/plugins/apm/public/components/alerting/fields.tsx @@ -11,13 +11,17 @@ import { EuiSelectOption } from '@elastic/eui'; import { getEnvironmentLabel } from '../../../common/environment_filter_values'; import { PopoverExpression } from './ServiceAlertTrigger/PopoverExpression'; +const ALL_OPTION = i18n.translate('xpack.apm.alerting.fields.all_option', { + defaultMessage: 'All', +}); + export function ServiceField({ value }: { value?: string }) { return ( ); } @@ -61,8 +65,10 @@ export function TransactionTypeField({ defaultMessage: 'Type', }); - if (!options || options.length === 1) { - return ; + if (!options || options.length <= 1) { + return ( + + ); } return ( diff --git a/x-pack/plugins/apm/public/components/app/Home/AlertIntegrations/index.tsx b/x-pack/plugins/apm/public/components/app/Home/AlertIntegrations/index.tsx new file mode 100644 index 0000000000000..6e3f734c1f97d --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/Home/AlertIntegrations/index.tsx @@ -0,0 +1,186 @@ +/* + * 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, + EuiContextMenuPanelDescriptor, + EuiPopover, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React, { useState } from 'react'; +import { AlertType } from '../../../../../common/alert_types'; +import { useApmPluginContext } from '../../../../hooks/useApmPluginContext'; +import { AlertingFlyout } from '../../../alerting/AlertingFlyout'; + +const alertLabel = i18n.translate('xpack.apm.home.alertsMenu.alerts', { + defaultMessage: 'Alerts', +}); +const transactionDurationLabel = i18n.translate( + 'xpack.apm.home.alertsMenu.transactionDuration', + { defaultMessage: 'Transaction duration' } +); +const transactionErrorRateLabel = i18n.translate( + 'xpack.apm.home.alertsMenu.transactionErrorRate', + { defaultMessage: 'Transaction error rate' } +); +const errorCountLabel = i18n.translate('xpack.apm.home.alertsMenu.errorCount', { + defaultMessage: 'Error count', +}); +const createThresholdAlertLabel = i18n.translate( + 'xpack.apm.home.alertsMenu.createThresholdAlert', + { defaultMessage: 'Create threshold alert' } +); +const createAnomalyAlertAlertLabel = i18n.translate( + 'xpack.apm.home.alertsMenu.createAnomalyAlert', + { defaultMessage: 'Create anomaly alert' } +); + +const CREATE_TRANSACTION_DURATION_ALERT_PANEL_ID = + 'create_transaction_duration_panel'; +const CREATE_TRANSACTION_ERROR_RATE_ALERT_PANEL_ID = + 'create_transaction_error_rate_panel'; +const CREATE_ERROR_COUNT_ALERT_PANEL_ID = 'create_error_count_panel'; + +interface Props { + canReadAlerts: boolean; + canSaveAlerts: boolean; + canReadAnomalies: boolean; +} + +export function AlertIntegrations(props: Props) { + const { canSaveAlerts, canReadAlerts, canReadAnomalies } = props; + + const plugin = useApmPluginContext(); + + const [popoverOpen, setPopoverOpen] = useState(false); + + const [alertType, setAlertType] = useState(null); + + const button = ( + setPopoverOpen(true)} + > + {alertLabel} + + ); + + const panels: EuiContextMenuPanelDescriptor[] = [ + { + id: 0, + title: alertLabel, + items: [ + ...(canSaveAlerts + ? [ + { + name: transactionDurationLabel, + panel: CREATE_TRANSACTION_DURATION_ALERT_PANEL_ID, + }, + { + name: transactionErrorRateLabel, + panel: CREATE_TRANSACTION_ERROR_RATE_ALERT_PANEL_ID, + }, + { + name: errorCountLabel, + panel: CREATE_ERROR_COUNT_ALERT_PANEL_ID, + }, + ] + : []), + ...(canReadAlerts + ? [ + { + name: i18n.translate( + 'xpack.apm.home.alertsMenu.viewActiveAlerts', + { defaultMessage: 'View active alerts' } + ), + href: plugin.core.http.basePath.prepend( + '/app/management/insightsAndAlerting/triggersActions/alerts' + ), + icon: 'tableOfContents', + }, + ] + : []), + ], + }, + + // transaction duration panel + { + id: CREATE_TRANSACTION_DURATION_ALERT_PANEL_ID, + title: transactionDurationLabel, + items: [ + // anomaly alerts + ...(canReadAnomalies + ? [ + { + name: createAnomalyAlertAlertLabel, + onClick: () => { + setAlertType(AlertType.TransactionDurationAnomaly); + setPopoverOpen(false); + }, + }, + ] + : []), + ], + }, + + // transaction error rate panel + { + id: CREATE_TRANSACTION_ERROR_RATE_ALERT_PANEL_ID, + title: transactionErrorRateLabel, + items: [ + // threshold alerts + { + name: createThresholdAlertLabel, + onClick: () => { + setAlertType(AlertType.TransactionErrorRate); + setPopoverOpen(false); + }, + }, + ], + }, + + // error alerts panel + { + id: CREATE_ERROR_COUNT_ALERT_PANEL_ID, + title: errorCountLabel, + items: [ + { + name: createThresholdAlertLabel, + onClick: () => { + setAlertType(AlertType.ErrorCount); + setPopoverOpen(false); + }, + }, + ], + }, + ]; + + return ( + <> + setPopoverOpen(false)} + panelPaddingSize="none" + anchorPosition="downRight" + > + + + { + if (!visible) { + setAlertType(null); + } + }} + /> + + ); +} diff --git a/x-pack/plugins/apm/public/components/app/Home/index.tsx b/x-pack/plugins/apm/public/components/app/Home/index.tsx index b2f15dbb11341..5924924650bed 100644 --- a/x-pack/plugins/apm/public/components/app/Home/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Home/index.tsx @@ -26,6 +26,8 @@ import { SetupInstructionsLink } from '../../shared/Links/SetupInstructionsLink' import { ServiceMap } from '../ServiceMap'; import { ServiceOverview } from '../ServiceOverview'; import { TraceOverview } from '../TraceOverview'; +import { AlertIntegrations } from './AlertIntegrations'; +import { useAlertingIntegrations } from '../../../hooks/use_alerting_integrations'; function getHomeTabs({ serviceMapEnabled = true, @@ -84,12 +86,20 @@ interface Props { export function Home({ tab }: Props) { const { config, core } = useApmPluginContext(); - const canAccessML = !!core.application.capabilities.ml?.canAccessML; + const capabilities = core.application.capabilities; + const canAccessML = !!capabilities.ml?.canAccessML; const homeTabs = getHomeTabs(config); const selectedTab = homeTabs.find( (homeTab) => homeTab.name === tab ) as $ElementType; + const { + isAlertingAvailable, + canReadAlerts, + canSaveAlerts, + canReadAnomalies, + } = useAlertingIntegrations(); + return (
@@ -106,6 +116,15 @@ export function Home({ tab }: Props) { + {isAlertingAvailable && ( + + + + )} {canAccessML && ( diff --git a/x-pack/plugins/apm/public/components/app/ServiceDetails/AlertIntegrations/index.tsx b/x-pack/plugins/apm/public/components/app/ServiceDetails/AlertIntegrations/index.tsx index c11bfdeae945b..1661dfb6ca27a 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceDetails/AlertIntegrations/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceDetails/AlertIntegrations/index.tsx @@ -7,14 +7,14 @@ import { EuiButtonEmpty, EuiContextMenu, - EuiPopover, EuiContextMenuPanelDescriptor, + EuiPopover, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React, { useState } from 'react'; import { AlertType } from '../../../../../common/alert_types'; -import { AlertingFlyout } from './AlertingFlyout'; import { useApmPluginContext } from '../../../../hooks/useApmPluginContext'; +import { AlertingFlyout } from '../../../alerting/AlertingFlyout'; const alertLabel = i18n.translate( 'xpack.apm.serviceDetails.alertsMenu.alerts', 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 67c4a7c4cde1b..796093fc94792 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceDetails/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceDetails/index.tsx @@ -14,6 +14,7 @@ import { i18n } from '@kbn/i18n'; import React from 'react'; import { RouteComponentProps } from 'react-router-dom'; import { useApmPluginContext } from '../../../hooks/useApmPluginContext'; +import { useAlertingIntegrations } from '../../../hooks/use_alerting_integrations'; import { ApmHeader } from '../../shared/ApmHeader'; import { AlertIntegrations } from './AlertIntegrations'; import { ServiceDetailTabs } from './ServiceDetailTabs'; @@ -25,18 +26,12 @@ interface Props extends RouteComponentProps<{ serviceName: string }> { export function ServiceDetails({ match, tab }: Props) { const plugin = useApmPluginContext(); const { serviceName } = match.params; - const capabilities = plugin.core.application.capabilities; - const canReadAlerts = !!capabilities.apm['alerting:show']; - const canSaveAlerts = !!capabilities.apm['alerting:save']; - const isAlertingPluginEnabled = 'alerts' in plugin.plugins; - const isAlertingAvailable = - isAlertingPluginEnabled && (canReadAlerts || canSaveAlerts); - const isMlPluginEnabled = 'ml' in plugin.plugins; - const canReadAnomalies = !!( - isMlPluginEnabled && - capabilities.ml.canAccessML && - capabilities.ml.canGetJobs - ); + const { + isAlertingAvailable, + canReadAlerts, + canSaveAlerts, + canReadAnomalies, + } = useAlertingIntegrations(); const ADD_DATA_LABEL = i18n.translate('xpack.apm.addDataButtonLabel', { defaultMessage: 'Add data', diff --git a/x-pack/plugins/apm/public/hooks/use_alerting_integrations.ts b/x-pack/plugins/apm/public/hooks/use_alerting_integrations.ts new file mode 100644 index 0000000000000..2f7c70b93f0a1 --- /dev/null +++ b/x-pack/plugins/apm/public/hooks/use_alerting_integrations.ts @@ -0,0 +1,31 @@ +/* + * 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 { useApmPluginContext } from './useApmPluginContext'; + +export const useAlertingIntegrations = () => { + const plugin = useApmPluginContext(); + + const capabilities = plugin.core.application.capabilities; + const canReadAlerts = !!capabilities.apm['alerting:show']; + const canSaveAlerts = !!capabilities.apm['alerting:save']; + const isAlertingPluginEnabled = 'alerts' in plugin.plugins; + const isAlertingAvailable = + isAlertingPluginEnabled && (canReadAlerts || canSaveAlerts); + const isMlPluginEnabled = 'ml' in plugin.plugins; + const canReadAnomalies = !!( + isMlPluginEnabled && + capabilities.ml.canAccessML && + capabilities.ml.canGetJobs + ); + + return { + isAlertingAvailable, + canReadAlerts, + canSaveAlerts, + canReadAnomalies, + }; +}; diff --git a/x-pack/plugins/apm/server/lib/alerts/register_error_count_alert_type.ts b/x-pack/plugins/apm/server/lib/alerts/register_error_count_alert_type.ts index 5455cd9f6a495..a449ccfe15485 100644 --- a/x-pack/plugins/apm/server/lib/alerts/register_error_count_alert_type.ts +++ b/x-pack/plugins/apm/server/lib/alerts/register_error_count_alert_type.ts @@ -10,10 +10,7 @@ import { take } from 'rxjs/operators'; import { ProcessorEvent } from '../../../common/processor_event'; import { getEnvironmentUiFilterES } from '../helpers/convert_ui_filters/get_environment_ui_filter_es'; import { AlertType, ALERT_TYPES_CONFIG } from '../../../common/alert_types'; -import { - ESSearchResponse, - ESSearchRequest, -} from '../../../typings/elasticsearch'; +import { ESSearchResponse } from '../../../typings/elasticsearch'; import { PROCESSOR_EVENT, SERVICE_NAME, @@ -22,6 +19,7 @@ import { AlertingPlugin } from '../../../../alerts/server'; import { getApmIndices } from '../settings/apm_indices/get_apm_indices'; import { APMConfig } from '../..'; import { apmActionVariables } from './action_variables'; +import { getCommaSeparetedAggregationKey } from './utils'; interface RegisterAlertParams { alerts: AlertingPlugin['setup']; @@ -32,7 +30,7 @@ const paramsSchema = schema.object({ windowSize: schema.number(), windowUnit: schema.string(), threshold: schema.number(), - serviceName: schema.string(), + serviceName: schema.maybe(schema.string()), environment: schema.string(), }); @@ -83,17 +81,27 @@ export function registerErrorCountAlertType({ }, }, { term: { [PROCESSOR_EVENT]: ProcessorEvent.error } }, - { term: { [SERVICE_NAME]: alertParams.serviceName } }, + ...(alertParams.serviceName + ? [{ term: { [SERVICE_NAME]: alertParams.serviceName } }] + : []), ...getEnvironmentUiFilterES(alertParams.environment), ], }, }, + aggs: { + services: { + terms: { + field: SERVICE_NAME, + size: 50, + }, + }, + }, }, }; const response: ESSearchResponse< unknown, - ESSearchRequest + typeof searchParams > = await services.callCluster('search', searchParams); const errorCount = response.hits.total.value; @@ -102,8 +110,13 @@ export function registerErrorCountAlertType({ const alertInstance = services.alertInstanceFactory( AlertType.ErrorCount ); + alertInstance.scheduleActions(alertTypeConfig.defaultActionGroupId, { - serviceName: alertParams.serviceName, + serviceName: + alertParams.serviceName || + getCommaSeparetedAggregationKey( + response.aggregations?.services.buckets + ), environment: alertParams.environment, threshold: alertParams.threshold, triggerValue: errorCount, diff --git a/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_anomaly_alert_type.ts b/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_anomaly_alert_type.ts index 61cd79b672735..a374bb022d0d3 100644 --- a/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_anomaly_alert_type.ts +++ b/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_anomaly_alert_type.ts @@ -18,6 +18,7 @@ import { APMConfig } from '../..'; import { MlPluginSetup } from '../../../../ml/server'; import { getMLJobIds } from '../service_map/get_service_anomalies'; import { apmActionVariables } from './action_variables'; +import { getCommaSeparetedAggregationKey } from './utils'; interface RegisterAlertParams { alerts: AlertingPlugin['setup']; @@ -26,8 +27,8 @@ interface RegisterAlertParams { } const paramsSchema = schema.object({ - serviceName: schema.string(), - transactionType: schema.string(), + serviceName: schema.maybe(schema.string()), + transactionType: schema.maybe(schema.string()), windowSize: schema.number(), windowUnit: schema.string(), environment: schema.string(), @@ -94,8 +95,8 @@ export function registerTransactionDurationAnomalyAlertType({ } const anomalySearchParams = { + terminateAfter: 1, body: { - terminateAfter: 1, size: 0, query: { bool: { @@ -110,11 +111,15 @@ export function registerTransactionDurationAnomalyAlertType({ }, }, }, - { - term: { - partition_field_value: alertParams.serviceName, - }, - }, + ...(alertParams.serviceName + ? [ + { + term: { + partition_field_value: alertParams.serviceName, + }, + }, + ] + : []), { range: { record_score: { @@ -125,12 +130,32 @@ export function registerTransactionDurationAnomalyAlertType({ ], }, }, + aggs: { + services: { + terms: { + field: 'partition_field_value', + size: 50, + }, + }, + transaction_types: { + terms: { + field: 'by_field_value', + }, + }, + }, }, }; const response = ((await mlAnomalySearch( anomalySearchParams - )) as unknown) as { hits: { total: { value: number } } }; + )) as unknown) as { + hits: { total: { value: number } }; + aggregations?: { + services: { buckets: Array<{ key: string }> }; + transaction_types: { buckets: Array<{ key: string }> }; + }; + }; + const hitCount = response.hits.total.value; if (hitCount > 0) { @@ -138,8 +163,16 @@ export function registerTransactionDurationAnomalyAlertType({ AlertType.TransactionDurationAnomaly ); alertInstance.scheduleActions(alertTypeConfig.defaultActionGroupId, { - serviceName: alertParams.serviceName, - transactionType: alertParams.transactionType, + serviceName: + alertParams.serviceName || + getCommaSeparetedAggregationKey( + response.aggregations?.services.buckets + ), + transactionType: + alertParams.transactionType || + getCommaSeparetedAggregationKey( + response.aggregations?.transaction_types.buckets + ), environment: alertParams.environment, }); } diff --git a/x-pack/plugins/apm/server/lib/alerts/register_transaction_error_rate_alert_type.ts b/x-pack/plugins/apm/server/lib/alerts/register_transaction_error_rate_alert_type.ts index a6ed40fc15ec6..5cd7c6f10c15d 100644 --- a/x-pack/plugins/apm/server/lib/alerts/register_transaction_error_rate_alert_type.ts +++ b/x-pack/plugins/apm/server/lib/alerts/register_transaction_error_rate_alert_type.ts @@ -22,6 +22,7 @@ import { getApmIndices } from '../settings/apm_indices/get_apm_indices'; import { APMConfig } from '../..'; import { getEnvironmentUiFilterES } from '../helpers/convert_ui_filters/get_environment_ui_filter_es'; import { apmActionVariables } from './action_variables'; +import { getCommaSeparetedAggregationKey } from './utils'; interface RegisterAlertParams { alerts: AlertingPlugin['setup']; @@ -32,8 +33,8 @@ const paramsSchema = schema.object({ windowSize: schema.number(), windowUnit: schema.string(), threshold: schema.number(), - transactionType: schema.string(), - serviceName: schema.string(), + transactionType: schema.maybe(schema.string()), + serviceName: schema.maybe(schema.string()), environment: schema.string(), }); @@ -84,8 +85,18 @@ export function registerTransactionErrorRateAlertType({ }, }, { term: { [PROCESSOR_EVENT]: ProcessorEvent.transaction } }, - { term: { [SERVICE_NAME]: alertParams.serviceName } }, - { term: { [TRANSACTION_TYPE]: alertParams.transactionType } }, + ...(alertParams.serviceName + ? [{ term: { [SERVICE_NAME]: alertParams.serviceName } }] + : []), + ...(alertParams.transactionType + ? [ + { + term: { + [TRANSACTION_TYPE]: alertParams.transactionType, + }, + }, + ] + : []), ...getEnvironmentUiFilterES(alertParams.environment), ], }, @@ -94,6 +105,13 @@ export function registerTransactionErrorRateAlertType({ erroneous_transactions: { filter: { term: { [EVENT_OUTCOME]: EventOutcome.failure } }, }, + services: { + terms: { + field: SERVICE_NAME, + size: 50, + }, + }, + transaction_types: { terms: { field: TRANSACTION_TYPE } }, }, }, }; @@ -119,8 +137,18 @@ export function registerTransactionErrorRateAlertType({ ); alertInstance.scheduleActions(alertTypeConfig.defaultActionGroupId, { - serviceName: alertParams.serviceName, - transactionType: alertParams.transactionType, + serviceName: + alertParams.serviceName || + getCommaSeparetedAggregationKey( + response.aggregations?.services.buckets + ), + + transactionType: + alertParams.transactionType || + getCommaSeparetedAggregationKey( + response.aggregations?.transaction_types.buckets + ), + environment: alertParams.environment, threshold: alertParams.threshold, triggerValue: transactionErrorRate, diff --git a/x-pack/plugins/apm/server/lib/alerts/utils.test.ts b/x-pack/plugins/apm/server/lib/alerts/utils.test.ts new file mode 100644 index 0000000000000..7a41005cff45b --- /dev/null +++ b/x-pack/plugins/apm/server/lib/alerts/utils.test.ts @@ -0,0 +1,15 @@ +/* + * 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 { getCommaSeparetedAggregationKey } from './utils'; + +describe('alerts utils', () => { + describe('getCommaSeparetedAggregationKey', () => { + it('returns comma separeted', () => { + const buckets = [{ key: 'foo' }, { key: 'bar' }]; + expect(getCommaSeparetedAggregationKey(buckets)).toEqual('foo,bar'); + }); + }); +}); diff --git a/x-pack/plugins/apm/server/lib/alerts/utils.ts b/x-pack/plugins/apm/server/lib/alerts/utils.ts new file mode 100644 index 0000000000000..f31c1d8eb8893 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/alerts/utils.ts @@ -0,0 +1,11 @@ +/* + * 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 function getCommaSeparetedAggregationKey( + buckets: Array<{ key: any; doc_count?: number }> | undefined +) { + return buckets?.map((bucket) => bucket.key).join(','); +} From bebf673b66615f22bbf187ee23810bb8c46cbe74 Mon Sep 17 00:00:00 2001 From: cauemarcondes Date: Thu, 24 Sep 2020 14:52:41 +0200 Subject: [PATCH 2/4] sending on alert per service environment and transaction type --- .../register_error_count_alert_type.test.ts | 197 +++++++++++ .../alerts/register_error_count_alert_type.ts | 66 +++- ...action_duration_anomaly_alert_type.test.ts | 326 ++++++++++++++++++ ...transaction_duration_anomaly_alert_type.ts | 96 ++++-- ..._transaction_error_rate_alert_type.test.ts | 289 ++++++++++++++++ ...ister_transaction_error_rate_alert_type.ts | 79 +++-- .../apm/server/lib/alerts/utils.test.ts | 15 - x-pack/plugins/apm/server/lib/alerts/utils.ts | 11 - .../lib/service_map/get_service_anomalies.ts | 12 +- 9 files changed, 996 insertions(+), 95 deletions(-) create mode 100644 x-pack/plugins/apm/server/lib/alerts/register_error_count_alert_type.test.ts create mode 100644 x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_anomaly_alert_type.test.ts create mode 100644 x-pack/plugins/apm/server/lib/alerts/register_transaction_error_rate_alert_type.test.ts delete mode 100644 x-pack/plugins/apm/server/lib/alerts/utils.test.ts delete mode 100644 x-pack/plugins/apm/server/lib/alerts/utils.ts diff --git a/x-pack/plugins/apm/server/lib/alerts/register_error_count_alert_type.test.ts b/x-pack/plugins/apm/server/lib/alerts/register_error_count_alert_type.test.ts new file mode 100644 index 0000000000000..6a6e2b7137055 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/alerts/register_error_count_alert_type.test.ts @@ -0,0 +1,197 @@ +/* + * 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 { Observable } from 'rxjs'; +import * as Rx from 'rxjs'; +import { toArray, map } from 'rxjs/operators'; + +import { AlertingPlugin } from '../../../../alerts/server'; +import { APMConfig } from '../..'; + +import { registerErrorCountAlertType } from './register_error_count_alert_type'; + +type Operator = (source: Rx.Observable) => Rx.Observable; +const pipeClosure = (fn: Operator): Operator => { + return (source: Rx.Observable) => { + return Rx.defer(() => fn(source)); + }; +}; +const mockedConfig$ = (Rx.of('apm_oss.errorIndices').pipe( + pipeClosure((source$) => { + return source$.pipe(map((i) => i)); + }), + toArray() +) as unknown) as Observable; + +describe('Error count alert', () => { + it("doesn't send an alert when error count is less than threshold", async () => { + let alertExecutor: any; + const alerts = { + registerType: ({ executor }) => { + alertExecutor = executor; + }, + } as AlertingPlugin['setup']; + + registerErrorCountAlertType({ + alerts, + config$: mockedConfig$, + }); + expect(alertExecutor).toBeDefined(); + + const services = { + callCluster: jest.fn(() => ({ + hits: { + total: { + value: 0, + }, + }, + })), + alertInstanceFactory: jest.fn(), + }; + const params = { threshold: 1 }; + + await alertExecutor!({ services, params }); + expect(services.alertInstanceFactory).not.toBeCalled(); + }); + + it('sends alerts with service name and environment', async () => { + let alertExecutor: any; + const alerts = { + registerType: ({ executor }) => { + alertExecutor = executor; + }, + } as AlertingPlugin['setup']; + + registerErrorCountAlertType({ + alerts, + config$: mockedConfig$, + }); + expect(alertExecutor).toBeDefined(); + + const scheduleActions = jest.fn(); + const services = { + callCluster: jest.fn(() => ({ + hits: { + total: { + value: 2, + }, + }, + aggregations: { + services: { + buckets: [ + { + key: 'foo', + environments: { + buckets: [{ key: 'env-foo' }, { key: 'env-foo-2' }], + }, + }, + { + key: 'bar', + environments: { + buckets: [{ key: 'env-bar' }, { key: 'env-bar-2' }], + }, + }, + ], + }, + }, + })), + alertInstanceFactory: jest.fn(() => ({ scheduleActions })), + }; + const params = { threshold: 1 }; + + await alertExecutor!({ services, params }); + [ + 'apm.error_rate_foo_env-foo', + 'apm.error_rate_foo_env-foo-2', + 'apm.error_rate_bar_env-bar', + 'apm.error_rate_bar_env-bar-2', + ].forEach((instanceName) => + expect(services.alertInstanceFactory).toHaveBeenCalledWith(instanceName) + ); + + expect(scheduleActions).toHaveBeenCalledWith('threshold_met', { + serviceName: 'foo', + environment: 'env-foo', + threshold: 1, + triggerValue: 2, + }); + expect(scheduleActions).toHaveBeenCalledWith('threshold_met', { + serviceName: 'foo', + environment: 'env-foo-2', + threshold: 1, + triggerValue: 2, + }); + expect(scheduleActions).toHaveBeenCalledWith('threshold_met', { + serviceName: 'bar', + environment: 'env-bar', + threshold: 1, + triggerValue: 2, + }); + expect(scheduleActions).toHaveBeenCalledWith('threshold_met', { + serviceName: 'bar', + environment: 'env-bar-2', + threshold: 1, + triggerValue: 2, + }); + }); + it('sends alerts with service name', async () => { + let alertExecutor: any; + const alerts = { + registerType: ({ executor }) => { + alertExecutor = executor; + }, + } as AlertingPlugin['setup']; + + registerErrorCountAlertType({ + alerts, + config$: mockedConfig$, + }); + expect(alertExecutor).toBeDefined(); + + const scheduleActions = jest.fn(); + const services = { + callCluster: jest.fn(() => ({ + hits: { + total: { + value: 2, + }, + }, + aggregations: { + services: { + buckets: [ + { + key: 'foo', + }, + { + key: 'bar', + }, + ], + }, + }, + })), + alertInstanceFactory: jest.fn(() => ({ scheduleActions })), + }; + const params = { threshold: 1 }; + + await alertExecutor!({ services, params }); + ['apm.error_rate_foo', 'apm.error_rate_bar'].forEach((instanceName) => + expect(services.alertInstanceFactory).toHaveBeenCalledWith(instanceName) + ); + + expect(scheduleActions).toHaveBeenCalledWith('threshold_met', { + serviceName: 'foo', + environment: undefined, + threshold: 1, + triggerValue: 2, + }); + expect(scheduleActions).toHaveBeenCalledWith('threshold_met', { + serviceName: 'bar', + environment: undefined, + threshold: 1, + triggerValue: 2, + }); + }); +}); diff --git a/x-pack/plugins/apm/server/lib/alerts/register_error_count_alert_type.ts b/x-pack/plugins/apm/server/lib/alerts/register_error_count_alert_type.ts index a449ccfe15485..26e4a5e84b995 100644 --- a/x-pack/plugins/apm/server/lib/alerts/register_error_count_alert_type.ts +++ b/x-pack/plugins/apm/server/lib/alerts/register_error_count_alert_type.ts @@ -5,21 +5,22 @@ */ import { schema } from '@kbn/config-schema'; +import { isEmpty } from 'lodash'; import { Observable } from 'rxjs'; import { take } from 'rxjs/operators'; -import { ProcessorEvent } from '../../../common/processor_event'; -import { getEnvironmentUiFilterES } from '../helpers/convert_ui_filters/get_environment_ui_filter_es'; +import { APMConfig } from '../..'; +import { AlertingPlugin } from '../../../../alerts/server'; import { AlertType, ALERT_TYPES_CONFIG } from '../../../common/alert_types'; -import { ESSearchResponse } from '../../../typings/elasticsearch'; import { PROCESSOR_EVENT, + SERVICE_ENVIRONMENT, SERVICE_NAME, } from '../../../common/elasticsearch_fieldnames'; -import { AlertingPlugin } from '../../../../alerts/server'; +import { ProcessorEvent } from '../../../common/processor_event'; +import { ESSearchResponse } from '../../../typings/elasticsearch'; +import { getEnvironmentUiFilterES } from '../helpers/convert_ui_filters/get_environment_ui_filter_es'; import { getApmIndices } from '../settings/apm_indices/get_apm_indices'; -import { APMConfig } from '../..'; import { apmActionVariables } from './action_variables'; -import { getCommaSeparetedAggregationKey } from './utils'; interface RegisterAlertParams { alerts: AlertingPlugin['setup']; @@ -94,6 +95,13 @@ export function registerErrorCountAlertType({ field: SERVICE_NAME, size: 50, }, + aggs: { + environments: { + terms: { + field: SERVICE_ENVIRONMENT, + }, + }, + }, }, }, }, @@ -107,19 +115,41 @@ export function registerErrorCountAlertType({ const errorCount = response.hits.total.value; if (errorCount > alertParams.threshold) { - const alertInstance = services.alertInstanceFactory( - AlertType.ErrorCount - ); + function scheduleAction({ + serviceName, + environment, + }: { + serviceName: string; + environment?: string; + }) { + const alertInstanceName = [ + AlertType.ErrorCount, + serviceName, + environment, + ] + .filter((name) => name) + .join('_'); - alertInstance.scheduleActions(alertTypeConfig.defaultActionGroupId, { - serviceName: - alertParams.serviceName || - getCommaSeparetedAggregationKey( - response.aggregations?.services.buckets - ), - environment: alertParams.environment, - threshold: alertParams.threshold, - triggerValue: errorCount, + const alertInstance = services.alertInstanceFactory( + alertInstanceName + ); + alertInstance.scheduleActions(alertTypeConfig.defaultActionGroupId, { + serviceName, + environment, + threshold: alertParams.threshold, + triggerValue: errorCount, + }); + } + response.aggregations?.services.buckets.forEach((serviceBucket) => { + const serviceName = serviceBucket.key as string; + if (isEmpty(serviceBucket.environments?.buckets)) { + scheduleAction({ serviceName }); + } else { + serviceBucket.environments.buckets.forEach((envBucket) => { + const environment = envBucket.key as string; + scheduleAction({ serviceName, environment }); + }); + } }); } }, diff --git a/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_anomaly_alert_type.test.ts b/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_anomaly_alert_type.test.ts new file mode 100644 index 0000000000000..6e97262dd77bb --- /dev/null +++ b/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_anomaly_alert_type.test.ts @@ -0,0 +1,326 @@ +/* + * 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 { Observable } from 'rxjs'; +import * as Rx from 'rxjs'; +import { toArray, map } from 'rxjs/operators'; +import { AlertingPlugin } from '../../../../alerts/server'; +import { registerTransactionDurationAnomalyAlertType } from './register_transaction_duration_anomaly_alert_type'; +import { APMConfig } from '../..'; +import { ANOMALY_SEVERITY } from '../../../../ml/common'; +import { Job, MlPluginSetup } from '../../../../ml/server'; +import * as GetServiceAnomalies from '../service_map/get_service_anomalies'; + +type Operator = (source: Rx.Observable) => Rx.Observable; +const pipeClosure = (fn: Operator): Operator => { + return (source: Rx.Observable) => { + return Rx.defer(() => fn(source)); + }; +}; +const mockedConfig$ = (Rx.of('apm_oss.errorIndices').pipe( + pipeClosure((source$) => { + return source$.pipe(map((i) => i)); + }), + toArray() +) as unknown) as Observable; + +describe('Transaction duration anomaly alert', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + describe("doesn't send alert", () => { + it('ml is not defined', async () => { + let alertExecutor: any; + const alerts = { + registerType: ({ executor }) => { + alertExecutor = executor; + }, + } as AlertingPlugin['setup']; + + registerTransactionDurationAnomalyAlertType({ + alerts, + ml: undefined, + config$: mockedConfig$, + }); + expect(alertExecutor).toBeDefined(); + + const services = { + callCluster: jest.fn(), + alertInstanceFactory: jest.fn(), + }; + const params = { anomalySeverityType: ANOMALY_SEVERITY.MINOR }; + + await alertExecutor!({ services, params }); + expect(services.callCluster).not.toHaveBeenCalled(); + expect(services.alertInstanceFactory).not.toHaveBeenCalled(); + }); + + it('ml jobs are not available', async () => { + jest + .spyOn(GetServiceAnomalies, 'getMLJobs') + .mockReturnValue(Promise.resolve([])); + + let alertExecutor: any; + + const alerts = { + registerType: ({ executor }) => { + alertExecutor = executor; + }, + } as AlertingPlugin['setup']; + + const ml = ({ + mlSystemProvider: () => ({ mlAnomalySearch: jest.fn() }), + anomalyDetectorsProvider: jest.fn(), + } as unknown) as MlPluginSetup; + + registerTransactionDurationAnomalyAlertType({ + alerts, + ml, + config$: mockedConfig$, + }); + expect(alertExecutor).toBeDefined(); + + const services = { + callCluster: jest.fn(), + alertInstanceFactory: jest.fn(), + }; + const params = { anomalySeverityType: ANOMALY_SEVERITY.MINOR }; + + await alertExecutor!({ services, params }); + expect(services.callCluster).not.toHaveBeenCalled(); + expect(services.alertInstanceFactory).not.toHaveBeenCalled(); + }); + + it('anomaly is less than threshold', async () => { + jest + .spyOn(GetServiceAnomalies, 'getMLJobs') + .mockReturnValue( + Promise.resolve([{ job_id: '1' }, { job_id: '2' }] as Job[]) + ); + + let alertExecutor: any; + + const alerts = { + registerType: ({ executor }) => { + alertExecutor = executor; + }, + } as AlertingPlugin['setup']; + + const ml = ({ + mlSystemProvider: () => ({ + mlAnomalySearch: () => ({ + hits: { total: { value: 0 } }, + }), + }), + anomalyDetectorsProvider: jest.fn(), + } as unknown) as MlPluginSetup; + + registerTransactionDurationAnomalyAlertType({ + alerts, + ml, + config$: mockedConfig$, + }); + expect(alertExecutor).toBeDefined(); + + const services = { + callCluster: jest.fn(), + alertInstanceFactory: jest.fn(), + }; + const params = { anomalySeverityType: ANOMALY_SEVERITY.MINOR }; + + await alertExecutor!({ services, params }); + expect(services.callCluster).not.toHaveBeenCalled(); + expect(services.alertInstanceFactory).not.toHaveBeenCalled(); + }); + }); + + describe('sends alert', () => { + it('with service name, environment and transaction type', async () => { + jest.spyOn(GetServiceAnomalies, 'getMLJobs').mockReturnValue( + Promise.resolve([ + { + job_id: '1', + custom_settings: { + job_tags: { + environment: 'production', + }, + }, + } as unknown, + { + job_id: '2', + custom_settings: { + job_tags: { + environment: 'production', + }, + }, + } as unknown, + ] as Job[]) + ); + + let alertExecutor: any; + + const alerts = { + registerType: ({ executor }) => { + alertExecutor = executor; + }, + } as AlertingPlugin['setup']; + + const ml = ({ + mlSystemProvider: () => ({ + mlAnomalySearch: () => ({ + hits: { total: { value: 2 } }, + aggregations: { + services: { + buckets: [ + { + key: 'foo', + transaction_types: { + buckets: [{ key: 'type-foo' }], + }, + }, + { + key: 'bar', + transaction_types: { + buckets: [{ key: 'type-bar' }], + }, + }, + ], + }, + }, + }), + }), + anomalyDetectorsProvider: jest.fn(), + } as unknown) as MlPluginSetup; + + registerTransactionDurationAnomalyAlertType({ + alerts, + ml, + config$: mockedConfig$, + }); + expect(alertExecutor).toBeDefined(); + + const scheduleActions = jest.fn(); + const services = { + callCluster: jest.fn(), + alertInstanceFactory: jest.fn(() => ({ scheduleActions })), + }; + const params = { anomalySeverityType: ANOMALY_SEVERITY.MINOR }; + + await alertExecutor!({ services, params }); + + await alertExecutor!({ services, params }); + [ + 'apm.transaction_duration_anomaly_foo_production_type-foo', + 'apm.transaction_duration_anomaly_bar_production_type-bar', + ].forEach((instanceName) => + expect(services.alertInstanceFactory).toHaveBeenCalledWith(instanceName) + ); + + expect(scheduleActions).toHaveBeenCalledWith('threshold_met', { + serviceName: 'foo', + transactionType: 'type-foo', + environment: 'production', + }); + expect(scheduleActions).toHaveBeenCalledWith('threshold_met', { + serviceName: 'bar', + transactionType: 'type-bar', + environment: 'production', + }); + }); + + it('with service name', async () => { + jest.spyOn(GetServiceAnomalies, 'getMLJobs').mockReturnValue( + Promise.resolve([ + { + job_id: '1', + custom_settings: { + job_tags: { + environment: 'production', + }, + }, + } as unknown, + { + job_id: '2', + custom_settings: { + job_tags: { + environment: 'testing', + }, + }, + } as unknown, + ] as Job[]) + ); + + let alertExecutor: any; + + const alerts = { + registerType: ({ executor }) => { + alertExecutor = executor; + }, + } as AlertingPlugin['setup']; + + const ml = ({ + mlSystemProvider: () => ({ + mlAnomalySearch: () => ({ + hits: { total: { value: 2 } }, + aggregations: { + services: { + buckets: [{ key: 'foo' }, { key: 'bar' }], + }, + }, + }), + }), + anomalyDetectorsProvider: jest.fn(), + } as unknown) as MlPluginSetup; + + registerTransactionDurationAnomalyAlertType({ + alerts, + ml, + config$: mockedConfig$, + }); + expect(alertExecutor).toBeDefined(); + + const scheduleActions = jest.fn(); + const services = { + callCluster: jest.fn(), + alertInstanceFactory: jest.fn(() => ({ scheduleActions })), + }; + const params = { anomalySeverityType: ANOMALY_SEVERITY.MINOR }; + + await alertExecutor!({ services, params }); + + await alertExecutor!({ services, params }); + [ + 'apm.transaction_duration_anomaly_foo_production', + 'apm.transaction_duration_anomaly_foo_testing', + 'apm.transaction_duration_anomaly_bar_production', + 'apm.transaction_duration_anomaly_bar_testing', + ].forEach((instanceName) => + expect(services.alertInstanceFactory).toHaveBeenCalledWith(instanceName) + ); + + expect(scheduleActions).toHaveBeenCalledWith('threshold_met', { + serviceName: 'foo', + transactionType: undefined, + environment: 'production', + }); + expect(scheduleActions).toHaveBeenCalledWith('threshold_met', { + serviceName: 'bar', + transactionType: undefined, + environment: 'production', + }); + expect(scheduleActions).toHaveBeenCalledWith('threshold_met', { + serviceName: 'foo', + transactionType: undefined, + environment: 'testing', + }); + expect(scheduleActions).toHaveBeenCalledWith('threshold_met', { + serviceName: 'bar', + transactionType: undefined, + environment: 'testing', + }); + }); + }); +}); diff --git a/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_anomaly_alert_type.ts b/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_anomaly_alert_type.ts index a374bb022d0d3..36b7964e8128d 100644 --- a/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_anomaly_alert_type.ts +++ b/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_anomaly_alert_type.ts @@ -6,6 +6,7 @@ import { schema } from '@kbn/config-schema'; import { Observable } from 'rxjs'; +import { isEmpty } from 'lodash'; import { ANOMALY_SEVERITY } from '../../../../ml/common'; import { KibanaRequest } from '../../../../../../src/core/server'; import { @@ -16,9 +17,8 @@ import { import { AlertingPlugin } from '../../../../alerts/server'; import { APMConfig } from '../..'; import { MlPluginSetup } from '../../../../ml/server'; -import { getMLJobIds } from '../service_map/get_service_anomalies'; +import { getMLJobs } from '../service_map/get_service_anomalies'; import { apmActionVariables } from './action_variables'; -import { getCommaSeparetedAggregationKey } from './utils'; interface RegisterAlertParams { alerts: AlertingPlugin['setup']; @@ -73,10 +73,7 @@ export function registerTransactionDurationAnomalyAlertType({ const { mlAnomalySearch } = ml.mlSystemProvider(request); const anomalyDetectors = ml.anomalyDetectorsProvider(request); - const mlJobIds = await getMLJobIds( - anomalyDetectors, - alertParams.environment - ); + const mlJobs = await getMLJobs(anomalyDetectors, alertParams.environment); const selectedOption = ANOMALY_ALERT_SEVERITY_TYPES.find( (option) => option.type === alertParams.anomalySeverityType @@ -90,7 +87,7 @@ export function registerTransactionDurationAnomalyAlertType({ const threshold = selectedOption.threshold; - if (mlJobIds.length === 0) { + if (mlJobs.length === 0) { return {}; } @@ -102,7 +99,7 @@ export function registerTransactionDurationAnomalyAlertType({ bool: { filter: [ { term: { result_type: 'record' } }, - { terms: { job_id: mlJobIds } }, + { terms: { job_id: mlJobs.map((job) => job.job_id) } }, { range: { timestamp: { @@ -120,6 +117,15 @@ export function registerTransactionDurationAnomalyAlertType({ }, ] : []), + ...(alertParams.transactionType + ? [ + { + term: { + by_field_value: alertParams.transactionType, + }, + }, + ] + : []), { range: { record_score: { @@ -136,10 +142,12 @@ export function registerTransactionDurationAnomalyAlertType({ field: 'partition_field_value', size: 50, }, - }, - transaction_types: { - terms: { - field: 'by_field_value', + aggs: { + transaction_types: { + terms: { + field: 'by_field_value', + }, + }, }, }, }, @@ -151,29 +159,59 @@ export function registerTransactionDurationAnomalyAlertType({ )) as unknown) as { hits: { total: { value: number } }; aggregations?: { - services: { buckets: Array<{ key: string }> }; - transaction_types: { buckets: Array<{ key: string }> }; + services: { + buckets: Array<{ + key: string; + transaction_types: { buckets: Array<{ key: string }> }; + }>; + }; }; }; const hitCount = response.hits.total.value; if (hitCount > 0) { - const alertInstance = services.alertInstanceFactory( - AlertType.TransactionDurationAnomaly - ); - alertInstance.scheduleActions(alertTypeConfig.defaultActionGroupId, { - serviceName: - alertParams.serviceName || - getCommaSeparetedAggregationKey( - response.aggregations?.services.buckets - ), - transactionType: - alertParams.transactionType || - getCommaSeparetedAggregationKey( - response.aggregations?.transaction_types.buckets - ), - environment: alertParams.environment, + function scheduleAction({ + serviceName, + environment, + transactionType, + }: { + serviceName: string; + environment?: string; + transactionType?: string; + }) { + const alertInstanceName = [ + AlertType.TransactionDurationAnomaly, + serviceName, + environment, + transactionType, + ] + .filter((name) => name) + .join('_'); + + const alertInstance = services.alertInstanceFactory( + alertInstanceName + ); + alertInstance.scheduleActions(alertTypeConfig.defaultActionGroupId, { + serviceName, + environment, + transactionType, + }); + } + + mlJobs.map((job) => { + const environment = job.custom_settings?.job_tags?.environment; + response.aggregations?.services.buckets.forEach((serviceBucket) => { + const serviceName = serviceBucket.key as string; + if (isEmpty(serviceBucket.transaction_types?.buckets)) { + scheduleAction({ serviceName, environment }); + } else { + serviceBucket.transaction_types?.buckets.forEach((typeBucket) => { + const transactionType = typeBucket.key as string; + scheduleAction({ serviceName, environment, transactionType }); + }); + } + }); }); } }, diff --git a/x-pack/plugins/apm/server/lib/alerts/register_transaction_error_rate_alert_type.test.ts b/x-pack/plugins/apm/server/lib/alerts/register_transaction_error_rate_alert_type.test.ts new file mode 100644 index 0000000000000..90db48f84b5d9 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/alerts/register_transaction_error_rate_alert_type.test.ts @@ -0,0 +1,289 @@ +/* + * 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 { Observable } from 'rxjs'; +import * as Rx from 'rxjs'; +import { toArray, map } from 'rxjs/operators'; +import { AlertingPlugin } from '../../../../alerts/server'; +import { APMConfig } from '../..'; +import { registerTransactionErrorRateAlertType } from './register_transaction_error_rate_alert_type'; + +type Operator = (source: Rx.Observable) => Rx.Observable; +const pipeClosure = (fn: Operator): Operator => { + return (source: Rx.Observable) => { + return Rx.defer(() => fn(source)); + }; +}; +const mockedConfig$ = (Rx.of('apm_oss.errorIndices').pipe( + pipeClosure((source$) => { + return source$.pipe(map((i) => i)); + }), + toArray() +) as unknown) as Observable; + +describe('Transaction error rate alert', () => { + it("doesn't send an alert when rate is less than threshold", async () => { + let alertExecutor: any; + const alerts = { + registerType: ({ executor }) => { + alertExecutor = executor; + }, + } as AlertingPlugin['setup']; + + registerTransactionErrorRateAlertType({ + alerts, + config$: mockedConfig$, + }); + expect(alertExecutor).toBeDefined(); + + const services = { + callCluster: jest.fn(() => ({ + hits: { + total: { + value: 0, + }, + }, + })), + alertInstanceFactory: jest.fn(), + }; + const params = { threshold: 1 }; + + await alertExecutor!({ services, params }); + expect(services.alertInstanceFactory).not.toBeCalled(); + }); + + it('sends alerts with service name, transaction type and environment', async () => { + let alertExecutor: any; + const alerts = { + registerType: ({ executor }) => { + alertExecutor = executor; + }, + } as AlertingPlugin['setup']; + + registerTransactionErrorRateAlertType({ + alerts, + config$: mockedConfig$, + }); + expect(alertExecutor).toBeDefined(); + + const scheduleActions = jest.fn(); + const services = { + callCluster: jest.fn(() => ({ + hits: { + total: { + value: 4, + }, + }, + aggregations: { + erroneous_transactions: { + doc_count: 2, + }, + services: { + buckets: [ + { + key: 'foo', + transaction_types: { + buckets: [ + { + key: 'type-foo', + environments: { + buckets: [{ key: 'env-foo' }, { key: 'env-foo-2' }], + }, + }, + ], + }, + }, + { + key: 'bar', + transaction_types: { + buckets: [ + { + key: 'type-bar', + environments: { + buckets: [{ key: 'env-bar' }, { key: 'env-bar-2' }], + }, + }, + ], + }, + }, + ], + }, + }, + })), + alertInstanceFactory: jest.fn(() => ({ scheduleActions })), + }; + const params = { threshold: 10 }; + + await alertExecutor!({ services, params }); + [ + 'apm.transaction_error_rate_foo_type-foo_env-foo', + 'apm.transaction_error_rate_foo_type-foo_env-foo-2', + 'apm.transaction_error_rate_bar_type-bar_env-bar', + 'apm.transaction_error_rate_bar_type-bar_env-bar-2', + ].forEach((instanceName) => + expect(services.alertInstanceFactory).toHaveBeenCalledWith(instanceName) + ); + + expect(scheduleActions).toHaveBeenCalledWith('threshold_met', { + serviceName: 'foo', + transactionType: 'type-foo', + environment: 'env-foo', + threshold: 10, + triggerValue: 50, + }); + expect(scheduleActions).toHaveBeenCalledWith('threshold_met', { + serviceName: 'foo', + transactionType: 'type-foo', + environment: 'env-foo-2', + threshold: 10, + triggerValue: 50, + }); + expect(scheduleActions).toHaveBeenCalledWith('threshold_met', { + serviceName: 'bar', + transactionType: 'type-bar', + environment: 'env-bar', + threshold: 10, + triggerValue: 50, + }); + expect(scheduleActions).toHaveBeenCalledWith('threshold_met', { + serviceName: 'bar', + transactionType: 'type-bar', + environment: 'env-bar-2', + threshold: 10, + triggerValue: 50, + }); + }); + it('sends alerts with service name and transaction type', async () => { + let alertExecutor: any; + const alerts = { + registerType: ({ executor }) => { + alertExecutor = executor; + }, + } as AlertingPlugin['setup']; + + registerTransactionErrorRateAlertType({ + alerts, + config$: mockedConfig$, + }); + expect(alertExecutor).toBeDefined(); + + const scheduleActions = jest.fn(); + const services = { + callCluster: jest.fn(() => ({ + hits: { + total: { + value: 4, + }, + }, + aggregations: { + erroneous_transactions: { + doc_count: 2, + }, + services: { + buckets: [ + { + key: 'foo', + transaction_types: { + buckets: [{ key: 'type-foo' }], + }, + }, + { + key: 'bar', + transaction_types: { + buckets: [{ key: 'type-bar' }], + }, + }, + ], + }, + }, + })), + alertInstanceFactory: jest.fn(() => ({ scheduleActions })), + }; + const params = { threshold: 10 }; + + await alertExecutor!({ services, params }); + [ + 'apm.transaction_error_rate_foo_type-foo', + 'apm.transaction_error_rate_bar_type-bar', + ].forEach((instanceName) => + expect(services.alertInstanceFactory).toHaveBeenCalledWith(instanceName) + ); + + expect(scheduleActions).toHaveBeenCalledWith('threshold_met', { + serviceName: 'foo', + transactionType: 'type-foo', + environment: undefined, + threshold: 10, + triggerValue: 50, + }); + expect(scheduleActions).toHaveBeenCalledWith('threshold_met', { + serviceName: 'bar', + transactionType: 'type-bar', + environment: undefined, + threshold: 10, + triggerValue: 50, + }); + }); + + it('sends alerts with service name', async () => { + let alertExecutor: any; + const alerts = { + registerType: ({ executor }) => { + alertExecutor = executor; + }, + } as AlertingPlugin['setup']; + + registerTransactionErrorRateAlertType({ + alerts, + config$: mockedConfig$, + }); + expect(alertExecutor).toBeDefined(); + + const scheduleActions = jest.fn(); + const services = { + callCluster: jest.fn(() => ({ + hits: { + total: { + value: 4, + }, + }, + aggregations: { + erroneous_transactions: { + doc_count: 2, + }, + services: { + buckets: [{ key: 'foo' }, { key: 'bar' }], + }, + }, + })), + alertInstanceFactory: jest.fn(() => ({ scheduleActions })), + }; + const params = { threshold: 10 }; + + await alertExecutor!({ services, params }); + [ + 'apm.transaction_error_rate_foo', + 'apm.transaction_error_rate_bar', + ].forEach((instanceName) => + expect(services.alertInstanceFactory).toHaveBeenCalledWith(instanceName) + ); + + expect(scheduleActions).toHaveBeenCalledWith('threshold_met', { + serviceName: 'foo', + transactionType: undefined, + environment: undefined, + threshold: 10, + triggerValue: 50, + }); + expect(scheduleActions).toHaveBeenCalledWith('threshold_met', { + serviceName: 'bar', + transactionType: undefined, + environment: undefined, + threshold: 10, + triggerValue: 50, + }); + }); +}); diff --git a/x-pack/plugins/apm/server/lib/alerts/register_transaction_error_rate_alert_type.ts b/x-pack/plugins/apm/server/lib/alerts/register_transaction_error_rate_alert_type.ts index 5cd7c6f10c15d..e14360029e5dd 100644 --- a/x-pack/plugins/apm/server/lib/alerts/register_transaction_error_rate_alert_type.ts +++ b/x-pack/plugins/apm/server/lib/alerts/register_transaction_error_rate_alert_type.ts @@ -7,6 +7,7 @@ import { schema } from '@kbn/config-schema'; import { Observable } from 'rxjs'; import { take } from 'rxjs/operators'; +import { isEmpty } from 'lodash'; import { ProcessorEvent } from '../../../common/processor_event'; import { EventOutcome } from '../../../common/event_outcome'; import { AlertType, ALERT_TYPES_CONFIG } from '../../../common/alert_types'; @@ -16,13 +17,13 @@ import { SERVICE_NAME, TRANSACTION_TYPE, EVENT_OUTCOME, + SERVICE_ENVIRONMENT, } from '../../../common/elasticsearch_fieldnames'; import { AlertingPlugin } from '../../../../alerts/server'; import { getApmIndices } from '../settings/apm_indices/get_apm_indices'; import { APMConfig } from '../..'; import { getEnvironmentUiFilterES } from '../helpers/convert_ui_filters/get_environment_ui_filter_es'; import { apmActionVariables } from './action_variables'; -import { getCommaSeparetedAggregationKey } from './utils'; interface RegisterAlertParams { alerts: AlertingPlugin['setup']; @@ -110,8 +111,19 @@ export function registerTransactionErrorRateAlertType({ field: SERVICE_NAME, size: 50, }, + aggs: { + transaction_types: { + terms: { field: TRANSACTION_TYPE }, + aggs: { + environments: { + terms: { + field: SERVICE_ENVIRONMENT, + }, + }, + }, + }, + }, }, - transaction_types: { terms: { field: TRANSACTION_TYPE } }, }, }, }; @@ -132,26 +144,53 @@ export function registerTransactionErrorRateAlertType({ (errornousTransactionsCount / totalTransactionCount) * 100; if (transactionErrorRate > alertParams.threshold) { - const alertInstance = services.alertInstanceFactory( - AlertType.TransactionErrorRate - ); - - alertInstance.scheduleActions(alertTypeConfig.defaultActionGroupId, { - serviceName: - alertParams.serviceName || - getCommaSeparetedAggregationKey( - response.aggregations?.services.buckets - ), + function scheduleAction({ + serviceName, + environment, + transactionType, + }: { + serviceName: string; + environment?: string; + transactionType?: string; + }) { + const alertInstanceName = [ + AlertType.TransactionErrorRate, + serviceName, + transactionType, + environment, + ] + .filter((name) => name) + .join('_'); - transactionType: - alertParams.transactionType || - getCommaSeparetedAggregationKey( - response.aggregations?.transaction_types.buckets - ), + const alertInstance = services.alertInstanceFactory( + alertInstanceName + ); + alertInstance.scheduleActions(alertTypeConfig.defaultActionGroupId, { + serviceName, + transactionType, + environment, + threshold: alertParams.threshold, + triggerValue: transactionErrorRate, + }); + } - environment: alertParams.environment, - threshold: alertParams.threshold, - triggerValue: transactionErrorRate, + response.aggregations?.services.buckets.forEach((serviceBucket) => { + const serviceName = serviceBucket.key as string; + if (isEmpty(serviceBucket.transaction_types?.buckets)) { + scheduleAction({ serviceName }); + } else { + serviceBucket.transaction_types.buckets.forEach((typeBucket) => { + const transactionType = typeBucket.key as string; + if (isEmpty(typeBucket.environments?.buckets)) { + scheduleAction({ serviceName, transactionType }); + } else { + typeBucket.environments.buckets.forEach((envBucket) => { + const environment = envBucket.key as string; + scheduleAction({ serviceName, transactionType, environment }); + }); + } + }); + } }); } }, diff --git a/x-pack/plugins/apm/server/lib/alerts/utils.test.ts b/x-pack/plugins/apm/server/lib/alerts/utils.test.ts deleted file mode 100644 index 7a41005cff45b..0000000000000 --- a/x-pack/plugins/apm/server/lib/alerts/utils.test.ts +++ /dev/null @@ -1,15 +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 { getCommaSeparetedAggregationKey } from './utils'; - -describe('alerts utils', () => { - describe('getCommaSeparetedAggregationKey', () => { - it('returns comma separeted', () => { - const buckets = [{ key: 'foo' }, { key: 'bar' }]; - expect(getCommaSeparetedAggregationKey(buckets)).toEqual('foo,bar'); - }); - }); -}); diff --git a/x-pack/plugins/apm/server/lib/alerts/utils.ts b/x-pack/plugins/apm/server/lib/alerts/utils.ts deleted file mode 100644 index f31c1d8eb8893..0000000000000 --- a/x-pack/plugins/apm/server/lib/alerts/utils.ts +++ /dev/null @@ -1,11 +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 function getCommaSeparetedAggregationKey( - buckets: Array<{ key: any; doc_count?: number }> | undefined -) { - return buckets?.map((bucket) => bucket.key).join(','); -} diff --git a/x-pack/plugins/apm/server/lib/service_map/get_service_anomalies.ts b/x-pack/plugins/apm/server/lib/service_map/get_service_anomalies.ts index 44c0c96142096..895fc70d76af1 100644 --- a/x-pack/plugins/apm/server/lib/service_map/get_service_anomalies.ts +++ b/x-pack/plugins/apm/server/lib/service_map/get_service_anomalies.ts @@ -180,7 +180,7 @@ function transformResponseToServiceAnomalies( return serviceAnomaliesMap; } -export async function getMLJobIds( +export async function getMLJobs( anomalyDetectors: ReturnType, environment?: string ) { @@ -198,7 +198,15 @@ export async function getMLJobIds( if (!matchingMLJob) { return []; } - return [matchingMLJob.job_id]; + return [matchingMLJob]; } + return mlJobs; +} + +export async function getMLJobIds( + anomalyDetectors: ReturnType, + environment?: string +) { + const mlJobs = await getMLJobs(anomalyDetectors, environment); return mlJobs.map((job) => job.job_id); } From 0f12597580209ed132c648a50780437286b62c81 Mon Sep 17 00:00:00 2001 From: cauemarcondes Date: Mon, 28 Sep 2020 09:53:17 +0200 Subject: [PATCH 3/4] addressing PR comment --- .../TransactionDurationAlertTrigger/index.tsx | 10 +++++----- .../index.tsx | 10 ++++------ .../TransactionErrorRateAlertTrigger/index.tsx | 10 +++++----- .../public/components/alerting/fields.test.tsx | 17 ++++++++++++++--- .../apm/public/components/alerting/fields.tsx | 1 + .../alerting/get_alert_capabilities.ts} | 15 ++++++++------- .../index.tsx | 2 +- .../apm/public/components/app/Home/index.tsx | 12 ++++++------ .../index.tsx | 2 +- .../components/app/ServiceDetails/index.tsx | 15 +++++++-------- 10 files changed, 52 insertions(+), 42 deletions(-) rename x-pack/plugins/apm/public/{hooks/use_alerting_integrations.ts => components/alerting/get_alert_capabilities.ts} (68%) rename x-pack/plugins/apm/public/components/app/Home/{AlertIntegrations => alerting_popover_flyout}/index.tsx (98%) rename x-pack/plugins/apm/public/components/app/ServiceDetails/{AlertIntegrations => alerting_popover_flyout}/index.tsx (98%) diff --git a/x-pack/plugins/apm/public/components/alerting/TransactionDurationAlertTrigger/index.tsx b/x-pack/plugins/apm/public/components/alerting/TransactionDurationAlertTrigger/index.tsx index 3ddd623d9e848..ce98354c94c7e 100644 --- a/x-pack/plugins/apm/public/components/alerting/TransactionDurationAlertTrigger/index.tsx +++ b/x-pack/plugins/apm/public/components/alerting/TransactionDurationAlertTrigger/index.tsx @@ -90,16 +90,16 @@ export function TransactionDurationAlertTrigger(props: Props) { const fields = [ , - setAlertParams('environment', e.target.value)} - />, ({ text: key, value: key }))} onChange={(e) => setAlertParams('transactionType', e.target.value)} />, + setAlertParams('environment', e.target.value)} + />, (); const { start, end } = urlParams; const { environmentOptions } = useEnvironments({ serviceName, start, end }); - const supportedTransactionTypes = transactionTypes.filter((transactionType) => - [TRANSACTION_PAGE_LOAD, TRANSACTION_REQUEST].includes(transactionType) - ); - if (serviceName && !supportedTransactionTypes.length) { + if (serviceName && !transactionTypes.length) { return null; } - // 'page-load' for RUM, 'request' otherwise - const transactionType = supportedTransactionTypes[0]; + const transactionType = transactionTypes.find( + (type) => type === urlParams.transactionType + ); const defaults: Params = { windowSize: 15, diff --git a/x-pack/plugins/apm/public/components/alerting/TransactionErrorRateAlertTrigger/index.tsx b/x-pack/plugins/apm/public/components/alerting/TransactionErrorRateAlertTrigger/index.tsx index 7f4937ce35478..a9ad212393ac4 100644 --- a/x-pack/plugins/apm/public/components/alerting/TransactionErrorRateAlertTrigger/index.tsx +++ b/x-pack/plugins/apm/public/components/alerting/TransactionErrorRateAlertTrigger/index.tsx @@ -62,16 +62,16 @@ export function TransactionErrorRateAlertTrigger(props: Props) { const fields = [ , - setAlertParams('environment', e.target.value)} - />, ({ text: key, value: key }))} onChange={(e) => setAlertParams('transactionType', e.target.value)} />, + setAlertParams('environment', e.target.value)} + />, { @@ -25,10 +25,21 @@ describe('alerting fields', () => { { text: 'Foo', value: 'foo' }, { text: 'Bar', value: 'bar' }, ]; - const component = render( + const { getByText, getByTestId } = render( ); - expectTextsInDocument(component, ['Foo']); + + act(() => { + fireEvent.click(getByText('Foo')); + }); + + const selectBar = getByTestId('transactionTypeField'); + expect(selectBar instanceof HTMLSelectElement).toBeTruthy(); + const selectOptions = (selectBar as HTMLSelectElement).options; + expect(selectOptions.length).toEqual(2); + expect( + Object.values(selectOptions).map((option) => option.value) + ).toEqual(['foo', 'bar']); }); it('renders read-only field when single option available', () => { const options = [{ text: 'Bar', value: 'bar' }]; diff --git a/x-pack/plugins/apm/public/components/alerting/fields.tsx b/x-pack/plugins/apm/public/components/alerting/fields.tsx index 70bee132094db..d1acdeff6e605 100644 --- a/x-pack/plugins/apm/public/components/alerting/fields.tsx +++ b/x-pack/plugins/apm/public/components/alerting/fields.tsx @@ -74,6 +74,7 @@ export function TransactionTypeField({ return ( { - const plugin = useApmPluginContext(); - - const capabilities = plugin.core.application.capabilities; +export const getAlertingCapabilities = ( + plugins: ApmPluginSetupDeps, + capabilities: Capabilities +) => { const canReadAlerts = !!capabilities.apm['alerting:show']; const canSaveAlerts = !!capabilities.apm['alerting:save']; - const isAlertingPluginEnabled = 'alerts' in plugin.plugins; + const isAlertingPluginEnabled = 'alerts' in plugins; const isAlertingAvailable = isAlertingPluginEnabled && (canReadAlerts || canSaveAlerts); - const isMlPluginEnabled = 'ml' in plugin.plugins; + const isMlPluginEnabled = 'ml' in plugins; const canReadAnomalies = !!( isMlPluginEnabled && capabilities.ml.canAccessML && diff --git a/x-pack/plugins/apm/public/components/app/Home/AlertIntegrations/index.tsx b/x-pack/plugins/apm/public/components/app/Home/alerting_popover_flyout/index.tsx similarity index 98% rename from x-pack/plugins/apm/public/components/app/Home/AlertIntegrations/index.tsx rename to x-pack/plugins/apm/public/components/app/Home/alerting_popover_flyout/index.tsx index 6e3f734c1f97d..7e6331c1fa3a8 100644 --- a/x-pack/plugins/apm/public/components/app/Home/AlertIntegrations/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Home/alerting_popover_flyout/index.tsx @@ -51,7 +51,7 @@ interface Props { canReadAnomalies: boolean; } -export function AlertIntegrations(props: Props) { +export function AlertingPopoverAndFlyout(props: Props) { const { canSaveAlerts, canReadAlerts, canReadAnomalies } = props; const plugin = useApmPluginContext(); diff --git a/x-pack/plugins/apm/public/components/app/Home/index.tsx b/x-pack/plugins/apm/public/components/app/Home/index.tsx index 5924924650bed..446f7b978a434 100644 --- a/x-pack/plugins/apm/public/components/app/Home/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Home/index.tsx @@ -15,19 +15,19 @@ import { i18n } from '@kbn/i18n'; import React from 'react'; import { $ElementType } from 'utility-types'; import { useApmPluginContext } from '../../../hooks/useApmPluginContext'; +import { getAlertingCapabilities } from '../../alerting/get_alert_capabilities'; import { ApmHeader } from '../../shared/ApmHeader'; import { EuiTabLink } from '../../shared/EuiTabLink'; +import { AnomalyDetectionSetupLink } from '../../shared/Links/apm/AnomalyDetectionSetupLink'; import { ServiceMapLink } from '../../shared/Links/apm/ServiceMapLink'; import { ServiceOverviewLink } from '../../shared/Links/apm/ServiceOverviewLink'; import { SettingsLink } from '../../shared/Links/apm/SettingsLink'; -import { AnomalyDetectionSetupLink } from '../../shared/Links/apm/AnomalyDetectionSetupLink'; import { TraceOverviewLink } from '../../shared/Links/apm/TraceOverviewLink'; import { SetupInstructionsLink } from '../../shared/Links/SetupInstructionsLink'; import { ServiceMap } from '../ServiceMap'; import { ServiceOverview } from '../ServiceOverview'; import { TraceOverview } from '../TraceOverview'; -import { AlertIntegrations } from './AlertIntegrations'; -import { useAlertingIntegrations } from '../../../hooks/use_alerting_integrations'; +import { AlertingPopoverAndFlyout } from './alerting_popover_flyout'; function getHomeTabs({ serviceMapEnabled = true, @@ -85,7 +85,7 @@ interface Props { } export function Home({ tab }: Props) { - const { config, core } = useApmPluginContext(); + const { config, core, plugins } = useApmPluginContext(); const capabilities = core.application.capabilities; const canAccessML = !!capabilities.ml?.canAccessML; const homeTabs = getHomeTabs(config); @@ -98,7 +98,7 @@ export function Home({ tab }: Props) { canReadAlerts, canSaveAlerts, canReadAnomalies, - } = useAlertingIntegrations(); + } = getAlertingCapabilities(plugins, core.application.capabilities); return (
@@ -118,7 +118,7 @@ export function Home({ tab }: Props) { {isAlertingAvailable && ( - { @@ -24,14 +24,15 @@ interface Props extends RouteComponentProps<{ serviceName: string }> { } export function ServiceDetails({ match, tab }: Props) { - const plugin = useApmPluginContext(); + const { core, plugins } = useApmPluginContext(); const { serviceName } = match.params; + const { isAlertingAvailable, canReadAlerts, canSaveAlerts, canReadAnomalies, - } = useAlertingIntegrations(); + } = getAlertingCapabilities(plugins, core.application.capabilities); const ADD_DATA_LABEL = i18n.translate('xpack.apm.addDataButtonLabel', { defaultMessage: 'Add data', @@ -48,7 +49,7 @@ export function ServiceDetails({ match, tab }: Props) { {isAlertingAvailable && ( - Date: Mon, 28 Sep 2020 13:14:49 +0200 Subject: [PATCH 4/4] addressing PR comment --- .../index.tsx | 18 +++++++----------- .../apm/public/components/alerting/fields.tsx | 2 +- 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/x-pack/plugins/apm/public/components/alerting/TransactionDurationAnomalyAlertTrigger/index.tsx b/x-pack/plugins/apm/public/components/alerting/TransactionDurationAnomalyAlertTrigger/index.tsx index e56f1d00f39a4..f910f34d258fd 100644 --- a/x-pack/plugins/apm/public/components/alerting/TransactionDurationAnomalyAlertTrigger/index.tsx +++ b/x-pack/plugins/apm/public/components/alerting/TransactionDurationAnomalyAlertTrigger/index.tsx @@ -19,10 +19,6 @@ import { SelectAnomalySeverity, } from './SelectAnomalySeverity'; import { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values'; -import { - TRANSACTION_PAGE_LOAD, - TRANSACTION_REQUEST, -} from '../../../../common/transaction_types'; import { EnvironmentField, ServiceField, @@ -53,21 +49,17 @@ export function TransactionDurationAnomalyAlertTrigger(props: Props) { const { urlParams } = useUrlParams(); const transactionTypes = useServiceTransactionTypes(urlParams); const { serviceName } = useParams<{ serviceName?: string }>(); - const { start, end } = urlParams; + const { start, end, transactionType } = urlParams; const { environmentOptions } = useEnvironments({ serviceName, start, end }); if (serviceName && !transactionTypes.length) { return null; } - const transactionType = transactionTypes.find( - (type) => type === urlParams.transactionType - ); - const defaults: Params = { windowSize: 15, windowUnit: 'm', - transactionType, + transactionType: transactionType || transactionTypes[0], serviceName, environment: urlParams.environment || ENVIRONMENT_ALL.value, anomalySeverityType: ANOMALY_SEVERITY.CRITICAL, @@ -80,7 +72,11 @@ export function TransactionDurationAnomalyAlertTrigger(props: Props) { const fields = [ , - , + ({ text: key, value: key }))} + onChange={(e) => setAlertParams('transactionType', e.target.value)} + />, ) => void; }) {