mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[APM] Display kubernetes metadata in service icons popup and node instance accordion (#139612)
* Extend endpoints to include containers metadata info * Display kubernetes metadata in icons popover * Display kubernetes metadata in node instance accordion * Fix translations * Fix types * Fix unit tests * Fix import order * Update storybooks * Hide labels if fields are empty * Display OS field and remove labels * CSS tweaks for aligning fields * Clean up types * Add API test for kubernetes metadata * Reword showFilterByOption to isFilterable * Use top_metrics aggs for kubernetes and container metadata * Rename getMetricIndices to getInfraMetricIndices * Fix lint errors * Fetching metadata of specific container * Clean up code * Service metrics on inventory page * specify size * Clarify the type of metric Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
cf4312658c
commit
9673ead47b
29 changed files with 18439 additions and 143 deletions
|
@ -37,8 +37,12 @@ exports[`Error CLOUD_REGION 1`] = `"europe-west1"`;
|
|||
|
||||
exports[`Error CLOUD_SERVICE_NAME 1`] = `undefined`;
|
||||
|
||||
exports[`Error CONTAINER 1`] = `undefined`;
|
||||
|
||||
exports[`Error CONTAINER_ID 1`] = `undefined`;
|
||||
|
||||
exports[`Error CONTAINER_IMAGE 1`] = `undefined`;
|
||||
|
||||
exports[`Error DESTINATION_ADDRESS 1`] = `undefined`;
|
||||
|
||||
exports[`Error ERROR_CULPRIT 1`] = `"handleOopsie"`;
|
||||
|
@ -93,6 +97,24 @@ exports[`Error INDEX 1`] = `undefined`;
|
|||
|
||||
exports[`Error KUBERNETES 1`] = `undefined`;
|
||||
|
||||
exports[`Error KUBERNETES_CONTAINER_NAME 1`] = `undefined`;
|
||||
|
||||
exports[`Error KUBERNETES_DEPLOYMENT 1`] = `undefined`;
|
||||
|
||||
exports[`Error KUBERNETES_DEPLOYMENT_NAME 1`] = `undefined`;
|
||||
|
||||
exports[`Error KUBERNETES_NAMESPACE 1`] = `undefined`;
|
||||
|
||||
exports[`Error KUBERNETES_NAMESPACE_NAME 1`] = `undefined`;
|
||||
|
||||
exports[`Error KUBERNETES_POD_NAME 1`] = `undefined`;
|
||||
|
||||
exports[`Error KUBERNETES_POD_UID 1`] = `undefined`;
|
||||
|
||||
exports[`Error KUBERNETES_REPLICASET 1`] = `undefined`;
|
||||
|
||||
exports[`Error KUBERNETES_REPLICASET_NAME 1`] = `undefined`;
|
||||
|
||||
exports[`Error LABEL_NAME 1`] = `undefined`;
|
||||
|
||||
exports[`Error METRIC_CGROUP_MEMORY_LIMIT_BYTES 1`] = `undefined`;
|
||||
|
@ -133,8 +155,6 @@ exports[`Error OBSERVER_LISTENING 1`] = `undefined`;
|
|||
|
||||
exports[`Error PARENT_ID 1`] = `"parentId"`;
|
||||
|
||||
exports[`Error POD_NAME 1`] = `undefined`;
|
||||
|
||||
exports[`Error PROCESSOR_EVENT 1`] = `"error"`;
|
||||
|
||||
exports[`Error SERVICE 1`] = `
|
||||
|
@ -266,8 +286,12 @@ exports[`Span CLOUD_REGION 1`] = `"europe-west1"`;
|
|||
|
||||
exports[`Span CLOUD_SERVICE_NAME 1`] = `undefined`;
|
||||
|
||||
exports[`Span CONTAINER 1`] = `undefined`;
|
||||
|
||||
exports[`Span CONTAINER_ID 1`] = `undefined`;
|
||||
|
||||
exports[`Span CONTAINER_IMAGE 1`] = `undefined`;
|
||||
|
||||
exports[`Span DESTINATION_ADDRESS 1`] = `undefined`;
|
||||
|
||||
exports[`Span ERROR_CULPRIT 1`] = `undefined`;
|
||||
|
@ -318,6 +342,24 @@ exports[`Span INDEX 1`] = `undefined`;
|
|||
|
||||
exports[`Span KUBERNETES 1`] = `undefined`;
|
||||
|
||||
exports[`Span KUBERNETES_CONTAINER_NAME 1`] = `undefined`;
|
||||
|
||||
exports[`Span KUBERNETES_DEPLOYMENT 1`] = `undefined`;
|
||||
|
||||
exports[`Span KUBERNETES_DEPLOYMENT_NAME 1`] = `undefined`;
|
||||
|
||||
exports[`Span KUBERNETES_NAMESPACE 1`] = `undefined`;
|
||||
|
||||
exports[`Span KUBERNETES_NAMESPACE_NAME 1`] = `undefined`;
|
||||
|
||||
exports[`Span KUBERNETES_POD_NAME 1`] = `undefined`;
|
||||
|
||||
exports[`Span KUBERNETES_POD_UID 1`] = `undefined`;
|
||||
|
||||
exports[`Span KUBERNETES_REPLICASET 1`] = `undefined`;
|
||||
|
||||
exports[`Span KUBERNETES_REPLICASET_NAME 1`] = `undefined`;
|
||||
|
||||
exports[`Span LABEL_NAME 1`] = `undefined`;
|
||||
|
||||
exports[`Span METRIC_CGROUP_MEMORY_LIMIT_BYTES 1`] = `undefined`;
|
||||
|
@ -358,8 +400,6 @@ exports[`Span OBSERVER_LISTENING 1`] = `undefined`;
|
|||
|
||||
exports[`Span PARENT_ID 1`] = `"parentId"`;
|
||||
|
||||
exports[`Span POD_NAME 1`] = `undefined`;
|
||||
|
||||
exports[`Span PROCESSOR_EVENT 1`] = `"span"`;
|
||||
|
||||
exports[`Span SERVICE 1`] = `
|
||||
|
@ -487,8 +527,16 @@ exports[`Transaction CLOUD_REGION 1`] = `"europe-west1"`;
|
|||
|
||||
exports[`Transaction CLOUD_SERVICE_NAME 1`] = `undefined`;
|
||||
|
||||
exports[`Transaction CONTAINER 1`] = `
|
||||
Object {
|
||||
"id": "container1234567890abcdef",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Transaction CONTAINER_ID 1`] = `"container1234567890abcdef"`;
|
||||
|
||||
exports[`Transaction CONTAINER_IMAGE 1`] = `undefined`;
|
||||
|
||||
exports[`Transaction DESTINATION_ADDRESS 1`] = `undefined`;
|
||||
|
||||
exports[`Transaction ERROR_CULPRIT 1`] = `undefined`;
|
||||
|
@ -549,6 +597,24 @@ Object {
|
|||
}
|
||||
`;
|
||||
|
||||
exports[`Transaction KUBERNETES_CONTAINER_NAME 1`] = `undefined`;
|
||||
|
||||
exports[`Transaction KUBERNETES_DEPLOYMENT 1`] = `undefined`;
|
||||
|
||||
exports[`Transaction KUBERNETES_DEPLOYMENT_NAME 1`] = `undefined`;
|
||||
|
||||
exports[`Transaction KUBERNETES_NAMESPACE 1`] = `undefined`;
|
||||
|
||||
exports[`Transaction KUBERNETES_NAMESPACE_NAME 1`] = `undefined`;
|
||||
|
||||
exports[`Transaction KUBERNETES_POD_NAME 1`] = `undefined`;
|
||||
|
||||
exports[`Transaction KUBERNETES_POD_UID 1`] = `"pod1234567890abcdef"`;
|
||||
|
||||
exports[`Transaction KUBERNETES_REPLICASET 1`] = `undefined`;
|
||||
|
||||
exports[`Transaction KUBERNETES_REPLICASET_NAME 1`] = `undefined`;
|
||||
|
||||
exports[`Transaction LABEL_NAME 1`] = `undefined`;
|
||||
|
||||
exports[`Transaction METRIC_CGROUP_MEMORY_LIMIT_BYTES 1`] = `undefined`;
|
||||
|
@ -589,8 +655,6 @@ exports[`Transaction OBSERVER_LISTENING 1`] = `undefined`;
|
|||
|
||||
exports[`Transaction PARENT_ID 1`] = `"parentId"`;
|
||||
|
||||
exports[`Transaction POD_NAME 1`] = `undefined`;
|
||||
|
||||
exports[`Transaction PROCESSOR_EVENT 1`] = `"transaction"`;
|
||||
|
||||
exports[`Transaction SERVICE 1`] = `
|
||||
|
|
|
@ -121,8 +121,20 @@ export const HOST_HOSTNAME = 'host.hostname'; // Do not use. Please use `HOST_NA
|
|||
export const HOST_NAME = 'host.name';
|
||||
export const HOST_OS_PLATFORM = 'host.os.platform';
|
||||
export const CONTAINER_ID = 'container.id';
|
||||
export const CONTAINER = 'container';
|
||||
export const CONTAINER_IMAGE = 'container.image.name';
|
||||
|
||||
// Kubernetes
|
||||
export const KUBERNETES = 'kubernetes';
|
||||
export const POD_NAME = 'kubernetes.pod.name';
|
||||
export const KUBERNETES_CONTAINER_NAME = 'kubernetes.container.name';
|
||||
export const KUBERNETES_DEPLOYMENT = 'kubernetes.deployment';
|
||||
export const KUBERNETES_DEPLOYMENT_NAME = 'kubernetes.deployment.name';
|
||||
export const KUBERNETES_NAMESPACE_NAME = 'kubernetes.namespace.name';
|
||||
export const KUBERNETES_NAMESPACE = 'kubernetes.namespace';
|
||||
export const KUBERNETES_POD_NAME = 'kubernetes.pod.name';
|
||||
export const KUBERNETES_POD_UID = 'kubernetes.pod.uid';
|
||||
export const KUBERNETES_REPLICASET = 'kubernetes.replicaset';
|
||||
export const KUBERNETES_REPLICASET_NAME = 'kubernetes.replicaset.name';
|
||||
|
||||
export const CLIENT_GEO_COUNTRY_ISO_CODE = 'client.geo.country_iso_code';
|
||||
|
||||
|
|
|
@ -18,12 +18,18 @@ import {
|
|||
CLOUD_PROVIDER,
|
||||
CONTAINER_ID,
|
||||
HOST_NAME,
|
||||
POD_NAME,
|
||||
SERVICE_NODE_NAME,
|
||||
SERVICE_RUNTIME_NAME,
|
||||
SERVICE_RUNTIME_VERSION,
|
||||
SERVICE_VERSION,
|
||||
KUBERNETES_CONTAINER_NAME,
|
||||
KUBERNETES_NAMESPACE,
|
||||
KUBERNETES_POD_NAME,
|
||||
KUBERNETES_POD_UID,
|
||||
KUBERNETES_REPLICASET_NAME,
|
||||
KUBERNETES_DEPLOYMENT_NAME,
|
||||
} from '../../../../../common/elasticsearch_fieldnames';
|
||||
|
||||
import { FETCH_STATUS } from '../../../../hooks/use_fetcher';
|
||||
import { useTheme } from '../../../../hooks/use_theme';
|
||||
import { APIReturnType } from '../../../../services/rest/create_call_apm_api';
|
||||
|
@ -42,8 +48,20 @@ interface Props {
|
|||
kuery: string;
|
||||
}
|
||||
|
||||
function toKeyValuePairs(keys: string[], data: ServiceInstanceDetails) {
|
||||
return keys.map((key) => ({ key, value: get(data, key) }));
|
||||
function toKeyValuePairs({
|
||||
keys,
|
||||
data,
|
||||
isFilterable = true,
|
||||
}: {
|
||||
keys: string[];
|
||||
data: ServiceInstanceDetails;
|
||||
isFilterable?: boolean;
|
||||
}) {
|
||||
return keys.map((key) => ({
|
||||
key,
|
||||
value: get(data, key),
|
||||
isFilterable,
|
||||
}));
|
||||
}
|
||||
|
||||
const serviceDetailsKeys = [
|
||||
|
@ -52,7 +70,18 @@ const serviceDetailsKeys = [
|
|||
SERVICE_RUNTIME_NAME,
|
||||
SERVICE_RUNTIME_VERSION,
|
||||
];
|
||||
const containerDetailsKeys = [CONTAINER_ID, HOST_NAME, POD_NAME];
|
||||
const containerDetailsKeys = [
|
||||
CONTAINER_ID,
|
||||
HOST_NAME,
|
||||
KUBERNETES_POD_UID,
|
||||
KUBERNETES_POD_NAME,
|
||||
];
|
||||
const metricsKubernetesDetailsKeys = [
|
||||
KUBERNETES_CONTAINER_NAME,
|
||||
KUBERNETES_NAMESPACE,
|
||||
KUBERNETES_REPLICASET_NAME,
|
||||
KUBERNETES_DEPLOYMENT_NAME,
|
||||
];
|
||||
const cloudDetailsKeys = [
|
||||
CLOUD_AVAILABILITY_ZONE,
|
||||
CLOUD_INSTANCE_ID,
|
||||
|
@ -93,12 +122,23 @@ export function InstanceDetails({
|
|||
pushNewItemToKueryBar({ kuery, history, key, value });
|
||||
};
|
||||
|
||||
const serviceDetailsKeyValuePairs = toKeyValuePairs(serviceDetailsKeys, data);
|
||||
const containerDetailsKeyValuePairs = toKeyValuePairs(
|
||||
containerDetailsKeys,
|
||||
data
|
||||
);
|
||||
const cloudDetailsKeyValuePairs = toKeyValuePairs(cloudDetailsKeys, data);
|
||||
const serviceDetailsKeyValuePairs = toKeyValuePairs({
|
||||
keys: serviceDetailsKeys,
|
||||
data,
|
||||
});
|
||||
const containerDetailsKeyValuePairs = toKeyValuePairs({
|
||||
keys: containerDetailsKeys,
|
||||
data,
|
||||
});
|
||||
const metricsKubernetesKeyValuePairs = toKeyValuePairs({
|
||||
keys: metricsKubernetesDetailsKeys,
|
||||
data,
|
||||
isFilterable: false,
|
||||
});
|
||||
const cloudDetailsKeyValuePairs = toKeyValuePairs({
|
||||
keys: cloudDetailsKeys,
|
||||
data,
|
||||
});
|
||||
|
||||
const containerType = data.kubernetes?.pod?.name ? 'Kubernetes' : 'Docker';
|
||||
return (
|
||||
|
@ -122,7 +162,10 @@ export function InstanceDetails({
|
|||
{ defaultMessage: 'Container' }
|
||||
)}
|
||||
icon={getContainerIcon(containerType)}
|
||||
keyValueList={containerDetailsKeyValuePairs}
|
||||
keyValueList={[
|
||||
...containerDetailsKeyValuePairs,
|
||||
...metricsKubernetesKeyValuePairs,
|
||||
]}
|
||||
onClickFilter={addKueryBarFilter}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
|
|
|
@ -19,10 +19,12 @@ import {
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import React, { Fragment } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { isEmpty } from 'lodash';
|
||||
|
||||
interface KeyValue {
|
||||
key: string;
|
||||
value: any | undefined;
|
||||
isFilterable: boolean;
|
||||
}
|
||||
|
||||
const StyledEuiAccordion = styled(EuiAccordion)`
|
||||
|
@ -43,13 +45,8 @@ const StyledEuiDescriptionList = styled(EuiDescriptionList)`
|
|||
display: flex;
|
||||
`;
|
||||
|
||||
const ValueContainer = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
function removeEmptyValues(items: KeyValue[]) {
|
||||
return items.filter(({ value }) => value !== undefined);
|
||||
return items.filter(({ value }) => !isEmpty(value));
|
||||
}
|
||||
|
||||
export function KeyValueFilterList({
|
||||
|
@ -78,12 +75,12 @@ export function KeyValueFilterList({
|
|||
buttonClassName="buttonContentContainer"
|
||||
>
|
||||
<StyledEuiDescriptionList type="column">
|
||||
{nonEmptyKeyValueList.map(({ key, value }) => {
|
||||
{nonEmptyKeyValueList.map(({ key, value, isFilterable }) => {
|
||||
return (
|
||||
<Fragment key={key}>
|
||||
<EuiDescriptionListTitle
|
||||
className="descriptionList__title"
|
||||
style={{ width: '20%' }}
|
||||
style={{ width: '20%', height: '40px' }}
|
||||
>
|
||||
<EuiText size="s" style={{ fontWeight: 'bold' }}>
|
||||
{key}
|
||||
|
@ -91,27 +88,37 @@ export function KeyValueFilterList({
|
|||
</EuiDescriptionListTitle>
|
||||
<EuiDescriptionListDescription
|
||||
className="descriptionList__description"
|
||||
style={{ width: '80%' }}
|
||||
style={{ width: '80%', height: '40px' }}
|
||||
>
|
||||
<ValueContainer>
|
||||
<EuiButtonEmpty
|
||||
onClick={() => {
|
||||
onClickFilter({ key, value });
|
||||
}}
|
||||
data-test-subj={`filter_by_${key}`}
|
||||
>
|
||||
<EuiToolTip
|
||||
position="top"
|
||||
content={i18n.translate(
|
||||
'xpack.apm.keyValueFilterList.actionFilterLabel',
|
||||
{ defaultMessage: 'Filter by value' }
|
||||
)}
|
||||
>
|
||||
<EuiIcon type="filter" color="text" size="m" />
|
||||
</EuiToolTip>
|
||||
</EuiButtonEmpty>
|
||||
<EuiText size="s">{value}</EuiText>
|
||||
</ValueContainer>
|
||||
<EuiFlexGroup
|
||||
alignItems="baseline"
|
||||
responsive={false}
|
||||
gutterSize="none"
|
||||
>
|
||||
<EuiFlexItem style={{ minWidth: '32px' }} grow={false}>
|
||||
{isFilterable && (
|
||||
<EuiButtonEmpty
|
||||
onClick={() => {
|
||||
onClickFilter({ key, value });
|
||||
}}
|
||||
data-test-subj={`filter_by_${key}`}
|
||||
>
|
||||
<EuiToolTip
|
||||
position="top"
|
||||
content={i18n.translate(
|
||||
'xpack.apm.keyValueFilterList.actionFilterLabel',
|
||||
{ defaultMessage: 'Filter by value' }
|
||||
)}
|
||||
>
|
||||
<EuiIcon type="filter" color="text" size="m" />
|
||||
</EuiToolTip>
|
||||
</EuiButtonEmpty>
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText size="s">{value}</EuiText>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiDescriptionListDescription>
|
||||
</Fragment>
|
||||
);
|
||||
|
|
|
@ -28,8 +28,8 @@ describe('KeyValueFilterList', () => {
|
|||
<KeyValueFilterList
|
||||
title="title"
|
||||
keyValueList={[
|
||||
{ key: 'foo', value: 'foo value' },
|
||||
{ key: 'bar', value: 'bar value' },
|
||||
{ key: 'foo', value: 'foo value', isFilterable: true },
|
||||
{ key: 'bar', value: 'bar value', isFilterable: true },
|
||||
]}
|
||||
onClickFilter={jest.fn}
|
||||
/>
|
||||
|
@ -48,8 +48,8 @@ describe('KeyValueFilterList', () => {
|
|||
title="title"
|
||||
icon="alert"
|
||||
keyValueList={[
|
||||
{ key: 'foo', value: 'foo value' },
|
||||
{ key: 'bar', value: 'bar value' },
|
||||
{ key: 'foo', value: 'foo value', isFilterable: true },
|
||||
{ key: 'bar', value: 'bar value', isFilterable: true },
|
||||
]}
|
||||
onClickFilter={jest.fn}
|
||||
/>
|
||||
|
@ -62,8 +62,8 @@ describe('KeyValueFilterList', () => {
|
|||
<KeyValueFilterList
|
||||
title="title"
|
||||
keyValueList={[
|
||||
{ key: 'foo', value: 'foo value' },
|
||||
{ key: 'bar', value: 'bar value' },
|
||||
{ key: 'foo', value: 'foo value', isFilterable: true },
|
||||
{ key: 'bar', value: 'bar value', isFilterable: true },
|
||||
]}
|
||||
onClickFilter={jest.fn}
|
||||
/>
|
||||
|
@ -71,20 +71,38 @@ describe('KeyValueFilterList', () => {
|
|||
expect(component.queryAllByTestId('accordion_title_icon')).toEqual([]);
|
||||
expectTextsInDocument(component, ['title']);
|
||||
});
|
||||
|
||||
it('hides filter by value option', () => {
|
||||
const component = renderWithTheme(
|
||||
<KeyValueFilterList
|
||||
title="title"
|
||||
keyValueList={[
|
||||
{ key: 'foo', value: 'foo value', isFilterable: false },
|
||||
{ key: 'bar', value: 'bar value', isFilterable: true },
|
||||
]}
|
||||
onClickFilter={jest.fn}
|
||||
/>
|
||||
);
|
||||
expect(component.queryAllByTestId('filter_by_foo')).toEqual([]);
|
||||
expect(component.queryAllByTestId('filter_by_bar')).toHaveLength(1);
|
||||
});
|
||||
it('returns selected key value when the filter button is clicked', () => {
|
||||
const mockFilter = jest.fn();
|
||||
const component = renderWithTheme(
|
||||
<KeyValueFilterList
|
||||
title="title"
|
||||
keyValueList={[
|
||||
{ key: 'foo', value: 'foo value' },
|
||||
{ key: 'bar', value: 'bar value' },
|
||||
{ key: 'foo', value: 'foo value', isFilterable: true },
|
||||
{ key: 'bar', value: 'bar value', isFilterable: true },
|
||||
]}
|
||||
onClickFilter={mockFilter}
|
||||
/>
|
||||
);
|
||||
|
||||
fireEvent.click(component.getByTestId('filter_by_foo'));
|
||||
expect(mockFilter).toHaveBeenCalledWith({ key: 'foo', value: 'foo value' });
|
||||
expect(mockFilter).toHaveBeenCalledWith({
|
||||
key: 'foo',
|
||||
value: 'foo value',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,7 +5,11 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EuiDescriptionList, EuiDescriptionListProps } from '@elastic/eui';
|
||||
import {
|
||||
EuiDescriptionList,
|
||||
EuiDescriptionListProps,
|
||||
EuiBadge,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
import { asInteger } from '../../../../common/utils/formatters';
|
||||
|
@ -16,18 +20,38 @@ type ServiceDetailsReturnType =
|
|||
|
||||
interface Props {
|
||||
container: ServiceDetailsReturnType['container'];
|
||||
kubernetes: ServiceDetailsReturnType['kubernetes'];
|
||||
}
|
||||
|
||||
export function ContainerDetails({ container }: Props) {
|
||||
export function ContainerDetails({ container, kubernetes }: Props) {
|
||||
if (!container) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const listItems: EuiDescriptionListProps['listItems'] = [];
|
||||
|
||||
if (kubernetes?.containerImages && kubernetes?.containerImages.length > 0) {
|
||||
listItems.push({
|
||||
title: i18n.translate(
|
||||
'xpack.apm.serviceIcons.serviceDetails.container.image.name',
|
||||
{ defaultMessage: 'Container images' }
|
||||
),
|
||||
description: (
|
||||
<ul>
|
||||
{kubernetes.containerImages.map((deployment, index) => (
|
||||
<li key={index}>
|
||||
<EuiBadge color="hollow">{deployment}</EuiBadge>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
if (container.os) {
|
||||
listItems.push({
|
||||
title: i18n.translate(
|
||||
'xpack.apm.serviceIcons.serviceDetails.container.osLabel',
|
||||
'xpack.apm.serviceIcons.serviceDetails.container.os.label',
|
||||
{
|
||||
defaultMessage: 'OS',
|
||||
}
|
||||
|
@ -36,25 +60,57 @@ export function ContainerDetails({ container }: Props) {
|
|||
});
|
||||
}
|
||||
|
||||
if (container.isContainerized !== undefined) {
|
||||
if (kubernetes?.deployments && kubernetes?.deployments.length > 0) {
|
||||
listItems.push({
|
||||
title: i18n.translate(
|
||||
'xpack.apm.serviceIcons.serviceDetails.container.containerizedLabel',
|
||||
{ defaultMessage: 'Containerized' }
|
||||
'xpack.apm.serviceIcons.serviceDetails.kubernetes.deployments',
|
||||
{ defaultMessage: 'Deployments' }
|
||||
),
|
||||
description: (
|
||||
<ul>
|
||||
{kubernetes.deployments.map((deployment, index) => (
|
||||
<li key={index}>
|
||||
<EuiBadge color="hollow">{deployment}</EuiBadge>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
if (kubernetes?.namespaces && kubernetes?.namespaces.length > 0) {
|
||||
listItems.push({
|
||||
title: i18n.translate(
|
||||
'xpack.apm.serviceIcons.serviceDetails.kubernetes.namespaces',
|
||||
{ defaultMessage: 'Namespaces' }
|
||||
),
|
||||
description: (
|
||||
<ul>
|
||||
{kubernetes.namespaces.map((namespace, index) => (
|
||||
<li key={index}>
|
||||
<EuiBadge color="hollow">{namespace}</EuiBadge>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
if (kubernetes?.replicasets && kubernetes?.replicasets.length > 0) {
|
||||
listItems.push({
|
||||
title: i18n.translate(
|
||||
'xpack.apm.serviceIcons.serviceDetails.kubernetes.replicasets',
|
||||
{ defaultMessage: 'Replicasets' }
|
||||
),
|
||||
description: (
|
||||
<ul>
|
||||
{kubernetes.replicasets.map((replicaset, index) => (
|
||||
<li key={index}>
|
||||
<EuiBadge color="hollow">{replicaset}</EuiBadge>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
),
|
||||
description: container.isContainerized
|
||||
? i18n.translate(
|
||||
'xpack.apm.serviceIcons.serviceDetails.container.yesLabel',
|
||||
{
|
||||
defaultMessage: 'Yes',
|
||||
}
|
||||
)
|
||||
: i18n.translate(
|
||||
'xpack.apm.serviceIcons.serviceDetails.container.noLabel',
|
||||
{
|
||||
defaultMessage: 'No',
|
||||
}
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -68,15 +124,5 @@ export function ContainerDetails({ container }: Props) {
|
|||
});
|
||||
}
|
||||
|
||||
if (container.type) {
|
||||
listItems.push({
|
||||
title: i18n.translate(
|
||||
'xpack.apm.serviceIcons.serviceDetails.container.orchestrationLabel',
|
||||
{ defaultMessage: 'Orchestration' }
|
||||
),
|
||||
description: container.type,
|
||||
});
|
||||
}
|
||||
|
||||
return <EuiDescriptionList textStyle="reverse" listItems={listItems} />;
|
||||
}
|
||||
|
|
|
@ -129,7 +129,12 @@ export function ServiceIcons({ start, end, serviceName }: Props) {
|
|||
title: i18n.translate('xpack.apm.serviceIcons.container', {
|
||||
defaultMessage: 'Container',
|
||||
}),
|
||||
component: <ContainerDetails container={details?.container} />,
|
||||
component: (
|
||||
<ContainerDetails
|
||||
container={details?.container}
|
||||
kubernetes={details?.kubernetes}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'serverless',
|
||||
|
|
|
@ -115,10 +115,13 @@ Example.args = {
|
|||
},
|
||||
},
|
||||
container: {
|
||||
os: 'Linux',
|
||||
type: 'Kubernetes',
|
||||
isContainerized: true,
|
||||
totalNumberInstances: 1,
|
||||
image: 'container image name',
|
||||
},
|
||||
kubernetes: {
|
||||
deployments: ['opbeans-java', 'opbeans-go-nsn'],
|
||||
replicasets: ['opbeans-go-6dff977956', 'opbeans-go-nsn-864bdcbc5b'],
|
||||
namespaces: ['default'],
|
||||
},
|
||||
cloud: {
|
||||
provider: 'gcp',
|
||||
|
|
|
@ -18,7 +18,7 @@ import {
|
|||
TRANSACTION_TYPE,
|
||||
AGENT_NAME,
|
||||
SERVICE_ENVIRONMENT,
|
||||
POD_NAME,
|
||||
KUBERNETES_POD_NAME,
|
||||
CONTAINER_ID,
|
||||
SERVICE_VERSION,
|
||||
TRANSACTION_RESULT,
|
||||
|
@ -91,7 +91,7 @@ export async function aggregateLatencyMetrics() {
|
|||
SERVICE_ENVIRONMENT,
|
||||
AGENT_NAME,
|
||||
HOST_NAME,
|
||||
POD_NAME,
|
||||
KUBERNETES_POD_NAME,
|
||||
CONTAINER_ID,
|
||||
TRANSACTION_NAME,
|
||||
TRANSACTION_RESULT,
|
||||
|
|
|
@ -30,7 +30,7 @@ import {
|
|||
HOST_OS_PLATFORM,
|
||||
OBSERVER_HOSTNAME,
|
||||
PARENT_ID,
|
||||
POD_NAME,
|
||||
KUBERNETES_POD_NAME,
|
||||
PROCESSOR_EVENT,
|
||||
SERVICE_ENVIRONMENT,
|
||||
SERVICE_FRAMEWORK_NAME,
|
||||
|
@ -180,7 +180,7 @@ export const tasks: TelemetryTask[] = [
|
|||
SERVICE_VERSION,
|
||||
HOST_NAME,
|
||||
CONTAINER_ID,
|
||||
POD_NAME,
|
||||
KUBERNETES_POD_NAME,
|
||||
].map((field) => ({ terms: { field, missing_bucket: true } }));
|
||||
|
||||
const observerHostname = {
|
||||
|
@ -1206,7 +1206,7 @@ export const tasks: TelemetryTask[] = [
|
|||
field: SERVICE_RUNTIME_VERSION,
|
||||
},
|
||||
{
|
||||
field: POD_NAME,
|
||||
field: KUBERNETES_POD_NAME,
|
||||
},
|
||||
{
|
||||
field: CONTAINER_ID,
|
||||
|
@ -1314,7 +1314,7 @@ export const tasks: TelemetryTask[] = [
|
|||
kubernetes: {
|
||||
pod: {
|
||||
name: serviceBucket.top_metrics?.top[0].metrics[
|
||||
POD_NAME
|
||||
KUBERNETES_POD_NAME
|
||||
] as string,
|
||||
},
|
||||
},
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
import { SavedObjectsClientContract } from '@kbn/core/server';
|
||||
import { APMRouteHandlerResources } from '../../routes/typings';
|
||||
|
||||
export async function getMetricIndices({
|
||||
export async function getInfraMetricIndices({
|
||||
infraPlugin,
|
||||
savedObjectsClient,
|
||||
}: {
|
||||
|
@ -16,7 +16,7 @@ export async function getMetricIndices({
|
|||
savedObjectsClient: SavedObjectsClientContract;
|
||||
}): Promise<string> {
|
||||
const infra = await infraPlugin.start();
|
||||
const metricIndices = await infra.getMetricIndices(savedObjectsClient);
|
||||
const infraMetricIndices = await infra.getMetricIndices(savedObjectsClient);
|
||||
|
||||
return metricIndices;
|
||||
return infraMetricIndices;
|
||||
}
|
|
@ -14,7 +14,7 @@ import {
|
|||
HOST_NAME,
|
||||
} from '../../../common/elasticsearch_fieldnames';
|
||||
import { ApmPluginRequestHandlerContext } from '../typings';
|
||||
import { getMetricIndices } from '../../lib/helpers/get_metric_indices';
|
||||
import { getInfraMetricIndices } from '../../lib/helpers/get_infra_metric_indices';
|
||||
|
||||
interface Aggs extends estypes.AggregationsMultiBucketAggregateBase {
|
||||
buckets: Array<{
|
||||
|
@ -92,7 +92,7 @@ export const getContainerHostNames = async ({
|
|||
if (containerIds.length) {
|
||||
const esClient = (await context.core).elasticsearch.client.asCurrentUser;
|
||||
const savedObjectsClient = (await context.core).savedObjects.client;
|
||||
const metricIndices = await getMetricIndices({
|
||||
const metricIndices = await getInfraMetricIndices({
|
||||
infraPlugin: infra,
|
||||
savedObjectsClient,
|
||||
});
|
||||
|
|
|
@ -13,7 +13,7 @@ import {
|
|||
SERVICE_NAME,
|
||||
CONTAINER_ID,
|
||||
HOST_HOSTNAME,
|
||||
POD_NAME,
|
||||
KUBERNETES_POD_NAME,
|
||||
} from '../../../common/elasticsearch_fieldnames';
|
||||
|
||||
export const getInfrastructureData = async ({
|
||||
|
@ -64,7 +64,7 @@ export const getInfrastructureData = async ({
|
|||
},
|
||||
podNames: {
|
||||
terms: {
|
||||
field: POD_NAME,
|
||||
field: KUBERNETES_POD_NAME,
|
||||
size: 500,
|
||||
},
|
||||
},
|
||||
|
|
|
@ -0,0 +1,100 @@
|
|||
/*
|
||||
* 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 { ElasticsearchClient } from '@kbn/core/server';
|
||||
import { rangeQuery } from '@kbn/observability-plugin/server';
|
||||
import {
|
||||
CONTAINER,
|
||||
CONTAINER_ID,
|
||||
CONTAINER_IMAGE,
|
||||
KUBERNETES,
|
||||
KUBERNETES_CONTAINER_NAME,
|
||||
KUBERNETES_NAMESPACE,
|
||||
KUBERNETES_POD_NAME,
|
||||
KUBERNETES_POD_UID,
|
||||
KUBERNETES_REPLICASET_NAME,
|
||||
KUBERNETES_DEPLOYMENT_NAME,
|
||||
} from '../../../common/elasticsearch_fieldnames';
|
||||
import { Kubernetes } from '../../../typings/es_schemas/raw/fields/kubernetes';
|
||||
import { maybe } from '../../../common/utils/maybe';
|
||||
|
||||
type ServiceInstanceContainerMetadataDetails =
|
||||
| {
|
||||
kubernetes: Kubernetes;
|
||||
}
|
||||
| undefined;
|
||||
|
||||
export const getServiceInstanceContainerMetadata = async ({
|
||||
esClient,
|
||||
indexName,
|
||||
containerId,
|
||||
start,
|
||||
end,
|
||||
}: {
|
||||
esClient: ElasticsearchClient;
|
||||
indexName?: string;
|
||||
containerId: string;
|
||||
start: number;
|
||||
end: number;
|
||||
}): Promise<ServiceInstanceContainerMetadataDetails> => {
|
||||
if (!indexName) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const should = [
|
||||
{ exists: { field: KUBERNETES } },
|
||||
{ exists: { field: CONTAINER_IMAGE } },
|
||||
{ exists: { field: KUBERNETES_CONTAINER_NAME } },
|
||||
{ exists: { field: KUBERNETES_NAMESPACE } },
|
||||
{ exists: { field: KUBERNETES_POD_NAME } },
|
||||
{ exists: { field: KUBERNETES_POD_UID } },
|
||||
{ exists: { field: KUBERNETES_REPLICASET_NAME } },
|
||||
{ exists: { field: KUBERNETES_DEPLOYMENT_NAME } },
|
||||
];
|
||||
|
||||
const response = await esClient.search<unknown>({
|
||||
index: [indexName],
|
||||
_source: [KUBERNETES, CONTAINER],
|
||||
size: 1,
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
{
|
||||
term: {
|
||||
[CONTAINER_ID]: containerId,
|
||||
},
|
||||
},
|
||||
...rangeQuery(start, end),
|
||||
],
|
||||
should,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const sample = maybe(response.hits.hits[0])
|
||||
?._source as ServiceInstanceContainerMetadataDetails;
|
||||
|
||||
return {
|
||||
kubernetes: {
|
||||
pod: {
|
||||
name: sample?.kubernetes?.pod?.name,
|
||||
uid: sample?.kubernetes?.pod?.uid,
|
||||
},
|
||||
deployment: {
|
||||
name: sample?.kubernetes?.deployment?.name,
|
||||
},
|
||||
replicaset: {
|
||||
name: sample?.kubernetes?.replicaset?.name,
|
||||
},
|
||||
namespace: sample?.kubernetes?.namespace,
|
||||
container: {
|
||||
name: sample?.kubernetes?.container?.name,
|
||||
id: sample?.kubernetes?.container?.id,
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
|
@ -9,6 +9,7 @@ import { rangeQuery } from '@kbn/observability-plugin/server';
|
|||
import { ProcessorEvent } from '@kbn/observability-plugin/common';
|
||||
import {
|
||||
AGENT,
|
||||
CONTAINER,
|
||||
CLOUD,
|
||||
CLOUD_AVAILABILITY_ZONE,
|
||||
CLOUD_REGION,
|
||||
|
@ -49,10 +50,10 @@ export interface ServiceMetadataDetails {
|
|||
};
|
||||
};
|
||||
container?: {
|
||||
ids?: string[];
|
||||
image?: string;
|
||||
os?: string;
|
||||
isContainerized?: boolean;
|
||||
totalNumberInstances?: number;
|
||||
type?: ContainerType;
|
||||
};
|
||||
serverless?: {
|
||||
type?: string;
|
||||
|
@ -67,6 +68,12 @@ export interface ServiceMetadataDetails {
|
|||
projectName?: string;
|
||||
serviceName?: string;
|
||||
};
|
||||
kubernetes?: {
|
||||
deployments?: string[];
|
||||
namespaces?: string[];
|
||||
replicasets?: string[];
|
||||
containerImages?: string[];
|
||||
};
|
||||
}
|
||||
|
||||
export async function getServiceMetadataDetails({
|
||||
|
@ -99,7 +106,7 @@ export async function getServiceMetadataDetails({
|
|||
},
|
||||
body: {
|
||||
size: 1,
|
||||
_source: [SERVICE, AGENT, HOST, CONTAINER_ID, KUBERNETES, CLOUD],
|
||||
_source: [SERVICE, AGENT, HOST, CONTAINER, KUBERNETES, CLOUD],
|
||||
query: { bool: { filter, should } },
|
||||
aggs: {
|
||||
serviceVersions: {
|
||||
|
@ -115,6 +122,12 @@ export async function getServiceMetadataDetails({
|
|||
size: 10,
|
||||
},
|
||||
},
|
||||
containerIds: {
|
||||
terms: {
|
||||
field: CONTAINER_ID,
|
||||
size: 10,
|
||||
},
|
||||
},
|
||||
regions: {
|
||||
terms: {
|
||||
field: CLOUD_REGION,
|
||||
|
@ -181,10 +194,12 @@ export async function getServiceMetadataDetails({
|
|||
const containerDetails =
|
||||
host || container || totalNumberInstances || kubernetes
|
||||
? {
|
||||
os: host?.os?.platform,
|
||||
type: (!!kubernetes ? 'Kubernetes' : 'Docker') as ContainerType,
|
||||
isContainerized: !!container?.id,
|
||||
os: host?.os?.platform,
|
||||
totalNumberInstances,
|
||||
ids: response.aggregations?.containerIds.buckets.map(
|
||||
(bucket) => bucket.key as string
|
||||
),
|
||||
}
|
||||
: undefined;
|
||||
|
||||
|
|
|
@ -14,7 +14,7 @@ import {
|
|||
CONTAINER_ID,
|
||||
KUBERNETES,
|
||||
SERVICE_NAME,
|
||||
POD_NAME,
|
||||
KUBERNETES_POD_NAME,
|
||||
HOST_OS_PLATFORM,
|
||||
} from '../../../common/elasticsearch_fieldnames';
|
||||
import { ContainerType } from '../../../common/service_metadata';
|
||||
|
@ -36,7 +36,7 @@ export interface ServiceMetadataIcons {
|
|||
|
||||
export const should = [
|
||||
{ exists: { field: CONTAINER_ID } },
|
||||
{ exists: { field: POD_NAME } },
|
||||
{ exists: { field: KUBERNETES_POD_NAME } },
|
||||
{ exists: { field: CLOUD_PROVIDER } },
|
||||
{ exists: { field: HOST_OS_PLATFORM } },
|
||||
{ exists: { field: AGENT_NAME } },
|
||||
|
|
|
@ -0,0 +1,131 @@
|
|||
/*
|
||||
* 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 { ElasticsearchClient } from '@kbn/core/server';
|
||||
import { rangeQuery } from '@kbn/observability-plugin/server';
|
||||
import {
|
||||
CONTAINER,
|
||||
CONTAINER_ID,
|
||||
CONTAINER_IMAGE,
|
||||
KUBERNETES,
|
||||
KUBERNETES_CONTAINER_NAME,
|
||||
KUBERNETES_NAMESPACE,
|
||||
KUBERNETES_POD_NAME,
|
||||
KUBERNETES_POD_UID,
|
||||
KUBERNETES_REPLICASET_NAME,
|
||||
KUBERNETES_DEPLOYMENT_NAME,
|
||||
} from '../../../common/elasticsearch_fieldnames';
|
||||
|
||||
type ServiceOverviewContainerMetadataDetails =
|
||||
| {
|
||||
kubernetes: {
|
||||
deployments?: string[];
|
||||
replicasets?: string[];
|
||||
namespaces?: string[];
|
||||
containerImages?: string[];
|
||||
};
|
||||
}
|
||||
| undefined;
|
||||
|
||||
interface ResponseAggregations {
|
||||
[key: string]: {
|
||||
buckets: Array<{
|
||||
key: string;
|
||||
}>;
|
||||
};
|
||||
}
|
||||
|
||||
export const getServiceOverviewContainerMetadata = async ({
|
||||
esClient,
|
||||
indexName,
|
||||
containerIds,
|
||||
start,
|
||||
end,
|
||||
}: {
|
||||
esClient: ElasticsearchClient;
|
||||
indexName?: string;
|
||||
containerIds: string[];
|
||||
start: number;
|
||||
end: number;
|
||||
}): Promise<ServiceOverviewContainerMetadataDetails> => {
|
||||
if (!indexName) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const should = [
|
||||
{ exists: { field: KUBERNETES } },
|
||||
{ exists: { field: CONTAINER_IMAGE } },
|
||||
{ exists: { field: KUBERNETES_CONTAINER_NAME } },
|
||||
{ exists: { field: KUBERNETES_NAMESPACE } },
|
||||
{ exists: { field: KUBERNETES_POD_NAME } },
|
||||
{ exists: { field: KUBERNETES_POD_UID } },
|
||||
{ exists: { field: KUBERNETES_REPLICASET_NAME } },
|
||||
{ exists: { field: KUBERNETES_DEPLOYMENT_NAME } },
|
||||
];
|
||||
|
||||
const response = await esClient.search<unknown, ResponseAggregations>({
|
||||
index: [indexName],
|
||||
_source: [KUBERNETES, CONTAINER],
|
||||
size: 0,
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
{
|
||||
terms: {
|
||||
[CONTAINER_ID]: containerIds,
|
||||
},
|
||||
},
|
||||
...rangeQuery(start, end),
|
||||
],
|
||||
should,
|
||||
},
|
||||
},
|
||||
aggs: {
|
||||
deployments: {
|
||||
terms: {
|
||||
field: KUBERNETES_DEPLOYMENT_NAME,
|
||||
size: 10,
|
||||
},
|
||||
},
|
||||
namespaces: {
|
||||
terms: {
|
||||
field: KUBERNETES_NAMESPACE,
|
||||
size: 10,
|
||||
},
|
||||
},
|
||||
replicasets: {
|
||||
terms: {
|
||||
field: KUBERNETES_REPLICASET_NAME,
|
||||
size: 10,
|
||||
},
|
||||
},
|
||||
containerImages: {
|
||||
terms: {
|
||||
field: CONTAINER_IMAGE,
|
||||
size: 10,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
kubernetes: {
|
||||
deployments: response.aggregations?.deployments?.buckets.map(
|
||||
(bucket) => bucket.key
|
||||
),
|
||||
replicasets: response.aggregations?.replicasets?.buckets.map(
|
||||
(bucket) => bucket.key
|
||||
),
|
||||
namespaces: response.aggregations?.namespaces?.buckets.map(
|
||||
(bucket) => bucket.key
|
||||
),
|
||||
containerImages: response.aggregations?.containerImages?.buckets.map(
|
||||
(bucket) => bucket.key
|
||||
),
|
||||
},
|
||||
};
|
||||
};
|
|
@ -8,7 +8,7 @@
|
|||
import Boom from '@hapi/boom';
|
||||
import { isoToEpochRt, jsonRt, toNumberRt } from '@kbn/io-ts-utils';
|
||||
import * as t from 'io-ts';
|
||||
import { uniq } from 'lodash';
|
||||
import { uniq, mergeWith } from 'lodash';
|
||||
import {
|
||||
UnknownMLCapabilitiesError,
|
||||
InsufficientMLCapabilities,
|
||||
|
@ -40,6 +40,8 @@ import {
|
|||
probabilityRt,
|
||||
} from '../default_api_types';
|
||||
import { offsetPreviousPeriodCoordinates } from '../../../common/utils/offset_previous_period_coordinate';
|
||||
import { getServiceOverviewContainerMetadata } from './get_service_overview_container_metadata';
|
||||
import { getServiceInstanceContainerMetadata } from './get_service_instance_container_metadata';
|
||||
import { getServicesDetailedStatistics } from './get_services_detailed_statistics';
|
||||
import { getServiceDependenciesBreakdown } from './get_service_dependencies_breakdown';
|
||||
import { getAnomalyTimeseries } from '../../lib/anomaly_detection/get_anomaly_timeseries';
|
||||
|
@ -51,7 +53,7 @@ import { ServiceHealthStatus } from '../../../common/service_health_status';
|
|||
import { getServiceGroup } from '../service_groups/get_service_group';
|
||||
import { offsetRt } from '../../../common/comparison_rt';
|
||||
import { getRandomSampler } from '../../lib/helpers/get_random_sampler';
|
||||
|
||||
import { getInfraMetricIndices } from '../../lib/helpers/get_infra_metric_indices';
|
||||
const servicesRoute = createApmServerRoute({
|
||||
endpoint: 'GET /internal/apm/services',
|
||||
params: t.type({
|
||||
|
@ -249,7 +251,7 @@ const serviceMetadataDetailsRoute = createApmServerRoute({
|
|||
import('./get_service_metadata_details').ServiceMetadataDetails
|
||||
> => {
|
||||
const setup = await setupRequest(resources);
|
||||
const { params } = resources;
|
||||
const { params, context, plugins } = resources;
|
||||
const { serviceName } = params.path;
|
||||
const { start, end } = params.query;
|
||||
|
||||
|
@ -261,13 +263,37 @@ const serviceMetadataDetailsRoute = createApmServerRoute({
|
|||
kuery: '',
|
||||
});
|
||||
|
||||
return getServiceMetadataDetails({
|
||||
const serviceMetadataDetails = await getServiceMetadataDetails({
|
||||
serviceName,
|
||||
setup,
|
||||
searchAggregatedTransactions,
|
||||
start,
|
||||
end,
|
||||
});
|
||||
|
||||
if (serviceMetadataDetails?.container?.ids) {
|
||||
const {
|
||||
savedObjects: { client: savedObjectsClient },
|
||||
elasticsearch: { client: esClient },
|
||||
} = await context.core;
|
||||
|
||||
const indexName = await getInfraMetricIndices({
|
||||
infraPlugin: plugins.infra,
|
||||
savedObjectsClient,
|
||||
});
|
||||
|
||||
const containerMetadata = await getServiceOverviewContainerMetadata({
|
||||
esClient: esClient.asCurrentUser,
|
||||
indexName,
|
||||
containerIds: serviceMetadataDetails.container.ids,
|
||||
start,
|
||||
end,
|
||||
});
|
||||
|
||||
return mergeWith(serviceMetadataDetails, containerMetadata);
|
||||
}
|
||||
|
||||
return serviceMetadataDetails;
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -862,16 +888,42 @@ export const serviceInstancesMetadataDetails = createApmServerRoute({
|
|||
| undefined;
|
||||
}> => {
|
||||
const setup = await setupRequest(resources);
|
||||
const { serviceName, serviceNodeName } = resources.params.path;
|
||||
const { start, end } = resources.params.query;
|
||||
const { params, context, plugins } = resources;
|
||||
const { serviceName, serviceNodeName } = params.path;
|
||||
const { start, end } = params.query;
|
||||
|
||||
return await getServiceInstanceMetadataDetails({
|
||||
setup,
|
||||
serviceName,
|
||||
serviceNodeName,
|
||||
start,
|
||||
end,
|
||||
});
|
||||
const serviceInstanceMetadataDetails =
|
||||
await getServiceInstanceMetadataDetails({
|
||||
setup,
|
||||
serviceName,
|
||||
serviceNodeName,
|
||||
start,
|
||||
end,
|
||||
});
|
||||
|
||||
if (serviceInstanceMetadataDetails?.container?.id) {
|
||||
const {
|
||||
savedObjects: { client: savedObjectsClient },
|
||||
elasticsearch: { client: esClient },
|
||||
} = await context.core;
|
||||
|
||||
const indexName = await getInfraMetricIndices({
|
||||
infraPlugin: plugins.infra,
|
||||
savedObjectsClient,
|
||||
});
|
||||
|
||||
const containerMetadata = await getServiceInstanceContainerMetadata({
|
||||
esClient: esClient.asCurrentUser,
|
||||
indexName,
|
||||
containerId: serviceInstanceMetadataDetails.container.id,
|
||||
start,
|
||||
end,
|
||||
});
|
||||
|
||||
return mergeWith(serviceInstanceMetadataDetails, containerMetadata);
|
||||
}
|
||||
|
||||
return serviceInstanceMetadataDetails;
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -6,5 +6,6 @@
|
|||
*/
|
||||
|
||||
export interface Container {
|
||||
id: string;
|
||||
id?: string | null;
|
||||
image?: string | null;
|
||||
}
|
||||
|
|
|
@ -6,5 +6,16 @@
|
|||
*/
|
||||
|
||||
export interface Kubernetes {
|
||||
pod?: { uid: string; [key: string]: unknown };
|
||||
pod?: { uid?: string | null; [key: string]: unknown };
|
||||
namespace?: string;
|
||||
replicaset?: {
|
||||
name?: string;
|
||||
};
|
||||
deployment?: {
|
||||
name?: string;
|
||||
};
|
||||
container?: {
|
||||
id?: string;
|
||||
name?: string;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -7197,12 +7197,7 @@
|
|||
"xpack.apm.serviceIcons.serviceDetails.cloud.projectIdLabel": "ID de projet",
|
||||
"xpack.apm.serviceIcons.serviceDetails.cloud.providerLabel": "Fournisseur cloud",
|
||||
"xpack.apm.serviceIcons.serviceDetails.cloud.serviceNameLabel": "Service Cloud",
|
||||
"xpack.apm.serviceIcons.serviceDetails.container.containerizedLabel": "Conteneurisé",
|
||||
"xpack.apm.serviceIcons.serviceDetails.container.noLabel": "Non",
|
||||
"xpack.apm.serviceIcons.serviceDetails.container.orchestrationLabel": "Orchestration",
|
||||
"xpack.apm.serviceIcons.serviceDetails.container.osLabel": "Système d'exploitation",
|
||||
"xpack.apm.serviceIcons.serviceDetails.container.totalNumberInstancesLabel": "Nombre total d'instances",
|
||||
"xpack.apm.serviceIcons.serviceDetails.container.yesLabel": "Oui",
|
||||
"xpack.apm.serviceIcons.serviceDetails.service.agentLabel": "Nom et version de l'agent",
|
||||
"xpack.apm.serviceIcons.serviceDetails.service.frameworkLabel": "Nom du framework",
|
||||
"xpack.apm.serviceIcons.serviceDetails.service.runtimeLabel": "Nom et version de l'exécution",
|
||||
|
|
|
@ -7184,12 +7184,7 @@
|
|||
"xpack.apm.serviceIcons.serviceDetails.cloud.projectIdLabel": "プロジェクト ID",
|
||||
"xpack.apm.serviceIcons.serviceDetails.cloud.providerLabel": "クラウドプロバイダー",
|
||||
"xpack.apm.serviceIcons.serviceDetails.cloud.serviceNameLabel": "クラウドサービス",
|
||||
"xpack.apm.serviceIcons.serviceDetails.container.containerizedLabel": "コンテナー化",
|
||||
"xpack.apm.serviceIcons.serviceDetails.container.noLabel": "いいえ",
|
||||
"xpack.apm.serviceIcons.serviceDetails.container.orchestrationLabel": "オーケストレーション",
|
||||
"xpack.apm.serviceIcons.serviceDetails.container.osLabel": "OS",
|
||||
"xpack.apm.serviceIcons.serviceDetails.container.totalNumberInstancesLabel": "インスタンスの合計数",
|
||||
"xpack.apm.serviceIcons.serviceDetails.container.yesLabel": "はい",
|
||||
"xpack.apm.serviceIcons.serviceDetails.service.agentLabel": "エージェント名・バージョン",
|
||||
"xpack.apm.serviceIcons.serviceDetails.service.frameworkLabel": "フレームワーク名",
|
||||
"xpack.apm.serviceIcons.serviceDetails.service.runtimeLabel": "ランタイム名・バージョン",
|
||||
|
|
|
@ -7198,12 +7198,7 @@
|
|||
"xpack.apm.serviceIcons.serviceDetails.cloud.projectIdLabel": "项目 ID",
|
||||
"xpack.apm.serviceIcons.serviceDetails.cloud.providerLabel": "云服务提供商",
|
||||
"xpack.apm.serviceIcons.serviceDetails.cloud.serviceNameLabel": "云服务",
|
||||
"xpack.apm.serviceIcons.serviceDetails.container.containerizedLabel": "容器化",
|
||||
"xpack.apm.serviceIcons.serviceDetails.container.noLabel": "否",
|
||||
"xpack.apm.serviceIcons.serviceDetails.container.orchestrationLabel": "编排",
|
||||
"xpack.apm.serviceIcons.serviceDetails.container.osLabel": "OS",
|
||||
"xpack.apm.serviceIcons.serviceDetails.container.totalNumberInstancesLabel": "实例总数",
|
||||
"xpack.apm.serviceIcons.serviceDetails.container.yesLabel": "是",
|
||||
"xpack.apm.serviceIcons.serviceDetails.service.agentLabel": "代理名称和版本",
|
||||
"xpack.apm.serviceIcons.serviceDetails.service.frameworkLabel": "框架名称",
|
||||
"xpack.apm.serviceIcons.serviceDetails.service.runtimeLabel": "运行时名称和版本",
|
||||
|
|
|
@ -10,4 +10,8 @@ export default {
|
|||
start: '2021-08-03T06:50:15.910Z',
|
||||
end: '2021-08-03T07:20:15.910Z',
|
||||
},
|
||||
infra_metrics_and_apm: {
|
||||
start: '2019-06-29T02:48:39.386555Z',
|
||||
end: '2022-06-29T06:46:26Z',
|
||||
},
|
||||
};
|
||||
|
|
Binary file not shown.
File diff suppressed because it is too large
Load diff
|
@ -17,6 +17,7 @@ type ArchiveName =
|
|||
| 'apm_8.0.0'
|
||||
| '8.0.0'
|
||||
| 'metrics_8.0.0'
|
||||
| 'infra_metrics_and_apm'
|
||||
| 'ml_8.0.0'
|
||||
| 'observability_overview'
|
||||
| 'rum_8.0.0'
|
||||
|
|
|
@ -87,12 +87,7 @@ export default function ApiTest({ getService }: FtrProviderContext) {
|
|||
});
|
||||
|
||||
it('returns correct container details', () => {
|
||||
const { containerOs } = dataConfig;
|
||||
|
||||
expect(body?.container?.isContainerized).to.be(true);
|
||||
expect(body?.container?.os).to.be(containerOs);
|
||||
expect(body?.container?.totalNumberInstances).to.be(1);
|
||||
expect(body?.container?.type).to.be('Kubernetes');
|
||||
});
|
||||
|
||||
it('returns correct serverless details', () => {
|
||||
|
|
|
@ -0,0 +1,170 @@
|
|||
/*
|
||||
* 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 expect from '@kbn/expect';
|
||||
import { APIReturnType } from '@kbn/apm-plugin/public/services/rest/create_call_apm_api';
|
||||
import { FtrProviderContext } from '../../../common/ftr_provider_context';
|
||||
import archives_metadata from '../../../common/fixtures/es_archiver/archives_metadata';
|
||||
|
||||
type ServiceOverviewInstanceDetails =
|
||||
APIReturnType<'GET /internal/apm/services/{serviceName}/service_overview_instances/details/{serviceNodeName}'>;
|
||||
|
||||
type ServiceDetails = APIReturnType<'GET /internal/apm/services/{serviceName}/metadata/details'>;
|
||||
|
||||
export default function ApiTest({ getService }: FtrProviderContext) {
|
||||
const registry = getService('registry');
|
||||
const apmApiClient = getService('apmApiClient');
|
||||
const archiveName = 'infra_metrics_and_apm';
|
||||
|
||||
const { start, end } = archives_metadata[archiveName];
|
||||
|
||||
registry.when(
|
||||
'When data is loaded',
|
||||
{ config: 'basic', archives: ['infra_metrics_and_apm'] },
|
||||
() => {
|
||||
describe('fetch service instance', () => {
|
||||
it('handles empty infra metrics data for a service node', async () => {
|
||||
const response = await apmApiClient.readUser({
|
||||
endpoint:
|
||||
'GET /internal/apm/services/{serviceName}/service_overview_instances/details/{serviceNodeName}',
|
||||
params: {
|
||||
path: {
|
||||
serviceName: 'opbeans-node',
|
||||
serviceNodeName: '768120daead4526f5ba3ec583e0b081a19a525843aa5632a5e0b1de3a367f52d',
|
||||
},
|
||||
query: {
|
||||
start,
|
||||
end,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const body: ServiceOverviewInstanceDetails = response.body;
|
||||
const status: number = response.status;
|
||||
|
||||
expect(status).to.be(200);
|
||||
|
||||
expect(body.kubernetes?.pod).to.eql({});
|
||||
expect(body.kubernetes?.deployment).to.eql({});
|
||||
expect(body.kubernetes?.replicaset).to.eql({});
|
||||
expect(body.kubernetes?.container).to.eql({});
|
||||
});
|
||||
|
||||
it('handles kubernetes metadata for a service node', async () => {
|
||||
const response = await apmApiClient.readUser({
|
||||
endpoint:
|
||||
'GET /internal/apm/services/{serviceName}/service_overview_instances/details/{serviceNodeName}',
|
||||
params: {
|
||||
path: {
|
||||
serviceName: 'opbeans-java',
|
||||
serviceNodeName: '31651f3c624b81c55dd4633df0b5b9f9ab06b151121b0404ae796632cd1f87ad',
|
||||
},
|
||||
query: {
|
||||
start,
|
||||
end,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const body: ServiceOverviewInstanceDetails = response.body;
|
||||
const status: number = response.status;
|
||||
|
||||
expect(status).to.be(200);
|
||||
|
||||
expect(body.kubernetes?.deployment?.name).to.eql('opbeans-java');
|
||||
expect(body.kubernetes?.pod?.name).to.eql('opbeans-java-5b5f75d696-5brrb');
|
||||
expect(body.kubernetes?.pod?.uid).to.eql('798f59e9-b1b2-11e9-9a96-42010a84004d');
|
||||
expect(body.kubernetes?.namespace).to.eql('default');
|
||||
expect(body.kubernetes?.replicaset?.name).to.eql('opbeans-java-5b5f75d696');
|
||||
expect(body.kubernetes?.container?.name).to.eql('opbeans-java');
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetch service overview metadata details', () => {
|
||||
it('handles service overview metadata with multiple kubernetes instances', async () => {
|
||||
const response = await apmApiClient.readUser({
|
||||
endpoint: 'GET /internal/apm/services/{serviceName}/metadata/details',
|
||||
params: {
|
||||
path: {
|
||||
serviceName: 'opbeans-java',
|
||||
},
|
||||
query: {
|
||||
start,
|
||||
end,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const body: ServiceDetails = response.body;
|
||||
const status: number = response.status;
|
||||
|
||||
expect(status).to.be(200);
|
||||
expect(body.kubernetes?.deployments).to.eql(['opbeans-java', 'opbeans-java-2']);
|
||||
expect(body.kubernetes?.namespaces).to.eql(['default']);
|
||||
expect(body.kubernetes?.containerImages).to.eql([
|
||||
'docker.elastic.co/observability-ci/opbeans-java@sha256:dda30dbabe5c43b8bcd62b48a727f04e9d17147443ea3b3ac2edfc44cb0e69fe',
|
||||
'mysql@sha256:c8f03238ca1783d25af320877f063a36dbfce0daa56a7b4955e6c6e05ab5c70b',
|
||||
]);
|
||||
expect(body.kubernetes?.replicasets).to.eql([
|
||||
'opbeans-java-5b5f75d696',
|
||||
'opbeans-java-5b5f75d697',
|
||||
]);
|
||||
});
|
||||
|
||||
it('handles partial infra metrics data', async () => {
|
||||
const response = await apmApiClient.readUser({
|
||||
endpoint: 'GET /internal/apm/services/{serviceName}/metadata/details',
|
||||
params: {
|
||||
path: {
|
||||
serviceName: 'opbeans-node',
|
||||
},
|
||||
query: {
|
||||
start,
|
||||
end,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const body: ServiceDetails = response.body;
|
||||
const status: number = response.status;
|
||||
|
||||
expect(status).to.be(200);
|
||||
expect(body.kubernetes?.containerImages).to.eql([
|
||||
'docker.elastic.co/observability-ci/opbeans-node@sha256:f72b0bfdd0ca24e4f9d10ee73cf713a591dbfa40f1fe9404b04e6f2f3e166949',
|
||||
'k8s.gcr.io/pause:3.1',
|
||||
]);
|
||||
expect(body.kubernetes?.deployments).to.eql([]);
|
||||
expect(body.kubernetes?.namespaces).to.eql([]);
|
||||
expect(body.kubernetes?.replicasets).to.eql([]);
|
||||
});
|
||||
|
||||
it('handles empty infra metrics data', async () => {
|
||||
const response = await apmApiClient.readUser({
|
||||
endpoint: 'GET /internal/apm/services/{serviceName}/metadata/details',
|
||||
params: {
|
||||
path: {
|
||||
serviceName: 'opbeans-ruby',
|
||||
},
|
||||
query: {
|
||||
start,
|
||||
end,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const body: ServiceDetails = response.body;
|
||||
const status: number = response.status;
|
||||
|
||||
expect(status).to.be(200);
|
||||
expect(body.kubernetes?.containerImages).to.eql([]);
|
||||
expect(body.kubernetes?.namespaces).to.eql([]);
|
||||
expect(body.kubernetes?.namespaces).to.eql([]);
|
||||
expect(body.kubernetes?.replicasets).to.eql([]);
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue