mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 01:13:23 -04:00
[APM] Add infrastructure metrics tables (#130223)
* [APM] Add infrastructure metrics tables * hide container and pods tab when no data, added timepicker * add container ids and pod names to host filter * fix es types * fix i18n * wip * added empty and failure prompts * added new route to get host names * copy changes and components for empty/failure state * improve gethostnames query * delete infra host route and move request to server side * PR comments * remove prop and the export from getHostNames * update infra components, improve tables rendering UI * fix useTabs test Co-authored-by: Boris Kirov <borisasenovkirov@gmail.com>
This commit is contained in:
parent
b9c7b73644
commit
e126ccc56f
20 changed files with 733 additions and 26 deletions
|
@ -72,8 +72,7 @@ describe('Infrastracture feature flag', () => {
|
|||
|
||||
it('shows infrastructure tab in service overview page', () => {
|
||||
cy.visit(serviceOverviewPath);
|
||||
cy.contains('a[role="tab"]', 'Infrastructure').click();
|
||||
cy.contains('Infrastructure data coming soon');
|
||||
cy.contains('a[role="tab"]', 'Infrastructure');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -4,21 +4,14 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import { EuiEmptyPrompt, EuiLoadingLogo } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiPanel } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { InfraTabs } from './infra_tabs';
|
||||
|
||||
export function InfraOverview() {
|
||||
return (
|
||||
<EuiEmptyPrompt
|
||||
icon={<EuiLoadingLogo logo="logoMetrics" size="xl" />}
|
||||
title={
|
||||
<h2>
|
||||
{i18n.translate('xpack.apm.infra.announcement', {
|
||||
defaultMessage: 'Infrastructure data coming soon',
|
||||
})}
|
||||
</h2>
|
||||
}
|
||||
/>
|
||||
<EuiPanel color="subdued" borderRadius="none" hasShadow={false}>
|
||||
<InfraTabs />
|
||||
</EuiPanel>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
* 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 { EuiEmptyPrompt, EuiPageTemplate } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
|
||||
export function EmptyPrompt() {
|
||||
return (
|
||||
<EuiPageTemplate
|
||||
pageContentProps={{
|
||||
color: 'transparent',
|
||||
}}
|
||||
template="centeredBody"
|
||||
>
|
||||
<EuiEmptyPrompt
|
||||
iconType="metricsApp"
|
||||
color="subdued"
|
||||
title={
|
||||
<h2>
|
||||
{i18n.translate('xpack.apm.infraTabs.noMetricsPromptTitle', {
|
||||
defaultMessage: 'No infrastructure data found',
|
||||
})}
|
||||
</h2>
|
||||
}
|
||||
titleSize="m"
|
||||
body={
|
||||
<p>
|
||||
{i18n.translate('xpack.apm.infraTabs.noMetricsPromptDescription', {
|
||||
defaultMessage:
|
||||
'Try adjusting your time range or check if you have any metrics data set up.',
|
||||
})}
|
||||
</p>
|
||||
}
|
||||
/>
|
||||
</EuiPageTemplate>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* 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 { EuiEmptyPrompt, EuiPageTemplate } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
|
||||
export function FailurePrompt() {
|
||||
return (
|
||||
<EuiPageTemplate
|
||||
pageContentProps={{
|
||||
color: 'transparent',
|
||||
}}
|
||||
template="centeredBody"
|
||||
>
|
||||
<EuiEmptyPrompt
|
||||
color="danger"
|
||||
iconType="alert"
|
||||
layout="vertical"
|
||||
title={
|
||||
<h2>
|
||||
{i18n.translate('xpack.apm.infraTabs.failurePromptTitle', {
|
||||
defaultMessage: 'Unable to load your infrastructure data',
|
||||
})}
|
||||
</h2>
|
||||
}
|
||||
titleSize="m"
|
||||
body={
|
||||
<p>
|
||||
{i18n.translate('xpack.apm.infraTabs.failurePromptDescription', {
|
||||
defaultMessage:
|
||||
'There was a problem loading the Infrastructure tab and your data. You can contact your administrator for help.',
|
||||
})}
|
||||
</p>
|
||||
}
|
||||
/>
|
||||
</EuiPageTemplate>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,101 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EuiTabbedContent, EuiLoadingSpinner } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { useApmServiceContext } from '../../../../context/apm_service/use_apm_service_context';
|
||||
import { useApmParams } from '../../../../hooks/use_apm_params';
|
||||
import { useTimeRange } from '../../../../hooks/use_time_range';
|
||||
import { FETCH_STATUS, useFetcher } from '../../../../hooks/use_fetcher';
|
||||
import { EmptyPrompt } from './empty_prompt';
|
||||
import { FailurePrompt } from './failure_prompt';
|
||||
import { useTabs } from './use_tabs';
|
||||
|
||||
const INITIAL_STATE = {
|
||||
containerIds: [],
|
||||
hostNames: [],
|
||||
podNames: [],
|
||||
};
|
||||
|
||||
export function InfraTabs() {
|
||||
const { serviceName } = useApmServiceContext();
|
||||
const {
|
||||
query: { environment, kuery, rangeFrom, rangeTo },
|
||||
} = useApmParams('/services/{serviceName}/infrastructure');
|
||||
const { start, end } = useTimeRange({ rangeFrom, rangeTo });
|
||||
|
||||
const { data = INITIAL_STATE, status } = useFetcher(
|
||||
(callApmApi) => {
|
||||
if (start && end) {
|
||||
return callApmApi(
|
||||
'GET /internal/apm/services/{serviceName}/infrastructure_attributes',
|
||||
{
|
||||
params: {
|
||||
path: { serviceName },
|
||||
query: {
|
||||
environment,
|
||||
kuery,
|
||||
start,
|
||||
end,
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
},
|
||||
[environment, kuery, serviceName, start, end]
|
||||
);
|
||||
|
||||
const { containerIds, podNames, hostNames } = data;
|
||||
|
||||
const tabs = useTabs({
|
||||
containerIds,
|
||||
podNames,
|
||||
hostNames,
|
||||
start,
|
||||
end,
|
||||
});
|
||||
|
||||
if (status === FETCH_STATUS.LOADING) {
|
||||
return (
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<EuiLoadingSpinner size="xl" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (status === FETCH_STATUS.FAILURE) {
|
||||
return (
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<FailurePrompt />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
status === FETCH_STATUS.SUCCESS &&
|
||||
!containerIds.length &&
|
||||
!podNames.length &&
|
||||
!hostNames.length
|
||||
) {
|
||||
return (
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<EmptyPrompt />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiTabbedContent
|
||||
tabs={tabs}
|
||||
initialSelectedTab={tabs[0]}
|
||||
autoFocus="selected"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,118 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import React, { ReactNode } from 'react';
|
||||
import { renderHook } from '@testing-library/react-hooks';
|
||||
import { useTabs } from './use_tabs';
|
||||
import { createKibanaReactContext } from '@kbn/kibana-react-plugin/public';
|
||||
import { CoreStart } from '@kbn/core/public';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
const KibanaReactContext = createKibanaReactContext({
|
||||
infra: {
|
||||
HostMetricsTable: () => 'Host metrics table',
|
||||
ContainerMetricsTable: () => 'Container metrics table',
|
||||
PodMetricsTable: () => 'Pods metrics table',
|
||||
},
|
||||
} as unknown as Partial<CoreStart>);
|
||||
function wrapper({ children }: { children: ReactNode }) {
|
||||
return <KibanaReactContext.Provider>{children}</KibanaReactContext.Provider>;
|
||||
}
|
||||
|
||||
describe('useTabs', () => {
|
||||
describe('when we have container ids, pod names and host names', () => {
|
||||
it('returns all the tabs', () => {
|
||||
const params = {
|
||||
containerIds: ['apple'],
|
||||
podNames: ['orange'],
|
||||
hostNames: ['banana'],
|
||||
start: '2022-05-18T11:43:23.367Z',
|
||||
end: '2022-05-18T11:58:23.367Z',
|
||||
};
|
||||
const { result } = renderHook(() => useTabs(params), { wrapper });
|
||||
|
||||
const tabs = [
|
||||
{
|
||||
id: 'containers',
|
||||
name: 'Containers',
|
||||
content: '<EuiSpacer />Container metrics table',
|
||||
},
|
||||
{
|
||||
id: 'pods',
|
||||
name: 'Pods',
|
||||
content: '<EuiSpacer />Pods metrics table',
|
||||
},
|
||||
{
|
||||
id: 'hosts',
|
||||
name: 'Hosts',
|
||||
content: '<EuiSpacer />Host metrics table',
|
||||
},
|
||||
];
|
||||
tabs.forEach(({ id, name, content }, index) => {
|
||||
const currentResult = result.current[index];
|
||||
const component = shallow(<div>{currentResult.content}</div>);
|
||||
expect(currentResult.id).toBe(id);
|
||||
expect(currentResult.name).toBe(name);
|
||||
expect(component.text()).toBe(content);
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('when there are not container ids nor pod names', () => {
|
||||
it('returns host tab', () => {
|
||||
const params = {
|
||||
containerIds: [],
|
||||
podNames: [],
|
||||
hostNames: ['banana'],
|
||||
start: '2022-05-18T11:43:23.367Z',
|
||||
end: '2022-05-18T11:58:23.367Z',
|
||||
};
|
||||
const { result } = renderHook(() => useTabs(params), { wrapper });
|
||||
const tabs = [
|
||||
{
|
||||
id: 'hosts',
|
||||
name: 'Hosts',
|
||||
content: '<EuiSpacer />Host metrics table',
|
||||
},
|
||||
];
|
||||
|
||||
tabs.forEach(({ id, name, content }, index) => {
|
||||
const currentResult = result.current[index];
|
||||
const component = shallow(<div>{currentResult.content}</div>);
|
||||
expect(currentResult.id).toBe(id);
|
||||
expect(currentResult.name).toBe(name);
|
||||
expect(component.text()).toBe(content);
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('when there are not container ids nor pod names nor host names', () => {
|
||||
it('returns host tab', () => {
|
||||
const params = {
|
||||
containerIds: [],
|
||||
podNames: [],
|
||||
hostNames: [],
|
||||
start: '2022-05-18T11:43:23.367Z',
|
||||
end: '2022-05-18T11:58:23.367Z',
|
||||
};
|
||||
|
||||
const { result } = renderHook(() => useTabs(params), { wrapper });
|
||||
const tabs = [
|
||||
{
|
||||
id: 'hosts',
|
||||
name: 'Hosts',
|
||||
content: '<EuiSpacer />Host metrics table',
|
||||
},
|
||||
];
|
||||
|
||||
tabs.forEach(({ id, name, content }, index) => {
|
||||
const currentResult = result.current[index];
|
||||
const component = shallow(<div>{currentResult.content}</div>);
|
||||
expect(currentResult.id).toBe(id);
|
||||
expect(currentResult.name).toBe(name);
|
||||
expect(component.text()).toBe(content);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,130 @@
|
|||
/*
|
||||
* 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 { EuiTabbedContentProps } from '@elastic/eui';
|
||||
import { useMemo } from 'react';
|
||||
import { useKibana } from '@kbn/kibana-react-plugin/public';
|
||||
import type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
|
||||
import React from 'react';
|
||||
import { EuiSpacer } from '@elastic/eui';
|
||||
import { ApmPluginStartDeps } from '../../../../plugin';
|
||||
|
||||
type Tab = NonNullable<EuiTabbedContentProps['tabs']>[0] & {
|
||||
id: 'containers' | 'pods' | 'hosts';
|
||||
hidden?: boolean;
|
||||
};
|
||||
|
||||
export function useTabs({
|
||||
containerIds,
|
||||
podNames,
|
||||
hostNames,
|
||||
start,
|
||||
end,
|
||||
}: {
|
||||
containerIds: string[];
|
||||
podNames: string[];
|
||||
hostNames: string[];
|
||||
start: string;
|
||||
end: string;
|
||||
}) {
|
||||
const { services } = useKibana<ApmPluginStartDeps>();
|
||||
const { infra } = services;
|
||||
const HostMetricsTable = infra?.HostMetricsTable;
|
||||
const ContainerMetricsTable = infra?.ContainerMetricsTable;
|
||||
const PodMetricsTable = infra?.PodMetricsTable;
|
||||
|
||||
const timerange = useMemo(
|
||||
() => ({
|
||||
from: start,
|
||||
to: end,
|
||||
}),
|
||||
[start, end]
|
||||
);
|
||||
|
||||
const hostsFilter = useMemo(
|
||||
(): QueryDslQueryContainer => ({
|
||||
bool: {
|
||||
should: [
|
||||
{
|
||||
terms: {
|
||||
'host.name': hostNames,
|
||||
},
|
||||
},
|
||||
],
|
||||
minimum_should_match: 1,
|
||||
},
|
||||
}),
|
||||
[hostNames]
|
||||
);
|
||||
const podsFilter = useMemo(
|
||||
() => ({
|
||||
bool: {
|
||||
filter: [{ terms: { 'kubernetes.pod.name': podNames } }],
|
||||
},
|
||||
}),
|
||||
[podNames]
|
||||
);
|
||||
const containersFilter = useMemo(
|
||||
() => ({
|
||||
bool: {
|
||||
filter: [{ terms: { 'container.id': containerIds } }],
|
||||
},
|
||||
}),
|
||||
[containerIds]
|
||||
);
|
||||
|
||||
const containerMetricsTable = (
|
||||
<>
|
||||
<EuiSpacer />
|
||||
{ContainerMetricsTable &&
|
||||
ContainerMetricsTable({ timerange, filterClauseDsl: containersFilter })}
|
||||
</>
|
||||
);
|
||||
|
||||
const podMetricsTable = (
|
||||
<>
|
||||
<EuiSpacer />
|
||||
{PodMetricsTable &&
|
||||
PodMetricsTable({ timerange, filterClauseDsl: podsFilter })}
|
||||
</>
|
||||
);
|
||||
|
||||
const hostMetricsTable = (
|
||||
<>
|
||||
<EuiSpacer />
|
||||
{HostMetricsTable &&
|
||||
HostMetricsTable({ timerange, filterClauseDsl: hostsFilter })}
|
||||
</>
|
||||
);
|
||||
|
||||
const tabs: Tab[] = [
|
||||
{
|
||||
id: 'containers',
|
||||
name: 'Containers',
|
||||
content: containerMetricsTable,
|
||||
hidden: containerIds && containerIds.length <= 0,
|
||||
},
|
||||
{
|
||||
id: 'pods',
|
||||
name: 'Pods',
|
||||
content: podMetricsTable,
|
||||
hidden: podNames && podNames.length <= 0,
|
||||
},
|
||||
{
|
||||
id: 'hosts',
|
||||
name: 'Hosts',
|
||||
content: hostMetricsTable,
|
||||
},
|
||||
];
|
||||
|
||||
return tabs
|
||||
.filter((t) => !t.hidden)
|
||||
.map(({ id, name, content }) => ({
|
||||
id,
|
||||
name,
|
||||
content,
|
||||
}));
|
||||
}
|
|
@ -36,7 +36,7 @@ export function ServiceLogs() {
|
|||
(callApmApi) => {
|
||||
if (start && end) {
|
||||
return callApmApi(
|
||||
'GET /internal/apm/services/{serviceName}/infrastructure',
|
||||
'GET /internal/apm/services/{serviceName}/infrastructure_attributes_for_logs',
|
||||
{
|
||||
params: {
|
||||
path: { serviceName },
|
||||
|
@ -96,7 +96,7 @@ export function ServiceLogs() {
|
|||
|
||||
export const getInfrastructureKQLFilter = (
|
||||
data:
|
||||
| APIReturnType<'GET /internal/apm/services/{serviceName}/infrastructure'>
|
||||
| APIReturnType<'GET /internal/apm/services/{serviceName}/infrastructure_attributes_for_logs'>
|
||||
| undefined,
|
||||
serviceName: string
|
||||
) => {
|
||||
|
|
|
@ -269,14 +269,16 @@ export const serviceDetail = {
|
|||
}),
|
||||
element: <ServiceProfiling />,
|
||||
}),
|
||||
'/services/{serviceName}/infra': page({
|
||||
tab: 'infra',
|
||||
'/services/{serviceName}/infrastructure': page({
|
||||
tab: 'infrastructure',
|
||||
title: i18n.translate('xpack.apm.views.infra.title', {
|
||||
defaultMessage: 'Infrastructure',
|
||||
}),
|
||||
element: <InfraOverview />,
|
||||
searchBarOptions: {
|
||||
hidden: true,
|
||||
showKueryBar: false,
|
||||
showTimeComparison: false,
|
||||
showTransactionTypeSelector: false,
|
||||
},
|
||||
}),
|
||||
'/services/{serviceName}/': {
|
||||
|
|
|
@ -44,7 +44,7 @@ type Tab = NonNullable<EuiPageHeaderProps['tabs']>[0] & {
|
|||
| 'errors'
|
||||
| 'metrics'
|
||||
| 'nodes'
|
||||
| 'infra'
|
||||
| 'infrastructure'
|
||||
| 'service-map'
|
||||
| 'logs'
|
||||
| 'profiling';
|
||||
|
@ -249,8 +249,8 @@ function useTabs({ selectedTab }: { selectedTab: Tab['key'] }) {
|
|||
hidden: isJVMsTabHidden({ agentName, runtimeName }),
|
||||
},
|
||||
{
|
||||
key: 'infra',
|
||||
href: router.link('/services/{serviceName}/infra', {
|
||||
key: 'infrastructure',
|
||||
href: router.link('/services/{serviceName}/infrastructure', {
|
||||
path: { serviceName },
|
||||
query,
|
||||
}),
|
||||
|
|
|
@ -47,6 +47,7 @@ import type {
|
|||
import type { SecurityPluginStart } from '@kbn/security-plugin/public';
|
||||
import { SpacesPluginStart } from '@kbn/spaces-plugin/public';
|
||||
import { enableServiceGroups } from '@kbn/observability-plugin/public';
|
||||
import { InfraClientStartExports } from '@kbn/infra-plugin/public';
|
||||
import { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public';
|
||||
import { registerApmAlerts } from './components/alerting/register_apm_alerts';
|
||||
import {
|
||||
|
@ -89,6 +90,7 @@ export interface ApmPluginStartDeps {
|
|||
fleet?: FleetStart;
|
||||
security?: SecurityPluginStart;
|
||||
spaces?: SpacesPluginStart;
|
||||
infra?: InfraClientStartExports;
|
||||
dataViews: DataViewsPublicPluginStart;
|
||||
unifiedSearch: UnifiedSearchPublicPluginStart;
|
||||
}
|
||||
|
|
|
@ -15,6 +15,7 @@ import { alertsChartPreviewRouteRepository } from '../alerts/route';
|
|||
import { backendsRouteRepository } from '../backends/route';
|
||||
import { environmentsRouteRepository } from '../environments/route';
|
||||
import { errorsRouteRepository } from '../errors/route';
|
||||
import { infrastructureRouteRepository } from '../infrastructure/route';
|
||||
import { apmFleetRouteRepository } from '../fleet/route';
|
||||
import { dataViewRouteRepository } from '../data_view/route';
|
||||
import { latencyDistributionRouteRepository } from '../latency_distribution/route';
|
||||
|
@ -69,6 +70,7 @@ function getTypedGlobalApmServerRouteRepository() {
|
|||
...eventMetadataRouteRepository,
|
||||
...agentKeysRouteRepository,
|
||||
...spanLinksRouteRepository,
|
||||
...infrastructureRouteRepository,
|
||||
...debugTelemetryRoute,
|
||||
};
|
||||
|
||||
|
|
|
@ -0,0 +1,110 @@
|
|||
/*
|
||||
* 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 { InfraPluginStart, InfraPluginSetup } from '@kbn/infra-plugin/server';
|
||||
import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
||||
import {
|
||||
CONTAINER_ID,
|
||||
HOST_NAME,
|
||||
} from '../../../common/elasticsearch_fieldnames';
|
||||
import { ApmPluginRequestHandlerContext } from '../typings';
|
||||
import { getMetricIndices } from '../../lib/helpers/get_metric_indices';
|
||||
|
||||
interface Aggs extends estypes.AggregationsMultiBucketAggregateBase {
|
||||
buckets: Array<{
|
||||
key: string;
|
||||
key_as_string?: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
interface InfraPlugin {
|
||||
setup: InfraPluginSetup;
|
||||
start: () => Promise<InfraPluginStart>;
|
||||
}
|
||||
|
||||
const getHostNames = async ({
|
||||
esClient,
|
||||
containerIds,
|
||||
index,
|
||||
start,
|
||||
end,
|
||||
}: {
|
||||
esClient: ElasticsearchClient;
|
||||
containerIds: string[];
|
||||
index: string;
|
||||
start: number;
|
||||
end: number;
|
||||
}) => {
|
||||
const response = await esClient.search<unknown, { hostNames: Aggs }>({
|
||||
index: [index],
|
||||
body: {
|
||||
size: 0,
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
{
|
||||
terms: {
|
||||
[CONTAINER_ID]: containerIds,
|
||||
},
|
||||
},
|
||||
...rangeQuery(start, end),
|
||||
],
|
||||
},
|
||||
},
|
||||
aggs: {
|
||||
hostNames: {
|
||||
terms: {
|
||||
field: HOST_NAME,
|
||||
size: 500,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
hostNames:
|
||||
response.aggregations?.hostNames?.buckets.map(
|
||||
(bucket) => bucket.key as string
|
||||
) ?? [],
|
||||
};
|
||||
};
|
||||
|
||||
export const getContainerHostNames = async ({
|
||||
containerIds,
|
||||
context,
|
||||
infra,
|
||||
start,
|
||||
end,
|
||||
}: {
|
||||
containerIds: string[];
|
||||
context: ApmPluginRequestHandlerContext;
|
||||
infra: InfraPlugin;
|
||||
start: number;
|
||||
end: number;
|
||||
}): Promise<string[]> => {
|
||||
if (containerIds.length) {
|
||||
const esClient = (await context.core).elasticsearch.client.asCurrentUser;
|
||||
const savedObjectsClient = (await context.core).savedObjects.client;
|
||||
const metricIndices = await getMetricIndices({
|
||||
infraPlugin: infra,
|
||||
savedObjectsClient,
|
||||
});
|
||||
|
||||
const containerHostNames = await getHostNames({
|
||||
esClient,
|
||||
containerIds,
|
||||
index: metricIndices,
|
||||
start,
|
||||
end,
|
||||
});
|
||||
return containerHostNames.hostNames;
|
||||
}
|
||||
return [];
|
||||
};
|
|
@ -0,0 +1,89 @@
|
|||
/*
|
||||
* 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 { rangeQuery, kqlQuery } from '@kbn/observability-plugin/server';
|
||||
import { Setup } from '../../lib/helpers/setup_request';
|
||||
import { environmentQuery } from '../../../common/utils/environment_query';
|
||||
import { ProcessorEvent } from '../../../common/processor_event';
|
||||
import {
|
||||
SERVICE_NAME,
|
||||
CONTAINER_ID,
|
||||
HOST_HOSTNAME,
|
||||
POD_NAME,
|
||||
} from '../../../common/elasticsearch_fieldnames';
|
||||
|
||||
export const getInfrastructureData = async ({
|
||||
kuery,
|
||||
serviceName,
|
||||
environment,
|
||||
setup,
|
||||
start,
|
||||
end,
|
||||
}: {
|
||||
kuery: string;
|
||||
serviceName: string;
|
||||
environment: string;
|
||||
setup: Setup;
|
||||
start: number;
|
||||
end: number;
|
||||
}) => {
|
||||
const { apmEventClient } = setup;
|
||||
|
||||
const response = await apmEventClient.search('get_service_infrastructure', {
|
||||
apm: {
|
||||
events: [ProcessorEvent.metric],
|
||||
},
|
||||
body: {
|
||||
size: 0,
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
{ term: { [SERVICE_NAME]: serviceName } },
|
||||
...rangeQuery(start, end),
|
||||
...environmentQuery(environment),
|
||||
...kqlQuery(kuery),
|
||||
],
|
||||
},
|
||||
},
|
||||
aggs: {
|
||||
containerIds: {
|
||||
terms: {
|
||||
field: CONTAINER_ID,
|
||||
size: 500,
|
||||
},
|
||||
},
|
||||
hostNames: {
|
||||
terms: {
|
||||
field: HOST_HOSTNAME,
|
||||
size: 500,
|
||||
},
|
||||
},
|
||||
podNames: {
|
||||
terms: {
|
||||
field: POD_NAME,
|
||||
size: 500,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
containerIds:
|
||||
response.aggregations?.containerIds?.buckets.map(
|
||||
(bucket) => bucket.key as string
|
||||
) ?? [],
|
||||
hostNames:
|
||||
response.aggregations?.hostNames?.buckets.map(
|
||||
(bucket) => bucket.key as string
|
||||
) ?? [],
|
||||
podNames:
|
||||
response.aggregations?.podNames?.buckets.map(
|
||||
(bucket) => bucket.key as string
|
||||
) ?? [],
|
||||
};
|
||||
};
|
76
x-pack/plugins/apm/server/routes/infrastructure/route.ts
Normal file
76
x-pack/plugins/apm/server/routes/infrastructure/route.ts
Normal file
|
@ -0,0 +1,76 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import * as t from 'io-ts';
|
||||
import { createApmServerRoute } from '../apm_routes/create_apm_server_route';
|
||||
import { setupRequest } from '../../lib/helpers/setup_request';
|
||||
import { environmentRt, kueryRt, rangeRt } from '../default_api_types';
|
||||
import { getInfrastructureData } from './get_infrastructure_data';
|
||||
import { getContainerHostNames } from './get_host_names';
|
||||
|
||||
const infrastructureRoute = createApmServerRoute({
|
||||
endpoint:
|
||||
'GET /internal/apm/services/{serviceName}/infrastructure_attributes',
|
||||
params: t.type({
|
||||
path: t.type({
|
||||
serviceName: t.string,
|
||||
}),
|
||||
query: t.intersection([kueryRt, rangeRt, environmentRt]),
|
||||
}),
|
||||
options: { tags: ['access:apm'] },
|
||||
handler: async (
|
||||
resources
|
||||
): Promise<{
|
||||
containerIds: string[];
|
||||
hostNames: string[];
|
||||
podNames: string[];
|
||||
}> => {
|
||||
const setup = await setupRequest(resources);
|
||||
|
||||
const {
|
||||
context,
|
||||
params,
|
||||
plugins: { infra },
|
||||
} = resources;
|
||||
|
||||
const {
|
||||
path: { serviceName },
|
||||
query: { environment, kuery, start, end },
|
||||
} = params;
|
||||
|
||||
const infrastructureData = await getInfrastructureData({
|
||||
setup,
|
||||
serviceName,
|
||||
environment,
|
||||
kuery,
|
||||
start,
|
||||
end,
|
||||
});
|
||||
|
||||
const containerIds = infrastructureData.containerIds;
|
||||
// due some limitations on the data we get from apm-metrics indices, if we have a service running in a container we want to query, to get the host.name, filtering by container.id
|
||||
const containerHostNames = await getContainerHostNames({
|
||||
containerIds,
|
||||
context,
|
||||
infra,
|
||||
start,
|
||||
end,
|
||||
});
|
||||
|
||||
return {
|
||||
containerIds,
|
||||
hostNames:
|
||||
containerIds.length > 0 // if we have container ids we rely on the hosts fetched filtering by container.id
|
||||
? containerHostNames
|
||||
: infrastructureData.hostNames,
|
||||
podNames: infrastructureData.podNames,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export const infrastructureRouteRepository = {
|
||||
...infrastructureRoute,
|
||||
};
|
|
@ -1118,8 +1118,10 @@ const serviceProfilingStatisticsRoute = createApmServerRoute({
|
|||
},
|
||||
});
|
||||
|
||||
// TODO: remove this endpoint in favour of
|
||||
const serviceInfrastructureRoute = createApmServerRoute({
|
||||
endpoint: 'GET /internal/apm/services/{serviceName}/infrastructure',
|
||||
endpoint:
|
||||
'GET /internal/apm/services/{serviceName}/infrastructure_attributes_for_logs',
|
||||
params: t.type({
|
||||
path: t.type({
|
||||
serviceName: t.string,
|
||||
|
|
|
@ -31,3 +31,4 @@ export type InfraAppId = 'logs' | 'metrics';
|
|||
// Shared components
|
||||
export { LazyLogStreamWrapper as LogStream } from './components/log_stream/lazy_log_stream_wrapper';
|
||||
export type { LogStreamProps } from './components/log_stream';
|
||||
export type { InfraClientStartExports } from './types';
|
||||
|
|
|
@ -7478,7 +7478,6 @@
|
|||
"xpack.apm.home.infraTabLabel": "Infrastructure",
|
||||
"xpack.apm.home.serviceLogsTabLabel": "Logs",
|
||||
"xpack.apm.home.serviceMapTabLabel": "Carte des services",
|
||||
"xpack.apm.infra.announcement": "Données sur les infrastructures bientôt disponibles",
|
||||
"xpack.apm.inspectButtonText": "Inspecter",
|
||||
"xpack.apm.instancesLatencyDistributionChartLegend": "Instances",
|
||||
"xpack.apm.instancesLatencyDistributionChartLegend.previousPeriod": "Période précédente",
|
||||
|
|
|
@ -7574,7 +7574,6 @@
|
|||
"xpack.apm.home.infraTabLabel": "インフラストラクチャー",
|
||||
"xpack.apm.home.serviceLogsTabLabel": "ログ",
|
||||
"xpack.apm.home.serviceMapTabLabel": "サービスマップ",
|
||||
"xpack.apm.infra.announcement": "インフラストラクチャデータは近日公開予定です",
|
||||
"xpack.apm.inspectButtonText": "検査",
|
||||
"xpack.apm.instancesLatencyDistributionChartLegend": "インスタンス",
|
||||
"xpack.apm.instancesLatencyDistributionChartLegend.previousPeriod": "前の期間",
|
||||
|
|
|
@ -7591,7 +7591,6 @@
|
|||
"xpack.apm.home.infraTabLabel": "基础设施",
|
||||
"xpack.apm.home.serviceLogsTabLabel": "日志",
|
||||
"xpack.apm.home.serviceMapTabLabel": "服务地图",
|
||||
"xpack.apm.infra.announcement": "即将提供基础架构数据",
|
||||
"xpack.apm.inspectButtonText": "检查",
|
||||
"xpack.apm.instancesLatencyDistributionChartLegend": "实例",
|
||||
"xpack.apm.instancesLatencyDistributionChartLegend.previousPeriod": "上一时段",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue