From c7d8b6a63bcc769f23800b31b84c69c47dd57c49 Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Thu, 3 Sep 2020 13:05:09 +0200 Subject: [PATCH 01/17] [APM] Service inventory redesign Closes #75252. --- .../anomaly_detection.test.ts} | 10 +- .../plugins/apm/common/anomaly_detection.ts | 40 ++ .../ServiceMap/Popover/AnomalyDetection.tsx | 9 +- .../app/ServiceMap/Popover/getSeverity.ts | 30 -- .../app/ServiceMap/cytoscapeOptions.ts | 28 +- .../public/components/app/ServiceMap/icons.ts | 29 +- .../components/app/ServiceMap/icons/dark.svg | 1 - .../ServiceOverview/ServiceList/MLCallout.tsx | 57 +++ .../ServiceList/__test__/List.test.js | 52 ++- .../__test__/__snapshots__/List.test.js.snap | 80 ++-- .../ServiceList/__test__/props.json | 29 +- .../app/ServiceOverview/ServiceList/index.tsx | 262 ++++++++++-- .../__test__/ServiceOverview.test.tsx | 106 ++++- .../ServiceOverview.test.tsx.snap | 398 +++++++++++++++--- .../components/app/ServiceOverview/index.tsx | 59 ++- .../shared/AgentIcon/get_agent_icon.ts | 31 ++ .../AgentIcon}/icons/dot-net.svg | 0 .../AgentIcon}/icons/go.svg | 0 .../AgentIcon}/icons/java.svg | 0 .../AgentIcon}/icons/nodejs.svg | 0 .../AgentIcon}/icons/php.svg | 0 .../AgentIcon}/icons/python.svg | 0 .../AgentIcon}/icons/ruby.svg | 0 .../AgentIcon}/icons/rumjs.svg | 0 .../components/shared/AgentIcon/index.tsx | 21 + .../components/shared/ManagedTable/index.tsx | 18 +- .../SelectAnomalySeverity.tsx | 6 +- .../shared/charts/SparkPlot/index.tsx | 44 ++ .../hooks/useAnomalyDetectionSettings.ts | 18 + .../apm/public/hooks/useLocalStorage.ts | 52 +++ x-pack/plugins/apm/scripts/tsconfig.json | 3 +- ...transaction_duration_anomaly_alert_type.ts | 5 + .../lib/helpers/get_bucket_size/index.ts | 21 +- .../plugins/apm/server/lib/helpers/metrics.ts | 2 +- .../java/gc/fetch_and_transform_gc_metrics.ts | 2 +- .../lib/service_map/get_service_anomalies.ts | 18 +- .../get_service_map_service_node_info.test.ts | 3 + .../__snapshots__/queries.test.ts.snap | 44 ++ .../get_services/get_services_items.ts | 12 +- .../get_services/get_services_items_stats.ts | 103 ++++- .../server/lib/services/get_services/index.ts | 12 +- .../apm/server/lib/services/queries.test.ts | 2 +- .../lib/transaction_groups/get_error_rate.ts | 2 +- .../avg_duration_by_browser/fetcher.ts | 2 +- .../charts/get_anomaly_data/index.ts | 16 +- .../charts/get_timeseries_data/fetcher.ts | 2 +- .../charts/get_timeseries_data/index.ts | 2 +- x-pack/plugins/apm/server/routes/services.ts | 12 +- .../public/hooks/use_chart_theme.tsx | 5 +- .../observability/public/hooks/use_theme.tsx | 13 + x-pack/plugins/observability/public/index.ts | 3 + 51 files changed, 1340 insertions(+), 324 deletions(-) rename x-pack/plugins/apm/{public/components/app/ServiceMap/Popover/getSeverity.test.ts => common/anomaly_detection.test.ts} (74%) delete mode 100644 x-pack/plugins/apm/public/components/app/ServiceMap/Popover/getSeverity.ts delete mode 100644 x-pack/plugins/apm/public/components/app/ServiceMap/icons/dark.svg create mode 100644 x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/MLCallout.tsx create mode 100644 x-pack/plugins/apm/public/components/shared/AgentIcon/get_agent_icon.ts rename x-pack/plugins/apm/public/components/{app/ServiceMap => shared/AgentIcon}/icons/dot-net.svg (100%) rename x-pack/plugins/apm/public/components/{app/ServiceMap => shared/AgentIcon}/icons/go.svg (100%) rename x-pack/plugins/apm/public/components/{app/ServiceMap => shared/AgentIcon}/icons/java.svg (100%) rename x-pack/plugins/apm/public/components/{app/ServiceMap => shared/AgentIcon}/icons/nodejs.svg (100%) rename x-pack/plugins/apm/public/components/{app/ServiceMap => shared/AgentIcon}/icons/php.svg (100%) rename x-pack/plugins/apm/public/components/{app/ServiceMap => shared/AgentIcon}/icons/python.svg (100%) rename x-pack/plugins/apm/public/components/{app/ServiceMap => shared/AgentIcon}/icons/ruby.svg (100%) rename x-pack/plugins/apm/public/components/{app/ServiceMap => shared/AgentIcon}/icons/rumjs.svg (100%) create mode 100644 x-pack/plugins/apm/public/components/shared/AgentIcon/index.tsx create mode 100644 x-pack/plugins/apm/public/components/shared/charts/SparkPlot/index.tsx create mode 100644 x-pack/plugins/apm/public/hooks/useAnomalyDetectionSettings.ts create mode 100644 x-pack/plugins/apm/public/hooks/useLocalStorage.ts create mode 100644 x-pack/plugins/observability/public/hooks/use_theme.tsx diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/getSeverity.test.ts b/x-pack/plugins/apm/common/anomaly_detection.test.ts similarity index 74% rename from x-pack/plugins/apm/public/components/app/ServiceMap/Popover/getSeverity.test.ts rename to x-pack/plugins/apm/common/anomaly_detection.test.ts index 52b7d54236db66..21963b5300f833 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/getSeverity.test.ts +++ b/x-pack/plugins/apm/common/anomaly_detection.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getSeverity, severity } from './getSeverity'; +import { getSeverity, Severity } from './anomaly_detection'; describe('getSeverity', () => { describe('when score is undefined', () => { @@ -15,25 +15,25 @@ describe('getSeverity', () => { describe('when score < 25', () => { it('returns warning', () => { - expect(getSeverity(10)).toEqual(severity.warning); + expect(getSeverity(10)).toEqual(Severity.warning); }); }); describe('when score is between 25 and 50', () => { it('returns minor', () => { - expect(getSeverity(40)).toEqual(severity.minor); + expect(getSeverity(40)).toEqual(Severity.minor); }); }); describe('when score is between 50 and 75', () => { it('returns major', () => { - expect(getSeverity(60)).toEqual(severity.major); + expect(getSeverity(60)).toEqual(Severity.major); }); }); describe('when score is 75 or more', () => { it('returns critical', () => { - expect(getSeverity(100)).toEqual(severity.critical); + expect(getSeverity(100)).toEqual(Severity.critical); }); }); }); diff --git a/x-pack/plugins/apm/common/anomaly_detection.ts b/x-pack/plugins/apm/common/anomaly_detection.ts index 07270b572a4bee..41f66ed8ad9946 100644 --- a/x-pack/plugins/apm/common/anomaly_detection.ts +++ b/x-pack/plugins/apm/common/anomaly_detection.ts @@ -5,6 +5,7 @@ */ import { i18n } from '@kbn/i18n'; +import { EuiTheme } from '../../../legacy/common/eui_styled_components'; export interface ServiceAnomalyStats { transactionType?: string; @@ -13,6 +14,45 @@ export interface ServiceAnomalyStats { jobId?: string; } +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 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 const ML_ERRORS = { INVALID_LICENSE: i18n.translate( 'xpack.apm.anomaly_detection.error.invalid_license', diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/AnomalyDetection.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/AnomalyDetection.tsx index b3d19e1aab2cc9..5699d0b56219b7 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/AnomalyDetection.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/AnomalyDetection.tsx @@ -18,10 +18,13 @@ import { useTheme } from '../../../../hooks/useTheme'; import { fontSize, px } from '../../../../style/variables'; import { asInteger, asDuration } from '../../../../utils/formatters'; import { MLJobLink } from '../../../shared/Links/MachineLearningLinks/MLJobLink'; -import { getSeverityColor, popoverWidth } from '../cytoscapeOptions'; +import { popoverWidth } from '../cytoscapeOptions'; import { TRANSACTION_REQUEST } from '../../../../../common/transaction_types'; -import { ServiceAnomalyStats } from '../../../../../common/anomaly_detection'; -import { getSeverity } from './getSeverity'; +import { + getSeverity, + getSeverityColor, + ServiceAnomalyStats, +} from '../../../../../common/anomaly_detection'; const HealthStatusTitle = styled(EuiTitle)` display: inline; diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/getSeverity.ts b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/getSeverity.ts deleted file mode 100644 index f4eb2033e92311..00000000000000 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/getSeverity.ts +++ /dev/null @@ -1,30 +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 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; - } -} diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/cytoscapeOptions.ts b/x-pack/plugins/apm/public/components/app/ServiceMap/cytoscapeOptions.ts index 9fedcc70bbbcff..1ac7157cc2aad4 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/cytoscapeOptions.ts +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/cytoscapeOptions.ts @@ -11,25 +11,15 @@ import { } from '../../../../common/elasticsearch_fieldnames'; import { EuiTheme } from '../../../../../observability/public'; import { defaultIcon, iconForNode } from './icons'; -import { ServiceAnomalyStats } from '../../../../common/anomaly_detection'; -import { severity, getSeverity } from './Popover/getSeverity'; +import { + getSeverity, + getSeverityColor, + ServiceAnomalyStats, + Severity, +} from '../../../../common/anomaly_detection'; export const popoverWidth = 280; -export function getSeverityColor(theme: EuiTheme, nodeSeverity?: string) { - switch (nodeSeverity) { - 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; - } -} - function getNodeSeverity(el: cytoscape.NodeSingular) { const serviceAnomalyStats: ServiceAnomalyStats | undefined = el.data( 'serviceAnomalyStats' @@ -60,7 +50,7 @@ const getBorderStyle: cytoscape.Css.MapperFunction< cytoscape.Css.LineStyle > = (el: cytoscape.NodeSingular) => { const nodeSeverity = getNodeSeverity(el); - if (nodeSeverity === severity.critical) { + if (nodeSeverity === Severity.critical) { return 'double'; } else { return 'solid'; @@ -70,9 +60,9 @@ const getBorderStyle: cytoscape.Css.MapperFunction< function getBorderWidth(el: cytoscape.NodeSingular) { const nodeSeverity = getNodeSeverity(el); - if (nodeSeverity === severity.minor || nodeSeverity === severity.major) { + if (nodeSeverity === Severity.minor || nodeSeverity === Severity.major) { return 4; - } else if (nodeSeverity === severity.critical) { + } else if (nodeSeverity === Severity.critical) { return 8; } else { return 4; diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/icons.ts b/x-pack/plugins/apm/public/components/app/ServiceMap/icons.ts index 2f4cc0d39d71cf..c85cf85d387026 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/icons.ts +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/icons.ts @@ -5,7 +5,6 @@ */ import cytoscape from 'cytoscape'; -import { getNormalizedAgentName } from '../../../../common/agent_name'; import { AGENT_NAME, SPAN_SUBTYPE, @@ -13,29 +12,22 @@ import { } from '../../../../common/elasticsearch_fieldnames'; import awsIcon from './icons/aws.svg'; import cassandraIcon from './icons/cassandra.svg'; -import darkIcon from './icons/dark.svg'; import databaseIcon from './icons/database.svg'; import defaultIconImport from './icons/default.svg'; import documentsIcon from './icons/documents.svg'; -import dotNetIcon from './icons/dot-net.svg'; import elasticsearchIcon from './icons/elasticsearch.svg'; import globeIcon from './icons/globe.svg'; -import goIcon from './icons/go.svg'; import graphqlIcon from './icons/graphql.svg'; import grpcIcon from './icons/grpc.svg'; import handlebarsIcon from './icons/handlebars.svg'; -import javaIcon from './icons/java.svg'; import kafkaIcon from './icons/kafka.svg'; import mongodbIcon from './icons/mongodb.svg'; import mysqlIcon from './icons/mysql.svg'; -import nodeJsIcon from './icons/nodejs.svg'; -import phpIcon from './icons/php.svg'; import postgresqlIcon from './icons/postgresql.svg'; -import pythonIcon from './icons/python.svg'; import redisIcon from './icons/redis.svg'; -import rubyIcon from './icons/ruby.svg'; -import rumJsIcon from './icons/rumjs.svg'; import websocketIcon from './icons/websocket.svg'; +import javaIcon from '../../shared/AgentIcon/icons/java.svg'; +import { getAgentIcon } from '../../shared/AgentIcon/get_agent_icon'; export const defaultIcon = defaultIconImport; @@ -74,23 +66,6 @@ const typeIcons: { [key: string]: { [key: string]: string } } = { }, }; -const agentIcons: { [key: string]: string } = { - dark: darkIcon, - dotnet: dotNetIcon, - go: goIcon, - java: javaIcon, - 'js-base': rumJsIcon, - nodejs: nodeJsIcon, - php: phpIcon, - python: pythonIcon, - ruby: rubyIcon, -}; - -function getAgentIcon(agentName?: string) { - const normalizedAgentName = getNormalizedAgentName(agentName); - return normalizedAgentName && agentIcons[normalizedAgentName]; -} - function getSpanIcon(type?: string, subtype?: string) { if (!type) { return; diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/icons/dark.svg b/x-pack/plugins/apm/public/components/app/ServiceMap/icons/dark.svg deleted file mode 100644 index 9ae4b31c1a0d61..00000000000000 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/icons/dark.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/MLCallout.tsx b/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/MLCallout.tsx new file mode 100644 index 00000000000000..01c33d92c35ce3 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/MLCallout.tsx @@ -0,0 +1,57 @@ +/* + * 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 { EuiCallOut } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { EuiButton } from '@elastic/eui'; +import { EuiFlexItem } from '@elastic/eui'; +import { EuiFlexGrid } from '@elastic/eui'; +import { APMLink } from '../../../shared/Links/apm/APMLink'; + +export function MLCallout({ onDismiss }: { onDismiss: () => void }) { + return ( + +

+ {i18n.translate('xpack.apm.serviceOverview.mlNudgeMessage.content', { + defaultMessage: `Our integration with ML anomaly detection will enable you to see your services' health status`, + })} +

