-
- My Go Service
-
+
+ Unknown
+
+
|
+ >
+
+
+
+
+
+
+
+
+
|
- go
+
+
+
+ test
+
+
+
+
+
+
+ dev
+
+
+
|
|
|
|
,
@@ -247,87 +423,91 @@ NodeList [
>
|
-
-
- test
-
-
-
-
-
-
+
+
+
|
- python
-
+ />
|
|
|
|
,
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 7146e471a7f82..d9d2cffb67620 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 { useAnomalyDetectionJobs } from '../../../hooks/useAnomalyDetectionJobs';
-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: anomalyDetectionJobsData,
+ status: anomalyDetectionJobsStatus,
+ } = useAnomalyDetectionJobs();
+
+ const [userHasDismissedCallout, setUserHasDismissedCallout] = useLocalStorage(
+ 'apm.userHasDismissedServiceInventoryMlCallout',
+ false
+ );
+
+ const canCreateJob = !!core.application.capabilities.ml?.canCreateJob;
+
+ const displayMlCallout =
+ anomalyDetectionJobsStatus === FETCH_STATUS.SUCCESS &&
+ !anomalyDetectionJobsData?.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 0000000000000..2475eecee8e34
--- /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 0000000000000..5646fc05bd28f
--- /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 ;
+}
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 9fe52aab83641..9db563a0f6ba8 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 fcbdb900368ea..5bddfc67200b1 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 0000000000000..18b914afea995
--- /dev/null
+++ b/x-pack/plugins/apm/public/components/shared/charts/SparkPlot/index.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 { 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;
+ series: Array<{ x: number; y: number | null }>;
+}
+
+export function SparkPlot(props: Props) {
+ const { series, color } = props;
+ const chartTheme = useChartTheme();
+
+ const isEmpty = series.every((point) => point.y === null);
+
+ if (isEmpty) {
+ return (
+
+
+
+
+
+
+ {NOT_AVAILABLE_LABEL}
+
+
+
+ );
+ }
+
+ return (
+
+
+
+
+ );
+}
diff --git a/x-pack/plugins/apm/public/hooks/useAnomalyDetectionJobs.ts b/x-pack/plugins/apm/public/hooks/useAnomalyDetectionJobs.ts
new file mode 100644
index 0000000000000..56c58bc82967b
--- /dev/null
+++ b/x-pack/plugins/apm/public/hooks/useAnomalyDetectionJobs.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 useAnomalyDetectionJobs() {
+ 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 0000000000000..cf37b45045f4d
--- /dev/null
+++ b/x-pack/plugins/apm/public/hooks/useLocalStorage.ts
@@ -0,0 +1,54 @@
+/*
+ * 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) {
+ window.localStorage.removeItem(key);
+ // eslint-disable-next-line no-console
+ console.log(`Unable to decode: ${key}`);
+ }
+ }
+
+ 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 64602bc6b2769..f1643608496ad 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 e7eb7b8de65e3..93af51b572aa5 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 75b0471424e79..5b78d97d5b681 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,22 +7,23 @@
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, 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 9f5b5cdf47552..ea018868f9517 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/helpers/setup_request.ts b/x-pack/plugins/apm/server/lib/helpers/setup_request.ts
index 6b69e57389dff..eba75433a5148 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 { isActivePlatinumLicense } 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 && isActivePlatinumLicense(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/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 551384da2cca7..d7e64bdcacd12 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,
@@ -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/service_map/get_service_anomalies.ts b/x-pack/plugins/apm/server/lib/service_map/get_service_anomalies.ts
index ec274d20b6005..ed8ae923e6e6c 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 d1c99d778c8f0..1e26b6f3f58f9 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 ca86c1d93fa6e..c5e072e073992 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",
},
},
+ "timeseries": 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 {
+ "timeseries": 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,
@@ -226,12 +257,37 @@ Array [
Object {
"apm": Object {
"events": Array [
- "error",
+ "transaction",
],
},
"body": Object {
"aggs": Object {
"services": Object {
+ "aggs": Object {
+ "outcomes": Object {
+ "terms": Object {
+ "field": "event.outcome",
+ },
+ },
+ "timeseries": Object {
+ "aggs": Object {
+ "outcomes": Object {
+ "terms": Object {
+ "field": "event.outcome",
+ },
+ },
+ },
+ "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,
@@ -255,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 d888b43b63fac..50a968467fb4b 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,15 +15,22 @@ import {
getTransactionDurationAverages,
getAgentNames,
getTransactionRates,
- getErrorRates,
+ getTransactionErrorRates,
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,
@@ -33,22 +40,25 @@ export async function getServicesItems(setup: ServicesItemsSetup) {
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),
]);
const allMetrics = [
...transactionDurationAverages,
...agentNames,
...transactionRates,
- ...errorRates,
+ ...transactionErrorRates,
...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 ddce3b667a603..ab6b61ca21746 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,10 +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';
@@ -15,6 +19,21 @@ import {
ServicesItemsSetup,
ServicesItemsProjection,
} from './get_services_items';
+import { getBucketSize } from '../../helpers/get_bucket_size';
+import {
+ getMLJobIds,
+ getServiceAnomalies,
+} from '../../service_map/get_service_anomalies';
+import { AggregationResultOf } from '../../../../typings/elasticsearch/aggregations';
+
+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 +49,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 +70,16 @@ export const getTransactionDurationAverages = async ({
field: TRANSACTION_DURATION,
},
},
+ timeseries: {
+ date_histogram: getDateHistogramOpts(start, end),
+ aggs: {
+ average: {
+ avg: {
+ field: TRANSACTION_DURATION,
+ },
+ },
+ },
+ },
},
},
},
@@ -64,9 +93,15 @@ export const getTransactionDurationAverages = async ({
return [];
}
- return aggregations.services.buckets.map((bucket) => ({
- serviceName: bucket.key as string,
- avgResponseTime: bucket.average.value,
+ return aggregations.services.buckets.map((serviceBucket) => ({
+ serviceName: serviceBucket.key as string,
+ avgResponseTime: {
+ value: serviceBucket.average.value,
+ timeseries: serviceBucket.timeseries.buckets.map((dateBucket) => ({
+ x: dateBucket.key,
+ y: dateBucket.average.value,
+ })),
+ },
}));
};
@@ -112,9 +147,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,
+ return aggregations.services.buckets.map((serviceBucket) => ({
+ serviceName: serviceBucket.key as string,
+ agentName: serviceBucket.agent_name.hits.hits[0]?._source.agent
+ .name as AgentName,
}));
};
@@ -122,7 +158,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 +172,11 @@ export const getTransactionRates = async ({
...projection.body.aggs.services.terms,
size: MAX_NUMBER_OF_SERVICES,
},
+ aggs: {
+ timeseries: {
+ date_histogram: getDateHistogramOpts(start, end),
+ },
+ },
},
},
},
@@ -150,33 +191,67 @@ 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,
- transactionsPerMinute,
+ serviceName: serviceBucket.key as string,
+ transactionsPerMinute: {
+ value: transactionsPerMinute,
+ timeseries: serviceBucket.timeseries.buckets.map((dateBucket) => ({
+ x: dateBucket.key,
+ y: dateBucket.doc_count / deltaAsMinutes,
+ })),
+ },
};
});
};
-export const getErrorRates = async ({
+export const getTransactionErrorRates = async ({
setup,
projection,
}: AggregationParams) => {
- const { apmEventClient } = setup;
+ 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: {
...projection.body.aggs.services.terms,
size: MAX_NUMBER_OF_SERVICES,
},
+ aggs: {
+ outcomes,
+ timeseries: {
+ date_histogram: getDateHistogramOpts(start, end),
+ aggs: {
+ outcomes,
+ },
+ },
+ },
},
},
},
@@ -189,13 +264,36 @@ 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 aggregations.services.buckets.map((bucket) => {
- const errorsPerMinute = bucket.doc_count / deltaAsMinutes;
+ return failedTransactions / (successfulTransactions + failedTransactions);
+ }
+
+ return aggregations.services.buckets.map((serviceBucket) => {
+ const transactionErrorRate = calculateTransactionErrorPercentage(
+ serviceBucket.outcomes
+ );
return {
- serviceName: bucket.key as string,
- errorsPerMinute,
+ serviceName: serviceBucket.key as string,
+ transactionErrorRate: {
+ value: transactionErrorRate,
+ timeseries: serviceBucket.timeseries.buckets.map((dateBucket) => {
+ return {
+ x: dateBucket.key,
+ y: calculateTransactionErrorPercentage(dateBucket.outcomes),
+ };
+ }),
+ },
};
});
};
@@ -241,8 +339,43 @@ 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
+ ),
}));
};
+
+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).map((serviceName) => {
+ const stats = anomalies.serviceAnomalies[serviceName];
+
+ const severity = getSeverity(stats.anomalyScore);
+
+ return {
+ 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 5a909ebd6ec54..28b4c64a4af47 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 99c58a17d396a..9b0dd7a03ca5b 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 f7b7f72168160..1e08b04416e17 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
@@ -62,7 +62,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 f68082dfaa1e1..51118278fb824 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 596c3137ec19f..d8865f0049d35 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 1498c22e327d6..f39529b59caa6 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 8a0fe1a57736f..ea06bd57bfff2 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/service_map.ts b/x-pack/plugins/apm/server/routes/service_map.ts
index 971e247d98986..8533d54ed6277 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/services.ts b/x-pack/plugins/apm/server/routes/services.ts
index 74ab717b8de59..cc7f25867df2c 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/apm/server/routes/settings/anomaly_detection.ts b/x-pack/plugins/apm/server/routes/settings/anomaly_detection.ts
index ac25f22751f2f..290e81bd29973 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 { 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';
@@ -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 (!isActivePlatinumLicense(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 (!isActivePlatinumLicense(context.licensing.license)) {
throw Boom.forbidden(ML_ERRORS.INVALID_LICENSE);
}
diff --git a/x-pack/plugins/apm/typings/elasticsearch/aggregations.ts b/x-pack/plugins/apm/typings/elasticsearch/aggregations.ts
index 7a7592b248960..bbd2c9eb86249 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
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 13f7159ba6043..b5bfe3eec7d35 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 0000000000000..d0449a4432d93
--- /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 03939736b64ae..0aecea59ad013 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';
diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json
index 603723111c051..cb6634b0202a6 100644
--- a/x-pack/plugins/translations/translations/ja-JP.json
+++ b/x-pack/plugins/translations/translations/ja-JP.json
@@ -4858,12 +4858,9 @@
"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 {# 個の環境}}",
- "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 d7d3e63ffd8bc..3858b84b279c2 100644
--- a/x-pack/plugins/translations/translations/zh-CN.json
+++ b/x-pack/plugins/translations/translations/zh-CN.json
@@ -4861,12 +4861,9 @@
"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 {# 个环境}}",
- "xpack.apm.servicesTable.errorsPerMinuteColumnLabel": "每分钟错误数",
- "xpack.apm.servicesTable.errorsPerMinuteUnitLabel": "错误",
"xpack.apm.servicesTable.nameColumnLabel": "名称",
"xpack.apm.servicesTable.noServicesLabel": "似乎您没有安装任何 APM 服务。让我们添加一些!",
"xpack.apm.servicesTable.notFoundLabel": "未找到任何服务",
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 e4cceca573ce8..a87d080e564a2 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 8d91f4542e454..116b2987db32a 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,18 +4,25 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { sortBy } from 'lodash';
import expect from '@kbn/expect';
+import { isEmpty, pick } from 'lodash';
+import { PromiseReturnType } from '../../../../../plugins/apm/typings/common';
import { expectSnapshot } from '../../../common/match_snapshot';
import { FtrProviderContext } from '../../../common/ftr_provider_context';
+import archives_metadata from '../../../common/archives_metadata';
export default function ApiTest({ getService }: FtrProviderContext) {
const supertest = getService('supertest');
const esArchiver = getService('esArchiver');
+ const archiveName = 'apm_8.0.0';
+
+ const range = archives_metadata[archiveName];
+
// 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', () => {
@@ -31,52 +38,189 @@ 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(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}`
- );
- // sort services to mitigate unstable sort order
- const services = sortBy(response.body.items, ['serviceName']);
+ 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}`
+ );
+ });
- expect(response.status).to.be(200);
- expectSnapshot(services).toMatchInline(`
- Array [
- Object {
- "agentName": "rum-js",
- "avgResponseTime": 116375,
- "environments": Array [],
- "errorsPerMinute": 2.75,
- "serviceName": "client",
- "transactionsPerMinute": 2,
- },
- Object {
- "agentName": "java",
- "avgResponseTime": 25636.349593495936,
- "environments": Array [
+ it('the response is successful', () => {
+ expect(response.status).to.eql(200);
+ });
+
+ 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', () => {
+ expectSnapshot(response.body.items.map((item: any) => item.serviceName)).toMatchInline(`
+ Array [
+ "opbeans-python",
+ "opbeans-node",
+ "opbeans-ruby",
+ "opbeans-go",
+ "opbeans-dotnet",
+ "opbeans-java",
+ "opbeans-rum",
+ ]
+ `);
+ });
+
+ it('returns the correct metrics averages', () => {
+ expectSnapshot(
+ response.body.items.map((item: any) =>
+ pick(
+ item,
+ 'transactionErrorRate.value',
+ 'avgResponseTime.value',
+ 'transactionsPerMinute.value'
+ )
+ )
+ ).toMatchInline(`
+ Array [
+ Object {
+ "avgResponseTime": Object {
+ "value": 208079.9121184089,
+ },
+ "transactionErrorRate": Object {
+ "value": 0.041666666666666664,
+ },
+ "transactionsPerMinute": Object {
+ "value": 18.016666666666666,
+ },
+ },
+ Object {
+ "avgResponseTime": Object {
+ "value": 578297.1431623931,
+ },
+ "transactionErrorRate": Object {
+ "value": 0.03317535545023697,
+ },
+ "transactionsPerMinute": Object {
+ "value": 7.8,
+ },
+ },
+ Object {
+ "avgResponseTime": Object {
+ "value": 60518.587926509186,
+ },
+ "transactionErrorRate": Object {
+ "value": 0.013123359580052493,
+ },
+ "transactionsPerMinute": Object {
+ "value": 6.35,
+ },
+ },
+ Object {
+ "avgResponseTime": Object {
+ "value": 25259.78717201166,
+ },
+ "transactionErrorRate": Object {
+ "value": 0.014577259475218658,
+ },
+ "transactionsPerMinute": Object {
+ "value": 5.716666666666667,
+ },
+ },
+ Object {
+ "avgResponseTime": Object {
+ "value": 527290.3218390804,
+ },
+ "transactionErrorRate": Object {
+ "value": 0.01532567049808429,
+ },
+ "transactionsPerMinute": Object {
+ "value": 4.35,
+ },
+ },
+ Object {
+ "avgResponseTime": Object {
+ "value": 530245.8571428572,
+ },
+ "transactionErrorRate": Object {
+ "value": 0.15384615384615385,
+ },
+ "transactionsPerMinute": Object {
+ "value": 3.033333333333333,
+ },
+ },
+ Object {
+ "avgResponseTime": Object {
+ "value": 896134.328358209,
+ },
+ "transactionsPerMinute": Object {
+ "value": 2.2333333333333334,
+ },
+ },
+ ]
+ `);
+ });
+
+ it('returns environments', () => {
+ expectSnapshot(response.body.items.map((item: any) => item.environments ?? []))
+ .toMatchInline(`
+ Array [
+ Array [
"production",
],
- "errorsPerMinute": 4.5,
- "serviceName": "opbeans-java",
- "transactionsPerMinute": 30.75,
- },
- Object {
- "agentName": "nodejs",
- "avgResponseTime": 38682.52419354839,
- "environments": Array [
+ Array [
+ "testing",
+ ],
+ Array [
"production",
],
- "errorsPerMinute": 3.75,
- "serviceName": "opbeans-node",
- "transactionsPerMinute": 31,
- },
- ]
- `);
-
- expect(response.body.hasHistoricalData).to.be(true);
- expect(response.body.hasLegacyData).to.be(false);
+ Array [
+ "testing",
+ ],
+ Array [
+ "production",
+ ],
+ Array [
+ "production",
+ ],
+ Array [
+ "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 rumServices = response.body.items.filter(
+ (item: any) => item.agentName === 'rum-js'
+ );
+
+ expect(rumServices.length).to.be.greaterThan(0);
+
+ expect(rumServices.every((item: any) => isEmpty(item.transactionErrorRate?.value)));
+ });
+
+ it('non-RUM services all report transaction error rates', () => {
+ const nonRumServices = response.body.items.filter(
+ (item: any) => item.agentName !== 'rum-js'
+ );
+
+ expect(
+ nonRumServices.every((item: any) => {
+ 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/index.ts b/x-pack/test/apm_api_integration/trial/tests/index.ts
index 48ffa13012696..c5ca086b5f370 100644
--- a/x-pack/test/apm_api_integration/trial/tests/index.ts
+++ b/x-pack/test/apm_api_integration/trial/tests/index.ts
@@ -16,6 +16,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 0000000000000..76af02ec1606e
--- /dev/null
+++ b/x-pack/test/apm_api_integration/trial/tests/services/top_services.ts
@@ -0,0 +1,75 @@
+/*
+ * 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 { expectSnapshot } from '../../../common/match_snapshot';
+import { PromiseReturnType } from '../../../../../plugins/apm/typings/common';
+import { FtrProviderContext } from '../../../common/ftr_provider_context';
+import archives_metadata from '../../../common/archives_metadata';
+
+export default function ApiTest({ getService }: FtrProviderContext) {
+ const supertest = getService('supertest');
+ const esArchiver = getService('esArchiver');
+
+ const archiveName = 'apm_8.0.0';
+
+ const range = archives_metadata[archiveName];
+
+ // 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(archiveName));
+ after(() => esArchiver.unload(archiveName));
+
+ 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}`
+ );
+ });
+
+ it('the response is successful', () => {
+ expect(response.status).to.eql(200);
+ });
+
+ it('there is at least one service', () => {
+ expect(response.body.items.length).to.be.greaterThan(0);
+ });
+
+ 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 severityScores = response.body.items.map((item: any) => item.severity);
+
+ expect(severityScores.filter(Boolean).length).to.be.greaterThan(0);
+
+ expectSnapshot(severityScores).toMatchInline(`
+ Array [
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ "warning",
+ undefined,
+ ]
+ `);
+ });
+ });
+ });
+ });
+}
|