mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
# Backport This will backport the following commits from `main` to `8.15`: - [[Infra] Limit the number of metrics accepted by Snapshot API (#188181)](https://github.com/elastic/kibana/pull/188181) <!--- Backport version: 9.4.3 --> ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) <!--BACKPORT [{"author":{"name":"Carlos Crespo","email":"crespocarlos@users.noreply.github.com"},"sourceCommit":{"committedDate":"2024-07-12T13:53:53Z","message":"[Infra] Limit the number of metrics accepted by Snapshot API (#188181)\n\npart of [3628](https://github.com/elastic/observability-dev/issues/3628)\r\n- private\r\n\r\n\r\n## Summary\r\n\r\nAfter adding 20 items, users can no longer add more metrics and will see\r\nthe \"Add metric\" button disabled with a tooltip\r\n\r\n<img width=\"1713\" alt=\"image\"\r\nsrc=\"https://github.com/user-attachments/assets/c784b08b-e118-4491-b53d-46bfde898216\">\r\n\r\n\r\n### How to test\r\n\r\n- Start a local Kibana instance pointing to an oblt cluster\r\n- Navigate to Infrastructure\r\n- Try to add more than 20 metrics in the Metrics dropdown.","sha":"f2d1a8b6d24486cedb0dad97e71cd660845f353c","branchLabelMapping":{"^v8.16.0$":"main","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:skip","backport:all-open","ci:project-deploy-observability","Team:obs-ux-infra_services","v8.16.0"],"title":"[Infra] Limit the number of metrics accepted by Snapshot API","number":188181,"url":"https://github.com/elastic/kibana/pull/188181","mergeCommit":{"message":"[Infra] Limit the number of metrics accepted by Snapshot API (#188181)\n\npart of [3628](https://github.com/elastic/observability-dev/issues/3628)\r\n- private\r\n\r\n\r\n## Summary\r\n\r\nAfter adding 20 items, users can no longer add more metrics and will see\r\nthe \"Add metric\" button disabled with a tooltip\r\n\r\n<img width=\"1713\" alt=\"image\"\r\nsrc=\"https://github.com/user-attachments/assets/c784b08b-e118-4491-b53d-46bfde898216\">\r\n\r\n\r\n### How to test\r\n\r\n- Start a local Kibana instance pointing to an oblt cluster\r\n- Navigate to Infrastructure\r\n- Try to add more than 20 metrics in the Metrics dropdown.","sha":"f2d1a8b6d24486cedb0dad97e71cd660845f353c"}},"sourceBranch":"main","suggestedTargetBranches":[],"targetPullRequestStates":[{"branch":"main","label":"v8.16.0","branchLabelMappingKey":"^v8.16.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/188181","number":188181,"mergeCommit":{"message":"[Infra] Limit the number of metrics accepted by Snapshot API (#188181)\n\npart of [3628](https://github.com/elastic/observability-dev/issues/3628)\r\n- private\r\n\r\n\r\n## Summary\r\n\r\nAfter adding 20 items, users can no longer add more metrics and will see\r\nthe \"Add metric\" button disabled with a tooltip\r\n\r\n<img width=\"1713\" alt=\"image\"\r\nsrc=\"https://github.com/user-attachments/assets/c784b08b-e118-4491-b53d-46bfde898216\">\r\n\r\n\r\n### How to test\r\n\r\n- Start a local Kibana instance pointing to an oblt cluster\r\n- Navigate to Infrastructure\r\n- Try to add more than 20 metrics in the Metrics dropdown.","sha":"f2d1a8b6d24486cedb0dad97e71cd660845f353c"}}]}] BACKPORT--> Co-authored-by: Carlos Crespo <crespocarlos@users.noreply.github.com>
This commit is contained in:
parent
27685e9782
commit
3354dbaba4
9 changed files with 236 additions and 113 deletions
|
@ -42,3 +42,5 @@ export const DEFAULT_METRICS_VIEW_ATTRIBUTES = {
|
|||
name: 'Metrics View',
|
||||
timeFieldName: TIMESTAMP_FIELD,
|
||||
};
|
||||
|
||||
export const SNAPSHOT_API_MAX_METRICS = 20;
|
||||
|
|
|
@ -198,6 +198,7 @@ export const CustomMetricForm = withTheme(({ theme, onCancel, onChange, metric }
|
|||
options={fieldOptions}
|
||||
onChange={handleFieldChange}
|
||||
isClearable={false}
|
||||
data-test-subj="infraCustomMetricFieldSelect"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
|
|
@ -9,6 +9,7 @@ import { EuiPopover } from '@elastic/eui';
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import { SnapshotMetricType } from '@kbn/metrics-data-access-plugin/common';
|
||||
import { SNAPSHOT_API_MAX_METRICS } from '../../../../../../../common/constants';
|
||||
import { getCustomMetricLabel } from '../../../../../../../common/formatters/get_custom_metric_label';
|
||||
import {
|
||||
SnapshotMetricInput,
|
||||
|
@ -132,10 +133,13 @@ export const WaffleMetricControls = ({
|
|||
return null;
|
||||
}
|
||||
|
||||
const canAdd = options.length + customMetrics.length < SNAPSHOT_API_MAX_METRICS;
|
||||
|
||||
const button = (
|
||||
<DropdownButton
|
||||
onClick={handleToggle}
|
||||
label={i18n.translate('xpack.infra.waffle.metriclabel', { defaultMessage: 'Metric' })}
|
||||
data-test-subj="infraInventoryMetricDropdown"
|
||||
>
|
||||
{currentLabel}
|
||||
</DropdownButton>
|
||||
|
@ -190,6 +194,7 @@ export const WaffleMetricControls = ({
|
|||
mode={mode}
|
||||
onSave={handleSaveEdit}
|
||||
customMetrics={customMetrics}
|
||||
disableAdd={!canAdd}
|
||||
/>
|
||||
</EuiPopover>
|
||||
</>
|
||||
|
|
|
@ -70,5 +70,11 @@ export const MetricsContextMenu = ({
|
|||
},
|
||||
];
|
||||
|
||||
return <EuiContextMenu initialPanelId={0} panels={panels} />;
|
||||
return (
|
||||
<EuiContextMenu
|
||||
initialPanelId={0}
|
||||
panels={panels}
|
||||
data-test-subj="infraInventoryMetricsContextMenu"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -5,98 +5,128 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiButtonEmpty, EuiButton } from '@elastic/eui';
|
||||
import {
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiButtonEmpty,
|
||||
EuiButton,
|
||||
EuiToolTip,
|
||||
useEuiTheme,
|
||||
} from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { EuiTheme, withTheme } from '@kbn/kibana-react-plugin/common';
|
||||
import { SNAPSHOT_API_MAX_METRICS } from '../../../../../../../common/constants';
|
||||
import { CustomMetricMode } from './types';
|
||||
import { SnapshotCustomMetricInput } from '../../../../../../../common/http_api/snapshot_api';
|
||||
|
||||
interface Props {
|
||||
theme: EuiTheme | undefined;
|
||||
onEdit: () => void;
|
||||
onAdd: () => void;
|
||||
onSave: () => void;
|
||||
onEditCancel: () => void;
|
||||
mode: CustomMetricMode;
|
||||
customMetrics: SnapshotCustomMetricInput[];
|
||||
disableAdd?: boolean;
|
||||
}
|
||||
|
||||
export const ModeSwitcher = withTheme(
|
||||
({ onSave, onEditCancel, onEdit, onAdd, mode, customMetrics, theme }: Props) => {
|
||||
if (['editMetric', 'addMetric'].includes(mode)) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
borderTop: `${theme?.eui.euiBorderWidthThin} solid ${theme?.eui.euiBorderColor}`,
|
||||
padding: 12,
|
||||
}}
|
||||
>
|
||||
<EuiFlexGroup justifyContent="spaceBetween">
|
||||
{mode === 'edit' ? (
|
||||
<>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty
|
||||
data-test-subj="infraModeSwitcherCancelButton"
|
||||
size="s"
|
||||
flush="left"
|
||||
onClick={onEditCancel}
|
||||
aria-label={i18n.translate(
|
||||
'xpack.infra.waffle.customMetrics.modeSwitcher.cancelAriaLabel',
|
||||
{ defaultMessage: 'Cancel edit mode' }
|
||||
)}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.infra.waffle.customMetrics.modeSwitcher.cancel"
|
||||
defaultMessage="Cancel"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
data-test-subj="infraModeSwitcherSaveButton"
|
||||
onClick={onSave}
|
||||
size="s"
|
||||
fill
|
||||
aria-label={i18n.translate(
|
||||
'xpack.infra.waffle.customMetrics.modeSwitcher.saveButtonAriaLabel',
|
||||
{ defaultMessage: 'Save changes to custom metrics' }
|
||||
)}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.infra.waffle.customMetrics.modeSwitcher.saveButton"
|
||||
defaultMessage="Save"
|
||||
/>
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty
|
||||
data-test-subj="infraModeSwitcherEditButton"
|
||||
size="s"
|
||||
flush="left"
|
||||
onClick={onEdit}
|
||||
disabled={customMetrics.length === 0}
|
||||
aria-label={i18n.translate(
|
||||
'xpack.infra.waffle.customMetrics.modeSwitcher.editAriaLabel',
|
||||
{ defaultMessage: 'Edit custom metrics' }
|
||||
)}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.infra.waffle.customMetrics.modeSwitcher.edit"
|
||||
defaultMessage="Edit"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
export const ModeSwitcher = ({
|
||||
onSave,
|
||||
onEditCancel,
|
||||
onEdit,
|
||||
onAdd,
|
||||
mode,
|
||||
customMetrics,
|
||||
disableAdd = false,
|
||||
}: Props) => {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
|
||||
if (['editMetric', 'addMetric'].includes(mode)) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
borderTop: `${euiTheme.border.thin} solid ${euiTheme.border.color}`,
|
||||
padding: 12,
|
||||
}}
|
||||
>
|
||||
<EuiFlexGroup justifyContent="spaceBetween">
|
||||
{mode === 'edit' ? (
|
||||
<>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty
|
||||
data-test-subj="infraModeSwitcherCancelButton"
|
||||
size="s"
|
||||
flush="left"
|
||||
onClick={onEditCancel}
|
||||
aria-label={i18n.translate(
|
||||
'xpack.infra.waffle.customMetrics.modeSwitcher.cancelAriaLabel',
|
||||
{ defaultMessage: 'Cancel edit mode' }
|
||||
)}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.infra.waffle.customMetrics.modeSwitcher.cancel"
|
||||
defaultMessage="Cancel"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
data-test-subj="infraModeSwitcherSaveButton"
|
||||
onClick={onSave}
|
||||
size="s"
|
||||
fill
|
||||
aria-label={i18n.translate(
|
||||
'xpack.infra.waffle.customMetrics.modeSwitcher.saveButtonAriaLabel',
|
||||
{ defaultMessage: 'Save changes to custom metrics' }
|
||||
)}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.infra.waffle.customMetrics.modeSwitcher.saveButton"
|
||||
defaultMessage="Save"
|
||||
/>
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty
|
||||
data-test-subj="infraModeSwitcherEditButton"
|
||||
size="s"
|
||||
flush="left"
|
||||
onClick={onEdit}
|
||||
disabled={customMetrics.length === 0}
|
||||
aria-label={i18n.translate(
|
||||
'xpack.infra.waffle.customMetrics.modeSwitcher.editAriaLabel',
|
||||
{ defaultMessage: 'Edit custom metrics' }
|
||||
)}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.infra.waffle.customMetrics.modeSwitcher.edit"
|
||||
defaultMessage="Edit"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiToolTip
|
||||
content={
|
||||
disableAdd
|
||||
? i18n.translate(
|
||||
'xpack.infra.waffle.customMetrics.modeSwitcher.addDisabledTooltip',
|
||||
{
|
||||
defaultMessage: 'Maximum number of {maxMetrics} metrics reached.',
|
||||
values: { maxMetrics: SNAPSHOT_API_MAX_METRICS },
|
||||
}
|
||||
)
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<EuiButtonEmpty
|
||||
data-test-subj="infraModeSwitcherAddMetricButton"
|
||||
onClick={onAdd}
|
||||
disabled={disableAdd}
|
||||
size="s"
|
||||
flush="right"
|
||||
aria-label={i18n.translate(
|
||||
|
@ -109,11 +139,11 @@ export const ModeSwitcher = withTheme(
|
|||
defaultMessage="Add metric"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
</>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
</EuiToolTip>
|
||||
</EuiFlexItem>
|
||||
</>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -6,21 +6,18 @@
|
|||
*/
|
||||
|
||||
import Boom from '@hapi/boom';
|
||||
import { schema } from '@kbn/config-schema';
|
||||
import { pipe } from 'fp-ts/lib/pipeable';
|
||||
import { fold } from 'fp-ts/lib/Either';
|
||||
import { identity } from 'fp-ts/lib/function';
|
||||
import { createRouteValidationFunction } from '@kbn/io-ts-utils';
|
||||
import { SNAPSHOT_API_MAX_METRICS } from '../../../common/constants';
|
||||
import { InfraBackendLibs } from '../../lib/infra_types';
|
||||
import { UsageCollector } from '../../usage/usage_collector';
|
||||
import { SnapshotRequestRT, SnapshotNodeResponseRT } from '../../../common/http_api/snapshot_api';
|
||||
import { throwErrors } from '../../../common/runtime_types';
|
||||
import { createSearchClient } from '../../lib/create_search_client';
|
||||
import { getNodes } from './lib/get_nodes';
|
||||
import { LogQueryFields } from '../../lib/metrics/types';
|
||||
|
||||
const escapeHatch = schema.object({}, { unknowns: 'allow' });
|
||||
|
||||
export const initSnapshotRoute = (libs: InfraBackendLibs) => {
|
||||
const validateBody = createRouteValidationFunction(SnapshotRequestRT);
|
||||
|
||||
const { framework } = libs;
|
||||
|
||||
framework.registerRoute(
|
||||
|
@ -28,34 +25,40 @@ export const initSnapshotRoute = (libs: InfraBackendLibs) => {
|
|||
method: 'post',
|
||||
path: '/api/metrics/snapshot',
|
||||
validate: {
|
||||
body: escapeHatch,
|
||||
body: validateBody,
|
||||
},
|
||||
},
|
||||
async (requestContext, request, response) => {
|
||||
const snapshotRequest = pipe(
|
||||
SnapshotRequestRT.decode(request.body),
|
||||
fold(throwErrors(Boom.badRequest), identity)
|
||||
);
|
||||
|
||||
const soClient = (await requestContext.core).savedObjects.client;
|
||||
const source = await libs.sources.getSourceConfiguration(soClient, snapshotRequest.sourceId);
|
||||
const compositeSize = libs.configuration.inventory.compositeSize;
|
||||
const [, { logsShared }] = await libs.getStartServices();
|
||||
const logQueryFields: LogQueryFields | undefined = await logsShared.logViews
|
||||
.getScopedClient(request)
|
||||
.getResolvedLogView({
|
||||
type: 'log-view-reference',
|
||||
logViewId: snapshotRequest.sourceId,
|
||||
})
|
||||
.then(
|
||||
({ indices }) => ({ indexPattern: indices }),
|
||||
() => undefined
|
||||
);
|
||||
|
||||
UsageCollector.countNode(snapshotRequest.nodeType);
|
||||
const client = createSearchClient(requestContext, framework, request);
|
||||
const snapshotRequest = request.body;
|
||||
|
||||
try {
|
||||
if (snapshotRequest.metrics.length > SNAPSHOT_API_MAX_METRICS) {
|
||||
throw Boom.badRequest(
|
||||
`'metrics' size is greater than maximum of ${SNAPSHOT_API_MAX_METRICS} allowed.`
|
||||
);
|
||||
}
|
||||
|
||||
const soClient = (await requestContext.core).savedObjects.client;
|
||||
const source = await libs.sources.getSourceConfiguration(
|
||||
soClient,
|
||||
snapshotRequest.sourceId
|
||||
);
|
||||
const compositeSize = libs.configuration.inventory.compositeSize;
|
||||
const [, { logsShared }] = await libs.getStartServices();
|
||||
const logQueryFields: LogQueryFields | undefined = await logsShared.logViews
|
||||
.getScopedClient(request)
|
||||
.getResolvedLogView({
|
||||
type: 'log-view-reference',
|
||||
logViewId: snapshotRequest.sourceId,
|
||||
})
|
||||
.then(
|
||||
({ indices }) => ({ indexPattern: indices }),
|
||||
() => undefined
|
||||
);
|
||||
|
||||
UsageCollector.countNode(snapshotRequest.nodeType);
|
||||
const client = createSearchClient(requestContext, framework, request);
|
||||
|
||||
const snapshotResponse = await getNodes(
|
||||
client,
|
||||
snapshotRequest,
|
||||
|
|
|
@ -622,5 +622,26 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('request validation', () => {
|
||||
it('should return 400 when requesting more than 20 metrics', async () => {
|
||||
const { min, max } = DATES['8.0.0'].logs_and_metrics;
|
||||
await fetchSnapshot(
|
||||
{
|
||||
sourceId: 'default',
|
||||
timerange: {
|
||||
to: max,
|
||||
from: min,
|
||||
interval: '1m',
|
||||
},
|
||||
metrics: Array(21).fill({ type: 'cpu' }),
|
||||
nodeType: 'host',
|
||||
groupBy: [{ field: 'service.type' }],
|
||||
includeTimeseries: true,
|
||||
},
|
||||
400
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -555,6 +555,39 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should not allow adding more than 20 custom metrics', async () => {
|
||||
// open
|
||||
await pageObjects.infraHome.clickCustomMetricDropdown();
|
||||
|
||||
const fields = [
|
||||
'process.cpu.pct',
|
||||
'process.memory.pct',
|
||||
'system.core.total.pct',
|
||||
'system.core.user.pct',
|
||||
'system.core.nice.pct',
|
||||
'system.core.idle.pct',
|
||||
'system.core.iowait.pct',
|
||||
'system.core.irq.pct',
|
||||
'system.core.softirq.pct',
|
||||
'system.core.steal.pct',
|
||||
'system.cpu.nice.pct',
|
||||
'system.cpu.idle.pct',
|
||||
'system.cpu.iowait.pct',
|
||||
'system.cpu.irq.pct',
|
||||
];
|
||||
|
||||
for (const field of fields) {
|
||||
await pageObjects.infraHome.addCustomMetric(field);
|
||||
}
|
||||
const metricsCount = await pageObjects.infraHome.getMetricsContextMenuItemsCount();
|
||||
// there are 6 default metrics in the context menu for hosts
|
||||
expect(metricsCount).to.eql(20);
|
||||
|
||||
await pageObjects.infraHome.ensureCustomMetricAddButtonIsDisabled();
|
||||
// close
|
||||
await pageObjects.infraHome.clickCustomMetricDropdown();
|
||||
});
|
||||
});
|
||||
|
||||
describe('alerts flyouts', () => {
|
||||
|
|
|
@ -482,5 +482,27 @@ export function InfraHomePageProvider({ getService, getPageObjects }: FtrProvide
|
|||
async clickCloseFlyoutButton() {
|
||||
return testSubjects.click('euiFlyoutCloseButton');
|
||||
},
|
||||
|
||||
async clickCustomMetricDropdown() {
|
||||
await testSubjects.click('infraInventoryMetricDropdown');
|
||||
},
|
||||
|
||||
async addCustomMetric(field: string) {
|
||||
await testSubjects.click('infraModeSwitcherAddMetricButton');
|
||||
const groupByCustomField = await testSubjects.find('infraCustomMetricFieldSelect');
|
||||
await comboBox.setElement(groupByCustomField, field);
|
||||
await testSubjects.click('infraCustomMetricFormSaveButton');
|
||||
},
|
||||
|
||||
async getMetricsContextMenuItemsCount() {
|
||||
const contextMenu = await testSubjects.find('infraInventoryMetricsContextMenu');
|
||||
const menuItems = await contextMenu.findAllByCssSelector('button.euiContextMenuItem');
|
||||
return menuItems.length;
|
||||
},
|
||||
|
||||
async ensureCustomMetricAddButtonIsDisabled() {
|
||||
const button = await testSubjects.find('infraModeSwitcherAddMetricButton');
|
||||
expect(await button.getAttribute('disabled')).to.be('true');
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue