[APM] Storage explorer improvements (#143179)

* Add total APM size to summary stats

* Show percentage of disk space used by the APM indices compared to the overall storage configured for ES

* Add tips and references section

* Add tooltips to summary metrics

* Add indices breakdown table

* Add callout for cross-cluster search

Co-authored-by: boriskirov <boris.kirov@elastic.co>
This commit is contained in:
Giorgos Bamparopoulos 2022-10-26 12:01:28 +01:00 committed by GitHub
parent b5c852bb40
commit e390743f2e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
32 changed files with 1615 additions and 437 deletions

View file

@ -50,6 +50,10 @@ export const getDocLinks = ({ kibanaBranch }: GetDocLinkOptions): DocLinks => {
overview: `${APM_DOCS}guide/${DOC_LINK_VERSION}/apm-overview.html`,
tailSamplingPolicies: `${APM_DOCS}guide/${DOC_LINK_VERSION}/configure-tail-based-sampling.html`,
elasticAgent: `${APM_DOCS}guide/${DOC_LINK_VERSION}/upgrade-to-apm-integration.html`,
storageExplorer: `${KIBANA_DOCS}storage-explorer.html`,
spanCompression: `${APM_DOCS}guide/${DOC_LINK_VERSION}/span-compression.html`,
transactionSampling: `${APM_DOCS}guide/${DOC_LINK_VERSION}/sampling.html`,
indexLifecycleManagement: `${APM_DOCS}guide/${DOC_LINK_VERSION}/ilm-how-to.html`,
},
canvas: {
guide: `${KIBANA_DOCS}canvas.html`,

View file

@ -35,6 +35,10 @@ export interface DocLinks {
readonly overview: string;
readonly tailSamplingPolicies: string;
readonly elasticAgent: string;
readonly storageExplorer: string;
readonly spanCompression: string;
readonly transactionSampling: string;
readonly indexLifecycleManagement: string;
};
readonly canvas: {
readonly guide: string;

View file

@ -79,6 +79,8 @@ describe('Storage Explorer', () => {
it('has a list of summary stats', () => {
cy.contains('Total APM size');
cy.contains('Disk space used');
cy.contains('Incremental APM size');
cy.contains('Daily data generation');
cy.contains('Traces per minute');
cy.contains('Number of services');
@ -200,6 +202,7 @@ describe('Storage Explorer', () => {
cy.contains('Service storage details');
cy.getByTestSubj('storageExplorerTimeseriesChart');
cy.getByTestSubj('serviceStorageDetailsTable');
cy.getByTestSubj('storageExplorerIndicesStatsTable');
});
});
});

View file

@ -21,7 +21,7 @@ Cypress.Commands.add('loginAsEditorUser', () => {
Cypress.Commands.add('loginAsMonitorUser', () => {
return cy.loginAs({
username: ApmUsername.apmMonitorIndices,
username: ApmUsername.apmMonitorClusterAndIndices,
password: 'changeme',
});
});

View file

@ -0,0 +1,24 @@
/*
* 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 { CoreStart } from '@kbn/core-lifecycle-browser';
export function getIndexManagementHref(core: CoreStart) {
return core.application.getUrlForApp('management', {
path: '/data/index_management/data_streams',
});
}
export function getStorageExplorerFeedbackHref() {
return 'https://ela.st/feedback-storage-explorer';
}
export function getKibanaAdvancedSettingsHref(core: CoreStart) {
return core.application.getUrlForApp('management', {
path: '/kibana/settings?query=category:(observability)',
});
}

View file

@ -12,8 +12,14 @@ import {
EuiSpacer,
EuiEmptyPrompt,
EuiLoadingSpinner,
EuiCallOut,
EuiLink,
EuiButton,
EuiPanel,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context';
import { IndexLifecyclePhaseSelect } from './index_lifecycle_phase_select';
import { ServicesTable } from './services_table';
import { SearchBar } from '../../shared/search_bar';
@ -22,18 +28,51 @@ import { PermissionDenied } from './prompts/permission_denied';
import { useFetcher, FETCH_STATUS } from '../../../hooks/use_fetcher';
import { SummaryStats } from './summary_stats';
import { ApmEnvironmentFilter } from '../../shared/environment_filter';
import { TipsAndResources } from './resources/tips_and_resources';
import { useLocalStorage } from '../../../hooks/use_local_storage';
import { getKibanaAdvancedSettingsHref } from './get_storage_explorer_links';
const INITIAL_DATA = { hasPrivileges: false };
type CalloutType = 'crossClusterSearch' | 'optimizePerformance';
const CALLOUT_DISMISS_INITIAL_STATE: Record<CalloutType, boolean> = {
crossClusterSearch: false,
optimizePerformance: false,
};
const dismissButtonText = i18n.translate(
'xpack.apm.storageExplorer.callout.dimissButton',
{
defaultMessage: 'Dismiss',
}
);
export function StorageExplorer() {
const { data: { hasPrivileges } = INITIAL_DATA, status } = useFetcher(
const { core } = useApmPluginContext();
const [calloutDismissed, setCalloutDismissed] = useLocalStorage(
'apm.storageExplorer.calloutDismissed',
CALLOUT_DISMISS_INITIAL_STATE
);
const { data: hasPrivilegesData, status: hasPrivilegesStatus } = useFetcher(
(callApmApi) => {
return callApmApi('GET /internal/apm/storage_explorer/privileges');
},
[]
);
const loading = status === FETCH_STATUS.LOADING;
const { data: isCrossClusterSearchData } = useFetcher(
(callApmApi) => {
if (!calloutDismissed.crossClusterSearch) {
return callApmApi(
'GET /internal/apm/storage_explorer/is_cross_cluster_search'
);
}
},
[calloutDismissed]
);
const loading = hasPrivilegesStatus === FETCH_STATUS.LOADING;
if (loading) {
return (
@ -51,7 +90,7 @@ export function StorageExplorer() {
);
}
if (!hasPrivileges) {
if (!hasPrivilegesData?.hasPrivileges) {
return <PermissionDenied />;
}
@ -67,11 +106,94 @@ export function StorageExplorer() {
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer />
{!calloutDismissed.optimizePerformance && (
<EuiCallOut
title={i18n.translate(
'xpack.apm.storageExplorer.longLoadingTimeCalloutTitle',
{
defaultMessage: 'Long loading time?',
}
)}
iconType="timeRefresh"
>
<p>
<FormattedMessage
id="xpack.apm.storageExplorer.longLoadingTimeCalloutText"
defaultMessage="Enable progressive loading of data or optimized sorting for services list in {kibanaAdvancedSettingsLink}."
values={{
kibanaAdvancedSettingsLink: (
<EuiLink href={getKibanaAdvancedSettingsHref(core)}>
{i18n.translate(
'xpack.apm.storageExplorer.longLoadingTimeCalloutLink',
{
defaultMessage: 'Kibana advanced settings',
}
)}
</EuiLink>
),
}}
/>
</p>
<EuiButton
onClick={() =>
setCalloutDismissed({
...calloutDismissed,
optimizePerformance: true,
})
}
>
{dismissButtonText}
</EuiButton>
</EuiCallOut>
)}
{!calloutDismissed.crossClusterSearch &&
isCrossClusterSearchData?.isCrossClusterSearch && (
<>
<EuiSpacer size="s" />
<EuiCallOut
title={i18n.translate(
'xpack.apm.storageExplorer.crossClusterSearchCalloutTitle',
{
defaultMessage: 'Searching across clusters?',
}
)}
iconType="search"
>
<p>
{i18n.translate(
'xpack.apm.storageExplorer.crossClusterSearchCalloutText',
{
defaultMessage:
'While getting document count works with cross-cluster search, index statistics such as size are only displayed for data that are stored in this cluster.',
}
)}
</p>
<EuiButton
onClick={() =>
setCalloutDismissed({
...calloutDismissed,
crossClusterSearch: true,
})
}
>
{dismissButtonText}
</EuiButton>
</EuiCallOut>
</>
)}
<EuiSpacer />
<SummaryStats />
<EuiSpacer />
<StorageChart />
<EuiPanel hasShadow={false} hasBorder={true}>
<StorageChart />
<EuiSpacer />
<ServicesTable />
</EuiPanel>
<EuiSpacer />
<ServicesTable />
<TipsAndResources />
</>
);
}

View file

@ -0,0 +1,203 @@
/*
* 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 React from 'react';
import {
EuiAccordion,
EuiPanel,
EuiFlexItem,
EuiTitle,
EuiButton,
EuiCard,
EuiIcon,
EuiListGroup,
EuiFlexGroup,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { useApmRouter } from '../../../../hooks/use_apm_router';
import { useApmParams } from '../../../../hooks/use_apm_params';
import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plugin_context';
import {
getIndexManagementHref,
getStorageExplorerFeedbackHref,
} from '../get_storage_explorer_links';
export function TipsAndResources() {
const router = useApmRouter();
const { core } = useApmPluginContext();
const { docLinks } = core;
const {
query: { rangeFrom, rangeTo, environment, kuery, comparisonEnabled },
} = useApmParams('/storage-explorer');
const cards = [
{
icon: 'beaker',
title: i18n.translate(
'xpack.apm.storageExplorer.resources.errorMessages.title',
{
defaultMessage: 'Reduce transactions',
}
),
description: i18n.translate(
'xpack.apm.storageExplorer.resources.errorMessages.description',
{
defaultMessage:
'Configure a more aggressive transaction sampling policy. Transaction sampling lowers the amount of data ingested without negatively impacting the usefulness of that data.',
}
),
href: docLinks.links.apm.transactionSampling,
},
{
icon: 'visLine',
title: i18n.translate(
'xpack.apm.storageExplorer.resources.compressedSpans.title',
{
defaultMessage: 'Reduce spans',
}
),
description: i18n.translate(
'xpack.apm.storageExplorer.resources.compressedSpans.description',
{
defaultMessage:
'Enable span compression. Span compression saves on data and transfer costs by compressing multiple similar spans into a single span.',
}
),
href: docLinks.links.apm.spanCompression,
},
{
icon: 'indexEdit',
title: i18n.translate(
'xpack.apm.storageExplorer.resources.samplingRate.title',
{
defaultMessage: 'Manage the index lifecycle',
}
),
description: i18n.translate(
'xpack.apm.storageExplorer.resources.samplingRate.description',
{
defaultMessage:
'Customize your index lifecycle policies. Index lifecycle policies allow you to manage indices according to your performance, resiliency, and retention requirements.',
}
),
href: docLinks.links.apm.indexLifecycleManagement,
},
];
const resourcesListItems = [
{
label: i18n.translate(
'xpack.apm.storageExplorer.resources.indexManagement',
{
defaultMessage: 'Index management',
}
),
href: getIndexManagementHref(core),
iconType: 'indexEdit',
},
{
label: i18n.translate(
'xpack.apm.storageExplorer.resources.serviceInventory',
{
defaultMessage: 'Service inventory',
}
),
href: router.link('/services', {
query: {
rangeFrom,
rangeTo,
environment,
comparisonEnabled,
kuery,
serviceGroup: '',
},
}),
iconType: 'tableDensityExpanded',
},
{
label: i18n.translate(
'xpack.apm.storageExplorer.resources.documentation',
{
defaultMessage: 'Documentation',
}
),
href: docLinks.links.apm.storageExplorer,
target: '_blank',
iconType: 'documentation',
},
{
label: i18n.translate(
'xpack.apm.storageExplorer.resources.sendFeedback',
{
defaultMessage: 'Send feedback',
}
),
href: getStorageExplorerFeedbackHref(),
target: '_blank',
iconType: 'editorComment',
},
];
return (
<EuiPanel hasBorder={true} hasShadow={false}>
<EuiAccordion
id="tipsAndResourcesAccordion"
buttonContent={
<EuiTitle size="xs">
<h2>
{i18n.translate(
'xpack.apm.storageExplorer.resources.accordionTitle',
{
defaultMessage: 'Tips and tricks',
}
)}
</h2>
</EuiTitle>
}
initialIsOpen
paddingSize="m"
>
<EuiFlexGroup justifyContent="spaceAround">
{cards.map(({ icon, title, description, href }) => (
<EuiFlexItem>
<EuiCard
icon={<EuiIcon size="xl" type={icon} />}
title={title}
description={description}
footer={
<EuiButton href={href} target="_blank">
{i18n.translate(
'xpack.apm.storageExplorer.resources.learnMoreButton',
{
defaultMessage: 'Learn more',
}
)}
</EuiButton>
}
/>
</EuiFlexItem>
))}
<EuiFlexItem grow={false}>
<EuiTitle size="xs">
<h3>
{i18n.translate('xpack.apm.storageExplorer.resources.title', {
defaultMessage: 'Resources',
})}
</h3>
</EuiTitle>
<EuiListGroup
listItems={resourcesListItems}
color="primary"
size="s"
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiAccordion>
</EuiPanel>
);
}

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import React, { useState, useEffect, ReactNode } from 'react';
import React, { useState, ReactNode } from 'react';
import {
EuiInMemoryTable,
EuiBasicTableColumn,
@ -14,9 +14,13 @@ import {
RIGHT_ALIGNMENT,
EuiToolTip,
EuiIcon,
EuiProgress,
EuiPanel,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { ValuesType } from 'utility-types';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import { apmServiceInventoryOptimizedSorting } from '@kbn/observability-plugin/common';
import { AgentName } from '../../../../../typings/es_schemas/ui/fields/agent';
import { EnvironmentBadge } from '../../../shared/environment_badge';
import { asPercent } from '../../../../../common/utils/formatters';
import { ServiceLink } from '../../../shared/service_link';
@ -27,14 +31,26 @@ import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plug
import { asDynamicBytes } from '../../../../../common/utils/formatters';
import { NOT_AVAILABLE_LABEL } from '../../../../../common/i18n';
import { useApmParams } from '../../../../hooks/use_apm_params';
import { FETCH_STATUS } from '../../../../hooks/use_fetcher';
import { FETCH_STATUS, useFetcher } from '../../../../hooks/use_fetcher';
import { useProgressiveFetcher } from '../../../../hooks/use_progressive_fetcher';
import { useTimeRange } from '../../../../hooks/use_time_range';
import { SizeLabel } from './size_label';
import type { APIReturnType } from '../../../../services/rest/create_call_apm_api';
import { joinByKey } from '../../../../../common/utils/join_by_key';
type StorageExplorerItems =
APIReturnType<'GET /internal/apm/storage_explorer'>['serviceStatistics'];
interface StorageExplorerItem {
serviceName: string;
environments?: string[];
size?: number;
agentName?: AgentName;
sampling?: number;
}
enum StorageExplorerFieldName {
ServiceName = 'serviceName',
Environments = 'environments',
Sampling = 'sampling',
Size = 'size',
}
export function ServicesTable() {
const [itemIdToExpandedRowMap, setItemIdToExpandedRowMap] = useState<
@ -76,7 +92,29 @@ export function ServicesTable() {
setItemIdToExpandedRowMap(expandedRowMapValues);
};
const { data, status } = useProgressiveFetcher(
const useOptimizedSorting =
useKibana().services.uiSettings?.get<boolean>(
apmServiceInventoryOptimizedSorting
) || false;
const sortedAndFilteredServicesFetch = useFetcher(
(callApmApi) => {
if (useOptimizedSorting) {
return callApmApi('GET /internal/apm/storage_explorer/get_services', {
params: {
query: {
environment,
kuery,
indexLifecyclePhase,
},
},
});
}
},
[environment, kuery, indexLifecyclePhase, useOptimizedSorting]
);
const serviceStatisticsFetch = useProgressiveFetcher(
(callApmApi) => {
return callApmApi('GET /internal/apm/storage_explorer', {
params: {
@ -93,165 +131,193 @@ export function ServicesTable() {
[indexLifecyclePhase, start, end, environment, kuery]
);
useEffect(() => {
// Closes any open rows when fetching new items
setItemIdToExpandedRowMap({});
}, [status]);
const serviceStatisticsItems =
serviceStatisticsFetch.data?.serviceStatistics ?? [];
const preloadedServices = sortedAndFilteredServicesFetch.data?.services || [];
const loading =
status === FETCH_STATUS.NOT_INITIATED || status === FETCH_STATUS.LOADING;
const initialSortField = useOptimizedSorting
? StorageExplorerFieldName.ServiceName
: StorageExplorerFieldName.Size;
const columns: Array<EuiBasicTableColumn<ValuesType<StorageExplorerItems>>> =
const initialSortDirection =
initialSortField === StorageExplorerFieldName.ServiceName ? 'asc' : 'desc';
const loading = serviceStatisticsFetch.status === FETCH_STATUS.LOADING;
const items = joinByKey(
[
{
field: 'serviceName',
name: i18n.translate(
'xpack.apm.storageExplorer.table.serviceColumnName',
{
defaultMessage: 'Service',
}
),
sortable: true,
render: (_, { serviceName, agentName }) => {
const serviceLinkQuery = {
comparisonEnabled,
environment,
kuery,
rangeFrom,
rangeTo,
serviceGroup: '',
};
...(initialSortField === StorageExplorerFieldName.ServiceName
? preloadedServices
: []),
...serviceStatisticsItems,
],
'serviceName'
);
return (
<TruncateWithTooltip
data-test-subj="apmStorageExplorerServiceLink"
text={serviceName || NOT_AVAILABLE_LABEL}
content={
<ServiceLink
query={serviceLinkQuery}
serviceName={serviceName}
agentName={agentName}
/>
}
/>
);
},
},
{
field: 'environment',
name: i18n.translate(
'xpack.apm.storageExplorer.table.environmentColumnName',
{
defaultMessage: 'Environment',
}
),
render: (_, { environments }) => (
<EnvironmentBadge environments={environments ?? []} />
),
sortable: true,
},
const columns: Array<EuiBasicTableColumn<StorageExplorerItem>> = [
{
field: 'serviceName',
name: i18n.translate(
'xpack.apm.storageExplorer.table.serviceColumnName',
{
defaultMessage: 'Service',
}
),
sortable: true,
render: (_, { serviceName, agentName }) => {
const serviceLinkQuery = {
comparisonEnabled,
environment,
kuery,
rangeFrom,
rangeTo,
serviceGroup: '',
};
{
field: 'sampling',
name: (
<EuiToolTip
content={i18n.translate(
'xpack.apm.storageExplorer.table.samplingColumnDescription',
{
defaultMessage: `The number of sampled transactions divided by total throughput. This value may differ from the configured transaction sample rate because it might be affected by the initial service's decision when using head-based sampling or by a set of policies when using tail-based sampling.`,
}
)}
>
<>
{i18n.translate(
'xpack.apm.storageExplorer.table.samplingColumnName',
{
defaultMessage: 'Sample rate',
}
)}{' '}
<EuiIcon
size="s"
color="subdued"
type="questionInCircle"
className="eui-alignTop"
return (
<TruncateWithTooltip
data-test-subj="apmStorageExplorerServiceLink"
text={serviceName || NOT_AVAILABLE_LABEL}
content={
<ServiceLink
query={serviceLinkQuery}
serviceName={serviceName}
agentName={agentName}
/>
</>
</EuiToolTip>
),
render: (value: string) => asPercent(parseFloat(value), 1),
sortable: true,
}
/>
);
},
{
field: 'size',
name: <SizeLabel />,
render: (_, { size }) => asDynamicBytes(size) || NOT_AVAILABLE_LABEL,
sortable: true,
},
{
align: RIGHT_ALIGNMENT,
width: '40px',
isExpander: true,
name: (
<EuiScreenReaderOnly>
<span>
{i18n.translate('xpack.apm.storageExplorer.table.expandRow', {
defaultMessage: 'Expand row',
})}
</span>
</EuiScreenReaderOnly>
),
render: ({ serviceName }: { serviceName: string }) => {
return (
<EuiButtonIcon
data-test-subj={`storageDetailsButton_${serviceName}`}
onClick={() => toggleRowDetails(serviceName)}
aria-label={
itemIdToExpandedRowMap[serviceName]
? i18n.translate('xpack.apm.storageExplorer.table.collapse', {
defaultMessage: 'Collapse',
})
: i18n.translate('xpack.apm.storageExplorer.table.expand', {
defaultMessage: 'Expand',
})
}
iconType={
itemIdToExpandedRowMap[serviceName] ? 'arrowUp' : 'arrowDown'
},
{
field: 'environment',
name: i18n.translate(
'xpack.apm.storageExplorer.table.environmentColumnName',
{
defaultMessage: 'Environment',
}
),
render: (_, { environments }) => (
<EnvironmentBadge environments={environments ?? []} />
),
sortable: true,
},
{
field: 'sampling',
name: (
<EuiToolTip
content={i18n.translate(
'xpack.apm.storageExplorer.table.samplingColumnDescription',
{
defaultMessage: `The number of sampled transactions divided by total throughput. This value may differ from the configured transaction sample rate because it might be affected by the initial service's decision when using head-based sampling or by a set of policies when using tail-based sampling.`,
}
)}
>
<>
{i18n.translate(
'xpack.apm.storageExplorer.table.samplingColumnName',
{
defaultMessage: 'Sample rate',
}
)}{' '}
<EuiIcon
size="s"
color="subdued"
type="questionInCircle"
className="eui-alignTop"
/>
);
},
</>
</EuiToolTip>
),
render: (value: string) => asPercent(parseFloat(value), 1),
sortable: true,
},
{
field: 'size',
name: <SizeLabel />,
render: (_, { size }) => asDynamicBytes(size) || NOT_AVAILABLE_LABEL,
sortable: true,
},
{
align: RIGHT_ALIGNMENT,
width: '40px',
isExpander: true,
name: (
<EuiScreenReaderOnly>
<span>
{i18n.translate('xpack.apm.storageExplorer.table.expandRow', {
defaultMessage: 'Expand row',
})}
</span>
</EuiScreenReaderOnly>
),
render: ({ serviceName }: { serviceName: string }) => {
return (
<EuiButtonIcon
data-test-subj={`storageDetailsButton_${serviceName}`}
onClick={() => toggleRowDetails(serviceName)}
aria-label={
itemIdToExpandedRowMap[serviceName]
? i18n.translate('xpack.apm.storageExplorer.table.collapse', {
defaultMessage: 'Collapse',
})
: i18n.translate('xpack.apm.storageExplorer.table.expand', {
defaultMessage: 'Expand',
})
}
iconType={
itemIdToExpandedRowMap[serviceName] ? 'arrowUp' : 'arrowDown'
}
/>
);
},
];
},
];
return (
<EuiInMemoryTable
tableCaption={i18n.translate('xpack.apm.storageExplorer.table.caption', {
defaultMessage: 'Storage explorer',
})}
items={data?.serviceStatistics ?? []}
columns={columns}
pagination={true}
sorting={true}
itemId="serviceName"
itemIdToExpandedRowMap={itemIdToExpandedRowMap}
loading={loading}
data-test-subj="storageExplorerServicesTable"
error={
status === FETCH_STATUS.FAILURE
? i18n.translate('xpack.apm.storageExplorer.table.errorMessage', {
defaultMessage: 'Failed to fetch',
})
: ''
}
message={
loading
? i18n.translate('xpack.apm.storageExplorer.table.loading', {
defaultMessage: 'Loading...',
})
: i18n.translate('xpack.apm.storageExplorer.table.noResults', {
defaultMessage: 'No data found',
})
}
/>
<EuiPanel
hasShadow={false}
paddingSize="none"
style={{ position: 'relative' }}
>
{loading && <EuiProgress size="xs" color="accent" position="absolute" />}
<EuiInMemoryTable
tableCaption={i18n.translate(
'xpack.apm.storageExplorer.table.caption',
{
defaultMessage: 'Storage explorer',
}
)}
items={items ?? []}
columns={columns}
pagination={true}
sorting={{
sort: {
field: initialSortField,
direction: initialSortDirection,
},
}}
itemId="serviceName"
itemIdToExpandedRowMap={itemIdToExpandedRowMap}
data-test-subj="storageExplorerServicesTable"
error={
status === FETCH_STATUS.FAILURE
? i18n.translate('xpack.apm.storageExplorer.table.errorMessage', {
defaultMessage: 'Failed to fetch',
})
: ''
}
message={
loading
? i18n.translate('xpack.apm.storageExplorer.table.loading', {
defaultMessage: 'Loading...',
})
: i18n.translate('xpack.apm.storageExplorer.table.noResults', {
defaultMessage: 'No data found',
})
}
/>
</EuiPanel>
);
}

View file

@ -0,0 +1,159 @@
/*
* 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 React from 'react';
import {
EuiInMemoryTable,
EuiBasicTableColumn,
EuiPanel,
EuiTitle,
EuiSpacer,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { ValuesType } from 'utility-types';
import { NOT_AVAILABLE_LABEL } from '../../../../../common/i18n';
import {
asDynamicBytes,
asInteger,
} from '../../../../../common/utils/formatters';
import { FETCH_STATUS } from '../../../../hooks/use_fetcher';
import type { APIReturnType } from '../../../../services/rest/create_call_apm_api';
import { SizeLabel } from './size_label';
type StorageExplorerIndicesStats =
APIReturnType<'GET /internal/apm/services/{serviceName}/storage_details'>['indicesStats'];
interface Props {
indicesStats: StorageExplorerIndicesStats;
status: FETCH_STATUS;
}
export function IndexStatsPerService({ indicesStats, status }: Props) {
const columns: Array<
EuiBasicTableColumn<ValuesType<StorageExplorerIndicesStats>>
> = [
{
field: 'indexName',
name: i18n.translate('xpack.apm.storageExplorer.indicesStats.indexName', {
defaultMessage: 'Name',
}),
sortable: true,
},
{
field: 'primary',
name: i18n.translate('xpack.apm.storageExplorer.indicesStats.primaries', {
defaultMessage: 'Primaries',
}),
render: (_, { primary }) => primary ?? NOT_AVAILABLE_LABEL,
sortable: true,
},
{
field: 'replica',
name: i18n.translate('xpack.apm.storageExplorer.indicesStats.replicas', {
defaultMessage: 'Replicas',
}),
render: (_, { replica }) => replica ?? NOT_AVAILABLE_LABEL,
sortable: true,
},
{
field: 'numberOfDocs',
name: i18n.translate(
'xpack.apm.storageExplorer.indicesStats.numberOfDocs',
{
defaultMessage: 'Docs count',
}
),
render: (_, { numberOfDocs }) => asInteger(numberOfDocs),
sortable: true,
},
{
field: 'size',
name: <SizeLabel />,
render: (_, { size }) => asDynamicBytes(size) ?? NOT_AVAILABLE_LABEL,
sortable: true,
},
{
field: 'dataStream',
name: i18n.translate(
'xpack.apm.storageExplorer.indicesStats.dataStream',
{
defaultMessage: 'Data stream',
}
),
render: (_, { dataStream }) => dataStream ?? NOT_AVAILABLE_LABEL,
sortable: true,
},
{
field: 'lifecyclePhase',
name: i18n.translate(
'xpack.apm.storageExplorer.indicesStats.lifecyclePhase',
{
defaultMessage: 'Lifecycle phase',
}
),
render: (_, { lifecyclePhase }) => lifecyclePhase ?? NOT_AVAILABLE_LABEL,
sortable: true,
},
];
const loading =
status === FETCH_STATUS.NOT_INITIATED || status === FETCH_STATUS.LOADING;
return (
<>
<EuiTitle size="xs">
<h5>
{i18n.translate('xpack.apm.storageExplorer.indicesStats.title', {
defaultMessage: 'Indices breakdown',
})}
</h5>
</EuiTitle>
<EuiSpacer />
<EuiPanel>
<EuiInMemoryTable
tableCaption={i18n.translate(
'xpack.apm.storageExplorer.indicesStats.table.caption',
{
defaultMessage: 'Storage explorer indices breakdown',
}
)}
items={indicesStats}
columns={columns}
pagination={true}
sorting={true}
loading={loading}
data-test-subj="storageExplorerIndicesStatsTable"
error={
status === FETCH_STATUS.FAILURE
? i18n.translate(
'xpack.apm.storageExplorer.indicesStats.table.errorMessage',
{
defaultMessage: 'Failed to fetch',
}
)
: ''
}
message={
loading
? i18n.translate(
'xpack.apm.storageExplorer.indicesStats.table.loading',
{
defaultMessage: 'Loading...',
}
)
: i18n.translate(
'xpack.apm.storageExplorer.indicesStats.table.noResults',
{
defaultMessage: 'No data found',
}
)
}
/>
</EuiPanel>
</>
);
}

View file

@ -41,6 +41,7 @@ import { asDynamicBytes } from '../../../../../common/utils/formatters';
import { getComparisonEnabled } from '../../../shared/time_comparison/get_comparison_enabled';
import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plugin_context';
import { SizeLabel } from './size_label';
import { IndexStatsPerService } from './index_stats_per_service';
interface Props {
serviceName: string;
@ -155,7 +156,7 @@ export function StorageDetailsPerService({
return (
<>
<EuiFlexGroup direction="column" responsive={false} gutterSize="m">
<EuiFlexGroup direction="column" responsive={false} gutterSize="l">
<EuiFlexItem>
<EuiFlexGroup justifyContent="spaceBetween">
<EuiFlexItem>
@ -265,6 +266,12 @@ export function StorageDetailsPerService({
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexItem>
<IndexStatsPerService
indicesStats={data.indicesStats}
status={status}
/>
</EuiFlexItem>
</EuiFlexGroup>
</>
);

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import React from 'react';
import { euiPaletteColorBlind, EuiPanel } from '@elastic/eui';
import { euiPaletteColorBlind } from '@elastic/eui';
import {
AreaSeries,
Axis,
@ -83,56 +83,54 @@ export function StorageChart() {
const isEmpty = isTimeseriesEmpty(storageTimeSeries);
return (
<EuiPanel hasShadow={false} hasBorder={true}>
<ChartContainer
hasData={!isEmpty}
height={400}
status={status}
id="storageExplorerTimeseriesChart"
>
<Chart id="storageExplorerTimeseriesChart">
<Settings
theme={[
{
areaSeriesStyle: {
line: { visible: false },
area: { opacity: 1 },
},
<ChartContainer
hasData={!isEmpty}
height={400}
status={status}
id="storageExplorerTimeseriesChart"
>
<Chart id="storageExplorerTimeseriesChart">
<Settings
theme={[
{
areaSeriesStyle: {
line: { visible: false },
area: { opacity: 1 },
},
...chartTheme,
]}
showLegend
legendPosition={Position.Right}
},
...chartTheme,
]}
showLegend
legendPosition={Position.Right}
/>
<Axis
id="x-axis"
position={Position.Bottom}
showOverlappingTicks
tickFormat={xFormatter}
gridLine={{ visible: false }}
/>
<Axis
id="y-axis"
position={Position.Left}
showGridLines
tickFormat={asDynamicBytes}
/>
{storageTimeSeries.map((serie) => (
<AreaSeries
timeZone={timeZone}
key={serie.title}
id={serie.title}
xScaleType={ScaleType.Time}
yScaleType={ScaleType.Linear}
xAccessor="x"
yAccessors={['y']}
data={isEmpty ? [] : serie.data}
color={serie.color}
stackAccessors={['x']}
/>
<Axis
id="x-axis"
position={Position.Bottom}
showOverlappingTicks
tickFormat={xFormatter}
gridLine={{ visible: false }}
/>
<Axis
id="y-axis"
position={Position.Left}
showGridLines
tickFormat={asDynamicBytes}
/>
{storageTimeSeries.map((serie) => (
<AreaSeries
timeZone={timeZone}
key={serie.title}
id={serie.title}
xScaleType={ScaleType.Time}
yScaleType={ScaleType.Linear}
xAccessor="x"
yAccessors={['y']}
data={isEmpty ? [] : serie.data}
color={serie.color}
stackAccessors={['x']}
/>
))}
</Chart>
</ChartContainer>
</EuiPanel>
))}
</Chart>
</ChartContainer>
);
}

View file

@ -14,30 +14,28 @@ import {
EuiText,
useEuiFontSize,
EuiLink,
EuiLoadingSpinner,
EuiToolTip,
EuiIcon,
EuiProgress,
EuiLoadingContent,
EuiSpacer,
} from '@elastic/eui';
import { useEuiTheme } from '@elastic/eui';
import { css } from '@emotion/react';
import { isEmpty } from 'lodash';
import { useProgressiveFetcher } from '../../../hooks/use_progressive_fetcher';
import { useTimeRange } from '../../../hooks/use_time_range';
import { useApmParams } from '../../../hooks/use_apm_params';
import { asDynamicBytes } from '../../../../common/utils/formatters';
import { asDynamicBytes, asPercent } from '../../../../common/utils/formatters';
import { useApmRouter } from '../../../hooks/use_apm_router';
import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context';
import { FETCH_STATUS } from '../../../hooks/use_fetcher';
import { asTransactionRate } from '../../../../common/utils/formatters';
const INITIAL_DATA = {
estimatedSize: 0,
dailyDataGeneration: 0,
tracesPerMinute: 0,
numberOfServices: 0,
};
import { getIndexManagementHref } from './get_storage_explorer_links';
export function SummaryStats() {
const router = useApmRouter();
const { core } = useApmPluginContext();
const { euiTheme } = useEuiTheme();
const {
query: {
@ -63,7 +61,7 @@ export function SummaryStats() {
},
});
const { data = INITIAL_DATA, status } = useProgressiveFetcher(
const { data, status } = useProgressiveFetcher(
(callApmApi) => {
return callApmApi('GET /internal/apm/storage_explorer_summary_stats', {
params: {
@ -80,92 +78,136 @@ export function SummaryStats() {
[indexLifecyclePhase, environment, kuery, start, end]
);
const loading = status === FETCH_STATUS.LOADING;
const loading =
status === FETCH_STATUS.LOADING || status === FETCH_STATUS.NOT_INITIATED;
const hasData = !isEmpty(data);
return (
<EuiPanel hasBorder={true} hasShadow={false} paddingSize="l">
{loading && (
<EuiText textAlign="center">
<EuiLoadingSpinner size="l" />
</EuiText>
)}
{!loading && (
<EuiFlexGroup justifyContent="spaceBetween">
<EuiFlexItem>
<EuiFlexGroup gutterSize="xl">
<SummaryMetric
label={i18n.translate(
'xpack.apm.storageExplorer.summary.totalSize',
{
defaultMessage: 'Total APM size',
}
)}
value={asDynamicBytes(data?.estimatedSize)}
color={euiTheme.colors.primary}
/>
<SummaryMetric
label={i18n.translate(
'xpack.apm.storageExplorer.summary.dailyDataGeneration',
{
defaultMessage: 'Daily data generation',
}
)}
value={asDynamicBytes(data?.dailyDataGeneration)}
color={euiTheme.colors.danger}
/>
<SummaryMetric
label={i18n.translate(
'xpack.apm.storageExplorer.summary.tracesPerMinute',
{
defaultMessage: 'Traces per minute',
}
)}
value={asTransactionRate(data?.tracesPerMinute)}
color={euiTheme.colors.accent}
/>
<SummaryMetric
label={i18n.translate(
'xpack.apm.storageExplorer.summary.numberOfServices',
{
defaultMessage: 'Number of services',
}
)}
value={data?.numberOfServices.toString()}
color={euiTheme.colors.success}
/>
</EuiFlexGroup>
</EuiFlexItem>
<EuiPanel
hasBorder={true}
hasShadow={false}
paddingSize="l"
style={{ position: 'relative' }}
>
{loading && <EuiProgress size="xs" color="accent" position="absolute" />}
<EuiFlexGroup justifyContent="spaceBetween">
<EuiFlexItem>
<EuiFlexGroup gutterSize="xl">
<SummaryMetric
label={i18n.translate(
'xpack.apm.storageExplorer.summary.totalSize',
{
defaultMessage: 'Total APM size',
}
)}
tooltipContent={i18n.translate(
'xpack.apm.storageExplorer.summary.totalSize.tooltip',
{
defaultMessage: 'The storage size used by the APM indices.',
}
)}
value={asDynamicBytes(data?.totalSize)}
loading={loading}
hasData={hasData}
/>
<SummaryMetric
label={i18n.translate(
'xpack.apm.storageExplorer.summary.diskSpaceUsedPct',
{
defaultMessage: 'Disk space used',
}
)}
tooltipContent={i18n.translate(
'xpack.apm.storageExplorer.summary.diskSpaceUsedPct.tooltip',
{
defaultMessage:
'The percentage of storage size used by the APM indices compared to the overall storage configured for Elasticsearch.',
}
)}
value={asPercent(data?.diskSpaceUsedPct, 1)}
loading={loading}
hasData={hasData}
/>
<SummaryMetric
label={i18n.translate(
'xpack.apm.storageExplorer.summary.incrementalSize',
{
defaultMessage: 'Incremental APM size',
}
)}
tooltipContent={i18n.translate(
'xpack.apm.storageExplorer.summary.incrementalSize.tooltip',
{
defaultMessage:
'The estimated storage size used by the APM indices based on the filters selected.',
}
)}
value={asDynamicBytes(data?.estimatedIncrementalSize)}
loading={loading}
hasData={hasData}
/>
<SummaryMetric
label={i18n.translate(
'xpack.apm.storageExplorer.summary.dailyDataGeneration',
{
defaultMessage: 'Daily data generation',
}
)}
value={asDynamicBytes(data?.dailyDataGeneration)}
loading={loading}
hasData={hasData}
/>
<SummaryMetric
label={i18n.translate(
'xpack.apm.storageExplorer.summary.tracesPerMinute',
{
defaultMessage: 'Traces per minute',
}
)}
value={asTransactionRate(data?.tracesPerMinute)}
loading={loading}
hasData={hasData}
/>
<SummaryMetric
label={i18n.translate(
'xpack.apm.storageExplorer.summary.numberOfServices',
{
defaultMessage: 'Number of services',
}
)}
value={(data?.numberOfServices ?? 0).toString()}
loading={loading}
hasData={hasData}
/>
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiFlexGroup direction="column" justifyContent="spaceBetween">
<EuiFlexItem>
<EuiLink href={serviceInventoryLink}>
{i18n.translate(
'xpack.apm.storageExplorer.summary.serviceInventoryLink',
{
defaultMessage: 'Go to Service Inventory',
}
)}
</EuiLink>
</EuiFlexItem>
<EuiFlexItem>
<EuiLink
href={core.http.basePath.prepend(
'/app/management/data/index_management/data_streams'
)}
>
{i18n.translate(
'xpack.apm.storageExplorer.summary.indexManagementLink',
{
defaultMessage: 'Go to Index Management',
}
)}
</EuiLink>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
)}
<EuiFlexItem grow={false}>
<EuiFlexGroup direction="column" justifyContent="spaceBetween">
<EuiFlexItem>
<EuiLink href={serviceInventoryLink}>
{i18n.translate(
'xpack.apm.storageExplorer.summary.serviceInventoryLink',
{
defaultMessage: 'Go to Service Inventory',
}
)}
</EuiLink>
</EuiFlexItem>
<EuiFlexItem>
<EuiLink href={getIndexManagementHref(core)}>
{i18n.translate(
'xpack.apm.storageExplorer.summary.indexManagementLink',
{
defaultMessage: 'Go to Index Management',
}
)}
</EuiLink>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
</EuiPanel>
);
}
@ -173,29 +215,55 @@ export function SummaryStats() {
function SummaryMetric({
label,
value,
color,
tooltipContent,
loading,
hasData,
}: {
label: string;
value: string;
color: string;
tooltipContent?: string;
loading: boolean;
hasData: boolean;
}) {
const xxlFontSize = useEuiFontSize('xxl', { measurement: 'px' });
const xlFontSize = useEuiFontSize('xl', { measurement: 'px' });
const { euiTheme } = useEuiTheme();
return (
<EuiFlexItem grow={false}>
<EuiText size="s" color="subdued">
{label}
</EuiText>
<EuiText
css={css`
${xxlFontSize}
font-weight: ${euiTheme.font.weight.bold};
color: ${color};
`}
>
{value}
</EuiText>
{tooltipContent ? (
<EuiToolTip content={tooltipContent}>
<EuiText size="s" color="subdued">
{label}{' '}
<EuiIcon
size="s"
color="subdued"
type="questionInCircle"
className="eui-alignTop"
/>
</EuiText>
</EuiToolTip>
) : (
<EuiText size="s" color="subdued">
{label}
</EuiText>
)}
{loading && !hasData && (
<>
<EuiSpacer size="s" />
<EuiLoadingContent lines={2} />
</>
)}
{hasData && (
<EuiText
css={css`
${xlFontSize}
font-weight: ${euiTheme.font.weight.bold};
color: ${euiTheme.colors.text};
`}
>
{value}
</EuiText>
)}
</EuiFlexItem>
);
}

View file

@ -6,18 +6,18 @@
*/
import React from 'react';
import { EuiTitle, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { EuiTitle } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import * as t from 'io-ts';
import { EuiLink } from '@elastic/eui';
import { StorageExplorer } from '../../app/storage_explorer';
import { BetaBadge } from '../../shared/beta_badge';
import { ApmMainTemplate } from '../templates/apm_main_template';
import { Breadcrumb } from '../../app/breadcrumb';
import {
indexLifecyclePhaseRt,
IndexLifecyclePhaseSelectOption,
} from '../../../../common/storage_explorer_types';
import { getStorageExplorerFeedbackHref } from '../../app/storage_explorer/get_storage_explorer_links';
export const storageExplorer = {
'/storage-explorer': {
@ -32,30 +32,16 @@ export const storageExplorer = {
pageHeader={{
alignItems: 'center',
pageTitle: (
<EuiFlexGroup
justifyContent="flexStart"
gutterSize="s"
alignItems="baseline"
>
<EuiFlexItem grow={false}>
<EuiTitle size="l">
<h2>
{i18n.translate('xpack.apm.views.storageExplorer.title', {
defaultMessage: 'Storage explorer',
})}
</h2>
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<BetaBadge />
</EuiFlexItem>
</EuiFlexGroup>
<EuiTitle size="l">
<h1>
{i18n.translate('xpack.apm.views.storageExplorer.title', {
defaultMessage: 'Storage explorer',
})}
</h1>
</EuiTitle>
),
rightSideItems: [
<EuiLink
href="https://ela.st/feedback-storage-explorer"
target="_blank"
>
<EuiLink href={getStorageExplorerFeedbackHref()} target="_blank">
{i18n.translate(
'xpack.apm.views.storageExplorer.giveFeedback',
{

View file

@ -7,6 +7,7 @@
import { Logger } from '@kbn/logging';
import { ProcessorEvent } from '@kbn/observability-plugin/common';
import { APMEventClient } from '../../../lib/helpers/create_es_client/create_apm_event_client';
import { SERVICE_NAME } from '../../../../common/elasticsearch_fieldnames';
import { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values';
import { Environment } from '../../../../common/environment_rt';
@ -15,7 +16,39 @@ import { ServiceGroup } from '../../../../common/service_groups';
import { Setup } from '../../../lib/helpers/setup_request';
import { getHealthStatuses } from './get_health_statuses';
import { lookupServices } from '../../service_groups/lookup_services';
import { APMEventClient } from '../../../lib/helpers/create_es_client/create_apm_event_client';
export async function getServiceNamesFromTermsEnum({
apmEventClient,
environment,
maxNumberOfServices,
}: {
apmEventClient: APMEventClient;
environment: Environment;
maxNumberOfServices: number;
}) {
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: maxNumberOfServices,
field: SERVICE_NAME,
},
}
);
return response.terms;
}
export async function getSortedAndFilteredServices({
setup,
@ -36,31 +69,6 @@ export async function getSortedAndFilteredServices({
serviceGroup: ServiceGroup | null;
maxNumberOfServices: number;
}) {
async function getServiceNamesFromTermsEnum() {
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: maxNumberOfServices,
field: SERVICE_NAME,
},
}
);
return response.terms;
}
const [servicesWithHealthStatuses, selectedServices] = await Promise.all([
getHealthStatuses({
setup,
@ -79,7 +87,11 @@ export async function getSortedAndFilteredServices({
maxNumberOfServices,
serviceGroup,
})
: getServiceNamesFromTermsEnum(),
: getServiceNamesFromTermsEnum({
apmEventClient,
environment,
maxNumberOfServices,
}),
]);
const services = joinByKey(

View file

@ -4,7 +4,6 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import {
termQuery,
kqlQuery,
@ -83,7 +82,7 @@ async function getMainServiceStatistics({
indexLifeCyclePhaseToDataTier[indexLifecyclePhase]
)
: []),
] as QueryDslQueryContainer[],
],
},
},
aggs: {

View file

@ -5,7 +5,6 @@
* 2.0.
*/
import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import {
termQuery,
kqlQuery,
@ -28,6 +27,8 @@ import { ApmPluginRequestHandlerContext } from '../typings';
import {
getTotalIndicesStats,
getEstimatedSizeForDocumentsInIndex,
getIndicesLifecycleStatus,
getIndicesInfo,
} from './indices_stats_helpers';
import { RandomSampler } from '../../lib/helpers/get_random_sampler';
import { APMEventClient } from '../../lib/helpers/create_es_client/create_apm_event_client';
@ -57,7 +58,7 @@ export async function getStorageDetailsPerProcessorEvent({
}) {
const [{ indices: allIndicesStats }, response] = await Promise.all([
getTotalIndicesStats({ setup, context }),
apmEventClient.search('get_storage_details_per_processor_event', {
apmEventClient.search('get_storage_details_per_service', {
apm: {
events: [
ProcessorEvent.span,
@ -82,7 +83,7 @@ export async function getStorageDetailsPerProcessorEvent({
indexLifeCyclePhaseToDataTier[indexLifecyclePhase]
)
: []),
] as QueryDslQueryContainer[],
],
},
},
aggs: {
@ -154,3 +155,122 @@ export async function getStorageDetailsPerProcessorEvent({
};
});
}
export async function getStorageDetailsPerIndex({
apmEventClient,
setup,
context,
indexLifecyclePhase,
randomSampler,
start,
end,
environment,
kuery,
serviceName,
}: {
apmEventClient: APMEventClient;
setup: Setup;
context: ApmPluginRequestHandlerContext;
indexLifecyclePhase: IndexLifecyclePhaseSelectOption;
randomSampler: RandomSampler;
start: number;
end: number;
environment: string;
kuery: string;
serviceName: string;
}) {
const [
{ indices: allIndicesStats },
indicesLifecycleStatus,
indicesInfo,
response,
] = await Promise.all([
getTotalIndicesStats({ setup, context }),
getIndicesLifecycleStatus({ setup, context }),
getIndicesInfo({ setup, context }),
apmEventClient.search('get_storage_details_per_index', {
apm: {
events: [
ProcessorEvent.span,
ProcessorEvent.transaction,
ProcessorEvent.error,
ProcessorEvent.metric,
],
},
body: {
size: 0,
track_total_hits: false,
query: {
bool: {
filter: [
...environmentQuery(environment),
...kqlQuery(kuery),
...rangeQuery(start, end),
...termQuery(SERVICE_NAME, serviceName),
...(indexLifecyclePhase !== IndexLifecyclePhaseSelectOption.All
? termQuery(
TIER,
indexLifeCyclePhaseToDataTier[indexLifecyclePhase]
)
: []),
],
},
},
aggs: {
sample: {
random_sampler: randomSampler,
aggs: {
indices: {
terms: {
field: INDEX,
size: 500,
},
aggs: {
number_of_metric_docs_for_index: {
value_count: {
field: INDEX,
},
},
},
},
},
},
},
},
}),
]);
return (
response.aggregations?.sample.indices.buckets.map((bucket) => {
const indexName = bucket.key as string;
const numberOfDocs = bucket.number_of_metric_docs_for_index.value;
const indexInfo = indicesInfo[indexName];
const indexLifecycle = indicesLifecycleStatus[indexName];
const size =
allIndicesStats &&
getEstimatedSizeForDocumentsInIndex({
allIndicesStats,
indexName,
numberOfDocs,
});
return {
indexName,
numberOfDocs,
primary: indexInfo
? indexInfo.settings?.index?.number_of_shards ?? 0
: undefined,
replica: indexInfo
? indexInfo.settings?.number_of_replicas ?? 0
: undefined,
size,
dataStream: indexInfo?.data_stream,
lifecyclePhase:
indexLifecycle && 'phase' in indexLifecycle
? indexLifecycle.phase
: undefined,
};
}) ?? []
);
}

View file

@ -6,7 +6,6 @@
*/
import { ProcessorEvent } from '@kbn/observability-plugin/common';
import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import {
termQuery,
kqlQuery,
@ -15,6 +14,7 @@ import {
import {
getTotalIndicesStats,
getEstimatedSizeForDocumentsInIndex,
getApmDiskSpacedUsedPct,
} from './indices_stats_helpers';
import { Setup } from '../../lib/helpers/setup_request';
import { ApmPluginRequestHandlerContext } from '../typings';
@ -121,8 +121,9 @@ export async function getMainSummaryStats({
environment: string;
kuery: string;
}) {
const [{ indices: allIndicesStats }, res] = await Promise.all([
const [totalIndicesStats, totalDiskSpace, res] = await Promise.all([
getTotalIndicesStats({ context, setup }),
getApmDiskSpacedUsedPct(context),
apmEventClient.search('get_storage_explorer_main_summary_stats', {
apm: {
events: [
@ -147,7 +148,7 @@ export async function getMainSummaryStats({
indexLifeCyclePhaseToDataTier[indexLifecyclePhase]
)
: []),
] as QueryDslQueryContainer[],
],
},
},
aggs: {
@ -179,7 +180,8 @@ export async function getMainSummaryStats({
}),
]);
const estimatedSize = allIndicesStats
const { indices: allIndicesStats } = totalIndicesStats;
const estimatedIncrementalSize = allIndicesStats
? res.aggregations?.sample.indices.buckets.reduce((prev, curr) => {
return (
prev +
@ -193,10 +195,13 @@ export async function getMainSummaryStats({
: 0;
const durationAsDays = (end - start) / 1000 / 60 / 60 / 24;
const totalApmSize = totalIndicesStats._all.total?.store?.size_in_bytes ?? 0;
return {
totalSize: totalApmSize,
diskSpaceUsedPct: totalApmSize / totalDiskSpace,
numberOfServices: res.aggregations?.services_count.value ?? 0,
estimatedSize,
dailyDataGeneration: estimatedSize / durationAsDays,
estimatedIncrementalSize,
dailyDataGeneration: estimatedIncrementalSize / durationAsDays,
};
}

View file

@ -4,7 +4,6 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import {
termQuery,
kqlQuery,
@ -68,7 +67,7 @@ export async function getTotalTransactionsPerService({
indexLifeCyclePhaseToDataTier[indexLifecyclePhase]
)
: []),
] as QueryDslQueryContainer[],
],
},
},
aggs: {

View file

@ -28,17 +28,19 @@ export async function hasStorageExplorerPrivileges({
);
const esClient = (await context.core).elasticsearch.client;
const { index } = await esClient.asCurrentUser.security.hasPrivileges({
body: {
index: [
{
names,
privileges: ['monitor'],
},
],
},
});
const { index, cluster } =
await esClient.asCurrentUser.security.hasPrivileges({
body: {
index: [
{
names,
privileges: ['monitor'],
},
],
cluster: ['monitor'],
},
});
const hasPrivileges = every(index, 'monitor');
const hasPrivileges = cluster.monitor && every(index, 'monitor');
return hasPrivileges;
}

View file

@ -4,7 +4,7 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { uniq } from 'lodash';
import { uniq, values, sumBy } from 'lodash';
import { IndicesStatsIndicesStats } from '@elastic/elasticsearch/lib/api/types';
import { Setup } from '../../lib/helpers/setup_request';
import { ApmPluginRequestHandlerContext } from '../typings';
@ -16,10 +16,7 @@ export async function getTotalIndicesStats({
context: ApmPluginRequestHandlerContext;
setup: Setup;
}) {
const {
indices: { transaction, span, metric, error },
} = setup;
const index = uniq([transaction, span, metric, error]).join();
const index = getApmIndicesCombined(setup);
const esClient = (await context.core).elasticsearch.client;
const totalStats = await esClient.asCurrentUser.indices.stats({ index });
return totalStats;
@ -44,3 +41,67 @@ export function getEstimatedSizeForDocumentsInIndex({
return estimatedSize;
}
export async function getApmDiskSpacedUsedPct(
context: ApmPluginRequestHandlerContext
) {
const esClient = (await context.core).elasticsearch.client;
const { nodes: diskSpacePerNode } = await esClient.asCurrentUser.nodes.stats({
metric: 'fs',
filter_path: 'nodes.*.fs.total.total_in_bytes',
});
const totalDiskSpace = sumBy(
values(diskSpacePerNode),
(node) => node?.fs?.total?.total_in_bytes ?? 0
);
return totalDiskSpace;
}
export async function getIndicesLifecycleStatus({
context,
setup,
}: {
context: ApmPluginRequestHandlerContext;
setup: Setup;
}) {
const index = getApmIndicesCombined(setup);
const esClient = (await context.core).elasticsearch.client;
const { indices } = await esClient.asCurrentUser.ilm.explainLifecycle({
index,
filter_path: 'indices.*.phase',
});
return indices;
}
export async function getIndicesInfo({
context,
setup,
}: {
context: ApmPluginRequestHandlerContext;
setup: Setup;
}) {
const index = getApmIndicesCombined(setup);
const esClient = (await context.core).elasticsearch.client;
const indicesInfo = await esClient.asCurrentUser.indices.get({
index,
filter_path: [
'*.settings.index.number_of_shards',
'*.settings.index.number_of_replicas',
'*.data_stream',
],
features: ['settings'],
});
return indicesInfo;
}
export function getApmIndicesCombined(setup: Setup) {
const {
indices: { transaction, span, metric, error },
} = setup;
return uniq([transaction, span, metric, error]).join();
}

View file

@ -0,0 +1,105 @@
/*
* 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 { Setup } from '../../lib/helpers/setup_request';
import { isCrossClusterSearch } from './is_cross_cluster_search';
import { ApmIndicesConfig } from '@kbn/observability-plugin/common/typings';
describe('isCrossClusterSearch', () => {
it('returns false when there are no remote clusters in APM indices', () => {
const mockedSetup = {
indices: {
transaction: 'traces-apm*',
span: 'traces-apm*',
metric: 'metrics-apm*',
error: 'logs-apm*',
} as ApmIndicesConfig,
} as unknown as Setup;
expect(isCrossClusterSearch(mockedSetup)).toBe(false);
});
it('returns false when there are multiple indices per type and no remote clusters in APM indices', () => {
const mockedSetup = {
indices: {
transaction: 'traces-apm*,test-apm*',
span: 'traces-apm*,test-apm*',
metric: 'metrics-apm*,test-apm*',
error: 'logs-apm*,test-apm*',
} as ApmIndicesConfig,
} as unknown as Setup;
expect(isCrossClusterSearch(mockedSetup)).toBe(false);
});
it('returns false when there are remote clusters in onboarding and sourcemap indices', () => {
const mockedSetup = {
indices: {
transaction: '',
span: '',
metric: '',
error: '',
onboarding: 'apm-*,remote_cluster:apm-*',
sourcemap: 'apm-*,remote_cluster:apm-*',
} as ApmIndicesConfig,
} as unknown as Setup;
expect(isCrossClusterSearch(mockedSetup)).toBe(false);
});
it('returns true when there are remote clusters in transaction indices', () => {
const mockedSetup = {
indices: {
transaction: 'traces-apm*,remote_cluster:traces-apm*',
span: '',
metric: '',
error: '',
} as ApmIndicesConfig,
} as unknown as Setup;
expect(isCrossClusterSearch(mockedSetup)).toBe(true);
});
it('returns true when there are remote clusters in span indices', () => {
const mockedSetup = {
indices: {
transaction: '',
span: 'traces-apm*,remote_cluster:traces-apm*',
metric: '',
error: '',
} as ApmIndicesConfig,
} as unknown as Setup;
expect(isCrossClusterSearch(mockedSetup)).toBe(true);
});
it('returns true when there are remote clusters in metrics indices', () => {
const mockedSetup = {
indices: {
transaction: '',
span: '',
metric: 'metrics-apm*,remote_cluster:metrics-apm*',
error: '',
} as ApmIndicesConfig,
} as unknown as Setup;
expect(isCrossClusterSearch(mockedSetup)).toBe(true);
});
it('returns true when there are remote clusters in error indices', () => {
const mockedSetup = {
indices: {
transaction: '',
span: '',
metric: '',
error: 'logs-apm*,remote_cluster:logs-apm*',
} as ApmIndicesConfig,
} as unknown as Setup;
expect(isCrossClusterSearch(mockedSetup)).toBe(true);
});
});

View file

@ -0,0 +1,14 @@
/*
* 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 { Setup } from '../../lib/helpers/setup_request';
import { getApmIndicesCombined } from './indices_stats_helpers';
export function isCrossClusterSearch(setup: Setup) {
// Check if a remote cluster is set in APM indices
return getApmIndicesCombined(setup).includes(':');
}

View file

@ -9,10 +9,14 @@ import * as t from 'io-ts';
import { ProcessorEvent } from '@kbn/observability-plugin/common';
import Boom from '@hapi/boom';
import { i18n } from '@kbn/i18n';
import { ENVIRONMENT_ALL } from '../../../common/environment_filter_values';
import { createApmServerRoute } from '../apm_routes/create_apm_server_route';
import { getSearchTransactionsEvents } from '../../lib/helpers/transactions';
import { setupRequest } from '../../lib/helpers/setup_request';
import { indexLifecyclePhaseRt } from '../../../common/storage_explorer_types';
import {
indexLifecyclePhaseRt,
IndexLifecyclePhaseSelectOption,
} from '../../../common/storage_explorer_types';
import { getServiceStatistics } from './get_service_statistics';
import {
probabilityRt,
@ -21,7 +25,10 @@ import {
rangeRt,
} from '../default_api_types';
import { AgentName } from '../../../typings/es_schemas/ui/fields/agent';
import { getStorageDetailsPerProcessorEvent } from './get_storage_details_per_processor_event';
import {
getStorageDetailsPerIndex,
getStorageDetailsPerProcessorEvent,
} from './get_storage_details_per_service';
import { getRandomSampler } from '../../lib/helpers/get_random_sampler';
import { getSizeTimeseries } from './get_size_timeseries';
import { hasStorageExplorerPrivileges } from './has_storage_explorer_privileges';
@ -30,6 +37,8 @@ import {
getTracesPerMinute,
} from './get_summary_statistics';
import { getApmEventClient } from '../../lib/helpers/get_apm_event_client';
import { isCrossClusterSearch } from './is_cross_cluster_search';
import { getServiceNamesFromTermsEnum } from '../services/get_services/get_sorted_and_filtered_services';
const storageExplorerRoute = createApmServerRoute({
endpoint: 'GET /internal/apm/storage_explorer',
@ -130,6 +139,15 @@ const storageExplorerServiceDetailsRoute = createApmServerRoute({
docs: number;
size: number;
}>;
indicesStats: Array<{
indexName: string;
numberOfDocs: number;
primary?: number | string;
replica?: number | string;
size?: number;
dataStream?: string;
lifecyclePhase?: string;
}>;
}> => {
const {
params,
@ -156,20 +174,34 @@ const storageExplorerServiceDetailsRoute = createApmServerRoute({
getRandomSampler({ security, request, probability }),
]);
const processorEventStats = await getStorageDetailsPerProcessorEvent({
setup,
apmEventClient,
context,
indexLifecyclePhase,
randomSampler,
environment,
kuery,
start,
end,
serviceName,
});
const [processorEventStats, indicesStats] = await Promise.all([
getStorageDetailsPerProcessorEvent({
apmEventClient,
setup,
context,
indexLifecyclePhase,
randomSampler,
environment,
kuery,
start,
end,
serviceName,
}),
getStorageDetailsPerIndex({
apmEventClient,
setup,
context,
indexLifecyclePhase,
randomSampler,
environment,
kuery,
start,
end,
serviceName,
}),
]);
return { processorEventStats };
return { processorEventStats, indicesStats };
},
});
@ -281,7 +313,9 @@ const storageExplorerSummaryStatsRoute = createApmServerRoute({
): Promise<{
tracesPerMinute: number;
numberOfServices: number;
estimatedSize: number;
totalSize: number;
diskSpaceUsedPct: number;
estimatedIncrementalSize: number;
dailyDataGeneration: number;
}> => {
const {
@ -344,12 +378,68 @@ const storageExplorerSummaryStatsRoute = createApmServerRoute({
},
});
const storageExplorerIsCrossClusterSearchRoute = createApmServerRoute({
endpoint: 'GET /internal/apm/storage_explorer/is_cross_cluster_search',
options: { tags: ['access:apm'] },
handler: async (resources): Promise<{ isCrossClusterSearch: boolean }> => {
const setup = await setupRequest(resources);
return { isCrossClusterSearch: isCrossClusterSearch(setup) };
},
});
const storageExplorerGetServices = createApmServerRoute({
endpoint: 'GET /internal/apm/storage_explorer/get_services',
options: {
tags: ['access:apm'],
},
params: t.type({
query: t.intersection([indexLifecyclePhaseRt, environmentRt, kueryRt]),
}),
handler: async (
resources
): Promise<{
services: Array<{
serviceName: string;
}>;
}> => {
const {
query: { environment, kuery, indexLifecyclePhase },
} = resources.params;
if (
kuery ||
indexLifecyclePhase !== IndexLifecyclePhaseSelectOption.All ||
environment !== ENVIRONMENT_ALL.value
) {
return {
services: [],
};
}
const apmEventClient = await getApmEventClient(resources);
const services = await getServiceNamesFromTermsEnum({
apmEventClient,
environment,
maxNumberOfServices: 500,
});
return {
services: services.map((serviceName): { serviceName: string } => ({
serviceName,
})),
};
},
});
export const storageExplorerRouteRepository = {
...storageExplorerRoute,
...storageExplorerServiceDetailsRoute,
...storageChartRoute,
...storageExplorerPrivilegesRoute,
...storageExplorerSummaryStatsRoute,
...storageExplorerIsCrossClusterSearchRoute,
...storageExplorerGetServices,
};
const SECURITY_REQUIRED_MESSAGE = i18n.translate(

View file

@ -15,7 +15,7 @@ export enum ApmUsername {
apmReadUserWithoutMlAccess = 'apm_read_user_without_ml_access',
apmManageOwnAgentKeys = 'apm_manage_own_agent_keys',
apmManageOwnAndCreateAgentKeys = 'apm_manage_own_and_create_agent_keys',
apmMonitorIndices = 'apm_monitor_indices',
apmMonitorClusterAndIndices = 'apm_monitor_cluster_and_indices',
}
export enum ApmCustomRolename {
@ -23,7 +23,7 @@ export enum ApmCustomRolename {
apmAnnotationsWriteUser = 'apm_annotations_write_user',
apmManageOwnAgentKeys = 'apm_manage_own_agent_keys',
apmManageOwnAndCreateAgentKeys = 'apm_manage_own_and_create_agent_keys',
apmMonitorIndices = 'apm_monitor_indices',
apmMonitorClusterAndIndices = 'apm_monitor_cluster_and_indices',
}
export const customRoles = {
@ -77,7 +77,7 @@ export const customRoles = {
},
],
},
[ApmCustomRolename.apmMonitorIndices]: {
[ApmCustomRolename.apmMonitorClusterAndIndices]: {
elasticsearch: {
indices: [
{
@ -85,6 +85,7 @@ export const customRoles = {
privileges: ['monitor'],
},
],
cluster: ['monitor'],
},
},
};
@ -118,9 +119,9 @@ export const users: Record<
ApmCustomRolename.apmManageOwnAndCreateAgentKeys,
],
},
[ApmUsername.apmMonitorIndices]: {
[ApmUsername.apmMonitorClusterAndIndices]: {
builtInRoleNames: ['viewer'],
customRoleNames: [ApmCustomRolename.apmMonitorIndices],
customRoleNames: [ApmCustomRolename.apmMonitorClusterAndIndices],
},
};

View file

@ -197,13 +197,13 @@ export const uiSettings: Record<string, UiSettings> = {
[apmServiceInventoryOptimizedSorting]: {
category: [observabilityFeatureId],
name: i18n.translate('xpack.observability.apmServiceInventoryOptimizedSorting', {
defaultMessage: 'Optimize APM Service Inventory page load performance',
defaultMessage: 'Optimize services list load performance in APM',
}),
description: i18n.translate(
'xpack.observability.apmServiceInventoryOptimizedSortingDescription',
{
defaultMessage:
'{technicalPreviewLabel} Default APM Service Inventory page sort (for Services without Machine Learning applied) to sort by Service Name. {feedbackLink}.',
'{technicalPreviewLabel} Default APM Service Inventory and Storage Explorer pages sort (for Services without Machine Learning applied) to sort by Service Name. {feedbackLink}.',
values: {
technicalPreviewLabel: `<em>[${technicalPreviewLabel}]</em>`,
feedbackLink: feedbackLink({ href: 'https://ela.st/feedback-apm-page-performance' }),

View file

@ -107,9 +107,9 @@ export function createTestConfig(config: ApmFtrConfig) {
kibanaServer,
username: ApmUsername.apmManageOwnAndCreateAgentKeys,
}),
monitorIndicesUser: await getApmApiClient({
monitorClusterAndIndicesUser: await getApmApiClient({
kibanaServer,
username: ApmUsername.apmMonitorIndices,
username: ApmUsername.apmMonitorClusterAndIndices,
}),
};
},

View file

@ -0,0 +1,116 @@
/*
* 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 { apm, timerange } from '@kbn/apm-synthtrace';
import expect from '@kbn/expect';
import { IndexLifecyclePhaseSelectOption } from '@kbn/apm-plugin/common/storage_explorer_types';
import { FtrProviderContext } from '../../common/ftr_provider_context';
export default function ApiTest({ getService }: FtrProviderContext) {
const registry = getService('registry');
const synthtraceEsClient = getService('synthtraceEsClient');
const apmApiClient = getService('apmApiClient');
const start = '2021-01-01T12:00:00.000Z';
const end = '2021-08-01T12:00:00.000Z';
// The terms enum API may return terms from deleted documents
// so we add a prefix to make sure we don't get data from other tests
const SERVICE_NAME_PREFIX = 'storage_explorer_services_';
async function getServices({
environment = 'ENVIRONMENT_ALL',
kuery = '',
indexLifecyclePhase = IndexLifecyclePhaseSelectOption.All,
}: {
environment?: string;
kuery?: string;
indexLifecyclePhase?: IndexLifecyclePhaseSelectOption;
} = {}) {
const response = await apmApiClient.readUser({
endpoint: 'GET /internal/apm/storage_explorer/get_services',
params: {
query: {
environment,
kuery,
indexLifecyclePhase,
},
},
});
return response.body.services
.filter((service) => service.serviceName.startsWith(SERVICE_NAME_PREFIX))
.map((service) => ({
...service,
serviceName: service.serviceName.replace(SERVICE_NAME_PREFIX, ''),
}));
}
registry.when('Get services', { config: 'basic', archives: [] }, () => {
before(async () => {
const serviceA = apm
.service({ name: `${SERVICE_NAME_PREFIX}a`, environment: 'production', agentName: 'java' })
.instance('a');
const serviceB = apm
.service({ name: `${SERVICE_NAME_PREFIX}b`, environment: 'development', agentName: 'go' })
.instance('b');
const serviceC = apm
.service({ name: `${SERVICE_NAME_PREFIX}c`, environment: 'development', agentName: 'go' })
.instance('c');
const eventsWithinTimerange = timerange(new Date(start).getTime(), new Date(end).getTime())
.interval('15m')
.rate(1)
.generator((timestamp) => [
serviceA.transaction({ transactionName: 'GET /api' }).duration(1000).timestamp(timestamp),
serviceB.transaction({ transactionName: 'GET /api' }).duration(1000).timestamp(timestamp),
]);
const eventsOutsideOfTimerange = timerange(
new Date('2021-01-01T00:00:00.000Z').getTime(),
new Date(start).getTime() - 1
)
.interval('15m')
.rate(1)
.generator((timestamp) =>
serviceC.transaction({ transactionName: 'GET /api' }).duration(1000).timestamp(timestamp)
);
await synthtraceEsClient.index(eventsWithinTimerange.merge(eventsOutsideOfTimerange));
});
after(() => synthtraceEsClient.clean());
it('with no kuery, environment or index lifecycle phase set it returns services based on the terms enum API', async () => {
const items = await getServices();
const serviceNames = items.map((item) => item.serviceName);
expect(serviceNames.sort()).to.eql(['a', 'b', 'c']);
});
it('with kuery set does it does not return any services', async () => {
const services = await getServices({
kuery: 'service.name:*',
});
expect(services).to.be.empty();
});
it('with environment set to production it does not return any services', async () => {
const services = await getServices({
environment: 'production',
});
expect(services).to.be.empty();
});
it('with index lifecycle phase set to hot it does not return any services', async () => {
const services = await getServices({
indexLifecyclePhase: IndexLifecyclePhaseSelectOption.Hot,
});
expect(services).to.be.empty();
});
});
}

View file

@ -36,7 +36,7 @@ export default function ApiTest({ getService }: FtrProviderContext) {
APIClientRequestParamsOf<'GET /internal/apm/services/{serviceName}/storage_details'>['params']
>
) {
return await apmApiClient.monitorIndicesUser({
return await apmApiClient.monitorClusterAndIndicesUser({
endpoint: 'GET /internal/apm/services/{serviceName}/storage_details',
params: {
path: {

View file

@ -27,7 +27,7 @@ export default function ApiTest({ getService }: FtrProviderContext) {
APIClientRequestParamsOf<'GET /internal/apm/storage_explorer'>['params']
>
) {
return await apmApiClient.monitorIndicesUser({
return await apmApiClient.monitorClusterAndIndicesUser({
endpoint: 'GET /internal/apm/storage_explorer',
params: {
query: {

View file

@ -20,7 +20,7 @@ export default function ApiTest({ getService }: FtrProviderContext) {
registry.when('Storage explorer privileges', { config: 'basic', archives: [] }, () => {
it('returns true when the user has the required indices privileges', async () => {
const { status, body } = await callApi(apmApiClient.monitorIndicesUser);
const { status, body } = await callApi(apmApiClient.monitorClusterAndIndicesUser);
expect(status).to.be(200);
expect(body.hasPrivileges).to.be(true);
});

View file

@ -27,7 +27,7 @@ export default function ApiTest({ getService }: FtrProviderContext) {
APIClientRequestParamsOf<'GET /internal/apm/storage_explorer_summary_stats'>['params']
>
) {
return await apmApiClient.monitorIndicesUser({
return await apmApiClient.monitorClusterAndIndicesUser({
endpoint: 'GET /internal/apm/storage_explorer_summary_stats',
params: {
query: {
@ -53,7 +53,9 @@ export default function ApiTest({ getService }: FtrProviderContext) {
expect(status).to.be(200);
expect(body.tracesPerMinute).to.be(0);
expect(body.numberOfServices).to.be(0);
expect(body.estimatedSize).to.be(0);
expect(body.totalSize).to.be(0);
expect(body.estimatedIncrementalSize).to.be(0);
expect(body.diskSpaceUsedPct).to.be(0);
expect(body.dailyDataGeneration).to.be(0);
});
}
@ -100,7 +102,9 @@ export default function ApiTest({ getService }: FtrProviderContext) {
expect(status).to.be(200);
expect(body.numberOfServices).to.be(2);
expect(roundNumber(body.tracesPerMinute)).to.be(2);
expect(body.estimatedSize).to.be.greaterThan(0);
expect(body.totalSize).to.be.greaterThan(0);
expect(body.estimatedIncrementalSize).to.be.greaterThan(0);
expect(body.diskSpaceUsedPct).to.be.greaterThan(0);
expect(body.dailyDataGeneration).to.be.greaterThan(0);
});
@ -114,7 +118,9 @@ export default function ApiTest({ getService }: FtrProviderContext) {
expect(status).to.be(200);
expect(body.numberOfServices).to.be(1);
expect(roundNumber(body.tracesPerMinute)).to.be(1);
expect(body.estimatedSize).to.be.greaterThan(0);
expect(body.totalSize).to.be.greaterThan(0);
expect(body.estimatedIncrementalSize).to.be.greaterThan(0);
expect(body.diskSpaceUsedPct).to.be.greaterThan(0);
expect(body.dailyDataGeneration).to.be.greaterThan(0);
});
@ -128,7 +134,9 @@ export default function ApiTest({ getService }: FtrProviderContext) {
expect(status).to.be(200);
expect(body.tracesPerMinute).to.be(0);
expect(body.numberOfServices).to.be(0);
expect(body.estimatedSize).to.be(0);
expect(body.totalSize).to.be.greaterThan(0);
expect(body.estimatedIncrementalSize).to.be(0);
expect(body.diskSpaceUsedPct).to.be.greaterThan(0);
expect(body.dailyDataGeneration).to.be(0);
});
@ -142,7 +150,9 @@ export default function ApiTest({ getService }: FtrProviderContext) {
expect(status).to.be(200);
expect(body.numberOfServices).to.be(1);
expect(roundNumber(body.tracesPerMinute)).to.be(1);
expect(body.estimatedSize).to.be.greaterThan(0);
expect(body.totalSize).to.be.greaterThan(0);
expect(body.estimatedIncrementalSize).to.be.greaterThan(0);
expect(body.diskSpaceUsedPct).to.be.greaterThan(0);
expect(body.dailyDataGeneration).to.be.greaterThan(0);
});
});

View file

@ -22,7 +22,7 @@ export default function ApiTest({ getService }: FtrProviderContext) {
const end = new Date('2021-01-01T00:15:00.000Z').getTime() - 1;
async function callApi() {
return await apmApiClient.monitorIndicesUser({
return await apmApiClient.monitorClusterAndIndicesUser({
endpoint: 'GET /internal/apm/storage_chart',
params: {
query: {