Skip to content

Commit ec08dca

Browse files
committed
Closes #72636. Adds alerting integration for APM transaction duration anomalies.
1 parent f7cfcea commit ec08dca

File tree

13 files changed

+512
-29
lines changed

13 files changed

+512
-29
lines changed

x-pack/plugins/apm/common/alert_types.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { i18n } from '@kbn/i18n';
99
export enum AlertType {
1010
ErrorRate = 'apm.error_rate',
1111
TransactionDuration = 'apm.transaction_duration',
12+
TransactionDurationAnomaly = 'apm.transaction_duration_anomaly',
1213
}
1314

1415
export const ALERT_TYPES_CONFIG = {
@@ -45,6 +46,24 @@ export const ALERT_TYPES_CONFIG = {
4546
defaultActionGroupId: 'threshold_met',
4647
producer: 'apm',
4748
},
49+
[AlertType.TransactionDurationAnomaly]: {
50+
name: i18n.translate('xpack.apm.transactionDurationAnomalyAlert.name', {
51+
defaultMessage: 'Transaction duration anomaly',
52+
}),
53+
actionGroups: [
54+
{
55+
id: 'threshold_met',
56+
name: i18n.translate(
57+
'xpack.apm.transactionDurationAlert.thresholdMet',
58+
{
59+
defaultMessage: 'Threshold met',
60+
}
61+
),
62+
},
63+
],
64+
defaultActionGroupId: 'threshold_met',
65+
producer: 'apm',
66+
},
4867
};
4968

5069
export const TRANSACTION_ALERT_AGGREGATION_TYPES = {

x-pack/plugins/apm/public/components/app/ServiceDetails/AlertIntegrations/index.tsx

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,11 @@ const CREATE_THRESHOLD_ALERT_PANEL_ID = 'create_threshold';
3535
interface Props {
3636
canReadAlerts: boolean;
3737
canSaveAlerts: boolean;
38+
canReadAnomalies: boolean;
3839
}
3940

4041
export function AlertIntegrations(props: Props) {
41-
const { canSaveAlerts, canReadAlerts } = props;
42+
const { canSaveAlerts, canReadAlerts, canReadAnomalies } = props;
4243

4344
const plugin = useApmPluginContext();
4445

@@ -105,6 +106,21 @@ export function AlertIntegrations(props: Props) {
105106
setAlertType(AlertType.TransactionDuration);
106107
},
107108
},
109+
...(canReadAnomalies
110+
? [
111+
{
112+
name: i18n.translate(
113+
'xpack.apm.serviceDetails.alertsMenu.transactionDurationAnomaly',
114+
{
115+
defaultMessage: 'Transaction duration anomaly',
116+
}
117+
),
118+
onClick: () => {
119+
setAlertType(AlertType.TransactionDurationAnomaly);
120+
},
121+
},
122+
]
123+
: []),
108124
{
109125
name: i18n.translate(
110126
'xpack.apm.serviceDetails.alertsMenu.errorRate',

x-pack/plugins/apm/public/components/app/ServiceDetails/index.tsx

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -26,19 +26,18 @@ export function ServiceDetails({ tab }: Props) {
2626
const plugin = useApmPluginContext();
2727
const { urlParams } = useUrlParams();
2828
const { serviceName } = urlParams;
29-
30-
const canReadAlerts = !!plugin.core.application.capabilities.apm[
31-
'alerting:show'
32-
];
33-
const canSaveAlerts = !!plugin.core.application.capabilities.apm[
34-
'alerting:save'
35-
];
29+
const capabilities = plugin.core.application.capabilities;
30+
const canReadAlerts = !!capabilities.apm['alerting:show'];
31+
const canSaveAlerts = !!capabilities.apm['alerting:save'];
3632
const isAlertingPluginEnabled = 'alerts' in plugin.plugins;
37-
3833
const isAlertingAvailable =
3934
isAlertingPluginEnabled && (canReadAlerts || canSaveAlerts);
40-
41-
const { core } = useApmPluginContext();
35+
const isMlPluginEnabled = 'ml' in plugin.plugins;
36+
const canReadAnomalies = !!(
37+
isMlPluginEnabled &&
38+
capabilities.ml.canAccessML &&
39+
capabilities.ml.canGetJobs
40+
);
4241

4342
const ADD_DATA_LABEL = i18n.translate('xpack.apm.addDataButtonLabel', {
4443
defaultMessage: 'Add data',
@@ -58,12 +57,15 @@ export function ServiceDetails({ tab }: Props) {
5857
<AlertIntegrations
5958
canReadAlerts={canReadAlerts}
6059
canSaveAlerts={canSaveAlerts}
60+
canReadAnomalies={canReadAnomalies}
6161
/>
6262
</EuiFlexItem>
6363
)}
6464
<EuiFlexItem grow={false}>
6565
<EuiButtonEmpty
66-
href={core.http.basePath.prepend('/app/home#/tutorial/apm')}
66+
href={plugin.core.http.basePath.prepend(
67+
'/app/home#/tutorial/apm'
68+
)}
6769
size="s"
6870
color="primary"
6971
iconType="plusInCircle"
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
import React, { useState, useEffect } from 'react';
8+
import { i18n } from '@kbn/i18n';
9+
import { FormattedMessage } from '@kbn/i18n/react';
10+
import { EuiHealth, EuiSpacer, EuiSuperSelect, EuiText } from '@elastic/eui';
11+
import { getSeverityColor } from '../../app/ServiceMap/cytoscapeOptions';
12+
import { useTheme } from '../../../hooks/useTheme';
13+
import { EuiTheme } from '../../../../../observability/public';
14+
import { severity as Severity } from '../../app/ServiceMap/Popover/getSeverity';
15+
16+
type SeverityScore = 0 | 25 | 50 | 75;
17+
const ANOMALY_SCORES: SeverityScore[] = [0, 25, 50, 75];
18+
19+
const anomalyScoreSeverityMap: {
20+
[key in SeverityScore]: { label: string; severity: Severity };
21+
} = {
22+
0: {
23+
label: i18n.translate('xpack.apm.alerts.anomalySeverity.warningLabel', {
24+
defaultMessage: 'warning',
25+
}),
26+
severity: Severity.warning,
27+
},
28+
25: {
29+
label: i18n.translate('xpack.apm.alerts.anomalySeverity.minorLabel', {
30+
defaultMessage: 'minor',
31+
}),
32+
severity: Severity.minor,
33+
},
34+
50: {
35+
label: i18n.translate('xpack.apm.alerts.anomalySeverity.majorLabel', {
36+
defaultMessage: 'major',
37+
}),
38+
severity: Severity.major,
39+
},
40+
75: {
41+
label: i18n.translate('xpack.apm.alerts.anomalySeverity.criticalLabel', {
42+
defaultMessage: 'critical',
43+
}),
44+
severity: Severity.critical,
45+
},
46+
};
47+
48+
const getOption = (theme: EuiTheme, value: SeverityScore) => {
49+
const { label, severity } = anomalyScoreSeverityMap[value];
50+
const defaultColor = theme.eui.euiColorMediumShade;
51+
const color = getSeverityColor(theme, severity) || defaultColor;
52+
return {
53+
value: value.toString(10),
54+
inputDisplay: (
55+
<>
56+
<EuiHealth color={color} style={{ lineHeight: 'inherit' }}>
57+
{label}
58+
</EuiHealth>
59+
</>
60+
),
61+
dropdownDisplay: (
62+
<>
63+
<EuiHealth color={color} style={{ lineHeight: 'inherit' }}>
64+
{label}
65+
</EuiHealth>
66+
<EuiSpacer size="xs" />
67+
<EuiText size="xs" color="subdued">
68+
<p className="euiTextColor--subdued">
69+
<FormattedMessage
70+
id="xpack.apm.alerts.anomalySeverity.scoreDetailsDescription"
71+
defaultMessage="score {value} and above"
72+
values={{ value }}
73+
/>
74+
</p>
75+
</EuiText>
76+
</>
77+
),
78+
};
79+
};
80+
81+
interface Props {
82+
onChange: (value: SeverityScore) => void;
83+
value: SeverityScore;
84+
}
85+
86+
export function SelectAnomalySeverity({ onChange, value }: Props) {
87+
const theme = useTheme();
88+
const options = ANOMALY_SCORES.map((anomalyScore) =>
89+
getOption(theme, anomalyScore)
90+
);
91+
const [anomalyScore, setAnomalyScore] = useState<SeverityScore>(value);
92+
93+
useEffect(() => {
94+
setAnomalyScore(value);
95+
}, [value]);
96+
97+
return (
98+
<EuiSuperSelect
99+
hasDividers
100+
style={{ width: 200 }}
101+
options={options}
102+
valueOfSelected={anomalyScore.toString(10)}
103+
onChange={(selectedValue: string) => {
104+
const selectedAnomalyScore = parseInt(
105+
selectedValue,
106+
10
107+
) as SeverityScore;
108+
setAnomalyScore(selectedAnomalyScore);
109+
onChange(selectedAnomalyScore);
110+
}}
111+
/>
112+
);
113+
}
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
import { EuiText, EuiSelect, EuiExpression } from '@elastic/eui';
7+
import { i18n } from '@kbn/i18n';
8+
import React from 'react';
9+
import { ALERT_TYPES_CONFIG } from '../../../../common/alert_types';
10+
import { ALL_OPTION, useEnvironments } from '../../../hooks/useEnvironments';
11+
import { useServiceTransactionTypes } from '../../../hooks/useServiceTransactionTypes';
12+
import { useUrlParams } from '../../../hooks/useUrlParams';
13+
import { ServiceAlertTrigger } from '../ServiceAlertTrigger';
14+
import { PopoverExpression } from '../ServiceAlertTrigger/PopoverExpression';
15+
import { SelectAnomalySeverity } from './SelectAnomalySeverity';
16+
17+
interface Params {
18+
windowSize: number;
19+
windowUnit: string;
20+
serviceName: string;
21+
transactionType: string;
22+
environment: string;
23+
anomalyScore: 0 | 25 | 50 | 75;
24+
}
25+
26+
interface Props {
27+
alertParams: Params;
28+
setAlertParams: (key: string, value: any) => void;
29+
setAlertProperty: (key: string, value: any) => void;
30+
}
31+
32+
export function TransactionDurationAnomalyAlertTrigger(props: Props) {
33+
const { setAlertParams, alertParams, setAlertProperty } = props;
34+
const { urlParams } = useUrlParams();
35+
const transactionTypes = useServiceTransactionTypes(urlParams);
36+
const { serviceName, start, end } = urlParams;
37+
const { environmentOptions } = useEnvironments({ serviceName, start, end });
38+
39+
if (!transactionTypes.length || !serviceName) {
40+
return null;
41+
}
42+
43+
const defaults: Params = {
44+
windowSize: 15,
45+
windowUnit: 'm',
46+
transactionType: transactionTypes[0],
47+
serviceName,
48+
environment: urlParams.environment || ALL_OPTION.value,
49+
anomalyScore: 75,
50+
};
51+
52+
const params = {
53+
...defaults,
54+
...alertParams,
55+
};
56+
57+
const fields = [
58+
<EuiExpression
59+
description={i18n.translate(
60+
'xpack.apm.transactionDurationAnomalyAlertTrigger.service',
61+
{
62+
defaultMessage: 'Service',
63+
}
64+
)}
65+
value={
66+
<EuiText className="eui-displayInlineBlock">
67+
<h5>{serviceName}</h5>
68+
</EuiText>
69+
}
70+
/>,
71+
<PopoverExpression
72+
value={
73+
params.environment === ALL_OPTION.value
74+
? ALL_OPTION.text
75+
: params.environment
76+
}
77+
title={i18n.translate(
78+
'xpack.apm.transactionDurationAnomalyAlertTrigger.environment',
79+
{
80+
defaultMessage: 'Environment',
81+
}
82+
)}
83+
>
84+
<EuiSelect
85+
value={params.environment}
86+
options={environmentOptions}
87+
onChange={(e) =>
88+
setAlertParams('environment', e.target.value as Params['environment'])
89+
}
90+
compressed
91+
/>
92+
</PopoverExpression>,
93+
<PopoverExpression
94+
value={params.transactionType}
95+
title={i18n.translate(
96+
'xpack.apm.transactionDurationAnomalyAlertTrigger.type',
97+
{
98+
defaultMessage: 'Type',
99+
}
100+
)}
101+
>
102+
<EuiSelect
103+
value={params.transactionType}
104+
options={transactionTypes.map((key) => {
105+
return {
106+
text: key,
107+
value: key,
108+
};
109+
})}
110+
onChange={(e) =>
111+
setAlertParams(
112+
'transactionType',
113+
e.target.value as Params['transactionType']
114+
)
115+
}
116+
compressed
117+
/>
118+
</PopoverExpression>,
119+
<PopoverExpression
120+
value={params.anomalyScore.toString(10)}
121+
title={i18n.translate(
122+
'xpack.apm.transactionDurationAnomalyAlertTrigger.anomalyScore',
123+
{
124+
defaultMessage: 'Has anomaly score',
125+
}
126+
)}
127+
>
128+
<SelectAnomalySeverity
129+
value={params.anomalyScore}
130+
onChange={(value) => {
131+
setAlertParams('anomalyScore', value);
132+
}}
133+
/>
134+
</PopoverExpression>,
135+
];
136+
137+
return (
138+
<ServiceAlertTrigger
139+
alertTypeName={
140+
ALERT_TYPES_CONFIG['apm.transaction_duration_anomaly'].name
141+
}
142+
fields={fields}
143+
defaults={defaults}
144+
setAlertParams={setAlertParams}
145+
setAlertProperty={setAlertProperty}
146+
/>
147+
);
148+
}
149+
150+
// Default export is required for React.lazy loading
151+
//
152+
// eslint-disable-next-line import/no-default-export
153+
export default TransactionDurationAnomalyAlertTrigger;

0 commit comments

Comments
 (0)