mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[APM] Storage Usage Explorer view (#130152)
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: boriskirov <boris.kirov@elastic.co>
This commit is contained in:
parent
0dee85815b
commit
cfce8825d3
31 changed files with 1631 additions and 89 deletions
|
@ -83,6 +83,8 @@ exports[`Error HTTP_REQUEST_METHOD 1`] = `undefined`;
|
|||
|
||||
exports[`Error HTTP_RESPONSE_STATUS_CODE 1`] = `undefined`;
|
||||
|
||||
exports[`Error INDEX 1`] = `undefined`;
|
||||
|
||||
exports[`Error KUBERNETES 1`] = `undefined`;
|
||||
|
||||
exports[`Error LABEL_NAME 1`] = `undefined`;
|
||||
|
@ -205,6 +207,8 @@ exports[`Error SPAN_SUBTYPE 1`] = `undefined`;
|
|||
|
||||
exports[`Error SPAN_TYPE 1`] = `undefined`;
|
||||
|
||||
exports[`Error TIER 1`] = `undefined`;
|
||||
|
||||
exports[`Error TRACE_ID 1`] = `"trace id"`;
|
||||
|
||||
exports[`Error TRANSACTION_DURATION 1`] = `undefined`;
|
||||
|
@ -318,6 +322,8 @@ exports[`Span HTTP_REQUEST_METHOD 1`] = `undefined`;
|
|||
|
||||
exports[`Span HTTP_RESPONSE_STATUS_CODE 1`] = `undefined`;
|
||||
|
||||
exports[`Span INDEX 1`] = `undefined`;
|
||||
|
||||
exports[`Span KUBERNETES 1`] = `undefined`;
|
||||
|
||||
exports[`Span LABEL_NAME 1`] = `undefined`;
|
||||
|
@ -436,6 +442,8 @@ exports[`Span SPAN_SUBTYPE 1`] = `"my subtype"`;
|
|||
|
||||
exports[`Span SPAN_TYPE 1`] = `"span type"`;
|
||||
|
||||
exports[`Span TIER 1`] = `undefined`;
|
||||
|
||||
exports[`Span TRACE_ID 1`] = `"trace id"`;
|
||||
|
||||
exports[`Span TRANSACTION_DURATION 1`] = `undefined`;
|
||||
|
@ -553,6 +561,8 @@ exports[`Transaction HTTP_REQUEST_METHOD 1`] = `"GET"`;
|
|||
|
||||
exports[`Transaction HTTP_RESPONSE_STATUS_CODE 1`] = `200`;
|
||||
|
||||
exports[`Transaction INDEX 1`] = `undefined`;
|
||||
|
||||
exports[`Transaction KUBERNETES 1`] = `
|
||||
Object {
|
||||
"pod": Object {
|
||||
|
@ -681,6 +691,8 @@ exports[`Transaction SPAN_SUBTYPE 1`] = `undefined`;
|
|||
|
||||
exports[`Transaction SPAN_TYPE 1`] = `undefined`;
|
||||
|
||||
exports[`Transaction TIER 1`] = `undefined`;
|
||||
|
||||
exports[`Transaction TRACE_ID 1`] = `"trace id"`;
|
||||
|
||||
exports[`Transaction TRANSACTION_DURATION 1`] = `1337`;
|
||||
|
|
|
@ -147,3 +147,7 @@ export const PROFILE_INUSE_SPACE = 'profile.inuse_space.bytes';
|
|||
export const FAAS_ID = 'faas.id';
|
||||
export const FAAS_COLDSTART = 'faas.coldstart';
|
||||
export const FAAS_TRIGGER_TYPE = 'faas.trigger.type';
|
||||
|
||||
// Metadata
|
||||
export const TIER = '_tier';
|
||||
export const INDEX = '_index';
|
||||
|
|
33
x-pack/plugins/apm/common/storage_explorer_types.ts
Normal file
33
x-pack/plugins/apm/common/storage_explorer_types.ts
Normal file
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* 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 * as t from 'io-ts';
|
||||
|
||||
export enum IndexLifecyclePhaseSelectOption {
|
||||
All = 'all',
|
||||
Hot = 'hot',
|
||||
Warm = 'warm',
|
||||
Cold = 'cold',
|
||||
Frozen = 'frozen',
|
||||
}
|
||||
|
||||
export const indexLifeCyclePhaseToDataTier = {
|
||||
[IndexLifecyclePhaseSelectOption.Hot]: 'data_hot',
|
||||
[IndexLifecyclePhaseSelectOption.Warm]: 'data_warm',
|
||||
[IndexLifecyclePhaseSelectOption.Cold]: 'data_cold',
|
||||
[IndexLifecyclePhaseSelectOption.Frozen]: 'data_frozen',
|
||||
};
|
||||
|
||||
export const indexLifecyclePhaseRt = t.type({
|
||||
indexLifecyclePhase: t.union([
|
||||
t.literal(IndexLifecyclePhaseSelectOption.All),
|
||||
t.literal(IndexLifecyclePhaseSelectOption.Hot),
|
||||
t.literal(IndexLifecyclePhaseSelectOption.Warm),
|
||||
t.literal(IndexLifecyclePhaseSelectOption.Cold),
|
||||
t.literal(IndexLifecyclePhaseSelectOption.Frozen),
|
||||
]),
|
||||
});
|
|
@ -72,7 +72,7 @@ const serviceOverviewLink = apmRouter.link('/services/:serviceName', {
|
|||
|
||||
If you're not in React context, you can also import `apmRouter` directly and call its `link` function - but you have to prepend the basePath manually in that case.
|
||||
|
||||
We also have the [`getLegacyApmHref` function and `APMLink` component](../public/components/shared/links/apm/APMLink.tsx), but we should consider them deprecated, in favor of `router.link`. Other components inside that directory contain other functions and components that provide the same functionality for linking to more specific sections inside the APM plugin.
|
||||
We also have the [`getLegacyApmHref` function and `LegacyAPMLink` component](../public/components/shared/links/apm/apm_link.tsx), but we should consider them deprecated, in favor of `router.link`. Other components inside that directory contain other functions and components that provide the same functionality for linking to more specific sections inside the APM plugin.
|
||||
|
||||
### Cross-app linking
|
||||
|
||||
|
|
|
@ -52,7 +52,9 @@ const stories: Meta<Args> = {
|
|||
|
||||
return (
|
||||
<MemoryRouter
|
||||
initialEntries={['/service-map?rangeFrom=now-15m&rangeTo=now']}
|
||||
initialEntries={[
|
||||
'/service-map?rangeFrom=now-15m&rangeTo=now&comparisonEnabled=true&offset=1d',
|
||||
]}
|
||||
>
|
||||
<KibanaReactContext.Provider>
|
||||
<MockUrlParamsContextProvider>
|
||||
|
|
|
@ -17,7 +17,7 @@ import {
|
|||
} from '../../../../../../../common/agent_configuration/all_option';
|
||||
import { useFetcher, FETCH_STATUS } from '../../../../../../hooks/use_fetcher';
|
||||
import { FormRowSelect } from './form_row_select';
|
||||
import { APMLink } from '../../../../../shared/links/apm/apm_link';
|
||||
import { LegacyAPMLink } from '../../../../../shared/links/apm/apm_link';
|
||||
import { FormRowSuggestionsSelect } from './form_row_suggestions_select';
|
||||
import { SERVICE_NAME } from '../../../../../../../common/elasticsearch_fieldnames';
|
||||
interface Props {
|
||||
|
@ -142,14 +142,14 @@ export function ServicePage({ newConfig, setNewConfig, onClickNext }: Props) {
|
|||
<EuiFlexGroup justifyContent="flexEnd">
|
||||
{/* Cancel button */}
|
||||
<EuiFlexItem grow={false}>
|
||||
<APMLink path="/settings/agent-configuration">
|
||||
<LegacyAPMLink path="/settings/agent-configuration">
|
||||
<EuiButtonEmpty color="primary">
|
||||
{i18n.translate(
|
||||
'xpack.apm.agentConfig.servicePage.cancelButton',
|
||||
{ defaultMessage: 'Cancel' }
|
||||
)}
|
||||
</EuiButtonEmpty>
|
||||
</APMLink>
|
||||
</LegacyAPMLink>
|
||||
</EuiFlexItem>
|
||||
|
||||
{/* Next button */}
|
||||
|
|
|
@ -9,7 +9,7 @@ import { EuiButton, EuiSpacer, EuiText } from '@elastic/eui';
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import React from 'react';
|
||||
import { APMLink } from '../../../../shared/links/apm/apm_link';
|
||||
import { LegacyAPMLink } from '../../../../shared/links/apm/apm_link';
|
||||
import { useFleetCloudAgentPolicyHref } from '../../../../shared/links/kibana';
|
||||
|
||||
export function CardFooterContent() {
|
||||
|
@ -31,12 +31,12 @@ export function CardFooterContent() {
|
|||
defaultMessage="or simply return to the {serviceInventoryLink}."
|
||||
values={{
|
||||
serviceInventoryLink: (
|
||||
<APMLink path="/services">
|
||||
<LegacyAPMLink path="/services">
|
||||
{i18n.translate(
|
||||
'xpack.apm.settings.schema.success.returnText.serviceInventoryLink',
|
||||
{ defaultMessage: 'Service inventory' }
|
||||
)}
|
||||
</APMLink>
|
||||
</LegacyAPMLink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui';
|
||||
import { IndexLifecyclePhaseSelect } from './index_lifecycle_phase_select';
|
||||
import { ServicesTable } from './services_table';
|
||||
import { SearchBar } from '../../shared/search_bar';
|
||||
|
||||
export function StorageExplorer() {
|
||||
return (
|
||||
<>
|
||||
<SearchBar />
|
||||
<EuiFlexGroup justifyContent="flexEnd">
|
||||
<EuiFlexItem grow={false}>
|
||||
<IndexLifecyclePhaseSelect />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer />
|
||||
<ServicesTable />
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,135 @@
|
|||
/*
|
||||
* 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 { EuiSuperSelect, EuiText } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { IndexLifecyclePhaseSelectOption } from '../../../../common/storage_explorer_types';
|
||||
import * as urlHelpers from '../../shared/links/url_helpers';
|
||||
import { useApmParams } from '../../../hooks/use_apm_params';
|
||||
|
||||
export function IndexLifecyclePhaseSelect() {
|
||||
const history = useHistory();
|
||||
|
||||
const {
|
||||
query: { indexLifecyclePhase },
|
||||
} = useApmParams('/storage-explorer');
|
||||
|
||||
const options = [
|
||||
{
|
||||
value: IndexLifecyclePhaseSelectOption.All,
|
||||
label: i18n.translate(
|
||||
'xpack.apm.settings.storageExplorer.indexLifecyclePhase.all.label',
|
||||
{
|
||||
defaultMessage: 'All',
|
||||
}
|
||||
),
|
||||
description: i18n.translate(
|
||||
'xpack.apm.settings.storageExplorer.indexLifecyclePhase.all.description',
|
||||
{
|
||||
defaultMessage: 'Search data in all lifecycle phases.',
|
||||
}
|
||||
),
|
||||
},
|
||||
{
|
||||
value: IndexLifecyclePhaseSelectOption.Hot,
|
||||
label: i18n.translate(
|
||||
'xpack.apm.settings.storageExplorer.indexLifecyclePhase.hot.label',
|
||||
{
|
||||
defaultMessage: 'Hot',
|
||||
}
|
||||
),
|
||||
description: i18n.translate(
|
||||
'xpack.apm.settings.storageExplorer.indexLifecyclePhase.hot.description',
|
||||
{
|
||||
defaultMessage:
|
||||
'Holds your most-recent, most-frequently-searched data.',
|
||||
}
|
||||
),
|
||||
},
|
||||
{
|
||||
value: IndexLifecyclePhaseSelectOption.Warm,
|
||||
label: i18n.translate(
|
||||
'xpack.apm.settings.storageExplorer.indexLifecyclePhase.warm.label',
|
||||
{
|
||||
defaultMessage: 'Warm',
|
||||
}
|
||||
),
|
||||
description: i18n.translate(
|
||||
'xpack.apm.settings.storageExplorer.indexLifecyclePhase.warm.description',
|
||||
{
|
||||
defaultMessage:
|
||||
'Holds data from recent weeks. Updates are still allowed, but likely infrequent.',
|
||||
}
|
||||
),
|
||||
},
|
||||
{
|
||||
value: IndexLifecyclePhaseSelectOption.Cold,
|
||||
label: i18n.translate(
|
||||
'xpack.apm.settings.storageExplorer.indexLifecyclePhase.cold.label',
|
||||
{
|
||||
defaultMessage: 'Cold',
|
||||
}
|
||||
),
|
||||
description: i18n.translate(
|
||||
'xpack.apm.settings.storageExplorer.indexLifecyclePhase.cold.description',
|
||||
{
|
||||
defaultMessage:
|
||||
'While still searchable, this tier is typically optimized for lower storage costs rather than search speed.',
|
||||
}
|
||||
),
|
||||
},
|
||||
{
|
||||
value: IndexLifecyclePhaseSelectOption.Frozen,
|
||||
label: i18n.translate(
|
||||
'xpack.apm.settings.storageExplorer.indexLifecyclePhase.frozen.label',
|
||||
{
|
||||
defaultMessage: 'Frozen',
|
||||
}
|
||||
),
|
||||
description: i18n.translate(
|
||||
'xpack.apm.settings.storageExplorer.indexLifecyclePhase.frozen.description',
|
||||
{
|
||||
defaultMessage:
|
||||
'Holds data that are no longer being queried, or being queried rarely.',
|
||||
}
|
||||
),
|
||||
},
|
||||
].map(({ value, label, description }) => ({
|
||||
value,
|
||||
inputDisplay: label,
|
||||
dropdownDisplay: (
|
||||
<>
|
||||
<strong>{label}</strong>
|
||||
<EuiText size="s" color="subdued">
|
||||
<p>{description}</p>
|
||||
</EuiText>
|
||||
</>
|
||||
),
|
||||
}));
|
||||
|
||||
return (
|
||||
<EuiSuperSelect
|
||||
prepend={i18n.translate(
|
||||
'xpack.apm.settings.storageExplorer.indexLifecyclePhase.label',
|
||||
{
|
||||
defaultMessage: 'Index lifecycle phase',
|
||||
}
|
||||
)}
|
||||
options={options}
|
||||
valueOfSelected={indexLifecyclePhase}
|
||||
onChange={(value) => {
|
||||
urlHelpers.push(history, {
|
||||
query: { indexLifecyclePhase: value },
|
||||
});
|
||||
}}
|
||||
hasDividers
|
||||
style={{ minWidth: 200 }}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,275 @@
|
|||
/*
|
||||
* 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, { useState, useEffect, ReactNode } from 'react';
|
||||
import {
|
||||
EuiInMemoryTable,
|
||||
EuiBasicTableColumn,
|
||||
EuiButtonIcon,
|
||||
EuiScreenReaderOnly,
|
||||
RIGHT_ALIGNMENT,
|
||||
EuiToolTip,
|
||||
EuiIcon,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { ValuesType } from 'utility-types';
|
||||
import { EnvironmentBadge } from '../../../shared/environment_badge';
|
||||
import { asPercent } from '../../../../../common/utils/formatters';
|
||||
import { ServiceLink } from '../../../shared/service_link';
|
||||
import { TruncateWithTooltip } from '../../../shared/truncate_with_tooltip';
|
||||
import { StorageDetailsPerService } from './storage_details_per_service';
|
||||
import { getComparisonEnabled } from '../../../shared/time_comparison/get_comparison_enabled';
|
||||
import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plugin_context';
|
||||
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 { 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';
|
||||
|
||||
type StorageExplorerItems =
|
||||
APIReturnType<'GET /internal/apm/storage_explorer'>['serviceStatistics'];
|
||||
|
||||
export function ServicesTable() {
|
||||
const [itemIdToExpandedRowMap, setItemIdToExpandedRowMap] = useState<
|
||||
Record<string, ReactNode>
|
||||
>({});
|
||||
|
||||
const { core } = useApmPluginContext();
|
||||
|
||||
const {
|
||||
query: {
|
||||
rangeFrom,
|
||||
rangeTo,
|
||||
environment,
|
||||
kuery,
|
||||
indexLifecyclePhase,
|
||||
comparisonEnabled: urlComparisonEnabled,
|
||||
},
|
||||
} = useApmParams('/storage-explorer');
|
||||
|
||||
const { start, end } = useTimeRange({ rangeFrom, rangeTo });
|
||||
|
||||
const comparisonEnabled = getComparisonEnabled({
|
||||
core,
|
||||
urlComparisonEnabled,
|
||||
});
|
||||
|
||||
const toggleRowDetails = (selectedServiceName: string) => {
|
||||
const expandedRowMapValues = { ...itemIdToExpandedRowMap };
|
||||
if (expandedRowMapValues[selectedServiceName]) {
|
||||
delete expandedRowMapValues[selectedServiceName];
|
||||
} else {
|
||||
expandedRowMapValues[selectedServiceName] = (
|
||||
<StorageDetailsPerService
|
||||
serviceName={selectedServiceName}
|
||||
indexLifecyclePhase={indexLifecyclePhase}
|
||||
/>
|
||||
);
|
||||
}
|
||||
setItemIdToExpandedRowMap(expandedRowMapValues);
|
||||
};
|
||||
|
||||
const { data, status } = useProgressiveFetcher(
|
||||
(callApmApi) => {
|
||||
return callApmApi('GET /internal/apm/storage_explorer', {
|
||||
params: {
|
||||
query: {
|
||||
indexLifecyclePhase,
|
||||
start,
|
||||
end,
|
||||
environment,
|
||||
kuery,
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
[indexLifecyclePhase, start, end, environment, kuery]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
// Closes any open rows when fetching new items
|
||||
setItemIdToExpandedRowMap({});
|
||||
}, [status]);
|
||||
|
||||
const loading =
|
||||
status === FETCH_STATUS.NOT_INITIATED || status === FETCH_STATUS.LOADING;
|
||||
|
||||
const columns: Array<EuiBasicTableColumn<ValuesType<StorageExplorerItems>>> =
|
||||
[
|
||||
{
|
||||
field: 'serviceName',
|
||||
name: i18n.translate(
|
||||
'xpack.apm.settings.storageExplorer.table.serviceColumnName',
|
||||
{
|
||||
defaultMessage: 'Service',
|
||||
}
|
||||
),
|
||||
sortable: true,
|
||||
render: (_, { serviceName, agentName }) => {
|
||||
const serviceLinkQuery = {
|
||||
comparisonEnabled,
|
||||
environment,
|
||||
kuery,
|
||||
rangeFrom,
|
||||
rangeTo,
|
||||
serviceGroup: '',
|
||||
};
|
||||
|
||||
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.settings.storageExplorer.table.environmentColumnName',
|
||||
{
|
||||
defaultMessage: 'Environment',
|
||||
}
|
||||
),
|
||||
render: (_, { environments }) => (
|
||||
<EnvironmentBadge environments={environments ?? []} />
|
||||
),
|
||||
sortable: true,
|
||||
},
|
||||
|
||||
{
|
||||
field: 'sampling',
|
||||
name: (
|
||||
<EuiToolTip
|
||||
content={i18n.translate(
|
||||
'xpack.apm.settings.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.settings.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.settings.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.settings.storageExplorer.table.collapse',
|
||||
{
|
||||
defaultMessage: 'Collapse',
|
||||
}
|
||||
)
|
||||
: i18n.translate(
|
||||
'xpack.apm.settings.storageExplorer.table.expand',
|
||||
{
|
||||
defaultMessage: 'Expand',
|
||||
}
|
||||
)
|
||||
}
|
||||
iconType={
|
||||
itemIdToExpandedRowMap[serviceName] ? 'arrowUp' : 'arrowDown'
|
||||
}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<EuiInMemoryTable
|
||||
tableCaption={i18n.translate(
|
||||
'xpack.apm.settings.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.settings.storageExplorer.table.errorMessage',
|
||||
{
|
||||
defaultMessage: 'Failed to fetch',
|
||||
}
|
||||
)
|
||||
: ''
|
||||
}
|
||||
message={
|
||||
loading
|
||||
? i18n.translate('xpack.apm.settings.storageExplorer.table.loading', {
|
||||
defaultMessage: 'Loading...',
|
||||
})
|
||||
: i18n.translate(
|
||||
'xpack.apm.settings.storageExplorer.table.noResults',
|
||||
{
|
||||
defaultMessage: 'No data found',
|
||||
}
|
||||
)
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
import { EuiToolTip, EuiIcon } from '@elastic/eui';
|
||||
|
||||
export function SizeLabel() {
|
||||
return (
|
||||
<EuiToolTip
|
||||
content={i18n.translate(
|
||||
'xpack.apm.settings.storageExplorer.sizeLabel.description',
|
||||
{
|
||||
defaultMessage: `The estimated storage size per service. This estimate includes primary and replica shards and is calculated by prorating the total size of your indices by the service's document count divided by the total number of documents.`,
|
||||
}
|
||||
)}
|
||||
>
|
||||
<>
|
||||
{i18n.translate('xpack.apm.settings.storageExplorer.sizeLabel.title', {
|
||||
defaultMessage: 'Size',
|
||||
})}{' '}
|
||||
<EuiIcon
|
||||
size="s"
|
||||
color="subdued"
|
||||
type="questionInCircle"
|
||||
className="eui-alignTop"
|
||||
/>
|
||||
</>
|
||||
</EuiToolTip>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,264 @@
|
|||
/*
|
||||
* 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 {
|
||||
EuiLoadingContent,
|
||||
EuiTitle,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiLink,
|
||||
euiPaletteColorBlind,
|
||||
EuiPanel,
|
||||
EuiFlexGrid,
|
||||
EuiSpacer,
|
||||
} from '@elastic/eui';
|
||||
import { useChartTheme } from '@kbn/observability-plugin/public';
|
||||
import {
|
||||
Chart,
|
||||
Partition,
|
||||
Settings,
|
||||
Datum,
|
||||
PartitionLayout,
|
||||
} from '@elastic/charts';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { css } from '@emotion/react';
|
||||
import { useEuiTheme } from '@elastic/eui';
|
||||
import { ProcessorEvent } from '@kbn/observability-plugin/common';
|
||||
import { IndexLifecyclePhaseSelectOption } from '../../../../../common/storage_explorer_types';
|
||||
import { useApmParams } from '../../../../hooks/use_apm_params';
|
||||
import { useTimeRange } from '../../../../hooks/use_time_range';
|
||||
import { FETCH_STATUS } from '../../../../hooks/use_fetcher';
|
||||
import { useProgressiveFetcher } from '../../../../hooks/use_progressive_fetcher';
|
||||
import { useApmRouter } from '../../../../hooks/use_apm_router';
|
||||
import { asInteger } from '../../../../../common/utils/formatters/formatters';
|
||||
import { NOT_AVAILABLE_LABEL } from '../../../../../common/i18n';
|
||||
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';
|
||||
|
||||
interface Props {
|
||||
serviceName: string;
|
||||
indexLifecyclePhase: IndexLifecyclePhaseSelectOption;
|
||||
}
|
||||
|
||||
const ProcessorEventLabelMap = {
|
||||
[ProcessorEvent.transaction]: i18n.translate(
|
||||
'xpack.apm.settings.storageExplorer.serviceDetails.transactions',
|
||||
{
|
||||
defaultMessage: 'Transactions',
|
||||
}
|
||||
),
|
||||
[ProcessorEvent.span]: i18n.translate(
|
||||
'xpack.apm.settings.storageExplorer.serviceDetails.spans',
|
||||
{
|
||||
defaultMessage: 'Spans',
|
||||
}
|
||||
),
|
||||
[ProcessorEvent.metric]: i18n.translate(
|
||||
'xpack.apm.settings.storageExplorer.serviceDetails.metrics',
|
||||
{
|
||||
defaultMessage: 'Metrics',
|
||||
}
|
||||
),
|
||||
[ProcessorEvent.error]: i18n.translate(
|
||||
'xpack.apm.settings.storageExplorer.serviceDetails.errors',
|
||||
{
|
||||
defaultMessage: 'Errors',
|
||||
}
|
||||
),
|
||||
};
|
||||
|
||||
export function StorageDetailsPerService({
|
||||
serviceName,
|
||||
indexLifecyclePhase,
|
||||
}: Props) {
|
||||
const { core } = useApmPluginContext();
|
||||
const chartTheme = useChartTheme();
|
||||
const router = useApmRouter();
|
||||
const { euiTheme } = useEuiTheme();
|
||||
|
||||
const { query } = useApmParams('/storage-explorer');
|
||||
const {
|
||||
rangeFrom,
|
||||
rangeTo,
|
||||
environment,
|
||||
kuery,
|
||||
comparisonEnabled: urlComparisonEnabled,
|
||||
} = query;
|
||||
|
||||
const { start, end } = useTimeRange({ rangeFrom, rangeTo });
|
||||
|
||||
const serviceOverviewLink = router.link('/services/{serviceName}/overview', {
|
||||
path: {
|
||||
serviceName,
|
||||
},
|
||||
query: {
|
||||
...query,
|
||||
serviceGroup: '',
|
||||
comparisonEnabled: getComparisonEnabled({ core, urlComparisonEnabled }),
|
||||
},
|
||||
});
|
||||
|
||||
const groupedPalette = euiPaletteColorBlind();
|
||||
|
||||
const { data, status } = useProgressiveFetcher(
|
||||
(callApmApi) => {
|
||||
return callApmApi(
|
||||
'GET /internal/apm/services/{serviceName}/storage_details',
|
||||
{
|
||||
params: {
|
||||
path: {
|
||||
serviceName,
|
||||
},
|
||||
query: {
|
||||
indexLifecyclePhase,
|
||||
start,
|
||||
end,
|
||||
environment,
|
||||
kuery,
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
},
|
||||
[indexLifecyclePhase, start, end, environment, kuery, serviceName]
|
||||
);
|
||||
|
||||
if (
|
||||
status === FETCH_STATUS.LOADING ||
|
||||
status === FETCH_STATUS.NOT_INITIATED
|
||||
) {
|
||||
return (
|
||||
<div style={{ width: '50%' }}>
|
||||
<EuiLoadingContent data-test-subj="loadingSpinner" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!data || !data.processorEventStats) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const processorEventStats = data.processorEventStats.map(
|
||||
({ processorEvent, docs, size }) => ({
|
||||
processorEventLabel: ProcessorEventLabelMap[processorEvent],
|
||||
docs,
|
||||
size,
|
||||
})
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiFlexGroup direction="column" responsive={false} gutterSize="m">
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGroup justifyContent="spaceBetween">
|
||||
<EuiFlexItem>
|
||||
<EuiTitle size="xs">
|
||||
<h4>
|
||||
{i18n.translate(
|
||||
'xpack.apm.settings.storageExplorer.serviceDetails.title',
|
||||
{
|
||||
defaultMessage: 'Service storage details',
|
||||
}
|
||||
)}
|
||||
</h4>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiLink href={serviceOverviewLink}>
|
||||
{i18n.translate(
|
||||
'xpack.apm.settings.storageExplorer.serviceDetails.serviceOverviewLink',
|
||||
{
|
||||
defaultMessage: 'Go to service overview',
|
||||
}
|
||||
)}
|
||||
</EuiLink>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGroup justifyContent="spaceBetween" gutterSize="m">
|
||||
<EuiFlexItem>
|
||||
<EuiPanel hasShadow={false}>
|
||||
<Chart>
|
||||
<Settings
|
||||
theme={[
|
||||
{
|
||||
partition: {
|
||||
fillLabel: {
|
||||
textColor: euiTheme.colors.emptyShade,
|
||||
},
|
||||
emptySizeRatio: 0.3,
|
||||
},
|
||||
},
|
||||
...chartTheme,
|
||||
]}
|
||||
showLegend
|
||||
/>
|
||||
<Partition
|
||||
layout={PartitionLayout.sunburst}
|
||||
id="storageExplorerSizeByProcessorType"
|
||||
data={processorEventStats}
|
||||
valueAccessor={(d) => d.size ?? 0}
|
||||
valueGetter="percent"
|
||||
valueFormatter={(d: number) =>
|
||||
asDynamicBytes(d) || NOT_AVAILABLE_LABEL
|
||||
}
|
||||
layers={[
|
||||
{
|
||||
groupByRollup: (d: Datum) => d.processorEventLabel,
|
||||
shape: {
|
||||
fillColor: (d) => groupedPalette[d.sortIndex],
|
||||
},
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Chart>
|
||||
</EuiPanel>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiPanel hasShadow={false} paddingSize="l">
|
||||
{processorEventStats.map(
|
||||
({ processorEventLabel, docs, size }) => (
|
||||
<>
|
||||
<EuiFlexGrid
|
||||
columns={2}
|
||||
css={css`
|
||||
font-weight: ${euiTheme.font.weight.semiBold};
|
||||
`}
|
||||
>
|
||||
<EuiFlexItem>{processorEventLabel}</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<SizeLabel />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGrid>
|
||||
<EuiFlexGrid
|
||||
columns={2}
|
||||
css={css`
|
||||
background-color: ${euiTheme.colors.lightestShade};
|
||||
border-top: 1px solid ${euiTheme.colors.lightShade};
|
||||
border-bottom: 1px solid ${euiTheme.colors.lightShade};
|
||||
`}
|
||||
>
|
||||
<EuiFlexItem>{asInteger(docs)}</EuiFlexItem>
|
||||
<EuiFlexItem>{asDynamicBytes(size)}</EuiFlexItem>
|
||||
</EuiFlexGrid>
|
||||
<EuiSpacer />
|
||||
</>
|
||||
)
|
||||
)}
|
||||
</EuiPanel>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -27,6 +27,7 @@ import { ApmMainTemplate } from '../templates/apm_main_template';
|
|||
import { ServiceGroupTemplate } from '../templates/service_group_template';
|
||||
import { dependencies } from './dependencies';
|
||||
import { legacyBackends } from './legacy_backends';
|
||||
import { storageExplorer } from './storage_explorer';
|
||||
|
||||
export function page<
|
||||
TPath extends string,
|
||||
|
@ -232,6 +233,7 @@ export const home = {
|
|||
}),
|
||||
...dependencies,
|
||||
...legacyBackends,
|
||||
...storageExplorer,
|
||||
'/': {
|
||||
element: (
|
||||
<ServiceGroupsRedirect>
|
||||
|
|
|
@ -0,0 +1,65 @@
|
|||
/*
|
||||
* 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 { EuiTitle, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import * as t from 'io-ts';
|
||||
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';
|
||||
|
||||
export const storageExplorer = {
|
||||
'/storage-explorer': {
|
||||
element: (
|
||||
<Breadcrumb
|
||||
title={i18n.translate('xpack.apm.views.storageExplorer.title', {
|
||||
defaultMessage: 'Storage explorer',
|
||||
})}
|
||||
href="/storage-explorer"
|
||||
>
|
||||
<ApmMainTemplate
|
||||
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>
|
||||
}
|
||||
>
|
||||
<StorageExplorer />
|
||||
</ApmMainTemplate>
|
||||
</Breadcrumb>
|
||||
),
|
||||
params: t.type({
|
||||
query: indexLifecyclePhaseRt,
|
||||
}),
|
||||
defaults: {
|
||||
query: {
|
||||
indexLifecyclePhase: IndexLifecyclePhaseSelectOption.All,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
|
@ -42,6 +42,16 @@ export function ApmHeaderActionMenu() {
|
|||
|
||||
return (
|
||||
<EuiHeaderLinks gutterSize="xs">
|
||||
<EuiHeaderLink
|
||||
color="text"
|
||||
href={apmHref('/storage-explorer')}
|
||||
iconType="beaker"
|
||||
data-test-subj="apmStorageExplorerHeaderLink"
|
||||
>
|
||||
{i18n.translate('xpack.apm.storageExplorerLinkLabel', {
|
||||
defaultMessage: 'Storage Explorer',
|
||||
})}
|
||||
</EuiHeaderLink>
|
||||
<EuiHeaderLink
|
||||
color="text"
|
||||
href={apmHref('/settings')}
|
||||
|
|
|
@ -24,6 +24,7 @@ export function isRouteWithTimeRange({
|
|||
route.path === '/dependencies/inventory' ||
|
||||
route.path === '/services/{serviceName}' ||
|
||||
route.path === '/service-groups' ||
|
||||
route.path === '/storage-explorer' ||
|
||||
location.pathname === '/' ||
|
||||
location.pathname === ''
|
||||
);
|
||||
|
|
|
@ -8,12 +8,14 @@
|
|||
import { Location } from 'history';
|
||||
import React from 'react';
|
||||
import { getRenderedHref } from '../../../../utils/test_helpers';
|
||||
import { APMLink } from './apm_link';
|
||||
import { LegacyAPMLink } from './apm_link';
|
||||
|
||||
describe('APMLink', () => {
|
||||
test('APMLink should produce the correct URL', async () => {
|
||||
describe('LegacyAPMLink', () => {
|
||||
test('LegacyAPMLink should produce the correct URL', async () => {
|
||||
const href = await getRenderedHref(
|
||||
() => <APMLink path="/some/path" query={{ transactionId: 'blah' }} />,
|
||||
() => (
|
||||
<LegacyAPMLink path="/some/path" query={{ transactionId: 'blah' }} />
|
||||
),
|
||||
{
|
||||
search:
|
||||
'?rangeFrom=now-5h&rangeTo=now-2h&refreshPaused=true&refreshInterval=0',
|
||||
|
@ -25,9 +27,11 @@ describe('APMLink', () => {
|
|||
);
|
||||
});
|
||||
|
||||
test('APMLink should retain current kuery value if it exists', async () => {
|
||||
test('LegacyAPMLink should retain current kuery value if it exists', async () => {
|
||||
const href = await getRenderedHref(
|
||||
() => <APMLink path="/some/path" query={{ transactionId: 'blah' }} />,
|
||||
() => (
|
||||
<LegacyAPMLink path="/some/path" query={{ transactionId: 'blah' }} />
|
||||
),
|
||||
{
|
||||
search:
|
||||
'?kuery=host.hostname~20~3A~20~22fakehostname~22&rangeFrom=now-5h&rangeTo=now-2h&refreshPaused=true&refreshInterval=0',
|
||||
|
@ -39,10 +43,10 @@ describe('APMLink', () => {
|
|||
);
|
||||
});
|
||||
|
||||
test('APMLink should overwrite current kuery value if new kuery value is provided', async () => {
|
||||
test('LegacyAPMLink should overwrite current kuery value if new kuery value is provided', async () => {
|
||||
const href = await getRenderedHref(
|
||||
() => (
|
||||
<APMLink
|
||||
<LegacyAPMLink
|
||||
path="/some/path"
|
||||
query={{ kuery: 'host.os~20~3A~20~22linux~22' }}
|
||||
/>
|
||||
|
|
|
@ -33,6 +33,7 @@ export const PERSISTENT_APM_PARAMS: Array<keyof APMQueryParams> = [
|
|||
'refreshInterval',
|
||||
'environment',
|
||||
'serviceGroup',
|
||||
'comparisonEnabled',
|
||||
];
|
||||
|
||||
/**
|
||||
|
@ -85,7 +86,12 @@ export function getLegacyApmHref({
|
|||
});
|
||||
}
|
||||
|
||||
export function APMLink({ path = '', query, mergeQuery, ...rest }: Props) {
|
||||
export function LegacyAPMLink({
|
||||
path = '',
|
||||
query,
|
||||
mergeQuery,
|
||||
...rest
|
||||
}: Props) {
|
||||
const { core } = useApmPluginContext();
|
||||
const { search } = useLocation();
|
||||
const { basePath } = core.http;
|
||||
|
|
|
@ -1,60 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { Location } from 'history';
|
||||
import React from 'react';
|
||||
import { getRenderedHref } from '../../../../utils/test_helpers';
|
||||
import { APMLink } from './apm_link';
|
||||
|
||||
describe('APMLink', () => {
|
||||
test('APMLink should produce the correct URL', async () => {
|
||||
const href = await getRenderedHref(
|
||||
() => <APMLink path="/some/path" query={{ transactionId: 'blah' }} />,
|
||||
{
|
||||
search:
|
||||
'?rangeFrom=now-5h&rangeTo=now-2h&refreshPaused=true&refreshInterval=0',
|
||||
} as Location
|
||||
);
|
||||
|
||||
expect(href).toMatchInlineSnapshot(
|
||||
`"/basepath/app/apm/some/path?rangeFrom=now-5h&rangeTo=now-2h&refreshPaused=true&refreshInterval=0&transactionId=blah"`
|
||||
);
|
||||
});
|
||||
|
||||
test('APMLink should retain current kuery value if it exists', async () => {
|
||||
const href = await getRenderedHref(
|
||||
() => <APMLink path="/some/path" query={{ transactionId: 'blah' }} />,
|
||||
{
|
||||
search:
|
||||
'?kuery=host.hostname~20~3A~20~22fakehostname~22&rangeFrom=now-5h&rangeTo=now-2h&refreshPaused=true&refreshInterval=0',
|
||||
} as Location
|
||||
);
|
||||
|
||||
expect(href).toMatchInlineSnapshot(
|
||||
`"/basepath/app/apm/some/path?kuery=host.hostname~20~3A~20~22fakehostname~22&rangeFrom=now-5h&rangeTo=now-2h&refreshPaused=true&refreshInterval=0&transactionId=blah"`
|
||||
);
|
||||
});
|
||||
|
||||
test('APMLink should overwrite current kuery value if new kuery value is provided', async () => {
|
||||
const href = await getRenderedHref(
|
||||
() => (
|
||||
<APMLink
|
||||
path="/some/path"
|
||||
query={{ kuery: 'host.os~20~3A~20~22linux~22' }}
|
||||
/>
|
||||
),
|
||||
{
|
||||
search:
|
||||
'?kuery=host.hostname~20~3A~20~22fakehostname~22&rangeFrom=now-5h&rangeTo=now-2h&refreshPaused=true&refreshInterval=0',
|
||||
} as Location
|
||||
);
|
||||
|
||||
expect(href).toMatchInlineSnapshot(
|
||||
`"/basepath/app/apm/some/path?kuery=host.os~20~3A~20~22linux~22&rangeFrom=now-5h&rangeTo=now-2h&refreshPaused=true&refreshInterval=0"`
|
||||
);
|
||||
});
|
||||
});
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { APMLink, APMLinkExtendProps } from './apm_link';
|
||||
import { LegacyAPMLink, APMLinkExtendProps } from './apm_link';
|
||||
|
||||
interface Props extends APMLinkExtendProps {
|
||||
serviceName: string;
|
||||
|
@ -15,7 +15,7 @@ interface Props extends APMLinkExtendProps {
|
|||
|
||||
function ErrorDetailLink({ serviceName, errorGroupId, ...rest }: Props) {
|
||||
return (
|
||||
<APMLink
|
||||
<LegacyAPMLink
|
||||
path={`/services/${serviceName}/errors/${errorGroupId}`}
|
||||
{...rest}
|
||||
/>
|
||||
|
|
|
@ -6,10 +6,10 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { APMLink, APMLinkExtendProps } from './apm_link';
|
||||
import { LegacyAPMLink, APMLinkExtendProps } from './apm_link';
|
||||
|
||||
function HomeLink(props: APMLinkExtendProps) {
|
||||
return <APMLink path="/" {...props} />;
|
||||
return <LegacyAPMLink path="/" {...props} />;
|
||||
}
|
||||
|
||||
export { HomeLink };
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import React from 'react';
|
||||
import { APMQueryParams } from '../url_helpers';
|
||||
import { APMLink, APMLinkExtendProps, useAPMHref } from './apm_link';
|
||||
import { LegacyAPMLink, APMLinkExtendProps, useAPMHref } from './apm_link';
|
||||
|
||||
const persistedFilters: Array<keyof APMQueryParams> = [
|
||||
'host',
|
||||
|
@ -28,5 +28,5 @@ interface Props extends APMLinkExtendProps {
|
|||
}
|
||||
|
||||
export function MetricOverviewLink({ serviceName, ...rest }: Props) {
|
||||
return <APMLink path={`/services/${serviceName}/metrics`} {...rest} />;
|
||||
return <LegacyAPMLink path={`/services/${serviceName}/metrics`} {...rest} />;
|
||||
}
|
||||
|
|
|
@ -16,7 +16,7 @@ import { i18n } from '@kbn/i18n';
|
|||
import React, { useState } from 'react';
|
||||
import { AnomalyDetectionSetupState } from '../../../../common/anomaly_detection/get_anomaly_detection_setup_state';
|
||||
import { useMlManageJobsHref } from '../../../hooks/use_ml_manage_jobs_href';
|
||||
import { APMLink } from '../links/apm/apm_link';
|
||||
import { LegacyAPMLink } from '../links/apm/apm_link';
|
||||
|
||||
export function shouldDisplayMlCallout(
|
||||
anomalyDetectionSetupState: AnomalyDetectionSetupState
|
||||
|
@ -58,7 +58,7 @@ export function MLCallout({
|
|||
|
||||
const getLearnMoreLink = (color: 'primary' | 'success') => (
|
||||
<EuiButton color={color}>
|
||||
<APMLink
|
||||
<LegacyAPMLink
|
||||
path="/settings/anomaly-detection"
|
||||
style={{ whiteSpace: 'nowrap' }}
|
||||
color={color}
|
||||
|
@ -66,7 +66,7 @@ export function MLCallout({
|
|||
{i18n.translate('xpack.apm.mlCallout.learnMoreButton', {
|
||||
defaultMessage: `Learn more`,
|
||||
})}
|
||||
</APMLink>
|
||||
</LegacyAPMLink>
|
||||
</EuiButton>
|
||||
);
|
||||
|
||||
|
|
|
@ -16,7 +16,7 @@ import {
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import { NO_PERMISSION_LABEL } from '../../../../../common/custom_link';
|
||||
import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plugin_context';
|
||||
import { APMLink } from '../../links/apm/apm_link';
|
||||
import { LegacyAPMLink } from '../../links/apm/apm_link';
|
||||
|
||||
export function CustomLinkToolbar({
|
||||
onClickCreate,
|
||||
|
@ -39,13 +39,13 @@ export function CustomLinkToolbar({
|
|||
defaultMessage: 'Manage custom links',
|
||||
})}
|
||||
>
|
||||
<APMLink path={`/settings/custom-links`}>
|
||||
<LegacyAPMLink path={`/settings/custom-links`}>
|
||||
<EuiIcon
|
||||
type="gear"
|
||||
color="text"
|
||||
aria-label="Custom links settings page"
|
||||
/>
|
||||
</APMLink>
|
||||
</LegacyAPMLink>
|
||||
</EuiToolTip>
|
||||
</EuiFlexItem>
|
||||
{showCreateButton && (
|
||||
|
|
|
@ -130,6 +130,13 @@ const apmSettingsTitle = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
const apmStorageExplorerTitle = i18n.translate(
|
||||
'xpack.apm.navigation.apmStorageExplorerTitle',
|
||||
{
|
||||
defaultMessage: 'Storage Explorer',
|
||||
}
|
||||
);
|
||||
|
||||
export class ApmPlugin implements Plugin<ApmPluginSetup, ApmPluginStart> {
|
||||
constructor(
|
||||
private readonly initializerContext: PluginInitializerContext<ConfigSchema>
|
||||
|
@ -311,6 +318,11 @@ export class ApmPlugin implements Plugin<ApmPluginSetup, ApmPluginStart> {
|
|||
path: '/dependencies/inventory',
|
||||
},
|
||||
{ id: 'settings', title: apmSettingsTitle, path: '/settings' },
|
||||
{
|
||||
id: 'storage-explorer',
|
||||
title: apmStorageExplorerTitle,
|
||||
path: '/storage-explorer',
|
||||
},
|
||||
],
|
||||
|
||||
async mount(appMountParameters: AppMountParameters<unknown>) {
|
||||
|
|
|
@ -40,6 +40,7 @@ import { suggestionsRouteRepository } from '../suggestions/route';
|
|||
import { timeRangeMetadataRoute } from '../time_range_metadata/route';
|
||||
import { traceRouteRepository } from '../traces/route';
|
||||
import { transactionRouteRepository } from '../transactions/route';
|
||||
import { storageExplorerRouteRepository } from '../storage_explorer/route';
|
||||
|
||||
function getTypedGlobalApmServerRouteRepository() {
|
||||
const repository = {
|
||||
|
@ -69,6 +70,7 @@ function getTypedGlobalApmServerRouteRepository() {
|
|||
...historicalDataRouteRepository,
|
||||
...eventMetadataRouteRepository,
|
||||
...agentKeysRouteRepository,
|
||||
...storageExplorerRouteRepository,
|
||||
...spanLinksRouteRepository,
|
||||
...infrastructureRouteRepository,
|
||||
...debugTelemetryRoute,
|
||||
|
|
|
@ -0,0 +1,241 @@
|
|||
/*
|
||||
* 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 { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
||||
import {
|
||||
termQuery,
|
||||
kqlQuery,
|
||||
rangeQuery,
|
||||
} from '@kbn/observability-plugin/server';
|
||||
import { ProcessorEvent } from '@kbn/observability-plugin/common';
|
||||
import { ApmPluginRequestHandlerContext } from '../typings';
|
||||
import { Setup } from '../../lib/helpers/setup_request';
|
||||
import {
|
||||
IndexLifecyclePhaseSelectOption,
|
||||
indexLifeCyclePhaseToDataTier,
|
||||
} from '../../../common/storage_explorer_types';
|
||||
import { getTotalTransactionsPerService } from './get_total_transactions_per_service';
|
||||
import {
|
||||
PROCESSOR_EVENT,
|
||||
SERVICE_NAME,
|
||||
SERVICE_ENVIRONMENT,
|
||||
TIER,
|
||||
TRANSACTION_SAMPLED,
|
||||
AGENT_NAME,
|
||||
INDEX,
|
||||
} from '../../../common/elasticsearch_fieldnames';
|
||||
import { environmentQuery } from '../../../common/utils/environment_query';
|
||||
import { AgentName } from '../../../typings/es_schemas/ui/fields/agent';
|
||||
import {
|
||||
getTotalIndicesStats,
|
||||
getEstimatedSizeForDocumentsInIndex,
|
||||
} from './indices_stats_helpers';
|
||||
import { RandomSampler } from '../../lib/helpers/get_random_sampler';
|
||||
|
||||
async function getMainServiceStatistics({
|
||||
setup,
|
||||
context,
|
||||
indexLifecyclePhase,
|
||||
randomSampler,
|
||||
start,
|
||||
end,
|
||||
environment,
|
||||
kuery,
|
||||
}: {
|
||||
setup: Setup;
|
||||
context: ApmPluginRequestHandlerContext;
|
||||
indexLifecyclePhase: IndexLifecyclePhaseSelectOption;
|
||||
randomSampler: RandomSampler;
|
||||
start: number;
|
||||
end: number;
|
||||
environment: string;
|
||||
kuery: string;
|
||||
}) {
|
||||
const { apmEventClient } = setup;
|
||||
|
||||
const [allIndicesStats, response] = await Promise.all([
|
||||
getTotalIndicesStats({ context, setup }),
|
||||
apmEventClient.search('get_main_service_statistics', {
|
||||
apm: {
|
||||
events: [
|
||||
ProcessorEvent.span,
|
||||
ProcessorEvent.transaction,
|
||||
ProcessorEvent.error,
|
||||
ProcessorEvent.metric,
|
||||
],
|
||||
},
|
||||
body: {
|
||||
size: 0,
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
...environmentQuery(environment),
|
||||
...kqlQuery(kuery),
|
||||
...rangeQuery(start, end),
|
||||
...(indexLifecyclePhase !== IndexLifecyclePhaseSelectOption.All
|
||||
? termQuery(
|
||||
TIER,
|
||||
indexLifeCyclePhaseToDataTier[indexLifecyclePhase]
|
||||
)
|
||||
: []),
|
||||
] as QueryDslQueryContainer[],
|
||||
},
|
||||
},
|
||||
aggs: {
|
||||
sample: {
|
||||
random_sampler: randomSampler,
|
||||
aggs: {
|
||||
services: {
|
||||
terms: {
|
||||
field: SERVICE_NAME,
|
||||
size: 500,
|
||||
},
|
||||
aggs: {
|
||||
sample: {
|
||||
top_metrics: {
|
||||
size: 1,
|
||||
metrics: { field: AGENT_NAME },
|
||||
sort: {
|
||||
'@timestamp': 'desc',
|
||||
},
|
||||
},
|
||||
},
|
||||
indices: {
|
||||
terms: {
|
||||
field: INDEX,
|
||||
size: 500,
|
||||
},
|
||||
aggs: {
|
||||
number_of_metric_docs: {
|
||||
value_count: {
|
||||
field: INDEX,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
environments: {
|
||||
terms: {
|
||||
field: SERVICE_ENVIRONMENT,
|
||||
},
|
||||
},
|
||||
transactions: {
|
||||
filter: {
|
||||
term: { [PROCESSOR_EVENT]: ProcessorEvent.transaction },
|
||||
},
|
||||
aggs: {
|
||||
sampled_transactions: {
|
||||
terms: {
|
||||
field: TRANSACTION_SAMPLED,
|
||||
size: 10,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
const serviceStats = response.aggregations?.sample.services.buckets.map(
|
||||
(bucket) => {
|
||||
const estimatedSize = allIndicesStats
|
||||
? bucket.indices.buckets.reduce((prev, curr) => {
|
||||
return (
|
||||
prev +
|
||||
getEstimatedSizeForDocumentsInIndex({
|
||||
allIndicesStats,
|
||||
indexName: curr.key as string,
|
||||
numberOfDocs: curr.number_of_metric_docs.value,
|
||||
})
|
||||
);
|
||||
}, 0)
|
||||
: 0;
|
||||
|
||||
return {
|
||||
serviceName: bucket.key as string,
|
||||
environments: bucket.environments.buckets.map(
|
||||
({ key }) => key as string
|
||||
),
|
||||
sampledTransactionDocs:
|
||||
bucket.transactions.sampled_transactions.buckets[0]?.doc_count,
|
||||
size: estimatedSize,
|
||||
agentName: bucket.sample.top[0]?.metrics[AGENT_NAME] as AgentName,
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
return serviceStats ?? [];
|
||||
}
|
||||
|
||||
export async function getServiceStatistics({
|
||||
setup,
|
||||
context,
|
||||
indexLifecyclePhase,
|
||||
randomSampler,
|
||||
start,
|
||||
end,
|
||||
environment,
|
||||
kuery,
|
||||
searchAggregatedTransactions,
|
||||
}: {
|
||||
setup: Setup;
|
||||
context: ApmPluginRequestHandlerContext;
|
||||
indexLifecyclePhase: IndexLifecyclePhaseSelectOption;
|
||||
randomSampler: RandomSampler;
|
||||
start: number;
|
||||
end: number;
|
||||
environment: string;
|
||||
kuery: string;
|
||||
searchAggregatedTransactions: boolean;
|
||||
}) {
|
||||
const [docCountPerProcessorEvent, totalTransactionsPerService] =
|
||||
await Promise.all([
|
||||
getMainServiceStatistics({
|
||||
setup,
|
||||
context,
|
||||
indexLifecyclePhase,
|
||||
randomSampler,
|
||||
environment,
|
||||
kuery,
|
||||
start,
|
||||
end,
|
||||
}),
|
||||
getTotalTransactionsPerService({
|
||||
setup,
|
||||
searchAggregatedTransactions,
|
||||
indexLifecyclePhase,
|
||||
randomSampler,
|
||||
environment,
|
||||
kuery,
|
||||
start,
|
||||
end,
|
||||
}),
|
||||
]);
|
||||
|
||||
const serviceStatistics = docCountPerProcessorEvent.map(
|
||||
({ serviceName, sampledTransactionDocs, ...rest }) => {
|
||||
const sampling =
|
||||
sampledTransactionDocs && totalTransactionsPerService[serviceName]
|
||||
? Math.min(
|
||||
sampledTransactionDocs / totalTransactionsPerService[serviceName],
|
||||
1
|
||||
)
|
||||
: 0;
|
||||
|
||||
return {
|
||||
...rest,
|
||||
serviceName,
|
||||
sampling,
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
return serviceStatistics;
|
||||
}
|
|
@ -0,0 +1,157 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
||||
import {
|
||||
termQuery,
|
||||
kqlQuery,
|
||||
rangeQuery,
|
||||
} from '@kbn/observability-plugin/server';
|
||||
import { ProcessorEvent } from '@kbn/observability-plugin/common';
|
||||
import { Setup } from '../../lib/helpers/setup_request';
|
||||
import {
|
||||
PROCESSOR_EVENT,
|
||||
SERVICE_NAME,
|
||||
TIER,
|
||||
INDEX,
|
||||
} from '../../../common/elasticsearch_fieldnames';
|
||||
import {
|
||||
IndexLifecyclePhaseSelectOption,
|
||||
indexLifeCyclePhaseToDataTier,
|
||||
} from '../../../common/storage_explorer_types';
|
||||
import { environmentQuery } from '../../../common/utils/environment_query';
|
||||
import { ApmPluginRequestHandlerContext } from '../typings';
|
||||
import {
|
||||
getTotalIndicesStats,
|
||||
getEstimatedSizeForDocumentsInIndex,
|
||||
} from './indices_stats_helpers';
|
||||
import { RandomSampler } from '../../lib/helpers/get_random_sampler';
|
||||
|
||||
export async function getStorageDetailsPerProcessorEvent({
|
||||
setup,
|
||||
context,
|
||||
indexLifecyclePhase,
|
||||
randomSampler,
|
||||
start,
|
||||
end,
|
||||
environment,
|
||||
kuery,
|
||||
serviceName,
|
||||
}: {
|
||||
setup: Setup;
|
||||
context: ApmPluginRequestHandlerContext;
|
||||
indexLifecyclePhase: IndexLifecyclePhaseSelectOption;
|
||||
randomSampler: RandomSampler;
|
||||
start: number;
|
||||
end: number;
|
||||
environment: string;
|
||||
kuery: string;
|
||||
serviceName: string;
|
||||
}) {
|
||||
const { apmEventClient } = setup;
|
||||
|
||||
const [allIndicesStats, response] = await Promise.all([
|
||||
getTotalIndicesStats({ setup, context }),
|
||||
apmEventClient.search('get_storage_details_per_processor_event', {
|
||||
apm: {
|
||||
events: [
|
||||
ProcessorEvent.span,
|
||||
ProcessorEvent.transaction,
|
||||
ProcessorEvent.error,
|
||||
ProcessorEvent.metric,
|
||||
],
|
||||
},
|
||||
body: {
|
||||
size: 0,
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
...environmentQuery(environment),
|
||||
...kqlQuery(kuery),
|
||||
...rangeQuery(start, end),
|
||||
...termQuery(SERVICE_NAME, serviceName),
|
||||
...(indexLifecyclePhase !== IndexLifecyclePhaseSelectOption.All
|
||||
? termQuery(
|
||||
TIER,
|
||||
indexLifeCyclePhaseToDataTier[indexLifecyclePhase]
|
||||
)
|
||||
: []),
|
||||
] as QueryDslQueryContainer[],
|
||||
},
|
||||
},
|
||||
aggs: {
|
||||
sample: {
|
||||
random_sampler: randomSampler,
|
||||
aggs: {
|
||||
processor_event: {
|
||||
terms: {
|
||||
field: PROCESSOR_EVENT,
|
||||
size: 10,
|
||||
},
|
||||
aggs: {
|
||||
number_of_metric_docs_for_processor_event: {
|
||||
value_count: {
|
||||
field: PROCESSOR_EVENT,
|
||||
},
|
||||
},
|
||||
indices: {
|
||||
terms: {
|
||||
field: INDEX,
|
||||
size: 500,
|
||||
},
|
||||
aggs: {
|
||||
number_of_metric_docs_for_index: {
|
||||
value_count: {
|
||||
field: INDEX,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
return [
|
||||
ProcessorEvent.transaction,
|
||||
ProcessorEvent.span,
|
||||
ProcessorEvent.metric,
|
||||
ProcessorEvent.error,
|
||||
].map((processorEvent) => {
|
||||
const bucketForProcessorEvent =
|
||||
response.aggregations?.sample.processor_event.buckets?.find(
|
||||
(x) => x.key === processorEvent
|
||||
);
|
||||
|
||||
return {
|
||||
processorEvent: processorEvent as Exclude<
|
||||
ProcessorEvent,
|
||||
ProcessorEvent.profile
|
||||
>,
|
||||
docs:
|
||||
bucketForProcessorEvent?.number_of_metric_docs_for_processor_event
|
||||
.value ?? 0,
|
||||
size:
|
||||
allIndicesStats && bucketForProcessorEvent
|
||||
? bucketForProcessorEvent.indices.buckets.reduce((prev, curr) => {
|
||||
return (
|
||||
prev +
|
||||
getEstimatedSizeForDocumentsInIndex({
|
||||
allIndicesStats,
|
||||
indexName: curr.key as string,
|
||||
numberOfDocs: curr.number_of_metric_docs_for_index.value,
|
||||
})
|
||||
);
|
||||
}, 0)
|
||||
: 0,
|
||||
};
|
||||
});
|
||||
}
|
|
@ -0,0 +1,101 @@
|
|||
/*
|
||||
* 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 { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
||||
import {
|
||||
termQuery,
|
||||
kqlQuery,
|
||||
rangeQuery,
|
||||
} from '@kbn/observability-plugin/server';
|
||||
import { Setup } from '../../lib/helpers/setup_request';
|
||||
import {
|
||||
getProcessorEventForTransactions,
|
||||
getDocumentTypeFilterForTransactions,
|
||||
} from '../../lib/helpers/transactions';
|
||||
import { SERVICE_NAME, TIER } from '../../../common/elasticsearch_fieldnames';
|
||||
import {
|
||||
IndexLifecyclePhaseSelectOption,
|
||||
indexLifeCyclePhaseToDataTier,
|
||||
} from '../../../common/storage_explorer_types';
|
||||
import { environmentQuery } from '../../../common/utils/environment_query';
|
||||
import { RandomSampler } from '../../lib/helpers/get_random_sampler';
|
||||
|
||||
export async function getTotalTransactionsPerService({
|
||||
setup,
|
||||
searchAggregatedTransactions,
|
||||
indexLifecyclePhase,
|
||||
randomSampler,
|
||||
start,
|
||||
end,
|
||||
environment,
|
||||
kuery,
|
||||
}: {
|
||||
setup: Setup;
|
||||
searchAggregatedTransactions: boolean;
|
||||
indexLifecyclePhase: IndexLifecyclePhaseSelectOption;
|
||||
randomSampler: RandomSampler;
|
||||
start: number;
|
||||
end: number;
|
||||
environment: string;
|
||||
kuery: string;
|
||||
}) {
|
||||
const { apmEventClient } = setup;
|
||||
|
||||
const response = await apmEventClient.search(
|
||||
'get_total_transactions_per_service',
|
||||
{
|
||||
apm: {
|
||||
events: [
|
||||
getProcessorEventForTransactions(searchAggregatedTransactions),
|
||||
],
|
||||
},
|
||||
body: {
|
||||
size: 0,
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
...getDocumentTypeFilterForTransactions(
|
||||
searchAggregatedTransactions
|
||||
),
|
||||
...environmentQuery(environment),
|
||||
...kqlQuery(kuery),
|
||||
...rangeQuery(start, end),
|
||||
...(indexLifecyclePhase !== IndexLifecyclePhaseSelectOption.All
|
||||
? termQuery(
|
||||
TIER,
|
||||
indexLifeCyclePhaseToDataTier[indexLifecyclePhase]
|
||||
)
|
||||
: []),
|
||||
] as QueryDslQueryContainer[],
|
||||
},
|
||||
},
|
||||
aggs: {
|
||||
sample: {
|
||||
random_sampler: randomSampler,
|
||||
aggs: {
|
||||
services: {
|
||||
terms: {
|
||||
field: SERVICE_NAME,
|
||||
size: 500,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
response.aggregations?.sample.services.buckets.reduce(
|
||||
(transactionsPerService, bucket) => {
|
||||
transactionsPerService[bucket.key as string] = bucket.doc_count;
|
||||
return transactionsPerService;
|
||||
},
|
||||
{} as Record<string, number>
|
||||
) ?? {}
|
||||
);
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
* 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 { uniq } from 'lodash';
|
||||
import { IndicesStatsIndicesStats } from '@elastic/elasticsearch/lib/api/types';
|
||||
import { Setup } from '../../lib/helpers/setup_request';
|
||||
import { ApmPluginRequestHandlerContext } from '../typings';
|
||||
|
||||
export async function getTotalIndicesStats({
|
||||
context,
|
||||
setup,
|
||||
}: {
|
||||
context: ApmPluginRequestHandlerContext;
|
||||
setup: Setup;
|
||||
}) {
|
||||
const {
|
||||
indices: { transaction, span, metric, error },
|
||||
} = setup;
|
||||
const index = uniq([transaction, span, metric, error]).join();
|
||||
const esClient = (await context.core).elasticsearch.client;
|
||||
const indicesStats = (await esClient.asCurrentUser.indices.stats({ index }))
|
||||
.indices;
|
||||
|
||||
return indicesStats;
|
||||
}
|
||||
|
||||
export function getEstimatedSizeForDocumentsInIndex({
|
||||
allIndicesStats,
|
||||
indexName,
|
||||
numberOfDocs,
|
||||
}: {
|
||||
allIndicesStats: Record<string, IndicesStatsIndicesStats>;
|
||||
indexName: string;
|
||||
numberOfDocs: number;
|
||||
}) {
|
||||
const indexStats = allIndicesStats[indexName];
|
||||
const indexTotalSize = indexStats?.total?.store?.size_in_bytes ?? 0;
|
||||
const indexTotalDocCount = indexStats?.total?.docs?.count;
|
||||
|
||||
const estimatedSize = indexTotalDocCount
|
||||
? (numberOfDocs / indexTotalDocCount) * indexTotalSize
|
||||
: 0;
|
||||
|
||||
return estimatedSize;
|
||||
}
|
166
x-pack/plugins/apm/server/routes/storage_explorer/route.ts
Normal file
166
x-pack/plugins/apm/server/routes/storage_explorer/route.ts
Normal file
|
@ -0,0 +1,166 @@
|
|||
/*
|
||||
* 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 * as t from 'io-ts';
|
||||
import { ProcessorEvent } from '@kbn/observability-plugin/common';
|
||||
import { createApmServerRoute } from '../apm_routes/create_apm_server_route';
|
||||
import { getSearchAggregatedTransactions } from '../../lib/helpers/transactions';
|
||||
import { setupRequest } from '../../lib/helpers/setup_request';
|
||||
import { indexLifecyclePhaseRt } from '../../../common/storage_explorer_types';
|
||||
import { getServiceStatistics } from './get_service_statistics';
|
||||
import {
|
||||
probabilityRt,
|
||||
environmentRt,
|
||||
kueryRt,
|
||||
rangeRt,
|
||||
} from '../default_api_types';
|
||||
import { AgentName } from '../../../typings/es_schemas/ui/fields/agent';
|
||||
import { getStorageDetailsPerProcessorEvent } from './get_storage_details_per_processor_event';
|
||||
import { getRandomSampler } from '../../lib/helpers/get_random_sampler';
|
||||
|
||||
const storageExplorerRoute = createApmServerRoute({
|
||||
endpoint: 'GET /internal/apm/storage_explorer',
|
||||
options: { tags: ['access:apm'] },
|
||||
params: t.type({
|
||||
query: t.intersection([
|
||||
indexLifecyclePhaseRt,
|
||||
probabilityRt,
|
||||
environmentRt,
|
||||
kueryRt,
|
||||
rangeRt,
|
||||
]),
|
||||
}),
|
||||
handler: async (
|
||||
resources
|
||||
): Promise<{
|
||||
serviceStatistics: Array<{
|
||||
serviceName: string;
|
||||
environments: string[];
|
||||
size?: number;
|
||||
agentName: AgentName;
|
||||
sampling: number;
|
||||
}>;
|
||||
}> => {
|
||||
const {
|
||||
params,
|
||||
context,
|
||||
request,
|
||||
plugins: { security },
|
||||
} = resources;
|
||||
|
||||
const {
|
||||
query: {
|
||||
indexLifecyclePhase,
|
||||
probability,
|
||||
environment,
|
||||
kuery,
|
||||
start,
|
||||
end,
|
||||
},
|
||||
} = params;
|
||||
|
||||
const [setup, randomSampler] = await Promise.all([
|
||||
setupRequest(resources),
|
||||
getRandomSampler({ security, request, probability }),
|
||||
]);
|
||||
|
||||
const searchAggregatedTransactions = await getSearchAggregatedTransactions({
|
||||
apmEventClient: setup.apmEventClient,
|
||||
config: setup.config,
|
||||
kuery,
|
||||
});
|
||||
|
||||
const serviceStatistics = await getServiceStatistics({
|
||||
setup,
|
||||
context,
|
||||
indexLifecyclePhase,
|
||||
randomSampler,
|
||||
environment,
|
||||
kuery,
|
||||
start,
|
||||
end,
|
||||
searchAggregatedTransactions,
|
||||
});
|
||||
|
||||
return {
|
||||
serviceStatistics,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
const storageExplorerServiceDetailsRoute = createApmServerRoute({
|
||||
endpoint: 'GET /internal/apm/services/{serviceName}/storage_details',
|
||||
options: { tags: ['access:apm'] },
|
||||
params: t.type({
|
||||
path: t.type({
|
||||
serviceName: t.string,
|
||||
}),
|
||||
query: t.intersection([
|
||||
indexLifecyclePhaseRt,
|
||||
probabilityRt,
|
||||
environmentRt,
|
||||
kueryRt,
|
||||
rangeRt,
|
||||
]),
|
||||
}),
|
||||
handler: async (
|
||||
resources
|
||||
): Promise<{
|
||||
processorEventStats: Array<{
|
||||
processorEvent:
|
||||
| ProcessorEvent.transaction
|
||||
| ProcessorEvent.error
|
||||
| ProcessorEvent.metric
|
||||
| ProcessorEvent.span;
|
||||
docs: number;
|
||||
size: number;
|
||||
}>;
|
||||
}> => {
|
||||
const {
|
||||
params,
|
||||
context,
|
||||
request,
|
||||
plugins: { security },
|
||||
} = resources;
|
||||
|
||||
const {
|
||||
path: { serviceName },
|
||||
query: {
|
||||
indexLifecyclePhase,
|
||||
probability,
|
||||
environment,
|
||||
kuery,
|
||||
start,
|
||||
end,
|
||||
},
|
||||
} = params;
|
||||
|
||||
const [setup, randomSampler] = await Promise.all([
|
||||
setupRequest(resources),
|
||||
getRandomSampler({ security, request, probability }),
|
||||
]);
|
||||
|
||||
const processorEventStats = await getStorageDetailsPerProcessorEvent({
|
||||
setup,
|
||||
context,
|
||||
indexLifecyclePhase,
|
||||
randomSampler,
|
||||
environment,
|
||||
kuery,
|
||||
start,
|
||||
end,
|
||||
serviceName,
|
||||
});
|
||||
|
||||
return { processorEventStats };
|
||||
},
|
||||
});
|
||||
|
||||
export const storageExplorerRouteRepository = {
|
||||
...storageExplorerRoute,
|
||||
...storageExplorerServiceDetailsRoute,
|
||||
};
|
Loading…
Add table
Add a link
Reference in a new issue