mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 01:13:23 -04:00
[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:
parent
b5c852bb40
commit
e390743f2e
32 changed files with 1615 additions and 437 deletions
|
@ -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`,
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -21,7 +21,7 @@ Cypress.Commands.add('loginAsEditorUser', () => {
|
|||
|
||||
Cypress.Commands.add('loginAsMonitorUser', () => {
|
||||
return cy.loginAs({
|
||||
username: ApmUsername.apmMonitorIndices,
|
||||
username: ApmUsername.apmMonitorClusterAndIndices,
|
||||
password: 'changeme',
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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)',
|
||||
});
|
||||
}
|
|
@ -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 />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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',
|
||||
{
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}) ?? []
|
||||
);
|
||||
}
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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(':');
|
||||
}
|
|
@ -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(
|
||||
|
|
|
@ -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],
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -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' }),
|
||||
|
|
|
@ -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,
|
||||
}),
|
||||
};
|
||||
},
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
}
|
|
@ -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: {
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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: {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue