[Metrics UI] Enhance Inventory View Tooltips (#69757)
* [Metrics UI] Enhance Inventory View Tooltips * Fixing typos * Removing toMetricOpt Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
parent
652b11f270
commit
b7f33b94a8
|
@ -34,7 +34,7 @@ export const SnapshotNodeMetricRT = rt.intersection([
|
|||
SnapshotNodeMetricOptionalRT,
|
||||
]);
|
||||
export const SnapshotNodeRT = rt.type({
|
||||
metric: SnapshotNodeMetricRT,
|
||||
metrics: rt.array(SnapshotNodeMetricRT),
|
||||
path: rt.array(SnapshotNodePathRT),
|
||||
});
|
||||
|
||||
|
@ -97,7 +97,7 @@ export const SnapshotMetricInputRT = rt.union([
|
|||
export const SnapshotRequestRT = rt.intersection([
|
||||
rt.type({
|
||||
timerange: InfraTimerangeInputRT,
|
||||
metric: SnapshotMetricInputRT,
|
||||
metrics: rt.array(SnapshotMetricInputRT),
|
||||
groupBy: SnapshotGroupByRT,
|
||||
nodeType: ItemTypeRT,
|
||||
sourceId: rt.string,
|
||||
|
|
|
@ -30,4 +30,5 @@ export const awsEC2: InventoryModel = {
|
|||
ip: 'aws.ec2.instance.public.ip',
|
||||
},
|
||||
requiredMetrics: ['awsEC2CpuUtilization', 'awsEC2NetworkTraffic', 'awsEC2DiskIOBytes'],
|
||||
tooltipMetrics: ['cpu', 'rx', 'tx'],
|
||||
};
|
||||
|
|
|
@ -35,4 +35,11 @@ export const awsRDS: InventoryModel = {
|
|||
'awsRDSActiveTransactions',
|
||||
'awsRDSLatency',
|
||||
],
|
||||
tooltipMetrics: [
|
||||
'cpu',
|
||||
'rdsLatency',
|
||||
'rdsConnections',
|
||||
'rdsQueriesExecuted',
|
||||
'rdsActiveTransactions',
|
||||
],
|
||||
};
|
||||
|
|
|
@ -35,4 +35,11 @@ export const awsS3: InventoryModel = {
|
|||
'awsS3DownloadBytes',
|
||||
'awsS3UploadBytes',
|
||||
],
|
||||
tooltipMetrics: [
|
||||
's3BucketSize',
|
||||
's3NumberOfObjects',
|
||||
's3TotalRequests',
|
||||
's3UploadBytes',
|
||||
's3DownloadBytes',
|
||||
],
|
||||
};
|
||||
|
|
|
@ -35,4 +35,11 @@ export const awsSQS: InventoryModel = {
|
|||
'awsSQSMessagesEmpty',
|
||||
'awsSQSOldestMessage',
|
||||
],
|
||||
tooltipMetrics: [
|
||||
'sqsMessagesVisible',
|
||||
'sqsMessagesDelayed',
|
||||
'sqsMessagesEmpty',
|
||||
'sqsMessagesSent',
|
||||
'sqsOldestMessage',
|
||||
],
|
||||
};
|
||||
|
|
|
@ -37,4 +37,5 @@ export const container: InventoryModel = {
|
|||
'containerDiskIOBytes',
|
||||
'containerDiskIOOps',
|
||||
],
|
||||
tooltipMetrics: ['cpu', 'memory', 'rx', 'tx'],
|
||||
};
|
||||
|
|
|
@ -47,4 +47,5 @@ export const host: InventoryModel = {
|
|||
...awsRequiredMetrics,
|
||||
...nginxRequireMetrics,
|
||||
],
|
||||
tooltipMetrics: ['cpu', 'memory', 'tx', 'rx'],
|
||||
};
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { SnapshotMetricType } from './types';
|
||||
export const CPUUsage = i18n.translate('xpack.infra.waffle.metricOptions.cpuUsageText', {
|
||||
defaultMessage: 'CPU usage',
|
||||
});
|
||||
|
@ -68,3 +69,81 @@ export const fieldToName = (field: string) => {
|
|||
};
|
||||
return LOOKUP[field] || field;
|
||||
};
|
||||
|
||||
export const SNAPSHOT_METRIC_TRANSLATIONS = {
|
||||
cpu: i18n.translate('xpack.infra.waffle.metricOptions.cpuUsageText', {
|
||||
defaultMessage: 'CPU usage',
|
||||
}),
|
||||
|
||||
memory: i18n.translate('xpack.infra.waffle.metricOptions.memoryUsageText', {
|
||||
defaultMessage: 'Memory usage',
|
||||
}),
|
||||
|
||||
rx: i18n.translate('xpack.infra.waffle.metricOptions.inboundTrafficText', {
|
||||
defaultMessage: 'Inbound traffic',
|
||||
}),
|
||||
|
||||
tx: i18n.translate('xpack.infra.waffle.metricOptions.outboundTrafficText', {
|
||||
defaultMessage: 'Outbound traffic',
|
||||
}),
|
||||
|
||||
logRate: i18n.translate('xpack.infra.waffle.metricOptions.hostLogRateText', {
|
||||
defaultMessage: 'Log rate',
|
||||
}),
|
||||
|
||||
load: i18n.translate('xpack.infra.waffle.metricOptions.loadText', {
|
||||
defaultMessage: 'Load',
|
||||
}),
|
||||
|
||||
count: i18n.translate('xpack.infra.waffle.metricOptions.countText', {
|
||||
defaultMessage: 'Count',
|
||||
}),
|
||||
diskIOReadBytes: i18n.translate('xpack.infra.waffle.metricOptions.diskIOReadBytes', {
|
||||
defaultMessage: 'Disk Reads',
|
||||
}),
|
||||
diskIOWriteBytes: i18n.translate('xpack.infra.waffle.metricOptions.diskIOWriteBytes', {
|
||||
defaultMessage: 'Disk Writes',
|
||||
}),
|
||||
s3BucketSize: i18n.translate('xpack.infra.waffle.metricOptions.s3BucketSize', {
|
||||
defaultMessage: 'Bucket Size',
|
||||
}),
|
||||
s3TotalRequests: i18n.translate('xpack.infra.waffle.metricOptions.s3TotalRequests', {
|
||||
defaultMessage: 'Total Requests',
|
||||
}),
|
||||
s3NumberOfObjects: i18n.translate('xpack.infra.waffle.metricOptions.s3NumberOfObjects', {
|
||||
defaultMessage: 'Number of Objects',
|
||||
}),
|
||||
s3DownloadBytes: i18n.translate('xpack.infra.waffle.metricOptions.s3DownloadBytes', {
|
||||
defaultMessage: 'Downloads (Bytes)',
|
||||
}),
|
||||
s3UploadBytes: i18n.translate('xpack.infra.waffle.metricOptions.s3UploadBytes', {
|
||||
defaultMessage: 'Uploads (Bytes)',
|
||||
}),
|
||||
rdsConnections: i18n.translate('xpack.infra.waffle.metricOptions.rdsConnections', {
|
||||
defaultMessage: 'Connections',
|
||||
}),
|
||||
rdsQueriesExecuted: i18n.translate('xpack.infra.waffle.metricOptions.rdsQueriesExecuted', {
|
||||
defaultMessage: 'Queries Executed',
|
||||
}),
|
||||
rdsActiveTransactions: i18n.translate('xpack.infra.waffle.metricOptions.rdsActiveTransactions', {
|
||||
defaultMessage: 'Active Transactions',
|
||||
}),
|
||||
rdsLatency: i18n.translate('xpack.infra.waffle.metricOptions.rdsLatency', {
|
||||
defaultMessage: 'Latency',
|
||||
}),
|
||||
sqsMessagesVisible: i18n.translate('xpack.infra.waffle.metricOptions.sqsMessagesVisible', {
|
||||
defaultMessage: 'Messages Available',
|
||||
}),
|
||||
sqsMessagesDelayed: i18n.translate('xpack.infra.waffle.metricOptions.sqsMessagesDelayed', {
|
||||
defaultMessage: 'Messages Delayed',
|
||||
}),
|
||||
sqsMessagesSent: i18n.translate('xpack.infra.waffle.metricOptions.sqsMessagesSent', {
|
||||
defaultMessage: 'Messages Added',
|
||||
}),
|
||||
sqsMessagesEmpty: i18n.translate('xpack.infra.waffle.metricOptions.sqsMessagesEmpty', {
|
||||
defaultMessage: 'Messages Returned Empty',
|
||||
}),
|
||||
sqsOldestMessage: i18n.translate('xpack.infra.waffle.metricOptions.sqsOldestMessage', {
|
||||
defaultMessage: 'Oldest Message',
|
||||
}),
|
||||
} as Record<SnapshotMetricType, string>;
|
||||
|
|
|
@ -37,4 +37,5 @@ export const pod: InventoryModel = {
|
|||
'podNetworkTraffic',
|
||||
...nginxRequiredMetrics,
|
||||
],
|
||||
tooltipMetrics: ['cpu', 'memory', 'rx', 'tx'],
|
||||
};
|
||||
|
|
|
@ -351,4 +351,5 @@ export interface InventoryModel {
|
|||
};
|
||||
metrics: InventoryMetrics;
|
||||
requiredMetrics: InventoryMetric[];
|
||||
tooltipMetrics: SnapshotMetricType[];
|
||||
}
|
||||
|
|
|
@ -22,7 +22,7 @@ export interface InfraWaffleMapNode {
|
|||
name: string;
|
||||
ip?: string | null;
|
||||
path: SnapshotNodePath[];
|
||||
metric: SnapshotNodeMetric;
|
||||
metrics: SnapshotNodeMetric[];
|
||||
}
|
||||
|
||||
export type InfraWaffleMapGroup = InfraWaffleMapGroupOfNodes | InfraWaffleMapGroupOfGroups;
|
||||
|
|
|
@ -9,7 +9,8 @@ import { useInterval } from 'react-use';
|
|||
|
||||
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import { convertIntervalToString } from '../../../../utils/convert_interval_to_string';
|
||||
import { NodesOverview, calculateBoundsFromNodes } from './nodes_overview';
|
||||
import { NodesOverview } from './nodes_overview';
|
||||
import { calculateBoundsFromNodes } from '../lib/calculate_bounds_from_nodes';
|
||||
import { PageContent } from '../../../../components/page';
|
||||
import { useSnapshot } from '../hooks/use_snaphot';
|
||||
import { useWaffleTimeContext } from '../hooks/use_waffle_time';
|
||||
|
@ -48,7 +49,7 @@ export const Layout = () => {
|
|||
const { filterQueryAsJson, applyFilterQuery } = useWaffleFiltersContext();
|
||||
const { loading, nodes, reload, interval } = useSnapshot(
|
||||
filterQueryAsJson,
|
||||
metric,
|
||||
[metric],
|
||||
groupBy,
|
||||
nodeType,
|
||||
sourceId,
|
||||
|
|
|
@ -5,7 +5,6 @@
|
|||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { max, min } from 'lodash';
|
||||
import React, { useCallback } from 'react';
|
||||
|
||||
import { InventoryItemType } from '../../../../../common/inventory_models/types';
|
||||
|
@ -16,6 +15,7 @@ import { InfraLoadingPanel } from '../../../../components/loading';
|
|||
import { Map } from './waffle/map';
|
||||
import { TableView } from './table_view';
|
||||
import { SnapshotNode } from '../../../../../common/http_api/snapshot_api';
|
||||
import { calculateBoundsFromNodes } from '../lib/calculate_bounds_from_nodes';
|
||||
|
||||
export interface KueryFilterQuery {
|
||||
kind: 'kuery';
|
||||
|
@ -36,18 +36,6 @@ interface Props {
|
|||
formatter: InfraFormatter;
|
||||
}
|
||||
|
||||
export const calculateBoundsFromNodes = (nodes: SnapshotNode[]): InfraWaffleMapBounds => {
|
||||
const maxValues = nodes.map((node) => node.metric.max);
|
||||
const minValues = nodes.map((node) => node.metric.value);
|
||||
// if there is only one value then we need to set the bottom range to zero for min
|
||||
// otherwise the legend will look silly since both values are the same for top and
|
||||
// bottom.
|
||||
if (minValues.length === 1) {
|
||||
minValues.unshift(0);
|
||||
}
|
||||
return { min: min(minValues) || 0, max: max(maxValues) || 0 };
|
||||
};
|
||||
|
||||
export const NodesOverview = ({
|
||||
autoBounds,
|
||||
boundsOverride,
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
import { EuiButtonEmpty, EuiInMemoryTable, EuiToolTip, EuiBasicTableColumn } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { last } from 'lodash';
|
||||
import { last, first } from 'lodash';
|
||||
import React, { useState, useCallback, useEffect } from 'react';
|
||||
import { createWaffleMapNode } from '../lib/nodes_to_wafflemap';
|
||||
import { InfraWaffleMapNode, InfraWaffleMapOptions } from '../../../../lib/lib';
|
||||
|
@ -142,6 +142,7 @@ export const TableView = (props: Props) => {
|
|||
|
||||
const items = nodes.map((node) => {
|
||||
const name = last(node.path);
|
||||
const metric = first(node.metrics);
|
||||
return {
|
||||
name: (name && name.label) || 'unknown',
|
||||
...getGroupPaths(node.path).reduce(
|
||||
|
@ -151,9 +152,9 @@ export const TableView = (props: Props) => {
|
|||
}),
|
||||
{}
|
||||
),
|
||||
value: node.metric.value,
|
||||
avg: node.metric.avg,
|
||||
max: node.metric.max,
|
||||
value: (metric && metric.value) || 0,
|
||||
avg: (metric && metric.avg) || 0,
|
||||
max: (metric && metric.max) || 0,
|
||||
node: createWaffleMapNode(node),
|
||||
};
|
||||
});
|
||||
|
|
|
@ -0,0 +1,80 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ConditionalToolTip should just work 1`] = `
|
||||
<div
|
||||
data-test-subj="conditionalTooltipContent"
|
||||
style={
|
||||
Object {
|
||||
"minWidth": 200,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"borderBottom": "1px solid #98a2b3",
|
||||
"marginBottom": "4px",
|
||||
"paddingBottom": "4px",
|
||||
}
|
||||
}
|
||||
>
|
||||
host-01
|
||||
</div>
|
||||
<EuiFlexGroup
|
||||
gutterSize="none"
|
||||
>
|
||||
<EuiFlexItem
|
||||
grow={1}
|
||||
>
|
||||
CPU usage
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem
|
||||
grow={false}
|
||||
>
|
||||
10%
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiFlexGroup
|
||||
gutterSize="none"
|
||||
>
|
||||
<EuiFlexItem
|
||||
grow={1}
|
||||
>
|
||||
Memory usage
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem
|
||||
grow={false}
|
||||
>
|
||||
80%
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiFlexGroup
|
||||
gutterSize="none"
|
||||
>
|
||||
<EuiFlexItem
|
||||
grow={1}
|
||||
>
|
||||
Outbound traffic
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem
|
||||
grow={false}
|
||||
>
|
||||
8Mbit/s
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiFlexGroup
|
||||
gutterSize="none"
|
||||
>
|
||||
<EuiFlexItem
|
||||
grow={1}
|
||||
>
|
||||
Inbound traffic
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem
|
||||
grow={false}
|
||||
>
|
||||
8Mbit/s
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</div>
|
||||
`;
|
|
@ -0,0 +1,155 @@
|
|||
/*
|
||||
* 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 React from 'react';
|
||||
import { mount } from 'enzyme';
|
||||
// import { act } from 'react-dom/test-utils';
|
||||
import { EuiThemeProvider } from '../../../../../../../observability/public';
|
||||
import { EuiToolTip } from '@elastic/eui';
|
||||
import { ConditionalToolTip } from './conditional_tooltip';
|
||||
import {
|
||||
InfraWaffleMapNode,
|
||||
InfraWaffleMapOptions,
|
||||
InfraFormatterType,
|
||||
} from '../../../../../lib/lib';
|
||||
|
||||
jest.mock('../../../../../containers/source', () => ({
|
||||
useSourceContext: () => ({ sourceId: 'default' }),
|
||||
}));
|
||||
|
||||
jest.mock('../../hooks/use_snaphot');
|
||||
import { useSnapshot } from '../../hooks/use_snaphot';
|
||||
const mockedUseSnapshot = useSnapshot as jest.Mock<ReturnType<typeof useSnapshot>>;
|
||||
|
||||
const NODE: InfraWaffleMapNode = {
|
||||
pathId: 'host-01',
|
||||
id: 'host-01',
|
||||
name: 'host-01',
|
||||
path: [{ value: 'host-01', label: 'host-01' }],
|
||||
metrics: [{ name: 'cpu' }],
|
||||
};
|
||||
|
||||
const OPTIONS: InfraWaffleMapOptions = {
|
||||
formatter: InfraFormatterType.percent,
|
||||
formatTemplate: '{value}',
|
||||
metric: { type: 'cpu' },
|
||||
groupBy: [],
|
||||
legend: {
|
||||
type: 'steppedGradient',
|
||||
rules: [],
|
||||
},
|
||||
sort: { by: 'value', direction: 'desc' },
|
||||
};
|
||||
|
||||
export const nextTick = () => new Promise((res) => process.nextTick(res));
|
||||
const ChildComponent = () => <div>child</div>;
|
||||
|
||||
describe('ConditionalToolTip', () => {
|
||||
afterEach(() => {
|
||||
mockedUseSnapshot.mockReset();
|
||||
});
|
||||
|
||||
function createWrapper(currentTime: number = Date.now(), hidden: boolean = false) {
|
||||
return mount(
|
||||
<EuiThemeProvider darkMode={false}>
|
||||
<ConditionalToolTip
|
||||
currentTime={currentTime}
|
||||
hidden={hidden}
|
||||
node={NODE}
|
||||
options={OPTIONS}
|
||||
formatter={(v: number) => `${v}`}
|
||||
nodeType="host"
|
||||
>
|
||||
<ChildComponent />
|
||||
</ConditionalToolTip>
|
||||
</EuiThemeProvider>
|
||||
);
|
||||
}
|
||||
|
||||
it('should return children when hidden', () => {
|
||||
mockedUseSnapshot.mockReturnValue({
|
||||
nodes: [],
|
||||
error: null,
|
||||
loading: true,
|
||||
interval: '',
|
||||
reload: jest.fn(() => Promise.resolve()),
|
||||
});
|
||||
const currentTime = Date.now();
|
||||
const wrapper = createWrapper(currentTime, true);
|
||||
expect(wrapper.find(ChildComponent).exists()).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should just work', () => {
|
||||
jest.useFakeTimers();
|
||||
const reloadMock = jest.fn(() => Promise.resolve());
|
||||
mockedUseSnapshot.mockReturnValue({
|
||||
nodes: [
|
||||
{
|
||||
path: [{ label: 'host-01', value: 'host-01', ip: '192.168.1.10' }],
|
||||
metrics: [
|
||||
{ name: 'cpu', value: 0.1, avg: 0.4, max: 0.7 },
|
||||
{ name: 'memory', value: 0.8, avg: 0.8, max: 1 },
|
||||
{ name: 'tx', value: 1000000, avg: 1000000, max: 1000000 },
|
||||
{ name: 'rx', value: 1000000, avg: 1000000, max: 1000000 },
|
||||
],
|
||||
},
|
||||
],
|
||||
error: null,
|
||||
loading: false,
|
||||
interval: '60s',
|
||||
reload: reloadMock,
|
||||
});
|
||||
const currentTime = Date.now();
|
||||
const wrapper = createWrapper(currentTime, false);
|
||||
expect(wrapper.find(ChildComponent).exists()).toBeTruthy();
|
||||
expect(wrapper.find(EuiToolTip).exists()).toBeTruthy();
|
||||
const expectedQuery = JSON.stringify({
|
||||
bool: {
|
||||
filter: {
|
||||
match_phrase: { 'host.name': 'host-01' },
|
||||
},
|
||||
},
|
||||
});
|
||||
const expectedMetrics = [{ type: 'cpu' }, { type: 'memory' }, { type: 'tx' }, { type: 'rx' }];
|
||||
expect(mockedUseSnapshot).toBeCalledWith(
|
||||
expectedQuery,
|
||||
expectedMetrics,
|
||||
[],
|
||||
'host',
|
||||
'default',
|
||||
currentTime,
|
||||
'',
|
||||
'',
|
||||
false
|
||||
);
|
||||
wrapper.find('[data-test-subj~="conditionalTooltipMouseHandler"]').simulate('mouseOver');
|
||||
wrapper.find(EuiToolTip).simulate('mouseOver');
|
||||
jest.advanceTimersByTime(500);
|
||||
expect(reloadMock).toHaveBeenCalled();
|
||||
expect(wrapper.find(EuiToolTip).props().content).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should not load data if mouse out before 200 ms', () => {
|
||||
jest.useFakeTimers();
|
||||
const reloadMock = jest.fn(() => Promise.resolve());
|
||||
mockedUseSnapshot.mockReturnValue({
|
||||
nodes: [],
|
||||
error: null,
|
||||
loading: true,
|
||||
interval: '',
|
||||
reload: reloadMock,
|
||||
});
|
||||
const currentTime = Date.now();
|
||||
const wrapper = createWrapper(currentTime, false);
|
||||
expect(wrapper.find(ChildComponent).exists()).toBeTruthy();
|
||||
expect(wrapper.find(EuiToolTip).exists()).toBeTruthy();
|
||||
wrapper.find('[data-test-subj~="conditionalTooltipMouseHandler"]').simulate('mouseOver');
|
||||
jest.advanceTimersByTime(100);
|
||||
wrapper.find('[data-test-subj~="conditionalTooltipMouseHandler"]').simulate('mouseOut');
|
||||
jest.advanceTimersByTime(200);
|
||||
expect(reloadMock).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
|
@ -3,18 +3,117 @@
|
|||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { EuiToolTip, EuiToolTipProps } from '@elastic/eui';
|
||||
import { omit } from 'lodash';
|
||||
import React, { useCallback, useState, useEffect } from 'react';
|
||||
import { EuiToolTip, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import { first } from 'lodash';
|
||||
import { withTheme, EuiTheme } from '../../../../../../../observability/public';
|
||||
import { useSourceContext } from '../../../../../containers/source';
|
||||
import { findInventoryModel } from '../../../../../../common/inventory_models';
|
||||
import {
|
||||
InventoryItemType,
|
||||
SnapshotMetricType,
|
||||
} from '../../../../../../common/inventory_models/types';
|
||||
import { InfraWaffleMapNode, InfraWaffleMapOptions } from '../../../../../lib/lib';
|
||||
import { useSnapshot } from '../../hooks/use_snaphot';
|
||||
import { createInventoryMetricFormatter } from '../../lib/create_inventory_metric_formatter';
|
||||
import { SNAPSHOT_METRIC_TRANSLATIONS } from '../../../../../../common/inventory_models/intl_strings';
|
||||
|
||||
interface Props extends EuiToolTipProps {
|
||||
export interface Props {
|
||||
currentTime: number;
|
||||
hidden: boolean;
|
||||
node: InfraWaffleMapNode;
|
||||
options: InfraWaffleMapOptions;
|
||||
formatter: (val: number) => string;
|
||||
children: React.ReactElement;
|
||||
nodeType: InventoryItemType;
|
||||
theme: EuiTheme | undefined;
|
||||
}
|
||||
|
||||
export const ConditionalToolTip = (props: Props) => {
|
||||
if (props.hidden) {
|
||||
return props.children;
|
||||
export const ConditionalToolTip = withTheme(
|
||||
({ theme, hidden, node, children, nodeType, currentTime }: Props) => {
|
||||
const { sourceId } = useSourceContext();
|
||||
const [timer, setTimer] = useState<ReturnType<typeof setTimeout> | null>(null);
|
||||
const model = findInventoryModel(nodeType);
|
||||
const requestMetrics = model.tooltipMetrics.map((type) => ({ type })) as Array<{
|
||||
type: SnapshotMetricType;
|
||||
}>;
|
||||
const query = JSON.stringify({
|
||||
bool: {
|
||||
filter: {
|
||||
match_phrase: { [model.fields.id]: node.id },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const { nodes, reload } = useSnapshot(
|
||||
query,
|
||||
requestMetrics,
|
||||
[],
|
||||
nodeType,
|
||||
sourceId,
|
||||
currentTime,
|
||||
'',
|
||||
'',
|
||||
false // Doesn't send request until reload() is called
|
||||
);
|
||||
|
||||
const handleDataLoad = useCallback(() => {
|
||||
const id = setTimeout(reload, 200);
|
||||
setTimer(id);
|
||||
}, [reload]);
|
||||
|
||||
const cancelDataLoad = useCallback(() => {
|
||||
return (timer && clearTimeout(timer)) || void 0;
|
||||
}, [timer]);
|
||||
|
||||
useEffect(() => {
|
||||
return cancelDataLoad;
|
||||
}, [timer, cancelDataLoad]);
|
||||
|
||||
if (hidden) {
|
||||
return children;
|
||||
}
|
||||
|
||||
const dataNode = first(nodes);
|
||||
const metrics = (dataNode && dataNode.metrics) || [];
|
||||
const content = (
|
||||
<div style={{ minWidth: 200 }} data-test-subj="conditionalTooltipContent">
|
||||
<div
|
||||
style={{
|
||||
borderBottom: `1px solid ${theme?.eui.euiColorMediumShade}`,
|
||||
paddingBottom: theme?.eui.paddingSizes.xs,
|
||||
marginBottom: theme?.eui.paddingSizes.xs,
|
||||
}}
|
||||
>
|
||||
{node.name}
|
||||
</div>
|
||||
{metrics.map((metric) => {
|
||||
const name = SNAPSHOT_METRIC_TRANSLATIONS[metric.name] || metric.name;
|
||||
const formatter = createInventoryMetricFormatter({ type: metric.name });
|
||||
return (
|
||||
<EuiFlexGroup gutterSize="none" key={metric.name}>
|
||||
<EuiFlexItem grow={1}>{name}</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
{(metric.value && formatter(metric.value)) || '-'}
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiToolTip delay="regular" position="right" content={content}>
|
||||
<div
|
||||
data-test-subj="conditionalTooltipMouseHandler"
|
||||
onMouseOver={handleDataLoad}
|
||||
onFocus={handleDataLoad}
|
||||
onMouseOut={cancelDataLoad}
|
||||
onBlur={cancelDataLoad}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</EuiToolTip>
|
||||
);
|
||||
}
|
||||
const propsWithoutHidden = omit(props, 'hidden');
|
||||
return <EuiToolTip {...propsWithoutHidden}>{props.children}</EuiToolTip>;
|
||||
};
|
||||
);
|
||||
|
|
|
@ -9,6 +9,7 @@ import React from 'react';
|
|||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { first } from 'lodash';
|
||||
import { ConditionalToolTip } from './conditional_tooltip';
|
||||
import { euiStyled } from '../../../../../../../observability/public';
|
||||
import {
|
||||
|
@ -41,7 +42,7 @@ export const Node = class extends React.PureComponent<Props, State> {
|
|||
public render() {
|
||||
const { nodeType, node, options, squareSize, bounds, formatter, currentTime } = this.props;
|
||||
const { isPopoverOpen } = this.state;
|
||||
const { metric } = node;
|
||||
const metric = first(node.metrics);
|
||||
const valueMode = squareSize > 70;
|
||||
const ellipsisMode = squareSize > 30;
|
||||
const rawValue = (metric && metric.value) || 0;
|
||||
|
@ -62,10 +63,12 @@ export const Node = class extends React.PureComponent<Props, State> {
|
|||
popoverPosition="downCenter"
|
||||
>
|
||||
<ConditionalToolTip
|
||||
delay="regular"
|
||||
currentTime={currentTime}
|
||||
formatter={formatter}
|
||||
hidden={isPopoverOpen}
|
||||
position="top"
|
||||
content={`${node.name} | ${value}`}
|
||||
node={node}
|
||||
options={options}
|
||||
nodeType={nodeType}
|
||||
>
|
||||
<NodeContainer
|
||||
data-test-subj="nodeContainer"
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
import { fold } from 'fp-ts/lib/Either';
|
||||
import { identity } from 'fp-ts/lib/function';
|
||||
import { pipe } from 'fp-ts/lib/pipeable';
|
||||
import { useEffect } from 'react';
|
||||
import { throwErrors, createPlainError } from '../../../../../common/runtime_types';
|
||||
import { useHTTPRequest } from '../../../../hooks/use_http_request';
|
||||
import {
|
||||
|
@ -23,13 +24,14 @@ import {
|
|||
|
||||
export function useSnapshot(
|
||||
filterQuery: string | null | undefined,
|
||||
metric: { type: SnapshotMetricType },
|
||||
metrics: Array<{ type: SnapshotMetricType }>,
|
||||
groupBy: SnapshotGroupBy,
|
||||
nodeType: InventoryItemType,
|
||||
sourceId: string,
|
||||
currentTime: number,
|
||||
accountId: string,
|
||||
region: string
|
||||
region: string,
|
||||
sendRequestImmediatly = true
|
||||
) {
|
||||
const decodeResponse = (response: any) => {
|
||||
return pipe(
|
||||
|
@ -49,7 +51,7 @@ export function useSnapshot(
|
|||
'/api/metrics/snapshot',
|
||||
'POST',
|
||||
JSON.stringify({
|
||||
metric,
|
||||
metrics,
|
||||
groupBy,
|
||||
nodeType,
|
||||
timerange,
|
||||
|
@ -62,6 +64,14 @@ export function useSnapshot(
|
|||
decodeResponse
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
if (sendRequestImmediatly) {
|
||||
await makeRequest();
|
||||
}
|
||||
})();
|
||||
}, [makeRequest, sendRequestImmediatly]);
|
||||
|
||||
return {
|
||||
error: (error && error.message) || null,
|
||||
loading,
|
||||
|
|
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
* 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 { calculateBoundsFromNodes } from './calculate_bounds_from_nodes';
|
||||
import { SnapshotNode } from '../../../../../common/http_api/snapshot_api';
|
||||
const nodes: SnapshotNode[] = [
|
||||
{
|
||||
path: [{ value: 'host-01', label: 'host-01' }],
|
||||
metrics: [
|
||||
{
|
||||
name: 'cpu',
|
||||
value: 0.5,
|
||||
max: 1.5,
|
||||
avg: 0.7,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: [{ value: 'host-02', label: 'host-02' }],
|
||||
metrics: [
|
||||
{
|
||||
name: 'cpu',
|
||||
value: 0.2,
|
||||
max: 0.7,
|
||||
avg: 0.4,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
describe('calculateBoundsFromNodes', () => {
|
||||
it('should just work', () => {
|
||||
const bounds = calculateBoundsFromNodes(nodes);
|
||||
expect(bounds).toEqual({
|
||||
min: 0.2,
|
||||
max: 1.5,
|
||||
});
|
||||
});
|
||||
it('should have a minimum of 0 for only a single node', () => {
|
||||
const bounds = calculateBoundsFromNodes([nodes[0]]);
|
||||
expect(bounds).toEqual({
|
||||
min: 0,
|
||||
max: 1.5,
|
||||
});
|
||||
});
|
||||
it('should return zero for empty nodes', () => {
|
||||
const bounds = calculateBoundsFromNodes([]);
|
||||
expect(bounds).toEqual({
|
||||
min: 0,
|
||||
max: 0,
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* 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 { first, min, max, isFinite } from 'lodash';
|
||||
import { SnapshotNode } from '../../../../../common/http_api/snapshot_api';
|
||||
import { InfraWaffleMapBounds } from '../../../../lib/lib';
|
||||
|
||||
export const calculateBoundsFromNodes = (nodes: SnapshotNode[]): InfraWaffleMapBounds => {
|
||||
const maxValues = nodes.map((node) => {
|
||||
const metric = first(node.metrics);
|
||||
if (!metric) return 0;
|
||||
return metric.max;
|
||||
});
|
||||
const minValues = nodes.map((node) => {
|
||||
const metric = first(node.metrics);
|
||||
if (!metric) return 0;
|
||||
return metric.value;
|
||||
});
|
||||
// if there is only one value then we need to set the bottom range to zero for min
|
||||
// otherwise the legend will look silly since both values are the same for top and
|
||||
// bottom.
|
||||
if (minValues.length === 1) {
|
||||
minValues.unshift(0);
|
||||
}
|
||||
const maxValue = max(maxValues) || 0;
|
||||
const minValue = min(minValues) || 0;
|
||||
return { min: isFinite(minValue) ? minValue : 0, max: isFinite(maxValue) ? maxValue : 0 };
|
||||
};
|
|
@ -36,12 +36,14 @@ describe('createUptimeLink()', () => {
|
|||
name: 'host-01',
|
||||
ip: '10.0.1.2',
|
||||
path: [],
|
||||
metric: {
|
||||
name: 'cpu' as SnapshotMetricType,
|
||||
value: 0.5,
|
||||
max: 0.8,
|
||||
avg: 0.6,
|
||||
},
|
||||
metrics: [
|
||||
{
|
||||
name: 'cpu' as SnapshotMetricType,
|
||||
value: 0.5,
|
||||
max: 0.8,
|
||||
avg: 0.6,
|
||||
},
|
||||
],
|
||||
};
|
||||
expect(createUptimeLink(options, 'host', node)).toStrictEqual({
|
||||
app: 'uptime',
|
||||
|
@ -56,12 +58,14 @@ describe('createUptimeLink()', () => {
|
|||
id: 'host-01',
|
||||
name: 'host-01',
|
||||
path: [],
|
||||
metric: {
|
||||
name: 'cpu' as SnapshotMetricType,
|
||||
value: 0.5,
|
||||
max: 0.8,
|
||||
avg: 0.6,
|
||||
},
|
||||
metrics: [
|
||||
{
|
||||
name: 'cpu' as SnapshotMetricType,
|
||||
value: 0.5,
|
||||
max: 0.8,
|
||||
avg: 0.6,
|
||||
},
|
||||
],
|
||||
};
|
||||
expect(createUptimeLink(options, 'host', node)).toStrictEqual({
|
||||
app: 'uptime',
|
||||
|
@ -76,12 +80,14 @@ describe('createUptimeLink()', () => {
|
|||
id: '29193-pod-02939',
|
||||
name: 'pod-01',
|
||||
path: [],
|
||||
metric: {
|
||||
name: 'cpu' as SnapshotMetricType,
|
||||
value: 0.5,
|
||||
max: 0.8,
|
||||
avg: 0.6,
|
||||
},
|
||||
metrics: [
|
||||
{
|
||||
name: 'cpu' as SnapshotMetricType,
|
||||
value: 0.5,
|
||||
max: 0.8,
|
||||
avg: 0.6,
|
||||
},
|
||||
],
|
||||
};
|
||||
expect(createUptimeLink(options, 'pod', node)).toStrictEqual({
|
||||
app: 'uptime',
|
||||
|
@ -96,12 +102,14 @@ describe('createUptimeLink()', () => {
|
|||
id: 'docker-1234',
|
||||
name: 'docker-01',
|
||||
path: [],
|
||||
metric: {
|
||||
name: 'cpu' as SnapshotMetricType,
|
||||
value: 0.5,
|
||||
max: 0.8,
|
||||
avg: 0.6,
|
||||
},
|
||||
metrics: [
|
||||
{
|
||||
name: 'cpu' as SnapshotMetricType,
|
||||
value: 0.5,
|
||||
max: 0.8,
|
||||
avg: 0.6,
|
||||
},
|
||||
],
|
||||
};
|
||||
expect(createUptimeLink(options, 'container', node)).toStrictEqual({
|
||||
app: 'uptime',
|
||||
|
|
|
@ -95,7 +95,7 @@ export function createWaffleMapNode(node: SnapshotNode): InfraWaffleMapNode {
|
|||
id: nodePathItem.value,
|
||||
ip: nodePathItem.ip,
|
||||
name: nodePathItem.label || nodePathItem.value,
|
||||
metric: node.metric,
|
||||
metrics: node.metrics,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
* 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 { sortNodes } from './sort_nodes';
|
||||
import { SnapshotNode } from '../../../../../common/http_api/snapshot_api';
|
||||
|
||||
const nodes: SnapshotNode[] = [
|
||||
{
|
||||
path: [{ value: 'host-01', label: 'host-01' }],
|
||||
metrics: [
|
||||
{
|
||||
name: 'cpu',
|
||||
value: 0.5,
|
||||
max: 1.5,
|
||||
avg: 0.7,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: [{ value: 'host-02', label: 'host-02' }],
|
||||
metrics: [
|
||||
{
|
||||
name: 'cpu',
|
||||
value: 0.2,
|
||||
max: 0.7,
|
||||
avg: 0.4,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
describe('sortNodes', () => {
|
||||
describe('asc', () => {
|
||||
it('should sort by name', () => {
|
||||
const sortedNodes = sortNodes({ by: 'name', direction: 'asc' }, nodes);
|
||||
expect(sortedNodes).toEqual(nodes);
|
||||
});
|
||||
it('should sort by merics', () => {
|
||||
const sortedNodes = sortNodes({ by: 'value', direction: 'asc' }, nodes);
|
||||
expect(sortedNodes).toEqual(nodes.reverse());
|
||||
});
|
||||
});
|
||||
describe('desc', () => {
|
||||
it('should sort by name', () => {
|
||||
const sortedNodes = sortNodes({ by: 'name', direction: 'desc' }, nodes);
|
||||
expect(sortedNodes).toEqual(nodes.reverse());
|
||||
});
|
||||
it('should sort by merics', () => {
|
||||
const sortedNodes = sortNodes({ by: 'value', direction: 'desc' }, nodes);
|
||||
expect(sortedNodes).toEqual(nodes);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -4,13 +4,16 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { sortBy, last } from 'lodash';
|
||||
import { sortBy, last, first } from 'lodash';
|
||||
import { SnapshotNode } from '../../../../../common/http_api/snapshot_api';
|
||||
import { WaffleSortOption } from '../hooks/use_waffle_options';
|
||||
|
||||
const SORT_PATHS = {
|
||||
name: (node: SnapshotNode) => last(node.path),
|
||||
value: (node: SnapshotNode) => node.metric.value || 0,
|
||||
value: (node: SnapshotNode) => {
|
||||
const metric = first(node.metrics);
|
||||
return (metric && metric.value) || 0;
|
||||
},
|
||||
};
|
||||
|
||||
export const sortNodes = (sort: WaffleSortOption, nodes: SnapshotNode[]) => {
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import { mapValues, last } from 'lodash';
|
||||
import { mapValues, last, first } from 'lodash';
|
||||
import moment from 'moment';
|
||||
import {
|
||||
InfraDatabaseSearchResponse,
|
||||
|
@ -95,7 +95,7 @@ const getData = async (
|
|||
nodeType,
|
||||
groupBy: [],
|
||||
sourceConfiguration,
|
||||
metric: { type: metric },
|
||||
metrics: [{ type: metric }],
|
||||
timerange,
|
||||
includeTimeseries: Boolean(timerange.lookbackSize),
|
||||
};
|
||||
|
@ -104,12 +104,13 @@ const getData = async (
|
|||
|
||||
return nodes.reduce((acc, n) => {
|
||||
const nodePathItem = last(n.path);
|
||||
if (n.metric?.value && n.metric?.timeseries) {
|
||||
const { timeseries } = n.metric;
|
||||
const m = first(n.metrics);
|
||||
if (m && m.value && m.timeseries) {
|
||||
const { timeseries } = m;
|
||||
const values = timeseries.rows.map((row) => row.metric_0) as Array<number | null>;
|
||||
acc[nodePathItem.label] = values;
|
||||
} else {
|
||||
acc[nodePathItem.label] = n.metric && n.metric.value;
|
||||
acc[nodePathItem.label] = m && m.value;
|
||||
}
|
||||
return acc;
|
||||
}, {} as Record<string, number | Array<number | string | null | undefined> | undefined | null>);
|
||||
|
|
|
@ -54,14 +54,18 @@ export const getMetricsSources = (options: InfraSnapshotRequestOptions) => {
|
|||
return [{ id: { terms: { field: fields.id } } }];
|
||||
};
|
||||
|
||||
export const metricToAggregation = (nodeType: InventoryItemType, metric: SnapshotMetricInput) => {
|
||||
export const metricToAggregation = (
|
||||
nodeType: InventoryItemType,
|
||||
metric: SnapshotMetricInput,
|
||||
index: number
|
||||
) => {
|
||||
const inventoryModel = findInventoryModel(nodeType);
|
||||
if (SnapshotCustomMetricInputRT.is(metric)) {
|
||||
if (metric.aggregation === 'rate') {
|
||||
return networkTraffic(metric.type, metric.field);
|
||||
return networkTraffic(`custom_${index}`, metric.field);
|
||||
}
|
||||
return {
|
||||
custom: {
|
||||
[`custom_${index}`]: {
|
||||
[metric.aggregation]: {
|
||||
field: metric.field,
|
||||
},
|
||||
|
@ -72,19 +76,22 @@ export const metricToAggregation = (nodeType: InventoryItemType, metric: Snapsho
|
|||
};
|
||||
|
||||
export const getMetricsAggregations = (options: InfraSnapshotRequestOptions): SnapshotModel => {
|
||||
const aggregation = metricToAggregation(options.nodeType, options.metric);
|
||||
if (!SnapshotModelRT.is(aggregation)) {
|
||||
throw new Error(
|
||||
i18n.translate('xpack.infra.snapshot.missingSnapshotMetricError', {
|
||||
defaultMessage: 'The aggregation for {metric} for {nodeType} is not available.',
|
||||
values: {
|
||||
nodeType: options.nodeType,
|
||||
metric: options.metric.type,
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
return aggregation;
|
||||
const { metrics } = options;
|
||||
return metrics.reduce((aggs, metric, index) => {
|
||||
const aggregation = metricToAggregation(options.nodeType, metric, index);
|
||||
if (!SnapshotModelRT.is(aggregation)) {
|
||||
throw new Error(
|
||||
i18n.translate('xpack.infra.snapshot.missingSnapshotMetricError', {
|
||||
defaultMessage: 'The aggregation for {metric} for {nodeType} is not available.',
|
||||
values: {
|
||||
nodeType: options.nodeType,
|
||||
metric: metric.type,
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
return { ...aggs, ...aggregation };
|
||||
}, {});
|
||||
};
|
||||
|
||||
export const getDateHistogramOffset = (from: number, interval: string): string => {
|
||||
|
|
|
@ -118,26 +118,28 @@ export const getNodeMetricsForLookup = (
|
|||
export const getNodeMetrics = (
|
||||
nodeBuckets: InfraSnapshotMetricsBucket[],
|
||||
options: InfraSnapshotRequestOptions
|
||||
): SnapshotNodeMetric => {
|
||||
): SnapshotNodeMetric[] => {
|
||||
if (!nodeBuckets) {
|
||||
return {
|
||||
name: options.metric.type,
|
||||
return options.metrics.map((metric) => ({
|
||||
name: metric.type,
|
||||
value: null,
|
||||
max: null,
|
||||
avg: null,
|
||||
};
|
||||
}));
|
||||
}
|
||||
const lastBucket = findLastFullBucket(nodeBuckets, options);
|
||||
const result: SnapshotNodeMetric = {
|
||||
name: options.metric.type,
|
||||
value: getMetricValueFromBucket(options.metric.type, lastBucket),
|
||||
max: calculateMax(nodeBuckets, options.metric.type),
|
||||
avg: calculateAvg(nodeBuckets, options.metric.type),
|
||||
};
|
||||
if (options.includeTimeseries) {
|
||||
result.timeseries = getTimeseriesData(nodeBuckets, options.metric.type);
|
||||
}
|
||||
return result;
|
||||
return options.metrics.map((metric, index) => {
|
||||
const metricResult: SnapshotNodeMetric = {
|
||||
name: metric.type,
|
||||
value: getMetricValueFromBucket(metric.type, lastBucket, index),
|
||||
max: calculateMax(nodeBuckets, metric.type, index),
|
||||
avg: calculateAvg(nodeBuckets, metric.type, index),
|
||||
};
|
||||
if (options.includeTimeseries) {
|
||||
metricResult.timeseries = getTimeseriesData(nodeBuckets, metric.type, index);
|
||||
}
|
||||
return metricResult;
|
||||
});
|
||||
};
|
||||
|
||||
const findLastFullBucket = (
|
||||
|
@ -156,22 +158,39 @@ const findLastFullBucket = (
|
|||
}, last(buckets));
|
||||
};
|
||||
|
||||
const getMetricValueFromBucket = (type: SnapshotMetricType, bucket: InfraSnapshotMetricsBucket) => {
|
||||
const metric = bucket[type];
|
||||
const getMetricValueFromBucket = (
|
||||
type: SnapshotMetricType,
|
||||
bucket: InfraSnapshotMetricsBucket,
|
||||
index: number
|
||||
) => {
|
||||
const key = type === 'custom' ? `custom_${index}` : type;
|
||||
const metric = bucket[key];
|
||||
return (metric && (metric.normalized_value || metric.value)) || 0;
|
||||
};
|
||||
|
||||
function calculateMax(buckets: InfraSnapshotMetricsBucket[], type: SnapshotMetricType) {
|
||||
return max(buckets.map((bucket) => getMetricValueFromBucket(type, bucket))) || 0;
|
||||
function calculateMax(
|
||||
buckets: InfraSnapshotMetricsBucket[],
|
||||
type: SnapshotMetricType,
|
||||
index: number
|
||||
) {
|
||||
return max(buckets.map((bucket) => getMetricValueFromBucket(type, bucket, index))) || 0;
|
||||
}
|
||||
|
||||
function calculateAvg(buckets: InfraSnapshotMetricsBucket[], type: SnapshotMetricType) {
|
||||
return sum(buckets.map((bucket) => getMetricValueFromBucket(type, bucket))) / buckets.length || 0;
|
||||
function calculateAvg(
|
||||
buckets: InfraSnapshotMetricsBucket[],
|
||||
type: SnapshotMetricType,
|
||||
index: number
|
||||
) {
|
||||
return (
|
||||
sum(buckets.map((bucket) => getMetricValueFromBucket(type, bucket, index))) / buckets.length ||
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
function getTimeseriesData(
|
||||
buckets: InfraSnapshotMetricsBucket[],
|
||||
type: SnapshotMetricType
|
||||
type: SnapshotMetricType,
|
||||
index: number
|
||||
): MetricsExplorerSeries {
|
||||
return {
|
||||
id: type,
|
||||
|
@ -181,7 +200,7 @@ function getTimeseriesData(
|
|||
],
|
||||
rows: buckets.map((bucket) => ({
|
||||
timestamp: bucket.key as number,
|
||||
metric_0: getMetricValueFromBucket(type, bucket),
|
||||
metric_0: getMetricValueFromBucket(type, bucket, index),
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
|
|
@ -110,15 +110,22 @@ const requestGroupedNodes = async (
|
|||
>(callClusterFactory(client), query, bucketSelector, handleAfterKey);
|
||||
};
|
||||
|
||||
const calculateIndexPatterBasedOnMetrics = (options: InfraSnapshotRequestOptions) => {
|
||||
const { metrics } = options;
|
||||
if (metrics.every((m) => m.type === 'logRate')) {
|
||||
return options.sourceConfiguration.logAlias;
|
||||
}
|
||||
if (metrics.some((m) => m.type === 'logRate')) {
|
||||
return `${options.sourceConfiguration.logAlias},${options.sourceConfiguration.metricAlias}`;
|
||||
}
|
||||
return options.sourceConfiguration.metricAlias;
|
||||
};
|
||||
|
||||
const requestNodeMetrics = async (
|
||||
client: ESSearchClient,
|
||||
options: InfraSnapshotRequestOptions
|
||||
): Promise<InfraSnapshotNodeMetricsBucket[]> => {
|
||||
const index =
|
||||
options.metric.type === 'logRate'
|
||||
? `${options.sourceConfiguration.logAlias}`
|
||||
: `${options.sourceConfiguration.metricAlias}`;
|
||||
|
||||
const index = calculateIndexPatterBasedOnMetrics(options);
|
||||
const query = {
|
||||
allowNoIndices: true,
|
||||
index,
|
||||
|
@ -179,7 +186,7 @@ const mergeNodeBuckets = (
|
|||
return nodeGroupByBuckets.map((node) => {
|
||||
return {
|
||||
path: getNodePath(node, options),
|
||||
metric: getNodeMetrics(nodeMetricsForLookup[node.key.id], options),
|
||||
metrics: getNodeMetrics(nodeMetricsForLookup[node.key.id], options),
|
||||
};
|
||||
});
|
||||
};
|
||||
|
|
|
@ -35,7 +35,7 @@ export const initSnapshotRoute = (libs: InfraBackendLibs) => {
|
|||
nodeType,
|
||||
groupBy,
|
||||
sourceId,
|
||||
metric,
|
||||
metrics,
|
||||
timerange,
|
||||
accountId,
|
||||
region,
|
||||
|
@ -56,7 +56,7 @@ export const initSnapshotRoute = (libs: InfraBackendLibs) => {
|
|||
nodeType,
|
||||
groupBy,
|
||||
sourceConfiguration: source.configuration,
|
||||
metric,
|
||||
metrics,
|
||||
timerange,
|
||||
includeTimeseries,
|
||||
};
|
||||
|
|
|
@ -47,7 +47,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
from: min,
|
||||
interval: '1m',
|
||||
},
|
||||
metric: { type: 'cpu' } as InfraSnapshotMetricInput,
|
||||
metrics: [{ type: 'cpu' }] as InfraSnapshotMetricInput[],
|
||||
nodeType: 'container' as InfraNodeType,
|
||||
groupBy: [],
|
||||
});
|
||||
|
@ -68,13 +68,15 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
'242fddb9d376bbf0e38025d81764847ee5ec0308adfa095918fd3266f9d06c6a'
|
||||
);
|
||||
expect(first(firstNode.path)).to.have.property('label', 'docker-autodiscovery_nginx_1');
|
||||
expect(firstNode).to.have.property('metric');
|
||||
expect(firstNode.metric).to.eql({
|
||||
name: 'cpu',
|
||||
value: 0,
|
||||
max: 0,
|
||||
avg: 0,
|
||||
});
|
||||
expect(firstNode).to.have.property('metrics');
|
||||
expect(firstNode.metrics).to.eql([
|
||||
{
|
||||
name: 'cpu',
|
||||
value: 0,
|
||||
max: 0,
|
||||
avg: 0,
|
||||
},
|
||||
]);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
@ -93,7 +95,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
from: min,
|
||||
interval: '1m',
|
||||
},
|
||||
metric: { type: 'cpu' } as InfraSnapshotMetricInput,
|
||||
metrics: [{ type: 'cpu' }] as InfraSnapshotMetricInput[],
|
||||
nodeType: 'pod' as InfraNodeType,
|
||||
groupBy: [],
|
||||
});
|
||||
|
@ -125,7 +127,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
from: min,
|
||||
interval: '1m',
|
||||
},
|
||||
metric: { type: 'cpu' } as InfraSnapshotMetricInput,
|
||||
metrics: [{ type: 'cpu' }] as InfraSnapshotMetricInput[],
|
||||
nodeType: 'container' as InfraNodeType,
|
||||
groupBy: [],
|
||||
});
|
||||
|
@ -164,7 +166,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
from: min,
|
||||
interval: '1m',
|
||||
},
|
||||
metric: { type: 'cpu' } as InfraSnapshotMetricInput,
|
||||
metrics: [{ type: 'cpu' }] as InfraSnapshotMetricInput[],
|
||||
nodeType: 'host' as InfraNodeType,
|
||||
groupBy: [],
|
||||
});
|
||||
|
@ -179,13 +181,15 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
expect(firstNode.path.length).to.equal(1);
|
||||
expect(first(firstNode.path)).to.have.property('value', 'demo-stack-mysql-01');
|
||||
expect(first(firstNode.path)).to.have.property('label', 'demo-stack-mysql-01');
|
||||
expect(firstNode).to.have.property('metric');
|
||||
expect(firstNode.metric).to.eql({
|
||||
name: 'cpu',
|
||||
value: 0.0032,
|
||||
max: 0.0038333333333333336,
|
||||
avg: 0.0027944444444444444,
|
||||
});
|
||||
expect(firstNode).to.have.property('metrics');
|
||||
expect(firstNode.metrics).to.eql([
|
||||
{
|
||||
name: 'cpu',
|
||||
value: 0.0032,
|
||||
max: 0.0038333333333333336,
|
||||
avg: 0.0027944444444444444,
|
||||
},
|
||||
]);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
@ -200,7 +204,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
forceInterval: true,
|
||||
ignoreLookback: true,
|
||||
},
|
||||
metric: { type: 'cpu' } as InfraSnapshotMetricInput,
|
||||
metrics: [{ type: 'cpu' }] as InfraSnapshotMetricInput[],
|
||||
nodeType: 'host' as InfraNodeType,
|
||||
groupBy: [],
|
||||
includeTimeseries: true,
|
||||
|
@ -216,10 +220,10 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
expect(firstNode.path.length).to.equal(1);
|
||||
expect(first(firstNode.path)).to.have.property('value', 'demo-stack-mysql-01');
|
||||
expect(first(firstNode.path)).to.have.property('label', 'demo-stack-mysql-01');
|
||||
expect(firstNode).to.have.property('metric');
|
||||
expect(firstNode.metric).to.have.property('timeseries');
|
||||
expect(firstNode.metric.timeseries?.rows.length).to.equal(58);
|
||||
const rows = firstNode.metric.timeseries?.rows;
|
||||
expect(firstNode).to.have.property('metrics');
|
||||
expect(firstNode.metrics[0]).to.have.property('timeseries');
|
||||
expect(firstNode.metrics[0].timeseries?.rows.length).to.equal(58);
|
||||
const rows = firstNode.metrics[0].timeseries?.rows;
|
||||
const rowInterval = (rows?.[1]?.timestamp || 0) - (rows?.[0]?.timestamp || 0);
|
||||
expect(rowInterval).to.equal(10000);
|
||||
}
|
||||
|
@ -235,7 +239,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
interval: '1m',
|
||||
lookbackSize: 6,
|
||||
},
|
||||
metric: { type: 'cpu' } as InfraSnapshotMetricInput,
|
||||
metrics: [{ type: 'cpu' }] as InfraSnapshotMetricInput[],
|
||||
nodeType: 'host' as InfraNodeType,
|
||||
groupBy: [],
|
||||
includeTimeseries: true,
|
||||
|
@ -251,9 +255,9 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
expect(firstNode.path.length).to.equal(1);
|
||||
expect(first(firstNode.path)).to.have.property('value', 'demo-stack-mysql-01');
|
||||
expect(first(firstNode.path)).to.have.property('label', 'demo-stack-mysql-01');
|
||||
expect(firstNode).to.have.property('metric');
|
||||
expect(firstNode.metric).to.have.property('timeseries');
|
||||
expect(firstNode.metric.timeseries?.rows.length).to.equal(7);
|
||||
expect(firstNode).to.have.property('metrics');
|
||||
expect(firstNode.metrics[0]).to.have.property('timeseries');
|
||||
expect(firstNode.metrics[0].timeseries?.rows.length).to.equal(7);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
@ -266,12 +270,14 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
from: min,
|
||||
interval: '1m',
|
||||
},
|
||||
metric: {
|
||||
type: 'custom',
|
||||
field: 'system.cpu.user.pct',
|
||||
aggregation: 'avg',
|
||||
id: '1',
|
||||
} as SnapshotMetricInput,
|
||||
metrics: [
|
||||
{
|
||||
type: 'custom',
|
||||
field: 'system.cpu.user.pct',
|
||||
aggregation: 'avg',
|
||||
id: '1',
|
||||
},
|
||||
] as SnapshotMetricInput[],
|
||||
nodeType: 'host' as InfraNodeType,
|
||||
groupBy: [],
|
||||
});
|
||||
|
@ -286,13 +292,15 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
expect(firstNode.path.length).to.equal(1);
|
||||
expect(first(firstNode.path)).to.have.property('value', 'demo-stack-mysql-01');
|
||||
expect(first(firstNode.path)).to.have.property('label', 'demo-stack-mysql-01');
|
||||
expect(firstNode).to.have.property('metric');
|
||||
expect(firstNode.metric).to.eql({
|
||||
name: 'custom',
|
||||
value: 0.0016,
|
||||
max: 0.0018333333333333333,
|
||||
avg: 0.0013666666666666669,
|
||||
});
|
||||
expect(firstNode).to.have.property('metrics');
|
||||
expect(firstNode.metrics).to.eql([
|
||||
{
|
||||
name: 'custom',
|
||||
value: 0.0016,
|
||||
max: 0.0018333333333333333,
|
||||
avg: 0.0013666666666666669,
|
||||
},
|
||||
]);
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -304,7 +312,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
from: min,
|
||||
interval: '1m',
|
||||
},
|
||||
metric: { type: 'cpu' } as InfraSnapshotMetricInput,
|
||||
metrics: [{ type: 'cpu' }] as InfraSnapshotMetricInput[],
|
||||
nodeType: 'host' as InfraNodeType,
|
||||
groupBy: [{ field: 'cloud.availability_zone' }],
|
||||
});
|
||||
|
@ -331,7 +339,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
from: min,
|
||||
interval: '1m',
|
||||
},
|
||||
metric: { type: 'cpu' } as InfraSnapshotMetricInput,
|
||||
metrics: [{ type: 'cpu' }] as InfraSnapshotMetricInput[],
|
||||
nodeType: 'host' as InfraNodeType,
|
||||
groupBy: [{ field: 'cloud.provider' }, { field: 'cloud.availability_zone' }],
|
||||
});
|
||||
|
@ -360,7 +368,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
from: min,
|
||||
interval: '1m',
|
||||
},
|
||||
metric: { type: 'cpu' } as InfraSnapshotMetricInput,
|
||||
metrics: [{ type: 'cpu' }] as InfraSnapshotMetricInput[],
|
||||
nodeType: 'host' as InfraNodeType,
|
||||
groupBy: [{ field: 'service.type' }],
|
||||
});
|
||||
|
@ -375,25 +383,29 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
expect(firstNode.path.length).to.equal(2);
|
||||
expect(firstNode.path[0]).to.have.property('value', 'mysql');
|
||||
expect(firstNode.path[1]).to.have.property('value', 'demo-stack-mysql-01');
|
||||
expect(firstNode).to.have.property('metric');
|
||||
expect(firstNode.metric).to.eql({
|
||||
name: 'cpu',
|
||||
value: 0.0032,
|
||||
max: 0.0038333333333333336,
|
||||
avg: 0.0027944444444444444,
|
||||
});
|
||||
expect(firstNode).to.have.property('metrics');
|
||||
expect(firstNode.metrics).to.eql([
|
||||
{
|
||||
name: 'cpu',
|
||||
value: 0.0032,
|
||||
max: 0.0038333333333333336,
|
||||
avg: 0.0027944444444444444,
|
||||
},
|
||||
]);
|
||||
const secondNode = nodes[1];
|
||||
expect(secondNode).to.have.property('path');
|
||||
expect(secondNode.path.length).to.equal(2);
|
||||
expect(secondNode.path[0]).to.have.property('value', 'system');
|
||||
expect(secondNode.path[1]).to.have.property('value', 'demo-stack-mysql-01');
|
||||
expect(secondNode).to.have.property('metric');
|
||||
expect(secondNode.metric).to.eql({
|
||||
name: 'cpu',
|
||||
value: 0.0032,
|
||||
max: 0.0038333333333333336,
|
||||
avg: 0.0027944444444444444,
|
||||
});
|
||||
expect(secondNode).to.have.property('metrics');
|
||||
expect(secondNode.metrics).to.eql([
|
||||
{
|
||||
name: 'cpu',
|
||||
value: 0.0032,
|
||||
max: 0.0038333333333333336,
|
||||
avg: 0.0027944444444444444,
|
||||
},
|
||||
]);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue