From 2b094777593e715aeac6fb9678eb48ccd03b5a7a Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Wed, 28 Sep 2022 09:06:15 +0100 Subject: [PATCH 01/17] [ML] Improving log pattern analysis messaging (#141130) * [ML] Improving log pattern analysis messaging * moving to separate component * translations * fixing unselection when time range changes * changes based on review --- .../log_categorization/information_text.tsx | 101 ++++++++++++++++++ .../log_categorization_page.tsx | 14 ++- 2 files changed, 114 insertions(+), 1 deletion(-) create mode 100644 x-pack/plugins/aiops/public/components/log_categorization/information_text.tsx diff --git a/x-pack/plugins/aiops/public/components/log_categorization/information_text.tsx b/x-pack/plugins/aiops/public/components/log_categorization/information_text.tsx new file mode 100644 index 0000000000000..bf80b55fee37c --- /dev/null +++ b/x-pack/plugins/aiops/public/components/log_categorization/information_text.tsx @@ -0,0 +1,101 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { FC } from 'react'; + +import { FormattedMessage } from '@kbn/i18n-react'; +import { EuiEmptyPrompt } from '@elastic/eui'; + +interface Props { + eventRateLength: number; + fieldSelected: boolean; + categoriesLength: number | null; + loading: boolean; +} + +export const InformationText: FC = ({ + eventRateLength, + fieldSelected, + categoriesLength, + loading, +}) => { + if (loading === true) { + return null; + } + return ( + <> + {eventRateLength === 0 ? ( + + + + } + titleSize="xs" + body={ +

+ +

+ } + data-test-subj="aiopsNoWindowParametersEmptyPrompt" + /> + ) : null} + + {eventRateLength > 0 && categoriesLength === null ? ( + + + + } + titleSize="xs" + body={ +

+ +

+ } + data-test-subj="aiopsNoWindowParametersEmptyPrompt" + /> + ) : null} + + {eventRateLength > 0 && categoriesLength !== null && categoriesLength === 0 ? ( + + + + } + titleSize="xs" + body={ +

+ +

+ } + data-test-subj="aiopsNoWindowParametersEmptyPrompt" + /> + ) : null} + + ); +}; diff --git a/x-pack/plugins/aiops/public/components/log_categorization/log_categorization_page.tsx b/x-pack/plugins/aiops/public/components/log_categorization/log_categorization_page.tsx index 845a4239f1104..4eb6da0e58b9f 100644 --- a/x-pack/plugins/aiops/public/components/log_categorization/log_categorization_page.tsx +++ b/x-pack/plugins/aiops/public/components/log_categorization/log_categorization_page.tsx @@ -40,6 +40,7 @@ import { useCategorizeRequest } from './use_categorize_request'; import type { EventRate, Category, SparkLinesPerCategory } from './use_categorize_request'; import { CategoryTable } from './category_table'; import { DocumentCountChart } from './document_count_chart'; +import { InformationText } from './information_text'; export interface LogCategorizationPageProps { dataView: DataView; @@ -160,6 +161,7 @@ export const LogCategorizationPage: FC = ({ docCount, })) ); + setCategories(null); setTotalCount(documentStats.totalCount); } }, [documentStats, earliest, latest, searchQueryLanguage, searchString, searchQuery]); @@ -210,6 +212,7 @@ export const LogCategorizationPage: FC = ({ ]); const onFieldChange = (value: EuiComboBoxOptionOption[] | undefined) => { + setCategories(null); setSelectedField(value && value.length ? value[0].label : undefined); }; @@ -313,8 +316,17 @@ export const LogCategorizationPage: FC = ({ ) : null} + {loading === true ? : null} - {categories !== null ? ( + + + + {selectedField !== undefined && categories !== null && categories.length > 0 ? ( Date: Wed, 28 Sep 2022 10:11:37 +0200 Subject: [PATCH 02/17] [Fleet] Fix for bulk update tags, updated API tests (#141905) * added action_status check for api tests * fix for update tags quickly * fixed test * updated tag labels * close bulk action menu when closing Add / remove tags popover * fixed test Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../components/bulk_actions.tsx | 1 + .../components/tags_add_remove.test.tsx | 53 +++++++++++++++++++ .../components/tags_add_remove.tsx | 25 ++++++--- .../fleet/server/services/agents/actions.ts | 1 + .../apis/agents/reassign.ts | 17 ++++-- .../apis/agents/unenroll.ts | 7 +++ .../apis/agents/update_agent_tags.ts | 48 +++++++++++++---- .../apis/agents/upgrade.ts | 13 +++-- 8 files changed, 142 insertions(+), 23 deletions(-) diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/bulk_actions.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/bulk_actions.tsx index bb0ec90f2b883..10ced1a5c0323 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/bulk_actions.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/bulk_actions.tsx @@ -228,6 +228,7 @@ export const AgentBulkActions: React.FunctionComponent = ({ }} onClosePopover={() => { setIsTagAddVisible(false); + closeMenu(); }} /> )} diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/tags_add_remove.test.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/tags_add_remove.test.tsx index 8c4f9f3003c82..465db5236338c 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/tags_add_remove.test.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/tags_add_remove.test.tsx @@ -270,6 +270,59 @@ describe('TagsAddRemove', () => { ); }); + it('should add new tag twice quickly when not found in search and button clicked - bulk selection', () => { + mockBulkUpdateTags.mockImplementation((agents, tagsToAdd, tagsToRemove, onSuccess) => + onSuccess(false) + ); + + const result = renderComponent(undefined, 'query'); + const searchInput = result.getByRole('combobox'); + + fireEvent.input(searchInput, { + target: { value: 'newTag' }, + }); + + fireEvent.click(result.getAllByText('Create a new tag "newTag"')[0].closest('button')!); + + fireEvent.input(searchInput, { + target: { value: 'newTag2' }, + }); + + fireEvent.click(result.getAllByText('Create a new tag "newTag2"')[0].closest('button')!); + + expect(mockBulkUpdateTags).toHaveBeenCalledWith( + 'query', + ['newTag2', 'newTag'], + [], + expect.anything(), + 'Tag created', + 'Tag creation failed' + ); + }); + + it('should remove tags twice quickly on bulk selection', () => { + selectedTags = ['tag1', 'tag2']; + mockBulkUpdateTags.mockImplementation((agents, tagsToAdd, tagsToRemove, onSuccess) => + onSuccess(false) + ); + + const result = renderComponent(undefined, ''); + const getTag = (name: string) => result.getByText(name); + + fireEvent.click(getTag('tag1')); + + fireEvent.click(getTag('tag2')); + + expect(mockBulkUpdateTags).toHaveBeenCalledWith( + '', + [], + ['tag2', 'tag1'], + expect.anything(), + undefined, + undefined + ); + }); + it('should make tag options button visible on mouse enter', async () => { const result = renderComponent('agent1'); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/tags_add_remove.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/tags_add_remove.tsx index a03ec3808e9ab..70b4da44dad68 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/tags_add_remove.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/tags_add_remove.tsx @@ -6,7 +6,7 @@ */ import React, { Fragment, useEffect, useState, useMemo, useCallback } from 'react'; -import { difference } from 'lodash'; +import { difference, uniq } from 'lodash'; import styled from 'styled-components'; import type { EuiSelectableOption } from '@elastic/eui'; import { @@ -95,8 +95,9 @@ export const TagsAddRemove: React.FC = ({ if (hasCompleted) { return onTagsUpdated(); } - const newSelectedTags = difference(selectedTags, tagsToRemove).concat(tagsToAdd); - const allTagsWithNew = allTags.includes(tagsToAdd[0]) ? allTags : allTags.concat(tagsToAdd); + const selected = labels.filter((tag) => tag.checked === 'on').map((tag) => tag.label); + const newSelectedTags = difference(selected, tagsToRemove).concat(tagsToAdd); + const allTagsWithNew = uniq(allTags.concat(newSelectedTags)); const allTagsWithRemove = isRenameOrDelete ? difference(allTagsWithNew, tagsToRemove) : allTagsWithNew; @@ -109,8 +110,8 @@ export const TagsAddRemove: React.FC = ({ successMessage?: string, errorMessage?: string ) => { - const newSelectedTags = difference(selectedTags, tagsToRemove).concat(tagsToAdd); if (agentId) { + const newSelectedTags = difference(selectedTags, tagsToRemove).concat(tagsToAdd); updateTagsHook.updateTags( agentId, newSelectedTags, @@ -119,10 +120,22 @@ export const TagsAddRemove: React.FC = ({ errorMessage ); } else { + // sending updated tags to add/remove, in case multiple actions are done quickly and the first one is not yet propagated + const updatedTagsToAdd = tagsToAdd.concat( + labels + .filter((tag) => tag.checked === 'on' && !selectedTags.includes(tag.label)) + .map((tag) => tag.label) + ); + const updatedTagsToRemove = tagsToRemove.concat( + labels + .filter((tag) => tag.checked !== 'on' && selectedTags.includes(tag.label)) + .map((tag) => tag.label) + ); + updateTagsHook.bulkUpdateTags( agents!, - tagsToAdd, - tagsToRemove, + updatedTagsToAdd, + updatedTagsToRemove, (hasCompleted) => handleTagsUpdated(tagsToAdd, tagsToRemove, hasCompleted), successMessage, errorMessage diff --git a/x-pack/plugins/fleet/server/services/agents/actions.ts b/x-pack/plugins/fleet/server/services/agents/actions.ts index 4a6c772b69b94..17c745bfd285f 100644 --- a/x-pack/plugins/fleet/server/services/agents/actions.ts +++ b/x-pack/plugins/fleet/server/services/agents/actions.ts @@ -134,6 +134,7 @@ export async function bulkCreateAgentActionResults( await esClient.bulk({ index: AGENT_ACTIONS_RESULTS_INDEX, body: bulkBody, + refresh: 'wait_for', }); } diff --git a/x-pack/test/fleet_api_integration/apis/agents/reassign.ts b/x-pack/test/fleet_api_integration/apis/agents/reassign.ts index 1283c9433ebba..2dd546511dd9a 100644 --- a/x-pack/test/fleet_api_integration/apis/agents/reassign.ts +++ b/x-pack/test/fleet_api_integration/apis/agents/reassign.ts @@ -121,9 +121,18 @@ export default function (providerContext: FtrProviderContext) { ]); expect(agent2data.body.item.policy_id).to.eql('policy2'); expect(agent3data.body.item.policy_id).to.eql('policy2'); + + const { body } = await supertest + .get(`/api/fleet/agents/action_status`) + .set('kbn-xsrf', 'xxx'); + const actionStatus = body.items[0]; + + expect(actionStatus.status).to.eql('FAILED'); + expect(actionStatus.nbAgentsActionCreated).to.eql(2); + expect(actionStatus.nbAgentsFailed).to.eql(3); }); - it('should allow to reassign multiple agents by id -- mixed invalid, hosted, etc', async () => { + it('should return error when none of the agents can be reassigned -- mixed invalid, hosted, etc', async () => { // agent1 is enrolled in policy1. set policy1 to hosted await supertest .put(`/api/fleet/agent_policies/policy1`) @@ -131,13 +140,15 @@ export default function (providerContext: FtrProviderContext) { .send({ name: 'Test policy', namespace: 'default', is_managed: true }) .expect(200); - await supertest + const { body } = await supertest .post(`/api/fleet/agents/bulk_reassign`) .set('kbn-xsrf', 'xxx') .send({ agents: ['agent2', 'INVALID_ID', 'agent3'], policy_id: 'policy2', - }); + }) + .expect(400); + expect(body.message).to.eql('No agents to reassign, already assigned or hosted agents'); const [agent2data, agent3data] = await Promise.all([ supertest.get(`/api/fleet/agents/agent2`), diff --git a/x-pack/test/fleet_api_integration/apis/agents/unenroll.ts b/x-pack/test/fleet_api_integration/apis/agents/unenroll.ts index fe31806038a5c..7f778d77f1a51 100644 --- a/x-pack/test/fleet_api_integration/apis/agents/unenroll.ts +++ b/x-pack/test/fleet_api_integration/apis/agents/unenroll.ts @@ -140,6 +140,13 @@ export default function (providerContext: FtrProviderContext) { expect(typeof agent3data.body.item.unenrollment_started_at).to.be('undefined'); expect(typeof agent3data.body.item.unenrolled_at).to.be('undefined'); expect(agent2data.body.item.active).to.eql(true); + + const { body } = await supertest + .get(`/api/fleet/agents/action_status`) + .set('kbn-xsrf', 'xxx'); + const actionStatus = body.items[0]; + expect(actionStatus.status).to.eql('FAILED'); + expect(actionStatus.nbAgentsFailed).to.eql(2); }); it('/agents/bulk_unenroll should allow to unenroll multiple agents by id from an regular agent policy', async () => { diff --git a/x-pack/test/fleet_api_integration/apis/agents/update_agent_tags.ts b/x-pack/test/fleet_api_integration/apis/agents/update_agent_tags.ts index 88079ae5e1aff..ff11076addcec 100644 --- a/x-pack/test/fleet_api_integration/apis/agents/update_agent_tags.ts +++ b/x-pack/test/fleet_api_integration/apis/agents/update_agent_tags.ts @@ -88,27 +88,46 @@ export default function (providerContext: FtrProviderContext) { }); it('should bulk update tags of multiple agents by kuery in batches', async () => { - await supertest + const { body: actionBody } = await supertest .post(`/api/fleet/agents/bulk_update_agent_tags`) .set('kbn-xsrf', 'xxx') .send({ agents: 'active: true', tagsToAdd: ['newTag'], tagsToRemove: ['existingTag'], - batchSize: 2, + batchSize: 3, }) .expect(200); + const actionId = actionBody.actionId; + + const verifyActionResult = async () => { + const { body } = await supertest.get(`/api/fleet/agents`).set('kbn-xsrf', 'xxx'); + expect(body.total).to.eql(4); + body.items.forEach((agent: any) => { + expect(agent.tags.includes('newTag')).to.be(true); + expect(agent.tags.includes('existingTag')).to.be(false); + }); + }; + await new Promise((resolve, reject) => { - setTimeout(async () => { - const { body } = await supertest.get(`/api/fleet/agents`).set('kbn-xsrf', 'xxx'); - expect(body.total).to.eql(4); - body.items.forEach((agent: any) => { - expect(agent.tags.includes('newTag')).to.be(true); - expect(agent.tags.includes('existingTag')).to.be(false); - }); - resolve({}); - }, 2000); + let attempts = 0; + const intervalId = setInterval(async () => { + if (attempts > 4) { + clearInterval(intervalId); + reject('action timed out'); + } + ++attempts; + const { + body: { items: actionStatuses }, + } = await supertest.get(`/api/fleet/agents/action_status`).set('kbn-xsrf', 'xxx'); + const action = actionStatuses.find((a: any) => a.actionId === actionId); + if (action && action.nbAgentsAck === 4) { + clearInterval(intervalId); + await verifyActionResult(); + resolve({}); + } + }, 1000); }).catch((e) => { throw e; }); @@ -156,6 +175,13 @@ export default function (providerContext: FtrProviderContext) { expect(agent1data.body.item.tags.includes('newTag')).to.be(false); expect(agent2data.body.item.tags.includes('newTag')).to.be(true); + + const { body } = await supertest + .get(`/api/fleet/agents/action_status`) + .set('kbn-xsrf', 'xxx'); + const actionStatus = body.items[0]; + expect(actionStatus.status).to.eql('FAILED'); + expect(actionStatus.nbAgentsFailed).to.eql(1); }); }); }); diff --git a/x-pack/test/fleet_api_integration/apis/agents/upgrade.ts b/x-pack/test/fleet_api_integration/apis/agents/upgrade.ts index e2762e21f33ad..e8dad8624021f 100644 --- a/x-pack/test/fleet_api_integration/apis/agents/upgrade.ts +++ b/x-pack/test/fleet_api_integration/apis/agents/upgrade.ts @@ -608,7 +608,7 @@ export default function (providerContext: FtrProviderContext) { .send({ agents: 'active:true', version: fleetServerVersion, - batchSize: 2, + batchSize: 3, }) .expect(200); @@ -626,7 +626,7 @@ export default function (providerContext: FtrProviderContext) { await new Promise((resolve, reject) => { let attempts = 0; const intervalId = setInterval(async () => { - if (attempts > 2) { + if (attempts > 4) { clearInterval(intervalId); reject('action timed out'); } @@ -636,7 +636,7 @@ export default function (providerContext: FtrProviderContext) { } = await supertest.get(`/api/fleet/agents/action_status`).set('kbn-xsrf', 'xxx'); const action = actionStatuses.find((a: any) => a.actionId === actionId); // 2 upgradeable - if (action && action.nbAgentsActionCreated === 2) { + if (action && action.nbAgentsActionCreated === 2 && action.nbAgentsFailed === 3) { clearInterval(intervalId); await verifyActionResult(); resolve({}); @@ -1032,6 +1032,13 @@ export default function (providerContext: FtrProviderContext) { expect(typeof agent1data.body.item.upgrade_started_at).to.be('undefined'); expect(typeof agent2data.body.item.upgrade_started_at).to.be('string'); + + const { body } = await supertest + .get(`/api/fleet/agents/action_status`) + .set('kbn-xsrf', 'xxx'); + const actionStatus = body.items[0]; + expect(actionStatus.status).to.eql('FAILED'); + expect(actionStatus.nbAgentsFailed).to.eql(1); }); it('enrolled in a hosted agent policy bulk upgrade with force flag should respond with 200 and update the agent SOs', async () => { From 7d69aae2696696342382b5f66267826dd9967cb6 Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Wed, 28 Sep 2022 09:54:33 +0100 Subject: [PATCH 03/17] [ML] Fixing case of log pattern analysis title (#142034) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../plugins/ml/public/application/aiops/log_categorization.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/ml/public/application/aiops/log_categorization.tsx b/x-pack/plugins/ml/public/application/aiops/log_categorization.tsx index e1d816d61357a..899006b5918dd 100644 --- a/x-pack/plugins/ml/public/application/aiops/log_categorization.tsx +++ b/x-pack/plugins/ml/public/application/aiops/log_categorization.tsx @@ -34,7 +34,7 @@ export const LogCategorizationPage: FC = () => { From 8f793cb83c167da32d696715e5b1f1a65f8c1c6c Mon Sep 17 00:00:00 2001 From: Uladzislau Lasitsa Date: Wed, 28 Sep 2022 12:39:26 +0300 Subject: [PATCH 04/17] [TSVB][Lens] Navigate to Lens TSVB Metric (#140878) * Added tsvb metric converting to Lens. * Allowed to convert to lens with invalid color rules. * Fixed static value converting. * Fixed tests. * Added unit tests for getPalette. * Added functional tests for converting to Lens. Co-authored-by: Yaroslav Kuznietsov Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../public/convert_to_lens/index.test.ts | 2 +- .../public/convert_to_lens/index.ts | 4 + .../lib/configurations/metric/index.ts | 67 +++++ .../lib/configurations/metric/palette.test.ts | 177 ++++++++++++ .../lib/configurations/metric/palette.ts | 214 ++++++++++++++ .../convert_to_lens/lib/convert/index.ts | 2 +- .../lib/convert/static_value.ts | 22 +- .../convert_to_lens/lib/convert/types.ts | 2 +- .../lib/metrics/supported_metrics.ts | 7 +- .../lib/series/metrics_columns.test.ts | 14 +- .../lib/series/metrics_columns.ts | 16 +- .../convert_to_lens/metric/index.test.ts | 272 ++++++++++++++++++ .../public/convert_to_lens/metric/index.ts | 130 +++++++++ .../convert_to_lens/metric/utils.test.ts | 104 +++++++ .../public/convert_to_lens/metric/utils.ts | 65 +++++ .../convert_to_lens/timeseries/index.ts | 4 +- .../public/convert_to_lens/top_n/index.ts | 4 +- .../public/convert_to_lens/types.ts | 8 +- .../convert_to_lens/types/configurations.ts | 24 +- .../visualizations/metric/suggestions.test.ts | 8 + .../visualizations/metric/suggestions.ts | 2 +- .../visualizations/metric/visualization.tsx | 40 ++- .../apps/lens/group3/tsvb_open_in_lens.ts | 13 +- 23 files changed, 1179 insertions(+), 22 deletions(-) create mode 100644 src/plugins/vis_types/timeseries/public/convert_to_lens/lib/configurations/metric/index.ts create mode 100644 src/plugins/vis_types/timeseries/public/convert_to_lens/lib/configurations/metric/palette.test.ts create mode 100644 src/plugins/vis_types/timeseries/public/convert_to_lens/lib/configurations/metric/palette.ts create mode 100644 src/plugins/vis_types/timeseries/public/convert_to_lens/metric/index.test.ts create mode 100644 src/plugins/vis_types/timeseries/public/convert_to_lens/metric/index.ts create mode 100644 src/plugins/vis_types/timeseries/public/convert_to_lens/metric/utils.test.ts create mode 100644 src/plugins/vis_types/timeseries/public/convert_to_lens/metric/utils.ts diff --git a/src/plugins/vis_types/timeseries/public/convert_to_lens/index.test.ts b/src/plugins/vis_types/timeseries/public/convert_to_lens/index.test.ts index 435335fe9dd25..309f066b18f29 100644 --- a/src/plugins/vis_types/timeseries/public/convert_to_lens/index.test.ts +++ b/src/plugins/vis_types/timeseries/public/convert_to_lens/index.test.ts @@ -40,7 +40,7 @@ describe('convertTSVBtoLensConfiguration', () => { test('should return null for a not supported chart', async () => { const metricModel = { ...model, - type: 'metric', + type: 'markdown', } as Panel; const triggerOptions = await convertTSVBtoLensConfiguration(metricModel); expect(triggerOptions).toBeNull(); diff --git a/src/plugins/vis_types/timeseries/public/convert_to_lens/index.ts b/src/plugins/vis_types/timeseries/public/convert_to_lens/index.ts index 5b92c0ab21668..a64118a1cb507 100644 --- a/src/plugins/vis_types/timeseries/public/convert_to_lens/index.ts +++ b/src/plugins/vis_types/timeseries/public/convert_to_lens/index.ts @@ -21,6 +21,10 @@ const getConvertFnByType = (type: PANEL_TYPES) => { const { convertToLens } = await import('./top_n'); return convertToLens; }, + [PANEL_TYPES.METRIC]: async () => { + const { convertToLens } = await import('./metric'); + return convertToLens; + }, }; return convertionFns[type]?.(); diff --git a/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/configurations/metric/index.ts b/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/configurations/metric/index.ts new file mode 100644 index 0000000000000..d1f24485d7646 --- /dev/null +++ b/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/configurations/metric/index.ts @@ -0,0 +1,67 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { MetricVisConfiguration } from '@kbn/visualizations-plugin/common'; +import { Metric, Panel, Series } from '../../../../../common/types'; +import { Column, Layer } from '../../convert'; +import { getSeriesAgg } from '../../series'; +import { getPalette } from './palette'; + +const getMetricWithCollapseFn = (series: Series | undefined) => { + if (!series) { + return; + } + const { metrics, seriesAgg } = getSeriesAgg(series.metrics); + const visibleMetric = metrics[metrics.length - 1]; + return { metric: visibleMetric, collapseFn: seriesAgg }; +}; + +const findMetricColumn = (metric: Metric | undefined, columns: Column[]) => { + if (!metric) { + return; + } + + return columns.find((column) => 'meta' in column && column.meta.metricId === metric.id); +}; + +export const getConfigurationForMetric = ( + model: Panel, + layer: Layer, + bucket?: Column +): MetricVisConfiguration | null => { + const [primarySeries, secondarySeries] = model.series.filter(({ hidden }) => !hidden); + + const primaryMetricWithCollapseFn = getMetricWithCollapseFn(primarySeries); + + if (!primaryMetricWithCollapseFn || !primaryMetricWithCollapseFn.metric) { + return null; + } + + const secondaryMetricWithCollapseFn = getMetricWithCollapseFn(secondarySeries); + const primaryColumn = findMetricColumn(primaryMetricWithCollapseFn.metric, layer.columns); + const secondaryColumn = findMetricColumn(secondaryMetricWithCollapseFn?.metric, layer.columns); + + if (primaryMetricWithCollapseFn.collapseFn && secondaryMetricWithCollapseFn?.collapseFn) { + return null; + } + + const palette = getPalette(model.background_color_rules ?? []); + if (palette === null) { + return null; + } + + return { + layerId: layer.layerId, + layerType: 'data', + metricAccessor: primaryColumn?.columnId, + secondaryMetricAccessor: secondaryColumn?.columnId, + breakdownByAccessor: bucket?.columnId, + palette, + collapseFn: primaryMetricWithCollapseFn.collapseFn ?? secondaryMetricWithCollapseFn?.collapseFn, + }; +}; diff --git a/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/configurations/metric/palette.test.ts b/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/configurations/metric/palette.test.ts new file mode 100644 index 0000000000000..827dc15ff171b --- /dev/null +++ b/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/configurations/metric/palette.test.ts @@ -0,0 +1,177 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { getPalette } from './palette'; + +describe('getPalette', () => { + const invalidRules = [ + { id: 'some-id-0' }, + { id: 'some-id-1', value: 10 }, + { id: 'some-id-2', operator: 'gte' }, + { id: 'some-id-3', color: '#000' }, + { id: 'some-id-4', background_color: '#000' }, + ]; + test('should return undefined if no filled rules was provided', () => { + expect(getPalette([])).toBeUndefined(); + expect(getPalette(invalidRules)).toBeUndefined(); + }); + + test('should return undefined if only one valid rule is provided and it is not lte', () => { + expect(getPalette([])).toBeUndefined(); + expect( + getPalette([ + ...invalidRules, + { id: 'some-id-5', operator: 'gt', value: 100, background_color: '#000' }, + ]) + ).toBeUndefined(); + }); + + test('should return custom palette if only one valid rule is provided and it is lte', () => { + expect(getPalette([])).toBeUndefined(); + expect( + getPalette([ + ...invalidRules, + { id: 'some-id-5', operator: 'lte', value: 100, background_color: '#000' }, + ]) + ).toEqual({ + name: 'custom', + params: { + colorStops: [{ color: '#000000', stop: 100 }], + continuity: 'below', + maxSteps: 5, + name: 'custom', + progression: 'fixed', + rangeMax: 100, + rangeMin: -Infinity, + rangeType: 'number', + reverse: false, + steps: 1, + stops: [{ color: '#000000', stop: 100 }], + }, + type: 'palette', + }); + }); + + test('should return undefined if more than two types of rules', () => { + expect(getPalette([])).toBeUndefined(); + expect( + getPalette([ + ...invalidRules, + { id: 'some-id-5', operator: 'lte', value: 100, background_color: '#000' }, + { id: 'some-id-6', operator: 'gte', value: 150, background_color: '#000' }, + { id: 'some-id-7', operator: 'lt', value: 200, background_color: '#000' }, + ]) + ).toBeUndefined(); + }); + + test('should return undefined if two types of rules and last rule is not lte', () => { + expect(getPalette([])).toBeUndefined(); + expect( + getPalette([ + ...invalidRules, + { id: 'some-id-5', operator: 'gte', value: 100, background_color: '#000' }, + { id: 'some-id-7', operator: 'lt', value: 200, background_color: '#000' }, + { id: 'some-id-6', operator: 'gte', value: 150, background_color: '#000' }, + ]) + ).toBeUndefined(); + }); + + test('should return undefined if all rules are lte', () => { + expect(getPalette([])).toBeUndefined(); + expect( + getPalette([ + ...invalidRules, + { id: 'some-id-5', operator: 'lte', value: 100, background_color: '#000' }, + { id: 'some-id-7', operator: 'lte', value: 200, background_color: '#000' }, + { id: 'some-id-6', operator: 'lte', value: 150, background_color: '#000' }, + ]) + ).toBeUndefined(); + }); + + test('should return undefined if two types of rules and all except last one are lt and last one is not lte', () => { + expect(getPalette([])).toBeUndefined(); + expect( + getPalette([ + ...invalidRules, + { id: 'some-id-5', operator: 'lt', value: 100, background_color: '#000' }, + { id: 'some-id-7', operator: 'gte', value: 200, background_color: '#000' }, + { id: 'some-id-6', operator: 'lt', value: 150, background_color: '#000' }, + ]) + ).toBeUndefined(); + }); + + test('should return custom palette if two types of rules and all except last one is lt and last one is lte', () => { + expect(getPalette([])).toBeUndefined(); + expect( + getPalette([ + ...invalidRules, + { id: 'some-id-5', operator: 'lt', value: 100, background_color: '#000' }, + { id: 'some-id-7', operator: 'lte', value: 200, background_color: '#000' }, + { id: 'some-id-6', operator: 'lt', value: 150, background_color: '#000' }, + ]) + ).toEqual({ + name: 'custom', + params: { + colorStops: [ + { color: '#000000', stop: -Infinity }, + { color: '#000000', stop: 100 }, + { color: '#000000', stop: 150 }, + ], + continuity: 'below', + maxSteps: 5, + name: 'custom', + progression: 'fixed', + rangeMax: 200, + rangeMin: -Infinity, + rangeType: 'number', + reverse: false, + steps: 4, + stops: [ + { color: '#000000', stop: 100 }, + { color: '#000000', stop: 150 }, + { color: '#000000', stop: 200 }, + ], + }, + type: 'palette', + }); + }); + + test('should return custom palette if last one is lte and all previous are gte', () => { + expect(getPalette([])).toBeUndefined(); + expect( + getPalette([ + ...invalidRules, + { id: 'some-id-5', operator: 'gte', value: 100, background_color: '#000' }, + { id: 'some-id-7', operator: 'lte', value: 200, background_color: '#000' }, + { id: 'some-id-6', operator: 'gte', value: 150, background_color: '#000' }, + ]) + ).toEqual({ + name: 'custom', + params: { + colorStops: [ + { color: '#000000', stop: 100 }, + { color: '#000000', stop: 150 }, + ], + continuity: 'none', + maxSteps: 5, + name: 'custom', + progression: 'fixed', + rangeMax: 200, + rangeMin: 100, + rangeType: 'number', + reverse: false, + steps: 2, + stops: [ + { color: '#000000', stop: 150 }, + { color: '#000000', stop: 200 }, + ], + }, + type: 'palette', + }); + }); +}); diff --git a/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/configurations/metric/palette.ts b/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/configurations/metric/palette.ts new file mode 100644 index 0000000000000..55741c57595e7 --- /dev/null +++ b/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/configurations/metric/palette.ts @@ -0,0 +1,214 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import color from 'color'; +import { ColorStop, CustomPaletteParams, PaletteOutput } from '@kbn/coloring'; +import { uniqBy } from 'lodash'; +import { Panel } from '../../../../../common/types'; + +const Operators = { + GTE: 'gte', + GT: 'gt', + LTE: 'lte', + LT: 'lt', +} as const; + +type ColorStopsWithMinMax = Pick< + CustomPaletteParams, + 'colorStops' | 'stops' | 'steps' | 'rangeMax' | 'rangeMin' | 'continuity' +>; + +const getColorStopsWithMinMaxForAllGteOrWithLte = ( + rules: Exclude, + tailOperator: string +): ColorStopsWithMinMax => { + const lastRule = rules[rules.length - 1]; + const lastRuleColor = (lastRule.background_color ?? lastRule.color)!; + + const colorStops = rules.reduce((colors, rule, index, rulesArr) => { + const rgbColor = (rule.background_color ?? rule.color)!; + if (index === rulesArr.length - 1 && tailOperator === Operators.LTE) { + return colors; + } + // if last operation is LTE, color of gte should be replaced by lte + if (index === rulesArr.length - 2 && tailOperator === Operators.LTE) { + return [ + ...colors, + { + color: color(lastRuleColor).hex(), + stop: rule.value!, + }, + ]; + } + return [ + ...colors, + { + color: color(rgbColor).hex(), + stop: rule.value!, + }, + ]; + }, []); + + const stops = colorStops.reduce((prevStops, colorStop, index, colorStopsArr) => { + if (index === colorStopsArr.length - 1) { + return [ + ...prevStops, + { + color: colorStop.color, + stop: tailOperator === Operators.LTE ? lastRule.value! : colorStop.stop + 1, + }, + ]; + } + return [...prevStops, { color: colorStop.color, stop: colorStopsArr[index + 1].stop }]; + }, []); + + const [rule] = rules; + return { + rangeMin: rule.value, + rangeMax: tailOperator === Operators.LTE ? lastRule.value : Infinity, + colorStops, + stops, + steps: colorStops.length, + continuity: tailOperator === Operators.LTE ? 'none' : 'above', + }; +}; + +const getColorStopsWithMinMaxForLtWithLte = ( + rules: Exclude +): ColorStopsWithMinMax => { + const lastRule = rules[rules.length - 1]; + const colorStops = rules.reduce((colors, rule, index, rulesArr) => { + if (index === 0) { + return [{ color: color((rule.background_color ?? rule.color)!).hex(), stop: -Infinity }]; + } + const rgbColor = (rule.background_color ?? rule.color)!; + return [ + ...colors, + { + color: color(rgbColor).hex(), + stop: rulesArr[index - 1].value!, + }, + ]; + }, []); + + const stops = colorStops.reduce((prevStops, colorStop, index, colorStopsArr) => { + if (index === colorStopsArr.length - 1) { + return [ + ...prevStops, + { + color: colorStop.color, + stop: lastRule.value!, + }, + ]; + } + return [...prevStops, { color: colorStop.color, stop: colorStopsArr[index + 1].stop }]; + }, []); + + return { + rangeMin: -Infinity, + rangeMax: lastRule.value, + colorStops, + stops, + steps: colorStops.length + 1, + continuity: 'below', + }; +}; + +const getColorStopWithMinMaxForLte = ( + rule: Exclude[number] +): ColorStopsWithMinMax => { + const colorStop = { + color: color((rule.background_color ?? rule.color)!).hex(), + stop: rule.value!, + }; + return { + rangeMin: -Infinity, + rangeMax: rule.value!, + colorStops: [colorStop], + stops: [colorStop], + steps: 1, + continuity: 'below', + }; +}; + +const getCustomPalette = ( + colorStopsWithMinMax: ColorStopsWithMinMax +): PaletteOutput => { + return { + name: 'custom', + params: { + continuity: 'all', + maxSteps: 5, + name: 'custom', + progression: 'fixed', + rangeMax: Infinity, + rangeMin: -Infinity, + rangeType: 'number', + reverse: false, + ...colorStopsWithMinMax, + }, + type: 'palette', + }; +}; + +export const getPalette = ( + rules: Exclude +): PaletteOutput | null | undefined => { + const validRules = + rules.filter( + ({ operator, color: textColor, value, background_color: bColor }) => + operator && (bColor ?? textColor) && value !== undefined + ) ?? []; + + validRules.sort((rule1, rule2) => { + return rule1.value! - rule2.value!; + }); + + const kindOfRules = uniqBy(validRules, 'operator'); + + if (!kindOfRules.length) { + return; + } + + // lnsMetric is supporting lte only, if one rule is defined + if (validRules.length === 1) { + if (validRules[0].operator !== Operators.LTE) { + return; + } + return getCustomPalette(getColorStopWithMinMaxForLte(validRules[0])); + } + + const headRules = validRules.slice(0, -1); + const tailRule = validRules[validRules.length - 1]; + const kindOfHeadRules = uniqBy(headRules, 'operator'); + + if ( + kindOfHeadRules.length > 1 || + (kindOfHeadRules[0].operator !== tailRule.operator && tailRule.operator !== Operators.LTE) + ) { + return; + } + + const [rule] = kindOfHeadRules; + + if (rule.operator === Operators.LTE) { + return; + } + + if (rule.operator === Operators.LT) { + if (tailRule.operator !== Operators.LTE) { + return; + } + return getCustomPalette(getColorStopsWithMinMaxForLtWithLte(validRules)); + } + + if (rule.operator === Operators.GTE) { + return getCustomPalette( + getColorStopsWithMinMaxForAllGteOrWithLte(validRules, tailRule.operator!) + ); + } +}; diff --git a/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/convert/index.ts b/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/convert/index.ts index 36f05c440bdc2..e03701e6ea153 100644 --- a/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/convert/index.ts +++ b/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/convert/index.ts @@ -17,7 +17,7 @@ export { export { convertToCumulativeSumColumns } from './cumulative_sum'; export { convertFilterRatioToFormulaColumn } from './filter_ratio'; export { convertToLastValueColumn } from './last_value'; -export { convertToStaticValueColumn } from './static_value'; +export { convertToStaticValueColumn, convertStaticValueToFormulaColumn } from './static_value'; export { convertToFiltersColumn } from './filters'; export { convertToDateHistogramColumn } from './date_histogram'; export { convertToTermsColumn } from './terms'; diff --git a/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/convert/static_value.ts b/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/convert/static_value.ts index c4400f72b289b..e03a9d7821364 100644 --- a/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/convert/static_value.ts +++ b/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/convert/static_value.ts @@ -7,9 +7,10 @@ */ import { StaticValueParams } from '@kbn/visualizations-plugin/common/convert_to_lens'; -import { CommonColumnsConverterArgs, StaticValueColumn } from './types'; +import { CommonColumnsConverterArgs, FormulaColumn, StaticValueColumn } from './types'; import type { Metric } from '../../../../common/types'; import { createColumn, getFormat } from './column'; +import { createFormulaColumn } from './formula'; export const convertToStaticValueParams = ({ value }: Metric): StaticValueParams => ({ value, @@ -37,3 +38,22 @@ export const convertToStaticValueColumn = ( }, }; }; + +export const convertStaticValueToFormulaColumn = ( + { series, metrics, dataView }: CommonColumnsConverterArgs, + { + visibleSeriesCount = 0, + reducedTimeRange, + }: { visibleSeriesCount?: number; reducedTimeRange?: string } = {} +): FormulaColumn | null => { + // Lens support reference lines only when at least one layer data exists + if (visibleSeriesCount === 1) { + return null; + } + const currentMetric = metrics[metrics.length - 1]; + return createFormulaColumn(currentMetric.value ?? '', { + series, + metric: currentMetric, + dataView, + }); +}; diff --git a/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/convert/types.ts b/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/convert/types.ts index d21291dc2622a..e5b862a0fe70f 100644 --- a/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/convert/types.ts +++ b/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/convert/types.ts @@ -85,7 +85,7 @@ export type MovingAverageColumn = GenericColumnWithMeta; export type StaticValueColumn = GenericColumnWithMeta; -type ColumnsWithoutMeta = FiltersColumn | TermsColumn | DateHistogramColumn; +export type ColumnsWithoutMeta = FiltersColumn | TermsColumn | DateHistogramColumn; export type AnyColumnWithReferences = GenericColumnWithMeta; type CommonColumns = Exclude; diff --git a/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/metrics/supported_metrics.ts b/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/metrics/supported_metrics.ts index 933e7e344b7fd..76d15793f4516 100644 --- a/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/metrics/supported_metrics.ts +++ b/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/metrics/supported_metrics.ts @@ -62,7 +62,12 @@ export type SupportedMetrics = LocalSupportedMetrics & { [Key in UnsupportedSupportedMetrics]?: null; }; -const supportedPanelTypes: readonly PANEL_TYPES[] = [PANEL_TYPES.TIMESERIES, PANEL_TYPES.TOP_N]; +const supportedPanelTypes: readonly PANEL_TYPES[] = [ + PANEL_TYPES.TIMESERIES, + PANEL_TYPES.TOP_N, + PANEL_TYPES.METRIC, +]; + const supportedTimeRangeModes: readonly TIME_RANGE_DATA_MODES[] = [ TIME_RANGE_DATA_MODES.ENTIRE_TIME_RANGE, TIME_RANGE_DATA_MODES.LAST_VALUE, diff --git a/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/series/metrics_columns.test.ts b/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/series/metrics_columns.test.ts index 5ca1fe71a0ada..4461072c8df62 100644 --- a/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/series/metrics_columns.test.ts +++ b/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/series/metrics_columns.test.ts @@ -20,6 +20,7 @@ const mockConvertToCounterRateColumn = jest.fn(); const mockConvertOtherAggsToFormulaColumn = jest.fn(); const mockConvertToLastValueColumn = jest.fn(); const mockConvertToStaticValueColumn = jest.fn(); +const mockConvertStaticValueToFormulaColumn = jest.fn(); const mockConvertToStandartDeviationColumn = jest.fn(); const mockConvertMetricAggregationColumnWithoutSpecialParams = jest.fn(); @@ -32,6 +33,7 @@ jest.mock('../convert', () => ({ convertOtherAggsToFormulaColumn: jest.fn(() => mockConvertOtherAggsToFormulaColumn()), convertToLastValueColumn: jest.fn(() => mockConvertToLastValueColumn()), convertToStaticValueColumn: jest.fn(() => mockConvertToStaticValueColumn()), + convertStaticValueToFormulaColumn: jest.fn(() => mockConvertStaticValueToFormulaColumn()), convertToStandartDeviationColumn: jest.fn(() => mockConvertToStandartDeviationColumn()), convertMetricAggregationColumnWithoutSpecialParams: jest.fn(() => mockConvertMetricAggregationColumnWithoutSpecialParams() @@ -138,8 +140,18 @@ describe('getMetricsColumns', () => { mockConvertToLastValueColumn, ], [ - 'call convertToStaticValueColumn if metric type is static', + 'call convertStaticValueToFormulaColumn if metric type is static', [createSeries({ metrics: [{ type: TSVB_METRIC_TYPES.STATIC, id: '1' }] }), dataView, 1], + mockConvertStaticValueToFormulaColumn, + ], + [ + 'call convertToStaticValueColumn if metric type is static and isStaticValueColumnSupported is true', + [ + createSeries({ metrics: [{ type: TSVB_METRIC_TYPES.STATIC, id: '1' }] }), + dataView, + 1, + { isStaticValueColumnSupported: true }, + ], mockConvertToStaticValueColumn, ], [ diff --git a/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/series/metrics_columns.ts b/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/series/metrics_columns.ts index 07294a3a61aaa..8f7d4ded0d076 100644 --- a/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/series/metrics_columns.ts +++ b/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/series/metrics_columns.ts @@ -21,6 +21,7 @@ import { convertFilterRatioToFormulaColumn, convertToLastValueColumn, convertToStaticValueColumn, + convertStaticValueToFormulaColumn, convertMetricAggregationColumnWithoutSpecialParams, convertToCounterRateColumn, convertToStandartDeviationColumn, @@ -31,7 +32,10 @@ export const getMetricsColumns = ( series: Series, dataView: DataView, visibleSeriesCount: number, - reducedTimeRange?: string + { + isStaticValueColumnSupported = false, + reducedTimeRange, + }: { reducedTimeRange?: string; isStaticValueColumnSupported?: boolean } = {} ): Column[] | null => { const { metrics: validMetrics, seriesAgg } = getSeriesAgg( series.metrics as [Metric, ...Metric[]] @@ -117,10 +121,12 @@ export const getMetricsColumns = ( return getValidColumns(column); } case 'static': { - const column = convertToStaticValueColumn(columnsConverterArgs, { - visibleSeriesCount, - reducedTimeRange, - }); + const column = isStaticValueColumnSupported + ? convertToStaticValueColumn(columnsConverterArgs, { + visibleSeriesCount, + reducedTimeRange, + }) + : convertStaticValueToFormulaColumn(columnsConverterArgs); return getValidColumns(column); } case 'std_deviation': { diff --git a/src/plugins/vis_types/timeseries/public/convert_to_lens/metric/index.test.ts b/src/plugins/vis_types/timeseries/public/convert_to_lens/metric/index.test.ts new file mode 100644 index 0000000000000..9407599573d9d --- /dev/null +++ b/src/plugins/vis_types/timeseries/public/convert_to_lens/metric/index.test.ts @@ -0,0 +1,272 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { METRIC_TYPES } from '@kbn/data-plugin/public'; +import { stubLogstashDataView } from '@kbn/data-views-plugin/common/data_view.stub'; +import { convertToLens } from '.'; +import { createPanel, createSeries } from '../lib/__mocks__'; + +const mockGetMetricsColumns = jest.fn(); +const mockGetBucketsColumns = jest.fn(); +const mockGetConfigurationForMetric = jest.fn(); +const mockIsValidMetrics = jest.fn(); +const mockGetDatasourceValue = jest + .fn() + .mockImplementation(() => Promise.resolve(stubLogstashDataView)); +const mockGetDataSourceInfo = jest.fn(); + +jest.mock('../../services', () => ({ + getDataViewsStart: jest.fn(() => mockGetDatasourceValue), +})); + +jest.mock('../lib/series', () => ({ + getMetricsColumns: jest.fn(() => mockGetMetricsColumns()), + getBucketsColumns: jest.fn(() => mockGetBucketsColumns()), +})); + +jest.mock('../lib/configurations/metric', () => ({ + getConfigurationForMetric: jest.fn(() => mockGetConfigurationForMetric()), +})); + +jest.mock('../lib/metrics', () => ({ + isValidMetrics: jest.fn(() => mockIsValidMetrics()), + getReducedTimeRange: jest.fn().mockReturnValue('10'), +})); + +jest.mock('../lib/datasource', () => ({ + getDataSourceInfo: jest.fn(() => mockGetDataSourceInfo()), +})); + +describe('convertToLens', () => { + const model = createPanel({ + series: [ + createSeries({ + metrics: [ + { id: 'some-id', type: METRIC_TYPES.AVG, field: 'test-field' }, + { id: 'some-id-1', type: METRIC_TYPES.COUNT }, + ], + }), + ], + }); + + const bucket = { + isBucketed: true, + isSplit: true, + operationType: 'terms', + params: { + exclude: [], + excludeIsRegex: true, + include: [], + includeIsRegex: true, + orderAgg: { + columnId: 'some-id-0', + dataType: 'number', + isBucketed: true, + isSplit: false, + operationType: 'average', + params: {}, + sourceField: 'bytes', + }, + orderBy: { columnId: 'some-id-0', type: 'column' }, + orderDirection: 'asc', + otherBucket: false, + parentFormat: { id: 'terms' }, + secondaryFields: [], + size: 3, + }, + sourceField: 'bytes', + }; + + const bucket2 = { + isBucketed: true, + isSplit: true, + operationType: 'terms', + params: { + exclude: [], + excludeIsRegex: true, + include: [], + includeIsRegex: true, + orderAgg: { + columnId: 'some-id-1', + dataType: 'number', + isBucketed: true, + isSplit: false, + operationType: 'average', + params: {}, + sourceField: 'bytes', + }, + orderBy: { columnId: 'some-id-1', type: 'column' }, + orderDirection: 'desc', + otherBucket: false, + parentFormat: { id: 'terms' }, + secondaryFields: [], + size: 10, + }, + sourceField: 'bytes', + }; + + const metric = { + meta: { metricId: 'some-id-0' }, + operationType: 'last_value', + params: { showArrayValues: false, sortField: '@timestamp' }, + reducedTimeRange: '10m', + }; + + beforeEach(() => { + mockIsValidMetrics.mockReturnValue(true); + mockGetDataSourceInfo.mockReturnValue({ + indexPatternId: 'test-index-pattern', + timeField: 'timeField', + indexPattern: { id: 'test-index-pattern' }, + }); + mockGetMetricsColumns.mockReturnValue([{}]); + mockGetBucketsColumns.mockReturnValue([{}]); + mockGetConfigurationForMetric.mockReturnValue({}); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + test('should return null for invalid metrics', async () => { + mockIsValidMetrics.mockReturnValue(null); + const result = await convertToLens(model); + expect(result).toBeNull(); + expect(mockIsValidMetrics).toBeCalledTimes(1); + }); + + test('should return null for invalid or unsupported metrics', async () => { + mockGetMetricsColumns.mockReturnValue(null); + const result = await convertToLens(model); + expect(result).toBeNull(); + expect(mockGetMetricsColumns).toBeCalledTimes(1); + }); + + test('should return null for invalid or unsupported buckets', async () => { + mockGetBucketsColumns.mockReturnValue(null); + const result = await convertToLens(model); + expect(result).toBeNull(); + expect(mockGetBucketsColumns).toBeCalledTimes(1); + }); + + test('should return state for valid model', async () => { + const result = await convertToLens(model); + expect(result).toBeDefined(); + expect(result?.type).toBe('lnsMetric'); + expect(mockGetBucketsColumns).toBeCalledTimes(model.series.length); + expect(mockGetConfigurationForMetric).toBeCalledTimes(1); + }); + + test('should skip hidden series', async () => { + const result = await convertToLens( + createPanel({ + series: [ + createSeries({ + metrics: [{ id: 'some-id', type: METRIC_TYPES.AVG, field: 'test-field' }], + hidden: true, + }), + ], + }) + ); + expect(result).toBeDefined(); + expect(result?.type).toBe('lnsMetric'); + expect(mockIsValidMetrics).toBeCalledTimes(0); + }); + + test('should return null if multiple indexPatterns are provided', async () => { + mockGetDataSourceInfo.mockReturnValueOnce({ + indexPatternId: 'test-index-pattern-1', + timeField: 'timeField', + indexPattern: { id: 'test-index-pattern-1' }, + }); + + const result = await convertToLens( + createPanel({ + series: [ + createSeries({ + metrics: [{ id: 'some-id', type: METRIC_TYPES.AVG, field: 'test-field' }], + hidden: false, + }), + createSeries({ + metrics: [{ id: 'some-id', type: METRIC_TYPES.AVG, field: 'test-field' }], + hidden: false, + }), + ], + }) + ); + expect(result).toBeNull(); + }); + + test('should return null if visible series is 2 and bucket is 1', async () => { + mockGetBucketsColumns.mockReturnValueOnce([bucket]); + mockGetBucketsColumns.mockReturnValueOnce([]); + mockGetMetricsColumns.mockReturnValueOnce([metric]); + + const result = await convertToLens( + createPanel({ + series: [ + createSeries({ + metrics: [{ id: 'some-id', type: METRIC_TYPES.AVG, field: 'test-field' }], + hidden: false, + }), + createSeries({ + metrics: [{ id: 'some-id', type: METRIC_TYPES.AVG, field: 'test-field' }], + hidden: false, + }), + ], + }) + ); + expect(result).toBeNull(); + }); + + test('should return null if visible series is 2 and two not unique buckets', async () => { + mockGetBucketsColumns.mockReturnValueOnce([bucket]); + mockGetBucketsColumns.mockReturnValueOnce([bucket2]); + mockGetMetricsColumns.mockReturnValueOnce([metric]); + + const result = await convertToLens( + createPanel({ + series: [ + createSeries({ + metrics: [{ id: 'some-id', type: METRIC_TYPES.AVG, field: 'test-field' }], + hidden: false, + }), + createSeries({ + metrics: [{ id: 'some-id', type: METRIC_TYPES.AVG, field: 'test-field' }], + hidden: false, + }), + ], + }) + ); + expect(result).toBeNull(); + }); + + test('should return state if visible series is 2 and two unique buckets', async () => { + mockGetBucketsColumns.mockReturnValueOnce([bucket]); + mockGetBucketsColumns.mockReturnValueOnce([bucket]); + mockGetMetricsColumns.mockReturnValueOnce([metric]); + + const result = await convertToLens( + createPanel({ + series: [ + createSeries({ + metrics: [{ id: 'some-id', type: METRIC_TYPES.AVG, field: 'test-field' }], + hidden: false, + }), + createSeries({ + metrics: [{ id: 'some-id', type: METRIC_TYPES.AVG, field: 'test-field' }], + hidden: false, + }), + ], + }) + ); + expect(result).toBeDefined(); + expect(result?.type).toBe('lnsMetric'); + expect(mockGetConfigurationForMetric).toBeCalledTimes(1); + }); +}); diff --git a/src/plugins/vis_types/timeseries/public/convert_to_lens/metric/index.ts b/src/plugins/vis_types/timeseries/public/convert_to_lens/metric/index.ts new file mode 100644 index 0000000000000..25f55b5a1c44c --- /dev/null +++ b/src/plugins/vis_types/timeseries/public/convert_to_lens/metric/index.ts @@ -0,0 +1,130 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import uuid from 'uuid'; +import { DataView, parseTimeShift } from '@kbn/data-plugin/common'; +import { getIndexPatternIds } from '@kbn/visualizations-plugin/common/convert_to_lens'; +import { PANEL_TYPES } from '../../../common/enums'; +import { getDataViewsStart } from '../../services'; +import { getDataSourceInfo } from '../lib/datasource'; +import { getMetricsColumns, getBucketsColumns } from '../lib/series'; +import { getConfigurationForMetric as getConfiguration } from '../lib/configurations/metric'; +import { getReducedTimeRange, isValidMetrics } from '../lib/metrics'; +import { ConvertTsvbToLensVisualization } from '../types'; +import { ColumnsWithoutMeta, Layer as ExtendedLayer } from '../lib/convert'; +import { excludeMetaFromLayers, getUniqueBuckets } from './utils'; + +const MAX_SERIES = 2; +const MAX_BUCKETS = 2; + +export const convertToLens: ConvertTsvbToLensVisualization = async (model, timeRange) => { + const dataViews = getDataViewsStart(); + const seriesNum = model.series.filter((series) => !series.hidden).length; + + const indexPatternIds = new Set(); + const visibleSeries = model.series.filter(({ hidden }) => !hidden); + let currentIndexPattern: DataView | null = null; + for (const series of visibleSeries) { + const datasourceInfo = await getDataSourceInfo( + model.index_pattern, + model.time_field, + Boolean(series.override_index_pattern), + series.series_index_pattern, + series.series_time_field, + dataViews + ); + + if (!datasourceInfo) { + return null; + } + + const { indexPatternId, indexPattern } = datasourceInfo; + indexPatternIds.add(indexPatternId); + currentIndexPattern = indexPattern; + } + + if (indexPatternIds.size > 1) { + return null; + } + + const [indexPatternId] = indexPatternIds.values(); + + const buckets = []; + const metrics = []; + + // handle multiple layers/series + for (const series of visibleSeries) { + // not valid time shift + if (series.offset_time && parseTimeShift(series.offset_time) === 'invalid') { + return null; + } + + if (!isValidMetrics(series.metrics, PANEL_TYPES.METRIC, series.time_range_mode)) { + return null; + } + + const reducedTimeRange = getReducedTimeRange(model, series, timeRange); + + // handle multiple metrics + const metricsColumns = getMetricsColumns(series, currentIndexPattern!, seriesNum, { + reducedTimeRange, + }); + if (metricsColumns === null) { + return null; + } + + const bucketsColumns = getBucketsColumns( + model, + series, + metricsColumns, + currentIndexPattern!, + false + ); + + if (bucketsColumns === null) { + return null; + } + + buckets.push(...bucketsColumns); + metrics.push(...metricsColumns); + } + + let uniqueBuckets = buckets; + if (visibleSeries.length === MAX_SERIES && buckets.length) { + if (buckets.length !== MAX_BUCKETS) { + return null; + } + + uniqueBuckets = getUniqueBuckets(buckets as ColumnsWithoutMeta[]); + if (uniqueBuckets.length !== 1) { + return null; + } + } + + const [bucket] = uniqueBuckets; + + const extendedLayer: ExtendedLayer = { + indexPatternId: indexPatternId as string, + layerId: uuid(), + columns: [...metrics, ...(bucket ? [bucket] : [])], + columnOrder: [], + }; + + const configuration = getConfiguration(model, extendedLayer, bucket); + if (!configuration) { + return null; + } + + const layers = Object.values(excludeMetaFromLayers({ 0: extendedLayer })); + return { + type: 'lnsMetric', + layers, + configuration, + indexPatternIds: getIndexPatternIds(layers), + }; +}; diff --git a/src/plugins/vis_types/timeseries/public/convert_to_lens/metric/utils.test.ts b/src/plugins/vis_types/timeseries/public/convert_to_lens/metric/utils.test.ts new file mode 100644 index 0000000000000..8f880dfe95c5e --- /dev/null +++ b/src/plugins/vis_types/timeseries/public/convert_to_lens/metric/utils.test.ts @@ -0,0 +1,104 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { Column, DateHistogramColumn, TermsColumn } from '../lib/convert'; +import { getUniqueBuckets } from './utils'; + +describe('getUniqueBuckets', () => { + const bucket2: TermsColumn = { + columnId: '12', + dataType: 'string', + isBucketed: true, + isSplit: true, + operationType: 'terms', + params: { + exclude: [], + excludeIsRegex: true, + include: [], + includeIsRegex: true, + orderAgg: { + columnId: 'some-id-1', + dataType: 'number', + isBucketed: true, + isSplit: false, + operationType: 'average', + params: {}, + sourceField: 'bytes', + }, + orderBy: { columnId: 'some-id-1', type: 'column' }, + orderDirection: 'desc', + otherBucket: false, + parentFormat: { id: 'terms' }, + secondaryFields: [], + size: 10, + }, + sourceField: 'bytes', + }; + + it('should return unique buckets', () => { + expect(getUniqueBuckets([bucket2, bucket2])).toEqual([bucket2]); + }); + + it('should ignore columnIds', () => { + const bucketWithOtherColumnIds = { + ...bucket2, + columnId: '22', + params: { + ...bucket2.params, + orderAgg: { ...bucket2.params.orderAgg, columnId: '---1' } as Column, + orderBy: { ...bucket2.params.orderBy, columnId: '---2' } as { + type: 'column'; + columnId: string; + }, + }, + }; + expect(getUniqueBuckets([bucket2, bucketWithOtherColumnIds])).toEqual([bucket2]); + }); + + it('should respect differences of terms', () => { + const bucketWithOtherColumnIds = { + ...bucket2, + columnId: '22', + params: { + ...bucket2.params, + orderAgg: { ...bucket2.params.orderAgg, columnId: '---1' } as Column, + orderBy: { ...bucket2.params.orderBy, columnId: '---2' } as { + type: 'column'; + columnId: string; + }, + }, + sourceField: 'name', + }; + expect(getUniqueBuckets([bucket2, bucketWithOtherColumnIds])).toEqual([ + bucket2, + bucketWithOtherColumnIds, + ]); + }); + + it('should respect differences of other buckets', () => { + const bucket: DateHistogramColumn = { + dataType: 'number', + isBucketed: true, + isSplit: false, + operationType: 'date_histogram', + params: { dropPartials: false, includeEmptyRows: true, interval: 'auto' }, + sourceField: 'field1', + columnId: '1', + }; + const bucket1 = { + ...bucket, + columnId: '22', + }; + expect(getUniqueBuckets([bucket, bucket1])).toEqual([bucket]); + const bucket3 = { + ...bucket, + field: 'some-other-field', + }; + expect(getUniqueBuckets([bucket, bucket3])).toEqual([bucket, bucket3]); + }); +}); diff --git a/src/plugins/vis_types/timeseries/public/convert_to_lens/metric/utils.ts b/src/plugins/vis_types/timeseries/public/convert_to_lens/metric/utils.ts new file mode 100644 index 0000000000000..8df1b0f40f8be --- /dev/null +++ b/src/plugins/vis_types/timeseries/public/convert_to_lens/metric/utils.ts @@ -0,0 +1,65 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { uniqWith } from 'lodash'; +import deepEqual from 'react-fast-compare'; +import { Layer, Operations, TermsColumn } from '@kbn/visualizations-plugin/common/convert_to_lens'; +import { Layer as ExtendedLayer, excludeMetaFromColumn, ColumnsWithoutMeta } from '../lib/convert'; + +export const excludeMetaFromLayers = ( + layers: Record +): Record => { + const newLayers: Record = {}; + Object.entries(layers).forEach(([layerId, layer]) => { + const columns = layer.columns.map(excludeMetaFromColumn); + newLayers[layerId] = { ...layer, columns }; + }); + + return newLayers; +}; + +const excludeColumnIdsFromBucket = (bucket: ColumnsWithoutMeta) => { + const { columnId, ...restBucket } = bucket; + if (bucket.operationType === Operations.TERMS) { + const { orderBy, orderAgg, ...restParams } = bucket.params; + let orderByWithoutColumn: Omit = orderBy; + if ('columnId' in orderBy) { + const { columnId: orderByColumnId, ...restOrderBy } = orderBy; + orderByWithoutColumn = restOrderBy; + } + + let orderAggWithoutColumn: Omit | undefined = + orderAgg; + if (orderAgg) { + const { columnId: cId, ...restOrderAgg } = orderAgg; + orderAggWithoutColumn = restOrderAgg; + } + + return { + ...restBucket, + params: { + ...restParams, + orderBy: orderByWithoutColumn, + orderAgg: orderAggWithoutColumn, + }, + }; + } + return restBucket; +}; + +export const getUniqueBuckets = (buckets: ColumnsWithoutMeta[]) => + uniqWith(buckets, (bucket1, bucket2) => { + if (bucket1.operationType !== bucket2.operationType) { + return false; + } + + const bucketWithoutColumnIds1 = excludeColumnIdsFromBucket(bucket1); + const bucketWithoutColumnIds2 = excludeColumnIdsFromBucket(bucket2); + + return deepEqual(bucketWithoutColumnIds1, bucketWithoutColumnIds2); + }); diff --git a/src/plugins/vis_types/timeseries/public/convert_to_lens/timeseries/index.ts b/src/plugins/vis_types/timeseries/public/convert_to_lens/timeseries/index.ts index 4610310c1b378..8cbbbf0f9e739 100644 --- a/src/plugins/vis_types/timeseries/public/convert_to_lens/timeseries/index.ts +++ b/src/plugins/vis_types/timeseries/public/convert_to_lens/timeseries/index.ts @@ -86,7 +86,9 @@ export const convertToLens: ConvertTsvbToLensVisualization = async (model: Panel return null; } // handle multiple metrics - const metricsColumns = getMetricsColumns(series, indexPattern!, seriesNum); + const metricsColumns = getMetricsColumns(series, indexPattern!, seriesNum, { + isStaticValueColumnSupported: true, + }); if (metricsColumns === null) { return null; } diff --git a/src/plugins/vis_types/timeseries/public/convert_to_lens/top_n/index.ts b/src/plugins/vis_types/timeseries/public/convert_to_lens/top_n/index.ts index 630a9e5cf4c97..020aaec28f573 100644 --- a/src/plugins/vis_types/timeseries/public/convert_to_lens/top_n/index.ts +++ b/src/plugins/vis_types/timeseries/public/convert_to_lens/top_n/index.ts @@ -65,7 +65,9 @@ export const convertToLens: ConvertTsvbToLensVisualization = async (model, timeR const reducedTimeRange = getReducedTimeRange(model, series, timeRange); // handle multiple metrics - const metricsColumns = getMetricsColumns(series, indexPattern!, seriesNum, reducedTimeRange); + const metricsColumns = getMetricsColumns(series, indexPattern!, seriesNum, { + reducedTimeRange, + }); if (!metricsColumns) { return null; } diff --git a/src/plugins/vis_types/timeseries/public/convert_to_lens/types.ts b/src/plugins/vis_types/timeseries/public/convert_to_lens/types.ts index acee3c2a1e83a..69a90c7864eb3 100644 --- a/src/plugins/vis_types/timeseries/public/convert_to_lens/types.ts +++ b/src/plugins/vis_types/timeseries/public/convert_to_lens/types.ts @@ -6,14 +6,18 @@ * Side Public License, v 1. */ -import { NavigateToLensContext, XYConfiguration } from '@kbn/visualizations-plugin/common'; +import { + MetricVisConfiguration, + NavigateToLensContext, + XYConfiguration, +} from '@kbn/visualizations-plugin/common'; import { TimeRange } from '@kbn/data-plugin/common'; import type { Panel } from '../../common/types'; export type ConvertTsvbToLensVisualization = ( model: Panel, timeRange?: TimeRange -) => Promise | null>; +) => Promise | null>; export interface Filter { kql?: string | { [key: string]: any } | undefined; diff --git a/src/plugins/visualizations/common/convert_to_lens/types/configurations.ts b/src/plugins/visualizations/common/convert_to_lens/types/configurations.ts index 8c348fec4c2fa..fc1e440db3506 100644 --- a/src/plugins/visualizations/common/convert_to_lens/types/configurations.ts +++ b/src/plugins/visualizations/common/convert_to_lens/types/configurations.ts @@ -6,9 +6,9 @@ * Side Public License, v 1. */ -import { HorizontalAlignment, Position, VerticalAlignment } from '@elastic/charts'; +import { HorizontalAlignment, LayoutDirection, Position, VerticalAlignment } from '@elastic/charts'; import { $Values } from '@kbn/utility-types'; -import type { PaletteOutput } from '@kbn/coloring'; +import type { CustomPaletteParams, PaletteOutput } from '@kbn/coloring'; import { KibanaQueryOutput } from '@kbn/data-plugin/common'; import { LegendSize } from '../../constants'; @@ -199,4 +199,22 @@ export interface TableVisConfiguration { paging?: PagingState; } -export type Configuration = XYConfiguration | TableVisConfiguration; +export interface MetricVisConfiguration { + layerId: string; + layerType: 'data'; + metricAccessor?: string; + secondaryMetricAccessor?: string; + maxAccessor?: string; + breakdownByAccessor?: string; + // the dimensions can optionally be single numbers + // computed by collapsing all rows + collapseFn?: string; + subtitle?: string; + secondaryPrefix?: string; + progressDirection?: LayoutDirection; + color?: string; + palette?: PaletteOutput; + maxCols?: number; +} + +export type Configuration = XYConfiguration | TableVisConfiguration | MetricVisConfiguration; diff --git a/x-pack/plugins/lens/public/visualizations/metric/suggestions.test.ts b/x-pack/plugins/lens/public/visualizations/metric/suggestions.test.ts index 47229650e05f7..45f332776a4d0 100644 --- a/x-pack/plugins/lens/public/visualizations/metric/suggestions.test.ts +++ b/x-pack/plugins/lens/public/visualizations/metric/suggestions.test.ts @@ -79,6 +79,10 @@ describe('metric suggestions', () => { ...metricColumn, columnId: 'metric-column2', }, + { + ...metricColumn, + columnId: 'metric-column3', + }, ], changeType: 'unchanged', }, @@ -99,6 +103,10 @@ describe('metric suggestions', () => { ...metricColumn, columnId: 'metric-column2', }, + { + ...metricColumn, + columnId: 'metric-column3', + }, ], changeType: 'unchanged', }, diff --git a/x-pack/plugins/lens/public/visualizations/metric/suggestions.ts b/x-pack/plugins/lens/public/visualizations/metric/suggestions.ts index ae40bf83574f4..c0354d4db65e0 100644 --- a/x-pack/plugins/lens/public/visualizations/metric/suggestions.ts +++ b/x-pack/plugins/lens/public/visualizations/metric/suggestions.ts @@ -11,7 +11,7 @@ import { layerTypes } from '../../../common'; import { metricLabel, MetricVisualizationState, supportedDataTypes } from './visualization'; const MAX_BUCKETED_COLUMNS = 1; -const MAX_METRIC_COLUMNS = 1; +const MAX_METRIC_COLUMNS = 2; // primary and secondary metric const hasLayerMismatch = (keptLayerIds: string[], table: TableSuggestion) => keptLayerIds.length > 1 || (keptLayerIds.length && table.layerId !== keptLayerIds[0]); diff --git a/x-pack/plugins/lens/public/visualizations/metric/visualization.tsx b/x-pack/plugins/lens/public/visualizations/metric/visualization.tsx index ed1efa900cf24..b58068b3ec202 100644 --- a/x-pack/plugins/lens/public/visualizations/metric/visualization.tsx +++ b/x-pack/plugins/lens/public/visualizations/metric/visualization.tsx @@ -19,13 +19,20 @@ import { KibanaThemeProvider } from '@kbn/kibana-react-plugin/public'; import { IconChartMetric } from '@kbn/chart-icons'; import { LayerType } from '../../../common'; import { getSuggestions } from './suggestions'; -import { Visualization, OperationMetadata, DatasourceLayers, AccessorConfig } from '../../types'; +import { + Visualization, + OperationMetadata, + DatasourceLayers, + AccessorConfig, + Suggestion, +} from '../../types'; import { layerTypes } from '../../../common'; import { GROUP_ID, LENS_METRIC_ID } from './constants'; import { DimensionEditor } from './dimension_editor'; import { Toolbar } from './toolbar'; import { generateId } from '../../id_generator'; import { FormatSelectorOptions } from '../../indexpattern_datasource/dimension_panel/format_selector'; +import { IndexPatternLayer } from '../../indexpattern_datasource/types'; export const DEFAULT_MAX_COLUMNS = 3; @@ -50,6 +57,16 @@ export interface MetricVisualizationState { maxCols?: number; } +interface MetricDatasourceState { + [prop: string]: unknown; + layers: IndexPatternLayer[]; +} + +export interface MetricSuggestion extends Suggestion { + datasourceState: MetricDatasourceState; + visualizationState: MetricVisualizationState; +} + export const supportedDataTypes = new Set(['number']); // TODO - deduplicate with gauges? @@ -484,4 +501,25 @@ export const getMetricVisualization = ({ noPadding: true, }; }, + + getSuggestionFromConvertToLensContext({ suggestions, context }) { + const allSuggestions = suggestions as MetricSuggestion[]; + return { + ...allSuggestions[0], + datasourceState: { + ...allSuggestions[0].datasourceState, + layers: allSuggestions.reduce( + (acc, s) => ({ + ...acc, + ...s.datasourceState.layers, + }), + {} + ), + }, + visualizationState: { + ...allSuggestions[0].visualizationState, + ...context.configuration, + }, + }; + }, }); diff --git a/x-pack/test/functional/apps/lens/group3/tsvb_open_in_lens.ts b/x-pack/test/functional/apps/lens/group3/tsvb_open_in_lens.ts index a7acd8bf5ba1c..173ab1c4fdf04 100644 --- a/x-pack/test/functional/apps/lens/group3/tsvb_open_in_lens.ts +++ b/x-pack/test/functional/apps/lens/group3/tsvb_open_in_lens.ts @@ -102,9 +102,18 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await visualBuilder.clickDataTab('metric'); }); - it('should hide the "Edit Visualization in Lens" menu item', async () => { + it('should show the "Edit Visualization in Lens" menu item', async () => { const button = await testSubjects.exists('visualizeEditInLensButton'); - expect(button).to.eql(false); + expect(button).to.eql(true); + }); + + it('should convert to Lens', async () => { + const button = await testSubjects.find('visualizeEditInLensButton'); + await button.click(); + await lens.waitForVisualization('mtrVis'); + + const metricData = await lens.getMetricVisualizationData(); + expect(metricData[0].title).to.eql('Count of records'); }); }); From 6993716021ae9683994feefcaaf949554beb3649 Mon Sep 17 00:00:00 2001 From: Khristinin Nikita Date: Wed, 28 Sep 2022 12:08:58 +0200 Subject: [PATCH 05/17] skip failed tests (#142040) --- .../cypress/e2e/detection_rules/related_integrations.cy.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/security_solution/cypress/e2e/detection_rules/related_integrations.cy.ts b/x-pack/plugins/security_solution/cypress/e2e/detection_rules/related_integrations.cy.ts index b5c6b5cd341ec..02ccff0c265dd 100644 --- a/x-pack/plugins/security_solution/cypress/e2e/detection_rules/related_integrations.cy.ts +++ b/x-pack/plugins/security_solution/cypress/e2e/detection_rules/related_integrations.cy.ts @@ -122,14 +122,14 @@ describe('Related integrations', () => { visit(DETECTIONS_RULE_MANAGEMENT_URL); }); - it('should display a badge with the installed integrations on the rule management page', () => { + it.skip('should display a badge with the installed integrations on the rule management page', () => { cy.get(INTEGRATIONS_POPOVER).should( 'have.text', `${rule.enabledIntegrations}/${rule.integrations.length} integrations` ); }); - it('should display a popover when clicking the badge with the installed integrations on the rule management page', () => { + it.skip('should display a popover when clicking the badge with the installed integrations on the rule management page', () => { openIntegrationsPopover(); cy.get(INTEGRATIONS_POPOVER_TITLE).should( @@ -148,7 +148,7 @@ describe('Related integrations', () => { }); }); - it('should display the integrations on the definition section', () => { + it.skip('should display the integrations on the definition section', () => { goToTheRuleDetailsOf(rule.name); cy.get(INTEGRATIONS).should('have.length', rule.integrations.length); From 042c76687c46d16a227def1c04199af391bf4e31 Mon Sep 17 00:00:00 2001 From: Dima Arnautov Date: Wed, 28 Sep 2022 12:34:30 +0200 Subject: [PATCH 06/17] [ML] Update the Notification indicator tooltip, add functional tests (#141775) --- .../ml_page/notifications_indicator.tsx | 46 +++++-- .../components/notifications_list.tsx | 6 +- .../routes/schemas/notifications_schema.ts | 16 --- x-pack/test/api_integration/apis/ml/index.ts | 1 + .../ml/notifications/count_notifications.ts | 55 ++++++++ .../ml/notifications/get_notifications.ts | 102 +++++++++++++++ .../apis/ml/notifications/index.ts | 15 +++ .../functional/apps/ml/short_tests/index.ts | 1 + .../ml/short_tests/notifications/index.ts | 16 +++ .../notifications/notification_list.ts | 96 ++++++++++++++ x-pack/test/functional/services/ml/api.ts | 61 +++++++++ .../services/ml/common_table_service.ts | 118 ++++++++++++++++++ x-pack/test/functional/services/ml/index.ts | 8 +- .../functional/services/ml/notifications.ts | 48 +++++++ 14 files changed, 557 insertions(+), 32 deletions(-) create mode 100644 x-pack/test/api_integration/apis/ml/notifications/count_notifications.ts create mode 100644 x-pack/test/api_integration/apis/ml/notifications/get_notifications.ts create mode 100644 x-pack/test/api_integration/apis/ml/notifications/index.ts create mode 100644 x-pack/test/functional/apps/ml/short_tests/notifications/index.ts create mode 100644 x-pack/test/functional/apps/ml/short_tests/notifications/notification_list.ts create mode 100644 x-pack/test/functional/services/ml/common_table_service.ts create mode 100644 x-pack/test/functional/services/ml/notifications.ts diff --git a/x-pack/plugins/ml/public/application/components/ml_page/notifications_indicator.tsx b/x-pack/plugins/ml/public/application/components/ml_page/notifications_indicator.tsx index 20d771f5654d4..d0e3516af3db0 100644 --- a/x-pack/plugins/ml/public/application/components/ml_page/notifications_indicator.tsx +++ b/x-pack/plugins/ml/public/application/components/ml_page/notifications_indicator.tsx @@ -16,7 +16,10 @@ import { EuiToolTip, } from '@elastic/eui'; import { combineLatest, of, timer } from 'rxjs'; -import { catchError, filter, switchMap } from 'rxjs/operators'; +import { catchError, switchMap } from 'rxjs/operators'; +import moment from 'moment'; +import { FIELD_FORMAT_IDS } from '@kbn/field-formats-plugin/common'; +import { useFieldFormatter } from '../../contexts/kibana/use_field_formatter'; import { useAsObservable } from '../../hooks'; import { NotificationsCountResponse } from '../../../../common/types/notifications'; import { useMlKibana } from '../../contexts/kibana'; @@ -31,21 +34,26 @@ export const NotificationsIndicator: FC = () => { mlServices: { mlApiServices }, }, } = useMlKibana(); - const [lastCheckedAt] = useStorage(ML_NOTIFICATIONS_LAST_CHECKED_AT); + const dateFormatter = useFieldFormatter(FIELD_FORMAT_IDS.DATE); + const [lastCheckedAt] = useStorage(ML_NOTIFICATIONS_LAST_CHECKED_AT); const lastCheckedAt$ = useAsObservable(lastCheckedAt); + /** Holds the value used for the actual request */ + const [lastCheckRequested, setLastCheckRequested] = useState(); const [notificationsCounts, setNotificationsCounts] = useState(); useEffect(function startPollingNotifications() { - const subscription = combineLatest([ - lastCheckedAt$.pipe(filter((v): v is number => !!v)), - timer(0, NOTIFICATIONS_CHECK_INTERVAL), - ]) + const subscription = combineLatest([lastCheckedAt$, timer(0, NOTIFICATIONS_CHECK_INTERVAL)]) .pipe( - switchMap(([lastChecked]) => - mlApiServices.notifications.countMessages$({ lastCheckedAt: lastChecked }) - ), + switchMap(([lastChecked]) => { + const lastCheckedAtQuery = lastChecked ?? moment().subtract(7, 'd').valueOf(); + setLastCheckRequested(lastCheckedAtQuery); + // Use the latest check time or 7 days ago by default. + return mlApiServices.notifications.countMessages$({ + lastCheckedAt: lastCheckedAtQuery, + }); + }), catchError((error) => { // Fail silently for now return of({} as NotificationsCountResponse); @@ -80,12 +88,22 @@ export const NotificationsIndicator: FC = () => { content={ } > - {errorsAndWarningCount} + + {errorsAndWarningCount} + ) : null} @@ -96,7 +114,8 @@ export const NotificationsIndicator: FC = () => { content={ } > @@ -106,6 +125,7 @@ export const NotificationsIndicator: FC = () => { aria-label={i18n.translate('xpack.ml.notificationsIndicator.unreadIcon', { defaultMessage: 'Unread notifications indicator.', })} + data-test-subj={'mlNotificationsIndicator'} /> diff --git a/x-pack/plugins/ml/public/application/notifications/components/notifications_list.tsx b/x-pack/plugins/ml/public/application/notifications/components/notifications_list.tsx index 811b1c43bce37..9ea6aa1b70f00 100644 --- a/x-pack/plugins/ml/public/application/notifications/components/notifications_list.tsx +++ b/x-pack/plugins/ml/public/application/notifications/components/notifications_list.tsx @@ -162,6 +162,7 @@ export const NotificationsList: FC = () => { const columns: Array> = [ { + id: 'timestamp', field: 'timestamp', name: , sortable: true, @@ -175,7 +176,7 @@ export const NotificationsList: FC = () => { name: , sortable: true, truncateText: false, - 'data-test-subj': 'mlNotificationLabel', + 'data-test-subj': 'mlNotificationLevel', render: (value: MlNotificationMessageLevel) => { return {value}; }, @@ -194,7 +195,7 @@ export const NotificationsList: FC = () => { }, { field: 'job_id', - name: , + name: , sortable: true, truncateText: false, 'data-test-subj': 'mlNotificationEntity', @@ -320,6 +321,7 @@ export const NotificationsList: FC = () => { }, }, }, + 'data-test-subj': 'mlNotificationsSearchBarInput', }} filters={filters} onChange={(e) => { diff --git a/x-pack/plugins/ml/server/routes/schemas/notifications_schema.ts b/x-pack/plugins/ml/server/routes/schemas/notifications_schema.ts index a82691ee52813..153ffd0aeea87 100644 --- a/x-pack/plugins/ml/server/routes/schemas/notifications_schema.ts +++ b/x-pack/plugins/ml/server/routes/schemas/notifications_schema.ts @@ -8,26 +8,10 @@ import { schema, TypeOf } from '@kbn/config-schema'; export const getNotificationsQuerySchema = schema.object({ - /** - * Message level, e.g. info, error - */ - level: schema.maybe(schema.string()), - /** - * Message type, e.g. anomaly_detector - */ - type: schema.maybe(schema.string()), /** * Search string for the message content */ queryString: schema.maybe(schema.string()), - /** - * Page numer, zero-indexed - */ - from: schema.number({ defaultValue: 0 }), - /** - * Number of messages to return - */ - size: schema.number({ defaultValue: 10 }), /** * Sort field */ diff --git a/x-pack/test/api_integration/apis/ml/index.ts b/x-pack/test/api_integration/apis/ml/index.ts index 915d755ca97c0..e76eef8cb82bf 100644 --- a/x-pack/test/api_integration/apis/ml/index.ts +++ b/x-pack/test/api_integration/apis/ml/index.ts @@ -67,5 +67,6 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./saved_objects')); loadTestFile(require.resolve('./system')); loadTestFile(require.resolve('./trained_models')); + loadTestFile(require.resolve('./notifications')); }); } diff --git a/x-pack/test/api_integration/apis/ml/notifications/count_notifications.ts b/x-pack/test/api_integration/apis/ml/notifications/count_notifications.ts new file mode 100644 index 0000000000000..58932ea199b5d --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/notifications/count_notifications.ts @@ -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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import moment from 'moment'; +import type { FtrProviderContext } from '../../../ftr_provider_context'; +import { USER } from '../../../../functional/services/ml/security_common'; +import { COMMON_REQUEST_HEADERS } from '../../../../functional/services/ml/common_api'; + +export default ({ getService }: FtrProviderContext) => { + const supertest = getService('supertestWithoutAuth'); + const ml = getService('ml'); + + describe('GET notifications count', () => { + before(async () => { + await ml.api.initSavedObjects(); + await ml.testResources.setKibanaTimeZoneToUTC(); + + const adJobConfig = ml.commonConfig.getADFqSingleMetricJobConfig('fq_job'); + await ml.api.createAnomalyDetectionJob(adJobConfig); + + await ml.api.waitForJobNotificationsToIndex('fq_job'); + }); + + after(async () => { + await ml.api.cleanMlIndices(); + await ml.testResources.cleanMLSavedObjects(); + }); + + it('return notifications count by level', async () => { + const { body, status } = await supertest + .get(`/api/ml/notifications/count`) + .query({ lastCheckedAt: moment().subtract(7, 'd').valueOf() }) + .auth(USER.ML_POWERUSER, ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER)) + .set(COMMON_REQUEST_HEADERS); + ml.api.assertResponseStatusCode(200, status, body); + + expect(body.info).to.eql(1); + expect(body.warning).to.eql(0); + expect(body.error).to.eql(0); + }); + + it('returns an error for unauthorized user', async () => { + const { body, status } = await supertest + .get(`/api/ml/notifications/count`) + .auth(USER.ML_UNAUTHORIZED, ml.securityCommon.getPasswordForUser(USER.ML_UNAUTHORIZED)) + .set(COMMON_REQUEST_HEADERS); + ml.api.assertResponseStatusCode(403, status, body); + }); + }); +}; diff --git a/x-pack/test/api_integration/apis/ml/notifications/get_notifications.ts b/x-pack/test/api_integration/apis/ml/notifications/get_notifications.ts new file mode 100644 index 0000000000000..992065cdae67d --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/notifications/get_notifications.ts @@ -0,0 +1,102 @@ +/* + * 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 expect from '@kbn/expect'; +import type { + NotificationItem, + NotificationsSearchResponse, +} from '@kbn/ml-plugin/common/types/notifications'; +import type { FtrProviderContext } from '../../../ftr_provider_context'; +import { USER } from '../../../../functional/services/ml/security_common'; +import { COMMON_REQUEST_HEADERS } from '../../../../functional/services/ml/common_api'; + +export default ({ getService }: FtrProviderContext) => { + const supertest = getService('supertestWithoutAuth'); + const esArchiver = getService('esArchiver'); + const ml = getService('ml'); + + describe('GET notifications', () => { + before(async () => { + await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/bm_classification'); + await ml.api.initSavedObjects(); + await ml.testResources.setKibanaTimeZoneToUTC(); + + const adJobConfig = ml.commonConfig.getADFqSingleMetricJobConfig('fq_job'); + await ml.api.createAnomalyDetectionJob(adJobConfig); + + const dfaJobConfig = ml.commonConfig.getDFABmClassificationJobConfig('df_job'); + await ml.api.createDataFrameAnalyticsJob(dfaJobConfig); + + // wait for notification to index + + await ml.api.waitForJobNotificationsToIndex('fq_job'); + await ml.api.waitForJobNotificationsToIndex('df_job'); + }); + + after(async () => { + await ml.api.cleanMlIndices(); + await ml.testResources.cleanMLSavedObjects(); + }); + + it('return all notifications ', async () => { + const { body, status } = await supertest + .get(`/api/ml/notifications`) + .query({ earliest: 'now-1d', latest: 'now' }) + .auth(USER.ML_POWERUSER, ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER)) + .set(COMMON_REQUEST_HEADERS); + ml.api.assertResponseStatusCode(200, status, body); + + expect((body as NotificationsSearchResponse).total).to.eql(2); + }); + + it('return notifications based on the query string', async () => { + const { body, status } = await supertest + .get(`/api/ml/notifications`) + .query({ earliest: 'now-1d', latest: 'now', queryString: 'job_type:anomaly_detector' }) + .auth(USER.ML_VIEWER, ml.securityCommon.getPasswordForUser(USER.ML_VIEWER)) + .set(COMMON_REQUEST_HEADERS); + ml.api.assertResponseStatusCode(200, status, body); + + expect((body as NotificationsSearchResponse).total).to.eql(1); + expect( + (body as NotificationsSearchResponse).results.filter( + (result: NotificationItem) => result.job_type === 'anomaly_detector' + ) + ).to.length(body.total); + }); + + it('supports sorting asc sorting by field', async () => { + const { body, status } = await supertest + .get(`/api/ml/notifications`) + .query({ earliest: 'now-1d', latest: 'now', sortField: 'job_id', sortDirection: 'asc' }) + .auth(USER.ML_POWERUSER, ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER)) + .set(COMMON_REQUEST_HEADERS); + ml.api.assertResponseStatusCode(200, status, body); + + expect(body.results[0].job_id).to.eql('df_job'); + }); + + it('supports sorting desc sorting by field', async () => { + const { body, status } = await supertest + .get(`/api/ml/notifications`) + .query({ earliest: 'now-1h', latest: 'now', sortField: 'job_id', sortDirection: 'desc' }) + .auth(USER.ML_POWERUSER, ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER)) + .set(COMMON_REQUEST_HEADERS); + ml.api.assertResponseStatusCode(200, status, body); + + expect(body.results[0].job_id).to.eql('fq_job'); + }); + + it('returns an error for unauthorized user', async () => { + const { body, status } = await supertest + .get(`/api/ml/notifications`) + .auth(USER.ML_UNAUTHORIZED, ml.securityCommon.getPasswordForUser(USER.ML_UNAUTHORIZED)) + .set(COMMON_REQUEST_HEADERS); + ml.api.assertResponseStatusCode(403, status, body); + }); + }); +}; diff --git a/x-pack/test/api_integration/apis/ml/notifications/index.ts b/x-pack/test/api_integration/apis/ml/notifications/index.ts new file mode 100644 index 0000000000000..4a09fce5ee51e --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/notifications/index.ts @@ -0,0 +1,15 @@ +/* + * 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 { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('Notifications', function () { + loadTestFile(require.resolve('./get_notifications')); + loadTestFile(require.resolve('./count_notifications')); + }); +} diff --git a/x-pack/test/functional/apps/ml/short_tests/index.ts b/x-pack/test/functional/apps/ml/short_tests/index.ts index f96d2b91ee0ef..d446a35933474 100644 --- a/x-pack/test/functional/apps/ml/short_tests/index.ts +++ b/x-pack/test/functional/apps/ml/short_tests/index.ts @@ -33,5 +33,6 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./model_management')); loadTestFile(require.resolve('./feature_controls')); loadTestFile(require.resolve('./settings')); + loadTestFile(require.resolve('./notifications')); }); } diff --git a/x-pack/test/functional/apps/ml/short_tests/notifications/index.ts b/x-pack/test/functional/apps/ml/short_tests/notifications/index.ts new file mode 100644 index 0000000000000..e026d44a67af2 --- /dev/null +++ b/x-pack/test/functional/apps/ml/short_tests/notifications/index.ts @@ -0,0 +1,16 @@ +/* + * 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 { FtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('Notifcations', function () { + this.tags(['ml', 'skipFirefox']); + + loadTestFile(require.resolve('./notification_list')); + }); +} diff --git a/x-pack/test/functional/apps/ml/short_tests/notifications/notification_list.ts b/x-pack/test/functional/apps/ml/short_tests/notifications/notification_list.ts new file mode 100644 index 0000000000000..c9ad8d2423ef8 --- /dev/null +++ b/x-pack/test/functional/apps/ml/short_tests/notifications/notification_list.ts @@ -0,0 +1,96 @@ +/* + * 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 { FtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const PageObjects = getPageObjects(['common']); + const esArchiver = getService('esArchiver'); + const ml = getService('ml'); + const browser = getService('browser'); + + describe('Notifications list', function () { + before(async () => { + await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/farequote'); + await ml.testResources.createIndexPatternIfNeeded('ft_farequote', '@timestamp'); + await ml.testResources.setKibanaTimeZoneToUTC(); + + // Prepare jobs to generate notifications + await Promise.all( + [ + { jobId: 'fq_001', spaceId: undefined }, + { jobId: 'fq_002', spaceId: 'space1' }, + ].map(async (v) => { + const datafeedConfig = ml.commonConfig.getADFqDatafeedConfig(v.jobId); + + await ml.api.createAnomalyDetectionJob( + ml.commonConfig.getADFqSingleMetricJobConfig(v.jobId), + v.spaceId + ); + await ml.api.openAnomalyDetectionJob(v.jobId); + await ml.api.createDatafeed(datafeedConfig, v.spaceId); + await ml.api.startDatafeed(datafeedConfig.datafeed_id); + }) + ); + + await ml.securityUI.loginAsMlPowerUser(); + await PageObjects.common.navigateToApp('ml', { + basePath: '', + }); + }); + + after(async () => { + await ml.api.cleanMlIndices(); + await ml.testResources.cleanMLSavedObjects(); + await ml.testResources.deleteIndexPatternByTitle('ft_farequote'); + }); + + it('displays a generic notification indicator', async () => { + await ml.notifications.assertNotificationIndicatorExist(); + }); + + it('opens the Notifications page', async () => { + await ml.navigation.navigateToNotifications(); + + await ml.notifications.table.waitForTableToLoad(); + await ml.notifications.table.assertRowsNumberPerPage(25); + }); + + it('does not show notifications from another space', async () => { + await ml.notifications.table.filterWithSearchString('Job created', 1); + }); + + it('display a number of errors in the notification indicator', async () => { + await ml.navigation.navigateToOverview(); + + const jobConfig = ml.commonConfig.getADFqSingleMetricJobConfig('fq_fail'); + jobConfig.analysis_config = { + bucket_span: '15m', + influencers: ['airline'], + detectors: [ + { function: 'mean', field_name: 'responsetime', partition_field_name: 'airline' }, + { function: 'min', field_name: 'responsetime', partition_field_name: 'airline' }, + { function: 'max', field_name: 'responsetime', partition_field_name: 'airline' }, + ], + }; + // Set extremely low memory limit to trigger an error + jobConfig.analysis_limits!.model_memory_limit = '1024kb'; + + const datafeedConfig = ml.commonConfig.getADFqDatafeedConfig(jobConfig.job_id); + + await ml.api.createAnomalyDetectionJob(jobConfig); + await ml.api.openAnomalyDetectionJob(jobConfig.job_id); + await ml.api.createDatafeed(datafeedConfig); + await ml.api.startDatafeed(datafeedConfig.datafeed_id); + await ml.api.waitForJobMemoryStatus(jobConfig.job_id, 'hard_limit'); + + // refresh the page to avoid 1m wait + await browser.refresh(); + await ml.notifications.assertNotificationErrorsCount(0); + }); + }); +} diff --git a/x-pack/test/functional/services/ml/api.ts b/x-pack/test/functional/services/ml/api.ts index 07bd1f346cf25..62d46e644f173 100644 --- a/x-pack/test/functional/services/ml/api.ts +++ b/x-pack/test/functional/services/ml/api.ts @@ -273,6 +273,16 @@ export function MachineLearningAPIProvider({ getService }: FtrProviderContext) { return state; }, + async getJobMemoryStatus(jobId: string): Promise<'hard_limit' | 'soft_limit' | 'ok'> { + const jobStats = await this.getADJobStats(jobId); + + expect(jobStats.jobs).to.have.length( + 1, + `Expected job stats to have exactly one job (got '${jobStats.length}')` + ); + return jobStats.jobs[0].model_size_stats.memory_status; + }, + async getADJobStats(jobId: string): Promise { log.debug(`Fetching anomaly detection job stats for job ${jobId}...`); const { body: jobStats, status } = await esSupertest.get( @@ -299,6 +309,27 @@ export function MachineLearningAPIProvider({ getService }: FtrProviderContext) { }); }, + async waitForJobMemoryStatus( + jobId: string, + expectedMemoryStatus: 'hard_limit' | 'soft_limit' | 'ok', + timeout: number = 2 * 60 * 1000 + ) { + await retry.waitForWithTimeout( + `job memory status to be ${expectedMemoryStatus}`, + timeout, + async () => { + const memoryStatus = await this.getJobMemoryStatus(jobId); + if (memoryStatus === expectedMemoryStatus) { + return true; + } else { + throw new Error( + `expected job memory status to be ${expectedMemoryStatus} but got ${memoryStatus}` + ); + } + } + ); + }, + async getDatafeedState(datafeedId: string): Promise { log.debug(`Fetching datafeed state for datafeed ${datafeedId}`); const { body: datafeedStats, status } = await esSupertest.get( @@ -576,6 +607,18 @@ export function MachineLearningAPIProvider({ getService }: FtrProviderContext) { return response; }, + async hasNotifications(query: object) { + const body = await es.search({ + index: '.ml-notifications*', + body: { + size: 10000, + query, + }, + }); + + return body.hits.hits.length > 0; + }, + async adJobExist(jobId: string) { this.validateJobId(jobId); try { @@ -608,6 +651,24 @@ export function MachineLearningAPIProvider({ getService }: FtrProviderContext) { }); }, + async waitForJobNotificationsToIndex(jobId: string, timeout: number = 60 * 1000) { + await retry.waitForWithTimeout(`Notifications for '${jobId}' to exist`, timeout, async () => { + if ( + await this.hasNotifications({ + term: { + job_id: { + value: jobId, + }, + }, + }) + ) { + return true; + } else { + throw new Error(`expected '${jobId}' notifications to exist`); + } + }); + }, + async createAnomalyDetectionJob(jobConfig: Job, space?: string) { const jobId = jobConfig.job_id; log.debug( diff --git a/x-pack/test/functional/services/ml/common_table_service.ts b/x-pack/test/functional/services/ml/common_table_service.ts new file mode 100644 index 0000000000000..50a40ab43f35a --- /dev/null +++ b/x-pack/test/functional/services/ml/common_table_service.ts @@ -0,0 +1,118 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { WebElementWrapper } from '../../../../../test/functional/services/lib/web_element_wrapper'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export type MlTableService = ReturnType; + +export function MlTableServiceProvider({ getPageObject, getService }: FtrProviderContext) { + const testSubjects = getService('testSubjects'); + const commonPage = getPageObject('common'); + + const TableService = class { + constructor( + public readonly tableTestSubj: string, + public readonly tableRowSubj: string, + public readonly columns: Array<{ id: string; testSubj: string }>, + public readonly searchInputSubj: string + ) {} + + public async assertTableLoaded() { + await testSubjects.existOrFail(`~${this.tableTestSubj} loaded`); + } + + public async assertTableLoading() { + await testSubjects.existOrFail(`~${this.tableTestSubj} loading`); + } + + public async parseTable() { + const table = await testSubjects.find(`~${this.tableTestSubj}`); + const $ = await table.parseDomContent(); + const rows = []; + + for (const tr of $.findTestSubjects(`~${this.tableRowSubj}`).toArray()) { + const $tr = $(tr); + + const rowObject = this.columns.reduce((acc, curr) => { + acc[curr.id] = $tr + .findTestSubject(curr.testSubj) + .find('.euiTableCellContent') + .text() + .trim(); + return acc; + }, {} as Record); + + rows.push(rowObject); + } + + return rows; + } + + public async assertRowsNumberPerPage(rowsNumber: 10 | 25 | 50 | 100) { + const textContent = await testSubjects.getVisibleText( + `~${this.tableTestSubj} > tablePaginationPopoverButton` + ); + expect(textContent).to.be(`Rows per page: ${rowsNumber}`); + } + + public async waitForTableToStartLoading() { + await testSubjects.existOrFail(`~${this.tableTestSubj}`, { timeout: 60 * 1000 }); + await testSubjects.existOrFail(`${this.tableTestSubj} loading`, { timeout: 30 * 1000 }); + } + + public async waitForTableToLoad() { + await testSubjects.existOrFail(`~${this.tableTestSubj}`, { timeout: 60 * 1000 }); + await testSubjects.existOrFail(`${this.tableTestSubj} loaded`, { timeout: 30 * 1000 }); + } + + async getSearchInput(): Promise { + return await testSubjects.find(this.searchInputSubj); + } + + public async assertSearchInputValue(expectedSearchValue: string) { + const searchBarInput = await this.getSearchInput(); + const actualSearchValue = await searchBarInput.getAttribute('value'); + expect(actualSearchValue).to.eql( + expectedSearchValue, + `Search input value should be '${expectedSearchValue}' (got '${actualSearchValue}')` + ); + } + + public async filterWithSearchString(queryString: string, expectedRowCount: number = 1) { + await this.waitForTableToLoad(); + const searchBarInput = await this.getSearchInput(); + await searchBarInput.clearValueWithKeyboard(); + await searchBarInput.type(queryString); + await commonPage.pressEnterKey(); + await this.assertSearchInputValue(queryString); + await this.waitForTableToStartLoading(); + await this.waitForTableToLoad(); + + const rows = await this.parseTable(); + + expect(rows).to.have.length( + expectedRowCount, + `Filtered table should have ${expectedRowCount} row(s) for filter '${queryString}' (got ${rows.length} matching items)` + ); + } + }; + + return { + getServiceInstance( + name: string, + tableTestSubj: string, + tableRowSubj: string, + columns: Array<{ id: string; testSubj: string }>, + searchInputSubj: string + ) { + Object.defineProperty(TableService, 'name', { value: name }); + return new TableService(tableTestSubj, tableRowSubj, columns, searchInputSubj); + }, + }; +} diff --git a/x-pack/test/functional/services/ml/index.ts b/x-pack/test/functional/services/ml/index.ts index d8c6924ec4cfd..9452baa324898 100644 --- a/x-pack/test/functional/services/ml/index.ts +++ b/x-pack/test/functional/services/ml/index.ts @@ -59,6 +59,8 @@ import { MachineLearningJobAnnotationsProvider } from './job_annotations_table'; import { MlNodesPanelProvider } from './ml_nodes_list'; import { MachineLearningCasesProvider } from './cases'; import { AnomalyChartsProvider } from './anomaly_charts'; +import { NotificationsProvider } from './notifications'; +import { MlTableServiceProvider } from './common_table_service'; export function MachineLearningProvider(context: FtrProviderContext) { const commonAPI = MachineLearningCommonAPIProvider(context); @@ -123,6 +125,7 @@ export function MachineLearningProvider(context: FtrProviderContext) { const settingsFilterList = MachineLearningSettingsFilterListProvider(context, commonUI); const singleMetricViewer = MachineLearningSingleMetricViewerProvider(context, commonUI); const stackManagementJobs = MachineLearningStackManagementJobsProvider(context); + const tableService = MlTableServiceProvider(context); const testExecution = MachineLearningTestExecutionProvider(context); const testResources = MachineLearningTestResourcesProvider(context, api); const alerting = MachineLearningAlertingProvider(context, api, commonUI); @@ -130,6 +133,7 @@ export function MachineLearningProvider(context: FtrProviderContext) { const trainedModels = TrainedModelsProvider(context, commonUI); const trainedModelsTable = TrainedModelsTableProvider(context, commonUI); const mlNodesPanel = MlNodesPanelProvider(context); + const notifications = NotificationsProvider(context, commonUI, tableService); const cases = MachineLearningCasesProvider(context, swimLane, anomalyCharts); @@ -171,7 +175,9 @@ export function MachineLearningProvider(context: FtrProviderContext) { jobWizardMultiMetric, jobWizardPopulation, lensVisualizations, + mlNodesPanel, navigation, + notifications, overviewPage, securityCommon, securityUI, @@ -181,10 +187,10 @@ export function MachineLearningProvider(context: FtrProviderContext) { singleMetricViewer, stackManagementJobs, swimLane, + tableService, testExecution, testResources, trainedModels, trainedModelsTable, - mlNodesPanel, }; } diff --git a/x-pack/test/functional/services/ml/notifications.ts b/x-pack/test/functional/services/ml/notifications.ts new file mode 100644 index 0000000000000..2365717d7226b --- /dev/null +++ b/x-pack/test/functional/services/ml/notifications.ts @@ -0,0 +1,48 @@ +/* + * 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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; +import { MlCommonUI } from './common_ui'; +import { MlTableService } from './common_table_service'; + +export function NotificationsProvider( + { getService }: FtrProviderContext, + mlCommonUI: MlCommonUI, + tableService: MlTableService +) { + const testSubjects = getService('testSubjects'); + + return { + async assertNotificationIndicatorExist(expectExist = true) { + if (expectExist) { + await testSubjects.existOrFail('mlNotificationsIndicator'); + } else { + await testSubjects.missingOrFail('mlNotificationsIndicator'); + } + }, + + async assertNotificationErrorsCount(expectedCount: number) { + const actualCount = await testSubjects.getVisibleText('mlNotificationErrorsIndicator'); + expect(actualCount).to.greaterThan(expectedCount); + }, + + table: tableService.getServiceInstance( + 'NotificationsTable', + 'mlNotificationsTable', + 'mlNotificationsTableRow', + [ + { id: 'timestamp', testSubj: 'mlNotificationTime' }, + { id: 'level', testSubj: 'mlNotificationLevel' }, + { id: 'job_type', testSubj: 'mlNotificationType' }, + { id: 'job_id', testSubj: 'mlNotificationEntity' }, + { id: 'message', testSubj: 'mlNotificationMessage' }, + ], + 'mlNotificationsSearchBarInput' + ), + }; +} From 0471095d7d01634ffae4ef5017797e28ff5b13a8 Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Wed, 28 Sep 2022 13:02:51 +0200 Subject: [PATCH 07/17] Run ILM jest tests as integration tests allowing them to run beyond 5s timeout (#141750) --- .../index_lifecycle_management/README.md | 6 +++++- .../__jest__/extend_index_management.test.tsx | 2 +- .../integration_tests/README.md | 14 ++++++++++++++ .../app/app.helpers.ts | 2 +- .../app/app.test.ts | 0 .../edit_policy/constants.ts | 4 ++-- .../features/delete_phase.helpers.ts | 0 .../edit_policy/features/delete_phase.test.ts | 2 +- .../features/downsample.helpers.ts | 2 +- .../edit_policy/features/downsample.test.ts | 0 .../edit_policy/features/edit_warning.test.ts | 0 .../edit_policy/features/frozen_phase.test.ts | 0 .../cloud_aware_behavior.helpers.ts | 2 +- .../cloud_aware_behavior.test.ts | 0 .../node_allocation/cold_phase.helpers.ts | 0 .../node_allocation/cold_phase.test.ts | 0 .../general_behavior.helpers.ts | 0 .../node_allocation/general_behavior.test.ts | 2 +- .../node_allocation/warm_phase.helpers.ts | 0 .../node_allocation/warm_phase.test.ts | 0 .../features/request_flyout.helpers.ts | 0 .../features/request_flyout.test.ts | 0 .../edit_policy/features/rollover.helpers.ts | 0 .../edit_policy/features/rollover.test.ts | 0 .../features/searchable_snapshots.helpers.ts | 2 +- .../features/searchable_snapshots.test.ts | 2 +- .../edit_policy/features/timeline.helpers.ts | 2 +- .../edit_policy/features/timeline.test.ts | 0 .../edit_policy/features/timing.helpers.ts | 0 .../edit_policy/features/timing.test.ts | 2 +- .../cold_phase_validation.test.ts | 5 ++--- .../downsample_interval.test.ts | 7 +++---- .../form_validation/error_indicators.test.ts | 3 +-- .../hot_phase_validation.test.ts | 5 ++--- .../policy_name_validation.test.ts | 5 ++--- .../form_validation/timing.test.ts | 7 +++---- .../form_validation/validation.helpers.ts | 0 .../warm_phase_validation.test.ts | 5 ++--- .../edit_policy/init_test_bed.ts | 6 +++--- .../policy_serialization.helpers.ts | 2 +- .../policy_serialization.test.ts | 2 +- .../helpers/actions/downsample_actions.ts | 4 ++-- .../helpers/actions/errors_actions.ts | 2 +- .../helpers/actions/forcemerge_actions.ts | 2 +- .../helpers/actions/form_set_value_action.ts | 0 .../helpers/actions/form_toggle_action.ts | 0 .../form_toggle_and_set_value_action.ts | 0 .../helpers/actions/index.ts | 0 .../helpers/actions/index_priority_actions.ts | 2 +- .../helpers/actions/min_age_actions.ts | 2 +- .../actions/node_allocation_actions.ts | 4 ++-- .../helpers/actions/phases.ts | 0 .../helpers/actions/readonly_actions.ts | 2 +- .../helpers/actions/replicas_action.ts | 2 +- .../helpers/actions/request_flyout_actions.ts | 0 .../helpers/actions/rollover_actions.ts | 0 .../helpers/actions/save_policy_action.ts | 0 .../actions/searchable_snapshot_actions.ts | 2 +- .../helpers/actions/shrink_actions.ts | 2 +- .../actions/snapshot_policy_actions.ts | 0 .../helpers/actions/toggle_phase_action.ts | 2 +- .../helpers/global_mocks.tsx | 0 .../helpers/http_requests.ts | 4 ++-- .../helpers/index.ts | 0 .../helpers/setup_environment.tsx | 10 +++++----- .../jest.integration.config.js | 19 +++++++++++++++++++ .../index_lifecycle_management/tsconfig.json | 1 + 67 files changed, 91 insertions(+), 60 deletions(-) create mode 100644 x-pack/plugins/index_lifecycle_management/integration_tests/README.md rename x-pack/plugins/index_lifecycle_management/{__jest__/client_integration => integration_tests}/app/app.helpers.ts (97%) rename x-pack/plugins/index_lifecycle_management/{__jest__/client_integration => integration_tests}/app/app.test.ts (100%) rename x-pack/plugins/index_lifecycle_management/{__jest__/client_integration => integration_tests}/edit_policy/constants.ts (97%) rename x-pack/plugins/index_lifecycle_management/{__jest__/client_integration => integration_tests}/edit_policy/features/delete_phase.helpers.ts (100%) rename x-pack/plugins/index_lifecycle_management/{__jest__/client_integration => integration_tests}/edit_policy/features/delete_phase.test.ts (98%) rename x-pack/plugins/index_lifecycle_management/{__jest__/client_integration => integration_tests}/edit_policy/features/downsample.helpers.ts (95%) rename x-pack/plugins/index_lifecycle_management/{__jest__/client_integration => integration_tests}/edit_policy/features/downsample.test.ts (100%) rename x-pack/plugins/index_lifecycle_management/{__jest__/client_integration => integration_tests}/edit_policy/features/edit_warning.test.ts (100%) rename x-pack/plugins/index_lifecycle_management/{__jest__/client_integration => integration_tests}/edit_policy/features/frozen_phase.test.ts (100%) rename x-pack/plugins/index_lifecycle_management/{__jest__/client_integration => integration_tests}/edit_policy/features/node_allocation/cloud_aware_behavior.helpers.ts (94%) rename x-pack/plugins/index_lifecycle_management/{__jest__/client_integration => integration_tests}/edit_policy/features/node_allocation/cloud_aware_behavior.test.ts (100%) rename x-pack/plugins/index_lifecycle_management/{__jest__/client_integration => integration_tests}/edit_policy/features/node_allocation/cold_phase.helpers.ts (100%) rename x-pack/plugins/index_lifecycle_management/{__jest__/client_integration => integration_tests}/edit_policy/features/node_allocation/cold_phase.test.ts (100%) rename x-pack/plugins/index_lifecycle_management/{__jest__/client_integration => integration_tests}/edit_policy/features/node_allocation/general_behavior.helpers.ts (100%) rename x-pack/plugins/index_lifecycle_management/{__jest__/client_integration => integration_tests}/edit_policy/features/node_allocation/general_behavior.test.ts (98%) rename x-pack/plugins/index_lifecycle_management/{__jest__/client_integration => integration_tests}/edit_policy/features/node_allocation/warm_phase.helpers.ts (100%) rename x-pack/plugins/index_lifecycle_management/{__jest__/client_integration => integration_tests}/edit_policy/features/node_allocation/warm_phase.test.ts (100%) rename x-pack/plugins/index_lifecycle_management/{__jest__/client_integration => integration_tests}/edit_policy/features/request_flyout.helpers.ts (100%) rename x-pack/plugins/index_lifecycle_management/{__jest__/client_integration => integration_tests}/edit_policy/features/request_flyout.test.ts (100%) rename x-pack/plugins/index_lifecycle_management/{__jest__/client_integration => integration_tests}/edit_policy/features/rollover.helpers.ts (100%) rename x-pack/plugins/index_lifecycle_management/{__jest__/client_integration => integration_tests}/edit_policy/features/rollover.test.ts (100%) rename x-pack/plugins/index_lifecycle_management/{__jest__/client_integration => integration_tests}/edit_policy/features/searchable_snapshots.helpers.ts (97%) rename x-pack/plugins/index_lifecycle_management/{__jest__/client_integration => integration_tests}/edit_policy/features/searchable_snapshots.test.ts (99%) rename x-pack/plugins/index_lifecycle_management/{__jest__/client_integration => integration_tests}/edit_policy/features/timeline.helpers.ts (94%) rename x-pack/plugins/index_lifecycle_management/{__jest__/client_integration => integration_tests}/edit_policy/features/timeline.test.ts (100%) rename x-pack/plugins/index_lifecycle_management/{__jest__/client_integration => integration_tests}/edit_policy/features/timing.helpers.ts (100%) rename x-pack/plugins/index_lifecycle_management/{__jest__/client_integration => integration_tests}/edit_policy/features/timing.test.ts (95%) rename x-pack/plugins/index_lifecycle_management/{__jest__/client_integration => integration_tests}/edit_policy/form_validation/cold_phase_validation.test.ts (91%) rename x-pack/plugins/index_lifecycle_management/{__jest__/client_integration => integration_tests}/edit_policy/form_validation/downsample_interval.test.ts (93%) rename x-pack/plugins/index_lifecycle_management/{__jest__/client_integration => integration_tests}/edit_policy/form_validation/error_indicators.test.ts (97%) rename x-pack/plugins/index_lifecycle_management/{__jest__/client_integration => integration_tests}/edit_policy/form_validation/hot_phase_validation.test.ts (97%) rename x-pack/plugins/index_lifecycle_management/{__jest__/client_integration => integration_tests}/edit_policy/form_validation/policy_name_validation.test.ts (93%) rename x-pack/plugins/index_lifecycle_management/{__jest__/client_integration => integration_tests}/edit_policy/form_validation/timing.test.ts (93%) rename x-pack/plugins/index_lifecycle_management/{__jest__/client_integration => integration_tests}/edit_policy/form_validation/validation.helpers.ts (100%) rename x-pack/plugins/index_lifecycle_management/{__jest__/client_integration => integration_tests}/edit_policy/form_validation/warm_phase_validation.test.ts (95%) rename x-pack/plugins/index_lifecycle_management/{__jest__/client_integration => integration_tests}/edit_policy/init_test_bed.ts (89%) rename x-pack/plugins/index_lifecycle_management/{__jest__/client_integration => integration_tests}/edit_policy/serialization/policy_serialization.helpers.ts (95%) rename x-pack/plugins/index_lifecycle_management/{__jest__/client_integration => integration_tests}/edit_policy/serialization/policy_serialization.test.ts (99%) rename x-pack/plugins/index_lifecycle_management/{__jest__/client_integration => integration_tests}/helpers/actions/downsample_actions.ts (96%) rename x-pack/plugins/index_lifecycle_management/{__jest__/client_integration => integration_tests}/helpers/actions/errors_actions.ts (96%) rename x-pack/plugins/index_lifecycle_management/{__jest__/client_integration => integration_tests}/helpers/actions/forcemerge_actions.ts (96%) rename x-pack/plugins/index_lifecycle_management/{__jest__/client_integration => integration_tests}/helpers/actions/form_set_value_action.ts (100%) rename x-pack/plugins/index_lifecycle_management/{__jest__/client_integration => integration_tests}/helpers/actions/form_toggle_action.ts (100%) rename x-pack/plugins/index_lifecycle_management/{__jest__/client_integration => integration_tests}/helpers/actions/form_toggle_and_set_value_action.ts (100%) rename x-pack/plugins/index_lifecycle_management/{__jest__/client_integration => integration_tests}/helpers/actions/index.ts (100%) rename x-pack/plugins/index_lifecycle_management/{__jest__/client_integration => integration_tests}/helpers/actions/index_priority_actions.ts (94%) rename x-pack/plugins/index_lifecycle_management/{__jest__/client_integration => integration_tests}/helpers/actions/min_age_actions.ts (94%) rename x-pack/plugins/index_lifecycle_management/{__jest__/client_integration => integration_tests}/helpers/actions/node_allocation_actions.ts (95%) rename x-pack/plugins/index_lifecycle_management/{__jest__/client_integration => integration_tests}/helpers/actions/phases.ts (100%) rename x-pack/plugins/index_lifecycle_management/{__jest__/client_integration => integration_tests}/helpers/actions/readonly_actions.ts (92%) rename x-pack/plugins/index_lifecycle_management/{__jest__/client_integration => integration_tests}/helpers/actions/replicas_action.ts (92%) rename x-pack/plugins/index_lifecycle_management/{__jest__/client_integration => integration_tests}/helpers/actions/request_flyout_actions.ts (100%) rename x-pack/plugins/index_lifecycle_management/{__jest__/client_integration => integration_tests}/helpers/actions/rollover_actions.ts (100%) rename x-pack/plugins/index_lifecycle_management/{__jest__/client_integration => integration_tests}/helpers/actions/save_policy_action.ts (100%) rename x-pack/plugins/index_lifecycle_management/{__jest__/client_integration => integration_tests}/helpers/actions/searchable_snapshot_actions.ts (96%) rename x-pack/plugins/index_lifecycle_management/{__jest__/client_integration => integration_tests}/helpers/actions/shrink_actions.ts (97%) rename x-pack/plugins/index_lifecycle_management/{__jest__/client_integration => integration_tests}/helpers/actions/snapshot_policy_actions.ts (100%) rename x-pack/plugins/index_lifecycle_management/{__jest__/client_integration => integration_tests}/helpers/actions/toggle_phase_action.ts (96%) rename x-pack/plugins/index_lifecycle_management/{__jest__/client_integration => integration_tests}/helpers/global_mocks.tsx (100%) rename x-pack/plugins/index_lifecycle_management/{__jest__/client_integration => integration_tests}/helpers/http_requests.ts (97%) rename x-pack/plugins/index_lifecycle_management/{__jest__/client_integration => integration_tests}/helpers/index.ts (100%) rename x-pack/plugins/index_lifecycle_management/{__jest__/client_integration => integration_tests}/helpers/setup_environment.tsx (80%) create mode 100644 x-pack/plugins/index_lifecycle_management/jest.integration.config.js diff --git a/x-pack/plugins/index_lifecycle_management/README.md b/x-pack/plugins/index_lifecycle_management/README.md index 35c2aa063ec23..912b19295790f 100644 --- a/x-pack/plugins/index_lifecycle_management/README.md +++ b/x-pack/plugins/index_lifecycle_management/README.md @@ -115,4 +115,8 @@ this by running: ```bash yarn es snapshot --license=trial -``` \ No newline at end of file +``` + +## Integration tests + +See `./integration_tests/README.md` \ No newline at end of file diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/extend_index_management.test.tsx b/x-pack/plugins/index_lifecycle_management/__jest__/extend_index_management.test.tsx index ecefe8132c499..246de6e8ed25b 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/extend_index_management.test.tsx +++ b/x-pack/plugins/index_lifecycle_management/__jest__/extend_index_management.test.tsx @@ -7,7 +7,7 @@ import moment from 'moment-timezone'; -import { init } from './client_integration/helpers/http_requests'; +import { init } from '../integration_tests/helpers/http_requests'; import { mountWithIntl } from '@kbn/test-jest-helpers'; import { usageCollectionPluginMock } from '@kbn/usage-collection-plugin/public/mocks'; import { Index } from '../common/types'; diff --git a/x-pack/plugins/index_lifecycle_management/integration_tests/README.md b/x-pack/plugins/index_lifecycle_management/integration_tests/README.md new file mode 100644 index 0000000000000..5e4bf4360cb72 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/integration_tests/README.md @@ -0,0 +1,14 @@ +Most plugins of the deployment management team follow +the similar testing infrastructure where integration tests are located in `__jest__` and run as unit tests. + +The `index_lifecycle_management` tests [were refactored](https://github.com/elastic/kibana/pull/141750) to be run as integration tests because they [became flaky hitting the 5 seconds timeout](https://github.com/elastic/kibana/issues/115307#issuecomment-1238417474) for a jest unit test. + +Jest integration tests are just sit in a different directory and have two main differences: +- They never use parallelism, this allows them to access file system resources, launch services, etc. without needing to worry about conflicts with other tests +- They are allowed to take their sweet time, the default timeout is currently 10 minutes. + +To run these tests use: + +``` +node scripts/jest_integration x-pack/plugins/index_lifecycle_management/ +``` \ No newline at end of file diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/app/app.helpers.ts b/x-pack/plugins/index_lifecycle_management/integration_tests/app/app.helpers.ts similarity index 97% rename from x-pack/plugins/index_lifecycle_management/__jest__/client_integration/app/app.helpers.ts rename to x-pack/plugins/index_lifecycle_management/integration_tests/app/app.helpers.ts index df64fbce3fa1d..66810ebb1e546 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/app/app.helpers.ts +++ b/x-pack/plugins/index_lifecycle_management/integration_tests/app/app.helpers.ts @@ -8,7 +8,7 @@ import { act } from 'react-dom/test-utils'; import { HttpSetup } from '@kbn/core/public'; import { registerTestBed, TestBed, TestBedConfig } from '@kbn/test-jest-helpers'; -import { App } from '../../../public/application/app'; +import { App } from '../../public/application/app'; import { WithAppDependencies } from '../helpers'; const getTestBedConfig = (initialEntries: string[]): TestBedConfig => ({ diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/app/app.test.ts b/x-pack/plugins/index_lifecycle_management/integration_tests/app/app.test.ts similarity index 100% rename from x-pack/plugins/index_lifecycle_management/__jest__/client_integration/app/app.test.ts rename to x-pack/plugins/index_lifecycle_management/integration_tests/app/app.test.ts diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/constants.ts b/x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/constants.ts similarity index 97% rename from x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/constants.ts rename to x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/constants.ts index 620cb9d6f8dde..a41bea1cb8415 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/constants.ts +++ b/x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/constants.ts @@ -7,9 +7,9 @@ import moment from 'moment-timezone'; -import { PolicyFromES } from '../../../common/types'; +import { PolicyFromES } from '../../common/types'; -import { defaultRolloverAction } from '../../../public/application/constants'; +import { defaultRolloverAction } from '../../public/application/constants'; export const POLICY_NAME = 'my_policy'; export const SNAPSHOT_POLICY_NAME = 'my_snapshot_policy'; diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/delete_phase.helpers.ts b/x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/features/delete_phase.helpers.ts similarity index 100% rename from x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/delete_phase.helpers.ts rename to x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/features/delete_phase.helpers.ts diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/delete_phase.test.ts b/x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/features/delete_phase.test.ts similarity index 98% rename from x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/delete_phase.test.ts rename to x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/features/delete_phase.test.ts index d877f05d06ae1..df6dd516c8a58 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/delete_phase.test.ts +++ b/x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/features/delete_phase.test.ts @@ -6,7 +6,7 @@ */ import { act } from 'react-dom/test-utils'; -import { API_BASE_PATH } from '../../../../common/constants'; +import { API_BASE_PATH } from '../../../common/constants'; import { setupEnvironment } from '../../helpers'; import { DELETE_PHASE_POLICY, diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/downsample.helpers.ts b/x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/features/downsample.helpers.ts similarity index 95% rename from x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/downsample.helpers.ts rename to x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/features/downsample.helpers.ts index be96aaabe4a71..1e6ed336a5f03 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/downsample.helpers.ts +++ b/x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/features/downsample.helpers.ts @@ -14,7 +14,7 @@ import { createTogglePhaseAction, } from '../../helpers'; import { initTestBed } from '../init_test_bed'; -import { AppServicesContext } from '../../../../public/types'; +import { AppServicesContext } from '../../../public/types'; type SetupReturn = ReturnType; diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/downsample.test.ts b/x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/features/downsample.test.ts similarity index 100% rename from x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/downsample.test.ts rename to x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/features/downsample.test.ts diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/edit_warning.test.ts b/x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/features/edit_warning.test.ts similarity index 100% rename from x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/edit_warning.test.ts rename to x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/features/edit_warning.test.ts diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/frozen_phase.test.ts b/x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/features/frozen_phase.test.ts similarity index 100% rename from x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/frozen_phase.test.ts rename to x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/features/frozen_phase.test.ts diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/node_allocation/cloud_aware_behavior.helpers.ts b/x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/features/node_allocation/cloud_aware_behavior.helpers.ts similarity index 94% rename from x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/node_allocation/cloud_aware_behavior.helpers.ts rename to x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/features/node_allocation/cloud_aware_behavior.helpers.ts index 7092d52289de9..b41075c6b6b68 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/node_allocation/cloud_aware_behavior.helpers.ts +++ b/x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/features/node_allocation/cloud_aware_behavior.helpers.ts @@ -8,7 +8,7 @@ import { HttpSetup } from '@kbn/core/public'; import { TestBedConfig } from '@kbn/test-jest-helpers'; -import { AppServicesContext } from '../../../../../public/types'; +import { AppServicesContext } from '../../../../public/types'; import { createTogglePhaseAction, createNodeAllocationActions } from '../../../helpers'; import { initTestBed } from '../../init_test_bed'; diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/node_allocation/cloud_aware_behavior.test.ts b/x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/features/node_allocation/cloud_aware_behavior.test.ts similarity index 100% rename from x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/node_allocation/cloud_aware_behavior.test.ts rename to x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/features/node_allocation/cloud_aware_behavior.test.ts diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/node_allocation/cold_phase.helpers.ts b/x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/features/node_allocation/cold_phase.helpers.ts similarity index 100% rename from x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/node_allocation/cold_phase.helpers.ts rename to x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/features/node_allocation/cold_phase.helpers.ts diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/node_allocation/cold_phase.test.ts b/x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/features/node_allocation/cold_phase.test.ts similarity index 100% rename from x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/node_allocation/cold_phase.test.ts rename to x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/features/node_allocation/cold_phase.test.ts diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/node_allocation/general_behavior.helpers.ts b/x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/features/node_allocation/general_behavior.helpers.ts similarity index 100% rename from x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/node_allocation/general_behavior.helpers.ts rename to x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/features/node_allocation/general_behavior.helpers.ts diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/node_allocation/general_behavior.test.ts b/x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/features/node_allocation/general_behavior.test.ts similarity index 98% rename from x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/node_allocation/general_behavior.test.ts rename to x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/features/node_allocation/general_behavior.test.ts index 1eecd5207664f..4830cee8ee237 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/node_allocation/general_behavior.test.ts +++ b/x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/features/node_allocation/general_behavior.test.ts @@ -17,7 +17,7 @@ import { POLICY_WITH_NODE_ATTR_AND_OFF_ALLOCATION, POLICY_WITH_NODE_ROLE_ALLOCATION, } from '../../constants'; -import { API_BASE_PATH } from '../../../../../common/constants'; +import { API_BASE_PATH } from '../../../../common/constants'; describe(' node allocation general behavior', () => { let testBed: GeneralNodeAllocationTestBed; diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/node_allocation/warm_phase.helpers.ts b/x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/features/node_allocation/warm_phase.helpers.ts similarity index 100% rename from x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/node_allocation/warm_phase.helpers.ts rename to x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/features/node_allocation/warm_phase.helpers.ts diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/node_allocation/warm_phase.test.ts b/x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/features/node_allocation/warm_phase.test.ts similarity index 100% rename from x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/node_allocation/warm_phase.test.ts rename to x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/features/node_allocation/warm_phase.test.ts diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/request_flyout.helpers.ts b/x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/features/request_flyout.helpers.ts similarity index 100% rename from x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/request_flyout.helpers.ts rename to x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/features/request_flyout.helpers.ts diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/request_flyout.test.ts b/x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/features/request_flyout.test.ts similarity index 100% rename from x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/request_flyout.test.ts rename to x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/features/request_flyout.test.ts diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/rollover.helpers.ts b/x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/features/rollover.helpers.ts similarity index 100% rename from x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/rollover.helpers.ts rename to x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/features/rollover.helpers.ts diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/rollover.test.ts b/x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/features/rollover.test.ts similarity index 100% rename from x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/rollover.test.ts rename to x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/features/rollover.test.ts diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/searchable_snapshots.helpers.ts b/x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/features/searchable_snapshots.helpers.ts similarity index 97% rename from x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/searchable_snapshots.helpers.ts rename to x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/features/searchable_snapshots.helpers.ts index e70f721a48075..d3b68ffc6a13e 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/searchable_snapshots.helpers.ts +++ b/x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/features/searchable_snapshots.helpers.ts @@ -18,7 +18,7 @@ import { createTogglePhaseAction, } from '../../helpers'; import { initTestBed } from '../init_test_bed'; -import { AppServicesContext } from '../../../../public/types'; +import { AppServicesContext } from '../../../public/types'; type SetupReturn = ReturnType; diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/searchable_snapshots.test.ts b/x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/features/searchable_snapshots.test.ts similarity index 99% rename from x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/searchable_snapshots.test.ts rename to x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/features/searchable_snapshots.test.ts index 03b7670c1eac5..68e74e23a781c 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/searchable_snapshots.test.ts +++ b/x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/features/searchable_snapshots.test.ts @@ -10,7 +10,7 @@ import { licensingMock } from '@kbn/licensing-plugin/public/mocks'; import { HttpFetchOptionsWithPath } from '@kbn/core/public'; import { setupEnvironment } from '../../helpers'; import { getDefaultHotPhasePolicy } from '../constants'; -import { API_BASE_PATH } from '../../../../common/constants'; +import { API_BASE_PATH } from '../../../common/constants'; import { SearchableSnapshotsTestBed, setupSearchableSnapshotsTestBed, diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/timeline.helpers.ts b/x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/features/timeline.helpers.ts similarity index 94% rename from x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/timeline.helpers.ts rename to x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/features/timeline.helpers.ts index 496b27330c931..202388a844464 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/timeline.helpers.ts +++ b/x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/features/timeline.helpers.ts @@ -8,7 +8,7 @@ import { HttpSetup } from '@kbn/core/public'; import { createTogglePhaseAction } from '../../helpers'; import { initTestBed } from '../init_test_bed'; -import { Phase } from '../../../../common/types'; +import { Phase } from '../../../common/types'; type SetupReturn = ReturnType; diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/timeline.test.ts b/x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/features/timeline.test.ts similarity index 100% rename from x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/timeline.test.ts rename to x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/features/timeline.test.ts diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/timing.helpers.ts b/x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/features/timing.helpers.ts similarity index 100% rename from x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/timing.helpers.ts rename to x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/features/timing.helpers.ts diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/timing.test.ts b/x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/features/timing.test.ts similarity index 95% rename from x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/timing.test.ts rename to x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/features/timing.test.ts index 0aee8eb5f0be1..72c6a2a4e789c 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/timing.test.ts +++ b/x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/features/timing.test.ts @@ -8,7 +8,7 @@ import { act } from 'react-dom/test-utils'; import { setupEnvironment } from '../../helpers'; import { setupTimingTestBed, TimingTestBed } from './timing.helpers'; -import { PhaseWithTiming } from '../../../../common/types'; +import { PhaseWithTiming } from '../../../common/types'; describe(' timing', () => { let testBed: TimingTestBed; diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/cold_phase_validation.test.ts b/x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/form_validation/cold_phase_validation.test.ts similarity index 91% rename from x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/cold_phase_validation.test.ts rename to x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/form_validation/cold_phase_validation.test.ts index ef2fc67002c14..e75d2cb72ab28 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/cold_phase_validation.test.ts +++ b/x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/form_validation/cold_phase_validation.test.ts @@ -6,12 +6,11 @@ */ import { act } from 'react-dom/test-utils'; -import { i18nTexts } from '../../../../public/application/sections/edit_policy/i18n_texts'; +import { i18nTexts } from '../../../public/application/sections/edit_policy/i18n_texts'; import { setupEnvironment } from '../../helpers'; import { setupValidationTestBed, ValidationTestBed } from './validation.helpers'; -// FLAKY: https://github.com/elastic/kibana/issues/141645 -describe.skip(' cold phase validation', () => { +describe(' cold phase validation', () => { let testBed: ValidationTestBed; const { httpSetup, httpRequestsMockHelpers } = setupEnvironment(); diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/downsample_interval.test.ts b/x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/form_validation/downsample_interval.test.ts similarity index 93% rename from x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/downsample_interval.test.ts rename to x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/form_validation/downsample_interval.test.ts index 79f5fdc6e2840..d2f9943eba68a 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/downsample_interval.test.ts +++ b/x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/form_validation/downsample_interval.test.ts @@ -6,14 +6,13 @@ */ import { act } from 'react-dom/test-utils'; -import { i18nTexts } from '../../../../public/application/sections/edit_policy/i18n_texts'; +import { i18nTexts } from '../../../public/application/sections/edit_policy/i18n_texts'; -import { PhaseWithDownsample } from '../../../../common/types'; +import { PhaseWithDownsample } from '../../../common/types'; import { setupEnvironment } from '../../helpers'; import { setupValidationTestBed, ValidationTestBed } from './validation.helpers'; -// FLAKY: https://github.com/elastic/kibana/issues/141160 -describe.skip(' downsample interval validation', () => { +describe(' downsample interval validation', () => { let testBed: ValidationTestBed; let actions: ValidationTestBed['actions']; const { httpSetup, httpRequestsMockHelpers } = setupEnvironment(); diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/error_indicators.test.ts b/x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/form_validation/error_indicators.test.ts similarity index 97% rename from x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/error_indicators.test.ts rename to x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/form_validation/error_indicators.test.ts index 349f98766620d..bd4a2caec0be5 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/error_indicators.test.ts +++ b/x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/form_validation/error_indicators.test.ts @@ -9,8 +9,7 @@ import { act } from 'react-dom/test-utils'; import { setupEnvironment } from '../../helpers'; import { setupValidationTestBed, ValidationTestBed } from './validation.helpers'; -// FLAKY: https://github.com/elastic/kibana/issues/141645 -describe.skip(' error indicators', () => { +describe(' error indicators', () => { let testBed: ValidationTestBed; const { httpSetup, httpRequestsMockHelpers } = setupEnvironment(); diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/hot_phase_validation.test.ts b/x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/form_validation/hot_phase_validation.test.ts similarity index 97% rename from x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/hot_phase_validation.test.ts rename to x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/form_validation/hot_phase_validation.test.ts index 82b3568d39ce7..71f83a59360d6 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/hot_phase_validation.test.ts +++ b/x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/form_validation/hot_phase_validation.test.ts @@ -6,12 +6,11 @@ */ import { act } from 'react-dom/test-utils'; -import { i18nTexts } from '../../../../public/application/sections/edit_policy/i18n_texts'; +import { i18nTexts } from '../../../public/application/sections/edit_policy/i18n_texts'; import { setupEnvironment } from '../../helpers'; import { setupValidationTestBed, ValidationTestBed } from './validation.helpers'; -// FLAKY: https://github.com/elastic/kibana/issues/141645 -describe.skip(' hot phase validation', () => { +describe(' hot phase validation', () => { let testBed: ValidationTestBed; let actions: ValidationTestBed['actions']; const { httpSetup, httpRequestsMockHelpers } = setupEnvironment(); diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/policy_name_validation.test.ts b/x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/form_validation/policy_name_validation.test.ts similarity index 93% rename from x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/policy_name_validation.test.ts rename to x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/form_validation/policy_name_validation.test.ts index 928380e3d1eac..c530f73a66c11 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/policy_name_validation.test.ts +++ b/x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/form_validation/policy_name_validation.test.ts @@ -6,13 +6,12 @@ */ import { act } from 'react-dom/test-utils'; -import { i18nTexts } from '../../../../public/application/sections/edit_policy/i18n_texts'; +import { i18nTexts } from '../../../public/application/sections/edit_policy/i18n_texts'; import { setupEnvironment } from '../../helpers'; import { getGeneratedPolicies } from '../constants'; import { setupValidationTestBed, ValidationTestBed } from './validation.helpers'; -// FLAKY: https://github.com/elastic/kibana/issues/141645 -describe.skip(' policy name validation', () => { +describe(' policy name validation', () => { let testBed: ValidationTestBed; let actions: ValidationTestBed['actions']; const { httpSetup, httpRequestsMockHelpers } = setupEnvironment(); diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/timing.test.ts b/x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/form_validation/timing.test.ts similarity index 93% rename from x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/timing.test.ts rename to x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/form_validation/timing.test.ts index 7db483d6d0ef2..5838f04ba70e9 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/timing.test.ts +++ b/x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/form_validation/timing.test.ts @@ -6,14 +6,13 @@ */ import { act } from 'react-dom/test-utils'; -import { i18nTexts } from '../../../../public/application/sections/edit_policy/i18n_texts'; +import { i18nTexts } from '../../../public/application/sections/edit_policy/i18n_texts'; -import { PhaseWithTiming } from '../../../../common/types'; +import { PhaseWithTiming } from '../../../common/types'; import { setupEnvironment } from '../../helpers'; import { setupValidationTestBed, ValidationTestBed } from './validation.helpers'; -// FLAKY: https://github.com/elastic/kibana/issues/115307 -describe.skip(' timing validation', () => { +describe(' timing validation', () => { let testBed: ValidationTestBed; let actions: ValidationTestBed['actions']; const { httpSetup, httpRequestsMockHelpers } = setupEnvironment(); diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/validation.helpers.ts b/x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/form_validation/validation.helpers.ts similarity index 100% rename from x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/validation.helpers.ts rename to x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/form_validation/validation.helpers.ts diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/warm_phase_validation.test.ts b/x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/form_validation/warm_phase_validation.test.ts similarity index 95% rename from x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/warm_phase_validation.test.ts rename to x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/form_validation/warm_phase_validation.test.ts index df0607a0a0e65..47917b1f8e3d7 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/warm_phase_validation.test.ts +++ b/x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/form_validation/warm_phase_validation.test.ts @@ -6,12 +6,11 @@ */ import { act } from 'react-dom/test-utils'; -import { i18nTexts } from '../../../../public/application/sections/edit_policy/i18n_texts'; +import { i18nTexts } from '../../../public/application/sections/edit_policy/i18n_texts'; import { setupEnvironment } from '../../helpers'; import { setupValidationTestBed, ValidationTestBed } from './validation.helpers'; -// FLAKY: https://github.com/elastic/kibana/issues/141645 -describe.skip(' warm phase validation', () => { +describe(' warm phase validation', () => { let testBed: ValidationTestBed; const { httpSetup, httpRequestsMockHelpers } = setupEnvironment(); diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/init_test_bed.ts b/x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/init_test_bed.ts similarity index 89% rename from x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/init_test_bed.ts rename to x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/init_test_bed.ts index 875bf9db36264..56bed7a6e50aa 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/init_test_bed.ts +++ b/x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/init_test_bed.ts @@ -5,12 +5,12 @@ * 2.0. */ +import { HttpSetup } from '@kbn/core/public'; import { registerTestBed, TestBedConfig } from '@kbn/test-jest-helpers'; -import { HttpSetup } from '@kbn/core/public'; import { WithAppDependencies } from '../helpers'; -import { EditPolicy } from '../../../public/application/sections/edit_policy'; -import { AppServicesContext } from '../../../public/types'; +import { EditPolicy } from '../../public/application/sections/edit_policy'; +import { AppServicesContext } from '../../public/types'; import { POLICY_NAME } from './constants'; const getTestBedConfig = (testBedConfig?: Partial): TestBedConfig => { diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/serialization/policy_serialization.helpers.ts b/x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/serialization/policy_serialization.helpers.ts similarity index 95% rename from x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/serialization/policy_serialization.helpers.ts rename to x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/serialization/policy_serialization.helpers.ts index 417f53c63a20c..e439fca0de512 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/serialization/policy_serialization.helpers.ts +++ b/x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/serialization/policy_serialization.helpers.ts @@ -6,7 +6,7 @@ */ import { HttpSetup } from '@kbn/core/public'; -import { AppServicesContext } from '../../../../public/types'; +import { AppServicesContext } from '../../../public/types'; import { createColdPhaseActions, createDeletePhaseActions, diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/serialization/policy_serialization.test.ts b/x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/serialization/policy_serialization.test.ts similarity index 99% rename from x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/serialization/policy_serialization.test.ts rename to x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/serialization/policy_serialization.test.ts index 983cfbadfa017..05aa66fcc5f25 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/serialization/policy_serialization.test.ts +++ b/x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/serialization/policy_serialization.test.ts @@ -9,7 +9,7 @@ import { act } from 'react-dom/test-utils'; import { licensingMock } from '@kbn/licensing-plugin/public/mocks'; import { HttpFetchOptionsWithPath } from '@kbn/core/public'; import { setupEnvironment } from '../../helpers'; -import { API_BASE_PATH } from '../../../../common/constants'; +import { API_BASE_PATH } from '../../../common/constants'; import { getDefaultHotPhasePolicy, POLICY_WITH_INCLUDE_EXCLUDE, diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/downsample_actions.ts b/x-pack/plugins/index_lifecycle_management/integration_tests/helpers/actions/downsample_actions.ts similarity index 96% rename from x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/downsample_actions.ts rename to x-pack/plugins/index_lifecycle_management/integration_tests/helpers/actions/downsample_actions.ts index 315ed3d58520a..a389a9deebe32 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/downsample_actions.ts +++ b/x-pack/plugins/index_lifecycle_management/integration_tests/helpers/actions/downsample_actions.ts @@ -5,9 +5,9 @@ * 2.0. */ -import { TestBed } from '@kbn/test-jest-helpers'; import { act } from 'react-dom/test-utils'; -import { Phase } from '../../../../common/types'; +import { TestBed } from '@kbn/test-jest-helpers'; +import { Phase } from '../../../common/types'; import { createFormToggleAction } from '..'; const createSetDownsampleIntervalAction = diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/errors_actions.ts b/x-pack/plugins/index_lifecycle_management/integration_tests/helpers/actions/errors_actions.ts similarity index 96% rename from x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/errors_actions.ts rename to x-pack/plugins/index_lifecycle_management/integration_tests/helpers/actions/errors_actions.ts index 4b863071e191c..18f07734fadee 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/errors_actions.ts +++ b/x-pack/plugins/index_lifecycle_management/integration_tests/helpers/actions/errors_actions.ts @@ -7,7 +7,7 @@ import { act } from 'react-dom/test-utils'; import { TestBed } from '@kbn/test-jest-helpers'; -import { Phase } from '../../../../common/types'; +import { Phase } from '../../../common/types'; const createWaitForValidationAction = (testBed: TestBed) => () => { const { component } = testBed; diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/forcemerge_actions.ts b/x-pack/plugins/index_lifecycle_management/integration_tests/helpers/actions/forcemerge_actions.ts similarity index 96% rename from x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/forcemerge_actions.ts rename to x-pack/plugins/index_lifecycle_management/integration_tests/helpers/actions/forcemerge_actions.ts index b6ed40de36ca9..619d6bd8be85f 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/forcemerge_actions.ts +++ b/x-pack/plugins/index_lifecycle_management/integration_tests/helpers/actions/forcemerge_actions.ts @@ -7,7 +7,7 @@ import { act } from 'react-dom/test-utils'; import { TestBed } from '@kbn/test-jest-helpers'; -import { Phase } from '../../../../common/types'; +import { Phase } from '../../../common/types'; import { createFormToggleAction } from './form_toggle_action'; import { createFormSetValueAction } from './form_set_value_action'; diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/form_set_value_action.ts b/x-pack/plugins/index_lifecycle_management/integration_tests/helpers/actions/form_set_value_action.ts similarity index 100% rename from x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/form_set_value_action.ts rename to x-pack/plugins/index_lifecycle_management/integration_tests/helpers/actions/form_set_value_action.ts diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/form_toggle_action.ts b/x-pack/plugins/index_lifecycle_management/integration_tests/helpers/actions/form_toggle_action.ts similarity index 100% rename from x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/form_toggle_action.ts rename to x-pack/plugins/index_lifecycle_management/integration_tests/helpers/actions/form_toggle_action.ts diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/form_toggle_and_set_value_action.ts b/x-pack/plugins/index_lifecycle_management/integration_tests/helpers/actions/form_toggle_and_set_value_action.ts similarity index 100% rename from x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/form_toggle_and_set_value_action.ts rename to x-pack/plugins/index_lifecycle_management/integration_tests/helpers/actions/form_toggle_and_set_value_action.ts diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/index.ts b/x-pack/plugins/index_lifecycle_management/integration_tests/helpers/actions/index.ts similarity index 100% rename from x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/index.ts rename to x-pack/plugins/index_lifecycle_management/integration_tests/helpers/actions/index.ts diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/index_priority_actions.ts b/x-pack/plugins/index_lifecycle_management/integration_tests/helpers/actions/index_priority_actions.ts similarity index 94% rename from x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/index_priority_actions.ts rename to x-pack/plugins/index_lifecycle_management/integration_tests/helpers/actions/index_priority_actions.ts index 79fb77e53cc59..4c6e4604be7d4 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/index_priority_actions.ts +++ b/x-pack/plugins/index_lifecycle_management/integration_tests/helpers/actions/index_priority_actions.ts @@ -6,7 +6,7 @@ */ import { TestBed } from '@kbn/test-jest-helpers'; -import { Phase } from '../../../../common/types'; +import { Phase } from '../../../common/types'; import { createFormToggleAction } from './form_toggle_action'; import { createFormToggleAndSetValueAction } from './form_toggle_and_set_value_action'; diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/min_age_actions.ts b/x-pack/plugins/index_lifecycle_management/integration_tests/helpers/actions/min_age_actions.ts similarity index 94% rename from x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/min_age_actions.ts rename to x-pack/plugins/index_lifecycle_management/integration_tests/helpers/actions/min_age_actions.ts index ef00fdc8d7757..8b4fa2e5f6179 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/min_age_actions.ts +++ b/x-pack/plugins/index_lifecycle_management/integration_tests/helpers/actions/min_age_actions.ts @@ -6,7 +6,7 @@ */ import { TestBed } from '@kbn/test-jest-helpers'; -import { Phase } from '../../../../common/types'; +import { Phase } from '../../../common/types'; import { createFormSetValueAction } from './form_set_value_action'; export const createMinAgeActions = (testBed: TestBed, phase: Phase) => { diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/node_allocation_actions.ts b/x-pack/plugins/index_lifecycle_management/integration_tests/helpers/actions/node_allocation_actions.ts similarity index 95% rename from x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/node_allocation_actions.ts rename to x-pack/plugins/index_lifecycle_management/integration_tests/helpers/actions/node_allocation_actions.ts index 2a680590654a8..a3cb607b60f99 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/node_allocation_actions.ts +++ b/x-pack/plugins/index_lifecycle_management/integration_tests/helpers/actions/node_allocation_actions.ts @@ -8,8 +8,8 @@ import { act } from 'react-dom/test-utils'; import { TestBed } from '@kbn/test-jest-helpers'; -import { DataTierAllocationType } from '../../../../public/application/sections/edit_policy/types'; -import { Phase } from '../../../../common/types'; +import { DataTierAllocationType } from '../../../public/application/sections/edit_policy/types'; +import { Phase } from '../../../common/types'; import { createFormSetValueAction } from './form_set_value_action'; export const createNodeAllocationActions = (testBed: TestBed, phase: Phase) => { diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/phases.ts b/x-pack/plugins/index_lifecycle_management/integration_tests/helpers/actions/phases.ts similarity index 100% rename from x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/phases.ts rename to x-pack/plugins/index_lifecycle_management/integration_tests/helpers/actions/phases.ts diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/readonly_actions.ts b/x-pack/plugins/index_lifecycle_management/integration_tests/helpers/actions/readonly_actions.ts similarity index 92% rename from x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/readonly_actions.ts rename to x-pack/plugins/index_lifecycle_management/integration_tests/helpers/actions/readonly_actions.ts index 1a7ec56815b00..fe4c71d76265a 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/readonly_actions.ts +++ b/x-pack/plugins/index_lifecycle_management/integration_tests/helpers/actions/readonly_actions.ts @@ -6,7 +6,7 @@ */ import { TestBed } from '@kbn/test-jest-helpers'; -import { Phase } from '../../../../common/types'; +import { Phase } from '../../../common/types'; import { createFormToggleAction } from './form_toggle_action'; export const createReadonlyActions = (testBed: TestBed, phase: Phase) => { diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/replicas_action.ts b/x-pack/plugins/index_lifecycle_management/integration_tests/helpers/actions/replicas_action.ts similarity index 92% rename from x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/replicas_action.ts rename to x-pack/plugins/index_lifecycle_management/integration_tests/helpers/actions/replicas_action.ts index 43eca26a4e1ce..35c3afc607513 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/replicas_action.ts +++ b/x-pack/plugins/index_lifecycle_management/integration_tests/helpers/actions/replicas_action.ts @@ -6,7 +6,7 @@ */ import { TestBed } from '@kbn/test-jest-helpers'; -import { Phase } from '../../../../common/types'; +import { Phase } from '../../../common/types'; import { createFormToggleAndSetValueAction } from './form_toggle_and_set_value_action'; export const createReplicasAction = (testBed: TestBed, phase: Phase) => { diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/request_flyout_actions.ts b/x-pack/plugins/index_lifecycle_management/integration_tests/helpers/actions/request_flyout_actions.ts similarity index 100% rename from x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/request_flyout_actions.ts rename to x-pack/plugins/index_lifecycle_management/integration_tests/helpers/actions/request_flyout_actions.ts diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/rollover_actions.ts b/x-pack/plugins/index_lifecycle_management/integration_tests/helpers/actions/rollover_actions.ts similarity index 100% rename from x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/rollover_actions.ts rename to x-pack/plugins/index_lifecycle_management/integration_tests/helpers/actions/rollover_actions.ts diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/save_policy_action.ts b/x-pack/plugins/index_lifecycle_management/integration_tests/helpers/actions/save_policy_action.ts similarity index 100% rename from x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/save_policy_action.ts rename to x-pack/plugins/index_lifecycle_management/integration_tests/helpers/actions/save_policy_action.ts diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/searchable_snapshot_actions.ts b/x-pack/plugins/index_lifecycle_management/integration_tests/helpers/actions/searchable_snapshot_actions.ts similarity index 96% rename from x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/searchable_snapshot_actions.ts rename to x-pack/plugins/index_lifecycle_management/integration_tests/helpers/actions/searchable_snapshot_actions.ts index 3efffcddbece6..c9a019c2bc842 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/searchable_snapshot_actions.ts +++ b/x-pack/plugins/index_lifecycle_management/integration_tests/helpers/actions/searchable_snapshot_actions.ts @@ -7,7 +7,7 @@ import { act } from 'react-dom/test-utils'; import { TestBed } from '@kbn/test-jest-helpers'; -import { Phase } from '../../../../common/types'; +import { Phase } from '../../../common/types'; import { createFormToggleAction } from './form_toggle_action'; export const createSearchableSnapshotActions = (testBed: TestBed, phase: Phase) => { diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/shrink_actions.ts b/x-pack/plugins/index_lifecycle_management/integration_tests/helpers/actions/shrink_actions.ts similarity index 97% rename from x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/shrink_actions.ts rename to x-pack/plugins/index_lifecycle_management/integration_tests/helpers/actions/shrink_actions.ts index def20f73b82fe..4bf39185b8c03 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/shrink_actions.ts +++ b/x-pack/plugins/index_lifecycle_management/integration_tests/helpers/actions/shrink_actions.ts @@ -7,7 +7,7 @@ import { TestBed } from '@kbn/test-jest-helpers'; import { act } from 'react-dom/test-utils'; -import { Phase } from '../../../../common/types'; +import { Phase } from '../../../common/types'; import { createFormSetValueAction } from './form_set_value_action'; export const createShrinkActions = (testBed: TestBed, phase: Phase) => { diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/snapshot_policy_actions.ts b/x-pack/plugins/index_lifecycle_management/integration_tests/helpers/actions/snapshot_policy_actions.ts similarity index 100% rename from x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/snapshot_policy_actions.ts rename to x-pack/plugins/index_lifecycle_management/integration_tests/helpers/actions/snapshot_policy_actions.ts diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/toggle_phase_action.ts b/x-pack/plugins/index_lifecycle_management/integration_tests/helpers/actions/toggle_phase_action.ts similarity index 96% rename from x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/toggle_phase_action.ts rename to x-pack/plugins/index_lifecycle_management/integration_tests/helpers/actions/toggle_phase_action.ts index c22efae87d5ac..fc89332e47a67 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/toggle_phase_action.ts +++ b/x-pack/plugins/index_lifecycle_management/integration_tests/helpers/actions/toggle_phase_action.ts @@ -8,7 +8,7 @@ import { TestBed } from '@kbn/test-jest-helpers'; import { act } from 'react-dom/test-utils'; -import { Phase } from '../../../../common/types'; +import { Phase } from '../../../common/types'; const toggleDeletePhase = async (testBed: TestBed) => { const { find, component } = testBed; diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/global_mocks.tsx b/x-pack/plugins/index_lifecycle_management/integration_tests/helpers/global_mocks.tsx similarity index 100% rename from x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/global_mocks.tsx rename to x-pack/plugins/index_lifecycle_management/integration_tests/helpers/global_mocks.tsx diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/http_requests.ts b/x-pack/plugins/index_lifecycle_management/integration_tests/helpers/http_requests.ts similarity index 97% rename from x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/http_requests.ts rename to x-pack/plugins/index_lifecycle_management/integration_tests/helpers/http_requests.ts index 7ddcd5358404f..70e85c8bc5df4 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/http_requests.ts +++ b/x-pack/plugins/index_lifecycle_management/integration_tests/helpers/http_requests.ts @@ -6,12 +6,12 @@ */ import { httpServiceMock } from '@kbn/core/public/mocks'; -import { API_BASE_PATH } from '../../../common/constants'; +import { API_BASE_PATH } from '../../common/constants'; import { ListNodesRouteResponse, ListSnapshotReposResponse, NodesDetailsResponse, -} from '../../../common/types'; +} from '../../common/types'; import { getDefaultHotPhasePolicy } from '../edit_policy/constants'; type HttpMethod = 'GET' | 'PUT' | 'DELETE' | 'POST'; diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/index.ts b/x-pack/plugins/index_lifecycle_management/integration_tests/helpers/index.ts similarity index 100% rename from x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/index.ts rename to x-pack/plugins/index_lifecycle_management/integration_tests/helpers/index.ts diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/setup_environment.tsx b/x-pack/plugins/index_lifecycle_management/integration_tests/helpers/setup_environment.tsx similarity index 80% rename from x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/setup_environment.tsx rename to x-pack/plugins/index_lifecycle_management/integration_tests/helpers/setup_environment.tsx index 055097c883e94..91aebb485ea7f 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/setup_environment.tsx +++ b/x-pack/plugins/index_lifecycle_management/integration_tests/helpers/setup_environment.tsx @@ -19,12 +19,12 @@ import { executionContextServiceMock, } from '@kbn/core/public/mocks'; import { licensingMock } from '@kbn/licensing-plugin/public/mocks'; -import { init as initHttp } from '../../../public/application/services/http'; +import { init as initHttp } from '../../public/application/services/http'; import { init as initHttpRequests } from './http_requests'; -import { init as initUiMetric } from '../../../public/application/services/ui_metric'; -import { init as initNotification } from '../../../public/application/services/notification'; -import { KibanaContextProvider } from '../../../public/shared_imports'; -import { createBreadcrumbsMock } from '../../../public/application/services/breadcrumbs.mock'; +import { init as initUiMetric } from '../../public/application/services/ui_metric'; +import { init as initNotification } from '../../public/application/services/notification'; +import { KibanaContextProvider } from '../../public/shared_imports'; +import { createBreadcrumbsMock } from '../../public/application/services/breadcrumbs.mock'; const breadcrumbService = createBreadcrumbsMock(); const appContextMock = { diff --git a/x-pack/plugins/index_lifecycle_management/jest.integration.config.js b/x-pack/plugins/index_lifecycle_management/jest.integration.config.js new file mode 100644 index 0000000000000..6d1ac5ec2fd8a --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/jest.integration.config.js @@ -0,0 +1,19 @@ +/* + * 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. + */ + +module.exports = { + preset: '@kbn/test/jest_integration', + rootDir: '../../..', + roots: ['/x-pack/plugins/index_lifecycle_management'], + testMatch: ['/**/integration_tests/**/*.test.{js,mjs,ts,tsx}'], + coverageDirectory: + '/target/kibana-coverage/jest/x-pack/plugins/index_lifecycle_management', + coverageReporters: ['text', 'html'], + collectCoverageFrom: [ + '/x-pack/plugins/index_lifecycle_management/{common,public,server}/**/*.{ts,tsx}', + ], +}; diff --git a/x-pack/plugins/index_lifecycle_management/tsconfig.json b/x-pack/plugins/index_lifecycle_management/tsconfig.json index d3a342e110211..4b5d7657ed9f6 100644 --- a/x-pack/plugins/index_lifecycle_management/tsconfig.json +++ b/x-pack/plugins/index_lifecycle_management/tsconfig.json @@ -8,6 +8,7 @@ }, "include": [ "__jest__/**/*", + "integration_tests/**/*", "common/**/*", "public/**/*", "server/**/*", From 0fbbd4f18ae78e4346366152192c4b2e0455ba24 Mon Sep 17 00:00:00 2001 From: Ying Mao Date: Wed, 28 Sep 2022 07:35:51 -0400 Subject: [PATCH 08/17] [Alerting][Docs] Adding link to ES docs for CCS setup (#141995) * Adding link to ES docs * Adding link to ES docs * Apply suggestions from code review Co-authored-by: Lisa Cawley Co-authored-by: Lisa Cawley --- docs/user/alerting/alerting-setup.asciidoc | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/user/alerting/alerting-setup.asciidoc b/docs/user/alerting/alerting-setup.asciidoc index 819f20005d7a4..36c8b46e7b801 100644 --- a/docs/user/alerting/alerting-setup.asciidoc +++ b/docs/user/alerting/alerting-setup.asciidoc @@ -98,3 +98,10 @@ user without those privileges updates the rule, the rule will no longer function. Conversely, if a user with greater or administrator privileges modifies the rule, it will begin running with increased privileges. ============================================== + +[float] +[[alerting-ccs-setup]] +=== {ccs-cap} + +If you want to use alerting rules with {ccs}, you must configure +{ref}/remote-clusters-privileges.html#clusters-privileges-ccs-kibana[privileges for {ccs-init} and {kib}]. \ No newline at end of file From 82492c791a3e1c27a88b8ceeecbe7a1d35c20ca5 Mon Sep 17 00:00:00 2001 From: jennypavlova Date: Wed, 28 Sep 2022 13:45:14 +0200 Subject: [PATCH 09/17] [Infra Monitoring UI] Change infrastructure pages titles to use breadcrumbs #135874 (#140824) * Use breadcrumbs to set page title intead of DocumentTitle * Remove document title component and use breadcrumbs - Logs Page * Remove no longer needed document title translations * Use docTitle.change in error page and remove document title component * Add document title check to the functional tests * Move test to check title after load * test: Title change * Remove unnecessary docTitle type * Add error page tests * Update snapshot * Functional tests: Wait for loading the header before checking the title * Change test to use react testing library Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../plugins/infra/public/apps/metrics_app.tsx | 1 + .../public/components/document_title.tsx | 72 ------------------- .../infra/public/hooks/use_breadcrumbs.ts | 9 ++- .../infra/public/hooks/use_document_title.tsx | 25 +++++++ .../infra/public/pages/logs/page_content.tsx | 3 - .../infra/public/pages/logs/stream/page.tsx | 2 - .../public/pages/logs/stream/page_header.tsx | 28 -------- .../public/pages/metrics/hosts/index.tsx | 13 ---- .../infra/public/pages/metrics/index.tsx | 7 -- .../pages/metrics/inventory_view/index.tsx | 14 ---- .../components/page_error.test.tsx | 47 ++++++++++++ .../metric_detail/components/page_error.tsx | 16 ++--- .../pages/metrics/metric_detail/index.tsx | 9 --- .../pages/metrics/metrics_explorer/index.tsx | 11 --- x-pack/plugins/infra/public/translations.ts | 4 ++ .../translations/translations/fr-FR.json | 7 +- .../translations/translations/ja-JP.json | 7 +- .../translations/translations/zh-CN.json | 7 +- .../test/functional/apps/infra/home_page.ts | 26 ++++++- x-pack/test/functional/apps/infra/link_to.ts | 2 + .../apps/infra/logs_source_configuration.ts | 10 +++ .../functional/apps/infra/metrics_explorer.ts | 8 +++ 22 files changed, 136 insertions(+), 192 deletions(-) delete mode 100644 x-pack/plugins/infra/public/components/document_title.tsx create mode 100644 x-pack/plugins/infra/public/hooks/use_document_title.tsx delete mode 100644 x-pack/plugins/infra/public/pages/logs/stream/page_header.tsx create mode 100644 x-pack/plugins/infra/public/pages/metrics/metric_detail/components/page_error.test.tsx diff --git a/x-pack/plugins/infra/public/apps/metrics_app.tsx b/x-pack/plugins/infra/public/apps/metrics_app.tsx index fdeb7173d5cee..ce8123b5f2223 100644 --- a/x-pack/plugins/infra/public/apps/metrics_app.tsx +++ b/x-pack/plugins/infra/public/apps/metrics_app.tsx @@ -45,6 +45,7 @@ export const renderApp = ( ); return () => { + core.chrome.docTitle.reset(); ReactDOM.unmountComponentAtNode(element); plugins.data.search.session.clear(); }; diff --git a/x-pack/plugins/infra/public/components/document_title.tsx b/x-pack/plugins/infra/public/components/document_title.tsx deleted file mode 100644 index 20e482d9df5b5..0000000000000 --- a/x-pack/plugins/infra/public/components/document_title.tsx +++ /dev/null @@ -1,72 +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'; - -type TitleProp = string | ((previousTitle: string) => string); - -interface DocumentTitleProps { - title: TitleProp; -} - -interface DocumentTitleState { - index: number; -} - -const wrapWithSharedState = () => { - const titles: string[] = []; - const TITLE_SUFFIX = ' - Kibana'; - - return class extends React.Component { - public componentDidMount() { - this.setState( - () => { - return { index: titles.push('') - 1 }; - }, - () => { - this.pushTitle(this.getTitle(this.props.title)); - this.updateDocumentTitle(); - } - ); - } - - public componentDidUpdate() { - this.pushTitle(this.getTitle(this.props.title)); - this.updateDocumentTitle(); - } - - public componentWillUnmount() { - this.removeTitle(); - this.updateDocumentTitle(); - } - - public render() { - return null; - } - - public getTitle(title: TitleProp) { - return typeof title === 'function' ? title(titles[this.state.index - 1]) : title; - } - - public pushTitle(title: string) { - titles[this.state.index] = title; - } - - public removeTitle() { - titles.pop(); - } - - public updateDocumentTitle() { - const title = (titles[titles.length - 1] || '') + TITLE_SUFFIX; - if (title !== document.title) { - document.title = title; - } - } - }; -}; - -export const DocumentTitle = wrapWithSharedState(); diff --git a/x-pack/plugins/infra/public/hooks/use_breadcrumbs.ts b/x-pack/plugins/infra/public/hooks/use_breadcrumbs.ts index 37801c16bcf1f..97f737380022d 100644 --- a/x-pack/plugins/infra/public/hooks/use_breadcrumbs.ts +++ b/x-pack/plugins/infra/public/hooks/use_breadcrumbs.ts @@ -22,7 +22,7 @@ export const useBreadcrumbs = (app: AppId, appTitle: string, extraCrumbs: Chrome const appLinkProps = useLinkProps({ app }); useEffect(() => { - chrome?.setBreadcrumbs?.([ + const breadcrumbs = [ { ...observabilityLinkProps, text: observabilityTitle, @@ -32,6 +32,11 @@ export const useBreadcrumbs = (app: AppId, appTitle: string, extraCrumbs: Chrome text: appTitle, }, ...extraCrumbs, - ]); + ]; + + const docTitle = [...breadcrumbs].reverse().map((breadcrumb) => breadcrumb.text as string); + + chrome.docTitle.change(docTitle); + chrome.setBreadcrumbs(breadcrumbs); }, [appLinkProps, appTitle, chrome, extraCrumbs, observabilityLinkProps]); }; diff --git a/x-pack/plugins/infra/public/hooks/use_document_title.tsx b/x-pack/plugins/infra/public/hooks/use_document_title.tsx new file mode 100644 index 0000000000000..82fb5a669eb91 --- /dev/null +++ b/x-pack/plugins/infra/public/hooks/use_document_title.tsx @@ -0,0 +1,25 @@ +/* + * 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 { ChromeBreadcrumb } from '@kbn/core/public'; +import { useEffect } from 'react'; +import { observabilityTitle } from '../translations'; +import { useKibanaContextForPlugin } from './use_kibana'; + +export const useDocumentTitle = (extraTitles: ChromeBreadcrumb[]) => { + const { + services: { chrome }, + } = useKibanaContextForPlugin(); + + useEffect(() => { + const docTitle = [{ text: observabilityTitle }, ...extraTitles] + .reverse() + .map((breadcrumb) => breadcrumb.text as string); + + chrome.docTitle.change(docTitle); + }, [chrome, extraTitles]); +}; diff --git a/x-pack/plugins/infra/public/pages/logs/page_content.tsx b/x-pack/plugins/infra/public/pages/logs/page_content.tsx index e42cfc2d7c54a..8acd4004603e9 100644 --- a/x-pack/plugins/infra/public/pages/logs/page_content.tsx +++ b/x-pack/plugins/infra/public/pages/logs/page_content.tsx @@ -12,7 +12,6 @@ import { Route, Switch } from 'react-router-dom'; import { useKibana } from '@kbn/kibana-react-plugin/public'; import { HeaderMenuPortal, useLinkProps } from '@kbn/observability-plugin/public'; import { AlertDropdown } from '../../alerting/log_threshold'; -import { DocumentTitle } from '../../components/document_title'; import { HelpCenterContent } from '../../components/help_center_content'; import { useReadOnlyBadge } from '../../hooks/use_readonly_badge'; import { HeaderActionMenuContext } from '../../utils/header_action_menu_provider'; @@ -62,8 +61,6 @@ export const LogsPageContent: React.FunctionComponent = () => { return ( <> - - {setHeaderActionMenu && theme$ && ( diff --git a/x-pack/plugins/infra/public/pages/logs/stream/page.tsx b/x-pack/plugins/infra/public/pages/logs/stream/page.tsx index bb8e4307fe3b9..19f098a6721bc 100644 --- a/x-pack/plugins/infra/public/pages/logs/stream/page.tsx +++ b/x-pack/plugins/infra/public/pages/logs/stream/page.tsx @@ -10,7 +10,6 @@ import React from 'react'; import { useTrackPageview } from '@kbn/observability-plugin/public'; import { useLogsBreadcrumbs } from '../../../hooks/use_logs_breadcrumbs'; import { StreamPageContent } from './page_content'; -import { StreamPageHeader } from './page_header'; import { LogsPageProviders } from './page_providers'; import { streamTitle } from '../../../translations'; @@ -26,7 +25,6 @@ export const StreamPage = () => { return ( - diff --git a/x-pack/plugins/infra/public/pages/logs/stream/page_header.tsx b/x-pack/plugins/infra/public/pages/logs/stream/page_header.tsx deleted file mode 100644 index f6c4ad8c8c139..0000000000000 --- a/x-pack/plugins/infra/public/pages/logs/stream/page_header.tsx +++ /dev/null @@ -1,28 +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 { i18n } from '@kbn/i18n'; -import React from 'react'; - -import { DocumentTitle } from '../../../components/document_title'; - -export const StreamPageHeader = () => { - return ( - <> - - i18n.translate('xpack.infra.logs.streamPage.documentTitle', { - defaultMessage: '{previousTitle} | Stream', - values: { - previousTitle, - }, - }) - } - /> - - ); -}; diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/index.tsx b/x-pack/plugins/infra/public/pages/metrics/hosts/index.tsx index 5c2fed9753b80..a5dfd7f2ddd0f 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/index.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/index.tsx @@ -6,13 +6,10 @@ */ import { EuiErrorBoundary } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; import React from 'react'; import { useTrackPageview } from '@kbn/observability-plugin/public'; import { APP_WRAPPER_CLASS } from '@kbn/core/public'; -import { DocumentTitle } from '../../../components/document_title'; - import { SourceErrorPage } from '../../../components/source_error_page'; import { SourceLoadingPage } from '../../../components/source_loading_page'; import { useSourceContext } from '../../../containers/metrics_source'; @@ -42,16 +39,6 @@ export const HostsPage = () => { ]); return ( - - i18n.translate('xpack.infra.infrastructureHostsPage.documentTitle', { - defaultMessage: '{previousTitle} | Hosts', - values: { - previousTitle, - }, - }) - } - /> {isLoading && !source ? ( ) : metricIndicesExist && source ? ( diff --git a/x-pack/plugins/infra/public/pages/metrics/index.tsx b/x-pack/plugins/infra/public/pages/metrics/index.tsx index 9c02424aac949..691069a978e83 100644 --- a/x-pack/plugins/infra/public/pages/metrics/index.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/index.tsx @@ -15,7 +15,6 @@ import { useKibana } from '@kbn/kibana-react-plugin/public'; import { HeaderMenuPortal } from '@kbn/observability-plugin/public'; import { useLinkProps } from '@kbn/observability-plugin/public'; import { MetricsSourceConfigurationProperties } from '../../../common/metrics_sources'; -import { DocumentTitle } from '../../components/document_title'; import { HelpCenterContent } from '../../components/help_center_content'; import { useReadOnlyBadge } from '../../hooks/use_readonly_badge'; import { @@ -73,12 +72,6 @@ export const InfrastructurePage = ({ match }: RouteComponentProps) => { - - { return ( - - i18n.translate('xpack.infra.infrastructureSnapshotPage.documentTitle', { - defaultMessage: '{previousTitle} | Inventory', - values: { - previousTitle, - }, - }) - } - /> {isLoading && !source ? ( ) : metricIndicesExist ? ( diff --git a/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/page_error.test.tsx b/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/page_error.test.tsx new file mode 100644 index 0000000000000..25ae3b3717bd6 --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/page_error.test.tsx @@ -0,0 +1,47 @@ +/* + * 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 { render } from '@testing-library/react'; + +import { PageError } from './page_error'; +import { errorTitle } from '../../../../translations'; +import { InfraHttpError } from '../../../../types'; +import { useDocumentTitle } from '../../../../hooks/use_document_title'; +import { I18nProvider } from '@kbn/i18n-react'; + +jest.mock('../../../../hooks/use_document_title', () => ({ + useDocumentTitle: jest.fn(), +})); + +const renderErrorPage = () => + render( + + + + ); + +describe('PageError component', () => { + it('renders correctly and set title', () => { + const { getByText } = renderErrorPage(); + expect(useDocumentTitle).toHaveBeenCalledWith([{ text: `${errorTitle}` }]); + + expect(getByText('Error Message')).toBeInTheDocument(); + expect(getByText('Please click the back button and try again.')).toBeInTheDocument(); + }); +}); diff --git a/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/page_error.tsx b/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/page_error.tsx index a6665e0d244f2..b4cdb47399e98 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/page_error.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/page_error.tsx @@ -6,11 +6,11 @@ */ import React from 'react'; -import { i18n } from '@kbn/i18n'; +import { useDocumentTitle } from '../../../../hooks/use_document_title'; import { InvalidNodeError } from './invalid_node'; -import { DocumentTitle } from '../../../../components/document_title'; import { ErrorPageBody } from '../../../error'; import { InfraHttpError } from '../../../../types'; +import { errorTitle } from '../../../../translations'; interface Props { name: string; @@ -18,18 +18,10 @@ interface Props { } export const PageError = ({ error, name }: Props) => { + useDocumentTitle([{ text: errorTitle }]); + return ( <> - - i18n.translate('xpack.infra.metricDetailPage.documentTitleError', { - defaultMessage: '{previousTitle} | Uh oh', - values: { - previousTitle, - }, - }) - } - /> {error.body?.statusCode === 404 ? ( ) : ( diff --git a/x-pack/plugins/infra/public/pages/metrics/metric_detail/index.tsx b/x-pack/plugins/infra/public/pages/metrics/metric_detail/index.tsx index 823b9d703f502..9b92901976bff 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metric_detail/index.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/metric_detail/index.tsx @@ -9,7 +9,6 @@ import { i18n } from '@kbn/i18n'; import React, { useState } from 'react'; import { EuiTheme, withTheme } from '@kbn/kibana-react-plugin/common'; import { useLinkProps } from '@kbn/observability-plugin/public'; -import { DocumentTitle } from '../../../components/document_title'; import { withMetricPageProviders } from './page_providers'; import { useMetadata } from './hooks/use_metadata'; import { useMetricsBreadcrumbs } from '../../../hooks/use_metrics_breadcrumbs'; @@ -100,14 +99,6 @@ export const MetricDetail = withMetricPageProviders( return ( <> - {metadata ? ( - - i18n.translate('xpack.infra.infrastructureMetricsExplorerPage.documentTitle', { - defaultMessage: '{previousTitle} | Metrics Explorer', - values: { - previousTitle, - }, - }) - } - /> { const esArchiver = getService('esArchiver'); + const browser = getService('browser'); const retry = getService('retry'); - const pageObjects = getPageObjects(['common', 'infraHome', 'infraSavedViews']); + const pageObjects = getPageObjects(['common', 'header', 'infraHome', 'infraSavedViews']); const kibanaServer = getService('kibanaServer'); describe('Home page', function () { @@ -34,6 +35,22 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await pageObjects.common.navigateToApp('infraOps'); await pageObjects.infraHome.getNoMetricsIndicesPrompt(); }); + + it('renders the correct error page title', async () => { + await pageObjects.common.navigateToUrlWithBrowserHistory( + 'infraOps', + '/detail/host/test', + '', + { + ensureCurrentUrl: false, + } + ); + await pageObjects.infraHome.waitForLoading(); + await pageObjects.header.waitUntilLoadingHasFinished(); + + const documentTitle = await browser.getTitle(); + expect(documentTitle).to.contain('Uh oh - Observability - Elastic'); + }); }); describe('with metrics present', () => { @@ -47,6 +64,13 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await esArchiver.unload('x-pack/test/functional/es_archives/infra/metrics_and_logs') ); + it('renders the correct page title', async () => { + await pageObjects.header.waitUntilLoadingHasFinished(); + + const documentTitle = await browser.getTitle(); + expect(documentTitle).to.contain('Inventory - Infrastructure - Observability - Elastic'); + }); + it('renders an empty data prompt for dates with no data', async () => { await pageObjects.infraHome.goToTime(DATE_WITHOUT_DATA); await pageObjects.infraHome.getNoMetricsDataPrompt(); diff --git a/x-pack/test/functional/apps/infra/link_to.ts b/x-pack/test/functional/apps/infra/link_to.ts index ebfcb740961b1..05eccc8e57ebc 100644 --- a/x-pack/test/functional/apps/infra/link_to.ts +++ b/x-pack/test/functional/apps/infra/link_to.ts @@ -42,6 +42,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await retry.tryForTime(5000, async () => { const currentUrl = await browser.getCurrentUrl(); const parsedUrl = new URL(currentUrl); + const documentTitle = await browser.getTitle(); expect(parsedUrl.pathname).to.be('/app/logs/stream'); expect(parsedUrl.searchParams.get('logFilter')).to.be( @@ -51,6 +52,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { `(end:'${endDate}',position:(tiebreaker:0,time:${timestamp}),start:'${startDate}',streamLive:!f)` ); expect(parsedUrl.searchParams.get('sourceId')).to.be('default'); + expect(documentTitle).to.contain('Stream - Logs - Observability - Elastic'); }); }); }); diff --git a/x-pack/test/functional/apps/infra/logs_source_configuration.ts b/x-pack/test/functional/apps/infra/logs_source_configuration.ts index 56eed5ec4b635..38cc795034a22 100644 --- a/x-pack/test/functional/apps/infra/logs_source_configuration.ts +++ b/x-pack/test/functional/apps/infra/logs_source_configuration.ts @@ -16,6 +16,7 @@ const COMMON_REQUEST_HEADERS = { export default ({ getPageObjects, getService }: FtrProviderContext) => { const esArchiver = getService('esArchiver'); + const browser = getService('browser'); const logsUi = getService('logsUi'); const infraSourceConfigurationForm = getService('infraSourceConfigurationForm'); const pageObjects = getPageObjects(['common', 'header', 'infraLogs']); @@ -49,6 +50,15 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await esArchiver.unload('x-pack/test/functional/es_archives/infra/metrics_and_logs'); }); + it('renders the correct page title', async () => { + await pageObjects.infraLogs.navigateToTab('settings'); + + await pageObjects.header.waitUntilLoadingHasFinished(); + const documentTitle = await browser.getTitle(); + + expect(documentTitle).to.contain('Settings - Logs - Observability - Elastic'); + }); + it('can change the log indices to a pattern that matches nothing', async () => { await pageObjects.infraLogs.navigateToTab('settings'); diff --git a/x-pack/test/functional/apps/infra/metrics_explorer.ts b/x-pack/test/functional/apps/infra/metrics_explorer.ts index fc620d9ba5665..4d6859a4e99e7 100644 --- a/x-pack/test/functional/apps/infra/metrics_explorer.ts +++ b/x-pack/test/functional/apps/infra/metrics_explorer.ts @@ -16,6 +16,7 @@ const timepickerFormat = 'MMM D, YYYY @ HH:mm:ss.SSS'; export default ({ getPageObjects, getService }: FtrProviderContext) => { const esArchiver = getService('esArchiver'); const kibanaServer = getService('kibanaServer'); + const browser = getService('browser'); const pageObjects = getPageObjects([ 'common', 'infraHome', @@ -42,6 +43,13 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); after(() => esArchiver.unload('x-pack/test/functional/es_archives/infra/metrics_and_logs')); + it('should render the correct page title', async () => { + const documentTitle = await browser.getTitle(); + expect(documentTitle).to.contain( + 'Metrics Explorer - Infrastructure - Observability - Elastic' + ); + }); + it('should have three metrics by default', async () => { const metrics = await pageObjects.infraMetricsExplorer.getMetrics(); expect(metrics.length).to.equal(3); From 5f057ff610139a88d660e48e41bd5c2648140800 Mon Sep 17 00:00:00 2001 From: Khristinin Nikita Date: Wed, 28 Sep 2022 13:49:07 +0200 Subject: [PATCH 10/17] Add enrichment event log time (#141433) * Add enrichment event log time * fix types * Fix test * Add avg field * Fix enrichments event log * Add telemetry * Update schema Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- x-pack/plugins/event_log/README.md | 1 + .../plugins/event_log/generated/mappings.json | 3 + x-pack/plugins/event_log/generated/schemas.ts | 1 + x-pack/plugins/event_log/scripts/mappings.js | 3 + .../model/execution_metrics.ts | 9 + .../client_for_executors/client.ts | 1 + .../client_for_executors/client_interface.ts | 1 + .../saved_objects_type.ts | 3 + .../create_security_rule_type_wrapper.ts | 5 + .../factories/bulk_create_factory.ts | 24 +- .../lib/detection_engine/rule_types/types.ts | 1 + .../rule_types/utils/index.ts | 1 + .../threat_mapping/create_threat_signals.ts | 1 + .../signals/threat_mapping/utils.test.ts | 38 ++ .../signals/threat_mapping/utils.ts | 4 + .../lib/detection_engine/signals/types.ts | 1 + .../detection_engine/signals/utils.test.ts | 13 + .../lib/detection_engine/signals/utils.ts | 7 + .../server/usage/collector.ts | 252 ++++++++++ .../detections/rules/get_initial_usage.ts | 1 + .../detections/rules/get_metrics.mocks.ts | 414 ++++++++++++++++ .../server/usage/detections/rules/types.ts | 1 + ...event_log_agg_by_rule_type_metrics.test.ts | 15 + .../get_event_log_agg_by_rule_type_metrics.ts | 15 + ...vent_log_agg_by_rule_types_metrics.test.ts | 60 +++ .../get_event_log_agg_by_statuses.test.ts | 60 +++ .../transform_single_rule_metric.test.ts | 14 + .../utils/transform_single_rule_metric.ts | 5 + .../security_solution/server/usage/types.ts | 9 + .../schema/xpack_plugins.json | 468 ++++++++++++++++-- 30 files changed, 1394 insertions(+), 37 deletions(-) diff --git a/x-pack/plugins/event_log/README.md b/x-pack/plugins/event_log/README.md index c1d5869e7ed48..05b05946b652d 100644 --- a/x-pack/plugins/event_log/README.md +++ b/x-pack/plugins/event_log/README.md @@ -159,6 +159,7 @@ Below is a document in the expected structure, with descriptions of the fields: es_search_duration_ms: "total time spent performing ES searches as measured by Elasticsearch", total_search_duration_ms: "total time spent performing ES searches as measured by Kibana; includes network latency and time spent serializing/deserializing request/response", total_indexing_duration_ms: "total time spent indexing documents during current rule execution cycle", + total_enrichment_duration_ms: "total time spent enriching documents during current rule execution cycle", execution_gap_duration_s: "duration in seconds of execution gap" } } diff --git a/x-pack/plugins/event_log/generated/mappings.json b/x-pack/plugins/event_log/generated/mappings.json index db6719fed996a..4756b4f2e5534 100644 --- a/x-pack/plugins/event_log/generated/mappings.json +++ b/x-pack/plugins/event_log/generated/mappings.json @@ -347,6 +347,9 @@ }, "total_run_duration_ms": { "type": "long" + }, + "total_enrichment_duration_ms": { + "type": "long" } } } diff --git a/x-pack/plugins/event_log/generated/schemas.ts b/x-pack/plugins/event_log/generated/schemas.ts index b5557f64e9ac7..523ad683eabf2 100644 --- a/x-pack/plugins/event_log/generated/schemas.ts +++ b/x-pack/plugins/event_log/generated/schemas.ts @@ -150,6 +150,7 @@ export const EventSchema = schema.maybe( claim_to_start_duration_ms: ecsStringOrNumber(), prepare_rule_duration_ms: ecsStringOrNumber(), total_run_duration_ms: ecsStringOrNumber(), + total_enrichment_duration_ms: ecsStringOrNumber(), }) ), }) diff --git a/x-pack/plugins/event_log/scripts/mappings.js b/x-pack/plugins/event_log/scripts/mappings.js index 98050b6557210..65c9220fb5355 100644 --- a/x-pack/plugins/event_log/scripts/mappings.js +++ b/x-pack/plugins/event_log/scripts/mappings.js @@ -130,6 +130,9 @@ exports.EcsCustomPropertyMappings = { total_run_duration_ms: { type: 'long', }, + total_enrichment_duration_ms: { + type: 'long', + }, }, }, }, diff --git a/x-pack/plugins/security_solution/common/detection_engine/rule_monitoring/model/execution_metrics.ts b/x-pack/plugins/security_solution/common/detection_engine/rule_monitoring/model/execution_metrics.ts index c6bb71970e599..b15c76119e441 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/rule_monitoring/model/execution_metrics.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/rule_monitoring/model/execution_metrics.ts @@ -12,8 +12,17 @@ export type DurationMetric = t.TypeOf; export const DurationMetric = PositiveInteger; export type RuleExecutionMetrics = t.TypeOf; + +/** + @property total_search_duration_ms - "total time spent performing ES searches as measured by Kibana; + includes network latency and time spent serializing/deserializing request/response", + @property total_indexing_duration_ms - "total time spent indexing documents during current rule execution cycle", + @property total_enrichment_duration_ms - total time spent enriching documents during current rule execution cycle + @property execution_gap_duration_s - "duration in seconds of execution gap" +*/ export const RuleExecutionMetrics = t.partial({ total_search_duration_ms: DurationMetric, total_indexing_duration_ms: DurationMetric, + total_enrichment_duration_ms: DurationMetric, execution_gap_duration_s: DurationMetric, }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/client_for_executors/client.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/client_for_executors/client.ts index 4116848b1ffcf..3c712847851fd 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/client_for_executors/client.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/client_for_executors/client.ts @@ -222,6 +222,7 @@ const normalizeStatusChangeArgs = (args: StatusChangeArgs): NormalizedStatusChan ? { total_search_duration_ms: normalizeDurations(metrics.searchDurations), total_indexing_duration_ms: normalizeDurations(metrics.indexingDurations), + total_enrichment_duration_ms: normalizeDurations(metrics.enrichmentDurations), execution_gap_duration_s: normalizeGap(metrics.executionGap), } : undefined, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/client_for_executors/client_interface.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/client_for_executors/client_interface.ts index 22392e699fcea..5e48a43e949c3 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/client_for_executors/client_interface.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/client_for_executors/client_interface.ts @@ -115,5 +115,6 @@ export interface StatusChangeArgs { export interface MetricsArgs { searchDurations?: string[]; indexingDurations?: string[]; + enrichmentDurations?: string[]; executionGap?: Duration; } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/execution_saved_object/saved_objects_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/execution_saved_object/saved_objects_type.ts index ac3b28e87e0d9..1e0a2e74cadcd 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/execution_saved_object/saved_objects_type.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/execution_saved_object/saved_objects_type.ts @@ -58,6 +58,9 @@ const ruleExecutionMappings: SavedObjectsType['mappings'] = { total_indexing_duration_ms: { type: 'long', }, + total_enrichment_duration_ms: { + type: 'long', + }, execution_gap_duration_s: { type: 'long', }, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/create_security_rule_type_wrapper.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/create_security_rule_type_wrapper.ts index ae3f310d841c3..05813ed1ee29c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/create_security_rule_type_wrapper.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/create_security_rule_type_wrapper.ts @@ -343,6 +343,7 @@ export const createSecurityRuleTypeWrapper: CreateSecurityRuleTypeWrapper = const warningMessages = result.warningMessages.concat(runResult.warningMessages); result = { bulkCreateTimes: result.bulkCreateTimes.concat(runResult.bulkCreateTimes), + enrichmentTimes: result.enrichmentTimes.concat(runResult.enrichmentTimes), createdSignals, createdSignalsCount: createdSignals.length, errors: result.errors.concat(runResult.errors), @@ -358,6 +359,7 @@ export const createSecurityRuleTypeWrapper: CreateSecurityRuleTypeWrapper = } else { result = { bulkCreateTimes: [], + enrichmentTimes: [], createdSignals: [], createdSignalsCount: 0, errors: [], @@ -434,6 +436,7 @@ export const createSecurityRuleTypeWrapper: CreateSecurityRuleTypeWrapper = metrics: { searchDurations: result.searchAfterTimes, indexingDurations: result.bulkCreateTimes, + enrichmentDurations: result.enrichmentTimes, }, }); } @@ -452,6 +455,7 @@ export const createSecurityRuleTypeWrapper: CreateSecurityRuleTypeWrapper = metrics: { searchDurations: result.searchAfterTimes, indexingDurations: result.bulkCreateTimes, + enrichmentDurations: result.enrichmentTimes, }, }); } @@ -464,6 +468,7 @@ export const createSecurityRuleTypeWrapper: CreateSecurityRuleTypeWrapper = metrics: { searchDurations: result.searchAfterTimes, indexingDurations: result.bulkCreateTimes, + enrichmentDurations: result.enrichmentTimes, }, }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/bulk_create_factory.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/bulk_create_factory.ts index 81f15364ff128..95bc32571f6ea 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/bulk_create_factory.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/bulk_create_factory.ts @@ -21,6 +21,7 @@ import type { export interface GenericBulkCreateResponse { success: boolean; bulkCreateDuration: string; + enrichmentDuration: string; createdItemsCount: number; createdItems: Array & { _id: string; _index: string }>; errors: string[]; @@ -45,6 +46,7 @@ export const bulkCreateFactory = return { errors: [], success: true, + enrichmentDuration: '0', bulkCreateDuration: '0', createdItemsCount: 0, createdItems: [], @@ -54,6 +56,24 @@ export const bulkCreateFactory = const start = performance.now(); + let enrichmentsTimeStart = 0; + let enrichmentsTimeFinish = 0; + let enrichAlertsWrapper: typeof enrichAlerts; + if (enrichAlerts) { + enrichAlertsWrapper = async (alerts, params) => { + enrichmentsTimeStart = performance.now(); + try { + const enrichedAlerts = await enrichAlerts(alerts, params); + return enrichedAlerts; + } catch (error) { + ruleExecutionLogger.error(`Enrichments failed ${error}`); + throw error; + } finally { + enrichmentsTimeFinish = performance.now(); + } + }; + } + const { createdAlerts, errors, alertsWereTruncated } = await alertWithPersistence( wrappedDocs.map((doc) => ({ _id: doc._id, @@ -62,7 +82,7 @@ export const bulkCreateFactory = })), refreshForBulkCreate, maxAlerts, - enrichAlerts + enrichAlertsWrapper ); const end = performance.now(); @@ -78,6 +98,7 @@ export const bulkCreateFactory = return { errors: Object.keys(errors), success: false, + enrichmentDuration: makeFloatString(enrichmentsTimeFinish - enrichmentsTimeStart), bulkCreateDuration: makeFloatString(end - start), createdItemsCount: createdAlerts.length, createdItems: createdAlerts, @@ -88,6 +109,7 @@ export const bulkCreateFactory = errors: [], success: true, bulkCreateDuration: makeFloatString(end - start), + enrichmentDuration: makeFloatString(enrichmentsTimeFinish - enrichmentsTimeStart), createdItemsCount: createdAlerts.length, createdItems: createdAlerts, alertsWereTruncated, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/types.ts index 403d6542f4f0c..e1af3077ec3e3 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/types.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/types.ts @@ -42,6 +42,7 @@ import type { IRuleExecutionLogForExecutors, IRuleExecutionLogService } from '.. export interface SecurityAlertTypeReturnValue { bulkCreateTimes: string[]; + enrichmentTimes: string[]; createdSignalsCount: number; createdSignals: unknown[]; errors: string[]; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/index.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/index.ts index 5d2dcd4a4b3d2..8f23da386f5c7 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/index.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/index.ts @@ -10,6 +10,7 @@ import type { SecurityAlertTypeReturnValue } from '../types'; export const createResultObject = (state: TState) => { const result: SecurityAlertTypeReturnValue = { + enrichmentTimes: [], bulkCreateTimes: [], createdSignalsCount: 0, createdSignals: [], diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signals.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signals.ts index d80e5cba026b0..0ab6ec2a01dda 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signals.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signals.ts @@ -65,6 +65,7 @@ export const createThreatSignals = async ({ let results: SearchAfterAndBulkCreateReturnType = { success: true, warning: false, + enrichmentTimes: [], bulkCreateTimes: [], searchAfterTimes: [], lastLookBackDate: null, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/utils.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/utils.test.ts index 981868589e4a1..0bcdc8450a830 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/utils.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/utils.test.ts @@ -55,6 +55,7 @@ describe('utils', () => { warning: false, searchAfterTimes: ['10', '20', '30'], bulkCreateTimes: ['5', '15', '25'], + enrichmentTimes: ['1', '2', '3'], lastLookBackDate: undefined, createdSignalsCount: 3, createdSignals: Array(3).fill(sampleSignalHit()), @@ -67,6 +68,7 @@ describe('utils', () => { warning: false, searchAfterTimes: ['10', '20', '30'], bulkCreateTimes: ['5', '15', '25'], + enrichmentTimes: ['1', '2', '3'], lastLookBackDate: undefined, createdSignalsCount: 3, createdSignals: Array(3).fill(sampleSignalHit()), @@ -83,6 +85,7 @@ describe('utils', () => { warning: false, searchAfterTimes: ['10', '20', '30'], bulkCreateTimes: ['5', '15', '25'], + enrichmentTimes: ['1', '2', '3'], lastLookBackDate: undefined, createdSignalsCount: 3, createdSignals: Array(3).fill(sampleSignalHit()), @@ -95,6 +98,7 @@ describe('utils', () => { warning: false, searchAfterTimes: ['10', '20', '30'], bulkCreateTimes: ['5', '15', '25'], + enrichmentTimes: ['1', '2', '3'], lastLookBackDate: undefined, createdSignalsCount: 3, createdSignals: Array(3).fill(sampleSignalHit()), @@ -111,6 +115,7 @@ describe('utils', () => { warning: false, searchAfterTimes: ['10', '20', '30'], bulkCreateTimes: ['5', '15', '25'], + enrichmentTimes: ['1', '2', '3'], lastLookBackDate: undefined, createdSignalsCount: 3, createdSignals: Array(3).fill(sampleSignalHit()), @@ -123,6 +128,7 @@ describe('utils', () => { warning: false, searchAfterTimes: ['10', '20', '30'], bulkCreateTimes: ['5', '15', '25'], + enrichmentTimes: ['1', '2', '3'], lastLookBackDate: new Date('2020-09-16T03:34:32.390Z'), createdSignalsCount: 3, createdSignals: Array(3).fill(sampleSignalHit()), @@ -139,6 +145,7 @@ describe('utils', () => { warning: false, searchAfterTimes: ['10', '20', '30'], bulkCreateTimes: ['5', '15', '25'], + enrichmentTimes: ['1', '2', '3'], lastLookBackDate: undefined, createdSignalsCount: 3, createdSignals: Array(3).fill(sampleSignalHit()), @@ -151,6 +158,7 @@ describe('utils', () => { warning: false, searchAfterTimes: ['10', '20', '30'], bulkCreateTimes: ['5', '15', '25'], + enrichmentTimes: ['1', '2', '3'], lastLookBackDate: new Date('2020-09-16T03:34:32.390Z'), createdSignalsCount: 3, createdSignals: Array(3).fill(sampleSignalHit()), @@ -162,6 +170,7 @@ describe('utils', () => { expect.objectContaining({ searchAfterTimes: ['60'], bulkCreateTimes: ['50'], + enrichmentTimes: ['6'], }) ); }); @@ -172,6 +181,7 @@ describe('utils', () => { warning: false, searchAfterTimes: ['10', '20', '30'], bulkCreateTimes: ['5', '15', '25'], + enrichmentTimes: ['1', '2', '3'], lastLookBackDate: undefined, createdSignalsCount: 3, createdSignals: Array(3).fill(sampleSignalHit()), @@ -184,6 +194,7 @@ describe('utils', () => { warning: false, searchAfterTimes: ['10', '20', '30'], bulkCreateTimes: ['5', '15', '25'], + enrichmentTimes: ['1', '2', '3'], lastLookBackDate: new Date('2020-09-16T03:34:32.390Z'), createdSignalsCount: 3, createdSignals: Array(3).fill(sampleSignalHit()), @@ -296,6 +307,7 @@ describe('utils', () => { warning: false, searchAfterTimes: ['10', '20', '30'], bulkCreateTimes: ['5', '15', '25'], + enrichmentTimes: ['1', '2', '3'], lastLookBackDate: undefined, createdSignalsCount: 3, createdSignals: Array(3).fill(sampleSignalHit()), @@ -307,6 +319,7 @@ describe('utils', () => { warning: false, searchAfterTimes: ['30'], // max value from existingResult.searchAfterTimes bulkCreateTimes: ['25'], // max value from existingResult.bulkCreateTimes + enrichmentTimes: ['3'], // max value from existingResult.enrichmentTimes lastLookBackDate: undefined, createdSignalsCount: 3, createdSignals: Array(3).fill(sampleSignalHit()), @@ -323,6 +336,7 @@ describe('utils', () => { warning: false, searchAfterTimes: ['10', '20', '30'], bulkCreateTimes: ['5', '15', '25'], + enrichmentTimes: ['1', '2', '3'], lastLookBackDate: undefined, createdSignalsCount: 3, createdSignals: Array(3).fill(sampleSignalHit()), @@ -334,6 +348,7 @@ describe('utils', () => { warning: false, searchAfterTimes: [], bulkCreateTimes: [], + enrichmentTimes: [], lastLookBackDate: undefined, createdSignalsCount: 0, createdSignals: [], @@ -345,6 +360,7 @@ describe('utils', () => { warning: false, searchAfterTimes: ['30'], // max value from existingResult.searchAfterTimes bulkCreateTimes: ['25'], // max value from existingResult.bulkCreateTimes + enrichmentTimes: ['3'], // max value from existingResult.enrichmentTimes lastLookBackDate: undefined, createdSignalsCount: 3, createdSignals: Array(3).fill(sampleSignalHit()), @@ -362,6 +378,7 @@ describe('utils', () => { warning: false, searchAfterTimes: ['10', '20', '30'], // max is 30 bulkCreateTimes: ['5', '15', '25'], // max is 25 + enrichmentTimes: ['1', '2', '3'], // max is 3 lastLookBackDate: undefined, createdSignalsCount: 3, createdSignals: Array(3).fill(sampleSignalHit()), @@ -373,6 +390,7 @@ describe('utils', () => { warning: false, searchAfterTimes: ['10', '20', '30'], bulkCreateTimes: ['5', '15', '25'], + enrichmentTimes: ['1', '2', '3'], lastLookBackDate: new Date('2020-09-16T03:34:32.390Z'), createdSignalsCount: 5, createdSignals: Array(5).fill(sampleSignalHit()), @@ -384,6 +402,7 @@ describe('utils', () => { warning: false, searchAfterTimes: ['40', '5', '15'], bulkCreateTimes: ['50', '5', '15'], + enrichmentTimes: ['4', '2', '3'], lastLookBackDate: new Date('2020-09-16T04:34:32.390Z'), createdSignalsCount: 8, createdSignals: Array(8).fill(sampleSignalHit()), @@ -396,6 +415,7 @@ describe('utils', () => { warning: false, searchAfterTimes: ['70'], // max value between newResult1 and newResult2 + max array value of existingResult (40 + 30 = 70) bulkCreateTimes: ['75'], // max value between newResult1 and newResult2 + max array value of existingResult (50 + 25 = 75) + enrichmentTimes: ['7'], // max value between newResult1 and newResult2 + max array value of existingResult (4 + 3 = 7) lastLookBackDate: new Date('2020-09-16T04:34:32.390Z'), // max lastLookBackDate createdSignalsCount: 16, // all the signals counted together (8 + 5 + 3) createdSignals: Array(16).fill(sampleSignalHit()), @@ -413,6 +433,7 @@ describe('utils', () => { warning: false, searchAfterTimes: ['10', '20', '30'], // max is 30 bulkCreateTimes: ['5', '15', '25'], // max is 25 + enrichmentTimes: ['1', '2', '3'], // max is 3 lastLookBackDate: undefined, createdSignalsCount: 3, createdSignals: Array(3).fill(sampleSignalHit()), @@ -424,6 +445,7 @@ describe('utils', () => { warning: false, searchAfterTimes: ['10', '20', '30'], bulkCreateTimes: ['5', '15', '25'], + enrichmentTimes: ['1', '2', '3'], lastLookBackDate: new Date('2020-09-16T03:34:32.390Z'), createdSignalsCount: 5, createdSignals: Array(5).fill(sampleSignalHit()), @@ -435,6 +457,7 @@ describe('utils', () => { warning: false, searchAfterTimes: ['40', '5', '15'], bulkCreateTimes: ['50', '5', '15'], + enrichmentTimes: ['5', '2', '3'], lastLookBackDate: new Date('2020-09-16T04:34:32.390Z'), createdSignalsCount: 8, createdSignals: Array(8).fill(sampleSignalHit()), @@ -447,6 +470,7 @@ describe('utils', () => { warning: false, searchAfterTimes: ['70'], // max value between newResult1 and newResult2 + max array value of existingResult (40 + 30 = 70) bulkCreateTimes: ['75'], // max value between newResult1 and newResult2 + max array value of existingResult (50 + 25 = 75) + enrichmentTimes: ['8'], // max value between newResult1 and newResult2 + max array value of existingResult (50 + 3 = 8) lastLookBackDate: new Date('2020-09-16T04:34:32.390Z'), // max lastLookBackDate createdSignalsCount: 16, // all the signals counted together (8 + 5 + 3) createdSignals: Array(16).fill(sampleSignalHit()), @@ -464,6 +488,7 @@ describe('utils', () => { warning: false, searchAfterTimes: ['10', '20', '30'], // max is 30 bulkCreateTimes: ['5', '15', '25'], // max is 25 + enrichmentTimes: ['1', '2', '3'], lastLookBackDate: undefined, createdSignalsCount: 3, createdSignals: Array(3).fill(sampleSignalHit()), @@ -475,6 +500,7 @@ describe('utils', () => { warning: false, searchAfterTimes: ['10', '20', '30'], bulkCreateTimes: ['5', '15', '25'], + enrichmentTimes: ['1', '2', '3'], lastLookBackDate: new Date('2020-09-16T03:34:32.390Z'), createdSignalsCount: 5, createdSignals: Array(5).fill(sampleSignalHit()), @@ -486,6 +512,7 @@ describe('utils', () => { warning: false, searchAfterTimes: ['40', '5', '15'], bulkCreateTimes: ['50', '5', '15'], + enrichmentTimes: ['5', '2', '3'], lastLookBackDate: null, createdSignalsCount: 8, createdSignals: Array(8).fill(sampleSignalHit()), @@ -498,6 +525,7 @@ describe('utils', () => { warning: false, searchAfterTimes: ['70'], // max value between newResult1 and newResult2 + max array value of existingResult (40 + 30 = 70) bulkCreateTimes: ['75'], // max value between newResult1 and newResult2 + max array value of existingResult (50 + 25 = 75) + enrichmentTimes: ['8'], // max value between newResult1 and newResult2 + max array value of existingResult (5 + 3 = 8) lastLookBackDate: new Date('2020-09-16T03:34:32.390Z'), // max lastLookBackDate createdSignalsCount: 16, // all the signals counted together (8 + 5 + 3) createdSignals: Array(16).fill(sampleSignalHit()), @@ -515,6 +543,7 @@ describe('utils', () => { warning: false, searchAfterTimes: ['10', '20', '30'], bulkCreateTimes: ['5', '15', '25'], + enrichmentTimes: ['1', '2', '3'], lastLookBackDate: undefined, createdSignalsCount: 3, createdSignals: Array(3).fill(sampleSignalHit()), @@ -527,6 +556,7 @@ describe('utils', () => { warning: false, searchAfterTimes: ['10', '20', '30'], bulkCreateTimes: ['5', '15', '25'], + enrichmentTimes: ['5', '2', '3'], lastLookBackDate: undefined, createdSignalsCount: 3, createdSignals: Array(3).fill(sampleSignalHit()), @@ -543,6 +573,7 @@ describe('utils', () => { warning: false, searchAfterTimes: ['10', '20', '30'], bulkCreateTimes: ['5', '15', '25'], + enrichmentTimes: ['1', '2', '3'], lastLookBackDate: undefined, createdSignalsCount: 3, createdSignals: Array(3).fill(sampleSignalHit()), @@ -555,6 +586,7 @@ describe('utils', () => { warning: false, searchAfterTimes: ['10', '20', '30'], bulkCreateTimes: ['5', '15', '25'], + enrichmentTimes: ['1', '2', '3'], lastLookBackDate: undefined, createdSignalsCount: 3, createdSignals: Array(3).fill(sampleSignalHit()), @@ -571,6 +603,7 @@ describe('utils', () => { warning: false, searchAfterTimes: ['10', '20', '30'], bulkCreateTimes: ['5', '15', '25'], + enrichmentTimes: ['1', '2', '3'], lastLookBackDate: undefined, createdSignalsCount: 3, createdSignals: Array(3).fill(sampleSignalHit()), @@ -583,6 +616,7 @@ describe('utils', () => { warning: false, searchAfterTimes: ['10', '20', '30'], bulkCreateTimes: ['5', '15', '25'], + enrichmentTimes: ['1', '2', '3'], lastLookBackDate: new Date('2020-09-16T03:34:32.390Z'), createdSignalsCount: 3, createdSignals: Array(3).fill(sampleSignalHit()), @@ -599,6 +633,7 @@ describe('utils', () => { warning: false, searchAfterTimes: ['10', '20', '30'], bulkCreateTimes: ['5', '15', '25'], + enrichmentTimes: ['1', '2', '3'], lastLookBackDate: undefined, createdSignalsCount: 3, createdSignals: Array(3).fill(sampleSignalHit()), @@ -611,6 +646,7 @@ describe('utils', () => { warning: false, searchAfterTimes: ['10', '20', '30'], bulkCreateTimes: ['5', '15', '25'], + enrichmentTimes: ['1', '2', '3'], lastLookBackDate: new Date('2020-09-16T03:34:32.390Z'), createdSignalsCount: 3, createdSignals: Array(3).fill(sampleSignalHit()), @@ -632,6 +668,7 @@ describe('utils', () => { warning: false, searchAfterTimes: ['10', '20', '30'], bulkCreateTimes: ['5', '15', '25'], + enrichmentTimes: ['1', '2', '3'], lastLookBackDate: undefined, createdSignalsCount: 3, createdSignals: Array(3).fill(sampleSignalHit()), @@ -644,6 +681,7 @@ describe('utils', () => { warning: false, searchAfterTimes: ['10', '20', '30'], bulkCreateTimes: ['5', '15', '25'], + enrichmentTimes: ['1', '2', '3'], lastLookBackDate: new Date('2020-09-16T03:34:32.390Z'), createdSignalsCount: 3, createdSignals: Array(3).fill(sampleSignalHit()), diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/utils.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/utils.ts index bfba9a6fd22a1..a73ead0bf946d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/utils.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/utils.ts @@ -70,6 +70,7 @@ export const combineResults = ( ): SearchAfterAndBulkCreateReturnType => ({ success: currentResult.success === false ? false : newResult.success, warning: currentResult.warning || newResult.warning, + enrichmentTimes: calculateAdditiveMax(currentResult.enrichmentTimes, newResult.enrichmentTimes), bulkCreateTimes: calculateAdditiveMax(currentResult.bulkCreateTimes, newResult.bulkCreateTimes), searchAfterTimes: calculateAdditiveMax( currentResult.searchAfterTimes, @@ -94,6 +95,7 @@ export const combineConcurrentResults = ( const maxedNewResult = newResult.reduce( (accum, item) => { const maxSearchAfterTime = calculateMax(accum.searchAfterTimes, item.searchAfterTimes); + const maxEnrichmentTimes = calculateMax(accum.enrichmentTimes, item.enrichmentTimes); const maxBulkCreateTimes = calculateMax(accum.bulkCreateTimes, item.bulkCreateTimes); const lastLookBackDate = calculateMaxLookBack(accum.lastLookBackDate, item.lastLookBackDate); return { @@ -101,6 +103,7 @@ export const combineConcurrentResults = ( warning: accum.warning || item.warning, searchAfterTimes: [maxSearchAfterTime], bulkCreateTimes: [maxBulkCreateTimes], + enrichmentTimes: [maxEnrichmentTimes], lastLookBackDate, createdSignalsCount: accum.createdSignalsCount + item.createdSignalsCount, createdSignals: [...accum.createdSignals, ...item.createdSignals], @@ -113,6 +116,7 @@ export const combineConcurrentResults = ( warning: false, searchAfterTimes: [], bulkCreateTimes: [], + enrichmentTimes: [], lastLookBackDate: undefined, createdSignalsCount: 0, createdSignals: [], diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts index 93ddadf826d73..f786a28b50044 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts @@ -286,6 +286,7 @@ export interface SearchAfterAndBulkCreateReturnType { success: boolean; warning: boolean; searchAfterTimes: string[]; + enrichmentTimes: string[]; bulkCreateTimes: string[]; lastLookBackDate: Date | null | undefined; createdSignalsCount: number; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts index d5603130400c7..d80ed256a0de0 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts @@ -953,6 +953,7 @@ describe('utils', () => { }); const expected: SearchAfterAndBulkCreateReturnType = { bulkCreateTimes: [], + enrichmentTimes: [], createdSignalsCount: 0, createdSignals: [], errors: [], @@ -973,6 +974,7 @@ describe('utils', () => { }); const expected: SearchAfterAndBulkCreateReturnType = { bulkCreateTimes: [], + enrichmentTimes: [], createdSignalsCount: 0, createdSignals: [], errors: [], @@ -1291,6 +1293,7 @@ describe('utils', () => { const searchAfterReturnType = createSearchAfterReturnType(); const expected: SearchAfterAndBulkCreateReturnType = { bulkCreateTimes: [], + enrichmentTimes: [], createdSignalsCount: 0, createdSignals: [], errors: [], @@ -1306,6 +1309,7 @@ describe('utils', () => { test('createSearchAfterReturnType can override all values', () => { const searchAfterReturnType = createSearchAfterReturnType({ bulkCreateTimes: ['123'], + enrichmentTimes: [], createdSignalsCount: 5, createdSignals: Array(5).fill(sampleSignalHit()), errors: ['error 1'], @@ -1317,6 +1321,7 @@ describe('utils', () => { }); const expected: SearchAfterAndBulkCreateReturnType = { bulkCreateTimes: ['123'], + enrichmentTimes: [], createdSignalsCount: 5, createdSignals: Array(5).fill(sampleSignalHit()), errors: ['error 1'], @@ -1337,6 +1342,7 @@ describe('utils', () => { }); const expected: SearchAfterAndBulkCreateReturnType = { bulkCreateTimes: [], + enrichmentTimes: [], createdSignalsCount: 5, createdSignals: Array(5).fill(sampleSignalHit()), errors: ['error 1'], @@ -1355,6 +1361,7 @@ describe('utils', () => { const merged = mergeReturns([createSearchAfterReturnType(), createSearchAfterReturnType()]); const expected: SearchAfterAndBulkCreateReturnType = { bulkCreateTimes: [], + enrichmentTimes: [], createdSignalsCount: 0, createdSignals: [], errors: [], @@ -1411,6 +1418,7 @@ describe('utils', () => { const merged = mergeReturns([ createSearchAfterReturnType({ bulkCreateTimes: ['123'], + enrichmentTimes: [], createdSignalsCount: 3, createdSignals: Array(3).fill(sampleSignalHit()), errors: ['error 1', 'error 2'], @@ -1421,6 +1429,7 @@ describe('utils', () => { }), createSearchAfterReturnType({ bulkCreateTimes: ['456'], + enrichmentTimes: [], createdSignalsCount: 2, createdSignals: Array(2).fill(sampleSignalHit()), errors: ['error 3'], @@ -1433,6 +1442,7 @@ describe('utils', () => { ]); const expected: SearchAfterAndBulkCreateReturnType = { bulkCreateTimes: ['123', '456'], // concatenates the prev and next together + enrichmentTimes: [], createdSignalsCount: 5, // Adds the 3 and 2 together createdSignals: Array(5).fill(sampleSignalHit()), errors: ['error 1', 'error 2', 'error 3'], // concatenates the prev and next together @@ -1452,6 +1462,7 @@ describe('utils', () => { const next: GenericBulkCreateResponse = { success: false, bulkCreateDuration: '100', + enrichmentDuration: '0', createdItemsCount: 1, createdItems: [], errors: ['new error'], @@ -1469,6 +1480,7 @@ describe('utils', () => { const next: GenericBulkCreateResponse = { success: true, bulkCreateDuration: '0', + enrichmentDuration: '0', createdItemsCount: 0, createdItems: [], errors: ['error 1'], @@ -1484,6 +1496,7 @@ describe('utils', () => { const next: GenericBulkCreateResponse = { success: true, bulkCreateDuration: '0', + enrichmentDuration: '0', createdItemsCount: 0, createdItems: [], errors: ['error 2'], diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts index 6030400da2b75..24b5b068d5689 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts @@ -649,6 +649,7 @@ export const createSearchAfterReturnType = ({ success, warning, searchAfterTimes, + enrichmentTimes, bulkCreateTimes, lastLookBackDate, createdSignalsCount, @@ -659,6 +660,7 @@ export const createSearchAfterReturnType = ({ success?: boolean | undefined; warning?: boolean; searchAfterTimes?: string[] | undefined; + enrichmentTimes?: string[] | undefined; bulkCreateTimes?: string[] | undefined; lastLookBackDate?: Date | undefined; createdSignalsCount?: number | undefined; @@ -670,6 +672,7 @@ export const createSearchAfterReturnType = ({ success: success ?? true, warning: warning ?? false, searchAfterTimes: searchAfterTimes ?? [], + enrichmentTimes: enrichmentTimes ?? [], bulkCreateTimes: bulkCreateTimes ?? [], lastLookBackDate: lastLookBackDate ?? null, createdSignalsCount: createdSignalsCount ?? 0, @@ -715,6 +718,7 @@ export const addToSearchAfterReturn = ({ current.createdSignalsCount += next.createdItemsCount; current.createdSignals.push(...next.createdItems); current.bulkCreateTimes.push(next.bulkCreateDuration); + current.enrichmentTimes.push(next.enrichmentDuration); current.errors = [...new Set([...current.errors, ...next.errors])]; }; @@ -727,6 +731,7 @@ export const mergeReturns = ( warning: existingWarning, searchAfterTimes: existingSearchAfterTimes, bulkCreateTimes: existingBulkCreateTimes, + enrichmentTimes: existingEnrichmentTimes, lastLookBackDate: existingLastLookBackDate, createdSignalsCount: existingCreatedSignalsCount, createdSignals: existingCreatedSignals, @@ -738,6 +743,7 @@ export const mergeReturns = ( success: newSuccess, warning: newWarning, searchAfterTimes: newSearchAfterTimes, + enrichmentTimes: newEnrichmentTimes, bulkCreateTimes: newBulkCreateTimes, lastLookBackDate: newLastLookBackDate, createdSignalsCount: newCreatedSignalsCount, @@ -750,6 +756,7 @@ export const mergeReturns = ( success: existingSuccess && newSuccess, warning: existingWarning || newWarning, searchAfterTimes: [...existingSearchAfterTimes, ...newSearchAfterTimes], + enrichmentTimes: [...existingEnrichmentTimes, ...newEnrichmentTimes], bulkCreateTimes: [...existingBulkCreateTimes, ...newBulkCreateTimes], lastLookBackDate: newLastLookBackDate ?? existingLastLookBackDate, createdSignalsCount: existingCreatedSignalsCount + newCreatedSignalsCount, diff --git a/x-pack/plugins/security_solution/server/usage/collector.ts b/x-pack/plugins/security_solution/server/usage/collector.ts index e14af8c553736..6f526051f17d1 100644 --- a/x-pack/plugins/security_solution/server/usage/collector.ts +++ b/x-pack/plugins/security_solution/server/usage/collector.ts @@ -414,6 +414,20 @@ export const registerCollector: RegisterCollector = ({ _meta: { description: 'The min duration' }, }, }, + enrichment_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, gap_duration: { max: { type: 'float', @@ -500,6 +514,20 @@ export const registerCollector: RegisterCollector = ({ _meta: { description: 'The min duration' }, }, }, + enrichment_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, gap_duration: { max: { type: 'float', @@ -586,6 +614,20 @@ export const registerCollector: RegisterCollector = ({ _meta: { description: 'The min duration' }, }, }, + enrichment_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, gap_duration: { max: { type: 'float', @@ -672,6 +714,20 @@ export const registerCollector: RegisterCollector = ({ _meta: { description: 'The min duration' }, }, }, + enrichment_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, gap_duration: { max: { type: 'float', @@ -758,6 +814,20 @@ export const registerCollector: RegisterCollector = ({ _meta: { description: 'The min duration' }, }, }, + enrichment_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, gap_duration: { max: { type: 'float', @@ -844,6 +914,20 @@ export const registerCollector: RegisterCollector = ({ _meta: { description: 'The min duration' }, }, }, + enrichment_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, gap_duration: { max: { type: 'float', @@ -946,6 +1030,20 @@ export const registerCollector: RegisterCollector = ({ _meta: { description: 'The min duration' }, }, }, + enrichment_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, gap_duration: { max: { type: 'float', @@ -1032,6 +1130,20 @@ export const registerCollector: RegisterCollector = ({ _meta: { description: 'The min duration' }, }, }, + enrichment_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, gap_duration: { max: { type: 'float', @@ -1118,6 +1230,20 @@ export const registerCollector: RegisterCollector = ({ _meta: { description: 'The min duration' }, }, }, + enrichment_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, gap_duration: { max: { type: 'float', @@ -1204,6 +1330,20 @@ export const registerCollector: RegisterCollector = ({ _meta: { description: 'The min duration' }, }, }, + enrichment_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, gap_duration: { max: { type: 'float', @@ -1290,6 +1430,20 @@ export const registerCollector: RegisterCollector = ({ _meta: { description: 'The min duration' }, }, }, + enrichment_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, gap_duration: { max: { type: 'float', @@ -1376,6 +1530,20 @@ export const registerCollector: RegisterCollector = ({ _meta: { description: 'The min duration' }, }, }, + enrichment_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, gap_duration: { max: { type: 'float', @@ -1478,6 +1646,20 @@ export const registerCollector: RegisterCollector = ({ _meta: { description: 'The min duration' }, }, }, + enrichment_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, gap_duration: { max: { type: 'float', @@ -1564,6 +1746,20 @@ export const registerCollector: RegisterCollector = ({ _meta: { description: 'The min duration' }, }, }, + enrichment_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, gap_duration: { max: { type: 'float', @@ -1650,6 +1846,20 @@ export const registerCollector: RegisterCollector = ({ _meta: { description: 'The min duration' }, }, }, + enrichment_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, gap_duration: { max: { type: 'float', @@ -1736,6 +1946,20 @@ export const registerCollector: RegisterCollector = ({ _meta: { description: 'The min duration' }, }, }, + enrichment_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, gap_duration: { max: { type: 'float', @@ -1822,6 +2046,20 @@ export const registerCollector: RegisterCollector = ({ _meta: { description: 'The min duration' }, }, }, + enrichment_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, gap_duration: { max: { type: 'float', @@ -1908,6 +2146,20 @@ export const registerCollector: RegisterCollector = ({ _meta: { description: 'The min duration' }, }, }, + enrichment_duration: { + max: { + type: 'float', + _meta: { description: 'The max duration' }, + }, + avg: { + type: 'float', + _meta: { description: 'The avg duration' }, + }, + min: { + type: 'float', + _meta: { description: 'The min duration' }, + }, + }, gap_duration: { max: { type: 'float', diff --git a/x-pack/plugins/security_solution/server/usage/detections/rules/get_initial_usage.ts b/x-pack/plugins/security_solution/server/usage/detections/rules/get_initial_usage.ts index eb32c58bda6cf..3ad6b5740b53e 100644 --- a/x-pack/plugins/security_solution/server/usage/detections/rules/get_initial_usage.ts +++ b/x-pack/plugins/security_solution/server/usage/detections/rules/get_initial_usage.ts @@ -144,6 +144,7 @@ export const getInitialSingleEventMetric = (): SingleEventMetric => ({ succeeded: 0, index_duration: getInitialMaxAvgMin(), search_duration: getInitialMaxAvgMin(), + enrichment_duration: getInitialMaxAvgMin(), gap_duration: getInitialMaxAvgMin(), gap_count: 0, }); diff --git a/x-pack/plugins/security_solution/server/usage/detections/rules/get_metrics.mocks.ts b/x-pack/plugins/security_solution/server/usage/detections/rules/get_metrics.mocks.ts index 129d2b32d0b3d..83eb7df28a026 100644 --- a/x-pack/plugins/security_solution/server/usage/detections/rules/get_metrics.mocks.ts +++ b/x-pack/plugins/security_solution/server/usage/detections/rules/get_metrics.mocks.ts @@ -155,6 +155,15 @@ export const getEventLogAllRules = (): SearchResponse ({ avg: 0, min: 0, }, + enrichment_duration: { + max: 0, + avg: 0, + min: 0, + }, gap_count: 0, }, threat_match: { @@ -2506,6 +2835,11 @@ export const getEventLogAllRulesResult = (): SingleEventLogStatusMetric => ({ avg: 0, min: 0, }, + enrichment_duration: { + max: 0, + avg: 0, + min: 0, + }, gap_count: 0, }, machine_learning: { @@ -2529,6 +2863,11 @@ export const getEventLogAllRulesResult = (): SingleEventLogStatusMetric => ({ avg: 0, min: 0, }, + enrichment_duration: { + max: 0, + avg: 0, + min: 0, + }, gap_count: 0, }, query: { @@ -2594,6 +2933,11 @@ export const getEventLogAllRulesResult = (): SingleEventLogStatusMetric => ({ avg: 4246.375, min: 2811, }, + enrichment_duration: { + max: 0, + avg: 0, + min: 0, + }, gap_count: 6, }, saved_query: { @@ -2617,6 +2961,11 @@ export const getEventLogAllRulesResult = (): SingleEventLogStatusMetric => ({ avg: 0, min: 0, }, + enrichment_duration: { + max: 0, + avg: 0, + min: 0, + }, gap_count: 0, }, threshold: { @@ -2640,6 +2989,11 @@ export const getEventLogAllRulesResult = (): SingleEventLogStatusMetric => ({ avg: 0, min: 0, }, + enrichment_duration: { + max: 0, + avg: 0, + min: 0, + }, gap_count: 0, }, total: { @@ -2676,6 +3030,11 @@ export const getEventLogElasticRulesResult = (): SingleEventLogStatusMetric => ( avg: 0, min: 0, }, + enrichment_duration: { + max: 0, + avg: 0, + min: 0, + }, gap_count: 0, }, threat_match: { @@ -2699,6 +3058,11 @@ export const getEventLogElasticRulesResult = (): SingleEventLogStatusMetric => ( avg: 0, min: 0, }, + enrichment_duration: { + max: 0, + avg: 0, + min: 0, + }, gap_count: 0, }, machine_learning: { @@ -2722,6 +3086,11 @@ export const getEventLogElasticRulesResult = (): SingleEventLogStatusMetric => ( avg: 0, min: 0, }, + enrichment_duration: { + max: 0, + avg: 0, + min: 0, + }, gap_count: 0, }, query: { @@ -2772,6 +3141,11 @@ export const getEventLogElasticRulesResult = (): SingleEventLogStatusMetric => ( avg: 4141.75, min: 2811, }, + enrichment_duration: { + max: 0, + avg: 0, + min: 0, + }, gap_count: 4, }, saved_query: { @@ -2795,6 +3169,11 @@ export const getEventLogElasticRulesResult = (): SingleEventLogStatusMetric => ( avg: 0, min: 0, }, + enrichment_duration: { + max: 0, + avg: 0, + min: 0, + }, gap_count: 0, }, threshold: { @@ -2818,6 +3197,11 @@ export const getEventLogElasticRulesResult = (): SingleEventLogStatusMetric => ( avg: 0, min: 0, }, + enrichment_duration: { + max: 0, + avg: 0, + min: 0, + }, gap_count: 0, }, total: { @@ -2854,6 +3238,11 @@ export const getEventLogCustomRulesResult = (): SingleEventLogStatusMetric => ({ avg: 0, min: 0, }, + enrichment_duration: { + max: 0, + avg: 0, + min: 0, + }, gap_count: 0, }, threat_match: { @@ -2877,6 +3266,11 @@ export const getEventLogCustomRulesResult = (): SingleEventLogStatusMetric => ({ avg: 0, min: 0, }, + enrichment_duration: { + max: 0, + avg: 0, + min: 0, + }, gap_count: 0, }, machine_learning: { @@ -2900,6 +3294,11 @@ export const getEventLogCustomRulesResult = (): SingleEventLogStatusMetric => ({ avg: 0, min: 0, }, + enrichment_duration: { + max: 0, + avg: 0, + min: 0, + }, gap_count: 0, }, query: { @@ -2940,6 +3339,11 @@ export const getEventLogCustomRulesResult = (): SingleEventLogStatusMetric => ({ avg: 4351, min: 3051, }, + enrichment_duration: { + max: 0, + avg: 0, + min: 0, + }, gap_count: 2, }, saved_query: { @@ -2963,6 +3367,11 @@ export const getEventLogCustomRulesResult = (): SingleEventLogStatusMetric => ({ avg: 0, min: 0, }, + enrichment_duration: { + max: 0, + avg: 0, + min: 0, + }, gap_count: 0, }, threshold: { @@ -2986,6 +3395,11 @@ export const getEventLogCustomRulesResult = (): SingleEventLogStatusMetric => ({ avg: 0, min: 0, }, + enrichment_duration: { + max: 0, + avg: 0, + min: 0, + }, gap_count: 0, }, total: { diff --git a/x-pack/plugins/security_solution/server/usage/detections/rules/types.ts b/x-pack/plugins/security_solution/server/usage/detections/rules/types.ts index 499c79b11fcfa..84fb656f793d4 100644 --- a/x-pack/plugins/security_solution/server/usage/detections/rules/types.ts +++ b/x-pack/plugins/security_solution/server/usage/detections/rules/types.ts @@ -85,6 +85,7 @@ export interface SingleEventMetric { succeeded: number; index_duration: MaxAvgMin; search_duration: MaxAvgMin; + enrichment_duration: MaxAvgMin; gap_duration: MaxAvgMin; gap_count: number; } diff --git a/x-pack/plugins/security_solution/server/usage/queries/utils/get_event_log_agg_by_rule_type_metrics.test.ts b/x-pack/plugins/security_solution/server/usage/queries/utils/get_event_log_agg_by_rule_type_metrics.test.ts index 09a988fbf02ef..693e3579d7c3a 100644 --- a/x-pack/plugins/security_solution/server/usage/queries/utils/get_event_log_agg_by_rule_type_metrics.test.ts +++ b/x-pack/plugins/security_solution/server/usage/queries/utils/get_event_log_agg_by_rule_type_metrics.test.ts @@ -68,6 +68,21 @@ describe('get_event_log_agg_by_rule_type_metrics', () => { field: 'kibana.alert.rule.execution.metrics.total_search_duration_ms', }, }, + maxTotalEnrichmentDuration: { + max: { + field: 'kibana.alert.rule.execution.metrics.total_enrichment_duration_ms', + }, + }, + minTotalEnrichmentDuration: { + min: { + field: 'kibana.alert.rule.execution.metrics.total_enrichment_duration_ms', + }, + }, + avgTotalEnrichmentDuration: { + avg: { + field: 'kibana.alert.rule.execution.metrics.total_enrichment_duration_ms', + }, + }, }, }); }); diff --git a/x-pack/plugins/security_solution/server/usage/queries/utils/get_event_log_agg_by_rule_type_metrics.ts b/x-pack/plugins/security_solution/server/usage/queries/utils/get_event_log_agg_by_rule_type_metrics.ts index 6fe8103e29a0d..957f56809d64f 100644 --- a/x-pack/plugins/security_solution/server/usage/queries/utils/get_event_log_agg_by_rule_type_metrics.ts +++ b/x-pack/plugins/security_solution/server/usage/queries/utils/get_event_log_agg_by_rule_type_metrics.ts @@ -74,6 +74,21 @@ export const getEventLogAggByRuleTypeMetrics = ( field: 'kibana.alert.rule.execution.metrics.total_search_duration_ms', }, }, + maxTotalEnrichmentDuration: { + max: { + field: 'kibana.alert.rule.execution.metrics.total_enrichment_duration_ms', + }, + }, + minTotalEnrichmentDuration: { + min: { + field: 'kibana.alert.rule.execution.metrics.total_enrichment_duration_ms', + }, + }, + avgTotalEnrichmentDuration: { + avg: { + field: 'kibana.alert.rule.execution.metrics.total_enrichment_duration_ms', + }, + }, }, }; }; diff --git a/x-pack/plugins/security_solution/server/usage/queries/utils/get_event_log_agg_by_rule_types_metrics.test.ts b/x-pack/plugins/security_solution/server/usage/queries/utils/get_event_log_agg_by_rule_types_metrics.test.ts index 22261ac48812c..4673ca1b6bcb8 100644 --- a/x-pack/plugins/security_solution/server/usage/queries/utils/get_event_log_agg_by_rule_types_metrics.test.ts +++ b/x-pack/plugins/security_solution/server/usage/queries/utils/get_event_log_agg_by_rule_types_metrics.test.ts @@ -74,6 +74,21 @@ describe('get_event_log_agg_by_rule_types_metrics', () => { field: 'kibana.alert.rule.execution.metrics.total_search_duration_ms', }, }, + maxTotalEnrichmentDuration: { + max: { + field: 'kibana.alert.rule.execution.metrics.total_enrichment_duration_ms', + }, + }, + minTotalEnrichmentDuration: { + min: { + field: 'kibana.alert.rule.execution.metrics.total_enrichment_duration_ms', + }, + }, + avgTotalEnrichmentDuration: { + avg: { + field: 'kibana.alert.rule.execution.metrics.total_enrichment_duration_ms', + }, + }, }, }, }); @@ -139,6 +154,21 @@ describe('get_event_log_agg_by_rule_types_metrics', () => { field: 'kibana.alert.rule.execution.metrics.total_search_duration_ms', }, }, + maxTotalEnrichmentDuration: { + max: { + field: 'kibana.alert.rule.execution.metrics.total_enrichment_duration_ms', + }, + }, + minTotalEnrichmentDuration: { + min: { + field: 'kibana.alert.rule.execution.metrics.total_enrichment_duration_ms', + }, + }, + avgTotalEnrichmentDuration: { + avg: { + field: 'kibana.alert.rule.execution.metrics.total_enrichment_duration_ms', + }, + }, }, }, }); @@ -204,6 +234,21 @@ describe('get_event_log_agg_by_rule_types_metrics', () => { field: 'kibana.alert.rule.execution.metrics.total_search_duration_ms', }, }, + maxTotalEnrichmentDuration: { + max: { + field: 'kibana.alert.rule.execution.metrics.total_enrichment_duration_ms', + }, + }, + minTotalEnrichmentDuration: { + min: { + field: 'kibana.alert.rule.execution.metrics.total_enrichment_duration_ms', + }, + }, + avgTotalEnrichmentDuration: { + avg: { + field: 'kibana.alert.rule.execution.metrics.total_enrichment_duration_ms', + }, + }, }, }, 'siem.indicatorRule': { @@ -263,6 +308,21 @@ describe('get_event_log_agg_by_rule_types_metrics', () => { field: 'kibana.alert.rule.execution.metrics.total_search_duration_ms', }, }, + maxTotalEnrichmentDuration: { + max: { + field: 'kibana.alert.rule.execution.metrics.total_enrichment_duration_ms', + }, + }, + minTotalEnrichmentDuration: { + min: { + field: 'kibana.alert.rule.execution.metrics.total_enrichment_duration_ms', + }, + }, + avgTotalEnrichmentDuration: { + avg: { + field: 'kibana.alert.rule.execution.metrics.total_enrichment_duration_ms', + }, + }, }, }, }); diff --git a/x-pack/plugins/security_solution/server/usage/queries/utils/get_event_log_agg_by_statuses.test.ts b/x-pack/plugins/security_solution/server/usage/queries/utils/get_event_log_agg_by_statuses.test.ts index 7d474769bd79f..a87046660fe16 100644 --- a/x-pack/plugins/security_solution/server/usage/queries/utils/get_event_log_agg_by_statuses.test.ts +++ b/x-pack/plugins/security_solution/server/usage/queries/utils/get_event_log_agg_by_statuses.test.ts @@ -137,6 +137,21 @@ describe('get_event_log_agg_by_statuses', () => { field: 'kibana.alert.rule.execution.metrics.total_search_duration_ms', }, }, + maxTotalEnrichmentDuration: { + max: { + field: 'kibana.alert.rule.execution.metrics.total_enrichment_duration_ms', + }, + }, + minTotalEnrichmentDuration: { + min: { + field: 'kibana.alert.rule.execution.metrics.total_enrichment_duration_ms', + }, + }, + avgTotalEnrichmentDuration: { + avg: { + field: 'kibana.alert.rule.execution.metrics.total_enrichment_duration_ms', + }, + }, }, }, }, @@ -246,6 +261,21 @@ describe('get_event_log_agg_by_statuses', () => { field: 'kibana.alert.rule.execution.metrics.total_search_duration_ms', }, }, + maxTotalEnrichmentDuration: { + max: { + field: 'kibana.alert.rule.execution.metrics.total_enrichment_duration_ms', + }, + }, + minTotalEnrichmentDuration: { + min: { + field: 'kibana.alert.rule.execution.metrics.total_enrichment_duration_ms', + }, + }, + avgTotalEnrichmentDuration: { + avg: { + field: 'kibana.alert.rule.execution.metrics.total_enrichment_duration_ms', + }, + }, }, }, }, @@ -418,6 +448,21 @@ describe('get_event_log_agg_by_statuses', () => { field: 'kibana.alert.rule.execution.metrics.total_search_duration_ms', }, }, + maxTotalEnrichmentDuration: { + max: { + field: 'kibana.alert.rule.execution.metrics.total_enrichment_duration_ms', + }, + }, + minTotalEnrichmentDuration: { + min: { + field: 'kibana.alert.rule.execution.metrics.total_enrichment_duration_ms', + }, + }, + avgTotalEnrichmentDuration: { + avg: { + field: 'kibana.alert.rule.execution.metrics.total_enrichment_duration_ms', + }, + }, }, }, 'siem.thresholdRule': { @@ -477,6 +522,21 @@ describe('get_event_log_agg_by_statuses', () => { field: 'kibana.alert.rule.execution.metrics.total_search_duration_ms', }, }, + maxTotalEnrichmentDuration: { + max: { + field: 'kibana.alert.rule.execution.metrics.total_enrichment_duration_ms', + }, + }, + minTotalEnrichmentDuration: { + min: { + field: 'kibana.alert.rule.execution.metrics.total_enrichment_duration_ms', + }, + }, + avgTotalEnrichmentDuration: { + avg: { + field: 'kibana.alert.rule.execution.metrics.total_enrichment_duration_ms', + }, + }, }, }, }, diff --git a/x-pack/plugins/security_solution/server/usage/queries/utils/transform_single_rule_metric.test.ts b/x-pack/plugins/security_solution/server/usage/queries/utils/transform_single_rule_metric.test.ts index c64f0833fe851..9c5810011a975 100644 --- a/x-pack/plugins/security_solution/server/usage/queries/utils/transform_single_rule_metric.test.ts +++ b/x-pack/plugins/security_solution/server/usage/queries/utils/transform_single_rule_metric.test.ts @@ -85,6 +85,15 @@ describe('transform_single_rule_metric', () => { minTotalSearchDuration: { value: 12, }, + minTotalEnrichmentDuration: { + value: 4, + }, + maxTotalEnrichmentDuration: { + value: 2, + }, + avgTotalEnrichmentDuration: { + value: 12, + }, }, }); @@ -131,6 +140,11 @@ describe('transform_single_rule_metric', () => { avg: 2, min: 9, }, + enrichment_duration: { + max: 2, + avg: 12, + min: 4, + }, gap_count: 4, }); }); diff --git a/x-pack/plugins/security_solution/server/usage/queries/utils/transform_single_rule_metric.ts b/x-pack/plugins/security_solution/server/usage/queries/utils/transform_single_rule_metric.ts index bebd867fb195f..5b3b6f8b7ebb2 100644 --- a/x-pack/plugins/security_solution/server/usage/queries/utils/transform_single_rule_metric.ts +++ b/x-pack/plugins/security_solution/server/usage/queries/utils/transform_single_rule_metric.ts @@ -52,6 +52,11 @@ export const transformSingleRuleMetric = ({ avg: singleMetric.avgTotalSearchDuration.value ?? 0.0, min: singleMetric.minTotalSearchDuration.value ?? 0.0, }, + enrichment_duration: { + max: singleMetric?.maxTotalEnrichmentDuration?.value ?? 0.0, + avg: singleMetric?.avgTotalEnrichmentDuration?.value ?? 0.0, + min: singleMetric?.minTotalEnrichmentDuration?.value ?? 0.0, + }, gap_duration: { max: singleMetric.maxGapDuration.value ?? 0.0, avg: singleMetric.avgGapDuration.value ?? 0.0, diff --git a/x-pack/plugins/security_solution/server/usage/types.ts b/x-pack/plugins/security_solution/server/usage/types.ts index fe7711196303c..a6db2e3d71e91 100644 --- a/x-pack/plugins/security_solution/server/usage/types.ts +++ b/x-pack/plugins/security_solution/server/usage/types.ts @@ -121,6 +121,15 @@ export interface SingleExecutionMetricAgg { minTotalSearchDuration: { value: number | null; }; + maxTotalEnrichmentDuration: { + value: number | null; + }; + avgTotalEnrichmentDuration: { + value: number | null; + }; + minTotalEnrichmentDuration: { + value: number | null; + }; } export interface EventLogTypeStatusAggs { diff --git a/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json index 0c389000f6c80..9a47ad9de093e 100644 --- a/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json +++ b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json @@ -4644,6 +4644,18 @@ "properties": { "all": { "properties": { + "total": { + "type": "long" + }, + "monthly": { + "type": "long" + }, + "weekly": { + "type": "long" + }, + "daily": { + "type": "long" + }, "assignees": { "properties": { "total": { @@ -4657,18 +4669,6 @@ } } }, - "total": { - "type": "long" - }, - "monthly": { - "type": "long" - }, - "weekly": { - "type": "long" - }, - "daily": { - "type": "long" - }, "status": { "properties": { "open": { @@ -4720,6 +4720,18 @@ }, "sec": { "properties": { + "total": { + "type": "long" + }, + "monthly": { + "type": "long" + }, + "weekly": { + "type": "long" + }, + "daily": { + "type": "long" + }, "assignees": { "properties": { "total": { @@ -4732,7 +4744,11 @@ "type": "long" } } - }, + } + } + }, + "obs": { + "properties": { "total": { "type": "long" }, @@ -4744,11 +4760,7 @@ }, "daily": { "type": "long" - } - } - }, - "obs": { - "properties": { + }, "assignees": { "properties": { "total": { @@ -4761,7 +4773,11 @@ "type": "long" } } - }, + } + } + }, + "main": { + "properties": { "total": { "type": "long" }, @@ -4773,11 +4789,7 @@ }, "daily": { "type": "long" - } - } - }, - "main": { - "properties": { + }, "assignees": { "properties": { "total": { @@ -4790,18 +4802,6 @@ "type": "long" } } - }, - "total": { - "type": "long" - }, - "monthly": { - "type": "long" - }, - "weekly": { - "type": "long" - }, - "daily": { - "type": "long" } } } @@ -10029,6 +10029,28 @@ } } }, + "enrichment_duration": { + "properties": { + "max": { + "type": "float", + "_meta": { + "description": "The max duration" + } + }, + "avg": { + "type": "float", + "_meta": { + "description": "The avg duration" + } + }, + "min": { + "type": "float", + "_meta": { + "description": "The min duration" + } + } + } + }, "gap_duration": { "properties": { "max": { @@ -10161,6 +10183,28 @@ } } }, + "enrichment_duration": { + "properties": { + "max": { + "type": "float", + "_meta": { + "description": "The max duration" + } + }, + "avg": { + "type": "float", + "_meta": { + "description": "The avg duration" + } + }, + "min": { + "type": "float", + "_meta": { + "description": "The min duration" + } + } + } + }, "gap_duration": { "properties": { "max": { @@ -10293,6 +10337,28 @@ } } }, + "enrichment_duration": { + "properties": { + "max": { + "type": "float", + "_meta": { + "description": "The max duration" + } + }, + "avg": { + "type": "float", + "_meta": { + "description": "The avg duration" + } + }, + "min": { + "type": "float", + "_meta": { + "description": "The min duration" + } + } + } + }, "gap_duration": { "properties": { "max": { @@ -10425,6 +10491,28 @@ } } }, + "enrichment_duration": { + "properties": { + "max": { + "type": "float", + "_meta": { + "description": "The max duration" + } + }, + "avg": { + "type": "float", + "_meta": { + "description": "The avg duration" + } + }, + "min": { + "type": "float", + "_meta": { + "description": "The min duration" + } + } + } + }, "gap_duration": { "properties": { "max": { @@ -10557,6 +10645,28 @@ } } }, + "enrichment_duration": { + "properties": { + "max": { + "type": "float", + "_meta": { + "description": "The max duration" + } + }, + "avg": { + "type": "float", + "_meta": { + "description": "The avg duration" + } + }, + "min": { + "type": "float", + "_meta": { + "description": "The min duration" + } + } + } + }, "gap_duration": { "properties": { "max": { @@ -10689,6 +10799,28 @@ } } }, + "enrichment_duration": { + "properties": { + "max": { + "type": "float", + "_meta": { + "description": "The max duration" + } + }, + "avg": { + "type": "float", + "_meta": { + "description": "The avg duration" + } + }, + "min": { + "type": "float", + "_meta": { + "description": "The min duration" + } + } + } + }, "gap_duration": { "properties": { "max": { @@ -10847,6 +10979,28 @@ } } }, + "enrichment_duration": { + "properties": { + "max": { + "type": "float", + "_meta": { + "description": "The max duration" + } + }, + "avg": { + "type": "float", + "_meta": { + "description": "The avg duration" + } + }, + "min": { + "type": "float", + "_meta": { + "description": "The min duration" + } + } + } + }, "gap_duration": { "properties": { "max": { @@ -10979,6 +11133,28 @@ } } }, + "enrichment_duration": { + "properties": { + "max": { + "type": "float", + "_meta": { + "description": "The max duration" + } + }, + "avg": { + "type": "float", + "_meta": { + "description": "The avg duration" + } + }, + "min": { + "type": "float", + "_meta": { + "description": "The min duration" + } + } + } + }, "gap_duration": { "properties": { "max": { @@ -11111,6 +11287,28 @@ } } }, + "enrichment_duration": { + "properties": { + "max": { + "type": "float", + "_meta": { + "description": "The max duration" + } + }, + "avg": { + "type": "float", + "_meta": { + "description": "The avg duration" + } + }, + "min": { + "type": "float", + "_meta": { + "description": "The min duration" + } + } + } + }, "gap_duration": { "properties": { "max": { @@ -11243,6 +11441,28 @@ } } }, + "enrichment_duration": { + "properties": { + "max": { + "type": "float", + "_meta": { + "description": "The max duration" + } + }, + "avg": { + "type": "float", + "_meta": { + "description": "The avg duration" + } + }, + "min": { + "type": "float", + "_meta": { + "description": "The min duration" + } + } + } + }, "gap_duration": { "properties": { "max": { @@ -11375,6 +11595,28 @@ } } }, + "enrichment_duration": { + "properties": { + "max": { + "type": "float", + "_meta": { + "description": "The max duration" + } + }, + "avg": { + "type": "float", + "_meta": { + "description": "The avg duration" + } + }, + "min": { + "type": "float", + "_meta": { + "description": "The min duration" + } + } + } + }, "gap_duration": { "properties": { "max": { @@ -11507,6 +11749,28 @@ } } }, + "enrichment_duration": { + "properties": { + "max": { + "type": "float", + "_meta": { + "description": "The max duration" + } + }, + "avg": { + "type": "float", + "_meta": { + "description": "The avg duration" + } + }, + "min": { + "type": "float", + "_meta": { + "description": "The min duration" + } + } + } + }, "gap_duration": { "properties": { "max": { @@ -11665,6 +11929,28 @@ } } }, + "enrichment_duration": { + "properties": { + "max": { + "type": "float", + "_meta": { + "description": "The max duration" + } + }, + "avg": { + "type": "float", + "_meta": { + "description": "The avg duration" + } + }, + "min": { + "type": "float", + "_meta": { + "description": "The min duration" + } + } + } + }, "gap_duration": { "properties": { "max": { @@ -11797,6 +12083,28 @@ } } }, + "enrichment_duration": { + "properties": { + "max": { + "type": "float", + "_meta": { + "description": "The max duration" + } + }, + "avg": { + "type": "float", + "_meta": { + "description": "The avg duration" + } + }, + "min": { + "type": "float", + "_meta": { + "description": "The min duration" + } + } + } + }, "gap_duration": { "properties": { "max": { @@ -11929,6 +12237,28 @@ } } }, + "enrichment_duration": { + "properties": { + "max": { + "type": "float", + "_meta": { + "description": "The max duration" + } + }, + "avg": { + "type": "float", + "_meta": { + "description": "The avg duration" + } + }, + "min": { + "type": "float", + "_meta": { + "description": "The min duration" + } + } + } + }, "gap_duration": { "properties": { "max": { @@ -12061,6 +12391,28 @@ } } }, + "enrichment_duration": { + "properties": { + "max": { + "type": "float", + "_meta": { + "description": "The max duration" + } + }, + "avg": { + "type": "float", + "_meta": { + "description": "The avg duration" + } + }, + "min": { + "type": "float", + "_meta": { + "description": "The min duration" + } + } + } + }, "gap_duration": { "properties": { "max": { @@ -12193,6 +12545,28 @@ } } }, + "enrichment_duration": { + "properties": { + "max": { + "type": "float", + "_meta": { + "description": "The max duration" + } + }, + "avg": { + "type": "float", + "_meta": { + "description": "The avg duration" + } + }, + "min": { + "type": "float", + "_meta": { + "description": "The min duration" + } + } + } + }, "gap_duration": { "properties": { "max": { @@ -12325,6 +12699,28 @@ } } }, + "enrichment_duration": { + "properties": { + "max": { + "type": "float", + "_meta": { + "description": "The max duration" + } + }, + "avg": { + "type": "float", + "_meta": { + "description": "The avg duration" + } + }, + "min": { + "type": "float", + "_meta": { + "description": "The min duration" + } + } + } + }, "gap_duration": { "properties": { "max": { From 4c12ff3fddf6edab74147ce9a714bc1e5e91c65e Mon Sep 17 00:00:00 2001 From: Kevin Delemme Date: Wed, 28 Sep 2022 08:11:57 -0400 Subject: [PATCH 11/17] chore(slo): remove space slo resources (#141662) --- .../observability/server/assets/constants.ts | 8 +--- x-pack/plugins/observability/server/plugin.ts | 2 - .../server/routes/register_routes.ts | 4 -- .../observability/server/routes/slo/route.ts | 22 ++------- .../server/services/slo/delete_slo.test.ts | 1 + .../server/services/slo/delete_slo.ts | 3 ++ .../services/slo/resource_installer.test.ts | 19 ++------ .../server/services/slo/resource_installer.ts | 23 ++++------ .../apm_transaction_duration.test.ts.snap | 4 +- .../apm_transaction_error_rate.test.ts.snap | 4 +- .../apm_transaction_duration.test.ts | 4 +- .../apm_transaction_duration.ts | 14 +++--- .../apm_transaction_error_rate.test.ts | 6 +-- .../apm_transaction_error_rate.ts | 14 +++--- .../transform_generator.ts | 2 +- .../services/slo/transform_manager.test.ts | 46 +++---------------- .../server/services/slo/transform_manager.ts | 5 +- 17 files changed, 58 insertions(+), 123 deletions(-) diff --git a/x-pack/plugins/observability/server/assets/constants.ts b/x-pack/plugins/observability/server/assets/constants.ts index 4c0cc0e2e6f83..182ca89712dcc 100644 --- a/x-pack/plugins/observability/server/assets/constants.ts +++ b/x-pack/plugins/observability/server/assets/constants.ts @@ -9,11 +9,7 @@ export const SLO_COMPONENT_TEMPLATE_MAPPINGS_NAME = 'slo-observability.sli-mappi export const SLO_COMPONENT_TEMPLATE_SETTINGS_NAME = 'slo-observability.sli-settings'; export const SLO_INDEX_TEMPLATE_NAME = 'slo-observability.sli'; export const SLO_RESOURCES_VERSION = 1; - -export const getSLOIngestPipelineName = (spaceId: string) => - `${SLO_INDEX_TEMPLATE_NAME}.monthly-${spaceId}`; - -export const getSLODestinationIndexName = (spaceId: string) => - `${SLO_INDEX_TEMPLATE_NAME}-v${SLO_RESOURCES_VERSION}-${spaceId}`; +export const SLO_INGEST_PIPELINE_NAME = `${SLO_INDEX_TEMPLATE_NAME}.monthly`; +export const SLO_DESTINATION_INDEX_NAME = `${SLO_INDEX_TEMPLATE_NAME}-v${SLO_RESOURCES_VERSION}`; export const getSLOTransformId = (sloId: string) => `slo-${sloId}`; diff --git a/x-pack/plugins/observability/server/plugin.ts b/x-pack/plugins/observability/server/plugin.ts index 1d9d3cbf455f1..ff5fd246bea1b 100644 --- a/x-pack/plugins/observability/server/plugin.ts +++ b/x-pack/plugins/observability/server/plugin.ts @@ -144,7 +144,6 @@ export class ObservabilityPlugin implements Plugin { const start = () => core.getStartServices().then(([coreStart]) => coreStart); - const { spacesService } = plugins.spaces; const { ruleDataService } = plugins.ruleRegistry; registerRoutes({ @@ -155,7 +154,6 @@ export class ObservabilityPlugin implements Plugin { logger: this.initContext.logger.get(), repository: getGlobalObservabilityServerRouteRepository(config), ruleDataService, - spacesService, }); return { diff --git a/x-pack/plugins/observability/server/routes/register_routes.ts b/x-pack/plugins/observability/server/routes/register_routes.ts index e0e9e94f5cb21..1c45eb6001479 100644 --- a/x-pack/plugins/observability/server/routes/register_routes.ts +++ b/x-pack/plugins/observability/server/routes/register_routes.ts @@ -14,7 +14,6 @@ import { CoreSetup, CoreStart, Logger, RouteRegistrar } from '@kbn/core/server'; import Boom from '@hapi/boom'; import { errors } from '@elastic/elasticsearch'; import { RuleDataPluginService } from '@kbn/rule-registry-plugin/server'; -import { SpacesServiceStart } from '@kbn/spaces-plugin/server'; import { ObservabilityRequestHandlerContext } from '../types'; import { AbstractObservabilityServerRouteRepository } from './types'; import { getHTTPResponseCode, ObservabilityError } from '../errors'; @@ -24,7 +23,6 @@ export function registerRoutes({ core, logger, ruleDataService, - spacesService, }: { core: { setup: CoreSetup; @@ -33,7 +31,6 @@ export function registerRoutes({ repository: AbstractObservabilityServerRouteRepository; logger: Logger; ruleDataService: RuleDataPluginService; - spacesService: SpacesServiceStart; }) { const routes = Object.values(repository); @@ -67,7 +64,6 @@ export function registerRoutes({ logger, params: decodedParams, ruleDataService, - spacesService, })) as any; if (data === undefined) { diff --git a/x-pack/plugins/observability/server/routes/slo/route.ts b/x-pack/plugins/observability/server/routes/slo/route.ts index 3f04d5e0b13c6..63ef4e1e07e64 100644 --- a/x-pack/plugins/observability/server/routes/slo/route.ts +++ b/x-pack/plugins/observability/server/routes/slo/route.ts @@ -38,19 +38,13 @@ const createSLORoute = createObservabilityServerRoute({ tags: [], }, params: createSLOParamsSchema, - handler: async ({ context, request, params, logger, spacesService }) => { + handler: async ({ context, params, logger }) => { const esClient = (await context.core).elasticsearch.client.asCurrentUser; const soClient = (await context.core).savedObjects.client; - const spaceId = spacesService.getSpaceId(request); - const resourceInstaller = new DefaultResourceInstaller(esClient, logger, spaceId); + const resourceInstaller = new DefaultResourceInstaller(esClient, logger); const repository = new KibanaSavedObjectsSLORepository(soClient); - const transformManager = new DefaultTransformManager( - transformGenerators, - esClient, - logger, - spaceId - ); + const transformManager = new DefaultTransformManager(transformGenerators, esClient, logger); const createSLO = new CreateSLO(resourceInstaller, repository, transformManager); const response = await createSLO.execute(params.body); @@ -65,18 +59,12 @@ const deleteSLORoute = createObservabilityServerRoute({ tags: [], }, params: deleteSLOParamsSchema, - handler: async ({ context, request, params, logger, spacesService }) => { + handler: async ({ context, params, logger }) => { const esClient = (await context.core).elasticsearch.client.asCurrentUser; const soClient = (await context.core).savedObjects.client; - const spaceId = spacesService.getSpaceId(request); const repository = new KibanaSavedObjectsSLORepository(soClient); - const transformManager = new DefaultTransformManager( - transformGenerators, - esClient, - logger, - spaceId - ); + const transformManager = new DefaultTransformManager(transformGenerators, esClient, logger); const deleteSLO = new DeleteSLO(repository, transformManager, esClient); diff --git a/x-pack/plugins/observability/server/services/slo/delete_slo.test.ts b/x-pack/plugins/observability/server/services/slo/delete_slo.test.ts index b1a68b9c04aee..2e43c81f6d382 100644 --- a/x-pack/plugins/observability/server/services/slo/delete_slo.test.ts +++ b/x-pack/plugins/observability/server/services/slo/delete_slo.test.ts @@ -33,6 +33,7 @@ describe('DeleteSLO', () => { await deleteSLO.execute(slo.id); + expect(mockRepository.findById).toHaveBeenCalledWith(slo.id); expect(mockTransformManager.stop).toHaveBeenCalledWith(getSLOTransformId(slo.id)); expect(mockTransformManager.uninstall).toHaveBeenCalledWith(getSLOTransformId(slo.id)); expect(mockEsClient.deleteByQuery).toHaveBeenCalledWith( diff --git a/x-pack/plugins/observability/server/services/slo/delete_slo.ts b/x-pack/plugins/observability/server/services/slo/delete_slo.ts index 59e5df8a5975a..a7d931174a59a 100644 --- a/x-pack/plugins/observability/server/services/slo/delete_slo.ts +++ b/x-pack/plugins/observability/server/services/slo/delete_slo.ts @@ -19,6 +19,9 @@ export class DeleteSLO { ) {} public async execute(sloId: string): Promise { + // ensure the slo exists on the request's space. + await this.repository.findById(sloId); + const sloTransformId = getSLOTransformId(sloId); await this.transformManager.stop(sloTransformId); await this.transformManager.uninstall(sloTransformId); diff --git a/x-pack/plugins/observability/server/services/slo/resource_installer.test.ts b/x-pack/plugins/observability/server/services/slo/resource_installer.test.ts index f8746f38cd246..90749176513da 100644 --- a/x-pack/plugins/observability/server/services/slo/resource_installer.test.ts +++ b/x-pack/plugins/observability/server/services/slo/resource_installer.test.ts @@ -9,7 +9,7 @@ import { IngestGetPipelineResponse } from '@elastic/elasticsearch/lib/api/typesW import { elasticsearchServiceMock } from '@kbn/core/server/mocks'; import { loggerMock } from '@kbn/logging-mocks'; import { - getSLOIngestPipelineName, + SLO_INGEST_PIPELINE_NAME, SLO_COMPONENT_TEMPLATE_MAPPINGS_NAME, SLO_COMPONENT_TEMPLATE_SETTINGS_NAME, SLO_INDEX_TEMPLATE_NAME, @@ -17,17 +17,12 @@ import { } from '../../assets/constants'; import { DefaultResourceInstaller } from './resource_installer'; -const SPACE_ID = 'space-id'; describe('resourceInstaller', () => { describe("when the common resources don't exist", () => { it('installs the common resources', async () => { const mockClusterClient = elasticsearchServiceMock.createElasticsearchClient(); mockClusterClient.indices.existsIndexTemplate.mockResponseOnce(false); - const installer = new DefaultResourceInstaller( - mockClusterClient, - loggerMock.create(), - SPACE_ID - ); + const installer = new DefaultResourceInstaller(mockClusterClient, loggerMock.create()); await installer.ensureCommonResourcesInstalled(); @@ -44,7 +39,7 @@ describe('resourceInstaller', () => { expect.objectContaining({ name: SLO_INDEX_TEMPLATE_NAME }) ); expect(mockClusterClient.ingest.putPipeline).toHaveBeenCalledWith( - expect.objectContaining({ id: getSLOIngestPipelineName(SPACE_ID) }) + expect.objectContaining({ id: SLO_INGEST_PIPELINE_NAME }) ); }); }); @@ -55,13 +50,9 @@ describe('resourceInstaller', () => { mockClusterClient.indices.existsIndexTemplate.mockResponseOnce(true); mockClusterClient.ingest.getPipeline.mockResponseOnce({ // @ts-ignore _meta not typed properly - [getSLOIngestPipelineName(SPACE_ID)]: { _meta: { version: SLO_RESOURCES_VERSION } }, + [SLO_INGEST_PIPELINE_NAME]: { _meta: { version: SLO_RESOURCES_VERSION } }, } as IngestGetPipelineResponse); - const installer = new DefaultResourceInstaller( - mockClusterClient, - loggerMock.create(), - SPACE_ID - ); + const installer = new DefaultResourceInstaller(mockClusterClient, loggerMock.create()); await installer.ensureCommonResourcesInstalled(); diff --git a/x-pack/plugins/observability/server/services/slo/resource_installer.ts b/x-pack/plugins/observability/server/services/slo/resource_installer.ts index 8ed8108b3c4b7..abc02052097b5 100644 --- a/x-pack/plugins/observability/server/services/slo/resource_installer.ts +++ b/x-pack/plugins/observability/server/services/slo/resource_installer.ts @@ -13,7 +13,7 @@ import type { import type { ElasticsearchClient, Logger } from '@kbn/core/server'; import { - getSLOIngestPipelineName, + SLO_INGEST_PIPELINE_NAME, SLO_COMPONENT_TEMPLATE_MAPPINGS_NAME, SLO_COMPONENT_TEMPLATE_SETTINGS_NAME, SLO_INDEX_TEMPLATE_NAME, @@ -29,11 +29,7 @@ export interface ResourceInstaller { } export class DefaultResourceInstaller implements ResourceInstaller { - constructor( - private esClient: ElasticsearchClient, - private logger: Logger, - private spaceId: string - ) {} + constructor(private esClient: ElasticsearchClient, private logger: Logger) {} public async ensureCommonResourcesInstalled(): Promise { const alreadyInstalled = await this.areResourcesAlreadyInstalled(); @@ -64,8 +60,8 @@ export class DefaultResourceInstaller implements ResourceInstaller { await this.createOrUpdateIngestPipelineTemplate( getSLOPipelineTemplate( - getSLOIngestPipelineName(this.spaceId), - this.getPipelinePrefix(SLO_RESOURCES_VERSION, this.spaceId) + SLO_INGEST_PIPELINE_NAME, + this.getPipelinePrefix(SLO_RESOURCES_VERSION) ) ); } catch (err) { @@ -74,10 +70,10 @@ export class DefaultResourceInstaller implements ResourceInstaller { } } - private getPipelinePrefix(version: number, spaceId: string): string { + private getPipelinePrefix(version: number): string { // Following https://www.elastic.co/blog/an-introduction-to-the-elastic-data-stream-naming-scheme - // slo-observability.sli--. - return `${SLO_INDEX_TEMPLATE_NAME}-v${version}-${spaceId}.`; + // slo-observability.sli-. + return `${SLO_INDEX_TEMPLATE_NAME}-v${version}.`; } private async areResourcesAlreadyInstalled(): Promise { @@ -87,12 +83,11 @@ export class DefaultResourceInstaller implements ResourceInstaller { let ingestPipelineExists = false; try { - const pipelineName = getSLOIngestPipelineName(this.spaceId); - const pipeline = await this.esClient.ingest.getPipeline({ id: pipelineName }); + const pipeline = await this.esClient.ingest.getPipeline({ id: SLO_INGEST_PIPELINE_NAME }); ingestPipelineExists = // @ts-ignore _meta is not defined on the type - pipeline && pipeline[pipelineName]._meta.version === SLO_RESOURCES_VERSION; + pipeline && pipeline[SLO_INGEST_PIPELINE_NAME]._meta.version === SLO_RESOURCES_VERSION; } catch (err) { return false; } diff --git a/x-pack/plugins/observability/server/services/slo/transform_generators/__snapshots__/apm_transaction_duration.test.ts.snap b/x-pack/plugins/observability/server/services/slo/transform_generators/__snapshots__/apm_transaction_duration.test.ts.snap index 239c3c93503b9..7b7f49061fd57 100644 --- a/x-pack/plugins/observability/server/services/slo/transform_generators/__snapshots__/apm_transaction_duration.test.ts.snap +++ b/x-pack/plugins/observability/server/services/slo/transform_generators/__snapshots__/apm_transaction_duration.test.ts.snap @@ -20,8 +20,8 @@ Object { "version": 1, }, "dest": Object { - "index": "slo-observability.sli-v1-my-namespace", - "pipeline": "slo-observability.sli.monthly-my-namespace", + "index": "slo-observability.sli-v1", + "pipeline": "slo-observability.sli.monthly", }, "frequency": "1m", "pivot": Object { diff --git a/x-pack/plugins/observability/server/services/slo/transform_generators/__snapshots__/apm_transaction_error_rate.test.ts.snap b/x-pack/plugins/observability/server/services/slo/transform_generators/__snapshots__/apm_transaction_error_rate.test.ts.snap index 1fdaec28f8977..8b73f76a8082d 100644 --- a/x-pack/plugins/observability/server/services/slo/transform_generators/__snapshots__/apm_transaction_error_rate.test.ts.snap +++ b/x-pack/plugins/observability/server/services/slo/transform_generators/__snapshots__/apm_transaction_error_rate.test.ts.snap @@ -20,8 +20,8 @@ Object { "version": 1, }, "dest": Object { - "index": "slo-observability.sli-v1-my-namespace", - "pipeline": "slo-observability.sli.monthly-my-namespace", + "index": "slo-observability.sli-v1", + "pipeline": "slo-observability.sli.monthly", }, "frequency": "1m", "pivot": Object { diff --git a/x-pack/plugins/observability/server/services/slo/transform_generators/apm_transaction_duration.test.ts b/x-pack/plugins/observability/server/services/slo/transform_generators/apm_transaction_duration.test.ts index 1e8fadf365d72..ba984e542619b 100644 --- a/x-pack/plugins/observability/server/services/slo/transform_generators/apm_transaction_duration.test.ts +++ b/x-pack/plugins/observability/server/services/slo/transform_generators/apm_transaction_duration.test.ts @@ -13,7 +13,7 @@ const generator = new ApmTransactionDurationTransformGenerator(); describe('APM Transaction Duration Transform Generator', () => { it('returns the correct transform params with every specified indicator params', async () => { const anSLO = createSLO(createAPMTransactionDurationIndicator()); - const transform = generator.getTransformParams(anSLO, 'my-namespace'); + const transform = generator.getTransformParams(anSLO); expect(transform).toMatchSnapshot({ transform_id: expect.any(String), @@ -34,7 +34,7 @@ describe('APM Transaction Duration Transform Generator', () => { transaction_type: '*', }) ); - const transform = generator.getTransformParams(anSLO, 'my-namespace'); + const transform = generator.getTransformParams(anSLO); expect(transform.source.query).toMatchSnapshot(); }); diff --git a/x-pack/plugins/observability/server/services/slo/transform_generators/apm_transaction_duration.ts b/x-pack/plugins/observability/server/services/slo/transform_generators/apm_transaction_duration.ts index 4c46421737630..bc45e12abbb30 100644 --- a/x-pack/plugins/observability/server/services/slo/transform_generators/apm_transaction_duration.ts +++ b/x-pack/plugins/observability/server/services/slo/transform_generators/apm_transaction_duration.ts @@ -12,8 +12,8 @@ import { } from '@elastic/elasticsearch/lib/api/types'; import { ALL_VALUE } from '../../../types/schema'; import { - getSLODestinationIndexName, - getSLOIngestPipelineName, + SLO_DESTINATION_INDEX_NAME, + SLO_INGEST_PIPELINE_NAME, getSLOTransformId, } from '../../../assets/constants'; import { getSLOTransformTemplate } from '../../../assets/transform_templates/slo_transform_template'; @@ -27,7 +27,7 @@ import { TransformGenerator } from '.'; const APM_SOURCE_INDEX = 'metrics-apm*'; export class ApmTransactionDurationTransformGenerator implements TransformGenerator { - public getTransformParams(slo: SLO, spaceId: string): TransformPutTransformRequest { + public getTransformParams(slo: SLO): TransformPutTransformRequest { if (!apmTransactionDurationSLOSchema.is(slo)) { throw new Error(`Cannot handle SLO of indicator type: ${slo.indicator.type}`); } @@ -35,7 +35,7 @@ export class ApmTransactionDurationTransformGenerator implements TransformGenera return getSLOTransformTemplate( this.buildTransformId(slo), this.buildSource(slo), - this.buildDestination(slo, spaceId), + this.buildDestination(), this.buildGroupBy(), this.buildAggregations(slo) ); @@ -104,10 +104,10 @@ export class ApmTransactionDurationTransformGenerator implements TransformGenera }; } - private buildDestination(slo: APMTransactionDurationSLO, spaceId: string) { + private buildDestination() { return { - pipeline: getSLOIngestPipelineName(spaceId), - index: getSLODestinationIndexName(spaceId), + pipeline: SLO_INGEST_PIPELINE_NAME, + index: SLO_DESTINATION_INDEX_NAME, }; } diff --git a/x-pack/plugins/observability/server/services/slo/transform_generators/apm_transaction_error_rate.test.ts b/x-pack/plugins/observability/server/services/slo/transform_generators/apm_transaction_error_rate.test.ts index afa904fa1f8cb..2bc88c576f8c4 100644 --- a/x-pack/plugins/observability/server/services/slo/transform_generators/apm_transaction_error_rate.test.ts +++ b/x-pack/plugins/observability/server/services/slo/transform_generators/apm_transaction_error_rate.test.ts @@ -13,7 +13,7 @@ const generator = new ApmTransactionErrorRateTransformGenerator(); describe('APM Transaction Error Rate Transform Generator', () => { it('returns the correct transform params with every specified indicator params', async () => { const anSLO = createSLO(createAPMTransactionErrorRateIndicator()); - const transform = generator.getTransformParams(anSLO, 'my-namespace'); + const transform = generator.getTransformParams(anSLO); expect(transform).toMatchSnapshot({ transform_id: expect.any(String), @@ -27,7 +27,7 @@ describe('APM Transaction Error Rate Transform Generator', () => { it("uses default values when 'good_status_codes' is not specified", async () => { const anSLO = createSLO(createAPMTransactionErrorRateIndicator({ good_status_codes: [] })); - const transform = generator.getTransformParams(anSLO, 'my-namespace'); + const transform = generator.getTransformParams(anSLO); expect(transform.pivot?.aggregations).toMatchSnapshot(); }); @@ -41,7 +41,7 @@ describe('APM Transaction Error Rate Transform Generator', () => { transaction_type: '*', }) ); - const transform = generator.getTransformParams(anSLO, 'my-namespace'); + const transform = generator.getTransformParams(anSLO); expect(transform.source.query).toMatchSnapshot(); }); diff --git a/x-pack/plugins/observability/server/services/slo/transform_generators/apm_transaction_error_rate.ts b/x-pack/plugins/observability/server/services/slo/transform_generators/apm_transaction_error_rate.ts index 6740bee2b707f..23a9a03f6e14c 100644 --- a/x-pack/plugins/observability/server/services/slo/transform_generators/apm_transaction_error_rate.ts +++ b/x-pack/plugins/observability/server/services/slo/transform_generators/apm_transaction_error_rate.ts @@ -14,8 +14,8 @@ import { ALL_VALUE } from '../../../types/schema'; import { getSLOTransformTemplate } from '../../../assets/transform_templates/slo_transform_template'; import { TransformGenerator } from '.'; import { - getSLODestinationIndexName, - getSLOIngestPipelineName, + SLO_DESTINATION_INDEX_NAME, + SLO_INGEST_PIPELINE_NAME, getSLOTransformId, } from '../../../assets/constants'; import { @@ -29,7 +29,7 @@ const ALLOWED_STATUS_CODES = ['2xx', '3xx', '4xx', '5xx']; const DEFAULT_GOOD_STATUS_CODES = ['2xx', '3xx', '4xx']; export class ApmTransactionErrorRateTransformGenerator implements TransformGenerator { - public getTransformParams(slo: SLO, spaceId: string): TransformPutTransformRequest { + public getTransformParams(slo: SLO): TransformPutTransformRequest { if (!apmTransactionErrorRateSLOSchema.is(slo)) { throw new Error(`Cannot handle SLO of indicator type: ${slo.indicator.type}`); } @@ -37,7 +37,7 @@ export class ApmTransactionErrorRateTransformGenerator implements TransformGener return getSLOTransformTemplate( this.buildTransformId(slo), this.buildSource(slo), - this.buildDestination(slo, spaceId), + this.buildDestination(), this.buildGroupBy(), this.buildAggregations(slo) ); @@ -106,10 +106,10 @@ export class ApmTransactionErrorRateTransformGenerator implements TransformGener }; } - private buildDestination(slo: APMTransactionErrorRateSLO, spaceId: string) { + private buildDestination() { return { - pipeline: getSLOIngestPipelineName(spaceId), - index: getSLODestinationIndexName(spaceId), + pipeline: SLO_INGEST_PIPELINE_NAME, + index: SLO_DESTINATION_INDEX_NAME, }; } diff --git a/x-pack/plugins/observability/server/services/slo/transform_generators/transform_generator.ts b/x-pack/plugins/observability/server/services/slo/transform_generators/transform_generator.ts index 21a917ea1af6d..3965e809373c8 100644 --- a/x-pack/plugins/observability/server/services/slo/transform_generators/transform_generator.ts +++ b/x-pack/plugins/observability/server/services/slo/transform_generators/transform_generator.ts @@ -9,5 +9,5 @@ import { TransformPutTransformRequest } from '@elastic/elasticsearch/lib/api/typ import { SLO } from '../../../types/models'; export interface TransformGenerator { - getTransformParams(slo: SLO, spaceId: string): TransformPutTransformRequest; + getTransformParams(slo: SLO): TransformPutTransformRequest; } diff --git a/x-pack/plugins/observability/server/services/slo/transform_manager.test.ts b/x-pack/plugins/observability/server/services/slo/transform_manager.test.ts index 1badb6b08e49f..434e6841ff0e9 100644 --- a/x-pack/plugins/observability/server/services/slo/transform_manager.test.ts +++ b/x-pack/plugins/observability/server/services/slo/transform_manager.test.ts @@ -23,8 +23,6 @@ import { import { SLO, SLITypes } from '../../types/models'; import { createAPMTransactionErrorRateIndicator, createSLO } from './fixtures/slo'; -const SPACE_ID = 'space-id'; - describe('TransformManager', () => { let esClientMock: ElasticsearchClientMock; let loggerMock: jest.Mocked; @@ -41,7 +39,7 @@ describe('TransformManager', () => { const generators: Record = { 'slo.apm.transaction_duration': new DummyTransformGenerator(), }; - const service = new DefaultTransformManager(generators, esClientMock, loggerMock, SPACE_ID); + const service = new DefaultTransformManager(generators, esClientMock, loggerMock); await expect( service.install( @@ -63,12 +61,7 @@ describe('TransformManager', () => { const generators: Record = { 'slo.apm.transaction_duration': new FailTransformGenerator(), }; - const transformManager = new DefaultTransformManager( - generators, - esClientMock, - loggerMock, - SPACE_ID - ); + const transformManager = new DefaultTransformManager(generators, esClientMock, loggerMock); await expect( transformManager.install( @@ -92,12 +85,7 @@ describe('TransformManager', () => { const generators: Record = { 'slo.apm.transaction_error_rate': new ApmTransactionErrorRateTransformGenerator(), }; - const transformManager = new DefaultTransformManager( - generators, - esClientMock, - loggerMock, - SPACE_ID - ); + const transformManager = new DefaultTransformManager(generators, esClientMock, loggerMock); const slo = createSLO(createAPMTransactionErrorRateIndicator()); const transformId = await transformManager.install(slo); @@ -113,12 +101,7 @@ describe('TransformManager', () => { const generators: Record = { 'slo.apm.transaction_error_rate': new ApmTransactionErrorRateTransformGenerator(), }; - const transformManager = new DefaultTransformManager( - generators, - esClientMock, - loggerMock, - SPACE_ID - ); + const transformManager = new DefaultTransformManager(generators, esClientMock, loggerMock); await transformManager.start('slo-transform-id'); @@ -132,12 +115,7 @@ describe('TransformManager', () => { const generators: Record = { 'slo.apm.transaction_error_rate': new ApmTransactionErrorRateTransformGenerator(), }; - const transformManager = new DefaultTransformManager( - generators, - esClientMock, - loggerMock, - SPACE_ID - ); + const transformManager = new DefaultTransformManager(generators, esClientMock, loggerMock); await transformManager.stop('slo-transform-id'); @@ -151,12 +129,7 @@ describe('TransformManager', () => { const generators: Record = { 'slo.apm.transaction_error_rate': new ApmTransactionErrorRateTransformGenerator(), }; - const transformManager = new DefaultTransformManager( - generators, - esClientMock, - loggerMock, - SPACE_ID - ); + const transformManager = new DefaultTransformManager(generators, esClientMock, loggerMock); await transformManager.uninstall('slo-transform-id'); @@ -171,12 +144,7 @@ describe('TransformManager', () => { const generators: Record = { 'slo.apm.transaction_error_rate': new ApmTransactionErrorRateTransformGenerator(), }; - const transformManager = new DefaultTransformManager( - generators, - esClientMock, - loggerMock, - SPACE_ID - ); + const transformManager = new DefaultTransformManager(generators, esClientMock, loggerMock); await transformManager.uninstall('slo-transform-id'); diff --git a/x-pack/plugins/observability/server/services/slo/transform_manager.ts b/x-pack/plugins/observability/server/services/slo/transform_manager.ts index ab7799a4a00c6..154660fccaf9f 100644 --- a/x-pack/plugins/observability/server/services/slo/transform_manager.ts +++ b/x-pack/plugins/observability/server/services/slo/transform_manager.ts @@ -24,8 +24,7 @@ export class DefaultTransformManager implements TransformManager { constructor( private generators: Record, private esClient: ElasticsearchClient, - private logger: Logger, - private spaceId: string + private logger: Logger ) {} async install(slo: SLO): Promise { @@ -35,7 +34,7 @@ export class DefaultTransformManager implements TransformManager { throw new Error(`Unsupported SLO type: ${slo.indicator.type}`); } - const transformParams = generator.getTransformParams(slo, this.spaceId); + const transformParams = generator.getTransformParams(slo); try { await retryTransientEsErrors(() => this.esClient.transform.putTransform(transformParams), { logger: this.logger, From 2d6a45b5ef95502e503f17600076680fac5693a0 Mon Sep 17 00:00:00 2001 From: Xavier Mouligneau Date: Wed, 28 Sep 2022 08:50:25 -0400 Subject: [PATCH 12/17] [RAM] Add Stats on top of execution logs (#140883) * WIP * [CI] Auto-commit changed files from 'node scripts/eslint --no-cache --fix' * add new route for global execution KPI + add filter * revert changes * [CI] Auto-commit changed files from 'node scripts/eslint --no-cache --fix' * update query kpi * add new route for global KPI * Add KPI to log tab and add filtering * Add types * Fix calling API multiple times when refresh, add unit tests * Fix lint and type checks * clean up * remove sorting * add tests * fix global kpi log * fix 141833 * fix query to match with filter * allow access to getRuleExecutionKPI * fix unit test * fix jest tests Co-authored-by: Jiawei Wu Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .../alerting/common/execution_log_types.ts | 13 + .../authorization/alerting_authorization.ts | 1 + .../lib/get_execution_log_aggregation.test.ts | 583 ++++++++++++++++++ .../lib/get_execution_log_aggregation.ts | 199 +++++- .../routes/get_global_execution_kpi.test.ts | 69 +++ .../server/routes/get_global_execution_kpi.ts | 50 ++ .../routes/get_rule_execution_kpi.test.ts | 102 +++ .../server/routes/get_rule_execution_kpi.ts | 56 ++ .../plugins/alerting/server/routes/index.ts | 4 + .../alerting/server/rules_client.mock.ts | 2 + .../server/rules_client/audit_events.ts | 14 + .../server/rules_client/rules_client.ts | 134 ++++ .../server/task_runner/task_runner.ts | 1 - .../alerting.test.ts | 8 + .../feature_privilege_builder/alerting.ts | 9 +- .../application/lib/rule_api/get_filter.ts | 34 + .../public/application/lib/rule_api/index.ts | 4 + .../rule_api/load_action_error_log.test.ts | 2 +- .../lib/rule_api/load_action_error_log.ts | 17 +- .../load_execution_kpi_aggregations.ts | 41 ++ .../load_execution_log_aggregations.ts | 17 +- .../load_global_execution_kpi_aggregations.ts | 38 ++ .../with_bulk_rule_api_operations.tsx | 32 +- .../rule_details/components/rule.test.tsx | 4 +- .../sections/rule_details/components/rule.tsx | 16 +- .../rule_event_log_list_kpi.test.tsx | 183 ++++++ .../components/rule_event_log_list_kpi.tsx | 251 ++++++++ .../components/rule_event_log_list_table.tsx | 19 +- .../alerting/get_global_execution_kpi.ts | 162 +++++ .../tests/alerting/get_rule_execution_kpi.ts | 133 ++++ 30 files changed, 2143 insertions(+), 55 deletions(-) create mode 100644 x-pack/plugins/alerting/server/routes/get_global_execution_kpi.test.ts create mode 100644 x-pack/plugins/alerting/server/routes/get_global_execution_kpi.ts create mode 100644 x-pack/plugins/alerting/server/routes/get_rule_execution_kpi.test.ts create mode 100644 x-pack/plugins/alerting/server/routes/get_rule_execution_kpi.ts create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/get_filter.ts create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/load_execution_kpi_aggregations.ts create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/load_global_execution_kpi_aggregations.ts create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_event_log_list_kpi.test.tsx create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_event_log_list_kpi.tsx create mode 100644 x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/get_global_execution_kpi.ts create mode 100644 x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/get_rule_execution_kpi.ts diff --git a/x-pack/plugins/alerting/common/execution_log_types.ts b/x-pack/plugins/alerting/common/execution_log_types.ts index 1938d8be4acd3..b08a0bf76d69e 100644 --- a/x-pack/plugins/alerting/common/execution_log_types.ts +++ b/x-pack/plugins/alerting/common/execution_log_types.ts @@ -26,6 +26,17 @@ export const actionErrorLogSortableColumns = [ 'event.action', ]; +export const EMPTY_EXECUTION_KPI_RESULT = { + success: 0, + unknown: 0, + failure: 0, + activeAlerts: 0, + newAlerts: 0, + recoveredAlerts: 0, + erroredActions: 0, + triggeredActions: 0, +}; + export type ExecutionLogSortFields = typeof executionLogSortableColumns[number]; export type ActionErrorLogSortFields = typeof actionErrorLogSortableColumns[number]; @@ -68,3 +79,5 @@ export interface IExecutionLogResult { total: number; data: IExecutionLog[]; } + +export type IExecutionKPIResult = typeof EMPTY_EXECUTION_KPI_RESULT; diff --git a/x-pack/plugins/alerting/server/authorization/alerting_authorization.ts b/x-pack/plugins/alerting/server/authorization/alerting_authorization.ts index 0c501f25a857d..7138aabe9d263 100644 --- a/x-pack/plugins/alerting/server/authorization/alerting_authorization.ts +++ b/x-pack/plugins/alerting/server/authorization/alerting_authorization.ts @@ -35,6 +35,7 @@ export enum ReadOperations { Find = 'find', GetAuthorizedAlertsIndices = 'getAuthorizedAlertsIndices', RunSoon = 'runSoon', + GetRuleExecutionKPI = 'getRuleExecutionKPI', } export enum WriteOperations { diff --git a/x-pack/plugins/alerting/server/lib/get_execution_log_aggregation.test.ts b/x-pack/plugins/alerting/server/lib/get_execution_log_aggregation.test.ts index e48483785490a..eb46ae67e2ef1 100644 --- a/x-pack/plugins/alerting/server/lib/get_execution_log_aggregation.test.ts +++ b/x-pack/plugins/alerting/server/lib/get_execution_log_aggregation.test.ts @@ -13,6 +13,8 @@ import { formatSortForBucketSort, formatSortForTermSort, ExecutionUuidAggResult, + getExecutionKPIAggregation, + formatExecutionKPIResult, } from './get_execution_log_aggregation'; describe('formatSortForBucketSort', () => { @@ -1689,3 +1691,584 @@ describe('formatExecutionLogResult', () => { }); }); }); + +describe('getExecutionKPIAggregation', () => { + test('should correctly generate aggregation', () => { + expect(getExecutionKPIAggregation()).toEqual({ + excludeExecuteStart: { + filter: { + bool: { + must_not: [ + { + term: { + 'event.action': 'execute-start', + }, + }, + ], + }, + }, + aggs: { + executionUuid: { + terms: { + field: 'kibana.alert.rule.execution.uuid', + size: 1000, + }, + aggs: { + executionUuidSorted: { + bucket_sort: { + from: 0, + size: 1000, + gap_policy: 'insert_zeros', + }, + }, + actionExecution: { + filter: { + bool: { + must: [ + { + bool: { + must: [ + { + match: { + 'event.action': 'execute', + }, + }, + { + match: { + 'event.provider': 'actions', + }, + }, + ], + }, + }, + ], + }, + }, + aggs: { + actionOutcomes: { + terms: { + field: 'event.outcome', + size: 2, + }, + }, + }, + }, + ruleExecution: { + filter: { + bool: { + must: [ + { + bool: { + must: [ + { + match: { + 'event.action': 'execute', + }, + }, + { + match: { + 'event.provider': 'alerting', + }, + }, + ], + }, + }, + ], + }, + }, + aggs: { + numTriggeredActions: { + sum: { + field: 'kibana.alert.rule.execution.metrics.number_of_triggered_actions', + missing: 0, + }, + }, + numGeneratedActions: { + sum: { + field: 'kibana.alert.rule.execution.metrics.number_of_generated_actions', + missing: 0, + }, + }, + numActiveAlerts: { + sum: { + field: 'kibana.alert.rule.execution.metrics.alert_counts.active', + missing: 0, + }, + }, + numRecoveredAlerts: { + sum: { + field: 'kibana.alert.rule.execution.metrics.alert_counts.recovered', + missing: 0, + }, + }, + numNewAlerts: { + sum: { + field: 'kibana.alert.rule.execution.metrics.alert_counts.new', + missing: 0, + }, + }, + ruleExecutionOutcomes: { + terms: { + field: 'event.outcome', + size: 2, + }, + }, + }, + }, + minExecutionUuidBucket: { + bucket_selector: { + buckets_path: { + count: 'ruleExecution._count', + }, + script: { + source: 'params.count > 0', + }, + }, + }, + }, + }, + }, + }, + }); + }); + + test('should correctly generate aggregation with a defined filter in the form of a string', () => { + expect(getExecutionKPIAggregation('test:test')).toEqual({ + excludeExecuteStart: { + filter: { + bool: { + must_not: [ + { + term: { + 'event.action': 'execute-start', + }, + }, + ], + }, + }, + aggs: { + executionUuid: { + terms: { + field: 'kibana.alert.rule.execution.uuid', + size: 1000, + }, + aggs: { + executionUuidSorted: { + bucket_sort: { + from: 0, + size: 1000, + gap_policy: 'insert_zeros', + }, + }, + actionExecution: { + filter: { + bool: { + must: [ + { + bool: { + must: [ + { + match: { + 'event.action': 'execute', + }, + }, + { + match: { + 'event.provider': 'actions', + }, + }, + ], + }, + }, + ], + }, + }, + aggs: { + actionOutcomes: { + terms: { + field: 'event.outcome', + size: 2, + }, + }, + }, + }, + ruleExecution: { + filter: { + bool: { + filter: { + bool: { + should: [ + { + match: { + test: 'test', + }, + }, + ], + minimum_should_match: 1, + }, + }, + must: [ + { + bool: { + must: [ + { + match: { + 'event.action': 'execute', + }, + }, + { + match: { + 'event.provider': 'alerting', + }, + }, + ], + }, + }, + ], + }, + }, + aggs: { + numTriggeredActions: { + sum: { + field: 'kibana.alert.rule.execution.metrics.number_of_triggered_actions', + missing: 0, + }, + }, + numGeneratedActions: { + sum: { + field: 'kibana.alert.rule.execution.metrics.number_of_generated_actions', + missing: 0, + }, + }, + numActiveAlerts: { + sum: { + field: 'kibana.alert.rule.execution.metrics.alert_counts.active', + missing: 0, + }, + }, + numRecoveredAlerts: { + sum: { + field: 'kibana.alert.rule.execution.metrics.alert_counts.recovered', + missing: 0, + }, + }, + numNewAlerts: { + sum: { + field: 'kibana.alert.rule.execution.metrics.alert_counts.new', + missing: 0, + }, + }, + ruleExecutionOutcomes: { + terms: { + field: 'event.outcome', + size: 2, + }, + }, + }, + }, + minExecutionUuidBucket: { + bucket_selector: { + buckets_path: { + count: 'ruleExecution._count', + }, + script: { + source: 'params.count > 0', + }, + }, + }, + }, + }, + }, + }, + }); + }); + + test('should correctly generate aggregation with a defined filter in the form of a KueryNode', () => { + expect(getExecutionKPIAggregation(fromKueryExpression('test:test'))).toEqual({ + excludeExecuteStart: { + filter: { + bool: { + must_not: [ + { + term: { + 'event.action': 'execute-start', + }, + }, + ], + }, + }, + aggs: { + executionUuid: { + terms: { + field: 'kibana.alert.rule.execution.uuid', + size: 1000, + }, + aggs: { + executionUuidSorted: { + bucket_sort: { + from: 0, + size: 1000, + gap_policy: 'insert_zeros', + }, + }, + actionExecution: { + filter: { + bool: { + must: [ + { + bool: { + must: [ + { + match: { + 'event.action': 'execute', + }, + }, + { + match: { + 'event.provider': 'actions', + }, + }, + ], + }, + }, + ], + }, + }, + aggs: { + actionOutcomes: { + terms: { + field: 'event.outcome', + size: 2, + }, + }, + }, + }, + ruleExecution: { + filter: { + bool: { + filter: { + bool: { + should: [ + { + match: { + test: 'test', + }, + }, + ], + minimum_should_match: 1, + }, + }, + must: [ + { + bool: { + must: [ + { + match: { + 'event.action': 'execute', + }, + }, + { + match: { + 'event.provider': 'alerting', + }, + }, + ], + }, + }, + ], + }, + }, + aggs: { + numTriggeredActions: { + sum: { + field: 'kibana.alert.rule.execution.metrics.number_of_triggered_actions', + missing: 0, + }, + }, + numGeneratedActions: { + sum: { + field: 'kibana.alert.rule.execution.metrics.number_of_generated_actions', + missing: 0, + }, + }, + numActiveAlerts: { + sum: { + field: 'kibana.alert.rule.execution.metrics.alert_counts.active', + missing: 0, + }, + }, + numRecoveredAlerts: { + sum: { + field: 'kibana.alert.rule.execution.metrics.alert_counts.recovered', + missing: 0, + }, + }, + numNewAlerts: { + sum: { + field: 'kibana.alert.rule.execution.metrics.alert_counts.new', + missing: 0, + }, + }, + ruleExecutionOutcomes: { + terms: { + field: 'event.outcome', + size: 2, + }, + }, + }, + }, + minExecutionUuidBucket: { + bucket_selector: { + buckets_path: { + count: 'ruleExecution._count', + }, + script: { + source: 'params.count > 0', + }, + }, + }, + }, + }, + }, + }, + }); + }); +}); + +describe('formatExecutionKPIAggBuckets', () => { + test('should return empty results if aggregations are undefined', () => { + expect( + formatExecutionKPIResult({ + aggregations: undefined, + }) + ).toEqual({ + activeAlerts: 0, + erroredActions: 0, + failure: 0, + newAlerts: 0, + recoveredAlerts: 0, + success: 0, + triggeredActions: 0, + unknown: 0, + }); + }); + + test('should format results correctly', () => { + const results = { + aggregations: { + excludeExecuteStart: { + meta: {}, + doc_count: 875, + executionUuid: { + meta: {}, + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + ruleExecution: { + meta: {}, + doc_count: 3, + numTriggeredActions: { + value: 5.0, + }, + numGeneratedActions: { + value: 5.0, + }, + numActiveAlerts: { + value: 5.0, + }, + numNewAlerts: { + value: 5.0, + }, + numRecoveredAlerts: { + value: 0.0, + }, + ruleExecutionOutcomes: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'success', + doc_count: 3, + }, + ], + }, + }, + actionExecution: { + meta: {}, + doc_count: 5, + actionOutcomes: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'success', + doc_count: 5, + }, + ], + }, + }, + }, + { + ruleExecution: { + meta: {}, + doc_count: 2, + numTriggeredActions: { + value: 5.0, + }, + numGeneratedActions: { + value: 5.0, + }, + numActiveAlerts: { + value: 5.0, + }, + numNewAlerts: { + value: 5.0, + }, + numRecoveredAlerts: { + value: 0.0, + }, + ruleExecutionOutcomes: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'success', + doc_count: 2, + }, + ], + }, + }, + actionExecution: { + meta: {}, + doc_count: 3, + actionOutcomes: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'failure', + doc_count: 3, + }, + ], + }, + }, + }, + ], + }, + }, + }, + }; + + expect(formatExecutionKPIResult(results)).toEqual({ + success: 5, + unknown: 0, + failure: 0, + activeAlerts: 10, + newAlerts: 10, + recoveredAlerts: 0, + erroredActions: 3, + triggeredActions: 10, + }); + }); +}); diff --git a/x-pack/plugins/alerting/server/lib/get_execution_log_aggregation.ts b/x-pack/plugins/alerting/server/lib/get_execution_log_aggregation.ts index 0854488d5f29e..fedc827a46c73 100644 --- a/x-pack/plugins/alerting/server/lib/get_execution_log_aggregation.ts +++ b/x-pack/plugins/alerting/server/lib/get_execution_log_aggregation.ts @@ -12,7 +12,7 @@ import { flatMap, get } from 'lodash'; import { AggregateEventsBySavedObjectResult } from '@kbn/event-log-plugin/server'; import { fromKueryExpression, toElasticsearchQuery } from '@kbn/es-query'; import { parseDuration } from '.'; -import { IExecutionLog, IExecutionLogResult } from '../../common'; +import { IExecutionLog, IExecutionLogResult, EMPTY_EXECUTION_KPI_RESULT } from '../../common'; const DEFAULT_MAX_BUCKETS_LIMIT = 1000; // do not retrieve more than this number of executions @@ -51,6 +51,21 @@ interface IActionExecution buckets: Array<{ key: string; doc_count: number }>; } +interface IExecutionUuidKpiAggBucket extends estypes.AggregationsStringTermsBucketKeys { + actionExecution: { + doc_count: number; + actionOutcomes: IActionExecution; + }; + ruleExecution: { + doc_count: number; + numTriggeredActions: estypes.AggregationsSumAggregate; + numGeneratedActions: estypes.AggregationsSumAggregate; + numActiveAlerts: estypes.AggregationsSumAggregate; + numRecoveredAlerts: estypes.AggregationsSumAggregate; + numNewAlerts: estypes.AggregationsSumAggregate; + ruleExecutionOutcomes: IActionExecution; + }; +} interface IExecutionUuidAggBucket extends estypes.AggregationsStringTermsBucketKeys { timeoutMessage: estypes.AggregationsMultiBucketBase; ruleExecution: { @@ -76,12 +91,22 @@ export interface ExecutionUuidAggResult buckets: TBucket[]; } +export interface ExecutionUuidKPIAggResult + extends estypes.AggregationsAggregateBase { + buckets: TBucket[]; +} + interface ExcludeExecuteStartAggResult extends estypes.AggregationsAggregateBase { executionUuid: ExecutionUuidAggResult; executionUuidCardinality: { executionUuidCardinality: estypes.AggregationsCardinalityAggregate; }; } + +interface ExcludeExecuteStartKpiAggResult extends estypes.AggregationsAggregateBase { + executionUuid: ExecutionUuidKPIAggResult; +} + export interface IExecutionLogAggOptions { filter?: string | KueryNode; page: number; @@ -102,6 +127,115 @@ const ExecutionLogSortFields: Record = { num_new_alerts: 'ruleExecution>numNewAlerts', }; +export const getExecutionKPIAggregation = (filter?: IExecutionLogAggOptions['filter']) => { + const dslFilterQuery: estypes.QueryDslBoolQuery['filter'] = buildDslFilterQuery(filter); + + return { + excludeExecuteStart: { + filter: { + bool: { + must_not: [ + { + term: { + 'event.action': 'execute-start', + }, + }, + ], + }, + }, + aggs: { + executionUuid: { + // Bucket by execution UUID + terms: { + field: EXECUTION_UUID_FIELD, + size: DEFAULT_MAX_BUCKETS_LIMIT, + }, + aggs: { + executionUuidSorted: { + bucket_sort: { + from: 0, + size: 1000, + gap_policy: 'insert_zeros' as estypes.AggregationsGapPolicy, + }, + }, + actionExecution: { + filter: { + bool: { + must: [getProviderAndActionFilter('actions', 'execute')], + }, + }, + aggs: { + actionOutcomes: { + terms: { + field: 'event.outcome', + size: 2, + }, + }, + }, + }, + ruleExecution: { + filter: { + bool: { + ...(dslFilterQuery ? { filter: dslFilterQuery } : {}), + must: [getProviderAndActionFilter('alerting', 'execute')], + }, + }, + aggs: { + numTriggeredActions: { + sum: { + field: 'kibana.alert.rule.execution.metrics.number_of_triggered_actions', + missing: 0, + }, + }, + numGeneratedActions: { + sum: { + field: 'kibana.alert.rule.execution.metrics.number_of_generated_actions', + missing: 0, + }, + }, + numActiveAlerts: { + sum: { + field: 'kibana.alert.rule.execution.metrics.alert_counts.active', + missing: 0, + }, + }, + numRecoveredAlerts: { + sum: { + field: 'kibana.alert.rule.execution.metrics.alert_counts.recovered', + missing: 0, + }, + }, + numNewAlerts: { + sum: { + field: 'kibana.alert.rule.execution.metrics.alert_counts.new', + missing: 0, + }, + }, + ruleExecutionOutcomes: { + terms: { + field: 'event.outcome', + size: 2, + }, + }, + }, + }, + minExecutionUuidBucket: { + bucket_selector: { + buckets_path: { + count: 'ruleExecution._count', + }, + script: { + source: 'params.count > 0', + }, + }, + }, + }, + }, + }, + }, + }; +}; + export function getExecutionLogAggregation({ filter, page, @@ -130,13 +264,7 @@ export function getExecutionLogAggregation({ throw Boom.badRequest(`Invalid perPage field "${perPage}" - must be greater than 0`); } - let dslFilterQuery: estypes.QueryDslBoolQuery['filter']; - try { - const filterKueryNode = typeof filter === 'string' ? fromKueryExpression(filter) : filter; - dslFilterQuery = filter ? toElasticsearchQuery(filterKueryNode) : undefined; - } catch (err) { - throw Boom.badRequest(`Invalid kuery syntax for filter ${filter}`); - } + const dslFilterQuery: estypes.QueryDslBoolQuery['filter'] = buildDslFilterQuery(filter); return { excludeExecuteStart: { @@ -295,6 +423,15 @@ export function getExecutionLogAggregation({ }; } +function buildDslFilterQuery(filter: IExecutionLogAggOptions['filter']) { + try { + const filterKueryNode = typeof filter === 'string' ? fromKueryExpression(filter) : filter; + return filter ? toElasticsearchQuery(filterKueryNode) : undefined; + } catch (err) { + throw Boom.badRequest(`Invalid kuery syntax for filter ${filter}`); + } +} + function getProviderAndActionFilter(provider: string, action: string) { return { bool: { @@ -362,6 +499,52 @@ function formatExecutionLogAggBucket(bucket: IExecutionUuidAggBucket): IExecutio }; } +function formatExecutionKPIAggBuckets(buckets: IExecutionUuidKpiAggBucket[]) { + const objToReturn = { + success: 0, + unknown: 0, + failure: 0, + activeAlerts: 0, + newAlerts: 0, + recoveredAlerts: 0, + erroredActions: 0, + triggeredActions: 0, + }; + + buckets.forEach((bucket) => { + const ruleExecutionOutcomes = bucket?.ruleExecution?.ruleExecutionOutcomes?.buckets ?? []; + const actionExecutionOutcomes = bucket?.actionExecution?.actionOutcomes?.buckets ?? []; + + const ruleExecutionCount = bucket?.ruleExecution?.doc_count ?? 0; + const successRuleExecution = + ruleExecutionOutcomes.find((subBucket) => subBucket?.key === 'success')?.doc_count ?? 0; + const failureRuleExecution = + ruleExecutionOutcomes.find((subBucket) => subBucket?.key === 'failure')?.doc_count ?? 0; + + objToReturn.success += successRuleExecution; + objToReturn.unknown += ruleExecutionCount - (successRuleExecution + failureRuleExecution); + objToReturn.failure += failureRuleExecution; + objToReturn.activeAlerts += bucket?.ruleExecution?.numActiveAlerts.value ?? 0; + objToReturn.newAlerts += bucket?.ruleExecution?.numNewAlerts.value ?? 0; + objToReturn.recoveredAlerts += bucket?.ruleExecution?.numRecoveredAlerts.value ?? 0; + objToReturn.erroredActions += + actionExecutionOutcomes.find((subBucket) => subBucket?.key === 'failure')?.doc_count ?? 0; + objToReturn.triggeredActions += bucket?.ruleExecution?.numTriggeredActions.value ?? 0; + }); + + return objToReturn; +} + +export function formatExecutionKPIResult(results: AggregateEventsBySavedObjectResult) { + const { aggregations } = results; + if (!aggregations || !aggregations.excludeExecuteStart) { + return EMPTY_EXECUTION_KPI_RESULT; + } + const aggs = aggregations.excludeExecuteStart as ExcludeExecuteStartKpiAggResult; + const buckets = aggs.executionUuid.buckets; + return formatExecutionKPIAggBuckets(buckets); +} + export function formatExecutionLogResult( results: AggregateEventsBySavedObjectResult ): IExecutionLogResult { diff --git a/x-pack/plugins/alerting/server/routes/get_global_execution_kpi.test.ts b/x-pack/plugins/alerting/server/routes/get_global_execution_kpi.test.ts new file mode 100644 index 0000000000000..89b21547c892a --- /dev/null +++ b/x-pack/plugins/alerting/server/routes/get_global_execution_kpi.test.ts @@ -0,0 +1,69 @@ +/* + * 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 { httpServiceMock } from '@kbn/core/server/mocks'; +import { licenseStateMock } from '../lib/license_state.mock'; +import { mockHandlerArguments } from './_mock_handler_arguments'; +import { rulesClientMock } from '../rules_client.mock'; +import { getGlobalExecutionKPIRoute } from './get_global_execution_kpi'; + +const rulesClient = rulesClientMock.create(); +jest.mock('../lib/license_api_access', () => ({ + verifyApiAccess: jest.fn(), +})); + +beforeEach(() => { + jest.resetAllMocks(); +}); + +describe('getGlobalExecutionKPIRoute', () => { + const dateString = new Date().toISOString(); + const mockedExecutionLog = { + success: 3, + unknown: 0, + failure: 0, + activeAlerts: 5, + newAlerts: 5, + recoveredAlerts: 0, + erroredActions: 0, + triggeredActions: 5, + }; + + it('gets global execution KPI', async () => { + const licenseState = licenseStateMock.create(); + const router = httpServiceMock.createRouter(); + + getGlobalExecutionKPIRoute(router, licenseState); + + const [config, handler] = router.get.mock.calls[0]; + + expect(config.path).toMatchInlineSnapshot(`"/internal/alerting/_global_execution_kpi"`); + + rulesClient.getGlobalExecutionKpiWithAuth.mockResolvedValue(mockedExecutionLog); + + const [context, req, res] = mockHandlerArguments( + { rulesClient }, + { + query: { + date_start: dateString, + }, + }, + ['ok'] + ); + + await handler(context, req, res); + + expect(rulesClient.getGlobalExecutionKpiWithAuth).toHaveBeenCalledTimes(1); + expect(rulesClient.getGlobalExecutionKpiWithAuth.mock.calls[0]).toEqual([ + { + dateStart: dateString, + }, + ]); + + expect(res.ok).toHaveBeenCalled(); + }); +}); diff --git a/x-pack/plugins/alerting/server/routes/get_global_execution_kpi.ts b/x-pack/plugins/alerting/server/routes/get_global_execution_kpi.ts new file mode 100644 index 0000000000000..29937cc3d8c98 --- /dev/null +++ b/x-pack/plugins/alerting/server/routes/get_global_execution_kpi.ts @@ -0,0 +1,50 @@ +/* + * 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 { IRouter } from '@kbn/core/server'; +import { schema } from '@kbn/config-schema'; +import { AlertingRequestHandlerContext, INTERNAL_BASE_ALERTING_API_PATH } from '../types'; +import { RewriteRequestCase, verifyAccessAndContext } from './lib'; +import { GetGlobalExecutionKPIParams } from '../rules_client'; +import { ILicenseState } from '../lib'; + +const querySchema = schema.object({ + date_start: schema.string(), + date_end: schema.maybe(schema.string()), + filter: schema.maybe(schema.string()), +}); + +const rewriteReq: RewriteRequestCase = ({ + date_start: dateStart, + date_end: dateEnd, + ...rest +}) => ({ + ...rest, + dateStart, + dateEnd, +}); + +export const getGlobalExecutionKPIRoute = ( + router: IRouter, + licenseState: ILicenseState +) => { + router.get( + { + path: `${INTERNAL_BASE_ALERTING_API_PATH}/_global_execution_kpi`, + validate: { + query: querySchema, + }, + }, + router.handleLegacyErrors( + verifyAccessAndContext(licenseState, async function (context, req, res) { + const rulesClient = (await context.alerting).getRulesClient(); + return res.ok({ + body: await rulesClient.getGlobalExecutionKpiWithAuth(rewriteReq(req.query)), + }); + }) + ) + ); +}; diff --git a/x-pack/plugins/alerting/server/routes/get_rule_execution_kpi.test.ts b/x-pack/plugins/alerting/server/routes/get_rule_execution_kpi.test.ts new file mode 100644 index 0000000000000..db5033404788c --- /dev/null +++ b/x-pack/plugins/alerting/server/routes/get_rule_execution_kpi.test.ts @@ -0,0 +1,102 @@ +/* + * 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 { httpServiceMock } from '@kbn/core/server/mocks'; +import { licenseStateMock } from '../lib/license_state.mock'; +import { mockHandlerArguments } from './_mock_handler_arguments'; +import { SavedObjectsErrorHelpers } from '@kbn/core/server'; +import { rulesClientMock } from '../rules_client.mock'; +import { getRuleExecutionKPIRoute } from './get_rule_execution_kpi'; + +const rulesClient = rulesClientMock.create(); +jest.mock('../lib/license_api_access', () => ({ + verifyApiAccess: jest.fn(), +})); + +beforeEach(() => { + jest.resetAllMocks(); +}); + +describe('getRuleExecutionKPIRoute', () => { + const dateString = new Date().toISOString(); + const mockedExecutionLog = { + success: 3, + unknown: 0, + failure: 0, + activeAlerts: 5, + newAlerts: 5, + recoveredAlerts: 0, + erroredActions: 0, + triggeredActions: 5, + }; + + it('gets rule execution KPI', async () => { + const licenseState = licenseStateMock.create(); + const router = httpServiceMock.createRouter(); + + getRuleExecutionKPIRoute(router, licenseState); + + const [config, handler] = router.get.mock.calls[0]; + + expect(config.path).toMatchInlineSnapshot(`"/internal/alerting/rule/{id}/_execution_kpi"`); + + rulesClient.getRuleExecutionKPI.mockResolvedValue(mockedExecutionLog); + + const [context, req, res] = mockHandlerArguments( + { rulesClient }, + { + params: { + id: '1', + }, + query: { + date_start: dateString, + }, + }, + ['ok'] + ); + + await handler(context, req, res); + + expect(rulesClient.getRuleExecutionKPI).toHaveBeenCalledTimes(1); + expect(rulesClient.getRuleExecutionKPI.mock.calls[0]).toEqual([ + { + dateStart: dateString, + id: '1', + }, + ]); + + expect(res.ok).toHaveBeenCalled(); + }); + + it('returns NOT-FOUND when rule is not found', async () => { + const licenseState = licenseStateMock.create(); + const router = httpServiceMock.createRouter(); + + getRuleExecutionKPIRoute(router, licenseState); + + const [, handler] = router.get.mock.calls[0]; + + rulesClient.getRuleExecutionKPI = jest + .fn() + .mockRejectedValueOnce(SavedObjectsErrorHelpers.createGenericNotFoundError('alert', '1')); + + const [context, req, res] = mockHandlerArguments( + { rulesClient }, + { + params: { + id: '1', + }, + query: {}, + }, + ['notFound'] + ); + + expect(handler(context, req, res)).rejects.toMatchInlineSnapshot( + `[Error: Saved object [alert/1] not found]` + ); + }); +}); diff --git a/x-pack/plugins/alerting/server/routes/get_rule_execution_kpi.ts b/x-pack/plugins/alerting/server/routes/get_rule_execution_kpi.ts new file mode 100644 index 0000000000000..11f7085c53290 --- /dev/null +++ b/x-pack/plugins/alerting/server/routes/get_rule_execution_kpi.ts @@ -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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { IRouter } from '@kbn/core/server'; +import { schema } from '@kbn/config-schema'; +import { AlertingRequestHandlerContext, INTERNAL_BASE_ALERTING_API_PATH } from '../types'; +import { RewriteRequestCase, verifyAccessAndContext } from './lib'; +import { GetRuleExecutionKPIParams } from '../rules_client'; +import { ILicenseState } from '../lib'; + +const paramSchema = schema.object({ + id: schema.string(), +}); + +const querySchema = schema.object({ + date_start: schema.string(), + date_end: schema.maybe(schema.string()), + filter: schema.maybe(schema.string()), +}); + +const rewriteReq: RewriteRequestCase = ({ + date_start: dateStart, + date_end: dateEnd, + ...rest +}) => ({ + ...rest, + dateStart, + dateEnd, +}); + +export const getRuleExecutionKPIRoute = ( + router: IRouter, + licenseState: ILicenseState +) => { + router.get( + { + path: `${INTERNAL_BASE_ALERTING_API_PATH}/rule/{id}/_execution_kpi`, + validate: { + params: paramSchema, + query: querySchema, + }, + }, + router.handleLegacyErrors( + verifyAccessAndContext(licenseState, async function (context, req, res) { + const rulesClient = (await context.alerting).getRulesClient(); + const { id } = req.params; + return res.ok({ + body: await rulesClient.getRuleExecutionKPI(rewriteReq({ id, ...req.query })), + }); + }) + ) + ); +}; diff --git a/x-pack/plugins/alerting/server/routes/index.ts b/x-pack/plugins/alerting/server/routes/index.ts index dca2e214d0786..de9e2f112d9e9 100644 --- a/x-pack/plugins/alerting/server/routes/index.ts +++ b/x-pack/plugins/alerting/server/routes/index.ts @@ -22,7 +22,9 @@ import { findRulesRoute, findInternalRulesRoute } from './find_rules'; import { getRuleAlertSummaryRoute } from './get_rule_alert_summary'; import { getRuleExecutionLogRoute } from './get_rule_execution_log'; import { getGlobalExecutionLogRoute } from './get_global_execution_logs'; +import { getGlobalExecutionKPIRoute } from './get_global_execution_kpi'; import { getActionErrorLogRoute } from './get_action_error_log'; +import { getRuleExecutionKPIRoute } from './get_rule_execution_kpi'; import { getRuleStateRoute } from './get_rule_state'; import { healthRoute } from './health'; import { resolveRuleRoute } from './resolve_rule'; @@ -63,6 +65,8 @@ export function defineRoutes(opts: RouteOptions) { getRuleExecutionLogRoute(router, licenseState); getGlobalExecutionLogRoute(router, licenseState); getActionErrorLogRoute(router, licenseState); + getRuleExecutionKPIRoute(router, licenseState); + getGlobalExecutionKPIRoute(router, licenseState); getRuleStateRoute(router, licenseState); healthRoute(router, licenseState, encryptedSavedObjects); ruleTypesRoute(router, licenseState); diff --git a/x-pack/plugins/alerting/server/rules_client.mock.ts b/x-pack/plugins/alerting/server/rules_client.mock.ts index 7333ad59bbb71..2092b98e48c0d 100644 --- a/x-pack/plugins/alerting/server/rules_client.mock.ts +++ b/x-pack/plugins/alerting/server/rules_client.mock.ts @@ -30,6 +30,8 @@ const createRulesClientMock = () => { listAlertTypes: jest.fn(), getAlertSummary: jest.fn(), getExecutionLogForRule: jest.fn(), + getRuleExecutionKPI: jest.fn(), + getGlobalExecutionKpiWithAuth: jest.fn(), getGlobalExecutionLogWithAuth: jest.fn(), getActionErrorLog: jest.fn(), getSpaceId: jest.fn(), diff --git a/x-pack/plugins/alerting/server/rules_client/audit_events.ts b/x-pack/plugins/alerting/server/rules_client/audit_events.ts index c29bc99aa6ed7..30b759c895c46 100644 --- a/x-pack/plugins/alerting/server/rules_client/audit_events.ts +++ b/x-pack/plugins/alerting/server/rules_client/audit_events.ts @@ -26,7 +26,9 @@ export enum RuleAuditAction { BULK_EDIT = 'rule_bulk_edit', GET_EXECUTION_LOG = 'rule_get_execution_log', GET_GLOBAL_EXECUTION_LOG = 'rule_get_global_execution_log', + GET_GLOBAL_EXECUTION_KPI = 'rule_get_global_execution_kpi', GET_ACTION_ERROR_LOG = 'rule_get_action_error_log', + GET_RULE_EXECUTION_KPI = 'rule_get_execution_kpi', SNOOZE = 'rule_snooze', UNSNOOZE = 'rule_unsnooze', RUN_SOON = 'rule_run_soon', @@ -68,6 +70,16 @@ const eventVerbs: Record = { rule_snooze: ['snooze', 'snoozing', 'snoozed'], rule_unsnooze: ['unsnooze', 'unsnoozing', 'unsnoozed'], rule_run_soon: ['run', 'running', 'ran'], + rule_get_execution_kpi: [ + 'access execution KPI for', + 'accessing execution KPI for', + 'accessed execution KPI for', + ], + rule_get_global_execution_kpi: [ + 'access global execution KPI for', + 'accessing global execution KPI for', + 'accessed global execution KPI for', + ], }; const eventTypes: Record = { @@ -92,6 +104,8 @@ const eventTypes: Record = { rule_snooze: 'change', rule_unsnooze: 'change', rule_run_soon: 'access', + rule_get_execution_kpi: 'access', + rule_get_global_execution_kpi: 'access', }; export interface RuleAuditEventParams { diff --git a/x-pack/plugins/alerting/server/rules_client/rules_client.ts b/x-pack/plugins/alerting/server/rules_client/rules_client.ts index c2a643ce8c296..f865a0472465a 100644 --- a/x-pack/plugins/alerting/server/rules_client/rules_client.ts +++ b/x-pack/plugins/alerting/server/rules_client/rules_client.ts @@ -118,7 +118,9 @@ import { import { AlertingRulesConfig } from '../config'; import { formatExecutionLogResult, + formatExecutionKPIResult, getExecutionLogAggregation, + getExecutionKPIAggregation, } from '../lib/get_execution_log_aggregation'; import { IExecutionLogResult, IExecutionErrorsResult } from '../../common'; import { validateSnoozeStartDate } from '../lib/validate_snooze_date'; @@ -381,6 +383,19 @@ export interface GetExecutionLogByIdParams { sort: estypes.Sort; } +export interface GetRuleExecutionKPIParams { + id: string; + dateStart: string; + dateEnd?: string; + filter?: string; +} + +export interface GetGlobalExecutionKPIParams { + dateStart: string; + dateEnd?: string; + filter?: string; +} + export interface GetGlobalExecutionLogParams { dateStart: string; dateEnd?: string; @@ -1039,6 +1054,125 @@ export class RulesClient { } } + public async getGlobalExecutionKpiWithAuth({ + dateStart, + dateEnd, + filter, + }: GetGlobalExecutionKPIParams) { + this.logger.debug(`getGlobalExecutionLogWithAuth(): getting global execution log`); + + let authorizationTuple; + try { + authorizationTuple = await this.authorization.getFindAuthorizationFilter( + AlertingAuthorizationEntity.Alert, + { + type: AlertingAuthorizationFilterType.KQL, + fieldNames: { + ruleTypeId: 'kibana.alert.rule.rule_type_id', + consumer: 'kibana.alert.rule.consumer', + }, + } + ); + } catch (error) { + this.auditLogger?.log( + ruleAuditEvent({ + action: RuleAuditAction.GET_GLOBAL_EXECUTION_KPI, + error, + }) + ); + throw error; + } + + this.auditLogger?.log( + ruleAuditEvent({ + action: RuleAuditAction.GET_GLOBAL_EXECUTION_KPI, + }) + ); + + const dateNow = new Date(); + const parsedDateStart = parseDate(dateStart, 'dateStart', dateNow); + const parsedDateEnd = parseDate(dateEnd, 'dateEnd', dateNow); + + const eventLogClient = await this.getEventLogClient(); + + try { + const aggResult = await eventLogClient.aggregateEventsWithAuthFilter( + 'alert', + authorizationTuple.filter as KueryNode, + { + start: parsedDateStart.toISOString(), + end: parsedDateEnd.toISOString(), + aggs: getExecutionKPIAggregation(filter), + } + ); + + return formatExecutionKPIResult(aggResult); + } catch (err) { + this.logger.debug( + `rulesClient.getGlobalExecutionKpiWithAuth(): error searching global execution KPI: ${err.message}` + ); + throw err; + } + } + + public async getRuleExecutionKPI({ id, dateStart, dateEnd, filter }: GetRuleExecutionKPIParams) { + this.logger.debug(`getRuleExecutionKPI(): getting execution KPI for rule ${id}`); + const rule = (await this.get({ id, includeLegacyId: true })) as SanitizedRuleWithLegacyId; + + try { + // Make sure user has access to this rule + await this.authorization.ensureAuthorized({ + ruleTypeId: rule.alertTypeId, + consumer: rule.consumer, + operation: ReadOperations.GetRuleExecutionKPI, + entity: AlertingAuthorizationEntity.Rule, + }); + } catch (error) { + this.auditLogger?.log( + ruleAuditEvent({ + action: RuleAuditAction.GET_RULE_EXECUTION_KPI, + savedObject: { type: 'alert', id }, + error, + }) + ); + throw error; + } + + this.auditLogger?.log( + ruleAuditEvent({ + action: RuleAuditAction.GET_RULE_EXECUTION_KPI, + savedObject: { type: 'alert', id }, + }) + ); + + // default duration of instance summary is 60 * rule interval + const dateNow = new Date(); + const parsedDateStart = parseDate(dateStart, 'dateStart', dateNow); + const parsedDateEnd = parseDate(dateEnd, 'dateEnd', dateNow); + + const eventLogClient = await this.getEventLogClient(); + + try { + const aggResult = await eventLogClient.aggregateEventsBySavedObjectIds( + 'alert', + [id], + { + start: parsedDateStart.toISOString(), + end: parsedDateEnd.toISOString(), + aggs: getExecutionKPIAggregation(filter), + }, + rule.legacyId !== null ? [rule.legacyId] : undefined + ); + + return formatExecutionKPIResult(aggResult); + } catch (err) { + this.logger.debug( + `rulesClient.getRuleExecutionKPI(): error searching execution KPI for rule ${id}: ${err.message}` + ); + throw err; + } + } + public async find({ options: { fields, ...options } = {}, excludeFromPublicApi = false, diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner.ts b/x-pack/plugins/alerting/server/task_runner/task_runner.ts index 457b2872faa62..41029af567524 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner.ts @@ -420,7 +420,6 @@ export class TaskRunner< checkHasReachedAlertLimit(); this.alertingEventLogger.setExecutionSucceeded(`rule executed: ${ruleLabel}`); - ruleRunMetricsStore.setSearchMetrics([ wrappedScopedClusterClient.getMetrics(), wrappedSearchSourceClient.getMetrics(), diff --git a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.test.ts b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.test.ts index 8272c9220e103..cd18a28e0d373 100644 --- a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.test.ts +++ b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.test.ts @@ -90,6 +90,7 @@ describe(`feature_privilege_builder`, () => { "alerting:1.0.0-zeta1:alert-type/my-feature/rule/getAlertSummary", "alerting:1.0.0-zeta1:alert-type/my-feature/rule/getExecutionLog", "alerting:1.0.0-zeta1:alert-type/my-feature/rule/find", + "alerting:1.0.0-zeta1:alert-type/my-feature/rule/getRuleExecutionKPI", ] `); }); @@ -174,6 +175,7 @@ describe(`feature_privilege_builder`, () => { "alerting:1.0.0-zeta1:alert-type/my-feature/rule/getAlertSummary", "alerting:1.0.0-zeta1:alert-type/my-feature/rule/getExecutionLog", "alerting:1.0.0-zeta1:alert-type/my-feature/rule/find", + "alerting:1.0.0-zeta1:alert-type/my-feature/rule/getRuleExecutionKPI", "alerting:1.0.0-zeta1:alert-type/my-feature/alert/get", "alerting:1.0.0-zeta1:alert-type/my-feature/alert/find", "alerting:1.0.0-zeta1:alert-type/my-feature/alert/getAuthorizedAlertsIndices", @@ -218,6 +220,7 @@ describe(`feature_privilege_builder`, () => { "alerting:1.0.0-zeta1:alert-type/my-feature/rule/getAlertSummary", "alerting:1.0.0-zeta1:alert-type/my-feature/rule/getExecutionLog", "alerting:1.0.0-zeta1:alert-type/my-feature/rule/find", + "alerting:1.0.0-zeta1:alert-type/my-feature/rule/getRuleExecutionKPI", "alerting:1.0.0-zeta1:alert-type/my-feature/rule/create", "alerting:1.0.0-zeta1:alert-type/my-feature/rule/delete", "alerting:1.0.0-zeta1:alert-type/my-feature/rule/update", @@ -316,6 +319,7 @@ describe(`feature_privilege_builder`, () => { "alerting:1.0.0-zeta1:alert-type/my-feature/rule/getAlertSummary", "alerting:1.0.0-zeta1:alert-type/my-feature/rule/getExecutionLog", "alerting:1.0.0-zeta1:alert-type/my-feature/rule/find", + "alerting:1.0.0-zeta1:alert-type/my-feature/rule/getRuleExecutionKPI", "alerting:1.0.0-zeta1:alert-type/my-feature/rule/create", "alerting:1.0.0-zeta1:alert-type/my-feature/rule/delete", "alerting:1.0.0-zeta1:alert-type/my-feature/rule/update", @@ -374,6 +378,7 @@ describe(`feature_privilege_builder`, () => { "alerting:1.0.0-zeta1:alert-type/my-feature/rule/getAlertSummary", "alerting:1.0.0-zeta1:alert-type/my-feature/rule/getExecutionLog", "alerting:1.0.0-zeta1:alert-type/my-feature/rule/find", + "alerting:1.0.0-zeta1:alert-type/my-feature/rule/getRuleExecutionKPI", "alerting:1.0.0-zeta1:alert-type/my-feature/rule/create", "alerting:1.0.0-zeta1:alert-type/my-feature/rule/delete", "alerting:1.0.0-zeta1:alert-type/my-feature/rule/update", @@ -392,6 +397,7 @@ describe(`feature_privilege_builder`, () => { "alerting:1.0.0-zeta1:readonly-alert-type/my-feature/rule/getAlertSummary", "alerting:1.0.0-zeta1:readonly-alert-type/my-feature/rule/getExecutionLog", "alerting:1.0.0-zeta1:readonly-alert-type/my-feature/rule/find", + "alerting:1.0.0-zeta1:readonly-alert-type/my-feature/rule/getRuleExecutionKPI", ] `); }); @@ -480,6 +486,7 @@ describe(`feature_privilege_builder`, () => { "alerting:1.0.0-zeta1:alert-type/my-feature/rule/getAlertSummary", "alerting:1.0.0-zeta1:alert-type/my-feature/rule/getExecutionLog", "alerting:1.0.0-zeta1:alert-type/my-feature/rule/find", + "alerting:1.0.0-zeta1:alert-type/my-feature/rule/getRuleExecutionKPI", "alerting:1.0.0-zeta1:alert-type/my-feature/rule/create", "alerting:1.0.0-zeta1:alert-type/my-feature/rule/delete", "alerting:1.0.0-zeta1:alert-type/my-feature/rule/update", @@ -498,6 +505,7 @@ describe(`feature_privilege_builder`, () => { "alerting:1.0.0-zeta1:readonly-alert-type/my-feature/rule/getAlertSummary", "alerting:1.0.0-zeta1:readonly-alert-type/my-feature/rule/getExecutionLog", "alerting:1.0.0-zeta1:readonly-alert-type/my-feature/rule/find", + "alerting:1.0.0-zeta1:readonly-alert-type/my-feature/rule/getRuleExecutionKPI", "alerting:1.0.0-zeta1:another-alert-type/my-feature/alert/get", "alerting:1.0.0-zeta1:another-alert-type/my-feature/alert/find", "alerting:1.0.0-zeta1:another-alert-type/my-feature/alert/getAuthorizedAlertsIndices", diff --git a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.ts b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.ts index 542dfd1267d4c..a11a4fa77bcdd 100644 --- a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.ts +++ b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.ts @@ -17,7 +17,14 @@ enum AlertingEntity { } const readOperations: Record = { - rule: ['get', 'getRuleState', 'getAlertSummary', 'getExecutionLog', 'find'], + rule: [ + 'get', + 'getRuleState', + 'getAlertSummary', + 'getExecutionLog', + 'find', + 'getRuleExecutionKPI', + ], alert: ['get', 'find', 'getAuthorizedAlertsIndices'], }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/get_filter.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/get_filter.ts new file mode 100644 index 0000000000000..65ace4fa72c7b --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/get_filter.ts @@ -0,0 +1,34 @@ +/* + * 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. + */ + +// TODO (Jiawei): Use node builder instead of strings +export const getFilter = ({ + message, + outcomeFilter, + runId, +}: { + message?: string; + outcomeFilter?: string[]; + runId?: string; +}) => { + const filter: string[] = []; + + if (message) { + const escapedMessage = message.replace(/([\)\(\<\>\}\{\"\:\\])/gm, '\\$&'); + filter.push(`message: "${escapedMessage}" OR error.message: "${escapedMessage}"`); + } + + if (outcomeFilter && outcomeFilter.length) { + filter.push(`event.outcome: ${outcomeFilter.join(' or ')}`); + } + + if (runId) { + filter.push(`kibana.alert.rule.execution.uuid: ${runId}`); + } + + return filter; +}; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/index.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/index.ts index e841506595c04..e23fe787e87c2 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/index.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/index.ts @@ -28,6 +28,10 @@ export { loadExecutionLogAggregations, loadGlobalExecutionLogAggregations, } from './load_execution_log_aggregations'; +export type { LoadExecutionKPIAggregationsProps } from './load_execution_kpi_aggregations'; +export { loadExecutionKPIAggregations } from './load_execution_kpi_aggregations'; +export type { LoadGlobalExecutionKPIAggregationsProps } from './load_global_execution_kpi_aggregations'; +export { loadGlobalExecutionKPIAggregations } from './load_global_execution_kpi_aggregations'; export type { LoadActionErrorLogProps } from './load_action_error_log'; export { loadActionErrorLog } from './load_action_error_log'; export { unmuteAlertInstance } from './unmute_alert'; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/load_action_error_log.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/load_action_error_log.test.ts index b4384713336f7..d06447be31fbc 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/load_action_error_log.test.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/load_action_error_log.test.ts @@ -117,7 +117,7 @@ describe('loadActionErrorLog', () => { "query": Object { "date_end": "2022-03-23T16:17:53.482Z", "date_start": "2022-03-23T16:17:53.482Z", - "filter": "kibana.alert.rule.execution.uuid: 123 and message: \\"test\\" OR error.message: \\"test\\"", + "filter": "message: \\"test\\" OR error.message: \\"test\\" and kibana.alert.rule.execution.uuid: 123", "page": 1, "per_page": 10, "sort": "[{\\"@timestamp\\":{\\"order\\":\\"asc\\"}}]", diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/load_action_error_log.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/load_action_error_log.ts index 4ec1a6949bfe5..10f2879085cd0 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/load_action_error_log.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/load_action_error_log.ts @@ -9,6 +9,7 @@ import { HttpSetup } from '@kbn/core/public'; import type { SortOrder } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { IExecutionErrorsResult, ActionErrorLogSortFields } from '@kbn/alerting-plugin/common'; import { INTERNAL_BASE_ALERTING_API_PATH } from '../../constants'; +import { getFilter } from './get_filter'; export type SortField = Record< ActionErrorLogSortFields, @@ -49,22 +50,6 @@ const getRenamedSort = (sort?: SortField[]) => { }); }; -// TODO (Jiawei): Use node builder instead of strings -const getFilter = ({ runId, message }: { runId?: string; message?: string }) => { - const filter: string[] = []; - - if (runId) { - filter.push(`kibana.alert.rule.execution.uuid: ${runId}`); - } - - if (message) { - const escapedMessage = message.replace(/([\)\(\<\>\}\{\"\:\\])/gm, '\\$&'); - filter.push(`message: "${escapedMessage}" OR error.message: "${escapedMessage}"`); - } - - return filter; -}; - export const loadActionErrorLog = ({ id, http, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/load_execution_kpi_aggregations.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/load_execution_kpi_aggregations.ts new file mode 100644 index 0000000000000..076e1167f444a --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/load_execution_kpi_aggregations.ts @@ -0,0 +1,41 @@ +/* + * 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 { HttpSetup } from '@kbn/core/public'; +import { IExecutionKPIResult } from '@kbn/alerting-plugin/common'; +import { INTERNAL_BASE_ALERTING_API_PATH } from '../../constants'; +import { getFilter } from './get_filter'; + +export interface LoadExecutionKPIAggregationsProps { + id: string; + outcomeFilter?: string[]; + message?: string; + dateStart: string; + dateEnd?: string; +} + +export const loadExecutionKPIAggregations = ({ + id, + http, + outcomeFilter, + message, + dateStart, + dateEnd, +}: LoadExecutionKPIAggregationsProps & { http: HttpSetup }) => { + const filter = getFilter({ outcomeFilter, message }); + + return http.get( + `${INTERNAL_BASE_ALERTING_API_PATH}/rule/${id}/_execution_kpi`, + { + query: { + filter: filter.length ? filter.join(' and ') : undefined, + date_start: dateStart, + date_end: dateEnd, + }, + } + ); +}; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/load_execution_log_aggregations.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/load_execution_log_aggregations.ts index c1f8487d842c5..bf5e529499b42 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/load_execution_log_aggregations.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/load_execution_log_aggregations.ts @@ -16,6 +16,7 @@ import { } from '@kbn/alerting-plugin/common'; import { AsApiContract, RewriteRequestCase } from '@kbn/actions-plugin/common'; import { INTERNAL_BASE_ALERTING_API_PATH } from '../../constants'; +import { getFilter } from './get_filter'; const getRenamedLog = (data: IExecutionLog) => { const { @@ -40,22 +41,6 @@ const rewriteBodyRes: RewriteRequestCase = ({ data, ...rest ...rest, }); -// TODO (Jiawei): Use node builder instead of strings -const getFilter = ({ outcomeFilter, message }: { outcomeFilter?: string[]; message?: string }) => { - const filter: string[] = []; - - if (outcomeFilter && outcomeFilter.length) { - filter.push(`event.outcome: ${outcomeFilter.join(' or ')}`); - } - - if (message) { - const escapedMessage = message.replace(/([\)\(\<\>\}\{\"\:\\])/gm, '\\$&'); - filter.push(`message: "${escapedMessage}" OR error.message: "${escapedMessage}"`); - } - - return filter; -}; - export type SortField = Record< ExecutionLogSortFields, { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/load_global_execution_kpi_aggregations.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/load_global_execution_kpi_aggregations.ts new file mode 100644 index 0000000000000..332e14ad4383f --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/load_global_execution_kpi_aggregations.ts @@ -0,0 +1,38 @@ +/* + * 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 { HttpSetup } from '@kbn/core/public'; +import { IExecutionKPIResult } from '@kbn/alerting-plugin/common'; +import { INTERNAL_BASE_ALERTING_API_PATH } from '../../constants'; +import { getFilter } from './get_filter'; + +export interface LoadGlobalExecutionKPIAggregationsProps { + id: string; + outcomeFilter?: string[]; + message?: string; + dateStart: string; + dateEnd?: string; +} + +export const loadGlobalExecutionKPIAggregations = ({ + id, + http, + outcomeFilter, + message, + dateStart, + dateEnd, +}: LoadGlobalExecutionKPIAggregationsProps & { http: HttpSetup }) => { + const filter = getFilter({ outcomeFilter, message }); + + return http.get(`${INTERNAL_BASE_ALERTING_API_PATH}/_global_execution_kpi`, { + query: { + filter: filter.length ? filter.join(' and ') : undefined, + date_start: dateStart, + date_end: dateEnd, + }, + }); +}; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/with_bulk_rule_api_operations.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/with_bulk_rule_api_operations.tsx index 3150a4cdf407b..fa93ae18ec701 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/with_bulk_rule_api_operations.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/with_bulk_rule_api_operations.tsx @@ -7,7 +7,11 @@ import React from 'react'; -import { IExecutionLogResult, IExecutionErrorsResult } from '@kbn/alerting-plugin/common'; +import { + IExecutionLogResult, + IExecutionErrorsResult, + IExecutionKPIResult, +} from '@kbn/alerting-plugin/common'; import { Rule, RuleType, @@ -46,6 +50,10 @@ import { bulkSnoozeRules, BulkSnoozeRulesProps, unsnoozeRule, + loadExecutionKPIAggregations, + LoadExecutionKPIAggregationsProps, + loadGlobalExecutionKPIAggregations, + LoadGlobalExecutionKPIAggregationsProps, bulkUnsnoozeRules, BulkUnsnoozeRulesProps, } from '../../../lib/rule_api'; @@ -74,12 +82,18 @@ export interface ComponentOpts { loadRuleState: (id: Rule['id']) => Promise; loadRuleSummary: (id: Rule['id'], numberOfExecutions?: number) => Promise; loadRuleTypes: () => Promise; + loadExecutionKPIAggregations: ( + props: LoadExecutionKPIAggregationsProps + ) => Promise; loadExecutionLogAggregations: ( props: LoadExecutionLogAggregationsProps ) => Promise; loadGlobalExecutionLogAggregations: ( props: LoadGlobalExecutionLogAggregationsProps ) => Promise; + loadGlobalExecutionKPIAggregations: ( + props: LoadGlobalExecutionKPIAggregationsProps + ) => Promise; loadActionErrorLog: (props: LoadActionErrorLogProps) => Promise; getHealth: () => Promise; resolveRule: (id: Rule['id']) => Promise; @@ -177,6 +191,22 @@ export function withBulkRuleOperations( http, }) } + loadExecutionKPIAggregations={async ( + loadExecutionKPIAggregationProps: LoadExecutionKPIAggregationsProps + ) => + loadExecutionKPIAggregations({ + ...loadExecutionKPIAggregationProps, + http, + }) + } + loadGlobalExecutionKPIAggregations={async ( + loadGlobalExecutionKPIAggregationsProps: LoadGlobalExecutionKPIAggregationsProps + ) => + loadGlobalExecutionKPIAggregations({ + ...loadGlobalExecutionKPIAggregationsProps, + http, + }) + } resolveRule={async (ruleId: Rule['id']) => resolveRule({ http, ruleId })} getHealth={async () => alertingFrameworkHealth({ http })} snoozeRule={async (rule: Rule, snoozeSchedule: SnoozeSchedule) => { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule.test.tsx index bfe337a1fb880..5eac73c4e87ac 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule.test.tsx @@ -419,8 +419,8 @@ describe('tabbed content', () => { tabbedContent.update(); }); - expect(tabbedContent.find('[aria-labelledby="rule_event_log_list"]').exists()).toBeTruthy(); - expect(tabbedContent.find('[aria-labelledby="rule_alert_list"]').exists()).toBeFalsy(); + expect(tabbedContent.find('[aria-labelledby="rule_event_log_list"]').exists()).toBeFalsy(); + expect(tabbedContent.find('[aria-labelledby="rule_alert_list"]').exists()).toBeTruthy(); tabbedContent.find('[data-test-subj="eventLogListTab"]').simulate('click'); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule.tsx index 5c44b9161b2c2..db82f36cbc90c 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule.tsx @@ -97,6 +97,14 @@ export function RuleComponent({ }; const tabs = [ + { + id: ALERT_LIST_TAB, + name: i18n.translate('xpack.triggersActionsUI.sections.ruleDetails.rule.alertsTabText', { + defaultMessage: 'Alerts', + }), + 'data-test-subj': 'ruleAlertListTab', + content: renderRuleAlertList(), + }, { id: EVENT_LOG_LIST_TAB, name: i18n.translate('xpack.triggersActionsUI.sections.ruleDetails.rule.eventLogTabText', { @@ -118,14 +126,6 @@ export function RuleComponent({ requestRefresh, }), }, - { - id: ALERT_LIST_TAB, - name: i18n.translate('xpack.triggersActionsUI.sections.ruleDetails.rule.alertsTabText', { - defaultMessage: 'Alerts', - }), - 'data-test-subj': 'ruleAlertListTab', - content: renderRuleAlertList(), - }, ]; const renderTabs = () => { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_event_log_list_kpi.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_event_log_list_kpi.test.tsx new file mode 100644 index 0000000000000..f77494e4f3c12 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_event_log_list_kpi.test.tsx @@ -0,0 +1,183 @@ +/* + * 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 { act } from 'react-dom/test-utils'; +import { mountWithIntl, nextTick } from '@kbn/test-jest-helpers'; +import { loadExecutionKPIAggregations } from '../../../lib/rule_api/load_execution_kpi_aggregations'; +import { loadGlobalExecutionKPIAggregations } from '../../../lib/rule_api/load_global_execution_kpi_aggregations'; +import { RuleEventLogListKPI } from './rule_event_log_list_kpi'; + +jest.mock('../../../../common/lib/kibana', () => ({ + useKibana: jest.fn().mockReturnValue({ + services: { + notifications: { toast: { addDanger: jest.fn() } }, + }, + }), +})); + +jest.mock('../../../lib/rule_api/load_execution_kpi_aggregations', () => ({ + loadExecutionKPIAggregations: jest.fn(), +})); + +jest.mock('../../../lib/rule_api/load_global_execution_kpi_aggregations', () => ({ + loadGlobalExecutionKPIAggregations: jest.fn(), +})); + +const mockKpiResponse = { + success: 4, + unknown: 10, + failure: 60, + activeAlerts: 100, + newAlerts: 40, + recoveredAlerts: 30, + erroredActions: 60, + triggeredActions: 140, +}; + +const loadExecutionKPIAggregationsMock = + loadExecutionKPIAggregations as unknown as jest.MockedFunction; +const loadGlobalExecutionKPIAggregationsMock = + loadGlobalExecutionKPIAggregations as unknown as jest.MockedFunction; + +describe('rule_event_log_list_kpi', () => { + beforeEach(() => { + jest.clearAllMocks(); + loadExecutionKPIAggregationsMock.mockResolvedValue(mockKpiResponse); + loadGlobalExecutionKPIAggregationsMock.mockResolvedValue(mockKpiResponse); + }); + + it('renders correctly', async () => { + const wrapper = mountWithIntl( + + ); + + expect(wrapper.find('[data-test-subj="centerJustifiedSpinner"]').exists()).toBeTruthy(); + + // Let the load resolve + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + expect(wrapper.find('[data-test-subj="centerJustifiedSpinner"]').exists()).toBeFalsy(); + + expect(loadExecutionKPIAggregationsMock).toHaveBeenCalledWith( + expect.objectContaining({ + id: '123', + message: undefined, + outcomeFilter: undefined, + }) + ); + + expect(loadGlobalExecutionKPIAggregations).not.toHaveBeenCalled(); + + expect( + wrapper + .find('[data-test-subj="ruleEventLogKpi-successOutcome"] .euiStat__title') + .first() + .text() + ).toEqual(`${mockKpiResponse.success}`); + expect( + wrapper + .find('[data-test-subj="ruleEventLogKpi-unknownOutcome"] .euiStat__title') + .first() + .text() + ).toEqual(`${mockKpiResponse.unknown}`); + expect( + wrapper + .find('[data-test-subj="ruleEventLogKpi-failureOutcome"] .euiStat__title') + .first() + .text() + ).toEqual(`${mockKpiResponse.failure}`); + expect( + wrapper.find('[data-test-subj="ruleEventLogKpi-activeAlerts"] .euiStat__title').first().text() + ).toEqual(`${mockKpiResponse.activeAlerts}`); + expect( + wrapper.find('[data-test-subj="ruleEventLogKpi-newAlerts"] .euiStat__title').first().text() + ).toEqual(`${mockKpiResponse.newAlerts}`); + expect( + wrapper + .find('[data-test-subj="ruleEventLogKpi-recoveredAlerts"] .euiStat__title') + .first() + .text() + ).toEqual(`${mockKpiResponse.recoveredAlerts}`); + expect( + wrapper + .find('[data-test-subj="ruleEventLogKpi-erroredActions"] .euiStat__title') + .first() + .text() + ).toEqual(`${mockKpiResponse.erroredActions}`); + expect( + wrapper + .find('[data-test-subj="ruleEventLogKpi-triggeredActions"] .euiStat__title') + .first() + .text() + ).toEqual(`${mockKpiResponse.triggeredActions}`); + }); + + it('calls global KPI API if provided global rule id', async () => { + const wrapper = mountWithIntl( + + ); + // Let the load resolve + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + expect(loadGlobalExecutionKPIAggregations).toHaveBeenCalledWith( + expect.objectContaining({ + id: '*', + message: undefined, + outcomeFilter: undefined, + }) + ); + + expect(loadExecutionKPIAggregationsMock).not.toHaveBeenCalled(); + }); + + it('calls KPI API with filters', async () => { + const wrapper = mountWithIntl( + + ); + + // Let the load resolve + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + expect(loadExecutionKPIAggregationsMock).toHaveBeenCalledWith( + expect.objectContaining({ + id: '123', + message: 'test', + outcomeFilter: ['status: 123', 'test:456'], + }) + ); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_event_log_list_kpi.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_event_log_list_kpi.tsx new file mode 100644 index 0000000000000..7fe7dd8fdb029 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_event_log_list_kpi.tsx @@ -0,0 +1,251 @@ +/* + * 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, { useEffect, useState, useMemo, useRef } from 'react'; +import { i18n } from '@kbn/i18n'; +import datemath from '@kbn/datemath'; +import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiIconTip, EuiStat, EuiSpacer } from '@elastic/eui'; +import { IExecutionKPIResult } from '@kbn/alerting-plugin/common'; +import { + ComponentOpts as RuleApis, + withBulkRuleOperations, +} from '../../common/components/with_bulk_rule_api_operations'; +import { useKibana } from '../../../../common/lib/kibana'; +import { CenterJustifiedSpinner } from '../../../components/center_justified_spinner'; +import { RuleEventLogListStatus } from './rule_event_log_list_status'; + +const getParsedDate = (date: string) => { + if (date.includes('now')) { + return datemath.parse(date)?.format() || date; + } + return date; +}; + +const API_FAILED_MESSAGE = i18n.translate( + 'xpack.triggersActionsUI.sections.ruleDetails.ruleEventLogListKpi.apiError', + { + defaultMessage: 'Failed to fetch event log KPI.', + } +); + +const RESPONSE_TOOLTIP = i18n.translate( + 'xpack.triggersActionsUI.sections.ruleDetails.ruleEventLogListKpi.responseTooltip', + { + defaultMessage: 'The responses for the latest rule runs.', + } +); + +const ALERTS_TOOLTIP = i18n.translate( + 'xpack.triggersActionsUI.sections.ruleDetails.ruleEventLogListKpi.alertsTooltip', + { + defaultMessage: 'The alert statuses for the latest rule runs.', + } +); + +const ACTIONS_TOOLTIP = i18n.translate( + 'xpack.triggersActionsUI.sections.ruleDetails.ruleEventLogListKpi.actionsTooltip', + { + defaultMessage: 'The action statuses for the latest rule runs.', + } +); + +const Stat = ({ + title, + tooltip, + children, +}: { + title: string; + tooltip: string; + children?: JSX.Element; +}) => { + return ( + + + + {title} + + + + + + + {children} + + ); +}; + +export type RuleEventLogListKPIProps = { + ruleId: string; + dateStart: string; + dateEnd: string; + outcomeFilter?: string[]; + message?: string; + refreshToken?: number; +} & Pick; + +export const RuleEventLogListKPI = (props: RuleEventLogListKPIProps) => { + const { + ruleId, + dateStart, + dateEnd, + outcomeFilter, + message, + refreshToken, + loadExecutionKPIAggregations, + loadGlobalExecutionKPIAggregations, + } = props; + const { + notifications: { toasts }, + } = useKibana().services; + + const isInitialized = useRef(false); + + const [isLoading, setIsLoading] = useState(false); + const [kpi, setKpi] = useState(); + + const loadKPIFn = useMemo(() => { + if (ruleId === '*') { + return loadGlobalExecutionKPIAggregations; + } + return loadExecutionKPIAggregations; + }, [ruleId, loadExecutionKPIAggregations, loadGlobalExecutionKPIAggregations]); + + const loadKPIs = async () => { + setIsLoading(true); + try { + const newKpi = await loadKPIFn({ + id: ruleId, + dateStart: getParsedDate(dateStart), + dateEnd: getParsedDate(dateEnd), + outcomeFilter, + message, + }); + setKpi(newKpi); + } catch (e) { + toasts.addDanger({ + title: API_FAILED_MESSAGE, + text: e.body?.message ?? e, + }); + } + setIsLoading(false); + }; + + useEffect(() => { + loadKPIs(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [ruleId, dateStart, dateEnd, outcomeFilter, message]); + + useEffect(() => { + if (isInitialized.current) { + loadKPIs(); + } + isInitialized.current = true; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [refreshToken]); + + if (isLoading || !kpi) { + return ; + } + + const getStatDescription = (element: React.ReactNode) => { + return ( + <> + {element} + + + ); + }; + + return ( + + + + + + )} + titleSize="s" + title={kpi.success} + /> + + + )} + titleSize="s" + title={kpi.unknown} + /> + + + )} + titleSize="s" + title={kpi.failure} + /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +}; + +export const RuleEventLogListKPIWithApi = withBulkRuleOperations(RuleEventLogListKPI); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_event_log_list_table.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_event_log_list_table.tsx index b647442a8eaf0..2c1d6df71fc3d 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_event_log_list_table.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_event_log_list_table.tsx @@ -30,9 +30,9 @@ import { RuleEventLogListStatusFilter } from './rule_event_log_list_status_filte import { RuleEventLogDataGrid } from './rule_event_log_data_grid'; import { CenterJustifiedSpinner } from '../../../components/center_justified_spinner'; import { RuleActionErrorLogFlyout } from './rule_action_error_log_flyout'; - import { RefineSearchPrompt } from '../refine_search_prompt'; import { LoadExecutionLogAggregationsProps } from '../../../lib/rule_api'; +import { RuleEventLogListKPIWithApi as RuleEventLogListKPI } from './rule_event_log_list_kpi'; import { ComponentOpts as RuleApis, withBulkRuleOperations, @@ -114,6 +114,9 @@ export const RuleEventLogListTable = ( const [search, setSearch] = useState(''); const [isFlyoutOpen, setIsFlyoutOpen] = useState(false); const [selectedRunLog, setSelectedRunLog] = useState(); + const [internalRefreshToken, setInternalRefreshToken] = useState( + refreshToken + ); // Data grid states const [logs, setLogs] = useState(); @@ -243,6 +246,7 @@ export const RuleEventLogListTable = ( ); const onRefresh = () => { + setInternalRefreshToken(Date.now()); loadEventLogs(); }; @@ -339,6 +343,10 @@ export const RuleEventLogListTable = ( localStorage.setItem(localStorageKey, JSON.stringify(visibleColumns)); }, [localStorageKey, visibleColumns]); + useEffect(() => { + setInternalRefreshToken(refreshToken); + }, [refreshToken]); + return ( <> @@ -371,6 +379,15 @@ export const RuleEventLogListTable = ( + + {renderList()} {isOnLastPage && ( { + const objectRemover = new ObjectRemover(supertest); + + after(() => objectRemover.removeAll()); + + it('should return KPI only from the current space', async () => { + const startTime = new Date().toISOString(); + + const spaceId = UserAtSpaceScenarios[1].space.id; + const user = UserAtSpaceScenarios[1].user; + const response = await supertest + .post(`${getUrlPrefix(spaceId)}/api/alerting/rule`) + .set('kbn-xsrf', 'foo') + .send( + getTestRuleData({ + rule_type_id: 'test.noop', + schedule: { interval: '1s' }, + throttle: null, + }) + ); + + expect(response.status).to.eql(200); + const ruleId = response.body.id; + objectRemover.add(spaceId, ruleId, 'rule', 'alerting'); + + const response2 = await supertest + .post(`${getUrlPrefix(spaceId)}/api/alerting/rule`) + .set('kbn-xsrf', 'foo') + .send( + getTestRuleData({ + rule_type_id: 'test.noop', + schedule: { interval: '1s' }, + throttle: null, + }) + ); + + expect(response2.status).to.eql(200); + const ruleId2 = response2.body.id; + objectRemover.add(spaceId, ruleId2, 'rule', 'alerting'); + + await retry.try(async () => { + // break AAD + await supertest + .put(`${getUrlPrefix(spaceId)}/api/alerts_fixture/saved_object/alert/${ruleId2}`) + .set('kbn-xsrf', 'foo') + .send({ + attributes: { + name: 'bar', + }, + }) + .expect(200); + }); + + await retry.try(async () => { + // there can be a successful execute before the error one + const someEvents = await getEventLog({ + getService, + spaceId, + type: 'alert', + id: ruleId2, + provider: 'alerting', + actions: new Map([['execute', { gte: 1 }]]), + }); + const errorEvents = someEvents.filter( + (event) => event?.kibana?.alerting?.status === 'error' + ); + expect(errorEvents.length).to.be.above(0); + }); + + await retry.try(async () => { + // there can be a successful execute before the error one + const logResponse = await supertestWithoutAuth + .get( + `${getUrlPrefix( + spaceId + )}/internal/alerting/_global_execution_logs?date_start=${startTime}&date_end=9999-12-31T23:59:59Z&per_page=50&page=1` + ) + .set('kbn-xsrf', 'foo') + .auth(user.username, user.password); + expect(logResponse.statusCode).to.be(200); + expect(logResponse.body.data.length).to.be.above(1); + }); + + await retry.try(async () => { + // break AAD + await supertest + .put(`${getUrlPrefix(spaceId)}/api/alerts_fixture/saved_object/alert/${ruleId}`) + .set('kbn-xsrf', 'foo') + .send({ + attributes: { + name: 'bar', + }, + }) + .expect(200); + }); + + await retry.try(async () => { + // there can be a successful execute before the error one + const someEvents = await getEventLog({ + getService, + spaceId, + type: 'alert', + id: ruleId, + provider: 'alerting', + actions: new Map([['execute', { gte: 1 }]]), + }); + const errorEvents = someEvents.filter( + (event) => event?.kibana?.alerting?.status === 'error' + ); + expect(errorEvents.length).to.be.above(0); + }); + + const kpiLogs = await retry.try(async () => { + // there can be a successful execute before the error one + const logResponse = await supertestWithoutAuth + .get( + `${getUrlPrefix( + spaceId + )}/internal/alerting/_global_execution_kpi?date_start=${startTime}&date_end=9999-12-31T23:59:59Z` + ) + .set('kbn-xsrf', 'foo') + .auth(user.username, user.password); + expect(logResponse.statusCode).to.be(200); + + return logResponse.body; + }); + + expect(Object.keys(kpiLogs)).to.eql([ + 'success', + 'unknown', + 'failure', + 'activeAlerts', + 'newAlerts', + 'recoveredAlerts', + 'erroredActions', + 'triggeredActions', + ]); + // it should be above 1 since we have two rule running + expect(kpiLogs.success).to.be.above(1); + expect(kpiLogs.failure).to.be.above(0); + }); + }); +} diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/get_rule_execution_kpi.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/get_rule_execution_kpi.ts new file mode 100644 index 0000000000000..2303fc616d8dc --- /dev/null +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/get_rule_execution_kpi.ts @@ -0,0 +1,133 @@ +/* + * 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 expect from '@kbn/expect'; +import { UserAtSpaceScenarios } from '../../../scenarios'; +import { getUrlPrefix, getTestRuleData, ObjectRemover, getEventLog } from '../../../../common/lib'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default function getRuleExecutionKpiTests({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const supertestWithoutAuth = getService('supertestWithoutAuth'); + + const retry = getService('retry'); + + describe('getRuleExecutionKpi', () => { + const objectRemover = new ObjectRemover(supertest); + + after(() => objectRemover.removeAll()); + + it('should return KPI only from the current space', async () => { + const startTime = new Date().toISOString(); + + const spaceId = UserAtSpaceScenarios[1].space.id; + const user = UserAtSpaceScenarios[1].user; + const response = await supertest + .post(`${getUrlPrefix(spaceId)}/api/alerting/rule`) + .set('kbn-xsrf', 'foo') + .send( + getTestRuleData({ + rule_type_id: 'test.noop', + schedule: { interval: '1s' }, + throttle: null, + }) + ); + + expect(response.status).to.eql(200); + const ruleId = response.body.id; + objectRemover.add(spaceId, ruleId, 'rule', 'alerting'); + + const spaceId2 = UserAtSpaceScenarios[4].space.id; + const response2 = await supertest + .post(`${getUrlPrefix(spaceId2)}/api/alerting/rule`) + .set('kbn-xsrf', 'foo') + .send( + getTestRuleData({ + rule_type_id: 'test.noop', + schedule: { interval: '1s' }, + throttle: null, + }) + ); + + expect(response2.status).to.eql(200); + const ruleId2 = response2.body.id; + objectRemover.add(spaceId2, ruleId2, 'rule', 'alerting'); + + await retry.try(async () => { + // there can be a successful execute before the error one + const someEvents = await getEventLog({ + getService, + spaceId, + type: 'alert', + id: ruleId, + provider: 'alerting', + actions: new Map([['execute', { gte: 1 }]]), + }); + + expect(someEvents.length).to.be.above(0); + }); + + await retry.try(async () => { + // break AAD + await supertest + .put(`${getUrlPrefix(spaceId)}/api/alerts_fixture/saved_object/alert/${ruleId}`) + .set('kbn-xsrf', 'foo') + .send({ + attributes: { + name: 'bar', + }, + }) + .expect(200); + }); + + await retry.try(async () => { + // there can be a successful execute before the error one + const someEvents = await getEventLog({ + getService, + spaceId, + type: 'alert', + id: ruleId, + provider: 'alerting', + actions: new Map([['execute', { gte: 1 }]]), + }); + const errorEvents = someEvents.filter( + (event) => event?.kibana?.alerting?.status === 'error' + ); + expect(errorEvents.length).to.be.above(0); + }); + + const kpiLogs = await retry.try(async () => { + // there can be a successful execute before the error one + const logResponse = await supertestWithoutAuth + .get( + `${getUrlPrefix( + spaceId + )}/internal/alerting/rule/${ruleId}/_execution_kpi?date_start=${startTime}&date_end=9999-12-31T23:59:59Z` + ) + .set('kbn-xsrf', 'foo') + .auth(user.username, user.password); + expect(logResponse.statusCode).to.be(200); + + return logResponse.body; + }); + + expect(Object.keys(kpiLogs)).to.eql([ + 'success', + 'unknown', + 'failure', + 'activeAlerts', + 'newAlerts', + 'recoveredAlerts', + 'erroredActions', + 'triggeredActions', + ]); + expect(kpiLogs.success).to.be.above(0); + expect(kpiLogs.failure).to.be.above(0); + }); + }); +} From 254b9a525d5508614d035a22dc690f53ed837a2e Mon Sep 17 00:00:00 2001 From: Stratoula Kalafateli Date: Wed, 28 Sep 2022 15:52:01 +0300 Subject: [PATCH 13/17] [Lens] Stabilize the table functional test (#142038) --- .../test/functional/apps/lens/group1/table.ts | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/x-pack/test/functional/apps/lens/group1/table.ts b/x-pack/test/functional/apps/lens/group1/table.ts index 7bf9b49c53d8d..724efbe6c6c82 100644 --- a/x-pack/test/functional/apps/lens/group1/table.ts +++ b/x-pack/test/functional/apps/lens/group1/table.ts @@ -9,7 +9,7 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { - const PageObjects = getPageObjects(['visualize', 'lens', 'common', 'header']); + const PageObjects = getPageObjects(['visualize', 'lens', 'common']); const listingTable = getService('listingTable'); const find = getService('find'); const retry = getService('retry'); @@ -91,14 +91,14 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('should allow to sort by transposed columns', async () => { await PageObjects.lens.changeTableSortingBy(2, 'ascending'); - await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.lens.waitForVisualization(); expect(await PageObjects.lens.getDatatableCellText(0, 2)).to.eql('17,246'); }); it('should show dynamic coloring feature for numeric columns', async () => { await PageObjects.lens.openDimensionEditor('lnsDatatable_metrics > lns-dimensionTrigger'); await PageObjects.lens.setTableDynamicColoring('text'); - await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.lens.waitForVisualization(); const styleObj = await PageObjects.lens.getDatatableCellStyle(0, 2); expect(styleObj['background-color']).to.be(undefined); expect(styleObj.color).to.be('rgb(133, 189, 177)'); @@ -106,7 +106,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('should allow to color cell background rather than text', async () => { await PageObjects.lens.setTableDynamicColoring('cell'); - await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.lens.waitForVisualization(); const styleObj = await PageObjects.lens.getDatatableCellStyle(0, 2); expect(styleObj['background-color']).to.be('rgb(133, 189, 177)'); // should also set text color when in cell mode @@ -115,9 +115,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('should open the palette panel to customize the palette look', async () => { await PageObjects.lens.openPalettePanel('lnsDatatable'); - await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.lens.waitForVisualization(); await PageObjects.lens.changePaletteTo('temperature'); - await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.lens.waitForVisualization(); const styleObj = await PageObjects.lens.getDatatableCellStyle(0, 2); expect(styleObj['background-color']).to.be('rgb(235, 239, 245)'); }); @@ -125,7 +125,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('should keep the coloring consistent when changing mode', async () => { // Change mode from percent to number await testSubjects.click('lnsPalettePanel_dynamicColoring_rangeType_groups_number'); - await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.lens.waitForVisualization(); // check that all remained the same const styleObj = await PageObjects.lens.getDatatableCellStyle(0, 2); expect(styleObj['background-color']).to.be('rgb(235, 239, 245)'); @@ -133,7 +133,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('should keep the coloring consistent when moving to custom palette from default', async () => { await PageObjects.lens.changePaletteTo('custom'); - await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.lens.waitForVisualization(); // check that all remained the same const styleObj = await PageObjects.lens.getDatatableCellStyle(0, 2); expect(styleObj['background-color']).to.be('rgb(235, 239, 245)'); @@ -149,7 +149,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); // when clicking on another row will trigger a sorting + update await testSubjects.click('lnsPalettePanel_dynamicColoring_range_value_1'); - await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.lens.waitForVisualization(); // pick a cell without color as is below the range const styleObj = await PageObjects.lens.getDatatableCellStyle(3, 3); expect(styleObj['background-color']).to.be(undefined); @@ -159,7 +159,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('should allow the user to reverse the palette', async () => { await testSubjects.click('lnsPalettePanel_dynamicColoring_reverseColors'); - await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.lens.waitForVisualization(); const styleObj = await PageObjects.lens.getDatatableCellStyle(1, 1); expect(styleObj['background-color']).to.be('rgb(168, 191, 218)'); // should also set text color when in cell mode @@ -169,7 +169,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('should allow to show a summary table for metric columns', async () => { await PageObjects.lens.setTableSummaryRowFunction('sum'); - await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.lens.waitForVisualization(); await PageObjects.lens.assertExactText( '[data-test-subj="lnsDataTable-footer-169.228.188.120-›-Average-of-bytes"]', 'Sum: 18,994' From c7301e51f172028bdce87ea626b25c15849fd7eb Mon Sep 17 00:00:00 2001 From: Vitalii Dmyterko <92328789+vitaliidm@users.noreply.github.com> Date: Wed, 28 Sep 2022 13:58:57 +0100 Subject: [PATCH 14/17] [Security Solution][Detections] adds missing bulk edit data view tests (#141915) ## Summary - addresses https://github.com/elastic/kibana/issues/135201 - adds Data View cypress and integration tests according to [Data view Bulk Edit test plan](https://docs.google.com/document/d/116x7ITTTJQ6cTiwaGK831_f6Ox7XB3qyLiHxC3Cmf8w/edit#heading=h.j583i3o7bg2g) (internal document) - integration tests were added earlier in https://github.com/elastic/kibana/pull/138448 --- .../bulk_edit_rules_data_view.cy.ts | 177 ++++++++++++++ .../cypress/screens/rules_bulk_edit.ts | 6 + .../cypress/tasks/api_calls/rules.ts | 228 +++++++++--------- .../cypress/tasks/rule_details.ts | 3 + .../cypress/tasks/rules_bulk_edit.ts | 9 + .../group10/perform_bulk_action.ts | 36 +++ 6 files changed, 344 insertions(+), 115 deletions(-) create mode 100644 x-pack/plugins/security_solution/cypress/e2e/detection_rules/bulk_edit_rules_data_view.cy.ts diff --git a/x-pack/plugins/security_solution/cypress/e2e/detection_rules/bulk_edit_rules_data_view.cy.ts b/x-pack/plugins/security_solution/cypress/e2e/detection_rules/bulk_edit_rules_data_view.cy.ts new file mode 100644 index 0000000000000..766c8d9483f0a --- /dev/null +++ b/x-pack/plugins/security_solution/cypress/e2e/detection_rules/bulk_edit_rules_data_view.cy.ts @@ -0,0 +1,177 @@ +/* + * 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 { + RULES_BULK_EDIT_DATA_VIEWS_WARNING, + RULES_BULK_EDIT_OVERWRITE_DATA_VIEW_CHECKBOX, +} from '../../screens/rules_bulk_edit'; + +import { DATA_VIEW_DETAILS, INDEX_PATTERNS_DETAILS } from '../../screens/rule_details'; + +import { + waitForRulesTableToBeLoaded, + goToRuleDetails, + selectNumberOfRules, +} from '../../tasks/alerts_detection_rules'; + +import { + typeIndexPatterns, + waitForBulkEditActionToFinish, + submitBulkEditForm, + checkOverwriteDataViewCheckbox, + checkOverwriteIndexPatternsCheckbox, + openBulkEditAddIndexPatternsForm, + openBulkEditDeleteIndexPatternsForm, +} from '../../tasks/rules_bulk_edit'; + +import { hasIndexPatterns, getDetails, assertDetailsNotExist } from '../../tasks/rule_details'; +import { login, visitWithoutDateRange } from '../../tasks/login'; + +import { SECURITY_DETECTIONS_RULES_URL } from '../../urls/navigation'; +import { + createCustomRule, + createCustomIndicatorRule, + createEventCorrelationRule, + createThresholdRule, + createNewTermsRule, + createSavedQueryRule, +} from '../../tasks/api_calls/rules'; +import { cleanKibana, deleteAlertsAndRules, postDataView } from '../../tasks/common'; + +import { + getEqlRule, + getNewThreatIndicatorRule, + getNewRule, + getNewThresholdRule, + getNewTermsRule, +} from '../../objects/rule'; + +import { esArchiverResetKibana } from '../../tasks/es_archiver'; + +const DATA_VIEW_ID = 'auditbeat'; + +const expectedIndexPatterns = ['index-1-*', 'index-2-*']; + +const expectedNumberOfCustomRulesToBeEdited = 6; + +const indexDataSource = { dataView: DATA_VIEW_ID, type: 'dataView' } as const; + +const defaultRuleData = { + dataSource: indexDataSource, +}; + +describe('Detection rules, bulk edit, data view', () => { + before(() => { + cleanKibana(); + login(); + }); + beforeEach(() => { + deleteAlertsAndRules(); + esArchiverResetKibana(); + + postDataView(DATA_VIEW_ID); + + createCustomRule({ ...getNewRule(), ...defaultRuleData }, '1'); + createEventCorrelationRule({ ...getEqlRule(), ...defaultRuleData }, '2'); + createCustomIndicatorRule({ ...getNewThreatIndicatorRule(), ...defaultRuleData }, '3'); + createThresholdRule({ ...getNewThresholdRule(), ...defaultRuleData }, '4'); + createNewTermsRule({ ...getNewTermsRule(), ...defaultRuleData }, '5'); + createSavedQueryRule({ ...getNewRule(), ...defaultRuleData, savedId: 'mocked' }, '6'); + + visitWithoutDateRange(SECURITY_DETECTIONS_RULES_URL); + + waitForRulesTableToBeLoaded(); + }); + + it('Add index patterns to custom rules with configured data view', () => { + selectNumberOfRules(expectedNumberOfCustomRulesToBeEdited); + + openBulkEditAddIndexPatternsForm(); + typeIndexPatterns(expectedIndexPatterns); + submitBulkEditForm(); + + waitForBulkEditActionToFinish({ rulesCount: expectedNumberOfCustomRulesToBeEdited }); + + // check if rule still has data view and index patterns field does not exist + goToRuleDetails(); + getDetails(DATA_VIEW_DETAILS).contains(DATA_VIEW_ID); + assertDetailsNotExist(INDEX_PATTERNS_DETAILS); + }); + + it('Add index patterns to custom rules with configured data view when data view checkbox is checked', () => { + selectNumberOfRules(expectedNumberOfCustomRulesToBeEdited); + + openBulkEditAddIndexPatternsForm(); + typeIndexPatterns(expectedIndexPatterns); + + // click on data view overwrite checkbox, ensure warning is displayed + cy.get(RULES_BULK_EDIT_DATA_VIEWS_WARNING).should('not.exist'); + checkOverwriteDataViewCheckbox(); + cy.get(RULES_BULK_EDIT_DATA_VIEWS_WARNING).should('be.visible'); + + submitBulkEditForm(); + + waitForBulkEditActionToFinish({ rulesCount: expectedNumberOfCustomRulesToBeEdited }); + + // check if rule has been updated with index patterns and data view does not exist + goToRuleDetails(); + hasIndexPatterns(expectedIndexPatterns.join('')); + assertDetailsNotExist(DATA_VIEW_DETAILS); + }); + + it('Overwrite index patterns in custom rules with configured data view', () => { + selectNumberOfRules(expectedNumberOfCustomRulesToBeEdited); + + openBulkEditAddIndexPatternsForm(); + typeIndexPatterns(expectedIndexPatterns); + checkOverwriteIndexPatternsCheckbox(); + submitBulkEditForm(); + + waitForBulkEditActionToFinish({ rulesCount: expectedNumberOfCustomRulesToBeEdited }); + + // check if rule still has data view and index patterns field does not exist + goToRuleDetails(); + getDetails(DATA_VIEW_DETAILS).contains(DATA_VIEW_ID); + assertDetailsNotExist(INDEX_PATTERNS_DETAILS); + }); + + it('Overwrite index patterns in custom rules with configured data view when data view checkbox is checked', () => { + selectNumberOfRules(expectedNumberOfCustomRulesToBeEdited); + + openBulkEditAddIndexPatternsForm(); + typeIndexPatterns(expectedIndexPatterns); + checkOverwriteIndexPatternsCheckbox(); + checkOverwriteDataViewCheckbox(); + + submitBulkEditForm(); + + waitForBulkEditActionToFinish({ rulesCount: expectedNumberOfCustomRulesToBeEdited }); + + // check if rule has been overwritten with index patterns and data view does not exist + goToRuleDetails(); + hasIndexPatterns(expectedIndexPatterns.join('')); + assertDetailsNotExist(DATA_VIEW_DETAILS); + }); + + it('Delete index patterns in custom rules with configured data view', () => { + selectNumberOfRules(expectedNumberOfCustomRulesToBeEdited); + + openBulkEditDeleteIndexPatternsForm(); + typeIndexPatterns(expectedIndexPatterns); + + // in delete form data view checkbox is absent + cy.get(RULES_BULK_EDIT_OVERWRITE_DATA_VIEW_CHECKBOX).should('not.exist'); + + submitBulkEditForm(); + + waitForBulkEditActionToFinish({ rulesCount: expectedNumberOfCustomRulesToBeEdited }); + + // check if rule still has data view and index patterns field does not exist + goToRuleDetails(); + getDetails(DATA_VIEW_DETAILS).contains(DATA_VIEW_ID); + }); +}); diff --git a/x-pack/plugins/security_solution/cypress/screens/rules_bulk_edit.ts b/x-pack/plugins/security_solution/cypress/screens/rules_bulk_edit.ts index 34e4f9515b27f..6f4056034a053 100644 --- a/x-pack/plugins/security_solution/cypress/screens/rules_bulk_edit.ts +++ b/x-pack/plugins/security_solution/cypress/screens/rules_bulk_edit.ts @@ -32,6 +32,9 @@ export const RULES_BULK_EDIT_INDEX_PATTERNS = '[data-test-subj="bulkEditRulesInd export const RULES_BULK_EDIT_OVERWRITE_INDEX_PATTERNS_CHECKBOX = '[data-test-subj="bulkEditRulesOverwriteIndexPatterns"]'; +export const RULES_BULK_EDIT_OVERWRITE_DATA_VIEW_CHECKBOX = + '[data-test-subj="bulkEditRulesOverwriteRulesWithDataViews"]'; + export const RULES_BULK_EDIT_TAGS = '[data-test-subj="bulkEditRulesTags"]'; export const RULES_BULK_EDIT_OVERWRITE_TAGS_CHECKBOX = @@ -48,6 +51,9 @@ export const RULES_BULK_EDIT_TIMELINE_TEMPLATES_SELECTOR = export const RULES_BULK_EDIT_TIMELINE_TEMPLATES_WARNING = '[data-test-subj="bulkEditRulesTimelineTemplateWarning"]'; +export const RULES_BULK_EDIT_DATA_VIEWS_WARNING = + '[data-test-subj="bulkEditRulesDataViewsWarning"]'; + export const RULES_BULK_EDIT_SCHEDULES_WARNING = '[data-test-subj="bulkEditRulesSchedulesWarning"]'; export const UPDATE_SCHEDULE_INTERVAL_INPUT = diff --git a/x-pack/plugins/security_solution/cypress/tasks/api_calls/rules.ts b/x-pack/plugins/security_solution/cypress/tasks/api_calls/rules.ts index 4fb076f11f445..80fb77013acbb 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/api_calls/rules.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/api_calls/rules.ts @@ -53,7 +53,8 @@ export const createCustomRule = ( severity: rule.severity.toLocaleLowerCase(), type: 'query', from: 'now-50000h', - index: rule.dataSource.type === 'indexPatterns' ? rule.dataSource.index : '', + index: rule.dataSource.type === 'indexPatterns' ? rule.dataSource.index : undefined, + data_view_id: rule.dataSource.type === 'dataView' ? rule.dataSource.dataView : undefined, query: rule.customQuery, language: 'kuery', enabled: false, @@ -71,83 +72,80 @@ export const createCustomRule = ( }); export const createEventCorrelationRule = (rule: CustomRule, ruleId = 'rule_testing') => { - if (rule.dataSource.type === 'indexPatterns') { - cy.request({ - method: 'POST', - url: 'api/detection_engine/rules', - body: { - rule_id: ruleId, - risk_score: parseInt(rule.riskScore, 10), - description: rule.description, - interval: `${rule.runsEvery.interval}${rule.runsEvery.type}`, - from: `now-${rule.lookBack.interval}${rule.lookBack.type}`, - name: rule.name, - severity: rule.severity.toLocaleLowerCase(), - type: 'eql', - index: rule.dataSource.index, - query: rule.customQuery, - language: 'eql', - enabled: true, - tags: rule.tags, - }, - headers: { 'kbn-xsrf': 'cypress-creds' }, - }); - } + cy.request({ + method: 'POST', + url: 'api/detection_engine/rules', + body: { + rule_id: ruleId, + risk_score: parseInt(rule.riskScore, 10), + description: rule.description, + interval: `${rule.runsEvery.interval}${rule.runsEvery.type}`, + from: `now-${rule.lookBack.interval}${rule.lookBack.type}`, + name: rule.name, + severity: rule.severity.toLocaleLowerCase(), + type: 'eql', + index: rule.dataSource.type === 'indexPatterns' ? rule.dataSource.index : undefined, + data_view_id: rule.dataSource.type === 'dataView' ? rule.dataSource.dataView : undefined, + query: rule.customQuery, + language: 'eql', + enabled: true, + tags: rule.tags, + }, + headers: { 'kbn-xsrf': 'cypress-creds' }, + }); }; export const createThresholdRule = (rule: ThresholdRule, ruleId = 'rule_testing') => { - if (rule.dataSource.type === 'indexPatterns') { - cy.request({ - method: 'POST', - url: 'api/detection_engine/rules', - body: { - rule_id: ruleId, - risk_score: parseInt(rule.riskScore, 10), - description: rule.description, - interval: `${rule.runsEvery.interval}${rule.runsEvery.type}`, - from: `now-${rule.lookBack.interval}${rule.lookBack.type}`, - name: rule.name, - severity: rule.severity.toLocaleLowerCase(), - type: 'threshold', - index: rule.dataSource.index, - query: rule.customQuery, - threshold: { - field: [rule.thresholdField], - value: parseInt(rule.threshold, 10), - cardinality: [], - }, - enabled: true, - tags: rule.tags, + cy.request({ + method: 'POST', + url: 'api/detection_engine/rules', + body: { + rule_id: ruleId, + risk_score: parseInt(rule.riskScore, 10), + description: rule.description, + interval: `${rule.runsEvery.interval}${rule.runsEvery.type}`, + from: `now-${rule.lookBack.interval}${rule.lookBack.type}`, + name: rule.name, + severity: rule.severity.toLocaleLowerCase(), + type: 'threshold', + index: rule.dataSource.type === 'indexPatterns' ? rule.dataSource.index : undefined, + data_view_id: rule.dataSource.type === 'dataView' ? rule.dataSource.dataView : undefined, + query: rule.customQuery, + threshold: { + field: [rule.thresholdField], + value: parseInt(rule.threshold, 10), + cardinality: [], }, - headers: { 'kbn-xsrf': 'cypress-creds' }, - }); - } + enabled: true, + tags: rule.tags, + }, + headers: { 'kbn-xsrf': 'cypress-creds' }, + }); }; export const createNewTermsRule = (rule: NewTermsRule, ruleId = 'rule_testing') => { - if (rule.dataSource.type === 'indexPatterns') { - cy.request({ - method: 'POST', - url: 'api/detection_engine/rules', - body: { - rule_id: ruleId, - risk_score: parseInt(rule.riskScore, 10), - description: rule.description, - interval: `${rule.runsEvery.interval}${rule.runsEvery.type}`, - from: `now-${rule.lookBack.interval}${rule.lookBack.type}`, - name: rule.name, - severity: rule.severity.toLocaleLowerCase(), - type: 'new_terms', - index: rule.dataSource.index, - query: rule.customQuery, - new_terms_fields: rule.newTermsFields, - history_window_start: `now-${rule.historyWindowSize.interval}${rule.historyWindowSize.type}`, - enabled: true, - tags: rule.tags, - }, - headers: { 'kbn-xsrf': 'cypress-creds' }, - }); - } + cy.request({ + method: 'POST', + url: 'api/detection_engine/rules', + body: { + rule_id: ruleId, + risk_score: parseInt(rule.riskScore, 10), + description: rule.description, + interval: `${rule.runsEvery.interval}${rule.runsEvery.type}`, + from: `now-${rule.lookBack.interval}${rule.lookBack.type}`, + name: rule.name, + severity: rule.severity.toLocaleLowerCase(), + type: 'new_terms', + index: rule.dataSource.type === 'indexPatterns' ? rule.dataSource.index : undefined, + data_view_id: rule.dataSource.type === 'dataView' ? rule.dataSource.dataView : undefined, + query: rule.customQuery, + new_terms_fields: rule.newTermsFields, + history_window_start: `now-${rule.historyWindowSize.interval}${rule.historyWindowSize.type}`, + enabled: true, + tags: rule.tags, + }, + headers: { 'kbn-xsrf': 'cypress-creds' }, + }); }; export const createSavedQueryRule = ( @@ -166,7 +164,8 @@ export const createSavedQueryRule = ( severity: rule.severity.toLocaleLowerCase(), type: 'saved_query', from: 'now-50000h', - index: rule.dataSource.type === 'indexPatterns' ? rule.dataSource.index : '', + index: rule.dataSource.type === 'indexPatterns' ? rule.dataSource.index : undefined, + data_view_id: rule.dataSource.type === 'dataView' ? rule.dataSource.dataView : undefined, saved_id: rule.savedId, language: 'kuery', enabled: false, @@ -184,49 +183,48 @@ export const createSavedQueryRule = ( }); export const createCustomIndicatorRule = (rule: ThreatIndicatorRule, ruleId = 'rule_testing') => { - if (rule.dataSource.type === 'indexPatterns') { - cy.request({ - method: 'POST', - url: 'api/detection_engine/rules', - body: { - rule_id: ruleId, - risk_score: parseInt(rule.riskScore, 10), - description: rule.description, - // Default interval is 1m, our tests config overwrite this to 1s - // See https://github.com/elastic/kibana/pull/125396 for details - interval: '10s', - name: rule.name, - severity: rule.severity.toLocaleLowerCase(), - type: 'threat_match', - timeline_id: rule.timeline.templateTimelineId, - timeline_title: rule.timeline.title, - threat_mapping: [ - { - entries: [ - { - field: rule.indicatorMappingField, - type: 'mapping', - value: rule.indicatorIndexField, - }, - ], - }, - ], - threat_query: '*:*', - threat_language: 'kuery', - threat_filters: [], - threat_index: rule.indicatorIndexPattern, - threat_indicator_path: rule.threatIndicatorPath, - from: 'now-50000h', - index: rule.dataSource.index, - query: rule.customQuery || '*:*', - language: 'kuery', - enabled: true, - tags: rule.tags, - }, - headers: { 'kbn-xsrf': 'cypress-creds' }, - failOnStatusCode: false, - }); - } + cy.request({ + method: 'POST', + url: 'api/detection_engine/rules', + body: { + rule_id: ruleId, + risk_score: parseInt(rule.riskScore, 10), + description: rule.description, + // Default interval is 1m, our tests config overwrite this to 1s + // See https://github.com/elastic/kibana/pull/125396 for details + interval: '10s', + name: rule.name, + severity: rule.severity.toLocaleLowerCase(), + type: 'threat_match', + timeline_id: rule.timeline.templateTimelineId, + timeline_title: rule.timeline.title, + threat_mapping: [ + { + entries: [ + { + field: rule.indicatorMappingField, + type: 'mapping', + value: rule.indicatorIndexField, + }, + ], + }, + ], + threat_query: '*:*', + threat_language: 'kuery', + threat_filters: [], + threat_index: rule.indicatorIndexPattern, + threat_indicator_path: rule.threatIndicatorPath, + from: 'now-50000h', + index: rule.dataSource.type === 'indexPatterns' ? rule.dataSource.index : undefined, + data_view_id: rule.dataSource.type === 'dataView' ? rule.dataSource.dataView : undefined, + query: rule.customQuery || '*:*', + language: 'kuery', + enabled: true, + tags: rule.tags, + }, + headers: { 'kbn-xsrf': 'cypress-creds' }, + failOnStatusCode: false, + }); }; export const createCustomRuleEnabled = ( diff --git a/x-pack/plugins/security_solution/cypress/tasks/rule_details.ts b/x-pack/plugins/security_solution/cypress/tasks/rule_details.ts index 75668b49aa207..0bd06911acf7d 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/rule_details.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/rule_details.ts @@ -158,6 +158,9 @@ export const goBackToAllRulesTable = () => { export const getDetails = (title: string | RegExp) => cy.get(DETAILS_TITLE).contains(title).next(DETAILS_DESCRIPTION); +export const assertDetailsNotExist = (title: string | RegExp) => + cy.get(DETAILS_TITLE).contains(title).should('not.exist'); + export const hasIndexPatterns = (indexPatterns: string) => { cy.get(DEFINITION_DETAILS).within(() => { getDetails(INDEX_PATTERNS_DETAILS).should('have.text', indexPatterns); diff --git a/x-pack/plugins/security_solution/cypress/tasks/rules_bulk_edit.ts b/x-pack/plugins/security_solution/cypress/tasks/rules_bulk_edit.ts index 5b3ae403f4a0b..0000a84f26683 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/rules_bulk_edit.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/rules_bulk_edit.ts @@ -30,6 +30,7 @@ import { APPLY_TIMELINE_RULE_BULK_MENU_ITEM, RULES_BULK_EDIT_OVERWRITE_TAGS_CHECKBOX, RULES_BULK_EDIT_OVERWRITE_INDEX_PATTERNS_CHECKBOX, + RULES_BULK_EDIT_OVERWRITE_DATA_VIEW_CHECKBOX, RULES_BULK_EDIT_TIMELINE_TEMPLATES_SELECTOR, UPDATE_SCHEDULE_MENU_ITEM, UPDATE_SCHEDULE_INTERVAL_INPUT, @@ -159,6 +160,14 @@ export const checkOverwriteIndexPatternsCheckbox = () => { .should('be.checked'); }; +export const checkOverwriteDataViewCheckbox = () => { + cy.get(RULES_BULK_EDIT_OVERWRITE_DATA_VIEW_CHECKBOX) + .should('have.text', 'Apply changes to rules configured with data views') + .click() + .get('input') + .should('be.checked'); +}; + export const selectTimelineTemplate = (timelineTitle: string) => { cy.get(RULES_BULK_EDIT_TIMELINE_TEMPLATES_SELECTOR).click(); cy.get(TIMELINE_SEARCHBOX).type(`${timelineTitle}{enter}`).should('not.exist'); diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/perform_bulk_action.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/perform_bulk_action.ts index c17e679b6be31..8f06bc93820a4 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/perform_bulk_action.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/perform_bulk_action.ts @@ -1829,6 +1829,42 @@ export default ({ getService }: FtrProviderContext): void => { expect(setIndexRule.data_view_id).to.eql(undefined); }); + it('should return error when set an empty index pattern to a rule and overwrite the data view when overwrite_data_views is true', async () => { + const dataViewId = 'index1-*'; + const simpleRule = { + ...getSimpleRule(), + index: undefined, + data_view_id: dataViewId, + }; + const rule = await createRule(supertest, log, simpleRule); + + const { body } = await postBulkAction() + .send({ + query: '', + action: BulkAction.edit, + [BulkAction.edit]: [ + { + type: BulkActionEditType.set_index_patterns, + value: [], + overwrite_data_views: true, + }, + ], + }) + .expect(500); + + expect(body.attributes.summary).to.eql({ failed: 1, succeeded: 0, total: 1 }); + expect(body.attributes.errors[0]).to.eql({ + message: "Mutated params invalid: Index patterns can't be empty", + status_code: 500, + rules: [ + { + id: rule.id, + name: rule.name, + }, + ], + }); + }); + it('should NOT set an index pattern to a rule and overwrite the data view when overwrite_data_views is false', async () => { const ruleId = 'ruleId'; const dataViewId = 'index1-*'; From 7cae4515534d8387c67cc8f114573ed241095106 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Wed, 28 Sep 2022 14:25:28 +0100 Subject: [PATCH 15/17] skip flaky suite (#133259) --- x-pack/test/api_integration/apis/osquery/packs.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/api_integration/apis/osquery/packs.ts b/x-pack/test/api_integration/apis/osquery/packs.ts index 9d00249a4e1b0..de490a489cec7 100644 --- a/x-pack/test/api_integration/apis/osquery/packs.ts +++ b/x-pack/test/api_integration/apis/osquery/packs.ts @@ -45,7 +45,8 @@ limit 1000;`; export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertest'); - describe('Packs', () => { + // FLAKY: https://github.com/elastic/kibana/issues/133259 + describe.skip('Packs', () => { let packId: string = ''; let hostedPolicy: Record; let packagePolicyId: string; From 421150a445e5811728620bd0c887188d830cc0be Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Wed, 28 Sep 2022 14:27:39 +0100 Subject: [PATCH 16/17] skip flaky suite (#142083) --- .../apps/ml/data_frame_analytics/outlier_detection_creation.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/functional/apps/ml/data_frame_analytics/outlier_detection_creation.ts b/x-pack/test/functional/apps/ml/data_frame_analytics/outlier_detection_creation.ts index 947cd82cdd342..b3c471f7255c9 100644 --- a/x-pack/test/functional/apps/ml/data_frame_analytics/outlier_detection_creation.ts +++ b/x-pack/test/functional/apps/ml/data_frame_analytics/outlier_detection_creation.ts @@ -13,7 +13,8 @@ export default function ({ getService }: FtrProviderContext) { const ml = getService('ml'); const editedDescription = 'Edited description'; - describe('outlier detection creation', function () { + // FLAKY: https://github.com/elastic/kibana/issues/142083 + describe.skip('outlier detection creation', function () { before(async () => { await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/ihp_outlier'); await ml.testResources.createIndexPatternIfNeeded('ft_ihp_outlier', '@timestamp'); From 6ae4764db6ed54ac8ffc690ac4f1c4c81ad176e1 Mon Sep 17 00:00:00 2001 From: Kaarina Tungseth Date: Wed, 28 Sep 2022 08:46:21 -0500 Subject: [PATCH 17/17] Updates warning for unsupported rolled up data function (#141524) * Updates warning for unsupported rolled up data function * Update the functional test * Fix text on functional test Co-authored-by: Stratoula Kalafateli --- x-pack/plugins/lens/public/indexpattern_datasource/utils.tsx | 2 +- x-pack/test/functional/apps/lens/group2/tsdb.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/utils.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/utils.tsx index 2cb1ba164d572..77a359729a5e0 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/utils.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/utils.tsx @@ -194,7 +194,7 @@ export function getTSDBRollupWarningMessages( ).map((label) => i18n.translate('xpack.lens.indexPattern.tsdbRollupWarning', { defaultMessage: - '"{label}" does not work for all indices in the selected data view because it\'s using a function which is not supported on rolled up data. Please edit the visualization to use another function or change the time range.', + '{label} uses a function that is unsupported by rolled up data. Select a different function or change the time range.', values: { label, }, diff --git a/x-pack/test/functional/apps/lens/group2/tsdb.ts b/x-pack/test/functional/apps/lens/group2/tsdb.ts index 7a43fc47471a5..d19ab9d19db7d 100644 --- a/x-pack/test/functional/apps/lens/group2/tsdb.ts +++ b/x-pack/test/functional/apps/lens/group2/tsdb.ts @@ -119,7 +119,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await testSubjects.click('lns-indexPatternDimension-median'); await PageObjects.lens.waitForVisualization('xyVisChart'); await PageObjects.lens.assertEditorWarning( - '"Median of kubernetes.container.memory.available.bytes" does not work for all indices in the selected data view because it\'s using a function which is not supported on rolled up data. Please edit the visualization to use another function or change the time range.' + 'Median of kubernetes.container.memory.available.bytes uses a function that is unsupported by rolled up data. Select a different function or change the time range.' ); }); it('shows warnings in dashboards as well', async () => { @@ -127,7 +127,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.dashboard.waitForRenderComplete(); await PageObjects.lens.assertInlineWarning( - '"Median of kubernetes.container.memory.available.bytes" does not work for all indices in the selected data view because it\'s using a function which is not supported on rolled up data. Please edit the visualization to use another function or change the time range.' + 'Median of kubernetes.container.memory.available.bytes uses a function that is unsupported by rolled up data. Select a different function or change the time range.' ); }); it('still shows other warnings as toast', async () => {