[APM] Align APM severity levels with ML (#77818)

This commit is contained in:
Dario Gieselaar 2020-09-18 14:02:37 +02:00 committed by GitHub
parent b08594ad5c
commit 217276e8a9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 363 additions and 283 deletions

View file

@ -5,6 +5,8 @@
*/
import { i18n } from '@kbn/i18n';
import { ValuesType } from 'utility-types';
import { ANOMALY_SEVERITY, ANOMALY_THRESHOLD } from '../../ml/common';
export enum AlertType {
ErrorCount = 'apm.error_rate', // ErrorRate was renamed to ErrorCount but the key is kept as `error_rate` for backwards-compat.
@ -55,6 +57,41 @@ export const ALERT_TYPES_CONFIG = {
},
};
export const ANOMALY_ALERT_SEVERITY_TYPES = [
{
type: ANOMALY_SEVERITY.CRITICAL,
label: i18n.translate('xpack.apm.alerts.anomalySeverity.criticalLabel', {
defaultMessage: 'critical',
}),
threshold: ANOMALY_THRESHOLD.CRITICAL,
},
{
type: ANOMALY_SEVERITY.MAJOR,
label: i18n.translate('xpack.apm.alerts.anomalySeverity.majorLabel', {
defaultMessage: 'major',
}),
threshold: ANOMALY_THRESHOLD.MAJOR,
},
{
type: ANOMALY_SEVERITY.MINOR,
label: i18n.translate('xpack.apm.alerts.anomalySeverity.minor', {
defaultMessage: 'minor',
}),
threshold: ANOMALY_THRESHOLD.MINOR,
},
{
type: ANOMALY_SEVERITY.WARNING,
label: i18n.translate('xpack.apm.alerts.anomalySeverity.warningLabel', {
defaultMessage: 'warning',
}),
threshold: ANOMALY_THRESHOLD.WARNING,
},
] as const;
export type AnomalyAlertSeverityType = ValuesType<
typeof ANOMALY_ALERT_SEVERITY_TYPES
>['type'];
// Server side registrations
// x-pack/plugins/apm/server/lib/alerts/<alert>.ts
// x-pack/plugins/apm/server/lib/alerts/register_apm_alerts.ts

View file

@ -1,39 +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 { getSeverity, Severity } from './anomaly_detection';
describe('getSeverity', () => {
describe('when score is undefined', () => {
it('returns undefined', () => {
expect(getSeverity(undefined)).toEqual(undefined);
});
});
describe('when score < 25', () => {
it('returns warning', () => {
expect(getSeverity(10)).toEqual(Severity.warning);
});
});
describe('when score is between 25 and 50', () => {
it('returns minor', () => {
expect(getSeverity(40)).toEqual(Severity.minor);
});
});
describe('when score is between 50 and 75', () => {
it('returns major', () => {
expect(getSeverity(60)).toEqual(Severity.major);
});
});
describe('when score is 75 or more', () => {
it('returns critical', () => {
expect(getSeverity(100)).toEqual(Severity.critical);
});
});
});

View file

@ -5,89 +5,31 @@
*/
import { i18n } from '@kbn/i18n';
import { EuiTheme } from '../../../legacy/common/eui_styled_components';
import { ANOMALY_SEVERITY } from '../../ml/common';
import {
getSeverityType,
getSeverityColor as mlGetSeverityColor,
} from '../../ml/common';
import { ServiceHealthStatus } from './service_health_status';
export interface ServiceAnomalyStats {
transactionType?: string;
anomalyScore?: number;
actualValue?: number;
jobId?: string;
healthStatus: ServiceHealthStatus;
}
export enum Severity {
critical = 'critical',
major = 'major',
minor = 'minor',
warning = 'warning',
}
// TODO: Replace with `getSeverity` from:
// https://github.com/elastic/kibana/blob/0f964f66916480f2de1f4b633e5afafc08cf62a0/x-pack/plugins/ml/common/util/anomaly_utils.ts#L129
export function getSeverity(score?: number) {
if (typeof score !== 'number') {
return undefined;
} else if (score < 25) {
return Severity.warning;
} else if (score >= 25 && score < 50) {
return Severity.minor;
} else if (score >= 50 && score < 75) {
return Severity.major;
} else if (score >= 75) {
return Severity.critical;
} else {
return undefined;
export function getSeverity(score: number | undefined) {
if (score === undefined) {
return ANOMALY_SEVERITY.UNKNOWN;
}
return getSeverityType(score);
}
export function getSeverityColor(theme: EuiTheme, severity?: Severity) {
switch (severity) {
case Severity.warning:
return theme.eui.euiColorVis0;
case Severity.minor:
case Severity.major:
return theme.eui.euiColorVis5;
case Severity.critical:
return theme.eui.euiColorVis9;
default:
return;
}
}
export function getSeverityLabel(severity?: Severity) {
switch (severity) {
case Severity.critical:
return i18n.translate(
'xpack.apm.servicesTable.serviceHealthStatus.critical',
{
defaultMessage: 'Critical',
}
);
case Severity.major:
case Severity.minor:
return i18n.translate(
'xpack.apm.servicesTable.serviceHealthStatus.warning',
{
defaultMessage: 'Warning',
}
);
case Severity.warning:
return i18n.translate(
'xpack.apm.servicesTable.serviceHealthStatus.healthy',
{
defaultMessage: 'Healthy',
}
);
default:
return i18n.translate(
'xpack.apm.servicesTable.serviceHealthStatus.unknown',
{
defaultMessage: 'Unknown',
}
);
}
export function getSeverityColor(score: number) {
return mlGetSeverityColor(score);
}
export const ML_ERRORS = {

View file

@ -0,0 +1,79 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { i18n } from '@kbn/i18n';
import { ANOMALY_SEVERITY } from '../../ml/common';
import { EuiTheme } from '../../../legacy/common/eui_styled_components';
export enum ServiceHealthStatus {
healthy = 'healthy',
critical = 'critical',
warning = 'warning',
unknown = 'unknown',
}
export function getServiceHealthStatus({
severity,
}: {
severity: ANOMALY_SEVERITY;
}) {
switch (severity) {
case ANOMALY_SEVERITY.CRITICAL:
case ANOMALY_SEVERITY.MAJOR:
return ServiceHealthStatus.critical;
case ANOMALY_SEVERITY.MINOR:
case ANOMALY_SEVERITY.WARNING:
return ServiceHealthStatus.warning;
case ANOMALY_SEVERITY.LOW:
return ServiceHealthStatus.healthy;
case ANOMALY_SEVERITY.UNKNOWN:
return ServiceHealthStatus.unknown;
}
}
export function getServiceHealthStatusColor(
theme: EuiTheme,
status: ServiceHealthStatus
) {
switch (status) {
case ServiceHealthStatus.healthy:
return theme.eui.euiColorVis0;
case ServiceHealthStatus.warning:
return theme.eui.euiColorVis5;
case ServiceHealthStatus.critical:
return theme.eui.euiColorVis9;
case ServiceHealthStatus.unknown:
return theme.eui.euiColorMediumShade;
}
}
export function getServiceHealthStatusLabel(status: ServiceHealthStatus) {
switch (status) {
case ServiceHealthStatus.critical:
return i18n.translate('xpack.apm.serviceHealthStatus.critical', {
defaultMessage: 'Critical',
});
case ServiceHealthStatus.warning:
return i18n.translate('xpack.apm.serviceHealthStatus.warning', {
defaultMessage: 'Warning',
});
case ServiceHealthStatus.healthy:
return i18n.translate('xpack.apm.serviceHealthStatus.healthy', {
defaultMessage: 'Healthy',
});
case ServiceHealthStatus.unknown:
return i18n.translate('xpack.apm.serviceHealthStatus.unknown', {
defaultMessage: 'Unknown',
});
}
}

View file

@ -23,13 +23,19 @@
],
"server": true,
"ui": true,
"configPath": ["xpack", "apm"],
"extraPublicDirs": ["public/style/variables"],
"configPath": [
"xpack",
"apm"
],
"extraPublicDirs": [
"public/style/variables"
],
"requiredBundles": [
"kibanaReact",
"kibanaUtils",
"observability",
"home",
"maps"
"maps",
"ml"
]
}

View file

@ -5,105 +5,60 @@
*/
import React from 'react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import { EuiHealth, EuiSpacer, EuiSuperSelect, EuiText } from '@elastic/eui';
import { getSeverityColor } from '../../../../common/anomaly_detection';
import {
getSeverityColor,
Severity,
} from '../../../../common/anomaly_detection';
import { useTheme } from '../../../hooks/useTheme';
AnomalyAlertSeverityType,
ANOMALY_ALERT_SEVERITY_TYPES,
} from '../../../../common/alert_types';
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,
},
};
export function AnomalySeverity({
severityScore,
}: {
severityScore: SeverityScore;
}) {
const theme = useTheme();
const { label, severity } = anomalyScoreSeverityMap[severityScore];
const defaultColor = theme.eui.euiColorMediumShade;
const color = getSeverityColor(theme, severity) || defaultColor;
export function AnomalySeverity({ type }: { type: AnomalyAlertSeverityType }) {
const selectedOption = ANOMALY_ALERT_SEVERITY_TYPES.find(
(option) => option.type === type
)!;
return (
<EuiHealth color={color} style={{ lineHeight: 'inherit' }}>
{label}
<EuiHealth
color={getSeverityColor(selectedOption.threshold)}
style={{ lineHeight: 'inherit' }}
>
{selectedOption.label}
</EuiHealth>
);
}
const getOption = (value: SeverityScore) => {
return {
value: value.toString(10),
inputDisplay: <AnomalySeverity severityScore={value} />,
dropdownDisplay: (
<>
<AnomalySeverity severityScore={value} />
<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;
onChange: (value: AnomalyAlertSeverityType) => void;
value: AnomalyAlertSeverityType;
}
export function SelectAnomalySeverity({ onChange, value }: Props) {
const options = ANOMALY_SCORES.map((anomalyScore) => getOption(anomalyScore));
return (
<EuiSuperSelect
hasDividers
style={{ width: 200 }}
options={options}
valueOfSelected={value.toString(10)}
onChange={(selectedValue: string) => {
const selectedAnomalyScore = parseInt(
selectedValue,
10
) as SeverityScore;
onChange(selectedAnomalyScore);
options={ANOMALY_ALERT_SEVERITY_TYPES.map((option) => ({
value: option.type,
inputDisplay: <AnomalySeverity type={option.type} />,
dropdownDisplay: (
<>
<AnomalySeverity type={option.type} />
<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>
</>
),
}))}
valueOfSelected={value}
onChange={(selectedValue: AnomalyAlertSeverityType) => {
onChange(selectedValue);
}}
/>
);

View file

@ -7,6 +7,7 @@
import { useParams } from 'react-router-dom';
import { i18n } from '@kbn/i18n';
import React from 'react';
import { ANOMALY_SEVERITY } from '../../../../../ml/common';
import { ALERT_TYPES_CONFIG } from '../../../../common/alert_types';
import { useEnvironments } from '../../../hooks/useEnvironments';
import { useServiceTransactionTypes } from '../../../hooks/useServiceTransactionTypes';
@ -34,7 +35,11 @@ interface Params {
serviceName: string;
transactionType: string;
environment: string;
anomalyScore: 0 | 25 | 50 | 75;
anomalySeverityType:
| ANOMALY_SEVERITY.CRITICAL
| ANOMALY_SEVERITY.MAJOR
| ANOMALY_SEVERITY.MINOR
| ANOMALY_SEVERITY.WARNING;
}
interface Props {
@ -67,7 +72,7 @@ export function TransactionDurationAnomalyAlertTrigger(props: Props) {
transactionType,
serviceName,
environment: urlParams.environment || ENVIRONMENT_ALL.value,
anomalyScore: 75,
anomalySeverityType: ANOMALY_SEVERITY.CRITICAL,
};
const params = {
@ -84,7 +89,7 @@ export function TransactionDurationAnomalyAlertTrigger(props: Props) {
onChange={(e) => setAlertParams('environment', e.target.value)}
/>,
<PopoverExpression
value={<AnomalySeverity severityScore={params.anomalyScore} />}
value={<AnomalySeverity type={params.anomalySeverityType} />}
title={i18n.translate(
'xpack.apm.transactionDurationAnomalyAlertTrigger.anomalySeverity',
{
@ -93,9 +98,9 @@ export function TransactionDurationAnomalyAlertTrigger(props: Props) {
)}
>
<SelectAnomalySeverity
value={params.anomalyScore}
value={params.anomalySeverityType}
onChange={(value) => {
setAlertParams('anomalyScore', value);
setAlertParams('anomalySeverityType', value);
}}
/>
</PopoverExpression>,

View file

@ -14,6 +14,10 @@ import {
EuiIconTip,
EuiHealth,
} from '@elastic/eui';
import {
getServiceHealthStatus,
getServiceHealthStatusColor,
} from '../../../../../common/service_health_status';
import { useTheme } from '../../../../hooks/useTheme';
import { fontSize, px } from '../../../../style/variables';
import { asInteger, asDuration } from '../../../../utils/formatters';
@ -22,7 +26,6 @@ import { popoverWidth } from '../cytoscapeOptions';
import { TRANSACTION_REQUEST } from '../../../../../common/transaction_types';
import {
getSeverity,
getSeverityColor,
ServiceAnomalyStats,
} from '../../../../../common/anomaly_detection';
@ -59,13 +62,15 @@ export function AnomalyDetection({ serviceName, serviceAnomalyStats }: Props) {
const theme = useTheme();
const anomalyScore = serviceAnomalyStats?.anomalyScore;
const anomalySeverity = getSeverity(anomalyScore);
const severity = getSeverity(anomalyScore);
const actualValue = serviceAnomalyStats?.actualValue;
const mlJobId = serviceAnomalyStats?.jobId;
const transactionType =
serviceAnomalyStats?.transactionType ?? TRANSACTION_REQUEST;
const hasAnomalyDetectionScore = anomalyScore !== undefined;
const healthStatus = getServiceHealthStatus({ severity });
return (
<>
<section>
@ -81,7 +86,9 @@ export function AnomalyDetection({ serviceName, serviceAnomalyStats }: Props) {
<EuiFlexGroup>
<EuiFlexItem>
<VerticallyCentered>
<EuiHealth color={getSeverityColor(theme, anomalySeverity)} />
<EuiHealth
color={getServiceHealthStatusColor(theme, healthStatus)}
/>
<SubduedText>{ANOMALY_DETECTION_SCORE_METRIC}</SubduedText>
</VerticallyCentered>
</EuiFlexItem>

View file

@ -5,26 +5,26 @@
*/
import cytoscape from 'cytoscape';
import { CSSProperties } from 'react';
import {
getServiceHealthStatusColor,
ServiceHealthStatus,
} from '../../../../common/service_health_status';
import {
SERVICE_NAME,
SPAN_DESTINATION_SERVICE_RESOURCE,
} from '../../../../common/elasticsearch_fieldnames';
import { EuiTheme } from '../../../../../observability/public';
import { defaultIcon, iconForNode } from './icons';
import {
getSeverity,
getSeverityColor,
ServiceAnomalyStats,
Severity,
} from '../../../../common/anomaly_detection';
import { ServiceAnomalyStats } from '../../../../common/anomaly_detection';
export const popoverWidth = 280;
function getNodeSeverity(el: cytoscape.NodeSingular) {
function getServiceAnomalyStats(el: cytoscape.NodeSingular) {
const serviceAnomalyStats: ServiceAnomalyStats | undefined = el.data(
'serviceAnomalyStats'
);
return getSeverity(serviceAnomalyStats?.anomalyScore);
return serviceAnomalyStats;
}
function getBorderColorFn(
@ -32,10 +32,11 @@ function getBorderColorFn(
): cytoscape.Css.MapperFunction<cytoscape.NodeSingular, string> {
return (el: cytoscape.NodeSingular) => {
const hasAnomalyDetectionJob = el.data('serviceAnomalyStats') !== undefined;
const nodeSeverity = getNodeSeverity(el);
const anomalyStats = getServiceAnomalyStats(el);
if (hasAnomalyDetectionJob) {
return (
getSeverityColor(theme, nodeSeverity) || theme.eui.euiColorMediumShade
return getServiceHealthStatusColor(
theme,
anomalyStats?.healthStatus ?? ServiceHealthStatus.unknown
);
}
if (el.hasClass('primary') || el.selected()) {
@ -49,8 +50,8 @@ const getBorderStyle: cytoscape.Css.MapperFunction<
cytoscape.NodeSingular,
cytoscape.Css.LineStyle
> = (el: cytoscape.NodeSingular) => {
const nodeSeverity = getNodeSeverity(el);
if (nodeSeverity === Severity.critical) {
const status = getServiceAnomalyStats(el)?.healthStatus;
if (status === ServiceHealthStatus.critical) {
return 'double';
} else {
return 'solid';
@ -58,11 +59,11 @@ const getBorderStyle: cytoscape.Css.MapperFunction<
};
function getBorderWidth(el: cytoscape.NodeSingular) {
const nodeSeverity = getNodeSeverity(el);
const status = getServiceAnomalyStats(el)?.healthStatus;
if (nodeSeverity === Severity.minor || nodeSeverity === Severity.major) {
if (status === ServiceHealthStatus.warning) {
return 4;
} else if (nodeSeverity === Severity.critical) {
} else if (status === ServiceHealthStatus.critical) {
return 8;
} else {
return 4;

View file

@ -6,20 +6,22 @@
import React from 'react';
import { EuiBadge } from '@elastic/eui';
import {
getSeverityColor,
getSeverityLabel,
Severity,
} from '../../../../../common/anomaly_detection';
getServiceHealthStatusColor,
getServiceHealthStatusLabel,
ServiceHealthStatus,
} from '../../../../../common/service_health_status';
import { useTheme } from '../../../../hooks/useTheme';
export function HealthBadge({ severity }: { severity?: Severity }) {
export function HealthBadge({
healthStatus,
}: {
healthStatus: ServiceHealthStatus;
}) {
const theme = useTheme();
const unknownColor = theme.eui.euiColorLightShade;
return (
<EuiBadge color={getSeverityColor(theme, severity) ?? unknownColor}>
{getSeverityLabel(severity)}
<EuiBadge color={getServiceHealthStatusColor(theme, healthStatus)}>
{getServiceHealthStatusLabel(healthStatus)}
</EuiBadge>
);
}

View file

@ -9,6 +9,7 @@ import { shallow } from 'enzyme';
import { ServiceList, SERVICE_COLUMNS } from '../index';
import props from './props.json';
import { mockMoment } from '../../../../../utils/testHelpers';
import { ServiceHealthStatus } from '../../../../../../common/service_health_status';
describe('ServiceOverview -> List', () => {
beforeAll(() => {
@ -52,25 +53,28 @@ describe('ServiceOverview -> List', () => {
describe('without ML data', () => {
it('does not render health column', () => {
const wrapper = shallow(
<ServiceList items={props.items} displayHealthStatus={false} />
);
const wrapper = shallow(<ServiceList items={props.items} />);
const columns = wrapper.props().columns;
expect(columns[0].field).not.toBe('severity');
expect(columns[0].field).not.toBe('healthStatus');
});
});
describe('with ML data', () => {
it('renders health column', () => {
const wrapper = shallow(
<ServiceList items={props.items} displayHealthStatus />
<ServiceList
items={props.items.map((item) => ({
...item,
healthStatus: ServiceHealthStatus.warning,
}))}
/>
);
const columns = wrapper.props().columns;
expect(columns[0].field).toBe('severity');
expect(columns[0].field).toBe('healthStatus');
});
});
});

View file

@ -1,6 +1,10 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ServiceOverview -> List renders columns correctly 1`] = `<HealthBadge />`;
exports[`ServiceOverview -> List renders columns correctly 1`] = `
<HealthBadge
healthStatus="unknown"
/>
`;
exports[`ServiceOverview -> List renders empty state 1`] = `
<Memo(UnoptimizedManagedTable)
@ -51,7 +55,7 @@ exports[`ServiceOverview -> List renders empty state 1`] = `
}
initialPageSize={50}
initialSortDirection="desc"
initialSortField="severity"
initialSortField="healthStatus"
items={Array []}
sortFn={[Function]}
/>
@ -106,7 +110,7 @@ exports[`ServiceOverview -> List renders with data 1`] = `
}
initialPageSize={50}
initialSortDirection="desc"
initialSortField="severity"
initialSortField="healthStatus"
items={
Array [
Object {

View file

@ -10,6 +10,7 @@ import React from 'react';
import styled from 'styled-components';
import { ValuesType } from 'utility-types';
import { orderBy } from 'lodash';
import { ServiceHealthStatus } from '../../../../../common/service_health_status';
import { asPercent } from '../../../../../common/utils/formatters';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { ServiceListAPIResponse } from '../../../../../server/lib/services/get_services';
@ -20,14 +21,12 @@ import { ManagedTable, ITableColumn } from '../../../shared/ManagedTable';
import { EnvironmentBadge } from '../../../shared/EnvironmentBadge';
import { TransactionOverviewLink } from '../../../shared/Links/apm/TransactionOverviewLink';
import { AgentIcon } from '../../../shared/AgentIcon';
import { Severity } from '../../../../../common/anomaly_detection';
import { HealthBadge } from './HealthBadge';
import { ServiceListMetric } from './ServiceListMetric';
interface Props {
items: ServiceListAPIResponse['items'];
noItemsMessage?: React.ReactNode;
displayHealthStatus: boolean;
}
type ServiceListItem = ValuesType<Props['items']>;
@ -53,14 +52,18 @@ const AppLink = styled(TransactionOverviewLink)`
export const SERVICE_COLUMNS: Array<ITableColumn<ServiceListItem>> = [
{
field: 'severity',
field: 'healthStatus',
name: i18n.translate('xpack.apm.servicesTable.healthColumnLabel', {
defaultMessage: 'Health',
}),
width: px(unit * 6),
sortable: true,
render: (_, { severity }) => {
return <HealthBadge severity={severity} />;
render: (_, { healthStatus }) => {
return (
<HealthBadge
healthStatus={healthStatus ?? ServiceHealthStatus.unknown}
/>
);
},
},
{
@ -172,40 +175,38 @@ export const SERVICE_COLUMNS: Array<ITableColumn<ServiceListItem>> = [
},
];
const SEVERITY_ORDER = [
Severity.warning,
Severity.minor,
Severity.major,
Severity.critical,
const SERVICE_HEALTH_STATUS_ORDER = [
ServiceHealthStatus.unknown,
ServiceHealthStatus.healthy,
ServiceHealthStatus.warning,
ServiceHealthStatus.critical,
];
export function ServiceList({
items,
displayHealthStatus,
noItemsMessage,
}: Props) {
export function ServiceList({ items, noItemsMessage }: Props) {
const displayHealthStatus = items.some((item) => 'healthStatus' in item);
const columns = displayHealthStatus
? SERVICE_COLUMNS
: SERVICE_COLUMNS.filter((column) => column.field !== 'severity');
: SERVICE_COLUMNS.filter((column) => column.field !== 'healthStatus');
return (
<ManagedTable
columns={columns}
items={items}
noItemsMessage={noItemsMessage}
initialSortField="severity"
initialSortField="healthStatus"
initialSortDirection="desc"
initialPageSize={50}
sortFn={(itemsToSort, sortField, sortDirection) => {
// For severity, sort items by severity first, then by TPM
// For healthStatus, sort items by healthStatus first, then by TPM
return sortField === 'severity'
return sortField === 'healthStatus'
? orderBy(
itemsToSort,
[
(item) => {
return item.severity
? SEVERITY_ORDER.indexOf(item.severity)
return item.healthStatus
? SERVICE_HEALTH_STATUS_ORDER.indexOf(item.healthStatus)
: -1;
},
(item) => item.transactionsPerMinute?.value ?? 0,

View file

@ -10,6 +10,7 @@ import { merge } from 'lodash';
import React, { FunctionComponent, ReactChild } from 'react';
import { MemoryRouter } from 'react-router-dom';
import { createKibanaReactContext } from 'src/plugins/kibana_react/public';
import { ServiceHealthStatus } from '../../../../../common/service_health_status';
import { ServiceOverview } from '..';
import { EuiThemeProvider } from '../../../../../../observability/public';
import { ApmPluginContextValue } from '../../../../context/ApmPluginContext';
@ -114,7 +115,7 @@ describe('Service Overview -> View', () => {
errorsPerMinute: 200,
avgResponseTime: 300,
environments: ['test', 'dev'],
severity: 1,
healthStatus: ServiceHealthStatus.warning,
},
{
serviceName: 'My Go Service',
@ -123,7 +124,7 @@ describe('Service Overview -> View', () => {
errorsPerMinute: 500,
avgResponseTime: 600,
environments: [],
severity: 10,
severity: ServiceHealthStatus.healthy,
},
],
});
@ -252,7 +253,7 @@ describe('Service Overview -> View', () => {
errorsPerMinute: 200,
avgResponseTime: 300,
environments: ['test', 'dev'],
severity: 1,
healthStatus: ServiceHealthStatus.warning,
},
],
});

View file

@ -153,8 +153,8 @@ NodeList [
>
<span
class="euiBadge euiBadge--iconLeft"
style="background-color: rgb(211, 218, 230); color: rgb(0, 0, 0);"
title="Unknown"
style="background-color: rgb(214, 191, 87); color: rgb(0, 0, 0);"
title="Warning"
>
<span
class="euiBadge__content"
@ -162,7 +162,7 @@ NodeList [
<span
class="euiBadge__text"
>
Unknown
Warning
</span>
</span>
</span>
@ -435,7 +435,7 @@ NodeList [
>
<span
class="euiBadge euiBadge--iconLeft"
style="background-color: rgb(211, 218, 230); color: rgb(0, 0, 0);"
style="background-color: rgb(152, 162, 179); color: rgb(0, 0, 0);"
title="Unknown"
>
<span

View file

@ -114,8 +114,6 @@ export function ServiceOverview() {
canCreateJob &&
!userHasDismissedCallout;
const displayHealthStatus = data.items.some((item) => 'severity' in item);
return (
<>
<EuiSpacer />
@ -134,7 +132,6 @@ export function ServiceOverview() {
<EuiPanel>
<ServiceList
items={data.items}
displayHealthStatus={displayHealthStatus}
noItemsMessage={
<NoServicesMessage
historicalDataFound={data.hasHistoricalData}

View file

@ -6,8 +6,13 @@
import { schema } from '@kbn/config-schema';
import { Observable } from 'rxjs';
import { ANOMALY_SEVERITY } from '../../../../ml/common';
import { KibanaRequest } from '../../../../../../src/core/server';
import { AlertType, ALERT_TYPES_CONFIG } from '../../../common/alert_types';
import {
AlertType,
ALERT_TYPES_CONFIG,
ANOMALY_ALERT_SEVERITY_TYPES,
} from '../../../common/alert_types';
import { AlertingPlugin } from '../../../../alerts/server';
import { APMConfig } from '../..';
import { MlPluginSetup } from '../../../../ml/server';
@ -26,7 +31,12 @@ const paramsSchema = schema.object({
windowSize: schema.number(),
windowUnit: schema.string(),
environment: schema.string(),
anomalyScore: schema.number(),
anomalySeverityType: schema.oneOf([
schema.literal(ANOMALY_SEVERITY.CRITICAL),
schema.literal(ANOMALY_SEVERITY.MAJOR),
schema.literal(ANOMALY_SEVERITY.MINOR),
schema.literal(ANOMALY_SEVERITY.WARNING),
]),
});
const alertTypeConfig =
@ -67,6 +77,18 @@ export function registerTransactionDurationAnomalyAlertType({
alertParams.environment
);
const selectedOption = ANOMALY_ALERT_SEVERITY_TYPES.find(
(option) => option.type === alertParams.anomalySeverityType
);
if (!selectedOption) {
throw new Error(
`Anomaly alert severity type ${alertParams.anomalySeverityType} is not supported.`
);
}
const threshold = selectedOption.threshold;
if (mlJobIds.length === 0) {
return {};
}
@ -96,7 +118,7 @@ export function registerTransactionDurationAnomalyAlertType({
{
range: {
record_score: {
gte: alertParams.anomalyScore,
gte: threshold,
},
},
},

View file

@ -4,6 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import Boom from 'boom';
import { getServiceHealthStatus } from '../../../common/service_health_status';
import { Setup, SetupTimeRange } from '../helpers/setup_request';
import { PromiseReturnType } from '../../../typings/common';
import {
@ -12,6 +13,7 @@ import {
} from '../../../common/transaction_types';
import {
ServiceAnomalyStats,
getSeverity,
ML_ERRORS,
} from '../../../common/anomaly_detection';
import { getMlJobsWithAPMGroup } from '../anomaly_detection/get_ml_jobs_with_apm_group';
@ -130,13 +132,19 @@ function transformResponseToServiceAnomalies(
response.aggregations?.services.buckets ?? []
).reduce(
(statsByServiceName, { key: serviceName, top_score: topScoreAgg }) => {
const anomalyScore = topScoreAgg.hits.hits[0]?.sort?.[0];
const severity = getSeverity(anomalyScore);
const healthStatus = getServiceHealthStatus({ severity });
return {
...statsByServiceName,
[serviceName]: {
transactionType: topScoreAgg.hits.hits[0]?._source?.by_field_value,
anomalyScore: topScoreAgg.hits.hits[0]?.sort?.[0],
anomalyScore,
actualValue: topScoreAgg.hits.hits[0]?._source?.actual?.[0],
jobId: topScoreAgg.hits.hits[0]?._source?.job_id,
healthStatus,
},
};
},

View file

@ -4,6 +4,8 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { ServiceHealthStatus } from '../../../common/service_health_status';
import {
AGENT_NAME,
SERVICE_ENVIRONMENT,
@ -43,6 +45,7 @@ const anomalies = {
actualValue: 10000,
anomalyScore: 50,
jobId: 'apm-test-1234-ml-module-name',
healthStatus: ServiceHealthStatus.warning,
},
},
};

View file

@ -4,6 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { getServiceHealthStatus } from '../../../../common/service_health_status';
import { EventOutcome } from '../../../../common/event_outcome';
import { getSeverity } from '../../../../common/anomaly_detection';
import { AgentName } from '../../../../typings/es_schemas/ui/fields/agent';
@ -413,10 +414,11 @@ export const getHealthStatuses = async (
const stats = anomalies.serviceAnomalies[serviceName];
const severity = getSeverity(stats.anomalyScore);
const healthStatus = getServiceHealthStatus({ severity });
return {
serviceName,
severity,
healthStatus,
};
});
};

View file

@ -5,3 +5,5 @@
*/
export { SearchResponse7 } from './types/es_client';
export { ANOMALY_SEVERITY, ANOMALY_THRESHOLD } from './constants/anomalies';
export { getSeverityColor, getSeverityType } from './util/anomaly_utils';

View file

@ -35,5 +35,8 @@
"dashboard",
"savedObjects",
"home"
],
"extraPublicDirs": [
"common"
]
}

View file

@ -77,6 +77,7 @@ Array [
"serviceAnomalyStats": Object {
"actualValue": 3933482.1764705875,
"anomalyScore": 2.6101702751482714,
"healthStatus": "healthy",
"jobId": "apm-testing-d457-high_mean_transaction_duration",
"transactionType": "request",
},
@ -104,6 +105,7 @@ Array [
"serviceAnomalyStats": Object {
"actualValue": 3933482.1764705875,
"anomalyScore": 2.6101702751482714,
"healthStatus": "healthy",
"jobId": "apm-testing-d457-high_mean_transaction_duration",
"transactionType": "request",
},
@ -130,6 +132,7 @@ Array [
"serviceAnomalyStats": Object {
"actualValue": 3933482.1764705875,
"anomalyScore": 2.6101702751482714,
"healthStatus": "healthy",
"jobId": "apm-testing-d457-high_mean_transaction_duration",
"transactionType": "request",
},
@ -156,6 +159,7 @@ Array [
"serviceAnomalyStats": Object {
"actualValue": 3933482.1764705875,
"anomalyScore": 2.6101702751482714,
"healthStatus": "healthy",
"jobId": "apm-testing-d457-high_mean_transaction_duration",
"transactionType": "request",
},
@ -209,6 +213,7 @@ Array [
"serviceAnomalyStats": Object {
"actualValue": 3933482.1764705875,
"anomalyScore": 2.6101702751482714,
"healthStatus": "healthy",
"jobId": "apm-testing-d457-high_mean_transaction_duration",
"transactionType": "request",
},
@ -255,6 +260,7 @@ Array [
"serviceAnomalyStats": Object {
"actualValue": 684716.5813953485,
"anomalyScore": 0.20498907719907372,
"healthStatus": "healthy",
"jobId": "apm-production-229a-high_mean_transaction_duration",
"transactionType": "request",
},
@ -301,6 +307,7 @@ Array [
"serviceAnomalyStats": Object {
"actualValue": 3933482.1764705875,
"anomalyScore": 2.6101702751482714,
"healthStatus": "healthy",
"jobId": "apm-testing-d457-high_mean_transaction_duration",
"transactionType": "request",
},
@ -407,6 +414,7 @@ Array [
"serviceAnomalyStats": Object {
"actualValue": 3933482.1764705875,
"anomalyScore": 2.6101702751482714,
"healthStatus": "healthy",
"jobId": "apm-testing-d457-high_mean_transaction_duration",
"transactionType": "request",
},
@ -465,6 +473,7 @@ Array [
"serviceAnomalyStats": Object {
"actualValue": 684716.5813953485,
"anomalyScore": 0.20498907719907372,
"healthStatus": "healthy",
"jobId": "apm-production-229a-high_mean_transaction_duration",
"transactionType": "request",
},
@ -491,6 +500,7 @@ Array [
"serviceAnomalyStats": Object {
"actualValue": 684716.5813953485,
"anomalyScore": 0.20498907719907372,
"healthStatus": "healthy",
"jobId": "apm-production-229a-high_mean_transaction_duration",
"transactionType": "request",
},
@ -504,6 +514,7 @@ Array [
"serviceAnomalyStats": Object {
"actualValue": 3933482.1764705875,
"anomalyScore": 2.6101702751482714,
"healthStatus": "healthy",
"jobId": "apm-testing-d457-high_mean_transaction_duration",
"transactionType": "request",
},
@ -523,6 +534,7 @@ Array [
"serviceAnomalyStats": Object {
"actualValue": 684716.5813953485,
"anomalyScore": 0.20498907719907372,
"healthStatus": "healthy",
"jobId": "apm-production-229a-high_mean_transaction_duration",
"transactionType": "request",
},
@ -548,6 +560,7 @@ Array [
"serviceAnomalyStats": Object {
"actualValue": 684716.5813953485,
"anomalyScore": 0.20498907719907372,
"healthStatus": "healthy",
"jobId": "apm-production-229a-high_mean_transaction_duration",
"transactionType": "request",
},
@ -573,6 +586,7 @@ Array [
"serviceAnomalyStats": Object {
"actualValue": 684716.5813953485,
"anomalyScore": 0.20498907719907372,
"healthStatus": "healthy",
"jobId": "apm-production-229a-high_mean_transaction_duration",
"transactionType": "request",
},
@ -605,6 +619,7 @@ Array [
"serviceAnomalyStats": Object {
"actualValue": 3933482.1764705875,
"anomalyScore": 2.6101702751482714,
"healthStatus": "healthy",
"jobId": "apm-testing-d457-high_mean_transaction_duration",
"transactionType": "request",
},
@ -687,6 +702,7 @@ Array [
"serviceAnomalyStats": Object {
"actualValue": 684716.5813953485,
"anomalyScore": 0.20498907719907372,
"healthStatus": "healthy",
"jobId": "apm-production-229a-high_mean_transaction_duration",
"transactionType": "request",
},
@ -734,6 +750,7 @@ Array [
"serviceAnomalyStats": Object {
"actualValue": 3933482.1764705875,
"anomalyScore": 2.6101702751482714,
"healthStatus": "healthy",
"jobId": "apm-testing-d457-high_mean_transaction_duration",
"transactionType": "request",
},
@ -748,6 +765,7 @@ Array [
"serviceAnomalyStats": Object {
"actualValue": 684716.5813953485,
"anomalyScore": 0.20498907719907372,
"healthStatus": "healthy",
"jobId": "apm-production-229a-high_mean_transaction_duration",
"transactionType": "request",
},
@ -806,6 +824,7 @@ Object {
"serviceAnomalyStats": Object {
"actualValue": 3933482.1764705875,
"anomalyScore": 2.6101702751482714,
"healthStatus": "healthy",
"jobId": "apm-testing-d457-high_mean_transaction_duration",
"transactionType": "request",
},
@ -833,6 +852,7 @@ Object {
"serviceAnomalyStats": Object {
"actualValue": 3933482.1764705875,
"anomalyScore": 2.6101702751482714,
"healthStatus": "healthy",
"jobId": "apm-testing-d457-high_mean_transaction_duration",
"transactionType": "request",
},
@ -859,6 +879,7 @@ Object {
"serviceAnomalyStats": Object {
"actualValue": 3933482.1764705875,
"anomalyScore": 2.6101702751482714,
"healthStatus": "healthy",
"jobId": "apm-testing-d457-high_mean_transaction_duration",
"transactionType": "request",
},
@ -885,6 +906,7 @@ Object {
"serviceAnomalyStats": Object {
"actualValue": 3933482.1764705875,
"anomalyScore": 2.6101702751482714,
"healthStatus": "healthy",
"jobId": "apm-testing-d457-high_mean_transaction_duration",
"transactionType": "request",
},
@ -938,6 +960,7 @@ Object {
"serviceAnomalyStats": Object {
"actualValue": 3933482.1764705875,
"anomalyScore": 2.6101702751482714,
"healthStatus": "healthy",
"jobId": "apm-testing-d457-high_mean_transaction_duration",
"transactionType": "request",
},
@ -984,6 +1007,7 @@ Object {
"serviceAnomalyStats": Object {
"actualValue": 684716.5813953485,
"anomalyScore": 0.20498907719907372,
"healthStatus": "healthy",
"jobId": "apm-production-229a-high_mean_transaction_duration",
"transactionType": "request",
},
@ -1030,6 +1054,7 @@ Object {
"serviceAnomalyStats": Object {
"actualValue": 3933482.1764705875,
"anomalyScore": 2.6101702751482714,
"healthStatus": "healthy",
"jobId": "apm-testing-d457-high_mean_transaction_duration",
"transactionType": "request",
},
@ -1136,6 +1161,7 @@ Object {
"serviceAnomalyStats": Object {
"actualValue": 3933482.1764705875,
"anomalyScore": 2.6101702751482714,
"healthStatus": "healthy",
"jobId": "apm-testing-d457-high_mean_transaction_duration",
"transactionType": "request",
},
@ -1194,6 +1220,7 @@ Object {
"serviceAnomalyStats": Object {
"actualValue": 684716.5813953485,
"anomalyScore": 0.20498907719907372,
"healthStatus": "healthy",
"jobId": "apm-production-229a-high_mean_transaction_duration",
"transactionType": "request",
},
@ -1220,6 +1247,7 @@ Object {
"serviceAnomalyStats": Object {
"actualValue": 684716.5813953485,
"anomalyScore": 0.20498907719907372,
"healthStatus": "healthy",
"jobId": "apm-production-229a-high_mean_transaction_duration",
"transactionType": "request",
},
@ -1233,6 +1261,7 @@ Object {
"serviceAnomalyStats": Object {
"actualValue": 3933482.1764705875,
"anomalyScore": 2.6101702751482714,
"healthStatus": "healthy",
"jobId": "apm-testing-d457-high_mean_transaction_duration",
"transactionType": "request",
},
@ -1252,6 +1281,7 @@ Object {
"serviceAnomalyStats": Object {
"actualValue": 684716.5813953485,
"anomalyScore": 0.20498907719907372,
"healthStatus": "healthy",
"jobId": "apm-production-229a-high_mean_transaction_duration",
"transactionType": "request",
},
@ -1277,6 +1307,7 @@ Object {
"serviceAnomalyStats": Object {
"actualValue": 684716.5813953485,
"anomalyScore": 0.20498907719907372,
"healthStatus": "healthy",
"jobId": "apm-production-229a-high_mean_transaction_duration",
"transactionType": "request",
},
@ -1302,6 +1333,7 @@ Object {
"serviceAnomalyStats": Object {
"actualValue": 684716.5813953485,
"anomalyScore": 0.20498907719907372,
"healthStatus": "healthy",
"jobId": "apm-production-229a-high_mean_transaction_duration",
"transactionType": "request",
},
@ -1334,6 +1366,7 @@ Object {
"serviceAnomalyStats": Object {
"actualValue": 3933482.1764705875,
"anomalyScore": 2.6101702751482714,
"healthStatus": "healthy",
"jobId": "apm-testing-d457-high_mean_transaction_duration",
"transactionType": "request",
},
@ -1416,6 +1449,7 @@ Object {
"serviceAnomalyStats": Object {
"actualValue": 684716.5813953485,
"anomalyScore": 0.20498907719907372,
"healthStatus": "healthy",
"jobId": "apm-production-229a-high_mean_transaction_duration",
"transactionType": "request",
},
@ -1463,6 +1497,7 @@ Object {
"serviceAnomalyStats": Object {
"actualValue": 3933482.1764705875,
"anomalyScore": 2.6101702751482714,
"healthStatus": "healthy",
"jobId": "apm-testing-d457-high_mean_transaction_duration",
"transactionType": "request",
},
@ -1477,6 +1512,7 @@ Object {
"serviceAnomalyStats": Object {
"actualValue": 684716.5813953485,
"anomalyScore": 0.20498907719907372,
"healthStatus": "healthy",
"jobId": "apm-production-229a-high_mean_transaction_duration",
"transactionType": "request",
},

View file

@ -165,6 +165,7 @@ export default function serviceMapsApiTests({ getService }: FtrProviderContext)
"serviceAnomalyStats": Object {
"actualValue": 3933482.1764705875,
"anomalyScore": 2.6101702751482714,
"healthStatus": "healthy",
"jobId": "apm-testing-d457-high_mean_transaction_duration",
"transactionType": "request",
},
@ -179,6 +180,7 @@ export default function serviceMapsApiTests({ getService }: FtrProviderContext)
"serviceAnomalyStats": Object {
"actualValue": 684716.5813953485,
"anomalyScore": 0.20498907719907372,
"healthStatus": "healthy",
"jobId": "apm-production-229a-high_mean_transaction_duration",
"transactionType": "request",
},

View file

@ -45,24 +45,24 @@ export default function ApiTest({ getService }: FtrProviderContext) {
expect(response.body.items.length).to.be.greaterThan(0);
});
it('some items have severity set', () => {
it('some items have a health status set', () => {
// Under the assumption that the loaded archive has
// at least one APM ML job, and the time range is longer
// than 15m, at least one items should have severity set.
// Note that we currently have a bug where healthy services
// report as unknown (so without any severity status):
// than 15m, at least one items should have a health status
// set. Note that we currently have a bug where healthy
// services report as unknown (so without any health status):
// https://github.com/elastic/kibana/issues/77083
const severityScores = response.body.items.map((item: any) => item.severity);
const healthStatuses = response.body.items.map((item: any) => item.healthStatus);
expect(severityScores.filter(Boolean).length).to.be.greaterThan(0);
expect(healthStatuses.filter(Boolean).length).to.be.greaterThan(0);
expectSnapshot(severityScores).toMatchInline(`
expectSnapshot(healthStatuses).toMatchInline(`
Array [
undefined,
undefined,
"warning",
"warning",
"healthy",
"healthy",
undefined,
undefined,
undefined,