mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
[Infrastructure UI]: Implement Telemetry events for Hosts View (#149497)
## 📓 Summary Re-opened to fix errors and conflicts on #149329 Closes #148739 Implement the following Telemetry custom events: - **Host Entry Clicked** - **Hosts View Query Submitted** When triggered, these events will track to FullStory some details relevant to the performed query or the selected host from the table. These changes include a necessary refactor of the hosts' table hook in order to trigger an event on each entry click without performance issues. ## 🧪 Testing As long as we don't have access to the staging environment of FullStory, is difficult to test E2E if an event is tracked or not. What we can firstly test is that our events are correctly collected by the `core.analytics` API and shipped correctly to the FullStory integration. To verify this, please follow these steps: 1. Add the following configuration to your `kibana.dev.yml` file: ```yml # Enable Fullstory Telemetry xpack.cloud.id: "elastic_kibana_dev" xpack.cloud.full_story.enabled: true # Staging FullStory OrgID xpack.cloud.full_story.org_id: "1397FY" ``` 2. Add debugging breakpoint into the [`sendToShipper()` function](https://github.com/elastic/kibana/blob/main/packages/analytics/client/src/analytics_client/analytics_client.ts#L275) in the analytics package. 3. Navigate to Hosts View and perform search queries or click on table entries to check whether the events are correctly composed. --------- Co-authored-by: Marco Antonio Ghiani <marcoantonio.ghiani@elastic.co>
This commit is contained in:
parent
6dc77ce630
commit
e1afb6584d
14 changed files with 339 additions and 275 deletions
|
@ -18,7 +18,11 @@ const configSchema = schema.object({
|
|||
schema.maybe(schema.string())
|
||||
),
|
||||
eventTypesAllowlist: schema.arrayOf(schema.string(), {
|
||||
defaultValue: ['Loaded Kibana'],
|
||||
defaultValue: [
|
||||
'Loaded Kibana', // Sent once per page refresh (potentially, once per session)
|
||||
'Hosts View Query Submitted', // Worst-case scenario 1 every 2 seconds
|
||||
'Host Entry Clicked', // Worst-case scenario once per second - AT RISK,
|
||||
],
|
||||
}),
|
||||
});
|
||||
|
||||
|
|
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
const ONE_MINUTE = 60000;
|
||||
const FIVE_MINUTES = ONE_MINUTE * 5;
|
||||
const TEN_MINUTES = ONE_MINUTE * 10;
|
||||
const THIRTY_MINUTES = ONE_MINUTE * 30;
|
||||
const ONE_HOUR = ONE_MINUTE * 60;
|
||||
const TWO_HOURS = ONE_HOUR * 2;
|
||||
const EIGHT_HOURS = ONE_HOUR * 8;
|
||||
const TWELVE_HOURS = ONE_HOUR * 12;
|
||||
const ONE_DAY = ONE_HOUR * 24;
|
||||
const TWO_DAYS = ONE_DAY * 2;
|
||||
const SEVEN_DAYS = ONE_DAY * 7;
|
||||
const FOURTEEN_DAYS = ONE_DAY * 14;
|
||||
const THIRTY_DAYS = ONE_DAY * 30;
|
||||
const SIXTY_DAYS = ONE_DAY * 60;
|
||||
const NINETY_DAYS = ONE_DAY * 90;
|
||||
const HALF_YEAR = ONE_DAY * 180;
|
||||
const ONE_YEAR = ONE_DAY * 365;
|
||||
|
||||
export const telemetryTimeRangeFormatter = (ms: number): string => {
|
||||
if (ms < ONE_MINUTE) return '1. Less than 1 minute';
|
||||
if (ms >= ONE_MINUTE && ms < FIVE_MINUTES) return '2. 1-5 minutes';
|
||||
if (ms >= FIVE_MINUTES && ms < TEN_MINUTES) return '3. 5-10 minutes';
|
||||
if (ms >= TEN_MINUTES && ms < THIRTY_MINUTES) return '4. 10-30 minutes';
|
||||
if (ms >= THIRTY_MINUTES && ms < ONE_HOUR) return '5. 30-60 minutes';
|
||||
if (ms >= ONE_HOUR && ms < TWO_HOURS) return '6. 1-2 hours';
|
||||
if (ms >= TWO_HOURS && ms < EIGHT_HOURS) return '7. 2-8 hours';
|
||||
if (ms >= EIGHT_HOURS && ms < TWELVE_HOURS) return '8. 8-12 hours';
|
||||
if (ms >= TWELVE_HOURS && ms < ONE_DAY) return '9. 12-24 hours';
|
||||
if (ms >= ONE_DAY && ms < TWO_DAYS) return '10. 1-2 days';
|
||||
if (ms >= TWO_DAYS && ms < SEVEN_DAYS) return '11. 2-7 days';
|
||||
if (ms >= SEVEN_DAYS && ms < FOURTEEN_DAYS) return '12. 7-14 days';
|
||||
if (ms >= FOURTEEN_DAYS && ms < THIRTY_DAYS) return '13. 14-30 days';
|
||||
if (ms >= THIRTY_DAYS && ms < SIXTY_DAYS) return '14. 30-60 days';
|
||||
if (ms >= SIXTY_DAYS && ms < NINETY_DAYS) return '15. 60-90 days';
|
||||
if (ms >= NINETY_DAYS && ms < HALF_YEAR) return '16. 90-180 days';
|
||||
if (ms >= HALF_YEAR && ms < ONE_YEAR) return '17. 180-365 days';
|
||||
return '18. More than 1 year';
|
||||
};
|
|
@ -1,53 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { EuiToolTip } from '@elastic/eui';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiText } from '@elastic/eui';
|
||||
|
||||
const cloudIcons: Record<string, string> = {
|
||||
gcp: 'logoGCP',
|
||||
aws: 'logoAWS',
|
||||
azure: 'logoAzure',
|
||||
unknownProvider: 'cloudSunny',
|
||||
};
|
||||
|
||||
export const CloudProviderIconWithTitle = ({
|
||||
provider,
|
||||
title,
|
||||
text,
|
||||
}: {
|
||||
provider?: string | null;
|
||||
title?: React.ReactNode;
|
||||
text: string;
|
||||
}) => {
|
||||
return (
|
||||
<EuiFlexGroup
|
||||
alignItems="center"
|
||||
className="eui-textTruncate"
|
||||
gutterSize="s"
|
||||
responsive={false}
|
||||
>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiToolTip delay="long" content={provider ?? 'Unknown'}>
|
||||
<EuiIcon
|
||||
type={(provider && cloudIcons[provider]) || cloudIcons.unknownProvider}
|
||||
size="m"
|
||||
title={text}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false} className="eui-textTruncate">
|
||||
{title ?? (
|
||||
<EuiText size="relative" className="eui-textTruncate">
|
||||
{text}
|
||||
</EuiText>
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
|
@ -5,11 +5,10 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useCallback, useEffect, useMemo } from 'react';
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import { EuiInMemoryTable } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { isEqual } from 'lodash';
|
||||
import { buildHostsTableColumns } from './hosts_table_columns';
|
||||
import { NoData } from '../../../../components/empty_states';
|
||||
import { InfraLoadingPanel } from '../../../../components/loading';
|
||||
import { useHostsTable } from '../hooks/use_hosts_table';
|
||||
|
@ -41,6 +40,8 @@ export const HostsTable = () => {
|
|||
metrics: HOST_TABLE_METRICS,
|
||||
});
|
||||
|
||||
const { columns, items } = useHostsTable(nodes, { time: unifiedSearchDateRange });
|
||||
|
||||
useEffect(() => {
|
||||
if (hostViewState.loading !== loading || nodes.length !== hostViewState.totalHits) {
|
||||
setHostViewState({
|
||||
|
@ -58,7 +59,6 @@ export const HostsTable = () => {
|
|||
setHostViewState,
|
||||
]);
|
||||
|
||||
const items = useHostsTable(nodes);
|
||||
const noData = items.length === 0;
|
||||
|
||||
const onTableChange = useCallback(
|
||||
|
@ -79,11 +79,6 @@ export const HostsTable = () => {
|
|||
[setProperties, properties.pagination, properties.sorting]
|
||||
);
|
||||
|
||||
const hostsTableColumns = useMemo(
|
||||
() => buildHostsTableColumns({ time: unifiedSearchDateRange }),
|
||||
[unifiedSearchDateRange]
|
||||
);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<InfraLoadingPanel
|
||||
|
@ -125,7 +120,7 @@ export const HostsTable = () => {
|
|||
'data-test-subj': 'hostsView-tableRow',
|
||||
}}
|
||||
items={items}
|
||||
columns={hostsTableColumns}
|
||||
columns={columns}
|
||||
onTableChange={onTableChange}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -1,139 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EuiBasicTableColumn } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiText } from '@elastic/eui';
|
||||
import { encode } from '@kbn/rison';
|
||||
import { TimeRange } from '@kbn/es-query';
|
||||
import type { SnapshotMetricInput, SnapshotNodeMetric } from '../../../../../common/http_api';
|
||||
import { createInventoryMetricFormatter } from '../../inventory_view/lib/create_inventory_metric_formatter';
|
||||
import { CloudProviderIconWithTitle } from './cloud_provider_icon_with_title';
|
||||
import { TruncateLinkWithTooltip } from './truncate_link_with_tooltip';
|
||||
|
||||
interface HostNodeRow extends HostMetrics {
|
||||
os?: string | null;
|
||||
servicesOnHost?: number | null;
|
||||
title: { name: string; cloudProvider?: string | null };
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface HostMetrics {
|
||||
cpuCores: SnapshotNodeMetric;
|
||||
diskLatency: SnapshotNodeMetric;
|
||||
rx: SnapshotNodeMetric;
|
||||
tx: SnapshotNodeMetric;
|
||||
memory: SnapshotNodeMetric;
|
||||
memoryTotal: SnapshotNodeMetric;
|
||||
}
|
||||
|
||||
const formatMetric = (type: SnapshotMetricInput['type'], value: number | undefined | null) =>
|
||||
value || value === 0 ? createInventoryMetricFormatter({ type })(value) : 'N/A';
|
||||
|
||||
interface HostBuilderParams {
|
||||
time: TimeRange;
|
||||
}
|
||||
|
||||
export const buildHostsTableColumns = ({
|
||||
time,
|
||||
}: HostBuilderParams): Array<EuiBasicTableColumn<HostNodeRow>> => {
|
||||
const hostLinkSearch = {
|
||||
_a: encode({ time: { ...time, interval: '>=1m' } }),
|
||||
};
|
||||
|
||||
return [
|
||||
{
|
||||
name: i18n.translate('xpack.infra.hostsViewPage.table.nameColumnHeader', {
|
||||
defaultMessage: 'Name',
|
||||
}),
|
||||
field: 'title',
|
||||
sortable: true,
|
||||
truncateText: true,
|
||||
render: (title: HostNodeRow['title']) => (
|
||||
<CloudProviderIconWithTitle
|
||||
provider={title?.cloudProvider}
|
||||
text={title.name}
|
||||
title={
|
||||
<TruncateLinkWithTooltip
|
||||
text={title.name}
|
||||
linkProps={{
|
||||
app: 'metrics',
|
||||
pathname: `/detail/host/${title.name}`,
|
||||
search: hostLinkSearch,
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: i18n.translate('xpack.infra.hostsViewPage.table.operatingSystemColumnHeader', {
|
||||
defaultMessage: 'Operating System',
|
||||
}),
|
||||
field: 'os',
|
||||
sortable: true,
|
||||
render: (os: string) => <EuiText size="s">{os}</EuiText>,
|
||||
},
|
||||
{
|
||||
name: i18n.translate('xpack.infra.hostsViewPage.table.numberOfCpusColumnHeader', {
|
||||
defaultMessage: '# of CPUs',
|
||||
}),
|
||||
field: 'cpuCores',
|
||||
sortable: true,
|
||||
render: (cpuCores: SnapshotNodeMetric) => (
|
||||
<>{formatMetric('cpuCores', cpuCores?.value ?? cpuCores?.max)}</>
|
||||
),
|
||||
align: 'right',
|
||||
},
|
||||
{
|
||||
name: i18n.translate('xpack.infra.hostsViewPage.table.diskLatencyColumnHeader', {
|
||||
defaultMessage: 'Disk Latency (avg.)',
|
||||
}),
|
||||
field: 'diskLatency.avg',
|
||||
sortable: true,
|
||||
render: (avg: number) => <>{formatMetric('diskLatency', avg)}</>,
|
||||
align: 'right',
|
||||
},
|
||||
{
|
||||
name: i18n.translate('xpack.infra.hostsViewPage.table.averageTxColumnHeader', {
|
||||
defaultMessage: 'TX (avg.)',
|
||||
}),
|
||||
field: 'tx.avg',
|
||||
sortable: true,
|
||||
render: (avg: number) => <>{formatMetric('tx', avg)}</>,
|
||||
align: 'right',
|
||||
},
|
||||
{
|
||||
name: i18n.translate('xpack.infra.hostsViewPage.table.averageRxColumnHeader', {
|
||||
defaultMessage: 'RX (avg.)',
|
||||
}),
|
||||
field: 'rx.avg',
|
||||
sortable: true,
|
||||
render: (avg: number) => <>{formatMetric('rx', avg)}</>,
|
||||
align: 'right',
|
||||
},
|
||||
{
|
||||
name: i18n.translate('xpack.infra.hostsViewPage.table.averageMemoryTotalColumnHeader', {
|
||||
defaultMessage: 'Memory total (avg.)',
|
||||
}),
|
||||
field: 'memoryTotal.avg',
|
||||
sortable: true,
|
||||
render: (avg: number) => <>{formatMetric('memoryTotal', avg)}</>,
|
||||
align: 'right',
|
||||
},
|
||||
{
|
||||
name: i18n.translate('xpack.infra.hostsViewPage.table.averageMemoryUsageColumnHeader', {
|
||||
defaultMessage: 'Memory usage (avg.)',
|
||||
}),
|
||||
field: 'memory.avg',
|
||||
sortable: true,
|
||||
render: (avg: number) => <>{formatMetric('memory', avg)}</>,
|
||||
align: 'right',
|
||||
},
|
||||
];
|
||||
};
|
|
@ -0,0 +1,62 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiLink, EuiToolTip, IconType } from '@elastic/eui';
|
||||
import { TimeRange } from '@kbn/es-query';
|
||||
import { useLinkProps } from '@kbn/observability-plugin/public';
|
||||
import { encode } from '@kbn/rison';
|
||||
import type { CloudProvider, HostNodeRow } from '../hooks/use_hosts_table';
|
||||
|
||||
const cloudIcons: Record<CloudProvider, IconType> = {
|
||||
gcp: 'logoGCP',
|
||||
aws: 'logoAWS',
|
||||
azure: 'logoAzure',
|
||||
unknownProvider: 'cloudSunny',
|
||||
};
|
||||
|
||||
interface HostsTableEntryTitleProps {
|
||||
onClick: () => void;
|
||||
time: TimeRange;
|
||||
title: HostNodeRow['title'];
|
||||
}
|
||||
|
||||
export const HostsTableEntryTitle = ({ onClick, time, title }: HostsTableEntryTitleProps) => {
|
||||
const { name, cloudProvider } = title;
|
||||
|
||||
const link = useLinkProps({
|
||||
app: 'metrics',
|
||||
pathname: `/detail/host/${name}`,
|
||||
search: {
|
||||
_a: encode({ time: { ...time, interval: '>=1m' } }),
|
||||
},
|
||||
});
|
||||
|
||||
const iconType = (cloudProvider && cloudIcons[cloudProvider]) || cloudIcons.unknownProvider;
|
||||
const providerName = cloudProvider ?? 'Unknown';
|
||||
|
||||
return (
|
||||
<EuiFlexGroup
|
||||
alignItems="center"
|
||||
className="eui-textTruncate"
|
||||
gutterSize="s"
|
||||
responsive={false}
|
||||
>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiToolTip delay="long" content={providerName}>
|
||||
<EuiIcon type={iconType} size="m" title={name} />
|
||||
</EuiToolTip>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false} className="eui-textTruncate" onClick={onClick}>
|
||||
<EuiToolTip delay="long" content={name}>
|
||||
<EuiLink className="eui-displayBlock eui-textTruncate" {...link}>
|
||||
{name}
|
||||
</EuiLink>
|
||||
</EuiToolTip>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
|
@ -120,6 +120,6 @@ export const KPIChart = ({
|
|||
|
||||
const KPIChartStyled = styled(Chart)`
|
||||
.echMetric {
|
||||
border-radius: 5px;
|
||||
border-radius: ${(p) => p.theme.eui.euiBorderRadius};
|
||||
}
|
||||
`;
|
||||
|
|
|
@ -1,36 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EuiToolTip, EuiLink } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { LinkDescriptor, useLinkProps } from '@kbn/observability-plugin/public';
|
||||
|
||||
interface Props {
|
||||
text: string;
|
||||
linkProps: LinkDescriptor;
|
||||
}
|
||||
|
||||
export function TruncateLinkWithTooltip(props: Props) {
|
||||
const { text, linkProps } = props;
|
||||
|
||||
const link = useLinkProps(linkProps);
|
||||
|
||||
return (
|
||||
<div className="eui-displayBlock eui-fullWidth">
|
||||
<EuiToolTip
|
||||
className="eui-fullWidth"
|
||||
delay="long"
|
||||
content={text}
|
||||
anchorClassName="eui-fullWidth"
|
||||
>
|
||||
<EuiLink className="eui-displayBlock eui-textTruncate" {...link}>
|
||||
{text}
|
||||
</EuiLink>
|
||||
</EuiToolTip>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -128,8 +128,10 @@ describe('useHostTable hook', () => {
|
|||
},
|
||||
},
|
||||
];
|
||||
const result = renderHook(() => useHostsTable(nodes));
|
||||
const time = { from: 'now-15m', to: 'now', interval: '>=1m' };
|
||||
|
||||
expect(result.result.current).toStrictEqual(items);
|
||||
const { result } = renderHook(() => useHostsTable(nodes, { time }));
|
||||
|
||||
expect(result.current.items).toStrictEqual(items);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,31 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { useMemo } from 'react';
|
||||
import type { SnapshotNode, SnapshotNodeMetric } from '../../../../../common/http_api';
|
||||
import { HostMetrics } from '../components/hosts_table_columns';
|
||||
|
||||
type MappedMetrics = Record<keyof HostMetrics, SnapshotNodeMetric>;
|
||||
|
||||
export const useHostsTable = (nodes: SnapshotNode[]) => {
|
||||
const items = useMemo(() => {
|
||||
return nodes.map(({ metrics, path, name }) => ({
|
||||
name,
|
||||
os: path.at(-1)?.os ?? '-',
|
||||
title: {
|
||||
name,
|
||||
cloudProvider: path.at(-1)?.cloudProvider ?? null,
|
||||
},
|
||||
...metrics.reduce((data, metric) => {
|
||||
data[metric.name as keyof HostMetrics] = metric;
|
||||
return data;
|
||||
}, {} as MappedMetrics),
|
||||
}));
|
||||
}, [nodes]);
|
||||
|
||||
return items;
|
||||
};
|
|
@ -0,0 +1,196 @@
|
|||
/*
|
||||
* 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, { useCallback, useMemo } from 'react';
|
||||
import { EuiBasicTableColumn, EuiText } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { TimeRange } from '@kbn/es-query';
|
||||
|
||||
import { useKibanaContextForPlugin } from '../../../../hooks/use_kibana';
|
||||
import { createInventoryMetricFormatter } from '../../inventory_view/lib/create_inventory_metric_formatter';
|
||||
import { HostsTableEntryTitle } from '../components/hosts_table_entry_title';
|
||||
import type {
|
||||
SnapshotNode,
|
||||
SnapshotNodeMetric,
|
||||
SnapshotMetricInput,
|
||||
} from '../../../../../common/http_api';
|
||||
|
||||
/**
|
||||
* Columns and items types
|
||||
*/
|
||||
export type CloudProvider = 'gcp' | 'aws' | 'azure' | 'unknownProvider';
|
||||
|
||||
type HostMetric = 'cpuCores' | 'diskLatency' | 'rx' | 'tx' | 'memory' | 'memoryTotal';
|
||||
|
||||
type HostMetrics = Record<HostMetric, SnapshotNodeMetric>;
|
||||
|
||||
export interface HostNodeRow extends HostMetrics {
|
||||
os?: string | null;
|
||||
servicesOnHost?: number | null;
|
||||
title: { name: string; cloudProvider?: CloudProvider | null };
|
||||
name: string;
|
||||
}
|
||||
|
||||
// type MappedMetrics = Record<keyof HostNodeRow, SnapshotNodeMetric>;
|
||||
|
||||
interface HostTableParams {
|
||||
time: TimeRange;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper functions
|
||||
*/
|
||||
const formatMetric = (type: SnapshotMetricInput['type'], value: number | undefined | null) => {
|
||||
return value || value === 0 ? createInventoryMetricFormatter({ type })(value) : 'N/A';
|
||||
};
|
||||
|
||||
const buildItemsList = (nodes: SnapshotNode[]) => {
|
||||
return nodes.map(({ metrics, path, name }) => ({
|
||||
name,
|
||||
os: path.at(-1)?.os ?? '-',
|
||||
title: {
|
||||
name,
|
||||
cloudProvider: path.at(-1)?.cloudProvider ?? null,
|
||||
},
|
||||
...metrics.reduce((data, metric) => {
|
||||
data[metric.name as HostMetric] = metric;
|
||||
return data;
|
||||
}, {} as HostMetrics),
|
||||
})) as HostNodeRow[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Columns translations
|
||||
*/
|
||||
const titleLabel = i18n.translate('xpack.infra.hostsViewPage.table.nameColumnHeader', {
|
||||
defaultMessage: 'Name',
|
||||
});
|
||||
|
||||
const osLabel = i18n.translate('xpack.infra.hostsViewPage.table.operatingSystemColumnHeader', {
|
||||
defaultMessage: 'Operating System',
|
||||
});
|
||||
|
||||
const cpuCountLabel = i18n.translate('xpack.infra.hostsViewPage.table.numberOfCpusColumnHeader', {
|
||||
defaultMessage: '# of CPUs',
|
||||
});
|
||||
|
||||
const diskLatencyLabel = i18n.translate('xpack.infra.hostsViewPage.table.diskLatencyColumnHeader', {
|
||||
defaultMessage: 'Disk Latency (avg.)',
|
||||
});
|
||||
|
||||
const averageTXLabel = i18n.translate('xpack.infra.hostsViewPage.table.averageTxColumnHeader', {
|
||||
defaultMessage: 'TX (avg.)',
|
||||
});
|
||||
|
||||
const averageRXLabel = i18n.translate('xpack.infra.hostsViewPage.table.averageRxColumnHeader', {
|
||||
defaultMessage: 'RX (avg.)',
|
||||
});
|
||||
|
||||
const averageTotalMemoryLabel = i18n.translate(
|
||||
'xpack.infra.hostsViewPage.table.averageMemoryTotalColumnHeader',
|
||||
{
|
||||
defaultMessage: 'Memory total (avg.)',
|
||||
}
|
||||
);
|
||||
|
||||
const averageMemoryUsageLabel = i18n.translate(
|
||||
'xpack.infra.hostsViewPage.table.averageMemoryUsageColumnHeader',
|
||||
{
|
||||
defaultMessage: 'Memory usage (avg.)',
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Build a table columns and items starting from the snapshot nodes.
|
||||
*/
|
||||
export const useHostsTable = (nodes: SnapshotNode[], { time }: HostTableParams) => {
|
||||
const {
|
||||
services: { telemetry },
|
||||
} = useKibanaContextForPlugin();
|
||||
|
||||
const reportHostEntryClick = useCallback(
|
||||
({ name, cloudProvider }: HostNodeRow['title']) => {
|
||||
telemetry.reportHostEntryClicked({
|
||||
hostname: name,
|
||||
cloud_provider: cloudProvider,
|
||||
});
|
||||
},
|
||||
[telemetry]
|
||||
);
|
||||
|
||||
const items = useMemo(() => buildItemsList(nodes), [nodes]);
|
||||
|
||||
const columns: Array<EuiBasicTableColumn<HostNodeRow>> = useMemo(
|
||||
() => [
|
||||
{
|
||||
name: titleLabel,
|
||||
field: 'title',
|
||||
sortable: true,
|
||||
truncateText: true,
|
||||
render: (title: HostNodeRow['title']) => (
|
||||
<HostsTableEntryTitle
|
||||
title={title}
|
||||
time={time}
|
||||
onClick={() => reportHostEntryClick(title)}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: osLabel,
|
||||
field: 'os',
|
||||
sortable: true,
|
||||
render: (os: string) => <EuiText size="s">{os}</EuiText>,
|
||||
},
|
||||
{
|
||||
name: cpuCountLabel,
|
||||
field: 'cpuCores',
|
||||
sortable: true,
|
||||
render: (cpuCores: SnapshotNodeMetric) =>
|
||||
formatMetric('cpuCores', cpuCores?.value ?? cpuCores?.max),
|
||||
align: 'right',
|
||||
},
|
||||
{
|
||||
name: diskLatencyLabel,
|
||||
field: 'diskLatency.avg',
|
||||
sortable: true,
|
||||
render: (avg: number) => formatMetric('diskLatency', avg),
|
||||
align: 'right',
|
||||
},
|
||||
{
|
||||
name: averageTXLabel,
|
||||
field: 'tx.avg',
|
||||
sortable: true,
|
||||
render: (avg: number) => formatMetric('tx', avg),
|
||||
align: 'right',
|
||||
},
|
||||
{
|
||||
name: averageRXLabel,
|
||||
field: 'rx.avg',
|
||||
sortable: true,
|
||||
render: (avg: number) => formatMetric('rx', avg),
|
||||
align: 'right',
|
||||
},
|
||||
{
|
||||
name: averageTotalMemoryLabel,
|
||||
field: 'memoryTotal.avg',
|
||||
sortable: true,
|
||||
render: (avg: number) => formatMetric('memoryTotal', avg),
|
||||
align: 'right',
|
||||
},
|
||||
{
|
||||
name: averageMemoryUsageLabel,
|
||||
field: 'memory.avg',
|
||||
sortable: true,
|
||||
render: (avg: number) => formatMetric('memory', avg),
|
||||
align: 'right',
|
||||
},
|
||||
],
|
||||
[reportHostEntryClick, time]
|
||||
);
|
||||
|
||||
return { columns, items };
|
||||
};
|
|
@ -11,10 +11,22 @@ import { buildEsQuery, Filter, Query, TimeRange } from '@kbn/es-query';
|
|||
import type { SavedQuery } from '@kbn/data-plugin/public';
|
||||
import { debounce } from 'lodash';
|
||||
import deepEqual from 'fast-deep-equal';
|
||||
import { telemetryTimeRangeFormatter } from '../../../../../common/formatters/telemetry_time_range';
|
||||
import type { InfraClientStartDeps } from '../../../../types';
|
||||
import { useMetricsDataViewContext } from './use_data_view';
|
||||
import { useSyncKibanaTimeFilterTime } from '../../../../hooks/use_kibana_timefilter_time';
|
||||
import { useHostsUrlState, INITIAL_DATE_RANGE } from './use_unified_search_url_state';
|
||||
import { useHostsUrlState, INITIAL_DATE_RANGE, HostsState } from './use_unified_search_url_state';
|
||||
|
||||
const buildQuerySubmittedPayload = (hostState: HostsState) => {
|
||||
const { panelFilters, filters, dateRangeTimestamp, query: queryObj } = hostState;
|
||||
|
||||
return {
|
||||
control_filters: panelFilters.map((filter) => JSON.stringify(filter)),
|
||||
filters: filters.map((filter) => JSON.stringify(filter)),
|
||||
interval: telemetryTimeRangeFormatter(dateRangeTimestamp.to - dateRangeTimestamp.from),
|
||||
query: queryObj.query,
|
||||
};
|
||||
};
|
||||
|
||||
export const useUnifiedSearch = () => {
|
||||
const { state, dispatch, getTime, getDateRangeAsTimestamp } = useHostsUrlState();
|
||||
|
@ -22,6 +34,7 @@ export const useUnifiedSearch = () => {
|
|||
const { services } = useKibana<InfraClientStartDeps>();
|
||||
const {
|
||||
data: { query: queryManager },
|
||||
telemetry,
|
||||
} = services;
|
||||
|
||||
useSyncKibanaTimeFilterTime(INITIAL_DATE_RANGE, {
|
||||
|
@ -62,6 +75,11 @@ export const useUnifiedSearch = () => {
|
|||
};
|
||||
});
|
||||
|
||||
// Track telemetry event on query/filter/date changes
|
||||
useEffect(() => {
|
||||
telemetry.reportHostsViewQuerySubmitted(buildQuerySubmittedPayload(state));
|
||||
}, [state, telemetry]);
|
||||
|
||||
const onSubmit = useCallback(
|
||||
(data?: {
|
||||
query?: Query;
|
||||
|
|
|
@ -33,12 +33,12 @@ export interface HostsViewQuerySubmittedSchema {
|
|||
|
||||
export interface HostEntryClickedParams {
|
||||
hostname: string;
|
||||
cloud_provider?: string;
|
||||
cloud_provider?: string | null;
|
||||
}
|
||||
|
||||
export interface HostEntryClickedSchema {
|
||||
hostname: SchemaValue<string>;
|
||||
cloud_provider: SchemaValue<string | undefined>;
|
||||
cloud_provider: SchemaValue<string | undefined | null>;
|
||||
}
|
||||
|
||||
export interface ITelemetryClient {
|
||||
|
|
|
@ -80,6 +80,7 @@ export interface InfraClientStartDeps {
|
|||
share: SharePluginStart;
|
||||
storage: IStorageWrapper;
|
||||
lens: LensPublicStart;
|
||||
telemetry: ITelemetryClient;
|
||||
}
|
||||
|
||||
export type InfraClientCoreSetup = CoreSetup<InfraClientStartDeps, InfraClientStartExports>;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue