Skip to content

Commit

Permalink
Closes #72636. Adds alerting integration for APM transaction duration…
Browse files Browse the repository at this point in the history
… anomalies.
  • Loading branch information
ogupte committed Aug 22, 2020
1 parent f7cfcea commit ec08dca
Show file tree
Hide file tree
Showing 13 changed files with 512 additions and 29 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
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,11 @@ const CREATE_THRESHOLD_ALERT_PANEL_ID = 'create_threshold';
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 Down Expand Up @@ -105,6 +106,21 @@ export function AlertIntegrations(props: Props) {
setAlertType(AlertType.TransactionDuration);
},
},
...(canReadAnomalies
? [
{
name: i18n.translate(
'xpack.apm.serviceDetails.alertsMenu.transactionDurationAnomaly',
{
defaultMessage: 'Transaction duration anomaly',
}
),
onClick: () => {
setAlertType(AlertType.TransactionDurationAnomaly);
},
},
]
: []),
{
name: i18n.translate(
'xpack.apm.serviceDetails.alertsMenu.errorRate',
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
@@ -0,0 +1,113 @@
/*
* 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, { useState, useEffect } from 'react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import { EuiHealth, EuiSpacer, EuiSuperSelect, EuiText } from '@elastic/eui';
import { getSeverityColor } from '../../app/ServiceMap/cytoscapeOptions';
import { useTheme } from '../../../hooks/useTheme';
import { EuiTheme } from '../../../../../observability/public';
import { severity as Severity } from '../../app/ServiceMap/Popover/getSeverity';

type SeverityScore = 0 | 25 | 50 | 75;
const ANOMALY_SCORES: SeverityScore[] = [0, 25, 50, 75];

const anomalyScoreSeverityMap: {
[key in SeverityScore]: { label: string; severity: Severity };
} = {
0: {
label: i18n.translate('xpack.apm.alerts.anomalySeverity.warningLabel', {
defaultMessage: 'warning',
}),
severity: Severity.warning,
},
25: {
label: i18n.translate('xpack.apm.alerts.anomalySeverity.minorLabel', {
defaultMessage: 'minor',
}),
severity: Severity.minor,
},
50: {
label: i18n.translate('xpack.apm.alerts.anomalySeverity.majorLabel', {
defaultMessage: 'major',
}),
severity: Severity.major,
},
75: {
label: i18n.translate('xpack.apm.alerts.anomalySeverity.criticalLabel', {
defaultMessage: 'critical',
}),
severity: Severity.critical,
},
};

const getOption = (theme: EuiTheme, value: SeverityScore) => {
const { label, severity } = anomalyScoreSeverityMap[value];
const defaultColor = theme.eui.euiColorMediumShade;
const color = getSeverityColor(theme, severity) || defaultColor;
return {
value: value.toString(10),
inputDisplay: (
<>
<EuiHealth color={color} style={{ lineHeight: 'inherit' }}>
{label}
</EuiHealth>
</>
),
dropdownDisplay: (
<>
<EuiHealth color={color} style={{ lineHeight: 'inherit' }}>
{label}
</EuiHealth>
<EuiSpacer size="xs" />
<EuiText size="xs" color="subdued">
<p className="euiTextColor--subdued">
<FormattedMessage
id="xpack.apm.alerts.anomalySeverity.scoreDetailsDescription"
defaultMessage="score {value} and above"
values={{ value }}
/>
</p>
</EuiText>
</>
),
};
};

interface Props {
onChange: (value: SeverityScore) => void;
value: SeverityScore;
}

export function SelectAnomalySeverity({ onChange, value }: Props) {
const theme = useTheme();
const options = ANOMALY_SCORES.map((anomalyScore) =>
getOption(theme, anomalyScore)
);
const [anomalyScore, setAnomalyScore] = useState<SeverityScore>(value);

useEffect(() => {
setAnomalyScore(value);
}, [value]);

return (
<EuiSuperSelect
hasDividers
style={{ width: 200 }}
options={options}
valueOfSelected={anomalyScore.toString(10)}
onChange={(selectedValue: string) => {
const selectedAnomalyScore = parseInt(
selectedValue,
10
) as SeverityScore;
setAnomalyScore(selectedAnomalyScore);
onChange(selectedAnomalyScore);
}}
/>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
/*
* 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 { EuiText, EuiSelect, EuiExpression } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React from 'react';
import { ALERT_TYPES_CONFIG } from '../../../../common/alert_types';
import { ALL_OPTION, useEnvironments } from '../../../hooks/useEnvironments';
import { useServiceTransactionTypes } from '../../../hooks/useServiceTransactionTypes';
import { useUrlParams } from '../../../hooks/useUrlParams';
import { ServiceAlertTrigger } from '../ServiceAlertTrigger';
import { PopoverExpression } from '../ServiceAlertTrigger/PopoverExpression';
import { SelectAnomalySeverity } from './SelectAnomalySeverity';

interface Params {
windowSize: number;
windowUnit: string;
serviceName: string;
transactionType: string;
environment: string;
anomalyScore: 0 | 25 | 50 | 75;
}

interface Props {
alertParams: Params;
setAlertParams: (key: string, value: any) => void;
setAlertProperty: (key: string, value: any) => void;
}

export function TransactionDurationAnomalyAlertTrigger(props: Props) {
const { setAlertParams, alertParams, setAlertProperty } = props;
const { urlParams } = useUrlParams();
const transactionTypes = useServiceTransactionTypes(urlParams);
const { serviceName, start, end } = urlParams;
const { environmentOptions } = useEnvironments({ serviceName, start, end });

if (!transactionTypes.length || !serviceName) {
return null;
}

const defaults: Params = {
windowSize: 15,
windowUnit: 'm',
transactionType: transactionTypes[0],
serviceName,
environment: urlParams.environment || ALL_OPTION.value,
anomalyScore: 75,
};

const params = {
...defaults,
...alertParams,
};

const fields = [
<EuiExpression
description={i18n.translate(
'xpack.apm.transactionDurationAnomalyAlertTrigger.service',
{
defaultMessage: 'Service',
}
)}
value={
<EuiText className="eui-displayInlineBlock">
<h5>{serviceName}</h5>
</EuiText>
}
/>,
<PopoverExpression
value={
params.environment === ALL_OPTION.value
? ALL_OPTION.text
: params.environment
}
title={i18n.translate(
'xpack.apm.transactionDurationAnomalyAlertTrigger.environment',
{
defaultMessage: 'Environment',
}
)}
>
<EuiSelect
value={params.environment}
options={environmentOptions}
onChange={(e) =>
setAlertParams('environment', e.target.value as Params['environment'])
}
compressed
/>
</PopoverExpression>,
<PopoverExpression
value={params.transactionType}
title={i18n.translate(
'xpack.apm.transactionDurationAnomalyAlertTrigger.type',
{
defaultMessage: 'Type',
}
)}
>
<EuiSelect
value={params.transactionType}
options={transactionTypes.map((key) => {
return {
text: key,
value: key,
};
})}
onChange={(e) =>
setAlertParams(
'transactionType',
e.target.value as Params['transactionType']
)
}
compressed
/>
</PopoverExpression>,
<PopoverExpression
value={params.anomalyScore.toString(10)}
title={i18n.translate(
'xpack.apm.transactionDurationAnomalyAlertTrigger.anomalyScore',
{
defaultMessage: 'Has anomaly score',
}
)}
>
<SelectAnomalySeverity
value={params.anomalyScore}
onChange={(value) => {
setAlertParams('anomalyScore', value);
}}
/>
</PopoverExpression>,
];

return (
<ServiceAlertTrigger
alertTypeName={
ALERT_TYPES_CONFIG['apm.transaction_duration_anomaly'].name
}
fields={fields}
defaults={defaults}
setAlertParams={setAlertParams}
setAlertProperty={setAlertProperty}
/>
);
}

// Default export is required for React.lazy loading
//
// eslint-disable-next-line import/no-default-export
export default TransactionDurationAnomalyAlertTrigger;
Loading

0 comments on commit ec08dca

Please sign in to comment.