mirror of
https://github.com/elastic/kibana.git
synced 2025-06-27 18:51:07 -04:00
Disable navigation to _other bucket and show warning tooltip (#148641)
Closes https://github.com/elastic/kibana/issues/146650 ### Summary This PR disables the link for the `_other` bucket on Services List page and instead shows a Warning Tooltip with the message. ### Changes - `service_link.tsx` prevents the navigation to the bucket - `other_service_group_bucket.ts` - Syntrace for the `_other` bucket generation ### Demo https://user-images.githubusercontent.com/7416358/211568522-e10bcf01-d07f-4259-996b-b3b612c7807d.mov - When kibana limit has been reached <img width="1561" alt="image" src="https://user-images.githubusercontent.com/1313018/214614001-0bd8a5d9-c2fe-48c7-a231-0b0805708bce.png"> - When having only `_other` <img width="1569" alt="image" src="https://user-images.githubusercontent.com/1313018/214614142-0d47d5b1-40d6-40f5-9a0d-5f5e596f9b98.png"> --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Yngrid Coello <yngrid.coello@elastic.co>
This commit is contained in:
parent
66794ac2c4
commit
065dfa1297
36 changed files with 772 additions and 394 deletions
|
@ -0,0 +1,72 @@
|
||||||
|
/*
|
||||||
|
* 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 and the Server Side Public License, v 1; you may not use this file except
|
||||||
|
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||||
|
* Side Public License, v 1.
|
||||||
|
*/
|
||||||
|
import { range as lodashRange } from 'lodash';
|
||||||
|
import { ApmFields, apm } from '@kbn/apm-synthtrace-client';
|
||||||
|
import { Scenario } from '../cli/scenario';
|
||||||
|
|
||||||
|
const scenario: Scenario<ApmFields> = async (runOptions) => {
|
||||||
|
const { logger } = runOptions;
|
||||||
|
const numServices = 10;
|
||||||
|
|
||||||
|
return {
|
||||||
|
generate: ({ range }) => {
|
||||||
|
const TRANSACTION_TYPES = ['request'];
|
||||||
|
const ENVIRONMENTS = ['production', 'development'];
|
||||||
|
|
||||||
|
const MIN_DURATION = 10;
|
||||||
|
const MAX_DURATION = 1000;
|
||||||
|
|
||||||
|
const MAX_BUCKETS = 50;
|
||||||
|
|
||||||
|
const BUCKET_SIZE = (MAX_DURATION - MIN_DURATION) / MAX_BUCKETS;
|
||||||
|
|
||||||
|
const serviceRange = [
|
||||||
|
...lodashRange(0, numServices).map((groupId) => `service-${groupId}`),
|
||||||
|
'_other',
|
||||||
|
];
|
||||||
|
|
||||||
|
const instances = serviceRange.flatMap((serviceName) => {
|
||||||
|
const services = ENVIRONMENTS.map((env) => apm.service(serviceName, env, 'go'));
|
||||||
|
|
||||||
|
return lodashRange(0, 2).flatMap((serviceNodeId) =>
|
||||||
|
services.map((service) => service.instance(`${serviceName}-${serviceNodeId}`))
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const transactionGroupRange = [
|
||||||
|
...lodashRange(0, 10).map((groupId) => `transaction-${groupId}`),
|
||||||
|
'_other',
|
||||||
|
];
|
||||||
|
|
||||||
|
return range.ratePerMinute(60).generator((timestamp, timestampIndex) => {
|
||||||
|
return logger.perf(
|
||||||
|
'generate_events_for_timestamp ' + new Date(timestamp).toISOString(),
|
||||||
|
() => {
|
||||||
|
const events = instances.flatMap((instance) =>
|
||||||
|
transactionGroupRange.flatMap((groupId, groupIndex) => {
|
||||||
|
const duration = Math.round(
|
||||||
|
(timestampIndex % MAX_BUCKETS) * BUCKET_SIZE + MIN_DURATION
|
||||||
|
);
|
||||||
|
|
||||||
|
return instance
|
||||||
|
.transaction(groupId, TRANSACTION_TYPES[groupIndex % TRANSACTION_TYPES.length])
|
||||||
|
.timestamp(timestamp)
|
||||||
|
.duration(duration)
|
||||||
|
.outcome('success' as const);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
return events;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default scenario;
|
|
@ -210,6 +210,8 @@ exports[`Error SERVICE_NAME 1`] = `"service name"`;
|
||||||
|
|
||||||
exports[`Error SERVICE_NODE_NAME 1`] = `undefined`;
|
exports[`Error SERVICE_NODE_NAME 1`] = `undefined`;
|
||||||
|
|
||||||
|
exports[`Error SERVICE_OVERFLOW_COUNT 1`] = `undefined`;
|
||||||
|
|
||||||
exports[`Error SERVICE_RUNTIME_NAME 1`] = `undefined`;
|
exports[`Error SERVICE_RUNTIME_NAME 1`] = `undefined`;
|
||||||
|
|
||||||
exports[`Error SERVICE_RUNTIME_VERSION 1`] = `undefined`;
|
exports[`Error SERVICE_RUNTIME_VERSION 1`] = `undefined`;
|
||||||
|
@ -487,6 +489,8 @@ exports[`Span SERVICE_NAME 1`] = `"service name"`;
|
||||||
|
|
||||||
exports[`Span SERVICE_NODE_NAME 1`] = `undefined`;
|
exports[`Span SERVICE_NODE_NAME 1`] = `undefined`;
|
||||||
|
|
||||||
|
exports[`Span SERVICE_OVERFLOW_COUNT 1`] = `undefined`;
|
||||||
|
|
||||||
exports[`Span SERVICE_RUNTIME_NAME 1`] = `undefined`;
|
exports[`Span SERVICE_RUNTIME_NAME 1`] = `undefined`;
|
||||||
|
|
||||||
exports[`Span SERVICE_RUNTIME_VERSION 1`] = `undefined`;
|
exports[`Span SERVICE_RUNTIME_VERSION 1`] = `undefined`;
|
||||||
|
@ -782,6 +786,8 @@ exports[`Transaction SERVICE_NAME 1`] = `"service name"`;
|
||||||
|
|
||||||
exports[`Transaction SERVICE_NODE_NAME 1`] = `undefined`;
|
exports[`Transaction SERVICE_NODE_NAME 1`] = `undefined`;
|
||||||
|
|
||||||
|
exports[`Transaction SERVICE_OVERFLOW_COUNT 1`] = `undefined`;
|
||||||
|
|
||||||
exports[`Transaction SERVICE_RUNTIME_NAME 1`] = `undefined`;
|
exports[`Transaction SERVICE_RUNTIME_NAME 1`] = `undefined`;
|
||||||
|
|
||||||
exports[`Transaction SERVICE_RUNTIME_VERSION 1`] = `undefined`;
|
exports[`Transaction SERVICE_RUNTIME_VERSION 1`] = `undefined`;
|
||||||
|
|
|
@ -33,6 +33,8 @@ export const SERVICE_RUNTIME_VERSION = 'service.runtime.version';
|
||||||
export const SERVICE_NODE_NAME = 'service.node.name';
|
export const SERVICE_NODE_NAME = 'service.node.name';
|
||||||
export const SERVICE_VERSION = 'service.version';
|
export const SERVICE_VERSION = 'service.version';
|
||||||
export const SERVICE_TARGET_TYPE = 'service.target.type';
|
export const SERVICE_TARGET_TYPE = 'service.target.type';
|
||||||
|
export const SERVICE_OVERFLOW_COUNT =
|
||||||
|
'service_transaction.aggregation.overflow_count';
|
||||||
|
|
||||||
export const URL_FULL = 'url.full';
|
export const URL_FULL = 'url.full';
|
||||||
export const HTTP_REQUEST_METHOD = 'http.request.method';
|
export const HTTP_REQUEST_METHOD = 'http.request.method';
|
||||||
|
|
|
@ -18,6 +18,7 @@ export interface ServiceListItem {
|
||||||
transactionErrorRate?: number | null;
|
transactionErrorRate?: number | null;
|
||||||
environments?: string[];
|
environments?: string[];
|
||||||
alertsCount?: number;
|
alertsCount?: number;
|
||||||
|
overflowCount?: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum ServiceInventoryFieldName {
|
export enum ServiceInventoryFieldName {
|
||||||
|
|
|
@ -12,7 +12,7 @@ import { getNodeName, NodeType } from '../../../../common/connections';
|
||||||
import { useApmParams } from '../../../hooks/use_apm_params';
|
import { useApmParams } from '../../../hooks/use_apm_params';
|
||||||
import { useFetcher } from '../../../hooks/use_fetcher';
|
import { useFetcher } from '../../../hooks/use_fetcher';
|
||||||
import { DependenciesTable } from '../../shared/dependencies_table';
|
import { DependenciesTable } from '../../shared/dependencies_table';
|
||||||
import { ServiceLink } from '../../shared/service_link';
|
import { ServiceLink } from '../../shared/links/apm/service_link';
|
||||||
import { useTimeRange } from '../../../hooks/use_time_range';
|
import { useTimeRange } from '../../../hooks/use_time_range';
|
||||||
import { getComparisonEnabled } from '../../shared/time_comparison/get_comparison_enabled';
|
import { getComparisonEnabled } from '../../shared/time_comparison/get_comparison_enabled';
|
||||||
import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context';
|
import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context';
|
||||||
|
|
|
@ -5,24 +5,31 @@
|
||||||
* 2.0.
|
* 2.0.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { EuiFlexGroup, EuiFlexItem, EuiEmptyPrompt } from '@elastic/eui';
|
import {
|
||||||
|
EuiCallOut,
|
||||||
|
EuiEmptyPrompt,
|
||||||
|
EuiFlexGroup,
|
||||||
|
EuiFlexItem,
|
||||||
|
EuiText,
|
||||||
|
} from '@elastic/eui';
|
||||||
import { i18n } from '@kbn/i18n';
|
import { i18n } from '@kbn/i18n';
|
||||||
import React from 'react';
|
import { FormattedMessage } from '@kbn/i18n-react';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
|
||||||
import { useKibana } from '@kbn/kibana-react-plugin/public';
|
import { useKibana } from '@kbn/kibana-react-plugin/public';
|
||||||
import { apmServiceInventoryOptimizedSorting } from '@kbn/observability-plugin/common';
|
import { apmServiceInventoryOptimizedSorting } from '@kbn/observability-plugin/common';
|
||||||
import { isTimeComparison } from '../../shared/time_comparison/get_comparison_options';
|
import React from 'react';
|
||||||
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
import { ServiceInventoryFieldName } from '../../../../common/service_inventory';
|
||||||
|
import { joinByKey } from '../../../../common/utils/join_by_key';
|
||||||
import { useAnomalyDetectionJobsContext } from '../../../context/anomaly_detection_jobs/use_anomaly_detection_jobs_context';
|
import { useAnomalyDetectionJobsContext } from '../../../context/anomaly_detection_jobs/use_anomaly_detection_jobs_context';
|
||||||
import { useLocalStorage } from '../../../hooks/use_local_storage';
|
|
||||||
import { useApmParams } from '../../../hooks/use_apm_params';
|
import { useApmParams } from '../../../hooks/use_apm_params';
|
||||||
import { FETCH_STATUS, useFetcher } from '../../../hooks/use_fetcher';
|
import { FETCH_STATUS, useFetcher } from '../../../hooks/use_fetcher';
|
||||||
import { useTimeRange } from '../../../hooks/use_time_range';
|
import { useLocalStorage } from '../../../hooks/use_local_storage';
|
||||||
import { SearchBar } from '../../shared/search_bar';
|
|
||||||
import { ServiceList } from './service_list';
|
|
||||||
import { MLCallout, shouldDisplayMlCallout } from '../../shared/ml_callout';
|
|
||||||
import { useProgressiveFetcher } from '../../../hooks/use_progressive_fetcher';
|
import { useProgressiveFetcher } from '../../../hooks/use_progressive_fetcher';
|
||||||
import { joinByKey } from '../../../../common/utils/join_by_key';
|
import { useTimeRange } from '../../../hooks/use_time_range';
|
||||||
import { ServiceInventoryFieldName } from '../../../../common/service_inventory';
|
import { MLCallout, shouldDisplayMlCallout } from '../../shared/ml_callout';
|
||||||
|
import { SearchBar } from '../../shared/search_bar';
|
||||||
|
import { isTimeComparison } from '../../shared/time_comparison/get_comparison_options';
|
||||||
|
import { ServiceList } from './service_list';
|
||||||
import { orderServiceItems } from './service_list/order_service_items';
|
import { orderServiceItems } from './service_list/order_service_items';
|
||||||
|
|
||||||
const initialData = {
|
const initialData = {
|
||||||
|
@ -202,6 +209,12 @@ export function ServiceInventory() {
|
||||||
...preloadedServices,
|
...preloadedServices,
|
||||||
].some((item) => 'healthStatus' in item);
|
].some((item) => 'healthStatus' in item);
|
||||||
|
|
||||||
|
const hasKibanaUiLimitRestrictedData =
|
||||||
|
mainStatisticsFetch.data?.maxServiceCountExceeded;
|
||||||
|
|
||||||
|
const serviceOverflowCount =
|
||||||
|
mainStatisticsFetch.data?.serviceOverflowCount ?? 0;
|
||||||
|
|
||||||
const displayAlerts = [...mainStatisticsItems, ...preloadedServices].some(
|
const displayAlerts = [...mainStatisticsItems, ...preloadedServices].some(
|
||||||
(item) => ServiceInventoryFieldName.AlertsCount in item
|
(item) => ServiceInventoryFieldName.AlertsCount in item
|
||||||
);
|
);
|
||||||
|
@ -280,19 +293,45 @@ export function ServiceInventory() {
|
||||||
'serviceName'
|
'serviceName'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const mlCallout = (
|
||||||
|
<EuiFlexItem>
|
||||||
|
<MLCallout
|
||||||
|
isOnSettingsPage={false}
|
||||||
|
anomalyDetectionSetupState={anomalyDetectionSetupState}
|
||||||
|
onDismiss={() => setUserHasDismissedCallout(true)}
|
||||||
|
/>
|
||||||
|
</EuiFlexItem>
|
||||||
|
);
|
||||||
|
|
||||||
|
const kibanaUiServiceLimitCallout = (
|
||||||
|
<EuiFlexItem>
|
||||||
|
<EuiCallOut
|
||||||
|
title={i18n.translate(
|
||||||
|
'xpack.apm.serviceList.ui.limit.warning.calloutTitle',
|
||||||
|
{
|
||||||
|
defaultMessage:
|
||||||
|
'Number of services exceed the allowed maximum that are displayed (1,000)',
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
color="warning"
|
||||||
|
iconType="alert"
|
||||||
|
>
|
||||||
|
<EuiText size="s">
|
||||||
|
<FormattedMessage
|
||||||
|
defaultMessage="Max. number of services that can be viewed in Kibana has been reached. Try narrowing down results by using the query bar or consider using service groups."
|
||||||
|
id="xpack.apm.serviceList.ui.limit.warning.calloutDescription"
|
||||||
|
/>
|
||||||
|
</EuiText>
|
||||||
|
</EuiCallOut>
|
||||||
|
</EuiFlexItem>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<SearchBar showTimeComparison />
|
<SearchBar showTimeComparison />
|
||||||
<EuiFlexGroup direction="column" gutterSize="m">
|
<EuiFlexGroup direction="column" gutterSize="m">
|
||||||
{displayMlCallout && (
|
{displayMlCallout && mlCallout}
|
||||||
<EuiFlexItem>
|
{hasKibanaUiLimitRestrictedData && kibanaUiServiceLimitCallout}
|
||||||
<MLCallout
|
|
||||||
isOnSettingsPage={false}
|
|
||||||
anomalyDetectionSetupState={anomalyDetectionSetupState}
|
|
||||||
onDismiss={() => setUserHasDismissedCallout(true)}
|
|
||||||
/>
|
|
||||||
</EuiFlexItem>
|
|
||||||
)}
|
|
||||||
<EuiFlexItem>
|
<EuiFlexItem>
|
||||||
<ServiceList
|
<ServiceList
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
|
@ -316,6 +355,7 @@ export function ServiceInventory() {
|
||||||
comparisonData={comparisonFetch?.data}
|
comparisonData={comparisonFetch?.data}
|
||||||
noItemsMessage={noItemsMessage}
|
noItemsMessage={noItemsMessage}
|
||||||
initialPageSize={INITIAL_PAGE_SIZE}
|
initialPageSize={INITIAL_PAGE_SIZE}
|
||||||
|
serviceOverflowCount={serviceOverflowCount}
|
||||||
/>
|
/>
|
||||||
</EuiFlexItem>
|
</EuiFlexItem>
|
||||||
</EuiFlexGroup>
|
</EuiFlexGroup>
|
||||||
|
|
|
@ -29,3 +29,33 @@ export const items: ServiceListAPIResponse['items'] = [
|
||||||
environments: [],
|
environments: [],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const overflowItems: ServiceListAPIResponse['items'] = [
|
||||||
|
{
|
||||||
|
serviceName: '_other',
|
||||||
|
transactionType: 'page-load',
|
||||||
|
agentName: 'python',
|
||||||
|
throughput: 86.93333333333334,
|
||||||
|
transactionErrorRate: 12.6,
|
||||||
|
latency: 91535.42944785276,
|
||||||
|
environments: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
serviceName: 'opbeans-node',
|
||||||
|
transactionType: 'request',
|
||||||
|
agentName: 'nodejs',
|
||||||
|
throughput: 0,
|
||||||
|
transactionErrorRate: 46.06666666666667,
|
||||||
|
latency: null,
|
||||||
|
environments: ['test'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
serviceName: 'opbeans-python',
|
||||||
|
transactionType: 'page-load',
|
||||||
|
agentName: 'python',
|
||||||
|
throughput: 86.93333333333334,
|
||||||
|
transactionErrorRate: 12.6,
|
||||||
|
latency: 91535.42944785276,
|
||||||
|
environments: [],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
|
@ -17,7 +17,6 @@ import {
|
||||||
import { i18n } from '@kbn/i18n';
|
import { i18n } from '@kbn/i18n';
|
||||||
import { TypeOf } from '@kbn/typed-react-router-config';
|
import { TypeOf } from '@kbn/typed-react-router-config';
|
||||||
import React, { useMemo } from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import { NOT_AVAILABLE_LABEL } from '../../../../../common/i18n';
|
|
||||||
import { ServiceHealthStatus } from '../../../../../common/service_health_status';
|
import { ServiceHealthStatus } from '../../../../../common/service_health_status';
|
||||||
import {
|
import {
|
||||||
ServiceInventoryFieldName,
|
ServiceInventoryFieldName,
|
||||||
|
@ -47,17 +46,12 @@ import {
|
||||||
import { EnvironmentBadge } from '../../../shared/environment_badge';
|
import { EnvironmentBadge } from '../../../shared/environment_badge';
|
||||||
import { ListMetric } from '../../../shared/list_metric';
|
import { ListMetric } from '../../../shared/list_metric';
|
||||||
import { ITableColumn, ManagedTable } from '../../../shared/managed_table';
|
import { ITableColumn, ManagedTable } from '../../../shared/managed_table';
|
||||||
import { ServiceLink } from '../../../shared/service_link';
|
import { ServiceLink } from '../../../shared/links/apm/service_link';
|
||||||
import { TruncateWithTooltip } from '../../../shared/truncate_with_tooltip';
|
|
||||||
import { HealthBadge } from './health_badge';
|
import { HealthBadge } from './health_badge';
|
||||||
|
|
||||||
type ServicesDetailedStatisticsAPIResponse =
|
type ServicesDetailedStatisticsAPIResponse =
|
||||||
APIReturnType<'POST /internal/apm/services/detailed_statistics'>;
|
APIReturnType<'POST /internal/apm/services/detailed_statistics'>;
|
||||||
|
|
||||||
function formatString(value?: string | null) {
|
|
||||||
return value || NOT_AVAILABLE_LABEL;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getServiceColumns({
|
export function getServiceColumns({
|
||||||
query,
|
query,
|
||||||
showTransactionTypeColumn,
|
showTransactionTypeColumn,
|
||||||
|
@ -67,6 +61,7 @@ export function getServiceColumns({
|
||||||
showHealthStatusColumn,
|
showHealthStatusColumn,
|
||||||
showAlertsColumn,
|
showAlertsColumn,
|
||||||
link,
|
link,
|
||||||
|
serviceOverflowCount,
|
||||||
}: {
|
}: {
|
||||||
query: TypeOf<ApmRoutes, '/services'>['query'];
|
query: TypeOf<ApmRoutes, '/services'>['query'];
|
||||||
showTransactionTypeColumn: boolean;
|
showTransactionTypeColumn: boolean;
|
||||||
|
@ -76,6 +71,7 @@ export function getServiceColumns({
|
||||||
breakpoints: Breakpoints;
|
breakpoints: Breakpoints;
|
||||||
comparisonData?: ServicesDetailedStatisticsAPIResponse;
|
comparisonData?: ServicesDetailedStatisticsAPIResponse;
|
||||||
link: any;
|
link: any;
|
||||||
|
serviceOverflowCount: number;
|
||||||
}): Array<ITableColumn<ServiceListItem>> {
|
}): Array<ITableColumn<ServiceListItem>> {
|
||||||
const { isSmall, isLarge, isXl } = breakpoints;
|
const { isSmall, isLarge, isXl } = breakpoints;
|
||||||
const showWhenSmallOrGreaterThanLarge = isSmall || !isLarge;
|
const showWhenSmallOrGreaterThanLarge = isSmall || !isLarge;
|
||||||
|
@ -136,16 +132,11 @@ export function getServiceColumns({
|
||||||
}),
|
}),
|
||||||
sortable: true,
|
sortable: true,
|
||||||
render: (_, { serviceName, agentName, transactionType }) => (
|
render: (_, { serviceName, agentName, transactionType }) => (
|
||||||
<TruncateWithTooltip
|
<ServiceLink
|
||||||
data-test-subj="apmServiceListAppLink"
|
agentName={agentName}
|
||||||
text={formatString(serviceName)}
|
query={{ ...query, transactionType }}
|
||||||
content={
|
serviceName={serviceName}
|
||||||
<ServiceLink
|
serviceOverflowCount={serviceOverflowCount}
|
||||||
agentName={agentName}
|
|
||||||
query={{ ...query, transactionType }}
|
|
||||||
serviceName={serviceName}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
@ -285,8 +276,9 @@ interface Props {
|
||||||
sortField: ServiceInventoryFieldName,
|
sortField: ServiceInventoryFieldName,
|
||||||
sortDirection: 'asc' | 'desc'
|
sortDirection: 'asc' | 'desc'
|
||||||
) => ServiceListItem[];
|
) => ServiceListItem[];
|
||||||
}
|
|
||||||
|
|
||||||
|
serviceOverflowCount: number;
|
||||||
|
}
|
||||||
export function ServiceList({
|
export function ServiceList({
|
||||||
items,
|
items,
|
||||||
noItemsMessage,
|
noItemsMessage,
|
||||||
|
@ -300,6 +292,7 @@ export function ServiceList({
|
||||||
initialSortDirection,
|
initialSortDirection,
|
||||||
initialPageSize,
|
initialPageSize,
|
||||||
sortFn,
|
sortFn,
|
||||||
|
serviceOverflowCount,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const breakpoints = useBreakpoints();
|
const breakpoints = useBreakpoints();
|
||||||
const { link } = useApmRouter();
|
const { link } = useApmRouter();
|
||||||
|
@ -337,6 +330,7 @@ export function ServiceList({
|
||||||
showHealthStatusColumn: displayHealthStatus,
|
showHealthStatusColumn: displayHealthStatus,
|
||||||
showAlertsColumn: displayAlerts,
|
showAlertsColumn: displayAlerts,
|
||||||
link,
|
link,
|
||||||
|
serviceOverflowCount,
|
||||||
}),
|
}),
|
||||||
[
|
[
|
||||||
query,
|
query,
|
||||||
|
@ -347,6 +341,7 @@ export function ServiceList({
|
||||||
displayHealthStatus,
|
displayHealthStatus,
|
||||||
displayAlerts,
|
displayAlerts,
|
||||||
link,
|
link,
|
||||||
|
serviceOverflowCount,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -191,40 +191,130 @@ describe('orderServiceItems', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when sorting by alphabetical fields', () => {
|
describe('when sorting by alphabetical fields', () => {
|
||||||
const sortedItems = orderServiceItems({
|
it('sorts correctly', () => {
|
||||||
primarySortField: ServiceInventoryFieldName.ServiceName,
|
const sortedItems = orderServiceItems({
|
||||||
sortDirection: 'asc',
|
primarySortField: ServiceInventoryFieldName.ServiceName,
|
||||||
tiebreakerField: ServiceInventoryFieldName.ServiceName,
|
sortDirection: 'asc',
|
||||||
items: [
|
tiebreakerField: ServiceInventoryFieldName.ServiceName,
|
||||||
{
|
items: [
|
||||||
serviceName: 'd-service',
|
{
|
||||||
healthStatus: ServiceHealthStatus.unknown,
|
serviceName: 'd-service',
|
||||||
},
|
healthStatus: ServiceHealthStatus.unknown,
|
||||||
{
|
},
|
||||||
serviceName: 'a-service',
|
{
|
||||||
healthStatus: ServiceHealthStatus.unknown,
|
serviceName: 'a-service',
|
||||||
},
|
healthStatus: ServiceHealthStatus.unknown,
|
||||||
{
|
},
|
||||||
serviceName: 'b-service',
|
{
|
||||||
healthStatus: ServiceHealthStatus.unknown,
|
serviceName: 'b-service',
|
||||||
},
|
healthStatus: ServiceHealthStatus.unknown,
|
||||||
{
|
},
|
||||||
serviceName: 'c-service',
|
{
|
||||||
healthStatus: ServiceHealthStatus.unknown,
|
serviceName: 'c-service',
|
||||||
},
|
healthStatus: ServiceHealthStatus.unknown,
|
||||||
{
|
},
|
||||||
serviceName: '0-service',
|
{
|
||||||
healthStatus: ServiceHealthStatus.unknown,
|
serviceName: '0-service',
|
||||||
},
|
healthStatus: ServiceHealthStatus.unknown,
|
||||||
],
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(sortedItems.map((item) => item.serviceName)).toEqual([
|
||||||
|
'0-service',
|
||||||
|
'a-service',
|
||||||
|
'b-service',
|
||||||
|
'c-service',
|
||||||
|
'd-service',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when sorting by alphabetical fields, service overflow bucket always appears 1st', () => {
|
||||||
|
it('asc', () => {
|
||||||
|
const sortedItems = orderServiceItems({
|
||||||
|
primarySortField: ServiceInventoryFieldName.ServiceName,
|
||||||
|
sortDirection: 'asc',
|
||||||
|
tiebreakerField: ServiceInventoryFieldName.ServiceName,
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
serviceName: 'd-service',
|
||||||
|
healthStatus: ServiceHealthStatus.unknown,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
serviceName: 'a-service',
|
||||||
|
healthStatus: ServiceHealthStatus.unknown,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
serviceName: 'b-service',
|
||||||
|
healthStatus: ServiceHealthStatus.unknown,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
serviceName: 'c-service',
|
||||||
|
healthStatus: ServiceHealthStatus.unknown,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
serviceName: '0-service',
|
||||||
|
healthStatus: ServiceHealthStatus.unknown,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
serviceName: '_other',
|
||||||
|
healthStatus: ServiceHealthStatus.unknown,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(sortedItems.map((item) => item.serviceName)).toEqual([
|
||||||
|
'_other',
|
||||||
|
'0-service',
|
||||||
|
'a-service',
|
||||||
|
'b-service',
|
||||||
|
'c-service',
|
||||||
|
'd-service',
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(sortedItems.map((item) => item.serviceName)).toEqual([
|
it('desc', () => {
|
||||||
'0-service',
|
const sortedItems = orderServiceItems({
|
||||||
'a-service',
|
primarySortField: ServiceInventoryFieldName.ServiceName,
|
||||||
'b-service',
|
sortDirection: 'desc',
|
||||||
'c-service',
|
tiebreakerField: ServiceInventoryFieldName.ServiceName,
|
||||||
'd-service',
|
items: [
|
||||||
]);
|
{
|
||||||
|
serviceName: 'd-service',
|
||||||
|
healthStatus: ServiceHealthStatus.unknown,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
serviceName: 'a-service',
|
||||||
|
healthStatus: ServiceHealthStatus.unknown,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
serviceName: 'b-service',
|
||||||
|
healthStatus: ServiceHealthStatus.unknown,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
serviceName: 'c-service',
|
||||||
|
healthStatus: ServiceHealthStatus.unknown,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
serviceName: '0-service',
|
||||||
|
healthStatus: ServiceHealthStatus.unknown,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
serviceName: '_other',
|
||||||
|
healthStatus: ServiceHealthStatus.unknown,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(sortedItems.map((item) => item.serviceName)).toEqual([
|
||||||
|
'_other',
|
||||||
|
'd-service',
|
||||||
|
'c-service',
|
||||||
|
'b-service',
|
||||||
|
'a-service',
|
||||||
|
'0-service',
|
||||||
|
]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -10,6 +10,7 @@ import {
|
||||||
ServiceListItem,
|
ServiceListItem,
|
||||||
ServiceInventoryFieldName,
|
ServiceInventoryFieldName,
|
||||||
} from '../../../../../common/service_inventory';
|
} from '../../../../../common/service_inventory';
|
||||||
|
import { OTHER_SERVICE_NAME } from '../../../shared/links/apm/service_link/service_max_groups_message';
|
||||||
|
|
||||||
type SortValueGetter = (item: ServiceListItem) => string | number;
|
type SortValueGetter = (item: ServiceListItem) => string | number;
|
||||||
|
|
||||||
|
@ -56,6 +57,11 @@ export function orderServiceItems({
|
||||||
// For healthStatus, sort items by healthStatus first, then by tie-breaker
|
// For healthStatus, sort items by healthStatus first, then by tie-breaker
|
||||||
|
|
||||||
const sortFn = sorts[primarySortField as ServiceInventoryFieldName];
|
const sortFn = sorts[primarySortField as ServiceInventoryFieldName];
|
||||||
|
const sortOtherBucketFirst = (item: ServiceListItem) => {
|
||||||
|
return item.serviceName === OTHER_SERVICE_NAME ? -1 : 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
const sortOtherBucketFirstDirection = 'asc';
|
||||||
|
|
||||||
if (primarySortField === ServiceInventoryFieldName.HealthStatus) {
|
if (primarySortField === ServiceInventoryFieldName.HealthStatus) {
|
||||||
const tiebreakerSortDirection =
|
const tiebreakerSortDirection =
|
||||||
|
@ -67,10 +73,13 @@ export function orderServiceItems({
|
||||||
|
|
||||||
return orderBy(
|
return orderBy(
|
||||||
items,
|
items,
|
||||||
[sortFn, tiebreakerSortFn],
|
[sortOtherBucketFirst, sortFn, tiebreakerSortFn],
|
||||||
[sortDirection, tiebreakerSortDirection]
|
[sortOtherBucketFirstDirection, sortDirection, tiebreakerSortDirection]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
return orderBy(
|
||||||
return orderBy(items, sortFn, sortDirection);
|
items,
|
||||||
|
[sortOtherBucketFirst, sortFn],
|
||||||
|
[sortOtherBucketFirstDirection, sortDirection]
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,7 +13,7 @@ import { ServiceHealthStatus } from '../../../../../common/service_health_status
|
||||||
import { ServiceInventoryFieldName } from '../../../../../common/service_inventory';
|
import { ServiceInventoryFieldName } from '../../../../../common/service_inventory';
|
||||||
import type { ApmPluginContextValue } from '../../../../context/apm_plugin/apm_plugin_context';
|
import type { ApmPluginContextValue } from '../../../../context/apm_plugin/apm_plugin_context';
|
||||||
import { MockApmPluginStorybook } from '../../../../context/apm_plugin/mock_apm_plugin_storybook';
|
import { MockApmPluginStorybook } from '../../../../context/apm_plugin/mock_apm_plugin_storybook';
|
||||||
import { items } from './__fixtures__/service_api_mock_data';
|
import { items, overflowItems } from './__fixtures__/service_api_mock_data';
|
||||||
|
|
||||||
type Args = ComponentProps<typeof ServiceList>;
|
type Args = ComponentProps<typeof ServiceList>;
|
||||||
|
|
||||||
|
@ -81,3 +81,17 @@ WithHealthWarnings.args = {
|
||||||
})),
|
})),
|
||||||
sortFn: (sortItems) => sortItems,
|
sortFn: (sortItems) => sortItems,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const WithOverflowBucket: Story<Args> = (args) => {
|
||||||
|
return <ServiceList {...args} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
WithOverflowBucket.args = {
|
||||||
|
isLoading: false,
|
||||||
|
items: overflowItems,
|
||||||
|
displayHealthStatus: false,
|
||||||
|
initialSortField: ServiceInventoryFieldName.HealthStatus,
|
||||||
|
initialSortDirection: 'desc',
|
||||||
|
initialPageSize: 25,
|
||||||
|
sortFn: (sortItems) => sortItems,
|
||||||
|
};
|
||||||
|
|
|
@ -82,6 +82,7 @@ describe('ServiceList', () => {
|
||||||
} as Breakpoints,
|
} as Breakpoints,
|
||||||
showAlertsColumn: true,
|
showAlertsColumn: true,
|
||||||
link: apmRouter.link,
|
link: apmRouter.link,
|
||||||
|
serviceOverflowCount: 0,
|
||||||
}).map((c) =>
|
}).map((c) =>
|
||||||
c.render ? c.render!(service[c.field!], service) : service[c.field!]
|
c.render ? c.render!(service[c.field!], service) : service[c.field!]
|
||||||
);
|
);
|
||||||
|
@ -122,6 +123,7 @@ describe('ServiceList', () => {
|
||||||
} as Breakpoints,
|
} as Breakpoints,
|
||||||
showAlertsColumn: true,
|
showAlertsColumn: true,
|
||||||
link: apmRouter.link,
|
link: apmRouter.link,
|
||||||
|
serviceOverflowCount: 0,
|
||||||
}).map((c) =>
|
}).map((c) =>
|
||||||
c.render ? c.render!(service[c.field!], service) : service[c.field!]
|
c.render ? c.render!(service[c.field!], service) : service[c.field!]
|
||||||
);
|
);
|
||||||
|
@ -151,6 +153,7 @@ describe('ServiceList', () => {
|
||||||
} as Breakpoints,
|
} as Breakpoints,
|
||||||
showAlertsColumn: true,
|
showAlertsColumn: true,
|
||||||
link: apmRouter.link,
|
link: apmRouter.link,
|
||||||
|
serviceOverflowCount: 0,
|
||||||
}).map((c) =>
|
}).map((c) =>
|
||||||
c.render ? c.render!(service[c.field!], service) : service[c.field!]
|
c.render ? c.render!(service[c.field!], service) : service[c.field!]
|
||||||
);
|
);
|
||||||
|
@ -190,6 +193,7 @@ describe('ServiceList', () => {
|
||||||
} as Breakpoints,
|
} as Breakpoints,
|
||||||
showAlertsColumn: true,
|
showAlertsColumn: true,
|
||||||
link: apmRouter.link,
|
link: apmRouter.link,
|
||||||
|
serviceOverflowCount: 0,
|
||||||
}).map((c) =>
|
}).map((c) =>
|
||||||
c.render ? c.render!(service[c.field!], service) : service[c.field!]
|
c.render ? c.render!(service[c.field!], service) : service[c.field!]
|
||||||
);
|
);
|
||||||
|
@ -232,6 +236,7 @@ describe('ServiceList', () => {
|
||||||
} as Breakpoints,
|
} as Breakpoints,
|
||||||
showAlertsColumn: true,
|
showAlertsColumn: true,
|
||||||
link: apmRouter.link,
|
link: apmRouter.link,
|
||||||
|
serviceOverflowCount: 0,
|
||||||
}).map((c) => c.field);
|
}).map((c) => c.field);
|
||||||
expect(renderedColumns.includes('healthStatus')).toBeFalsy();
|
expect(renderedColumns.includes('healthStatus')).toBeFalsy();
|
||||||
});
|
});
|
||||||
|
@ -251,6 +256,7 @@ describe('ServiceList', () => {
|
||||||
} as Breakpoints,
|
} as Breakpoints,
|
||||||
showAlertsColumn: true,
|
showAlertsColumn: true,
|
||||||
link: apmRouter.link,
|
link: apmRouter.link,
|
||||||
|
serviceOverflowCount: 0,
|
||||||
}).map((c) => c.field);
|
}).map((c) => c.field);
|
||||||
expect(renderedColumns.includes('healthStatus')).toBeTruthy();
|
expect(renderedColumns.includes('healthStatus')).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
@ -270,6 +276,7 @@ describe('ServiceList', () => {
|
||||||
} as Breakpoints,
|
} as Breakpoints,
|
||||||
showAlertsColumn: false,
|
showAlertsColumn: false,
|
||||||
link: apmRouter.link,
|
link: apmRouter.link,
|
||||||
|
serviceOverflowCount: 0,
|
||||||
}).map((c) => c.field);
|
}).map((c) => c.field);
|
||||||
expect(renderedColumns.includes('alertsCount')).toBeFalsy();
|
expect(renderedColumns.includes('alertsCount')).toBeFalsy();
|
||||||
});
|
});
|
||||||
|
@ -289,6 +296,7 @@ describe('ServiceList', () => {
|
||||||
} as Breakpoints,
|
} as Breakpoints,
|
||||||
showAlertsColumn: true,
|
showAlertsColumn: true,
|
||||||
link: apmRouter.link,
|
link: apmRouter.link,
|
||||||
|
serviceOverflowCount: 0,
|
||||||
}).map((c) => c.field);
|
}).map((c) => c.field);
|
||||||
expect(renderedColumns.includes('alertsCount')).toBeTruthy();
|
expect(renderedColumns.includes('alertsCount')).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
|
@ -18,7 +18,7 @@ import { useFetcher } from '../../../../hooks/use_fetcher';
|
||||||
import { useTimeRange } from '../../../../hooks/use_time_range';
|
import { useTimeRange } from '../../../../hooks/use_time_range';
|
||||||
import { DependencyLink } from '../../../shared/dependency_link';
|
import { DependencyLink } from '../../../shared/dependency_link';
|
||||||
import { DependenciesTable } from '../../../shared/dependencies_table';
|
import { DependenciesTable } from '../../../shared/dependencies_table';
|
||||||
import { ServiceLink } from '../../../shared/service_link';
|
import { ServiceLink } from '../../../shared/links/apm/service_link';
|
||||||
|
|
||||||
interface ServiceOverviewDependenciesTableProps {
|
interface ServiceOverviewDependenciesTableProps {
|
||||||
fixedHeight?: boolean;
|
fixedHeight?: boolean;
|
||||||
|
|
|
@ -13,7 +13,7 @@ import { AgentName } from '../../../../../../../typings/es_schemas/ui/fields/age
|
||||||
import { useApmPluginContext } from '../../../../../../context/apm_plugin/use_apm_plugin_context';
|
import { useApmPluginContext } from '../../../../../../context/apm_plugin/use_apm_plugin_context';
|
||||||
import { useDefaultTimeRange } from '../../../../../../hooks/use_default_time_range';
|
import { useDefaultTimeRange } from '../../../../../../hooks/use_default_time_range';
|
||||||
import { ApmRoutes } from '../../../../../routing/apm_route_config';
|
import { ApmRoutes } from '../../../../../routing/apm_route_config';
|
||||||
import { ServiceLink } from '../../../../../shared/service_link';
|
import { ServiceLink } from '../../../../../shared/links/apm/service_link';
|
||||||
import { StickyProperties } from '../../../../../shared/sticky_properties';
|
import { StickyProperties } from '../../../../../shared/sticky_properties';
|
||||||
import { getComparisonEnabled } from '../../../../../shared/time_comparison/get_comparison_enabled';
|
import { getComparisonEnabled } from '../../../../../shared/time_comparison/get_comparison_enabled';
|
||||||
import { TruncateWithTooltip } from '../../../../../shared/truncate_with_tooltip';
|
import { TruncateWithTooltip } from '../../../../../shared/truncate_with_tooltip';
|
||||||
|
|
|
@ -23,7 +23,7 @@ import { apmServiceInventoryOptimizedSorting } from '@kbn/observability-plugin/c
|
||||||
import { AgentName } from '../../../../../typings/es_schemas/ui/fields/agent';
|
import { AgentName } from '../../../../../typings/es_schemas/ui/fields/agent';
|
||||||
import { EnvironmentBadge } from '../../../shared/environment_badge';
|
import { EnvironmentBadge } from '../../../shared/environment_badge';
|
||||||
import { asPercent } from '../../../../../common/utils/formatters';
|
import { asPercent } from '../../../../../common/utils/formatters';
|
||||||
import { ServiceLink } from '../../../shared/service_link';
|
import { ServiceLink } from '../../../shared/links/apm/service_link';
|
||||||
import { TruncateWithTooltip } from '../../../shared/truncate_with_tooltip';
|
import { TruncateWithTooltip } from '../../../shared/truncate_with_tooltip';
|
||||||
import { StorageDetailsPerService } from './storage_details_per_service';
|
import { StorageDetailsPerService } from './storage_details_per_service';
|
||||||
import { getComparisonEnabled } from '../../../shared/time_comparison/get_comparison_enabled';
|
import { getComparisonEnabled } from '../../../shared/time_comparison/get_comparison_enabled';
|
||||||
|
|
|
@ -23,7 +23,7 @@ import { EmptyMessage } from '../../shared/empty_message';
|
||||||
import { ImpactBar } from '../../shared/impact_bar';
|
import { ImpactBar } from '../../shared/impact_bar';
|
||||||
import { TransactionDetailLink } from '../../shared/links/apm/transaction_detail_link';
|
import { TransactionDetailLink } from '../../shared/links/apm/transaction_detail_link';
|
||||||
import { ITableColumn, ManagedTable } from '../../shared/managed_table';
|
import { ITableColumn, ManagedTable } from '../../shared/managed_table';
|
||||||
import { ServiceLink } from '../../shared/service_link';
|
import { ServiceLink } from '../../shared/links/apm/service_link';
|
||||||
import { TruncateWithTooltip } from '../../shared/truncate_with_tooltip';
|
import { TruncateWithTooltip } from '../../shared/truncate_with_tooltip';
|
||||||
import { NOT_AVAILABLE_LABEL } from '../../../../common/i18n';
|
import { NOT_AVAILABLE_LABEL } from '../../../../common/i18n';
|
||||||
|
|
||||||
|
|
|
@ -16,7 +16,7 @@ import { LatencyAggregationType } from '../../../../../../../common/latency_aggr
|
||||||
import { Transaction } from '../../../../../../../typings/es_schemas/ui/transaction';
|
import { Transaction } from '../../../../../../../typings/es_schemas/ui/transaction';
|
||||||
import { useAnyOfApmParams } from '../../../../../../hooks/use_apm_params';
|
import { useAnyOfApmParams } from '../../../../../../hooks/use_apm_params';
|
||||||
import { TransactionDetailLink } from '../../../../../shared/links/apm/transaction_detail_link';
|
import { TransactionDetailLink } from '../../../../../shared/links/apm/transaction_detail_link';
|
||||||
import { ServiceLink } from '../../../../../shared/service_link';
|
import { ServiceLink } from '../../../../../shared/links/apm/service_link';
|
||||||
import { StickyProperties } from '../../../../../shared/sticky_properties';
|
import { StickyProperties } from '../../../../../shared/sticky_properties';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
|
|
@ -21,7 +21,7 @@ import { Transaction } from '../../../../../../../../typings/es_schemas/ui/trans
|
||||||
import { useAnyOfApmParams } from '../../../../../../../hooks/use_apm_params';
|
import { useAnyOfApmParams } from '../../../../../../../hooks/use_apm_params';
|
||||||
import { DependencyLink } from '../../../../../../shared/dependency_link';
|
import { DependencyLink } from '../../../../../../shared/dependency_link';
|
||||||
import { TransactionDetailLink } from '../../../../../../shared/links/apm/transaction_detail_link';
|
import { TransactionDetailLink } from '../../../../../../shared/links/apm/transaction_detail_link';
|
||||||
import { ServiceLink } from '../../../../../../shared/service_link';
|
import { ServiceLink } from '../../../../../../shared/links/apm/service_link';
|
||||||
import { StickyProperties } from '../../../../../../shared/sticky_properties';
|
import { StickyProperties } from '../../../../../../shared/sticky_properties';
|
||||||
import { LatencyAggregationType } from '../../../../../../../../common/latency_aggregation_types';
|
import { LatencyAggregationType } from '../../../../../../../../common/latency_aggregation_types';
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,107 @@
|
||||||
|
/*
|
||||||
|
* 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 { EuiFlexGroup, EuiFlexItem, EuiLink, EuiText } from '@elastic/eui';
|
||||||
|
import { i18n } from '@kbn/i18n';
|
||||||
|
import { euiStyled } from '@kbn/kibana-react-plugin/common';
|
||||||
|
import { TypeOf } from '@kbn/typed-react-router-config';
|
||||||
|
import React from 'react';
|
||||||
|
import { isMobileAgentName } from '../../../../../../common/agent_name';
|
||||||
|
import { NOT_AVAILABLE_LABEL } from '../../../../../../common/i18n';
|
||||||
|
import { AgentName } from '../../../../../../typings/es_schemas/ui/fields/agent';
|
||||||
|
import { useApmRouter } from '../../../../../hooks/use_apm_router';
|
||||||
|
import { truncate, unit } from '../../../../../utils/style';
|
||||||
|
import { ApmRoutes } from '../../../../routing/apm_route_config';
|
||||||
|
import { AgentIcon } from '../../../agent_icon';
|
||||||
|
import { PopoverTooltip } from '../../../popover_tooltip';
|
||||||
|
import { TruncateWithTooltip } from '../../../truncate_with_tooltip';
|
||||||
|
import {
|
||||||
|
OTHER_SERVICE_NAME,
|
||||||
|
ServiceMaxGroupsMessage,
|
||||||
|
} from './service_max_groups_message';
|
||||||
|
|
||||||
|
const StyledLink = euiStyled(EuiLink)`${truncate('100%')};`;
|
||||||
|
|
||||||
|
function formatString(value?: string | null) {
|
||||||
|
return value || NOT_AVAILABLE_LABEL;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ServiceLinkProps {
|
||||||
|
agentName?: AgentName;
|
||||||
|
query: TypeOf<ApmRoutes, '/services/{serviceName}/overview'>['query'];
|
||||||
|
serviceName: string;
|
||||||
|
serviceOverflowCount?: number;
|
||||||
|
}
|
||||||
|
export function ServiceLink({
|
||||||
|
agentName,
|
||||||
|
query,
|
||||||
|
serviceName,
|
||||||
|
serviceOverflowCount,
|
||||||
|
}: ServiceLinkProps) {
|
||||||
|
const { link } = useApmRouter();
|
||||||
|
|
||||||
|
const serviceLink = isMobileAgentName(agentName)
|
||||||
|
? '/mobile-services/{serviceName}/overview'
|
||||||
|
: '/services/{serviceName}/overview';
|
||||||
|
|
||||||
|
if (serviceName === OTHER_SERVICE_NAME) {
|
||||||
|
return (
|
||||||
|
<EuiFlexGroup alignItems="center" gutterSize="xs">
|
||||||
|
<EuiFlexItem grow={false}>
|
||||||
|
<EuiText
|
||||||
|
grow={false}
|
||||||
|
style={{ fontStyle: 'italic', fontSize: '1rem' }}
|
||||||
|
>
|
||||||
|
{i18n.translate('xpack.apm.serviceLink.otherBucketName', {
|
||||||
|
defaultMessage: 'Remaining Services',
|
||||||
|
})}
|
||||||
|
</EuiText>
|
||||||
|
</EuiFlexItem>
|
||||||
|
<EuiFlexItem>
|
||||||
|
<PopoverTooltip
|
||||||
|
ariaLabel={i18n.translate('xpack.apm.serviceLink.tooltip', {
|
||||||
|
defaultMessage:
|
||||||
|
'Number of services instrumented has reached the current capacity of the APM server',
|
||||||
|
})}
|
||||||
|
iconType="alert"
|
||||||
|
>
|
||||||
|
<EuiText style={{ width: `${unit * 28}px` }} size="s">
|
||||||
|
<ServiceMaxGroupsMessage
|
||||||
|
serviceOverflowCount={serviceOverflowCount}
|
||||||
|
/>
|
||||||
|
</EuiText>
|
||||||
|
</PopoverTooltip>
|
||||||
|
</EuiFlexItem>
|
||||||
|
</EuiFlexGroup>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TruncateWithTooltip
|
||||||
|
data-test-subj="apmServiceListAppLink"
|
||||||
|
text={formatString(serviceName)}
|
||||||
|
content={
|
||||||
|
<StyledLink
|
||||||
|
data-test-subj={`serviceLink_${agentName}`}
|
||||||
|
href={link(serviceLink, {
|
||||||
|
path: { serviceName },
|
||||||
|
query,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<EuiFlexGroup alignItems="center" gutterSize="s" responsive={false}>
|
||||||
|
<EuiFlexItem grow={false}>
|
||||||
|
<AgentIcon agentName={agentName} />
|
||||||
|
</EuiFlexItem>
|
||||||
|
<EuiFlexItem className="eui-textTruncate">
|
||||||
|
<span className="eui-textTruncate">{serviceName}</span>
|
||||||
|
</EuiFlexItem>
|
||||||
|
</EuiFlexGroup>
|
||||||
|
</StyledLink>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
|
@ -7,8 +7,8 @@
|
||||||
|
|
||||||
import { Story } from '@storybook/react';
|
import { Story } from '@storybook/react';
|
||||||
import React, { ComponentProps, ComponentType } from 'react';
|
import React, { ComponentProps, ComponentType } from 'react';
|
||||||
import { MockApmPluginStorybook } from '../../context/apm_plugin/mock_apm_plugin_storybook';
|
import { ServiceLink } from '.';
|
||||||
import { ServiceLink } from './service_link';
|
import { MockApmPluginStorybook } from '../../../../../context/apm_plugin/mock_apm_plugin_storybook';
|
||||||
|
|
||||||
type Args = ComponentProps<typeof ServiceLink>;
|
type Args = ComponentProps<typeof ServiceLink>;
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
/*
|
||||||
|
* 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 { FormattedMessage } from '@kbn/i18n-react';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
export const OTHER_SERVICE_NAME = '_other';
|
||||||
|
|
||||||
|
export function ServiceMaxGroupsMessage({
|
||||||
|
serviceOverflowCount = 0,
|
||||||
|
}: {
|
||||||
|
serviceOverflowCount?: number;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<FormattedMessage
|
||||||
|
defaultMessage="Number of services that have been instrumented has reached the current max. capacity that can be handled by the APM server. There are at least {serviceOverflowCount, plural, one {1 service} other {# services}} missing in this list. Please increase the memory allocated to APM server."
|
||||||
|
id="xpack.apm.serviceDetail.maxGroups.message"
|
||||||
|
values={{
|
||||||
|
serviceOverflowCount,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
|
@ -10,13 +10,13 @@ import React, { useState } from 'react';
|
||||||
|
|
||||||
interface PopoverTooltipProps {
|
interface PopoverTooltipProps {
|
||||||
ariaLabel?: string;
|
ariaLabel?: string;
|
||||||
children: React.ReactNode;
|
|
||||||
iconType?: string;
|
iconType?: string;
|
||||||
|
children: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function PopoverTooltip({
|
export function PopoverTooltip({
|
||||||
ariaLabel,
|
ariaLabel,
|
||||||
iconType,
|
iconType = 'questionInCircle',
|
||||||
children,
|
children,
|
||||||
}: PopoverTooltipProps) {
|
}: PopoverTooltipProps) {
|
||||||
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
|
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
|
||||||
|
@ -35,7 +35,7 @@ export function PopoverTooltip({
|
||||||
}}
|
}}
|
||||||
size="xs"
|
size="xs"
|
||||||
color="primary"
|
color="primary"
|
||||||
iconType={iconType ?? 'questionInCircle'}
|
iconType={iconType}
|
||||||
style={{ height: 'auto' }}
|
style={{ height: 'auto' }}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,56 +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
|
|
||||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
|
||||||
* 2.0.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { EuiFlexGroup, EuiFlexItem, EuiLink } from '@elastic/eui';
|
|
||||||
import React from 'react';
|
|
||||||
import { TypeOf } from '@kbn/typed-react-router-config';
|
|
||||||
import { euiStyled } from '@kbn/kibana-react-plugin/common';
|
|
||||||
import { truncate } from '../../utils/style';
|
|
||||||
import { useApmRouter } from '../../hooks/use_apm_router';
|
|
||||||
import { AgentIcon } from './agent_icon';
|
|
||||||
import { AgentName } from '../../../typings/es_schemas/ui/fields/agent';
|
|
||||||
import { ApmRoutes } from '../routing/apm_route_config';
|
|
||||||
import { isMobileAgentName } from '../../../common/agent_name';
|
|
||||||
|
|
||||||
const StyledLink = euiStyled(EuiLink)`${truncate('100%')};`;
|
|
||||||
|
|
||||||
interface ServiceLinkProps {
|
|
||||||
agentName?: AgentName;
|
|
||||||
query: TypeOf<ApmRoutes, '/services/{serviceName}/overview'>['query'];
|
|
||||||
serviceName: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ServiceLink({
|
|
||||||
agentName,
|
|
||||||
query,
|
|
||||||
serviceName,
|
|
||||||
}: ServiceLinkProps) {
|
|
||||||
const { link } = useApmRouter();
|
|
||||||
|
|
||||||
const serviceLink = isMobileAgentName(agentName)
|
|
||||||
? '/mobile-services/{serviceName}/overview'
|
|
||||||
: '/services/{serviceName}/overview';
|
|
||||||
|
|
||||||
return (
|
|
||||||
<StyledLink
|
|
||||||
data-test-subj={`serviceLink_${agentName}`}
|
|
||||||
href={link(serviceLink, {
|
|
||||||
path: { serviceName },
|
|
||||||
query,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<EuiFlexGroup alignItems="center" gutterSize="s" responsive={false}>
|
|
||||||
<EuiFlexItem grow={false}>
|
|
||||||
<AgentIcon agentName={agentName} />
|
|
||||||
</EuiFlexItem>
|
|
||||||
<EuiFlexItem className="eui-textTruncate">
|
|
||||||
<span className="eui-textTruncate">{serviceName}</span>
|
|
||||||
</EuiFlexItem>
|
|
||||||
</EuiFlexGroup>
|
|
||||||
</StyledLink>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -23,7 +23,7 @@ import { SpanLinkDetails } from '../../../../common/span_links';
|
||||||
import { asDuration } from '../../../../common/utils/formatters';
|
import { asDuration } from '../../../../common/utils/formatters';
|
||||||
import { useAnyOfApmParams } from '../../../hooks/use_apm_params';
|
import { useAnyOfApmParams } from '../../../hooks/use_apm_params';
|
||||||
import { useApmRouter } from '../../../hooks/use_apm_router';
|
import { useApmRouter } from '../../../hooks/use_apm_router';
|
||||||
import { ServiceLink } from '../service_link';
|
import { ServiceLink } from '../links/apm/service_link';
|
||||||
import { getSpanIcon } from '../span_icon/get_span_icon';
|
import { getSpanIcon } from '../span_icon/get_span_icon';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
|
|
@ -99,6 +99,11 @@ Array [
|
||||||
"aggs": Object {
|
"aggs": Object {
|
||||||
"sample": Object {
|
"sample": Object {
|
||||||
"aggs": Object {
|
"aggs": Object {
|
||||||
|
"service_overflow_count": Object {
|
||||||
|
"sum": Object {
|
||||||
|
"field": "service_transaction.aggregation.overflow_count",
|
||||||
|
},
|
||||||
|
},
|
||||||
"services": Object {
|
"services": Object {
|
||||||
"aggs": Object {
|
"aggs": Object {
|
||||||
"transactionType": Object {
|
"transactionType": Object {
|
||||||
|
@ -142,7 +147,7 @@ Array [
|
||||||
},
|
},
|
||||||
"terms": Object {
|
"terms": Object {
|
||||||
"field": "service.name",
|
"field": "service.name",
|
||||||
"size": 500,
|
"size": 1000,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -204,7 +209,7 @@ Array [
|
||||||
},
|
},
|
||||||
"terms": Object {
|
"terms": Object {
|
||||||
"field": "service.name",
|
"field": "service.name",
|
||||||
"size": 500,
|
"size": 1000,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -0,0 +1,167 @@
|
||||||
|
/*
|
||||||
|
* 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 { kqlQuery, rangeQuery } from '@kbn/observability-plugin/server';
|
||||||
|
import {
|
||||||
|
AGENT_NAME,
|
||||||
|
SERVICE_ENVIRONMENT,
|
||||||
|
SERVICE_NAME,
|
||||||
|
SERVICE_OVERFLOW_COUNT,
|
||||||
|
TRANSACTION_TYPE,
|
||||||
|
} from '../../../../common/es_fields/apm';
|
||||||
|
import {
|
||||||
|
TRANSACTION_PAGE_LOAD,
|
||||||
|
TRANSACTION_REQUEST,
|
||||||
|
} from '../../../../common/transaction_types';
|
||||||
|
import { environmentQuery } from '../../../../common/utils/environment_query';
|
||||||
|
import { AgentName } from '../../../../typings/es_schemas/ui/fields/agent';
|
||||||
|
import {
|
||||||
|
getDocumentTypeFilterForTransactions,
|
||||||
|
getDurationFieldForTransactions,
|
||||||
|
getProcessorEventForTransactions,
|
||||||
|
} from '../../../lib/helpers/transactions';
|
||||||
|
import { calculateThroughputWithRange } from '../../../lib/helpers/calculate_throughput';
|
||||||
|
import {
|
||||||
|
calculateFailedTransactionRate,
|
||||||
|
getOutcomeAggregation,
|
||||||
|
} from '../../../lib/helpers/transaction_error_rate';
|
||||||
|
import { serviceGroupQuery } from '../../../lib/service_group_query';
|
||||||
|
import { ServiceGroup } from '../../../../common/service_groups';
|
||||||
|
import { RandomSampler } from '../../../lib/helpers/get_random_sampler';
|
||||||
|
import { APMEventClient } from '../../../lib/helpers/create_es_client/create_apm_event_client';
|
||||||
|
|
||||||
|
interface AggregationParams {
|
||||||
|
environment: string;
|
||||||
|
kuery: string;
|
||||||
|
apmEventClient: APMEventClient;
|
||||||
|
searchAggregatedTransactions: boolean;
|
||||||
|
maxNumServices: number;
|
||||||
|
start: number;
|
||||||
|
end: number;
|
||||||
|
serviceGroup: ServiceGroup | null;
|
||||||
|
randomSampler: RandomSampler;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getServiceStats({
|
||||||
|
environment,
|
||||||
|
kuery,
|
||||||
|
apmEventClient,
|
||||||
|
searchAggregatedTransactions,
|
||||||
|
maxNumServices,
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
serviceGroup,
|
||||||
|
randomSampler,
|
||||||
|
}: AggregationParams) {
|
||||||
|
const outcomes = getOutcomeAggregation();
|
||||||
|
|
||||||
|
const metrics = {
|
||||||
|
avg_duration: {
|
||||||
|
avg: {
|
||||||
|
field: getDurationFieldForTransactions(searchAggregatedTransactions),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
outcomes,
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await apmEventClient.search('get_service_stats', {
|
||||||
|
apm: {
|
||||||
|
events: [getProcessorEventForTransactions(searchAggregatedTransactions)],
|
||||||
|
},
|
||||||
|
body: {
|
||||||
|
track_total_hits: false,
|
||||||
|
size: 0,
|
||||||
|
query: {
|
||||||
|
bool: {
|
||||||
|
filter: [
|
||||||
|
...getDocumentTypeFilterForTransactions(
|
||||||
|
searchAggregatedTransactions
|
||||||
|
),
|
||||||
|
...rangeQuery(start, end),
|
||||||
|
...environmentQuery(environment),
|
||||||
|
...kqlQuery(kuery),
|
||||||
|
...serviceGroupQuery(serviceGroup),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
aggs: {
|
||||||
|
sample: {
|
||||||
|
random_sampler: randomSampler,
|
||||||
|
aggs: {
|
||||||
|
service_overflow_count: {
|
||||||
|
sum: {
|
||||||
|
field: SERVICE_OVERFLOW_COUNT,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
services: {
|
||||||
|
terms: {
|
||||||
|
field: SERVICE_NAME,
|
||||||
|
size: maxNumServices,
|
||||||
|
},
|
||||||
|
aggs: {
|
||||||
|
transactionType: {
|
||||||
|
terms: {
|
||||||
|
field: TRANSACTION_TYPE,
|
||||||
|
},
|
||||||
|
aggs: {
|
||||||
|
...metrics,
|
||||||
|
environments: {
|
||||||
|
terms: {
|
||||||
|
field: SERVICE_ENVIRONMENT,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
sample: {
|
||||||
|
top_metrics: {
|
||||||
|
metrics: [{ field: AGENT_NAME } as const],
|
||||||
|
sort: {
|
||||||
|
'@timestamp': 'desc' as const,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
serviceStats:
|
||||||
|
response.aggregations?.sample.services.buckets.map((bucket) => {
|
||||||
|
const topTransactionTypeBucket =
|
||||||
|
bucket.transactionType.buckets.find(
|
||||||
|
({ key }) =>
|
||||||
|
key === TRANSACTION_REQUEST || key === TRANSACTION_PAGE_LOAD
|
||||||
|
) ?? bucket.transactionType.buckets[0];
|
||||||
|
|
||||||
|
return {
|
||||||
|
serviceName: bucket.key as string,
|
||||||
|
transactionType: topTransactionTypeBucket.key as string,
|
||||||
|
environments: topTransactionTypeBucket.environments.buckets.map(
|
||||||
|
(environmentBucket) => environmentBucket.key as string
|
||||||
|
),
|
||||||
|
agentName: topTransactionTypeBucket.sample.top[0].metrics[
|
||||||
|
AGENT_NAME
|
||||||
|
] as AgentName,
|
||||||
|
latency: topTransactionTypeBucket.avg_duration.value,
|
||||||
|
transactionErrorRate: calculateFailedTransactionRate(
|
||||||
|
topTransactionTypeBucket.outcomes
|
||||||
|
),
|
||||||
|
throughput: calculateThroughputWithRange({
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
value: topTransactionTypeBucket.doc_count,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}) ?? [],
|
||||||
|
serviceOverflowCount:
|
||||||
|
response.aggregations?.sample?.service_overflow_count.value || 0,
|
||||||
|
};
|
||||||
|
}
|
|
@ -11,6 +11,7 @@ import {
|
||||||
AGENT_NAME,
|
AGENT_NAME,
|
||||||
SERVICE_ENVIRONMENT,
|
SERVICE_ENVIRONMENT,
|
||||||
SERVICE_NAME,
|
SERVICE_NAME,
|
||||||
|
SERVICE_OVERFLOW_COUNT,
|
||||||
TRANSACTION_TYPE,
|
TRANSACTION_TYPE,
|
||||||
TRANSACTION_DURATION_SUMMARY,
|
TRANSACTION_DURATION_SUMMARY,
|
||||||
TRANSACTION_FAILURE_COUNT,
|
TRANSACTION_FAILURE_COUNT,
|
||||||
|
@ -40,7 +41,7 @@ interface AggregationParams {
|
||||||
randomSampler: RandomSampler;
|
randomSampler: RandomSampler;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getServiceAggregatedTransactionStats({
|
export async function getServiceStatsForServiceMetrics({
|
||||||
environment,
|
environment,
|
||||||
kuery,
|
kuery,
|
||||||
apmEventClient,
|
apmEventClient,
|
||||||
|
@ -51,7 +52,7 @@ export async function getServiceAggregatedTransactionStats({
|
||||||
randomSampler,
|
randomSampler,
|
||||||
}: AggregationParams) {
|
}: AggregationParams) {
|
||||||
const response = await apmEventClient.search(
|
const response = await apmEventClient.search(
|
||||||
'get_service_aggregated_transaction_stats',
|
'get_service_stats_for_service_metric',
|
||||||
{
|
{
|
||||||
apm: {
|
apm: {
|
||||||
events: [ProcessorEvent.metric],
|
events: [ProcessorEvent.metric],
|
||||||
|
@ -74,6 +75,11 @@ export async function getServiceAggregatedTransactionStats({
|
||||||
sample: {
|
sample: {
|
||||||
random_sampler: randomSampler,
|
random_sampler: randomSampler,
|
||||||
aggs: {
|
aggs: {
|
||||||
|
overflowCount: {
|
||||||
|
sum: {
|
||||||
|
field: SERVICE_OVERFLOW_COUNT,
|
||||||
|
},
|
||||||
|
},
|
||||||
services: {
|
services: {
|
||||||
terms: {
|
terms: {
|
||||||
field: SERVICE_NAME,
|
field: SERVICE_NAME,
|
||||||
|
@ -124,34 +130,39 @@ export async function getServiceAggregatedTransactionStats({
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return {
|
||||||
response.aggregations?.sample.services.buckets.map((bucket) => {
|
serviceStats:
|
||||||
const topTransactionTypeBucket =
|
response.aggregations?.sample.services.buckets.map((bucket) => {
|
||||||
bucket.transactionType.buckets.find(
|
const topTransactionTypeBucket =
|
||||||
({ key }) =>
|
bucket.transactionType.buckets.find(
|
||||||
key === TRANSACTION_REQUEST || key === TRANSACTION_PAGE_LOAD
|
({ key }) =>
|
||||||
) ?? bucket.transactionType.buckets[0];
|
key === TRANSACTION_REQUEST || key === TRANSACTION_PAGE_LOAD
|
||||||
|
) ?? bucket.transactionType.buckets[0];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
serviceName: bucket.key as string,
|
serviceName: bucket.key as string,
|
||||||
transactionType: topTransactionTypeBucket.key as string,
|
transactionType: topTransactionTypeBucket.key as string,
|
||||||
environments: topTransactionTypeBucket.environments.buckets.map(
|
environments: topTransactionTypeBucket.environments.buckets.map(
|
||||||
(environmentBucket) => environmentBucket.key as string
|
(environmentBucket) => environmentBucket.key as string
|
||||||
),
|
),
|
||||||
agentName: topTransactionTypeBucket.sample.top[0].metrics[
|
agentName: topTransactionTypeBucket.sample.top[0].metrics[
|
||||||
AGENT_NAME
|
AGENT_NAME
|
||||||
] as AgentName,
|
] as AgentName,
|
||||||
latency: topTransactionTypeBucket.avg_duration.value,
|
latency: topTransactionTypeBucket.avg_duration.value,
|
||||||
transactionErrorRate: calculateFailedTransactionRateFromServiceMetrics({
|
transactionErrorRate:
|
||||||
failedTransactions: topTransactionTypeBucket.failure_count.value,
|
calculateFailedTransactionRateFromServiceMetrics({
|
||||||
successfulTransactions: topTransactionTypeBucket.success_count.value,
|
failedTransactions: topTransactionTypeBucket.failure_count.value,
|
||||||
}),
|
successfulTransactions:
|
||||||
throughput: calculateThroughputWithRange({
|
topTransactionTypeBucket.success_count.value,
|
||||||
start,
|
}),
|
||||||
end,
|
throughput: calculateThroughputWithRange({
|
||||||
value: topTransactionTypeBucket.doc_count,
|
start,
|
||||||
}),
|
end,
|
||||||
};
|
value: topTransactionTypeBucket.doc_count,
|
||||||
}) ?? []
|
}),
|
||||||
);
|
};
|
||||||
|
}) ?? [],
|
||||||
|
serviceOverflowCount:
|
||||||
|
response.aggregations?.sample?.overflowCount.value || 0,
|
||||||
|
};
|
||||||
}
|
}
|
|
@ -1,163 +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
|
|
||||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
|
||||||
* 2.0.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { kqlQuery, rangeQuery } from '@kbn/observability-plugin/server';
|
|
||||||
import {
|
|
||||||
AGENT_NAME,
|
|
||||||
SERVICE_ENVIRONMENT,
|
|
||||||
SERVICE_NAME,
|
|
||||||
TRANSACTION_TYPE,
|
|
||||||
} from '../../../../common/es_fields/apm';
|
|
||||||
import {
|
|
||||||
TRANSACTION_PAGE_LOAD,
|
|
||||||
TRANSACTION_REQUEST,
|
|
||||||
} from '../../../../common/transaction_types';
|
|
||||||
import { environmentQuery } from '../../../../common/utils/environment_query';
|
|
||||||
import { AgentName } from '../../../../typings/es_schemas/ui/fields/agent';
|
|
||||||
import {
|
|
||||||
getDocumentTypeFilterForTransactions,
|
|
||||||
getDurationFieldForTransactions,
|
|
||||||
getProcessorEventForTransactions,
|
|
||||||
} from '../../../lib/helpers/transactions';
|
|
||||||
import { calculateThroughputWithRange } from '../../../lib/helpers/calculate_throughput';
|
|
||||||
import {
|
|
||||||
calculateFailedTransactionRate,
|
|
||||||
getOutcomeAggregation,
|
|
||||||
} from '../../../lib/helpers/transaction_error_rate';
|
|
||||||
import { serviceGroupQuery } from '../../../lib/service_group_query';
|
|
||||||
import { ServiceGroup } from '../../../../common/service_groups';
|
|
||||||
import { RandomSampler } from '../../../lib/helpers/get_random_sampler';
|
|
||||||
import { APMEventClient } from '../../../lib/helpers/create_es_client/create_apm_event_client';
|
|
||||||
|
|
||||||
interface AggregationParams {
|
|
||||||
environment: string;
|
|
||||||
kuery: string;
|
|
||||||
apmEventClient: APMEventClient;
|
|
||||||
searchAggregatedTransactions: boolean;
|
|
||||||
maxNumServices: number;
|
|
||||||
start: number;
|
|
||||||
end: number;
|
|
||||||
serviceGroup: ServiceGroup | null;
|
|
||||||
randomSampler: RandomSampler;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getServiceTransactionStats({
|
|
||||||
environment,
|
|
||||||
kuery,
|
|
||||||
apmEventClient,
|
|
||||||
searchAggregatedTransactions,
|
|
||||||
maxNumServices,
|
|
||||||
start,
|
|
||||||
end,
|
|
||||||
serviceGroup,
|
|
||||||
randomSampler,
|
|
||||||
}: AggregationParams) {
|
|
||||||
const outcomes = getOutcomeAggregation();
|
|
||||||
|
|
||||||
const metrics = {
|
|
||||||
avg_duration: {
|
|
||||||
avg: {
|
|
||||||
field: getDurationFieldForTransactions(searchAggregatedTransactions),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
outcomes,
|
|
||||||
};
|
|
||||||
|
|
||||||
const response = await apmEventClient.search(
|
|
||||||
'get_service_transaction_stats',
|
|
||||||
{
|
|
||||||
apm: {
|
|
||||||
events: [
|
|
||||||
getProcessorEventForTransactions(searchAggregatedTransactions),
|
|
||||||
],
|
|
||||||
},
|
|
||||||
body: {
|
|
||||||
track_total_hits: false,
|
|
||||||
size: 0,
|
|
||||||
query: {
|
|
||||||
bool: {
|
|
||||||
filter: [
|
|
||||||
...getDocumentTypeFilterForTransactions(
|
|
||||||
searchAggregatedTransactions
|
|
||||||
),
|
|
||||||
...rangeQuery(start, end),
|
|
||||||
...environmentQuery(environment),
|
|
||||||
...kqlQuery(kuery),
|
|
||||||
...serviceGroupQuery(serviceGroup),
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
aggs: {
|
|
||||||
sample: {
|
|
||||||
random_sampler: randomSampler,
|
|
||||||
aggs: {
|
|
||||||
services: {
|
|
||||||
terms: {
|
|
||||||
field: SERVICE_NAME,
|
|
||||||
size: maxNumServices,
|
|
||||||
},
|
|
||||||
aggs: {
|
|
||||||
transactionType: {
|
|
||||||
terms: {
|
|
||||||
field: TRANSACTION_TYPE,
|
|
||||||
},
|
|
||||||
aggs: {
|
|
||||||
...metrics,
|
|
||||||
environments: {
|
|
||||||
terms: {
|
|
||||||
field: SERVICE_ENVIRONMENT,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
sample: {
|
|
||||||
top_metrics: {
|
|
||||||
metrics: [{ field: AGENT_NAME } as const],
|
|
||||||
sort: {
|
|
||||||
'@timestamp': 'desc' as const,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
response.aggregations?.sample.services.buckets.map((bucket) => {
|
|
||||||
const topTransactionTypeBucket =
|
|
||||||
bucket.transactionType.buckets.find(
|
|
||||||
({ key }) =>
|
|
||||||
key === TRANSACTION_REQUEST || key === TRANSACTION_PAGE_LOAD
|
|
||||||
) ?? bucket.transactionType.buckets[0];
|
|
||||||
|
|
||||||
return {
|
|
||||||
serviceName: bucket.key as string,
|
|
||||||
transactionType: topTransactionTypeBucket.key as string,
|
|
||||||
environments: topTransactionTypeBucket.environments.buckets.map(
|
|
||||||
(environmentBucket) => environmentBucket.key as string
|
|
||||||
),
|
|
||||||
agentName: topTransactionTypeBucket.sample.top[0].metrics[
|
|
||||||
AGENT_NAME
|
|
||||||
] as AgentName,
|
|
||||||
latency: topTransactionTypeBucket.avg_duration.value,
|
|
||||||
transactionErrorRate: calculateFailedTransactionRate(
|
|
||||||
topTransactionTypeBucket.outcomes
|
|
||||||
),
|
|
||||||
throughput: calculateThroughputWithRange({
|
|
||||||
start,
|
|
||||||
end,
|
|
||||||
value: topTransactionTypeBucket.doc_count,
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
}) ?? []
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -87,15 +87,18 @@ export async function getServicesFromErrorAndMetricDocuments({
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return {
|
||||||
response.aggregations?.sample.services.buckets.map((bucket) => {
|
services:
|
||||||
return {
|
response.aggregations?.sample.services.buckets.map((bucket) => {
|
||||||
serviceName: bucket.key as string,
|
return {
|
||||||
environments: bucket.environments.buckets.map(
|
serviceName: bucket.key as string,
|
||||||
(envBucket) => envBucket.key as string
|
environments: bucket.environments.buckets.map(
|
||||||
),
|
(envBucket) => envBucket.key as string
|
||||||
agentName: bucket.latest.top[0].metrics[AGENT_NAME] as AgentName,
|
),
|
||||||
};
|
agentName: bucket.latest.top[0].metrics[AGENT_NAME] as AgentName,
|
||||||
}) ?? []
|
};
|
||||||
);
|
}) ?? [],
|
||||||
|
maxServiceCountExceeded:
|
||||||
|
(response.aggregations?.sample.services.sum_other_doc_count ?? 0) > 0,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,8 +10,8 @@ import { withApmSpan } from '../../../utils/with_apm_span';
|
||||||
import { MlClient } from '../../../lib/helpers/get_ml_client';
|
import { MlClient } from '../../../lib/helpers/get_ml_client';
|
||||||
import { getHealthStatuses } from './get_health_statuses';
|
import { getHealthStatuses } from './get_health_statuses';
|
||||||
import { getServicesFromErrorAndMetricDocuments } from './get_services_from_error_and_metric_documents';
|
import { getServicesFromErrorAndMetricDocuments } from './get_services_from_error_and_metric_documents';
|
||||||
import { getServiceTransactionStats } from './get_service_transaction_stats';
|
import { getServiceStats } from './get_service_stats';
|
||||||
import { getServiceAggregatedTransactionStats } from './get_service_aggregated_transaction_stats';
|
import { getServiceStatsForServiceMetrics } from './get_service_stats_for_service_metric';
|
||||||
import { mergeServiceStats } from './merge_service_stats';
|
import { mergeServiceStats } from './merge_service_stats';
|
||||||
import { ServiceGroup } from '../../../../common/service_groups';
|
import { ServiceGroup } from '../../../../common/service_groups';
|
||||||
import { RandomSampler } from '../../../lib/helpers/get_random_sampler';
|
import { RandomSampler } from '../../../lib/helpers/get_random_sampler';
|
||||||
|
@ -19,7 +19,7 @@ import { APMEventClient } from '../../../lib/helpers/create_es_client/create_apm
|
||||||
import { getServicesAlerts } from './get_service_alerts';
|
import { getServicesAlerts } from './get_service_alerts';
|
||||||
import { ApmAlertsClient } from '../../../lib/helpers/get_apm_alerts_client';
|
import { ApmAlertsClient } from '../../../lib/helpers/get_apm_alerts_client';
|
||||||
|
|
||||||
export const MAX_NUMBER_OF_SERVICES = 500;
|
export const MAX_NUMBER_OF_SERVICES = 1_000;
|
||||||
|
|
||||||
export async function getServicesItems({
|
export async function getServicesItems({
|
||||||
environment,
|
environment,
|
||||||
|
@ -62,17 +62,17 @@ export async function getServicesItems({
|
||||||
};
|
};
|
||||||
|
|
||||||
const [
|
const [
|
||||||
transactionStats,
|
{ serviceStats, serviceOverflowCount },
|
||||||
servicesFromErrorAndMetricDocuments,
|
{ services, maxServiceCountExceeded },
|
||||||
healthStatuses,
|
healthStatuses,
|
||||||
alertCounts,
|
alertCounts,
|
||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
searchAggregatedServiceMetrics
|
searchAggregatedServiceMetrics
|
||||||
? getServiceAggregatedTransactionStats({
|
? getServiceStatsForServiceMetrics({
|
||||||
...commonParams,
|
...commonParams,
|
||||||
apmEventClient,
|
apmEventClient,
|
||||||
})
|
})
|
||||||
: getServiceTransactionStats({
|
: getServiceStats({
|
||||||
...commonParams,
|
...commonParams,
|
||||||
apmEventClient,
|
apmEventClient,
|
||||||
}),
|
}),
|
||||||
|
@ -90,11 +90,16 @@ export async function getServicesItems({
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return mergeServiceStats({
|
return {
|
||||||
transactionStats,
|
items:
|
||||||
servicesFromErrorAndMetricDocuments,
|
mergeServiceStats({
|
||||||
healthStatuses,
|
serviceStats,
|
||||||
alertCounts,
|
servicesFromErrorAndMetricDocuments: services,
|
||||||
});
|
healthStatuses,
|
||||||
|
alertCounts,
|
||||||
|
}) ?? [],
|
||||||
|
maxServiceCountExceeded,
|
||||||
|
serviceOverflowCount,
|
||||||
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,23 +42,26 @@ export async function getServices({
|
||||||
randomSampler: RandomSampler;
|
randomSampler: RandomSampler;
|
||||||
}) {
|
}) {
|
||||||
return withApmSpan('get_services', async () => {
|
return withApmSpan('get_services', async () => {
|
||||||
const items = await getServicesItems({
|
const { items, maxServiceCountExceeded, serviceOverflowCount } =
|
||||||
environment,
|
await getServicesItems({
|
||||||
kuery,
|
environment,
|
||||||
mlClient,
|
kuery,
|
||||||
apmEventClient,
|
mlClient,
|
||||||
apmAlertsClient,
|
apmEventClient,
|
||||||
searchAggregatedTransactions,
|
apmAlertsClient,
|
||||||
searchAggregatedServiceMetrics,
|
searchAggregatedTransactions,
|
||||||
logger,
|
searchAggregatedServiceMetrics,
|
||||||
start,
|
logger,
|
||||||
end,
|
start,
|
||||||
serviceGroup,
|
end,
|
||||||
randomSampler,
|
serviceGroup,
|
||||||
});
|
randomSampler,
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
items,
|
items,
|
||||||
|
maxServiceCountExceeded,
|
||||||
|
serviceOverflowCount,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,12 +5,12 @@
|
||||||
* 2.0.
|
* 2.0.
|
||||||
*/
|
*/
|
||||||
import { ServiceHealthStatus } from '../../../../common/service_health_status';
|
import { ServiceHealthStatus } from '../../../../common/service_health_status';
|
||||||
import { getServiceTransactionStats } from './get_service_transaction_stats';
|
import { getServiceStats } from './get_service_stats';
|
||||||
import { mergeServiceStats } from './merge_service_stats';
|
import { mergeServiceStats } from './merge_service_stats';
|
||||||
|
|
||||||
type ServiceTransactionStat = Awaited<
|
type ServiceTransactionStat = Awaited<
|
||||||
ReturnType<typeof getServiceTransactionStats>
|
ReturnType<typeof getServiceStats>
|
||||||
>[number];
|
>['serviceStats'][number];
|
||||||
|
|
||||||
function stat(values: Partial<ServiceTransactionStat>): ServiceTransactionStat {
|
function stat(values: Partial<ServiceTransactionStat>): ServiceTransactionStat {
|
||||||
return {
|
return {
|
||||||
|
@ -29,7 +29,7 @@ describe('mergeServiceStats', () => {
|
||||||
it('joins stats by service name', () => {
|
it('joins stats by service name', () => {
|
||||||
expect(
|
expect(
|
||||||
mergeServiceStats({
|
mergeServiceStats({
|
||||||
transactionStats: [
|
serviceStats: [
|
||||||
stat({
|
stat({
|
||||||
serviceName: 'opbeans-java',
|
serviceName: 'opbeans-java',
|
||||||
environments: ['production'],
|
environments: ['production'],
|
||||||
|
@ -87,7 +87,7 @@ describe('mergeServiceStats', () => {
|
||||||
it('shows services that only have metric documents', () => {
|
it('shows services that only have metric documents', () => {
|
||||||
expect(
|
expect(
|
||||||
mergeServiceStats({
|
mergeServiceStats({
|
||||||
transactionStats: [
|
serviceStats: [
|
||||||
stat({
|
stat({
|
||||||
serviceName: 'opbeans-java-2',
|
serviceName: 'opbeans-java-2',
|
||||||
environments: ['staging'],
|
environments: ['staging'],
|
||||||
|
@ -136,7 +136,7 @@ describe('mergeServiceStats', () => {
|
||||||
it('does not show services that only have ML data', () => {
|
it('does not show services that only have ML data', () => {
|
||||||
expect(
|
expect(
|
||||||
mergeServiceStats({
|
mergeServiceStats({
|
||||||
transactionStats: [
|
serviceStats: [
|
||||||
stat({
|
stat({
|
||||||
serviceName: 'opbeans-java-2',
|
serviceName: 'opbeans-java-2',
|
||||||
environments: ['staging'],
|
environments: ['staging'],
|
||||||
|
@ -173,7 +173,7 @@ describe('mergeServiceStats', () => {
|
||||||
it('concatenates environments from metric/transaction data', () => {
|
it('concatenates environments from metric/transaction data', () => {
|
||||||
expect(
|
expect(
|
||||||
mergeServiceStats({
|
mergeServiceStats({
|
||||||
transactionStats: [
|
serviceStats: [
|
||||||
stat({
|
stat({
|
||||||
serviceName: 'opbeans-java',
|
serviceName: 'opbeans-java',
|
||||||
environments: ['staging'],
|
environments: ['staging'],
|
||||||
|
|
|
@ -10,24 +10,22 @@ import { joinByKey } from '../../../../common/utils/join_by_key';
|
||||||
import { getServicesAlerts } from './get_service_alerts';
|
import { getServicesAlerts } from './get_service_alerts';
|
||||||
import { getHealthStatuses } from './get_health_statuses';
|
import { getHealthStatuses } from './get_health_statuses';
|
||||||
import { getServicesFromErrorAndMetricDocuments } from './get_services_from_error_and_metric_documents';
|
import { getServicesFromErrorAndMetricDocuments } from './get_services_from_error_and_metric_documents';
|
||||||
import { getServiceTransactionStats } from './get_service_transaction_stats';
|
import { getServiceStats } from './get_service_stats';
|
||||||
|
|
||||||
export function mergeServiceStats({
|
export function mergeServiceStats({
|
||||||
transactionStats,
|
serviceStats,
|
||||||
servicesFromErrorAndMetricDocuments,
|
servicesFromErrorAndMetricDocuments,
|
||||||
healthStatuses,
|
healthStatuses,
|
||||||
alertCounts,
|
alertCounts,
|
||||||
}: {
|
}: {
|
||||||
transactionStats: Awaited<ReturnType<typeof getServiceTransactionStats>>;
|
serviceStats: Awaited<ReturnType<typeof getServiceStats>>['serviceStats'];
|
||||||
servicesFromErrorAndMetricDocuments: Awaited<
|
servicesFromErrorAndMetricDocuments: Awaited<
|
||||||
ReturnType<typeof getServicesFromErrorAndMetricDocuments>
|
ReturnType<typeof getServicesFromErrorAndMetricDocuments>
|
||||||
>;
|
>['services'];
|
||||||
healthStatuses: Awaited<ReturnType<typeof getHealthStatuses>>;
|
healthStatuses: Awaited<ReturnType<typeof getHealthStatuses>>;
|
||||||
alertCounts: Awaited<ReturnType<typeof getServicesAlerts>>;
|
alertCounts: Awaited<ReturnType<typeof getServicesAlerts>>;
|
||||||
}) {
|
}) {
|
||||||
const foundServiceNames = transactionStats.map(
|
const foundServiceNames = serviceStats.map(({ serviceName }) => serviceName);
|
||||||
({ serviceName }) => serviceName
|
|
||||||
);
|
|
||||||
|
|
||||||
const servicesWithOnlyMetricDocuments =
|
const servicesWithOnlyMetricDocuments =
|
||||||
servicesFromErrorAndMetricDocuments.filter(
|
servicesFromErrorAndMetricDocuments.filter(
|
||||||
|
@ -46,7 +44,7 @@ export function mergeServiceStats({
|
||||||
|
|
||||||
return joinByKey(
|
return joinByKey(
|
||||||
asMutableArray([
|
asMutableArray([
|
||||||
...transactionStats,
|
...serviceStats,
|
||||||
...servicesFromErrorAndMetricDocuments,
|
...servicesFromErrorAndMetricDocuments,
|
||||||
...matchedHealthStatuses,
|
...matchedHealthStatuses,
|
||||||
...alertCounts,
|
...alertCounts,
|
||||||
|
|
|
@ -116,6 +116,8 @@ const servicesRoute = createApmServerRoute({
|
||||||
alertsCount: number;
|
alertsCount: number;
|
||||||
}
|
}
|
||||||
>;
|
>;
|
||||||
|
maxServiceCountExceeded: boolean;
|
||||||
|
serviceOverflowCount: number;
|
||||||
}> {
|
}> {
|
||||||
const {
|
const {
|
||||||
config,
|
config,
|
||||||
|
|
|
@ -51,6 +51,8 @@ export default function ApiTest({ getService }: FtrProviderContext) {
|
||||||
|
|
||||||
expect(response.status).to.be(200);
|
expect(response.status).to.be(200);
|
||||||
expect(response.body.items.length).to.be(0);
|
expect(response.body.items.length).to.be(0);
|
||||||
|
expect(response.body.maxServiceCountExceeded).to.be(false);
|
||||||
|
expect(response.body.serviceOverflowCount).to.be(0);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue