mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[APM] Updated header icons (#84760)
* creating service name header * fixing icons * removing unused api import * fixing some stuff * adding API tests * refactoring some stuff * fixing tests * refactoring some stuff * fixing i18n * reverting * renaming * applying min width * addressing PR comments and adding test * sorting service version * changing sort type to desc * addressing pr comments * changing to show total and not avg * addressing pr comments * addressing pr comments * addressing pr comments Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
1fddf94274
commit
53da425c8e
23 changed files with 1423 additions and 6 deletions
|
@ -1,5 +1,12 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Error AGENT 1`] = `
|
||||
Object {
|
||||
"name": "java",
|
||||
"version": "agent version",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Error AGENT_NAME 1`] = `"java"`;
|
||||
|
||||
exports[`Error AGENT_VERSION 1`] = `"agent version"`;
|
||||
|
@ -8,14 +15,26 @@ exports[`Error CLIENT_GEO 1`] = `undefined`;
|
|||
|
||||
exports[`Error CLIENT_GEO_COUNTRY_ISO_CODE 1`] = `undefined`;
|
||||
|
||||
exports[`Error CLOUD 1`] = `
|
||||
Object {
|
||||
"availability_zone": "europe-west1-c",
|
||||
"provider": "gcp",
|
||||
"region": "europe-west1",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Error CLOUD_AVAILABILITY_ZONE 1`] = `"europe-west1-c"`;
|
||||
|
||||
exports[`Error CLOUD_MACHINE_TYPE 1`] = `undefined`;
|
||||
|
||||
exports[`Error CLOUD_PROVIDER 1`] = `"gcp"`;
|
||||
|
||||
exports[`Error CLOUD_REGION 1`] = `"europe-west1"`;
|
||||
|
||||
exports[`Error CLS_FIELD 1`] = `undefined`;
|
||||
|
||||
exports[`Error CONTAINER 1`] = `undefined`;
|
||||
|
||||
exports[`Error CONTAINER_ID 1`] = `undefined`;
|
||||
|
||||
exports[`Error DESTINATION_ADDRESS 1`] = `undefined`;
|
||||
|
@ -42,12 +61,20 @@ exports[`Error FCP_FIELD 1`] = `undefined`;
|
|||
|
||||
exports[`Error FID_FIELD 1`] = `undefined`;
|
||||
|
||||
exports[`Error HOST 1`] = `
|
||||
Object {
|
||||
"hostname": "my hostname",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Error HOST_NAME 1`] = `"my hostname"`;
|
||||
|
||||
exports[`Error HTTP_REQUEST_METHOD 1`] = `undefined`;
|
||||
|
||||
exports[`Error HTTP_RESPONSE_STATUS_CODE 1`] = `undefined`;
|
||||
|
||||
exports[`Error KUBERNETES 1`] = `undefined`;
|
||||
|
||||
exports[`Error LABEL_NAME 1`] = `undefined`;
|
||||
|
||||
exports[`Error LCP_FIELD 1`] = `undefined`;
|
||||
|
@ -94,6 +121,16 @@ exports[`Error POD_NAME 1`] = `undefined`;
|
|||
|
||||
exports[`Error PROCESSOR_EVENT 1`] = `"error"`;
|
||||
|
||||
exports[`Error SERVICE 1`] = `
|
||||
Object {
|
||||
"language": Object {
|
||||
"name": "nodejs",
|
||||
"version": "v1337",
|
||||
},
|
||||
"name": "service name",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Error SERVICE_ENVIRONMENT 1`] = `undefined`;
|
||||
|
||||
exports[`Error SERVICE_FRAMEWORK_NAME 1`] = `undefined`;
|
||||
|
@ -176,6 +213,13 @@ exports[`Error USER_AGENT_OS 1`] = `undefined`;
|
|||
|
||||
exports[`Error USER_ID 1`] = `undefined`;
|
||||
|
||||
exports[`Span AGENT 1`] = `
|
||||
Object {
|
||||
"name": "java",
|
||||
"version": "agent version",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Span AGENT_NAME 1`] = `"java"`;
|
||||
|
||||
exports[`Span AGENT_VERSION 1`] = `"agent version"`;
|
||||
|
@ -184,14 +228,26 @@ exports[`Span CLIENT_GEO 1`] = `undefined`;
|
|||
|
||||
exports[`Span CLIENT_GEO_COUNTRY_ISO_CODE 1`] = `undefined`;
|
||||
|
||||
exports[`Span CLOUD 1`] = `
|
||||
Object {
|
||||
"availability_zone": "europe-west1-c",
|
||||
"provider": "gcp",
|
||||
"region": "europe-west1",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Span CLOUD_AVAILABILITY_ZONE 1`] = `"europe-west1-c"`;
|
||||
|
||||
exports[`Span CLOUD_MACHINE_TYPE 1`] = `undefined`;
|
||||
|
||||
exports[`Span CLOUD_PROVIDER 1`] = `"gcp"`;
|
||||
|
||||
exports[`Span CLOUD_REGION 1`] = `"europe-west1"`;
|
||||
|
||||
exports[`Span CLS_FIELD 1`] = `undefined`;
|
||||
|
||||
exports[`Span CONTAINER 1`] = `undefined`;
|
||||
|
||||
exports[`Span CONTAINER_ID 1`] = `undefined`;
|
||||
|
||||
exports[`Span DESTINATION_ADDRESS 1`] = `undefined`;
|
||||
|
@ -218,12 +274,16 @@ exports[`Span FCP_FIELD 1`] = `undefined`;
|
|||
|
||||
exports[`Span FID_FIELD 1`] = `undefined`;
|
||||
|
||||
exports[`Span HOST 1`] = `undefined`;
|
||||
|
||||
exports[`Span HOST_NAME 1`] = `undefined`;
|
||||
|
||||
exports[`Span HTTP_REQUEST_METHOD 1`] = `undefined`;
|
||||
|
||||
exports[`Span HTTP_RESPONSE_STATUS_CODE 1`] = `undefined`;
|
||||
|
||||
exports[`Span KUBERNETES 1`] = `undefined`;
|
||||
|
||||
exports[`Span LABEL_NAME 1`] = `undefined`;
|
||||
|
||||
exports[`Span LCP_FIELD 1`] = `undefined`;
|
||||
|
@ -270,6 +330,12 @@ exports[`Span POD_NAME 1`] = `undefined`;
|
|||
|
||||
exports[`Span PROCESSOR_EVENT 1`] = `"span"`;
|
||||
|
||||
exports[`Span SERVICE 1`] = `
|
||||
Object {
|
||||
"name": "service name",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Span SERVICE_ENVIRONMENT 1`] = `undefined`;
|
||||
|
||||
exports[`Span SERVICE_FRAMEWORK_NAME 1`] = `undefined`;
|
||||
|
@ -352,6 +418,13 @@ exports[`Span USER_AGENT_OS 1`] = `undefined`;
|
|||
|
||||
exports[`Span USER_ID 1`] = `undefined`;
|
||||
|
||||
exports[`Transaction AGENT 1`] = `
|
||||
Object {
|
||||
"name": "java",
|
||||
"version": "agent version",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Transaction AGENT_NAME 1`] = `"java"`;
|
||||
|
||||
exports[`Transaction AGENT_VERSION 1`] = `"agent version"`;
|
||||
|
@ -360,14 +433,26 @@ exports[`Transaction CLIENT_GEO 1`] = `undefined`;
|
|||
|
||||
exports[`Transaction CLIENT_GEO_COUNTRY_ISO_CODE 1`] = `undefined`;
|
||||
|
||||
exports[`Transaction CLOUD 1`] = `
|
||||
Object {
|
||||
"availability_zone": "europe-west1-c",
|
||||
"provider": "gcp",
|
||||
"region": "europe-west1",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Transaction CLOUD_AVAILABILITY_ZONE 1`] = `"europe-west1-c"`;
|
||||
|
||||
exports[`Transaction CLOUD_MACHINE_TYPE 1`] = `undefined`;
|
||||
|
||||
exports[`Transaction CLOUD_PROVIDER 1`] = `"gcp"`;
|
||||
|
||||
exports[`Transaction CLOUD_REGION 1`] = `"europe-west1"`;
|
||||
|
||||
exports[`Transaction CLS_FIELD 1`] = `undefined`;
|
||||
|
||||
exports[`Transaction CONTAINER 1`] = `"container1234567890abcdef"`;
|
||||
|
||||
exports[`Transaction CONTAINER_ID 1`] = `"container1234567890abcdef"`;
|
||||
|
||||
exports[`Transaction DESTINATION_ADDRESS 1`] = `undefined`;
|
||||
|
@ -394,12 +479,26 @@ exports[`Transaction FCP_FIELD 1`] = `undefined`;
|
|||
|
||||
exports[`Transaction FID_FIELD 1`] = `undefined`;
|
||||
|
||||
exports[`Transaction HOST 1`] = `
|
||||
Object {
|
||||
"hostname": "my hostname",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Transaction HOST_NAME 1`] = `"my hostname"`;
|
||||
|
||||
exports[`Transaction HTTP_REQUEST_METHOD 1`] = `"GET"`;
|
||||
|
||||
exports[`Transaction HTTP_RESPONSE_STATUS_CODE 1`] = `200`;
|
||||
|
||||
exports[`Transaction KUBERNETES 1`] = `
|
||||
Object {
|
||||
"pod": Object {
|
||||
"uid": "pod1234567890abcdef",
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Transaction LABEL_NAME 1`] = `undefined`;
|
||||
|
||||
exports[`Transaction LCP_FIELD 1`] = `undefined`;
|
||||
|
@ -446,6 +545,16 @@ exports[`Transaction POD_NAME 1`] = `undefined`;
|
|||
|
||||
exports[`Transaction PROCESSOR_EVENT 1`] = `"transaction"`;
|
||||
|
||||
exports[`Transaction SERVICE 1`] = `
|
||||
Object {
|
||||
"language": Object {
|
||||
"name": "nodejs",
|
||||
"version": "v1337",
|
||||
},
|
||||
"name": "service name",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Transaction SERVICE_ENVIRONMENT 1`] = `undefined`;
|
||||
|
||||
exports[`Transaction SERVICE_FRAMEWORK_NAME 1`] = `undefined`;
|
||||
|
|
|
@ -4,10 +4,13 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export const CLOUD = 'cloud';
|
||||
export const CLOUD_AVAILABILITY_ZONE = 'cloud.availability_zone';
|
||||
export const CLOUD_PROVIDER = 'cloud.provider';
|
||||
export const CLOUD_REGION = 'cloud.region';
|
||||
export const CLOUD_MACHINE_TYPE = 'cloud.machine.type';
|
||||
|
||||
export const SERVICE = 'service';
|
||||
export const SERVICE_NAME = 'service.name';
|
||||
export const SERVICE_ENVIRONMENT = 'service.environment';
|
||||
export const SERVICE_FRAMEWORK_NAME = 'service.framework.name';
|
||||
|
@ -19,6 +22,7 @@ export const SERVICE_RUNTIME_VERSION = 'service.runtime.version';
|
|||
export const SERVICE_NODE_NAME = 'service.node.name';
|
||||
export const SERVICE_VERSION = 'service.version';
|
||||
|
||||
export const AGENT = 'agent';
|
||||
export const AGENT_NAME = 'agent.name';
|
||||
export const AGENT_VERSION = 'agent.version';
|
||||
|
||||
|
@ -102,8 +106,11 @@ export const METRIC_JAVA_GC_TIME = 'jvm.gc.time';
|
|||
|
||||
export const LABEL_NAME = 'labels.name';
|
||||
|
||||
export const HOST = 'host';
|
||||
export const HOST_NAME = 'host.hostname';
|
||||
export const CONTAINER = 'container.id';
|
||||
export const CONTAINER_ID = 'container.id';
|
||||
export const KUBERNETES = 'kubernetes';
|
||||
export const POD_NAME = 'kubernetes.pod.name';
|
||||
|
||||
export const CLIENT_GEO_COUNTRY_ISO_CODE = 'client.geo.country_iso_code';
|
||||
|
|
7
x-pack/plugins/apm/common/service_metadata.ts
Normal file
7
x-pack/plugins/apm/common/service_metadata.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export type ContainerType = 'Kubernetes' | 'Docker' | undefined;
|
|
@ -4,10 +4,11 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { EuiTitle } from '@elastic/eui';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiTitle } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { RouteComponentProps } from 'react-router-dom';
|
||||
import { ApmHeader } from '../../shared/ApmHeader';
|
||||
import { ServiceIcons } from './service_icons';
|
||||
import { ServiceDetailTabs } from './service_detail_tabs';
|
||||
|
||||
interface Props extends RouteComponentProps<{ serviceName: string }> {
|
||||
|
@ -20,9 +21,16 @@ export function ServiceDetails({ match, tab }: Props) {
|
|||
return (
|
||||
<div>
|
||||
<ApmHeader>
|
||||
<EuiTitle>
|
||||
<h1>{serviceName}</h1>
|
||||
</EuiTitle>
|
||||
<EuiFlexGroup alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiTitle>
|
||||
<h1>{serviceName}</h1>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<ServiceIcons serviceName={serviceName} />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</ApmHeader>
|
||||
<ServiceDetailTabs serviceName={serviceName} tab={tab} />
|
||||
</div>
|
||||
|
|
|
@ -0,0 +1,94 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { EuiBadge, EuiDescriptionList } from '@elastic/eui';
|
||||
import { EuiDescriptionListProps } from '@elastic/eui/src/components/description_list/description_list';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
import { APIReturnType } from '../../../../services/rest/createCallApmApi';
|
||||
|
||||
type ServiceDetailsReturnType = APIReturnType<'GET /api/apm/services/{serviceName}/metadata/details'>;
|
||||
|
||||
interface Props {
|
||||
cloud: ServiceDetailsReturnType['cloud'];
|
||||
}
|
||||
|
||||
export function CloudDetails({ cloud }: Props) {
|
||||
if (!cloud) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const listItems: EuiDescriptionListProps['listItems'] = [];
|
||||
if (cloud.provider) {
|
||||
listItems.push({
|
||||
title: i18n.translate(
|
||||
'xpack.apm.serviceIcons.serviceDetails.cloud.providerLabel',
|
||||
{
|
||||
defaultMessage: 'Cloud provider',
|
||||
}
|
||||
),
|
||||
description: cloud.provider,
|
||||
});
|
||||
}
|
||||
|
||||
if (!!cloud.availabilityZones?.length) {
|
||||
listItems.push({
|
||||
title: i18n.translate(
|
||||
'xpack.apm.serviceIcons.serviceDetails.cloud.availabilityZoneLabel',
|
||||
{
|
||||
defaultMessage:
|
||||
'{zones, plural, =0 {Availability zone} one {Availability zone} other {Availability zones}} ',
|
||||
values: { zones: cloud.availabilityZones.length },
|
||||
}
|
||||
),
|
||||
description: (
|
||||
<ul>
|
||||
{cloud.availabilityZones.map((zone, index) => (
|
||||
<li key={index}>
|
||||
<EuiBadge color="hollow">{zone}</EuiBadge>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
if (cloud.machineTypes) {
|
||||
listItems.push({
|
||||
title: i18n.translate(
|
||||
'xpack.apm.serviceIcons.serviceDetails.cloud.machineTypesLabel',
|
||||
{
|
||||
defaultMessage:
|
||||
'{machineTypes, plural, =0{Machine type} one {Machine type} other {Machine types}} ',
|
||||
values: { machineTypes: cloud.machineTypes.length },
|
||||
}
|
||||
),
|
||||
description: (
|
||||
<ul>
|
||||
{cloud.machineTypes.map((type, index) => (
|
||||
<li key={index}>
|
||||
<EuiBadge color="hollow">{type}</EuiBadge>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
if (cloud.projectName) {
|
||||
listItems.push({
|
||||
title: i18n.translate(
|
||||
'xpack.apm.serviceIcons.serviceDetails.cloud.projectIdLabel',
|
||||
{
|
||||
defaultMessage: 'Project ID',
|
||||
}
|
||||
),
|
||||
description: cloud.projectName,
|
||||
});
|
||||
}
|
||||
|
||||
return <EuiDescriptionList textStyle="reverse" listItems={listItems} />;
|
||||
}
|
|
@ -0,0 +1,81 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { EuiDescriptionList } from '@elastic/eui';
|
||||
import { EuiDescriptionListProps } from '@elastic/eui/src/components/description_list/description_list';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
import { asInteger } from '../../../../../common/utils/formatters';
|
||||
import { APIReturnType } from '../../../../services/rest/createCallApmApi';
|
||||
|
||||
type ServiceDetailsReturnType = APIReturnType<'GET /api/apm/services/{serviceName}/metadata/details'>;
|
||||
|
||||
interface Props {
|
||||
container: ServiceDetailsReturnType['container'];
|
||||
}
|
||||
|
||||
export function ContainerDetails({ container }: Props) {
|
||||
if (!container) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const listItems: EuiDescriptionListProps['listItems'] = [];
|
||||
if (container.os) {
|
||||
listItems.push({
|
||||
title: i18n.translate(
|
||||
'xpack.apm.serviceIcons.serviceDetails.container.osLabel',
|
||||
{
|
||||
defaultMessage: 'OS',
|
||||
}
|
||||
),
|
||||
description: container.os,
|
||||
});
|
||||
}
|
||||
|
||||
if (container.isContainerized !== undefined) {
|
||||
listItems.push({
|
||||
title: i18n.translate(
|
||||
'xpack.apm.serviceIcons.serviceDetails.container.containerizedLabel',
|
||||
{ defaultMessage: 'Containerized' }
|
||||
),
|
||||
description: container.isContainerized
|
||||
? i18n.translate(
|
||||
'xpack.apm.serviceIcons.serviceDetails.container.yesLabel',
|
||||
{
|
||||
defaultMessage: 'Yes',
|
||||
}
|
||||
)
|
||||
: i18n.translate(
|
||||
'xpack.apm.serviceIcons.serviceDetails.container.noLabel',
|
||||
{
|
||||
defaultMessage: 'No',
|
||||
}
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
if (container.totalNumberInstances) {
|
||||
listItems.push({
|
||||
title: i18n.translate(
|
||||
'xpack.apm.serviceIcons.serviceDetails.container.totalNumberInstancesLabel',
|
||||
{ defaultMessage: 'Total number of instances' }
|
||||
),
|
||||
description: asInteger(container.totalNumberInstances),
|
||||
});
|
||||
}
|
||||
|
||||
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} />;
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import {
|
||||
EuiButtonEmpty,
|
||||
EuiIcon,
|
||||
EuiLoadingContent,
|
||||
EuiPopover,
|
||||
EuiPopoverTitle,
|
||||
} from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { FETCH_STATUS } from '../../../../hooks/use_fetcher';
|
||||
import { px } from '../../../../style/variables';
|
||||
|
||||
interface IconPopoverProps {
|
||||
title: string;
|
||||
children: React.ReactChild;
|
||||
onOpen: () => void;
|
||||
onClose: () => void;
|
||||
detailsFetchStatus: FETCH_STATUS;
|
||||
isOpen: boolean;
|
||||
icon?: string;
|
||||
}
|
||||
export function IconPopover({
|
||||
icon,
|
||||
title,
|
||||
children,
|
||||
onOpen,
|
||||
onClose,
|
||||
detailsFetchStatus,
|
||||
isOpen,
|
||||
}: IconPopoverProps) {
|
||||
if (!icon) {
|
||||
return null;
|
||||
}
|
||||
const isLoading =
|
||||
detailsFetchStatus === FETCH_STATUS.LOADING ||
|
||||
detailsFetchStatus === FETCH_STATUS.PENDING;
|
||||
|
||||
return (
|
||||
<EuiPopover
|
||||
anchorPosition="downCenter"
|
||||
ownFocus={false}
|
||||
button={
|
||||
<EuiButtonEmpty onClick={onOpen} data-test-subj={`popover_${title}`}>
|
||||
<EuiIcon type={icon} size="l" color="black" />
|
||||
</EuiButtonEmpty>
|
||||
}
|
||||
isOpen={isOpen}
|
||||
closePopover={onClose}
|
||||
>
|
||||
<EuiPopoverTitle>{title}</EuiPopoverTitle>
|
||||
<div style={{ minWidth: px(300) }}>
|
||||
{isLoading ? (
|
||||
<EuiLoadingContent data-test-subj="loading-content" />
|
||||
) : (
|
||||
children
|
||||
)}
|
||||
</div>
|
||||
</EuiPopover>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,232 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { fireEvent, render } from '@testing-library/react';
|
||||
import { CoreStart } from 'kibana/public';
|
||||
import { merge } from 'lodash';
|
||||
// import { renderWithTheme } from '../../../../utils/testHelpers';
|
||||
import React, { ReactNode } from 'react';
|
||||
import { createKibanaReactContext } from 'src/plugins/kibana_react/public';
|
||||
import { MockUrlParamsContextProvider } from '../../../../context/url_params_context/mock_url_params_context_provider';
|
||||
import { ApmPluginContextValue } from '../../../../context/apm_plugin/apm_plugin_context';
|
||||
import {
|
||||
mockApmPluginContextValue,
|
||||
MockApmPluginContextWrapper,
|
||||
} from '../../../../context/apm_plugin/mock_apm_plugin_context';
|
||||
import * as fetcherHook from '../../../../hooks/use_fetcher';
|
||||
import { ServiceIcons } from './';
|
||||
|
||||
const KibanaReactContext = createKibanaReactContext({
|
||||
usageCollection: { reportUiCounter: () => {} },
|
||||
} as Partial<CoreStart>);
|
||||
|
||||
const addWarning = jest.fn();
|
||||
const httpGet = jest.fn();
|
||||
|
||||
function Wrapper({ children }: { children?: ReactNode }) {
|
||||
const mockPluginContext = (merge({}, mockApmPluginContextValue, {
|
||||
core: { http: { get: httpGet }, notifications: { toasts: { addWarning } } },
|
||||
}) as unknown) as ApmPluginContextValue;
|
||||
|
||||
return (
|
||||
<KibanaReactContext.Provider>
|
||||
<MockApmPluginContextWrapper value={mockPluginContext}>
|
||||
<MockUrlParamsContextProvider
|
||||
params={{
|
||||
rangeFrom: 'now-15m',
|
||||
rangeTo: 'now',
|
||||
start: 'mystart',
|
||||
end: 'myend',
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</MockUrlParamsContextProvider>
|
||||
</MockApmPluginContextWrapper>
|
||||
</KibanaReactContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
describe('ServiceIcons', () => {
|
||||
describe('icons', () => {
|
||||
it('Shows loading spinner while fetching data', () => {
|
||||
jest.spyOn(fetcherHook, 'useFetcher').mockReturnValue({
|
||||
data: undefined,
|
||||
status: fetcherHook.FETCH_STATUS.LOADING,
|
||||
refetch: jest.fn(),
|
||||
});
|
||||
const { getByTestId, queryAllByTestId } = render(
|
||||
<Wrapper>
|
||||
<ServiceIcons serviceName="foo" />
|
||||
</Wrapper>
|
||||
);
|
||||
expect(getByTestId('loading')).toBeInTheDocument();
|
||||
expect(queryAllByTestId('service')).toHaveLength(0);
|
||||
expect(queryAllByTestId('container')).toHaveLength(0);
|
||||
expect(queryAllByTestId('cloud')).toHaveLength(0);
|
||||
});
|
||||
it("doesn't show any icons", () => {
|
||||
jest.spyOn(fetcherHook, 'useFetcher').mockReturnValue({
|
||||
data: {},
|
||||
status: fetcherHook.FETCH_STATUS.SUCCESS,
|
||||
refetch: jest.fn(),
|
||||
});
|
||||
|
||||
const { queryAllByTestId } = render(
|
||||
<Wrapper>
|
||||
<ServiceIcons serviceName="foo" />
|
||||
</Wrapper>
|
||||
);
|
||||
expect(queryAllByTestId('loading')).toHaveLength(0);
|
||||
expect(queryAllByTestId('service')).toHaveLength(0);
|
||||
expect(queryAllByTestId('container')).toHaveLength(0);
|
||||
expect(queryAllByTestId('cloud')).toHaveLength(0);
|
||||
});
|
||||
it('shows service icon', () => {
|
||||
jest.spyOn(fetcherHook, 'useFetcher').mockReturnValue({
|
||||
data: {
|
||||
agentName: 'java',
|
||||
},
|
||||
status: fetcherHook.FETCH_STATUS.SUCCESS,
|
||||
refetch: jest.fn(),
|
||||
});
|
||||
|
||||
const { queryAllByTestId, getByTestId } = render(
|
||||
<Wrapper>
|
||||
<ServiceIcons serviceName="foo" />
|
||||
</Wrapper>
|
||||
);
|
||||
expect(queryAllByTestId('loading')).toHaveLength(0);
|
||||
expect(getByTestId('service')).toBeInTheDocument();
|
||||
expect(queryAllByTestId('container')).toHaveLength(0);
|
||||
expect(queryAllByTestId('cloud')).toHaveLength(0);
|
||||
});
|
||||
it('shows service and container icons', () => {
|
||||
jest.spyOn(fetcherHook, 'useFetcher').mockReturnValue({
|
||||
data: {
|
||||
agentName: 'java',
|
||||
containerType: 'Kubernetes',
|
||||
},
|
||||
status: fetcherHook.FETCH_STATUS.SUCCESS,
|
||||
refetch: jest.fn(),
|
||||
});
|
||||
|
||||
const { queryAllByTestId, getByTestId } = render(
|
||||
<Wrapper>
|
||||
<ServiceIcons serviceName="foo" />
|
||||
</Wrapper>
|
||||
);
|
||||
expect(queryAllByTestId('loading')).toHaveLength(0);
|
||||
expect(queryAllByTestId('cloud')).toHaveLength(0);
|
||||
expect(getByTestId('service')).toBeInTheDocument();
|
||||
expect(getByTestId('container')).toBeInTheDocument();
|
||||
});
|
||||
it('shows service, container and cloud icons', () => {
|
||||
jest.spyOn(fetcherHook, 'useFetcher').mockReturnValue({
|
||||
data: {
|
||||
agentName: 'java',
|
||||
containerType: 'Kubernetes',
|
||||
cloudProvider: 'gcp',
|
||||
},
|
||||
status: fetcherHook.FETCH_STATUS.SUCCESS,
|
||||
refetch: jest.fn(),
|
||||
});
|
||||
|
||||
const { queryAllByTestId, getByTestId } = render(
|
||||
<Wrapper>
|
||||
<ServiceIcons serviceName="foo" />
|
||||
</Wrapper>
|
||||
);
|
||||
expect(queryAllByTestId('loading')).toHaveLength(0);
|
||||
expect(getByTestId('service')).toBeInTheDocument();
|
||||
expect(getByTestId('container')).toBeInTheDocument();
|
||||
expect(getByTestId('cloud')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('details', () => {
|
||||
const callApmApi = (apisMockData: Record<string, object>) => ({
|
||||
endpoint,
|
||||
}: {
|
||||
endpoint: string;
|
||||
}) => {
|
||||
return apisMockData[endpoint];
|
||||
};
|
||||
it('Shows loading spinner while fetching data', () => {
|
||||
const apisMockData = {
|
||||
'GET /api/apm/services/{serviceName}/metadata/icons': {
|
||||
data: {
|
||||
agentName: 'java',
|
||||
containerType: 'Kubernetes',
|
||||
cloudProvider: 'gcp',
|
||||
},
|
||||
status: fetcherHook.FETCH_STATUS.SUCCESS,
|
||||
refetch: jest.fn(),
|
||||
},
|
||||
'GET /api/apm/services/{serviceName}/metadata/details': {
|
||||
data: undefined,
|
||||
status: fetcherHook.FETCH_STATUS.LOADING,
|
||||
refetch: jest.fn(),
|
||||
},
|
||||
};
|
||||
jest
|
||||
.spyOn(fetcherHook, 'useFetcher')
|
||||
.mockImplementation((func: Function, deps: string[]) => {
|
||||
return func(callApmApi(apisMockData)) || {};
|
||||
});
|
||||
|
||||
const { queryAllByTestId, getByTestId } = render(
|
||||
<Wrapper>
|
||||
<ServiceIcons serviceName="foo" />
|
||||
</Wrapper>
|
||||
);
|
||||
expect(queryAllByTestId('loading')).toHaveLength(0);
|
||||
expect(getByTestId('service')).toBeInTheDocument();
|
||||
expect(getByTestId('container')).toBeInTheDocument();
|
||||
expect(getByTestId('cloud')).toBeInTheDocument();
|
||||
fireEvent.click(getByTestId('popover_Service'));
|
||||
expect(getByTestId('loading-content')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows service content', () => {
|
||||
const apisMockData = {
|
||||
'GET /api/apm/services/{serviceName}/metadata/icons': {
|
||||
data: {
|
||||
agentName: 'java',
|
||||
containerType: 'Kubernetes',
|
||||
cloudProvider: 'gcp',
|
||||
},
|
||||
status: fetcherHook.FETCH_STATUS.SUCCESS,
|
||||
refetch: jest.fn(),
|
||||
},
|
||||
'GET /api/apm/services/{serviceName}/metadata/details': {
|
||||
data: { service: { versions: ['v1.0.0'] } },
|
||||
status: fetcherHook.FETCH_STATUS.SUCCESS,
|
||||
refetch: jest.fn(),
|
||||
},
|
||||
};
|
||||
jest
|
||||
.spyOn(fetcherHook, 'useFetcher')
|
||||
.mockImplementation((func: Function, deps: string[]) => {
|
||||
return func(callApmApi(apisMockData)) || {};
|
||||
});
|
||||
|
||||
const { queryAllByTestId, getByTestId, getByText } = render(
|
||||
<Wrapper>
|
||||
<ServiceIcons serviceName="foo" />
|
||||
</Wrapper>
|
||||
);
|
||||
expect(queryAllByTestId('loading')).toHaveLength(0);
|
||||
expect(getByTestId('service')).toBeInTheDocument();
|
||||
expect(getByTestId('container')).toBeInTheDocument();
|
||||
expect(getByTestId('cloud')).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(getByTestId('popover_Service'));
|
||||
expect(queryAllByTestId('loading-content')).toHaveLength(0);
|
||||
expect(getByText('Service')).toBeInTheDocument();
|
||||
expect(getByText('v1.0.0')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,161 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React, { ReactChild, useState } from 'react';
|
||||
import { ContainerType } from '../../../../../common/service_metadata';
|
||||
import { useUrlParams } from '../../../../context/url_params_context/use_url_params';
|
||||
import { FETCH_STATUS, useFetcher } from '../../../../hooks/use_fetcher';
|
||||
import { getAgentIcon } from '../../../shared/AgentIcon/get_agent_icon';
|
||||
import { CloudDetails } from './cloud_details';
|
||||
import { ContainerDetails } from './container_details';
|
||||
import { IconPopover } from './icon_popover';
|
||||
import { ServiceDetails } from './service_details';
|
||||
|
||||
interface Props {
|
||||
serviceName: string;
|
||||
}
|
||||
|
||||
const cloudIcons: Record<string, string> = {
|
||||
gcp: 'logoGCP',
|
||||
aws: 'logoAWS',
|
||||
azure: 'logoAzure',
|
||||
};
|
||||
|
||||
function getCloudIcon(provider?: string) {
|
||||
if (provider) {
|
||||
return cloudIcons[provider];
|
||||
}
|
||||
}
|
||||
|
||||
function getContainerIcon(container?: ContainerType) {
|
||||
if (!container) {
|
||||
return;
|
||||
}
|
||||
switch (container) {
|
||||
case 'Kubernetes':
|
||||
return 'logoKubernetes';
|
||||
default:
|
||||
return 'logoDocker';
|
||||
}
|
||||
}
|
||||
|
||||
type Icons = 'service' | 'container' | 'cloud';
|
||||
interface PopoverItem {
|
||||
key: Icons;
|
||||
icon?: string;
|
||||
isVisible: boolean;
|
||||
title: string;
|
||||
component: ReactChild;
|
||||
}
|
||||
|
||||
export function ServiceIcons({ serviceName }: Props) {
|
||||
const {
|
||||
urlParams: { start, end },
|
||||
uiFilters,
|
||||
} = useUrlParams();
|
||||
const [
|
||||
selectedIconPopover,
|
||||
setSelectedIconPopover,
|
||||
] = useState<Icons | null>();
|
||||
|
||||
const { data: icons, status: iconsFetchStatus } = useFetcher(
|
||||
(callApmApi) => {
|
||||
if (serviceName && start && end) {
|
||||
return callApmApi({
|
||||
endpoint: 'GET /api/apm/services/{serviceName}/metadata/icons',
|
||||
params: {
|
||||
path: { serviceName },
|
||||
query: { start, end, uiFilters: JSON.stringify(uiFilters) },
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
[serviceName, start, end, uiFilters]
|
||||
);
|
||||
|
||||
const { data: details, status: detailsFetchStatus } = useFetcher(
|
||||
(callApmApi) => {
|
||||
if (selectedIconPopover && serviceName && start && end) {
|
||||
return callApmApi({
|
||||
endpoint: 'GET /api/apm/services/{serviceName}/metadata/details',
|
||||
params: {
|
||||
path: { serviceName },
|
||||
query: { start, end, uiFilters: JSON.stringify(uiFilters) },
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
[selectedIconPopover, serviceName, start, end, uiFilters]
|
||||
);
|
||||
|
||||
const isLoading =
|
||||
!icons &&
|
||||
(iconsFetchStatus === FETCH_STATUS.LOADING ||
|
||||
iconsFetchStatus === FETCH_STATUS.PENDING);
|
||||
|
||||
if (isLoading) {
|
||||
return <EuiLoadingSpinner data-test-subj="loading" />;
|
||||
}
|
||||
|
||||
const popoverItems: PopoverItem[] = [
|
||||
{
|
||||
key: 'service',
|
||||
icon: getAgentIcon(icons?.agentName) || 'node',
|
||||
isVisible: !!icons?.agentName,
|
||||
title: i18n.translate('xpack.apm.serviceIcons.service', {
|
||||
defaultMessage: 'Service',
|
||||
}),
|
||||
component: <ServiceDetails service={details?.service} />,
|
||||
},
|
||||
{
|
||||
key: 'container',
|
||||
icon: getContainerIcon(icons?.containerType),
|
||||
isVisible: !!icons?.containerType,
|
||||
title: i18n.translate('xpack.apm.serviceIcons.container', {
|
||||
defaultMessage: 'Container',
|
||||
}),
|
||||
component: <ContainerDetails container={details?.container} />,
|
||||
},
|
||||
{
|
||||
key: 'cloud',
|
||||
icon: getCloudIcon(icons?.cloudProvider),
|
||||
isVisible: !!icons?.cloudProvider,
|
||||
title: i18n.translate('xpack.apm.serviceIcons.cloud', {
|
||||
defaultMessage: 'Cloud',
|
||||
}),
|
||||
component: <CloudDetails cloud={details?.cloud} />,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<EuiFlexGroup gutterSize="s" responsive={false}>
|
||||
{popoverItems.map((item) => {
|
||||
if (item.isVisible) {
|
||||
return (
|
||||
<EuiFlexItem grow={false} data-test-subj={item.key} key={item.key}>
|
||||
<IconPopover
|
||||
isOpen={selectedIconPopover === item.key}
|
||||
icon={item.icon}
|
||||
detailsFetchStatus={detailsFetchStatus}
|
||||
title={item.title}
|
||||
onOpen={() => {
|
||||
setSelectedIconPopover(item.key);
|
||||
}}
|
||||
onClose={() => {
|
||||
setSelectedIconPopover(null);
|
||||
}}
|
||||
>
|
||||
{item.component}
|
||||
</IconPopover>
|
||||
</EuiFlexItem>
|
||||
);
|
||||
}
|
||||
})}
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,88 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { EuiDescriptionList } from '@elastic/eui';
|
||||
import { EuiDescriptionListProps } from '@elastic/eui/src/components/description_list/description_list';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
import { APIReturnType } from '../../../../services/rest/createCallApmApi';
|
||||
|
||||
type ServiceDetailsReturnType = APIReturnType<'GET /api/apm/services/{serviceName}/metadata/details'>;
|
||||
|
||||
interface Props {
|
||||
service: ServiceDetailsReturnType['service'];
|
||||
}
|
||||
|
||||
export function ServiceDetails({ service }: Props) {
|
||||
if (!service) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const listItems: EuiDescriptionListProps['listItems'] = [];
|
||||
if (!!service.versions?.length) {
|
||||
listItems.push({
|
||||
title: i18n.translate(
|
||||
'xpack.apm.serviceIcons.serviceDetails.service.versionLabel',
|
||||
{
|
||||
defaultMessage: 'Service version',
|
||||
}
|
||||
),
|
||||
description: (
|
||||
<ul>
|
||||
{service.versions.map((version, index) => (
|
||||
<li key={index}>{version}</li>
|
||||
))}
|
||||
</ul>
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
if (service.runtime) {
|
||||
listItems.push({
|
||||
title: i18n.translate(
|
||||
'xpack.apm.serviceIcons.serviceDetails.service.runtimeLabel',
|
||||
{
|
||||
defaultMessage: 'Runtime name & version',
|
||||
}
|
||||
),
|
||||
description: (
|
||||
<>
|
||||
{service.runtime.name} {service.runtime.version}
|
||||
</>
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
if (service.framework) {
|
||||
listItems.push({
|
||||
title: i18n.translate(
|
||||
'xpack.apm.serviceIcons.serviceDetails.service.frameworkLabel',
|
||||
{
|
||||
defaultMessage: 'Framework name',
|
||||
}
|
||||
),
|
||||
description: service.framework,
|
||||
});
|
||||
}
|
||||
|
||||
if (service.agent) {
|
||||
listItems.push({
|
||||
title: i18n.translate(
|
||||
'xpack.apm.serviceIcons.serviceDetails.service.agentLabel',
|
||||
{
|
||||
defaultMessage: 'Agent name & version',
|
||||
}
|
||||
),
|
||||
description: (
|
||||
<>
|
||||
{service.agent.name} {service.agent.version}
|
||||
</>
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
return <EuiDescriptionList textStyle="reverse" listItems={listItems} />;
|
||||
}
|
|
@ -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;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { SortOptions } from '../../../../../typings/elasticsearch';
|
||||
import {
|
||||
AGENT,
|
||||
CLOUD,
|
||||
CLOUD_AVAILABILITY_ZONE,
|
||||
CLOUD_MACHINE_TYPE,
|
||||
CONTAINER,
|
||||
HOST,
|
||||
KUBERNETES,
|
||||
PROCESSOR_EVENT,
|
||||
SERVICE,
|
||||
SERVICE_NAME,
|
||||
SERVICE_NODE_NAME,
|
||||
SERVICE_VERSION,
|
||||
} from '../../../common/elasticsearch_fieldnames';
|
||||
import { ProcessorEvent } from '../../../common/processor_event';
|
||||
import { ContainerType } from '../../../common/service_metadata';
|
||||
import { rangeFilter } from '../../../common/utils/range_filter';
|
||||
import { TransactionRaw } from '../../../typings/es_schemas/raw/transaction_raw';
|
||||
import { Setup, SetupTimeRange } from '../helpers/setup_request';
|
||||
|
||||
type ServiceMetadataDetailsRaw = Pick<
|
||||
TransactionRaw,
|
||||
'service' | 'agent' | 'host' | 'container' | 'kubernetes' | 'cloud'
|
||||
>;
|
||||
|
||||
interface ServiceMetadataDetails {
|
||||
service?: {
|
||||
versions?: string[];
|
||||
runtime?: {
|
||||
name: string;
|
||||
version: string;
|
||||
};
|
||||
framework?: string;
|
||||
agent: {
|
||||
name: string;
|
||||
version: string;
|
||||
};
|
||||
};
|
||||
container?: {
|
||||
os?: string;
|
||||
isContainerized?: boolean;
|
||||
totalNumberInstances?: number;
|
||||
type?: ContainerType;
|
||||
};
|
||||
cloud?: {
|
||||
provider?: string;
|
||||
availabilityZones?: string[];
|
||||
machineTypes?: string[];
|
||||
projectName?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export async function getServiceMetadataDetails({
|
||||
serviceName,
|
||||
setup,
|
||||
}: {
|
||||
serviceName: string;
|
||||
setup: Setup & SetupTimeRange;
|
||||
}): Promise<ServiceMetadataDetails> {
|
||||
const { start, end, apmEventClient } = setup;
|
||||
|
||||
const filter = [
|
||||
{ term: { [SERVICE_NAME]: serviceName } },
|
||||
{ term: { [PROCESSOR_EVENT]: ProcessorEvent.transaction } },
|
||||
{ range: rangeFilter(start, end) },
|
||||
...setup.esFilter,
|
||||
];
|
||||
|
||||
const should = [
|
||||
{ exists: { field: CONTAINER } },
|
||||
{ exists: { field: KUBERNETES } },
|
||||
{ exists: { field: CLOUD } },
|
||||
{ exists: { field: HOST } },
|
||||
{ exists: { field: AGENT } },
|
||||
];
|
||||
|
||||
const params = {
|
||||
apm: {
|
||||
events: [ProcessorEvent.transaction],
|
||||
},
|
||||
body: {
|
||||
size: 1,
|
||||
_source: [SERVICE, AGENT, HOST, CONTAINER, KUBERNETES, CLOUD],
|
||||
query: { bool: { filter, should } },
|
||||
aggs: {
|
||||
serviceVersions: {
|
||||
terms: {
|
||||
field: SERVICE_VERSION,
|
||||
size: 10,
|
||||
order: { _key: 'desc' } as SortOptions,
|
||||
},
|
||||
},
|
||||
availabilityZones: {
|
||||
terms: {
|
||||
field: CLOUD_AVAILABILITY_ZONE,
|
||||
size: 10,
|
||||
},
|
||||
},
|
||||
machineTypes: {
|
||||
terms: {
|
||||
field: CLOUD_MACHINE_TYPE,
|
||||
size: 10,
|
||||
},
|
||||
},
|
||||
totalNumberInstances: { cardinality: { field: SERVICE_NODE_NAME } },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const response = await apmEventClient.search(params);
|
||||
|
||||
if (response.hits.total.value === 0) {
|
||||
return {
|
||||
service: undefined,
|
||||
container: undefined,
|
||||
cloud: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
const { service, agent, host, kubernetes, container, cloud } = response.hits
|
||||
.hits[0]._source as ServiceMetadataDetailsRaw;
|
||||
|
||||
const serviceMetadataDetails = {
|
||||
versions: response.aggregations?.serviceVersions.buckets.map(
|
||||
(bucket) => bucket.key as string
|
||||
),
|
||||
runtime: service.runtime,
|
||||
framework: service.framework?.name,
|
||||
agent,
|
||||
};
|
||||
|
||||
const totalNumberInstances =
|
||||
response.aggregations?.totalNumberInstances.value;
|
||||
|
||||
const containerDetails =
|
||||
host || container || totalNumberInstances || kubernetes
|
||||
? {
|
||||
os: host?.os?.platform,
|
||||
type: (!!kubernetes ? 'Kubernetes' : 'Docker') as ContainerType,
|
||||
isContainerized: !!container?.id,
|
||||
totalNumberInstances,
|
||||
}
|
||||
: undefined;
|
||||
|
||||
const cloudDetails = cloud
|
||||
? {
|
||||
provider: cloud.provider,
|
||||
projectName: cloud.project?.name,
|
||||
availabilityZones: response.aggregations?.availabilityZones.buckets.map(
|
||||
(bucket) => bucket.key as string
|
||||
),
|
||||
machineTypes: response.aggregations?.machineTypes.buckets.map(
|
||||
(bucket) => bucket.key as string
|
||||
),
|
||||
}
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
service: serviceMetadataDetails,
|
||||
container: containerDetails,
|
||||
cloud: cloudDetails,
|
||||
};
|
||||
}
|
|
@ -0,0 +1,85 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import {
|
||||
AGENT_NAME,
|
||||
CLOUD_PROVIDER,
|
||||
CONTAINER_ID,
|
||||
KUBERNETES,
|
||||
PROCESSOR_EVENT,
|
||||
SERVICE_NAME,
|
||||
} from '../../../common/elasticsearch_fieldnames';
|
||||
import { ProcessorEvent } from '../../../common/processor_event';
|
||||
import { ContainerType } from '../../../common/service_metadata';
|
||||
import { rangeFilter } from '../../../common/utils/range_filter';
|
||||
import { TransactionRaw } from '../../../typings/es_schemas/raw/transaction_raw';
|
||||
import { Setup, SetupTimeRange } from '../helpers/setup_request';
|
||||
|
||||
type ServiceMetadataIconsRaw = Pick<
|
||||
TransactionRaw,
|
||||
'kubernetes' | 'cloud' | 'container' | 'agent'
|
||||
>;
|
||||
|
||||
interface ServiceMetadataIcons {
|
||||
agentName?: string;
|
||||
containerType?: ContainerType;
|
||||
cloudProvider?: string;
|
||||
}
|
||||
|
||||
export async function getServiceMetadataIcons({
|
||||
serviceName,
|
||||
setup,
|
||||
}: {
|
||||
serviceName: string;
|
||||
setup: Setup & SetupTimeRange;
|
||||
}): Promise<ServiceMetadataIcons> {
|
||||
const { start, end, apmEventClient } = setup;
|
||||
|
||||
const filter = [
|
||||
{ term: { [SERVICE_NAME]: serviceName } },
|
||||
{ term: { [PROCESSOR_EVENT]: ProcessorEvent.transaction } },
|
||||
{ range: rangeFilter(start, end) },
|
||||
...setup.esFilter,
|
||||
];
|
||||
|
||||
const params = {
|
||||
apm: {
|
||||
events: [ProcessorEvent.transaction],
|
||||
},
|
||||
terminateAfter: 1,
|
||||
body: {
|
||||
size: 1,
|
||||
_source: [KUBERNETES, CLOUD_PROVIDER, CONTAINER_ID, AGENT_NAME],
|
||||
query: { bool: { filter } },
|
||||
},
|
||||
};
|
||||
|
||||
const response = await apmEventClient.search(params);
|
||||
|
||||
if (response.hits.total.value === 0) {
|
||||
return {
|
||||
agentName: undefined,
|
||||
containerType: undefined,
|
||||
cloudProvider: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
const { kubernetes, cloud, container, agent } = response.hits.hits[0]
|
||||
._source as ServiceMetadataIconsRaw;
|
||||
|
||||
let containerType: ContainerType;
|
||||
if (!!kubernetes) {
|
||||
containerType = 'Kubernetes';
|
||||
} else if (!!container) {
|
||||
containerType = 'Docker';
|
||||
}
|
||||
|
||||
return {
|
||||
agentName: agent?.name,
|
||||
containerType,
|
||||
cloudProvider: cloud?.provider,
|
||||
};
|
||||
}
|
|
@ -24,6 +24,8 @@ import {
|
|||
serviceErrorGroupsRoute,
|
||||
serviceThroughputRoute,
|
||||
serviceDependenciesRoute,
|
||||
serviceMetadataDetailsRoute,
|
||||
serviceMetadataIconsRoute,
|
||||
serviceInstancesRoute,
|
||||
} from './services';
|
||||
import {
|
||||
|
@ -130,6 +132,8 @@ const createApmApi = () => {
|
|||
.add(serviceErrorGroupsRoute)
|
||||
.add(serviceThroughputRoute)
|
||||
.add(serviceDependenciesRoute)
|
||||
.add(serviceMetadataDetailsRoute)
|
||||
.add(serviceMetadataIconsRoute)
|
||||
.add(serviceInstancesRoute)
|
||||
|
||||
// Agent configuration
|
||||
|
|
|
@ -22,6 +22,8 @@ import { getServiceDependencies } from '../lib/services/get_service_dependencies
|
|||
import { toNumberRt } from '../../common/runtime_types/to_number_rt';
|
||||
import { getThroughput } from '../lib/services/get_throughput';
|
||||
import { getServiceInstances } from '../lib/services/get_service_instances';
|
||||
import { getServiceMetadataDetails } from '../lib/services/get_service_metadata_details';
|
||||
import { getServiceMetadataIcons } from '../lib/services/get_service_metadata_icons';
|
||||
|
||||
export const servicesRoute = createRoute({
|
||||
endpoint: 'GET /api/apm/services',
|
||||
|
@ -46,6 +48,36 @@ export const servicesRoute = createRoute({
|
|||
},
|
||||
});
|
||||
|
||||
export const serviceMetadataDetailsRoute = createRoute({
|
||||
endpoint: 'GET /api/apm/services/{serviceName}/metadata/details',
|
||||
params: t.type({
|
||||
path: t.type({ serviceName: t.string }),
|
||||
query: t.intersection([uiFiltersRt, rangeRt]),
|
||||
}),
|
||||
options: { tags: ['access:apm'] },
|
||||
handler: async ({ context, request }) => {
|
||||
const setup = await setupRequest(context, request);
|
||||
const { serviceName } = context.params.path;
|
||||
|
||||
return getServiceMetadataDetails({ serviceName, setup });
|
||||
},
|
||||
});
|
||||
|
||||
export const serviceMetadataIconsRoute = createRoute({
|
||||
endpoint: 'GET /api/apm/services/{serviceName}/metadata/icons',
|
||||
params: t.type({
|
||||
path: t.type({ serviceName: t.string }),
|
||||
query: t.intersection([uiFiltersRt, rangeRt]),
|
||||
}),
|
||||
options: { tags: ['access:apm'] },
|
||||
handler: async ({ context, request }) => {
|
||||
const setup = await setupRequest(context, request);
|
||||
const { serviceName } = context.params.path;
|
||||
|
||||
return getServiceMetadataIcons({ serviceName, setup });
|
||||
},
|
||||
});
|
||||
|
||||
export const serviceAgentNameRoute = createRoute({
|
||||
endpoint: 'GET /api/apm/services/{serviceName}/agent_name',
|
||||
params: t.type({
|
||||
|
|
29
x-pack/plugins/apm/typings/es_schemas/raw/fields/cloud.ts
Normal file
29
x-pack/plugins/apm/typings/es_schemas/raw/fields/cloud.ts
Normal file
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export interface Cloud {
|
||||
availability_zone?: string;
|
||||
instance?: {
|
||||
name: string;
|
||||
id: string;
|
||||
};
|
||||
machine?: {
|
||||
type: string;
|
||||
};
|
||||
project?: {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
provider?: string;
|
||||
region?: string;
|
||||
account?: {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
image?: {
|
||||
id: string;
|
||||
};
|
||||
}
|
|
@ -5,6 +5,7 @@
|
|||
*/
|
||||
|
||||
import { APMBaseDoc } from './apm_base_doc';
|
||||
import { Cloud } from './fields/cloud';
|
||||
import { Container } from './fields/container';
|
||||
import { Host } from './fields/host';
|
||||
import { Http } from './fields/http';
|
||||
|
@ -64,4 +65,5 @@ export interface TransactionRaw extends APMBaseDoc {
|
|||
url?: Url;
|
||||
user?: User;
|
||||
user_agent?: UserAgent;
|
||||
cloud?: Cloud;
|
||||
}
|
||||
|
|
|
@ -5014,7 +5014,6 @@
|
|||
"xpack.apm.servicesTable.7xUpgradeServerMessage": "バージョン7.xより前からのアップグレードですか?また、\n APMサーバーインスタンスを7.0以降にアップグレードしていることも確認してください。",
|
||||
"xpack.apm.servicesTable.avgResponseTimeColumnLabel": "平均応答時間",
|
||||
"xpack.apm.servicesTable.environmentColumnLabel": "環境",
|
||||
"xpack.apm.servicesTable.environmentCount": "{environmentCount, plural, one {1 個の環境} other {# 個の環境}}",
|
||||
"xpack.apm.servicesTable.healthColumnLabel": "ヘルス",
|
||||
"xpack.apm.servicesTable.nameColumnLabel": "名前",
|
||||
"xpack.apm.servicesTable.noServicesLabel": "APM サービスがインストールされていないようです。追加しましょう!",
|
||||
|
|
|
@ -5018,7 +5018,6 @@
|
|||
"xpack.apm.servicesTable.7xUpgradeServerMessage": "从 7.x 之前的版本升级?另外,确保您已将\n APM Server 实例升级到至少 7.0。",
|
||||
"xpack.apm.servicesTable.avgResponseTimeColumnLabel": "平均响应时间",
|
||||
"xpack.apm.servicesTable.environmentColumnLabel": "环境",
|
||||
"xpack.apm.servicesTable.environmentCount": "{environmentCount, plural, one {1 个环境} other {# 个环境}}",
|
||||
"xpack.apm.servicesTable.healthColumnLabel": "运行状况",
|
||||
"xpack.apm.servicesTable.nameColumnLabel": "名称",
|
||||
"xpack.apm.servicesTable.noServicesLabel": "似乎您没有安装任何 APM 服务。让我们添加一些!",
|
||||
|
|
|
@ -170,6 +170,20 @@ export default function featureControlsTests({ getService }: FtrProviderContext)
|
|||
expectForbidden: expect403,
|
||||
expectResponse: expect200,
|
||||
},
|
||||
{
|
||||
req: {
|
||||
url: `/api/apm/services/foo/metadata/details?start=${start}&end=${end}&uiFilters=%7B%7D`,
|
||||
},
|
||||
expectForbidden: expect403,
|
||||
expectResponse: expect200,
|
||||
},
|
||||
{
|
||||
req: {
|
||||
url: `/api/apm/services/foo/metadata/icons?start=${start}&end=${end}&uiFilters=%7B%7D`,
|
||||
},
|
||||
expectForbidden: expect403,
|
||||
expectResponse: expect200,
|
||||
},
|
||||
];
|
||||
|
||||
const elasticsearchPrivileges = {
|
||||
|
|
|
@ -25,6 +25,8 @@ export default function apmApiIntegrationTests({ loadTestFile }: FtrProviderCont
|
|||
loadTestFile(require.resolve('./services/throughput'));
|
||||
loadTestFile(require.resolve('./services/top_services'));
|
||||
loadTestFile(require.resolve('./services/transaction_types'));
|
||||
loadTestFile(require.resolve('./services/service_details'));
|
||||
loadTestFile(require.resolve('./services/service_icons'));
|
||||
});
|
||||
|
||||
describe('Service overview', function () {
|
||||
|
|
|
@ -0,0 +1,134 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import expect from '@kbn/expect';
|
||||
import url from 'url';
|
||||
import { FtrProviderContext } from '../../../../common/ftr_provider_context';
|
||||
import archives from '../../../common/archives_metadata';
|
||||
|
||||
export default function ApiTest({ getService }: FtrProviderContext) {
|
||||
const supertest = getService('supertest');
|
||||
const esArchiver = getService('esArchiver');
|
||||
|
||||
const archiveName = 'apm_8.0.0';
|
||||
const { start, end } = archives[archiveName];
|
||||
|
||||
describe('Service details', () => {
|
||||
describe('when data is not loaded ', () => {
|
||||
it('handles the empty state', async () => {
|
||||
const response = await supertest.get(
|
||||
url.format({
|
||||
pathname: `/api/apm/services/opbeans-java/metadata/details`,
|
||||
query: {
|
||||
start,
|
||||
end,
|
||||
uiFilters: '{}',
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
expect(response.status).to.be(200);
|
||||
expect(response.body).to.eql({});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when data is loaded', () => {
|
||||
before(() => esArchiver.load(archiveName));
|
||||
after(() => esArchiver.unload(archiveName));
|
||||
|
||||
it('returns java service details', async () => {
|
||||
const response = await supertest.get(
|
||||
url.format({
|
||||
pathname: `/api/apm/services/opbeans-java/metadata/details`,
|
||||
query: {
|
||||
start,
|
||||
end,
|
||||
uiFilters: '{}',
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
expect(response.status).to.be(200);
|
||||
|
||||
expectSnapshot(response.body).toMatchInline(`
|
||||
Object {
|
||||
"container": Object {
|
||||
"isContainerized": true,
|
||||
"os": "Linux",
|
||||
"totalNumberInstances": 1,
|
||||
"type": "Kubernetes",
|
||||
},
|
||||
"service": Object {
|
||||
"agent": Object {
|
||||
"ephemeral_id": "d27b2271-06b4-48c8-a02a-cfd963c0b4d0",
|
||||
"name": "java",
|
||||
"version": "1.19.1-SNAPSHOT.null",
|
||||
},
|
||||
"framework": "Servlet API",
|
||||
"runtime": Object {
|
||||
"name": "Java",
|
||||
"version": "11.0.9.1",
|
||||
},
|
||||
"versions": Array [
|
||||
"2020-12-08 03:35:36",
|
||||
],
|
||||
},
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('returns python service details', async () => {
|
||||
const response = await supertest.get(
|
||||
url.format({
|
||||
pathname: `/api/apm/services/opbeans-python/metadata/details`,
|
||||
query: {
|
||||
start,
|
||||
end,
|
||||
uiFilters: '{}',
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
expect(response.status).to.be(200);
|
||||
|
||||
expectSnapshot(response.body).toMatchInline(`
|
||||
Object {
|
||||
"cloud": Object {
|
||||
"availabilityZones": Array [
|
||||
"europe-west1-c",
|
||||
],
|
||||
"machineTypes": Array [
|
||||
"n1-standard-4",
|
||||
],
|
||||
"projectName": "elastic-observability",
|
||||
"provider": "gcp",
|
||||
},
|
||||
"container": Object {
|
||||
"isContainerized": true,
|
||||
"os": "linux",
|
||||
"totalNumberInstances": 1,
|
||||
"type": "Kubernetes",
|
||||
},
|
||||
"service": Object {
|
||||
"agent": Object {
|
||||
"name": "python",
|
||||
"version": "5.10.0",
|
||||
},
|
||||
"framework": "django",
|
||||
"runtime": Object {
|
||||
"name": "CPython",
|
||||
"version": "3.8.6",
|
||||
},
|
||||
"versions": Array [
|
||||
"2020-12-08 03:35:35",
|
||||
],
|
||||
},
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -0,0 +1,88 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import expect from '@kbn/expect';
|
||||
import url from 'url';
|
||||
import { FtrProviderContext } from '../../../../common/ftr_provider_context';
|
||||
import archives from '../../../common/archives_metadata';
|
||||
|
||||
export default function ApiTest({ getService }: FtrProviderContext) {
|
||||
const supertest = getService('supertest');
|
||||
const esArchiver = getService('esArchiver');
|
||||
|
||||
const archiveName = 'apm_8.0.0';
|
||||
const { start, end } = archives[archiveName];
|
||||
|
||||
describe('Service icons', () => {
|
||||
describe('when data is not loaded ', () => {
|
||||
it('handles the empty state', async () => {
|
||||
const response = await supertest.get(
|
||||
url.format({
|
||||
pathname: `/api/apm/services/opbeans-java/metadata/icons`,
|
||||
query: {
|
||||
start,
|
||||
end,
|
||||
uiFilters: '{}',
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
expect(response.status).to.be(200);
|
||||
expect(response.body).to.eql({});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when data is loaded', () => {
|
||||
before(() => esArchiver.load(archiveName));
|
||||
after(() => esArchiver.unload(archiveName));
|
||||
|
||||
it('returns java service icons', async () => {
|
||||
const response = await supertest.get(
|
||||
url.format({
|
||||
pathname: `/api/apm/services/opbeans-java/metadata/icons`,
|
||||
query: {
|
||||
start,
|
||||
end,
|
||||
uiFilters: '{}',
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
expect(response.status).to.be(200);
|
||||
|
||||
expectSnapshot(response.body).toMatchInline(`
|
||||
Object {
|
||||
"agentName": "java",
|
||||
"containerType": "Kubernetes",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('returns python service icons', async () => {
|
||||
const response = await supertest.get(
|
||||
url.format({
|
||||
pathname: `/api/apm/services/opbeans-python/metadata/icons`,
|
||||
query: {
|
||||
start,
|
||||
end,
|
||||
uiFilters: '{}',
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
expect(response.status).to.be(200);
|
||||
|
||||
expectSnapshot(response.body).toMatchInline(`
|
||||
Object {
|
||||
"agentName": "python",
|
||||
"cloudProvider": "gcp",
|
||||
"containerType": "Kubernetes",
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -183,6 +183,11 @@ export interface AggregationOptionsByType {
|
|||
metrics: { field: string } | MaybeReadonlyArray<{ field: string }>;
|
||||
sort: SortOptions;
|
||||
};
|
||||
avg_bucket: {
|
||||
buckets_path: string;
|
||||
gap_policy?: 'skip' | 'insert_zeros';
|
||||
format?: string;
|
||||
};
|
||||
}
|
||||
|
||||
type AggregationType = keyof AggregationOptionsByType;
|
||||
|
@ -390,6 +395,9 @@ interface AggregationResponsePart<TAggregationOptionsMap extends AggregationOpti
|
|||
>;
|
||||
}
|
||||
];
|
||||
avg_bucket: {
|
||||
value: number | null;
|
||||
};
|
||||
}
|
||||
|
||||
type TopMetricsMap<TFieldName> = TFieldName extends string
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue