diff --git a/x-pack/plugins/apm/common/service_inventory.ts b/x-pack/plugins/apm/common/service_inventory.ts
new file mode 100644
index 0000000000000..b7c8c0ea90a58
--- /dev/null
+++ b/x-pack/plugins/apm/common/service_inventory.ts
@@ -0,0 +1,20 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { AgentName } from '../typings/es_schemas/ui/fields/agent';
+import { ServiceHealthStatus } from './service_health_status';
+
+export interface ServiceListItem {
+ serviceName: string;
+ healthStatus?: ServiceHealthStatus;
+ transactionType?: string;
+ agentName?: AgentName;
+ throughput?: number;
+ latency?: number | null;
+ transactionErrorRate?: number | null;
+ environments?: string[];
+}
diff --git a/x-pack/plugins/apm/public/components/app/service_inventory/index.tsx b/x-pack/plugins/apm/public/components/app/service_inventory/index.tsx
index 1e736409a9604..cc4cbd975f5cd 100644
--- a/x-pack/plugins/apm/public/components/app/service_inventory/index.tsx
+++ b/x-pack/plugins/apm/public/components/app/service_inventory/index.tsx
@@ -12,21 +12,20 @@ import uuid from 'uuid';
import { useAnomalyDetectionJobsContext } from '../../../context/anomaly_detection_jobs/use_anomaly_detection_jobs_context';
import { useLegacyUrlParams } from '../../../context/url_params_context/use_url_params';
import { useLocalStorage } from '../../../hooks/use_local_storage';
-import { useAnyOfApmParams } from '../../../hooks/use_apm_params';
+import { useApmParams } from '../../../hooks/use_apm_params';
import { FETCH_STATUS, useFetcher } from '../../../hooks/use_fetcher';
import { useTimeRange } from '../../../hooks/use_time_range';
import { SearchBar } from '../../shared/search_bar';
import { getTimeRangeComparison } from '../../shared/time_comparison/get_time_range_comparison';
import { ServiceList } from './service_list';
import { MLCallout, shouldDisplayMlCallout } from '../../shared/ml_callout';
+import { joinByKey } from '../../../../common/utils/join_by_key';
const initialData = {
requestId: '',
- mainStatisticsData: {
- items: [],
- hasHistoricalData: true,
- hasLegacyData: false,
- },
+ items: [],
+ hasHistoricalData: true,
+ hasLegacyData: false,
};
function useServicesFetcher() {
@@ -36,7 +35,7 @@ function useServicesFetcher() {
const {
query: { rangeFrom, rangeTo, environment, kuery },
- } = useAnyOfApmParams('/services/{serviceName}', '/services');
+ } = useApmParams('/services');
const { start, end } = useTimeRange({ rangeFrom, rangeTo });
@@ -47,7 +46,23 @@ function useServicesFetcher() {
comparisonType,
});
- const { data = initialData, status: mainStatisticsStatus } = useFetcher(
+ const sortedAndFilteredServicesFetch = useFetcher(
+ (callApmApi) => {
+ return callApmApi('GET /internal/apm/sorted_and_filtered_services', {
+ params: {
+ query: {
+ start,
+ end,
+ environment,
+ kuery,
+ },
+ },
+ });
+ },
+ [start, end, environment, kuery]
+ );
+
+ const mainStatisticsFetch = useFetcher(
(callApmApi) => {
if (start && end) {
return callApmApi('GET /internal/apm/services', {
@@ -62,7 +77,7 @@ function useServicesFetcher() {
}).then((mainStatisticsData) => {
return {
requestId: uuid(),
- mainStatisticsData,
+ ...mainStatisticsData,
};
});
}
@@ -70,9 +85,9 @@ function useServicesFetcher() {
[environment, kuery, start, end]
);
- const { mainStatisticsData, requestId } = data;
+ const { data: mainStatisticsData = initialData } = mainStatisticsFetch;
- const { data: comparisonData } = useFetcher(
+ const comparisonFetch = useFetcher(
(callApmApi) => {
if (start && end && mainStatisticsData.items.length) {
return callApmApi('GET /internal/apm/services/detailed_statistics', {
@@ -96,20 +111,23 @@ function useServicesFetcher() {
},
// only fetches detailed statistics when requestId is invalidated by main statistics api call or offset is changed
// eslint-disable-next-line react-hooks/exhaustive-deps
- [requestId, offset],
+ [mainStatisticsData.requestId, offset],
{ preservePreviousData: false }
);
return {
- mainStatisticsData,
- mainStatisticsStatus,
- comparisonData,
+ sortedAndFilteredServicesFetch,
+ mainStatisticsFetch,
+ comparisonFetch,
};
}
export function ServiceInventory() {
- const { mainStatisticsData, mainStatisticsStatus, comparisonData } =
- useServicesFetcher();
+ const {
+ sortedAndFilteredServicesFetch,
+ mainStatisticsFetch,
+ comparisonFetch,
+ } = useServicesFetcher();
const { anomalyDetectionSetupState } = useAnomalyDetectionJobsContext();
@@ -122,8 +140,13 @@ export function ServiceInventory() {
!userHasDismissedCallout &&
shouldDisplayMlCallout(anomalyDetectionSetupState);
- const isLoading = mainStatisticsStatus === FETCH_STATUS.LOADING;
- const isFailure = mainStatisticsStatus === FETCH_STATUS.FAILURE;
+ const isLoading =
+ sortedAndFilteredServicesFetch.status === FETCH_STATUS.LOADING ||
+ (sortedAndFilteredServicesFetch.status === FETCH_STATUS.SUCCESS &&
+ sortedAndFilteredServicesFetch.data?.services.length === 0 &&
+ mainStatisticsFetch.status === FETCH_STATUS.LOADING);
+
+ const isFailure = mainStatisticsFetch.status === FETCH_STATUS.FAILURE;
const noItemsMessage = (
);
+ const items = joinByKey(
+ [
+ ...(sortedAndFilteredServicesFetch.data?.services ?? []),
+ ...(mainStatisticsFetch.data?.items ?? []),
+ ],
+ 'serviceName'
+ );
+
return (
<>
@@ -154,8 +185,8 @@ export function ServiceInventory() {
diff --git a/x-pack/plugins/apm/public/components/app/service_inventory/service_inventory.stories.tsx b/x-pack/plugins/apm/public/components/app/service_inventory/service_inventory.stories.tsx
index bececfb545ba9..01430c93b4b5a 100644
--- a/x-pack/plugins/apm/public/components/app/service_inventory/service_inventory.stories.tsx
+++ b/x-pack/plugins/apm/public/components/app/service_inventory/service_inventory.stories.tsx
@@ -30,6 +30,10 @@ const stories: Meta<{}> = {
switch (endpoint) {
case '/internal/apm/services':
return { items: [] };
+
+ case '/internal/apm/sorted_and_filtered_services':
+ return { services: [] };
+
default:
return {};
}
diff --git a/x-pack/plugins/apm/public/components/app/service_inventory/service_list/index.tsx b/x-pack/plugins/apm/public/components/app/service_inventory/service_list/index.tsx
index 760b849775429..2d01a11d92186 100644
--- a/x-pack/plugins/apm/public/components/app/service_inventory/service_list/index.tsx
+++ b/x-pack/plugins/apm/public/components/app/service_inventory/service_list/index.tsx
@@ -17,7 +17,6 @@ import { i18n } from '@kbn/i18n';
import { TypeOf } from '@kbn/typed-react-router-config';
import { orderBy } from 'lodash';
import React, { useMemo } from 'react';
-import { ValuesType } from 'utility-types';
import { NOT_AVAILABLE_LABEL } from '../../../../../common/i18n';
import { ServiceHealthStatus } from '../../../../../common/service_health_status';
import {
@@ -46,14 +45,11 @@ import {
getTimeSeriesColor,
} from '../../../shared/charts/helper/get_timeseries_color';
import { HealthBadge } from './health_badge';
+import { ServiceListItem } from '../../../../../common/service_inventory';
-type ServiceListAPIResponse = APIReturnType<'GET /internal/apm/services'>;
-type Items = ServiceListAPIResponse['items'];
type ServicesDetailedStatisticsAPIResponse =
APIReturnType<'GET /internal/apm/services/detailed_statistics'>;
-type ServiceListItem = ValuesType;
-
function formatString(value?: string | null) {
return value || NOT_AVAILABLE_LABEL;
}
@@ -239,7 +235,7 @@ export function getServiceColumns({
}
interface Props {
- items: Items;
+ items: ServiceListItem[];
comparisonData?: ServicesDetailedStatisticsAPIResponse;
noItemsMessage?: React.ReactNode;
isLoading: boolean;
@@ -287,9 +283,8 @@ export function ServiceList({
]
);
- const initialSortField = displayHealthStatus
- ? 'healthStatus'
- : 'transactionsPerMinute';
+ const initialSortField = displayHealthStatus ? 'healthStatus' : 'serviceName';
+ const initialSortDirection = displayHealthStatus ? 'desc' : 'asc';
return (
@@ -336,9 +331,9 @@ export function ServiceList({
items={items}
noItemsMessage={noItemsMessage}
initialSortField={initialSortField}
- initialSortDirection="desc"
+ initialSortDirection={initialSortDirection}
sortFn={(itemsToSort, sortField, sortDirection) => {
- // For healthStatus, sort items by healthStatus first, then by TPM
+ // For healthStatus, sort items by healthStatus first, then by name
return sortField === 'healthStatus'
? orderBy(
itemsToSort,
@@ -348,9 +343,9 @@ export function ServiceList({
? SERVICE_HEALTH_STATUS_ORDER.indexOf(item.healthStatus)
: -1;
},
- (item) => item.throughput ?? 0,
+ (item) => item.serviceName.toLowerCase(),
],
- [sortDirection, sortDirection]
+ [sortDirection, sortDirection === 'asc' ? 'desc' : 'asc']
)
: orderBy(
itemsToSort,
diff --git a/x-pack/plugins/apm/server/routes/services/get_services/get_health_statuses.ts b/x-pack/plugins/apm/server/routes/services/get_services/get_health_statuses.ts
index 65fb04b821ffa..4e8795aacc228 100644
--- a/x-pack/plugins/apm/server/routes/services/get_services/get_health_statuses.ts
+++ b/x-pack/plugins/apm/server/routes/services/get_services/get_health_statuses.ts
@@ -6,14 +6,16 @@
*/
import { getSeverity } from '../../../../common/anomaly_detection';
-import { getServiceHealthStatus } from '../../../../common/service_health_status';
+import {
+ getServiceHealthStatus,
+ ServiceHealthStatus,
+} from '../../../../common/service_health_status';
import { getServiceAnomalies } from '../../../routes/service_map/get_service_anomalies';
import { ServicesItemsSetup } from './get_services_items';
interface AggregationParams {
environment: string;
setup: ServicesItemsSetup;
- searchAggregatedTransactions: boolean;
start: number;
end: number;
}
@@ -23,7 +25,9 @@ export const getHealthStatuses = async ({
setup,
start,
end,
-}: AggregationParams) => {
+}: AggregationParams): Promise<
+ Array<{ serviceName: string; healthStatus: ServiceHealthStatus }>
+> => {
if (!setup.ml) {
return [];
}
diff --git a/x-pack/plugins/apm/server/routes/services/get_services/get_sorted_and_filtered_services.ts b/x-pack/plugins/apm/server/routes/services/get_services/get_sorted_and_filtered_services.ts
new file mode 100644
index 0000000000000..5a7df14fa3186
--- /dev/null
+++ b/x-pack/plugins/apm/server/routes/services/get_services/get_sorted_and_filtered_services.ts
@@ -0,0 +1,80 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { Logger } from '@kbn/logging';
+import { SERVICE_NAME } from '../../../../common/elasticsearch_fieldnames';
+import { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values';
+import { Environment } from '../../../../common/environment_rt';
+import { ProcessorEvent } from '../../../../common/processor_event';
+import { joinByKey } from '../../../../common/utils/join_by_key';
+import { Setup } from '../../../lib/helpers/setup_request';
+import { getHealthStatuses } from './get_health_statuses';
+
+export async function getSortedAndFilteredServices({
+ setup,
+ start,
+ end,
+ environment,
+ logger,
+}: {
+ setup: Setup;
+ start: number;
+ end: number;
+ environment: Environment;
+ logger: Logger;
+}) {
+ const { apmEventClient } = setup;
+
+ async function getServicesFromTermsEnum() {
+ if (environment !== ENVIRONMENT_ALL.value) {
+ return [];
+ }
+ const response = await apmEventClient.termsEnum(
+ 'get_services_from_terms_enum',
+ {
+ apm: {
+ events: [
+ ProcessorEvent.transaction,
+ ProcessorEvent.span,
+ ProcessorEvent.metric,
+ ProcessorEvent.error,
+ ],
+ },
+ body: {
+ size: 500,
+ field: SERVICE_NAME,
+ },
+ }
+ );
+
+ return response.terms;
+ }
+
+ const [servicesWithHealthStatuses, serviceNamesFromTermsEnum] =
+ await Promise.all([
+ getHealthStatuses({
+ setup,
+ start,
+ end,
+ environment,
+ }).catch((error) => {
+ logger.error(error);
+ return [];
+ }),
+ getServicesFromTermsEnum(),
+ ]);
+
+ const services = joinByKey(
+ [
+ ...servicesWithHealthStatuses,
+ ...serviceNamesFromTermsEnum.map((serviceName) => ({ serviceName })),
+ ],
+ 'serviceName'
+ );
+
+ return services;
+}
diff --git a/x-pack/plugins/apm/server/routes/services/route.ts b/x-pack/plugins/apm/server/routes/services/route.ts
index 55f7b4f14b7b6..7c69f3b1df802 100644
--- a/x-pack/plugins/apm/server/routes/services/route.ts
+++ b/x-pack/plugins/apm/server/routes/services/route.ts
@@ -52,6 +52,8 @@ import { ML_ERRORS } from '../../../common/anomaly_detection';
import { ScopedAnnotationsClient } from '../../../../observability/server';
import { Annotation } from './../../../../observability/common/annotations';
import { ConnectionStatsItemWithImpact } from './../../../common/connections';
+import { getSortedAndFilteredServices } from './get_services/get_sorted_and_filtered_services';
+import { ServiceHealthStatus } from './../../../common/service_health_status';
const servicesRoute = createApmServerRoute({
endpoint: 'GET /internal/apm/services',
@@ -1224,6 +1226,45 @@ const serviceAnomalyChartsRoute = createApmServerRoute({
},
});
+const sortedAndFilteredServicesRoute = createApmServerRoute({
+ endpoint: 'GET /internal/apm/sorted_and_filtered_services',
+ options: {
+ tags: ['access:apm'],
+ },
+ params: t.type({
+ query: t.intersection([rangeRt, environmentRt, kueryRt]),
+ }),
+ handler: async (
+ resources
+ ): Promise<{
+ services: Array<{
+ serviceName: string;
+ healthStatus?: ServiceHealthStatus;
+ }>;
+ }> => {
+ const {
+ query: { start, end, environment, kuery },
+ } = resources.params;
+
+ if (kuery) {
+ return {
+ services: [],
+ };
+ }
+
+ const setup = await setupRequest(resources);
+ return {
+ services: await getSortedAndFilteredServices({
+ setup,
+ start,
+ end,
+ environment,
+ logger: resources.logger,
+ }),
+ };
+ },
+});
+
export const serviceRouteRepository = {
...servicesRoute,
...servicesDetailedStatisticsRoute,
@@ -1245,4 +1286,5 @@ export const serviceRouteRepository = {
...serviceAlertsRoute,
...serviceInfrastructureRoute,
...serviceAnomalyChartsRoute,
+ ...sortedAndFilteredServicesRoute,
};
diff --git a/x-pack/test/apm_api_integration/common/utils/create_and_run_apm_ml_job.ts b/x-pack/test/apm_api_integration/common/utils/create_and_run_apm_ml_job.ts
new file mode 100644
index 0000000000000..bd03493039b49
--- /dev/null
+++ b/x-pack/test/apm_api_integration/common/utils/create_and_run_apm_ml_job.ts
@@ -0,0 +1,38 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import job from '../../../../plugins/ml/server/models/data_recognizer/modules/apm_transaction/ml/apm_tx_metrics.json';
+import datafeed from '../../../../plugins/ml/server/models/data_recognizer/modules/apm_transaction/ml/datafeed_apm_tx_metrics.json';
+import { MlApi } from '../../../functional/services/ml/api';
+
+export function createAndRunApmMlJob({ ml, environment }: { ml: MlApi; environment: string }) {
+ return ml.createAndRunAnomalyDetectionLookbackJob(
+ // @ts-expect-error not entire job config
+ {
+ ...job,
+ job_id: `apm-tx-metrics-${environment}`,
+ allow_lazy_open: false,
+ custom_settings: {
+ job_tags: {
+ apm_ml_version: '3',
+ environment,
+ },
+ },
+ },
+ {
+ ...datafeed,
+ job_id: `apm-tx-metrics-${environment}`,
+ indices: ['apm-*'],
+ datafeed_id: `apm-tx-metrics-${environment}-datafeed`,
+ query: {
+ bool: {
+ filter: [...datafeed.query.bool.filter, { term: { 'service.environment': environment } }],
+ },
+ },
+ }
+ );
+}
diff --git a/x-pack/test/apm_api_integration/tests/anomalies/anomaly_charts.spec.ts b/x-pack/test/apm_api_integration/tests/anomalies/anomaly_charts.spec.ts
index 00bdc258fb40f..e06c519a978ab 100644
--- a/x-pack/test/apm_api_integration/tests/anomalies/anomaly_charts.spec.ts
+++ b/x-pack/test/apm_api_integration/tests/anomalies/anomaly_charts.spec.ts
@@ -10,10 +10,9 @@ import { range, omit } from 'lodash';
import { apm, timerange } from '@elastic/apm-synthtrace';
import { FtrProviderContext } from '../../common/ftr_provider_context';
import { ApmApiError } from '../../common/apm_api_supertest';
-import job from '../../../../plugins/ml/server/models/data_recognizer/modules/apm_transaction/ml/apm_tx_metrics.json';
-import datafeed from '../../../../plugins/ml/server/models/data_recognizer/modules/apm_transaction/ml/datafeed_apm_tx_metrics.json';
import { ServiceAnomalyTimeseries } from '../../../../plugins/apm/common/anomaly_detection/service_anomaly_timeseries';
import { ApmMlDetectorType } from '../../../../plugins/apm/common/anomaly_detection/apm_ml_detectors';
+import { createAndRunApmMlJob } from '../../common/utils/create_and_run_apm_ml_job';
export default function ApiTest({ getService }: FtrProviderContext) {
const registry = getService('registry');
@@ -169,62 +168,8 @@ export default function ApiTest({ getService }: FtrProviderContext) {
describe('with ml jobs', () => {
before(async () => {
await Promise.all([
- ml.createAndRunAnomalyDetectionLookbackJob(
- // @ts-expect-error not entire job config
- {
- ...job,
- job_id: 'apm-tx-metrics-prod',
- allow_lazy_open: false,
- custom_settings: {
- job_tags: {
- apm_ml_version: '3',
- environment: 'production',
- },
- },
- },
- {
- ...datafeed,
- job_id: 'apm-tx-metrics-prod',
- indices: ['apm-*'],
- datafeed_id: 'apm-tx-metrics-prod-datafeed',
- query: {
- bool: {
- filter: [
- ...datafeed.query.bool.filter,
- { term: { 'service.environment': 'production' } },
- ],
- },
- },
- }
- ),
- ml.createAndRunAnomalyDetectionLookbackJob(
- // @ts-expect-error not entire job config
- {
- ...job,
- job_id: 'apm-tx-metrics-development',
- allow_lazy_open: false,
- custom_settings: {
- job_tags: {
- apm_ml_version: '3',
- environment: 'development',
- },
- },
- },
- {
- ...datafeed,
- job_id: 'apm-tx-metrics-development',
- indices: ['apm-*'],
- datafeed_id: 'apm-tx-metrics-development-datafeed',
- query: {
- bool: {
- filter: [
- ...datafeed.query.bool.filter,
- { term: { 'service.environment': 'development' } },
- ],
- },
- },
- }
- ),
+ createAndRunApmMlJob({ environment: 'production', ml }),
+ createAndRunApmMlJob({ environment: 'development', ml }),
]);
});
@@ -288,7 +233,7 @@ export default function ApiTest({ getService }: FtrProviderContext) {
expect(omitTimeseriesData(latencySeries)).to.eql({
type: ApmMlDetectorType.txLatency,
- jobId: 'apm-tx-metrics-prod',
+ jobId: 'apm-tx-metrics-production',
serviceName: 'a',
environment: 'production',
transactionType: 'request',
@@ -297,7 +242,7 @@ export default function ApiTest({ getService }: FtrProviderContext) {
expect(omitTimeseriesData(throughputSeries)).to.eql({
type: ApmMlDetectorType.txThroughput,
- jobId: 'apm-tx-metrics-prod',
+ jobId: 'apm-tx-metrics-production',
serviceName: 'a',
environment: 'production',
transactionType: 'request',
@@ -306,7 +251,7 @@ export default function ApiTest({ getService }: FtrProviderContext) {
expect(omitTimeseriesData(failureRateSeries)).to.eql({
type: ApmMlDetectorType.txFailureRate,
- jobId: 'apm-tx-metrics-prod',
+ jobId: 'apm-tx-metrics-production',
serviceName: 'a',
environment: 'production',
transactionType: 'request',
diff --git a/x-pack/test/apm_api_integration/tests/services/sorted_and_filtered_services.spec.ts b/x-pack/test/apm_api_integration/tests/services/sorted_and_filtered_services.spec.ts
new file mode 100644
index 0000000000000..b529ab36c5637
--- /dev/null
+++ b/x-pack/test/apm_api_integration/tests/services/sorted_and_filtered_services.spec.ts
@@ -0,0 +1,157 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+import { ValuesType } from 'utility-types';
+import { apm, timerange } from '@elastic/apm-synthtrace';
+import expect from '@kbn/expect';
+import { FtrProviderContext } from '../../common/ftr_provider_context';
+import { createAndRunApmMlJob } from '../../common/utils/create_and_run_apm_ml_job';
+
+export default function ApiTest({ getService }: FtrProviderContext) {
+ const registry = getService('registry');
+ const synthtraceClient = getService('synthtraceEsClient');
+ const apmApiClient = getService('apmApiClient');
+ const ml = getService('ml');
+
+ const start = '2021-01-01T12:00:00.000Z';
+ const end = '2021-08-01T12:00:00.000Z';
+
+ // the terms enum API will return names for deleted services,
+ // so we add a prefix to make sure we don't get data from other
+ // tests
+ const SERVICE_NAME_PREFIX = 'sorted_and_filtered_';
+
+ async function getSortedAndFilteredServices({
+ environment = 'ENVIRONMENT_ALL',
+ kuery = '',
+ }: { environment?: string; kuery?: string } = {}) {
+ const response = await apmApiClient.readUser({
+ endpoint: 'GET /internal/apm/sorted_and_filtered_services',
+ params: {
+ query: {
+ start,
+ end,
+ environment,
+ kuery,
+ },
+ },
+ });
+
+ return response.body.services
+ .filter((service) => service.serviceName.startsWith(SERVICE_NAME_PREFIX))
+ .map((service) => ({
+ ...service,
+ serviceName: service.serviceName.replace(SERVICE_NAME_PREFIX, ''),
+ }));
+ }
+
+ type ServiceListItem = ValuesType>>;
+
+ registry.when(
+ 'Sorted and filtered services',
+ { config: 'trial', archives: ['apm_mappings_only_8.0.0'] },
+ () => {
+ before(async () => {
+ const serviceA = apm.service(SERVICE_NAME_PREFIX + 'a', 'production', 'java').instance('a');
+
+ const serviceB = apm.service(SERVICE_NAME_PREFIX + 'b', 'development', 'go').instance('b');
+
+ const serviceC = apm.service(SERVICE_NAME_PREFIX + 'c', 'development', 'go').instance('c');
+
+ const spikeStart = new Date('2021-01-07T12:00:00.000Z').getTime();
+ const spikeEnd = new Date('2021-01-07T14:00:00.000Z').getTime();
+
+ const eventsWithinTimerange = timerange(new Date(start).getTime(), new Date(end).getTime())
+ .interval('15m')
+ .rate(1)
+ .spans((timestamp) => {
+ const isInSpike = spikeStart <= timestamp && spikeEnd >= timestamp;
+ return [
+ ...serviceA
+ .transaction('GET /api')
+ .duration(isInSpike ? 1000 : 1100)
+ .timestamp(timestamp)
+ .serialize(),
+ ...serviceB
+ .transaction('GET /api')
+ .duration(isInSpike ? 1000 : 4000)
+ .timestamp(timestamp)
+ .serialize(),
+ ];
+ });
+
+ const eventsOutsideOfTimerange = timerange(
+ new Date('2021-01-01T00:00:00.000Z').getTime(),
+ new Date(start).getTime() - 1
+ )
+ .interval('15m')
+ .rate(1)
+ .spans((timestamp) => {
+ return serviceC
+ .transaction('GET /api', 'custom')
+ .duration(1000)
+ .timestamp(timestamp)
+ .serialize();
+ });
+
+ await synthtraceClient.index(eventsWithinTimerange.concat(eventsOutsideOfTimerange));
+
+ await Promise.all([
+ createAndRunApmMlJob({ environment: 'production', ml }),
+ createAndRunApmMlJob({ environment: 'development', ml }),
+ ]);
+ });
+
+ after(() => {
+ return Promise.all([synthtraceClient.clean(), ml.cleanMlIndices()]);
+ });
+
+ describe('with no kuery or environment are set', () => {
+ let items: ServiceListItem[];
+
+ before(async () => {
+ items = await getSortedAndFilteredServices();
+ });
+
+ it('returns services based on the terms enum API and ML data', () => {
+ const serviceNames = items.map((item) => item.serviceName);
+
+ expect(serviceNames.sort()).to.eql(['a', 'b', 'c']);
+ });
+ });
+
+ describe('with kuery set', () => {
+ let items: ServiceListItem[];
+
+ before(async () => {
+ items = await getSortedAndFilteredServices({
+ kuery: 'service.name:*',
+ });
+ });
+
+ it('does not return any services', () => {
+ expect(items.length).to.be(0);
+ });
+ });
+
+ describe('with environment set to production', () => {
+ let items: ServiceListItem[];
+
+ before(async () => {
+ items = await getSortedAndFilteredServices({
+ environment: 'production',
+ });
+ });
+
+ it('returns services for production only', () => {
+ const serviceNames = items.map((item) => item.serviceName);
+
+ expect(serviceNames.sort()).to.eql(['a']);
+ });
+ });
+ }
+ );
+}
diff --git a/x-pack/test/functional/apps/apm/correlations/failed_transaction_correlations.ts b/x-pack/test/functional/apps/apm/correlations/failed_transaction_correlations.ts
index d9a5ea35e5393..f70bd4736bd7e 100644
--- a/x-pack/test/functional/apps/apm/correlations/failed_transaction_correlations.ts
+++ b/x-pack/test/functional/apps/apm/correlations/failed_transaction_correlations.ts
@@ -36,6 +36,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
});
after(async () => {
+ await esArchiver.unload('x-pack/test/functional/es_archives/infra/8.0.0/metrics_and_apm');
await spacesService.delete('custom_space');
});
@@ -54,10 +55,6 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
await testSubjects.existOrFail('apmMainContainer', {
timeout: 10000,
});
-
- const apmMainContainerText = await testSubjects.getVisibleTextAll('apmMainContainer');
- const apmMainContainerTextItems = apmMainContainerText[0].split('\n');
- expect(apmMainContainerTextItems).to.contain('No services found');
});
});
diff --git a/x-pack/test/functional/apps/apm/correlations/latency_correlations.ts b/x-pack/test/functional/apps/apm/correlations/latency_correlations.ts
index af3633798133b..200b3367b9723 100644
--- a/x-pack/test/functional/apps/apm/correlations/latency_correlations.ts
+++ b/x-pack/test/functional/apps/apm/correlations/latency_correlations.ts
@@ -37,6 +37,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
});
after(async () => {
+ await esArchiver.unload('x-pack/test/functional/es_archives/infra/8.0.0/metrics_and_apm');
await spacesService.delete('custom_space');
});
@@ -55,10 +56,6 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
await testSubjects.existOrFail('apmMainContainer', {
timeout: 10000,
});
-
- const apmMainContainerText = await testSubjects.getVisibleTextAll('apmMainContainer');
- const apmMainContainerTextItems = apmMainContainerText[0].split('\n');
- expect(apmMainContainerTextItems).to.contain('No services found');
});
});