Skip to content

Commit

Permalink
[APM] Transaction duration anomaly alerting integration (#75719)
Browse files Browse the repository at this point in the history
* Closes #72636. Adds alerting integration for APM transaction duration anomalies.

* Code review feedback

* Display alert summary with the selected anomaly severity label instead of the anomaly score.

* - refactored ALL_OPTION and NOT_DEFINED_OPTION to be shared from common/environment_filter_values
- utilize getEnvironmentLabel in the alerting trigger components and added support for the 'All' label

* refactor get_all_environments to minimize exports and be more consistent and clean

* - Reorg the alerts menu for different alert types (threshold/anomaly)
- default environment alert settings to the selected filter

* - Filters default transaction type to only those supported in the APM anomaly detection jobs
- Removes Service name and transaction type from the set of expressions in the alerting setup

* - remove bell icon from alerts menu

* Adds target service back into the anomaly alert setup as a ready-only expression

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
  • Loading branch information
ogupte and elasticmachine authored Aug 28, 2020
1 parent 1b75690 commit b1cb8b8
Show file tree
Hide file tree
Showing 30 changed files with 577 additions and 122 deletions.
19 changes: 19 additions & 0 deletions x-pack/plugins/apm/common/alert_types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { i18n } from '@kbn/i18n';
export enum AlertType {
ErrorRate = 'apm.error_rate',
TransactionDuration = 'apm.transaction_duration',
TransactionDurationAnomaly = 'apm.transaction_duration_anomaly',
}

export const ALERT_TYPES_CONFIG = {
Expand Down Expand Up @@ -45,6 +46,24 @@ export const ALERT_TYPES_CONFIG = {
defaultActionGroupId: 'threshold_met',
producer: 'apm',
},
[AlertType.TransactionDurationAnomaly]: {
name: i18n.translate('xpack.apm.transactionDurationAnomalyAlert.name', {
defaultMessage: 'Transaction duration anomaly',
}),
actionGroups: [
{
id: 'threshold_met',
name: i18n.translate(
'xpack.apm.transactionDurationAlert.thresholdMet',
{
defaultMessage: 'Threshold met',
}
),
},
],
defaultActionGroupId: 'threshold_met',
producer: 'apm',
},
};

export const TRANSACTION_ALERT_AGGREGATION_TYPES = {
Expand Down
32 changes: 24 additions & 8 deletions x-pack/plugins/apm/common/environment_filter_values.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,30 @@

import { i18n } from '@kbn/i18n';

export const ENVIRONMENT_ALL = 'ENVIRONMENT_ALL';
export const ENVIRONMENT_NOT_DEFINED = 'ENVIRONMENT_NOT_DEFINED';
const ENVIRONMENT_ALL_VALUE = 'ENVIRONMENT_ALL';
const ENVIRONMENT_NOT_DEFINED_VALUE = 'ENVIRONMENT_NOT_DEFINED';

const environmentLabels: Record<string, string> = {
[ENVIRONMENT_ALL_VALUE]: i18n.translate(
'xpack.apm.filter.environment.allLabel',
{ defaultMessage: 'All' }
),
[ENVIRONMENT_NOT_DEFINED_VALUE]: i18n.translate(
'xpack.apm.filter.environment.notDefinedLabel',
{ defaultMessage: 'Not defined' }
),
};

export const ENVIRONMENT_ALL = {
value: ENVIRONMENT_ALL_VALUE,
text: environmentLabels[ENVIRONMENT_ALL_VALUE],
};

export const ENVIRONMENT_NOT_DEFINED = {
value: ENVIRONMENT_NOT_DEFINED_VALUE,
text: environmentLabels[ENVIRONMENT_NOT_DEFINED_VALUE],
};

export function getEnvironmentLabel(environment: string) {
if (environment === ENVIRONMENT_NOT_DEFINED) {
return i18n.translate('xpack.apm.filter.environment.notDefinedLabel', {
defaultMessage: 'Not defined',
});
}
return environment;
return environmentLabels[environment] || environment;
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,27 +18,37 @@ import { useApmPluginContext } from '../../../../hooks/useApmPluginContext';

const alertLabel = i18n.translate(
'xpack.apm.serviceDetails.alertsMenu.alerts',
{
defaultMessage: 'Alerts',
}
{ defaultMessage: 'Alerts' }
);
const transactionDurationLabel = i18n.translate(
'xpack.apm.serviceDetails.alertsMenu.transactionDuration',
{ defaultMessage: 'Transaction duration' }
);
const errorRateLabel = i18n.translate(
'xpack.apm.serviceDetails.alertsMenu.errorRate',
{ defaultMessage: 'Error rate' }
);

const createThresholdAlertLabel = i18n.translate(
'xpack.apm.serviceDetails.alertsMenu.createThresholdAlert',
{
defaultMessage: 'Create threshold alert',
}
{ defaultMessage: 'Create threshold alert' }
);
const createAnomalyAlertAlertLabel = i18n.translate(
'xpack.apm.serviceDetails.alertsMenu.createAnomalyAlert',
{ defaultMessage: 'Create anomaly alert' }
);

const CREATE_THRESHOLD_ALERT_PANEL_ID = 'create_threshold';
const CREATE_TRANSACTION_DURATION_ALERT_PANEL_ID =
'create_transaction_duration';
const CREATE_ERROR_RATE_ALERT_PANEL_ID = 'create_error_rate';

interface Props {
canReadAlerts: boolean;
canSaveAlerts: boolean;
canReadAnomalies: boolean;
}

export function AlertIntegrations(props: Props) {
const { canSaveAlerts, canReadAlerts } = props;
const { canSaveAlerts, canReadAlerts, canReadAnomalies } = props;

const plugin = useApmPluginContext();

Expand All @@ -52,9 +62,7 @@ export function AlertIntegrations(props: Props) {
iconSide="right"
onClick={() => setPopoverOpen(true)}
>
{i18n.translate('xpack.apm.serviceDetails.alertsMenu.alerts', {
defaultMessage: 'Alerts',
})}
{alertLabel}
</EuiButtonEmpty>
);

Expand All @@ -66,20 +74,18 @@ export function AlertIntegrations(props: Props) {
...(canSaveAlerts
? [
{
name: createThresholdAlertLabel,
panel: CREATE_THRESHOLD_ALERT_PANEL_ID,
icon: 'bell',
name: transactionDurationLabel,
panel: CREATE_TRANSACTION_DURATION_ALERT_PANEL_ID,
},
{ name: errorRateLabel, panel: CREATE_ERROR_RATE_ALERT_PANEL_ID },
]
: []),
...(canReadAlerts
? [
{
name: i18n.translate(
'xpack.apm.serviceDetails.alertsMenu.viewActiveAlerts',
{
defaultMessage: 'View active alerts',
}
{ defaultMessage: 'View active alerts' }
),
href: plugin.core.http.basePath.prepend(
'/app/management/insightsAndAlerting/triggersActions/alerts'
Expand All @@ -91,29 +97,38 @@ export function AlertIntegrations(props: Props) {
],
},
{
id: CREATE_THRESHOLD_ALERT_PANEL_ID,
title: createThresholdAlertLabel,
id: CREATE_TRANSACTION_DURATION_ALERT_PANEL_ID,
title: transactionDurationLabel,
items: [
{
name: i18n.translate(
'xpack.apm.serviceDetails.alertsMenu.transactionDuration',
{
defaultMessage: 'Transaction duration',
}
),
name: createThresholdAlertLabel,
onClick: () => {
setAlertType(AlertType.TransactionDuration);
setPopoverOpen(false);
},
},
...(canReadAnomalies
? [
{
name: createAnomalyAlertAlertLabel,
onClick: () => {
setAlertType(AlertType.TransactionDurationAnomaly);
setPopoverOpen(false);
},
},
]
: []),
],
},
{
id: CREATE_ERROR_RATE_ALERT_PANEL_ID,
title: errorRateLabel,
items: [
{
name: i18n.translate(
'xpack.apm.serviceDetails.alertsMenu.errorRate',
{
defaultMessage: 'Error rate',
}
),
name: createThresholdAlertLabel,
onClick: () => {
setAlertType(AlertType.ErrorRate);
setPopoverOpen(false);
},
},
],
Expand Down
24 changes: 13 additions & 11 deletions x-pack/plugins/apm/public/components/app/ServiceDetails/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,19 +26,18 @@ export function ServiceDetails({ tab }: Props) {
const plugin = useApmPluginContext();
const { urlParams } = useUrlParams();
const { serviceName } = urlParams;

const canReadAlerts = !!plugin.core.application.capabilities.apm[
'alerting:show'
];
const canSaveAlerts = !!plugin.core.application.capabilities.apm[
'alerting:save'
];
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 { core } = useApmPluginContext();
const isMlPluginEnabled = 'ml' in plugin.plugins;
const canReadAnomalies = !!(
isMlPluginEnabled &&
capabilities.ml.canAccessML &&
capabilities.ml.canGetJobs
);

const ADD_DATA_LABEL = i18n.translate('xpack.apm.addDataButtonLabel', {
defaultMessage: 'Add data',
Expand All @@ -58,12 +57,15 @@ export function ServiceDetails({ tab }: Props) {
<AlertIntegrations
canReadAlerts={canReadAlerts}
canSaveAlerts={canSaveAlerts}
canReadAnomalies={canReadAnomalies}
/>
</EuiFlexItem>
)}
<EuiFlexItem grow={false}>
<EuiButtonEmpty
href={core.http.basePath.prepend('/app/home#/tutorial/apm')}
href={plugin.core.http.basePath.prepend(
'/app/home#/tutorial/apm'
)}
size="s"
color="primary"
iconType="plusInCircle"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,14 @@ import {
ENVIRONMENT_ALL,
ENVIRONMENT_NOT_DEFINED,
} from '../../../../common/environment_filter_values';
import { useEnvironments, ALL_OPTION } from '../../../hooks/useEnvironments';
import { useEnvironments } from '../../../hooks/useEnvironments';

function updateEnvironmentUrl(
location: ReturnType<typeof useLocation>,
environment?: string
) {
const nextEnvironmentQueryParam =
environment !== ENVIRONMENT_ALL ? environment : undefined;
environment !== ENVIRONMENT_ALL.value ? environment : undefined;
history.push({
...location,
search: fromQuery({
Expand All @@ -32,13 +32,6 @@ function updateEnvironmentUrl(
});
}

const NOT_DEFINED_OPTION = {
value: ENVIRONMENT_NOT_DEFINED,
text: i18n.translate('xpack.apm.filter.environment.notDefinedLabel', {
defaultMessage: 'Not defined',
}),
};

const SEPARATOR_OPTION = {
text: `- ${i18n.translate(
'xpack.apm.filter.environment.selectEnvironmentLabel',
Expand All @@ -49,16 +42,16 @@ const SEPARATOR_OPTION = {

function getOptions(environments: string[]) {
const environmentOptions = environments
.filter((env) => env !== ENVIRONMENT_NOT_DEFINED)
.filter((env) => env !== ENVIRONMENT_NOT_DEFINED.value)
.map((environment) => ({
value: environment,
text: environment,
}));

return [
ALL_OPTION,
...(environments.includes(ENVIRONMENT_NOT_DEFINED)
? [NOT_DEFINED_OPTION]
ENVIRONMENT_ALL,
...(environments.includes(ENVIRONMENT_NOT_DEFINED.value)
? [ENVIRONMENT_NOT_DEFINED]
: []),
...(environmentOptions.length > 0 ? [SEPARATOR_OPTION] : []),
...environmentOptions,
Expand All @@ -83,7 +76,7 @@ export function EnvironmentFilter() {
defaultMessage: 'environment',
})}
options={getOptions(environments)}
value={environment || ENVIRONMENT_ALL}
value={environment || ENVIRONMENT_ALL.value}
onChange={(event) => {
updateEnvironmentUrl(location, event.target.value);
}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,12 @@ import { ForLastExpression } from '../../../../../triggers_actions_ui/public';
import { ALERT_TYPES_CONFIG } from '../../../../common/alert_types';
import { ServiceAlertTrigger } from '../ServiceAlertTrigger';
import { PopoverExpression } from '../ServiceAlertTrigger/PopoverExpression';
import { useEnvironments, ALL_OPTION } from '../../../hooks/useEnvironments';
import { useEnvironments } from '../../../hooks/useEnvironments';
import { useUrlParams } from '../../../hooks/useUrlParams';
import {
ENVIRONMENT_ALL,
getEnvironmentLabel,
} from '../../../../common/environment_filter_values';

export interface ErrorRateAlertTriggerParams {
windowSize: number;
Expand All @@ -39,7 +43,7 @@ export function ErrorRateAlertTrigger(props: Props) {
threshold: 25,
windowSize: 1,
windowUnit: 'm',
environment: ALL_OPTION.value,
environment: urlParams.environment || ENVIRONMENT_ALL.value,
};

const params = {
Expand All @@ -51,11 +55,7 @@ export function ErrorRateAlertTrigger(props: Props) {

const fields = [
<PopoverExpression
value={
params.environment === ALL_OPTION.value
? ALL_OPTION.text
: params.environment
}
value={getEnvironmentLabel(params.environment)}
title={i18n.translate('xpack.apm.errorRateAlertTrigger.environment', {
defaultMessage: 'Environment',
})}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ import React, { useState } from 'react';
import { EuiExpression, EuiPopover } from '@elastic/eui';

interface Props {
title: string;
value: string;
title: React.ReactNode;
value: React.ReactNode;
children?: React.ReactNode;
}

Expand Down
Loading

0 comments on commit b1cb8b8

Please sign in to comment.