+ + + + + {i18n.translate( + 'xpack.apm.serviceOverview.mlNudgeMessage.learnMoreButton', + { + defaultMessage: `Learn more`, + } + )} + + + + + onDismiss()}> + {i18n.translate( + 'xpack.apm.serviceOverview.mlNudgeMessage.dismissButton', + { + defaultMessage: `Dismiss message`, + } + )} + + + +
+ ); +} diff --git a/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/__test__/List.test.js b/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/__test__/List.test.js index 927779b571fd8c..f79f616b591251 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/__test__/List.test.js +++ b/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/__test__/List.test.js @@ -15,34 +15,62 @@ describe('ServiceOverview -> List', () => { mockMoment(); }); - it('should render empty state', () => { + it('renders empty state', () => { const wrapper = shallow(); expect(wrapper).toMatchSnapshot(); }); - it('should render with data', () => { + it('renders with data', () => { const wrapper = shallow(); expect(wrapper).toMatchSnapshot(); }); - it('should render columns correctly', () => { + it('renders columns correctly', () => { const service = { serviceName: 'opbeans-python', agentName: 'python', - transactionsPerMinute: 86.93333333333334, - errorsPerMinute: 12.6, - avgResponseTime: 91535.42944785276, + transactionsPerMinute: { + value: 86.93333333333334, + over_time: [], + }, + errorsPerMinute: { + value: 12.6, + over_time: [], + }, + avgResponseTime: { + value: 91535.42944785276, + over_time: [], + }, environments: ['test'], }; const renderedColumns = SERVICE_COLUMNS.map((c) => c.render(service[c.field], service) ); + expect(renderedColumns[0]).toMatchSnapshot(); - expect(renderedColumns.slice(2)).toEqual([ - 'python', - '92 ms', - '86.9 tpm', - '12.6 err.', - ]); + }); + + describe('without ML data', () => { + it('does not render health column', () => { + const wrapper = shallow( + + ); + + const columns = wrapper.props().columns; + + expect(columns[0].field).not.toBe('severity'); + }); + }); + + describe('with ML data', () => { + it('renders health column', () => { + const wrapper = shallow( + + ); + + const columns = wrapper.props().columns; + + expect(columns[0].field).toBe('severity'); + }); }); }); diff --git a/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/__test__/__snapshots__/List.test.js.snap b/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/__test__/__snapshots__/List.test.js.snap index 146f6f58031bb7..15e20095382703 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/__test__/__snapshots__/List.test.js.snap +++ b/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/__test__/__snapshots__/List.test.js.snap @@ -1,21 +1,8 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`ServiceOverview -> List should render columns correctly 1`] = ` - - - opbeans-python - - -`; +exports[`ServiceOverview -> List renders columns correctly 1`] = ``; -exports[`ServiceOverview -> List should render empty state 1`] = ` +exports[`ServiceOverview -> List renders empty state 1`] = ` List should render empty state 1`] = ` "name": "Environment", "render": [Function], "sortable": true, - "width": "20%", - }, - Object { - "field": "agentName", - "name": "Agent", - "render": [Function], - "sortable": true, + "width": "160px", }, Object { + "align": "left", "dataType": "number", "field": "avgResponseTime", "name": "Avg. response time", "render": [Function], "sortable": true, + "width": "160px", }, Object { + "align": "left", "dataType": "number", "field": "transactionsPerMinute", "name": "Trans. per minute", "render": [Function], "sortable": true, + "width": "160px", }, Object { + "align": "left", "dataType": "number", "field": "errorsPerMinute", "name": "Errors per minute", "render": [Function], "sortable": true, + "width": "160px", }, ] } initialPageSize={50} - initialSortField="serviceName" + initialSortDirection="desc" + initialSortField="severity" items={Array []} + sortFn={[Function]} /> `; -exports[`ServiceOverview -> List should render with data 1`] = ` +exports[`ServiceOverview -> List renders with data 1`] = ` List should render with data 1`] = ` "name": "Environment", "render": [Function], "sortable": true, - "width": "20%", - }, - Object { - "field": "agentName", - "name": "Agent", - "render": [Function], - "sortable": true, + "width": "160px", }, Object { + "align": "left", "dataType": "number", "field": "avgResponseTime", "name": "Avg. response time", "render": [Function], "sortable": true, + "width": "160px", }, Object { + "align": "left", "dataType": "number", "field": "transactionsPerMinute", "name": "Trans. per minute", "render": [Function], "sortable": true, + "width": "160px", }, Object { + "align": "left", "dataType": "number", "field": "errorsPerMinute", "name": "Errors per minute", "render": [Function], "sortable": true, + "width": "160px", }, ] } initialPageSize={50} - initialSortField="serviceName" + initialSortDirection="desc" + initialSortField="severity" items={ Array [ Object { @@ -125,19 +115,35 @@ exports[`ServiceOverview -> List should render with data 1`] = ` "environments": Array [ "test", ], - "errorsPerMinute": 46.06666666666667, + "errorsPerMinute": Object { + "over_time": Array [], + "value": 46.06666666666667, + }, "serviceName": "opbeans-node", - "transactionsPerMinute": 0, + "transactionsPerMinute": Object { + "over_time": Array [], + "value": 0, + }, }, Object { "agentName": "python", - "avgResponseTime": 91535.42944785276, + "avgResponseTime": Object { + "over_time": Array [], + "value": 91535.42944785276, + }, "environments": Array [], - "errorsPerMinute": 12.6, + "errorsPerMinute": Object { + "over_time": Array [], + "value": 12.6, + }, "serviceName": "opbeans-python", - "transactionsPerMinute": 86.93333333333334, + "transactionsPerMinute": Object { + "over_time": Array [], + "value": 86.93333333333334, + }, }, ] } + sortFn={[Function]} /> `; diff --git a/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/__test__/props.json b/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/__test__/props.json index 2379d27407e04f..b3d906857cd3ae 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/__test__/props.json +++ b/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/__test__/props.json @@ -3,17 +3,34 @@ { "serviceName": "opbeans-node", "agentName": "nodejs", - "transactionsPerMinute": 0, - "errorsPerMinute": 46.06666666666667, + "transactionsPerMinute": { + "value": 0, + "over_time": [] + }, + "errorsPerMinute": { + "value": 46.06666666666667, + "over_time": [] + }, "avgResponseTime": null, - "environments": ["test"] + "environments": [ + "test" + ] }, { "serviceName": "opbeans-python", "agentName": "python", - "transactionsPerMinute": 86.93333333333334, - "errorsPerMinute": 12.6, - "avgResponseTime": 91535.42944785276, + "transactionsPerMinute": { + "value": 86.93333333333334, + "over_time": [] + }, + "errorsPerMinute": { + "value": 12.6, + "over_time": [] + }, + "avgResponseTime": { + "value": 91535.42944785276, + "over_time": [] + }, "environments": [] } ] diff --git a/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/index.tsx b/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/index.tsx index 90cc9af45273e5..3ddc28c30ca591 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/index.tsx @@ -4,24 +4,38 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiToolTip } from '@elastic/eui'; +import { EuiFlexItem, EuiFlexGroup, EuiBadge, EuiToolTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React from 'react'; import styled from 'styled-components'; +import { ValuesType } from 'utility-types'; +import { orderBy } from 'lodash'; +import { useTheme } from '../../../../../../observability/public'; +import { useUrlParams } from '../../../../hooks/useUrlParams'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { ServiceListAPIResponse } from '../../../../../server/lib/services/get_services'; import { NOT_AVAILABLE_LABEL } from '../../../../../common/i18n'; -import { fontSizes, truncate } from '../../../../style/variables'; +import { fontSizes, px, truncate } from '../../../../style/variables'; import { asDecimal, asMillisecondDuration } from '../../../../utils/formatters'; -import { ManagedTable } from '../../../shared/ManagedTable'; +import { ManagedTable, ITableColumn } from '../../../shared/ManagedTable'; import { EnvironmentBadge } from '../../../shared/EnvironmentBadge'; import { TransactionOverviewLink } from '../../../shared/Links/apm/TransactionOverviewLink'; +import { AgentIcon } from '../../../shared/AgentIcon'; +import { SparkPlot } from '../../../shared/charts/SparkPlot'; +import { getEmptySeries } from '../../../shared/charts/CustomPlot/getEmptySeries'; +import { + getSeverityColor, + Severity, +} from '../../../../../common/anomaly_detection'; interface Props { items: ServiceListAPIResponse['items']; noItemsMessage?: React.ReactNode; + displayHealthStatus: boolean; } +type ServiceListItem = ValuesType; + function formatNumber(value: number) { if (value === 0) { return '0'; @@ -36,12 +50,111 @@ function formatString(value?: string | null) { return value || NOT_AVAILABLE_LABEL; } +function ServiceListMetric({ + color, + series, + valueLabel, +}: { + color: 'euiColorVis1' | 'euiColorVis0' | 'euiColorVis7'; + series?: Array<{ x: number; y: number | null }>; + valueLabel: React.ReactNode; +}) { + const theme = useTheme(); + + const { + urlParams: { start, end }, + } = useUrlParams(); + + const colorValue = theme.eui[color]; + + return ( + + + + + + {valueLabel} + + + ); +} + +function HealthBadge({ severity }: { severity?: Severity }) { + const theme = useTheme(); + + let label: string = ''; + + switch (severity) { + case Severity.critical: + label = i18n.translate( + 'xpack.apm.servicesTable.serviceHealthStatus.critical', + { + defaultMessage: 'Critical', + } + ); + break; + + case Severity.major: + case Severity.minor: + label = i18n.translate( + 'xpack.apm.servicesTable.serviceHealthStatus.warning', + { + defaultMessage: 'Warning', + } + ); + break; + + case Severity.warning: + label = i18n.translate( + 'xpack.apm.servicesTable.serviceHealthStatus.healthy', + { + defaultMessage: 'Healthy', + } + ); + break; + + default: + label = i18n.translate( + 'xpack.apm.servicesTable.serviceHealthStatus.unknown', + { + defaultMessage: 'Unknown', + } + ); + break; + } + + const unknownColor = theme.eui.euiColorLightShade; + + return ( + + {label} + + ); +} + const AppLink = styled(TransactionOverviewLink)` font-size: ${fontSizes.large}; ${truncate('100%')}; `; -export const SERVICE_COLUMNS = [ +export const SERVICE_COLUMNS: Array> = [ + { + field: 'severity', + name: i18n.translate('xpack.apm.servicesTable.healthColumnLabel', { + defaultMessage: 'Health', + }), + width: px(80), + sortable: true, + render: (_, { severity }) => { + return ; + }, + }, { field: 'serviceName', name: i18n.translate('xpack.apm.servicesTable.nameColumnLabel', { @@ -49,9 +162,20 @@ export const SERVICE_COLUMNS = [ }), width: '40%', sortable: true, - render: (serviceName: string) => ( + render: (_, { serviceName, agentName }) => ( - {formatString(serviceName)} + + {agentName && ( + + + + )} + + + {formatString(serviceName)} + + + ), }, @@ -60,20 +184,12 @@ export const SERVICE_COLUMNS = [ name: i18n.translate('xpack.apm.servicesTable.environmentColumnLabel', { defaultMessage: 'Environment', }), - width: '20%', + width: px(160), sortable: true, - render: (environments: string[]) => ( - + render: (_, { environments }) => ( + ), }, - { - field: 'agentName', - name: i18n.translate('xpack.apm.servicesTable.agentColumnLabel', { - defaultMessage: 'Agent', - }), - sortable: true, - render: (agentName: string) => formatString(agentName), - }, { field: 'avgResponseTime', name: i18n.translate('xpack.apm.servicesTable.avgResponseTimeColumnLabel', { @@ -81,7 +197,15 @@ export const SERVICE_COLUMNS = [ }), sortable: true, dataType: 'number', - render: (time: number) => asMillisecondDuration(time), + render: (_, { avgResponseTime }) => ( + + ), + align: 'left', + width: px(160), }, { field: 'transactionsPerMinute', @@ -93,13 +217,22 @@ export const SERVICE_COLUMNS = [ ), sortable: true, dataType: 'number', - render: (value: number) => - `${formatNumber(value)} ${i18n.translate( - 'xpack.apm.servicesTable.transactionsPerMinuteUnitLabel', - { - defaultMessage: 'tpm', - } - )}`, + render: (_, { transactionsPerMinute }) => ( + + ), + align: 'left', + width: px(160), }, { field: 'errorsPerMinute', @@ -108,24 +241,83 @@ export const SERVICE_COLUMNS = [ }), sortable: true, dataType: 'number', - render: (value: number) => - `${formatNumber(value)} ${i18n.translate( - 'xpack.apm.servicesTable.errorsPerMinuteUnitLabel', - { - defaultMessage: 'err.', - } - )}`, + render: (_, { errorsPerMinute }) => ( + + ), + align: 'left', + width: px(160), }, ]; -export function ServiceList({ items, noItemsMessage }: Props) { +const SEVERITY_ORDER = [ + Severity.warning, + Severity.minor, + Severity.major, + Severity.critical, +]; + +export function ServiceList({ + items, + displayHealthStatus, + noItemsMessage, +}: Props) { + const columns = displayHealthStatus + ? SERVICE_COLUMNS + : SERVICE_COLUMNS.filter((column) => column.field !== 'severity'); + return ( { + // For severity, sort items by severity first, then by TPM + + return sortField === 'severity' + ? orderBy( + itemsToSort, + [ + (item) => { + return item.severity + ? SEVERITY_ORDER.indexOf(item.severity) + : -1; + }, + (item) => item.transactionsPerMinute?.value ?? 0, + ], + [sortDirection, sortDirection] + ) + : orderBy( + itemsToSort, + (item) => { + switch (sortField) { + case 'avgResponseTime': + return item.avgResponseTime?.value ?? 0; + case 'transactionsPerMinute': + return item.transactionsPerMinute?.value ?? 0; + case 'errorsPerMinute': + return item.errorsPerMinute?.value ?? 0; + + default: + return item[sortField as keyof typeof item]; + } + }, + sortDirection + ); + }} /> ); } diff --git a/x-pack/plugins/apm/public/components/app/ServiceOverview/__test__/ServiceOverview.test.tsx b/x-pack/plugins/apm/public/components/app/ServiceOverview/__test__/ServiceOverview.test.tsx index d9c5ff5130df6a..660d7c1a9bc275 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceOverview/__test__/ServiceOverview.test.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceOverview/__test__/ServiceOverview.test.tsx @@ -17,7 +17,9 @@ import { import { FETCH_STATUS } from '../../../../hooks/useFetcher'; import * as useLocalUIFilters from '../../../../hooks/useLocalUIFilters'; import * as urlParamsHooks from '../../../../hooks/useUrlParams'; +import * as useAnomalyDetectionSettings from '../../../../hooks/useAnomalyDetectionSettings'; import { SessionStorageMock } from '../../../../services/__test__/SessionStorageMock'; +import { EuiThemeProvider } from '../../../../../../../legacy/common/eui_styled_components'; const KibanaReactContext = createKibanaReactContext({ usageCollection: { reportUiStats: () => {} }, @@ -26,26 +28,28 @@ const KibanaReactContext = createKibanaReactContext({ function wrapper({ children }: { children: ReactChild }) { return ( - + - {children} - + } as unknown) as ApmPluginContextValue + } + > + {children} + + ); } @@ -80,6 +84,17 @@ describe('Service Overview -> View', () => { clearValues: () => null, status: FETCH_STATUS.SUCCESS, }); + + jest + .spyOn(useAnomalyDetectionSettings, 'useAnomalyDetectionSettings') + .mockReturnValue({ + status: FETCH_STATUS.SUCCESS, + data: { + jobs: [], + hasLegacyJobs: false, + }, + refetch: () => undefined, + }); }); afterEach(() => { @@ -99,6 +114,7 @@ describe('Service Overview -> View', () => { errorsPerMinute: 200, avgResponseTime: 300, environments: ['test', 'dev'], + severity: 1, }, { serviceName: 'My Go Service', @@ -107,6 +123,7 @@ describe('Service Overview -> View', () => { errorsPerMinute: 500, avgResponseTime: 600, environments: [], + severity: 10, }, ], }); @@ -195,4 +212,57 @@ describe('Service Overview -> View', () => { expect(addWarning).not.toHaveBeenCalled(); }); }); + + describe('when ML data is not found', () => { + it('does not render the health column', async () => { + httpGet.mockResolvedValueOnce({ + hasLegacyData: false, + hasHistoricalData: true, + items: [ + { + serviceName: 'My Python Service', + agentName: 'python', + transactionsPerMinute: 100, + errorsPerMinute: 200, + avgResponseTime: 300, + environments: ['test', 'dev'], + }, + ], + }); + + const { queryByText } = renderServiceOverview(); + + // wait for requests to be made + await wait(() => expect(httpGet).toHaveBeenCalledTimes(1)); + + expect(queryByText('Health')).toBeNull(); + }); + }); + + describe('when ML data is found', () => { + it('renders the health column', async () => { + httpGet.mockResolvedValueOnce({ + hasLegacyData: false, + hasHistoricalData: true, + items: [ + { + serviceName: 'My Python Service', + agentName: 'python', + transactionsPerMinute: 100, + errorsPerMinute: 200, + avgResponseTime: 300, + environments: ['test', 'dev'], + severity: 1, + }, + ], + }); + + const { queryAllByText } = renderServiceOverview(); + + // wait for requests to be made + await wait(() => expect(httpGet).toHaveBeenCalledTimes(1)); + + expect(queryAllByText('Health').length).toBeGreaterThan(1); + }); + }); }); diff --git a/x-pack/plugins/apm/public/components/app/ServiceOverview/__test__/__snapshots__/ServiceOverview.test.tsx.snap b/x-pack/plugins/apm/public/components/app/ServiceOverview/__test__/__snapshots__/ServiceOverview.test.tsx.snap index 6d447887627bfe..767c81cdab9198 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceOverview/__test__/__snapshots__/ServiceOverview.test.tsx.snap +++ b/x-pack/plugins/apm/public/components/app/ServiceOverview/__test__/__snapshots__/ServiceOverview.test.tsx.snap @@ -7,7 +7,7 @@ NodeList [ >
- Name + Health
- - My Go Service - + + Unknown + +
- Environment + Name
+ > + + + +
- Agent + Environment
- go + + + + test + + + + + + + dev + + +
- 0.6 ms +
+
+
+
+
+
+
+
+
+
+ 0 ms +
+
- 400.0 tpm +
+
+
+
+
+
+
+
+
+
+ 0 tpm +
+
- 500.0 err. +
+
+
+
+
+
+
+
+
+
+ 0 err. +
+
, @@ -247,87 +410,91 @@ NodeList [ >
- Name + Health
- - My Python Service - + + Unknown + +
- Environment + Name
- - - test - - - - - - +
+
- dev - - + + My Go Service + +
+
- Agent + Environment
- python -
+ />
- 0.3 ms +
+
+
+
+
+
+
+
+
+
+ 0 ms +
+
- 100.0 tpm +
+
+
+
+
+
+
+
+
+
+ 0 tpm +
+
- 200.0 err. +
+
+
+
+
+
+
+
+
+
+ 0 err. +
+
, diff --git a/x-pack/plugins/apm/public/components/app/ServiceOverview/index.tsx b/x-pack/plugins/apm/public/components/app/ServiceOverview/index.tsx index 7146e471a7f82f..5b450ab03cd30f 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceOverview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceOverview/index.tsx @@ -10,7 +10,7 @@ import { i18n } from '@kbn/i18n'; import React, { useEffect, useMemo } from 'react'; import url from 'url'; import { toMountPoint } from '../../../../../../../src/plugins/kibana_react/public'; -import { useFetcher } from '../../../hooks/useFetcher'; +import { useFetcher, FETCH_STATUS } from '../../../hooks/useFetcher'; import { NoServicesMessage } from './NoServicesMessage'; import { ServiceList } from './ServiceList'; import { useUrlParams } from '../../../hooks/useUrlParams'; @@ -18,8 +18,11 @@ import { useTrackPageview } from '../../../../../observability/public'; import { Projection } from '../../../../common/projections'; import { LocalUIFilters } from '../../shared/LocalUIFilters'; import { useApmPluginContext } from '../../../hooks/useApmPluginContext'; +import { MLCallout } from './ServiceList/MLCallout'; +import { useLocalStorage } from '../../../hooks/useLocalStorage'; +import { useAnomalyDetectionSettings } from '../../../hooks/useAnomalyDetectionSettings'; -const initalData = { +const initialData = { items: [], hasHistoricalData: true, hasLegacyData: false, @@ -33,7 +36,7 @@ export function ServiceOverview() { urlParams: { start, end }, uiFilters, } = useUrlParams(); - const { data = initalData, status } = useFetcher( + const { data = initialData, status } = useFetcher( (callApmApi) => { if (start && end) { return callApmApi({ @@ -93,6 +96,26 @@ export function ServiceOverview() { [] ); + const { + data: anomalyDetectionSettingsData, + status: anomalyDetectionSettingsStatus, + } = useAnomalyDetectionSettings(); + + const [userHasDismissedCallout, setUserHasDismissedCallout] = useLocalStorage( + 'apm.userHasDismissedServiceInventoryMlCallout', + false + ); + + const canCreateJob = !!core.application.capabilities.ml?.canCreateJob; + + const displayMlCallout = + anomalyDetectionSettingsStatus === FETCH_STATUS.SUCCESS && + !anomalyDetectionSettingsData?.jobs.length && + canCreateJob && + !userHasDismissedCallout; + + const displayHealthStatus = data.items.some((item) => 'severity' in item); + return ( <> @@ -101,17 +124,27 @@ export function ServiceOverview() { - - + {displayMlCallout ? ( + + setUserHasDismissedCallout(true)} /> + + ) : null} + + + + } /> - } - /> - + + + diff --git a/x-pack/plugins/apm/public/components/shared/AgentIcon/get_agent_icon.ts b/x-pack/plugins/apm/public/components/shared/AgentIcon/get_agent_icon.ts new file mode 100644 index 00000000000000..2475eecee8e349 --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/AgentIcon/get_agent_icon.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 { getNormalizedAgentName } from '../../../../common/agent_name'; +import dotNetIcon from './icons/dot-net.svg'; +import goIcon from './icons/go.svg'; +import javaIcon from './icons/java.svg'; +import nodeJsIcon from './icons/nodejs.svg'; +import phpIcon from './icons/php.svg'; +import pythonIcon from './icons/python.svg'; +import rubyIcon from './icons/ruby.svg'; +import rumJsIcon from './icons/rumjs.svg'; + +const agentIcons: { [key: string]: string } = { + dotnet: dotNetIcon, + go: goIcon, + java: javaIcon, + 'js-base': rumJsIcon, + nodejs: nodeJsIcon, + php: phpIcon, + python: pythonIcon, + ruby: rubyIcon, +}; + +export function getAgentIcon(agentName?: string) { + const normalizedAgentName = getNormalizedAgentName(agentName); + return normalizedAgentName && agentIcons[normalizedAgentName]; +} diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/icons/dot-net.svg b/x-pack/plugins/apm/public/components/shared/AgentIcon/icons/dot-net.svg similarity index 100% rename from x-pack/plugins/apm/public/components/app/ServiceMap/icons/dot-net.svg rename to x-pack/plugins/apm/public/components/shared/AgentIcon/icons/dot-net.svg diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/icons/go.svg b/x-pack/plugins/apm/public/components/shared/AgentIcon/icons/go.svg similarity index 100% rename from x-pack/plugins/apm/public/components/app/ServiceMap/icons/go.svg rename to x-pack/plugins/apm/public/components/shared/AgentIcon/icons/go.svg diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/icons/java.svg b/x-pack/plugins/apm/public/components/shared/AgentIcon/icons/java.svg similarity index 100% rename from x-pack/plugins/apm/public/components/app/ServiceMap/icons/java.svg rename to x-pack/plugins/apm/public/components/shared/AgentIcon/icons/java.svg diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/icons/nodejs.svg b/x-pack/plugins/apm/public/components/shared/AgentIcon/icons/nodejs.svg similarity index 100% rename from x-pack/plugins/apm/public/components/app/ServiceMap/icons/nodejs.svg rename to x-pack/plugins/apm/public/components/shared/AgentIcon/icons/nodejs.svg diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/icons/php.svg b/x-pack/plugins/apm/public/components/shared/AgentIcon/icons/php.svg similarity index 100% rename from x-pack/plugins/apm/public/components/app/ServiceMap/icons/php.svg rename to x-pack/plugins/apm/public/components/shared/AgentIcon/icons/php.svg diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/icons/python.svg b/x-pack/plugins/apm/public/components/shared/AgentIcon/icons/python.svg similarity index 100% rename from x-pack/plugins/apm/public/components/app/ServiceMap/icons/python.svg rename to x-pack/plugins/apm/public/components/shared/AgentIcon/icons/python.svg diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/icons/ruby.svg b/x-pack/plugins/apm/public/components/shared/AgentIcon/icons/ruby.svg similarity index 100% rename from x-pack/plugins/apm/public/components/app/ServiceMap/icons/ruby.svg rename to x-pack/plugins/apm/public/components/shared/AgentIcon/icons/ruby.svg diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/icons/rumjs.svg b/x-pack/plugins/apm/public/components/shared/AgentIcon/icons/rumjs.svg similarity index 100% rename from x-pack/plugins/apm/public/components/app/ServiceMap/icons/rumjs.svg rename to x-pack/plugins/apm/public/components/shared/AgentIcon/icons/rumjs.svg diff --git a/x-pack/plugins/apm/public/components/shared/AgentIcon/index.tsx b/x-pack/plugins/apm/public/components/shared/AgentIcon/index.tsx new file mode 100644 index 00000000000000..5646fc05bd28ff --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/AgentIcon/index.tsx @@ -0,0 +1,21 @@ +/* + * 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 { AgentName } from '../../../../typings/es_schemas/ui/fields/agent'; +import { getAgentIcon } from './get_agent_icon'; +import { px } from '../../../style/variables'; + +interface Props { + agentName: AgentName; +} + +export function AgentIcon(props: Props) { + const { agentName } = props; + + const icon = getAgentIcon(agentName); + + return {agentName}; +} diff --git a/x-pack/plugins/apm/public/components/shared/ManagedTable/index.tsx b/x-pack/plugins/apm/public/components/shared/ManagedTable/index.tsx index 9fe52aab836417..9db563a0f6ba87 100644 --- a/x-pack/plugins/apm/public/components/shared/ManagedTable/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/ManagedTable/index.tsx @@ -33,9 +33,22 @@ interface Props { hidePerPageOptions?: boolean; noItemsMessage?: React.ReactNode; sortItems?: boolean; + sortFn?: ( + items: T[], + sortField: string, + sortDirection: 'asc' | 'desc' + ) => T[]; pagination?: boolean; } +function defaultSortFn( + items: T[], + sortField: string, + sortDirection: 'asc' | 'desc' +) { + return orderBy(items, sortField, sortDirection); +} + function UnoptimizedManagedTable(props: Props) { const history = useHistory(); const { @@ -48,6 +61,7 @@ function UnoptimizedManagedTable(props: Props) { hidePerPageOptions = true, noItemsMessage, sortItems = true, + sortFn = defaultSortFn, pagination = true, } = props; @@ -62,11 +76,11 @@ function UnoptimizedManagedTable(props: Props) { const renderedItems = useMemo(() => { const sortedItems = sortItems - ? orderBy(items, sortField, sortDirection as 'asc' | 'desc') + ? sortFn(items, sortField, sortDirection as 'asc' | 'desc') : items; return sortedItems.slice(page * pageSize, (page + 1) * pageSize); - }, [page, pageSize, sortField, sortDirection, items, sortItems]); + }, [page, pageSize, sortField, sortDirection, items, sortItems, sortFn]); const sort = useMemo(() => { return { diff --git a/x-pack/plugins/apm/public/components/shared/TransactionDurationAnomalyAlertTrigger/SelectAnomalySeverity.tsx b/x-pack/plugins/apm/public/components/shared/TransactionDurationAnomalyAlertTrigger/SelectAnomalySeverity.tsx index fcbdb900368ea5..5bddfc67200b14 100644 --- a/x-pack/plugins/apm/public/components/shared/TransactionDurationAnomalyAlertTrigger/SelectAnomalySeverity.tsx +++ b/x-pack/plugins/apm/public/components/shared/TransactionDurationAnomalyAlertTrigger/SelectAnomalySeverity.tsx @@ -8,9 +8,11 @@ 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 '../../app/ServiceMap/cytoscapeOptions'; +import { + getSeverityColor, + Severity, +} from '../../../../common/anomaly_detection'; import { useTheme } from '../../../hooks/useTheme'; -import { severity as Severity } from '../../app/ServiceMap/Popover/getSeverity'; type SeverityScore = 0 | 25 | 50 | 75; const ANOMALY_SCORES: SeverityScore[] = [0, 25, 50, 75]; diff --git a/x-pack/plugins/apm/public/components/shared/charts/SparkPlot/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/SparkPlot/index.tsx new file mode 100644 index 00000000000000..97dfa383e55a49 --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/charts/SparkPlot/index.tsx @@ -0,0 +1,44 @@ +/* + * 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 { ScaleType, Chart, Settings, AreaSeries } from '@elastic/charts'; +import { px } from '../../../../style/variables'; +import { useChartTheme } from '../../../../../../observability/public'; + +interface Props { + color: string; + series: Array<{ x: number; y: number | null }>; +} + +export function SparkPlot(props: Props) { + const { series, color } = props; + const theme = useChartTheme(); + + return ( + + + + + ); +} diff --git a/x-pack/plugins/apm/public/hooks/useAnomalyDetectionSettings.ts b/x-pack/plugins/apm/public/hooks/useAnomalyDetectionSettings.ts new file mode 100644 index 00000000000000..d3ec664d9bd70a --- /dev/null +++ b/x-pack/plugins/apm/public/hooks/useAnomalyDetectionSettings.ts @@ -0,0 +1,18 @@ +/* + * 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 { useFetcher } from './useFetcher'; + +export function useAnomalyDetectionSettings() { + return useFetcher( + (callApmApi) => + callApmApi({ + pathname: `/api/apm/settings/anomaly-detection`, + }), + [], + { showToastOnError: false } + ); +} diff --git a/x-pack/plugins/apm/public/hooks/useLocalStorage.ts b/x-pack/plugins/apm/public/hooks/useLocalStorage.ts new file mode 100644 index 00000000000000..ae7b277075725e --- /dev/null +++ b/x-pack/plugins/apm/public/hooks/useLocalStorage.ts @@ -0,0 +1,52 @@ +/* + * 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 { useState, useEffect } from 'react'; + +export function useLocalStorage(key: string, defaultValue: T) { + const [item, setItem] = useState(getFromStorage()); + + function getFromStorage() { + const storedItem = window.localStorage.getItem(key); + + let toStore: T = defaultValue; + + if (storedItem !== null) { + try { + toStore = JSON.parse(storedItem) as T; + } catch (err) { + // do nothing + } + } + + return toStore; + } + + const updateFromStorage = () => { + const storedItem = getFromStorage(); + setItem(storedItem); + }; + + const saveToStorage = (value: T) => { + if (value === undefined) { + window.localStorage.removeItem(key); + } else { + window.localStorage.setItem(key, JSON.stringify(value)); + updateFromStorage(); + } + }; + + useEffect(() => { + window.addEventListener('storage', (event: StorageEvent) => { + if (event.key === key) { + updateFromStorage(); + } + }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return [item, saveToStorage] as const; +} diff --git a/x-pack/plugins/apm/scripts/tsconfig.json b/x-pack/plugins/apm/scripts/tsconfig.json index 64602bc6b27699..f1643608496ad4 100644 --- a/x-pack/plugins/apm/scripts/tsconfig.json +++ b/x-pack/plugins/apm/scripts/tsconfig.json @@ -1,7 +1,8 @@ { "extends": "../../../../tsconfig.base.json", "include": [ - "./**/*" + "./**/*", + "../observability" ], "exclude": [], "compilerOptions": { 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 e7eb7b8de65e35..9234f585232efe 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 @@ -81,6 +81,11 @@ export function registerTransactionDurationAnomalyAlertType({ anomalyDetectors, alertParams.environment ); + + if (mlJobIds.length === 0) { + return; + } + const anomalySearchParams = { body: { size: 0, diff --git a/x-pack/plugins/apm/server/lib/helpers/get_bucket_size/index.ts b/x-pack/plugins/apm/server/lib/helpers/get_bucket_size/index.ts index 75b0471424e79f..3f193b2b20ca1d 100644 --- a/x-pack/plugins/apm/server/lib/helpers/get_bucket_size/index.ts +++ b/x-pack/plugins/apm/server/lib/helpers/get_bucket_size/index.ts @@ -10,19 +10,22 @@ import { calculateAuto } from './calculate_auto'; // @ts-expect-error import { unitToSeconds } from './unit_to_seconds'; -export function getBucketSize(start: number, end: number, interval: string) { +export function getBucketSize( + start: number, + end: number, + numBuckets: number = 100 +) { const duration = moment.duration(end - start, 'ms'); - const bucketSize = Math.max(calculateAuto.near(100, duration).asSeconds(), 1); + const bucketSize = Math.max( + calculateAuto.near(numBuckets, duration).asSeconds(), + 1 + ); const intervalString = `${bucketSize}s`; - const matches = interval && interval.match(/^([\d]+)([shmdwMy]|ms)$/); - const minBucketSize = matches - ? Number(matches[1]) * unitToSeconds(matches[2]) - : 0; - if (bucketSize < minBucketSize) { + if (bucketSize < 0) { return { - bucketSize: minBucketSize, - intervalString: interval, + bucketSize: 0, + intervalString: 'auto', }; } diff --git a/x-pack/plugins/apm/server/lib/helpers/metrics.ts b/x-pack/plugins/apm/server/lib/helpers/metrics.ts index 9f5b5cdf475526..ea018868f95179 100644 --- a/x-pack/plugins/apm/server/lib/helpers/metrics.ts +++ b/x-pack/plugins/apm/server/lib/helpers/metrics.ts @@ -11,7 +11,7 @@ export function getMetricsDateHistogramParams( end: number, metricsInterval: number ) { - const { bucketSize } = getBucketSize(start, end, 'auto'); + const { bucketSize } = getBucketSize(start, end); return { field: '@timestamp', diff --git a/x-pack/plugins/apm/server/lib/metrics/by_agent/java/gc/fetch_and_transform_gc_metrics.ts b/x-pack/plugins/apm/server/lib/metrics/by_agent/java/gc/fetch_and_transform_gc_metrics.ts index 551384da2cca78..e845bf3b27e520 100644 --- a/x-pack/plugins/apm/server/lib/metrics/by_agent/java/gc/fetch_and_transform_gc_metrics.ts +++ b/x-pack/plugins/apm/server/lib/metrics/by_agent/java/gc/fetch_and_transform_gc_metrics.ts @@ -44,7 +44,7 @@ export async function fetchAndTransformGcMetrics({ }) { const { start, end, apmEventClient, config } = setup; - const { bucketSize } = getBucketSize(start, end, 'auto'); + const { bucketSize } = getBucketSize(start, end); const projection = getMetricsProjection({ setup, 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 ec274d20b60053..ed8ae923e6e6c8 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 @@ -3,7 +3,6 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { Logger } from 'kibana/server'; import Boom from 'boom'; import { Setup, SetupTimeRange } from '../helpers/setup_request'; import { PromiseReturnType } from '../../../typings/common'; @@ -27,11 +26,9 @@ export type ServiceAnomaliesResponse = PromiseReturnType< export async function getServiceAnomalies({ setup, - logger, environment, }: { setup: Setup & SetupTimeRange; - logger: Logger; environment?: string; }) { const { ml, start, end } = setup; @@ -41,11 +38,20 @@ export async function getServiceAnomalies({ } const mlCapabilities = await ml.mlSystem.mlCapabilities(); + if (!mlCapabilities.mlFeatureEnabledInSpace) { throw Boom.forbidden(ML_ERRORS.ML_NOT_AVAILABLE_IN_SPACE); } const mlJobIds = await getMLJobIds(ml.anomalyDetectors, environment); + + if (!mlJobIds.length) { + return { + mlJobIds: [], + serviceAnomalies: {}, + }; + } + const params = { body: { size: 0, @@ -120,7 +126,9 @@ interface ServiceAnomaliesAggResponse { function transformResponseToServiceAnomalies( response: ServiceAnomaliesAggResponse ): Record { - const serviceAnomaliesMap = response.aggregations.services.buckets.reduce( + const serviceAnomaliesMap = ( + response.aggregations?.services.buckets ?? [] + ).reduce( (statsByServiceName, { key: serviceName, top_score: topScoreAgg }) => { return { ...statsByServiceName, @@ -153,7 +161,7 @@ export async function getMLJobIds( (job) => job.custom_settings?.job_tags?.environment === environment ); if (!matchingMLJob) { - throw new Error(`ML job Not Found for environment "${environment}".`); + return []; } return [matchingMLJob.job_id]; } diff --git a/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.test.ts b/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.test.ts index d1c99d778c8f08..1e26b6f3f58f9e 100644 --- a/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.test.ts +++ b/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.test.ts @@ -58,6 +58,9 @@ describe('getServiceMapServiceNodeInfo', () => { indices: {}, start: 1593460053026000, end: 1593497863217000, + config: { + 'xpack.apm.metricsInterval': 30, + }, } as unknown) as Setup & SetupTimeRange; const environment = 'test environment'; const serviceName = 'test service name'; diff --git a/x-pack/plugins/apm/server/lib/services/__snapshots__/queries.test.ts.snap b/x-pack/plugins/apm/server/lib/services/__snapshots__/queries.test.ts.snap index ca86c1d93fa6e7..34f4babf55ea1f 100644 --- a/x-pack/plugins/apm/server/lib/services/__snapshots__/queries.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/services/__snapshots__/queries.test.ts.snap @@ -105,6 +105,24 @@ Array [ "field": "transaction.duration.us", }, }, + "over_time": Object { + "aggs": Object { + "average": Object { + "avg": Object { + "field": "transaction.duration.us", + }, + }, + }, + "date_histogram": Object { + "extended_bounds": Object { + "max": 1528977600000, + "min": 1528113600000, + }, + "field": "@timestamp", + "fixed_interval": "43200s", + "min_doc_count": 0, + }, + }, }, "terms": Object { "field": "service.name", @@ -194,6 +212,19 @@ Array [ "body": Object { "aggs": Object { "services": Object { + "aggs": Object { + "over_time": Object { + "date_histogram": Object { + "extended_bounds": Object { + "max": 1528977600000, + "min": 1528113600000, + }, + "field": "@timestamp", + "fixed_interval": "43200s", + "min_doc_count": 0, + }, + }, + }, "terms": Object { "field": "service.name", "size": 500, @@ -232,6 +263,19 @@ Array [ "body": Object { "aggs": Object { "services": Object { + "aggs": Object { + "over_time": Object { + "date_histogram": Object { + "extended_bounds": Object { + "max": 1528977600000, + "min": 1528113600000, + }, + "field": "@timestamp", + "fixed_interval": "43200s", + "min_doc_count": 0, + }, + }, + }, "terms": Object { "field": "service.name", "size": 500, diff --git a/x-pack/plugins/apm/server/lib/services/get_services/get_services_items.ts b/x-pack/plugins/apm/server/lib/services/get_services/get_services_items.ts index d888b43b63fac6..84e6e230ff3b8a 100644 --- a/x-pack/plugins/apm/server/lib/services/get_services/get_services_items.ts +++ b/x-pack/plugins/apm/server/lib/services/get_services/get_services_items.ts @@ -17,13 +17,20 @@ import { getTransactionRates, getErrorRates, getEnvironments, + getHealthStatuses, } from './get_services_items_stats'; export type ServiceListAPIResponse = PromiseReturnType; export type ServicesItemsSetup = Setup & SetupTimeRange & SetupUIFilters; export type ServicesItemsProjection = ReturnType; -export async function getServicesItems(setup: ServicesItemsSetup) { +export async function getServicesItems({ + setup, + mlAnomaliesEnvironment, +}: { + setup: ServicesItemsSetup; + mlAnomaliesEnvironment?: string; +}) { const params = { projection: getServicesProjection({ setup }), setup, @@ -35,12 +42,14 @@ export async function getServicesItems(setup: ServicesItemsSetup) { transactionRates, errorRates, environments, + healthStatuses, ] = await Promise.all([ getTransactionDurationAverages(params), getAgentNames(params), getTransactionRates(params), getErrorRates(params), getEnvironments(params), + getHealthStatuses(params, mlAnomaliesEnvironment), ]); const allMetrics = [ @@ -49,6 +58,7 @@ export async function getServicesItems(setup: ServicesItemsSetup) { ...transactionRates, ...errorRates, ...environments, + ...healthStatuses, ]; return joinByKey(allMetrics, 'serviceName'); diff --git a/x-pack/plugins/apm/server/lib/services/get_services/get_services_items_stats.ts b/x-pack/plugins/apm/server/lib/services/get_services/get_services_items_stats.ts index ddce3b667a6033..e29aa534877901 100644 --- a/x-pack/plugins/apm/server/lib/services/get_services/get_services_items_stats.ts +++ b/x-pack/plugins/apm/server/lib/services/get_services/get_services_items_stats.ts @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import { getSeverity, Severity } from '../../../../common/anomaly_detection'; +import { AgentName } from '../../../../typings/es_schemas/ui/fields/agent'; import { TRANSACTION_DURATION, AGENT_NAME, @@ -15,6 +17,20 @@ import { ServicesItemsSetup, ServicesItemsProjection, } from './get_services_items'; +import { getBucketSize } from '../../helpers/get_bucket_size'; +import { + getMLJobIds, + getServiceAnomalies, +} from '../../service_map/get_service_anomalies'; + +function getDateHistogramOpts(start: number, end: number) { + return { + field: '@timestamp', + fixed_interval: getBucketSize(start, end, 20).intervalString, + min_doc_count: 0, + extended_bounds: { min: start, max: end }, + }; +} const MAX_NUMBER_OF_SERVICES = 500; @@ -30,7 +46,7 @@ export const getTransactionDurationAverages = async ({ setup, projection, }: AggregationParams) => { - const { apmEventClient } = setup; + const { apmEventClient, start, end } = setup; const response = await apmEventClient.search( mergeProjection(projection, { @@ -51,6 +67,16 @@ export const getTransactionDurationAverages = async ({ field: TRANSACTION_DURATION, }, }, + over_time: { + date_histogram: getDateHistogramOpts(start, end), + aggs: { + average: { + avg: { + field: TRANSACTION_DURATION, + }, + }, + }, + }, }, }, }, @@ -66,7 +92,13 @@ export const getTransactionDurationAverages = async ({ return aggregations.services.buckets.map((bucket) => ({ serviceName: bucket.key as string, - avgResponseTime: bucket.average.value, + avgResponseTime: { + value: bucket.average.value, + over_time: bucket.over_time.buckets.map((dateBucket) => ({ + x: dateBucket.key, + y: dateBucket.average.value, + })), + }, })); }; @@ -114,7 +146,7 @@ export const getAgentNames = async ({ return aggregations.services.buckets.map((bucket) => ({ serviceName: bucket.key as string, - agentName: bucket.agent_name.hits.hits[0]?._source.agent.name, + agentName: bucket.agent_name.hits.hits[0]?._source.agent.name as AgentName, })); }; @@ -122,7 +154,7 @@ export const getTransactionRates = async ({ setup, projection, }: AggregationParams) => { - const { apmEventClient } = setup; + const { apmEventClient, start, end } = setup; const response = await apmEventClient.search( mergeProjection(projection, { apm: { @@ -136,6 +168,11 @@ export const getTransactionRates = async ({ ...projection.body.aggs.services.terms, size: MAX_NUMBER_OF_SERVICES, }, + aggs: { + over_time: { + date_histogram: getDateHistogramOpts(start, end), + }, + }, }, }, }, @@ -154,7 +191,13 @@ export const getTransactionRates = async ({ const transactionsPerMinute = bucket.doc_count / deltaAsMinutes; return { serviceName: bucket.key as string, - transactionsPerMinute, + transactionsPerMinute: { + value: transactionsPerMinute, + over_time: bucket.over_time.buckets.map((dateBucket) => ({ + x: dateBucket.key, + y: dateBucket.doc_count / deltaAsMinutes, + })), + }, }; }); }; @@ -163,7 +206,7 @@ export const getErrorRates = async ({ setup, projection, }: AggregationParams) => { - const { apmEventClient } = setup; + const { apmEventClient, start, end } = setup; const response = await apmEventClient.search( mergeProjection(projection, { apm: { @@ -177,6 +220,11 @@ export const getErrorRates = async ({ ...projection.body.aggs.services.terms, size: MAX_NUMBER_OF_SERVICES, }, + aggs: { + over_time: { + date_histogram: getDateHistogramOpts(start, end), + }, + }, }, }, }, @@ -195,7 +243,13 @@ export const getErrorRates = async ({ const errorsPerMinute = bucket.doc_count / deltaAsMinutes; return { serviceName: bucket.key as string, - errorsPerMinute, + errorsPerMinute: { + value: errorsPerMinute, + over_time: bucket.over_time.buckets.map((dateBucket) => ({ + x: dateBucket.key, + y: dateBucket.doc_count / deltaAsMinutes, + })), + }, }; }); }; @@ -246,3 +300,38 @@ export const getEnvironments = async ({ environments: bucket.environments.buckets.map((env) => env.key as string), })); }; + +export const getHealthStatuses = async ( + { setup }: AggregationParams, + mlAnomaliesEnvironment?: string +) => { + if (!setup.ml) { + return []; + } + + const jobIds = await getMLJobIds( + setup.ml.anomalyDetectors, + mlAnomaliesEnvironment + ); + if (!jobIds.length) { + return []; + } + + const anomalies = await getServiceAnomalies({ + setup, + environment: mlAnomaliesEnvironment, + }); + + return Object.keys(anomalies.serviceAnomalies).reduce< + Array<{ serviceName: string; severity?: Severity }> + >((prev, serviceName) => { + const stats = anomalies.serviceAnomalies[serviceName]; + + const severity = getSeverity(stats.anomalyScore); + + return prev.concat({ + serviceName, + severity, + }); + }, []); +}; diff --git a/x-pack/plugins/apm/server/lib/services/get_services/index.ts b/x-pack/plugins/apm/server/lib/services/get_services/index.ts index 5a909ebd6ec549..28b4c64a4af475 100644 --- a/x-pack/plugins/apm/server/lib/services/get_services/index.ts +++ b/x-pack/plugins/apm/server/lib/services/get_services/index.ts @@ -17,11 +17,15 @@ import { getServicesItems } from './get_services_items'; export type ServiceListAPIResponse = PromiseReturnType; -export async function getServices( - setup: Setup & SetupTimeRange & SetupUIFilters -) { +export async function getServices({ + setup, + mlAnomaliesEnvironment, +}: { + setup: Setup & SetupTimeRange & SetupUIFilters; + mlAnomaliesEnvironment?: string; +}) { const [items, hasLegacyData] = await Promise.all([ - getServicesItems(setup), + getServicesItems({ setup, mlAnomaliesEnvironment }), getLegacyDataStatus(setup), ]); diff --git a/x-pack/plugins/apm/server/lib/services/queries.test.ts b/x-pack/plugins/apm/server/lib/services/queries.test.ts index 99c58a17d396a1..9b0dd7a03ca5b8 100644 --- a/x-pack/plugins/apm/server/lib/services/queries.test.ts +++ b/x-pack/plugins/apm/server/lib/services/queries.test.ts @@ -38,7 +38,7 @@ describe('services queries', () => { }); it('fetches the service items', async () => { - mock = await inspectSearchParams((setup) => getServicesItems(setup)); + mock = await inspectSearchParams((setup) => getServicesItems({ setup })); const allParams = mock.spy.mock.calls.map((call) => call[0]); diff --git a/x-pack/plugins/apm/server/lib/transaction_groups/get_error_rate.ts b/x-pack/plugins/apm/server/lib/transaction_groups/get_error_rate.ts index ec2d8144cf3ffc..d588fb5bbde712 100644 --- a/x-pack/plugins/apm/server/lib/transaction_groups/get_error_rate.ts +++ b/x-pack/plugins/apm/server/lib/transaction_groups/get_error_rate.ts @@ -59,7 +59,7 @@ export async function getErrorRate({ total_transactions: { date_histogram: { field: '@timestamp', - fixed_interval: getBucketSize(start, end, 'auto').intervalString, + fixed_interval: getBucketSize(start, end).intervalString, min_doc_count: 0, extended_bounds: { min: start, max: end }, }, diff --git a/x-pack/plugins/apm/server/lib/transactions/avg_duration_by_browser/fetcher.ts b/x-pack/plugins/apm/server/lib/transactions/avg_duration_by_browser/fetcher.ts index f68082dfaa1e1a..51118278fb8243 100644 --- a/x-pack/plugins/apm/server/lib/transactions/avg_duration_by_browser/fetcher.ts +++ b/x-pack/plugins/apm/server/lib/transactions/avg_duration_by_browser/fetcher.ts @@ -24,7 +24,7 @@ export type ESResponse = PromiseReturnType; export function fetcher(options: Options) { const { end, apmEventClient, start, uiFiltersES } = options.setup; const { serviceName, transactionName } = options; - const { intervalString } = getBucketSize(start, end, 'auto'); + const { intervalString } = getBucketSize(start, end); const transactionNameFilter = transactionName ? [{ term: { [TRANSACTION_NAME]: transactionName } }] diff --git a/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/index.ts b/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/index.ts index 596c3137ec19f3..d8865f0049d357 100644 --- a/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/index.ts +++ b/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/index.ts @@ -64,16 +64,10 @@ export async function getAnomalySeries({ return; } - let mlJobIds: string[] = []; - try { - mlJobIds = await getMLJobIds( - setup.ml.anomalyDetectors, - uiFilters.environment - ); - } catch (error) { - logger.error(error); - return; - } + const mlJobIds = await getMLJobIds( + setup.ml.anomalyDetectors, + uiFilters.environment + ); // don't fetch anomalies if there are isn't exaclty 1 ML job match for the given environment if (mlJobIds.length !== 1) { @@ -87,7 +81,7 @@ export async function getAnomalySeries({ } const { start, end } = setup; - const { intervalString, bucketSize } = getBucketSize(start, end, 'auto'); + const { intervalString, bucketSize } = getBucketSize(start, end); const esResponse = await anomalySeriesFetcher({ serviceName, diff --git a/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/fetcher.ts b/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/fetcher.ts index 1498c22e327d66..f39529b59caa6b 100644 --- a/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/fetcher.ts +++ b/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/fetcher.ts @@ -35,7 +35,7 @@ export function timeseriesFetcher({ setup: Setup & SetupTimeRange & SetupUIFilters; }) { const { start, end, uiFiltersES, apmEventClient } = setup; - const { intervalString } = getBucketSize(start, end, 'auto'); + const { intervalString } = getBucketSize(start, end); const filter: ESFilter[] = [ { term: { [SERVICE_NAME]: serviceName } }, diff --git a/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/index.ts b/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/index.ts index 8a0fe1a57736fd..ea06bd57bfff28 100644 --- a/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/index.ts +++ b/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/index.ts @@ -20,7 +20,7 @@ export async function getApmTimeseriesData(options: { setup: Setup & SetupTimeRange & SetupUIFilters; }) { const { start, end } = options.setup; - const { bucketSize } = getBucketSize(start, end, 'auto'); + const { bucketSize } = getBucketSize(start, end); const durationAsMinutes = (end - start) / 1000 / 60; const timeseriesResponse = await timeseriesFetcher(options); diff --git a/x-pack/plugins/apm/server/routes/services.ts b/x-pack/plugins/apm/server/routes/services.ts index 74ab717b8de592..cc7f25867df2cd 100644 --- a/x-pack/plugins/apm/server/routes/services.ts +++ b/x-pack/plugins/apm/server/routes/services.ts @@ -16,6 +16,7 @@ import { createRoute } from './create_route'; import { uiFiltersRt, rangeRt } from './default_api_types'; import { getServiceAnnotations } from '../lib/services/annotations'; import { dateAsStringRt } from '../../common/runtime_types/date_as_string_rt'; +import { getParsedUiFilters } from '../lib/helpers/convert_ui_filters/get_parsed_ui_filters'; export const servicesRoute = createRoute(() => ({ path: '/api/apm/services', @@ -23,8 +24,17 @@ export const servicesRoute = createRoute(() => ({ query: t.intersection([uiFiltersRt, rangeRt]), }, handler: async ({ context, request }) => { + const { environment } = getParsedUiFilters({ + uiFilters: context.params.query.uiFilters, + logger: context.logger, + }); + const setup = await setupRequest(context, request); - const services = await getServices(setup); + + const services = await getServices({ + setup, + mlAnomaliesEnvironment: environment, + }); return services; }, diff --git a/x-pack/plugins/observability/public/hooks/use_chart_theme.tsx b/x-pack/plugins/observability/public/hooks/use_chart_theme.tsx index 13f7159ba6043f..b5bfe3eec7d35b 100644 --- a/x-pack/plugins/observability/public/hooks/use_chart_theme.tsx +++ b/x-pack/plugins/observability/public/hooks/use_chart_theme.tsx @@ -4,10 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ import { EUI_CHARTS_THEME_DARK, EUI_CHARTS_THEME_LIGHT } from '@elastic/eui/dist/eui_charts_theme'; -import { useContext } from 'react'; -import { ThemeContext } from 'styled-components'; +import { useTheme } from './use_theme'; export function useChartTheme() { - const theme = useContext(ThemeContext); + const theme = useTheme(); return theme.darkMode ? EUI_CHARTS_THEME_DARK.theme : EUI_CHARTS_THEME_LIGHT.theme; } diff --git a/x-pack/plugins/observability/public/hooks/use_theme.tsx b/x-pack/plugins/observability/public/hooks/use_theme.tsx new file mode 100644 index 00000000000000..d0449a4432d93c --- /dev/null +++ b/x-pack/plugins/observability/public/hooks/use_theme.tsx @@ -0,0 +1,13 @@ +/* + * 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 { useContext } from 'react'; +import { ThemeContext } from 'styled-components'; +import { EuiTheme } from '../../../../legacy/common/eui_styled_components'; + +export function useTheme() { + const theme: EuiTheme = useContext(ThemeContext); + return theme; +} diff --git a/x-pack/plugins/observability/public/index.ts b/x-pack/plugins/observability/public/index.ts index 03939736b64ae9..0aecea59ad013e 100644 --- a/x-pack/plugins/observability/public/index.ts +++ b/x-pack/plugins/observability/public/index.ts @@ -26,3 +26,6 @@ export { } from './hooks/use_track_metric'; export * from './typings'; + +export { useChartTheme } from './hooks/use_chart_theme'; +export { useTheme } from './hooks/use_theme'; From d721c17562a54e61ac67c7693a41b8fcbeb7405d Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Fri, 4 Sep 2020 11:05:50 +0200 Subject: [PATCH 02/17] Split out HealthBadge/ServiceListMetric into separate files --- .../ServiceList/HealthBadge.tsx | 66 ++++++++++ .../ServiceList/ServiceListMetric.tsx | 49 ++++++++ .../app/ServiceOverview/ServiceList/index.tsx | 113 ++---------------- .../ServiceOverview.test.tsx.snap | 4 +- 4 files changed, 127 insertions(+), 105 deletions(-) create mode 100644 x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/HealthBadge.tsx create mode 100644 x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/ServiceListMetric.tsx diff --git a/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/HealthBadge.tsx b/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/HealthBadge.tsx new file mode 100644 index 00000000000000..000ece45aafcc1 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/HealthBadge.tsx @@ -0,0 +1,66 @@ +/* + * 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 { EuiBadge } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { + getSeverityColor, + Severity, +} from '../../../../../common/anomaly_detection'; +import { useTheme } from '../../../../hooks/useTheme'; + +export function HealthBadge({ severity }: { severity?: Severity }) { + const theme = useTheme(); + + let label: string = ''; + + switch (severity) { + case Severity.critical: + label = i18n.translate( + 'xpack.apm.servicesTable.serviceHealthStatus.critical', + { + defaultMessage: 'Critical', + } + ); + break; + + case Severity.major: + case Severity.minor: + label = i18n.translate( + 'xpack.apm.servicesTable.serviceHealthStatus.warning', + { + defaultMessage: 'Warning', + } + ); + break; + + case Severity.warning: + label = i18n.translate( + 'xpack.apm.servicesTable.serviceHealthStatus.healthy', + { + defaultMessage: 'Healthy', + } + ); + break; + + default: + label = i18n.translate( + 'xpack.apm.servicesTable.serviceHealthStatus.unknown', + { + defaultMessage: 'Unknown', + } + ); + break; + } + + const unknownColor = theme.eui.euiColorLightShade; + + return ( + + {label} + + ); +} diff --git a/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/ServiceListMetric.tsx b/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/ServiceListMetric.tsx new file mode 100644 index 00000000000000..c94c94d4a0b72c --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/ServiceListMetric.tsx @@ -0,0 +1,49 @@ +/* + * 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 { EuiFlexItem } from '@elastic/eui'; +import { EuiFlexGroup } from '@elastic/eui'; + +import React from 'react'; +import { useTheme } from '../../../../hooks/useTheme'; +import { useUrlParams } from '../../../../hooks/useUrlParams'; +import { getEmptySeries } from '../../../shared/charts/CustomPlot/getEmptySeries'; +import { SparkPlot } from '../../../shared/charts/SparkPlot'; + +export function ServiceListMetric({ + color, + series, + valueLabel, +}: { + color: 'euiColorVis1' | 'euiColorVis0' | 'euiColorVis7'; + series?: Array<{ x: number; y: number | null }>; + valueLabel: React.ReactNode; +}) { + const theme = useTheme(); + + const { + urlParams: { start, end }, + } = useUrlParams(); + + const colorValue = theme.eui[color]; + + return ( + + + + + + {valueLabel} + + + ); +} diff --git a/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/index.tsx b/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/index.tsx index 3ddc28c30ca591..d8c3ea4058d324 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/index.tsx @@ -4,29 +4,24 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiFlexItem, EuiFlexGroup, EuiBadge, EuiToolTip } from '@elastic/eui'; +import { EuiFlexItem, EuiFlexGroup, EuiToolTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React from 'react'; import styled from 'styled-components'; import { ValuesType } from 'utility-types'; import { orderBy } from 'lodash'; -import { useTheme } from '../../../../../../observability/public'; -import { useUrlParams } from '../../../../hooks/useUrlParams'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { ServiceListAPIResponse } from '../../../../../server/lib/services/get_services'; import { NOT_AVAILABLE_LABEL } from '../../../../../common/i18n'; -import { fontSizes, px, truncate } from '../../../../style/variables'; +import { fontSizes, px, truncate, unit } from '../../../../style/variables'; import { asDecimal, asMillisecondDuration } from '../../../../utils/formatters'; import { ManagedTable, ITableColumn } from '../../../shared/ManagedTable'; import { EnvironmentBadge } from '../../../shared/EnvironmentBadge'; import { TransactionOverviewLink } from '../../../shared/Links/apm/TransactionOverviewLink'; import { AgentIcon } from '../../../shared/AgentIcon'; -import { SparkPlot } from '../../../shared/charts/SparkPlot'; -import { getEmptySeries } from '../../../shared/charts/CustomPlot/getEmptySeries'; -import { - getSeverityColor, - Severity, -} from '../../../../../common/anomaly_detection'; +import { Severity } from '../../../../../common/anomaly_detection'; +import { HealthBadge } from './HealthBadge'; +import { ServiceListMetric } from './ServiceListMetric'; interface Props { items: ServiceListAPIResponse['items']; @@ -50,94 +45,6 @@ function formatString(value?: string | null) { return value || NOT_AVAILABLE_LABEL; } -function ServiceListMetric({ - color, - series, - valueLabel, -}: { - color: 'euiColorVis1' | 'euiColorVis0' | 'euiColorVis7'; - series?: Array<{ x: number; y: number | null }>; - valueLabel: React.ReactNode; -}) { - const theme = useTheme(); - - const { - urlParams: { start, end }, - } = useUrlParams(); - - const colorValue = theme.eui[color]; - - return ( - - - - - - {valueLabel} - - - ); -} - -function HealthBadge({ severity }: { severity?: Severity }) { - const theme = useTheme(); - - let label: string = ''; - - switch (severity) { - case Severity.critical: - label = i18n.translate( - 'xpack.apm.servicesTable.serviceHealthStatus.critical', - { - defaultMessage: 'Critical', - } - ); - break; - - case Severity.major: - case Severity.minor: - label = i18n.translate( - 'xpack.apm.servicesTable.serviceHealthStatus.warning', - { - defaultMessage: 'Warning', - } - ); - break; - - case Severity.warning: - label = i18n.translate( - 'xpack.apm.servicesTable.serviceHealthStatus.healthy', - { - defaultMessage: 'Healthy', - } - ); - break; - - default: - label = i18n.translate( - 'xpack.apm.servicesTable.serviceHealthStatus.unknown', - { - defaultMessage: 'Unknown', - } - ); - break; - } - - const unknownColor = theme.eui.euiColorLightShade; - - return ( - - {label} - - ); -} - const AppLink = styled(TransactionOverviewLink)` font-size: ${fontSizes.large}; ${truncate('100%')}; @@ -149,7 +56,7 @@ export const SERVICE_COLUMNS: Array> = [ name: i18n.translate('xpack.apm.servicesTable.healthColumnLabel', { defaultMessage: 'Health', }), - width: px(80), + width: px(unit * 6), sortable: true, render: (_, { severity }) => { return ; @@ -184,7 +91,7 @@ export const SERVICE_COLUMNS: Array> = [ name: i18n.translate('xpack.apm.servicesTable.environmentColumnLabel', { defaultMessage: 'Environment', }), - width: px(160), + width: px(unit * 10), sortable: true, render: (_, { environments }) => ( @@ -205,7 +112,7 @@ export const SERVICE_COLUMNS: Array> = [ /> ), align: 'left', - width: px(160), + width: px(unit * 10), }, { field: 'transactionsPerMinute', @@ -232,7 +139,7 @@ export const SERVICE_COLUMNS: Array> = [ /> ), align: 'left', - width: px(160), + width: px(unit * 10), }, { field: 'errorsPerMinute', @@ -256,7 +163,7 @@ export const SERVICE_COLUMNS: Array> = [ /> ), align: 'left', - width: px(160), + width: px(unit * 10), }, ]; diff --git a/x-pack/plugins/apm/public/components/app/ServiceOverview/__test__/__snapshots__/ServiceOverview.test.tsx.snap b/x-pack/plugins/apm/public/components/app/ServiceOverview/__test__/__snapshots__/ServiceOverview.test.tsx.snap index 767c81cdab9198..29bc7630d18531 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceOverview/__test__/__snapshots__/ServiceOverview.test.tsx.snap +++ b/x-pack/plugins/apm/public/components/app/ServiceOverview/__test__/__snapshots__/ServiceOverview.test.tsx.snap @@ -141,7 +141,7 @@ NodeList [ >
Date: Fri, 4 Sep 2020 11:28:28 +0200 Subject: [PATCH 03/17] Return empty object in anomaly alert --- .../alerts/register_transaction_duration_anomaly_alert_type.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 9234f585232efe..93af51b572aa57 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 @@ -83,7 +83,7 @@ export function registerTransactionDurationAnomalyAlertType({ ); if (mlJobIds.length === 0) { - return; + return {}; } const anomalySearchParams = { From eed20ef6bc9d4753910c3acc766c922ec1bcce7a Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Fri, 4 Sep 2020 11:39:29 +0200 Subject: [PATCH 04/17] Remove unused translations --- x-pack/plugins/translations/translations/ja-JP.json | 1 - x-pack/plugins/translations/translations/zh-CN.json | 1 - 2 files changed, 2 deletions(-) diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index a745c2fc98b449..af15cc7e7c4e14 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -4862,7 +4862,6 @@ "xpack.apm.serviceOverview.upgradeAssistantLink": "アップグレードアシスタント", "xpack.apm.servicesTable.7xOldDataMessage": "また、移行が必要な古いデータがある可能性もあります。", "xpack.apm.servicesTable.7xUpgradeServerMessage": "バージョン7.xより前からのアップグレードですか?また、\n APMサーバーインスタンスを7.0以降にアップグレードしていることも確認してください。", - "xpack.apm.servicesTable.agentColumnLabel": "エージェント", "xpack.apm.servicesTable.avgResponseTimeColumnLabel": "平均応答時間", "xpack.apm.servicesTable.environmentColumnLabel": "環境", "xpack.apm.servicesTable.environmentCount": "{environmentCount, plural, one {1 個の環境} other {# 個の環境}}", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 7b630e1c1348b0..968bd63071678c 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -4865,7 +4865,6 @@ "xpack.apm.serviceOverview.upgradeAssistantLink": "升级助手", "xpack.apm.servicesTable.7xOldDataMessage": "可能还有需要迁移的旧数据。", "xpack.apm.servicesTable.7xUpgradeServerMessage": "从 7.x 之前的版本升级?另外,确保您已将\n APM Server 实例升级到至少 7.0。", - "xpack.apm.servicesTable.agentColumnLabel": "代理", "xpack.apm.servicesTable.avgResponseTimeColumnLabel": "平均响应时间", "xpack.apm.servicesTable.environmentColumnLabel": "环境", "xpack.apm.servicesTable.environmentCount": "{environmentCount, plural, one {1 个环境} other {# 个环境}}", From 8a7fa9f9a21b0dbe633e783427ed6aa178109e0f Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Mon, 7 Sep 2020 13:14:27 +0200 Subject: [PATCH 05/17] Review feedback --- .../plugins/apm/common/anomaly_detection.ts | 44 ++++++++++++++++ .../ServiceList/HealthBadge.tsx | 45 +--------------- .../app/ServiceOverview/ServiceList/index.tsx | 6 ++- .../__test__/ServiceOverview.test.tsx | 43 ++++++++-------- .../components/app/ServiceOverview/index.tsx | 12 ++--- ...Settings.ts => useAnomalyDetectionJobs.ts} | 2 +- .../get_services/get_services_items_stats.ts | 51 ++++++++++--------- 7 files changed, 104 insertions(+), 99 deletions(-) rename x-pack/plugins/apm/public/hooks/{useAnomalyDetectionSettings.ts => useAnomalyDetectionJobs.ts} (90%) diff --git a/x-pack/plugins/apm/common/anomaly_detection.ts b/x-pack/plugins/apm/common/anomaly_detection.ts index 41f66ed8ad9946..bce46721c52591 100644 --- a/x-pack/plugins/apm/common/anomaly_detection.ts +++ b/x-pack/plugins/apm/common/anomaly_detection.ts @@ -53,6 +53,50 @@ export function getSeverityColor(theme: EuiTheme, severity?: Severity) { } } +export function getSeverityLabel(severity?: Severity) { + let label: string = ''; + + switch (severity) { + case Severity.critical: + label = i18n.translate( + 'xpack.apm.servicesTable.serviceHealthStatus.critical', + { + defaultMessage: 'Critical', + } + ); + break; + + case Severity.major: + case Severity.minor: + label = i18n.translate( + 'xpack.apm.servicesTable.serviceHealthStatus.warning', + { + defaultMessage: 'Warning', + } + ); + break; + + case Severity.warning: + label = i18n.translate( + 'xpack.apm.servicesTable.serviceHealthStatus.healthy', + { + defaultMessage: 'Healthy', + } + ); + break; + + default: + label = i18n.translate( + 'xpack.apm.servicesTable.serviceHealthStatus.unknown', + { + defaultMessage: 'Unknown', + } + ); + break; + } + return label; +} + export const ML_ERRORS = { INVALID_LICENSE: i18n.translate( 'xpack.apm.anomaly_detection.error.invalid_license', diff --git a/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/HealthBadge.tsx b/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/HealthBadge.tsx index 000ece45aafcc1..94353080bc7d5b 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/HealthBadge.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/HealthBadge.tsx @@ -5,9 +5,9 @@ */ import React from 'react'; import { EuiBadge } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; import { getSeverityColor, + getSeverityLabel, Severity, } from '../../../../../common/anomaly_detection'; import { useTheme } from '../../../../hooks/useTheme'; @@ -15,52 +15,11 @@ import { useTheme } from '../../../../hooks/useTheme'; export function HealthBadge({ severity }: { severity?: Severity }) { const theme = useTheme(); - let label: string = ''; - - switch (severity) { - case Severity.critical: - label = i18n.translate( - 'xpack.apm.servicesTable.serviceHealthStatus.critical', - { - defaultMessage: 'Critical', - } - ); - break; - - case Severity.major: - case Severity.minor: - label = i18n.translate( - 'xpack.apm.servicesTable.serviceHealthStatus.warning', - { - defaultMessage: 'Warning', - } - ); - break; - - case Severity.warning: - label = i18n.translate( - 'xpack.apm.servicesTable.serviceHealthStatus.healthy', - { - defaultMessage: 'Healthy', - } - ); - break; - - default: - label = i18n.translate( - 'xpack.apm.servicesTable.serviceHealthStatus.unknown', - { - defaultMessage: 'Unknown', - } - ); - break; - } - const unknownColor = theme.eui.euiColorLightShade; return ( - {label} + {getSeverityLabel(severity)} ); } diff --git a/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/index.tsx b/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/index.tsx index d8c3ea4058d324..31eb77a08d681a 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/index.tsx @@ -70,7 +70,11 @@ export const SERVICE_COLUMNS: Array> = [ width: '40%', sortable: true, render: (_, { serviceName, agentName }) => ( - + {agentName && ( diff --git a/x-pack/plugins/apm/public/components/app/ServiceOverview/__test__/ServiceOverview.test.tsx b/x-pack/plugins/apm/public/components/app/ServiceOverview/__test__/ServiceOverview.test.tsx index 660d7c1a9bc275..8eeff018ad03f3 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceOverview/__test__/ServiceOverview.test.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceOverview/__test__/ServiceOverview.test.tsx @@ -8,6 +8,7 @@ import { render, wait, waitForElement } from '@testing-library/react'; import { CoreStart } from 'kibana/public'; import React, { FunctionComponent, ReactChild } from 'react'; import { createKibanaReactContext } from 'src/plugins/kibana_react/public'; +import { merge } from 'lodash'; import { ServiceOverview } from '..'; import { ApmPluginContextValue } from '../../../../context/ApmPluginContext'; import { @@ -17,7 +18,7 @@ import { import { FETCH_STATUS } from '../../../../hooks/useFetcher'; import * as useLocalUIFilters from '../../../../hooks/useLocalUIFilters'; import * as urlParamsHooks from '../../../../hooks/useUrlParams'; -import * as useAnomalyDetectionSettings from '../../../../hooks/useAnomalyDetectionSettings'; +import * as useAnomalyDetectionJobs from '../../../../hooks/useAnomalyDetectionJobs'; import { SessionStorageMock } from '../../../../services/__test__/SessionStorageMock'; import { EuiThemeProvider } from '../../../../../../../legacy/common/eui_styled_components'; @@ -25,28 +26,27 @@ const KibanaReactContext = createKibanaReactContext({ usageCollection: { reportUiStats: () => {} }, } as Partial); +const addWarning = jest.fn(); +const httpGet = jest.fn(); + function wrapper({ children }: { children: ReactChild }) { + const mockPluginContext = (merge({}, mockApmPluginContextValue, { + core: { + http: { + get: httpGet, + }, + notifications: { + toasts: { + addWarning, + }, + }, + }, + }) as unknown) as ApmPluginContextValue; + return ( - + {children} @@ -60,9 +60,6 @@ function renderServiceOverview() { }); } -const addWarning = jest.fn(); -const httpGet = jest.fn(); - describe('Service Overview -> View', () => { beforeEach(() => { // @ts-expect-error @@ -86,7 +83,7 @@ describe('Service Overview -> View', () => { }); jest - .spyOn(useAnomalyDetectionSettings, 'useAnomalyDetectionSettings') + .spyOn(useAnomalyDetectionJobs, 'useAnomalyDetectionJobs') .mockReturnValue({ status: FETCH_STATUS.SUCCESS, data: { diff --git a/x-pack/plugins/apm/public/components/app/ServiceOverview/index.tsx b/x-pack/plugins/apm/public/components/app/ServiceOverview/index.tsx index 5b450ab03cd30f..d9d2cffb676205 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceOverview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceOverview/index.tsx @@ -20,7 +20,7 @@ import { LocalUIFilters } from '../../shared/LocalUIFilters'; import { useApmPluginContext } from '../../../hooks/useApmPluginContext'; import { MLCallout } from './ServiceList/MLCallout'; import { useLocalStorage } from '../../../hooks/useLocalStorage'; -import { useAnomalyDetectionSettings } from '../../../hooks/useAnomalyDetectionSettings'; +import { useAnomalyDetectionJobs } from '../../../hooks/useAnomalyDetectionJobs'; const initialData = { items: [], @@ -97,9 +97,9 @@ export function ServiceOverview() { ); const { - data: anomalyDetectionSettingsData, - status: anomalyDetectionSettingsStatus, - } = useAnomalyDetectionSettings(); + data: anomalyDetectionJobsData, + status: anomalyDetectionJobsStatus, + } = useAnomalyDetectionJobs(); const [userHasDismissedCallout, setUserHasDismissedCallout] = useLocalStorage( 'apm.userHasDismissedServiceInventoryMlCallout', @@ -109,8 +109,8 @@ export function ServiceOverview() { const canCreateJob = !!core.application.capabilities.ml?.canCreateJob; const displayMlCallout = - anomalyDetectionSettingsStatus === FETCH_STATUS.SUCCESS && - !anomalyDetectionSettingsData?.jobs.length && + anomalyDetectionJobsStatus === FETCH_STATUS.SUCCESS && + !anomalyDetectionJobsData?.jobs.length && canCreateJob && !userHasDismissedCallout; diff --git a/x-pack/plugins/apm/public/hooks/useAnomalyDetectionSettings.ts b/x-pack/plugins/apm/public/hooks/useAnomalyDetectionJobs.ts similarity index 90% rename from x-pack/plugins/apm/public/hooks/useAnomalyDetectionSettings.ts rename to x-pack/plugins/apm/public/hooks/useAnomalyDetectionJobs.ts index d3ec664d9bd70a..56c58bc82967b8 100644 --- a/x-pack/plugins/apm/public/hooks/useAnomalyDetectionSettings.ts +++ b/x-pack/plugins/apm/public/hooks/useAnomalyDetectionJobs.ts @@ -6,7 +6,7 @@ import { useFetcher } from './useFetcher'; -export function useAnomalyDetectionSettings() { +export function useAnomalyDetectionJobs() { return useFetcher( (callApmApi) => callApmApi({ diff --git a/x-pack/plugins/apm/server/lib/services/get_services/get_services_items_stats.ts b/x-pack/plugins/apm/server/lib/services/get_services/get_services_items_stats.ts index e29aa534877901..077e88cd471d86 100644 --- a/x-pack/plugins/apm/server/lib/services/get_services/get_services_items_stats.ts +++ b/x-pack/plugins/apm/server/lib/services/get_services/get_services_items_stats.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getSeverity, Severity } from '../../../../common/anomaly_detection'; +import { getSeverity } from '../../../../common/anomaly_detection'; import { AgentName } from '../../../../typings/es_schemas/ui/fields/agent'; import { TRANSACTION_DURATION, @@ -90,11 +90,11 @@ export const getTransactionDurationAverages = async ({ return []; } - return aggregations.services.buckets.map((bucket) => ({ - serviceName: bucket.key as string, + return aggregations.services.buckets.map((serviceBucket) => ({ + serviceName: serviceBucket.key as string, avgResponseTime: { - value: bucket.average.value, - over_time: bucket.over_time.buckets.map((dateBucket) => ({ + value: serviceBucket.average.value, + over_time: serviceBucket.over_time.buckets.map((dateBucket) => ({ x: dateBucket.key, y: dateBucket.average.value, })), @@ -144,9 +144,10 @@ export const getAgentNames = async ({ return []; } - return aggregations.services.buckets.map((bucket) => ({ - serviceName: bucket.key as string, - agentName: bucket.agent_name.hits.hits[0]?._source.agent.name as AgentName, + return aggregations.services.buckets.map((serviceBucket) => ({ + serviceName: serviceBucket.key as string, + agentName: serviceBucket.agent_name.hits.hits[0]?._source.agent + .name as AgentName, })); }; @@ -187,13 +188,13 @@ export const getTransactionRates = async ({ const deltaAsMinutes = getDeltaAsMinutes(setup); - return aggregations.services.buckets.map((bucket) => { - const transactionsPerMinute = bucket.doc_count / deltaAsMinutes; + return aggregations.services.buckets.map((serviceBucket) => { + const transactionsPerMinute = serviceBucket.doc_count / deltaAsMinutes; return { - serviceName: bucket.key as string, + serviceName: serviceBucket.key as string, transactionsPerMinute: { value: transactionsPerMinute, - over_time: bucket.over_time.buckets.map((dateBucket) => ({ + over_time: serviceBucket.over_time.buckets.map((dateBucket) => ({ x: dateBucket.key, y: dateBucket.doc_count / deltaAsMinutes, })), @@ -239,13 +240,13 @@ export const getErrorRates = async ({ const deltaAsMinutes = getDeltaAsMinutes(setup); - return aggregations.services.buckets.map((bucket) => { - const errorsPerMinute = bucket.doc_count / deltaAsMinutes; + return aggregations.services.buckets.map((serviceBucket) => { + const errorsPerMinute = serviceBucket.doc_count / deltaAsMinutes; return { - serviceName: bucket.key as string, + serviceName: serviceBucket.key as string, errorsPerMinute: { value: errorsPerMinute, - over_time: bucket.over_time.buckets.map((dateBucket) => ({ + over_time: serviceBucket.over_time.buckets.map((dateBucket) => ({ x: dateBucket.key, y: dateBucket.doc_count / deltaAsMinutes, })), @@ -295,9 +296,11 @@ export const getEnvironments = async ({ return []; } - return aggregations.services.buckets.map((bucket) => ({ - serviceName: bucket.key as string, - environments: bucket.environments.buckets.map((env) => env.key as string), + return aggregations.services.buckets.map((serviceBucket) => ({ + serviceName: serviceBucket.key as string, + environments: serviceBucket.environments.buckets.map( + (envBucket) => envBucket.key as string + ), })); }; @@ -322,16 +325,14 @@ export const getHealthStatuses = async ( environment: mlAnomaliesEnvironment, }); - return Object.keys(anomalies.serviceAnomalies).reduce< - Array<{ serviceName: string; severity?: Severity }> - >((prev, serviceName) => { + return Object.keys(anomalies.serviceAnomalies).map((serviceName) => { const stats = anomalies.serviceAnomalies[serviceName]; const severity = getSeverity(stats.anomalyScore); - return prev.concat({ + return { serviceName, severity, - }); - }, []); + }; + }); }; From d2dce16f72ba2ed39ce08c4caf3e674867bed096 Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Mon, 7 Sep 2020 16:02:53 +0200 Subject: [PATCH 06/17] Use transaction error rate for services overview --- .../__test__/__snapshots__/List.test.js.snap | 4 +- .../app/ServiceOverview/ServiceList/index.tsx | 22 +++---- .../ServiceOverview.test.tsx.snap | 8 +-- .../__snapshots__/queries.test.ts.snap | 22 ++++++- .../get_services/get_services_items.ts | 8 +-- .../get_services/get_services_items_stats.ts | 63 ++++++++++++++++--- .../apm/typings/elasticsearch/aggregations.ts | 6 ++ 7 files changed, 98 insertions(+), 35 deletions(-) diff --git a/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/__test__/__snapshots__/List.test.js.snap b/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/__test__/__snapshots__/List.test.js.snap index 15e20095382703..41dbe9d949a760 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/__test__/__snapshots__/List.test.js.snap +++ b/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/__test__/__snapshots__/List.test.js.snap @@ -42,7 +42,7 @@ exports[`ServiceOverview -> List renders empty state 1`] = ` "align": "left", "dataType": "number", "field": "errorsPerMinute", - "name": "Errors per minute", + "name": "Error rate %", "render": [Function], "sortable": true, "width": "160px", @@ -97,7 +97,7 @@ exports[`ServiceOverview -> List renders with data 1`] = ` "align": "left", "dataType": "number", "field": "errorsPerMinute", - "name": "Errors per minute", + "name": "Error rate %", "render": [Function], "sortable": true, "width": "160px", diff --git a/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/index.tsx b/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/index.tsx index 31eb77a08d681a..96cddbd7df2038 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/index.tsx @@ -10,6 +10,7 @@ import React from 'react'; import styled from 'styled-components'; import { ValuesType } from 'utility-types'; import { orderBy } from 'lodash'; +import { asPercent } from '../../../../../common/utils/formatters'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { ServiceListAPIResponse } from '../../../../../server/lib/services/get_services'; import { NOT_AVAILABLE_LABEL } from '../../../../../common/i18n'; @@ -147,23 +148,16 @@ export const SERVICE_COLUMNS: Array> = [ }, { field: 'errorsPerMinute', - name: i18n.translate('xpack.apm.servicesTable.errorsPerMinuteColumnLabel', { - defaultMessage: 'Errors per minute', + name: i18n.translate('xpack.apm.servicesTable.transactionErrorRate', { + defaultMessage: 'Error rate %', }), sortable: true, dataType: 'number', - render: (_, { errorsPerMinute }) => ( + render: (_, { transactionErrorRate }) => ( ), align: 'left', @@ -219,8 +213,8 @@ export function ServiceList({ return item.avgResponseTime?.value ?? 0; case 'transactionsPerMinute': return item.transactionsPerMinute?.value ?? 0; - case 'errorsPerMinute': - return item.errorsPerMinute?.value ?? 0; + case 'transactionErrorRate': + return item.transactionErrorRate?.value ?? 0; default: return item[sortField as keyof typeof item]; diff --git a/x-pack/plugins/apm/public/components/app/ServiceOverview/__test__/__snapshots__/ServiceOverview.test.tsx.snap b/x-pack/plugins/apm/public/components/app/ServiceOverview/__test__/__snapshots__/ServiceOverview.test.tsx.snap index 29bc7630d18531..ed5a8d797dd1bd 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceOverview/__test__/__snapshots__/ServiceOverview.test.tsx.snap +++ b/x-pack/plugins/apm/public/components/app/ServiceOverview/__test__/__snapshots__/ServiceOverview.test.tsx.snap @@ -355,7 +355,7 @@ NodeList [
- Errors per minute + Error rate %
- 0 err. + 0%
@@ -595,7 +595,7 @@ NodeList [
- Errors per minute + Error rate %
- 0 err. + 0%
diff --git a/x-pack/plugins/apm/server/lib/services/__snapshots__/queries.test.ts.snap b/x-pack/plugins/apm/server/lib/services/__snapshots__/queries.test.ts.snap index 34f4babf55ea1f..6a367e961ae6df 100644 --- a/x-pack/plugins/apm/server/lib/services/__snapshots__/queries.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/services/__snapshots__/queries.test.ts.snap @@ -257,14 +257,26 @@ Array [ Object { "apm": Object { "events": Array [ - "error", + "transaction", ], }, "body": Object { "aggs": Object { "services": Object { "aggs": Object { + "outcomes": Object { + "terms": Object { + "field": "event.outcome", + }, + }, "over_time": Object { + "aggs": Object { + "outcomes": Object { + "terms": Object { + "field": "event.outcome", + }, + }, + }, "date_histogram": Object { "extended_bounds": Object { "max": 1528977600000, @@ -299,6 +311,14 @@ Array [ "my.custom.ui.filter": "foo-bar", }, }, + Object { + "terms": Object { + "event.outcome": Array [ + "failure", + "success", + ], + }, + }, ], }, }, diff --git a/x-pack/plugins/apm/server/lib/services/get_services/get_services_items.ts b/x-pack/plugins/apm/server/lib/services/get_services/get_services_items.ts index 84e6e230ff3b8a..50a968467fb4b5 100644 --- a/x-pack/plugins/apm/server/lib/services/get_services/get_services_items.ts +++ b/x-pack/plugins/apm/server/lib/services/get_services/get_services_items.ts @@ -15,7 +15,7 @@ import { getTransactionDurationAverages, getAgentNames, getTransactionRates, - getErrorRates, + getTransactionErrorRates, getEnvironments, getHealthStatuses, } from './get_services_items_stats'; @@ -40,14 +40,14 @@ export async function getServicesItems({ transactionDurationAverages, agentNames, transactionRates, - errorRates, + transactionErrorRates, environments, healthStatuses, ] = await Promise.all([ getTransactionDurationAverages(params), getAgentNames(params), getTransactionRates(params), - getErrorRates(params), + getTransactionErrorRates(params), getEnvironments(params), getHealthStatuses(params, mlAnomaliesEnvironment), ]); @@ -56,7 +56,7 @@ export async function getServicesItems({ ...transactionDurationAverages, ...agentNames, ...transactionRates, - ...errorRates, + ...transactionErrorRates, ...environments, ...healthStatuses, ]; diff --git a/x-pack/plugins/apm/server/lib/services/get_services/get_services_items_stats.ts b/x-pack/plugins/apm/server/lib/services/get_services/get_services_items_stats.ts index 077e88cd471d86..f7019fd8b0dce6 100644 --- a/x-pack/plugins/apm/server/lib/services/get_services/get_services_items_stats.ts +++ b/x-pack/plugins/apm/server/lib/services/get_services/get_services_items_stats.ts @@ -4,12 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ +import { EventOutcome } from '../../../../common/event_outcome'; import { getSeverity } from '../../../../common/anomaly_detection'; import { AgentName } from '../../../../typings/es_schemas/ui/fields/agent'; import { TRANSACTION_DURATION, AGENT_NAME, SERVICE_ENVIRONMENT, + EVENT_OUTCOME, } from '../../../../common/elasticsearch_fieldnames'; import { mergeProjection } from '../../../projections/util/merge_projection'; import { ProcessorEvent } from '../../../../common/processor_event'; @@ -22,6 +24,7 @@ import { getMLJobIds, getServiceAnomalies, } from '../../service_map/get_service_anomalies'; +import { AggregationResultOf } from '../../../../typings/elasticsearch/aggregations'; function getDateHistogramOpts(start: number, end: number) { return { @@ -203,18 +206,37 @@ export const getTransactionRates = async ({ }); }; -export const getErrorRates = async ({ +export const getTransactionErrorRates = async ({ setup, projection, }: AggregationParams) => { const { apmEventClient, start, end } = setup; + + const outcomes = { + terms: { + field: EVENT_OUTCOME, + }, + }; + const response = await apmEventClient.search( mergeProjection(projection, { apm: { - events: [ProcessorEvent.error], + events: [ProcessorEvent.transaction], }, body: { size: 0, + query: { + bool: { + filter: [ + ...projection.body.query.bool.filter, + { + terms: { + [EVENT_OUTCOME]: [EventOutcome.failure, EventOutcome.success], + }, + }, + ], + }, + }, aggs: { services: { terms: { @@ -222,8 +244,12 @@ export const getErrorRates = async ({ size: MAX_NUMBER_OF_SERVICES, }, aggs: { + outcomes, over_time: { date_histogram: getDateHistogramOpts(start, end), + aggs: { + outcomes, + }, }, }, }, @@ -238,18 +264,35 @@ export const getErrorRates = async ({ return []; } - const deltaAsMinutes = getDeltaAsMinutes(setup); + function calculateTransactionErrorPercentage( + outcomeResponse: AggregationResultOf + ) { + const successfulTransactions = + outcomeResponse.buckets.find( + (bucket) => bucket.key === EventOutcome.success + )?.doc_count ?? 0; + const failedTransactions = + outcomeResponse.buckets.find( + (bucket) => bucket.key === EventOutcome.failure + )?.doc_count ?? 0; + + return failedTransactions / (successfulTransactions + failedTransactions); + } return aggregations.services.buckets.map((serviceBucket) => { - const errorsPerMinute = serviceBucket.doc_count / deltaAsMinutes; + const transactionErrorRate = calculateTransactionErrorPercentage( + serviceBucket.outcomes + ); return { serviceName: serviceBucket.key as string, - errorsPerMinute: { - value: errorsPerMinute, - over_time: serviceBucket.over_time.buckets.map((dateBucket) => ({ - x: dateBucket.key, - y: dateBucket.doc_count / deltaAsMinutes, - })), + transactionErrorRate: { + value: transactionErrorRate, + over_time: serviceBucket.over_time.buckets.map((dateBucket) => { + return { + x: dateBucket.key, + y: calculateTransactionErrorPercentage(dateBucket.outcomes), + }; + }), }, }; }); diff --git a/x-pack/plugins/apm/typings/elasticsearch/aggregations.ts b/x-pack/plugins/apm/typings/elasticsearch/aggregations.ts index f9576141225475..ef2b59c7315488 100644 --- a/x-pack/plugins/apm/typings/elasticsearch/aggregations.ts +++ b/x-pack/plugins/apm/typings/elasticsearch/aggregations.ts @@ -346,6 +346,12 @@ export type ValidAggregationKeysOf< T extends Record > = keyof (UnionToIntersection extends never ? T : UnionToIntersection); +export type AggregationResultOf< + TAggregationOptionsMap extends AggregationOptionsMap, + TDocument +> = AggregationResponsePart[AggregationType & + ValidAggregationKeysOf]; + export type AggregationResponseMap< TAggregationInputMap extends AggregationInputMap | undefined, TDocument From f35ca0413a9d1bf3d2c2984f5389b9045ce4b700 Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Tue, 8 Sep 2020 10:37:38 +0200 Subject: [PATCH 07/17] Remove unused translations --- x-pack/plugins/translations/translations/ja-JP.json | 2 -- x-pack/plugins/translations/translations/zh-CN.json | 2 -- 2 files changed, 4 deletions(-) diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 385030909a6115..20c07c3d8eda33 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -4865,8 +4865,6 @@ "xpack.apm.servicesTable.avgResponseTimeColumnLabel": "平均応答時間", "xpack.apm.servicesTable.environmentColumnLabel": "環境", "xpack.apm.servicesTable.environmentCount": "{environmentCount, plural, one {1 個の環境} other {# 個の環境}}", - "xpack.apm.servicesTable.errorsPerMinuteColumnLabel": "1 分あたりのエラー", - "xpack.apm.servicesTable.errorsPerMinuteUnitLabel": "エラー", "xpack.apm.servicesTable.nameColumnLabel": "名前", "xpack.apm.servicesTable.noServicesLabel": "APM サービスがインストールされていないようです。追加しましょう!", "xpack.apm.servicesTable.notFoundLabel": "サービスが見つかりません", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 2dc231566d7c65..4c049e8c002d04 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -4868,8 +4868,6 @@ "xpack.apm.servicesTable.avgResponseTimeColumnLabel": "平均响应时间", "xpack.apm.servicesTable.environmentColumnLabel": "环境", "xpack.apm.servicesTable.environmentCount": "{environmentCount, plural, one {1 个环境} other {# 个环境}}", - "xpack.apm.servicesTable.errorsPerMinuteColumnLabel": "每分钟错误数", - "xpack.apm.servicesTable.errorsPerMinuteUnitLabel": "错误", "xpack.apm.servicesTable.nameColumnLabel": "名称", "xpack.apm.servicesTable.noServicesLabel": "似乎您没有安装任何 APM 服务。让我们添加一些!", "xpack.apm.servicesTable.notFoundLabel": "未找到任何服务", From dd10641487bdabef82a6c62ece822d7549dec4eb Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Wed, 9 Sep 2020 12:43:49 +0200 Subject: [PATCH 08/17] API tests --- .../apm/common/utils/decode_or_throw.ts | 26 ++++++ .../apm/server/lib/helpers/setup_request.ts | 20 ++--- .../routes/settings/anomaly_detection.ts | 7 +- .../basic/tests/services/top_services.ts | 87 +++++++++++-------- .../apm_api_integration/trial/tests/index.ts | 1 + .../trial/tests/services/top_services.ts | 79 +++++++++++++++++ 6 files changed, 170 insertions(+), 50 deletions(-) create mode 100644 x-pack/plugins/apm/common/utils/decode_or_throw.ts create mode 100644 x-pack/test/apm_api_integration/trial/tests/services/top_services.ts diff --git a/x-pack/plugins/apm/common/utils/decode_or_throw.ts b/x-pack/plugins/apm/common/utils/decode_or_throw.ts new file mode 100644 index 00000000000000..b5ed7c9d5072fb --- /dev/null +++ b/x-pack/plugins/apm/common/utils/decode_or_throw.ts @@ -0,0 +1,26 @@ +/* + * 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 * as t from 'io-ts'; +import { fold } from 'fp-ts/lib/Either'; +import { pipe } from 'fp-ts/lib/pipeable'; + +import { identity } from 'fp-ts/lib/function'; +import { throwErrors } from '../../../infra/common/runtime_types'; + +export const decodeOrThrow = ( + runtimeType: t.Type, + inputValue: any +): A => + pipe( + runtimeType.decode(inputValue), + fold( + throwErrors((error) => { + throw new Error(error); + }), + identity + ) + ); diff --git a/x-pack/plugins/apm/server/lib/helpers/setup_request.ts b/x-pack/plugins/apm/server/lib/helpers/setup_request.ts index 6b69e57389dffd..46773b6eb2c378 100644 --- a/x-pack/plugins/apm/server/lib/helpers/setup_request.ts +++ b/x-pack/plugins/apm/server/lib/helpers/setup_request.ts @@ -5,6 +5,7 @@ */ import moment from 'moment'; +import { isValidPlatinumLicense } from '../../../common/service_map'; import { UI_SETTINGS } from '../../../../../../src/plugins/data/common'; import { KibanaRequest } from '../../../../../../src/core/server'; import { APMConfig } from '../..'; @@ -98,11 +99,14 @@ export async function setupRequest( context, request, }), - ml: getMlSetup( - context.plugins.ml, - context.core.savedObjects.client, - request - ), + ml: + context.plugins.ml && isValidPlatinumLicense(context.licensing.license) + ? getMlSetup( + context.plugins.ml, + context.core.savedObjects.client, + request + ) + : undefined, config, }; @@ -115,14 +119,10 @@ export async function setupRequest( } function getMlSetup( - ml: APMRequestHandlerContext['plugins']['ml'], + ml: Required['ml'], savedObjectsClient: APMRequestHandlerContext['core']['savedObjects']['client'], request: KibanaRequest ) { - if (!ml) { - return; - } - return { mlSystem: ml.mlSystemProvider(request), anomalyDetectors: ml.anomalyDetectorsProvider(request), diff --git a/x-pack/plugins/apm/server/routes/settings/anomaly_detection.ts b/x-pack/plugins/apm/server/routes/settings/anomaly_detection.ts index ac25f22751f2f5..4463437b61abde 100644 --- a/x-pack/plugins/apm/server/routes/settings/anomaly_detection.ts +++ b/x-pack/plugins/apm/server/routes/settings/anomaly_detection.ts @@ -6,6 +6,7 @@ import * as t from 'io-ts'; import Boom from 'boom'; +import { isValidPlatinumLicense } from '../../../common/service_map'; import { ML_ERRORS } from '../../../common/anomaly_detection'; import { createRoute } from '../create_route'; import { getAnomalyDetectionJobs } from '../../lib/anomaly_detection/get_anomaly_detection_jobs'; @@ -24,8 +25,7 @@ export const anomalyDetectionJobsRoute = createRoute(() => ({ handler: async ({ context, request }) => { const setup = await setupRequest(context, request); - const license = context.licensing.license; - if (!license.isActive || !license.hasAtLeast('platinum')) { + if (!isValidPlatinumLicense(context.licensing.license)) { throw Boom.forbidden(ML_ERRORS.INVALID_LICENSE); } @@ -56,8 +56,7 @@ export const createAnomalyDetectionJobsRoute = createRoute(() => ({ const { environments } = context.params.body; const setup = await setupRequest(context, request); - const license = context.licensing.license; - if (!license.isActive || !license.hasAtLeast('platinum')) { + if (!isValidPlatinumLicense(context.licensing.license)) { throw Boom.forbidden(ML_ERRORS.INVALID_LICENSE); } diff --git a/x-pack/test/apm_api_integration/basic/tests/services/top_services.ts b/x-pack/test/apm_api_integration/basic/tests/services/top_services.ts index ea3ed2539c12fb..f54e53f04729b1 100644 --- a/x-pack/test/apm_api_integration/basic/tests/services/top_services.ts +++ b/x-pack/test/apm_api_integration/basic/tests/services/top_services.ts @@ -4,17 +4,22 @@ * you may not use this file except in compliance with the Elastic License. */ -import { sortBy } from 'lodash'; import expect from '@kbn/expect'; +import * as t from 'io-ts'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { decodeOrThrow } from '../../../../../plugins/apm/common/utils/decode_or_throw'; +import archives_metadata from '../../archives_metadata'; export default function ApiTest({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const esArchiver = getService('esArchiver'); + const range = archives_metadata['apm_8.0.0']; + // url parameters - const start = encodeURIComponent('2020-06-29T06:45:00.000Z'); - const end = encodeURIComponent('2020-06-29T06:49:00.000Z'); + const start = encodeURIComponent(range.start); + const end = encodeURIComponent(range.end); + const uiFilters = encodeURIComponent(JSON.stringify({})); describe('APM Services Overview', () => { @@ -30,46 +35,56 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); describe('when data is loaded', () => { - before(() => esArchiver.load('8.0.0')); - after(() => esArchiver.unload('8.0.0')); + before(() => esArchiver.load('apm_8.0.0')); + after(() => esArchiver.unload('apm_8.0.0')); it('returns a list of services', async () => { const response = await supertest.get( `/api/apm/services?start=${start}&end=${end}&uiFilters=${uiFilters}` ); - // sort services to mitigate unstable sort order - const services = sortBy(response.body.items, ['serviceName']); expect(response.status).to.be(200); - expect(services).to.eql([ - { - serviceName: 'client', - agentName: 'rum-js', - transactionsPerMinute: 2, - errorsPerMinute: 2.75, - avgResponseTime: 116375, - environments: [], - }, - { - serviceName: 'opbeans-java', - agentName: 'java', - transactionsPerMinute: 30.75, - errorsPerMinute: 4.5, - avgResponseTime: 25636.349593495936, - environments: ['production'], - }, - { - serviceName: 'opbeans-node', - agentName: 'nodejs', - transactionsPerMinute: 31, - errorsPerMinute: 3.75, - avgResponseTime: 38682.52419354839, - environments: ['production'], - }, - ]); - - expect(response.body.hasHistoricalData).to.be(true); - expect(response.body.hasLegacyData).to.be(false); + + const metricType = t.strict({ + value: t.number, + over_time: t.array( + t.type({ + x: t.number, + y: t.union([t.number, t.null]), + }) + ), + }); + + const serviceType = t.strict({ + serviceName: t.string, + agentName: t.string, + transactionsPerMinute: metricType, + avgResponseTime: metricType, + // the RUM service will not have transaction error data + transactionErrorRate: t.union([metricType, t.undefined]), + environments: t.array(t.string), + // basic license should not have anomaly scores + severity: t.undefined, + }); + + const responseType = t.type({ + items: t.array(serviceType), + hasHistoricalData: t.literal(true), + hasLegacyData: t.literal(false), + }); + + const data = decodeOrThrow(responseType, response.body); + + const rumService = data.items.find((item) => item.serviceName === 'client'); + + expect(rumService!.transactionErrorRate).to.be(undefined); + + const nonRumServices = data.items.filter((item) => item.serviceName !== 'client'); + + // all other services should have a transaction error rate + expect( + nonRumServices.every((item) => typeof item.transactionErrorRate?.value === 'number') + ); }); }); }); diff --git a/x-pack/test/apm_api_integration/trial/tests/index.ts b/x-pack/test/apm_api_integration/trial/tests/index.ts index 1b3b5602445edc..28b096c8b5cd3c 100644 --- a/x-pack/test/apm_api_integration/trial/tests/index.ts +++ b/x-pack/test/apm_api_integration/trial/tests/index.ts @@ -13,6 +13,7 @@ export default function observabilityApiIntegrationTests({ loadTestFile }: FtrPr describe('Services', function () { loadTestFile(require.resolve('./services/annotations')); loadTestFile(require.resolve('./services/rum_services.ts')); + loadTestFile(require.resolve('./services/top_services.ts')); }); describe('Settings', function () { diff --git a/x-pack/test/apm_api_integration/trial/tests/services/top_services.ts b/x-pack/test/apm_api_integration/trial/tests/services/top_services.ts new file mode 100644 index 00000000000000..4da922a8ab67b9 --- /dev/null +++ b/x-pack/test/apm_api_integration/trial/tests/services/top_services.ts @@ -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 expect from '@kbn/expect'; +import * as t from 'io-ts'; +import { Severity } from '../../../../../plugins/apm/common/anomaly_detection'; +import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { decodeOrThrow } from '../../../../../plugins/apm/common/utils/decode_or_throw'; +import archives_metadata from '../../archives_metadata'; + +export default function ApiTest({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + + const range = archives_metadata['apm_8.0.0']; + + // url parameters + const start = encodeURIComponent(range.start); + const end = encodeURIComponent(range.end); + + const uiFilters = encodeURIComponent(JSON.stringify({})); + + describe('APM Services Overview', () => { + describe('when data is loaded', () => { + before(() => esArchiver.load('apm_8.0.0')); + after(() => esArchiver.unload('apm_8.0.0')); + + it('returns a list of services with anomaly scores', async () => { + const response = await supertest.get( + `/api/apm/services?start=${start}&end=${end}&uiFilters=${uiFilters}` + ); + + expect(response.status).to.be(200); + + const metricType = t.strict({ + value: t.number, + over_time: t.array( + t.type({ + x: t.number, + y: t.union([t.number, t.null]), + }) + ), + }); + + const serviceType = t.strict({ + serviceName: t.string, + agentName: t.string, + transactionsPerMinute: metricType, + avgResponseTime: metricType, + // the RUM service will not have transaction error data + transactionErrorRate: t.union([metricType, t.undefined]), + environments: t.array(t.string), + severity: t.union([ + t.literal(Severity.critical), + t.literal(Severity.major), + t.literal(Severity.minor), + t.literal(Severity.warning), + t.undefined, + ]), + }); + + const responseType = t.type({ + items: t.array(serviceType), + hasHistoricalData: t.literal(true), + hasLegacyData: t.literal(false), + }); + + const data = decodeOrThrow(responseType, response.body); + + expect(data.items.length).to.be.greaterThan(0); + + expect(data.items.some((item) => item.severity !== undefined)).to.be(true); + }); + }); + }); +} From a9174526c06cde91a3861935ca53f5cd2745aff8 Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Wed, 9 Sep 2020 14:08:33 +0200 Subject: [PATCH 09/17] Use empty icon/label for error rate --- .../ServiceOverview/ServiceList/MLCallout.tsx | 5 +- .../app/ServiceOverview/ServiceList/index.tsx | 21 +- .../ServiceOverview.test.tsx.snap | 218 ++++++++++-------- .../shared/charts/SparkPlot/index.tsx | 28 ++- 4 files changed, 164 insertions(+), 108 deletions(-) diff --git a/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/MLCallout.tsx b/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/MLCallout.tsx index 01c33d92c35ce3..dd632db0f15fec 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/MLCallout.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/MLCallout.tsx @@ -9,6 +9,7 @@ import { i18n } from '@kbn/i18n'; import { EuiButton } from '@elastic/eui'; import { EuiFlexItem } from '@elastic/eui'; import { EuiFlexGrid } from '@elastic/eui'; +import { EuiButtonEmpty } from '@elastic/eui'; import { APMLink } from '../../../shared/Links/apm/APMLink'; export function MLCallout({ onDismiss }: { onDismiss: () => void }) { @@ -42,14 +43,14 @@ export function MLCallout({ onDismiss }: { onDismiss: () => void }) { - onDismiss()}> + onDismiss()}> {i18n.translate( 'xpack.apm.serviceOverview.mlNudgeMessage.dismissButton', { defaultMessage: `Dismiss message`, } )} - + diff --git a/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/index.tsx b/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/index.tsx index 96cddbd7df2038..cf7a82e2bec5f5 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/index.tsx @@ -153,13 +153,20 @@ export const SERVICE_COLUMNS: Array> = [ }), sortable: true, dataType: 'number', - render: (_, { transactionErrorRate }) => ( - - ), + render: (_, { transactionErrorRate }) => { + const value = transactionErrorRate?.value; + + const valueLabel = + value !== null && value !== undefined ? asPercent(value, 1) : ''; + + return ( + + ); + }, align: 'left', width: px(unit * 10), }, diff --git a/x-pack/plugins/apm/public/components/app/ServiceOverview/__test__/__snapshots__/ServiceOverview.test.tsx.snap b/x-pack/plugins/apm/public/components/app/ServiceOverview/__test__/__snapshots__/ServiceOverview.test.tsx.snap index ed5a8d797dd1bd..b56f7d68202747 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceOverview/__test__/__snapshots__/ServiceOverview.test.tsx.snap +++ b/x-pack/plugins/apm/public/components/app/ServiceOverview/__test__/__snapshots__/ServiceOverview.test.tsx.snap @@ -271,24 +271,29 @@ NodeList [ class="euiFlexItem euiFlexItem--flexGrowZero" >
-
-
+ class="euiFlexItem euiFlexItem--flexGrowZero" + > +
+
+ class="euiFlexItem euiFlexItem--flexGrowZero" + > +
+
+ N/A +
+
+
-
-
+ class="euiFlexItem euiFlexItem--flexGrowZero" + > +
+
+ class="euiFlexItem euiFlexItem--flexGrowZero" + > +
+
+ N/A +
+
+
-
-
+ class="euiFlexItem euiFlexItem--flexGrowZero" + > +
+
+ class="euiFlexItem euiFlexItem--flexGrowZero" + > +
+
+ N/A +
+
+
- 0% -
+ />
@@ -511,24 +524,29 @@ NodeList [ class="euiFlexItem euiFlexItem--flexGrowZero" >
-
-
+ class="euiFlexItem euiFlexItem--flexGrowZero" + > +
+
+ class="euiFlexItem euiFlexItem--flexGrowZero" + > +
+
+ N/A +
+
+
-
-
+ class="euiFlexItem euiFlexItem--flexGrowZero" + > +
+
+ class="euiFlexItem euiFlexItem--flexGrowZero" + > +
+
+ N/A +
+
+
-
-
+ class="euiFlexItem euiFlexItem--flexGrowZero" + > +
+
+ class="euiFlexItem euiFlexItem--flexGrowZero" + > +
+
+ N/A +
+
+
- 0% -
+ />
diff --git a/x-pack/plugins/apm/public/components/shared/charts/SparkPlot/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/SparkPlot/index.tsx index 97dfa383e55a49..18b914afea995e 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/SparkPlot/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/SparkPlot/index.tsx @@ -5,8 +5,13 @@ */ import React from 'react'; import { ScaleType, Chart, Settings, AreaSeries } from '@elastic/charts'; +import { EuiIcon } from '@elastic/eui'; +import { EuiFlexItem } from '@elastic/eui'; +import { EuiFlexGroup } from '@elastic/eui'; +import { EuiText } from '@elastic/eui'; import { px } from '../../../../style/variables'; import { useChartTheme } from '../../../../../../observability/public'; +import { NOT_AVAILABLE_LABEL } from '../../../../../common/i18n'; interface Props { color: string; @@ -15,15 +20,32 @@ interface Props { export function SparkPlot(props: Props) { const { series, color } = props; - const theme = useChartTheme(); + const chartTheme = useChartTheme(); + + const isEmpty = series.every((point) => point.y === null); + + if (isEmpty) { + return ( + + + + + + + {NOT_AVAILABLE_LABEL} + + + + ); + } return ( Date: Wed, 9 Sep 2020 16:11:03 +0200 Subject: [PATCH 10/17] Fix top_services basic test --- .../apm_api_integration/basic/tests/services/top_services.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/test/apm_api_integration/basic/tests/services/top_services.ts b/x-pack/test/apm_api_integration/basic/tests/services/top_services.ts index f54e53f04729b1..0658a9e41db499 100644 --- a/x-pack/test/apm_api_integration/basic/tests/services/top_services.ts +++ b/x-pack/test/apm_api_integration/basic/tests/services/top_services.ts @@ -75,11 +75,11 @@ export default function ApiTest({ getService }: FtrProviderContext) { const data = decodeOrThrow(responseType, response.body); - const rumService = data.items.find((item) => item.serviceName === 'client'); + const rumService = data.items.find((item) => item.agentName === 'rum-js'); expect(rumService!.transactionErrorRate).to.be(undefined); - const nonRumServices = data.items.filter((item) => item.serviceName !== 'client'); + const nonRumServices = data.items.filter((item) => item.agentName === 'rum-js'); // all other services should have a transaction error rate expect( From 4c699bcb1af6b05300a6acfa2b1778ed5ebc1115 Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Thu, 10 Sep 2020 10:50:03 +0200 Subject: [PATCH 11/17] Clarify tests --- .../{decode_or_throw.ts => get_value_or_throw.ts} | 2 +- .../basic/tests/services/top_services.ts | 14 ++++++++------ .../trial/tests/services/top_services.ts | 6 ++++-- 3 files changed, 13 insertions(+), 9 deletions(-) rename x-pack/plugins/apm/common/utils/{decode_or_throw.ts => get_value_or_throw.ts} (95%) diff --git a/x-pack/plugins/apm/common/utils/decode_or_throw.ts b/x-pack/plugins/apm/common/utils/get_value_or_throw.ts similarity index 95% rename from x-pack/plugins/apm/common/utils/decode_or_throw.ts rename to x-pack/plugins/apm/common/utils/get_value_or_throw.ts index b5ed7c9d5072fb..15d197007d2a1d 100644 --- a/x-pack/plugins/apm/common/utils/decode_or_throw.ts +++ b/x-pack/plugins/apm/common/utils/get_value_or_throw.ts @@ -11,7 +11,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { identity } from 'fp-ts/lib/function'; import { throwErrors } from '../../../infra/common/runtime_types'; -export const decodeOrThrow = ( +export const getValueOrThrow = ( runtimeType: t.Type, inputValue: any ): A => diff --git a/x-pack/test/apm_api_integration/basic/tests/services/top_services.ts b/x-pack/test/apm_api_integration/basic/tests/services/top_services.ts index 0658a9e41db499..6dee3ad94513d2 100644 --- a/x-pack/test/apm_api_integration/basic/tests/services/top_services.ts +++ b/x-pack/test/apm_api_integration/basic/tests/services/top_services.ts @@ -7,7 +7,7 @@ import expect from '@kbn/expect'; import * as t from 'io-ts'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; -import { decodeOrThrow } from '../../../../../plugins/apm/common/utils/decode_or_throw'; +import { getValueOrThrow } from '../../../../../plugins/apm/common/utils/get_value_or_throw'; import archives_metadata from '../../archives_metadata'; export default function ApiTest({ getService }: FtrProviderContext) { @@ -43,7 +43,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { `/api/apm/services?start=${start}&end=${end}&uiFilters=${uiFilters}` ); - expect(response.status).to.be(200); + expect(response.status).to.eql(200, 'Response status should be 200'); const metricType = t.strict({ value: t.number, @@ -73,18 +73,20 @@ export default function ApiTest({ getService }: FtrProviderContext) { hasLegacyData: t.literal(false), }); - const data = decodeOrThrow(responseType, response.body); + const data = getValueOrThrow(responseType, response.body); const rumService = data.items.find((item) => item.agentName === 'rum-js'); + // RUM transactions don't have event.outcome set, + // so they should not have an error rate expect(rumService!.transactionErrorRate).to.be(undefined); - const nonRumServices = data.items.filter((item) => item.agentName === 'rum-js'); + const nonRumServices = data.items.filter((item) => item.agentName !== 'rum-js'); - // all other services should have a transaction error rate + // All non-RUM services should report an error rate expect( nonRumServices.every((item) => typeof item.transactionErrorRate?.value === 'number') - ); + ).to.be(true); }); }); }); diff --git a/x-pack/test/apm_api_integration/trial/tests/services/top_services.ts b/x-pack/test/apm_api_integration/trial/tests/services/top_services.ts index 4da922a8ab67b9..aad5d7cb4fc201 100644 --- a/x-pack/test/apm_api_integration/trial/tests/services/top_services.ts +++ b/x-pack/test/apm_api_integration/trial/tests/services/top_services.ts @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; import * as t from 'io-ts'; import { Severity } from '../../../../../plugins/apm/common/anomaly_detection'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; -import { decodeOrThrow } from '../../../../../plugins/apm/common/utils/decode_or_throw'; +import { getValueOrThrow } from '../../../../../plugins/apm/common/utils/get_value_or_throw'; import archives_metadata from '../../archives_metadata'; export default function ApiTest({ getService }: FtrProviderContext) { @@ -68,10 +68,12 @@ export default function ApiTest({ getService }: FtrProviderContext) { hasLegacyData: t.literal(false), }); - const data = decodeOrThrow(responseType, response.body); + const data = getValueOrThrow(responseType, response.body); + // there should be at least one service expect(data.items.length).to.be.greaterThan(0); + // at least one item should have a severity set expect(data.items.some((item) => item.severity !== undefined)).to.be(true); }); }); From 30273101c20ede3b27093d8d7509ee70da9ca1fb Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Thu, 10 Sep 2020 13:28:05 +0200 Subject: [PATCH 12/17] Use archiveName variable --- .../basic/tests/services/agent_name.ts | 7 ++++--- .../basic/tests/services/top_services.ts | 6 ++++-- .../trial/tests/services/top_services.ts | 8 +++++--- 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/x-pack/test/apm_api_integration/basic/tests/services/agent_name.ts b/x-pack/test/apm_api_integration/basic/tests/services/agent_name.ts index 7cff4974916a62..ab9ce6194a7613 100644 --- a/x-pack/test/apm_api_integration/basic/tests/services/agent_name.ts +++ b/x-pack/test/apm_api_integration/basic/tests/services/agent_name.ts @@ -12,7 +12,8 @@ export default function ApiTest({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const esArchiver = getService('esArchiver'); - const range = archives['apm_8.0.0']; + const archiveName = 'apm_8.0.0'; + const range = archives[archiveName]; const start = encodeURIComponent(range.start); const end = encodeURIComponent(range.end); @@ -29,8 +30,8 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); describe('when data is loaded', () => { - before(() => esArchiver.load('apm_8.0.0')); - after(() => esArchiver.unload('apm_8.0.0')); + before(() => esArchiver.load(archiveName)); + after(() => esArchiver.unload(archiveName)); it('returns the agent name', async () => { const response = await supertest.get( diff --git a/x-pack/test/apm_api_integration/basic/tests/services/top_services.ts b/x-pack/test/apm_api_integration/basic/tests/services/top_services.ts index 6dee3ad94513d2..148eead1689261 100644 --- a/x-pack/test/apm_api_integration/basic/tests/services/top_services.ts +++ b/x-pack/test/apm_api_integration/basic/tests/services/top_services.ts @@ -14,6 +14,8 @@ export default function ApiTest({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const esArchiver = getService('esArchiver'); + const archiveName = 'apm_8.0.0'; + const range = archives_metadata['apm_8.0.0']; // url parameters @@ -35,8 +37,8 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); describe('when data is loaded', () => { - before(() => esArchiver.load('apm_8.0.0')); - after(() => esArchiver.unload('apm_8.0.0')); + before(() => esArchiver.load(archiveName)); + after(() => esArchiver.unload(archiveName)); it('returns a list of services', async () => { const response = await supertest.get( diff --git a/x-pack/test/apm_api_integration/trial/tests/services/top_services.ts b/x-pack/test/apm_api_integration/trial/tests/services/top_services.ts index aad5d7cb4fc201..5c2e6d8376c507 100644 --- a/x-pack/test/apm_api_integration/trial/tests/services/top_services.ts +++ b/x-pack/test/apm_api_integration/trial/tests/services/top_services.ts @@ -15,7 +15,9 @@ export default function ApiTest({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const esArchiver = getService('esArchiver'); - const range = archives_metadata['apm_8.0.0']; + const archiveName = 'apm_8.0.0'; + + const range = archives_metadata[archiveName]; // url parameters const start = encodeURIComponent(range.start); @@ -25,8 +27,8 @@ export default function ApiTest({ getService }: FtrProviderContext) { describe('APM Services Overview', () => { describe('when data is loaded', () => { - before(() => esArchiver.load('apm_8.0.0')); - after(() => esArchiver.unload('apm_8.0.0')); + before(() => esArchiver.load(archiveName)); + after(() => esArchiver.unload(archiveName)); it('returns a list of services with anomaly scores', async () => { const response = await supertest.get( From 137d0b1a7b04c77c4d9aaf917f1205d506b910b6 Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Thu, 10 Sep 2020 14:08:54 +0200 Subject: [PATCH 13/17] Rename isValidPlatinumLicense to isActivePlatinumLicense --- x-pack/plugins/apm/common/service_map.test.ts | 12 ++++++------ x-pack/plugins/apm/common/service_map.ts | 2 +- .../apm/public/components/app/ServiceMap/index.tsx | 6 +++--- .../plugins/apm/server/lib/helpers/setup_request.ts | 4 ++-- x-pack/plugins/apm/server/routes/service_map.ts | 6 +++--- .../apm/server/routes/settings/anomaly_detection.ts | 6 +++--- 6 files changed, 18 insertions(+), 18 deletions(-) diff --git a/x-pack/plugins/apm/common/service_map.test.ts b/x-pack/plugins/apm/common/service_map.test.ts index 346403efc46ae0..31f439a7aaec90 100644 --- a/x-pack/plugins/apm/common/service_map.test.ts +++ b/x-pack/plugins/apm/common/service_map.test.ts @@ -8,7 +8,7 @@ import { License } from '../../licensing/common/license'; import * as serviceMap from './service_map'; describe('service map helpers', () => { - describe('isValidPlatinumLicense', () => { + describe('isActivePlatinumLicense', () => { describe('with an expired license', () => { it('returns false', () => { const license = new License({ @@ -22,7 +22,7 @@ describe('service map helpers', () => { signature: 'test signature', }); - expect(serviceMap.isValidPlatinumLicense(license)).toEqual(false); + expect(serviceMap.isActivePlatinumLicense(license)).toEqual(false); }); }); @@ -39,7 +39,7 @@ describe('service map helpers', () => { signature: 'test signature', }); - expect(serviceMap.isValidPlatinumLicense(license)).toEqual(false); + expect(serviceMap.isActivePlatinumLicense(license)).toEqual(false); }); }); @@ -56,7 +56,7 @@ describe('service map helpers', () => { signature: 'test signature', }); - expect(serviceMap.isValidPlatinumLicense(license)).toEqual(true); + expect(serviceMap.isActivePlatinumLicense(license)).toEqual(true); }); }); @@ -73,7 +73,7 @@ describe('service map helpers', () => { signature: 'test signature', }); - expect(serviceMap.isValidPlatinumLicense(license)).toEqual(true); + expect(serviceMap.isActivePlatinumLicense(license)).toEqual(true); }); }); @@ -90,7 +90,7 @@ describe('service map helpers', () => { signature: 'test signature', }); - expect(serviceMap.isValidPlatinumLicense(license)).toEqual(true); + expect(serviceMap.isActivePlatinumLicense(license)).toEqual(true); }); }); }); diff --git a/x-pack/plugins/apm/common/service_map.ts b/x-pack/plugins/apm/common/service_map.ts index 7f46fc685d9ca9..1dc4d598cd2ee0 100644 --- a/x-pack/plugins/apm/common/service_map.ts +++ b/x-pack/plugins/apm/common/service_map.ts @@ -46,7 +46,7 @@ export interface ServiceNodeStats { avgErrorRate: number | null; } -export function isValidPlatinumLicense(license: ILicense) { +export function isActivePlatinumLicense(license: ILicense) { return license.isActive && license.hasAtLeast('platinum'); } diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/index.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/index.tsx index 83fab95bc91c9b..cb5a57e9ab9fba 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/index.tsx @@ -9,7 +9,7 @@ import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { useTheme } from '../../../hooks/useTheme'; import { invalidLicenseMessage, - isValidPlatinumLicense, + isActivePlatinumLicense, } from '../../../../common/service_map'; import { useFetcher } from '../../../hooks/useFetcher'; import { useLicense } from '../../../hooks/useLicense'; @@ -36,7 +36,7 @@ export function ServiceMap({ serviceName }: ServiceMapProps) { const { data = { elements: [] } } = useFetcher(() => { // When we don't have a license or a valid license, don't make the request. - if (!license || !isValidPlatinumLicense(license)) { + if (!license || !isActivePlatinumLicense(license)) { return; } @@ -66,7 +66,7 @@ export function ServiceMap({ serviceName }: ServiceMapProps) { return null; } - return isValidPlatinumLicense(license) ? ( + return isActivePlatinumLicense(license) ? (
( request, }), ml: - context.plugins.ml && isValidPlatinumLicense(context.licensing.license) + context.plugins.ml && isActivePlatinumLicense(context.licensing.license) ? getMlSetup( context.plugins.ml, context.core.savedObjects.client, diff --git a/x-pack/plugins/apm/server/routes/service_map.ts b/x-pack/plugins/apm/server/routes/service_map.ts index 971e247d98986e..8533d54ed62778 100644 --- a/x-pack/plugins/apm/server/routes/service_map.ts +++ b/x-pack/plugins/apm/server/routes/service_map.ts @@ -8,7 +8,7 @@ import Boom from 'boom'; import * as t from 'io-ts'; import { invalidLicenseMessage, - isValidPlatinumLicense, + isActivePlatinumLicense, } from '../../common/service_map'; import { setupRequest } from '../lib/helpers/setup_request'; import { getServiceMap } from '../lib/service_map/get_service_map'; @@ -33,7 +33,7 @@ export const serviceMapRoute = createRoute(() => ({ if (!context.config['xpack.apm.serviceMapEnabled']) { throw Boom.notFound(); } - if (!isValidPlatinumLicense(context.licensing.license)) { + if (!isActivePlatinumLicense(context.licensing.license)) { throw Boom.forbidden(invalidLicenseMessage); } context.licensing.featureUsage.notifyUsage(APM_SERVICE_MAPS_FEATURE_NAME); @@ -59,7 +59,7 @@ export const serviceMapServiceNodeRoute = createRoute(() => ({ if (!context.config['xpack.apm.serviceMapEnabled']) { throw Boom.notFound(); } - if (!isValidPlatinumLicense(context.licensing.license)) { + if (!isActivePlatinumLicense(context.licensing.license)) { throw Boom.forbidden(invalidLicenseMessage); } const logger = context.logger; diff --git a/x-pack/plugins/apm/server/routes/settings/anomaly_detection.ts b/x-pack/plugins/apm/server/routes/settings/anomaly_detection.ts index 4463437b61abde..290e81bd29973e 100644 --- a/x-pack/plugins/apm/server/routes/settings/anomaly_detection.ts +++ b/x-pack/plugins/apm/server/routes/settings/anomaly_detection.ts @@ -6,7 +6,7 @@ import * as t from 'io-ts'; import Boom from 'boom'; -import { isValidPlatinumLicense } from '../../../common/service_map'; +import { isActivePlatinumLicense } from '../../../common/service_map'; import { ML_ERRORS } from '../../../common/anomaly_detection'; import { createRoute } from '../create_route'; import { getAnomalyDetectionJobs } from '../../lib/anomaly_detection/get_anomaly_detection_jobs'; @@ -25,7 +25,7 @@ export const anomalyDetectionJobsRoute = createRoute(() => ({ handler: async ({ context, request }) => { const setup = await setupRequest(context, request); - if (!isValidPlatinumLicense(context.licensing.license)) { + if (!isActivePlatinumLicense(context.licensing.license)) { throw Boom.forbidden(ML_ERRORS.INVALID_LICENSE); } @@ -56,7 +56,7 @@ export const createAnomalyDetectionJobsRoute = createRoute(() => ({ const { environments } = context.params.body; const setup = await setupRequest(context, request); - if (!isValidPlatinumLicense(context.licensing.license)) { + if (!isActivePlatinumLicense(context.licensing.license)) { throw Boom.forbidden(ML_ERRORS.INVALID_LICENSE); } From 1f46dda2a2c9ed1ed0b21896acc5d67640c83617 Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Thu, 10 Sep 2020 14:52:32 +0200 Subject: [PATCH 14/17] Review feedback --- x-pack/plugins/apm/common/anomaly_detection.ts | 11 ++++------- .../apm/server/lib/helpers/get_bucket_size/index.ts | 2 -- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/x-pack/plugins/apm/common/anomaly_detection.ts b/x-pack/plugins/apm/common/anomaly_detection.ts index bce46721c52591..23d2d03caca1bb 100644 --- a/x-pack/plugins/apm/common/anomaly_detection.ts +++ b/x-pack/plugins/apm/common/anomaly_detection.ts @@ -54,11 +54,9 @@ export function getSeverityColor(theme: EuiTheme, severity?: Severity) { } export function getSeverityLabel(severity?: Severity) { - let label: string = ''; - switch (severity) { case Severity.critical: - label = i18n.translate( + return i18n.translate( 'xpack.apm.servicesTable.serviceHealthStatus.critical', { defaultMessage: 'Critical', @@ -68,7 +66,7 @@ export function getSeverityLabel(severity?: Severity) { case Severity.major: case Severity.minor: - label = i18n.translate( + return i18n.translate( 'xpack.apm.servicesTable.serviceHealthStatus.warning', { defaultMessage: 'Warning', @@ -77,7 +75,7 @@ export function getSeverityLabel(severity?: Severity) { break; case Severity.warning: - label = i18n.translate( + return i18n.translate( 'xpack.apm.servicesTable.serviceHealthStatus.healthy', { defaultMessage: 'Healthy', @@ -86,7 +84,7 @@ export function getSeverityLabel(severity?: Severity) { break; default: - label = i18n.translate( + return i18n.translate( 'xpack.apm.servicesTable.serviceHealthStatus.unknown', { defaultMessage: 'Unknown', @@ -94,7 +92,6 @@ export function getSeverityLabel(severity?: Severity) { ); break; } - return label; } export const ML_ERRORS = { diff --git a/x-pack/plugins/apm/server/lib/helpers/get_bucket_size/index.ts b/x-pack/plugins/apm/server/lib/helpers/get_bucket_size/index.ts index 3f193b2b20ca1d..5b78d97d5b6818 100644 --- a/x-pack/plugins/apm/server/lib/helpers/get_bucket_size/index.ts +++ b/x-pack/plugins/apm/server/lib/helpers/get_bucket_size/index.ts @@ -7,8 +7,6 @@ import moment from 'moment'; // @ts-expect-error import { calculateAuto } from './calculate_auto'; -// @ts-expect-error -import { unitToSeconds } from './unit_to_seconds'; export function getBucketSize( start: number, From 46a21716d228a975b6c175d396c3ba6c628697d9 Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Thu, 10 Sep 2020 15:34:26 +0200 Subject: [PATCH 15/17] Review feedback --- .../ServiceList/__test__/List.test.js | 6 +- .../__test__/__snapshots__/List.test.js.snap | 10 +- .../ServiceList/__test__/props.json | 10 +- .../app/ServiceOverview/ServiceList/index.tsx | 6 +- .../java/gc/fetch_and_transform_gc_metrics.ts | 4 +- .../__snapshots__/queries.test.ts.snap | 6 +- .../get_services/get_services_items_stats.ts | 12 +- .../basic/tests/services/top_services.ts | 106 +++++++++++------- .../trial/tests/services/top_services.ts | 101 ++++++++++------- 9 files changed, 155 insertions(+), 106 deletions(-) diff --git a/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/__test__/List.test.js b/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/__test__/List.test.js index f79f616b591251..519d74827097b4 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/__test__/List.test.js +++ b/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/__test__/List.test.js @@ -31,15 +31,15 @@ describe('ServiceOverview -> List', () => { agentName: 'python', transactionsPerMinute: { value: 86.93333333333334, - over_time: [], + timeseries: [], }, errorsPerMinute: { value: 12.6, - over_time: [], + timeseries: [], }, avgResponseTime: { value: 91535.42944785276, - over_time: [], + timeseries: [], }, environments: ['test'], }; diff --git a/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/__test__/__snapshots__/List.test.js.snap b/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/__test__/__snapshots__/List.test.js.snap index 41dbe9d949a760..da3f6ae89940ad 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/__test__/__snapshots__/List.test.js.snap +++ b/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/__test__/__snapshots__/List.test.js.snap @@ -116,29 +116,29 @@ exports[`ServiceOverview -> List renders with data 1`] = ` "test", ], "errorsPerMinute": Object { - "over_time": Array [], + "timeseries": Array [], "value": 46.06666666666667, }, "serviceName": "opbeans-node", "transactionsPerMinute": Object { - "over_time": Array [], + "timeseries": Array [], "value": 0, }, }, Object { "agentName": "python", "avgResponseTime": Object { - "over_time": Array [], + "timeseries": Array [], "value": 91535.42944785276, }, "environments": Array [], "errorsPerMinute": Object { - "over_time": Array [], + "timeseries": Array [], "value": 12.6, }, "serviceName": "opbeans-python", "transactionsPerMinute": Object { - "over_time": Array [], + "timeseries": Array [], "value": 86.93333333333334, }, }, diff --git a/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/__test__/props.json b/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/__test__/props.json index b3d906857cd3ae..7f24ad8b0d3082 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/__test__/props.json +++ b/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/__test__/props.json @@ -5,11 +5,11 @@ "agentName": "nodejs", "transactionsPerMinute": { "value": 0, - "over_time": [] + "timeseries": [] }, "errorsPerMinute": { "value": 46.06666666666667, - "over_time": [] + "timeseries": [] }, "avgResponseTime": null, "environments": [ @@ -21,15 +21,15 @@ "agentName": "python", "transactionsPerMinute": { "value": 86.93333333333334, - "over_time": [] + "timeseries": [] }, "errorsPerMinute": { "value": 12.6, - "over_time": [] + "timeseries": [] }, "avgResponseTime": { "value": 91535.42944785276, - "over_time": [] + "timeseries": [] }, "environments": [] } diff --git a/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/index.tsx b/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/index.tsx index cf7a82e2bec5f5..ce256137481cbf 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/index.tsx @@ -111,7 +111,7 @@ export const SERVICE_COLUMNS: Array> = [ dataType: 'number', render: (_, { avgResponseTime }) => ( @@ -131,7 +131,7 @@ export const SERVICE_COLUMNS: Array> = [ dataType: 'number', render: (_, { transactionsPerMinute }) => ( > = [ return ( diff --git a/x-pack/plugins/apm/server/lib/metrics/by_agent/java/gc/fetch_and_transform_gc_metrics.ts b/x-pack/plugins/apm/server/lib/metrics/by_agent/java/gc/fetch_and_transform_gc_metrics.ts index e845bf3b27e520..d7e64bdcacd12b 100644 --- a/x-pack/plugins/apm/server/lib/metrics/by_agent/java/gc/fetch_and_transform_gc_metrics.ts +++ b/x-pack/plugins/apm/server/lib/metrics/by_agent/java/gc/fetch_and_transform_gc_metrics.ts @@ -74,7 +74,7 @@ export async function fetchAndTransformGcMetrics({ field: `${LABEL_NAME}`, }, aggs: { - over_time: { + timeseries: { date_histogram: getMetricsDateHistogramParams( start, end, @@ -123,7 +123,7 @@ export async function fetchAndTransformGcMetrics({ const series = aggregations.per_pool.buckets.map((poolBucket, i) => { const label = poolBucket.key as string; - const timeseriesData = poolBucket.over_time; + const timeseriesData = poolBucket.timeseries; const data = timeseriesData.buckets.map((bucket) => { // derivative/value will be undefined for the first hit and if the `max` value is null diff --git a/x-pack/plugins/apm/server/lib/services/__snapshots__/queries.test.ts.snap b/x-pack/plugins/apm/server/lib/services/__snapshots__/queries.test.ts.snap index 6a367e961ae6df..c5e072e0739928 100644 --- a/x-pack/plugins/apm/server/lib/services/__snapshots__/queries.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/services/__snapshots__/queries.test.ts.snap @@ -105,7 +105,7 @@ Array [ "field": "transaction.duration.us", }, }, - "over_time": Object { + "timeseries": Object { "aggs": Object { "average": Object { "avg": Object { @@ -213,7 +213,7 @@ Array [ "aggs": Object { "services": Object { "aggs": Object { - "over_time": Object { + "timeseries": Object { "date_histogram": Object { "extended_bounds": Object { "max": 1528977600000, @@ -269,7 +269,7 @@ Array [ "field": "event.outcome", }, }, - "over_time": Object { + "timeseries": Object { "aggs": Object { "outcomes": Object { "terms": Object { diff --git a/x-pack/plugins/apm/server/lib/services/get_services/get_services_items_stats.ts b/x-pack/plugins/apm/server/lib/services/get_services/get_services_items_stats.ts index f7019fd8b0dce6..ab6b61ca217467 100644 --- a/x-pack/plugins/apm/server/lib/services/get_services/get_services_items_stats.ts +++ b/x-pack/plugins/apm/server/lib/services/get_services/get_services_items_stats.ts @@ -70,7 +70,7 @@ export const getTransactionDurationAverages = async ({ field: TRANSACTION_DURATION, }, }, - over_time: { + timeseries: { date_histogram: getDateHistogramOpts(start, end), aggs: { average: { @@ -97,7 +97,7 @@ export const getTransactionDurationAverages = async ({ serviceName: serviceBucket.key as string, avgResponseTime: { value: serviceBucket.average.value, - over_time: serviceBucket.over_time.buckets.map((dateBucket) => ({ + timeseries: serviceBucket.timeseries.buckets.map((dateBucket) => ({ x: dateBucket.key, y: dateBucket.average.value, })), @@ -173,7 +173,7 @@ export const getTransactionRates = async ({ size: MAX_NUMBER_OF_SERVICES, }, aggs: { - over_time: { + timeseries: { date_histogram: getDateHistogramOpts(start, end), }, }, @@ -197,7 +197,7 @@ export const getTransactionRates = async ({ serviceName: serviceBucket.key as string, transactionsPerMinute: { value: transactionsPerMinute, - over_time: serviceBucket.over_time.buckets.map((dateBucket) => ({ + timeseries: serviceBucket.timeseries.buckets.map((dateBucket) => ({ x: dateBucket.key, y: dateBucket.doc_count / deltaAsMinutes, })), @@ -245,7 +245,7 @@ export const getTransactionErrorRates = async ({ }, aggs: { outcomes, - over_time: { + timeseries: { date_histogram: getDateHistogramOpts(start, end), aggs: { outcomes, @@ -287,7 +287,7 @@ export const getTransactionErrorRates = async ({ serviceName: serviceBucket.key as string, transactionErrorRate: { value: transactionErrorRate, - over_time: serviceBucket.over_time.buckets.map((dateBucket) => { + timeseries: serviceBucket.timeseries.buckets.map((dateBucket) => { return { x: dateBucket.key, y: calculateTransactionErrorPercentage(dateBucket.outcomes), diff --git a/x-pack/test/apm_api_integration/basic/tests/services/top_services.ts b/x-pack/test/apm_api_integration/basic/tests/services/top_services.ts index 148eead1689261..b7a106676f27fb 100644 --- a/x-pack/test/apm_api_integration/basic/tests/services/top_services.ts +++ b/x-pack/test/apm_api_integration/basic/tests/services/top_services.ts @@ -6,17 +6,47 @@ import expect from '@kbn/expect'; import * as t from 'io-ts'; +import { isEmpty } from 'lodash'; +import { PromiseReturnType } from '../../../../../plugins/apm/typings/common'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; import { getValueOrThrow } from '../../../../../plugins/apm/common/utils/get_value_or_throw'; import archives_metadata from '../../archives_metadata'; +const metricType = t.strict({ + value: t.number, + timeseries: t.array( + t.type({ + x: t.number, + y: t.union([t.number, t.null]), + }) + ), +}); + +const serviceType = t.strict({ + serviceName: t.string, + agentName: t.string, + transactionsPerMinute: metricType, + avgResponseTime: metricType, + // the RUM service will not have transaction error data + transactionErrorRate: t.union([metricType, t.undefined]), + environments: t.array(t.string), + // basic license should not have anomaly scores + severity: t.undefined, +}); + +const responseType = t.type({ + items: t.array(serviceType), + hasHistoricalData: t.literal(true), + hasLegacyData: t.literal(false), +}); + export default function ApiTest({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const esArchiver = getService('esArchiver'); const archiveName = 'apm_8.0.0'; - const range = archives_metadata['apm_8.0.0']; + const range = archives_metadata[archiveName]; // url parameters const start = encodeURIComponent(range.start); @@ -40,55 +70,51 @@ export default function ApiTest({ getService }: FtrProviderContext) { before(() => esArchiver.load(archiveName)); after(() => esArchiver.unload(archiveName)); - it('returns a list of services', async () => { - const response = await supertest.get( - `/api/apm/services?start=${start}&end=${end}&uiFilters=${uiFilters}` - ); - - expect(response.status).to.eql(200, 'Response status should be 200'); - - const metricType = t.strict({ - value: t.number, - over_time: t.array( - t.type({ - x: t.number, - y: t.union([t.number, t.null]), - }) - ), + describe('and fetching a list of services', () => { + let response: PromiseReturnType; + before(async () => { + response = await supertest.get( + `/api/apm/services?start=${start}&end=${end}&uiFilters=${uiFilters}` + ); }); - const serviceType = t.strict({ - serviceName: t.string, - agentName: t.string, - transactionsPerMinute: metricType, - avgResponseTime: metricType, - // the RUM service will not have transaction error data - transactionErrorRate: t.union([metricType, t.undefined]), - environments: t.array(t.string), - // basic license should not have anomaly scores - severity: t.undefined, + it('the response is successful', () => { + expect(response.status).to.eql(200); }); - const responseType = t.type({ - items: t.array(serviceType), - hasHistoricalData: t.literal(true), - hasLegacyData: t.literal(false), + it('the response outline matches the shape we expect', () => { + expect(() => { + getValueOrThrow(responseType, response.body); + }).not.throwError(); }); - const data = getValueOrThrow(responseType, response.body); + it(`RUM services don't report any transaction error rates`, () => { + // RUM transactions don't have event.outcome set, + // so they should not have an error rate - const rumService = data.items.find((item) => item.agentName === 'rum-js'); + const data = getValueOrThrow(responseType, response.body); - // RUM transactions don't have event.outcome set, - // so they should not have an error rate - expect(rumService!.transactionErrorRate).to.be(undefined); + const rumServices = data.items.filter((item) => item.agentName === 'rum-js'); - const nonRumServices = data.items.filter((item) => item.agentName !== 'rum-js'); + expect(rumServices.length).to.be.greaterThan(0); - // All non-RUM services should report an error rate - expect( - nonRumServices.every((item) => typeof item.transactionErrorRate?.value === 'number') - ).to.be(true); + expect(rumServices.every((item) => isEmpty(item.transactionErrorRate?.value))); + }); + + it('non-RUM services all report transaction error rates', () => { + const data = getValueOrThrow(responseType, response.body); + + const nonRumServices = data.items.filter((item) => item.agentName !== 'rum-js'); + + expect( + nonRumServices.every((item) => { + return ( + typeof item.transactionErrorRate?.value === 'number' && + item.transactionErrorRate.timeseries.length > 0 + ); + }) + ).to.be(true); + }); }); }); }); diff --git a/x-pack/test/apm_api_integration/trial/tests/services/top_services.ts b/x-pack/test/apm_api_integration/trial/tests/services/top_services.ts index 5c2e6d8376c507..8caf69f212d099 100644 --- a/x-pack/test/apm_api_integration/trial/tests/services/top_services.ts +++ b/x-pack/test/apm_api_integration/trial/tests/services/top_services.ts @@ -6,11 +6,45 @@ import expect from '@kbn/expect'; import * as t from 'io-ts'; +import { PromiseReturnType } from '../../../../../plugins/apm/typings/common'; import { Severity } from '../../../../../plugins/apm/common/anomaly_detection'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; import { getValueOrThrow } from '../../../../../plugins/apm/common/utils/get_value_or_throw'; import archives_metadata from '../../archives_metadata'; +const metricType = t.strict({ + value: t.number, + timeseries: t.array( + t.type({ + x: t.number, + y: t.union([t.number, t.null]), + }) + ), +}); + +const serviceType = t.strict({ + serviceName: t.string, + agentName: t.string, + transactionsPerMinute: metricType, + avgResponseTime: metricType, + // the RUM service will not have transaction error data + transactionErrorRate: t.union([metricType, t.undefined]), + environments: t.array(t.string), + severity: t.union([ + t.literal(Severity.critical), + t.literal(Severity.major), + t.literal(Severity.minor), + t.literal(Severity.warning), + t.undefined, + ]), +}); + +const responseType = t.type({ + items: t.array(serviceType), + hasHistoricalData: t.literal(true), + hasLegacyData: t.literal(false), +}); + export default function ApiTest({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const esArchiver = getService('esArchiver'); @@ -30,53 +64,42 @@ export default function ApiTest({ getService }: FtrProviderContext) { before(() => esArchiver.load(archiveName)); after(() => esArchiver.unload(archiveName)); - it('returns a list of services with anomaly scores', async () => { - const response = await supertest.get( - `/api/apm/services?start=${start}&end=${end}&uiFilters=${uiFilters}` - ); - - expect(response.status).to.be(200); - - const metricType = t.strict({ - value: t.number, - over_time: t.array( - t.type({ - x: t.number, - y: t.union([t.number, t.null]), - }) - ), + describe('and fetching a list of services', () => { + let response: PromiseReturnType; + before(async () => { + response = await supertest.get( + `/api/apm/services?start=${start}&end=${end}&uiFilters=${uiFilters}` + ); }); - const serviceType = t.strict({ - serviceName: t.string, - agentName: t.string, - transactionsPerMinute: metricType, - avgResponseTime: metricType, - // the RUM service will not have transaction error data - transactionErrorRate: t.union([metricType, t.undefined]), - environments: t.array(t.string), - severity: t.union([ - t.literal(Severity.critical), - t.literal(Severity.major), - t.literal(Severity.minor), - t.literal(Severity.warning), - t.undefined, - ]), + it('the response is successful', () => { + expect(response.status).to.eql(200); }); - const responseType = t.type({ - items: t.array(serviceType), - hasHistoricalData: t.literal(true), - hasLegacyData: t.literal(false), + it('the response outline matches the shape we expect', () => { + expect(() => { + getValueOrThrow(responseType, response.body); + }).not.throwError(); }); - const data = getValueOrThrow(responseType, response.body); + it('there is at least one service', () => { + const data = getValueOrThrow(responseType, response.body); - // there should be at least one service - expect(data.items.length).to.be.greaterThan(0); + expect(data.items.length).to.be.greaterThan(0); + }); - // at least one item should have a severity set - expect(data.items.some((item) => item.severity !== undefined)).to.be(true); + it('some items have severity 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): + // https://github.com/elastic/kibana/issues/77083 + + const data = getValueOrThrow(responseType, response.body); + + expect(data.items.some((item) => item.severity !== undefined)).to.be(true); + }); }); }); }); From 48ae3dda14cc465bcbca95d7912a8d4f6d18d585 Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Thu, 10 Sep 2020 15:40:08 +0200 Subject: [PATCH 16/17] Remove invalid stored item in useLocalStorage --- x-pack/plugins/apm/public/hooks/useLocalStorage.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/apm/public/hooks/useLocalStorage.ts b/x-pack/plugins/apm/public/hooks/useLocalStorage.ts index ae7b277075725e..cf37b45045f4dc 100644 --- a/x-pack/plugins/apm/public/hooks/useLocalStorage.ts +++ b/x-pack/plugins/apm/public/hooks/useLocalStorage.ts @@ -18,7 +18,9 @@ export function useLocalStorage(key: string, defaultValue: T) { try { toStore = JSON.parse(storedItem) as T; } catch (err) { - // do nothing + window.localStorage.removeItem(key); + // eslint-disable-next-line no-console + console.log(`Unable to decode: ${key}`); } } From 13f5ec5cbbec1582389a47abdf6ee0a0fe348154 Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Sun, 13 Sep 2020 15:13:44 +0200 Subject: [PATCH 17/17] Remove io-ts, add value tests --- .../apm/common/utils/get_value_or_throw.ts | 26 ---- .../basic/tests/services/top_services.ts | 132 ++++++++++++------ .../trial/tests/services/top_services.ts | 62 ++------ 3 files changed, 102 insertions(+), 118 deletions(-) delete mode 100644 x-pack/plugins/apm/common/utils/get_value_or_throw.ts diff --git a/x-pack/plugins/apm/common/utils/get_value_or_throw.ts b/x-pack/plugins/apm/common/utils/get_value_or_throw.ts deleted file mode 100644 index 15d197007d2a1d..00000000000000 --- a/x-pack/plugins/apm/common/utils/get_value_or_throw.ts +++ /dev/null @@ -1,26 +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 * as t from 'io-ts'; -import { fold } from 'fp-ts/lib/Either'; -import { pipe } from 'fp-ts/lib/pipeable'; - -import { identity } from 'fp-ts/lib/function'; -import { throwErrors } from '../../../infra/common/runtime_types'; - -export const getValueOrThrow = ( - runtimeType: t.Type, - inputValue: any -): A => - pipe( - runtimeType.decode(inputValue), - fold( - throwErrors((error) => { - throw new Error(error); - }), - identity - ) - ); diff --git a/x-pack/test/apm_api_integration/basic/tests/services/top_services.ts b/x-pack/test/apm_api_integration/basic/tests/services/top_services.ts index b7a106676f27fb..730f4a52f19252 100644 --- a/x-pack/test/apm_api_integration/basic/tests/services/top_services.ts +++ b/x-pack/test/apm_api_integration/basic/tests/services/top_services.ts @@ -5,40 +5,10 @@ */ import expect from '@kbn/expect'; -import * as t from 'io-ts'; -import { isEmpty } from 'lodash'; +import { isEmpty, pick } from 'lodash'; import { PromiseReturnType } from '../../../../../plugins/apm/typings/common'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; -import { getValueOrThrow } from '../../../../../plugins/apm/common/utils/get_value_or_throw'; -import archives_metadata from '../../archives_metadata'; - -const metricType = t.strict({ - value: t.number, - timeseries: t.array( - t.type({ - x: t.number, - y: t.union([t.number, t.null]), - }) - ), -}); - -const serviceType = t.strict({ - serviceName: t.string, - agentName: t.string, - transactionsPerMinute: metricType, - avgResponseTime: metricType, - // the RUM service will not have transaction error data - transactionErrorRate: t.union([metricType, t.undefined]), - environments: t.array(t.string), - // basic license should not have anomaly scores - severity: t.undefined, -}); - -const responseType = t.type({ - items: t.array(serviceType), - hasHistoricalData: t.literal(true), - hasLegacyData: t.literal(false), -}); +import archives_metadata from '../../../common/archives_metadata'; export default function ApiTest({ getService }: FtrProviderContext) { const supertest = getService('supertest'); @@ -82,32 +52,106 @@ export default function ApiTest({ getService }: FtrProviderContext) { expect(response.status).to.eql(200); }); - it('the response outline matches the shape we expect', () => { - expect(() => { - getValueOrThrow(responseType, response.body); - }).not.throwError(); + it('returns hasHistoricalData: true', () => { + expect(response.body.hasHistoricalData).to.be(true); + }); + + it('returns hasLegacyData: false', () => { + expect(response.body.hasLegacyData).to.be(false); + }); + + it('returns the correct service names', () => { + expect(response.body.items.map((item: any) => item.serviceName)).to.eql([ + 'opbeans-python', + 'opbeans-node', + 'opbeans-ruby', + 'opbeans-go', + 'opbeans-dotnet', + 'opbeans-java', + 'opbeans-rum', + ]); + }); + + it('returns the correct metrics averages', () => { + expect( + response.body.items.map((item: any) => + pick( + item, + 'transactionErrorRate.value', + 'avgResponseTime.value', + 'transactionsPerMinute.value' + ) + ) + ).to.eql([ + { + transactionErrorRate: { value: 0.041666666666666664 }, + avgResponseTime: { value: 208079.9121184089 }, + transactionsPerMinute: { value: 18.016666666666666 }, + }, + { + transactionErrorRate: { value: 0.03317535545023697 }, + avgResponseTime: { value: 578297.1431623931 }, + transactionsPerMinute: { value: 7.8 }, + }, + { + transactionErrorRate: { value: 0.013123359580052493 }, + avgResponseTime: { value: 60518.587926509186 }, + transactionsPerMinute: { value: 6.35 }, + }, + { + transactionErrorRate: { value: 0.014577259475218658 }, + avgResponseTime: { value: 25259.78717201166 }, + transactionsPerMinute: { value: 5.716666666666667 }, + }, + { + transactionErrorRate: { value: 0.01532567049808429 }, + avgResponseTime: { value: 527290.3218390804 }, + transactionsPerMinute: { value: 4.35 }, + }, + { + transactionErrorRate: { value: 0.15384615384615385 }, + avgResponseTime: { value: 530245.8571428572 }, + transactionsPerMinute: { value: 3.033333333333333 }, + }, + { + avgResponseTime: { value: 896134.328358209 }, + transactionsPerMinute: { value: 2.2333333333333334 }, + }, + ]); + }); + + it('returns environments', () => { + expect(response.body.items.map((item: any) => item.environments ?? [])).to.eql([ + ['production'], + ['testing'], + ['production'], + ['testing'], + ['production'], + ['production'], + ['testing'], + ]); }); it(`RUM services don't report any transaction error rates`, () => { // RUM transactions don't have event.outcome set, // so they should not have an error rate - const data = getValueOrThrow(responseType, response.body); - - const rumServices = data.items.filter((item) => item.agentName === 'rum-js'); + const rumServices = response.body.items.filter( + (item: any) => item.agentName === 'rum-js' + ); expect(rumServices.length).to.be.greaterThan(0); - expect(rumServices.every((item) => isEmpty(item.transactionErrorRate?.value))); + expect(rumServices.every((item: any) => isEmpty(item.transactionErrorRate?.value))); }); it('non-RUM services all report transaction error rates', () => { - const data = getValueOrThrow(responseType, response.body); - - const nonRumServices = data.items.filter((item) => item.agentName !== 'rum-js'); + const nonRumServices = response.body.items.filter( + (item: any) => item.agentName !== 'rum-js' + ); expect( - nonRumServices.every((item) => { + nonRumServices.every((item: any) => { return ( typeof item.transactionErrorRate?.value === 'number' && item.transactionErrorRate.timeseries.length > 0 diff --git a/x-pack/test/apm_api_integration/trial/tests/services/top_services.ts b/x-pack/test/apm_api_integration/trial/tests/services/top_services.ts index 8caf69f212d099..ed4712c8adcb29 100644 --- a/x-pack/test/apm_api_integration/trial/tests/services/top_services.ts +++ b/x-pack/test/apm_api_integration/trial/tests/services/top_services.ts @@ -5,45 +5,9 @@ */ import expect from '@kbn/expect'; -import * as t from 'io-ts'; import { PromiseReturnType } from '../../../../../plugins/apm/typings/common'; -import { Severity } from '../../../../../plugins/apm/common/anomaly_detection'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; -import { getValueOrThrow } from '../../../../../plugins/apm/common/utils/get_value_or_throw'; -import archives_metadata from '../../archives_metadata'; - -const metricType = t.strict({ - value: t.number, - timeseries: t.array( - t.type({ - x: t.number, - y: t.union([t.number, t.null]), - }) - ), -}); - -const serviceType = t.strict({ - serviceName: t.string, - agentName: t.string, - transactionsPerMinute: metricType, - avgResponseTime: metricType, - // the RUM service will not have transaction error data - transactionErrorRate: t.union([metricType, t.undefined]), - environments: t.array(t.string), - severity: t.union([ - t.literal(Severity.critical), - t.literal(Severity.major), - t.literal(Severity.minor), - t.literal(Severity.warning), - t.undefined, - ]), -}); - -const responseType = t.type({ - items: t.array(serviceType), - hasHistoricalData: t.literal(true), - hasLegacyData: t.literal(false), -}); +import archives_metadata from '../../../common/archives_metadata'; export default function ApiTest({ getService }: FtrProviderContext) { const supertest = getService('supertest'); @@ -76,16 +40,8 @@ export default function ApiTest({ getService }: FtrProviderContext) { expect(response.status).to.eql(200); }); - it('the response outline matches the shape we expect', () => { - expect(() => { - getValueOrThrow(responseType, response.body); - }).not.throwError(); - }); - it('there is at least one service', () => { - const data = getValueOrThrow(responseType, response.body); - - expect(data.items.length).to.be.greaterThan(0); + expect(response.body.items.length).to.be.greaterThan(0); }); it('some items have severity set', () => { @@ -96,9 +52,19 @@ export default function ApiTest({ getService }: FtrProviderContext) { // report as unknown (so without any severity status): // https://github.com/elastic/kibana/issues/77083 - const data = getValueOrThrow(responseType, response.body); + const severityScores = response.body.items.map((item: any) => item.severity); + + expect(severityScores.filter(Boolean).length).to.be.greaterThan(0); - expect(data.items.some((item) => item.severity !== undefined)).to.be(true); + expect(severityScores).to.eql([ + undefined, + undefined, + undefined, + undefined, + undefined, + 'warning', + undefined, + ]); }); }); });