[ObsUX][Infra] Enhance Metadata tab and metadata in containers overview (#185786)

Closes https://github.com/elastic/kibana/issues/183355

### Summary
This PR adds the metadata fields to the container views overview and
metadata fields in synthtrace for testing
<img width="1788" alt="Screenshot 2024-06-11 at 15 27 37"
src="a6f1e09f-9f34-4895-94a9-3197f8562e86">
<img width="1788" alt="Screenshot 2024-06-11 at 15 28 03"
src="14311b25-8a39-435f-894f-dd04e0707d5f">
This commit is contained in:
Miriam 2024-06-12 18:45:24 +01:00 committed by GitHub
parent 04add9a1f1
commit 09e27bed08
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 233 additions and 29 deletions

View file

@ -13,14 +13,22 @@ import { Serializable } from '../serializable';
interface DockerContainerDocument extends Fields {
'container.id': string;
'metricset.name'?: string;
'container.name'?: string;
'container.image.name'?: string;
'container.runtime'?: string;
'host.name'?: string;
'cloud.provider'?: string;
'cloud.instance.id'?: string;
'cloud.image.id'?: string;
'event.dataset'?: string;
}
export class DockerContainer extends Entity<DockerContainerDocument> {
metrics() {
return new DockerContainerMetrics({
...this.fields,
'docker.cpu.total.pct': 25,
'docker.memory.usage.pct': 20,
'docker.cpu.total.pct': 0.25,
'docker.memory.usage.pct': 0.2,
'docker.network.inbound.bytes': 100,
'docker.network.outbound.bytes': 200,
'docker.diskio.read.ops': 10,

View file

@ -15,6 +15,14 @@ interface K8sContainerDocument extends Fields {
'kubernetes.pod.uid': string;
'kubernetes.node.name': string;
'metricset.name'?: string;
'container.name'?: string;
'container.image.name'?: string;
'container.runtime'?: string;
'host.name'?: string;
'cloud.provider'?: string;
'cloud.instance.id'?: string;
'cloud.image.id'?: string;
'event.dataset'?: string;
}
export class K8sContainer extends Entity<K8sContainerDocument> {

View file

@ -6,7 +6,7 @@
* Side Public License, v 1.
*/
import { InfraDocument, infra } from '@kbn/apm-synthtrace-client';
import { InfraDocument, infra, generateShortId } from '@kbn/apm-synthtrace-client';
import { Scenario } from '../cli/scenario';
import { withClient } from '../lib/utils/with_client';
@ -19,7 +19,20 @@ const scenario: Scenario<InfraDocument> = async (runOptions) => {
const CONTAINERS = Array(numContainers)
.fill(0)
.map((_, idx) => infra.dockerContainer(`container-${idx}`));
.map((_, idx) => {
const id = generateShortId();
return infra.dockerContainer(id).defaults({
'container.name': `container-${idx}`,
'container.id': id,
'container.runtime': 'docker',
'container.image.name': 'image-1',
'host.name': 'host-1',
'cloud.instance.id': 'instance-1',
'cloud.image.id': 'image-1',
'cloud.provider': 'aws',
'event.dataset': 'docker.container',
});
});
const containers = range
.interval('30s')

View file

@ -6,7 +6,7 @@
* Side Public License, v 1.
*/
import { InfraDocument, infra } from '@kbn/apm-synthtrace-client';
import { InfraDocument, infra, generateShortId } from '@kbn/apm-synthtrace-client';
import { Scenario } from '../cli/scenario';
import { withClient } from '../lib/utils/with_client';
@ -19,7 +19,22 @@ const scenario: Scenario<InfraDocument> = async (runOptions) => {
const CONTAINERS = Array(numContainers)
.fill(0)
.map((_, idx) => infra.k8sContainer(`container-${idx}`, `pod-${idx}`, `node-${idx}`));
.map((_, idx) => {
const id = generateShortId();
return infra.k8sContainer(id, `pod-${idx}`, `node-${idx}`).defaults({
'container.id': id,
'kubernetes.pod.uid': `pod-${idx}`,
'kubernetes.node.name': `node-${idx}`,
'container.name': `container-${idx}`,
'container.runtime': 'docker',
'container.image.name': 'image-1',
'host.name': 'host-1',
'cloud.instance.id': 'instance-1',
'cloud.image.id': 'image-1',
'cloud.provider': 'aws',
'event.dataset': 'kubernetes.container',
});
});
const containers = range
.interval('30s')

View file

@ -44,6 +44,13 @@ export const InfraMetadataHostRT = rt.partial({
containerized: rt.boolean,
});
export const InfraMetadataContainerRT = rt.partial({
name: rt.string,
id: rt.string,
runtime: rt.string,
imageName: rt.string,
});
export const InfraMetadataInstanceRT = rt.partial({
id: rt.string,
name: rt.string,
@ -71,6 +78,7 @@ export const InfraMetadataCloudRT = rt.partial({
project: InfraMetadataProjectRT,
machine: InfraMetadataMachineRT,
region: rt.string,
imageId: rt.string,
});
export const InfraMetadataAgentRT = rt.partial({
@ -82,6 +90,7 @@ export const InfraMetadataAgentRT = rt.partial({
export const InfraMetadataInfoRT = rt.partial({
cloud: InfraMetadataCloudRT,
host: InfraMetadataHostRT,
container: InfraMetadataContainerRT,
agent: InfraMetadataAgentRT,
'@timestamp': rt.string,
});
@ -89,6 +98,7 @@ export const InfraMetadataInfoRT = rt.partial({
export const InfraMetadataInfoResponseRT = rt.partial({
cloud: InfraMetadataCloudRT,
host: InfraMetadataHostRT,
container: InfraMetadataContainerRT,
agent: InfraMetadataAgentRT,
timestamp: rt.string,
});
@ -123,4 +133,6 @@ export type InfraMetadataMachine = rt.TypeOf<typeof InfraMetadataMachineRT>;
export type InfraMetadataHost = rt.TypeOf<typeof InfraMetadataHostRT>;
export type InfraMetadataContainer = rt.TypeOf<typeof InfraMetadataContainerRT>;
export type InfraMetadataOS = rt.TypeOf<typeof InfraMetadataOSRT>;

View file

@ -18,7 +18,10 @@ interface FieldsByCategory {
export const getAllFields = (metadata: InfraMetadata | null) => {
if (!metadata?.info) return [];
const mapNestedProperties = (category: 'cloud' | 'host' | 'agent', property: string) => {
const mapNestedProperties = (
category: 'cloud' | 'host' | 'agent' | 'container',
property: string
) => {
const fieldsByCategory: FieldsByCategory = metadata?.info?.[`${category}`] ?? {};
if (fieldsByCategory.hasOwnProperty(property)) {
const value = fieldsByCategory[property];
@ -54,8 +57,11 @@ export const getAllFields = (metadata: InfraMetadata | null) => {
const host = Object.keys(metadata?.info?.host ?? {}).flatMap((prop) =>
mapNestedProperties('host', prop)
);
const container = Object.keys(metadata?.info?.container ?? {}).flatMap((prop) =>
mapNestedProperties('container', prop)
);
return prune([...host, ...agent, ...cloud]);
return prune([...host, ...container, ...agent, ...cloud]);
};
const prune = (fields: Field[]) => fields.filter((f) => !!f?.value);

View file

@ -21,15 +21,39 @@ const columnTitles = {
hostOsVersion: i18n.translate('xpack.infra.assetDetails.overview.metadataHostOsVersionHeading', {
defaultMessage: 'Host OS version',
}),
hostName: i18n.translate('xpack.infra.assetDetails.overview.metadataHostNameHeading', {
defaultMessage: 'Host name',
}),
cloudProvider: i18n.translate('xpack.infra.assetDetails.overview.metadataCloudProviderHeading', {
defaultMessage: 'Cloud provider',
}),
cloudInstanceId: i18n.translate(
'xpack.infra.assetDetails.overview.metadataCloudInstanceIdHeading',
{
defaultMessage: 'Cloud instance ID',
}
),
cloudImageId: i18n.translate('xpack.infra.assetDetails.overview.metadataCloudImageIdHeading', {
defaultMessage: 'Cloud image ID',
}),
operatingSystem: i18n.translate(
'xpack.infra.assetDetails.overview.metadataOperatingSystemHeading',
{
defaultMessage: 'Operating system',
}
),
containerId: i18n.translate('xpack.infra.assetDetails.overview.metadataContainerIdHeading', {
defaultMessage: 'Container ID',
}),
containerImageName: i18n.translate(
'xpack.infra.assetDetails.overview.metadataContainerImageNameHeading',
{
defaultMessage: 'Container image name',
}
),
runtime: i18n.translate('xpack.infra.assetDetails.overview.metadataRuntimeHeading', {
defaultMessage: 'Runtime',
}),
};
type MetadataFields = 'hostIp' | 'hostOsVersion';

View file

@ -29,6 +29,7 @@ import { Section } from '../../../components/section';
interface MetadataSummaryProps {
metadata: InfraMetadata | null;
loading: boolean;
assetType: string;
}
interface MetadataSummaryWrapperProps {
visibleMetadata: MetadataData[];
@ -42,7 +43,7 @@ export interface MetadataData {
tooltipLink?: string;
}
const extendedMetadata = (metadataInfo: InfraMetadata['info']): MetadataData[] => [
const hostExtendedMetadata = (metadataInfo: InfraMetadata['info']): MetadataData[] => [
{
field: 'cloudProvider',
value: metadataInfo?.cloud?.provider,
@ -56,7 +57,7 @@ const extendedMetadata = (metadataInfo: InfraMetadata['info']): MetadataData[] =
},
];
const metadataData = (metadataInfo: InfraMetadata['info']): MetadataData[] => [
const hostMetadataData = (metadataInfo: InfraMetadata['info']): MetadataData[] => [
{
field: 'hostIp',
value: metadataInfo?.host?.ip,
@ -70,6 +71,47 @@ const metadataData = (metadataInfo: InfraMetadata['info']): MetadataData[] => [
},
];
const containerExtendedMetadata = (metadataInfo: InfraMetadata['info']): MetadataData[] => [
{
field: 'runtime',
value: metadataInfo?.container?.runtime,
tooltipFieldLabel: 'container.runtime',
},
{
field: 'cloudInstanceId',
value: metadataInfo?.cloud?.instance?.id,
tooltipFieldLabel: 'cloud.instance.id',
},
{
field: 'cloudImageId',
value: metadataInfo?.cloud?.imageId,
tooltipFieldLabel: 'cloud.image.id',
},
{
field: 'cloudProvider',
value: metadataInfo?.cloud?.provider,
tooltipFieldLabel: 'cloud.provider',
},
];
const containerMetadataData = (metadataInfo: InfraMetadata['info']): MetadataData[] => [
{
field: 'containerId',
value: metadataInfo?.container?.id,
tooltipFieldLabel: 'container.id',
},
{
field: 'containerImageName',
value: metadataInfo?.container?.imageName,
tooltipFieldLabel: 'container.image.name',
},
{
field: 'hostName',
value: metadataInfo?.host?.name,
tooltipFieldLabel: 'host.name',
},
];
const MetadataSummaryListWrapper = ({
loading: metadataLoading,
visibleMetadata,
@ -138,13 +180,54 @@ const MetadataSummaryListWrapper = ({
</Section>
);
};
export const MetadataSummaryList = ({ metadata, loading }: MetadataSummaryProps) => (
<MetadataSummaryListWrapper
visibleMetadata={[...metadataData(metadata?.info), ...extendedMetadata(metadata?.info)]}
loading={loading}
/>
);
export const MetadataSummaryList = ({ metadata, loading, assetType }: MetadataSummaryProps) => {
switch (assetType) {
case 'host':
return (
<MetadataSummaryListWrapper
visibleMetadata={[
...hostMetadataData(metadata?.info),
...hostExtendedMetadata(metadata?.info),
]}
loading={loading}
/>
);
case 'container':
return (
<MetadataSummaryListWrapper
visibleMetadata={[
...containerMetadataData(metadata?.info),
...containerExtendedMetadata(metadata?.info),
]}
loading={loading}
/>
);
default:
return <MetadataSummaryListWrapper visibleMetadata={[]} loading={loading} />;
}
};
export const MetadataSummaryListCompact = ({ metadata, loading }: MetadataSummaryProps) => (
<MetadataSummaryListWrapper visibleMetadata={metadataData(metadata?.info)} loading={loading} />
);
export const MetadataSummaryListCompact = ({
metadata,
loading,
assetType,
}: MetadataSummaryProps) => {
switch (assetType) {
case 'host':
return (
<MetadataSummaryListWrapper
visibleMetadata={hostMetadataData(metadata?.info)}
loading={loading}
/>
);
case 'container':
return (
<MetadataSummaryListWrapper
visibleMetadata={containerMetadataData(metadata?.info)}
loading={loading}
/>
);
default:
return <MetadataSummaryListWrapper visibleMetadata={[]} loading={loading} />;
}
};

View file

@ -40,9 +40,13 @@ export const Overview = () => {
const state = useIntersectingState(ref, { dateRange });
const metadataSummarySection = isFullPageView ? (
<MetadataSummaryList metadata={metadata} loading={metadataLoading} />
<MetadataSummaryList metadata={metadata} loading={metadataLoading} assetType={asset.type} />
) : (
<MetadataSummaryListCompact metadata={metadata} loading={metadataLoading} />
<MetadataSummaryListCompact
metadata={metadata}
loading={metadataLoading}
assetType={asset.type}
/>
);
return (

View file

@ -59,7 +59,7 @@ export const getNodeInfo = async (
index: sourceConfiguration.metricAlias,
body: {
size: 1,
_source: ['host.*', 'cloud.*', 'agent.*', TIMESTAMP_FIELD],
_source: ['host.*', 'cloud.*', 'agent.*', 'container.*', TIMESTAMP_FIELD],
sort: [{ [TIMESTAMP_FIELD]: 'desc' }],
query: {
bool: {

View file

@ -60,7 +60,19 @@ export function generateDockerContainersData({
const containers = Array(count)
.fill(0)
.map((_, idx) => infra.dockerContainer(`container-id-${idx}`));
.map((_, idx) =>
infra.dockerContainer(`container-id-${idx}`).defaults({
'container.name': `container-id-${idx}`,
'container.id': `container-id-${idx}`,
'container.runtime': 'docker',
'container.image.name': 'image-1',
'host.name': 'host-1',
'cloud.instance.id': 'instance-1',
'cloud.image.id': 'image-1',
'cloud.provider': 'aws',
'event.dataset': 'docker.container',
})
);
return range
.interval('30s')

View file

@ -292,10 +292,10 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
});
[
{ metric: 'cpuUsage', value: '2,500.0%' },
{ metric: 'memoryUsage', value: '2,000.0%' },
{ metric: 'cpuUsage', value: '25.0%' },
{ metric: 'memoryUsage', value: '20.0%' },
].forEach(({ metric, value }) => {
it.skip(`${metric} tile should show ${value}`, async () => {
it(`${metric} tile should show ${value}`, async () => {
await retry.tryForTime(3 * 1000, async () => {
const tileValue = await pageObjects.assetDetails.getAssetDetailsKPITileValue(
metric

View file

@ -38,6 +38,8 @@ const START_HOST_KUBERNETES_SECTION_DATE = moment.utc(
const END_HOST_KUBERNETES_SECTION_DATE = moment.utc(
DATES.metricsAndLogs.hosts.kubernetesSectionEndDate
);
const START_CONTAINER_DATE = moment.utc(DATE_WITH_DOCKER_DATA_FROM);
const END_CONTAINER_DATE = moment.utc(DATE_WITH_DOCKER_DATA_TO);
export default ({ getPageObjects, getService }: FtrProviderContext) => {
const observability = getService('observability');
@ -651,25 +653,42 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
});
describe('when container asset view is enabled', () => {
it('should show asset container details page', async () => {
before(async () => {
await setInfrastructureContainerAssetViewUiSetting(true);
await navigateToNodeDetails('container-id-0', 'container-id-0', 'container');
await pageObjects.header.waitUntilLoadingHasFinished();
await pageObjects.timePicker.setAbsoluteRange(
START_CONTAINER_DATE.format(DATE_PICKER_FORMAT),
END_CONTAINER_DATE.format(DATE_PICKER_FORMAT)
);
});
it('should show asset container details page', async () => {
await pageObjects.assetDetails.getOverviewTab();
});
[
{ metric: 'cpu', chartsCount: 1 },
{ metric: 'memory', chartsCount: 1 },
{ metric: 'disk', chartsCount: 1 },
{ metric: 'network', chartsCount: 1 },
].forEach(({ metric, chartsCount }) => {
it.skip(`should render ${chartsCount} ${metric} chart(s) in the Metrics section`, async () => {
it(`should render ${chartsCount} ${metric} chart(s) in the Metrics section`, async () => {
const charts = await pageObjects.assetDetails.getOverviewTabDockerMetricCharts(
metric
);
expect(charts.length).to.equal(chartsCount);
});
});
describe('Metadata Tab', () => {
before(async () => {
await pageObjects.assetDetails.clickMetadataTab();
});
it('should show metadata table', async () => {
await pageObjects.assetDetails.metadataTableExists();
});
});
describe('Logs Tab', () => {
before(async () => {
await pageObjects.assetDetails.clickLogsTab();